fix: downsample local artwork in offline grids

This commit is contained in:
edde746
2026-04-16 22:44:01 +02:00
parent 157669bd76
commit ddb4a91b08
2 changed files with 52 additions and 25 deletions
+5 -4
View File
@@ -1,4 +1,3 @@
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
@@ -408,10 +407,12 @@ class _EpisodeCardState extends State<EpisodeCard> {
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)),
);
}
+47 -21
View File
@@ -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