mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-24 15:48:14 +02:00
feat: Add initial mobile dashboard screen with Xtream data and UI state providers.
This commit is contained in:
1 parent
3145af9e8d
commit
b249074db1
30 files changed
+1217
-582
No files matched your search
@@ -183,11 +183,13 @@ class _PlaylistsTab extends ConsumerWidget {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('${playlistsAsync.asData?.value.length ?? 0} Playlists',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge
|
||||
?.copyWith(color: Colors.white70)),
|
||||
Text(
|
||||
'${playlistsAsync.asData?.value.length ?? 0} Playlists',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge
|
||||
?.copyWith(color: Colors.white70),
|
||||
),
|
||||
GradientButton(
|
||||
label: 'Add Playlist',
|
||||
icon: Icons.add,
|
||||
@@ -200,20 +202,27 @@ class _PlaylistsTab extends ConsumerWidget {
|
||||
child: playlistsAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, stack) => Center(
|
||||
child: Text('Error: $error',
|
||||
style: const TextStyle(color: AppColors.error))),
|
||||
child: Text(
|
||||
'Error: $error',
|
||||
style: const TextStyle(color: AppColors.error),
|
||||
),
|
||||
),
|
||||
data: (playlists) {
|
||||
if (playlists.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.playlist_remove,
|
||||
size: 64,
|
||||
color: AppColors.textSecondary.withOpacity(0.5)),
|
||||
Icon(
|
||||
Icons.playlist_remove,
|
||||
size: 64,
|
||||
color: AppColors.textSecondary.withOpacity(0.5),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('No playlists configured',
|
||||
style: Theme.of(context).textTheme.titleMedium),
|
||||
Text(
|
||||
'No playlists configured',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed: () => _showPlaylistDialog(context, ref),
|
||||
@@ -242,25 +251,31 @@ class _PlaylistsTab extends ConsumerWidget {
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(Icons.playlist_play,
|
||||
color: AppColors.textPrimary),
|
||||
child: const Icon(
|
||||
Icons.playlist_play,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(playlist.name,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(color: Colors.white)),
|
||||
Text(
|
||||
playlist.name,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(color: Colors.white),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(playlist.dns,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: Colors.white60)),
|
||||
Text(
|
||||
playlist.dns,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: Colors.white60),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -294,8 +309,11 @@ class _PlaylistsTab extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _showPlaylistDialog(BuildContext context, WidgetRef ref,
|
||||
[PlaylistConfig? playlist]) {
|
||||
void _showPlaylistDialog(
|
||||
BuildContext context,
|
||||
WidgetRef ref, [
|
||||
PlaylistConfig? playlist,
|
||||
]) {
|
||||
final nameController = TextEditingController(text: playlist?.name);
|
||||
final dnsController = TextEditingController(text: playlist?.dns);
|
||||
final usernameController = TextEditingController(text: playlist?.username);
|
||||
@@ -307,41 +325,44 @@ class _PlaylistsTab extends ConsumerWidget {
|
||||
context: context,
|
||||
builder: (dialogContext) => StatefulBuilder(
|
||||
builder: (context, setState) => AlertDialog(
|
||||
title: Text(playlist == null
|
||||
? 'Ajouter une playlist'
|
||||
: 'Modifier la playlist'),
|
||||
title: Text(
|
||||
playlist == null ? 'Ajouter une playlist' : 'Modifier la playlist',
|
||||
),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: nameController,
|
||||
decoration: const InputDecoration(labelText: 'Nom')),
|
||||
controller: nameController,
|
||||
decoration: const InputDecoration(labelText: 'Nom'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: dnsController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'URL du serveur',
|
||||
hintText: 'http://server.com:8080')),
|
||||
controller: dnsController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'URL du serveur',
|
||||
hintText: 'http://server.com:8080',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: usernameController,
|
||||
decoration:
|
||||
const InputDecoration(labelText: 'Utilisateur')),
|
||||
controller: usernameController,
|
||||
decoration: const InputDecoration(labelText: 'Utilisateur'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: passwordController,
|
||||
decoration:
|
||||
const InputDecoration(labelText: 'Mot de passe'),
|
||||
obscureText: true),
|
||||
controller: passwordController,
|
||||
decoration: const InputDecoration(labelText: 'Mot de passe'),
|
||||
obscureText: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed:
|
||||
isLoading ? null : () => Navigator.pop(dialogContext),
|
||||
child: const Text('Annuler')),
|
||||
onPressed: isLoading ? null : () => Navigator.pop(dialogContext),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: isLoading
|
||||
? null
|
||||
@@ -349,8 +370,10 @@ class _PlaylistsTab extends ConsumerWidget {
|
||||
if (nameController.text.isEmpty ||
|
||||
dnsController.text.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Tous les champs sont requis')));
|
||||
const SnackBar(
|
||||
content: Text('Tous les champs sont requis'),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
setState(() => isLoading = true);
|
||||
@@ -359,17 +382,19 @@ class _PlaylistsTab extends ConsumerWidget {
|
||||
.replaceAll(RegExp(r'/$'), '');
|
||||
if (playlist == null) {
|
||||
await service.createPlaylist(
|
||||
name: nameController.text.trim(),
|
||||
dns: dns,
|
||||
username: usernameController.text.trim(),
|
||||
password: passwordController.text);
|
||||
name: nameController.text.trim(),
|
||||
dns: dns,
|
||||
username: usernameController.text.trim(),
|
||||
password: passwordController.text,
|
||||
);
|
||||
} else {
|
||||
await service.updatePlaylist(
|
||||
id: playlist.id,
|
||||
name: nameController.text.trim(),
|
||||
dns: dns,
|
||||
username: usernameController.text.trim(),
|
||||
password: passwordController.text);
|
||||
id: playlist.id,
|
||||
name: nameController.text.trim(),
|
||||
dns: dns,
|
||||
username: usernameController.text.trim(),
|
||||
password: passwordController.text,
|
||||
);
|
||||
}
|
||||
if (dialogContext.mounted) Navigator.pop(dialogContext);
|
||||
ref.invalidate(playlistsProvider);
|
||||
@@ -378,7 +403,8 @@ class _PlaylistsTab extends ConsumerWidget {
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2))
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('Sauvegarder'),
|
||||
),
|
||||
],
|
||||
@@ -388,7 +414,10 @@ class _PlaylistsTab extends ConsumerWidget {
|
||||
}
|
||||
|
||||
void _deletePlaylist(
|
||||
BuildContext context, WidgetRef ref, PlaylistConfig playlist) {
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
PlaylistConfig playlist,
|
||||
) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
@@ -396,8 +425,9 @@ class _PlaylistsTab extends ConsumerWidget {
|
||||
content: Text('Supprimer "${playlist.name}" ?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: const Text('Annuler')),
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(dialogContext);
|
||||
@@ -428,11 +458,13 @@ class _UsersTab extends ConsumerWidget {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('${usersAsync.asData?.value.length ?? 0} Users',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge
|
||||
?.copyWith(color: Colors.white70)),
|
||||
Text(
|
||||
'${usersAsync.asData?.value.length ?? 0} Users',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge
|
||||
?.copyWith(color: Colors.white70),
|
||||
),
|
||||
GradientButton(
|
||||
label: 'Create User',
|
||||
icon: Icons.person_add,
|
||||
@@ -445,20 +477,27 @@ class _UsersTab extends ConsumerWidget {
|
||||
child: usersAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, stack) => Center(
|
||||
child: Text('Error: $error',
|
||||
style: const TextStyle(color: AppColors.error))),
|
||||
child: Text(
|
||||
'Error: $error',
|
||||
style: const TextStyle(color: AppColors.error),
|
||||
),
|
||||
),
|
||||
data: (users) {
|
||||
if (users.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.people_outline,
|
||||
size: 64,
|
||||
color: AppColors.textSecondary.withOpacity(0.5)),
|
||||
Icon(
|
||||
Icons.people_outline,
|
||||
size: 64,
|
||||
color: AppColors.textSecondary.withOpacity(0.5),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('No users found',
|
||||
style: Theme.of(context).textTheme.titleMedium),
|
||||
Text(
|
||||
'No users found',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -501,45 +540,57 @@ class _UsersTab extends ConsumerWidget {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(user.username,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(color: Colors.white)),
|
||||
Text(
|
||||
user.username,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(color: Colors.white),
|
||||
),
|
||||
if (user.isAdmin) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 2),
|
||||
horizontal: 8,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.focusColor
|
||||
.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
child: const Text('ADMIN',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.focusColor)),
|
||||
child: const Text(
|
||||
'ADMIN',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.focusColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text('ID: ${user.id}',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: Colors.white60)),
|
||||
Text(
|
||||
'ID: ${user.id}',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: Colors.white60),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert,
|
||||
color: Colors.white70),
|
||||
icon: const Icon(
|
||||
Icons.more_vert,
|
||||
color: Colors.white70,
|
||||
),
|
||||
color: AppColors.surface,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
onSelected: (value) {
|
||||
switch (value) {
|
||||
case 'password':
|
||||
@@ -552,21 +603,32 @@ class _UsersTab extends ConsumerWidget {
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(
|
||||
value: 'password',
|
||||
child: Row(children: [
|
||||
value: 'password',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.lock_reset, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text('Change Password')
|
||||
])),
|
||||
Text('Change Password'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(children: [
|
||||
Icon(Icons.delete,
|
||||
color: AppColors.error, size: 18),
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.delete,
|
||||
color: AppColors.error,
|
||||
size: 18,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text('Delete',
|
||||
style: TextStyle(color: AppColors.error))
|
||||
])),
|
||||
Text(
|
||||
'Delete',
|
||||
style: TextStyle(color: AppColors.error),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -596,17 +658,21 @@ class _UsersTab extends ConsumerWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: usernameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nom d\'utilisateur',
|
||||
prefixIcon: Icon(Icons.person_outline))),
|
||||
controller: usernameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nom d\'utilisateur',
|
||||
prefixIcon: Icon(Icons.person_outline),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: passwordController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Mot de passe',
|
||||
prefixIcon: Icon(Icons.lock_outline)),
|
||||
obscureText: true),
|
||||
controller: passwordController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Mot de passe',
|
||||
prefixIcon: Icon(Icons.lock_outline),
|
||||
),
|
||||
obscureText: true,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SwitchListTile(
|
||||
title: const Text('Administrateur'),
|
||||
@@ -618,9 +684,9 @@ class _UsersTab extends ConsumerWidget {
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed:
|
||||
isLoading ? null : () => Navigator.pop(dialogContext),
|
||||
child: const Text('Annuler')),
|
||||
onPressed: isLoading ? null : () => Navigator.pop(dialogContext),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: isLoading
|
||||
? null
|
||||
@@ -628,28 +694,35 @@ class _UsersTab extends ConsumerWidget {
|
||||
if (usernameController.text.isEmpty ||
|
||||
passwordController.text.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Tous les champs sont requis')));
|
||||
const SnackBar(
|
||||
content: Text('Tous les champs sont requis'),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
setState(() => isLoading = true);
|
||||
final result = await UserApiService().createUser(
|
||||
username: usernameController.text.trim(),
|
||||
password: passwordController.text,
|
||||
isAdmin: isAdmin);
|
||||
username: usernameController.text.trim(),
|
||||
password: passwordController.text,
|
||||
isAdmin: isAdmin,
|
||||
);
|
||||
if (dialogContext.mounted) Navigator.pop(dialogContext);
|
||||
ref.invalidate(usersProvider);
|
||||
if (!result.success && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(result.error ?? 'Erreur'),
|
||||
backgroundColor: AppColors.error));
|
||||
backgroundColor: AppColors.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2))
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('Créer'),
|
||||
),
|
||||
],
|
||||
@@ -659,7 +732,10 @@ class _UsersTab extends ConsumerWidget {
|
||||
}
|
||||
|
||||
void _showChangePasswordDialog(
|
||||
BuildContext context, WidgetRef ref, AppUser user) {
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
AppUser user,
|
||||
) {
|
||||
final passwordController = TextEditingController();
|
||||
bool isLoading = false;
|
||||
|
||||
@@ -669,47 +745,58 @@ class _UsersTab extends ConsumerWidget {
|
||||
builder: (context, setState) => AlertDialog(
|
||||
title: Text('Changer le mot de passe de ${user.username}'),
|
||||
content: TextField(
|
||||
controller: passwordController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nouveau mot de passe',
|
||||
prefixIcon: Icon(Icons.lock_outline)),
|
||||
obscureText: true),
|
||||
controller: passwordController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nouveau mot de passe',
|
||||
prefixIcon: Icon(Icons.lock_outline),
|
||||
),
|
||||
obscureText: true,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed:
|
||||
isLoading ? null : () => Navigator.pop(dialogContext),
|
||||
child: const Text('Annuler')),
|
||||
onPressed: isLoading ? null : () => Navigator.pop(dialogContext),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: isLoading
|
||||
? null
|
||||
: () async {
|
||||
if (passwordController.text.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Mot de passe requis')));
|
||||
const SnackBar(
|
||||
content: Text('Mot de passe requis'),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
setState(() => isLoading = true);
|
||||
final result = await UserApiService().updatePassword(
|
||||
userId: user.id,
|
||||
newPassword: passwordController.text);
|
||||
userId: user.id,
|
||||
newPassword: passwordController.text,
|
||||
);
|
||||
if (dialogContext.mounted) Navigator.pop(dialogContext);
|
||||
if (result.success && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Mot de passe modifié'),
|
||||
backgroundColor: AppColors.success));
|
||||
const SnackBar(
|
||||
content: Text('Mot de passe modifié'),
|
||||
backgroundColor: AppColors.success,
|
||||
),
|
||||
);
|
||||
} else if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(result.error ?? 'Erreur'),
|
||||
backgroundColor: AppColors.error));
|
||||
backgroundColor: AppColors.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2))
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('Changer'),
|
||||
),
|
||||
],
|
||||
@@ -724,20 +811,25 @@ class _UsersTab extends ConsumerWidget {
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('Supprimer l\'utilisateur'),
|
||||
content: Text(
|
||||
'Supprimer "${user.username}" ? Cette action est irréversible.'),
|
||||
'Supprimer "${user.username}" ? Cette action est irréversible.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: const Text('Annuler')),
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(dialogContext);
|
||||
final result = await UserApiService().deleteUser(user.id);
|
||||
ref.invalidate(usersProvider);
|
||||
if (!result.success && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(result.error ?? 'Erreur'),
|
||||
backgroundColor: AppColors.error));
|
||||
backgroundColor: AppColors.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
style: FilledButton.styleFrom(backgroundColor: AppColors.error),
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../core/widgets/components/ui_components.dart';
|
||||
|
||||
class LoginScreen extends ConsumerStatefulWidget {
|
||||
@@ -148,24 +147,29 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||
labelText: 'Username',
|
||||
labelStyle:
|
||||
const TextStyle(color: Colors.white70),
|
||||
prefixIcon: const Icon(Icons.person_outline,
|
||||
color: Colors.white70),
|
||||
prefixIcon: const Icon(
|
||||
Icons.person_outline,
|
||||
color: Colors.white70,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.white.withOpacity(0.05),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.white.withOpacity(0.1)),
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.white.withOpacity(0.1)),
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(
|
||||
color: AppColors.primary),
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
@@ -186,24 +190,29 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||
labelText: 'Password',
|
||||
labelStyle:
|
||||
const TextStyle(color: Colors.white70),
|
||||
prefixIcon: const Icon(Icons.lock_outline,
|
||||
color: Colors.white70),
|
||||
prefixIcon: const Icon(
|
||||
Icons.lock_outline,
|
||||
color: Colors.white70,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.white.withOpacity(0.05),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.white.withOpacity(0.1)),
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.white.withOpacity(0.1)),
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(
|
||||
color: AppColors.primary),
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
|
||||
@@ -33,7 +33,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen>
|
||||
Icons.live_tv,
|
||||
Icons.movie,
|
||||
Icons.tv,
|
||||
Icons.settings
|
||||
Icons.settings,
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -85,7 +85,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen>
|
||||
child: Container(
|
||||
height: 50,
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 600), // Prevent taking too much space
|
||||
maxWidth: 600,
|
||||
), // Prevent taking too much space
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
@@ -105,7 +106,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen>
|
||||
unselectedLabelColor:
|
||||
Colors.white70, // Fixed: Improved readability
|
||||
labelStyle: GoogleFonts.inter(
|
||||
fontWeight: FontWeight.w700, fontSize: 13),
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 13,
|
||||
),
|
||||
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
||||
tabs: List.generate(_tabs.length, (index) {
|
||||
return Tab(
|
||||
@@ -182,16 +185,21 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen>
|
||||
children: [
|
||||
// Padding top to account for the transparent AppBar
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 80),
|
||||
child: LiveTVTab(playlist: widget.playlist)),
|
||||
padding: const EdgeInsets.only(top: 80),
|
||||
child: LiveTVTab(playlist: widget.playlist),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 80),
|
||||
child: MoviesTab(playlist: widget.playlist)),
|
||||
padding: const EdgeInsets.only(top: 80),
|
||||
child: MoviesTab(playlist: widget.playlist),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 80),
|
||||
child: SeriesTab(playlist: widget.playlist)),
|
||||
padding: const EdgeInsets.only(top: 80),
|
||||
child: SeriesTab(playlist: widget.playlist),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 80), child: SettingsTab()),
|
||||
padding: EdgeInsets.only(top: 80),
|
||||
child: SettingsTab(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -206,10 +214,12 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen>
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(colors: [
|
||||
Color(0xFF43cea2),
|
||||
Color(0xFF185a9d)
|
||||
]), // Green to Blue
|
||||
gradient: const LinearGradient(
|
||||
colors: [
|
||||
Color(0xFF43cea2),
|
||||
Color(0xFF185a9d),
|
||||
],
|
||||
), // Green to Blue
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Icon(Icons.play_arrow_rounded, color: Colors.white),
|
||||
@@ -222,9 +232,10 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen>
|
||||
Text(
|
||||
'Xtrem',
|
||||
style: GoogleFonts.inter(
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 16,
|
||||
color: Colors.white),
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Flow',
|
||||
|
||||
@@ -2,9 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
import '../../../core/shims/ui_web.dart' as ui_web;
|
||||
import 'dart:convert';
|
||||
import 'dart:async';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../providers/xtream_provider.dart';
|
||||
import '../providers/playback_positions_provider.dart';
|
||||
import '../providers/settings_provider.dart';
|
||||
@@ -187,7 +185,8 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
||||
|
||||
if (usingTranscoding) {
|
||||
debugPrint(
|
||||
'Seeking in transcoded VOD - reloading player at ${value.round()}s');
|
||||
'Seeking in transcoded VOD - reloading player at ${value.round()}s',
|
||||
);
|
||||
ref
|
||||
.read(playbackPositionsProvider.notifier)
|
||||
.savePosition(_contentId, value, _totalDuration);
|
||||
@@ -365,9 +364,13 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
||||
final encodedUrl = Uri.encodeComponent(
|
||||
widget.streamType == StreamType.vod
|
||||
? xtreamService.getVodStreamUrl(
|
||||
currentStreamId, widget.containerExtension)
|
||||
currentStreamId,
|
||||
widget.containerExtension,
|
||||
)
|
||||
: xtreamService.getSeriesStreamUrl(
|
||||
currentStreamId, widget.containerExtension),
|
||||
currentStreamId,
|
||||
widget.containerExtension,
|
||||
),
|
||||
);
|
||||
|
||||
String qualityParam = 'medium';
|
||||
@@ -419,7 +422,8 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
||||
startTime = positions.getPosition(_contentId);
|
||||
if (startTime > 0) {
|
||||
debugPrint(
|
||||
'PlayerScreen: Resuming from ${startTime.toStringAsFixed(0)}s');
|
||||
'PlayerScreen: Resuming from ${startTime.toStringAsFixed(0)}s',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -556,20 +560,25 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close,
|
||||
color: Colors.white54),
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
color: Colors.white54,
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
tooltip: 'Fermer',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text('FORMAT D\'IMAGE',
|
||||
style: TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1)),
|
||||
const Text(
|
||||
'FORMAT D\'IMAGE',
|
||||
style: TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
@@ -579,23 +588,38 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildRatioOption('Original', 'contain',
|
||||
Icons.crop_original, setDialogState),
|
||||
_buildRatioOption('Remplir', 'cover',
|
||||
Icons.crop_free, setDialogState),
|
||||
_buildRatioOption('Étirer', 'fill',
|
||||
Icons.aspect_ratio, setDialogState),
|
||||
_buildRatioOption(
|
||||
'Original',
|
||||
'contain',
|
||||
Icons.crop_original,
|
||||
setDialogState,
|
||||
),
|
||||
_buildRatioOption(
|
||||
'Remplir',
|
||||
'cover',
|
||||
Icons.crop_free,
|
||||
setDialogState,
|
||||
),
|
||||
_buildRatioOption(
|
||||
'Étirer',
|
||||
'fill',
|
||||
Icons.aspect_ratio,
|
||||
setDialogState,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_audioTracks.isNotEmpty) ...[
|
||||
const SizedBox(height: 32),
|
||||
const Text('AUDIO',
|
||||
style: TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1)),
|
||||
const Text(
|
||||
'AUDIO',
|
||||
style: TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
constraints:
|
||||
@@ -604,7 +628,8 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
||||
color: Colors.black26,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.05)),
|
||||
color: Colors.white.withOpacity(0.05),
|
||||
),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
@@ -615,24 +640,30 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
||||
color: Colors.transparent,
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
track['label'] ??
|
||||
'Piste ${track['id']}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14)),
|
||||
subtitle: Text(track['lang'] ?? '',
|
||||
style: const TextStyle(
|
||||
color: Colors.white54,
|
||||
fontSize: 12)),
|
||||
track['label'] ??
|
||||
'Piste ${track['id']}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
track['lang'] ?? '',
|
||||
style: const TextStyle(
|
||||
color: Colors.white54,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
_setAudioTrack(track['id']);
|
||||
setDialogState(() {});
|
||||
},
|
||||
dense: true,
|
||||
leading: const Icon(
|
||||
Icons.audiotrack,
|
||||
size: 18,
|
||||
color: Colors.white54),
|
||||
Icons.audiotrack,
|
||||
size: 18,
|
||||
color: Colors.white54,
|
||||
),
|
||||
hoverColor:
|
||||
Colors.white.withOpacity(0.1),
|
||||
),
|
||||
@@ -644,12 +675,15 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 32),
|
||||
const Text('AFFICHAGE',
|
||||
style: TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1)),
|
||||
const Text(
|
||||
'AFFICHAGE',
|
||||
style: TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SwitchListTile(
|
||||
value: showClock,
|
||||
@@ -659,11 +693,17 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
||||
});
|
||||
settingsNotifier.setShowClock(val);
|
||||
},
|
||||
title: const Text('Afficher l\'heure',
|
||||
style: TextStyle(
|
||||
color: Colors.white, fontSize: 14)),
|
||||
secondary: const Icon(Icons.access_time,
|
||||
color: Colors.white54),
|
||||
title: const Text(
|
||||
'Afficher l\'heure',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
secondary: const Icon(
|
||||
Icons.access_time,
|
||||
color: Colors.white54,
|
||||
),
|
||||
activeThumbColor: AppColors.primary,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
@@ -682,7 +722,11 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
||||
}
|
||||
|
||||
Widget _buildRatioOption(
|
||||
String label, String value, IconData icon, StateSetter setDialogState) {
|
||||
String label,
|
||||
String value,
|
||||
IconData icon,
|
||||
StateSetter setDialogState,
|
||||
) {
|
||||
final isSelected = _aspectRatio == value;
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
@@ -705,16 +749,20 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
||||
boxShadow: isSelected
|
||||
? [
|
||||
const BoxShadow(
|
||||
color: Colors.black12,
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 2))
|
||||
color: Colors.black12,
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(icon,
|
||||
color: isSelected ? Colors.white : Colors.white54, size: 20),
|
||||
Icon(
|
||||
icon,
|
||||
color: isSelected ? Colors.white : Colors.white54,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
label,
|
||||
@@ -767,14 +815,21 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.error_outline,
|
||||
size: 64, color: Colors.red),
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: Colors.red,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text('Failed to load stream',
|
||||
style: TextStyle(color: Colors.white, fontSize: 18)),
|
||||
const Text(
|
||||
'Failed to load stream',
|
||||
style: TextStyle(color: Colors.white, fontSize: 18),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(_errorMessage!,
|
||||
style: const TextStyle(color: Colors.white70)),
|
||||
Text(
|
||||
_errorMessage!,
|
||||
style: const TextStyle(color: Colors.white70),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
@@ -858,7 +913,8 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12),
|
||||
vertical: 12,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
PointerInterceptor(
|
||||
@@ -887,10 +943,12 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
||||
trackHeight: 4,
|
||||
thumbShape:
|
||||
const RoundSliderThumbShape(
|
||||
enabledThumbRadius: 6),
|
||||
enabledThumbRadius: 6,
|
||||
),
|
||||
overlayShape:
|
||||
const RoundSliderOverlayShape(
|
||||
overlayRadius: 12),
|
||||
overlayRadius: 12,
|
||||
),
|
||||
activeTrackColor: Colors.white,
|
||||
inactiveTrackColor:
|
||||
Colors.white24,
|
||||
@@ -961,8 +1019,9 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
||||
Colors.black.withOpacity(0.5),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Colors.white,
|
||||
width: 2),
|
||||
color: Colors.white,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
_isPlaying
|
||||
@@ -1040,10 +1099,12 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
||||
data: SliderThemeData(
|
||||
thumbShape:
|
||||
const RoundSliderThumbShape(
|
||||
enabledThumbRadius: 8),
|
||||
enabledThumbRadius: 8,
|
||||
),
|
||||
overlayShape:
|
||||
const RoundSliderOverlayShape(
|
||||
overlayRadius: 16),
|
||||
overlayRadius: 16,
|
||||
),
|
||||
activeTrackColor: AppColors.primary,
|
||||
inactiveTrackColor: Colors.white24,
|
||||
thumbColor: AppColors.primary,
|
||||
@@ -1060,31 +1121,40 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12),
|
||||
horizontal: 12,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_formatDuration(Duration(
|
||||
_formatDuration(
|
||||
Duration(
|
||||
seconds: _currentPosition
|
||||
.isFinite
|
||||
? _currentPosition
|
||||
.toInt()
|
||||
: 0)),
|
||||
: 0,
|
||||
),
|
||||
),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12),
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_formatDuration(Duration(
|
||||
_formatDuration(
|
||||
Duration(
|
||||
seconds: _totalDuration
|
||||
.isFinite
|
||||
? _totalDuration.toInt()
|
||||
: 0)),
|
||||
: 0,
|
||||
),
|
||||
),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12),
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1113,7 +1183,9 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
||||
fontWeight: FontWeight.bold,
|
||||
shadows: [
|
||||
Shadow(
|
||||
color: Colors.black, blurRadius: 8)
|
||||
color: Colors.black,
|
||||
blurRadius: 8,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -27,7 +27,9 @@ class PlaylistSelectionScreen extends ConsumerWidget {
|
||||
title: Text(
|
||||
'Select Playlist',
|
||||
style: GoogleFonts.roboto(
|
||||
fontWeight: FontWeight.w600, color: Colors.white),
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
@@ -71,9 +73,13 @@ class PlaylistSelectionScreen extends ConsumerWidget {
|
||||
children: [
|
||||
Icon(Icons.error_outline, size: 64, color: Colors.red.shade400),
|
||||
const SizedBox(height: 16),
|
||||
Text('Error loading playlists',
|
||||
style: GoogleFonts.roboto(
|
||||
fontSize: 18, color: Colors.white70)),
|
||||
Text(
|
||||
'Error loading playlists',
|
||||
style: GoogleFonts.roboto(
|
||||
fontSize: 18,
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () => ref.refresh(playlistsProvider),
|
||||
@@ -118,7 +124,11 @@ class PlaylistSelectionScreen extends ConsumerWidget {
|
||||
|
||||
return GridView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
24, 100, 24, 24), // Top padding for transparent AppBar
|
||||
24,
|
||||
100,
|
||||
24,
|
||||
24,
|
||||
), // Top padding for transparent AppBar
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
crossAxisSpacing: 24,
|
||||
|
||||
@@ -331,8 +331,10 @@ class XtreamService {
|
||||
}
|
||||
|
||||
/// Get movies with pagination support
|
||||
Future<List<xm.Movie>> getMoviesPaginated(
|
||||
{int offset = 0, int limit = 100}) async {
|
||||
Future<List<xm.Movie>> getMoviesPaginated({
|
||||
int offset = 0,
|
||||
int limit = 100,
|
||||
}) async {
|
||||
if (_currentPlaylist == null) throw Exception('No playlist configured');
|
||||
|
||||
try {
|
||||
@@ -411,8 +413,10 @@ class XtreamService {
|
||||
}
|
||||
|
||||
/// Get series with pagination support (returns flat list)
|
||||
Future<List<xm.Series>> getSeriesPaginated(
|
||||
{int offset = 0, int limit = 100}) async {
|
||||
Future<List<xm.Series>> getSeriesPaginated({
|
||||
int offset = 0,
|
||||
int limit = 100,
|
||||
}) async {
|
||||
if (_currentPlaylist == null) throw Exception('No playlist configured');
|
||||
|
||||
try {
|
||||
|
||||
@@ -74,7 +74,7 @@ class XtreamServiceMobile {
|
||||
print('XtreamServiceMobile: Will use IP $_resolvedIp for ${uri.host}');
|
||||
} else {
|
||||
print(
|
||||
'XtreamServiceMobile: DNS resolution failed, will use hostname directly');
|
||||
'XtreamServiceMobile: DNS resolution failed, will use hostname directly',);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,8 +162,9 @@ class XtreamServiceMobile {
|
||||
);
|
||||
|
||||
if (response.data is! Map<String, dynamic>) {
|
||||
if (response.data is String)
|
||||
if (response.data is String) {
|
||||
throw Exception('Auth Error: ${response.data}');
|
||||
}
|
||||
throw Exception('Invalid auth response format');
|
||||
}
|
||||
return response.data as Map<String, dynamic>;
|
||||
@@ -188,8 +189,9 @@ class XtreamServiceMobile {
|
||||
);
|
||||
|
||||
if (response.data is! List) {
|
||||
if (response.data is String)
|
||||
if (response.data is String) {
|
||||
throw Exception('Category Error: ${response.data}');
|
||||
}
|
||||
return {};
|
||||
}
|
||||
final List<dynamic> categories = response.data as List<dynamic>;
|
||||
@@ -229,8 +231,9 @@ class XtreamServiceMobile {
|
||||
);
|
||||
|
||||
if (response.data is! List) {
|
||||
if (response.data is String)
|
||||
if (response.data is String) {
|
||||
throw Exception('Live TV Error: ${response.data}');
|
||||
}
|
||||
return {};
|
||||
}
|
||||
final List<dynamic> streams = response.data as List<dynamic>;
|
||||
@@ -325,7 +328,7 @@ class XtreamServiceMobile {
|
||||
|
||||
/// Get movies with pagination support (uses in-memory cache for performance)
|
||||
Future<List<xm.Movie>> getMoviesPaginated(
|
||||
{int offset = 0, int limit = 100}) async {
|
||||
{int offset = 0, int limit = 100,}) async {
|
||||
if (_currentPlaylist == null) throw Exception('No playlist configured');
|
||||
|
||||
try {
|
||||
@@ -345,8 +348,9 @@ class XtreamServiceMobile {
|
||||
);
|
||||
|
||||
if (response.data is! List) {
|
||||
if (response.data is String)
|
||||
if (response.data is String) {
|
||||
throw Exception('VOD Error: ${response.data}');
|
||||
}
|
||||
_cachedMoviesRaw = [];
|
||||
} else {
|
||||
_cachedMoviesRaw = response.data as List<dynamic>;
|
||||
@@ -419,7 +423,7 @@ class XtreamServiceMobile {
|
||||
|
||||
/// Get series with pagination support (uses in-memory cache for performance)
|
||||
Future<List<xm.Series>> getSeriesPaginated(
|
||||
{int offset = 0, int limit = 100}) async {
|
||||
{int offset = 0, int limit = 100,}) async {
|
||||
if (_currentPlaylist == null) throw Exception('No playlist configured');
|
||||
|
||||
try {
|
||||
@@ -439,8 +443,9 @@ class XtreamServiceMobile {
|
||||
);
|
||||
|
||||
if (response.data is! List) {
|
||||
if (response.data is String)
|
||||
if (response.data is String) {
|
||||
throw Exception('Series Error: ${response.data}');
|
||||
}
|
||||
_cachedSeriesRaw = [];
|
||||
} else {
|
||||
_cachedSeriesRaw = response.data as List<dynamic>;
|
||||
@@ -580,7 +585,7 @@ class XtreamServiceMobile {
|
||||
/// Get short EPG as ShortEPG object (for EPGWidget)
|
||||
/// Caches results for 12 hours by default
|
||||
Future<xm.ShortEPG> getShortEPG(String streamId,
|
||||
{bool forceRefresh = false}) async {
|
||||
{bool forceRefresh = false,}) async {
|
||||
if (_currentPlaylist == null) throw Exception('No playlist configured');
|
||||
|
||||
try {
|
||||
|
||||
@@ -116,8 +116,10 @@ class _LiveTVTabState extends ConsumerState<LiveTVTab>
|
||||
_searchQuery.isEmpty &&
|
||||
!_showFavoritesOnly) ...[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back,
|
||||
color: AppColors.textPrimary),
|
||||
icon: const Icon(
|
||||
Icons.arrow_back,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_selectedCategory = null;
|
||||
@@ -156,14 +158,19 @@ class _LiveTVTabState extends ConsumerState<LiveTVTab>
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.search,
|
||||
size: 20, color: AppColors.textSecondary),
|
||||
const Icon(
|
||||
Icons.search,
|
||||
size: 20,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
style: const TextStyle(
|
||||
fontSize: 14, color: Colors.white),
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Rechercher...',
|
||||
hintStyle: TextStyle(color: Colors.white54),
|
||||
@@ -176,8 +183,11 @@ class _LiveTVTabState extends ConsumerState<LiveTVTab>
|
||||
if (_searchQuery.isNotEmpty)
|
||||
GestureDetector(
|
||||
onTap: () => _searchController.clear(),
|
||||
child: const Icon(Icons.close,
|
||||
size: 16, color: AppColors.textSecondary),
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
size: 16,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -250,7 +260,9 @@ class _LiveTVTabState extends ConsumerState<LiveTVTab>
|
||||
}
|
||||
|
||||
Widget _buildCategoryGrid(
|
||||
List<String> categories, Map<String, List<Channel>> groupedChannels) {
|
||||
List<String> categories,
|
||||
Map<String, List<Channel>> groupedChannels,
|
||||
) {
|
||||
final columns = ResponsiveLayout.value(
|
||||
context,
|
||||
mobile: 2,
|
||||
@@ -307,8 +319,11 @@ class _LiveTVTabState extends ConsumerState<LiveTVTab>
|
||||
color: AppColors.primary.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.folder_open,
|
||||
color: AppColors.primary, size: 32),
|
||||
child: const Icon(
|
||||
Icons.folder_open,
|
||||
color: AppColors.primary,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../../../core/models/playlist_config.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
@@ -238,14 +237,19 @@ class _MoviesTabState extends ConsumerState<MoviesTab> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.search,
|
||||
size: 20, color: AppColors.textSecondary),
|
||||
const Icon(
|
||||
Icons.search,
|
||||
size: 20,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
style: const TextStyle(
|
||||
fontSize: 14, color: Colors.white),
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Rechercher...',
|
||||
hintStyle: TextStyle(color: Colors.white54),
|
||||
@@ -260,17 +264,21 @@ class _MoviesTabState extends ConsumerState<MoviesTab> {
|
||||
),
|
||||
if (_isSearching)
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(strokeWidth: 2)),
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
if (_searchQuery.isNotEmpty)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
_searchController.clear();
|
||||
_onSearchChanged('');
|
||||
},
|
||||
child: const Icon(Icons.close,
|
||||
size: 16, color: AppColors.textSecondary),
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
size: 16,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../../../core/models/playlist_config.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
@@ -230,14 +229,19 @@ class _SeriesTabState extends ConsumerState<SeriesTab> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.search,
|
||||
size: 20, color: AppColors.textSecondary),
|
||||
const Icon(
|
||||
Icons.search,
|
||||
size: 20,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
style: const TextStyle(
|
||||
fontSize: 14, color: Colors.white),
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Rechercher...',
|
||||
hintStyle: TextStyle(color: Colors.white54),
|
||||
@@ -252,17 +256,21 @@ class _SeriesTabState extends ConsumerState<SeriesTab> {
|
||||
),
|
||||
if (_isSearching)
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(strokeWidth: 2)),
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
if (_searchQuery.isNotEmpty)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
_searchController.clear();
|
||||
_onSearchChanged('');
|
||||
},
|
||||
child: const Icon(Icons.close,
|
||||
size: 16, color: AppColors.textSecondary),
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
size: 16,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -280,8 +288,9 @@ class _SeriesTabState extends ConsumerState<SeriesTab> {
|
||||
child: HeroCarousel(
|
||||
items: heroItems,
|
||||
onTap: (item) {
|
||||
final series = _series.firstWhere((s) =>
|
||||
s.name == item.title); // Fallback by title if ID mismatch
|
||||
final series = _series.firstWhere(
|
||||
(s) => s.name == item.title,
|
||||
); // Fallback by title if ID mismatch
|
||||
_openSeries(series);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -96,8 +96,9 @@ class _SettingsTabState extends ConsumerState<SettingsTab>
|
||||
const Tab(icon: Icon(Icons.palette), text: 'Apparence'),
|
||||
if (currentUser?.isAdmin ?? false)
|
||||
const Tab(
|
||||
icon: Icon(Icons.admin_panel_settings),
|
||||
text: 'Administration'),
|
||||
icon: Icon(Icons.admin_panel_settings),
|
||||
text: 'Administration',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -212,7 +213,9 @@ class _SettingsTabState extends ConsumerState<SettingsTab>
|
||||
title: Text(
|
||||
'Mode actuel',
|
||||
style: GoogleFonts.inter(
|
||||
fontWeight: FontWeight.w500, color: Colors.white),
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
isDark ? 'Thème sombre actif' : 'Thème clair actif',
|
||||
@@ -305,7 +308,10 @@ class _SettingsTabState extends ConsumerState<SettingsTab>
|
||||
}
|
||||
|
||||
Widget _buildFiltersTab(
|
||||
BuildContext context, dynamic currentUser, IptvSettings settings) {
|
||||
BuildContext context,
|
||||
dynamic currentUser,
|
||||
IptvSettings settings,
|
||||
) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
@@ -326,13 +332,19 @@ class _SettingsTabState extends ConsumerState<SettingsTab>
|
||||
subtitle: Text(
|
||||
currentUser?.username ?? 'Unknown',
|
||||
style: GoogleFonts.roboto(
|
||||
fontWeight: FontWeight.w600, color: Colors.white),
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
trailing: currentUser?.isAdmin ?? false
|
||||
? Chip(
|
||||
label: Text('Admin',
|
||||
style: GoogleFonts.roboto(
|
||||
fontSize: 11, color: Colors.black)),
|
||||
label: Text(
|
||||
'Admin',
|
||||
style: GoogleFonts.roboto(
|
||||
fontSize: 11,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.white,
|
||||
)
|
||||
: null,
|
||||
@@ -416,8 +428,10 @@ class _SettingsTabState extends ConsumerState<SettingsTab>
|
||||
),
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.playlist_play, color: Colors.white),
|
||||
title: Text('Changer de Playlist',
|
||||
style: GoogleFonts.roboto(color: Colors.white)),
|
||||
title: Text(
|
||||
'Changer de Playlist',
|
||||
style: GoogleFonts.roboto(color: Colors.white),
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right, color: Colors.white54),
|
||||
onTap: () => context.go('/playlists'),
|
||||
),
|
||||
@@ -433,8 +447,10 @@ class _SettingsTabState extends ConsumerState<SettingsTab>
|
||||
),
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.info_outline, color: Colors.white),
|
||||
title: Text('À propos',
|
||||
style: GoogleFonts.roboto(color: Colors.white)),
|
||||
title: Text(
|
||||
'À propos',
|
||||
style: GoogleFonts.roboto(color: Colors.white),
|
||||
),
|
||||
subtitle: Text(
|
||||
'XtremFlow IPTV v1.0.0',
|
||||
style: GoogleFonts.roboto(fontSize: 12, color: Colors.white54),
|
||||
@@ -452,8 +468,10 @@ class _SettingsTabState extends ConsumerState<SettingsTab>
|
||||
),
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.cleaning_services, color: Colors.orange),
|
||||
title: Text('Maintenance',
|
||||
style: GoogleFonts.roboto(color: Colors.white)),
|
||||
title: Text(
|
||||
'Maintenance',
|
||||
style: GoogleFonts.roboto(color: Colors.white),
|
||||
),
|
||||
subtitle: Text(
|
||||
'En cas de problèmes d\'affichage ou de mise à jour',
|
||||
style: GoogleFonts.roboto(fontSize: 12, color: Colors.white54),
|
||||
@@ -465,8 +483,10 @@ class _SettingsTabState extends ConsumerState<SettingsTab>
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: const Color(0xFF1E1E1E),
|
||||
title: const Text('Vider le cache ?',
|
||||
style: TextStyle(color: Colors.white)),
|
||||
title: const Text(
|
||||
'Vider le cache ?',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
content: const Text(
|
||||
'Cette action va recharger l\'application et effacer les données temporaires.\n'
|
||||
'Vos réglages et favoris sont sauvegardés sur le serveur.',
|
||||
@@ -479,7 +499,8 @@ class _SettingsTabState extends ConsumerState<SettingsTab>
|
||||
),
|
||||
FilledButton(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Colors.orange),
|
||||
backgroundColor: Colors.orange,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
|
||||
@@ -580,8 +601,11 @@ class _SettingsTabState extends ConsumerState<SettingsTab>
|
||||
),
|
||||
suffixIcon: controller.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear,
|
||||
size: 16, color: Colors.white54),
|
||||
icon: const Icon(
|
||||
Icons.clear,
|
||||
size: 16,
|
||||
color: Colors.white54,
|
||||
),
|
||||
onPressed: onClear,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
|
||||
@@ -248,10 +248,14 @@ class StreamingSettingsTab extends ConsumerWidget {
|
||||
required String Function(T) labelBuilder,
|
||||
}) {
|
||||
return ListTile(
|
||||
title: Text(title,
|
||||
style: GoogleFonts.roboto(fontSize: 14, color: Colors.white)),
|
||||
subtitle: Text(subtitle,
|
||||
style: GoogleFonts.roboto(fontSize: 12, color: Colors.white54)),
|
||||
title: Text(
|
||||
title,
|
||||
style: GoogleFonts.roboto(fontSize: 14, color: Colors.white),
|
||||
),
|
||||
subtitle: Text(
|
||||
subtitle,
|
||||
style: GoogleFonts.roboto(fontSize: 12, color: Colors.white54),
|
||||
),
|
||||
trailing: DropdownButton<T>(
|
||||
value: value,
|
||||
underline: const SizedBox(),
|
||||
|
||||
Reference in new issue
Block a user