mbkbucket 1.1.2 → 1.2.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.
@@ -1,1399 +1,6 @@
1
1
  {{!-- File Management System - Bucket Dashboard --}}
2
- <style>
3
- /* Modern Card Styles matching home.handlebars */
4
- .dashboard-card {
5
- background: white;
6
- border-radius: var(--border-radius-lg-ml);
7
- box-shadow: var(--shadow-md-ml);
8
- border: 1px solid var(--gray-light-color-ml);
9
- overflow: hidden;
10
- transition: var(--transition-base-ml);
11
- }
12
-
13
- .dashboard-card:hover {
14
- box-shadow: var(--shadow-lg-ml);
15
- }
16
-
17
- .card-header {
18
- background: linear-gradient(135deg, var(--primary-color-ml) 0%, var(--primary-dark-color-ml) 100%);
19
- color: white;
20
- padding: 1rem;
21
- border-bottom: 1px solid var(--gray-light-color-ml);
22
- }
23
-
24
- .card-title {
25
- font-size: 1.25rem;
26
- font-weight: 700;
27
- margin: 0;
28
- display: flex;
29
- align-items: center;
30
- gap: 0.75rem;
31
- }
32
-
33
- .card-icon {
34
- width: 40px;
35
- height: 40px;
36
- background: rgba(255, 255, 255, 0.2);
37
- border-radius: 50%;
38
- display: flex;
39
- align-items: center;
40
- justify-content: center;
41
- font-size: 1.1rem;
42
- }
43
-
44
- .card-body {
45
- padding: 1rem;
46
- }
47
-
48
- /* File Stats Card - Welcome Style */
49
- .file-stats {
50
- background: linear-gradient(135deg, var(--primary-color-ml) 0%, var(--primary-dark-color-ml) 100%);
51
- color: white;
52
- border: none;
53
- margin-bottom: 2rem;
54
- }
55
-
56
- .stats-content {
57
- display: flex;
58
- align-items: center;
59
- gap: 1rem;
60
- }
61
-
62
- .stats-icon {
63
- width: 60px;
64
- height: 60px;
65
- background: rgba(255, 255, 255, 0.2);
66
- border-radius: 50%;
67
- display: flex;
68
- align-items: center;
69
- justify-content: center;
70
- font-size: 1.5rem;
71
- flex-shrink: 0;
72
- }
73
-
74
- .stats-text h2 {
75
- margin: 0;
76
- font-size: 1.5rem;
77
- font-weight: 700;
78
- }
79
-
80
- .stats-text p {
81
- margin: 0.5rem 0 0;
82
- opacity: 0.9;
83
- font-size: 0.9rem;
84
- }
85
-
86
- /* Form styling - Modern approach */
87
- .form-control {
88
- border: 1px solid var(--gray-light-color-ml);
89
- border-radius: var(--border-radius-r-ml);
90
- padding: 0.75rem 1rem;
91
- font-size: 0.9rem;
92
- transition: var(--transition-fast-ml);
93
- font-family: var(--font-family);
94
- background: white;
95
- }
96
-
97
- .form-control:focus {
98
- border-color: var(--primary-color-ml);
99
- box-shadow: 0 0 0 3px rgba(13, 148, 136, 0.1);
100
- outline: none;
101
- }
102
-
103
- .form-control-lg {
104
- padding: 1rem 1.25rem;
105
- font-size: 1rem;
106
- border-radius: var(--border-radius-lg-ml);
107
- }
108
-
109
- .form-label {
110
- font-weight: 600;
111
- color: var(--dark-color-ml);
112
- margin-bottom: 0.5rem;
113
- font-size: 0.9rem;
114
- display: block;
115
- }
116
-
117
- .form-text {
118
- color: var(--gray-color-ml);
119
- font-size: 0.8rem;
120
- margin-top: 0.25rem;
121
- display: flex;
122
- align-items: center;
123
- gap: 0.5rem;
124
- }
125
-
126
- /* Button styling - Modern hover effects */
127
- .btn {
128
- background: var(--primary-color-ml);
129
- color: white;
130
- border: none;
131
- padding: 0.75rem 1.5rem;
132
- border-radius: var(--border-radius-r-ml);
133
- font-weight: 600;
134
- cursor: pointer;
135
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
136
- text-decoration: none;
137
- display: inline-flex;
138
- align-items: center;
139
- justify-content: center;
140
- gap: 0.5rem;
141
- font-size: 0.9rem;
142
- font-family: var(--font-family);
143
- position: relative;
144
- overflow: hidden;
145
- }
146
-
147
- .btn::before {
148
- content: '';
149
- position: absolute;
150
- top: 50%;
151
- left: 50%;
152
- width: 0;
153
- height: 0;
154
- border-radius: 50%;
155
- background: rgba(255, 255, 255, 0.2);
156
- transform: translate(-50%, -50%);
157
- transition: width 0.6s, height 0.6s;
158
- }
159
-
160
- .btn:hover::before {
161
- width: 300px;
162
- height: 300px;
163
- }
164
-
165
- .btn:hover {
166
- background: var(--primary-dark-color-ml);
167
- box-shadow: 0 4px 12px rgba(13, 148, 136, 0.3);
168
- }
169
-
170
- .btn:active {
171
- transform: translateY(0);
172
- }
173
-
174
- .btn>* {
175
- position: relative;
176
- z-index: 1;
177
- }
178
-
179
- .btn-lg {
180
- padding: 1rem 2rem;
181
- font-size: 1rem;
182
- border-radius: var(--border-radius-lg-ml);
183
- }
184
-
185
- .btn-sm {
186
- padding: 0.5rem 1rem;
187
- font-size: 0.8rem;
188
- }
189
-
190
- .btn-secondary {
191
- background: var(--gray-color-ml);
192
- }
193
-
194
- .btn-secondary:hover {
195
- background: var(--dark-color-ml);
196
- box-shadow: 0 4px 12px rgba(113, 128, 150, 0.3);
197
- }
198
-
199
- .btn-primary {
200
- background: var(--primary-color-ml);
201
- }
202
-
203
- .btn-primary:hover {
204
- background: var(--primary-dark-color-ml);
205
- }
206
-
207
- .btn-outline-secondary {
208
- background: transparent;
209
- color: black;
210
- border: 2px solid black;
211
- }
212
-
213
- .btn-outline-secondary:hover {
214
- background: var(--gray-color-ml);
215
- color: white;
216
- border-color: var(--gray-color-ml);
217
- }
218
-
219
- .btn-success {
220
- background: var(--success-color-ml);
221
- }
222
-
223
- .btn-success:hover {
224
- background: #059669;
225
- box-shadow: 0 4px 12px rgba(22, 163, 74, 0.3);
226
- }
227
-
228
- .btn-danger {
229
- background: var(--danger-color-ml);
230
- }
231
-
232
- .btn-danger:hover {
233
- background: #b91c1c;
234
- box-shadow: 0 4px 12px rgba(220, 38, 38, 0.3);
235
- }
236
-
237
- .w-100 {
238
- width: 100%;
239
- }
240
-
241
- /* Alert styling - Modern gradients */
242
- .alert {
243
- padding: 1rem 1.5rem;
244
- border-radius: var(--border-radius-lg-ml);
245
- border: none;
246
- margin-bottom: 1.5rem;
247
- display: flex;
248
- align-items: center;
249
- gap: 0.75rem;
250
- font-size: 0.9rem;
251
- box-shadow: var(--shadow-md-ml);
252
- animation: slideInDown 0.3s ease;
253
- }
254
-
255
- @keyframes slideInDown {
256
- from {
257
- opacity: 0;
258
- transform: translateY(-20px);
259
- }
260
-
261
- to {
262
- opacity: 1;
263
- transform: translateY(0);
264
- }
265
- }
266
-
267
- .alert-success {
268
- background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
269
- color: #065f46;
270
- border-left: 4px solid var(--success-color-ml);
271
- }
272
-
273
- .alert-danger {
274
- background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
275
- color: #991b1b;
276
- border-left: 4px solid var(--danger-color-ml);
277
- }
278
-
279
- .alert i {
280
- font-size: 1.25rem;
281
- }
282
-
283
- .btn-close {
284
- background: none;
285
- border: none;
286
- font-size: 1.2rem;
287
- cursor: pointer;
288
- margin-left: auto;
289
- opacity: 0.7;
290
- transition: var(--transition-fast-ml);
291
- }
292
-
293
- .btn-close:hover {
294
- opacity: 1;
295
- }
296
-
297
- /* Grid layout - Consistent spacing */
298
- .row {
299
- display: flex;
300
- margin: -0.75rem;
301
- flex-wrap: wrap;
302
- }
303
-
304
- .col-lg-6 {
305
- flex: 0 0 50%;
306
- max-width: 50%;
307
- padding: 0.75rem;
308
- }
309
-
310
- .g-4 {
311
- margin: -1.25rem;
312
- }
313
-
314
- .g-4>* {
315
- padding: 1.25rem;
316
- }
317
-
318
- .mb-4 {
319
- margin-bottom: 1rem;
320
- }
321
-
322
- .h-100 {
323
- height: 100%;
324
- display: flex;
325
- flex-direction: column;
326
- }
327
-
328
- /* Table styling - Modern clean design */
329
- .table-responsive {
330
- overflow-x: auto;
331
- box-shadow: inset 0 0 0 1px var(--gray-light-color-ml);
332
- }
333
-
334
- .table {
335
- width: 100%;
336
- margin-bottom: 0;
337
- background: white;
338
- border-collapse: collapse;
339
- }
340
-
341
- .table thead {
342
- background: linear-gradient(135deg, var(--dark-color-ml) 0%, #2d3748 100%);
343
- color: white;
344
- }
345
-
346
- .table th,
347
- .table td {
348
- padding: 0.5rem 0.75rem;
349
- vertical-align: middle;
350
- border-bottom: 1px solid var(--gray-light-color-ml);
351
- font-size: 0.9rem;
352
- }
353
-
354
- .table th {
355
- font-weight: 600;
356
- text-transform: uppercase;
357
- font-size: 0.75rem;
358
- letter-spacing: 0.05em;
359
- }
360
-
361
- .table tbody tr {
362
- transition: all 0.2s ease;
363
- position: relative;
364
- }
365
-
366
- .table tbody tr:hover {
367
- background: linear-gradient(90deg, rgba(13, 148, 136, 0.03) 0%, transparent 100%);
368
- }
369
-
370
- .file-row {
371
- border-left: 3px solid transparent;
372
- transition: all 0.2s ease;
373
- }
374
-
375
- .file-row:hover {
376
- border-left-color: var(--primary-color-ml);
377
- background: linear-gradient(90deg, rgba(13, 148, 136, 0.05) 0%, transparent 100%);
378
- }
379
-
380
- .folder-row {
381
- border-left: 3px solid transparent;
382
- transition: all 0.2s ease;
383
- cursor: pointer;
384
- }
385
-
386
- .folder-row:hover {
387
- border-left-color: #f59e0b;
388
- background: linear-gradient(90deg, rgba(245, 158, 11, 0.05) 0%, transparent 100%);
389
- }
390
-
391
- .folder-row .file-icon {
392
- background: linear-gradient(135deg, #f59e0b 0%, #fbbf24 50%, rgba(245, 158, 11, 0.15) 100%);
393
- color: #f59e0b;
394
- box-shadow: 0 2px 8px rgba(245, 158, 11, 0.15);
395
- }
396
-
397
- .folder-row:hover .file-icon {
398
- transform: scale(1.1);
399
- box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
400
- }
401
-
402
- .breadcrumb-nav {
403
- padding: 1rem 1.5rem;
404
- background: linear-gradient(135deg, var(--light-color-ml) 0%, #f9fafb 100%);
405
- border-bottom: 1px solid var(--gray-light-color-ml);
406
- display: flex;
407
- align-items: center;
408
- gap: 0.5rem;
409
- flex-wrap: wrap;
410
- }
411
-
412
- .breadcrumb-item {
413
- display: flex;
414
- align-items: center;
415
- gap: 0.5rem;
416
- font-size: 0.9rem;
417
- }
418
-
419
- .breadcrumb-link {
420
- color: var(--primary-color-ml);
421
- cursor: pointer;
422
- transition: all 0.2s ease;
423
- text-decoration: none;
424
- font-weight: 600;
425
- }
426
-
427
- .breadcrumb-link:hover {
428
- color: var(--primary-dark-color-ml);
429
- text-decoration: underline;
430
- }
431
-
432
- .breadcrumb-separator {
433
- color: var(--gray-color-ml);
434
- margin: 0 0.25rem;
435
- }
436
-
437
- .breadcrumb-current {
438
- color: var(--dark-color-ml);
439
- font-weight: 600;
440
- }
441
-
442
- .file-icon {
443
- width: 52px;
444
- height: 52px;
445
- background: linear-gradient(135deg, var(--primary-light-color-ml) 0%, rgba(13, 148, 136, 0.08) 100%);
446
- border-radius: 10px;
447
- display: flex;
448
- align-items: center;
449
- justify-content: center;
450
- margin-right: 0.9rem;
451
- color: var(--primary-color-ml);
452
- font-size: 1.25rem;
453
- flex-shrink: 0;
454
- transition: transform 0.25s ease, box-shadow 0.25s ease;
455
- }
456
-
457
- .file-row:hover .file-icon {
458
- transform: scale(1.1);
459
- box-shadow: 0 2px 8px rgba(13, 148, 136, 0.2);
460
- }
461
-
462
- .file-info {
463
- flex: 1;
464
- min-width: 0;
465
- }
466
-
467
- .file-name {
468
- font-weight: 600;
469
- color: var(--dark-color-ml);
470
- margin-bottom: 0;
471
- font-size: 0.9rem;
472
- white-space: nowrap;
473
- overflow: hidden;
474
- text-overflow: ellipsis;
475
- cursor: help;
476
- overflow-wrap: anywhere;
477
- word-break: break-word;
478
- }
479
-
480
- .folder-path {
481
- color: var(--gray-color-ml);
482
- font-size: 0.85rem;
483
- font-weight: 500;
484
- white-space: nowrap;
485
- overflow: hidden;
486
- text-overflow: ellipsis;
487
- display: block;
488
- max-width: 300px;
489
- cursor: help;
490
- overflow-wrap: anywhere;
491
- word-break: break-word;
492
- }
493
-
494
- .folder-path.clickable {
495
- color: var(--primary-color-ml);
496
- cursor: pointer;
497
- transition: all 0.2s ease;
498
- text-decoration: none;
499
- }
500
-
501
- .folder-path.clickable:hover {
502
- color: var(--primary-dark-color-ml);
503
- text-decoration: underline;
504
- }
505
-
506
- .file-size {
507
- background: linear-gradient(135deg, var(--light-color-ml) 0%, #f9fafb 100%);
508
- color: var(--dark-color-ml);
509
- padding: 0.35rem 0.85rem;
510
- border-radius: var(--border-radius-r-ml);
511
- font-size: 0.8rem;
512
- font-weight: 600;
513
- border: 1px solid var(--gray-light-color-ml);
514
- white-space: nowrap;
515
- }
516
-
517
- .btn-group {
518
- display: flex;
519
- gap: 0.35rem;
520
- }
521
-
522
- .btn-group .btn {
523
- min-width: 42px;
524
- display: flex;
525
- align-items: center;
526
- justify-content: center;
527
- padding: 0.65rem 0.85rem;
528
- }
529
-
530
- .btn-group .btn i {
531
- font-size: 0.95rem;
532
- }
533
-
534
- /* File type specific styling - Enhanced gradients */
535
- .file-icon.image-file {
536
- background: linear-gradient(135deg, #f59e0b 0%, #fbbf24 50%, rgba(245, 158, 11, 0.15) 100%);
537
- color: #f59e0b;
538
- box-shadow: 0 2px 8px rgba(245, 158, 11, 0.15);
539
- }
540
-
541
- .file-icon.video-file {
542
- background: linear-gradient(135deg, #ef4444 0%, #f87171 50%, rgba(239, 68, 68, 0.15) 100%);
543
- color: #ef4444;
544
- box-shadow: 0 2px 8px rgba(239, 68, 68, 0.15);
545
- }
546
-
547
- .file-icon.audio-file {
548
- background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 50%, rgba(139, 92, 246, 0.15) 100%);
549
- color: #8b5cf6;
550
- box-shadow: 0 2px 8px rgba(139, 92, 246, 0.15);
551
- }
552
-
553
- .file-icon.document-file {
554
- background: linear-gradient(135deg, #06b6d4 0%, #22d3ee 50%, rgba(6, 182, 212, 0.15) 100%);
555
- color: #06b6d4;
556
- box-shadow: 0 2px 8px rgba(6, 182, 212, 0.15);
557
- }
558
-
559
- .file-icon.code-file {
560
- background: linear-gradient(135deg, #10b981 0%, #34d399 50%, rgba(16, 185, 129, 0.15) 100%);
561
- color: #10b981;
562
- box-shadow: 0 2px 8px rgba(16, 185, 129, 0.15);
563
- }
564
-
565
- .file-row:hover .file-icon.image-file {
566
- box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
567
- }
568
-
569
- .file-row:hover .file-icon.video-file {
570
- box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
571
- }
572
-
573
- .file-row:hover .file-icon.audio-file {
574
- box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
575
- }
576
-
577
- .file-row:hover .file-icon.document-file {
578
- box-shadow: 0 4px 12px rgba(6, 182, 212, 0.3);
579
- }
580
-
581
- .file-row:hover .file-icon.code-file {
582
- box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
583
- }
584
-
585
- /* Empty state - Modern design */
586
- .empty-state {
587
- text-align: center;
588
- padding: 4rem 2rem;
589
- color: var(--gray-color-ml);
590
- }
591
-
592
- .empty-state i {
593
- font-size: 5rem;
594
- margin-bottom: 1.5rem;
595
- opacity: 0.3;
596
- background: linear-gradient(135deg, var(--primary-color-ml), var(--primary-dark-color-ml));
597
- -webkit-background-clip: text;
598
- -webkit-text-fill-color: transparent;
599
- background-clip: text;
600
- }
601
-
602
- .empty-state h4 {
603
- color: var(--dark-color-ml);
604
- margin-bottom: 0.75rem;
605
- font-weight: 700;
606
- font-size: 1.5rem;
607
- }
608
-
609
- .empty-state p {
610
- margin-bottom: 2rem;
611
- max-width: 400px;
612
- margin-left: auto;
613
- margin-right: auto;
614
- line-height: 1.6;
615
- color: var(--gray-color-ml);
616
- }
617
-
618
- /* Modal styling - Modern design */
619
- .modal {
620
- position: fixed;
621
- top: 0;
622
- left: 0;
623
- width: 100%;
624
- height: 100%;
625
- background: rgba(0, 0, 0, 0.6);
626
- backdrop-filter: blur(4px);
627
- display: none;
628
- align-items: center;
629
- justify-content: center;
630
- z-index: 1000;
631
- animation: fadeIn 0.2s ease;
632
- }
633
-
634
- @keyframes fadeIn {
635
- from {
636
- opacity: 0;
637
- }
638
-
639
- to {
640
- opacity: 1;
641
- }
642
- }
643
-
644
- .modal.show {
645
- display: flex;
646
- }
647
-
648
- .modal-dialog {
649
- max-width: 500px;
650
- width: 90%;
651
- animation: slideUp 0.3s ease;
652
- }
653
-
654
- /* Preview modal sizing variants */
655
- .modal-dialog.preview-compact {
656
- max-width: 700px;
657
- }
658
-
659
- .modal-dialog.preview-large {
660
- max-width: 1100px;
661
- }
662
-
663
- @keyframes slideUp {
664
- from {
665
- opacity: 0;
666
- transform: translateY(20px);
667
- }
668
-
669
- to {
670
- opacity: 1;
671
- transform: translateY(0);
672
- }
673
- }
674
-
675
- .modal-content {
676
- background: white;
677
- border-radius: var(--border-radius-lg-ml);
678
- box-shadow: 0 8px 16px rgba(0, 0, 0, 0.08), 0 4px 8px rgba(0, 0, 0, 0.05);
679
- overflow: hidden;
680
- }
681
-
682
- /* Preview-specific styles */
683
- .preview-header-controls {
684
- display: flex;
685
- gap: 0.5rem;
686
- align-items: center;
687
- }
688
-
689
- #previewModal .preview-content {
690
- width: 100%;
691
- display: flex;
692
- align-items: center;
693
- justify-content: center;
694
- min-height: 120px;
695
- }
696
-
697
- #previewModal img.preview-img {
698
- max-width: 100%;
699
- max-height: 75vh;
700
- display: block;
701
- }
702
-
703
- #previewModal video.preview-video,
704
- #previewModal audio.preview-audio {
705
- width: 100%;
706
- max-height: 75vh;
707
- display: block;
708
- }
709
-
710
- #previewModal iframe.preview-iframe {
711
- width: 100%;
712
- height: 75vh;
713
- border: none;
714
- }
715
-
716
- #previewModal pre {
717
- text-align: left;
718
- max-height: 70vh;
719
- overflow: auto;
720
- white-space: pre-wrap;
721
- padding: 1rem;
722
- border-radius: 6px;
723
- background: #f8fafc;
724
- color: var(--dark-color-ml);
725
- }
726
-
727
- .preview-footer {
728
- display: flex;
729
- gap: 0.5rem;
730
- align-items: center;
731
- justify-content: flex-end;
732
- }
733
-
734
- .preview-actions {
735
- display: flex;
736
- gap: 0.5rem;
737
- align-items: center;
738
- }
739
-
740
- /* Fullscreen state (native fullscreen) */
741
- :fullscreen .modal-dialog,
742
- :-webkit-full-screen .modal-dialog {
743
- width: 100vw;
744
- height: 100vh;
745
- }
746
-
747
- :fullscreen .preview-content,
748
- :-webkit-full-screen .preview-content {
749
- height: 100vh;
750
- }
751
-
752
- .btn-close[aria-label] {
753
- font-weight: 700;
754
- }
755
-
756
-
757
- .modal-header {
758
- background: linear-gradient(135deg, var(--danger-color-ml) 0%, #b91c1c 100%);
759
- color: white;
760
- padding: 0.5rem;
761
- display: flex;
762
- align-items: center;
763
- justify-content: space-between;
764
- }
765
-
766
- .modal-title {
767
- margin: 0;
768
- font-size: 1.25rem;
769
- font-weight: 700;
770
- display: flex;
771
- align-items: center;
772
- gap: 0.75rem;
773
- }
774
-
775
- /* Allow preview filename to wrap and break long words */
776
- #previewModal .modal-title #previewFileName {
777
- display: inline-block;
778
- max-width: calc(100% - 120px);
779
- overflow-wrap: anywhere;
780
- word-break: break-word;
781
- white-space: normal;
782
- }
783
-
784
- .modal-body {
785
- padding: 0.5rem;
786
- text-align: center;
787
- }
788
-
789
- .modal-body i {
790
- color: var(--danger-color-ml);
791
- margin-bottom: 1.25rem;
792
- }
793
-
794
- .modal-body h6 {
795
- color: var(--dark-color-ml);
796
- margin-bottom: 1rem;
797
- font-weight: 700;
798
- font-size: 1.1rem;
799
- }
800
-
801
- .modal-body p {
802
- margin-bottom: 0;
803
- }
804
-
805
- .modal-footer {
806
- padding: 0.5rem;
807
- display: flex;
808
- gap: 1rem;
809
- justify-content: center;
810
- border-top: 1px solid var(--gray-light-color-ml);
811
- background: var(--light-color-ml);
812
- }
813
-
814
- /* Pagination - Modern quick-link style */
815
- .pagination {
816
- display: flex;
817
- justify-content: center;
818
- gap: 0.5rem;
819
- margin: 0;
820
- padding: 0;
821
- }
822
-
823
- .page-item {
824
- list-style: none;
825
- }
826
-
827
- .page-link {
828
- display: flex;
829
- align-items: center;
830
- justify-content: center;
831
- min-width: 42px;
832
- height: 42px;
833
- padding: 0.5rem 1rem;
834
- color: var(--dark-color-ml);
835
- background: white;
836
- border: 1px solid var(--gray-light-color-ml);
837
- border-radius: var(--border-radius-md-ml);
838
- text-decoration: none;
839
- transition: all var(--transition-base-ml);
840
- font-size: 0.95rem;
841
- font-weight: 600;
842
- position: relative;
843
- overflow: hidden;
844
- }
845
-
846
- .page-link::before {
847
- content: '';
848
- position: absolute;
849
- top: 0;
850
- left: -100%;
851
- width: 100%;
852
- height: 100%;
853
- background: linear-gradient(90deg, transparent, rgba(13, 148, 136, 0.1), transparent);
854
- transition: left 0.5s;
855
- }
856
-
857
- .page-link:hover::before {
858
- left: 100%;
859
- }
860
-
861
- .page-link:hover {
862
- background: var(--primary-color-ml);
863
- color: white;
864
- border-color: var(--primary-color-ml);
865
- transform: translateY(-2px);
866
- box-shadow: 0 4px 12px rgba(13, 148, 136, 0.3);
867
- }
868
-
869
- .page-item.active .page-link {
870
- background: linear-gradient(135deg, var(--primary-color-ml), var(--primary-dark-color-ml));
871
- color: white;
872
- border-color: var(--primary-color-ml);
873
- box-shadow: 0 4px 12px rgba(13, 148, 136, 0.3);
874
- }
875
-
876
- .page-item.disabled .page-link {
877
- color: var(--text-muted-color-ml);
878
- background: var(--light-color-ml);
879
- border-color: var(--gray-light-color-ml);
880
- cursor: not-allowed;
881
- opacity: 0.6;
882
- }
883
-
884
- .page-item.disabled .page-link:hover {
885
- transform: none;
886
- box-shadow: none;
887
- }
888
-
889
- /* Upload Progress Styling - Enhanced shimmer effect */
890
- .progress-container {
891
- width: 100%;
892
- height: 10px;
893
- background: var(--gray-light-color-ml);
894
- border-radius: var(--border-radius-r-ml);
895
- overflow: hidden;
896
- position: relative;
897
- box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
898
- }
899
-
900
- .progress-bar {
901
- height: 100%;
902
- background: linear-gradient(135deg, var(--primary-color-ml) 0%, var(--success-color-ml) 100%);
903
- border-radius: var(--border-radius-r-ml);
904
- transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
905
- position: relative;
906
- overflow: hidden;
907
- box-shadow: 0 2px 4px rgba(13, 148, 136, 0.3);
908
- }
909
-
910
- .progress-bar::before {
911
- content: '';
912
- position: absolute;
913
- top: 0;
914
- left: 0;
915
- bottom: 0;
916
- right: 0;
917
- background: linear-gradient(90deg,
918
- transparent,
919
- rgba(255, 255, 255, 0.2),
920
- transparent);
921
- animation: shimmer 2s infinite;
922
- }
923
-
924
- @keyframes shimmer {
925
- 0% {
926
- transform: translateX(-100%);
927
- }
928
-
929
- 100% {
930
- transform: translateX(100%);
931
- }
932
- }
933
-
934
- .upload-label {
935
- font-weight: 600;
936
- color: var(--dark-color-ml);
937
- font-size: 0.9rem;
938
- }
939
-
940
- .upload-percent {
941
- font-weight: 700;
942
- color: var(--primary-color-ml);
943
- font-size: 0.9rem;
944
- }
945
-
946
- .upload-details {
947
- display: flex;
948
- justify-content: space-between;
949
- align-items: center;
950
- }
951
-
952
- /* Drag & Drop and upload queue styles */
953
- .upload-dropzone {
954
- border: 2px dashed var(--gray-light-color-ml);
955
- border-radius: var(--border-radius-r-ml);
956
- padding: 1rem;
957
- background: var(--light-color-ml);
958
- transition: background 0.15s ease, border-color 0.15s ease;
959
- }
960
-
961
- .upload-dropzone.dragover {
962
- background: linear-gradient(90deg, rgba(13, 148, 136, 0.04), transparent);
963
- border-color: var(--primary-color-ml);
964
- }
965
-
966
- .upload-queue .upload-item {
967
- display: flex;
968
- align-items: center;
969
- justify-content: space-between;
970
- gap: 1rem;
971
- padding: 0.5rem 0.75rem;
972
- border-bottom: 1px dashed var(--gray-light-color-ml);
973
- }
974
-
975
- .upload-queue .upload-item .meta {
976
- display: flex;
977
- gap: 0.75rem;
978
- align-items: center;
979
- min-width: 0;
980
- }
981
-
982
- .upload-queue .upload-item .meta .name {
983
- font-weight: 600;
984
- white-space: nowrap;
985
- overflow: hidden;
986
- text-overflow: ellipsis;
987
- max-width: 240px;
988
- }
989
-
990
- .upload-queue .upload-item .controls {
991
- display: flex;
992
- gap: 0.5rem;
993
- align-items: center;
994
- }
995
-
996
- /* Skeleton loaders */
997
- .skeleton-row {
998
- display: flex;
999
- gap: 1rem;
1000
- padding: 1rem;
1001
- }
1002
-
1003
- .skeleton-box {
1004
- background: linear-gradient(90deg, #f5f7fa, #eef2f6);
1005
- border-radius: 6px;
1006
- }
1007
-
1008
- .skeleton-box.s-80 {
1009
- width: 80px;
1010
- height: 16px;
1011
- }
1012
-
1013
- .skeleton-box.s-200 {
1014
- width: 200px;
1015
- height: 16px;
1016
- }
1017
-
1018
- .skeleton-box.s-40 {
1019
- width: 40px;
1020
- height: 16px;
1021
- }
1022
-
1023
- .skeleton-row .file-icon {
1024
- width: 44px;
1025
- height: 44px;
1026
- border-radius: 8px;
1027
- }
1028
-
1029
- .btn:disabled {
1030
- opacity: 0.5;
1031
- cursor: not-allowed;
1032
- transform: none !important;
1033
- pointer-events: none;
1034
- box-shadow: none !important;
1035
- background: var(--gray-light-color-ml) !important;
1036
- color: var(--text-muted-color-ml) !important;
1037
- border-color: var(--gray-light-color-ml) !important;
1038
- }
1039
-
1040
- .btn.uploading {
1041
- background: linear-gradient(135deg, var(--warning-color-ml), #d97706) !important;
1042
- pointer-events: none;
1043
- position: relative;
1044
- overflow: hidden;
1045
- }
1046
-
1047
- .btn.uploading::after {
1048
- content: '';
1049
- position: absolute;
1050
- top: 0;
1051
- left: -100%;
1052
- width: 100%;
1053
- height: 100%;
1054
- background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
1055
- animation: slideRight 1.5s infinite;
1056
- }
1057
-
1058
- @keyframes slideRight {
1059
- 0% {
1060
- left: -100%;
1061
- }
1062
-
1063
- 100% {
1064
- left: 200%;
1065
- }
1066
- }
1067
-
1068
- .btn.uploading:hover {
1069
- background: linear-gradient(135deg, var(--warning-color-ml), #d97706) !important;
1070
- transform: none;
1071
- }
1072
-
1073
- .upload-success {
1074
- color: var(--success-color-ml);
1075
- font-weight: 600;
1076
- }
1077
-
1078
- .upload-error {
1079
- color: var(--danger-color-ml);
1080
- font-weight: 600;
1081
- }
1082
-
1083
- .badge {
1084
- display: inline-block;
1085
- padding: 0.2rem 0.5rem;
1086
- border-radius: 0.35rem;
1087
- font-size: 0.8rem;
1088
- font-weight: 700;
1089
- vertical-align: middle;
1090
- }
1091
-
1092
- .bg-info {
1093
- background: var(--info-color-ml);
1094
- color: white;
1095
- }
1096
-
1097
- /* Responsive design */
1098
- /* Responsive Design - Enhanced breakpoints */
1099
- @media (max-width: 992px) {
1100
- .col-lg-6 {
1101
- flex: 0 0 100%;
1102
- max-width: 100%;
1103
- }
1104
-
1105
- .stats-content {
1106
- flex-direction: column;
1107
- text-align: center;
1108
- }
1109
-
1110
- .stats-icon {
1111
- margin: 0 auto;
1112
- }
1113
-
1114
- .filter-form {
1115
- flex-direction: column;
1116
- }
1117
-
1118
- .filter-form .form-control {
1119
- width: 100%;
1120
- }
1121
- }
1122
-
1123
- @media (max-width: 768px) {
1124
- .card-body {
1125
- padding: 1rem;
1126
- }
1127
-
1128
- .stats-text h2 {
1129
- font-size: 1.35rem;
1130
- }
1131
-
1132
- .btn-group {
1133
- flex-direction: row;
1134
- width: 100%;
1135
- gap: 0.35rem;
1136
- justify-content: flex-start;
1137
- }
1138
-
1139
- .btn-group .btn {
1140
- border-radius: var(--border-radius-r-ml) !important;
1141
- width: auto;
1142
- min-width: 40px;
1143
- padding: 0.5rem 0.65rem;
1144
- }
1145
-
1146
- /* Mobile table improvements */
1147
- .table thead {
1148
- display: none;
1149
- }
1150
-
1151
- .table tbody tr {
1152
- display: block;
1153
- border: 1px solid var(--gray-light-color-ml);
1154
- background: white;
1155
- }
1156
-
1157
- .table tbody tr td {
1158
- display: block;
1159
- padding: 0;
1160
- border: none;
1161
- }
1162
-
1163
- /* File row mobile layout — improved card-like layout for touch */
1164
- /* Hide the leading checkbox column to reduce clutter on small screens */
1165
- .file-row td:first-child {
1166
- display: none !important;
1167
- }
1168
-
1169
- /* Main row: file icon + name + actions */
1170
- .file-row td:nth-child(2) {
1171
- padding: 0.5rem !important;
1172
- display: flex;
1173
- align-items: center;
1174
- gap: 1rem;
1175
- background: transparent;
1176
- }
1177
-
1178
- .file-row td:nth-child(2) .file-icon {
1179
- width: 56px;
1180
- height: 56px;
1181
- font-size: 1.25rem;
1182
- border-radius: 10px;
1183
- }
1184
-
1185
- .file-row td:nth-child(2) .file-info {
1186
- display: block;
1187
- min-width: 0;
1188
- }
1189
-
1190
- .file-row td:nth-child(2) .file-name {
1191
- font-weight: 700;
1192
- font-size: 1rem;
1193
- color: var(--dark-color-ml);
1194
- white-space: normal;
1195
- overflow: hidden;
1196
- text-overflow: ellipsis;
1197
- max-width: calc(100% - 120px);
1198
- }
1199
-
1200
- /* Actions column — ensure buttons are touch friendly */
1201
- .file-row td:nth-child(6),
1202
- .file-row td:nth-child(5) {
1203
- padding: 0.75rem 1rem !important;
1204
- display: flex;
1205
- align-items: center;
1206
- justify-content: flex-end;
1207
- gap: 0.5rem;
1208
- }
1209
-
1210
- .file-row .btn-group {
1211
- justify-content: flex-end;
1212
- }
1213
-
1214
- .file-row .btn-group .btn {
1215
- min-width: 44px;
1216
- height: 44px;
1217
- padding: 0.4rem;
1218
- border-radius: 8px;
1219
- display: inline-flex;
1220
- align-items: center;
1221
- justify-content: center;
1222
- }
1223
-
1224
- /* Metadata row: folder / size / modified shown as compact badges */
1225
- .file-row td:nth-child(3),
1226
- .file-row td:nth-child(4) {
1227
- padding: 0rem 1rem !important;
1228
- display: flex;
1229
- align-items: center;
1230
- color: var(--gray-color-ml);
1231
- font-size: 0.85rem;
1232
- flex-wrap: wrap;
1233
- }
1234
-
1235
- .file-row td:nth-child(3)::before {
1236
- content: "Size:";
1237
- font-weight: 600;
1238
- color: var(--dark-color-ml);
1239
- margin-right: 0.35rem;
1240
- }
1241
-
1242
- .file-row td:nth-child(4)::before {
1243
- content: "Modified:";
1244
- font-weight: 600;
1245
- color: var(--dark-color-ml);
1246
- margin-right: 0.35rem;
1247
- }
1248
-
1249
- /* Ensure clickable feel but keep action buttons interactive */
1250
- .file-row {
1251
- cursor: pointer;
1252
- }
1253
-
1254
- .file-row .btn {
1255
- cursor: auto;
1256
- }
1257
-
1258
- /* Folder row mobile layout — show folder name, hide checkbox and extra columns */
1259
- /* Hide the leading checkbox column and surface the folder name (which is in td:nth-child(2)) */
1260
- .folder-row td:first-child {
1261
- display: none !important;
1262
- /* hide empty checkbox column */
1263
- }
1264
-
1265
- .folder-row td:nth-child(2) {
1266
- display: block !important;
1267
- padding: 1rem !important;
1268
- border-bottom: 1px solid var(--gray-light-color-ml);
1269
- background: var(--light-color-ml);
1270
- border-radius: var(--border-radius-md-ml);
1271
- }
1272
-
1273
- /* Hide the remaining metadata columns on mobile for folder rows */
1274
- .folder-row td:nth-child(3),
1275
- .folder-row td:nth-child(4),
1276
- .folder-row td:nth-child(5) {
1277
- display: none !important;
1278
- }
1279
-
1280
- /* Make folder icon and name more prominent on mobile */
1281
- .folder-row .file-icon {
1282
- width: 48px;
1283
- height: 48px;
1284
- font-size: 1.2rem;
1285
- margin-right: 0.75rem;
1286
- }
1287
-
1288
- .folder-row .file-name {
1289
- font-weight: 700;
1290
- font-size: 1rem;
1291
- }
1292
-
1293
- /* Show file extension on mobile */
1294
- .file-name {
1295
- max-width: 100% !important;
1296
- white-space: normal !important;
1297
- word-break: break-word;
1298
- font-size: 0.9rem;
1299
- }
1300
-
1301
- .file-info {
1302
- flex: 1;
1303
- min-width: 0;
1304
- overflow: visible;
1305
- }
1306
-
1307
- .file-icon {
1308
- width: 48px;
1309
- height: 48px;
1310
- font-size: 1.2rem;
1311
- margin-right: 0.85rem;
1312
- }
1313
-
1314
- .folder-path {
1315
- max-width: 100%;
1316
- white-space: nowrap;
1317
- overflow: hidden;
1318
- text-overflow: ellipsis;
1319
- }
1320
-
1321
- .action-btn {
1322
- padding: 0.45rem 0.85rem;
1323
- font-size: 0.85rem;
1324
- }
1325
-
1326
- .pagination {
1327
- flex-wrap: wrap;
1328
- gap: 0.35rem;
1329
- }
1330
-
1331
- .page-link {
1332
- min-width: 38px;
1333
- height: 38px;
1334
- padding: 0.4rem 0.8rem;
1335
- font-size: 0.85rem;
1336
- }
1337
-
1338
- .breadcrumb-nav {
1339
- padding: 0.85rem 1rem;
1340
- font-size: 0.85rem;
1341
- }
1342
-
1343
- .file-size {
1344
- font-size: 0.75rem;
1345
- padding: 0.3rem 0.7rem;
1346
- }
1347
- }
1348
-
1349
- @media (max-width: 576px) {
1350
- .card-header {
1351
- padding: 1rem;
1352
- }
1353
-
1354
- .card-title {
1355
- font-size: 1.15rem;
1356
- }
1357
-
1358
- .empty-state {
1359
- padding: 2.5rem 1.25rem;
1360
- }
1361
-
1362
- .empty-state i {
1363
- font-size: 4rem;
1364
- }
1365
-
1366
- .modal-dialog {
1367
- width: 95%;
1368
- }
1369
-
1370
- .modal-body {
1371
- padding: 0.5rem;
1372
- }
1373
-
1374
- .table-responsive {
1375
- font-size: 0.8rem;
1376
- }
1377
-
1378
- .file-name {
1379
- max-width: 120px;
1380
- }
1381
-
1382
- .btn {
1383
- padding: 0.6rem 1rem;
1384
- font-size: 0.85rem;
1385
- }
1386
-
1387
- .file-stats .stats-text h2 {
1388
- font-size: 1.25rem;
1389
- }
1390
-
1391
- .file-stats .stats-text p {
1392
- font-size: 0.85rem;
1393
- }
1394
- }
1395
- </style>
1396
-
2
+ <link rel="stylesheet" href="/css/bucketadmin.css?v={{assetVersion}}">
3
+ <div id="bucketAppData" data-app-name="{{mbkautheVar.APP_NAME}}" hidden></div>
1397
4
  <!-- File Statistics -->
1398
5
  <div class="dashboard-card file-stats">
1399
6
  <div class="card-body">
@@ -1531,13 +138,22 @@
1531
138
 
1532
139
  <!-- File List -->
1533
140
  <div class="dashboard-card">
1534
- <div class="card-header">
141
+ <div class="card-header" style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:1rem;">
1535
142
  <h5 class="card-title">
1536
143
  <div>
1537
144
  <i class="fas fa-folder-open"></i>
1538
145
  </div>
1539
146
  <span id="fileListTitle">Files</span>
1540
147
  </h5>
148
+
149
+ <div class="btn-group" role="group" aria-label="View Mode">
150
+ <button type="button" class="btn btn-sm btn-light view-toggle-btn active" id="viewBtnFolder" onclick="changeViewMode('folder')" title="Folder View" style="border-top-right-radius: 0; border-bottom-right-radius: 0;">
151
+ <i class="fas fa-folder"></i> Folders
152
+ </button>
153
+ <button type="button" class="btn btn-sm btn-outline-light view-toggle-btn" id="viewBtnFlat" onclick="changeViewMode('flat')" title="Flat List (All Files)" style="border-top-left-radius: 0; border-bottom-left-radius: 0;">
154
+ <i class="fas fa-list-ul"></i> Flat View
155
+ </button>
156
+ </div>
1541
157
  </div>
1542
158
  <div class="card-body" style="padding: 0;">
1543
159
  <div id="fileListContainer">
@@ -1634,1282 +250,4 @@
1634
250
  </div>
1635
251
  </div>
1636
252
 
1637
- <script>
1638
- // Global state
1639
- let currentPage = 1;
1640
- let currentPrefix = '';
1641
- let currentSearch = '';
1642
- // Bucket is configured centrally on the server; client should not select a bucket.
1643
-
1644
- // Show alert message
1645
- function showAlert(message, type = 'success') {
1646
- const alertId = type === 'success' ? 'successAlert' : 'errorAlert';
1647
- const messageId = type === 'success' ? 'successMessage' : 'errorMessage';
1648
-
1649
- const alertEl = document.getElementById(alertId);
1650
- const messageEl = document.getElementById(messageId);
1651
-
1652
- if (alertEl && messageEl) {
1653
- messageEl.textContent = message;
1654
- alertEl.style.display = 'flex';
1655
-
1656
- // Auto dismiss after 5 seconds
1657
- setTimeout(() => {
1658
- alertEl.style.opacity = '0';
1659
- alertEl.style.transform = 'translateY(-10px)';
1660
- setTimeout(() => {
1661
- alertEl.style.display = 'none';
1662
- alertEl.style.opacity = '1';
1663
- alertEl.style.transform = 'translateY(0)';
1664
- }, 300);
1665
- }, 5000);
1666
- }
1667
- }
1668
-
1669
- // Fetch and render files
1670
- async function loadFiles(page = 1, prefix = '', search = '') {
1671
- currentPage = page;
1672
- currentPrefix = prefix;
1673
- currentSearch = search;
1674
-
1675
- // Update browser URL so navigation is shareable/bookmarkable (folder shown as readable path)
1676
- try {
1677
- const parts = [];
1678
- if (prefix) {
1679
- // preserve slashes in folder param for readability: encode then restore slashes
1680
- const folderEncoded = encodeURIComponent(prefix).replace(/%2F/g, '/');
1681
- parts.push(`folder=${folderEncoded}`);
1682
- }
1683
- if (page && page > 1) parts.push(`page=${page}`);
1684
- if (search) parts.push(`search=${encodeURIComponent(search)}`);
1685
- const newUrl = '/mbkbucket' + (parts.length ? '?' + parts.join('&') : '');
1686
- window.history.replaceState(null, '', newUrl);
1687
- } catch (e) {
1688
- // Ignore history errors in older browsers
1689
- console.warn('history update failed', e);
1690
- }
1691
-
1692
- const container = document.getElementById('fileListContainer');
1693
- const paginationContainer = document.getElementById('paginationContainer');
1694
- const fileListTitle = document.getElementById('fileListTitle');
1695
- const totalFilesCount = document.getElementById('totalFilesCount');
1696
- const filterInfo = document.getElementById('filterInfo');
1697
-
1698
- // Show loading skeleton state
1699
- container.innerHTML = `
1700
- <div class="table-responsive">
1701
- <div class="skeleton-row">
1702
- <div class="skeleton-box s-40"></div>
1703
- <div class="skeleton-box s-200"></div>
1704
- <div class="skeleton-box s-80"></div>
1705
- <div class="skeleton-box s-80"></div>
1706
- </div>
1707
- <div class="skeleton-row">
1708
- <div class="skeleton-box s-40"></div>
1709
- <div class="skeleton-box s-200"></div>
1710
- <div class="skeleton-box s-80"></div>
1711
- <div class="skeleton-box s-80"></div>
1712
- </div>
1713
- <div class="skeleton-row">
1714
- <div class="skeleton-box s-40"></div>
1715
- <div class="skeleton-box s-200"></div>
1716
- <div class="skeleton-box s-80"></div>
1717
- <div class="skeleton-box s-80"></div>
1718
- </div>
1719
- </div>
1720
- `;
1721
-
1722
- try {
1723
- const searchParam = search ? `&search=${encodeURIComponent(search)}` : '';
1724
- const response = await fetch(`/mbkbucket/api/files?page=${page}&prefix=${encodeURIComponent(prefix)}${searchParam}`);
1725
- const data = await response.json();
1726
-
1727
- if (!data.success) {
1728
- throw new Error(data.error || 'Failed to load files');
1729
- }
1730
-
1731
- // Update stats
1732
- totalFilesCount.textContent = `${data.totalFiles} ${search ? 'Results' : 'Files'}`;
1733
-
1734
- if (search) {
1735
- filterInfo.textContent = `Search results for: "${search}"`;
1736
- fileListTitle.innerHTML = `Search Results <span class="text-muted">for: </span><span class="badge bg-info">${search}</span>`;
1737
- } else if (prefix) {
1738
- filterInfo.textContent = `Filtered by folder: ${prefix}`;
1739
- fileListTitle.innerHTML = `Files <span class="text-muted">in: </span><span class="badge bg-info">${prefix}</span>`;
1740
- } else {
1741
- filterInfo.textContent = 'Manage and organize your files efficiently';
1742
- fileListTitle.innerHTML = 'Files';
1743
- }
1744
-
1745
- // Render files or empty state
1746
- if (data.files.length === 0 && !prefix) {
1747
- container.innerHTML = `
1748
- <div class="empty-state">
1749
- <i class="fas fa-folder-open"></i>
1750
- <h4>No files found</h4>
1751
- <p>Your bucket is empty. Upload some files to get started!</p>
1752
- <button class="btn btn-lg" onclick="document.getElementById('fileInput').click()">
1753
- <i class="fas fa-plus"></i> Upload Your First File
1754
- </button>
1755
- </div>
1756
- `;
1757
- paginationContainer.style.display = 'none';
1758
- } else {
1759
- // Extract unique folders from files at current level
1760
- const folders = search ? [] : extractFoldersAtCurrentLevel(data.files, prefix);
1761
- renderFilesTable(data.files, folders, prefix);
1762
- renderPagination(data);
1763
- paginationContainer.style.display = 'block';
1764
- }
1765
-
1766
- } catch (error) {
1767
- console.error('Error loading files:', error);
1768
- container.innerHTML = `
1769
- <div class="empty-state">
1770
- <i class="fas fa-exclamation-triangle" style="color: var(--danger-color-ml);"></i>
1771
- <h4>Error Loading Files</h4>
1772
- <p>${error.message}</p>
1773
- <button class="btn btn-primary" onclick="loadFiles(${page}, '${prefix}')">
1774
- <i class="fas fa-redo"></i> Retry
1775
- </button>
1776
- </div>
1777
- `;
1778
- paginationContainer.style.display = 'none';
1779
- }
1780
- }
1781
-
1782
- // Perform search
1783
- function performSearch() {
1784
- const searchInput = document.getElementById('searchInput');
1785
- const search = searchInput.value.trim();
1786
- if (search) {
1787
- loadFiles(1, currentPrefix, search);
1788
- }
1789
- }
1790
-
1791
- // Clear search
1792
- function clearSearch() {
1793
- document.getElementById('searchInput').value = '';
1794
- loadFiles(1, currentPrefix, '');
1795
- }
1796
-
1797
- // Extract unique folders at current level
1798
- function extractFoldersAtCurrentLevel(files, prefix) {
1799
- const folderSet = new Set();
1800
- const prefixDepth = prefix ? prefix.split('/').filter(p => p).length : 0;
1801
-
1802
- files.forEach(file => {
1803
- const key = file.Key;
1804
- // Remove prefix from key
1805
- const relativePath = prefix ? key.substring(prefix.length + (prefix.endsWith('/') ? 0 : 1)) : key;
1806
-
1807
- // Check if there's a folder in the relative path
1808
- const slashIndex = relativePath.indexOf('/');
1809
- if (slashIndex !== -1) {
1810
- const folderName = relativePath.substring(0, slashIndex);
1811
- const fullFolderPath = prefix ? `${prefix}/${folderName}` : folderName;
1812
- folderSet.add(fullFolderPath);
1813
- }
1814
- });
1815
-
1816
- return Array.from(folderSet).sort();
1817
- }
1818
-
1819
- // Render files table
1820
- function renderFilesTable(files, folders = [], prefix = '') {
1821
- // Filter files to show only those at current level (not in subfolders)
1822
- const filesAtCurrentLevel = files.filter(file => {
1823
- const relativePath = prefix ? file.Key.substring(prefix.length + (prefix.endsWith('/') ? 0 : 1)) : file.Key;
1824
- // File is at current level if it has no '/' in relative path
1825
- return !relativePath.includes('/');
1826
- });
1827
-
1828
- // Generate breadcrumb navigation
1829
- let breadcrumb = '';
1830
- if (prefix) {
1831
- const parts = prefix.split('/').filter(p => p);
1832
- let breadcrumbHTML = `
1833
- <div class="breadcrumb-nav">
1834
- <div class="breadcrumb-item">
1835
- <i class="fas fa-home"></i>
1836
- <span class="breadcrumb-link" onclick="loadFiles(1, '')" title="Go to root">Root</span>
1837
- </div>
1838
- `;
1839
-
1840
- let currentPath = '';
1841
- parts.forEach((part, index) => {
1842
- currentPath += (currentPath ? '/' : '') + part;
1843
- const isLast = index === parts.length - 1;
1844
-
1845
- if (isLast) {
1846
- breadcrumbHTML += `
1847
- <span class="breadcrumb-separator"><i class="fas fa-chevron-right"></i></span>
1848
- <div class="breadcrumb-item">
1849
- <span class="breadcrumb-current">${part}</span>
1850
- </div>
1851
- `;
1852
- } else {
1853
- const pathCopy = currentPath;
1854
- breadcrumbHTML += `
1855
- <span class="breadcrumb-separator"><i class="fas fa-chevron-right"></i></span>
1856
- <div class="breadcrumb-item">
1857
- <span class="breadcrumb-link" onclick="loadFiles(1, '${pathCopy}')" title="Go to ${pathCopy}">${part}</span>
1858
- </div>
1859
- `;
1860
- }
1861
- });
1862
-
1863
- breadcrumbHTML += `</div>`;
1864
- breadcrumb = breadcrumbHTML;
1865
- }
1866
-
1867
- // Generate folder rows
1868
- const folderRows = folders.map(folderPath => {
1869
- const folderName = folderPath.split('/').pop();
1870
- return `
1871
- <tr class="folder-row" onclick="loadFiles(1, '${folderPath}')" title="Open folder: ${folderName}">
1872
- <td style="text-align:center;">
1873
- <!-- no selection for folders -->
1874
- </td>
1875
- <td style="padding-left: 1.5rem;">
1876
- <div style="display: flex; align-items: center;">
1877
- <div class="file-icon">
1878
- <i class="fas fa-folder"></i>
1879
- </div>
1880
- <div class="file-info">
1881
- <div class="file-name" style="font-weight: 700;">
1882
- <i class="fas fa-folder" style="margin-right: 0.5rem; color: #f59e0b;"></i>${folderName}/
1883
- </div>
1884
- </div>
1885
- </div>
1886
- </td>
1887
- <td>
1888
- <span style="color: var(--gray-color-ml);">\u2014</span>
1889
- </td>
1890
- <td style="text-align: center;">
1891
- <span style="color: var(--gray-color-ml);">\u2014</span>
1892
- </td>
1893
- <td style="text-align: center;">
1894
- <div class="btn-group" role="group">
1895
- <button type="button" class="btn btn-danger btn-sm delete-btn" data-key="${folderPath}/" data-is-folder="true" title="Delete folder">
1896
- <i class="fas fa-trash"></i>
1897
- </button>
1898
- </div>
1899
- </td>
1900
- </tr>
1901
- `;
1902
- }).join('');
1903
-
1904
- // Generate file rows
1905
- const fileRows = filesAtCurrentLevel.map(file => {
1906
- const encodedKey = encodeURIComponent(file.Key);
1907
- return `
1908
- <tr class="file-row">
1909
- <td style="text-align:center;">
1910
- <input type="checkbox" class="selectFileCheckbox" data-key="${file.Key}" aria-label="Select ${file.Key}" />
1911
- </td>
1912
- <td style="padding-left: 1.5rem;">
1913
- <div style="display: flex; align-items: center;">
1914
- <div class="file-icon">
1915
- <i class="fas fa-file-alt"></i>
1916
- </div>
1917
- <div class="file-info">
1918
- <div class="file-name" data-full-path="${file.Key}"></div>
1919
- </div>
1920
- </div>
1921
- </td>
1922
- <td>
1923
- ${file.Size ? `<span class="file-size">${formatFileSize(file.Size)}</span>` : '<span style="color: var(--gray-color-ml);">N/A</span>'}
1924
- </td>
1925
- <td>
1926
- ${file.LastModified ? `<span style="color: var(--gray-color-ml);">${formatDate(file.LastModified)}</span>` : '<span style="color: var(--gray-color-ml);">N/A</span>'}
1927
- </td>
1928
- <td style="text-align: center;">
1929
- <div class="btn-group" role="group">
1930
- <button type="button" class="btn btn-primary btn-sm view-btn" data-key="${file.Key}" data-filename="${file.Key}"
1931
- data-key-enc="${encodedKey}" title="View file in browser">
1932
- <i class="fas fa-eye"></i>
1933
- </button>
1934
- <a href="/mbkbucket/download/${encodedKey}" class="btn btn-success btn-sm"
1935
- title="Download file">
1936
- <i class="fas fa-download"></i>
1937
- </a>
1938
- <button type="button" class="btn btn-danger btn-sm delete-btn" data-key="${file.Key}" title="Delete file">
1939
- <i class="fas fa-trash"></i>
1940
- </button>
1941
- </div>
1942
- </td>
1943
- </tr>
1944
- `;
1945
- }).join('');
1946
-
1947
- const allRows = folderRows + fileRows;
1948
-
1949
- if (allRows === '') {
1950
- document.getElementById('fileListContainer').innerHTML = `
1951
- <div class="empty-state">
1952
- <i class="fas fa-folder-open"></i>
1953
- <h4>No files or folders</h4>
1954
- <p>${prefix ? `No files in folder: ${prefix}` : 'This folder is empty'}</p>
1955
- <div style="display:flex; gap:0.5rem; justify-content:center; margin-top:1rem;">
1956
- <button class="btn btn-primary" onclick="document.getElementById('fileInput').click()">
1957
- <i class="fas fa-cloud-upload-alt"></i> Upload File
1958
- </button>
1959
- <button class="btn btn-outline-secondary" onclick="createFolder()">
1960
- <i class="fas fa-folder-plus"></i> Create Folder
1961
- </button>
1962
- ${prefix ? `<button class="btn btn-light" onclick="loadFiles(1, '')">
1963
- <i class="fas fa-arrow-left"></i> Back to Root
1964
- </button>` : ''}
1965
- </div>
1966
- </div>
1967
- `;
1968
- return;
1969
- }
1970
-
1971
- document.getElementById('fileListContainer').innerHTML = `
1972
- ${breadcrumb}
1973
- <div class="table-responsive">
1974
- <div id="bulkActionsBar" style="display:none; padding: 0.75rem; border-bottom: 1px solid var(--gray-light-color-ml); background: var(--light-color-ml);">
1975
- <span id="bulkSelectedCount">0 selected</span>
1976
- <div style="float: right;">
1977
- <button class="btn btn-danger btn-sm" id="bulkDeleteBtn" disabled>
1978
- <i class="fas fa-trash"></i> Delete Selected
1979
- </button>
1980
- </div>
1981
- </div>
1982
- <table class="table table-hover mb-0">
1983
- <thead>
1984
- <tr>
1985
- <th style="width:48px; text-align:center;">
1986
- <input type="checkbox" id="selectAllCheckbox" aria-label="Select all files" />
1987
- </th>
1988
- <th style="padding-left: 1.5rem;">
1989
- <i class="fas fa-file-alt"></i> Name
1990
- </th>
1991
- <th>
1992
- <i class="fas fa-weight"></i> Size
1993
- </th>
1994
- <th>
1995
- <i class="fas fa-calendar"></i> Last Modified
1996
- </th>
1997
- <th style="text-align: center;">
1998
- <i class="fas fa-cogs"></i> Actions
1999
- </th>
2000
- </tr>
2001
- </thead>
2002
- <tbody>
2003
- ${allRows}
2004
- </tbody>
2005
- </table>
2006
- </div>
2007
- `;
2008
-
2009
- // Update file icons and names
2010
- updateFileIcons();
2011
- // Wire selection checkboxes and bulk actions
2012
- setupSelection();
2013
- }
2014
-
2015
- // Render pagination
2016
- function renderPagination(data) {
2017
- const { currentPage, totalPages, hasPrevPage, hasNextPage } = data;
2018
-
2019
- let paginationHTML = '';
2020
-
2021
- // Previous button
2022
- if (hasPrevPage) {
2023
- paginationHTML += `
2024
- <li class="page-item">
2025
- <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${currentPage - 1}, '${currentPrefix}', '${currentSearch}')">
2026
- <i class="fas fa-chevron-left"></i> Previous
2027
- </a>
2028
- </li>
2029
- `;
2030
- } else {
2031
- paginationHTML += `
2032
- <li class="page-item disabled">
2033
- <span class="page-link">
2034
- <i class="fas fa-chevron-left"></i> Previous
2035
- </span>
2036
- </li>
2037
- `;
2038
- }
2039
-
2040
- // Page numbers
2041
- for (let i = 1; i <= totalPages; i++) {
2042
- if (i === currentPage) {
2043
- paginationHTML += `
2044
- <li class="page-item active">
2045
- <span class="page-link">${i}</span>
2046
- </li>
2047
- `;
2048
- } else {
2049
- paginationHTML += `
2050
- <li class="page-item">
2051
- <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${i}, '${currentPrefix}', '${currentSearch}')">${i}</a>
2052
- </li>
2053
- `;
2054
- }
2055
- }
2056
-
2057
- // Next button
2058
- if (hasNextPage) {
2059
- paginationHTML += `
2060
- <li class="page-item">
2061
- <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${currentPage + 1}, '${currentPrefix}', '${currentSearch}')">
2062
- Next <i class="fas fa-chevron-right"></i>
2063
- </a>
2064
- </li>
2065
- `;
2066
- } else {
2067
- paginationHTML += `
2068
- <li class="page-item disabled">
2069
- <span class="page-link">
2070
- Next <i class="fas fa-chevron-right"></i>
2071
- </span>
2072
- </li>
2073
- `;
2074
- }
2075
-
2076
- document.getElementById('paginationList').innerHTML = paginationHTML;
2077
- }
2078
-
2079
- function confirmDelete(key, isFolder = false) {
2080
- document.getElementById('deleteKey').value = key;
2081
- document.getElementById('deleteIsFolder').value = isFolder ? 'true' : '';
2082
- document.getElementById('deleteFileName').textContent = key;
2083
-
2084
- // Update modal message and button label for folders
2085
- const modalHeading = document.querySelector('#deleteModal .modal-body h6');
2086
- const smallText = document.querySelector('#deleteModal .modal-body small');
2087
- const confirmLabel = document.getElementById('deleteConfirmLabel');
2088
-
2089
- if (isFolder) {
2090
- if (modalHeading) modalHeading.textContent = 'Are you sure you want to delete this folder and all its contents?';
2091
- if (smallText) smallText.textContent = 'This action will remove the folder and all files within it and cannot be undone.';
2092
- if (confirmLabel) confirmLabel.textContent = 'Delete Folder';
2093
- } else {
2094
- if (modalHeading) modalHeading.textContent = 'Are you sure you want to delete this file?';
2095
- if (smallText) smallText.textContent = 'This action cannot be undone.';
2096
- if (confirmLabel) confirmLabel.textContent = 'Delete File';
2097
- }
2098
-
2099
- document.getElementById('deleteModal').classList.add('show');
2100
- document.getElementById('deleteModal').style.display = 'flex';
2101
- }
2102
-
2103
- function closeModal() {
2104
- document.getElementById('deleteModal').classList.remove('show');
2105
- document.getElementById('deleteModal').style.display = 'none';
2106
- }
2107
-
2108
- // Handle delete form submission
2109
- document.getElementById('deleteForm')?.addEventListener('submit', async function (e) {
2110
- e.preventDefault();
2111
-
2112
- const key = document.getElementById('deleteKey').value;
2113
- const isFolder = document.getElementById('deleteIsFolder').value === 'true';
2114
-
2115
- try {
2116
- const response = await fetch('/mbkbucket/delete', {
2117
- method: 'POST',
2118
- headers: {
2119
- 'Content-Type': 'application/json'
2120
- },
2121
- body: JSON.stringify({ key, folder: isFolder })
2122
- });
2123
-
2124
- const data = await response.json();
2125
-
2126
- if (data.success) {
2127
- showAlert('File deleted successfully', 'success');
2128
- closeModal();
2129
- // Reload file list
2130
- loadFiles(currentPage, currentPrefix);
2131
- } else {
2132
- showAlert(data.error || 'Delete failed', 'error');
2133
- closeModal();
2134
- }
2135
- } catch (error) {
2136
- console.error('Error deleting file:', error);
2137
- showAlert('Error deleting file: ' + error.message, 'error');
2138
- closeModal();
2139
- }
2140
- });
2141
-
2142
- // Auto-dismiss alerts with fade effect
2143
- setTimeout(() => {
2144
- const alerts = document.querySelectorAll('.alert');
2145
- alerts.forEach(alert => {
2146
- alert.style.opacity = '0';
2147
- alert.style.transform = 'translateY(-10px)';
2148
- setTimeout(() => alert.remove(), 300);
2149
- });
2150
- }, 5000);
2151
-
2152
- // Upload queue, drag-and-drop and multi-file support
2153
- const uploadQueue = []; // { id, file, status, progress, controller/xhr, uploadId }
2154
- const xhrMap = new Map();
2155
- const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB chunk threshold
2156
-
2157
- function createId() { return Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 9); }
2158
-
2159
- function addFilesToQueue(fileList) {
2160
- for (const f of Array.from(fileList)) {
2161
- uploadQueue.push({ id: createId(), file: f, status: 'queued', progress: 0 });
2162
- }
2163
- renderUploadQueue();
2164
- }
2165
-
2166
- // Render upload queue UI
2167
- function renderUploadQueue() {
2168
- const container = document.getElementById('uploadQueue');
2169
- if (!container) return;
2170
- if (uploadQueue.length === 0) {
2171
- container.innerHTML = `
2172
- <div class="form-text">No files selected.</div>
2173
- `;
2174
- return;
2175
- }
2176
-
2177
- container.innerHTML = uploadQueue.map(item => `
2178
- <div class="upload-item" data-id="${item.id}">
2179
- <div class="meta">
2180
- <div class="file-icon"><i class="fas fa-file"></i></div>
2181
- <div>
2182
- <div class="name">${item.file.name}</div>
2183
- <div class="form-text">${formatFileSize(item.file.size)}</div>
2184
- </div>
2185
- </div>
2186
- <div style="min-width:220px;">
2187
- <div class="progress-container" style="height: 8px; margin-bottom: 6px;">
2188
- <div class="progress-bar" style="width: ${Math.round(item.progress)}%; height: 8px;"></div>
2189
- </div>
2190
- <div style="display:flex; justify-content:space-between; align-items:center; gap:0.5rem;">
2191
- <small class="text-muted status">${item.status}</small>
2192
- <div class="controls">
2193
- ${item.status === 'uploading' ? `<button class="btn btn-sm btn-outline-secondary cancel-upload" data-id="${item.id}"><i class="fas fa-ban"></i></button>` : `<button class="btn btn-sm btn-danger remove-queued" data-id="${item.id}"><i class="fas fa-trash"></i></button>`}
2194
- </div>
2195
- </div>
2196
- </div>
2197
- </div>
2198
- `).join('');
2199
-
2200
- // Wire up controls
2201
- document.querySelectorAll('.remove-queued').forEach(btn => btn.addEventListener('click', function (e) {
2202
- const id = this.dataset.id;
2203
- const idx = uploadQueue.findIndex(i => i.id === id);
2204
- if (idx !== -1) uploadQueue.splice(idx, 1);
2205
- renderUploadQueue();
2206
- }));
2207
-
2208
- document.querySelectorAll('.cancel-upload').forEach(btn => btn.addEventListener('click', function (e) {
2209
- const id = this.dataset.id;
2210
- const handle = xhrMap.get(id);
2211
- if (handle && handle.abort) {
2212
- handle.abort();
2213
- } else if (handle && handle.xhr) {
2214
- handle.xhr.abort();
2215
- }
2216
- const entry = uploadQueue.find(i => i.id === id);
2217
- if (entry) { entry.status = 'cancelled'; entry.progress = 0; }
2218
- xhrMap.delete(id);
2219
- renderUploadQueue();
2220
- }));
2221
- }
2222
-
2223
- // Drag & drop wiring
2224
- const dropZone = document.getElementById('dropZone');
2225
- const fileInputEl = document.getElementById('fileInput');
2226
- if (dropZone) {
2227
- dropZone.addEventListener('click', () => fileInputEl.click());
2228
- dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('dragover'); });
2229
- dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
2230
- dropZone.addEventListener('drop', (e) => {
2231
- e.preventDefault(); dropZone.classList.remove('dragover');
2232
- if (e.dataTransfer && e.dataTransfer.files) addFilesToQueue(e.dataTransfer.files);
2233
- });
2234
- }
2235
-
2236
- fileInputEl?.addEventListener('change', function (e) {
2237
- if (e.target.files && e.target.files.length) {
2238
- addFilesToQueue(e.target.files);
2239
- }
2240
- });
2241
-
2242
- // Start queued uploads (single or chunked) and provide cancelation controls
2243
- async function startUploadEntry(entry) {
2244
- if (!entry || entry.status === 'uploading' || entry.status === 'done') return;
2245
- entry.status = 'uploading';
2246
- renderUploadQueue();
2247
-
2248
- const file = entry.file;
2249
- try {
2250
- if (file.size > CHUNK_SIZE) {
2251
- // chunked upload
2252
- const uploadId = createId();
2253
- entry.uploadId = uploadId;
2254
- const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
2255
- let uploadedBytes = 0;
2256
- for (let i = 0; i < totalChunks; i++) {
2257
- if (entry.status === 'cancelled') throw new Error('Upload cancelled');
2258
- const start = i * CHUNK_SIZE;
2259
- const end = Math.min(file.size, start + CHUNK_SIZE);
2260
- const blob = file.slice(start, end);
2261
-
2262
- const fd = new FormData();
2263
- fd.append('chunk', blob, file.name);
2264
- fd.append('uploadId', uploadId);
2265
- fd.append('fileName', file.name);
2266
- fd.append('chunkIndex', String(i));
2267
- fd.append('totalChunks', String(totalChunks));
2268
-
2269
- const controller = new AbortController();
2270
- xhrMap.set(entry.id, { controller });
2271
-
2272
- const resp = await fetch('/mbkbucket/upload-chunk', { method: 'POST', body: fd, signal: controller.signal });
2273
- if (!resp.ok) throw new Error('Chunk upload failed');
2274
- uploadedBytes += blob.size;
2275
- entry.progress = (uploadedBytes / file.size) * 95; // up to 95% until assembly
2276
- renderUploadQueue();
2277
- }
2278
-
2279
- // complete
2280
- const completeResp = await fetch('/mbkbucket/upload-complete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ uploadId: entry.uploadId, fileName: file.name, prefix: currentPrefix, contentType: file.type || 'application/octet-stream' }) });
2281
- const completeJson = await completeResp.json();
2282
- if (!completeResp.ok || !completeJson.success) throw new Error(completeJson.error || 'Assemble failed');
2283
-
2284
- entry.status = 'done'; entry.progress = 100; xhrMap.delete(entry.id); renderUploadQueue();
2285
- showAlert('File uploaded successfully', 'success');
2286
- loadFiles(currentPage, currentPrefix);
2287
- } else {
2288
- // small file XHR
2289
- const fd = new FormData();
2290
- fd.append('file', file, file.name);
2291
- fd.append('prefix', currentPrefix);
2292
-
2293
- const xhr = new XMLHttpRequest();
2294
- xhr.open('POST', '/mbkbucket/upload');
2295
-
2296
- xhr.upload.addEventListener('progress', (e) => {
2297
- if (!e.lengthComputable) return;
2298
- entry.progress = (e.loaded / e.total) * 100;
2299
- renderUploadQueue();
2300
- });
2301
-
2302
- xhr.addEventListener('load', () => {
2303
- try {
2304
- const resp = JSON.parse(xhr.responseText);
2305
- if (xhr.status >= 200 && xhr.status < 300 && resp.success) {
2306
- entry.status = 'done'; entry.progress = 100; renderUploadQueue();
2307
- showAlert('File uploaded successfully', 'success');
2308
- loadFiles(currentPage, currentPrefix);
2309
- } else {
2310
- entry.status = 'error'; renderUploadQueue();
2311
- showAlert(resp.error || 'Upload failed', 'error');
2312
- }
2313
- } catch (err) {
2314
- entry.status = 'error'; renderUploadQueue();
2315
- showAlert('Upload response parse error', 'error');
2316
- }
2317
- xhrMap.delete(entry.id);
2318
- });
2319
-
2320
- xhr.addEventListener('error', () => { entry.status = 'error'; renderUploadQueue(); showAlert('Network error during upload', 'error'); xhrMap.delete(entry.id); });
2321
- xhr.addEventListener('abort', () => { entry.status = 'cancelled'; renderUploadQueue(); xhrMap.delete(entry.id); });
2322
-
2323
- xhrMap.set(entry.id, { xhr });
2324
- xhr.send(fd);
2325
- }
2326
- } catch (err) {
2327
- console.error('Upload failed for', entry.file.name, err);
2328
- entry.status = entry.status === 'cancelled' ? 'cancelled' : 'error';
2329
- renderUploadQueue();
2330
- showAlert('Upload failed: ' + (err.message || 'unknown'), 'error');
2331
- }
2332
- }
2333
-
2334
- function startAllUploads() {
2335
- for (const entry of uploadQueue.slice()) {
2336
- if (entry.status === 'queued' || entry.status === 'error') startUploadEntry(entry);
2337
- }
2338
- }
2339
-
2340
- document.getElementById('startUploadBtn')?.addEventListener('click', startAllUploads);
2341
- document.getElementById('clearQueueBtn')?.addEventListener('click', function () { uploadQueue.length = 0; renderUploadQueue(); });
2342
-
2343
- // Reset upload form
2344
- function resetUploadForm() {
2345
- const uploadBtn = document.getElementById('uploadBtn');
2346
- const uploadProgress = document.getElementById('uploadProgress');
2347
-
2348
- setTimeout(() => {
2349
- uploadBtn.disabled = false;
2350
- uploadBtn.classList.remove('uploading');
2351
- uploadBtn.innerHTML = '<i class="fas fa-cloud-upload-alt"></i> Upload File';
2352
- uploadProgress.style.display = 'none';
2353
-
2354
- // Reset progress
2355
- document.getElementById('progressBar').style.width = '0%';
2356
- document.getElementById('uploadPercent').textContent = '0%';
2357
- }, 3000);
2358
- }
2359
-
2360
- // Add hover effects and animations
2361
- document.addEventListener('DOMContentLoaded', function () {
2362
- // Load initial files (support both legacy 'prefix' and new 'folder' query param)
2363
- const urlParams = new URLSearchParams(window.location.search);
2364
- const initialPrefix = urlParams.get('folder') || urlParams.get('prefix') || '{{mbkautheVar.APP_NAME}}' || '';
2365
- const initialPage = parseInt(urlParams.get('page')) || 1;
2366
- loadFiles(initialPage, initialPrefix);
2367
-
2368
- // Debounced search input (improves UX and reduces API calls)
2369
- const searchInputEl = document.getElementById('searchInput');
2370
- if (searchInputEl && typeof debounce === 'function') {
2371
- searchInputEl.addEventListener('input', debounce(() => performSearch(), 350));
2372
- }
2373
-
2374
- // Close modal when clicking outside
2375
- document.getElementById('deleteModal').addEventListener('click', function (e) {
2376
- if (e.target === this) {
2377
- closeModal();
2378
- }
2379
- });
2380
-
2381
- // Delegate view and delete button clicks to avoid inline JS with unescaped data
2382
- document.addEventListener('click', function (e) {
2383
- const vbtn = e.target.closest('.view-btn');
2384
- if (vbtn) {
2385
- const keyEnc = vbtn.dataset.keyEnc;
2386
- const key = keyEnc || vbtn.dataset.key;
2387
- const filename = vbtn.dataset.filename || (vbtn.dataset.key || key);
2388
- handleViewFile(key, filename, e);
2389
- return;
2390
- }
2391
-
2392
- const dbtn = e.target.closest('.delete-btn');
2393
- if (dbtn) {
2394
- e.stopPropagation();
2395
- const key = dbtn.dataset.key;
2396
- const isFolder = dbtn.dataset.isFolder === 'true';
2397
- if (key) confirmDelete(key, isFolder);
2398
- return;
2399
- }
2400
-
2401
-
2402
- });
2403
- });
2404
-
2405
- // Function to check if file can be viewed in browser
2406
-
2407
- // Enhanced view button click handler
2408
- function handleViewFile(key, filename, event) {
2409
- // Prevent default action and event bubbling
2410
- if (event) {
2411
- event.preventDefault();
2412
- event.stopPropagation();
2413
- }
2414
-
2415
- if (!isViewable(filename)) {
2416
- alert(`File type "${filename.split('.').pop().toUpperCase()}" is not supported for viewing in browser.\n\nSupported types include: Images, Videos (MP4, WebM), Audio (MP3, WAV), Text files, Code files, and PDF documents.`);
2417
- return false;
2418
- }
2419
-
2420
- // Determine file type
2421
- const extension = filename.split('.').pop().toLowerCase();
2422
- const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico'];
2423
- const videoTypes = ['mp4', 'webm', 'ogg'];
2424
-
2425
- // Use inline preview modal for supported types
2426
- const previewableImage = imageTypes.includes(extension);
2427
- const previewableVideo = videoTypes.includes(extension);
2428
- const previewableAudio = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a'].includes(extension);
2429
- const previewablePdf = extension === 'pdf';
2430
- const previewableText = ['txt', 'md', 'json', 'xml', 'csv', 'log', 'js', 'ts', 'html', 'htm', 'css', 'py', 'java', 'cpp', 'c', 'h', 'cs', 'rb', 'go', 'rs', 'sql', 'sh', 'bat', 'ps1', 'yaml', 'yml', 'toml', 'ini', 'conf'].includes(extension);
2431
-
2432
- // Build an encoded key safely (if key already appears encoded, don't double-encode)
2433
- const encodedKey = (key && key.includes('%')) ? key : encodeURIComponent(key || filename);
2434
-
2435
- // Show the inline preview
2436
- showPreviewModal(encodedKey, filename, extension);
2437
- return false;
2438
- }
2439
-
2440
- // Function to get file type category
2441
-
2442
- // Preview modal helpers
2443
- function showPreviewModal(encodedKey, filename, extension) {
2444
- const modal = document.getElementById('previewModal');
2445
- if (!modal) {
2446
- window.open(`/mbkbucket/view/${encodedKey}`, '_blank', 'noopener,noreferrer');
2447
- return;
2448
- }
2449
-
2450
- const dialog = modal.querySelector('.modal-dialog');
2451
- const titleEl = document.getElementById('previewFileName');
2452
- const contentEl = modal.querySelector('.preview-content');
2453
- const openBtn = document.getElementById('previewOpenBtn');
2454
- const downloadBtn = document.getElementById('previewDownloadBtn');
2455
- const closeBtn = document.getElementById('previewCloseBtn');
2456
- const closeFooter = document.getElementById('previewCloseFooter');
2457
- const fsBtn = document.getElementById('previewFullscreenBtn');
2458
-
2459
- titleEl.textContent = filename;
2460
- openBtn.href = `/mbkbucket/view/${encodedKey}`;
2461
- downloadBtn.href = `/mbkbucket/download/${encodedKey}`;
2462
- // Ensure downloaded file has the original filename
2463
- downloadBtn.setAttribute('download', filename);
2464
-
2465
- // Reset sizing classes
2466
- dialog.classList.remove('preview-large', 'preview-compact');
2467
-
2468
- // Clear previous content and show loading
2469
- contentEl.innerHTML = `<div class="empty-preview"><i class="fas fa-spinner fa-spin"></i><div style="margin-top:0.5rem; color:var(--gray-color-ml);">Loading preview...</div></div>`;
2470
-
2471
- const imgTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico'];
2472
- const videoTypes = ['mp4', 'webm', 'ogg'];
2473
- const audioTypes = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a'];
2474
- const textTypes = ['txt', 'md', 'json', 'xml', 'csv', 'log', 'js', 'ts', 'html', 'htm', 'css', 'py', 'java', 'cpp', 'c', 'h', 'cs', 'rb', 'go', 'rs', 'sql', 'sh', 'bat', 'ps1', 'yaml', 'yml', 'toml', 'ini', 'conf'];
2475
-
2476
- // Render content and adjust dialog size
2477
- if (imgTypes.includes(extension)) {
2478
- dialog.classList.add('preview-compact');
2479
- const img = document.createElement('img');
2480
- img.className = 'preview-img';
2481
- img.src = `/mbkbucket/view/${encodedKey}`;
2482
- img.alt = filename;
2483
- img.onload = () => { };
2484
- img.onerror = () => { contentEl.textContent = 'Could not load preview.'; };
2485
- contentEl.innerHTML = '';
2486
- contentEl.appendChild(img);
2487
- } else if (videoTypes.includes(extension)) {
2488
- dialog.classList.add('preview-large');
2489
- const video = document.createElement('video');
2490
- video.className = 'preview-video';
2491
- video.controls = true;
2492
- video.src = `/mbkbucket/view/${encodedKey}`;
2493
- contentEl.innerHTML = '';
2494
- contentEl.appendChild(video);
2495
- } else if (audioTypes.includes(extension)) {
2496
- dialog.classList.add('preview-compact');
2497
- const audio = document.createElement('audio');
2498
- audio.className = 'preview-audio';
2499
- audio.controls = true;
2500
- audio.src = `/mbkbucket/view/${encodedKey}`;
2501
- contentEl.innerHTML = '';
2502
- contentEl.appendChild(audio);
2503
- } else if (extension === 'pdf') {
2504
- dialog.classList.add('preview-large');
2505
- const iframe = document.createElement('iframe');
2506
- iframe.className = 'preview-iframe';
2507
- iframe.src = `/mbkbucket/view/${encodedKey}`;
2508
- contentEl.innerHTML = '';
2509
- contentEl.appendChild(iframe);
2510
- } else if (textTypes.includes(extension)) {
2511
- dialog.classList.add('preview-compact');
2512
- fetch(`/mbkbucket/view/${encodedKey}`)
2513
- .then(r => r.text())
2514
- .then(text => {
2515
- contentEl.innerHTML = '';
2516
- const pre = document.createElement('pre');
2517
- pre.textContent = text;
2518
- pre.className = 'preview-text';
2519
- contentEl.appendChild(pre);
2520
- })
2521
- .catch(err => { contentEl.textContent = 'Could not load preview.'; });
2522
- } else {
2523
- contentEl.innerHTML = `<div style="text-align:center;">Preview not available for this file type. <a href="/mbkbucket/download/${encodedKey}">Download</a></div>`;
2524
- }
2525
-
2526
- // Show modal
2527
- modal.classList.add('show');
2528
- modal.style.display = 'flex';
2529
- modal.setAttribute('aria-hidden', 'false');
2530
-
2531
- // Focus management + focus trap
2532
- const prevFocus = document.activeElement;
2533
- const focusableSelector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
2534
- function getFocusable() { return Array.from(modal.querySelectorAll(focusableSelector)).filter(el => !el.hasAttribute('disabled')); }
2535
- const focusable = () => getFocusable();
2536
- const firstFocusable = () => focusable()[0];
2537
- const lastFocusable = () => focusable()[focusable().length - 1];
2538
-
2539
- // Focus initial
2540
- (closeBtn || closeFooter).focus();
2541
-
2542
- function onKeyDown(e) {
2543
- if (e.key === 'Escape') return cleanup();
2544
- if (e.key === 'Tab') {
2545
- const f = focusable();
2546
- if (f.length === 0) return;
2547
- if (e.shiftKey && document.activeElement === f[0]) { e.preventDefault(); f[f.length - 1].focus(); }
2548
- else if (!e.shiftKey && document.activeElement === f[f.length - 1]) { e.preventDefault(); f[0].focus(); }
2549
- }
2550
- }
2551
-
2552
- function onBackdropClick(e) { if (e.target === modal) cleanup(); }
2553
-
2554
- // Fullscreen toggle
2555
- let fsActive = false;
2556
- function toggleFullscreen() {
2557
- if (!document.fullscreenElement) {
2558
- dialog.requestFullscreen?.();
2559
- fsBtn.innerHTML = '<i class="fas fa-compress"></i>';
2560
- fsActive = true;
2561
- } else {
2562
- document.exitFullscreen?.();
2563
- fsBtn.innerHTML = '<i class="fas fa-expand"></i>';
2564
- fsActive = false;
2565
- }
2566
- }
2567
-
2568
- function cleanup() {
2569
- // Exit fullscreen if active
2570
- if (fsActive && document.fullscreenElement) document.exitFullscreen?.();
2571
- modal.classList.remove('show');
2572
- modal.style.display = 'none';
2573
- modal.setAttribute('aria-hidden', 'true');
2574
- if (prevFocus && prevFocus.focus) prevFocus.focus();
2575
- modal.removeEventListener('click', onBackdropClick);
2576
- document.removeEventListener('keydown', onKeyDown);
2577
- fsBtn.removeEventListener('click', toggleFullscreen);
2578
- (closeBtn || closeFooter).removeEventListener('click', cleanup);
2579
- closeFooter.removeEventListener('click', cleanup);
2580
- }
2581
-
2582
- modal.addEventListener('click', onBackdropClick);
2583
- document.addEventListener('keydown', onKeyDown);
2584
- fsBtn.addEventListener('click', toggleFullscreen);
2585
- (closeBtn || closeFooter).addEventListener('click', cleanup);
2586
- closeFooter.addEventListener('click', cleanup);
2587
- }
2588
-
2589
- function closePreviewModal() {
2590
- const modal = document.getElementById('previewModal');
2591
- if (!modal) return;
2592
- modal.classList.remove('show');
2593
- modal.style.display = 'none';
2594
- modal.setAttribute('aria-hidden', 'true');
2595
- }
2596
-
2597
- // Update file icons in the table
2598
- function updateFileIcons() {
2599
- const fileRows = document.querySelectorAll('.file-row');
2600
- fileRows.forEach(row => {
2601
- const fileNameElement = row.querySelector('.file-name');
2602
- const folderPathElement = row.querySelector('.folder-path');
2603
- const fileIconElement = row.querySelector('.file-icon i');
2604
- const fileIconContainer = row.querySelector('.file-icon');
2605
- const viewButton = row.querySelector('.view-btn');
2606
-
2607
- if (fileNameElement && fileIconElement && fileIconContainer) {
2608
- const fullPath = fileNameElement.getAttribute('data-full-path');
2609
-
2610
- // Split the path into folder and filename
2611
- const lastSlashIndex = fullPath.lastIndexOf('/');
2612
- let folder = '';
2613
- let filename = fullPath;
2614
-
2615
- if (lastSlashIndex !== -1) {
2616
- folder = fullPath.substring(0, lastSlashIndex);
2617
- filename = fullPath.substring(lastSlashIndex + 1);
2618
- }
2619
-
2620
- // Truncate long filenames
2621
- const truncatedName = truncateFileName(filename);
2622
-
2623
- // Update the display
2624
- fileNameElement.textContent = truncatedName;
2625
- fileNameElement.title = filename; // Full name on hover
2626
-
2627
-
2628
-
2629
- const iconClass = getFileIcon(filename);
2630
- const typeCategory = getFileTypeCategory(filename);
2631
-
2632
- // Update icon
2633
- fileIconElement.className = iconClass;
2634
-
2635
- // Add type-specific styling
2636
- if (typeCategory) {
2637
- fileIconContainer.classList.add(typeCategory);
2638
- }
2639
-
2640
- // Show/hide and update view button based on file type
2641
- if (viewButton) {
2642
- const isFileViewable = isViewable(filename);
2643
-
2644
- if (isFileViewable) {
2645
- viewButton.style.display = 'inline-flex';
2646
- viewButton.style.opacity = '1';
2647
- viewButton.title = 'View file in browser';
2648
- viewButton.disabled = false;
2649
- } else {
2650
- viewButton.style.opacity = '0.5';
2651
- viewButton.title = 'File type not supported for viewing';
2652
- viewButton.disabled = true;
2653
- }
2654
- }
2655
- }
2656
- });
2657
- }
2658
-
2659
- // Selection & bulk actions
2660
- function setupSelection() {
2661
- const selectAll = document.getElementById('selectAllCheckbox');
2662
- const bulkBar = document.getElementById('bulkActionsBar');
2663
- const bulkCount = document.getElementById('bulkSelectedCount');
2664
- const bulkDeleteBtn = document.getElementById('bulkDeleteBtn');
2665
-
2666
- const checkboxes = Array.from(document.querySelectorAll('.selectFileCheckbox'));
2667
-
2668
- const selected = new Set();
2669
-
2670
- function updateUI() {
2671
- const count = selected.size;
2672
- bulkCount.textContent = `${count} selected`;
2673
- bulkBar.style.display = count > 0 ? 'block' : 'none';
2674
- bulkDeleteBtn.disabled = count === 0;
2675
- if (selectAll) selectAll.checked = checkboxes.length > 0 && selected.size === checkboxes.length;
2676
- }
2677
-
2678
- checkboxes.forEach(cb => {
2679
- cb.checked = false;
2680
- cb.addEventListener('change', function (e) {
2681
- const key = this.dataset.key;
2682
- if (this.checked) selected.add(key); else selected.delete(key);
2683
- updateUI();
2684
- });
2685
- });
2686
-
2687
- if (selectAll) {
2688
- selectAll.checked = false;
2689
- selectAll.addEventListener('change', function () {
2690
- const checked = this.checked;
2691
- selected.clear();
2692
- checkboxes.forEach(cb => { cb.checked = checked; if (checked) selected.add(cb.dataset.key); });
2693
- updateUI();
2694
- });
2695
- }
2696
-
2697
- if (bulkDeleteBtn) {
2698
- bulkDeleteBtn.addEventListener('click', async function () {
2699
- if (!confirm('Delete selected files? This cannot be undone.')) return;
2700
- const keys = Array.from(selected);
2701
- try {
2702
- const resp = await fetch('/mbkbucket/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ keys }) });
2703
- const json = await resp.json();
2704
- if (json.success) {
2705
- showAlert('Deleted selected files', 'success');
2706
- loadFiles(currentPage, currentPrefix);
2707
- } else {
2708
- showAlert(json.error || 'Delete failed', 'error');
2709
- }
2710
- } catch (err) {
2711
- showAlert('Delete failed: ' + err.message, 'error');
2712
- }
2713
- });
2714
- }
2715
- }
2716
-
2717
- // Create folder (zero-byte placeholder object)
2718
- async function createFolder() {
2719
- const name = prompt('Enter new folder name (no slashes):');
2720
- if (!name) return;
2721
- const safe = name.replace(/[\\/]+/g, '').trim();
2722
- if (!safe) return showAlert('Invalid folder name', 'error');
2723
-
2724
- try {
2725
- const resp = await fetch('/mbkbucket/create-folder', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prefix: currentPrefix, folderName: safe }) });
2726
- const json = await resp.json();
2727
- if (json.success) {
2728
- showAlert('Folder created', 'success');
2729
- loadFiles(currentPage, currentPrefix);
2730
- } else {
2731
- showAlert(json.error || 'Create folder failed', 'error');
2732
- }
2733
- } catch (err) {
2734
- showAlert('Create folder failed: ' + err.message, 'error');
2735
- }
2736
- }
2737
-
2738
- // Start/clear upload buttons
2739
- document.getElementById('startUploadBtn')?.addEventListener('click', startAllUploads);
2740
- document.getElementById('clearQueueBtn')?.addEventListener('click', function () { uploadQueue.length = 0; renderUploadQueue(); });
2741
-
2742
- </script>
2743
- <script>
2744
- // mbkbucket shared utilities (exposed to global window for backward compatibility)
2745
- (function (w) {
2746
- 'use strict';
2747
-
2748
- w.formatDate = function (dateString) {
2749
- const date = new Date(dateString);
2750
- return date.toLocaleString();
2751
- };
2752
-
2753
- w.formatFileSize = function (bytes) {
2754
- if (bytes === 0) return '0 Bytes';
2755
- const k = 1024;
2756
- const sizes = ['Bytes', 'KB', 'MB', 'GB'];
2757
- const i = Math.floor(Math.log(bytes) / Math.log(k));
2758
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
2759
- };
2760
-
2761
- w.formatTime = function (seconds) {
2762
- if (seconds < 60) {
2763
- return Math.round(seconds) + 's';
2764
- } else if (seconds < 3600) {
2765
- const minutes = Math.floor(seconds / 60);
2766
- const remainingSeconds = Math.round(seconds % 60);
2767
- return minutes + 'm ' + remainingSeconds + 's';
2768
- } else {
2769
- const hours = Math.floor(seconds / 3600);
2770
- const minutes = Math.floor((seconds % 3600) / 60);
2771
- return hours + 'h ' + minutes + 'm';
2772
- }
2773
- };
2774
-
2775
- w.getFileIcon = function (filename) {
2776
- const extension = (filename || '').split('.').pop().toLowerCase();
2777
-
2778
- const iconMap = {
2779
- jpg: 'fas fa-image',
2780
- jpeg: 'fas fa-image',
2781
- png: 'fas fa-image',
2782
- gif: 'fas fa-image',
2783
- webp: 'fas fa-image',
2784
- svg: 'fas fa-image',
2785
- bmp: 'fas fa-image',
2786
- ico: 'fas fa-image',
2787
- mp4: 'fas fa-video',
2788
- webm: 'fas fa-video',
2789
- ogg: 'fas fa-video',
2790
- avi: 'fas fa-video',
2791
- mov: 'fas fa-video',
2792
- wmv: 'fas fa-video',
2793
- flv: 'fas fa-video',
2794
- mkv: 'fas fa-video',
2795
- mp3: 'fas fa-music',
2796
- wav: 'fas fa-music',
2797
- flac: 'fas fa-music',
2798
- aac: 'fas fa-music',
2799
- m4a: 'fas fa-music',
2800
- pdf: 'fas fa-file-pdf',
2801
- doc: 'fas fa-file-word',
2802
- docx: 'fas fa-file-word',
2803
- xls: 'fas fa-file-excel',
2804
- xlsx: 'fas fa-file-excel',
2805
- ppt: 'fas fa-file-powerpoint',
2806
- pptx: 'fas fa-file-powerpoint',
2807
- js: 'fab fa-js-square',
2808
- ts: 'fas fa-code',
2809
- html: 'fab fa-html5',
2810
- htm: 'fab fa-html5',
2811
- css: 'fab fa-css3-alt',
2812
- php: 'fab fa-php',
2813
- py: 'fab fa-python',
2814
- java: 'fab fa-java',
2815
- cpp: 'fas fa-code',
2816
- c: 'fas fa-code',
2817
- h: 'fas fa-code',
2818
- cs: 'fas fa-code',
2819
- rb: 'fas fa-code',
2820
- go: 'fas fa-code',
2821
- rs: 'fas fa-code',
2822
- sql: 'fas fa-database',
2823
- sh: 'fas fa-terminal',
2824
- bat: 'fas fa-terminal',
2825
- ps1: 'fas fa-terminal',
2826
- txt: 'fas fa-file-alt',
2827
- md: 'fab fa-markdown',
2828
- json: 'fas fa-code',
2829
- xml: 'fas fa-code',
2830
- csv: 'fas fa-file-csv',
2831
- log: 'fas fa-file-alt',
2832
- yaml: 'fas fa-code',
2833
- yml: 'fas fa-code',
2834
- toml: 'fas fa-code',
2835
- ini: 'fas fa-cog',
2836
- conf: 'fas fa-cog',
2837
- zip: 'fas fa-file-archive',
2838
- rar: 'fas fa-file-archive',
2839
- '7z': 'fas fa-file-archive',
2840
- tar: 'fas fa-file-archive',
2841
- gz: 'fas fa-file-archive'
2842
- };
2843
-
2844
- return iconMap[extension] || 'fas fa-file-alt';
2845
- };
2846
-
2847
- w.isViewable = function (filename) {
2848
- const extension = (filename || '').split('.').pop().toLowerCase();
2849
- const viewableTypes = [
2850
- 'jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico',
2851
- 'mp4', 'webm', 'ogg',
2852
- 'mp3', 'wav', 'ogg',
2853
- 'txt', 'md', 'json', 'xml', 'csv', 'log', 'js', 'ts', 'html', 'htm', 'css',
2854
- 'php', 'py', 'java', 'cpp', 'c', 'h', 'cs', 'rb', 'go', 'rs', 'sql',
2855
- 'sh', 'bat', 'ps1', 'yaml', 'yml', 'toml', 'ini', 'conf',
2856
- 'pdf'
2857
- ];
2858
-
2859
- return viewableTypes.includes(extension);
2860
- };
2861
-
2862
- w.getFileTypeCategory = function (filename) {
2863
- const extension = (filename || '').split('.').pop().toLowerCase();
2864
- const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico'];
2865
- const videoTypes = ['mp4', 'webm', 'ogg', 'avi', 'mov', 'wmv', 'flv', 'mkv'];
2866
- const audioTypes = ['mp3', 'wav', 'flac', 'aac', 'm4a'];
2867
- const documentTypes = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'];
2868
- const codeTypes = ['js', 'ts', 'html', 'htm', 'css', 'php', 'py', 'java', 'cpp', 'c', 'h', 'cs', 'rb', 'go', 'rs', 'sql', 'sh', 'bat', 'ps1', 'yaml', 'yml', 'toml', 'ini', 'conf', 'txt', 'md', 'json', 'xml', 'csv', 'log'];
2869
-
2870
- if (imageTypes.includes(extension)) return 'image-file';
2871
- if (videoTypes.includes(extension)) return 'video-file';
2872
- if (audioTypes.includes(extension)) return 'audio-file';
2873
- if (documentTypes.includes(extension)) return 'document-file';
2874
- if (codeTypes.includes(extension)) return 'code-file';
2875
-
2876
- return '';
2877
- };
2878
-
2879
- w.truncateFileName = function (filename) {
2880
- const isMobile = window.innerWidth <= 768;
2881
- if (isMobile) return filename;
2882
- const maxLength = 30;
2883
- if ((filename || '').length <= maxLength) return filename;
2884
- const dotIndex = filename.lastIndexOf('.');
2885
- let name = filename;
2886
- let extension = '';
2887
- if (dotIndex !== -1) {
2888
- name = filename.substring(0, dotIndex);
2889
- extension = filename.substring(dotIndex);
2890
- }
2891
- const lastChars = 3;
2892
- const extensionLength = extension.length;
2893
- const ellipsis = '...';
2894
- const availableForName = maxLength - ellipsis.length - lastChars - extensionLength;
2895
- if (availableForName <= 0) return filename.substring(0, maxLength - ellipsis.length) + ellipsis;
2896
- const firstPart = name.substring(0, availableForName);
2897
- const lastPart = name.substring(name.length - lastChars);
2898
- return firstPart + ellipsis + lastPart + extension;
2899
- };
2900
-
2901
- // Debounce helper - returns a function that delays invocation until after wait ms
2902
- w.debounce = function (fn, wait) {
2903
- let timeout = null;
2904
- return function () {
2905
- const ctx = this;
2906
- const args = arguments;
2907
- clearTimeout(timeout);
2908
- timeout = setTimeout(function () {
2909
- fn.apply(ctx, args);
2910
- }, wait);
2911
- };
2912
- };
2913
-
2914
- })(window);
2915
- </script>
253
+ <script src="/js/bucketadmincontent.js?v={{assetVersion}}"></script>