diff --git a/lib/screens/queue_tab_item_widgets.dart b/lib/screens/queue_tab_item_widgets.dart index 14e7e95a..91caf9bb 100644 --- a/lib/screens/queue_tab_item_widgets.dart +++ b/lib/screens/queue_tab_item_widgets.dart @@ -644,7 +644,6 @@ extension _QueueTabItemWidgets on _QueueTabState { width: size, height: size, memCacheWidth: cacheSize, - memCacheHeight: cacheSize, placeholder: (context, url) => buildPlaceholder(), errorWidget: (context, url, error) => buildPlaceholder(), ); @@ -680,7 +679,6 @@ extension _QueueTabItemWidgets on _QueueTabState { height: size, fit: BoxFit.cover, cacheWidth: cacheSize, - cacheHeight: cacheSize, gaplessPlayback: true, frameBuilder: (context, child, frame, wasSynchronouslyLoaded) => fadeInFileImage(child, frame, wasSynchronouslyLoaded), @@ -696,7 +694,6 @@ extension _QueueTabItemWidgets on _QueueTabState { width: size, height: size, memCacheWidth: cacheSize, - memCacheHeight: cacheSize, borderRadius: BorderRadius.circular(8), placeholder: (context, url) => buildPlaceholder(), errorWidget: (context, url, error) => buildPlaceholder(), @@ -714,7 +711,6 @@ extension _QueueTabItemWidgets on _QueueTabState { height: size, fit: BoxFit.cover, cacheWidth: cacheSize, - cacheHeight: cacheSize, gaplessPlayback: true, frameBuilder: (context, child, frame, wasSynchronouslyLoaded) => fadeInFileImage(child, frame, wasSynchronouslyLoaded), diff --git a/lib/widgets/cached_cover_image.dart b/lib/widgets/cached_cover_image.dart index 5cd79d3a..bbfbd055 100644 --- a/lib/widgets/cached_cover_image.dart +++ b/lib/widgets/cached_cover_image.dart @@ -56,9 +56,15 @@ class CachedCoverImage extends StatelessWidget { Widget _buildImage(BuildContext context, BoxConstraints constraints) { var autoMemCacheWidth = - memCacheWidth ?? _cacheExtentForLogicalSize(context, width); - var autoMemCacheHeight = - memCacheHeight ?? _cacheExtentForLogicalSize(context, height); + memCacheWidth ?? + (memCacheHeight == null + ? _cacheExtentForLogicalSize(context, width) + : null); + // ResizeImage stretches the decoded bitmap when both axes are supplied. + // Keep one decode axis so BoxFit can crop the original proportions. + var autoMemCacheHeight = autoMemCacheWidth == null + ? memCacheHeight ?? _cacheExtentForLogicalSize(context, height) + : null; if (autoMemCacheWidth == null && autoMemCacheHeight == null) { // Infer one axis to preserve the source aspect ratio and respect any // explicit decode override used by large artwork/header consumers. diff --git a/test/cached_cover_decode_test.dart b/test/cached_cover_decode_test.dart index 1127d8af..d46d3c09 100644 --- a/test/cached_cover_decode_test.dart +++ b/test/cached_cover_decode_test.dart @@ -45,6 +45,93 @@ void main() { ); } + testWidgets('explicit height decode keeps one axis even with square bounds', ( + tester, + ) async { + await tester.pumpWidget( + const MaterialApp( + home: Center( + child: CachedCoverImage( + imageUrl: 'https://example.invalid/height-cover.png', + width: 56, + height: 56, + memCacheHeight: 128, + ), + ), + ), + ); + final image = tester.widget( + find.byType(CachedNetworkImage), + ); + expect(image.memCacheWidth, isNull); + expect(image.memCacheHeight, 128); + await tester.pumpWidget(const SizedBox()); + }); + + for (final override in [false, true]) { + testWidgets( + 'playlist thumbnail preserves network proportions ($override)', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: MediaQuery( + data: const MediaQueryData(devicePixelRatio: 2), + child: Center( + child: SizedBox.square( + dimension: 56, + child: LocalOrNetworkCoverImage( + url: 'https://example.invalid/playlist-cover.png', + width: 56, + height: 56, + networkCacheWidth: override ? 112 : null, + placeholder: (_) => const SizedBox(), + ), + ), + ), + ), + ), + ); + final image = tester.widget( + find.byType(CachedNetworkImage), + ); + expect(image.memCacheWidth, 112); + expect(image.memCacheHeight, isNull); + expect(image.fit, BoxFit.cover); + expect( + tester.getSize(find.byType(LocalOrNetworkCoverImage)), + const Size(56, 56), + ); + // Exercise the same Flutter decode sizing used by CachedNetworkImage, + // with non-square artwork rather than checking only the widget bounds. + final directory = await tester.runAsync(() async { + final directory = await Directory.systemTemp.createTemp( + 'playlist-cover-', + ); + await _writeCover(directory, const Size(320, 160)); + return directory; + }); + try { + final decoded = await tester.runAsync(() async { + final bytes = await File( + '${directory!.path}/cover.png', + ).readAsBytes(); + return _decodedImageSize( + ResizeImage.resizeIfNeeded( + image.memCacheWidth, + image.memCacheHeight, + MemoryImage(bytes), + ), + ); + }); + expect(decoded, const Size(112, 56)); + } finally { + await tester.runAsync(() => directory!.delete(recursive: true)); + } + await tester.pumpWidget(const SizedBox()); + }, + ); + } + for (final scenario in [ ( name: 'explicit row size',