85dad89d5b
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>
141 lines
3.9 KiB
Dart
141 lines
3.9 KiB
Dart
/// Search Provider - Search state management
|
|
library;
|
|
|
|
import 'dart:async';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../../../infrastructure/datasources/remote/music_api_service.dart';
|
|
import '../../../domain/entities/track.dart';
|
|
import '../../../domain/entities/artist.dart';
|
|
import '../../../domain/entities/album.dart';
|
|
|
|
/// Search state
|
|
class SearchState {
|
|
final String query;
|
|
final bool isSearching;
|
|
final List<Track> tracks;
|
|
final List<Artist> artists;
|
|
final List<Album> albums;
|
|
final String? error;
|
|
|
|
const SearchState({
|
|
this.query = '',
|
|
this.isSearching = false,
|
|
this.tracks = const [],
|
|
this.artists = const [],
|
|
this.albums = const [],
|
|
this.error,
|
|
});
|
|
|
|
SearchState copyWith({
|
|
String? query,
|
|
bool? isSearching,
|
|
List<Track>? tracks,
|
|
List<Artist>? artists,
|
|
List<Album>? albums,
|
|
String? error,
|
|
}) {
|
|
return SearchState(
|
|
query: query ?? this.query,
|
|
isSearching: isSearching ?? this.isSearching,
|
|
tracks: tracks ?? this.tracks,
|
|
artists: artists ?? this.artists,
|
|
albums: albums ?? this.albums,
|
|
error: error,
|
|
);
|
|
}
|
|
|
|
int get totalResults => tracks.length + artists.length + albums.length;
|
|
}
|
|
|
|
/// Search notifier with debouncing
|
|
class SearchNotifier extends StateNotifier<SearchState> {
|
|
SearchNotifier(this._musicApiService) : super(const SearchState());
|
|
|
|
final MusicApiService _musicApiService;
|
|
Timer? _debounceTimer;
|
|
|
|
static const _debounceDuration = Duration(milliseconds: 500);
|
|
|
|
void search(String query) {
|
|
if (query.trim().isEmpty) {
|
|
state = const SearchState();
|
|
_debounceTimer?.cancel();
|
|
return;
|
|
}
|
|
|
|
_debounceTimer?.cancel();
|
|
state = state.copyWith(query: query, isSearching: true);
|
|
|
|
_debounceTimer = Timer(_debounceDuration, () => _performSearch(query));
|
|
}
|
|
|
|
Future<void> _performSearch(String query) async {
|
|
// Store the original query to check for race conditions
|
|
final originalQuery = query;
|
|
|
|
try {
|
|
final results = await _musicApiService.search(
|
|
query,
|
|
type: 'all',
|
|
limit: 20,
|
|
);
|
|
|
|
// CRITICAL: Only update state if this is still the current search query
|
|
// This prevents race conditions where old search results overwrite newer ones
|
|
if (state.query == originalQuery) {
|
|
state = SearchState(
|
|
query: query,
|
|
tracks: (results['tracks'] as List?)
|
|
?.map((json) => Track.fromJson(json as Map<String, dynamic>))
|
|
.toList() ??
|
|
[],
|
|
artists: (results['artists'] as List?)
|
|
?.map((json) => Artist.fromJson(json as Map<String, dynamic>))
|
|
.toList() ??
|
|
[],
|
|
albums: (results['albums'] as List?)
|
|
?.map((json) => Album.fromJson(json as Map<String, dynamic>))
|
|
.toList() ??
|
|
[],
|
|
);
|
|
} else {
|
|
// This search result is stale, ignore it
|
|
debugPrint('Ignoring stale search results for "$originalQuery" (current: "${state.query}")');
|
|
}
|
|
} catch (e) {
|
|
// Only update error state if this is still the current query
|
|
if (state.query == originalQuery) {
|
|
debugPrint('Search failed for "$originalQuery": $e');
|
|
state = SearchState(
|
|
query: query,
|
|
error: e.toString(),
|
|
);
|
|
}
|
|
} finally {
|
|
// Only clear loading state if this is still the current query
|
|
if (state.query == originalQuery) {
|
|
state = state.copyWith(isSearching: false);
|
|
}
|
|
}
|
|
}
|
|
|
|
void clear() {
|
|
_debounceTimer?.cancel();
|
|
state = const SearchState();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_debounceTimer?.cancel();
|
|
super.dispose();
|
|
}
|
|
}
|
|
|
|
/// Search provider
|
|
final searchProvider = StateNotifierProvider<SearchNotifier, SearchState>((ref) {
|
|
final musicApiService = ref.watch(musicApiServiceProvider);
|
|
return SearchNotifier(musicApiService);
|
|
});
|