mbkbucket 3.8.3 → 4.0.1

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.
Files changed (36) hide show
  1. package/.env.example +4 -3
  2. package/README.md +375 -167
  3. package/index.js +3 -1
  4. package/lib/cli.js +239 -239
  5. package/lib/public/bucketadmin.css +855 -521
  6. package/lib/public/bucketadmin.js +367 -31
  7. package/lib/public/mbkbucket-helper-demo.html +4 -24
  8. package/lib/public/mbkbucket-helper.css +28 -34
  9. package/lib/public/mbkbucket-helper.js +4 -3
  10. package/lib/src/config/validation.js +18 -5
  11. package/lib/src/controllers/api.controller.js +66 -17
  12. package/lib/src/controllers/dashboard.controller.js +30 -10
  13. package/lib/src/controllers/info.controller.js +34 -3
  14. package/lib/src/controllers/view.controller.js +16 -14
  15. package/lib/src/core/storage/errors.js +62 -0
  16. package/lib/src/core/storage/gdrive-auth.js +193 -0
  17. package/lib/src/core/storage/gdrive-provider.js +656 -0
  18. package/lib/src/core/storage/index.js +14 -0
  19. package/lib/src/core/storage/models.js +80 -0
  20. package/lib/src/core/storage/path-strategy.js +65 -0
  21. package/lib/src/core/storage/s3-provider.js +491 -0
  22. package/lib/src/core/storage/storage-manager.js +203 -0
  23. package/lib/src/core/storage/storage-provider.js +217 -0
  24. package/lib/src/middleware/bucket-resolver.js +6 -6
  25. package/lib/src/routes/api.routes.js +5 -1
  26. package/lib/src/routes/index.js +2 -2
  27. package/lib/src/routes/view.routes.js +5 -2
  28. package/lib/src/services/s3.service.js +16 -568
  29. package/lib/src/services/storage.service.js +213 -0
  30. package/lib/src/utils/errors.js +15 -5
  31. package/package.json +2 -3
  32. package/views/bucket.hbs +47 -71
  33. package/views/bucketadmin_move_modal.hbs +35 -0
  34. package/views/bucketadmin_preview_modal.hbs +81 -13
  35. package/views/bucketadmincontent.hbs +56 -7
  36. package/views/mbkbucket_info.hbs +109 -207
@@ -20,7 +20,7 @@
20
20
  let currentOrder = 'asc';
21
21
  let currentViewMode = localStorage.getItem('mbk_view_mode') || 'grid'; // 'grid' or 'list'
22
22
  let currentQuickFilter = 'all'; // 'all', 'media', 'docs', 'code', 'starred'
23
- let showThumbnails = localStorage.getItem('mbk_show_thumbnails') === 'true'; // Fast Mode by default (saves Vercel resources & tokens)
23
+ let showThumbnails = localStorage.getItem('mbk_show_thumbnails') === 'true'; // Fast Mode by default
24
24
  let thumbObserver = null;
25
25
  let pageFiles = [];
26
26
  let pageFolders = [];
@@ -28,6 +28,7 @@
28
28
  let currentNextToken = null;
29
29
  let selectedFiles = new Set();
30
30
  let starredFiles = new Set(JSON.parse(localStorage.getItem('mbk_starred_keys') || '[]'));
31
+ let currentPreviewKey = '';
31
32
 
32
33
  // Upload State
33
34
  let uploadQueue = [];
@@ -36,6 +37,7 @@
36
37
  // DOM Elements & Config
37
38
  const bucketAppData = document.getElementById('bucketAppData');
38
39
  const bucketAppName = bucketAppData?.dataset.appName || '';
40
+ const isPublicViewEnabled = bucketAppData?.dataset.publicViewEnabled === 'true';
39
41
  const urlParams = new URLSearchParams(window.location.search);
40
42
  const selectedBucket = urlParams.get('bucket') || '';
41
43
  let activeBucket = selectedBucket;
@@ -46,7 +48,7 @@
46
48
  // File categories & extensions
47
49
  const MEDIA_EXTS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'mp4', 'webm', 'ogg', 'avi', 'mov', 'mp3', 'wav', 'flac', 'aac', 'm4a']);
48
50
  const DOC_EXTS = new Set(['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'md', 'csv', 'rtf']);
49
- 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']);
51
+ 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', 'env']);
50
52
 
51
53
  const FILE_TYPES = {
52
54
  pdf: { color: 'var(--red)', bg: 'var(--red-lt)', label: 'PDF' },
@@ -85,6 +87,8 @@
85
87
  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>',
86
88
  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>',
87
89
  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>',
90
+ upload: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 11v3H2v-3"/><path d="M11 6L8 3 5 6"/><line x1="8" y1="3" x2="8" y2="11"/></svg>',
91
+ download: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 11v3H2v-3"/><path d="M5 8l3 3 3-3"/><line x1="8" y1="2" x2="8" y2="11"/></svg>',
88
92
  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>'
89
93
  };
90
94
 
@@ -121,7 +125,7 @@
121
125
  if (!dateStr) return '—';
122
126
  const d = new Date(dateStr);
123
127
  if (isNaN(d.getTime())) return String(dateStr);
124
- return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
128
+ return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' });
125
129
  }
126
130
 
127
131
  function withBucketUrl(url) {
@@ -134,6 +138,10 @@
134
138
  return withBucketUrl(`/mbkbucket/view/${encodeURIComponent(key)}`);
135
139
  }
136
140
 
141
+ function getPublicViewUrl(key) {
142
+ return withBucketUrl(`/mbkbucket/p_view/${encodeURIComponent(key)}`);
143
+ }
144
+
137
145
  function getDownloadUrl(key) {
138
146
  return withBucketUrl(`/mbkbucket/download/${encodeURIComponent(key)}`);
139
147
  }
@@ -158,6 +166,10 @@
158
166
  return CODE_EXTS.has(ext) || ['txt', 'md', 'csv', 'log', 'json', 'xml', 'env'].includes(ext);
159
167
  }
160
168
 
169
+ function isMarkdown(fileName) {
170
+ return getFileExt(fileName) === 'md';
171
+ }
172
+
161
173
  function isPdf(fileName) {
162
174
  return getFileExt(fileName) === 'pdf';
163
175
  }
@@ -208,6 +220,22 @@
208
220
  return Array.from(folderSet).sort();
209
221
  }
210
222
 
223
+ function renderMarkdownToHtml(mdText) {
224
+ let html = escapeHtml(mdText);
225
+ html = html.replace(/```([a-zA-Z0-9_+-]*)\n([\s\S]*?)```/g, (_m, _lang, code) => `<pre><code>${code}</code></pre>`);
226
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
227
+ html = html.replace(/^### (.*$)/gim, '<h3>$1</h3>');
228
+ html = html.replace(/^## (.*$)/gim, '<h2>$1</h2>');
229
+ html = html.replace(/^# (.*$)/gim, '<h1>$1</h1>');
230
+ html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
231
+ html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>');
232
+ html = html.replace(/^\> (.*$)/gim, '<blockquote>$1</blockquote>');
233
+ html = html.replace(/^\s*[\-\*] (.*$)/gim, '<li>$1</li>');
234
+ html = html.replace(/(<li>.*<\/li>)/gim, '<ul>$1</ul>');
235
+ html = html.replace(/\n\n/g, '<br><br>');
236
+ return `<div class="rendered-markdown">${html}</div>`;
237
+ }
238
+
211
239
  function syncStateUrl() {
212
240
  try {
213
241
  const parts = [];
@@ -255,6 +283,70 @@
255
283
  }, 4000);
256
284
  }
257
285
 
286
+ // Live Health Check
287
+ async function checkConnectionHealth(silent = true) {
288
+ const healthBadge = document.getElementById('topHealthBadge');
289
+ const healthText = document.getElementById('topHealthText');
290
+ const latencyTag = document.getElementById('storageLatency');
291
+
292
+ if (healthBadge) {
293
+ healthBadge.className = 'health-status-badge checking';
294
+ if (healthText) healthText.textContent = 'Checking...';
295
+ }
296
+
297
+ const startTime = Date.now();
298
+ try {
299
+ const response = await fetch(withBucketUrl('/mbkbucket/api/health'));
300
+ const data = await response.json();
301
+ const latency = data.responseTime || (Date.now() - startTime);
302
+
303
+ if (data.status === 'healthy' || data.success) {
304
+ if (healthBadge) {
305
+ healthBadge.className = 'health-status-badge healthy';
306
+ if (healthText) healthText.textContent = 'Healthy';
307
+ }
308
+ if (latencyTag) latencyTag.textContent = `${latency}ms`;
309
+ if (!silent) showToast(`Storage connection healthy (${latency}ms)`, 'success');
310
+ } else {
311
+ if (healthBadge) {
312
+ healthBadge.className = 'health-status-badge unhealthy';
313
+ if (healthText) healthText.textContent = 'Unhealthy';
314
+ }
315
+ if (latencyTag) latencyTag.textContent = 'err';
316
+ if (!silent) showToast(`Connection error: ${data.error || 'Check configuration'}`, 'error');
317
+ }
318
+ } catch (err) {
319
+ if (healthBadge) {
320
+ healthBadge.className = 'health-status-badge unhealthy';
321
+ if (healthText) healthText.textContent = 'Offline';
322
+ }
323
+ if (latencyTag) latencyTag.textContent = 'offline';
324
+ if (!silent) showToast(`Connection test failed: ${err.message}`, 'error');
325
+ }
326
+ }
327
+
328
+ // Batch Selection System
329
+ function updateBatchBar() {
330
+ const bar = document.getElementById('batchActionBar');
331
+ const countEl = document.getElementById('batchSelectedCount');
332
+ if (!bar) return;
333
+
334
+ const count = selectedFiles.size;
335
+ if (count > 0) {
336
+ bar.style.display = 'block';
337
+ if (countEl) countEl.textContent = count;
338
+ } else {
339
+ bar.style.display = 'none';
340
+ }
341
+ }
342
+
343
+ function clearBatchSelection() {
344
+ selectedFiles.clear();
345
+ document.querySelectorAll('.file-select-cb').forEach(cb => { cb.checked = false; });
346
+ document.querySelectorAll('.fm-item.is-selected').forEach(item => { item.classList.remove('is-selected'); });
347
+ updateBatchBar();
348
+ }
349
+
258
350
  // Tree Renderer
259
351
  function renderTree() {
260
352
  const treeEl = document.getElementById('fm-tree');
@@ -366,13 +458,11 @@
366
458
  function getFilteredFiles() {
367
459
  let list = (pageFiles || []).filter(f => !String(f.Key || '').endsWith('/'));
368
460
 
369
- // Search filter
370
461
  if (currentSearch) {
371
462
  const q = currentSearch.toLowerCase();
372
463
  list = list.filter(f => getBaseName(f.Key).toLowerCase().includes(q));
373
464
  }
374
465
 
375
- // Quick filter
376
466
  if (currentQuickFilter === 'media') {
377
467
  list = list.filter(f => MEDIA_EXTS.has(getFileExt(f.Key)));
378
468
  } else if (currentQuickFilter === 'docs') {
@@ -404,7 +494,6 @@
404
494
  const files = getFilteredFiles();
405
495
  const folders = getFilteredFolders();
406
496
 
407
- // Update stats
408
497
  const totalCountEl = document.getElementById('totalFilesCount');
409
498
  const filterInfoEl = document.getElementById('filterInfo');
410
499
  const totalItems = files.length + folders.length;
@@ -433,11 +522,10 @@
433
522
 
434
523
  let html = '';
435
524
 
436
- // If List view, add header row
437
525
  if (currentViewMode === 'list') {
438
526
  html += `
439
527
  <div class="fm-list-header">
440
- <div></div>
528
+ <div><input type="checkbox" id="selectAllCb" title="Select All"></div>
441
529
  <div></div>
442
530
  <div>Name</div>
443
531
  <div>Size</div>
@@ -499,6 +587,7 @@
499
587
  const safeKey = escapeAttr(rawKey);
500
588
  const safeName = escapeHtml(fileName);
501
589
  const isStarred = starredFiles.has(rawKey);
590
+ const isSelected = selectedFiles.has(rawKey);
502
591
  const isImg = isImage(fileName);
503
592
  const typeCfg = getFileTypeConfig(fileName);
504
593
  const formattedSize = file.Size ? formatFileSize(file.Size) : '—';
@@ -512,7 +601,10 @@
512
601
 
513
602
  if (currentViewMode === 'grid') {
514
603
  html += `
515
- <div class="fm-item ${isStarred ? 'starred' : ''}" data-type="file" data-key="${safeKey}" tabindex="0">
604
+ <div class="fm-item ${isStarred ? 'starred' : ''} ${isSelected ? 'is-selected' : ''}" data-type="file" data-key="${safeKey}" tabindex="0">
605
+ <div class="fm-item-checkbox-grid">
606
+ <input type="checkbox" class="file-select-cb" data-key="${safeKey}" ${isSelected ? 'checked' : ''} title="Select">
607
+ </div>
516
608
  <div class="fm-item-icon" style="${showImgThumb ? '' : `background:${typeCfg.bg};color:${typeCfg.color}`}">
517
609
  ${iconInner}
518
610
  </div>
@@ -530,9 +622,9 @@
530
622
  `;
531
623
  } else {
532
624
  html += `
533
- <div class="fm-item ${isStarred ? 'starred' : ''}" data-type="file" data-key="${safeKey}" tabindex="0">
625
+ <div class="fm-item ${isStarred ? 'starred' : ''} ${isSelected ? 'is-selected' : ''}" data-type="file" data-key="${safeKey}" tabindex="0">
534
626
  <div class="fm-item-checkbox">
535
- <input type="checkbox" class="file-select-cb" data-key="${safeKey}">
627
+ <input type="checkbox" class="file-select-cb" data-key="${safeKey}" ${isSelected ? 'checked' : ''} title="Select">
536
628
  </div>
537
629
  <div class="fm-item-icon" style="${isImg ? '' : `background:${typeCfg.bg};color:${typeCfg.color}`}">
538
630
  <span class="ext">${typeCfg.label}</span>
@@ -553,6 +645,7 @@
553
645
 
554
646
  gridEl.innerHTML = html;
555
647
  observeThumbnails();
648
+ updateBatchBar();
556
649
  }
557
650
 
558
651
  function observeThumbnails() {
@@ -646,6 +739,7 @@
646
739
  renderBreadcrumbs();
647
740
  renderFiles();
648
741
  renderPagination(data);
742
+ checkConnectionHealth(true);
649
743
  } catch (err) {
650
744
  console.error('Error loading files:', err);
651
745
  showToast(err.message || 'Error loading files', 'error');
@@ -717,8 +811,8 @@
717
811
 
718
812
  menu.style.display = 'block';
719
813
 
720
- const menuWidth = 180;
721
- const menuHeight = menu.offsetHeight || 150;
814
+ const menuWidth = 190;
815
+ const menuHeight = menu.offsetHeight || 160;
722
816
  const posX = Math.min(x, window.innerWidth - menuWidth - 10);
723
817
  const posY = Math.min(y, window.innerHeight - menuHeight - 10);
724
818
 
@@ -743,7 +837,7 @@
743
837
  if (menu) menu.style.display = 'none';
744
838
  }
745
839
 
746
- // Actions: Star, Download, Preview, Delete, New Folder
840
+ // Actions: Star, Download, Preview, Delete, New Folder, Move
747
841
  function toggleStar(key) {
748
842
  if (starredFiles.has(key)) {
749
843
  starredFiles.delete(key);
@@ -787,8 +881,18 @@
787
881
  });
788
882
  }
789
883
 
884
+ function copyPublicLink(key) {
885
+ const publicUrl = window.location.origin + getPublicViewUrl(key);
886
+ navigator.clipboard.writeText(publicUrl).then(() => {
887
+ showToast('Public link copied to clipboard', 'success');
888
+ }).catch(() => {
889
+ showToast('Failed to copy public link', 'error');
890
+ });
891
+ }
892
+
790
893
  // Preview Modal
791
- function openPreview(key) {
894
+ async function openPreview(key) {
895
+ currentPreviewKey = key;
792
896
  const modal = document.getElementById('previewModal');
793
897
  const titleEl = document.getElementById('previewFileName');
794
898
  const contentEl = document.getElementById('previewContent');
@@ -797,6 +901,13 @@
797
901
 
798
902
  if (!modal || !contentEl) return;
799
903
 
904
+ // Reset tabs
905
+ document.querySelectorAll('.preview-tab').forEach(t => t.classList.toggle('active', t.dataset.previewTab === 'preview'));
906
+ const panePreview = document.getElementById('pane-preview');
907
+ const paneInfo = document.getElementById('pane-info');
908
+ if (panePreview) panePreview.style.display = 'block';
909
+ if (paneInfo) paneInfo.style.display = 'none';
910
+
800
911
  const fileName = getBaseName(key);
801
912
  const viewUrl = getViewUrl(key);
802
913
  const downloadUrl = getDownloadUrl(key);
@@ -808,6 +919,42 @@
808
919
  contentEl.innerHTML = '<div class="empty-preview"><div class="spinner-border text-light"></div><div class="mt-2 text-light">Loading preview...</div></div>';
809
920
  modal.classList.add('show');
810
921
 
922
+ // Populate file info tab
923
+ const fileInfoKey = document.getElementById('infoFileKey');
924
+ const fileInfoSize = document.getElementById('infoFileSize');
925
+ const fileInfoMime = document.getElementById('infoMimeType');
926
+ const fileInfoDate = document.getElementById('infoLastModified');
927
+ const fileInfoETag = document.getElementById('infoETag');
928
+
929
+ if (fileInfoKey) fileInfoKey.textContent = key;
930
+
931
+ // Async fetch full metadata
932
+ fetch(withBucketUrl(`/mbkbucket/api/file-info/${encodeURIComponent(key)}`))
933
+ .then(r => r.json())
934
+ .then(info => {
935
+ if (fileInfoSize) fileInfoSize.textContent = info.ContentLength ? formatFileSize(info.ContentLength) : '—';
936
+ if (fileInfoMime) fileInfoMime.textContent = info.ContentType || '—';
937
+ if (fileInfoDate) fileInfoDate.textContent = info.LastModified ? formatDate(info.LastModified) : '—';
938
+ if (fileInfoETag) fileInfoETag.textContent = info.ETag || '—';
939
+ }).catch(() => {});
940
+
941
+ // Configure public link button and metadata card
942
+ const copyPublicBtn = document.getElementById('previewCopyPublicLinkBtn');
943
+ if (copyPublicBtn) {
944
+ copyPublicBtn.style.display = isPublicViewEnabled ? '' : 'none';
945
+ }
946
+
947
+ const publicUrl = window.location.origin + getPublicViewUrl(key);
948
+ const infoPublicLinkCard = document.getElementById('infoPublicLinkCard');
949
+ const infoPublicUrl = document.getElementById('infoPublicUrl');
950
+ if (infoPublicLinkCard) {
951
+ infoPublicLinkCard.style.display = isPublicViewEnabled ? '' : 'none';
952
+ }
953
+ if (infoPublicUrl) {
954
+ infoPublicUrl.textContent = publicUrl;
955
+ }
956
+
957
+ // Render Preview Content
811
958
  if (isImage(fileName)) {
812
959
  contentEl.innerHTML = `<img src="${viewUrl}" alt="${escapeAttr(fileName)}">`;
813
960
  } else if (isVideo(fileName)) {
@@ -816,6 +963,12 @@
816
963
  contentEl.innerHTML = `<div class="p-4 text-center"><audio controls autoplay class="w-100"><source src="${viewUrl}"></audio></div>`;
817
964
  } else if (isPdf(fileName)) {
818
965
  contentEl.innerHTML = `<iframe src="${viewUrl}" class="preview-iframe"></iframe>`;
966
+ } else if (isMarkdown(fileName)) {
967
+ fetch(viewUrl).then(res => res.text()).then(text => {
968
+ contentEl.innerHTML = renderMarkdownToHtml(text);
969
+ }).catch(err => {
970
+ contentEl.innerHTML = `<div class="p-4 text-danger text-center">Failed to load preview: ${escapeHtml(err.message)}</div>`;
971
+ });
819
972
  } else if (isTextOrCode(fileName)) {
820
973
  fetch(viewUrl).then(res => res.text()).then(text => {
821
974
  contentEl.innerHTML = `<pre class="preview-code-block"><code>${escapeHtml(text)}</code></pre>`;
@@ -837,6 +990,70 @@
837
990
  if (modal) modal.classList.remove('show');
838
991
  };
839
992
 
993
+ window.copyCurrentPreviewLink = function () {
994
+ if (currentPreviewKey) copyShareLink(currentPreviewKey);
995
+ };
996
+
997
+ window.copyCurrentPreviewPublicLink = function () {
998
+ if (currentPreviewKey) copyPublicLink(currentPreviewKey);
999
+ };
1000
+
1001
+ // Move & Rename Modal
1002
+ function promptMove(key, isCopy = false) {
1003
+ const modal = document.getElementById('moveModal');
1004
+ const title = document.getElementById('moveModalTitle');
1005
+ const preview = document.getElementById('moveSourcePathPreview');
1006
+ const srcInput = document.getElementById('moveSourceKeyInput');
1007
+ const destInput = document.getElementById('moveDestNameInput');
1008
+ const copyInput = document.getElementById('moveIsCopyInput');
1009
+ const label = document.getElementById('moveDestNameLabel');
1010
+
1011
+ if (!modal) return;
1012
+
1013
+ if (title) title.textContent = isCopy ? 'Copy File' : 'Move or Rename File';
1014
+ if (preview) preview.textContent = key;
1015
+ if (srcInput) srcInput.value = key;
1016
+ if (copyInput) copyInput.value = isCopy ? 'true' : 'false';
1017
+ if (destInput) destInput.value = key;
1018
+ if (label) label.textContent = isCopy ? 'Destination Key / Name' : 'New Key / Name';
1019
+
1020
+ modal.classList.add('show');
1021
+ setTimeout(() => destInput?.focus(), 50);
1022
+ }
1023
+
1024
+ window.closeMoveModal = function () {
1025
+ const modal = document.getElementById('moveModal');
1026
+ if (modal) modal.classList.remove('show');
1027
+ };
1028
+
1029
+ window.submitMoveItem = async function () {
1030
+ const srcKey = document.getElementById('moveSourceKeyInput')?.value;
1031
+ const destKey = document.getElementById('moveDestNameInput')?.value?.trim();
1032
+ const isCopy = document.getElementById('moveIsCopyInput')?.value === 'true';
1033
+
1034
+ if (!srcKey || !destKey) {
1035
+ showToast('Source and destination paths are required', 'warning');
1036
+ return;
1037
+ }
1038
+
1039
+ const endpoint = isCopy ? '/mbkbucket/copy' : '/mbkbucket/move';
1040
+ try {
1041
+ const response = await fetch(withBucketUrl(endpoint), {
1042
+ method: 'POST',
1043
+ headers: { 'Content-Type': 'application/json' },
1044
+ body: JSON.stringify({ sourceKey: srcKey, destKey })
1045
+ });
1046
+ const data = await response.json();
1047
+ if (!data.success) throw new Error(data.error || `${isCopy ? 'Copy' : 'Move'} failed`);
1048
+
1049
+ window.closeMoveModal();
1050
+ showToast(`${isCopy ? 'Copied' : 'Moved'} to ${destKey}`, 'success');
1051
+ loadFiles(currentPage, currentPrefix);
1052
+ } catch (err) {
1053
+ showToast(err.message, 'error');
1054
+ }
1055
+ };
1056
+
840
1057
  // Delete Modal
841
1058
  function promptDelete(key, isFolder = false) {
842
1059
  const modal = document.getElementById('deleteModal');
@@ -989,6 +1206,35 @@
989
1206
 
990
1207
  // Event Listeners Setup
991
1208
  function initEventListeners() {
1209
+ // Refresh button
1210
+ document.getElementById('refresh-btn')?.addEventListener('click', () => {
1211
+ loadFiles(currentPage, currentPrefix, currentSearch);
1212
+ checkConnectionHealth(false);
1213
+ });
1214
+
1215
+ // Health check button
1216
+ document.getElementById('checkHealthBtn')?.addEventListener('click', () => {
1217
+ checkConnectionHealth(false);
1218
+ });
1219
+
1220
+ // Preview Tabs switcher
1221
+ document.querySelector('.preview-tabs')?.addEventListener('click', (e) => {
1222
+ const btn = e.target.closest('.preview-tab');
1223
+ if (!btn) return;
1224
+ document.querySelectorAll('.preview-tab').forEach(b => b.classList.remove('active'));
1225
+ btn.classList.add('active');
1226
+ const target = btn.dataset.previewTab;
1227
+ const pPane = document.getElementById('pane-preview');
1228
+ const iPane = document.getElementById('pane-info');
1229
+ if (target === 'preview') {
1230
+ if (pPane) pPane.style.display = 'block';
1231
+ if (iPane) iPane.style.display = 'none';
1232
+ } else {
1233
+ if (pPane) pPane.style.display = 'none';
1234
+ if (iPane) iPane.style.display = 'block';
1235
+ }
1236
+ });
1237
+
992
1238
  // Tree clicks
993
1239
  document.getElementById('fm-tree')?.addEventListener('click', (e) => {
994
1240
  const link = e.target.closest('.tree-link');
@@ -1092,8 +1338,40 @@
1092
1338
  renderFiles();
1093
1339
  });
1094
1340
 
1095
- // Grid Item Interactions (Click, Double Click, Star, Context Menu)
1341
+ // Grid Item Interactions (Click, Double Click, Star, Checkbox, Context Menu)
1096
1342
  document.getElementById('fm-grid')?.addEventListener('click', (e) => {
1343
+ // Checkbox selection
1344
+ const cb = e.target.closest('.file-select-cb');
1345
+ if (cb) {
1346
+ e.stopPropagation();
1347
+ const key = cb.dataset.key;
1348
+ if (cb.checked) {
1349
+ selectedFiles.add(key);
1350
+ } else {
1351
+ selectedFiles.delete(key);
1352
+ }
1353
+ const parentItem = cb.closest('.fm-item');
1354
+ if (parentItem) parentItem.classList.toggle('is-selected', cb.checked);
1355
+ updateBatchBar();
1356
+ return;
1357
+ }
1358
+
1359
+ // Select All in list header
1360
+ const selectAll = e.target.closest('#selectAllCb');
1361
+ if (selectAll) {
1362
+ e.stopPropagation();
1363
+ const checked = selectAll.checked;
1364
+ const visibleFiles = getFilteredFiles();
1365
+ visibleFiles.forEach(f => {
1366
+ if (checked) selectedFiles.add(f.Key);
1367
+ else selectedFiles.delete(f.Key);
1368
+ });
1369
+ document.querySelectorAll('.file-select-cb').forEach(c => { c.checked = checked; });
1370
+ document.querySelectorAll('.fm-item[data-type="file"]').forEach(item => item.classList.toggle('is-selected', checked));
1371
+ updateBatchBar();
1372
+ return;
1373
+ }
1374
+
1097
1375
  // Star click
1098
1376
  const starBtn = e.target.closest('[data-star-key]');
1099
1377
  if (starBtn) {
@@ -1112,19 +1390,35 @@
1112
1390
 
1113
1391
  if (fileKey) {
1114
1392
  const isStarred = starredFiles.has(fileKey);
1115
- openContextMenu(rect.left, rect.bottom + 4, [
1116
- { id: 'view', label: 'Preview', icon: '<i class="fas fa-eye"></i>', action: () => openPreview(fileKey) },
1117
- { id: 'download', label: 'Download', icon: '<i class="fas fa-download"></i>', action: () => downloadFile(fileKey) },
1118
- { id: 'share', label: 'Copy link', icon: '<i class="fas fa-link"></i>', action: () => copyShareLink(fileKey) },
1119
- { id: 'star', label: isStarred ? 'Unstar' : 'Star', icon: '<i class="fas fa-star"></i>', action: () => toggleStar(fileKey) },
1393
+ const menuItems = [
1394
+ { id: 'view', label: 'Preview & Info', icon: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1 8s3-5 7-5 7 5 7 5-3 5-7 5-7-5-7-5z"/><circle cx="8" cy="8" r="2.5"/></svg>', action: () => openPreview(fileKey) },
1395
+ { id: 'download', label: 'Download', icon: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 11v3H2v-3"/><path d="M5 8l3 3 3-3"/><line x1="8" y1="2" x2="8" y2="11"/></svg>', action: () => downloadFile(fileKey) },
1396
+ { id: 'share', label: 'Copy Link', icon: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M5 2h8a1 1 0 011 1v8M3 5h8a1 1 0 011 1v8a1 1 0 01-1 1H3a1 1 0 01-1-1V6a1 1 0 011-1z"/></svg>', action: () => copyShareLink(fileKey) },
1397
+ ];
1398
+
1399
+ if (isPublicViewEnabled) {
1400
+ menuItems.push({
1401
+ id: 'public-share',
1402
+ label: 'Copy Public Link',
1403
+ icon: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="6.25"/><path d="M1.75 8h12.5M8 1.75a10.5 10.5 0 010 12.5 10.5 10.5 0 010-12.5z"/></svg>',
1404
+ action: () => copyPublicLink(fileKey)
1405
+ });
1406
+ }
1407
+
1408
+ menuItems.push(
1409
+ { id: 'move', label: 'Rename / Move', icon: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M11 2L14 5L11 8M14 5H5C3.34315 5 2 6.34315 2 8V14"/></svg>', action: () => promptMove(fileKey, false) },
1410
+ { id: 'copy', label: 'Duplicate / Copy', icon: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="5" y="5" width="9" height="9" rx="1.5"/><path d="M3 11V3a1.5 1.5 0 011.5-1.5h8"/></svg>', action: () => promptMove(fileKey, true) },
1411
+ { id: 'star', label: isStarred ? 'Unstar' : 'Star', icon: '<svg width="13" height="13" 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>', action: () => toggleStar(fileKey) },
1120
1412
  '-',
1121
- { id: 'delete', label: 'Delete', icon: '<i class="fas fa-trash"></i>', danger: true, action: () => promptDelete(fileKey, false) }
1122
- ]);
1413
+ { id: 'delete', label: 'Delete', icon: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 4h10M6 4V2h4v2M5 4l1 9h4l1-9"/></svg>', danger: true, action: () => promptDelete(fileKey, false) }
1414
+ );
1415
+
1416
+ openContextMenu(rect.left, rect.bottom + 4, menuItems);
1123
1417
  } else if (folderPath) {
1124
1418
  openContextMenu(rect.left, rect.bottom + 4, [
1125
- { id: 'open', label: 'Open Folder', icon: '<i class="fas fa-folder-open"></i>', action: () => loadFiles(1, folderPath) },
1419
+ { id: 'open', label: 'Open Folder', icon: '<svg width="13" height="13" 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>', action: () => loadFiles(1, folderPath) },
1126
1420
  '-',
1127
- { id: 'delete', label: 'Delete Folder', icon: '<i class="fas fa-trash"></i>', danger: true, action: () => promptDelete(folderPath + '/', true) }
1421
+ { id: 'delete', label: 'Delete Folder', icon: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 4h10M6 4V2h4v2M5 4l1 9h4l1-9"/></svg>', danger: true, action: () => promptDelete(folderPath + '/', true) }
1128
1422
  ]);
1129
1423
  }
1130
1424
  return;
@@ -1140,17 +1434,53 @@
1140
1434
 
1141
1435
  // File click to preview
1142
1436
  const fileItem = e.target.closest('[data-type="file"]');
1143
- if (fileItem && !e.target.closest('.file-select-cb')) {
1437
+ if (fileItem && !e.target.closest('.file-select-cb') && !e.target.closest('.fm-star')) {
1144
1438
  e.stopPropagation();
1145
1439
  openPreview(fileItem.dataset.key);
1146
1440
  }
1147
1441
  });
1148
1442
 
1443
+ // Batch Bar Actions
1444
+ document.getElementById('batchClearBtn')?.addEventListener('click', clearBatchSelection);
1445
+
1446
+ document.getElementById('batchDownloadBtn')?.addEventListener('click', async () => {
1447
+ const keys = Array.from(selectedFiles);
1448
+ if (!keys.length) return;
1449
+ showToast(`Starting download for ${keys.length} item(s)...`);
1450
+ for (const key of keys) {
1451
+ await downloadFile(key);
1452
+ }
1453
+ });
1454
+
1455
+ document.getElementById('batchDeleteBtn')?.addEventListener('click', () => {
1456
+ const keys = Array.from(selectedFiles);
1457
+ if (!keys.length) return;
1458
+ promptDelete(`${keys.length} selected files`, false);
1459
+ });
1460
+
1461
+ document.getElementById('batchMoveBtn')?.addEventListener('click', () => {
1462
+ const keys = Array.from(selectedFiles);
1463
+ if (!keys.length) return;
1464
+ promptMove(keys[0], false);
1465
+ });
1466
+
1149
1467
  // Bucket Selector
1150
1468
  document.getElementById('bucketSelector')?.addEventListener('change', (e) => {
1469
+ const opt = e.target.selectedOptions?.[0];
1151
1470
  const newBucket = e.target.value;
1152
1471
  if (!newBucket) return;
1153
1472
  activeBucket = newBucket;
1473
+
1474
+ // Update provider badge
1475
+ const providerType = opt?.dataset?.type || 's3';
1476
+ const badge = document.getElementById('topProviderBadge');
1477
+ if (badge) {
1478
+ badge.className = `provider-badge provider-${providerType}`;
1479
+ const lbl = badge.querySelector('.provider-label');
1480
+ if (lbl) lbl.textContent = providerType;
1481
+ }
1482
+
1483
+ clearBatchSelection();
1154
1484
  loadFiles(1, '');
1155
1485
  });
1156
1486
 
@@ -1236,17 +1566,22 @@
1236
1566
  const key = formData.get('key');
1237
1567
  const isFolder = formData.get('folder') === 'true';
1238
1568
 
1569
+ const payload = selectedFiles.size > 0 && key?.includes('selected')
1570
+ ? { keys: Array.from(selectedFiles), prefix: currentPrefix }
1571
+ : { key, isFolder, prefix: currentPrefix };
1572
+
1239
1573
  try {
1240
1574
  const response = await fetch(withBucketUrl('/mbkbucket/delete'), {
1241
1575
  method: 'POST',
1242
1576
  headers: { 'Content-Type': 'application/json' },
1243
- body: JSON.stringify({ key, isFolder, prefix: currentPrefix })
1577
+ body: JSON.stringify(payload)
1244
1578
  });
1245
1579
  const data = await response.json();
1246
1580
  if (!data.success) throw new Error(data.error || 'Delete failed');
1247
1581
 
1248
1582
  closeModal();
1249
- showToast(`Successfully deleted ${key}`, 'success');
1583
+ clearBatchSelection();
1584
+ showToast('Deleted successfully', 'success');
1250
1585
  loadFiles(currentPage, currentPrefix);
1251
1586
  } catch (err) {
1252
1587
  showToast(err.message, 'error');
@@ -1256,7 +1591,6 @@
1256
1591
 
1257
1592
  // Initialization
1258
1593
  document.addEventListener('DOMContentLoaded', () => {
1259
- // Sync view mode buttons
1260
1594
  if (currentViewMode === 'list') {
1261
1595
  document.getElementById('view-list')?.classList.add('active');
1262
1596
  document.getElementById('view-grid')?.classList.remove('active');
@@ -1267,19 +1601,21 @@
1267
1601
 
1268
1602
  initEventListeners();
1269
1603
 
1270
- // Initial load with URL params
1271
1604
  const initialFolder = urlParams.get('folder') || '';
1272
1605
  const initialPage = parseInt(urlParams.get('page') || '1', 10);
1273
1606
  const initialSearch = urlParams.get('search') || '';
1274
1607
 
1275
1608
  loadFiles(initialPage, initialFolder, initialSearch);
1609
+ checkConnectionHealth(true);
1276
1610
  });
1277
1611
 
1278
- // Expose global methods if needed
1612
+ // Expose global methods
1279
1613
  window.mbkbucket = {
1280
1614
  loadFiles,
1281
1615
  openPreview,
1282
1616
  downloadFile,
1617
+ promptMove,
1618
+ checkConnectionHealth,
1283
1619
  showToast
1284
1620
  };
1285
1621