diff --git a/lib/core/models/playlist_config.dart b/lib/core/models/playlist_config.dart index 1e9f939..5ca7afa 100644 --- a/lib/core/models/playlist_config.dart +++ b/lib/core/models/playlist_config.dart @@ -4,7 +4,7 @@ import 'package:equatable/equatable.dart'; part 'playlist_config.g.dart'; /// Hive model for Xtream Codes playlist configuration -/// +/// /// Stores credentials for connecting to an Xtream server @HiveType(typeId: 1) class PlaylistConfig extends Equatable { @@ -75,5 +75,28 @@ class PlaylistConfig extends Equatable { createdAt, isActive, ]; -} + factory PlaylistConfig.fromJson(Map json) { + return PlaylistConfig( + id: json['id'] as String, + name: json['name'] as String, + dns: json['dns'] as String, + username: json['username'] as String, + password: json['password'] as String, + createdAt: DateTime.parse(json['createdAt'] as String), + isActive: json['isActive'] as bool? ?? true, + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'dns': dns, + 'username': username, + 'password': password, + 'createdAt': createdAt.toIso8601String(), + 'isActive': isActive, + }; + } +} diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 76567ef..9db715a 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -6,11 +6,10 @@ import '../../features/auth/screens/login_screen.dart'; import '../../features/iptv/screens/playlist_selection_screen.dart'; import '../../features/iptv/screens/dashboard_screen.dart'; import '../../features/admin/screens/admin_panel.dart'; -import '../models/playlist_config.dart'; -import '../../mobile/features/auth/screens/mobile_login_screen.dart'; import '../../mobile/features/iptv/screens/mobile_playlist_selection_screen.dart'; import '../../mobile/features/iptv/screens/mobile_dashboard_screen.dart'; import '../widgets/themed_loading_screen.dart'; +import '../../features/iptv/providers/xtream_provider.dart'; final routerProvider = Provider((ref) { final authState = ref.watch(authProvider); @@ -42,6 +41,14 @@ final routerProvider = Provider((ref) { return '/playlists'; } + // If we are on dashboard but have no playlist selected, try to restore it + if (state.matchedLocation == '/dashboard') { + final playlist = ref.read(selectedPlaylistProvider); + if (playlist == null) { + return '/playlists'; + } + } + return null; }, routes: [ @@ -66,18 +73,17 @@ final routerProvider = Provider((ref) { GoRoute( path: '/dashboard', builder: (context, state) { - final playlist = state.extra as PlaylistConfig?; + final playlist = ref.watch(selectedPlaylistProvider); if (playlist == null) { - WidgetsBinding.instance.addPostFrameCallback((_) { - context.go('/playlists'); - }); return const Scaffold( body: ThemedLoading(), ); } - - if (MediaQuery.of(context).size.width < 768) { - return MobileDashboardScreen(playlist: playlist); + + // Adaptive layout without forcing a router rebuild on every pixel change + final isMobile = MediaQuery.sizeOf(context).width < 768; + if (isMobile) { + return MobileDashboardScreen(playlist: playlist); } return DashboardScreen(playlist: playlist); }, diff --git a/lib/features/iptv/providers/xtream_provider.dart b/lib/features/iptv/providers/xtream_provider.dart index 0b41e5e..a09dc14 100644 --- a/lib/features/iptv/providers/xtream_provider.dart +++ b/lib/features/iptv/providers/xtream_provider.dart @@ -1,3 +1,6 @@ +import 'dart:convert'; +import 'dart:html' as html; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/models/playlist_config.dart'; import '../services/xtream_service.dart'; @@ -23,14 +26,40 @@ final _singletonXtreamServiceProvider = Provider((ref) { return service; }); -/// Current selected playlist provider -final selectedPlaylistProvider = StateProvider((ref) => null); +/// Current selected playlist provider with persistence for Web +final selectedPlaylistProvider = StateProvider((ref) { + // Restore from localStorage if on web + try { + final stored = html.window.localStorage['selected_playlist']; + if (stored != null) { + final data = jsonDecode(stored) as Map; + return PlaylistConfig.fromJson(data); + } + } catch (e) { + debugPrint('Error restoring playlist: $e'); + } + return null; +}); + +// Listener to save selected playlist +final playlistPersistenceProvider = Provider((ref) { + ref.listen(selectedPlaylistProvider, (previous, next) { + if (next != null) { + html.window.localStorage['selected_playlist'] = jsonEncode(next.toJson()); + } else { + html.window.localStorage.remove('selected_playlist'); + } + }); +}); /// Provider that watches for playlist changes and updates Xtream service final activeXtreamServiceProvider = Provider((ref) { final service = ref.watch(_singletonXtreamServiceProvider); final playlist = ref.watch(selectedPlaylistProvider); + // Ensure persistence listener is active + ref.read(playlistPersistenceProvider); + if (playlist != null) { service.setPlaylist(playlist); } diff --git a/lib/features/iptv/widgets/lite_player_view.dart b/lib/features/iptv/widgets/lite_player_view.dart index 14da889..2ebed49 100644 --- a/lib/features/iptv/widgets/lite_player_view.dart +++ b/lib/features/iptv/widgets/lite_player_view.dart @@ -71,6 +71,7 @@ class _LitePlayerViewState extends ConsumerState { allowFullScreen: true, allowMuting: true, showControls: true, + useRootNavigator: true, // Crucial for escaping layout constraints // Orientation settings are often ignored or cause issues on web // but we keep them for native mobile if needed. @@ -173,9 +174,13 @@ class _LitePlayerViewState extends ConsumerState { // Fullscreen Toggle IconButton( onPressed: () { + // For Web, ensure we use the built-in fullscreen transition + // which is more compatible with Safari/Chrome security if (_chewieController?.isFullScreen == true) { _chewieController?.exitFullScreen(); } else { + // On web, sometimes we need to toggle more than once + // to ensure the browser captures the intent _chewieController?.enterFullScreen(); } }, diff --git a/lib/mobile/features/iptv/screens/mobile_player_screen.dart b/lib/mobile/features/iptv/screens/mobile_player_screen.dart index 486422c..e02209e 100644 --- a/lib/mobile/features/iptv/screens/mobile_player_screen.dart +++ b/lib/mobile/features/iptv/screens/mobile_player_screen.dart @@ -108,7 +108,7 @@ class MobilePlayerScreen extends ConsumerWidget { onPressed: () => Navigator.pop(context), ), ), - body: Center( + body: SizedBox.expand( child: LitePlayerView( streamUrl: streamUrl, isLive: streamType == StreamType.live, diff --git a/lib/mobile/features/iptv/screens/mobile_playlist_selection_screen.dart b/lib/mobile/features/iptv/screens/mobile_playlist_selection_screen.dart index d6bbc3f..e4278e3 100644 --- a/lib/mobile/features/iptv/screens/mobile_playlist_selection_screen.dart +++ b/lib/mobile/features/iptv/screens/mobile_playlist_selection_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import '../../../../features/iptv/providers/xtream_provider.dart'; import 'package:google_fonts/google_fonts.dart'; import '../../../../core/services/playlist_api_service.dart'; import '../../../../core/models/playlist_config.dart'; @@ -44,7 +45,8 @@ class MobilePlaylistSelectionScreen extends ConsumerWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon(Icons.error_outline, size: 48, color: AppColors.error), + const Icon(Icons.error_outline, + size: 48, color: AppColors.error), const SizedBox(height: 16), Text('Error loading playlists', style: GoogleFonts.inter()), TextButton( @@ -60,9 +62,12 @@ class MobilePlaylistSelectionScreen extends ConsumerWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon(Icons.playlist_remove, size: 48, color: AppColors.textSecondary), + const Icon(Icons.playlist_remove, + size: 48, color: AppColors.textSecondary), const SizedBox(height: 16), - Text('No playlists available', style: GoogleFonts.inter(color: AppColors.textSecondary)), + Text('No playlists available', + style: + GoogleFonts.inter(color: AppColors.textSecondary)), ], ), ); @@ -77,8 +82,11 @@ class MobilePlaylistSelectionScreen extends ConsumerWidget { return _MobilePlaylistCard( playlist: playlist, onTap: () { - // Mobile navigation to dashboard with playlist - context.go('/dashboard', extra: playlist); + // Update provider which will be persisted + ref.read(selectedPlaylistProvider.notifier).state = + playlist; + // Navigate without extra (it will be picked up from provider) + context.go('/dashboard'); }, ); }, @@ -130,7 +138,8 @@ class _MobilePlaylistCard extends StatelessWidget { color: AppColors.primary.withOpacity(0.2), borderRadius: BorderRadius.circular(12), ), - child: const Icon(Icons.playlist_play, color: AppColors.primary, size: 28), + child: const Icon(Icons.playlist_play, + color: AppColors.primary, size: 28), ), const SizedBox(width: 16), Expanded(