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/screens/home_screen.dart
T

434 lines
16 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 'package:flutter/material.dart';
import 'dart:async';
import '../viewmodels/melo_home_viewmodel.dart';
import '../models/song.dart';
import '../models/playlist.dart';
import '../utils/farb_theme.dart';
import '../widgets/mini_player.dart';
import '../widgets/melo_header.dart';
import '../widgets/statistik_card.dart';
import '../widgets/tag_leiste.dart';
import '../widgets/song_tile.dart';
import '../widgets/navidrome_browser.dart';
import '../widgets/playlist_sheet.dart';
import 'download_screen.dart';
class MeloHome extends StatefulWidget {
const MeloHome({super.key});
@override
State<MeloHome> createState() => _MeloHomeState();
}
class _MeloHomeState extends State<MeloHome> {
final MeloHomeViewModel _vm = MeloHomeViewModel();
int _aktiverTab = 0;
@override
void initState() {
super.initState();
_vm.ladeSongs();
}
@override
void dispose() {
_vm.dispose();
super.dispose();
}
Future<void> _zeigeSuche() async {
final controller = TextEditingController();
String modus = 'Alle';
final ergebnis = await showDialog<String>(
context: context,
builder: (ctx) => StatefulBuilder(
builder: (ctx, setDialogState) => AlertDialog(
backgroundColor: MeloTheme.dunkel1,
title: const Text('🔍 Song suchen', style: TextStyle(color: Colors.white, fontSize: 18)),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: controller,
autofocus: true,
style: const TextStyle(color: Colors.white),
decoration: const InputDecoration(
hintText: 'Titel, Künstler oder Tag...',
hintStyle: TextStyle(color: Colors.grey),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
Row(
children: [
_suchChip('Alle', modus == 'Alle', () => setDialogState(() => modus = 'Alle')),
const SizedBox(width: 6),
_suchChip('Titel', modus == 'Titel', () => setDialogState(() => modus = 'Titel')),
const SizedBox(width: 6),
_suchChip('Künstler', modus == 'Künstler', () => setDialogState(() => modus = 'Künstler')),
const SizedBox(width: 6),
_suchChip('Tag', modus == 'Tag', () => setDialogState(() => modus = 'Tag')),
],
),
],
),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Abbrechen')),
TextButton(
onPressed: () => Navigator.pop(ctx, '${controller.text}|$modus'),
child: const Text('Suchen', style: TextStyle(color: MeloTheme.rot)),
),
],
),
),
);
if (ergebnis == null || ergebnis.isEmpty) return;
final teile = ergebnis.split('|');
final suchtext = teile[0].toLowerCase();
final suchModus = teile.length > 1 ? teile[1] : 'Alle';
if (suchtext.isEmpty) return;
final gefiltert = _vm.songs.where((s) {
if (suchModus == 'Titel') return s.titel.toLowerCase().contains(suchtext);
if (suchModus == 'Künstler') return s.kuenstler.toLowerCase().contains(suchtext);
if (suchModus == 'Tag') {
return _vm.tags.any((t) =>
t['name'] != 'Alle' &&
t['name']!.toLowerCase().contains(suchtext) &&
'${s.titel} ${s.kuenstler}'.toLowerCase().contains(t['name']!.toLowerCase()));
}
// Alle
if (s.titel.toLowerCase().contains(suchtext)) return true;
if (s.kuenstler.toLowerCase().contains(suchtext)) return true;
return _vm.tags.any((t) =>
t['name'] != 'Alle' &&
t['name']!.toLowerCase().contains(suchtext) &&
'${s.titel} ${s.kuenstler}'.toLowerCase().contains(t['name']!.toLowerCase()));
}).toList();
if (!mounted) return;
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: MeloTheme.dunkel1,
title: Text('🔍 ${gefiltert.length} Treffer', style: const TextStyle(color: Colors.white)),
content: SizedBox(
width: double.maxFinite,
height: 300,
child: gefiltert.isEmpty
? const Center(child: Text('Keine Treffer', style: TextStyle(color: Colors.grey)))
: ListView.builder(
itemCount: gefiltert.length,
itemBuilder: (_, i) => ListTile(
leading: Container(
width: 36, height: 36,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
gradient: const LinearGradient(colors: [Color(0xFF1A0000), Color(0xFF660000)]),
),
child: const Center(child: Text('♪', style: TextStyle(fontSize: 14, color: Colors.white54))),
),
title: Text(gefiltert[i].titel, style: const TextStyle(color: Colors.white)),
subtitle: Text(gefiltert[i].kuenstler, style: const TextStyle(color: Colors.grey)),
onTap: () { Navigator.pop(ctx); _vm.spieleSong(gefiltert[i]); },
),
),
),
actions: [TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Schließen'))],
),
);
}
Future<void> _scanMusik() async {
final erlaubt = await _vm.scanner.frageSpeicherZugriff();
if (!erlaubt) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Bitte Speicherzugriff erlauben')),
);
}
return;
}
await _vm.scanner.scanneMusikOrdner();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Scannen fertig: ${_vm.scanner.anzahlNeueSongs} neue Songs gefunden')),
);
await _vm.ladeSongs();
}
}
void _zeigeServerBrowser() {
showModalBottomSheet(
context: context,
backgroundColor: MeloTheme.schwarz,
isScrollControlled: true,
builder: (_) => SizedBox(
height: MediaQuery.of(context).size.height * 0.7,
child: Column(
children: [
Container(
margin: const EdgeInsets.symmetric(vertical: 8),
width: 40, height: 4,
decoration: BoxDecoration(
color: MeloTheme.dunkel2,
borderRadius: BorderRadius.circular(2),
),
),
Expanded(child: NavidromeBrowser(vm: _vm)),
],
),
),
);
}
void _zeigePlaylistSheet() {
showModalBottomSheet(
context: context,
backgroundColor: MeloTheme.schwarz,
isScrollControlled: true,
builder: (_) => SizedBox(
height: MediaQuery.of(context).size.height * 0.7,
child: PlaylistSheet(vm: _vm),
),
);
}
void _zeigeAddToPlaylist(Song song) async {
final playlists = await _vm.playlists.allePlaylists();
if (!mounted || playlists.isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Erst eine Playlist erstellen')),
);
}
return;
}
final auswahl = await showDialog<Playlist>(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: MeloTheme.dunkel1,
title: const Text('Zu Playlist hinzufügen', style: TextStyle(color: Colors.white, fontSize: 16)),
content: SizedBox(
width: double.maxFinite,
child: ListView.builder(
shrinkWrap: true,
itemCount: playlists.length,
itemBuilder: (_, i) => ListTile(
leading: const Icon(Icons.queue_music, color: MeloTheme.rot, size: 18),
title: Text(playlists[i].name, style: const TextStyle(color: Colors.white, fontSize: 14)),
subtitle: Text('${playlists[i].songCount} Songs', style: const TextStyle(fontSize: 11, color: MeloTheme.textSekundaer)),
onTap: () => Navigator.pop(ctx, playlists[i]),
),
),
),
actions: [TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Abbrechen'))],
),
);
if (auswahl != null && song.id != null) {
await _vm.playlists.songHinzufuegen(auswahl.id!, song.id!);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('→ ${auswahl.name}')),
);
}
}
}
Future<void> _zeigeDownloadDialog() async {
// Statt Dialog → zum Download-Tab wechseln
setState(() => _aktiverTab = 1);
}
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: _vm,
builder: (_, _) {
if (_vm.ladt) {
return const Scaffold(
backgroundColor: MeloTheme.schwarz,
body: Center(child: CircularProgressIndicator(color: MeloTheme.rot)),
);
}
final gesamtMB = _vm.songs.isEmpty ? '0'
: (_vm.songs.fold(0, (int s, Song song) => s + song.groesseBytes) / 1048576).toStringAsFixed(0);
final gesamtMin = _vm.songs.isEmpty ? 0
: (_vm.songs.fold(0, (int s, Song song) => s + song.dauerSekunden) / 60).round();
return Scaffold(
backgroundColor: MeloTheme.schwarz,
body: SafeArea(
child: _aktiverTab == 1
? DownloadScreen(
downloader: _vm.downloader,
onSongsChanged: _vm.ladeSongs,
)
: Column(
children: [
MeloHeader(onDownload: _zeigeDownloadDialog, onSearch: _zeigeSuche, onServer: _zeigeServerBrowser),
// ─── EIN/AUS: RecentWidget (Zuletzt gehört) ───
// Entferne die Kommentarzeichen um RecentWidget zu aktivieren:
// RecentWidget(songs: _vm.letzteSongs, onPlay: _vm.spieleSong),
StatistikCard(
anzahlSongs: _vm.songs.length,
gesamtMB: gesamtMB,
gesamtMin: gesamtMin,
anzahlFavoriten: _vm.favoritenIds.length,
),
// ─── EIN/AUS: Hidden Message "Seit 2008" ───
// Entferne die Kommentarzeichen um die Botschaft zu aktivieren:
if (_vm.zeigeBotschaft) _botschaftBanner(),
TagLeiste(
tags: _vm.tags,
aktiveTags: _vm.aktiveTags,
onTagToggled: _vm.toggleTag,
),
// ─── EIN/AUS: TagStatsWidget (Tag-Counts) ───
// Entferne die Kommentarzeichen um TagStatsWidget zu aktivieren:
// TagStatsWidget(tagCounts: _vm.tagCounts),
Expanded(child: _songListe()),
const MiniPlayer(),
const SizedBox(height: 8),
],
),
),
bottomNavigationBar: _bottomNav(),
);
},
);
}
Widget _songListe() {
final songs = _vm.gefilterteSongs;
return Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('📂 Alle Songs', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white)),
Row(children: [
Text('${songs.length} Titel${_vm.aktiveTags.isNotEmpty ? ' (gefiltert)' : ''}', style: TextStyle(fontSize: 12, color: MeloTheme.rot)),
const SizedBox(width: 8),
GestureDetector(
onTap: _scanMusik,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
border: Border.all(color: MeloTheme.dunkel2),
borderRadius: BorderRadius.circular(8),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.refresh, size: 12, color: MeloTheme.rot),
SizedBox(width: 4),
Text('Scannen', style: TextStyle(fontSize: 11, color: MeloTheme.rot)),
],
),
),
),
]),
],
),
),
Expanded(
child: ListView.builder(
padding: const EdgeInsets.fromLTRB(20, 4, 20, 4),
itemCount: songs.length,
itemBuilder: (_, i) => SongTile(
song: songs[i],
istFavorit: songs[i].id != null && _vm.favoritenIds.contains(songs[i].id),
onFavoriteToggle: _vm.favoritenUmschalten,
onPlay: _vm.spieleSong,
onMetadataChanged: _vm.ladeSongs,
onAddToPlaylist: _zeigeAddToPlaylist,
),
),
),
],
);
}
Widget _bottomNav() {
return Container(
decoration: const BoxDecoration(
border: Border(top: BorderSide(color: MeloTheme.dunkel1)),
),
child: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
backgroundColor: MeloTheme.schwarz,
selectedItemColor: MeloTheme.rot,
unselectedItemColor: MeloTheme.textSekundaer,
currentIndex: _aktiverTab,
onTap: (i) {
setState(() => _aktiverTab = i);
if (i == 2) _zeigePlaylistSheet(); // Tags-Tab → Playlists
if (i == 3) _zeigePlaylistSheet(); // Favoriten-Tab → Playlists
},
items: const [
BottomNavigationBarItem(icon: Icon(Icons.music_note, size: 22), label: 'Musik'),
BottomNavigationBarItem(icon: Icon(Icons.download, size: 22), label: 'Downloads'),
BottomNavigationBarItem(icon: Icon(Icons.label, size: 22), label: 'Tags'),
BottomNavigationBarItem(icon: Icon(Icons.favorite, size: 22), label: 'Favoriten'),
BottomNavigationBarItem(icon: Icon(Icons.settings, size: 22), label: 'Einstellungen'),
],
),
);
}
/// Hilfs-Widget für Such-Modus-Chips
Widget _suchChip(String label, bool aktiv, VoidCallback onTap) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: aktiv ? MeloTheme.rot : MeloTheme.dunkel2,
borderRadius: BorderRadius.circular(12),
),
child: Text(label, style: TextStyle(fontSize: 11, color: aktiv ? Colors.white : MeloTheme.textSekundaer)),
),
);
}
/// Banner "💌 Seit 2008" erscheint nach 10 Playbacks
Widget _botschaftBanner() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
gradient: const LinearGradient(colors: [Color(0xFF2A0000), Color(0xFF1A0000)]),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: MeloTheme.rot.withValues(alpha: 0.4)),
),
child: Row(
children: [
const Text('💌', style: TextStyle(fontSize: 20)),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text('Seit 2008',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: Colors.white)),
Text('Danke, dass du immer da bist ♥',
style: TextStyle(fontSize: 11, color: MeloTheme.textSekundaer)),
],
),
),
GestureDetector(
onTap: _vm.botschaftAusblenden,
child: const Icon(Icons.close, size: 16, color: MeloTheme.textSekundaer),
),
],
),
),
);
}
}