- Now Playing shows where music plays and opens the phone's output switcher - Settings hub: Wi-Fi-only downloads (default on), storage, sound, help, legal - Privacy Policy and Terms bundled as Markdown (assets/legal), shown in-app - 'Is this your song?' report form feeding takedown_requests (+ kind, user_id) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
190 lines
6.8 KiB
Dart
190 lines
6.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import '../../app/router.dart';
|
|
import '../../app/theme.dart';
|
|
import '../../offline/download_store.dart';
|
|
import '../../audio/player_providers.dart';
|
|
import '../../core/models.dart';
|
|
import '../../data/catalog_repository.dart';
|
|
import '../../data/user_library.dart';
|
|
import 'artwork.dart';
|
|
|
|
class TrackTile extends ConsumerWidget {
|
|
const TrackTile({super.key, required this.track, required this.onTap, this.subtitle, this.card = false});
|
|
|
|
final Track track;
|
|
final VoidCallback onTap;
|
|
final String? subtitle;
|
|
|
|
/// Card style (rounded surface) used on Home; plain rows elsewhere.
|
|
final bool card;
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final catalog = ref.watch(catalogProvider).requireValue;
|
|
final isCurrent = ref.watch(mediaItemProvider).value?.id == track.id;
|
|
final downloads = ref.watch(downloadsProvider);
|
|
final text = Theme.of(context).textTheme;
|
|
|
|
final row = InkWell(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(16),
|
|
child: Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: card ? 12 : 0, vertical: card ? 10 : 6),
|
|
child: Row(
|
|
children: [
|
|
TrackArtwork.track(track, size: card ? 60 : 52, radius: 10),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
track.titleLatin,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: text.titleMedium?.copyWith(color: isCurrent ? NkColors.sun : NkColors.cream),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Row(
|
|
children: [
|
|
if (downloads.isDownloaded(track.id))
|
|
const Padding(
|
|
padding: EdgeInsets.only(right: 4),
|
|
child: Icon(Icons.download_done_rounded, size: 16, color: NkColors.teaLeaf),
|
|
)
|
|
else if (downloads.progress.containsKey(track.id))
|
|
const Padding(
|
|
padding: EdgeInsets.only(right: 6),
|
|
child: SizedBox.square(dimension: 12, child: CircularProgressIndicator(strokeWidth: 2)),
|
|
),
|
|
Expanded(
|
|
child: Text(
|
|
subtitle ?? '${catalog.creditLine(track)} · ${formatDuration(track.duration)}',
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: text.bodyMedium?.copyWith(color: NkColors.muted),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
TrackMenuButton(track: track),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
if (!card) return row;
|
|
return Material(color: NkColors.surface, borderRadius: BorderRadius.circular(16), child: row);
|
|
}
|
|
}
|
|
|
|
class TrackMenuButton extends ConsumerWidget {
|
|
const TrackMenuButton({super.key, required this.track});
|
|
|
|
final Track track;
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
return IconButton(
|
|
tooltip: 'More options',
|
|
icon: const Icon(Icons.more_vert, color: NkColors.muted),
|
|
onPressed: () => showTrackActions(context, ref, track),
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> showTrackActions(BuildContext context, WidgetRef ref, Track track) {
|
|
final catalog = ref.read(catalogProvider).requireValue;
|
|
final controller = ref.read(playerControllerProvider);
|
|
final liked = ref.read(likedSongsProvider).contains(track.id);
|
|
final downloaded = ref.read(downloadsProvider).isDownloaded(track.id);
|
|
final artists = catalog.artistsOf(track);
|
|
|
|
void done(String message) {
|
|
Navigator.of(context).pop();
|
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
|
|
}
|
|
|
|
return showModalBottomSheet<void>(
|
|
context: context,
|
|
backgroundColor: NkColors.surface,
|
|
showDragHandle: true,
|
|
builder: (sheetContext) => SafeArea(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
ListTile(
|
|
leading: TrackArtwork.track(track, size: 48),
|
|
title: Text(track.titleLatin, style: Theme.of(context).textTheme.titleMedium),
|
|
subtitle: Text(catalog.creditLine(track)),
|
|
),
|
|
const Divider(color: NkColors.outline),
|
|
ListTile(
|
|
leading: const Icon(Icons.playlist_play),
|
|
title: const Text('Play next'),
|
|
onTap: () {
|
|
controller.playNext(track);
|
|
done('Playing next');
|
|
},
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.queue_music),
|
|
title: const Text('Add to queue'),
|
|
onTap: () {
|
|
controller.addToQueue(track);
|
|
done('Added to queue');
|
|
},
|
|
),
|
|
ListTile(
|
|
leading: Icon(liked ? Icons.favorite : Icons.favorite_border, color: liked ? NkColors.like : null),
|
|
title: Text(liked ? 'Remove from liked songs' : 'Add to liked songs'),
|
|
onTap: () {
|
|
ref.read(likedSongsProvider.notifier).toggle(track.id);
|
|
done(liked ? 'Removed from liked songs' : 'Added to liked songs');
|
|
},
|
|
),
|
|
ListTile(
|
|
leading: Icon(downloaded ? Icons.download_done_rounded : Icons.download_rounded,
|
|
color: downloaded ? NkColors.teaLeaf : null),
|
|
title: Text(downloaded ? 'Remove download' : 'Download'),
|
|
onTap: () {
|
|
if (downloaded) {
|
|
ref.read(downloadStoreProvider).remove(track.id);
|
|
done('Download removed');
|
|
} else if (!isSignedIn) {
|
|
Navigator.of(sheetContext).pop();
|
|
context.push('/login?mode=signup');
|
|
} else {
|
|
controller.download([track]).then((problem) => done(problem ?? 'Downloading. Find it in Downloads.'));
|
|
}
|
|
},
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.flag_outlined),
|
|
title: const Text('Is this your song? Report'),
|
|
onTap: () {
|
|
Navigator.of(sheetContext).pop();
|
|
context.push('/report?track=${track.id}');
|
|
},
|
|
),
|
|
for (final a in artists)
|
|
ListTile(
|
|
leading: const Icon(Icons.person_outline),
|
|
title: Text('Go to ${a.name}'),
|
|
onTap: () {
|
|
Navigator.of(sheetContext).pop();
|
|
context.push('/artist/${a.id}');
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|