diff --git a/lib/screens/settings/external_player_screen.dart b/lib/screens/settings/external_player_screen.dart index 1aa6e1e4..a52f3f37 100644 --- a/lib/screens/settings/external_player_screen.dart +++ b/lib/screens/settings/external_player_screen.dart @@ -47,70 +47,76 @@ class _ExternalPlayerScreenState extends State { 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, + ), + ], + ), + ), + ], + ]), + ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/screens/settings/logs_screen.dart b/lib/screens/settings/logs_screen.dart index cabf5481..37e95ac8 100644 --- a/lib/screens/settings/logs_screen.dart +++ b/lib/screens/settings/logs_screen.dart @@ -167,55 +167,58 @@ class _LogsScreenState extends State { @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), + ), + ), + ], + ), ), ), ); diff --git a/lib/screens/settings/mpv_config_screen.dart b/lib/screens/settings/mpv_config_screen.dart index 2689eb03..489028df 100644 --- a/lib/screens/settings/mpv_config_screen.dart +++ b/lib/screens/settings/mpv_config_screen.dart @@ -238,31 +238,37 @@ class _MpvConfigScreenState extends State { 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), + ]), + ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/screens/settings/subtitle_styling_screen.dart b/lib/screens/settings/subtitle_styling_screen.dart index 871781de..127c98ca 100644 --- a/lib/screens/settings/subtitle_styling_screen.dart +++ b/lib/screens/settings/subtitle_styling_screen.dart @@ -284,24 +284,32 @@ class _SubtitleStylingScreenState extends State { 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)]), + ), + ), + ], + ), ), ), ); diff --git a/lib/watch_together/screens/watch_together_screen.dart b/lib/watch_together/screens/watch_together_screen.dart index 9a521b5d..31ea81fb 100644 --- a/lib/watch_together/screens/watch_together_screen.dart +++ b/lib/watch_together/screens/watch_together_screen.dart @@ -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), + ], + ), ), ), ); diff --git a/lib/watch_together/widgets/join_session_dialog.dart b/lib/watch_together/widgets/join_session_dialog.dart index 433f0032..900cd59e 100644 --- a/lib/watch_together/widgets/join_session_dialog.dart +++ b/lib/watch_together/widgets/join_session_dialog.dart @@ -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 { 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 { 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), + ), ), ], ), diff --git a/lib/widgets/focused_scroll_scaffold.dart b/lib/widgets/focused_scroll_scaffold.dart index 4dcea050..0f7307b6 100644 --- a/lib/widgets/focused_scroll_scaffold.dart +++ b/lib/widgets/focused_scroll_scaffold.dart @@ -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, + ], + ), ), ), );