mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-27 05:12:29 +02:00
fix(ui): align metadata and wrap track badges
This commit is contained in:
@@ -320,32 +320,25 @@ class _TrackItemWithStatus extends ConsumerWidget {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Row(
|
TrackMetadataBadgesLine(
|
||||||
children: [
|
primary: ClickableArtistName(
|
||||||
Flexible(
|
artistName: track.artistName,
|
||||||
child: ClickableArtistName(
|
artistId: track.artistId,
|
||||||
artistName: track.artistName,
|
coverUrl: track.coverUrl,
|
||||||
artistId: track.artistId,
|
extensionId: extensionId,
|
||||||
coverUrl: track.coverUrl,
|
style: Theme.of(context).textTheme.bodySmall
|
||||||
extensionId: extensionId,
|
?.copyWith(color: colorScheme.onSurfaceVariant),
|
||||||
style: Theme.of(context).textTheme.bodySmall
|
maxLines: 1,
|
||||||
?.copyWith(
|
overflow: TextOverflow.ellipsis,
|
||||||
color: colorScheme.onSurfaceVariant,
|
),
|
||||||
),
|
badges: [
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
...buildQualityBadges(
|
...buildQualityBadges(
|
||||||
audioQuality: track.audioQuality,
|
audioQuality: track.audioQuality,
|
||||||
audioModes: track.audioModes,
|
audioModes: track.audioModes,
|
||||||
colorScheme: colorScheme,
|
colorScheme: colorScheme,
|
||||||
explicit: track.isExplicit,
|
explicit: track.isExplicit,
|
||||||
),
|
),
|
||||||
if (isInLocalLibrary) ...[
|
if (isInLocalLibrary) const InLibraryBadge(),
|
||||||
const SizedBox(width: 6),
|
|
||||||
const InLibraryBadge(),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -522,7 +522,8 @@ extension _TrackMetadataCards on _TrackMetadataScreenState {
|
|||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 4),
|
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 4),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||||
|
textBaseline: TextBaseline.alphabetic,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 100,
|
width: 100,
|
||||||
|
|||||||
@@ -144,8 +144,44 @@ class DolbyLogoPainter extends CustomPainter {
|
|||||||
color != oldDelegate.color;
|
color != oldDelegate.color;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience builder: returns a list of quality badge widgets for a track.
|
/// Keeps artist text and optional track badges responsive on narrow rows.
|
||||||
/// Pass the result into a Row using spread operator.
|
///
|
||||||
|
/// 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<Widget> 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<Widget> buildQualityBadges({
|
List<Widget> buildQualityBadges({
|
||||||
required String? audioQuality,
|
required String? audioQuality,
|
||||||
required String? audioModes,
|
required String? audioModes,
|
||||||
@@ -154,17 +190,14 @@ List<Widget> buildQualityBadges({
|
|||||||
}) {
|
}) {
|
||||||
final badges = <Widget>[];
|
final badges = <Widget>[];
|
||||||
if (explicit) {
|
if (explicit) {
|
||||||
badges.add(const SizedBox(width: 6));
|
|
||||||
badges.add(ExplicitBadge(colorScheme: colorScheme));
|
badges.add(ExplicitBadge(colorScheme: colorScheme));
|
||||||
}
|
}
|
||||||
if (audioQuality != null && audioQuality.isNotEmpty) {
|
if (audioQuality != null && audioQuality.isNotEmpty) {
|
||||||
badges.add(const SizedBox(width: 6));
|
|
||||||
badges.add(
|
badges.add(
|
||||||
AudioQualityBadge(label: audioQuality, colorScheme: colorScheme),
|
AudioQualityBadge(label: audioQuality, colorScheme: colorScheme),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (audioModes != null && audioModes.contains('DOLBY_ATMOS')) {
|
if (audioModes != null && audioModes.contains('DOLBY_ATMOS')) {
|
||||||
badges.add(const SizedBox(width: 4));
|
|
||||||
badges.add(DolbyAtmosBadge(colorScheme: colorScheme));
|
badges.add(DolbyAtmosBadge(colorScheme: colorScheme));
|
||||||
}
|
}
|
||||||
return badges;
|
return badges;
|
||||||
|
|||||||
@@ -76,36 +76,31 @@ class TrackListTile extends ConsumerWidget {
|
|||||||
isSelected: isSelected,
|
isSelected: isSelected,
|
||||||
leading: leading,
|
leading: leading,
|
||||||
title: track.name,
|
title: track.name,
|
||||||
subtitle: Row(
|
subtitle: TrackMetadataBadgesLine(
|
||||||
children: [
|
primary: clickableArtist && !isSelectionMode
|
||||||
Flexible(
|
? ClickableArtistName(
|
||||||
child: clickableArtist && !isSelectionMode
|
artistName: track.artistName,
|
||||||
? ClickableArtistName(
|
artistId: track.artistId,
|
||||||
artistName: track.artistName,
|
coverUrl: track.coverUrl,
|
||||||
artistId: track.artistId,
|
extensionId: track.source,
|
||||||
coverUrl: track.coverUrl,
|
maxLines: 1,
|
||||||
extensionId: track.source,
|
overflow: TextOverflow.ellipsis,
|
||||||
maxLines: 1,
|
style: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||||
overflow: TextOverflow.ellipsis,
|
)
|
||||||
style: TextStyle(color: colorScheme.onSurfaceVariant),
|
: Text(
|
||||||
)
|
track.artistName,
|
||||||
: Text(
|
maxLines: 1,
|
||||||
track.artistName,
|
overflow: TextOverflow.ellipsis,
|
||||||
maxLines: 1,
|
style: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||||
overflow: TextOverflow.ellipsis,
|
),
|
||||||
style: TextStyle(color: colorScheme.onSurfaceVariant),
|
badges: [
|
||||||
),
|
|
||||||
),
|
|
||||||
...buildQualityBadges(
|
...buildQualityBadges(
|
||||||
audioQuality: track.audioQuality,
|
audioQuality: track.audioQuality,
|
||||||
audioModes: track.audioModes,
|
audioModes: track.audioModes,
|
||||||
colorScheme: colorScheme,
|
colorScheme: colorScheme,
|
||||||
explicit: track.isExplicit,
|
explicit: track.isExplicit,
|
||||||
),
|
),
|
||||||
if (isInLocalLibrary || isInHistory) ...[
|
if (isInLocalLibrary || isInHistory) const InLibraryBadge(),
|
||||||
const SizedBox(width: 6),
|
|
||||||
const InLibraryBadge(),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
|
|||||||
@@ -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),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -56,5 +56,14 @@ void main() {
|
|||||||
separators.every((text) => text.style?.color == Colors.white70),
|
separators.every((text) => text.style?.color == Colors.white70),
|
||||||
isTrue,
|
isTrue,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final durationLabel = find.text('Duration');
|
||||||
|
final metadataRow = find
|
||||||
|
.ancestor(of: durationLabel, matching: find.byType(Row))
|
||||||
|
.first;
|
||||||
|
final row = tester.widget<Row>(metadataRow);
|
||||||
|
|
||||||
|
expect(row.crossAxisAlignment, CrossAxisAlignment.baseline);
|
||||||
|
expect(row.textBaseline, TextBaseline.alphabetic);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user