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:
GitHub CopilotandClaude Haiku 4.5 committed 2026-03-25 22:21:49 +01:00
1 parent d0f0589d09
commit cb1788be8a
16 files changed
+361 -5

No files matched your search

+114
View File
@@ -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';
}
}
@@ -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';
@@ -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';
@@ -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';
+11 -2
View File
@@ -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()));
}