mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-11 17:30:00 +02:00
feat(iptv): introduce web-based video player screen for live, VOD, and series content
This commit is contained in:
1 parent
ec9ea0f66b
commit
d48d25f65e
6 files changed
+51
-14
No files matched your search
@@ -88,7 +88,8 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
||||
final type = data['type'];
|
||||
if (type == 'playback_position' && _contentId.isNotEmpty) {
|
||||
final currentTime = (data['currentTime'] as num).toDouble();
|
||||
final duration = (data['duration'] 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;
|
||||
@@ -936,11 +937,11 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_formatDuration(Duration(seconds: _currentPosition.toInt())),
|
||||
_formatDuration(Duration(seconds: _currentPosition.isFinite ? _currentPosition.toInt() : 0)),
|
||||
style: const TextStyle(color: Colors.white, fontSize: 12),
|
||||
),
|
||||
Text(
|
||||
_formatDuration(Duration(seconds: _totalDuration.toInt())),
|
||||
_formatDuration(Duration(seconds: _totalDuration.isFinite ? _totalDuration.toInt() : 0)),
|
||||
style: const TextStyle(color: Colors.white, fontSize: 12),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -140,6 +140,15 @@ class _MoviesTabState extends ConsumerState<MoviesTab> {
|
||||
return rating;
|
||||
}
|
||||
|
||||
/// Proxy HTTP images through backend to avoid CORS/mixed-content issues
|
||||
String? _getProxiedImageUrl(String? originalUrl) {
|
||||
if (originalUrl == null || originalUrl.isEmpty) return null;
|
||||
if (originalUrl.startsWith('http://')) {
|
||||
return '/api/xtream/$originalUrl';
|
||||
}
|
||||
return originalUrl;
|
||||
}
|
||||
|
||||
void _playMovie(Movie movie) {
|
||||
ref.read(watchHistoryProvider.notifier).markMovieWatched(movie.streamId);
|
||||
|
||||
@@ -175,7 +184,7 @@ class _MoviesTabState extends ConsumerState<MoviesTab> {
|
||||
final heroItems = displayMovies.take(5).map((m) => HeroItem(
|
||||
id: m.streamId,
|
||||
title: m.name,
|
||||
imageUrl: m.streamIcon ?? '',
|
||||
imageUrl: _getProxiedImageUrl(m.streamIcon),
|
||||
subtitle: m.rating != null ? '${_formatRating(m.rating)} ★' : null,
|
||||
onMoreInfo: () {
|
||||
_playMovie(m);
|
||||
@@ -291,7 +300,7 @@ class _MoviesTabState extends ConsumerState<MoviesTab> {
|
||||
|
||||
return MediaCard(
|
||||
title: movie.name,
|
||||
imageUrl: movie.streamIcon,
|
||||
imageUrl: _getProxiedImageUrl(movie.streamIcon),
|
||||
|
||||
subtitle: movie.rating != null ? '${_formatRating(movie.rating)} ★' : null,
|
||||
rating: _formatRating(movie.rating),
|
||||
|
||||
@@ -140,6 +140,15 @@ class _SeriesTabState extends ConsumerState<SeriesTab> {
|
||||
return rating;
|
||||
}
|
||||
|
||||
/// Proxy HTTP images through backend to avoid CORS/mixed-content issues
|
||||
String? _getProxiedImageUrl(String? originalUrl) {
|
||||
if (originalUrl == null || originalUrl.isEmpty) return null;
|
||||
if (originalUrl.startsWith('http://')) {
|
||||
return '/api/xtream/$originalUrl';
|
||||
}
|
||||
return originalUrl;
|
||||
}
|
||||
|
||||
void _openSeries(Series series) {
|
||||
Navigator.push(
|
||||
context,
|
||||
@@ -173,7 +182,7 @@ class _SeriesTabState extends ConsumerState<SeriesTab> {
|
||||
final heroItems = displaySeries.take(5).map((s) => HeroItem(
|
||||
id: s.seriesId.toString(), // ID logic might differ for Series
|
||||
title: s.name,
|
||||
imageUrl: s.cover ?? '',
|
||||
imageUrl: _getProxiedImageUrl(s.cover),
|
||||
subtitle: s.rating != null ? '${_formatRating(s.rating)} ★' : null,
|
||||
onMoreInfo: () => _openSeries(s),
|
||||
)).toList();
|
||||
@@ -281,7 +290,7 @@ class _SeriesTabState extends ConsumerState<SeriesTab> {
|
||||
final serie = displaySeries[index];
|
||||
return MediaCard(
|
||||
title: serie.name,
|
||||
imageUrl: serie.cover,
|
||||
imageUrl: _getProxiedImageUrl(serie.cover),
|
||||
|
||||
subtitle: serie.rating != null ? '${_formatRating(serie.rating)} ★' : null,
|
||||
rating: _formatRating(serie.rating),
|
||||
|
||||
@@ -125,6 +125,15 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab> {
|
||||
return rating;
|
||||
}
|
||||
|
||||
/// Proxy HTTP images through backend to avoid CORS/mixed-content issues
|
||||
String? _getProxiedImageUrl(String? originalUrl) {
|
||||
if (originalUrl == null || originalUrl.isEmpty) return null;
|
||||
if (originalUrl.startsWith('http://')) {
|
||||
return '/api/xtream/$originalUrl';
|
||||
}
|
||||
return originalUrl;
|
||||
}
|
||||
|
||||
void _playMovie(Movie movie) {
|
||||
ref.read(watchHistoryProvider.notifier).markMovieWatched(movie.streamId);
|
||||
|
||||
@@ -160,7 +169,7 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab> {
|
||||
final heroItems = _movies.take(3).map((m) => HeroItem(
|
||||
id: m.streamId,
|
||||
title: m.name,
|
||||
imageUrl: m.streamIcon ?? '',
|
||||
imageUrl: _getProxiedImageUrl(m.streamIcon),
|
||||
subtitle: m.rating != null ? '${_formatRating(m.rating)} ★' : null,
|
||||
onMoreInfo: () => _playMovie(m),
|
||||
)).toList();
|
||||
@@ -251,7 +260,7 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab> {
|
||||
|
||||
return MediaCard(
|
||||
title: movie.name,
|
||||
imageUrl: movie.streamIcon,
|
||||
imageUrl: _getProxiedImageUrl(movie.streamIcon),
|
||||
|
||||
subtitle: movie.rating != null ? '${_formatRating(movie.rating)} ★' : null,
|
||||
rating: _formatRating(movie.rating),
|
||||
|
||||
@@ -124,6 +124,15 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab> {
|
||||
return rating;
|
||||
}
|
||||
|
||||
/// Proxy HTTP images through backend to avoid CORS/mixed-content issues
|
||||
String? _getProxiedImageUrl(String? originalUrl) {
|
||||
if (originalUrl == null || originalUrl.isEmpty) return null;
|
||||
if (originalUrl.startsWith('http://')) {
|
||||
return '/api/xtream/$originalUrl';
|
||||
}
|
||||
return originalUrl;
|
||||
}
|
||||
|
||||
void _openSeries(Series series) {
|
||||
Navigator.push(
|
||||
context,
|
||||
@@ -153,7 +162,7 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab> {
|
||||
final heroItems = _series.take(3).map((s) => HeroItem(
|
||||
id: s.seriesId.toString(),
|
||||
title: s.name,
|
||||
imageUrl: s.cover ?? '',
|
||||
imageUrl: _getProxiedImageUrl(s.cover),
|
||||
subtitle: s.rating != null ? '${_formatRating(s.rating)} ★' : null,
|
||||
onMoreInfo: () => _openSeries(s),
|
||||
)).toList();
|
||||
@@ -251,7 +260,7 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab> {
|
||||
|
||||
return MediaCard(
|
||||
title: series.name,
|
||||
imageUrl: series.cover,
|
||||
imageUrl: _getProxiedImageUrl(series.cover),
|
||||
|
||||
subtitle: series.rating != null ? '${_formatRating(series.rating)} ★' : null,
|
||||
rating: _formatRating(series.rating),
|
||||
|
||||
+3
-3
@@ -144,7 +144,7 @@
|
||||
window.parent.postMessage({
|
||||
type: 'playback_position',
|
||||
currentTime: video.currentTime,
|
||||
duration: video.duration || 0
|
||||
duration: isFinite(video.duration) ? video.duration : 0
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
@@ -158,7 +158,7 @@
|
||||
window.parent.postMessage({
|
||||
type: 'playback_position',
|
||||
currentTime: video.currentTime,
|
||||
duration: video.duration || 0
|
||||
duration: isFinite(video.duration) ? video.duration : 0
|
||||
}, '*');
|
||||
});
|
||||
|
||||
@@ -166,7 +166,7 @@
|
||||
video.addEventListener('ended', () => {
|
||||
window.parent.postMessage({
|
||||
type: 'playback_ended',
|
||||
duration: video.duration || 0
|
||||
duration: isFinite(video.duration) ? video.duration : 0
|
||||
}, '*');
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user