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
This commit is contained in:
Michael committed 2025-12-10 19:23:24 +01:00
1 parent 1841f7f706
commit 7fbd87e929
2 files changed
+117 -79

No files matched your search

@@ -49,6 +49,13 @@ class _MobilePlaylistScreenState extends ConsumerState<MobilePlaylistScreen> {
final usernameController = TextEditingController(text: playlist?.username ?? ''); final usernameController = TextEditingController(text: playlist?.username ?? '');
final passwordController = TextEditingController(text: playlist?.password ?? ''); 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( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
@@ -61,85 +68,106 @@ class _MobilePlaylistScreenState extends ConsumerState<MobilePlaylistScreen> {
padding: EdgeInsets.only( padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom, bottom: MediaQuery.of(context).viewInsets.bottom,
), ),
child: Padding( child: FocusTraversalGroup(
padding: const EdgeInsets.all(24), policy: OrderedTraversalPolicy(), // Ensure logical order
child: Column( child: Padding(
mainAxisSize: MainAxisSize.min, padding: const EdgeInsets.all(24),
crossAxisAlignment: CrossAxisAlignment.start, child: SingleChildScrollView( // Ensure scrolling when keyboard/focus moves
children: [ child: Column(
// Handle bar mainAxisSize: MainAxisSize.min,
Center( crossAxisAlignment: CrossAxisAlignment.start,
child: Container( children: [
width: 40, // Handle bar
height: 4, Center(
decoration: BoxDecoration( child: Container(
color: AppColors.textTertiary, width: 40,
borderRadius: BorderRadius.circular(2), height: 4,
decoration: BoxDecoration(
color: AppColors.textTertiary,
borderRadius: BorderRadius.circular(2),
),
),
), ),
), const SizedBox(height: 24),
),
const SizedBox(height: 24), // Title
Text(
// Title isEditing ? 'Modifier la playlist' : 'Ajouter une playlist',
Text( style: const TextStyle(
isEditing ? 'Modifier la playlist' : 'Ajouter une playlist', fontSize: 20,
style: const TextStyle( fontWeight: FontWeight.bold,
fontSize: 20, color: AppColors.textPrimary,
fontWeight: FontWeight.bold, ),
color: AppColors.textPrimary, ),
), const SizedBox(height: 24),
),
const SizedBox(height: 24), // Form fields
_buildTextField(
// Form fields controller: nameController,
_buildTextField( label: 'Nom',
controller: nameController, hint: 'Ma Playlist IPTV',
label: 'Nom', icon: Icons.label_outline,
hint: 'Ma Playlist IPTV', textAction: TextInputAction.next,
icon: Icons.label_outline, onSubmitted: () => FocusScope.of(context).requestFocus(dnsFocus),
), autofocus: !isEditing, // Auto-focus first field when adding
const SizedBox(height: 16), focusNode: nameFocus,
_buildTextField( ),
controller: dnsController,
label: 'Serveur URL', const SizedBox(height: 16),
hint: 'http://example.com:8080', _buildTextField(
icon: Icons.dns_outlined, controller: dnsController,
keyboardType: TextInputType.url, label: 'Serveur URL',
), hint: 'http://example.com:8080',
const SizedBox(height: 16), icon: Icons.dns_outlined,
_buildTextField( keyboardType: TextInputType.url,
controller: usernameController, textAction: TextInputAction.next,
label: 'Nom d\'utilisateur', onSubmitted: () => FocusScope.of(context).requestFocus(usernameFocus),
hint: 'username', focusNode: dnsFocus,
icon: Icons.person_outline, ),
),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildTextField( _buildTextField(
controller: passwordController, controller: usernameController,
label: 'Mot de passe', label: 'Nom d\'utilisateur',
hint: '••••••••', hint: 'username',
icon: Icons.lock_outline, icon: Icons.person_outline,
obscureText: true, textAction: TextInputAction.next,
), onSubmitted: () => FocusScope.of(context).requestFocus(passwordFocus),
const SizedBox(height: 32), focusNode: usernameFocus,
),
// Save button
SizedBox( const SizedBox(height: 16),
width: double.infinity, _buildTextField(
child: FilledButton.icon( controller: passwordController,
onPressed: () async { label: 'Mot de passe',
if (nameController.text.isEmpty || hint: '••••••••',
dnsController.text.isEmpty || icon: Icons.lock_outline,
usernameController.text.isEmpty || obscureText: true,
passwordController.text.isEmpty) { textAction: TextInputAction.done,
ScaffoldMessenger.of(context).showSnackBar( onSubmitted: () => FocusScope.of(context).requestFocus(buttonFocus),
const SnackBar( focusNode: passwordFocus,
content: Text('Veuillez remplir tous les champs'), ),
backgroundColor: AppColors.error,
), const SizedBox(height: 32),
);
return; // 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 // Auto-fix URL scheme
String dns = dnsController.text.trim(); String dns = dnsController.text.trim();
@@ -188,6 +216,8 @@ class _MobilePlaylistScreenState extends ConsumerState<MobilePlaylistScreen> {
), ),
), ),
), ),
),
),
); );
} }
@@ -198,11 +228,19 @@ class _MobilePlaylistScreenState extends ConsumerState<MobilePlaylistScreen> {
required IconData icon, required IconData icon,
bool obscureText = false, bool obscureText = false,
TextInputType? keyboardType, TextInputType? keyboardType,
TextInputAction? textAction,
VoidCallback? onSubmitted,
bool autofocus = false,
FocusNode? focusNode,
}) { }) {
return TextField( return TextField(
controller: controller, controller: controller,
focusNode: focusNode,
obscureText: obscureText, obscureText: obscureText,
keyboardType: keyboardType, keyboardType: keyboardType,
textInputAction: textAction,
onSubmitted: (_) => onSubmitted?.call(),
autofocus: autofocus,
style: const TextStyle(color: AppColors.textPrimary), style: const TextStyle(color: AppColors.textPrimary),
decoration: InputDecoration( decoration: InputDecoration(
labelText: label, labelText: label,
@@ -108,7 +108,7 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
), ),
SizedBox(height: 4), SizedBox(height: 4),
Text( Text(
'Version 1.1.5', 'Version 1.1.6',
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: AppColors.textSecondary, color: AppColors.textSecondary,