From 2a1c99ac06279feff9c2545469583060d7bb2efb Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sun, 8 Feb 2026 10:30:14 +0100 Subject: [PATCH] fix: dialog autofocus and button sizing --- lib/screens/discover_screen.dart | 19 +++---- lib/screens/main_screen.dart | 14 ++++- lib/screens/media_detail_screen.dart | 41 +++++--------- lib/screens/settings/mpv_config_screen.dart | 16 ++---- lib/screens/video_player_screen.dart | 23 +++----- lib/utils/dialogs.dart | 54 +++++++++++++------ .../screens/watch_together_screen.dart | 33 ++++++------ .../widgets/watch_together_overlay.dart | 33 +++++------- 8 files changed, 115 insertions(+), 118 deletions(-) diff --git a/lib/screens/discover_screen.dart b/lib/screens/discover_screen.dart index 1f7da0f0..d5a5d55e 100644 --- a/lib/screens/discover_screen.dart +++ b/lib/screens/discover_screen.dart @@ -28,6 +28,7 @@ import '../mixins/item_updatable.dart'; import '../mixins/watch_state_aware.dart'; import '../utils/watch_state_notifier.dart'; import '../utils/app_logger.dart'; +import '../utils/dialogs.dart'; import '../utils/provider_extensions.dart'; import '../utils/video_player_navigation.dart'; import '../utils/layout_constants.dart'; @@ -867,19 +868,15 @@ class _DiscoverScreenState extends State } Future _handleLogout() async { - final confirm = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(t.common.logout), - content: Text(t.messages.logoutConfirm), - actions: [ - TextButton(onPressed: () => Navigator.pop(context, false), child: Text(t.common.cancel)), - FilledButton(onPressed: () => Navigator.pop(context, true), child: Text(t.common.logout)), - ], - ), + final confirm = await showConfirmDialog( + context, + title: t.common.logout, + message: t.messages.logoutConfirm, + confirmText: t.common.logout, + isDestructive: true, ); - if (confirm == true && mounted) { + if (confirm && mounted) { // Use comprehensive logout through UserProfileProvider final userProfileProvider = Provider.of(context, listen: false); final multiServerProvider = context.read(); diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index 57883fd6..15b8e536 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -198,12 +198,24 @@ class _MainScreenState extends State with RouteAware, WindowListener ], ), actions: [ - TextButton(onPressed: () => Navigator.pop(dialogContext), child: Text(t.common.later)), + TextButton( + autofocus: true, + onPressed: () => Navigator.pop(dialogContext), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14), + shape: const StadiumBorder(), + ), + child: Text(t.common.later), + ), 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), ), FilledButton( diff --git a/lib/screens/media_detail_screen.dart b/lib/screens/media_detail_screen.dart index eadc6113..e4c418e2 100644 --- a/lib/screens/media_detail_screen.dart +++ b/lib/screens/media_detail_screen.dart @@ -30,6 +30,7 @@ import '../theme/mono_tokens.dart'; import '../utils/app_logger.dart'; import '../utils/formatters.dart'; import '../utils/provider_extensions.dart'; +import '../utils/dialogs.dart'; import '../utils/snackbar_helper.dart'; import '../utils/video_player_navigation.dart'; import '../widgets/app_bar_back_button.dart'; @@ -477,24 +478,20 @@ class _MediaDetailScreenState extends State with WatchStateAw return IconButton.filledTonal( onPressed: () async { // Show options: Delete or Retry - final action = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Cancelled Download'), - content: const Text('This download was cancelled. What would you like to do?'), - actions: [ - TextButton(onPressed: () => Navigator.pop(context, 'delete'), child: Text(t.common.delete)), - TextButton(onPressed: () => Navigator.pop(context, 'retry'), child: const Text('Retry')), - ], - ), + final retry = await showConfirmDialog( + context, + title: 'Cancelled Download', + message: 'This download was cancelled. What would you like to do?', + cancelText: t.common.delete, + confirmText: 'Retry', ); - if (action == 'delete' && context.mounted) { + if (!retry && context.mounted) { await downloadProvider.deleteDownload(globalKey); if (context.mounted) { showSuccessSnackBar(context, t.downloads.downloadDeleted); } - } else if (action == 'retry' && context.mounted) { + } else if (retry && context.mounted) { final client = _getClientForMetadata(context); if (client == null) return; await downloadProvider.deleteDownload(globalKey); @@ -559,23 +556,13 @@ class _MediaDetailScreenState extends State with WatchStateAw return IconButton.filledTonal( onPressed: () async { // Show delete download confirmation - final confirmed = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(t.downloads.deleteDownload), - content: Text(t.downloads.deleteConfirm(title: metadata.title)), - actions: [ - TextButton(onPressed: () => Navigator.pop(context, false), child: Text(t.common.cancel)), - TextButton( - onPressed: () => Navigator.pop(context, true), - style: TextButton.styleFrom(foregroundColor: Colors.red), - child: Text(t.common.delete), - ), - ], - ), + final confirmed = await showDeleteConfirmation( + context, + title: t.downloads.deleteDownload, + message: t.downloads.deleteConfirm(title: metadata.title), ); - if (confirmed == true && context.mounted) { + if (confirmed && context.mounted) { await downloadProvider.deleteDownload(globalKey); if (context.mounted) { showSuccessSnackBar(context, t.downloads.downloadDeleted); diff --git a/lib/screens/settings/mpv_config_screen.dart b/lib/screens/settings/mpv_config_screen.dart index 9a38e393..f5ba81fd 100644 --- a/lib/screens/settings/mpv_config_screen.dart +++ b/lib/screens/settings/mpv_config_screen.dart @@ -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 '../../utils/dialogs.dart'; import '../../utils/snackbar_helper.dart'; import '../../services/settings_service.dart'; import '../../widgets/desktop_app_bar.dart'; @@ -143,19 +144,8 @@ class _MpvConfigScreenState extends State { saveFocusNode.dispose(); } - Future _showConfirmDeleteDialog({required String title, required String content}) async { - final result = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(title), - content: Text(content), - actions: [ - TextButton(onPressed: () => Navigator.pop(context, false), child: Text(t.common.cancel)), - TextButton(onPressed: () => Navigator.pop(context, true), child: Text(t.common.delete)), - ], - ), - ); - return result == true; + Future _showConfirmDeleteDialog({required String title, required String content}) { + return showDeleteConfirmation(context, title: title, message: content); } Future _showDeleteEntryDialog(int index) async { diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 206cc32f..36df6e8c 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -37,6 +37,7 @@ import '../services/shader_service.dart'; import '../providers/shader_provider.dart'; import '../providers/user_profile_provider.dart'; import '../utils/app_logger.dart'; +import '../utils/dialogs.dart'; import '../utils/orientation_helper.dart'; import '../utils/platform_detector.dart'; import '../utils/provider_extensions.dart'; @@ -1131,23 +1132,15 @@ class VideoPlayerScreenState extends State with WidgetsBindin try { // For non-host participants, show leave session confirmation if (_watchTogetherProvider != null && _watchTogetherProvider!.isInSession && !_watchTogetherProvider!.isHost) { - final confirmed = await showDialog( - context: context, - builder: (dialogContext) => AlertDialog( - title: const Text('Leave Session?'), - content: const Text('You will be removed from the session.'), - actions: [ - TextButton(onPressed: () => Navigator.pop(dialogContext, false), child: const Text('Cancel')), - FilledButton( - onPressed: () => Navigator.pop(dialogContext, true), - style: FilledButton.styleFrom(backgroundColor: Theme.of(dialogContext).colorScheme.error), - child: const Text('Leave'), - ), - ], - ), + final confirmed = await showConfirmDialog( + context, + title: 'Leave Session?', + message: 'You will be removed from the session.', + confirmText: 'Leave', + isDestructive: true, ); - if (confirmed == true && mounted) { + if (confirmed && mounted) { await _watchTogetherProvider!.leaveSession(); if (mounted) { // Exit fullscreen before leaving player (Windows/Linux only) diff --git a/lib/utils/dialogs.dart b/lib/utils/dialogs.dart index c2cbc928..29833006 100644 --- a/lib/utils/dialogs.dart +++ b/lib/utils/dialogs.dart @@ -3,28 +3,52 @@ import '../i18n/strings.g.dart'; /// Utility functions for showing common dialogs -/// Shows a delete confirmation dialog -/// Returns true if user confirmed, false if cancelled -Future showDeleteConfirmation(BuildContext context, {required String title, required String message}) async { +const _buttonPadding = EdgeInsets.symmetric(horizontal: 18, vertical: 14); +const _buttonShape = StadiumBorder(); + +/// Shows a confirmation dialog with consistent button sizing and autofocus. +/// Returns true if user confirmed, false if cancelled. +Future showConfirmDialog( + BuildContext context, { + required String title, + required String message, + required String confirmText, + String? cancelText, + bool isDestructive = false, +}) async { final confirmed = await showDialog( context: context, - builder: (context) => AlertDialog( - title: Text(title), - content: Text(message), - actions: [ - TextButton(onPressed: () => Navigator.pop(context, false), child: Text(t.common.cancel)), - TextButton( - onPressed: () => Navigator.pop(context, true), - style: TextButton.styleFrom(foregroundColor: Colors.red), - child: Text(t.common.delete), - ), - ], - ), + builder: (dialogContext) { + final colorScheme = Theme.of(dialogContext).colorScheme; + return AlertDialog( + title: Text(title), + content: Text(message), + actions: [ + TextButton( + autofocus: true, + onPressed: () => Navigator.pop(dialogContext, false), + style: TextButton.styleFrom(padding: _buttonPadding, shape: _buttonShape), + child: Text(cancelText ?? t.common.cancel), + ), + FilledButton( + onPressed: () => Navigator.pop(dialogContext, true), + style: isDestructive ? FilledButton.styleFrom(backgroundColor: colorScheme.error) : null, + child: Text(confirmText), + ), + ], + ); + }, ); return confirmed ?? false; } +/// Shows a delete confirmation dialog. +/// Convenience wrapper around [showConfirmDialog] with destructive styling. +Future showDeleteConfirmation(BuildContext context, {required String title, required String message}) { + return showConfirmDialog(context, title: title, message: message, confirmText: t.common.delete, isDestructive: true); +} + /// Shows a text input dialog for creating/naming items /// Returns the entered text, or null if cancelled Future showTextInputDialog( diff --git a/lib/watch_together/screens/watch_together_screen.dart b/lib/watch_together/screens/watch_together_screen.dart index b5371772..846c42ac 100644 --- a/lib/watch_together/screens/watch_together_screen.dart +++ b/lib/watch_together/screens/watch_together_screen.dart @@ -5,6 +5,7 @@ import 'package:provider/provider.dart'; import '../../i18n/strings.g.dart'; import '../../utils/app_logger.dart'; +import '../../utils/dialogs.dart'; import '../../widgets/focused_scroll_scaffold.dart'; import '../models/watch_session.dart'; import '../providers/watch_together_provider.dart'; @@ -143,15 +144,23 @@ class _NotInSessionViewState extends State<_NotInSessionView> { } Future _showControlModeDialog() { + const buttonPadding = EdgeInsets.symmetric(horizontal: 18, vertical: 14); + const buttonShape = StadiumBorder(); return showDialog( context: context, builder: (context) => AlertDialog( title: Text(t.watchTogether.controlMode), content: Text(t.watchTogether.controlModeQuestion), actions: [ - TextButton(onPressed: () => Navigator.pop(context), child: Text(t.common.cancel)), + TextButton( + autofocus: true, + 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), ), FilledButton( @@ -336,23 +345,15 @@ class _ActiveSessionContent extends StatelessWidget { } Future _leaveSession(BuildContext context) async { - final confirmed = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(watchTogether.isHost ? t.watchTogether.endSessionQuestion : t.watchTogether.leaveSessionQuestion), - content: Text(watchTogether.isHost ? t.watchTogether.endSessionConfirm : t.watchTogether.leaveSessionConfirm), - actions: [ - TextButton(onPressed: () => Navigator.pop(context, false), child: Text(t.common.cancel)), - FilledButton( - onPressed: () => Navigator.pop(context, true), - style: FilledButton.styleFrom(backgroundColor: Theme.of(context).colorScheme.error), - child: Text(watchTogether.isHost ? t.watchTogether.end : t.watchTogether.leave), - ), - ], - ), + final confirmed = await showConfirmDialog( + context, + title: watchTogether.isHost ? t.watchTogether.endSessionQuestion : t.watchTogether.leaveSessionQuestion, + message: watchTogether.isHost ? t.watchTogether.endSessionConfirm : t.watchTogether.leaveSessionConfirm, + confirmText: watchTogether.isHost ? t.watchTogether.end : t.watchTogether.leave, + isDestructive: true, ); - if (confirmed == true) { + if (confirmed) { await watchTogether.leaveSession(); } } diff --git a/lib/watch_together/widgets/watch_together_overlay.dart b/lib/watch_together/widgets/watch_together_overlay.dart index 53e6f856..3284a094 100644 --- a/lib/watch_together/widgets/watch_together_overlay.dart +++ b/lib/watch_together/widgets/watch_together_overlay.dart @@ -4,6 +4,7 @@ import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; import '../../i18n/strings.g.dart'; +import '../../utils/dialogs.dart'; import '../../utils/snackbar_helper.dart'; import '../models/watch_session.dart'; import '../providers/watch_together_provider.dart'; @@ -259,27 +260,19 @@ class _SessionMenuSheet extends StatelessWidget { showSuccessSnackBar(context, t.watchTogether.sessionCodeCopied); } - void _confirmLeave(BuildContext context) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(provider.isHost ? t.watchTogether.endSessionQuestion : t.watchTogether.leaveSessionQuestion), - content: Text( - provider.isHost ? t.watchTogether.endSessionConfirmOverlay : t.watchTogether.leaveSessionConfirmOverlay, - ), - actions: [ - TextButton(onPressed: () => Navigator.pop(context), child: Text(t.common.cancel)), - FilledButton( - onPressed: () { - Navigator.pop(context); - provider.leaveSession(); - onLeaveSession?.call(); - }, - child: Text(provider.isHost ? t.watchTogether.endSession : t.watchTogether.leave), - ), - ], - ), + void _confirmLeave(BuildContext context) async { + final confirmed = await showConfirmDialog( + context, + title: provider.isHost ? t.watchTogether.endSessionQuestion : t.watchTogether.leaveSessionQuestion, + message: provider.isHost ? t.watchTogether.endSessionConfirmOverlay : t.watchTogether.leaveSessionConfirmOverlay, + confirmText: provider.isHost ? t.watchTogether.endSession : t.watchTogether.leave, + isDestructive: true, ); + + if (confirmed) { + provider.leaveSession(); + onLeaveSession?.call(); + } } }