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 4b9cfefe00 v2.51.3 — Schnellaktionen per Longpress auf Songs
## Schnellaktionen (song_tile.dart)
- Longpress öffnet Bottom-Sheet mit: ▶️ Abspielen,  Als Nächstes,  Am Ende hinzufügen,  Favorit (hinzufügen/entfernen), ⬇️ Herunterladen/Erneut herunterladen, 🏷 Tags bearbeiten, ✏️ Umbenennen
- Nur verfügbare Aktionen werden gezeigt (Download nur mit ytUrl, Tags/Umbenennen nur mit DB-id)
- Song-Info-Kopfzeile im Sheet (Titel + Künstler)
2026-08-05 07:57:37 +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' : 'Herunterladen',
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),
],
),
),
);
}
}