/// Eine Zeile eines mitlaufenden Songtexts. class LyricZeile { const LyricZeile({required this.zeit, required this.text}); final Duration zeit; final String text; } /// Zeitmarke am Zeilenanfang: `[mm:ss]`, `[mm:ss.x]`, `[mm:ss.xx]` oder /// `[mm:ss.xxx]`. Mehrere Marken vor derselben Zeile sind erlaubt und im /// Refrain üblich. final _marke = RegExp(r'\[(\d{1,3}):(\d{1,2})(?:[.:](\d{1,3}))?\]'); /// Ob [text] ein LRC-Text ist — also überhaupt Zeitmarken trägt. /// /// Der Songtext kann aus dem Tag der Datei oder vom Server kommen, und beides /// liefert mal die eine, mal die andere Form. Statt zu raten, wird geschaut. bool istSynchron(String text) => _marke.hasMatch(text); /// Zerlegt einen LRC-Text in Zeilen mit Zeitmarke, nach Zeit sortiert. /// /// Gibt eine leere Liste zurück, wenn keine Marken vorkommen — dann ist es /// eine gewöhnliche Textwand und wird auch so angezeigt. List parseLrc(String text) { final zeilen = []; for (final roh in text.split('\n')) { final marken = _marke.allMatches(roh).toList(); if (marken.isEmpty) continue; // Alles hinter der letzten Marke ist der Text. Kopfzeilen wie `[ar:Neoni]` // passen nicht auf das Muster und fallen schon oben heraus. final inhalt = roh.substring(marken.last.end).trim(); for (final marke in marken) { zeilen.add(LyricZeile(zeit: _zeitAus(marke), text: inhalt)); } } zeilen.sort((a, b) => a.zeit.compareTo(b.zeit)); return zeilen; } Duration _zeitAus(RegExpMatch marke) { final minuten = int.parse(marke.group(1)!); final sekunden = int.parse(marke.group(2)!); final bruch = marke.group(3); // `.5` sind fünf Zehntel, `.05` fünf Hundertstel, `.050` fünfzig // Tausendstel — die Stellenzahl entscheidet, nicht der Wert. final millis = bruch == null ? 0 : (int.parse(bruch) * (bruch.length == 1 ? 100 : bruch.length == 2 ? 10 : 1)); return Duration(minutes: minuten, seconds: sekunden, milliseconds: millis); } /// Index der Zeile, die bei [position] gilt — die letzte, deren Marke bereits /// erreicht ist. Vor der ersten Marke gilt die erste Zeile, damit nie eine /// leere Anzeige entsteht. int? aktiveZeile(List zeilen, Duration position) { if (zeilen.isEmpty) return null; var index = 0; for (var i = 0; i < zeilen.length; i++) { if (zeilen[i].zeit > position) break; index = i; } return index; }