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 82003c55d6 v2.48.4 — Als Nächstes abspielen (Play Next)
## SongTile
- Long-Press auf Song-Tile öffnet Kontextmenü (Bottom-Sheet):
  ▶ Als Nächstes abspielen + ⏭ Am Ende hinzufügen

## ViewModel
- spieleAlsNaechstes(Song) / amEndeHinzufuegen(Song) → PlayerService

## HomeScreen
- SongTile-Callbacks verdrahtet, SnackBar-Bestätigung bei beiden Aktionen

Keine neuen Packages, flutter analyze 0 Issues, Tests 4/4
2026-08-04 16:25:53 +02:00

181 lines
6.3 KiB
Dart

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 && (onSpieleAlsNaechstes != null || onAmEndeHinzufuegen != null))
? () => _zeigeKontextMenu(context)
: null,
);
}
/// Kontextmenü per Long-Press: "Als Nächstes abspielen" + "Am Ende hinzufügen"
void _zeigeKontextMenu(BuildContext context) {
showModalBottomSheet(
context: context,
backgroundColor: MeloTheme.dunkel1,
builder: (ctx) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
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);
},
),
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);
},
),
],
),
),
);
}
}