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.
@@ -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);