Neue Melo-UI Phase 1: 4 Tabs (Meine Musik/Suche/Favoriten/Download) + Sortierung

- Meine Musik als Startbildschirm ohne Topbar: Kopfzeile (Einstellungen,
  Suche, Musikerkennung), Schnellzugriffe, Shuffle + Sortieren, Liederliste
- Sortierung nach Zuletzt hinzugefuegt / Name (A-Z-#) / Wie oft abgespielt,
  auf- und absteigend, pro Liste gespeichert (SortStore)
- Favoriten als eigener Tab mit gleicher Shuffle-/Sortier-Leiste
- Download-Tab uebernimmt den Navidrome-Server-Browser
- DB-Schema 3: playCount, gezaehlt beim Titelstart
- Theme auf #0B0B10 / #c0392b
- Scan-Aktionen aus der entfallenen Topbar jetzt in den Einstellungen

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011snPXPafPC5i87o68H14W7
This commit is contained in:
2026-08-20 18:00:53 +02:00
co-authored by Claude Opus 5
parent 5d08da1086
commit 2667613a2e
24 changed files with 1413 additions and 372 deletions
+61
View File
@@ -0,0 +1,61 @@
import 'database.dart';
/// Sortier-Kriterien der Liederlisten (Meine Musik + Favoriten).
enum SortMode { dateAdded, name, playCount }
extension SortModeLabel on SortMode {
String get label => switch (this) {
SortMode.dateAdded => 'Zuletzt hinzugefügt',
SortMode.name => 'Name (AZ#)',
SortMode.playCount => 'Wie oft abgespielt',
};
}
/// Richtung, in der ein Modus üblicherweise gelesen wird: neueste bzw.
/// meistgespielte Songs zuerst, Namen dagegen von A nach Z.
bool defaultAscending(SortMode mode) => mode == SortMode.name;
/// Sortiert eine Kopie von [songs]. Bei gleichem Sortierwert entscheidet der
/// Titel, damit die Reihenfolge zwischen Aufrufen stabil bleibt.
List<Song> sortSongs(List<Song> songs, SortMode mode, {required bool ascending}) {
final sorted = [...songs];
sorted.sort((a, b) {
final cmp = switch (mode) {
SortMode.dateAdded => a.dateAddedMs.compareTo(b.dateAddedMs),
SortMode.playCount => a.playCount.compareTo(b.playCount),
SortMode.name => _compareNames(a.title, b.title),
};
if (cmp != 0) return ascending ? cmp : -cmp;
// Tiebreak immer aufsteigend nach Titel — sonst wäre die Reihenfolge
// gleichwertiger Songs beim Umschalten der Richtung willkürlich.
return _compareNames(a.title, b.title);
});
return sorted;
}
/// AZ zuerst, alles was nicht mit einem Buchstaben beginnt (Ziffern,
/// Sonderzeichen) landet unter "#" am Ende.
int _compareNames(String a, String b) {
final keyA = _sortKey(a);
final keyB = _sortKey(b);
final letterA = _startsWithLetter(keyA);
final letterB = _startsWithLetter(keyB);
if (letterA != letterB) return letterA ? -1 : 1;
return keyA.compareTo(keyB);
}
final _letter = RegExp(r'^\p{L}', unicode: true);
bool _startsWithLetter(String s) => _letter.hasMatch(s);
/// Kleinschreibung + Umlaute auf ihren Grundbuchstaben, damit "Ärger"
/// zwischen "Anfang" und "Berg" steht und nicht hinter "Zug".
String _sortKey(String title) {
return title
.trim()
.toLowerCase()
.replaceAll('ä', 'a')
.replaceAll('ö', 'o')
.replaceAll('ü', 'u')
.replaceAll('ß', 'ss');
}