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:
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -164,6 +164,9 @@
|
||||
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M3 4h18v2H3V4zm0 7h18v2H3v-2zm0 7h18v2H3v-2z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<button class="view-toggle-btn browser-refresh-btn" id="refreshBtn" onclick="refreshBrowser()" data-i18n-title="browser.refresh" title="Refresh">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M17.65 6.35A7.958 7.958 0 0012 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0112 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
|
||||
</button>
|
||||
<button class="browser-play-all-btn" id="playAllBtn" onclick="playAllFolder()" data-i18n-title="browser.play_all" title="Play All" style="display: none;">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M8 5v14l11-7z"/></svg>
|
||||
<span data-i18n="browser.play_all">Play All</span>
|
||||
|
||||
@@ -1873,12 +1873,17 @@ async function loadThumbnail(imgElement, fileName) {
|
||||
} else {
|
||||
// Fallback to icon (204 = no thumbnail available)
|
||||
const parent = imgElement.parentElement;
|
||||
const isList = parent.classList.contains('browser-list-icon');
|
||||
imgElement.remove();
|
||||
if (isList) {
|
||||
parent.textContent = '🎵';
|
||||
} else {
|
||||
const icon = document.createElement('div');
|
||||
icon.className = 'browser-icon';
|
||||
icon.textContent = '🎵';
|
||||
parent.insertBefore(icon, parent.firstChild);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading thumbnail:', error);
|
||||
imgElement.classList.remove('loading');
|
||||
@@ -2008,7 +2013,16 @@ function nextPage() {
|
||||
}
|
||||
}
|
||||
|
||||
function refreshBrowser() {
|
||||
if (currentFolderId) {
|
||||
browsePath(currentFolderId, currentPath, currentOffset);
|
||||
} else {
|
||||
loadMediaFolders();
|
||||
}
|
||||
}
|
||||
|
||||
function setViewMode(mode) {
|
||||
if (mode === viewMode) return;
|
||||
viewMode = mode;
|
||||
localStorage.setItem('mediaBrowser.viewMode', mode);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user