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
+39 -1

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,8 +68,11 @@ class _MobilePlaylistScreenState extends ConsumerState<MobilePlaylistScreen> {
padding: EdgeInsets.only( padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom, bottom: MediaQuery.of(context).viewInsets.bottom,
), ),
child: FocusTraversalGroup(
policy: OrderedTraversalPolicy(), // Ensure logical order
child: Padding( child: Padding(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
child: SingleChildScrollView( // Ensure scrolling when keyboard/focus moves
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -97,7 +107,12 @@ class _MobilePlaylistScreenState extends ConsumerState<MobilePlaylistScreen> {
label: 'Nom', label: 'Nom',
hint: 'Ma Playlist IPTV', hint: 'Ma Playlist IPTV',
icon: Icons.label_outline, 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), const SizedBox(height: 16),
_buildTextField( _buildTextField(
controller: dnsController, controller: dnsController,
@@ -105,14 +120,22 @@ class _MobilePlaylistScreenState extends ConsumerState<MobilePlaylistScreen> {
hint: 'http://example.com:8080', hint: 'http://example.com:8080',
icon: Icons.dns_outlined, icon: Icons.dns_outlined,
keyboardType: TextInputType.url, keyboardType: TextInputType.url,
textAction: TextInputAction.next,
onSubmitted: () => FocusScope.of(context).requestFocus(usernameFocus),
focusNode: dnsFocus,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildTextField( _buildTextField(
controller: usernameController, controller: usernameController,
label: 'Nom d\'utilisateur', label: 'Nom d\'utilisateur',
hint: 'username', hint: 'username',
icon: Icons.person_outline, icon: Icons.person_outline,
textAction: TextInputAction.next,
onSubmitted: () => FocusScope.of(context).requestFocus(passwordFocus),
focusNode: usernameFocus,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildTextField( _buildTextField(
controller: passwordController, controller: passwordController,
@@ -120,13 +143,18 @@ class _MobilePlaylistScreenState extends ConsumerState<MobilePlaylistScreen> {
hint: '••••••••', hint: '••••••••',
icon: Icons.lock_outline, icon: Icons.lock_outline,
obscureText: true, obscureText: true,
textAction: TextInputAction.done,
onSubmitted: () => FocusScope.of(context).requestFocus(buttonFocus),
focusNode: passwordFocus,
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
// Save button // Save button
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: FilledButton.icon( child: FilledButton.icon(
focusNode: buttonFocus,
onPressed: () async { onPressed: () async {
if (nameController.text.isEmpty || if (nameController.text.isEmpty ||
dnsController.text.isEmpty || dnsController.text.isEmpty ||
@@ -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,