a89c7894cf
Backend: - FastAPI avec PostgreSQL et Redis - Authentification JWT complète - API REST pour musique, playlists, recherche - Streaming audio via yt-dlp - SQLAlchemy 2.0 async Frontend: - Flutter avec thème néon cyberpunk - State management Riverpod - Layout adaptatif desktop/mobile - Lecteur audio avec mini-player Infrastructure: - Docker Compose (PostgreSQL + Redis) - Scripts d'installation automatisés - Scripts de build pour exécutables Fichiers ajoutés: - BUILD_CLIENT_*.bat/sh: Scripts de compilation - BUILD_CLIENT_README.md: Documentation compilation - CHECK_FLUTTER.sh: Vérificateur d'environnement - requirements.txt mis à jour pour Python 3.13 - Modèles SQLAlchemy corrigés (metadata -> extra_metadata) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
225 lines
5.4 KiB
Dart
225 lines
5.4 KiB
Dart
/// Music Provider - Player state management
|
|
library;
|
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:just_audio/just_audio.dart';
|
|
|
|
import '../../../domain/entities/track.dart';
|
|
|
|
/// Player state
|
|
class PlayerState {
|
|
final List<Track> queue;
|
|
final int currentIndex;
|
|
final bool isPlaying;
|
|
final Duration position;
|
|
final Duration duration;
|
|
final bool isLoading;
|
|
final String? errorMessage;
|
|
|
|
const PlayerState({
|
|
this.queue = const [],
|
|
this.currentIndex = -1,
|
|
this.isPlaying = false,
|
|
this.position = Duration.zero,
|
|
this.duration = Duration.zero,
|
|
this.isLoading = false,
|
|
this.errorMessage,
|
|
});
|
|
|
|
Track? get currentTrack =>
|
|
currentIndex >= 0 && currentIndex < queue.length
|
|
? queue[currentIndex]
|
|
: null;
|
|
|
|
PlayerState copyWith({
|
|
List<Track>? queue,
|
|
int? currentIndex,
|
|
bool? isPlaying,
|
|
Duration? position,
|
|
Duration? duration,
|
|
bool? isLoading,
|
|
String? errorMessage,
|
|
}) {
|
|
return PlayerState(
|
|
queue: queue ?? this.queue,
|
|
currentIndex: currentIndex ?? this.currentIndex,
|
|
isPlaying: isPlaying ?? this.isPlaying,
|
|
position: position ?? this.position,
|
|
duration: duration ?? this.duration,
|
|
isLoading: isLoading ?? this.isLoading,
|
|
errorMessage: errorMessage,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Player notifier
|
|
class PlayerNotifier extends StateNotifier<PlayerState> {
|
|
PlayerNotifier() : super(const PlayerState()) {
|
|
_player = AudioPlayer();
|
|
_init();
|
|
}
|
|
|
|
late final AudioPlayer _player;
|
|
|
|
void _init() {
|
|
_player.positionStream.listen((position) {
|
|
state = state.copyWith(position: position);
|
|
});
|
|
|
|
_player.durationStream.listen((duration) {
|
|
state = state.copyWith(duration: duration ?? Duration.zero);
|
|
});
|
|
|
|
_player.playerStateStream.listen((playerState) {
|
|
state = state.copyWith(
|
|
isPlaying: playerState.playing,
|
|
isLoading: playerState.processingState == ProcessingState.loading,
|
|
);
|
|
});
|
|
}
|
|
|
|
Future<void> loadTrack(Track track) async {
|
|
state = state.copyWith(isLoading: true);
|
|
|
|
try {
|
|
// Get stream URL from API
|
|
final streamUrl = track.audioUrl ?? '';
|
|
|
|
await _player.setUrl(streamUrl);
|
|
|
|
if (state.queue.isEmpty) {
|
|
state = state.copyWith(queue: [track], currentIndex: 0);
|
|
}
|
|
} catch (e) {
|
|
state = state.copyWith(
|
|
isLoading: false,
|
|
errorMessage: e.toString(),
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> play() async {
|
|
if (state.currentTrack != null) {
|
|
await _player.play();
|
|
state = state.copyWith(isPlaying: true);
|
|
}
|
|
}
|
|
|
|
Future<void> pause() async {
|
|
await _player.pause();
|
|
state = state.copyWith(isPlaying: false);
|
|
}
|
|
|
|
Future<void> seek(Duration position) async {
|
|
await _player.seek(position);
|
|
}
|
|
|
|
Future<void> next() async {
|
|
if (state.currentIndex < state.queue.length - 1) {
|
|
final nextTrack = state.queue[state.currentIndex + 1];
|
|
await loadTrack(nextTrack);
|
|
state = state.copyWith(currentIndex: state.currentIndex + 1);
|
|
await play();
|
|
}
|
|
}
|
|
|
|
Future<void> previous() async {
|
|
if (state.currentIndex > 0) {
|
|
final previousTrack = state.queue[state.currentIndex - 1];
|
|
await loadTrack(previousTrack);
|
|
state = state.copyWith(currentIndex: state.currentIndex - 1);
|
|
await play();
|
|
}
|
|
}
|
|
|
|
void setQueue(List<Track> tracks, {int startIndex = 0}) {
|
|
state = state.copyWith(
|
|
queue: tracks,
|
|
currentIndex: startIndex,
|
|
);
|
|
}
|
|
|
|
void addToQueue(Track track) {
|
|
final newQueue = [...state.queue, track];
|
|
state = state.copyWith(queue: newQueue);
|
|
}
|
|
|
|
void removeFromQueue(int index) {
|
|
if (index >= 0 && index < state.queue.length) {
|
|
final newQueue = [...state.queue]..removeAt(index);
|
|
state = state.copyWith(queue: newQueue);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_player.dispose();
|
|
super.dispose();
|
|
}
|
|
}
|
|
|
|
/// Player provider
|
|
final playerProvider =
|
|
StateNotifierProvider<PlayerNotifier, PlayerState>((ref) {
|
|
return PlayerNotifier();
|
|
});
|
|
|
|
/// Current track provider
|
|
final currentTrackProvider = Provider<Track?>((ref) {
|
|
return ref.watch(playerProvider).currentTrack;
|
|
});
|
|
|
|
/// Queue view data class
|
|
class QueueViewData {
|
|
final Track? currentTrack;
|
|
final List<Track> queue;
|
|
final int currentIndex;
|
|
final bool isPlaying;
|
|
|
|
const QueueViewData({
|
|
required this.currentTrack,
|
|
required this.queue,
|
|
required this.currentIndex,
|
|
required this.isPlaying,
|
|
});
|
|
|
|
/// Get upcoming tracks (after current)
|
|
List<Track> get nextTracks {
|
|
if (currentIndex < 0 || currentIndex >= queue.length - 1) {
|
|
return [];
|
|
}
|
|
return queue.sublist(currentIndex + 1);
|
|
}
|
|
|
|
/// Get previously played tracks (before current)
|
|
List<Track> get previousTracks {
|
|
if (currentIndex <= 0) {
|
|
return [];
|
|
}
|
|
return queue.sublist(0, currentIndex);
|
|
}
|
|
|
|
/// Check if queue has tracks
|
|
bool get hasQueue => queue.isNotEmpty;
|
|
|
|
/// Check if there are upcoming tracks
|
|
bool get hasNextTracks => nextTracks.isNotEmpty;
|
|
|
|
/// Check if there are previous tracks
|
|
bool get hasPreviousTracks => previousTracks.isNotEmpty;
|
|
|
|
/// Get total queue count excluding current
|
|
int get queueCount => queue.length - 1;
|
|
}
|
|
|
|
/// Queue view provider
|
|
final queueProvider = Provider<QueueViewData>((ref) {
|
|
final playerState = ref.watch(playerProvider);
|
|
return QueueViewData(
|
|
currentTrack: playerState.currentTrack,
|
|
queue: playerState.queue,
|
|
currentIndex: playerState.currentIndex,
|
|
isPlaying: playerState.isPlaying,
|
|
);
|
|
});
|