From 1a71a77cd1e483a3330cc79a17a79df4e3fab174 Mon Sep 17 00:00:00 2001 From: Rune Berg Date: Wed, 21 Aug 2024 09:53:37 +1200 Subject: [PATCH] Quality of life changes - Prompt user if they are about to clear message history without saving - Prompt user if they are about to exit the app (desktop) and there are unsaved messages in the chat - Context sensitive prompting of unsaved changes when user resets history or moves across models - Allow user to set a default AI Provider on app startup - Harmonize all popup windows to use the same title style - Set dialog messages to be more readable across devices --- confichat/analysis_options.yaml | 3 + confichat/lib/app_data.dart | 5 +- confichat/lib/main.dart | 87 ++++++++- confichat/lib/ui_advanced_options.dart | 4 +- confichat/lib/ui_app_bar.dart | 32 ++-- confichat/lib/ui_app_settings.dart | 253 +++++++++++++++---------- confichat/lib/ui_canvass.dart | 76 ++++++-- confichat/lib/ui_save_session.dart | 5 +- confichat/lib/ui_sidebar.dart | 4 +- confichat/lib/ui_widgets.dart | 6 +- 10 files changed, 331 insertions(+), 144 deletions(-) diff --git a/confichat/analysis_options.yaml b/confichat/analysis_options.yaml index d4e0f0c..c3130fd 100644 --- a/confichat/analysis_options.yaml +++ b/confichat/analysis_options.yaml @@ -7,6 +7,9 @@ # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. +analyzer: + errors: + unused_field: ignore include: package:flutter_lints/flutter.yaml linter: diff --git a/confichat/lib/app_data.dart b/confichat/lib/app_data.dart index 0beb4b2..500aea5 100644 --- a/confichat/lib/app_data.dart +++ b/confichat/lib/app_data.dart @@ -6,6 +6,7 @@ import 'dart:io'; +import 'package:confichat/ui_widgets.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -42,9 +43,11 @@ class AppData { LlmApi api = LlmApiFactory.create(AiProvider.ollama.name); bool clearMessagesOnModelSwitch = true; bool filterHistoryByModel = false; + bool haveUnsavedMessages = false; int appScrollDurationInms = 100; double windowWidth = 1024; double windowHeight = 1024; + AiProvider defaultProvider = AiProvider.ollama; String rootPath = ''; void defaultCallback(AiProvider? provider) { @@ -161,7 +164,7 @@ class ShowErrorDialog extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( - title: Text(title), + title: DialogTitle(title: title), content: Text(content), actions: [ TextButton( diff --git a/confichat/lib/main.dart b/confichat/lib/main.dart index b8b67aa..b5df1cc 100644 --- a/confichat/lib/main.dart +++ b/confichat/lib/main.dart @@ -4,8 +4,12 @@ * SPDX-License-Identifier: Apache-2.0 */ +import 'dart:ui'; + +import 'package:confichat/ui_widgets.dart'; import 'package:flutter/material.dart'; import 'package:confichat/themes.dart'; +import 'package:flutter/scheduler.dart'; import 'package:provider/provider.dart'; import 'package:path_provider/path_provider.dart'; import 'package:desktop_window/desktop_window.dart'; @@ -60,9 +64,33 @@ class ConfiChat extends StatelessWidget { // Set theme if (context.mounted) { final themeProvider = Provider.of(context, listen: false); - final selectedTheme = jsonContent['app']['selectedTheme'] ?? 'Light'; + final selectedTheme = jsonContent['app']['selectedTheme'] ?? 'Onyx'; themeProvider.setTheme(selectedTheme); } + + // Set default provider + if(context.mounted){ + final defaultProvider = jsonContent['app']['selectedDefaultProvider'] ?? 'Ollama'; + + AiProvider selectedProvider; + switch (defaultProvider.toLowerCase()) { + case 'ollama': + selectedProvider = AiProvider.ollama; + break; + case 'openai': + selectedProvider = AiProvider.openai; + break; + case 'llamacpp': + selectedProvider = AiProvider.llamacpp; + break; + default: + selectedProvider = AiProvider.ollama; // Fallback to Ollama if the string doesn't match + break; + } + + AppData.instance.defaultProvider = selectedProvider; + } + } } } @@ -117,7 +145,7 @@ class HomePage extends StatefulWidget { State createState() => _HomePageState(); } -class _HomePageState extends State { +class _HomePageState extends State { final ChatSessionSelectedNotifier chatSessionSelectedNotifier = ChatSessionSelectedNotifier(); TextEditingController providerController = TextEditingController(); AiProvider? selectedProvider; @@ -125,13 +153,60 @@ class _HomePageState extends State { TextEditingController providerModel = TextEditingController(); ModelItem? selectedModel; + late final AppLifecycleListener _lifecycleListener; + late AppLifecycleState? _lifecycleState; + @override void initState() { - super.initState(); + super.initState(); - if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { - DesktopWindow.setWindowSize(Size(widget.appData.windowWidth, widget.appData.windowHeight)); - } + if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { + DesktopWindow.setWindowSize(Size(widget.appData.windowWidth, widget.appData.windowHeight)); + } + + _lifecycleState = SchedulerBinding.instance.lifecycleState; + _lifecycleListener = AppLifecycleListener( + onExitRequested: _checkForUnsavedChat + ); + } + + Future _checkForUnsavedChat() async { + + // If there are no unsaved changes, proceed to exit + if(!widget.appData.haveUnsavedMessages) { return AppExitResponse.exit;} + + bool shouldExit = false; + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const DialogTitle(title: 'Warning', isError: true), + content: Text( + 'There are unsaved messages in the current chat window - they will be lost. Proceed?', + style: Theme.of(context).textTheme.bodyLarge, + ), + actions: [ + ElevatedButton( + child: const Text('Yes'), + onPressed: () { + shouldExit = true; + Navigator.of(context).pop(); + }, + ), + ElevatedButton( + child: const Text('Cancel'), + onPressed: () { + shouldExit = false; + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + + return shouldExit ? AppExitResponse.exit : AppExitResponse.cancel; + } @override diff --git a/confichat/lib/ui_advanced_options.dart b/confichat/lib/ui_advanced_options.dart index 0deaf51..062fc69 100644 --- a/confichat/lib/ui_advanced_options.dart +++ b/confichat/lib/ui_advanced_options.dart @@ -274,8 +274,8 @@ class AdvancedOptionsState extends State { context: context, builder: (BuildContext context) { return AlertDialog( - title: const Text('Unsaved changes'), - content: const Text('You have unsaved changes to the advanced options. Are you sure you want to exit?'), + title: const DialogTitle(title: 'Unsaved changes', isError: true), + content: Text('You have unsaved changes to the advanced options. Are you sure you want to exit?', style: Theme.of(context).textTheme.bodyLarge,), actions: [ ElevatedButton( onPressed: () { diff --git a/confichat/lib/ui_app_bar.dart b/confichat/lib/ui_app_bar.dart index ecedd7b..9b9be06 100644 --- a/confichat/lib/ui_app_bar.dart +++ b/confichat/lib/ui_app_bar.dart @@ -39,7 +39,7 @@ class CCAppBarState extends State { @override void initState() { super.initState(); - _switchProvider(AiProvider.ollama); + _switchProvider(widget.appData.defaultProvider); _populateModelList(true); widget.appData.callbackSwitchProvider = _switchProvider; @@ -78,7 +78,7 @@ class CCAppBarState extends State { }, ), actions: [ - _buildModelProviderDropdown(context, isPhone), + _buildModelProviderDropdown(context, isPhone, selectedProvider ?? AiProvider.ollama), _buildModelDropdown(context, isPhone), if (!isPhone) _buildConfigButton(context), if (!isPhone) _buildAddButton(context), @@ -88,11 +88,11 @@ class CCAppBarState extends State { ); } - Widget _buildModelProviderDropdown(BuildContext context, bool isPhone) { + Widget _buildModelProviderDropdown(BuildContext context, bool isPhone, AiProvider selectedProvider) { return Container( margin: const EdgeInsets.all(10), child: DropdownMenu( - initialSelection: AiProvider.ollama, + initialSelection: selectedProvider, controller: widget.providerController, requestFocusOnTap: true, textStyle: TextStyle( @@ -292,19 +292,27 @@ class CCAppBarState extends State { } } - void _showModelChangeWarning(BuildContext context, ModelItem newModel) { - showDialog( + Future _showModelChangeWarning(BuildContext context, ModelItem newModel) async { + + if(!widget.appData.haveUnsavedMessages) { + _setModelItem(newModel); + return; + } + + await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( - title: const Text('Warning'), - content: const Text( - 'Any messages in the current chat window will be lost. Proceed?', + title: const DialogTitle(title: 'Warning', isError: true), + content: Text( + 'There are unsaved messages in the current chat window - they will be lost. Proceed?', + style: Theme.of(context).textTheme.bodyLarge, ), - actions: [ + actions: [ ElevatedButton( child: const Text('Yes'), onPressed: () { + widget.appData.haveUnsavedMessages = false; _setModelItem(newModel); Navigator.of(context).pop(); }, @@ -312,6 +320,7 @@ class CCAppBarState extends State { ElevatedButton( child: const Text('Cancel'), onPressed: () { + if(mounted && selectedModel != null) {_setModelItem(selectedModel!);} Navigator.of(context).pop(); }, ), @@ -339,9 +348,8 @@ class CCAppBarState extends State { if(mounted) { setState(() { selectedProvider = provider; + _populateModelList(true); }); - - _populateModelList(true); } } diff --git a/confichat/lib/ui_app_settings.dart b/confichat/lib/ui_app_settings.dart index 425fb88..00b8fc0 100644 --- a/confichat/lib/ui_app_settings.dart +++ b/confichat/lib/ui_app_settings.dart @@ -37,6 +37,7 @@ class AppSettingsState extends State { late double windowHeight; late bool hasChanges; late String selectedTheme; + late String selectedDefaultProvider; late String rootPath; @override @@ -66,7 +67,8 @@ class AppSettingsState extends State { scrollDuration = 100; windowWidth = 1024; windowHeight = 1024; - selectedTheme = 'Sapphire'; + selectedTheme = 'Onyx'; + selectedDefaultProvider = 'Ollama'; rootPath = ''; _scrollDuration.text = scrollDuration.toString(); @@ -90,10 +92,13 @@ class AppSettingsState extends State { scrollDuration = jsonContent['app']['appScrollDurationInms'] ?? widget.appData.appScrollDurationInms; windowWidth = jsonContent['app']['windowWidth'] ?? widget.appData.windowWidth; windowHeight = jsonContent['app']['windowHeight'] ?? widget.appData.windowHeight; - selectedTheme = jsonContent['app']['selectedTheme'] ?? 'Light'; // Load the saved theme + selectedTheme = jsonContent['app']['selectedTheme'] ?? 'Onyx'; + selectedDefaultProvider = jsonContent['app']['selectedDefaultProvider'] ?? 'Ollama'; }); - // ignore: use_build_context_synchronously - Provider.of(context, listen: false).setTheme(selectedTheme); // Apply the loaded theme + + if(widget.appData.navigatorKey.currentContext != null){ + Provider.of(widget.appData.navigatorKey.currentContext!, listen: false).setTheme(selectedTheme); + } } } @@ -134,7 +139,8 @@ class AppSettingsState extends State { 'appScrollDurationInms': scrollDuration, 'windowWidth': windowWidth, 'windowHeight': windowHeight, - 'selectedTheme': selectedTheme, // Save the selected theme + 'selectedTheme': selectedTheme, + 'selectedDefaultProvider': selectedDefaultProvider, }; await file.writeAsString(const JsonEncoder.withIndent(' ').convert(content)); @@ -174,16 +180,9 @@ class AppSettingsState extends State { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Unsaved Changes'), - content: const Text('You have unsaved changes. Do you want to save them?'), + title: const DialogTitle(title:'Unsaved Changes', isError: true), + content: Text('You have unsaved changes. Do you want to save them?', style: Theme.of(context).textTheme.bodyLarge,), actions: [ - ElevatedButton( - onPressed: () { - _saveSettings(); - Navigator.of(context).pop(); - }, - child: const Text('Save'), - ), ElevatedButton( onPressed: () { Navigator.of(context).pop(); @@ -221,105 +220,149 @@ class AppSettingsState extends State { const DialogTitle(title: 'Application Settings'), const SizedBox(height: 18), - // Clear messages - SwitchListTile( - title: const Text('Clear messages when switching Models'), - activeColor: Theme.of(context).colorScheme.secondary, - contentPadding: EdgeInsets.zero, - value: clearMessages, - onChanged: (value) { - setState(() { - clearMessages = value; - }); - }, - ), - - // Theme switcher - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Text('Theme', style: TextStyle(fontSize: 16)), - const SizedBox(width: 8), - MenuAnchor( - builder: (BuildContext context, MenuController controller, Widget? child) { - return TextButton( - onPressed: controller.open, - child: Row( - children: [ - Text(themeProvider.currentThemeName), - const Icon(Icons.arrow_drop_down), - ], - ), - ); + SingleChildScrollView( + scrollDirection: Axis.vertical, child: Column ( children: [ + + // Clear messages + SwitchListTile( + title: const Text('Clear messages when switching Models'), + activeColor: Theme.of(context).colorScheme.secondary, + contentPadding: EdgeInsets.zero, + value: clearMessages, + onChanged: (value) { + setState(() { + clearMessages = value; + }); }, - menuChildren: themeProvider.themes.keys.map((String themeName) { - return MenuItemButton( - onPressed: () { - themeProvider.setTheme(themeName); - setState(() { - selectedTheme = themeName; // Update selected theme - }); + ), + + // Theme switcher + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text('Theme', style: TextStyle(fontSize: 16)), + const SizedBox(width: 8), + MenuAnchor( + builder: (BuildContext context, MenuController controller, Widget? child) { + return TextButton( + onPressed: controller.open, + child: Row( + children: [ + Text(themeProvider.currentThemeName), + const Icon(Icons.arrow_drop_down), + ], + ), + ); }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), - child: Text(themeName), - ), - ); - }).toList(), + menuChildren: themeProvider.themes.keys.map((String themeName) { + return MenuItemButton( + onPressed: () { + themeProvider.setTheme(themeName); + setState(() { + selectedTheme = themeName; // Update selected theme + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + child: Text(themeName), + ), + ); + }).toList(), + ), + ], ), - ], - ), - // Scroll duration - const SizedBox(height: 8), - TextField( - controller: _scrollDuration, - decoration: InputDecoration(labelText: 'Auto-scroll duration (ms)', labelStyle: Theme.of(context).textTheme.labelSmall), - keyboardType: TextInputType.number, - onChanged: (value) { - setState(() { - scrollDuration = int.tryParse(value) ?? widget.appData.appScrollDurationInms; - }); - }, - ), + // Default AI Provider switcher + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text('Default provider', style: TextStyle(fontSize: 16)), + const SizedBox(width: 8), + MenuAnchor( + builder: (BuildContext context, MenuController controller, Widget? child) { + return TextButton( + onPressed: controller.open, + child: Row( + children: [ + Text(selectedDefaultProvider), + const Icon(Icons.arrow_drop_down), + ], + ), + ); + }, + menuChildren: AiProvider.values.map((AiProvider provider) { + return MenuItemButton( + onPressed: () { + setState(() { selectedDefaultProvider = provider.name; }); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + child: Text(provider.name), + ), + ); + }).toList(), + ), + ], + ), - // Window width - TextField( - controller: _windowWidth, - decoration: InputDecoration(labelText: 'Default window width', labelStyle: Theme.of(context).textTheme.labelSmall), - keyboardType: const TextInputType.numberWithOptions(decimal: true), - onChanged: (value) { - setState(() { - windowWidth = double.tryParse(value) ?? widget.appData.windowWidth; - }); - }, - ), + // Scroll duration + const SizedBox(height: 8), + TextField( + controller: _scrollDuration, + decoration: InputDecoration(labelText: 'Auto-scroll duration (ms)', labelStyle: Theme.of(context).textTheme.labelSmall), + keyboardType: TextInputType.number, + onChanged: (value) { + setState(() { + scrollDuration = int.tryParse(value) ?? widget.appData.appScrollDurationInms; + }); + }, + ), - // Window height - TextField( - controller: _windowHeight, - decoration: InputDecoration(labelText: 'Default window height', labelStyle: Theme.of(context).textTheme.labelSmall), - keyboardType: const TextInputType.numberWithOptions(decimal: true), - onChanged: (value) { - setState(() { - windowHeight = double.tryParse(value) ?? widget.appData.windowHeight; - }); - }, - ), + // Window width + TextField( + controller: _windowWidth, + decoration: InputDecoration(labelText: 'Default window width', labelStyle: Theme.of(context).textTheme.labelSmall), + keyboardType: const TextInputType.numberWithOptions(decimal: true), + onChanged: (value) { + setState(() { + windowWidth = double.tryParse(value) ?? widget.appData.windowWidth; + }); + }, + ), - // Default app root path - TextField( - decoration: InputDecoration( - labelText: 'Override app data path (leave blank for system default)', - labelStyle: Theme.of(context).textTheme.labelSmall, + // Window height + TextField( + controller: _windowHeight, + decoration: InputDecoration(labelText: 'Default window height', labelStyle: Theme.of(context).textTheme.labelSmall), + keyboardType: const TextInputType.numberWithOptions(decimal: true), + onChanged: (value) { + setState(() { + windowHeight = double.tryParse(value) ?? widget.appData.windowHeight; + }); + }, ), - readOnly: true, - controller: _rootpath, - ), - const SizedBox(height: 16), - TextButton( - onPressed: _pickDirectory, - child: const Text('Pick Directory'), + + // Default app root path + TextField( + decoration: InputDecoration( + labelText: 'Override app data path (leave blank for system default)', + labelStyle: Theme.of(context).textTheme.labelSmall, + ), + readOnly: true, + controller: _rootpath, + ), + const SizedBox(height: 16), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextButton( + onPressed: _pickDirectory, + child: const Text('Pick Directory'), + ), + ] + ), + ] + ) ), // Buttons diff --git a/confichat/lib/ui_canvass.dart b/confichat/lib/ui_canvass.dart index abbfe77..a5580e4 100644 --- a/confichat/lib/ui_canvass.dart +++ b/confichat/lib/ui_canvass.dart @@ -350,7 +350,7 @@ class CanvassState extends State { final selectedModel = selectedModelProvider.selectedModel; if(selectedModel!.name.isEmpty) { - setState(() { _resetHistory(); }); + setState(() { _resetHistory(true); }); return false; } @@ -362,7 +362,7 @@ class CanvassState extends State { // Set chat history to cached messages if (messages.isNotEmpty) { setState(() { - _resetHistory(); + _resetHistory(true); for(var msg in messages){ chatData.add(msg); _scrollToBottom(); @@ -419,14 +419,14 @@ class CanvassState extends State { context: context, builder: (BuildContext context) { return AlertDialog( - title: const DialogTitle(title: 'Confirm Session Reset'), - content: const Text('Are you sure you want to clear all chat messages?'), + title: const DialogTitle(title: 'Confirm Session Reset', isError: true), + content: Text('Are you sure you want to clear all chat messages?', style: Theme.of(context).textTheme.bodyLarge,), actions: [ ElevatedButton( child: const Text('Clear'), onPressed: () { Navigator.of(context).pop(); - _resetHistory(); + _resetHistory(true); setState(() { _promptController.text = ''; }); @@ -460,7 +460,52 @@ class CanvassState extends State { // Clear messages - void _resetHistory() { + Future _resetHistory([bool force = false]) async { + + bool shouldCancel = false; + + if(!force && widget.appData.haveUnsavedMessages) + { + if(widget.appData.haveUnsavedMessages){ + + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const DialogTitle(title: 'Warning', isError: true), + content: Text( + 'There are unsaved messages in the current chat window - they will be lost. Proceed?', + style: Theme.of(context).textTheme.bodyLarge, + ), + actions: [ + + ElevatedButton( + child: const Text('Yes'), + onPressed: () { + shouldCancel = false; + Navigator.of(context).pop(); + }, + ), + + ElevatedButton( + child: const Text('Cancel'), + onPressed: () { + shouldCancel = true; + Navigator.of(context).pop(); + }, + ), + + ], + ); + }, + ); + + } + } + + // Cancel if user intervened + if(shouldCancel) { return; } + setState(() { base64Images.clear(); documents.clear(); @@ -471,6 +516,9 @@ class CanvassState extends State { chatData.clear(); }); + // Untag unsaved changes + widget.appData.haveUnsavedMessages = false; + // Reset prompt options widget.appData.api.temperature = widget.appData.api.defaultTemperature; widget.appData.api.probability = widget.appData.api.defaultProbability; @@ -499,15 +547,16 @@ class CanvassState extends State { ) ), - child: OutlinedText( - textData: 'Load Chat Session', - textStyle: Theme.of(context).textTheme.titleMedium, + child: const DialogTitle( + title: 'Load Chat Session', + isError: true, ) ), content: Text( - 'Are you sure you want to load chat session: $selectedChatSession?\n' + 'Are you sure you want to load chat session: \n\n$selectedChatSession?\n\n' 'This will clear all current messages and if unsaved, will be lost.', + style: Theme.of(context).textTheme.bodyMedium, ), actions: [ ElevatedButton( @@ -615,6 +664,9 @@ class CanvassState extends State { if(codeFiles.isNotEmpty) { chatCodeFiles[chatData.length - 1] = codeFiles.keys.toList(); } }); + // Tag unsaved messages for warning + widget.appData.haveUnsavedMessages = true; + // Scroll to bottom _scrollToBottom(); @@ -954,11 +1006,11 @@ class DecryptDialog { builder: (BuildContext context) { return AlertDialog( - title: const Text('Encrypted Content'), + title: const DialogTitle(title: 'Encrypted Content'), content: Column( mainAxisSize: MainAxisSize.min, children: [ - const Text('This chat contains encrypted content. Please provide the key to decrypt.'), + Text('This chat contains encrypted content. Please provide the key to decrypt.', style: Theme.of(context).textTheme.bodyLarge,), TextField( controller: keyController, decoration: const InputDecoration( diff --git a/confichat/lib/ui_save_session.dart b/confichat/lib/ui_save_session.dart index d3a57d6..d228901 100644 --- a/confichat/lib/ui_save_session.dart +++ b/confichat/lib/ui_save_session.dart @@ -65,7 +65,7 @@ class SaveChatSessionState extends State { context: context, builder: (BuildContext context) { return AlertDialog( - title: Text(success ? 'Success' : 'Fail'), + title: DialogTitle(title: success ? 'Success' : 'Fail', isError: !success,), content: SelectableText(message), actions: [ ElevatedButton( @@ -168,11 +168,12 @@ class SaveChatSessionState extends State { // Success String folder = PersistentStorage.cleanupModelName(selectedModelName); + AppData.instance.haveUnsavedMessages = false; _showResultDialog( // ignore: use_build_context_synchronously context, success: true, - message: 'Saved to ${widget.chatSessionsDir.path}/$folder/${AppData.appFilenameBookend}$fileName${AppData.appFilenameBookend}.json', + message: 'Saved to: \n${widget.chatSessionsDir.path}/$folder/${AppData.appFilenameBookend}$fileName${AppData.appFilenameBookend}.json', ); diff --git a/confichat/lib/ui_sidebar.dart b/confichat/lib/ui_sidebar.dart index 2e227c3..9ca6026 100644 --- a/confichat/lib/ui_sidebar.dart +++ b/confichat/lib/ui_sidebar.dart @@ -304,8 +304,8 @@ class SidebarState extends State { context: context, builder: (BuildContext context) { return AlertDialog( - title: const DialogTitle(title: 'Confirm Delete'), - content: Text('Are you sure you want to delete $filename?'), + title: const DialogTitle(title: 'Confirm Delete', isError: true), + content: Text('Are you sure you want to delete $filename?', style: Theme.of(context).textTheme.bodyLarge,), actions: [ const SizedBox(height: 20), ElevatedButton( diff --git a/confichat/lib/ui_widgets.dart b/confichat/lib/ui_widgets.dart index 0407ee6..5750108 100644 --- a/confichat/lib/ui_widgets.dart +++ b/confichat/lib/ui_widgets.dart @@ -220,12 +220,14 @@ class DialogTitle extends StatelessWidget { final String title; final double width; final double height; + final bool isError; const DialogTitle({ super.key, required this.title, this.width = double.infinity, - this.height = 50.0 + this.height = 50.0, + this.isError = false }); @override @@ -235,7 +237,7 @@ class DialogTitle extends StatelessWidget { height: height, alignment: Alignment.center, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer, + color: isError ? Theme.of(context).colorScheme.error : Theme.of(context).colorScheme.primaryContainer, borderRadius: const BorderRadius.only( topLeft: Radius.circular(16.0), topRight: Radius.circular(16.0),