Initial commit: AudiOhm - Alternative Spotify avec streaming YouTube
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>
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
/// 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;
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user