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:
Michael committed 2026-03-09 13:16:29 +01:00
1 parent b1f887af69
commit 829c5751ca
6 files changed
+112 -80

No files matched your search

@@ -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),
),