import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import '../../../../core/models/playlist_config.dart'; import '../../../../core/theme/app_colors.dart'; import '../../../../core/widgets/components/hero_carousel.dart'; import '../../../../core/widgets/glass_container.dart'; import '../../../../features/iptv/models/xtream_models.dart'; import '../../../../features/iptv/providers/xtream_provider.dart'; import '../../../../features/iptv/providers/settings_provider.dart'; import '../screens/mobile_series_detail_screen.dart'; class MobileSeriesTab extends ConsumerStatefulWidget { final PlaylistConfig playlist; const MobileSeriesTab({super.key, required this.playlist}); @override ConsumerState createState() => _MobileSeriesTabState(); } class _MobileSeriesTabState 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 = 50; 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 (!_isLoading && _hasMore && _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 || !mounted) return; setState(() => _isLoading = true); try { final service = ref.read(xtreamServiceProvider(widget.playlist)); final newSeries = await service.getSeriesPaginated( offset: _currentOffset, limit: _pageSize, ); if (mounted) { setState(() { _series.addAll(newSeries); _currentOffset += _pageSize; _hasMore = newSeries.length == _pageSize; _isLoading = false; }); } } catch (e) { if (mounted) { setState(() => _isLoading = false); } } } String? _formatRating(String? rating) { if (rating == null || rating.isEmpty) return null; final value = double.tryParse(rating); if (value != null) { return value.toStringAsFixed(1); } return rating; } String _getProxiedImageUrl(String? originalUrl, WidgetRef ref) { if (originalUrl == null || originalUrl.isEmpty) return ''; if (originalUrl.startsWith('http')) { final service = ref.read(xtreamServiceProvider(widget.playlist)); return '${service.backendBaseUrl}/api/xtream/$originalUrl'; } return originalUrl; } void _openSeries(Series series) { Navigator.push( context, MaterialPageRoute( builder: (context) => MobileSeriesDetailScreen( series: series, playlist: widget.playlist, ), ), ); } @override Widget build(BuildContext context) { final settings = ref.watch(iptvSettingsProvider); List displaySeries; if (_searchQuery.isNotEmpty && _searchResults != null) { displaySeries = _searchResults!; } else { displaySeries = settings.seriesKeywords.isEmpty ? _series : _series .where((s) => settings.matchesSeriesFilter(s.categoryName)) .toList(); } final heroItems = displaySeries .take(3) .map( (s) => HeroItem( id: s.seriesId.toString(), title: s.name, imageUrl: _getProxiedImageUrl(s.cover, ref), subtitle: s.rating != null ? '${_formatRating(s.rating)} ★' : null, onMoreInfo: () => _openSeries(s), ), ) .toList(); return SafeArea( bottom: false, child: CustomScrollView( controller: _scrollController, slivers: [ // Glass Header & Search SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16), child: GlassContainer.glass( borderRadius: 100, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ const Icon(Icons.search, size: 20, color: AppColors.onSurfaceVariant), const SizedBox(width: 8), Expanded( child: TextField( controller: _searchController, style: GoogleFonts.instrumentSans( fontSize: 14, color: AppColors.onSurface, ), decoration: InputDecoration( hintText: 'Search Series...', hintStyle: GoogleFonts.instrumentSans(color: AppColors.onSurfaceVariant), border: InputBorder.none, isDense: true, contentPadding: const EdgeInsets.only(bottom: 11), ), onChanged: _onSearchChanged, ), ), if (_isSearching) const SizedBox( width: 12, height: 12, child: CircularProgressIndicator( strokeWidth: 2, color: AppColors.onSurface, ), ), if (_searchQuery.isNotEmpty) GestureDetector( onTap: () { _searchController.clear(); _onSearchChanged(''); }, child: const Icon( Icons.close, size: 16, color: AppColors.onSurface, ), ), ], ), ), ), ), // Hero Section if (_searchQuery.isEmpty && heroItems.isNotEmpty) SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.only(bottom: 24), child: SizedBox( height: 250, child: HeroCarousel( items: heroItems, onTap: (item) { try { final series = _series.firstWhere( (s) => s.seriesId.toString() == item.id, ); _openSeries(series); } catch (e) { // Fallback } }, ), ), ), ), // Grid SliverPadding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 80), sliver: SliverGrid( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 12, mainAxisSpacing: 16, childAspectRatio: 0.65, ), delegate: SliverChildBuilderDelegate( (context, index) { if (index >= displaySeries.length) return null; final series = displaySeries[index]; return GestureDetector( onTap: () => _openSeries(series), child: Stack( fit: StackFit.expand, children: [ // Poster ClipRRect( borderRadius: BorderRadius.circular(16), child: series.cover != null && series.cover!.isNotEmpty ? Image.network( _getProxiedImageUrl(series.cover, ref), fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container(color: AppColors.surfaceContainerLow), ) : Container( color: AppColors.surfaceContainerLow, child: const Icon( Icons.tv, color: AppColors.outlineVariant, ), ), ), // Gradient Overlay Positioned( bottom: 0, left: 0, right: 0, height: 80, child: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.transparent, AppColors.background.withOpacity(0.9), ], ), borderRadius: const BorderRadius.vertical( bottom: Radius.circular(16), ), ), ), ), // Title Positioned( bottom: 12, left: 12, right: 12, child: Text( series.name, style: GoogleFonts.instrumentSans( color: AppColors.onSurface, fontSize: 13, fontWeight: FontWeight.w600, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), // Rating if (series.rating != null && series.rating!.isNotEmpty) Positioned( top: 8, right: 8, child: GlassContainer.glass( borderRadius: 4, padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.star, size: 10, color: Colors.amber, ), const SizedBox(width: 4), Text( _formatRating(series.rating)!, style: GoogleFonts.instrumentSans( fontSize: 10, fontWeight: FontWeight.bold, color: AppColors.onSurface, ), ), ], ), ), ), ], ), ); }, childCount: displaySeries.length, ), ), ), if (_isLoading) const SliverToBoxAdapter( child: Padding( padding: EdgeInsets.all(24), child: Center( child: CircularProgressIndicator(color: AppColors.onSurface), ), ), ), ], ), ); } }