import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'dart:html' as html; import 'dart:ui_web' as ui_web; import 'dart:async'; import 'package:google_fonts/google_fonts.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; import '../providers/xtream_provider.dart'; import '../providers/playback_positions_provider.dart'; import '../providers/settings_provider.dart'; import '../widgets/epg_overlay.dart'; import '../../../core/models/playlist_config.dart'; import '../../../core/models/iptv_models.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/widgets/glass_container.dart'; import '../../../core/widgets/tv_focusable_card.dart'; enum StreamType { live, vod, series } class PlayerScreen extends ConsumerStatefulWidget { final String streamId; final String title; final StreamType streamType; final String containerExtension; final PlaylistConfig playlist; final List? channels; final int initialIndex; const PlayerScreen({ super.key, required this.streamId, required this.title, required this.playlist, this.streamType = StreamType.live, this.containerExtension = 'mp4', this.channels, this.initialIndex = 0, }); @override ConsumerState createState() => _PlayerScreenState(); } class _PlayerScreenState extends ConsumerState { // ... (Keep existing state variables) String? _errorMessage; String? _statusMessage; late String _viewId; late String _contentId; late int _currentIndex; bool _isInitialized = false; bool _isLoading = true; bool _showControls = false; bool _isPlaying = true; double _currentPosition = 0; double _totalDuration = 1; double _volume = 1.0; double _previousVolume = 1.0; bool _isFullscreen = false; Timer? _controlsTimer; StreamSubscription? _messageSubscription; List> _audioTracks = []; String _aspectRatio = 'contain'; // ... (Keep existing methods: _formatDuration, _setupMessageListener, _sendMessage, etc.) // For brevity in this artifact, I will focus on the BUILD method and styling changes. // The logic remains largely the same, just the UI wrapper changes. String _formatDuration(Duration duration) { String twoDigits(int n) => n.toString().padLeft(2, '0'); String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60)); String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60)); return "${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds"; } void _setupMessageListener() { _messageSubscription = html.window.onMessage.listen((event) { // ... (Same logic as before) try { final data = event.data; if (data is Map) { final type = data['type']; if (type == 'playback_position' && _contentId.isNotEmpty) { final currentTime = (data['currentTime'] as num).toDouble(); final rawDuration = (data['duration'] as num).toDouble(); final duration = rawDuration.isFinite ? rawDuration : 0.0; setState(() { _currentPosition = currentTime; _totalDuration = duration > 0 ? duration : 1; }); } else if (type == 'playback_status') { setState(() => _isPlaying = data['status'] == 'playing'); } else if (type == 'user_activity') { _onHover(); } } } catch (e) { debugPrint('PostMessage Error: $e'); } }); } void _sendMessage(Map message) { final iframe = html.document.getElementById(_viewId) as html.IFrameElement?; iframe?.contentWindow?.postMessage(message, '*'); } @override void initState() { super.initState(); _currentIndex = widget.initialIndex; _initializePlayer(); _setupMessageListener(); } @override void dispose() { _messageSubscription?.cancel(); _controlsTimer?.cancel(); super.dispose(); } void _onHover() { if (!_showControls) setState(() => _showControls = true); _controlsTimer?.cancel(); _controlsTimer = Timer(const Duration(seconds: 4), () { if (mounted) setState(() => _showControls = false); }); } void _togglePlayPause() { setState(() => _isPlaying = !_isPlaying); _sendMessage({'type': _isPlaying ? 'play' : 'pause'}); _onHover(); } void _toggleFullscreen() { if (!_isFullscreen) { html.document.documentElement?.requestFullscreen(); } else { html.document.exitFullscreen(); } setState(() => _isFullscreen = !_isFullscreen); } void _nextChannel() { if (widget.channels == null || widget.channels!.isEmpty) return; setState(() { _currentIndex = (_currentIndex + 1) % widget.channels!.length; _isInitialized = false; // Trigger reload }); // Update player for new stream // We need to re-initialize with the new stream ID from the list _initializePlayer(isChannelSwitch: true); } void _previousChannel() { if (widget.channels == null || widget.channels!.isEmpty) return; setState(() { _currentIndex = (_currentIndex - 1 + widget.channels!.length) % widget.channels!.length; _isInitialized = false; }); _initializePlayer(isChannelSwitch: true); } Future _initializePlayer({double? startTimeOverride, bool isChannelSwitch = false}) async { setState(() { _isLoading = true; _statusMessage = 'Loading...'; }); try { // Determine Stream ID (Initial or Current Index) final currentStreamId = isChannelSwitch && widget.channels != null ? widget.channels![_currentIndex].streamId : widget.streamId; _viewId = 'iptv-player-$currentStreamId-${DateTime.now().millisecondsSinceEpoch}'; final service = ref.read(xtreamServiceProvider(widget.playlist)); String streamUrl = ''; // Generate Stream URL based on type if (widget.streamType == StreamType.live) { streamUrl = service.getLiveStreamUrl(currentStreamId); } else if (widget.streamType == StreamType.vod) { streamUrl = service.getVodStreamUrl(currentStreamId, widget.containerExtension); } else if (widget.streamType == StreamType.series) { streamUrl = service.getSeriesStreamUrl(currentStreamId, widget.containerExtension); } final encodedUrl = Uri.encodeComponent(streamUrl); var playerSrc = 'player.html?url=$encodedUrl'; if (startTimeOverride != null) { playerSrc += '&t=$startTimeOverride'; } // Register View Factory ui_web.platformViewRegistry.registerViewFactory(_viewId, (int viewId) { final iframe = html.IFrameElement() ..id = _viewId ..src = playerSrc ..style.border = 'none' ..style.width = '100%' ..style.height = '100%' ..allow = 'autoplay; fullscreen; picture-in-picture; encrypted-media'; return iframe; }); if (mounted) { setState(() { _isInitialized = true; _isLoading = false; }); } } catch (e) { if (mounted) { setState(() { _errorMessage = 'Failed to load stream: $e'; _isLoading = false; }); } } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, body: Stack( fit: StackFit.expand, children: [ // 1. Video Player Layer if (_isInitialized) HtmlElementView(viewType: _viewId), // 2. Interaction Layer Positioned.fill( child: PointerInterceptor( child: MouseRegion( onHover: (_) => _onHover(), child: GestureDetector( onTap: () { if (_showControls) { setState(() => _showControls = false); } else { _onHover(); // Shows controls and starts timer } }, behavior: HitTestBehavior.translucent, child: Container(color: Colors.transparent), ), ), ), ), // 3. UI Overlay (Glassmorphism) if (_showControls || _isLoading) Positioned.fill( child: PointerInterceptor( child: Container( color: Colors.black.withOpacity(0.3), // Dim background child: Stack( children: [ // Top Bar (Back + Title) Positioned( top: 24, left: 24, right: 24, child: Row( children: [ TvFocusableCard( onTap: () => Navigator.pop(context), borderRadius: 50, child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white.withOpacity(0.1), shape: BoxShape.circle, ), child: const Icon(Icons.arrow_back_rounded, color: Colors.white), ), ), const SizedBox(width: 24), Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( widget.channels != null ? widget.channels![_currentIndex].name : widget.title, style: GoogleFonts.inter( fontSize: 24, fontWeight: FontWeight.w700, color: Colors.white, ), ), if (widget.streamType == StreamType.live && widget.channels != null) Text( 'Live TV', style: GoogleFonts.inter(fontSize: 14, color: AppColors.live, fontWeight: FontWeight.bold) ), ], ), ], ), ), // Center Play/Pause (Animated) Center( child: _isLoading ? const CircularProgressIndicator(color: Colors.white) : TvFocusableCard( onTap: _togglePlayPause, borderRadius: 100, scaleFactor: 1.2, child: Container( width: 80, height: 80, decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), shape: BoxShape.circle, border: Border.all(color: Colors.white.withOpacity(0.3)), ), child: Icon( _isPlaying ? Icons.pause_rounded : Icons.play_arrow_rounded, color: Colors.white, size: 48, ), ), ), ), // EPG Overlay (Moved out of controls box) if (widget.streamType == StreamType.live) Positioned( bottom: 140, // Positioned above the controls bar left: 40, child: EpgOverlay( streamId: widget.channels != null ? widget.channels![_currentIndex].streamId : widget.streamId, playlist: widget.playlist, ), ), // Bottom Control Bar Positioned( bottom: 40, left: 40, right: 40, child: GlassContainer( borderRadius: 24, opacity: 0.8, padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Progress Bar (if not live) if (widget.streamType != StreamType.live) Padding( padding: const EdgeInsets.only(bottom: 16), child: Row( children: [ Text(_formatDuration(Duration(seconds: _currentPosition.toInt())), style: const TextStyle(color: Colors.white70)), const SizedBox(width: 16), Expanded( child: SliderTheme( data: SliderThemeData( trackHeight: 4, thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8), overlayShape: const RoundSliderOverlayShape(overlayRadius: 16), activeTrackColor: AppColors.primary, inactiveTrackColor: Colors.white24, thumbColor: Colors.white, ), child: Slider( value: _currentPosition, min: 0, max: _totalDuration, onChanged: (v) { _sendMessage({'type': 'seek', 'value': v}); }, ), ), ), const SizedBox(width: 16), Text(_formatDuration(Duration(seconds: _totalDuration.toInt())), style: const TextStyle(color: Colors.white70)), ], ), ), // Controls Row Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildTvControl(Icons.skip_previous_rounded, _previousChannel), const SizedBox(width: 32), // Increased spacing _buildTvControl(Icons.skip_next_rounded, _nextChannel), const Spacer(), _buildTvControl(Icons.subtitles_rounded, () {}), const SizedBox(width: 16), _buildTvControl(Icons.aspect_ratio_rounded, _toggleFullscreen), ], ), ], ), ), ), ], ), ), ), ), ], ), ); } Widget _buildTvControl(IconData icon, VoidCallback onTap) { return TvFocusableCard( onTap: onTap, borderRadius: 12, child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: Icon(icon, color: Colors.white, size: 24), ), ); } }