refactor: consolidate dialogs to DialogActionButton
This commit is contained in:
@@ -18,6 +18,7 @@ import '../utils/app_logger.dart';
|
|||||||
import '../utils/platform_detector.dart';
|
import '../utils/platform_detector.dart';
|
||||||
import '../focus/focusable_button.dart';
|
import '../focus/focusable_button.dart';
|
||||||
import '../utils/navigation_transitions.dart';
|
import '../utils/navigation_transitions.dart';
|
||||||
|
import '../widgets/dialog_action_button.dart';
|
||||||
import 'main_screen.dart';
|
import 'main_screen.dart';
|
||||||
|
|
||||||
class AuthScreen extends StatefulWidget {
|
class AuthScreen extends StatefulWidget {
|
||||||
@@ -261,8 +262,8 @@ class _AuthScreenState extends State<AuthScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.of(context).pop(), child: Text(t.common.cancel)),
|
DialogActionButton(onPressed: () => Navigator.of(context).pop(), label: t.common.cancel),
|
||||||
ElevatedButton(
|
DialogActionButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final token = tokenController.text.trim();
|
final token = tokenController.text.trim();
|
||||||
if (token.isEmpty) {
|
if (token.isEmpty) {
|
||||||
@@ -298,7 +299,8 @@ class _AuthScreenState extends State<AuthScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Text(t.auth.authenticate),
|
label: t.auth.authenticate,
|
||||||
|
isPrimary: true,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import '../../providers/companion_remote_provider.dart';
|
|||||||
import '../../utils/platform_detector.dart';
|
import '../../utils/platform_detector.dart';
|
||||||
import '../../theme/mono_tokens.dart';
|
import '../../theme/mono_tokens.dart';
|
||||||
import '../../utils/app_logger.dart';
|
import '../../utils/app_logger.dart';
|
||||||
|
import '../../utils/dialogs.dart';
|
||||||
import '../../widgets/companion_remote/discovery_view.dart';
|
import '../../widgets/companion_remote/discovery_view.dart';
|
||||||
import '../../widgets/overlay_sheet.dart';
|
import '../../widgets/overlay_sheet.dart';
|
||||||
import '../../widgets/pill_input_decoration.dart';
|
import '../../widgets/pill_input_decoration.dart';
|
||||||
@@ -36,19 +37,15 @@ class _MobileRemoteScreenState extends State<MobileRemoteScreen> {
|
|||||||
return IconButton(
|
return IconButton(
|
||||||
icon: const Icon(Icons.link_off),
|
icon: const Icon(Icons.link_off),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showConfirmDialog(
|
||||||
context: context,
|
context,
|
||||||
builder: (context) => AlertDialog(
|
title: t.common.disconnect,
|
||||||
title: Text(t.common.disconnect),
|
message: t.companionRemote.remote.disconnectConfirm,
|
||||||
content: Text(t.companionRemote.remote.disconnectConfirm),
|
confirmText: t.common.disconnect,
|
||||||
actions: [
|
isDestructive: true,
|
||||||
TextButton(onPressed: () => Navigator.pop(context, false), child: Text(t.common.cancel)),
|
|
||||||
TextButton(onPressed: () => Navigator.pop(context, true), child: Text(t.common.disconnect)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (confirmed == true && context.mounted) {
|
if (confirmed && context.mounted) {
|
||||||
await context.read<CompanionRemoteProvider>().leaveSession();
|
await context.read<CompanionRemoteProvider>().leaveSession();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../../focus/focus_theme.dart';
|
import '../../focus/focus_theme.dart';
|
||||||
import '../../focus/focusable_action_bar.dart';
|
import '../../focus/focusable_action_bar.dart';
|
||||||
import '../../focus/focusable_button.dart';
|
|
||||||
import '../../focus/dpad_navigator.dart';
|
import '../../focus/dpad_navigator.dart';
|
||||||
import '../../focus/input_mode_tracker.dart';
|
import '../../focus/input_mode_tracker.dart';
|
||||||
import '../../focus/key_event_utils.dart';
|
import '../../focus/key_event_utils.dart';
|
||||||
@@ -18,6 +17,7 @@ import '../../providers/hidden_libraries_provider.dart';
|
|||||||
import '../../providers/libraries_provider.dart';
|
import '../../providers/libraries_provider.dart';
|
||||||
import '../../providers/multi_server_provider.dart';
|
import '../../providers/multi_server_provider.dart';
|
||||||
import '../../utils/app_logger.dart';
|
import '../../utils/app_logger.dart';
|
||||||
|
import '../../utils/dialogs.dart';
|
||||||
import '../../utils/platform_detector.dart';
|
import '../../utils/platform_detector.dart';
|
||||||
import '../../utils/provider_extensions.dart';
|
import '../../utils/provider_extensions.dart';
|
||||||
import '../../utils/snackbar_helper.dart';
|
import '../../utils/snackbar_helper.dart';
|
||||||
@@ -593,29 +593,14 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
|||||||
if (item == null) return;
|
if (item == null) return;
|
||||||
|
|
||||||
if (item.requiresConfirmation) {
|
if (item.requiresConfirmation) {
|
||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showConfirmDialog(
|
||||||
context: context,
|
context,
|
||||||
builder: (context) => AlertDialog(
|
title: item.confirmationTitle ?? t.dialog.confirmAction,
|
||||||
title: Text(item.confirmationTitle ?? t.dialog.confirmAction),
|
message: item.confirmationMessage ?? t.libraries.confirmActionMessage,
|
||||||
content: Text(item.confirmationMessage ?? t.libraries.confirmActionMessage),
|
confirmText: t.common.confirm,
|
||||||
actions: [
|
isDestructive: item.isDestructive,
|
||||||
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),
|
|
||||||
style: item.isDestructive ? TextButton.styleFrom(foregroundColor: Colors.red) : null,
|
|
||||||
child: Text(t.common.confirm),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
if (confirmed != true) return;
|
if (!confirmed) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import '../../services/plex_client.dart';
|
|||||||
import '../i18n/strings.g.dart';
|
import '../i18n/strings.g.dart';
|
||||||
import '../services/update_service.dart';
|
import '../services/update_service.dart';
|
||||||
import '../utils/app_logger.dart';
|
import '../utils/app_logger.dart';
|
||||||
import '../focus/focusable_button.dart';
|
import '../widgets/dialog_action_button.dart';
|
||||||
import '../utils/dialogs.dart';
|
import '../utils/dialogs.dart';
|
||||||
import '../utils/provider_extensions.dart';
|
import '../utils/provider_extensions.dart';
|
||||||
import '../utils/platform_detector.dart';
|
import '../utils/platform_detector.dart';
|
||||||
@@ -254,36 +254,15 @@ class _MainScreenState extends State<MainScreen> with RouteAware, WindowListener
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
FocusableButton(
|
DialogActionButton(onPressed: () => Navigator.pop(dialogContext), label: t.common.later),
|
||||||
autofocus: true,
|
DialogActionButton(
|
||||||
onPressed: () => Navigator.pop(dialogContext),
|
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
FocusableButton(
|
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await UpdateService.skipVersion(updateInfo['latestVersion']);
|
await UpdateService.skipVersion(updateInfo['latestVersion']);
|
||||||
if (dialogContext.mounted) Navigator.pop(dialogContext);
|
if (dialogContext.mounted) Navigator.pop(dialogContext);
|
||||||
},
|
},
|
||||||
child: TextButton(
|
label: t.update.skipVersion,
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
FocusableButton(
|
DialogActionButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final url = Uri.parse(updateInfo['releaseUrl']);
|
final url = Uri.parse(updateInfo['releaseUrl']);
|
||||||
if (await canLaunchUrl(url)) {
|
if (await canLaunchUrl(url)) {
|
||||||
@@ -291,16 +270,8 @@ class _MainScreenState extends State<MainScreen> with RouteAware, WindowListener
|
|||||||
}
|
}
|
||||||
if (dialogContext.mounted) Navigator.pop(dialogContext);
|
if (dialogContext.mounted) Navigator.pop(dialogContext);
|
||||||
},
|
},
|
||||||
child: FilledButton(
|
label: t.update.viewRelease,
|
||||||
onPressed: () async {
|
isPrimary: true,
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -591,7 +591,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
|
|||||||
title: Text(t.settings.introPattern),
|
title: Text(t.settings.introPattern),
|
||||||
subtitle: Text(t.settings.introPatternDescription),
|
subtitle: Text(t.settings.introPatternDescription),
|
||||||
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
|
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
|
||||||
onTap: () => showTextInputDialog(
|
onTap: () => showRegexInputDialog(
|
||||||
context: context,
|
context: context,
|
||||||
title: t.settings.introPattern,
|
title: t.settings.introPattern,
|
||||||
currentValue: _introPattern,
|
currentValue: _introPattern,
|
||||||
@@ -610,7 +610,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
|
|||||||
title: Text(t.settings.creditsPattern),
|
title: Text(t.settings.creditsPattern),
|
||||||
subtitle: Text(t.settings.creditsPatternDescription),
|
subtitle: Text(t.settings.creditsPatternDescription),
|
||||||
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
|
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
|
||||||
onTap: () => showTextInputDialog(
|
onTap: () => showRegexInputDialog(
|
||||||
context: context,
|
context: context,
|
||||||
title: t.settings.creditsPattern,
|
title: t.settings.creditsPattern,
|
||||||
currentValue: _creditsPattern,
|
currentValue: _creditsPattern,
|
||||||
|
|||||||
@@ -28,9 +28,11 @@ import '../../providers/trakt_account_provider.dart';
|
|||||||
import '../../services/keyboard_shortcuts_service.dart';
|
import '../../services/keyboard_shortcuts_service.dart';
|
||||||
import '../../services/settings_service.dart' as settings;
|
import '../../services/settings_service.dart' as settings;
|
||||||
import '../../services/update_service.dart';
|
import '../../services/update_service.dart';
|
||||||
|
import '../../utils/dialogs.dart';
|
||||||
import '../../utils/snackbar_helper.dart';
|
import '../../utils/snackbar_helper.dart';
|
||||||
import '../../utils/platform_detector.dart';
|
import '../../utils/platform_detector.dart';
|
||||||
import '../../widgets/desktop_app_bar.dart';
|
import '../../widgets/desktop_app_bar.dart';
|
||||||
|
import '../../widgets/dialog_action_button.dart';
|
||||||
import '../../widgets/settings_section.dart';
|
import '../../widgets/settings_section.dart';
|
||||||
import 'about_screen.dart';
|
import 'about_screen.dart';
|
||||||
import 'appearance_settings_screen.dart';
|
import 'appearance_settings_screen.dart';
|
||||||
@@ -557,20 +559,21 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
if (isCustom)
|
if (isCustom)
|
||||||
TextButton(
|
DialogActionButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
Navigator.pop(dialogContext);
|
Navigator.pop(dialogContext);
|
||||||
await _resetDownloadLocation();
|
await _resetDownloadLocation();
|
||||||
},
|
},
|
||||||
child: Text(t.settings.resetToDefault),
|
label: t.settings.resetToDefault,
|
||||||
),
|
),
|
||||||
TextButton(onPressed: () => Navigator.pop(dialogContext), child: Text(t.common.cancel)),
|
DialogActionButton(onPressed: () => Navigator.pop(dialogContext), label: t.common.cancel),
|
||||||
FilledButton(
|
DialogActionButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
Navigator.pop(dialogContext);
|
Navigator.pop(dialogContext);
|
||||||
await _selectDownloadLocation();
|
await _selectDownloadLocation();
|
||||||
},
|
},
|
||||||
child: Text(t.settings.selectFolder),
|
label: t.settings.selectFolder,
|
||||||
|
isPrimary: true,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -654,17 +657,17 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
|
|||||||
onEditingComplete: () => saveFocusNode.requestFocus(),
|
onEditingComplete: () => saveFocusNode.requestFocus(),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
DialogActionButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
controller.clear();
|
controller.clear();
|
||||||
await _settingsService.setCustomRelayUrl(null);
|
await _settingsService.setCustomRelayUrl(null);
|
||||||
if (mounted) setState(() => _customRelayUrl = null);
|
if (mounted) setState(() => _customRelayUrl = null);
|
||||||
if (dialogContext.mounted) Navigator.pop(dialogContext);
|
if (dialogContext.mounted) Navigator.pop(dialogContext);
|
||||||
},
|
},
|
||||||
child: Text(t.settings.resetToDefault),
|
label: t.settings.resetToDefault,
|
||||||
),
|
),
|
||||||
TextButton(onPressed: () => Navigator.pop(dialogContext), child: Text(t.common.cancel)),
|
DialogActionButton(onPressed: () => Navigator.pop(dialogContext), label: t.common.cancel),
|
||||||
TextButton(
|
DialogActionButton(
|
||||||
focusNode: saveFocusNode,
|
focusNode: saveFocusNode,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final url = controller.text.trim().isEmpty ? null : controller.text.trim();
|
final url = controller.text.trim().isEmpty ? null : controller.text.trim();
|
||||||
@@ -672,7 +675,7 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
|
|||||||
if (mounted) setState(() => _customRelayUrl = url);
|
if (mounted) setState(() => _customRelayUrl = url);
|
||||||
if (dialogContext.mounted) Navigator.pop(dialogContext);
|
if (dialogContext.mounted) Navigator.pop(dialogContext);
|
||||||
},
|
},
|
||||||
child: Text(t.common.save),
|
label: t.common.save,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -683,58 +686,33 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showClearCacheDialog() {
|
Future<void> _showClearCacheDialog() async {
|
||||||
showDialog(
|
final confirmed = await showConfirmDialog(
|
||||||
context: context,
|
context,
|
||||||
builder: (BuildContext context) {
|
title: t.settings.clearCache,
|
||||||
return AlertDialog(
|
message: t.settings.clearCacheDescription,
|
||||||
title: Text(t.settings.clearCache),
|
confirmText: t.common.clear,
|
||||||
content: Text(t.settings.clearCacheDescription),
|
|
||||||
actions: [
|
|
||||||
TextButton(autofocus: true, onPressed: () => Navigator.pop(context), child: Text(t.common.cancel)),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () async {
|
|
||||||
final navigator = Navigator.of(context);
|
|
||||||
await _settingsService.clearCache();
|
|
||||||
if (mounted) {
|
|
||||||
navigator.pop();
|
|
||||||
showSuccessSnackBar(this.context, t.settings.clearCacheSuccess);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Text(t.common.clear),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
if (!confirmed) return;
|
||||||
|
await _settingsService.clearCache();
|
||||||
|
if (mounted) showSuccessSnackBar(context, t.settings.clearCacheSuccess);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showResetSettingsDialog() {
|
Future<void> _showResetSettingsDialog() async {
|
||||||
showDialog(
|
final confirmed = await showConfirmDialog(
|
||||||
context: context,
|
context,
|
||||||
builder: (BuildContext context) {
|
title: t.settings.resetSettings,
|
||||||
return AlertDialog(
|
message: t.settings.resetSettingsDescription,
|
||||||
title: Text(t.settings.resetSettings),
|
confirmText: t.common.reset,
|
||||||
content: Text(t.settings.resetSettingsDescription),
|
isDestructive: true,
|
||||||
actions: [
|
|
||||||
TextButton(autofocus: true, onPressed: () => Navigator.pop(context), child: Text(t.common.cancel)),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () async {
|
|
||||||
final navigator = Navigator.of(context);
|
|
||||||
await _settingsService.resetAllSettings();
|
|
||||||
await _keyboardService?.resetToDefaults();
|
|
||||||
if (mounted) {
|
|
||||||
navigator.pop();
|
|
||||||
showSuccessSnackBar(this.context, t.settings.resetSettingsSuccess);
|
|
||||||
_loadSettings();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Text(t.common.reset),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
if (!confirmed) return;
|
||||||
|
await _settingsService.resetAllSettings();
|
||||||
|
await _keyboardService?.resetToDefaults();
|
||||||
|
if (mounted) {
|
||||||
|
showSuccessSnackBar(context, t.settings.resetSettingsSuccess);
|
||||||
|
_loadSettings();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleExportSettings() async {
|
Future<void> _handleExportSettings() async {
|
||||||
@@ -750,26 +728,15 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showImportSettingsDialog() {
|
Future<void> _showImportSettingsDialog() async {
|
||||||
showDialog(
|
final confirmed = await showConfirmDialog(
|
||||||
context: context,
|
context,
|
||||||
builder: (BuildContext dialogContext) {
|
title: t.settings.importSettings,
|
||||||
return AlertDialog(
|
message: t.settings.importSettingsConfirm,
|
||||||
title: Text(t.settings.importSettings),
|
confirmText: t.settings.importSettings,
|
||||||
content: Text(t.settings.importSettingsConfirm),
|
|
||||||
actions: [
|
|
||||||
TextButton(autofocus: true, onPressed: () => Navigator.pop(dialogContext), child: Text(t.common.cancel)),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(dialogContext);
|
|
||||||
_handleImportSettings();
|
|
||||||
},
|
|
||||||
child: Text(t.settings.importSettings),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
if (!confirmed) return;
|
||||||
|
await _handleImportSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleImportSettings() async {
|
Future<void> _handleImportSettings() async {
|
||||||
@@ -856,8 +823,8 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(autofocus: true, onPressed: () => Navigator.pop(context), child: Text(t.common.close)),
|
DialogActionButton(onPressed: () => Navigator.pop(context), label: t.common.close),
|
||||||
FilledButton(
|
DialogActionButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final url = Uri.parse(_updateInfo!['releaseUrl']);
|
final url = Uri.parse(_updateInfo!['releaseUrl']);
|
||||||
if (await canLaunchUrl(url)) {
|
if (await canLaunchUrl(url)) {
|
||||||
@@ -865,7 +832,8 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
|
|||||||
}
|
}
|
||||||
if (context.mounted) Navigator.pop(context);
|
if (context.mounted) Navigator.pop(context);
|
||||||
},
|
},
|
||||||
child: Text(t.update.viewRelease),
|
label: t.update.viewRelease,
|
||||||
|
isPrimary: true,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import '../../focus/input_mode_tracker.dart';
|
import '../../focus/input_mode_tracker.dart';
|
||||||
import '../../i18n/strings.g.dart';
|
import '../../i18n/strings.g.dart';
|
||||||
|
import '../../widgets/dialog_action_button.dart';
|
||||||
|
import '../../widgets/focusable_list_tile.dart';
|
||||||
import '../../widgets/tv_number_spinner.dart';
|
import '../../widgets/tv_number_spinner.dart';
|
||||||
|
|
||||||
/// Model for option selection dialogs.
|
/// Model for option selection dialogs.
|
||||||
@@ -13,7 +15,7 @@ class DialogOption<T> {
|
|||||||
const DialogOption({required this.value, required this.title, this.subtitle});
|
const DialogOption({required this.value, required this.title, this.subtitle});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows an M3-conformant AlertDialog with RadioListTiles for multi-option selection.
|
/// Shows a selection dialog with focusable rows for dpad/keyboard navigation.
|
||||||
/// Used for settings with 5+ options (language, buffer size, etc.).
|
/// Used for settings with 5+ options (language, buffer size, etc.).
|
||||||
Future<T?> showSelectionDialog<T>({
|
Future<T?> showSelectionDialog<T>({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
@@ -21,27 +23,29 @@ Future<T?> showSelectionDialog<T>({
|
|||||||
required List<DialogOption<T>> options,
|
required List<DialogOption<T>> options,
|
||||||
required T currentValue,
|
required T currentValue,
|
||||||
}) {
|
}) {
|
||||||
|
final focusFirstItem = InputModeTracker.isKeyboardMode(context);
|
||||||
return showDialog<T>(
|
return showDialog<T>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (dialogContext) => AlertDialog(
|
builder: (dialogContext) => AlertDialog(
|
||||||
title: Text(title),
|
title: Text(title),
|
||||||
contentPadding: const EdgeInsets.only(top: 12, bottom: 24),
|
contentPadding: const EdgeInsets.only(top: 12, bottom: 24),
|
||||||
content: SingleChildScrollView(
|
content: SingleChildScrollView(
|
||||||
child: RadioGroup<T>(
|
child: Column(
|
||||||
groupValue: currentValue,
|
mainAxisSize: MainAxisSize.min,
|
||||||
onChanged: (value) => Navigator.pop(dialogContext, value),
|
children: options.map((option) {
|
||||||
child: Column(
|
final selected = option.value == currentValue;
|
||||||
mainAxisSize: MainAxisSize.min,
|
return FocusableListTile(
|
||||||
children: options
|
leading: Icon(
|
||||||
.map(
|
selected ? Icons.radio_button_checked : Icons.radio_button_unchecked,
|
||||||
(option) => RadioListTile<T>(
|
color: selected ? Theme.of(dialogContext).colorScheme.primary : null,
|
||||||
title: Text(option.title),
|
),
|
||||||
subtitle: option.subtitle != null ? Text(option.subtitle!) : null,
|
title: Text(option.title),
|
||||||
value: option.value,
|
subtitle: option.subtitle != null ? Text(option.subtitle!) : null,
|
||||||
),
|
selected: selected,
|
||||||
)
|
autofocus: focusFirstItem && selected,
|
||||||
.toList(),
|
onTap: () => Navigator.pop(dialogContext, option.value),
|
||||||
),
|
);
|
||||||
|
}).toList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -133,8 +137,8 @@ void _showNumericInputDialogTV({
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(dialogContext), child: Text(t.common.cancel)),
|
DialogActionButton(onPressed: () => Navigator.pop(dialogContext), label: t.common.cancel),
|
||||||
TextButton(
|
DialogActionButton(
|
||||||
focusNode: saveFocusNode,
|
focusNode: saveFocusNode,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await onSave(spinnerValue);
|
await onSave(spinnerValue);
|
||||||
@@ -142,7 +146,7 @@ void _showNumericInputDialogTV({
|
|||||||
Navigator.pop(dialogContext);
|
Navigator.pop(dialogContext);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Text(t.common.save),
|
label: t.common.save,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -201,8 +205,8 @@ void _showNumericInputDialogStandard({
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(dialogContext), child: Text(t.common.cancel)),
|
DialogActionButton(onPressed: () => Navigator.pop(dialogContext), label: t.common.cancel),
|
||||||
TextButton(
|
DialogActionButton(
|
||||||
focusNode: saveFocusNode,
|
focusNode: saveFocusNode,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final parsed = int.tryParse(controller.text);
|
final parsed = int.tryParse(controller.text);
|
||||||
@@ -213,7 +217,7 @@ void _showNumericInputDialogStandard({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Text(t.common.save),
|
label: t.common.save,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -227,7 +231,7 @@ void _showNumericInputDialogStandard({
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Shows a text input dialog with regex validation and reset-to-default support.
|
/// Shows a text input dialog with regex validation and reset-to-default support.
|
||||||
void showTextInputDialog({
|
void showRegexInputDialog({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
required String title,
|
required String title,
|
||||||
required String currentValue,
|
required String currentValue,
|
||||||
@@ -263,22 +267,22 @@ void showTextInputDialog({
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
DialogActionButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
controller.text = defaultValue;
|
controller.text = defaultValue;
|
||||||
setDialogState(() => errorText = null);
|
setDialogState(() => errorText = null);
|
||||||
},
|
},
|
||||||
child: Text(t.settings.resetToDefault),
|
label: t.settings.resetToDefault,
|
||||||
),
|
),
|
||||||
TextButton(onPressed: () => Navigator.pop(dialogContext), child: Text(t.common.cancel)),
|
DialogActionButton(onPressed: () => Navigator.pop(dialogContext), label: t.common.cancel),
|
||||||
TextButton(
|
DialogActionButton(
|
||||||
focusNode: saveFocusNode,
|
focusNode: saveFocusNode,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (errorText != null) return;
|
if (errorText != null) return;
|
||||||
await onSave(controller.text);
|
await onSave(controller.text);
|
||||||
if (dialogContext.mounted) Navigator.pop(dialogContext);
|
if (dialogContext.mounted) Navigator.pop(dialogContext);
|
||||||
},
|
},
|
||||||
child: Text(t.common.save),
|
label: t.common.save,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ import '../../services/settings_service.dart';
|
|||||||
import '../../services/trakt/trakt_scrobble_service.dart';
|
import '../../services/trakt/trakt_scrobble_service.dart';
|
||||||
import '../../services/trakt/trakt_sync_service.dart';
|
import '../../services/trakt/trakt_sync_service.dart';
|
||||||
import '../../utils/app_logger.dart';
|
import '../../utils/app_logger.dart';
|
||||||
|
import '../../utils/dialogs.dart';
|
||||||
import '../../utils/snackbar_helper.dart';
|
import '../../utils/snackbar_helper.dart';
|
||||||
|
import '../../widgets/dialog_action_button.dart';
|
||||||
import '../../widgets/app_icon.dart';
|
import '../../widgets/app_icon.dart';
|
||||||
import '../../widgets/focused_scroll_scaffold.dart';
|
import '../../widgets/focused_scroll_scaffold.dart';
|
||||||
import '../../widgets/settings_section.dart';
|
import '../../widgets/settings_section.dart';
|
||||||
@@ -99,21 +101,14 @@ class _TraktSettingsScreenState extends State<TraktSettingsScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _disconnect(TraktAccountProvider account) async {
|
Future<void> _disconnect(TraktAccountProvider account) async {
|
||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showConfirmDialog(
|
||||||
context: context,
|
context,
|
||||||
builder: (ctx) => AlertDialog(
|
title: t.trakt.disconnectConfirm,
|
||||||
title: Text(t.trakt.disconnectConfirm),
|
message: t.trakt.disconnectConfirmBody,
|
||||||
content: Text(t.trakt.disconnectConfirmBody),
|
confirmText: t.common.disconnect,
|
||||||
actions: [
|
isDestructive: true,
|
||||||
TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: Text(t.common.cancel)),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(ctx).pop(true),
|
|
||||||
child: Text(t.common.disconnect, style: TextStyle(color: Theme.of(ctx).colorScheme.error)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
if (confirmed != true) return;
|
if (!confirmed) return;
|
||||||
await account.disconnect();
|
await account.disconnect();
|
||||||
// build()'s post-frame handler pops the screen once the provider rebuilds
|
// build()'s post-frame handler pops the screen once the provider rebuilds
|
||||||
// with isConnected == false — don't pop here too.
|
// with isConnected == false — don't pop here too.
|
||||||
@@ -262,12 +257,12 @@ class _DeviceCodeDialogState extends State<_DeviceCodeDialog> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
DialogActionButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
widget.account.cancelConnect();
|
widget.account.cancelConnect();
|
||||||
Navigator.of(context, rootNavigator: true).pop();
|
Navigator.of(context, rootNavigator: true).pop();
|
||||||
},
|
},
|
||||||
child: Text(t.common.cancel),
|
label: t.common.cancel,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import '../../utils/app_logger.dart';
|
|||||||
import '../../utils/provider_extensions.dart';
|
import '../../utils/provider_extensions.dart';
|
||||||
import '../../utils/dialogs.dart';
|
import '../../utils/dialogs.dart';
|
||||||
import '../../utils/snackbar_helper.dart';
|
import '../../utils/snackbar_helper.dart';
|
||||||
|
import '../../widgets/dialog_action_button.dart';
|
||||||
import '../../utils/video_player_navigation.dart';
|
import '../../utils/video_player_navigation.dart';
|
||||||
import '../../widgets/focused_scroll_scaffold.dart';
|
import '../../widgets/focused_scroll_scaffold.dart';
|
||||||
import '../models/watch_session.dart';
|
import '../models/watch_session.dart';
|
||||||
@@ -241,37 +242,21 @@ class _NotInSessionViewState extends State<_NotInSessionView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<ControlMode?> _showControlModeDialog() {
|
Future<ControlMode?> _showControlModeDialog() {
|
||||||
const buttonPadding = EdgeInsets.symmetric(horizontal: 18, vertical: 14);
|
|
||||||
const buttonShape = StadiumBorder();
|
|
||||||
return showDialog<ControlMode>(
|
return showDialog<ControlMode>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: Text(t.watchTogether.controlMode),
|
title: Text(t.watchTogether.controlMode),
|
||||||
content: Text(t.watchTogether.controlModeQuestion),
|
content: Text(t.watchTogether.controlModeQuestion),
|
||||||
actions: [
|
actions: [
|
||||||
FocusableButton(
|
DialogActionButton(onPressed: () => Navigator.pop(context), label: t.common.cancel),
|
||||||
autofocus: true,
|
DialogActionButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
style: TextButton.styleFrom(padding: buttonPadding, shape: buttonShape),
|
|
||||||
child: Text(t.common.cancel),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
FocusableButton(
|
|
||||||
onPressed: () => Navigator.pop(context, ControlMode.hostOnly),
|
onPressed: () => Navigator.pop(context, ControlMode.hostOnly),
|
||||||
child: TextButton(
|
label: t.watchTogether.hostOnly,
|
||||||
onPressed: () => Navigator.pop(context, ControlMode.hostOnly),
|
|
||||||
style: TextButton.styleFrom(padding: buttonPadding, shape: buttonShape),
|
|
||||||
child: Text(t.watchTogether.hostOnly),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
FocusableButton(
|
DialogActionButton(
|
||||||
onPressed: () => Navigator.pop(context, ControlMode.anyone),
|
onPressed: () => Navigator.pop(context, ControlMode.anyone),
|
||||||
child: FilledButton(
|
label: t.watchTogether.anyone,
|
||||||
onPressed: () => Navigator.pop(context, ControlMode.anyone),
|
isPrimary: true,
|
||||||
child: Text(t.watchTogether.anyone),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user