From cb1788be8a25f603774e91f5fa4fc68fb9216b3b Mon Sep 17 00:00:00 2001 From: GitHub Copilot Date: Wed, 25 Mar 2026 22:21:49 +0100 Subject: [PATCH] Phase 3: Architecture refactor - consolidate folders and extract patterns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Consolidate lib/mobile/features/iptv → lib/features/iptv * Move all IPTV screens, models, services, widgets to unified location * Update imports in main.dart and affected files * Remove old mobile/features/iptv directory - Create lib/core/patterns/ for reusable implementations * HiveServiceBase: Template for Hive-based persistent storage * VideoPlayerWrapper: Abstraction for video player selection * StreamResolverPattern: IPTV stream resolution and validation - Router already simplified (no kIsWeb checks, Android-only) All changes maintain Android-only architecture and improve maintainability. Co-Authored-By: Claude Haiku 4.5 --- lib/core/patterns/hive_service_pattern.dart | 114 +++++++++++++++ .../patterns/stream_resolver_pattern.dart | 137 ++++++++++++++++++ lib/core/patterns/video_player_wrapper.dart | 96 ++++++++++++ .../features/iptv/models/xtream_models.dart | 0 .../iptv/screens/lite_player_screen.dart | 2 +- .../iptv/screens/mobile_dashboard_screen.dart | 0 .../iptv/screens/mobile_playlist_screen.dart | 0 .../mobile_playlist_selection_screen.dart | 0 .../screens/mobile_series_detail_screen.dart | 0 .../iptv/screens/native_player_screen.dart | 2 +- .../iptv/services/xtream_service_mobile.dart | 0 .../iptv/widgets/mobile_live_tv_tab.dart | 2 +- .../iptv/widgets/mobile_movies_tab.dart | 0 .../iptv/widgets/mobile_series_tab.dart | 0 .../iptv/widgets/mobile_settings_tab.dart | 0 lib/main.dart | 13 +- 16 files changed, 361 insertions(+), 5 deletions(-) create mode 100644 lib/core/patterns/hive_service_pattern.dart create mode 100644 lib/core/patterns/stream_resolver_pattern.dart create mode 100644 lib/core/patterns/video_player_wrapper.dart rename lib/{mobile => }/features/iptv/models/xtream_models.dart (100%) rename lib/{mobile => }/features/iptv/screens/lite_player_screen.dart (99%) rename lib/{mobile => }/features/iptv/screens/mobile_dashboard_screen.dart (100%) rename lib/{mobile => }/features/iptv/screens/mobile_playlist_screen.dart (100%) rename lib/{mobile => }/features/iptv/screens/mobile_playlist_selection_screen.dart (100%) rename lib/{mobile => }/features/iptv/screens/mobile_series_detail_screen.dart (100%) rename lib/{mobile => }/features/iptv/screens/native_player_screen.dart (99%) rename lib/{mobile => }/features/iptv/services/xtream_service_mobile.dart (100%) rename lib/{mobile => }/features/iptv/widgets/mobile_live_tv_tab.dart (99%) rename lib/{mobile => }/features/iptv/widgets/mobile_movies_tab.dart (100%) rename lib/{mobile => }/features/iptv/widgets/mobile_series_tab.dart (100%) rename lib/{mobile => }/features/iptv/widgets/mobile_settings_tab.dart (100%) diff --git a/lib/core/patterns/hive_service_pattern.dart b/lib/core/patterns/hive_service_pattern.dart new file mode 100644 index 0000000..7986e82 --- /dev/null +++ b/lib/core/patterns/hive_service_pattern.dart @@ -0,0 +1,114 @@ +import 'package:hive/hive.dart'; +import 'package:flutter/foundation.dart'; + +/// Base pattern for Hive-based services +/// Provides a template for implementing persistent storage with Hive +abstract class HiveServiceBase { + late Box box; + final String boxName; + final bool encrypted; + List? encryptionKey; + + HiveServiceBase({ + required this.boxName, + this.encrypted = false, + this.encryptionKey, + }); + + /// Initialize the Hive box + Future init() async { + try { + if (Hive.isBoxOpen(boxName)) { + box = Hive.box(boxName); + } else { + if (encrypted && encryptionKey != null) { + box = await Hive.openBox( + boxName, + encryptionCipher: HiveAesCipher(encryptionKey!), + ); + } else { + box = await Hive.openBox(boxName); + } + } + if (kDebugMode) { + print('✅ Hive box initialized: $boxName'); + } + } catch (e) { + if (kDebugMode) { + print('❌ Failed to init Hive box $boxName: $e'); + } + rethrow; + } + } + + /// Get a value from the box + Future get(String key) async { + try { + return box.get(key); + } catch (e) { + if (kDebugMode) { + print('❌ Error getting $key from $boxName: $e'); + } + return null; + } + } + + /// Put a value in the box + Future put(String key, T value) async { + try { + await box.put(key, value); + } catch (e) { + if (kDebugMode) { + print('❌ Error putting $key to $boxName: $e'); + } + rethrow; + } + } + + /// Delete a value from the box + Future delete(String key) async { + try { + await box.delete(key); + } catch (e) { + if (kDebugMode) { + print('❌ Error deleting $key from $boxName: $e'); + } + rethrow; + } + } + + /// Clear all values from the box + Future clear() async { + try { + await box.clear(); + } catch (e) { + if (kDebugMode) { + print('❌ Error clearing $boxName: $e'); + } + rethrow; + } + } + + /// Get all values from the box + List getAll() { + try { + return box.values.toList(); + } catch (e) { + if (kDebugMode) { + print('❌ Error getting all from $boxName: $e'); + } + return []; + } + } + + /// Close the box + Future close() async { + try { + await box.close(); + } catch (e) { + if (kDebugMode) { + print('❌ Error closing $boxName: $e'); + } + } + } +} diff --git a/lib/core/patterns/stream_resolver_pattern.dart b/lib/core/patterns/stream_resolver_pattern.dart new file mode 100644 index 0000000..b8e8506 --- /dev/null +++ b/lib/core/patterns/stream_resolver_pattern.dart @@ -0,0 +1,137 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:flutter/foundation.dart'; + +/// Pattern for IPTV stream metadata resolution +/// Handles URL validation, DNS resolution, and codec detection +class StreamResolverPattern { + /// Result of stream resolution + final String url; + final String? codec; + final int? bitrate; + final bool isResolvable; + final String? error; + + StreamResolverPattern({ + required this.url, + this.codec, + this.bitrate, + required this.isResolvable, + this.error, + }); + + /// Resolve stream URL with parallel checks + static Future resolve( + String streamUrl, { + Duration timeout = const Duration(seconds: 5), + }) async { + try { + if (streamUrl.isEmpty) { + return StreamResolverPattern( + url: streamUrl, + isResolvable: false, + error: 'Empty URL', + ); + } + + // 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 isValid = results[0] as bool; + final dnsResolved = results[1] as bool; + + if (!isValid) { + return StreamResolverPattern( + url: streamUrl, + isResolvable: false, + error: 'Invalid URL format', + ); + } + + if (!dnsResolved) { + if (kDebugMode) { + debugPrint('⚠️ DNS resolution warning for: $streamUrl'); + } + } + + return StreamResolverPattern( + url: streamUrl, + isResolvable: true, + codec: await _detectCodec(streamUrl), + ); + } catch (e) { + return StreamResolverPattern( + url: streamUrl, + isResolvable: false, + error: e.toString(), + ); + } + } + + /// Validate URL format + static Future _validateUrl(String url) async { + try { + final uri = Uri.parse(url); + return uri.hasScheme && (uri.scheme == 'http' || uri.scheme == 'https'); + } catch (e) { + return false; + } + } + + /// Perform DNS resolution + static Future _performDnsResolution(String url) async { + try { + final uri = Uri.parse(url); + final host = uri.host; + + if (host.isEmpty) return false; + + // Attempt DNS lookup + final result = await InternetAddress.lookup(host) + .timeout(const Duration(seconds: 3)); + + return result.isNotEmpty; + } on SocketException { + return false; + } catch (e) { + return false; + } + } + + /// Detect codec from URL or headers + static Future _detectCodec(String url) async { + try { + // Extract codec hints from URL + if (url.contains('h264')) return 'h264'; + if (url.contains('h265') || url.contains('hevc')) return 'hevc'; + if (url.contains('vp9')) return 'vp9'; + if (url.contains('av1')) return 'av1'; + + // Default to h264 for IPTV streams + return 'h264'; + } catch (e) { + return null; + } + } + + /// Get human-readable status + String get statusMessage { + if (isResolvable) return '✅ Resolvable'; + return '❌ ${error ?? "Unresolvable"}'; + } + + /// Export as JSON + Map toJson() { + return { + 'url': url, + 'codec': codec, + 'bitrate': bitrate, + 'isResolvable': isResolvable, + 'error': error, + 'status': statusMessage, + }; + } +} diff --git a/lib/core/patterns/video_player_wrapper.dart b/lib/core/patterns/video_player_wrapper.dart new file mode 100644 index 0000000..ec50eba --- /dev/null +++ b/lib/core/patterns/video_player_wrapper.dart @@ -0,0 +1,96 @@ +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 +/// Handles fallback logic and player initialization +class VideoPlayerWrapper { + static const _availablePlayers = ['mediakit']; + + /// Enum for supported players + enum PlayerType { mediakit, native } + + /// Get the preferred player type based on stream and platform + static PlayerType getPreferredPlayer({ + required String streamUrl, + required String? codec, + bool preferLite = false, + }) { + // Priority: MediaKit for most cases + // Could add fallback logic here based on codec, bitrate, etc. + return PlayerType.mediakit; + } + + /// Validate stream URL and get playable format + static String? validateAndGetPlayableUrl(String streamUrl) { + try { + if (streamUrl.isEmpty) return null; + + // Check for common URL patterns + if (streamUrl.startsWith('http://') || streamUrl.startsWith('https://')) { + return streamUrl; + } + + // Handle m3u playlist entries + if (streamUrl.startsWith('#EXTINF')) { + return null; // Invalid - should be extracted from m3u + } + + return streamUrl; + } catch (e) { + debugPrint('❌ Error validating URL: $e'); + return null; + } + } + + /// Create media player with standard settings + static Player createPlayer({ + bool enableLogs = false, + }) { + final player = Player( + configuration: PlayerConfiguration( + logLevel: enableLogs ? MPVLogLevel.debug : MPVLogLevel.no, + ), + ); + return player; + } + + /// Validate codec support + static bool isCodecSupported(String? codec) { + if (codec == null) return true; // Unknown codecs are attempted anyway + + final supportedCodecs = { + 'h264', + 'hevc', + 'h265', + 'mpeg2video', + 'mpeg1video', + 'vp8', + 'vp9', + 'av1', + }; + + return supportedCodecs.contains(codec.toLowerCase()); + } + + /// Get player-specific error message + static String getPlayerErrorMessage(String error, PlayerType playerType) { + return switch (playerType) { + PlayerType.mediakit => _getMediaKitError(error), + PlayerType.native => _getNativePlayerError(error), + }; + } + + static String _getMediaKitError(String error) { + if (error.contains('Network')) return 'Stream connection failed'; + if (error.contains('Timeout')) return 'Stream took too long to load'; + if (error.contains('Codec')) return 'Video codec not supported'; + return 'Playback error: $error'; + } + + static String _getNativePlayerError(String error) { + if (error.contains('Network')) return 'Connection lost'; + return 'Player error: $error'; + } +} diff --git a/lib/mobile/features/iptv/models/xtream_models.dart b/lib/features/iptv/models/xtream_models.dart similarity index 100% rename from lib/mobile/features/iptv/models/xtream_models.dart rename to lib/features/iptv/models/xtream_models.dart diff --git a/lib/mobile/features/iptv/screens/lite_player_screen.dart b/lib/features/iptv/screens/lite_player_screen.dart similarity index 99% rename from lib/mobile/features/iptv/screens/lite_player_screen.dart rename to lib/features/iptv/screens/lite_player_screen.dart index 70cc3cb..b7151b4 100644 --- a/lib/mobile/features/iptv/screens/lite_player_screen.dart +++ b/lib/features/iptv/screens/lite_player_screen.dart @@ -11,7 +11,7 @@ import 'package:xtremflow/core/models/playlist_config.dart'; import 'package:xtremflow/features/iptv/services/xtream_service_mobile.dart'; import 'package:xtremflow/mobile/providers/mobile_settings_providers.dart'; import 'package:xtremflow/core/theme/app_colors.dart'; -import 'package:xtremflow/mobile/features/iptv/screens/native_player_screen.dart'; +import 'package:xtremflow/features/iptv/screens/native_player_screen.dart'; import 'package:xtremflow/features/iptv/models/xtream_models.dart' as xm; import 'package:xtremflow/mobile/widgets/tv_focusable.dart'; diff --git a/lib/mobile/features/iptv/screens/mobile_dashboard_screen.dart b/lib/features/iptv/screens/mobile_dashboard_screen.dart similarity index 100% rename from lib/mobile/features/iptv/screens/mobile_dashboard_screen.dart rename to lib/features/iptv/screens/mobile_dashboard_screen.dart diff --git a/lib/mobile/features/iptv/screens/mobile_playlist_screen.dart b/lib/features/iptv/screens/mobile_playlist_screen.dart similarity index 100% rename from lib/mobile/features/iptv/screens/mobile_playlist_screen.dart rename to lib/features/iptv/screens/mobile_playlist_screen.dart diff --git a/lib/mobile/features/iptv/screens/mobile_playlist_selection_screen.dart b/lib/features/iptv/screens/mobile_playlist_selection_screen.dart similarity index 100% rename from lib/mobile/features/iptv/screens/mobile_playlist_selection_screen.dart rename to lib/features/iptv/screens/mobile_playlist_selection_screen.dart diff --git a/lib/mobile/features/iptv/screens/mobile_series_detail_screen.dart b/lib/features/iptv/screens/mobile_series_detail_screen.dart similarity index 100% rename from lib/mobile/features/iptv/screens/mobile_series_detail_screen.dart rename to lib/features/iptv/screens/mobile_series_detail_screen.dart diff --git a/lib/mobile/features/iptv/screens/native_player_screen.dart b/lib/features/iptv/screens/native_player_screen.dart similarity index 99% rename from lib/mobile/features/iptv/screens/native_player_screen.dart rename to lib/features/iptv/screens/native_player_screen.dart index 7f678df..7caf947 100644 --- a/lib/mobile/features/iptv/screens/native_player_screen.dart +++ b/lib/features/iptv/screens/native_player_screen.dart @@ -12,7 +12,7 @@ import 'package:xtremflow/core/models/playlist_config.dart'; import 'package:xtremflow/features/iptv/services/xtream_service_mobile.dart'; import 'package:xtremflow/mobile/providers/mobile_settings_providers.dart'; import 'package:xtremflow/core/theme/app_colors.dart'; -import 'package:xtremflow/mobile/features/iptv/screens/lite_player_screen.dart'; +import 'package:xtremflow/features/iptv/screens/lite_player_screen.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:xtremflow/mobile/providers/mobile_xtream_providers.dart'; diff --git a/lib/mobile/features/iptv/services/xtream_service_mobile.dart b/lib/features/iptv/services/xtream_service_mobile.dart similarity index 100% rename from lib/mobile/features/iptv/services/xtream_service_mobile.dart rename to lib/features/iptv/services/xtream_service_mobile.dart diff --git a/lib/mobile/features/iptv/widgets/mobile_live_tv_tab.dart b/lib/features/iptv/widgets/mobile_live_tv_tab.dart similarity index 99% rename from lib/mobile/features/iptv/widgets/mobile_live_tv_tab.dart rename to lib/features/iptv/widgets/mobile_live_tv_tab.dart index 3754123..df1f6a2 100644 --- a/lib/mobile/features/iptv/widgets/mobile_live_tv_tab.dart +++ b/lib/features/iptv/widgets/mobile_live_tv_tab.dart @@ -12,7 +12,7 @@ import '../../../../core/theme/app_colors.dart'; import '../../../../core/api/dns_resolver.dart'; import '../../../theme/mobile_theme.dart'; import 'package:xtremflow/mobile/widgets/tv_focusable.dart'; -import 'package:xtremflow/mobile/features/iptv/screens/lite_player_screen.dart'; +import 'package:xtremflow/features/iptv/screens/lite_player_screen.dart'; import 'package:flutter/services.dart'; import '../../../../core/services/ip_service.dart'; diff --git a/lib/mobile/features/iptv/widgets/mobile_movies_tab.dart b/lib/features/iptv/widgets/mobile_movies_tab.dart similarity index 100% rename from lib/mobile/features/iptv/widgets/mobile_movies_tab.dart rename to lib/features/iptv/widgets/mobile_movies_tab.dart diff --git a/lib/mobile/features/iptv/widgets/mobile_series_tab.dart b/lib/features/iptv/widgets/mobile_series_tab.dart similarity index 100% rename from lib/mobile/features/iptv/widgets/mobile_series_tab.dart rename to lib/features/iptv/widgets/mobile_series_tab.dart diff --git a/lib/mobile/features/iptv/widgets/mobile_settings_tab.dart b/lib/features/iptv/widgets/mobile_settings_tab.dart similarity index 100% rename from lib/mobile/features/iptv/widgets/mobile_settings_tab.dart rename to lib/features/iptv/widgets/mobile_settings_tab.dart diff --git a/lib/main.dart b/lib/main.dart index 2ad4664..e261550 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,21 +4,27 @@ 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'; -import 'mobile/features/iptv/screens/mobile_playlist_screen.dart'; -import 'mobile/features/iptv/screens/mobile_dashboard_screen.dart'; +import 'features/iptv/screens/mobile_playlist_screen.dart'; +import 'features/iptv/screens/mobile_dashboard_screen.dart'; import 'core/models/playlist_config.dart'; import 'core/database/hive_service.dart'; +import 'core/utils/startup_profiler.dart'; /// Android-optimized entry point for XtremFlow IPTV /// No authentication - direct access to playlist selection void main() async { + StartupProfiler.start('app_init'); + WidgetsFlutterBinding.ensureInitialized(); + StartupProfiler.mark('flutter_binding_init'); // Initialize MediaKit (required for video playback) MediaKit.ensureInitialized(); + StartupProfiler.mark('media_kit_init'); // Initialize Hive (Centralized) await HiveService.init(); + StartupProfiler.mark('hive_init'); // CLEAR CACHE ON STARTUP (Requested by User) try { @@ -28,6 +34,9 @@ void main() async { } catch (e) { debugPrint('XtremFlow: Failed to clear cache: $e'); } + StartupProfiler.mark('cache_clear'); + + await StartupProfiler.reportAll(); runApp(const ProviderScope(child: XtremFlowApp())); }