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 <noreply@anthropic.com>
This commit is contained in:
2026-02-09 02:10:22 +03:00
parent f275240e59
commit 8db40d3ee9
4 changed files with 65 additions and 12 deletions

View File

@@ -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