fix(libraries): give square music grids an inter-card gutter

Square-card (music) grids had zero delegate spacing - the only
separation was each card's internal 3px padding, and square shapes are
excluded from the full-card spacing path, so playlist and album tiles
sat nearly edge to edge. Square grids now get an 8px cross/main gutter
(automotive keeps its larger spacing); poster, list, and full-card
layouts are unchanged.
This commit is contained in:
edde746
2026-08-17 22:44:12 +02:00
parent cff6666219
commit 1bdeca3b5f
4 changed files with 92 additions and 3 deletions
+5
View File
@@ -51,6 +51,11 @@ class GridLayoutConstants {
/// reserves ([posterAspectRatio] adds 0.3 to the 2:3 image's denominator).
static const double squareGridCellAspectRatio = 2 / 2.3;
/// Inter-card gutter for square (music) grids on touch platforms. Cards
/// already carry their own 3px internal padding, so the net visual gutter
/// is ~14px horizontal. Automotive keeps its larger [crossAxisSpacing].
static const double squareGridSpacing = 8.0;
static double get crossAxisSpacing => PlatformDetector.isAutomotive() ? 24 : 0;
static double get mainAxisSpacing => PlatformDetector.isAutomotive() ? 24 : 0;
+21 -3
View File
@@ -36,9 +36,22 @@ class MediaGridDelegate {
return maxCrossAxisExtent;
}
static double spacingFor({required BuildContext context, bool fullBleedImage = false}) {
/// Inter-cell gutter for the resolved shape. Square (music) grids get
/// [GridLayoutConstants.squareGridSpacing] so cards have breathing room;
/// every other shape keeps the platform default (0, or 24 on automotive).
/// Full-bleed TV grids use the scaled full-card gutter.
static double spacingFor({
required BuildContext context,
bool useWideAspectRatio = false,
bool fullBleedImage = false,
CardShape? shape,
}) {
if (PlatformDetector.isAutomotive()) return GridLayoutConstants.crossAxisSpacing;
if (!fullBleedImage) return GridLayoutConstants.crossAxisSpacing;
if (!fullBleedImage) {
return _resolveShape(shape, useWideAspectRatio) == CardShape.square
? GridLayoutConstants.squareGridSpacing
: GridLayoutConstants.crossAxisSpacing;
}
return GridLayoutConstants.fullCardGridSpacingForScale(TvLayoutConstants.scaleOf(context));
}
@@ -101,7 +114,12 @@ class MediaGridGeometry {
bool fullBleedImage = false,
CardShape? shape,
}) {
final spacing = MediaGridDelegate.spacingFor(context: context, fullBleedImage: fullBleedImage);
final spacing = MediaGridDelegate.spacingFor(
context: context,
useWideAspectRatio: useWideAspectRatio,
fullBleedImage: fullBleedImage,
shape: shape,
);
final aspectRatio = MediaGridDelegate.aspectRatioFor(
useWideAspectRatio: useWideAspectRatio,
fullBleedImage: fullBleedImage,
@@ -20,6 +20,7 @@ import 'package:plezy/services/multi_server_manager.dart';
import 'package:plezy/services/plex_client.dart';
import 'package:plezy/services/plex_api_cache.dart';
import 'package:plezy/services/settings_service.dart';
import 'package:plezy/utils/layout_constants.dart';
import 'package:plezy/utils/platform_detector.dart';
import 'package:plezy/widgets/card_inflation_budget.dart';
import 'package:plezy/widgets/focusable_media_card.dart';
@@ -182,6 +183,13 @@ void main() {
.every((card) => card.cardShapeOverride == CardShape.square),
isTrue,
);
// Square grids keep their square gutter spacing even on TV full-card
// layout (which is disabled for square shapes).
final gridDelegate =
tester.widget<SliverGrid>(find.byType(SliverGrid)).gridDelegate as SliverGridDelegateWithMaxCrossAxisExtent;
expect(gridDelegate.crossAxisSpacing, GridLayoutConstants.squareGridSpacing);
expect(gridDelegate.mainAxisSpacing, GridLayoutConstants.squareGridSpacing);
});
}
+58
View File
@@ -60,6 +60,64 @@ void main() {
expect(MediaGridDelegate.aspectRatioFor(useWideAspectRatio: true), GridLayoutConstants.episodeGridCellAspectRatio);
});
testWidgets('square grid delegates use square gutters, others unchanged', (tester) async {
tester.view.devicePixelRatio = 1.0;
tester.view.physicalSize = const Size(1280, 720);
addTearDown(() {
tester.view.resetDevicePixelRatio();
tester.view.resetPhysicalSize();
});
SliverGridDelegateWithMaxCrossAxisExtent? square;
SliverGridDelegateWithMaxCrossAxisExtent? poster;
SliverGridDelegateWithMaxCrossAxisExtent? wide;
SliverGridDelegateWithMaxCrossAxisExtent? fullBleed;
await tester.pumpWidget(
_TestApp(
child: Builder(
builder: (context) {
square = MediaGridGeometry.resolve(
context: context,
crossAxisExtent: 1280,
density: LibraryDensity.defaultValue,
shape: CardShape.square,
).delegate;
poster = MediaGridGeometry.resolve(
context: context,
crossAxisExtent: 1280,
density: LibraryDensity.defaultValue,
).delegate;
wide = MediaGridGeometry.resolve(
context: context,
crossAxisExtent: 1280,
density: LibraryDensity.defaultValue,
useWideAspectRatio: true,
).delegate;
fullBleed = MediaGridGeometry.resolve(
context: context,
crossAxisExtent: 1280,
density: LibraryDensity.defaultValue,
fullBleedImage: true,
).delegate;
return const SizedBox.shrink();
},
),
),
);
// Square (music) grids get breathing room.
expect(square!.crossAxisSpacing, GridLayoutConstants.squareGridSpacing);
expect(square!.mainAxisSpacing, GridLayoutConstants.squareGridSpacing);
// Poster and wide grids keep the platform default (0 off-automotive).
expect(poster!.crossAxisSpacing, GridLayoutConstants.crossAxisSpacing);
expect(poster!.mainAxisSpacing, GridLayoutConstants.crossAxisSpacing);
expect(wide!.crossAxisSpacing, GridLayoutConstants.crossAxisSpacing);
expect(wide!.mainAxisSpacing, GridLayoutConstants.crossAxisSpacing);
// Full-bleed TV gutters are unchanged.
expect(fullBleed!.crossAxisSpacing, GridLayoutConstants.fullCardGridSpacingForScale(0.85));
expect(fullBleed!.mainAxisSpacing, fullBleed!.crossAxisSpacing);
});
test('list layout sizes square cards 1:1', () {
final base = MediaCardListLayout.basePosterWidth(LibraryDensity.defaultValue);
expect(MediaCardListLayout.posterWidth(density: LibraryDensity.defaultValue, shape: CardShape.square), base);