From a37d762b1bd48bcb7e957fe6d476e27c8b8aaa23 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Thu, 18 Dec 2025 23:42:19 +0800 Subject: [PATCH] :bug: Bug fixes of libraries --- lib/data/playlist_repository.dart | 18 ++- lib/data/track_repository.dart | 19 ++- lib/ui/screens/library_screen.dart | 102 ++++++++++++ lib/ui/screens/settings_screen.dart | 235 +++++++++------------------- 4 files changed, 202 insertions(+), 172 deletions(-) diff --git a/lib/data/playlist_repository.dart b/lib/data/playlist_repository.dart index ed78d3b..e5c5744 100644 --- a/lib/data/playlist_repository.dart +++ b/lib/data/playlist_repository.dart @@ -71,16 +71,22 @@ class PlaylistRepository extends _$PlaylistRepository { Stream> watchAllAlbums() { final db = ref.watch(databaseProvider); - // Distinct albums by grouping + // Distinct albums by grouping - group by album name only to prevent duplicates + // when the same album has inconsistent artist metadata across tracks final query = db.selectOnly(db.tracks) - ..addColumns([db.tracks.album, db.tracks.artist, db.tracks.artUri]) - ..groupBy([db.tracks.album, db.tracks.artist]); + ..addColumns([ + db.tracks.album, + db.tracks.artist.min(), // Get the first non-null artist + db.tracks.artUri.min(), // Get the first non-null art URI + ]) + ..where(db.tracks.album.isNotNull()) + ..groupBy([db.tracks.album]); return query.map((row) { return AlbumData( - album: row.read(db.tracks.album) ?? 'Unknown Album', - artist: row.read(db.tracks.artist) ?? 'Unknown Artist', - artUri: row.read(db.tracks.artUri), + album: row.read(db.tracks.album)!, + artist: row.read(db.tracks.artist.min()) ?? 'Various Artists', + artUri: row.read(db.tracks.artUri.min()), ); }).watch(); } diff --git a/lib/data/track_repository.dart b/lib/data/track_repository.dart index 5a2d02b..da5ed97 100644 --- a/lib/data/track_repository.dart +++ b/lib/data/track_repository.dart @@ -24,13 +24,28 @@ class TrackRepository extends _$TrackRepository { } Future importFiles(List filePaths) async { + final db = ref.read(databaseProvider); final settings = ref.read(settingsProvider).value; final importMode = settings?.importMode ?? ImportMode.copy; + // Filter out files that are already indexed + final existingPaths = await (db.select( + db.tracks, + )..where((t) => t.path.isIn(filePaths))).map((t) => t.path).get(); + + final existingPathsSet = existingPaths.toSet(); + final newFilePaths = filePaths + .where((path) => !existingPathsSet.contains(path)) + .toList(); + + if (newFilePaths.isEmpty) { + return; // All files already indexed + } + if (importMode == ImportMode.copy) { - await _importFilesWithCopy(filePaths); + await _importFilesWithCopy(newFilePaths); } else { - await _importFilesInPlace(filePaths); + await _importFilesInPlace(newFilePaths); } } diff --git a/lib/ui/screens/library_screen.dart b/lib/ui/screens/library_screen.dart index 0bae43b..8b63cee 100644 --- a/lib/ui/screens/library_screen.dart +++ b/lib/ui/screens/library_screen.dart @@ -8,6 +8,7 @@ import 'package:groovybox/data/playlist_repository.dart'; import 'package:groovybox/data/track_repository.dart'; import 'package:groovybox/logic/lyrics_parser.dart'; import 'package:groovybox/providers/audio_provider.dart'; +import 'package:groovybox/providers/watch_folder_provider.dart'; import 'package:groovybox/ui/screens/settings_screen.dart'; import 'package:groovybox/ui/tabs/albums_tab.dart'; import 'package:groovybox/ui/tabs/playlists_tab.dart'; @@ -597,6 +598,14 @@ class LibraryScreen extends HookConsumerWidget { _showAddToPlaylistDialog(context, ref, track); }, ), + ListTile( + leading: const Icon(Icons.info_outline), + title: const Text('View Details'), + onTap: () { + Navigator.pop(context); + _showTrackDetails(context, ref, track); + }, + ), ListTile( leading: const Icon(Icons.edit), title: const Text('Edit Metadata'), @@ -708,6 +717,99 @@ class LibraryScreen extends HookConsumerWidget { ); } + void _showTrackDetails( + BuildContext context, + WidgetRef ref, + Track track, + ) async { + // Try to get file info + String fileSize = 'Unknown'; + String libraryName = 'Unknown'; + String dateAdded = 'Unknown'; + + try { + final file = File(track.path); + if (await file.exists()) { + final stat = await file.stat(); + final sizeInMB = (stat.size / (1024 * 1024)).toStringAsFixed(2); + fileSize = '${sizeInMB} MB'; + dateAdded = stat.modified.toString().split( + ' ', + )[0]; // Just the date part + } + } catch (e) { + // Ignore file access errors + } + + // Try to find which library this track belongs to + final watchFoldersAsync = ref.read(watchFoldersProvider); + watchFoldersAsync.whenData((folders) { + for (final folder in folders) { + if (track.path.startsWith(folder.path)) { + libraryName = folder.name; + break; + } + } + }); + + final screenSize = MediaQuery.of(context).size; + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Track Details'), + content: ConstrainedBox( + constraints: BoxConstraints(maxWidth: screenSize.width * 0.8), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _buildDetailRow('Title', track.title), + _buildDetailRow('Artist', track.artist ?? 'Unknown'), + _buildDetailRow('Album', track.album ?? 'Unknown'), + _buildDetailRow('Duration', _formatDuration(track.duration)), + _buildDetailRow('File Size', fileSize), + _buildDetailRow('Library', libraryName), + _buildDetailRow('File Path', track.path), + _buildDetailRow('Date Added', dateAdded), + if (track.artUri != null) + _buildDetailRow('Album Art', 'Present'), + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Close'), + ), + ], + ), + ); + } + + Widget _buildDetailRow(String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 100, + child: Text( + '$label:', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + Expanded( + child: Text(value, style: const TextStyle(color: Colors.grey)), + ), + ], + ), + ); + } + void _showEditDialog(BuildContext context, WidgetRef ref, Track track) { final titleController = TextEditingController(text: track.title); final artistController = TextEditingController(text: track.artist); diff --git a/lib/ui/screens/settings_screen.dart b/lib/ui/screens/settings_screen.dart index dc14c46..a967b2c 100644 --- a/lib/ui/screens/settings_screen.dart +++ b/lib/ui/screens/settings_screen.dart @@ -20,85 +20,6 @@ class SettingsScreen extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Import Mode Section - Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Import Mode', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Column( - children: [ - ListTile( - title: Text(ImportMode.copy.displayName), - subtitle: const Text( - 'Copy music files to internal storage', - ), - leading: RadioGroup( - groupValue: settings.importMode, - onChanged: (value) { - if (value != null) { - ref - .read(importModeProvider.notifier) - .update(value); - } - }, - child: Radio(value: ImportMode.copy), - ), - ), - ListTile( - title: Text(ImportMode.inplace.displayName), - subtitle: const Text( - 'Index music files in their original location', - ), - leading: RadioGroup( - groupValue: settings.importMode, - onChanged: (value) { - if (value != null) { - ref - .read(importModeProvider.notifier) - .update(value); - } - }, - child: Radio( - value: ImportMode.inplace, - ), - ), - ), - ListTile( - title: Text(ImportMode.mixed.displayName), - subtitle: const Text( - 'Use internal storage and add folder libraries', - ), - leading: RadioGroup( - groupValue: settings.importMode, - onChanged: (value) { - if (value != null) { - ref - .read(importModeProvider.notifier) - .update(value); - } - }, - child: Radio(value: ImportMode.mixed), - ), - ), - ], - ), - ], - ), - ), - ), - - const SizedBox(height: 16), - // Auto Scan Section Card( child: Padding( @@ -160,95 +81,81 @@ class SettingsScreen extends ConsumerWidget { fontWeight: FontWeight.bold, ), ), - if (settings.importMode == ImportMode.inplace || - settings.importMode == ImportMode.mixed) - Row( - children: [ - IconButton( - onPressed: () => _scanLibraries(context, ref), - icon: const Icon(Icons.refresh), - tooltip: 'Scan Libraries', - ), - IconButton( - onPressed: () => - _addMusicLibrary(context, ref), - icon: const Icon(Icons.add), - tooltip: 'Add Music Library', - ), - ], - ), + Row( + children: [ + IconButton( + onPressed: () => _scanLibraries(context, ref), + icon: const Icon(Icons.refresh), + tooltip: 'Scan Libraries', + ), + IconButton( + onPressed: () => _addMusicLibrary(context, ref), + icon: const Icon(Icons.add), + tooltip: 'Add Music Library', + ), + ], + ), ], ), const SizedBox(height: 8), - if (settings.importMode == ImportMode.inplace || - settings.importMode == ImportMode.mixed) ...[ - const Text( - 'Add folder libraries to index music files in their original location.', - style: TextStyle(color: Colors.grey, fontSize: 14), - ), - const SizedBox(height: 8), - ], - if (settings.importMode == ImportMode.copy) - const Text( - 'Folder libraries are available in in-place and mixed modes.', - style: TextStyle(color: Colors.grey, fontSize: 14), - ), - if (settings.importMode == ImportMode.inplace || - settings.importMode == ImportMode.mixed) ...[ - watchFoldersAsync.when( - data: (folders) => folders.isEmpty - ? const Text( - 'No music libraries added yet.', - style: TextStyle( - color: Colors.grey, - fontSize: 14, - ), - ) - : Column( - children: folders - .map( - (folder) => ListTile( - title: Text(folder.name), - subtitle: Text(folder.path), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Switch( - value: folder.isActive, - onChanged: (value) { - ref - .read( - watchFolderServiceProvider, - ) - .toggleWatchFolder( - folder.id, - value, - ); - }, - ), - IconButton( - icon: const Icon(Icons.delete), - onPressed: () { - ref - .read( - watchFolderServiceProvider, - ) - .removeWatchFolder( - folder.id, - ); - }, - ), - ], - ), - ), - ) - .toList(), + const Text( + 'Add folder libraries to index music files. Files will be copied to internal storage for playback.', + style: TextStyle(color: Colors.grey, fontSize: 14), + ), + const SizedBox(height: 8), + watchFoldersAsync.when( + data: (folders) => folders.isEmpty + ? const Text( + 'No music libraries added yet.', + style: TextStyle( + color: Colors.grey, + fontSize: 14, ), - loading: () => const CircularProgressIndicator(), - error: (error, _) => - Text('Error loading libraries: $error'), - ), - ], + ) + : Column( + children: folders + .map( + (folder) => ListTile( + title: Text(folder.name), + subtitle: Text(folder.path), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Switch( + value: folder.isActive, + onChanged: (value) { + ref + .read( + watchFolderServiceProvider, + ) + .toggleWatchFolder( + folder.id, + value, + ); + }, + ), + IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + ref + .read( + watchFolderServiceProvider, + ) + .removeWatchFolder( + folder.id, + ); + }, + ), + ], + ), + ), + ) + .toList(), + ), + loading: () => const CircularProgressIndicator(), + error: (error, _) => + Text('Error loading libraries: $error'), + ), ], ), ),