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 'package:xtremobile/core/models/playlist_config.dart'; import 'package:xtremobile/mobile/providers/mobile_xtream_providers.dart'; import 'package:xtremobile/mobile/providers/mobile_settings_providers.dart'; import 'package:xtremobile/features/iptv/screens/native_player_screen.dart'; import 'package:xtremobile/core/theme/app_colors.dart'; import 'package:xtremobile/core/utils/image_cache_config.dart'; import 'package:xtremobile/core/models/iptv_models.dart' as model; class MobileSeriesDetailScreen extends ConsumerStatefulWidget { final model.Series series; final PlaylistConfig playlist; const MobileSeriesDetailScreen({ super.key, required this.series, required this.playlist, }); @override ConsumerState createState() => _MobileSeriesDetailScreenState(); } class _MobileSeriesDetailScreenState extends ConsumerState { model.SeriesInfo? _seriesInfo; bool _isLoading = true; String? _error; String _selectedSeason = "1"; @override void initState() { super.initState(); _loadSeriesInfo(); } 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; } Future _loadSeriesInfo() async { try { setState(() { _isLoading = true; _error = null; }); final service = await ref.read(mobileXtreamServiceProvider(widget.playlist).future); final info = await service.getSeriesInfo(widget.series.seriesId.toString()); if (mounted) { setState(() { _seriesInfo = info; _isLoading = false; if (_seriesInfo!.episodes.isNotEmpty) { _selectedSeason = _seriesInfo!.episodes.keys.first.toString(); } }); } } catch (e) { if (mounted) { setState(() { _error = e.toString(); _isLoading = false; }); } } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.background, body: _isLoading ? const Center(child: CircularProgressIndicator()) : _error != null ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.error_outline, size: 48, color: AppColors.error, ), const SizedBox(height: 16), Text( 'Error: $_error', style: const TextStyle(color: AppColors.textSecondary), ), TextButton( onPressed: _loadSeriesInfo, child: const Text('Retry'), ), ], ), ) : _buildMobileContent(), ); } Widget _buildMobileContent() { if (_seriesInfo == null) return const SizedBox.shrink(); final currentEpisodes = _seriesInfo!.episodes[_selectedSeason] ?? []; return CustomScrollView( slivers: [ // App Bar with Cover SliverAppBar( expandedHeight: 280, // Reduced from 400 for better TV visibility pinned: true, backgroundColor: AppColors.background, leading: Container( margin: const EdgeInsets.all(8), decoration: const BoxDecoration( color: Colors.black45, shape: BoxShape.circle, ), child: IconButton( icon: const Icon(Icons.arrow_back, color: AppColors.onSurface), onPressed: () => Navigator.pop(context), tooltip: 'Back', ), ), flexibleSpace: FlexibleSpaceBar( background: Stack( fit: StackFit.expand, children: [ if (_seriesInfo!.coverUrl != null) // [P1-2 FIX] Optimize series cover image cache (400x600 display) CachedNetworkImage( imageUrl: _seriesInfo!.coverUrl!, fit: BoxFit.cover, memCacheWidth: 480, memCacheHeight: 720, maxWidthDiskCache: 480, maxHeightDiskCache: 720, cacheManager: AppCacheManager.instance, errorWidget: (_, __, ___) => Container(color: AppColors.surfaceContainerLowest), ), // Gradient Overlay Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.transparent, AppColors.background.withOpacity(0.8), AppColors.background, ], stops: const [0.5, 0.8, 1.0], ), ), ), // Info Overlay Positioned( bottom: 16, left: 16, right: 16, child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( _seriesInfo!.title, style: GoogleFonts.inter( fontSize: 24, fontWeight: FontWeight.bold, color: AppColors.textPrimary, ), ), const SizedBox(height: 8), Row( children: [ if (_seriesInfo!.rating != null) ...[ const Icon( Icons.star, size: 16, color: AppColors.ratingGold, ), const SizedBox(width: 4), Text( _formatRating(_seriesInfo!.rating!)!, style: const TextStyle( color: AppColors.textSecondary, fontSize: 13, ), ), const SizedBox(width: 16), ], Text( '${_seriesInfo!.episodes.keys.length} Saisons', style: const TextStyle( color: AppColors.textSecondary, fontSize: 13, ), ), ], ), ], ), ), ], ), ), ), // Plot if (_seriesInfo!.plot != null && _seriesInfo!.plot!.isNotEmpty) SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16.0), child: Text( _seriesInfo?.plot ?? '', style: const TextStyle( color: AppColors.textSecondary, height: 1.4, ), maxLines: 4, overflow: TextOverflow.ellipsis, ), ), ), // Seasons Filter SliverToBoxAdapter( child: SizedBox( height: 50, child: Builder( builder: (context) { final isDark = Theme.of(context).brightness == Brightness.dark; return ListView.separated( padding: const EdgeInsets.symmetric(horizontal: 16), scrollDirection: Axis.horizontal, itemCount: _seriesInfo!.episodes.keys.length, separatorBuilder: (_, __) => const SizedBox(width: 8), itemBuilder: (context, index) { final seasonNum = _seriesInfo!.episodes.keys.elementAt(index); final isSelected = seasonNum == _selectedSeason; return ChoiceChip( label: Text('Saison $seasonNum'), selected: isSelected, onSelected: (selected) { if (selected) { setState(() => _selectedSeason = seasonNum.toString()); } }, backgroundColor: isDark ? AppColors.surfaceContainer : AppColors.surfaceContainerHigh, selectedColor: isDark ? AppColors.onSurface : AppColors.primaryFill, labelStyle: TextStyle( color: isSelected ? (isDark ? AppColors.onPrimaryFixed : AppColors.onSurface) : (isDark ? AppColors.textSecondary : AppColors.outlineVariant), fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), showCheckmark: false, side: BorderSide.none, ); }, ); }, ), ), ), const SliverToBoxAdapter(child: SizedBox(height: 16)), // Episodes List SliverPadding( padding: const EdgeInsets.only(bottom: 32), sliver: SliverList( delegate: SliverChildBuilderDelegate( (context, index) { final episode = currentEpisodes[index]; return _buildEpisodeTile(episode); }, childCount: currentEpisodes.length, ), ), ), ], ); } Widget _buildEpisodeTile(model.Episode episode) { final watchHistory = ref.watch(mobileWatchHistoryProvider); final episodeKey = MobileWatchHistory.episodeKey( widget.series.seriesId, int.tryParse(_selectedSeason) ?? 1, episode.episodeNum, ); final isWatched = watchHistory.isEpisodeWatched(episodeKey); final resumePos = watchHistory.getResumePosition(episodeKey); // Calculate progress double progress = 0.0; if (episode.duration > 0 && resumePos > 0) { progress = (resumePos / episode.duration).clamp(0.0, 1.0); } // Force dark mode colors since background is hardcoded dark const textColor = AppColors.onSurface; final secondaryTextColor = AppColors.textSecondary; const surfaceColor = AppColors.surfaceContainer; const primaryColor = AppColors.onSurface; // Or accent color if preferred return InkWell( onTap: () { // Enforce Native Player for Series (VOD) Navigator.push( context, MaterialPageRoute( builder: (context) => NativePlayerScreen( streamId: episode.id, title: "${widget.series.name} S${episode.season}E${episode.episodeNum}", playlist: widget.playlist, streamType: model.StreamType.series, containerExtension: episode.containerExtension, seriesId: widget.series.seriesId, season: int.tryParse(episode.season.toString()), episodeNum: episode.episodeNum, // Pass explicit initial position for resume initialPosition: resumePos > 0 ? Duration(seconds: resumePos) : null, ), ), ); }, child: Column( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: const BoxDecoration( border: Border(bottom: BorderSide(color: AppColors.onSurface12)), ), child: Row( children: [ // Play Button / Indicator Container( width: 40, height: 40, decoration: BoxDecoration( color: isWatched ? primaryColor : surfaceColor, shape: BoxShape.circle, border: isWatched ? null : Border.all(color: primaryColor, width: 2), ), child: Icon( isWatched ? Icons.check : Icons.play_arrow, color: isWatched ? AppColors.onPrimaryFixed // Checkmark on white bg : primaryColor, // Play icon size: 20, ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'E${episode.episodeNum} - ${episode.title}', style: TextStyle( color: isWatched ? secondaryTextColor : textColor, fontWeight: FontWeight.w600, fontSize: 14, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), if (episode.duration > 0) Text( _formatDuration(episode.duration), style: TextStyle( color: secondaryTextColor, fontSize: 12, ), ), ], ), ), ], ), ), // Progress Bar (Red like YouTube/Netflix) if (progress > 0 && !isWatched) LinearProgressIndicator( value: progress, backgroundColor: Colors.transparent, valueColor: const AlwaysStoppedAnimation(AppColors.error), minHeight: 2, ), ], ), ); } String _formatDuration(int seconds) { final hours = seconds ~/ 3600; final minutes = (seconds % 3600) ~/ 60; if (hours > 0) return '${hours}h ${minutes}m'; return '${minutes}m'; } }