From 719f317b5df47faca33ed2e2b4201b574d22da2a Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Fri, 26 Jun 2026 07:16:39 +0200 Subject: [PATCH] feat: show genres on the media detail screen close #1390 --- lib/screens/media_detail_screen.dart | 76 +++++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 7 deletions(-) diff --git a/lib/screens/media_detail_screen.dart b/lib/screens/media_detail_screen.dart index d60becd3..cb5226e6 100644 --- a/lib/screens/media_detail_screen.dart +++ b/lib/screens/media_detail_screen.dart @@ -3126,9 +3126,14 @@ class _MediaDetailScreenState extends State // Main content SliverToBoxAdapter( child: Padding( - padding: .symmetric( - horizontal: isTv ? TvLayoutConstants.horizontalInset : 16, - vertical: isTv ? 8 : 16, + // Reduced top inset keeps the Overview/first section + // tight under the hero's action row (the hero already + // contributes its own bottom inset above this). + padding: .fromLTRB( + isTv ? TvLayoutConstants.horizontalInset : 16, + isTv ? 8 : 4, + isTv ? TvLayoutConstants.horizontalInset : 16, + isTv ? 8 : 16, ), child: Column( crossAxisAlignment: .start, @@ -3446,22 +3451,29 @@ class _MediaDetailScreenState extends State final availableHeight = constraints.maxHeight.isFinite ? constraints.maxHeight : 264.0; final desiredLogoHeight = 220 * scale; - final minLogoHeight = 72 * scale; + final minLogoHeight = 60 * scale; final desiredLogoWidth = 790 * scale; final metadataLineHeight = 22 * scale; + final genreLineHeight = 22 * scale; + final genreGap = 6 * scale; final logoMetadataGap = 10 * scale; - final summaryGap = 10 * scale; + final summaryGap = 6 * scale; final summaryFontSize = availableHeight < 260 * scale ? 16.2 * scale : 18 * scale; final summaryLineHeight = summaryFontSize * 1.35; final actionHeight = _tvDetailActionSize * scale; final actionGap = 12 * scale; final hasDescription = description != null && description.isNotEmpty; + // Genres come from the show/movie, not the focused episode, so the line + // stays stable as episode rows gain focus. + final genres = metadata.genres ?? const []; + final genreBlockHeight = genres.isEmpty ? 0.0 : genreGap + genreLineHeight; var summaryMaxLines = 0; var logoHeight = 0.0; for (var lines = hasDescription ? 3 : 0; lines >= 0; lines--) { final descriptionHeight = lines > 0 ? summaryGap + (summaryLineHeight * lines) : 0.0; - final reservedHeight = logoMetadataGap + metadataLineHeight + descriptionHeight + actionGap + actionHeight; + final reservedHeight = + logoMetadataGap + metadataLineHeight + genreBlockHeight + descriptionHeight + actionGap + actionHeight; final remainingForLogo = availableHeight - reservedHeight; if (remainingForLogo >= minLogoHeight || lines == 0) { summaryMaxLines = lines; @@ -3475,6 +3487,7 @@ class _MediaDetailScreenState extends State final contentHeight = (showLogo ? logoHeight + logoMetadataGap : 0) + metadataLineHeight + + genreBlockHeight + descriptionHeight + actionGap + actionHeight; @@ -3513,6 +3526,26 @@ class _MediaDetailScreenState extends State height: metadataLineHeight, child: Align(alignment: .centerLeft, child: _buildTvDetailMetadataLine(context, metadata, scale)), ), + if (genres.isNotEmpty) ...[ + SizedBox(height: genreGap), + SizedBox( + height: genreLineHeight, + child: Align( + alignment: .centerLeft, + child: Text( + genres.join(' • '), + maxLines: 1, + overflow: .ellipsis, + style: TextStyle( + color: mutedForegroundColor, + fontSize: 16 * scale, + fontWeight: .w600, + letterSpacing: 0.1, + ), + ), + ), + ), + ], if (hasDescription && summaryMaxLines > 0) ...[ SizedBox(height: summaryGap), SizedBox( @@ -4106,8 +4139,12 @@ class _MediaDetailScreenState extends State bottom: 16, left: 0, right: 0, + // bottom: false — the hero is the top sliver, so the bottom safe-area + // inset would otherwise push the action row far up off the hero edge. + // Left/right stay enabled for the landscape notch. child: SafeArea( top: false, + bottom: false, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: _buildHeroHeaderContent(context, metadata), @@ -4135,13 +4172,24 @@ class _MediaDetailScreenState extends State for (final label in buildMediaQualityLabels(metadata)) _buildMetadataChip(label), ..._buildRatingChips(metadata), ]; + // Genres render on their own line below the metadata chips. + final genreChips = [for (final genre in metadata.genres ?? const []) _buildMetadataChip(genre)]; final showActions = availableHeight >= actionHeight; final remainingAfterActions = availableHeight - (showActions ? actionHeight : 0); final showChips = chips.isNotEmpty && remainingAfterActions >= 88; final chipHeight = showChips ? (remainingAfterActions >= 170 ? 68.0 : 32.0) : 0.0; final chipActionGap = showChips && showActions ? (availableHeight < 180 ? 8.0 : 16.0) : 0.0; - final remainingForLogo = remainingAfterActions - chipHeight - chipActionGap; + // Reserve a dedicated genre row, but only when the logo still keeps room + // afterwards so the title isn't crowded out on short heroes. + const genreRowHeight = 32.0; + const genreGap = 8.0; + final showGenres = + showChips && + genreChips.isNotEmpty && + remainingAfterActions - chipHeight - chipActionGap - (genreRowHeight + genreGap) >= 52; + final genreBlockHeight = showGenres ? genreRowHeight + genreGap : 0.0; + final remainingForLogo = remainingAfterActions - chipHeight - chipActionGap - genreBlockHeight; final logoGap = remainingForLogo >= 52 && (showChips || showActions) ? (availableHeight < 180 ? 8.0 : 12.0) : 0.0; @@ -4153,6 +4201,7 @@ class _MediaDetailScreenState extends State final contentHeight = (showLogo ? logoHeight + effectiveLogoGap : 0.0) + chipHeight + + genreBlockHeight + chipActionGap + (showActions ? actionHeight : 0.0); @@ -4196,6 +4245,19 @@ class _MediaDetailScreenState extends State ), ), ), + if (showGenres) ...[ + const SizedBox(height: genreGap), + ClipRect( + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: genreRowHeight), + child: Align( + alignment: .bottomLeft, + heightFactor: 1, + child: Wrap(spacing: 8, runSpacing: 8, children: genreChips), + ), + ), + ), + ], if (chipActionGap > 0) SizedBox(height: chipActionGap), if (showActions) SizedBox(height: actionHeight, child: _buildActionButtons(metadata)), ],