import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:path/path.dart' as p; 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 '../services/cloud_service.dart'; import '../services/melo_logger.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:path_provider/path_provider.dart'; import 'dart:io'; 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(); final CloudService cloud = CloudService(); Timer? _autoSyncTimer; 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; String? _nutzerBotschaft; /// Per-User Easteregg-Botschaften static const _botschaften = { 'Baka': 'πŸ’Œ Seit 2008 – Danke, dass du immer da bist β™₯', 'Tinker': 'πŸŽ€ FΓΌr meine beste Freundin – Melo & Melo πŸ’•', }; String? get nutzerBotschaft => _nutzerBotschaft; void setzeNutzer(String name) { _nutzerBotschaft = _botschaften[name]; } 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 ALLEN aktiven Tags (AND-Logik) return songs.where((s) { if (s.tagIds == null) return false; return aktiveTags.every((tagName) { final tag = _tagsMap[tagName]; return tag != null && s.tagIds!.contains(tag.id); }); }).toList(); } Future ladeSongs() async { ladt = true; notifyListeners(); try { // Auto-Restore der Navidrome-Session beim Start // Dummies bereinigen await db.alteDummiesLoeschen(); await navidrome.ladeGespeicherteZugangsdaten(); if (navidrome.istVerbunden) { ladeNavidromeAlben(); } // Echte Songs aus SQLite laden (ohne unspielbare Fake-Dummies) songs = await db.alleSongs(); favoritenIds = await favoriten.favoritenIds(); letzteSongs = await db.letzteWiedergaben(); await ladeTags(); _starteCloudSyncScheduler(); } 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); } @override void dispose() { _autoSyncTimer?.cancel(); _positionsSub?.cancel(); player.dispose(); super.dispose(); } Future _starteCloudSyncScheduler() async { _autoSyncTimer?.cancel(); final prefs = await SharedPreferences.getInstance(); final autoSync = prefs.getBool('cloud_auto') ?? true; final intervalStunden = prefs.getInt('cloud_interval') ?? 6; if (!autoSync || intervalStunden <= 0) return; _autoSyncTimer = Timer.periodic(Duration(hours: intervalStunden), (_) async { try { await cloud.restoreLogin(); if (!cloud.istAngemeldet) return; final serverSongs = await cloud.listSongs(); if (serverSongs.isEmpty) return; // Nur neue Songs herunterladen final dir = Directory('${(await getApplicationDocumentsDirectory()).path}/music'); if (!await dir.exists()) await dir.create(recursive: true); final localFiles = dir.listSync().whereType() .map((f) => f.path.split('/').last).toSet(); for (final s in serverSongs) { final title = p.basename(s['title'].toString()); if (localFiles.contains(title)) continue; // schon lokal await cloud.download(s['id'].toString(), '${dir.path}/$title'); } await ladeSongs(); // UI aktualisieren } catch (e) { MeloLogger().fehler('background_auto_sync', e); } }); } }