mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-23 07:08:16 +02:00
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:
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,
|
||||||
|
|||||||
Reference in new issue
Block a user