mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-11 17:29:26 +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/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';
|
||||
|
||||
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/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';
|
||||
|
||||
File renamed without changes.
+1
-1
@@ -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';
|
||||
|
||||
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 '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()));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user