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
92 lines
2.4 KiB
Dart
92 lines
2.4 KiB
Dart
import 'dart:async';
|
|
import 'package:audio_service/audio_service.dart';
|
|
import 'package:just_audio/just_audio.dart';
|
|
import '../models/song.dart';
|
|
import 'player_service.dart';
|
|
|
|
/// Hintergrund-Audio-Handler für Sperrbildschirm & Benachrichtigung
|
|
class MeloAudioHandler extends BaseAudioHandler {
|
|
final PlayerService _player = PlayerService();
|
|
|
|
MeloAudioHandler() {
|
|
// Zustand vom Player an audio_service weiterleiten
|
|
_player.stateStream.listen(_updateState);
|
|
_player.positionStream.listen((pos) {
|
|
if (_playing) {
|
|
playbackState.add(playbackState.value.copyWith(
|
|
updatePosition: pos,
|
|
));
|
|
}
|
|
});
|
|
_player.onSongWechsel.listen((song) {
|
|
if (song != null) {
|
|
mediaItem.add(_toMediaItem(song));
|
|
}
|
|
});
|
|
}
|
|
|
|
bool _playing = false;
|
|
|
|
void _updateState(PlayerState state) {
|
|
_playing = state.playing;
|
|
playbackState.add(PlaybackState(
|
|
controls: _playing
|
|
? [MediaControl.pause, MediaControl.skipToPrevious, MediaControl.skipToNext]
|
|
: [MediaControl.play, MediaControl.skipToPrevious, MediaControl.skipToNext],
|
|
systemActions: const {MediaAction.seek},
|
|
androidCompactActionIndices: const [0, 1, 2],
|
|
processingState: _playing ? AudioProcessingState.ready : AudioProcessingState.idle,
|
|
playing: _playing,
|
|
speed: 1.0,
|
|
));
|
|
}
|
|
|
|
MediaItem _toMediaItem(Song song) => MediaItem(
|
|
id: song.id?.toString() ?? '0',
|
|
album: song.album.isEmpty ? 'Melo' : song.album,
|
|
title: song.titel,
|
|
artist: song.kuenstler,
|
|
duration: Duration(seconds: song.dauerSekunden),
|
|
artUri: song.coverPfad != null ? Uri.file(song.coverPfad!) : null,
|
|
);
|
|
|
|
@override
|
|
Future<void> play() async {
|
|
if (_player.aktuellerSong != null) {
|
|
_playing = true;
|
|
await _player.play();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> pause() async {
|
|
_playing = false;
|
|
await _player.pause();
|
|
}
|
|
|
|
@override
|
|
Future<void> stop() async {
|
|
_playing = false;
|
|
await _player.pause();
|
|
playbackState.add(playbackState.value.copyWith(
|
|
controls: [MediaControl.play],
|
|
playing: false,
|
|
processingState: AudioProcessingState.idle,
|
|
));
|
|
}
|
|
|
|
@override
|
|
Future<void> seek(Duration position) async {
|
|
await _player.seek(position);
|
|
}
|
|
|
|
@override
|
|
Future<void> skipToNext() => _player.naechstes();
|
|
|
|
@override
|
|
Future<void> skipToPrevious() => _player.vorheriges();
|
|
|
|
// Exposed for the ViewModel
|
|
PlayerService get player => _player;
|
|
}
|