From 744d2c88fb3586304a3fc026e3a1b97486d7e794 Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle Date: Tue, 31 Mar 2026 18:35:34 +0200 Subject: [PATCH 1/5] refactor: simplify multiSelect module (remove duplicates, etc) - remove duplicate code - use one uniq selection bar (for both list & grid view) --- static/css/components/multiSelect.css | 6 +- static/index.html | 3 + static/js/app/main.js | 2 +- static/js/features/files/multiSelect.js | 171 +++++++----------------- 4 files changed, 57 insertions(+), 125 deletions(-) diff --git a/static/css/components/multiSelect.css b/static/css/components/multiSelect.css index 1b39ce1c..0416753c 100644 --- a/static/css/components/multiSelect.css +++ b/static/css/components/multiSelect.css @@ -34,7 +34,7 @@ min-width: 0; } -.batch-action-bar { +.batch-selection-bar { display: flex; align-items: center; justify-content: space-between; @@ -51,7 +51,7 @@ pointer-events: none; } -.batch-action-bar.visible { +.batch-selection-bar.visible { opacity: 1; max-height: 60px; transform: translateY(0); @@ -140,7 +140,7 @@ accent-color: #ff5e3a; } -[data-theme="dark"] .batch-action-bar { +[data-theme="dark"] .batch-selection-bar { background-color: #1e293b; border-color: #334155; color: #e2e8f0; diff --git a/static/index.html b/static/index.html index a9fdb626..a7e957c4 100644 --- a/static/index.html +++ b/static/index.html @@ -255,6 +255,8 @@ Home +
+
@@ -332,5 +334,6 @@ + diff --git a/static/js/app/main.js b/static/js/app/main.js index 55cef862..196575c9 100644 --- a/static/js/app/main.js +++ b/static/js/app/main.js @@ -653,7 +653,7 @@ function setupEventListeners() { // selection state; this handler only covers the legacy CSS class removal. // Skip if a rubber-band selection just finished — the click is a side-effect. if (window.__rubberBandJustFinished) return; - if (!e.target.closest('.file-card') && !e.target.closest('.file-item') && !e.target.closest('.context-menu') && !e.target.closest('.about-modal') && !e.target.closest('.batch-action-bar') && !e.target.closest('.list-header.selection-mode')) { + if (!e.target.closest('.file-card') && !e.target.closest('.file-item') && !e.target.closest('.context-menu') && !e.target.closest('.about-modal') && !e.target.closest('.batch-selection-bar') && !e.target.closest('.list-header.selection-mode')) { document.querySelectorAll('.file-card.selected').forEach(c => c.classList.remove('selected')); document.querySelectorAll('.file-item.selected').forEach(c => c.classList.remove('selected')); } diff --git a/static/js/features/files/multiSelect.js b/static/js/features/files/multiSelect.js index c2c14c80..cca75273 100644 --- a/static/js/features/files/multiSelect.js +++ b/static/js/features/files/multiSelect.js @@ -6,6 +6,9 @@ * provides batch delete / move / download / favorites operations. */ +// TODO: rename into selection-bar ? +// TODO: merge with photo part + const multiSelect = { /** Currently selected items: Map */ _selected: new Map(), @@ -16,9 +19,6 @@ const multiSelect = { /** Whether the selection bar is currently visible */ _barVisible: false, - /** Saved original list-header HTML so we can restore it */ - _savedHeaderHTML: '', - // ── Public API ────────────────────────────────────────── get count() { return this._selected.size; }, @@ -93,11 +93,14 @@ const multiSelect = { }, _getAllVisibleItems() { + return [...document.querySelectorAll('.file-item, .file-card')]; + /* const grid = document.getElementById('files-grid'); if (grid && grid.style.display !== 'none') { return [...grid.querySelectorAll('.file-card')]; } return [...document.querySelectorAll('#files-list-view .file-item')]; + */ }, _extractInfo(el) { @@ -151,146 +154,61 @@ const multiSelect = { * Build the inner HTML for the selection bar that replaces the * normal list-header columns (Name / Type / Size / Modified). */ - _buildSelectionBarHTML(n) { - const countText = n === 1 - ? (this._t('batch.one_selected') || '1 item selected') - : (this._t('batch.n_selected', { count: n }) || `${n} items selected`); - - const favLabel = this._t('batch.add_favorites') || 'Add to favorites'; - const moveLabel = this._t('batch.move_copy') || 'Move or copy'; - const dlLabel = this._t('actions.download') || 'Download'; - const delLabel = this._t('actions.delete') || 'Delete'; + _buildSelectionBarHTML() { + //FIXME: should support i18n lang change return ` -
- +
+ +
- ${countText}
- - - -
`; }, - /** Ensure the grid-view batch bar exists (shown only when grid is visible) */ - _ensureGridBar() { - if (document.getElementById('batch-grid-bar')) return; - const bar = document.createElement('div'); - bar.id = 'batch-grid-bar'; - bar.className = 'batch-action-bar'; // reuse same styles - const container = document.querySelector('.files-container'); - if (container) { - container.insertBefore(bar, container.firstChild); - } - }, - /** Main UI sync — called after every selection change */ _syncUI() { - const listHeader = document.querySelector('.list-header'); const n = this._selected.size; - // ── Save original header HTML on first use ── - if (listHeader && !this._savedHeaderHTML) { - this._savedHeaderHTML = listHeader.innerHTML; - } + const batchSelectionBar = document.getElementById('batch-selection-bar'); if (n > 0) { this._barVisible = true; - // ── List view: replace header with selection bar ── - if (listHeader) { - listHeader.classList.add('selection-mode'); - listHeader.innerHTML = this._buildSelectionBarHTML(n); + const countText = n === 1 + ? (this._t('batch.one_selected') || '1 item selected') + : (this._t('batch.n_selected', { count: n }) || `${n} items selected`); + document.getElementById("batch-bar-count").innerText = countText; - // Wire checkbox - const cb = document.getElementById('select-all-checkbox'); - if (cb) cb.addEventListener('change', () => this.toggleAll()); - - // Wire action buttons - this._wireBarButtons(); - } - - // ── Grid view: show floating bar ── - this._ensureGridBar(); - const gridBar = document.getElementById('batch-grid-bar'); - if (gridBar) { - const grid = document.getElementById('files-grid'); - const gridVisible = grid && grid.style.display !== 'none'; - if (gridVisible) { - gridBar.classList.add('visible'); - gridBar.innerHTML = ` -
- - ${ - n === 1 - ? (this._t('batch.one_selected') || '1 item selected') - : (this._t('batch.n_selected', { count: n }) || `${n} items selected`) - } -
-
- - - - -
- `; - const closeBtn = document.getElementById('batch-grid-close'); - if (closeBtn) closeBtn.addEventListener('click', () => this.clear()); - this._wireBarButtons(); - } else { - gridBar.classList.remove('visible'); - } - } + batchSelectionBar.classList.add('visible'); + + } else { this._barVisible = false; - // Restore original list header - if (listHeader) { - listHeader.classList.remove('selection-mode'); - if (this._savedHeaderHTML) { - listHeader.innerHTML = this._savedHeaderHTML; - } - // Re-wire the select-all checkbox - const cb = document.getElementById('select-all-checkbox'); - if (cb) cb.addEventListener('change', () => this.toggleAll()); - // Translate restored header (scoped to list header) - if (window.i18n && window.i18n.translateElement) window.i18n.translateElement(listHeader); - } - // Hide grid bar - const gridBar = document.getElementById('batch-grid-bar'); - if (gridBar) gridBar.classList.remove('visible'); + batchSelectionBar.classList.remove('visible'); } // Sync individual item checkboxes @@ -301,14 +219,16 @@ const multiSelect = { /** Wire click handlers on batch action buttons (idempotent per render) */ _wireBarButtons() { - const del = document.getElementById('batch-delete'); - const move = document.getElementById('batch-move'); - const dl = document.getElementById('batch-download'); - const fav = document.getElementById('batch-fav'); - if (del) del.onclick = () => this.batchDelete(); - if (move) move.onclick = () => this.batchMove(); - if (dl) dl.onclick = () => this.batchDownload(); - if (fav) fav.onclick = () => this.batchFavorites(); + const del = document.getElementById('batch-delete'); + const move = document.getElementById('batch-move'); + const dl = document.getElementById('batch-download'); + const fav = document.getElementById('batch-fav'); + const closeBtn = document.getElementById('batch-grid-close'); + if (del) del.onclick = () => this.batchDelete(); + if (move) move.onclick = () => this.batchMove(); + if (dl) dl.onclick = () => this.batchDownload(); + if (fav) fav.onclick = () => this.batchFavorites(); + if (closeBtn) closeBtn.onclick = () => this.clear(); }, _syncItemCheckboxes() { @@ -522,6 +442,15 @@ const multiSelect = { if (e.key === 'Escape' && this.hasSelection) this.clear(); if (e.key === 'Delete' && this.hasSelection) this.batchDelete(); }); + + const batchSelectionBar = document.getElementById('batch-selection-bar'); + batchSelectionBar.innerHTML = this._buildSelectionBarHTML(); + + if (window.i18n && window.i18n.translateElement) { + window.i18n.translateElement(batchSelectionBar); + } + this._wireBarButtons(); + }, _injectListHeaderCheckbox() { @@ -533,7 +462,7 @@ const multiSelect = { _hookGlobalDeselect() { document.addEventListener('click', (e) => { if (window.__rubberBandJustFinished) return; - if (e.target.closest('.file-card, .file-item, .context-menu, .batch-action-bar, .list-header.selection-mode, .about-modal, .rename-dialog, .share-dialog, .confirm-dialog, .modal-overlay, input, button')) return; + if (e.target.closest('.file-card, .file-item, .context-menu, .batch-selection-bar, .list-header.selection-mode, .about-modal, .rename-dialog, .share-dialog, .confirm-dialog, .modal-overlay, input, button')) return; if (this.hasSelection) this.clear(); }); } From 8d2a45c0ffe8ac56bcf6830b4031a6e81a9dd1d0 Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle Date: Wed, 1 Apr 2026 23:59:23 +0200 Subject: [PATCH 2/5] feat(navigation): no change/blink section if already selected --- static/js/app/navigation.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/static/js/app/navigation.js b/static/js/app/navigation.js index 0121d037..9522f8c3 100644 --- a/static/js/app/navigation.js +++ b/static/js/app/navigation.js @@ -118,8 +118,12 @@ function getSectionFromNavItem(navItem) { /** * Set the current active section, updating all view flags and nav UI. * @param {string} section - The section to activate ('files', 'shared', 'recent', 'favorites', 'trash') + * @returns {boolean} true if the section changed */ function setCurrentSection(section) { + + if (window.app.currentSection == section) return false; + // Set all view flags - true for active section, false for others Object.entries(VIEW_FLAGS).forEach(([key, flag]) => { window.app[flag] = (key === section); @@ -148,10 +152,13 @@ function setCurrentSection(section) { if (section !== 'photos' && window.photosView) { window.photosView.hide(); } + + return true; } function switchToSharedView() { - setCurrentSection('shared'); + + if (!setCurrentSection('shared')) return; // Hide breadcrumb (only shown in Files view) const breadcrumb = document.querySelector('.breadcrumb'); @@ -175,7 +182,7 @@ function switchToSharedView() { } function switchToFilesView() { - setCurrentSection('files'); + if (!setCurrentSection('files')) return; // Set actions bar mode window.setActionsBarMode('files', true); @@ -200,7 +207,7 @@ function switchToFilesView() { } function switchToFavoritesView() { - setCurrentSection('favorites'); + if (!setCurrentSection('favorites')) return; // Set actions bar mode window.setActionsBarMode('favorites'); @@ -233,7 +240,7 @@ function switchToFavoritesView() { } function switchToRecentFilesView() { - setCurrentSection('recent'); + if (!setCurrentSection('recent')) return; // Set actions bar mode window.setActionsBarMode('recent'); @@ -266,7 +273,7 @@ function switchToRecentFilesView() { } function switchToPhotosView() { - setCurrentSection('photos'); + if (!setCurrentSection('photos')) return; // Hide breadcrumb const breadcrumb = document.querySelector('.breadcrumb'); From db30639823f8e90ded779ddc87ae6ff0235387c3 Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle Date: Thu, 2 Apr 2026 20:44:06 +0200 Subject: [PATCH 3/5] refactor: rename View into Section to keep coherence --- static/js/app/main.js | 26 +++++++++++------------ static/js/app/navigation.js | 26 ++++++++++++----------- static/js/features/sharing/fileSharing.js | 4 ++-- static/js/views/shared/sharedView.js | 2 +- 4 files changed, 30 insertions(+), 28 deletions(-) diff --git a/static/js/app/main.js b/static/js/app/main.js index 196575c9..8427b411 100644 --- a/static/js/app/main.js +++ b/static/js/app/main.js @@ -283,31 +283,31 @@ function switchSectionTo(section) { switch (section) { case "files": - switchToFilesView(); + switchToFilesSection(); case "shared": - switchToSharedView(); + switchToSharedSection(); break; case "recent": - switchToRecentFilesView(); + switchToRecentFilesSection(); break; case "favorites": - switchToFavoritesView(); + switchToFavoritesSection(); break; case "photos": - switchToPhotosView(); + switchToPhotosSection(); break; case "trash": - switchToTrashView(); + switchToTrashSection(); break; default: console.warn(`context view ${section} unkonwn fallback to drive section`); - switchToFilesView(); + switchToFilesSection(); } } @@ -586,30 +586,30 @@ function setupEventListeners() { switch(itemI18nKey) { case 'nav.shared': // Switch to shared view - switchToSharedView(); + switchToSharedSection(); break; case 'nav.favorites': // Switch to favorites view - switchToFavoritesView(); + switchToFavoritesSection(); break; case 'nav.recent': // Switch to recent files view - switchToRecentFilesView(); + switchToRecentFilesSection(); break; case 'nav.photos': - switchToPhotosView(); + switchToPhotosSection(); break; case 'nav.trash': - switchToTrashView(); + switchToTrashSection(); break; default: // Use the proper switchToFilesView function which handles all UI restoration - window.switchToFilesView(); + window.switchToFilesSection(); // FIXME: because fileview handles it: need to converge code _updateHistory = false; } diff --git a/static/js/app/navigation.js b/static/js/app/navigation.js index 9522f8c3..dca8e261 100644 --- a/static/js/app/navigation.js +++ b/static/js/app/navigation.js @@ -156,7 +156,7 @@ function setCurrentSection(section) { return true; } -function switchToSharedView() { +function switchToSharedSection() { if (!setCurrentSection('shared')) return; @@ -181,7 +181,7 @@ function switchToSharedView() { } } -function switchToFilesView() { +function switchToFilesSection() { if (!setCurrentSection('files')) return; // Set actions bar mode @@ -206,7 +206,7 @@ function switchToFilesView() { window.loadFiles(); } -function switchToFavoritesView() { +function switchToFavoritesSection() { if (!setCurrentSection('favorites')) return; // Set actions bar mode @@ -239,7 +239,7 @@ function switchToFavoritesView() { } } -function switchToRecentFilesView() { +function switchToRecentFilesSection() { if (!setCurrentSection('recent')) return; // Set actions bar mode @@ -272,7 +272,7 @@ function switchToRecentFilesView() { } } -function switchToPhotosView() { +function switchToPhotosSection() { if (!setCurrentSection('photos')) return; // Hide breadcrumb @@ -294,7 +294,7 @@ function switchToPhotosView() { } } -function switchToTrashView() { +function switchToTrashSection() { setCurrentSection('trash'); // Hide breadcrumb (only shown in Files view) @@ -311,11 +311,13 @@ function switchToTrashView() { // Load trash items window.loadTrashItems(); + + if (window.multiSelect) window.multiSelect.clear(); } -window.switchToFilesView = switchToFilesView; -window.switchToSharedView = switchToSharedView; -window.switchToFavoritesView = switchToFavoritesView; -window.switchToRecentFilesView = switchToRecentFilesView; -window.switchToPhotosView = switchToPhotosView; -window.switchToTrashView = switchToTrashView; +window.switchToFilesSection = switchToFilesSection; +window.switchToSharedSection = switchToSharedSection; +window.switchToFavoritesSection = switchToFavoritesSection; +window.switchToRecentFilesSection = switchToRecentFilesSection; +window.switchToPhotosSection = switchToPhotosSection; +window.switchToTrashSection = switchToTrashSection; diff --git a/static/js/features/sharing/fileSharing.js b/static/js/features/sharing/fileSharing.js index e95b89d9..420e5da9 100644 --- a/static/js/features/sharing/fileSharing.js +++ b/static/js/features/sharing/fileSharing.js @@ -188,8 +188,8 @@ const fileSharing = { const span = item.querySelector('span'); if (span && span.getAttribute('data-i18n') === 'nav.shared') { item.addEventListener('click', () => { - if (window.switchToSharedView) { - window.switchToSharedView(); + if (window.switchToSharedSection) { + window.switchToSharedSection(); } }); } diff --git a/static/js/views/shared/sharedView.js b/static/js/views/shared/sharedView.js index 0cfa5b49..cc186dfa 100644 --- a/static/js/views/shared/sharedView.js +++ b/static/js/views/shared/sharedView.js @@ -243,7 +243,7 @@ const sharedView = { const goToFilesBtn = document.getElementById('go-to-files-btn'); if (goToFilesBtn) { goToFilesBtn.addEventListener('click', () => { - if (window.switchToFilesView) window.switchToFilesView(); + if (window.switchToFilesSection) window.switchToFilesSection(); }); } }, From 5b821a7eab2854316b1bd621bf917a8e57d4bacd Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle Date: Fri, 3 Apr 2026 00:28:53 +0200 Subject: [PATCH 4/5] refactor(empty-list): simplify empty list, will now work on list view --- static/css/base/reset.css | 2 +- static/index.html | 6 ++++++ static/js/app/filesView.js | 10 ++-------- static/js/app/navigation.js | 18 ++++++++++++++++++ static/js/app/ui.js | 11 +++++++++++ 5 files changed, 38 insertions(+), 9 deletions(-) diff --git a/static/css/base/reset.css b/static/css/base/reset.css index 227c2a59..9617291f 100644 --- a/static/css/base/reset.css +++ b/static/css/base/reset.css @@ -21,4 +21,4 @@ html[dir='rtl'] .fa-sign-out-alt { transform: rotate(180deg); } /* Utility: hide elements without inline style="" (CSP-safe) */ -.hidden { display: none; } \ No newline at end of file +.hidden { display: none !important; } diff --git a/static/index.html b/static/index.html index a7e957c4..04e3de44 100644 --- a/static/index.html +++ b/static/index.html @@ -276,6 +276,12 @@
+ + diff --git a/static/js/app/filesView.js b/static/js/app/filesView.js index 882cdd47..f34e2bcd 100644 --- a/static/js/app/filesView.js +++ b/static/js/app/filesView.js @@ -237,15 +237,9 @@ async function loadFiles(options = { insertHistory: true}) { const fileList = Array.isArray(listing.files) ? listing.files : []; if (folderList.length === 0 && fileList.length === 0) { - const emptyState = document.createElement('div'); - emptyState.className = 'empty-state'; - emptyState.innerHTML = ` - -

${_t('files.no_files')}

-

${_t('files.empty_hint')}

- `; - elements.filesGrid.appendChild(emptyState); + window.showEmptyList(true); } else { + window.showEmptyList(false); window.ui.renderFolders(folderList); window.ui.renderFiles(fileList); } diff --git a/static/js/app/navigation.js b/static/js/app/navigation.js index dca8e261..3b519658 100644 --- a/static/js/app/navigation.js +++ b/static/js/app/navigation.js @@ -174,6 +174,9 @@ function switchToSharedSection() { if (filesGrid) filesGrid.classList.add('hidden'); if (filesListView) filesListView.classList.add('hidden'); + //hide by default empty list + window.showEmptyList(false); + // Show shared view if (window.sharedView) { window.sharedView.init(); @@ -198,6 +201,9 @@ function switchToFilesSection() { if (filesListView) filesListView.style.display = window.app.currentView === 'list' ? 'flex' : 'none'; if (filesListView) filesListView.classList.toggle('hidden', window.app.currentView !== 'list'); + //hide by default empty list + window.showEmptyList(false); + // Reset to home folder and update breadcrumb window.app.currentPath = window.app.userHomeFolderId || ''; window.app.breadcrumbPath = []; @@ -222,6 +228,9 @@ function switchToFavoritesSection() { if (filesGrid) filesGrid.classList.toggle('hidden', window.app.currentView !== 'grid'); if (filesListView) filesListView.style.display = window.app.currentView === 'list' ? 'flex' : 'none'; if (filesListView) filesListView.classList.toggle('hidden', window.app.currentView !== 'list'); + + //hide by default empty list + window.showEmptyList(false); if (window.favorites) { window.favorites.displayFavorites(); @@ -256,6 +265,9 @@ function switchToRecentFilesSection() { if (filesListView) filesListView.style.display = window.app.currentView === 'list' ? 'flex' : 'none'; if (filesListView) filesListView.classList.toggle('hidden', window.app.currentView !== 'list'); + //hide by default empty list + window.showEmptyList(false); + if (window.recent) { window.recent.displayRecentFiles(); } else { @@ -288,6 +300,9 @@ function switchToPhotosSection() { if (filesGrid) { filesGrid.style.display = 'none'; filesGrid.classList.add('hidden'); } if (filesListView) { filesListView.style.display = 'none'; filesListView.classList.add('hidden'); } + //hide by default empty list + window.showEmptyList(false); + // Show photos view if (window.photosView) { window.photosView.show(); @@ -309,6 +324,9 @@ function switchToTrashSection() { setActionsBarMode('trash'); + //hide by default empty list + window.showEmptyList(false); + // Load trash items window.loadTrashItems(); diff --git a/static/js/app/ui.js b/static/js/app/ui.js index 1ab842f0..37bf9a93 100644 --- a/static/js/app/ui.js +++ b/static/js/app/ui.js @@ -1493,10 +1493,21 @@ if (document.readyState === 'loading') { initRubberBandSelection(); } +/** + * + * @param {boolean} show + */ +function showEmptyList(show) { + const emptyArea=document.getElementById("empty-files-state"); + emptyArea.classList.toggle("hidden", !show); +} + // Expose helpers globally window.toggleCardSelection = toggleCardSelection; window.showContextMenuAtElement = showContextMenuAtElement; window.initRubberBandSelection = initRubberBandSelection; +window.showEmptyList = showEmptyList; + /** * Show a modern confirm dialog (replaces native confirm()) From 534d4dc190c43209cee89f1c1c3cbcc07a4dcc8a Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle Date: Wed, 25 Mar 2026 18:49:38 +0100 Subject: [PATCH 5/5] feat: improve drag & drop * permits multiple drag & drop * synchronize grid & list view on selection * permits copy during ddrag & drop (use sift/alt key according your OS) * use batch move / copy on drag & drop --- static/css/components/multiSelect.css | 12 +- static/css/layout/content.css | 52 ++++- static/index.html | 6 +- static/js/app/main.js | 17 +- static/js/app/navigation.js | 9 + static/js/app/ui.js | 209 +++++++++++++++------ static/js/core/notifications.js | 1 - static/js/features/files/contextMenus.js | 77 +------- static/js/features/files/fileOperations.js | 95 ++++++++++ static/js/features/files/multiSelect.js | 81 ++++++-- 10 files changed, 386 insertions(+), 173 deletions(-) diff --git a/static/css/components/multiSelect.css b/static/css/components/multiSelect.css index 0416753c..39c48cd6 100644 --- a/static/css/components/multiSelect.css +++ b/static/css/components/multiSelect.css @@ -43,21 +43,11 @@ padding: 10px 20px; border-radius: 12px; margin: 0 0 12px; - opacity: 0; - max-height: 0; + height: 60px; overflow: hidden; transform: translateY(-8px); transition: opacity 0.2s, max-height 0.25s, transform 0.2s, margin 0.2s, padding 0.2s; - pointer-events: none; -} - -.batch-selection-bar.visible { - opacity: 1; - max-height: 60px; - transform: translateY(0); pointer-events: auto; - padding: 10px 20px; - margin: 0 0 12px; } .batch-bar-left { diff --git a/static/css/layout/content.css b/static/css/layout/content.css index 061954d6..f4685c96 100644 --- a/static/css/layout/content.css +++ b/static/css/layout/content.css @@ -16,7 +16,9 @@ .actions-bar { display: flex; justify-content: space-between; - margin-bottom: 20px; + margin: 0 0 12px; + height: 60px; + padding: 10px; } .action-buttons { @@ -36,3 +38,51 @@ height: 300px; grid-column: 1 / -1; } + +/* invisible element, permits building of drag element without altering display */ +.drag-preview { + position: absolute; + top: -9999px; + left: -9999px; + pointer-events: none; + width: 360px; +} + +.dragged-items > div { + position: relative; + display: inline-block; + padding: 4px; + width: 360px; + background-color: white; + height: 46px; +} + +.dragged-items > div.fading { + -webkit-mask-image: linear-gradient(to bottom, white 20%, transparent); + mask-image: linear-gradient(to bottom, white 20%, transparent); +} + +.dragged-items-badge { + position: absolute; + top: 0; + right: 0; + + transform: translate(50%, -50%); + + background: red; + color: white; + + border-radius: 50%; + min-width: 20px; + height: 20px; + + display: flex; + align-items: center; + justify-content: center; + + font-size: 12px; + font-weight: bold; + + padding: 2px; +} + diff --git a/static/index.html b/static/index.html index 04e3de44..3989ff6b 100644 --- a/static/index.html +++ b/static/index.html @@ -204,7 +204,7 @@

Files

-
+
+ +

Drag files here or click to select

@@ -255,8 +257,6 @@ Home
-
-
diff --git a/static/js/app/main.js b/static/js/app/main.js index 8427b411..6f39aab4 100644 --- a/static/js/app/main.js +++ b/static/js/app/main.js @@ -105,8 +105,9 @@ function setActionsBarMode(mode, force = false) { if (!elements.actionsBar) return; if (mode === 'hidden') { - elements.actionsBar.style.display = 'none'; + elements.actionsBar.classList.add("hidden"); elements.actionsBar.dataset.mode = 'hidden'; + console.log("......setup actions bar to hidden"); return; } @@ -118,7 +119,7 @@ function setActionsBarMode(mode, force = false) { if (!html) return; elements.actionsBar.innerHTML = html; - elements.actionsBar.style.display = 'flex'; + elements.actionsBar.classList.remove("hidden"); elements.actionsBar.dataset.mode = mode; // Refresh cached action elements after rebuild @@ -414,7 +415,7 @@ function cacheElements() { elements.listViewBtn = document.getElementById('list-view-btn'); elements.breadcrumb = document.querySelector('.breadcrumb'); elements.pageTitle = document.querySelector('.page-title'); - elements.actionsBar = document.querySelector('.actions-bar'); + elements.actionsBar = document.getElementById('actions-bar'); elements.navItems = document.querySelectorAll('.nav-item'); elements.searchInput = document.querySelector('.search-container input'); } @@ -647,16 +648,6 @@ function setupEventListeners() { !fileMenu.contains(e.target)) { ui.closeFileContextMenu(); } - - // Deselect all cards when clicking empty area (not on a card, menu, or modal) - // Note: multiSelect._hookGlobalDeselect() handles clearing the internal - // selection state; this handler only covers the legacy CSS class removal. - // Skip if a rubber-band selection just finished — the click is a side-effect. - if (window.__rubberBandJustFinished) return; - if (!e.target.closest('.file-card') && !e.target.closest('.file-item') && !e.target.closest('.context-menu') && !e.target.closest('.about-modal') && !e.target.closest('.batch-selection-bar') && !e.target.closest('.list-header.selection-mode')) { - document.querySelectorAll('.file-card.selected').forEach(c => c.classList.remove('selected')); - document.querySelectorAll('.file-item.selected').forEach(c => c.classList.remove('selected')); - } }); } diff --git a/static/js/app/navigation.js b/static/js/app/navigation.js index 3b519658..d1d8e98c 100644 --- a/static/js/app/navigation.js +++ b/static/js/app/navigation.js @@ -182,6 +182,8 @@ function switchToSharedSection() { window.sharedView.init(); window.sharedView.show(); } + if (window.multiSelect) window.multiSelect.clear(); + } function switchToFilesSection() { @@ -208,6 +210,7 @@ function switchToFilesSection() { window.app.currentPath = window.app.userHomeFolderId || ''; window.app.breadcrumbPath = []; window.ui.updateBreadcrumb(); + if (window.multiSelect) window.multiSelect.clear(); window.loadFiles(); } @@ -246,6 +249,9 @@ function switchToFavoritesSection() { `; } } + + if (window.multiSelect) window.multiSelect.clear(); + } function switchToRecentFilesSection() { @@ -282,6 +288,7 @@ function switchToRecentFilesSection() { `; } } + if (window.multiSelect) window.multiSelect.clear(); } function switchToPhotosSection() { @@ -307,6 +314,8 @@ function switchToPhotosSection() { if (window.photosView) { window.photosView.show(); } + if (window.multiSelect) window.multiSelect.clear(); + } function switchToTrashSection() { diff --git a/static/js/app/ui.js b/static/js/app/ui.js index 37bf9a93..894d17ac 100644 --- a/static/js/app/ui.js +++ b/static/js/app/ui.js @@ -3,6 +3,8 @@ * This file handles UI-related functions, view toggling, and interface interactions */ +// @ts-check + // UI Module const ui = { /** @@ -278,6 +280,12 @@ const ui = { * Set up drag and drop functionality */ setupDragAndDrop() { + // prepare area to build dragged elements + this.dragPreview = document.createElement("div"); + this.dragPreview.className="drag-preview"; + document.body.appendChild(this.dragPreview); + this.draggedItems = null; + const dropzone = document.getElementById('dropzone'); const collectDroppedEntries = async (dataTransfer) => { @@ -577,11 +585,8 @@ const ui = { e.preventDefault(); card.classList.remove('drop-target'); - const id = e.dataTransfer.getData('text/plain'); - const isFolder = - e.dataTransfer.getData('application/oxicloud-folder') === 'true'; - - await self.move( id, isFolder, targetFolderId); + const action = e.dataTransfer?.dropEffect; + await self._dropToFolder( action, targetFolderId, e.dataTransfer ); }); } else { // Last segment: current location, not clickable @@ -591,32 +596,6 @@ const ui = { }); }, - - // TODO: support multiple elements to move ? (API does) - /** - * proceed to the drag & drop - * @param {string} sourceId to move (can be a uniq object or) - * @param {boolean} sourceIsAFolder - * @param {string} targetFolderId - */ - async move(sourceId, sourceIsAFolder, targetFolderId) { - if (!sourceId) return; - - console.log(`request move ${ sourceIsAFolder ? "folder": "file"} ${sourceId} to folder ${targetFolderId}`); - - if (sourceIsAFolder) { - if (sourceId === targetFolderId) { - alert("You cannot move a folder to itself"); - return; - } - // TODO: handle errors... - await fileOps.moveFolder(sourceId, targetFolderId); - } else { - // TODO: handle errors... - await fileOps.moveFile(sourceId, targetFolderId); - } - }, - /** * Check if a file can be previewed in the viewer * @param {Object} file - File object with mime_type property @@ -742,6 +721,66 @@ const ui = { } }, + /** + * handle the drop + * @param {string} action copy|move + * @param {string} targetFolderId the target + * @param {any} dataTransfer fallback if nothing is selected + */ + async _dropToFolder(action, targetFolderId, dataTransfer) { + let selection = window.multiSelect.getSelection(targetFolderId); + + window.multiSelect.clear(); + + if (selection.fileIds.length == 0 && selection.folderIds.length == 0) { + + // try to use dataTransfer (direct move without selection) + const id = dataTransfer.getData('text/plain'); + const isFolder = dataTransfer.getData('application/oxicloud-folder') === 'true'; + + if (isFolder && id === targetFolderId) { + console.log("nothing to do"); + return; //nothing to do + } + // append current item to selection + if (isFolder) { + console.log(`adding ${id} as folder`); + selection.folderIds.push(id); + } + else { + console.log(`adding ${id} as file`); + selection.fileIds.push(id); + } + } + + console.log(`request ${action} of: `, selection); + /* + TODO do we prefer use atomic operation on 1 item ? like: + await fileOps.moveFolder(sourceId, targetFolderId); + await fileOps.moveFile(sourceId, targetFolderId); + */ + + let result; + switch (action) { + case "copy": + result = await window.fileOps.batchCopy(selection.fileIds, selection.folderIds, targetFolderId); + break; + + case "move": + result = await window.fileOps.batchMove(selection.fileIds, selection.folderIds, targetFolderId); + // redraw directory + if (result.success > 0) + window.loadFiles(); + break; + + default: + console.error(`drag and drop: action ${action} unknown`); + return; + } + window.multiSelect.showBatchResult(action, result); + console.log( result); + }, + _hydrateViewIfNeeded(view) { // Only hydrate if there is at least one rendered item in the opposite/current DOM. // This prevents stale cache hydration in empty-state screens. @@ -755,7 +794,6 @@ const ui = { this._renderFoldersToView(this._lastFolders, 'grid'); this._renderFilesToView(this._lastFiles, 'grid'); - return; } if (view === 'list') { @@ -787,9 +825,9 @@ const ui = { const itemInfo = (card) => { if (!card) return null; const fileId = card.dataset.fileId; - if (fileId) return { type: 'file', id: fileId, data: self._items.get(fileId) }; + if (fileId) return { type: 'file', id: fileId, name: card.dataset.fileName, data: self._items.get(fileId) }; const folderId = card.dataset.folderId; - if (folderId) return { type: 'folder', id: folderId, data: self._items.get(folderId) }; + if (folderId) return { type: 'folder', id: folderId, name: card.dataset.folderName, data: self._items.get(folderId) }; return null; }; @@ -947,31 +985,75 @@ const ui = { // dragstart container.addEventListener('dragstart', (e) => { - const card = e.target.closest(sel); + let card = e.target.closest(sel); if (!card) { e.preventDefault(); return; } - - // Grid items must be selected to start dragging - if (container === grid && - !card.classList.contains('selected')) { - e.preventDefault(); - return; - } - + const info = itemInfo(card); if (!info) { e.preventDefault(); return; } + if (container === grid) { + // pickup the equivalent item in list + const selector = (info.type === 'folder') ? "data-folder-id" : "data-file-id"; + card = list.querySelector(`div.file-item[${selector}="${info.id}"]`); + } + e.dataTransfer.setData('text/plain', info.id); - if (info.type === 'folder') { + if (info.type === 'folder') { e.dataTransfer.setData( 'application/oxicloud-folder', 'true'); } - card.classList.add('dragging'); + // allow copy or move (handled by the browser) + e.dataTransfer.effectAllowed = "copyMove"; + + self.draggedItems = document.createElement("div"); + self.draggedItems.className = "dragged-items"; + + // Fplease note that dragged elements are taken from list view (uniformisation) + let selectedCardFromList = list.querySelectorAll(`div.selected > div.name-cell`); + if (selectedCardFromList.length == 0) { + // fallback to current element + selectedCardFromList = card.querySelectorAll('div.name-cell'); + } + + let index = 0; + const maxElements = 4; + let lastItemDiv = null; + + while (index < selectedCardFromList.length && index < maxElements) { + let div = document.createElement("div"); + div.className="file-item"; + let clone = selectedCardFromList[index].cloneNode(true); + let star = clone.querySelector('.favorite-star-inline'); + clone.querySelectorAll('img').forEach((img) => { img.loading="eager"; } ); + if (star) clone.removeChild(star); + div.appendChild(clone); + self.draggedItems.appendChild( div); + index += 1; + lastItemDiv = div; + } + + // if more than 1 item, display the badge + if (selectedCardFromList.length > 1) { + let badge = document.createElement("span"); + badge.className="dragged-items-badge"; + badge.innerText=selectedCardFromList.length; + self.draggedItems.appendChild( badge); + } + + // if more than maxElements display the fading + if (selectedCardFromList.length > maxElements) { + lastItemDiv.classList.add("fading"); + } + + self.dragPreview.appendChild( self.draggedItems); + e.dataTransfer.setDragImage(self.draggedItems, -20, -20); + }); // dragend container.addEventListener('dragend', (e) => { - const card = e.target.closest(sel); - if (card) card.classList.remove('dragging'); + + self.dragPreview.removeChild( self.draggedItems); document.querySelectorAll('.drop-target') .forEach(el => el.classList.remove('drop-target')); }); @@ -1002,11 +1084,8 @@ const ui = { e.preventDefault(); card.classList.remove('drop-target'); - const id = e.dataTransfer.getData('text/plain'); - const isFolder = - e.dataTransfer.getData('application/oxicloud-folder') === 'true'; - - await self.move( id, isFolder, targetFolderId); + const action = e.dataTransfer.dropEffect; + await self._dropToFolder( action, targetFolderId, e.dataTransfer); }); } }, @@ -1266,7 +1345,8 @@ const ui = { this._items.set(folder.id, folder); } - this._renderFoldersToView(safeFolders, this._getActiveView()); + this._renderFoldersToView(safeFolders, 'grid'); + this._renderFoldersToView(safeFolders, 'list'); }, /** @@ -1282,7 +1362,8 @@ const ui = { this._items.set(file.id, file); } - this._renderFilesToView(safeFiles, this._getActiveView()); + this._renderFilesToView(safeFiles, 'grid'); + this._renderFilesToView(safeFiles, 'list'); }, /* ================================================================ @@ -1305,7 +1386,8 @@ const ui = { this._items.set(folder.id, folder); this._upsertById(this._lastFolders, folder); - this._renderFoldersToView([folder], this._getActiveView()); + this._renderFoldersToView([folder], 'grid'); + this._renderFoldersToView([folder], 'list'); }, /** @@ -1324,7 +1406,8 @@ const ui = { this._items.set(file.id, file); this._upsertById(this._lastFiles, file); - this._renderFilesToView([file], this._getActiveView()); + this._renderFilesToView([file], 'grid'); + this._renderFilesToView([file], 'list'); } }; @@ -1393,6 +1476,7 @@ function initRubberBandSelection() { let active = false; let startX = 0, startY = 0; + const list = document.getElementById('files-list-view'); // We listen on the whole files-container (covers grid + empty space) const container = document.querySelector('.files-container') || document.getElementById('files-grid'); @@ -1454,9 +1538,16 @@ function initRubberBandSelection() { if (intersects) { card.classList.add('selected'); + // Sync with multiSelect module if (window.multiSelect) { const info = window.multiSelect._extractInfo(card); + + // select the equivalent list-item to activate it too + let selector = (info.type === 'folder') ? "data-folder-id" : "data-file-id"; + const fileItem = list?.querySelector(`div.file-item[${selector}="${info.id}"]`); + fileItem?.classList.add('selected'); + if (info) window.multiSelect.select(info.id, info.name, info.type, info.parentId); } } else { @@ -1464,6 +1555,12 @@ function initRubberBandSelection() { // Deselect from multiSelect module if (window.multiSelect) { const info = window.multiSelect._extractInfo(card); + + // select the equivalent list-item to activate it too + let selector = (info.type === 'folder') ? "data-folder-id" : "data-file-id"; + const fileItem = list?.querySelector(`div.file-item[${selector}="${info.id}"]`); + fileItem?.classList.remove('selected'); + if (info) window.multiSelect.deselect(info.id); } } diff --git a/static/js/core/notifications.js b/static/js/core/notifications.js index 69b723de..117bfb01 100644 --- a/static/js/core/notifications.js +++ b/static/js/core/notifications.js @@ -79,7 +79,6 @@ const notifications = (() => { } function _renderBadge() { const badge = $('notif-badge'); - console.log(`badge`, badge); if (!badge) return; if (_badgeCount > 0) { badge.classList.remove("hidden"); diff --git a/static/js/features/files/contextMenus.js b/static/js/features/files/contextMenus.js index 095c0e68..d6f57c34 100644 --- a/static/js/features/files/contextMenus.js +++ b/static/js/features/files/contextMenus.js @@ -288,40 +288,13 @@ const contextMenus = { const fileIds = items.filter(i => i.type === 'file').map(i => i.id); const folderIds = items.filter(i => i.type === 'folder').map(i => i.id); - let success = 0, errors = 0; - - try { - // Batch copy files - if (fileIds.length > 0) { - const res = await fetch('/api/batch/files/copy', { - method: 'POST', - headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' }, - body: JSON.stringify({ file_ids: fileIds, target_folder_id: targetId }) - }); - const data = await res.json(); - success += data.stats?.successful || 0; - errors += data.stats?.failed || 0; - } - - // Note: Folder copy is not yet implemented in batch API - if (folderIds.length > 0) { - window.ui.showNotification('Info', 'Folder copy is not yet supported in batch mode'); - } - } catch (err) { - console.error('Batch copy error:', err); - errors++; - } - + let result = await window.fileOps.batchCopy( fileIds, folderIds, targetId); + this.closeMoveDialog(); window.multiSelect.clear(); window.loadFiles(); - if (errors > 0) { - window.ui.showNotification('Batch copy', `${success} copied, ${errors} failed`); - } else { - window.ui.showNotification('Items copied', - `${success} item${success !== 1 ? 's' : ''} copied successfully`); - } + window.multiSelect.showBatchResult( "copy", result); return; } @@ -353,48 +326,14 @@ const contextMenus = { const fileIds = items.filter(i => i.type === 'file').map(i => i.id); const folderIds = items.filter(i => i.type === 'folder' && i.id !== targetId).map(i => i.id); - - let success = 0, errors = 0; - - try { - // Batch move files in a single request - if (fileIds.length > 0) { - const res = await fetch('/api/batch/files/move', { - method: 'POST', - headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' }, - body: JSON.stringify({ file_ids: fileIds, target_folder_id: targetId }) - }); - const data = await res.json(); - success += data.stats?.successful || 0; - errors += data.stats?.failed || 0; - } - - // Batch move folders in a single request - if (folderIds.length > 0) { - const res = await fetch('/api/batch/folders/move', { - method: 'POST', - headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' }, - body: JSON.stringify({ folder_ids: folderIds, target_folder_id: targetId }) - }); - const data = await res.json(); - success += data.stats?.successful || 0; - errors += data.stats?.failed || 0; - } - } catch (err) { - console.error('Batch move error:', err); - errors++; - } - + + let result = await window.fileOps.batchMove( fileIds, folderIds, targetId); + this.closeMoveDialog(); window.multiSelect.clear(); window.loadFiles(); - - if (errors > 0) { - window.ui.showNotification('Batch move', `${success} moved, ${errors} failed`); - } else { - window.ui.showNotification('Items moved', - `${success} item${success !== 1 ? 's' : ''} moved successfully`); - } + window.multiSelect.showBatchResult( "move", result); + return; } diff --git a/static/js/features/files/fileOperations.js b/static/js/features/files/fileOperations.js index dc959a18..13346a1a 100644 --- a/static/js/features/files/fileOperations.js +++ b/static/js/features/files/fileOperations.js @@ -771,6 +771,60 @@ const fileOps = { } }, + /** + * @typedef {Object} BatchResult + * @property {number} success number of files|folders sucessfully updated + * @property {number} errors number of files|folders in error + * / + + /** + * Move files & folders + * @param {string[]} fileIds - File IDs + * @param {string[]} folderIds - Folder IDs + * @param {string} targetFolderId - Target folder ID + * @returns {Promise} - Success status + */ + async batchMove(fileIds, folderIds, targetFolderId) { + + // TODO ensure not moving a folder into itself + let success = 0, errors = 0; + + try { + // Batch move files in a single request + if (fileIds.length > 0) { + const res = await fetch('/api/batch/files/move', { + method: 'POST', + headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' }, + body: JSON.stringify({ file_ids: fileIds, target_folder_id: targetFolderId }) + }); + const data = await res.json(); + success += data.stats?.successful || 0; + errors += data.stats?.failed || 0; + } + + // Batch move folders in a single request + if (folderIds.length > 0) { + const res = await fetch('/api/batch/folders/move', { + method: 'POST', + headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' }, + body: JSON.stringify({ folder_ids: folderIds, target_folder_id: targetFolderId }) + }); + const data = await res.json(); + success += data.stats?.successful || 0; + errors += data.stats?.failed || 0; + } + } + catch (err) { + console.error('Batch move error:', err); + errors++; + } + + return { + success, + errors + }; + }, + /** * Copy a file to another folder * @param {string} fileId - File ID @@ -828,6 +882,47 @@ const fileOps = { return false; }, + /** + * Copy files & folders + * @param {string[]} fileIds - File IDs + * @param {string[]} folderIds - Folder IDs + * @param {string} targetFolderId - Target folder ID + * @returns {Promise} - Success status + */ + async batchCopy(fileIds, folderIds, targetFolderId) { + + // FIXME ensure not moving a folder into itself + + let success = 0, errors = 0; + try { + // Batch copy files + if (fileIds.length > 0) { + const res = await fetch('/api/batch/files/copy', { + method: 'POST', + headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' }, + body: JSON.stringify({ file_ids: fileIds, target_folder_id: targetFolderId }) + }); + const data = await res.json(); + success += data.stats?.successful || 0; + errors += data.stats?.failed || 0; + } + + // Note: Folder copy is not yet implemented in batch API + if (folderIds.length > 0) { + window.ui.showNotification('Info', 'Folder copy is not yet supported in batch mode'); + errors += folderIds.lenngth; + } + } catch (err) { + console.error('Batch copy error:', err); + errors++; + } + + return { + success, + errors + }; + }, + /** * Rename a file * @param {string} fileId - File ID diff --git a/static/js/features/files/multiSelect.js b/static/js/features/files/multiSelect.js index cca75273..4cd111f7 100644 --- a/static/js/features/files/multiSelect.js +++ b/static/js/features/files/multiSelect.js @@ -76,6 +76,61 @@ const multiSelect = { } }, + /** + * @typedef {Object} ItemSelection + * @property {string[]} fileIds list of files' id + * @property {string[]} folderIds list of folders' id + */ + + /** + * get selection + * @param {string} [targtFolderId] an optional targget (will be removed from selected item) + * @return {ItemSelection} + */ + getSelection(targtFolderId) { + let fileIds=[]; + let folderIds=[]; + + // TODO optimize & check if _selected is a better use + document.querySelectorAll(`div.file-item.selected`).forEach( (item) => { + if (item.dataset.fileId) { + fileIds.push(item.dataset.fileId); + } + else { + // ignore selectedItem if this is the target + if (targtFolderId && targtFolderId !== item.dataset.folderId) + folderIds.push(item.dataset.folderId); + } + }); + + return { + "fileIds": fileIds, + "folderIds": folderIds, + }; + }, + + /** + * @param {string} action move|copy + * @param {BatchResult} result result of batch + */ + showBatchResult(action, result) { + if (action === "copy") { + if (result.errors > 0) { + window.ui.showNotification('Batch copy', `${result.success} copied, ${result.errors} failed`); + } else { + window.ui.showNotification('Items copied', + `${result.success} item${result.success !== 1 ? 's' : ''} copied successfully`); + } + } else { + if (result.errors > 0) { + window.ui.showNotification('Batch move', `${result.success} moved, ${result.errors} failed`); + } else { + window.ui.showNotification('Items moved', + `${result.success} item${result.success !== 1 ? 's' : ''} moved successfully`); + } + } + }, + // ── DOM helpers ───────────────────────────────────────── _selectElement(el) { @@ -94,13 +149,6 @@ const multiSelect = { _getAllVisibleItems() { return [...document.querySelectorAll('.file-item, .file-card')]; - /* - const grid = document.getElementById('files-grid'); - if (grid && grid.style.display !== 'none') { - return [...grid.querySelectorAll('.file-card')]; - } - return [...document.querySelectorAll('#files-list-view .file-item')]; - */ }, _extractInfo(el) { @@ -192,6 +240,7 @@ const multiSelect = { const n = this._selected.size; const batchSelectionBar = document.getElementById('batch-selection-bar'); + const actionsBar = document.getElementById('actions-bar'); if (n > 0) { this._barVisible = true; @@ -201,14 +250,18 @@ const multiSelect = { : (this._t('batch.n_selected', { count: n }) || `${n} items selected`); document.getElementById("batch-bar-count").innerText = countText; - batchSelectionBar.classList.add('visible'); + actionsBar.classList.add('hidden'); + batchSelectionBar.classList.remove('hidden'); } else { this._barVisible = false; // Hide grid bar - batchSelectionBar.classList.remove('visible'); + batchSelectionBar.classList.add('hidden'); + + if (actionsBar.dataset.mode !== "hidden") + actionsBar.classList.remove('hidden'); } // Sync individual item checkboxes @@ -425,9 +478,6 @@ const multiSelect = { // Wire the initial select-all checkbox this._injectListHeaderCheckbox(); - // Global deselect on empty-area click - this._hookGlobalDeselect(); - // Keyboard shortcuts document.addEventListener('keydown', (e) => { if (e.target.closest('input, textarea, [contenteditable], .rename-dialog, .share-dialog, .confirm-dialog')) return; @@ -459,13 +509,6 @@ const multiSelect = { cb.addEventListener('change', () => this.toggleAll()); }, - _hookGlobalDeselect() { - document.addEventListener('click', (e) => { - if (window.__rubberBandJustFinished) return; - if (e.target.closest('.file-card, .file-item, .context-menu, .batch-selection-bar, .list-header.selection-mode, .about-modal, .rename-dialog, .share-dialog, .confirm-dialog, .modal-overlay, input, button')) return; - if (this.hasSelection) this.clear(); - }); - } }; // Expose globally