import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import '../../../../features/iptv/providers/xtream_provider.dart'; import '../../../../features/iptv/providers/settings_provider.dart'; import '../../../../features/iptv/providers/favorites_provider.dart'; import '../screens/mobile_player_screen.dart'; import '../../../../core/api/channel_logo.dart'; import '../../../../core/models/iptv_models.dart'; import '../../../../core/models/playlist_config.dart'; import '../../../../core/theme/app_colors.dart'; import '../../../../core/widgets/glass_container.dart'; class MobileLiveTVTab extends ConsumerStatefulWidget { final PlaylistConfig playlist; const MobileLiveTVTab({super.key, required this.playlist}); @override ConsumerState createState() => _MobileLiveTVTabState(); } class _MobileLiveTVTabState extends ConsumerState { String? _selectedCategory; final TextEditingController _searchController = TextEditingController(); String _searchQuery = ''; bool _showFavoritesOnly = false; @override void initState() { super.initState(); _searchController.addListener(() { setState(() => _searchQuery = _searchController.text.toLowerCase()); }); } @override Widget build(BuildContext context) { final channelsAsync = ref.watch(liveChannelsByPlaylistProvider(widget.playlist)); final favorites = ref.watch(favoritesProvider); final settings = ref.watch(iptvSettingsProvider); return Scaffold( backgroundColor: Colors.transparent, // Handled by MobileScaffold body: channelsAsync.when( loading: () => Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const CircularProgressIndicator(), const SizedBox(height: 16), const Text( 'Loading channels...', style: TextStyle(color: AppColors.onSurfaceVariant), ), const SizedBox(height: 24), FutureBuilder( future: Future.delayed(const Duration(seconds: 8)), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { return TextButton( onPressed: () => ref.refresh( liveChannelsByPlaylistProvider(widget.playlist), ), child: const Text('Taking too long? Tap to retry'), ); } return const SizedBox.shrink(); }, ), ], ), ), error: (e, s) => Center( child: Padding( padding: const EdgeInsets.all(24.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline, color: AppColors.error, size: 48), const SizedBox(height: 16), Text( 'Failed to load channels', style: GoogleFonts.karla( color: AppColors.onSurface, fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), Text( '$e', style: const TextStyle(color: AppColors.onSurface54), textAlign: TextAlign.center, ), const SizedBox(height: 24), ElevatedButton( onPressed: () => ref .refresh(liveChannelsByPlaylistProvider(widget.playlist)), child: const Text('Retry'), ), ], ), ), ), data: (groupedChannels) { var categories = groupedChannels.keys.toList(); if (settings.liveTvKeywords.isNotEmpty) { categories = categories .where((cat) => settings.matchesLiveTvFilter(cat)) .toList(); } // REMOVED alphabetic sort to respect server/settings order List displayedChannels = []; if (_searchQuery.isNotEmpty) { displayedChannels = groupedChannels.values .expand((l) => l) .where((c) => c.name.toLowerCase().contains(_searchQuery)) .toList(); } else if (_showFavoritesOnly) { displayedChannels = groupedChannels.values .expand((l) => l) .where((c) => favorites.contains(c.streamId)) .toList(); } else { if (_selectedCategory == null && categories.isNotEmpty) { _selectedCategory = categories.first; } // If the selected category is no longer available (due to filters), reset it if (_selectedCategory != null && !categories.contains(_selectedCategory)) { _selectedCategory = categories.isNotEmpty ? categories.first : null; } if (_selectedCategory != null) { displayedChannels = groupedChannels[_selectedCategory] ?? []; } } return SafeArea( bottom: false, child: Column( children: [ // Header & Search Padding( padding: const EdgeInsets.all(16), child: GlassContainer.glass( borderRadius: 16, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ Expanded( child: TextField( controller: _searchController, style: GoogleFonts.karla(color: AppColors.onSurface), decoration: InputDecoration( hintText: 'Search channels...', hintStyle: GoogleFonts.karla(color: AppColors.onSurface54), border: InputBorder.none, prefixIcon: const Icon( Icons.search, color: AppColors.onSurface54, ), isDense: true, ), ), ), if (_searchQuery.isNotEmpty) GestureDetector( onTap: () => _searchController.clear(), child: const Icon(Icons.close, color: AppColors.onSurface54), ), ], ), ), ), // Categories if (_searchQuery.isEmpty && !_showFavoritesOnly) SizedBox( height: 40, child: ListView.separated( padding: const EdgeInsets.symmetric(horizontal: 16), scrollDirection: Axis.horizontal, itemCount: categories.length, separatorBuilder: (_, __) => const SizedBox(width: 8), itemBuilder: (context, index) { final category = categories[index]; final isSelected = category == _selectedCategory; return GestureDetector( onTap: () => setState(() => _selectedCategory = category), child: AnimatedContainer( duration: const Duration(milliseconds: 200), padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 10, ), decoration: BoxDecoration( color: isSelected ? AppColors.onSurface : Colors.black.withOpacity(0.4), borderRadius: BorderRadius.circular(20), ), child: Text( category, style: GoogleFonts.karla( color: isSelected ? Colors.black : AppColors.onSurfaceVariant, fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, fontSize: 13, ), ), ), ); }, ), ), // Sub-header Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), child: Row( children: [ Text( _searchQuery.isNotEmpty ? 'Results' : _showFavoritesOnly ? 'Favorites' : _selectedCategory ?? 'Channels', style: GoogleFonts.karla( color: AppColors.onSurface, fontWeight: FontWeight.bold, fontSize: 18, ), ), const Spacer(), GestureDetector( onTap: () => setState( () => _showFavoritesOnly = !_showFavoritesOnly, ), child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: _showFavoritesOnly ? AppColors.live.withOpacity(0.2) : AppColors.onSurface.withOpacity(0.1), borderRadius: BorderRadius.circular(8), border: _showFavoritesOnly ? Border.all(color: AppColors.live) : null, ), child: Icon( _showFavoritesOnly ? Icons.favorite : Icons.favorite_border, color: _showFavoritesOnly ? AppColors.live : AppColors.onSurface, size: 20, ), ), ), ], ), ), // List Expanded( child: displayedChannels.isEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.tv_off, color: AppColors.onSurface24, size: 48, ), const SizedBox(height: 16), Text( 'No channels found', style: GoogleFonts.karla(color: AppColors.onSurface38), ), ], ), ) : ListView.separated( padding: const EdgeInsets.fromLTRB(16, 0, 16, 100), itemCount: displayedChannels.length, separatorBuilder: (_, __) => const SizedBox(height: 8), itemBuilder: (context, index) { final channel = displayedChannels[index]; return _MobileChannelTile( channel: channel, onTap: () => _playChannel( context, channel, displayedChannels, ), ); }, ), ), ], ), ); }, ), ); } void _playChannel( BuildContext context, Channel channel, List channels, ) { Navigator.of(context).push( MaterialPageRoute( builder: (context) => MobilePlayerScreen( streamId: channel.streamId, title: channel.name, playlist: widget.playlist, streamType: StreamType.live, channels: channels, ), ), ); } } class _MobileChannelTile extends ConsumerWidget { final Channel channel; final VoidCallback onTap; const _MobileChannelTile({ required this.channel, required this.onTap, }); @override Widget build(BuildContext context, WidgetRef ref) { // Sans ce bouton, aucun moyen d'ajouter un favori : le filtre « Favoris » // de l'en-tête affichait toujours une liste vide. final isFav = ref.watch(favoritesProvider).contains(channel.streamId); final iconUrl = channelLogoUrl( streamIcon: channel.streamIcon, name: channel.name, ); return GlassContainer.glass( borderRadius: 12, padding: EdgeInsets.zero, child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(12), child: Row( children: [ Container( width: 48, height: 48, decoration: BoxDecoration( color: AppColors.onSurface.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), padding: const EdgeInsets.all(4), child: Image.network( iconUrl, fit: BoxFit.contain, cacheWidth: 120, filterQuality: FilterQuality.low, errorBuilder: (_, __, ___) => const Icon(Icons.tv, color: AppColors.onSurface24), ), ), const SizedBox(width: 16), Expanded( child: Text( channel.name, style: GoogleFonts.karla( color: AppColors.onSurface, fontWeight: FontWeight.w500, fontSize: 14, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), IconButton( visualDensity: VisualDensity.compact, tooltip: isFav ? 'Retirer des favoris' : 'Ajouter aux favoris', icon: Icon( isFav ? Icons.favorite : Icons.favorite_border, color: isFav ? AppColors.primary : AppColors.onSurface54, size: 20, ), onPressed: () => ref .read(favoritesProvider.notifier) .toggleFavorite(channel.streamId), ), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: AppColors.onSurface.withOpacity(0.1), shape: BoxShape.circle, ), child: const Icon( Icons.play_arrow_rounded, color: AppColors.onSurface, size: 16, ), ), ], ), ), ), ); } }