🐛 Bug fixes of libraries

This commit is contained in:
2025-12-18 23:42:19 +08:00
parent 4f6e5883b7
commit a37d762b1b
4 changed files with 202 additions and 172 deletions

View File

@@ -71,16 +71,22 @@ class PlaylistRepository extends _$PlaylistRepository {
Stream<List<AlbumData>> watchAllAlbums() { Stream<List<AlbumData>> watchAllAlbums() {
final db = ref.watch(databaseProvider); 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) final query = db.selectOnly(db.tracks)
..addColumns([db.tracks.album, db.tracks.artist, db.tracks.artUri]) ..addColumns([
..groupBy([db.tracks.album, db.tracks.artist]); 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 query.map((row) {
return AlbumData( return AlbumData(
album: row.read(db.tracks.album) ?? 'Unknown Album', album: row.read(db.tracks.album)!,
artist: row.read(db.tracks.artist) ?? 'Unknown Artist', artist: row.read(db.tracks.artist.min()) ?? 'Various Artists',
artUri: row.read(db.tracks.artUri), artUri: row.read(db.tracks.artUri.min()),
); );
}).watch(); }).watch();
} }

View File

@@ -24,13 +24,28 @@ class TrackRepository extends _$TrackRepository {
} }
Future<void> importFiles(List<String> filePaths) async { Future<void> importFiles(List<String> filePaths) async {
final db = ref.read(databaseProvider);
final settings = ref.read(settingsProvider).value; final settings = ref.read(settingsProvider).value;
final importMode = settings?.importMode ?? ImportMode.copy; 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) { if (importMode == ImportMode.copy) {
await _importFilesWithCopy(filePaths); await _importFilesWithCopy(newFilePaths);
} else { } else {
await _importFilesInPlace(filePaths); await _importFilesInPlace(newFilePaths);
} }
} }

View File

@@ -8,6 +8,7 @@ import 'package:groovybox/data/playlist_repository.dart';
import 'package:groovybox/data/track_repository.dart'; import 'package:groovybox/data/track_repository.dart';
import 'package:groovybox/logic/lyrics_parser.dart'; import 'package:groovybox/logic/lyrics_parser.dart';
import 'package:groovybox/providers/audio_provider.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/screens/settings_screen.dart';
import 'package:groovybox/ui/tabs/albums_tab.dart'; import 'package:groovybox/ui/tabs/albums_tab.dart';
import 'package:groovybox/ui/tabs/playlists_tab.dart'; import 'package:groovybox/ui/tabs/playlists_tab.dart';
@@ -597,6 +598,14 @@ class LibraryScreen extends HookConsumerWidget {
_showAddToPlaylistDialog(context, ref, track); _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( ListTile(
leading: const Icon(Icons.edit), leading: const Icon(Icons.edit),
title: const Text('Edit Metadata'), 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) { void _showEditDialog(BuildContext context, WidgetRef ref, Track track) {
final titleController = TextEditingController(text: track.title); final titleController = TextEditingController(text: track.title);
final artistController = TextEditingController(text: track.artist); final artistController = TextEditingController(text: track.artist);

View File

@@ -20,85 +20,6 @@ class SettingsScreen extends ConsumerWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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<ImportMode>(
groupValue: settings.importMode,
onChanged: (value) {
if (value != null) {
ref
.read(importModeProvider.notifier)
.update(value);
}
},
child: Radio<ImportMode>(value: ImportMode.copy),
),
),
ListTile(
title: Text(ImportMode.inplace.displayName),
subtitle: const Text(
'Index music files in their original location',
),
leading: RadioGroup<ImportMode>(
groupValue: settings.importMode,
onChanged: (value) {
if (value != null) {
ref
.read(importModeProvider.notifier)
.update(value);
}
},
child: Radio<ImportMode>(
value: ImportMode.inplace,
),
),
),
ListTile(
title: Text(ImportMode.mixed.displayName),
subtitle: const Text(
'Use internal storage and add folder libraries',
),
leading: RadioGroup<ImportMode>(
groupValue: settings.importMode,
onChanged: (value) {
if (value != null) {
ref
.read(importModeProvider.notifier)
.update(value);
}
},
child: Radio<ImportMode>(value: ImportMode.mixed),
),
),
],
),
],
),
),
),
const SizedBox(height: 16),
// Auto Scan Section // Auto Scan Section
Card( Card(
child: Padding( child: Padding(
@@ -160,8 +81,6 @@ class SettingsScreen extends ConsumerWidget {
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
if (settings.importMode == ImportMode.inplace ||
settings.importMode == ImportMode.mixed)
Row( Row(
children: [ children: [
IconButton( IconButton(
@@ -170,8 +89,7 @@ class SettingsScreen extends ConsumerWidget {
tooltip: 'Scan Libraries', tooltip: 'Scan Libraries',
), ),
IconButton( IconButton(
onPressed: () => onPressed: () => _addMusicLibrary(context, ref),
_addMusicLibrary(context, ref),
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
tooltip: 'Add Music Library', tooltip: 'Add Music Library',
), ),
@@ -180,21 +98,11 @@ class SettingsScreen extends ConsumerWidget {
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
if (settings.importMode == ImportMode.inplace ||
settings.importMode == ImportMode.mixed) ...[
const Text( const Text(
'Add folder libraries to index music files in their original location.', 'Add folder libraries to index music files. Files will be copied to internal storage for playback.',
style: TextStyle(color: Colors.grey, fontSize: 14), style: TextStyle(color: Colors.grey, fontSize: 14),
), ),
const SizedBox(height: 8), 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( watchFoldersAsync.when(
data: (folders) => folders.isEmpty data: (folders) => folders.isEmpty
? const Text( ? const Text(
@@ -249,7 +157,6 @@ class SettingsScreen extends ConsumerWidget {
Text('Error loading libraries: $error'), Text('Error loading libraries: $error'),
), ),
], ],
],
), ),
), ),
), ),