diff --git a/lib/screens/home_tab_widgets.dart b/lib/screens/home_tab_widgets.dart index 4be5a2e9..81897569 100644 --- a/lib/screens/home_tab_widgets.dart +++ b/lib/screens/home_tab_widgets.dart @@ -320,32 +320,25 @@ class _TrackItemWithStatus extends ConsumerWidget { overflow: TextOverflow.ellipsis, ), const SizedBox(height: 2), - Row( - children: [ - Flexible( - child: ClickableArtistName( - artistName: track.artistName, - artistId: track.artistId, - coverUrl: track.coverUrl, - extensionId: extensionId, - style: Theme.of(context).textTheme.bodySmall - ?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), + TrackMetadataBadgesLine( + primary: ClickableArtistName( + artistName: track.artistName, + artistId: track.artistId, + coverUrl: track.coverUrl, + extensionId: extensionId, + style: Theme.of(context).textTheme.bodySmall + ?.copyWith(color: colorScheme.onSurfaceVariant), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + badges: [ ...buildQualityBadges( audioQuality: track.audioQuality, audioModes: track.audioModes, colorScheme: colorScheme, explicit: track.isExplicit, ), - if (isInLocalLibrary) ...[ - const SizedBox(width: 6), - const InLibraryBadge(), - ], + if (isInLocalLibrary) const InLibraryBadge(), ], ), ], diff --git a/lib/screens/track_metadata_cards.dart b/lib/screens/track_metadata_cards.dart index 34a8896e..ffbe9113 100644 --- a/lib/screens/track_metadata_cards.dart +++ b/lib/screens/track_metadata_cards.dart @@ -522,7 +522,8 @@ extension _TrackMetadataCards on _TrackMetadataScreenState { child: Padding( padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 4), child: Row( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, children: [ SizedBox( width: 100, diff --git a/lib/widgets/audio_quality_badges.dart b/lib/widgets/audio_quality_badges.dart index a4868d6f..c07a7624 100644 --- a/lib/widgets/audio_quality_badges.dart +++ b/lib/widgets/audio_quality_badges.dart @@ -144,8 +144,44 @@ class DolbyLogoPainter extends CustomPainter { color != oldDelegate.color; } -/// Convenience builder: returns a list of quality badge widgets for a track. -/// Pass the result into a Row using spread operator. +/// Keeps artist text and optional track badges responsive on narrow rows. +/// +/// A [Row] with a flexible artist still overflows when the badges themselves +/// need more room than remains. [Wrap] moves whole badges to another run while +/// keeping the artist constrained and ellipsized when it alone is too long. +class TrackMetadataBadgesLine extends StatelessWidget { + const TrackMetadataBadgesLine({ + super.key, + required this.primary, + this.badges = const [], + }); + + final Widget primary; + final List badges; + + @override + Widget build(BuildContext context) { + if (badges.isEmpty) return primary; + + return LayoutBuilder( + builder: (context, constraints) => Wrap( + spacing: 6, + runSpacing: 4, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ConstrainedBox( + constraints: BoxConstraints(maxWidth: constraints.maxWidth), + child: primary, + ), + ...badges, + ], + ), + ); + } +} + +/// Returns the quality-related badge widgets for a track. Spacing and wrapping +/// belong to [TrackMetadataBadgesLine] so the group stays responsive. List buildQualityBadges({ required String? audioQuality, required String? audioModes, @@ -154,17 +190,14 @@ List buildQualityBadges({ }) { final badges = []; if (explicit) { - badges.add(const SizedBox(width: 6)); badges.add(ExplicitBadge(colorScheme: colorScheme)); } if (audioQuality != null && audioQuality.isNotEmpty) { - badges.add(const SizedBox(width: 6)); badges.add( AudioQualityBadge(label: audioQuality, colorScheme: colorScheme), ); } if (audioModes != null && audioModes.contains('DOLBY_ATMOS')) { - badges.add(const SizedBox(width: 4)); badges.add(DolbyAtmosBadge(colorScheme: colorScheme)); } return badges; diff --git a/lib/widgets/track_list_tile.dart b/lib/widgets/track_list_tile.dart index 9ef1d4d2..e8b0d977 100644 --- a/lib/widgets/track_list_tile.dart +++ b/lib/widgets/track_list_tile.dart @@ -76,36 +76,31 @@ class TrackListTile extends ConsumerWidget { isSelected: isSelected, leading: leading, title: track.name, - subtitle: Row( - children: [ - Flexible( - child: clickableArtist && !isSelectionMode - ? ClickableArtistName( - artistName: track.artistName, - artistId: track.artistId, - coverUrl: track.coverUrl, - extensionId: track.source, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle(color: colorScheme.onSurfaceVariant), - ) - : Text( - track.artistName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle(color: colorScheme.onSurfaceVariant), - ), - ), + subtitle: TrackMetadataBadgesLine( + primary: clickableArtist && !isSelectionMode + ? ClickableArtistName( + artistName: track.artistName, + artistId: track.artistId, + coverUrl: track.coverUrl, + extensionId: track.source, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: colorScheme.onSurfaceVariant), + ) + : Text( + track.artistName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: colorScheme.onSurfaceVariant), + ), + badges: [ ...buildQualityBadges( audioQuality: track.audioQuality, audioModes: track.audioModes, colorScheme: colorScheme, explicit: track.isExplicit, ), - if (isInLocalLibrary || isInHistory) ...[ - const SizedBox(width: 6), - const InLibraryBadge(), - ], + if (isInLocalLibrary || isInHistory) const InLibraryBadge(), ], ), trailing: Row( diff --git a/test/track_metadata_badges_line_test.dart b/test/track_metadata_badges_line_test.dart new file mode 100644 index 00000000..99a2b418 --- /dev/null +++ b/test/track_metadata_badges_line_test.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spotiflac_android/l10n/app_localizations.dart'; +import 'package:spotiflac_android/widgets/audio_quality_badges.dart'; +import 'package:spotiflac_android/widgets/in_library_badge.dart'; + +void main() { + testWidgets('track metadata badges wrap on a narrow result row', ( + tester, + ) async { + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold( + body: Align( + child: SizedBox( + width: 180, + child: Builder( + builder: (context) { + final colorScheme = Theme.of(context).colorScheme; + return TrackMetadataBadgesLine( + primary: const Text( + 'RADWIMPS', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + badges: [ + AudioQualityBadge( + label: '16-bit', + colorScheme: colorScheme, + ), + DolbyAtmosBadge(colorScheme: colorScheme), + const InLibraryBadge(), + ], + ); + }, + ), + ), + ), + ), + ), + ); + + expect(tester.takeException(), isNull); + expect( + tester.getTopLeft(find.byType(InLibraryBadge)).dy, + greaterThan(tester.getTopLeft(find.text('RADWIMPS')).dy), + ); + }); +} diff --git a/test/track_metadata_header_contrast_test.dart b/test/track_metadata_header_contrast_test.dart index 8f6e3853..e206dc1c 100644 --- a/test/track_metadata_header_contrast_test.dart +++ b/test/track_metadata_header_contrast_test.dart @@ -56,5 +56,14 @@ void main() { separators.every((text) => text.style?.color == Colors.white70), isTrue, ); + + final durationLabel = find.text('Duration'); + final metadataRow = find + .ancestor(of: durationLabel, matching: find.byType(Row)) + .first; + final row = tester.widget(metadataRow); + + expect(row.crossAxisAlignment, CrossAxisAlignment.baseline); + expect(row.textBaseline, TextBaseline.alphabetic); }); }