Bisher scrollte man durch die Bibliothek und musste raten, wo man ist — die einzige Rueckmeldung war die Leiste ganz unten. Drei tanzende Balken ueber dem Coverbild des laufenden Titels, sein Name in Rot. Die Balken liegen auf dem Cover statt daneben: eine vierte Spalte haette dem Titel Platz genommen und die Zeile bei jedem Titelwechsel in der Breite springen lassen. Pausiert bleibt die Markierung stehen, die Balken stehen still. Bei abgeschalteten Systemanimationen ebenfalls — eine Endlosschleife ist genau das, was diese Einstellung vermeiden soll. Die Markierung sitzt in SongZeile und gilt damit in einem Rutsch fuer Meine Musik, Favoriten, Suche, Kuenstler, Kategorie und "Zuletzt hinzugefuegt". Warteschlange und Wiedergabeliste bauen ihre Zeilen selbst (dort steht rechts Entfernen statt Herz und Menue) und haben dasselbe Zeichen einzeln bekommen. Nur die laufende Zeile hoert auf den Wiedergabe-Zustand: der meldet auch den Pufferstand und feuert mehrmals pro Sekunde. Haenge jede sichtbare Zeile daran, baute sich die halbe Liste staendig neu auf. Aus dem Code-Review nachgebessert: Startwert fuer den Strom, sonst erschien die Markierung beim Zurueckscrollen einen Frame zu spaet; die Zeile aendert beim Wechsel nicht mehr die Form ihres Aufbaus, sonst verlor das Herz daneben sein Abo und sah kurz leer aus; die Wiedergabeliste bekam die Markierung ueberhaupt erst (der Changelog hatte sie faelschlich schon behauptet); der Abdunkel-Schleier nimmt den Eckenradius jetzt entgegen statt ihn fest zu verdrahten. Testluecke geschlossen, die der Review benannt hat: geprueft waren nur die Regel und die Balken je fuer sich, die Verdrahtung dazwischen nicht — genau dort sassen alle drei Fehler. 411 Tests gruen (vorher 396), flutter analyze ohne Befund, Release-APK gebaut. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013xAHJTJM6UUqmjUgk1PUEd
153 lines
5.3 KiB
Dart
153 lines
5.3 KiB
Dart
import 'package:audio_service/audio_service.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import '../library/category_service.dart';
|
|
import '../library/database.dart';
|
|
import '../library/playlist_service.dart';
|
|
import '../library/song_media.dart';
|
|
import '../player/audio_handler.dart';
|
|
import '../settings/app_settings.dart';
|
|
import '../shared/cover.dart';
|
|
import '../shared/lauf_balken.dart';
|
|
import '../shared/theme.dart';
|
|
|
|
/// Playlist-Detailansicht: Songs anzeigen, per Drag & Drop umsortieren, entfernen.
|
|
class PlaylistDetailScreen extends StatelessWidget {
|
|
const PlaylistDetailScreen({super.key, required this.playlist});
|
|
final Playlist playlist;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final db = context.read<MeloDb>();
|
|
final service = context.read<PlaylistService>();
|
|
return Scaffold(
|
|
appBar: AppBar(title: Text(playlist.name)),
|
|
body: StreamBuilder<List<Song>>(
|
|
stream: db.watchPlaylistSongs(playlist.id),
|
|
builder: (context, snapshot) {
|
|
if (snapshot.hasError) {
|
|
return Center(
|
|
child: Text('Fehler: ${snapshot.error}',
|
|
style: const TextStyle(color: MeloTheme.text2)),
|
|
);
|
|
}
|
|
final songs = snapshot.data ?? const [];
|
|
if (songs.isEmpty) {
|
|
return const Center(
|
|
child: Text('Playlist ist leer — füge Songs aus der Bibliothek hinzu',
|
|
style: TextStyle(color: MeloTheme.text2)),
|
|
);
|
|
}
|
|
return ReorderableListView.builder(
|
|
itemCount: songs.length,
|
|
onReorderItem: (oldIndex, newIndex) {
|
|
final reordered = List<Song>.from(songs);
|
|
final moved = reordered.removeAt(oldIndex);
|
|
reordered.insert(newIndex, moved);
|
|
service.reorderAll(playlist.id, reordered.map((s) => s.id).toList());
|
|
},
|
|
itemBuilder: (context, i) => _Zeile(
|
|
key: ValueKey(songs[i].id),
|
|
song: songs[i],
|
|
alleSongs: songs,
|
|
index: i,
|
|
onEntfernen: () =>
|
|
service.removeSongFromPlaylist(playlist.id, songs[i].id),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Eine Zeile der Wiedergabeliste.
|
|
///
|
|
/// Nicht [SongZeile]: hier steht rechts „Aus Playlist entfernen" statt Herz
|
|
/// und Menü. Die Lauf-Markierung ist dieselbe — der Nutzer soll dasselbe
|
|
/// Zeichen sehen wie in der Bibliothek.
|
|
class _Zeile extends StatelessWidget {
|
|
const _Zeile({
|
|
super.key,
|
|
required this.song,
|
|
required this.alleSongs,
|
|
required this.index,
|
|
required this.onEntfernen,
|
|
});
|
|
|
|
final Song song;
|
|
final List<Song> alleSongs;
|
|
final int index;
|
|
final VoidCallback onEntfernen;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final handler = context.read<MeloAudioHandler>();
|
|
final categories = context.watch<CategoryService>();
|
|
final settings = context.watch<AppSettings>();
|
|
final cover = categories.coverFor(song,
|
|
groupByCategory: settings.groupCoversByCategory);
|
|
return StreamBuilder<MediaItem?>(
|
|
stream: handler.mediaItem,
|
|
initialData: handler.mediaItem.valueOrNull,
|
|
builder: (context, laufSnap) {
|
|
final markiert = istAmLaufen(laufSnap.data, song.id);
|
|
return StreamBuilder<PlaybackState>(
|
|
stream: markiert ? handler.playbackState : null,
|
|
initialData: handler.playbackState.valueOrNull,
|
|
builder: (context, zustandSnap) => ListTile(
|
|
leading: LaufMarkierung(
|
|
markiert: markiert,
|
|
spielt: markiert && (zustandSnap.data?.playing ?? false),
|
|
radius: 6,
|
|
child: CoverImage(
|
|
artUri: cover != null ? Uri.file(cover) : null,
|
|
size: 48,
|
|
radius: 6,
|
|
),
|
|
),
|
|
title: Text(
|
|
song.title,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: markiert
|
|
? const TextStyle(
|
|
color: MeloTheme.red, fontWeight: FontWeight.w600)
|
|
: null,
|
|
),
|
|
subtitle: Text(song.artist ?? 'Unbekannt',
|
|
maxLines: 1, overflow: TextOverflow.ellipsis),
|
|
trailing: IconButton(
|
|
tooltip: 'Aus Playlist entfernen',
|
|
icon: const Icon(Icons.remove_circle_outline),
|
|
onPressed: onEntfernen,
|
|
),
|
|
onTap: () async {
|
|
final messenger = ScaffoldMessenger.of(context);
|
|
try {
|
|
final uebersprungen = await playSongs(
|
|
handler,
|
|
alleSongs,
|
|
index,
|
|
coverOf: (s) => categories.coverFor(s,
|
|
groupByCategory: settings.groupCoversByCategory),
|
|
);
|
|
if (uebersprungen > 0) {
|
|
messenger.showSnackBar(
|
|
SnackBar(content: Text(offlineHinweis(uebersprungen))),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
messenger.showSnackBar(
|
|
SnackBar(content: Text('Wiedergabe fehlgeschlagen: $e')),
|
|
);
|
|
}
|
|
},
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|