mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-22 06:28:04 +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'];
|
final type = data['type'];
|
||||||
if (type == 'playback_position' && _contentId.isNotEmpty) {
|
if (type == 'playback_position' && _contentId.isNotEmpty) {
|
||||||
final currentTime = (data['currentTime'] as num).toDouble();
|
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(() {
|
setState(() {
|
||||||
_currentPosition = currentTime;
|
_currentPosition = currentTime;
|
||||||
_totalDuration = duration > 0 ? duration : 1;
|
_totalDuration = duration > 0 ? duration : 1;
|
||||||
@@ -936,11 +937,11 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
_formatDuration(Duration(seconds: _currentPosition.toInt())),
|
_formatDuration(Duration(seconds: _currentPosition.isFinite ? _currentPosition.toInt() : 0)),
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 12),
|
style: const TextStyle(color: Colors.white, fontSize: 12),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
_formatDuration(Duration(seconds: _totalDuration.toInt())),
|
_formatDuration(Duration(seconds: _totalDuration.isFinite ? _totalDuration.toInt() : 0)),
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 12),
|
style: const TextStyle(color: Colors.white, fontSize: 12),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -140,6 +140,15 @@ class _MoviesTabState extends ConsumerState<MoviesTab> {
|
|||||||
return rating;
|
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) {
|
void _playMovie(Movie movie) {
|
||||||
ref.read(watchHistoryProvider.notifier).markMovieWatched(movie.streamId);
|
ref.read(watchHistoryProvider.notifier).markMovieWatched(movie.streamId);
|
||||||
|
|
||||||
@@ -175,7 +184,7 @@ class _MoviesTabState extends ConsumerState<MoviesTab> {
|
|||||||
final heroItems = displayMovies.take(5).map((m) => HeroItem(
|
final heroItems = displayMovies.take(5).map((m) => HeroItem(
|
||||||
id: m.streamId,
|
id: m.streamId,
|
||||||
title: m.name,
|
title: m.name,
|
||||||
imageUrl: m.streamIcon ?? '',
|
imageUrl: _getProxiedImageUrl(m.streamIcon),
|
||||||
subtitle: m.rating != null ? '${_formatRating(m.rating)} ★' : null,
|
subtitle: m.rating != null ? '${_formatRating(m.rating)} ★' : null,
|
||||||
onMoreInfo: () {
|
onMoreInfo: () {
|
||||||
_playMovie(m);
|
_playMovie(m);
|
||||||
@@ -291,7 +300,7 @@ class _MoviesTabState extends ConsumerState<MoviesTab> {
|
|||||||
|
|
||||||
return MediaCard(
|
return MediaCard(
|
||||||
title: movie.name,
|
title: movie.name,
|
||||||
imageUrl: movie.streamIcon,
|
imageUrl: _getProxiedImageUrl(movie.streamIcon),
|
||||||
|
|
||||||
subtitle: movie.rating != null ? '${_formatRating(movie.rating)} ★' : null,
|
subtitle: movie.rating != null ? '${_formatRating(movie.rating)} ★' : null,
|
||||||
rating: _formatRating(movie.rating),
|
rating: _formatRating(movie.rating),
|
||||||
|
|||||||
@@ -140,6 +140,15 @@ class _SeriesTabState extends ConsumerState<SeriesTab> {
|
|||||||
return rating;
|
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) {
|
void _openSeries(Series series) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
@@ -173,7 +182,7 @@ class _SeriesTabState extends ConsumerState<SeriesTab> {
|
|||||||
final heroItems = displaySeries.take(5).map((s) => HeroItem(
|
final heroItems = displaySeries.take(5).map((s) => HeroItem(
|
||||||
id: s.seriesId.toString(), // ID logic might differ for Series
|
id: s.seriesId.toString(), // ID logic might differ for Series
|
||||||
title: s.name,
|
title: s.name,
|
||||||
imageUrl: s.cover ?? '',
|
imageUrl: _getProxiedImageUrl(s.cover),
|
||||||
subtitle: s.rating != null ? '${_formatRating(s.rating)} ★' : null,
|
subtitle: s.rating != null ? '${_formatRating(s.rating)} ★' : null,
|
||||||
onMoreInfo: () => _openSeries(s),
|
onMoreInfo: () => _openSeries(s),
|
||||||
)).toList();
|
)).toList();
|
||||||
@@ -281,7 +290,7 @@ class _SeriesTabState extends ConsumerState<SeriesTab> {
|
|||||||
final serie = displaySeries[index];
|
final serie = displaySeries[index];
|
||||||
return MediaCard(
|
return MediaCard(
|
||||||
title: serie.name,
|
title: serie.name,
|
||||||
imageUrl: serie.cover,
|
imageUrl: _getProxiedImageUrl(serie.cover),
|
||||||
|
|
||||||
subtitle: serie.rating != null ? '${_formatRating(serie.rating)} ★' : null,
|
subtitle: serie.rating != null ? '${_formatRating(serie.rating)} ★' : null,
|
||||||
rating: _formatRating(serie.rating),
|
rating: _formatRating(serie.rating),
|
||||||
|
|||||||
@@ -125,6 +125,15 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab> {
|
|||||||
return rating;
|
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) {
|
void _playMovie(Movie movie) {
|
||||||
ref.read(watchHistoryProvider.notifier).markMovieWatched(movie.streamId);
|
ref.read(watchHistoryProvider.notifier).markMovieWatched(movie.streamId);
|
||||||
|
|
||||||
@@ -160,7 +169,7 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab> {
|
|||||||
final heroItems = _movies.take(3).map((m) => HeroItem(
|
final heroItems = _movies.take(3).map((m) => HeroItem(
|
||||||
id: m.streamId,
|
id: m.streamId,
|
||||||
title: m.name,
|
title: m.name,
|
||||||
imageUrl: m.streamIcon ?? '',
|
imageUrl: _getProxiedImageUrl(m.streamIcon),
|
||||||
subtitle: m.rating != null ? '${_formatRating(m.rating)} ★' : null,
|
subtitle: m.rating != null ? '${_formatRating(m.rating)} ★' : null,
|
||||||
onMoreInfo: () => _playMovie(m),
|
onMoreInfo: () => _playMovie(m),
|
||||||
)).toList();
|
)).toList();
|
||||||
@@ -251,7 +260,7 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab> {
|
|||||||
|
|
||||||
return MediaCard(
|
return MediaCard(
|
||||||
title: movie.name,
|
title: movie.name,
|
||||||
imageUrl: movie.streamIcon,
|
imageUrl: _getProxiedImageUrl(movie.streamIcon),
|
||||||
|
|
||||||
subtitle: movie.rating != null ? '${_formatRating(movie.rating)} ★' : null,
|
subtitle: movie.rating != null ? '${_formatRating(movie.rating)} ★' : null,
|
||||||
rating: _formatRating(movie.rating),
|
rating: _formatRating(movie.rating),
|
||||||
|
|||||||
@@ -124,6 +124,15 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab> {
|
|||||||
return rating;
|
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) {
|
void _openSeries(Series series) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
@@ -153,7 +162,7 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab> {
|
|||||||
final heroItems = _series.take(3).map((s) => HeroItem(
|
final heroItems = _series.take(3).map((s) => HeroItem(
|
||||||
id: s.seriesId.toString(),
|
id: s.seriesId.toString(),
|
||||||
title: s.name,
|
title: s.name,
|
||||||
imageUrl: s.cover ?? '',
|
imageUrl: _getProxiedImageUrl(s.cover),
|
||||||
subtitle: s.rating != null ? '${_formatRating(s.rating)} ★' : null,
|
subtitle: s.rating != null ? '${_formatRating(s.rating)} ★' : null,
|
||||||
onMoreInfo: () => _openSeries(s),
|
onMoreInfo: () => _openSeries(s),
|
||||||
)).toList();
|
)).toList();
|
||||||
@@ -251,7 +260,7 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab> {
|
|||||||
|
|
||||||
return MediaCard(
|
return MediaCard(
|
||||||
title: series.name,
|
title: series.name,
|
||||||
imageUrl: series.cover,
|
imageUrl: _getProxiedImageUrl(series.cover),
|
||||||
|
|
||||||
subtitle: series.rating != null ? '${_formatRating(series.rating)} ★' : null,
|
subtitle: series.rating != null ? '${_formatRating(series.rating)} ★' : null,
|
||||||
rating: _formatRating(series.rating),
|
rating: _formatRating(series.rating),
|
||||||
|
|||||||
+3
-3
@@ -144,7 +144,7 @@
|
|||||||
window.parent.postMessage({
|
window.parent.postMessage({
|
||||||
type: 'playback_position',
|
type: 'playback_position',
|
||||||
currentTime: video.currentTime,
|
currentTime: video.currentTime,
|
||||||
duration: video.duration || 0
|
duration: isFinite(video.duration) ? video.duration : 0
|
||||||
}, '*');
|
}, '*');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
window.parent.postMessage({
|
window.parent.postMessage({
|
||||||
type: 'playback_position',
|
type: 'playback_position',
|
||||||
currentTime: video.currentTime,
|
currentTime: video.currentTime,
|
||||||
duration: video.duration || 0
|
duration: isFinite(video.duration) ? video.duration : 0
|
||||||
}, '*');
|
}, '*');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -166,7 +166,7 @@
|
|||||||
video.addEventListener('ended', () => {
|
video.addEventListener('ended', () => {
|
||||||
window.parent.postMessage({
|
window.parent.postMessage({
|
||||||
type: 'playback_ended',
|
type: 'playback_ended',
|
||||||
duration: video.duration || 0
|
duration: isFinite(video.duration) ? video.duration : 0
|
||||||
}, '*');
|
}, '*');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user