mbkbucket 3.0.1 → 3.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/package.json +8 -8
- package/public/bucketadmin.css +1139 -2017
- package/public/bucketadmin.js +1065 -1977
- package/src/app.js +3 -5
- package/src/controllers/api.controller.js +19 -7
- package/src/controllers/dashboard.controller.js +11 -10
- package/src/controllers/view.controller.js +7 -3
- package/src/routes/api.routes.js +1 -1
- package/src/routes/view.routes.js +3 -3
- package/src/services/s3.service.js +10 -3
- package/src/utils/helpers.js +24 -0
- package/views/bucket.handlebars +59 -129
- package/views/bucketadmin_delete_modal.handlebars +14 -18
- package/views/bucketadmin_filelist_skeleton.handlebars +28 -16
- package/views/bucketadmin_newfolder_modal.handlebars +29 -0
- package/views/bucketadmin_preview_modal.handlebars +21 -14
- package/views/bucketadmin_upload_modal.handlebars +54 -0
- package/views/bucketadmincontent.handlebars +113 -143
package/public/bucketadmin.js
CHANGED
|
@@ -1,2134 +1,1222 @@
|
|
|
1
|
-
// Global state
|
|
2
|
-
let currentPage = 1;
|
|
3
|
-
let currentPrefix = '';
|
|
4
|
-
let currentSearch = '';
|
|
5
|
-
let currentSort = 'name';
|
|
6
|
-
let currentOrder = 'asc';
|
|
7
|
-
let currentViewMode = 'folder'; // 'folder' or 'flat'
|
|
8
|
-
let pageFiles = [];
|
|
9
|
-
let pageFolders = [];
|
|
10
|
-
let currentNextToken = null;
|
|
11
|
-
let currentApiPrefix = '';
|
|
12
|
-
const bucketAppName = document.getElementById('bucketAppData')?.dataset.appName || '';
|
|
13
|
-
const selectedBucket = new URLSearchParams(window.location.search).get('bucket') || '';
|
|
14
|
-
let activeBucket = selectedBucket;
|
|
15
|
-
const app = String(bucketAppName || '').toLowerCase();
|
|
16
|
-
const isRootApp = app === 'portal' || app === 'mbkbucket';const rootLabel = isRootApp ? 'root' : (bucketAppName || 'app');
|
|
17
|
-
let currentQuickFilter = 'all';
|
|
18
|
-
let virtualRenderLimit = 0;
|
|
19
|
-
const VIRTUAL_RENDER_THRESHOLD = 500;
|
|
20
|
-
const VIRTUAL_RENDER_BATCH = 250;
|
|
21
|
-
let lastRenderContext = null;
|
|
22
|
-
const MEDIA_EXTENSIONS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'mp4', 'webm', 'ogg', 'avi', 'mov', 'mp3', 'wav', 'flac', 'aac', 'm4a']);
|
|
23
|
-
const DOC_EXTENSIONS = new Set(['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'md', 'csv']);
|
|
24
|
-
const CODE_EXTENSIONS = new Set(['js', 'ts', 'html', 'htm', 'css', 'php', 'py', 'java', 'cpp', 'c', 'h', 'cs', 'rb', 'go', 'rs', 'sql', 'sh', 'bat', 'ps1', 'yaml', 'yml', 'toml', 'ini', 'conf', 'json', 'xml', 'log']);
|
|
25
|
-
|
|
26
|
-
function withBucketUrl(url) {
|
|
27
|
-
if (!activeBucket) return url;
|
|
28
|
-
const separator = url.includes('?') ? '&' : '?';
|
|
29
|
-
return `${url}${separator}bucket=${encodeURIComponent(activeBucket)}`;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function getViewUrl(encodedKey) {
|
|
33
|
-
return withBucketUrl(`/mbkbucket/view/${encodedKey}`);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function getDownloadUrl(encodedKey) {
|
|
37
|
-
return withBucketUrl(`/mbkbucket/download/${encodedKey}`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
1
|
/**
|
|
41
|
-
*
|
|
42
|
-
* (unlike <a href> navigation which cancels the previous download).
|
|
43
|
-
* @param {string} url - The download URL
|
|
44
|
-
* @param {string} filename - The filename to save as
|
|
2
|
+
* MBKBucket File Manager - Gentelella v4 UI & Client Logic
|
|
45
3
|
*/
|
|
46
|
-
async function downloadFileViaFetch(url, filename) {
|
|
47
|
-
try {
|
|
48
|
-
const response = await fetch(url);
|
|
49
|
-
if (!response.ok) {
|
|
50
|
-
const err = await response.json().catch(() => ({}));
|
|
51
|
-
throw new Error(err.message || err.error || `Download failed (${response.status})`);
|
|
52
|
-
}
|
|
53
|
-
const blob = await response.blob();
|
|
54
|
-
const objectUrl = URL.createObjectURL(blob);
|
|
55
|
-
const a = document.createElement('a');
|
|
56
|
-
a.href = objectUrl;
|
|
57
|
-
a.download = filename;
|
|
58
|
-
document.body.appendChild(a);
|
|
59
|
-
a.click();
|
|
60
|
-
document.body.removeChild(a);
|
|
61
|
-
// Revoke the object URL after a short delay to allow the browser to start the download
|
|
62
|
-
setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);
|
|
63
|
-
} catch (err) {
|
|
64
|
-
console.error('Download failed:', err);
|
|
65
|
-
alert('Download failed: ' + err.message);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
4
|
|
|
69
|
-
function
|
|
70
|
-
|
|
5
|
+
(function () {
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
// Global State
|
|
9
|
+
let currentPage = 1;
|
|
10
|
+
let currentPrefix = '';
|
|
11
|
+
let currentSearch = '';
|
|
12
|
+
let currentSort = 'name';
|
|
13
|
+
let currentOrder = 'asc';
|
|
14
|
+
let currentViewMode = localStorage.getItem('mbk_view_mode') || 'grid'; // 'grid' or 'list'
|
|
15
|
+
let currentQuickFilter = 'all'; // 'all', 'media', 'docs', 'code', 'starred'
|
|
16
|
+
let pageFiles = [];
|
|
17
|
+
let pageFolders = [];
|
|
18
|
+
let currentApiPrefix = '';
|
|
19
|
+
let currentNextToken = null;
|
|
20
|
+
let selectedFiles = new Set();
|
|
21
|
+
let starredFiles = new Set(JSON.parse(localStorage.getItem('mbk_starred_keys') || '[]'));
|
|
22
|
+
|
|
23
|
+
// Upload State
|
|
24
|
+
let uploadQueue = [];
|
|
25
|
+
let isUploading = false;
|
|
26
|
+
|
|
27
|
+
// DOM Elements & Config
|
|
28
|
+
const bucketAppData = document.getElementById('bucketAppData');
|
|
29
|
+
const bucketAppName = bucketAppData?.dataset.appName || '';
|
|
30
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
31
|
+
const selectedBucket = urlParams.get('bucket') || '';
|
|
32
|
+
let activeBucket = selectedBucket;
|
|
33
|
+
const app = String(bucketAppName || '').toLowerCase();
|
|
34
|
+
const isRootApp = app === 'portal' || app === 'mbkbucket';
|
|
35
|
+
const rootLabel = isRootApp ? 'My Drive' : (bucketAppName || 'Bucket Drive');
|
|
36
|
+
|
|
37
|
+
// File categories & extensions
|
|
38
|
+
const MEDIA_EXTS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'mp4', 'webm', 'ogg', 'avi', 'mov', 'mp3', 'wav', 'flac', 'aac', 'm4a']);
|
|
39
|
+
const DOC_EXTS = new Set(['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'md', 'csv', 'rtf']);
|
|
40
|
+
const CODE_EXTS = new Set(['js', 'ts', 'jsx', 'tsx', 'html', 'htm', 'css', 'scss', 'php', 'py', 'java', 'cpp', 'c', 'h', 'cs', 'rb', 'go', 'rs', 'sql', 'sh', 'bat', 'ps1', 'yaml', 'yml', 'toml', 'ini', 'conf', 'json', 'xml', 'log']);
|
|
41
|
+
|
|
42
|
+
const FILE_TYPES = {
|
|
43
|
+
pdf: { color: 'var(--red)', bg: 'var(--red-lt)', label: 'PDF' },
|
|
44
|
+
doc: { color: 'var(--blue)', bg: 'var(--blue-lt)', label: 'DOC' },
|
|
45
|
+
docx: { color: 'var(--blue)', bg: 'var(--blue-lt)', label: 'DOC' },
|
|
46
|
+
xls: { color: 'var(--green)', bg: 'var(--green-lt)', label: 'XLS' },
|
|
47
|
+
xlsx: { color: 'var(--green)', bg: 'var(--green-lt)', label: 'XLS' },
|
|
48
|
+
csv: { color: 'var(--green)', bg: 'var(--green-lt)', label: 'CSV' },
|
|
49
|
+
ppt: { color: 'var(--orange)', bg: 'rgba(247, 103, 7, 0.1)', label: 'PPT' },
|
|
50
|
+
pptx: { color: 'var(--orange)', bg: 'rgba(247, 103, 7, 0.1)', label: 'PPT' },
|
|
51
|
+
txt: { color: 'var(--text-secondary)', bg: 'var(--bg-surface-secondary)', label: 'TXT' },
|
|
52
|
+
md: { color: 'var(--purple)', bg: 'var(--purple-lt)', label: 'MD' },
|
|
53
|
+
jpg: { color: 'var(--yellow)', bg: 'var(--yellow-lt)', label: 'IMG' },
|
|
54
|
+
jpeg: { color: 'var(--yellow)', bg: 'var(--yellow-lt)', label: 'IMG' },
|
|
55
|
+
png: { color: 'var(--yellow)', bg: 'var(--yellow-lt)', label: 'IMG' },
|
|
56
|
+
gif: { color: 'var(--yellow)', bg: 'var(--yellow-lt)', label: 'GIF' },
|
|
57
|
+
webp: { color: 'var(--yellow)', bg: 'var(--yellow-lt)', label: 'IMG' },
|
|
58
|
+
svg: { color: 'var(--orange)', bg: 'rgba(247, 103, 7, 0.1)', label: 'SVG' },
|
|
59
|
+
mp4: { color: 'var(--azure)', bg: 'var(--azure-lt)', label: 'VID' },
|
|
60
|
+
webm: { color: 'var(--azure)', bg: 'var(--azure-lt)', label: 'VID' },
|
|
61
|
+
mov: { color: 'var(--azure)', bg: 'var(--azure-lt)', label: 'VID' },
|
|
62
|
+
mp3: { color: 'var(--purple)', bg: 'var(--purple-lt)', label: 'AUD' },
|
|
63
|
+
wav: { color: 'var(--purple)', bg: 'var(--purple-lt)', label: 'AUD' },
|
|
64
|
+
zip: { color: 'var(--text-secondary)', bg: 'var(--bg-surface-secondary)', label: 'ZIP' },
|
|
65
|
+
tar: { color: 'var(--text-secondary)', bg: 'var(--bg-surface-secondary)', label: 'TAR' },
|
|
66
|
+
gz: { color: 'var(--text-secondary)', bg: 'var(--bg-surface-secondary)', label: 'GZ' },
|
|
67
|
+
code: { color: 'var(--cyan)', bg: 'var(--cyan-lt)', label: 'CODE' }
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// SVGs
|
|
71
|
+
const ICONS = {
|
|
72
|
+
folder: '<svg class="folder-svg" viewBox="0 0 32 32" fill="var(--primary-lt)" stroke="var(--primary)" stroke-width="1.3"><path d="M4 8a2 2 0 012-2h6l3 3h11a2 2 0 012 2v13a2 2 0 01-2 2H6a2 2 0 01-2-2V8z"/></svg>',
|
|
73
|
+
folderSm: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 4a1 1 0 011-1h3l2 2h5a1 1 0 011 1v6a1 1 0 01-1 1H3a1 1 0 01-1-1V4z"/></svg>',
|
|
74
|
+
drive: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 6l4-4h4l4 4v8H2V6z"/></svg>',
|
|
75
|
+
starred: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 1l2 5 5 .5-4 3.5 1 5-4-2.5-4 2.5 1-5-4-3.5 5-.5z"/></svg>',
|
|
76
|
+
media: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="2" width="12" height="12" rx="2"/><circle cx="5.5" cy="5.5" r="1.5"/><path d="M14 10l-4-4-6 6"/></svg>',
|
|
77
|
+
docs: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 2h7l4 4v8a1 1 0 01-1 1H3a1 1 0 01-1-1V3a1 1 0 011-1z"/><path d="M10 2v4h4"/></svg>',
|
|
78
|
+
code: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M5 5L2 8l3 3M11 5l3 3-3 3M9 3L7 13"/></svg>',
|
|
79
|
+
more: '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><circle cx="8" cy="3" r="1.2"/><circle cx="8" cy="8" r="1.2"/><circle cx="8" cy="13" r="1.2"/></svg>'
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Helper Functions
|
|
83
|
+
function escapeHtml(str) {
|
|
84
|
+
return String(str || '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function escapeAttr(str) {
|
|
88
|
+
return escapeHtml(str);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getBaseName(path) {
|
|
92
|
+
const val = String(path || '').replace(/\/+$/, '');
|
|
93
|
+
const lastSlash = val.lastIndexOf('/');
|
|
94
|
+
return lastSlash === -1 ? val : val.substring(lastSlash + 1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getFileExt(fileName) {
|
|
98
|
+
const name = getBaseName(fileName);
|
|
99
|
+
const dot = name.lastIndexOf('.');
|
|
100
|
+
return dot > -1 ? name.slice(dot + 1).toLowerCase() : '';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function formatFileSize(bytes) {
|
|
104
|
+
if (!bytes || bytes === 0) return '0 B';
|
|
105
|
+
const k = 1024;
|
|
106
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
107
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
108
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function formatDate(dateStr) {
|
|
112
|
+
if (!dateStr) return '—';
|
|
113
|
+
const d = new Date(dateStr);
|
|
114
|
+
if (isNaN(d.getTime())) return String(dateStr);
|
|
115
|
+
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function withBucketUrl(url) {
|
|
119
|
+
if (!activeBucket) return url;
|
|
120
|
+
const sep = url.includes('?') ? '&' : '?';
|
|
121
|
+
return `${url}${sep}bucket=${encodeURIComponent(activeBucket)}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function getViewUrl(key) {
|
|
125
|
+
return withBucketUrl(`/mbkbucket/view/${encodeURIComponent(key)}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getDownloadUrl(key) {
|
|
129
|
+
return withBucketUrl(`/mbkbucket/download/${encodeURIComponent(key)}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isImage(fileName) {
|
|
133
|
+
const ext = getFileExt(fileName);
|
|
134
|
+
return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp'].includes(ext);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isVideo(fileName) {
|
|
138
|
+
const ext = getFileExt(fileName);
|
|
139
|
+
return ['mp4', 'webm', 'ogg', 'mov', 'mkv'].includes(ext);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isAudio(fileName) {
|
|
143
|
+
const ext = getFileExt(fileName);
|
|
144
|
+
return ['mp3', 'wav', 'ogg', 'flac', 'm4a', 'aac'].includes(ext);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function isTextOrCode(fileName) {
|
|
148
|
+
const ext = getFileExt(fileName);
|
|
149
|
+
return CODE_EXTS.has(ext) || ['txt', 'md', 'csv', 'log', 'json', 'xml', 'env'].includes(ext);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isPdf(fileName) {
|
|
153
|
+
return getFileExt(fileName) === 'pdf';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getFileTypeConfig(fileName) {
|
|
157
|
+
const ext = getFileExt(fileName);
|
|
158
|
+
if (FILE_TYPES[ext]) return FILE_TYPES[ext];
|
|
159
|
+
if (CODE_EXTS.has(ext)) return FILE_TYPES.code;
|
|
160
|
+
return { color: 'var(--text-secondary)', bg: 'var(--bg-surface-secondary)', label: ext.toUpperCase().slice(0, 4) || 'FILE' };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function stripApiPrefixFromPath(path, apiPrefix = '') {
|
|
164
|
+
const normalizedPath = String(path || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
71
165
|
const normalizedApiPrefix = String(apiPrefix || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
72
|
-
|
|
73
166
|
if (!normalizedApiPrefix) return normalizedPath;
|
|
74
167
|
if (normalizedPath === normalizedApiPrefix || normalizedPath === `${normalizedApiPrefix}/`) return '';
|
|
75
168
|
if (normalizedPath.startsWith(`${normalizedApiPrefix}/`)) {
|
|
76
|
-
|
|
169
|
+
return normalizedPath.slice(normalizedApiPrefix.length + 1);
|
|
77
170
|
}
|
|
78
|
-
|
|
79
171
|
return normalizedPath;
|
|
80
|
-
}
|
|
172
|
+
}
|
|
81
173
|
|
|
82
|
-
function toApiFolderPath(prefix = '', apiPrefix = '') {
|
|
83
|
-
const uiPrefix = String(prefix || '').replace(/^\/+/, '');
|
|
174
|
+
function toApiFolderPath(prefix = '', apiPrefix = '') {
|
|
175
|
+
const uiPrefix = String(prefix || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
84
176
|
const normalizedApiPrefix = String(apiPrefix || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
85
|
-
|
|
86
177
|
if (!uiPrefix) return '';
|
|
87
178
|
if (!normalizedApiPrefix) return uiPrefix;
|
|
88
|
-
if (uiPrefix === normalizedApiPrefix || uiPrefix.startsWith(`${normalizedApiPrefix}/`))
|
|
89
|
-
return uiPrefix;
|
|
90
|
-
}
|
|
179
|
+
if (uiPrefix === normalizedApiPrefix || uiPrefix.startsWith(`${normalizedApiPrefix}/`)) return uiPrefix;
|
|
91
180
|
return `${normalizedApiPrefix}/${uiPrefix}`;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function
|
|
114
|
-
return `data-${name}="${escapeAttr(value)}"`;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function mutedDash() {
|
|
118
|
-
return '<span class="muted-cell-value">—</span>';
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function syncStateUrl(page = currentPage, prefix = currentPrefix, search = currentSearch) {
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function extractFoldersAtCurrentLevel(files, prefix = '') {
|
|
184
|
+
const folderSet = new Set();
|
|
185
|
+
const cleanPrefix = String(prefix || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
186
|
+
const prefixOffset = cleanPrefix ? cleanPrefix.length + 1 : 0;
|
|
187
|
+
|
|
188
|
+
(files || []).forEach(file => {
|
|
189
|
+
const key = String(file.Key || '').replace(/^\/+/, '');
|
|
190
|
+
if (cleanPrefix && !key.startsWith(cleanPrefix + '/')) return;
|
|
191
|
+
const relPath = cleanPrefix ? key.substring(prefixOffset) : key;
|
|
192
|
+
const slashIdx = relPath.indexOf('/');
|
|
193
|
+
if (slashIdx !== -1) {
|
|
194
|
+
const folder = relPath.substring(0, slashIdx);
|
|
195
|
+
folderSet.add(cleanPrefix ? `${cleanPrefix}/${folder}` : folder);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return Array.from(folderSet).sort();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function syncStateUrl() {
|
|
122
203
|
try {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
window.history.replaceState(null, '', newUrl);
|
|
204
|
+
const parts = [];
|
|
205
|
+
if (activeBucket) parts.push(`bucket=${encodeURIComponent(activeBucket)}`);
|
|
206
|
+
if (currentPrefix) {
|
|
207
|
+
const folderPath = toApiFolderPath(currentPrefix, currentApiPrefix);
|
|
208
|
+
parts.push(`folder=${encodeURIComponent(folderPath).replace(/%2F/g, '/')}`);
|
|
209
|
+
}
|
|
210
|
+
if (currentPage > 1) parts.push(`page=${currentPage}`);
|
|
211
|
+
if (currentSearch) parts.push(`search=${encodeURIComponent(currentSearch)}`);
|
|
212
|
+
if (currentSort !== 'name') parts.push(`sort=${encodeURIComponent(currentSort)}`);
|
|
213
|
+
if (currentOrder !== 'asc') parts.push(`order=${encodeURIComponent(currentOrder)}`);
|
|
214
|
+
if (currentViewMode !== 'grid') parts.push(`view=${encodeURIComponent(currentViewMode)}`);
|
|
215
|
+
if (currentQuickFilter !== 'all') parts.push(`filter=${encodeURIComponent(currentQuickFilter)}`);
|
|
216
|
+
|
|
217
|
+
const newUrl = '/mbkbucket' + (parts.length ? '?' + parts.join('&') : '');
|
|
218
|
+
window.history.replaceState(null, '', newUrl);
|
|
139
219
|
} catch (e) {
|
|
140
|
-
|
|
220
|
+
console.warn('History update failed', e);
|
|
141
221
|
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function getActiveBucketLabel() {
|
|
145
|
-
return activeBucket || 'default bucket';
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function matchesQuickFilter(fileName = '', filter = currentQuickFilter) {
|
|
149
|
-
const ext = getBaseName(fileName).split('.').pop().toLowerCase();
|
|
150
|
-
if (filter === 'media') return MEDIA_EXTENSIONS.has(ext);
|
|
151
|
-
if (filter === 'docs') return DOC_EXTENSIONS.has(ext);
|
|
152
|
-
if (filter === 'code') return CODE_EXTENSIONS.has(ext);
|
|
153
|
-
return true;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function updateQuickFilterUI() {
|
|
157
|
-
document.querySelectorAll('.quick-filter-btn').forEach((btn) => {
|
|
158
|
-
btn.classList.toggle('active', btn.dataset.filter === currentQuickFilter);
|
|
159
|
-
});
|
|
160
|
-
}
|
|
222
|
+
}
|
|
161
223
|
|
|
162
|
-
//
|
|
163
|
-
function
|
|
224
|
+
// Toast System
|
|
225
|
+
function showToast(message, type = 'success') {
|
|
164
226
|
const container = document.getElementById('toast-container');
|
|
165
|
-
if (!container)
|
|
166
|
-
// Fallback to old method if container missing
|
|
167
|
-
const alertId = type === 'success' ? 'successAlert' : 'errorAlert';
|
|
168
|
-
const messageId = type === 'success' ? 'successMessage' : 'errorMessage';
|
|
169
|
-
const alertEl = document.getElementById(alertId);
|
|
170
|
-
const messageEl = document.getElementById(messageId);
|
|
171
|
-
if (alertEl && messageEl) {
|
|
172
|
-
messageEl.textContent = message;
|
|
173
|
-
alertEl.style.display = 'flex';
|
|
174
|
-
setTimeout(() => alertEl.style.display = 'none', 5000);
|
|
175
|
-
}
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
227
|
+
if (!container) return;
|
|
178
228
|
|
|
179
|
-
// Create toast element
|
|
180
229
|
const toast = document.createElement('div');
|
|
181
|
-
const isSuccess = type === 'success';
|
|
182
230
|
toast.className = `toast-notification ${type}`;
|
|
183
|
-
toast.style.cssText = `
|
|
184
|
-
background: ${isSuccess ? 'linear-gradient(135deg, #10b981, #059669)' : 'linear-gradient(135deg, #ef4444, #dc2626)'};
|
|
185
|
-
color: white;
|
|
186
|
-
padding: 1rem 1.5rem;
|
|
187
|
-
border-radius: 8px;
|
|
188
|
-
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
189
|
-
display: flex;
|
|
190
|
-
align-items: center;
|
|
191
|
-
gap: 12px;
|
|
192
|
-
min-width: 300px;
|
|
193
|
-
transform: translateX(100%);
|
|
194
|
-
opacity: 0;
|
|
195
|
-
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
196
|
-
font-weight: 500;
|
|
197
|
-
`;
|
|
198
|
-
|
|
199
231
|
toast.innerHTML = `
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
<button type="button" data-action="close-toast" class="toast-close-btn">×</button>
|
|
232
|
+
<span>${escapeHtml(message)}</span>
|
|
233
|
+
<button type="button" class="toast-close-btn">×</button>
|
|
203
234
|
`;
|
|
204
235
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
// Animate in
|
|
208
|
-
requestAnimationFrame(() => {
|
|
209
|
-
toast.style.transform = 'translateX(0)';
|
|
210
|
-
toast.style.opacity = '1';
|
|
236
|
+
toast.querySelector('.toast-close-btn').addEventListener('click', () => {
|
|
237
|
+
toast.remove();
|
|
211
238
|
});
|
|
212
239
|
|
|
213
|
-
|
|
214
|
-
setTimeout(() => {
|
|
215
|
-
toast.style.transform = 'translateX(100%)';
|
|
216
|
-
toast.style.opacity = '0';
|
|
217
|
-
setTimeout(() => toast.remove(), 300);
|
|
218
|
-
}, 5000);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Fetch and render files
|
|
222
|
-
async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort, order = currentOrder, token = '') {
|
|
223
|
-
currentPage = page;
|
|
224
|
-
currentPrefix = prefix;
|
|
225
|
-
currentSearch = search;
|
|
226
|
-
currentSort = sort;
|
|
227
|
-
currentOrder = order;
|
|
228
|
-
virtualRenderLimit = 0;
|
|
229
|
-
|
|
230
|
-
// Update browser URL so navigation is shareable/bookmarkable.
|
|
231
|
-
syncStateUrl(page, prefix, search);
|
|
240
|
+
container.appendChild(toast);
|
|
232
241
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
242
|
+
setTimeout(() => {
|
|
243
|
+
toast.style.opacity = '0';
|
|
244
|
+
toast.style.transform = 'translateY(10px)';
|
|
245
|
+
setTimeout(() => toast.remove(), 250);
|
|
246
|
+
}, 4000);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Tree Renderer
|
|
250
|
+
function renderTree() {
|
|
251
|
+
const treeEl = document.getElementById('fm-tree');
|
|
252
|
+
if (!treeEl) return;
|
|
253
|
+
|
|
254
|
+
const allFolders = pageFolders || [];
|
|
255
|
+
const isStarredActive = currentQuickFilter === 'starred';
|
|
256
|
+
const isRootActive = !currentPrefix && currentQuickFilter === 'all';
|
|
257
|
+
|
|
258
|
+
let treeHtml = `
|
|
259
|
+
<div class="tree-group">
|
|
260
|
+
<button type="button" class="tree-link ${isRootActive ? 'active' : ''}" data-tree-prefix="" data-tree-filter="all">
|
|
261
|
+
${ICONS.drive}
|
|
262
|
+
<span>${escapeHtml(rootLabel)}</span>
|
|
263
|
+
</button>
|
|
254
264
|
</div>
|
|
255
265
|
`;
|
|
256
266
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
// Update stats
|
|
278
|
-
if (data.totalFiles === -1) {
|
|
279
|
-
totalFilesCount.textContent = `${(data.files.length + (data.folders ? data.folders.length : 0))}${data.hasNextPage ? '+' : ''} Items`;
|
|
280
|
-
} else {
|
|
281
|
-
totalFilesCount.textContent = `${data.totalFiles} ${search ? 'Results' : 'Files'}`;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (search) {
|
|
285
|
-
const filterSuffix = currentQuickFilter !== 'all' ? ` • Filter: ${currentQuickFilter}` : '';
|
|
286
|
-
filterInfo.textContent = `Bucket: ${getActiveBucketLabel()} • Search: "${search}"${filterSuffix}`;
|
|
287
|
-
fileListTitle.innerHTML = `Search Results <span class="text-muted">for: </span><span class="badge bg-info">${search}</span>`;
|
|
288
|
-
} else {
|
|
289
|
-
const label = prefix || rootLabel;
|
|
290
|
-
const filterSuffix = currentQuickFilter !== 'all' ? ` • Filter: ${currentQuickFilter}` : '';
|
|
291
|
-
filterInfo.textContent = `Bucket: ${getActiveBucketLabel()} • Folder: ${label}${filterSuffix}`;
|
|
292
|
-
fileListTitle.innerHTML = `Files <span class="text-muted">in: </span><span class="badge bg-info">${label}</span>`;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Render files or empty state
|
|
296
|
-
const hasContent = (data.files && data.files.length > 0) || (data.folders && data.folders.length > 0);
|
|
297
|
-
|
|
298
|
-
if (!hasContent && !prefix && !search) {
|
|
299
|
-
container.innerHTML = `
|
|
300
|
-
<div class="empty-state">
|
|
301
|
-
<i class="fas fa-folder-open"></i>
|
|
302
|
-
<h4>No files found</h4>
|
|
303
|
-
<p>Bucket <strong>${escapeHtml(getActiveBucketLabel())}</strong> is empty. Upload some files to get started!</p>
|
|
304
|
-
<button class="btn btn-lg" data-action="upload-files">
|
|
305
|
-
<i class="fas fa-plus"></i> Upload Your First File
|
|
306
|
-
</button>
|
|
267
|
+
// Folder hierarchy
|
|
268
|
+
if (allFolders.length > 0) {
|
|
269
|
+
treeHtml += `
|
|
270
|
+
<div class="tree-group">
|
|
271
|
+
<div class="tree-subs">
|
|
272
|
+
${allFolders.map(folder => {
|
|
273
|
+
const cleanFolder = String(folder || '').replace(/\/+$/, '');
|
|
274
|
+
const folderName = getBaseName(cleanFolder);
|
|
275
|
+
const folderPathForAction = currentPrefix && !cleanFolder.startsWith(currentPrefix + '/') && cleanFolder !== currentPrefix
|
|
276
|
+
? `${currentPrefix}/${cleanFolder}`
|
|
277
|
+
: cleanFolder;
|
|
278
|
+
const isFolderActive = currentPrefix === folderPathForAction;
|
|
279
|
+
return `
|
|
280
|
+
<button type="button" class="tree-link tree-sub ${isFolderActive ? 'active' : ''}" data-tree-prefix="${escapeAttr(folderPathForAction)}" data-tree-filter="all">
|
|
281
|
+
${ICONS.folderSm}
|
|
282
|
+
<span>${escapeHtml(folderName)}</span>
|
|
283
|
+
</button>
|
|
284
|
+
`;
|
|
285
|
+
}).join('')}
|
|
307
286
|
</div>
|
|
308
|
-
`;
|
|
309
|
-
paginationContainer.style.display = 'none';
|
|
310
|
-
} else {
|
|
311
|
-
// Store for client-side sorting and view switching
|
|
312
|
-
pageFiles = data.files || [];
|
|
313
|
-
|
|
314
|
-
if (data.mode === 'optimized') {
|
|
315
|
-
pageFolders = (data.folders || []).map(folder => stripApiPrefixFromPath(folder, currentApiPrefix)).filter(Boolean);
|
|
316
|
-
} else {
|
|
317
|
-
pageFolders = search
|
|
318
|
-
? []
|
|
319
|
-
: extractFoldersAtCurrentLevel(data.files, currentApiPrefix || prefix)
|
|
320
|
-
.map(folder => stripApiPrefixFromPath(folder, currentApiPrefix))
|
|
321
|
-
.filter(Boolean);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
renderFilesTable(pageFiles, pageFolders, prefix, currentApiPrefix);
|
|
325
|
-
renderPagination(data);
|
|
326
|
-
paginationContainer.style.display = 'block';
|
|
327
|
-
}
|
|
328
|
-
} catch (error) {
|
|
329
|
-
console.error("app: ", app);
|
|
330
|
-
console.error("isRootApp: ", isRootApp, "rootLabel: ", rootLabel);
|
|
331
|
-
console.error("bucketAppName : ", bucketAppName);
|
|
332
|
-
console.error('Error loading files:', error);
|
|
333
|
-
container.innerHTML = `
|
|
334
|
-
<div class="empty-state">
|
|
335
|
-
<i class="fas fa-exclamation-triangle empty-state-error-icon"></i>
|
|
336
|
-
<h4>Error Loading Files</h4>
|
|
337
|
-
<p>Bucket <strong>${escapeHtml(getActiveBucketLabel())}</strong>: ${escapeHtml(error.message)}</p>
|
|
338
|
-
<button class="btn btn-primary" data-action="retry-load" ${dataAttr('retry-page', page)} ${dataAttr('retry-prefix', prefix)}>
|
|
339
|
-
<i class="fas fa-redo"></i> Retry
|
|
340
|
-
</button>
|
|
341
287
|
</div>
|
|
342
288
|
`;
|
|
343
|
-
paginationContainer.style.display = 'none';
|
|
344
289
|
}
|
|
345
|
-
}
|
|
346
290
|
|
|
347
|
-
//
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
291
|
+
// Quick navigation filters
|
|
292
|
+
treeHtml += `
|
|
293
|
+
<div class="tree-group" style="margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--border-color-light);">
|
|
294
|
+
<button type="button" class="tree-link ${isStarredActive ? 'active' : ''}" data-tree-filter="starred">
|
|
295
|
+
${ICONS.starred}
|
|
296
|
+
<span>Starred</span>
|
|
297
|
+
${starredFiles.size > 0 ? `<span class="tree-badge">${starredFiles.size}</span>` : ''}
|
|
298
|
+
</button>
|
|
299
|
+
<button type="button" class="tree-link ${currentQuickFilter === 'media' ? 'active' : ''}" data-tree-filter="media">
|
|
300
|
+
${ICONS.media}
|
|
301
|
+
<span>Media</span>
|
|
302
|
+
</button>
|
|
303
|
+
<button type="button" class="tree-link ${currentQuickFilter === 'docs' ? 'active' : ''}" data-tree-filter="docs">
|
|
304
|
+
${ICONS.docs}
|
|
305
|
+
<span>Documents</span>
|
|
306
|
+
</button>
|
|
307
|
+
<button type="button" class="tree-link ${currentQuickFilter === 'code' ? 'active' : ''}" data-tree-filter="code">
|
|
308
|
+
${ICONS.code}
|
|
309
|
+
<span>Code</span>
|
|
310
|
+
</button>
|
|
311
|
+
</div>
|
|
312
|
+
`;
|
|
355
313
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
} else if (field === 'date') {
|
|
364
|
-
valA = new Date(a.LastModified || 0).getTime();
|
|
365
|
-
valB = new Date(b.LastModified || 0).getTime();
|
|
366
|
-
} else {
|
|
367
|
-
valA = getBaseName(a.Key).toLowerCase();
|
|
368
|
-
valB = getBaseName(b.Key).toLowerCase();
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
if (valA < valB) return currentOrder === 'asc' ? -1 : 1;
|
|
372
|
-
if (valA > valB) return currentOrder === 'asc' ? 1 : -1;
|
|
373
|
-
return 0;
|
|
374
|
-
});
|
|
314
|
+
treeEl.innerHTML = treeHtml;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Breadcrumbs Renderer
|
|
318
|
+
function renderBreadcrumbs() {
|
|
319
|
+
const bcEl = document.getElementById('fm-breadcrumb');
|
|
320
|
+
if (!bcEl) return;
|
|
375
321
|
|
|
376
|
-
|
|
377
|
-
|
|
322
|
+
if (!currentPrefix && currentQuickFilter === 'all') {
|
|
323
|
+
bcEl.innerHTML = `<button type="button" class="bc-link current">${escapeHtml(rootLabel)}</button>`;
|
|
324
|
+
return;
|
|
378
325
|
}
|
|
379
|
-
}
|
|
380
326
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
327
|
+
if (currentQuickFilter !== 'all') {
|
|
328
|
+
bcEl.innerHTML = `
|
|
329
|
+
<button type="button" class="bc-link" data-nav-prefix="">${escapeHtml(rootLabel)}</button>
|
|
330
|
+
<span class="sep">›</span>
|
|
331
|
+
<button type="button" class="bc-link current">${escapeHtml(currentQuickFilter.charAt(0).toUpperCase() + currentQuickFilter.slice(1))}</button>
|
|
332
|
+
`;
|
|
333
|
+
return;
|
|
387
334
|
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Clear search
|
|
391
|
-
function clearSearch() {
|
|
392
|
-
document.getElementById('searchInput').value = '';
|
|
393
|
-
loadFiles(1, currentPrefix, '');
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function setQuickFilter(filter = 'all') {
|
|
397
|
-
const nextFilter = String(filter || 'all').toLowerCase();
|
|
398
|
-
if (currentQuickFilter === nextFilter) return;
|
|
399
|
-
currentQuickFilter = nextFilter;
|
|
400
|
-
updateQuickFilterUI();
|
|
401
|
-
renderFilesTable(pageFiles, pageFolders, currentPrefix, currentApiPrefix);
|
|
402
|
-
syncStateUrl(currentPage, currentPrefix, currentSearch);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// Extract unique folders at current level
|
|
406
|
-
function extractFoldersAtCurrentLevel(files, prefix) {
|
|
407
|
-
const folderSet = new Set();
|
|
408
335
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
336
|
+
const parts = currentPrefix.split('/').filter(Boolean);
|
|
337
|
+
let accum = '';
|
|
338
|
+
const breadcrumbHtml = [
|
|
339
|
+
`<button type="button" class="bc-link" data-nav-prefix="">${escapeHtml(rootLabel)}</button>`
|
|
340
|
+
];
|
|
341
|
+
|
|
342
|
+
parts.forEach((part, index) => {
|
|
343
|
+
accum += (accum ? '/' : '') + part;
|
|
344
|
+
const isLast = index === parts.length - 1;
|
|
345
|
+
breadcrumbHtml.push('<span class="sep">›</span>');
|
|
346
|
+
if (isLast) {
|
|
347
|
+
breadcrumbHtml.push(`<button type="button" class="bc-link current">${escapeHtml(part)}</button>`);
|
|
348
|
+
} else {
|
|
349
|
+
breadcrumbHtml.push(`<button type="button" class="bc-link" data-nav-prefix="${escapeAttr(accum)}">${escapeHtml(part)}</button>`);
|
|
350
|
+
}
|
|
421
351
|
});
|
|
422
352
|
|
|
423
|
-
|
|
424
|
-
}
|
|
353
|
+
bcEl.innerHTML = breadcrumbHtml.join('');
|
|
354
|
+
}
|
|
425
355
|
|
|
426
|
-
//
|
|
427
|
-
function
|
|
428
|
-
|
|
429
|
-
currentViewMode = mode;
|
|
356
|
+
// File Filtering Logic
|
|
357
|
+
function getFilteredFiles() {
|
|
358
|
+
let list = (pageFiles || []).filter(f => !String(f.Key || '').endsWith('/'));
|
|
430
359
|
|
|
431
|
-
//
|
|
432
|
-
|
|
433
|
-
|
|
360
|
+
// Search filter
|
|
361
|
+
if (currentSearch) {
|
|
362
|
+
const q = currentSearch.toLowerCase();
|
|
363
|
+
list = list.filter(f => getBaseName(f.Key).toLowerCase().includes(q));
|
|
364
|
+
}
|
|
434
365
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
366
|
+
// Quick filter
|
|
367
|
+
if (currentQuickFilter === 'media') {
|
|
368
|
+
list = list.filter(f => MEDIA_EXTS.has(getFileExt(f.Key)));
|
|
369
|
+
} else if (currentQuickFilter === 'docs') {
|
|
370
|
+
list = list.filter(f => DOC_EXTS.has(getFileExt(f.Key)));
|
|
371
|
+
} else if (currentQuickFilter === 'code') {
|
|
372
|
+
list = list.filter(f => CODE_EXTS.has(getFileExt(f.Key)));
|
|
373
|
+
} else if (currentQuickFilter === 'starred') {
|
|
374
|
+
list = list.filter(f => starredFiles.has(f.Key));
|
|
375
|
+
}
|
|
439
376
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
btnFlat.classList.add('btn-outline-light');
|
|
443
|
-
} else {
|
|
444
|
-
btnFlat.classList.add('active');
|
|
445
|
-
btnFlat.classList.remove('btn-outline-light');
|
|
446
|
-
btnFlat.classList.add('btn-light');
|
|
377
|
+
return list;
|
|
378
|
+
}
|
|
447
379
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
380
|
+
function getFilteredFolders() {
|
|
381
|
+
if (currentQuickFilter !== 'all') return [];
|
|
382
|
+
if (currentSearch) {
|
|
383
|
+
const q = currentSearch.toLowerCase();
|
|
384
|
+
return (pageFolders || []).filter(f => getBaseName(f).toLowerCase().includes(q));
|
|
451
385
|
}
|
|
386
|
+
return pageFolders || [];
|
|
387
|
+
}
|
|
452
388
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
if (!lastRenderContext) return;
|
|
459
|
-
virtualRenderLimit += VIRTUAL_RENDER_BATCH;
|
|
460
|
-
renderFilesTable(lastRenderContext.files, lastRenderContext.folders, lastRenderContext.prefix, lastRenderContext.apiPrefix);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// Extract folders that match a search query from file paths
|
|
464
|
-
function extractMatchingFolders(files, search) {
|
|
465
|
-
if (!search) return [];
|
|
466
|
-
|
|
467
|
-
const searchLower = search.toLowerCase();
|
|
468
|
-
const folderSet = new Set();
|
|
469
|
-
|
|
470
|
-
files.forEach(file => {
|
|
471
|
-
const parts = file.Key.split('/');
|
|
472
|
-
// Remove filename
|
|
473
|
-
parts.pop();
|
|
474
|
-
|
|
475
|
-
let currentPath = '';
|
|
476
|
-
parts.forEach(part => {
|
|
477
|
-
const segmentPath = currentPath ? `${currentPath}/${part}` : part;
|
|
478
|
-
currentPath = segmentPath;
|
|
479
|
-
|
|
480
|
-
// If any part of the path matches the search, include that full path as a folder
|
|
481
|
-
// AND ensure we don't include the full file path, just the directory
|
|
482
|
-
if (part.toLowerCase().includes(searchLower)) {
|
|
483
|
-
folderSet.add(currentPath);
|
|
484
|
-
}
|
|
485
|
-
});
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
return Array.from(folderSet).sort();
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// Render files table
|
|
492
|
-
function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
493
|
-
lastRenderContext = { files, folders, prefix, apiPrefix };
|
|
494
|
-
const normalizedPrefix = String(prefix || '').replace(/\/+$/, '');
|
|
495
|
-
const basePrefix = apiPrefix || prefix;
|
|
496
|
-
const relativeBaseOffset = basePrefix ? basePrefix.length + (basePrefix.endsWith('/') ? 0 : 1) : 0;
|
|
497
|
-
|
|
498
|
-
// Determine which files/folders to show based on view mode
|
|
499
|
-
let displayFiles = [];
|
|
500
|
-
let displayFolders = [];
|
|
501
|
-
|
|
502
|
-
// Force flat view if searching
|
|
503
|
-
const isSearchActive = !!currentSearch;
|
|
504
|
-
const mode = isSearchActive ? 'flat' : currentViewMode;
|
|
505
|
-
|
|
506
|
-
if (mode === 'flat') {
|
|
507
|
-
// In flat mode, show all files in the current loaded page/list regardless of folder depth
|
|
508
|
-
if (isSearchActive) {
|
|
509
|
-
// Search Mode:
|
|
510
|
-
// 1. Show files where FILENAME matches the search term
|
|
511
|
-
const searchLower = currentSearch.toLowerCase();
|
|
512
|
-
displayFiles = files.filter(f => {
|
|
513
|
-
// Skip folder marker objects (keys ending with /)
|
|
514
|
-
if (f.Key.endsWith('/')) return false;
|
|
515
|
-
|
|
516
|
-
const fileName = getBaseName(f.Key);
|
|
517
|
-
return fileName.toLowerCase().includes(searchLower);
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
// 2. Show folders where FOLDER NAME matches the search term
|
|
521
|
-
displayFolders = extractMatchingFolders(files, currentSearch);
|
|
522
|
-
} else {
|
|
523
|
-
// Normal Flat Mode: Show all files, no folders
|
|
524
|
-
// Filter out folder marker objects (keys ending with /)
|
|
525
|
-
displayFiles = files.filter(f => !f.Key.endsWith('/'));
|
|
526
|
-
displayFolders = [];
|
|
527
|
-
}
|
|
528
|
-
} else {
|
|
529
|
-
// Normal Folder View logic
|
|
530
|
-
// Filter files to show only those at current level (not in subfolders)
|
|
531
|
-
displayFiles = files.filter(file => {
|
|
532
|
-
// Skip folder marker objects (keys ending with /)
|
|
533
|
-
if (file.Key.endsWith('/')) return false;
|
|
534
|
-
|
|
535
|
-
const relativePath = basePrefix ? file.Key.substring(relativeBaseOffset) : file.Key;
|
|
536
|
-
// File is at current level if it has no '/' in relative path
|
|
537
|
-
return !relativePath.includes('/');
|
|
538
|
-
});
|
|
539
|
-
displayFolders = folders;
|
|
540
|
-
}
|
|
389
|
+
// Main Files & Grid Renderer
|
|
390
|
+
function renderFiles() {
|
|
391
|
+
const gridEl = document.getElementById('fm-grid');
|
|
392
|
+
const emptyEl = document.getElementById('fm-empty');
|
|
393
|
+
if (!gridEl) return;
|
|
541
394
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
const name = getBaseName(f?.Key);
|
|
545
|
-
return matchesQuickFilter(name, currentQuickFilter);
|
|
546
|
-
});
|
|
395
|
+
const files = getFilteredFiles();
|
|
396
|
+
const folders = getFilteredFolders();
|
|
547
397
|
|
|
548
|
-
//
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
if (prefix) {
|
|
553
|
-
const parts = prefix.split('/').filter(p => p);
|
|
554
|
-
const safeRootLabel = escapeHtml(rootLabel);
|
|
555
|
-
const safeRootTitle = escapeAttr(rootLabel);
|
|
556
|
-
const breadcrumbParts = [`
|
|
557
|
-
<div class="breadcrumb-nav">
|
|
558
|
-
<div class="breadcrumb-item">
|
|
559
|
-
<i class="fas fa-home"></i>
|
|
560
|
-
<button type="button" class="breadcrumb-link" ${dataAttr('nav-prefix', '')} title="Go to ${safeRootTitle}">${safeRootLabel}</button>
|
|
561
|
-
</div>
|
|
562
|
-
`];
|
|
563
|
-
|
|
564
|
-
let currentPath = '';
|
|
565
|
-
parts.forEach((part, index) => {
|
|
566
|
-
currentPath += (currentPath ? '/' : '') + part;
|
|
567
|
-
const isLast = index === parts.length - 1;
|
|
568
|
-
|
|
569
|
-
if (isLast) {
|
|
570
|
-
const safePart = escapeHtml(part);
|
|
571
|
-
breadcrumbParts.push(`
|
|
572
|
-
<span class="breadcrumb-separator"><i class="fas fa-chevron-right"></i></span>
|
|
573
|
-
<div class="breadcrumb-item">
|
|
574
|
-
<span class="breadcrumb-current">${safePart}</span>
|
|
575
|
-
</div>
|
|
576
|
-
`);
|
|
577
|
-
} else {
|
|
578
|
-
const pathCopy = currentPath;
|
|
579
|
-
const safePart = escapeHtml(part);
|
|
580
|
-
const safePathTitle = escapeAttr(pathCopy);
|
|
581
|
-
breadcrumbParts.push(`
|
|
582
|
-
<span class="breadcrumb-separator"><i class="fas fa-chevron-right"></i></span>
|
|
583
|
-
<div class="breadcrumb-item">
|
|
584
|
-
<button type="button" class="breadcrumb-link" ${dataAttr('nav-prefix', pathCopy)} title="Go to ${safePathTitle}">${safePart}</button>
|
|
585
|
-
</div>
|
|
586
|
-
`);
|
|
587
|
-
}
|
|
588
|
-
});
|
|
398
|
+
// Update stats
|
|
399
|
+
const totalCountEl = document.getElementById('totalFilesCount');
|
|
400
|
+
const filterInfoEl = document.getElementById('filterInfo');
|
|
401
|
+
const totalItems = files.length + folders.length;
|
|
589
402
|
|
|
590
|
-
|
|
591
|
-
|
|
403
|
+
if (totalCountEl) {
|
|
404
|
+
totalCountEl.textContent = `${totalItems} item${totalItems === 1 ? '' : 's'}`;
|
|
405
|
+
}
|
|
406
|
+
if (filterInfoEl) {
|
|
407
|
+
filterInfoEl.textContent = activeBucket ? `Bucket: ${activeBucket}` : 'Default bucket';
|
|
592
408
|
}
|
|
593
409
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
const folderKeyForDelete = `${folderPathForAction}/`;
|
|
603
|
-
const folderName = getBaseName(cleanPath);
|
|
604
|
-
const safeFolderName = escapeHtml(folderName);
|
|
605
|
-
const safeFolderPathAttr = escapeAttr(`${folderPathForAction}/`);
|
|
606
|
-
const safeFolderDeleteKeyAttr = escapeAttr(folderKeyForDelete);
|
|
607
|
-
|
|
608
|
-
return `
|
|
609
|
-
<tr class="folder-row" data-folder-path="${safeFolderPathAttr}" title="Open folder: ${safeFolderName}">
|
|
610
|
-
<td class="table-cell-center">
|
|
611
|
-
<!-- no selection for folders -->
|
|
612
|
-
</td>
|
|
613
|
-
<td class="file-name-cell">
|
|
614
|
-
<div class="file-row-main">
|
|
615
|
-
<div class="file-icon">
|
|
616
|
-
<i class="fas fa-folder"></i>
|
|
617
|
-
</div>
|
|
618
|
-
<div class="file-info">
|
|
619
|
-
<div class="file-name folder-name">
|
|
620
|
-
<i class="fas fa-folder folder-leading-icon"></i>${safeFolderName}/
|
|
621
|
-
</div>
|
|
622
|
-
</div>
|
|
623
|
-
<button type="button" class="btn btn-danger btn-sm delete-btn dtm" data-key="${safeFolderDeleteKeyAttr}" data-is-folder="true" title="Delete folder">
|
|
624
|
-
<i class="fas fa-trash"></i>
|
|
625
|
-
</button>
|
|
626
|
-
</div>
|
|
627
|
-
</td>
|
|
628
|
-
<td>
|
|
629
|
-
${mutedDash()}
|
|
630
|
-
</td>
|
|
631
|
-
<td class="table-cell-center">
|
|
632
|
-
${mutedDash()}
|
|
633
|
-
</td>
|
|
634
|
-
<td class="table-cell-center">
|
|
635
|
-
<div class="btn-group" role="group">
|
|
636
|
-
<button type="button" class="btn btn-danger btn-sm delete-btn" data-key="${safeFolderDeleteKeyAttr}" data-is-folder="true" title="Delete folder">
|
|
637
|
-
<i class="fas fa-trash"></i>
|
|
638
|
-
</button>
|
|
639
|
-
</div>
|
|
640
|
-
</td>
|
|
641
|
-
</tr>
|
|
642
|
-
`;
|
|
643
|
-
}).join('');
|
|
410
|
+
if (totalItems === 0) {
|
|
411
|
+
gridEl.innerHTML = '';
|
|
412
|
+
if (emptyEl) {
|
|
413
|
+
emptyEl.hidden = false;
|
|
414
|
+
emptyEl.style.display = 'flex';
|
|
415
|
+
}
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
644
418
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
}
|
|
649
|
-
|
|
419
|
+
if (emptyEl) {
|
|
420
|
+
emptyEl.hidden = true;
|
|
421
|
+
emptyEl.style.display = 'none';
|
|
422
|
+
}
|
|
423
|
+
gridEl.className = `fm-grid view-${currentViewMode}`;
|
|
424
|
+
|
|
425
|
+
let html = '';
|
|
426
|
+
|
|
427
|
+
// If List view, add header row
|
|
428
|
+
if (currentViewMode === 'list') {
|
|
429
|
+
html += `
|
|
430
|
+
<div class="fm-list-header">
|
|
431
|
+
<div></div>
|
|
432
|
+
<div></div>
|
|
433
|
+
<div>Name</div>
|
|
434
|
+
<div>Size</div>
|
|
435
|
+
<div>Modified</div>
|
|
436
|
+
<div></div>
|
|
437
|
+
<div></div>
|
|
438
|
+
</div>
|
|
439
|
+
`;
|
|
650
440
|
}
|
|
651
|
-
const visibleFiles = isVirtualized ? displayFiles.slice(0, virtualRenderLimit) : displayFiles;
|
|
652
|
-
|
|
653
|
-
// Generate file rows
|
|
654
|
-
const fileRows = visibleFiles.map(file => {
|
|
655
|
-
const rawKey = String(file.Key || '');
|
|
656
|
-
const encodedKey = encodeURIComponent(rawKey);
|
|
657
|
-
const safeKeyAttr = escapeAttr(rawKey);
|
|
658
|
-
|
|
659
|
-
// Calculate display name and icon directly
|
|
660
|
-
const fullPath = rawKey;
|
|
661
|
-
const lastSlashIndex = fullPath.lastIndexOf('/');
|
|
662
|
-
let filename = fullPath;
|
|
663
|
-
let folderPath = '';
|
|
664
|
-
|
|
665
|
-
if (lastSlashIndex !== -1) {
|
|
666
|
-
filename = fullPath.substring(lastSlashIndex + 1);
|
|
667
|
-
folderPath = fullPath.substring(0, lastSlashIndex + 1); // include trailing slash
|
|
668
|
-
}
|
|
669
441
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
: '';
|
|
686
|
-
|
|
687
|
-
return `
|
|
688
|
-
<tr class="file-row">
|
|
689
|
-
<td class="table-cell-center">
|
|
690
|
-
<input type="checkbox" class="selectFileCheckbox" data-key="${safeKeyAttr}" aria-label="Select ${safeKeyAttr}" />
|
|
691
|
-
</td>
|
|
692
|
-
<td class="file-name-cell">
|
|
693
|
-
<div class="file-row-main">
|
|
694
|
-
<div class="file-icon ${typeCategory}">
|
|
695
|
-
<i class="${iconClass}"></i>
|
|
696
|
-
</div>
|
|
697
|
-
<div class="file-info">
|
|
698
|
-
${pathDisplay}
|
|
699
|
-
<div class="file-name" data-full-path="${safeKeyAttr}" title="${safeFilename}">${safeTruncatedName}</div>
|
|
700
|
-
</div>
|
|
442
|
+
// Render Folders
|
|
443
|
+
folders.forEach(folder => {
|
|
444
|
+
const cleanFolder = String(folder || '').replace(/\/+$/, '');
|
|
445
|
+
const folderName = getBaseName(cleanFolder);
|
|
446
|
+
const folderPathForAction = currentPrefix && !cleanFolder.startsWith(currentPrefix + '/') && cleanFolder !== currentPrefix
|
|
447
|
+
? `${currentPrefix}/${cleanFolder}`
|
|
448
|
+
: cleanFolder;
|
|
449
|
+
const safePath = escapeAttr(folderPathForAction);
|
|
450
|
+
const safeName = escapeHtml(folderName);
|
|
451
|
+
|
|
452
|
+
if (currentViewMode === 'grid') {
|
|
453
|
+
html += `
|
|
454
|
+
<div class="fm-item" data-type="folder" data-folder-path="${safePath}" tabindex="0">
|
|
455
|
+
<div class="fm-item-icon">
|
|
456
|
+
${ICONS.folder}
|
|
701
457
|
</div>
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
</td>
|
|
706
|
-
<td>
|
|
707
|
-
${file.LastModified ? `<span class="muted-cell-value">${formatDate(file.LastModified)}</span>` : '<span class="muted-cell-value">N/A</span>'}
|
|
708
|
-
</td>
|
|
709
|
-
<td class="table-cell-center">
|
|
710
|
-
<div class="btn-group" role="group">
|
|
711
|
-
<button type="button" class="${viewButtonClass}" data-key="${safeKeyAttr}" data-filename="${safeKeyAttr}"
|
|
712
|
-
data-key-enc="${encodedKey}" title="${isFileViewable ? 'View file in browser' : 'File type not supported for viewing'}" ${viewButtonDisabled}>
|
|
713
|
-
<i class="fas fa-eye"></i>
|
|
714
|
-
</button>
|
|
715
|
-
<button type="button" class="btn btn-info btn-sm copy-btn" data-key-enc="${encodedKey}" title="Copy Link">
|
|
716
|
-
<i class="fas fa-link"></i>
|
|
717
|
-
</button>
|
|
718
|
-
<a href="#" onclick="event.preventDefault(); downloadFileViaFetch('${getDownloadUrl(encodedKey)}', '${escapeAttr(filename)}')" class="btn btn-success btn-sm"
|
|
719
|
-
title="Download file">
|
|
720
|
-
<i class="fas fa-download"></i>
|
|
721
|
-
</a>
|
|
722
|
-
<button type="button" class="btn btn-danger btn-sm delete-btn" data-key="${safeKeyAttr}" title="Delete file">
|
|
723
|
-
<i class="fas fa-trash"></i>
|
|
724
|
-
</button>
|
|
458
|
+
<div class="fm-item-details">
|
|
459
|
+
<div class="fm-item-name" title="${safeName}">${safeName}</div>
|
|
460
|
+
<div class="fm-item-meta">Folder</div>
|
|
725
461
|
</div>
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
462
|
+
<button type="button" class="fm-item-menu" data-menu-folder="${safePath}" aria-label="Folder options">
|
|
463
|
+
${ICONS.more}
|
|
464
|
+
</button>
|
|
465
|
+
</div>
|
|
466
|
+
`;
|
|
467
|
+
} else {
|
|
468
|
+
html += `
|
|
469
|
+
<div class="fm-item" data-type="folder" data-folder-path="${safePath}" tabindex="0">
|
|
470
|
+
<div class="fm-item-checkbox"></div>
|
|
471
|
+
<div class="fm-item-icon">
|
|
472
|
+
${ICONS.folderSm}
|
|
473
|
+
</div>
|
|
474
|
+
<div class="fm-item-name" title="${safeName}">${safeName}</div>
|
|
475
|
+
<div class="fm-item-size fm-item-meta">—</div>
|
|
476
|
+
<div class="fm-item-date fm-item-meta">—</div>
|
|
477
|
+
<div class="fm-star-wrap"></div>
|
|
478
|
+
<button type="button" class="fm-item-menu" data-menu-folder="${safePath}" aria-label="Folder options">
|
|
479
|
+
${ICONS.more}
|
|
480
|
+
</button>
|
|
481
|
+
</div>
|
|
482
|
+
`;
|
|
483
|
+
}
|
|
484
|
+
});
|
|
730
485
|
|
|
731
|
-
|
|
486
|
+
// Render Files
|
|
487
|
+
files.forEach(file => {
|
|
488
|
+
const rawKey = String(file.Key || '');
|
|
489
|
+
const fileName = getBaseName(rawKey);
|
|
490
|
+
const safeKey = escapeAttr(rawKey);
|
|
491
|
+
const safeName = escapeHtml(fileName);
|
|
492
|
+
const isStarred = starredFiles.has(rawKey);
|
|
493
|
+
const isImg = isImage(fileName);
|
|
494
|
+
const typeCfg = getFileTypeConfig(fileName);
|
|
495
|
+
const formattedSize = file.Size ? formatFileSize(file.Size) : '—';
|
|
496
|
+
const formattedDate = file.LastModified ? formatDate(file.LastModified) : '—';
|
|
497
|
+
|
|
498
|
+
let iconInner = `<span class="ext">${typeCfg.label}</span>`;
|
|
499
|
+
if (isImg) {
|
|
500
|
+
iconInner = `<img src="${getViewUrl(rawKey)}" class="fm-item-thumb" alt="${safeName}" loading="lazy" onerror="this.outerHTML='<span class=\\'ext\\'>IMG</span>'">`;
|
|
501
|
+
}
|
|
732
502
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
<
|
|
503
|
+
if (currentViewMode === 'grid') {
|
|
504
|
+
html += `
|
|
505
|
+
<div class="fm-item ${isStarred ? 'starred' : ''}" data-type="file" data-key="${safeKey}" tabindex="0">
|
|
506
|
+
<div class="fm-item-icon" style="${isImg ? '' : `background:${typeCfg.bg};color:${typeCfg.color}`}">
|
|
507
|
+
${iconInner}
|
|
508
|
+
</div>
|
|
509
|
+
<div class="fm-item-details">
|
|
510
|
+
<div class="fm-item-name" title="${safeName}">${safeName}</div>
|
|
511
|
+
<div class="fm-item-meta">${formattedSize} · ${formattedDate}</div>
|
|
512
|
+
</div>
|
|
513
|
+
<button type="button" class="fm-star ${isStarred ? 'on' : ''}" data-star-key="${safeKey}" aria-label="${isStarred ? 'Unstar' : 'Star'}">
|
|
514
|
+
${ICONS.starred}
|
|
515
|
+
</button>
|
|
516
|
+
<button type="button" class="fm-item-menu" data-menu-file="${safeKey}" aria-label="File options">
|
|
517
|
+
${ICONS.more}
|
|
518
|
+
</button>
|
|
740
519
|
</div>
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
520
|
+
`;
|
|
521
|
+
} else {
|
|
522
|
+
html += `
|
|
523
|
+
<div class="fm-item ${isStarred ? 'starred' : ''}" data-type="file" data-key="${safeKey}" tabindex="0">
|
|
524
|
+
<div class="fm-item-checkbox">
|
|
525
|
+
<input type="checkbox" class="file-select-cb" data-key="${safeKey}">
|
|
526
|
+
</div>
|
|
527
|
+
<div class="fm-item-icon" style="${isImg ? '' : `background:${typeCfg.bg};color:${typeCfg.color}`}">
|
|
528
|
+
<span class="ext">${typeCfg.label}</span>
|
|
529
|
+
</div>
|
|
530
|
+
<div class="fm-item-name" title="${safeName}">${safeName}</div>
|
|
531
|
+
<div class="fm-item-size fm-item-meta">${formattedSize}</div>
|
|
532
|
+
<div class="fm-item-date fm-item-meta">${formattedDate}</div>
|
|
533
|
+
<button type="button" class="fm-star ${isStarred ? 'on' : ''}" data-star-key="${safeKey}" aria-label="${isStarred ? 'Unstar' : 'Star'}">
|
|
534
|
+
${ICONS.starred}
|
|
748
535
|
</button>
|
|
749
|
-
<button class="
|
|
750
|
-
|
|
536
|
+
<button type="button" class="fm-item-menu" data-menu-file="${safeKey}" aria-label="File options">
|
|
537
|
+
${ICONS.more}
|
|
751
538
|
</button>
|
|
752
|
-
${prefix ? `<button class="btn btn-light" data-action="go-root">
|
|
753
|
-
<i class="fas fa-home"></i> Go to ${rootLabel}
|
|
754
|
-
</button>` : ''}
|
|
755
539
|
</div>
|
|
540
|
+
`;
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
gridEl.innerHTML = html;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Load Files API Call
|
|
548
|
+
async function loadFiles(page = 1, prefix = '', search = '') {
|
|
549
|
+
currentPage = page;
|
|
550
|
+
currentPrefix = String(prefix || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
551
|
+
currentSearch = search;
|
|
552
|
+
|
|
553
|
+
syncStateUrl();
|
|
554
|
+
|
|
555
|
+
const gridEl = document.getElementById('fm-grid');
|
|
556
|
+
if (gridEl) {
|
|
557
|
+
gridEl.innerHTML = `
|
|
558
|
+
<div class="fm-skeleton-grid">
|
|
559
|
+
${[...Array(6)].map(() => `
|
|
560
|
+
<div class="fm-skeleton-card">
|
|
561
|
+
<div class="fm-skeleton-icon"></div>
|
|
562
|
+
<div class="fm-skeleton-line w-75"></div>
|
|
563
|
+
<div class="fm-skeleton-line w-50"></div>
|
|
564
|
+
</div>
|
|
565
|
+
`).join('')}
|
|
756
566
|
</div>
|
|
757
567
|
`;
|
|
758
|
-
return;
|
|
759
568
|
}
|
|
760
569
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
: '<i class="fas fa-sort-down sort-icon sort-icon-active"></i>';
|
|
767
|
-
};
|
|
768
|
-
|
|
769
|
-
const virtualizedInfo = isVirtualized
|
|
770
|
-
? `<div class="virtualized-info">
|
|
771
|
-
<span>Showing ${visibleFiles.length} of ${displayFiles.length} files in this view.</span>
|
|
772
|
-
<div class="virtualized-actions">
|
|
773
|
-
${visibleFiles.length < displayFiles.length ? '<button class="btn btn-sm btn-outline-secondary" data-action="expand-virtual">Load more</button>' : ''}
|
|
774
|
-
</div>
|
|
775
|
-
</div>`
|
|
776
|
-
: '';
|
|
777
|
-
|
|
778
|
-
document.getElementById('fileListContainer').innerHTML = `
|
|
779
|
-
${breadcrumb}
|
|
780
|
-
<div class="table-responsive">
|
|
781
|
-
<div id="bulkActionsBar" class="bulk-actions-bar">
|
|
782
|
-
<span id="bulkSelectedCount">0 selected</span>
|
|
783
|
-
<div class="bulk-actions-controls">
|
|
784
|
-
<button class="btn btn-danger btn-sm" id="bulkDeleteBtn" disabled>
|
|
785
|
-
<i class="fas fa-trash"></i> Delete Selected
|
|
786
|
-
</button>
|
|
787
|
-
</div>
|
|
788
|
-
</div>
|
|
789
|
-
<table class="table table-hover mb-0">
|
|
790
|
-
<thead>
|
|
791
|
-
<tr>
|
|
792
|
-
<th class="table-select-col">
|
|
793
|
-
<input type="checkbox" id="selectAllCheckbox" aria-label="Select all files" />
|
|
794
|
-
</th>
|
|
795
|
-
<th class="sortable-heading file-name-heading" data-sort-field="name">
|
|
796
|
-
<i class="fas fa-file-alt"></i> Name ${getSortIcon('name')}
|
|
797
|
-
</th>
|
|
798
|
-
<th class="sortable-heading" data-sort-field="size">
|
|
799
|
-
<i class="fas fa-weight"></i> Size ${getSortIcon('size')}
|
|
800
|
-
</th>
|
|
801
|
-
<th class="sortable-heading" data-sort-field="date">
|
|
802
|
-
<i class="fas fa-calendar"></i> Last Modified ${getSortIcon('date')}
|
|
803
|
-
</th>
|
|
804
|
-
<th class="table-cell-center">
|
|
805
|
-
<i class="fas fa-cogs"></i> Actions
|
|
806
|
-
</th>
|
|
807
|
-
</tr>
|
|
808
|
-
</thead>
|
|
809
|
-
<tbody>
|
|
810
|
-
${allRows}
|
|
811
|
-
</tbody>
|
|
812
|
-
</table>
|
|
813
|
-
${virtualizedInfo}
|
|
814
|
-
</div>
|
|
815
|
-
`;
|
|
570
|
+
try {
|
|
571
|
+
const searchParam = search ? `&search=${encodeURIComponent(search)}` : '';
|
|
572
|
+
const sortParam = `&sort=${currentSort}&order=${currentOrder}`;
|
|
573
|
+
const recursiveParam = `&recursive=false`;
|
|
574
|
+
const url = withBucketUrl(`/mbkbucket/api/files?page=${page}&prefix=${encodeURIComponent(currentPrefix)}${searchParam}${sortParam}${recursiveParam}`);
|
|
816
575
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
// Render pagination
|
|
822
|
-
function renderPagination(data) {
|
|
823
|
-
const { currentPage, totalPages, hasPrevPage, hasNextPage, mode } = data;
|
|
824
|
-
|
|
825
|
-
let paginationHTML = '';
|
|
826
|
-
const pageLink = (page, label, extra = {}) => {
|
|
827
|
-
const attrs = [
|
|
828
|
-
dataAttr('page', page),
|
|
829
|
-
dataAttr('prefix', currentPrefix),
|
|
830
|
-
dataAttr('search', currentSearch),
|
|
831
|
-
dataAttr('sort', extra.sort || currentSort),
|
|
832
|
-
dataAttr('order', extra.order || currentOrder)
|
|
833
|
-
];
|
|
834
|
-
if (extra.token) attrs.push(dataAttr('token', extra.token));
|
|
835
|
-
return `<a class="page-link" href="#" ${attrs.join(' ')}>${label}</a>`;
|
|
836
|
-
};
|
|
837
|
-
|
|
838
|
-
if (mode === 'optimized') {
|
|
839
|
-
// Optimized Mode: Token-based pagination (Next only, limited Prev)
|
|
840
|
-
|
|
841
|
-
// Previous button (Disabled for now as we don't track history stack of tokens)
|
|
842
|
-
paginationHTML += `
|
|
843
|
-
<li class="page-item disabled">
|
|
844
|
-
<span class="page-link" title="Previous page not supported in optimized view">
|
|
845
|
-
<i class="fas fa-chevron-left"></i> Previous
|
|
846
|
-
</span>
|
|
847
|
-
</li>
|
|
848
|
-
`;
|
|
849
|
-
|
|
850
|
-
// Current Page Indicator
|
|
851
|
-
paginationHTML += `
|
|
852
|
-
<li class="page-item active">
|
|
853
|
-
<span class="page-link">${currentPage}</span>
|
|
854
|
-
</li>
|
|
855
|
-
`;
|
|
576
|
+
const response = await fetch(url);
|
|
577
|
+
const data = await response.json();
|
|
856
578
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
paginationHTML += `
|
|
860
|
-
<li class="page-item">
|
|
861
|
-
${pageLink(currentPage + 1, 'Next <i class="fas fa-chevron-right"></i>', { token: currentNextToken || '' })}
|
|
862
|
-
</li>
|
|
863
|
-
`;
|
|
864
|
-
} else {
|
|
865
|
-
paginationHTML += `
|
|
866
|
-
<li class="page-item disabled">
|
|
867
|
-
<span class="page-link">
|
|
868
|
-
Next <i class="fas fa-chevron-right"></i>
|
|
869
|
-
</span>
|
|
870
|
-
</li>
|
|
871
|
-
`;
|
|
579
|
+
if (!data.success) {
|
|
580
|
+
throw new Error(data.error || 'Failed to load files');
|
|
872
581
|
}
|
|
873
582
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
// Previous button
|
|
878
|
-
if (hasPrevPage) {
|
|
879
|
-
paginationHTML += `
|
|
880
|
-
<li class="page-item">
|
|
881
|
-
${pageLink(currentPage - 1, '<i class="fas fa-chevron-left"></i> Previous')}
|
|
882
|
-
</li>
|
|
883
|
-
`;
|
|
884
|
-
} else {
|
|
885
|
-
paginationHTML += `
|
|
886
|
-
<li class="page-item disabled">
|
|
887
|
-
<span class="page-link">
|
|
888
|
-
<i class="fas fa-chevron-left"></i> Previous
|
|
889
|
-
</span>
|
|
890
|
-
</li>
|
|
891
|
-
`;
|
|
892
|
-
}
|
|
583
|
+
currentNextToken = data.nextContinuationToken || null;
|
|
584
|
+
currentApiPrefix = data.prefix || '';
|
|
585
|
+
pageFiles = data.files || [];
|
|
893
586
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
587
|
+
if (data.mode === 'optimized') {
|
|
588
|
+
pageFolders = (data.folders || []).map(folder => stripApiPrefixFromPath(folder, currentApiPrefix)).filter(Boolean);
|
|
589
|
+
} else {
|
|
590
|
+
pageFolders = currentSearch
|
|
591
|
+
? []
|
|
592
|
+
: extractFoldersAtCurrentLevel(pageFiles, currentApiPrefix || currentPrefix)
|
|
593
|
+
.map(folder => stripApiPrefixFromPath(folder, currentApiPrefix))
|
|
594
|
+
.filter(Boolean);
|
|
595
|
+
}
|
|
902
596
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
597
|
+
renderTree();
|
|
598
|
+
renderBreadcrumbs();
|
|
599
|
+
renderFiles();
|
|
600
|
+
renderPagination(data);
|
|
601
|
+
} catch (err) {
|
|
602
|
+
console.error('Error loading files:', err);
|
|
603
|
+
showToast(err.message || 'Error loading files', 'error');
|
|
604
|
+
if (gridEl) {
|
|
605
|
+
gridEl.innerHTML = `
|
|
606
|
+
<div class="fm-empty">
|
|
607
|
+
<div class="empty-title">Error Loading Files</div>
|
|
608
|
+
<div class="empty-desc">${escapeHtml(err.message)}</div>
|
|
609
|
+
<button type="button" class="btn btn-primary btn-sm mt-3" onclick="window.mbkbucket.loadFiles(${page}, '${escapeAttr(currentPrefix)}')">Retry</button>
|
|
610
|
+
</div>
|
|
611
|
+
`;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
913
615
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
</li>
|
|
920
|
-
`;
|
|
921
|
-
} else {
|
|
922
|
-
paginationHTML += `
|
|
923
|
-
<li class="page-item">
|
|
924
|
-
${pageLink(i, String(i))}
|
|
925
|
-
</li>
|
|
926
|
-
`;
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
// Last page
|
|
931
|
-
if (endPage < totalPages) {
|
|
932
|
-
if (endPage < totalPages - 1) {
|
|
933
|
-
paginationHTML += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
|
|
934
|
-
}
|
|
935
|
-
paginationHTML += `
|
|
936
|
-
<li class="page-item">
|
|
937
|
-
${pageLink(totalPages, String(totalPages))}
|
|
938
|
-
</li>`;
|
|
939
|
-
}
|
|
616
|
+
// Pagination Renderer
|
|
617
|
+
function renderPagination(data) {
|
|
618
|
+
const paginationContainer = document.getElementById('paginationContainer');
|
|
619
|
+
const paginationList = document.getElementById('paginationList');
|
|
620
|
+
if (!paginationContainer || !paginationList) return;
|
|
940
621
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
<li class="page-item">
|
|
945
|
-
${pageLink(currentPage + 1, 'Next <i class="fas fa-chevron-right"></i>')}
|
|
946
|
-
</li>
|
|
947
|
-
`;
|
|
948
|
-
} else {
|
|
949
|
-
paginationHTML += `
|
|
950
|
-
<li class="page-item disabled">
|
|
951
|
-
<span class="page-link">
|
|
952
|
-
Next <i class="fas fa-chevron-right"></i>
|
|
953
|
-
</span>
|
|
954
|
-
</li>
|
|
955
|
-
`;
|
|
956
|
-
}
|
|
622
|
+
if (!data.totalPages || data.totalPages <= 1) {
|
|
623
|
+
paginationContainer.style.display = 'none';
|
|
624
|
+
return;
|
|
957
625
|
}
|
|
958
626
|
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
function confirmDelete(key, isFolder = false) {
|
|
963
|
-
document.getElementById('deleteKey').value = key;
|
|
964
|
-
document.getElementById('deleteIsFolder').value = isFolder ? 'true' : '';
|
|
965
|
-
document.getElementById('deleteFileName').textContent = key;
|
|
627
|
+
paginationContainer.style.display = 'flex';
|
|
628
|
+
let html = '';
|
|
966
629
|
|
|
967
|
-
//
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
630
|
+
// Prev
|
|
631
|
+
html += `
|
|
632
|
+
<li class="page-item ${data.currentPage <= 1 ? 'disabled' : ''}">
|
|
633
|
+
<button type="button" class="page-link" data-page="${data.currentPage - 1}">‹</button>
|
|
634
|
+
</li>
|
|
635
|
+
`;
|
|
971
636
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
637
|
+
// Pages
|
|
638
|
+
for (let i = 1; i <= data.totalPages; i++) {
|
|
639
|
+
if (i === 1 || i === data.totalPages || (i >= data.currentPage - 2 && i <= data.currentPage + 2)) {
|
|
640
|
+
html += `
|
|
641
|
+
<li class="page-item ${i === data.currentPage ? 'active' : ''}">
|
|
642
|
+
<button type="button" class="page-link" data-page="${i}">${i}</button>
|
|
643
|
+
</li>
|
|
644
|
+
`;
|
|
645
|
+
} else if (i === data.currentPage - 3 || i === data.currentPage + 3) {
|
|
646
|
+
html += `<li class="page-item disabled"><span class="page-link">…</span></li>`;
|
|
647
|
+
}
|
|
980
648
|
}
|
|
981
649
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
}
|
|
650
|
+
// Next
|
|
651
|
+
html += `
|
|
652
|
+
<li class="page-item ${data.currentPage >= data.totalPages ? 'disabled' : ''}">
|
|
653
|
+
<button type="button" class="page-link" data-page="${data.currentPage + 1}">›</button>
|
|
654
|
+
</li>
|
|
655
|
+
`;
|
|
656
|
+
|
|
657
|
+
paginationList.innerHTML = html;
|
|
658
|
+
}
|
|
985
659
|
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
document.getElementById('
|
|
989
|
-
|
|
660
|
+
// Context Menu
|
|
661
|
+
function openContextMenu(x, y, items) {
|
|
662
|
+
const menu = document.getElementById('fm-context-menu');
|
|
663
|
+
if (!menu) return;
|
|
990
664
|
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
665
|
+
menu.innerHTML = items.map(item => {
|
|
666
|
+
if (item === '-') return '<div class="fm-context-divider"></div>';
|
|
667
|
+
return `<button type="button" class="fm-context-item ${item.danger ? 'danger' : ''}" data-action="${item.id}">${item.icon || ''} ${escapeHtml(item.label)}</button>`;
|
|
668
|
+
}).join('');
|
|
994
669
|
|
|
995
|
-
|
|
996
|
-
const isFolder = document.getElementById('deleteIsFolder').value === 'true';
|
|
670
|
+
menu.style.display = 'block';
|
|
997
671
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
'Content-Type': 'application/json'
|
|
1003
|
-
},
|
|
1004
|
-
body: JSON.stringify({ key, folder: isFolder })
|
|
1005
|
-
});
|
|
672
|
+
const menuWidth = 180;
|
|
673
|
+
const menuHeight = menu.offsetHeight || 150;
|
|
674
|
+
const posX = Math.min(x, window.innerWidth - menuWidth - 10);
|
|
675
|
+
const posY = Math.min(y, window.innerHeight - menuHeight - 10);
|
|
1006
676
|
|
|
1007
|
-
|
|
677
|
+
menu.style.left = `${posX}px`;
|
|
678
|
+
menu.style.top = `${posY}px`;
|
|
1008
679
|
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
680
|
+
menu.querySelectorAll('.fm-context-item').forEach(btn => {
|
|
681
|
+
btn.addEventListener('click', (e) => {
|
|
682
|
+
e.stopPropagation();
|
|
683
|
+
const actionId = btn.dataset.action;
|
|
684
|
+
const targetItem = items.find(it => it.id === actionId);
|
|
685
|
+
menu.style.display = 'none';
|
|
686
|
+
if (targetItem && typeof targetItem.action === 'function') {
|
|
687
|
+
targetItem.action();
|
|
1017
688
|
}
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
689
|
+
});
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function closeContextMenu() {
|
|
694
|
+
const menu = document.getElementById('fm-context-menu');
|
|
695
|
+
if (menu) menu.style.display = 'none';
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Actions: Star, Download, Preview, Delete, New Folder
|
|
699
|
+
function toggleStar(key) {
|
|
700
|
+
if (starredFiles.has(key)) {
|
|
701
|
+
starredFiles.delete(key);
|
|
702
|
+
showToast('Removed from Starred');
|
|
703
|
+
} else {
|
|
704
|
+
starredFiles.add(key);
|
|
705
|
+
showToast('Added to Starred', 'success');
|
|
706
|
+
}
|
|
707
|
+
localStorage.setItem('mbk_starred_keys', JSON.stringify([...starredFiles]));
|
|
708
|
+
renderTree();
|
|
709
|
+
renderFiles();
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
async function downloadFile(key) {
|
|
713
|
+
const fileName = getBaseName(key);
|
|
714
|
+
const downloadUrl = getDownloadUrl(key);
|
|
715
|
+
showToast(`Downloading ${fileName}...`);
|
|
716
|
+
try {
|
|
717
|
+
const response = await fetch(downloadUrl);
|
|
718
|
+
if (!response.ok) throw new Error('Download failed');
|
|
719
|
+
const blob = await response.blob();
|
|
720
|
+
const url = URL.createObjectURL(blob);
|
|
721
|
+
const a = document.createElement('a');
|
|
722
|
+
a.href = url;
|
|
723
|
+
a.download = fileName;
|
|
724
|
+
document.body.appendChild(a);
|
|
725
|
+
a.click();
|
|
726
|
+
document.body.removeChild(a);
|
|
727
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
728
|
+
} catch (err) {
|
|
729
|
+
showToast(err.message, 'error');
|
|
1022
730
|
}
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
setTimeout(() => alert.remove(), 300);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function copyShareLink(key) {
|
|
734
|
+
const viewUrl = window.location.origin + getViewUrl(key);
|
|
735
|
+
navigator.clipboard.writeText(viewUrl).then(() => {
|
|
736
|
+
showToast('Link copied to clipboard', 'success');
|
|
737
|
+
}).catch(() => {
|
|
738
|
+
showToast('Failed to copy link', 'error');
|
|
1032
739
|
});
|
|
1033
|
-
}
|
|
740
|
+
}
|
|
1034
741
|
|
|
1035
|
-
//
|
|
1036
|
-
|
|
1037
|
-
const
|
|
1038
|
-
const
|
|
742
|
+
// Preview Modal
|
|
743
|
+
function openPreview(key) {
|
|
744
|
+
const modal = document.getElementById('previewModal');
|
|
745
|
+
const titleEl = document.getElementById('previewFileName');
|
|
746
|
+
const contentEl = document.getElementById('previewContent');
|
|
747
|
+
const openBtn = document.getElementById('previewOpenBtn');
|
|
748
|
+
const downloadBtn = document.getElementById('previewDownloadBtn');
|
|
1039
749
|
|
|
1040
|
-
|
|
750
|
+
if (!modal || !contentEl) return;
|
|
1041
751
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
}
|
|
1046
|
-
renderUploadQueue();
|
|
1047
|
-
}
|
|
752
|
+
const fileName = getBaseName(key);
|
|
753
|
+
const viewUrl = getViewUrl(key);
|
|
754
|
+
const downloadUrl = getDownloadUrl(key);
|
|
1048
755
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
756
|
+
if (titleEl) titleEl.textContent = fileName;
|
|
757
|
+
if (openBtn) openBtn.href = viewUrl;
|
|
758
|
+
if (downloadBtn) downloadBtn.href = downloadUrl;
|
|
759
|
+
|
|
760
|
+
contentEl.innerHTML = '<div class="empty-preview"><div class="spinner-border text-light"></div><div class="mt-2 text-light">Loading preview...</div></div>';
|
|
761
|
+
modal.classList.add('show');
|
|
762
|
+
|
|
763
|
+
if (isImage(fileName)) {
|
|
764
|
+
contentEl.innerHTML = `<img src="${viewUrl}" alt="${escapeAttr(fileName)}">`;
|
|
765
|
+
} else if (isVideo(fileName)) {
|
|
766
|
+
contentEl.innerHTML = `<video controls autoplay class="w-100" style="max-height: 70vh;"><source src="${viewUrl}"></video>`;
|
|
767
|
+
} else if (isAudio(fileName)) {
|
|
768
|
+
contentEl.innerHTML = `<div class="p-4 text-center"><audio controls autoplay class="w-100"><source src="${viewUrl}"></audio></div>`;
|
|
769
|
+
} else if (isPdf(fileName)) {
|
|
770
|
+
contentEl.innerHTML = `<iframe src="${viewUrl}" class="preview-iframe"></iframe>`;
|
|
771
|
+
} else if (isTextOrCode(fileName)) {
|
|
772
|
+
fetch(viewUrl).then(res => res.text()).then(text => {
|
|
773
|
+
contentEl.innerHTML = `<pre class="preview-code-block"><code>${escapeHtml(text)}</code></pre>`;
|
|
774
|
+
}).catch(err => {
|
|
775
|
+
contentEl.innerHTML = `<div class="p-4 text-danger text-center">Failed to load text preview: ${escapeHtml(err.message)}</div>`;
|
|
776
|
+
});
|
|
777
|
+
} else {
|
|
778
|
+
contentEl.innerHTML = `
|
|
779
|
+
<div class="p-5 text-center text-light">
|
|
780
|
+
<h5>No inline preview available for this file type.</h5>
|
|
781
|
+
<p class="text-muted small">You can open it in a new tab or download it directly.</p>
|
|
782
|
+
</div>
|
|
1056
783
|
`;
|
|
1057
|
-
return;
|
|
1058
784
|
}
|
|
785
|
+
}
|
|
1059
786
|
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
|
1065
|
-
return (bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1) + ' ' + units[i];
|
|
1066
|
-
}
|
|
787
|
+
window.closePreviewModal = function () {
|
|
788
|
+
const modal = document.getElementById('previewModal');
|
|
789
|
+
if (modal) modal.classList.remove('show');
|
|
790
|
+
};
|
|
1067
791
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
statusText = fmtSize(item.uploadedBytes || 0) + ' / ' + fmtSize(item.file.size) + ' (' + pct + '%)';
|
|
1076
|
-
if (item.uploadRate > 0) statusText += ' \u00b7 ' + fmtSize(item.uploadRate) + '/s';
|
|
1077
|
-
} else {
|
|
1078
|
-
statusText = item.status;
|
|
1079
|
-
}
|
|
1080
|
-
const actionBtn = item.status === 'uploading'
|
|
1081
|
-
? `<button class="btn btn-sm btn-outline-secondary cancel-upload" data-id="${item.id}"><i class="fas fa-ban"></i></button>`
|
|
1082
|
-
: `<button class="btn btn-sm btn-danger remove-queued" data-id="${item.id}"><i class="fas fa-trash"></i></button>`;
|
|
1083
|
-
return `
|
|
1084
|
-
<div class="upload-item" data-id="${item.id}">
|
|
1085
|
-
<div class="meta">
|
|
1086
|
-
<div class="upload-item-meta-row">
|
|
1087
|
-
<div class="file-icon upload-file-icon ${typeCategory}"><i class="${iconClass}"></i></div>
|
|
1088
|
-
<div class="name upload-item-name" title="${safeName}">${safeName}</div>
|
|
1089
|
-
<div class="form-text file-size upload-item-size-label">${formatFileSize(item.file.size)}</div>
|
|
1090
|
-
</div>
|
|
1091
|
-
</div>
|
|
1092
|
-
<div class="upload-progress-cell">
|
|
1093
|
-
<div class="progress-container upload-progress-track">
|
|
1094
|
-
<div class="progress-bar upload-progress-bar" style="width: ${Math.round(item.progress)}%;"></div>
|
|
1095
|
-
</div>
|
|
1096
|
-
<div class="upload-item-status-row">
|
|
1097
|
-
<small class="text-muted status">${statusText}</small>
|
|
1098
|
-
<div class="controls">${actionBtn}</div>
|
|
1099
|
-
</div>
|
|
1100
|
-
</div>
|
|
1101
|
-
</div>`;
|
|
1102
|
-
}).join('');
|
|
792
|
+
// Delete Modal
|
|
793
|
+
function promptDelete(key, isFolder = false) {
|
|
794
|
+
const modal = document.getElementById('deleteModal');
|
|
795
|
+
const deleteFileName = document.getElementById('deleteFileName');
|
|
796
|
+
const deleteKey = document.getElementById('deleteKey');
|
|
797
|
+
const deleteIsFolder = document.getElementById('deleteIsFolder');
|
|
798
|
+
const deleteConfirmLabel = document.getElementById('deleteConfirmLabel');
|
|
1103
799
|
|
|
1104
|
-
|
|
1105
|
-
document.querySelectorAll('.remove-queued').forEach(btn => btn.addEventListener('click', function (e) {
|
|
1106
|
-
const id = this.dataset.id;
|
|
1107
|
-
const idx = uploadQueue.findIndex(i => i.id === id);
|
|
1108
|
-
if (idx !== -1) uploadQueue.splice(idx, 1);
|
|
1109
|
-
renderUploadQueue();
|
|
1110
|
-
}));
|
|
1111
|
-
|
|
1112
|
-
document.querySelectorAll('.cancel-upload').forEach(btn => btn.addEventListener('click', async function (e) {
|
|
1113
|
-
const id = this.dataset.id;
|
|
1114
|
-
const handle = xhrMap.get(id);
|
|
1115
|
-
const entry = uploadQueue.find(i => i.id === id);
|
|
1116
|
-
|
|
1117
|
-
// Abort in-flight fetch
|
|
1118
|
-
if (handle && handle.controller) handle.controller.abort();
|
|
1119
|
-
// Abort legacy XHR (small file path)
|
|
1120
|
-
if (handle && handle.xhr) handle.xhr.abort();
|
|
1121
|
-
|
|
1122
|
-
// Tell S3 to discard the incomplete multipart upload
|
|
1123
|
-
if (entry && entry.s3UploadId && entry.s3Key) {
|
|
1124
|
-
try {
|
|
1125
|
-
await fetch(withBucketUrl('/mbkbucket/upload-abort'), {
|
|
1126
|
-
method: 'POST',
|
|
1127
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1128
|
-
body: JSON.stringify({ uploadId: entry.s3UploadId, key: entry.s3Key })
|
|
1129
|
-
});
|
|
1130
|
-
} catch (_) { /* best-effort */ }
|
|
1131
|
-
}
|
|
800
|
+
if (!modal) return;
|
|
1132
801
|
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
// Drag & drop wiring
|
|
1140
|
-
const dropZone = document.getElementById('dropZone');
|
|
1141
|
-
const fileInputEl = document.getElementById('fileInput');
|
|
1142
|
-
if (dropZone) {
|
|
1143
|
-
dropZone.addEventListener('click', () => fileInputEl.click());
|
|
1144
|
-
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('dragover'); });
|
|
1145
|
-
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
|
|
1146
|
-
dropZone.addEventListener('drop', (e) => {
|
|
1147
|
-
e.preventDefault(); dropZone.classList.remove('dragover');
|
|
1148
|
-
if (e.dataTransfer && e.dataTransfer.files) addFilesToQueue(e.dataTransfer.files);
|
|
1149
|
-
});
|
|
1150
|
-
}
|
|
802
|
+
if (deleteFileName) deleteFileName.textContent = key;
|
|
803
|
+
if (deleteKey) deleteKey.value = key;
|
|
804
|
+
if (deleteIsFolder) deleteIsFolder.value = isFolder ? 'true' : '';
|
|
805
|
+
if (deleteConfirmLabel) deleteConfirmLabel.textContent = isFolder ? 'Delete Folder' : 'Delete File';
|
|
1151
806
|
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
807
|
+
modal.classList.add('show');
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
window.closeModal = function () {
|
|
811
|
+
const deleteModal = document.getElementById('deleteModal');
|
|
812
|
+
if (deleteModal) deleteModal.classList.remove('show');
|
|
813
|
+
};
|
|
1157
814
|
|
|
1158
|
-
//
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
815
|
+
// New Folder Modal
|
|
816
|
+
function openNewFolderModal() {
|
|
817
|
+
const modal = document.getElementById('newFolderModal');
|
|
818
|
+
const input = document.getElementById('newFolderNameInput');
|
|
819
|
+
if (!modal) return;
|
|
820
|
+
if (input) input.value = '';
|
|
821
|
+
modal.classList.add('show');
|
|
822
|
+
if (input) setTimeout(() => input.focus(), 50);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
window.closeNewFolderModal = function () {
|
|
826
|
+
const modal = document.getElementById('newFolderModal');
|
|
827
|
+
if (modal) modal.classList.remove('show');
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
window.submitCreateFolder = async function () {
|
|
831
|
+
const input = document.getElementById('newFolderNameInput');
|
|
832
|
+
const name = input ? input.value.trim() : '';
|
|
833
|
+
if (!name) {
|
|
834
|
+
showToast('Please enter a folder name', 'warning');
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
1163
837
|
|
|
1164
|
-
const file = entry.file;
|
|
1165
838
|
try {
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
contentType: file.type || 'application/octet-stream'
|
|
1178
|
-
})
|
|
1179
|
-
});
|
|
1180
|
-
const initJson = await initResp.json();
|
|
1181
|
-
if (!initResp.ok || !initJson.success) throw new Error(initJson.error || 'Failed to initiate upload');
|
|
1182
|
-
|
|
1183
|
-
const { uploadId, key } = initJson;
|
|
1184
|
-
entry.s3UploadId = uploadId; // stored so cancel handler can abort
|
|
1185
|
-
entry.s3Key = key;
|
|
1186
|
-
|
|
1187
|
-
// ---------------------------------------------------------------
|
|
1188
|
-
// Step 2 — Upload parts (1-based partNumber, min 5 MB except last)
|
|
1189
|
-
// ---------------------------------------------------------------
|
|
1190
|
-
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
|
|
1191
|
-
const parts = []; // [{ partNumber, ETag }]
|
|
1192
|
-
let uploadedBytes = 0;
|
|
1193
|
-
|
|
1194
|
-
for (let i = 0; i < totalChunks; i++) {
|
|
1195
|
-
const _chunkStart = Date.now();
|
|
1196
|
-
if (entry.status === 'cancelled') {
|
|
1197
|
-
// Clean up on S3
|
|
1198
|
-
await fetch(withBucketUrl('/mbkbucket/upload-abort'), {
|
|
1199
|
-
method: 'POST',
|
|
1200
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1201
|
-
body: JSON.stringify({ uploadId, key })
|
|
1202
|
-
}).catch(() => {});
|
|
1203
|
-
throw new Error('Upload cancelled');
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
const start = i * CHUNK_SIZE;
|
|
1207
|
-
const blob = file.slice(start, Math.min(file.size, start + CHUNK_SIZE));
|
|
1208
|
-
const partNumber = i + 1; // S3 parts are 1-based
|
|
1209
|
-
|
|
1210
|
-
const fd = new FormData();
|
|
1211
|
-
fd.append('chunk', blob, file.name);
|
|
1212
|
-
fd.append('uploadId', uploadId);
|
|
1213
|
-
fd.append('key', key);
|
|
1214
|
-
fd.append('partNumber', String(partNumber));
|
|
1215
|
-
|
|
1216
|
-
const controller = new AbortController();
|
|
1217
|
-
xhrMap.set(entry.id, { controller, s3UploadId: uploadId, s3Key: key });
|
|
1218
|
-
|
|
1219
|
-
const chunkResp = await fetch(withBucketUrl('/mbkbucket/upload-chunk'), {
|
|
1220
|
-
method: 'POST',
|
|
1221
|
-
body: fd,
|
|
1222
|
-
signal: controller.signal
|
|
1223
|
-
});
|
|
1224
|
-
const chunkJson = await chunkResp.json();
|
|
1225
|
-
if (!chunkResp.ok || !chunkJson.success) throw new Error(chunkJson.error || `Part ${partNumber} upload failed`);
|
|
1226
|
-
|
|
1227
|
-
parts.push({ partNumber: chunkJson.partNumber, ETag: chunkJson.ETag });
|
|
1228
|
-
uploadedBytes += blob.size;
|
|
1229
|
-
const _chunkDuration = (Date.now() - _chunkStart) / 1000;
|
|
1230
|
-
entry.uploadRate = _chunkDuration > 0 ? blob.size / _chunkDuration : 0;
|
|
1231
|
-
entry.uploadedBytes = uploadedBytes;
|
|
1232
|
-
entry.progress = (uploadedBytes / file.size) * 95; // reserve last 5% for complete call
|
|
1233
|
-
renderUploadQueue();
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
// ---------------------------------------------------------------
|
|
1237
|
-
// Step 3 — Complete: assemble all parts on S3
|
|
1238
|
-
// ---------------------------------------------------------------
|
|
1239
|
-
const completeResp = await fetch(withBucketUrl('/mbkbucket/upload-complete'), {
|
|
1240
|
-
method: 'POST',
|
|
1241
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1242
|
-
body: JSON.stringify({ uploadId, key, parts })
|
|
1243
|
-
});
|
|
1244
|
-
const completeJson = await completeResp.json();
|
|
1245
|
-
if (!completeResp.ok || !completeJson.success) throw new Error(completeJson.error || 'Failed to complete upload');
|
|
1246
|
-
|
|
1247
|
-
entry.status = 'done'; entry.progress = 100;
|
|
1248
|
-
xhrMap.delete(entry.id);
|
|
1249
|
-
renderUploadQueue();
|
|
1250
|
-
showAlert(`${file.name} uploaded successfully`, 'success');
|
|
1251
|
-
loadFiles(currentPage, currentPrefix);
|
|
1252
|
-
} else {
|
|
1253
|
-
// small file XHR
|
|
1254
|
-
const fd = new FormData();
|
|
1255
|
-
fd.append('file', file, file.name);
|
|
1256
|
-
fd.append('prefix', currentPrefix);
|
|
1257
|
-
|
|
1258
|
-
const xhr = new XMLHttpRequest();
|
|
1259
|
-
xhr.open('POST', withBucketUrl('/mbkbucket/upload'));
|
|
1260
|
-
|
|
1261
|
-
xhr.upload.addEventListener('progress', (e) => {
|
|
1262
|
-
if (!e.lengthComputable) return;
|
|
1263
|
-
const _now = Date.now();
|
|
1264
|
-
const _dt = (_now - (entry._rateLastTime || _now)) / 1000;
|
|
1265
|
-
const _db = e.loaded - (entry._rateLastBytes || 0);
|
|
1266
|
-
if (_dt > 0.2) {
|
|
1267
|
-
entry.uploadRate = _db / _dt;
|
|
1268
|
-
entry._rateLastTime = _now;
|
|
1269
|
-
entry._rateLastBytes = e.loaded;
|
|
1270
|
-
}
|
|
1271
|
-
entry.uploadedBytes = e.loaded;
|
|
1272
|
-
entry.progress = (e.loaded / e.total) * 100;
|
|
1273
|
-
renderUploadQueue();
|
|
1274
|
-
});
|
|
1275
|
-
entry._rateLastTime = Date.now();
|
|
1276
|
-
entry._rateLastBytes = 0;
|
|
1277
|
-
|
|
1278
|
-
xhr.addEventListener('load', () => {
|
|
1279
|
-
try {
|
|
1280
|
-
const resp = JSON.parse(xhr.responseText);
|
|
1281
|
-
if (xhr.status >= 200 && xhr.status < 300 && resp.success) {
|
|
1282
|
-
entry.status = 'done'; entry.progress = 100; renderUploadQueue();
|
|
1283
|
-
showAlert('File uploaded successfully', 'success');
|
|
1284
|
-
loadFiles(currentPage, currentPrefix);
|
|
1285
|
-
} else {
|
|
1286
|
-
entry.status = 'error'; renderUploadQueue();
|
|
1287
|
-
showAlert(resp.error || 'Upload failed', 'error');
|
|
1288
|
-
}
|
|
1289
|
-
} catch (err) {
|
|
1290
|
-
entry.status = 'error'; renderUploadQueue();
|
|
1291
|
-
showAlert('Upload response parse error', 'error');
|
|
1292
|
-
}
|
|
1293
|
-
xhrMap.delete(entry.id);
|
|
1294
|
-
});
|
|
1295
|
-
|
|
1296
|
-
xhr.addEventListener('error', () => { entry.status = 'error'; renderUploadQueue(); showAlert('Network error during upload', 'error'); xhrMap.delete(entry.id); });
|
|
1297
|
-
xhr.addEventListener('abort', () => { entry.status = 'cancelled'; renderUploadQueue(); xhrMap.delete(entry.id); });
|
|
1298
|
-
|
|
1299
|
-
xhrMap.set(entry.id, { xhr });
|
|
1300
|
-
xhr.send(fd);
|
|
1301
|
-
}
|
|
839
|
+
const response = await fetch(withBucketUrl('/mbkbucket/create-folder'), {
|
|
840
|
+
method: 'POST',
|
|
841
|
+
headers: { 'Content-Type': 'application/json' },
|
|
842
|
+
body: JSON.stringify({ folderName: name, prefix: currentPrefix })
|
|
843
|
+
});
|
|
844
|
+
const data = await response.json();
|
|
845
|
+
if (!data.success) throw new Error(data.error || 'Failed to create folder');
|
|
846
|
+
|
|
847
|
+
closeNewFolderModal();
|
|
848
|
+
showToast(`Folder "${name}" created`, 'success');
|
|
849
|
+
loadFiles(1, currentPrefix);
|
|
1302
850
|
} catch (err) {
|
|
1303
|
-
|
|
1304
|
-
// If a multipart upload was initiated but didn't finish (not a user cancel), abort it on S3
|
|
1305
|
-
if (entry.s3UploadId && entry.s3Key && entry.status !== 'cancelled') {
|
|
1306
|
-
fetch(withBucketUrl('/mbkbucket/upload-abort'), {
|
|
1307
|
-
method: 'POST',
|
|
1308
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1309
|
-
body: JSON.stringify({ uploadId: entry.s3UploadId, key: entry.s3Key })
|
|
1310
|
-
}).catch(() => {});
|
|
1311
|
-
}
|
|
1312
|
-
entry.status = entry.status === 'cancelled' ? 'cancelled' : 'error';
|
|
1313
|
-
xhrMap.delete(entry.id);
|
|
1314
|
-
renderUploadQueue();
|
|
1315
|
-
showAlert('Upload failed: ' + (err.message || 'unknown'), 'error');
|
|
851
|
+
showToast(err.message, 'error');
|
|
1316
852
|
}
|
|
1317
|
-
}
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
// Upload Modal & Flow
|
|
856
|
+
function openUploadModal() {
|
|
857
|
+
const modal = document.getElementById('uploadModal');
|
|
858
|
+
const prefixInput = document.getElementById('uploadPrefixInput');
|
|
859
|
+
if (prefixInput) prefixInput.value = currentPrefix;
|
|
860
|
+
if (modal) modal.classList.add('show');
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
window.closeUploadModal = function () {
|
|
864
|
+
const modal = document.getElementById('uploadModal');
|
|
865
|
+
if (modal) modal.classList.remove('show');
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
function updateUploadQueueUI() {
|
|
869
|
+
const queueEl = document.getElementById('uploadQueue');
|
|
870
|
+
const clearBtn = document.getElementById('clearQueueBtn');
|
|
871
|
+
if (!queueEl) return;
|
|
1318
872
|
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
873
|
+
if (uploadQueue.length === 0) {
|
|
874
|
+
queueEl.innerHTML = '';
|
|
875
|
+
if (clearBtn) clearBtn.style.display = 'none';
|
|
876
|
+
return;
|
|
1322
877
|
}
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
document.getElementById('startUploadBtn')?.addEventListener('click', startAllUploads);
|
|
1326
|
-
document.getElementById('clearQueueBtn')?.addEventListener('click', function () { uploadQueue.length = 0; renderUploadQueue(); });
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
// Initialize upload section collapse functionality
|
|
1331
|
-
function initializeUploadCollapse() {
|
|
1332
|
-
const toggleBtn = document.getElementById('uploadToggleBtn');
|
|
1333
|
-
const uploadSection = document.getElementById('uploadSection');
|
|
1334
|
-
const uploadCard = uploadSection?.closest('.dashboard-card');
|
|
1335
|
-
|
|
1336
|
-
if (!toggleBtn || !uploadSection) return;
|
|
1337
|
-
|
|
1338
|
-
const syncUploadCollapseState = (expanded) => {
|
|
1339
|
-
uploadSection.classList.toggle('expanded', expanded);
|
|
1340
|
-
toggleBtn.classList.toggle('expanded', expanded);
|
|
1341
|
-
toggleBtn.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
|
1342
|
-
uploadSection.setAttribute('aria-hidden', expanded ? 'false' : 'true');
|
|
1343
|
-
if (uploadCard) uploadCard.classList.toggle('upload-panel-open', expanded);
|
|
1344
|
-
};
|
|
1345
|
-
|
|
1346
|
-
toggleBtn.setAttribute('role', 'button');
|
|
1347
|
-
toggleBtn.setAttribute('tabindex', '0');
|
|
1348
|
-
toggleBtn.setAttribute('aria-controls', 'uploadSection');
|
|
1349
|
-
syncUploadCollapseState(uploadSection.classList.contains('expanded'));
|
|
1350
|
-
|
|
1351
|
-
toggleBtn.addEventListener('click', function() {
|
|
1352
|
-
const isExpanded = uploadSection.classList.contains('expanded');
|
|
1353
|
-
syncUploadCollapseState(!isExpanded);
|
|
1354
|
-
});
|
|
1355
878
|
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
}
|
|
879
|
+
if (clearBtn) clearBtn.style.display = 'inline-flex';
|
|
880
|
+
queueEl.innerHTML = uploadQueue.map((file, idx) => `
|
|
881
|
+
<div class="upload-queue-item">
|
|
882
|
+
<div class="d-flex align-items-center gap-2">
|
|
883
|
+
<span class="font-medium">${escapeHtml(file.name)}</span>
|
|
884
|
+
<span class="upload-queue-meta">${formatFileSize(file.size)}</span>
|
|
885
|
+
</div>
|
|
886
|
+
<button type="button" class="btn-close" data-remove-queue="${idx}" style="font-size: 14px;">×</button>
|
|
887
|
+
</div>
|
|
888
|
+
`).join('');
|
|
889
|
+
}
|
|
1364
890
|
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
initializeUploadCollapse();
|
|
891
|
+
async function processUploads() {
|
|
892
|
+
if (isUploading || uploadQueue.length === 0) return;
|
|
893
|
+
isUploading = true;
|
|
1369
894
|
|
|
1370
|
-
const
|
|
1371
|
-
const
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
if (bucketSelectorEl) {
|
|
1376
|
-
bucketSelectorEl.addEventListener('change', function () {
|
|
1377
|
-
const nextBucket = String(this.value || '').trim();
|
|
1378
|
-
if (!nextBucket) return;
|
|
1379
|
-
if (nextBucket === activeBucket) return;
|
|
1380
|
-
|
|
1381
|
-
const nextParams = new URLSearchParams(window.location.search);
|
|
1382
|
-
nextParams.set('bucket', nextBucket);
|
|
1383
|
-
nextParams.delete('page');
|
|
1384
|
-
nextParams.delete('token');
|
|
1385
|
-
|
|
1386
|
-
this.disabled = true;
|
|
1387
|
-
this.setAttribute('aria-busy', 'true');
|
|
1388
|
-
bucketSwitchStatusEl?.classList.remove('is-hidden');
|
|
1389
|
-
|
|
1390
|
-
const qs = nextParams.toString();
|
|
1391
|
-
window.location.href = `/mbkbucket${qs ? `?${qs}` : ''}`;
|
|
1392
|
-
});
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
// Load initial files with persisted state.
|
|
1396
|
-
const urlParams = new URLSearchParams(window.location.search);
|
|
1397
|
-
const initialPrefix = urlParams.get('folder') || urlParams.get('prefix') || '';
|
|
1398
|
-
const initialPage = parseInt(urlParams.get('page')) || 1;
|
|
1399
|
-
const initialSearch = urlParams.get('search') || '';
|
|
1400
|
-
const initialSort = (urlParams.get('sort') || 'name').toLowerCase();
|
|
1401
|
-
const initialOrder = (urlParams.get('order') || 'asc').toLowerCase();
|
|
1402
|
-
const initialView = (urlParams.get('view') || currentViewMode).toLowerCase();
|
|
1403
|
-
const initialFilter = (urlParams.get('filter') || 'all').toLowerCase();
|
|
895
|
+
const progressWrap = document.getElementById('uploadProgress');
|
|
896
|
+
const progressBar = document.getElementById('progressBar');
|
|
897
|
+
const percentEl = document.getElementById('uploadPercent');
|
|
898
|
+
const statusLabel = document.getElementById('uploadStatusLabel');
|
|
899
|
+
const startBtn = document.getElementById('startUploadBtn');
|
|
1404
900
|
|
|
1405
|
-
if (
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
btnFolder?.classList.add('btn-outline-light');
|
|
1419
|
-
}
|
|
901
|
+
if (progressWrap) progressWrap.style.display = 'block';
|
|
902
|
+
if (startBtn) startBtn.disabled = true;
|
|
903
|
+
|
|
904
|
+
const totalFiles = uploadQueue.length;
|
|
905
|
+
let completed = 0;
|
|
906
|
+
|
|
907
|
+
for (let i = 0; i < uploadQueue.length; i++) {
|
|
908
|
+
const file = uploadQueue[i];
|
|
909
|
+
if (statusLabel) statusLabel.textContent = `Uploading ${file.name} (${i + 1}/${totalFiles})...`;
|
|
910
|
+
|
|
911
|
+
const formData = new FormData();
|
|
912
|
+
formData.append('file', file);
|
|
913
|
+
formData.append('prefix', currentPrefix);
|
|
1420
914
|
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
// Search behavior: Trigger when the Search button is clicked OR when the user presses Enter in the search input
|
|
1426
|
-
const searchInputEl = document.getElementById('searchInput');
|
|
1427
|
-
if (searchInputEl) {
|
|
1428
|
-
// Trigger search when Enter is pressed in the input
|
|
1429
|
-
searchInputEl.addEventListener('keydown', function (e) {
|
|
1430
|
-
if (e.key === 'Enter') {
|
|
1431
|
-
e.preventDefault();
|
|
1432
|
-
performSearch();
|
|
1433
|
-
}
|
|
915
|
+
try {
|
|
916
|
+
const response = await fetch(withBucketUrl('/mbkbucket/upload'), {
|
|
917
|
+
method: 'POST',
|
|
918
|
+
body: formData
|
|
1434
919
|
});
|
|
920
|
+
const result = await response.json();
|
|
921
|
+
if (!result.success) throw new Error(result.error || 'Upload failed');
|
|
922
|
+
completed++;
|
|
923
|
+
} catch (err) {
|
|
924
|
+
showToast(`Failed to upload ${file.name}: ${err.message}`, 'error');
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const percent = Math.round(((i + 1) / totalFiles) * 100);
|
|
928
|
+
if (progressBar) progressBar.style.width = `${percent}%`;
|
|
929
|
+
if (percentEl) percentEl.textContent = `${percent}%`;
|
|
1435
930
|
}
|
|
1436
931
|
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
932
|
+
showToast(`Successfully uploaded ${completed} file(s)`, 'success');
|
|
933
|
+
uploadQueue = [];
|
|
934
|
+
isUploading = false;
|
|
935
|
+
if (startBtn) startBtn.disabled = false;
|
|
936
|
+
if (progressWrap) progressWrap.style.display = 'none';
|
|
937
|
+
closeUploadModal();
|
|
938
|
+
updateUploadQueueUI();
|
|
939
|
+
loadFiles(1, currentPrefix);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Event Listeners Setup
|
|
943
|
+
function initEventListeners() {
|
|
944
|
+
// Tree clicks
|
|
945
|
+
document.getElementById('fm-tree')?.addEventListener('click', (e) => {
|
|
946
|
+
const link = e.target.closest('.tree-link');
|
|
947
|
+
if (!link) return;
|
|
948
|
+
e.preventDefault();
|
|
949
|
+
const filter = link.dataset.treeFilter;
|
|
950
|
+
const prefix = link.dataset.treePrefix;
|
|
951
|
+
|
|
952
|
+
if (filter && filter !== 'all') {
|
|
953
|
+
currentQuickFilter = filter;
|
|
954
|
+
document.querySelectorAll('.quick-filter-btn').forEach(b => b.classList.remove('active'));
|
|
955
|
+
renderTree();
|
|
956
|
+
renderBreadcrumbs();
|
|
957
|
+
renderFiles();
|
|
958
|
+
} else {
|
|
959
|
+
currentQuickFilter = 'all';
|
|
960
|
+
document.querySelectorAll('.quick-filter-btn').forEach(b => {
|
|
961
|
+
b.classList.toggle('active', b.dataset.filter === 'all');
|
|
962
|
+
});
|
|
963
|
+
loadFiles(1, prefix !== undefined ? prefix : '');
|
|
964
|
+
}
|
|
1442
965
|
});
|
|
1443
966
|
|
|
1444
|
-
//
|
|
1445
|
-
document.addEventListener('click',
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
} else if (action === 'create-folder') {
|
|
1453
|
-
createFolder();
|
|
1454
|
-
} else if (action === 'create-file') {
|
|
1455
|
-
createFile();
|
|
1456
|
-
} else if (action === 'go-root') {
|
|
1457
|
-
loadFiles(1, '');
|
|
1458
|
-
} else if (action === 'expand-virtual') {
|
|
1459
|
-
expandVirtualRows();
|
|
1460
|
-
} else if (action === 'search') {
|
|
1461
|
-
performSearch();
|
|
1462
|
-
} else if (action === 'clear-search') {
|
|
1463
|
-
clearSearch();
|
|
1464
|
-
} else if (action === 'retry-load') {
|
|
1465
|
-
loadFiles(parseInt(actionBtn.dataset.retryPage, 10) || 1, actionBtn.dataset.retryPrefix || '');
|
|
1466
|
-
} else if (action === 'close-toast') {
|
|
1467
|
-
actionBtn.closest('.toast-notification')?.remove();
|
|
1468
|
-
}
|
|
1469
|
-
return;
|
|
1470
|
-
}
|
|
967
|
+
// Breadcrumb clicks
|
|
968
|
+
document.getElementById('fm-breadcrumb')?.addEventListener('click', (e) => {
|
|
969
|
+
const link = e.target.closest('[data-nav-prefix]');
|
|
970
|
+
if (!link) return;
|
|
971
|
+
e.preventDefault();
|
|
972
|
+
currentQuickFilter = 'all';
|
|
973
|
+
loadFiles(1, link.dataset.navPrefix || '');
|
|
974
|
+
});
|
|
1471
975
|
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
976
|
+
// Quick filters
|
|
977
|
+
document.querySelectorAll('.quick-filter-btn').forEach(btn => {
|
|
978
|
+
btn.addEventListener('click', () => {
|
|
979
|
+
document.querySelectorAll('.quick-filter-btn').forEach(b => b.classList.remove('active'));
|
|
980
|
+
btn.classList.add('active');
|
|
981
|
+
currentQuickFilter = btn.dataset.filter || 'all';
|
|
982
|
+
renderTree();
|
|
983
|
+
renderBreadcrumbs();
|
|
984
|
+
renderFiles();
|
|
985
|
+
syncStateUrl();
|
|
986
|
+
});
|
|
987
|
+
});
|
|
1477
988
|
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
989
|
+
// View mode toggle
|
|
990
|
+
const gridBtn = document.getElementById('view-grid');
|
|
991
|
+
const listBtn = document.getElementById('view-list');
|
|
992
|
+
|
|
993
|
+
gridBtn?.addEventListener('click', () => {
|
|
994
|
+
currentViewMode = 'grid';
|
|
995
|
+
localStorage.setItem('mbk_view_mode', 'grid');
|
|
996
|
+
gridBtn.classList.add('active');
|
|
997
|
+
listBtn?.classList.remove('active');
|
|
998
|
+
renderFiles();
|
|
999
|
+
syncStateUrl();
|
|
1000
|
+
});
|
|
1483
1001
|
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1002
|
+
listBtn?.addEventListener('click', () => {
|
|
1003
|
+
currentViewMode = 'list';
|
|
1004
|
+
localStorage.setItem('mbk_view_mode', 'list');
|
|
1005
|
+
listBtn.classList.add('active');
|
|
1006
|
+
gridBtn?.classList.remove('active');
|
|
1007
|
+
renderFiles();
|
|
1008
|
+
syncStateUrl();
|
|
1009
|
+
});
|
|
1489
1010
|
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1011
|
+
// Search input
|
|
1012
|
+
const searchInput = document.getElementById('searchInput');
|
|
1013
|
+
const clearSearchBtn = document.getElementById('searchClearBtn');
|
|
1014
|
+
|
|
1015
|
+
let searchTimer = null;
|
|
1016
|
+
searchInput?.addEventListener('input', (e) => {
|
|
1017
|
+
const val = e.target.value;
|
|
1018
|
+
if (clearSearchBtn) clearSearchBtn.style.display = val ? 'block' : 'none';
|
|
1019
|
+
|
|
1020
|
+
clearTimeout(searchTimer);
|
|
1021
|
+
searchTimer = setTimeout(() => {
|
|
1022
|
+
currentSearch = val.trim();
|
|
1023
|
+
renderFiles();
|
|
1024
|
+
}, 250);
|
|
1025
|
+
});
|
|
1495
1026
|
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
pageLink.dataset.search || '',
|
|
1503
|
-
pageLink.dataset.sort || currentSort,
|
|
1504
|
-
pageLink.dataset.order || currentOrder,
|
|
1505
|
-
pageLink.dataset.token || ''
|
|
1506
|
-
);
|
|
1507
|
-
return;
|
|
1508
|
-
}
|
|
1027
|
+
clearSearchBtn?.addEventListener('click', () => {
|
|
1028
|
+
if (searchInput) searchInput.value = '';
|
|
1029
|
+
clearSearchBtn.style.display = 'none';
|
|
1030
|
+
currentSearch = '';
|
|
1031
|
+
renderFiles();
|
|
1032
|
+
});
|
|
1509
1033
|
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1034
|
+
// Grid Item Interactions (Click, Double Click, Star, Context Menu)
|
|
1035
|
+
document.getElementById('fm-grid')?.addEventListener('click', (e) => {
|
|
1036
|
+
// Star click
|
|
1037
|
+
const starBtn = e.target.closest('[data-star-key]');
|
|
1038
|
+
if (starBtn) {
|
|
1039
|
+
e.stopPropagation();
|
|
1040
|
+
toggleStar(starBtn.dataset.starKey);
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1516
1043
|
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1044
|
+
// Menu click
|
|
1045
|
+
const menuBtn = e.target.closest('.fm-item-menu');
|
|
1046
|
+
if (menuBtn) {
|
|
1047
|
+
e.stopPropagation();
|
|
1048
|
+
const rect = menuBtn.getBoundingClientRect();
|
|
1049
|
+
const fileKey = menuBtn.dataset.menuFile;
|
|
1050
|
+
const folderPath = menuBtn.dataset.menuFolder;
|
|
1051
|
+
|
|
1052
|
+
if (fileKey) {
|
|
1053
|
+
const isStarred = starredFiles.has(fileKey);
|
|
1054
|
+
openContextMenu(rect.left, rect.bottom + 4, [
|
|
1055
|
+
{ id: 'view', label: 'Preview', icon: '<i class="fas fa-eye"></i>', action: () => openPreview(fileKey) },
|
|
1056
|
+
{ id: 'download', label: 'Download', icon: '<i class="fas fa-download"></i>', action: () => downloadFile(fileKey) },
|
|
1057
|
+
{ id: 'share', label: 'Copy link', icon: '<i class="fas fa-link"></i>', action: () => copyShareLink(fileKey) },
|
|
1058
|
+
{ id: 'star', label: isStarred ? 'Unstar' : 'Star', icon: '<i class="fas fa-star"></i>', action: () => toggleStar(fileKey) },
|
|
1059
|
+
'-',
|
|
1060
|
+
{ id: 'delete', label: 'Delete', icon: '<i class="fas fa-trash"></i>', danger: true, action: () => promptDelete(fileKey, false) }
|
|
1061
|
+
]);
|
|
1062
|
+
} else if (folderPath) {
|
|
1063
|
+
openContextMenu(rect.left, rect.bottom + 4, [
|
|
1064
|
+
{ id: 'open', label: 'Open Folder', icon: '<i class="fas fa-folder-open"></i>', action: () => loadFiles(1, folderPath) },
|
|
1065
|
+
'-',
|
|
1066
|
+
{ id: 'delete', label: 'Delete Folder', icon: '<i class="fas fa-trash"></i>', danger: true, action: () => promptDelete(folderPath + '/', true) }
|
|
1067
|
+
]);
|
|
1524
1068
|
}
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1525
1071
|
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
}
|
|
1072
|
+
// Folder click to navigate
|
|
1073
|
+
const folderItem = e.target.closest('[data-type="folder"]');
|
|
1074
|
+
if (folderItem) {
|
|
1075
|
+
e.stopPropagation();
|
|
1076
|
+
loadFiles(1, folderItem.dataset.folderPath);
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1534
1079
|
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
try {
|
|
1543
|
-
const ta = document.createElement('textarea');
|
|
1544
|
-
ta.value = link;
|
|
1545
|
-
ta.style.position = 'fixed';
|
|
1546
|
-
ta.style.opacity = '0';
|
|
1547
|
-
document.body.appendChild(ta);
|
|
1548
|
-
ta.select();
|
|
1549
|
-
document.execCommand('copy');
|
|
1550
|
-
document.body.removeChild(ta);
|
|
1551
|
-
showAlert('Link copied to clipboard!', 'success');
|
|
1552
|
-
} catch {
|
|
1553
|
-
showAlert('Failed to copy link', 'error');
|
|
1554
|
-
}
|
|
1555
|
-
};
|
|
1556
|
-
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
1557
|
-
navigator.clipboard.writeText(link).then(() => {
|
|
1558
|
-
showAlert('Link copied to clipboard!', 'success');
|
|
1559
|
-
}).catch(fallbackCopy);
|
|
1560
|
-
} else {
|
|
1561
|
-
fallbackCopy();
|
|
1562
|
-
}
|
|
1563
|
-
return;
|
|
1564
|
-
}
|
|
1080
|
+
// File click to preview
|
|
1081
|
+
const fileItem = e.target.closest('[data-type="file"]');
|
|
1082
|
+
if (fileItem && !e.target.closest('.file-select-cb')) {
|
|
1083
|
+
e.stopPropagation();
|
|
1084
|
+
openPreview(fileItem.dataset.key);
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1565
1087
|
|
|
1088
|
+
// Bucket Selector
|
|
1089
|
+
document.getElementById('bucketSelector')?.addEventListener('change', (e) => {
|
|
1090
|
+
const newBucket = e.target.value;
|
|
1091
|
+
if (!newBucket) return;
|
|
1092
|
+
activeBucket = newBucket;
|
|
1093
|
+
loadFiles(1, '');
|
|
1566
1094
|
});
|
|
1567
|
-
});
|
|
1568
1095
|
|
|
1569
|
-
//
|
|
1096
|
+
// Top action buttons & Modals
|
|
1097
|
+
document.querySelectorAll('[data-action="upload-files"]').forEach(btn => {
|
|
1098
|
+
btn.addEventListener('click', openUploadModal);
|
|
1099
|
+
});
|
|
1570
1100
|
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
if (event) {
|
|
1575
|
-
event.preventDefault();
|
|
1576
|
-
event.stopPropagation();
|
|
1577
|
-
}
|
|
1101
|
+
document.querySelectorAll('[data-action="create-folder"]').forEach(btn => {
|
|
1102
|
+
btn.addEventListener('click', openNewFolderModal);
|
|
1103
|
+
});
|
|
1578
1104
|
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
}
|
|
1105
|
+
// Drag and Drop Upload Zone
|
|
1106
|
+
const dropZone = document.getElementById('dropZone');
|
|
1107
|
+
const fileInput = document.getElementById('fileInput');
|
|
1583
1108
|
|
|
1584
|
-
|
|
1585
|
-
const extension = filename.split('.').pop().toLowerCase();
|
|
1586
|
-
const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico'];
|
|
1587
|
-
const videoTypes = ['mp4', 'webm', 'ogg'];
|
|
1588
|
-
|
|
1589
|
-
// Determine preview mode from extension groups
|
|
1590
|
-
const previewableImage = imageTypes.includes(extension);
|
|
1591
|
-
const previewableVideo = videoTypes.includes(extension);
|
|
1592
|
-
const previewableAudio = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a'].includes(extension);
|
|
1593
|
-
const previewablePdf = extension === 'pdf';
|
|
1594
|
-
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);
|
|
1595
|
-
|
|
1596
|
-
let previewMode = 'unsupported';
|
|
1597
|
-
if (previewableImage) previewMode = 'image';
|
|
1598
|
-
else if (previewableVideo) previewMode = 'video';
|
|
1599
|
-
else if (previewableAudio) previewMode = 'audio';
|
|
1600
|
-
else if (previewablePdf) previewMode = 'pdf';
|
|
1601
|
-
else if (previewableText) previewMode = 'text';
|
|
1602
|
-
|
|
1603
|
-
// Build an encoded key safely (if key already appears encoded, don't double-encode)
|
|
1604
|
-
const encodedKey = (key && key.includes('%')) ? key : encodeURIComponent(key || filename);
|
|
1605
|
-
|
|
1606
|
-
// Show the inline preview
|
|
1607
|
-
showPreviewModal(encodedKey, filename, extension, previewMode);
|
|
1608
|
-
return false;
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
// Function to get file type category
|
|
1612
|
-
|
|
1613
|
-
// Preview modal helpers
|
|
1614
|
-
function showPreviewModal(encodedKey, filename, extension, previewMode = 'auto') {
|
|
1615
|
-
const modal = document.getElementById('previewModal');
|
|
1616
|
-
if (!modal) {
|
|
1617
|
-
window.open(getViewUrl(encodedKey), '_blank', 'noopener,noreferrer');
|
|
1618
|
-
return;
|
|
1619
|
-
}
|
|
1109
|
+
dropZone?.addEventListener('click', () => fileInput?.click());
|
|
1620
1110
|
|
|
1621
|
-
|
|
1622
|
-
const titleEl = document.getElementById('previewFileName');
|
|
1623
|
-
const contentEl = modal.querySelector('.preview-content');
|
|
1624
|
-
const openBtn = document.getElementById('previewOpenBtn');
|
|
1625
|
-
const downloadBtn = document.getElementById('previewDownloadBtn');
|
|
1626
|
-
const closeBtn = document.getElementById('previewCloseBtn');
|
|
1627
|
-
const closeFooter = document.getElementById('previewCloseFooter');
|
|
1628
|
-
const fsBtn = document.getElementById('previewFullscreenBtn');
|
|
1629
|
-
|
|
1630
|
-
titleEl.textContent = filename;
|
|
1631
|
-
titleEl.title = filename;
|
|
1632
|
-
openBtn.href = getViewUrl(encodedKey);
|
|
1633
|
-
downloadBtn.href = '#';
|
|
1634
|
-
downloadBtn.onclick = (e) => {
|
|
1111
|
+
dropZone?.addEventListener('dragover', (e) => {
|
|
1635
1112
|
e.preventDefault();
|
|
1636
|
-
|
|
1637
|
-
};
|
|
1638
|
-
downloadBtn.removeAttribute('download');
|
|
1639
|
-
|
|
1640
|
-
// Reset sizing classes
|
|
1641
|
-
dialog.classList.remove('preview-large', 'preview-compact');
|
|
1642
|
-
|
|
1643
|
-
// Clear previous content and show loading
|
|
1644
|
-
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>`;
|
|
1645
|
-
|
|
1646
|
-
const imgTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico'];
|
|
1647
|
-
const videoTypes = ['mp4', 'webm', 'ogg'];
|
|
1648
|
-
const audioTypes = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a'];
|
|
1649
|
-
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'];
|
|
1650
|
-
const effectiveMode = previewMode === 'auto'
|
|
1651
|
-
? (imgTypes.includes(extension) ? 'image'
|
|
1652
|
-
: videoTypes.includes(extension) ? 'video'
|
|
1653
|
-
: audioTypes.includes(extension) ? 'audio'
|
|
1654
|
-
: extension === 'pdf' ? 'pdf'
|
|
1655
|
-
: textTypes.includes(extension) ? 'text'
|
|
1656
|
-
: 'unsupported')
|
|
1657
|
-
: previewMode;
|
|
1658
|
-
|
|
1659
|
-
// Render content and adjust dialog size
|
|
1660
|
-
if (effectiveMode === 'image') {
|
|
1661
|
-
dialog.classList.add('preview-compact');
|
|
1662
|
-
const img = document.createElement('img');
|
|
1663
|
-
img.className = 'preview-img';
|
|
1664
|
-
img.src = getViewUrl(encodedKey);
|
|
1665
|
-
img.alt = filename;
|
|
1666
|
-
img.onload = () => { };
|
|
1667
|
-
img.onerror = () => { contentEl.textContent = 'Could not load preview.'; };
|
|
1668
|
-
contentEl.innerHTML = '';
|
|
1669
|
-
contentEl.appendChild(img);
|
|
1670
|
-
} else if (effectiveMode === 'video') {
|
|
1671
|
-
dialog.classList.add('preview-large');
|
|
1672
|
-
const video = document.createElement('video');
|
|
1673
|
-
video.className = 'preview-video';
|
|
1674
|
-
video.controls = true;
|
|
1675
|
-
video.src = getViewUrl(encodedKey);
|
|
1676
|
-
contentEl.innerHTML = '';
|
|
1677
|
-
contentEl.appendChild(video);
|
|
1678
|
-
} else if (effectiveMode === 'audio') {
|
|
1679
|
-
dialog.classList.add('preview-compact');
|
|
1680
|
-
const audio = document.createElement('audio');
|
|
1681
|
-
audio.className = 'preview-audio';
|
|
1682
|
-
audio.controls = true;
|
|
1683
|
-
audio.src = getViewUrl(encodedKey);
|
|
1684
|
-
contentEl.innerHTML = '';
|
|
1685
|
-
contentEl.appendChild(audio);
|
|
1686
|
-
} else if (effectiveMode === 'pdf') {
|
|
1687
|
-
dialog.classList.add('preview-large');
|
|
1688
|
-
const iframe = document.createElement('iframe');
|
|
1689
|
-
iframe.className = 'preview-iframe';
|
|
1690
|
-
iframe.src = getViewUrl(encodedKey);
|
|
1691
|
-
contentEl.innerHTML = '';
|
|
1692
|
-
contentEl.appendChild(iframe);
|
|
1693
|
-
} else if (effectiveMode === 'text') {
|
|
1694
|
-
dialog.classList.add('preview-compact');
|
|
1695
|
-
|
|
1696
|
-
fetch(getViewUrl(encodedKey))
|
|
1697
|
-
.then(r => r.text())
|
|
1698
|
-
.then(text => {
|
|
1699
|
-
contentEl.innerHTML = '';
|
|
1700
|
-
const pre = document.createElement('pre');
|
|
1701
|
-
pre.textContent = text;
|
|
1702
|
-
pre.className = 'preview-text';
|
|
1703
|
-
contentEl.appendChild(pre);
|
|
1704
|
-
})
|
|
1705
|
-
.catch(err => { contentEl.textContent = 'Could not load preview.'; });
|
|
1706
|
-
} else {
|
|
1707
|
-
contentEl.innerHTML = `<div style="text-align:center;">Preview not available for this file type. <a href="${getDownloadUrl(encodedKey)}">Download</a></div>`;
|
|
1708
|
-
}
|
|
1113
|
+
dropZone.classList.add('dragover');
|
|
1114
|
+
});
|
|
1709
1115
|
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
modal.setAttribute('aria-hidden', 'false');
|
|
1714
|
-
|
|
1715
|
-
// Focus management + focus trap
|
|
1716
|
-
const prevFocus = document.activeElement;
|
|
1717
|
-
const focusableSelector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
1718
|
-
function getFocusable() { return Array.from(modal.querySelectorAll(focusableSelector)).filter(el => !el.hasAttribute('disabled')); }
|
|
1719
|
-
const focusable = () => getFocusable();
|
|
1720
|
-
const firstFocusable = () => focusable()[0];
|
|
1721
|
-
const lastFocusable = () => focusable()[focusable().length - 1];
|
|
1722
|
-
|
|
1723
|
-
// Focus initial
|
|
1724
|
-
(closeBtn || closeFooter).focus();
|
|
1725
|
-
|
|
1726
|
-
function onKeyDown(e) {
|
|
1727
|
-
if (e.key === 'Escape') return cleanup();
|
|
1728
|
-
if (e.key === 'Tab') {
|
|
1729
|
-
const f = focusable();
|
|
1730
|
-
if (f.length === 0) return;
|
|
1731
|
-
if (e.shiftKey && document.activeElement === f[0]) { e.preventDefault(); f[f.length - 1].focus(); }
|
|
1732
|
-
else if (!e.shiftKey && document.activeElement === f[f.length - 1]) { e.preventDefault(); f[0].focus(); }
|
|
1733
|
-
}
|
|
1734
|
-
}
|
|
1116
|
+
dropZone?.addEventListener('dragleave', () => {
|
|
1117
|
+
dropZone.classList.remove('dragover');
|
|
1118
|
+
});
|
|
1735
1119
|
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
fsActive = true;
|
|
1745
|
-
} else {
|
|
1746
|
-
document.exitFullscreen?.();
|
|
1747
|
-
fsBtn.innerHTML = '<i class="fas fa-expand"></i>';
|
|
1748
|
-
fsActive = false;
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1120
|
+
dropZone?.addEventListener('drop', (e) => {
|
|
1121
|
+
e.preventDefault();
|
|
1122
|
+
dropZone.classList.remove('dragover');
|
|
1123
|
+
if (e.dataTransfer.files?.length) {
|
|
1124
|
+
uploadQueue.push(...Array.from(e.dataTransfer.files));
|
|
1125
|
+
updateUploadQueueUI();
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
1751
1128
|
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
if (prevFocus && prevFocus.focus) prevFocus.focus();
|
|
1759
|
-
modal.removeEventListener('click', onBackdropClick);
|
|
1760
|
-
document.removeEventListener('keydown', onKeyDown);
|
|
1761
|
-
fsBtn.removeEventListener('click', toggleFullscreen);
|
|
1762
|
-
(closeBtn || closeFooter).removeEventListener('click', cleanup);
|
|
1763
|
-
closeFooter.removeEventListener('click', cleanup);
|
|
1764
|
-
}
|
|
1129
|
+
fileInput?.addEventListener('change', (e) => {
|
|
1130
|
+
if (e.target.files?.length) {
|
|
1131
|
+
uploadQueue.push(...Array.from(e.target.files));
|
|
1132
|
+
updateUploadQueueUI();
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1765
1135
|
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1136
|
+
document.getElementById('uploadQueue')?.addEventListener('click', (e) => {
|
|
1137
|
+
const removeBtn = e.target.closest('[data-remove-queue]');
|
|
1138
|
+
if (removeBtn) {
|
|
1139
|
+
const idx = parseInt(removeBtn.dataset.removeQueue, 10);
|
|
1140
|
+
uploadQueue.splice(idx, 1);
|
|
1141
|
+
updateUploadQueueUI();
|
|
1142
|
+
}
|
|
1143
|
+
});
|
|
1772
1144
|
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
modal.classList.remove('show');
|
|
1777
|
-
modal.style.display = 'none';
|
|
1778
|
-
modal.setAttribute('aria-hidden', 'true');
|
|
1779
|
-
}
|
|
1780
|
-
|
|
1781
|
-
// Update file icons in the table
|
|
1782
|
-
function updateFileIcons() {
|
|
1783
|
-
const fileRows = document.querySelectorAll('.file-row');
|
|
1784
|
-
fileRows.forEach(row => {
|
|
1785
|
-
const fileNameElement = row.querySelector('.file-name');
|
|
1786
|
-
const folderPathElement = row.querySelector('.folder-path');
|
|
1787
|
-
const fileIconElement = row.querySelector('.file-icon i');
|
|
1788
|
-
const fileIconContainer = row.querySelector('.file-icon');
|
|
1789
|
-
const viewButton = row.querySelector('.view-btn');
|
|
1790
|
-
|
|
1791
|
-
if (fileNameElement && fileIconElement && fileIconContainer) {
|
|
1792
|
-
const fullPath = fileNameElement.getAttribute('data-full-path');
|
|
1793
|
-
|
|
1794
|
-
// Split the path into folder and filename
|
|
1795
|
-
const lastSlashIndex = fullPath.lastIndexOf('/');
|
|
1796
|
-
let folder = '';
|
|
1797
|
-
let filename = fullPath;
|
|
1798
|
-
|
|
1799
|
-
if (lastSlashIndex !== -1) {
|
|
1800
|
-
folder = fullPath.substring(0, lastSlashIndex);
|
|
1801
|
-
filename = fullPath.substring(lastSlashIndex + 1);
|
|
1802
|
-
}
|
|
1803
|
-
|
|
1804
|
-
// Truncate long filenames
|
|
1805
|
-
const truncatedName = truncateFileName(filename);
|
|
1806
|
-
|
|
1807
|
-
// Update the display
|
|
1808
|
-
fileNameElement.textContent = truncatedName;
|
|
1809
|
-
fileNameElement.title = filename; // Full name on hover
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
const iconClass = getFileIcon(filename);
|
|
1814
|
-
const typeCategory = getFileTypeCategory(filename);
|
|
1815
|
-
|
|
1816
|
-
// Update icon
|
|
1817
|
-
fileIconElement.className = iconClass;
|
|
1818
|
-
|
|
1819
|
-
// Add type-specific styling
|
|
1820
|
-
if (typeCategory) {
|
|
1821
|
-
fileIconContainer.classList.add(typeCategory);
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
// Show/hide and update view button based on file type
|
|
1825
|
-
if (viewButton) {
|
|
1826
|
-
const isFileViewable = isViewable(filename);
|
|
1827
|
-
|
|
1828
|
-
if (isFileViewable) {
|
|
1829
|
-
viewButton.style.display = 'inline-flex';
|
|
1830
|
-
viewButton.style.opacity = '1';
|
|
1831
|
-
viewButton.title = 'View file in browser';
|
|
1832
|
-
viewButton.disabled = false;
|
|
1833
|
-
} else {
|
|
1834
|
-
viewButton.style.opacity = '0.5';
|
|
1835
|
-
viewButton.title = 'File type not supported for viewing';
|
|
1836
|
-
viewButton.disabled = true;
|
|
1837
|
-
}
|
|
1838
|
-
}
|
|
1839
|
-
}
|
|
1145
|
+
document.getElementById('clearQueueBtn')?.addEventListener('click', () => {
|
|
1146
|
+
uploadQueue = [];
|
|
1147
|
+
updateUploadQueueUI();
|
|
1840
1148
|
});
|
|
1841
|
-
}
|
|
1842
1149
|
|
|
1843
|
-
|
|
1844
|
-
function setupSelection() {
|
|
1845
|
-
const selectAll = document.getElementById('selectAllCheckbox');
|
|
1846
|
-
const bulkBar = document.getElementById('bulkActionsBar');
|
|
1847
|
-
const bulkCount = document.getElementById('bulkSelectedCount');
|
|
1848
|
-
const bulkDeleteBtn = document.getElementById('bulkDeleteBtn');
|
|
1150
|
+
document.getElementById('startUploadBtn')?.addEventListener('click', processUploads);
|
|
1849
1151
|
|
|
1850
|
-
|
|
1152
|
+
// Fullscreen Preview Toggle
|
|
1153
|
+
document.getElementById('previewFullscreenBtn')?.addEventListener('click', () => {
|
|
1154
|
+
document.querySelector('.preview-modal-dialog')?.classList.toggle('is-fullscreen');
|
|
1155
|
+
});
|
|
1851
1156
|
|
|
1852
|
-
|
|
1157
|
+
// Pagination clicks
|
|
1158
|
+
document.getElementById('paginationList')?.addEventListener('click', (e) => {
|
|
1159
|
+
const link = e.target.closest('[data-page]');
|
|
1160
|
+
if (!link) return;
|
|
1161
|
+
const page = parseInt(link.dataset.page, 10);
|
|
1162
|
+
if (!isNaN(page) && page > 0) {
|
|
1163
|
+
loadFiles(page, currentPrefix, currentSearch);
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1853
1166
|
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
bulkCount.textContent = `${count} selected`;
|
|
1857
|
-
bulkBar.style.display = count > 0 ? 'block' : 'none';
|
|
1858
|
-
bulkDeleteBtn.disabled = count === 0;
|
|
1859
|
-
if (selectAll) selectAll.checked = checkboxes.length > 0 && selected.size === checkboxes.length;
|
|
1860
|
-
}
|
|
1167
|
+
// Global click to close context menu
|
|
1168
|
+
document.addEventListener('click', closeContextMenu);
|
|
1861
1169
|
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
});
|
|
1170
|
+
// Handle delete form submission
|
|
1171
|
+
const deleteForm = document.getElementById('deleteForm');
|
|
1172
|
+
deleteForm?.addEventListener('submit', async (e) => {
|
|
1173
|
+
e.preventDefault();
|
|
1174
|
+
const formData = new FormData(deleteForm);
|
|
1175
|
+
const key = formData.get('key');
|
|
1176
|
+
const isFolder = formData.get('folder') === 'true';
|
|
1870
1177
|
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
checkboxes.forEach(cb => { cb.checked = checked; if (checked) selected.add(cb.dataset.key); });
|
|
1877
|
-
updateUI();
|
|
1178
|
+
try {
|
|
1179
|
+
const response = await fetch(withBucketUrl('/mbkbucket/delete'), {
|
|
1180
|
+
method: 'POST',
|
|
1181
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1182
|
+
body: JSON.stringify({ key, isFolder, prefix: currentPrefix })
|
|
1878
1183
|
});
|
|
1879
|
-
|
|
1184
|
+
const data = await response.json();
|
|
1185
|
+
if (!data.success) throw new Error(data.error || 'Delete failed');
|
|
1880
1186
|
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
}
|
|
1897
|
-
});
|
|
1187
|
+
closeModal();
|
|
1188
|
+
showToast(`Successfully deleted ${key}`, 'success');
|
|
1189
|
+
loadFiles(currentPage, currentPrefix);
|
|
1190
|
+
} catch (err) {
|
|
1191
|
+
showToast(err.message, 'error');
|
|
1192
|
+
}
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// Initialization
|
|
1197
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
1198
|
+
// Sync view mode buttons
|
|
1199
|
+
if (currentViewMode === 'list') {
|
|
1200
|
+
document.getElementById('view-list')?.classList.add('active');
|
|
1201
|
+
document.getElementById('view-grid')?.classList.remove('active');
|
|
1898
1202
|
}
|
|
1899
|
-
}
|
|
1900
1203
|
|
|
1901
|
-
|
|
1902
|
-
async function createFolder() {
|
|
1903
|
-
const name = prompt('Enter new folder name (no slashes):');
|
|
1904
|
-
if (!name) return;
|
|
1905
|
-
const safe = name.replace(/[\\/]+/g, '').trim();
|
|
1906
|
-
if (!safe) return showAlert('Invalid folder name', 'error');
|
|
1204
|
+
initEventListeners();
|
|
1907
1205
|
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
if (!name) return;
|
|
1926
|
-
|
|
1927
|
-
name = name.trim();
|
|
1928
|
-
if (!name.includes('.')) {
|
|
1929
|
-
name += '.txt';
|
|
1930
|
-
}
|
|
1931
|
-
|
|
1932
|
-
// Ensure no leading slashes, we upload to currentPrefix
|
|
1933
|
-
if (name.startsWith('/')) name = name.substring(1);
|
|
1934
|
-
|
|
1935
|
-
// Check if valid
|
|
1936
|
-
if (!name) return showAlert('Invalid file name', 'error');
|
|
1937
|
-
|
|
1938
|
-
// Create empty file content
|
|
1939
|
-
const file = new File([" "], name, { type: "text/plain" });
|
|
1940
|
-
const formData = new FormData();
|
|
1941
|
-
formData.append('file', file);
|
|
1942
|
-
formData.append('prefix', currentPrefix);
|
|
1943
|
-
|
|
1944
|
-
try {
|
|
1945
|
-
const resp = await fetch(withBucketUrl('/mbkbucket/upload'), {
|
|
1946
|
-
method: 'POST',
|
|
1947
|
-
body: formData
|
|
1948
|
-
});
|
|
1949
|
-
|
|
1950
|
-
const json = await resp.json();
|
|
1951
|
-
if (json.success) {
|
|
1952
|
-
showAlert('File created', 'success');
|
|
1953
|
-
loadFiles(currentPage, currentPrefix);
|
|
1954
|
-
} else {
|
|
1955
|
-
showAlert(json.error || 'Create file failed', 'error');
|
|
1956
|
-
}
|
|
1957
|
-
} catch (err) {
|
|
1958
|
-
showAlert('Create file failed: ' + err.message, 'error');
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
// mbkbucket shared utilities (exposed to global window for backward compatibility)
|
|
1965
|
-
(function (w) {
|
|
1966
|
-
'use strict';
|
|
1967
|
-
|
|
1968
|
-
w.formatDate = function (dateString) {
|
|
1969
|
-
const date = new Date(dateString);
|
|
1970
|
-
return date.toLocaleString();
|
|
1971
|
-
};
|
|
1972
|
-
|
|
1973
|
-
w.formatFileSize = function (bytes) {
|
|
1974
|
-
if (bytes === 0) return '0 Bytes';
|
|
1975
|
-
const k = 1024;
|
|
1976
|
-
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
1977
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1978
|
-
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
1979
|
-
};
|
|
1980
|
-
|
|
1981
|
-
w.formatTime = function (seconds) {
|
|
1982
|
-
if (seconds < 60) {
|
|
1983
|
-
return Math.round(seconds) + 's';
|
|
1984
|
-
} else if (seconds < 3600) {
|
|
1985
|
-
const minutes = Math.floor(seconds / 60);
|
|
1986
|
-
const remainingSeconds = Math.round(seconds % 60);
|
|
1987
|
-
return minutes + 'm ' + remainingSeconds + 's';
|
|
1988
|
-
} else {
|
|
1989
|
-
const hours = Math.floor(seconds / 3600);
|
|
1990
|
-
const minutes = Math.floor((seconds % 3600) / 60);
|
|
1991
|
-
return hours + 'h ' + minutes + 'm';
|
|
1992
|
-
}
|
|
1993
|
-
};
|
|
1994
|
-
|
|
1995
|
-
w.getFileIcon = function (filename) {
|
|
1996
|
-
const extension = (filename || '').split('.').pop().toLowerCase();
|
|
1997
|
-
|
|
1998
|
-
const iconMap = {
|
|
1999
|
-
jpg: 'fas fa-image',
|
|
2000
|
-
jpeg: 'fas fa-image',
|
|
2001
|
-
png: 'fas fa-image',
|
|
2002
|
-
gif: 'fas fa-image',
|
|
2003
|
-
webp: 'fas fa-image',
|
|
2004
|
-
svg: 'fas fa-image',
|
|
2005
|
-
bmp: 'fas fa-image',
|
|
2006
|
-
ico: 'fas fa-image',
|
|
2007
|
-
mp4: 'fas fa-video',
|
|
2008
|
-
webm: 'fas fa-video',
|
|
2009
|
-
ogg: 'fas fa-video',
|
|
2010
|
-
avi: 'fas fa-video',
|
|
2011
|
-
mov: 'fas fa-video',
|
|
2012
|
-
wmv: 'fas fa-video',
|
|
2013
|
-
flv: 'fas fa-video',
|
|
2014
|
-
mkv: 'fas fa-video',
|
|
2015
|
-
mp3: 'fas fa-music',
|
|
2016
|
-
wav: 'fas fa-music',
|
|
2017
|
-
flac: 'fas fa-music',
|
|
2018
|
-
aac: 'fas fa-music',
|
|
2019
|
-
m4a: 'fas fa-music',
|
|
2020
|
-
pdf: 'fas fa-file-pdf',
|
|
2021
|
-
doc: 'fas fa-file-word',
|
|
2022
|
-
docx: 'fas fa-file-word',
|
|
2023
|
-
xls: 'fas fa-file-excel',
|
|
2024
|
-
xlsx: 'fas fa-file-excel',
|
|
2025
|
-
ppt: 'fas fa-file-powerpoint',
|
|
2026
|
-
pptx: 'fas fa-file-powerpoint',
|
|
2027
|
-
js: 'fab fa-js-square',
|
|
2028
|
-
ts: 'fas fa-code',
|
|
2029
|
-
html: 'fab fa-html5',
|
|
2030
|
-
htm: 'fab fa-html5',
|
|
2031
|
-
css: 'fab fa-css3-alt',
|
|
2032
|
-
php: 'fab fa-php',
|
|
2033
|
-
py: 'fab fa-python',
|
|
2034
|
-
java: 'fab fa-java',
|
|
2035
|
-
cpp: 'fas fa-code',
|
|
2036
|
-
c: 'fas fa-code',
|
|
2037
|
-
h: 'fas fa-code',
|
|
2038
|
-
cs: 'fas fa-code',
|
|
2039
|
-
rb: 'fas fa-code',
|
|
2040
|
-
go: 'fas fa-code',
|
|
2041
|
-
rs: 'fas fa-code',
|
|
2042
|
-
sql: 'fas fa-database',
|
|
2043
|
-
sh: 'fas fa-terminal',
|
|
2044
|
-
bat: 'fas fa-terminal',
|
|
2045
|
-
ps1: 'fas fa-terminal',
|
|
2046
|
-
txt: 'fas fa-file-alt',
|
|
2047
|
-
md: 'fab fa-markdown',
|
|
2048
|
-
json: 'fas fa-code',
|
|
2049
|
-
xml: 'fas fa-code',
|
|
2050
|
-
csv: 'fas fa-file-csv',
|
|
2051
|
-
log: 'fas fa-file-alt',
|
|
2052
|
-
yaml: 'fas fa-code',
|
|
2053
|
-
yml: 'fas fa-code',
|
|
2054
|
-
toml: 'fas fa-code',
|
|
2055
|
-
ini: 'fas fa-cog',
|
|
2056
|
-
conf: 'fas fa-cog',
|
|
2057
|
-
zip: 'fas fa-file-archive',
|
|
2058
|
-
rar: 'fas fa-file-archive',
|
|
2059
|
-
'7z': 'fas fa-file-archive',
|
|
2060
|
-
tar: 'fas fa-file-archive',
|
|
2061
|
-
gz: 'fas fa-file-archive'
|
|
2062
|
-
};
|
|
2063
|
-
|
|
2064
|
-
return iconMap[extension] || 'fas fa-file-alt';
|
|
2065
|
-
};
|
|
2066
|
-
|
|
2067
|
-
w.isViewable = function (filename) {
|
|
2068
|
-
const extension = (filename || '').split('.').pop().toLowerCase();
|
|
2069
|
-
const viewableTypes = [
|
|
2070
|
-
'jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico',
|
|
2071
|
-
'mp4', 'webm', 'ogg',
|
|
2072
|
-
'mp3', 'wav', 'ogg',
|
|
2073
|
-
'txt', 'md', 'json', 'xml', 'csv', 'log', 'js', 'ts', 'html', 'htm', 'css',
|
|
2074
|
-
'php', 'py', 'java', 'cpp', 'c', 'h', 'cs', 'rb', 'go', 'rs', 'sql',
|
|
2075
|
-
'sh', 'bat', 'ps1', 'yaml', 'yml', 'toml', 'ini', 'conf',
|
|
2076
|
-
'pdf'
|
|
2077
|
-
];
|
|
2078
|
-
|
|
2079
|
-
return viewableTypes.includes(extension);
|
|
2080
|
-
};
|
|
2081
|
-
|
|
2082
|
-
w.getFileTypeCategory = function (filename) {
|
|
2083
|
-
const extension = (filename || '').split('.').pop().toLowerCase();
|
|
2084
|
-
const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico'];
|
|
2085
|
-
const videoTypes = ['mp4', 'webm', 'ogg', 'avi', 'mov', 'wmv', 'flv', 'mkv'];
|
|
2086
|
-
const audioTypes = ['mp3', 'wav', 'flac', 'aac', 'm4a'];
|
|
2087
|
-
const documentTypes = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'];
|
|
2088
|
-
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'];
|
|
2089
|
-
|
|
2090
|
-
if (imageTypes.includes(extension)) return 'image-file';
|
|
2091
|
-
if (videoTypes.includes(extension)) return 'video-file';
|
|
2092
|
-
if (audioTypes.includes(extension)) return 'audio-file';
|
|
2093
|
-
if (documentTypes.includes(extension)) return 'document-file';
|
|
2094
|
-
if (codeTypes.includes(extension)) return 'code-file';
|
|
2095
|
-
|
|
2096
|
-
return '';
|
|
2097
|
-
};
|
|
2098
|
-
|
|
2099
|
-
w.truncateFileName = function (filename) {
|
|
2100
|
-
const isMobile = window.innerWidth <= 768;
|
|
2101
|
-
if (isMobile) return filename;
|
|
2102
|
-
const maxLength = 30;
|
|
2103
|
-
if ((filename || '').length <= maxLength) return filename;
|
|
2104
|
-
const dotIndex = filename.lastIndexOf('.');
|
|
2105
|
-
let name = filename;
|
|
2106
|
-
let extension = '';
|
|
2107
|
-
if (dotIndex !== -1) {
|
|
2108
|
-
name = filename.substring(0, dotIndex);
|
|
2109
|
-
extension = filename.substring(dotIndex);
|
|
2110
|
-
}
|
|
2111
|
-
const lastChars = 3;
|
|
2112
|
-
const extensionLength = extension.length;
|
|
2113
|
-
const ellipsis = '...';
|
|
2114
|
-
const availableForName = maxLength - ellipsis.length - lastChars - extensionLength;
|
|
2115
|
-
if (availableForName <= 0) return filename.substring(0, maxLength - ellipsis.length) + ellipsis;
|
|
2116
|
-
const firstPart = name.substring(0, availableForName);
|
|
2117
|
-
const lastPart = name.substring(name.length - lastChars);
|
|
2118
|
-
return firstPart + ellipsis + lastPart + extension;
|
|
2119
|
-
};
|
|
2120
|
-
|
|
2121
|
-
// Debounce helper - returns a function that delays invocation until after wait ms
|
|
2122
|
-
w.debounce = function (fn, wait) {
|
|
2123
|
-
let timeout = null;
|
|
2124
|
-
return function () {
|
|
2125
|
-
const ctx = this;
|
|
2126
|
-
const args = arguments;
|
|
2127
|
-
clearTimeout(timeout);
|
|
2128
|
-
timeout = setTimeout(function () {
|
|
2129
|
-
fn.apply(ctx, args);
|
|
2130
|
-
}, wait);
|
|
2131
|
-
};
|
|
2132
|
-
};
|
|
2133
|
-
|
|
2134
|
-
})(window);
|
|
1206
|
+
// Initial load with URL params
|
|
1207
|
+
const initialFolder = urlParams.get('folder') || '';
|
|
1208
|
+
const initialPage = parseInt(urlParams.get('page') || '1', 10);
|
|
1209
|
+
const initialSearch = urlParams.get('search') || '';
|
|
1210
|
+
|
|
1211
|
+
loadFiles(initialPage, initialFolder, initialSearch);
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
// Expose global methods if needed
|
|
1215
|
+
window.mbkbucket = {
|
|
1216
|
+
loadFiles,
|
|
1217
|
+
openPreview,
|
|
1218
|
+
downloadFile,
|
|
1219
|
+
showToast
|
|
1220
|
+
};
|
|
1221
|
+
|
|
1222
|
+
})();
|