From 310d4b46b23218ee02133b3edc90b5318d8acc8a Mon Sep 17 00:00:00 2001 From: Micah Morrison Date: Sun, 26 Apr 2026 15:11:24 -0400 Subject: [PATCH] Show season posters on tab in show view --- lib/screens/media_detail_screen.dart | 34 ++++++++++++++++++++++++ lib/widgets/focusable_tab_chip.dart | 39 ++++++++++++++++++++++------ 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/lib/screens/media_detail_screen.dart b/lib/screens/media_detail_screen.dart index fa5da6df..ae0552d7 100644 --- a/lib/screens/media_detail_screen.dart +++ b/lib/screens/media_detail_screen.dart @@ -1796,6 +1796,39 @@ class _MediaDetailScreenState extends State final season = _seasons[index]; final contextMenuKey = _seasonContextMenuKeys.putIfAbsent(index, () => GlobalKey()); Offset? tapPosition; + final posterPath = season.thumb; + Widget? topImage; + if (posterPath != null && posterPath.isNotEmpty) { + const posterWidth = 72.0; + const posterHeight = 108.0; + final dpr = PlexImageHelper.effectiveDevicePixelRatio(context); + final client = _getClientForMetadata(context); + final imageUrl = PlexImageHelper.getOptimizedImageUrl( + client: client, + thumbPath: posterPath, + maxWidth: posterWidth, + maxHeight: posterHeight, + devicePixelRatio: dpr, + imageType: ImageType.poster, + ); + final (memWidth, _) = PlexImageHelper.getMemCacheDimensions( + displayWidth: (posterWidth * dpr).round(), + displayHeight: (posterHeight * dpr).round(), + imageType: ImageType.poster, + ); + topImage = SizedBox( + width: posterWidth, + height: posterHeight, + child: CachedNetworkImage( + imageUrl: imageUrl, + cacheManager: PlexImageCacheManager.instance, + fit: BoxFit.cover, + memCacheWidth: memWidth, + placeholder: (context, url) => const PlaceholderContainer(), + errorWidget: (context, url, error) => const PlaceholderContainer(), + ), + ); + } return Padding( padding: const EdgeInsets.only(right: 8), child: MediaContextMenu( @@ -1819,6 +1852,7 @@ class _MediaDetailScreenState extends State child: FocusableTabChip( label: season.title!, isSelected: index == _selectedSeasonIndex, + topImage: topImage, focusNode: _seasonTabFocusNodes.length > index ? _seasonTabFocusNodes[index] : null, onSelect: () { if (index == _selectedSeasonIndex) return; diff --git a/lib/widgets/focusable_tab_chip.dart b/lib/widgets/focusable_tab_chip.dart index 6639babc..0f325b12 100644 --- a/lib/widgets/focusable_tab_chip.dart +++ b/lib/widgets/focusable_tab_chip.dart @@ -36,6 +36,10 @@ class FocusableTabChip extends StatefulWidget { /// Called when SELECT key is held (D-pad long press). final VoidCallback? onLongPress; + /// Optional image to show above the label (e.g. a poster). + /// When provided, the chip lays out vertically with image on top, label below. + final Widget? topImage; + const FocusableTabChip({ super.key, required this.label, @@ -47,6 +51,7 @@ class FocusableTabChip extends StatefulWidget { this.onNavigateDown, this.onBack, this.onLongPress, + this.topImage, }); @override @@ -123,20 +128,38 @@ class _FocusableTabChipState extends State with FocusableChipS final isHighlighted = showFocus || widget.isSelected; + final label = Text( + widget.label, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: foregroundColor, + fontWeight: isHighlighted ? FontWeight.w600 : FontWeight.normal, + ), + ); + + final hasImage = widget.topImage != null; return FocusBuilders.buildFocusableChip( context: context, focusNode: focusNode, onKeyEvent: _handleKeyEvent, onTap: widget.onSelect, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: hasImage + ? const EdgeInsets.all(8) + : const EdgeInsets.symmetric(horizontal: 16, vertical: 8), backgroundColor: backgroundColor, - child: Text( - widget.label, - style: Theme.of(context).textTheme.labelLarge?.copyWith( - color: foregroundColor, - fontWeight: isHighlighted ? FontWeight.w600 : FontWeight.normal, - ), - ), + borderRadius: hasImage ? 12 : 20, + child: hasImage + ? Column( + mainAxisSize: MainAxisSize.min, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(6), + child: widget.topImage!, + ), + const SizedBox(height: 6), + label, + ], + ) + : label, ); } }