diff --git a/src/infrastructure/repositories/pg/file_blob_read_repository.rs b/src/infrastructure/repositories/pg/file_blob_read_repository.rs index ce8525a2..9625a3be 100755 --- a/src/infrastructure/repositories/pg/file_blob_read_repository.rs +++ b/src/infrastructure/repositories/pg/file_blob_read_repository.rs @@ -7,6 +7,20 @@ //! File paths are resolved by JOINing with `storage.folders.path` (the //! materialized path column), so no recursive CTEs or N+1 queries are needed. +/// Row shape returned by media-file queries (avoids `clippy::type_complexity`). +type MediaFileRow = ( + String, // id + String, // name + Option, // folder_id + Option, // folder path + i64, // size + String, // mime_type + i64, // created_at + i64, // updated_at + Option, // user_id + i64, // sort_date +); + use bytes::Bytes; use futures::{Stream, TryStreamExt}; use moka::sync::Cache; @@ -156,18 +170,7 @@ impl FileBlobReadRepository { before: Option, limit: i64, ) -> Result<(Vec, Vec), DomainError> { - let rows: Vec<( - String, // id - String, // name - Option, // folder_id - Option, // folder path - i64, // size - String, // mime_type - i64, // created_at - i64, // updated_at - Option, // user_id - i64, // sort_date - )> = sqlx::query_as( + let rows: Vec = sqlx::query_as( r#" SELECT fi.id::text, fi.name, fi.folder_id::text, fo.path, fi.size, fi.mime_type, diff --git a/src/infrastructure/repositories/pg/file_metadata_repository.rs b/src/infrastructure/repositories/pg/file_metadata_repository.rs index e7c6d305..9f029327 100644 --- a/src/infrastructure/repositories/pg/file_metadata_repository.rs +++ b/src/infrastructure/repositories/pg/file_metadata_repository.rs @@ -10,6 +10,19 @@ use tracing::error; use crate::common::errors::DomainError; use crate::infrastructure::services::exif_service::ExifMetadata; +/// Row shape returned by metadata queries (avoids `clippy::type_complexity`). +type MetadataRow = ( + String, + Option>, + Option, + Option, + Option, + Option, + Option, + Option, + Option, +); + /// Metadata as stored/retrieved from the database. #[derive(Debug, Clone, Serialize)] pub struct StoredMetadata { @@ -73,17 +86,7 @@ impl FileMetadataRepository { /// Get metadata for a single file. pub async fn get(&self, file_id: &str) -> Result, DomainError> { - let row: Option<( - String, - Option>, - Option, - Option, - Option, - Option, - Option, - Option, - Option, - )> = sqlx::query_as( + let row: Option = sqlx::query_as( r#" SELECT file_id::text, captured_at, latitude, longitude, camera_make, camera_model, orientation, width, height @@ -135,17 +138,7 @@ impl FileMetadataRepository { return Ok(HashMap::new()); } - let rows: Vec<( - String, - Option>, - Option, - Option, - Option, - Option, - Option, - Option, - Option, - )> = sqlx::query_as( + let rows: Vec = sqlx::query_as( r#" SELECT file_id::text, captured_at, latitude, longitude, camera_make, camera_model, orientation, width, height diff --git a/src/interfaces/nextcloud/preview_handler.rs b/src/interfaces/nextcloud/preview_handler.rs index 2cf95b7e..5bdebb9f 100644 --- a/src/interfaces/nextcloud/preview_handler.rs +++ b/src/interfaces/nextcloud/preview_handler.rs @@ -39,7 +39,11 @@ pub async fn handle_preview( ) -> impl IntoResponse { // Parse the Nextcloud file ID — the NC app may append an instance suffix // (e.g. "00000326ocnca"), so strip non-digit characters first. - let numeric_part: String = params.file_id.chars().take_while(|c| c.is_ascii_digit()).collect(); + let numeric_part: String = params + .file_id + .chars() + .take_while(|c| c.is_ascii_digit()) + .collect(); let nc_file_id: i64 = match numeric_part.parse() { Ok(id) => id, Err(_) => { diff --git a/static/css/components/fileList.css b/static/css/components/fileList.css index 4dfb4ceb..61e9477d 100755 --- a/static/css/components/fileList.css +++ b/static/css/components/fileList.css @@ -13,6 +13,7 @@ .list-header { display: grid; grid-template-columns: var(--files-list-columns); + column-gap: 12px; padding: 15px; font-weight: 600; color: #2d3748; @@ -24,6 +25,7 @@ .file-item { display: grid; grid-template-columns: var(--files-list-columns); + column-gap: 12px; padding: 12px 15px; border-bottom: 1px solid #f0f0f0; align-items: center; @@ -332,6 +334,7 @@ border: 2px dashed #ffc107; } + [data-theme="dark"] .files-list-view { background-color: #1e293b; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); @@ -380,3 +383,4 @@ [data-theme="dark"] .file-item.drop-target { background-color: rgba(255, 193, 7, 0.05); } + diff --git a/static/js/app/main.js b/static/js/app/main.js index 21a6496a..510ab653 100755 --- a/static/js/app/main.js +++ b/static/js/app/main.js @@ -517,6 +517,8 @@ function setupEventListeners() { // 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-action-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/ui.js b/static/js/app/ui.js index b07a221e..3a96b29e 100755 --- a/static/js/app/ui.js +++ b/static/js/app/ui.js @@ -1350,9 +1350,11 @@ function initRubberBandSelection() { container.addEventListener('mousedown', (e) => { // Only start if clicking empty area (not on a card, button, menu, input…) if (e.button !== 0) return; // left click only - if (e.target.closest('.file-card') || e.target.closest('.context-menu') || + if (e.target.closest('.file-card') || e.target.closest('.file-item') || + e.target.closest('.context-menu') || e.target.closest('.upload-dropdown') || e.target.closest('button') || - e.target.closest('input') || e.target.closest('.breadcrumb')) return; + e.target.closest('input') || e.target.closest('.breadcrumb') || + e.target.closest('.list-header')) return; active = true; startX = e.clientX; @@ -1420,9 +1422,16 @@ function initRubberBandSelection() { document.addEventListener('mouseup', () => { if (!active) return; active = false; + const hadSelection = selRect.style.display === 'block'; selRect.style.display = 'none'; // Update the batch bar after rubber band selection completes if (window.multiSelect) window.multiSelect._syncUI(); + // Suppress the click event that follows mouseup so the global + // deselect handler doesn't immediately clear the selection. + if (hadSelection) { + window.__rubberBandJustFinished = true; + requestAnimationFrame(() => { window.__rubberBandJustFinished = false; }); + } }); } diff --git a/static/js/features/files/multiSelect.js b/static/js/features/files/multiSelect.js index e9cd54d3..c2c14c80 100755 --- a/static/js/features/files/multiSelect.js +++ b/static/js/features/files/multiSelect.js @@ -532,6 +532,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 (this.hasSelection) this.clear(); });