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.
- package/index.js +9 -0
- package/lib/bucket.js +13 -2
- package/package.json +1 -1
- package/public/css/bucketadmin.css +1413 -0
- package/public/js/bucketadmincontent.js +1400 -0
- package/views/bucketadmincontent.handlebars +13 -2675
- package/views/info.handlebars +1056 -0
|
@@ -1,1399 +1,6 @@
|
|
|
1
1
|
{{!-- File Management System - Bucket Dashboard --}}
|
|
2
|
-
<
|
|
3
|
-
|
|
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>
|