mbkbucket 3.5.1 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +14 -14
- package/.mbkbucket.config.example.json +14 -14
- package/LICENSE +20 -20
- package/README.md +377 -350
- package/index.js +30 -30
- package/lib/cli.js +649 -649
- package/lib/public/bucketadmin.css +1420 -1420
- package/lib/public/bucketadmin.js +1229 -1229
- package/lib/public/mbkbucket-helper-demo.html +1184 -1184
- package/lib/public/mbkbucket-helper.css +1235 -1235
- package/lib/public/mbkbucket-helper.js +1557 -1557
- package/lib/src/app.js +109 -109
- package/lib/src/cli/auth-helper.js +150 -150
- package/lib/src/config/index.js +112 -112
- package/lib/src/config/validation.js +73 -73
- package/lib/src/controllers/api.controller.js +410 -410
- package/lib/src/controllers/dashboard.controller.js +48 -48
- package/lib/src/controllers/info.controller.js +34 -34
- package/lib/src/controllers/view.controller.js +233 -233
- package/lib/src/middleware/async-handler.js +23 -23
- package/lib/src/middleware/bucket-resolver.js +37 -37
- package/lib/src/middleware/error-handler.js +54 -54
- package/lib/src/middleware/rate-limiter.js +22 -22
- package/lib/src/middleware/security.js +27 -27
- package/lib/src/routes/api.routes.js +54 -40
- package/lib/src/routes/index.js +40 -21
- package/lib/src/routes/info.routes.js +9 -9
- package/lib/src/routes/view.routes.js +25 -20
- package/lib/src/services/s3.service.js +603 -603
- package/lib/src/utils/errors.js +54 -54
- package/lib/src/utils/helpers.js +251 -251
- package/lib/src/utils/logger.js +17 -17
- package/package.json +83 -83
- package/views/bucket.handlebars +196 -196
- package/views/bucketadmin_alerts.handlebars +15 -15
- package/views/bucketadmin_delete_modal.handlebars +36 -36
- package/views/bucketadmin_filelist_skeleton.handlebars +32 -32
- package/views/bucketadmin_newfolder_modal.handlebars +29 -29
- package/views/bucketadmin_preview_modal.handlebars +40 -40
- package/views/bucketadmin_upload_modal.handlebars +54 -54
- package/views/bucketadmincontent.handlebars +141 -141
- package/views/mbkbucket_info.handlebars +232 -232
|
@@ -1,1229 +1,1229 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MBKBucket
|
|
3
|
-
* Copyright (c) 2026 Muhammad Bin Khalid, MBKTech.org and contributors
|
|
4
|
-
* Licensed under the MIT License.
|
|
5
|
-
* Source: https://github.com/MIbnEKhalid/mbkbucket
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* MBKBucket File Manager - UI & Client Logic
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
(function () {
|
|
13
|
-
'use strict';
|
|
14
|
-
|
|
15
|
-
// Global State
|
|
16
|
-
let currentPage = 1;
|
|
17
|
-
let currentPrefix = '';
|
|
18
|
-
let currentSearch = '';
|
|
19
|
-
let currentSort = 'name';
|
|
20
|
-
let currentOrder = 'asc';
|
|
21
|
-
let currentViewMode = localStorage.getItem('mbk_view_mode') || 'grid'; // 'grid' or 'list'
|
|
22
|
-
let currentQuickFilter = 'all'; // 'all', 'media', 'docs', 'code', 'starred'
|
|
23
|
-
let pageFiles = [];
|
|
24
|
-
let pageFolders = [];
|
|
25
|
-
let currentApiPrefix = '';
|
|
26
|
-
let currentNextToken = null;
|
|
27
|
-
let selectedFiles = new Set();
|
|
28
|
-
let starredFiles = new Set(JSON.parse(localStorage.getItem('mbk_starred_keys') || '[]'));
|
|
29
|
-
|
|
30
|
-
// Upload State
|
|
31
|
-
let uploadQueue = [];
|
|
32
|
-
let isUploading = false;
|
|
33
|
-
|
|
34
|
-
// DOM Elements & Config
|
|
35
|
-
const bucketAppData = document.getElementById('bucketAppData');
|
|
36
|
-
const bucketAppName = bucketAppData?.dataset.appName || '';
|
|
37
|
-
const urlParams = new URLSearchParams(window.location.search);
|
|
38
|
-
const selectedBucket = urlParams.get('bucket') || '';
|
|
39
|
-
let activeBucket = selectedBucket;
|
|
40
|
-
const app = String(bucketAppName || '').toLowerCase();
|
|
41
|
-
const isRootApp = app === 'portal' || app === 'mbkbucket';
|
|
42
|
-
const rootLabel = isRootApp ? 'My Drive' : (bucketAppName || 'Bucket Drive');
|
|
43
|
-
|
|
44
|
-
// File categories & extensions
|
|
45
|
-
const MEDIA_EXTS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'mp4', 'webm', 'ogg', 'avi', 'mov', 'mp3', 'wav', 'flac', 'aac', 'm4a']);
|
|
46
|
-
const DOC_EXTS = new Set(['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'md', 'csv', 'rtf']);
|
|
47
|
-
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']);
|
|
48
|
-
|
|
49
|
-
const FILE_TYPES = {
|
|
50
|
-
pdf: { color: 'var(--red)', bg: 'var(--red-lt)', label: 'PDF' },
|
|
51
|
-
doc: { color: 'var(--blue)', bg: 'var(--blue-lt)', label: 'DOC' },
|
|
52
|
-
docx: { color: 'var(--blue)', bg: 'var(--blue-lt)', label: 'DOC' },
|
|
53
|
-
xls: { color: 'var(--green)', bg: 'var(--green-lt)', label: 'XLS' },
|
|
54
|
-
xlsx: { color: 'var(--green)', bg: 'var(--green-lt)', label: 'XLS' },
|
|
55
|
-
csv: { color: 'var(--green)', bg: 'var(--green-lt)', label: 'CSV' },
|
|
56
|
-
ppt: { color: 'var(--orange)', bg: 'rgba(247, 103, 7, 0.1)', label: 'PPT' },
|
|
57
|
-
pptx: { color: 'var(--orange)', bg: 'rgba(247, 103, 7, 0.1)', label: 'PPT' },
|
|
58
|
-
txt: { color: 'var(--text-secondary)', bg: 'var(--bg-surface-secondary)', label: 'TXT' },
|
|
59
|
-
md: { color: 'var(--purple)', bg: 'var(--purple-lt)', label: 'MD' },
|
|
60
|
-
jpg: { color: 'var(--yellow)', bg: 'var(--yellow-lt)', label: 'IMG' },
|
|
61
|
-
jpeg: { color: 'var(--yellow)', bg: 'var(--yellow-lt)', label: 'IMG' },
|
|
62
|
-
png: { color: 'var(--yellow)', bg: 'var(--yellow-lt)', label: 'IMG' },
|
|
63
|
-
gif: { color: 'var(--yellow)', bg: 'var(--yellow-lt)', label: 'GIF' },
|
|
64
|
-
webp: { color: 'var(--yellow)', bg: 'var(--yellow-lt)', label: 'IMG' },
|
|
65
|
-
svg: { color: 'var(--orange)', bg: 'rgba(247, 103, 7, 0.1)', label: 'SVG' },
|
|
66
|
-
mp4: { color: 'var(--azure)', bg: 'var(--azure-lt)', label: 'VID' },
|
|
67
|
-
webm: { color: 'var(--azure)', bg: 'var(--azure-lt)', label: 'VID' },
|
|
68
|
-
mov: { color: 'var(--azure)', bg: 'var(--azure-lt)', label: 'VID' },
|
|
69
|
-
mp3: { color: 'var(--purple)', bg: 'var(--purple-lt)', label: 'AUD' },
|
|
70
|
-
wav: { color: 'var(--purple)', bg: 'var(--purple-lt)', label: 'AUD' },
|
|
71
|
-
zip: { color: 'var(--text-secondary)', bg: 'var(--bg-surface-secondary)', label: 'ZIP' },
|
|
72
|
-
tar: { color: 'var(--text-secondary)', bg: 'var(--bg-surface-secondary)', label: 'TAR' },
|
|
73
|
-
gz: { color: 'var(--text-secondary)', bg: 'var(--bg-surface-secondary)', label: 'GZ' },
|
|
74
|
-
code: { color: 'var(--cyan)', bg: 'var(--cyan-lt)', label: 'CODE' }
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
// SVGs
|
|
78
|
-
const ICONS = {
|
|
79
|
-
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>',
|
|
80
|
-
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>',
|
|
81
|
-
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>',
|
|
82
|
-
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>',
|
|
83
|
-
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>',
|
|
84
|
-
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>',
|
|
85
|
-
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>',
|
|
86
|
-
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>'
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
// Helper Functions
|
|
90
|
-
function escapeHtml(str) {
|
|
91
|
-
return String(str || '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function escapeAttr(str) {
|
|
95
|
-
return escapeHtml(str);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function getBaseName(path) {
|
|
99
|
-
const val = String(path || '').replace(/\/+$/, '');
|
|
100
|
-
const lastSlash = val.lastIndexOf('/');
|
|
101
|
-
return lastSlash === -1 ? val : val.substring(lastSlash + 1);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function getFileExt(fileName) {
|
|
105
|
-
const name = getBaseName(fileName);
|
|
106
|
-
const dot = name.lastIndexOf('.');
|
|
107
|
-
return dot > -1 ? name.slice(dot + 1).toLowerCase() : '';
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function formatFileSize(bytes) {
|
|
111
|
-
if (!bytes || bytes === 0) return '0 B';
|
|
112
|
-
const k = 1024;
|
|
113
|
-
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
114
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
115
|
-
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function formatDate(dateStr) {
|
|
119
|
-
if (!dateStr) return '—';
|
|
120
|
-
const d = new Date(dateStr);
|
|
121
|
-
if (isNaN(d.getTime())) return String(dateStr);
|
|
122
|
-
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function withBucketUrl(url) {
|
|
126
|
-
if (!activeBucket) return url;
|
|
127
|
-
const sep = url.includes('?') ? '&' : '?';
|
|
128
|
-
return `${url}${sep}bucket=${encodeURIComponent(activeBucket)}`;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function getViewUrl(key) {
|
|
132
|
-
return withBucketUrl(`/mbkbucket/view/${encodeURIComponent(key)}`);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function getDownloadUrl(key) {
|
|
136
|
-
return withBucketUrl(`/mbkbucket/download/${encodeURIComponent(key)}`);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function isImage(fileName) {
|
|
140
|
-
const ext = getFileExt(fileName);
|
|
141
|
-
return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp'].includes(ext);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function isVideo(fileName) {
|
|
145
|
-
const ext = getFileExt(fileName);
|
|
146
|
-
return ['mp4', 'webm', 'ogg', 'mov', 'mkv'].includes(ext);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function isAudio(fileName) {
|
|
150
|
-
const ext = getFileExt(fileName);
|
|
151
|
-
return ['mp3', 'wav', 'ogg', 'flac', 'm4a', 'aac'].includes(ext);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function isTextOrCode(fileName) {
|
|
155
|
-
const ext = getFileExt(fileName);
|
|
156
|
-
return CODE_EXTS.has(ext) || ['txt', 'md', 'csv', 'log', 'json', 'xml', 'env'].includes(ext);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function isPdf(fileName) {
|
|
160
|
-
return getFileExt(fileName) === 'pdf';
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function getFileTypeConfig(fileName) {
|
|
164
|
-
const ext = getFileExt(fileName);
|
|
165
|
-
if (FILE_TYPES[ext]) return FILE_TYPES[ext];
|
|
166
|
-
if (CODE_EXTS.has(ext)) return FILE_TYPES.code;
|
|
167
|
-
return { color: 'var(--text-secondary)', bg: 'var(--bg-surface-secondary)', label: ext.toUpperCase().slice(0, 4) || 'FILE' };
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function stripApiPrefixFromPath(path, apiPrefix = '') {
|
|
171
|
-
const normalizedPath = String(path || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
172
|
-
const normalizedApiPrefix = String(apiPrefix || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
173
|
-
if (!normalizedApiPrefix) return normalizedPath;
|
|
174
|
-
if (normalizedPath === normalizedApiPrefix || normalizedPath === `${normalizedApiPrefix}/`) return '';
|
|
175
|
-
if (normalizedPath.startsWith(`${normalizedApiPrefix}/`)) {
|
|
176
|
-
return normalizedPath.slice(normalizedApiPrefix.length + 1);
|
|
177
|
-
}
|
|
178
|
-
return normalizedPath;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function toApiFolderPath(prefix = '', apiPrefix = '') {
|
|
182
|
-
const uiPrefix = String(prefix || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
183
|
-
const normalizedApiPrefix = String(apiPrefix || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
184
|
-
if (!uiPrefix) return '';
|
|
185
|
-
if (!normalizedApiPrefix) return uiPrefix;
|
|
186
|
-
if (uiPrefix === normalizedApiPrefix || uiPrefix.startsWith(`${normalizedApiPrefix}/`)) return uiPrefix;
|
|
187
|
-
return `${normalizedApiPrefix}/${uiPrefix}`;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function extractFoldersAtCurrentLevel(files, prefix = '') {
|
|
191
|
-
const folderSet = new Set();
|
|
192
|
-
const cleanPrefix = String(prefix || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
193
|
-
const prefixOffset = cleanPrefix ? cleanPrefix.length + 1 : 0;
|
|
194
|
-
|
|
195
|
-
(files || []).forEach(file => {
|
|
196
|
-
const key = String(file.Key || '').replace(/^\/+/, '');
|
|
197
|
-
if (cleanPrefix && !key.startsWith(cleanPrefix + '/')) return;
|
|
198
|
-
const relPath = cleanPrefix ? key.substring(prefixOffset) : key;
|
|
199
|
-
const slashIdx = relPath.indexOf('/');
|
|
200
|
-
if (slashIdx !== -1) {
|
|
201
|
-
const folder = relPath.substring(0, slashIdx);
|
|
202
|
-
folderSet.add(cleanPrefix ? `${cleanPrefix}/${folder}` : folder);
|
|
203
|
-
}
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
return Array.from(folderSet).sort();
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function syncStateUrl() {
|
|
210
|
-
try {
|
|
211
|
-
const parts = [];
|
|
212
|
-
if (activeBucket) parts.push(`bucket=${encodeURIComponent(activeBucket)}`);
|
|
213
|
-
if (currentPrefix) {
|
|
214
|
-
const folderPath = toApiFolderPath(currentPrefix, currentApiPrefix);
|
|
215
|
-
parts.push(`folder=${encodeURIComponent(folderPath).replace(/%2F/g, '/')}`);
|
|
216
|
-
}
|
|
217
|
-
if (currentPage > 1) parts.push(`page=${currentPage}`);
|
|
218
|
-
if (currentSearch) parts.push(`search=${encodeURIComponent(currentSearch)}`);
|
|
219
|
-
if (currentSort !== 'name') parts.push(`sort=${encodeURIComponent(currentSort)}`);
|
|
220
|
-
if (currentOrder !== 'asc') parts.push(`order=${encodeURIComponent(currentOrder)}`);
|
|
221
|
-
if (currentViewMode !== 'grid') parts.push(`view=${encodeURIComponent(currentViewMode)}`);
|
|
222
|
-
if (currentQuickFilter !== 'all') parts.push(`filter=${encodeURIComponent(currentQuickFilter)}`);
|
|
223
|
-
|
|
224
|
-
const newUrl = '/mbkbucket' + (parts.length ? '?' + parts.join('&') : '');
|
|
225
|
-
window.history.replaceState(null, '', newUrl);
|
|
226
|
-
} catch (e) {
|
|
227
|
-
console.warn('History update failed', e);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Toast System
|
|
232
|
-
function showToast(message, type = 'success') {
|
|
233
|
-
const container = document.getElementById('toast-container');
|
|
234
|
-
if (!container) return;
|
|
235
|
-
|
|
236
|
-
const toast = document.createElement('div');
|
|
237
|
-
toast.className = `toast-notification ${type}`;
|
|
238
|
-
toast.innerHTML = `
|
|
239
|
-
<span>${escapeHtml(message)}</span>
|
|
240
|
-
<button type="button" class="toast-close-btn">×</button>
|
|
241
|
-
`;
|
|
242
|
-
|
|
243
|
-
toast.querySelector('.toast-close-btn').addEventListener('click', () => {
|
|
244
|
-
toast.remove();
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
container.appendChild(toast);
|
|
248
|
-
|
|
249
|
-
setTimeout(() => {
|
|
250
|
-
toast.style.opacity = '0';
|
|
251
|
-
toast.style.transform = 'translateY(10px)';
|
|
252
|
-
setTimeout(() => toast.remove(), 250);
|
|
253
|
-
}, 4000);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Tree Renderer
|
|
257
|
-
function renderTree() {
|
|
258
|
-
const treeEl = document.getElementById('fm-tree');
|
|
259
|
-
if (!treeEl) return;
|
|
260
|
-
|
|
261
|
-
const allFolders = pageFolders || [];
|
|
262
|
-
const isStarredActive = currentQuickFilter === 'starred';
|
|
263
|
-
const isRootActive = !currentPrefix && currentQuickFilter === 'all';
|
|
264
|
-
|
|
265
|
-
let treeHtml = `
|
|
266
|
-
<div class="tree-group">
|
|
267
|
-
<button type="button" class="tree-link ${isRootActive ? 'active' : ''}" data-tree-prefix="" data-tree-filter="all">
|
|
268
|
-
${ICONS.drive}
|
|
269
|
-
<span>${escapeHtml(rootLabel)}</span>
|
|
270
|
-
</button>
|
|
271
|
-
</div>
|
|
272
|
-
`;
|
|
273
|
-
|
|
274
|
-
// Folder hierarchy
|
|
275
|
-
if (allFolders.length > 0) {
|
|
276
|
-
treeHtml += `
|
|
277
|
-
<div class="tree-group">
|
|
278
|
-
<div class="tree-subs">
|
|
279
|
-
${allFolders.map(folder => {
|
|
280
|
-
const cleanFolder = String(folder || '').replace(/\/+$/, '');
|
|
281
|
-
const folderName = getBaseName(cleanFolder);
|
|
282
|
-
const folderPathForAction = currentPrefix && !cleanFolder.startsWith(currentPrefix + '/') && cleanFolder !== currentPrefix
|
|
283
|
-
? `${currentPrefix}/${cleanFolder}`
|
|
284
|
-
: cleanFolder;
|
|
285
|
-
const isFolderActive = currentPrefix === folderPathForAction;
|
|
286
|
-
return `
|
|
287
|
-
<button type="button" class="tree-link tree-sub ${isFolderActive ? 'active' : ''}" data-tree-prefix="${escapeAttr(folderPathForAction)}" data-tree-filter="all">
|
|
288
|
-
${ICONS.folderSm}
|
|
289
|
-
<span>${escapeHtml(folderName)}</span>
|
|
290
|
-
</button>
|
|
291
|
-
`;
|
|
292
|
-
}).join('')}
|
|
293
|
-
</div>
|
|
294
|
-
</div>
|
|
295
|
-
`;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Quick navigation filters
|
|
299
|
-
treeHtml += `
|
|
300
|
-
<div class="tree-group" style="margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--border-color-light);">
|
|
301
|
-
<button type="button" class="tree-link ${isStarredActive ? 'active' : ''}" data-tree-filter="starred">
|
|
302
|
-
${ICONS.starred}
|
|
303
|
-
<span>Starred</span>
|
|
304
|
-
${starredFiles.size > 0 ? `<span class="tree-badge">${starredFiles.size}</span>` : ''}
|
|
305
|
-
</button>
|
|
306
|
-
<button type="button" class="tree-link ${currentQuickFilter === 'media' ? 'active' : ''}" data-tree-filter="media">
|
|
307
|
-
${ICONS.media}
|
|
308
|
-
<span>Media</span>
|
|
309
|
-
</button>
|
|
310
|
-
<button type="button" class="tree-link ${currentQuickFilter === 'docs' ? 'active' : ''}" data-tree-filter="docs">
|
|
311
|
-
${ICONS.docs}
|
|
312
|
-
<span>Documents</span>
|
|
313
|
-
</button>
|
|
314
|
-
<button type="button" class="tree-link ${currentQuickFilter === 'code' ? 'active' : ''}" data-tree-filter="code">
|
|
315
|
-
${ICONS.code}
|
|
316
|
-
<span>Code</span>
|
|
317
|
-
</button>
|
|
318
|
-
</div>
|
|
319
|
-
`;
|
|
320
|
-
|
|
321
|
-
treeEl.innerHTML = treeHtml;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Breadcrumbs Renderer
|
|
325
|
-
function renderBreadcrumbs() {
|
|
326
|
-
const bcEl = document.getElementById('fm-breadcrumb');
|
|
327
|
-
if (!bcEl) return;
|
|
328
|
-
|
|
329
|
-
if (!currentPrefix && currentQuickFilter === 'all') {
|
|
330
|
-
bcEl.innerHTML = `<button type="button" class="bc-link current">${escapeHtml(rootLabel)}</button>`;
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
if (currentQuickFilter !== 'all') {
|
|
335
|
-
bcEl.innerHTML = `
|
|
336
|
-
<button type="button" class="bc-link" data-nav-prefix="">${escapeHtml(rootLabel)}</button>
|
|
337
|
-
<span class="sep">›</span>
|
|
338
|
-
<button type="button" class="bc-link current">${escapeHtml(currentQuickFilter.charAt(0).toUpperCase() + currentQuickFilter.slice(1))}</button>
|
|
339
|
-
`;
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const parts = currentPrefix.split('/').filter(Boolean);
|
|
344
|
-
let accum = '';
|
|
345
|
-
const breadcrumbHtml = [
|
|
346
|
-
`<button type="button" class="bc-link" data-nav-prefix="">${escapeHtml(rootLabel)}</button>`
|
|
347
|
-
];
|
|
348
|
-
|
|
349
|
-
parts.forEach((part, index) => {
|
|
350
|
-
accum += (accum ? '/' : '') + part;
|
|
351
|
-
const isLast = index === parts.length - 1;
|
|
352
|
-
breadcrumbHtml.push('<span class="sep">›</span>');
|
|
353
|
-
if (isLast) {
|
|
354
|
-
breadcrumbHtml.push(`<button type="button" class="bc-link current">${escapeHtml(part)}</button>`);
|
|
355
|
-
} else {
|
|
356
|
-
breadcrumbHtml.push(`<button type="button" class="bc-link" data-nav-prefix="${escapeAttr(accum)}">${escapeHtml(part)}</button>`);
|
|
357
|
-
}
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
bcEl.innerHTML = breadcrumbHtml.join('');
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// File Filtering Logic
|
|
364
|
-
function getFilteredFiles() {
|
|
365
|
-
let list = (pageFiles || []).filter(f => !String(f.Key || '').endsWith('/'));
|
|
366
|
-
|
|
367
|
-
// Search filter
|
|
368
|
-
if (currentSearch) {
|
|
369
|
-
const q = currentSearch.toLowerCase();
|
|
370
|
-
list = list.filter(f => getBaseName(f.Key).toLowerCase().includes(q));
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Quick filter
|
|
374
|
-
if (currentQuickFilter === 'media') {
|
|
375
|
-
list = list.filter(f => MEDIA_EXTS.has(getFileExt(f.Key)));
|
|
376
|
-
} else if (currentQuickFilter === 'docs') {
|
|
377
|
-
list = list.filter(f => DOC_EXTS.has(getFileExt(f.Key)));
|
|
378
|
-
} else if (currentQuickFilter === 'code') {
|
|
379
|
-
list = list.filter(f => CODE_EXTS.has(getFileExt(f.Key)));
|
|
380
|
-
} else if (currentQuickFilter === 'starred') {
|
|
381
|
-
list = list.filter(f => starredFiles.has(f.Key));
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
return list;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function getFilteredFolders() {
|
|
388
|
-
if (currentQuickFilter !== 'all') return [];
|
|
389
|
-
if (currentSearch) {
|
|
390
|
-
const q = currentSearch.toLowerCase();
|
|
391
|
-
return (pageFolders || []).filter(f => getBaseName(f).toLowerCase().includes(q));
|
|
392
|
-
}
|
|
393
|
-
return pageFolders || [];
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Main Files & Grid Renderer
|
|
397
|
-
function renderFiles() {
|
|
398
|
-
const gridEl = document.getElementById('fm-grid');
|
|
399
|
-
const emptyEl = document.getElementById('fm-empty');
|
|
400
|
-
if (!gridEl) return;
|
|
401
|
-
|
|
402
|
-
const files = getFilteredFiles();
|
|
403
|
-
const folders = getFilteredFolders();
|
|
404
|
-
|
|
405
|
-
// Update stats
|
|
406
|
-
const totalCountEl = document.getElementById('totalFilesCount');
|
|
407
|
-
const filterInfoEl = document.getElementById('filterInfo');
|
|
408
|
-
const totalItems = files.length + folders.length;
|
|
409
|
-
|
|
410
|
-
if (totalCountEl) {
|
|
411
|
-
totalCountEl.textContent = `${totalItems} item${totalItems === 1 ? '' : 's'}`;
|
|
412
|
-
}
|
|
413
|
-
if (filterInfoEl) {
|
|
414
|
-
filterInfoEl.textContent = activeBucket ? `Bucket: ${activeBucket}` : 'Default bucket';
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
if (totalItems === 0) {
|
|
418
|
-
gridEl.innerHTML = '';
|
|
419
|
-
if (emptyEl) {
|
|
420
|
-
emptyEl.hidden = false;
|
|
421
|
-
emptyEl.style.display = 'flex';
|
|
422
|
-
}
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
if (emptyEl) {
|
|
427
|
-
emptyEl.hidden = true;
|
|
428
|
-
emptyEl.style.display = 'none';
|
|
429
|
-
}
|
|
430
|
-
gridEl.className = `fm-grid view-${currentViewMode}`;
|
|
431
|
-
|
|
432
|
-
let html = '';
|
|
433
|
-
|
|
434
|
-
// If List view, add header row
|
|
435
|
-
if (currentViewMode === 'list') {
|
|
436
|
-
html += `
|
|
437
|
-
<div class="fm-list-header">
|
|
438
|
-
<div></div>
|
|
439
|
-
<div></div>
|
|
440
|
-
<div>Name</div>
|
|
441
|
-
<div>Size</div>
|
|
442
|
-
<div>Modified</div>
|
|
443
|
-
<div></div>
|
|
444
|
-
<div></div>
|
|
445
|
-
</div>
|
|
446
|
-
`;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// Render Folders
|
|
450
|
-
folders.forEach(folder => {
|
|
451
|
-
const cleanFolder = String(folder || '').replace(/\/+$/, '');
|
|
452
|
-
const folderName = getBaseName(cleanFolder);
|
|
453
|
-
const folderPathForAction = currentPrefix && !cleanFolder.startsWith(currentPrefix + '/') && cleanFolder !== currentPrefix
|
|
454
|
-
? `${currentPrefix}/${cleanFolder}`
|
|
455
|
-
: cleanFolder;
|
|
456
|
-
const safePath = escapeAttr(folderPathForAction);
|
|
457
|
-
const safeName = escapeHtml(folderName);
|
|
458
|
-
|
|
459
|
-
if (currentViewMode === 'grid') {
|
|
460
|
-
html += `
|
|
461
|
-
<div class="fm-item" data-type="folder" data-folder-path="${safePath}" tabindex="0">
|
|
462
|
-
<div class="fm-item-icon">
|
|
463
|
-
${ICONS.folder}
|
|
464
|
-
</div>
|
|
465
|
-
<div class="fm-item-details">
|
|
466
|
-
<div class="fm-item-name" title="${safeName}">${safeName}</div>
|
|
467
|
-
<div class="fm-item-meta">Folder</div>
|
|
468
|
-
</div>
|
|
469
|
-
<button type="button" class="fm-item-menu" data-menu-folder="${safePath}" aria-label="Folder options">
|
|
470
|
-
${ICONS.more}
|
|
471
|
-
</button>
|
|
472
|
-
</div>
|
|
473
|
-
`;
|
|
474
|
-
} else {
|
|
475
|
-
html += `
|
|
476
|
-
<div class="fm-item" data-type="folder" data-folder-path="${safePath}" tabindex="0">
|
|
477
|
-
<div class="fm-item-checkbox"></div>
|
|
478
|
-
<div class="fm-item-icon">
|
|
479
|
-
${ICONS.folderSm}
|
|
480
|
-
</div>
|
|
481
|
-
<div class="fm-item-name" title="${safeName}">${safeName}</div>
|
|
482
|
-
<div class="fm-item-size fm-item-meta">—</div>
|
|
483
|
-
<div class="fm-item-date fm-item-meta">—</div>
|
|
484
|
-
<div class="fm-star-wrap"></div>
|
|
485
|
-
<button type="button" class="fm-item-menu" data-menu-folder="${safePath}" aria-label="Folder options">
|
|
486
|
-
${ICONS.more}
|
|
487
|
-
</button>
|
|
488
|
-
</div>
|
|
489
|
-
`;
|
|
490
|
-
}
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
// Render Files
|
|
494
|
-
files.forEach(file => {
|
|
495
|
-
const rawKey = String(file.Key || '');
|
|
496
|
-
const fileName = getBaseName(rawKey);
|
|
497
|
-
const safeKey = escapeAttr(rawKey);
|
|
498
|
-
const safeName = escapeHtml(fileName);
|
|
499
|
-
const isStarred = starredFiles.has(rawKey);
|
|
500
|
-
const isImg = isImage(fileName);
|
|
501
|
-
const typeCfg = getFileTypeConfig(fileName);
|
|
502
|
-
const formattedSize = file.Size ? formatFileSize(file.Size) : '—';
|
|
503
|
-
const formattedDate = file.LastModified ? formatDate(file.LastModified) : '—';
|
|
504
|
-
|
|
505
|
-
let iconInner = `<span class="ext">${typeCfg.label}</span>`;
|
|
506
|
-
if (isImg) {
|
|
507
|
-
iconInner = `<img src="${getViewUrl(rawKey)}" class="fm-item-thumb" alt="${safeName}" loading="lazy" onerror="this.outerHTML='<span class=\\'ext\\'>IMG</span>'">`;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
if (currentViewMode === 'grid') {
|
|
511
|
-
html += `
|
|
512
|
-
<div class="fm-item ${isStarred ? 'starred' : ''}" data-type="file" data-key="${safeKey}" tabindex="0">
|
|
513
|
-
<div class="fm-item-icon" style="${isImg ? '' : `background:${typeCfg.bg};color:${typeCfg.color}`}">
|
|
514
|
-
${iconInner}
|
|
515
|
-
</div>
|
|
516
|
-
<div class="fm-item-details">
|
|
517
|
-
<div class="fm-item-name" title="${safeName}">${safeName}</div>
|
|
518
|
-
<div class="fm-item-meta">${formattedSize} · ${formattedDate}</div>
|
|
519
|
-
</div>
|
|
520
|
-
<button type="button" class="fm-star ${isStarred ? 'on' : ''}" data-star-key="${safeKey}" aria-label="${isStarred ? 'Unstar' : 'Star'}">
|
|
521
|
-
${ICONS.starred}
|
|
522
|
-
</button>
|
|
523
|
-
<button type="button" class="fm-item-menu" data-menu-file="${safeKey}" aria-label="File options">
|
|
524
|
-
${ICONS.more}
|
|
525
|
-
</button>
|
|
526
|
-
</div>
|
|
527
|
-
`;
|
|
528
|
-
} else {
|
|
529
|
-
html += `
|
|
530
|
-
<div class="fm-item ${isStarred ? 'starred' : ''}" data-type="file" data-key="${safeKey}" tabindex="0">
|
|
531
|
-
<div class="fm-item-checkbox">
|
|
532
|
-
<input type="checkbox" class="file-select-cb" data-key="${safeKey}">
|
|
533
|
-
</div>
|
|
534
|
-
<div class="fm-item-icon" style="${isImg ? '' : `background:${typeCfg.bg};color:${typeCfg.color}`}">
|
|
535
|
-
<span class="ext">${typeCfg.label}</span>
|
|
536
|
-
</div>
|
|
537
|
-
<div class="fm-item-name" title="${safeName}">${safeName}</div>
|
|
538
|
-
<div class="fm-item-size fm-item-meta">${formattedSize}</div>
|
|
539
|
-
<div class="fm-item-date fm-item-meta">${formattedDate}</div>
|
|
540
|
-
<button type="button" class="fm-star ${isStarred ? 'on' : ''}" data-star-key="${safeKey}" aria-label="${isStarred ? 'Unstar' : 'Star'}">
|
|
541
|
-
${ICONS.starred}
|
|
542
|
-
</button>
|
|
543
|
-
<button type="button" class="fm-item-menu" data-menu-file="${safeKey}" aria-label="File options">
|
|
544
|
-
${ICONS.more}
|
|
545
|
-
</button>
|
|
546
|
-
</div>
|
|
547
|
-
`;
|
|
548
|
-
}
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
gridEl.innerHTML = html;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// Load Files API Call
|
|
555
|
-
async function loadFiles(page = 1, prefix = '', search = '') {
|
|
556
|
-
currentPage = page;
|
|
557
|
-
currentPrefix = String(prefix || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
558
|
-
currentSearch = search;
|
|
559
|
-
|
|
560
|
-
syncStateUrl();
|
|
561
|
-
|
|
562
|
-
const gridEl = document.getElementById('fm-grid');
|
|
563
|
-
if (gridEl) {
|
|
564
|
-
gridEl.innerHTML = `
|
|
565
|
-
<div class="fm-skeleton-grid">
|
|
566
|
-
${[...Array(6)].map(() => `
|
|
567
|
-
<div class="fm-skeleton-card">
|
|
568
|
-
<div class="fm-skeleton-icon"></div>
|
|
569
|
-
<div class="fm-skeleton-line w-75"></div>
|
|
570
|
-
<div class="fm-skeleton-line w-50"></div>
|
|
571
|
-
</div>
|
|
572
|
-
`).join('')}
|
|
573
|
-
</div>
|
|
574
|
-
`;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
try {
|
|
578
|
-
const searchParam = search ? `&search=${encodeURIComponent(search)}` : '';
|
|
579
|
-
const sortParam = `&sort=${currentSort}&order=${currentOrder}`;
|
|
580
|
-
const recursiveParam = `&recursive=false`;
|
|
581
|
-
const url = withBucketUrl(`/mbkbucket/api/files?page=${page}&prefix=${encodeURIComponent(currentPrefix)}${searchParam}${sortParam}${recursiveParam}`);
|
|
582
|
-
|
|
583
|
-
const response = await fetch(url);
|
|
584
|
-
const data = await response.json();
|
|
585
|
-
|
|
586
|
-
if (!data.success) {
|
|
587
|
-
throw new Error(data.error || 'Failed to load files');
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
currentNextToken = data.nextContinuationToken || null;
|
|
591
|
-
currentApiPrefix = data.prefix || '';
|
|
592
|
-
pageFiles = data.files || [];
|
|
593
|
-
|
|
594
|
-
if (data.mode === 'optimized') {
|
|
595
|
-
pageFolders = (data.folders || []).map(folder => stripApiPrefixFromPath(folder, currentApiPrefix)).filter(Boolean);
|
|
596
|
-
} else {
|
|
597
|
-
pageFolders = currentSearch
|
|
598
|
-
? []
|
|
599
|
-
: extractFoldersAtCurrentLevel(pageFiles, currentApiPrefix || currentPrefix)
|
|
600
|
-
.map(folder => stripApiPrefixFromPath(folder, currentApiPrefix))
|
|
601
|
-
.filter(Boolean);
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
renderTree();
|
|
605
|
-
renderBreadcrumbs();
|
|
606
|
-
renderFiles();
|
|
607
|
-
renderPagination(data);
|
|
608
|
-
} catch (err) {
|
|
609
|
-
console.error('Error loading files:', err);
|
|
610
|
-
showToast(err.message || 'Error loading files', 'error');
|
|
611
|
-
if (gridEl) {
|
|
612
|
-
gridEl.innerHTML = `
|
|
613
|
-
<div class="fm-empty">
|
|
614
|
-
<div class="empty-title">Error Loading Files</div>
|
|
615
|
-
<div class="empty-desc">${escapeHtml(err.message)}</div>
|
|
616
|
-
<button type="button" class="btn btn-primary btn-sm mt-3" onclick="window.mbkbucket.loadFiles(${page}, '${escapeAttr(currentPrefix)}')">Retry</button>
|
|
617
|
-
</div>
|
|
618
|
-
`;
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
// Pagination Renderer
|
|
624
|
-
function renderPagination(data) {
|
|
625
|
-
const paginationContainer = document.getElementById('paginationContainer');
|
|
626
|
-
const paginationList = document.getElementById('paginationList');
|
|
627
|
-
if (!paginationContainer || !paginationList) return;
|
|
628
|
-
|
|
629
|
-
if (!data.totalPages || data.totalPages <= 1) {
|
|
630
|
-
paginationContainer.style.display = 'none';
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
paginationContainer.style.display = 'flex';
|
|
635
|
-
let html = '';
|
|
636
|
-
|
|
637
|
-
// Prev
|
|
638
|
-
html += `
|
|
639
|
-
<li class="page-item ${data.currentPage <= 1 ? 'disabled' : ''}">
|
|
640
|
-
<button type="button" class="page-link" data-page="${data.currentPage - 1}">‹</button>
|
|
641
|
-
</li>
|
|
642
|
-
`;
|
|
643
|
-
|
|
644
|
-
// Pages
|
|
645
|
-
for (let i = 1; i <= data.totalPages; i++) {
|
|
646
|
-
if (i === 1 || i === data.totalPages || (i >= data.currentPage - 2 && i <= data.currentPage + 2)) {
|
|
647
|
-
html += `
|
|
648
|
-
<li class="page-item ${i === data.currentPage ? 'active' : ''}">
|
|
649
|
-
<button type="button" class="page-link" data-page="${i}">${i}</button>
|
|
650
|
-
</li>
|
|
651
|
-
`;
|
|
652
|
-
} else if (i === data.currentPage - 3 || i === data.currentPage + 3) {
|
|
653
|
-
html += `<li class="page-item disabled"><span class="page-link">…</span></li>`;
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
// Next
|
|
658
|
-
html += `
|
|
659
|
-
<li class="page-item ${data.currentPage >= data.totalPages ? 'disabled' : ''}">
|
|
660
|
-
<button type="button" class="page-link" data-page="${data.currentPage + 1}">›</button>
|
|
661
|
-
</li>
|
|
662
|
-
`;
|
|
663
|
-
|
|
664
|
-
paginationList.innerHTML = html;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// Context Menu
|
|
668
|
-
function openContextMenu(x, y, items) {
|
|
669
|
-
const menu = document.getElementById('fm-context-menu');
|
|
670
|
-
if (!menu) return;
|
|
671
|
-
|
|
672
|
-
menu.innerHTML = items.map(item => {
|
|
673
|
-
if (item === '-') return '<div class="fm-context-divider"></div>';
|
|
674
|
-
return `<button type="button" class="fm-context-item ${item.danger ? 'danger' : ''}" data-action="${item.id}">${item.icon || ''} ${escapeHtml(item.label)}</button>`;
|
|
675
|
-
}).join('');
|
|
676
|
-
|
|
677
|
-
menu.style.display = 'block';
|
|
678
|
-
|
|
679
|
-
const menuWidth = 180;
|
|
680
|
-
const menuHeight = menu.offsetHeight || 150;
|
|
681
|
-
const posX = Math.min(x, window.innerWidth - menuWidth - 10);
|
|
682
|
-
const posY = Math.min(y, window.innerHeight - menuHeight - 10);
|
|
683
|
-
|
|
684
|
-
menu.style.left = `${posX}px`;
|
|
685
|
-
menu.style.top = `${posY}px`;
|
|
686
|
-
|
|
687
|
-
menu.querySelectorAll('.fm-context-item').forEach(btn => {
|
|
688
|
-
btn.addEventListener('click', (e) => {
|
|
689
|
-
e.stopPropagation();
|
|
690
|
-
const actionId = btn.dataset.action;
|
|
691
|
-
const targetItem = items.find(it => it.id === actionId);
|
|
692
|
-
menu.style.display = 'none';
|
|
693
|
-
if (targetItem && typeof targetItem.action === 'function') {
|
|
694
|
-
targetItem.action();
|
|
695
|
-
}
|
|
696
|
-
});
|
|
697
|
-
});
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
function closeContextMenu() {
|
|
701
|
-
const menu = document.getElementById('fm-context-menu');
|
|
702
|
-
if (menu) menu.style.display = 'none';
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
// Actions: Star, Download, Preview, Delete, New Folder
|
|
706
|
-
function toggleStar(key) {
|
|
707
|
-
if (starredFiles.has(key)) {
|
|
708
|
-
starredFiles.delete(key);
|
|
709
|
-
showToast('Removed from Starred');
|
|
710
|
-
} else {
|
|
711
|
-
starredFiles.add(key);
|
|
712
|
-
showToast('Added to Starred', 'success');
|
|
713
|
-
}
|
|
714
|
-
localStorage.setItem('mbk_starred_keys', JSON.stringify([...starredFiles]));
|
|
715
|
-
renderTree();
|
|
716
|
-
renderFiles();
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
async function downloadFile(key) {
|
|
720
|
-
const fileName = getBaseName(key);
|
|
721
|
-
const downloadUrl = getDownloadUrl(key);
|
|
722
|
-
showToast(`Downloading ${fileName}...`);
|
|
723
|
-
try {
|
|
724
|
-
const response = await fetch(downloadUrl);
|
|
725
|
-
if (!response.ok) throw new Error('Download failed');
|
|
726
|
-
const blob = await response.blob();
|
|
727
|
-
const url = URL.createObjectURL(blob);
|
|
728
|
-
const a = document.createElement('a');
|
|
729
|
-
a.href = url;
|
|
730
|
-
a.download = fileName;
|
|
731
|
-
document.body.appendChild(a);
|
|
732
|
-
a.click();
|
|
733
|
-
document.body.removeChild(a);
|
|
734
|
-
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
735
|
-
} catch (err) {
|
|
736
|
-
showToast(err.message, 'error');
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
function copyShareLink(key) {
|
|
741
|
-
const viewUrl = window.location.origin + getViewUrl(key);
|
|
742
|
-
navigator.clipboard.writeText(viewUrl).then(() => {
|
|
743
|
-
showToast('Link copied to clipboard', 'success');
|
|
744
|
-
}).catch(() => {
|
|
745
|
-
showToast('Failed to copy link', 'error');
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
// Preview Modal
|
|
750
|
-
function openPreview(key) {
|
|
751
|
-
const modal = document.getElementById('previewModal');
|
|
752
|
-
const titleEl = document.getElementById('previewFileName');
|
|
753
|
-
const contentEl = document.getElementById('previewContent');
|
|
754
|
-
const openBtn = document.getElementById('previewOpenBtn');
|
|
755
|
-
const downloadBtn = document.getElementById('previewDownloadBtn');
|
|
756
|
-
|
|
757
|
-
if (!modal || !contentEl) return;
|
|
758
|
-
|
|
759
|
-
const fileName = getBaseName(key);
|
|
760
|
-
const viewUrl = getViewUrl(key);
|
|
761
|
-
const downloadUrl = getDownloadUrl(key);
|
|
762
|
-
|
|
763
|
-
if (titleEl) titleEl.textContent = fileName;
|
|
764
|
-
if (openBtn) openBtn.href = viewUrl;
|
|
765
|
-
if (downloadBtn) downloadBtn.href = downloadUrl;
|
|
766
|
-
|
|
767
|
-
contentEl.innerHTML = '<div class="empty-preview"><div class="spinner-border text-light"></div><div class="mt-2 text-light">Loading preview...</div></div>';
|
|
768
|
-
modal.classList.add('show');
|
|
769
|
-
|
|
770
|
-
if (isImage(fileName)) {
|
|
771
|
-
contentEl.innerHTML = `<img src="${viewUrl}" alt="${escapeAttr(fileName)}">`;
|
|
772
|
-
} else if (isVideo(fileName)) {
|
|
773
|
-
contentEl.innerHTML = `<video controls autoplay class="w-100" style="max-height: 70vh;"><source src="${viewUrl}"></video>`;
|
|
774
|
-
} else if (isAudio(fileName)) {
|
|
775
|
-
contentEl.innerHTML = `<div class="p-4 text-center"><audio controls autoplay class="w-100"><source src="${viewUrl}"></audio></div>`;
|
|
776
|
-
} else if (isPdf(fileName)) {
|
|
777
|
-
contentEl.innerHTML = `<iframe src="${viewUrl}" class="preview-iframe"></iframe>`;
|
|
778
|
-
} else if (isTextOrCode(fileName)) {
|
|
779
|
-
fetch(viewUrl).then(res => res.text()).then(text => {
|
|
780
|
-
contentEl.innerHTML = `<pre class="preview-code-block"><code>${escapeHtml(text)}</code></pre>`;
|
|
781
|
-
}).catch(err => {
|
|
782
|
-
contentEl.innerHTML = `<div class="p-4 text-danger text-center">Failed to load text preview: ${escapeHtml(err.message)}</div>`;
|
|
783
|
-
});
|
|
784
|
-
} else {
|
|
785
|
-
contentEl.innerHTML = `
|
|
786
|
-
<div class="p-5 text-center text-light">
|
|
787
|
-
<h5>No inline preview available for this file type.</h5>
|
|
788
|
-
<p class="text-muted small">You can open it in a new tab or download it directly.</p>
|
|
789
|
-
</div>
|
|
790
|
-
`;
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
window.closePreviewModal = function () {
|
|
795
|
-
const modal = document.getElementById('previewModal');
|
|
796
|
-
if (modal) modal.classList.remove('show');
|
|
797
|
-
};
|
|
798
|
-
|
|
799
|
-
// Delete Modal
|
|
800
|
-
function promptDelete(key, isFolder = false) {
|
|
801
|
-
const modal = document.getElementById('deleteModal');
|
|
802
|
-
const deleteFileName = document.getElementById('deleteFileName');
|
|
803
|
-
const deleteKey = document.getElementById('deleteKey');
|
|
804
|
-
const deleteIsFolder = document.getElementById('deleteIsFolder');
|
|
805
|
-
const deleteConfirmLabel = document.getElementById('deleteConfirmLabel');
|
|
806
|
-
|
|
807
|
-
if (!modal) return;
|
|
808
|
-
|
|
809
|
-
if (deleteFileName) deleteFileName.textContent = key;
|
|
810
|
-
if (deleteKey) deleteKey.value = key;
|
|
811
|
-
if (deleteIsFolder) deleteIsFolder.value = isFolder ? 'true' : '';
|
|
812
|
-
if (deleteConfirmLabel) deleteConfirmLabel.textContent = isFolder ? 'Delete Folder' : 'Delete File';
|
|
813
|
-
|
|
814
|
-
modal.classList.add('show');
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
window.closeModal = function () {
|
|
818
|
-
const deleteModal = document.getElementById('deleteModal');
|
|
819
|
-
if (deleteModal) deleteModal.classList.remove('show');
|
|
820
|
-
};
|
|
821
|
-
|
|
822
|
-
// New Folder Modal
|
|
823
|
-
function openNewFolderModal() {
|
|
824
|
-
const modal = document.getElementById('newFolderModal');
|
|
825
|
-
const input = document.getElementById('newFolderNameInput');
|
|
826
|
-
if (!modal) return;
|
|
827
|
-
if (input) input.value = '';
|
|
828
|
-
modal.classList.add('show');
|
|
829
|
-
if (input) setTimeout(() => input.focus(), 50);
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
window.closeNewFolderModal = function () {
|
|
833
|
-
const modal = document.getElementById('newFolderModal');
|
|
834
|
-
if (modal) modal.classList.remove('show');
|
|
835
|
-
};
|
|
836
|
-
|
|
837
|
-
window.submitCreateFolder = async function () {
|
|
838
|
-
const input = document.getElementById('newFolderNameInput');
|
|
839
|
-
const name = input ? input.value.trim() : '';
|
|
840
|
-
if (!name) {
|
|
841
|
-
showToast('Please enter a folder name', 'warning');
|
|
842
|
-
return;
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
try {
|
|
846
|
-
const response = await fetch(withBucketUrl('/mbkbucket/create-folder'), {
|
|
847
|
-
method: 'POST',
|
|
848
|
-
headers: { 'Content-Type': 'application/json' },
|
|
849
|
-
body: JSON.stringify({ folderName: name, prefix: currentPrefix })
|
|
850
|
-
});
|
|
851
|
-
const data = await response.json();
|
|
852
|
-
if (!data.success) throw new Error(data.error || 'Failed to create folder');
|
|
853
|
-
|
|
854
|
-
closeNewFolderModal();
|
|
855
|
-
showToast(`Folder "${name}" created`, 'success');
|
|
856
|
-
loadFiles(1, currentPrefix);
|
|
857
|
-
} catch (err) {
|
|
858
|
-
showToast(err.message, 'error');
|
|
859
|
-
}
|
|
860
|
-
};
|
|
861
|
-
|
|
862
|
-
// Upload Modal & Flow
|
|
863
|
-
function openUploadModal() {
|
|
864
|
-
const modal = document.getElementById('uploadModal');
|
|
865
|
-
const prefixInput = document.getElementById('uploadPrefixInput');
|
|
866
|
-
if (prefixInput) prefixInput.value = currentPrefix;
|
|
867
|
-
if (modal) modal.classList.add('show');
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
window.closeUploadModal = function () {
|
|
871
|
-
const modal = document.getElementById('uploadModal');
|
|
872
|
-
if (modal) modal.classList.remove('show');
|
|
873
|
-
};
|
|
874
|
-
|
|
875
|
-
function updateUploadQueueUI() {
|
|
876
|
-
const queueEl = document.getElementById('uploadQueue');
|
|
877
|
-
const clearBtn = document.getElementById('clearQueueBtn');
|
|
878
|
-
if (!queueEl) return;
|
|
879
|
-
|
|
880
|
-
if (uploadQueue.length === 0) {
|
|
881
|
-
queueEl.innerHTML = '';
|
|
882
|
-
if (clearBtn) clearBtn.style.display = 'none';
|
|
883
|
-
return;
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
if (clearBtn) clearBtn.style.display = 'inline-flex';
|
|
887
|
-
queueEl.innerHTML = uploadQueue.map((file, idx) => `
|
|
888
|
-
<div class="upload-queue-item">
|
|
889
|
-
<div class="d-flex align-items-center gap-2">
|
|
890
|
-
<span class="font-medium">${escapeHtml(file.name)}</span>
|
|
891
|
-
<span class="upload-queue-meta">${formatFileSize(file.size)}</span>
|
|
892
|
-
</div>
|
|
893
|
-
<button type="button" class="btn-close" data-remove-queue="${idx}" style="font-size: 14px;">×</button>
|
|
894
|
-
</div>
|
|
895
|
-
`).join('');
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
async function processUploads() {
|
|
899
|
-
if (isUploading || uploadQueue.length === 0) return;
|
|
900
|
-
isUploading = true;
|
|
901
|
-
|
|
902
|
-
const progressWrap = document.getElementById('uploadProgress');
|
|
903
|
-
const progressBar = document.getElementById('progressBar');
|
|
904
|
-
const percentEl = document.getElementById('uploadPercent');
|
|
905
|
-
const statusLabel = document.getElementById('uploadStatusLabel');
|
|
906
|
-
const startBtn = document.getElementById('startUploadBtn');
|
|
907
|
-
|
|
908
|
-
if (progressWrap) progressWrap.style.display = 'block';
|
|
909
|
-
if (startBtn) startBtn.disabled = true;
|
|
910
|
-
|
|
911
|
-
const totalFiles = uploadQueue.length;
|
|
912
|
-
let completed = 0;
|
|
913
|
-
|
|
914
|
-
for (let i = 0; i < uploadQueue.length; i++) {
|
|
915
|
-
const file = uploadQueue[i];
|
|
916
|
-
if (statusLabel) statusLabel.textContent = `Uploading ${file.name} (${i + 1}/${totalFiles})...`;
|
|
917
|
-
|
|
918
|
-
const formData = new FormData();
|
|
919
|
-
formData.append('file', file);
|
|
920
|
-
formData.append('prefix', currentPrefix);
|
|
921
|
-
|
|
922
|
-
try {
|
|
923
|
-
const response = await fetch(withBucketUrl('/mbkbucket/upload'), {
|
|
924
|
-
method: 'POST',
|
|
925
|
-
body: formData
|
|
926
|
-
});
|
|
927
|
-
const result = await response.json();
|
|
928
|
-
if (!result.success) throw new Error(result.error || 'Upload failed');
|
|
929
|
-
completed++;
|
|
930
|
-
} catch (err) {
|
|
931
|
-
showToast(`Failed to upload ${file.name}: ${err.message}`, 'error');
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
const percent = Math.round(((i + 1) / totalFiles) * 100);
|
|
935
|
-
if (progressBar) progressBar.style.width = `${percent}%`;
|
|
936
|
-
if (percentEl) percentEl.textContent = `${percent}%`;
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
showToast(`Successfully uploaded ${completed} file(s)`, 'success');
|
|
940
|
-
uploadQueue = [];
|
|
941
|
-
isUploading = false;
|
|
942
|
-
if (startBtn) startBtn.disabled = false;
|
|
943
|
-
if (progressWrap) progressWrap.style.display = 'none';
|
|
944
|
-
closeUploadModal();
|
|
945
|
-
updateUploadQueueUI();
|
|
946
|
-
loadFiles(1, currentPrefix);
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
// Event Listeners Setup
|
|
950
|
-
function initEventListeners() {
|
|
951
|
-
// Tree clicks
|
|
952
|
-
document.getElementById('fm-tree')?.addEventListener('click', (e) => {
|
|
953
|
-
const link = e.target.closest('.tree-link');
|
|
954
|
-
if (!link) return;
|
|
955
|
-
e.preventDefault();
|
|
956
|
-
const filter = link.dataset.treeFilter;
|
|
957
|
-
const prefix = link.dataset.treePrefix;
|
|
958
|
-
|
|
959
|
-
if (filter && filter !== 'all') {
|
|
960
|
-
currentQuickFilter = filter;
|
|
961
|
-
document.querySelectorAll('.quick-filter-btn').forEach(b => b.classList.remove('active'));
|
|
962
|
-
renderTree();
|
|
963
|
-
renderBreadcrumbs();
|
|
964
|
-
renderFiles();
|
|
965
|
-
} else {
|
|
966
|
-
currentQuickFilter = 'all';
|
|
967
|
-
document.querySelectorAll('.quick-filter-btn').forEach(b => {
|
|
968
|
-
b.classList.toggle('active', b.dataset.filter === 'all');
|
|
969
|
-
});
|
|
970
|
-
loadFiles(1, prefix !== undefined ? prefix : '');
|
|
971
|
-
}
|
|
972
|
-
});
|
|
973
|
-
|
|
974
|
-
// Breadcrumb clicks
|
|
975
|
-
document.getElementById('fm-breadcrumb')?.addEventListener('click', (e) => {
|
|
976
|
-
const link = e.target.closest('[data-nav-prefix]');
|
|
977
|
-
if (!link) return;
|
|
978
|
-
e.preventDefault();
|
|
979
|
-
currentQuickFilter = 'all';
|
|
980
|
-
loadFiles(1, link.dataset.navPrefix || '');
|
|
981
|
-
});
|
|
982
|
-
|
|
983
|
-
// Quick filters
|
|
984
|
-
document.querySelectorAll('.quick-filter-btn').forEach(btn => {
|
|
985
|
-
btn.addEventListener('click', () => {
|
|
986
|
-
document.querySelectorAll('.quick-filter-btn').forEach(b => b.classList.remove('active'));
|
|
987
|
-
btn.classList.add('active');
|
|
988
|
-
currentQuickFilter = btn.dataset.filter || 'all';
|
|
989
|
-
renderTree();
|
|
990
|
-
renderBreadcrumbs();
|
|
991
|
-
renderFiles();
|
|
992
|
-
syncStateUrl();
|
|
993
|
-
});
|
|
994
|
-
});
|
|
995
|
-
|
|
996
|
-
// View mode toggle
|
|
997
|
-
const gridBtn = document.getElementById('view-grid');
|
|
998
|
-
const listBtn = document.getElementById('view-list');
|
|
999
|
-
|
|
1000
|
-
gridBtn?.addEventListener('click', () => {
|
|
1001
|
-
currentViewMode = 'grid';
|
|
1002
|
-
localStorage.setItem('mbk_view_mode', 'grid');
|
|
1003
|
-
gridBtn.classList.add('active');
|
|
1004
|
-
listBtn?.classList.remove('active');
|
|
1005
|
-
renderFiles();
|
|
1006
|
-
syncStateUrl();
|
|
1007
|
-
});
|
|
1008
|
-
|
|
1009
|
-
listBtn?.addEventListener('click', () => {
|
|
1010
|
-
currentViewMode = 'list';
|
|
1011
|
-
localStorage.setItem('mbk_view_mode', 'list');
|
|
1012
|
-
listBtn.classList.add('active');
|
|
1013
|
-
gridBtn?.classList.remove('active');
|
|
1014
|
-
renderFiles();
|
|
1015
|
-
syncStateUrl();
|
|
1016
|
-
});
|
|
1017
|
-
|
|
1018
|
-
// Search input
|
|
1019
|
-
const searchInput = document.getElementById('searchInput');
|
|
1020
|
-
const clearSearchBtn = document.getElementById('searchClearBtn');
|
|
1021
|
-
|
|
1022
|
-
let searchTimer = null;
|
|
1023
|
-
searchInput?.addEventListener('input', (e) => {
|
|
1024
|
-
const val = e.target.value;
|
|
1025
|
-
if (clearSearchBtn) clearSearchBtn.style.display = val ? 'block' : 'none';
|
|
1026
|
-
|
|
1027
|
-
clearTimeout(searchTimer);
|
|
1028
|
-
searchTimer = setTimeout(() => {
|
|
1029
|
-
currentSearch = val.trim();
|
|
1030
|
-
renderFiles();
|
|
1031
|
-
}, 250);
|
|
1032
|
-
});
|
|
1033
|
-
|
|
1034
|
-
clearSearchBtn?.addEventListener('click', () => {
|
|
1035
|
-
if (searchInput) searchInput.value = '';
|
|
1036
|
-
clearSearchBtn.style.display = 'none';
|
|
1037
|
-
currentSearch = '';
|
|
1038
|
-
renderFiles();
|
|
1039
|
-
});
|
|
1040
|
-
|
|
1041
|
-
// Grid Item Interactions (Click, Double Click, Star, Context Menu)
|
|
1042
|
-
document.getElementById('fm-grid')?.addEventListener('click', (e) => {
|
|
1043
|
-
// Star click
|
|
1044
|
-
const starBtn = e.target.closest('[data-star-key]');
|
|
1045
|
-
if (starBtn) {
|
|
1046
|
-
e.stopPropagation();
|
|
1047
|
-
toggleStar(starBtn.dataset.starKey);
|
|
1048
|
-
return;
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
// Menu click
|
|
1052
|
-
const menuBtn = e.target.closest('.fm-item-menu');
|
|
1053
|
-
if (menuBtn) {
|
|
1054
|
-
e.stopPropagation();
|
|
1055
|
-
const rect = menuBtn.getBoundingClientRect();
|
|
1056
|
-
const fileKey = menuBtn.dataset.menuFile;
|
|
1057
|
-
const folderPath = menuBtn.dataset.menuFolder;
|
|
1058
|
-
|
|
1059
|
-
if (fileKey) {
|
|
1060
|
-
const isStarred = starredFiles.has(fileKey);
|
|
1061
|
-
openContextMenu(rect.left, rect.bottom + 4, [
|
|
1062
|
-
{ id: 'view', label: 'Preview', icon: '<i class="fas fa-eye"></i>', action: () => openPreview(fileKey) },
|
|
1063
|
-
{ id: 'download', label: 'Download', icon: '<i class="fas fa-download"></i>', action: () => downloadFile(fileKey) },
|
|
1064
|
-
{ id: 'share', label: 'Copy link', icon: '<i class="fas fa-link"></i>', action: () => copyShareLink(fileKey) },
|
|
1065
|
-
{ id: 'star', label: isStarred ? 'Unstar' : 'Star', icon: '<i class="fas fa-star"></i>', action: () => toggleStar(fileKey) },
|
|
1066
|
-
'-',
|
|
1067
|
-
{ id: 'delete', label: 'Delete', icon: '<i class="fas fa-trash"></i>', danger: true, action: () => promptDelete(fileKey, false) }
|
|
1068
|
-
]);
|
|
1069
|
-
} else if (folderPath) {
|
|
1070
|
-
openContextMenu(rect.left, rect.bottom + 4, [
|
|
1071
|
-
{ id: 'open', label: 'Open Folder', icon: '<i class="fas fa-folder-open"></i>', action: () => loadFiles(1, folderPath) },
|
|
1072
|
-
'-',
|
|
1073
|
-
{ id: 'delete', label: 'Delete Folder', icon: '<i class="fas fa-trash"></i>', danger: true, action: () => promptDelete(folderPath + '/', true) }
|
|
1074
|
-
]);
|
|
1075
|
-
}
|
|
1076
|
-
return;
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
// Folder click to navigate
|
|
1080
|
-
const folderItem = e.target.closest('[data-type="folder"]');
|
|
1081
|
-
if (folderItem) {
|
|
1082
|
-
e.stopPropagation();
|
|
1083
|
-
loadFiles(1, folderItem.dataset.folderPath);
|
|
1084
|
-
return;
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
// File click to preview
|
|
1088
|
-
const fileItem = e.target.closest('[data-type="file"]');
|
|
1089
|
-
if (fileItem && !e.target.closest('.file-select-cb')) {
|
|
1090
|
-
e.stopPropagation();
|
|
1091
|
-
openPreview(fileItem.dataset.key);
|
|
1092
|
-
}
|
|
1093
|
-
});
|
|
1094
|
-
|
|
1095
|
-
// Bucket Selector
|
|
1096
|
-
document.getElementById('bucketSelector')?.addEventListener('change', (e) => {
|
|
1097
|
-
const newBucket = e.target.value;
|
|
1098
|
-
if (!newBucket) return;
|
|
1099
|
-
activeBucket = newBucket;
|
|
1100
|
-
loadFiles(1, '');
|
|
1101
|
-
});
|
|
1102
|
-
|
|
1103
|
-
// Top action buttons & Modals
|
|
1104
|
-
document.querySelectorAll('[data-action="upload-files"]').forEach(btn => {
|
|
1105
|
-
btn.addEventListener('click', openUploadModal);
|
|
1106
|
-
});
|
|
1107
|
-
|
|
1108
|
-
document.querySelectorAll('[data-action="create-folder"]').forEach(btn => {
|
|
1109
|
-
btn.addEventListener('click', openNewFolderModal);
|
|
1110
|
-
});
|
|
1111
|
-
|
|
1112
|
-
// Drag and Drop Upload Zone
|
|
1113
|
-
const dropZone = document.getElementById('dropZone');
|
|
1114
|
-
const fileInput = document.getElementById('fileInput');
|
|
1115
|
-
|
|
1116
|
-
dropZone?.addEventListener('click', () => fileInput?.click());
|
|
1117
|
-
|
|
1118
|
-
dropZone?.addEventListener('dragover', (e) => {
|
|
1119
|
-
e.preventDefault();
|
|
1120
|
-
dropZone.classList.add('dragover');
|
|
1121
|
-
});
|
|
1122
|
-
|
|
1123
|
-
dropZone?.addEventListener('dragleave', () => {
|
|
1124
|
-
dropZone.classList.remove('dragover');
|
|
1125
|
-
});
|
|
1126
|
-
|
|
1127
|
-
dropZone?.addEventListener('drop', (e) => {
|
|
1128
|
-
e.preventDefault();
|
|
1129
|
-
dropZone.classList.remove('dragover');
|
|
1130
|
-
if (e.dataTransfer.files?.length) {
|
|
1131
|
-
uploadQueue.push(...Array.from(e.dataTransfer.files));
|
|
1132
|
-
updateUploadQueueUI();
|
|
1133
|
-
}
|
|
1134
|
-
});
|
|
1135
|
-
|
|
1136
|
-
fileInput?.addEventListener('change', (e) => {
|
|
1137
|
-
if (e.target.files?.length) {
|
|
1138
|
-
uploadQueue.push(...Array.from(e.target.files));
|
|
1139
|
-
updateUploadQueueUI();
|
|
1140
|
-
}
|
|
1141
|
-
});
|
|
1142
|
-
|
|
1143
|
-
document.getElementById('uploadQueue')?.addEventListener('click', (e) => {
|
|
1144
|
-
const removeBtn = e.target.closest('[data-remove-queue]');
|
|
1145
|
-
if (removeBtn) {
|
|
1146
|
-
const idx = parseInt(removeBtn.dataset.removeQueue, 10);
|
|
1147
|
-
uploadQueue.splice(idx, 1);
|
|
1148
|
-
updateUploadQueueUI();
|
|
1149
|
-
}
|
|
1150
|
-
});
|
|
1151
|
-
|
|
1152
|
-
document.getElementById('clearQueueBtn')?.addEventListener('click', () => {
|
|
1153
|
-
uploadQueue = [];
|
|
1154
|
-
updateUploadQueueUI();
|
|
1155
|
-
});
|
|
1156
|
-
|
|
1157
|
-
document.getElementById('startUploadBtn')?.addEventListener('click', processUploads);
|
|
1158
|
-
|
|
1159
|
-
// Fullscreen Preview Toggle
|
|
1160
|
-
document.getElementById('previewFullscreenBtn')?.addEventListener('click', () => {
|
|
1161
|
-
document.querySelector('.preview-modal-dialog')?.classList.toggle('is-fullscreen');
|
|
1162
|
-
});
|
|
1163
|
-
|
|
1164
|
-
// Pagination clicks
|
|
1165
|
-
document.getElementById('paginationList')?.addEventListener('click', (e) => {
|
|
1166
|
-
const link = e.target.closest('[data-page]');
|
|
1167
|
-
if (!link) return;
|
|
1168
|
-
const page = parseInt(link.dataset.page, 10);
|
|
1169
|
-
if (!isNaN(page) && page > 0) {
|
|
1170
|
-
loadFiles(page, currentPrefix, currentSearch);
|
|
1171
|
-
}
|
|
1172
|
-
});
|
|
1173
|
-
|
|
1174
|
-
// Global click to close context menu
|
|
1175
|
-
document.addEventListener('click', closeContextMenu);
|
|
1176
|
-
|
|
1177
|
-
// Handle delete form submission
|
|
1178
|
-
const deleteForm = document.getElementById('deleteForm');
|
|
1179
|
-
deleteForm?.addEventListener('submit', async (e) => {
|
|
1180
|
-
e.preventDefault();
|
|
1181
|
-
const formData = new FormData(deleteForm);
|
|
1182
|
-
const key = formData.get('key');
|
|
1183
|
-
const isFolder = formData.get('folder') === 'true';
|
|
1184
|
-
|
|
1185
|
-
try {
|
|
1186
|
-
const response = await fetch(withBucketUrl('/mbkbucket/delete'), {
|
|
1187
|
-
method: 'POST',
|
|
1188
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1189
|
-
body: JSON.stringify({ key, isFolder, prefix: currentPrefix })
|
|
1190
|
-
});
|
|
1191
|
-
const data = await response.json();
|
|
1192
|
-
if (!data.success) throw new Error(data.error || 'Delete failed');
|
|
1193
|
-
|
|
1194
|
-
closeModal();
|
|
1195
|
-
showToast(`Successfully deleted ${key}`, 'success');
|
|
1196
|
-
loadFiles(currentPage, currentPrefix);
|
|
1197
|
-
} catch (err) {
|
|
1198
|
-
showToast(err.message, 'error');
|
|
1199
|
-
}
|
|
1200
|
-
});
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
// Initialization
|
|
1204
|
-
document.addEventListener('DOMContentLoaded', () => {
|
|
1205
|
-
// Sync view mode buttons
|
|
1206
|
-
if (currentViewMode === 'list') {
|
|
1207
|
-
document.getElementById('view-list')?.classList.add('active');
|
|
1208
|
-
document.getElementById('view-grid')?.classList.remove('active');
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
initEventListeners();
|
|
1212
|
-
|
|
1213
|
-
// Initial load with URL params
|
|
1214
|
-
const initialFolder = urlParams.get('folder') || '';
|
|
1215
|
-
const initialPage = parseInt(urlParams.get('page') || '1', 10);
|
|
1216
|
-
const initialSearch = urlParams.get('search') || '';
|
|
1217
|
-
|
|
1218
|
-
loadFiles(initialPage, initialFolder, initialSearch);
|
|
1219
|
-
});
|
|
1220
|
-
|
|
1221
|
-
// Expose global methods if needed
|
|
1222
|
-
window.mbkbucket = {
|
|
1223
|
-
loadFiles,
|
|
1224
|
-
openPreview,
|
|
1225
|
-
downloadFile,
|
|
1226
|
-
showToast
|
|
1227
|
-
};
|
|
1228
|
-
|
|
1229
|
-
})();
|
|
1
|
+
/**
|
|
2
|
+
* MBKBucket
|
|
3
|
+
* Copyright (c) 2026 Muhammad Bin Khalid, MBKTech.org and contributors
|
|
4
|
+
* Licensed under the MIT License.
|
|
5
|
+
* Source: https://github.com/MIbnEKhalid/mbkbucket
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* MBKBucket File Manager - UI & Client Logic
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
(function () {
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
// Global State
|
|
16
|
+
let currentPage = 1;
|
|
17
|
+
let currentPrefix = '';
|
|
18
|
+
let currentSearch = '';
|
|
19
|
+
let currentSort = 'name';
|
|
20
|
+
let currentOrder = 'asc';
|
|
21
|
+
let currentViewMode = localStorage.getItem('mbk_view_mode') || 'grid'; // 'grid' or 'list'
|
|
22
|
+
let currentQuickFilter = 'all'; // 'all', 'media', 'docs', 'code', 'starred'
|
|
23
|
+
let pageFiles = [];
|
|
24
|
+
let pageFolders = [];
|
|
25
|
+
let currentApiPrefix = '';
|
|
26
|
+
let currentNextToken = null;
|
|
27
|
+
let selectedFiles = new Set();
|
|
28
|
+
let starredFiles = new Set(JSON.parse(localStorage.getItem('mbk_starred_keys') || '[]'));
|
|
29
|
+
|
|
30
|
+
// Upload State
|
|
31
|
+
let uploadQueue = [];
|
|
32
|
+
let isUploading = false;
|
|
33
|
+
|
|
34
|
+
// DOM Elements & Config
|
|
35
|
+
const bucketAppData = document.getElementById('bucketAppData');
|
|
36
|
+
const bucketAppName = bucketAppData?.dataset.appName || '';
|
|
37
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
38
|
+
const selectedBucket = urlParams.get('bucket') || '';
|
|
39
|
+
let activeBucket = selectedBucket;
|
|
40
|
+
const app = String(bucketAppName || '').toLowerCase();
|
|
41
|
+
const isRootApp = app === 'portal' || app === 'mbkbucket';
|
|
42
|
+
const rootLabel = isRootApp ? 'My Drive' : (bucketAppName || 'Bucket Drive');
|
|
43
|
+
|
|
44
|
+
// File categories & extensions
|
|
45
|
+
const MEDIA_EXTS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'mp4', 'webm', 'ogg', 'avi', 'mov', 'mp3', 'wav', 'flac', 'aac', 'm4a']);
|
|
46
|
+
const DOC_EXTS = new Set(['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'md', 'csv', 'rtf']);
|
|
47
|
+
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']);
|
|
48
|
+
|
|
49
|
+
const FILE_TYPES = {
|
|
50
|
+
pdf: { color: 'var(--red)', bg: 'var(--red-lt)', label: 'PDF' },
|
|
51
|
+
doc: { color: 'var(--blue)', bg: 'var(--blue-lt)', label: 'DOC' },
|
|
52
|
+
docx: { color: 'var(--blue)', bg: 'var(--blue-lt)', label: 'DOC' },
|
|
53
|
+
xls: { color: 'var(--green)', bg: 'var(--green-lt)', label: 'XLS' },
|
|
54
|
+
xlsx: { color: 'var(--green)', bg: 'var(--green-lt)', label: 'XLS' },
|
|
55
|
+
csv: { color: 'var(--green)', bg: 'var(--green-lt)', label: 'CSV' },
|
|
56
|
+
ppt: { color: 'var(--orange)', bg: 'rgba(247, 103, 7, 0.1)', label: 'PPT' },
|
|
57
|
+
pptx: { color: 'var(--orange)', bg: 'rgba(247, 103, 7, 0.1)', label: 'PPT' },
|
|
58
|
+
txt: { color: 'var(--text-secondary)', bg: 'var(--bg-surface-secondary)', label: 'TXT' },
|
|
59
|
+
md: { color: 'var(--purple)', bg: 'var(--purple-lt)', label: 'MD' },
|
|
60
|
+
jpg: { color: 'var(--yellow)', bg: 'var(--yellow-lt)', label: 'IMG' },
|
|
61
|
+
jpeg: { color: 'var(--yellow)', bg: 'var(--yellow-lt)', label: 'IMG' },
|
|
62
|
+
png: { color: 'var(--yellow)', bg: 'var(--yellow-lt)', label: 'IMG' },
|
|
63
|
+
gif: { color: 'var(--yellow)', bg: 'var(--yellow-lt)', label: 'GIF' },
|
|
64
|
+
webp: { color: 'var(--yellow)', bg: 'var(--yellow-lt)', label: 'IMG' },
|
|
65
|
+
svg: { color: 'var(--orange)', bg: 'rgba(247, 103, 7, 0.1)', label: 'SVG' },
|
|
66
|
+
mp4: { color: 'var(--azure)', bg: 'var(--azure-lt)', label: 'VID' },
|
|
67
|
+
webm: { color: 'var(--azure)', bg: 'var(--azure-lt)', label: 'VID' },
|
|
68
|
+
mov: { color: 'var(--azure)', bg: 'var(--azure-lt)', label: 'VID' },
|
|
69
|
+
mp3: { color: 'var(--purple)', bg: 'var(--purple-lt)', label: 'AUD' },
|
|
70
|
+
wav: { color: 'var(--purple)', bg: 'var(--purple-lt)', label: 'AUD' },
|
|
71
|
+
zip: { color: 'var(--text-secondary)', bg: 'var(--bg-surface-secondary)', label: 'ZIP' },
|
|
72
|
+
tar: { color: 'var(--text-secondary)', bg: 'var(--bg-surface-secondary)', label: 'TAR' },
|
|
73
|
+
gz: { color: 'var(--text-secondary)', bg: 'var(--bg-surface-secondary)', label: 'GZ' },
|
|
74
|
+
code: { color: 'var(--cyan)', bg: 'var(--cyan-lt)', label: 'CODE' }
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// SVGs
|
|
78
|
+
const ICONS = {
|
|
79
|
+
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>',
|
|
80
|
+
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>',
|
|
81
|
+
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>',
|
|
82
|
+
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>',
|
|
83
|
+
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>',
|
|
84
|
+
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>',
|
|
85
|
+
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>',
|
|
86
|
+
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>'
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Helper Functions
|
|
90
|
+
function escapeHtml(str) {
|
|
91
|
+
return String(str || '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function escapeAttr(str) {
|
|
95
|
+
return escapeHtml(str);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getBaseName(path) {
|
|
99
|
+
const val = String(path || '').replace(/\/+$/, '');
|
|
100
|
+
const lastSlash = val.lastIndexOf('/');
|
|
101
|
+
return lastSlash === -1 ? val : val.substring(lastSlash + 1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getFileExt(fileName) {
|
|
105
|
+
const name = getBaseName(fileName);
|
|
106
|
+
const dot = name.lastIndexOf('.');
|
|
107
|
+
return dot > -1 ? name.slice(dot + 1).toLowerCase() : '';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function formatFileSize(bytes) {
|
|
111
|
+
if (!bytes || bytes === 0) return '0 B';
|
|
112
|
+
const k = 1024;
|
|
113
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
114
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
115
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function formatDate(dateStr) {
|
|
119
|
+
if (!dateStr) return '—';
|
|
120
|
+
const d = new Date(dateStr);
|
|
121
|
+
if (isNaN(d.getTime())) return String(dateStr);
|
|
122
|
+
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function withBucketUrl(url) {
|
|
126
|
+
if (!activeBucket) return url;
|
|
127
|
+
const sep = url.includes('?') ? '&' : '?';
|
|
128
|
+
return `${url}${sep}bucket=${encodeURIComponent(activeBucket)}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getViewUrl(key) {
|
|
132
|
+
return withBucketUrl(`/mbkbucket/view/${encodeURIComponent(key)}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function getDownloadUrl(key) {
|
|
136
|
+
return withBucketUrl(`/mbkbucket/download/${encodeURIComponent(key)}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isImage(fileName) {
|
|
140
|
+
const ext = getFileExt(fileName);
|
|
141
|
+
return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp'].includes(ext);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isVideo(fileName) {
|
|
145
|
+
const ext = getFileExt(fileName);
|
|
146
|
+
return ['mp4', 'webm', 'ogg', 'mov', 'mkv'].includes(ext);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function isAudio(fileName) {
|
|
150
|
+
const ext = getFileExt(fileName);
|
|
151
|
+
return ['mp3', 'wav', 'ogg', 'flac', 'm4a', 'aac'].includes(ext);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function isTextOrCode(fileName) {
|
|
155
|
+
const ext = getFileExt(fileName);
|
|
156
|
+
return CODE_EXTS.has(ext) || ['txt', 'md', 'csv', 'log', 'json', 'xml', 'env'].includes(ext);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isPdf(fileName) {
|
|
160
|
+
return getFileExt(fileName) === 'pdf';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function getFileTypeConfig(fileName) {
|
|
164
|
+
const ext = getFileExt(fileName);
|
|
165
|
+
if (FILE_TYPES[ext]) return FILE_TYPES[ext];
|
|
166
|
+
if (CODE_EXTS.has(ext)) return FILE_TYPES.code;
|
|
167
|
+
return { color: 'var(--text-secondary)', bg: 'var(--bg-surface-secondary)', label: ext.toUpperCase().slice(0, 4) || 'FILE' };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function stripApiPrefixFromPath(path, apiPrefix = '') {
|
|
171
|
+
const normalizedPath = String(path || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
172
|
+
const normalizedApiPrefix = String(apiPrefix || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
173
|
+
if (!normalizedApiPrefix) return normalizedPath;
|
|
174
|
+
if (normalizedPath === normalizedApiPrefix || normalizedPath === `${normalizedApiPrefix}/`) return '';
|
|
175
|
+
if (normalizedPath.startsWith(`${normalizedApiPrefix}/`)) {
|
|
176
|
+
return normalizedPath.slice(normalizedApiPrefix.length + 1);
|
|
177
|
+
}
|
|
178
|
+
return normalizedPath;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function toApiFolderPath(prefix = '', apiPrefix = '') {
|
|
182
|
+
const uiPrefix = String(prefix || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
183
|
+
const normalizedApiPrefix = String(apiPrefix || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
184
|
+
if (!uiPrefix) return '';
|
|
185
|
+
if (!normalizedApiPrefix) return uiPrefix;
|
|
186
|
+
if (uiPrefix === normalizedApiPrefix || uiPrefix.startsWith(`${normalizedApiPrefix}/`)) return uiPrefix;
|
|
187
|
+
return `${normalizedApiPrefix}/${uiPrefix}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function extractFoldersAtCurrentLevel(files, prefix = '') {
|
|
191
|
+
const folderSet = new Set();
|
|
192
|
+
const cleanPrefix = String(prefix || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
193
|
+
const prefixOffset = cleanPrefix ? cleanPrefix.length + 1 : 0;
|
|
194
|
+
|
|
195
|
+
(files || []).forEach(file => {
|
|
196
|
+
const key = String(file.Key || '').replace(/^\/+/, '');
|
|
197
|
+
if (cleanPrefix && !key.startsWith(cleanPrefix + '/')) return;
|
|
198
|
+
const relPath = cleanPrefix ? key.substring(prefixOffset) : key;
|
|
199
|
+
const slashIdx = relPath.indexOf('/');
|
|
200
|
+
if (slashIdx !== -1) {
|
|
201
|
+
const folder = relPath.substring(0, slashIdx);
|
|
202
|
+
folderSet.add(cleanPrefix ? `${cleanPrefix}/${folder}` : folder);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
return Array.from(folderSet).sort();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function syncStateUrl() {
|
|
210
|
+
try {
|
|
211
|
+
const parts = [];
|
|
212
|
+
if (activeBucket) parts.push(`bucket=${encodeURIComponent(activeBucket)}`);
|
|
213
|
+
if (currentPrefix) {
|
|
214
|
+
const folderPath = toApiFolderPath(currentPrefix, currentApiPrefix);
|
|
215
|
+
parts.push(`folder=${encodeURIComponent(folderPath).replace(/%2F/g, '/')}`);
|
|
216
|
+
}
|
|
217
|
+
if (currentPage > 1) parts.push(`page=${currentPage}`);
|
|
218
|
+
if (currentSearch) parts.push(`search=${encodeURIComponent(currentSearch)}`);
|
|
219
|
+
if (currentSort !== 'name') parts.push(`sort=${encodeURIComponent(currentSort)}`);
|
|
220
|
+
if (currentOrder !== 'asc') parts.push(`order=${encodeURIComponent(currentOrder)}`);
|
|
221
|
+
if (currentViewMode !== 'grid') parts.push(`view=${encodeURIComponent(currentViewMode)}`);
|
|
222
|
+
if (currentQuickFilter !== 'all') parts.push(`filter=${encodeURIComponent(currentQuickFilter)}`);
|
|
223
|
+
|
|
224
|
+
const newUrl = '/mbkbucket' + (parts.length ? '?' + parts.join('&') : '');
|
|
225
|
+
window.history.replaceState(null, '', newUrl);
|
|
226
|
+
} catch (e) {
|
|
227
|
+
console.warn('History update failed', e);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Toast System
|
|
232
|
+
function showToast(message, type = 'success') {
|
|
233
|
+
const container = document.getElementById('toast-container');
|
|
234
|
+
if (!container) return;
|
|
235
|
+
|
|
236
|
+
const toast = document.createElement('div');
|
|
237
|
+
toast.className = `toast-notification ${type}`;
|
|
238
|
+
toast.innerHTML = `
|
|
239
|
+
<span>${escapeHtml(message)}</span>
|
|
240
|
+
<button type="button" class="toast-close-btn">×</button>
|
|
241
|
+
`;
|
|
242
|
+
|
|
243
|
+
toast.querySelector('.toast-close-btn').addEventListener('click', () => {
|
|
244
|
+
toast.remove();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
container.appendChild(toast);
|
|
248
|
+
|
|
249
|
+
setTimeout(() => {
|
|
250
|
+
toast.style.opacity = '0';
|
|
251
|
+
toast.style.transform = 'translateY(10px)';
|
|
252
|
+
setTimeout(() => toast.remove(), 250);
|
|
253
|
+
}, 4000);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Tree Renderer
|
|
257
|
+
function renderTree() {
|
|
258
|
+
const treeEl = document.getElementById('fm-tree');
|
|
259
|
+
if (!treeEl) return;
|
|
260
|
+
|
|
261
|
+
const allFolders = pageFolders || [];
|
|
262
|
+
const isStarredActive = currentQuickFilter === 'starred';
|
|
263
|
+
const isRootActive = !currentPrefix && currentQuickFilter === 'all';
|
|
264
|
+
|
|
265
|
+
let treeHtml = `
|
|
266
|
+
<div class="tree-group">
|
|
267
|
+
<button type="button" class="tree-link ${isRootActive ? 'active' : ''}" data-tree-prefix="" data-tree-filter="all">
|
|
268
|
+
${ICONS.drive}
|
|
269
|
+
<span>${escapeHtml(rootLabel)}</span>
|
|
270
|
+
</button>
|
|
271
|
+
</div>
|
|
272
|
+
`;
|
|
273
|
+
|
|
274
|
+
// Folder hierarchy
|
|
275
|
+
if (allFolders.length > 0) {
|
|
276
|
+
treeHtml += `
|
|
277
|
+
<div class="tree-group">
|
|
278
|
+
<div class="tree-subs">
|
|
279
|
+
${allFolders.map(folder => {
|
|
280
|
+
const cleanFolder = String(folder || '').replace(/\/+$/, '');
|
|
281
|
+
const folderName = getBaseName(cleanFolder);
|
|
282
|
+
const folderPathForAction = currentPrefix && !cleanFolder.startsWith(currentPrefix + '/') && cleanFolder !== currentPrefix
|
|
283
|
+
? `${currentPrefix}/${cleanFolder}`
|
|
284
|
+
: cleanFolder;
|
|
285
|
+
const isFolderActive = currentPrefix === folderPathForAction;
|
|
286
|
+
return `
|
|
287
|
+
<button type="button" class="tree-link tree-sub ${isFolderActive ? 'active' : ''}" data-tree-prefix="${escapeAttr(folderPathForAction)}" data-tree-filter="all">
|
|
288
|
+
${ICONS.folderSm}
|
|
289
|
+
<span>${escapeHtml(folderName)}</span>
|
|
290
|
+
</button>
|
|
291
|
+
`;
|
|
292
|
+
}).join('')}
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Quick navigation filters
|
|
299
|
+
treeHtml += `
|
|
300
|
+
<div class="tree-group" style="margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--border-color-light);">
|
|
301
|
+
<button type="button" class="tree-link ${isStarredActive ? 'active' : ''}" data-tree-filter="starred">
|
|
302
|
+
${ICONS.starred}
|
|
303
|
+
<span>Starred</span>
|
|
304
|
+
${starredFiles.size > 0 ? `<span class="tree-badge">${starredFiles.size}</span>` : ''}
|
|
305
|
+
</button>
|
|
306
|
+
<button type="button" class="tree-link ${currentQuickFilter === 'media' ? 'active' : ''}" data-tree-filter="media">
|
|
307
|
+
${ICONS.media}
|
|
308
|
+
<span>Media</span>
|
|
309
|
+
</button>
|
|
310
|
+
<button type="button" class="tree-link ${currentQuickFilter === 'docs' ? 'active' : ''}" data-tree-filter="docs">
|
|
311
|
+
${ICONS.docs}
|
|
312
|
+
<span>Documents</span>
|
|
313
|
+
</button>
|
|
314
|
+
<button type="button" class="tree-link ${currentQuickFilter === 'code' ? 'active' : ''}" data-tree-filter="code">
|
|
315
|
+
${ICONS.code}
|
|
316
|
+
<span>Code</span>
|
|
317
|
+
</button>
|
|
318
|
+
</div>
|
|
319
|
+
`;
|
|
320
|
+
|
|
321
|
+
treeEl.innerHTML = treeHtml;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Breadcrumbs Renderer
|
|
325
|
+
function renderBreadcrumbs() {
|
|
326
|
+
const bcEl = document.getElementById('fm-breadcrumb');
|
|
327
|
+
if (!bcEl) return;
|
|
328
|
+
|
|
329
|
+
if (!currentPrefix && currentQuickFilter === 'all') {
|
|
330
|
+
bcEl.innerHTML = `<button type="button" class="bc-link current">${escapeHtml(rootLabel)}</button>`;
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (currentQuickFilter !== 'all') {
|
|
335
|
+
bcEl.innerHTML = `
|
|
336
|
+
<button type="button" class="bc-link" data-nav-prefix="">${escapeHtml(rootLabel)}</button>
|
|
337
|
+
<span class="sep">›</span>
|
|
338
|
+
<button type="button" class="bc-link current">${escapeHtml(currentQuickFilter.charAt(0).toUpperCase() + currentQuickFilter.slice(1))}</button>
|
|
339
|
+
`;
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const parts = currentPrefix.split('/').filter(Boolean);
|
|
344
|
+
let accum = '';
|
|
345
|
+
const breadcrumbHtml = [
|
|
346
|
+
`<button type="button" class="bc-link" data-nav-prefix="">${escapeHtml(rootLabel)}</button>`
|
|
347
|
+
];
|
|
348
|
+
|
|
349
|
+
parts.forEach((part, index) => {
|
|
350
|
+
accum += (accum ? '/' : '') + part;
|
|
351
|
+
const isLast = index === parts.length - 1;
|
|
352
|
+
breadcrumbHtml.push('<span class="sep">›</span>');
|
|
353
|
+
if (isLast) {
|
|
354
|
+
breadcrumbHtml.push(`<button type="button" class="bc-link current">${escapeHtml(part)}</button>`);
|
|
355
|
+
} else {
|
|
356
|
+
breadcrumbHtml.push(`<button type="button" class="bc-link" data-nav-prefix="${escapeAttr(accum)}">${escapeHtml(part)}</button>`);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
bcEl.innerHTML = breadcrumbHtml.join('');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// File Filtering Logic
|
|
364
|
+
function getFilteredFiles() {
|
|
365
|
+
let list = (pageFiles || []).filter(f => !String(f.Key || '').endsWith('/'));
|
|
366
|
+
|
|
367
|
+
// Search filter
|
|
368
|
+
if (currentSearch) {
|
|
369
|
+
const q = currentSearch.toLowerCase();
|
|
370
|
+
list = list.filter(f => getBaseName(f.Key).toLowerCase().includes(q));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Quick filter
|
|
374
|
+
if (currentQuickFilter === 'media') {
|
|
375
|
+
list = list.filter(f => MEDIA_EXTS.has(getFileExt(f.Key)));
|
|
376
|
+
} else if (currentQuickFilter === 'docs') {
|
|
377
|
+
list = list.filter(f => DOC_EXTS.has(getFileExt(f.Key)));
|
|
378
|
+
} else if (currentQuickFilter === 'code') {
|
|
379
|
+
list = list.filter(f => CODE_EXTS.has(getFileExt(f.Key)));
|
|
380
|
+
} else if (currentQuickFilter === 'starred') {
|
|
381
|
+
list = list.filter(f => starredFiles.has(f.Key));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return list;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function getFilteredFolders() {
|
|
388
|
+
if (currentQuickFilter !== 'all') return [];
|
|
389
|
+
if (currentSearch) {
|
|
390
|
+
const q = currentSearch.toLowerCase();
|
|
391
|
+
return (pageFolders || []).filter(f => getBaseName(f).toLowerCase().includes(q));
|
|
392
|
+
}
|
|
393
|
+
return pageFolders || [];
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Main Files & Grid Renderer
|
|
397
|
+
function renderFiles() {
|
|
398
|
+
const gridEl = document.getElementById('fm-grid');
|
|
399
|
+
const emptyEl = document.getElementById('fm-empty');
|
|
400
|
+
if (!gridEl) return;
|
|
401
|
+
|
|
402
|
+
const files = getFilteredFiles();
|
|
403
|
+
const folders = getFilteredFolders();
|
|
404
|
+
|
|
405
|
+
// Update stats
|
|
406
|
+
const totalCountEl = document.getElementById('totalFilesCount');
|
|
407
|
+
const filterInfoEl = document.getElementById('filterInfo');
|
|
408
|
+
const totalItems = files.length + folders.length;
|
|
409
|
+
|
|
410
|
+
if (totalCountEl) {
|
|
411
|
+
totalCountEl.textContent = `${totalItems} item${totalItems === 1 ? '' : 's'}`;
|
|
412
|
+
}
|
|
413
|
+
if (filterInfoEl) {
|
|
414
|
+
filterInfoEl.textContent = activeBucket ? `Bucket: ${activeBucket}` : 'Default bucket';
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (totalItems === 0) {
|
|
418
|
+
gridEl.innerHTML = '';
|
|
419
|
+
if (emptyEl) {
|
|
420
|
+
emptyEl.hidden = false;
|
|
421
|
+
emptyEl.style.display = 'flex';
|
|
422
|
+
}
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (emptyEl) {
|
|
427
|
+
emptyEl.hidden = true;
|
|
428
|
+
emptyEl.style.display = 'none';
|
|
429
|
+
}
|
|
430
|
+
gridEl.className = `fm-grid view-${currentViewMode}`;
|
|
431
|
+
|
|
432
|
+
let html = '';
|
|
433
|
+
|
|
434
|
+
// If List view, add header row
|
|
435
|
+
if (currentViewMode === 'list') {
|
|
436
|
+
html += `
|
|
437
|
+
<div class="fm-list-header">
|
|
438
|
+
<div></div>
|
|
439
|
+
<div></div>
|
|
440
|
+
<div>Name</div>
|
|
441
|
+
<div>Size</div>
|
|
442
|
+
<div>Modified</div>
|
|
443
|
+
<div></div>
|
|
444
|
+
<div></div>
|
|
445
|
+
</div>
|
|
446
|
+
`;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Render Folders
|
|
450
|
+
folders.forEach(folder => {
|
|
451
|
+
const cleanFolder = String(folder || '').replace(/\/+$/, '');
|
|
452
|
+
const folderName = getBaseName(cleanFolder);
|
|
453
|
+
const folderPathForAction = currentPrefix && !cleanFolder.startsWith(currentPrefix + '/') && cleanFolder !== currentPrefix
|
|
454
|
+
? `${currentPrefix}/${cleanFolder}`
|
|
455
|
+
: cleanFolder;
|
|
456
|
+
const safePath = escapeAttr(folderPathForAction);
|
|
457
|
+
const safeName = escapeHtml(folderName);
|
|
458
|
+
|
|
459
|
+
if (currentViewMode === 'grid') {
|
|
460
|
+
html += `
|
|
461
|
+
<div class="fm-item" data-type="folder" data-folder-path="${safePath}" tabindex="0">
|
|
462
|
+
<div class="fm-item-icon">
|
|
463
|
+
${ICONS.folder}
|
|
464
|
+
</div>
|
|
465
|
+
<div class="fm-item-details">
|
|
466
|
+
<div class="fm-item-name" title="${safeName}">${safeName}</div>
|
|
467
|
+
<div class="fm-item-meta">Folder</div>
|
|
468
|
+
</div>
|
|
469
|
+
<button type="button" class="fm-item-menu" data-menu-folder="${safePath}" aria-label="Folder options">
|
|
470
|
+
${ICONS.more}
|
|
471
|
+
</button>
|
|
472
|
+
</div>
|
|
473
|
+
`;
|
|
474
|
+
} else {
|
|
475
|
+
html += `
|
|
476
|
+
<div class="fm-item" data-type="folder" data-folder-path="${safePath}" tabindex="0">
|
|
477
|
+
<div class="fm-item-checkbox"></div>
|
|
478
|
+
<div class="fm-item-icon">
|
|
479
|
+
${ICONS.folderSm}
|
|
480
|
+
</div>
|
|
481
|
+
<div class="fm-item-name" title="${safeName}">${safeName}</div>
|
|
482
|
+
<div class="fm-item-size fm-item-meta">—</div>
|
|
483
|
+
<div class="fm-item-date fm-item-meta">—</div>
|
|
484
|
+
<div class="fm-star-wrap"></div>
|
|
485
|
+
<button type="button" class="fm-item-menu" data-menu-folder="${safePath}" aria-label="Folder options">
|
|
486
|
+
${ICONS.more}
|
|
487
|
+
</button>
|
|
488
|
+
</div>
|
|
489
|
+
`;
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
// Render Files
|
|
494
|
+
files.forEach(file => {
|
|
495
|
+
const rawKey = String(file.Key || '');
|
|
496
|
+
const fileName = getBaseName(rawKey);
|
|
497
|
+
const safeKey = escapeAttr(rawKey);
|
|
498
|
+
const safeName = escapeHtml(fileName);
|
|
499
|
+
const isStarred = starredFiles.has(rawKey);
|
|
500
|
+
const isImg = isImage(fileName);
|
|
501
|
+
const typeCfg = getFileTypeConfig(fileName);
|
|
502
|
+
const formattedSize = file.Size ? formatFileSize(file.Size) : '—';
|
|
503
|
+
const formattedDate = file.LastModified ? formatDate(file.LastModified) : '—';
|
|
504
|
+
|
|
505
|
+
let iconInner = `<span class="ext">${typeCfg.label}</span>`;
|
|
506
|
+
if (isImg) {
|
|
507
|
+
iconInner = `<img src="${getViewUrl(rawKey)}" class="fm-item-thumb" alt="${safeName}" loading="lazy" onerror="this.outerHTML='<span class=\\'ext\\'>IMG</span>'">`;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (currentViewMode === 'grid') {
|
|
511
|
+
html += `
|
|
512
|
+
<div class="fm-item ${isStarred ? 'starred' : ''}" data-type="file" data-key="${safeKey}" tabindex="0">
|
|
513
|
+
<div class="fm-item-icon" style="${isImg ? '' : `background:${typeCfg.bg};color:${typeCfg.color}`}">
|
|
514
|
+
${iconInner}
|
|
515
|
+
</div>
|
|
516
|
+
<div class="fm-item-details">
|
|
517
|
+
<div class="fm-item-name" title="${safeName}">${safeName}</div>
|
|
518
|
+
<div class="fm-item-meta">${formattedSize} · ${formattedDate}</div>
|
|
519
|
+
</div>
|
|
520
|
+
<button type="button" class="fm-star ${isStarred ? 'on' : ''}" data-star-key="${safeKey}" aria-label="${isStarred ? 'Unstar' : 'Star'}">
|
|
521
|
+
${ICONS.starred}
|
|
522
|
+
</button>
|
|
523
|
+
<button type="button" class="fm-item-menu" data-menu-file="${safeKey}" aria-label="File options">
|
|
524
|
+
${ICONS.more}
|
|
525
|
+
</button>
|
|
526
|
+
</div>
|
|
527
|
+
`;
|
|
528
|
+
} else {
|
|
529
|
+
html += `
|
|
530
|
+
<div class="fm-item ${isStarred ? 'starred' : ''}" data-type="file" data-key="${safeKey}" tabindex="0">
|
|
531
|
+
<div class="fm-item-checkbox">
|
|
532
|
+
<input type="checkbox" class="file-select-cb" data-key="${safeKey}">
|
|
533
|
+
</div>
|
|
534
|
+
<div class="fm-item-icon" style="${isImg ? '' : `background:${typeCfg.bg};color:${typeCfg.color}`}">
|
|
535
|
+
<span class="ext">${typeCfg.label}</span>
|
|
536
|
+
</div>
|
|
537
|
+
<div class="fm-item-name" title="${safeName}">${safeName}</div>
|
|
538
|
+
<div class="fm-item-size fm-item-meta">${formattedSize}</div>
|
|
539
|
+
<div class="fm-item-date fm-item-meta">${formattedDate}</div>
|
|
540
|
+
<button type="button" class="fm-star ${isStarred ? 'on' : ''}" data-star-key="${safeKey}" aria-label="${isStarred ? 'Unstar' : 'Star'}">
|
|
541
|
+
${ICONS.starred}
|
|
542
|
+
</button>
|
|
543
|
+
<button type="button" class="fm-item-menu" data-menu-file="${safeKey}" aria-label="File options">
|
|
544
|
+
${ICONS.more}
|
|
545
|
+
</button>
|
|
546
|
+
</div>
|
|
547
|
+
`;
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
gridEl.innerHTML = html;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Load Files API Call
|
|
555
|
+
async function loadFiles(page = 1, prefix = '', search = '') {
|
|
556
|
+
currentPage = page;
|
|
557
|
+
currentPrefix = String(prefix || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
558
|
+
currentSearch = search;
|
|
559
|
+
|
|
560
|
+
syncStateUrl();
|
|
561
|
+
|
|
562
|
+
const gridEl = document.getElementById('fm-grid');
|
|
563
|
+
if (gridEl) {
|
|
564
|
+
gridEl.innerHTML = `
|
|
565
|
+
<div class="fm-skeleton-grid">
|
|
566
|
+
${[...Array(6)].map(() => `
|
|
567
|
+
<div class="fm-skeleton-card">
|
|
568
|
+
<div class="fm-skeleton-icon"></div>
|
|
569
|
+
<div class="fm-skeleton-line w-75"></div>
|
|
570
|
+
<div class="fm-skeleton-line w-50"></div>
|
|
571
|
+
</div>
|
|
572
|
+
`).join('')}
|
|
573
|
+
</div>
|
|
574
|
+
`;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
try {
|
|
578
|
+
const searchParam = search ? `&search=${encodeURIComponent(search)}` : '';
|
|
579
|
+
const sortParam = `&sort=${currentSort}&order=${currentOrder}`;
|
|
580
|
+
const recursiveParam = `&recursive=false`;
|
|
581
|
+
const url = withBucketUrl(`/mbkbucket/api/files?page=${page}&prefix=${encodeURIComponent(currentPrefix)}${searchParam}${sortParam}${recursiveParam}`);
|
|
582
|
+
|
|
583
|
+
const response = await fetch(url);
|
|
584
|
+
const data = await response.json();
|
|
585
|
+
|
|
586
|
+
if (!data.success) {
|
|
587
|
+
throw new Error(data.error || 'Failed to load files');
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
currentNextToken = data.nextContinuationToken || null;
|
|
591
|
+
currentApiPrefix = data.prefix || '';
|
|
592
|
+
pageFiles = data.files || [];
|
|
593
|
+
|
|
594
|
+
if (data.mode === 'optimized') {
|
|
595
|
+
pageFolders = (data.folders || []).map(folder => stripApiPrefixFromPath(folder, currentApiPrefix)).filter(Boolean);
|
|
596
|
+
} else {
|
|
597
|
+
pageFolders = currentSearch
|
|
598
|
+
? []
|
|
599
|
+
: extractFoldersAtCurrentLevel(pageFiles, currentApiPrefix || currentPrefix)
|
|
600
|
+
.map(folder => stripApiPrefixFromPath(folder, currentApiPrefix))
|
|
601
|
+
.filter(Boolean);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
renderTree();
|
|
605
|
+
renderBreadcrumbs();
|
|
606
|
+
renderFiles();
|
|
607
|
+
renderPagination(data);
|
|
608
|
+
} catch (err) {
|
|
609
|
+
console.error('Error loading files:', err);
|
|
610
|
+
showToast(err.message || 'Error loading files', 'error');
|
|
611
|
+
if (gridEl) {
|
|
612
|
+
gridEl.innerHTML = `
|
|
613
|
+
<div class="fm-empty">
|
|
614
|
+
<div class="empty-title">Error Loading Files</div>
|
|
615
|
+
<div class="empty-desc">${escapeHtml(err.message)}</div>
|
|
616
|
+
<button type="button" class="btn btn-primary btn-sm mt-3" onclick="window.mbkbucket.loadFiles(${page}, '${escapeAttr(currentPrefix)}')">Retry</button>
|
|
617
|
+
</div>
|
|
618
|
+
`;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Pagination Renderer
|
|
624
|
+
function renderPagination(data) {
|
|
625
|
+
const paginationContainer = document.getElementById('paginationContainer');
|
|
626
|
+
const paginationList = document.getElementById('paginationList');
|
|
627
|
+
if (!paginationContainer || !paginationList) return;
|
|
628
|
+
|
|
629
|
+
if (!data.totalPages || data.totalPages <= 1) {
|
|
630
|
+
paginationContainer.style.display = 'none';
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
paginationContainer.style.display = 'flex';
|
|
635
|
+
let html = '';
|
|
636
|
+
|
|
637
|
+
// Prev
|
|
638
|
+
html += `
|
|
639
|
+
<li class="page-item ${data.currentPage <= 1 ? 'disabled' : ''}">
|
|
640
|
+
<button type="button" class="page-link" data-page="${data.currentPage - 1}">‹</button>
|
|
641
|
+
</li>
|
|
642
|
+
`;
|
|
643
|
+
|
|
644
|
+
// Pages
|
|
645
|
+
for (let i = 1; i <= data.totalPages; i++) {
|
|
646
|
+
if (i === 1 || i === data.totalPages || (i >= data.currentPage - 2 && i <= data.currentPage + 2)) {
|
|
647
|
+
html += `
|
|
648
|
+
<li class="page-item ${i === data.currentPage ? 'active' : ''}">
|
|
649
|
+
<button type="button" class="page-link" data-page="${i}">${i}</button>
|
|
650
|
+
</li>
|
|
651
|
+
`;
|
|
652
|
+
} else if (i === data.currentPage - 3 || i === data.currentPage + 3) {
|
|
653
|
+
html += `<li class="page-item disabled"><span class="page-link">…</span></li>`;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Next
|
|
658
|
+
html += `
|
|
659
|
+
<li class="page-item ${data.currentPage >= data.totalPages ? 'disabled' : ''}">
|
|
660
|
+
<button type="button" class="page-link" data-page="${data.currentPage + 1}">›</button>
|
|
661
|
+
</li>
|
|
662
|
+
`;
|
|
663
|
+
|
|
664
|
+
paginationList.innerHTML = html;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Context Menu
|
|
668
|
+
function openContextMenu(x, y, items) {
|
|
669
|
+
const menu = document.getElementById('fm-context-menu');
|
|
670
|
+
if (!menu) return;
|
|
671
|
+
|
|
672
|
+
menu.innerHTML = items.map(item => {
|
|
673
|
+
if (item === '-') return '<div class="fm-context-divider"></div>';
|
|
674
|
+
return `<button type="button" class="fm-context-item ${item.danger ? 'danger' : ''}" data-action="${item.id}">${item.icon || ''} ${escapeHtml(item.label)}</button>`;
|
|
675
|
+
}).join('');
|
|
676
|
+
|
|
677
|
+
menu.style.display = 'block';
|
|
678
|
+
|
|
679
|
+
const menuWidth = 180;
|
|
680
|
+
const menuHeight = menu.offsetHeight || 150;
|
|
681
|
+
const posX = Math.min(x, window.innerWidth - menuWidth - 10);
|
|
682
|
+
const posY = Math.min(y, window.innerHeight - menuHeight - 10);
|
|
683
|
+
|
|
684
|
+
menu.style.left = `${posX}px`;
|
|
685
|
+
menu.style.top = `${posY}px`;
|
|
686
|
+
|
|
687
|
+
menu.querySelectorAll('.fm-context-item').forEach(btn => {
|
|
688
|
+
btn.addEventListener('click', (e) => {
|
|
689
|
+
e.stopPropagation();
|
|
690
|
+
const actionId = btn.dataset.action;
|
|
691
|
+
const targetItem = items.find(it => it.id === actionId);
|
|
692
|
+
menu.style.display = 'none';
|
|
693
|
+
if (targetItem && typeof targetItem.action === 'function') {
|
|
694
|
+
targetItem.action();
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function closeContextMenu() {
|
|
701
|
+
const menu = document.getElementById('fm-context-menu');
|
|
702
|
+
if (menu) menu.style.display = 'none';
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Actions: Star, Download, Preview, Delete, New Folder
|
|
706
|
+
function toggleStar(key) {
|
|
707
|
+
if (starredFiles.has(key)) {
|
|
708
|
+
starredFiles.delete(key);
|
|
709
|
+
showToast('Removed from Starred');
|
|
710
|
+
} else {
|
|
711
|
+
starredFiles.add(key);
|
|
712
|
+
showToast('Added to Starred', 'success');
|
|
713
|
+
}
|
|
714
|
+
localStorage.setItem('mbk_starred_keys', JSON.stringify([...starredFiles]));
|
|
715
|
+
renderTree();
|
|
716
|
+
renderFiles();
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
async function downloadFile(key) {
|
|
720
|
+
const fileName = getBaseName(key);
|
|
721
|
+
const downloadUrl = getDownloadUrl(key);
|
|
722
|
+
showToast(`Downloading ${fileName}...`);
|
|
723
|
+
try {
|
|
724
|
+
const response = await fetch(downloadUrl);
|
|
725
|
+
if (!response.ok) throw new Error('Download failed');
|
|
726
|
+
const blob = await response.blob();
|
|
727
|
+
const url = URL.createObjectURL(blob);
|
|
728
|
+
const a = document.createElement('a');
|
|
729
|
+
a.href = url;
|
|
730
|
+
a.download = fileName;
|
|
731
|
+
document.body.appendChild(a);
|
|
732
|
+
a.click();
|
|
733
|
+
document.body.removeChild(a);
|
|
734
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
735
|
+
} catch (err) {
|
|
736
|
+
showToast(err.message, 'error');
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function copyShareLink(key) {
|
|
741
|
+
const viewUrl = window.location.origin + getViewUrl(key);
|
|
742
|
+
navigator.clipboard.writeText(viewUrl).then(() => {
|
|
743
|
+
showToast('Link copied to clipboard', 'success');
|
|
744
|
+
}).catch(() => {
|
|
745
|
+
showToast('Failed to copy link', 'error');
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Preview Modal
|
|
750
|
+
function openPreview(key) {
|
|
751
|
+
const modal = document.getElementById('previewModal');
|
|
752
|
+
const titleEl = document.getElementById('previewFileName');
|
|
753
|
+
const contentEl = document.getElementById('previewContent');
|
|
754
|
+
const openBtn = document.getElementById('previewOpenBtn');
|
|
755
|
+
const downloadBtn = document.getElementById('previewDownloadBtn');
|
|
756
|
+
|
|
757
|
+
if (!modal || !contentEl) return;
|
|
758
|
+
|
|
759
|
+
const fileName = getBaseName(key);
|
|
760
|
+
const viewUrl = getViewUrl(key);
|
|
761
|
+
const downloadUrl = getDownloadUrl(key);
|
|
762
|
+
|
|
763
|
+
if (titleEl) titleEl.textContent = fileName;
|
|
764
|
+
if (openBtn) openBtn.href = viewUrl;
|
|
765
|
+
if (downloadBtn) downloadBtn.href = downloadUrl;
|
|
766
|
+
|
|
767
|
+
contentEl.innerHTML = '<div class="empty-preview"><div class="spinner-border text-light"></div><div class="mt-2 text-light">Loading preview...</div></div>';
|
|
768
|
+
modal.classList.add('show');
|
|
769
|
+
|
|
770
|
+
if (isImage(fileName)) {
|
|
771
|
+
contentEl.innerHTML = `<img src="${viewUrl}" alt="${escapeAttr(fileName)}">`;
|
|
772
|
+
} else if (isVideo(fileName)) {
|
|
773
|
+
contentEl.innerHTML = `<video controls autoplay class="w-100" style="max-height: 70vh;"><source src="${viewUrl}"></video>`;
|
|
774
|
+
} else if (isAudio(fileName)) {
|
|
775
|
+
contentEl.innerHTML = `<div class="p-4 text-center"><audio controls autoplay class="w-100"><source src="${viewUrl}"></audio></div>`;
|
|
776
|
+
} else if (isPdf(fileName)) {
|
|
777
|
+
contentEl.innerHTML = `<iframe src="${viewUrl}" class="preview-iframe"></iframe>`;
|
|
778
|
+
} else if (isTextOrCode(fileName)) {
|
|
779
|
+
fetch(viewUrl).then(res => res.text()).then(text => {
|
|
780
|
+
contentEl.innerHTML = `<pre class="preview-code-block"><code>${escapeHtml(text)}</code></pre>`;
|
|
781
|
+
}).catch(err => {
|
|
782
|
+
contentEl.innerHTML = `<div class="p-4 text-danger text-center">Failed to load text preview: ${escapeHtml(err.message)}</div>`;
|
|
783
|
+
});
|
|
784
|
+
} else {
|
|
785
|
+
contentEl.innerHTML = `
|
|
786
|
+
<div class="p-5 text-center text-light">
|
|
787
|
+
<h5>No inline preview available for this file type.</h5>
|
|
788
|
+
<p class="text-muted small">You can open it in a new tab or download it directly.</p>
|
|
789
|
+
</div>
|
|
790
|
+
`;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
window.closePreviewModal = function () {
|
|
795
|
+
const modal = document.getElementById('previewModal');
|
|
796
|
+
if (modal) modal.classList.remove('show');
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
// Delete Modal
|
|
800
|
+
function promptDelete(key, isFolder = false) {
|
|
801
|
+
const modal = document.getElementById('deleteModal');
|
|
802
|
+
const deleteFileName = document.getElementById('deleteFileName');
|
|
803
|
+
const deleteKey = document.getElementById('deleteKey');
|
|
804
|
+
const deleteIsFolder = document.getElementById('deleteIsFolder');
|
|
805
|
+
const deleteConfirmLabel = document.getElementById('deleteConfirmLabel');
|
|
806
|
+
|
|
807
|
+
if (!modal) return;
|
|
808
|
+
|
|
809
|
+
if (deleteFileName) deleteFileName.textContent = key;
|
|
810
|
+
if (deleteKey) deleteKey.value = key;
|
|
811
|
+
if (deleteIsFolder) deleteIsFolder.value = isFolder ? 'true' : '';
|
|
812
|
+
if (deleteConfirmLabel) deleteConfirmLabel.textContent = isFolder ? 'Delete Folder' : 'Delete File';
|
|
813
|
+
|
|
814
|
+
modal.classList.add('show');
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
window.closeModal = function () {
|
|
818
|
+
const deleteModal = document.getElementById('deleteModal');
|
|
819
|
+
if (deleteModal) deleteModal.classList.remove('show');
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
// New Folder Modal
|
|
823
|
+
function openNewFolderModal() {
|
|
824
|
+
const modal = document.getElementById('newFolderModal');
|
|
825
|
+
const input = document.getElementById('newFolderNameInput');
|
|
826
|
+
if (!modal) return;
|
|
827
|
+
if (input) input.value = '';
|
|
828
|
+
modal.classList.add('show');
|
|
829
|
+
if (input) setTimeout(() => input.focus(), 50);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
window.closeNewFolderModal = function () {
|
|
833
|
+
const modal = document.getElementById('newFolderModal');
|
|
834
|
+
if (modal) modal.classList.remove('show');
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
window.submitCreateFolder = async function () {
|
|
838
|
+
const input = document.getElementById('newFolderNameInput');
|
|
839
|
+
const name = input ? input.value.trim() : '';
|
|
840
|
+
if (!name) {
|
|
841
|
+
showToast('Please enter a folder name', 'warning');
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
try {
|
|
846
|
+
const response = await fetch(withBucketUrl('/mbkbucket/create-folder'), {
|
|
847
|
+
method: 'POST',
|
|
848
|
+
headers: { 'Content-Type': 'application/json' },
|
|
849
|
+
body: JSON.stringify({ folderName: name, prefix: currentPrefix })
|
|
850
|
+
});
|
|
851
|
+
const data = await response.json();
|
|
852
|
+
if (!data.success) throw new Error(data.error || 'Failed to create folder');
|
|
853
|
+
|
|
854
|
+
closeNewFolderModal();
|
|
855
|
+
showToast(`Folder "${name}" created`, 'success');
|
|
856
|
+
loadFiles(1, currentPrefix);
|
|
857
|
+
} catch (err) {
|
|
858
|
+
showToast(err.message, 'error');
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
// Upload Modal & Flow
|
|
863
|
+
function openUploadModal() {
|
|
864
|
+
const modal = document.getElementById('uploadModal');
|
|
865
|
+
const prefixInput = document.getElementById('uploadPrefixInput');
|
|
866
|
+
if (prefixInput) prefixInput.value = currentPrefix;
|
|
867
|
+
if (modal) modal.classList.add('show');
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
window.closeUploadModal = function () {
|
|
871
|
+
const modal = document.getElementById('uploadModal');
|
|
872
|
+
if (modal) modal.classList.remove('show');
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
function updateUploadQueueUI() {
|
|
876
|
+
const queueEl = document.getElementById('uploadQueue');
|
|
877
|
+
const clearBtn = document.getElementById('clearQueueBtn');
|
|
878
|
+
if (!queueEl) return;
|
|
879
|
+
|
|
880
|
+
if (uploadQueue.length === 0) {
|
|
881
|
+
queueEl.innerHTML = '';
|
|
882
|
+
if (clearBtn) clearBtn.style.display = 'none';
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (clearBtn) clearBtn.style.display = 'inline-flex';
|
|
887
|
+
queueEl.innerHTML = uploadQueue.map((file, idx) => `
|
|
888
|
+
<div class="upload-queue-item">
|
|
889
|
+
<div class="d-flex align-items-center gap-2">
|
|
890
|
+
<span class="font-medium">${escapeHtml(file.name)}</span>
|
|
891
|
+
<span class="upload-queue-meta">${formatFileSize(file.size)}</span>
|
|
892
|
+
</div>
|
|
893
|
+
<button type="button" class="btn-close" data-remove-queue="${idx}" style="font-size: 14px;">×</button>
|
|
894
|
+
</div>
|
|
895
|
+
`).join('');
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
async function processUploads() {
|
|
899
|
+
if (isUploading || uploadQueue.length === 0) return;
|
|
900
|
+
isUploading = true;
|
|
901
|
+
|
|
902
|
+
const progressWrap = document.getElementById('uploadProgress');
|
|
903
|
+
const progressBar = document.getElementById('progressBar');
|
|
904
|
+
const percentEl = document.getElementById('uploadPercent');
|
|
905
|
+
const statusLabel = document.getElementById('uploadStatusLabel');
|
|
906
|
+
const startBtn = document.getElementById('startUploadBtn');
|
|
907
|
+
|
|
908
|
+
if (progressWrap) progressWrap.style.display = 'block';
|
|
909
|
+
if (startBtn) startBtn.disabled = true;
|
|
910
|
+
|
|
911
|
+
const totalFiles = uploadQueue.length;
|
|
912
|
+
let completed = 0;
|
|
913
|
+
|
|
914
|
+
for (let i = 0; i < uploadQueue.length; i++) {
|
|
915
|
+
const file = uploadQueue[i];
|
|
916
|
+
if (statusLabel) statusLabel.textContent = `Uploading ${file.name} (${i + 1}/${totalFiles})...`;
|
|
917
|
+
|
|
918
|
+
const formData = new FormData();
|
|
919
|
+
formData.append('file', file);
|
|
920
|
+
formData.append('prefix', currentPrefix);
|
|
921
|
+
|
|
922
|
+
try {
|
|
923
|
+
const response = await fetch(withBucketUrl('/mbkbucket/upload'), {
|
|
924
|
+
method: 'POST',
|
|
925
|
+
body: formData
|
|
926
|
+
});
|
|
927
|
+
const result = await response.json();
|
|
928
|
+
if (!result.success) throw new Error(result.error || 'Upload failed');
|
|
929
|
+
completed++;
|
|
930
|
+
} catch (err) {
|
|
931
|
+
showToast(`Failed to upload ${file.name}: ${err.message}`, 'error');
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const percent = Math.round(((i + 1) / totalFiles) * 100);
|
|
935
|
+
if (progressBar) progressBar.style.width = `${percent}%`;
|
|
936
|
+
if (percentEl) percentEl.textContent = `${percent}%`;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
showToast(`Successfully uploaded ${completed} file(s)`, 'success');
|
|
940
|
+
uploadQueue = [];
|
|
941
|
+
isUploading = false;
|
|
942
|
+
if (startBtn) startBtn.disabled = false;
|
|
943
|
+
if (progressWrap) progressWrap.style.display = 'none';
|
|
944
|
+
closeUploadModal();
|
|
945
|
+
updateUploadQueueUI();
|
|
946
|
+
loadFiles(1, currentPrefix);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// Event Listeners Setup
|
|
950
|
+
function initEventListeners() {
|
|
951
|
+
// Tree clicks
|
|
952
|
+
document.getElementById('fm-tree')?.addEventListener('click', (e) => {
|
|
953
|
+
const link = e.target.closest('.tree-link');
|
|
954
|
+
if (!link) return;
|
|
955
|
+
e.preventDefault();
|
|
956
|
+
const filter = link.dataset.treeFilter;
|
|
957
|
+
const prefix = link.dataset.treePrefix;
|
|
958
|
+
|
|
959
|
+
if (filter && filter !== 'all') {
|
|
960
|
+
currentQuickFilter = filter;
|
|
961
|
+
document.querySelectorAll('.quick-filter-btn').forEach(b => b.classList.remove('active'));
|
|
962
|
+
renderTree();
|
|
963
|
+
renderBreadcrumbs();
|
|
964
|
+
renderFiles();
|
|
965
|
+
} else {
|
|
966
|
+
currentQuickFilter = 'all';
|
|
967
|
+
document.querySelectorAll('.quick-filter-btn').forEach(b => {
|
|
968
|
+
b.classList.toggle('active', b.dataset.filter === 'all');
|
|
969
|
+
});
|
|
970
|
+
loadFiles(1, prefix !== undefined ? prefix : '');
|
|
971
|
+
}
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
// Breadcrumb clicks
|
|
975
|
+
document.getElementById('fm-breadcrumb')?.addEventListener('click', (e) => {
|
|
976
|
+
const link = e.target.closest('[data-nav-prefix]');
|
|
977
|
+
if (!link) return;
|
|
978
|
+
e.preventDefault();
|
|
979
|
+
currentQuickFilter = 'all';
|
|
980
|
+
loadFiles(1, link.dataset.navPrefix || '');
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
// Quick filters
|
|
984
|
+
document.querySelectorAll('.quick-filter-btn').forEach(btn => {
|
|
985
|
+
btn.addEventListener('click', () => {
|
|
986
|
+
document.querySelectorAll('.quick-filter-btn').forEach(b => b.classList.remove('active'));
|
|
987
|
+
btn.classList.add('active');
|
|
988
|
+
currentQuickFilter = btn.dataset.filter || 'all';
|
|
989
|
+
renderTree();
|
|
990
|
+
renderBreadcrumbs();
|
|
991
|
+
renderFiles();
|
|
992
|
+
syncStateUrl();
|
|
993
|
+
});
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
// View mode toggle
|
|
997
|
+
const gridBtn = document.getElementById('view-grid');
|
|
998
|
+
const listBtn = document.getElementById('view-list');
|
|
999
|
+
|
|
1000
|
+
gridBtn?.addEventListener('click', () => {
|
|
1001
|
+
currentViewMode = 'grid';
|
|
1002
|
+
localStorage.setItem('mbk_view_mode', 'grid');
|
|
1003
|
+
gridBtn.classList.add('active');
|
|
1004
|
+
listBtn?.classList.remove('active');
|
|
1005
|
+
renderFiles();
|
|
1006
|
+
syncStateUrl();
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
listBtn?.addEventListener('click', () => {
|
|
1010
|
+
currentViewMode = 'list';
|
|
1011
|
+
localStorage.setItem('mbk_view_mode', 'list');
|
|
1012
|
+
listBtn.classList.add('active');
|
|
1013
|
+
gridBtn?.classList.remove('active');
|
|
1014
|
+
renderFiles();
|
|
1015
|
+
syncStateUrl();
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
// Search input
|
|
1019
|
+
const searchInput = document.getElementById('searchInput');
|
|
1020
|
+
const clearSearchBtn = document.getElementById('searchClearBtn');
|
|
1021
|
+
|
|
1022
|
+
let searchTimer = null;
|
|
1023
|
+
searchInput?.addEventListener('input', (e) => {
|
|
1024
|
+
const val = e.target.value;
|
|
1025
|
+
if (clearSearchBtn) clearSearchBtn.style.display = val ? 'block' : 'none';
|
|
1026
|
+
|
|
1027
|
+
clearTimeout(searchTimer);
|
|
1028
|
+
searchTimer = setTimeout(() => {
|
|
1029
|
+
currentSearch = val.trim();
|
|
1030
|
+
renderFiles();
|
|
1031
|
+
}, 250);
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
clearSearchBtn?.addEventListener('click', () => {
|
|
1035
|
+
if (searchInput) searchInput.value = '';
|
|
1036
|
+
clearSearchBtn.style.display = 'none';
|
|
1037
|
+
currentSearch = '';
|
|
1038
|
+
renderFiles();
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
// Grid Item Interactions (Click, Double Click, Star, Context Menu)
|
|
1042
|
+
document.getElementById('fm-grid')?.addEventListener('click', (e) => {
|
|
1043
|
+
// Star click
|
|
1044
|
+
const starBtn = e.target.closest('[data-star-key]');
|
|
1045
|
+
if (starBtn) {
|
|
1046
|
+
e.stopPropagation();
|
|
1047
|
+
toggleStar(starBtn.dataset.starKey);
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Menu click
|
|
1052
|
+
const menuBtn = e.target.closest('.fm-item-menu');
|
|
1053
|
+
if (menuBtn) {
|
|
1054
|
+
e.stopPropagation();
|
|
1055
|
+
const rect = menuBtn.getBoundingClientRect();
|
|
1056
|
+
const fileKey = menuBtn.dataset.menuFile;
|
|
1057
|
+
const folderPath = menuBtn.dataset.menuFolder;
|
|
1058
|
+
|
|
1059
|
+
if (fileKey) {
|
|
1060
|
+
const isStarred = starredFiles.has(fileKey);
|
|
1061
|
+
openContextMenu(rect.left, rect.bottom + 4, [
|
|
1062
|
+
{ id: 'view', label: 'Preview', icon: '<i class="fas fa-eye"></i>', action: () => openPreview(fileKey) },
|
|
1063
|
+
{ id: 'download', label: 'Download', icon: '<i class="fas fa-download"></i>', action: () => downloadFile(fileKey) },
|
|
1064
|
+
{ id: 'share', label: 'Copy link', icon: '<i class="fas fa-link"></i>', action: () => copyShareLink(fileKey) },
|
|
1065
|
+
{ id: 'star', label: isStarred ? 'Unstar' : 'Star', icon: '<i class="fas fa-star"></i>', action: () => toggleStar(fileKey) },
|
|
1066
|
+
'-',
|
|
1067
|
+
{ id: 'delete', label: 'Delete', icon: '<i class="fas fa-trash"></i>', danger: true, action: () => promptDelete(fileKey, false) }
|
|
1068
|
+
]);
|
|
1069
|
+
} else if (folderPath) {
|
|
1070
|
+
openContextMenu(rect.left, rect.bottom + 4, [
|
|
1071
|
+
{ id: 'open', label: 'Open Folder', icon: '<i class="fas fa-folder-open"></i>', action: () => loadFiles(1, folderPath) },
|
|
1072
|
+
'-',
|
|
1073
|
+
{ id: 'delete', label: 'Delete Folder', icon: '<i class="fas fa-trash"></i>', danger: true, action: () => promptDelete(folderPath + '/', true) }
|
|
1074
|
+
]);
|
|
1075
|
+
}
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// Folder click to navigate
|
|
1080
|
+
const folderItem = e.target.closest('[data-type="folder"]');
|
|
1081
|
+
if (folderItem) {
|
|
1082
|
+
e.stopPropagation();
|
|
1083
|
+
loadFiles(1, folderItem.dataset.folderPath);
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// File click to preview
|
|
1088
|
+
const fileItem = e.target.closest('[data-type="file"]');
|
|
1089
|
+
if (fileItem && !e.target.closest('.file-select-cb')) {
|
|
1090
|
+
e.stopPropagation();
|
|
1091
|
+
openPreview(fileItem.dataset.key);
|
|
1092
|
+
}
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
// Bucket Selector
|
|
1096
|
+
document.getElementById('bucketSelector')?.addEventListener('change', (e) => {
|
|
1097
|
+
const newBucket = e.target.value;
|
|
1098
|
+
if (!newBucket) return;
|
|
1099
|
+
activeBucket = newBucket;
|
|
1100
|
+
loadFiles(1, '');
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
// Top action buttons & Modals
|
|
1104
|
+
document.querySelectorAll('[data-action="upload-files"]').forEach(btn => {
|
|
1105
|
+
btn.addEventListener('click', openUploadModal);
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
document.querySelectorAll('[data-action="create-folder"]').forEach(btn => {
|
|
1109
|
+
btn.addEventListener('click', openNewFolderModal);
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
// Drag and Drop Upload Zone
|
|
1113
|
+
const dropZone = document.getElementById('dropZone');
|
|
1114
|
+
const fileInput = document.getElementById('fileInput');
|
|
1115
|
+
|
|
1116
|
+
dropZone?.addEventListener('click', () => fileInput?.click());
|
|
1117
|
+
|
|
1118
|
+
dropZone?.addEventListener('dragover', (e) => {
|
|
1119
|
+
e.preventDefault();
|
|
1120
|
+
dropZone.classList.add('dragover');
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
dropZone?.addEventListener('dragleave', () => {
|
|
1124
|
+
dropZone.classList.remove('dragover');
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
dropZone?.addEventListener('drop', (e) => {
|
|
1128
|
+
e.preventDefault();
|
|
1129
|
+
dropZone.classList.remove('dragover');
|
|
1130
|
+
if (e.dataTransfer.files?.length) {
|
|
1131
|
+
uploadQueue.push(...Array.from(e.dataTransfer.files));
|
|
1132
|
+
updateUploadQueueUI();
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
fileInput?.addEventListener('change', (e) => {
|
|
1137
|
+
if (e.target.files?.length) {
|
|
1138
|
+
uploadQueue.push(...Array.from(e.target.files));
|
|
1139
|
+
updateUploadQueueUI();
|
|
1140
|
+
}
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
document.getElementById('uploadQueue')?.addEventListener('click', (e) => {
|
|
1144
|
+
const removeBtn = e.target.closest('[data-remove-queue]');
|
|
1145
|
+
if (removeBtn) {
|
|
1146
|
+
const idx = parseInt(removeBtn.dataset.removeQueue, 10);
|
|
1147
|
+
uploadQueue.splice(idx, 1);
|
|
1148
|
+
updateUploadQueueUI();
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
document.getElementById('clearQueueBtn')?.addEventListener('click', () => {
|
|
1153
|
+
uploadQueue = [];
|
|
1154
|
+
updateUploadQueueUI();
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
document.getElementById('startUploadBtn')?.addEventListener('click', processUploads);
|
|
1158
|
+
|
|
1159
|
+
// Fullscreen Preview Toggle
|
|
1160
|
+
document.getElementById('previewFullscreenBtn')?.addEventListener('click', () => {
|
|
1161
|
+
document.querySelector('.preview-modal-dialog')?.classList.toggle('is-fullscreen');
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
// Pagination clicks
|
|
1165
|
+
document.getElementById('paginationList')?.addEventListener('click', (e) => {
|
|
1166
|
+
const link = e.target.closest('[data-page]');
|
|
1167
|
+
if (!link) return;
|
|
1168
|
+
const page = parseInt(link.dataset.page, 10);
|
|
1169
|
+
if (!isNaN(page) && page > 0) {
|
|
1170
|
+
loadFiles(page, currentPrefix, currentSearch);
|
|
1171
|
+
}
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
// Global click to close context menu
|
|
1175
|
+
document.addEventListener('click', closeContextMenu);
|
|
1176
|
+
|
|
1177
|
+
// Handle delete form submission
|
|
1178
|
+
const deleteForm = document.getElementById('deleteForm');
|
|
1179
|
+
deleteForm?.addEventListener('submit', async (e) => {
|
|
1180
|
+
e.preventDefault();
|
|
1181
|
+
const formData = new FormData(deleteForm);
|
|
1182
|
+
const key = formData.get('key');
|
|
1183
|
+
const isFolder = formData.get('folder') === 'true';
|
|
1184
|
+
|
|
1185
|
+
try {
|
|
1186
|
+
const response = await fetch(withBucketUrl('/mbkbucket/delete'), {
|
|
1187
|
+
method: 'POST',
|
|
1188
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1189
|
+
body: JSON.stringify({ key, isFolder, prefix: currentPrefix })
|
|
1190
|
+
});
|
|
1191
|
+
const data = await response.json();
|
|
1192
|
+
if (!data.success) throw new Error(data.error || 'Delete failed');
|
|
1193
|
+
|
|
1194
|
+
closeModal();
|
|
1195
|
+
showToast(`Successfully deleted ${key}`, 'success');
|
|
1196
|
+
loadFiles(currentPage, currentPrefix);
|
|
1197
|
+
} catch (err) {
|
|
1198
|
+
showToast(err.message, 'error');
|
|
1199
|
+
}
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// Initialization
|
|
1204
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
1205
|
+
// Sync view mode buttons
|
|
1206
|
+
if (currentViewMode === 'list') {
|
|
1207
|
+
document.getElementById('view-list')?.classList.add('active');
|
|
1208
|
+
document.getElementById('view-grid')?.classList.remove('active');
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
initEventListeners();
|
|
1212
|
+
|
|
1213
|
+
// Initial load with URL params
|
|
1214
|
+
const initialFolder = urlParams.get('folder') || '';
|
|
1215
|
+
const initialPage = parseInt(urlParams.get('page') || '1', 10);
|
|
1216
|
+
const initialSearch = urlParams.get('search') || '';
|
|
1217
|
+
|
|
1218
|
+
loadFiles(initialPage, initialFolder, initialSearch);
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
// Expose global methods if needed
|
|
1222
|
+
window.mbkbucket = {
|
|
1223
|
+
loadFiles,
|
|
1224
|
+
openPreview,
|
|
1225
|
+
downloadFile,
|
|
1226
|
+
showToast
|
|
1227
|
+
};
|
|
1228
|
+
|
|
1229
|
+
})();
|