fix: auto-focus first content item on settings screens in dpad mode
This commit is contained in:
@@ -8,8 +8,7 @@ import '../../focus/focusable_button.dart';
|
||||
import '../../i18n/strings.g.dart';
|
||||
import '../../models/external_player_models.dart';
|
||||
import '../../services/settings_service.dart';
|
||||
import '../../focus/key_event_utils.dart';
|
||||
import '../../widgets/desktop_app_bar.dart';
|
||||
import '../../widgets/focused_scroll_scaffold.dart';
|
||||
|
||||
class ExternalPlayerScreen extends StatefulWidget {
|
||||
const ExternalPlayerScreen({super.key});
|
||||
@@ -47,27 +46,17 @@ class _ExternalPlayerScreenState extends State<ExternalPlayerScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isLoading) {
|
||||
return Focus(
|
||||
canRequestFocus: false,
|
||||
onKeyEvent: (_, event) => handleBackKeyNavigation(context, event),
|
||||
child: FocusScope(
|
||||
autofocus: true,
|
||||
child: const Scaffold(body: Center(child: CircularProgressIndicator())),
|
||||
),
|
||||
return FocusedScrollScaffold(
|
||||
title: Text(t.externalPlayer.title),
|
||||
slivers: [const SliverFillRemaining(child: Center(child: CircularProgressIndicator()))],
|
||||
);
|
||||
}
|
||||
|
||||
final knownPlayers = KnownPlayers.getForCurrentPlatform();
|
||||
|
||||
return Focus(
|
||||
canRequestFocus: false,
|
||||
onKeyEvent: (_, event) => handleBackKeyNavigation(context, event),
|
||||
child: FocusScope(
|
||||
autofocus: true,
|
||||
child: Scaffold(
|
||||
body: CustomScrollView(
|
||||
return FocusedScrollScaffold(
|
||||
title: Text(t.externalPlayer.title),
|
||||
slivers: [
|
||||
CustomAppBar(title: Text(t.externalPlayer.title), pinned: true),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
sliver: SliverList(
|
||||
@@ -117,9 +106,6 @@ class _ExternalPlayerScreenState extends State<ExternalPlayerScreen> {
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,7 @@ import '../../focus/focusable_button.dart';
|
||||
import '../../i18n/strings.g.dart';
|
||||
import '../../utils/app_logger.dart';
|
||||
import '../../utils/snackbar_helper.dart';
|
||||
import '../../focus/key_event_utils.dart';
|
||||
import '../../widgets/desktop_app_bar.dart';
|
||||
import '../../widgets/focused_scroll_scaffold.dart';
|
||||
|
||||
class LogsScreen extends StatefulWidget {
|
||||
const LogsScreen({super.key});
|
||||
@@ -176,17 +175,8 @@ class _LogsScreenState extends State<LogsScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Focus(
|
||||
canRequestFocus: false,
|
||||
onKeyEvent: (_, event) => handleBackKeyNavigation(context, event),
|
||||
child: FocusScope(
|
||||
autofocus: true,
|
||||
child: Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
CustomAppBar(
|
||||
return FocusedScrollScaffold(
|
||||
title: Text(t.screens.logs),
|
||||
pinned: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const AppIcon(Symbols.refresh_rounded, fill: 1),
|
||||
@@ -209,7 +199,7 @@ class _LogsScreenState extends State<LogsScreen> {
|
||||
tooltip: t.logs.clearLogs,
|
||||
),
|
||||
],
|
||||
),
|
||||
slivers: [
|
||||
if (_logs.isEmpty)
|
||||
SliverFillRemaining(child: Center(child: Text(t.messages.noLogsAvailable)))
|
||||
else
|
||||
@@ -228,9 +218,6 @@ class _LogsScreenState extends State<LogsScreen> {
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,7 @@ import '../../focus/focusable_button.dart';
|
||||
import '../../utils/dialogs.dart';
|
||||
import '../../utils/snackbar_helper.dart';
|
||||
import '../../services/settings_service.dart';
|
||||
import '../../focus/key_event_utils.dart';
|
||||
import '../../widgets/desktop_app_bar.dart';
|
||||
import '../../widgets/focused_scroll_scaffold.dart';
|
||||
|
||||
class MpvConfigScreen extends StatefulWidget {
|
||||
const MpvConfigScreen({super.key});
|
||||
@@ -263,26 +262,11 @@ class _MpvConfigScreenState extends State<MpvConfigScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isLoading) {
|
||||
return Focus(
|
||||
canRequestFocus: false,
|
||||
onKeyEvent: (_, event) => handleBackKeyNavigation(context, event),
|
||||
child: FocusScope(
|
||||
autofocus: true,
|
||||
child: const Scaffold(body: Center(child: CircularProgressIndicator())),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Focus(
|
||||
canRequestFocus: false,
|
||||
onKeyEvent: (_, event) => handleBackKeyNavigation(context, event),
|
||||
child: FocusScope(
|
||||
autofocus: true,
|
||||
child: Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
CustomAppBar(title: Text(t.screens.mpvConfig), pinned: true),
|
||||
return FocusedScrollScaffold(
|
||||
title: Text(t.screens.mpvConfig),
|
||||
slivers: _isLoading
|
||||
? [const SliverFillRemaining(child: Center(child: CircularProgressIndicator()))]
|
||||
: [
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
sliver: SliverList(
|
||||
@@ -295,9 +279,6 @@ class _MpvConfigScreenState extends State<MpvConfigScreen> {
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import '../../services/update_service.dart';
|
||||
import '../../utils/snackbar_helper.dart';
|
||||
import '../../utils/platform_detector.dart';
|
||||
import '../../widgets/desktop_app_bar.dart';
|
||||
import '../../widgets/focused_scroll_scaffold.dart';
|
||||
import '../../widgets/tv_number_spinner.dart';
|
||||
import 'hotkey_recorder_widget.dart';
|
||||
import '../../providers/companion_remote_provider.dart';
|
||||
@@ -212,7 +213,7 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
|
||||
onKeyEvent: _handleKeyEvent,
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
CustomAppBar(title: Text(t.settings.title), pinned: true),
|
||||
ExcludeFocus(child: CustomAppBar(title: Text(t.settings.title), pinned: true)),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
sliver: SliverList(
|
||||
@@ -1754,16 +1755,8 @@ class _KeyboardShortcutsScreenState extends State<_KeyboardShortcutsScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isLoading) {
|
||||
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
CustomAppBar(
|
||||
return FocusedScrollScaffold(
|
||||
title: Text(t.settings.keyboardShortcuts),
|
||||
pinned: true,
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
@@ -1776,7 +1769,9 @@ class _KeyboardShortcutsScreenState extends State<_KeyboardShortcutsScreen> {
|
||||
child: Text(t.common.reset),
|
||||
),
|
||||
],
|
||||
),
|
||||
slivers: _isLoading
|
||||
? [const SliverFillRemaining(child: Center(child: CircularProgressIndicator()))]
|
||||
: [
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
sliver: SliverList(
|
||||
@@ -1808,7 +1803,6 @@ class _KeyboardShortcutsScreenState extends State<_KeyboardShortcutsScreen> {
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,7 @@ import '../../focus/focusable_button.dart';
|
||||
import '../../i18n/strings.g.dart';
|
||||
import '../../services/settings_service.dart';
|
||||
import '../../focus/input_mode_tracker.dart';
|
||||
import '../../focus/key_event_utils.dart';
|
||||
import '../../widgets/desktop_app_bar.dart';
|
||||
import '../../widgets/focused_scroll_scaffold.dart';
|
||||
import '../../widgets/tv_color_picker.dart';
|
||||
import '../../widgets/tv_number_spinner.dart';
|
||||
|
||||
@@ -301,26 +300,11 @@ class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isLoading) {
|
||||
return Focus(
|
||||
canRequestFocus: false,
|
||||
onKeyEvent: (_, event) => handleBackKeyNavigation(context, event),
|
||||
child: FocusScope(
|
||||
autofocus: true,
|
||||
child: const Scaffold(body: Center(child: CircularProgressIndicator())),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Focus(
|
||||
canRequestFocus: false,
|
||||
onKeyEvent: (_, event) => handleBackKeyNavigation(context, event),
|
||||
child: FocusScope(
|
||||
autofocus: true,
|
||||
child: Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
CustomAppBar(title: Text(t.screens.subtitleStyling), pinned: true),
|
||||
return FocusedScrollScaffold(
|
||||
title: Text(t.screens.subtitleStyling),
|
||||
slivers: _isLoading
|
||||
? [const SliverFillRemaining(child: Center(child: CircularProgressIndicator()))]
|
||||
: [
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
sliver: SliverList(
|
||||
@@ -328,9 +312,6 @@ class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../focus/input_mode_tracker.dart';
|
||||
import '../focus/key_event_utils.dart';
|
||||
import 'desktop_app_bar.dart';
|
||||
|
||||
@@ -9,7 +10,10 @@ import 'desktop_app_bar.dart';
|
||||
/// - Keyboard navigation (back key handling)
|
||||
/// - Custom scrollable content with slivers
|
||||
/// - Consistent app bar with title and optional actions
|
||||
class FocusedScrollScaffold extends StatelessWidget {
|
||||
///
|
||||
/// Automatically focuses the first content item (skipping the app bar)
|
||||
/// when in keyboard navigation mode.
|
||||
class FocusedScrollScaffold extends StatefulWidget {
|
||||
/// The title to display in the app bar.
|
||||
/// Can be a Text widget or a more complex widget like Column.
|
||||
final Widget title;
|
||||
@@ -38,23 +42,54 @@ class FocusedScrollScaffold extends StatelessWidget {
|
||||
this.automaticallyImplyLeading = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<FocusedScrollScaffold> createState() => _FocusedScrollScaffoldState();
|
||||
}
|
||||
|
||||
class _FocusedScrollScaffoldState extends State<FocusedScrollScaffold> {
|
||||
final _scopeNode = FocusScopeNode();
|
||||
bool _focusRequested = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scopeNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _requestInitialFocus() {
|
||||
if (_focusRequested || !mounted || !InputModeTracker.isKeyboardMode(context)) return;
|
||||
_focusRequested = true;
|
||||
_scopeNode.requestFocus();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
primaryFocus?.nextFocus();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Request focus after first build when in keyboard mode
|
||||
if (!_focusRequested && InputModeTracker.isKeyboardMode(context)) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _requestInitialFocus());
|
||||
}
|
||||
|
||||
return Focus(
|
||||
canRequestFocus: false,
|
||||
onKeyEvent: (_, event) => handleBackKeyNavigation(context, event),
|
||||
child: FocusScope(
|
||||
autofocus: true,
|
||||
node: _scopeNode,
|
||||
child: Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
CustomAppBar(
|
||||
title: title,
|
||||
pinned: pinned,
|
||||
actions: actions,
|
||||
automaticallyImplyLeading: automaticallyImplyLeading,
|
||||
ExcludeFocus(
|
||||
child: CustomAppBar(
|
||||
title: widget.title,
|
||||
pinned: widget.pinned,
|
||||
actions: widget.actions,
|
||||
automaticallyImplyLeading: widget.automaticallyImplyLeading,
|
||||
),
|
||||
...slivers,
|
||||
),
|
||||
...widget.slivers,
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user