diff --git a/lib/widgets/episode_card.dart b/lib/widgets/episode_card.dart index 4eda99cd..3b6738d2 100644 --- a/lib/widgets/episode_card.dart +++ b/lib/widgets/episode_card.dart @@ -1,4 +1,3 @@ -import 'dart:io'; import 'dart:ui'; import 'package:flutter/material.dart'; @@ -408,10 +407,12 @@ class _EpisodeCardState extends State { Widget _buildEpisodeThumbnail() { if (widget.isOffline && widget.localPosterPath != null) { - return Image.file( - File(widget.localPosterPath!), + return PlexOptimizedImage.thumb( + client: null, + imagePath: null, + localFilePath: widget.localPosterPath, fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) => + errorWidget: (context, url, error) => const PlaceholderContainer(child: AppIcon(Symbols.movie_rounded, fill: 1, size: 32)), ); } diff --git a/lib/widgets/plex_optimized_image.dart b/lib/widgets/plex_optimized_image.dart index f3a40c05..20b82479 100644 --- a/lib/widgets/plex_optimized_image.dart +++ b/lib/widgets/plex_optimized_image.dart @@ -261,32 +261,21 @@ class PlexOptimizedImage extends StatelessWidget { @override Widget build(BuildContext context) { - // Check for local file first - if (localFilePath != null) { - final file = File(localFilePath!); - if (file.existsSync()) { - return blurArtwork( - Image.file( - file, - width: width, - height: height, - fit: fit, - filterQuality: filterQuality, - alignment: alignment, - errorBuilder: (context, error, stackTrace) => _buildErrorWidget(context, error), - ), - ); - } - } + final localFile = localFilePath != null ? File(localFilePath!) : null; + final hasLocal = localFile != null && localFile.existsSync(); - // Return empty container if no image path - if (imagePath == null || imagePath!.isEmpty) { + // No local file and no network path → fallback + if (!hasLocal && (imagePath == null || imagePath!.isEmpty)) { return _buildFallback(context); } // Fast path: skip LayoutBuilder when both dimensions are explicitly known if (_hasKnownDimensions) { - return blurArtwork(_buildCachedImage(context, width!, height!)); + return blurArtwork( + hasLocal + ? _buildLocalFileImage(context, localFile, width!, height!) + : _buildCachedImage(context, width!, height!), + ); } return blurArtwork( @@ -294,12 +283,49 @@ class PlexOptimizedImage extends StatelessWidget { builder: (context, constraints) { final effectiveWidth = _resolvedDimension(width, constraints.maxWidth, 300.0); final effectiveHeight = _resolvedDimension(height, constraints.maxHeight, 450.0); - return _buildCachedImage(context, effectiveWidth, effectiveHeight); + return hasLocal + ? _buildLocalFileImage(context, localFile, effectiveWidth, effectiveHeight) + : _buildCachedImage(context, effectiveWidth, effectiveHeight); }, ), ); } + Widget _buildLocalFileImage( + BuildContext context, + File file, + double effectiveWidth, + double effectiveHeight, + ) { + final dpr = PlexImageHelper.effectiveDevicePixelRatio(context); + final scaledWidth = effectiveWidth * dpr; + final scaledHeight = effectiveHeight * dpr; + final (_, memHeight) = PlexImageHelper.getMemCacheDimensions( + displayWidth: scaledWidth.isFinite && scaledWidth > 0 ? scaledWidth.round() : 0, + displayHeight: scaledHeight.isFinite && scaledHeight > 0 ? scaledHeight.round() : 0, + imageType: imageType, + ); + + return Image.file( + file, + width: width, + height: height, + // Only cacheHeight: leaving cacheWidth null preserves decode aspect + // ratio, mirroring the network branch which only passes maxHeight to + // CachedNetworkImageProvider. + cacheHeight: memHeight > 0 ? memHeight : null, + fit: fit, + filterQuality: filterQuality, + alignment: alignment, + errorBuilder: (context, error, stackTrace) { + if (errorWidget != null) { + return errorWidget!(context, file.path, error); + } + return _buildErrorWidget(context, error); + }, + ); + } + static double _resolvedDimension(double? explicit, double constraintMax, double fallback) { // Pick the explicit size when it's a finite positive number, otherwise // fall back to the constraint or a sensible default so we don't end up