mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-23 23:18:06 +02:00
feat: introduce mobile player screen and lite player view with channel zapping functionality
This commit is contained in:
1 parent
4d1e08148b
commit
33c6385d7e
6 files changed
+90
-18
No files matched your search
@@ -75,5 +75,28 @@ class PlaylistConfig extends Equatable {
|
|||||||
createdAt,
|
createdAt,
|
||||||
isActive,
|
isActive,
|
||||||
];
|
];
|
||||||
}
|
|
||||||
|
|
||||||
|
factory PlaylistConfig.fromJson(Map<String, dynamic> 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<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'name': name,
|
||||||
|
'dns': dns,
|
||||||
|
'username': username,
|
||||||
|
'password': password,
|
||||||
|
'createdAt': createdAt.toIso8601String(),
|
||||||
|
'isActive': isActive,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,11 +6,10 @@ import '../../features/auth/screens/login_screen.dart';
|
|||||||
import '../../features/iptv/screens/playlist_selection_screen.dart';
|
import '../../features/iptv/screens/playlist_selection_screen.dart';
|
||||||
import '../../features/iptv/screens/dashboard_screen.dart';
|
import '../../features/iptv/screens/dashboard_screen.dart';
|
||||||
import '../../features/admin/screens/admin_panel.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_playlist_selection_screen.dart';
|
||||||
import '../../mobile/features/iptv/screens/mobile_dashboard_screen.dart';
|
import '../../mobile/features/iptv/screens/mobile_dashboard_screen.dart';
|
||||||
import '../widgets/themed_loading_screen.dart';
|
import '../widgets/themed_loading_screen.dart';
|
||||||
|
import '../../features/iptv/providers/xtream_provider.dart';
|
||||||
|
|
||||||
final routerProvider = Provider<GoRouter>((ref) {
|
final routerProvider = Provider<GoRouter>((ref) {
|
||||||
final authState = ref.watch(authProvider);
|
final authState = ref.watch(authProvider);
|
||||||
@@ -42,6 +41,14 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
return '/playlists';
|
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;
|
return null;
|
||||||
},
|
},
|
||||||
routes: [
|
routes: [
|
||||||
@@ -66,18 +73,17 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/dashboard',
|
path: '/dashboard',
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final playlist = state.extra as PlaylistConfig?;
|
final playlist = ref.watch(selectedPlaylistProvider);
|
||||||
if (playlist == null) {
|
if (playlist == null) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
context.go('/playlists');
|
|
||||||
});
|
|
||||||
return const Scaffold(
|
return const Scaffold(
|
||||||
body: ThemedLoading(),
|
body: ThemedLoading(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MediaQuery.of(context).size.width < 768) {
|
// Adaptive layout without forcing a router rebuild on every pixel change
|
||||||
return MobileDashboardScreen(playlist: playlist);
|
final isMobile = MediaQuery.sizeOf(context).width < 768;
|
||||||
|
if (isMobile) {
|
||||||
|
return MobileDashboardScreen(playlist: playlist);
|
||||||
}
|
}
|
||||||
return DashboardScreen(playlist: playlist);
|
return DashboardScreen(playlist: playlist);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../../../core/models/playlist_config.dart';
|
import '../../../core/models/playlist_config.dart';
|
||||||
import '../services/xtream_service.dart';
|
import '../services/xtream_service.dart';
|
||||||
@@ -23,14 +26,40 @@ final _singletonXtreamServiceProvider = Provider<XtreamService>((ref) {
|
|||||||
return service;
|
return service;
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Current selected playlist provider
|
/// Current selected playlist provider with persistence for Web
|
||||||
final selectedPlaylistProvider = StateProvider<PlaylistConfig?>((ref) => null);
|
final selectedPlaylistProvider = StateProvider<PlaylistConfig?>((ref) {
|
||||||
|
// Restore from localStorage if on web
|
||||||
|
try {
|
||||||
|
final stored = html.window.localStorage['selected_playlist'];
|
||||||
|
if (stored != null) {
|
||||||
|
final data = jsonDecode(stored) as Map<String, dynamic>;
|
||||||
|
return PlaylistConfig.fromJson(data);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error restoring playlist: $e');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listener to save selected playlist
|
||||||
|
final playlistPersistenceProvider = Provider<void>((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
|
/// Provider that watches for playlist changes and updates Xtream service
|
||||||
final activeXtreamServiceProvider = Provider<XtreamService>((ref) {
|
final activeXtreamServiceProvider = Provider<XtreamService>((ref) {
|
||||||
final service = ref.watch(_singletonXtreamServiceProvider);
|
final service = ref.watch(_singletonXtreamServiceProvider);
|
||||||
final playlist = ref.watch(selectedPlaylistProvider);
|
final playlist = ref.watch(selectedPlaylistProvider);
|
||||||
|
|
||||||
|
// Ensure persistence listener is active
|
||||||
|
ref.read(playlistPersistenceProvider);
|
||||||
|
|
||||||
if (playlist != null) {
|
if (playlist != null) {
|
||||||
service.setPlaylist(playlist);
|
service.setPlaylist(playlist);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ class _LitePlayerViewState extends ConsumerState<LitePlayerView> {
|
|||||||
allowFullScreen: true,
|
allowFullScreen: true,
|
||||||
allowMuting: true,
|
allowMuting: true,
|
||||||
showControls: true,
|
showControls: true,
|
||||||
|
useRootNavigator: true, // Crucial for escaping layout constraints
|
||||||
|
|
||||||
// Orientation settings are often ignored or cause issues on web
|
// Orientation settings are often ignored or cause issues on web
|
||||||
// but we keep them for native mobile if needed.
|
// but we keep them for native mobile if needed.
|
||||||
@@ -173,9 +174,13 @@ class _LitePlayerViewState extends ConsumerState<LitePlayerView> {
|
|||||||
// Fullscreen Toggle
|
// Fullscreen Toggle
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
// For Web, ensure we use the built-in fullscreen transition
|
||||||
|
// which is more compatible with Safari/Chrome security
|
||||||
if (_chewieController?.isFullScreen == true) {
|
if (_chewieController?.isFullScreen == true) {
|
||||||
_chewieController?.exitFullScreen();
|
_chewieController?.exitFullScreen();
|
||||||
} else {
|
} else {
|
||||||
|
// On web, sometimes we need to toggle more than once
|
||||||
|
// to ensure the browser captures the intent
|
||||||
_chewieController?.enterFullScreen();
|
_chewieController?.enterFullScreen();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ class MobilePlayerScreen extends ConsumerWidget {
|
|||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: Center(
|
body: SizedBox.expand(
|
||||||
child: LitePlayerView(
|
child: LitePlayerView(
|
||||||
streamUrl: streamUrl,
|
streamUrl: streamUrl,
|
||||||
isLive: streamType == StreamType.live,
|
isLive: streamType == StreamType.live,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import '../../../../features/iptv/providers/xtream_provider.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import '../../../../core/services/playlist_api_service.dart';
|
import '../../../../core/services/playlist_api_service.dart';
|
||||||
import '../../../../core/models/playlist_config.dart';
|
import '../../../../core/models/playlist_config.dart';
|
||||||
@@ -44,7 +45,8 @@ class MobilePlaylistSelectionScreen extends ConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
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),
|
const SizedBox(height: 16),
|
||||||
Text('Error loading playlists', style: GoogleFonts.inter()),
|
Text('Error loading playlists', style: GoogleFonts.inter()),
|
||||||
TextButton(
|
TextButton(
|
||||||
@@ -60,9 +62,12 @@ class MobilePlaylistSelectionScreen extends ConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
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),
|
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(
|
return _MobilePlaylistCard(
|
||||||
playlist: playlist,
|
playlist: playlist,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// Mobile navigation to dashboard with playlist
|
// Update provider which will be persisted
|
||||||
context.go('/dashboard', extra: playlist);
|
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),
|
color: AppColors.primary.withOpacity(0.2),
|
||||||
borderRadius: BorderRadius.circular(12),
|
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),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|||||||
Reference in new issue
Block a user