Files
AudiOhm/frontend/lib/presentation/providers/music_provider.dart
T
root 85dad89d5b feat: Modernisation UI/UX et configuration Flutter multi-plateforme
Phase 1 - Corrections Critiques:
- Fixed memory leaks dans music_provider.dart (stream subscriptions)
- Fixed race conditions dans search_provider.dart (stale results)
- Fixed token refresh errors dans api_service.dart
- Improved error handling avec messages utilisateur
- Changed API URL to HTTPS by default

Phase 2 - Améliorations UX Desktop:
- Ajouté cursor pointers sur tous les éléments cliquables
- Implémenté hover states avec effets néon glow (200ms transitions)
- Créé skeleton loading states avec shimmer animation
- Ajouté widgets: ClickableWrapper, ErrorDisplay, SkeletonLoading
- Enhanced visual feedback pour desktop users

Phase 3 - Configuration Flutter:
- Configuré Android (Gradle 8.1.0, Kotlin 1.9.0, minSdk 21, targetSdk 34)
- Créé launcher icons cyberpunk néon (5 densités)
- Configuré Windows desktop (structure complète)
- Activé Linux desktop support
- Ajouté package équatable pour entités de domaine
- Corrigé imports (colors.dart, auth_provider.dart)
- Fixed Dio API compatibility (RequestOptions)

Documentation:
- STYLE_GUIDE.md: Guide complet (100+ pages)
- DESIGN_IMPLEMENTATION_GUIDE.md: Implémentation Flutter
- BUILD_STATUS.md: Status builds + troubleshooting
- QUICKSTART_BUILDS.md: Guide rapide
- BUILD_INDEX.md: Index documentation
- PHASE_1_CORRECTIONS.md: Corrections Phase 1
- PHASE_2_UX_IMPROVEMENTS.md: Améliorations Phase 2
- PR_REVIEW_SUMMARY.md: Revue code complète
- CODE_ANALYSIS_AND_PRIORITIES.md: Analyse code

Scripts & Builds:
- BUILD_ALL.sh: Script automatisé builds multi-plateforme
- builds/: Structure avec README par plateforme
- design-system/: Système de design complet

Backend:
- Ajouté streaming HTTP Range pour audio progressif
- Enhanced YouTube service avec métadonnées complètes
- Improved error handling et validation

Generated with [Claude Code](https://claude.com/claude-code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-01-19 07:44:40 +00:00

262 lines
6.8 KiB
Dart

/// Music Provider - Player state management
library;
import 'dart:async';
import 'package:flutter/foundation.dart';
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();
_subscriptions = [];
_init();
}
late final AudioPlayer _player;
final List<StreamSubscription> _subscriptions = [];
void _init() {
// Subscribe to position stream and store subscription
_subscriptions.add(_player.positionStream.listen((position) {
state = state.copyWith(position: position);
}));
// Subscribe to duration stream and store subscription
_subscriptions.add(_player.durationStream.listen((duration) {
state = state.copyWith(duration: duration ?? Duration.zero);
}));
// Subscribe to player state stream and store subscription
_subscriptions.add(_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, errorMessage: null);
try {
// Validate audio URL exists
final streamUrl = track.audioUrl;
if (streamUrl == null || streamUrl.isEmpty) {
throw Exception('No audio URL available for track: ${track.title}');
}
await _player.setUrl(streamUrl);
if (state.queue.isEmpty) {
state = state.copyWith(queue: [track], currentIndex: 0);
}
// Clear error and loading state on success
state = state.copyWith(isLoading: false, errorMessage: null);
} on PlayerException catch (e) {
// Specific audio player errors
debugPrint('Player error loading track: ${e.message}');
state = state.copyWith(
isLoading: false,
errorMessage: 'Unable to play this track. Please try another.',
);
} catch (e) {
// Network or other errors
debugPrint('Error loading track: $e');
state = state.copyWith(
isLoading: false,
errorMessage: 'An error occurred while loading the track.',
);
}
}
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);
}
/// Convenience method to toggle play/pause
Future<void> togglePlay() async {
if (state.isPlaying) {
await pause();
} else {
await play();
}
}
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() {
// Cancel all stream subscriptions to prevent memory leaks
for (final subscription in _subscriptions) {
subscription.cancel();
}
_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,
);
});