mbkbucket 3.0.1 → 3.2.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.
@@ -1,2134 +1,1222 @@
1
- // Global state
2
- let currentPage = 1;
3
- let currentPrefix = '';
4
- let currentSearch = '';
5
- let currentSort = 'name';
6
- let currentOrder = 'asc';
7
- let currentViewMode = 'folder'; // 'folder' or 'flat'
8
- let pageFiles = [];
9
- let pageFolders = [];
10
- let currentNextToken = null;
11
- let currentApiPrefix = '';
12
- const bucketAppName = document.getElementById('bucketAppData')?.dataset.appName || '';
13
- const selectedBucket = new URLSearchParams(window.location.search).get('bucket') || '';
14
- let activeBucket = selectedBucket;
15
- const app = String(bucketAppName || '').toLowerCase();
16
- const isRootApp = app === 'portal' || app === 'mbkbucket';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
- }
39
-
40
1
  /**
41
- * Download a file via fetch + Blob, which allows multiple concurrent downloads
42
- * (unlike <a href> navigation which cancels the previous download).
43
- * @param {string} url - The download URL
44
- * @param {string} filename - The filename to save as
2
+ * MBKBucket File Manager - Gentelella v4 UI & Client Logic
45
3
  */
46
- async function downloadFileViaFetch(url, filename) {
47
- try {
48
- const response = await fetch(url);
49
- if (!response.ok) {
50
- const err = await response.json().catch(() => ({}));
51
- throw new Error(err.message || err.error || `Download failed (${response.status})`);
52
- }
53
- const blob = await response.blob();
54
- const objectUrl = URL.createObjectURL(blob);
55
- const a = document.createElement('a');
56
- a.href = objectUrl;
57
- a.download = filename;
58
- document.body.appendChild(a);
59
- a.click();
60
- document.body.removeChild(a);
61
- // Revoke the object URL after a short delay to allow the browser to start the download
62
- setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);
63
- } catch (err) {
64
- console.error('Download failed:', err);
65
- alert('Download failed: ' + err.message);
66
- }
67
- }
68
4
 
69
- function stripApiPrefixFromPath(path, apiPrefix = '') {
70
- const normalizedPath = String(path || '').replace(/^\/+/, '');
5
+ (function () {
6
+ 'use strict';
7
+
8
+ // Global State
9
+ let currentPage = 1;
10
+ let currentPrefix = '';
11
+ let currentSearch = '';
12
+ let currentSort = 'name';
13
+ let currentOrder = 'asc';
14
+ let currentViewMode = localStorage.getItem('mbk_view_mode') || 'grid'; // 'grid' or 'list'
15
+ let currentQuickFilter = 'all'; // 'all', 'media', 'docs', 'code', 'starred'
16
+ let pageFiles = [];
17
+ let pageFolders = [];
18
+ let currentApiPrefix = '';
19
+ let currentNextToken = null;
20
+ let selectedFiles = new Set();
21
+ let starredFiles = new Set(JSON.parse(localStorage.getItem('mbk_starred_keys') || '[]'));
22
+
23
+ // Upload State
24
+ let uploadQueue = [];
25
+ let isUploading = false;
26
+
27
+ // DOM Elements & Config
28
+ const bucketAppData = document.getElementById('bucketAppData');
29
+ const bucketAppName = bucketAppData?.dataset.appName || '';
30
+ const urlParams = new URLSearchParams(window.location.search);
31
+ const selectedBucket = urlParams.get('bucket') || '';
32
+ let activeBucket = selectedBucket;
33
+ const app = String(bucketAppName || '').toLowerCase();
34
+ const isRootApp = app === 'portal' || app === 'mbkbucket';
35
+ const rootLabel = isRootApp ? 'My Drive' : (bucketAppName || 'Bucket Drive');
36
+
37
+ // File categories & extensions
38
+ const MEDIA_EXTS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'mp4', 'webm', 'ogg', 'avi', 'mov', 'mp3', 'wav', 'flac', 'aac', 'm4a']);
39
+ const DOC_EXTS = new Set(['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'md', 'csv', 'rtf']);
40
+ const CODE_EXTS = new Set(['js', 'ts', 'jsx', 'tsx', 'html', 'htm', 'css', 'scss', 'php', 'py', 'java', 'cpp', 'c', 'h', 'cs', 'rb', 'go', 'rs', 'sql', 'sh', 'bat', 'ps1', 'yaml', 'yml', 'toml', 'ini', 'conf', 'json', 'xml', 'log']);
41
+
42
+ const FILE_TYPES = {
43
+ pdf: { color: 'var(--red)', bg: 'var(--red-lt)', label: 'PDF' },
44
+ doc: { color: 'var(--blue)', bg: 'var(--blue-lt)', label: 'DOC' },
45
+ docx: { color: 'var(--blue)', bg: 'var(--blue-lt)', label: 'DOC' },
46
+ xls: { color: 'var(--green)', bg: 'var(--green-lt)', label: 'XLS' },
47
+ xlsx: { color: 'var(--green)', bg: 'var(--green-lt)', label: 'XLS' },
48
+ csv: { color: 'var(--green)', bg: 'var(--green-lt)', label: 'CSV' },
49
+ ppt: { color: 'var(--orange)', bg: 'rgba(247, 103, 7, 0.1)', label: 'PPT' },
50
+ pptx: { color: 'var(--orange)', bg: 'rgba(247, 103, 7, 0.1)', label: 'PPT' },
51
+ txt: { color: 'var(--text-secondary)', bg: 'var(--bg-surface-secondary)', label: 'TXT' },
52
+ md: { color: 'var(--purple)', bg: 'var(--purple-lt)', label: 'MD' },
53
+ jpg: { color: 'var(--yellow)', bg: 'var(--yellow-lt)', label: 'IMG' },
54
+ jpeg: { color: 'var(--yellow)', bg: 'var(--yellow-lt)', label: 'IMG' },
55
+ png: { color: 'var(--yellow)', bg: 'var(--yellow-lt)', label: 'IMG' },
56
+ gif: { color: 'var(--yellow)', bg: 'var(--yellow-lt)', label: 'GIF' },
57
+ webp: { color: 'var(--yellow)', bg: 'var(--yellow-lt)', label: 'IMG' },
58
+ svg: { color: 'var(--orange)', bg: 'rgba(247, 103, 7, 0.1)', label: 'SVG' },
59
+ mp4: { color: 'var(--azure)', bg: 'var(--azure-lt)', label: 'VID' },
60
+ webm: { color: 'var(--azure)', bg: 'var(--azure-lt)', label: 'VID' },
61
+ mov: { color: 'var(--azure)', bg: 'var(--azure-lt)', label: 'VID' },
62
+ mp3: { color: 'var(--purple)', bg: 'var(--purple-lt)', label: 'AUD' },
63
+ wav: { color: 'var(--purple)', bg: 'var(--purple-lt)', label: 'AUD' },
64
+ zip: { color: 'var(--text-secondary)', bg: 'var(--bg-surface-secondary)', label: 'ZIP' },
65
+ tar: { color: 'var(--text-secondary)', bg: 'var(--bg-surface-secondary)', label: 'TAR' },
66
+ gz: { color: 'var(--text-secondary)', bg: 'var(--bg-surface-secondary)', label: 'GZ' },
67
+ code: { color: 'var(--cyan)', bg: 'var(--cyan-lt)', label: 'CODE' }
68
+ };
69
+
70
+ // SVGs
71
+ const ICONS = {
72
+ folder: '<svg class="folder-svg" viewBox="0 0 32 32" fill="var(--primary-lt)" stroke="var(--primary)" stroke-width="1.3"><path d="M4 8a2 2 0 012-2h6l3 3h11a2 2 0 012 2v13a2 2 0 01-2 2H6a2 2 0 01-2-2V8z"/></svg>',
73
+ folderSm: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 4a1 1 0 011-1h3l2 2h5a1 1 0 011 1v6a1 1 0 01-1 1H3a1 1 0 01-1-1V4z"/></svg>',
74
+ drive: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 6l4-4h4l4 4v8H2V6z"/></svg>',
75
+ starred: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 1l2 5 5 .5-4 3.5 1 5-4-2.5-4 2.5 1-5-4-3.5 5-.5z"/></svg>',
76
+ media: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="2" width="12" height="12" rx="2"/><circle cx="5.5" cy="5.5" r="1.5"/><path d="M14 10l-4-4-6 6"/></svg>',
77
+ docs: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 2h7l4 4v8a1 1 0 01-1 1H3a1 1 0 01-1-1V3a1 1 0 011-1z"/><path d="M10 2v4h4"/></svg>',
78
+ code: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M5 5L2 8l3 3M11 5l3 3-3 3M9 3L7 13"/></svg>',
79
+ more: '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><circle cx="8" cy="3" r="1.2"/><circle cx="8" cy="8" r="1.2"/><circle cx="8" cy="13" r="1.2"/></svg>'
80
+ };
81
+
82
+ // Helper Functions
83
+ function escapeHtml(str) {
84
+ return String(str || '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
85
+ }
86
+
87
+ function escapeAttr(str) {
88
+ return escapeHtml(str);
89
+ }
90
+
91
+ function getBaseName(path) {
92
+ const val = String(path || '').replace(/\/+$/, '');
93
+ const lastSlash = val.lastIndexOf('/');
94
+ return lastSlash === -1 ? val : val.substring(lastSlash + 1);
95
+ }
96
+
97
+ function getFileExt(fileName) {
98
+ const name = getBaseName(fileName);
99
+ const dot = name.lastIndexOf('.');
100
+ return dot > -1 ? name.slice(dot + 1).toLowerCase() : '';
101
+ }
102
+
103
+ function formatFileSize(bytes) {
104
+ if (!bytes || bytes === 0) return '0 B';
105
+ const k = 1024;
106
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
107
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
108
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
109
+ }
110
+
111
+ function formatDate(dateStr) {
112
+ if (!dateStr) return '—';
113
+ const d = new Date(dateStr);
114
+ if (isNaN(d.getTime())) return String(dateStr);
115
+ return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
116
+ }
117
+
118
+ function withBucketUrl(url) {
119
+ if (!activeBucket) return url;
120
+ const sep = url.includes('?') ? '&' : '?';
121
+ return `${url}${sep}bucket=${encodeURIComponent(activeBucket)}`;
122
+ }
123
+
124
+ function getViewUrl(key) {
125
+ return withBucketUrl(`/mbkbucket/view/${encodeURIComponent(key)}`);
126
+ }
127
+
128
+ function getDownloadUrl(key) {
129
+ return withBucketUrl(`/mbkbucket/download/${encodeURIComponent(key)}`);
130
+ }
131
+
132
+ function isImage(fileName) {
133
+ const ext = getFileExt(fileName);
134
+ return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp'].includes(ext);
135
+ }
136
+
137
+ function isVideo(fileName) {
138
+ const ext = getFileExt(fileName);
139
+ return ['mp4', 'webm', 'ogg', 'mov', 'mkv'].includes(ext);
140
+ }
141
+
142
+ function isAudio(fileName) {
143
+ const ext = getFileExt(fileName);
144
+ return ['mp3', 'wav', 'ogg', 'flac', 'm4a', 'aac'].includes(ext);
145
+ }
146
+
147
+ function isTextOrCode(fileName) {
148
+ const ext = getFileExt(fileName);
149
+ return CODE_EXTS.has(ext) || ['txt', 'md', 'csv', 'log', 'json', 'xml', 'env'].includes(ext);
150
+ }
151
+
152
+ function isPdf(fileName) {
153
+ return getFileExt(fileName) === 'pdf';
154
+ }
155
+
156
+ function getFileTypeConfig(fileName) {
157
+ const ext = getFileExt(fileName);
158
+ if (FILE_TYPES[ext]) return FILE_TYPES[ext];
159
+ if (CODE_EXTS.has(ext)) return FILE_TYPES.code;
160
+ return { color: 'var(--text-secondary)', bg: 'var(--bg-surface-secondary)', label: ext.toUpperCase().slice(0, 4) || 'FILE' };
161
+ }
162
+
163
+ function stripApiPrefixFromPath(path, apiPrefix = '') {
164
+ const normalizedPath = String(path || '').replace(/^\/+/, '').replace(/\/+$/, '');
71
165
  const normalizedApiPrefix = String(apiPrefix || '').replace(/^\/+/, '').replace(/\/+$/, '');
72
-
73
166
  if (!normalizedApiPrefix) return normalizedPath;
74
167
  if (normalizedPath === normalizedApiPrefix || normalizedPath === `${normalizedApiPrefix}/`) return '';
75
168
  if (normalizedPath.startsWith(`${normalizedApiPrefix}/`)) {
76
- return normalizedPath.slice(normalizedApiPrefix.length + 1);
169
+ return normalizedPath.slice(normalizedApiPrefix.length + 1);
77
170
  }
78
-
79
171
  return normalizedPath;
80
- }
172
+ }
81
173
 
82
- function toApiFolderPath(prefix = '', apiPrefix = '') {
83
- const uiPrefix = String(prefix || '').replace(/^\/+/, '');
174
+ function toApiFolderPath(prefix = '', apiPrefix = '') {
175
+ const uiPrefix = String(prefix || '').replace(/^\/+/, '').replace(/\/+$/, '');
84
176
  const normalizedApiPrefix = String(apiPrefix || '').replace(/^\/+/, '').replace(/\/+$/, '');
85
-
86
177
  if (!uiPrefix) return '';
87
178
  if (!normalizedApiPrefix) return uiPrefix;
88
- if (uiPrefix === normalizedApiPrefix || uiPrefix.startsWith(`${normalizedApiPrefix}/`)) {
89
- return uiPrefix;
90
- }
179
+ if (uiPrefix === normalizedApiPrefix || uiPrefix.startsWith(`${normalizedApiPrefix}/`)) return uiPrefix;
91
180
  return `${normalizedApiPrefix}/${uiPrefix}`;
92
- }
93
-
94
- function escapeHtml(value = '') {
95
- return String(value)
96
- .replace(/&/g, '&amp;')
97
- .replace(/</g, '&lt;')
98
- .replace(/>/g, '&gt;')
99
- .replace(/"/g, '&quot;')
100
- .replace(/'/g, '&#39;');
101
- }
102
-
103
- function escapeAttr(value = '') {
104
- return escapeHtml(value);
105
- }
106
-
107
- function getBaseName(path = '') {
108
- const value = String(path || '');
109
- const lastSlashIndex = value.lastIndexOf('/');
110
- return lastSlashIndex === -1 ? value : value.substring(lastSlashIndex + 1);
111
- }
112
-
113
- function dataAttr(name, value = '') {
114
- return `data-${name}="${escapeAttr(value)}"`;
115
- }
116
-
117
- function mutedDash() {
118
- return '<span class="muted-cell-value">&mdash;</span>';
119
- }
120
-
121
- function syncStateUrl(page = currentPage, prefix = currentPrefix, search = currentSearch) {
181
+ }
182
+
183
+ function extractFoldersAtCurrentLevel(files, prefix = '') {
184
+ const folderSet = new Set();
185
+ const cleanPrefix = String(prefix || '').replace(/^\/+/, '').replace(/\/+$/, '');
186
+ const prefixOffset = cleanPrefix ? cleanPrefix.length + 1 : 0;
187
+
188
+ (files || []).forEach(file => {
189
+ const key = String(file.Key || '').replace(/^\/+/, '');
190
+ if (cleanPrefix && !key.startsWith(cleanPrefix + '/')) return;
191
+ const relPath = cleanPrefix ? key.substring(prefixOffset) : key;
192
+ const slashIdx = relPath.indexOf('/');
193
+ if (slashIdx !== -1) {
194
+ const folder = relPath.substring(0, slashIdx);
195
+ folderSet.add(cleanPrefix ? `${cleanPrefix}/${folder}` : folder);
196
+ }
197
+ });
198
+
199
+ return Array.from(folderSet).sort();
200
+ }
201
+
202
+ function syncStateUrl() {
122
203
  try {
123
- const parts = [];
124
- if (activeBucket) parts.push(`bucket=${encodeURIComponent(activeBucket)}`);
125
- if (prefix) {
126
- const folderPathForUrl = toApiFolderPath(prefix, currentApiPrefix);
127
- const folderEncoded = encodeURIComponent(folderPathForUrl).replace(/%2F/g, '/');
128
- parts.push(`folder=${folderEncoded}`);
129
- }
130
- if (page && page > 1) parts.push(`page=${page}`);
131
- if (search) parts.push(`search=${encodeURIComponent(search)}`);
132
- parts.push(`sort=${encodeURIComponent(currentSort)}`);
133
- parts.push(`order=${encodeURIComponent(currentOrder)}`);
134
- parts.push(`view=${encodeURIComponent(currentViewMode)}`);
135
- parts.push(`filter=${encodeURIComponent(currentQuickFilter)}`);
136
-
137
- const newUrl = '/mbkbucket' + (parts.length ? '?' + parts.join('&') : '');
138
- window.history.replaceState(null, '', newUrl);
204
+ const parts = [];
205
+ if (activeBucket) parts.push(`bucket=${encodeURIComponent(activeBucket)}`);
206
+ if (currentPrefix) {
207
+ const folderPath = toApiFolderPath(currentPrefix, currentApiPrefix);
208
+ parts.push(`folder=${encodeURIComponent(folderPath).replace(/%2F/g, '/')}`);
209
+ }
210
+ if (currentPage > 1) parts.push(`page=${currentPage}`);
211
+ if (currentSearch) parts.push(`search=${encodeURIComponent(currentSearch)}`);
212
+ if (currentSort !== 'name') parts.push(`sort=${encodeURIComponent(currentSort)}`);
213
+ if (currentOrder !== 'asc') parts.push(`order=${encodeURIComponent(currentOrder)}`);
214
+ if (currentViewMode !== 'grid') parts.push(`view=${encodeURIComponent(currentViewMode)}`);
215
+ if (currentQuickFilter !== 'all') parts.push(`filter=${encodeURIComponent(currentQuickFilter)}`);
216
+
217
+ const newUrl = '/mbkbucket' + (parts.length ? '?' + parts.join('&') : '');
218
+ window.history.replaceState(null, '', newUrl);
139
219
  } catch (e) {
140
- console.warn('history update failed', e);
220
+ console.warn('History update failed', e);
141
221
  }
142
- }
143
-
144
- function getActiveBucketLabel() {
145
- return activeBucket || 'default bucket';
146
- }
147
-
148
- function matchesQuickFilter(fileName = '', filter = currentQuickFilter) {
149
- const ext = getBaseName(fileName).split('.').pop().toLowerCase();
150
- if (filter === 'media') return MEDIA_EXTENSIONS.has(ext);
151
- if (filter === 'docs') return DOC_EXTENSIONS.has(ext);
152
- if (filter === 'code') return CODE_EXTENSIONS.has(ext);
153
- return true;
154
- }
155
-
156
- function updateQuickFilterUI() {
157
- document.querySelectorAll('.quick-filter-btn').forEach((btn) => {
158
- btn.classList.toggle('active', btn.dataset.filter === currentQuickFilter);
159
- });
160
- }
222
+ }
161
223
 
162
- // Show alert message (Toast)
163
- function showAlert(message, type = 'success') {
224
+ // Toast System
225
+ function showToast(message, type = 'success') {
164
226
  const container = document.getElementById('toast-container');
165
- if (!container) {
166
- // Fallback to old method if container missing
167
- const alertId = type === 'success' ? 'successAlert' : 'errorAlert';
168
- const messageId = type === 'success' ? 'successMessage' : 'errorMessage';
169
- const alertEl = document.getElementById(alertId);
170
- const messageEl = document.getElementById(messageId);
171
- if (alertEl && messageEl) {
172
- messageEl.textContent = message;
173
- alertEl.style.display = 'flex';
174
- setTimeout(() => alertEl.style.display = 'none', 5000);
175
- }
176
- return;
177
- }
227
+ if (!container) return;
178
228
 
179
- // Create toast element
180
229
  const toast = document.createElement('div');
181
- const isSuccess = type === 'success';
182
230
  toast.className = `toast-notification ${type}`;
183
- toast.style.cssText = `
184
- background: ${isSuccess ? 'linear-gradient(135deg, #10b981, #059669)' : 'linear-gradient(135deg, #ef4444, #dc2626)'};
185
- color: white;
186
- padding: 1rem 1.5rem;
187
- border-radius: 8px;
188
- box-shadow: 0 4px 12px rgba(0,0,0,0.15);
189
- display: flex;
190
- align-items: center;
191
- gap: 12px;
192
- min-width: 300px;
193
- transform: translateX(100%);
194
- opacity: 0;
195
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
196
- font-weight: 500;
197
- `;
198
-
199
231
  toast.innerHTML = `
200
- <i class="fas ${isSuccess ? 'fa-check-circle' : 'fa-exclamation-circle'} toast-icon"></i>
201
- <div class="toast-message">${message}</div>
202
- <button type="button" data-action="close-toast" class="toast-close-btn">&times;</button>
232
+ <span>${escapeHtml(message)}</span>
233
+ <button type="button" class="toast-close-btn">&times;</button>
203
234
  `;
204
235
 
205
- container.appendChild(toast);
206
-
207
- // Animate in
208
- requestAnimationFrame(() => {
209
- toast.style.transform = 'translateX(0)';
210
- toast.style.opacity = '1';
236
+ toast.querySelector('.toast-close-btn').addEventListener('click', () => {
237
+ toast.remove();
211
238
  });
212
239
 
213
- // Auto dismiss
214
- setTimeout(() => {
215
- toast.style.transform = 'translateX(100%)';
216
- toast.style.opacity = '0';
217
- setTimeout(() => toast.remove(), 300);
218
- }, 5000);
219
- }
220
-
221
- // Fetch and render files
222
- async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort, order = currentOrder, token = '') {
223
- currentPage = page;
224
- currentPrefix = prefix;
225
- currentSearch = search;
226
- currentSort = sort;
227
- currentOrder = order;
228
- virtualRenderLimit = 0;
229
-
230
- // Update browser URL so navigation is shareable/bookmarkable.
231
- syncStateUrl(page, prefix, search);
240
+ container.appendChild(toast);
232
241
 
233
- const container = document.getElementById('fileListContainer');
234
- const paginationContainer = document.getElementById('paginationContainer');
235
- const fileListTitle = document.getElementById('fileListTitle');
236
- const totalFilesCount = document.getElementById('totalFilesCount');
237
- const filterInfo = document.getElementById('filterInfo');
238
-
239
- // Show loading skeleton state
240
- container.innerHTML = `
241
- <div class="table-responsive">
242
- <div class="skeleton-row">
243
- <div class="skeleton-box s-40"></div>
244
- <div class="skeleton-box s-200"></div>
245
- <div class="skeleton-box s-80"></div>
246
- <div class="skeleton-box s-200"></div>
247
- </div>
248
- <div class="skeleton-row">
249
- <div class="skeleton-box s-40"></div>
250
- <div class="skeleton-box s-200"></div>
251
- <div class="skeleton-box s-80"></div>
252
- <div class="skeleton-box s-200"></div>
253
- </div>
242
+ setTimeout(() => {
243
+ toast.style.opacity = '0';
244
+ toast.style.transform = 'translateY(10px)';
245
+ setTimeout(() => toast.remove(), 250);
246
+ }, 4000);
247
+ }
248
+
249
+ // Tree Renderer
250
+ function renderTree() {
251
+ const treeEl = document.getElementById('fm-tree');
252
+ if (!treeEl) return;
253
+
254
+ const allFolders = pageFolders || [];
255
+ const isStarredActive = currentQuickFilter === 'starred';
256
+ const isRootActive = !currentPrefix && currentQuickFilter === 'all';
257
+
258
+ let treeHtml = `
259
+ <div class="tree-group">
260
+ <button type="button" class="tree-link ${isRootActive ? 'active' : ''}" data-tree-prefix="" data-tree-filter="all">
261
+ ${ICONS.drive}
262
+ <span>${escapeHtml(rootLabel)}</span>
263
+ </button>
254
264
  </div>
255
265
  `;
256
266
 
257
- try {
258
- const searchParam = search ? `&search=${encodeURIComponent(search)}` : '';
259
- const sortParam = `&sort=${sort}&order=${order}`;
260
-
261
- // Determine recursive mode
262
- const isFolderMode = currentViewMode === 'folder';
263
- const recursiveParam = `&recursive=${!isFolderMode}`; // false if folder mode, true if flat mode
264
- const tokenParam = token ? `&token=${encodeURIComponent(token)}` : '';
265
-
266
- const response = await fetch(withBucketUrl(`/mbkbucket/api/files?page=${page}&prefix=${encodeURIComponent(prefix)}${searchParam}${sortParam}${recursiveParam}${tokenParam}`));
267
- const data = await response.json();
268
-
269
- if (!data.success) {
270
- throw new Error(data.error || 'Failed to load files');
271
- }
272
-
273
- // Store next token
274
- currentNextToken = data.nextContinuationToken || null;
275
- currentApiPrefix = data.prefix || '';
276
-
277
- // Update stats
278
- if (data.totalFiles === -1) {
279
- totalFilesCount.textContent = `${(data.files.length + (data.folders ? data.folders.length : 0))}${data.hasNextPage ? '+' : ''} Items`;
280
- } else {
281
- totalFilesCount.textContent = `${data.totalFiles} ${search ? 'Results' : 'Files'}`;
282
- }
283
-
284
- if (search) {
285
- const filterSuffix = currentQuickFilter !== 'all' ? ` • Filter: ${currentQuickFilter}` : '';
286
- filterInfo.textContent = `Bucket: ${getActiveBucketLabel()} • Search: "${search}"${filterSuffix}`;
287
- fileListTitle.innerHTML = `Search Results <span class="text-muted">for: </span><span class="badge bg-info">${search}</span>`;
288
- } else {
289
- const label = prefix || rootLabel;
290
- const filterSuffix = currentQuickFilter !== 'all' ? ` • Filter: ${currentQuickFilter}` : '';
291
- filterInfo.textContent = `Bucket: ${getActiveBucketLabel()} • Folder: ${label}${filterSuffix}`;
292
- fileListTitle.innerHTML = `Files <span class="text-muted">in: </span><span class="badge bg-info">${label}</span>`;
293
- }
294
-
295
- // Render files or empty state
296
- const hasContent = (data.files && data.files.length > 0) || (data.folders && data.folders.length > 0);
297
-
298
- if (!hasContent && !prefix && !search) {
299
- container.innerHTML = `
300
- <div class="empty-state">
301
- <i class="fas fa-folder-open"></i>
302
- <h4>No files found</h4>
303
- <p>Bucket <strong>${escapeHtml(getActiveBucketLabel())}</strong> is empty. Upload some files to get started!</p>
304
- <button class="btn btn-lg" data-action="upload-files">
305
- <i class="fas fa-plus"></i> Upload Your First File
306
- </button>
267
+ // Folder hierarchy
268
+ if (allFolders.length > 0) {
269
+ treeHtml += `
270
+ <div class="tree-group">
271
+ <div class="tree-subs">
272
+ ${allFolders.map(folder => {
273
+ const cleanFolder = String(folder || '').replace(/\/+$/, '');
274
+ const folderName = getBaseName(cleanFolder);
275
+ const folderPathForAction = currentPrefix && !cleanFolder.startsWith(currentPrefix + '/') && cleanFolder !== currentPrefix
276
+ ? `${currentPrefix}/${cleanFolder}`
277
+ : cleanFolder;
278
+ const isFolderActive = currentPrefix === folderPathForAction;
279
+ return `
280
+ <button type="button" class="tree-link tree-sub ${isFolderActive ? 'active' : ''}" data-tree-prefix="${escapeAttr(folderPathForAction)}" data-tree-filter="all">
281
+ ${ICONS.folderSm}
282
+ <span>${escapeHtml(folderName)}</span>
283
+ </button>
284
+ `;
285
+ }).join('')}
307
286
  </div>
308
- `;
309
- paginationContainer.style.display = 'none';
310
- } else {
311
- // Store for client-side sorting and view switching
312
- pageFiles = data.files || [];
313
-
314
- if (data.mode === 'optimized') {
315
- pageFolders = (data.folders || []).map(folder => stripApiPrefixFromPath(folder, currentApiPrefix)).filter(Boolean);
316
- } else {
317
- pageFolders = search
318
- ? []
319
- : extractFoldersAtCurrentLevel(data.files, currentApiPrefix || prefix)
320
- .map(folder => stripApiPrefixFromPath(folder, currentApiPrefix))
321
- .filter(Boolean);
322
- }
323
-
324
- renderFilesTable(pageFiles, pageFolders, prefix, currentApiPrefix);
325
- renderPagination(data);
326
- paginationContainer.style.display = 'block';
327
- }
328
- } catch (error) {
329
- console.error("app: ", app);
330
- console.error("isRootApp: ", isRootApp, "rootLabel: ", rootLabel);
331
- console.error("bucketAppName : ", bucketAppName);
332
- console.error('Error loading files:', error);
333
- container.innerHTML = `
334
- <div class="empty-state">
335
- <i class="fas fa-exclamation-triangle empty-state-error-icon"></i>
336
- <h4>Error Loading Files</h4>
337
- <p>Bucket <strong>${escapeHtml(getActiveBucketLabel())}</strong>: ${escapeHtml(error.message)}</p>
338
- <button class="btn btn-primary" data-action="retry-load" ${dataAttr('retry-page', page)} ${dataAttr('retry-prefix', prefix)}>
339
- <i class="fas fa-redo"></i> Retry
340
- </button>
341
287
  </div>
342
288
  `;
343
- paginationContainer.style.display = 'none';
344
289
  }
345
- }
346
290
 
347
- // Toggle sort order
348
- function toggleSort(field) {
349
- if (currentSort === field) {
350
- currentOrder = currentOrder === 'asc' ? 'desc' : 'asc';
351
- } else {
352
- currentSort = field;
353
- currentOrder = 'asc';
354
- }
291
+ // Quick navigation filters
292
+ treeHtml += `
293
+ <div class="tree-group" style="margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--border-color-light);">
294
+ <button type="button" class="tree-link ${isStarredActive ? 'active' : ''}" data-tree-filter="starred">
295
+ ${ICONS.starred}
296
+ <span>Starred</span>
297
+ ${starredFiles.size > 0 ? `<span class="tree-badge">${starredFiles.size}</span>` : ''}
298
+ </button>
299
+ <button type="button" class="tree-link ${currentQuickFilter === 'media' ? 'active' : ''}" data-tree-filter="media">
300
+ ${ICONS.media}
301
+ <span>Media</span>
302
+ </button>
303
+ <button type="button" class="tree-link ${currentQuickFilter === 'docs' ? 'active' : ''}" data-tree-filter="docs">
304
+ ${ICONS.docs}
305
+ <span>Documents</span>
306
+ </button>
307
+ <button type="button" class="tree-link ${currentQuickFilter === 'code' ? 'active' : ''}" data-tree-filter="code">
308
+ ${ICONS.code}
309
+ <span>Code</span>
310
+ </button>
311
+ </div>
312
+ `;
355
313
 
356
- // Sort local data instead of calling API
357
- if (pageFiles && pageFiles.length > 0) {
358
- pageFiles.sort((a, b) => {
359
- let valA, valB;
360
- if (field === 'size') {
361
- valA = a.Size || 0;
362
- valB = b.Size || 0;
363
- } else if (field === 'date') {
364
- valA = new Date(a.LastModified || 0).getTime();
365
- valB = new Date(b.LastModified || 0).getTime();
366
- } else {
367
- valA = getBaseName(a.Key).toLowerCase();
368
- valB = getBaseName(b.Key).toLowerCase();
369
- }
370
-
371
- if (valA < valB) return currentOrder === 'asc' ? -1 : 1;
372
- if (valA > valB) return currentOrder === 'asc' ? 1 : -1;
373
- return 0;
374
- });
314
+ treeEl.innerHTML = treeHtml;
315
+ }
316
+
317
+ // Breadcrumbs Renderer
318
+ function renderBreadcrumbs() {
319
+ const bcEl = document.getElementById('fm-breadcrumb');
320
+ if (!bcEl) return;
375
321
 
376
- renderFilesTable(pageFiles, pageFolders, currentPrefix, currentApiPrefix);
377
- syncStateUrl(currentPage, currentPrefix, currentSearch);
322
+ if (!currentPrefix && currentQuickFilter === 'all') {
323
+ bcEl.innerHTML = `<button type="button" class="bc-link current">${escapeHtml(rootLabel)}</button>`;
324
+ return;
378
325
  }
379
- }
380
326
 
381
- // Perform search
382
- function performSearch() {
383
- const searchInput = document.getElementById('searchInput');
384
- const search = searchInput.value.trim();
385
- if (search) {
386
- loadFiles(1, currentPrefix, search);
327
+ if (currentQuickFilter !== 'all') {
328
+ bcEl.innerHTML = `
329
+ <button type="button" class="bc-link" data-nav-prefix="">${escapeHtml(rootLabel)}</button>
330
+ <span class="sep">›</span>
331
+ <button type="button" class="bc-link current">${escapeHtml(currentQuickFilter.charAt(0).toUpperCase() + currentQuickFilter.slice(1))}</button>
332
+ `;
333
+ return;
387
334
  }
388
- }
389
-
390
- // Clear search
391
- function clearSearch() {
392
- document.getElementById('searchInput').value = '';
393
- loadFiles(1, currentPrefix, '');
394
- }
395
-
396
- function setQuickFilter(filter = 'all') {
397
- const nextFilter = String(filter || 'all').toLowerCase();
398
- if (currentQuickFilter === nextFilter) return;
399
- currentQuickFilter = nextFilter;
400
- updateQuickFilterUI();
401
- renderFilesTable(pageFiles, pageFolders, currentPrefix, currentApiPrefix);
402
- syncStateUrl(currentPage, currentPrefix, currentSearch);
403
- }
404
-
405
- // Extract unique folders at current level
406
- function extractFoldersAtCurrentLevel(files, prefix) {
407
- const folderSet = new Set();
408
335
 
409
- files.forEach(file => {
410
- const key = file.Key;
411
- // Remove prefix from key
412
- const relativePath = prefix ? key.substring(prefix.length + (prefix.endsWith('/') ? 0 : 1)) : key;
413
-
414
- // Check if there's a folder in the relative path
415
- const slashIndex = relativePath.indexOf('/');
416
- if (slashIndex !== -1) {
417
- const folderName = relativePath.substring(0, slashIndex);
418
- const fullFolderPath = prefix ? `${prefix}/${folderName}` : folderName;
419
- folderSet.add(fullFolderPath);
420
- }
336
+ const parts = currentPrefix.split('/').filter(Boolean);
337
+ let accum = '';
338
+ const breadcrumbHtml = [
339
+ `<button type="button" class="bc-link" data-nav-prefix="">${escapeHtml(rootLabel)}</button>`
340
+ ];
341
+
342
+ parts.forEach((part, index) => {
343
+ accum += (accum ? '/' : '') + part;
344
+ const isLast = index === parts.length - 1;
345
+ breadcrumbHtml.push('<span class="sep">›</span>');
346
+ if (isLast) {
347
+ breadcrumbHtml.push(`<button type="button" class="bc-link current">${escapeHtml(part)}</button>`);
348
+ } else {
349
+ breadcrumbHtml.push(`<button type="button" class="bc-link" data-nav-prefix="${escapeAttr(accum)}">${escapeHtml(part)}</button>`);
350
+ }
421
351
  });
422
352
 
423
- return Array.from(folderSet).sort();
424
- }
353
+ bcEl.innerHTML = breadcrumbHtml.join('');
354
+ }
425
355
 
426
- // Change view mode
427
- function changeViewMode(mode) {
428
- if (currentViewMode === mode) return;
429
- currentViewMode = mode;
356
+ // File Filtering Logic
357
+ function getFilteredFiles() {
358
+ let list = (pageFiles || []).filter(f => !String(f.Key || '').endsWith('/'));
430
359
 
431
- // Update button states
432
- const btnFolder = document.getElementById('viewBtnFolder');
433
- const btnFlat = document.getElementById('viewBtnFlat');
360
+ // Search filter
361
+ if (currentSearch) {
362
+ const q = currentSearch.toLowerCase();
363
+ list = list.filter(f => getBaseName(f.Key).toLowerCase().includes(q));
364
+ }
434
365
 
435
- if (mode === 'folder') {
436
- btnFolder.classList.add('active');
437
- btnFolder.classList.remove('btn-outline-light');
438
- btnFolder.classList.add('btn-light');
366
+ // Quick filter
367
+ if (currentQuickFilter === 'media') {
368
+ list = list.filter(f => MEDIA_EXTS.has(getFileExt(f.Key)));
369
+ } else if (currentQuickFilter === 'docs') {
370
+ list = list.filter(f => DOC_EXTS.has(getFileExt(f.Key)));
371
+ } else if (currentQuickFilter === 'code') {
372
+ list = list.filter(f => CODE_EXTS.has(getFileExt(f.Key)));
373
+ } else if (currentQuickFilter === 'starred') {
374
+ list = list.filter(f => starredFiles.has(f.Key));
375
+ }
439
376
 
440
- btnFlat.classList.remove('active');
441
- btnFlat.classList.remove('btn-light');
442
- btnFlat.classList.add('btn-outline-light');
443
- } else {
444
- btnFlat.classList.add('active');
445
- btnFlat.classList.remove('btn-outline-light');
446
- btnFlat.classList.add('btn-light');
377
+ return list;
378
+ }
447
379
 
448
- btnFolder.classList.remove('active');
449
- btnFolder.classList.remove('btn-light');
450
- btnFolder.classList.add('btn-outline-light');
380
+ function getFilteredFolders() {
381
+ if (currentQuickFilter !== 'all') return [];
382
+ if (currentSearch) {
383
+ const q = currentSearch.toLowerCase();
384
+ return (pageFolders || []).filter(f => getBaseName(f).toLowerCase().includes(q));
451
385
  }
386
+ return pageFolders || [];
387
+ }
452
388
 
453
- // Refetch to respect recursive/non-recursive mode and keep URL state.
454
- loadFiles(1, currentPrefix, currentSearch, currentSort, currentOrder);
455
- }
456
-
457
- function expandVirtualRows() {
458
- if (!lastRenderContext) return;
459
- virtualRenderLimit += VIRTUAL_RENDER_BATCH;
460
- renderFilesTable(lastRenderContext.files, lastRenderContext.folders, lastRenderContext.prefix, lastRenderContext.apiPrefix);
461
- }
462
-
463
- // Extract folders that match a search query from file paths
464
- function extractMatchingFolders(files, search) {
465
- if (!search) return [];
466
-
467
- const searchLower = search.toLowerCase();
468
- const folderSet = new Set();
469
-
470
- files.forEach(file => {
471
- const parts = file.Key.split('/');
472
- // Remove filename
473
- parts.pop();
474
-
475
- let currentPath = '';
476
- parts.forEach(part => {
477
- const segmentPath = currentPath ? `${currentPath}/${part}` : part;
478
- currentPath = segmentPath;
479
-
480
- // If any part of the path matches the search, include that full path as a folder
481
- // AND ensure we don't include the full file path, just the directory
482
- if (part.toLowerCase().includes(searchLower)) {
483
- folderSet.add(currentPath);
484
- }
485
- });
486
- });
487
-
488
- return Array.from(folderSet).sort();
489
- }
490
-
491
- // Render files table
492
- function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
493
- lastRenderContext = { files, folders, prefix, apiPrefix };
494
- const normalizedPrefix = String(prefix || '').replace(/\/+$/, '');
495
- const basePrefix = apiPrefix || prefix;
496
- const relativeBaseOffset = basePrefix ? basePrefix.length + (basePrefix.endsWith('/') ? 0 : 1) : 0;
497
-
498
- // Determine which files/folders to show based on view mode
499
- let displayFiles = [];
500
- let displayFolders = [];
501
-
502
- // Force flat view if searching
503
- const isSearchActive = !!currentSearch;
504
- const mode = isSearchActive ? 'flat' : currentViewMode;
505
-
506
- if (mode === 'flat') {
507
- // In flat mode, show all files in the current loaded page/list regardless of folder depth
508
- if (isSearchActive) {
509
- // Search Mode:
510
- // 1. Show files where FILENAME matches the search term
511
- const searchLower = currentSearch.toLowerCase();
512
- displayFiles = files.filter(f => {
513
- // Skip folder marker objects (keys ending with /)
514
- if (f.Key.endsWith('/')) return false;
515
-
516
- const fileName = getBaseName(f.Key);
517
- return fileName.toLowerCase().includes(searchLower);
518
- });
519
-
520
- // 2. Show folders where FOLDER NAME matches the search term
521
- displayFolders = extractMatchingFolders(files, currentSearch);
522
- } else {
523
- // Normal Flat Mode: Show all files, no folders
524
- // Filter out folder marker objects (keys ending with /)
525
- displayFiles = files.filter(f => !f.Key.endsWith('/'));
526
- displayFolders = [];
527
- }
528
- } else {
529
- // Normal Folder View logic
530
- // Filter files to show only those at current level (not in subfolders)
531
- displayFiles = files.filter(file => {
532
- // Skip folder marker objects (keys ending with /)
533
- if (file.Key.endsWith('/')) return false;
534
-
535
- const relativePath = basePrefix ? file.Key.substring(relativeBaseOffset) : file.Key;
536
- // File is at current level if it has no '/' in relative path
537
- return !relativePath.includes('/');
538
- });
539
- displayFolders = folders;
540
- }
389
+ // Main Files & Grid Renderer
390
+ function renderFiles() {
391
+ const gridEl = document.getElementById('fm-grid');
392
+ const emptyEl = document.getElementById('fm-empty');
393
+ if (!gridEl) return;
541
394
 
542
- // Apply quick file-type filter after mode/path filtering.
543
- displayFiles = displayFiles.filter((f) => {
544
- const name = getBaseName(f?.Key);
545
- return matchesQuickFilter(name, currentQuickFilter);
546
- });
395
+ const files = getFilteredFiles();
396
+ const folders = getFilteredFolders();
547
397
 
548
- // Generate breadcrumb navigation
549
- let breadcrumb = '';
550
- // Show breadcrumb only if we are in folder mode OR if we have a prefix filter active even in flat mode
551
- // (In flat mode, the prefix acts as a base filter, but we see recursive files under it)
552
- if (prefix) {
553
- const parts = prefix.split('/').filter(p => p);
554
- const safeRootLabel = escapeHtml(rootLabel);
555
- const safeRootTitle = escapeAttr(rootLabel);
556
- const breadcrumbParts = [`
557
- <div class="breadcrumb-nav">
558
- <div class="breadcrumb-item">
559
- <i class="fas fa-home"></i>
560
- <button type="button" class="breadcrumb-link" ${dataAttr('nav-prefix', '')} title="Go to ${safeRootTitle}">${safeRootLabel}</button>
561
- </div>
562
- `];
563
-
564
- let currentPath = '';
565
- parts.forEach((part, index) => {
566
- currentPath += (currentPath ? '/' : '') + part;
567
- const isLast = index === parts.length - 1;
568
-
569
- if (isLast) {
570
- const safePart = escapeHtml(part);
571
- breadcrumbParts.push(`
572
- <span class="breadcrumb-separator"><i class="fas fa-chevron-right"></i></span>
573
- <div class="breadcrumb-item">
574
- <span class="breadcrumb-current">${safePart}</span>
575
- </div>
576
- `);
577
- } else {
578
- const pathCopy = currentPath;
579
- const safePart = escapeHtml(part);
580
- const safePathTitle = escapeAttr(pathCopy);
581
- breadcrumbParts.push(`
582
- <span class="breadcrumb-separator"><i class="fas fa-chevron-right"></i></span>
583
- <div class="breadcrumb-item">
584
- <button type="button" class="breadcrumb-link" ${dataAttr('nav-prefix', pathCopy)} title="Go to ${safePathTitle}">${safePart}</button>
585
- </div>
586
- `);
587
- }
588
- });
398
+ // Update stats
399
+ const totalCountEl = document.getElementById('totalFilesCount');
400
+ const filterInfoEl = document.getElementById('filterInfo');
401
+ const totalItems = files.length + folders.length;
589
402
 
590
- breadcrumbParts.push(`</div>`);
591
- breadcrumb = breadcrumbParts.join('');
403
+ if (totalCountEl) {
404
+ totalCountEl.textContent = `${totalItems} item${totalItems === 1 ? '' : 's'}`;
405
+ }
406
+ if (filterInfoEl) {
407
+ filterInfoEl.textContent = activeBucket ? `Bucket: ${activeBucket}` : 'Default bucket';
592
408
  }
593
409
 
594
- // Generate folder rows (only used in folder mode)
595
- const folderRows = displayFolders.map(folderPath => {
596
- // Handle folder paths which typically end in a slash (e.g., "folder/subfolder/")
597
- const rawFolderPath = String(folderPath || '');
598
- const cleanPath = rawFolderPath.replace(/\/+$/, '');
599
- const folderPathForAction = normalizedPrefix && cleanPath && !cleanPath.startsWith(`${normalizedPrefix}/`)
600
- ? `${normalizedPrefix}/${cleanPath}`
601
- : cleanPath;
602
- const folderKeyForDelete = `${folderPathForAction}/`;
603
- const folderName = getBaseName(cleanPath);
604
- const safeFolderName = escapeHtml(folderName);
605
- const safeFolderPathAttr = escapeAttr(`${folderPathForAction}/`);
606
- const safeFolderDeleteKeyAttr = escapeAttr(folderKeyForDelete);
607
-
608
- return `
609
- <tr class="folder-row" data-folder-path="${safeFolderPathAttr}" title="Open folder: ${safeFolderName}">
610
- <td class="table-cell-center">
611
- <!-- no selection for folders -->
612
- </td>
613
- <td class="file-name-cell">
614
- <div class="file-row-main">
615
- <div class="file-icon">
616
- <i class="fas fa-folder"></i>
617
- </div>
618
- <div class="file-info">
619
- <div class="file-name folder-name">
620
- <i class="fas fa-folder folder-leading-icon"></i>${safeFolderName}/
621
- </div>
622
- </div>
623
- <button type="button" class="btn btn-danger btn-sm delete-btn dtm" data-key="${safeFolderDeleteKeyAttr}" data-is-folder="true" title="Delete folder">
624
- <i class="fas fa-trash"></i>
625
- </button>
626
- </div>
627
- </td>
628
- <td>
629
- ${mutedDash()}
630
- </td>
631
- <td class="table-cell-center">
632
- ${mutedDash()}
633
- </td>
634
- <td class="table-cell-center">
635
- <div class="btn-group" role="group">
636
- <button type="button" class="btn btn-danger btn-sm delete-btn" data-key="${safeFolderDeleteKeyAttr}" data-is-folder="true" title="Delete folder">
637
- <i class="fas fa-trash"></i>
638
- </button>
639
- </div>
640
- </td>
641
- </tr>
642
- `;
643
- }).join('');
410
+ if (totalItems === 0) {
411
+ gridEl.innerHTML = '';
412
+ if (emptyEl) {
413
+ emptyEl.hidden = false;
414
+ emptyEl.style.display = 'flex';
415
+ }
416
+ return;
417
+ }
644
418
 
645
- const isVirtualized = displayFiles.length > VIRTUAL_RENDER_THRESHOLD;
646
- if (!isVirtualized) {
647
- virtualRenderLimit = 0;
648
- } else if (!virtualRenderLimit) {
649
- virtualRenderLimit = VIRTUAL_RENDER_BATCH;
419
+ if (emptyEl) {
420
+ emptyEl.hidden = true;
421
+ emptyEl.style.display = 'none';
422
+ }
423
+ gridEl.className = `fm-grid view-${currentViewMode}`;
424
+
425
+ let html = '';
426
+
427
+ // If List view, add header row
428
+ if (currentViewMode === 'list') {
429
+ html += `
430
+ <div class="fm-list-header">
431
+ <div></div>
432
+ <div></div>
433
+ <div>Name</div>
434
+ <div>Size</div>
435
+ <div>Modified</div>
436
+ <div></div>
437
+ <div></div>
438
+ </div>
439
+ `;
650
440
  }
651
- const visibleFiles = isVirtualized ? displayFiles.slice(0, virtualRenderLimit) : displayFiles;
652
-
653
- // Generate file rows
654
- const fileRows = visibleFiles.map(file => {
655
- const rawKey = String(file.Key || '');
656
- const encodedKey = encodeURIComponent(rawKey);
657
- const safeKeyAttr = escapeAttr(rawKey);
658
-
659
- // Calculate display name and icon directly
660
- const fullPath = rawKey;
661
- const lastSlashIndex = fullPath.lastIndexOf('/');
662
- let filename = fullPath;
663
- let folderPath = '';
664
-
665
- if (lastSlashIndex !== -1) {
666
- filename = fullPath.substring(lastSlashIndex + 1);
667
- folderPath = fullPath.substring(0, lastSlashIndex + 1); // include trailing slash
668
- }
669
441
 
670
- const iconClass = window.getFileIcon ? window.getFileIcon(filename) : 'fas fa-file';
671
- const typeCategory = window.getFileTypeCategory ? window.getFileTypeCategory(filename) : '';
672
- const truncatedName = (window.truncateFileName ? window.truncateFileName(filename) : filename) || filename;
673
- const isFileViewable = window.isViewable ? window.isViewable(filename) : false;
674
- const safeFilename = escapeHtml(filename);
675
- const safeTruncatedName = escapeHtml(truncatedName);
676
- const safeFolderPath = escapeHtml(folderPath);
677
- const viewButtonClass = isFileViewable ? 'btn btn-primary btn-sm view-btn' : 'btn btn-primary btn-sm view-btn is-disabled';
678
- const viewButtonDisabled = isFileViewable ? '' : 'disabled';
679
-
680
- // If in flat mode, we might want to show the folder path more prominently
681
- const pathDisplay = (currentViewMode === 'flat' && folderPath && folderPath !== prefix + (prefix ? '/' : ''))
682
- ? `<div class="file-path-display">
683
- <i class="fas fa-folder path-display-icon"></i> ${safeFolderPath}
684
- </div>`
685
- : '';
686
-
687
- return `
688
- <tr class="file-row">
689
- <td class="table-cell-center">
690
- <input type="checkbox" class="selectFileCheckbox" data-key="${safeKeyAttr}" aria-label="Select ${safeKeyAttr}" />
691
- </td>
692
- <td class="file-name-cell">
693
- <div class="file-row-main">
694
- <div class="file-icon ${typeCategory}">
695
- <i class="${iconClass}"></i>
696
- </div>
697
- <div class="file-info">
698
- ${pathDisplay}
699
- <div class="file-name" data-full-path="${safeKeyAttr}" title="${safeFilename}">${safeTruncatedName}</div>
700
- </div>
442
+ // Render Folders
443
+ folders.forEach(folder => {
444
+ const cleanFolder = String(folder || '').replace(/\/+$/, '');
445
+ const folderName = getBaseName(cleanFolder);
446
+ const folderPathForAction = currentPrefix && !cleanFolder.startsWith(currentPrefix + '/') && cleanFolder !== currentPrefix
447
+ ? `${currentPrefix}/${cleanFolder}`
448
+ : cleanFolder;
449
+ const safePath = escapeAttr(folderPathForAction);
450
+ const safeName = escapeHtml(folderName);
451
+
452
+ if (currentViewMode === 'grid') {
453
+ html += `
454
+ <div class="fm-item" data-type="folder" data-folder-path="${safePath}" tabindex="0">
455
+ <div class="fm-item-icon">
456
+ ${ICONS.folder}
701
457
  </div>
702
- </td>
703
- <td>
704
- ${file.Size ? `<span class="file-size">${formatFileSize(file.Size)}</span>` : '<span class="muted-cell-value">N/A</span>'}
705
- </td>
706
- <td>
707
- ${file.LastModified ? `<span class="muted-cell-value">${formatDate(file.LastModified)}</span>` : '<span class="muted-cell-value">N/A</span>'}
708
- </td>
709
- <td class="table-cell-center">
710
- <div class="btn-group" role="group">
711
- <button type="button" class="${viewButtonClass}" data-key="${safeKeyAttr}" data-filename="${safeKeyAttr}"
712
- data-key-enc="${encodedKey}" title="${isFileViewable ? 'View file in browser' : 'File type not supported for viewing'}" ${viewButtonDisabled}>
713
- <i class="fas fa-eye"></i>
714
- </button>
715
- <button type="button" class="btn btn-info btn-sm copy-btn" data-key-enc="${encodedKey}" title="Copy Link">
716
- <i class="fas fa-link"></i>
717
- </button>
718
- <a href="#" onclick="event.preventDefault(); downloadFileViaFetch('${getDownloadUrl(encodedKey)}', '${escapeAttr(filename)}')" class="btn btn-success btn-sm"
719
- title="Download file">
720
- <i class="fas fa-download"></i>
721
- </a>
722
- <button type="button" class="btn btn-danger btn-sm delete-btn" data-key="${safeKeyAttr}" title="Delete file">
723
- <i class="fas fa-trash"></i>
724
- </button>
458
+ <div class="fm-item-details">
459
+ <div class="fm-item-name" title="${safeName}">${safeName}</div>
460
+ <div class="fm-item-meta">Folder</div>
725
461
  </div>
726
- </td>
727
- </tr>
728
- `;
729
- }).join('');
462
+ <button type="button" class="fm-item-menu" data-menu-folder="${safePath}" aria-label="Folder options">
463
+ ${ICONS.more}
464
+ </button>
465
+ </div>
466
+ `;
467
+ } else {
468
+ html += `
469
+ <div class="fm-item" data-type="folder" data-folder-path="${safePath}" tabindex="0">
470
+ <div class="fm-item-checkbox"></div>
471
+ <div class="fm-item-icon">
472
+ ${ICONS.folderSm}
473
+ </div>
474
+ <div class="fm-item-name" title="${safeName}">${safeName}</div>
475
+ <div class="fm-item-size fm-item-meta">—</div>
476
+ <div class="fm-item-date fm-item-meta">—</div>
477
+ <div class="fm-star-wrap"></div>
478
+ <button type="button" class="fm-item-menu" data-menu-folder="${safePath}" aria-label="Folder options">
479
+ ${ICONS.more}
480
+ </button>
481
+ </div>
482
+ `;
483
+ }
484
+ });
730
485
 
731
- const allRows = folderRows + fileRows;
486
+ // Render Files
487
+ files.forEach(file => {
488
+ const rawKey = String(file.Key || '');
489
+ const fileName = getBaseName(rawKey);
490
+ const safeKey = escapeAttr(rawKey);
491
+ const safeName = escapeHtml(fileName);
492
+ const isStarred = starredFiles.has(rawKey);
493
+ const isImg = isImage(fileName);
494
+ const typeCfg = getFileTypeConfig(fileName);
495
+ const formattedSize = file.Size ? formatFileSize(file.Size) : '—';
496
+ const formattedDate = file.LastModified ? formatDate(file.LastModified) : '—';
497
+
498
+ let iconInner = `<span class="ext">${typeCfg.label}</span>`;
499
+ if (isImg) {
500
+ iconInner = `<img src="${getViewUrl(rawKey)}" class="fm-item-thumb" alt="${safeName}" loading="lazy" onerror="this.outerHTML='<span class=\\'ext\\'>IMG</span>'">`;
501
+ }
732
502
 
733
- if (allRows === '') {
734
- document.getElementById('fileListContainer').innerHTML = `
735
- ${breadcrumb}
736
- <div class="empty-state">
737
- <div class="empty-state-illustration">
738
- <i class="fas fa-folder-open empty-state-folder-icon"></i>
739
- <i class="fas fa-search empty-state-search-icon"></i>
503
+ if (currentViewMode === 'grid') {
504
+ html += `
505
+ <div class="fm-item ${isStarred ? 'starred' : ''}" data-type="file" data-key="${safeKey}" tabindex="0">
506
+ <div class="fm-item-icon" style="${isImg ? '' : `background:${typeCfg.bg};color:${typeCfg.color}`}">
507
+ ${iconInner}
508
+ </div>
509
+ <div class="fm-item-details">
510
+ <div class="fm-item-name" title="${safeName}">${safeName}</div>
511
+ <div class="fm-item-meta">${formattedSize} · ${formattedDate}</div>
512
+ </div>
513
+ <button type="button" class="fm-star ${isStarred ? 'on' : ''}" data-star-key="${safeKey}" aria-label="${isStarred ? 'Unstar' : 'Star'}">
514
+ ${ICONS.starred}
515
+ </button>
516
+ <button type="button" class="fm-item-menu" data-menu-file="${safeKey}" aria-label="File options">
517
+ ${ICONS.more}
518
+ </button>
740
519
  </div>
741
- <h4>No files found</h4>
742
- <p class="empty-state-description">
743
- ${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.`}
744
- </p>
745
- <div class="empty-state-actions">
746
- <button class="btn btn-primary" data-action="upload-files">
747
- <i class="fas fa-cloud-upload-alt"></i> Upload Files
520
+ `;
521
+ } else {
522
+ html += `
523
+ <div class="fm-item ${isStarred ? 'starred' : ''}" data-type="file" data-key="${safeKey}" tabindex="0">
524
+ <div class="fm-item-checkbox">
525
+ <input type="checkbox" class="file-select-cb" data-key="${safeKey}">
526
+ </div>
527
+ <div class="fm-item-icon" style="${isImg ? '' : `background:${typeCfg.bg};color:${typeCfg.color}`}">
528
+ <span class="ext">${typeCfg.label}</span>
529
+ </div>
530
+ <div class="fm-item-name" title="${safeName}">${safeName}</div>
531
+ <div class="fm-item-size fm-item-meta">${formattedSize}</div>
532
+ <div class="fm-item-date fm-item-meta">${formattedDate}</div>
533
+ <button type="button" class="fm-star ${isStarred ? 'on' : ''}" data-star-key="${safeKey}" aria-label="${isStarred ? 'Unstar' : 'Star'}">
534
+ ${ICONS.starred}
748
535
  </button>
749
- <button class="btn btn-outline-secondary" data-action="create-folder">
750
- <i class="fas fa-folder-plus"></i> New Folder
536
+ <button type="button" class="fm-item-menu" data-menu-file="${safeKey}" aria-label="File options">
537
+ ${ICONS.more}
751
538
  </button>
752
- ${prefix ? `<button class="btn btn-light" data-action="go-root">
753
- <i class="fas fa-home"></i> Go to ${rootLabel}
754
- </button>` : ''}
755
539
  </div>
540
+ `;
541
+ }
542
+ });
543
+
544
+ gridEl.innerHTML = html;
545
+ }
546
+
547
+ // Load Files API Call
548
+ async function loadFiles(page = 1, prefix = '', search = '') {
549
+ currentPage = page;
550
+ currentPrefix = String(prefix || '').replace(/^\/+/, '').replace(/\/+$/, '');
551
+ currentSearch = search;
552
+
553
+ syncStateUrl();
554
+
555
+ const gridEl = document.getElementById('fm-grid');
556
+ if (gridEl) {
557
+ gridEl.innerHTML = `
558
+ <div class="fm-skeleton-grid">
559
+ ${[...Array(6)].map(() => `
560
+ <div class="fm-skeleton-card">
561
+ <div class="fm-skeleton-icon"></div>
562
+ <div class="fm-skeleton-line w-75"></div>
563
+ <div class="fm-skeleton-line w-50"></div>
564
+ </div>
565
+ `).join('')}
756
566
  </div>
757
567
  `;
758
- return;
759
568
  }
760
569
 
761
- // Sort icon helper
762
- const getSortIcon = (field) => {
763
- if (currentSort !== field) return '<i class="fas fa-sort sort-icon sort-icon-inactive"></i>';
764
- return currentOrder === 'asc'
765
- ? '<i class="fas fa-sort-up sort-icon sort-icon-active"></i>'
766
- : '<i class="fas fa-sort-down sort-icon sort-icon-active"></i>';
767
- };
768
-
769
- const virtualizedInfo = isVirtualized
770
- ? `<div class="virtualized-info">
771
- <span>Showing ${visibleFiles.length} of ${displayFiles.length} files in this view.</span>
772
- <div class="virtualized-actions">
773
- ${visibleFiles.length < displayFiles.length ? '<button class="btn btn-sm btn-outline-secondary" data-action="expand-virtual">Load more</button>' : ''}
774
- </div>
775
- </div>`
776
- : '';
777
-
778
- document.getElementById('fileListContainer').innerHTML = `
779
- ${breadcrumb}
780
- <div class="table-responsive">
781
- <div id="bulkActionsBar" class="bulk-actions-bar">
782
- <span id="bulkSelectedCount">0 selected</span>
783
- <div class="bulk-actions-controls">
784
- <button class="btn btn-danger btn-sm" id="bulkDeleteBtn" disabled>
785
- <i class="fas fa-trash"></i> Delete Selected
786
- </button>
787
- </div>
788
- </div>
789
- <table class="table table-hover mb-0">
790
- <thead>
791
- <tr>
792
- <th class="table-select-col">
793
- <input type="checkbox" id="selectAllCheckbox" aria-label="Select all files" />
794
- </th>
795
- <th class="sortable-heading file-name-heading" data-sort-field="name">
796
- <i class="fas fa-file-alt"></i> Name ${getSortIcon('name')}
797
- </th>
798
- <th class="sortable-heading" data-sort-field="size">
799
- <i class="fas fa-weight"></i> Size ${getSortIcon('size')}
800
- </th>
801
- <th class="sortable-heading" data-sort-field="date">
802
- <i class="fas fa-calendar"></i> Last Modified ${getSortIcon('date')}
803
- </th>
804
- <th class="table-cell-center">
805
- <i class="fas fa-cogs"></i> Actions
806
- </th>
807
- </tr>
808
- </thead>
809
- <tbody>
810
- ${allRows}
811
- </tbody>
812
- </table>
813
- ${virtualizedInfo}
814
- </div>
815
- `;
570
+ try {
571
+ const searchParam = search ? `&search=${encodeURIComponent(search)}` : '';
572
+ const sortParam = `&sort=${currentSort}&order=${currentOrder}`;
573
+ const recursiveParam = `&recursive=false`;
574
+ const url = withBucketUrl(`/mbkbucket/api/files?page=${page}&prefix=${encodeURIComponent(currentPrefix)}${searchParam}${sortParam}${recursiveParam}`);
816
575
 
817
- // Wire selection checkboxes and bulk actions
818
- setupSelection();
819
- }
820
-
821
- // Render pagination
822
- function renderPagination(data) {
823
- const { currentPage, totalPages, hasPrevPage, hasNextPage, mode } = data;
824
-
825
- let paginationHTML = '';
826
- const pageLink = (page, label, extra = {}) => {
827
- const attrs = [
828
- dataAttr('page', page),
829
- dataAttr('prefix', currentPrefix),
830
- dataAttr('search', currentSearch),
831
- dataAttr('sort', extra.sort || currentSort),
832
- dataAttr('order', extra.order || currentOrder)
833
- ];
834
- if (extra.token) attrs.push(dataAttr('token', extra.token));
835
- return `<a class="page-link" href="#" ${attrs.join(' ')}>${label}</a>`;
836
- };
837
-
838
- if (mode === 'optimized') {
839
- // Optimized Mode: Token-based pagination (Next only, limited Prev)
840
-
841
- // Previous button (Disabled for now as we don't track history stack of tokens)
842
- paginationHTML += `
843
- <li class="page-item disabled">
844
- <span class="page-link" title="Previous page not supported in optimized view">
845
- <i class="fas fa-chevron-left"></i> Previous
846
- </span>
847
- </li>
848
- `;
849
-
850
- // Current Page Indicator
851
- paginationHTML += `
852
- <li class="page-item active">
853
- <span class="page-link">${currentPage}</span>
854
- </li>
855
- `;
576
+ const response = await fetch(url);
577
+ const data = await response.json();
856
578
 
857
- // Next button
858
- if (hasNextPage) {
859
- paginationHTML += `
860
- <li class="page-item">
861
- ${pageLink(currentPage + 1, 'Next <i class="fas fa-chevron-right"></i>', { token: currentNextToken || '' })}
862
- </li>
863
- `;
864
- } else {
865
- paginationHTML += `
866
- <li class="page-item disabled">
867
- <span class="page-link">
868
- Next <i class="fas fa-chevron-right"></i>
869
- </span>
870
- </li>
871
- `;
579
+ if (!data.success) {
580
+ throw new Error(data.error || 'Failed to load files');
872
581
  }
873
582
 
874
- } else {
875
- // Legacy Mode: Numbered pagination
876
-
877
- // Previous button
878
- if (hasPrevPage) {
879
- paginationHTML += `
880
- <li class="page-item">
881
- ${pageLink(currentPage - 1, '<i class="fas fa-chevron-left"></i> Previous')}
882
- </li>
883
- `;
884
- } else {
885
- paginationHTML += `
886
- <li class="page-item disabled">
887
- <span class="page-link">
888
- <i class="fas fa-chevron-left"></i> Previous
889
- </span>
890
- </li>
891
- `;
892
- }
583
+ currentNextToken = data.nextContinuationToken || null;
584
+ currentApiPrefix = data.prefix || '';
585
+ pageFiles = data.files || [];
893
586
 
894
- // Smart Page Numbers (limit visible pages)
895
- const maxPagesToShow = 7;
896
- let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2));
897
- let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1);
898
-
899
- if (endPage - startPage + 1 < maxPagesToShow) {
900
- startPage = Math.max(1, endPage - maxPagesToShow + 1);
901
- }
587
+ if (data.mode === 'optimized') {
588
+ pageFolders = (data.folders || []).map(folder => stripApiPrefixFromPath(folder, currentApiPrefix)).filter(Boolean);
589
+ } else {
590
+ pageFolders = currentSearch
591
+ ? []
592
+ : extractFoldersAtCurrentLevel(pageFiles, currentApiPrefix || currentPrefix)
593
+ .map(folder => stripApiPrefixFromPath(folder, currentApiPrefix))
594
+ .filter(Boolean);
595
+ }
902
596
 
903
- // First page
904
- if (startPage > 1) {
905
- paginationHTML += `
906
- <li class="page-item">
907
- ${pageLink(1, '1')}
908
- </li>`;
909
- if (startPage > 2) {
910
- paginationHTML += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
911
- }
912
- }
597
+ renderTree();
598
+ renderBreadcrumbs();
599
+ renderFiles();
600
+ renderPagination(data);
601
+ } catch (err) {
602
+ console.error('Error loading files:', err);
603
+ showToast(err.message || 'Error loading files', 'error');
604
+ if (gridEl) {
605
+ gridEl.innerHTML = `
606
+ <div class="fm-empty">
607
+ <div class="empty-title">Error Loading Files</div>
608
+ <div class="empty-desc">${escapeHtml(err.message)}</div>
609
+ <button type="button" class="btn btn-primary btn-sm mt-3" onclick="window.mbkbucket.loadFiles(${page}, '${escapeAttr(currentPrefix)}')">Retry</button>
610
+ </div>
611
+ `;
612
+ }
613
+ }
614
+ }
913
615
 
914
- for (let i = startPage; i <= endPage; i++) {
915
- if (i === currentPage) {
916
- paginationHTML += `
917
- <li class="page-item active">
918
- <span class="page-link">${i}</span>
919
- </li>
920
- `;
921
- } else {
922
- paginationHTML += `
923
- <li class="page-item">
924
- ${pageLink(i, String(i))}
925
- </li>
926
- `;
927
- }
928
- }
929
-
930
- // Last page
931
- if (endPage < totalPages) {
932
- if (endPage < totalPages - 1) {
933
- paginationHTML += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
934
- }
935
- paginationHTML += `
936
- <li class="page-item">
937
- ${pageLink(totalPages, String(totalPages))}
938
- </li>`;
939
- }
616
+ // Pagination Renderer
617
+ function renderPagination(data) {
618
+ const paginationContainer = document.getElementById('paginationContainer');
619
+ const paginationList = document.getElementById('paginationList');
620
+ if (!paginationContainer || !paginationList) return;
940
621
 
941
- // Next button
942
- if (hasNextPage) {
943
- paginationHTML += `
944
- <li class="page-item">
945
- ${pageLink(currentPage + 1, 'Next <i class="fas fa-chevron-right"></i>')}
946
- </li>
947
- `;
948
- } else {
949
- paginationHTML += `
950
- <li class="page-item disabled">
951
- <span class="page-link">
952
- Next <i class="fas fa-chevron-right"></i>
953
- </span>
954
- </li>
955
- `;
956
- }
622
+ if (!data.totalPages || data.totalPages <= 1) {
623
+ paginationContainer.style.display = 'none';
624
+ return;
957
625
  }
958
626
 
959
- document.getElementById('paginationList').innerHTML = paginationHTML;
960
- }
961
-
962
- function confirmDelete(key, isFolder = false) {
963
- document.getElementById('deleteKey').value = key;
964
- document.getElementById('deleteIsFolder').value = isFolder ? 'true' : '';
965
- document.getElementById('deleteFileName').textContent = key;
627
+ paginationContainer.style.display = 'flex';
628
+ let html = '';
966
629
 
967
- // Update modal message and button label for folders
968
- const modalHeading = document.querySelector('#deleteModal .modal-body h6');
969
- const smallText = document.querySelector('#deleteModal .modal-body small');
970
- const confirmLabel = document.getElementById('deleteConfirmLabel');
630
+ // Prev
631
+ html += `
632
+ <li class="page-item ${data.currentPage <= 1 ? 'disabled' : ''}">
633
+ <button type="button" class="page-link" data-page="${data.currentPage - 1}">‹</button>
634
+ </li>
635
+ `;
971
636
 
972
- if (isFolder) {
973
- if (modalHeading) modalHeading.textContent = 'Are you sure you want to delete this folder and all its contents?';
974
- if (smallText) smallText.textContent = 'This action will remove the folder and all files within it and cannot be undone.';
975
- if (confirmLabel) confirmLabel.textContent = 'Delete Folder';
976
- } else {
977
- if (modalHeading) modalHeading.textContent = 'Are you sure you want to delete this file?';
978
- if (smallText) smallText.textContent = 'This action cannot be undone.';
979
- if (confirmLabel) confirmLabel.textContent = 'Delete File';
637
+ // Pages
638
+ for (let i = 1; i <= data.totalPages; i++) {
639
+ if (i === 1 || i === data.totalPages || (i >= data.currentPage - 2 && i <= data.currentPage + 2)) {
640
+ html += `
641
+ <li class="page-item ${i === data.currentPage ? 'active' : ''}">
642
+ <button type="button" class="page-link" data-page="${i}">${i}</button>
643
+ </li>
644
+ `;
645
+ } else if (i === data.currentPage - 3 || i === data.currentPage + 3) {
646
+ html += `<li class="page-item disabled"><span class="page-link">…</span></li>`;
647
+ }
980
648
  }
981
649
 
982
- document.getElementById('deleteModal').classList.add('show');
983
- document.getElementById('deleteModal').style.display = 'flex';
984
- }
650
+ // Next
651
+ html += `
652
+ <li class="page-item ${data.currentPage >= data.totalPages ? 'disabled' : ''}">
653
+ <button type="button" class="page-link" data-page="${data.currentPage + 1}">›</button>
654
+ </li>
655
+ `;
656
+
657
+ paginationList.innerHTML = html;
658
+ }
985
659
 
986
- function closeModal() {
987
- document.getElementById('deleteModal').classList.remove('show');
988
- document.getElementById('deleteModal').style.display = 'none';
989
- }
660
+ // Context Menu
661
+ function openContextMenu(x, y, items) {
662
+ const menu = document.getElementById('fm-context-menu');
663
+ if (!menu) return;
990
664
 
991
- // Handle delete form submission
992
- document.getElementById('deleteForm')?.addEventListener('submit', async function (e) {
993
- e.preventDefault();
665
+ menu.innerHTML = items.map(item => {
666
+ if (item === '-') return '<div class="fm-context-divider"></div>';
667
+ return `<button type="button" class="fm-context-item ${item.danger ? 'danger' : ''}" data-action="${item.id}">${item.icon || ''} ${escapeHtml(item.label)}</button>`;
668
+ }).join('');
994
669
 
995
- const key = document.getElementById('deleteKey').value;
996
- const isFolder = document.getElementById('deleteIsFolder').value === 'true';
670
+ menu.style.display = 'block';
997
671
 
998
- try {
999
- const response = await fetch(withBucketUrl('/mbkbucket/delete'), {
1000
- method: 'POST',
1001
- headers: {
1002
- 'Content-Type': 'application/json'
1003
- },
1004
- body: JSON.stringify({ key, folder: isFolder })
1005
- });
672
+ const menuWidth = 180;
673
+ const menuHeight = menu.offsetHeight || 150;
674
+ const posX = Math.min(x, window.innerWidth - menuWidth - 10);
675
+ const posY = Math.min(y, window.innerHeight - menuHeight - 10);
1006
676
 
1007
- const data = await response.json();
677
+ menu.style.left = `${posX}px`;
678
+ menu.style.top = `${posY}px`;
1008
679
 
1009
- if (data.success) {
1010
- showAlert('File deleted successfully', 'success');
1011
- closeModal();
1012
- // Reload file list
1013
- loadFiles(currentPage, currentPrefix);
1014
- } else {
1015
- showAlert(data.error || 'Delete failed', 'error');
1016
- closeModal();
680
+ menu.querySelectorAll('.fm-context-item').forEach(btn => {
681
+ btn.addEventListener('click', (e) => {
682
+ e.stopPropagation();
683
+ const actionId = btn.dataset.action;
684
+ const targetItem = items.find(it => it.id === actionId);
685
+ menu.style.display = 'none';
686
+ if (targetItem && typeof targetItem.action === 'function') {
687
+ targetItem.action();
1017
688
  }
1018
- } catch (error) {
1019
- console.error('Error deleting file:', error);
1020
- showAlert('Error deleting file: ' + error.message, 'error');
1021
- closeModal();
689
+ });
690
+ });
691
+ }
692
+
693
+ function closeContextMenu() {
694
+ const menu = document.getElementById('fm-context-menu');
695
+ if (menu) menu.style.display = 'none';
696
+ }
697
+
698
+ // Actions: Star, Download, Preview, Delete, New Folder
699
+ function toggleStar(key) {
700
+ if (starredFiles.has(key)) {
701
+ starredFiles.delete(key);
702
+ showToast('Removed from Starred');
703
+ } else {
704
+ starredFiles.add(key);
705
+ showToast('Added to Starred', 'success');
706
+ }
707
+ localStorage.setItem('mbk_starred_keys', JSON.stringify([...starredFiles]));
708
+ renderTree();
709
+ renderFiles();
710
+ }
711
+
712
+ async function downloadFile(key) {
713
+ const fileName = getBaseName(key);
714
+ const downloadUrl = getDownloadUrl(key);
715
+ showToast(`Downloading ${fileName}...`);
716
+ try {
717
+ const response = await fetch(downloadUrl);
718
+ if (!response.ok) throw new Error('Download failed');
719
+ const blob = await response.blob();
720
+ const url = URL.createObjectURL(blob);
721
+ const a = document.createElement('a');
722
+ a.href = url;
723
+ a.download = fileName;
724
+ document.body.appendChild(a);
725
+ a.click();
726
+ document.body.removeChild(a);
727
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
728
+ } catch (err) {
729
+ showToast(err.message, 'error');
1022
730
  }
1023
- });
1024
-
1025
- // Auto-dismiss alerts with fade effect
1026
- setTimeout(() => {
1027
- const alerts = document.querySelectorAll('.alert');
1028
- alerts.forEach(alert => {
1029
- alert.style.opacity = '0';
1030
- alert.style.transform = 'translateY(-10px)';
1031
- setTimeout(() => alert.remove(), 300);
731
+ }
732
+
733
+ function copyShareLink(key) {
734
+ const viewUrl = window.location.origin + getViewUrl(key);
735
+ navigator.clipboard.writeText(viewUrl).then(() => {
736
+ showToast('Link copied to clipboard', 'success');
737
+ }).catch(() => {
738
+ showToast('Failed to copy link', 'error');
1032
739
  });
1033
- }, 5000);
740
+ }
1034
741
 
1035
- // Upload queue, drag-and-drop and multi-file support
1036
- const uploadQueue = []; // { id, file, status, progress, controller/xhr, uploadId }
1037
- const xhrMap = new Map();
1038
- const CHUNK_SIZE = 50 * 1024 * 1024; // 50MB chunk threshold
742
+ // Preview Modal
743
+ function openPreview(key) {
744
+ const modal = document.getElementById('previewModal');
745
+ const titleEl = document.getElementById('previewFileName');
746
+ const contentEl = document.getElementById('previewContent');
747
+ const openBtn = document.getElementById('previewOpenBtn');
748
+ const downloadBtn = document.getElementById('previewDownloadBtn');
1039
749
 
1040
- function createId() { return Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 9); }
750
+ if (!modal || !contentEl) return;
1041
751
 
1042
- function addFilesToQueue(fileList) {
1043
- for (const f of Array.from(fileList)) {
1044
- uploadQueue.push({ id: createId(), file: f, status: 'queued', progress: 0, uploadedBytes: 0, uploadRate: 0 });
1045
- }
1046
- renderUploadQueue();
1047
- }
752
+ const fileName = getBaseName(key);
753
+ const viewUrl = getViewUrl(key);
754
+ const downloadUrl = getDownloadUrl(key);
1048
755
 
1049
- // Render upload queue UI
1050
- function renderUploadQueue() {
1051
- const container = document.getElementById('uploadQueue');
1052
- if (!container) return;
1053
- if (uploadQueue.length === 0) {
1054
- container.innerHTML = `
1055
- <div class="form-text">No files selected.</div>
756
+ if (titleEl) titleEl.textContent = fileName;
757
+ if (openBtn) openBtn.href = viewUrl;
758
+ if (downloadBtn) downloadBtn.href = downloadUrl;
759
+
760
+ contentEl.innerHTML = '<div class="empty-preview"><div class="spinner-border text-light"></div><div class="mt-2 text-light">Loading preview...</div></div>';
761
+ modal.classList.add('show');
762
+
763
+ if (isImage(fileName)) {
764
+ contentEl.innerHTML = `<img src="${viewUrl}" alt="${escapeAttr(fileName)}">`;
765
+ } else if (isVideo(fileName)) {
766
+ contentEl.innerHTML = `<video controls autoplay class="w-100" style="max-height: 70vh;"><source src="${viewUrl}"></video>`;
767
+ } else if (isAudio(fileName)) {
768
+ contentEl.innerHTML = `<div class="p-4 text-center"><audio controls autoplay class="w-100"><source src="${viewUrl}"></audio></div>`;
769
+ } else if (isPdf(fileName)) {
770
+ contentEl.innerHTML = `<iframe src="${viewUrl}" class="preview-iframe"></iframe>`;
771
+ } else if (isTextOrCode(fileName)) {
772
+ fetch(viewUrl).then(res => res.text()).then(text => {
773
+ contentEl.innerHTML = `<pre class="preview-code-block"><code>${escapeHtml(text)}</code></pre>`;
774
+ }).catch(err => {
775
+ contentEl.innerHTML = `<div class="p-4 text-danger text-center">Failed to load text preview: ${escapeHtml(err.message)}</div>`;
776
+ });
777
+ } else {
778
+ contentEl.innerHTML = `
779
+ <div class="p-5 text-center text-light">
780
+ <h5>No inline preview available for this file type.</h5>
781
+ <p class="text-muted small">You can open it in a new tab or download it directly.</p>
782
+ </div>
1056
783
  `;
1057
- return;
1058
784
  }
785
+ }
1059
786
 
1060
- // Local size formatter so it's always in scope regardless of IIFE load order
1061
- function fmtSize(bytes) {
1062
- if (!bytes || bytes === 0) return '0 B';
1063
- const units = ['B', 'KB', 'MB', 'GB'];
1064
- const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
1065
- return (bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1) + ' ' + units[i];
1066
- }
787
+ window.closePreviewModal = function () {
788
+ const modal = document.getElementById('previewModal');
789
+ if (modal) modal.classList.remove('show');
790
+ };
1067
791
 
1068
- container.innerHTML = uploadQueue.map(item => {
1069
- const safeName = escapeHtml(item.file.name);
1070
- const iconClass = window.getFileIcon ? window.getFileIcon(item.file.name) : 'fas fa-file-alt';
1071
- const typeCategory = window.getFileTypeCategory ? window.getFileTypeCategory(item.file.name) : '';
1072
- let statusText;
1073
- if (item.status === 'uploading') {
1074
- const pct = Math.round(item.progress || 0);
1075
- statusText = fmtSize(item.uploadedBytes || 0) + ' / ' + fmtSize(item.file.size) + ' (' + pct + '%)';
1076
- if (item.uploadRate > 0) statusText += ' \u00b7 ' + fmtSize(item.uploadRate) + '/s';
1077
- } else {
1078
- statusText = item.status;
1079
- }
1080
- const actionBtn = item.status === 'uploading'
1081
- ? `<button class="btn btn-sm btn-outline-secondary cancel-upload" data-id="${item.id}"><i class="fas fa-ban"></i></button>`
1082
- : `<button class="btn btn-sm btn-danger remove-queued" data-id="${item.id}"><i class="fas fa-trash"></i></button>`;
1083
- return `
1084
- <div class="upload-item" data-id="${item.id}">
1085
- <div class="meta">
1086
- <div class="upload-item-meta-row">
1087
- <div class="file-icon upload-file-icon ${typeCategory}"><i class="${iconClass}"></i></div>
1088
- <div class="name upload-item-name" title="${safeName}">${safeName}</div>
1089
- <div class="form-text file-size upload-item-size-label">${formatFileSize(item.file.size)}</div>
1090
- </div>
1091
- </div>
1092
- <div class="upload-progress-cell">
1093
- <div class="progress-container upload-progress-track">
1094
- <div class="progress-bar upload-progress-bar" style="width: ${Math.round(item.progress)}%;"></div>
1095
- </div>
1096
- <div class="upload-item-status-row">
1097
- <small class="text-muted status">${statusText}</small>
1098
- <div class="controls">${actionBtn}</div>
1099
- </div>
1100
- </div>
1101
- </div>`;
1102
- }).join('');
792
+ // Delete Modal
793
+ function promptDelete(key, isFolder = false) {
794
+ const modal = document.getElementById('deleteModal');
795
+ const deleteFileName = document.getElementById('deleteFileName');
796
+ const deleteKey = document.getElementById('deleteKey');
797
+ const deleteIsFolder = document.getElementById('deleteIsFolder');
798
+ const deleteConfirmLabel = document.getElementById('deleteConfirmLabel');
1103
799
 
1104
- // Wire up controls
1105
- document.querySelectorAll('.remove-queued').forEach(btn => btn.addEventListener('click', function (e) {
1106
- const id = this.dataset.id;
1107
- const idx = uploadQueue.findIndex(i => i.id === id);
1108
- if (idx !== -1) uploadQueue.splice(idx, 1);
1109
- renderUploadQueue();
1110
- }));
1111
-
1112
- document.querySelectorAll('.cancel-upload').forEach(btn => btn.addEventListener('click', async function (e) {
1113
- const id = this.dataset.id;
1114
- const handle = xhrMap.get(id);
1115
- const entry = uploadQueue.find(i => i.id === id);
1116
-
1117
- // Abort in-flight fetch
1118
- if (handle && handle.controller) handle.controller.abort();
1119
- // Abort legacy XHR (small file path)
1120
- if (handle && handle.xhr) handle.xhr.abort();
1121
-
1122
- // Tell S3 to discard the incomplete multipart upload
1123
- if (entry && entry.s3UploadId && entry.s3Key) {
1124
- try {
1125
- await fetch(withBucketUrl('/mbkbucket/upload-abort'), {
1126
- method: 'POST',
1127
- headers: { 'Content-Type': 'application/json' },
1128
- body: JSON.stringify({ uploadId: entry.s3UploadId, key: entry.s3Key })
1129
- });
1130
- } catch (_) { /* best-effort */ }
1131
- }
800
+ if (!modal) return;
1132
801
 
1133
- if (entry) { entry.status = 'cancelled'; entry.progress = 0; }
1134
- xhrMap.delete(id);
1135
- renderUploadQueue();
1136
- }));
1137
- }
1138
-
1139
- // Drag & drop wiring
1140
- const dropZone = document.getElementById('dropZone');
1141
- const fileInputEl = document.getElementById('fileInput');
1142
- if (dropZone) {
1143
- dropZone.addEventListener('click', () => fileInputEl.click());
1144
- dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('dragover'); });
1145
- dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
1146
- dropZone.addEventListener('drop', (e) => {
1147
- e.preventDefault(); dropZone.classList.remove('dragover');
1148
- if (e.dataTransfer && e.dataTransfer.files) addFilesToQueue(e.dataTransfer.files);
1149
- });
1150
- }
802
+ if (deleteFileName) deleteFileName.textContent = key;
803
+ if (deleteKey) deleteKey.value = key;
804
+ if (deleteIsFolder) deleteIsFolder.value = isFolder ? 'true' : '';
805
+ if (deleteConfirmLabel) deleteConfirmLabel.textContent = isFolder ? 'Delete Folder' : 'Delete File';
1151
806
 
1152
- fileInputEl?.addEventListener('change', function (e) {
1153
- if (e.target.files && e.target.files.length) {
1154
- addFilesToQueue(e.target.files);
1155
- }
1156
- });
807
+ modal.classList.add('show');
808
+ }
809
+
810
+ window.closeModal = function () {
811
+ const deleteModal = document.getElementById('deleteModal');
812
+ if (deleteModal) deleteModal.classList.remove('show');
813
+ };
1157
814
 
1158
- // Start queued uploads (single or chunked) and provide cancelation controls
1159
- async function startUploadEntry(entry) {
1160
- if (!entry || entry.status === 'uploading' || entry.status === 'done') return;
1161
- entry.status = 'uploading';
1162
- renderUploadQueue();
815
+ // New Folder Modal
816
+ function openNewFolderModal() {
817
+ const modal = document.getElementById('newFolderModal');
818
+ const input = document.getElementById('newFolderNameInput');
819
+ if (!modal) return;
820
+ if (input) input.value = '';
821
+ modal.classList.add('show');
822
+ if (input) setTimeout(() => input.focus(), 50);
823
+ }
824
+
825
+ window.closeNewFolderModal = function () {
826
+ const modal = document.getElementById('newFolderModal');
827
+ if (modal) modal.classList.remove('show');
828
+ };
829
+
830
+ window.submitCreateFolder = async function () {
831
+ const input = document.getElementById('newFolderNameInput');
832
+ const name = input ? input.value.trim() : '';
833
+ if (!name) {
834
+ showToast('Please enter a folder name', 'warning');
835
+ return;
836
+ }
1163
837
 
1164
- const file = entry.file;
1165
838
  try {
1166
- if (file.size > CHUNK_SIZE) {
1167
- // ---------------------------------------------------------------
1168
- // S3 Multipart Upload
1169
- // Step 1 — Initiate: get uploadId + key from the server
1170
- // ---------------------------------------------------------------
1171
- const initResp = await fetch(withBucketUrl('/mbkbucket/upload-init'), {
1172
- method: 'POST',
1173
- headers: { 'Content-Type': 'application/json' },
1174
- body: JSON.stringify({
1175
- fileName: file.name,
1176
- prefix: currentPrefix,
1177
- contentType: file.type || 'application/octet-stream'
1178
- })
1179
- });
1180
- const initJson = await initResp.json();
1181
- if (!initResp.ok || !initJson.success) throw new Error(initJson.error || 'Failed to initiate upload');
1182
-
1183
- const { uploadId, key } = initJson;
1184
- entry.s3UploadId = uploadId; // stored so cancel handler can abort
1185
- entry.s3Key = key;
1186
-
1187
- // ---------------------------------------------------------------
1188
- // Step 2 — Upload parts (1-based partNumber, min 5 MB except last)
1189
- // ---------------------------------------------------------------
1190
- const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
1191
- const parts = []; // [{ partNumber, ETag }]
1192
- let uploadedBytes = 0;
1193
-
1194
- for (let i = 0; i < totalChunks; i++) {
1195
- const _chunkStart = Date.now();
1196
- if (entry.status === 'cancelled') {
1197
- // Clean up on S3
1198
- await fetch(withBucketUrl('/mbkbucket/upload-abort'), {
1199
- method: 'POST',
1200
- headers: { 'Content-Type': 'application/json' },
1201
- body: JSON.stringify({ uploadId, key })
1202
- }).catch(() => {});
1203
- throw new Error('Upload cancelled');
1204
- }
1205
-
1206
- const start = i * CHUNK_SIZE;
1207
- const blob = file.slice(start, Math.min(file.size, start + CHUNK_SIZE));
1208
- const partNumber = i + 1; // S3 parts are 1-based
1209
-
1210
- const fd = new FormData();
1211
- fd.append('chunk', blob, file.name);
1212
- fd.append('uploadId', uploadId);
1213
- fd.append('key', key);
1214
- fd.append('partNumber', String(partNumber));
1215
-
1216
- const controller = new AbortController();
1217
- xhrMap.set(entry.id, { controller, s3UploadId: uploadId, s3Key: key });
1218
-
1219
- const chunkResp = await fetch(withBucketUrl('/mbkbucket/upload-chunk'), {
1220
- method: 'POST',
1221
- body: fd,
1222
- signal: controller.signal
1223
- });
1224
- const chunkJson = await chunkResp.json();
1225
- if (!chunkResp.ok || !chunkJson.success) throw new Error(chunkJson.error || `Part ${partNumber} upload failed`);
1226
-
1227
- parts.push({ partNumber: chunkJson.partNumber, ETag: chunkJson.ETag });
1228
- uploadedBytes += blob.size;
1229
- const _chunkDuration = (Date.now() - _chunkStart) / 1000;
1230
- entry.uploadRate = _chunkDuration > 0 ? blob.size / _chunkDuration : 0;
1231
- entry.uploadedBytes = uploadedBytes;
1232
- entry.progress = (uploadedBytes / file.size) * 95; // reserve last 5% for complete call
1233
- renderUploadQueue();
1234
- }
1235
-
1236
- // ---------------------------------------------------------------
1237
- // Step 3 — Complete: assemble all parts on S3
1238
- // ---------------------------------------------------------------
1239
- const completeResp = await fetch(withBucketUrl('/mbkbucket/upload-complete'), {
1240
- method: 'POST',
1241
- headers: { 'Content-Type': 'application/json' },
1242
- body: JSON.stringify({ uploadId, key, parts })
1243
- });
1244
- const completeJson = await completeResp.json();
1245
- if (!completeResp.ok || !completeJson.success) throw new Error(completeJson.error || 'Failed to complete upload');
1246
-
1247
- entry.status = 'done'; entry.progress = 100;
1248
- xhrMap.delete(entry.id);
1249
- renderUploadQueue();
1250
- showAlert(`${file.name} uploaded successfully`, 'success');
1251
- loadFiles(currentPage, currentPrefix);
1252
- } else {
1253
- // small file XHR
1254
- const fd = new FormData();
1255
- fd.append('file', file, file.name);
1256
- fd.append('prefix', currentPrefix);
1257
-
1258
- const xhr = new XMLHttpRequest();
1259
- xhr.open('POST', withBucketUrl('/mbkbucket/upload'));
1260
-
1261
- xhr.upload.addEventListener('progress', (e) => {
1262
- if (!e.lengthComputable) return;
1263
- const _now = Date.now();
1264
- const _dt = (_now - (entry._rateLastTime || _now)) / 1000;
1265
- const _db = e.loaded - (entry._rateLastBytes || 0);
1266
- if (_dt > 0.2) {
1267
- entry.uploadRate = _db / _dt;
1268
- entry._rateLastTime = _now;
1269
- entry._rateLastBytes = e.loaded;
1270
- }
1271
- entry.uploadedBytes = e.loaded;
1272
- entry.progress = (e.loaded / e.total) * 100;
1273
- renderUploadQueue();
1274
- });
1275
- entry._rateLastTime = Date.now();
1276
- entry._rateLastBytes = 0;
1277
-
1278
- xhr.addEventListener('load', () => {
1279
- try {
1280
- const resp = JSON.parse(xhr.responseText);
1281
- if (xhr.status >= 200 && xhr.status < 300 && resp.success) {
1282
- entry.status = 'done'; entry.progress = 100; renderUploadQueue();
1283
- showAlert('File uploaded successfully', 'success');
1284
- loadFiles(currentPage, currentPrefix);
1285
- } else {
1286
- entry.status = 'error'; renderUploadQueue();
1287
- showAlert(resp.error || 'Upload failed', 'error');
1288
- }
1289
- } catch (err) {
1290
- entry.status = 'error'; renderUploadQueue();
1291
- showAlert('Upload response parse error', 'error');
1292
- }
1293
- xhrMap.delete(entry.id);
1294
- });
1295
-
1296
- xhr.addEventListener('error', () => { entry.status = 'error'; renderUploadQueue(); showAlert('Network error during upload', 'error'); xhrMap.delete(entry.id); });
1297
- xhr.addEventListener('abort', () => { entry.status = 'cancelled'; renderUploadQueue(); xhrMap.delete(entry.id); });
1298
-
1299
- xhrMap.set(entry.id, { xhr });
1300
- xhr.send(fd);
1301
- }
839
+ const response = await fetch(withBucketUrl('/mbkbucket/create-folder'), {
840
+ method: 'POST',
841
+ headers: { 'Content-Type': 'application/json' },
842
+ body: JSON.stringify({ folderName: name, prefix: currentPrefix })
843
+ });
844
+ const data = await response.json();
845
+ if (!data.success) throw new Error(data.error || 'Failed to create folder');
846
+
847
+ closeNewFolderModal();
848
+ showToast(`Folder "${name}" created`, 'success');
849
+ loadFiles(1, currentPrefix);
1302
850
  } catch (err) {
1303
- console.error('Upload failed for', entry.file.name, err);
1304
- // If a multipart upload was initiated but didn't finish (not a user cancel), abort it on S3
1305
- if (entry.s3UploadId && entry.s3Key && entry.status !== 'cancelled') {
1306
- fetch(withBucketUrl('/mbkbucket/upload-abort'), {
1307
- method: 'POST',
1308
- headers: { 'Content-Type': 'application/json' },
1309
- body: JSON.stringify({ uploadId: entry.s3UploadId, key: entry.s3Key })
1310
- }).catch(() => {});
1311
- }
1312
- entry.status = entry.status === 'cancelled' ? 'cancelled' : 'error';
1313
- xhrMap.delete(entry.id);
1314
- renderUploadQueue();
1315
- showAlert('Upload failed: ' + (err.message || 'unknown'), 'error');
851
+ showToast(err.message, 'error');
1316
852
  }
1317
- }
853
+ };
854
+
855
+ // Upload Modal & Flow
856
+ function openUploadModal() {
857
+ const modal = document.getElementById('uploadModal');
858
+ const prefixInput = document.getElementById('uploadPrefixInput');
859
+ if (prefixInput) prefixInput.value = currentPrefix;
860
+ if (modal) modal.classList.add('show');
861
+ }
862
+
863
+ window.closeUploadModal = function () {
864
+ const modal = document.getElementById('uploadModal');
865
+ if (modal) modal.classList.remove('show');
866
+ };
867
+
868
+ function updateUploadQueueUI() {
869
+ const queueEl = document.getElementById('uploadQueue');
870
+ const clearBtn = document.getElementById('clearQueueBtn');
871
+ if (!queueEl) return;
1318
872
 
1319
- function startAllUploads() {
1320
- for (const entry of uploadQueue.slice()) {
1321
- if (entry.status === 'queued' || entry.status === 'error') startUploadEntry(entry);
873
+ if (uploadQueue.length === 0) {
874
+ queueEl.innerHTML = '';
875
+ if (clearBtn) clearBtn.style.display = 'none';
876
+ return;
1322
877
  }
1323
- }
1324
-
1325
- document.getElementById('startUploadBtn')?.addEventListener('click', startAllUploads);
1326
- document.getElementById('clearQueueBtn')?.addEventListener('click', function () { uploadQueue.length = 0; renderUploadQueue(); });
1327
-
1328
-
1329
-
1330
- // Initialize upload section collapse functionality
1331
- function initializeUploadCollapse() {
1332
- const toggleBtn = document.getElementById('uploadToggleBtn');
1333
- const uploadSection = document.getElementById('uploadSection');
1334
- const uploadCard = uploadSection?.closest('.dashboard-card');
1335
-
1336
- if (!toggleBtn || !uploadSection) return;
1337
-
1338
- const syncUploadCollapseState = (expanded) => {
1339
- uploadSection.classList.toggle('expanded', expanded);
1340
- toggleBtn.classList.toggle('expanded', expanded);
1341
- toggleBtn.setAttribute('aria-expanded', expanded ? 'true' : 'false');
1342
- uploadSection.setAttribute('aria-hidden', expanded ? 'false' : 'true');
1343
- if (uploadCard) uploadCard.classList.toggle('upload-panel-open', expanded);
1344
- };
1345
-
1346
- toggleBtn.setAttribute('role', 'button');
1347
- toggleBtn.setAttribute('tabindex', '0');
1348
- toggleBtn.setAttribute('aria-controls', 'uploadSection');
1349
- syncUploadCollapseState(uploadSection.classList.contains('expanded'));
1350
-
1351
- toggleBtn.addEventListener('click', function() {
1352
- const isExpanded = uploadSection.classList.contains('expanded');
1353
- syncUploadCollapseState(!isExpanded);
1354
- });
1355
878
 
1356
- toggleBtn.addEventListener('keydown', function (event) {
1357
- if (event.key === 'Enter' || event.key === ' ') {
1358
- event.preventDefault();
1359
- const isExpanded = uploadSection.classList.contains('expanded');
1360
- syncUploadCollapseState(!isExpanded);
1361
- }
1362
- });
1363
- }
879
+ if (clearBtn) clearBtn.style.display = 'inline-flex';
880
+ queueEl.innerHTML = uploadQueue.map((file, idx) => `
881
+ <div class="upload-queue-item">
882
+ <div class="d-flex align-items-center gap-2">
883
+ <span class="font-medium">${escapeHtml(file.name)}</span>
884
+ <span class="upload-queue-meta">${formatFileSize(file.size)}</span>
885
+ </div>
886
+ <button type="button" class="btn-close" data-remove-queue="${idx}" style="font-size: 14px;">&times;</button>
887
+ </div>
888
+ `).join('');
889
+ }
1364
890
 
1365
- // Add hover effects and animations
1366
- document.addEventListener('DOMContentLoaded', function () {
1367
- // Initialize collapsible upload section
1368
- initializeUploadCollapse();
891
+ async function processUploads() {
892
+ if (isUploading || uploadQueue.length === 0) return;
893
+ isUploading = true;
1369
894
 
1370
- const bucketSelectorEl = document.getElementById('bucketSelector');
1371
- const bucketSwitchStatusEl = document.getElementById('bucketSwitchStatus');
1372
- if (bucketSelectorEl && bucketSelectorEl.value) {
1373
- activeBucket = bucketSelectorEl.value;
1374
- }
1375
- if (bucketSelectorEl) {
1376
- bucketSelectorEl.addEventListener('change', function () {
1377
- const nextBucket = String(this.value || '').trim();
1378
- if (!nextBucket) return;
1379
- if (nextBucket === activeBucket) return;
1380
-
1381
- const nextParams = new URLSearchParams(window.location.search);
1382
- nextParams.set('bucket', nextBucket);
1383
- nextParams.delete('page');
1384
- nextParams.delete('token');
1385
-
1386
- this.disabled = true;
1387
- this.setAttribute('aria-busy', 'true');
1388
- bucketSwitchStatusEl?.classList.remove('is-hidden');
1389
-
1390
- const qs = nextParams.toString();
1391
- window.location.href = `/mbkbucket${qs ? `?${qs}` : ''}`;
1392
- });
1393
- }
1394
-
1395
- // Load initial files with persisted state.
1396
- const urlParams = new URLSearchParams(window.location.search);
1397
- const initialPrefix = urlParams.get('folder') || urlParams.get('prefix') || '';
1398
- const initialPage = parseInt(urlParams.get('page')) || 1;
1399
- const initialSearch = urlParams.get('search') || '';
1400
- const initialSort = (urlParams.get('sort') || 'name').toLowerCase();
1401
- const initialOrder = (urlParams.get('order') || 'asc').toLowerCase();
1402
- const initialView = (urlParams.get('view') || currentViewMode).toLowerCase();
1403
- const initialFilter = (urlParams.get('filter') || 'all').toLowerCase();
895
+ const progressWrap = document.getElementById('uploadProgress');
896
+ const progressBar = document.getElementById('progressBar');
897
+ const percentEl = document.getElementById('uploadPercent');
898
+ const statusLabel = document.getElementById('uploadStatusLabel');
899
+ const startBtn = document.getElementById('startUploadBtn');
1404
900
 
1405
- if (initialView === 'folder' || initialView === 'flat') {
1406
- currentViewMode = initialView;
1407
- }
1408
- currentQuickFilter = ['all', 'media', 'docs', 'code'].includes(initialFilter) ? initialFilter : 'all';
1409
- updateQuickFilterUI();
1410
-
1411
- // Ensure view button state reflects restored mode before first render.
1412
- if (currentViewMode === 'flat') {
1413
- const btnFolder = document.getElementById('viewBtnFolder');
1414
- const btnFlat = document.getElementById('viewBtnFlat');
1415
- btnFlat?.classList.add('active', 'btn-light');
1416
- btnFlat?.classList.remove('btn-outline-light');
1417
- btnFolder?.classList.remove('active', 'btn-light');
1418
- btnFolder?.classList.add('btn-outline-light');
1419
- }
901
+ if (progressWrap) progressWrap.style.display = 'block';
902
+ if (startBtn) startBtn.disabled = true;
903
+
904
+ const totalFiles = uploadQueue.length;
905
+ let completed = 0;
906
+
907
+ for (let i = 0; i < uploadQueue.length; i++) {
908
+ const file = uploadQueue[i];
909
+ if (statusLabel) statusLabel.textContent = `Uploading ${file.name} (${i + 1}/${totalFiles})...`;
910
+
911
+ const formData = new FormData();
912
+ formData.append('file', file);
913
+ formData.append('prefix', currentPrefix);
1420
914
 
1421
- const searchInputInit = document.getElementById('searchInput');
1422
- if (searchInputInit) searchInputInit.value = initialSearch;
1423
- loadFiles(initialPage, initialPrefix, initialSearch, initialSort, initialOrder);
1424
-
1425
- // Search behavior: Trigger when the Search button is clicked OR when the user presses Enter in the search input
1426
- const searchInputEl = document.getElementById('searchInput');
1427
- if (searchInputEl) {
1428
- // Trigger search when Enter is pressed in the input
1429
- searchInputEl.addEventListener('keydown', function (e) {
1430
- if (e.key === 'Enter') {
1431
- e.preventDefault();
1432
- performSearch();
1433
- }
915
+ try {
916
+ const response = await fetch(withBucketUrl('/mbkbucket/upload'), {
917
+ method: 'POST',
918
+ body: formData
1434
919
  });
920
+ const result = await response.json();
921
+ if (!result.success) throw new Error(result.error || 'Upload failed');
922
+ completed++;
923
+ } catch (err) {
924
+ showToast(`Failed to upload ${file.name}: ${err.message}`, 'error');
925
+ }
926
+
927
+ const percent = Math.round(((i + 1) / totalFiles) * 100);
928
+ if (progressBar) progressBar.style.width = `${percent}%`;
929
+ if (percentEl) percentEl.textContent = `${percent}%`;
1435
930
  }
1436
931
 
1437
- // Close modal when clicking outside
1438
- document.getElementById('deleteModal').addEventListener('click', function (e) {
1439
- if (e.target === this) {
1440
- closeModal();
1441
- }
932
+ showToast(`Successfully uploaded ${completed} file(s)`, 'success');
933
+ uploadQueue = [];
934
+ isUploading = false;
935
+ if (startBtn) startBtn.disabled = false;
936
+ if (progressWrap) progressWrap.style.display = 'none';
937
+ closeUploadModal();
938
+ updateUploadQueueUI();
939
+ loadFiles(1, currentPrefix);
940
+ }
941
+
942
+ // Event Listeners Setup
943
+ function initEventListeners() {
944
+ // Tree clicks
945
+ document.getElementById('fm-tree')?.addEventListener('click', (e) => {
946
+ const link = e.target.closest('.tree-link');
947
+ if (!link) return;
948
+ e.preventDefault();
949
+ const filter = link.dataset.treeFilter;
950
+ const prefix = link.dataset.treePrefix;
951
+
952
+ if (filter && filter !== 'all') {
953
+ currentQuickFilter = filter;
954
+ document.querySelectorAll('.quick-filter-btn').forEach(b => b.classList.remove('active'));
955
+ renderTree();
956
+ renderBreadcrumbs();
957
+ renderFiles();
958
+ } else {
959
+ currentQuickFilter = 'all';
960
+ document.querySelectorAll('.quick-filter-btn').forEach(b => {
961
+ b.classList.toggle('active', b.dataset.filter === 'all');
962
+ });
963
+ loadFiles(1, prefix !== undefined ? prefix : '');
964
+ }
1442
965
  });
1443
966
 
1444
- // Delegate view and delete button clicks to avoid inline JS with unescaped data
1445
- document.addEventListener('click', function (e) {
1446
- const actionBtn = e.target.closest('[data-action]');
1447
- if (actionBtn) {
1448
- const action = actionBtn.dataset.action;
1449
- if (action === 'upload-files') {
1450
- window.scrollTo({ top: 0, behavior: 'smooth' });
1451
- document.getElementById('fileInput')?.click();
1452
- } else if (action === 'create-folder') {
1453
- createFolder();
1454
- } else if (action === 'create-file') {
1455
- createFile();
1456
- } else if (action === 'go-root') {
1457
- loadFiles(1, '');
1458
- } else if (action === 'expand-virtual') {
1459
- expandVirtualRows();
1460
- } else if (action === 'search') {
1461
- performSearch();
1462
- } else if (action === 'clear-search') {
1463
- clearSearch();
1464
- } else if (action === 'retry-load') {
1465
- loadFiles(parseInt(actionBtn.dataset.retryPage, 10) || 1, actionBtn.dataset.retryPrefix || '');
1466
- } else if (action === 'close-toast') {
1467
- actionBtn.closest('.toast-notification')?.remove();
1468
- }
1469
- return;
1470
- }
967
+ // Breadcrumb clicks
968
+ document.getElementById('fm-breadcrumb')?.addEventListener('click', (e) => {
969
+ const link = e.target.closest('[data-nav-prefix]');
970
+ if (!link) return;
971
+ e.preventDefault();
972
+ currentQuickFilter = 'all';
973
+ loadFiles(1, link.dataset.navPrefix || '');
974
+ });
1471
975
 
1472
- const quickFilterBtn = e.target.closest('[data-filter]');
1473
- if (quickFilterBtn) {
1474
- setQuickFilter(quickFilterBtn.dataset.filter || 'all');
1475
- return;
1476
- }
976
+ // Quick filters
977
+ document.querySelectorAll('.quick-filter-btn').forEach(btn => {
978
+ btn.addEventListener('click', () => {
979
+ document.querySelectorAll('.quick-filter-btn').forEach(b => b.classList.remove('active'));
980
+ btn.classList.add('active');
981
+ currentQuickFilter = btn.dataset.filter || 'all';
982
+ renderTree();
983
+ renderBreadcrumbs();
984
+ renderFiles();
985
+ syncStateUrl();
986
+ });
987
+ });
1477
988
 
1478
- const viewModeBtn = e.target.closest('[data-view-mode]');
1479
- if (viewModeBtn) {
1480
- changeViewMode(viewModeBtn.dataset.viewMode);
1481
- return;
1482
- }
989
+ // View mode toggle
990
+ const gridBtn = document.getElementById('view-grid');
991
+ const listBtn = document.getElementById('view-list');
992
+
993
+ gridBtn?.addEventListener('click', () => {
994
+ currentViewMode = 'grid';
995
+ localStorage.setItem('mbk_view_mode', 'grid');
996
+ gridBtn.classList.add('active');
997
+ listBtn?.classList.remove('active');
998
+ renderFiles();
999
+ syncStateUrl();
1000
+ });
1483
1001
 
1484
- const navBtn = e.target.closest('[data-nav-prefix]');
1485
- if (navBtn) {
1486
- loadFiles(1, navBtn.dataset.navPrefix || '');
1487
- return;
1488
- }
1002
+ listBtn?.addEventListener('click', () => {
1003
+ currentViewMode = 'list';
1004
+ localStorage.setItem('mbk_view_mode', 'list');
1005
+ listBtn.classList.add('active');
1006
+ gridBtn?.classList.remove('active');
1007
+ renderFiles();
1008
+ syncStateUrl();
1009
+ });
1489
1010
 
1490
- const sortHeader = e.target.closest('[data-sort-field]');
1491
- if (sortHeader) {
1492
- toggleSort(sortHeader.dataset.sortField);
1493
- return;
1494
- }
1011
+ // Search input
1012
+ const searchInput = document.getElementById('searchInput');
1013
+ const clearSearchBtn = document.getElementById('searchClearBtn');
1014
+
1015
+ let searchTimer = null;
1016
+ searchInput?.addEventListener('input', (e) => {
1017
+ const val = e.target.value;
1018
+ if (clearSearchBtn) clearSearchBtn.style.display = val ? 'block' : 'none';
1019
+
1020
+ clearTimeout(searchTimer);
1021
+ searchTimer = setTimeout(() => {
1022
+ currentSearch = val.trim();
1023
+ renderFiles();
1024
+ }, 250);
1025
+ });
1495
1026
 
1496
- const pageLink = e.target.closest('.page-link[data-page]');
1497
- if (pageLink) {
1498
- e.preventDefault();
1499
- loadFiles(
1500
- parseInt(pageLink.dataset.page, 10) || 1,
1501
- pageLink.dataset.prefix || '',
1502
- pageLink.dataset.search || '',
1503
- pageLink.dataset.sort || currentSort,
1504
- pageLink.dataset.order || currentOrder,
1505
- pageLink.dataset.token || ''
1506
- );
1507
- return;
1508
- }
1027
+ clearSearchBtn?.addEventListener('click', () => {
1028
+ if (searchInput) searchInput.value = '';
1029
+ clearSearchBtn.style.display = 'none';
1030
+ currentSearch = '';
1031
+ renderFiles();
1032
+ });
1509
1033
 
1510
- const folderRow = e.target.closest('.folder-row');
1511
- if (folderRow && !e.target.closest('.delete-btn')) {
1512
- const folderPath = folderRow.dataset.folderPath;
1513
- if (folderPath) loadFiles(1, folderPath);
1514
- return;
1515
- }
1034
+ // Grid Item Interactions (Click, Double Click, Star, Context Menu)
1035
+ document.getElementById('fm-grid')?.addEventListener('click', (e) => {
1036
+ // Star click
1037
+ const starBtn = e.target.closest('[data-star-key]');
1038
+ if (starBtn) {
1039
+ e.stopPropagation();
1040
+ toggleStar(starBtn.dataset.starKey);
1041
+ return;
1042
+ }
1516
1043
 
1517
- const vbtn = e.target.closest('.view-btn');
1518
- if (vbtn) {
1519
- const keyEnc = vbtn.dataset.keyEnc;
1520
- const key = keyEnc || vbtn.dataset.key;
1521
- const filename = vbtn.dataset.filename || (vbtn.dataset.key || key);
1522
- handleViewFile(key, filename, e);
1523
- return;
1044
+ // Menu click
1045
+ const menuBtn = e.target.closest('.fm-item-menu');
1046
+ if (menuBtn) {
1047
+ e.stopPropagation();
1048
+ const rect = menuBtn.getBoundingClientRect();
1049
+ const fileKey = menuBtn.dataset.menuFile;
1050
+ const folderPath = menuBtn.dataset.menuFolder;
1051
+
1052
+ if (fileKey) {
1053
+ const isStarred = starredFiles.has(fileKey);
1054
+ openContextMenu(rect.left, rect.bottom + 4, [
1055
+ { id: 'view', label: 'Preview', icon: '<i class="fas fa-eye"></i>', action: () => openPreview(fileKey) },
1056
+ { id: 'download', label: 'Download', icon: '<i class="fas fa-download"></i>', action: () => downloadFile(fileKey) },
1057
+ { id: 'share', label: 'Copy link', icon: '<i class="fas fa-link"></i>', action: () => copyShareLink(fileKey) },
1058
+ { id: 'star', label: isStarred ? 'Unstar' : 'Star', icon: '<i class="fas fa-star"></i>', action: () => toggleStar(fileKey) },
1059
+ '-',
1060
+ { id: 'delete', label: 'Delete', icon: '<i class="fas fa-trash"></i>', danger: true, action: () => promptDelete(fileKey, false) }
1061
+ ]);
1062
+ } else if (folderPath) {
1063
+ openContextMenu(rect.left, rect.bottom + 4, [
1064
+ { id: 'open', label: 'Open Folder', icon: '<i class="fas fa-folder-open"></i>', action: () => loadFiles(1, folderPath) },
1065
+ '-',
1066
+ { id: 'delete', label: 'Delete Folder', icon: '<i class="fas fa-trash"></i>', danger: true, action: () => promptDelete(folderPath + '/', true) }
1067
+ ]);
1524
1068
  }
1069
+ return;
1070
+ }
1525
1071
 
1526
- const dbtn = e.target.closest('.delete-btn');
1527
- if (dbtn) {
1528
- e.stopPropagation();
1529
- const key = dbtn.dataset.key;
1530
- const isFolder = dbtn.dataset.isFolder === 'true';
1531
- if (key) confirmDelete(key, isFolder);
1532
- return;
1533
- }
1072
+ // Folder click to navigate
1073
+ const folderItem = e.target.closest('[data-type="folder"]');
1074
+ if (folderItem) {
1075
+ e.stopPropagation();
1076
+ loadFiles(1, folderItem.dataset.folderPath);
1077
+ return;
1078
+ }
1534
1079
 
1535
- const cbtn = e.target.closest('.copy-btn');
1536
- if (cbtn) {
1537
- e.stopPropagation();
1538
- const keyEnc = cbtn.dataset.keyEnc;
1539
- if (!keyEnc) return;
1540
- const link = `${window.location.origin}${getViewUrl(keyEnc)}`;
1541
- const fallbackCopy = () => {
1542
- try {
1543
- const ta = document.createElement('textarea');
1544
- ta.value = link;
1545
- ta.style.position = 'fixed';
1546
- ta.style.opacity = '0';
1547
- document.body.appendChild(ta);
1548
- ta.select();
1549
- document.execCommand('copy');
1550
- document.body.removeChild(ta);
1551
- showAlert('Link copied to clipboard!', 'success');
1552
- } catch {
1553
- showAlert('Failed to copy link', 'error');
1554
- }
1555
- };
1556
- if (navigator.clipboard && navigator.clipboard.writeText) {
1557
- navigator.clipboard.writeText(link).then(() => {
1558
- showAlert('Link copied to clipboard!', 'success');
1559
- }).catch(fallbackCopy);
1560
- } else {
1561
- fallbackCopy();
1562
- }
1563
- return;
1564
- }
1080
+ // File click to preview
1081
+ const fileItem = e.target.closest('[data-type="file"]');
1082
+ if (fileItem && !e.target.closest('.file-select-cb')) {
1083
+ e.stopPropagation();
1084
+ openPreview(fileItem.dataset.key);
1085
+ }
1086
+ });
1565
1087
 
1088
+ // Bucket Selector
1089
+ document.getElementById('bucketSelector')?.addEventListener('change', (e) => {
1090
+ const newBucket = e.target.value;
1091
+ if (!newBucket) return;
1092
+ activeBucket = newBucket;
1093
+ loadFiles(1, '');
1566
1094
  });
1567
- });
1568
1095
 
1569
- // Function to check if file can be viewed in browser
1096
+ // Top action buttons & Modals
1097
+ document.querySelectorAll('[data-action="upload-files"]').forEach(btn => {
1098
+ btn.addEventListener('click', openUploadModal);
1099
+ });
1570
1100
 
1571
- // Enhanced view button click handler
1572
- function handleViewFile(key, filename, event) {
1573
- // Prevent default action and event bubbling
1574
- if (event) {
1575
- event.preventDefault();
1576
- event.stopPropagation();
1577
- }
1101
+ document.querySelectorAll('[data-action="create-folder"]').forEach(btn => {
1102
+ btn.addEventListener('click', openNewFolderModal);
1103
+ });
1578
1104
 
1579
- if (!isViewable(filename)) {
1580
- alert(`File type "${filename.split('.').pop().toUpperCase()}" is not supported for viewing in browser.\n\nSupported types include: Images, Videos (MP4, WebM), Audio (MP3, WAV), Text files, Code files, and PDF documents.`);
1581
- return false;
1582
- }
1105
+ // Drag and Drop Upload Zone
1106
+ const dropZone = document.getElementById('dropZone');
1107
+ const fileInput = document.getElementById('fileInput');
1583
1108
 
1584
- // Determine file type
1585
- const extension = filename.split('.').pop().toLowerCase();
1586
- const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico'];
1587
- const videoTypes = ['mp4', 'webm', 'ogg'];
1588
-
1589
- // Determine preview mode from extension groups
1590
- const previewableImage = imageTypes.includes(extension);
1591
- const previewableVideo = videoTypes.includes(extension);
1592
- const previewableAudio = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a'].includes(extension);
1593
- const previewablePdf = extension === 'pdf';
1594
- const previewableText = ['txt', 'md', 'json', 'xml', 'csv', 'log', 'js', 'ts', 'html', 'htm', 'css', 'py', 'java', 'cpp', 'c', 'h', 'cs', 'rb', 'go', 'rs', 'sql', 'sh', 'bat', 'ps1', 'yaml', 'yml', 'toml', 'ini', 'conf'].includes(extension);
1595
-
1596
- let previewMode = 'unsupported';
1597
- if (previewableImage) previewMode = 'image';
1598
- else if (previewableVideo) previewMode = 'video';
1599
- else if (previewableAudio) previewMode = 'audio';
1600
- else if (previewablePdf) previewMode = 'pdf';
1601
- else if (previewableText) previewMode = 'text';
1602
-
1603
- // Build an encoded key safely (if key already appears encoded, don't double-encode)
1604
- const encodedKey = (key && key.includes('%')) ? key : encodeURIComponent(key || filename);
1605
-
1606
- // Show the inline preview
1607
- showPreviewModal(encodedKey, filename, extension, previewMode);
1608
- return false;
1609
- }
1610
-
1611
- // Function to get file type category
1612
-
1613
- // Preview modal helpers
1614
- function showPreviewModal(encodedKey, filename, extension, previewMode = 'auto') {
1615
- const modal = document.getElementById('previewModal');
1616
- if (!modal) {
1617
- window.open(getViewUrl(encodedKey), '_blank', 'noopener,noreferrer');
1618
- return;
1619
- }
1109
+ dropZone?.addEventListener('click', () => fileInput?.click());
1620
1110
 
1621
- const dialog = modal.querySelector('.modal-dialog');
1622
- const titleEl = document.getElementById('previewFileName');
1623
- const contentEl = modal.querySelector('.preview-content');
1624
- const openBtn = document.getElementById('previewOpenBtn');
1625
- const downloadBtn = document.getElementById('previewDownloadBtn');
1626
- const closeBtn = document.getElementById('previewCloseBtn');
1627
- const closeFooter = document.getElementById('previewCloseFooter');
1628
- const fsBtn = document.getElementById('previewFullscreenBtn');
1629
-
1630
- titleEl.textContent = filename;
1631
- titleEl.title = filename;
1632
- openBtn.href = getViewUrl(encodedKey);
1633
- downloadBtn.href = '#';
1634
- downloadBtn.onclick = (e) => {
1111
+ dropZone?.addEventListener('dragover', (e) => {
1635
1112
  e.preventDefault();
1636
- downloadFileViaFetch(getDownloadUrl(encodedKey), filename);
1637
- };
1638
- downloadBtn.removeAttribute('download');
1639
-
1640
- // Reset sizing classes
1641
- dialog.classList.remove('preview-large', 'preview-compact');
1642
-
1643
- // Clear previous content and show loading
1644
- contentEl.innerHTML = `<div class="empty-preview"><i class="fas fa-spinner fa-spin"></i><div style="margin-top:0.5rem; color:var(--gray-color-ml);">Loading preview...</div></div>`;
1645
-
1646
- const imgTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico'];
1647
- const videoTypes = ['mp4', 'webm', 'ogg'];
1648
- const audioTypes = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a'];
1649
- const textTypes = ['txt', 'md', 'json', 'xml', 'csv', 'log', 'js', 'ts', 'html', 'htm', 'css', 'py', 'java', 'cpp', 'c', 'h', 'cs', 'rb', 'go', 'rs', 'sql', 'sh', 'bat', 'ps1', 'yaml', 'yml', 'toml', 'ini', 'conf'];
1650
- const effectiveMode = previewMode === 'auto'
1651
- ? (imgTypes.includes(extension) ? 'image'
1652
- : videoTypes.includes(extension) ? 'video'
1653
- : audioTypes.includes(extension) ? 'audio'
1654
- : extension === 'pdf' ? 'pdf'
1655
- : textTypes.includes(extension) ? 'text'
1656
- : 'unsupported')
1657
- : previewMode;
1658
-
1659
- // Render content and adjust dialog size
1660
- if (effectiveMode === 'image') {
1661
- dialog.classList.add('preview-compact');
1662
- const img = document.createElement('img');
1663
- img.className = 'preview-img';
1664
- img.src = getViewUrl(encodedKey);
1665
- img.alt = filename;
1666
- img.onload = () => { };
1667
- img.onerror = () => { contentEl.textContent = 'Could not load preview.'; };
1668
- contentEl.innerHTML = '';
1669
- contentEl.appendChild(img);
1670
- } else if (effectiveMode === 'video') {
1671
- dialog.classList.add('preview-large');
1672
- const video = document.createElement('video');
1673
- video.className = 'preview-video';
1674
- video.controls = true;
1675
- video.src = getViewUrl(encodedKey);
1676
- contentEl.innerHTML = '';
1677
- contentEl.appendChild(video);
1678
- } else if (effectiveMode === 'audio') {
1679
- dialog.classList.add('preview-compact');
1680
- const audio = document.createElement('audio');
1681
- audio.className = 'preview-audio';
1682
- audio.controls = true;
1683
- audio.src = getViewUrl(encodedKey);
1684
- contentEl.innerHTML = '';
1685
- contentEl.appendChild(audio);
1686
- } else if (effectiveMode === 'pdf') {
1687
- dialog.classList.add('preview-large');
1688
- const iframe = document.createElement('iframe');
1689
- iframe.className = 'preview-iframe';
1690
- iframe.src = getViewUrl(encodedKey);
1691
- contentEl.innerHTML = '';
1692
- contentEl.appendChild(iframe);
1693
- } else if (effectiveMode === 'text') {
1694
- dialog.classList.add('preview-compact');
1695
-
1696
- fetch(getViewUrl(encodedKey))
1697
- .then(r => r.text())
1698
- .then(text => {
1699
- contentEl.innerHTML = '';
1700
- const pre = document.createElement('pre');
1701
- pre.textContent = text;
1702
- pre.className = 'preview-text';
1703
- contentEl.appendChild(pre);
1704
- })
1705
- .catch(err => { contentEl.textContent = 'Could not load preview.'; });
1706
- } else {
1707
- contentEl.innerHTML = `<div style="text-align:center;">Preview not available for this file type. <a href="${getDownloadUrl(encodedKey)}">Download</a></div>`;
1708
- }
1113
+ dropZone.classList.add('dragover');
1114
+ });
1709
1115
 
1710
- // Show modal
1711
- modal.classList.add('show');
1712
- modal.style.display = 'flex';
1713
- modal.setAttribute('aria-hidden', 'false');
1714
-
1715
- // Focus management + focus trap
1716
- const prevFocus = document.activeElement;
1717
- const focusableSelector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
1718
- function getFocusable() { return Array.from(modal.querySelectorAll(focusableSelector)).filter(el => !el.hasAttribute('disabled')); }
1719
- const focusable = () => getFocusable();
1720
- const firstFocusable = () => focusable()[0];
1721
- const lastFocusable = () => focusable()[focusable().length - 1];
1722
-
1723
- // Focus initial
1724
- (closeBtn || closeFooter).focus();
1725
-
1726
- function onKeyDown(e) {
1727
- if (e.key === 'Escape') return cleanup();
1728
- if (e.key === 'Tab') {
1729
- const f = focusable();
1730
- if (f.length === 0) return;
1731
- if (e.shiftKey && document.activeElement === f[0]) { e.preventDefault(); f[f.length - 1].focus(); }
1732
- else if (!e.shiftKey && document.activeElement === f[f.length - 1]) { e.preventDefault(); f[0].focus(); }
1733
- }
1734
- }
1116
+ dropZone?.addEventListener('dragleave', () => {
1117
+ dropZone.classList.remove('dragover');
1118
+ });
1735
1119
 
1736
- function onBackdropClick(e) { if (e.target === modal) cleanup(); }
1737
-
1738
- // Fullscreen toggle
1739
- let fsActive = false;
1740
- function toggleFullscreen() {
1741
- if (!document.fullscreenElement) {
1742
- dialog.requestFullscreen?.();
1743
- fsBtn.innerHTML = '<i class="fas fa-compress"></i>';
1744
- fsActive = true;
1745
- } else {
1746
- document.exitFullscreen?.();
1747
- fsBtn.innerHTML = '<i class="fas fa-expand"></i>';
1748
- fsActive = false;
1749
- }
1750
- }
1120
+ dropZone?.addEventListener('drop', (e) => {
1121
+ e.preventDefault();
1122
+ dropZone.classList.remove('dragover');
1123
+ if (e.dataTransfer.files?.length) {
1124
+ uploadQueue.push(...Array.from(e.dataTransfer.files));
1125
+ updateUploadQueueUI();
1126
+ }
1127
+ });
1751
1128
 
1752
- function cleanup() {
1753
- // Exit fullscreen if active
1754
- if (fsActive && document.fullscreenElement) document.exitFullscreen?.();
1755
- modal.classList.remove('show');
1756
- modal.style.display = 'none';
1757
- modal.setAttribute('aria-hidden', 'true');
1758
- if (prevFocus && prevFocus.focus) prevFocus.focus();
1759
- modal.removeEventListener('click', onBackdropClick);
1760
- document.removeEventListener('keydown', onKeyDown);
1761
- fsBtn.removeEventListener('click', toggleFullscreen);
1762
- (closeBtn || closeFooter).removeEventListener('click', cleanup);
1763
- closeFooter.removeEventListener('click', cleanup);
1764
- }
1129
+ fileInput?.addEventListener('change', (e) => {
1130
+ if (e.target.files?.length) {
1131
+ uploadQueue.push(...Array.from(e.target.files));
1132
+ updateUploadQueueUI();
1133
+ }
1134
+ });
1765
1135
 
1766
- modal.addEventListener('click', onBackdropClick);
1767
- document.addEventListener('keydown', onKeyDown);
1768
- fsBtn.addEventListener('click', toggleFullscreen);
1769
- (closeBtn || closeFooter).addEventListener('click', cleanup);
1770
- closeFooter.addEventListener('click', cleanup);
1771
- }
1136
+ document.getElementById('uploadQueue')?.addEventListener('click', (e) => {
1137
+ const removeBtn = e.target.closest('[data-remove-queue]');
1138
+ if (removeBtn) {
1139
+ const idx = parseInt(removeBtn.dataset.removeQueue, 10);
1140
+ uploadQueue.splice(idx, 1);
1141
+ updateUploadQueueUI();
1142
+ }
1143
+ });
1772
1144
 
1773
- function closePreviewModal() {
1774
- const modal = document.getElementById('previewModal');
1775
- if (!modal) return;
1776
- modal.classList.remove('show');
1777
- modal.style.display = 'none';
1778
- modal.setAttribute('aria-hidden', 'true');
1779
- }
1780
-
1781
- // Update file icons in the table
1782
- function updateFileIcons() {
1783
- const fileRows = document.querySelectorAll('.file-row');
1784
- fileRows.forEach(row => {
1785
- const fileNameElement = row.querySelector('.file-name');
1786
- const folderPathElement = row.querySelector('.folder-path');
1787
- const fileIconElement = row.querySelector('.file-icon i');
1788
- const fileIconContainer = row.querySelector('.file-icon');
1789
- const viewButton = row.querySelector('.view-btn');
1790
-
1791
- if (fileNameElement && fileIconElement && fileIconContainer) {
1792
- const fullPath = fileNameElement.getAttribute('data-full-path');
1793
-
1794
- // Split the path into folder and filename
1795
- const lastSlashIndex = fullPath.lastIndexOf('/');
1796
- let folder = '';
1797
- let filename = fullPath;
1798
-
1799
- if (lastSlashIndex !== -1) {
1800
- folder = fullPath.substring(0, lastSlashIndex);
1801
- filename = fullPath.substring(lastSlashIndex + 1);
1802
- }
1803
-
1804
- // Truncate long filenames
1805
- const truncatedName = truncateFileName(filename);
1806
-
1807
- // Update the display
1808
- fileNameElement.textContent = truncatedName;
1809
- fileNameElement.title = filename; // Full name on hover
1810
-
1811
-
1812
-
1813
- const iconClass = getFileIcon(filename);
1814
- const typeCategory = getFileTypeCategory(filename);
1815
-
1816
- // Update icon
1817
- fileIconElement.className = iconClass;
1818
-
1819
- // Add type-specific styling
1820
- if (typeCategory) {
1821
- fileIconContainer.classList.add(typeCategory);
1822
- }
1823
-
1824
- // Show/hide and update view button based on file type
1825
- if (viewButton) {
1826
- const isFileViewable = isViewable(filename);
1827
-
1828
- if (isFileViewable) {
1829
- viewButton.style.display = 'inline-flex';
1830
- viewButton.style.opacity = '1';
1831
- viewButton.title = 'View file in browser';
1832
- viewButton.disabled = false;
1833
- } else {
1834
- viewButton.style.opacity = '0.5';
1835
- viewButton.title = 'File type not supported for viewing';
1836
- viewButton.disabled = true;
1837
- }
1838
- }
1839
- }
1145
+ document.getElementById('clearQueueBtn')?.addEventListener('click', () => {
1146
+ uploadQueue = [];
1147
+ updateUploadQueueUI();
1840
1148
  });
1841
- }
1842
1149
 
1843
- // Selection & bulk actions
1844
- function setupSelection() {
1845
- const selectAll = document.getElementById('selectAllCheckbox');
1846
- const bulkBar = document.getElementById('bulkActionsBar');
1847
- const bulkCount = document.getElementById('bulkSelectedCount');
1848
- const bulkDeleteBtn = document.getElementById('bulkDeleteBtn');
1150
+ document.getElementById('startUploadBtn')?.addEventListener('click', processUploads);
1849
1151
 
1850
- const checkboxes = Array.from(document.querySelectorAll('.selectFileCheckbox'));
1152
+ // Fullscreen Preview Toggle
1153
+ document.getElementById('previewFullscreenBtn')?.addEventListener('click', () => {
1154
+ document.querySelector('.preview-modal-dialog')?.classList.toggle('is-fullscreen');
1155
+ });
1851
1156
 
1852
- const selected = new Set();
1157
+ // Pagination clicks
1158
+ document.getElementById('paginationList')?.addEventListener('click', (e) => {
1159
+ const link = e.target.closest('[data-page]');
1160
+ if (!link) return;
1161
+ const page = parseInt(link.dataset.page, 10);
1162
+ if (!isNaN(page) && page > 0) {
1163
+ loadFiles(page, currentPrefix, currentSearch);
1164
+ }
1165
+ });
1853
1166
 
1854
- function updateUI() {
1855
- const count = selected.size;
1856
- bulkCount.textContent = `${count} selected`;
1857
- bulkBar.style.display = count > 0 ? 'block' : 'none';
1858
- bulkDeleteBtn.disabled = count === 0;
1859
- if (selectAll) selectAll.checked = checkboxes.length > 0 && selected.size === checkboxes.length;
1860
- }
1167
+ // Global click to close context menu
1168
+ document.addEventListener('click', closeContextMenu);
1861
1169
 
1862
- checkboxes.forEach(cb => {
1863
- cb.checked = false;
1864
- cb.addEventListener('change', function (e) {
1865
- const key = this.dataset.key;
1866
- if (this.checked) selected.add(key); else selected.delete(key);
1867
- updateUI();
1868
- });
1869
- });
1170
+ // Handle delete form submission
1171
+ const deleteForm = document.getElementById('deleteForm');
1172
+ deleteForm?.addEventListener('submit', async (e) => {
1173
+ e.preventDefault();
1174
+ const formData = new FormData(deleteForm);
1175
+ const key = formData.get('key');
1176
+ const isFolder = formData.get('folder') === 'true';
1870
1177
 
1871
- if (selectAll) {
1872
- selectAll.checked = false;
1873
- selectAll.addEventListener('change', function () {
1874
- const checked = this.checked;
1875
- selected.clear();
1876
- checkboxes.forEach(cb => { cb.checked = checked; if (checked) selected.add(cb.dataset.key); });
1877
- updateUI();
1178
+ try {
1179
+ const response = await fetch(withBucketUrl('/mbkbucket/delete'), {
1180
+ method: 'POST',
1181
+ headers: { 'Content-Type': 'application/json' },
1182
+ body: JSON.stringify({ key, isFolder, prefix: currentPrefix })
1878
1183
  });
1879
- }
1184
+ const data = await response.json();
1185
+ if (!data.success) throw new Error(data.error || 'Delete failed');
1880
1186
 
1881
- if (bulkDeleteBtn) {
1882
- bulkDeleteBtn.addEventListener('click', async function () {
1883
- if (!confirm('Delete selected files? This cannot be undone.')) return;
1884
- const keys = Array.from(selected);
1885
- try {
1886
- const resp = await fetch(withBucketUrl('/mbkbucket/delete'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ keys }) });
1887
- const json = await resp.json();
1888
- if (json.success) {
1889
- showAlert('Deleted selected files', 'success');
1890
- loadFiles(currentPage, currentPrefix);
1891
- } else {
1892
- showAlert(json.error || 'Delete failed', 'error');
1893
- }
1894
- } catch (err) {
1895
- showAlert('Delete failed: ' + err.message, 'error');
1896
- }
1897
- });
1187
+ closeModal();
1188
+ showToast(`Successfully deleted ${key}`, 'success');
1189
+ loadFiles(currentPage, currentPrefix);
1190
+ } catch (err) {
1191
+ showToast(err.message, 'error');
1192
+ }
1193
+ });
1194
+ }
1195
+
1196
+ // Initialization
1197
+ document.addEventListener('DOMContentLoaded', () => {
1198
+ // Sync view mode buttons
1199
+ if (currentViewMode === 'list') {
1200
+ document.getElementById('view-list')?.classList.add('active');
1201
+ document.getElementById('view-grid')?.classList.remove('active');
1898
1202
  }
1899
- }
1900
1203
 
1901
- // Create folder (zero-byte placeholder object)
1902
- async function createFolder() {
1903
- const name = prompt('Enter new folder name (no slashes):');
1904
- if (!name) return;
1905
- const safe = name.replace(/[\\/]+/g, '').trim();
1906
- if (!safe) return showAlert('Invalid folder name', 'error');
1204
+ initEventListeners();
1907
1205
 
1908
- try {
1909
- const resp = await fetch(withBucketUrl('/mbkbucket/create-folder'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prefix: currentPrefix, folderName: safe }) });
1910
- const json = await resp.json();
1911
- if (json.success) {
1912
- showAlert('Folder created', 'success');
1913
- loadFiles(currentPage, currentPrefix);
1914
- } else {
1915
- showAlert(json.error || 'Create folder failed', 'error');
1916
- }
1917
- } catch (err) {
1918
- showAlert('Create folder failed: ' + err.message, 'error');
1919
- }
1920
- }
1921
-
1922
- // Create new file
1923
- async function createFile() {
1924
- let name = prompt('Enter new file name:');
1925
- if (!name) return;
1926
-
1927
- name = name.trim();
1928
- if (!name.includes('.')) {
1929
- name += '.txt';
1930
- }
1931
-
1932
- // Ensure no leading slashes, we upload to currentPrefix
1933
- if (name.startsWith('/')) name = name.substring(1);
1934
-
1935
- // Check if valid
1936
- if (!name) return showAlert('Invalid file name', 'error');
1937
-
1938
- // Create empty file content
1939
- const file = new File([" "], name, { type: "text/plain" });
1940
- const formData = new FormData();
1941
- formData.append('file', file);
1942
- formData.append('prefix', currentPrefix);
1943
-
1944
- try {
1945
- const resp = await fetch(withBucketUrl('/mbkbucket/upload'), {
1946
- method: 'POST',
1947
- body: formData
1948
- });
1949
-
1950
- const json = await resp.json();
1951
- if (json.success) {
1952
- showAlert('File created', 'success');
1953
- loadFiles(currentPage, currentPrefix);
1954
- } else {
1955
- showAlert(json.error || 'Create file failed', 'error');
1956
- }
1957
- } catch (err) {
1958
- showAlert('Create file failed: ' + err.message, 'error');
1959
- }
1960
- }
1961
-
1962
-
1963
-
1964
- // mbkbucket shared utilities (exposed to global window for backward compatibility)
1965
- (function (w) {
1966
- 'use strict';
1967
-
1968
- w.formatDate = function (dateString) {
1969
- const date = new Date(dateString);
1970
- return date.toLocaleString();
1971
- };
1972
-
1973
- w.formatFileSize = function (bytes) {
1974
- if (bytes === 0) return '0 Bytes';
1975
- const k = 1024;
1976
- const sizes = ['Bytes', 'KB', 'MB', 'GB'];
1977
- const i = Math.floor(Math.log(bytes) / Math.log(k));
1978
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
1979
- };
1980
-
1981
- w.formatTime = function (seconds) {
1982
- if (seconds < 60) {
1983
- return Math.round(seconds) + 's';
1984
- } else if (seconds < 3600) {
1985
- const minutes = Math.floor(seconds / 60);
1986
- const remainingSeconds = Math.round(seconds % 60);
1987
- return minutes + 'm ' + remainingSeconds + 's';
1988
- } else {
1989
- const hours = Math.floor(seconds / 3600);
1990
- const minutes = Math.floor((seconds % 3600) / 60);
1991
- return hours + 'h ' + minutes + 'm';
1992
- }
1993
- };
1994
-
1995
- w.getFileIcon = function (filename) {
1996
- const extension = (filename || '').split('.').pop().toLowerCase();
1997
-
1998
- const iconMap = {
1999
- jpg: 'fas fa-image',
2000
- jpeg: 'fas fa-image',
2001
- png: 'fas fa-image',
2002
- gif: 'fas fa-image',
2003
- webp: 'fas fa-image',
2004
- svg: 'fas fa-image',
2005
- bmp: 'fas fa-image',
2006
- ico: 'fas fa-image',
2007
- mp4: 'fas fa-video',
2008
- webm: 'fas fa-video',
2009
- ogg: 'fas fa-video',
2010
- avi: 'fas fa-video',
2011
- mov: 'fas fa-video',
2012
- wmv: 'fas fa-video',
2013
- flv: 'fas fa-video',
2014
- mkv: 'fas fa-video',
2015
- mp3: 'fas fa-music',
2016
- wav: 'fas fa-music',
2017
- flac: 'fas fa-music',
2018
- aac: 'fas fa-music',
2019
- m4a: 'fas fa-music',
2020
- pdf: 'fas fa-file-pdf',
2021
- doc: 'fas fa-file-word',
2022
- docx: 'fas fa-file-word',
2023
- xls: 'fas fa-file-excel',
2024
- xlsx: 'fas fa-file-excel',
2025
- ppt: 'fas fa-file-powerpoint',
2026
- pptx: 'fas fa-file-powerpoint',
2027
- js: 'fab fa-js-square',
2028
- ts: 'fas fa-code',
2029
- html: 'fab fa-html5',
2030
- htm: 'fab fa-html5',
2031
- css: 'fab fa-css3-alt',
2032
- php: 'fab fa-php',
2033
- py: 'fab fa-python',
2034
- java: 'fab fa-java',
2035
- cpp: 'fas fa-code',
2036
- c: 'fas fa-code',
2037
- h: 'fas fa-code',
2038
- cs: 'fas fa-code',
2039
- rb: 'fas fa-code',
2040
- go: 'fas fa-code',
2041
- rs: 'fas fa-code',
2042
- sql: 'fas fa-database',
2043
- sh: 'fas fa-terminal',
2044
- bat: 'fas fa-terminal',
2045
- ps1: 'fas fa-terminal',
2046
- txt: 'fas fa-file-alt',
2047
- md: 'fab fa-markdown',
2048
- json: 'fas fa-code',
2049
- xml: 'fas fa-code',
2050
- csv: 'fas fa-file-csv',
2051
- log: 'fas fa-file-alt',
2052
- yaml: 'fas fa-code',
2053
- yml: 'fas fa-code',
2054
- toml: 'fas fa-code',
2055
- ini: 'fas fa-cog',
2056
- conf: 'fas fa-cog',
2057
- zip: 'fas fa-file-archive',
2058
- rar: 'fas fa-file-archive',
2059
- '7z': 'fas fa-file-archive',
2060
- tar: 'fas fa-file-archive',
2061
- gz: 'fas fa-file-archive'
2062
- };
2063
-
2064
- return iconMap[extension] || 'fas fa-file-alt';
2065
- };
2066
-
2067
- w.isViewable = function (filename) {
2068
- const extension = (filename || '').split('.').pop().toLowerCase();
2069
- const viewableTypes = [
2070
- 'jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico',
2071
- 'mp4', 'webm', 'ogg',
2072
- 'mp3', 'wav', 'ogg',
2073
- 'txt', 'md', 'json', 'xml', 'csv', 'log', 'js', 'ts', 'html', 'htm', 'css',
2074
- 'php', 'py', 'java', 'cpp', 'c', 'h', 'cs', 'rb', 'go', 'rs', 'sql',
2075
- 'sh', 'bat', 'ps1', 'yaml', 'yml', 'toml', 'ini', 'conf',
2076
- 'pdf'
2077
- ];
2078
-
2079
- return viewableTypes.includes(extension);
2080
- };
2081
-
2082
- w.getFileTypeCategory = function (filename) {
2083
- const extension = (filename || '').split('.').pop().toLowerCase();
2084
- const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico'];
2085
- const videoTypes = ['mp4', 'webm', 'ogg', 'avi', 'mov', 'wmv', 'flv', 'mkv'];
2086
- const audioTypes = ['mp3', 'wav', 'flac', 'aac', 'm4a'];
2087
- const documentTypes = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'];
2088
- const codeTypes = ['js', 'ts', 'html', 'htm', 'css', 'php', 'py', 'java', 'cpp', 'c', 'h', 'cs', 'rb', 'go', 'rs', 'sql', 'sh', 'bat', 'ps1', 'yaml', 'yml', 'toml', 'ini', 'conf', 'txt', 'md', 'json', 'xml', 'csv', 'log'];
2089
-
2090
- if (imageTypes.includes(extension)) return 'image-file';
2091
- if (videoTypes.includes(extension)) return 'video-file';
2092
- if (audioTypes.includes(extension)) return 'audio-file';
2093
- if (documentTypes.includes(extension)) return 'document-file';
2094
- if (codeTypes.includes(extension)) return 'code-file';
2095
-
2096
- return '';
2097
- };
2098
-
2099
- w.truncateFileName = function (filename) {
2100
- const isMobile = window.innerWidth <= 768;
2101
- if (isMobile) return filename;
2102
- const maxLength = 30;
2103
- if ((filename || '').length <= maxLength) return filename;
2104
- const dotIndex = filename.lastIndexOf('.');
2105
- let name = filename;
2106
- let extension = '';
2107
- if (dotIndex !== -1) {
2108
- name = filename.substring(0, dotIndex);
2109
- extension = filename.substring(dotIndex);
2110
- }
2111
- const lastChars = 3;
2112
- const extensionLength = extension.length;
2113
- const ellipsis = '...';
2114
- const availableForName = maxLength - ellipsis.length - lastChars - extensionLength;
2115
- if (availableForName <= 0) return filename.substring(0, maxLength - ellipsis.length) + ellipsis;
2116
- const firstPart = name.substring(0, availableForName);
2117
- const lastPart = name.substring(name.length - lastChars);
2118
- return firstPart + ellipsis + lastPart + extension;
2119
- };
2120
-
2121
- // Debounce helper - returns a function that delays invocation until after wait ms
2122
- w.debounce = function (fn, wait) {
2123
- let timeout = null;
2124
- return function () {
2125
- const ctx = this;
2126
- const args = arguments;
2127
- clearTimeout(timeout);
2128
- timeout = setTimeout(function () {
2129
- fn.apply(ctx, args);
2130
- }, wait);
2131
- };
2132
- };
2133
-
2134
- })(window);
1206
+ // Initial load with URL params
1207
+ const initialFolder = urlParams.get('folder') || '';
1208
+ const initialPage = parseInt(urlParams.get('page') || '1', 10);
1209
+ const initialSearch = urlParams.get('search') || '';
1210
+
1211
+ loadFiles(initialPage, initialFolder, initialSearch);
1212
+ });
1213
+
1214
+ // Expose global methods if needed
1215
+ window.mbkbucket = {
1216
+ loadFiles,
1217
+ openPreview,
1218
+ downloadFile,
1219
+ showToast
1220
+ };
1221
+
1222
+ })();