fix(artwork): show square background art on portrait heroes

Cycling backdrops reach a fallback path only once every rotating path
has failed to load, but every hero passed the rotation-agnostic backdrop
list as the rotation set and the aspect-ordered candidates as the
fallback. One servable wide backdrop was therefore enough to hide the
square background for good, so phone detail and Discover heroes
cover-fitted a 16:9 backdrop into a portrait box instead of showing the
square image Plex supplies.

Give the rotation set the same aspect-aware preference the candidate
list already has: near-square containers rotate the square background
alone and keep the backdrops behind it as fallbacks.

close #1700
This commit is contained in:
edde746
2026-07-28 10:31:18 +02:00
parent 9a0e96114f
commit 3b019c8fe2
6 changed files with 126 additions and 6 deletions
+26 -2
View File
@@ -14,6 +14,11 @@ import 'media_version.dart';
part 'media_item.freezed.dart';
part 'media_item.g.dart';
/// Container aspect ratio below which a hero prefers square background art.
/// A 16:9 backdrop only cover-fits a taller box by discarding most of the
/// frame, so portrait phone/tablet heroes read better with the square image.
const double _squareHeroAspectRatio = 1.39;
/// Backend-neutral media item shape used by UI, providers, persistence, and
/// playback. Concrete variants retain backend-only fields without forcing the
/// rest of the app to traffic in Plex/Jellyfin DTOs.
@@ -676,14 +681,33 @@ sealed class MediaItem with _$MediaItem {
return resolvedBackdropPaths;
}
/// The backdrops a hero may rotate through in a container of
/// [containerAspectRatio].
///
/// `CyclingMediaBackdrop` cycles its rotation set indefinitely and reaches a
/// fallback path only once every rotating path has failed to load, so the
/// rotation set must hold whatever [heroArtCandidates] prefers — otherwise
/// one servable wide backdrop hides the square background for good and a
/// near-square hero is stuck with a cropped 16:9 frame. Such containers
/// therefore rotate the square background alone, which is to say they hold
/// still.
List<String> heroRotationPaths({required double containerAspectRatio}) {
if (containerAspectRatio < _squareHeroAspectRatio) {
final square = backgroundSquarePath;
if (square != null && square.isNotEmpty) return [square];
}
return heroBackdropPaths;
}
/// Returns hero art candidates in display-preference order.
List<String> heroArtCandidates({required double containerAspectRatio}) {
final own = resolvedBackdropPaths;
final inherited = resolvedGrandparentBackdropPaths;
final isNearSquare = containerAspectRatio < _squareHeroAspectRatio;
final preferred = switch (kind) {
MediaKind.episode when containerAspectRatio < 1.39 => <String?>[backgroundSquarePath, ...inherited, ...own],
MediaKind.episode when isNearSquare => <String?>[backgroundSquarePath, ...inherited, ...own],
MediaKind.episode => <String?>[...inherited, ...own, backgroundSquarePath],
_ when containerAspectRatio < 1.39 => <String?>[backgroundSquarePath, ...own],
_ when isNearSquare => <String?>[backgroundSquarePath, ...own],
_ => <String?>[...own, backgroundSquarePath],
};