mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-14 23:20:25 +02:00
chore: rebuild dev history without streaming-era commits
This commit is contained in:
@@ -22,7 +22,7 @@ class BuiltInService {
|
||||
});
|
||||
}
|
||||
|
||||
/// Default quality options for built-in services (Tidal, Qobuz, Amazon, YouTube)
|
||||
/// Default quality options for built-in services
|
||||
/// Note: Tidal lossy (HIGH) removed - use YouTube for lossy downloads
|
||||
const _builtInServices = [
|
||||
BuiltInService(
|
||||
@@ -129,6 +129,7 @@ class DownloadServicePicker extends ConsumerStatefulWidget {
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
backgroundColor: colorScheme.surfaceContainerHigh,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,134 +13,9 @@ Future<void> showAddTrackToPlaylistSheet(
|
||||
WidgetRef ref,
|
||||
Track track,
|
||||
) async {
|
||||
final notifier = ref.read(libraryCollectionsProvider.notifier);
|
||||
final state = ref.read(libraryCollectionsProvider);
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
await showModalBottomSheet<void>(
|
||||
context: context,
|
||||
showDragHandle: true,
|
||||
builder: (sheetContext) {
|
||||
final playlists = ref.watch(
|
||||
libraryCollectionsProvider.select((state) => state.playlists),
|
||||
);
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.playlist_add),
|
||||
title: Text(sheetContext.l10n.collectionAddToPlaylist),
|
||||
subtitle: Text('${track.name} • ${track.artistName}'),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.add_circle_outline),
|
||||
title: Text(sheetContext.l10n.collectionCreatePlaylist),
|
||||
onTap: () async {
|
||||
Navigator.of(sheetContext).pop();
|
||||
final name = await _promptPlaylistName(context);
|
||||
if (name == null || name.trim().isEmpty || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
final playlistId = await notifier.createPlaylist(name.trim());
|
||||
final added = await notifier.addTrackToPlaylist(
|
||||
playlistId,
|
||||
track,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
added
|
||||
? context.l10n.collectionAddedToPlaylist(name.trim())
|
||||
: context.l10n.collectionAlreadyInPlaylist(
|
||||
name.trim(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (playlists.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
|
||||
child: Text(
|
||||
sheetContext.l10n.collectionNoPlaylistsYet,
|
||||
style: Theme.of(sheetContext).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(sheetContext).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 320),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: playlists.length,
|
||||
itemBuilder: (context, index) {
|
||||
final playlist = playlists[index];
|
||||
final alreadyInPlaylist = playlist.containsTrack(track);
|
||||
return ListTile(
|
||||
leading: _PlaylistPickerThumbnail(
|
||||
playlist: playlist,
|
||||
isSelected: alreadyInPlaylist,
|
||||
),
|
||||
title: Text(playlist.name),
|
||||
subtitle: Text(
|
||||
context.l10n.collectionPlaylistTracks(
|
||||
playlist.tracks.length,
|
||||
),
|
||||
),
|
||||
enabled: !alreadyInPlaylist,
|
||||
onTap: !alreadyInPlaylist
|
||||
? () async {
|
||||
final added = await notifier.addTrackToPlaylist(
|
||||
playlist.id,
|
||||
track,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(sheetContext).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
added
|
||||
? context.l10n
|
||||
.collectionAddedToPlaylist(
|
||||
playlist.name,
|
||||
)
|
||||
: context.l10n
|
||||
.collectionAlreadyInPlaylist(
|
||||
playlist.name,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
final afterState = ref.read(libraryCollectionsProvider);
|
||||
if (afterState.playlists.length != state.playlists.length) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.collectionPlaylistCreated)),
|
||||
);
|
||||
}
|
||||
return showAddTracksToPlaylistSheet(context, ref, [track]);
|
||||
}
|
||||
|
||||
/// Batch version: add multiple tracks to a chosen playlist.
|
||||
Future<void> showAddTracksToPlaylistSheet(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
@@ -148,79 +23,157 @@ Future<void> showAddTracksToPlaylistSheet(
|
||||
) async {
|
||||
if (tracks.isEmpty) return;
|
||||
|
||||
final notifier = ref.read(libraryCollectionsProvider.notifier);
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
await showModalBottomSheet<void>(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
showDragHandle: true,
|
||||
isScrollControlled: true,
|
||||
builder: (sheetContext) {
|
||||
final playlists = ref.watch(
|
||||
libraryCollectionsProvider.select((state) => state.playlists),
|
||||
return _PlaylistPickerSheetContent(tracks: tracks);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class _PlaylistPickerSheetContent extends ConsumerStatefulWidget {
|
||||
final List<Track> tracks;
|
||||
|
||||
const _PlaylistPickerSheetContent({required this.tracks});
|
||||
|
||||
@override
|
||||
ConsumerState<_PlaylistPickerSheetContent> createState() =>
|
||||
_PlaylistPickerSheetContentState();
|
||||
}
|
||||
|
||||
class _PlaylistPickerSheetContentState
|
||||
extends ConsumerState<_PlaylistPickerSheetContent> {
|
||||
final Set<String> _selectedPlaylistIds = {};
|
||||
final Set<String> _initialDisabledIds = {};
|
||||
bool _initialized = false;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (!_initialized) {
|
||||
final playlists = ref.read(libraryCollectionsProvider).playlists;
|
||||
for (final playlist in playlists) {
|
||||
final alreadyInPlaylist =
|
||||
widget.tracks.every((t) => playlist.containsTrack(t));
|
||||
if (alreadyInPlaylist) {
|
||||
_initialDisabledIds.add(playlist.id);
|
||||
_selectedPlaylistIds.add(playlist.id);
|
||||
}
|
||||
}
|
||||
_initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleDone() async {
|
||||
final notifier = ref.read(libraryCollectionsProvider.notifier);
|
||||
final idsToAdd = _selectedPlaylistIds.difference(_initialDisabledIds);
|
||||
final addedNames = <String>[];
|
||||
|
||||
for (final playlistId in idsToAdd) {
|
||||
final playlist =
|
||||
ref.read(libraryCollectionsProvider).playlistById(playlistId);
|
||||
if (playlist != null) {
|
||||
addedNames.add(playlist.name);
|
||||
}
|
||||
await notifier.addTracksToPlaylist(playlistId, widget.tracks);
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
|
||||
if (addedNames.isNotEmpty) {
|
||||
final name =
|
||||
addedNames.length == 1 ? addedNames.first : addedNames.join(', ');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.collectionAddedToPlaylist(name)),
|
||||
),
|
||||
);
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.playlist_add),
|
||||
title: Text(sheetContext.l10n.collectionAddToPlaylist),
|
||||
subtitle: Text(
|
||||
'${tracks.length} ${tracks.length == 1 ? 'track' : 'tracks'}',
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.add_circle_outline),
|
||||
title: Text(sheetContext.l10n.collectionCreatePlaylist),
|
||||
onTap: () async {
|
||||
Navigator.of(sheetContext).pop();
|
||||
final name = await _promptPlaylistName(context);
|
||||
if (name == null || name.trim().isEmpty || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
final playlistId = await notifier.createPlaylist(name.trim());
|
||||
final result = await notifier.addTracksToPlaylist(
|
||||
playlistId,
|
||||
tracks,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
result.addedCount > 0
|
||||
? context.l10n.collectionAddedToPlaylist(name.trim())
|
||||
: context.l10n.collectionAlreadyInPlaylist(
|
||||
name.trim(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (playlists.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
|
||||
child: Text(
|
||||
sheetContext.l10n.collectionNoPlaylistsYet,
|
||||
style: Theme.of(sheetContext).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(sheetContext).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final playlists = ref.watch(
|
||||
libraryCollectionsProvider.select((state) => state.playlists),
|
||||
);
|
||||
final notifier = ref.read(libraryCollectionsProvider.notifier);
|
||||
|
||||
final String subtitle;
|
||||
if (widget.tracks.length == 1) {
|
||||
final track = widget.tracks.first;
|
||||
subtitle = '${track.name} • ${track.artistName}';
|
||||
} else {
|
||||
subtitle =
|
||||
'${widget.tracks.length} ${widget.tracks.length == 1 ? 'track' : 'tracks'}';
|
||||
}
|
||||
|
||||
final idsToAdd = _selectedPlaylistIds.difference(_initialDisabledIds);
|
||||
final hasNewSelections = idsToAdd.isNotEmpty;
|
||||
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.playlist_add),
|
||||
title: Text(context.l10n.collectionAddToPlaylist),
|
||||
subtitle: Text(subtitle),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.add_circle_outline),
|
||||
title: Text(context.l10n.collectionCreatePlaylist),
|
||||
onTap: () async {
|
||||
final name = await _promptPlaylistName(context);
|
||||
if (name == null || name.trim().isEmpty || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
final playlistId = await notifier.createPlaylist(name.trim());
|
||||
await notifier.addTracksToPlaylist(playlistId, widget.tracks);
|
||||
setState(() {
|
||||
_initialDisabledIds.add(playlistId);
|
||||
_selectedPlaylistIds.add(playlistId);
|
||||
});
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.collectionAddedToPlaylist(name.trim())),
|
||||
),
|
||||
)
|
||||
else
|
||||
ConstrainedBox(
|
||||
);
|
||||
},
|
||||
),
|
||||
if (playlists.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
|
||||
child: Text(
|
||||
context.l10n.collectionNoPlaylistsYet,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Flexible(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 320),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: playlists.length,
|
||||
itemBuilder: (context, index) {
|
||||
final playlist = playlists[index];
|
||||
final isAlreadyIn = _initialDisabledIds.contains(playlist.id);
|
||||
final isSelected = _selectedPlaylistIds.contains(playlist.id);
|
||||
|
||||
return ListTile(
|
||||
leading: _PlaylistPickerThumbnail(
|
||||
playlist: playlist,
|
||||
isSelected: false,
|
||||
isSelected: isSelected,
|
||||
),
|
||||
title: Text(playlist.name),
|
||||
subtitle: Text(
|
||||
@@ -228,37 +181,44 @@ Future<void> showAddTracksToPlaylistSheet(
|
||||
playlist.tracks.length,
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
final result = await notifier.addTracksToPlaylist(
|
||||
playlist.id,
|
||||
tracks,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(sheetContext).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
result.addedCount > 0
|
||||
? context.l10n.collectionAddedToPlaylist(
|
||||
playlist.name,
|
||||
)
|
||||
: context.l10n.collectionAlreadyInPlaylist(
|
||||
playlist.name,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
enabled: !isAlreadyIn,
|
||||
onTap: !isAlreadyIn
|
||||
? () {
|
||||
setState(() {
|
||||
if (isSelected) {
|
||||
_selectedPlaylistIds.remove(playlist.id);
|
||||
} else {
|
||||
_selectedPlaylistIds.add(playlist.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
: null,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton(
|
||||
onPressed: () {
|
||||
if (hasNewSelections) {
|
||||
_handleDone();
|
||||
} else {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
child: Text(context.l10n.dialogDone),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _promptPlaylistName(BuildContext context) async {
|
||||
@@ -352,10 +312,7 @@ class _PlaylistPickerThumbnail extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primary,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: colorScheme.primary,
|
||||
width: 1.5,
|
||||
),
|
||||
border: Border.all(color: colorScheme.primary, width: 1.5),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.check,
|
||||
|
||||
@@ -3,17 +3,36 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/models/track.dart';
|
||||
import 'package:spotiflac_android/providers/download_queue_provider.dart';
|
||||
import 'package:spotiflac_android/providers/library_collections_provider.dart';
|
||||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/services/cover_cache_manager.dart';
|
||||
import 'package:spotiflac_android/widgets/download_service_picker.dart';
|
||||
import 'package:spotiflac_android/widgets/playlist_picker_sheet.dart';
|
||||
import 'package:spotiflac_android/utils/clickable_metadata.dart';
|
||||
|
||||
class TrackCollectionQuickActions extends ConsumerWidget {
|
||||
final Track track;
|
||||
|
||||
const TrackCollectionQuickActions({
|
||||
super.key,
|
||||
required this.track,
|
||||
});
|
||||
const TrackCollectionQuickActions({super.key, required this.track});
|
||||
|
||||
static void showTrackOptionsSheet(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
Track track,
|
||||
) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: colorScheme.surfaceContainerHigh,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
|
||||
),
|
||||
builder: (sheetContext) => _TrackOptionsSheet(track: track),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -25,24 +44,11 @@ class TrackCollectionQuickActions extends ConsumerWidget {
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () => _showTrackOptionsSheet(context, ref),
|
||||
onPressed: () => showTrackOptionsSheet(context, ref, track),
|
||||
padding: const EdgeInsets.only(left: 12),
|
||||
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
|
||||
);
|
||||
}
|
||||
|
||||
void _showTrackOptionsSheet(BuildContext context, WidgetRef ref) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: colorScheme.surfaceContainerHigh,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
|
||||
),
|
||||
builder: (sheetContext) => _TrackOptionsSheet(track: track),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TrackOptionsSheet extends ConsumerWidget {
|
||||
@@ -53,6 +59,9 @@ class _TrackOptionsSheet extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final settings = ref.watch(settingsProvider);
|
||||
final rootContext = Navigator.of(context, rootNavigator: true).context;
|
||||
final container = ProviderScope.containerOf(rootContext, listen: false);
|
||||
|
||||
final isLoved = ref.watch(
|
||||
libraryCollectionsProvider.select((state) => state.isLoved(track)),
|
||||
@@ -62,155 +71,214 @@ class _TrackOptionsSheet extends ConsumerWidget {
|
||||
);
|
||||
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Header with drag handle + track info (matches _TrackInfoHeader)
|
||||
Column(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.82,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.4),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
|
||||
child: Row(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: track.coverUrl != null && track.coverUrl!.isNotEmpty
|
||||
? CachedNetworkImage(
|
||||
imageUrl: track.coverUrl!,
|
||||
width: 56,
|
||||
height: 56,
|
||||
fit: BoxFit.cover,
|
||||
memCacheWidth: 112,
|
||||
cacheManager: CoverCacheManager.instance,
|
||||
errorWidget: (context, url, error) => Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
child: Icon(
|
||||
Icons.music_note,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
child: Icon(
|
||||
Icons.music_note,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
// Header with drag handle + track info (matches _TrackInfoHeader)
|
||||
Column(
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.onSurfaceVariant.withValues(
|
||||
alpha: 0.4,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
track.name,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.w600),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
track.artistName,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
|
||||
child: Row(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child:
|
||||
track.coverUrl != null &&
|
||||
track.coverUrl!.isNotEmpty
|
||||
? CachedNetworkImage(
|
||||
imageUrl: track.coverUrl!,
|
||||
width: 56,
|
||||
height: 56,
|
||||
fit: BoxFit.cover,
|
||||
memCacheWidth: 112,
|
||||
cacheManager: CoverCacheManager.instance,
|
||||
errorWidget: (context, url, error) =>
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
color:
|
||||
colorScheme.surfaceContainerHighest,
|
||||
child: Icon(
|
||||
Icons.music_note,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
child: Icon(
|
||||
Icons.music_note,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
track.name,
|
||||
style: Theme.of(context).textTheme.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.w600),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
ClickableArtistName(
|
||||
artistName: track.artistName,
|
||||
artistId: track.artistId,
|
||||
coverUrl: track.coverUrl,
|
||||
style: Theme.of(context).textTheme.bodyMedium
|
||||
?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Divider(
|
||||
height: 1,
|
||||
color: colorScheme.outlineVariant.withValues(alpha: 0.5),
|
||||
),
|
||||
|
||||
// Action items (matches _QualityOption style)
|
||||
_OptionTile(
|
||||
icon: Icons.download_rounded,
|
||||
title: context.l10n.downloadTitle,
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
if (settings.askQualityBeforeDownload) {
|
||||
DownloadServicePicker.show(
|
||||
rootContext,
|
||||
trackName: track.name,
|
||||
artistName: track.artistName,
|
||||
coverUrl: track.coverUrl,
|
||||
onSelect: (quality, service) {
|
||||
container
|
||||
.read(downloadQueueProvider.notifier)
|
||||
.addToQueue(
|
||||
track,
|
||||
service,
|
||||
qualityOverride: quality,
|
||||
);
|
||||
ScaffoldMessenger.of(rootContext).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
rootContext.l10n.snackbarAddedToQueue(track.name),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
container
|
||||
.read(downloadQueueProvider.notifier)
|
||||
.addToQueue(track, settings.defaultService);
|
||||
if (!rootContext.mounted) {
|
||||
return;
|
||||
}
|
||||
ScaffoldMessenger.of(rootContext).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
rootContext.l10n.snackbarAddedToQueue(track.name),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
_OptionTile(
|
||||
icon: isLoved ? Icons.favorite : Icons.favorite_border,
|
||||
iconColor: isLoved ? colorScheme.error : null,
|
||||
title: isLoved
|
||||
? context.l10n.trackOptionRemoveFromLoved
|
||||
: context.l10n.trackOptionAddToLoved,
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
final added = await ref
|
||||
.read(libraryCollectionsProvider.notifier)
|
||||
.toggleLoved(track);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
added
|
||||
? context.l10n.collectionAddedToLoved(track.name)
|
||||
: context.l10n.collectionRemovedFromLoved(
|
||||
track.name,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
_OptionTile(
|
||||
icon: isInWishlist
|
||||
? Icons.playlist_add_check_circle
|
||||
: Icons.add_circle_outline,
|
||||
iconColor: isInWishlist ? colorScheme.primary : null,
|
||||
title: isInWishlist
|
||||
? context.l10n.trackOptionRemoveFromWishlist
|
||||
: context.l10n.trackOptionAddToWishlist,
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
final added = await ref
|
||||
.read(libraryCollectionsProvider.notifier)
|
||||
.toggleWishlist(track);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
added
|
||||
? context.l10n.collectionAddedToWishlist(track.name)
|
||||
: context.l10n.collectionRemovedFromWishlist(
|
||||
track.name,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
_OptionTile(
|
||||
icon: Icons.playlist_add,
|
||||
title: context.l10n.collectionAddToPlaylist,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
showAddTrackToPlaylistSheet(context, ref, track);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
Divider(
|
||||
height: 1,
|
||||
color: colorScheme.outlineVariant.withValues(alpha: 0.5),
|
||||
),
|
||||
|
||||
// Action items (matches _QualityOption style)
|
||||
_OptionTile(
|
||||
icon: isLoved ? Icons.favorite : Icons.favorite_border,
|
||||
iconColor: isLoved ? colorScheme.error : null,
|
||||
title: isLoved
|
||||
? context.l10n.trackOptionRemoveFromLoved
|
||||
: context.l10n.trackOptionAddToLoved,
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
final added = await ref
|
||||
.read(libraryCollectionsProvider.notifier)
|
||||
.toggleLoved(track);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
added
|
||||
? context.l10n.collectionAddedToLoved(track.name)
|
||||
: context.l10n.collectionRemovedFromLoved(track.name),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
_OptionTile(
|
||||
icon: isInWishlist
|
||||
? Icons.playlist_add_check_circle
|
||||
: Icons.add_circle_outline,
|
||||
iconColor: isInWishlist ? colorScheme.primary : null,
|
||||
title: isInWishlist
|
||||
? context.l10n.trackOptionRemoveFromWishlist
|
||||
: context.l10n.trackOptionAddToWishlist,
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
final added = await ref
|
||||
.read(libraryCollectionsProvider.notifier)
|
||||
.toggleWishlist(track);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
added
|
||||
? context.l10n.collectionAddedToWishlist(track.name)
|
||||
: context.l10n.collectionRemovedFromWishlist(
|
||||
track.name),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
_OptionTile(
|
||||
icon: Icons.playlist_add,
|
||||
title: context.l10n.collectionAddToPlaylist,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
showAddTrackToPlaylistSheet(context, ref, track);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user