fix: dpad focus on pushed screens

close #510
This commit is contained in:
edde746
2026-02-22 11:46:47 +01:00
parent 83c0cce7f3
commit 1536feb034
7 changed files with 275 additions and 193 deletions
@@ -47,70 +47,76 @@ class _ExternalPlayerScreenState extends State<ExternalPlayerScreen> {
Widget build(BuildContext context) {
if (_isLoading) {
return Focus(
autofocus: true,
canRequestFocus: false,
onKeyEvent: (_, event) => handleBackKeyNavigation(context, event),
child: const Scaffold(body: Center(child: CircularProgressIndicator())),
child: FocusScope(
autofocus: true,
child: const Scaffold(body: Center(child: CircularProgressIndicator())),
),
);
}
final knownPlayers = KnownPlayers.getForCurrentPlatform();
return Focus(
autofocus: true,
canRequestFocus: false,
onKeyEvent: (_, event) => handleBackKeyNavigation(context, event),
child: Scaffold(
body: CustomScrollView(
slivers: [
CustomAppBar(title: Text(t.externalPlayer.title), pinned: true),
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverList(
delegate: SliverChildListDelegate([
Card(
child: SwitchListTile(
secondary: const AppIcon(Symbols.open_in_new_rounded, fill: 1),
title: Text(t.externalPlayer.useExternalPlayer),
subtitle: Text(t.externalPlayer.useExternalPlayerDescription),
value: _useExternalPlayer,
onChanged: (value) async {
setState(() => _useExternalPlayer = value);
await _settingsService.setUseExternalPlayer(value);
},
),
),
if (_useExternalPlayer) ...[
const SizedBox(height: 16),
child: FocusScope(
autofocus: true,
child: Scaffold(
body: CustomScrollView(
slivers: [
CustomAppBar(title: Text(t.externalPlayer.title), pinned: true),
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverList(
delegate: SliverChildListDelegate([
Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Text(
t.externalPlayer.selectPlayer,
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
),
// Known players
...knownPlayers.map((player) => _buildPlayerTile(player)),
// Custom players
if (_customPlayers.isNotEmpty) const Divider(),
..._customPlayers.map((player) => _buildPlayerTile(player, isCustom: true)),
// Add custom player button
const Divider(),
ListTile(
leading: const AppIcon(Symbols.add_rounded, fill: 1),
title: Text(t.externalPlayer.addCustomPlayer),
onTap: _showAddCustomPlayerDialog,
),
],
child: SwitchListTile(
secondary: const AppIcon(Symbols.open_in_new_rounded, fill: 1),
title: Text(t.externalPlayer.useExternalPlayer),
subtitle: Text(t.externalPlayer.useExternalPlayerDescription),
value: _useExternalPlayer,
onChanged: (value) async {
setState(() => _useExternalPlayer = value);
await _settingsService.setUseExternalPlayer(value);
},
),
),
],
]),
if (_useExternalPlayer) ...[
const SizedBox(height: 16),
Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Text(
t.externalPlayer.selectPlayer,
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
),
// Known players
...knownPlayers.map((player) => _buildPlayerTile(player)),
// Custom players
if (_customPlayers.isNotEmpty) const Divider(),
..._customPlayers.map((player) => _buildPlayerTile(player, isCustom: true)),
// Add custom player button
const Divider(),
ListTile(
leading: const AppIcon(Symbols.add_rounded, fill: 1),
title: Text(t.externalPlayer.addCustomPlayer),
onTap: _showAddCustomPlayerDialog,
),
],
),
),
],
]),
),
),
),
],
],
),
),
),
);
+50 -47
View File
@@ -167,55 +167,58 @@ class _LogsScreenState extends State<LogsScreen> {
@override
Widget build(BuildContext context) {
return Focus(
autofocus: true,
canRequestFocus: false,
onKeyEvent: (_, event) => handleBackKeyNavigation(context, event),
child: Scaffold(
body: CustomScrollView(
slivers: [
CustomAppBar(
title: Text(t.screens.logs),
pinned: true,
actions: [
IconButton(
icon: const AppIcon(Symbols.refresh_rounded, fill: 1),
onPressed: _loadLogs,
tooltip: t.common.refresh,
),
IconButton(
icon: const AppIcon(Symbols.upload_rounded, fill: 1),
onPressed: _logs.isNotEmpty ? _uploadLogs : null,
tooltip: t.logs.uploadLogs,
),
IconButton(
icon: const AppIcon(Symbols.content_copy_rounded, fill: 1),
onPressed: _logs.isNotEmpty ? _copyAllLogs : null,
tooltip: t.logs.copyLogs,
),
IconButton(
icon: const AppIcon(Symbols.delete_outline_rounded, fill: 1),
onPressed: _logs.isNotEmpty ? _clearLogs : null,
tooltip: t.logs.clearLogs,
),
],
),
if (_logs.isEmpty)
SliverFillRemaining(child: Center(child: Text(t.messages.noLogsAvailable)))
else
SliverPadding(
padding: const EdgeInsets.all(8),
sliver: SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
final log = _logs[index];
return _LogEntryCard(
log: log,
formatTime: _formatTime,
levelColor: _getLevelColor(log.level),
levelIcon: _getLevelIcon(log.level),
);
}, childCount: _logs.length),
),
child: FocusScope(
autofocus: true,
child: Scaffold(
body: CustomScrollView(
slivers: [
CustomAppBar(
title: Text(t.screens.logs),
pinned: true,
actions: [
IconButton(
icon: const AppIcon(Symbols.refresh_rounded, fill: 1),
onPressed: _loadLogs,
tooltip: t.common.refresh,
),
IconButton(
icon: const AppIcon(Symbols.upload_rounded, fill: 1),
onPressed: _logs.isNotEmpty ? _uploadLogs : null,
tooltip: t.logs.uploadLogs,
),
IconButton(
icon: const AppIcon(Symbols.content_copy_rounded, fill: 1),
onPressed: _logs.isNotEmpty ? _copyAllLogs : null,
tooltip: t.logs.copyLogs,
),
IconButton(
icon: const AppIcon(Symbols.delete_outline_rounded, fill: 1),
onPressed: _logs.isNotEmpty ? _clearLogs : null,
tooltip: t.logs.clearLogs,
),
],
),
],
if (_logs.isEmpty)
SliverFillRemaining(child: Center(child: Text(t.messages.noLogsAvailable)))
else
SliverPadding(
padding: const EdgeInsets.all(8),
sliver: SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
final log = _logs[index];
return _LogEntryCard(
log: log,
formatTime: _formatTime,
levelColor: _getLevelColor(log.level),
levelIcon: _getLevelIcon(log.level),
);
}, childCount: _logs.length),
),
),
],
),
),
),
);
+24 -18
View File
@@ -238,31 +238,37 @@ class _MpvConfigScreenState extends State<MpvConfigScreen> {
Widget build(BuildContext context) {
if (_isLoading) {
return Focus(
autofocus: true,
canRequestFocus: false,
onKeyEvent: (_, event) => handleBackKeyNavigation(context, event),
child: const Scaffold(body: Center(child: CircularProgressIndicator())),
child: FocusScope(
autofocus: true,
child: const Scaffold(body: Center(child: CircularProgressIndicator())),
),
);
}
return Focus(
autofocus: true,
canRequestFocus: false,
onKeyEvent: (_, event) => handleBackKeyNavigation(context, event),
child: Scaffold(
body: CustomScrollView(
slivers: [
CustomAppBar(title: Text(t.screens.mpvConfig), pinned: true),
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverList(
delegate: SliverChildListDelegate([
_buildPresetsCard(),
const SizedBox(height: 16),
_buildEntriesCard(),
const SizedBox(height: 24),
]),
child: FocusScope(
autofocus: true,
child: Scaffold(
body: CustomScrollView(
slivers: [
CustomAppBar(title: Text(t.screens.mpvConfig), pinned: true),
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverList(
delegate: SliverChildListDelegate([
_buildPresetsCard(),
const SizedBox(height: 16),
_buildEntriesCard(),
const SizedBox(height: 24),
]),
),
),
),
],
],
),
),
),
);
@@ -284,24 +284,32 @@ class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
Widget build(BuildContext context) {
if (_isLoading) {
return Focus(
autofocus: true,
canRequestFocus: false,
onKeyEvent: (_, event) => handleBackKeyNavigation(context, event),
child: const Scaffold(body: Center(child: CircularProgressIndicator())),
child: FocusScope(
autofocus: true,
child: const Scaffold(body: Center(child: CircularProgressIndicator())),
),
);
}
return Focus(
autofocus: true,
canRequestFocus: false,
onKeyEvent: (_, event) => handleBackKeyNavigation(context, event),
child: Scaffold(
body: CustomScrollView(
slivers: [
CustomAppBar(title: Text(t.screens.subtitleStyling), pinned: true),
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverList(delegate: SliverChildListDelegate([_buildStylingCard(), const SizedBox(height: 24)])),
),
],
child: FocusScope(
autofocus: true,
child: Scaffold(
body: CustomScrollView(
slivers: [
CustomAppBar(title: Text(t.screens.subtitleStyling), pinned: true),
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverList(
delegate: SliverChildListDelegate([_buildStylingCard(), const SizedBox(height: 24)]),
),
),
],
),
),
),
);
@@ -6,6 +6,7 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import '../../i18n/strings.g.dart';
import '../../focus/focusable_wrapper.dart';
import '../../utils/app_logger.dart';
import '../../utils/dialogs.dart';
import '../../widgets/focused_scroll_scaffold.dart';
@@ -144,24 +145,36 @@ class _NotInSessionViewState extends State<_NotInSessionView> {
const SizedBox(height: 48),
SizedBox(
width: double.infinity,
child: FilledButton.icon(
child: FocusableWrapper(
autofocus: true,
onPressed: _isCreating || _isJoining ? null : _createSession,
icon: _isCreating
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))
: const Icon(Symbols.add_rounded),
label: Text(_isCreating ? t.watchTogether.creating : t.watchTogether.createSession),
useBackgroundFocus: true,
disableScale: true,
borderRadius: 100,
onSelect: _isCreating || _isJoining ? null : _createSession,
child: FilledButton.icon(
onPressed: _isCreating || _isJoining ? null : _createSession,
icon: _isCreating
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))
: const Icon(Symbols.add_rounded),
label: Text(_isCreating ? t.watchTogether.creating : t.watchTogether.createSession),
),
),
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: _isCreating || _isJoining ? null : _joinSession,
icon: _isJoining
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))
: const Icon(Symbols.group_add_rounded),
label: Text(_isJoining ? t.watchTogether.joining : t.watchTogether.joinSession),
child: FocusableWrapper(
useBackgroundFocus: true,
disableScale: true,
borderRadius: 100,
onSelect: _isCreating || _isJoining ? null : _joinSession,
child: OutlinedButton.icon(
onPressed: _isCreating || _isJoining ? null : _joinSession,
icon: _isJoining
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))
: const Icon(Symbols.group_add_rounded),
label: Text(_isJoining ? t.watchTogether.joining : t.watchTogether.joinSession),
),
),
),
],
@@ -200,20 +213,38 @@ class _NotInSessionViewState extends State<_NotInSessionView> {
title: Text(t.watchTogether.controlMode),
content: Text(t.watchTogether.controlModeQuestion),
actions: [
TextButton(
FocusableWrapper(
autofocus: true,
onPressed: () => Navigator.pop(context),
style: TextButton.styleFrom(padding: buttonPadding, shape: buttonShape),
child: Text(t.common.cancel),
useBackgroundFocus: true,
disableScale: true,
borderRadius: 100,
onSelect: () => Navigator.pop(context),
child: TextButton(
onPressed: () => Navigator.pop(context),
style: TextButton.styleFrom(padding: buttonPadding, shape: buttonShape),
child: Text(t.common.cancel),
),
),
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.hostOnly),
child: TextButton(
onPressed: () => Navigator.pop(context, ControlMode.hostOnly),
style: TextButton.styleFrom(padding: buttonPadding, shape: buttonShape),
child: Text(t.watchTogether.hostOnly),
),
),
FilledButton(
onPressed: () => Navigator.pop(context, ControlMode.anyone),
child: Text(t.watchTogether.anyone),
FocusableWrapper(
useBackgroundFocus: true,
disableScale: true,
borderRadius: 100,
onSelect: () => Navigator.pop(context, ControlMode.anyone),
child: FilledButton(
onPressed: () => Navigator.pop(context, ControlMode.anyone),
child: Text(t.watchTogether.anyone),
),
),
],
),
@@ -377,15 +408,21 @@ class _ActiveSessionContent extends StatelessWidget {
// Leave/End Session Button
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
child: FocusableWrapper(
autofocus: true,
onPressed: () => _leaveSession(context),
style: OutlinedButton.styleFrom(
foregroundColor: theme.colorScheme.error,
side: BorderSide(color: theme.colorScheme.error),
useBackgroundFocus: true,
disableScale: true,
borderRadius: 100,
onSelect: () => _leaveSession(context),
child: OutlinedButton.icon(
onPressed: () => _leaveSession(context),
style: OutlinedButton.styleFrom(
foregroundColor: theme.colorScheme.error,
side: BorderSide(color: theme.colorScheme.error),
),
icon: Icon(watchTogether.isHost ? Symbols.close_rounded : Symbols.logout_rounded),
label: Text(watchTogether.isHost ? t.watchTogether.endSession : t.watchTogether.leaveSession),
),
icon: Icon(watchTogether.isHost ? Symbols.close_rounded : Symbols.logout_rounded),
label: Text(watchTogether.isHost ? t.watchTogether.endSession : t.watchTogether.leaveSession),
),
),
],
@@ -417,24 +454,30 @@ class _SessionCodeRow extends StatelessWidget {
Widget build(BuildContext context) {
final theme = Theme.of(context);
return InkWell(
onTap: () => _copySessionCode(context),
borderRadius: const BorderRadius.all(Radius.circular(4)),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'${t.watchTogether.sessionCode}: $sessionId',
style: theme.textTheme.bodySmall?.copyWith(
fontFamily: 'monospace',
color: theme.colorScheme.onSurfaceVariant,
return FocusableWrapper(
useBackgroundFocus: true,
disableScale: true,
borderRadius: 4,
onSelect: () => _copySessionCode(context),
child: InkWell(
onTap: () => _copySessionCode(context),
borderRadius: const BorderRadius.all(Radius.circular(4)),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'${t.watchTogether.sessionCode}: $sessionId',
style: theme.textTheme.bodySmall?.copyWith(
fontFamily: 'monospace',
color: theme.colorScheme.onSurfaceVariant,
),
),
),
const SizedBox(width: 4),
Icon(Symbols.content_copy_rounded, size: 14, color: theme.colorScheme.onSurfaceVariant),
],
const SizedBox(width: 4),
Icon(Symbols.content_copy_rounded, size: 14, color: theme.colorScheme.onSurfaceVariant),
],
),
),
),
);
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../focus/focusable_wrapper.dart';
import '../../i18n/strings.g.dart';
/// Dialog for joining a watch together session
@@ -43,7 +44,13 @@ class _JoinSessionDialogState extends State<JoinSessionDialog> {
Icon(Symbols.group_add, color: theme.colorScheme.primary),
const SizedBox(width: 12),
Expanded(child: Text(t.watchTogether.joinWatchSession, style: theme.textTheme.titleLarge)),
IconButton(onPressed: () => Navigator.of(context).pop(), icon: const Icon(Symbols.close)),
FocusableWrapper(
useBackgroundFocus: true,
disableScale: true,
borderRadius: 20,
onSelect: () => Navigator.of(context).pop(),
child: IconButton(onPressed: () => Navigator.of(context).pop(), icon: const Icon(Symbols.close)),
),
],
),
@@ -93,10 +100,16 @@ class _JoinSessionDialogState extends State<JoinSessionDialog> {
const SizedBox(height: 24),
// Join button
FilledButton.icon(
onPressed: _join,
icon: const Icon(Symbols.group_add),
label: Text(t.watchTogether.joinSession),
FocusableWrapper(
useBackgroundFocus: true,
disableScale: true,
borderRadius: 100,
onSelect: _join,
child: FilledButton.icon(
onPressed: _join,
icon: const Icon(Symbols.group_add),
label: Text(t.watchTogether.joinSession),
),
),
],
),
+15 -12
View File
@@ -41,19 +41,22 @@ class FocusedScrollScaffold extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Focus(
autofocus: true,
canRequestFocus: false,
onKeyEvent: (_, event) => handleBackKeyNavigation(context, event),
child: Scaffold(
body: CustomScrollView(
slivers: [
CustomAppBar(
title: title,
pinned: pinned,
actions: actions,
automaticallyImplyLeading: automaticallyImplyLeading,
),
...slivers,
],
child: FocusScope(
autofocus: true,
child: Scaffold(
body: CustomScrollView(
slivers: [
CustomAppBar(
title: title,
pinned: pinned,
actions: actions,
automaticallyImplyLeading: automaticallyImplyLeading,
),
...slivers,
],
),
),
),
);