- DB-Schema 4: Tabelle song_categories + Spalte categories_edited - Kategorien kommen aus dem Genre-Tag (Trennung an ; , | /), manuell bearbeitbar; von Hand gesetzte Kategorien ueberlebt der naechste Scan - Songzeile zeigt "Kuenstler | Kategorie1 - Kategorie2" - Einstellung "Gleiche Kategorie = gleiches Coverbild" (Standard an) - Metadaten-Sheet mit ausklappbarem Expertenmodus - Neu: categories.dart, category_service.dart, song_detail_sheet.dart, app_settings.dart Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011snPXPafPC5i87o68H14W7
62 lines
2.2 KiB
Dart
62 lines
2.2 KiB
Dart
import 'database.dart';
|
|
|
|
/// Trennzeichen, mit denen Genre-Tags mehrere Werte in einem Feld ablegen.
|
|
final _separators = RegExp(r'[;,|/\n]');
|
|
|
|
/// Zerlegt einen Genre-Tag in einzelne Kategorien. Doppelte Einträge fallen
|
|
/// weg (ohne Rücksicht auf Groß-/Kleinschreibung), die erste Schreibweise
|
|
/// gewinnt.
|
|
List<String> parseCategories(String? raw) {
|
|
if (raw == null) return const [];
|
|
return _dedupe(raw.split(_separators).map((e) => e.trim()));
|
|
}
|
|
|
|
/// Wie [parseCategories], aber für Tag-Formate, die bereits mehrere Felder
|
|
/// liefern (z. B. ID3v2 mit mehreren GENRE-Frames).
|
|
List<String> parseCategoryList(Iterable<String> raw) {
|
|
return _dedupe(raw.expand((e) => e.split(_separators)).map((e) => e.trim()));
|
|
}
|
|
|
|
List<String> _dedupe(Iterable<String> candidates) {
|
|
final seen = <String>{};
|
|
final result = <String>[];
|
|
for (final candidate in candidates) {
|
|
if (candidate.isEmpty) continue;
|
|
if (seen.add(candidate.toLowerCase())) result.add(candidate);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// Ermittelt je Kategorie ein Coverbild: das erste vorhandene Cover unter den
|
|
/// Songs der Kategorie, nach Titel geordnet — damit dieselbe Bibliothek immer
|
|
/// dasselbe Bild ergibt. Kategorien ohne jedes Cover fehlen im Ergebnis.
|
|
Map<String, String> categoryCovers(
|
|
List<Song> songs,
|
|
Map<String, List<String>> categoriesBySong,
|
|
) {
|
|
final ordered = [...songs]
|
|
..sort((a, b) => a.title.toLowerCase().compareTo(b.title.toLowerCase()));
|
|
final covers = <String, String>{};
|
|
for (final song in ordered) {
|
|
final cover = song.coverPath;
|
|
if (cover == null) continue;
|
|
for (final category in categoriesBySong[song.id] ?? const <String>[]) {
|
|
covers.putIfAbsent(category, () => cover);
|
|
}
|
|
}
|
|
return covers;
|
|
}
|
|
|
|
/// Das anzuzeigende Cover eines Songs: bei aktiver Einstellung "Gleiche
|
|
/// Kategorie = gleiches Coverbild" das Bild seiner **ersten** Kategorie,
|
|
/// sonst (oder wenn die Kategorie kein Bild hat) sein eigenes.
|
|
String? effectiveCover(
|
|
Song song,
|
|
List<String> categories,
|
|
Map<String, String> covers, {
|
|
required bool groupByCategory,
|
|
}) {
|
|
if (!groupByCategory || categories.isEmpty) return song.coverPath;
|
|
return covers[categories.first] ?? song.coverPath;
|
|
}
|