fix: dpad focus for dialogs and buttons
This commit is contained in:
@@ -29,8 +29,9 @@ class FocusTheme {
|
||||
BuildContext context, {
|
||||
required bool isFocused,
|
||||
double borderRadius = defaultBorderRadius,
|
||||
Color? color,
|
||||
}) {
|
||||
final focusColor = getFocusBorderColor(context);
|
||||
final focusColor = color ?? getFocusBorderColor(context);
|
||||
|
||||
return BoxDecoration(
|
||||
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.
|
||||
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.
|
||||
final VoidCallback? onNavigateLeft;
|
||||
|
||||
@@ -83,10 +86,19 @@ class FocusableWrapper extends StatefulWidget {
|
||||
/// Useful for video controls where outline doesn't look good.
|
||||
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.
|
||||
/// Useful for elements like sliders where scaling looks odd.
|
||||
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({
|
||||
super.key,
|
||||
required this.child,
|
||||
@@ -94,6 +106,7 @@ class FocusableWrapper extends StatefulWidget {
|
||||
this.onLongPress,
|
||||
this.onFocusChange,
|
||||
this.onNavigateUp,
|
||||
this.onNavigateDown,
|
||||
this.onNavigateLeft,
|
||||
this.onNavigateRight,
|
||||
this.onBack,
|
||||
@@ -109,7 +122,9 @@ class FocusableWrapper extends StatefulWidget {
|
||||
this.enableLongPress = false,
|
||||
this.longPressDuration = const Duration(milliseconds: 500),
|
||||
this.useBackgroundFocus = false,
|
||||
this.focusColor,
|
||||
this.disableScale = false,
|
||||
this.descendantsAreFocusable = true,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -367,6 +382,12 @@ class _FocusableWrapperState extends State<FocusableWrapper> with SingleTickerPr
|
||||
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
|
||||
// for only providing this callback when the item is at the left edge)
|
||||
if (key == LogicalKeyboardKey.arrowLeft && widget.onNavigateLeft != null) {
|
||||
@@ -398,11 +419,12 @@ class _FocusableWrapperState extends State<FocusableWrapper> with SingleTickerPr
|
||||
// Choose decoration based on useBackgroundFocus
|
||||
final decoration = widget.useBackgroundFocus
|
||||
? 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(
|
||||
focusNode: _focusNode,
|
||||
autofocus: widget.autofocus,
|
||||
descendantsAreFocusable: widget.descendantsAreFocusable,
|
||||
onFocusChange: _handleFocusChange,
|
||||
onKeyEvent: _handleKeyEvent,
|
||||
child: AnimatedBuilder(
|
||||
|
||||
@@ -16,6 +16,7 @@ import '../i18n/strings.g.dart';
|
||||
import '../theme/mono_tokens.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../utils/platform_detector.dart';
|
||||
import '../focus/focusable_button.dart';
|
||||
import 'main_screen.dart';
|
||||
|
||||
class AuthScreen extends StatefulWidget {
|
||||
@@ -398,7 +399,7 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
children: [
|
||||
if (isTV) ...[
|
||||
// On TV: QR is primary, browser is secondary
|
||||
ElevatedButton(
|
||||
FocusableButton(
|
||||
autofocus: true,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
@@ -406,14 +407,25 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
});
|
||||
_startAuthentication();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
|
||||
child: Text(t.auth.showQRCode),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_useQrFlow = true;
|
||||
});
|
||||
_startAuthentication();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
|
||||
child: Text(t.auth.showQRCode),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
OutlinedButton(
|
||||
FocusableButton(
|
||||
onPressed: _startAuthentication,
|
||||
style: OutlinedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
|
||||
child: Text(t.auth.useBrowser),
|
||||
child: OutlinedButton(
|
||||
onPressed: _startAuthentication,
|
||||
style: OutlinedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
|
||||
child: Text(t.auth.useBrowser),
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
// On other platforms: Browser is primary, QR is secondary
|
||||
@@ -500,22 +512,33 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
OutlinedButton(
|
||||
FocusableButton(
|
||||
autofocus: true,
|
||||
onPressed: _retryAuthentication,
|
||||
style: OutlinedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24)),
|
||||
child: Text(t.common.retry),
|
||||
child: OutlinedButton(
|
||||
onPressed: _retryAuthentication,
|
||||
style: OutlinedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24)),
|
||||
child: Text(t.common.retry),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
OutlinedButton(
|
||||
FocusableButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_useQrFlow = false;
|
||||
});
|
||||
_startAuthentication();
|
||||
},
|
||||
style: OutlinedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24)),
|
||||
child: Text(t.auth.useBrowser),
|
||||
child: OutlinedButton(
|
||||
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:provider/provider.dart';
|
||||
|
||||
import '../../focus/focusable_button.dart';
|
||||
import '../../i18n/strings.g.dart';
|
||||
import '../../providers/companion_remote_provider.dart';
|
||||
import '../../utils/formatters.dart';
|
||||
@@ -441,8 +442,18 @@ class _PairingScreenState extends State<PairingScreen> {
|
||||
title: Text(t.companionRemote.pairing.removeRecentConnection),
|
||||
content: Text(t.companionRemote.pairing.removeConfirm(name: session.deviceName)),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context, false), child: Text(t.common.cancel)),
|
||||
TextButton(onPressed: () => Navigator.pop(context, true), child: Text(t.common.remove)),
|
||||
FocusableButton(
|
||||
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:provider/provider.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../focus/focusable_button.dart';
|
||||
import '../../focus/dpad_navigator.dart';
|
||||
import '../../focus/focus_theme.dart';
|
||||
import '../../focus/input_mode_tracker.dart';
|
||||
@@ -1480,11 +1481,21 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet> {
|
||||
title: Text(selectedItem.confirmationTitle ?? t.dialog.confirmAction),
|
||||
content: Text(selectedItem.confirmationMessage ?? t.libraries.confirmActionMessage),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context, false), child: Text(t.common.cancel)),
|
||||
TextButton(
|
||||
FocusableButton(
|
||||
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),
|
||||
style: selectedItem.isDestructive ? TextButton.styleFrom(foregroundColor: Colors.red) : null,
|
||||
child: Text(t.common.confirm),
|
||||
child: TextButton(
|
||||
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:provider/provider.dart';
|
||||
|
||||
import '../../focus/focusable_button.dart';
|
||||
import '../../focus/focusable_wrapper.dart';
|
||||
import '../../focus/key_event_utils.dart';
|
||||
import '../../i18n/strings.g.dart';
|
||||
@@ -103,8 +104,18 @@ class _DvrRecordingsScreenState extends State<DvrRecordingsScreen> with SingleTi
|
||||
title: Text(t.liveTv.deleteSubscription),
|
||||
content: Text(t.liveTv.deleteSubscriptionConfirm),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.of(context).pop(false), child: Text(t.common.cancel)),
|
||||
FilledButton(onPressed: () => Navigator.of(context).pop(true), child: Text(t.common.delete)),
|
||||
FocusableButton(
|
||||
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> {
|
||||
late Map<String, String> _prefs;
|
||||
final Map<String, TextEditingController> _textControllers = {};
|
||||
final _saveFocusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -332,6 +344,7 @@ class _SubscriptionEditDialogState extends State<_SubscriptionEditDialog> {
|
||||
for (final controller in _textControllers.values) {
|
||||
controller.dispose();
|
||||
}
|
||||
_saveFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -351,8 +364,18 @@ class _SubscriptionEditDialogState extends State<_SubscriptionEditDialog> {
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.of(context).pop(null), child: Text(t.common.cancel)),
|
||||
FilledButton(onPressed: () => Navigator.of(context).pop(_prefs), child: Text(t.common.save)),
|
||||
FocusableButton(
|
||||
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),
|
||||
subtitle: TextField(
|
||||
controller: controller,
|
||||
textInputAction: TextInputAction.done,
|
||||
onChanged: (newValue) {
|
||||
_prefs[setting.id] = newValue;
|
||||
},
|
||||
onSubmitted: (_) => _saveFocusNode.requestFocus(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import '../../focus/focusable_wrapper.dart';
|
||||
import '../../focus/focusable_button.dart';
|
||||
import '../../i18n/strings.g.dart';
|
||||
import '../../models/livetv_channel.dart';
|
||||
import '../../models/livetv_program.dart';
|
||||
@@ -119,18 +119,15 @@ class _ProgramDetailsSheetContentState extends State<_ProgramDetailsSheetContent
|
||||
if (program.isCurrentlyAiring && widget.onTuneChannel != null) {
|
||||
final idx = buttonIndex;
|
||||
buttons.add(
|
||||
FocusableWrapper(
|
||||
FocusableButton(
|
||||
focusNode: _buttonFocusNodes[idx],
|
||||
onSelect: () {
|
||||
onPressed: () {
|
||||
closeSheet();
|
||||
widget.onTuneChannel!();
|
||||
},
|
||||
onNavigateLeft: idx > 0 ? () => _focusButton(idx - 1) : null,
|
||||
onNavigateRight: idx < _buttonFocusNodes.length - 1 ? () => _focusButton(idx + 1) : null,
|
||||
onBack: closeSheet,
|
||||
borderRadius: 100,
|
||||
useBackgroundFocus: true,
|
||||
disableScale: true,
|
||||
child: FilledButton.icon(
|
||||
style: FilledButton.styleFrom(tapTargetSize: MaterialTapTargetSize.shrinkWrap),
|
||||
onPressed: () {
|
||||
@@ -181,18 +178,15 @@ class _ProgramDetailsSheetContentState extends State<_ProgramDetailsSheetContent
|
||||
buttons.add(const SizedBox(width: 8));
|
||||
final idx = buttonIndex;
|
||||
buttons.add(
|
||||
FocusableWrapper(
|
||||
FocusableButton(
|
||||
focusNode: _buttonFocusNodes[idx],
|
||||
onSelect: () {
|
||||
onPressed: () {
|
||||
closeSheet();
|
||||
widget.onTuneChannel!();
|
||||
},
|
||||
onNavigateLeft: idx > 0 ? () => _focusButton(idx - 1) : null,
|
||||
onNavigateRight: idx < _buttonFocusNodes.length - 1 ? () => _focusButton(idx + 1) : null,
|
||||
onBack: closeSheet,
|
||||
borderRadius: 100,
|
||||
useBackgroundFocus: true,
|
||||
disableScale: true,
|
||||
child: OutlinedButton.icon(
|
||||
style: OutlinedButton.styleFrom(tapTargetSize: MaterialTapTargetSize.shrinkWrap),
|
||||
onPressed: () {
|
||||
|
||||
@@ -10,6 +10,7 @@ import '../../services/plex_client.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
import '../services/update_service.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../focus/focusable_button.dart';
|
||||
import '../utils/dialogs.dart';
|
||||
import '../utils/provider_extensions.dart';
|
||||
import '../utils/platform_detector.dart';
|
||||
@@ -226,27 +227,36 @@ class _MainScreenState extends State<MainScreen> with RouteAware, WindowListener
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
FocusableButton(
|
||||
autofocus: true,
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
|
||||
shape: const StadiumBorder(),
|
||||
child: TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
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 {
|
||||
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: TextButton(
|
||||
onPressed: () async {
|
||||
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 {
|
||||
final url = Uri.parse(updateInfo['releaseUrl']);
|
||||
if (await canLaunchUrl(url)) {
|
||||
@@ -254,7 +264,16 @@ class _MainScreenState extends State<MainScreen> with RouteAware, WindowListener
|
||||
}
|
||||
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/input_mode_tracker.dart';
|
||||
import '../../focus/key_event_utils.dart';
|
||||
import '../../focus/focusable_button.dart';
|
||||
import '../../i18n/strings.g.dart';
|
||||
import '../../utils/platform_detector.dart';
|
||||
import '../../widgets/app_icon.dart';
|
||||
@@ -100,9 +101,18 @@ class _PinEntryDialogState extends State<PinEntryDialog> with SingleTickerProvid
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: _cancel, child: Text(t.common.cancel)),
|
||||
FocusableButton(
|
||||
onPressed: _cancel,
|
||||
child: TextButton(onPressed: _cancel, child: Text(t.common.cancel)),
|
||||
),
|
||||
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:plezy/widgets/app_icon.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import '../../focus/focusable_button.dart';
|
||||
import '../../i18n/strings.g.dart';
|
||||
import '../../models/external_player_models.dart';
|
||||
import '../../services/settings_service.dart';
|
||||
@@ -183,6 +184,8 @@ class _ExternalPlayerScreenState extends State<ExternalPlayerScreen> {
|
||||
Future<void> _showAddCustomPlayerDialog() async {
|
||||
final nameController = TextEditingController();
|
||||
final valueController = TextEditingController();
|
||||
final valueFocusNode = FocusNode();
|
||||
final saveFocusNode = FocusNode();
|
||||
var selectedType = CustomPlayerType.command;
|
||||
|
||||
final result = await showDialog<bool>(
|
||||
@@ -215,6 +218,7 @@ class _ExternalPlayerScreenState extends State<ExternalPlayerScreen> {
|
||||
decoration: InputDecoration(labelText: t.externalPlayer.playerName, hintText: 'My Player'),
|
||||
autofocus: true,
|
||||
textInputAction: TextInputAction.next,
|
||||
onSubmitted: (_) => primaryFocus?.nextFocus(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
@@ -238,21 +242,34 @@ class _ExternalPlayerScreenState extends State<ExternalPlayerScreen> {
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: valueController,
|
||||
focusNode: valueFocusNode,
|
||||
decoration: InputDecoration(labelText: fieldLabel, hintText: fieldHint),
|
||||
textInputAction: TextInputAction.done,
|
||||
onSubmitted: (_) => saveFocusNode.requestFocus(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: Text(t.common.cancel)),
|
||||
FilledButton(
|
||||
FocusableButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: TextButton(onPressed: () => Navigator.pop(context), child: Text(t.common.cancel)),
|
||||
),
|
||||
FocusableButton(
|
||||
focusNode: saveFocusNode,
|
||||
onPressed: () {
|
||||
if (nameController.text.isNotEmpty && valueController.text.isNotEmpty) {
|
||||
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;
|
||||
|
||||
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:flutter/services.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import '../../focus/focusable_button.dart';
|
||||
import '../../i18n/strings.g.dart';
|
||||
import '../../utils/app_logger.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 (_) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:plezy/widgets/app_icon.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import '../../i18n/strings.g.dart';
|
||||
import '../../models/mpv_config_models.dart';
|
||||
import '../../focus/focusable_button.dart';
|
||||
import '../../utils/dialogs.dart';
|
||||
import '../../utils/snackbar_helper.dart';
|
||||
import '../../services/settings_service.dart';
|
||||
@@ -111,15 +112,25 @@ class _MpvConfigScreenState extends State<MpvConfigScreen> {
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context, false), child: Text(t.common.cancel)),
|
||||
TextButton(
|
||||
FocusableButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: TextButton(onPressed: () => Navigator.pop(context, false), child: Text(t.common.cancel)),
|
||||
),
|
||||
FocusableButton(
|
||||
focusNode: saveFocusNode,
|
||||
onPressed: () {
|
||||
if (keyController.text.isNotEmpty && valueController.text.isNotEmpty) {
|
||||
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;
|
||||
|
||||
final nameController = TextEditingController();
|
||||
final saveFocusNode = FocusNode();
|
||||
|
||||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
@@ -173,21 +185,36 @@ class _MpvConfigScreenState extends State<MpvConfigScreen> {
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(labelText: t.mpvConfig.presetName, hintText: t.mpvConfig.presetNameHint),
|
||||
autofocus: true,
|
||||
textInputAction: TextInputAction.done,
|
||||
onSubmitted: (_) => saveFocusNode.requestFocus(),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context, false), child: Text(t.common.cancel)),
|
||||
TextButton(
|
||||
FocusableButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: TextButton(onPressed: () => Navigator.pop(context, false), child: Text(t.common.cancel)),
|
||||
),
|
||||
FocusableButton(
|
||||
focusNode: saveFocusNode,
|
||||
onPressed: () {
|
||||
if (nameController.text.isNotEmpty) {
|
||||
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) {
|
||||
await _settingsService.saveMpvPreset(nameController.text.trim(), _entries);
|
||||
if (!mounted) return;
|
||||
|
||||
@@ -9,6 +9,7 @@ import '../../models/hotkey_model.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
|
||||
import '../../focus/focus_memory_tracker.dart';
|
||||
import '../../focus/input_mode_tracker.dart';
|
||||
import '../../i18n/strings.g.dart';
|
||||
@@ -1408,7 +1409,11 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
|
||||
title: Text(t.settings.clearCache),
|
||||
content: Text(t.settings.clearCacheDescription),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: Text(t.common.cancel)),
|
||||
TextButton(
|
||||
autofocus: true,
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(t.common.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final navigator = Navigator.of(context);
|
||||
@@ -1434,7 +1439,11 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
|
||||
title: Text(t.settings.resetSettings),
|
||||
content: Text(t.settings.resetSettingsDescription),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: Text(t.common.cancel)),
|
||||
TextButton(
|
||||
autofocus: true,
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(t.common.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final navigator = Navigator.of(context);
|
||||
@@ -1443,7 +1452,6 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
|
||||
if (mounted) {
|
||||
navigator.pop();
|
||||
showSuccessSnackBar(this.context, t.settings.resetSettingsSuccess);
|
||||
// Reload settings
|
||||
_loadSettings();
|
||||
}
|
||||
},
|
||||
@@ -1581,7 +1589,11 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: Text(t.common.close)),
|
||||
TextButton(
|
||||
autofocus: true,
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(t.common.close),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
final url = Uri.parse(_updateInfo!['releaseUrl']);
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:plezy/widgets/app_icon.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:flex_color_picker/flex_color_picker.dart';
|
||||
import '../../focus/focusable_button.dart';
|
||||
import '../../i18n/strings.g.dart';
|
||||
import '../../services/settings_service.dart';
|
||||
import '../../utils/platform_detector.dart';
|
||||
@@ -198,14 +199,23 @@ class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(dialogContext), child: Text(t.common.cancel)),
|
||||
TextButton(
|
||||
FocusableButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: TextButton(onPressed: () => Navigator.pop(dialogContext), child: Text(t.common.cancel)),
|
||||
),
|
||||
FocusableButton(
|
||||
focusNode: saveFocusNode,
|
||||
onPressed: () {
|
||||
onSave(spinnerValue);
|
||||
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(),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(dialogContext), child: Text(t.common.cancel)),
|
||||
TextButton(
|
||||
FocusableButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: TextButton(onPressed: () => Navigator.pop(dialogContext), child: Text(t.common.cancel)),
|
||||
),
|
||||
FocusableButton(
|
||||
focusNode: saveFocusNode,
|
||||
onPressed: () {
|
||||
onColorSelected(_colorToHex(pickerColor));
|
||||
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 '../widgets/overlay_sheet.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/dpad_navigator.dart';
|
||||
import '../focus/key_event_utils.dart';
|
||||
@@ -2500,27 +2500,13 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FocusableWrapper(
|
||||
child: FocusableButton(
|
||||
focusNode: _playNextCancelFocusNode,
|
||||
onSelect: _cancelAutoPlay,
|
||||
useBackgroundFocus: true,
|
||||
onPressed: _cancelAutoPlay,
|
||||
autoScroll: false,
|
||||
borderRadius: 20,
|
||||
onKeyEvent: (node, event) {
|
||||
if (event is KeyDownEvent) {
|
||||
// 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;
|
||||
},
|
||||
onNavigateRight: () => _playNextConfirmFocusNode.requestFocus(),
|
||||
onNavigateUp: () {}, // Trap focus
|
||||
onNavigateDown: () {}, // Trap focus
|
||||
child: OutlinedButton(
|
||||
onPressed: _cancelAutoPlay,
|
||||
style: OutlinedButton.styleFrom(
|
||||
@@ -2534,27 +2520,13 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: FocusableWrapper(
|
||||
child: FocusableButton(
|
||||
focusNode: _playNextConfirmFocusNode,
|
||||
onSelect: _playNext,
|
||||
useBackgroundFocus: true,
|
||||
onPressed: _playNext,
|
||||
autoScroll: false,
|
||||
borderRadius: 20,
|
||||
onKeyEvent: (node, event) {
|
||||
if (event is KeyDownEvent) {
|
||||
// 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;
|
||||
},
|
||||
onNavigateLeft: () => _playNextCancelFocusNode.requestFocus(),
|
||||
onNavigateUp: () {}, // Trap focus
|
||||
onNavigateDown: () {}, // Trap focus
|
||||
child: FilledButton(
|
||||
onPressed: _playNext,
|
||||
style: FilledButton.styleFrom(
|
||||
|
||||
+39
-16
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../focus/focusable_button.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
|
||||
/// Utility functions for showing common dialogs
|
||||
@@ -24,18 +25,24 @@ Future<bool> showConfirmDialog(
|
||||
title: Text(title),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
TextButton(
|
||||
FocusableButton(
|
||||
autofocus: true,
|
||||
onPressed: () => Navigator.pop(dialogContext, false),
|
||||
style: TextButton.styleFrom(padding: _buttonPadding, shape: _buttonShape),
|
||||
child: Text(cancelText ?? t.common.cancel),
|
||||
child: TextButton(
|
||||
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),
|
||||
style: isDestructive
|
||||
? FilledButton.styleFrom(backgroundColor: colorScheme.error, foregroundColor: colorScheme.onError)
|
||||
: null,
|
||||
child: Text(confirmText),
|
||||
child: FilledButton(
|
||||
onPressed: () => Navigator.pop(dialogContext, true),
|
||||
style: isDestructive
|
||||
? FilledButton.styleFrom(backgroundColor: colorScheme.error, foregroundColor: colorScheme.onError)
|
||||
: null,
|
||||
child: Text(confirmText),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -80,15 +87,21 @@ Future<({bool confirmed, bool checked})> showConfirmDialogWithCheckbox(
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
FocusableButton(
|
||||
autofocus: true,
|
||||
onPressed: () => Navigator.pop(dialogContext, false),
|
||||
style: TextButton.styleFrom(padding: _buttonPadding, shape: _buttonShape),
|
||||
child: Text(cancelText ?? t.common.cancel),
|
||||
child: TextButton(
|
||||
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),
|
||||
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> {
|
||||
late final TextEditingController _controller;
|
||||
final _saveFocusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -146,6 +160,7 @@ class _TextInputDialogState extends State<_TextInputDialog> {
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_saveFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -163,11 +178,19 @@ class _TextInputDialogState extends State<_TextInputDialog> {
|
||||
controller: _controller,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(labelText: widget.labelText, hintText: widget.hintText),
|
||||
onSubmitted: (_) => _submit(),
|
||||
textInputAction: TextInputAction.done,
|
||||
onSubmitted: (_) => _saveFocusNode.requestFocus(),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: Text(t.common.cancel)),
|
||||
TextButton(onPressed: _submit, child: Text(t.common.save)),
|
||||
FocusableButton(
|
||||
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 '../../i18n/strings.g.dart';
|
||||
import '../../focus/focusable_button.dart';
|
||||
import '../../focus/focusable_wrapper.dart';
|
||||
import '../../utils/app_logger.dart';
|
||||
import '../../utils/dialogs.dart';
|
||||
@@ -145,12 +146,9 @@ class _NotInSessionViewState extends State<_NotInSessionView> {
|
||||
const SizedBox(height: 48),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FocusableWrapper(
|
||||
child: FocusableButton(
|
||||
autofocus: true,
|
||||
useBackgroundFocus: true,
|
||||
disableScale: true,
|
||||
borderRadius: 100,
|
||||
onSelect: _isCreating || _isJoining ? null : _createSession,
|
||||
onPressed: _isCreating || _isJoining ? null : _createSession,
|
||||
child: FilledButton.icon(
|
||||
onPressed: _isCreating || _isJoining ? null : _createSession,
|
||||
icon: _isCreating
|
||||
@@ -163,11 +161,8 @@ class _NotInSessionViewState extends State<_NotInSessionView> {
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FocusableWrapper(
|
||||
useBackgroundFocus: true,
|
||||
disableScale: true,
|
||||
borderRadius: 100,
|
||||
onSelect: _isCreating || _isJoining ? null : _joinSession,
|
||||
child: FocusableButton(
|
||||
onPressed: _isCreating || _isJoining ? null : _joinSession,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: _isCreating || _isJoining ? null : _joinSession,
|
||||
icon: _isJoining
|
||||
@@ -213,34 +208,25 @@ class _NotInSessionViewState extends State<_NotInSessionView> {
|
||||
title: Text(t.watchTogether.controlMode),
|
||||
content: Text(t.watchTogether.controlModeQuestion),
|
||||
actions: [
|
||||
FocusableWrapper(
|
||||
FocusableButton(
|
||||
autofocus: true,
|
||||
useBackgroundFocus: true,
|
||||
disableScale: true,
|
||||
borderRadius: 100,
|
||||
onSelect: () => Navigator.pop(context),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: TextButton.styleFrom(padding: buttonPadding, shape: buttonShape),
|
||||
child: Text(t.common.cancel),
|
||||
),
|
||||
),
|
||||
FocusableWrapper(
|
||||
useBackgroundFocus: true,
|
||||
disableScale: true,
|
||||
borderRadius: 100,
|
||||
onSelect: () => Navigator.pop(context, ControlMode.hostOnly),
|
||||
FocusableButton(
|
||||
onPressed: () => Navigator.pop(context, ControlMode.hostOnly),
|
||||
child: TextButton(
|
||||
onPressed: () => Navigator.pop(context, ControlMode.hostOnly),
|
||||
style: TextButton.styleFrom(padding: buttonPadding, shape: buttonShape),
|
||||
child: Text(t.watchTogether.hostOnly),
|
||||
),
|
||||
),
|
||||
FocusableWrapper(
|
||||
useBackgroundFocus: true,
|
||||
disableScale: true,
|
||||
borderRadius: 100,
|
||||
onSelect: () => Navigator.pop(context, ControlMode.anyone),
|
||||
FocusableButton(
|
||||
onPressed: () => Navigator.pop(context, ControlMode.anyone),
|
||||
child: FilledButton(
|
||||
onPressed: () => Navigator.pop(context, ControlMode.anyone),
|
||||
child: Text(t.watchTogether.anyone),
|
||||
@@ -408,12 +394,9 @@ class _ActiveSessionContent extends StatelessWidget {
|
||||
// Leave/End Session Button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FocusableWrapper(
|
||||
child: FocusableButton(
|
||||
autofocus: true,
|
||||
useBackgroundFocus: true,
|
||||
disableScale: true,
|
||||
borderRadius: 100,
|
||||
onSelect: () => _leaveSession(context),
|
||||
onPressed: () => _leaveSession(context),
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => _leaveSession(context),
|
||||
style: OutlinedButton.styleFrom(
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import '../../focus/focusable_button.dart';
|
||||
import '../../focus/focusable_wrapper.dart';
|
||||
import '../../i18n/strings.g.dart';
|
||||
|
||||
@@ -100,11 +101,8 @@ class _JoinSessionDialogState extends State<JoinSessionDialog> {
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Join button
|
||||
FocusableWrapper(
|
||||
useBackgroundFocus: true,
|
||||
disableScale: true,
|
||||
borderRadius: 100,
|
||||
onSelect: _join,
|
||||
FocusableButton(
|
||||
onPressed: _join,
|
||||
child: FilledButton.icon(
|
||||
onPressed: _join,
|
||||
icon: const Icon(Symbols.group_add),
|
||||
|
||||
@@ -17,6 +17,7 @@ import '../utils/snackbar_helper.dart';
|
||||
import '../utils/dialogs.dart';
|
||||
import '../utils/focus_utils.dart';
|
||||
import '../services/external_player_service.dart';
|
||||
import '../focus/focusable_button.dart';
|
||||
import '../focus/dpad_navigator.dart';
|
||||
import '../screens/media_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