mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-23 23:28:15 +02:00
Phase 3: Architecture refactor - consolidate folders and extract patterns
- 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 <noreply@anthropic.com>
This commit is contained in:
1 parent
d0f0589d09
commit
cb1788be8a
16 files changed
+361
-5
No files matched your search
@@ -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<T> {
|
||||||
|
late Box<T> box;
|
||||||
|
final String boxName;
|
||||||
|
final bool encrypted;
|
||||||
|
List<int>? encryptionKey;
|
||||||
|
|
||||||
|
HiveServiceBase({
|
||||||
|
required this.boxName,
|
||||||
|
this.encrypted = false,
|
||||||
|
this.encryptionKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Initialize the Hive box
|
||||||
|
Future<void> init() async {
|
||||||
|
try {
|
||||||
|
if (Hive.isBoxOpen(boxName)) {
|
||||||
|
box = Hive.box<T>(boxName);
|
||||||
|
} else {
|
||||||
|
if (encrypted && encryptionKey != null) {
|
||||||
|
box = await Hive.openBox<T>(
|
||||||
|
boxName,
|
||||||
|
encryptionCipher: HiveAesCipher(encryptionKey!),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
box = await Hive.openBox<T>(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<T?> 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<void> 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<void> 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<void> clear() async {
|
||||||
|
try {
|
||||||
|
await box.clear();
|
||||||
|
} catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print('❌ Error clearing $boxName: $e');
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all values from the box
|
||||||
|
List<T> getAll() {
|
||||||
|
try {
|
||||||
|
return box.values.toList();
|
||||||
|
} catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print('❌ Error getting all from $boxName: $e');
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close the box
|
||||||
|
Future<void> close() async {
|
||||||
|
try {
|
||||||
|
await box.close();
|
||||||
|
} catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print('❌ Error closing $boxName: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<StreamResolverPattern> 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<bool> _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<bool> _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<String?> _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<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'url': url,
|
||||||
|
'codec': codec,
|
||||||
|
'bitrate': bitrate,
|
||||||
|
'isResolvable': isResolvable,
|
||||||
|
'error': error,
|
||||||
|
'status': statusMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
|
}
|
||||||
|
}
|
||||||
File renamed without changes.
+1
-1
@@ -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/features/iptv/services/xtream_service_mobile.dart';
|
||||||
import 'package:xtremflow/mobile/providers/mobile_settings_providers.dart';
|
import 'package:xtremflow/mobile/providers/mobile_settings_providers.dart';
|
||||||
import 'package:xtremflow/core/theme/app_colors.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/features/iptv/models/xtream_models.dart' as xm;
|
||||||
import 'package:xtremflow/mobile/widgets/tv_focusable.dart';
|
import 'package:xtremflow/mobile/widgets/tv_focusable.dart';
|
||||||
|
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -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/features/iptv/services/xtream_service_mobile.dart';
|
||||||
import 'package:xtremflow/mobile/providers/mobile_settings_providers.dart';
|
import 'package:xtremflow/mobile/providers/mobile_settings_providers.dart';
|
||||||
import 'package:xtremflow/core/theme/app_colors.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:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:xtremflow/mobile/providers/mobile_xtream_providers.dart';
|
import 'package:xtremflow/mobile/providers/mobile_xtream_providers.dart';
|
||||||
|
|
||||||
File renamed without changes.
+1
-1
@@ -12,7 +12,7 @@ import '../../../../core/theme/app_colors.dart';
|
|||||||
import '../../../../core/api/dns_resolver.dart';
|
import '../../../../core/api/dns_resolver.dart';
|
||||||
import '../../../theme/mobile_theme.dart';
|
import '../../../theme/mobile_theme.dart';
|
||||||
import 'package:xtremflow/mobile/widgets/tv_focusable.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 'package:flutter/services.dart';
|
||||||
import '../../../../core/services/ip_service.dart';
|
import '../../../../core/services/ip_service.dart';
|
||||||
|
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+11
-2
@@ -4,21 +4,27 @@ import 'package:hive_flutter/hive_flutter.dart';
|
|||||||
import 'package:media_kit/media_kit.dart';
|
import 'package:media_kit/media_kit.dart';
|
||||||
import 'core/theme/app_theme.dart';
|
import 'core/theme/app_theme.dart';
|
||||||
import 'core/providers/theme_provider.dart';
|
import 'core/providers/theme_provider.dart';
|
||||||
import 'mobile/features/iptv/screens/mobile_playlist_screen.dart';
|
import 'features/iptv/screens/mobile_playlist_screen.dart';
|
||||||
import 'mobile/features/iptv/screens/mobile_dashboard_screen.dart';
|
import 'features/iptv/screens/mobile_dashboard_screen.dart';
|
||||||
import 'core/models/playlist_config.dart';
|
import 'core/models/playlist_config.dart';
|
||||||
import 'core/database/hive_service.dart';
|
import 'core/database/hive_service.dart';
|
||||||
|
import 'core/utils/startup_profiler.dart';
|
||||||
|
|
||||||
/// Android-optimized entry point for XtremFlow IPTV
|
/// Android-optimized entry point for XtremFlow IPTV
|
||||||
/// No authentication - direct access to playlist selection
|
/// No authentication - direct access to playlist selection
|
||||||
void main() async {
|
void main() async {
|
||||||
|
StartupProfiler.start('app_init');
|
||||||
|
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
StartupProfiler.mark('flutter_binding_init');
|
||||||
|
|
||||||
// Initialize MediaKit (required for video playback)
|
// Initialize MediaKit (required for video playback)
|
||||||
MediaKit.ensureInitialized();
|
MediaKit.ensureInitialized();
|
||||||
|
StartupProfiler.mark('media_kit_init');
|
||||||
|
|
||||||
// Initialize Hive (Centralized)
|
// Initialize Hive (Centralized)
|
||||||
await HiveService.init();
|
await HiveService.init();
|
||||||
|
StartupProfiler.mark('hive_init');
|
||||||
|
|
||||||
// CLEAR CACHE ON STARTUP (Requested by User)
|
// CLEAR CACHE ON STARTUP (Requested by User)
|
||||||
try {
|
try {
|
||||||
@@ -28,6 +34,9 @@ void main() async {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('XtremFlow: Failed to clear cache: $e');
|
debugPrint('XtremFlow: Failed to clear cache: $e');
|
||||||
}
|
}
|
||||||
|
StartupProfiler.mark('cache_clear');
|
||||||
|
|
||||||
|
await StartupProfiler.reportAll();
|
||||||
|
|
||||||
runApp(const ProviderScope(child: XtremFlowApp()));
|
runApp(const ProviderScope(child: XtremFlowApp()));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user