mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-11 17:29:26 +02:00
feat: Add initial IPTV player with HLS/MPEG-TS support, query parameter parsing, and parent window communication.
This commit is contained in:
1 parent
8c53cd2bb0
commit
38f3f9c96b
5 files changed
+138
-11
No files matched your search
@@ -165,7 +165,7 @@ class _HeroCarouselState extends State<HeroCarousel> {
|
||||
children: [
|
||||
_HeroButton(
|
||||
icon: Icons.play_arrow,
|
||||
label: 'Play',
|
||||
label: 'Lecture',
|
||||
onPressed: () => widget.onTap(item),
|
||||
isPrimary: true,
|
||||
),
|
||||
@@ -173,7 +173,7 @@ class _HeroCarouselState extends State<HeroCarousel> {
|
||||
if (item.onMoreInfo != null)
|
||||
_HeroButton(
|
||||
icon: Icons.info_outline,
|
||||
label: 'More Info',
|
||||
label: 'Plus d\'infos',
|
||||
onPressed: item.onMoreInfo!,
|
||||
isPrimary: false,
|
||||
),
|
||||
|
||||
@@ -56,8 +56,18 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
||||
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<Map<String, dynamic>> _audioTracks = [];
|
||||
@@ -75,9 +85,18 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
||||
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<PlayerScreen> {
|
||||
});
|
||||
}
|
||||
_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<PlayerScreen> {
|
||||
|
||||
// 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,
|
||||
|
||||
@@ -162,8 +162,8 @@ class _MoviesTabState extends ConsumerState<MoviesTab> {
|
||||
: _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 ?? '',
|
||||
|
||||
@@ -161,7 +161,7 @@ class _SeriesTabState extends ConsumerState<SeriesTab> {
|
||||
}
|
||||
|
||||
// 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 ?? '',
|
||||
|
||||
Reference in new issue
Block a user