import 'dart:async'; import 'package:flutter/foundation.dart'; import '../database/db_helper.dart'; import '../services/player_service.dart'; import '../services/musik_scanner.dart'; import '../services/favoriten_service.dart'; import '../services/download_service.dart'; import '../services/navidrome_service.dart'; import '../services/playlist_service.dart'; import '../models/song.dart'; import '../models/tag.dart'; class MeloHomeViewModel extends ChangeNotifier { final PlayerService player = PlayerService(); final DbHelper db = DbHelper(); final FavoritenService favoriten = FavoritenService(); final MusikScanner scanner = MusikScanner(); final DownloadService downloader = DownloadService(); final NavidromeService navidrome = NavidromeService(); final PlaylistService playlists = PlaylistService(); List songs = []; Set aktiveTags = {}; bool ladt = true; Set favoritenIds = {}; List> tags = []; List letzteSongs = []; Map tagCounts = {}; bool zeigeBotschaft = false; bool serverLadt = false; List navidromeAlben = []; Map _tagsMap = {}; // Name → Tag für Filter-Lookup Map get tagsMap => _tagsMap; int _playCount = 0; static const int _botschaftSchwellwert = 10; StreamSubscription? _positionsSub; int _letzteGespeicherteSekunde = -1; void _startePositionTracking() { _positionsSub?.cancel(); _letzteGespeicherteSekunde = -1; _positionsSub = player.positionStream.listen((pos) { final s = player.aktuellerSong; final sek = pos.inSeconds; if (s?.id != null && sek > 0 && sek % 10 == 0 && sek != _letzteGespeicherteSekunde) { _letzteGespeicherteSekunde = sek; db.positionAktualisieren(s!.id!, sek); } }); } static const _defaultTags = [ {'name': 'Alle', 'icon': ''}, {'name': '❤️ Für uns', 'icon': '❤️'}, {'name': 'Nightcore', 'icon': '⚡'}, {'name': 'Traurig', 'icon': '😢'}, {'name': 'Party', 'icon': '🎉'}, {'name': 'Mitsingen', 'icon': '🎤'}, {'name': '2000er', 'icon': '📀'}, ]; List get gefilterteSongs { if (aktiveTags.isEmpty) return songs; // Favoriten-Filter if (aktiveTags.contains('★ Favoriten')) { return songs.where((s) => s.id != null && favoritenIds.contains(s.id)).toList(); } // Filter: Songs mit mindestens einem der aktiven Tags return songs.where((s) { if (s.tagIds == null) return false; return aktiveTags.any((tagName) { final tag = _tagsMap[tagName]; return tag != null && s.tagIds!.contains(tag.id); }); }).toList(); } Future ladeSongs() async { ladt = true; notifyListeners(); try { var alle = await db.alleSongs(); if (alle.isEmpty) { final beispiele = [ Song(titel: 'Leichtes Gepäck', kuenstler: 'Silbermond', album: 'Schritte', dauerSekunden: 232, dateiPfad: '', groesseBytes: 5242880, istHeruntergeladen: true), Song(titel: 'Auf uns', kuenstler: 'Andreas Bourani', album: 'Hey', dauerSekunden: 241, dateiPfad: '', groesseBytes: 4819200, istHeruntergeladen: true), Song(titel: '80 Millionen', kuenstler: 'Max Giesinger', album: 'Der Junge', dauerSekunden: 225, dateiPfad: '', groesseBytes: 5107200, istHeruntergeladen: true), Song(titel: 'Tage wie diese', kuenstler: 'Die Toten Hosen', album: 'Ballast', dauerSekunden: 252, dateiPfad: '', groesseBytes: 4300800, istHeruntergeladen: true), Song(titel: 'Atlantis', kuenstler: 'Frida Gold', album: 'Liebe', dauerSekunden: 228, dateiPfad: '', groesseBytes: 3987200, istHeruntergeladen: true), ]; await db.songsEinfuegen(beispiele); alle = await db.alleSongs(); } songs = alle; favoritenIds = await favoriten.favoritenIds(); letzteSongs = await db.letzteWiedergaben(); await ladeTags(); } catch (e, stack) { debugPrint('ladeSongs Fehler: $e\n$stack'); } ladt = false; notifyListeners(); } Map _berechneTagCounts() { final counts = {}; for (final s in songs) { if (s.tagIds == null) continue; for (final tagId in s.tagIds!) { for (final entry in _tagsMap.entries) { if (entry.value.id == tagId) { counts[entry.key] = (counts[entry.key] ?? 0) + 1; } } } } return counts; } Future ladeTags() async { var dbTags = await db.alleTags(); if (dbTags.isEmpty) { for (final t in _defaultTags) { if (t['name'] != 'Alle') { await db.tagErstellen(t['name']!, icon: t['icon']); } } dbTags = await db.alleTags(); } // Tags-Map füllen _tagsMap = {for (final t in dbTags) t.name: t}; // Tag-IDs für jeden Song laden for (final song in songs) { if (song.id != null) { final songTags = await db.tagsFuerSong(song.id!); song.tagIds = songTags.map((t) => t.id!).toSet(); } } tags = _defaultTags; tagCounts = _berechneTagCounts(); notifyListeners(); } void spieleSong(Song song, {List? warteschlange}) { player.setWarteschlange(warteschlange ?? songs, startIndex: (warteschlange ?? songs).indexWhere((s) => s.id == song.id)); // Wiederaufnahme final pos = (song.zuletztPosition != null && song.zuletztPosition! > 5) ? song.zuletztPosition! : 0; player.spiele(song, position: pos); // Position-Tracking starten (alle 10s speichern) _startePositionTracking(); // In Verlauf speichern if (song.id != null) { db.positionAktualisieren(song.id!, song.zuletztPosition ?? 0); } // Play-Counter für Hidden Message _playCount++; if (_playCount >= _botschaftSchwellwert && !zeigeBotschaft) { zeigeBotschaft = true; notifyListeners(); } // Letzte Songs aktualisieren db.letzteWiedergaben().then((liste) { letzteSongs = liste; notifyListeners(); }); } /// Manuell die Botschaft "Seit 2008" anzeigen void botschaftAnzeigen() { zeigeBotschaft = true; notifyListeners(); } /// Botschaft ausblenden void botschaftAusblenden() { zeigeBotschaft = false; notifyListeners(); } void toggleTag(String tag) { if (tag == 'Alle') { aktiveTags.clear(); } else if (aktiveTags.contains(tag)) { aktiveTags.remove(tag); } else { aktiveTags.add(tag); } notifyListeners(); } Future favoritenUmschalten(Song song) async { if (song.id == null) return; await favoriten.umschalten(song.id!); favoritenIds = await favoriten.favoritenIds(); notifyListeners(); } // ─── Navidrome Server-Sync ───────────────────────── /// Navidrome-Zugangsdaten setzen void verbindeNavidrome(String url, String user, String password) { navidrome.setCredentials(url, user, password); } /// Verbindung testen und Alben laden Future ladeNavidromeAlben() async { if (!navidrome.istVerbunden) return false; serverLadt = true; notifyListeners(); final ok = await navidrome.ping(); if (ok) { navidromeAlben = await navidrome.getAlben(); } serverLadt = false; notifyListeners(); return ok; } /// Songs eines Albums laden Future> ladeAlbumSongs(String albumId) async { return navidrome.getSongs(albumId); } /// Song von Navidrome runterladen Future downloadNavidromeSong(SubsonicSong s) async { final song = await navidrome.downloadSong(s); if (song != null) { await ladeSongs(); return true; } return false; } /// Song direkt vom Server streamen (ohne Download) void starteNavidromeStream(SubsonicSong s) { final uri = navidrome.streamUrl(s.id); final song = Song( titel: s.titel, kuenstler: s.kuenstler, album: s.album, dauerSekunden: s.dauerSekunden, dateiPfad: '', groesseBytes: s.groesseBytes, downloadQuelle: 'server', streamUrl: uri.toString(), ); player.spiele(song); } /// Fügt den Song direkt nach dem aktuellen in die Warteschlange ein void spieleAlsNaechstes(Song song) => player.spieleAlsNaechstes(song); /// Hängt den Song ans Ende der Warteschlange an void amEndeHinzufuegen(Song song) => player.amEndeHinzufuegen(song); @override void dispose() { _positionsSub?.cancel(); // PlayerService ist Singleton — nicht hier disposesen, lebt für die App-Lebensdauer super.dispose(); } }