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>
244 lines
6.3 KiB
Dart
244 lines
6.3 KiB
Dart
/// Playlist Provider - State management for playlist details
|
|
library;
|
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../../../domain/entities/playlist.dart';
|
|
import '../../../domain/entities/track.dart';
|
|
import '../../../infrastructure/datasources/remote/playlist_api_service.dart';
|
|
import '../../providers/music_provider.dart';
|
|
|
|
/// Playlist state
|
|
class PlaylistState {
|
|
final Playlist? playlist;
|
|
final List<Track> tracks;
|
|
final bool isLoading;
|
|
final bool isReordering;
|
|
final String? error;
|
|
|
|
const PlaylistState({
|
|
this.playlist,
|
|
this.tracks = const [],
|
|
this.isLoading = false,
|
|
this.isReordering = false,
|
|
this.error,
|
|
});
|
|
|
|
PlaylistState copyWith({
|
|
Playlist? playlist,
|
|
List<Track>? tracks,
|
|
bool? isLoading,
|
|
bool? isReordering,
|
|
String? error,
|
|
}) {
|
|
return PlaylistState(
|
|
playlist: playlist ?? this.playlist,
|
|
tracks: tracks ?? this.tracks,
|
|
isLoading: isLoading ?? this.isLoading,
|
|
isReordering: isReordering ?? this.isReordering,
|
|
error: error,
|
|
);
|
|
}
|
|
|
|
/// Get total duration of all tracks
|
|
Duration get totalDuration {
|
|
final totalSeconds = tracks.fold<int>(
|
|
0,
|
|
(sum, track) => sum + (track.duration ?? 0),
|
|
);
|
|
return Duration(seconds: totalSeconds);
|
|
}
|
|
|
|
/// Format total duration
|
|
String get formattedTotalDuration {
|
|
final duration = totalDuration;
|
|
final hours = duration.inHours;
|
|
final minutes = duration.inMinutes.remainder(60);
|
|
|
|
if (hours > 0) {
|
|
return '${hours}h ${minutes}m';
|
|
} else {
|
|
return '${minutes}m';
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Playlist notifier
|
|
class PlaylistNotifier extends StateNotifier<PlaylistState> {
|
|
PlaylistNotifier(this._playlistApiService) : super(const PlaylistState());
|
|
|
|
final PlaylistApiService _playlistApiService;
|
|
|
|
/// Load playlist with tracks
|
|
Future<void> loadPlaylist(String playlistId) async {
|
|
state = state.copyWith(isLoading: true, error: null);
|
|
|
|
try {
|
|
final response = await _playlistApiService.getPlaylist(playlistId);
|
|
|
|
// Parse playlist
|
|
final playlist = Playlist.fromJson(response);
|
|
|
|
// Parse tracks from response
|
|
final tracks = <Track>[];
|
|
if (response['tracks'] != null) {
|
|
for (final trackData in response['tracks'] as List) {
|
|
if (trackData is Map<String, dynamic> && trackData['track'] != null) {
|
|
final track = Track.fromJson(trackData['track'] as Map<String, dynamic>);
|
|
tracks.add(track);
|
|
}
|
|
}
|
|
}
|
|
|
|
state = PlaylistState(
|
|
playlist: playlist,
|
|
tracks: tracks,
|
|
isLoading: false,
|
|
);
|
|
} catch (e) {
|
|
state = state.copyWith(
|
|
isLoading: false,
|
|
error: e.toString(),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Add track to playlist
|
|
Future<void> addTrack(Track track, {int? position}) async {
|
|
if (state.playlist == null) return;
|
|
|
|
try {
|
|
await _playlistApiService.addTracks(
|
|
state.playlist!.id,
|
|
[track.id],
|
|
position: position,
|
|
);
|
|
|
|
// Reload playlist to get updated tracks
|
|
await loadPlaylist(state.playlist!.id);
|
|
} catch (e) {
|
|
state = state.copyWith(error: e.toString());
|
|
}
|
|
}
|
|
|
|
/// Remove track from playlist
|
|
Future<void> removeTrack(String trackId) async {
|
|
if (state.playlist == null) return;
|
|
|
|
try {
|
|
await _playlistApiService.removeTrack(state.playlist!.id, trackId);
|
|
|
|
// Update local state
|
|
final updatedTracks = state.tracks.where((t) => t.id != trackId).toList();
|
|
|
|
state = state.copyWith(tracks: updatedTracks);
|
|
} catch (e) {
|
|
state = state.copyWith(error: e.toString());
|
|
}
|
|
}
|
|
|
|
/// Reorder tracks
|
|
Future<void> reorderTracks(int oldIndex, int newIndex) async {
|
|
if (state.playlist == null || state.tracks.isEmpty) return;
|
|
|
|
// Update local state immediately for responsiveness
|
|
final updatedTracks = List<Track>.from(state.tracks);
|
|
final track = updatedTracks.removeAt(oldIndex);
|
|
updatedTracks.insert(newIndex, track);
|
|
|
|
state = state.copyWith(tracks: updatedTracks, isReordering: true);
|
|
|
|
try {
|
|
// Call API to update position
|
|
await _playlistApiService.reorderTrack(
|
|
state.playlist!.id,
|
|
track.id,
|
|
newIndex,
|
|
);
|
|
|
|
state = state.copyWith(isReordering: false);
|
|
} catch (e) {
|
|
// Revert on error
|
|
state = state.copyWith(
|
|
tracks: state.tracks,
|
|
isReordering: false,
|
|
error: e.toString(),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Update playlist details
|
|
Future<void> updatePlaylist({
|
|
String? name,
|
|
String? description,
|
|
String? imageUrl,
|
|
bool? isPublic,
|
|
}) async {
|
|
if (state.playlist == null) return;
|
|
|
|
try {
|
|
final response = await _playlistApiService.updatePlaylist(
|
|
state.playlist!.id,
|
|
name: name,
|
|
description: description,
|
|
imageUrl: imageUrl,
|
|
isPublic: isPublic,
|
|
);
|
|
|
|
final updatedPlaylist = Playlist.fromJson(response);
|
|
state = state.copyWith(playlist: updatedPlaylist);
|
|
} catch (e) {
|
|
state = state.copyWith(error: e.toString());
|
|
}
|
|
}
|
|
|
|
/// Delete playlist
|
|
Future<void> deletePlaylist() async {
|
|
if (state.playlist == null) return;
|
|
|
|
try {
|
|
await _playlistApiService.deletePlaylist(state.playlist!.id);
|
|
state = const PlaylistState();
|
|
} catch (e) {
|
|
state = state.copyWith(error: e.toString());
|
|
}
|
|
}
|
|
|
|
/// Shuffle and play playlist
|
|
void shufflePlaylist(PlayerNotifier playerNotifier) {
|
|
if (state.tracks.isEmpty) return;
|
|
|
|
final shuffledTracks = List<Track>.from(state.tracks)..shuffle();
|
|
playerNotifier.setQueue(shuffledTracks, startIndex: 0);
|
|
}
|
|
|
|
/// Play playlist from start
|
|
void playPlaylist(PlayerNotifier playerNotifier) {
|
|
if (state.tracks.isEmpty) return;
|
|
|
|
playerNotifier.setQueue(state.tracks, startIndex: 0);
|
|
}
|
|
}
|
|
|
|
/// Playlist provider
|
|
final playlistProvider =
|
|
StateNotifierProvider.family<PlaylistNotifier, PlaylistState, String>(
|
|
(ref, playlistId) {
|
|
final playlistApiService = ref.watch(playlistApiServiceProvider);
|
|
final notifier = PlaylistNotifier(playlistApiService);
|
|
|
|
// Auto-load playlist
|
|
Future.microtask(() => notifier.loadPlaylist(playlistId));
|
|
|
|
return notifier;
|
|
},
|
|
);
|
|
|
|
/// Current playlist tracks provider (for easy access)
|
|
final playlistTracksProvider = Provider.family<List<Track>, String>(
|
|
(ref, playlistId) {
|
|
final playlistState = ref.watch(playlistProvider(playlistId));
|
|
return playlistState.tracks;
|
|
},
|
|
);
|