fix(artwork): preserve thumbnail aspect ratio during decoding

This commit is contained in:
zarzet committed 2026-10-02 13:58:13 +07:00
1 parent 26d713915f
commit c91d12dbf0
3 files changed
+96 -7

No files matched your search

-4
View File
@@ -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),
+9 -3
View File
@@ -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.
+87
View File
@@ -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<CachedNetworkImage>(
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<CachedNetworkImage>(
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',