diff --git a/lib/core/widgets/components/hero_carousel.dart b/lib/core/widgets/components/hero_carousel.dart index 49e4994..af80b0f 100644 --- a/lib/core/widgets/components/hero_carousel.dart +++ b/lib/core/widgets/components/hero_carousel.dart @@ -165,7 +165,7 @@ class _HeroCarouselState extends State { children: [ _HeroButton( icon: Icons.play_arrow, - label: 'Play', + label: 'Lecture', onPressed: () => widget.onTap(item), isPrimary: true, ), @@ -173,7 +173,7 @@ class _HeroCarouselState extends State { if (item.onMoreInfo != null) _HeroButton( icon: Icons.info_outline, - label: 'More Info', + label: 'Plus d\'infos', onPressed: item.onMoreInfo!, isPrimary: false, ), diff --git a/lib/features/iptv/screens/player_screen.dart b/lib/features/iptv/screens/player_screen.dart index 0afbfde..e794e04 100644 --- a/lib/features/iptv/screens/player_screen.dart +++ b/lib/features/iptv/screens/player_screen.dart @@ -56,8 +56,18 @@ class _PlayerScreenState extends ConsumerState { bool _isInitialized = false; bool _isLoading = true; bool _showControls = false; + bool _isPlaying = true; + double _currentPosition = 0; + double _totalDuration = 1; Timer? _controlsTimer; StreamSubscription? _messageSubscription; + + 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"; + } // Premium Features State List> _audioTracks = []; @@ -75,9 +85,18 @@ class _PlayerScreenState extends ConsumerState { if (type == 'playback_position' && _contentId.isNotEmpty) { final currentTime = (data['currentTime'] as num).toDouble(); final duration = (data['duration'] as num).toDouble(); + setState(() { + _currentPosition = currentTime; + _totalDuration = duration > 0 ? duration : 1; + }); ref.read(playbackPositionsProvider.notifier).savePosition( _contentId, currentTime, duration ); + } else if (type == 'playback_status') { + final status = data['status']; + setState(() { + _isPlaying = status == 'playing'; + }); } else if (type == 'playback_ended' && _contentId.isNotEmpty) { } else if (type == 'user_activity') { _onHover(); @@ -138,7 +157,23 @@ class _PlayerScreenState extends ConsumerState { }); } _controlsTimer?.cancel(); - _controlsTimer = Timer(const Duration(seconds: 3), _hideControls); + _controlsTimer = Timer(const Duration(seconds: 4), _hideControls); + } + + void _togglePlayPause() { + setState(() { + _isPlaying = !_isPlaying; + }); + _sendMessage({'type': _isPlaying ? 'play' : 'pause'}); + _onHover(); // Keep controls visible + } + + void _seekTo(double value) { + setState(() { + _currentPosition = value; + }); + _sendMessage({'type': 'seek', 'value': value}); + _onHover(); } void _playNext() { @@ -583,11 +618,87 @@ class _PlayerScreenState extends ConsumerState { // Hover Controls Layer if (_showControls) ...[ + // Play/Pause Button (Centered) + Positioned( + top: 0, + bottom: 0, + left: 0, + right: 0, + child: Center( + child: PointerInterceptor( + child: InkWell( + onTap: _togglePlayPause, + borderRadius: BorderRadius.circular(50), + child: Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + ), + child: Icon( + _isPlaying ? Icons.pause : Icons.play_arrow, + color: Colors.white, + size: 64, + ), + ), + ), + ), + ), + ), + + // Progress Bar (Bottom) - Only for VOD/Series + if (widget.streamType != StreamType.live) + Positioned( + bottom: 80, + left: 24, + right: 24, + child: PointerInterceptor( + child: Column( + children: [ + SliderTheme( + data: SliderThemeData( + thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8), + overlayShape: const RoundSliderOverlayShape(overlayRadius: 16), + activeTrackColor: AppColors.primary, + inactiveTrackColor: Colors.white24, + thumbColor: AppColors.primary, + overlayColor: AppColors.primary.withOpacity(0.2), + ), + child: Slider( + value: _currentPosition, + max: _totalDuration, + onChanged: (value) { + _seekTo(value); + }, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + _formatDuration(Duration(seconds: _currentPosition.toInt())), + style: const TextStyle(color: Colors.white, fontSize: 12), + ), + Text( + _formatDuration(Duration(seconds: _totalDuration.toInt())), + style: const TextStyle(color: Colors.white, fontSize: 12), + ), + ], + ), + ), + ], + ), + ), + ), + // Previous Channel (Left Top Zone) if (widget.channels != null && widget.channels!.isNotEmpty) Positioned( left: 0, - top: 80, // Moved to top + top: 80, bottom: null, height: 200, width: 100, diff --git a/lib/features/iptv/widgets/movies_tab.dart b/lib/features/iptv/widgets/movies_tab.dart index 6f6713d..1ed171f 100644 --- a/lib/features/iptv/widgets/movies_tab.dart +++ b/lib/features/iptv/widgets/movies_tab.dart @@ -162,8 +162,8 @@ class _MoviesTabState extends ConsumerState { : _movies.where((m) => settings.matchesMoviesFilter(m.categoryName)).toList(); } - // Hero Items (Take 5 random or first 5) - final heroItems = _movies.take(5).map((m) => HeroItem( + // Hero Items (Take 5 random or first 5 from filtered list) + final heroItems = displayMovies.take(5).map((m) => HeroItem( id: m.streamId, title: m.name, imageUrl: m.streamIcon ?? '', diff --git a/lib/features/iptv/widgets/series_tab.dart b/lib/features/iptv/widgets/series_tab.dart index b24da26..f2c82fb 100644 --- a/lib/features/iptv/widgets/series_tab.dart +++ b/lib/features/iptv/widgets/series_tab.dart @@ -161,7 +161,7 @@ class _SeriesTabState extends ConsumerState { } // Hero Items - final heroItems = _series.take(5).map((s) => HeroItem( + final heroItems = displaySeries.take(5).map((s) => HeroItem( id: s.seriesId.toString(), // ID logic might differ for Series title: s.name, imageUrl: s.cover ?? '', diff --git a/web/player.html b/web/player.html index 0f9958d..8415300 100644 --- a/web/player.html +++ b/web/player.html @@ -182,17 +182,33 @@ video.style.objectFit = 'contain'; } } else if (data.type === 'set_audio_track') { - // Not standard in HTML5 Video API across all browsers universally for MP4 - // But HLS.js and mpegts.js have specific APIs. - // For native "video.audioTracks", it is experimental. - // We will try to handle HLS.js switching if active. log('Switching audio track index: ' + data.index); if (window.hlsInstance) { window.hlsInstance.audioTrack = data.index; } + } else if (data.type === 'play') { + log('Received play command'); + video.play().catch(e => log('Play failed: ' + e)); + } else if (data.type === 'pause') { + log('Received pause command'); + video.pause(); + } else if (data.type === 'seek') { + log('Seeking to: ' + data.value); + if (isFinite(data.value)) { + video.currentTime = data.value; + } } }); + // Notify parent about playback state changes + video.addEventListener('play', () => { + window.parent.postMessage({ type: 'playback_status', status: 'playing' }, '*'); + }); + video.addEventListener('pause', () => { + window.parent.postMessage({ type: 'playback_status', status: 'paused' }, '*'); + }); + + // Report available audio tracks function reportAudioTracks() { let tracks = [];