feat: square art in hero sections
This commit is contained in:
@@ -112,6 +112,9 @@ class PlexMetadata with MultiServerFields {
|
||||
// Clear logo URL (extracted from Image array, but serialized for offline storage)
|
||||
final String? clearLogo;
|
||||
|
||||
// Square background art URL (extracted from Image array, used for near-square hero layouts)
|
||||
final String? backgroundSquare;
|
||||
|
||||
/// Global unique identifier across all servers (serverId:ratingKey)
|
||||
String get globalKey => serverId != null ? buildGlobalKey(serverId!, ratingKey) : ratingKey;
|
||||
|
||||
@@ -186,6 +189,7 @@ class PlexMetadata with MultiServerFields {
|
||||
this.serverId,
|
||||
this.serverName,
|
||||
this.clearLogo,
|
||||
this.backgroundSquare,
|
||||
});
|
||||
|
||||
/// Create a copy of this metadata with optional field overrides
|
||||
@@ -242,6 +246,7 @@ class PlexMetadata with MultiServerFields {
|
||||
String? serverId,
|
||||
String? serverName,
|
||||
String? clearLogo,
|
||||
String? backgroundSquare,
|
||||
}) {
|
||||
return PlexMetadata(
|
||||
ratingKey: ratingKey ?? this.ratingKey,
|
||||
@@ -296,35 +301,48 @@ class PlexMetadata with MultiServerFields {
|
||||
serverId: serverId ?? this.serverId,
|
||||
serverName: serverName ?? this.serverName,
|
||||
clearLogo: clearLogo ?? this.clearLogo,
|
||||
backgroundSquare: backgroundSquare ?? this.backgroundSquare,
|
||||
);
|
||||
}
|
||||
|
||||
/// Extract clearLogo from Image array in raw JSON
|
||||
static String? _extractClearLogoFromJson(Map<String, dynamic> json) {
|
||||
/// Extract an image URL by type from the Image array in raw JSON
|
||||
static String? _extractImageFromJson(Map<String, dynamic> json, String imageType) {
|
||||
if (!json.containsKey('Image')) return null;
|
||||
|
||||
final images = json['Image'] as List?;
|
||||
if (images == null) return null;
|
||||
|
||||
for (var image in images) {
|
||||
if (image is Map && image['type'] == 'clearLogo') {
|
||||
if (image is Map && image['type'] == imageType) {
|
||||
return image['url'] as String?;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Create from JSON with clearLogo extracted from Image array
|
||||
/// Create from JSON with Image array fields extracted
|
||||
factory PlexMetadata.fromJsonWithImages(Map<String, dynamic> json) {
|
||||
// Extract clearLogo before parsing
|
||||
final clearLogoUrl = _extractClearLogoFromJson(json);
|
||||
// Add it to the json so it gets parsed
|
||||
final clearLogoUrl = _extractImageFromJson(json, 'clearLogo');
|
||||
if (clearLogoUrl != null) {
|
||||
json['clearLogo'] = clearLogoUrl;
|
||||
}
|
||||
final backgroundSquareUrl = _extractImageFromJson(json, 'backgroundSquare');
|
||||
if (backgroundSquareUrl != null) {
|
||||
json['backgroundSquare'] = backgroundSquareUrl;
|
||||
}
|
||||
return PlexMetadata.fromJson(json);
|
||||
}
|
||||
|
||||
/// Returns the best hero art path based on the container's aspect ratio.
|
||||
/// Uses backgroundSquare when the container is closer to 1:1 than 16:9.
|
||||
String? heroArt({required double containerAspectRatio}) {
|
||||
// Threshold = midpoint of 1:1 (1.0) and 16:9 (~1.78) ≈ 1.39
|
||||
if (containerAspectRatio < 1.39 && backgroundSquare != null) {
|
||||
return backgroundSquare;
|
||||
}
|
||||
return art;
|
||||
}
|
||||
|
||||
// Helper to get the display title (show name for episodes/seasons, title otherwise)
|
||||
String get displayTitle {
|
||||
final itemType = type.toLowerCase();
|
||||
|
||||
@@ -57,6 +57,7 @@ PlexMetadata _$PlexMetadataFromJson(Map<String, dynamic> json) => PlexMetadata(
|
||||
extraType: (json['extraType'] as num?)?.toInt(),
|
||||
primaryExtraKey: json['primaryExtraKey'] as String?,
|
||||
clearLogo: json['clearLogo'] as String?,
|
||||
backgroundSquare: json['backgroundSquare'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PlexMetadataToJson(PlexMetadata instance) => <String, dynamic>{
|
||||
@@ -110,4 +111,5 @@ Map<String, dynamic> _$PlexMetadataToJson(PlexMetadata instance) => <String, dyn
|
||||
'extraType': instance.extraType,
|
||||
'primaryExtraKey': instance.primaryExtraKey,
|
||||
'clearLogo': instance.clearLogo,
|
||||
'backgroundSquare': instance.backgroundSquare,
|
||||
};
|
||||
|
||||
@@ -1264,7 +1264,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
}
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
return _buildHeroItem(_onDeck[index]);
|
||||
return _buildHeroItem(_onDeck[index], heroHeight);
|
||||
},
|
||||
),
|
||||
// Bottom gradient that extends past hero bounds to ensure seamless blend
|
||||
@@ -1380,7 +1380,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeroItem(PlexMetadata heroItem) {
|
||||
Widget _buildHeroItem(PlexMetadata heroItem, double heroHeight) {
|
||||
final isEpisode = heroItem.isEpisode;
|
||||
final showName = heroItem.grandparentTitle ?? heroItem.title;
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
@@ -1406,7 +1406,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
// Background Image with fade/zoom animation and parallax
|
||||
if (heroItem.art != null || heroItem.grandparentArt != null)
|
||||
if (heroItem.art != null || heroItem.backgroundSquare != null || heroItem.grandparentArt != null)
|
||||
ClipRect(
|
||||
child: AnimatedBuilder(
|
||||
animation: _scrollController,
|
||||
@@ -1429,9 +1429,10 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
final client = _getClientForItem(heroItem);
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
final dpr = PlexImageHelper.effectiveDevicePixelRatio(context);
|
||||
final containerAspect = screenWidth / heroHeight;
|
||||
final imageUrl = PlexImageHelper.getOptimizedImageUrl(
|
||||
client: client,
|
||||
thumbPath: heroItem.art ?? heroItem.grandparentArt,
|
||||
thumbPath: heroItem.heroArt(containerAspectRatio: containerAspect) ?? heroItem.grandparentArt,
|
||||
maxWidth: mediaQuery.size.width,
|
||||
maxHeight: mediaQuery.size.height * 0.7,
|
||||
devicePixelRatio: dpr,
|
||||
|
||||
@@ -1853,14 +1853,17 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> with WatchStateAw
|
||||
SizedBox(
|
||||
height: headerHeight,
|
||||
width: double.infinity,
|
||||
child: metadata.art != null
|
||||
child: (metadata.art != null || metadata.backgroundSquare != null)
|
||||
? Builder(
|
||||
builder: (context) {
|
||||
final containerAspect = size.width / headerHeight;
|
||||
final heroArtPath = metadata.heroArt(containerAspectRatio: containerAspect);
|
||||
|
||||
// Check for offline local file first
|
||||
if (widget.isOffline && widget.metadata.serverId != null) {
|
||||
final localPath = context.read<DownloadProvider>().getArtworkLocalPath(
|
||||
widget.metadata.serverId!,
|
||||
metadata.art,
|
||||
heroArtPath,
|
||||
);
|
||||
if (localPath != null && File(localPath).existsSync()) {
|
||||
return Image.file(
|
||||
@@ -1879,7 +1882,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> with WatchStateAw
|
||||
final dpr = PlexImageHelper.effectiveDevicePixelRatio(context);
|
||||
final imageUrl = PlexImageHelper.getOptimizedImageUrl(
|
||||
client: client,
|
||||
thumbPath: metadata.art,
|
||||
thumbPath: heroArtPath,
|
||||
maxWidth: mediaQuery.size.width,
|
||||
maxHeight: mediaQuery.size.height * 0.6,
|
||||
devicePixelRatio: dpr,
|
||||
|
||||
@@ -916,6 +916,11 @@ class DownloadManagerService {
|
||||
await _downloadSingleArtwork(serverId, metadata.art!, client);
|
||||
}
|
||||
|
||||
// Download square background art
|
||||
if (metadata.backgroundSquare != null) {
|
||||
await _downloadSingleArtwork(serverId, metadata.backgroundSquare!, client);
|
||||
}
|
||||
|
||||
// Store thumb reference in database (primary artwork for display)
|
||||
await _database.updateArtworkPaths(globalKey: globalKey, thumbPath: metadata.thumb);
|
||||
|
||||
@@ -977,6 +982,11 @@ class DownloadManagerService {
|
||||
if (metadata.art != null) {
|
||||
await _downloadSingleArtwork(serverId, metadata.art!, client);
|
||||
}
|
||||
|
||||
// Download square background art
|
||||
if (metadata.backgroundSquare != null) {
|
||||
await _downloadSingleArtwork(serverId, metadata.backgroundSquare!, client);
|
||||
}
|
||||
}
|
||||
|
||||
/// Download chapter thumbnail images for a media item
|
||||
|
||||
Reference in New Issue
Block a user