mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-24 15:48:14 +02:00
v1.1.8: Fix Android TV navigation with dual-focus system
- Implemented _TVTextField wrapper for playlist fields - D-pad navigation moves focus without opening keyboard - 'OK/Select' press opens keyboard for editing - Fixed missing classes and imports in MobilePlaylistScreen
This commit is contained in:
1 parent
8eb9384a9e
commit
2d332a50e8
2 files changed
+141
-26
No files matched your search
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../../../../core/models/playlist_config.dart';
|
import '../../../../core/models/playlist_config.dart';
|
||||||
import '../../../../core/theme/app_colors.dart';
|
import '../../../../core/theme/app_colors.dart';
|
||||||
@@ -229,38 +230,29 @@ class _MobilePlaylistScreenState extends ConsumerState<MobilePlaylistScreen> {
|
|||||||
TextInputType? keyboardType,
|
TextInputType? keyboardType,
|
||||||
TextInputAction? textAction,
|
TextInputAction? textAction,
|
||||||
VoidCallback? onSubmitted,
|
VoidCallback? onSubmitted,
|
||||||
bool autofocus = false,
|
FocusNode? focusNode, // This is now the "navigation" focus
|
||||||
FocusNode? focusNode,
|
|
||||||
}) {
|
}) {
|
||||||
return TextField(
|
// Create a dedicated internal focus node for the actual TextField
|
||||||
|
// We don't expose this, allowing us to control when the keyboard opens
|
||||||
|
|
||||||
|
// Since we are inside a stateless method but need state for focus nodes if we want to create them on the fly...
|
||||||
|
// Actually, to avoid State complexity, we will trust the passed 'focusNode' is for NAVIGATION.
|
||||||
|
// And we will use a LayoutBuilder + Stateful wrapper or just a customized Focus widget.
|
||||||
|
|
||||||
|
return _TVTextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
focusNode: focusNode,
|
label: label,
|
||||||
|
hint: hint,
|
||||||
|
icon: icon,
|
||||||
obscureText: obscureText,
|
obscureText: obscureText,
|
||||||
keyboardType: keyboardType,
|
keyboardType: keyboardType,
|
||||||
textInputAction: textAction,
|
textAction: textAction,
|
||||||
onSubmitted: (_) => onSubmitted?.call(),
|
onSubmitted: onSubmitted,
|
||||||
autofocus: autofocus,
|
navigationFocus: focusNode,
|
||||||
style: const TextStyle(color: AppColors.textPrimary),
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: label,
|
|
||||||
hintText: hint,
|
|
||||||
prefixIcon: Icon(icon, color: AppColors.textSecondary),
|
|
||||||
labelStyle: const TextStyle(color: AppColors.textSecondary),
|
|
||||||
hintStyle: const TextStyle(color: AppColors.textTertiary),
|
|
||||||
filled: true,
|
|
||||||
fillColor: AppColors.background,
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
borderSide: BorderSide.none,
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
borderSide: const BorderSide(color: AppColors.primary),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void _showOptionsSheet(PlaylistConfig playlist) {
|
void _showOptionsSheet(PlaylistConfig playlist) {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -542,3 +534,126 @@ class _PlaylistCard extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _TVTextField extends StatefulWidget {
|
||||||
|
final TextEditingController controller;
|
||||||
|
final String label;
|
||||||
|
final String hint;
|
||||||
|
final IconData icon;
|
||||||
|
final bool obscureText;
|
||||||
|
final TextInputType? keyboardType;
|
||||||
|
final TextInputAction? textAction;
|
||||||
|
final VoidCallback? onSubmitted;
|
||||||
|
final FocusNode? navigationFocus;
|
||||||
|
|
||||||
|
const _TVTextField({
|
||||||
|
required this.controller,
|
||||||
|
required this.label,
|
||||||
|
required this.hint,
|
||||||
|
required this.icon,
|
||||||
|
this.obscureText = false,
|
||||||
|
this.keyboardType,
|
||||||
|
this.textAction,
|
||||||
|
this.onSubmitted,
|
||||||
|
this.navigationFocus,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_TVTextField> createState() => _TVTextFieldState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TVTextFieldState extends State<_TVTextField> {
|
||||||
|
late FocusNode _inputFocus;
|
||||||
|
late FocusNode _navFocus;
|
||||||
|
bool _isEditing = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_inputFocus = FocusNode();
|
||||||
|
_navFocus = widget.navigationFocus ?? FocusNode();
|
||||||
|
|
||||||
|
// When input loses focus (keyboard closed), return to nav focus
|
||||||
|
_inputFocus.addListener(() {
|
||||||
|
if (!_inputFocus.hasFocus && _isEditing) {
|
||||||
|
setState(() => _isEditing = false);
|
||||||
|
_navFocus.requestFocus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_inputFocus.dispose();
|
||||||
|
if (widget.navigationFocus == null) _navFocus.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _activateInput() {
|
||||||
|
setState(() => _isEditing = true);
|
||||||
|
_inputFocus.requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Focus(
|
||||||
|
focusNode: _navFocus,
|
||||||
|
onKeyEvent: (node, event) {
|
||||||
|
if (event is KeyDownEvent) {
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.select ||
|
||||||
|
event.logicalKey == LogicalKeyboardKey.enter ||
|
||||||
|
event.logicalKey == LogicalKeyboardKey.space) {
|
||||||
|
_activateInput();
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
},
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
final isFocused = Focus.of(context).hasFocus;
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: _activateInput,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: isFocused ? Border.all(color: AppColors.primary, width: 2) : null,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
controller: widget.controller,
|
||||||
|
focusNode: _inputFocus,
|
||||||
|
obscureText: widget.obscureText,
|
||||||
|
keyboardType: widget.keyboardType,
|
||||||
|
textInputAction: widget.textAction,
|
||||||
|
onSubmitted: (_) {
|
||||||
|
setState(() => _isEditing = false);
|
||||||
|
// Give focus back to nav node first so we can move to next
|
||||||
|
_navFocus.requestFocus();
|
||||||
|
widget.onSubmitted?.call();
|
||||||
|
},
|
||||||
|
style: const TextStyle(color: AppColors.textPrimary),
|
||||||
|
// Important: Disable standard focus behavior to prevent keyboard popup
|
||||||
|
// unless we explicitly requested it via _activateInput
|
||||||
|
canRequestFocus: _isEditing,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: widget.label,
|
||||||
|
hintText: widget.hint,
|
||||||
|
prefixIcon: Icon(widget.icon, color: isFocused ? AppColors.primary : AppColors.textSecondary),
|
||||||
|
labelStyle: TextStyle(color: isFocused ? AppColors.primary : AppColors.textSecondary),
|
||||||
|
hintStyle: const TextStyle(color: AppColors.textTertiary),
|
||||||
|
filled: true,
|
||||||
|
fillColor: AppColors.surface,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -108,7 +108,7 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
|||||||
),
|
),
|
||||||
SizedBox(height: 4),
|
SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'Version 1.1.7',
|
'Version 1.1.8',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: AppColors.textSecondary,
|
color: AppColors.textSecondary,
|
||||||
|
|||||||
Reference in new issue
Block a user