@@ -3126,9 +3126,14 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
// 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<MediaDetailScreen>
|
||||
|
||||
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 <String>[];
|
||||
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<MediaDetailScreen>
|
||||
final contentHeight =
|
||||
(showLogo ? logoHeight + logoMetadataGap : 0) +
|
||||
metadataLineHeight +
|
||||
genreBlockHeight +
|
||||
descriptionHeight +
|
||||
actionGap +
|
||||
actionHeight;
|
||||
@@ -3513,6 +3526,26 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
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<MediaDetailScreen>
|
||||
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<MediaDetailScreen>
|
||||
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 <String>[]) _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<MediaDetailScreen>
|
||||
final contentHeight =
|
||||
(showLogo ? logoHeight + effectiveLogoGap : 0.0) +
|
||||
chipHeight +
|
||||
genreBlockHeight +
|
||||
chipActionGap +
|
||||
(showActions ? actionHeight : 0.0);
|
||||
|
||||
@@ -4196,6 +4245,19 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
),
|
||||
),
|
||||
),
|
||||
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)),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user