diff --git a/lib/core/models/folder.dart b/lib/core/models/folder.dart index eba1306..1d5d4f1 100644 --- a/lib/core/models/folder.dart +++ b/lib/core/models/folder.dart @@ -1,41 +1,63 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'folder.freezed.dart'; -part 'folder.g.dart'; - -// Timestamp converter for Unix timestamps -class TimestampConverter implements JsonConverter { - const TimestampConverter(); - - @override - DateTime fromJson(dynamic json) { - if (json is String) { - return DateTime.parse(json); - } else if (json is int) { - return DateTime.fromMillisecondsSinceEpoch(json * 1000); - } else { - throw ArgumentError('Invalid date format: $json'); - } - } - - @override - dynamic toJson(DateTime object) { - return object.millisecondsSinceEpoch ~/ 1000; - } -} @freezed sealed class Folder with _$Folder { const factory Folder({ required String id, required String name, - @TimestampConverter() required DateTime createdAt, - @TimestampConverter() required DateTime updatedAt, String? parentId, + String? userId, + DateTime? createdAt, + DateTime? updatedAt, + @Default(false) bool isExpanded, @Default([]) List conversationIds, - @Default([]) List subfolders, - @Default({}) Map metadata, + Map? meta, + Map? data, + Map? items, }) = _Folder; - factory Folder.fromJson(Map json) => _$FolderFromJson(json); + factory Folder.fromJson(Map json) { + // Extract conversation IDs from items.chats if available + final items = json['items'] as Map?; + final chats = items?['chats'] as List?; + + // Handle both string IDs and conversation objects + final conversationIds = chats?.map((chat) { + if (chat is String) { + return chat; + } else if (chat is Map) { + return chat['id'] as String? ?? ''; + } + return ''; + }).where((id) => id.isNotEmpty).toList().cast() ?? []; + + // Handle Unix timestamp conversion + DateTime? parseTimestamp(dynamic timestamp) { + if (timestamp == null) return null; + if (timestamp is int) { + return DateTime.fromMillisecondsSinceEpoch(timestamp * 1000); + } + if (timestamp is String) { + return DateTime.parse(timestamp); + } + return null; + } + + // Create the modified JSON with proper field mapping + return Folder( + id: json['id'] as String, + name: json['name'] as String, + parentId: json['parent_id'] as String?, + userId: json['user_id'] as String?, + createdAt: parseTimestamp(json['created_at']), + updatedAt: parseTimestamp(json['updated_at']), + isExpanded: json['is_expanded'] as bool? ?? false, + conversationIds: conversationIds, + meta: json['meta'] as Map?, + data: json['data'] as Map?, + items: json['items'] as Map?, + ); + } } diff --git a/lib/core/providers/app_providers.dart b/lib/core/providers/app_providers.dart index 6370453..8c34f40 100644 --- a/lib/core/providers/app_providers.dart +++ b/lib/core/providers/app_providers.dart @@ -13,6 +13,7 @@ import '../models/server_config.dart'; import '../models/user.dart'; import '../models/model.dart'; import '../models/conversation.dart'; +import '../models/folder.dart'; import '../models/user_settings.dart'; import '../models/folder.dart'; import '../models/file_info.dart'; @@ -278,7 +279,60 @@ final conversationsProvider = FutureProvider>((ref) async { foundation.debugPrint( 'DEBUG: Successfully fetched ${conversations.length} conversations', ); - return conversations; + + // Also fetch folder information and update conversations with folder IDs + try { + final foldersData = await api.getFolders(); + foundation.debugPrint('DEBUG: Fetched ${foldersData.length} folders for conversation mapping'); + + // Parse folder data into Folder objects + final folders = foldersData.map((folderData) => Folder.fromJson(folderData)).toList(); + + // Create a map of conversation ID to folder ID + final conversationToFolder = {}; + for (final folder in folders) { + for (final conversationId in folder.conversationIds) { + conversationToFolder[conversationId] = folder.id; + } + } + + // Update conversations with folder IDs and add missing folder conversations + final updatedConversations = []; + final existingConversationIds = conversations.map((c) => c.id).toSet(); + + for (final conversation in conversations) { + final folderId = conversationToFolder[conversation.id]; + if (folderId != null) { + updatedConversations.add(conversation.copyWith(folderId: folderId)); + foundation.debugPrint('DEBUG: Updated conversation ${conversation.id.substring(0, 8)} with folderId: $folderId'); + } else { + updatedConversations.add(conversation); + } + } + + // Add conversations that are in folders but not in the main list + for (final folder in folders) { + for (final conversationId in folder.conversationIds) { + if (!existingConversationIds.contains(conversationId)) { + // Create a minimal conversation object for folder-only conversations + // We'll need to fetch the full conversation details + try { + final fullConversation = await api.getConversation(conversationId); + updatedConversations.add(fullConversation.copyWith(folderId: folder.id)); + foundation.debugPrint('DEBUG: Added folder conversation ${conversationId.substring(0, 8)} from folder ${folder.name}'); + } catch (e) { + foundation.debugPrint('DEBUG: Failed to fetch folder conversation $conversationId: $e'); + } + } + } + } + + foundation.debugPrint('DEBUG: Final conversation count: ${updatedConversations.length}'); + return updatedConversations; + } catch (e) { + foundation.debugPrint('DEBUG: Failed to fetch folder information: $e'); + return conversations; // Return original conversations if folder fetch fails + } } catch (e, stackTrace) { foundation.debugPrint('DEBUG: Error fetching conversations: $e'); foundation.debugPrint('DEBUG: Stack trace: $stackTrace'); @@ -649,13 +703,20 @@ final conversationSuggestionsProvider = FutureProvider>(( // Folders provider final foldersProvider = FutureProvider>((ref) async { final api = ref.watch(apiServiceProvider); - if (api == null) return []; + if (api == null) { + foundation.debugPrint('DEBUG: No API service available for folders'); + return []; + } try { + foundation.debugPrint('DEBUG: Fetching folders from API...'); final foldersData = await api.getFolders(); - return foldersData + foundation.debugPrint('DEBUG: Raw folders data: $foldersData'); + final folders = foldersData .map((folderData) => Folder.fromJson(folderData)) .toList(); + foundation.debugPrint('DEBUG: Parsed ${folders.length} folders'); + return folders; } catch (e) { foundation.debugPrint('DEBUG: Error fetching folders: $e'); return []; diff --git a/lib/core/services/api_service.dart b/lib/core/services/api_service.dart index cf27cb8..d944c93 100644 --- a/lib/core/services/api_service.dart +++ b/lib/core/services/api_service.dart @@ -404,6 +404,17 @@ class ApiService { // Process regular conversations (excluding pinned and archived ones) for (final chatData in regularChatList) { try { + // Debug: Check if conversation has folder_id in raw data + if (chatData.containsKey('folder_id') && chatData['folder_id'] != null) { + debugPrint('🔍 DEBUG: Found conversation with folder_id in raw data: ${chatData['id']} -> ${chatData['folder_id']}'); + } + + // Debug: Check what fields are available in the chat data + if (regularChatList.indexOf(chatData) == 0) { + debugPrint('🔍 DEBUG: Sample chat data fields: ${chatData.keys.toList()}'); + debugPrint('🔍 DEBUG: Sample chat data: ${chatData.toString().substring(0, 200)}...'); + } + final conversation = _parseOpenWebUIChat(chatData); // Only add if not already added as pinned or archived if (!pinnedIds.contains(conversation.id) && @@ -477,6 +488,11 @@ class ApiService { final archived = chatData['archived'] as bool? ?? false; final shareId = chatData['share_id'] as String?; final folderId = chatData['folder_id'] as String?; + + // Debug logging for folder assignment + if (folderId != null) { + debugPrint('🔍 DEBUG: Conversation ${id.substring(0, 8)} has folderId: $folderId'); + } debugPrint( 'DEBUG: Parsed conversation $id: pinned=$pinned, archived=$archived', @@ -929,13 +945,24 @@ class ApiService { // Folders Future>> getFolders() async { - debugPrint('DEBUG: Fetching folders'); - final response = await _dio.get('/api/v1/folders/'); - final data = response.data; - if (data is List) { - return data.cast>(); + try { + debugPrint('DEBUG: Fetching folders from /api/v1/folders/'); + final response = await _dio.get('/api/v1/folders/'); + debugPrint('DEBUG: Folders response status: ${response.statusCode}'); + debugPrint('DEBUG: Folders response data: ${response.data}'); + + final data = response.data; + if (data is List) { + debugPrint('DEBUG: Found ${data.length} folders'); + return data.cast>(); + } else { + debugPrint('DEBUG: Response data is not a list: ${data.runtimeType}'); + return []; + } + } catch (e) { + debugPrint('DEBUG: Error in getFolders: $e'); + rethrow; } - return []; } Future> createFolder({ diff --git a/lib/core/services/navigation_service.dart b/lib/core/services/navigation_service.dart index bf50b66..9a32177 100644 --- a/lib/core/services/navigation_service.dart +++ b/lib/core/services/navigation_service.dart @@ -6,6 +6,7 @@ import '../../features/auth/views/connect_signin_page.dart'; import '../../features/settings/views/searchable_settings_page.dart'; import '../../features/profile/views/profile_page.dart'; import '../../features/files/views/files_page.dart'; + import '../../features/chat/views/conversation_search_page.dart'; import '../../shared/widgets/themed_dialogs.dart'; @@ -221,6 +222,8 @@ class NavigationService { page = const FilesPage(); break; + + case Routes.chatsList: page = const ChatsListPage(); break; @@ -246,5 +249,6 @@ class Routes { static const String serverConnection = '/server-connection'; static const String search = '/search'; static const String files = '/files'; + static const String chatsList = '/chats-list'; } diff --git a/lib/core/widgets/error_boundary.dart b/lib/core/widgets/error_boundary.dart index e3f0613..2b3f236 100644 --- a/lib/core/widgets/error_boundary.dart +++ b/lib/core/widgets/error_boundary.dart @@ -105,8 +105,10 @@ class _ErrorBoundaryState extends ConsumerState { } // Default error UI - return Scaffold( - backgroundColor: context.conduitTheme.surfaceBackground, + return Directionality( + textDirection: TextDirection.ltr, + child: Scaffold( + backgroundColor: context.conduitTheme.surfaceBackground, body: SafeArea( child: Padding( padding: const EdgeInsets.all(16.0), @@ -145,7 +147,8 @@ class _ErrorBoundaryState extends ConsumerState { ), ), ), - ); + ), + ); } // Wrap child in error handler diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index 284cc1c..9c7cd4c 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -546,6 +546,7 @@ class _ChatPageState extends ConsumerState { _navigateToFiles(); }, ), + ListTile( leading: Icon( Platform.isIOS ? CupertinoIcons.person : Icons.person_outline, @@ -582,6 +583,8 @@ class _ChatPageState extends ConsumerState { ).push(MaterialPageRoute(builder: (context) => const FilesPage())); } + + void _navigateToProfile() { Navigator.of( context, @@ -1229,6 +1232,7 @@ class _ChatPageState extends ConsumerState { }, child: Row( mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, children: [ Flexible( child: Text( @@ -1239,6 +1243,7 @@ class _ChatPageState extends ConsumerState { ), maxLines: 1, overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, ), ), const SizedBox(width: Spacing.xs), @@ -1618,25 +1623,7 @@ class _ModelSelectorSheetState extends ConsumerState<_ModelSelectorSheet> { ), ), - // Header - Padding( - padding: const EdgeInsets.only(bottom: Spacing.sm), - child: Row( - children: [ - Expanded( - child: Text( - 'Choose Model', - style: TextStyle( - color: context.conduitTheme.textPrimary, - fontSize: AppTypography.headlineMedium, - fontWeight: FontWeight.w600, - ), - ), - ), - // Removed capabilities legend to reduce icon noise - ], - ), - ), + // Search field Padding( diff --git a/lib/features/chat/views/model_selector_page.dart b/lib/features/chat/views/model_selector_page.dart index b5fbfed..b95f3d4 100644 --- a/lib/features/chat/views/model_selector_page.dart +++ b/lib/features/chat/views/model_selector_page.dart @@ -6,6 +6,7 @@ import '../../../core/models/model.dart'; import '../../../core/providers/app_providers.dart'; import '../../../shared/theme/theme_extensions.dart'; import '../../../shared/theme/app_theme.dart'; +import '../../../shared/widgets/conduit_components.dart'; class ModelSelectorPage extends ConsumerStatefulWidget { const ModelSelectorPage({super.key}); @@ -53,17 +54,27 @@ class _ModelSelectorPageState extends ConsumerState { @override Widget build(BuildContext context) { - final theme = Theme.of(context); final modelsAsync = ref.watch(modelsProvider); final selectedModel = ref.watch(selectedModelProvider); return Scaffold( appBar: AppBar( - title: const Text('Select Model'), - leading: IconButton( - icon: Icon(Platform.isIOS ? CupertinoIcons.back : Icons.arrow_back), + backgroundColor: context.conduitTheme.surfaceBackground, + elevation: Elevation.none, + scrolledUnderElevation: Elevation.none, + leading: ConduitIconButton( + icon: Platform.isIOS + ? CupertinoIcons.back + : Icons.arrow_back_rounded, onPressed: () => Navigator.pop(context), ), + title: Text( + 'Select Model', + style: AppTypography.headlineMediumStyle.copyWith( + color: context.conduitTheme.textPrimary, + fontWeight: FontWeight.w600, + ), + ), ), body: Column( children: [ @@ -71,10 +82,10 @@ class _ModelSelectorPageState extends ConsumerState { Container( padding: const EdgeInsets.all(Spacing.md), decoration: BoxDecoration( - color: theme.scaffoldBackgroundColor, + color: context.conduitTheme.surfaceBackground, border: Border( bottom: BorderSide( - color: theme.dividerColor.withValues(alpha: 0.1), + color: context.conduitTheme.dividerColor.withValues(alpha: 0.1), width: BorderWidth.regular, ), ), @@ -96,27 +107,22 @@ class _ModelSelectorPageState extends ConsumerState { Platform.isIOS ? CupertinoIcons.cube_box : Icons.view_in_ar, - size: 64, - color: theme.colorScheme.onSurface.withValues( - alpha: 0.3, - ), + size: IconSize.xxl, + color: context.conduitTheme.iconSecondary, ), - const SizedBox(height: Spacing.md), + const SizedBox(height: Spacing.lg), Text( 'No models available', - style: theme.textTheme.titleMedium?.copyWith( - color: theme.colorScheme.onSurface.withValues( - alpha: 0.6, - ), + style: AppTypography.headlineSmallStyle.copyWith( + color: context.conduitTheme.textPrimary, + fontWeight: FontWeight.w600, ), ), const SizedBox(height: Spacing.sm), Text( 'Please check your Open-WebUI configuration', - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurface.withValues( - alpha: 0.5, - ), + style: AppTypography.bodyMediumStyle.copyWith( + color: context.conduitTheme.textSecondary, ), ), ], @@ -132,28 +138,23 @@ class _ModelSelectorPageState extends ConsumerState { Icon( Platform.isIOS ? CupertinoIcons.search - : Icons.search_off, - size: 64, - color: theme.colorScheme.onSurface.withValues( - alpha: 0.3, - ), + : Icons.search_rounded, + size: IconSize.xxl, + color: context.conduitTheme.iconSecondary, ), - const SizedBox(height: Spacing.md), + const SizedBox(height: Spacing.lg), Text( 'No models found', - style: theme.textTheme.titleMedium?.copyWith( - color: theme.colorScheme.onSurface.withValues( - alpha: 0.6, - ), + style: AppTypography.headlineSmallStyle.copyWith( + color: context.conduitTheme.textPrimary, + fontWeight: FontWeight.w600, ), ), const SizedBox(height: Spacing.sm), Text( 'Try searching with different keywords', - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurface.withValues( - alpha: 0.5, - ), + style: AppTypography.bodyMediumStyle.copyWith( + color: context.conduitTheme.textSecondary, ), ), ], @@ -183,9 +184,10 @@ class _ModelSelectorPageState extends ConsumerState { ), child: Text( group.title!, - style: theme.textTheme.titleSmall?.copyWith( - color: theme.colorScheme.primary, + style: AppTypography.labelStyle.copyWith( + color: context.conduitTheme.textSecondary, fontWeight: FontWeight.w600, + letterSpacing: 0.5, ), ), ), @@ -206,7 +208,13 @@ class _ModelSelectorPageState extends ConsumerState { }, ); }, - loading: () => const Center(child: CircularProgressIndicator()), + loading: () => Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + context.conduitTheme.buttonPrimary, + ), + ), + ), error: (error, _) => Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -214,32 +222,48 @@ class _ModelSelectorPageState extends ConsumerState { Icon( Platform.isIOS ? CupertinoIcons.exclamationmark_triangle - : Icons.error_outline, - size: 48, - color: theme.colorScheme.error, + : Icons.error_rounded, + size: IconSize.xxl, + color: context.conduitTheme.error, ), - const SizedBox(height: Spacing.md), + const SizedBox(height: Spacing.lg), Text( 'Failed to load models', - style: theme.textTheme.titleMedium, + style: AppTypography.headlineSmallStyle.copyWith( + color: context.conduitTheme.textPrimary, + fontWeight: FontWeight.w600, + ), ), const SizedBox(height: Spacing.sm), Text( - error.toString(), - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurface.withValues( - alpha: 0.6, - ), + 'Please try again later', + style: AppTypography.bodyMediumStyle.copyWith( + color: context.conduitTheme.textSecondary, ), textAlign: TextAlign.center, ), - const SizedBox(height: Spacing.lg), - ElevatedButton.icon( + const SizedBox(height: Spacing.xl), + ElevatedButton( onPressed: () => ref.refresh(modelsProvider), - icon: Icon( - Platform.isIOS ? CupertinoIcons.refresh : Icons.refresh, + style: ElevatedButton.styleFrom( + backgroundColor: context.conduitTheme.buttonPrimary, + foregroundColor: context.conduitTheme.buttonPrimaryText, + padding: const EdgeInsets.symmetric( + horizontal: Spacing.buttonPadding, + vertical: Spacing.md, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.button), + ), + elevation: Elevation.none, + ), + child: Text( + 'Retry', + style: AppTypography.labelStyle.copyWith( + color: context.conduitTheme.buttonPrimaryText, + fontWeight: FontWeight.w600, + ), ), - label: const Text('Retry'), ), ], ), @@ -339,20 +363,18 @@ class ModelTile extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - return Card( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), elevation: isSelected ? 2 : 0, color: isSelected - ? theme.colorScheme.primary.withValues(alpha: 0.1) - : null, + ? context.conduitTheme.buttonPrimary.withValues(alpha: 0.1) + : context.conduitTheme.cardBackground, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppBorderRadius.md), side: BorderSide( color: isSelected - ? theme.colorScheme.primary - : theme.dividerColor.withValues(alpha: 0.3), + ? context.conduitTheme.buttonPrimary + : context.conduitTheme.dividerColor.withValues(alpha: 0.3), width: isSelected ? 2 : 1, ), ), @@ -369,9 +391,11 @@ class ModelTile extends StatelessWidget { Expanded( child: Text( model.name, - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: isSelected ? FontWeight.w600 : null, - color: isSelected ? theme.colorScheme.primary : null, + style: AppTypography.bodyLargeStyle.copyWith( + fontWeight: FontWeight.w600, + color: isSelected + ? context.conduitTheme.buttonPrimary + : context.conduitTheme.textPrimary, ), ), ), @@ -380,7 +404,7 @@ class ModelTile extends StatelessWidget { Platform.isIOS ? CupertinoIcons.checkmark_circle_fill : Icons.check_circle, - color: theme.colorScheme.primary, + color: context.conduitTheme.buttonPrimary, ), ], ), @@ -388,8 +412,8 @@ class ModelTile extends StatelessWidget { const SizedBox(height: Spacing.xs), Text( model.description!, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurface.withValues(alpha: 0.6), + style: AppTypography.bodySmallStyle.copyWith( + color: context.conduitTheme.textSecondary, ), maxLines: 2, overflow: TextOverflow.ellipsis, diff --git a/lib/features/chat/widgets/folder_management_dialog.dart b/lib/features/chat/widgets/folder_management_dialog.dart index add4352..57d60a2 100644 --- a/lib/features/chat/widgets/folder_management_dialog.dart +++ b/lib/features/chat/widgets/folder_management_dialog.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; import '../../../shared/theme/app_theme.dart'; import '../../../shared/theme/theme_extensions.dart'; import '../../../shared/widgets/conduit_components.dart'; @@ -13,8 +14,9 @@ import '../../../core/providers/app_providers.dart'; class FolderManagementDialog extends ConsumerStatefulWidget { final Conversation? conversation; + final BuildContext? parentContext; - const FolderManagementDialog({super.key, this.conversation}); + const FolderManagementDialog({super.key, this.conversation, this.parentContext}); @override ConsumerState createState() => @@ -35,265 +37,280 @@ class _FolderManagementDialogState @override Widget build(BuildContext context) { final folders = ref.watch(foldersProvider); + final isMovingConversation = widget.conversation != null; - return Dialog( - backgroundColor: Colors.transparent, - child: Container( - width: 400, - constraints: const BoxConstraints(maxHeight: 600), - decoration: BoxDecoration( - color: context.conduitTheme.cardBackground, - borderRadius: BorderRadius.circular(AppBorderRadius.xl), - border: Border.all( - color: context.conduitTheme.cardBorder.withValues(alpha: 0.3), - width: BorderWidth.thin, + return Directionality( + textDirection: TextDirection.ltr, + child: Dialog( + backgroundColor: Colors.transparent, + child: Container( + width: 480, + constraints: const BoxConstraints(maxHeight: 680), + decoration: BoxDecoration( + color: context.conduitTheme.surfaceBackground, + borderRadius: BorderRadius.circular(AppBorderRadius.modal), + border: Border.all( + color: context.conduitTheme.cardBorder.withValues(alpha: 0.2), + width: BorderWidth.regular, + ), + boxShadow: ConduitShadows.modal, ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Header - Container( - padding: const EdgeInsets.all(Spacing.lg), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - context.conduitTheme.buttonPrimary, - context.conduitTheme.buttonPrimary.withValues(alpha: 0.8), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: const BorderRadius.vertical( - top: Radius.circular(AppBorderRadius.xl), - ), - ), - child: Row( - children: [ - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: context.conduitTheme.textInverse.withValues( - alpha: 0.2, - ), - borderRadius: BorderRadius.circular(AppBorderRadius.sm), - ), - child: Icon( - Platform.isIOS - ? CupertinoIcons.folder - : Icons.folder_rounded, - color: context.conduitTheme.textInverse, - size: IconSize.md, - ), - ), - const SizedBox(width: Spacing.md), - Expanded( - child: Text( - widget.conversation != null - ? 'Move to Folder' - : 'Manage Folders', - style: TextStyle( - color: context.conduitTheme.textInverse, - fontSize: AppTypography.headlineSmall, - fontWeight: FontWeight.w600, - ), - ), - ), - ConduitIconButton( - icon: Platform.isIOS - ? CupertinoIcons.xmark - : Icons.close_rounded, - onPressed: () => Navigator.pop(context), - ), - ], - ), - ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Modern Header + _buildModernHeader(context, isMovingConversation), - // Create new folder section - Padding( - padding: const EdgeInsets.all(Spacing.lg), - child: Row( - children: [ - Expanded( - child: Container( - decoration: BoxDecoration( - color: context.conduitTheme.inputBackground, - borderRadius: BorderRadius.circular(AppBorderRadius.md), - border: Border.all( - color: context.conduitTheme.inputBorder, - width: BorderWidth.thin, - ), - ), - child: TextField( - controller: _nameController, - style: TextStyle( - color: context.conduitTheme.inputText, - fontSize: AppTypography.bodyLarge, - ), - decoration: InputDecoration( - hintText: 'New folder name', - hintStyle: TextStyle( - color: context.conduitTheme.inputPlaceholder - .withValues(alpha: 0.5), - fontSize: AppTypography.bodyLarge, - ), - border: InputBorder.none, - contentPadding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), - prefixIcon: Icon( - Platform.isIOS - ? CupertinoIcons.folder_badge_plus - : Icons.create_new_folder_rounded, - color: context.conduitTheme.iconSecondary, - size: IconSize.md, - ), - ), - onSubmitted: (_) => _createFolder(), - ), - ), - ), - const SizedBox(width: Spacing.md), - ConduitButton( - text: 'Create', - onPressed: _isCreating ? null : _createFolder, - isLoading: _isCreating, - width: 80, - ), - ], - ), - ), - - // Divider - Container( - height: 0.5, - margin: const EdgeInsets.symmetric(horizontal: Spacing.lg), - color: context.conduitTheme.dividerColor.withValues(alpha: 0.3), - ), - - // Folders list - Expanded( - child: folders.when( - data: (folderList) => folderList.isEmpty - ? _buildEmptyState() - : ListView.builder( - padding: const EdgeInsets.symmetric( - vertical: Spacing.sm, - ), - itemCount: folderList.length, - itemBuilder: (context, index) { - final folder = folderList[index]; - return _buildFolderTile(folder); - }, - ), - loading: () => _buildLoadingState(), - error: (error, _) => _buildErrorState(error), - ), - ), - - // Bottom actions - if (widget.conversation != null) ...[ - Container( - height: 0.5, - margin: const EdgeInsets.symmetric(horizontal: Spacing.lg), - color: context.conduitTheme.dividerColor.withValues(alpha: 0.3), - ), - Padding( - padding: const EdgeInsets.all(Spacing.lg), - child: Row( + // Content Section + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + // Create folder section (only if managing folders) + if (!isMovingConversation) ...[ + _buildCreateFolderSection(context), + ConduitDivider(color: context.conduitTheme.dividerColor.withValues(alpha: 0.2)), + ], + + // Folders list Expanded( - child: ConduitButton( - text: 'Remove from Folder', - onPressed: () => _moveToFolder(null), - isSecondary: true, - ), - ), - const SizedBox(width: Spacing.md), - Expanded( - child: ConduitButton( - text: 'Cancel', - onPressed: () => Navigator.pop(context), - isSecondary: true, + child: folders.when( + data: (folderList) => _buildFoldersList(context, folderList, isMovingConversation), + loading: () => _buildLoadingState(context), + error: (error, _) => _buildErrorState(context, error), ), ), ], ), ), + + // Bottom actions (only for conversation moving) + if (isMovingConversation) _buildBottomActions(context), ], - ], + ), + ).animate().slideY( + begin: 0.1, + duration: AnimationDuration.modalPresentation, + curve: AnimationCurves.modalPresentation, + ).fadeIn( + duration: AnimationDuration.modalPresentation, + curve: AnimationCurves.easeOut, ), ), ); } - Widget _buildEmptyState() { + // Modern header with clean design + Widget _buildModernHeader(BuildContext context, bool isMovingConversation) { + return Container( + padding: const EdgeInsets.all(Spacing.lg), + decoration: BoxDecoration( + color: context.conduitTheme.cardBackground, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(AppBorderRadius.modal), + ), + border: Border( + bottom: BorderSide( + color: context.conduitTheme.dividerColor.withValues(alpha: 0.1), + width: BorderWidth.regular, + ), + ), + ), + child: Row( + children: [ + // Modern icon container + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: context.conduitTheme.buttonPrimary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(AppBorderRadius.lg), + ), + child: Icon( + Platform.isIOS ? CupertinoIcons.folder_fill : Icons.folder_rounded, + color: context.conduitTheme.buttonPrimary, + size: IconSize.medium, + ), + ), + const SizedBox(width: Spacing.md), + + // Title and subtitle + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + isMovingConversation ? 'Move to Folder' : 'Manage Folders', + style: AppTypography.headlineMediumStyle.copyWith( + color: context.conduitTheme.textPrimary, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: Spacing.xs), + Text( + isMovingConversation + ? 'Select a folder for "${widget.conversation?.title ?? 'this conversation'}"' + : 'Create and organize your conversation folders', + style: AppTypography.bodyMediumStyle.copyWith( + color: context.conduitTheme.textSecondary, + ), + ), + ], + ), + ), + + // Close button + ConduitIconButton( + icon: Platform.isIOS ? CupertinoIcons.xmark : Icons.close_rounded, + onPressed: () => Navigator.pop(context), + tooltip: 'Close', + ), + ], + ), + ); + } + + // Create folder section with improved UX + Widget _buildCreateFolderSection(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: Spacing.xl, vertical: Spacing.lg), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Create New Folder', + style: AppTypography.bodyMediumStyle.copyWith( + color: context.conduitTheme.textPrimary, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: Spacing.sm), + Row( + children: [ + Expanded( + child: AccessibleFormField( + controller: _nameController, + hint: 'Enter folder name', + prefixIcon: Icon( + Platform.isIOS + ? CupertinoIcons.folder_badge_plus + : Icons.create_new_folder_rounded, + color: context.conduitTheme.iconSecondary, + size: IconSize.medium, + ), + onSubmitted: (_) => _createFolder(), + isCompact: true, + ), + ), + const SizedBox(width: Spacing.md), + ConduitButton( + text: 'Create', + onPressed: _isCreating ? null : _createFolder, + isLoading: _isCreating, + icon: Platform.isIOS ? CupertinoIcons.add : Icons.add_rounded, + isCompact: true, + ), + ], + ), + ], + ), + ); + } + + // Enhanced folders list + Widget _buildFoldersList(BuildContext context, List folderList, bool isMovingConversation) { + if (folderList.isEmpty) { + return _buildEmptyState(context, isMovingConversation); + } + + return ListView.separated( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.xl, + vertical: Spacing.md, + ), + itemCount: folderList.length, + separatorBuilder: (context, index) => const SizedBox(height: Spacing.xs), + itemBuilder: (context, index) { + final folder = folderList[index]; + return _buildFolderTile(folder, index).animate(delay: Duration(milliseconds: index * 50)) + .slideX(begin: 0.2, duration: AnimationDuration.fast) + .fadeIn(duration: AnimationDuration.fast); + }, + ); + } + + Widget _buildEmptyState(BuildContext context, bool isMovingConversation) { + return ConduitEmptyState( + icon: Platform.isIOS ? CupertinoIcons.folder : Icons.folder_outlined, + title: 'No folders yet', + message: isMovingConversation + ? 'Create a folder first' + : 'Use the form above to create your first folder', + isCompact: true, + ); + } + + Widget _buildLoadingState(BuildContext context) { return Center( child: Padding( padding: const EdgeInsets.all(Spacing.xl), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: 80, - height: 80, - decoration: BoxDecoration( - color: context.conduitTheme.cardBackground.withValues( - alpha: 0.6, - ), - borderRadius: BorderRadius.circular(AppBorderRadius.round), - ), - child: Icon( - Platform.isIOS ? CupertinoIcons.folder : Icons.folder_outlined, - size: 40, - color: context.conduitTheme.iconSecondary, - ), - ), - const SizedBox(height: Spacing.lg), - Text( - 'No folders yet', - style: TextStyle( - color: context.conduitTheme.textPrimary, - fontSize: AppTypography.headlineSmall, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: Spacing.sm), - Text( - 'Create a folder to organize\nyour conversations', - style: TextStyle( - color: context.conduitTheme.textSecondary, - fontSize: AppTypography.labelLarge, - height: 1.4, - ), - textAlign: TextAlign.center, - ), - ], + child: ConduitLoadingIndicator( + message: 'Loading folders...', + size: IconSize.xl, ), ), ); } - Widget _buildLoadingState() { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + Widget _buildErrorState(BuildContext context, Object error) { + return ConduitEmptyState( + icon: Icons.error_outline_rounded, + title: 'Failed to load folders', + message: 'Please check your connection and try again', + isCompact: true, + action: ConduitButton( + text: 'Retry', + onPressed: () => ref.invalidate(foldersProvider), + icon: Icons.refresh_rounded, + isCompact: true, + ), + ); + } + + // Bottom actions for conversation moving + Widget _buildBottomActions(BuildContext context) { + return Container( + padding: const EdgeInsets.all(Spacing.lg), + decoration: BoxDecoration( + color: context.conduitTheme.cardBackground, + borderRadius: const BorderRadius.vertical( + bottom: Radius.circular(AppBorderRadius.modal), + ), + border: Border( + top: BorderSide( + color: context.conduitTheme.dividerColor.withValues(alpha: 0.1), + width: BorderWidth.regular, + ), + ), + ), + child: Row( children: [ - CircularProgressIndicator.adaptive( - strokeWidth: 3, - valueColor: AlwaysStoppedAnimation( - context.conduitTheme.buttonPrimary, + Expanded( + child: ConduitButton( + text: 'Remove from Folder', + onPressed: () => _moveToFolder(null), + isSecondary: true, + icon: Platform.isIOS ? CupertinoIcons.folder_badge_minus : Icons.folder_off_rounded, ), ), - SizedBox(height: Spacing.lg), - Text( - 'Loading folders...', - style: TextStyle( - color: context.conduitTheme.textSecondary, - fontSize: AppTypography.bodyLarge, - fontWeight: FontWeight.w500, + const SizedBox(width: Spacing.md), + Expanded( + child: ConduitButton( + text: 'Cancel', + onPressed: () => Navigator.pop(context), + isSecondary: true, + icon: Platform.isIOS ? CupertinoIcons.xmark : Icons.close_rounded, ), ), ], @@ -301,222 +318,175 @@ class _FolderManagementDialogState ); } - Widget _buildErrorState(Object error) { - return Center( - child: Padding( - padding: const EdgeInsets.all(Spacing.xl), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + Widget _buildFolderTile(Folder folder, int index) { + final isSelected = widget.conversation?.folderId == folder.id; + final isMovingConversation = widget.conversation != null; + + return ConduitCard( + onTap: isMovingConversation ? () => _moveToFolder(folder.id) : null, + isSelected: isSelected, + child: ConduitListItem( + leading: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: isSelected + ? context.conduitTheme.buttonPrimary.withValues(alpha: 0.15) + : context.conduitTheme.surfaceContainer, + borderRadius: BorderRadius.circular(AppBorderRadius.lg), + border: isSelected ? Border.all( + color: context.conduitTheme.buttonPrimary.withValues(alpha: 0.3), + width: BorderWidth.regular, + ) : null, + ), + child: Icon( + Platform.isIOS ? CupertinoIcons.folder_fill : Icons.folder_rounded, + color: isSelected + ? context.conduitTheme.buttonPrimary + : context.conduitTheme.iconSecondary, + size: IconSize.lg, + ), + ), + title: Text( + folder.name, + style: AppTypography.bodyLargeStyle.copyWith( + color: isSelected + ? context.conduitTheme.buttonPrimary + : context.conduitTheme.textPrimary, + fontWeight: FontWeight.w600, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Row( children: [ - Container( - width: 80, - height: 80, - decoration: BoxDecoration( - color: context.conduitTheme.error.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(AppBorderRadius.round), - ), - child: Icon( - Icons.error_outline_rounded, - size: 40, - color: context.conduitTheme.error, - ), + Icon( + Platform.isIOS ? CupertinoIcons.chat_bubble_2 : Icons.chat_bubble_outline_rounded, + size: IconSize.xs, + color: context.conduitTheme.textTertiary, ), - const SizedBox(height: Spacing.lg), + const SizedBox(width: Spacing.xs), Text( - 'Failed to load folders', - style: TextStyle( - color: context.conduitTheme.textPrimary, - fontSize: AppTypography.headlineSmall, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: Spacing.sm), - Text( - error.toString(), - style: TextStyle( + '${folder.conversationIds.length} conversation${folder.conversationIds.length != 1 ? 's' : ''}', + style: AppTypography.bodySmallStyle.copyWith( color: context.conduitTheme.textSecondary, - fontSize: AppTypography.labelLarge, - height: 1.4, ), - textAlign: TextAlign.center, ), + if (folder.conversationIds.isNotEmpty) ...[ + const SizedBox(width: Spacing.sm), + Container( + width: 4, + height: 4, + decoration: BoxDecoration( + color: context.conduitTheme.textTertiary, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: Spacing.sm), + Text( + 'Active', + style: AppTypography.captionStyle.copyWith( + color: context.conduitTheme.success, + fontWeight: FontWeight.w500, + ), + ), + ], ], ), + trailing: _buildFolderActions(folder, isSelected, isMovingConversation), + isSelected: isSelected, ), ); } - Widget _buildFolderTile(Folder folder) { - final isSelected = widget.conversation?.folderId == folder.id; - - return Container( - margin: const EdgeInsets.symmetric( - horizontal: Spacing.lg, - vertical: Spacing.xs, - ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: widget.conversation != null - ? () => _moveToFolder(folder.id) - : null, - borderRadius: BorderRadius.circular(AppBorderRadius.md), - child: Container( - padding: const EdgeInsets.all(Spacing.md), - decoration: BoxDecoration( - color: isSelected - ? context.conduitTheme.buttonPrimary.withValues(alpha: 0.1) - : context.conduitTheme.cardBackground.withValues(alpha: 0.5), - borderRadius: BorderRadius.circular(AppBorderRadius.md), - border: Border.all( - color: isSelected - ? context.conduitTheme.buttonPrimary.withValues(alpha: 0.3) - : context.conduitTheme.cardBorder.withValues(alpha: 0.2), - width: BorderWidth.thin, + Widget _buildFolderActions(Folder folder, bool isSelected, bool isMovingConversation) { + if (isMovingConversation) { + return isSelected + ? Container( + padding: const EdgeInsets.all(Spacing.xs), + decoration: BoxDecoration( + color: context.conduitTheme.buttonPrimary, + borderRadius: BorderRadius.circular(AppBorderRadius.round), ), - ), - child: Row( - children: [ - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: isSelected - ? context.conduitTheme.buttonPrimary.withValues( - alpha: 0.2, - ) - : context.conduitTheme.cardBorder.withValues( - alpha: 0.6, - ), - borderRadius: BorderRadius.circular(AppBorderRadius.sm), - ), - child: Icon( - Platform.isIOS - ? CupertinoIcons.folder_fill - : Icons.folder_rounded, - color: isSelected - ? context.conduitTheme.buttonPrimary - : context.conduitTheme.iconSecondary, - size: IconSize.md, - ), - ), - const SizedBox(width: Spacing.md), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - folder.name, - style: TextStyle( - color: isSelected - ? context.conduitTheme.buttonPrimary - : context.conduitTheme.textPrimary, - fontSize: AppTypography.bodyLarge, - fontWeight: isSelected - ? FontWeight.w600 - : FontWeight.w500, - ), - ), - const SizedBox(height: Spacing.xxs), - Text( - '${folder.conversationIds.length} conversations', - style: TextStyle( - color: context.conduitTheme.textSecondary, - fontSize: AppTypography.labelMedium, - ), - ), - ], - ), - ), - if (widget.conversation != null && isSelected) - Container( - width: 24, - height: 24, - decoration: BoxDecoration( - color: context.conduitTheme.buttonPrimary, - borderRadius: BorderRadius.circular( - AppBorderRadius.round, - ), - ), - child: Icon( - Icons.check_rounded, - color: context.conduitTheme.textInverse, - size: 16, - ), - ) - else if (widget.conversation == null) - PopupMenuButton( - icon: Icon( - Icons.more_vert_rounded, - color: context.conduitTheme.iconSecondary, - size: IconSize.md, - ), - color: context.conduitTheme.cardBackground, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - side: BorderSide( - color: context.conduitTheme.cardBorder.withValues( - alpha: 0.3, - ), - width: BorderWidth.thin, - ), - ), - onSelected: (value) { - switch (value) { - case 'rename': - _renameFolder(folder); - break; - case 'delete': - _deleteFolder(folder); - break; - } - }, - itemBuilder: (context) => [ - PopupMenuItem( - value: 'rename', - child: Row( - children: [ - Icon( - Icons.edit_rounded, - size: 18, - color: context.conduitTheme.iconSecondary, - ), - const SizedBox(width: Spacing.sm), - Text( - 'Rename', - style: TextStyle( - color: context.conduitTheme.textPrimary, - ), - ), - ], - ), - ), - PopupMenuItem( - value: 'delete', - child: Row( - children: [ - Icon( - Icons.delete_rounded, - size: 18, - color: context.conduitTheme.error, - ), - const SizedBox(width: Spacing.sm), - Text( - 'Delete', - style: TextStyle( - color: context.conduitTheme.error, - ), - ), - ], - ), - ), - ], - ), - ], - ), - ), + child: Icon( + Platform.isIOS ? CupertinoIcons.checkmark : Icons.check_rounded, + color: context.conduitTheme.buttonPrimaryText, + size: IconSize.small, + ), + ) + : Icon( + Platform.isIOS ? CupertinoIcons.chevron_right : Icons.arrow_forward_ios_rounded, + color: context.conduitTheme.iconSecondary.withValues(alpha: 0.6), + size: IconSize.small, + ); + } + + // Management mode - show actions menu + return PopupMenuButton( + icon: Icon( + Platform.isIOS ? CupertinoIcons.ellipsis : Icons.more_vert_rounded, + color: context.conduitTheme.iconSecondary, + size: IconSize.medium, + ), + color: context.conduitTheme.surfaceBackground, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.lg), + side: BorderSide( + color: context.conduitTheme.cardBorder.withValues(alpha: 0.2), + width: BorderWidth.regular, ), ), + elevation: Elevation.medium, + onSelected: (value) { + switch (value) { + case 'rename': + _renameFolder(folder); + break; + case 'delete': + _deleteFolder(folder); + break; + } + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: 'rename', + child: Row( + children: [ + Icon( + Platform.isIOS ? CupertinoIcons.pencil : Icons.edit_rounded, + size: IconSize.small, + color: context.conduitTheme.iconSecondary, + ), + const SizedBox(width: Spacing.md), + Text( + 'Rename', + style: AppTypography.bodyMediumStyle.copyWith( + color: context.conduitTheme.textPrimary, + ), + ), + ], + ), + ), + PopupMenuItem( + value: 'delete', + child: Row( + children: [ + Icon( + Platform.isIOS ? CupertinoIcons.delete : Icons.delete_outline_rounded, + size: IconSize.small, + color: context.conduitTheme.error, + ), + const SizedBox(width: Spacing.md), + Text( + 'Delete', + style: AppTypography.bodyMediumStyle.copyWith( + color: context.conduitTheme.error, + ), + ), + ], + ), + ), + ], ); } @@ -535,11 +505,11 @@ class _FolderManagementDialogState _nameController.clear(); if (mounted) { - UiUtils.showMessage(context, 'Folder "$name" created'); + UiUtils.showMessage(widget.parentContext ?? context, 'Folder "$name" created'); } } catch (e) { if (mounted) { - UiUtils.showMessage(context, 'Error creating folder: $e'); + UiUtils.showMessage(widget.parentContext ?? context, 'Error creating folder: $e'); } } finally { if (mounted) { @@ -562,7 +532,7 @@ class _FolderManagementDialogState if (mounted) { Navigator.pop(context); UiUtils.showMessage( - context, + widget.parentContext ?? context, folderId != null ? 'Conversation moved to folder' : 'Conversation removed from folder', @@ -570,66 +540,161 @@ class _FolderManagementDialogState } } catch (e) { if (mounted) { - UiUtils.showMessage(context, 'Error moving conversation: $e'); + UiUtils.showMessage(widget.parentContext ?? context, 'Error moving conversation: $e'); } } } void _renameFolder(Folder folder) async { final controller = TextEditingController(text: folder.name); + final result = await showDialog( context: context, - builder: (context) => AlertDialog( - backgroundColor: AppTheme.neutral700, - title: Text( - 'Rename Folder', - style: TextStyle(color: context.conduitTheme.textPrimary), - ), - content: TextField( - controller: controller, - style: TextStyle(color: context.conduitTheme.inputText), - decoration: InputDecoration( - hintText: 'Folder name', - hintStyle: TextStyle( - color: context.conduitTheme.inputPlaceholder.withValues( - alpha: 0.5, + builder: (dialogContext) => Directionality( + textDirection: TextDirection.ltr, + child: Dialog( + backgroundColor: Colors.transparent, + child: Container( + width: 400, + decoration: BoxDecoration( + color: dialogContext.conduitTheme.surfaceBackground, + borderRadius: BorderRadius.circular(AppBorderRadius.modal), + border: Border.all( + color: dialogContext.conduitTheme.cardBorder.withValues(alpha: 0.2), + width: BorderWidth.regular, + ), + boxShadow: ConduitShadows.modal, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Header + Container( + padding: const EdgeInsets.all(Spacing.xl), + decoration: BoxDecoration( + color: dialogContext.conduitTheme.cardBackground, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(AppBorderRadius.modal), + ), + border: Border( + bottom: BorderSide( + color: dialogContext.conduitTheme.dividerColor.withValues(alpha: 0.1), + width: BorderWidth.regular, + ), + ), + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: dialogContext.conduitTheme.buttonPrimary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(AppBorderRadius.lg), + ), + child: Icon( + Platform.isIOS ? CupertinoIcons.pencil : Icons.edit_rounded, + color: dialogContext.conduitTheme.buttonPrimary, + size: IconSize.medium, + ), + ), + const SizedBox(width: Spacing.md), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Rename Folder', + style: AppTypography.headlineSmallStyle.copyWith( + color: dialogContext.conduitTheme.textPrimary, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: Spacing.xs), + Text( + 'Enter a new name for your folder', + style: AppTypography.bodyMediumStyle.copyWith( + color: dialogContext.conduitTheme.textSecondary, + ), + ), + ], + ), + ), + ], + ), ), - ), - border: OutlineInputBorder( - borderSide: BorderSide( - color: context.conduitTheme.inputBorder.withValues(alpha: 0.2), + + // Content + Padding( + padding: const EdgeInsets.all(Spacing.xl), + child: AccessibleFormField( + controller: controller, + label: 'Folder Name', + hint: 'Enter folder name', + autofocus: true, + isRequired: true, + onSubmitted: (value) { + if (value.trim().isNotEmpty) { + Navigator.pop(dialogContext, value.trim()); + } + }, + ), ), - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: context.conduitTheme.inputBorder.withValues(alpha: 0.2), + + // Actions + Container( + padding: const EdgeInsets.all(Spacing.xl), + decoration: BoxDecoration( + color: dialogContext.conduitTheme.cardBackground, + borderRadius: const BorderRadius.vertical( + bottom: Radius.circular(AppBorderRadius.modal), + ), + border: Border( + top: BorderSide( + color: dialogContext.conduitTheme.dividerColor.withValues(alpha: 0.1), + width: BorderWidth.regular, + ), + ), + ), + child: Row( + children: [ + Expanded( + child: ConduitButton( + text: 'Cancel', + onPressed: () => Navigator.pop(dialogContext), + isSecondary: true, + ), + ), + const SizedBox(width: Spacing.md), + Expanded( + child: ConduitButton( + text: 'Rename', + onPressed: () { + final newName = controller.text.trim(); + if (newName.isNotEmpty) { + Navigator.pop(dialogContext, newName); + } + }, + icon: Platform.isIOS ? CupertinoIcons.checkmark : Icons.check_rounded, + ), + ), + ], + ), ), - ), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: context.conduitTheme.buttonPrimary), - ), + ], ), ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text( - 'Cancel', - style: TextStyle( - color: context.conduitTheme.textPrimary.withValues(alpha: 0.7), - ), - ), - ), - FilledButton( - onPressed: () => Navigator.pop(context, controller.text.trim()), - style: FilledButton.styleFrom( - backgroundColor: context.conduitTheme.buttonPrimary, - ), - child: const Text('Rename'), - ), - ], + ).animate().slideY( + begin: 0.1, + duration: AnimationDuration.modalPresentation, + curve: AnimationCurves.modalPresentation, + ).fadeIn( + duration: AnimationDuration.modalPresentation, + curve: AnimationCurves.easeOut, ), - ); + ), + ); if (result != null && result.isNotEmpty && result != folder.name) { try { @@ -639,12 +704,12 @@ class _FolderManagementDialogState ref.invalidate(foldersProvider); if (mounted) { - UiUtils.showMessage(context, 'Folder renamed to "$result"'); + UiUtils.showMessage(widget.parentContext ?? context, 'Folder renamed to "$result"'); } } } catch (e) { if (mounted) { - UiUtils.showMessage(context, 'Failed to rename folder: $e'); + UiUtils.showMessage(widget.parentContext ?? context, 'Failed to rename folder: $e'); } } } @@ -655,36 +720,202 @@ class _FolderManagementDialogState void _deleteFolder(Folder folder) async { final confirmed = await showDialog( context: context, - builder: (context) => AlertDialog( - backgroundColor: context.conduitTheme.cardBackground, - title: Text( - 'Delete Folder', - style: TextStyle(color: context.conduitTheme.textPrimary), - ), - content: Text( - 'Are you sure you want to delete "${folder.name}"?\n\nThis action cannot be undone. Conversations in this folder will be moved to the main folder.', - style: TextStyle(color: context.conduitTheme.textPrimary), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: Text( - 'Cancel', - style: TextStyle( - color: context.conduitTheme.textPrimary.withValues(alpha: 0.7), + builder: (dialogContext) => Directionality( + textDirection: TextDirection.ltr, + child: Dialog( + backgroundColor: Colors.transparent, + child: Container( + width: 400, + decoration: BoxDecoration( + color: dialogContext.conduitTheme.surfaceBackground, + borderRadius: BorderRadius.circular(AppBorderRadius.modal), + border: Border.all( + color: dialogContext.conduitTheme.cardBorder.withValues(alpha: 0.2), + width: BorderWidth.regular, + ), + boxShadow: ConduitShadows.modal, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Header + Container( + padding: const EdgeInsets.all(Spacing.xl), + decoration: BoxDecoration( + color: dialogContext.conduitTheme.cardBackground, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(AppBorderRadius.modal), + ), + border: Border( + bottom: BorderSide( + color: dialogContext.conduitTheme.dividerColor.withValues(alpha: 0.1), + width: BorderWidth.regular, + ), + ), + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: dialogContext.conduitTheme.error.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(AppBorderRadius.lg), + ), + child: Icon( + Platform.isIOS ? CupertinoIcons.delete : Icons.delete_outline_rounded, + color: dialogContext.conduitTheme.error, + size: IconSize.medium, + ), + ), + const SizedBox(width: Spacing.md), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Delete Folder', + style: AppTypography.headlineSmallStyle.copyWith( + color: dialogContext.conduitTheme.textPrimary, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: Spacing.xs), + Text( + 'This action cannot be undone', + style: AppTypography.bodyMediumStyle.copyWith( + color: dialogContext.conduitTheme.error, + ), + ), + ], + ), + ), + ], + ), ), - ), + + // Content + Padding( + padding: const EdgeInsets.all(Spacing.xl), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(Spacing.md), + decoration: BoxDecoration( + color: dialogContext.conduitTheme.surfaceContainer, + borderRadius: BorderRadius.circular(AppBorderRadius.lg), + border: Border.all( + color: dialogContext.conduitTheme.dividerColor.withValues(alpha: 0.2), + width: BorderWidth.regular, + ), + ), + child: Row( + children: [ + Icon( + Platform.isIOS ? CupertinoIcons.folder_fill : Icons.folder_rounded, + color: dialogContext.conduitTheme.iconSecondary, + size: IconSize.medium, + ), + const SizedBox(width: Spacing.md), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + folder.name, + style: AppTypography.bodyLargeStyle.copyWith( + color: dialogContext.conduitTheme.textPrimary, + fontWeight: FontWeight.w600, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: Spacing.xs), + Text( + '${folder.conversationIds.length} conversation${folder.conversationIds.length != 1 ? 's' : ''}', + style: AppTypography.bodySmallStyle.copyWith( + color: dialogContext.conduitTheme.textSecondary, + ), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: Spacing.lg), + Text( + 'Are you sure you want to delete this folder?', + style: AppTypography.bodyLargeStyle.copyWith( + color: dialogContext.conduitTheme.textPrimary, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: Spacing.sm), + Text( + folder.conversationIds.isNotEmpty + ? 'All conversations in this folder will be moved to the main chat list.' + : 'This folder is empty and will be permanently deleted.', + style: AppTypography.bodyMediumStyle.copyWith( + color: dialogContext.conduitTheme.textSecondary, + height: 1.4, + ), + ), + ], + ), + ), + + // Actions + Container( + padding: const EdgeInsets.all(Spacing.xl), + decoration: BoxDecoration( + color: dialogContext.conduitTheme.cardBackground, + borderRadius: const BorderRadius.vertical( + bottom: Radius.circular(AppBorderRadius.modal), + ), + border: Border( + top: BorderSide( + color: dialogContext.conduitTheme.dividerColor.withValues(alpha: 0.1), + width: BorderWidth.regular, + ), + ), + ), + child: Row( + children: [ + Expanded( + child: ConduitButton( + text: 'Cancel', + onPressed: () => Navigator.pop(dialogContext, false), + isSecondary: true, + ), + ), + const SizedBox(width: Spacing.md), + Expanded( + child: ConduitButton( + text: 'Delete Folder', + onPressed: () => Navigator.pop(dialogContext, true), + isDestructive: true, + icon: Platform.isIOS ? CupertinoIcons.delete : Icons.delete_outline_rounded, + ), + ), + ], + ), + ), + ], ), - FilledButton( - onPressed: () => Navigator.pop(context, true), - style: FilledButton.styleFrom( - backgroundColor: context.conduitTheme.error, - ), - child: const Text('Delete'), - ), - ], + ), + ).animate().slideY( + begin: 0.1, + duration: AnimationDuration.modalPresentation, + curve: AnimationCurves.modalPresentation, + ).fadeIn( + duration: AnimationDuration.modalPresentation, + curve: AnimationCurves.easeOut, ), - ); + ), + ); if (confirmed == true) { try { @@ -695,12 +926,12 @@ class _FolderManagementDialogState ref.invalidate(conversationsProvider); if (mounted) { - UiUtils.showMessage(context, 'Folder "${folder.name}" deleted'); + UiUtils.showMessage(widget.parentContext ?? context, 'Folder "${folder.name}" deleted'); } } } catch (e) { if (mounted) { - UiUtils.showMessage(context, 'Failed to delete folder: $e'); + UiUtils.showMessage(widget.parentContext ?? context, 'Failed to delete folder: $e'); } } } diff --git a/lib/features/chat/widgets/modern_message_bubble.dart b/lib/features/chat/widgets/modern_message_bubble.dart index b648562..705c9d0 100644 --- a/lib/features/chat/widgets/modern_message_bubble.dart +++ b/lib/features/chat/widgets/modern_message_bubble.dart @@ -303,66 +303,71 @@ class _ModernMessageBubbleState extends ConsumerState left: Spacing.xxxl, right: Spacing.xs, ), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, children: [ - Flexible( - child: GestureDetector( - onLongPress: () => _toggleActions(), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.messagePadding, - vertical: Spacing.sm, - ), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - context.conduitTheme.chatBubbleUser.withValues( - alpha: 0.95, + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Flexible( + child: GestureDetector( + onLongPress: () => _toggleActions(), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.messagePadding, + vertical: Spacing.sm, + ), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + context.conduitTheme.chatBubbleUser.withValues( + alpha: 0.95, + ), + context.conduitTheme.chatBubbleUser, + ], ), - context.conduitTheme.chatBubbleUser, - ], - ), - borderRadius: BorderRadius.circular( - AppBorderRadius.messageBubble, - ), - border: Border.all( - color: context.conduitTheme.chatBubbleUserBorder, - width: BorderWidth.regular, - ), - boxShadow: ConduitShadows.high, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Display images if any - if (widget.message.attachmentIds != null && - widget.message.attachmentIds!.isNotEmpty) - _buildAttachmentImages(), - - // Display text content if any - if (widget.message.content.isNotEmpty) ...[ + borderRadius: BorderRadius.circular( + AppBorderRadius.messageBubble, + ), + border: Border.all( + color: context.conduitTheme.chatBubbleUserBorder, + width: BorderWidth.regular, + ), + boxShadow: ConduitShadows.high, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Display images if any if (widget.message.attachmentIds != null && widget.message.attachmentIds!.isNotEmpty) - const SizedBox(height: Spacing.sm), - _buildCustomText( - widget.message.content, - context.conduitTheme.chatBubbleUserText, - ), - ], + _buildAttachmentImages(), - // Action buttons for user messages - if (_showActions) ...[ - const SizedBox(height: Spacing.md), - _buildUserActionButtons(), + // Display text content if any + if (widget.message.content.isNotEmpty) ...[ + if (widget.message.attachmentIds != null && + widget.message.attachmentIds!.isNotEmpty) + const SizedBox(height: Spacing.sm), + _buildCustomText( + widget.message.content, + context.conduitTheme.chatBubbleUserText, + ), + ], ], - ], + ), ), + ), ), - ), + ], ), + + // Action buttons below the message bubble + if (_showActions) ...[ + const SizedBox(height: Spacing.sm), + _buildUserActionButtons(), + ], ], ), ) @@ -461,16 +466,16 @@ class _ModernMessageBubbleState extends ConsumerState ] else // Fallback: show empty state for non-streaming empty messages const SizedBox.shrink(), - - // Action buttons - if (_showActions) ...[ - const SizedBox(height: Spacing.md), - _buildActionButtons(), - ], ], ), ), ), + + // Action buttons below the message content + if (_showActions) ...[ + const SizedBox(height: Spacing.sm), + _buildActionButtons(), + ], ], ), ) diff --git a/lib/features/navigation/views/chats_list_page.dart b/lib/features/navigation/views/chats_list_page.dart index c9d401d..0aa9392 100644 --- a/lib/features/navigation/views/chats_list_page.dart +++ b/lib/features/navigation/views/chats_list_page.dart @@ -13,7 +13,8 @@ import '../../../core/providers/app_providers.dart'; import '../../../shared/widgets/themed_dialogs.dart'; import '../../../shared/widgets/conduit_components.dart'; import '../../chat/providers/chat_providers.dart'; -import '../../chat/widgets/folder_management_dialog.dart'; +import '../../chat/views/chat_page_helpers.dart'; + /// Optimized conversation list page with Conduit design aesthetics class ChatsListPage extends ConsumerStatefulWidget { @@ -39,6 +40,9 @@ class _ChatsListPageState extends ConsumerState // Provider for archived section visibility static final _showArchivedProvider = StateProvider((ref) => false); + + // Provider for folder expansion state (Map) + static final _expandedFoldersProvider = StateProvider>((ref) => {}); @override bool get wantKeepAlive => true; // Keep state alive for better performance @@ -94,7 +98,7 @@ class _ChatsListPageState extends ConsumerState ], ), floatingActionButton: FloatingActionButton( - onPressed: _startNewChat, + onPressed: _showCreateMenu, backgroundColor: context.conduitTheme.buttonPrimary, foregroundColor: context.conduitTheme.buttonPrimaryText, elevation: Elevation.medium, @@ -162,62 +166,65 @@ class _ChatsListPageState extends ConsumerState _searchFocusNode.requestFocus(); }, child: Container( - margin: const EdgeInsets.all(Spacing.pagePadding), - padding: const EdgeInsets.symmetric( - horizontal: Spacing.inputPadding, - vertical: Spacing.sm, - ), + margin: const EdgeInsets.all(Spacing.md), decoration: BoxDecoration( - color: context.conduitTheme.inputBackground, - borderRadius: BorderRadius.circular(AppBorderRadius.input), + gradient: LinearGradient( + colors: [ + context.conduitTheme.inputBackground.withValues(alpha: 0.6), + context.conduitTheme.inputBackground.withValues(alpha: 0.3), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(AppBorderRadius.lg), border: Border.all( color: isFocused - ? context.conduitTheme.buttonPrimary - : context.conduitTheme.inputBorder, - width: BorderWidth.regular, + ? context.conduitTheme.buttonPrimary.withValues(alpha: 0.8) + : context.conduitTheme.inputBorder.withValues(alpha: 0.3), + width: BorderWidth.thin, ), - boxShadow: ConduitShadows.input, ), - child: Row( - children: [ - Icon( - Platform.isIOS ? CupertinoIcons.search : Icons.search_rounded, - size: IconSize.medium, + child: TextField( + controller: _searchController, + focusNode: _searchFocusNode, + style: TextStyle( + color: context.conduitTheme.inputText, + fontSize: AppTypography.bodyMedium, + ), + decoration: InputDecoration( + hintText: 'Search conversations...', + hintStyle: TextStyle( + color: context.conduitTheme.inputPlaceholder.withValues(alpha: 0.8), + fontSize: AppTypography.bodyMedium, + ), + prefixIcon: Icon( + Platform.isIOS ? CupertinoIcons.search : Icons.search, color: context.conduitTheme.iconSecondary, + size: IconSize.md, ), - const SizedBox(width: Spacing.sm), - Expanded( - child: TextField( - controller: _searchController, - focusNode: _searchFocusNode, - style: AppTypography.bodyMediumStyle.copyWith( - color: context.conduitTheme.inputText, - ), - decoration: InputDecoration( - hintText: 'Search conversations...', - hintStyle: AppTypography.bodyMediumStyle.copyWith( - color: context.conduitTheme.inputPlaceholder, - ), - border: InputBorder.none, // Remove default border - focusedBorder: - InputBorder.none, // Remove default focus border - enabledBorder: InputBorder.none, - contentPadding: EdgeInsets.zero, - ), - ), + suffixIcon: _searchQuery.isNotEmpty + ? IconButton( + icon: Icon( + Platform.isIOS + ? CupertinoIcons.clear_circled_solid + : Icons.clear, + color: context.conduitTheme.iconSecondary, + size: IconSize.md, + ), + onPressed: () { + _searchController.clear(); + _searchQuery = ''; + ref.read(searchQueryProvider.notifier).state = ''; + _searchFocusNode.unfocus(); + }, + ) + : null, + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, ), - if (_searchController.text.isNotEmpty) - ConduitIconButton( - icon: Platform.isIOS - ? CupertinoIcons.clear - : Icons.clear_rounded, - onPressed: () { - _searchController.clear(); - _searchQuery = ''; - ref.read(searchQueryProvider.notifier).state = ''; - }, - ), - ], + ), ), ), ).animate().fadeIn( @@ -243,23 +250,36 @@ class _ChatsListPageState extends ConsumerState return _buildNoResultsState(); } - // Separate conversations by status + // Separate conversations by status and folder final pinnedConversations = filteredConversations .where((c) => c.pinned == true) .toList(); final regularConversations = filteredConversations - .where((c) => c.pinned != true && c.archived != true) + .where((c) => c.pinned != true && c.archived != true && (c.folderId == null || c.folderId!.isEmpty)) + .toList(); + final folderConversations = filteredConversations + .where((c) => c.pinned != true && c.archived != true && c.folderId != null && c.folderId!.isNotEmpty) .toList(); final archivedConversations = filteredConversations .where((c) => c.archived == true) .toList(); + // Debug logging + print('🔍 DEBUG: Total conversations: ${filteredConversations.length}'); + print('🔍 DEBUG: Pinned: ${pinnedConversations.length}'); + print('🔍 DEBUG: Regular: ${regularConversations.length}'); + print('🔍 DEBUG: Folder: ${folderConversations.length}'); + print('🔍 DEBUG: Archived: ${archivedConversations.length}'); + + // Check first few conversations for folder IDs + for (int i = 0; i < filteredConversations.take(5).length; i++) { + final conv = filteredConversations[i]; + print('🔍 DEBUG: Conv ${i}: id=${conv.id.substring(0, 8)}, folderId=${conv.folderId}, pinned=${conv.pinned}, archived=${conv.archived}'); + } + return ListView( controller: _scrollController, - padding: const EdgeInsets.symmetric( - horizontal: Spacing.pagePadding, - vertical: Spacing.sm, - ), + padding: const EdgeInsets.all(Spacing.md), children: [ // Pinned conversations section if (pinnedConversations.isNotEmpty) ...[ @@ -271,7 +291,44 @@ class _ChatsListPageState extends ConsumerState isPinned: true, ); }), - const SizedBox(height: Spacing.lg), + const SizedBox(height: Spacing.md), + ], + + // Folder conversations sections (after pinned, before recent) + if (folderConversations.isNotEmpty) ...[ + ...ref.watch(foldersProvider).when( + data: (folders) { + // Group conversations by folder + final groupedByFolder = >{}; + for (final conv in folderConversations) { + if (conv.folderId != null) { + groupedByFolder.putIfAbsent(conv.folderId!, () => []).add(conv); + } + } + + // Build folder sections + return folders.where((folder) => groupedByFolder.containsKey(folder.id)).map((folder) { + final conversations = groupedByFolder[folder.id]!; + final expandedFolders = ref.watch(_expandedFoldersProvider); + final isExpanded = expandedFolders[folder.id] ?? false; + + return Column( + children: [ + _buildFolderHeader(folder.id, folder.name, conversations.length), + // Only show conversations if folder is expanded + if (isExpanded) ...[ + ...conversations.asMap().entries.map((entry) { + return _buildConversationTile(entry.value, entry.key, inFolder: true); + }), + ], + const SizedBox(height: Spacing.md), + ], + ); + }).toList(); + }, + loading: () => [const SizedBox.shrink()], + error: (_, __) => [const SizedBox.shrink()], + ), ], // Regular conversations section @@ -284,7 +341,7 @@ class _ChatsListPageState extends ConsumerState // Archived conversations section (collapsed by default) if (archivedConversations.isNotEmpty) ...[ - const SizedBox(height: Spacing.lg), + const SizedBox(height: Spacing.md), _buildArchivedSection(archivedConversations), ], ], @@ -312,177 +369,171 @@ class _ChatsListPageState extends ConsumerState int index, { bool isPinned = false, bool isArchived = false, + bool inFolder = false, }) { final isSelected = ref.watch(activeConversationProvider)?.id == conversation.id; - // TODO: Use pinned status for future conversation management features - // final conversationIsPinned = conversation.pinned ?? false; final isLoading = _isLoadingConversation && isSelected; - return Container( - margin: const EdgeInsets.only(bottom: Spacing.listGap), - decoration: BoxDecoration( - gradient: isSelected - ? LinearGradient( - colors: [ - context.conduitTheme.navigationSelectedBackground.withValues( + return PressableScale( + onTap: isLoading ? null : () => _selectConversation(conversation), + borderRadius: BorderRadius.circular(AppBorderRadius.md), + child: Container( + margin: EdgeInsets.only( + bottom: Spacing.md, + left: inFolder ? Spacing.md : 0.0, + ), + decoration: BoxDecoration( + gradient: isSelected + ? LinearGradient( + colors: [ + context.conduitTheme.buttonPrimary.withValues(alpha: 0.2), + context.conduitTheme.buttonPrimary.withValues(alpha: 0.1), + ], + ) + : null, + color: isSelected + ? null + : isArchived + ? context.conduitTheme.surfaceBackground.withValues(alpha: 0.3) + : context.conduitTheme.surfaceBackground.withValues(alpha: 0.05), + borderRadius: BorderRadius.circular(AppBorderRadius.md), + border: Border.all( + color: isSelected + ? context.conduitTheme.buttonPrimary.withValues(alpha: 0.5) + : context.conduitTheme.dividerColor, + width: BorderWidth.regular, + ), + boxShadow: isSelected ? ConduitShadows.card : null, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.md, + vertical: Spacing.sm, + ), + child: Row( + children: [ + // Conversation icon (32x32 like model selector) + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: context.conduitTheme.buttonPrimary.withValues( alpha: 0.15, ), - context.conduitTheme.navigationSelectedBackground.withValues( - alpha: 0.05, - ), - ], - ) - : null, - color: isSelected - ? null - : isArchived - ? context.conduitTheme.surfaceContainer.withValues(alpha: 0.3) - : context.conduitTheme.cardBackground, - borderRadius: BorderRadius.circular(AppBorderRadius.card), - border: Border.all( - color: isSelected - ? context.conduitTheme.navigationSelected - : isArchived - ? context.conduitTheme.dividerColor.withValues(alpha: 0.5) - : context.conduitTheme.cardBorder, - width: BorderWidth.regular, - ), - boxShadow: isSelected ? ConduitShadows.high : ConduitShadows.low, - ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: isLoading ? null : () => _selectConversation(conversation), - onLongPress: isLoading - ? null - : () => _showConversationOptions(conversation), - borderRadius: BorderRadius.circular(AppBorderRadius.card), - child: Padding( - padding: const EdgeInsets.all(Spacing.listItemPadding), - child: Row( - children: [ - // Conversation icon/avatar - Container( - width: IconSize.avatar, - height: IconSize.avatar, - decoration: BoxDecoration( - color: context.conduitTheme.buttonPrimary, - borderRadius: BorderRadius.circular(AppBorderRadius.avatar), - boxShadow: ConduitShadows.card, - ), - child: Icon( - Platform.isIOS - ? CupertinoIcons.chat_bubble - : Icons.chat_rounded, - size: IconSize.medium, - color: context.conduitTheme.buttonPrimaryText, - ), + borderRadius: BorderRadius.circular(AppBorderRadius.md), ), - const SizedBox(width: Spacing.md), - - // Conversation details - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Text( - conversation.title ?? 'New Chat', - style: AppTypography.bodyLargeStyle.copyWith( - color: isArchived - ? context.conduitTheme.textSecondary - : context.conduitTheme.textPrimary, - fontWeight: FontWeight.w600, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - if (isPinned) - Icon( - Platform.isIOS - ? CupertinoIcons.pin_fill - : Icons.push_pin, - size: IconSize.small, - color: context.conduitTheme.warning, - ), - ], - ), - const SizedBox(height: Spacing.xs), - Text( - _getConversationPreview(conversation), - style: AppTypography.bodySmallStyle.copyWith( - color: isArchived - ? context.conduitTheme.textTertiary - : context.conduitTheme.textSecondary, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: Spacing.xs), - Text( - _formatConversationDate(conversation.updatedAt), - style: AppTypography.captionStyle.copyWith( - color: isArchived - ? context.conduitTheme.textTertiary.withValues( - alpha: 0.5, - ) - : context.conduitTheme.textTertiary, - ), - ), - ], - ), + child: Icon( + Platform.isIOS + ? CupertinoIcons.chat_bubble + : Icons.chat_rounded, + color: context.conduitTheme.buttonPrimary, + size: 16, ), + ), + const SizedBox(width: Spacing.md), - // Action buttons - Column( + // Conversation details + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (isLoading) - SizedBox( - width: IconSize.medium, - height: IconSize.medium, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - context.conduitTheme.buttonPrimary, - ), - ), - ) - else ...[ - ConduitIconButton( - icon: Platform.isIOS - ? CupertinoIcons.ellipsis - : Icons.more_vert_rounded, - onPressed: () => _showConversationOptions(conversation), + Text( + conversation.title ?? 'New Chat', + style: TextStyle( + color: isArchived + ? context.conduitTheme.textSecondary + : context.conduitTheme.textPrimary, + fontWeight: FontWeight.w600, + fontSize: AppTypography.bodyMedium, ), - if (conversation.messages.isNotEmpty) - Container( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.xs, - vertical: Spacing.xxs, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: Spacing.xs), + Row( + children: [ + if (isPinned) + _buildStatusChip( + icon: Platform.isIOS + ? CupertinoIcons.pin_fill + : Icons.push_pin, + label: 'Pinned', + color: context.conduitTheme.warning, ), - decoration: BoxDecoration( - color: context.conduitTheme.buttonPrimary, - borderRadius: BorderRadius.circular( - AppBorderRadius.badge, + if (isArchived) + _buildStatusChip( + icon: Platform.isIOS + ? CupertinoIcons.archivebox_fill + : Icons.archive, + label: 'Archived', + color: context.conduitTheme.textSecondary, + ), + if (!isPinned && !isArchived) + Text( + _formatConversationDate(conversation.updatedAt), + style: TextStyle( + color: context.conduitTheme.textTertiary, + fontSize: AppTypography.labelSmall, ), ), - child: Text( - conversation.messages.length.toString(), - style: AppTypography.captionStyle.copyWith( - color: context.conduitTheme.buttonPrimaryText, - fontWeight: FontWeight.w600, - ), - ), - ), - ], + ], + ), ], ), - ], - ), + ), + + // Action indicator (like model selector check) + GestureDetector( + onTap: () => _showConversationOptions(conversation), + child: AnimatedOpacity( + opacity: isSelected ? 1 : 0.6, + duration: AnimationDuration.fast, + child: Container( + padding: const EdgeInsets.all(Spacing.xxs), + decoration: BoxDecoration( + color: isSelected + ? context.conduitTheme.buttonPrimary + : context.conduitTheme.surfaceBackground, + borderRadius: BorderRadius.circular(AppBorderRadius.md), + border: Border.all( + color: isSelected + ? context.conduitTheme.buttonPrimary.withValues( + alpha: 0.6, + ) + : context.conduitTheme.dividerColor, + ), + ), + child: isLoading + ? SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + isSelected + ? context.conduitTheme.textInverse + : context.conduitTheme.buttonPrimary, + ), + ), + ) + : Icon( + isSelected + ? (Platform.isIOS + ? CupertinoIcons.check_mark + : Icons.check) + : (Platform.isIOS + ? CupertinoIcons.ellipsis + : Icons.more_vert), + color: isSelected + ? context.conduitTheme.textInverse + : context.conduitTheme.iconSecondary, + size: 14, + ), + ), + ), + ), + ], ), ), ), @@ -495,6 +546,40 @@ class _ChatsListPageState extends ConsumerState ); } + Widget _buildStatusChip({ + required IconData icon, + required String label, + required Color color, + }) { + return Container( + margin: const EdgeInsets.only(right: Spacing.xs), + padding: const EdgeInsets.symmetric(horizontal: Spacing.xs, vertical: 2), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(AppBorderRadius.chip), + border: Border.all( + color: color.withValues(alpha: 0.3), + width: BorderWidth.thin, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 12, color: color), + const SizedBox(width: 4), + Text( + label, + style: TextStyle( + fontSize: AppTypography.labelSmall, + color: color, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } + Widget _buildEmptyState() { return Center( child: Column( @@ -713,20 +798,13 @@ class _ChatsListPageState extends ConsumerState return conversations.where((conversation) { final title = conversation.title?.toLowerCase() ?? ''; - final content = _getConversationPreview(conversation).toLowerCase(); final query = _searchQuery.toLowerCase(); - return title.contains(query) || content.contains(query); + return title.contains(query); }).toList(); } - String _getConversationPreview(dynamic conversation) { - if (conversation.messages != null && conversation.messages.isNotEmpty) { - final lastMessage = conversation.messages.last; - return lastMessage.content ?? 'No content'; - } - return 'Start a new conversation'; - } + String _formatConversationDate(DateTime? date) { if (date == null) return ''; @@ -816,24 +894,6 @@ class _ChatsListPageState extends ConsumerState ), ), // Options - ListTile( - leading: Icon( - Platform.isIOS - ? CupertinoIcons.folder - : Icons.folder_rounded, - color: context.conduitTheme.iconPrimary, - ), - title: Text( - 'Manage Folders', - style: AppTypography.bodyMediumStyle.copyWith( - color: context.conduitTheme.textPrimary, - ), - ), - onTap: () { - Navigator.pop(context); - _showFolderManagement(); - }, - ), ListTile( leading: Icon( Platform.isIOS @@ -987,24 +1047,7 @@ class _ChatsListPageState extends ConsumerState _togglePinConversation(conversation); }, ), - ListTile( - leading: Icon( - Platform.isIOS - ? CupertinoIcons.folder - : Icons.folder_rounded, - color: context.conduitTheme.iconPrimary, - ), - title: Text( - 'Move to Folder', - style: AppTypography.bodyMediumStyle.copyWith( - color: context.conduitTheme.textPrimary, - ), - ), - onTap: () { - Navigator.pop(context); - _moveToFolder(conversation); - }, - ), + ListTile( leading: Icon( Platform.isIOS @@ -1058,13 +1101,178 @@ class _ChatsListPageState extends ConsumerState } } - void _showFolderManagement() { + void _showCreateMenu() { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + builder: (context) => Container( + decoration: BoxDecoration( + color: context.conduitTheme.surfaceBackground, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(AppBorderRadius.bottomSheet), + ), + border: Border.all( + color: context.conduitTheme.dividerColor, + width: BorderWidth.regular, + ), + boxShadow: ConduitShadows.modal, + ), + child: SafeArea( + top: false, + bottom: true, + child: Padding( + padding: const EdgeInsets.all(Spacing.bottomSheetPadding), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Handle bar + Container( + width: 40, + height: 4, + margin: const EdgeInsets.only(bottom: Spacing.lg), + decoration: BoxDecoration( + color: context.conduitTheme.dividerColor, + borderRadius: BorderRadius.circular(AppBorderRadius.xs), + ), + ), + // Header + Padding( + padding: const EdgeInsets.only(bottom: Spacing.md), + child: Text( + 'Create New', + style: AppTypography.headlineSmallStyle.copyWith( + color: context.conduitTheme.textPrimary, + fontWeight: FontWeight.w600, + ), + ), + ), + // Options + ListTile( + leading: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: context.conduitTheme.buttonPrimary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(AppBorderRadius.lg), + ), + child: Icon( + Platform.isIOS ? CupertinoIcons.chat_bubble : Icons.chat_rounded, + color: context.conduitTheme.buttonPrimary, + size: IconSize.medium, + ), + ), + title: Text( + 'New Chat', + style: AppTypography.bodyLargeStyle.copyWith( + color: context.conduitTheme.textPrimary, + fontWeight: FontWeight.w600, + ), + ), + subtitle: Text( + 'Start a new conversation', + style: AppTypography.bodyMediumStyle.copyWith( + color: context.conduitTheme.textSecondary, + ), + ), + onTap: () { + Navigator.pop(context); + _startNewChat(); + }, + ), + const SizedBox(height: Spacing.sm), + ListTile( + leading: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: context.conduitTheme.info.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(AppBorderRadius.lg), + ), + child: Icon( + Platform.isIOS ? CupertinoIcons.folder_badge_plus : Icons.create_new_folder_rounded, + color: context.conduitTheme.info, + size: IconSize.medium, + ), + ), + title: Text( + 'New Folder', + style: AppTypography.bodyLargeStyle.copyWith( + color: context.conduitTheme.textPrimary, + fontWeight: FontWeight.w600, + ), + ), + subtitle: Text( + 'Create a folder to organize chats', + style: AppTypography.bodyMediumStyle.copyWith( + color: context.conduitTheme.textSecondary, + ), + ), + onTap: () { + Navigator.pop(context); + _showCreateFolderDialog(); + }, + ), + ], + ), + ), + ), + ), + ); + } + + + + void _showCreateFolderDialog() { + final nameController = TextEditingController(); + showDialog( context: context, - builder: (context) => const FolderManagementDialog(), + builder: (dialogContext) => _CreateFolderDialog( + nameController: nameController, + onCreateFolder: (name) => _createFolderFromDialog(name, dialogContext), + ), ); } + Future _createFolderFromDialog(String name, BuildContext dialogContext) async { + try { + final api = ref.read(apiServiceProvider); + if (api == null) throw Exception('No API service available'); + + await api.createFolder(name: name); + ref.invalidate(foldersProvider); + + if (mounted) { + Navigator.pop(dialogContext); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Folder "$name" created', + style: AppTypography.bodyMediumStyle.copyWith( + color: context.conduitTheme.textInverse, + ), + ), + backgroundColor: context.conduitTheme.success, + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Failed to create folder: $e', + style: AppTypography.bodyMediumStyle.copyWith( + color: context.conduitTheme.textInverse, + ), + ), + backgroundColor: context.conduitTheme.error, + ), + ); + } + } + } + void _togglePinConversation(dynamic conversation) async { try { final api = ref.read(apiServiceProvider); @@ -1107,22 +1315,7 @@ class _ChatsListPageState extends ConsumerState } } - void _moveToFolder(dynamic conversation) { - // TODO: Implement folder selection dialog - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Move to folder feature coming soon!', - style: AppTypography.bodyMediumStyle.copyWith( - color: context.conduitTheme.textInverse, - ), - ), - backgroundColor: context.conduitTheme.info, - ), - ); - } - } + void _archiveConversation(dynamic conversation) async { try { @@ -1238,8 +1431,8 @@ class _ChatsListPageState extends ConsumerState Widget _buildSectionHeader(String title, int count) { return Padding( padding: const EdgeInsets.symmetric( - horizontal: Spacing.sm, - vertical: Spacing.md, + horizontal: Spacing.md, + vertical: Spacing.sm, ), child: Row( children: [ @@ -1251,7 +1444,7 @@ class _ChatsListPageState extends ConsumerState letterSpacing: 0.5, ), ), - const SizedBox(width: Spacing.xs), + const SizedBox(width: Spacing.sm), Container( padding: const EdgeInsets.symmetric( horizontal: Spacing.xs, @@ -1274,6 +1467,71 @@ class _ChatsListPageState extends ConsumerState ); } + Widget _buildFolderHeader(String folderId, String folderName, int count) { + final expandedFolders = ref.watch(_expandedFoldersProvider); + final isExpanded = expandedFolders[folderId] ?? false; + + return GestureDetector( + onTap: () { + final currentState = ref.read(_expandedFoldersProvider); + ref.read(_expandedFoldersProvider.notifier).state = { + ...currentState, + folderId: !isExpanded, + }; + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.md, + vertical: Spacing.sm, + ), + child: Row( + children: [ + Icon( + isExpanded + ? (Platform.isIOS ? CupertinoIcons.chevron_down : Icons.expand_more_rounded) + : (Platform.isIOS ? CupertinoIcons.chevron_right : Icons.chevron_right_rounded), + size: IconSize.small, + color: context.conduitTheme.textSecondary, + ), + const SizedBox(width: Spacing.sm), + Icon( + Platform.isIOS ? CupertinoIcons.folder_fill : Icons.folder_rounded, + size: IconSize.small, + color: context.conduitTheme.textSecondary, + ), + const SizedBox(width: Spacing.sm), + Text( + folderName, + style: AppTypography.labelStyle.copyWith( + color: context.conduitTheme.textSecondary, + fontWeight: FontWeight.w600, + letterSpacing: 0.5, + ), + ), + const SizedBox(width: Spacing.sm), + Container( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.xs, + vertical: Spacing.xxs, + ), + decoration: BoxDecoration( + color: context.conduitTheme.surfaceContainer, + borderRadius: BorderRadius.circular(AppBorderRadius.badge), + ), + child: Text( + count.toString(), + style: AppTypography.captionStyle.copyWith( + color: context.conduitTheme.textTertiary, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + ); + } + Widget _buildArchivedSection(List archivedConversations) { return Consumer( builder: (context, ref, child) { @@ -1288,7 +1546,10 @@ class _ChatsListPageState extends ConsumerState }, borderRadius: BorderRadius.circular(AppBorderRadius.card), child: Padding( - padding: const EdgeInsets.all(Spacing.md), + padding: const EdgeInsets.symmetric( + horizontal: Spacing.md, + vertical: Spacing.sm, + ), child: Row( children: [ Icon( @@ -1360,3 +1621,252 @@ class _ChatsListPageState extends ConsumerState ); } } + +class _CreateFolderDialog extends StatefulWidget { + final TextEditingController nameController; + final Future Function(String name) onCreateFolder; + + const _CreateFolderDialog({ + required this.nameController, + required this.onCreateFolder, + }); + + @override + State<_CreateFolderDialog> createState() => _CreateFolderDialogState(); +} + +class _CreateFolderDialogState extends State<_CreateFolderDialog> { + bool isCreating = false; + + @override + Widget build(BuildContext context) { + return Directionality( + textDirection: TextDirection.ltr, + child: Dialog( + backgroundColor: Colors.transparent, + child: Container( + width: 400, + decoration: BoxDecoration( + color: context.conduitTheme.surfaceBackground, + borderRadius: BorderRadius.circular(AppBorderRadius.modal), + border: Border.all( + color: context.conduitTheme.cardBorder.withValues(alpha: 0.2), + width: BorderWidth.regular, + ), + boxShadow: ConduitShadows.modal, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Header + Container( + padding: const EdgeInsets.all(Spacing.xl), + decoration: BoxDecoration( + color: context.conduitTheme.cardBackground, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(AppBorderRadius.modal), + ), + border: Border( + bottom: BorderSide( + color: context.conduitTheme.dividerColor.withValues(alpha: 0.1), + width: BorderWidth.regular, + ), + ), + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: context.conduitTheme.info.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(AppBorderRadius.lg), + ), + child: Icon( + Platform.isIOS ? CupertinoIcons.folder_badge_plus : Icons.create_new_folder_rounded, + color: context.conduitTheme.info, + size: IconSize.medium, + ), + ), + const SizedBox(width: Spacing.md), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Create New Folder', + style: AppTypography.headlineSmallStyle.copyWith( + color: context.conduitTheme.textPrimary, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: Spacing.xs), + Text( + 'Enter a name for your folder', + style: AppTypography.bodyMediumStyle.copyWith( + color: context.conduitTheme.textSecondary, + ), + ), + ], + ), + ), + ConduitIconButton( + icon: Platform.isIOS ? CupertinoIcons.xmark : Icons.close_rounded, + onPressed: isCreating ? null : () => Navigator.pop(context), + ), + ], + ), + ), + + // Content + Padding( + padding: const EdgeInsets.all(Spacing.xl), + child: TextField( + controller: widget.nameController, + autofocus: true, + enabled: !isCreating, + decoration: InputDecoration( + labelText: 'Folder Name', + hintText: 'Enter folder name', + prefixIcon: Icon( + Platform.isIOS ? CupertinoIcons.folder : Icons.folder_outlined, + color: context.conduitTheme.iconSecondary, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.input), + borderSide: BorderSide( + color: context.conduitTheme.inputBorder, + width: BorderWidth.regular, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.input), + borderSide: BorderSide( + color: context.conduitTheme.buttonPrimary, + width: BorderWidth.regular, + ), + ), + labelStyle: AppTypography.bodyMediumStyle.copyWith( + color: context.conduitTheme.textSecondary, + ), + hintStyle: AppTypography.bodyMediumStyle.copyWith( + color: context.conduitTheme.inputPlaceholder, + ), + ), + style: AppTypography.bodyMediumStyle.copyWith( + color: context.conduitTheme.textPrimary, + ), + onSubmitted: (value) { + if (value.trim().isNotEmpty && !isCreating) { + _createFolder(); + } + }, + ), + ), + + // Actions + Container( + padding: const EdgeInsets.all(Spacing.xl), + decoration: BoxDecoration( + color: context.conduitTheme.cardBackground, + borderRadius: const BorderRadius.vertical( + bottom: Radius.circular(AppBorderRadius.modal), + ), + border: Border( + top: BorderSide( + color: context.conduitTheme.dividerColor.withValues(alpha: 0.1), + width: BorderWidth.regular, + ), + ), + ), + child: Row( + children: [ + Expanded( + child: TextButton( + onPressed: isCreating ? null : () => Navigator.pop(context), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: Spacing.md), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.button), + ), + ), + child: Text( + 'Cancel', + style: AppTypography.labelStyle.copyWith( + color: context.conduitTheme.textSecondary, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + const SizedBox(width: Spacing.md), + Expanded( + child: ElevatedButton( + onPressed: isCreating ? null : () { + final name = widget.nameController.text.trim(); + if (name.isNotEmpty) { + _createFolder(); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: context.conduitTheme.buttonPrimary, + foregroundColor: context.conduitTheme.buttonPrimaryText, + padding: const EdgeInsets.symmetric(vertical: Spacing.md), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.button), + ), + elevation: Elevation.none, + ), + child: isCreating + ? SizedBox( + width: IconSize.medium, + height: IconSize.medium, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + context.conduitTheme.buttonPrimaryText, + ), + ), + ) + : Text( + 'Create', + style: AppTypography.labelStyle.copyWith( + color: context.conduitTheme.buttonPrimaryText, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ), + ], + ), + ).animate().slideY( + begin: 0.1, + duration: AnimationDuration.modalPresentation, + curve: AnimationCurves.modalPresentation, + ).fadeIn( + duration: AnimationDuration.modalPresentation, + curve: AnimationCurves.easeOut, + ), + ), + ); + } + + Future _createFolder() async { + final name = widget.nameController.text.trim(); + if (name.isEmpty) return; + + setState(() => isCreating = true); + + try { + await widget.onCreateFolder(name); + } finally { + if (mounted) { + setState(() => isCreating = false); + } + } + } +} diff --git a/lib/shared/widgets/conduit_components.dart b/lib/shared/widgets/conduit_components.dart index 9205dc6..f0e1d19 100644 --- a/lib/shared/widgets/conduit_components.dart +++ b/lib/shared/widgets/conduit_components.dart @@ -483,44 +483,49 @@ class ConduitEmptyState extends StatelessWidget { return Center( child: Padding( padding: EdgeInsets.all(isCompact ? Spacing.md : Spacing.lg), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: isCompact ? IconSize.xxl : IconSize.xxl + Spacing.md, - height: isCompact ? IconSize.xxl : IconSize.xxl + Spacing.md, - decoration: BoxDecoration( - color: context.conduitTheme.surfaceBackground, - borderRadius: BorderRadius.circular(AppBorderRadius.circular), + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: isCompact ? IconSize.xxl : IconSize.xxl + Spacing.md, + height: isCompact ? IconSize.xxl : IconSize.xxl + Spacing.md, + decoration: BoxDecoration( + color: context.conduitTheme.surfaceBackground, + borderRadius: BorderRadius.circular(AppBorderRadius.circular), + ), + child: Icon( + icon, + size: isCompact ? IconSize.xl : TouchTarget.minimum, + color: context.conduitTheme.iconSecondary, + ), ), - child: Icon( - icon, - size: isCompact ? IconSize.xl : TouchTarget.minimum, - color: context.conduitTheme.iconSecondary, + SizedBox(height: isCompact ? Spacing.sm : Spacing.md), + Text( + title, + style: AppTypography.headlineSmallStyle.copyWith( + color: context.conduitTheme.textPrimary, + fontWeight: FontWeight.w600, + ), + textAlign: TextAlign.center, ), - ), - SizedBox(height: isCompact ? Spacing.sm : Spacing.md), - Text( - title, - style: AppTypography.headlineSmallStyle.copyWith( - color: context.conduitTheme.textPrimary, - fontWeight: FontWeight.w600, + SizedBox(height: Spacing.sm), + Text( + message, + style: AppTypography.standard.copyWith( + color: context.conduitTheme.textSecondary, + ), + textAlign: TextAlign.center, + maxLines: isCompact ? 2 : null, + overflow: isCompact ? TextOverflow.ellipsis : null, ), - textAlign: TextAlign.center, - ), - SizedBox(height: Spacing.sm), - Text( - message, - style: AppTypography.standard.copyWith( - color: context.conduitTheme.textSecondary, - ), - textAlign: TextAlign.center, - ), - if (action != null) ...[ - SizedBox(height: isCompact ? Spacing.md : Spacing.lg), - action!, + if (action != null) ...[ + SizedBox(height: isCompact ? Spacing.md : Spacing.lg), + action!, + ], ], - ], + ), ), ), );