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/utils/responsive_layout.dart'; import '../../../core/widgets/hero_carousel.dart'; import '../../../core/widgets/glass_container.dart'; import '../../../core/widgets/tv_focusable_card.dart'; import '../providers/watch_history_provider.dart'; import '../models/xtream_models.dart'; import '../providers/xtream_provider.dart'; import '../providers/settings_provider.dart'; import '../screens/player_screen.dart'; class MoviesTab extends ConsumerStatefulWidget { final PlaylistConfig playlist; const MoviesTab({super.key, required this.playlist}); @override ConsumerState createState() => _MoviesTabState(); } class _MoviesTabState extends ConsumerState { final ScrollController _scrollController = ScrollController(); final TextEditingController _searchController = TextEditingController(); final List _movies = []; 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(); _loadMoreMovies(); _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) { _loadMoreMovies(); } } 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.searchMovies(query); if (mounted && _searchQuery == query) { setState(() { _searchResults = results; _isSearching = false; }); } } catch (e) { if (mounted) { setState(() => _isSearching = false); } } } Future _loadMoreMovies() async { if (_isLoading || !_hasMore) return; setState(() { _isLoading = true; }); try { final service = ref.read(xtreamServiceProvider(widget.playlist)); final newMovies = await service.getMoviesPaginated( offset: _currentOffset, limit: _pageSize, ); setState(() { _movies.addAll(newMovies); _currentOffset += _pageSize; _hasMore = newMovies.length == _pageSize; _isLoading = false; }); } catch (e) { setState(() { _isLoading = false; }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Failed to load movies: $e')), ); } } } 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) { if (originalUrl == null || originalUrl.isEmpty) return ''; if (originalUrl.startsWith('http://')) { return '/api/xtream/$originalUrl'; } return originalUrl; } Future _playMovie(Movie movie) async { ref.read(watchHistoryProvider.notifier).markMovieWatched(movie.streamId); // Fetch actual duration from API if not available in movie data Duration? movieDuration; if (movie.durationSecs != null && movie.durationSecs! > 0) { movieDuration = Duration(seconds: movie.durationSecs!); } else { // Try to fetch from API final service = ref.read(xtreamServiceProvider(widget.playlist)); final durationSecs = await service.getVodDuration(movie.streamId); if (durationSecs != null && durationSecs > 0) { movieDuration = Duration(seconds: durationSecs); } } if (!mounted) return; Navigator.push( context, MaterialPageRoute( builder: (context) => PlayerScreen( streamId: movie.streamId, title: movie.name, playlist: widget.playlist, streamType: StreamType.vod, containerExtension: movie.containerExtension ?? 'mp4', duration: movieDuration, ), ), ); } @override Widget build(BuildContext context) { final settings = ref.watch(iptvSettingsProvider); final watchHistory = ref.watch(watchHistoryProvider); List displayMovies; if (_searchQuery.isNotEmpty && _searchResults != null) { displayMovies = _searchResults!; } else { displayMovies = settings.moviesKeywords.isEmpty ? _movies : _movies .where((m) => settings.matchesMoviesFilter(m.categoryName)) .toList(); } const double gridItemRatio = 0.65; final int crossAxisCount = ResponsiveLayout.value( context, mobile: 3, tablet: 5, desktop: 7, ); return Scaffold( backgroundColor: Colors.transparent, body: CustomScrollView( controller: _scrollController, slivers: [ // Header (Floating Glass) SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(24, 16, 24, 24), child: GlassContainer( borderRadius: 100, opacity: 0.6, padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), child: Row( children: [ Text( 'Movies', style: GoogleFonts.inter( fontSize: 24, fontWeight: FontWeight.w700, color: Colors.white, ), ), const Spacer(), // Search Bar Container( width: 300, height: 40, decoration: BoxDecoration( color: Colors.white.withOpacity(0.1), borderRadius: BorderRadius.circular(100), ), padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ const Icon( Icons.search, size: 20, color: Colors.white54, ), const SizedBox(width: 8), Expanded( child: TextField( controller: _searchController, style: GoogleFonts.inter( fontSize: 14, color: Colors.white, ), decoration: InputDecoration( hintText: 'Search movies...', hintStyle: GoogleFonts.inter(color: Colors.white54), border: InputBorder.none, isDense: true, contentPadding: const EdgeInsets.only(bottom: 12), ), onChanged: _onSearchChanged, ), ), if (_isSearching) const SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ), if (_searchQuery.isNotEmpty) GestureDetector( onTap: () { _searchController.clear(); _onSearchChanged(''); }, child: const Icon( Icons.close, size: 16, color: Colors.white, ), ), ], ), ), ], ), ), ), ), // Hero Carousel (Only when not searching) if (_searchQuery.isEmpty && displayMovies.isNotEmpty) SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.only(bottom: 32), child: HeroCarousel( items: displayMovies .take(5) .map( (m) => HeroCarouselItem( id: m.streamId, title: m.name, imageUrl: _getProxiedImageUrl(m.streamIcon), subtitle: m.rating != null ? '${_formatRating(m.rating)} ★' : null, onPlay: () => _playMovie(m), onTap: () => _playMovie(m), ), ) .toList(), ), ), ), // Movies Grid SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 24), sliver: SliverGrid( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, crossAxisSpacing: 20, mainAxisSpacing: 20, childAspectRatio: gridItemRatio, ), delegate: SliverChildBuilderDelegate( (context, index) { if (index >= displayMovies.length) return null; final movie = displayMovies[index]; final isWatched = watchHistory.isMovieWatched(movie.streamId); return TvFocusableCard( onTap: () => _playMovie(movie), borderRadius: 12, child: Stack( fit: StackFit.expand, children: [ // Poster Image ClipRRect( borderRadius: BorderRadius.circular(12), child: movie.streamIcon != null && movie.streamIcon!.isNotEmpty ? Image.network( _getProxiedImageUrl(movie.streamIcon), fit: BoxFit.cover, errorBuilder: (ctx, err, stack) => Container( color: AppColors.surfaceVariant, child: const Center( child: Icon( Icons.movie, size: 48, color: Colors.white24, ), ), ), ) : Container( color: AppColors.surfaceVariant, child: const Center( child: Icon( Icons.movie, size: 48, color: Colors.white24, ), ), ), ), // Gradient Overlay Positioned( left: 0, right: 0, bottom: 0, height: 80, child: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.transparent, Colors.black.withOpacity(0.9), ], ), borderRadius: const BorderRadius.vertical( bottom: Radius.circular(12), ), ), ), ), // Rating Badge if (movie.rating != null && movie.rating!.isNotEmpty) Positioned( top: 8, right: 8, child: Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: Colors.black.withOpacity(0.7), borderRadius: BorderRadius.circular(4), border: Border.all(color: Colors.white24), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.star, size: 10, color: Colors.amber, ), const SizedBox(width: 4), Text( _formatRating(movie.rating)!, style: GoogleFonts.inter( fontSize: 10, fontWeight: FontWeight.bold, color: Colors.white, ), ), ], ), ), ), // Title Positioned( left: 12, right: 12, bottom: 12, child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( movie.name, style: GoogleFonts.inter( color: Colors.white, fontSize: 13, fontWeight: FontWeight.w600, shadows: [ const Shadow( color: Colors.black, blurRadius: 4, ), ], ), maxLines: 2, overflow: TextOverflow.ellipsis, ), if (isWatched) Padding( padding: const EdgeInsets.only(top: 4), child: Text( 'WATCHED', style: GoogleFonts.inter( color: AppColors.primary, fontSize: 10, fontWeight: FontWeight.bold, ), ), ), ], ), ), ], ), ); }, childCount: displayMovies.length, ), ), ), // Loader if (_isLoading) const SliverToBoxAdapter( child: Padding( padding: EdgeInsets.all(32), child: Center( child: CircularProgressIndicator(color: Colors.white), ), ), ), const SliverPadding(padding: EdgeInsets.only(bottom: 50)), ], ), ); } }