import 'package:flutter/material.dart'; import '../models/video_item.dart'; import '../services/app_settings_service.dart'; import '../services/preferred_language_service.dart'; import '../services/youtube_service.dart'; import '../widgets/video_result_card.dart'; import 'video_player_page.dart'; class SearchPage extends StatefulWidget { const SearchPage({super.key}); @override State createState() => _SearchPageState(); } class _SearchPageState extends State { final TextEditingController _controller = TextEditingController(); final YoutubeService _youtubeService = YoutubeService(); List _results = []; bool _loading = false; String? _error; String _lastQuery = ''; Future _search() async { final query = _controller.text.trim(); if (query.isEmpty || _loading) { return; } FocusScope.of(context).unfocus(); setState(() { _loading = true; _error = null; _lastQuery = query; }); try { final results = await _youtubeService.searchVideos(query); final preferredLanguage = AppSettingsService.instance.preferredLanguage.value; final rankedResults = PreferredLanguageService.rankVideos( results, preferredLanguage, ); if (!mounted) { return; } setState(() { _results = rankedResults; }); } catch (error) { if (!mounted) { return; } setState(() { _results = []; _error = 'Impossible de récupérer les résultats YouTube.\n' 'Vérifie ta connexion puis réessaie.'; }); } finally { if (mounted) { setState(() { _loading = false; }); } } } void _clear() { _controller.clear(); setState(() { _results = []; _error = null; _lastQuery = ''; }); } void _openVideo(VideoItem video) { Navigator.of(context).push( MaterialPageRoute( builder: (context) => VideoPlayerPage(video: video), ), ); } @override void dispose() { _controller.dispose(); _youtubeService.close(); super.dispose(); } @override Widget build(BuildContext context) { return SafeArea( child: Column( children: [ _SearchHeader( controller: _controller, loading: _loading, onSearch: _search, onClear: _clear, ), Expanded( child: _buildBody(), ), ], ), ); } Widget _buildBody() { if (_loading && _results.isEmpty) { return Center( child: CircularProgressIndicator(), ); } if (_error != null) { return _ErrorState( message: _error!, onRetry: _search, ); } if (_results.isEmpty) { if (_lastQuery.isNotEmpty) { return Center( child: Padding( padding: EdgeInsets.all(24), child: Text( 'Aucune vidéo trouvée pour « $_lastQuery ».', textAlign: TextAlign.center, style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.86), fontSize: 16, ), ), ), ); } return _EmptySearchState(); } return Column( children: [ Padding( padding: EdgeInsets.fromLTRB(20, 4, 20, 6), child: Row( children: [ Expanded( child: Text( 'Résultats pour « $_lastQuery »', maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant, fontWeight: FontWeight.w500, ), ), ), if (_loading) SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ), ], ), ), Expanded( child: ListView.separated( padding: EdgeInsets.fromLTRB(12, 4, 12, 24), itemCount: _results.length, separatorBuilder: (context, index) => SizedBox(height: 4), itemBuilder: (context, index) { final video = _results[index]; return VideoResultCard( video: video, onTap: () => _openVideo(video), ); }, ), ), ], ); } } class _SearchHeader extends StatelessWidget { const _SearchHeader({ required this.controller, required this.loading, required this.onSearch, required this.onClear, }); final TextEditingController controller; final bool loading; final VoidCallback onSearch; final VoidCallback onClear; @override Widget build(BuildContext context) { final wide = MediaQuery.sizeOf(context).width >= 700; return Padding( padding: EdgeInsets.fromLTRB( wide ? 28 : 16, 18, wide ? 28 : 16, 14, ), child: Center( child: ConstrainedBox( constraints: BoxConstraints(maxWidth: 900), child: Row( children: [ Expanded( child: TextField( controller: controller, textInputAction: TextInputAction.search, onSubmitted: (_) => onSearch(), decoration: InputDecoration( hintText: 'Rechercher sur YouTube...', prefixIcon: Icon(Icons.search), suffixIcon: ValueListenableBuilder( valueListenable: controller, builder: (context, value, child) { if (value.text.isEmpty) { return SizedBox.shrink(); } return IconButton( tooltip: 'Effacer', onPressed: onClear, icon: Icon(Icons.close), ); }, ), filled: true, fillColor: Theme.of(context).colorScheme.surfaceContainerHigh, border: OutlineInputBorder( borderRadius: BorderRadius.circular(30), borderSide: BorderSide.none, ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(30), borderSide: BorderSide( color: Theme.of(context).colorScheme.outlineVariant, ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(30), borderSide: BorderSide( color: Color(0xFFFF4E45), width: 1.5, ), ), ), ), ), SizedBox(width: 10), IconButton.filled( tooltip: 'Rechercher', onPressed: loading ? null : onSearch, icon: Icon(Icons.search), ), ], ), ), ), ); } } class _EmptySearchState extends StatelessWidget { const _EmptySearchState(); @override Widget build(BuildContext context) { return Center( child: Padding( padding: EdgeInsets.all(28), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.youtube_searched_for, size: 70, color: Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.44), ), SizedBox(height: 18), Text( 'Recherche YouTube', style: TextStyle( fontSize: 22, fontWeight: FontWeight.w600, ), ), SizedBox(height: 8), Text( 'Saisis un titre, une chaîne ou un sujet.', textAlign: TextAlign.center, style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.86), fontSize: 15, ), ), ], ), ), ); } } class _ErrorState extends StatelessWidget { const _ErrorState({ required this.message, required this.onRetry, }); final String message; final VoidCallback onRetry; @override Widget build(BuildContext context) { return Center( child: Padding( padding: EdgeInsets.all(28), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.cloud_off_outlined, size: 62, color: Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.62), ), SizedBox(height: 16), Text( message, textAlign: TextAlign.center, style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 15, ), ), SizedBox(height: 18), FilledButton.icon( onPressed: onRetry, icon: Icon(Icons.refresh), label: Text('Réessayer'), ), ], ), ), ); } }