fix: dpad focus for dialogs and buttons

This commit is contained in:
edde746
2026-02-22 14:50:59 +01:00
parent 1536feb034
commit 7f8d4c7946
20 changed files with 450 additions and 161 deletions
+2 -1
View File
@@ -29,8 +29,9 @@ class FocusTheme {
BuildContext context, { BuildContext context, {
required bool isFocused, required bool isFocused,
double borderRadius = defaultBorderRadius, double borderRadius = defaultBorderRadius,
Color? color,
}) { }) {
final focusColor = getFocusBorderColor(context); final focusColor = color ?? getFocusBorderColor(context);
return BoxDecoration( return BoxDecoration(
borderRadius: BorderRadius.circular(borderRadius), borderRadius: BorderRadius.circular(borderRadius),
+90
View File
@@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
import 'focus_theme.dart';
import 'focusable_wrapper.dart';
import 'input_mode_tracker.dart';
/// A focusable button wrapper for D-pad navigation on TV.
///
/// Wraps any button widget with [FocusableWrapper] and adds a white overlay
/// + contrasting border when focused. Tracks focus state internally so callers
/// don't need manual state management.
///
/// ```dart
/// FocusableButton(
/// autofocus: true,
/// onPressed: _doSomething,
/// child: FilledButton.icon(
/// onPressed: _doSomething,
/// icon: Icon(Symbols.add_rounded),
/// label: Text('Create'),
/// ),
/// )
/// ```
class FocusableButton extends StatefulWidget {
final Widget child;
final VoidCallback? onPressed;
final bool autofocus;
final FocusNode? focusNode;
/// Navigation callbacks for explicit focus control (e.g. horizontal button rows).
final VoidCallback? onNavigateUp;
final VoidCallback? onNavigateDown;
final VoidCallback? onNavigateLeft;
final VoidCallback? onNavigateRight;
final VoidCallback? onBack;
/// Whether to scroll the widget into view when focused.
final bool autoScroll;
const FocusableButton({
super.key,
required this.child,
this.onPressed,
this.autofocus = false,
this.focusNode,
this.onNavigateUp,
this.onNavigateDown,
this.onNavigateLeft,
this.onNavigateRight,
this.onBack,
this.autoScroll = true,
});
@override
State<FocusableButton> createState() => _FocusableButtonState();
}
class _FocusableButtonState extends State<FocusableButton> {
bool _isFocused = false;
@override
Widget build(BuildContext context) {
final isKeyboard = InputModeTracker.isKeyboardMode(context);
final showFocus = _isFocused && isKeyboard;
final duration = FocusTheme.getAnimationDuration(context);
// In dpad mode: focused = full opacity, unfocused = dimmed
final opacity = isKeyboard && !_isFocused ? 0.6 : 1.0;
return FocusableWrapper(
autofocus: widget.autofocus,
focusNode: widget.focusNode,
disableScale: true,
borderRadius: 100,
descendantsAreFocusable: false,
onFocusChange: (f) => setState(() => _isFocused = f),
autoScroll: widget.autoScroll,
onSelect: widget.onPressed,
onNavigateUp: widget.onNavigateUp,
onNavigateDown: widget.onNavigateDown,
onNavigateLeft: widget.onNavigateLeft,
onNavigateRight: widget.onNavigateRight,
onBack: widget.onBack,
child: AnimatedOpacity(
opacity: showFocus ? 1.0 : opacity,
duration: duration,
child: widget.child,
),
);
}
}
+23 -1
View File
@@ -33,6 +33,9 @@ class FocusableWrapper extends StatefulWidget {
/// Called when the user presses UP and there's no focusable item above. /// Called when the user presses UP and there's no focusable item above.
final VoidCallback? onNavigateUp; final VoidCallback? onNavigateUp;
/// Called when the user presses DOWN and there's no focusable item below.
final VoidCallback? onNavigateDown;
/// Called when the user presses LEFT and there's no focusable item to the left. /// Called when the user presses LEFT and there's no focusable item to the left.
final VoidCallback? onNavigateLeft; final VoidCallback? onNavigateLeft;
@@ -83,10 +86,19 @@ class FocusableWrapper extends StatefulWidget {
/// Useful for video controls where outline doesn't look good. /// Useful for video controls where outline doesn't look good.
final bool useBackgroundFocus; final bool useBackgroundFocus;
/// Custom color for the focus border. Only used when [useBackgroundFocus] is false.
/// Useful for filled buttons where the default primary border blends in.
final Color? focusColor;
/// Whether to disable the scale animation on focus. /// Whether to disable the scale animation on focus.
/// Useful for elements like sliders where scaling looks odd. /// Useful for elements like sliders where scaling looks odd.
final bool disableScale; final bool disableScale;
/// Whether descendants can receive focus.
/// Set to false when the child widget has its own Focus (e.g. buttons)
/// that would compete with this wrapper's focus handling.
final bool descendantsAreFocusable;
const FocusableWrapper({ const FocusableWrapper({
super.key, super.key,
required this.child, required this.child,
@@ -94,6 +106,7 @@ class FocusableWrapper extends StatefulWidget {
this.onLongPress, this.onLongPress,
this.onFocusChange, this.onFocusChange,
this.onNavigateUp, this.onNavigateUp,
this.onNavigateDown,
this.onNavigateLeft, this.onNavigateLeft,
this.onNavigateRight, this.onNavigateRight,
this.onBack, this.onBack,
@@ -109,7 +122,9 @@ class FocusableWrapper extends StatefulWidget {
this.enableLongPress = false, this.enableLongPress = false,
this.longPressDuration = const Duration(milliseconds: 500), this.longPressDuration = const Duration(milliseconds: 500),
this.useBackgroundFocus = false, this.useBackgroundFocus = false,
this.focusColor,
this.disableScale = false, this.disableScale = false,
this.descendantsAreFocusable = true,
}); });
@override @override
@@ -367,6 +382,12 @@ class _FocusableWrapperState extends State<FocusableWrapper> with SingleTickerPr
return KeyEventResult.handled; return KeyEventResult.handled;
} }
// DOWN arrow - if callback provided, navigate down
if (key == LogicalKeyboardKey.arrowDown && widget.onNavigateDown != null) {
widget.onNavigateDown!();
return KeyEventResult.handled;
}
// LEFT arrow - if callback provided, navigate left (caller is responsible // LEFT arrow - if callback provided, navigate left (caller is responsible
// for only providing this callback when the item is at the left edge) // for only providing this callback when the item is at the left edge)
if (key == LogicalKeyboardKey.arrowLeft && widget.onNavigateLeft != null) { if (key == LogicalKeyboardKey.arrowLeft && widget.onNavigateLeft != null) {
@@ -398,11 +419,12 @@ class _FocusableWrapperState extends State<FocusableWrapper> with SingleTickerPr
// Choose decoration based on useBackgroundFocus // Choose decoration based on useBackgroundFocus
final decoration = widget.useBackgroundFocus final decoration = widget.useBackgroundFocus
? FocusTheme.focusBackgroundDecoration(isFocused: showFocus, borderRadius: widget.borderRadius) ? FocusTheme.focusBackgroundDecoration(isFocused: showFocus, borderRadius: widget.borderRadius)
: FocusTheme.focusDecoration(context, isFocused: showFocus, borderRadius: widget.borderRadius); : FocusTheme.focusDecoration(context, isFocused: showFocus, borderRadius: widget.borderRadius, color: widget.focusColor);
Widget result = Focus( Widget result = Focus(
focusNode: _focusNode, focusNode: _focusNode,
autofocus: widget.autofocus, autofocus: widget.autofocus,
descendantsAreFocusable: widget.descendantsAreFocusable,
onFocusChange: _handleFocusChange, onFocusChange: _handleFocusChange,
onKeyEvent: _handleKeyEvent, onKeyEvent: _handleKeyEvent,
child: AnimatedBuilder( child: AnimatedBuilder(
+35 -12
View File
@@ -16,6 +16,7 @@ import '../i18n/strings.g.dart';
import '../theme/mono_tokens.dart'; import '../theme/mono_tokens.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import '../utils/platform_detector.dart'; import '../utils/platform_detector.dart';
import '../focus/focusable_button.dart';
import 'main_screen.dart'; import 'main_screen.dart';
class AuthScreen extends StatefulWidget { class AuthScreen extends StatefulWidget {
@@ -398,7 +399,7 @@ class _AuthScreenState extends State<AuthScreen> {
children: [ children: [
if (isTV) ...[ if (isTV) ...[
// On TV: QR is primary, browser is secondary // On TV: QR is primary, browser is secondary
ElevatedButton( FocusableButton(
autofocus: true, autofocus: true,
onPressed: () { onPressed: () {
setState(() { setState(() {
@@ -406,14 +407,25 @@ class _AuthScreenState extends State<AuthScreen> {
}); });
_startAuthentication(); _startAuthentication();
}, },
style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)), child: ElevatedButton(
child: Text(t.auth.showQRCode), onPressed: () {
setState(() {
_useQrFlow = true;
});
_startAuthentication();
},
style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
child: Text(t.auth.showQRCode),
),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
OutlinedButton( FocusableButton(
onPressed: _startAuthentication, onPressed: _startAuthentication,
style: OutlinedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)), child: OutlinedButton(
child: Text(t.auth.useBrowser), onPressed: _startAuthentication,
style: OutlinedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
child: Text(t.auth.useBrowser),
),
), ),
] else ...[ ] else ...[
// On other platforms: Browser is primary, QR is secondary // On other platforms: Browser is primary, QR is secondary
@@ -500,22 +512,33 @@ class _AuthScreenState extends State<AuthScreen> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
OutlinedButton( FocusableButton(
autofocus: true, autofocus: true,
onPressed: _retryAuthentication, onPressed: _retryAuthentication,
style: OutlinedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24)), child: OutlinedButton(
child: Text(t.common.retry), onPressed: _retryAuthentication,
style: OutlinedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24)),
child: Text(t.common.retry),
),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
OutlinedButton( FocusableButton(
onPressed: () { onPressed: () {
setState(() { setState(() {
_useQrFlow = false; _useQrFlow = false;
}); });
_startAuthentication(); _startAuthentication();
}, },
style: OutlinedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24)), child: OutlinedButton(
child: Text(t.auth.useBrowser), onPressed: () {
setState(() {
_useQrFlow = false;
});
_startAuthentication();
},
style: OutlinedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24)),
child: Text(t.auth.useBrowser),
),
), ),
], ],
), ),
@@ -5,6 +5,7 @@ import 'package:flutter/services.dart';
import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../focus/focusable_button.dart';
import '../../i18n/strings.g.dart'; import '../../i18n/strings.g.dart';
import '../../providers/companion_remote_provider.dart'; import '../../providers/companion_remote_provider.dart';
import '../../utils/formatters.dart'; import '../../utils/formatters.dart';
@@ -441,8 +442,18 @@ class _PairingScreenState extends State<PairingScreen> {
title: Text(t.companionRemote.pairing.removeRecentConnection), title: Text(t.companionRemote.pairing.removeRecentConnection),
content: Text(t.companionRemote.pairing.removeConfirm(name: session.deviceName)), content: Text(t.companionRemote.pairing.removeConfirm(name: session.deviceName)),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: Text(t.common.cancel)), FocusableButton(
TextButton(onPressed: () => Navigator.pop(context, true), child: Text(t.common.remove)), autofocus: true,
onPressed: () => Navigator.pop(context, false),
child: TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text(t.common.cancel),
),
),
FocusableButton(
onPressed: () => Navigator.pop(context, true),
child: TextButton(onPressed: () => Navigator.pop(context, true), child: Text(t.common.remove)),
),
], ],
), ),
); );
+15 -4
View File
@@ -4,6 +4,7 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import '../../focus/focusable_button.dart';
import '../../focus/dpad_navigator.dart'; import '../../focus/dpad_navigator.dart';
import '../../focus/focus_theme.dart'; import '../../focus/focus_theme.dart';
import '../../focus/input_mode_tracker.dart'; import '../../focus/input_mode_tracker.dart';
@@ -1480,11 +1481,21 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet> {
title: Text(selectedItem.confirmationTitle ?? t.dialog.confirmAction), title: Text(selectedItem.confirmationTitle ?? t.dialog.confirmAction),
content: Text(selectedItem.confirmationMessage ?? t.libraries.confirmActionMessage), content: Text(selectedItem.confirmationMessage ?? t.libraries.confirmActionMessage),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: Text(t.common.cancel)), FocusableButton(
TextButton( autofocus: true,
onPressed: () => Navigator.pop(context, false),
child: TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text(t.common.cancel),
),
),
FocusableButton(
onPressed: () => Navigator.pop(context, true), onPressed: () => Navigator.pop(context, true),
style: selectedItem.isDestructive ? TextButton.styleFrom(foregroundColor: Colors.red) : null, child: TextButton(
child: Text(t.common.confirm), onPressed: () => Navigator.pop(context, true),
style: selectedItem.isDestructive ? TextButton.styleFrom(foregroundColor: Colors.red) : null,
child: Text(t.common.confirm),
),
), ),
], ],
), ),
+29 -4
View File
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../focus/focusable_button.dart';
import '../../focus/focusable_wrapper.dart'; import '../../focus/focusable_wrapper.dart';
import '../../focus/key_event_utils.dart'; import '../../focus/key_event_utils.dart';
import '../../i18n/strings.g.dart'; import '../../i18n/strings.g.dart';
@@ -103,8 +104,18 @@ class _DvrRecordingsScreenState extends State<DvrRecordingsScreen> with SingleTi
title: Text(t.liveTv.deleteSubscription), title: Text(t.liveTv.deleteSubscription),
content: Text(t.liveTv.deleteSubscriptionConfirm), content: Text(t.liveTv.deleteSubscriptionConfirm),
actions: [ actions: [
TextButton(onPressed: () => Navigator.of(context).pop(false), child: Text(t.common.cancel)), FocusableButton(
FilledButton(onPressed: () => Navigator.of(context).pop(true), child: Text(t.common.delete)), autofocus: true,
onPressed: () => Navigator.of(context).pop(false),
child: TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(t.common.cancel),
),
),
FocusableButton(
onPressed: () => Navigator.of(context).pop(true),
child: FilledButton(onPressed: () => Navigator.of(context).pop(true), child: Text(t.common.delete)),
),
], ],
), ),
); );
@@ -320,6 +331,7 @@ class _SubscriptionEditDialog extends StatefulWidget {
class _SubscriptionEditDialogState extends State<_SubscriptionEditDialog> { class _SubscriptionEditDialogState extends State<_SubscriptionEditDialog> {
late Map<String, String> _prefs; late Map<String, String> _prefs;
final Map<String, TextEditingController> _textControllers = {}; final Map<String, TextEditingController> _textControllers = {};
final _saveFocusNode = FocusNode();
@override @override
void initState() { void initState() {
@@ -332,6 +344,7 @@ class _SubscriptionEditDialogState extends State<_SubscriptionEditDialog> {
for (final controller in _textControllers.values) { for (final controller in _textControllers.values) {
controller.dispose(); controller.dispose();
} }
_saveFocusNode.dispose();
super.dispose(); super.dispose();
} }
@@ -351,8 +364,18 @@ class _SubscriptionEditDialogState extends State<_SubscriptionEditDialog> {
), ),
), ),
actions: [ actions: [
TextButton(onPressed: () => Navigator.of(context).pop(null), child: Text(t.common.cancel)), FocusableButton(
FilledButton(onPressed: () => Navigator.of(context).pop(_prefs), child: Text(t.common.save)), onPressed: () => Navigator.of(context).pop(null),
child: TextButton(onPressed: () => Navigator.of(context).pop(null), child: Text(t.common.cancel)),
),
FocusableButton(
focusNode: _saveFocusNode,
onPressed: () => Navigator.of(context).pop(_prefs),
child: FilledButton(
onPressed: () => Navigator.of(context).pop(_prefs),
child: Text(t.common.save),
),
),
], ],
); );
} }
@@ -399,9 +422,11 @@ class _SubscriptionEditDialogState extends State<_SubscriptionEditDialog> {
title: Text(setting.label ?? setting.id), title: Text(setting.label ?? setting.id),
subtitle: TextField( subtitle: TextField(
controller: controller, controller: controller,
textInputAction: TextInputAction.done,
onChanged: (newValue) { onChanged: (newValue) {
_prefs[setting.id] = newValue; _prefs[setting.id] = newValue;
}, },
onSubmitted: (_) => _saveFocusNode.requestFocus(),
), ),
); );
} }
+5 -11
View File
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import '../../focus/focusable_wrapper.dart'; import '../../focus/focusable_button.dart';
import '../../i18n/strings.g.dart'; import '../../i18n/strings.g.dart';
import '../../models/livetv_channel.dart'; import '../../models/livetv_channel.dart';
import '../../models/livetv_program.dart'; import '../../models/livetv_program.dart';
@@ -119,18 +119,15 @@ class _ProgramDetailsSheetContentState extends State<_ProgramDetailsSheetContent
if (program.isCurrentlyAiring && widget.onTuneChannel != null) { if (program.isCurrentlyAiring && widget.onTuneChannel != null) {
final idx = buttonIndex; final idx = buttonIndex;
buttons.add( buttons.add(
FocusableWrapper( FocusableButton(
focusNode: _buttonFocusNodes[idx], focusNode: _buttonFocusNodes[idx],
onSelect: () { onPressed: () {
closeSheet(); closeSheet();
widget.onTuneChannel!(); widget.onTuneChannel!();
}, },
onNavigateLeft: idx > 0 ? () => _focusButton(idx - 1) : null, onNavigateLeft: idx > 0 ? () => _focusButton(idx - 1) : null,
onNavigateRight: idx < _buttonFocusNodes.length - 1 ? () => _focusButton(idx + 1) : null, onNavigateRight: idx < _buttonFocusNodes.length - 1 ? () => _focusButton(idx + 1) : null,
onBack: closeSheet, onBack: closeSheet,
borderRadius: 100,
useBackgroundFocus: true,
disableScale: true,
child: FilledButton.icon( child: FilledButton.icon(
style: FilledButton.styleFrom(tapTargetSize: MaterialTapTargetSize.shrinkWrap), style: FilledButton.styleFrom(tapTargetSize: MaterialTapTargetSize.shrinkWrap),
onPressed: () { onPressed: () {
@@ -181,18 +178,15 @@ class _ProgramDetailsSheetContentState extends State<_ProgramDetailsSheetContent
buttons.add(const SizedBox(width: 8)); buttons.add(const SizedBox(width: 8));
final idx = buttonIndex; final idx = buttonIndex;
buttons.add( buttons.add(
FocusableWrapper( FocusableButton(
focusNode: _buttonFocusNodes[idx], focusNode: _buttonFocusNodes[idx],
onSelect: () { onPressed: () {
closeSheet(); closeSheet();
widget.onTuneChannel!(); widget.onTuneChannel!();
}, },
onNavigateLeft: idx > 0 ? () => _focusButton(idx - 1) : null, onNavigateLeft: idx > 0 ? () => _focusButton(idx - 1) : null,
onNavigateRight: idx < _buttonFocusNodes.length - 1 ? () => _focusButton(idx + 1) : null, onNavigateRight: idx < _buttonFocusNodes.length - 1 ? () => _focusButton(idx + 1) : null,
onBack: closeSheet, onBack: closeSheet,
borderRadius: 100,
useBackgroundFocus: true,
disableScale: true,
child: OutlinedButton.icon( child: OutlinedButton.icon(
style: OutlinedButton.styleFrom(tapTargetSize: MaterialTapTargetSize.shrinkWrap), style: OutlinedButton.styleFrom(tapTargetSize: MaterialTapTargetSize.shrinkWrap),
onPressed: () { onPressed: () {
+31 -12
View File
@@ -10,6 +10,7 @@ import '../../services/plex_client.dart';
import '../i18n/strings.g.dart'; import '../i18n/strings.g.dart';
import '../services/update_service.dart'; import '../services/update_service.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import '../focus/focusable_button.dart';
import '../utils/dialogs.dart'; import '../utils/dialogs.dart';
import '../utils/provider_extensions.dart'; import '../utils/provider_extensions.dart';
import '../utils/platform_detector.dart'; import '../utils/platform_detector.dart';
@@ -226,27 +227,36 @@ class _MainScreenState extends State<MainScreen> with RouteAware, WindowListener
], ],
), ),
actions: [ actions: [
TextButton( FocusableButton(
autofocus: true, autofocus: true,
onPressed: () => Navigator.pop(dialogContext), onPressed: () => Navigator.pop(dialogContext),
style: TextButton.styleFrom( child: TextButton(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14), onPressed: () => Navigator.pop(dialogContext),
shape: const StadiumBorder(), style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
shape: const StadiumBorder(),
),
child: Text(t.common.later),
), ),
child: Text(t.common.later),
), ),
TextButton( FocusableButton(
onPressed: () async { onPressed: () async {
await UpdateService.skipVersion(updateInfo['latestVersion']); await UpdateService.skipVersion(updateInfo['latestVersion']);
if (dialogContext.mounted) Navigator.pop(dialogContext); if (dialogContext.mounted) Navigator.pop(dialogContext);
}, },
style: TextButton.styleFrom( child: TextButton(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14), onPressed: () async {
shape: const StadiumBorder(), await UpdateService.skipVersion(updateInfo['latestVersion']);
if (dialogContext.mounted) Navigator.pop(dialogContext);
},
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
shape: const StadiumBorder(),
),
child: Text(t.update.skipVersion),
), ),
child: Text(t.update.skipVersion),
), ),
FilledButton( FocusableButton(
onPressed: () async { onPressed: () async {
final url = Uri.parse(updateInfo['releaseUrl']); final url = Uri.parse(updateInfo['releaseUrl']);
if (await canLaunchUrl(url)) { if (await canLaunchUrl(url)) {
@@ -254,7 +264,16 @@ class _MainScreenState extends State<MainScreen> with RouteAware, WindowListener
} }
if (dialogContext.mounted) Navigator.pop(dialogContext); if (dialogContext.mounted) Navigator.pop(dialogContext);
}, },
child: Text(t.update.viewRelease), child: FilledButton(
onPressed: () async {
final url = Uri.parse(updateInfo['releaseUrl']);
if (await canLaunchUrl(url)) {
await launchUrl(url, mode: LaunchMode.externalApplication);
}
if (dialogContext.mounted) Navigator.pop(dialogContext);
},
child: Text(t.update.viewRelease),
),
), ),
], ],
); );
+12 -2
View File
@@ -8,6 +8,7 @@ import '../../focus/dpad_navigator.dart';
import '../../focus/focus_theme.dart'; import '../../focus/focus_theme.dart';
import '../../focus/input_mode_tracker.dart'; import '../../focus/input_mode_tracker.dart';
import '../../focus/key_event_utils.dart'; import '../../focus/key_event_utils.dart';
import '../../focus/focusable_button.dart';
import '../../i18n/strings.g.dart'; import '../../i18n/strings.g.dart';
import '../../utils/platform_detector.dart'; import '../../utils/platform_detector.dart';
import '../../widgets/app_icon.dart'; import '../../widgets/app_icon.dart';
@@ -100,9 +101,18 @@ class _PinEntryDialogState extends State<PinEntryDialog> with SingleTickerProvid
], ],
), ),
actions: [ actions: [
TextButton(onPressed: _cancel, child: Text(t.common.cancel)), FocusableButton(
onPressed: _cancel,
child: TextButton(onPressed: _cancel, child: Text(t.common.cancel)),
),
if (!isMobile) if (!isMobile)
FilledButton(onPressed: () => _pinInputKey.currentState?._trySubmit(), child: Text(t.common.submit)), FocusableButton(
onPressed: () => _pinInputKey.currentState?._trySubmit(),
child: FilledButton(
onPressed: () => _pinInputKey.currentState?._trySubmit(),
child: Text(t.common.submit),
),
),
], ],
), ),
); );
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:plezy/widgets/app_icon.dart'; import 'package:plezy/widgets/app_icon.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import '../../focus/focusable_button.dart';
import '../../i18n/strings.g.dart'; import '../../i18n/strings.g.dart';
import '../../models/external_player_models.dart'; import '../../models/external_player_models.dart';
import '../../services/settings_service.dart'; import '../../services/settings_service.dart';
@@ -183,6 +184,8 @@ class _ExternalPlayerScreenState extends State<ExternalPlayerScreen> {
Future<void> _showAddCustomPlayerDialog() async { Future<void> _showAddCustomPlayerDialog() async {
final nameController = TextEditingController(); final nameController = TextEditingController();
final valueController = TextEditingController(); final valueController = TextEditingController();
final valueFocusNode = FocusNode();
final saveFocusNode = FocusNode();
var selectedType = CustomPlayerType.command; var selectedType = CustomPlayerType.command;
final result = await showDialog<bool>( final result = await showDialog<bool>(
@@ -215,6 +218,7 @@ class _ExternalPlayerScreenState extends State<ExternalPlayerScreen> {
decoration: InputDecoration(labelText: t.externalPlayer.playerName, hintText: 'My Player'), decoration: InputDecoration(labelText: t.externalPlayer.playerName, hintText: 'My Player'),
autofocus: true, autofocus: true,
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
onSubmitted: (_) => primaryFocus?.nextFocus(),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
SizedBox( SizedBox(
@@ -238,21 +242,34 @@ class _ExternalPlayerScreenState extends State<ExternalPlayerScreen> {
const SizedBox(height: 16), const SizedBox(height: 16),
TextField( TextField(
controller: valueController, controller: valueController,
focusNode: valueFocusNode,
decoration: InputDecoration(labelText: fieldLabel, hintText: fieldHint), decoration: InputDecoration(labelText: fieldLabel, hintText: fieldHint),
textInputAction: TextInputAction.done, textInputAction: TextInputAction.done,
onSubmitted: (_) => saveFocusNode.requestFocus(),
), ),
], ],
), ),
), ),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text(t.common.cancel)), FocusableButton(
FilledButton( onPressed: () => Navigator.pop(context),
child: TextButton(onPressed: () => Navigator.pop(context), child: Text(t.common.cancel)),
),
FocusableButton(
focusNode: saveFocusNode,
onPressed: () { onPressed: () {
if (nameController.text.isNotEmpty && valueController.text.isNotEmpty) { if (nameController.text.isNotEmpty && valueController.text.isNotEmpty) {
Navigator.pop(context, true); Navigator.pop(context, true);
} }
}, },
child: Text(t.common.save), child: FilledButton(
onPressed: () {
if (nameController.text.isNotEmpty && valueController.text.isNotEmpty) {
Navigator.pop(context, true);
}
},
child: Text(t.common.save),
),
), ),
], ],
); );
@@ -260,6 +277,9 @@ class _ExternalPlayerScreenState extends State<ExternalPlayerScreen> {
), ),
); );
valueFocusNode.dispose();
saveFocusNode.dispose();
if (result != true) return; if (result != true) return;
final id = 'custom_${DateTime.now().millisecondsSinceEpoch}'; final id = 'custom_${DateTime.now().millisecondsSinceEpoch}';
+11 -1
View File
@@ -6,6 +6,7 @@ import 'package:plezy/widgets/app_icon.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import '../../focus/focusable_button.dart';
import '../../i18n/strings.g.dart'; import '../../i18n/strings.g.dart';
import '../../utils/app_logger.dart'; import '../../utils/app_logger.dart';
import '../../utils/snackbar_helper.dart'; import '../../utils/snackbar_helper.dart';
@@ -120,7 +121,16 @@ class _LogsScreenState extends State<LogsScreen> {
), ),
], ],
), ),
actions: [TextButton(onPressed: () => Navigator.of(ctx).pop(), child: Text(t.common.close))], actions: [
FocusableButton(
autofocus: true,
onPressed: () => Navigator.of(ctx).pop(),
child: TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: Text(t.common.close),
),
),
],
), ),
); );
} catch (_) { } catch (_) {
+33 -6
View File
@@ -3,6 +3,7 @@ import 'package:plezy/widgets/app_icon.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import '../../i18n/strings.g.dart'; import '../../i18n/strings.g.dart';
import '../../models/mpv_config_models.dart'; import '../../models/mpv_config_models.dart';
import '../../focus/focusable_button.dart';
import '../../utils/dialogs.dart'; import '../../utils/dialogs.dart';
import '../../utils/snackbar_helper.dart'; import '../../utils/snackbar_helper.dart';
import '../../services/settings_service.dart'; import '../../services/settings_service.dart';
@@ -111,15 +112,25 @@ class _MpvConfigScreenState extends State<MpvConfigScreen> {
], ],
), ),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: Text(t.common.cancel)), FocusableButton(
TextButton( onPressed: () => Navigator.pop(context, false),
child: TextButton(onPressed: () => Navigator.pop(context, false), child: Text(t.common.cancel)),
),
FocusableButton(
focusNode: saveFocusNode, focusNode: saveFocusNode,
onPressed: () { onPressed: () {
if (keyController.text.isNotEmpty && valueController.text.isNotEmpty) { if (keyController.text.isNotEmpty && valueController.text.isNotEmpty) {
Navigator.pop(context, true); Navigator.pop(context, true);
} }
}, },
child: Text(t.common.save), child: TextButton(
onPressed: () {
if (keyController.text.isNotEmpty && valueController.text.isNotEmpty) {
Navigator.pop(context, true);
}
},
child: Text(t.common.save),
),
), ),
], ],
), ),
@@ -164,6 +175,7 @@ class _MpvConfigScreenState extends State<MpvConfigScreen> {
if (_entries.isEmpty) return; if (_entries.isEmpty) return;
final nameController = TextEditingController(); final nameController = TextEditingController();
final saveFocusNode = FocusNode();
final result = await showDialog<bool>( final result = await showDialog<bool>(
context: context, context: context,
@@ -173,21 +185,36 @@ class _MpvConfigScreenState extends State<MpvConfigScreen> {
controller: nameController, controller: nameController,
decoration: InputDecoration(labelText: t.mpvConfig.presetName, hintText: t.mpvConfig.presetNameHint), decoration: InputDecoration(labelText: t.mpvConfig.presetName, hintText: t.mpvConfig.presetNameHint),
autofocus: true, autofocus: true,
textInputAction: TextInputAction.done,
onSubmitted: (_) => saveFocusNode.requestFocus(),
), ),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: Text(t.common.cancel)), FocusableButton(
TextButton( onPressed: () => Navigator.pop(context, false),
child: TextButton(onPressed: () => Navigator.pop(context, false), child: Text(t.common.cancel)),
),
FocusableButton(
focusNode: saveFocusNode,
onPressed: () { onPressed: () {
if (nameController.text.isNotEmpty) { if (nameController.text.isNotEmpty) {
Navigator.pop(context, true); Navigator.pop(context, true);
} }
}, },
child: Text(t.common.save), child: FilledButton(
onPressed: () {
if (nameController.text.isNotEmpty) {
Navigator.pop(context, true);
}
},
child: Text(t.common.save),
),
), ),
], ],
), ),
); );
saveFocusNode.dispose();
if (result == true) { if (result == true) {
await _settingsService.saveMpvPreset(nameController.text.trim(), _entries); await _settingsService.saveMpvPreset(nameController.text.trim(), _entries);
if (!mounted) return; if (!mounted) return;
+16 -4
View File
@@ -9,6 +9,7 @@ import '../../models/hotkey_model.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../../focus/focus_memory_tracker.dart'; import '../../focus/focus_memory_tracker.dart';
import '../../focus/input_mode_tracker.dart'; import '../../focus/input_mode_tracker.dart';
import '../../i18n/strings.g.dart'; import '../../i18n/strings.g.dart';
@@ -1408,7 +1409,11 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
title: Text(t.settings.clearCache), title: Text(t.settings.clearCache),
content: Text(t.settings.clearCacheDescription), content: Text(t.settings.clearCacheDescription),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text(t.common.cancel)), TextButton(
autofocus: true,
onPressed: () => Navigator.pop(context),
child: Text(t.common.cancel),
),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
final navigator = Navigator.of(context); final navigator = Navigator.of(context);
@@ -1434,7 +1439,11 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
title: Text(t.settings.resetSettings), title: Text(t.settings.resetSettings),
content: Text(t.settings.resetSettingsDescription), content: Text(t.settings.resetSettingsDescription),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text(t.common.cancel)), TextButton(
autofocus: true,
onPressed: () => Navigator.pop(context),
child: Text(t.common.cancel),
),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
final navigator = Navigator.of(context); final navigator = Navigator.of(context);
@@ -1443,7 +1452,6 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
if (mounted) { if (mounted) {
navigator.pop(); navigator.pop();
showSuccessSnackBar(this.context, t.settings.resetSettingsSuccess); showSuccessSnackBar(this.context, t.settings.resetSettingsSuccess);
// Reload settings
_loadSettings(); _loadSettings();
} }
}, },
@@ -1581,7 +1589,11 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
], ],
), ),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text(t.common.close)), TextButton(
autofocus: true,
onPressed: () => Navigator.pop(context),
child: Text(t.common.close),
),
FilledButton( FilledButton(
onPressed: () async { onPressed: () async {
final url = Uri.parse(_updateInfo!['releaseUrl']); final url = Uri.parse(_updateInfo!['releaseUrl']);
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:plezy/widgets/app_icon.dart'; import 'package:plezy/widgets/app_icon.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:flex_color_picker/flex_color_picker.dart'; import 'package:flex_color_picker/flex_color_picker.dart';
import '../../focus/focusable_button.dart';
import '../../i18n/strings.g.dart'; import '../../i18n/strings.g.dart';
import '../../services/settings_service.dart'; import '../../services/settings_service.dart';
import '../../utils/platform_detector.dart'; import '../../utils/platform_detector.dart';
@@ -198,14 +199,23 @@ class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
], ],
), ),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(dialogContext), child: Text(t.common.cancel)), FocusableButton(
TextButton( onPressed: () => Navigator.pop(dialogContext),
child: TextButton(onPressed: () => Navigator.pop(dialogContext), child: Text(t.common.cancel)),
),
FocusableButton(
focusNode: saveFocusNode, focusNode: saveFocusNode,
onPressed: () { onPressed: () {
onSave(spinnerValue); onSave(spinnerValue);
Navigator.pop(dialogContext); Navigator.pop(dialogContext);
}, },
child: Text(t.common.save), child: TextButton(
onPressed: () {
onSave(spinnerValue);
Navigator.pop(dialogContext);
},
child: Text(t.common.save),
),
), ),
], ],
); );
@@ -263,14 +273,23 @@ class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
onConfirm: () => saveFocusNode.requestFocus(), onConfirm: () => saveFocusNode.requestFocus(),
), ),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(dialogContext), child: Text(t.common.cancel)), FocusableButton(
TextButton( onPressed: () => Navigator.pop(dialogContext),
child: TextButton(onPressed: () => Navigator.pop(dialogContext), child: Text(t.common.cancel)),
),
FocusableButton(
focusNode: saveFocusNode, focusNode: saveFocusNode,
onPressed: () { onPressed: () {
onColorSelected(_colorToHex(pickerColor)); onColorSelected(_colorToHex(pickerColor));
Navigator.pop(dialogContext); Navigator.pop(dialogContext);
}, },
child: Text(t.common.save), child: TextButton(
onPressed: () {
onColorSelected(_colorToHex(pickerColor));
Navigator.pop(dialogContext);
},
child: Text(t.common.save),
),
), ),
], ],
); );
+11 -39
View File
@@ -57,7 +57,7 @@ import '../utils/plex_url_helper.dart';
import '../utils/video_player_navigation.dart'; import '../utils/video_player_navigation.dart';
import '../widgets/overlay_sheet.dart'; import '../widgets/overlay_sheet.dart';
import '../widgets/video_controls/video_controls.dart'; import '../widgets/video_controls/video_controls.dart';
import '../focus/focusable_wrapper.dart'; import '../focus/focusable_button.dart';
import '../focus/input_mode_tracker.dart'; import '../focus/input_mode_tracker.dart';
import '../focus/dpad_navigator.dart'; import '../focus/dpad_navigator.dart';
import '../focus/key_event_utils.dart'; import '../focus/key_event_utils.dart';
@@ -2500,27 +2500,13 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
Row( Row(
children: [ children: [
Expanded( Expanded(
child: FocusableWrapper( child: FocusableButton(
focusNode: _playNextCancelFocusNode, focusNode: _playNextCancelFocusNode,
onSelect: _cancelAutoPlay, onPressed: _cancelAutoPlay,
useBackgroundFocus: true,
autoScroll: false, autoScroll: false,
borderRadius: 20, onNavigateRight: () => _playNextConfirmFocusNode.requestFocus(),
onKeyEvent: (node, event) { onNavigateUp: () {}, // Trap focus
if (event is KeyDownEvent) { onNavigateDown: () {}, // Trap focus
// RIGHT arrow moves focus to Play Next button
if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
_playNextConfirmFocusNode.requestFocus();
return KeyEventResult.handled;
}
// Trap focus - consume UP/DOWN to prevent escape
if (event.logicalKey == LogicalKeyboardKey.arrowUp ||
event.logicalKey == LogicalKeyboardKey.arrowDown) {
return KeyEventResult.handled;
}
}
return KeyEventResult.ignored;
},
child: OutlinedButton( child: OutlinedButton(
onPressed: _cancelAutoPlay, onPressed: _cancelAutoPlay,
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
@@ -2534,27 +2520,13 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: FocusableWrapper( child: FocusableButton(
focusNode: _playNextConfirmFocusNode, focusNode: _playNextConfirmFocusNode,
onSelect: _playNext, onPressed: _playNext,
useBackgroundFocus: true,
autoScroll: false, autoScroll: false,
borderRadius: 20, onNavigateLeft: () => _playNextCancelFocusNode.requestFocus(),
onKeyEvent: (node, event) { onNavigateUp: () {}, // Trap focus
if (event is KeyDownEvent) { onNavigateDown: () {}, // Trap focus
// LEFT arrow moves focus to Cancel button
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
_playNextCancelFocusNode.requestFocus();
return KeyEventResult.handled;
}
// Trap focus - consume UP/DOWN to prevent escape
if (event.logicalKey == LogicalKeyboardKey.arrowUp ||
event.logicalKey == LogicalKeyboardKey.arrowDown) {
return KeyEventResult.handled;
}
}
return KeyEventResult.ignored;
},
child: FilledButton( child: FilledButton(
onPressed: _playNext, onPressed: _playNext,
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
+39 -16
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../focus/focusable_button.dart';
import '../i18n/strings.g.dart'; import '../i18n/strings.g.dart';
/// Utility functions for showing common dialogs /// Utility functions for showing common dialogs
@@ -24,18 +25,24 @@ Future<bool> showConfirmDialog(
title: Text(title), title: Text(title),
content: Text(message), content: Text(message),
actions: [ actions: [
TextButton( FocusableButton(
autofocus: true, autofocus: true,
onPressed: () => Navigator.pop(dialogContext, false), onPressed: () => Navigator.pop(dialogContext, false),
style: TextButton.styleFrom(padding: _buttonPadding, shape: _buttonShape), child: TextButton(
child: Text(cancelText ?? t.common.cancel), onPressed: () => Navigator.pop(dialogContext, false),
style: TextButton.styleFrom(padding: _buttonPadding, shape: _buttonShape),
child: Text(cancelText ?? t.common.cancel),
),
), ),
FilledButton( FocusableButton(
onPressed: () => Navigator.pop(dialogContext, true), onPressed: () => Navigator.pop(dialogContext, true),
style: isDestructive child: FilledButton(
? FilledButton.styleFrom(backgroundColor: colorScheme.error, foregroundColor: colorScheme.onError) onPressed: () => Navigator.pop(dialogContext, true),
: null, style: isDestructive
child: Text(confirmText), ? FilledButton.styleFrom(backgroundColor: colorScheme.error, foregroundColor: colorScheme.onError)
: null,
child: Text(confirmText),
),
), ),
], ],
); );
@@ -80,15 +87,21 @@ Future<({bool confirmed, bool checked})> showConfirmDialogWithCheckbox(
], ],
), ),
actions: [ actions: [
TextButton( FocusableButton(
autofocus: true, autofocus: true,
onPressed: () => Navigator.pop(dialogContext, false), onPressed: () => Navigator.pop(dialogContext, false),
style: TextButton.styleFrom(padding: _buttonPadding, shape: _buttonShape), child: TextButton(
child: Text(cancelText ?? t.common.cancel), onPressed: () => Navigator.pop(dialogContext, false),
style: TextButton.styleFrom(padding: _buttonPadding, shape: _buttonShape),
child: Text(cancelText ?? t.common.cancel),
),
), ),
FilledButton( FocusableButton(
onPressed: () => Navigator.pop(dialogContext, true), onPressed: () => Navigator.pop(dialogContext, true),
child: Text(confirmText), child: FilledButton(
onPressed: () => Navigator.pop(dialogContext, true),
child: Text(confirmText),
),
), ),
], ],
); );
@@ -136,6 +149,7 @@ class _TextInputDialog extends StatefulWidget {
class _TextInputDialogState extends State<_TextInputDialog> { class _TextInputDialogState extends State<_TextInputDialog> {
late final TextEditingController _controller; late final TextEditingController _controller;
final _saveFocusNode = FocusNode();
@override @override
void initState() { void initState() {
@@ -146,6 +160,7 @@ class _TextInputDialogState extends State<_TextInputDialog> {
@override @override
void dispose() { void dispose() {
_controller.dispose(); _controller.dispose();
_saveFocusNode.dispose();
super.dispose(); super.dispose();
} }
@@ -163,11 +178,19 @@ class _TextInputDialogState extends State<_TextInputDialog> {
controller: _controller, controller: _controller,
autofocus: true, autofocus: true,
decoration: InputDecoration(labelText: widget.labelText, hintText: widget.hintText), decoration: InputDecoration(labelText: widget.labelText, hintText: widget.hintText),
onSubmitted: (_) => _submit(), textInputAction: TextInputAction.done,
onSubmitted: (_) => _saveFocusNode.requestFocus(),
), ),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text(t.common.cancel)), FocusableButton(
TextButton(onPressed: _submit, child: Text(t.common.save)), onPressed: () => Navigator.pop(context),
child: TextButton(onPressed: () => Navigator.pop(context), child: Text(t.common.cancel)),
),
FocusableButton(
focusNode: _saveFocusNode,
onPressed: _submit,
child: TextButton(onPressed: _submit, child: Text(t.common.save)),
),
], ],
); );
} }
@@ -6,6 +6,7 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../i18n/strings.g.dart'; import '../../i18n/strings.g.dart';
import '../../focus/focusable_button.dart';
import '../../focus/focusable_wrapper.dart'; import '../../focus/focusable_wrapper.dart';
import '../../utils/app_logger.dart'; import '../../utils/app_logger.dart';
import '../../utils/dialogs.dart'; import '../../utils/dialogs.dart';
@@ -145,12 +146,9 @@ class _NotInSessionViewState extends State<_NotInSessionView> {
const SizedBox(height: 48), const SizedBox(height: 48),
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: FocusableWrapper( child: FocusableButton(
autofocus: true, autofocus: true,
useBackgroundFocus: true, onPressed: _isCreating || _isJoining ? null : _createSession,
disableScale: true,
borderRadius: 100,
onSelect: _isCreating || _isJoining ? null : _createSession,
child: FilledButton.icon( child: FilledButton.icon(
onPressed: _isCreating || _isJoining ? null : _createSession, onPressed: _isCreating || _isJoining ? null : _createSession,
icon: _isCreating icon: _isCreating
@@ -163,11 +161,8 @@ class _NotInSessionViewState extends State<_NotInSessionView> {
const SizedBox(height: 16), const SizedBox(height: 16),
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: FocusableWrapper( child: FocusableButton(
useBackgroundFocus: true, onPressed: _isCreating || _isJoining ? null : _joinSession,
disableScale: true,
borderRadius: 100,
onSelect: _isCreating || _isJoining ? null : _joinSession,
child: OutlinedButton.icon( child: OutlinedButton.icon(
onPressed: _isCreating || _isJoining ? null : _joinSession, onPressed: _isCreating || _isJoining ? null : _joinSession,
icon: _isJoining icon: _isJoining
@@ -213,34 +208,25 @@ class _NotInSessionViewState extends State<_NotInSessionView> {
title: Text(t.watchTogether.controlMode), title: Text(t.watchTogether.controlMode),
content: Text(t.watchTogether.controlModeQuestion), content: Text(t.watchTogether.controlModeQuestion),
actions: [ actions: [
FocusableWrapper( FocusableButton(
autofocus: true, autofocus: true,
useBackgroundFocus: true, onPressed: () => Navigator.pop(context),
disableScale: true,
borderRadius: 100,
onSelect: () => Navigator.pop(context),
child: TextButton( child: TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
style: TextButton.styleFrom(padding: buttonPadding, shape: buttonShape), style: TextButton.styleFrom(padding: buttonPadding, shape: buttonShape),
child: Text(t.common.cancel), child: Text(t.common.cancel),
), ),
), ),
FocusableWrapper( FocusableButton(
useBackgroundFocus: true, onPressed: () => Navigator.pop(context, ControlMode.hostOnly),
disableScale: true,
borderRadius: 100,
onSelect: () => Navigator.pop(context, ControlMode.hostOnly),
child: TextButton( child: TextButton(
onPressed: () => Navigator.pop(context, ControlMode.hostOnly), onPressed: () => Navigator.pop(context, ControlMode.hostOnly),
style: TextButton.styleFrom(padding: buttonPadding, shape: buttonShape), style: TextButton.styleFrom(padding: buttonPadding, shape: buttonShape),
child: Text(t.watchTogether.hostOnly), child: Text(t.watchTogether.hostOnly),
), ),
), ),
FocusableWrapper( FocusableButton(
useBackgroundFocus: true, onPressed: () => Navigator.pop(context, ControlMode.anyone),
disableScale: true,
borderRadius: 100,
onSelect: () => Navigator.pop(context, ControlMode.anyone),
child: FilledButton( child: FilledButton(
onPressed: () => Navigator.pop(context, ControlMode.anyone), onPressed: () => Navigator.pop(context, ControlMode.anyone),
child: Text(t.watchTogether.anyone), child: Text(t.watchTogether.anyone),
@@ -408,12 +394,9 @@ class _ActiveSessionContent extends StatelessWidget {
// Leave/End Session Button // Leave/End Session Button
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: FocusableWrapper( child: FocusableButton(
autofocus: true, autofocus: true,
useBackgroundFocus: true, onPressed: () => _leaveSession(context),
disableScale: true,
borderRadius: 100,
onSelect: () => _leaveSession(context),
child: OutlinedButton.icon( child: OutlinedButton.icon(
onPressed: () => _leaveSession(context), onPressed: () => _leaveSession(context),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import '../../focus/focusable_button.dart';
import '../../focus/focusable_wrapper.dart'; import '../../focus/focusable_wrapper.dart';
import '../../i18n/strings.g.dart'; import '../../i18n/strings.g.dart';
@@ -100,11 +101,8 @@ class _JoinSessionDialogState extends State<JoinSessionDialog> {
const SizedBox(height: 24), const SizedBox(height: 24),
// Join button // Join button
FocusableWrapper( FocusableButton(
useBackgroundFocus: true, onPressed: _join,
disableScale: true,
borderRadius: 100,
onSelect: _join,
child: FilledButton.icon( child: FilledButton.icon(
onPressed: _join, onPressed: _join,
icon: const Icon(Symbols.group_add), icon: const Icon(Symbols.group_add),
+21 -2
View File
@@ -17,6 +17,7 @@ import '../utils/snackbar_helper.dart';
import '../utils/dialogs.dart'; import '../utils/dialogs.dart';
import '../utils/focus_utils.dart'; import '../utils/focus_utils.dart';
import '../services/external_player_service.dart'; import '../services/external_player_service.dart';
import '../focus/focusable_button.dart';
import '../focus/dpad_navigator.dart'; import '../focus/dpad_navigator.dart';
import '../screens/media_detail_screen.dart'; import '../screens/media_detail_screen.dart';
import '../screens/season_detail_screen.dart'; import '../screens/season_detail_screen.dart';
@@ -1252,7 +1253,16 @@ class _PlaylistSelectionDialog extends StatelessWidget {
}, },
), ),
), ),
actions: [TextButton(onPressed: () => Navigator.pop(context), child: Text(t.common.cancel))], actions: [
FocusableButton(
autofocus: true,
onPressed: () => Navigator.pop(context),
child: TextButton(
onPressed: () => Navigator.pop(context),
child: Text(t.common.cancel),
),
),
],
); );
} }
} }
@@ -1292,7 +1302,16 @@ class _CollectionSelectionDialog extends StatelessWidget {
}, },
), ),
), ),
actions: [TextButton(onPressed: () => Navigator.pop(context), child: Text(t.common.cancel))], actions: [
FocusableButton(
autofocus: true,
onPressed: () => Navigator.pop(context),
child: TextButton(
onPressed: () => Navigator.pop(context),
child: Text(t.common.cancel),
),
),
],
); );
} }
} }