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,224 @@
|
||||
/// 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,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user