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/viewmodels/melo_home_viewmodel.dart
T
Dustin e46ffa76a3 fix: 6 Claude-Audit-Fixes (3 CRIT + 3 HIGH)
CRITICAL:
- Logout: LoginScreen statt MeloHome pushen (home_screen.dart)
- Path Traversal: sanitizeDateiname() zentral + sync-loop abgesichert
- SecureStorage: flutter_secure_storage für Token+Passwort (auth_service, navidrome)

HIGH:
- Seek: PlayerService.seek() reload-frei (audio_handler)
- Play/Pause: Zielzustand statt Toggle für System-Controls
- X-User: Header entfernt, Server verlässt sich auf JWT
2026-08-03 01:41:04 +02:00

276 lines
8.3 KiB
Dart

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<Song> songs = [];
Set<String> aktiveTags = {};
bool ladt = true;
Set<int> favoritenIds = {};
List<Map<String, String>> tags = [];
List<Song> letzteSongs = [];
Map<String, int> tagCounts = {};
bool zeigeBotschaft = false;
bool serverLadt = false;
List<SubsonicAlbum> navidromeAlben = [];
Map<String, Tag> _tagsMap = {}; // Name → Tag für Filter-Lookup
Map<String, Tag> get tagsMap => _tagsMap;
int _playCount = 0;
static const int _botschaftSchwellwert = 10;
StreamSubscription<Duration>? _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<Song> 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<void> 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<String, int> _berechneTagCounts() {
final counts = <String, int>{};
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<void> 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<Song>? 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<void> 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<bool> 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<List<SubsonicSong>> ladeAlbumSongs(String albumId) async {
return navidrome.getSongs(albumId);
}
/// Song von Navidrome runterladen
Future<bool> 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() {
_positionsSub?.cancel();
// PlayerService ist Singleton — nicht hier disposesen, lebt für die App-Lebensdauer
super.dispose();
}
}