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>
197 lines
5.5 KiB
Dart
197 lines
5.5 KiB
Dart
/// Artist Provider - Artist details state management
|
|
library;
|
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../../../infrastructure/datasources/remote/music_api_service.dart';
|
|
import '../../../domain/entities/artist.dart';
|
|
import '../../../domain/entities/track.dart';
|
|
import '../../../domain/entities/album.dart';
|
|
|
|
/// Artist state
|
|
class ArtistState {
|
|
final Artist? artist;
|
|
final List<Track> topTracks;
|
|
final List<Album> albums;
|
|
final List<Track> relatedTracks;
|
|
final bool isLoading;
|
|
final String? error;
|
|
|
|
const ArtistState({
|
|
this.artist,
|
|
this.topTracks = const [],
|
|
this.albums = const [],
|
|
this.relatedTracks = const [],
|
|
this.isLoading = false,
|
|
this.error,
|
|
});
|
|
|
|
ArtistState copyWith({
|
|
Artist? artist,
|
|
List<Track>? topTracks,
|
|
List<Album>? albums,
|
|
List<Track>? relatedTracks,
|
|
bool? isLoading,
|
|
String? error,
|
|
}) {
|
|
return ArtistState(
|
|
artist: artist ?? this.artist,
|
|
topTracks: topTracks ?? this.topTracks,
|
|
albums: albums ?? this.albums,
|
|
relatedTracks: relatedTracks ?? this.relatedTracks,
|
|
isLoading: isLoading ?? this.isLoading,
|
|
error: error,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Artist notifier
|
|
class ArtistNotifier extends StateNotifier<ArtistState> {
|
|
ArtistNotifier(this._musicApiService) : super(const ArtistState());
|
|
|
|
final MusicApiService _musicApiService;
|
|
|
|
/// Load complete artist information
|
|
Future<void> loadArtist(String artistId) async {
|
|
state = state.copyWith(isLoading: true, error: null);
|
|
|
|
try {
|
|
// Load artist details
|
|
final artistData = await _musicApiService.getArtist(artistId);
|
|
final artist = Artist.fromJson(artistData);
|
|
|
|
state = state.copyWith(
|
|
artist: artist,
|
|
isLoading: false,
|
|
);
|
|
} catch (e) {
|
|
state = state.copyWith(
|
|
isLoading: false,
|
|
error: e.toString(),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Load artist's top tracks
|
|
Future<void> loadTopTracks(String artistId) async {
|
|
try {
|
|
final tracksData = await _musicApiService.getArtistTopTracks(artistId);
|
|
final tracks = tracksData
|
|
.map((json) => Track.fromJson(json as Map<String, dynamic>))
|
|
.toList();
|
|
|
|
state = state.copyWith(topTracks: tracks);
|
|
} catch (e) {
|
|
state = state.copyWith(error: e.toString());
|
|
}
|
|
}
|
|
|
|
/// Load artist's albums
|
|
Future<void> loadAlbums(String artistId) async {
|
|
try {
|
|
final albumsData = await _musicApiService.getArtistAlbums(artistId);
|
|
final albums = albumsData
|
|
.map((json) => Album.fromJson(json as Map<String, dynamic>))
|
|
.toList();
|
|
|
|
state = state.copyWith(albums: albums);
|
|
} catch (e) {
|
|
state = state.copyWith(error: e.toString());
|
|
}
|
|
}
|
|
|
|
/// Load related tracks (based on artist's top track)
|
|
Future<void> loadRelatedTracks(String artistId) async {
|
|
try {
|
|
// Get first track from top tracks for recommendations
|
|
if (state.topTracks.isEmpty) {
|
|
await loadTopTracks(artistId);
|
|
}
|
|
|
|
if (state.topTracks.isNotEmpty) {
|
|
final firstTrack = state.topTracks.first;
|
|
final relatedData =
|
|
await _musicApiService.getRecommendations(firstTrack.id);
|
|
final related = relatedData
|
|
.map((json) => Track.fromJson(json as Map<String, dynamic>))
|
|
.toList();
|
|
|
|
state = state.copyWith(relatedTracks: related);
|
|
}
|
|
} catch (e) {
|
|
state = state.copyWith(error: e.toString());
|
|
}
|
|
}
|
|
|
|
/// Load all artist data at once
|
|
Future<void> loadAllArtistData(String artistId) async {
|
|
state = state.copyWith(isLoading: true, error: null);
|
|
|
|
try {
|
|
// Load all data in parallel
|
|
final results = await Future.wait([
|
|
_musicApiService.getArtist(artistId),
|
|
_musicApiService.getArtistTopTracks(artistId),
|
|
_musicApiService.getArtistAlbums(artistId),
|
|
]);
|
|
|
|
final artist = Artist.fromJson(results[0] as Map<String, dynamic>);
|
|
final topTracks = (results[1] as List)
|
|
.map((json) => Track.fromJson(json as Map<String, dynamic>))
|
|
.toList();
|
|
final albums = (results[2] as List)
|
|
.map((json) => Album.fromJson(json as Map<String, dynamic>))
|
|
.toList();
|
|
|
|
// Load related tracks based on first top track
|
|
List<Track> relatedTracks = [];
|
|
if (topTracks.isNotEmpty) {
|
|
try {
|
|
final relatedData =
|
|
await _musicApiService.getRecommendations(topTracks.first.id);
|
|
relatedTracks = relatedData
|
|
.map((json) => Track.fromJson(json as Map<String, dynamic>))
|
|
.toList();
|
|
} catch (_) {
|
|
// Don't fail if recommendations fail
|
|
}
|
|
}
|
|
|
|
state = ArtistState(
|
|
artist: artist,
|
|
topTracks: topTracks,
|
|
albums: albums,
|
|
relatedTracks: relatedTracks,
|
|
isLoading: false,
|
|
);
|
|
} catch (e) {
|
|
state = state.copyWith(
|
|
isLoading: false,
|
|
error: e.toString(),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Clear state
|
|
void clear() {
|
|
state = const ArtistState();
|
|
}
|
|
}
|
|
|
|
/// Artist provider
|
|
final artistProvider =
|
|
StateNotifierProvider<ArtistNotifier, ArtistState>((ref) {
|
|
final musicApiService = ref.watch(musicApiServiceProvider);
|
|
return ArtistNotifier(musicApiService);
|
|
});
|
|
|
|
/// Artist data provider for a specific artist ID
|
|
final artistDataProvider = Provider.family<ArtistState, String>((ref, artistId) {
|
|
final notifier = ref.watch(artistProvider.notifier);
|
|
// Load data when first accessed
|
|
if (notifier.state.artist?.id != artistId) {
|
|
Future.microtask(() => notifier.loadAllArtistData(artistId));
|
|
}
|
|
return notifier.state;
|
|
});
|