feat(live): refresh the channel list on demand and on resume

Live categories and channels were fetched once per app session and kept
in memory until the process died, so a TV box left running never picked
up provider changes.

- Live tab: pull-to-refresh plus a remote-friendly refresh button, and an
  automatic refetch when the app returns to the foreground after 4 hours.
- Settings: the "refresh cache" entry was a no-op delay; it now forces a
  reload of live channels, movies, series and EPG.
- The Xtream service keeps the last non-empty live lists and serves them
  when a refetch fails, so a refresh on a not-yet-connected network does
  not blank the screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5.5 committed 2026-09-26 22:21:56 +02:00
1 parent 9ce707a702
commit 60c7f4cece
6 files changed
+198 -43

No files matched your search

@@ -98,6 +98,14 @@ class XtreamServiceMobile {
/// changing any call site. /// changing any call site.
final Map<String, model.Channel> _channelIndex = {}; final Map<String, model.Channel> _channelIndex = {};
/// Last non-empty live lists, keyed by category id ('' = all channels).
///
/// The fetchers swallow errors and return `[]`, so a refresh attempted while
/// the network is down (typically right after a TV wakes from standby) would
/// otherwise replace a good list with an empty screen.
final Map<String, List<model.Channel>> _lastLiveChannels = {};
List<model.Category>? _lastLiveCategories;
late final XmltvService _xmltv = XmltvService(cacheDir); late final XmltvService _xmltv = XmltvService(cacheDir);
/// User-provided XMLTV URL (empty = not configured). /// User-provided XMLTV URL (empty = not configured).
@@ -200,7 +208,18 @@ class XtreamServiceMobile {
/// Get live channels for a category (with batch EPG support) /// Get live channels for a category (with batch EPG support)
/// If categoryId is empty, fetches ALL channels from all categories /// If categoryId is empty, fetches ALL channels from all categories
///
/// Falls back to the last good list for this category when the fetch fails.
Future<List<model.Channel>> getLiveChannels(String categoryId) async { Future<List<model.Channel>> getLiveChannels(String categoryId) async {
final channels = await _fetchLiveChannels(categoryId);
if (channels.isNotEmpty) {
_lastLiveChannels[categoryId] = channels;
return channels;
}
return _lastLiveChannels[categoryId] ?? channels;
}
Future<List<model.Channel>> _fetchLiveChannels(String categoryId) async {
try { try {
final queryParams = { final queryParams = {
'username': _username, 'username': _username,
@@ -463,7 +482,18 @@ class XtreamServiceMobile {
} }
/// Get live TV categories /// Get live TV categories
///
/// Falls back to the last good list when the fetch fails.
Future<List<model.Category>> getLiveCategories() async { Future<List<model.Category>> getLiveCategories() async {
final categories = await _fetchLiveCategories();
if (categories.isNotEmpty) {
_lastLiveCategories = categories;
return categories;
}
return _lastLiveCategories ?? categories;
}
Future<List<model.Category>> _fetchLiveCategories() async {
try { try {
if (kDebugMode) print('🔍 Loading live TV categories with 8s timeout...'); if (kDebugMode) print('🔍 Loading live TV categories with 8s timeout...');
@@ -27,7 +27,11 @@ class MobileLiveTVTab extends ConsumerStatefulWidget {
} }
class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
with AutomaticKeepAliveClientMixin { with AutomaticKeepAliveClientMixin, WidgetsBindingObserver {
/// Coming back to the foreground after this long refetches the channel list,
/// so an app left running for days on a TV box picks up provider changes.
static const _autoRefreshAfter = Duration(hours: 4);
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
final TextEditingController _searchController = TextEditingController(); final TextEditingController _searchController = TextEditingController();
final FocusNode _searchFocusNode = FocusNode(); final FocusNode _searchFocusNode = FocusNode();
@@ -36,12 +40,16 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
bool _isSearchEditing = false; bool _isSearchEditing = false;
bool _justReturnedFromPlayer = bool _justReturnedFromPlayer =
false; // Flag to prevent PopScope interception after player false; // Flag to prevent PopScope interception after player
bool _isRefreshing = false;
Timer? _searchTimer; Timer? _searchTimer;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this);
// Anchor the auto-refresh clock to this first load.
ref.read(liveCatalogRefreshedAtProvider);
// [P1-1 FIX] Debounce search input to prevent per-keystroke rebuilds // [P1-1 FIX] Debounce search input to prevent per-keystroke rebuilds
_searchTimer = null; _searchTimer = null;
_searchController.addListener(() { _searchController.addListener(() {
@@ -62,6 +70,7 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
@override @override
void dispose() { void dispose() {
WidgetsBinding.instance.removeObserver(this);
_scrollController.dispose(); _scrollController.dispose();
_searchController.dispose(); _searchController.dispose();
_searchFocusNode.dispose(); _searchFocusNode.dispose();
@@ -72,6 +81,39 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state != AppLifecycleState.resumed) return;
final lastRefresh = ref.read(liveCatalogRefreshedAtProvider);
if (DateTime.now().difference(lastRefresh) >= _autoRefreshAfter) {
_refreshChannels();
}
}
/// Refetch categories and the open category's channels. The current lists
/// stay on screen until the new ones arrive.
Future<void> _refreshChannels() async {
if (_isRefreshing) return;
setState(() => _isRefreshing = true);
refreshLiveCatalog(ref);
final categoryId = ref.read(mobileLiveTvUiStateProvider).selectedCategoryId;
try {
await Future.wait<Object>([
ref.read(mobileLiveCategoriesProvider(widget.playlist).future),
if (categoryId != null)
ref.read(
mobileLiveChannelsByCategoryProvider((widget.playlist, categoryId))
.future,
),
]);
} catch (_) {
// Errors surface through the providers' own error states.
} finally {
if (mounted) setState(() => _isRefreshing = false);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
@@ -210,15 +252,20 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
return SafeArea( return SafeArea(
bottom: false, bottom: false,
child: RefreshIndicator(
onRefresh: _refreshChannels,
color: AppColors.primary,
child: CustomScrollView( child: CustomScrollView(
controller: _scrollController, controller: _scrollController,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [ slivers: [
// Floating Header (Search + Navigation) // Floating Header (Search + Navigation)
SliverAppBar( SliverAppBar(
floating: false, floating: false,
pinned: true, pinned: true,
snap: false, snap: false,
backgroundColor: const Color(0xFF000000), // Opaque black backgroundColor:
const Color(0xFF000000), // Opaque black
elevation: 0, elevation: 0,
scrolledUnderElevation: 0, scrolledUnderElevation: 0,
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
@@ -249,7 +296,8 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
child: Container( child: Container(
height: 48, height: 48,
decoration: AppDecorations.searchBar(context), decoration:
AppDecorations.searchBar(context),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 12), horizontal: 12),
child: Row( child: Row(
@@ -269,7 +317,8 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
focusNode: _searchFocusNode, focusNode: _searchFocusNode,
readOnly: !_isSearchEditing, readOnly: !_isSearchEditing,
style: TextStyle( style: TextStyle(
color: AppDecorations.textPrimary( color:
AppDecorations.textPrimary(
context), context),
), ),
decoration: const InputDecoration( decoration: const InputDecoration(
@@ -292,7 +341,8 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
_searchController.clear(), _searchController.clear(),
child: Icon( child: Icon(
Icons.close, Icons.close,
color: AppDecorations.textSecondary( color:
AppDecorations.textSecondary(
context), context),
), ),
), ),
@@ -312,8 +362,8 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
IconButton( IconButton(
icon: Icon( icon: Icon(
Icons.arrow_back, Icons.arrow_back,
color: color: AppDecorations.textPrimary(
AppDecorations.textPrimary(context), context),
), ),
onPressed: () { onPressed: () {
if (_searchQuery.isNotEmpty) { if (_searchQuery.isNotEmpty) {
@@ -341,17 +391,38 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
? 'Favoris' ? 'Favoris'
: showGrid : showGrid
? 'Catégories' ? 'Catégories'
: uiState.selectedCategory ?? : uiState
.selectedCategory ??
'Chaînes', 'Chaînes',
style: TextStyle( style: TextStyle(
color: color: AppDecorations.textPrimary(
AppDecorations.textPrimary(context), context),
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 18, fontSize: 18,
), ),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
IconButton(
tooltip: 'Actualiser les chaînes',
icon: _isRefreshing
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: Icon(
Icons.refresh,
color:
AppDecorations.textSecondary(
context),
),
onPressed: _isRefreshing
? null
: _refreshChannels,
),
IconButton( IconButton(
icon: Icon( icon: Icon(
_showFavoritesOnly _showFavoritesOnly
@@ -363,7 +434,8 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
context), context),
), ),
onPressed: () => setState(() { onPressed: () => setState(() {
_showFavoritesOnly = !_showFavoritesOnly; _showFavoritesOnly =
!_showFavoritesOnly;
if (_showFavoritesOnly) { if (_showFavoritesOnly) {
uiNotifier.state = uiState.copyWith( uiNotifier.state = uiState.copyWith(
isCategoryView: false, isCategoryView: false,
@@ -402,6 +474,7 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
), ),
], ],
), ),
),
); );
}, },
), ),
@@ -225,6 +225,7 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); // Required for AutomaticKeepAliveClientMixin super.build(context); // Required for AutomaticKeepAliveClientMixin
ref.listen<int>(fullCatalogRefreshTickProvider, (_, __) => _refresh());
final settings = ref.watch(mobileSettingsProvider); final settings = ref.watch(mobileSettingsProvider);
final watchHistory = ref.watch(mobileWatchHistoryProvider); final watchHistory = ref.watch(mobileWatchHistoryProvider);
@@ -216,6 +216,7 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
ref.listen<int>(fullCatalogRefreshTickProvider, (_, __) => _refresh());
final settings = ref.watch(mobileSettingsProvider); final settings = ref.watch(mobileSettingsProvider);
if (_categories.isEmpty && !_isLoading) { if (_categories.isEmpty && !_isLoading) {
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
// import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_store.dart'; // import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_store.dart';
import 'package:xtremobile/mobile/providers/mobile_settings_providers.dart'; import 'package:xtremobile/mobile/providers/mobile_settings_providers.dart';
import 'package:xtremobile/mobile/providers/mobile_xtream_providers.dart';
import 'package:xtremobile/mobile/widgets/tv_focusable.dart'; import 'package:xtremobile/mobile/widgets/tv_focusable.dart';
import 'package:xtremobile/core/theme/app_decorations.dart'; import 'package:xtremobile/core/theme/app_decorations.dart';
import 'package:xtremobile/core/theme/app_theme.dart'; import 'package:xtremobile/core/theme/app_theme.dart';
@@ -106,7 +107,7 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
), ),
), ),
Text( Text(
'Version 1.6.1', 'Version 1.7.2',
style: GoogleFonts.inter( style: GoogleFonts.inter(
fontSize: 13, fontSize: 13,
color: AppDecorations.textSecondary(context), color: AppDecorations.textSecondary(context),
@@ -179,8 +180,7 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
_buildSettingItem( _buildSettingItem(
icon: Icons.public, icon: Icons.public,
title: 'EPG communautaire', title: 'EPG communautaire',
subtitle: subtitle: 'Repli quand le fournisseur ne renvoie aucun programme',
'Repli quand le fournisseur ne renvoie aucun programme',
value: settings.useCommunityEpg ? 'Activé' : 'Désactivé', value: settings.useCommunityEpg ? 'Activé' : 'Désactivé',
onTap: () => ref onTap: () => ref
.read(mobileSettingsProvider.notifier) .read(mobileSettingsProvider.notifier)
@@ -197,9 +197,8 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
onTap: () => _showKeyboardDialog( onTap: () => _showKeyboardDialog(
'URL XMLTV', 'URL XMLTV',
_xmltvController, _xmltvController,
(val) => ref (val) =>
.read(mobileSettingsProvider.notifier) ref.read(mobileSettingsProvider.notifier).setXmltvUrl(val),
.setXmltvUrl(val),
), ),
), ),
@@ -254,8 +253,8 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
_buildSettingItem( _buildSettingItem(
icon: _isRefreshingCache ? Icons.hourglass_empty : Icons.refresh, icon: _isRefreshingCache ? Icons.hourglass_empty : Icons.refresh,
title: 'Actualiser le cache', title: 'Forcer l\'actualisation',
subtitle: 'Recharge films, séries et EPG', subtitle: 'Recharge chaînes, films, séries et EPG',
value: _isRefreshingCache ? '...' : 'Appuyer', value: _isRefreshingCache ? '...' : 'Appuyer',
onTap: _isRefreshingCache ? null : _refreshCache, onTap: _isRefreshingCache ? null : _refreshCache,
), ),
@@ -319,15 +318,15 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
setState(() => _isRefreshingCache = true); setState(() => _isRefreshingCache = true);
try { try {
// final dir = await getApplicationDocumentsDirectory(); refreshFullCatalog(ref);
// final cacheStore = HiveCacheStore(dir.path); // Brief pause so the tap visibly registers on a TV remote.
// await cacheStore.clean(); await Future.delayed(const Duration(milliseconds: 600));
await Future.delayed(const Duration(seconds: 1));
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text('Cache actualisé ! Relancez l\'app pour recharger.'), content:
Text('Chaînes, films, séries et EPG en cours de rechargement'),
backgroundColor: AppColors.success, backgroundColor: AppColors.success,
duration: Duration(seconds: 3), duration: Duration(seconds: 3),
), ),
@@ -336,7 +335,8 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur: $e'), backgroundColor: AppColors.error), SnackBar(
content: Text('Erreur: $e'), backgroundColor: AppColors.error),
); );
} }
} finally { } finally {
@@ -392,21 +392,25 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
children: segments.map((seg) { children: segments.map((seg) {
final (mode, icon, label) = seg; final (mode, icon, label) = seg;
final selected = themeState.appThemeMode == mode; final selected = themeState.appThemeMode == mode;
final accentColor = Theme.of(context).colorScheme.primary; final accentColor =
Theme.of(context).colorScheme.primary;
return Expanded( return Expanded(
child: TVFocusable( child: TVFocusable(
onPressed: () => notifier.setThemeMode(mode), onPressed: () => notifier.setThemeMode(mode),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: AnimatedContainer( child: AnimatedContainer(
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.symmetric(horizontal: 2), margin:
const EdgeInsets.symmetric(horizontal: 2),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 7, vertical: 7,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: selected color: selected
? accentColor ? accentColor
: Theme.of(context).colorScheme.surfaceContainerHighest, : Theme.of(context)
.colorScheme
.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Column( child: Column(
@@ -417,7 +421,8 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
size: 16, size: 16,
color: selected color: selected
? AppColors.onSurface ? AppColors.onSurface
: AppDecorations.textSecondary(context), : AppDecorations.textSecondary(
context),
), ),
const SizedBox(height: 3), const SizedBox(height: 3),
Text( Text(
@@ -429,7 +434,8 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
: FontWeight.w500, : FontWeight.w500,
color: selected color: selected
? AppColors.onSurface ? AppColors.onSurface
: AppDecorations.textSecondary(context), : AppDecorations.textSecondary(
context),
), ),
), ),
], ],
@@ -483,7 +489,8 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
), ),
child: Row( child: Row(
children: [ children: [
Icon(icon, color: Theme.of(context).colorScheme.primary, size: 24), Icon(icon,
color: Theme.of(context).colorScheme.primary, size: 24),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(
child: Column( child: Column(
@@ -514,7 +521,10 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
vertical: 6, vertical: 6,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.15), color: Theme.of(context)
.colorScheme
.primary
.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Text( child: Text(
@@ -562,7 +572,8 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
), ),
child: Row( child: Row(
children: [ children: [
Icon(icon, color: Theme.of(context).colorScheme.primary, size: 24), Icon(icon,
color: Theme.of(context).colorScheme.primary, size: 24),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(
child: Column( child: Column(
@@ -592,7 +603,8 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
], ],
), ),
), ),
Icon(Icons.edit, color: AppDecorations.textSecondary(context), size: 20), Icon(Icons.edit,
color: AppDecorations.textSecondary(context), size: 20),
], ],
), ),
), ),
@@ -30,10 +30,44 @@ final mobileXtreamServiceProvider =
} }
}); });
// A full catalogue refresh from the settings also drops the cached EPG.
ref.listen<int>(
fullCatalogRefreshTickProvider, (_, __) => service.clearCache());
ref.onDispose(service.dispose); ref.onDispose(service.dispose);
return service; return service;
}); });
/// When the live catalogue was last fetched or explicitly refreshed.
/// Drives the auto-refresh when the app comes back to the foreground.
final liveCatalogRefreshedAtProvider =
StateProvider<DateTime>((ref) => DateTime.now());
/// Bumped by the settings "force refresh". The movies and series tabs, which
/// hold their lists in local state, listen to it to reload.
final fullCatalogRefreshTickProvider = StateProvider<int>((ref) => 0);
/// Forces the live categories and channels to be refetched from the panel.
///
/// The Xtream service is kept: it falls back to its last good lists if the
/// refetch fails, so a flaky network never empties the screen.
void refreshLiveCatalog(WidgetRef ref) {
ref.invalidate(mobileLiveCategoriesProvider);
ref.invalidate(mobileLiveChannelsByCategoryProvider);
ref.invalidate(mobileLiveChannelsProvider);
ref.read(liveCatalogRefreshedAtProvider.notifier).state = DateTime.now();
}
/// Forces the whole catalogue (live, movies, series) and the EPG to reload.
void refreshFullCatalog(WidgetRef ref) {
refreshLiveCatalog(ref);
ref.invalidate(mobileMoviesProvider);
ref.invalidate(mobileSeriesCategoriesProvider);
ref.invalidate(mobileSeriesProvider);
ref.invalidate(mobileSeriesInfoByPlaylistProvider);
ref.read(fullCatalogRefreshTickProvider.notifier).state++;
}
/// Mobile-specific live categories provider (for Live TV category grid) /// Mobile-specific live categories provider (for Live TV category grid)
final mobileLiveCategoriesProvider = final mobileLiveCategoriesProvider =
FutureProvider.family<List<model.Category>, PlaylistConfig>( FutureProvider.family<List<model.Category>, PlaylistConfig>(
@@ -43,9 +77,9 @@ final mobileLiveCategoriesProvider =
}); });
/// Mobile-specific live channels provider (loads channels for a specific category) /// Mobile-specific live channels provider (loads channels for a specific category)
final mobileLiveChannelsByCategoryProvider = FutureProvider.family< final mobileLiveChannelsByCategoryProvider =
List<model.Channel>, FutureProvider.family<List<model.Channel>, (PlaylistConfig, String)>(
(PlaylistConfig, String)>((ref, params) async { (ref, params) async {
final (playlist, categoryId) = params; final (playlist, categoryId) = params;
final service = await ref.watch(mobileXtreamServiceProvider(playlist).future); final service = await ref.watch(mobileXtreamServiceProvider(playlist).future);
return service.getLiveChannels(categoryId); return service.getLiveChannels(categoryId);
@@ -79,8 +113,9 @@ final mobileLiveChannelsProvider =
for (int batchNum = 0; batchNum < totalBatches; batchNum++) { for (int batchNum = 0; batchNum < totalBatches; batchNum++) {
int startIdx = batchNum * batchSize; int startIdx = batchNum * batchSize;
int endIdx = int endIdx = (startIdx + batchSize < categories.length)
(startIdx + batchSize < categories.length) ? startIdx + batchSize : categories.length; ? startIdx + batchSize
: categories.length;
final batch = categories.sublist(startIdx, endIdx); final batch = categories.sublist(startIdx, endIdx);
print( print(
@@ -120,7 +155,8 @@ final mobileLiveChannelsProvider =
/// Mobile-specific movies provider /// Mobile-specific movies provider
final mobileMoviesProvider = final mobileMoviesProvider =
FutureProvider.family<List<model.VodItem>, PlaylistConfig>((ref, playlist) async { FutureProvider.family<List<model.VodItem>, PlaylistConfig>(
(ref, playlist) async {
final service = await ref.watch(mobileXtreamServiceProvider(playlist).future); final service = await ref.watch(mobileXtreamServiceProvider(playlist).future);
return service.getMoviesByCategory(""); // Fetch all or default return service.getMoviesByCategory(""); // Fetch all or default
}); });
@@ -134,8 +170,9 @@ final mobileSeriesCategoriesProvider =
}); });
/// Mobile-specific series pagination provider /// Mobile-specific series pagination provider
final mobileSeriesProvider = FutureProvider.family<List<model.Series>, final mobileSeriesProvider =
(PlaylistConfig, String?)>((ref, params) async { FutureProvider.family<List<model.Series>, (PlaylistConfig, String?)>(
(ref, params) async {
final (playlist, categoryId) = params; final (playlist, categoryId) = params;
final service = await ref.watch(mobileXtreamServiceProvider(playlist).future); final service = await ref.watch(mobileXtreamServiceProvider(playlist).future);
return service.getSeriesPaginated(categoryId: categoryId); return service.getSeriesPaginated(categoryId: categoryId);
@@ -161,7 +198,8 @@ class SeriesInfoRequest {
} }
final mobileSeriesInfoByPlaylistProvider = final mobileSeriesInfoByPlaylistProvider =
FutureProvider.family<model.SeriesInfo?, SeriesInfoRequest>((ref, request) async { FutureProvider.family<model.SeriesInfo?, SeriesInfoRequest>(
(ref, request) async {
final service = final service =
await ref.watch(mobileXtreamServiceProvider(request.playlist).future); await ref.watch(mobileXtreamServiceProvider(request.playlist).future);
return service.getSeriesInfo(request.seriesId); return service.getSeriesInfo(request.seriesId);