mbkbucket 1.5.1 → 2.1.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,32 @@ 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
+ const MEDIA_EXTENSIONS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'mp4', 'webm', 'ogg', 'avi', 'mov', 'mp3', 'wav', 'flac', 'aac', 'm4a']);
23
+ const DOC_EXTENSIONS = new Set(['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'md', 'csv']);
24
+ const CODE_EXTENSIONS = new Set(['js', 'ts', 'html', 'htm', 'css', 'php', 'py', 'java', 'cpp', 'c', 'h', 'cs', 'rb', 'go', 'rs', 'sql', 'sh', 'bat', 'ps1', 'yaml', 'yml', 'toml', 'ini', 'conf', 'json', 'xml', 'log']);
25
+
26
+ function withBucketUrl(url) {
27
+ if (!activeBucket) return url;
28
+ const separator = url.includes('?') ? '&' : '?';
29
+ return `${url}${separator}bucket=${encodeURIComponent(activeBucket)}`;
30
+ }
31
+
32
+ function getViewUrl(encodedKey) {
33
+ return withBucketUrl(`/mbkbucket/view/${encodedKey}`);
34
+ }
35
+
36
+ function getDownloadUrl(encodedKey) {
37
+ return withBucketUrl(`/mbkbucket/download/${encodedKey}`);
38
+ }
14
39
 
15
40
  function stripApiPrefixFromPath(path, apiPrefix = '') {
16
41
  const normalizedPath = String(path || '').replace(/^\/+/, '');
@@ -37,6 +62,74 @@ function toApiFolderPath(prefix = '', apiPrefix = '') {
37
62
  return `${normalizedApiPrefix}/${uiPrefix}`;
38
63
  }
39
64
 
65
+ function escapeHtml(value = '') {
66
+ return String(value)
67
+ .replace(/&/g, '&')
68
+ .replace(/</g, '&lt;')
69
+ .replace(/>/g, '&gt;')
70
+ .replace(/"/g, '&quot;')
71
+ .replace(/'/g, '&#39;');
72
+ }
73
+
74
+ function escapeAttr(value = '') {
75
+ return escapeHtml(value);
76
+ }
77
+
78
+ function getBaseName(path = '') {
79
+ const value = String(path || '');
80
+ const lastSlashIndex = value.lastIndexOf('/');
81
+ return lastSlashIndex === -1 ? value : value.substring(lastSlashIndex + 1);
82
+ }
83
+
84
+ function dataAttr(name, value = '') {
85
+ return `data-${name}="${escapeAttr(value)}"`;
86
+ }
87
+
88
+ function mutedDash() {
89
+ return '<span class="muted-cell-value">&mdash;</span>';
90
+ }
91
+
92
+ function syncStateUrl(page = currentPage, prefix = currentPrefix, search = currentSearch) {
93
+ try {
94
+ const parts = [];
95
+ if (activeBucket) parts.push(`bucket=${encodeURIComponent(activeBucket)}`);
96
+ if (prefix) {
97
+ const folderPathForUrl = toApiFolderPath(prefix, currentApiPrefix);
98
+ const folderEncoded = encodeURIComponent(folderPathForUrl).replace(/%2F/g, '/');
99
+ parts.push(`folder=${folderEncoded}`);
100
+ }
101
+ if (page && page > 1) parts.push(`page=${page}`);
102
+ if (search) parts.push(`search=${encodeURIComponent(search)}`);
103
+ parts.push(`sort=${encodeURIComponent(currentSort)}`);
104
+ parts.push(`order=${encodeURIComponent(currentOrder)}`);
105
+ parts.push(`view=${encodeURIComponent(currentViewMode)}`);
106
+ parts.push(`filter=${encodeURIComponent(currentQuickFilter)}`);
107
+
108
+ const newUrl = '/mbkbucket' + (parts.length ? '?' + parts.join('&') : '');
109
+ window.history.replaceState(null, '', newUrl);
110
+ } catch (e) {
111
+ console.warn('history update failed', e);
112
+ }
113
+ }
114
+
115
+ function getActiveBucketLabel() {
116
+ return activeBucket || 'default bucket';
117
+ }
118
+
119
+ function matchesQuickFilter(fileName = '', filter = currentQuickFilter) {
120
+ const ext = getBaseName(fileName).split('.').pop().toLowerCase();
121
+ if (filter === 'media') return MEDIA_EXTENSIONS.has(ext);
122
+ if (filter === 'docs') return DOC_EXTENSIONS.has(ext);
123
+ if (filter === 'code') return CODE_EXTENSIONS.has(ext);
124
+ return true;
125
+ }
126
+
127
+ function updateQuickFilterUI() {
128
+ document.querySelectorAll('.quick-filter-btn').forEach((btn) => {
129
+ btn.classList.toggle('active', btn.dataset.filter === currentQuickFilter);
130
+ });
131
+ }
132
+
40
133
  // Show alert message (Toast)
41
134
  function showAlert(message, type = 'success') {
42
135
  const container = document.getElementById('toast-container');
@@ -75,9 +168,9 @@ function showAlert(message, type = 'success') {
75
168
  `;
76
169
 
77
170
  toast.innerHTML = `
78
- <i class="fas ${isSuccess ? 'fa-check-circle' : 'fa-exclamation-circle'}" style="font-size: 1.25rem;"></i>
79
- <div style="flex: 1;">${message}</div>
80
- <button onclick="this.parentElement.remove()" class="toast-close-btn">&times;</button>
171
+ <i class="fas ${isSuccess ? 'fa-check-circle' : 'fa-exclamation-circle'} toast-icon"></i>
172
+ <div class="toast-message">${message}</div>
173
+ <button type="button" data-action="close-toast" class="toast-close-btn">&times;</button>
81
174
  `;
82
175
 
83
176
  container.appendChild(toast);
@@ -103,26 +196,10 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
103
196
  currentSearch = search;
104
197
  currentSort = sort;
105
198
  currentOrder = order;
199
+ virtualRenderLimit = 0;
106
200
 
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
- }
201
+ // Update browser URL so navigation is shareable/bookmarkable.
202
+ syncStateUrl(page, prefix, search);
126
203
 
127
204
  const container = document.getElementById('fileListContainer');
128
205
  const paginationContainer = document.getElementById('paginationContainer');
@@ -157,7 +234,7 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
157
234
  const recursiveParam = `&recursive=${!isFolderMode}`; // false if folder mode, true if flat mode
158
235
  const tokenParam = token ? `&token=${encodeURIComponent(token)}` : '';
159
236
 
160
- const response = await fetch(`/mbkbucket/api/files?page=${page}&prefix=${encodeURIComponent(prefix)}${searchParam}${sortParam}${recursiveParam}${tokenParam}`);
237
+ const response = await fetch(withBucketUrl(`/mbkbucket/api/files?page=${page}&prefix=${encodeURIComponent(prefix)}${searchParam}${sortParam}${recursiveParam}${tokenParam}`));
161
238
  const data = await response.json();
162
239
 
163
240
  if (!data.success) {
@@ -176,11 +253,13 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
176
253
  }
177
254
 
178
255
  if (search) {
179
- filterInfo.textContent = `Search results for: "${search}"`;
256
+ const filterSuffix = currentQuickFilter !== 'all' ? ` • Filter: ${currentQuickFilter}` : '';
257
+ filterInfo.textContent = `Bucket: ${getActiveBucketLabel()} • Search: "${search}"${filterSuffix}`;
180
258
  fileListTitle.innerHTML = `Search Results <span class="text-muted">for: </span><span class="badge bg-info">${search}</span>`;
181
259
  } else {
182
- const label = prefix || bucketAppName || 'app';
183
- filterInfo.textContent = `Filtered by folder: ${label}`;
260
+ const label = prefix || rootLabel;
261
+ const filterSuffix = currentQuickFilter !== 'all' ? ` • Filter: ${currentQuickFilter}` : '';
262
+ filterInfo.textContent = `Bucket: ${getActiveBucketLabel()} • Folder: ${label}${filterSuffix}`;
184
263
  fileListTitle.innerHTML = `Files <span class="text-muted">in: </span><span class="badge bg-info">${label}</span>`;
185
264
  }
186
265
 
@@ -192,8 +271,8 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
192
271
  <div class="empty-state">
193
272
  <i class="fas fa-folder-open"></i>
194
273
  <h4>No files found</h4>
195
- <p>Your bucket is empty. Upload some files to get started!</p>
196
- <button class="btn btn-lg" onclick="document.getElementById('fileInput').click()">
274
+ <p>Bucket <strong>${escapeHtml(getActiveBucketLabel())}</strong> is empty. Upload some files to get started!</p>
275
+ <button class="btn btn-lg" data-action="upload-files">
197
276
  <i class="fas fa-plus"></i> Upload Your First File
198
277
  </button>
199
278
  </div>
@@ -221,10 +300,10 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
221
300
  console.error('Error loading files:', error);
222
301
  container.innerHTML = `
223
302
  <div class="empty-state">
224
- <i class="fas fa-exclamation-triangle" style="color: var(--danger-color-ml);"></i>
303
+ <i class="fas fa-exclamation-triangle empty-state-error-icon"></i>
225
304
  <h4>Error Loading Files</h4>
226
- <p>${error.message}</p>
227
- <button class="btn btn-primary" onclick="loadFiles(${page}, '${prefix}')">
305
+ <p>Bucket <strong>${escapeHtml(getActiveBucketLabel())}</strong>: ${escapeHtml(error.message)}</p>
306
+ <button class="btn btn-primary" data-action="retry-load" ${dataAttr('retry-page', page)} ${dataAttr('retry-prefix', prefix)}>
228
307
  <i class="fas fa-redo"></i> Retry
229
308
  </button>
230
309
  </div>
@@ -253,8 +332,8 @@ function toggleSort(field) {
253
332
  valA = new Date(a.LastModified || 0).getTime();
254
333
  valB = new Date(b.LastModified || 0).getTime();
255
334
  } else {
256
- valA = (a.Key || '').split('/').pop().toLowerCase();
257
- valB = (b.Key || '').split('/').pop().toLowerCase();
335
+ valA = getBaseName(a.Key).toLowerCase();
336
+ valB = getBaseName(b.Key).toLowerCase();
258
337
  }
259
338
 
260
339
  if (valA < valB) return currentOrder === 'asc' ? -1 : 1;
@@ -263,6 +342,7 @@ function toggleSort(field) {
263
342
  });
264
343
 
265
344
  renderFilesTable(pageFiles, pageFolders, currentPrefix, currentApiPrefix);
345
+ syncStateUrl(currentPage, currentPrefix, currentSearch);
266
346
  }
267
347
  }
268
348
 
@@ -281,10 +361,18 @@ function clearSearch() {
281
361
  loadFiles(1, currentPrefix, '');
282
362
  }
283
363
 
364
+ function setQuickFilter(filter = 'all') {
365
+ const nextFilter = String(filter || 'all').toLowerCase();
366
+ if (currentQuickFilter === nextFilter) return;
367
+ currentQuickFilter = nextFilter;
368
+ updateQuickFilterUI();
369
+ renderFilesTable(pageFiles, pageFolders, currentPrefix, currentApiPrefix);
370
+ syncStateUrl(currentPage, currentPrefix, currentSearch);
371
+ }
372
+
284
373
  // Extract unique folders at current level
285
374
  function extractFoldersAtCurrentLevel(files, prefix) {
286
375
  const folderSet = new Set();
287
- const prefixDepth = prefix ? prefix.split('/').filter(p => p).length : 0;
288
376
 
289
377
  files.forEach(file => {
290
378
  const key = file.Key;
@@ -330,8 +418,14 @@ function changeViewMode(mode) {
330
418
  btnFolder.classList.add('btn-outline-light');
331
419
  }
332
420
 
333
- // Re-render with existing data
334
- renderFilesTable(pageFiles, pageFolders, currentPrefix, currentApiPrefix);
421
+ // Refetch to respect recursive/non-recursive mode and keep URL state.
422
+ loadFiles(1, currentPrefix, currentSearch, currentSort, currentOrder);
423
+ }
424
+
425
+ function expandVirtualRows() {
426
+ if (!lastRenderContext) return;
427
+ virtualRenderLimit += VIRTUAL_RENDER_BATCH;
428
+ renderFilesTable(lastRenderContext.files, lastRenderContext.folders, lastRenderContext.prefix, lastRenderContext.apiPrefix);
335
429
  }
336
430
 
337
431
  // Extract folders that match a search query from file paths
@@ -364,6 +458,11 @@ function extractMatchingFolders(files, search) {
364
458
 
365
459
  // Render files table
366
460
  function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
461
+ lastRenderContext = { files, folders, prefix, apiPrefix };
462
+ const normalizedPrefix = String(prefix || '').replace(/\/+$/, '');
463
+ const basePrefix = apiPrefix || prefix;
464
+ const relativeBaseOffset = basePrefix ? basePrefix.length + (basePrefix.endsWith('/') ? 0 : 1) : 0;
465
+
367
466
  // Determine which files/folders to show based on view mode
368
467
  let displayFiles = [];
369
468
  let displayFolders = [];
@@ -382,7 +481,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
382
481
  // Skip folder marker objects (keys ending with /)
383
482
  if (f.Key.endsWith('/')) return false;
384
483
 
385
- const fileName = f.Key.split('/').pop();
484
+ const fileName = getBaseName(f.Key);
386
485
  return fileName.toLowerCase().includes(searchLower);
387
486
  });
388
487
 
@@ -401,27 +500,34 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
401
500
  // Skip folder marker objects (keys ending with /)
402
501
  if (file.Key.endsWith('/')) return false;
403
502
 
404
- const basePrefix = apiPrefix || prefix;
405
- const relativePath = basePrefix ? file.Key.substring(basePrefix.length + (basePrefix.endsWith('/') ? 0 : 1)) : file.Key;
503
+ const relativePath = basePrefix ? file.Key.substring(relativeBaseOffset) : file.Key;
406
504
  // File is at current level if it has no '/' in relative path
407
505
  return !relativePath.includes('/');
408
506
  });
409
507
  displayFolders = folders;
410
508
  }
411
509
 
510
+ // Apply quick file-type filter after mode/path filtering.
511
+ displayFiles = displayFiles.filter((f) => {
512
+ const name = getBaseName(f?.Key);
513
+ return matchesQuickFilter(name, currentQuickFilter);
514
+ });
515
+
412
516
  // Generate breadcrumb navigation
413
517
  let breadcrumb = '';
414
518
  // Show breadcrumb only if we are in folder mode OR if we have a prefix filter active even in flat mode
415
519
  // (In flat mode, the prefix acts as a base filter, but we see recursive files under it)
416
520
  if (prefix) {
417
521
  const parts = prefix.split('/').filter(p => p);
418
- let breadcrumbHTML = `
522
+ const safeRootLabel = escapeHtml(rootLabel);
523
+ const safeRootTitle = escapeAttr(rootLabel);
524
+ const breadcrumbParts = [`
419
525
  <div class="breadcrumb-nav">
420
526
  <div class="breadcrumb-item">
421
527
  <i class="fas fa-home"></i>
422
- <span class="breadcrumb-link" onclick="loadFiles(1, '')" title="Go to ${bucketAppName || 'app'}">${bucketAppName || 'app'}</span>
528
+ <button type="button" class="breadcrumb-link" ${dataAttr('nav-prefix', '')} title="Go to ${safeRootTitle}">${safeRootLabel}</button>
423
529
  </div>
424
- `;
530
+ `];
425
531
 
426
532
  let currentPath = '';
427
533
  parts.forEach((part, index) => {
@@ -429,62 +535,73 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
429
535
  const isLast = index === parts.length - 1;
430
536
 
431
537
  if (isLast) {
432
- breadcrumbHTML += `
538
+ const safePart = escapeHtml(part);
539
+ breadcrumbParts.push(`
433
540
  <span class="breadcrumb-separator"><i class="fas fa-chevron-right"></i></span>
434
541
  <div class="breadcrumb-item">
435
- <span class="breadcrumb-current">${part}</span>
542
+ <span class="breadcrumb-current">${safePart}</span>
436
543
  </div>
437
- `;
544
+ `);
438
545
  } else {
439
546
  const pathCopy = currentPath;
440
- breadcrumbHTML += `
547
+ const safePart = escapeHtml(part);
548
+ const safePathTitle = escapeAttr(pathCopy);
549
+ breadcrumbParts.push(`
441
550
  <span class="breadcrumb-separator"><i class="fas fa-chevron-right"></i></span>
442
551
  <div class="breadcrumb-item">
443
- <span class="breadcrumb-link" onclick="loadFiles(1, '${pathCopy}')" title="Go to ${pathCopy}">${part}</span>
552
+ <button type="button" class="breadcrumb-link" ${dataAttr('nav-prefix', pathCopy)} title="Go to ${safePathTitle}">${safePart}</button>
444
553
  </div>
445
- `;
554
+ `);
446
555
  }
447
556
  });
448
557
 
449
- breadcrumbHTML += `</div>`;
450
- breadcrumb = breadcrumbHTML;
558
+ breadcrumbParts.push(`</div>`);
559
+ breadcrumb = breadcrumbParts.join('');
451
560
  }
452
561
 
453
562
  // Generate folder rows (only used in folder mode)
454
- const folderRows = displayFolders.map(folderPath => {
563
+ const folderRows = displayFolders.map(folderPath => {
455
564
  // Handle folder paths which typically end in a slash (e.g., "folder/subfolder/")
456
- const cleanPath = folderPath.replace(/\/+$/, '');
457
- const folderName = cleanPath.split('/').pop();
565
+ const rawFolderPath = String(folderPath || '');
566
+ const cleanPath = rawFolderPath.replace(/\/+$/, '');
567
+ const folderPathForAction = normalizedPrefix && cleanPath && !cleanPath.startsWith(`${normalizedPrefix}/`)
568
+ ? `${normalizedPrefix}/${cleanPath}`
569
+ : cleanPath;
570
+ const folderKeyForDelete = `${folderPathForAction}/`;
571
+ const folderName = getBaseName(cleanPath);
572
+ const safeFolderName = escapeHtml(folderName);
573
+ const safeFolderPathAttr = escapeAttr(`${folderPathForAction}/`);
574
+ const safeFolderDeleteKeyAttr = escapeAttr(folderKeyForDelete);
458
575
 
459
576
  return `
460
- <tr class="folder-row" onclick="loadFiles(1, '${folderPath}')" title="Open folder: ${folderName}">
461
- <td style="text-align:center;">
577
+ <tr class="folder-row" data-folder-path="${safeFolderPathAttr}" title="Open folder: ${safeFolderName}">
578
+ <td class="table-cell-center">
462
579
  <!-- no selection for folders -->
463
580
  </td>
464
- <td style="padding-left: 1.5rem;">
465
- <div style="display: flex; align-items: center;">
581
+ <td class="file-name-cell">
582
+ <div class="file-row-main">
466
583
  <div class="file-icon">
467
584
  <i class="fas fa-folder"></i>
468
585
  </div>
469
586
  <div class="file-info">
470
- <div class="file-name" style="font-weight: 700;">
471
- <i class="fas fa-folder" style="margin-right: 0.5rem; color: #f59e0b;"></i>${folderName}/
587
+ <div class="file-name folder-name">
588
+ <i class="fas fa-folder folder-leading-icon"></i>${safeFolderName}/
472
589
  </div>
473
590
  </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">
591
+ <button type="button" class="btn btn-danger btn-sm delete-btn dtm" data-key="${safeFolderDeleteKeyAttr}" data-is-folder="true" title="Delete folder">
475
592
  <i class="fas fa-trash"></i>
476
593
  </button>
477
594
  </div>
478
595
  </td>
479
596
  <td>
480
- <span style="color: var(--gray-color-ml);">\u2014</span>
597
+ ${mutedDash()}
481
598
  </td>
482
- <td style="text-align: center;">
483
- <span style="color: var(--gray-color-ml);">\u2014</span>
599
+ <td class="table-cell-center">
600
+ ${mutedDash()}
484
601
  </td>
485
- <td style="text-align: center;">
602
+ <td class="table-cell-center">
486
603
  <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">
604
+ <button type="button" class="btn btn-danger btn-sm delete-btn" data-key="${safeFolderDeleteKeyAttr}" data-is-folder="true" title="Delete folder">
488
605
  <i class="fas fa-trash"></i>
489
606
  </button>
490
607
  </div>
@@ -493,12 +610,22 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
493
610
  `;
494
611
  }).join('');
495
612
 
613
+ const isVirtualized = displayFiles.length > VIRTUAL_RENDER_THRESHOLD;
614
+ if (!isVirtualized) {
615
+ virtualRenderLimit = 0;
616
+ } else if (!virtualRenderLimit) {
617
+ virtualRenderLimit = VIRTUAL_RENDER_BATCH;
618
+ }
619
+ const visibleFiles = isVirtualized ? displayFiles.slice(0, virtualRenderLimit) : displayFiles;
620
+
496
621
  // Generate file rows
497
- const fileRows = displayFiles.map(file => {
498
- const encodedKey = encodeURIComponent(file.Key);
622
+ const fileRows = visibleFiles.map(file => {
623
+ const rawKey = String(file.Key || '');
624
+ const encodedKey = encodeURIComponent(rawKey);
625
+ const safeKeyAttr = escapeAttr(rawKey);
499
626
 
500
627
  // Calculate display name and icon directly
501
- const fullPath = file.Key;
628
+ const fullPath = rawKey;
502
629
  const lastSlashIndex = fullPath.lastIndexOf('/');
503
630
  let filename = fullPath;
504
631
  let folderPath = '';
@@ -512,50 +639,55 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
512
639
  const typeCategory = window.getFileTypeCategory ? window.getFileTypeCategory(filename) : '';
513
640
  const truncatedName = (window.truncateFileName ? window.truncateFileName(filename) : filename) || filename;
514
641
  const isFileViewable = window.isViewable ? window.isViewable(filename) : false;
642
+ const safeFilename = escapeHtml(filename);
643
+ const safeTruncatedName = escapeHtml(truncatedName);
644
+ const safeFolderPath = escapeHtml(folderPath);
645
+ const viewButtonClass = isFileViewable ? 'btn btn-primary btn-sm view-btn' : 'btn btn-primary btn-sm view-btn is-disabled';
646
+ const viewButtonDisabled = isFileViewable ? '' : 'disabled';
515
647
 
516
648
  // If in flat mode, we might want to show the folder path more prominently
517
649
  const pathDisplay = (currentViewMode === 'flat' && folderPath && folderPath !== prefix + (prefix ? '/' : ''))
518
650
  ? `<div class="file-path-display">
519
- <i class="fas fa-folder" style="margin-right:4px; opacity:0.5;"></i> ${folderPath}
651
+ <i class="fas fa-folder path-display-icon"></i> ${safeFolderPath}
520
652
  </div>`
521
653
  : '';
522
654
 
523
655
  return `
524
656
  <tr class="file-row">
525
- <td style="text-align:center;">
526
- <input type="checkbox" class="selectFileCheckbox" data-key="${file.Key}" aria-label="Select ${file.Key}" />
657
+ <td class="table-cell-center">
658
+ <input type="checkbox" class="selectFileCheckbox" data-key="${safeKeyAttr}" aria-label="Select ${safeKeyAttr}" />
527
659
  </td>
528
- <td style="padding-left: 1.5rem;">
529
- <div style="display: flex; align-items: center;">
660
+ <td class="file-name-cell">
661
+ <div class="file-row-main">
530
662
  <div class="file-icon ${typeCategory}">
531
663
  <i class="${iconClass}"></i>
532
664
  </div>
533
665
  <div class="file-info">
534
666
  ${pathDisplay}
535
- <div class="file-name" data-full-path="${file.Key}" title="${filename}">${truncatedName}</div>
667
+ <div class="file-name" data-full-path="${safeKeyAttr}" title="${safeFilename}">${safeTruncatedName}</div>
536
668
  </div>
537
669
  </div>
538
670
  </td>
539
671
  <td>
540
- ${file.Size ? `<span class="file-size">${formatFileSize(file.Size)}</span>` : '<span style="color: var(--gray-color-ml);">N/A</span>'}
672
+ ${file.Size ? `<span class="file-size">${formatFileSize(file.Size)}</span>` : '<span class="muted-cell-value">N/A</span>'}
541
673
  </td>
542
674
  <td>
543
- ${file.LastModified ? `<span style="color: var(--gray-color-ml);">${formatDate(file.LastModified)}</span>` : '<span style="color: var(--gray-color-ml);">N/A</span>'}
675
+ ${file.LastModified ? `<span class="muted-cell-value">${formatDate(file.LastModified)}</span>` : '<span class="muted-cell-value">N/A</span>'}
544
676
  </td>
545
- <td style="text-align: center;">
677
+ <td class="table-cell-center">
546
678
  <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}"
548
- data-key-enc="${encodedKey}" title="${isFileViewable ? 'View file in browser' : 'File type not supported for viewing'}" ${!isFileViewable ? 'disabled style="opacity:0.5"' : ''}>
679
+ <button type="button" class="${viewButtonClass}" data-key="${safeKeyAttr}" data-filename="${safeKeyAttr}"
680
+ data-key-enc="${encodedKey}" title="${isFileViewable ? 'View file in browser' : 'File type not supported for viewing'}" ${viewButtonDisabled}>
549
681
  <i class="fas fa-eye"></i>
550
682
  </button>
551
- <button type="button" class="btn btn-info btn-sm copy-btn" data-key-enc="${encodedKey}" title="Copy Link" style="color:white;">
683
+ <button type="button" class="btn btn-info btn-sm copy-btn" data-key-enc="${encodedKey}" title="Copy Link">
552
684
  <i class="fas fa-link"></i>
553
685
  </button>
554
- <a href="/mbkbucket/download/${encodedKey}" class="btn btn-success btn-sm"
686
+ <a href="${getDownloadUrl(encodedKey)}" class="btn btn-success btn-sm"
555
687
  title="Download file">
556
688
  <i class="fas fa-download"></i>
557
689
  </a>
558
- <button type="button" class="btn btn-danger btn-sm delete-btn" data-key="${file.Key}" title="Delete file">
690
+ <button type="button" class="btn btn-danger btn-sm delete-btn" data-key="${safeKeyAttr}" title="Delete file">
559
691
  <i class="fas fa-trash"></i>
560
692
  </button>
561
693
  </div>
@@ -571,22 +703,22 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
571
703
  ${breadcrumb}
572
704
  <div class="empty-state">
573
705
  <div class="empty-state-illustration">
574
- <i class="fas fa-folder-open" style="font-size: 4rem; color: #cbd5e0;"></i>
706
+ <i class="fas fa-folder-open empty-state-folder-icon"></i>
575
707
  <i class="fas fa-search empty-state-search-icon"></i>
576
708
  </div>
577
709
  <h4>No files found</h4>
578
710
  <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.'}
711
+ ${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
712
  </p>
581
713
  <div class="empty-state-actions">
582
- <button class="btn btn-primary" onclick="window.scrollTo({top: 0, behavior: 'smooth'}); document.getElementById('fileInput').click()">
714
+ <button class="btn btn-primary" data-action="upload-files">
583
715
  <i class="fas fa-cloud-upload-alt"></i> Upload Files
584
716
  </button>
585
- <button class="btn btn-outline-secondary" onclick="createFolder()">
717
+ <button class="btn btn-outline-secondary" data-action="create-folder">
586
718
  <i class="fas fa-folder-plus"></i> New Folder
587
719
  </button>
588
- ${prefix ? `<button class="btn btn-light" onclick="loadFiles(1, '')">
589
- <i class="fas fa-home"></i> Go to ${bucketAppName || 'app'}
720
+ ${prefix ? `<button class="btn btn-light" data-action="go-root">
721
+ <i class="fas fa-home"></i> Go to ${rootLabel}
590
722
  </button>` : ''}
591
723
  </div>
592
724
  </div>
@@ -602,12 +734,21 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
602
734
  : '<i class="fas fa-sort-down sort-icon sort-icon-active"></i>';
603
735
  };
604
736
 
605
- document.getElementById('fileListContainer').innerHTML = `
737
+ const virtualizedInfo = isVirtualized
738
+ ? `<div class="virtualized-info">
739
+ <span>Showing ${visibleFiles.length} of ${displayFiles.length} files in this view.</span>
740
+ <div class="virtualized-actions">
741
+ ${visibleFiles.length < displayFiles.length ? '<button class="btn btn-sm btn-outline-secondary" data-action="expand-virtual">Load more</button>' : ''}
742
+ </div>
743
+ </div>`
744
+ : '';
745
+
746
+ document.getElementById('fileListContainer').innerHTML = `
606
747
  ${breadcrumb}
607
748
  <div class="table-responsive">
608
749
  <div id="bulkActionsBar" class="bulk-actions-bar">
609
750
  <span id="bulkSelectedCount">0 selected</span>
610
- <div style="float: right;">
751
+ <div class="bulk-actions-controls">
611
752
  <button class="btn btn-danger btn-sm" id="bulkDeleteBtn" disabled>
612
753
  <i class="fas fa-trash"></i> Delete Selected
613
754
  </button>
@@ -616,19 +757,19 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
616
757
  <table class="table table-hover mb-0">
617
758
  <thead>
618
759
  <tr>
619
- <th style="width:48px; text-align:center;">
760
+ <th class="table-select-col">
620
761
  <input type="checkbox" id="selectAllCheckbox" aria-label="Select all files" />
621
762
  </th>
622
- <th style="padding-left: 1.5rem; cursor: pointer;" onclick="toggleSort('name')">
763
+ <th class="sortable-heading file-name-heading" data-sort-field="name">
623
764
  <i class="fas fa-file-alt"></i> Name ${getSortIcon('name')}
624
765
  </th>
625
- <th style="cursor: pointer;" onclick="toggleSort('size')">
766
+ <th class="sortable-heading" data-sort-field="size">
626
767
  <i class="fas fa-weight"></i> Size ${getSortIcon('size')}
627
768
  </th>
628
- <th style="cursor: pointer;" onclick="toggleSort('date')">
769
+ <th class="sortable-heading" data-sort-field="date">
629
770
  <i class="fas fa-calendar"></i> Last Modified ${getSortIcon('date')}
630
771
  </th>
631
- <th style="text-align: center;">
772
+ <th class="table-cell-center">
632
773
  <i class="fas fa-cogs"></i> Actions
633
774
  </th>
634
775
  </tr>
@@ -637,6 +778,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
637
778
  ${allRows}
638
779
  </tbody>
639
780
  </table>
781
+ ${virtualizedInfo}
640
782
  </div>
641
783
  `;
642
784
 
@@ -649,6 +791,17 @@ function renderPagination(data) {
649
791
  const { currentPage, totalPages, hasPrevPage, hasNextPage, mode } = data;
650
792
 
651
793
  let paginationHTML = '';
794
+ const pageLink = (page, label, extra = {}) => {
795
+ const attrs = [
796
+ dataAttr('page', page),
797
+ dataAttr('prefix', currentPrefix),
798
+ dataAttr('search', currentSearch),
799
+ dataAttr('sort', extra.sort || currentSort),
800
+ dataAttr('order', extra.order || currentOrder)
801
+ ];
802
+ if (extra.token) attrs.push(dataAttr('token', extra.token));
803
+ return `<a class="page-link" href="#" ${attrs.join(' ')}>${label}</a>`;
804
+ };
652
805
 
653
806
  if (mode === 'optimized') {
654
807
  // Optimized Mode: Token-based pagination (Next only, limited Prev)
@@ -671,13 +824,9 @@ function renderPagination(data) {
671
824
 
672
825
  // Next button
673
826
  if (hasNextPage) {
674
- // Quote the token correctly
675
- const tokenStr = currentNextToken ? currentNextToken.replace(/'/g, "\\'") : "";
676
827
  paginationHTML += `
677
828
  <li class="page-item">
678
- <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${currentPage + 1}, '${currentPrefix}', '${currentSearch}', '${currentSort}', '${currentOrder}', '${tokenStr}')">
679
- Next <i class="fas fa-chevron-right"></i>
680
- </a>
829
+ ${pageLink(currentPage + 1, 'Next <i class="fas fa-chevron-right"></i>', { token: currentNextToken || '' })}
681
830
  </li>
682
831
  `;
683
832
  } else {
@@ -697,9 +846,7 @@ function renderPagination(data) {
697
846
  if (hasPrevPage) {
698
847
  paginationHTML += `
699
848
  <li class="page-item">
700
- <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${currentPage - 1}, '${currentPrefix}', '${currentSearch}')">
701
- <i class="fas fa-chevron-left"></i> Previous
702
- </a>
849
+ ${pageLink(currentPage - 1, '<i class="fas fa-chevron-left"></i> Previous')}
703
850
  </li>
704
851
  `;
705
852
  } else {
@@ -725,7 +872,7 @@ function renderPagination(data) {
725
872
  if (startPage > 1) {
726
873
  paginationHTML += `
727
874
  <li class="page-item">
728
- <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(1, '${currentPrefix}', '${currentSearch}')">1</a>
875
+ ${pageLink(1, '1')}
729
876
  </li>`;
730
877
  if (startPage > 2) {
731
878
  paginationHTML += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
@@ -742,7 +889,7 @@ function renderPagination(data) {
742
889
  } else {
743
890
  paginationHTML += `
744
891
  <li class="page-item">
745
- <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${i}, '${currentPrefix}', '${currentSearch}')">${i}</a>
892
+ ${pageLink(i, String(i))}
746
893
  </li>
747
894
  `;
748
895
  }
@@ -755,7 +902,7 @@ function renderPagination(data) {
755
902
  }
756
903
  paginationHTML += `
757
904
  <li class="page-item">
758
- <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${totalPages}, '${currentPrefix}', '${currentSearch}')">${totalPages}</a>
905
+ ${pageLink(totalPages, String(totalPages))}
759
906
  </li>`;
760
907
  }
761
908
 
@@ -763,9 +910,7 @@ function renderPagination(data) {
763
910
  if (hasNextPage) {
764
911
  paginationHTML += `
765
912
  <li class="page-item">
766
- <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${currentPage + 1}, '${currentPrefix}', '${currentSearch}')">
767
- Next <i class="fas fa-chevron-right"></i>
768
- </a>
913
+ ${pageLink(currentPage + 1, 'Next <i class="fas fa-chevron-right"></i>')}
769
914
  </li>
770
915
  `;
771
916
  } else {
@@ -819,7 +964,7 @@ document.getElementById('deleteForm')?.addEventListener('submit', async function
819
964
  const isFolder = document.getElementById('deleteIsFolder').value === 'true';
820
965
 
821
966
  try {
822
- const response = await fetch('/mbkbucket/delete', {
967
+ const response = await fetch(withBucketUrl('/mbkbucket/delete'), {
823
968
  method: 'POST',
824
969
  headers: {
825
970
  'Content-Type': 'application/json'
@@ -858,7 +1003,7 @@ setTimeout(() => {
858
1003
  // Upload queue, drag-and-drop and multi-file support
859
1004
  const uploadQueue = []; // { id, file, status, progress, controller/xhr, uploadId }
860
1005
  const xhrMap = new Map();
861
- const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB chunk threshold
1006
+ const CHUNK_SIZE = 50 * 1024 * 1024; // 50MB chunk threshold
862
1007
 
863
1008
  function createId() { return Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 9); }
864
1009
 
@@ -889,6 +1034,9 @@ function renderUploadQueue() {
889
1034
  }
890
1035
 
891
1036
  container.innerHTML = uploadQueue.map(item => {
1037
+ const safeName = escapeHtml(item.file.name);
1038
+ const iconClass = window.getFileIcon ? window.getFileIcon(item.file.name) : 'fas fa-file-alt';
1039
+ const typeCategory = window.getFileTypeCategory ? window.getFileTypeCategory(item.file.name) : '';
892
1040
  let statusText;
893
1041
  if (item.status === 'uploading') {
894
1042
  const pct = Math.round(item.progress || 0);
@@ -904,14 +1052,14 @@ function renderUploadQueue() {
904
1052
  <div class="upload-item" data-id="${item.id}">
905
1053
  <div class="meta">
906
1054
  <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>
1055
+ <div class="file-icon upload-file-icon ${typeCategory}"><i class="${iconClass}"></i></div>
1056
+ <div class="name upload-item-name" title="${safeName}">${safeName}</div>
909
1057
  <div class="form-text file-size upload-item-size-label">${formatFileSize(item.file.size)}</div>
910
1058
  </div>
911
1059
  </div>
912
- <div style="min-width:220px;">
913
- <div class="progress-container" style="height: 8px; margin-bottom: 6px;">
914
- <div class="progress-bar" style="width: ${Math.round(item.progress)}%; height: 8px;"></div>
1060
+ <div class="upload-progress-cell">
1061
+ <div class="progress-container upload-progress-track">
1062
+ <div class="progress-bar upload-progress-bar" style="width: ${Math.round(item.progress)}%;"></div>
915
1063
  </div>
916
1064
  <div class="upload-item-status-row">
917
1065
  <small class="text-muted status">${statusText}</small>
@@ -942,7 +1090,7 @@ function renderUploadQueue() {
942
1090
  // Tell S3 to discard the incomplete multipart upload
943
1091
  if (entry && entry.s3UploadId && entry.s3Key) {
944
1092
  try {
945
- await fetch('/mbkbucket/upload-abort', {
1093
+ await fetch(withBucketUrl('/mbkbucket/upload-abort'), {
946
1094
  method: 'POST',
947
1095
  headers: { 'Content-Type': 'application/json' },
948
1096
  body: JSON.stringify({ uploadId: entry.s3UploadId, key: entry.s3Key })
@@ -988,7 +1136,7 @@ async function startUploadEntry(entry) {
988
1136
  // S3 Multipart Upload
989
1137
  // Step 1 — Initiate: get uploadId + key from the server
990
1138
  // ---------------------------------------------------------------
991
- const initResp = await fetch('/mbkbucket/upload-init', {
1139
+ const initResp = await fetch(withBucketUrl('/mbkbucket/upload-init'), {
992
1140
  method: 'POST',
993
1141
  headers: { 'Content-Type': 'application/json' },
994
1142
  body: JSON.stringify({
@@ -1015,7 +1163,7 @@ async function startUploadEntry(entry) {
1015
1163
  const _chunkStart = Date.now();
1016
1164
  if (entry.status === 'cancelled') {
1017
1165
  // Clean up on S3
1018
- await fetch('/mbkbucket/upload-abort', {
1166
+ await fetch(withBucketUrl('/mbkbucket/upload-abort'), {
1019
1167
  method: 'POST',
1020
1168
  headers: { 'Content-Type': 'application/json' },
1021
1169
  body: JSON.stringify({ uploadId, key })
@@ -1036,7 +1184,7 @@ async function startUploadEntry(entry) {
1036
1184
  const controller = new AbortController();
1037
1185
  xhrMap.set(entry.id, { controller, s3UploadId: uploadId, s3Key: key });
1038
1186
 
1039
- const chunkResp = await fetch('/mbkbucket/upload-chunk', {
1187
+ const chunkResp = await fetch(withBucketUrl('/mbkbucket/upload-chunk'), {
1040
1188
  method: 'POST',
1041
1189
  body: fd,
1042
1190
  signal: controller.signal
@@ -1056,7 +1204,7 @@ async function startUploadEntry(entry) {
1056
1204
  // ---------------------------------------------------------------
1057
1205
  // Step 3 — Complete: assemble all parts on S3
1058
1206
  // ---------------------------------------------------------------
1059
- const completeResp = await fetch('/mbkbucket/upload-complete', {
1207
+ const completeResp = await fetch(withBucketUrl('/mbkbucket/upload-complete'), {
1060
1208
  method: 'POST',
1061
1209
  headers: { 'Content-Type': 'application/json' },
1062
1210
  body: JSON.stringify({ uploadId, key, parts })
@@ -1076,7 +1224,7 @@ async function startUploadEntry(entry) {
1076
1224
  fd.append('prefix', currentPrefix);
1077
1225
 
1078
1226
  const xhr = new XMLHttpRequest();
1079
- xhr.open('POST', '/mbkbucket/upload');
1227
+ xhr.open('POST', withBucketUrl('/mbkbucket/upload'));
1080
1228
 
1081
1229
  xhr.upload.addEventListener('progress', (e) => {
1082
1230
  if (!e.lengthComputable) return;
@@ -1123,7 +1271,7 @@ async function startUploadEntry(entry) {
1123
1271
  console.error('Upload failed for', entry.file.name, err);
1124
1272
  // If a multipart upload was initiated but didn't finish (not a user cancel), abort it on S3
1125
1273
  if (entry.s3UploadId && entry.s3Key && entry.status !== 'cancelled') {
1126
- fetch('/mbkbucket/upload-abort', {
1274
+ fetch(withBucketUrl('/mbkbucket/upload-abort'), {
1127
1275
  method: 'POST',
1128
1276
  headers: { 'Content-Type': 'application/json' },
1129
1277
  body: JSON.stringify({ uploadId: entry.s3UploadId, key: entry.s3Key })
@@ -1151,20 +1299,33 @@ document.getElementById('clearQueueBtn')?.addEventListener('click', function ()
1151
1299
  function initializeUploadCollapse() {
1152
1300
  const toggleBtn = document.getElementById('uploadToggleBtn');
1153
1301
  const uploadSection = document.getElementById('uploadSection');
1302
+ const uploadCard = uploadSection?.closest('.dashboard-card');
1154
1303
 
1155
1304
  if (!toggleBtn || !uploadSection) return;
1305
+
1306
+ const syncUploadCollapseState = (expanded) => {
1307
+ uploadSection.classList.toggle('expanded', expanded);
1308
+ toggleBtn.classList.toggle('expanded', expanded);
1309
+ toggleBtn.setAttribute('aria-expanded', expanded ? 'true' : 'false');
1310
+ uploadSection.setAttribute('aria-hidden', expanded ? 'false' : 'true');
1311
+ if (uploadCard) uploadCard.classList.toggle('upload-panel-open', expanded);
1312
+ };
1313
+
1314
+ toggleBtn.setAttribute('role', 'button');
1315
+ toggleBtn.setAttribute('tabindex', '0');
1316
+ toggleBtn.setAttribute('aria-controls', 'uploadSection');
1317
+ syncUploadCollapseState(uploadSection.classList.contains('expanded'));
1156
1318
 
1157
1319
  toggleBtn.addEventListener('click', function() {
1158
1320
  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');
1321
+ syncUploadCollapseState(!isExpanded);
1322
+ });
1323
+
1324
+ toggleBtn.addEventListener('keydown', function (event) {
1325
+ if (event.key === 'Enter' || event.key === ' ') {
1326
+ event.preventDefault();
1327
+ const isExpanded = uploadSection.classList.contains('expanded');
1328
+ syncUploadCollapseState(!isExpanded);
1168
1329
  }
1169
1330
  });
1170
1331
  }
@@ -1173,12 +1334,61 @@ function initializeUploadCollapse() {
1173
1334
  document.addEventListener('DOMContentLoaded', function () {
1174
1335
  // Initialize collapsible upload section
1175
1336
  initializeUploadCollapse();
1337
+
1338
+ const bucketSelectorEl = document.getElementById('bucketSelector');
1339
+ const bucketSwitchStatusEl = document.getElementById('bucketSwitchStatus');
1340
+ if (bucketSelectorEl && bucketSelectorEl.value) {
1341
+ activeBucket = bucketSelectorEl.value;
1342
+ }
1343
+ if (bucketSelectorEl) {
1344
+ bucketSelectorEl.addEventListener('change', function () {
1345
+ const nextBucket = String(this.value || '').trim();
1346
+ if (!nextBucket) return;
1347
+ if (nextBucket === activeBucket) return;
1348
+
1349
+ const nextParams = new URLSearchParams(window.location.search);
1350
+ nextParams.set('bucket', nextBucket);
1351
+ nextParams.delete('page');
1352
+ nextParams.delete('token');
1353
+
1354
+ this.disabled = true;
1355
+ this.setAttribute('aria-busy', 'true');
1356
+ bucketSwitchStatusEl?.classList.remove('is-hidden');
1357
+
1358
+ const qs = nextParams.toString();
1359
+ window.location.href = `/mbkbucket${qs ? `?${qs}` : ''}`;
1360
+ });
1361
+ }
1176
1362
 
1177
- // Load initial files (support both legacy 'prefix' and new 'folder' query param)
1363
+ // Load initial files with persisted state.
1178
1364
  const urlParams = new URLSearchParams(window.location.search);
1179
1365
  const initialPrefix = urlParams.get('folder') || urlParams.get('prefix') || '';
1180
1366
  const initialPage = parseInt(urlParams.get('page')) || 1;
1181
- loadFiles(initialPage, initialPrefix);
1367
+ const initialSearch = urlParams.get('search') || '';
1368
+ const initialSort = (urlParams.get('sort') || 'name').toLowerCase();
1369
+ const initialOrder = (urlParams.get('order') || 'asc').toLowerCase();
1370
+ const initialView = (urlParams.get('view') || currentViewMode).toLowerCase();
1371
+ const initialFilter = (urlParams.get('filter') || 'all').toLowerCase();
1372
+
1373
+ if (initialView === 'folder' || initialView === 'flat') {
1374
+ currentViewMode = initialView;
1375
+ }
1376
+ currentQuickFilter = ['all', 'media', 'docs', 'code'].includes(initialFilter) ? initialFilter : 'all';
1377
+ updateQuickFilterUI();
1378
+
1379
+ // Ensure view button state reflects restored mode before first render.
1380
+ if (currentViewMode === 'flat') {
1381
+ const btnFolder = document.getElementById('viewBtnFolder');
1382
+ const btnFlat = document.getElementById('viewBtnFlat');
1383
+ btnFlat?.classList.add('active', 'btn-light');
1384
+ btnFlat?.classList.remove('btn-outline-light');
1385
+ btnFolder?.classList.remove('active', 'btn-light');
1386
+ btnFolder?.classList.add('btn-outline-light');
1387
+ }
1388
+
1389
+ const searchInputInit = document.getElementById('searchInput');
1390
+ if (searchInputInit) searchInputInit.value = initialSearch;
1391
+ loadFiles(initialPage, initialPrefix, initialSearch, initialSort, initialOrder);
1182
1392
 
1183
1393
  // Search behavior: Trigger when the Search button is clicked OR when the user presses Enter in the search input
1184
1394
  const searchInputEl = document.getElementById('searchInput');
@@ -1201,6 +1411,77 @@ document.addEventListener('DOMContentLoaded', function () {
1201
1411
 
1202
1412
  // Delegate view and delete button clicks to avoid inline JS with unescaped data
1203
1413
  document.addEventListener('click', function (e) {
1414
+ const actionBtn = e.target.closest('[data-action]');
1415
+ if (actionBtn) {
1416
+ const action = actionBtn.dataset.action;
1417
+ if (action === 'upload-files') {
1418
+ window.scrollTo({ top: 0, behavior: 'smooth' });
1419
+ document.getElementById('fileInput')?.click();
1420
+ } else if (action === 'create-folder') {
1421
+ createFolder();
1422
+ } else if (action === 'create-file') {
1423
+ createFile();
1424
+ } else if (action === 'go-root') {
1425
+ loadFiles(1, '');
1426
+ } else if (action === 'expand-virtual') {
1427
+ expandVirtualRows();
1428
+ } else if (action === 'search') {
1429
+ performSearch();
1430
+ } else if (action === 'clear-search') {
1431
+ clearSearch();
1432
+ } else if (action === 'retry-load') {
1433
+ loadFiles(parseInt(actionBtn.dataset.retryPage, 10) || 1, actionBtn.dataset.retryPrefix || '');
1434
+ } else if (action === 'close-toast') {
1435
+ actionBtn.closest('.toast-notification')?.remove();
1436
+ }
1437
+ return;
1438
+ }
1439
+
1440
+ const quickFilterBtn = e.target.closest('[data-filter]');
1441
+ if (quickFilterBtn) {
1442
+ setQuickFilter(quickFilterBtn.dataset.filter || 'all');
1443
+ return;
1444
+ }
1445
+
1446
+ const viewModeBtn = e.target.closest('[data-view-mode]');
1447
+ if (viewModeBtn) {
1448
+ changeViewMode(viewModeBtn.dataset.viewMode);
1449
+ return;
1450
+ }
1451
+
1452
+ const navBtn = e.target.closest('[data-nav-prefix]');
1453
+ if (navBtn) {
1454
+ loadFiles(1, navBtn.dataset.navPrefix || '');
1455
+ return;
1456
+ }
1457
+
1458
+ const sortHeader = e.target.closest('[data-sort-field]');
1459
+ if (sortHeader) {
1460
+ toggleSort(sortHeader.dataset.sortField);
1461
+ return;
1462
+ }
1463
+
1464
+ const pageLink = e.target.closest('.page-link[data-page]');
1465
+ if (pageLink) {
1466
+ e.preventDefault();
1467
+ loadFiles(
1468
+ parseInt(pageLink.dataset.page, 10) || 1,
1469
+ pageLink.dataset.prefix || '',
1470
+ pageLink.dataset.search || '',
1471
+ pageLink.dataset.sort || currentSort,
1472
+ pageLink.dataset.order || currentOrder,
1473
+ pageLink.dataset.token || ''
1474
+ );
1475
+ return;
1476
+ }
1477
+
1478
+ const folderRow = e.target.closest('.folder-row');
1479
+ if (folderRow && !e.target.closest('.delete-btn')) {
1480
+ const folderPath = folderRow.dataset.folderPath;
1481
+ if (folderPath) loadFiles(1, folderPath);
1482
+ return;
1483
+ }
1484
+
1204
1485
  const vbtn = e.target.closest('.view-btn');
1205
1486
  if (vbtn) {
1206
1487
  const keyEnc = vbtn.dataset.keyEnc;
@@ -1220,12 +1501,11 @@ document.addEventListener('DOMContentLoaded', function () {
1220
1501
  }
1221
1502
 
1222
1503
  const cbtn = e.target.closest('.copy-btn');
1223
- console.log(cbtn);
1224
1504
  if (cbtn) {
1225
1505
  e.stopPropagation();
1226
1506
  const keyEnc = cbtn.dataset.keyEnc;
1227
1507
  if (!keyEnc) return;
1228
- const link = `${window.location.origin}/mbkbucket/view/${keyEnc}`;
1508
+ const link = `${window.location.origin}${getViewUrl(keyEnc)}`;
1229
1509
  const fallbackCopy = () => {
1230
1510
  try {
1231
1511
  const ta = document.createElement('textarea');
@@ -1302,7 +1582,7 @@ function handleViewFile(key, filename, event) {
1302
1582
  function showPreviewModal(encodedKey, filename, extension, previewMode = 'auto') {
1303
1583
  const modal = document.getElementById('previewModal');
1304
1584
  if (!modal) {
1305
- window.open(`/mbkbucket/view/${encodedKey}`, '_blank', 'noopener,noreferrer');
1585
+ window.open(getViewUrl(encodedKey), '_blank', 'noopener,noreferrer');
1306
1586
  return;
1307
1587
  }
1308
1588
 
@@ -1316,8 +1596,9 @@ function showPreviewModal(encodedKey, filename, extension, previewMode = 'auto')
1316
1596
  const fsBtn = document.getElementById('previewFullscreenBtn');
1317
1597
 
1318
1598
  titleEl.textContent = filename;
1319
- openBtn.href = `/mbkbucket/view/${encodedKey}`;
1320
- downloadBtn.href = `/mbkbucket/download/${encodedKey}`;
1599
+ titleEl.title = filename;
1600
+ openBtn.href = getViewUrl(encodedKey);
1601
+ downloadBtn.href = getDownloadUrl(encodedKey);
1321
1602
  // Ensure downloaded file has the original filename
1322
1603
  downloadBtn.setAttribute('download', filename);
1323
1604
 
@@ -1345,7 +1626,7 @@ function showPreviewModal(encodedKey, filename, extension, previewMode = 'auto')
1345
1626
  dialog.classList.add('preview-compact');
1346
1627
  const img = document.createElement('img');
1347
1628
  img.className = 'preview-img';
1348
- img.src = `/mbkbucket/view/${encodedKey}`;
1629
+ img.src = getViewUrl(encodedKey);
1349
1630
  img.alt = filename;
1350
1631
  img.onload = () => { };
1351
1632
  img.onerror = () => { contentEl.textContent = 'Could not load preview.'; };
@@ -1356,7 +1637,7 @@ function showPreviewModal(encodedKey, filename, extension, previewMode = 'auto')
1356
1637
  const video = document.createElement('video');
1357
1638
  video.className = 'preview-video';
1358
1639
  video.controls = true;
1359
- video.src = `/mbkbucket/view/${encodedKey}`;
1640
+ video.src = getViewUrl(encodedKey);
1360
1641
  contentEl.innerHTML = '';
1361
1642
  contentEl.appendChild(video);
1362
1643
  } else if (effectiveMode === 'audio') {
@@ -1364,20 +1645,20 @@ function showPreviewModal(encodedKey, filename, extension, previewMode = 'auto')
1364
1645
  const audio = document.createElement('audio');
1365
1646
  audio.className = 'preview-audio';
1366
1647
  audio.controls = true;
1367
- audio.src = `/mbkbucket/view/${encodedKey}`;
1648
+ audio.src = getViewUrl(encodedKey);
1368
1649
  contentEl.innerHTML = '';
1369
1650
  contentEl.appendChild(audio);
1370
1651
  } else if (effectiveMode === 'pdf') {
1371
1652
  dialog.classList.add('preview-large');
1372
1653
  const iframe = document.createElement('iframe');
1373
1654
  iframe.className = 'preview-iframe';
1374
- iframe.src = `/mbkbucket/view/${encodedKey}`;
1655
+ iframe.src = getViewUrl(encodedKey);
1375
1656
  contentEl.innerHTML = '';
1376
1657
  contentEl.appendChild(iframe);
1377
1658
  } else if (effectiveMode === 'text') {
1378
1659
  dialog.classList.add('preview-compact');
1379
1660
 
1380
- fetch(`/mbkbucket/view/${encodedKey}`)
1661
+ fetch(getViewUrl(encodedKey))
1381
1662
  .then(r => r.text())
1382
1663
  .then(text => {
1383
1664
  contentEl.innerHTML = '';
@@ -1388,7 +1669,7 @@ function showPreviewModal(encodedKey, filename, extension, previewMode = 'auto')
1388
1669
  })
1389
1670
  .catch(err => { contentEl.textContent = 'Could not load preview.'; });
1390
1671
  } else {
1391
- contentEl.innerHTML = `<div style="text-align:center;">Preview not available for this file type. <a href="/mbkbucket/download/${encodedKey}">Download</a></div>`;
1672
+ contentEl.innerHTML = `<div style="text-align:center;">Preview not available for this file type. <a href="${getDownloadUrl(encodedKey)}">Download</a></div>`;
1392
1673
  }
1393
1674
 
1394
1675
  // Show modal
@@ -1567,7 +1848,7 @@ function setupSelection() {
1567
1848
  if (!confirm('Delete selected files? This cannot be undone.')) return;
1568
1849
  const keys = Array.from(selected);
1569
1850
  try {
1570
- const resp = await fetch('/mbkbucket/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ keys }) });
1851
+ const resp = await fetch(withBucketUrl('/mbkbucket/delete'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ keys }) });
1571
1852
  const json = await resp.json();
1572
1853
  if (json.success) {
1573
1854
  showAlert('Deleted selected files', 'success');
@@ -1590,7 +1871,7 @@ async function createFolder() {
1590
1871
  if (!safe) return showAlert('Invalid folder name', 'error');
1591
1872
 
1592
1873
  try {
1593
- const resp = await fetch('/mbkbucket/create-folder', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prefix: currentPrefix, folderName: safe }) });
1874
+ const resp = await fetch(withBucketUrl('/mbkbucket/create-folder'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prefix: currentPrefix, folderName: safe }) });
1594
1875
  const json = await resp.json();
1595
1876
  if (json.success) {
1596
1877
  showAlert('Folder created', 'success');
@@ -1626,7 +1907,7 @@ async function createFile() {
1626
1907
  formData.append('prefix', currentPrefix);
1627
1908
 
1628
1909
  try {
1629
- const resp = await fetch('/mbkbucket/upload', {
1910
+ const resp = await fetch(withBucketUrl('/mbkbucket/upload'), {
1630
1911
  method: 'POST',
1631
1912
  body: formData
1632
1913
  });
@@ -1815,4 +2096,4 @@ async function createFile() {
1815
2096
  };
1816
2097
  };
1817
2098
 
1818
- })(window);
2099
+ })(window);