Songtext aus Datei-Tags, Warteschlangen-Aktionen, Kategorie-Cover im

Sperrbildschirm und ReplayGain

- DB-Schema 6: lyrics + gain_db
- Songtext bevorzugt lokalen Tag, Server nur als Rueckfall; Song-ID-Bug
  beim Lyrics-Aufruf behoben
- "Als Naechstes spielen" / "Zur Warteschlange hinzufuegen" ohne Eingriff
  in Favoriten oder Wiedergabelisten
- songToMediaItem nimmt eine Cover-Vorgabe: Sperrbildschirm zeigt das
  Kategorie-Cover
- ReplayGain: Tags werden gelesen, laute Titel abgesenkt, abschaltbar

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:41:36 +02:00
co-authored by Claude Opus 5
parent 1325badf90
commit 4ebd0a8dd4
17 changed files with 534 additions and 46 deletions
+31 -19
View File
@@ -2,6 +2,7 @@ import 'package:audio_service/audio_service.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../library/database.dart';
import '../services/navidrome_service.dart';
import '../shared/cover.dart';
import '../shared/favorite_button.dart';
@@ -36,9 +37,10 @@ class NowPlayingScreen extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
tooltip: 'Lyrics',
tooltip: 'Songtext',
icon: const Icon(Icons.lyrics),
onPressed: () => _showLyrics(context, item.id),
onPressed: () => _showLyrics(
context, item.extras?['songId'] as String? ?? item.id),
),
FavoriteButton(songId: item.extras?['songId'] as String? ?? ''),
],
@@ -314,26 +316,36 @@ class _LyricsSheet extends StatefulWidget {
class _LyricsSheetState extends State<_LyricsSheet> {
late final NavidromeService _nav = NavidromeService();
Lyrics? _lyrics;
String? _text;
bool _loading = true;
@override
void initState() {
super.initState();
_nav.ladeGespeicherteZugangsdaten().then((_) {
if (_nav.istVerbunden) {
_nav.getLyrics(widget.songId).then((lyrics) {
if (mounted) {
setState(() {
_lyrics = lyrics;
_loading = false;
});
}
_load();
}
/// Zuerst der Songtext aus dem Tag der Datei — der ist sofort da und
/// funktioniert offline. Nur wenn keiner drinsteht, wird der Server gefragt.
Future<void> _load() async {
final db = context.read<MeloDb>();
final lokal = await db.lyricsOf(widget.songId);
if (lokal != null) {
if (mounted) setState(() { _text = lokal; _loading = false; });
return;
}
await _nav.ladeGespeicherteZugangsdaten();
if (_nav.istVerbunden) {
final lyrics = await _nav.getLyrics(widget.songId);
if (mounted) {
setState(() {
_text = lyrics.isEmpty ? null : lyrics.text;
_loading = false;
});
} else {
setState(() => _loading = false);
}
});
return;
}
if (mounted) setState(() => _loading = false);
}
@override
@@ -347,7 +359,7 @@ class _LyricsSheetState extends State<_LyricsSheet> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('📝 Lyrics',
const Text('📝 Songtext',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
IconButton(
icon: const Icon(Icons.close),
@@ -361,15 +373,15 @@ class _LyricsSheetState extends State<_LyricsSheet> {
? const Center(
child: CircularProgressIndicator(color: MeloTheme.red),
)
: _lyrics?.isEmpty ?? true
: _text == null
? const Center(
child: Text('Keine Lyrics verfügbar',
child: Text('Kein Songtext verfügbar',
style: TextStyle(color: Colors.white54)),
)
: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Text(
_lyrics?.text ?? '',
_text!,
style: const TextStyle(
fontSize: 14,
height: 1.6,