mbkbucket 2.0.0 → 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.
@@ -19,6 +19,9 @@ let virtualRenderLimit = 0;
19
19
  const VIRTUAL_RENDER_THRESHOLD = 500;
20
20
  const VIRTUAL_RENDER_BATCH = 250;
21
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']);
22
25
 
23
26
  function withBucketUrl(url) {
24
27
  if (!activeBucket) return url;
@@ -72,6 +75,20 @@ function escapeAttr(value = '') {
72
75
  return escapeHtml(value);
73
76
  }
74
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
+
75
92
  function syncStateUrl(page = currentPage, prefix = currentPrefix, search = currentSearch) {
76
93
  try {
77
94
  const parts = [];
@@ -100,14 +117,10 @@ function getActiveBucketLabel() {
100
117
  }
101
118
 
102
119
  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);
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);
111
124
  return true;
112
125
  }
113
126
 
@@ -155,9 +168,9 @@ function showAlert(message, type = 'success') {
155
168
  `;
156
169
 
157
170
  toast.innerHTML = `
158
- <i class="fas ${isSuccess ? 'fa-check-circle' : 'fa-exclamation-circle'}" style="font-size: 1.25rem;"></i>
159
- <div style="flex: 1;">${message}</div>
160
- <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>
161
174
  `;
162
175
 
163
176
  container.appendChild(toast);
@@ -259,7 +272,7 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
259
272
  <i class="fas fa-folder-open"></i>
260
273
  <h4>No files found</h4>
261
274
  <p>Bucket <strong>${escapeHtml(getActiveBucketLabel())}</strong> is empty. Upload some files to get started!</p>
262
- <button class="btn btn-lg" onclick="document.getElementById('fileInput').click()">
275
+ <button class="btn btn-lg" data-action="upload-files">
263
276
  <i class="fas fa-plus"></i> Upload Your First File
264
277
  </button>
265
278
  </div>
@@ -287,10 +300,10 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
287
300
  console.error('Error loading files:', error);
288
301
  container.innerHTML = `
289
302
  <div class="empty-state">
290
- <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>
291
304
  <h4>Error Loading Files</h4>
292
305
  <p>Bucket <strong>${escapeHtml(getActiveBucketLabel())}</strong>: ${escapeHtml(error.message)}</p>
293
- <button class="btn btn-primary" onclick="loadFiles(${page}, '${prefix}')">
306
+ <button class="btn btn-primary" data-action="retry-load" ${dataAttr('retry-page', page)} ${dataAttr('retry-prefix', prefix)}>
294
307
  <i class="fas fa-redo"></i> Retry
295
308
  </button>
296
309
  </div>
@@ -319,8 +332,8 @@ function toggleSort(field) {
319
332
  valA = new Date(a.LastModified || 0).getTime();
320
333
  valB = new Date(b.LastModified || 0).getTime();
321
334
  } else {
322
- valA = (a.Key || '').split('/').pop().toLowerCase();
323
- valB = (b.Key || '').split('/').pop().toLowerCase();
335
+ valA = getBaseName(a.Key).toLowerCase();
336
+ valB = getBaseName(b.Key).toLowerCase();
324
337
  }
325
338
 
326
339
  if (valA < valB) return currentOrder === 'asc' ? -1 : 1;
@@ -360,7 +373,6 @@ function setQuickFilter(filter = 'all') {
360
373
  // Extract unique folders at current level
361
374
  function extractFoldersAtCurrentLevel(files, prefix) {
362
375
  const folderSet = new Set();
363
- const prefixDepth = prefix ? prefix.split('/').filter(p => p).length : 0;
364
376
 
365
377
  files.forEach(file => {
366
378
  const key = file.Key;
@@ -447,6 +459,9 @@ function extractMatchingFolders(files, search) {
447
459
  // Render files table
448
460
  function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
449
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;
450
465
 
451
466
  // Determine which files/folders to show based on view mode
452
467
  let displayFiles = [];
@@ -466,7 +481,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
466
481
  // Skip folder marker objects (keys ending with /)
467
482
  if (f.Key.endsWith('/')) return false;
468
483
 
469
- const fileName = f.Key.split('/').pop();
484
+ const fileName = getBaseName(f.Key);
470
485
  return fileName.toLowerCase().includes(searchLower);
471
486
  });
472
487
 
@@ -485,8 +500,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
485
500
  // Skip folder marker objects (keys ending with /)
486
501
  if (file.Key.endsWith('/')) return false;
487
502
 
488
- const basePrefix = apiPrefix || prefix;
489
- const relativePath = basePrefix ? file.Key.substring(basePrefix.length + (basePrefix.endsWith('/') ? 0 : 1)) : file.Key;
503
+ const relativePath = basePrefix ? file.Key.substring(relativeBaseOffset) : file.Key;
490
504
  // File is at current level if it has no '/' in relative path
491
505
  return !relativePath.includes('/');
492
506
  });
@@ -495,7 +509,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
495
509
 
496
510
  // Apply quick file-type filter after mode/path filtering.
497
511
  displayFiles = displayFiles.filter((f) => {
498
- const name = String(f?.Key || '').split('/').pop();
512
+ const name = getBaseName(f?.Key);
499
513
  return matchesQuickFilter(name, currentQuickFilter);
500
514
  });
501
515
 
@@ -505,13 +519,15 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
505
519
  // (In flat mode, the prefix acts as a base filter, but we see recursive files under it)
506
520
  if (prefix) {
507
521
  const parts = prefix.split('/').filter(p => p);
508
- let breadcrumbHTML = `
522
+ const safeRootLabel = escapeHtml(rootLabel);
523
+ const safeRootTitle = escapeAttr(rootLabel);
524
+ const breadcrumbParts = [`
509
525
  <div class="breadcrumb-nav">
510
526
  <div class="breadcrumb-item">
511
527
  <i class="fas fa-home"></i>
512
- <span class="breadcrumb-link" onclick="loadFiles(1, '')" title="Go to ${rootLabel}">${rootLabel}</span>
528
+ <button type="button" class="breadcrumb-link" ${dataAttr('nav-prefix', '')} title="Go to ${safeRootTitle}">${safeRootLabel}</button>
513
529
  </div>
514
- `;
530
+ `];
515
531
 
516
532
  let currentPath = '';
517
533
  parts.forEach((part, index) => {
@@ -519,51 +535,57 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
519
535
  const isLast = index === parts.length - 1;
520
536
 
521
537
  if (isLast) {
522
- breadcrumbHTML += `
538
+ const safePart = escapeHtml(part);
539
+ breadcrumbParts.push(`
523
540
  <span class="breadcrumb-separator"><i class="fas fa-chevron-right"></i></span>
524
541
  <div class="breadcrumb-item">
525
- <span class="breadcrumb-current">${part}</span>
542
+ <span class="breadcrumb-current">${safePart}</span>
526
543
  </div>
527
- `;
544
+ `);
528
545
  } else {
529
546
  const pathCopy = currentPath;
530
- breadcrumbHTML += `
547
+ const safePart = escapeHtml(part);
548
+ const safePathTitle = escapeAttr(pathCopy);
549
+ breadcrumbParts.push(`
531
550
  <span class="breadcrumb-separator"><i class="fas fa-chevron-right"></i></span>
532
551
  <div class="breadcrumb-item">
533
- <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>
534
553
  </div>
535
- `;
554
+ `);
536
555
  }
537
556
  });
538
557
 
539
- breadcrumbHTML += `</div>`;
540
- breadcrumb = breadcrumbHTML;
558
+ breadcrumbParts.push(`</div>`);
559
+ breadcrumb = breadcrumbParts.join('');
541
560
  }
542
561
 
543
562
  // Generate folder rows (only used in folder mode)
544
563
  const folderRows = displayFolders.map(folderPath => {
545
564
  // Handle folder paths which typically end in a slash (e.g., "folder/subfolder/")
546
565
  const rawFolderPath = String(folderPath || '');
547
- const cleanPath = rawFolderPath.replace(/\/+$/, '');
548
- const folderKeyForDelete = `${cleanPath}/`;
549
- const folderName = cleanPath.split('/').pop();
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);
550
572
  const safeFolderName = escapeHtml(folderName);
551
- const safeFolderPathAttr = escapeAttr(rawFolderPath);
573
+ const safeFolderPathAttr = escapeAttr(`${folderPathForAction}/`);
552
574
  const safeFolderDeleteKeyAttr = escapeAttr(folderKeyForDelete);
553
575
 
554
576
  return `
555
577
  <tr class="folder-row" data-folder-path="${safeFolderPathAttr}" title="Open folder: ${safeFolderName}">
556
- <td style="text-align:center;">
578
+ <td class="table-cell-center">
557
579
  <!-- no selection for folders -->
558
580
  </td>
559
- <td style="padding-left: 1.5rem;">
560
- <div style="display: flex; align-items: center;">
581
+ <td class="file-name-cell">
582
+ <div class="file-row-main">
561
583
  <div class="file-icon">
562
584
  <i class="fas fa-folder"></i>
563
585
  </div>
564
586
  <div class="file-info">
565
- <div class="file-name" style="font-weight: 700;">
566
- <i class="fas fa-folder" style="margin-right: 0.5rem; color: #f59e0b;"></i>${safeFolderName}/
587
+ <div class="file-name folder-name">
588
+ <i class="fas fa-folder folder-leading-icon"></i>${safeFolderName}/
567
589
  </div>
568
590
  </div>
569
591
  <button type="button" class="btn btn-danger btn-sm delete-btn dtm" data-key="${safeFolderDeleteKeyAttr}" data-is-folder="true" title="Delete folder">
@@ -572,12 +594,12 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
572
594
  </div>
573
595
  </td>
574
596
  <td>
575
- <span style="color: var(--gray-color-ml);">\u2014</span>
597
+ ${mutedDash()}
576
598
  </td>
577
- <td style="text-align: center;">
578
- <span style="color: var(--gray-color-ml);">\u2014</span>
599
+ <td class="table-cell-center">
600
+ ${mutedDash()}
579
601
  </td>
580
- <td style="text-align: center;">
602
+ <td class="table-cell-center">
581
603
  <div class="btn-group" role="group">
582
604
  <button type="button" class="btn btn-danger btn-sm delete-btn" data-key="${safeFolderDeleteKeyAttr}" data-is-folder="true" title="Delete folder">
583
605
  <i class="fas fa-trash"></i>
@@ -620,21 +642,23 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
620
642
  const safeFilename = escapeHtml(filename);
621
643
  const safeTruncatedName = escapeHtml(truncatedName);
622
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';
623
647
 
624
648
  // If in flat mode, we might want to show the folder path more prominently
625
649
  const pathDisplay = (currentViewMode === 'flat' && folderPath && folderPath !== prefix + (prefix ? '/' : ''))
626
650
  ? `<div class="file-path-display">
627
- <i class="fas fa-folder" style="margin-right:4px; opacity:0.5;"></i> ${safeFolderPath}
651
+ <i class="fas fa-folder path-display-icon"></i> ${safeFolderPath}
628
652
  </div>`
629
653
  : '';
630
654
 
631
655
  return `
632
656
  <tr class="file-row">
633
- <td style="text-align:center;">
657
+ <td class="table-cell-center">
634
658
  <input type="checkbox" class="selectFileCheckbox" data-key="${safeKeyAttr}" aria-label="Select ${safeKeyAttr}" />
635
659
  </td>
636
- <td style="padding-left: 1.5rem;">
637
- <div style="display: flex; align-items: center;">
660
+ <td class="file-name-cell">
661
+ <div class="file-row-main">
638
662
  <div class="file-icon ${typeCategory}">
639
663
  <i class="${iconClass}"></i>
640
664
  </div>
@@ -645,18 +669,18 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
645
669
  </div>
646
670
  </td>
647
671
  <td>
648
- ${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>'}
649
673
  </td>
650
674
  <td>
651
- ${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>'}
652
676
  </td>
653
- <td style="text-align: center;">
677
+ <td class="table-cell-center">
654
678
  <div class="btn-group" role="group">
655
- <button type="button" class="btn btn-primary btn-sm view-btn" data-key="${safeKeyAttr}" data-filename="${safeKeyAttr}"
656
- 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}>
657
681
  <i class="fas fa-eye"></i>
658
682
  </button>
659
- <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">
660
684
  <i class="fas fa-link"></i>
661
685
  </button>
662
686
  <a href="${getDownloadUrl(encodedKey)}" class="btn btn-success btn-sm"
@@ -679,7 +703,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
679
703
  ${breadcrumb}
680
704
  <div class="empty-state">
681
705
  <div class="empty-state-illustration">
682
- <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>
683
707
  <i class="fas fa-search empty-state-search-icon"></i>
684
708
  </div>
685
709
  <h4>No files found</h4>
@@ -687,13 +711,13 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
687
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.`}
688
712
  </p>
689
713
  <div class="empty-state-actions">
690
- <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">
691
715
  <i class="fas fa-cloud-upload-alt"></i> Upload Files
692
716
  </button>
693
- <button class="btn btn-outline-secondary" onclick="createFolder()">
717
+ <button class="btn btn-outline-secondary" data-action="create-folder">
694
718
  <i class="fas fa-folder-plus"></i> New Folder
695
719
  </button>
696
- ${prefix ? `<button class="btn btn-light" onclick="loadFiles(1, '')">
720
+ ${prefix ? `<button class="btn btn-light" data-action="go-root">
697
721
  <i class="fas fa-home"></i> Go to ${rootLabel}
698
722
  </button>` : ''}
699
723
  </div>
@@ -714,7 +738,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
714
738
  ? `<div class="virtualized-info">
715
739
  <span>Showing ${visibleFiles.length} of ${displayFiles.length} files in this view.</span>
716
740
  <div class="virtualized-actions">
717
- ${visibleFiles.length < displayFiles.length ? '<button class="btn btn-sm btn-outline-secondary" onclick="expandVirtualRows()">Load more</button>' : ''}
741
+ ${visibleFiles.length < displayFiles.length ? '<button class="btn btn-sm btn-outline-secondary" data-action="expand-virtual">Load more</button>' : ''}
718
742
  </div>
719
743
  </div>`
720
744
  : '';
@@ -724,7 +748,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
724
748
  <div class="table-responsive">
725
749
  <div id="bulkActionsBar" class="bulk-actions-bar">
726
750
  <span id="bulkSelectedCount">0 selected</span>
727
- <div style="float: right;">
751
+ <div class="bulk-actions-controls">
728
752
  <button class="btn btn-danger btn-sm" id="bulkDeleteBtn" disabled>
729
753
  <i class="fas fa-trash"></i> Delete Selected
730
754
  </button>
@@ -733,19 +757,19 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
733
757
  <table class="table table-hover mb-0">
734
758
  <thead>
735
759
  <tr>
736
- <th style="width:48px; text-align:center;">
760
+ <th class="table-select-col">
737
761
  <input type="checkbox" id="selectAllCheckbox" aria-label="Select all files" />
738
762
  </th>
739
- <th style="padding-left: 1.5rem; cursor: pointer;" onclick="toggleSort('name')">
763
+ <th class="sortable-heading file-name-heading" data-sort-field="name">
740
764
  <i class="fas fa-file-alt"></i> Name ${getSortIcon('name')}
741
765
  </th>
742
- <th style="cursor: pointer;" onclick="toggleSort('size')">
766
+ <th class="sortable-heading" data-sort-field="size">
743
767
  <i class="fas fa-weight"></i> Size ${getSortIcon('size')}
744
768
  </th>
745
- <th style="cursor: pointer;" onclick="toggleSort('date')">
769
+ <th class="sortable-heading" data-sort-field="date">
746
770
  <i class="fas fa-calendar"></i> Last Modified ${getSortIcon('date')}
747
771
  </th>
748
- <th style="text-align: center;">
772
+ <th class="table-cell-center">
749
773
  <i class="fas fa-cogs"></i> Actions
750
774
  </th>
751
775
  </tr>
@@ -767,6 +791,17 @@ function renderPagination(data) {
767
791
  const { currentPage, totalPages, hasPrevPage, hasNextPage, mode } = data;
768
792
 
769
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
+ };
770
805
 
771
806
  if (mode === 'optimized') {
772
807
  // Optimized Mode: Token-based pagination (Next only, limited Prev)
@@ -789,13 +824,9 @@ function renderPagination(data) {
789
824
 
790
825
  // Next button
791
826
  if (hasNextPage) {
792
- // Quote the token correctly
793
- const tokenStr = currentNextToken ? currentNextToken.replace(/'/g, "\\'") : "";
794
827
  paginationHTML += `
795
828
  <li class="page-item">
796
- <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${currentPage + 1}, '${currentPrefix}', '${currentSearch}', '${currentSort}', '${currentOrder}', '${tokenStr}')">
797
- Next <i class="fas fa-chevron-right"></i>
798
- </a>
829
+ ${pageLink(currentPage + 1, 'Next <i class="fas fa-chevron-right"></i>', { token: currentNextToken || '' })}
799
830
  </li>
800
831
  `;
801
832
  } else {
@@ -815,9 +846,7 @@ function renderPagination(data) {
815
846
  if (hasPrevPage) {
816
847
  paginationHTML += `
817
848
  <li class="page-item">
818
- <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${currentPage - 1}, '${currentPrefix}', '${currentSearch}')">
819
- <i class="fas fa-chevron-left"></i> Previous
820
- </a>
849
+ ${pageLink(currentPage - 1, '<i class="fas fa-chevron-left"></i> Previous')}
821
850
  </li>
822
851
  `;
823
852
  } else {
@@ -843,7 +872,7 @@ function renderPagination(data) {
843
872
  if (startPage > 1) {
844
873
  paginationHTML += `
845
874
  <li class="page-item">
846
- <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(1, '${currentPrefix}', '${currentSearch}')">1</a>
875
+ ${pageLink(1, '1')}
847
876
  </li>`;
848
877
  if (startPage > 2) {
849
878
  paginationHTML += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
@@ -860,7 +889,7 @@ function renderPagination(data) {
860
889
  } else {
861
890
  paginationHTML += `
862
891
  <li class="page-item">
863
- <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${i}, '${currentPrefix}', '${currentSearch}')">${i}</a>
892
+ ${pageLink(i, String(i))}
864
893
  </li>
865
894
  `;
866
895
  }
@@ -873,7 +902,7 @@ function renderPagination(data) {
873
902
  }
874
903
  paginationHTML += `
875
904
  <li class="page-item">
876
- <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${totalPages}, '${currentPrefix}', '${currentSearch}')">${totalPages}</a>
905
+ ${pageLink(totalPages, String(totalPages))}
877
906
  </li>`;
878
907
  }
879
908
 
@@ -881,9 +910,7 @@ function renderPagination(data) {
881
910
  if (hasNextPage) {
882
911
  paginationHTML += `
883
912
  <li class="page-item">
884
- <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${currentPage + 1}, '${currentPrefix}', '${currentSearch}')">
885
- Next <i class="fas fa-chevron-right"></i>
886
- </a>
913
+ ${pageLink(currentPage + 1, 'Next <i class="fas fa-chevron-right"></i>')}
887
914
  </li>
888
915
  `;
889
916
  } else {
@@ -976,7 +1003,7 @@ setTimeout(() => {
976
1003
  // Upload queue, drag-and-drop and multi-file support
977
1004
  const uploadQueue = []; // { id, file, status, progress, controller/xhr, uploadId }
978
1005
  const xhrMap = new Map();
979
- const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB chunk threshold
1006
+ const CHUNK_SIZE = 50 * 1024 * 1024; // 50MB chunk threshold
980
1007
 
981
1008
  function createId() { return Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 9); }
982
1009
 
@@ -1030,9 +1057,9 @@ function renderUploadQueue() {
1030
1057
  <div class="form-text file-size upload-item-size-label">${formatFileSize(item.file.size)}</div>
1031
1058
  </div>
1032
1059
  </div>
1033
- <div style="min-width:220px;">
1034
- <div class="progress-container" style="height: 8px; margin-bottom: 6px;">
1035
- <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>
1036
1063
  </div>
1037
1064
  <div class="upload-item-status-row">
1038
1065
  <small class="text-muted status">${statusText}</small>
@@ -1326,7 +1353,7 @@ document.addEventListener('DOMContentLoaded', function () {
1326
1353
 
1327
1354
  this.disabled = true;
1328
1355
  this.setAttribute('aria-busy', 'true');
1329
- if (bucketSwitchStatusEl) bucketSwitchStatusEl.style.display = 'inline';
1356
+ bucketSwitchStatusEl?.classList.remove('is-hidden');
1330
1357
 
1331
1358
  const qs = nextParams.toString();
1332
1359
  window.location.href = `/mbkbucket${qs ? `?${qs}` : ''}`;
@@ -1384,6 +1411,70 @@ document.addEventListener('DOMContentLoaded', function () {
1384
1411
 
1385
1412
  // Delegate view and delete button clicks to avoid inline JS with unescaped data
1386
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
+
1387
1478
  const folderRow = e.target.closest('.folder-row');
1388
1479
  if (folderRow && !e.target.closest('.delete-btn')) {
1389
1480
  const folderPath = folderRow.dataset.folderPath;
@@ -2005,4 +2096,4 @@ async function createFile() {
2005
2096
  };
2006
2097
  };
2007
2098
 
2008
- })(window);
2099
+ })(window);