mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-11 17:30:00 +02:00
feat: implement initial mobile IPTV feature with player screen, live TV, movies, and series tabs, along with the Xtream API service.
This commit is contained in:
1 parent
b1f887af69
commit
829c5751ca
6 files changed
+112
-80
No files matched your search
@@ -1,12 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../../features/iptv/screens/player_screen.dart';
|
||||
export '../../../../features/iptv/screens/player_screen.dart' show StreamType;
|
||||
import '../../../../core/models/playlist_config.dart';
|
||||
import '../../../../features/iptv/widgets/lite_player_view.dart';
|
||||
import '../../../../features/iptv/providers/xtream_provider.dart';
|
||||
|
||||
/// Mobile wrapper for the PlayerScreen.
|
||||
/// Currently reuses the desktop PlayerScreen as it adapts well to mobile (video player),
|
||||
/// but allows for future mobile-specific overlays or gesture controls.
|
||||
class MobilePlayerScreen extends StatelessWidget {
|
||||
class MobilePlayerScreen extends ConsumerWidget {
|
||||
final String streamId;
|
||||
final String title;
|
||||
final StreamType streamType;
|
||||
@@ -23,15 +23,35 @@ class MobilePlayerScreen extends StatelessWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// For now, we reuse the existing PlayerScreen which is responsive.
|
||||
// In the future, we can add Mobile-specific gesture detectors here.
|
||||
return PlayerScreen(
|
||||
streamId: streamId,
|
||||
title: title,
|
||||
playlist: playlist,
|
||||
streamType: streamType,
|
||||
containerExtension: containerExtension,
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final service = ref.read(xtreamServiceProvider(playlist));
|
||||
String streamUrl = '';
|
||||
|
||||
if (streamType == StreamType.live) {
|
||||
streamUrl = service.getLiveStreamUrl(streamId);
|
||||
} else if (streamType == StreamType.vod) {
|
||||
streamUrl = service.getVodStreamUrl(streamId, containerExtension);
|
||||
} else if (streamType == StreamType.series) {
|
||||
streamUrl = service.getSeriesStreamUrl(streamId, containerExtension);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
title: Text(title, style: const TextStyle(color: Colors.white)),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
body: Center(
|
||||
child: LitePlayerView(
|
||||
streamUrl: streamUrl,
|
||||
isLive: streamType == StreamType.live,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> {
|
||||
.where((cat) => settings.matchesLiveTvFilter(cat))
|
||||
.toList();
|
||||
}
|
||||
categories.sort();
|
||||
// REMOVED alphabetic sort to respect server/settings order
|
||||
|
||||
List<Channel> displayedChannels = [];
|
||||
if (_searchQuery.isNotEmpty) {
|
||||
@@ -70,6 +70,12 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> {
|
||||
if (_selectedCategory == null && categories.isNotEmpty) {
|
||||
_selectedCategory = categories.first;
|
||||
}
|
||||
// If the selected category is no longer available (due to filters), reset it
|
||||
if (_selectedCategory != null &&
|
||||
!categories.contains(_selectedCategory)) {
|
||||
_selectedCategory =
|
||||
categories.isNotEmpty ? categories.first : null;
|
||||
}
|
||||
if (_selectedCategory != null) {
|
||||
displayedChannels = groupedChannels[_selectedCategory] ?? [];
|
||||
}
|
||||
|
||||
@@ -129,10 +129,12 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab> {
|
||||
return rating;
|
||||
}
|
||||
|
||||
String _getProxiedImageUrl(String? originalUrl) {
|
||||
String _getProxiedImageUrl(String? originalUrl, WidgetRef ref) {
|
||||
if (originalUrl == null || originalUrl.isEmpty) return '';
|
||||
if (originalUrl.startsWith('http://')) {
|
||||
return '/api/xtream/$originalUrl';
|
||||
if (originalUrl.startsWith('http')) {
|
||||
final service = ref.read(xtreamServiceProvider(widget.playlist));
|
||||
// Re-use core _wrapWithProxy logic (via a new public method or direct construction)
|
||||
return '${service.backendBaseUrl}/api/xtream/$originalUrl';
|
||||
}
|
||||
return originalUrl;
|
||||
}
|
||||
@@ -176,7 +178,7 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab> {
|
||||
(m) => HeroItem(
|
||||
id: m.streamId,
|
||||
title: m.name,
|
||||
imageUrl: _getProxiedImageUrl(m.streamIcon),
|
||||
imageUrl: _getProxiedImageUrl(m.streamIcon, ref),
|
||||
subtitle: m.rating != null ? '${_formatRating(m.rating)} ★' : null,
|
||||
onMoreInfo: () => _playMovie(m),
|
||||
),
|
||||
@@ -296,7 +298,7 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab> {
|
||||
child: movie.streamIcon != null &&
|
||||
movie.streamIcon!.isNotEmpty
|
||||
? Image.network(
|
||||
_getProxiedImageUrl(movie.streamIcon),
|
||||
_getProxiedImageUrl(movie.streamIcon, ref),
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) =>
|
||||
Container(color: Colors.white10),
|
||||
|
||||
@@ -127,10 +127,11 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab> {
|
||||
return rating;
|
||||
}
|
||||
|
||||
String _getProxiedImageUrl(String? originalUrl) {
|
||||
String _getProxiedImageUrl(String? originalUrl, WidgetRef ref) {
|
||||
if (originalUrl == null || originalUrl.isEmpty) return '';
|
||||
if (originalUrl.startsWith('http://')) {
|
||||
return '/api/xtream/$originalUrl';
|
||||
if (originalUrl.startsWith('http')) {
|
||||
final service = ref.read(xtreamServiceProvider(widget.playlist));
|
||||
return '${service.backendBaseUrl}/api/xtream/$originalUrl';
|
||||
}
|
||||
return originalUrl;
|
||||
}
|
||||
@@ -168,7 +169,7 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab> {
|
||||
(s) => HeroItem(
|
||||
id: s.seriesId.toString(),
|
||||
title: s.name,
|
||||
imageUrl: _getProxiedImageUrl(s.cover),
|
||||
imageUrl: _getProxiedImageUrl(s.cover, ref),
|
||||
subtitle: s.rating != null ? '${_formatRating(s.rating)} ★' : null,
|
||||
onMoreInfo: () => _openSeries(s),
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user