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.
- package/index.d.ts +1 -1
- package/index.js +16 -2
- package/lib/bucket.js +111 -48
- package/lib/s3.js +9 -3
- package/package.json +2 -2
- package/public/{css/bucketadmin.css → bucketadmin.css} +271 -51
- package/public/{js/bucketadmincontent.js → bucketadmin.js} +359 -127
- package/views/bucket.handlebars +2 -2
- package/views/bucketadmincontent.handlebars +44 -58
|
@@ -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
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
messageEl
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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;">×</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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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>
|
|
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-
|
|
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.
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
-
|
|
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">${
|
|
627
|
+
<span class="page-link">${currentPage}</span>
|
|
534
628
|
</li>
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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(${
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
|
669
|
-
|
|
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
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
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
|
-
//
|
|
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
|
|
859
|
-
|
|
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
|
-
//
|
|
1227
|
-
|
|
1228
|
-
|
|
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) {
|