mbkbucket 1.1.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +9 -0
- package/lib/bucket.js +13 -2
- package/package.json +1 -1
- package/public/css/bucketadmin.css +1413 -0
- package/public/js/bucketadmincontent.js +1400 -0
- package/views/bucketadmincontent.handlebars +13 -2675
- package/views/info.handlebars +1056 -0
|
@@ -0,0 +1,1400 @@
|
|
|
1
|
+
// Global state
|
|
2
|
+
let currentPage = 1;
|
|
3
|
+
let currentPrefix = '';
|
|
4
|
+
let currentSearch = '';
|
|
5
|
+
let currentSort = 'name';
|
|
6
|
+
let currentOrder = 'asc';
|
|
7
|
+
let currentViewMode = 'folder'; // 'folder' or 'flat'
|
|
8
|
+
let pageFiles = [];
|
|
9
|
+
let pageFolders = [];
|
|
10
|
+
const bucketAppName = document.getElementById('bucketAppData')?.dataset.appName || '';
|
|
11
|
+
// Bucket is configured centrally on the server; client should not select a bucket.
|
|
12
|
+
|
|
13
|
+
// Show alert message
|
|
14
|
+
function showAlert(message, type = 'success') {
|
|
15
|
+
const alertId = type === 'success' ? 'successAlert' : 'errorAlert';
|
|
16
|
+
const messageId = type === 'success' ? 'successMessage' : 'errorMessage';
|
|
17
|
+
|
|
18
|
+
const alertEl = document.getElementById(alertId);
|
|
19
|
+
const messageEl = document.getElementById(messageId);
|
|
20
|
+
|
|
21
|
+
if (alertEl && messageEl) {
|
|
22
|
+
messageEl.textContent = message;
|
|
23
|
+
alertEl.style.display = 'flex';
|
|
24
|
+
|
|
25
|
+
// Auto dismiss after 5 seconds
|
|
26
|
+
setTimeout(() => {
|
|
27
|
+
alertEl.style.opacity = '0';
|
|
28
|
+
alertEl.style.transform = 'translateY(-10px)';
|
|
29
|
+
setTimeout(() => {
|
|
30
|
+
alertEl.style.display = 'none';
|
|
31
|
+
alertEl.style.opacity = '1';
|
|
32
|
+
alertEl.style.transform = 'translateY(0)';
|
|
33
|
+
}, 300);
|
|
34
|
+
}, 5000);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Fetch and render files
|
|
39
|
+
async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort, order = currentOrder) {
|
|
40
|
+
currentPage = page;
|
|
41
|
+
currentPrefix = prefix;
|
|
42
|
+
currentSearch = search;
|
|
43
|
+
currentSort = sort;
|
|
44
|
+
currentOrder = order;
|
|
45
|
+
|
|
46
|
+
// Update browser URL so navigation is shareable/bookmarkable (folder shown as readable path)
|
|
47
|
+
try {
|
|
48
|
+
const parts = [];
|
|
49
|
+
if (prefix) {
|
|
50
|
+
// preserve slashes in folder param for readability: encode then restore slashes
|
|
51
|
+
const folderEncoded = encodeURIComponent(prefix).replace(/%2F/g, '/');
|
|
52
|
+
parts.push(`folder=${folderEncoded}`);
|
|
53
|
+
}
|
|
54
|
+
if (page && page > 1) parts.push(`page=${page}`);
|
|
55
|
+
if (search) parts.push(`search=${encodeURIComponent(search)}`);
|
|
56
|
+
// Optional: Add sort/order to URL if desired, skipping for simplicity/clean URLs for now
|
|
57
|
+
|
|
58
|
+
const newUrl = '/mbkbucket' + (parts.length ? '?' + parts.join('&') : '');
|
|
59
|
+
window.history.replaceState(null, '', newUrl);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
// Ignore history errors in older browsers
|
|
62
|
+
console.warn('history update failed', e);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const container = document.getElementById('fileListContainer');
|
|
66
|
+
const paginationContainer = document.getElementById('paginationContainer');
|
|
67
|
+
const fileListTitle = document.getElementById('fileListTitle');
|
|
68
|
+
const totalFilesCount = document.getElementById('totalFilesCount');
|
|
69
|
+
const filterInfo = document.getElementById('filterInfo');
|
|
70
|
+
|
|
71
|
+
// Show loading skeleton state
|
|
72
|
+
container.innerHTML = `
|
|
73
|
+
<div class="table-responsive">
|
|
74
|
+
<div class="skeleton-row">
|
|
75
|
+
<div class="skeleton-box s-40"></div>
|
|
76
|
+
<div class="skeleton-box s-200"></div>
|
|
77
|
+
<div class="skeleton-box s-80"></div>
|
|
78
|
+
<div class="skeleton-box s-80"></div>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="skeleton-row">
|
|
81
|
+
<div class="skeleton-box s-40"></div>
|
|
82
|
+
<div class="skeleton-box s-200"></div>
|
|
83
|
+
<div class="skeleton-box s-80"></div>
|
|
84
|
+
<div class="skeleton-box s-80"></div>
|
|
85
|
+
</div>
|
|
86
|
+
<div class="skeleton-row">
|
|
87
|
+
<div class="skeleton-box s-40"></div>
|
|
88
|
+
<div class="skeleton-box s-200"></div>
|
|
89
|
+
<div class="skeleton-box s-80"></div>
|
|
90
|
+
<div class="skeleton-box s-80"></div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const searchParam = search ? `&search=${encodeURIComponent(search)}` : '';
|
|
97
|
+
const sortParam = `&sort=${sort}&order=${order}`;
|
|
98
|
+
const response = await fetch(`/mbkbucket/api/files?page=${page}&prefix=${encodeURIComponent(prefix)}${searchParam}${sortParam}`);
|
|
99
|
+
const data = await response.json();
|
|
100
|
+
|
|
101
|
+
if (!data.success) {
|
|
102
|
+
throw new Error(data.error || 'Failed to load files');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Update stats
|
|
106
|
+
totalFilesCount.textContent = `${data.totalFiles} ${search ? 'Results' : 'Files'}`;
|
|
107
|
+
|
|
108
|
+
if (search) {
|
|
109
|
+
filterInfo.textContent = `Search results for: "${search}"`;
|
|
110
|
+
fileListTitle.innerHTML = `Search Results <span class="text-muted">for: </span><span class="badge bg-info">${search}</span>`;
|
|
111
|
+
} else {
|
|
112
|
+
const label = prefix || bucketAppName || 'Root';
|
|
113
|
+
filterInfo.textContent = `Filtered by folder: ${label}`;
|
|
114
|
+
fileListTitle.innerHTML = `Files <span class="text-muted">in: </span><span class="badge bg-info">${label}</span>`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Render files or empty state
|
|
118
|
+
if (data.files.length === 0 && !prefix) {
|
|
119
|
+
container.innerHTML = `
|
|
120
|
+
<div class="empty-state">
|
|
121
|
+
<i class="fas fa-folder-open"></i>
|
|
122
|
+
<h4>No files found</h4>
|
|
123
|
+
<p>Your bucket is empty. Upload some files to get started!</p>
|
|
124
|
+
<button class="btn btn-lg" onclick="document.getElementById('fileInput').click()">
|
|
125
|
+
<i class="fas fa-plus"></i> Upload Your First File
|
|
126
|
+
</button>
|
|
127
|
+
</div>
|
|
128
|
+
`;
|
|
129
|
+
paginationContainer.style.display = 'none';
|
|
130
|
+
} else {
|
|
131
|
+
// Store for client-side sorting and view switching
|
|
132
|
+
pageFiles = data.files;
|
|
133
|
+
|
|
134
|
+
// Only calculate folders if we are in search or folder mode (for potential future switching)
|
|
135
|
+
// But mainly we rely on currentViewMode in the render function
|
|
136
|
+
pageFolders = search ? [] : extractFoldersAtCurrentLevel(data.files, prefix);
|
|
137
|
+
|
|
138
|
+
renderFilesTable(pageFiles, pageFolders, prefix);
|
|
139
|
+
renderPagination(data);
|
|
140
|
+
paginationContainer.style.display = 'block';
|
|
141
|
+
}
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error('Error loading files:', error);
|
|
144
|
+
container.innerHTML = `
|
|
145
|
+
<div class="empty-state">
|
|
146
|
+
<i class="fas fa-exclamation-triangle" style="color: var(--danger-color-ml);"></i>
|
|
147
|
+
<h4>Error Loading Files</h4>
|
|
148
|
+
<p>${error.message}</p>
|
|
149
|
+
<button class="btn btn-primary" onclick="loadFiles(${page}, '${prefix}')">
|
|
150
|
+
<i class="fas fa-redo"></i> Retry
|
|
151
|
+
</button>
|
|
152
|
+
</div>
|
|
153
|
+
`;
|
|
154
|
+
paginationContainer.style.display = 'none';
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Toggle sort order
|
|
159
|
+
function toggleSort(field) {
|
|
160
|
+
if (currentSort === field) {
|
|
161
|
+
currentOrder = currentOrder === 'asc' ? 'desc' : 'asc';
|
|
162
|
+
} else {
|
|
163
|
+
currentSort = field;
|
|
164
|
+
currentOrder = 'asc';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Sort local data instead of calling API
|
|
168
|
+
if (pageFiles && pageFiles.length > 0) {
|
|
169
|
+
pageFiles.sort((a, b) => {
|
|
170
|
+
let valA, valB;
|
|
171
|
+
if (field === 'size') {
|
|
172
|
+
valA = a.Size || 0;
|
|
173
|
+
valB = b.Size || 0;
|
|
174
|
+
} else if (field === 'date') {
|
|
175
|
+
valA = new Date(a.LastModified || 0).getTime();
|
|
176
|
+
valB = new Date(b.LastModified || 0).getTime();
|
|
177
|
+
} else {
|
|
178
|
+
valA = (a.Key || '').split('/').pop().toLowerCase();
|
|
179
|
+
valB = (b.Key || '').split('/').pop().toLowerCase();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (valA < valB) return currentOrder === 'asc' ? -1 : 1;
|
|
183
|
+
if (valA > valB) return currentOrder === 'asc' ? 1 : -1;
|
|
184
|
+
return 0;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
renderFilesTable(pageFiles, pageFolders, currentPrefix);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Perform search
|
|
192
|
+
function performSearch() {
|
|
193
|
+
const searchInput = document.getElementById('searchInput');
|
|
194
|
+
const search = searchInput.value.trim();
|
|
195
|
+
if (search) {
|
|
196
|
+
loadFiles(1, currentPrefix, search);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Clear search
|
|
201
|
+
function clearSearch() {
|
|
202
|
+
document.getElementById('searchInput').value = '';
|
|
203
|
+
loadFiles(1, currentPrefix, '');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Extract unique folders at current level
|
|
207
|
+
function extractFoldersAtCurrentLevel(files, prefix) {
|
|
208
|
+
const folderSet = new Set();
|
|
209
|
+
const prefixDepth = prefix ? prefix.split('/').filter(p => p).length : 0;
|
|
210
|
+
|
|
211
|
+
files.forEach(file => {
|
|
212
|
+
const key = file.Key;
|
|
213
|
+
// Remove prefix from key
|
|
214
|
+
const relativePath = prefix ? key.substring(prefix.length + (prefix.endsWith('/') ? 0 : 1)) : key;
|
|
215
|
+
|
|
216
|
+
// Check if there's a folder in the relative path
|
|
217
|
+
const slashIndex = relativePath.indexOf('/');
|
|
218
|
+
if (slashIndex !== -1) {
|
|
219
|
+
const folderName = relativePath.substring(0, slashIndex);
|
|
220
|
+
const fullFolderPath = prefix ? `${prefix}/${folderName}` : folderName;
|
|
221
|
+
folderSet.add(fullFolderPath);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
return Array.from(folderSet).sort();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Change view mode
|
|
229
|
+
function changeViewMode(mode) {
|
|
230
|
+
if (currentViewMode === mode) return;
|
|
231
|
+
currentViewMode = mode;
|
|
232
|
+
|
|
233
|
+
// Update button states
|
|
234
|
+
const btnFolder = document.getElementById('viewBtnFolder');
|
|
235
|
+
const btnFlat = document.getElementById('viewBtnFlat');
|
|
236
|
+
|
|
237
|
+
if (mode === 'folder') {
|
|
238
|
+
btnFolder.classList.add('active');
|
|
239
|
+
btnFolder.classList.remove('btn-outline-light');
|
|
240
|
+
btnFolder.classList.add('btn-light');
|
|
241
|
+
|
|
242
|
+
btnFlat.classList.remove('active');
|
|
243
|
+
btnFlat.classList.remove('btn-light');
|
|
244
|
+
btnFlat.classList.add('btn-outline-light');
|
|
245
|
+
} else {
|
|
246
|
+
btnFlat.classList.add('active');
|
|
247
|
+
btnFlat.classList.remove('btn-outline-light');
|
|
248
|
+
btnFlat.classList.add('btn-light');
|
|
249
|
+
|
|
250
|
+
btnFolder.classList.remove('active');
|
|
251
|
+
btnFolder.classList.remove('btn-light');
|
|
252
|
+
btnFolder.classList.add('btn-outline-light');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Re-render with existing data
|
|
256
|
+
renderFilesTable(pageFiles, pageFolders, currentPrefix);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Render files table
|
|
260
|
+
function renderFilesTable(files, folders = [], prefix = '') {
|
|
261
|
+
// Determine which files/folders to show based on view mode
|
|
262
|
+
let displayFiles = [];
|
|
263
|
+
let displayFolders = [];
|
|
264
|
+
|
|
265
|
+
if (currentViewMode === 'flat') {
|
|
266
|
+
// In flat mode, show all files in the current loaded page/list regardless of folder depth
|
|
267
|
+
// And ignore folder objects (we just show file paths)
|
|
268
|
+
displayFiles = files;
|
|
269
|
+
// We don't show expandable folders in flat view, unless we want to keep them?
|
|
270
|
+
// User asked for "all files displayed without folder". So empty folders.
|
|
271
|
+
displayFolders = [];
|
|
272
|
+
} else {
|
|
273
|
+
// Normal Folder View logic
|
|
274
|
+
// Filter files to show only those at current level (not in subfolders)
|
|
275
|
+
displayFiles = files.filter(file => {
|
|
276
|
+
const relativePath = prefix ? file.Key.substring(prefix.length + (prefix.endsWith('/') ? 0 : 1)) : file.Key;
|
|
277
|
+
// File is at current level if it has no '/' in relative path
|
|
278
|
+
return !relativePath.includes('/');
|
|
279
|
+
});
|
|
280
|
+
displayFolders = folders;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Generate breadcrumb navigation
|
|
284
|
+
let breadcrumb = '';
|
|
285
|
+
// Show breadcrumb only if we are in folder mode OR if we have a prefix filter active even in flat mode
|
|
286
|
+
// (In flat mode, the prefix acts as a base filter, but we see recursive files under it)
|
|
287
|
+
if (prefix) {
|
|
288
|
+
const parts = prefix.split('/').filter(p => p);
|
|
289
|
+
let breadcrumbHTML = `
|
|
290
|
+
<div class="breadcrumb-nav">
|
|
291
|
+
<div class="breadcrumb-item">
|
|
292
|
+
<i class="fas fa-home"></i>
|
|
293
|
+
<span class="breadcrumb-link" onclick="loadFiles(1, '')" title="Go to root">Root</span>
|
|
294
|
+
</div>
|
|
295
|
+
`;
|
|
296
|
+
|
|
297
|
+
let currentPath = '';
|
|
298
|
+
parts.forEach((part, index) => {
|
|
299
|
+
currentPath += (currentPath ? '/' : '') + part;
|
|
300
|
+
const isLast = index === parts.length - 1;
|
|
301
|
+
|
|
302
|
+
if (isLast) {
|
|
303
|
+
breadcrumbHTML += `
|
|
304
|
+
<span class="breadcrumb-separator"><i class="fas fa-chevron-right"></i></span>
|
|
305
|
+
<div class="breadcrumb-item">
|
|
306
|
+
<span class="breadcrumb-current">${part}</span>
|
|
307
|
+
</div>
|
|
308
|
+
`;
|
|
309
|
+
} else {
|
|
310
|
+
const pathCopy = currentPath;
|
|
311
|
+
breadcrumbHTML += `
|
|
312
|
+
<span class="breadcrumb-separator"><i class="fas fa-chevron-right"></i></span>
|
|
313
|
+
<div class="breadcrumb-item">
|
|
314
|
+
<span class="breadcrumb-link" onclick="loadFiles(1, '${pathCopy}')" title="Go to ${pathCopy}">${part}</span>
|
|
315
|
+
</div>
|
|
316
|
+
`;
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
breadcrumbHTML += `</div>`;
|
|
321
|
+
breadcrumb = breadcrumbHTML;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Generate folder rows (only used in folder mode)
|
|
325
|
+
const folderRows = displayFolders.map(folderPath => {
|
|
326
|
+
const folderName = folderPath.split('/').pop();
|
|
327
|
+
return `
|
|
328
|
+
<tr class="folder-row" onclick="loadFiles(1, '${folderPath}')" title="Open folder: ${folderName}">
|
|
329
|
+
<td style="text-align:center;">
|
|
330
|
+
<!-- no selection for folders -->
|
|
331
|
+
</td>
|
|
332
|
+
<td style="padding-left: 1.5rem;">
|
|
333
|
+
<div style="display: flex; align-items: center;">
|
|
334
|
+
<div class="file-icon">
|
|
335
|
+
<i class="fas fa-folder"></i>
|
|
336
|
+
</div>
|
|
337
|
+
<div class="file-info">
|
|
338
|
+
<div class="file-name" style="font-weight: 700;">
|
|
339
|
+
<i class="fas fa-folder" style="margin-right: 0.5rem; color: #f59e0b;"></i>${folderName}/
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
</td>
|
|
344
|
+
<td>
|
|
345
|
+
<span style="color: var(--gray-color-ml);">\u2014</span>
|
|
346
|
+
</td>
|
|
347
|
+
<td style="text-align: center;">
|
|
348
|
+
<span style="color: var(--gray-color-ml);">\u2014</span>
|
|
349
|
+
</td>
|
|
350
|
+
<td style="text-align: center;">
|
|
351
|
+
<div class="btn-group" role="group">
|
|
352
|
+
<button type="button" class="btn btn-danger btn-sm delete-btn" data-key="${folderPath}/" data-is-folder="true" title="Delete folder">
|
|
353
|
+
<i class="fas fa-trash"></i>
|
|
354
|
+
</button>
|
|
355
|
+
</div>
|
|
356
|
+
</td>
|
|
357
|
+
</tr>
|
|
358
|
+
`;
|
|
359
|
+
}).join('');
|
|
360
|
+
|
|
361
|
+
// Generate file rows
|
|
362
|
+
const fileRows = displayFiles.map(file => {
|
|
363
|
+
const encodedKey = encodeURIComponent(file.Key);
|
|
364
|
+
|
|
365
|
+
// Calculate display name and icon directly
|
|
366
|
+
const fullPath = file.Key;
|
|
367
|
+
const lastSlashIndex = fullPath.lastIndexOf('/');
|
|
368
|
+
let filename = fullPath;
|
|
369
|
+
let folderPath = '';
|
|
370
|
+
|
|
371
|
+
if (lastSlashIndex !== -1) {
|
|
372
|
+
filename = fullPath.substring(lastSlashIndex + 1);
|
|
373
|
+
folderPath = fullPath.substring(0, lastSlashIndex + 1); // include trailing slash
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const iconClass = getFileIcon(filename);
|
|
377
|
+
const typeCategory = getFileTypeCategory(filename);
|
|
378
|
+
const truncatedName = truncateFileName(filename);
|
|
379
|
+
const isFileViewable = isViewable(filename);
|
|
380
|
+
|
|
381
|
+
// If in flat mode, we might want to show the folder path more prominently
|
|
382
|
+
const pathDisplay = (currentViewMode === 'flat' && folderPath && folderPath !== prefix + (prefix ? '/' : ''))
|
|
383
|
+
? `<div style="font-size: 0.75rem; color: var(--gray-color-ml); margin-bottom:2px;">
|
|
384
|
+
<i class="fas fa-folder" style="margin-right:4px; opacity:0.5;"></i> ${folderPath}
|
|
385
|
+
</div>`
|
|
386
|
+
: '';
|
|
387
|
+
|
|
388
|
+
return `
|
|
389
|
+
<tr class="file-row">
|
|
390
|
+
<td style="text-align:center;">
|
|
391
|
+
<input type="checkbox" class="selectFileCheckbox" data-key="${file.Key}" aria-label="Select ${file.Key}" />
|
|
392
|
+
</td>
|
|
393
|
+
<td style="padding-left: 1.5rem;">
|
|
394
|
+
<div style="display: flex; align-items: center;">
|
|
395
|
+
<div class="file-icon ${typeCategory}">
|
|
396
|
+
<i class="${iconClass}"></i>
|
|
397
|
+
</div>
|
|
398
|
+
<div class="file-info">
|
|
399
|
+
${pathDisplay}
|
|
400
|
+
<div class="file-name" data-full-path="${file.Key}" title="${filename}">${truncatedName}</div>
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
</td>
|
|
404
|
+
<td>
|
|
405
|
+
${file.Size ? `<span class="file-size">${formatFileSize(file.Size)}</span>` : '<span style="color: var(--gray-color-ml);">N/A</span>'}
|
|
406
|
+
</td>
|
|
407
|
+
<td>
|
|
408
|
+
${file.LastModified ? `<span style="color: var(--gray-color-ml);">${formatDate(file.LastModified)}</span>` : '<span style="color: var(--gray-color-ml);">N/A</span>'}
|
|
409
|
+
</td>
|
|
410
|
+
<td style="text-align: center;">
|
|
411
|
+
<div class="btn-group" role="group">
|
|
412
|
+
<button type="button" class="btn btn-primary btn-sm view-btn" data-key="${file.Key}" data-filename="${file.Key}"
|
|
413
|
+
data-key-enc="${encodedKey}" title="${isFileViewable ? 'View file in browser' : 'File type not supported for viewing'}" ${!isFileViewable ? 'disabled style="opacity:0.5"' : ''}>
|
|
414
|
+
<i class="fas fa-eye"></i>
|
|
415
|
+
</button>
|
|
416
|
+
<a href="/mbkbucket/download/${encodedKey}" class="btn btn-success btn-sm"
|
|
417
|
+
title="Download file">
|
|
418
|
+
<i class="fas fa-download"></i>
|
|
419
|
+
</a>
|
|
420
|
+
<button type="button" class="btn btn-danger btn-sm delete-btn" data-key="${file.Key}" title="Delete file">
|
|
421
|
+
<i class="fas fa-trash"></i>
|
|
422
|
+
</button>
|
|
423
|
+
</div>
|
|
424
|
+
</td>
|
|
425
|
+
</tr>
|
|
426
|
+
`;
|
|
427
|
+
}).join('');
|
|
428
|
+
|
|
429
|
+
const allRows = folderRows + fileRows;
|
|
430
|
+
|
|
431
|
+
if (allRows === '') {
|
|
432
|
+
document.getElementById('fileListContainer').innerHTML = `
|
|
433
|
+
<div class="empty-state">
|
|
434
|
+
<i class="fas fa-folder-open"></i>
|
|
435
|
+
<h4>No files or folders</h4>
|
|
436
|
+
<p>${prefix ? `No files in folder: ${prefix}` : 'This folder is empty'}</p>
|
|
437
|
+
<div style="display:flex; gap:0.5rem; justify-content:center; margin-top:1rem;">
|
|
438
|
+
<button class="btn btn-primary" onclick="document.getElementById('fileInput').click()">
|
|
439
|
+
<i class="fas fa-cloud-upload-alt"></i> Upload File
|
|
440
|
+
</button>
|
|
441
|
+
<button class="btn btn-outline-secondary" onclick="createFolder()">
|
|
442
|
+
<i class="fas fa-folder-plus"></i> Create Folder
|
|
443
|
+
</button>
|
|
444
|
+
${prefix ? `<button class="btn btn-light" onclick="loadFiles(1, '')">
|
|
445
|
+
<i class="fas fa-arrow-left"></i> Back to Root
|
|
446
|
+
</button>` : ''}
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
`;
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Sort icon helper
|
|
454
|
+
const getSortIcon = (field) => {
|
|
455
|
+
if (currentSort !== field) return '<i class="fas fa-sort" style="color:var(--gray-color-ml); opacity:0.3; font-size:0.8rem; margin-left:0.5rem;"></i>';
|
|
456
|
+
return currentOrder === 'asc'
|
|
457
|
+
? '<i class="fas fa-sort-up" style="color:var(--primary-color-ml); font-size:0.9rem; margin-left:0.5rem;"></i>'
|
|
458
|
+
: '<i class="fas fa-sort-down" style="color:var(--primary-color-ml); font-size:0.9rem; margin-left:0.5rem;"></i>';
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
document.getElementById('fileListContainer').innerHTML = `
|
|
462
|
+
${breadcrumb}
|
|
463
|
+
<div class="table-responsive">
|
|
464
|
+
<div id="bulkActionsBar" style="display:none; padding: 0.75rem; border-bottom: 1px solid var(--gray-light-color-ml); background: var(--light-color-ml);">
|
|
465
|
+
<span id="bulkSelectedCount">0 selected</span>
|
|
466
|
+
<div style="float: right;">
|
|
467
|
+
<button class="btn btn-danger btn-sm" id="bulkDeleteBtn" disabled>
|
|
468
|
+
<i class="fas fa-trash"></i> Delete Selected
|
|
469
|
+
</button>
|
|
470
|
+
</div>
|
|
471
|
+
</div>
|
|
472
|
+
<table class="table table-hover mb-0">
|
|
473
|
+
<thead>
|
|
474
|
+
<tr>
|
|
475
|
+
<th style="width:48px; text-align:center;">
|
|
476
|
+
<input type="checkbox" id="selectAllCheckbox" aria-label="Select all files" />
|
|
477
|
+
</th>
|
|
478
|
+
<th style="padding-left: 1.5rem; cursor: pointer;" onclick="toggleSort('name')">
|
|
479
|
+
<i class="fas fa-file-alt"></i> Name ${getSortIcon('name')}
|
|
480
|
+
</th>
|
|
481
|
+
<th style="cursor: pointer;" onclick="toggleSort('size')">
|
|
482
|
+
<i class="fas fa-weight"></i> Size ${getSortIcon('size')}
|
|
483
|
+
</th>
|
|
484
|
+
<th style="cursor: pointer;" onclick="toggleSort('date')">
|
|
485
|
+
<i class="fas fa-calendar"></i> Last Modified ${getSortIcon('date')}
|
|
486
|
+
</th>
|
|
487
|
+
<th style="text-align: center;">
|
|
488
|
+
<i class="fas fa-cogs"></i> Actions
|
|
489
|
+
</th>
|
|
490
|
+
</tr>
|
|
491
|
+
</thead>
|
|
492
|
+
<tbody>
|
|
493
|
+
${allRows}
|
|
494
|
+
</tbody>
|
|
495
|
+
</table>
|
|
496
|
+
</div>
|
|
497
|
+
`;
|
|
498
|
+
|
|
499
|
+
// Wire selection checkboxes and bulk actions
|
|
500
|
+
setupSelection();
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Render pagination
|
|
504
|
+
function renderPagination(data) {
|
|
505
|
+
const { currentPage, totalPages, hasPrevPage, hasNextPage } = data;
|
|
506
|
+
|
|
507
|
+
let paginationHTML = '';
|
|
508
|
+
|
|
509
|
+
// Previous button
|
|
510
|
+
if (hasPrevPage) {
|
|
511
|
+
paginationHTML += `
|
|
512
|
+
<li class="page-item">
|
|
513
|
+
<a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${currentPage - 1}, '${currentPrefix}', '${currentSearch}')">
|
|
514
|
+
<i class="fas fa-chevron-left"></i> Previous
|
|
515
|
+
</a>
|
|
516
|
+
</li>
|
|
517
|
+
`;
|
|
518
|
+
} else {
|
|
519
|
+
paginationHTML += `
|
|
520
|
+
<li class="page-item disabled">
|
|
521
|
+
<span class="page-link">
|
|
522
|
+
<i class="fas fa-chevron-left"></i> Previous
|
|
523
|
+
</span>
|
|
524
|
+
</li>
|
|
525
|
+
`;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Page numbers
|
|
529
|
+
for (let i = 1; i <= totalPages; i++) {
|
|
530
|
+
if (i === currentPage) {
|
|
531
|
+
paginationHTML += `
|
|
532
|
+
<li class="page-item active">
|
|
533
|
+
<span class="page-link">${i}</span>
|
|
534
|
+
</li>
|
|
535
|
+
`;
|
|
536
|
+
} else {
|
|
537
|
+
paginationHTML += `
|
|
538
|
+
<li class="page-item">
|
|
539
|
+
<a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${i}, '${currentPrefix}', '${currentSearch}')">${i}</a>
|
|
540
|
+
</li>
|
|
541
|
+
`;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Next button
|
|
546
|
+
if (hasNextPage) {
|
|
547
|
+
paginationHTML += `
|
|
548
|
+
<li class="page-item">
|
|
549
|
+
<a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${currentPage + 1}, '${currentPrefix}', '${currentSearch}')">
|
|
550
|
+
Next <i class="fas fa-chevron-right"></i>
|
|
551
|
+
</a>
|
|
552
|
+
</li>
|
|
553
|
+
`;
|
|
554
|
+
} else {
|
|
555
|
+
paginationHTML += `
|
|
556
|
+
<li class="page-item disabled">
|
|
557
|
+
<span class="page-link">
|
|
558
|
+
Next <i class="fas fa-chevron-right"></i>
|
|
559
|
+
</span>
|
|
560
|
+
</li>
|
|
561
|
+
`;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
document.getElementById('paginationList').innerHTML = paginationHTML;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function confirmDelete(key, isFolder = false) {
|
|
568
|
+
document.getElementById('deleteKey').value = key;
|
|
569
|
+
document.getElementById('deleteIsFolder').value = isFolder ? 'true' : '';
|
|
570
|
+
document.getElementById('deleteFileName').textContent = key;
|
|
571
|
+
|
|
572
|
+
// Update modal message and button label for folders
|
|
573
|
+
const modalHeading = document.querySelector('#deleteModal .modal-body h6');
|
|
574
|
+
const smallText = document.querySelector('#deleteModal .modal-body small');
|
|
575
|
+
const confirmLabel = document.getElementById('deleteConfirmLabel');
|
|
576
|
+
|
|
577
|
+
if (isFolder) {
|
|
578
|
+
if (modalHeading) modalHeading.textContent = 'Are you sure you want to delete this folder and all its contents?';
|
|
579
|
+
if (smallText) smallText.textContent = 'This action will remove the folder and all files within it and cannot be undone.';
|
|
580
|
+
if (confirmLabel) confirmLabel.textContent = 'Delete Folder';
|
|
581
|
+
} else {
|
|
582
|
+
if (modalHeading) modalHeading.textContent = 'Are you sure you want to delete this file?';
|
|
583
|
+
if (smallText) smallText.textContent = 'This action cannot be undone.';
|
|
584
|
+
if (confirmLabel) confirmLabel.textContent = 'Delete File';
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
document.getElementById('deleteModal').classList.add('show');
|
|
588
|
+
document.getElementById('deleteModal').style.display = 'flex';
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function closeModal() {
|
|
592
|
+
document.getElementById('deleteModal').classList.remove('show');
|
|
593
|
+
document.getElementById('deleteModal').style.display = 'none';
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Handle delete form submission
|
|
597
|
+
document.getElementById('deleteForm')?.addEventListener('submit', async function (e) {
|
|
598
|
+
e.preventDefault();
|
|
599
|
+
|
|
600
|
+
const key = document.getElementById('deleteKey').value;
|
|
601
|
+
const isFolder = document.getElementById('deleteIsFolder').value === 'true';
|
|
602
|
+
|
|
603
|
+
try {
|
|
604
|
+
const response = await fetch('/mbkbucket/delete', {
|
|
605
|
+
method: 'POST',
|
|
606
|
+
headers: {
|
|
607
|
+
'Content-Type': 'application/json'
|
|
608
|
+
},
|
|
609
|
+
body: JSON.stringify({ key, folder: isFolder })
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
const data = await response.json();
|
|
613
|
+
|
|
614
|
+
if (data.success) {
|
|
615
|
+
showAlert('File deleted successfully', 'success');
|
|
616
|
+
closeModal();
|
|
617
|
+
// Reload file list
|
|
618
|
+
loadFiles(currentPage, currentPrefix);
|
|
619
|
+
} else {
|
|
620
|
+
showAlert(data.error || 'Delete failed', 'error');
|
|
621
|
+
closeModal();
|
|
622
|
+
}
|
|
623
|
+
} catch (error) {
|
|
624
|
+
console.error('Error deleting file:', error);
|
|
625
|
+
showAlert('Error deleting file: ' + error.message, 'error');
|
|
626
|
+
closeModal();
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// Auto-dismiss alerts with fade effect
|
|
631
|
+
setTimeout(() => {
|
|
632
|
+
const alerts = document.querySelectorAll('.alert');
|
|
633
|
+
alerts.forEach(alert => {
|
|
634
|
+
alert.style.opacity = '0';
|
|
635
|
+
alert.style.transform = 'translateY(-10px)';
|
|
636
|
+
setTimeout(() => alert.remove(), 300);
|
|
637
|
+
});
|
|
638
|
+
}, 5000);
|
|
639
|
+
|
|
640
|
+
// Upload queue, drag-and-drop and multi-file support
|
|
641
|
+
const uploadQueue = []; // { id, file, status, progress, controller/xhr, uploadId }
|
|
642
|
+
const xhrMap = new Map();
|
|
643
|
+
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB chunk threshold
|
|
644
|
+
|
|
645
|
+
function createId() { return Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 9); }
|
|
646
|
+
|
|
647
|
+
function addFilesToQueue(fileList) {
|
|
648
|
+
for (const f of Array.from(fileList)) {
|
|
649
|
+
uploadQueue.push({ id: createId(), file: f, status: 'queued', progress: 0 });
|
|
650
|
+
}
|
|
651
|
+
renderUploadQueue();
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Render upload queue UI
|
|
655
|
+
function renderUploadQueue() {
|
|
656
|
+
const container = document.getElementById('uploadQueue');
|
|
657
|
+
if (!container) return;
|
|
658
|
+
if (uploadQueue.length === 0) {
|
|
659
|
+
container.innerHTML = `
|
|
660
|
+
<div class="form-text">No files selected.</div>
|
|
661
|
+
`;
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
container.innerHTML = uploadQueue.map(item => `
|
|
666
|
+
<div class="upload-item" data-id="${item.id}">
|
|
667
|
+
<div class="meta">
|
|
668
|
+
<div class="file-icon"><i class="fas fa-file"></i></div>
|
|
669
|
+
<div>
|
|
670
|
+
<div class="name">${item.file.name}</div>
|
|
671
|
+
<div class="form-text">${formatFileSize(item.file.size)}</div>
|
|
672
|
+
</div>
|
|
673
|
+
</div>
|
|
674
|
+
<div style="min-width:220px;">
|
|
675
|
+
<div class="progress-container" style="height: 8px; margin-bottom: 6px;">
|
|
676
|
+
<div class="progress-bar" style="width: ${Math.round(item.progress)}%; height: 8px;"></div>
|
|
677
|
+
</div>
|
|
678
|
+
<div style="display:flex; justify-content:space-between; align-items:center; gap:0.5rem;">
|
|
679
|
+
<small class="text-muted status">${item.status}</small>
|
|
680
|
+
<div class="controls">
|
|
681
|
+
${item.status === 'uploading' ? `<button class="btn btn-sm btn-outline-secondary cancel-upload" data-id="${item.id}"><i class="fas fa-ban"></i></button>` : `<button class="btn btn-sm btn-danger remove-queued" data-id="${item.id}"><i class="fas fa-trash"></i></button>`}
|
|
682
|
+
</div>
|
|
683
|
+
</div>
|
|
684
|
+
</div>
|
|
685
|
+
</div>
|
|
686
|
+
`).join('');
|
|
687
|
+
|
|
688
|
+
// Wire up controls
|
|
689
|
+
document.querySelectorAll('.remove-queued').forEach(btn => btn.addEventListener('click', function (e) {
|
|
690
|
+
const id = this.dataset.id;
|
|
691
|
+
const idx = uploadQueue.findIndex(i => i.id === id);
|
|
692
|
+
if (idx !== -1) uploadQueue.splice(idx, 1);
|
|
693
|
+
renderUploadQueue();
|
|
694
|
+
}));
|
|
695
|
+
|
|
696
|
+
document.querySelectorAll('.cancel-upload').forEach(btn => btn.addEventListener('click', function (e) {
|
|
697
|
+
const id = this.dataset.id;
|
|
698
|
+
const handle = xhrMap.get(id);
|
|
699
|
+
if (handle && handle.abort) {
|
|
700
|
+
handle.abort();
|
|
701
|
+
} else if (handle && handle.xhr) {
|
|
702
|
+
handle.xhr.abort();
|
|
703
|
+
}
|
|
704
|
+
const entry = uploadQueue.find(i => i.id === id);
|
|
705
|
+
if (entry) { entry.status = 'cancelled'; entry.progress = 0; }
|
|
706
|
+
xhrMap.delete(id);
|
|
707
|
+
renderUploadQueue();
|
|
708
|
+
}));
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Drag & drop wiring
|
|
712
|
+
const dropZone = document.getElementById('dropZone');
|
|
713
|
+
const fileInputEl = document.getElementById('fileInput');
|
|
714
|
+
if (dropZone) {
|
|
715
|
+
dropZone.addEventListener('click', () => fileInputEl.click());
|
|
716
|
+
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('dragover'); });
|
|
717
|
+
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
|
|
718
|
+
dropZone.addEventListener('drop', (e) => {
|
|
719
|
+
e.preventDefault(); dropZone.classList.remove('dragover');
|
|
720
|
+
if (e.dataTransfer && e.dataTransfer.files) addFilesToQueue(e.dataTransfer.files);
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
fileInputEl?.addEventListener('change', function (e) {
|
|
725
|
+
if (e.target.files && e.target.files.length) {
|
|
726
|
+
addFilesToQueue(e.target.files);
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// Start queued uploads (single or chunked) and provide cancelation controls
|
|
731
|
+
async function startUploadEntry(entry) {
|
|
732
|
+
if (!entry || entry.status === 'uploading' || entry.status === 'done') return;
|
|
733
|
+
entry.status = 'uploading';
|
|
734
|
+
renderUploadQueue();
|
|
735
|
+
|
|
736
|
+
const file = entry.file;
|
|
737
|
+
try {
|
|
738
|
+
if (file.size > CHUNK_SIZE) {
|
|
739
|
+
// chunked upload
|
|
740
|
+
const uploadId = createId();
|
|
741
|
+
entry.uploadId = uploadId;
|
|
742
|
+
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
|
|
743
|
+
let uploadedBytes = 0;
|
|
744
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
745
|
+
if (entry.status === 'cancelled') throw new Error('Upload cancelled');
|
|
746
|
+
const start = i * CHUNK_SIZE;
|
|
747
|
+
const end = Math.min(file.size, start + CHUNK_SIZE);
|
|
748
|
+
const blob = file.slice(start, end);
|
|
749
|
+
|
|
750
|
+
const fd = new FormData();
|
|
751
|
+
fd.append('chunk', blob, file.name);
|
|
752
|
+
fd.append('uploadId', uploadId);
|
|
753
|
+
fd.append('fileName', file.name);
|
|
754
|
+
fd.append('chunkIndex', String(i));
|
|
755
|
+
fd.append('totalChunks', String(totalChunks));
|
|
756
|
+
|
|
757
|
+
const controller = new AbortController();
|
|
758
|
+
xhrMap.set(entry.id, { controller });
|
|
759
|
+
|
|
760
|
+
const resp = await fetch('/mbkbucket/upload-chunk', { method: 'POST', body: fd, signal: controller.signal });
|
|
761
|
+
if (!resp.ok) throw new Error('Chunk upload failed');
|
|
762
|
+
uploadedBytes += blob.size;
|
|
763
|
+
entry.progress = (uploadedBytes / file.size) * 95; // up to 95% until assembly
|
|
764
|
+
renderUploadQueue();
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// complete
|
|
768
|
+
const completeResp = await fetch('/mbkbucket/upload-complete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ uploadId: entry.uploadId, fileName: file.name, prefix: currentPrefix, contentType: file.type || 'application/octet-stream' }) });
|
|
769
|
+
const completeJson = await completeResp.json();
|
|
770
|
+
if (!completeResp.ok || !completeJson.success) throw new Error(completeJson.error || 'Assemble failed');
|
|
771
|
+
|
|
772
|
+
entry.status = 'done'; entry.progress = 100; xhrMap.delete(entry.id); renderUploadQueue();
|
|
773
|
+
showAlert('File uploaded successfully', 'success');
|
|
774
|
+
loadFiles(currentPage, currentPrefix);
|
|
775
|
+
} else {
|
|
776
|
+
// small file XHR
|
|
777
|
+
const fd = new FormData();
|
|
778
|
+
fd.append('file', file, file.name);
|
|
779
|
+
fd.append('prefix', currentPrefix);
|
|
780
|
+
|
|
781
|
+
const xhr = new XMLHttpRequest();
|
|
782
|
+
xhr.open('POST', '/mbkbucket/upload');
|
|
783
|
+
|
|
784
|
+
xhr.upload.addEventListener('progress', (e) => {
|
|
785
|
+
if (!e.lengthComputable) return;
|
|
786
|
+
entry.progress = (e.loaded / e.total) * 100;
|
|
787
|
+
renderUploadQueue();
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
xhr.addEventListener('load', () => {
|
|
791
|
+
try {
|
|
792
|
+
const resp = JSON.parse(xhr.responseText);
|
|
793
|
+
if (xhr.status >= 200 && xhr.status < 300 && resp.success) {
|
|
794
|
+
entry.status = 'done'; entry.progress = 100; renderUploadQueue();
|
|
795
|
+
showAlert('File uploaded successfully', 'success');
|
|
796
|
+
loadFiles(currentPage, currentPrefix);
|
|
797
|
+
} else {
|
|
798
|
+
entry.status = 'error'; renderUploadQueue();
|
|
799
|
+
showAlert(resp.error || 'Upload failed', 'error');
|
|
800
|
+
}
|
|
801
|
+
} catch (err) {
|
|
802
|
+
entry.status = 'error'; renderUploadQueue();
|
|
803
|
+
showAlert('Upload response parse error', 'error');
|
|
804
|
+
}
|
|
805
|
+
xhrMap.delete(entry.id);
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
xhr.addEventListener('error', () => { entry.status = 'error'; renderUploadQueue(); showAlert('Network error during upload', 'error'); xhrMap.delete(entry.id); });
|
|
809
|
+
xhr.addEventListener('abort', () => { entry.status = 'cancelled'; renderUploadQueue(); xhrMap.delete(entry.id); });
|
|
810
|
+
|
|
811
|
+
xhrMap.set(entry.id, { xhr });
|
|
812
|
+
xhr.send(fd);
|
|
813
|
+
}
|
|
814
|
+
} catch (err) {
|
|
815
|
+
console.error('Upload failed for', entry.file.name, err);
|
|
816
|
+
entry.status = entry.status === 'cancelled' ? 'cancelled' : 'error';
|
|
817
|
+
renderUploadQueue();
|
|
818
|
+
showAlert('Upload failed: ' + (err.message || 'unknown'), 'error');
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function startAllUploads() {
|
|
823
|
+
for (const entry of uploadQueue.slice()) {
|
|
824
|
+
if (entry.status === 'queued' || entry.status === 'error') startUploadEntry(entry);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
document.getElementById('startUploadBtn')?.addEventListener('click', startAllUploads);
|
|
829
|
+
document.getElementById('clearQueueBtn')?.addEventListener('click', function () { uploadQueue.length = 0; renderUploadQueue(); });
|
|
830
|
+
|
|
831
|
+
// Reset upload form
|
|
832
|
+
function resetUploadForm() {
|
|
833
|
+
const uploadBtn = document.getElementById('uploadBtn');
|
|
834
|
+
const uploadProgress = document.getElementById('uploadProgress');
|
|
835
|
+
|
|
836
|
+
setTimeout(() => {
|
|
837
|
+
uploadBtn.disabled = false;
|
|
838
|
+
uploadBtn.classList.remove('uploading');
|
|
839
|
+
uploadBtn.innerHTML = '<i class="fas fa-cloud-upload-alt"></i> Upload File';
|
|
840
|
+
uploadProgress.style.display = 'none';
|
|
841
|
+
|
|
842
|
+
// Reset progress
|
|
843
|
+
document.getElementById('progressBar').style.width = '0%';
|
|
844
|
+
document.getElementById('uploadPercent').textContent = '0%';
|
|
845
|
+
}, 3000);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Add hover effects and animations
|
|
849
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
850
|
+
// Load initial files (support both legacy 'prefix' and new 'folder' query param)
|
|
851
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
852
|
+
const initialPrefix = urlParams.get('folder') || urlParams.get('prefix') || bucketAppName || '';
|
|
853
|
+
const initialPage = parseInt(urlParams.get('page')) || 1;
|
|
854
|
+
loadFiles(initialPage, initialPrefix);
|
|
855
|
+
|
|
856
|
+
// Debounced search input (improves UX and reduces API calls)
|
|
857
|
+
const searchInputEl = document.getElementById('searchInput');
|
|
858
|
+
if (searchInputEl && typeof debounce === 'function') {
|
|
859
|
+
searchInputEl.addEventListener('input', debounce(() => performSearch(), 350));
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// Close modal when clicking outside
|
|
863
|
+
document.getElementById('deleteModal').addEventListener('click', function (e) {
|
|
864
|
+
if (e.target === this) {
|
|
865
|
+
closeModal();
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
// Delegate view and delete button clicks to avoid inline JS with unescaped data
|
|
870
|
+
document.addEventListener('click', function (e) {
|
|
871
|
+
const vbtn = e.target.closest('.view-btn');
|
|
872
|
+
if (vbtn) {
|
|
873
|
+
const keyEnc = vbtn.dataset.keyEnc;
|
|
874
|
+
const key = keyEnc || vbtn.dataset.key;
|
|
875
|
+
const filename = vbtn.dataset.filename || (vbtn.dataset.key || key);
|
|
876
|
+
handleViewFile(key, filename, e);
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const dbtn = e.target.closest('.delete-btn');
|
|
881
|
+
if (dbtn) {
|
|
882
|
+
e.stopPropagation();
|
|
883
|
+
const key = dbtn.dataset.key;
|
|
884
|
+
const isFolder = dbtn.dataset.isFolder === 'true';
|
|
885
|
+
if (key) confirmDelete(key, isFolder);
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
});
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
// Function to check if file can be viewed in browser
|
|
894
|
+
|
|
895
|
+
// Enhanced view button click handler
|
|
896
|
+
function handleViewFile(key, filename, event) {
|
|
897
|
+
// Prevent default action and event bubbling
|
|
898
|
+
if (event) {
|
|
899
|
+
event.preventDefault();
|
|
900
|
+
event.stopPropagation();
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (!isViewable(filename)) {
|
|
904
|
+
alert(`File type "${filename.split('.').pop().toUpperCase()}" is not supported for viewing in browser.\n\nSupported types include: Images, Videos (MP4, WebM), Audio (MP3, WAV), Text files, Code files, and PDF documents.`);
|
|
905
|
+
return false;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Determine file type
|
|
909
|
+
const extension = filename.split('.').pop().toLowerCase();
|
|
910
|
+
const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico'];
|
|
911
|
+
const videoTypes = ['mp4', 'webm', 'ogg'];
|
|
912
|
+
|
|
913
|
+
// Use inline preview modal for supported types
|
|
914
|
+
const previewableImage = imageTypes.includes(extension);
|
|
915
|
+
const previewableVideo = videoTypes.includes(extension);
|
|
916
|
+
const previewableAudio = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a'].includes(extension);
|
|
917
|
+
const previewablePdf = extension === 'pdf';
|
|
918
|
+
const previewableText = ['txt', 'md', 'json', 'xml', 'csv', 'log', 'js', 'ts', 'html', 'htm', 'css', 'py', 'java', 'cpp', 'c', 'h', 'cs', 'rb', 'go', 'rs', 'sql', 'sh', 'bat', 'ps1', 'yaml', 'yml', 'toml', 'ini', 'conf'].includes(extension);
|
|
919
|
+
|
|
920
|
+
// Build an encoded key safely (if key already appears encoded, don't double-encode)
|
|
921
|
+
const encodedKey = (key && key.includes('%')) ? key : encodeURIComponent(key || filename);
|
|
922
|
+
|
|
923
|
+
// Show the inline preview
|
|
924
|
+
showPreviewModal(encodedKey, filename, extension);
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// Function to get file type category
|
|
929
|
+
|
|
930
|
+
// Preview modal helpers
|
|
931
|
+
function showPreviewModal(encodedKey, filename, extension) {
|
|
932
|
+
const modal = document.getElementById('previewModal');
|
|
933
|
+
if (!modal) {
|
|
934
|
+
window.open(`/mbkbucket/view/${encodedKey}`, '_blank', 'noopener,noreferrer');
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const dialog = modal.querySelector('.modal-dialog');
|
|
939
|
+
const titleEl = document.getElementById('previewFileName');
|
|
940
|
+
const contentEl = modal.querySelector('.preview-content');
|
|
941
|
+
const openBtn = document.getElementById('previewOpenBtn');
|
|
942
|
+
const downloadBtn = document.getElementById('previewDownloadBtn');
|
|
943
|
+
const closeBtn = document.getElementById('previewCloseBtn');
|
|
944
|
+
const closeFooter = document.getElementById('previewCloseFooter');
|
|
945
|
+
const fsBtn = document.getElementById('previewFullscreenBtn');
|
|
946
|
+
|
|
947
|
+
titleEl.textContent = filename;
|
|
948
|
+
openBtn.href = `/mbkbucket/view/${encodedKey}`;
|
|
949
|
+
downloadBtn.href = `/mbkbucket/download/${encodedKey}`;
|
|
950
|
+
// Ensure downloaded file has the original filename
|
|
951
|
+
downloadBtn.setAttribute('download', filename);
|
|
952
|
+
|
|
953
|
+
// Reset sizing classes
|
|
954
|
+
dialog.classList.remove('preview-large', 'preview-compact');
|
|
955
|
+
|
|
956
|
+
// Clear previous content and show loading
|
|
957
|
+
contentEl.innerHTML = `<div class="empty-preview"><i class="fas fa-spinner fa-spin"></i><div style="margin-top:0.5rem; color:var(--gray-color-ml);">Loading preview...</div></div>`;
|
|
958
|
+
|
|
959
|
+
const imgTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico'];
|
|
960
|
+
const videoTypes = ['mp4', 'webm', 'ogg'];
|
|
961
|
+
const audioTypes = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a'];
|
|
962
|
+
const textTypes = ['txt', 'md', 'json', 'xml', 'csv', 'log', 'js', 'ts', 'html', 'htm', 'css', 'py', 'java', 'cpp', 'c', 'h', 'cs', 'rb', 'go', 'rs', 'sql', 'sh', 'bat', 'ps1', 'yaml', 'yml', 'toml', 'ini', 'conf'];
|
|
963
|
+
|
|
964
|
+
// Render content and adjust dialog size
|
|
965
|
+
if (imgTypes.includes(extension)) {
|
|
966
|
+
dialog.classList.add('preview-compact');
|
|
967
|
+
const img = document.createElement('img');
|
|
968
|
+
img.className = 'preview-img';
|
|
969
|
+
img.src = `/mbkbucket/view/${encodedKey}`;
|
|
970
|
+
img.alt = filename;
|
|
971
|
+
img.onload = () => { };
|
|
972
|
+
img.onerror = () => { contentEl.textContent = 'Could not load preview.'; };
|
|
973
|
+
contentEl.innerHTML = '';
|
|
974
|
+
contentEl.appendChild(img);
|
|
975
|
+
} else if (videoTypes.includes(extension)) {
|
|
976
|
+
dialog.classList.add('preview-large');
|
|
977
|
+
const video = document.createElement('video');
|
|
978
|
+
video.className = 'preview-video';
|
|
979
|
+
video.controls = true;
|
|
980
|
+
video.src = `/mbkbucket/view/${encodedKey}`;
|
|
981
|
+
contentEl.innerHTML = '';
|
|
982
|
+
contentEl.appendChild(video);
|
|
983
|
+
} else if (audioTypes.includes(extension)) {
|
|
984
|
+
dialog.classList.add('preview-compact');
|
|
985
|
+
const audio = document.createElement('audio');
|
|
986
|
+
audio.className = 'preview-audio';
|
|
987
|
+
audio.controls = true;
|
|
988
|
+
audio.src = `/mbkbucket/view/${encodedKey}`;
|
|
989
|
+
contentEl.innerHTML = '';
|
|
990
|
+
contentEl.appendChild(audio);
|
|
991
|
+
} else if (extension === 'pdf') {
|
|
992
|
+
dialog.classList.add('preview-large');
|
|
993
|
+
const iframe = document.createElement('iframe');
|
|
994
|
+
iframe.className = 'preview-iframe';
|
|
995
|
+
iframe.src = `/mbkbucket/view/${encodedKey}`;
|
|
996
|
+
contentEl.innerHTML = '';
|
|
997
|
+
contentEl.appendChild(iframe);
|
|
998
|
+
} else if (textTypes.includes(extension)) {
|
|
999
|
+
dialog.classList.add('preview-compact');
|
|
1000
|
+
fetch(`/mbkbucket/view/${encodedKey}`)
|
|
1001
|
+
.then(r => r.text())
|
|
1002
|
+
.then(text => {
|
|
1003
|
+
contentEl.innerHTML = '';
|
|
1004
|
+
const pre = document.createElement('pre');
|
|
1005
|
+
pre.textContent = text;
|
|
1006
|
+
pre.className = 'preview-text';
|
|
1007
|
+
contentEl.appendChild(pre);
|
|
1008
|
+
})
|
|
1009
|
+
.catch(err => { contentEl.textContent = 'Could not load preview.'; });
|
|
1010
|
+
} else {
|
|
1011
|
+
contentEl.innerHTML = `<div style="text-align:center;">Preview not available for this file type. <a href="/mbkbucket/download/${encodedKey}">Download</a></div>`;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Show modal
|
|
1015
|
+
modal.classList.add('show');
|
|
1016
|
+
modal.style.display = 'flex';
|
|
1017
|
+
modal.setAttribute('aria-hidden', 'false');
|
|
1018
|
+
|
|
1019
|
+
// Focus management + focus trap
|
|
1020
|
+
const prevFocus = document.activeElement;
|
|
1021
|
+
const focusableSelector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
1022
|
+
function getFocusable() { return Array.from(modal.querySelectorAll(focusableSelector)).filter(el => !el.hasAttribute('disabled')); }
|
|
1023
|
+
const focusable = () => getFocusable();
|
|
1024
|
+
const firstFocusable = () => focusable()[0];
|
|
1025
|
+
const lastFocusable = () => focusable()[focusable().length - 1];
|
|
1026
|
+
|
|
1027
|
+
// Focus initial
|
|
1028
|
+
(closeBtn || closeFooter).focus();
|
|
1029
|
+
|
|
1030
|
+
function onKeyDown(e) {
|
|
1031
|
+
if (e.key === 'Escape') return cleanup();
|
|
1032
|
+
if (e.key === 'Tab') {
|
|
1033
|
+
const f = focusable();
|
|
1034
|
+
if (f.length === 0) return;
|
|
1035
|
+
if (e.shiftKey && document.activeElement === f[0]) { e.preventDefault(); f[f.length - 1].focus(); }
|
|
1036
|
+
else if (!e.shiftKey && document.activeElement === f[f.length - 1]) { e.preventDefault(); f[0].focus(); }
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function onBackdropClick(e) { if (e.target === modal) cleanup(); }
|
|
1041
|
+
|
|
1042
|
+
// Fullscreen toggle
|
|
1043
|
+
let fsActive = false;
|
|
1044
|
+
function toggleFullscreen() {
|
|
1045
|
+
if (!document.fullscreenElement) {
|
|
1046
|
+
dialog.requestFullscreen?.();
|
|
1047
|
+
fsBtn.innerHTML = '<i class="fas fa-compress"></i>';
|
|
1048
|
+
fsActive = true;
|
|
1049
|
+
} else {
|
|
1050
|
+
document.exitFullscreen?.();
|
|
1051
|
+
fsBtn.innerHTML = '<i class="fas fa-expand"></i>';
|
|
1052
|
+
fsActive = false;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function cleanup() {
|
|
1057
|
+
// Exit fullscreen if active
|
|
1058
|
+
if (fsActive && document.fullscreenElement) document.exitFullscreen?.();
|
|
1059
|
+
modal.classList.remove('show');
|
|
1060
|
+
modal.style.display = 'none';
|
|
1061
|
+
modal.setAttribute('aria-hidden', 'true');
|
|
1062
|
+
if (prevFocus && prevFocus.focus) prevFocus.focus();
|
|
1063
|
+
modal.removeEventListener('click', onBackdropClick);
|
|
1064
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
1065
|
+
fsBtn.removeEventListener('click', toggleFullscreen);
|
|
1066
|
+
(closeBtn || closeFooter).removeEventListener('click', cleanup);
|
|
1067
|
+
closeFooter.removeEventListener('click', cleanup);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
modal.addEventListener('click', onBackdropClick);
|
|
1071
|
+
document.addEventListener('keydown', onKeyDown);
|
|
1072
|
+
fsBtn.addEventListener('click', toggleFullscreen);
|
|
1073
|
+
(closeBtn || closeFooter).addEventListener('click', cleanup);
|
|
1074
|
+
closeFooter.addEventListener('click', cleanup);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function closePreviewModal() {
|
|
1078
|
+
const modal = document.getElementById('previewModal');
|
|
1079
|
+
if (!modal) return;
|
|
1080
|
+
modal.classList.remove('show');
|
|
1081
|
+
modal.style.display = 'none';
|
|
1082
|
+
modal.setAttribute('aria-hidden', 'true');
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// Update file icons in the table
|
|
1086
|
+
function updateFileIcons() {
|
|
1087
|
+
const fileRows = document.querySelectorAll('.file-row');
|
|
1088
|
+
fileRows.forEach(row => {
|
|
1089
|
+
const fileNameElement = row.querySelector('.file-name');
|
|
1090
|
+
const folderPathElement = row.querySelector('.folder-path');
|
|
1091
|
+
const fileIconElement = row.querySelector('.file-icon i');
|
|
1092
|
+
const fileIconContainer = row.querySelector('.file-icon');
|
|
1093
|
+
const viewButton = row.querySelector('.view-btn');
|
|
1094
|
+
|
|
1095
|
+
if (fileNameElement && fileIconElement && fileIconContainer) {
|
|
1096
|
+
const fullPath = fileNameElement.getAttribute('data-full-path');
|
|
1097
|
+
|
|
1098
|
+
// Split the path into folder and filename
|
|
1099
|
+
const lastSlashIndex = fullPath.lastIndexOf('/');
|
|
1100
|
+
let folder = '';
|
|
1101
|
+
let filename = fullPath;
|
|
1102
|
+
|
|
1103
|
+
if (lastSlashIndex !== -1) {
|
|
1104
|
+
folder = fullPath.substring(0, lastSlashIndex);
|
|
1105
|
+
filename = fullPath.substring(lastSlashIndex + 1);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// Truncate long filenames
|
|
1109
|
+
const truncatedName = truncateFileName(filename);
|
|
1110
|
+
|
|
1111
|
+
// Update the display
|
|
1112
|
+
fileNameElement.textContent = truncatedName;
|
|
1113
|
+
fileNameElement.title = filename; // Full name on hover
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
const iconClass = getFileIcon(filename);
|
|
1118
|
+
const typeCategory = getFileTypeCategory(filename);
|
|
1119
|
+
|
|
1120
|
+
// Update icon
|
|
1121
|
+
fileIconElement.className = iconClass;
|
|
1122
|
+
|
|
1123
|
+
// Add type-specific styling
|
|
1124
|
+
if (typeCategory) {
|
|
1125
|
+
fileIconContainer.classList.add(typeCategory);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// Show/hide and update view button based on file type
|
|
1129
|
+
if (viewButton) {
|
|
1130
|
+
const isFileViewable = isViewable(filename);
|
|
1131
|
+
|
|
1132
|
+
if (isFileViewable) {
|
|
1133
|
+
viewButton.style.display = 'inline-flex';
|
|
1134
|
+
viewButton.style.opacity = '1';
|
|
1135
|
+
viewButton.title = 'View file in browser';
|
|
1136
|
+
viewButton.disabled = false;
|
|
1137
|
+
} else {
|
|
1138
|
+
viewButton.style.opacity = '0.5';
|
|
1139
|
+
viewButton.title = 'File type not supported for viewing';
|
|
1140
|
+
viewButton.disabled = true;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// Selection & bulk actions
|
|
1148
|
+
function setupSelection() {
|
|
1149
|
+
const selectAll = document.getElementById('selectAllCheckbox');
|
|
1150
|
+
const bulkBar = document.getElementById('bulkActionsBar');
|
|
1151
|
+
const bulkCount = document.getElementById('bulkSelectedCount');
|
|
1152
|
+
const bulkDeleteBtn = document.getElementById('bulkDeleteBtn');
|
|
1153
|
+
|
|
1154
|
+
const checkboxes = Array.from(document.querySelectorAll('.selectFileCheckbox'));
|
|
1155
|
+
|
|
1156
|
+
const selected = new Set();
|
|
1157
|
+
|
|
1158
|
+
function updateUI() {
|
|
1159
|
+
const count = selected.size;
|
|
1160
|
+
bulkCount.textContent = `${count} selected`;
|
|
1161
|
+
bulkBar.style.display = count > 0 ? 'block' : 'none';
|
|
1162
|
+
bulkDeleteBtn.disabled = count === 0;
|
|
1163
|
+
if (selectAll) selectAll.checked = checkboxes.length > 0 && selected.size === checkboxes.length;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
checkboxes.forEach(cb => {
|
|
1167
|
+
cb.checked = false;
|
|
1168
|
+
cb.addEventListener('change', function (e) {
|
|
1169
|
+
const key = this.dataset.key;
|
|
1170
|
+
if (this.checked) selected.add(key); else selected.delete(key);
|
|
1171
|
+
updateUI();
|
|
1172
|
+
});
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
if (selectAll) {
|
|
1176
|
+
selectAll.checked = false;
|
|
1177
|
+
selectAll.addEventListener('change', function () {
|
|
1178
|
+
const checked = this.checked;
|
|
1179
|
+
selected.clear();
|
|
1180
|
+
checkboxes.forEach(cb => { cb.checked = checked; if (checked) selected.add(cb.dataset.key); });
|
|
1181
|
+
updateUI();
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
if (bulkDeleteBtn) {
|
|
1186
|
+
bulkDeleteBtn.addEventListener('click', async function () {
|
|
1187
|
+
if (!confirm('Delete selected files? This cannot be undone.')) return;
|
|
1188
|
+
const keys = Array.from(selected);
|
|
1189
|
+
try {
|
|
1190
|
+
const resp = await fetch('/mbkbucket/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ keys }) });
|
|
1191
|
+
const json = await resp.json();
|
|
1192
|
+
if (json.success) {
|
|
1193
|
+
showAlert('Deleted selected files', 'success');
|
|
1194
|
+
loadFiles(currentPage, currentPrefix);
|
|
1195
|
+
} else {
|
|
1196
|
+
showAlert(json.error || 'Delete failed', 'error');
|
|
1197
|
+
}
|
|
1198
|
+
} catch (err) {
|
|
1199
|
+
showAlert('Delete failed: ' + err.message, 'error');
|
|
1200
|
+
}
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// Create folder (zero-byte placeholder object)
|
|
1206
|
+
async function createFolder() {
|
|
1207
|
+
const name = prompt('Enter new folder name (no slashes):');
|
|
1208
|
+
if (!name) return;
|
|
1209
|
+
const safe = name.replace(/[\\/]+/g, '').trim();
|
|
1210
|
+
if (!safe) return showAlert('Invalid folder name', 'error');
|
|
1211
|
+
|
|
1212
|
+
try {
|
|
1213
|
+
const resp = await fetch('/mbkbucket/create-folder', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prefix: currentPrefix, folderName: safe }) });
|
|
1214
|
+
const json = await resp.json();
|
|
1215
|
+
if (json.success) {
|
|
1216
|
+
showAlert('Folder created', 'success');
|
|
1217
|
+
loadFiles(currentPage, currentPrefix);
|
|
1218
|
+
} else {
|
|
1219
|
+
showAlert(json.error || 'Create folder failed', 'error');
|
|
1220
|
+
}
|
|
1221
|
+
} catch (err) {
|
|
1222
|
+
showAlert('Create folder failed: ' + err.message, 'error');
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// Start/clear upload buttons
|
|
1227
|
+
document.getElementById('startUploadBtn')?.addEventListener('click', startAllUploads);
|
|
1228
|
+
document.getElementById('clearQueueBtn')?.addEventListener('click', function () { uploadQueue.length = 0; renderUploadQueue(); });
|
|
1229
|
+
|
|
1230
|
+
// mbkbucket shared utilities (exposed to global window for backward compatibility)
|
|
1231
|
+
(function (w) {
|
|
1232
|
+
'use strict';
|
|
1233
|
+
|
|
1234
|
+
w.formatDate = function (dateString) {
|
|
1235
|
+
const date = new Date(dateString);
|
|
1236
|
+
return date.toLocaleString();
|
|
1237
|
+
};
|
|
1238
|
+
|
|
1239
|
+
w.formatFileSize = function (bytes) {
|
|
1240
|
+
if (bytes === 0) return '0 Bytes';
|
|
1241
|
+
const k = 1024;
|
|
1242
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
1243
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1244
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
1245
|
+
};
|
|
1246
|
+
|
|
1247
|
+
w.formatTime = function (seconds) {
|
|
1248
|
+
if (seconds < 60) {
|
|
1249
|
+
return Math.round(seconds) + 's';
|
|
1250
|
+
} else if (seconds < 3600) {
|
|
1251
|
+
const minutes = Math.floor(seconds / 60);
|
|
1252
|
+
const remainingSeconds = Math.round(seconds % 60);
|
|
1253
|
+
return minutes + 'm ' + remainingSeconds + 's';
|
|
1254
|
+
} else {
|
|
1255
|
+
const hours = Math.floor(seconds / 3600);
|
|
1256
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
1257
|
+
return hours + 'h ' + minutes + 'm';
|
|
1258
|
+
}
|
|
1259
|
+
};
|
|
1260
|
+
|
|
1261
|
+
w.getFileIcon = function (filename) {
|
|
1262
|
+
const extension = (filename || '').split('.').pop().toLowerCase();
|
|
1263
|
+
|
|
1264
|
+
const iconMap = {
|
|
1265
|
+
jpg: 'fas fa-image',
|
|
1266
|
+
jpeg: 'fas fa-image',
|
|
1267
|
+
png: 'fas fa-image',
|
|
1268
|
+
gif: 'fas fa-image',
|
|
1269
|
+
webp: 'fas fa-image',
|
|
1270
|
+
svg: 'fas fa-image',
|
|
1271
|
+
bmp: 'fas fa-image',
|
|
1272
|
+
ico: 'fas fa-image',
|
|
1273
|
+
mp4: 'fas fa-video',
|
|
1274
|
+
webm: 'fas fa-video',
|
|
1275
|
+
ogg: 'fas fa-video',
|
|
1276
|
+
avi: 'fas fa-video',
|
|
1277
|
+
mov: 'fas fa-video',
|
|
1278
|
+
wmv: 'fas fa-video',
|
|
1279
|
+
flv: 'fas fa-video',
|
|
1280
|
+
mkv: 'fas fa-video',
|
|
1281
|
+
mp3: 'fas fa-music',
|
|
1282
|
+
wav: 'fas fa-music',
|
|
1283
|
+
flac: 'fas fa-music',
|
|
1284
|
+
aac: 'fas fa-music',
|
|
1285
|
+
m4a: 'fas fa-music',
|
|
1286
|
+
pdf: 'fas fa-file-pdf',
|
|
1287
|
+
doc: 'fas fa-file-word',
|
|
1288
|
+
docx: 'fas fa-file-word',
|
|
1289
|
+
xls: 'fas fa-file-excel',
|
|
1290
|
+
xlsx: 'fas fa-file-excel',
|
|
1291
|
+
ppt: 'fas fa-file-powerpoint',
|
|
1292
|
+
pptx: 'fas fa-file-powerpoint',
|
|
1293
|
+
js: 'fab fa-js-square',
|
|
1294
|
+
ts: 'fas fa-code',
|
|
1295
|
+
html: 'fab fa-html5',
|
|
1296
|
+
htm: 'fab fa-html5',
|
|
1297
|
+
css: 'fab fa-css3-alt',
|
|
1298
|
+
php: 'fab fa-php',
|
|
1299
|
+
py: 'fab fa-python',
|
|
1300
|
+
java: 'fab fa-java',
|
|
1301
|
+
cpp: 'fas fa-code',
|
|
1302
|
+
c: 'fas fa-code',
|
|
1303
|
+
h: 'fas fa-code',
|
|
1304
|
+
cs: 'fas fa-code',
|
|
1305
|
+
rb: 'fas fa-code',
|
|
1306
|
+
go: 'fas fa-code',
|
|
1307
|
+
rs: 'fas fa-code',
|
|
1308
|
+
sql: 'fas fa-database',
|
|
1309
|
+
sh: 'fas fa-terminal',
|
|
1310
|
+
bat: 'fas fa-terminal',
|
|
1311
|
+
ps1: 'fas fa-terminal',
|
|
1312
|
+
txt: 'fas fa-file-alt',
|
|
1313
|
+
md: 'fab fa-markdown',
|
|
1314
|
+
json: 'fas fa-code',
|
|
1315
|
+
xml: 'fas fa-code',
|
|
1316
|
+
csv: 'fas fa-file-csv',
|
|
1317
|
+
log: 'fas fa-file-alt',
|
|
1318
|
+
yaml: 'fas fa-code',
|
|
1319
|
+
yml: 'fas fa-code',
|
|
1320
|
+
toml: 'fas fa-code',
|
|
1321
|
+
ini: 'fas fa-cog',
|
|
1322
|
+
conf: 'fas fa-cog',
|
|
1323
|
+
zip: 'fas fa-file-archive',
|
|
1324
|
+
rar: 'fas fa-file-archive',
|
|
1325
|
+
'7z': 'fas fa-file-archive',
|
|
1326
|
+
tar: 'fas fa-file-archive',
|
|
1327
|
+
gz: 'fas fa-file-archive'
|
|
1328
|
+
};
|
|
1329
|
+
|
|
1330
|
+
return iconMap[extension] || 'fas fa-file-alt';
|
|
1331
|
+
};
|
|
1332
|
+
|
|
1333
|
+
w.isViewable = function (filename) {
|
|
1334
|
+
const extension = (filename || '').split('.').pop().toLowerCase();
|
|
1335
|
+
const viewableTypes = [
|
|
1336
|
+
'jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico',
|
|
1337
|
+
'mp4', 'webm', 'ogg',
|
|
1338
|
+
'mp3', 'wav', 'ogg',
|
|
1339
|
+
'txt', 'md', 'json', 'xml', 'csv', 'log', 'js', 'ts', 'html', 'htm', 'css',
|
|
1340
|
+
'php', 'py', 'java', 'cpp', 'c', 'h', 'cs', 'rb', 'go', 'rs', 'sql',
|
|
1341
|
+
'sh', 'bat', 'ps1', 'yaml', 'yml', 'toml', 'ini', 'conf',
|
|
1342
|
+
'pdf'
|
|
1343
|
+
];
|
|
1344
|
+
|
|
1345
|
+
return viewableTypes.includes(extension);
|
|
1346
|
+
};
|
|
1347
|
+
|
|
1348
|
+
w.getFileTypeCategory = function (filename) {
|
|
1349
|
+
const extension = (filename || '').split('.').pop().toLowerCase();
|
|
1350
|
+
const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico'];
|
|
1351
|
+
const videoTypes = ['mp4', 'webm', 'ogg', 'avi', 'mov', 'wmv', 'flv', 'mkv'];
|
|
1352
|
+
const audioTypes = ['mp3', 'wav', 'flac', 'aac', 'm4a'];
|
|
1353
|
+
const documentTypes = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'];
|
|
1354
|
+
const codeTypes = ['js', 'ts', 'html', 'htm', 'css', 'php', 'py', 'java', 'cpp', 'c', 'h', 'cs', 'rb', 'go', 'rs', 'sql', 'sh', 'bat', 'ps1', 'yaml', 'yml', 'toml', 'ini', 'conf', 'txt', 'md', 'json', 'xml', 'csv', 'log'];
|
|
1355
|
+
|
|
1356
|
+
if (imageTypes.includes(extension)) return 'image-file';
|
|
1357
|
+
if (videoTypes.includes(extension)) return 'video-file';
|
|
1358
|
+
if (audioTypes.includes(extension)) return 'audio-file';
|
|
1359
|
+
if (documentTypes.includes(extension)) return 'document-file';
|
|
1360
|
+
if (codeTypes.includes(extension)) return 'code-file';
|
|
1361
|
+
|
|
1362
|
+
return '';
|
|
1363
|
+
};
|
|
1364
|
+
|
|
1365
|
+
w.truncateFileName = function (filename) {
|
|
1366
|
+
const isMobile = window.innerWidth <= 768;
|
|
1367
|
+
if (isMobile) return filename;
|
|
1368
|
+
const maxLength = 30;
|
|
1369
|
+
if ((filename || '').length <= maxLength) return filename;
|
|
1370
|
+
const dotIndex = filename.lastIndexOf('.');
|
|
1371
|
+
let name = filename;
|
|
1372
|
+
let extension = '';
|
|
1373
|
+
if (dotIndex !== -1) {
|
|
1374
|
+
name = filename.substring(0, dotIndex);
|
|
1375
|
+
extension = filename.substring(dotIndex);
|
|
1376
|
+
}
|
|
1377
|
+
const lastChars = 3;
|
|
1378
|
+
const extensionLength = extension.length;
|
|
1379
|
+
const ellipsis = '...';
|
|
1380
|
+
const availableForName = maxLength - ellipsis.length - lastChars - extensionLength;
|
|
1381
|
+
if (availableForName <= 0) return filename.substring(0, maxLength - ellipsis.length) + ellipsis;
|
|
1382
|
+
const firstPart = name.substring(0, availableForName);
|
|
1383
|
+
const lastPart = name.substring(name.length - lastChars);
|
|
1384
|
+
return firstPart + ellipsis + lastPart + extension;
|
|
1385
|
+
};
|
|
1386
|
+
|
|
1387
|
+
// Debounce helper - returns a function that delays invocation until after wait ms
|
|
1388
|
+
w.debounce = function (fn, wait) {
|
|
1389
|
+
let timeout = null;
|
|
1390
|
+
return function () {
|
|
1391
|
+
const ctx = this;
|
|
1392
|
+
const args = arguments;
|
|
1393
|
+
clearTimeout(timeout);
|
|
1394
|
+
timeout = setTimeout(function () {
|
|
1395
|
+
fn.apply(ctx, args);
|
|
1396
|
+
}, wait);
|
|
1397
|
+
};
|
|
1398
|
+
};
|
|
1399
|
+
|
|
1400
|
+
})(window);
|