mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-31 08:17:22 +02:00
feat(metadata): surface explicit content flag with [E] badge
Port of the explicit-content indicator from SpotiFLAC-Web (issue #456): - Go: add explicit to ExtTrackMetadata (parsed generically from any extension via explicit/is_explicit/isExplicit), TrackMetadata, and AlbumTrackMetadata; built-in Deezer maps explicit_lyrics and explicit_content_lyrics == 1 - Dart: add explicit to the Track model with a tolerant parseExplicitFlag helper wired into all backend track parsers - UI: new ExplicitBadge rendered through buildQualityBadges on album, playlist, search, and home track rows
This commit is contained in:
@@ -28,6 +28,7 @@ class Track {
|
||||
final String? itemType;
|
||||
final String? audioQuality;
|
||||
final String? audioModes;
|
||||
final bool? explicit;
|
||||
|
||||
const Track({
|
||||
required this.id,
|
||||
@@ -54,6 +55,7 @@ class Track {
|
||||
this.itemType,
|
||||
this.audioQuality,
|
||||
this.audioModes,
|
||||
this.explicit,
|
||||
});
|
||||
|
||||
bool get isSingle {
|
||||
@@ -82,6 +84,8 @@ class Track {
|
||||
bool get isDolbyAtmos =>
|
||||
audioModes != null && audioModes!.contains('DOLBY_ATMOS');
|
||||
|
||||
bool get isExplicit => explicit == true;
|
||||
|
||||
bool get hasAudioQuality => audioQuality != null && audioQuality!.isNotEmpty;
|
||||
|
||||
bool get hasPreview => previewUrl != null && previewUrl!.isNotEmpty;
|
||||
|
||||
@@ -35,6 +35,7 @@ Track _$TrackFromJson(Map<String, dynamic> json) => Track(
|
||||
itemType: json['itemType'] as String?,
|
||||
audioQuality: json['audioQuality'] as String?,
|
||||
audioModes: json['audioModes'] as String?,
|
||||
explicit: json['explicit'] as bool?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TrackToJson(Track instance) => <String, dynamic>{
|
||||
@@ -62,6 +63,7 @@ Map<String, dynamic> _$TrackToJson(Track instance) => <String, dynamic>{
|
||||
'itemType': instance.itemType,
|
||||
'audioQuality': instance.audioQuality,
|
||||
'audioModes': instance.audioModes,
|
||||
'explicit': instance.explicit,
|
||||
};
|
||||
|
||||
ServiceAvailability _$ServiceAvailabilityFromJson(Map<String, dynamic> json) =>
|
||||
|
||||
@@ -750,6 +750,7 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
itemType: track.itemType,
|
||||
audioQuality: track.audioQuality,
|
||||
audioModes: track.audioModes,
|
||||
explicit: track.explicit,
|
||||
availability: ServiceAvailability(
|
||||
tidal: availability['tidal'] as bool? ?? false,
|
||||
qobuz: availability['qobuz'] as bool? ?? false,
|
||||
@@ -846,6 +847,7 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
audioQuality: data['audio_quality']?.toString(),
|
||||
audioModes: data['audio_modes']?.toString(),
|
||||
previewUrl: data['preview_url']?.toString(),
|
||||
explicit: parseExplicitFlag(data['explicit']),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -471,6 +471,7 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
audioQuality: data['audio_quality']?.toString(),
|
||||
audioModes: data['audio_modes']?.toString(),
|
||||
previewUrl: data['preview_url']?.toString(),
|
||||
explicit: parseExplicitFlag(data['explicit']),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1294,6 +1295,7 @@ class _AlbumTrackItem extends ConsumerWidget {
|
||||
audioQuality: track.audioQuality,
|
||||
audioModes: track.audioModes,
|
||||
colorScheme: colorScheme,
|
||||
explicit: track.isExplicit,
|
||||
),
|
||||
if (isInLocalLibrary || isInHistory) ...[
|
||||
const SizedBox(width: 6),
|
||||
|
||||
@@ -338,6 +338,7 @@ class _TrackItemWithStatus extends ConsumerWidget {
|
||||
audioQuality: track.audioQuality,
|
||||
audioModes: track.audioModes,
|
||||
colorScheme: colorScheme,
|
||||
explicit: track.isExplicit,
|
||||
),
|
||||
if (isInLocalLibrary) ...[
|
||||
const SizedBox(width: 6),
|
||||
@@ -1126,6 +1127,7 @@ class _ExtensionAlbumScreenState extends ConsumerState<ExtensionAlbumScreen> {
|
||||
audioQuality: data['audio_quality']?.toString(),
|
||||
audioModes: data['audio_modes']?.toString(),
|
||||
previewUrl: data['preview_url']?.toString(),
|
||||
explicit: parseExplicitFlag(data['explicit']),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1296,6 +1298,7 @@ class _ExtensionPlaylistScreenState
|
||||
audioQuality: data['audio_quality']?.toString(),
|
||||
audioModes: data['audio_modes']?.toString(),
|
||||
previewUrl: data['preview_url']?.toString(),
|
||||
explicit: parseExplicitFlag(data['explicit']),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -226,6 +226,7 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen> {
|
||||
audioQuality: data['audio_quality']?.toString(),
|
||||
audioModes: data['audio_modes']?.toString(),
|
||||
previewUrl: data['preview_url']?.toString(),
|
||||
explicit: parseExplicitFlag(data['explicit']),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1064,6 +1065,7 @@ class _PlaylistTrackItem extends ConsumerWidget {
|
||||
audioQuality: track.audioQuality,
|
||||
audioModes: track.audioModes,
|
||||
colorScheme: colorScheme,
|
||||
explicit: track.isExplicit,
|
||||
),
|
||||
if (isInLocalLibrary || isInHistory) ...[
|
||||
const SizedBox(width: 6),
|
||||
|
||||
@@ -231,6 +231,7 @@ class _SearchTrackTile extends ConsumerWidget {
|
||||
audioQuality: track.audioQuality,
|
||||
audioModes: track.audioModes,
|
||||
colorScheme: colorScheme,
|
||||
explicit: track.isExplicit,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -6,6 +6,20 @@ String? normalizeOptionalString(String? value) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/// Parses the parental-advisory flag from backend/extension payloads, which
|
||||
/// may arrive as a bool, a 0/1 number, or a "true"/"1" string.
|
||||
bool? parseExplicitFlag(dynamic value) {
|
||||
if (value == null) return null;
|
||||
if (value is bool) return value;
|
||||
if (value is num) return value == 1;
|
||||
if (value is String) {
|
||||
final normalized = value.trim().toLowerCase();
|
||||
if (normalized.isEmpty || normalized == 'null') return null;
|
||||
return normalized == 'true' || normalized == '1';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
final RegExp _windowsAbsolutePathPattern = RegExp(r'^[A-Za-z]:[\\/]');
|
||||
|
||||
bool _looksLikeLocalReference(String value) {
|
||||
|
||||
@@ -31,6 +31,37 @@ class AudioQualityBadge extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class ExplicitBadge extends StatelessWidget {
|
||||
final ColorScheme colorScheme;
|
||||
|
||||
const ExplicitBadge({super.key, required this.colorScheme});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
message: 'Explicit',
|
||||
child: Container(
|
||||
width: 14,
|
||||
height: 14,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.85),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Text(
|
||||
'E',
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: colorScheme.surface,
|
||||
height: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DolbyAtmosBadge extends StatelessWidget {
|
||||
final ColorScheme colorScheme;
|
||||
|
||||
@@ -118,8 +149,13 @@ List<Widget> buildQualityBadges({
|
||||
required String? audioQuality,
|
||||
required String? audioModes,
|
||||
required ColorScheme colorScheme,
|
||||
bool explicit = false,
|
||||
}) {
|
||||
final badges = <Widget>[];
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user