Fix: Review-Findings korrigiert (Tag-Leiste, Profil, Singleton u.a.)

CRIT:
- Tag-Leiste: Toggle-Kopf 'Tags & Filter' eingebaut (war unerreichbar) + TagStats
- Profil: Cloud-Count vor Dialog aufloesen (kein 'Instance of Future' mehr)
HIGH:
- CloudEinstellungen: toten Sync-Timer entfernt (echter Timer im ViewModel)
- StatistikCard + RecentWidget jetzt eingebaut (gesamtMB/gesamtMin endlich genutzt)
- CloudService als Singleton (konsistenter Token-Zustand in allen Services)
MED/LOW:
- Playlist-Erkennung nur noch via list= Parameter
- Player: fehlende Quelle wird geloggt statt still
- song_tile: null-ID-Guard vor Tag-Dialog
- Scanner-Log mit Exception-Objekt
- FavoritenService: anzahlFavoriten() fuer StatistikCard
This commit is contained in:
Hermes (Server)
2026-07-31 15:42:54 +02:00
parent b3aedc53f9
commit 8b6a04ead8
18 changed files with 767 additions and 503 deletions
+68 -6
View File
@@ -1,7 +1,9 @@
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;
@@ -10,6 +12,7 @@ class SongTile extends StatelessWidget {
final ValueChanged<Song> onPlay;
final VoidCallback onMetadataChanged;
final ValueChanged<Song>? onAddToPlaylist;
final ValueChanged<Song>? onDelete;
const SongTile({
super.key,
@@ -19,20 +22,26 @@ class SongTile extends StatelessWidget {
required this.onPlay,
required this.onMetadataChanged,
this.onAddToPlaylist,
this.onDelete,
});
@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: Container(
width: 44, height: 44,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
gradient: const LinearGradient(colors: [Color(0xFF1A0000), Color(0xFF660000)]),
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))),
),
child: const Center(child: Text('', style: TextStyle(fontSize: 18, color: Colors.white54))),
),
title: Text(song.titel, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)),
subtitle: Text(song.kuenstler, style: const TextStyle(fontSize: 12, color: MeloTheme.textSekundaer)),
@@ -53,6 +62,26 @@ class SongTile extends StatelessWidget {
child: Icon(Icons.edit, size: 14, color: MeloTheme.textSekundaer),
),
),
InkWell(
borderRadius: BorderRadius.circular(50),
onTap: () {
// Guard gegen null-ID (defensive)
final id = song.id;
if (id == null) return;
showDialog(
context: context,
builder: (_) => TagAuswahlDialog(
songId: 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),
@@ -77,6 +106,39 @@ class SongTile extends StatelessWidget {
],
),
onTap: hatDatei ? () => onPlay(song) : null,
onLongPress: onDelete != null ? () {
showModalBottomSheet(
context: context,
backgroundColor: MeloTheme.dunkel1,
builder: (ctx) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.edit, color: Colors.white),
title: const Text('Bearbeiten', style: TextStyle(color: Colors.white)),
onTap: () async {
Navigator.pop(ctx);
final geaendert = await showDialog<bool>(
context: context,
builder: (_) => MetadatenDialog(song: song),
);
if (geaendert == true) onMetadataChanged();
},
),
ListTile(
leading: const Icon(Icons.delete, color: Colors.red),
title: const Text('Löschen', style: TextStyle(color: Colors.red)),
onTap: () {
Navigator.pop(ctx);
onDelete!(song);
},
),
],
),
),
);
} : null,
);
}
}