Library able to search tracks

This commit is contained in:
2025-12-17 00:03:16 +08:00
parent f436a8a49b
commit c1652e6743

View File

@@ -46,6 +46,7 @@ class LibraryScreen extends HookConsumerWidget {
// Let's assume we use StreamBuilder for now to avoid creating another file/provider on the fly. // Let's assume we use StreamBuilder for now to avoid creating another file/provider on the fly.
final repo = ref.watch(trackRepositoryProvider.notifier); final repo = ref.watch(trackRepositoryProvider.notifier);
final selectedTrackIds = useState<Set<int>>({}); final selectedTrackIds = useState<Set<int>>({});
final searchQuery = useState<String>('');
final isSelectionMode = selectedTrackIds.value.isNotEmpty; final isSelectionMode = selectedTrackIds.value.isNotEmpty;
void toggleSelection(int id) { void toggleSelection(int id) {
@@ -180,135 +181,192 @@ class LibraryScreen extends HookConsumerWidget {
return const Center(child: Text('No tracks yet. Add some!')); return const Center(child: Text('No tracks yet. Add some!'));
} }
return ListView.builder( List<Track> filteredTracks;
padding: EdgeInsets.only( if (searchQuery.value.isEmpty) {
bottom: 72 + MediaQuery.paddingOf(context).bottom, filteredTracks = tracks;
), } else {
itemCount: tracks.length, final query = searchQuery.value.toLowerCase();
itemBuilder: (context, index) { filteredTracks = tracks.where((track) {
final track = tracks[index]; if (track.title.toLowerCase().contains(query)) return true;
final isSelected = selectedTrackIds.value.contains( if (track.artist?.toLowerCase().contains(query) ?? false)
track.id, return true;
); if (track.album?.toLowerCase().contains(query) ?? false)
return true;
if (isSelectionMode) { if (track.lyrics != null) {
return ListTile( try {
selected: isSelected, final lyricsData = LyricsData.fromJsonString(
selectedTileColor: Colors.white10, track.lyrics!,
leading: Checkbox( );
value: isSelected, for (final line in lyricsData.lines) {
onChanged: (_) => toggleSelection(track.id), if (line.text.toLowerCase().contains(query))
), return true;
title: Text( }
track.title, } catch (e) {
maxLines: 1, // Ignore parsing errors
overflow: TextOverflow.ellipsis, }
),
subtitle: Text(
'${track.artist ?? 'Unknown Artist'}${_formatDuration(track.duration)}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
onTap: () => toggleSelection(track.id),
);
} }
return false;
}).toList();
}
return Dismissible( if (filteredTracks.isEmpty && searchQuery.value.isNotEmpty) {
key: Key('track_${track.id}'), return const Center(
direction: DismissDirection.endToStart, child: Text('No tracks match your search.'),
background: Container( );
color: Colors.red, }
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20), return Column(
child: const Icon(Icons.delete, color: Colors.white), children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
onChanged: (value) => searchQuery.value = value,
decoration: const InputDecoration(
hintText: 'Search tracks...',
prefixIcon: Icon(Icons.search),
),
), ),
confirmDismiss: (direction) async { ),
return await showDialog( Expanded(
context: context, child: ListView.builder(
builder: (context) { padding: EdgeInsets.only(
return AlertDialog( bottom: 72 + MediaQuery.paddingOf(context).bottom,
title: const Text('Delete Track?'), ),
content: Text( itemCount: filteredTracks.length,
'Are you sure you want to delete "${track.title}"? This cannot be undone.', itemBuilder: (context, index) {
final track = filteredTracks[index];
final isSelected = selectedTrackIds.value.contains(
track.id,
);
if (isSelectionMode) {
return ListTile(
selected: isSelected,
selectedTileColor: Colors.white10,
leading: Checkbox(
value: isSelected,
onChanged: (_) => toggleSelection(track.id),
), ),
actions: [ title: Text(
TextButton( track.title,
onPressed: () => maxLines: 1,
Navigator.of(context).pop(false), overflow: TextOverflow.ellipsis,
child: const Text('Cancel'), ),
), subtitle: Text(
TextButton( '${track.artist ?? 'Unknown Artist'}${_formatDuration(track.duration)}',
onPressed: () => maxLines: 1,
Navigator.of(context).pop(true), overflow: TextOverflow.ellipsis,
style: TextButton.styleFrom( ),
foregroundColor: Colors.red, onTap: () => toggleSelection(track.id),
),
child: const Text('Delete'),
),
],
); );
}, }
);
}, return Dismissible(
onDismissed: (direction) { key: Key('track_${track.id}'),
ref direction: DismissDirection.endToStart,
.read(trackRepositoryProvider.notifier) background: Container(
.deleteTrack(track.id); color: Colors.red,
ScaffoldMessenger.of(context).showSnackBar( alignment: Alignment.centerRight,
SnackBar(content: Text('Deleted "${track.title}"')), padding: const EdgeInsets.only(right: 20),
); child: const Icon(
}, Icons.delete,
child: ListTile( color: Colors.white,
leading: AspectRatio(
aspectRatio: 1,
child: Container(
decoration: BoxDecoration(
color: Colors.grey[800],
borderRadius: BorderRadius.circular(8),
image: track.artUri != null
? DecorationImage(
image: FileImage(File(track.artUri!)),
fit: BoxFit.cover,
)
: null,
),
child: track.artUri == null
? const Icon(
Icons.music_note,
color: Colors.white54,
)
: null,
),
),
title: Text(
track.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
'${track.artist ?? 'Unknown Artist'}${_formatDuration(track.duration)}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: isSelectionMode
? null
: IconButton(
icon: const Icon(Icons.more_vert),
onPressed: () {
_showTrackOptions(context, ref, track);
},
), ),
onTap: () { ),
final audio = ref.read(audioHandlerProvider); confirmDismiss: (direction) async {
audio.playTrack(track); return await showDialog(
}, context: context,
onLongPress: () { builder: (context) {
// Enter selection mode return AlertDialog(
toggleSelection(track.id); title: const Text('Delete Track?'),
content: Text(
'Are you sure you want to delete "${track.title}"? This cannot be undone.',
),
actions: [
TextButton(
onPressed: () =>
Navigator.of(context).pop(false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () =>
Navigator.of(context).pop(true),
style: TextButton.styleFrom(
foregroundColor: Colors.red,
),
child: const Text('Delete'),
),
],
);
},
);
},
onDismissed: (direction) {
ref
.read(trackRepositoryProvider.notifier)
.deleteTrack(track.id);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Deleted "${track.title}"'),
),
);
},
child: ListTile(
leading: AspectRatio(
aspectRatio: 1,
child: Container(
decoration: BoxDecoration(
color: Colors.grey[800],
borderRadius: BorderRadius.circular(8),
image: track.artUri != null
? DecorationImage(
image: FileImage(
File(track.artUri!),
),
fit: BoxFit.cover,
)
: null,
),
child: track.artUri == null
? const Icon(
Icons.music_note,
color: Colors.white54,
)
: null,
),
),
title: Text(
track.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
'${track.artist ?? 'Unknown Artist'}${_formatDuration(track.duration)}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: isSelectionMode
? null
: IconButton(
icon: const Icon(Icons.more_vert),
onPressed: () {
_showTrackOptions(context, ref, track);
},
),
onTap: () {
final audio = ref.read(audioHandlerProvider);
audio.playTrack(track);
},
onLongPress: () {
// Enter selection mode
toggleSelection(track.id);
},
),
);
}, },
), ),
); ),
}, ],
); );
}, },
), ),