mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-11 17:29:26 +02:00
fix: restore EPG display in live TV player and channel cards
- Fix EPG not showing in NativePlayerScreen by reusing the shared mobileXtreamServiceProvider (cache already populated by channel cards) instead of creating a fresh service instance per player session - Add periodic EPG refresh timer (every 5 min) matching LitePlayerScreen - Fix empty-title fallback in _buildEPGBox - Force dark theme, invert default live TV player (NativePlayer default), remove next-program display from OSD - Fix back button exit dialog firing from wrong tab (add tab index guards) - Halve card grid count for Movies/Series (6 categories, 8 items) - Revamp Movies/Series search bars to match Live TV aesthetic - Fix first channel row clipping when entering a category (scroll-to-top) - Fix playlist edit dialog TV remote navigation - Fix empty playlist screen button readability - Remove theme switcher from settings Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
f07a6e91f9
commit
6bb0d73f38
8 files changed
+564
-350
No files matched your search
@@ -19,7 +19,30 @@
|
||||
"WebFetch(domain:www.color-hex.com)",
|
||||
"WebFetch(domain:iosexample.com)",
|
||||
"WebFetch(domain:blog.karenying.com)",
|
||||
"WebFetch(domain:gist.github.com)"
|
||||
"WebFetch(domain:gist.github.com)",
|
||||
"Bash(~/.flutter/bin/flutter analyze:*)",
|
||||
"Bash(where flutter:*)",
|
||||
"Read(//c/Users/Michael/**)",
|
||||
"Bash(/c/Users/Michael/Git/flutter/bin/flutter analyze:*)",
|
||||
"Bash(/c/Users/Michael/Git/flutter/bin/flutter build:*)",
|
||||
"Bash(echo \"ANDROID_HOME=$ANDROID_HOME\")",
|
||||
"Bash(echo \"ANDROID_SDK_ROOT=$ANDROID_SDK_ROOT\")",
|
||||
"Bash(java -version)",
|
||||
"Bash(where java:*)",
|
||||
"Bash(echo \"JAVA_HOME=$JAVA_HOME\")",
|
||||
"Bash(winget --version)",
|
||||
"Bash(winget install:*)",
|
||||
"Bash(export JAVA_HOME=\"/c/Program Files/Microsoft/jdk-21.0.10.7-hotspot\")",
|
||||
"Bash(export PATH=\"$JAVA_HOME/bin:$PATH\")",
|
||||
"Bash(export JAVA_HOME=\"/c/Program Files/Microsoft/jdk-21.0.10.7-hotspot\" export ANDROID_HOME=\"/c/Users/Michael/Android/Sdk\" export PATH=\"$JAVA_HOME/bin:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$PATH\" __NEW_LINE_a84857a0b65daa44__ SDKMANAGER=\"/c/Users/Michael/Android/Sdk/cmdline-tools/latest/bin/sdkmanager.bat\" __NEW_LINE_a84857a0b65daa44__ yes)",
|
||||
"Bash(\"$SDKMANAGER\" --licenses)",
|
||||
"Bash(export ANDROID_HOME=\"/c/Users/Michael/Android/Sdk\")",
|
||||
"Bash(SDKMANAGER=\"/c/Users/Michael/Android/Sdk/cmdline-tools/latest/bin/sdkmanager.bat\")",
|
||||
"Bash(__NEW_LINE_633e55f3dba9c695__ \"$SDKMANAGER\" \"platform-tools\" \"platforms;android-35\" \"build-tools;35.0.0\")",
|
||||
"Bash(export PATH=\"$JAVA_HOME/bin:$ANDROID_HOME/platform-tools:$PATH\")",
|
||||
"Bash(__NEW_LINE_fa059bc1c566f6ee__ cd:*)",
|
||||
"Bash(__NEW_LINE_70fecd5c4080fe72__ cd:*)",
|
||||
"Bash(__NEW_LINE_2a0d1cacdabfd662__ cd:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -41,26 +41,8 @@ class ThemeNotifier extends StateNotifier<ThemeState> {
|
||||
_loadTheme();
|
||||
}
|
||||
|
||||
/// Load saved theme preference from SharedPreferences
|
||||
Future<void> _loadTheme() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final savedMode = prefs.getString(_ThemeKeys.themeMode);
|
||||
|
||||
if (savedMode != null) {
|
||||
final appMode = AppThemeMode.values.firstWhere(
|
||||
(e) => e.name == savedMode,
|
||||
orElse: () => AppThemeMode.dark,
|
||||
);
|
||||
state = state.copyWith(
|
||||
appThemeMode: appMode,
|
||||
themeMode: _mapToThemeMode(appMode),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error loading theme: $e');
|
||||
}
|
||||
}
|
||||
/// Theme is always forced to dark — no loading from preferences
|
||||
Future<void> _loadTheme() async {}
|
||||
|
||||
/// Set theme mode and persist to SharedPreferences
|
||||
Future<void> setThemeMode(AppThemeMode mode) async {
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../../../../core/services/playlist_api_service.dart';
|
||||
import '../../../../core/models/playlist_config.dart';
|
||||
import '../../../../mobile/widgets/tv_focusable.dart';
|
||||
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
import '../../../mobile/theme/mobile_theme.dart';
|
||||
@@ -45,155 +46,37 @@ class _MobilePlaylistSelectionScreenState
|
||||
|
||||
Future<void> _showPlaylistDialog({PlaylistConfig? playlist}) async {
|
||||
final isEditing = playlist != null;
|
||||
final nameController = TextEditingController(text: playlist?.name ?? '');
|
||||
final dnsController = TextEditingController(text: playlist?.dns ?? '');
|
||||
final userController =
|
||||
TextEditingController(text: playlist?.username ?? '');
|
||||
final passController =
|
||||
TextEditingController(text: playlist?.password ?? '');
|
||||
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: AppColors.surface,
|
||||
title: Text(
|
||||
isEditing ? 'Modifier la Playlist' : 'Ajouter une Playlist',
|
||||
style: GoogleFonts.inter(color: Colors.white),
|
||||
),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: nameController,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nom',
|
||||
labelStyle: TextStyle(color: Colors.white70),
|
||||
enabledBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.white24),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: dnsController,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'URL (DNS)',
|
||||
labelStyle: TextStyle(color: Colors.white70),
|
||||
enabledBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.white24),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: userController,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Utilisateur',
|
||||
labelStyle: TextStyle(color: Colors.white70),
|
||||
enabledBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.white24),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: passController,
|
||||
obscureText: true,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Mot de passe',
|
||||
labelStyle: TextStyle(color: Colors.white70),
|
||||
enabledBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.white24),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
if (isEditing)
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: AppColors.surface,
|
||||
title: const Text(
|
||||
'Confirmer',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
content: const Text(
|
||||
'Supprimer cette playlist ?',
|
||||
style: TextStyle(color: Colors.white70),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
style:
|
||||
TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
child: const Text('Supprimer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirm == true) {
|
||||
await PlaylistApiService().deletePlaylist(playlist.id);
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
ref.invalidate(playlistsProvider);
|
||||
}
|
||||
}
|
||||
},
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
child: const Text('Supprimer'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (nameController.text.isEmpty || dnsController.text.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final service = PlaylistApiService();
|
||||
if (isEditing) {
|
||||
await service.updatePlaylist(
|
||||
id: playlist.id,
|
||||
name: nameController.text,
|
||||
dns: dnsController.text,
|
||||
username: userController.text,
|
||||
password: passController.text,
|
||||
);
|
||||
} else {
|
||||
await service.createPlaylist(
|
||||
name: nameController.text,
|
||||
dns: dnsController.text,
|
||||
username: userController.text,
|
||||
password: passController.text,
|
||||
);
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
builder: (context) => _PlaylistEditDialog(
|
||||
isEditing: isEditing,
|
||||
playlist: playlist,
|
||||
onSave: (name, dns, username, password) async {
|
||||
final service = PlaylistApiService();
|
||||
if (isEditing) {
|
||||
await service.updatePlaylist(
|
||||
id: playlist.id,
|
||||
name: name,
|
||||
dns: dns,
|
||||
username: username,
|
||||
password: password,
|
||||
);
|
||||
} else {
|
||||
await service.createPlaylist(
|
||||
name: name,
|
||||
dns: dns,
|
||||
username: username,
|
||||
password: password,
|
||||
);
|
||||
}
|
||||
ref.invalidate(playlistsProvider);
|
||||
},
|
||||
onDelete: isEditing
|
||||
? () async {
|
||||
await PlaylistApiService().deletePlaylist(playlist.id);
|
||||
ref.invalidate(playlistsProvider);
|
||||
}
|
||||
},
|
||||
child: const Text('Enregistrer'),
|
||||
),
|
||||
],
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -271,16 +154,32 @@ class _MobilePlaylistSelectionScreenState
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
ElevatedButton.icon(
|
||||
TVFocusable(
|
||||
onPressed: () => _showPlaylistDialog(),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Ajouter une Playlist'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: Colors.black,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
horizontal: 28,
|
||||
vertical: 14,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0A84FF),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.add, color: Colors.white, size: 20),
|
||||
SizedBox(width: 10),
|
||||
Text(
|
||||
'Ajouter une Playlist',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -322,6 +221,285 @@ class _MobilePlaylistSelectionScreenState
|
||||
}
|
||||
}
|
||||
|
||||
/// TV-friendly playlist edit dialog.
|
||||
/// Each field is a focusable row; pressing select opens a single-field input dialog.
|
||||
class _PlaylistEditDialog extends StatefulWidget {
|
||||
final bool isEditing;
|
||||
final PlaylistConfig? playlist;
|
||||
final Future<void> Function(String name, String dns, String username, String password) onSave;
|
||||
final Future<void> Function()? onDelete;
|
||||
|
||||
const _PlaylistEditDialog({
|
||||
required this.isEditing,
|
||||
required this.playlist,
|
||||
required this.onSave,
|
||||
this.onDelete,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_PlaylistEditDialog> createState() => _PlaylistEditDialogState();
|
||||
}
|
||||
|
||||
class _PlaylistEditDialogState extends State<_PlaylistEditDialog> {
|
||||
late String _name;
|
||||
late String _dns;
|
||||
late String _username;
|
||||
late String _password;
|
||||
bool _isSaving = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_name = widget.playlist?.name ?? '';
|
||||
_dns = widget.playlist?.dns ?? '';
|
||||
_username = widget.playlist?.username ?? '';
|
||||
_password = widget.playlist?.password ?? '';
|
||||
}
|
||||
|
||||
Future<void> _editField({
|
||||
required String label,
|
||||
required String currentValue,
|
||||
required void Function(String) onSaved,
|
||||
bool obscure = false,
|
||||
TextInputType keyboardType = TextInputType.text,
|
||||
}) async {
|
||||
final controller = TextEditingController(text: currentValue);
|
||||
final result = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: AppColors.surface,
|
||||
title: Text(label, style: const TextStyle(color: Colors.white)),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
obscureText: obscure,
|
||||
keyboardType: keyboardType,
|
||||
textInputAction: TextInputAction.done,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
decoration: const InputDecoration(
|
||||
labelStyle: TextStyle(color: Colors.white70),
|
||||
enabledBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.white24),
|
||||
),
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.white),
|
||||
),
|
||||
),
|
||||
onSubmitted: (val) => Navigator.pop(ctx, val),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
autofocus: false,
|
||||
onPressed: () => Navigator.pop(ctx, controller.text),
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
controller.dispose();
|
||||
if (result != null) {
|
||||
onSaved(result);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildFieldRow({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required String value,
|
||||
required VoidCallback onTap,
|
||||
bool obscure = false,
|
||||
}) {
|
||||
final displayValue = obscure && value.isNotEmpty
|
||||
? '•' * value.length.clamp(1, 12)
|
||||
: value.isEmpty
|
||||
? '—'
|
||||
: value;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: TVFocusable(
|
||||
onPressed: onTap,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.07),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: AppColors.primary, size: 22),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
displayValue,
|
||||
style: TextStyle(
|
||||
color: value.isEmpty ? Colors.white38 : Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(Icons.edit, color: Colors.white38, size: 18),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
backgroundColor: AppColors.surface,
|
||||
title: Text(
|
||||
widget.isEditing ? 'Modifier la Playlist' : 'Ajouter une Playlist',
|
||||
style: GoogleFonts.inter(color: Colors.white),
|
||||
),
|
||||
content: SizedBox(
|
||||
width: 480,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildFieldRow(
|
||||
icon: Icons.label_outline,
|
||||
label: 'Nom',
|
||||
value: _name,
|
||||
onTap: () => _editField(
|
||||
label: 'Nom',
|
||||
currentValue: _name,
|
||||
onSaved: (v) => setState(() => _name = v),
|
||||
),
|
||||
),
|
||||
_buildFieldRow(
|
||||
icon: Icons.link,
|
||||
label: 'URL (DNS)',
|
||||
value: _dns,
|
||||
onTap: () => _editField(
|
||||
label: 'URL (DNS)',
|
||||
currentValue: _dns,
|
||||
keyboardType: TextInputType.url,
|
||||
onSaved: (v) => setState(() => _dns = v),
|
||||
),
|
||||
),
|
||||
_buildFieldRow(
|
||||
icon: Icons.person_outline,
|
||||
label: 'Utilisateur',
|
||||
value: _username,
|
||||
onTap: () => _editField(
|
||||
label: 'Utilisateur',
|
||||
currentValue: _username,
|
||||
onSaved: (v) => setState(() => _username = v),
|
||||
),
|
||||
),
|
||||
_buildFieldRow(
|
||||
icon: Icons.lock_outline,
|
||||
label: 'Mot de passe',
|
||||
value: _password,
|
||||
obscure: true,
|
||||
onTap: () => _editField(
|
||||
label: 'Mot de passe',
|
||||
currentValue: _password,
|
||||
obscure: true,
|
||||
onSaved: (v) => setState(() => _password = v),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
if (widget.onDelete != null)
|
||||
TVFocusable(
|
||||
onPressed: () async {
|
||||
final confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: AppColors.surface,
|
||||
title: const Text('Confirmer', style: TextStyle(color: Colors.white)),
|
||||
content: const Text('Supprimer cette playlist ?', style: TextStyle(color: Colors.white70)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
child: const Text('Supprimer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirm == true) {
|
||||
await widget.onDelete!();
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Text('Supprimer', style: TextStyle(color: Colors.red)),
|
||||
),
|
||||
),
|
||||
TVFocusable(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Text('Annuler', style: TextStyle(color: Colors.white70)),
|
||||
),
|
||||
),
|
||||
TVFocusable(
|
||||
onPressed: _isSaving || _name.isEmpty || _dns.isEmpty
|
||||
? null
|
||||
: () async {
|
||||
setState(() => _isSaving = true);
|
||||
await widget.onSave(_name, _dns, _username, _password);
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: _name.isEmpty || _dns.isEmpty
|
||||
? Colors.white12
|
||||
: AppColors.primary,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
_isSaving ? '...' : 'Enregistrer',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MobilePlaylistCard extends StatelessWidget {
|
||||
final PlaylistConfig playlist;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:xtremobile/core/models/iptv_models.dart' as model;
|
||||
import 'package:xtremobile/core/models/playlist_config.dart';
|
||||
import 'package:xtremobile/features/iptv/services/xtream_service_mobile.dart';
|
||||
import 'package:xtremobile/mobile/providers/mobile_settings_providers.dart';
|
||||
import 'package:xtremobile/mobile/providers/mobile_xtream_providers.dart';
|
||||
import 'package:xtremobile/core/theme/app_colors.dart';
|
||||
import 'package:xtremobile/features/iptv/screens/lite_player_screen.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
@@ -86,6 +87,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
||||
|
||||
Timer? _clockTimer;
|
||||
Timer? _controlsTimer; // Auto-hide timer
|
||||
Timer? _epgTimer; // Periodic EPG refresh for live TV
|
||||
Timer? _liveWatchdog; // Watchdog for live stream reconnection
|
||||
int _reconnectAttempts = 0;
|
||||
static const int _maxReconnectAttempts = 5;
|
||||
@@ -464,6 +466,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
||||
|
||||
_clockTimer?.cancel();
|
||||
_controlsTimer?.cancel();
|
||||
_epgTimer?.cancel();
|
||||
_liveWatchdog?.cancel();
|
||||
|
||||
// Stop playback first to prevent audio continuing in background
|
||||
@@ -702,9 +705,13 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
||||
? widget.channels![_currentIndex].streamId
|
||||
: widget.streamId;
|
||||
|
||||
// Load EPG if Live TV
|
||||
// Load EPG if Live TV (with periodic refresh every 5 min)
|
||||
if (widget.streamType == model.StreamType.live) {
|
||||
_loadEpg(currentStreamId);
|
||||
_epgTimer?.cancel();
|
||||
_epgTimer = Timer.periodic(const Duration(minutes: 5), (_) {
|
||||
_loadEpg(currentStreamId);
|
||||
});
|
||||
}
|
||||
|
||||
// ... (rest of _loadStream logic: get url, open player)
|
||||
@@ -806,7 +813,9 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
||||
|
||||
Future<void> _loadEpg(String streamId) async {
|
||||
try {
|
||||
final epg = await _xtreamService!.getShortEPG(streamId);
|
||||
// Use shared provider service so EPG cached by channel cards is reused instantly
|
||||
final service = await ref.read(mobileXtreamServiceProvider(widget.playlist).future);
|
||||
final epg = await service.getShortEPG(streamId);
|
||||
if (mounted) {
|
||||
setState(() => _epg = epg);
|
||||
}
|
||||
@@ -1323,7 +1332,8 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
||||
}
|
||||
|
||||
Widget _buildEPGBox(String title) {
|
||||
String nowPlaying = _epg?.nowPlaying ?? "Pas d'infos EPG";
|
||||
final rawNow = _epg?.nowPlaying ?? '';
|
||||
String nowPlaying = rawNow.isNotEmpty ? rawNow : "Pas d'infos EPG";
|
||||
String nextPlaying =
|
||||
_epg?.nextPlaying != null ? "Suivant: ${_epg!.nextPlaying}" : "";
|
||||
|
||||
@@ -1375,19 +1385,6 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
||||
),
|
||||
],
|
||||
),
|
||||
if (nextPlaying.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
nextPlaying,
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ class MobileLiveTVTab extends ConsumerStatefulWidget {
|
||||
|
||||
class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final FocusNode _searchFocusNode = FocusNode();
|
||||
String _searchQuery = '';
|
||||
@@ -60,6 +61,7 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
_searchController.dispose();
|
||||
_searchFocusNode.dispose();
|
||||
_searchTimer?.cancel();
|
||||
@@ -80,13 +82,24 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
|
||||
final uiState = ref.watch(mobileLiveTvUiStateProvider);
|
||||
final uiNotifier = ref.read(mobileLiveTvUiStateProvider.notifier);
|
||||
|
||||
// Scroll to top when entering a category so first row is fully visible
|
||||
ref.listen(mobileLiveTvUiStateProvider, (previous, next) {
|
||||
if (previous?.isCategoryView == true && !next.isCategoryView) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_scrollController.hasClients) {
|
||||
_scrollController.jumpTo(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Determine if we should show the category grid
|
||||
final bool showGrid = uiState.isCategoryView &&
|
||||
_searchQuery.isEmpty &&
|
||||
!_showFavoritesOnly;
|
||||
|
||||
return Container(
|
||||
color: const Color(0xFF000000), // Pure black background
|
||||
color: const Color(0xFF000000),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: PopScope(
|
||||
@@ -95,6 +108,9 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
|
||||
onPopInvokedWithResult: (didPop, _) async {
|
||||
if (didPop) return;
|
||||
|
||||
// Only handle back if this tab is currently active (tab index 0)
|
||||
if (ref.read(mobileDashboardIndexProvider) != 0) return;
|
||||
|
||||
// If just returned from player, reset the flag and do nothing (stay on channel list)
|
||||
if (_justReturnedFromPlayer) {
|
||||
_justReturnedFromPlayer = false;
|
||||
@@ -194,6 +210,7 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
|
||||
return SafeArea(
|
||||
bottom: false,
|
||||
child: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
slivers: [
|
||||
// Floating Header (Search + Navigation)
|
||||
SliverAppBar(
|
||||
@@ -447,14 +464,13 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NativePlayerScreen(
|
||||
builder: (context) => LitePlayerScreen(
|
||||
streamId: channel.streamId,
|
||||
title: channel.name,
|
||||
playlist: playlist,
|
||||
streamType: StreamType.live,
|
||||
channels: channels, // Pass full channel list for zapping
|
||||
channels: channels,
|
||||
initialIndex: index,
|
||||
forceDeinterlace: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -462,13 +478,14 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => LitePlayerScreen(
|
||||
builder: (context) => NativePlayerScreen(
|
||||
streamId: channel.streamId,
|
||||
title: channel.name,
|
||||
playlist: playlist,
|
||||
streamType: StreamType.live,
|
||||
channels: channels, // Pass full channel list for zapping
|
||||
channels: channels,
|
||||
initialIndex: index,
|
||||
forceDeinterlace: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -264,6 +264,9 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab>
|
||||
onPopInvokedWithResult: (didPop, _) async {
|
||||
if (didPop) return;
|
||||
|
||||
// Only handle back if this tab is currently active (tab index 1)
|
||||
if (ref.read(mobileDashboardIndexProvider) != 1) return;
|
||||
|
||||
// Handle Search Exit
|
||||
if (_searchQuery.isNotEmpty || _isSearching) {
|
||||
_onSearchChanged(''); // clear search
|
||||
@@ -327,94 +330,101 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab>
|
||||
// Pinned Header
|
||||
SliverAppBar(
|
||||
pinned: true,
|
||||
floating: true,
|
||||
backgroundColor: AppDecorations.background(context).gradient is LinearGradient
|
||||
? (AppDecorations.background(context).gradient as LinearGradient).colors.first
|
||||
: Theme.of(context).scaffoldBackgroundColor,
|
||||
elevation: 4,
|
||||
floating: false,
|
||||
snap: false,
|
||||
backgroundColor: const Color(0xFF000000),
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
automaticallyImplyLeading: false,
|
||||
expandedHeight: showingMovies ? 110 : 110,
|
||||
collapsedHeight: showingMovies ? 110 : 110,
|
||||
toolbarHeight: showingMovies ? 120.0 : 110.0,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
background: Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (showingMovies)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
// Search Bar
|
||||
TVFocusable(
|
||||
scale: 1.0,
|
||||
focusColor: Colors.white,
|
||||
onPressed: () {
|
||||
setState(() => _isSearchEditing = true);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_searchFocusNode.requestFocus();
|
||||
SystemChannels.textInput.invokeMethod('TextInput.show');
|
||||
});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
height: 48,
|
||||
decoration: AppDecorations.searchBar(context),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
||||
onPressed: _onBack,
|
||||
),
|
||||
Icon(Icons.search, color: AppDecorations.textSecondary(context)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_selectedCategoryName ?? 'Films',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold),
|
||||
child: ExcludeFocus(
|
||||
excluding: !_isSearchEditing,
|
||||
child: TextField(
|
||||
cursorColor: Colors.white,
|
||||
controller: _searchController,
|
||||
focusNode: _searchFocusNode,
|
||||
readOnly: !_isSearchEditing,
|
||||
style: TextStyle(color: AppDecorations.textPrimary(context)),
|
||||
decoration: InputDecoration(
|
||||
hintText: _selectedCategoryId != null ? 'Rechercher...' : 'Rechercher un film...',
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
isDense: true,
|
||||
),
|
||||
onChanged: _onSearchChanged,
|
||||
onSubmitted: (_) => setState(() => _isSearchEditing = false),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_isSearching) const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)),
|
||||
if (_searchQuery.isNotEmpty)
|
||||
GestureDetector(
|
||||
onTap: () { _searchController.clear(); _onSearchChanged(''); },
|
||||
child: Icon(Icons.close, color: AppDecorations.textSecondary(context)),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
const SizedBox(height: 10),
|
||||
),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: TVFocusable(
|
||||
scale: 1.0,
|
||||
focusColor: Colors.white,
|
||||
onPressed: () {
|
||||
setState(() => _isSearchEditing = true);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_searchFocusNode.requestFocus();
|
||||
});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
height: 40,
|
||||
decoration: AppDecorations.searchBar(context),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.search, size: 20, color: AppDecorations.textSecondary(context)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: ExcludeFocus(
|
||||
excluding: !_isSearchEditing,
|
||||
child: TextField(
|
||||
cursorColor: AppDecorations.textPrimary(context),
|
||||
controller: _searchController,
|
||||
focusNode: _searchFocusNode,
|
||||
readOnly: !_isSearchEditing,
|
||||
style: TextStyle(fontSize: 14, color: AppDecorations.textPrimary(context)),
|
||||
decoration: InputDecoration(
|
||||
hintText: _selectedCategoryId != null ? 'Rechercher...' : 'Rechercher un film...',
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.only(bottom: 11),
|
||||
),
|
||||
onChanged: _onSearchChanged,
|
||||
onSubmitted: (_) => setState(() => _isSearchEditing = false),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_isSearching) const SizedBox(width: 12, height: 12, child: CircularProgressIndicator(strokeWidth: 2)),
|
||||
if (_searchQuery.isNotEmpty)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
_searchController.clear();
|
||||
_onSearchChanged('');
|
||||
},
|
||||
child: Icon(Icons.close, size: 16, color: AppDecorations.textSecondary(context)),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Navigation Row
|
||||
Row(
|
||||
children: [
|
||||
if (showingMovies) ...[
|
||||
IconButton(
|
||||
icon: Icon(Icons.arrow_back, color: AppDecorations.textPrimary(context)),
|
||||
onPressed: _onBack,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
showingMovies
|
||||
? (_selectedCategoryName ?? 'Films')
|
||||
: showingSearch
|
||||
? 'Résultats de recherche'
|
||||
: 'Catégories',
|
||||
style: TextStyle(
|
||||
color: AppDecorations.textPrimary(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -427,7 +437,7 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab>
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 80),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: showingCategories ? 3 : 4,
|
||||
crossAxisCount: showingCategories ? 6 : 8,
|
||||
crossAxisSpacing: 10,
|
||||
mainAxisSpacing: 10,
|
||||
childAspectRatio: showingCategories ? 2.0 : 0.67,
|
||||
|
||||
@@ -251,6 +251,9 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab>
|
||||
onPopInvokedWithResult: (didPop, _) async {
|
||||
if (didPop) return;
|
||||
|
||||
// Only handle back if this tab is currently active (tab index 2)
|
||||
if (ref.read(mobileDashboardIndexProvider) != 2) return;
|
||||
|
||||
if (_searchQuery.isNotEmpty || _isSearching) {
|
||||
_onSearchChanged('');
|
||||
_searchController.clear();
|
||||
@@ -297,94 +300,101 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab>
|
||||
// Pinned Header
|
||||
SliverAppBar(
|
||||
pinned: true,
|
||||
floating: true,
|
||||
backgroundColor: AppDecorations.background(context).gradient is LinearGradient
|
||||
? (AppDecorations.background(context).gradient as LinearGradient).colors.first
|
||||
: Theme.of(context).scaffoldBackgroundColor,
|
||||
elevation: 4,
|
||||
floating: false,
|
||||
snap: false,
|
||||
backgroundColor: const Color(0xFF000000),
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
automaticallyImplyLeading: false,
|
||||
expandedHeight: showingSeries ? 110 : 110,
|
||||
collapsedHeight: showingSeries ? 110 : 110,
|
||||
toolbarHeight: showingSeries ? 120.0 : 110.0,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
background: Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (showingSeries)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
// Search Bar
|
||||
TVFocusable(
|
||||
scale: 1.0,
|
||||
focusColor: Colors.white,
|
||||
onPressed: () {
|
||||
setState(() => _isSearchEditing = true);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_searchFocusNode.requestFocus();
|
||||
SystemChannels.textInput.invokeMethod('TextInput.show');
|
||||
});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
height: 48,
|
||||
decoration: AppDecorations.searchBar(context),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
||||
onPressed: _onBack,
|
||||
),
|
||||
Icon(Icons.search, color: AppDecorations.textSecondary(context)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_selectedCategoryName ?? 'Séries',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold),
|
||||
child: ExcludeFocus(
|
||||
excluding: !_isSearchEditing,
|
||||
child: TextField(
|
||||
cursorColor: Colors.white,
|
||||
controller: _searchController,
|
||||
focusNode: _searchFocusNode,
|
||||
readOnly: !_isSearchEditing,
|
||||
style: TextStyle(color: AppDecorations.textPrimary(context)),
|
||||
decoration: InputDecoration(
|
||||
hintText: _selectedCategoryId != null ? 'Rechercher...' : 'Rechercher une série...',
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
isDense: true,
|
||||
),
|
||||
onChanged: _onSearchChanged,
|
||||
onSubmitted: (_) => setState(() => _isSearchEditing = false),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_isSearching) const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)),
|
||||
if (_searchQuery.isNotEmpty)
|
||||
GestureDetector(
|
||||
onTap: () { _searchController.clear(); _onSearchChanged(''); },
|
||||
child: Icon(Icons.close, color: AppDecorations.textSecondary(context)),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
const SizedBox(height: 10),
|
||||
),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: TVFocusable(
|
||||
scale: 1.0,
|
||||
focusColor: Colors.white,
|
||||
onPressed: () {
|
||||
setState(() => _isSearchEditing = true);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_searchFocusNode.requestFocus();
|
||||
});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
height: 40,
|
||||
decoration: AppDecorations.searchBar(context),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.search, size: 20, color: AppDecorations.textSecondary(context)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: ExcludeFocus(
|
||||
excluding: !_isSearchEditing,
|
||||
child: TextField(
|
||||
cursorColor: AppDecorations.textPrimary(context),
|
||||
controller: _searchController,
|
||||
focusNode: _searchFocusNode,
|
||||
readOnly: !_isSearchEditing,
|
||||
style: TextStyle(fontSize: 14, color: AppDecorations.textPrimary(context)),
|
||||
decoration: InputDecoration(
|
||||
hintText: _selectedCategoryId != null ? 'Rechercher...' : 'Rechercher une série...',
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.only(bottom: 11),
|
||||
),
|
||||
onChanged: _onSearchChanged,
|
||||
onSubmitted: (_) => setState(() => _isSearchEditing = false),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_isSearching) const SizedBox(width: 12, height: 12, child: CircularProgressIndicator(strokeWidth: 2)),
|
||||
if (_searchQuery.isNotEmpty)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
_searchController.clear();
|
||||
_onSearchChanged('');
|
||||
},
|
||||
child: Icon(Icons.close, size: 16, color: AppDecorations.textSecondary(context)),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Navigation Row
|
||||
Row(
|
||||
children: [
|
||||
if (showingSeries) ...[
|
||||
IconButton(
|
||||
icon: Icon(Icons.arrow_back, color: AppDecorations.textPrimary(context)),
|
||||
onPressed: _onBack,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
showingSeries
|
||||
? (_selectedCategoryName ?? 'Séries')
|
||||
: showingSearch
|
||||
? 'Résultats de recherche'
|
||||
: 'Catégories',
|
||||
style: TextStyle(
|
||||
color: AppDecorations.textPrimary(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -396,7 +406,7 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab>
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 80),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: showingCategories ? 3 : 4,
|
||||
crossAxisCount: showingCategories ? 6 : 8,
|
||||
crossAxisSpacing: 10,
|
||||
mainAxisSpacing: 10,
|
||||
childAspectRatio: showingCategories ? 2.0 : 0.67,
|
||||
|
||||
@@ -117,10 +117,6 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
// === APPARENCE ===
|
||||
_buildSectionHeader('Apparence'),
|
||||
|
||||
_buildThemeSwitcher(),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
_buildSettingItem(
|
||||
icon: Icons.access_time,
|
||||
title: 'Afficher l\'heure',
|
||||
@@ -360,8 +356,9 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
||||
final selected = themeState.appThemeMode == mode;
|
||||
final accentColor = Theme.of(context).colorScheme.primary;
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => notifier.setThemeMode(mode),
|
||||
child: TVFocusable(
|
||||
onPressed: () => notifier.setThemeMode(mode),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||
|
||||
Reference in new issue
Block a user