mbkbucket 1.2.0 → 1.3.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.
@@ -7,36 +7,71 @@ let currentOrder = 'asc';
7
7
  let currentViewMode = 'folder'; // 'folder' or 'flat'
8
8
  let pageFiles = [];
9
9
  let pageFolders = [];
10
+ let currentNextToken = null;
10
11
  const bucketAppName = document.getElementById('bucketAppData')?.dataset.appName || '';
11
12
  // Bucket is configured centrally on the server; client should not select a bucket.
12
13
 
13
- // Show alert message
14
+ // Show alert message (Toast)
14
15
  function showAlert(message, type = 'success') {
15
- const alertId = type === 'success' ? 'successAlert' : 'errorAlert';
16
- const messageId = type === 'success' ? 'successMessage' : 'errorMessage';
17
-
18
- const alertEl = document.getElementById(alertId);
19
- const messageEl = document.getElementById(messageId);
20
-
21
- if (alertEl && messageEl) {
22
- messageEl.textContent = message;
23
- alertEl.style.display = 'flex';
24
-
25
- // Auto dismiss after 5 seconds
26
- setTimeout(() => {
27
- alertEl.style.opacity = '0';
28
- alertEl.style.transform = 'translateY(-10px)';
29
- setTimeout(() => {
30
- alertEl.style.display = 'none';
31
- alertEl.style.opacity = '1';
32
- alertEl.style.transform = 'translateY(0)';
33
- }, 300);
34
- }, 5000);
16
+ const container = document.getElementById('toast-container');
17
+ if (!container) {
18
+ // Fallback to old method if container missing
19
+ const alertId = type === 'success' ? 'successAlert' : 'errorAlert';
20
+ const messageId = type === 'success' ? 'successMessage' : 'errorMessage';
21
+ const alertEl = document.getElementById(alertId);
22
+ const messageEl = document.getElementById(messageId);
23
+ if (alertEl && messageEl) {
24
+ messageEl.textContent = message;
25
+ alertEl.style.display = 'flex';
26
+ setTimeout(() => alertEl.style.display = 'none', 5000);
27
+ }
28
+ return;
35
29
  }
30
+
31
+ // Create toast element
32
+ const toast = document.createElement('div');
33
+ const isSuccess = type === 'success';
34
+ toast.className = `toast-notification ${type}`;
35
+ toast.style.cssText = `
36
+ background: ${isSuccess ? 'linear-gradient(135deg, #10b981, #059669)' : 'linear-gradient(135deg, #ef4444, #dc2626)'};
37
+ color: white;
38
+ padding: 1rem 1.5rem;
39
+ border-radius: 8px;
40
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
41
+ display: flex;
42
+ align-items: center;
43
+ gap: 12px;
44
+ min-width: 300px;
45
+ transform: translateX(100%);
46
+ opacity: 0;
47
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
48
+ font-weight: 500;
49
+ `;
50
+
51
+ toast.innerHTML = `
52
+ <i class="fas ${isSuccess ? 'fa-check-circle' : 'fa-exclamation-circle'}" style="font-size: 1.25rem;"></i>
53
+ <div style="flex: 1;">${message}</div>
54
+ <button onclick="this.parentElement.remove()" style="background:none; border:none; color:white; cursor:pointer; opacity:0.8; font-size:1.2rem;">&times;</button>
55
+ `;
56
+
57
+ container.appendChild(toast);
58
+
59
+ // Animate in
60
+ requestAnimationFrame(() => {
61
+ toast.style.transform = 'translateX(0)';
62
+ toast.style.opacity = '1';
63
+ });
64
+
65
+ // Auto dismiss
66
+ setTimeout(() => {
67
+ toast.style.transform = 'translateX(100%)';
68
+ toast.style.opacity = '0';
69
+ setTimeout(() => toast.remove(), 300);
70
+ }, 5000);
36
71
  }
37
72
 
38
73
  // Fetch and render files
39
- async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort, order = currentOrder) {
74
+ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort, order = currentOrder, token = '') {
40
75
  currentPage = page;
41
76
  currentPrefix = prefix;
42
77
  currentSearch = search;
@@ -75,19 +110,13 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
75
110
  <div class="skeleton-box s-40"></div>
76
111
  <div class="skeleton-box s-200"></div>
77
112
  <div class="skeleton-box s-80"></div>
78
- <div class="skeleton-box s-80"></div>
79
- </div>
80
- <div class="skeleton-row">
81
- <div class="skeleton-box s-40"></div>
82
113
  <div class="skeleton-box s-200"></div>
83
- <div class="skeleton-box s-80"></div>
84
- <div class="skeleton-box s-80"></div>
85
114
  </div>
86
115
  <div class="skeleton-row">
87
116
  <div class="skeleton-box s-40"></div>
88
117
  <div class="skeleton-box s-200"></div>
89
118
  <div class="skeleton-box s-80"></div>
90
- <div class="skeleton-box s-80"></div>
119
+ <div class="skeleton-box s-200"></div>
91
120
  </div>
92
121
  </div>
93
122
  `;
@@ -95,27 +124,42 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
95
124
  try {
96
125
  const searchParam = search ? `&search=${encodeURIComponent(search)}` : '';
97
126
  const sortParam = `&sort=${sort}&order=${order}`;
98
- const response = await fetch(`/mbkbucket/api/files?page=${page}&prefix=${encodeURIComponent(prefix)}${searchParam}${sortParam}`);
127
+
128
+ // Determine recursive mode
129
+ const isFolderMode = currentViewMode === 'folder';
130
+ const recursiveParam = `&recursive=${!isFolderMode}`; // false if folder mode, true if flat mode
131
+ const tokenParam = token ? `&token=${encodeURIComponent(token)}` : '';
132
+
133
+ const response = await fetch(`/mbkbucket/api/files?page=${page}&prefix=${encodeURIComponent(prefix)}${searchParam}${sortParam}${recursiveParam}${tokenParam}`);
99
134
  const data = await response.json();
100
135
 
101
136
  if (!data.success) {
102
137
  throw new Error(data.error || 'Failed to load files');
103
138
  }
139
+
140
+ // Store next token
141
+ currentNextToken = data.nextContinuationToken || null;
104
142
 
105
143
  // Update stats
106
- totalFilesCount.textContent = `${data.totalFiles} ${search ? 'Results' : 'Files'}`;
144
+ if (data.totalFiles === -1) {
145
+ totalFilesCount.textContent = `${(data.files.length + (data.folders ? data.folders.length : 0))}${data.hasNextPage ? '+' : ''} Items`;
146
+ } else {
147
+ totalFilesCount.textContent = `${data.totalFiles} ${search ? 'Results' : 'Files'}`;
148
+ }
107
149
 
108
150
  if (search) {
109
- filterInfo.textContent = `Search results for: "${search}"`;
110
- fileListTitle.innerHTML = `Search Results <span class="text-muted">for: </span><span class="badge bg-info">${search}</span>`;
151
+ filterInfo.textContent = `Search results for: "${search}"`;
152
+ fileListTitle.innerHTML = `Search Results <span class="text-muted">for: </span><span class="badge bg-info">${search}</span>`;
111
153
  } else {
112
- const label = prefix || bucketAppName || 'Root';
113
- filterInfo.textContent = `Filtered by folder: ${label}`;
114
- fileListTitle.innerHTML = `Files <span class="text-muted">in: </span><span class="badge bg-info">${label}</span>`;
154
+ const label = prefix || bucketAppName || 'Root';
155
+ filterInfo.textContent = `Filtered by folder: ${label}`;
156
+ fileListTitle.innerHTML = `Files <span class="text-muted">in: </span><span class="badge bg-info">${label}</span>`;
115
157
  }
116
158
 
117
159
  // Render files or empty state
118
- if (data.files.length === 0 && !prefix) {
160
+ const hasContent = (data.files && data.files.length > 0) || (data.folders && data.folders.length > 0);
161
+
162
+ if (!hasContent && !prefix && !search) {
119
163
  container.innerHTML = `
120
164
  <div class="empty-state">
121
165
  <i class="fas fa-folder-open"></i>
@@ -129,11 +173,13 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
129
173
  paginationContainer.style.display = 'none';
130
174
  } else {
131
175
  // Store for client-side sorting and view switching
132
- pageFiles = data.files;
176
+ pageFiles = data.files || [];
133
177
 
134
- // Only calculate folders if we are in search or folder mode (for potential future switching)
135
- // But mainly we rely on currentViewMode in the render function
136
- pageFolders = search ? [] : extractFoldersAtCurrentLevel(data.files, prefix);
178
+ if (data.mode === 'optimized') {
179
+ pageFolders = data.folders || [];
180
+ } else {
181
+ pageFolders = search ? [] : extractFoldersAtCurrentLevel(data.files, prefix);
182
+ }
137
183
 
138
184
  renderFilesTable(pageFiles, pageFolders, prefix);
139
185
  renderPagination(data);
@@ -256,19 +302,62 @@ function changeViewMode(mode) {
256
302
  renderFilesTable(pageFiles, pageFolders, currentPrefix);
257
303
  }
258
304
 
305
+ // Extract folders that match a search query from file paths
306
+ function extractMatchingFolders(files, search) {
307
+ if (!search) return [];
308
+
309
+ const searchLower = search.toLowerCase();
310
+ const folderSet = new Set();
311
+
312
+ files.forEach(file => {
313
+ const parts = file.Key.split('/');
314
+ // Remove filename
315
+ parts.pop();
316
+
317
+ let currentPath = '';
318
+ parts.forEach(part => {
319
+ const segmentPath = currentPath ? `${currentPath}/${part}` : part;
320
+ currentPath = segmentPath;
321
+
322
+ // If any part of the path matches the search, include that full path as a folder
323
+ // AND ensure we don't include the full file path, just the directory
324
+ if (part.toLowerCase().includes(searchLower)) {
325
+ folderSet.add(currentPath);
326
+ }
327
+ });
328
+ });
329
+
330
+ return Array.from(folderSet).sort();
331
+ }
332
+
259
333
  // Render files table
260
334
  function renderFilesTable(files, folders = [], prefix = '') {
261
335
  // Determine which files/folders to show based on view mode
262
336
  let displayFiles = [];
263
337
  let displayFolders = [];
264
338
 
265
- if (currentViewMode === 'flat') {
339
+ // Force flat view if searching
340
+ const isSearchActive = !!currentSearch;
341
+ const mode = isSearchActive ? 'flat' : currentViewMode;
342
+
343
+ if (mode === 'flat') {
266
344
  // In flat mode, show all files in the current loaded page/list regardless of folder depth
267
- // And ignore folder objects (we just show file paths)
268
- displayFiles = files;
269
- // We don't show expandable folders in flat view, unless we want to keep them?
270
- // User asked for "all files displayed without folder". So empty folders.
271
- displayFolders = [];
345
+ if (isSearchActive) {
346
+ // Search Mode:
347
+ // 1. Show files where FILENAME matches the search term
348
+ const searchLower = currentSearch.toLowerCase();
349
+ displayFiles = files.filter(f => {
350
+ const fileName = f.Key.split('/').pop();
351
+ return fileName.toLowerCase().includes(searchLower);
352
+ });
353
+
354
+ // 2. Show folders where FOLDER NAME matches the search term
355
+ displayFolders = extractMatchingFolders(files, currentSearch);
356
+ } else {
357
+ // Normal Flat Mode: Show all files, no folders
358
+ displayFiles = files;
359
+ displayFolders = [];
360
+ }
272
361
  } else {
273
362
  // Normal Folder View logic
274
363
  // Filter files to show only those at current level (not in subfolders)
@@ -323,7 +412,10 @@ function renderFilesTable(files, folders = [], prefix = '') {
323
412
 
324
413
  // Generate folder rows (only used in folder mode)
325
414
  const folderRows = displayFolders.map(folderPath => {
326
- const folderName = folderPath.split('/').pop();
415
+ // Handle folder paths which typically end in a slash (e.g., "folder/subfolder/")
416
+ const cleanPath = folderPath.replace(/\/+$/, '');
417
+ const folderName = cleanPath.split('/').pop();
418
+
327
419
  return `
328
420
  <tr class="folder-row" onclick="loadFiles(1, '${folderPath}')" title="Open folder: ${folderName}">
329
421
  <td style="text-align:center;">
@@ -338,7 +430,10 @@ function renderFilesTable(files, folders = [], prefix = '') {
338
430
  <div class="file-name" style="font-weight: 700;">
339
431
  <i class="fas fa-folder" style="margin-right: 0.5rem; color: #f59e0b;"></i>${folderName}/
340
432
  </div>
341
- </div>
433
+ </div>
434
+ <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">
435
+ <i class="fas fa-trash"></i>
436
+ </button>
342
437
  </div>
343
438
  </td>
344
439
  <td>
@@ -349,7 +444,7 @@ function renderFilesTable(files, folders = [], prefix = '') {
349
444
  </td>
350
445
  <td style="text-align: center;">
351
446
  <div class="btn-group" role="group">
352
- <button type="button" class="btn btn-danger btn-sm delete-btn" data-key="${folderPath}/" data-is-folder="true" title="Delete folder">
447
+ <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">
353
448
  <i class="fas fa-trash"></i>
354
449
  </button>
355
450
  </div>
@@ -373,10 +468,10 @@ function renderFilesTable(files, folders = [], prefix = '') {
373
468
  folderPath = fullPath.substring(0, lastSlashIndex + 1); // include trailing slash
374
469
  }
375
470
 
376
- const iconClass = getFileIcon(filename);
377
- const typeCategory = getFileTypeCategory(filename);
378
- const truncatedName = truncateFileName(filename);
379
- const isFileViewable = isViewable(filename);
471
+ const iconClass = window.getFileIcon ? window.getFileIcon(filename) : 'fas fa-file';
472
+ const typeCategory = window.getFileTypeCategory ? window.getFileTypeCategory(filename) : '';
473
+ const truncatedName = (window.truncateFileName ? window.truncateFileName(filename) : filename) || filename;
474
+ const isFileViewable = window.isViewable ? window.isViewable(filename) : false;
380
475
 
381
476
  // If in flat mode, we might want to show the folder path more prominently
382
477
  const pathDisplay = (currentViewMode === 'flat' && folderPath && folderPath !== prefix + (prefix ? '/' : ''))
@@ -413,6 +508,9 @@ function renderFilesTable(files, folders = [], prefix = '') {
413
508
  data-key-enc="${encodedKey}" title="${isFileViewable ? 'View file in browser' : 'File type not supported for viewing'}" ${!isFileViewable ? 'disabled style="opacity:0.5"' : ''}>
414
509
  <i class="fas fa-eye"></i>
415
510
  </button>
511
+ <button type="button" class="btn btn-info btn-sm copy-btn" data-key-enc="${encodedKey}" title="Copy Link" style="color:white;">
512
+ <i class="fas fa-link"></i>
513
+ </button>
416
514
  <a href="/mbkbucket/download/${encodedKey}" class="btn btn-success btn-sm"
417
515
  title="Download file">
418
516
  <i class="fas fa-download"></i>
@@ -431,18 +529,23 @@ function renderFilesTable(files, folders = [], prefix = '') {
431
529
  if (allRows === '') {
432
530
  document.getElementById('fileListContainer').innerHTML = `
433
531
  <div class="empty-state">
434
- <i class="fas fa-folder-open"></i>
435
- <h4>No files or folders</h4>
436
- <p>${prefix ? `No files in folder: ${prefix}` : 'This folder is empty'}</p>
437
- <div style="display:flex; gap:0.5rem; justify-content:center; margin-top:1rem;">
438
- <button class="btn btn-primary" onclick="document.getElementById('fileInput').click()">
439
- <i class="fas fa-cloud-upload-alt"></i> Upload File
532
+ <div style="margin-bottom: 1.5rem; position: relative; display: inline-block;">
533
+ <i class="fas fa-folder-open" style="font-size: 4rem; color: #cbd5e0;"></i>
534
+ <i class="fas fa-search" style="font-size: 2rem; color: var(--primary-color-ml); position: absolute; bottom: -5px; right: -10px; background: white; border-radius: 50%; padding: 5px;"></i>
535
+ </div>
536
+ <h4>No files found</h4>
537
+ <p style="max-width: 400px; margin: 0 auto 1.5rem; color: var(--text-muted-color-ml);">
538
+ ${prefix ? `This folder <strong>${prefix}</strong> is currently empty.` : 'Your bucket is empty. Start by uploading files or creating a new folder.'}
539
+ </p>
540
+ <div style="display:flex; gap:0.75rem; justify-content:center; flex-wrap: wrap;">
541
+ <button class="btn btn-primary" onclick="window.scrollTo({top: 0, behavior: 'smooth'}); document.getElementById('fileInput').click()">
542
+ <i class="fas fa-cloud-upload-alt"></i> Upload Files
440
543
  </button>
441
544
  <button class="btn btn-outline-secondary" onclick="createFolder()">
442
- <i class="fas fa-folder-plus"></i> Create Folder
545
+ <i class="fas fa-folder-plus"></i> New Folder
443
546
  </button>
444
547
  ${prefix ? `<button class="btn btn-light" onclick="loadFiles(1, '')">
445
- <i class="fas fa-arrow-left"></i> Back to Root
548
+ <i class="fas fa-home"></i> Go to Root
446
549
  </button>` : ''}
447
550
  </div>
448
551
  </div>
@@ -461,7 +564,7 @@ function renderFilesTable(files, folders = [], prefix = '') {
461
564
  document.getElementById('fileListContainer').innerHTML = `
462
565
  ${breadcrumb}
463
566
  <div class="table-responsive">
464
- <div id="bulkActionsBar" style="display:none; padding: 0.75rem; border-bottom: 1px solid var(--gray-light-color-ml); background: var(--light-color-ml);">
567
+ <div id="bulkActionsBar" style="display:none; padding: 0.5rem; border-bottom: 1px solid var(--gray-light-color-ml); background: var(--light-color-ml);">
465
568
  <span id="bulkSelectedCount">0 selected</span>
466
569
  <div style="float: right;">
467
570
  <button class="btn btn-danger btn-sm" id="bulkDeleteBtn" disabled>
@@ -502,63 +605,137 @@ function renderFilesTable(files, folders = [], prefix = '') {
502
605
 
503
606
  // Render pagination
504
607
  function renderPagination(data) {
505
- const { currentPage, totalPages, hasPrevPage, hasNextPage } = data;
608
+ const { currentPage, totalPages, hasPrevPage, hasNextPage, mode } = data;
506
609
 
507
610
  let paginationHTML = '';
508
611
 
509
- // Previous button
510
- if (hasPrevPage) {
511
- paginationHTML += `
512
- <li class="page-item">
513
- <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${currentPage - 1}, '${currentPrefix}', '${currentSearch}')">
514
- <i class="fas fa-chevron-left"></i> Previous
515
- </a>
516
- </li>
517
- `;
518
- } else {
612
+ if (mode === 'optimized') {
613
+ // Optimized Mode: Token-based pagination (Next only, limited Prev)
614
+
615
+ // Previous button (Disabled for now as we don't track history stack of tokens)
519
616
  paginationHTML += `
520
617
  <li class="page-item disabled">
521
- <span class="page-link">
618
+ <span class="page-link" title="Previous page not supported in optimized view">
522
619
  <i class="fas fa-chevron-left"></i> Previous
523
620
  </span>
524
621
  </li>
525
622
  `;
526
- }
527
-
528
- // Page numbers
529
- for (let i = 1; i <= totalPages; i++) {
530
- if (i === currentPage) {
531
- paginationHTML += `
623
+
624
+ // Current Page Indicator
625
+ paginationHTML += `
532
626
  <li class="page-item active">
533
- <span class="page-link">${i}</span>
627
+ <span class="page-link">${currentPage}</span>
534
628
  </li>
535
- `;
536
- } else {
537
- paginationHTML += `
629
+ `;
630
+
631
+ // Next button
632
+ if (hasNextPage) {
633
+ // Quote the token correctly
634
+ const tokenStr = currentNextToken ? currentNextToken.replace(/'/g, "\\'") : "";
635
+ paginationHTML += `
538
636
  <li class="page-item">
539
- <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${i}, '${currentPrefix}', '${currentSearch}')">${i}</a>
637
+ <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${currentPage + 1}, '${currentPrefix}', '${currentSearch}', '${currentSort}', '${currentOrder}', '${tokenStr}')">
638
+ Next <i class="fas fa-chevron-right"></i>
639
+ </a>
540
640
  </li>
541
641
  `;
542
- }
543
- }
642
+ } else {
643
+ paginationHTML += `
644
+ <li class="page-item disabled">
645
+ <span class="page-link">
646
+ Next <i class="fas fa-chevron-right"></i>
647
+ </span>
648
+ </li>
649
+ `;
650
+ }
544
651
 
545
- // Next button
546
- if (hasNextPage) {
547
- paginationHTML += `
548
- <li class="page-item">
549
- <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${currentPage + 1}, '${currentPrefix}', '${currentSearch}')">
550
- Next <i class="fas fa-chevron-right"></i>
551
- </a>
552
- </li>
553
- `;
554
652
  } else {
555
- paginationHTML += `
556
- <li class="page-item disabled">
557
- <span class="page-link">
558
- Next <i class="fas fa-chevron-right"></i>
559
- </span>
560
- </li>
561
- `;
653
+ // Legacy Mode: Numbered pagination
654
+
655
+ // Previous button
656
+ if (hasPrevPage) {
657
+ paginationHTML += `
658
+ <li class="page-item">
659
+ <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${currentPage - 1}, '${currentPrefix}', '${currentSearch}')">
660
+ <i class="fas fa-chevron-left"></i> Previous
661
+ </a>
662
+ </li>
663
+ `;
664
+ } else {
665
+ paginationHTML += `
666
+ <li class="page-item disabled">
667
+ <span class="page-link">
668
+ <i class="fas fa-chevron-left"></i> Previous
669
+ </span>
670
+ </li>
671
+ `;
672
+ }
673
+
674
+ // Smart Page Numbers (limit visible pages)
675
+ const maxPagesToShow = 7;
676
+ let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2));
677
+ let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1);
678
+
679
+ if (endPage - startPage + 1 < maxPagesToShow) {
680
+ startPage = Math.max(1, endPage - maxPagesToShow + 1);
681
+ }
682
+
683
+ // First page
684
+ if (startPage > 1) {
685
+ paginationHTML += `
686
+ <li class="page-item">
687
+ <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(1, '${currentPrefix}', '${currentSearch}')">1</a>
688
+ </li>`;
689
+ if (startPage > 2) {
690
+ paginationHTML += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
691
+ }
692
+ }
693
+
694
+ for (let i = startPage; i <= endPage; i++) {
695
+ if (i === currentPage) {
696
+ paginationHTML += `
697
+ <li class="page-item active">
698
+ <span class="page-link">${i}</span>
699
+ </li>
700
+ `;
701
+ } else {
702
+ paginationHTML += `
703
+ <li class="page-item">
704
+ <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${i}, '${currentPrefix}', '${currentSearch}')">${i}</a>
705
+ </li>
706
+ `;
707
+ }
708
+ }
709
+
710
+ // Last page
711
+ if (endPage < totalPages) {
712
+ if (endPage < totalPages - 1) {
713
+ paginationHTML += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
714
+ }
715
+ paginationHTML += `
716
+ <li class="page-item">
717
+ <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${totalPages}, '${currentPrefix}', '${currentSearch}')">${totalPages}</a>
718
+ </li>`;
719
+ }
720
+
721
+ // Next button
722
+ if (hasNextPage) {
723
+ paginationHTML += `
724
+ <li class="page-item">
725
+ <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${currentPage + 1}, '${currentPrefix}', '${currentSearch}')">
726
+ Next <i class="fas fa-chevron-right"></i>
727
+ </a>
728
+ </li>
729
+ `;
730
+ } else {
731
+ paginationHTML += `
732
+ <li class="page-item disabled">
733
+ <span class="page-link">
734
+ Next <i class="fas fa-chevron-right"></i>
735
+ </span>
736
+ </li>
737
+ `;
738
+ }
562
739
  }
563
740
 
564
741
  document.getElementById('paginationList').innerHTML = paginationHTML;
@@ -665,10 +842,10 @@ function renderUploadQueue() {
665
842
  container.innerHTML = uploadQueue.map(item => `
666
843
  <div class="upload-item" data-id="${item.id}">
667
844
  <div class="meta">
668
- <div class="file-icon"><i class="fas fa-file"></i></div>
669
- <div>
845
+ <div style="display:flex; align-items:center; gap:0.75rem; min-width:0; width:100%; margin-bottom:5px;">
846
+ <div class="file-icon"><i class="fas fa-file"></i></div>
670
847
  <div class="name">${item.file.name}</div>
671
- <div class="form-text">${formatFileSize(item.file.size)}</div>
848
+ <div class="form-text file-size" style="white-space:nowrap; color:var(--text-muted-color-ml); font-size:0.875rem; margin-left:auto;">${formatFileSize(item.file.size)}</div>
672
849
  </div>
673
850
  </div>
674
851
  <div style="min-width:220px;">
@@ -828,35 +1005,51 @@ function startAllUploads() {
828
1005
  document.getElementById('startUploadBtn')?.addEventListener('click', startAllUploads);
829
1006
  document.getElementById('clearQueueBtn')?.addEventListener('click', function () { uploadQueue.length = 0; renderUploadQueue(); });
830
1007
 
831
- // Reset upload form
832
- function resetUploadForm() {
833
- const uploadBtn = document.getElementById('uploadBtn');
834
- const uploadProgress = document.getElementById('uploadProgress');
835
1008
 
836
- setTimeout(() => {
837
- uploadBtn.disabled = false;
838
- uploadBtn.classList.remove('uploading');
839
- uploadBtn.innerHTML = '<i class="fas fa-cloud-upload-alt"></i> Upload File';
840
- uploadProgress.style.display = 'none';
841
-
842
- // Reset progress
843
- document.getElementById('progressBar').style.width = '0%';
844
- document.getElementById('uploadPercent').textContent = '0%';
845
- }, 3000);
1009
+
1010
+ // Initialize upload section collapse functionality
1011
+ function initializeUploadCollapse() {
1012
+ const toggleBtn = document.getElementById('uploadToggleBtn');
1013
+ const uploadSection = document.getElementById('uploadSection');
1014
+
1015
+ if (!toggleBtn || !uploadSection) return;
1016
+
1017
+ toggleBtn.addEventListener('click', function() {
1018
+ const isExpanded = uploadSection.classList.contains('expanded');
1019
+
1020
+ if (isExpanded) {
1021
+ // Collapse
1022
+ uploadSection.classList.remove('expanded');
1023
+ toggleBtn.classList.remove('expanded');
1024
+ } else {
1025
+ // Expand
1026
+ uploadSection.classList.add('expanded');
1027
+ toggleBtn.classList.add('expanded');
1028
+ }
1029
+ });
846
1030
  }
847
1031
 
848
1032
  // Add hover effects and animations
849
1033
  document.addEventListener('DOMContentLoaded', function () {
1034
+ // Initialize collapsible upload section
1035
+ initializeUploadCollapse();
1036
+
850
1037
  // Load initial files (support both legacy 'prefix' and new 'folder' query param)
851
1038
  const urlParams = new URLSearchParams(window.location.search);
852
1039
  const initialPrefix = urlParams.get('folder') || urlParams.get('prefix') || bucketAppName || '';
853
1040
  const initialPage = parseInt(urlParams.get('page')) || 1;
854
1041
  loadFiles(initialPage, initialPrefix);
855
1042
 
856
- // Debounced search input (improves UX and reduces API calls)
1043
+ // Search behavior: Trigger when the Search button is clicked OR when the user presses Enter in the search input
857
1044
  const searchInputEl = document.getElementById('searchInput');
858
- if (searchInputEl && typeof debounce === 'function') {
859
- searchInputEl.addEventListener('input', debounce(() => performSearch(), 350));
1045
+ if (searchInputEl) {
1046
+ // Trigger search when Enter is pressed in the input
1047
+ searchInputEl.addEventListener('keydown', function (e) {
1048
+ if (e.key === 'Enter') {
1049
+ e.preventDefault();
1050
+ performSearch();
1051
+ }
1052
+ });
860
1053
  }
861
1054
 
862
1055
  // Close modal when clicking outside
@@ -997,6 +1190,7 @@ function showPreviewModal(encodedKey, filename, extension) {
997
1190
  contentEl.appendChild(iframe);
998
1191
  } else if (textTypes.includes(extension)) {
999
1192
  dialog.classList.add('preview-compact');
1193
+
1000
1194
  fetch(`/mbkbucket/view/${encodedKey}`)
1001
1195
  .then(r => r.text())
1002
1196
  .then(text => {
@@ -1223,9 +1417,47 @@ async function createFolder() {
1223
1417
  }
1224
1418
  }
1225
1419
 
1226
- // Start/clear upload buttons
1227
- document.getElementById('startUploadBtn')?.addEventListener('click', startAllUploads);
1228
- document.getElementById('clearQueueBtn')?.addEventListener('click', function () { uploadQueue.length = 0; renderUploadQueue(); });
1420
+ // Create new file
1421
+ async function createFile() {
1422
+ let name = prompt('Enter new file name:');
1423
+ if (!name) return;
1424
+
1425
+ name = name.trim();
1426
+ if (!name.includes('.')) {
1427
+ name += '.txt';
1428
+ }
1429
+
1430
+ // Ensure no leading slashes, we upload to currentPrefix
1431
+ if (name.startsWith('/')) name = name.substring(1);
1432
+
1433
+ // Check if valid
1434
+ if (!name) return showAlert('Invalid file name', 'error');
1435
+
1436
+ // Create empty file content
1437
+ const file = new File([" "], name, { type: "text/plain" });
1438
+ const formData = new FormData();
1439
+ formData.append('file', file);
1440
+ formData.append('prefix', currentPrefix);
1441
+
1442
+ try {
1443
+ const resp = await fetch('/mbkbucket/upload', {
1444
+ method: 'POST',
1445
+ body: formData
1446
+ });
1447
+
1448
+ const json = await resp.json();
1449
+ if (json.success) {
1450
+ showAlert('File created', 'success');
1451
+ loadFiles(currentPage, currentPrefix);
1452
+ } else {
1453
+ showAlert(json.error || 'Create file failed', 'error');
1454
+ }
1455
+ } catch (err) {
1456
+ showAlert('Create file failed: ' + err.message, 'error');
1457
+ }
1458
+ }
1459
+
1460
+
1229
1461
 
1230
1462
  // mbkbucket shared utilities (exposed to global window for backward compatibility)
1231
1463
  (function (w) {