9c504d2c3d
Features: - Frontend Flutter avec thème néon cyberpunk - Backend FastAPI avec streaming YouTube - Base de données PostgreSQL + Redis - Authentification JWT complète - Recherche multi-source (DB + YouTube) - Playlists CRUD avec drag & drop - Queue management - Settings avec audio quality - Interface adaptative (Desktop + Mobile) Tech Stack: - Frontend: Flutter 3.2+, Riverpod - Backend: Python 3.11+, FastAPI - Database: PostgreSQL 15+ - Cache: Redis 7+ - Streaming: yt-dlp + FFmpeg 🚀 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
167 lines
4.6 KiB
Dart
167 lines
4.6 KiB
Dart
/// Album Provider - Album details state management
|
|
library;
|
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'dart:math';
|
|
|
|
import '../../../infrastructure/datasources/remote/music_api_service.dart';
|
|
import '../../../domain/entities/album.dart';
|
|
import '../../../domain/entities/track.dart';
|
|
import 'music_provider.dart';
|
|
|
|
/// Album state
|
|
class AlbumState {
|
|
final Album? album;
|
|
final List<Track> tracks;
|
|
final bool isLoading;
|
|
final String? error;
|
|
|
|
const AlbumState({
|
|
this.album,
|
|
this.tracks = const [],
|
|
this.isLoading = false,
|
|
this.error,
|
|
});
|
|
|
|
AlbumState copyWith({
|
|
Album? album,
|
|
List<Track>? tracks,
|
|
bool? isLoading,
|
|
String? error,
|
|
}) {
|
|
return AlbumState(
|
|
album: album ?? this.album,
|
|
tracks: tracks ?? this.tracks,
|
|
isLoading: isLoading ?? this.isLoading,
|
|
error: error,
|
|
);
|
|
}
|
|
|
|
/// Get total duration of all tracks in seconds
|
|
int get totalDuration {
|
|
return tracks.fold(0, (sum, track) {
|
|
return sum + (track.duration ?? 0);
|
|
});
|
|
}
|
|
|
|
/// Get formatted total duration (hours:minutes:seconds)
|
|
String get formattedTotalDuration {
|
|
final totalSeconds = totalDuration;
|
|
final hours = totalSeconds ~/ 3600;
|
|
final minutes = (totalSeconds % 3600) ~/ 60;
|
|
final seconds = totalSeconds % 60;
|
|
|
|
if (hours > 0) {
|
|
return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
|
} else {
|
|
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Album notifier
|
|
class AlbumNotifier extends StateNotifier<AlbumState> {
|
|
AlbumNotifier(this._musicApiService) : super(const AlbumState());
|
|
|
|
final MusicApiService _musicApiService;
|
|
|
|
/// Load complete album information with tracks
|
|
Future<void> loadAlbum(String albumId) async {
|
|
state = state.copyWith(isLoading: true, error: null);
|
|
|
|
try {
|
|
// Load album details and tracks in parallel
|
|
final results = await Future.wait([
|
|
_musicApiService.getAlbum(albumId),
|
|
_musicApiService.getAlbumTracks(albumId),
|
|
]);
|
|
|
|
final album = Album.fromJson(results[0] as Map<String, dynamic>);
|
|
final tracks = (results[1] as List)
|
|
.map((json) => Track.fromJson(json as Map<String, dynamic>))
|
|
.toList();
|
|
|
|
// Sort tracks by track number
|
|
tracks.sort((a, b) {
|
|
final aNum = a.trackNumber ?? 0;
|
|
final bNum = b.trackNumber ?? 0;
|
|
return aNum.compareTo(bNum);
|
|
});
|
|
|
|
state = AlbumState(
|
|
album: album,
|
|
tracks: tracks,
|
|
isLoading: false,
|
|
);
|
|
} catch (e) {
|
|
state = state.copyWith(
|
|
isLoading: false,
|
|
error: e.toString(),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Play all tracks from album
|
|
Future<void> playAll(PlayerNotifier playerNotifier) async {
|
|
if (state.tracks.isEmpty) return;
|
|
|
|
playerNotifier.setQueue(state.tracks, startIndex: 0);
|
|
await playerNotifier.loadTrack(state.tracks.first);
|
|
await playerNotifier.play();
|
|
}
|
|
|
|
/// Shuffle and play all tracks from album
|
|
Future<void> shuffle(PlayerNotifier playerNotifier) async {
|
|
if (state.tracks.isEmpty) return;
|
|
|
|
// Create shuffled list
|
|
final shuffledTracks = List<Track>.from(state.tracks);
|
|
final random = Random();
|
|
for (int i = shuffledTracks.length - 1; i > 0; i--) {
|
|
final j = random.nextInt(i + 1);
|
|
final temp = shuffledTracks[i];
|
|
shuffledTracks[i] = shuffledTracks[j];
|
|
shuffledTracks[j] = temp;
|
|
}
|
|
|
|
playerNotifier.setQueue(shuffledTracks, startIndex: 0);
|
|
await playerNotifier.loadTrack(shuffledTracks.first);
|
|
await playerNotifier.play();
|
|
}
|
|
|
|
/// Play specific track from album
|
|
Future<void> playTrack(
|
|
PlayerNotifier playerNotifier,
|
|
Track track,
|
|
) async {
|
|
final index = state.tracks.indexWhere((t) => t.id == track.id);
|
|
if (index == -1) return;
|
|
|
|
playerNotifier.setQueue(state.tracks, startIndex: index);
|
|
await playerNotifier.loadTrack(track);
|
|
await playerNotifier.play();
|
|
}
|
|
|
|
/// Clear state
|
|
void clear() {
|
|
state = const AlbumState();
|
|
}
|
|
}
|
|
|
|
/// Album provider
|
|
final albumProvider =
|
|
StateNotifierProvider<AlbumNotifier, AlbumState>((ref) {
|
|
final musicApiService = ref.watch(musicApiServiceProvider);
|
|
return AlbumNotifier(musicApiService);
|
|
});
|
|
|
|
/// Album data provider for a specific album ID
|
|
final albumDataProvider = Provider.family<AlbumState, String>((ref, albumId) {
|
|
final notifier = ref.watch(albumProvider.notifier);
|
|
// Load data when first accessed
|
|
if (notifier.state.album?.id != albumId) {
|
|
Future.microtask(() => notifier.loadAlbum(albumId));
|
|
}
|
|
return notifier.state;
|
|
});
|