From 8db40d3ee9ca59ae45523259277b43032bb5830d Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Mon, 9 Feb 2026 02:10:22 +0300 Subject: [PATCH] UI polish: refresh button, negative thumbnail cache, and style fixes - Add refresh button to browser toolbar to re-fetch current folder - Cache "no thumbnail" results to avoid repeated slow SMB lookups - Fix list view fallback icon sizing for files without album art - Fix view toggle button hover (no background/scale on hover) - Skip re-render when clicking already-active view mode button Co-Authored-By: Claude Opus 4.6 --- media_server/services/thumbnail_service.py | 44 +++++++++++++++++++--- media_server/static/css/styles.css | 8 +++- media_server/static/index.html | 3 ++ media_server/static/js/app.js | 22 +++++++++-- 4 files changed, 65 insertions(+), 12 deletions(-) diff --git a/media_server/services/thumbnail_service.py b/media_server/services/thumbnail_service.py index dc13808..cb6c3f2 100644 --- a/media_server/services/thumbnail_service.py +++ b/media_server/services/thumbnail_service.py @@ -51,6 +51,9 @@ class ThumbnailService: absolute_path = str(file_path.resolve()) return hashlib.sha256(absolute_path.encode()).hexdigest() + # Sentinel value indicating "no thumbnail available" is cached + NO_THUMBNAIL = b"" + @staticmethod def get_cached_thumbnail(file_path: Path, size: str) -> Optional[bytes]: """Get cached thumbnail if valid. @@ -60,11 +63,24 @@ class ThumbnailService: size: Thumbnail size ("small" or "medium"). Returns: - Thumbnail bytes if cached and valid, None otherwise. + Thumbnail bytes if cached, empty bytes if cached as "no thumbnail", + None if not cached. """ cache_dir = ThumbnailService.get_cache_dir() cache_key = ThumbnailService.get_cache_key(file_path) - cache_path = cache_dir / cache_key / f"{size}.jpg" + cache_folder = cache_dir / cache_key + cache_path = cache_folder / f"{size}.jpg" + no_thumb_path = cache_folder / ".no_thumbnail" + + # Check negative cache first (no thumbnail available) + if no_thumb_path.exists(): + try: + file_mtime = file_path.stat().st_mtime + cache_mtime = no_thumb_path.stat().st_mtime + if file_mtime <= cache_mtime: + return ThumbnailService.NO_THUMBNAIL + except (OSError, PermissionError): + pass if not cache_path.exists(): return None @@ -109,6 +125,20 @@ class ThumbnailService: except (OSError, PermissionError) as e: logger.error(f"Error caching thumbnail: {e}") + @staticmethod + def cache_no_thumbnail(file_path: Path) -> None: + """Cache that a file has no thumbnail (negative cache).""" + cache_dir = ThumbnailService.get_cache_dir() + cache_key = ThumbnailService.get_cache_key(file_path) + cache_folder = cache_dir / cache_key + cache_folder.mkdir(parents=True, exist_ok=True) + + try: + (cache_folder / ".no_thumbnail").touch() + logger.debug(f"Cached no-thumbnail for {file_path.name}") + except (OSError, PermissionError) as e: + logger.error(f"Error caching no-thumbnail marker: {e}") + @staticmethod def generate_audio_thumbnail(file_path: Path, size: str) -> Optional[bytes]: """Generate thumbnail from audio file (extract album art). @@ -277,10 +307,10 @@ class ThumbnailService: if size not in THUMBNAIL_SIZES: size = "medium" - # Check cache first + # Check cache first (returns bytes, empty bytes for negative cache, or None) cached = ThumbnailService.get_cached_thumbnail(file_path, size) - if cached: - return cached + if cached is not None: + return cached if cached else None # Generate thumbnail based on file type suffix = file_path.suffix.lower() @@ -299,9 +329,11 @@ class ThumbnailService: # Video files - already async thumbnail_data = await ThumbnailService.generate_video_thumbnail(file_path, size) - # Cache if generated successfully + # Cache result (positive or negative) if thumbnail_data: ThumbnailService.cache_thumbnail(file_path, size, thumbnail_data) + else: + ThumbnailService.cache_no_thumbnail(file_path) return thumbnail_data diff --git a/media_server/static/css/styles.css b/media_server/static/css/styles.css index fabfd81..2423522 100644 --- a/media_server/static/css/styles.css +++ b/media_server/static/css/styles.css @@ -1214,8 +1214,8 @@ .view-toggle-btn:hover { color: var(--text-primary); - background: var(--border); - transform: none; + background: transparent !important; + transform: none !important; } .view-toggle-btn.active { @@ -1223,6 +1223,10 @@ background: var(--bg-primary); } +.browser-refresh-btn { + margin-left: 0.35rem; +} + .view-toggle-btn svg { fill: currentColor; } diff --git a/media_server/static/index.html b/media_server/static/index.html index b77e79e..7d36213 100644 --- a/media_server/static/index.html +++ b/media_server/static/index.html @@ -164,6 +164,9 @@ +