Files
Melo/lib/library/categories.dart
T
dustinandClaude Opus 5 7aa98864aa Kategorien (mehrere je Song), Metadaten-Ansicht mit Expertenmodus
- 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
2026-08-20 18:29:45 +02:00

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;
}