mbkbucket 1.5.0 → 2.0.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.
@@ -10,7 +10,29 @@ let pageFolders = [];
10
10
  let currentNextToken = null;
11
11
  let currentApiPrefix = '';
12
12
  const bucketAppName = document.getElementById('bucketAppData')?.dataset.appName || '';
13
- // Bucket is configured centrally on the server; client should not select a bucket.
13
+ const selectedBucket = new URLSearchParams(window.location.search).get('bucket') || '';
14
+ let activeBucket = selectedBucket;
15
+ const isRootApp = String(bucketAppName || '').toLowerCase() === 'portal';
16
+ const rootLabel = isRootApp ? 'root' : (bucketAppName || 'app');
17
+ let currentQuickFilter = 'all';
18
+ let virtualRenderLimit = 0;
19
+ const VIRTUAL_RENDER_THRESHOLD = 500;
20
+ const VIRTUAL_RENDER_BATCH = 250;
21
+ let lastRenderContext = null;
22
+
23
+ function withBucketUrl(url) {
24
+ if (!activeBucket) return url;
25
+ const separator = url.includes('?') ? '&' : '?';
26
+ return `${url}${separator}bucket=${encodeURIComponent(activeBucket)}`;
27
+ }
28
+
29
+ function getViewUrl(encodedKey) {
30
+ return withBucketUrl(`/mbkbucket/view/${encodedKey}`);
31
+ }
32
+
33
+ function getDownloadUrl(encodedKey) {
34
+ return withBucketUrl(`/mbkbucket/download/${encodedKey}`);
35
+ }
14
36
 
15
37
  function stripApiPrefixFromPath(path, apiPrefix = '') {
16
38
  const normalizedPath = String(path || '').replace(/^\/+/, '');
@@ -37,6 +59,64 @@ function toApiFolderPath(prefix = '', apiPrefix = '') {
37
59
  return `${normalizedApiPrefix}/${uiPrefix}`;
38
60
  }
39
61
 
62
+ function escapeHtml(value = '') {
63
+ return String(value)
64
+ .replace(/&/g, '&')
65
+ .replace(/</g, '&lt;')
66
+ .replace(/>/g, '&gt;')
67
+ .replace(/"/g, '&quot;')
68
+ .replace(/'/g, '&#39;');
69
+ }
70
+
71
+ function escapeAttr(value = '') {
72
+ return escapeHtml(value);
73
+ }
74
+
75
+ function syncStateUrl(page = currentPage, prefix = currentPrefix, search = currentSearch) {
76
+ try {
77
+ const parts = [];
78
+ if (activeBucket) parts.push(`bucket=${encodeURIComponent(activeBucket)}`);
79
+ if (prefix) {
80
+ const folderPathForUrl = toApiFolderPath(prefix, currentApiPrefix);
81
+ const folderEncoded = encodeURIComponent(folderPathForUrl).replace(/%2F/g, '/');
82
+ parts.push(`folder=${folderEncoded}`);
83
+ }
84
+ if (page && page > 1) parts.push(`page=${page}`);
85
+ if (search) parts.push(`search=${encodeURIComponent(search)}`);
86
+ parts.push(`sort=${encodeURIComponent(currentSort)}`);
87
+ parts.push(`order=${encodeURIComponent(currentOrder)}`);
88
+ parts.push(`view=${encodeURIComponent(currentViewMode)}`);
89
+ parts.push(`filter=${encodeURIComponent(currentQuickFilter)}`);
90
+
91
+ const newUrl = '/mbkbucket' + (parts.length ? '?' + parts.join('&') : '');
92
+ window.history.replaceState(null, '', newUrl);
93
+ } catch (e) {
94
+ console.warn('history update failed', e);
95
+ }
96
+ }
97
+
98
+ function getActiveBucketLabel() {
99
+ return activeBucket || 'default bucket';
100
+ }
101
+
102
+ function matchesQuickFilter(fileName = '', filter = currentQuickFilter) {
103
+ const ext = String(fileName).split('.').pop().toLowerCase();
104
+ const media = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'mp4', 'webm', 'ogg', 'avi', 'mov', 'mp3', 'wav', 'flac', 'aac', 'm4a']);
105
+ const docs = new Set(['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'md', 'csv']);
106
+ const code = new Set(['js', 'ts', 'html', 'htm', 'css', 'php', 'py', 'java', 'cpp', 'c', 'h', 'cs', 'rb', 'go', 'rs', 'sql', 'sh', 'bat', 'ps1', 'yaml', 'yml', 'toml', 'ini', 'conf', 'json', 'xml', 'log']);
107
+
108
+ if (filter === 'media') return media.has(ext);
109
+ if (filter === 'docs') return docs.has(ext);
110
+ if (filter === 'code') return code.has(ext);
111
+ return true;
112
+ }
113
+
114
+ function updateQuickFilterUI() {
115
+ document.querySelectorAll('.quick-filter-btn').forEach((btn) => {
116
+ btn.classList.toggle('active', btn.dataset.filter === currentQuickFilter);
117
+ });
118
+ }
119
+
40
120
  // Show alert message (Toast)
41
121
  function showAlert(message, type = 'success') {
42
122
  const container = document.getElementById('toast-container');
@@ -103,26 +183,10 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
103
183
  currentSearch = search;
104
184
  currentSort = sort;
105
185
  currentOrder = order;
186
+ virtualRenderLimit = 0;
106
187
 
107
- // Update browser URL so navigation is shareable/bookmarkable (folder shown as readable path)
108
- try {
109
- const parts = [];
110
- if (prefix) {
111
- const folderPathForUrl = toApiFolderPath(prefix, currentApiPrefix);
112
- // preserve slashes in folder param for readability: encode then restore slashes
113
- const folderEncoded = encodeURIComponent(folderPathForUrl).replace(/%2F/g, '/');
114
- parts.push(`folder=${folderEncoded}`);
115
- }
116
- if (page && page > 1) parts.push(`page=${page}`);
117
- if (search) parts.push(`search=${encodeURIComponent(search)}`);
118
- // Optional: Add sort/order to URL if desired, skipping for simplicity/clean URLs for now
119
-
120
- const newUrl = '/mbkbucket' + (parts.length ? '?' + parts.join('&') : '');
121
- window.history.replaceState(null, '', newUrl);
122
- } catch (e) {
123
- // Ignore history errors in older browsers
124
- console.warn('history update failed', e);
125
- }
188
+ // Update browser URL so navigation is shareable/bookmarkable.
189
+ syncStateUrl(page, prefix, search);
126
190
 
127
191
  const container = document.getElementById('fileListContainer');
128
192
  const paginationContainer = document.getElementById('paginationContainer');
@@ -157,7 +221,7 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
157
221
  const recursiveParam = `&recursive=${!isFolderMode}`; // false if folder mode, true if flat mode
158
222
  const tokenParam = token ? `&token=${encodeURIComponent(token)}` : '';
159
223
 
160
- const response = await fetch(`/mbkbucket/api/files?page=${page}&prefix=${encodeURIComponent(prefix)}${searchParam}${sortParam}${recursiveParam}${tokenParam}`);
224
+ const response = await fetch(withBucketUrl(`/mbkbucket/api/files?page=${page}&prefix=${encodeURIComponent(prefix)}${searchParam}${sortParam}${recursiveParam}${tokenParam}`));
161
225
  const data = await response.json();
162
226
 
163
227
  if (!data.success) {
@@ -176,11 +240,13 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
176
240
  }
177
241
 
178
242
  if (search) {
179
- filterInfo.textContent = `Search results for: "${search}"`;
243
+ const filterSuffix = currentQuickFilter !== 'all' ? ` • Filter: ${currentQuickFilter}` : '';
244
+ filterInfo.textContent = `Bucket: ${getActiveBucketLabel()} • Search: "${search}"${filterSuffix}`;
180
245
  fileListTitle.innerHTML = `Search Results <span class="text-muted">for: </span><span class="badge bg-info">${search}</span>`;
181
246
  } else {
182
- const label = prefix || bucketAppName || 'app';
183
- filterInfo.textContent = `Filtered by folder: ${label}`;
247
+ const label = prefix || rootLabel;
248
+ const filterSuffix = currentQuickFilter !== 'all' ? ` • Filter: ${currentQuickFilter}` : '';
249
+ filterInfo.textContent = `Bucket: ${getActiveBucketLabel()} • Folder: ${label}${filterSuffix}`;
184
250
  fileListTitle.innerHTML = `Files <span class="text-muted">in: </span><span class="badge bg-info">${label}</span>`;
185
251
  }
186
252
 
@@ -192,7 +258,7 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
192
258
  <div class="empty-state">
193
259
  <i class="fas fa-folder-open"></i>
194
260
  <h4>No files found</h4>
195
- <p>Your bucket is empty. Upload some files to get started!</p>
261
+ <p>Bucket <strong>${escapeHtml(getActiveBucketLabel())}</strong> is empty. Upload some files to get started!</p>
196
262
  <button class="btn btn-lg" onclick="document.getElementById('fileInput').click()">
197
263
  <i class="fas fa-plus"></i> Upload Your First File
198
264
  </button>
@@ -223,7 +289,7 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
223
289
  <div class="empty-state">
224
290
  <i class="fas fa-exclamation-triangle" style="color: var(--danger-color-ml);"></i>
225
291
  <h4>Error Loading Files</h4>
226
- <p>${error.message}</p>
292
+ <p>Bucket <strong>${escapeHtml(getActiveBucketLabel())}</strong>: ${escapeHtml(error.message)}</p>
227
293
  <button class="btn btn-primary" onclick="loadFiles(${page}, '${prefix}')">
228
294
  <i class="fas fa-redo"></i> Retry
229
295
  </button>
@@ -263,6 +329,7 @@ function toggleSort(field) {
263
329
  });
264
330
 
265
331
  renderFilesTable(pageFiles, pageFolders, currentPrefix, currentApiPrefix);
332
+ syncStateUrl(currentPage, currentPrefix, currentSearch);
266
333
  }
267
334
  }
268
335
 
@@ -281,6 +348,15 @@ function clearSearch() {
281
348
  loadFiles(1, currentPrefix, '');
282
349
  }
283
350
 
351
+ function setQuickFilter(filter = 'all') {
352
+ const nextFilter = String(filter || 'all').toLowerCase();
353
+ if (currentQuickFilter === nextFilter) return;
354
+ currentQuickFilter = nextFilter;
355
+ updateQuickFilterUI();
356
+ renderFilesTable(pageFiles, pageFolders, currentPrefix, currentApiPrefix);
357
+ syncStateUrl(currentPage, currentPrefix, currentSearch);
358
+ }
359
+
284
360
  // Extract unique folders at current level
285
361
  function extractFoldersAtCurrentLevel(files, prefix) {
286
362
  const folderSet = new Set();
@@ -330,8 +406,14 @@ function changeViewMode(mode) {
330
406
  btnFolder.classList.add('btn-outline-light');
331
407
  }
332
408
 
333
- // Re-render with existing data
334
- renderFilesTable(pageFiles, pageFolders, currentPrefix, currentApiPrefix);
409
+ // Refetch to respect recursive/non-recursive mode and keep URL state.
410
+ loadFiles(1, currentPrefix, currentSearch, currentSort, currentOrder);
411
+ }
412
+
413
+ function expandVirtualRows() {
414
+ if (!lastRenderContext) return;
415
+ virtualRenderLimit += VIRTUAL_RENDER_BATCH;
416
+ renderFilesTable(lastRenderContext.files, lastRenderContext.folders, lastRenderContext.prefix, lastRenderContext.apiPrefix);
335
417
  }
336
418
 
337
419
  // Extract folders that match a search query from file paths
@@ -364,6 +446,8 @@ function extractMatchingFolders(files, search) {
364
446
 
365
447
  // Render files table
366
448
  function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
449
+ lastRenderContext = { files, folders, prefix, apiPrefix };
450
+
367
451
  // Determine which files/folders to show based on view mode
368
452
  let displayFiles = [];
369
453
  let displayFolders = [];
@@ -409,6 +493,12 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
409
493
  displayFolders = folders;
410
494
  }
411
495
 
496
+ // Apply quick file-type filter after mode/path filtering.
497
+ displayFiles = displayFiles.filter((f) => {
498
+ const name = String(f?.Key || '').split('/').pop();
499
+ return matchesQuickFilter(name, currentQuickFilter);
500
+ });
501
+
412
502
  // Generate breadcrumb navigation
413
503
  let breadcrumb = '';
414
504
  // Show breadcrumb only if we are in folder mode OR if we have a prefix filter active even in flat mode
@@ -419,7 +509,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
419
509
  <div class="breadcrumb-nav">
420
510
  <div class="breadcrumb-item">
421
511
  <i class="fas fa-home"></i>
422
- <span class="breadcrumb-link" onclick="loadFiles(1, '')" title="Go to ${bucketAppName || 'app'}">${bucketAppName || 'app'}</span>
512
+ <span class="breadcrumb-link" onclick="loadFiles(1, '')" title="Go to ${rootLabel}">${rootLabel}</span>
423
513
  </div>
424
514
  `;
425
515
 
@@ -451,13 +541,18 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
451
541
  }
452
542
 
453
543
  // Generate folder rows (only used in folder mode)
454
- const folderRows = displayFolders.map(folderPath => {
544
+ const folderRows = displayFolders.map(folderPath => {
455
545
  // Handle folder paths which typically end in a slash (e.g., "folder/subfolder/")
456
- const cleanPath = folderPath.replace(/\/+$/, '');
546
+ const rawFolderPath = String(folderPath || '');
547
+ const cleanPath = rawFolderPath.replace(/\/+$/, '');
548
+ const folderKeyForDelete = `${cleanPath}/`;
457
549
  const folderName = cleanPath.split('/').pop();
550
+ const safeFolderName = escapeHtml(folderName);
551
+ const safeFolderPathAttr = escapeAttr(rawFolderPath);
552
+ const safeFolderDeleteKeyAttr = escapeAttr(folderKeyForDelete);
458
553
 
459
554
  return `
460
- <tr class="folder-row" onclick="loadFiles(1, '${folderPath}')" title="Open folder: ${folderName}">
555
+ <tr class="folder-row" data-folder-path="${safeFolderPathAttr}" title="Open folder: ${safeFolderName}">
461
556
  <td style="text-align:center;">
462
557
  <!-- no selection for folders -->
463
558
  </td>
@@ -468,10 +563,10 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
468
563
  </div>
469
564
  <div class="file-info">
470
565
  <div class="file-name" style="font-weight: 700;">
471
- <i class="fas fa-folder" style="margin-right: 0.5rem; color: #f59e0b;"></i>${folderName}/
566
+ <i class="fas fa-folder" style="margin-right: 0.5rem; color: #f59e0b;"></i>${safeFolderName}/
472
567
  </div>
473
568
  </div>
474
- <button type="button" class="btn btn-danger btn-sm delete-btn dtm" onclick="event.stopPropagation(); confirmDelete('${folderPath}/', true);" data-key="${folderPath}/" data-is-folder="true" title="Delete folder">
569
+ <button type="button" class="btn btn-danger btn-sm delete-btn dtm" data-key="${safeFolderDeleteKeyAttr}" data-is-folder="true" title="Delete folder">
475
570
  <i class="fas fa-trash"></i>
476
571
  </button>
477
572
  </div>
@@ -484,7 +579,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
484
579
  </td>
485
580
  <td style="text-align: center;">
486
581
  <div class="btn-group" role="group">
487
- <button type="button" class="btn btn-danger btn-sm delete-btn" onclick="event.stopPropagation(); confirmDelete('${folderPath}/', true);" data-key="${folderPath}/" data-is-folder="true" title="Delete folder">
582
+ <button type="button" class="btn btn-danger btn-sm delete-btn" data-key="${safeFolderDeleteKeyAttr}" data-is-folder="true" title="Delete folder">
488
583
  <i class="fas fa-trash"></i>
489
584
  </button>
490
585
  </div>
@@ -493,12 +588,22 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
493
588
  `;
494
589
  }).join('');
495
590
 
591
+ const isVirtualized = displayFiles.length > VIRTUAL_RENDER_THRESHOLD;
592
+ if (!isVirtualized) {
593
+ virtualRenderLimit = 0;
594
+ } else if (!virtualRenderLimit) {
595
+ virtualRenderLimit = VIRTUAL_RENDER_BATCH;
596
+ }
597
+ const visibleFiles = isVirtualized ? displayFiles.slice(0, virtualRenderLimit) : displayFiles;
598
+
496
599
  // Generate file rows
497
- const fileRows = displayFiles.map(file => {
498
- const encodedKey = encodeURIComponent(file.Key);
600
+ const fileRows = visibleFiles.map(file => {
601
+ const rawKey = String(file.Key || '');
602
+ const encodedKey = encodeURIComponent(rawKey);
603
+ const safeKeyAttr = escapeAttr(rawKey);
499
604
 
500
605
  // Calculate display name and icon directly
501
- const fullPath = file.Key;
606
+ const fullPath = rawKey;
502
607
  const lastSlashIndex = fullPath.lastIndexOf('/');
503
608
  let filename = fullPath;
504
609
  let folderPath = '';
@@ -512,18 +617,21 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
512
617
  const typeCategory = window.getFileTypeCategory ? window.getFileTypeCategory(filename) : '';
513
618
  const truncatedName = (window.truncateFileName ? window.truncateFileName(filename) : filename) || filename;
514
619
  const isFileViewable = window.isViewable ? window.isViewable(filename) : false;
620
+ const safeFilename = escapeHtml(filename);
621
+ const safeTruncatedName = escapeHtml(truncatedName);
622
+ const safeFolderPath = escapeHtml(folderPath);
515
623
 
516
624
  // If in flat mode, we might want to show the folder path more prominently
517
625
  const pathDisplay = (currentViewMode === 'flat' && folderPath && folderPath !== prefix + (prefix ? '/' : ''))
518
626
  ? `<div class="file-path-display">
519
- <i class="fas fa-folder" style="margin-right:4px; opacity:0.5;"></i> ${folderPath}
627
+ <i class="fas fa-folder" style="margin-right:4px; opacity:0.5;"></i> ${safeFolderPath}
520
628
  </div>`
521
629
  : '';
522
630
 
523
631
  return `
524
632
  <tr class="file-row">
525
633
  <td style="text-align:center;">
526
- <input type="checkbox" class="selectFileCheckbox" data-key="${file.Key}" aria-label="Select ${file.Key}" />
634
+ <input type="checkbox" class="selectFileCheckbox" data-key="${safeKeyAttr}" aria-label="Select ${safeKeyAttr}" />
527
635
  </td>
528
636
  <td style="padding-left: 1.5rem;">
529
637
  <div style="display: flex; align-items: center;">
@@ -532,7 +640,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
532
640
  </div>
533
641
  <div class="file-info">
534
642
  ${pathDisplay}
535
- <div class="file-name" data-full-path="${file.Key}" title="${filename}">${truncatedName}</div>
643
+ <div class="file-name" data-full-path="${safeKeyAttr}" title="${safeFilename}">${safeTruncatedName}</div>
536
644
  </div>
537
645
  </div>
538
646
  </td>
@@ -544,18 +652,18 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
544
652
  </td>
545
653
  <td style="text-align: center;">
546
654
  <div class="btn-group" role="group">
547
- <button type="button" class="btn btn-primary btn-sm view-btn" data-key="${file.Key}" data-filename="${file.Key}"
655
+ <button type="button" class="btn btn-primary btn-sm view-btn" data-key="${safeKeyAttr}" data-filename="${safeKeyAttr}"
548
656
  data-key-enc="${encodedKey}" title="${isFileViewable ? 'View file in browser' : 'File type not supported for viewing'}" ${!isFileViewable ? 'disabled style="opacity:0.5"' : ''}>
549
657
  <i class="fas fa-eye"></i>
550
658
  </button>
551
659
  <button type="button" class="btn btn-info btn-sm copy-btn" data-key-enc="${encodedKey}" title="Copy Link" style="color:white;">
552
660
  <i class="fas fa-link"></i>
553
661
  </button>
554
- <a href="/mbkbucket/download/${encodedKey}" class="btn btn-success btn-sm"
662
+ <a href="${getDownloadUrl(encodedKey)}" class="btn btn-success btn-sm"
555
663
  title="Download file">
556
664
  <i class="fas fa-download"></i>
557
665
  </a>
558
- <button type="button" class="btn btn-danger btn-sm delete-btn" data-key="${file.Key}" title="Delete file">
666
+ <button type="button" class="btn btn-danger btn-sm delete-btn" data-key="${safeKeyAttr}" title="Delete file">
559
667
  <i class="fas fa-trash"></i>
560
668
  </button>
561
669
  </div>
@@ -576,7 +684,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
576
684
  </div>
577
685
  <h4>No files found</h4>
578
686
  <p class="empty-state-description">
579
- ${prefix ? `This folder <strong>${prefix}</strong> is currently empty.` : 'Your bucket is empty. Start by uploading files or creating a new folder.'}
687
+ ${prefix ? `This folder <strong>${escapeHtml(prefix)}</strong> is currently empty in bucket <strong>${escapeHtml(getActiveBucketLabel())}</strong>.` : `Bucket <strong>${escapeHtml(getActiveBucketLabel())}</strong> is empty. Start by uploading files or creating a new folder.`}
580
688
  </p>
581
689
  <div class="empty-state-actions">
582
690
  <button class="btn btn-primary" onclick="window.scrollTo({top: 0, behavior: 'smooth'}); document.getElementById('fileInput').click()">
@@ -586,7 +694,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
586
694
  <i class="fas fa-folder-plus"></i> New Folder
587
695
  </button>
588
696
  ${prefix ? `<button class="btn btn-light" onclick="loadFiles(1, '')">
589
- <i class="fas fa-home"></i> Go to ${bucketAppName || 'app'}
697
+ <i class="fas fa-home"></i> Go to ${rootLabel}
590
698
  </button>` : ''}
591
699
  </div>
592
700
  </div>
@@ -602,7 +710,16 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
602
710
  : '<i class="fas fa-sort-down sort-icon sort-icon-active"></i>';
603
711
  };
604
712
 
605
- document.getElementById('fileListContainer').innerHTML = `
713
+ const virtualizedInfo = isVirtualized
714
+ ? `<div class="virtualized-info">
715
+ <span>Showing ${visibleFiles.length} of ${displayFiles.length} files in this view.</span>
716
+ <div class="virtualized-actions">
717
+ ${visibleFiles.length < displayFiles.length ? '<button class="btn btn-sm btn-outline-secondary" onclick="expandVirtualRows()">Load more</button>' : ''}
718
+ </div>
719
+ </div>`
720
+ : '';
721
+
722
+ document.getElementById('fileListContainer').innerHTML = `
606
723
  ${breadcrumb}
607
724
  <div class="table-responsive">
608
725
  <div id="bulkActionsBar" class="bulk-actions-bar">
@@ -637,6 +754,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
637
754
  ${allRows}
638
755
  </tbody>
639
756
  </table>
757
+ ${virtualizedInfo}
640
758
  </div>
641
759
  `;
642
760
 
@@ -819,7 +937,7 @@ document.getElementById('deleteForm')?.addEventListener('submit', async function
819
937
  const isFolder = document.getElementById('deleteIsFolder').value === 'true';
820
938
 
821
939
  try {
822
- const response = await fetch('/mbkbucket/delete', {
940
+ const response = await fetch(withBucketUrl('/mbkbucket/delete'), {
823
941
  method: 'POST',
824
942
  headers: {
825
943
  'Content-Type': 'application/json'
@@ -889,6 +1007,9 @@ function renderUploadQueue() {
889
1007
  }
890
1008
 
891
1009
  container.innerHTML = uploadQueue.map(item => {
1010
+ const safeName = escapeHtml(item.file.name);
1011
+ const iconClass = window.getFileIcon ? window.getFileIcon(item.file.name) : 'fas fa-file-alt';
1012
+ const typeCategory = window.getFileTypeCategory ? window.getFileTypeCategory(item.file.name) : '';
892
1013
  let statusText;
893
1014
  if (item.status === 'uploading') {
894
1015
  const pct = Math.round(item.progress || 0);
@@ -904,8 +1025,8 @@ function renderUploadQueue() {
904
1025
  <div class="upload-item" data-id="${item.id}">
905
1026
  <div class="meta">
906
1027
  <div class="upload-item-meta-row">
907
- <div class="file-icon"><i class="fas fa-file"></i></div>
908
- <div class="name">${item.file.name}</div>
1028
+ <div class="file-icon upload-file-icon ${typeCategory}"><i class="${iconClass}"></i></div>
1029
+ <div class="name upload-item-name" title="${safeName}">${safeName}</div>
909
1030
  <div class="form-text file-size upload-item-size-label">${formatFileSize(item.file.size)}</div>
910
1031
  </div>
911
1032
  </div>
@@ -942,7 +1063,7 @@ function renderUploadQueue() {
942
1063
  // Tell S3 to discard the incomplete multipart upload
943
1064
  if (entry && entry.s3UploadId && entry.s3Key) {
944
1065
  try {
945
- await fetch('/mbkbucket/upload-abort', {
1066
+ await fetch(withBucketUrl('/mbkbucket/upload-abort'), {
946
1067
  method: 'POST',
947
1068
  headers: { 'Content-Type': 'application/json' },
948
1069
  body: JSON.stringify({ uploadId: entry.s3UploadId, key: entry.s3Key })
@@ -988,7 +1109,7 @@ async function startUploadEntry(entry) {
988
1109
  // S3 Multipart Upload
989
1110
  // Step 1 — Initiate: get uploadId + key from the server
990
1111
  // ---------------------------------------------------------------
991
- const initResp = await fetch('/mbkbucket/upload-init', {
1112
+ const initResp = await fetch(withBucketUrl('/mbkbucket/upload-init'), {
992
1113
  method: 'POST',
993
1114
  headers: { 'Content-Type': 'application/json' },
994
1115
  body: JSON.stringify({
@@ -1015,7 +1136,7 @@ async function startUploadEntry(entry) {
1015
1136
  const _chunkStart = Date.now();
1016
1137
  if (entry.status === 'cancelled') {
1017
1138
  // Clean up on S3
1018
- await fetch('/mbkbucket/upload-abort', {
1139
+ await fetch(withBucketUrl('/mbkbucket/upload-abort'), {
1019
1140
  method: 'POST',
1020
1141
  headers: { 'Content-Type': 'application/json' },
1021
1142
  body: JSON.stringify({ uploadId, key })
@@ -1036,7 +1157,7 @@ async function startUploadEntry(entry) {
1036
1157
  const controller = new AbortController();
1037
1158
  xhrMap.set(entry.id, { controller, s3UploadId: uploadId, s3Key: key });
1038
1159
 
1039
- const chunkResp = await fetch('/mbkbucket/upload-chunk', {
1160
+ const chunkResp = await fetch(withBucketUrl('/mbkbucket/upload-chunk'), {
1040
1161
  method: 'POST',
1041
1162
  body: fd,
1042
1163
  signal: controller.signal
@@ -1056,7 +1177,7 @@ async function startUploadEntry(entry) {
1056
1177
  // ---------------------------------------------------------------
1057
1178
  // Step 3 — Complete: assemble all parts on S3
1058
1179
  // ---------------------------------------------------------------
1059
- const completeResp = await fetch('/mbkbucket/upload-complete', {
1180
+ const completeResp = await fetch(withBucketUrl('/mbkbucket/upload-complete'), {
1060
1181
  method: 'POST',
1061
1182
  headers: { 'Content-Type': 'application/json' },
1062
1183
  body: JSON.stringify({ uploadId, key, parts })
@@ -1076,7 +1197,7 @@ async function startUploadEntry(entry) {
1076
1197
  fd.append('prefix', currentPrefix);
1077
1198
 
1078
1199
  const xhr = new XMLHttpRequest();
1079
- xhr.open('POST', '/mbkbucket/upload');
1200
+ xhr.open('POST', withBucketUrl('/mbkbucket/upload'));
1080
1201
 
1081
1202
  xhr.upload.addEventListener('progress', (e) => {
1082
1203
  if (!e.lengthComputable) return;
@@ -1123,7 +1244,7 @@ async function startUploadEntry(entry) {
1123
1244
  console.error('Upload failed for', entry.file.name, err);
1124
1245
  // If a multipart upload was initiated but didn't finish (not a user cancel), abort it on S3
1125
1246
  if (entry.s3UploadId && entry.s3Key && entry.status !== 'cancelled') {
1126
- fetch('/mbkbucket/upload-abort', {
1247
+ fetch(withBucketUrl('/mbkbucket/upload-abort'), {
1127
1248
  method: 'POST',
1128
1249
  headers: { 'Content-Type': 'application/json' },
1129
1250
  body: JSON.stringify({ uploadId: entry.s3UploadId, key: entry.s3Key })
@@ -1151,20 +1272,33 @@ document.getElementById('clearQueueBtn')?.addEventListener('click', function ()
1151
1272
  function initializeUploadCollapse() {
1152
1273
  const toggleBtn = document.getElementById('uploadToggleBtn');
1153
1274
  const uploadSection = document.getElementById('uploadSection');
1275
+ const uploadCard = uploadSection?.closest('.dashboard-card');
1154
1276
 
1155
1277
  if (!toggleBtn || !uploadSection) return;
1278
+
1279
+ const syncUploadCollapseState = (expanded) => {
1280
+ uploadSection.classList.toggle('expanded', expanded);
1281
+ toggleBtn.classList.toggle('expanded', expanded);
1282
+ toggleBtn.setAttribute('aria-expanded', expanded ? 'true' : 'false');
1283
+ uploadSection.setAttribute('aria-hidden', expanded ? 'false' : 'true');
1284
+ if (uploadCard) uploadCard.classList.toggle('upload-panel-open', expanded);
1285
+ };
1286
+
1287
+ toggleBtn.setAttribute('role', 'button');
1288
+ toggleBtn.setAttribute('tabindex', '0');
1289
+ toggleBtn.setAttribute('aria-controls', 'uploadSection');
1290
+ syncUploadCollapseState(uploadSection.classList.contains('expanded'));
1156
1291
 
1157
1292
  toggleBtn.addEventListener('click', function() {
1158
1293
  const isExpanded = uploadSection.classList.contains('expanded');
1159
-
1160
- if (isExpanded) {
1161
- // Collapse
1162
- uploadSection.classList.remove('expanded');
1163
- toggleBtn.classList.remove('expanded');
1164
- } else {
1165
- // Expand
1166
- uploadSection.classList.add('expanded');
1167
- toggleBtn.classList.add('expanded');
1294
+ syncUploadCollapseState(!isExpanded);
1295
+ });
1296
+
1297
+ toggleBtn.addEventListener('keydown', function (event) {
1298
+ if (event.key === 'Enter' || event.key === ' ') {
1299
+ event.preventDefault();
1300
+ const isExpanded = uploadSection.classList.contains('expanded');
1301
+ syncUploadCollapseState(!isExpanded);
1168
1302
  }
1169
1303
  });
1170
1304
  }
@@ -1173,12 +1307,61 @@ function initializeUploadCollapse() {
1173
1307
  document.addEventListener('DOMContentLoaded', function () {
1174
1308
  // Initialize collapsible upload section
1175
1309
  initializeUploadCollapse();
1310
+
1311
+ const bucketSelectorEl = document.getElementById('bucketSelector');
1312
+ const bucketSwitchStatusEl = document.getElementById('bucketSwitchStatus');
1313
+ if (bucketSelectorEl && bucketSelectorEl.value) {
1314
+ activeBucket = bucketSelectorEl.value;
1315
+ }
1316
+ if (bucketSelectorEl) {
1317
+ bucketSelectorEl.addEventListener('change', function () {
1318
+ const nextBucket = String(this.value || '').trim();
1319
+ if (!nextBucket) return;
1320
+ if (nextBucket === activeBucket) return;
1321
+
1322
+ const nextParams = new URLSearchParams(window.location.search);
1323
+ nextParams.set('bucket', nextBucket);
1324
+ nextParams.delete('page');
1325
+ nextParams.delete('token');
1326
+
1327
+ this.disabled = true;
1328
+ this.setAttribute('aria-busy', 'true');
1329
+ if (bucketSwitchStatusEl) bucketSwitchStatusEl.style.display = 'inline';
1330
+
1331
+ const qs = nextParams.toString();
1332
+ window.location.href = `/mbkbucket${qs ? `?${qs}` : ''}`;
1333
+ });
1334
+ }
1176
1335
 
1177
- // Load initial files (support both legacy 'prefix' and new 'folder' query param)
1336
+ // Load initial files with persisted state.
1178
1337
  const urlParams = new URLSearchParams(window.location.search);
1179
1338
  const initialPrefix = urlParams.get('folder') || urlParams.get('prefix') || '';
1180
1339
  const initialPage = parseInt(urlParams.get('page')) || 1;
1181
- loadFiles(initialPage, initialPrefix);
1340
+ const initialSearch = urlParams.get('search') || '';
1341
+ const initialSort = (urlParams.get('sort') || 'name').toLowerCase();
1342
+ const initialOrder = (urlParams.get('order') || 'asc').toLowerCase();
1343
+ const initialView = (urlParams.get('view') || currentViewMode).toLowerCase();
1344
+ const initialFilter = (urlParams.get('filter') || 'all').toLowerCase();
1345
+
1346
+ if (initialView === 'folder' || initialView === 'flat') {
1347
+ currentViewMode = initialView;
1348
+ }
1349
+ currentQuickFilter = ['all', 'media', 'docs', 'code'].includes(initialFilter) ? initialFilter : 'all';
1350
+ updateQuickFilterUI();
1351
+
1352
+ // Ensure view button state reflects restored mode before first render.
1353
+ if (currentViewMode === 'flat') {
1354
+ const btnFolder = document.getElementById('viewBtnFolder');
1355
+ const btnFlat = document.getElementById('viewBtnFlat');
1356
+ btnFlat?.classList.add('active', 'btn-light');
1357
+ btnFlat?.classList.remove('btn-outline-light');
1358
+ btnFolder?.classList.remove('active', 'btn-light');
1359
+ btnFolder?.classList.add('btn-outline-light');
1360
+ }
1361
+
1362
+ const searchInputInit = document.getElementById('searchInput');
1363
+ if (searchInputInit) searchInputInit.value = initialSearch;
1364
+ loadFiles(initialPage, initialPrefix, initialSearch, initialSort, initialOrder);
1182
1365
 
1183
1366
  // Search behavior: Trigger when the Search button is clicked OR when the user presses Enter in the search input
1184
1367
  const searchInputEl = document.getElementById('searchInput');
@@ -1201,6 +1384,13 @@ document.addEventListener('DOMContentLoaded', function () {
1201
1384
 
1202
1385
  // Delegate view and delete button clicks to avoid inline JS with unescaped data
1203
1386
  document.addEventListener('click', function (e) {
1387
+ const folderRow = e.target.closest('.folder-row');
1388
+ if (folderRow && !e.target.closest('.delete-btn')) {
1389
+ const folderPath = folderRow.dataset.folderPath;
1390
+ if (folderPath) loadFiles(1, folderPath);
1391
+ return;
1392
+ }
1393
+
1204
1394
  const vbtn = e.target.closest('.view-btn');
1205
1395
  if (vbtn) {
1206
1396
  const keyEnc = vbtn.dataset.keyEnc;
@@ -1220,12 +1410,11 @@ document.addEventListener('DOMContentLoaded', function () {
1220
1410
  }
1221
1411
 
1222
1412
  const cbtn = e.target.closest('.copy-btn');
1223
- console.log(cbtn);
1224
1413
  if (cbtn) {
1225
1414
  e.stopPropagation();
1226
1415
  const keyEnc = cbtn.dataset.keyEnc;
1227
1416
  if (!keyEnc) return;
1228
- const link = `${window.location.origin}/mbkbucket/view/${keyEnc}`;
1417
+ const link = `${window.location.origin}${getViewUrl(keyEnc)}`;
1229
1418
  const fallbackCopy = () => {
1230
1419
  try {
1231
1420
  const ta = document.createElement('textarea');
@@ -1302,7 +1491,7 @@ function handleViewFile(key, filename, event) {
1302
1491
  function showPreviewModal(encodedKey, filename, extension, previewMode = 'auto') {
1303
1492
  const modal = document.getElementById('previewModal');
1304
1493
  if (!modal) {
1305
- window.open(`/mbkbucket/view/${encodedKey}`, '_blank', 'noopener,noreferrer');
1494
+ window.open(getViewUrl(encodedKey), '_blank', 'noopener,noreferrer');
1306
1495
  return;
1307
1496
  }
1308
1497
 
@@ -1316,8 +1505,9 @@ function showPreviewModal(encodedKey, filename, extension, previewMode = 'auto')
1316
1505
  const fsBtn = document.getElementById('previewFullscreenBtn');
1317
1506
 
1318
1507
  titleEl.textContent = filename;
1319
- openBtn.href = `/mbkbucket/view/${encodedKey}`;
1320
- downloadBtn.href = `/mbkbucket/download/${encodedKey}`;
1508
+ titleEl.title = filename;
1509
+ openBtn.href = getViewUrl(encodedKey);
1510
+ downloadBtn.href = getDownloadUrl(encodedKey);
1321
1511
  // Ensure downloaded file has the original filename
1322
1512
  downloadBtn.setAttribute('download', filename);
1323
1513
 
@@ -1345,7 +1535,7 @@ function showPreviewModal(encodedKey, filename, extension, previewMode = 'auto')
1345
1535
  dialog.classList.add('preview-compact');
1346
1536
  const img = document.createElement('img');
1347
1537
  img.className = 'preview-img';
1348
- img.src = `/mbkbucket/view/${encodedKey}`;
1538
+ img.src = getViewUrl(encodedKey);
1349
1539
  img.alt = filename;
1350
1540
  img.onload = () => { };
1351
1541
  img.onerror = () => { contentEl.textContent = 'Could not load preview.'; };
@@ -1356,7 +1546,7 @@ function showPreviewModal(encodedKey, filename, extension, previewMode = 'auto')
1356
1546
  const video = document.createElement('video');
1357
1547
  video.className = 'preview-video';
1358
1548
  video.controls = true;
1359
- video.src = `/mbkbucket/view/${encodedKey}`;
1549
+ video.src = getViewUrl(encodedKey);
1360
1550
  contentEl.innerHTML = '';
1361
1551
  contentEl.appendChild(video);
1362
1552
  } else if (effectiveMode === 'audio') {
@@ -1364,20 +1554,20 @@ function showPreviewModal(encodedKey, filename, extension, previewMode = 'auto')
1364
1554
  const audio = document.createElement('audio');
1365
1555
  audio.className = 'preview-audio';
1366
1556
  audio.controls = true;
1367
- audio.src = `/mbkbucket/view/${encodedKey}`;
1557
+ audio.src = getViewUrl(encodedKey);
1368
1558
  contentEl.innerHTML = '';
1369
1559
  contentEl.appendChild(audio);
1370
1560
  } else if (effectiveMode === 'pdf') {
1371
1561
  dialog.classList.add('preview-large');
1372
1562
  const iframe = document.createElement('iframe');
1373
1563
  iframe.className = 'preview-iframe';
1374
- iframe.src = `/mbkbucket/view/${encodedKey}`;
1564
+ iframe.src = getViewUrl(encodedKey);
1375
1565
  contentEl.innerHTML = '';
1376
1566
  contentEl.appendChild(iframe);
1377
1567
  } else if (effectiveMode === 'text') {
1378
1568
  dialog.classList.add('preview-compact');
1379
1569
 
1380
- fetch(`/mbkbucket/view/${encodedKey}`)
1570
+ fetch(getViewUrl(encodedKey))
1381
1571
  .then(r => r.text())
1382
1572
  .then(text => {
1383
1573
  contentEl.innerHTML = '';
@@ -1388,7 +1578,7 @@ function showPreviewModal(encodedKey, filename, extension, previewMode = 'auto')
1388
1578
  })
1389
1579
  .catch(err => { contentEl.textContent = 'Could not load preview.'; });
1390
1580
  } else {
1391
- contentEl.innerHTML = `<div style="text-align:center;">Preview not available for this file type. <a href="/mbkbucket/download/${encodedKey}">Download</a></div>`;
1581
+ contentEl.innerHTML = `<div style="text-align:center;">Preview not available for this file type. <a href="${getDownloadUrl(encodedKey)}">Download</a></div>`;
1392
1582
  }
1393
1583
 
1394
1584
  // Show modal
@@ -1567,7 +1757,7 @@ function setupSelection() {
1567
1757
  if (!confirm('Delete selected files? This cannot be undone.')) return;
1568
1758
  const keys = Array.from(selected);
1569
1759
  try {
1570
- const resp = await fetch('/mbkbucket/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ keys }) });
1760
+ const resp = await fetch(withBucketUrl('/mbkbucket/delete'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ keys }) });
1571
1761
  const json = await resp.json();
1572
1762
  if (json.success) {
1573
1763
  showAlert('Deleted selected files', 'success');
@@ -1590,7 +1780,7 @@ async function createFolder() {
1590
1780
  if (!safe) return showAlert('Invalid folder name', 'error');
1591
1781
 
1592
1782
  try {
1593
- const resp = await fetch('/mbkbucket/create-folder', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prefix: currentPrefix, folderName: safe }) });
1783
+ const resp = await fetch(withBucketUrl('/mbkbucket/create-folder'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prefix: currentPrefix, folderName: safe }) });
1594
1784
  const json = await resp.json();
1595
1785
  if (json.success) {
1596
1786
  showAlert('Folder created', 'success');
@@ -1626,7 +1816,7 @@ async function createFile() {
1626
1816
  formData.append('prefix', currentPrefix);
1627
1817
 
1628
1818
  try {
1629
- const resp = await fetch('/mbkbucket/upload', {
1819
+ const resp = await fetch(withBucketUrl('/mbkbucket/upload'), {
1630
1820
  method: 'POST',
1631
1821
  body: formData
1632
1822
  });