🐛 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() {
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();
}

View File

@@ -24,13 +24,28 @@ class TrackRepository extends _$TrackRepository {
}
Future<void> importFiles(List<String> 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);
}
}

View File

@@ -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);

View File

@@ -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<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
Card(
child: Padding(
@@ -160,8 +81,6 @@ class SettingsScreen extends ConsumerWidget {
fontWeight: FontWeight.bold,
),
),
if (settings.importMode == ImportMode.inplace ||
settings.importMode == ImportMode.mixed)
Row(
children: [
IconButton(
@@ -170,8 +89,7 @@ class SettingsScreen extends ConsumerWidget {
tooltip: 'Scan Libraries',
),
IconButton(
onPressed: () =>
_addMusicLibrary(context, ref),
onPressed: () => _addMusicLibrary(context, ref),
icon: const Icon(Icons.add),
tooltip: 'Add Music Library',
),
@@ -180,21 +98,11 @@ class SettingsScreen extends ConsumerWidget {
],
),
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.',
'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),
],
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(
@@ -249,7 +157,6 @@ class SettingsScreen extends ConsumerWidget {
Text('Error loading libraries: $error'),
),
],
],
),
),
),