This repository has been archived on 2026-08-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
melo-app/lib/widgets/song_tile.dart
T
Dustin 4694955f5f v2.51.4 — Review-Fix LOW: Longpress-Aktion „Herunterladen“ → „Neu laden“
## LOW (song_tile.dart)
- Label für gesunden Song mit ytUrl heißt jetzt „Neu laden“ statt
  „Herunterladen“ — die Aktion ist ein destruktiver reDownload
  (löscht DB-Eintrag + lokale Datei), das alte Label war irreführend
2026-08-05 08:12:45 +02:00

285 lines
11 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:io';
import 'package:flutter/material.dart';
import '../models/song.dart';
import '../utils/farb_theme.dart';
import 'metadaten_dialog.dart';
import 'tag_auswahl_dialog.dart';
class SongTile extends StatelessWidget {
final Song song;
final bool istFavorit;
final ValueChanged<Song> onFavoriteToggle;
final ValueChanged<Song> onPlay;
final VoidCallback onMetadataChanged;
final ValueChanged<Song>? onAddToPlaylist;
final ValueChanged<Song>? onErneutHerunterladen;
final ValueChanged<Song>? onSpieleAlsNaechstes;
final ValueChanged<Song>? onAmEndeHinzufuegen;
const SongTile({
super.key,
required this.song,
required this.istFavorit,
required this.onFavoriteToggle,
required this.onPlay,
required this.onMetadataChanged,
this.onAddToPlaylist,
this.onErneutHerunterladen,
this.onSpieleAlsNaechstes,
this.onAmEndeHinzufuegen,
});
@override
Widget build(BuildContext context) {
final hatDatei = song.dateiPfad.isNotEmpty;
final hatCover = song.coverPfad != null && File(song.coverPfad!).existsSync();
return ListTile(
contentPadding: const EdgeInsets.symmetric(vertical: 2),
leading: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Container(
width: 44, height: 44,
decoration: const BoxDecoration(
gradient: LinearGradient(colors: [Color(0xFF1A0000), Color(0xFF660000)]),
),
child: hatCover
? Image.file(File(song.coverPfad!), fit: BoxFit.cover)
: const Center(child: Text('♪', style: TextStyle(fontSize: 18, color: Colors.white54))),
),
),
title: Row(
children: [
if (song.istKorrupt)
const Padding(
padding: EdgeInsets.only(right: 6),
child: Text('⚠️', style: TextStyle(fontSize: 16)),
),
Expanded(
child: Text(
song.titel,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: song.istKorrupt ? MeloTheme.textSekundaer : Colors.white,
decoration: song.istKorrupt ? TextDecoration.lineThrough : null,
),
),
),
],
),
subtitle: Text(song.kuenstler, style: const TextStyle(fontSize: 12, color: MeloTheme.textSekundaer)),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
// ── Korrupt mit ytUrl: 🔄 Neu laden ──
if (song.istKorrupt && song.ytUrl != null && onErneutHerunterladen != null)
InkWell(
borderRadius: BorderRadius.circular(50),
onTap: () => onErneutHerunterladen!(song),
child: const Padding(
padding: EdgeInsets.all(6),
child: Text('🔄', style: TextStyle(fontSize: 16)),
),
),
InkWell(
borderRadius: BorderRadius.circular(50),
onTap: () async {
final geaendert = await showDialog<bool>(
context: context,
builder: (_) => MetadatenDialog(song: song),
);
if (geaendert == true) onMetadataChanged();
},
child: Padding(
padding: const EdgeInsets.all(6),
child: Icon(Icons.edit, size: 14, color: MeloTheme.textSekundaer),
),
),
InkWell(
borderRadius: BorderRadius.circular(50),
onTap: () => showDialog(
context: context,
builder: (_) => TagAuswahlDialog(
songId: song.id!,
songTitel: song.titel,
onChanged: onMetadataChanged,
),
),
child: Padding(
padding: const EdgeInsets.all(6),
child: const Icon(Icons.label_outline, size: 14, color: MeloTheme.textSekundaer),
),
),
if (onAddToPlaylist != null)
InkWell(
borderRadius: BorderRadius.circular(50),
onTap: () => onAddToPlaylist!(song),
child: Padding(
padding: const EdgeInsets.all(6),
child: const Icon(Icons.playlist_add, size: 14, color: MeloTheme.textSekundaer),
),
),
InkWell(
borderRadius: BorderRadius.circular(50),
onTap: () => onFavoriteToggle(song),
child: Padding(
padding: const EdgeInsets.all(6),
child: Icon(
istFavorit ? Icons.favorite : Icons.favorite_border,
size: 16, color: MeloTheme.rot,
),
),
),
Text(song.dauerFormatiert, style: const TextStyle(fontSize: 12, color: MeloTheme.textSekundaer)),
],
),
onTap: hatDatei ? () => onPlay(song) : null,
onLongPress: hatDatei ? () => _zeigeKontextMenu(context) : null,
);
}
/// Kontextmenü per Long-Press: Schnellaktionen auf den Song.
/// Zeigt nur Aktionen, deren Funktionen verfügbar sind:
/// ▶️ Abspielen · Warteschlange · ⭐ Favorit · ⬇️ Download · 🏷 Tags · ✏️ Umbenennen
void _zeigeKontextMenu(BuildContext context) {
final hatTags = song.id != null;
showModalBottomSheet(
context: context,
backgroundColor: MeloTheme.dunkel1,
builder: (ctx) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Song-Info-Kopfzeile
Padding(
padding: const EdgeInsets.fromLTRB(20, 14, 20, 6),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
width: 36,
height: 36,
decoration: const BoxDecoration(
gradient: LinearGradient(colors: [Color(0xFF1A0000), Color(0xFF660000)]),
),
child: const Center(child: Text('♪', style: TextStyle(fontSize: 14, color: Colors.white54))),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
song.titel,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600),
),
Text(
song.kuenstler,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 12),
),
],
),
),
],
),
),
const Divider(color: MeloTheme.dunkel2, height: 12),
// ▶️ Abspielen
ListTile(
leading: const Icon(Icons.play_arrow, color: MeloTheme.rot, size: 20),
title: const Text('Abspielen', style: TextStyle(color: Colors.white, fontSize: 14)),
onTap: () {
Navigator.pop(ctx);
onPlay(song);
},
),
// Warteschlange (Play Next)
if (onSpieleAlsNaechstes != null)
ListTile(
leading: const Icon(Icons.playlist_play, color: MeloTheme.rot, size: 20),
title: const Text('Als Nächstes abspielen', style: TextStyle(color: Colors.white, fontSize: 14)),
onTap: () {
Navigator.pop(ctx);
onSpieleAlsNaechstes!.call(song);
},
),
// Am Ende hinzufügen
if (onAmEndeHinzufuegen != null)
ListTile(
leading: const Icon(Icons.playlist_add, color: MeloTheme.rot, size: 20),
title: const Text('Am Ende hinzufügen', style: TextStyle(color: Colors.white, fontSize: 14)),
onTap: () {
Navigator.pop(ctx);
onAmEndeHinzufuegen!(song);
},
),
// ⭐ Favorit
ListTile(
leading: Icon(istFavorit ? Icons.favorite : Icons.favorite_border, color: MeloTheme.rot, size: 20),
title: Text(
istFavorit ? 'Favorit entfernen' : 'Zu Favoriten',
style: const TextStyle(color: Colors.white, fontSize: 14),
),
onTap: () {
Navigator.pop(ctx);
onFavoriteToggle(song);
},
),
// ⬇️ Download nur wenn eine YouTube-Quell-URL existiert
if (song.ytUrl != null && onErneutHerunterladen != null)
ListTile(
leading: const Icon(Icons.download, color: MeloTheme.rot, size: 20),
title: Text(
song.istKorrupt ? 'Erneut herunterladen' : 'Neu laden',
style: const TextStyle(color: Colors.white, fontSize: 14),
),
onTap: () {
Navigator.pop(ctx);
onErneutHerunterladen!(song);
},
),
// 🏷 Tags nur wenn der Song in der DB ist
if (hatTags)
ListTile(
leading: const Icon(Icons.label_outline, color: MeloTheme.rot, size: 20),
title: const Text('Tags bearbeiten', style: TextStyle(color: Colors.white, fontSize: 14)),
onTap: () {
Navigator.pop(ctx);
showDialog(
context: context,
builder: (_) => TagAuswahlDialog(
songId: song.id!,
songTitel: song.titel,
onChanged: onMetadataChanged,
),
);
},
),
// ✏️ Umbenennen (Metadaten)
if (hatTags)
ListTile(
leading: const Icon(Icons.edit, color: MeloTheme.rot, size: 20),
title: const Text('Umbenennen', style: TextStyle(color: Colors.white, fontSize: 14)),
onTap: () async {
Navigator.pop(ctx);
final geaendert = await showDialog<bool>(
context: context,
builder: (_) => MetadatenDialog(song: song),
);
if (geaendert == true) onMetadataChanged();
},
),
const SizedBox(height: 8),
],
),
),
);
}
}