refactor: dialog boxes

This commit is contained in:
cogwheel0
2025-09-07 23:48:47 +05:30
parent bbb1f081d1
commit adbe3eb85f
13 changed files with 210 additions and 181 deletions

View File

@@ -12,6 +12,7 @@ import '../../chat/providers/chat_providers.dart' as chat;
// import '../../files/views/files_page.dart'; // import '../../files/views/files_page.dart';
import '../../profile/views/profile_page.dart'; import '../../profile/views/profile_page.dart';
import '../../../shared/utils/ui_utils.dart'; import '../../../shared/utils/ui_utils.dart';
import '../../../shared/widgets/themed_dialogs.dart';
import '../../../core/auth/auth_state_manager.dart'; import '../../../core/auth/auth_state_manager.dart';
import 'package:conduit/l10n/app_localizations.dart'; import 'package:conduit/l10n/app_localizations.dart';
import '../../../core/models/user.dart' as models; import '../../../core/models/user.dart' as models;
@@ -580,43 +581,12 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
} }
Future<void> _promptCreateFolder() async { Future<void> _promptCreateFolder() async {
final theme = context.conduitTheme; final name = await ThemedDialogs.promptTextInput(
final controller = TextEditingController(); context,
final name = await showDialog<String>( title: AppLocalizations.of(context)!.newFolder,
context: context, hintText: AppLocalizations.of(context)!.folderName,
builder: (ctx) => AlertDialog( confirmText: AppLocalizations.of(context)!.create,
backgroundColor: theme.surfaceBackground, cancelText: AppLocalizations.of(context)!.cancel,
title: Text(
AppLocalizations.of(context)!.newFolder,
style: TextStyle(color: theme.textPrimary),
),
content: TextField(
controller: controller,
autofocus: true,
style: TextStyle(color: theme.inputText),
decoration: InputDecoration(
hintText: AppLocalizations.of(context)!.folderName,
hintStyle: TextStyle(color: theme.inputPlaceholder),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: theme.inputBorder),
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: theme.buttonPrimary),
),
),
onSubmitted: (v) => Navigator.pop(ctx, controller.text.trim()),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text(AppLocalizations.of(context)!.cancel),
),
TextButton(
onPressed: () => Navigator.pop(ctx, controller.text.trim()),
child: Text(AppLocalizations.of(context)!.create),
),
],
),
); );
if (name == null) return; if (name == null) return;
@@ -822,50 +792,13 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
String folderId, String folderId,
String currentName, String currentName,
) async { ) async {
final theme = context.conduitTheme; final newName = await ThemedDialogs.promptTextInput(
final controller = TextEditingController(text: currentName); context,
title: AppLocalizations.of(context)!.rename,
final newName = await showDialog<String>( hintText: AppLocalizations.of(context)!.folderName,
context: context, initialValue: currentName,
builder: (dialogContext) { confirmText: AppLocalizations.of(context)!.save,
return AlertDialog( cancelText: AppLocalizations.of(context)!.cancel,
backgroundColor: theme.surfaceBackground,
title: Text(
AppLocalizations.of(context)!.rename,
style: TextStyle(color: theme.textPrimary),
),
content: TextField(
controller: controller,
autofocus: true,
style: TextStyle(color: theme.inputText),
decoration: InputDecoration(
hintText: AppLocalizations.of(context)!.folderName,
hintStyle: TextStyle(color: theme.inputPlaceholder),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: theme.inputBorder),
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: theme.buttonPrimary),
),
),
textInputAction: TextInputAction.done,
onSubmitted: (value) => Navigator.pop(dialogContext, value.trim()),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(AppLocalizations.of(context)!.cancel),
),
TextButton(
onPressed: () {
HapticFeedback.lightImpact();
Navigator.pop(dialogContext, controller.text.trim());
},
child: Text(AppLocalizations.of(context)!.save),
),
],
);
},
); );
if (newName == null) return; if (newName == null) return;
@@ -892,10 +825,10 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
String folderId, String folderId,
String folderName, String folderName,
) async { ) async {
final confirmed = await UiUtils.showConfirmationDialog( final confirmed = await ThemedDialogs.confirm(
context, context,
title: 'Delete Folder', title: AppLocalizations.of(context)!.deleteFolderTitle,
message: 'This folder and its assignment references will be removed.', message: AppLocalizations.of(context)!.deleteFolderMessage,
confirmText: AppLocalizations.of(context)!.delete, confirmText: AppLocalizations.of(context)!.delete,
isDestructive: true, isDestructive: true,
); );
@@ -912,7 +845,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
if (!mounted) return; if (!mounted) return;
UiUtils.showMessage( UiUtils.showMessage(
this.context, this.context,
'Failed to delete folder', AppLocalizations.of(context)!.failedToDeleteFolder,
isError: true, isError: true,
); );
} }
@@ -1470,50 +1403,13 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
String conversationId, String conversationId,
String currentTitle, String currentTitle,
) async { ) async {
final theme = context.conduitTheme; final newName = await ThemedDialogs.promptTextInput(
final controller = TextEditingController(text: currentTitle); context,
title: AppLocalizations.of(context)!.renameChat,
final newName = await showDialog<String>( hintText: AppLocalizations.of(context)!.enterChatName,
context: context, initialValue: currentTitle,
builder: (dialogContext) { confirmText: AppLocalizations.of(context)!.save,
return AlertDialog( cancelText: AppLocalizations.of(context)!.cancel,
backgroundColor: theme.surfaceBackground,
title: Text(
AppLocalizations.of(context)!.renameChat,
style: TextStyle(color: theme.textPrimary),
),
content: TextField(
controller: controller,
autofocus: true,
style: TextStyle(color: theme.inputText),
decoration: InputDecoration(
hintText: AppLocalizations.of(context)!.enterChatName,
hintStyle: TextStyle(color: theme.inputPlaceholder),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: theme.inputBorder),
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: theme.buttonPrimary),
),
),
textInputAction: TextInputAction.done,
onSubmitted: (value) => Navigator.pop(dialogContext, value.trim()),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(AppLocalizations.of(context)!.cancel),
),
TextButton(
onPressed: () {
HapticFeedback.lightImpact();
Navigator.pop(dialogContext, controller.text.trim());
},
child: Text(AppLocalizations.of(context)!.save),
),
],
);
},
); );
if (newName == null) return; if (newName == null) return;
@@ -1546,7 +1442,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
BuildContext context, BuildContext context,
String conversationId, String conversationId,
) async { ) async {
final confirmed = await UiUtils.showConfirmationDialog( final confirmed = await ThemedDialogs.confirm(
context, context,
title: AppLocalizations.of(context)!.deleteChatTitle, title: AppLocalizations.of(context)!.deleteChatTitle,
message: AppLocalizations.of(context)!.deleteChatMessage, message: AppLocalizations.of(context)!.deleteChatMessage,

View File

@@ -11,6 +11,7 @@ import '../../../core/widgets/error_boundary.dart';
import '../../../shared/widgets/improved_loading_states.dart'; import '../../../shared/widgets/improved_loading_states.dart';
import '../../../shared/utils/ui_utils.dart'; import '../../../shared/utils/ui_utils.dart';
import '../../../shared/widgets/themed_dialogs.dart';
import '../../../shared/widgets/sheet_handle.dart'; import '../../../shared/widgets/sheet_handle.dart';
import '../../../shared/widgets/conduit_components.dart'; import '../../../shared/widgets/conduit_components.dart';
import '../../../core/providers/app_providers.dart'; import '../../../core/providers/app_providers.dart';
@@ -574,7 +575,7 @@ class ProfilePage extends ConsumerWidget {
} }
void _signOut(BuildContext context, WidgetRef ref) async { void _signOut(BuildContext context, WidgetRef ref) async {
final confirm = await UiUtils.showConfirmationDialog( final confirm = await ThemedDialogs.confirm(
context, context,
title: AppLocalizations.of(context)!.signOut, title: AppLocalizations.of(context)!.signOut,
message: AppLocalizations.of(context)!.endYourSession, message: AppLocalizations.of(context)!.endYourSession,

View File

@@ -199,6 +199,9 @@
}, },
"deleteChatTitle": "Chat löschen", "deleteChatTitle": "Chat löschen",
"deleteChatMessage": "Dieser Chat wird dauerhaft gelöscht.", "deleteChatMessage": "Dieser Chat wird dauerhaft gelöscht.",
"deleteFolderTitle": "Ordner löschen",
"deleteFolderMessage": "Dieser Ordner und seine Zuordnungen werden entfernt.",
"failedToDeleteFolder": "Ordner konnte nicht gelöscht werden",
"aboutApp": "Über die App", "aboutApp": "Über die App",
"aboutAppSubtitle": "Conduit Informationen und Links", "aboutAppSubtitle": "Conduit Informationen und Links",
"typeBelowToBegin": "Unten tippen, um zu beginnen", "typeBelowToBegin": "Unten tippen, um zu beginnen",

View File

@@ -405,6 +405,12 @@
"@deleteChatTitle": {"description": "Dialog title asking to confirm deletion of a chat."}, "@deleteChatTitle": {"description": "Dialog title asking to confirm deletion of a chat."},
"deleteChatMessage": "This chat will be permanently deleted.", "deleteChatMessage": "This chat will be permanently deleted.",
"@deleteChatMessage": {"description": "Warning that deleting a chat cannot be undone."}, "@deleteChatMessage": {"description": "Warning that deleting a chat cannot be undone."},
"deleteFolderTitle": "Delete Folder",
"@deleteFolderTitle": {"description": "Dialog title asking to confirm deletion of a folder."},
"deleteFolderMessage": "This folder and its assignment references will be removed.",
"@deleteFolderMessage": {"description": "Warning that deleting a folder will remove it and its associations."},
"failedToDeleteFolder": "Failed to delete folder",
"@failedToDeleteFolder": {"description": "Error notice when deleting a folder fails."},
"aboutApp": "About App", "aboutApp": "About App",
"@aboutApp": {"description": "Settings tile title to view app information."}, "@aboutApp": {"description": "Settings tile title to view app information."},
"aboutAppSubtitle": "Conduit information and links", "aboutAppSubtitle": "Conduit information and links",

View File

@@ -199,7 +199,10 @@
}, },
"deleteChatTitle": "Supprimer le chat", "deleteChatTitle": "Supprimer le chat",
"deleteChatMessage": "Ce chat sera supprimé définitivement.", "deleteChatMessage": "Ce chat sera supprimé définitivement.",
"aboutApp": "À propos de l'application", "deleteFolderTitle": "Supprimer le dossier",
"deleteFolderMessage": "Ce dossier et ses associations seront supprimés.",
"failedToDeleteFolder": "Échec de la suppression du dossier",
"aboutApp": "À propos de lapplication",
"aboutAppSubtitle": "Informations et liens Conduit", "aboutAppSubtitle": "Informations et liens Conduit",
"typeBelowToBegin": "Saisissez cidessous pour commencer", "typeBelowToBegin": "Saisissez cidessous pour commencer",
"web": "Web", "web": "Web",

View File

@@ -199,7 +199,10 @@
}, },
"deleteChatTitle": "Elimina chat", "deleteChatTitle": "Elimina chat",
"deleteChatMessage": "Questa chat verrà eliminata definitivamente.", "deleteChatMessage": "Questa chat verrà eliminata definitivamente.",
"aboutApp": "Informazioni sull'app", "deleteFolderTitle": "Elimina cartella",
"deleteFolderMessage": "Questa cartella e le sue associazioni verranno rimosse.",
"failedToDeleteFolder": "Impossibile eliminare la cartella",
"aboutApp": "Informazioni sullapp",
"aboutAppSubtitle": "Informazioni e link di Conduit", "aboutAppSubtitle": "Informazioni e link di Conduit",
"typeBelowToBegin": "Scrivi qui sotto per iniziare", "typeBelowToBegin": "Scrivi qui sotto per iniziare",
"web": "Web", "web": "Web",

View File

@@ -1182,6 +1182,24 @@ abstract class AppLocalizations {
/// **'This chat will be permanently deleted.'** /// **'This chat will be permanently deleted.'**
String get deleteChatMessage; String get deleteChatMessage;
/// Dialog title asking to confirm deletion of a folder.
///
/// In en, this message translates to:
/// **'Delete Folder'**
String get deleteFolderTitle;
/// Warning that deleting a folder will remove it and its associations.
///
/// In en, this message translates to:
/// **'This folder and its assignment references will be removed.'**
String get deleteFolderMessage;
/// Error notice when deleting a folder fails.
///
/// In en, this message translates to:
/// **'Failed to delete folder'**
String get failedToDeleteFolder;
/// Settings tile title to view app information. /// Settings tile title to view app information.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View File

@@ -600,6 +600,16 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get deleteChatMessage => 'Dieser Chat wird dauerhaft gelöscht.'; String get deleteChatMessage => 'Dieser Chat wird dauerhaft gelöscht.';
@override
String get deleteFolderTitle => 'Ordner löschen';
@override
String get deleteFolderMessage =>
'Dieser Ordner und seine Zuordnungen werden entfernt.';
@override
String get failedToDeleteFolder => 'Ordner konnte nicht gelöscht werden';
@override @override
String get aboutApp => 'Über die App'; String get aboutApp => 'Über die App';

View File

@@ -595,6 +595,16 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get deleteChatMessage => 'This chat will be permanently deleted.'; String get deleteChatMessage => 'This chat will be permanently deleted.';
@override
String get deleteFolderTitle => 'Delete Folder';
@override
String get deleteFolderMessage =>
'This folder and its assignment references will be removed.';
@override
String get failedToDeleteFolder => 'Failed to delete folder';
@override @override
String get aboutApp => 'About App'; String get aboutApp => 'About App';

View File

@@ -605,6 +605,17 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get deleteChatMessage => 'Ce chat sera supprimé définitivement.'; String get deleteChatMessage => 'Ce chat sera supprimé définitivement.';
@override
String get deleteFolderTitle => 'Supprimer le dossier';
@override
String get deleteFolderMessage =>
'Ce dossier et ses associations seront supprimés.';
@override
String get failedToDeleteFolder =>
'Échec de la suppression du dossier';
@override @override
String get aboutApp => 'À propos de l\'application'; String get aboutApp => 'À propos de l\'application';

View File

@@ -598,6 +598,17 @@ class AppLocalizationsIt extends AppLocalizations {
String get deleteChatMessage => String get deleteChatMessage =>
'Questa chat verrà eliminata definitivamente.'; 'Questa chat verrà eliminata definitivamente.';
@override
String get deleteFolderTitle => 'Elimina cartella';
@override
String get deleteFolderMessage =>
'Questa cartella e le sue associazioni verranno rimosse.';
@override
String get failedToDeleteFolder =>
'Impossibile eliminare la cartella';
@override @override
String get aboutApp => 'Informazioni sull\'app'; String get aboutApp => 'Informazioni sull\'app';

View File

@@ -115,54 +115,7 @@ class UiUtils {
); );
} }
/// Shows a Conduit-styled confirmation dialog // Confirmation dialog moved to shared ThemedDialogs.confirm for cohesion
static Future<bool> showConfirmationDialog(
BuildContext context, {
required String title,
required String message,
String confirmText = 'Confirm',
String cancelText = 'Cancel',
bool isDestructive = false,
}) async {
return await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
backgroundColor: context.conduitTheme.surfaceBackground,
title: Text(
title,
style: TextStyle(color: context.conduitTheme.textPrimary),
),
content: Text(
message,
style: TextStyle(color: context.conduitTheme.textSecondary),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text(
cancelText.isNotEmpty
? cancelText
: AppLocalizations.of(context)!.cancel,
),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
style: isDestructive
? TextButton.styleFrom(
foregroundColor: context.conduitTheme.error,
)
: null,
child: Text(
confirmText.isNotEmpty
? confirmText
: AppLocalizations.of(context)!.confirm,
),
),
],
),
) ??
false;
}
/// Formats dates in a conversational way following Conduit patterns /// Formats dates in a conversational way following Conduit patterns
static String formatDate(DateTime date) { static String formatDate(DateTime date) {

View File

@@ -96,4 +96,108 @@ class ThemedDialogs {
), ),
); );
} }
/// Cohesive text input dialog used for rename/create flows
static Future<String?> promptTextInput(
BuildContext context, {
required String title,
required String hintText,
String? initialValue,
String confirmText = 'Save',
String cancelText = 'Cancel',
bool barrierDismissible = true,
TextInputType? keyboardType,
TextCapitalization textCapitalization = TextCapitalization.sentences,
int? maxLength,
}) async {
final theme = context.conduitTheme;
final controller = TextEditingController(text: initialValue ?? '');
String? result = await showDialog<String>(
context: context,
barrierDismissible: barrierDismissible,
builder: (ctx) {
return buildBase(
context: ctx,
title: title,
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: controller,
autofocus: true,
keyboardType: keyboardType,
textCapitalization: textCapitalization,
maxLength: maxLength,
style: TextStyle(color: theme.inputText),
decoration: InputDecoration(
hintText: hintText,
hintStyle: TextStyle(color: theme.inputPlaceholder),
filled: true,
fillColor: theme.inputBackground,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.md),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.md),
borderSide: BorderSide(color: theme.inputBorder, width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.md),
borderSide:
BorderSide(color: theme.buttonPrimary, width: 1),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: Spacing.md,
vertical: Spacing.md,
),
),
onSubmitted: (v) {
final trimmed = v.trim();
final unchanged = (initialValue != null &&
trimmed == initialValue.trim());
if (trimmed.isEmpty || unchanged) return;
Navigator.of(ctx).pop(trimmed);
},
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: Text(
cancelText,
style: TextStyle(color: theme.textSecondary),
),
),
ValueListenableBuilder<TextEditingValue>(
valueListenable: controller,
builder: (context, value, _) {
final trimmed = value.text.trim();
final unchanged =
(initialValue != null && trimmed == initialValue.trim());
final enabled = trimmed.isNotEmpty && !unchanged;
return TextButton(
onPressed: enabled
? () => Navigator.of(ctx).pop(trimmed)
: null,
child: Text(
confirmText,
style: TextStyle(
color: enabled
? theme.buttonPrimary
: theme.textSecondary,
),
),
);
},
),
],
);
},
);
return result;
}
} }