mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-11 17:29:26 +02:00
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:
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.
|
||||
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);
|
||||
|
||||
/// User-provided XMLTV URL (empty = not configured).
|
||||
@@ -200,7 +208,18 @@ class XtreamServiceMobile {
|
||||
|
||||
/// Get live channels for a category (with batch EPG support)
|
||||
/// 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 {
|
||||
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 {
|
||||
final queryParams = {
|
||||
'username': _username,
|
||||
@@ -463,7 +482,18 @@ class XtreamServiceMobile {
|
||||
}
|
||||
|
||||
/// Get live TV categories
|
||||
///
|
||||
/// Falls back to the last good list when the fetch fails.
|
||||
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 {
|
||||
if (kDebugMode) print('🔍 Loading live TV categories with 8s timeout...');
|
||||
|
||||
|
||||
@@ -27,7 +27,11 @@ class MobileLiveTVTab extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
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 TextEditingController _searchController = TextEditingController();
|
||||
final FocusNode _searchFocusNode = FocusNode();
|
||||
@@ -36,12 +40,16 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
|
||||
bool _isSearchEditing = false;
|
||||
bool _justReturnedFromPlayer =
|
||||
false; // Flag to prevent PopScope interception after player
|
||||
bool _isRefreshing = false;
|
||||
|
||||
Timer? _searchTimer;
|
||||
|
||||
@override
|
||||
void 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
|
||||
_searchTimer = null;
|
||||
_searchController.addListener(() {
|
||||
@@ -62,6 +70,7 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_scrollController.dispose();
|
||||
_searchController.dispose();
|
||||
_searchFocusNode.dispose();
|
||||
@@ -72,6 +81,39 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
|
||||
@override
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
@@ -210,15 +252,20 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
|
||||
|
||||
return SafeArea(
|
||||
bottom: false,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _refreshChannels,
|
||||
color: AppColors.primary,
|
||||
child: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
// Floating Header (Search + Navigation)
|
||||
SliverAppBar(
|
||||
floating: false,
|
||||
pinned: true,
|
||||
snap: false,
|
||||
backgroundColor: const Color(0xFF000000), // Opaque black
|
||||
backgroundColor:
|
||||
const Color(0xFF000000), // Opaque black
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
automaticallyImplyLeading: false,
|
||||
@@ -249,7 +296,8 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
height: 48,
|
||||
decoration: AppDecorations.searchBar(context),
|
||||
decoration:
|
||||
AppDecorations.searchBar(context),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12),
|
||||
child: Row(
|
||||
@@ -269,7 +317,8 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
|
||||
focusNode: _searchFocusNode,
|
||||
readOnly: !_isSearchEditing,
|
||||
style: TextStyle(
|
||||
color: AppDecorations.textPrimary(
|
||||
color:
|
||||
AppDecorations.textPrimary(
|
||||
context),
|
||||
),
|
||||
decoration: const InputDecoration(
|
||||
@@ -292,7 +341,8 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
|
||||
_searchController.clear(),
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
color: AppDecorations.textSecondary(
|
||||
color:
|
||||
AppDecorations.textSecondary(
|
||||
context),
|
||||
),
|
||||
),
|
||||
@@ -312,8 +362,8 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.arrow_back,
|
||||
color:
|
||||
AppDecorations.textPrimary(context),
|
||||
color: AppDecorations.textPrimary(
|
||||
context),
|
||||
),
|
||||
onPressed: () {
|
||||
if (_searchQuery.isNotEmpty) {
|
||||
@@ -341,17 +391,38 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
|
||||
? 'Favoris'
|
||||
: showGrid
|
||||
? 'Catégories'
|
||||
: uiState.selectedCategory ??
|
||||
: uiState
|
||||
.selectedCategory ??
|
||||
'Chaînes',
|
||||
style: TextStyle(
|
||||
color:
|
||||
AppDecorations.textPrimary(context),
|
||||
color: AppDecorations.textPrimary(
|
||||
context),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
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(
|
||||
icon: Icon(
|
||||
_showFavoritesOnly
|
||||
@@ -363,7 +434,8 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
|
||||
context),
|
||||
),
|
||||
onPressed: () => setState(() {
|
||||
_showFavoritesOnly = !_showFavoritesOnly;
|
||||
_showFavoritesOnly =
|
||||
!_showFavoritesOnly;
|
||||
if (_showFavoritesOnly) {
|
||||
uiNotifier.state = uiState.copyWith(
|
||||
isCategoryView: false,
|
||||
@@ -402,6 +474,7 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -225,6 +225,7 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context); // Required for AutomaticKeepAliveClientMixin
|
||||
ref.listen<int>(fullCatalogRefreshTickProvider, (_, __) => _refresh());
|
||||
final settings = ref.watch(mobileSettingsProvider);
|
||||
final watchHistory = ref.watch(mobileWatchHistoryProvider);
|
||||
|
||||
|
||||
@@ -216,6 +216,7 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
ref.listen<int>(fullCatalogRefreshTickProvider, (_, __) => _refresh());
|
||||
final settings = ref.watch(mobileSettingsProvider);
|
||||
|
||||
if (_categories.isEmpty && !_isLoading) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:google_fonts/google_fonts.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_xtream_providers.dart';
|
||||
import 'package:xtremobile/mobile/widgets/tv_focusable.dart';
|
||||
import 'package:xtremobile/core/theme/app_decorations.dart';
|
||||
import 'package:xtremobile/core/theme/app_theme.dart';
|
||||
@@ -106,7 +107,7 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Version 1.6.1',
|
||||
'Version 1.7.2',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 13,
|
||||
color: AppDecorations.textSecondary(context),
|
||||
@@ -179,8 +180,7 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
_buildSettingItem(
|
||||
icon: Icons.public,
|
||||
title: 'EPG communautaire',
|
||||
subtitle:
|
||||
'Repli quand le fournisseur ne renvoie aucun programme',
|
||||
subtitle: 'Repli quand le fournisseur ne renvoie aucun programme',
|
||||
value: settings.useCommunityEpg ? 'Activé' : 'Désactivé',
|
||||
onTap: () => ref
|
||||
.read(mobileSettingsProvider.notifier)
|
||||
@@ -197,9 +197,8 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
onTap: () => _showKeyboardDialog(
|
||||
'URL XMLTV',
|
||||
_xmltvController,
|
||||
(val) => ref
|
||||
.read(mobileSettingsProvider.notifier)
|
||||
.setXmltvUrl(val),
|
||||
(val) =>
|
||||
ref.read(mobileSettingsProvider.notifier).setXmltvUrl(val),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -254,8 +253,8 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
|
||||
_buildSettingItem(
|
||||
icon: _isRefreshingCache ? Icons.hourglass_empty : Icons.refresh,
|
||||
title: 'Actualiser le cache',
|
||||
subtitle: 'Recharge films, séries et EPG',
|
||||
title: 'Forcer l\'actualisation',
|
||||
subtitle: 'Recharge chaînes, films, séries et EPG',
|
||||
value: _isRefreshingCache ? '...' : 'Appuyer',
|
||||
onTap: _isRefreshingCache ? null : _refreshCache,
|
||||
),
|
||||
@@ -319,15 +318,15 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
setState(() => _isRefreshingCache = true);
|
||||
|
||||
try {
|
||||
// final dir = await getApplicationDocumentsDirectory();
|
||||
// final cacheStore = HiveCacheStore(dir.path);
|
||||
// await cacheStore.clean();
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
refreshFullCatalog(ref);
|
||||
// Brief pause so the tap visibly registers on a TV remote.
|
||||
await Future.delayed(const Duration(milliseconds: 600));
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
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,
|
||||
duration: Duration(seconds: 3),
|
||||
),
|
||||
@@ -336,7 +335,8 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Erreur: $e'), backgroundColor: AppColors.error),
|
||||
SnackBar(
|
||||
content: Text('Erreur: $e'), backgroundColor: AppColors.error),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
@@ -392,21 +392,25 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
children: segments.map((seg) {
|
||||
final (mode, icon, label) = seg;
|
||||
final selected = themeState.appThemeMode == mode;
|
||||
final accentColor = Theme.of(context).colorScheme.primary;
|
||||
final accentColor =
|
||||
Theme.of(context).colorScheme.primary;
|
||||
return Expanded(
|
||||
child: TVFocusable(
|
||||
onPressed: () => notifier.setThemeMode(mode),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||
margin:
|
||||
const EdgeInsets.symmetric(horizontal: 2),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 7,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: selected
|
||||
? accentColor
|
||||
: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
@@ -417,7 +421,8 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
size: 16,
|
||||
color: selected
|
||||
? AppColors.onSurface
|
||||
: AppDecorations.textSecondary(context),
|
||||
: AppDecorations.textSecondary(
|
||||
context),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
@@ -429,7 +434,8 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
: FontWeight.w500,
|
||||
color: selected
|
||||
? AppColors.onSurface
|
||||
: AppDecorations.textSecondary(context),
|
||||
: AppDecorations.textSecondary(
|
||||
context),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -483,7 +489,8 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
),
|
||||
child: Row(
|
||||
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),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -514,7 +521,10 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
vertical: 6,
|
||||
),
|
||||
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),
|
||||
),
|
||||
child: Text(
|
||||
@@ -562,7 +572,8 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
),
|
||||
child: Row(
|
||||
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),
|
||||
Expanded(
|
||||
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);
|
||||
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)
|
||||
final mobileLiveCategoriesProvider =
|
||||
FutureProvider.family<List<model.Category>, PlaylistConfig>(
|
||||
@@ -43,9 +77,9 @@ final mobileLiveCategoriesProvider =
|
||||
});
|
||||
|
||||
/// Mobile-specific live channels provider (loads channels for a specific category)
|
||||
final mobileLiveChannelsByCategoryProvider = FutureProvider.family<
|
||||
List<model.Channel>,
|
||||
(PlaylistConfig, String)>((ref, params) async {
|
||||
final mobileLiveChannelsByCategoryProvider =
|
||||
FutureProvider.family<List<model.Channel>, (PlaylistConfig, String)>(
|
||||
(ref, params) async {
|
||||
final (playlist, categoryId) = params;
|
||||
final service = await ref.watch(mobileXtreamServiceProvider(playlist).future);
|
||||
return service.getLiveChannels(categoryId);
|
||||
@@ -79,8 +113,9 @@ final mobileLiveChannelsProvider =
|
||||
|
||||
for (int batchNum = 0; batchNum < totalBatches; batchNum++) {
|
||||
int startIdx = batchNum * batchSize;
|
||||
int endIdx =
|
||||
(startIdx + batchSize < categories.length) ? startIdx + batchSize : categories.length;
|
||||
int endIdx = (startIdx + batchSize < categories.length)
|
||||
? startIdx + batchSize
|
||||
: categories.length;
|
||||
final batch = categories.sublist(startIdx, endIdx);
|
||||
|
||||
print(
|
||||
@@ -120,7 +155,8 @@ final mobileLiveChannelsProvider =
|
||||
|
||||
/// Mobile-specific movies provider
|
||||
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);
|
||||
return service.getMoviesByCategory(""); // Fetch all or default
|
||||
});
|
||||
@@ -134,8 +170,9 @@ final mobileSeriesCategoriesProvider =
|
||||
});
|
||||
|
||||
/// Mobile-specific series pagination provider
|
||||
final mobileSeriesProvider = FutureProvider.family<List<model.Series>,
|
||||
(PlaylistConfig, String?)>((ref, params) async {
|
||||
final mobileSeriesProvider =
|
||||
FutureProvider.family<List<model.Series>, (PlaylistConfig, String?)>(
|
||||
(ref, params) async {
|
||||
final (playlist, categoryId) = params;
|
||||
final service = await ref.watch(mobileXtreamServiceProvider(playlist).future);
|
||||
return service.getSeriesPaginated(categoryId: categoryId);
|
||||
@@ -161,7 +198,8 @@ class SeriesInfoRequest {
|
||||
}
|
||||
|
||||
final mobileSeriesInfoByPlaylistProvider =
|
||||
FutureProvider.family<model.SeriesInfo?, SeriesInfoRequest>((ref, request) async {
|
||||
FutureProvider.family<model.SeriesInfo?, SeriesInfoRequest>(
|
||||
(ref, request) async {
|
||||
final service =
|
||||
await ref.watch(mobileXtreamServiceProvider(request.playlist).future);
|
||||
return service.getSeriesInfo(request.seriesId);
|
||||
|
||||
Reference in new issue
Block a user