fix: dpad focus for dialogs and buttons
This commit is contained in:
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: () {
|
||||||
|
|||||||
@@ -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),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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}';
|
||||||
|
|||||||
@@ -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 (_) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
@@ -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),
|
||||||
|
|||||||
@@ -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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user