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 79b15a3cb8 v2.56 — UI/Design-Rework: Melo Dark Fusion (neue Startseite, 2-stufige Einstellungen, Design-System)
- Design-System: MeloDesignTokens ThemeExtension, Outfit-Font, Farben #0B0B10/#14141C/#1C1C26
- Akzentfarbe-User-Einstellung behalten (MeloTheme.akzentNotifier), Gradient aus Akzent
- Startseite: Hero-Weiterhören-Karte mit Cover, Titel, Glow-Play-Button
- 2-stufige Einstellungen: Standard + Expertenmodus (SharedPreferences Key 'experten_modus')
- Komponenten: Pill-Buttons (radius 999), Card-Radius 20, Glow-Shadow, PressScale-Widget
- Login: Gradient-Button mit Akzent-Glow
- 199 Tests grün (194 bestehend + 5 neu): ThemeExtension, Expertenmodus, Akzent, PressScale, Hero
- flutter analyze lib/ + test/ = 0 Issues
- KEIN APK
2026-08-06 19:56:02 +02:00

294 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(12),
child: Container(
width: 44, height: 44,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
MeloTheme.akzent.withValues(alpha: 0.6),
MeloTheme.akzent.withValues(alpha: 0.15),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
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: 🔄 Neu laden (Server-Quelle via cloud_id oder Legacy-ytUrl) ──
if (song.istKorrupt &&
(song.cloudId != null || 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.akzent,
),
),
),
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);
},
),
// ⬇️ Neu laden wenn Server-Quelle (cloud_id) oder Legacy-ytUrl existiert
if ((song.cloudId != null || 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),
],
),
),
);
}
}