mbkbucket 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +1 -1
- package/.github/PACKAGE.md +0 -4
- package/index.d.ts +126 -37
- package/index.js +1 -1
- package/lib/index.js +2 -2
- package/lib/routes/api.js +501 -0
- package/lib/routes/index.js +17 -0
- package/lib/routes/info.js +28 -0
- package/lib/routes/view.js +458 -0
- package/package.json +2 -2
- package/public/bucketadmin.css +144 -32
- package/public/bucketadmin.js +114 -34
- package/views/bucket.handlebars +3 -3
- package/views/bucketadmin_alerts.handlebars +15 -0
- package/views/bucketadmin_delete_modal.handlebars +40 -0
- package/views/bucketadmin_filelist_skeleton.handlebars +20 -0
- package/views/bucketadmin_preview_modal.handlebars +33 -0
- package/views/bucketadmincontent.handlebars +5 -94
- package/lib/bucket.js +0 -979
package/public/bucketadmin.css
CHANGED
|
@@ -123,6 +123,16 @@
|
|
|
123
123
|
font-size: 0.9rem;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
.stats-bucket-badge {
|
|
127
|
+
background: rgba(255, 255, 255, 0.2);
|
|
128
|
+
padding: 0.35rem 0.75rem;
|
|
129
|
+
border-radius: 0.5rem;
|
|
130
|
+
font-size: 0.85rem;
|
|
131
|
+
font-weight: 600;
|
|
132
|
+
color: white;
|
|
133
|
+
letter-spacing: 0.02em;
|
|
134
|
+
}
|
|
135
|
+
|
|
126
136
|
/* Form styling - Modern approach */
|
|
127
137
|
.form-control {
|
|
128
138
|
border: 1px solid var(--gray-light-color-ml);
|
|
@@ -511,13 +521,14 @@
|
|
|
511
521
|
}
|
|
512
522
|
|
|
513
523
|
.folder-row .file-icon {
|
|
514
|
-
background: linear-gradient(
|
|
515
|
-
color: #
|
|
516
|
-
|
|
524
|
+
background: linear-gradient(160deg, #fff5db 0%, #ffe7b2 100%);
|
|
525
|
+
color: #b45309;
|
|
526
|
+
border: 1px solid rgba(180, 83, 9, 0.2);
|
|
527
|
+
box-shadow: 0 3px 10px rgba(180, 83, 9, 0.14);
|
|
517
528
|
}
|
|
518
529
|
|
|
519
530
|
.folder-row:hover .file-icon {
|
|
520
|
-
box-shadow: 0
|
|
531
|
+
box-shadow: 0 8px 18px rgba(180, 83, 9, 0.24);
|
|
521
532
|
}
|
|
522
533
|
|
|
523
534
|
.breadcrumb-nav {
|
|
@@ -561,22 +572,28 @@
|
|
|
561
572
|
}
|
|
562
573
|
|
|
563
574
|
.file-icon {
|
|
564
|
-
width:
|
|
565
|
-
height:
|
|
566
|
-
background: linear-gradient(
|
|
567
|
-
border
|
|
575
|
+
width: 42px;
|
|
576
|
+
height: 42px;
|
|
577
|
+
background: linear-gradient(160deg, #ecfdf8 0%, #d9f7ef 100%);
|
|
578
|
+
border: 1px solid rgba(13, 148, 136, 0.18);
|
|
579
|
+
border-radius: 12px;
|
|
568
580
|
display: flex;
|
|
569
581
|
align-items: center;
|
|
570
582
|
justify-content: center;
|
|
571
583
|
margin-right: 0.9rem;
|
|
572
|
-
color:
|
|
573
|
-
font-size: 1.
|
|
584
|
+
color: #0f766e;
|
|
585
|
+
font-size: 1.15rem;
|
|
574
586
|
flex-shrink: 0;
|
|
575
|
-
|
|
587
|
+
box-shadow: 0 3px 10px rgba(13, 148, 136, 0.12);
|
|
588
|
+
transition: transform 0.22s ease, box-shadow 0.22s ease, border-color 0.22s ease;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.file-icon i {
|
|
592
|
+
transition: transform 0.22s ease;
|
|
576
593
|
}
|
|
577
594
|
|
|
578
595
|
.file-row:hover .file-icon {
|
|
579
|
-
box-shadow: 0
|
|
596
|
+
box-shadow: 0 8px 18px rgba(13, 148, 136, 0.2);
|
|
580
597
|
}
|
|
581
598
|
|
|
582
599
|
.file-info {
|
|
@@ -653,53 +670,58 @@
|
|
|
653
670
|
|
|
654
671
|
/* File type specific styling - Enhanced gradients */
|
|
655
672
|
.file-icon.image-file {
|
|
656
|
-
background: linear-gradient(
|
|
657
|
-
color: #
|
|
658
|
-
|
|
673
|
+
background: linear-gradient(160deg, #fff6e3 0%, #ffe7bf 100%);
|
|
674
|
+
color: #b45309;
|
|
675
|
+
border-color: rgba(180, 83, 9, 0.22);
|
|
676
|
+
box-shadow: 0 3px 10px rgba(180, 83, 9, 0.16);
|
|
659
677
|
}
|
|
660
678
|
|
|
661
679
|
.file-icon.video-file {
|
|
662
|
-
background: linear-gradient(
|
|
663
|
-
color: #
|
|
664
|
-
|
|
680
|
+
background: linear-gradient(160deg, #ffe9e7 0%, #ffd1cc 100%);
|
|
681
|
+
color: #b91c1c;
|
|
682
|
+
border-color: rgba(185, 28, 28, 0.22);
|
|
683
|
+
box-shadow: 0 3px 10px rgba(185, 28, 28, 0.16);
|
|
665
684
|
}
|
|
666
685
|
|
|
667
686
|
.file-icon.audio-file {
|
|
668
|
-
background: linear-gradient(
|
|
669
|
-
color: #
|
|
670
|
-
|
|
687
|
+
background: linear-gradient(160deg, #e6efff 0%, #d6e5ff 100%);
|
|
688
|
+
color: #1d4ed8;
|
|
689
|
+
border-color: rgba(29, 78, 216, 0.22);
|
|
690
|
+
box-shadow: 0 3px 10px rgba(29, 78, 216, 0.16);
|
|
671
691
|
}
|
|
672
692
|
|
|
673
693
|
.file-icon.document-file {
|
|
674
|
-
background: linear-gradient(
|
|
675
|
-
color: #
|
|
676
|
-
|
|
694
|
+
background: linear-gradient(160deg, #e6faf9 0%, #c8f0ed 100%);
|
|
695
|
+
color: #0f766e;
|
|
696
|
+
border-color: rgba(15, 118, 110, 0.22);
|
|
697
|
+
box-shadow: 0 3px 10px rgba(15, 118, 110, 0.16);
|
|
677
698
|
}
|
|
678
699
|
|
|
679
700
|
.file-icon.code-file {
|
|
680
|
-
background: linear-gradient(
|
|
681
|
-
color: #
|
|
682
|
-
|
|
701
|
+
background: linear-gradient(160deg, #e9fbe9 0%, #d5f4d5 100%);
|
|
702
|
+
color: #166534;
|
|
703
|
+
border-color: rgba(22, 101, 52, 0.22);
|
|
704
|
+
box-shadow: 0 3px 10px rgba(22, 101, 52, 0.16);
|
|
683
705
|
}
|
|
684
706
|
|
|
685
707
|
.file-row:hover .file-icon.image-file {
|
|
686
|
-
box-shadow: 0
|
|
708
|
+
box-shadow: 0 8px 18px rgba(180, 83, 9, 0.24);
|
|
687
709
|
}
|
|
688
710
|
|
|
689
711
|
.file-row:hover .file-icon.video-file {
|
|
690
|
-
box-shadow: 0
|
|
712
|
+
box-shadow: 0 8px 18px rgba(185, 28, 28, 0.24);
|
|
691
713
|
}
|
|
692
714
|
|
|
693
715
|
.file-row:hover .file-icon.audio-file {
|
|
694
|
-
box-shadow: 0
|
|
716
|
+
box-shadow: 0 8px 18px rgba(29, 78, 216, 0.24);
|
|
695
717
|
}
|
|
696
718
|
|
|
697
719
|
.file-row:hover .file-icon.document-file {
|
|
698
|
-
box-shadow: 0
|
|
720
|
+
box-shadow: 0 8px 18px rgba(15, 118, 110, 0.24);
|
|
699
721
|
}
|
|
700
722
|
|
|
701
723
|
.file-row:hover .file-icon.code-file {
|
|
702
|
-
box-shadow: 0
|
|
724
|
+
box-shadow: 0 8px 18px rgba(22, 101, 52, 0.24);
|
|
703
725
|
}
|
|
704
726
|
|
|
705
727
|
/* Empty state - Modern design */
|
|
@@ -741,6 +763,96 @@
|
|
|
741
763
|
color: var(--gray-color-ml);
|
|
742
764
|
}
|
|
743
765
|
|
|
766
|
+
.empty-state-illustration {
|
|
767
|
+
margin-bottom: 1.5rem;
|
|
768
|
+
position: relative;
|
|
769
|
+
display: inline-block;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
.empty-state-search-icon {
|
|
773
|
+
font-size: 2rem;
|
|
774
|
+
color: var(--primary-color-ml);
|
|
775
|
+
position: absolute;
|
|
776
|
+
bottom: -5px;
|
|
777
|
+
right: -10px;
|
|
778
|
+
background: white;
|
|
779
|
+
border-radius: 50%;
|
|
780
|
+
padding: 5px;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
.empty-state-description {
|
|
784
|
+
max-width: 400px;
|
|
785
|
+
margin: 0 auto 1.5rem;
|
|
786
|
+
color: var(--text-muted-color-ml);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
.empty-state-actions {
|
|
790
|
+
display: flex;
|
|
791
|
+
gap: 0.75rem;
|
|
792
|
+
justify-content: center;
|
|
793
|
+
flex-wrap: wrap;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
.file-path-display {
|
|
797
|
+
font-size: 0.75rem;
|
|
798
|
+
color: var(--gray-color-ml);
|
|
799
|
+
margin-bottom: 2px;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
.sort-icon {
|
|
803
|
+
margin-left: 0.5rem;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
.sort-icon-inactive {
|
|
807
|
+
color: var(--gray-color-ml);
|
|
808
|
+
opacity: 0.3;
|
|
809
|
+
font-size: 0.8rem;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
.sort-icon-active {
|
|
813
|
+
color: var(--primary-color-ml);
|
|
814
|
+
font-size: 0.9rem;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
.bulk-actions-bar {
|
|
818
|
+
display: none;
|
|
819
|
+
padding: 0.5rem;
|
|
820
|
+
border-bottom: 1px solid var(--gray-light-color-ml);
|
|
821
|
+
background: var(--light-color-ml);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
.toast-close-btn {
|
|
825
|
+
background: none;
|
|
826
|
+
border: none;
|
|
827
|
+
color: white;
|
|
828
|
+
cursor: pointer;
|
|
829
|
+
opacity: 0.8;
|
|
830
|
+
font-size: 1.2rem;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
.upload-item-meta-row {
|
|
834
|
+
display: flex;
|
|
835
|
+
align-items: center;
|
|
836
|
+
gap: 0.75rem;
|
|
837
|
+
min-width: 0;
|
|
838
|
+
width: 100%;
|
|
839
|
+
margin-bottom: 5px;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
.upload-item-size-label {
|
|
843
|
+
white-space: nowrap;
|
|
844
|
+
color: var(--text-muted-color-ml);
|
|
845
|
+
font-size: 0.875rem;
|
|
846
|
+
margin-left: auto;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
.upload-item-status-row {
|
|
850
|
+
display: flex;
|
|
851
|
+
justify-content: space-between;
|
|
852
|
+
align-items: center;
|
|
853
|
+
gap: 0.5rem;
|
|
854
|
+
}
|
|
855
|
+
|
|
744
856
|
/* Modal styling - Modern design */
|
|
745
857
|
.modal {
|
|
746
858
|
position: fixed;
|
package/public/bucketadmin.js
CHANGED
|
@@ -8,9 +8,35 @@ let currentViewMode = 'folder'; // 'folder' or 'flat'
|
|
|
8
8
|
let pageFiles = [];
|
|
9
9
|
let pageFolders = [];
|
|
10
10
|
let currentNextToken = null;
|
|
11
|
+
let currentApiPrefix = '';
|
|
11
12
|
const bucketAppName = document.getElementById('bucketAppData')?.dataset.appName || '';
|
|
12
13
|
// Bucket is configured centrally on the server; client should not select a bucket.
|
|
13
14
|
|
|
15
|
+
function stripApiPrefixFromPath(path, apiPrefix = '') {
|
|
16
|
+
const normalizedPath = String(path || '').replace(/^\/+/, '');
|
|
17
|
+
const normalizedApiPrefix = String(apiPrefix || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
18
|
+
|
|
19
|
+
if (!normalizedApiPrefix) return normalizedPath;
|
|
20
|
+
if (normalizedPath === normalizedApiPrefix || normalizedPath === `${normalizedApiPrefix}/`) return '';
|
|
21
|
+
if (normalizedPath.startsWith(`${normalizedApiPrefix}/`)) {
|
|
22
|
+
return normalizedPath.slice(normalizedApiPrefix.length + 1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return normalizedPath;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function toApiFolderPath(prefix = '', apiPrefix = '') {
|
|
29
|
+
const uiPrefix = String(prefix || '').replace(/^\/+/, '');
|
|
30
|
+
const normalizedApiPrefix = String(apiPrefix || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
31
|
+
|
|
32
|
+
if (!uiPrefix) return '';
|
|
33
|
+
if (!normalizedApiPrefix) return uiPrefix;
|
|
34
|
+
if (uiPrefix === normalizedApiPrefix || uiPrefix.startsWith(`${normalizedApiPrefix}/`)) {
|
|
35
|
+
return uiPrefix;
|
|
36
|
+
}
|
|
37
|
+
return `${normalizedApiPrefix}/${uiPrefix}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
14
40
|
// Show alert message (Toast)
|
|
15
41
|
function showAlert(message, type = 'success') {
|
|
16
42
|
const container = document.getElementById('toast-container');
|
|
@@ -51,7 +77,7 @@ function showAlert(message, type = 'success') {
|
|
|
51
77
|
toast.innerHTML = `
|
|
52
78
|
<i class="fas ${isSuccess ? 'fa-check-circle' : 'fa-exclamation-circle'}" style="font-size: 1.25rem;"></i>
|
|
53
79
|
<div style="flex: 1;">${message}</div>
|
|
54
|
-
<button onclick="this.parentElement.remove()"
|
|
80
|
+
<button onclick="this.parentElement.remove()" class="toast-close-btn">×</button>
|
|
55
81
|
`;
|
|
56
82
|
|
|
57
83
|
container.appendChild(toast);
|
|
@@ -82,8 +108,9 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
|
|
|
82
108
|
try {
|
|
83
109
|
const parts = [];
|
|
84
110
|
if (prefix) {
|
|
111
|
+
const folderPathForUrl = toApiFolderPath(prefix, currentApiPrefix);
|
|
85
112
|
// preserve slashes in folder param for readability: encode then restore slashes
|
|
86
|
-
const folderEncoded = encodeURIComponent(
|
|
113
|
+
const folderEncoded = encodeURIComponent(folderPathForUrl).replace(/%2F/g, '/');
|
|
87
114
|
parts.push(`folder=${folderEncoded}`);
|
|
88
115
|
}
|
|
89
116
|
if (page && page > 1) parts.push(`page=${page}`);
|
|
@@ -139,6 +166,7 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
|
|
|
139
166
|
|
|
140
167
|
// Store next token
|
|
141
168
|
currentNextToken = data.nextContinuationToken || null;
|
|
169
|
+
currentApiPrefix = data.prefix || '';
|
|
142
170
|
|
|
143
171
|
// Update stats
|
|
144
172
|
if (data.totalFiles === -1) {
|
|
@@ -151,7 +179,7 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
|
|
|
151
179
|
filterInfo.textContent = `Search results for: "${search}"`;
|
|
152
180
|
fileListTitle.innerHTML = `Search Results <span class="text-muted">for: </span><span class="badge bg-info">${search}</span>`;
|
|
153
181
|
} else {
|
|
154
|
-
const label = prefix || bucketAppName || '
|
|
182
|
+
const label = prefix || bucketAppName || 'app';
|
|
155
183
|
filterInfo.textContent = `Filtered by folder: ${label}`;
|
|
156
184
|
fileListTitle.innerHTML = `Files <span class="text-muted">in: </span><span class="badge bg-info">${label}</span>`;
|
|
157
185
|
}
|
|
@@ -176,12 +204,16 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
|
|
|
176
204
|
pageFiles = data.files || [];
|
|
177
205
|
|
|
178
206
|
if (data.mode === 'optimized') {
|
|
179
|
-
|
|
207
|
+
pageFolders = (data.folders || []).map(folder => stripApiPrefixFromPath(folder, currentApiPrefix)).filter(Boolean);
|
|
180
208
|
} else {
|
|
181
|
-
|
|
209
|
+
pageFolders = search
|
|
210
|
+
? []
|
|
211
|
+
: extractFoldersAtCurrentLevel(data.files, currentApiPrefix || prefix)
|
|
212
|
+
.map(folder => stripApiPrefixFromPath(folder, currentApiPrefix))
|
|
213
|
+
.filter(Boolean);
|
|
182
214
|
}
|
|
183
215
|
|
|
184
|
-
|
|
216
|
+
renderFilesTable(pageFiles, pageFolders, prefix, currentApiPrefix);
|
|
185
217
|
renderPagination(data);
|
|
186
218
|
paginationContainer.style.display = 'block';
|
|
187
219
|
}
|
|
@@ -230,7 +262,7 @@ function toggleSort(field) {
|
|
|
230
262
|
return 0;
|
|
231
263
|
});
|
|
232
264
|
|
|
233
|
-
renderFilesTable(pageFiles, pageFolders, currentPrefix);
|
|
265
|
+
renderFilesTable(pageFiles, pageFolders, currentPrefix, currentApiPrefix);
|
|
234
266
|
}
|
|
235
267
|
}
|
|
236
268
|
|
|
@@ -299,7 +331,7 @@ function changeViewMode(mode) {
|
|
|
299
331
|
}
|
|
300
332
|
|
|
301
333
|
// Re-render with existing data
|
|
302
|
-
renderFilesTable(pageFiles, pageFolders, currentPrefix);
|
|
334
|
+
renderFilesTable(pageFiles, pageFolders, currentPrefix, currentApiPrefix);
|
|
303
335
|
}
|
|
304
336
|
|
|
305
337
|
// Extract folders that match a search query from file paths
|
|
@@ -331,7 +363,7 @@ function extractMatchingFolders(files, search) {
|
|
|
331
363
|
}
|
|
332
364
|
|
|
333
365
|
// Render files table
|
|
334
|
-
function renderFilesTable(files, folders = [], prefix = '') {
|
|
366
|
+
function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
335
367
|
// Determine which files/folders to show based on view mode
|
|
336
368
|
let displayFiles = [];
|
|
337
369
|
let displayFolders = [];
|
|
@@ -369,7 +401,8 @@ function renderFilesTable(files, folders = [], prefix = '') {
|
|
|
369
401
|
// Skip folder marker objects (keys ending with /)
|
|
370
402
|
if (file.Key.endsWith('/')) return false;
|
|
371
403
|
|
|
372
|
-
const
|
|
404
|
+
const basePrefix = apiPrefix || prefix;
|
|
405
|
+
const relativePath = basePrefix ? file.Key.substring(basePrefix.length + (basePrefix.endsWith('/') ? 0 : 1)) : file.Key;
|
|
373
406
|
// File is at current level if it has no '/' in relative path
|
|
374
407
|
return !relativePath.includes('/');
|
|
375
408
|
});
|
|
@@ -386,7 +419,7 @@ function renderFilesTable(files, folders = [], prefix = '') {
|
|
|
386
419
|
<div class="breadcrumb-nav">
|
|
387
420
|
<div class="breadcrumb-item">
|
|
388
421
|
<i class="fas fa-home"></i>
|
|
389
|
-
<span class="breadcrumb-link" onclick="loadFiles(1, '')" title="Go to
|
|
422
|
+
<span class="breadcrumb-link" onclick="loadFiles(1, '')" title="Go to ${bucketAppName || 'app'}">${bucketAppName || 'app'}</span>
|
|
390
423
|
</div>
|
|
391
424
|
`;
|
|
392
425
|
|
|
@@ -482,7 +515,7 @@ function renderFilesTable(files, folders = [], prefix = '') {
|
|
|
482
515
|
|
|
483
516
|
// If in flat mode, we might want to show the folder path more prominently
|
|
484
517
|
const pathDisplay = (currentViewMode === 'flat' && folderPath && folderPath !== prefix + (prefix ? '/' : ''))
|
|
485
|
-
? `<div
|
|
518
|
+
? `<div class="file-path-display">
|
|
486
519
|
<i class="fas fa-folder" style="margin-right:4px; opacity:0.5;"></i> ${folderPath}
|
|
487
520
|
</div>`
|
|
488
521
|
: '';
|
|
@@ -535,24 +568,25 @@ function renderFilesTable(files, folders = [], prefix = '') {
|
|
|
535
568
|
|
|
536
569
|
if (allRows === '') {
|
|
537
570
|
document.getElementById('fileListContainer').innerHTML = `
|
|
571
|
+
${breadcrumb}
|
|
538
572
|
<div class="empty-state">
|
|
539
|
-
<div
|
|
573
|
+
<div class="empty-state-illustration">
|
|
540
574
|
<i class="fas fa-folder-open" style="font-size: 4rem; color: #cbd5e0;"></i>
|
|
541
|
-
<i class="fas fa-search
|
|
575
|
+
<i class="fas fa-search empty-state-search-icon"></i>
|
|
542
576
|
</div>
|
|
543
577
|
<h4>No files found</h4>
|
|
544
|
-
<p
|
|
578
|
+
<p class="empty-state-description">
|
|
545
579
|
${prefix ? `This folder <strong>${prefix}</strong> is currently empty.` : 'Your bucket is empty. Start by uploading files or creating a new folder.'}
|
|
546
580
|
</p>
|
|
547
|
-
<div
|
|
581
|
+
<div class="empty-state-actions">
|
|
548
582
|
<button class="btn btn-primary" onclick="window.scrollTo({top: 0, behavior: 'smooth'}); document.getElementById('fileInput').click()">
|
|
549
583
|
<i class="fas fa-cloud-upload-alt"></i> Upload Files
|
|
550
584
|
</button>
|
|
551
585
|
<button class="btn btn-outline-secondary" onclick="createFolder()">
|
|
552
586
|
<i class="fas fa-folder-plus"></i> New Folder
|
|
553
587
|
</button>
|
|
554
|
-
|
|
555
|
-
|
|
588
|
+
${prefix ? `<button class="btn btn-light" onclick="loadFiles(1, '')">
|
|
589
|
+
<i class="fas fa-home"></i> Go to ${bucketAppName || 'app'}
|
|
556
590
|
</button>` : ''}
|
|
557
591
|
</div>
|
|
558
592
|
</div>
|
|
@@ -562,16 +596,16 @@ function renderFilesTable(files, folders = [], prefix = '') {
|
|
|
562
596
|
|
|
563
597
|
// Sort icon helper
|
|
564
598
|
const getSortIcon = (field) => {
|
|
565
|
-
if (currentSort !== field) return '<i class="fas fa-sort
|
|
599
|
+
if (currentSort !== field) return '<i class="fas fa-sort sort-icon sort-icon-inactive"></i>';
|
|
566
600
|
return currentOrder === 'asc'
|
|
567
|
-
? '<i class="fas fa-sort-up
|
|
568
|
-
: '<i class="fas fa-sort-down
|
|
601
|
+
? '<i class="fas fa-sort-up sort-icon sort-icon-active"></i>'
|
|
602
|
+
: '<i class="fas fa-sort-down sort-icon sort-icon-active"></i>';
|
|
569
603
|
};
|
|
570
604
|
|
|
571
605
|
document.getElementById('fileListContainer').innerHTML = `
|
|
572
606
|
${breadcrumb}
|
|
573
607
|
<div class="table-responsive">
|
|
574
|
-
<div id="bulkActionsBar"
|
|
608
|
+
<div id="bulkActionsBar" class="bulk-actions-bar">
|
|
575
609
|
<span id="bulkSelectedCount">0 selected</span>
|
|
576
610
|
<div style="float: right;">
|
|
577
611
|
<button class="btn btn-danger btn-sm" id="bulkDeleteBtn" disabled>
|
|
@@ -869,17 +903,17 @@ function renderUploadQueue() {
|
|
|
869
903
|
return `
|
|
870
904
|
<div class="upload-item" data-id="${item.id}">
|
|
871
905
|
<div class="meta">
|
|
872
|
-
<div
|
|
906
|
+
<div class="upload-item-meta-row">
|
|
873
907
|
<div class="file-icon"><i class="fas fa-file"></i></div>
|
|
874
908
|
<div class="name">${item.file.name}</div>
|
|
875
|
-
<div class="form-text file-size
|
|
909
|
+
<div class="form-text file-size upload-item-size-label">${formatFileSize(item.file.size)}</div>
|
|
876
910
|
</div>
|
|
877
911
|
</div>
|
|
878
912
|
<div style="min-width:220px;">
|
|
879
913
|
<div class="progress-container" style="height: 8px; margin-bottom: 6px;">
|
|
880
914
|
<div class="progress-bar" style="width: ${Math.round(item.progress)}%; height: 8px;"></div>
|
|
881
915
|
</div>
|
|
882
|
-
<div
|
|
916
|
+
<div class="upload-item-status-row">
|
|
883
917
|
<small class="text-muted status">${statusText}</small>
|
|
884
918
|
<div class="controls">${actionBtn}</div>
|
|
885
919
|
</div>
|
|
@@ -1142,7 +1176,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1142
1176
|
|
|
1143
1177
|
// Load initial files (support both legacy 'prefix' and new 'folder' query param)
|
|
1144
1178
|
const urlParams = new URLSearchParams(window.location.search);
|
|
1145
|
-
const initialPrefix = urlParams.get('folder') || urlParams.get('prefix') ||
|
|
1179
|
+
const initialPrefix = urlParams.get('folder') || urlParams.get('prefix') || '';
|
|
1146
1180
|
const initialPage = parseInt(urlParams.get('page')) || 1;
|
|
1147
1181
|
loadFiles(initialPage, initialPrefix);
|
|
1148
1182
|
|
|
@@ -1185,6 +1219,37 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1185
1219
|
return;
|
|
1186
1220
|
}
|
|
1187
1221
|
|
|
1222
|
+
const cbtn = e.target.closest('.copy-btn');
|
|
1223
|
+
console.log(cbtn);
|
|
1224
|
+
if (cbtn) {
|
|
1225
|
+
e.stopPropagation();
|
|
1226
|
+
const keyEnc = cbtn.dataset.keyEnc;
|
|
1227
|
+
if (!keyEnc) return;
|
|
1228
|
+
const link = `${window.location.origin}/mbkbucket/view/${keyEnc}`;
|
|
1229
|
+
const fallbackCopy = () => {
|
|
1230
|
+
try {
|
|
1231
|
+
const ta = document.createElement('textarea');
|
|
1232
|
+
ta.value = link;
|
|
1233
|
+
ta.style.position = 'fixed';
|
|
1234
|
+
ta.style.opacity = '0';
|
|
1235
|
+
document.body.appendChild(ta);
|
|
1236
|
+
ta.select();
|
|
1237
|
+
document.execCommand('copy');
|
|
1238
|
+
document.body.removeChild(ta);
|
|
1239
|
+
showAlert('Link copied to clipboard!', 'success');
|
|
1240
|
+
} catch {
|
|
1241
|
+
showAlert('Failed to copy link', 'error');
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1244
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
1245
|
+
navigator.clipboard.writeText(link).then(() => {
|
|
1246
|
+
showAlert('Link copied to clipboard!', 'success');
|
|
1247
|
+
}).catch(fallbackCopy);
|
|
1248
|
+
} else {
|
|
1249
|
+
fallbackCopy();
|
|
1250
|
+
}
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1188
1253
|
|
|
1189
1254
|
});
|
|
1190
1255
|
});
|
|
@@ -1209,25 +1274,32 @@ function handleViewFile(key, filename, event) {
|
|
|
1209
1274
|
const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico'];
|
|
1210
1275
|
const videoTypes = ['mp4', 'webm', 'ogg'];
|
|
1211
1276
|
|
|
1212
|
-
//
|
|
1277
|
+
// Determine preview mode from extension groups
|
|
1213
1278
|
const previewableImage = imageTypes.includes(extension);
|
|
1214
1279
|
const previewableVideo = videoTypes.includes(extension);
|
|
1215
1280
|
const previewableAudio = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a'].includes(extension);
|
|
1216
1281
|
const previewablePdf = extension === 'pdf';
|
|
1217
1282
|
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);
|
|
1218
1283
|
|
|
1284
|
+
let previewMode = 'unsupported';
|
|
1285
|
+
if (previewableImage) previewMode = 'image';
|
|
1286
|
+
else if (previewableVideo) previewMode = 'video';
|
|
1287
|
+
else if (previewableAudio) previewMode = 'audio';
|
|
1288
|
+
else if (previewablePdf) previewMode = 'pdf';
|
|
1289
|
+
else if (previewableText) previewMode = 'text';
|
|
1290
|
+
|
|
1219
1291
|
// Build an encoded key safely (if key already appears encoded, don't double-encode)
|
|
1220
1292
|
const encodedKey = (key && key.includes('%')) ? key : encodeURIComponent(key || filename);
|
|
1221
1293
|
|
|
1222
1294
|
// Show the inline preview
|
|
1223
|
-
showPreviewModal(encodedKey, filename, extension);
|
|
1295
|
+
showPreviewModal(encodedKey, filename, extension, previewMode);
|
|
1224
1296
|
return false;
|
|
1225
1297
|
}
|
|
1226
1298
|
|
|
1227
1299
|
// Function to get file type category
|
|
1228
1300
|
|
|
1229
1301
|
// Preview modal helpers
|
|
1230
|
-
function showPreviewModal(encodedKey, filename, extension) {
|
|
1302
|
+
function showPreviewModal(encodedKey, filename, extension, previewMode = 'auto') {
|
|
1231
1303
|
const modal = document.getElementById('previewModal');
|
|
1232
1304
|
if (!modal) {
|
|
1233
1305
|
window.open(`/mbkbucket/view/${encodedKey}`, '_blank', 'noopener,noreferrer');
|
|
@@ -1259,9 +1331,17 @@ function showPreviewModal(encodedKey, filename, extension) {
|
|
|
1259
1331
|
const videoTypes = ['mp4', 'webm', 'ogg'];
|
|
1260
1332
|
const audioTypes = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a'];
|
|
1261
1333
|
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'];
|
|
1334
|
+
const effectiveMode = previewMode === 'auto'
|
|
1335
|
+
? (imgTypes.includes(extension) ? 'image'
|
|
1336
|
+
: videoTypes.includes(extension) ? 'video'
|
|
1337
|
+
: audioTypes.includes(extension) ? 'audio'
|
|
1338
|
+
: extension === 'pdf' ? 'pdf'
|
|
1339
|
+
: textTypes.includes(extension) ? 'text'
|
|
1340
|
+
: 'unsupported')
|
|
1341
|
+
: previewMode;
|
|
1262
1342
|
|
|
1263
1343
|
// Render content and adjust dialog size
|
|
1264
|
-
if (
|
|
1344
|
+
if (effectiveMode === 'image') {
|
|
1265
1345
|
dialog.classList.add('preview-compact');
|
|
1266
1346
|
const img = document.createElement('img');
|
|
1267
1347
|
img.className = 'preview-img';
|
|
@@ -1271,7 +1351,7 @@ function showPreviewModal(encodedKey, filename, extension) {
|
|
|
1271
1351
|
img.onerror = () => { contentEl.textContent = 'Could not load preview.'; };
|
|
1272
1352
|
contentEl.innerHTML = '';
|
|
1273
1353
|
contentEl.appendChild(img);
|
|
1274
|
-
} else if (
|
|
1354
|
+
} else if (effectiveMode === 'video') {
|
|
1275
1355
|
dialog.classList.add('preview-large');
|
|
1276
1356
|
const video = document.createElement('video');
|
|
1277
1357
|
video.className = 'preview-video';
|
|
@@ -1279,7 +1359,7 @@ function showPreviewModal(encodedKey, filename, extension) {
|
|
|
1279
1359
|
video.src = `/mbkbucket/view/${encodedKey}`;
|
|
1280
1360
|
contentEl.innerHTML = '';
|
|
1281
1361
|
contentEl.appendChild(video);
|
|
1282
|
-
} else if (
|
|
1362
|
+
} else if (effectiveMode === 'audio') {
|
|
1283
1363
|
dialog.classList.add('preview-compact');
|
|
1284
1364
|
const audio = document.createElement('audio');
|
|
1285
1365
|
audio.className = 'preview-audio';
|
|
@@ -1287,14 +1367,14 @@ function showPreviewModal(encodedKey, filename, extension) {
|
|
|
1287
1367
|
audio.src = `/mbkbucket/view/${encodedKey}`;
|
|
1288
1368
|
contentEl.innerHTML = '';
|
|
1289
1369
|
contentEl.appendChild(audio);
|
|
1290
|
-
} else if (
|
|
1370
|
+
} else if (effectiveMode === 'pdf') {
|
|
1291
1371
|
dialog.classList.add('preview-large');
|
|
1292
1372
|
const iframe = document.createElement('iframe');
|
|
1293
1373
|
iframe.className = 'preview-iframe';
|
|
1294
1374
|
iframe.src = `/mbkbucket/view/${encodedKey}`;
|
|
1295
1375
|
contentEl.innerHTML = '';
|
|
1296
1376
|
contentEl.appendChild(iframe);
|
|
1297
|
-
} else if (
|
|
1377
|
+
} else if (effectiveMode === 'text') {
|
|
1298
1378
|
dialog.classList.add('preview-compact');
|
|
1299
1379
|
|
|
1300
1380
|
fetch(`/mbkbucket/view/${encodedKey}`)
|
package/views/bucket.handlebars
CHANGED
|
@@ -63,12 +63,12 @@
|
|
|
63
63
|
/* Removed unused sidebar/menu styles to reduce CSS size. */
|
|
64
64
|
|
|
65
65
|
.logo {
|
|
66
|
-
font-size:
|
|
66
|
+
font-size: 2rem;
|
|
67
67
|
font-weight: 700;
|
|
68
68
|
color: var(--primary-color-ml);
|
|
69
69
|
display: flex;
|
|
70
70
|
align-items: center;
|
|
71
|
-
gap: .
|
|
71
|
+
gap: .3rem
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
.logo img {
|
|
@@ -205,7 +205,7 @@
|
|
|
205
205
|
<nav class="top-nav">
|
|
206
206
|
<div class="logo">
|
|
207
207
|
<img onclick="window.location='/'" src="/icon.svg" alt="Logo">
|
|
208
|
-
<span onclick="window.location='/'" style="cursor:pointer;">
|
|
208
|
+
<span onclick="window.location='/'" style="cursor:pointer;">BKBucket</span>
|
|
209
209
|
</div>
|
|
210
210
|
|
|
211
211
|
{{> profilemenu }}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{{#if message}}
|
|
2
|
+
<div class="alert alert-success alert-dismissible fade show" role="alert" id="successAlert">
|
|
3
|
+
<i class="fas fa-check-circle"></i>
|
|
4
|
+
<span id="successMessage">{{message}}</span>
|
|
5
|
+
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">×</button>
|
|
6
|
+
</div>
|
|
7
|
+
{{/if}}
|
|
8
|
+
|
|
9
|
+
{{#if error}}
|
|
10
|
+
<div class="alert alert-danger alert-dismissible fade show" role="alert" id="errorAlert">
|
|
11
|
+
<i class="fas fa-exclamation-triangle"></i>
|
|
12
|
+
<span id="errorMessage">{{error}}</span>
|
|
13
|
+
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">×</button>
|
|
14
|
+
</div>
|
|
15
|
+
{{/if}}
|