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
+114 -76

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 // Title
Text( Text(
isEditing ? 'Modifier la playlist' : 'Ajouter une playlist', isEditing ? 'Modifier la playlist' : 'Ajouter une playlist',
style: const TextStyle( style: const TextStyle(
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: AppColors.textPrimary, color: AppColors.textPrimary,
), ),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
// Form fields // Form fields
_buildTextField( _buildTextField(
controller: nameController, controller: nameController,
label: 'Nom', label: 'Nom',
hint: 'Ma Playlist IPTV', hint: 'Ma Playlist IPTV',
icon: Icons.label_outline, icon: Icons.label_outline,
), textAction: TextInputAction.next,
const SizedBox(height: 16), onSubmitted: () => FocusScope.of(context).requestFocus(dnsFocus),
_buildTextField( autofocus: !isEditing, // Auto-focus first field when adding
controller: dnsController, focusNode: nameFocus,
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 const SizedBox(height: 16),
SizedBox( _buildTextField(
width: double.infinity, controller: dnsController,
child: FilledButton.icon( label: 'Serveur URL',
onPressed: () async { hint: 'http://example.com:8080',
if (nameController.text.isEmpty || icon: Icons.dns_outlined,
dnsController.text.isEmpty || keyboardType: TextInputType.url,
usernameController.text.isEmpty || textAction: TextInputAction.next,
passwordController.text.isEmpty) { onSubmitted: () => FocusScope.of(context).requestFocus(usernameFocus),
ScaffoldMessenger.of(context).showSnackBar( focusNode: dnsFocus,
const SnackBar( ),
content: Text('Veuillez remplir tous les champs'),
backgroundColor: AppColors.error, const SizedBox(height: 16),
), _buildTextField(
); controller: usernameController,
return; 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 // 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,