✨ Library able to search tracks
This commit is contained in:
@@ -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);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user