diff --git a/lib/core/api/dns_service.dart b/lib/core/api/dns_service.dart index 91ca9ef..0f8c9e6 100644 --- a/lib/core/api/dns_service.dart +++ b/lib/core/api/dns_service.dart @@ -9,6 +9,7 @@ /// - Result: Same hostname resolved twice, wasting network calls /// /// Solution: Single unified service with shared cache +library; import 'dart:convert'; import 'dart:io'; @@ -166,6 +167,5 @@ class _DnsCacheEntry { _DnsCacheEntry(this.ip) : timestamp = DateTime.now(); - bool get isExpired => - DateTime.now().difference(timestamp).inSeconds > 3600; + bool get isExpired => DateTime.now().difference(timestamp).inSeconds > 3600; } diff --git a/lib/core/patterns/stream_resolver_pattern.dart b/lib/core/patterns/stream_resolver_pattern.dart index 42c0e59..afd1578 100644 --- a/lib/core/patterns/stream_resolver_pattern.dart +++ b/lib/core/patterns/stream_resolver_pattern.dart @@ -35,10 +35,14 @@ class StreamResolverPattern { } // Parallel resolution attempts - final results = await Future.wait([ - _validateUrl(streamUrl).timeout(timeout, onTimeout: () => false), - _performDnsResolution(streamUrl).timeout(timeout, onTimeout: () => false), - ], eagerError: false).catchError((_) => [false, false]); + final results = await Future.wait( + [ + _validateUrl(streamUrl).timeout(timeout, onTimeout: () => false), + _performDnsResolution(streamUrl) + .timeout(timeout, onTimeout: () => false), + ], + eagerError: false, + ).catchError((_) => [false, false]); final isValid = results[0]; final dnsResolved = results[1]; diff --git a/lib/core/patterns/video_player_wrapper.dart b/lib/core/patterns/video_player_wrapper.dart index ba4d048..1d9b866 100644 --- a/lib/core/patterns/video_player_wrapper.dart +++ b/lib/core/patterns/video_player_wrapper.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:media_kit/media_kit.dart'; -import 'package:media_kit_video/media_kit_video.dart'; /// Video player selection and wrapper pattern /// Abstracts between MediaKit and other video players diff --git a/lib/core/utils/device_info.dart b/lib/core/utils/device_info.dart index c50eac3..8f0ef37 100644 --- a/lib/core/utils/device_info.dart +++ b/lib/core/utils/device_info.dart @@ -2,6 +2,7 @@ /// /// Detects device RAM and adjusts player buffer size accordingly /// to prevent OOM crashes on low-end devices +library; import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; diff --git a/lib/core/utils/hive_encryption_benchmark.dart b/lib/core/utils/hive_encryption_benchmark.dart index 67b842b..c7de9c0 100644 --- a/lib/core/utils/hive_encryption_benchmark.dart +++ b/lib/core/utils/hive_encryption_benchmark.dart @@ -13,7 +13,7 @@ class HiveEncryptionBenchmark { String valueSize = 'medium', // small, medium, large }) async { if (kDebugMode) { - print('\n' + ('=' * 60)); + print('\n${'=' * 60}'); print('🔐 HIVE ENCRYPTION BENCHMARK'); print('=' * 60); print('Write Operations: $writeCount'); @@ -42,7 +42,10 @@ class HiveEncryptionBenchmark { } static Future _benchmarkUnencrypted( - int writes, int reads, String testValue) async { + int writes, + int reads, + String testValue, + ) async { const boxName = 'benchmark_unencrypted'; try { // Clean up if exists @@ -62,7 +65,7 @@ class HiveEncryptionBenchmark { // Read benchmark final readStop = Stopwatch()..start(); for (int i = 0; i < reads; i++) { - await box.get('key_${i % writes}'); + box.get('key_${i % writes}'); } readStop.stop(); @@ -93,7 +96,10 @@ class HiveEncryptionBenchmark { } static Future _benchmarkEncrypted( - int writes, int reads, String testValue) async { + int writes, + int reads, + String testValue, + ) async { const boxName = 'benchmark_encrypted'; try { // Clean up if exists @@ -118,7 +124,7 @@ class HiveEncryptionBenchmark { // Read benchmark final readStop = Stopwatch()..start(); for (int i = 0; i < reads; i++) { - await box.get('key_${i % writes}'); + box.get('key_${i % writes}'); } readStop.stop(); @@ -157,21 +163,25 @@ class HiveEncryptionBenchmark { final readOverhead = ((enc.readTimeMs / unenc.readTimeMs - 1) * 100); if (kDebugMode) { - print('\n' + ('=' * 60)); + print('\n${'=' * 60}'); print('📊 PERFORMANCE COMPARISON'); print('=' * 60); print('\nWrite Performance:'); print( - ' Unencrypted: ${unenc.writeTimeMs}ms (${(unenc.writeTimeMs / unenc.operationCount).toStringAsFixed(2)}µs/op)'); + ' Unencrypted: ${unenc.writeTimeMs}ms (${(unenc.writeTimeMs / unenc.operationCount).toStringAsFixed(2)}µs/op)', + ); print( - ' Encrypted: ${enc.writeTimeMs}ms (${(enc.writeTimeMs / enc.operationCount).toStringAsFixed(2)}µs/op)'); + ' Encrypted: ${enc.writeTimeMs}ms (${(enc.writeTimeMs / enc.operationCount).toStringAsFixed(2)}µs/op)', + ); print(' Overhead: ${writeOverhead.toStringAsFixed(1)}%'); print('\nRead Performance:'); print( - ' Unencrypted: ${unenc.readTimeMs}ms (${(unenc.readTimeMs / unenc.operationCount).toStringAsFixed(2)}µs/op)'); + ' Unencrypted: ${unenc.readTimeMs}ms (${(unenc.readTimeMs / unenc.operationCount).toStringAsFixed(2)}µs/op)', + ); print( - ' Encrypted: ${enc.readTimeMs}ms (${(enc.readTimeMs / enc.operationCount).toStringAsFixed(2)}µs/op)'); + ' Encrypted: ${enc.readTimeMs}ms (${(enc.readTimeMs / enc.operationCount).toStringAsFixed(2)}µs/op)', + ); print(' Overhead: ${readOverhead.toStringAsFixed(1)}%'); // Status @@ -180,8 +190,10 @@ class HiveEncryptionBenchmark { final readStatus = readOverhead > acceptableThreshold ? '⚠️' : '✅'; print('\nStatus:'); - print(' $writeStatus Write overhead: ${writeOverhead.toStringAsFixed(1)}% (target: <$acceptableThreshold%)'); - print(' $readStatus Read overhead: ${readOverhead.toStringAsFixed(1)}% (target: <$acceptableThreshold%)'); + print( + ' $writeStatus Write overhead: ${writeOverhead.toStringAsFixed(1)}% (target: <$acceptableThreshold%)'); + print( + ' $readStatus Read overhead: ${readOverhead.toStringAsFixed(1)}% (target: <$acceptableThreshold%)'); print('=' * 60 + '\n'); } } @@ -229,7 +241,8 @@ class EncryptionBenchmarkResult { void print() { print(' Name: $name'); - print(' Write: ${writeTimeMs}ms (${writePerOpUs.toStringAsFixed(2)}µs/op)'); + print( + ' Write: ${writeTimeMs}ms (${writePerOpUs.toStringAsFixed(2)}µs/op)'); print(' Read: ${readTimeMs}ms (${readPerOpUs.toStringAsFixed(2)}µs/op)'); print(' Delete: ${deleteTimeMs}ms'); } diff --git a/lib/core/utils/image_cache_config.dart b/lib/core/utils/image_cache_config.dart index d83e80f..9e4bbef 100644 --- a/lib/core/utils/image_cache_config.dart +++ b/lib/core/utils/image_cache_config.dart @@ -5,6 +5,7 @@ /// but displays them at 40-100px sizes, wasting memory and disk space. /// /// Solution: Specify memCacheWidth/Height for display size, limiting disk cache. +library; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; @@ -29,7 +30,7 @@ class ImageCacheConfigs { static const ImageCacheConfig channelIcon = ImageCacheConfig( displayWidth: 40, displayHeight: 40, - cacheWidth: 50, // Slight buffer for upscaling + cacheWidth: 50, // Slight buffer for upscaling cacheHeight: 50, ); diff --git a/lib/core/utils/memory_profiler.dart b/lib/core/utils/memory_profiler.dart index fb4674d..2ea71c8 100644 --- a/lib/core/utils/memory_profiler.dart +++ b/lib/core/utils/memory_profiler.dart @@ -35,7 +35,8 @@ class MemoryProfiler { count++; }); if (kDebugMode) { - print('⏰ PERIODIC MEMORY MONITORING STARTED (${interval.inSeconds}s interval)'); + print( + '⏰ PERIODIC MEMORY MONITORING STARTED (${interval.inSeconds}s interval)'); } } @@ -50,9 +51,10 @@ class MemoryProfiler { /// Analyze memory growth between snapshots static MemoryAnalysis analyzeGrowth(String startLabel, String endLabel) { - final start = - _snapshots.firstWhere((s) => s.label == startLabel, orElse: () => _snapshots.first); - final end = _snapshots.firstWhere((s) => s.label == endLabel, orElse: () => _snapshots.last); + final start = _snapshots.firstWhere((s) => s.label == startLabel, + orElse: () => _snapshots.first); + final end = _snapshots.firstWhere((s) => s.label == endLabel, + orElse: () => _snapshots.last); return MemoryAnalysis( startLabel: startLabel, @@ -65,7 +67,7 @@ class MemoryProfiler { /// Generate report static void printReport() { if (kDebugMode) { - print('\n' + ('=' * 60)); + print('\n${'=' * 60}'); print('📊 MEMORY PROFILING REPORT'); print('=' * 60); for (int i = 0; i < _snapshots.length; i++) { @@ -75,19 +77,20 @@ class MemoryProfiler { // Show growth analysis if (_snapshots.length >= 2) { - print('\n' + ('-' * 60)); + print('\n${'-' * 60}'); print('📈 GROWTH ANALYSIS'); print('-' * 60); for (int i = 0; i < _snapshots.length - 1; i++) { final snapshot = _snapshots[i]; final nextSnapshot = _snapshots[i + 1]; - final growth = nextSnapshot.description.compareTo(snapshot.description); + final growth = + nextSnapshot.description.compareTo(snapshot.description); print(' ${snapshot.label} → ${nextSnapshot.label}: $growth'); } } - print('\n' + ('=' * 60) + '\n'); + print('\n${'=' * 60}\n'); } } diff --git a/lib/core/utils/startup_profiler.dart b/lib/core/utils/startup_profiler.dart index 068ec32..2ccaefb 100644 --- a/lib/core/utils/startup_profiler.dart +++ b/lib/core/utils/startup_profiler.dart @@ -35,7 +35,7 @@ class StartupProfiler { /// Report all metrics static Future reportAll() async { if (kDebugMode) { - print('\n' + ('=' * 50)); + print('\n${'=' * 50}'); print('⏱️ STARTUP METRICS'); print('=' * 50); _timings.forEach((key, watch) { diff --git a/lib/core/utils/video_profiler.dart b/lib/core/utils/video_profiler.dart index 2146fdf..5655f2a 100644 --- a/lib/core/utils/video_profiler.dart +++ b/lib/core/utils/video_profiler.dart @@ -32,8 +32,7 @@ class VideoProfiler { static void markFirstFrame(String streamName) { final m = metrics[streamName]; if (m != null) { - m.firstFrameTime = - DateTime.now().difference(m.loadStart).inMilliseconds; + m.firstFrameTime = DateTime.now().difference(m.loadStart).inMilliseconds; if (kDebugMode) { print('🎬 FIRST FRAME: ${m.firstFrameTime}ms'); } @@ -78,10 +77,9 @@ class VideoProfiler { final m = metrics[streamName]; if (m != null) { m.endTime = DateTime.now(); - m.totalDurationMs = - m.endTime!.difference(m.startTime).inMilliseconds; + m.totalDurationMs = m.endTime!.difference(m.startTime).inMilliseconds; if (kDebugMode) { - print('\n' + ('=' * 50)); + print('\n${'=' * 50}'); print('📊 VIDEO METRICS: $streamName'); print('=' * 50); m.printMetrics(); @@ -93,7 +91,7 @@ class VideoProfiler { /// Get summary of all metrics static void printSummary() { if (kDebugMode) { - print('\n' + ('=' * 50)); + print('\n${'=' * 50}'); print('📈 VIDEO PROFILING SUMMARY'); print('=' * 50); metrics.forEach((key, m) { diff --git a/lib/features/iptv/screens/lite_player_screen.dart b/lib/features/iptv/screens/lite_player_screen.dart index 0c013bb..9e800d9 100644 --- a/lib/features/iptv/screens/lite_player_screen.dart +++ b/lib/features/iptv/screens/lite_player_screen.dart @@ -147,7 +147,8 @@ class _LitePlayerScreenState extends ConsumerState } Future _updateEPG() async { - if (widget.streamType != model.StreamType.live || _xtreamService == null) return; + if (widget.streamType != model.StreamType.live || _xtreamService == null) + return; try { final currentChannelId = widget.channels?[_currentIndex].streamId ?? widget.streamId; @@ -198,10 +199,12 @@ class _LitePlayerScreenState extends ConsumerState ); _controller = controller; - await controller.initialize().timeout(const Duration(seconds: 15), - onTimeout: () { - throw Exception('Connection timed out'); - }); + await controller.initialize().timeout( + const Duration(seconds: 15), + onTimeout: () { + throw Exception('Connection timed out'); + }, + ); if (!mounted) { await controller.dispose(); @@ -221,11 +224,12 @@ class _LitePlayerScreenState extends ConsumerState _resetControlsTimer(); controller.addListener(_videoListener); } catch (e) { - if (mounted && currentLoadId == _loadId) + if (mounted && currentLoadId == _loadId) { setState(() { _isLoading = false; _errorMessage = "Playback Error: $e"; }); + } } } @@ -233,8 +237,9 @@ class _LitePlayerScreenState extends ConsumerState if (_controller == null || !mounted) return; final value = _controller!.value; - if (value.isPlaying != _isPlaying) + if (value.isPlaying != _isPlaying) { setState(() => _isPlaying = value.isPlaying); + } // Stabilization Masking: spinner stays until 1.5s of smooth play // Fix: If playing, ignore isBuffering to prevent infinite spinner on some TS streams @@ -302,16 +307,18 @@ class _LitePlayerScreenState extends ConsumerState final key = event.logicalKey; if (key == LogicalKeyboardKey.channelUp) { - if (widget.channels != null) + if (widget.channels != null) { _switchChannel((_currentIndex + 1) % widget.channels!.length); + } return true; } if (key == LogicalKeyboardKey.channelDown) { - if (widget.channels != null) + if (widget.channels != null) { _switchChannel( (_currentIndex - 1 + widget.channels!.length) % widget.channels!.length, ); + } return true; } @@ -393,7 +400,7 @@ class _LitePlayerScreenState extends ConsumerState borderRadius: BorderRadius.circular(50), child: Container( padding: const EdgeInsets.all(8), - decoration: BoxDecoration( + decoration: const BoxDecoration( color: Colors.black45, shape: BoxShape.circle, ), @@ -408,7 +415,9 @@ class _LitePlayerScreenState extends ConsumerState if (settings.showClock) Container( padding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 6), + horizontal: 12, + vertical: 6, + ), decoration: BoxDecoration( color: Colors.black45, borderRadius: BorderRadius.circular(20), @@ -460,9 +469,10 @@ class _LitePlayerScreenState extends ConsumerState Widget _buildAspectRatioWrapper() { final mode = ref.read(mobileSettingsProvider).aspectRatioMode; - if (mode == 'fill') + if (mode == 'fill') { return SizedBox.expand(child: VideoPlayer(_controller!)); - if (mode == 'cover') + } + if (mode == 'cover') { return SizedBox.expand( child: FittedBox( fit: BoxFit.cover, @@ -473,6 +483,7 @@ class _LitePlayerScreenState extends ConsumerState ), ), ); + } return AspectRatio( aspectRatio: _controller!.value.aspectRatio, child: VideoPlayer(_controller!), diff --git a/lib/features/iptv/screens/native_player_screen.dart b/lib/features/iptv/screens/native_player_screen.dart index 3e59cff..6f3acd4 100644 --- a/lib/features/iptv/screens/native_player_screen.dart +++ b/lib/features/iptv/screens/native_player_screen.dart @@ -16,7 +16,7 @@ import 'package:xtremobile/features/iptv/screens/lite_player_screen.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:xtremobile/core/utils/device_info.dart'; -/// Stream type enum for player +/// Stream type enum for player // (Moved to global or imported from iptv_models if available) /// Native video player screen for Android/iOS @@ -165,7 +165,9 @@ class _NativePlayerScreenState extends ConsumerState 'MediaKitPlayer: Applying LIVE optimization profile (STABILITY)', ); (_player.platform as dynamic)?.setProperty( - 'cache-secs', '10'); // Reduced to 10s for faster startup + 'cache-secs', + '10', + ); // Reduced to 10s for faster startup (_player.platform as dynamic)?.setProperty( 'demuxer-max-bytes', '32000000', @@ -190,7 +192,8 @@ class _NativePlayerScreenState extends ConsumerState final backBytes = (bufferBytes / 10).toInt(); // 10% for backward buffer debugPrint( - 'MediaKitPlayer: VOD profile - ${deviceInfo.getDeviceProfile()}, buffer=${bufferBytes ~/ (1024 * 1024)}MB'); + 'MediaKitPlayer: VOD profile - ${deviceInfo.getDeviceProfile()}, buffer=${bufferBytes ~/ (1024 * 1024)}MB', + ); (_player.platform as dynamic)?.setProperty('cache-secs', '100'); (_player.platform as dynamic) @@ -309,7 +312,8 @@ class _NativePlayerScreenState extends ConsumerState _checkAndMarkWatched(position); // Auto-save position every 10 seconds (resilience against app kill/crash) - if (widget.streamType != model.StreamType.live && !_hasMarkedAsWatched) { + if (widget.streamType != model.StreamType.live && + !_hasMarkedAsWatched) { final now = DateTime.now(); if (_lastSaveTime == null || now.difference(_lastSaveTime!) > const Duration(seconds: 10)) { @@ -594,7 +598,8 @@ class _NativePlayerScreenState extends ConsumerState Future.delayed(Duration(seconds: 2 * _reconnectAttempts), () { if (mounted) { // If VOD/Series, try to resume from current position - if (widget.streamType != model.StreamType.live && _position.inSeconds > 0) { + if (widget.streamType != model.StreamType.live && + _position.inSeconds > 0) { _loadStream(startAt: _position); } else { _loadStream(); @@ -949,7 +954,8 @@ class _NativePlayerScreenState extends ConsumerState // Channel Down = Previous Channel (inverted as requested) if (key == LogicalKeyboardKey.channelDown) { - if (widget.streamType == model.StreamType.live && widget.channels != null) { + if (widget.streamType == model.StreamType.live && + widget.channels != null) { _playPrevious(); _onUserInteraction(); return true; @@ -958,7 +964,8 @@ class _NativePlayerScreenState extends ConsumerState // Channel Up = Next Channel (inverted as requested) if (key == LogicalKeyboardKey.channelUp) { - if (widget.streamType == model.StreamType.live && widget.channels != null) { + if (widget.streamType == model.StreamType.live && + widget.channels != null) { _playNext(); _onUserInteraction(); return true; @@ -1522,9 +1529,11 @@ class _NativePlayerScreenState extends ConsumerState LogicalKeyboardKey.arrowLeft) { final newPos = _position - const Duration(seconds: 10); - _player.seek(newPos < Duration.zero - ? Duration.zero - : newPos); + _player.seek( + newPos < Duration.zero + ? Duration.zero + : newPos, + ); _resetControlsTimer(); return KeyEventResult.handled; } diff --git a/lib/features/iptv/services/xtream_service_mobile.dart b/lib/features/iptv/services/xtream_service_mobile.dart index cbdb0d5..8c79777 100644 --- a/lib/features/iptv/services/xtream_service_mobile.dart +++ b/lib/features/iptv/services/xtream_service_mobile.dart @@ -2,13 +2,13 @@ /// /// Handles Xtream API communication with batching and caching optimizations /// for mobile platforms. Designed for performance with TiviMate-level efficiency. +library; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'dart:async'; import '../models/xtream_models.dart' as model; import 'package:xtremobile/core/models/playlist_config.dart'; -import 'package:xtremobile/core/api/api_client.dart'; /// EPG cache entry with TTL class _EpgCacheEntry { @@ -18,7 +18,8 @@ class _EpgCacheEntry { _EpgCacheEntry(this.data) : timestamp = DateTime.now(); - bool get isExpired => DateTime.now().difference(timestamp).inSeconds > ttlSeconds; + bool get isExpired => + DateTime.now().difference(timestamp).inSeconds > ttlSeconds; } /// Xtream API Service for Mobile @@ -49,8 +50,18 @@ class XtreamServiceMobile { _username = config.username; _password = config.password; - // Use shared Dio instance from ApiClient (with DNS resolution, etc.) - _dio = ApiClient().dio; + // Create independent Dio for Xtream API (avoid ApiClient baseUrl issues) + _dio = Dio( + BaseOptions( + connectTimeout: const Duration(seconds: 15), + receiveTimeout: const Duration(seconds: 30), + sendTimeout: const Duration(seconds: 15), + ), + ); + + if (kDebugMode) { + print('✅ [XtreamServiceMobile] Initialized with URL: $_baseUrl'); + } } /// Get live channels for a category (with batch EPG support) @@ -81,23 +92,31 @@ class XtreamServiceMobile { ), ); - if (response.statusCode == 200 && response.data is List) { - final channels = (response.data as List) - .map((e) => model.Channel.fromJson(e)) - .toList(); + if (response.statusCode == 200) { + if (response.data is List) { + final channels = (response.data as List) + .map((e) => model.Channel.fromJson(e)) + .toList(); - if (kDebugMode) { - final categories = channels.map((c) => c.categoryName).toSet(); - print( - '✅ Loaded ${channels.length} channels with ${categories.length} categories: $categories', - ); + if (kDebugMode) { + final categories = channels.map((c) => c.categoryName).toSet(); + print( + '✅ Loaded ${channels.length} channels with ${categories.length} categories: $categories', + ); + } + + return channels; + } else if (response.data is Map) { + if (kDebugMode) print('⚠️ API returned a Map instead of List: ${response.data}'); + return []; + } else { + if (kDebugMode) print('⚠️ API returned unexpected type: ${response.data.runtimeType}'); + return []; } - - return channels; } if (kDebugMode) { - print('⚠️ Unexpected response status: ${response.statusCode}'); + print('⚠️ Unexpected response status: ${response.statusCode}, body: ${response.data}'); } return []; } catch (e) { @@ -110,12 +129,13 @@ class XtreamServiceMobile { Future getShortEPG(String streamId) async { // For backward compatibility, but prefer batch loading final batch = await getBatchEPG([streamId]); - return batch[streamId] ?? model.ShortEPG( - id: streamId, - title: 'No Program', - start: DateTime.now().toIso8601String(), - end: DateTime.now().add(const Duration(hours: 1)).toIso8601String(), - ); + return batch[streamId] ?? + model.ShortEPG( + id: streamId, + title: 'No Program', + start: DateTime.now().toIso8601String(), + end: DateTime.now().add(const Duration(hours: 1)).toIso8601String(), + ); } /// Get SHORT EPG for MULTIPLE channels in ONE request @@ -123,7 +143,8 @@ class XtreamServiceMobile { /// This is the optimized method that prevents N+1 queries. /// Instead of 50 requests for 50 channels, this loads all at once. /// Results are cached for 1 hour to avoid repeated requests. - Future> getBatchEPG(List streamIds) async { + Future> getBatchEPG( + List streamIds) async { if (streamIds.isEmpty) return {}; // Create cache key from sorted IDs for consistency @@ -133,7 +154,8 @@ class XtreamServiceMobile { // Check if we have valid cached data final cached = _epgBatchCache[cacheKeyStr]; if (cached != null && !cached.isExpired) { - if (kDebugMode) print('✅ EPG batch cache hit: ${streamIds.length} channels'); + if (kDebugMode) + print('✅ EPG batch cache hit: ${streamIds.length} channels'); return cached.data; } @@ -158,7 +180,9 @@ class XtreamServiceMobile { /// Internal: Load EPG batch from API Future> _loadEpgBatch( - List streamIds, String cacheKey) async { + List streamIds, + String cacheKey, + ) async { try { final result = {}; @@ -186,18 +210,20 @@ class XtreamServiceMobile { if (item is Map && item.containsKey('stream_id')) { final streamId = item['stream_id'].toString(); // Assuming the API returns a list of EPG entries for this stream - if (item['epg_listings'] is List && (item['epg_listings'] as List).isNotEmpty) { - final epgJson = (item['epg_listings'] as List).first; - result[streamId] = model.ShortEPG.fromJson({ - ...epgJson, - 'id': streamId, - }); + if (item['epg_listings'] is List && + (item['epg_listings'] as List).isNotEmpty) { + final epgJson = (item['epg_listings'] as List).first; + result[streamId] = model.ShortEPG.fromJson({ + ...epgJson, + 'id': streamId, + }); } } } if (kDebugMode) { - print('✅ Loaded EPG for ${result.length}/${streamIds.length} channels'); + print( + '✅ Loaded EPG for ${result.length}/${streamIds.length} channels'); } } @@ -228,25 +254,34 @@ class XtreamServiceMobile { ) .timeout(const Duration(seconds: 8)); - if (response.statusCode == 200 && response.data is List) { - final categories = (response.data as List) - .map((e) => model.Category.fromJson(e)) - .toList(); + if (response.statusCode == 200) { + if (response.data is List) { + final categories = (response.data as List) + .map((e) => model.Category.fromJson(e)) + .toList(); - if (kDebugMode) { - print('✅ Loaded ${categories.length} live categories'); + if (kDebugMode) { + print('✅ Loaded ${categories.length} live categories: ${categories.map((c) => c.categoryName).toList()}'); + } + + return categories; + } else if (response.data is Map) { + if (kDebugMode) print('⚠️ get_live_categories returned a Map (not a list): ${response.data}'); + return []; + } else { + if (kDebugMode) print('⚠️ get_live_categories returned unexpected type: ${response.data.runtimeType}'); + return []; } - - return categories; } if (kDebugMode) { - print('⚠️ Failed to load categories (status: ${response.statusCode})'); + print('⚠️ Failed to load categories (status: ${response.statusCode}, body: ${response.data})'); } return []; } on TimeoutException { if (kDebugMode) { - print('⏱️ Timeout loading live categories - will fallback to loading all channels'); + print( + '⏱️ Timeout loading live categories - will fallback to loading all channels'); } return []; } catch (e) { @@ -335,7 +370,9 @@ class XtreamServiceMobile { /// Search VOD movies Future> searchMovies(String query) async { final all = await getMovies(); - return all.where((m) => m.name.toLowerCase().contains(query.toLowerCase())).toList(); + return all + .where((m) => m.name.toLowerCase().contains(query.toLowerCase())) + .toList(); } /// Get all series @@ -363,7 +400,9 @@ class XtreamServiceMobile { /// Search series Future> searchSeries(String query) async { final all = await getSeries(); - return all.where((s) => s.name.toLowerCase().contains(query.toLowerCase())).toList(); + return all + .where((s) => s.name.toLowerCase().contains(query.toLowerCase())) + .toList(); } /// Get series paginated @@ -373,7 +412,7 @@ class XtreamServiceMobile { /// Get series info Future getSeriesInfo(String seriesId) async { - try { + try { final response = await _dio.get( '$_baseUrl/player_api.php', queryParameters: { @@ -387,7 +426,7 @@ class XtreamServiceMobile { if (response.statusCode == 200 && response.data is Map) { final data = response.data as Map; final seriesModel = model.Series.fromJson(data['info'] ?? {}); - + final episodesMap = >{}; if (data['episodes'] is Map) { final seasons = data['episodes'] as Map; @@ -399,7 +438,7 @@ class XtreamServiceMobile { } }); } - + return model.SeriesInfo( series: seriesModel, episodes: episodesMap, diff --git a/lib/features/iptv/widgets/mobile_live_tv_tab.dart b/lib/features/iptv/widgets/mobile_live_tv_tab.dart index 68c293b..7b10ae2 100644 --- a/lib/features/iptv/widgets/mobile_live_tv_tab.dart +++ b/lib/features/iptv/widgets/mobile_live_tv_tab.dart @@ -3,7 +3,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:xtremobile/core/utils/image_cache_config.dart'; -import 'dart:io'; import 'dart:async'; import 'package:xtremobile/mobile/providers/mobile_xtream_providers.dart'; import 'package:xtremobile/mobile/providers/mobile_settings_providers.dart'; @@ -11,8 +10,6 @@ import 'package:xtremobile/features/iptv/screens/native_player_screen.dart'; import 'package:xtremobile/core/models/iptv_models.dart'; import 'package:xtremobile/core/models/playlist_config.dart'; import 'package:xtremobile/core/theme/app_colors.dart'; -import 'package:xtremobile/core/api/dns_resolver.dart'; -import 'package:xtremobile/mobile/theme/mobile_theme.dart'; import 'package:xtremobile/mobile/widgets/tv_focusable.dart'; import 'package:xtremobile/features/iptv/screens/lite_player_screen.dart'; import 'package:flutter/services.dart'; @@ -158,7 +155,8 @@ class _MobileLiveTVTabState extends ConsumerState ), data: (groupedChannels) { // Prepare categories - var categories = groupedChannels.map((c) => c.categoryName).toSet().toList(); + var categories = + groupedChannels.map((c) => c.categoryName).toSet().toList(); if (settings.liveTvKeywords.isNotEmpty) { categories = categories .where((cat) => settings.matchesLiveTvFilter(cat)) @@ -198,8 +196,9 @@ class _MobileLiveTVTabState extends ConsumerState .where((c) => favorites.contains(c.streamId)) .toList(); } else if (uiState.selectedCategory != null) { - displayedChannels = - groupedChannels.where((c) => c.categoryName == uiState.selectedCategory).toList(); + displayedChannels = groupedChannels + .where((c) => c.categoryName == uiState.selectedCategory) + .toList(); } } diff --git a/lib/features/iptv/widgets/mobile_movies_tab.dart b/lib/features/iptv/widgets/mobile_movies_tab.dart index 8d85c9e..0267c9c 100644 --- a/lib/features/iptv/widgets/mobile_movies_tab.dart +++ b/lib/features/iptv/widgets/mobile_movies_tab.dart @@ -86,7 +86,9 @@ class _MobileMoviesTabState extends ConsumerState final service = await ref.read(mobileXtreamServiceProvider(widget.playlist).future); final movies = await service.getMovies(); - final results = movies.where((m) => m.name.toLowerCase().contains(query.toLowerCase())).toList(); + final results = movies + .where((m) => m.name.toLowerCase().contains(query.toLowerCase())) + .toList(); if (mounted && _searchQuery == query) { setState(() { @@ -110,7 +112,9 @@ class _MobileMoviesTabState extends ConsumerState if (mounted) { setState(() { - _categories = categories.map((c) => MapEntry(c.categoryId, c.categoryName)).toList() + _categories = categories + .map((c) => MapEntry(c.categoryId, c.categoryName)) + .toList() ..sort((a, b) => a.value.compareTo(b.value)); _isLoading = false; }); @@ -400,7 +404,8 @@ class _MobileMoviesTabState extends ConsumerState focusedBorder: InputBorder.none, enabledBorder: InputBorder.none, isDense: true, - contentPadding: EdgeInsets.only(bottom: 11), + contentPadding: + const EdgeInsets.only(bottom: 11), ), onChanged: _onSearchChanged, onSubmitted: (_) => diff --git a/lib/features/iptv/widgets/mobile_settings_tab.dart b/lib/features/iptv/widgets/mobile_settings_tab.dart index 6f0b443..ea23d30 100644 --- a/lib/features/iptv/widgets/mobile_settings_tab.dart +++ b/lib/features/iptv/widgets/mobile_settings_tab.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; 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'; diff --git a/lib/main.dart b/lib/main.dart index c87c691..d8dfa72 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:hive_flutter/hive_flutter.dart'; import 'package:media_kit/media_kit.dart'; import 'core/theme/app_theme.dart'; import 'core/providers/theme_provider.dart'; @@ -75,4 +74,3 @@ class XtremFlowApp extends ConsumerWidget { ); } } - diff --git a/lib/mobile/providers/mobile_xtream_providers.dart b/lib/mobile/providers/mobile_xtream_providers.dart index cc37256..3bd410d 100644 --- a/lib/mobile/providers/mobile_xtream_providers.dart +++ b/lib/mobile/providers/mobile_xtream_providers.dart @@ -16,35 +16,88 @@ final mobileXtreamServiceProvider = return service; }); -/// Mobile-specific live channels provider -/// Loads ALL channels from ALL categories +/// Mobile-specific live categories provider (for Live TV category grid) +final mobileLiveCategoriesProvider = + FutureProvider.family, PlaylistConfig>( + (ref, playlist) async { + final service = await ref.watch(mobileXtreamServiceProvider(playlist).future); + return service.getLiveCategories(); +}); + +/// Mobile-specific live channels provider (loads channels for a specific category) +final mobileLiveChannelsByCategoryProvider = FutureProvider.family< + List, + (PlaylistConfig, String)>((ref, params) async { + final (playlist, categoryId) = params; + final service = await ref.watch(mobileXtreamServiceProvider(playlist).future); + return service.getLiveChannels(categoryId); +}); + +/// Load ALL live channels efficiently +/// Strategy: Load categories, then load channels in SMALL BATCHES (5 at a time) +/// This balances speed with robustness - not too slow, not too many parallel requests final mobileLiveChannelsProvider = FutureProvider.family, PlaylistConfig>( (ref, playlist) async { + print('📺 [LiveTV] Starting to load ALL channels...'); + final service = await ref.watch(mobileXtreamServiceProvider(playlist).future); - // Strategy: Try to load categories first (with timeout) - // If categories load successfully, load channels for each category - // If categories fail/timeout, load all channels at once - List categories = []; - try { - categories = await service.getLiveCategories(); - } catch (e) { - // Silently fall back to loading all channels + // Step 1: Load categories + print('🔄 [LiveTV] Loading categories...'); + final categories = await service.getLiveCategories(); + print('✅ [LiveTV] Loaded ${categories.length} categories'); + + if (categories.isEmpty) { + print('❌ [LiveTV] No categories found!'); + return []; } - if (categories.isNotEmpty) { - // Load channels from each category and combine them - final allChannels = []; - for (final category in categories) { - final channels = await service.getLiveChannels(category.categoryId); + // Step 2: Load channels in SMALL BATCHES (5 at a time for balance) + final allChannels = []; + const batchSize = 5; + int loaded = 0; + int totalBatches = (categories.length / batchSize).ceil(); + + for (int batchNum = 0; batchNum < totalBatches; batchNum++) { + int startIdx = batchNum * batchSize; + int endIdx = + (startIdx + batchSize < categories.length) ? startIdx + batchSize : categories.length; + final batch = categories.sublist(startIdx, endIdx); + + print( + '🔄 [LiveTV] Batch ${batchNum + 1}/$totalBatches (${batch.length} categories)...', + ); + + // Load this batch in parallel + final futures = batch + .map( + (cat) => service + .getLiveChannels(cat.categoryId) + .timeout(const Duration(seconds: 15)) + .catchError((_) => []), + ) + .toList(); + + final batchResults = await Future.wait(futures); + + // Combine results from this batch + for (final channels in batchResults) { allChannels.addAll(channels); } - return allChannels; + + loaded += batch.length; + print( + '✅ [LiveTV] Batch complete: $loaded/${categories.length} categories, ' + '${allChannels.length} total channels', + ); } - // Fallback: Load all channels without category filter - return service.getLiveChannels(""); + print( + '🎉 [LiveTV] ALL DONE! ${categories.length} categories, ' + '${allChannels.length} total channels', + ); + return allChannels; }); /// Mobile-specific movies provider