diff --git a/lib/core/services/network_service.dart b/lib/core/services/network_service.dart deleted file mode 100644 index f388168..0000000 --- a/lib/core/services/network_service.dart +++ /dev/null @@ -1,287 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:dio_cache_interceptor/dio_cache_interceptor.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'dart:io'; - -/// Advanced network configuration for streaming -class NetworkConfig { - final String? proxyUrl; - final String? userAgent; - final Map customHeaders; - final Duration connectTimeout; - final Duration receiveTimeout; - final bool enableValidateSSL; - - const NetworkConfig({ - this.proxyUrl, - this.userAgent = 'VLC/3.0.18 LibVLC/3.0.18', - this.customHeaders = const {}, - this.connectTimeout = const Duration(seconds: 30), - this.receiveTimeout = const Duration(seconds: 60), - this.enableValidateSSL = true, - }); - - NetworkConfig copyWith({ - String? proxyUrl, - String? userAgent, - Map? customHeaders, - Duration? connectTimeout, - Duration? receiveTimeout, - bool? enableValidateSSL, - }) => - NetworkConfig( - proxyUrl: proxyUrl ?? this.proxyUrl, - userAgent: userAgent ?? this.userAgent, - customHeaders: customHeaders ?? this.customHeaders, - connectTimeout: connectTimeout ?? this.connectTimeout, - receiveTimeout: receiveTimeout ?? this.receiveTimeout, - enableValidateSSL: enableValidateSSL ?? this.enableValidateSSL, - ); -} - -/// Advanced network service with proxy and optimization support -class OptimizedNetworkService { - late Dio _dio; - late NetworkConfig _config; - late CacheInterceptor _cacheInterceptor; - - OptimizedNetworkService({required NetworkConfig config}) { - _config = config; - _initializeDio(); - } - - void _initializeDio() { - _cacheInterceptor = CacheInterceptor( - options: CacheOptions( - store: MemCacheStore(), - policy: CachePolicy.requestFirst, - hitCacheOnErrorExcept: [], - allowPostMethod: false, - ), - ); - - _dio = Dio( - BaseOptions( - connectTimeout: _config.connectTimeout, - receiveTimeout: _config.receiveTimeout, - validateStatus: (status) => status != null && status < 500, - headers: { - 'User-Agent': _config.userAgent, - ..._config.customHeaders, - }, - ), - ); - - // Configure proxy if specified - if (_config.proxyUrl != null) { - _configureProxy(_config.proxyUrl!); - } - - // Add interceptors - _dio.interceptors.addAll([ - _cacheInterceptor, - _BandwidthTrackingInterceptor(), - _RetryInterceptor(), - ]); - } - - /// Configure native Dart HttpClient with proxy - void _configureProxy(String proxyUrl) { - try { - final uri = Uri.parse(proxyUrl); - _dio.httpClientAdapter = HttpClientAdapter( - onHttpClientCreate: (client) { - client.findProxy = (uri) { - return 'PROXY ${uri.host}:${uri.port}'; - }; - return client; - }, - ); - } catch (e) { - print('Error configuring proxy: $e'); - } - } - - /// Update network configuration - void updateConfig(NetworkConfig config) { - _config = config; - _initializeDio(); - } - - Dio get dio => _dio; - - /// Download file with resume support - Future downloadFile({ - required String url, - required String savePath, - required Function(int, int) onProgress, - CancelToken? cancelToken, - }) async { - try { - await _dio.download( - url, - savePath, - onReceiveProgress: onProgress, - cancelToken: cancelToken, - options: Options( - headers: {'Range': 'bytes=0-'}, - ), - ); - } catch (e) { - rethrow; - } - } - - /// Stream request with large response handling - Future streamRequest({ - required String url, - required Function(List) onData, - required VoidCallback onDone, - required Function(dynamic) onError, - CancelToken? cancelToken, - }) async { - try { - final response = await _dio.get( - url, - options: Options( - responseType: ResponseType.stream, - receiveTimeout: const Duration(minutes: 5), - ), - cancelToken: cancelToken, - ); - - final stream = response.data?.stream; - if (stream != null) { - stream.listen( - onData, - onError: onError, - onDone: onDone, - ); - } - } catch (e) { - onError(e); - } - } - - /// Clear cache - void clearCache() { - _cacheInterceptor.clearCache(); - } -} - -/// Interceptor for tracking bandwidth -class _BandwidthTrackingInterceptor extends Interceptor { - int _bytesReceived = 0; - DateTime _lastReset = DateTime.now(); - - int get bytesReceivedLastSecond { - final now = DateTime.now(); - final secondsElapsed = now.difference(_lastReset).inSeconds; - if (secondsElapsed > 1) { - final bps = _bytesReceived ~/ secondsElapsed; - _bytesReceived = 0; - _lastReset = now; - return bps; - } - return 0; - } - - @override - void onResponse(Response response, ResponseInterceptorHandler handler) { - if (response.data is ResponseBody) { - // Track bytes for streaming responses - } else if (response.data is List) { - _bytesReceived += (response.data as List).length; - } - handler.next(response); - } -} - -/// Interceptor for automatic retry on failure -class _RetryInterceptor extends Interceptor { - static const _maxRetries = 3; - - @override - Future onError( - DioException err, - ErrorInterceptorHandler handler, - ) async { - // Only retry on timeout or connection errors - if (err.type == DioExceptionType.connectionTimeout || - err.type == DioExceptionType.receiveTimeout) { - final requestOptions = err.requestOptions; - int retryCount = requestOptions.extra['retryCount'] ?? 0; - - if (retryCount < _maxRetries) { - requestOptions.extra['retryCount'] = retryCount + 1; - - // Exponential backoff: 100ms, 200ms, 400ms - final delay = Duration(milliseconds: 100 * (1 << retryCount)); - await Future.delayed(delay); - - final dio = Dio(); - try { - final response = await dio.request( - requestOptions.path, - options: Options( - method: requestOptions.method, - headers: requestOptions.headers, - ), - data: requestOptions.data, - queryParameters: requestOptions.queryParameters, - ); - return handler.resolve(response); - } on DioException catch (e) { - return handler.next(e); - } - } - } - - handler.next(err); - } -} - -// Riverpod providers - -final networkConfigProvider = - StateNotifierProvider((ref) { - return NetworkConfigNotifier(); -}); - -class NetworkConfigNotifier extends StateNotifier { - NetworkConfigNotifier() - : super( - const NetworkConfig( - userAgent: 'VLC/3.0.18 LibVLC/3.0.18', - ), - ); - - void setProxy(String? proxyUrl) { - state = state.copyWith(proxyUrl: proxyUrl); - } - - void setUserAgent(String userAgent) { - state = state.copyWith(userAgent: userAgent); - } - - void addCustomHeader(String key, String value) { - final newHeaders = {...state.customHeaders}; - newHeaders[key] = value; - state = state.copyWith(customHeaders: newHeaders); - } - - void removeCustomHeader(String key) { - final newHeaders = {...state.customHeaders}; - newHeaders.remove(key); - state = state.copyWith(customHeaders: newHeaders); - } - - void clearCustomHeaders() { - state = state.copyWith(customHeaders: {}); - } -} - -final networkServiceProvider = Provider((ref) { - final config = ref.watch(networkConfigProvider); - return OptimizedNetworkService(config: config); -}); diff --git a/lib/features/iptv/screens/dashboard_screen.dart b/lib/features/iptv/screens/dashboard_screen.dart index 3188acc..e07e4cb 100644 --- a/lib/features/iptv/screens/dashboard_screen.dart +++ b/lib/features/iptv/screens/dashboard_screen.dart @@ -124,12 +124,8 @@ class _DashboardScreenState extends ConsumerState { top: 24, bottom: 24, width: 80, - child: GlassContainer( + child: GlassContainer.glass( borderRadius: 24, - opacity: 0.1, - // blur is hardcoded in GlassContainer (15), so we don't pass it - // border is bool, borderColor allows customization - hasBorder: true, borderColor: AppColors.border, child: Column( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/features/iptv/screens/epg_grid_screen.dart b/lib/features/iptv/screens/epg_grid_screen.dart deleted file mode 100644 index bf733ba..0000000 --- a/lib/features/iptv/screens/epg_grid_screen.dart +++ /dev/null @@ -1,496 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:intl/intl.dart'; -import '../../../core/models/playlist_config.dart'; -import '../models/playlist.dart'; -import '../services/xtream_service.dart'; - -/// EPG Program information -class EpgProgram { - final String id; - final String channelId; - final String title; - final String description; - final DateTime startTime; - final DateTime endTime; - final String? imageUrl; - final bool isNow; - final bool isNext; - - EpgProgram({ - required this.id, - required this.channelId, - required this.title, - required this.description, - required this.startTime, - required this.endTime, - this.imageUrl, - this.isNow = false, - this.isNext = false, - }); - - Duration get duration => endTime.difference(startTime); - - double get progress { - if (!isNow) return 0; - final now = DateTime.now(); - if (now.isBefore(startTime)) return 0; - if (now.isAfter(endTime)) return 100; - return ((now.difference(startTime).inSeconds) / duration.inSeconds) * 100; - } -} - -/// EPG Grid View Screen (7 days, 24 hours) -class EpgGridScreen extends ConsumerStatefulWidget { - final Playlist playlist; - - const EpgGridScreen({ - super.key, - required this.playlist, - }); - - @override - ConsumerState createState() => _EpgGridScreenState(); -} - -class _EpgGridScreenState extends ConsumerState - with TickerProviderStateMixin { - late TabController _tabController; - final _horizontalScroll = ScrollController(); - final _verticalScroll = ScrollController(); - Map> _epgData = {}; - DateTime _selectedDate = DateTime.now(); - bool _isLoading = false; - - @override - void initState() { - super.initState(); - _tabController = TabController(length: 7, vsync: this); - WidgetsBinding.instance.addPostFrameCallback((_) { - _loadEpgData(); - }); - } - - @override - void dispose() { - _tabController.dispose(); - _horizontalScroll.dispose(); - _verticalScroll.dispose(); - super.dispose(); - } - - Future _loadEpgData() async { - if (_isLoading) return; - setState(() => _isLoading = true); - - try { - final service = ref.read(xtreamServiceProvider(widget.playlist)); - final channels = await service.getLiveChannels(); - - final epgMap = >{}; - - // Load EPG for each channel - for (final channel in channels) { - try { - final epg = await service.getFullEpg(channel.streamId.toString()); - final programs = _parseEpgPrograms(epg, channel.streamId.toString()); - epgMap[channel.streamId.toString()] = programs; - } catch (e) { - print('Error loading EPG for ${channel.name}: $e'); - epgMap[channel.streamId.toString()] = []; - } - } - - setState(() { - _epgData = epgMap; - _isLoading = false; - }); - } catch (e) { - print('Error loading EPG: $e'); - setState(() => _isLoading = false); - } - } - - List _parseEpgPrograms( - Map epgData, - String channelId, - ) { - final programs = []; - final now = DateTime.now(); - - // Parse EPG data structure - if (epgData.containsKey('epg_listings')) { - final listings = epgData['epg_listings'] as List; - for (final item in listings) { - try { - final startStr = item['start'] as String?; - final endStr = item['end'] as String?; - final title = item['title'] as String?; - final desc = item['description'] as String? ?? ''; - - if (startStr != null && endStr != null && title != null) { - final start = _parseEpgTime(startStr); - final end = _parseEpgTime(endStr); - - programs.add( - EpgProgram( - id: '${channelId}_${start.millisecondsSinceEpoch}', - channelId: channelId, - title: title, - description: desc, - startTime: start, - endTime: end, - isNow: now.isAfter(start) && now.isBefore(end), - isNext: - start.isAfter(now) && start.difference(now).inMinutes < 60, - ), - ); - } - } catch (e) { - continue; - } - } - } - - // Sort by start time - programs.sort((a, b) => a.startTime.compareTo(b.startTime)); - return programs; - } - - DateTime _parseEpgTime(String timeStr) { - try { - // Handle various formats: "12:30", "12:30:00", unix timestamp - if (timeStr.contains(':')) { - final parts = timeStr.split(':'); - final hours = int.parse(parts[0]); - final minutes = int.parse(parts[1]); - final today = DateTime.now(); - return DateTime(today.year, today.month, today.day, hours, minutes); - } else { - return DateTime.fromMillisecondsSinceEpoch(int.parse(timeStr) * 1000); - } - } catch (e) { - return DateTime.now(); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('EPG Guide'), - bottom: TabBar( - controller: _tabController, - isScrollable: true, - onTap: (index) { - setState(() { - _selectedDate = DateTime.now().add(Duration(days: index)); - }); - }, - tabs: List.generate(7, (index) { - final date = DateTime.now().add(Duration(days: index)); - final day = DateFormat('EEE').format(date); - final dateNum = DateFormat('d').format(date); - return Tab( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(day, style: const TextStyle(fontSize: 11)), - Text( - dateNum, - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ); - }), - ), - ), - body: _isLoading - ? const Center(child: CircularProgressIndicator()) - : _buildEpgGrid(), - ); - } - - Widget _buildEpgGrid() { - if (_epgData.isEmpty) { - return const Center( - child: Text('No EPG data available'), - ); - } - - final channels = _epgData.keys.toList(); - - return Scrollbar( - controller: _horizontalScroll, - child: SingleChildScrollView( - controller: _horizontalScroll, - scrollDirection: Axis.horizontal, - child: Column( - children: [ - // Time headers - SizedBox( - height: 60, - child: Row( - children: [ - SizedBox( - width: 150, - child: Container( - decoration: BoxDecoration( - border: Border( - right: BorderSide(color: Colors.grey[700]!), - bottom: BorderSide(color: Colors.grey[700]!), - ), - ), - ), - ), - ...List.generate(24, (hour) { - return Container( - width: 100, - height: 60, - decoration: BoxDecoration( - border: Border( - right: BorderSide(color: Colors.grey[700]!), - bottom: BorderSide(color: Colors.grey[700]!), - ), - ), - child: Center( - child: Text( - '$hour:00', - style: const TextStyle( - fontSize: 11, - fontWeight: FontWeight.bold, - ), - ), - ), - ); - }), - ], - ), - ), - - // Channel rows - Expanded( - child: Scrollbar( - controller: _verticalScroll, - child: ListView.builder( - controller: _verticalScroll, - itemCount: channels.length, - itemBuilder: (context, index) { - final channelId = channels[index]; - final programs = _epgData[channelId] ?? []; - - return _EpgChannelRow( - channelId: channelId, - programs: programs, - onProgramTap: (program) { - _showProgramDetail(program); - }, - ); - }, - ), - ), - ), - ], - ), - ), - ); - } - - void _showProgramDetail(EpgProgram program) { - showModalBottomSheet( - context: context, - builder: (context) => _ProgramDetailSheet(program: program), - ); - } -} - -/// Individual channel row in EPG grid -class _EpgChannelRow extends StatelessWidget { - final String channelId; - final List programs; - final Function(EpgProgram) onProgramTap; - - const _EpgChannelRow({ - required this.channelId, - required this.programs, - required this.onProgramTap, - }); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - // Channel name - Container( - width: 150, - height: 100, - decoration: BoxDecoration( - border: Border( - right: BorderSide(color: Colors.grey[700]!), - bottom: BorderSide(color: Colors.grey[700]!), - ), - ), - child: Padding( - padding: const EdgeInsets.all(8), - child: Text( - channelId, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500), - ), - ), - ), - - // Programs - ...programs.map((program) { - final duration = program.duration.inMinutes; - final width = - (duration / 60 * 100).clamp(40, double.infinity).toDouble(); - - return GestureDetector( - onTap: () => onProgramTap(program), - child: Container( - width: width, - height: 100, - decoration: BoxDecoration( - color: program.isNow - ? Colors.blue.withOpacity(0.7) - : program.isNext - ? Colors.purple.withOpacity(0.5) - : Colors.grey[800], - border: Border( - right: BorderSide(color: Colors.grey[700]!), - bottom: BorderSide(color: Colors.grey[700]!), - ), - ), - child: Stack( - fit: StackFit.expand, - children: [ - // Program info - Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - program.title, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 2), - Text( - '${DateFormat('HH:mm').format(program.startTime)} - ${DateFormat('HH:mm').format(program.endTime)}', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 8), - ), - ], - ), - ), - - // Progress bar for now playing - if (program.isNow) - Positioned( - bottom: 0, - left: 0, - right: 0, - child: ClipRRect( - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(4), - bottomRight: Radius.circular(4), - ), - child: LinearProgressIndicator( - value: program.progress / 100, - minHeight: 3, - backgroundColor: Colors.white24, - valueColor: const AlwaysStoppedAnimation(Colors.red), - ), - ), - ), - ], - ), - ), - ); - }), - ], - ); - } -} - -/// Program detail bottom sheet -class _ProgramDetailSheet extends StatelessWidget { - final EpgProgram program; - - const _ProgramDetailSheet({required this.program}); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - program.title, - style: const TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Row( - children: [ - Icon( - program.isNow ? Icons.play_circle : Icons.schedule, - color: program.isNow ? Colors.green : Colors.orange, - size: 16, - ), - const SizedBox(width: 8), - Text( - '${DateFormat('HH:mm').format(program.startTime)} - ${DateFormat('HH:mm').format(program.endTime)}', - style: const TextStyle(fontSize: 14, color: Colors.grey), - ), - const SizedBox(width: 16), - Text( - '${program.duration.inMinutes} min', - style: const TextStyle(fontSize: 14, color: Colors.grey), - ), - ], - ), - const SizedBox(height: 16), - Text( - program.description.isEmpty - ? 'No description available' - : program.description, - style: const TextStyle(fontSize: 14), - ), - const SizedBox(height: 24), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - ElevatedButton.icon( - onPressed: () => Navigator.pop(context), - icon: const Icon(Icons.play_arrow), - label: const Text('Watch Now'), - ), - ElevatedButton.icon( - onPressed: () => Navigator.pop(context), - icon: const Icon(Icons.event), - label: const Text('Add Reminder'), - ), - ], - ), - ], - ), - ); - } -} diff --git a/lib/features/iptv/screens/player_screen.dart b/lib/features/iptv/screens/player_screen.dart index 7cb1e48..9562b42 100644 --- a/lib/features/iptv/screens/player_screen.dart +++ b/lib/features/iptv/screens/player_screen.dart @@ -598,10 +598,9 @@ class _PlayerScreenState extends ConsumerState { top: 24, left: 24, right: 24, - child: GlassContainer( + child: GlassContainer.glass( height: 72, borderRadius: 24, - opacity: 0.1, padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ @@ -638,10 +637,8 @@ class _PlayerScreenState extends ConsumerState { bottom: 40, left: 40, right: 40, - child: GlassContainer( + child: GlassContainer.glass( borderRadius: 24, - opacity: 0.2, // Slightly more opaque for controls - hasBorder: true, borderColor: Colors.white.withOpacity(0.1), padding: const EdgeInsets.all(24), child: Column(