From 7fbd87e929c759d48cde0430c3aa073df27269bb Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Wed, 10 Dec 2025 19:23:24 +0100 Subject: [PATCH] v1.1.6: Fix Android TV navigation in playlist creation - Added FocusTraversalGroup for D-pad navigation - Handled text field submission to focus next field - Configured keyboard actions (Next/Done) - Added SingleChildScrollView preventing focus/keyboard issues --- .../iptv/screens/mobile_playlist_screen.dart | 194 +++++++++++------- .../iptv/widgets/mobile_settings_tab.dart | 2 +- 2 files changed, 117 insertions(+), 79 deletions(-) diff --git a/lib/mobile/features/iptv/screens/mobile_playlist_screen.dart b/lib/mobile/features/iptv/screens/mobile_playlist_screen.dart index 1552b47..cc10042 100644 --- a/lib/mobile/features/iptv/screens/mobile_playlist_screen.dart +++ b/lib/mobile/features/iptv/screens/mobile_playlist_screen.dart @@ -49,6 +49,13 @@ class _MobilePlaylistScreenState extends ConsumerState { final usernameController = TextEditingController(text: playlist?.username ?? ''); final passwordController = TextEditingController(text: playlist?.password ?? ''); + // Focus nodes for TV navigation + final nameFocus = FocusNode(); + final dnsFocus = FocusNode(); + final usernameFocus = FocusNode(); + final passwordFocus = FocusNode(); + final buttonFocus = FocusNode(); + showModalBottomSheet( context: context, isScrollControlled: true, @@ -61,85 +68,106 @@ class _MobilePlaylistScreenState extends ConsumerState { padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Handle bar - Center( - child: Container( - width: 40, - height: 4, - decoration: BoxDecoration( - color: AppColors.textTertiary, - borderRadius: BorderRadius.circular(2), + child: FocusTraversalGroup( + policy: OrderedTraversalPolicy(), // Ensure logical order + child: Padding( + padding: const EdgeInsets.all(24), + child: SingleChildScrollView( // Ensure scrolling when keyboard/focus moves + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Handle bar + Center( + child: Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: AppColors.textTertiary, + borderRadius: BorderRadius.circular(2), + ), + ), ), - ), - ), - const SizedBox(height: 24), - - // Title - Text( - isEditing ? 'Modifier la playlist' : 'Ajouter une playlist', - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: AppColors.textPrimary, - ), - ), - const SizedBox(height: 24), - - // Form fields - _buildTextField( - controller: nameController, - label: 'Nom', - hint: 'Ma Playlist IPTV', - icon: Icons.label_outline, - ), - const SizedBox(height: 16), - _buildTextField( - controller: dnsController, - label: 'Serveur URL', - hint: 'http://example.com:8080', - icon: Icons.dns_outlined, - keyboardType: TextInputType.url, - ), - const SizedBox(height: 16), - _buildTextField( - controller: usernameController, - label: 'Nom d\'utilisateur', - hint: 'username', - icon: Icons.person_outline, - ), - const SizedBox(height: 16), - _buildTextField( - controller: passwordController, - label: 'Mot de passe', - hint: '••••••••', - icon: Icons.lock_outline, - obscureText: true, - ), - const SizedBox(height: 32), - - // Save button - SizedBox( - width: double.infinity, - child: FilledButton.icon( - onPressed: () async { - if (nameController.text.isEmpty || - dnsController.text.isEmpty || - usernameController.text.isEmpty || - passwordController.text.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Veuillez remplir tous les champs'), - backgroundColor: AppColors.error, - ), - ); - return; - } + const SizedBox(height: 24), + + // Title + Text( + isEditing ? 'Modifier la playlist' : 'Ajouter une playlist', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: 24), + + // Form fields + _buildTextField( + controller: nameController, + label: 'Nom', + hint: 'Ma Playlist IPTV', + icon: Icons.label_outline, + textAction: TextInputAction.next, + onSubmitted: () => FocusScope.of(context).requestFocus(dnsFocus), + autofocus: !isEditing, // Auto-focus first field when adding + focusNode: nameFocus, + ), + + const SizedBox(height: 16), + _buildTextField( + controller: dnsController, + label: 'Serveur URL', + hint: 'http://example.com:8080', + icon: Icons.dns_outlined, + keyboardType: TextInputType.url, + textAction: TextInputAction.next, + onSubmitted: () => FocusScope.of(context).requestFocus(usernameFocus), + focusNode: dnsFocus, + ), + + const SizedBox(height: 16), + _buildTextField( + controller: usernameController, + label: 'Nom d\'utilisateur', + hint: 'username', + icon: Icons.person_outline, + textAction: TextInputAction.next, + onSubmitted: () => FocusScope.of(context).requestFocus(passwordFocus), + focusNode: usernameFocus, + ), + + const SizedBox(height: 16), + _buildTextField( + controller: passwordController, + label: 'Mot de passe', + hint: '••••••••', + icon: Icons.lock_outline, + obscureText: true, + textAction: TextInputAction.done, + onSubmitted: () => FocusScope.of(context).requestFocus(buttonFocus), + focusNode: passwordFocus, + ), + + const SizedBox(height: 32), + + // Save button + SizedBox( + width: double.infinity, + child: FilledButton.icon( + focusNode: buttonFocus, + onPressed: () async { + if (nameController.text.isEmpty || + dnsController.text.isEmpty || + usernameController.text.isEmpty || + passwordController.text.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Veuillez remplir tous les champs'), + backgroundColor: AppColors.error, + ), + ); + return; + } // Auto-fix URL scheme String dns = dnsController.text.trim(); @@ -188,6 +216,8 @@ class _MobilePlaylistScreenState extends ConsumerState { ), ), ), + ), + ), ); } @@ -198,11 +228,19 @@ class _MobilePlaylistScreenState extends ConsumerState { required IconData icon, bool obscureText = false, TextInputType? keyboardType, + TextInputAction? textAction, + VoidCallback? onSubmitted, + bool autofocus = false, + FocusNode? focusNode, }) { return TextField( controller: controller, + focusNode: focusNode, obscureText: obscureText, keyboardType: keyboardType, + textInputAction: textAction, + onSubmitted: (_) => onSubmitted?.call(), + autofocus: autofocus, style: const TextStyle(color: AppColors.textPrimary), decoration: InputDecoration( labelText: label, diff --git a/lib/mobile/features/iptv/widgets/mobile_settings_tab.dart b/lib/mobile/features/iptv/widgets/mobile_settings_tab.dart index 02025ba..7eee10b 100644 --- a/lib/mobile/features/iptv/widgets/mobile_settings_tab.dart +++ b/lib/mobile/features/iptv/widgets/mobile_settings_tab.dart @@ -108,7 +108,7 @@ class _MobileSettingsTabState extends ConsumerState { ), SizedBox(height: 4), Text( - 'Version 1.1.5', + 'Version 1.1.6', style: TextStyle( fontSize: 13, color: AppColors.textSecondary,