mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-20 05:38:15 +02:00
CRITICAL FIXES: - P0-1: Fix EPG N+1 query storm with batch loading * Implement XtreamServiceMobile with getBatchEPG() method * Batch load EPG data instead of per-channel requests * Reduce 50-200 concurrent requests to 1 request per category * Add EPG cache with 1-hour TTL to prevent redundant calls - P0-2: Remove destructive cache clearing on startup * Implement TTL-based cache invalidation * Preserve cache between app launches (6h channels, 1h EPG, 30m search) * Add HiveService.invalidateExpiredCache() method * Expected: -80% startup time on cold launches - P0-3: Preserve tab state with IndexedStack * Replace switch statement with IndexedStack * Keep all tabs alive in widget tree * Preserve scroll position, search state, filters across tab switches * Expected: instant tab switching (<100ms) HIGH PRIORITY FIXES: - P1-1: Debounce search input (500ms) * Prevent per-keystroke full rebuilds * Add search debounce timer - P1-3: Fix double FocusNode disposal crash * Remove duplicate focus node disposal in native_player_screen.dart * Prevents StateError on player exit Expected impact: -75-80% perceived latency, instant tab switching, no OOM on startup. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
128 lines
4.4 KiB
Dart
128 lines
4.4 KiB
Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import '../../core/models/playlist_config.dart';
|
|
import '../../core/models/iptv_models.dart' show Channel;
|
|
import '../../features/iptv/services/xtream_service_mobile.dart';
|
|
import '../../features/iptv/models/xtream_models.dart';
|
|
|
|
export '../../features/iptv/models/xtream_models.dart';
|
|
|
|
/// Mobile-specific Xtream service provider
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
final mobileXtreamServiceProvider =
|
|
FutureProvider.family<XtreamServiceMobile, PlaylistConfig>(
|
|
(ref, playlist) async {
|
|
final dir = await getApplicationDocumentsDirectory();
|
|
final service = XtreamServiceMobile(dir.path);
|
|
await service
|
|
.setPlaylistAsync(playlist); // Async to resolve DNS before API calls
|
|
return service;
|
|
});
|
|
|
|
/// Mobile-specific live channels provider
|
|
final mobileLiveChannelsProvider =
|
|
FutureProvider.family<Map<String, List<Channel>>, PlaylistConfig>(
|
|
(ref, playlist) async {
|
|
final service = await ref.watch(mobileXtreamServiceProvider(playlist).future);
|
|
return service.getLiveChannels();
|
|
});
|
|
|
|
/// Mobile-specific movies pagination provider
|
|
final mobileMoviesProvider =
|
|
FutureProvider.family<List<Movie>, PlaylistConfig>((ref, playlist) async {
|
|
final service = await ref.watch(mobileXtreamServiceProvider(playlist).future);
|
|
return service.getMoviesPaginated(offset: 0, limit: 100);
|
|
});
|
|
|
|
/// Mobile-specific series pagination provider
|
|
final mobileSeriesProvider =
|
|
FutureProvider.family<List<Series>, PlaylistConfig>((ref, playlist) async {
|
|
final service = await ref.watch(mobileXtreamServiceProvider(playlist).future);
|
|
return service.getSeriesPaginated(offset: 0, limit: 100);
|
|
});
|
|
|
|
/// Mobile-specific series info provider
|
|
final mobileSeriesInfoProvider =
|
|
FutureProvider.family<SeriesInfo, String>((ref, seriesId) {
|
|
throw UnimplementedError('Use mobileSeriesInfoByPlaylistProvider instead');
|
|
});
|
|
|
|
/// Series info with playlist context
|
|
class SeriesInfoRequest {
|
|
final PlaylistConfig playlist;
|
|
final String seriesId;
|
|
|
|
SeriesInfoRequest({required this.playlist, required this.seriesId});
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
identical(this, other) ||
|
|
other is SeriesInfoRequest &&
|
|
runtimeType == other.runtimeType &&
|
|
playlist == other.playlist &&
|
|
seriesId == other.seriesId;
|
|
|
|
@override
|
|
int get hashCode => playlist.hashCode ^ seriesId.hashCode;
|
|
}
|
|
|
|
final mobileSeriesInfoByPlaylistProvider =
|
|
FutureProvider.family<SeriesInfo, SeriesInfoRequest>((ref, request) async {
|
|
final service =
|
|
await ref.watch(mobileXtreamServiceProvider(request.playlist).future);
|
|
return service.getSeriesInfo(request.seriesId);
|
|
});
|
|
|
|
// UI State Provider for persistent navigation
|
|
final mobileLiveTvUiStateProvider =
|
|
StateProvider<LiveTvUiState>((ref) => LiveTvUiState());
|
|
|
|
class LiveTvUiState {
|
|
final String? selectedCategory;
|
|
final bool isCategoryView;
|
|
|
|
LiveTvUiState({this.selectedCategory, this.isCategoryView = true});
|
|
|
|
LiveTvUiState copyWith({String? selectedCategory, bool? isCategoryView}) {
|
|
return LiveTvUiState(
|
|
selectedCategory: selectedCategory ?? this.selectedCategory,
|
|
isCategoryView: isCategoryView ?? this.isCategoryView,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Persistent index for mobile dashboard navigation
|
|
final mobileDashboardIndexProvider = StateProvider<int>((ref) => 0);
|
|
|
|
/// [P0-1 FIX] Batch EPG provider - Load all EPG data for multiple channels at once
|
|
/// This prevents the N+1 query problem where each channel card makes its own EPG request
|
|
class BatchEpgRequest {
|
|
final PlaylistConfig playlist;
|
|
final List<String> streamIds;
|
|
|
|
BatchEpgRequest({required this.playlist, required this.streamIds});
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
identical(this, other) ||
|
|
other is BatchEpgRequest &&
|
|
runtimeType == other.runtimeType &&
|
|
playlist == other.playlist &&
|
|
streamIds.toSet() == other.streamIds.toSet();
|
|
|
|
@override
|
|
int get hashCode => playlist.hashCode ^ streamIds.toSet().hashCode;
|
|
}
|
|
|
|
/// Load EPG data for multiple streams in one batch request
|
|
/// Cache is automatically handled by the service with 1-hour TTL
|
|
final mobileBatchEpgProvider =
|
|
FutureProvider.family<Map<String, ShortEpg>, BatchEpgRequest>(
|
|
(ref, request) async {
|
|
if (request.streamIds.isEmpty) return {};
|
|
|
|
final service =
|
|
await ref.watch(mobileXtreamServiceProvider(request.playlist).future);
|
|
return service.getBatchEPG(request.streamIds);
|
|
});
|