Files
AudiOhm/frontend/lib/presentation/providers/search_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

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);
});