import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:google_fonts/google_fonts.dart'; import '../../../core/models/playlist_config.dart'; import '../models/xtream_models.dart'; import '../providers/xtream_provider.dart'; import '../providers/settings_provider.dart'; import '../screens/series_detail_screen.dart'; class SeriesTab extends ConsumerStatefulWidget { final PlaylistConfig playlist; const SeriesTab({super.key, required this.playlist}); @override ConsumerState createState() => _SeriesTabState(); } class _SeriesTabState extends ConsumerState { final ScrollController _scrollController = ScrollController(); final TextEditingController _searchController = TextEditingController(); final List _series = []; List? _searchResults; String _searchQuery = ''; bool _isLoading = false; bool _isSearching = false; bool _hasMore = true; int _currentOffset = 0; static const int _pageSize = 100; Timer? _searchDebounce; @override void initState() { super.initState(); _loadMoreSeries(); _scrollController.addListener(_onScroll); } @override void dispose() { _scrollController.dispose(); _searchController.dispose(); _searchDebounce?.cancel(); super.dispose(); } void _onScroll() { if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent * 0.8) { _loadMoreSeries(); } } void _onSearchChanged(String query) { _searchDebounce?.cancel(); setState(() { _searchQuery = query; if (query.isEmpty) { _searchResults = null; _isSearching = false; } }); if (query.length >= 2) { _searchDebounce = Timer(const Duration(milliseconds: 500), () { _performSearch(query); }); } } Future _performSearch(String query) async { if (query.isEmpty) return; setState(() => _isSearching = true); try { final service = ref.read(xtreamServiceProvider(widget.playlist)); final results = await service.searchSeries(query); if (mounted && _searchQuery == query) { setState(() { _searchResults = results; _isSearching = false; }); } } catch (e) { if (mounted) { setState(() => _isSearching = false); } } } Future _loadMoreSeries() async { if (_isLoading || !_hasMore) return; setState(() { _isLoading = true; }); try { final service = ref.read(xtreamServiceProvider(widget.playlist)); final newSeries = await service.getSeriesPaginated( offset: _currentOffset, limit: _pageSize, ); setState(() { _series.addAll(newSeries); _currentOffset += _pageSize; _hasMore = newSeries.length == _pageSize; _isLoading = false; }); } catch (e) { setState(() { _isLoading = false; }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Failed to load series: $e')), ); } } } @override Widget build(BuildContext context) { final settings = ref.watch(iptvSettingsProvider); // Use search results if searching, otherwise use loaded series with category filter List displaySeries; if (_searchQuery.isNotEmpty && _searchResults != null) { displaySeries = _searchResults!; } else { displaySeries = settings.seriesKeywords.isEmpty ? _series : _series.where((s) => settings.matchesSeriesFilter(s.categoryName)).toList(); } if (_series.isEmpty && !_isLoading) { return const Center(child: Text('No series available')); } return Column( children: [ // Search bar Padding( padding: const EdgeInsets.all(8.0), child: TextField( controller: _searchController, decoration: InputDecoration( hintText: 'Search all series...', hintStyle: GoogleFonts.roboto(color: Colors.grey.shade500), prefixIcon: _isSearching ? const Padding( padding: EdgeInsets.all(12), child: SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ), ) : const Icon(Icons.search, color: Colors.grey), suffixIcon: _searchQuery.isNotEmpty ? IconButton( icon: const Icon(Icons.clear, color: Colors.grey), onPressed: () { _searchController.clear(); _onSearchChanged(''); }, ) : null, filled: true, fillColor: Colors.grey.shade800, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none, ), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), ), style: GoogleFonts.roboto(color: Colors.white), onChanged: _onSearchChanged, ), ), // Results count if (_searchQuery.isNotEmpty) Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: Align( alignment: Alignment.centerLeft, child: Text( '${displaySeries.length} result${displaySeries.length != 1 ? 's' : ''} found', style: GoogleFonts.roboto(fontSize: 12, color: Colors.grey.shade600), ), ), ), // Series grid Expanded( child: displaySeries.isEmpty && _series.isNotEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( _searchQuery.isNotEmpty ? Icons.search_off : Icons.filter_list_off, size: 48, color: Colors.grey, ), const SizedBox(height: 16), Text( _searchQuery.isNotEmpty ? 'No series match "$_searchQuery"' : 'No series match the filter', style: GoogleFonts.roboto(color: Colors.grey), ), ], ), ) : GridView.builder( controller: _scrollController, padding: const EdgeInsets.all(8), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 7, crossAxisSpacing: 6, mainAxisSpacing: 6, childAspectRatio: 0.65, ), itemCount: displaySeries.length + (_hasMore && _searchQuery.isEmpty ? 1 : 0), itemBuilder: (context, index) { if (index >= displaySeries.length) { return const Center(child: CircularProgressIndicator()); } final serie = displaySeries[index]; return _SeriesCard(series: serie, playlist: widget.playlist); }, ), ), ], ); } } class _SeriesCard extends StatelessWidget { final Series series; final PlaylistConfig playlist; const _SeriesCard({required this.series, required this.playlist}); @override Widget build(BuildContext context) { return Card( clipBehavior: Clip.antiAlias, child: InkWell( onTap: () { // Navigate to series detail screen for episode selection Navigator.push( context, MaterialPageRoute( builder: (context) => SeriesDetailScreen( series: series, playlist: playlist, ), ), ); }, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( child: series.cover != null ? CachedNetworkImage( imageUrl: series.cover!, fit: BoxFit.cover, placeholder: (context, url) => Container( color: Colors.grey.shade800, child: const Center( child: CircularProgressIndicator(strokeWidth: 2), ), ), errorWidget: (context, url, error) => Container( color: Colors.grey.shade800, child: const Icon(Icons.tv, size: 48), ), ) : Container( color: Colors.grey.shade800, child: const Icon(Icons.tv, size: 48), ), ), Container( padding: const EdgeInsets.all(8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( series.name, style: GoogleFonts.roboto( fontSize: 11, fontWeight: FontWeight.w500, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), if (series.rating != null) ...[ const SizedBox(height: 2), Row( children: [ Icon( Icons.star, size: 12, color: Colors.amber.shade700, ), const SizedBox(width: 2), Text( series.rating!, style: GoogleFonts.roboto( fontSize: 10, color: Colors.grey.shade600, ), ), ], ), ], ], ), ), ], ), ), ); } }