Files
Melo/test/player/lrc_test.dart
Hermes (Server)andClaude Opus 5 0d76d8cc58 Album-Ansicht, mitlaufender Songtext, Cover-Cache, Server-Favoriten
Die letzten vier Punkte aus dem Review-Bericht.

G2 — Server-Bereich: Ein Tipp auf ein Album spielte sofort alles ab;
hineinsehen ging nicht. Jetzt Album- und Kuenstler-Ansicht mit Cover,
Kuenstler, Titelzahl, Gesamtdauer, "Abspielen"/"Zufaellig" und der
Titelliste; Antippen startet an dieser Stelle. Dazu "Mehr Alben laden" —
die Liste hoerte bei 50 auf, ohne dass etwas darauf hinwies.
NICHT umgesetzt: den Server-Bereich aus dem Download-Tab loesen (das
widerspricht Commit 34d6d8e, "Auf Wunsch von Dustin") und eine
Server-Suche (die Suche bleibt ausdruecklich rein lokal).

G8 — Songtext: Traegt der Text Zeitmarken, laeuft er mit; die aktive Zeile
ist hervorgehoben, Antippen springt an die Stelle. Ohne Zeitmarken bleibt
es eine Textwand — die App schaut nach, statt zu raten.

G10 — Cover bleiben liegen. Eigener Ordner, einmal geholt. Der Schluessel
haengt an der Album-Kennung, nicht an der Adresse: die traegt bei jedem
App-Start ein neues Token, und der Ordner waere sonst gewachsen, ohne je
einen Treffer zu liefern. Keine neue Abhaengigkeit.

G3 — Das Herz ist bei Server-Titeln nicht mehr ausgegraut, sondern
favorisiert am Server (star/unstar) — dort, wo der Favoriten-Abgleich sie
auch wieder abholt. Offen bleibt der schwere Teil: Wiedergabezaehler,
Historie und Kategorien braeuchten fuer Server-Titel eine Zeile in der
Bibliothek.

Aus dem Code-Review nachgebessert (sieben Punkte): das Herz behielt beim
Titelwechsel seinen Zustand; recycelte Listenzeilen zeigten kurz das Cover
der vorherigen; der neue Songtext bekam bei Server-Titeln die
Abspiel-Adresse statt der Server-Kennung und fand deshalb nie einen Text;
ein einmal fehlgeschlagenes Cover blieb die ganze Sitzung grau; "Server
nicht erreichbar" sah aus wie "Album hat keine Titel", womit der
Wiederholen-Knopf unerreichbar war; die Kuenstler-Ansicht waere immer leer
geblieben, weil getArtist die Titel gar nicht mitliefert; und zwei
gleichzeitige Cover-Abrufe konnten ein zerschnittenes Bild hinterlassen.

Selbst gefunden: das Haekchen "schon geladen" fragte pro Zeile die Platte
ab, bei jedem Positionswechsel neu. Jetzt einmal je Bildschirm.

457 Tests gruen (vorher 433), flutter analyze ohne Befund, Release-APK
gebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xAHJTJM6UUqmjUgk1PUEd
2026-08-23 21:53:31 +02:00

100 lines
3.2 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:melo/player/lrc.dart';
/// Der Songtext war eine Textwand ohne Bezug zur Wiedergabe. Diese Regeln
/// machen aus einem LRC-Text mitlaufende Zeilen.
void main() {
group('parseLrc', () {
test('liest Zeitmarke und Text', () {
final zeilen = parseLrc('[00:12.50]Erste Zeile');
expect(zeilen, hasLength(1));
expect(zeilen.single.zeit, const Duration(seconds: 12, milliseconds: 500));
expect(zeilen.single.text, 'Erste Zeile');
});
test('versteht Hundertstel und Tausendstel', () {
expect(parseLrc('[00:01.5]A').single.zeit,
const Duration(seconds: 1, milliseconds: 500));
expect(parseLrc('[00:01.05]A').single.zeit,
const Duration(seconds: 1, milliseconds: 50));
expect(parseLrc('[00:01.050]A').single.zeit,
const Duration(seconds: 1, milliseconds: 50));
});
test('kommt auch ohne Bruchteile aus', () {
expect(parseLrc('[01:30]A').single.zeit, const Duration(minutes: 1, seconds: 30));
});
test('sortiert nach Zeit, auch wenn die Datei es nicht tut', () {
final zeilen = parseLrc('[00:20.00]Zwei\n[00:10.00]Eins');
expect(zeilen.map((z) => z.text), ['Eins', 'Zwei']);
});
test('eine Zeile mit mehreren Marken erscheint mehrfach', () {
// Im Refrain üblich: dieselbe Zeile für mehrere Stellen.
final zeilen = parseLrc('[00:10.00][01:10.00]Refrain');
expect(zeilen, hasLength(2));
expect(zeilen.first.zeit, const Duration(seconds: 10));
expect(zeilen.last.zeit, const Duration(minutes: 1, seconds: 10));
});
test('überspringt Kopfzeilen wie [ar:] und [ti:]', () {
final zeilen = parseLrc('[ar:Neoni]\n[ti:Roses]\n[00:05.00]Los');
expect(zeilen.map((z) => z.text), ['Los']);
});
test('leere Textzeilen bleiben erhalten — sie sind Pausen', () {
final zeilen = parseLrc('[00:05.00]A\n[00:08.00]\n[00:12.00]B');
expect(zeilen, hasLength(3));
expect(zeilen[1].text, isEmpty);
});
test('gewöhnlicher Text ohne Marken ergibt nichts', () {
expect(parseLrc('Nur eine Textwand\nohne Zeitangaben'), isEmpty);
});
test('leerer Text ergibt nichts', () {
expect(parseLrc(''), isEmpty);
});
});
group('istSynchron', () {
test('erkennt einen LRC-Text', () {
expect(istSynchron('[00:12.50]Zeile'), isTrue);
});
test('eine Textwand ist nicht synchron', () {
expect(istSynchron('Roses are red\nViolets are blue'), isFalse);
});
});
group('aktiveZeile', () {
final zeilen = parseLrc('[00:00.00]Null\n[00:10.00]Zehn\n[00:20.00]Zwanzig');
test('vor der ersten Marke gilt die erste Zeile', () {
expect(aktiveZeile(zeilen, Duration.zero), 0);
});
test('genau auf einer Marke gilt diese Zeile', () {
expect(aktiveZeile(zeilen, const Duration(seconds: 10)), 1);
});
test('zwischen zwei Marken gilt die frühere', () {
expect(aktiveZeile(zeilen, const Duration(seconds: 15)), 1);
});
test('nach der letzten Marke gilt die letzte Zeile', () {
expect(aktiveZeile(zeilen, const Duration(minutes: 5)), 2);
});
test('ohne Zeilen gibt es keine aktive', () {
expect(aktiveZeile(const [], Duration.zero), isNull);
});
});
}