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 '../../profile/views/profile_page.dart';
import '../../../shared/utils/ui_utils.dart';
import '../../../shared/widgets/themed_dialogs.dart';
import '../../../core/auth/auth_state_manager.dart';
import 'package:conduit/l10n/app_localizations.dart';
import '../../../core/models/user.dart' as models;
@@ -580,43 +581,12 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
}
Future<void> _promptCreateFolder() async {
final theme = context.conduitTheme;
final controller = TextEditingController();
final name = await showDialog<String>(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: theme.surfaceBackground,
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),
),
],
),
final name = await ThemedDialogs.promptTextInput(
context,
title: AppLocalizations.of(context)!.newFolder,
hintText: AppLocalizations.of(context)!.folderName,
confirmText: AppLocalizations.of(context)!.create,
cancelText: AppLocalizations.of(context)!.cancel,
);
if (name == null) return;
@@ -822,50 +792,13 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
String folderId,
String currentName,
) async {
final theme = context.conduitTheme;
final controller = TextEditingController(text: currentName);
final newName = await showDialog<String>(
context: context,
builder: (dialogContext) {
return AlertDialog(
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),
),
],
);
},
final newName = await ThemedDialogs.promptTextInput(
context,
title: AppLocalizations.of(context)!.rename,
hintText: AppLocalizations.of(context)!.folderName,
initialValue: currentName,
confirmText: AppLocalizations.of(context)!.save,
cancelText: AppLocalizations.of(context)!.cancel,
);
if (newName == null) return;
@@ -892,10 +825,10 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
String folderId,
String folderName,
) async {
final confirmed = await UiUtils.showConfirmationDialog(
final confirmed = await ThemedDialogs.confirm(
context,
title: 'Delete Folder',
message: 'This folder and its assignment references will be removed.',
title: AppLocalizations.of(context)!.deleteFolderTitle,
message: AppLocalizations.of(context)!.deleteFolderMessage,
confirmText: AppLocalizations.of(context)!.delete,
isDestructive: true,
);
@@ -912,7 +845,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
if (!mounted) return;
UiUtils.showMessage(
this.context,
'Failed to delete folder',
AppLocalizations.of(context)!.failedToDeleteFolder,
isError: true,
);
}
@@ -1470,50 +1403,13 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
String conversationId,
String currentTitle,
) async {
final theme = context.conduitTheme;
final controller = TextEditingController(text: currentTitle);
final newName = await showDialog<String>(
context: context,
builder: (dialogContext) {
return AlertDialog(
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),
),
],
);
},
final newName = await ThemedDialogs.promptTextInput(
context,
title: AppLocalizations.of(context)!.renameChat,
hintText: AppLocalizations.of(context)!.enterChatName,
initialValue: currentTitle,
confirmText: AppLocalizations.of(context)!.save,
cancelText: AppLocalizations.of(context)!.cancel,
);
if (newName == null) return;
@@ -1546,7 +1442,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
BuildContext context,
String conversationId,
) async {
final confirmed = await UiUtils.showConfirmationDialog(
final confirmed = await ThemedDialogs.confirm(
context,
title: AppLocalizations.of(context)!.deleteChatTitle,
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/utils/ui_utils.dart';
import '../../../shared/widgets/themed_dialogs.dart';
import '../../../shared/widgets/sheet_handle.dart';
import '../../../shared/widgets/conduit_components.dart';
import '../../../core/providers/app_providers.dart';
@@ -574,7 +575,7 @@ class ProfilePage extends ConsumerWidget {
}
void _signOut(BuildContext context, WidgetRef ref) async {
final confirm = await UiUtils.showConfirmationDialog(
final confirm = await ThemedDialogs.confirm(
context,
title: AppLocalizations.of(context)!.signOut,
message: AppLocalizations.of(context)!.endYourSession,

View File

@@ -199,6 +199,9 @@
},
"deleteChatTitle": "Chat löschen",
"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",
"aboutAppSubtitle": "Conduit Informationen und Links",
"typeBelowToBegin": "Unten tippen, um zu beginnen",

View File

@@ -405,6 +405,12 @@
"@deleteChatTitle": {"description": "Dialog title asking to confirm deletion of a chat."},
"deleteChatMessage": "This chat will be permanently deleted.",
"@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": {"description": "Settings tile title to view app information."},
"aboutAppSubtitle": "Conduit information and links",

View File

@@ -199,7 +199,10 @@
},
"deleteChatTitle": "Supprimer le chat",
"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",
"typeBelowToBegin": "Saisissez cidessous pour commencer",
"web": "Web",

View File

@@ -199,7 +199,10 @@
},
"deleteChatTitle": "Elimina chat",
"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",
"typeBelowToBegin": "Scrivi qui sotto per iniziare",
"web": "Web",

View File

@@ -1182,6 +1182,24 @@ abstract class AppLocalizations {
/// **'This chat will be permanently deleted.'**
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.
///
/// In en, this message translates to:

View File

@@ -600,6 +600,16 @@ class AppLocalizationsDe extends AppLocalizations {
@override
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
String get aboutApp => 'Über die App';

View File

@@ -595,6 +595,16 @@ class AppLocalizationsEn extends AppLocalizations {
@override
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
String get aboutApp => 'About App';

View File

@@ -605,6 +605,17 @@ class AppLocalizationsFr extends AppLocalizations {
@override
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
String get aboutApp => 'À propos de l\'application';

View File

@@ -598,6 +598,17 @@ class AppLocalizationsIt extends AppLocalizations {
String get deleteChatMessage =>
'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
String get aboutApp => 'Informazioni sull\'app';

View File

@@ -115,54 +115,7 @@ class UiUtils {
);
}
/// Shows a Conduit-styled confirmation dialog
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;
}
// Confirmation dialog moved to shared ThemedDialogs.confirm for cohesion
/// Formats dates in a conversational way following Conduit patterns
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;
}
}