diff --git a/lib/core/services/api_service.dart b/lib/core/services/api_service.dart index e1eeecb..a43ff4c 100644 --- a/lib/core/services/api_service.dart +++ b/lib/core/services/api_service.dart @@ -1773,19 +1773,12 @@ class ApiService { return response.data; } on DioException catch (e) { debugPrint('DEBUG: images/generations failed: ${e.response?.statusCode}'); - // Fallback to singular path some servers use - final response = await _dio.post( - '/api/v1/image/generations', - data: { - 'prompt': prompt, - if (model != null) 'model': model, - if (width != null) 'width': width, - if (height != null) 'height': height, - if (steps != null) 'steps': steps, - if (guidance != null) 'guidance': guidance, - }, + DebugLogger.error( + 'Image generation request to /api/v1/images/generations failed', + e, ); - return response.data; + // Do not attempt singular fallback here - surface the original error + rethrow; } } diff --git a/lib/core/services/deep_link_service.dart b/lib/core/services/deep_link_service.dart deleted file mode 100644 index 0e781e7..0000000 --- a/lib/core/services/deep_link_service.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -import '../../features/chat/views/chat_page.dart'; -import '../../features/files/views/workspace_page.dart'; -import '../../features/profile/views/profile_page.dart'; - -/// Service for handling deep links and navigation routing -class DeepLinkService { - /// Route to chat tab - static void navigateToChat(BuildContext context) { - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const ChatPage()), - (route) => false, - ); - } - - /// In single-screen mode, workspace/profile deep links route via navigator - static void navigateToWorkspace(BuildContext context) { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const WorkspacePage()), - ); - } - - static void navigateToProfile(BuildContext context) { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const ProfilePage()), - ); - } - - /// Parse route and determine target tab - static String? parsePath(String route) { - switch (route) { - case '/chat': - case '/main/chat': - return '/chat'; - // Support both new and legacy paths for workspace - case '/workspace': - case '/main/workspace': - case '/files': // legacy - case '/main/files': // legacy - return '/workspace'; - case '/profile': - case '/main/profile': - return '/profile'; - default: - return null; - } - } - - /// Handle deep link navigation - static Widget handleDeepLink(String route) { - final path = parsePath(route); - switch (path) { - case '/workspace': - return const WorkspacePage(); - case '/profile': - return const ProfilePage(); - case '/chat': - default: - return const ChatPage(); - } - } -} - -/// Provider for deep link navigation -final deepLinkProvider = Provider((ref) => DeepLinkService()); diff --git a/lib/core/services/error_handling_service.dart b/lib/core/services/error_handling_service.dart deleted file mode 100644 index 07c0b40..0000000 --- a/lib/core/services/error_handling_service.dart +++ /dev/null @@ -1,241 +0,0 @@ -import 'package:flutter/material.dart'; -import '../../shared/theme/theme_extensions.dart'; -import '../../shared/widgets/themed_dialogs.dart'; -import 'user_friendly_error_handler.dart'; - -class ErrorHandlingService { - static final _userFriendlyHandler = UserFriendlyErrorHandler(); - - static String getErrorMessage(dynamic error) { - // Use the enhanced user-friendly error handler - return _userFriendlyHandler.getUserMessage(error); - } - - /// Get recovery actions for an error - static List getRecoveryActions(dynamic error) { - return _userFriendlyHandler.getRecoveryActions(error); - } - - static void showErrorSnackBar( - BuildContext context, - dynamic error, { - VoidCallback? onRetry, - String? customMessage, - }) { - if (customMessage != null) { - // Use custom message if provided - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(customMessage), - backgroundColor: context.conduitTheme.error, - behavior: SnackBarBehavior.floating, - action: onRetry != null - ? SnackBarAction( - label: 'Retry', - textColor: context.conduitTheme.textInverse, - onPressed: onRetry, - ) - : null, - duration: const Duration(seconds: 4), - ), - ); - } else { - // Use enhanced error handler - _userFriendlyHandler.showErrorSnackbar(context, error, onRetry: onRetry); - } - } - - /// Show enhanced error dialog with recovery options - static Future showErrorDialog( - BuildContext context, - dynamic error, { - VoidCallback? onRetry, - bool showDetails = false, - }) async { - return _userFriendlyHandler.showErrorDialog( - context, - error, - onRetry: onRetry, - showDetails: showDetails, - ); - } - - static void showSuccessSnackBar( - BuildContext context, - String message, { - Duration? duration, - }) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(message), - backgroundColor: context.conduitTheme.success, - behavior: SnackBarBehavior.floating, - duration: duration ?? const Duration(seconds: 2), - ), - ); - } - - static Future showConfirmationDialog( - BuildContext context, { - required String title, - required String content, - String confirmText = 'Confirm', - String cancelText = 'Cancel', - bool isDestructive = false, - }) async { - return await ThemedDialogs.confirm( - context, - title: title, - message: content, - confirmText: confirmText, - cancelText: cancelText, - isDestructive: isDestructive, - ); - } - - static Widget buildErrorWidget({ - required String message, - VoidCallback? onRetry, - IconData? icon, - dynamic error, - }) { - if (error != null) { - // Use enhanced error handler for full error objects - return _userFriendlyHandler.buildErrorWidget(error, onRetry: onRetry); - } - - // Fallback to legacy implementation for string messages - return Builder( - builder: (context) { - final theme = Theme.of(context); - return Center( - child: Padding( - padding: const EdgeInsets.all(Spacing.lg), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - icon ?? Icons.error_outline, - size: Spacing.xxxl, - color: theme.colorScheme.error, - ), - const SizedBox(height: Spacing.md), - Text( - 'Something went wrong', - style: theme.textTheme.headlineSmall?.copyWith( - color: theme.colorScheme.error, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: Spacing.sm), - Text( - message, - style: theme.textTheme.bodyMedium, - textAlign: TextAlign.center, - ), - if (onRetry != null) ...[ - const SizedBox(height: Spacing.lg), - ElevatedButton.icon( - onPressed: onRetry, - icon: const Icon(Icons.refresh), - label: const Text('Try Again'), - ), - ], - ], - ), - ), - ); - }, - ); - } - - /// Build enhanced error widget with recovery actions - static Widget buildEnhancedErrorWidget( - dynamic error, { - VoidCallback? onRetry, - VoidCallback? onDismiss, - bool showDetails = false, - }) { - return _userFriendlyHandler.buildErrorWidget( - error, - onRetry: onRetry, - onDismiss: onDismiss, - showDetails: showDetails, - ); - } - - static Widget buildLoadingWidget({String? message}) { - return Builder( - builder: (context) { - final theme = Theme.of(context); - return Center( - child: Padding( - padding: const EdgeInsets.all(Spacing.lg), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator(color: theme.colorScheme.primary), - if (message != null) ...[ - const SizedBox(height: Spacing.md), - Text( - message, - style: theme.textTheme.bodyMedium, - textAlign: TextAlign.center, - ), - ], - ], - ), - ), - ); - }, - ); - } - - static Widget buildEmptyStateWidget({ - required String title, - required String message, - IconData? icon, - Widget? action, - }) { - return Builder( - builder: (context) { - final theme = Theme.of(context); - return Center( - child: Padding( - padding: const EdgeInsets.all(Spacing.lg), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - icon ?? Icons.inbox_outlined, - size: Spacing.xxxl, - color: theme.colorScheme.onSurface.withValues(alpha: 0.4), - ), - const SizedBox(height: Spacing.md), - Text( - title, - style: theme.textTheme.headlineSmall?.copyWith( - color: theme.colorScheme.onSurface.withValues(alpha: 0.7), - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: Spacing.sm), - Text( - message, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurface.withValues(alpha: 0.6), - ), - textAlign: TextAlign.center, - ), - if (action != null) ...[ - const SizedBox(height: Spacing.lg), - action, - ], - ], - ), - ), - ); - }, - ); - } -} diff --git a/lib/core/services/error_recovery_service.dart b/lib/core/services/error_recovery_service.dart deleted file mode 100644 index 58b3b7e..0000000 --- a/lib/core/services/error_recovery_service.dart +++ /dev/null @@ -1,373 +0,0 @@ -import 'dart:async'; -import 'dart:math'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:dio/dio.dart'; -import '../../shared/theme/theme_extensions.dart'; - -/// Enhanced error recovery service with retry strategies and user feedback -class ErrorRecoveryService { - final Map _retryConfigs = {}; - final Map _lastRetryTimes = {}; - - ErrorRecoveryService(Dio dio); - - /// Execute an operation with automatic retry and recovery - Future executeWithRecovery({ - required String operationId, - required Future Function() operation, - RetryConfig? retryConfig, - RecoveryAction? recoveryAction, - }) async { - final config = retryConfig ?? RetryConfig.defaultConfig(); - _retryConfigs[operationId] = config; - - int attempts = 0; - Exception? lastError; - - while (attempts < config.maxRetries) { - try { - final result = await operation(); - _clearRetryState(operationId); - return result; - } catch (error) { - attempts++; - lastError = error is Exception ? error : Exception(error.toString()); - - final shouldRetry = _shouldRetry(error, attempts, config); - if (!shouldRetry || attempts >= config.maxRetries) { - break; - } - - // Execute recovery action if provided - if (recoveryAction != null) { - try { - await recoveryAction.execute(error, attempts); - } catch (recoveryError) { - // Recovery action failed, continue with retry - } - } - - // Wait before retry with exponential backoff - final delay = _calculateRetryDelay(attempts, config); - await Future.delayed(delay); - } - } - - _clearRetryState(operationId); - throw ErrorRecoveryException(lastError!, attempts); - } - - /// Check if we should retry based on error type and configuration - bool _shouldRetry(dynamic error, int attempts, RetryConfig config) { - if (attempts >= config.maxRetries) return false; - - // Check cooldown period - final lastRetry = _lastRetryTimes[config.operationId]; - if (lastRetry != null) { - final timeSinceLastRetry = DateTime.now().difference(lastRetry); - if (timeSinceLastRetry < config.cooldownPeriod) { - return false; - } - } - - // Network errors are usually retryable - if (error is DioException) { - switch (error.type) { - case DioExceptionType.connectionTimeout: - case DioExceptionType.sendTimeout: - case DioExceptionType.receiveTimeout: - case DioExceptionType.connectionError: - return true; - case DioExceptionType.badResponse: - // Retry on server errors (5xx) but not client errors (4xx) - final statusCode = error.response?.statusCode; - return statusCode != null && statusCode >= 500; - default: - return false; - } - } - - // Check custom retry conditions - return config.retryCondition?.call(error) ?? false; - } - - Duration _calculateRetryDelay(int attempt, RetryConfig config) { - if (config.retryStrategy == RetryStrategy.exponentialBackoff) { - final baseDelay = config.baseDelay.inMilliseconds; - final delay = baseDelay * pow(2, attempt - 1); - final jitter = Random().nextDouble() * 0.1 * delay; // Add 10% jitter - return Duration(milliseconds: (delay + jitter).round()); - } else { - return config.baseDelay; - } - } - - void _clearRetryState(String operationId) { - _retryConfigs.remove(operationId); - _lastRetryTimes.remove(operationId); - } - - /// Get user-friendly error message - String getErrorMessage(dynamic error) { - if (error is ErrorRecoveryException) { - return _getRecoveryErrorMessage(error); - } - - if (error is DioException) { - switch (error.type) { - case DioExceptionType.connectionTimeout: - return 'The connection is taking too long. Please check your internet and try again.'; - case DioExceptionType.sendTimeout: - return 'Failed to send your request. Please try again.'; - case DioExceptionType.receiveTimeout: - return 'The server is taking too long to respond. Please try again.'; - case DioExceptionType.connectionError: - return 'Unable to connect. Please check your internet connection.'; - case DioExceptionType.badResponse: - final statusCode = error.response?.statusCode; - if (statusCode == 401) { - return 'Your session has expired. Please sign in again.'; - } else if (statusCode == 403) { - return 'You don\'t have permission to perform this action.'; - } else if (statusCode == 404) { - return 'The requested resource was not found.'; - } else if (statusCode != null && statusCode >= 500) { - return 'The server is experiencing issues. Please try again later.'; - } - return 'Something went wrong with your request.'; - case DioExceptionType.cancel: - return 'The request was cancelled.'; - case DioExceptionType.badCertificate: - return 'There\'s a security issue with the connection.'; - case DioExceptionType.unknown: - return 'Something unexpected happened. Please try again.'; - } - } - - return error.toString(); - } - - String _getRecoveryErrorMessage(ErrorRecoveryException error) { - final attempts = error.attempts; - final originalError = getErrorMessage(error.originalError); - - return 'Failed after $attempts attempts: $originalError'; - } -} - -/// Configuration for retry behavior -class RetryConfig { - final String operationId; - final int maxRetries; - final Duration baseDelay; - final Duration cooldownPeriod; - final RetryStrategy retryStrategy; - final bool Function(dynamic error)? retryCondition; - - const RetryConfig({ - required this.operationId, - this.maxRetries = 3, - this.baseDelay = const Duration(seconds: 1), - this.cooldownPeriod = const Duration(seconds: 5), - this.retryStrategy = RetryStrategy.exponentialBackoff, - this.retryCondition, - }); - - static RetryConfig defaultConfig() => const RetryConfig( - operationId: 'default', - maxRetries: 3, - baseDelay: Duration(seconds: 1), - retryStrategy: RetryStrategy.exponentialBackoff, - ); - - static RetryConfig networkConfig() => const RetryConfig( - operationId: 'network', - maxRetries: 5, - baseDelay: Duration(milliseconds: 500), - retryStrategy: RetryStrategy.exponentialBackoff, - ); - - static RetryConfig chatConfig() => const RetryConfig( - operationId: 'chat', - maxRetries: 3, - baseDelay: Duration(seconds: 2), - retryStrategy: RetryStrategy.exponentialBackoff, - ); -} - -enum RetryStrategy { fixed, exponentialBackoff } - -/// Recovery action to execute between retries -abstract class RecoveryAction { - Future execute(dynamic error, int attempt); -} - -/// Reconnect to server recovery action -class ReconnectAction extends RecoveryAction { - final Future Function() reconnectFunction; - - ReconnectAction(this.reconnectFunction); - - @override - Future execute(dynamic error, int attempt) async { - if (attempt == 1) { - // Only try to reconnect on the first retry - await reconnectFunction(); - } - } -} - -/// Refresh token recovery action -class RefreshTokenAction extends RecoveryAction { - final Future Function() refreshFunction; - - RefreshTokenAction(this.refreshFunction); - - @override - Future execute(dynamic error, int attempt) async { - if (error is DioException && error.response?.statusCode == 401) { - await refreshFunction(); - } - } -} - -/// Clear cache recovery action -class ClearCacheAction extends RecoveryAction { - final Future Function() clearCacheFunction; - - ClearCacheAction(this.clearCacheFunction); - - @override - Future execute(dynamic error, int attempt) async { - if (attempt == 2) { - // Clear cache on second attempt - await clearCacheFunction(); - } - } -} - -/// Error recovery exception -class ErrorRecoveryException implements Exception { - final Exception originalError; - final int attempts; - - const ErrorRecoveryException(this.originalError, this.attempts); - - @override - String toString() => - 'ErrorRecoveryException: $originalError (after $attempts attempts)'; -} - -/// Providers -final errorRecoveryServiceProvider = Provider((ref) { - // This should use the same Dio instance as the API service - final dio = Dio(); // Replace with actual Dio provider - return ErrorRecoveryService(dio); -}); - -/// Error boundary widget for handling UI errors -class ErrorBoundary extends StatefulWidget { - final Widget child; - final Widget Function(Object error, VoidCallback retry)? errorBuilder; - final void Function(Object error, StackTrace stackTrace)? onError; - - const ErrorBoundary({ - super.key, - required this.child, - this.errorBuilder, - this.onError, - }); - - @override - State createState() => _ErrorBoundaryState(); -} - -class _ErrorBoundaryState extends State { - Object? error; - StackTrace? stackTrace; - - @override - Widget build(BuildContext context) { - if (error != null) { - return widget.errorBuilder?.call(error!, _retry) ?? - _buildDefaultErrorWidget(); - } - - return ErrorDetector( - onError: (error, stackTrace) { - setState(() { - this.error = error; - this.stackTrace = stackTrace; - }); - widget.onError?.call(error, stackTrace); - }, - child: widget.child, - ); - } - - Widget _buildDefaultErrorWidget() { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.error_outline, - size: Spacing.xxxl, - color: context.conduitTheme.error, - ), - const SizedBox(height: Spacing.md), - const Text( - 'Something went wrong', - style: TextStyle( - fontSize: AppTypography.headlineSmall, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: Spacing.sm), - Text( - error.toString(), - textAlign: TextAlign.center, - style: TextStyle(color: context.conduitTheme.textSecondary), - ), - const SizedBox(height: Spacing.md), - ElevatedButton(onPressed: _retry, child: const Text('Try Again')), - ], - ), - ); - } - - void _retry() { - setState(() { - error = null; - stackTrace = null; - }); - } -} - -/// Widget to detect and handle errors in child widgets -class ErrorDetector extends StatefulWidget { - final Widget child; - final void Function(Object error, StackTrace stackTrace) onError; - - const ErrorDetector({super.key, required this.child, required this.onError}); - - @override - State createState() => _ErrorDetectorState(); -} - -class _ErrorDetectorState extends State { - @override - Widget build(BuildContext context) { - return widget.child; - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - // Set up error handling - FlutterError.onError = (details) { - widget.onError(details.exception, details.stack ?? StackTrace.current); - }; - } -} diff --git a/lib/core/services/focus_management_service.dart b/lib/core/services/focus_management_service.dart deleted file mode 100644 index f1966a2..0000000 --- a/lib/core/services/focus_management_service.dart +++ /dev/null @@ -1,408 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/semantics.dart'; - -/// Comprehensive focus management service for accessibility -class FocusManagementService { - static final Map _focusNodes = {}; - static final Map _disposedNodes = {}; - static FocusNode? _lastFocusedNode; - static final List _focusHistory = []; - - /// Register a focus node with a unique identifier - static FocusNode registerFocusNode( - String identifier, { - String? debugLabel, - FocusOnKeyEventCallback? onKeyEvent, - bool skipTraversal = false, - bool canRequestFocus = true, - }) { - // Check if node already exists - if (_focusNodes.containsKey(identifier)) { - return _focusNodes[identifier]!; - } - - // Create new focus node - final focusNode = FocusNode( - debugLabel: debugLabel ?? identifier, - onKeyEvent: onKeyEvent, - skipTraversal: skipTraversal, - canRequestFocus: canRequestFocus, - ); - - // Add listener to track focus changes - focusNode.addListener(() { - if (focusNode.hasFocus) { - _onFocusChanged(focusNode); - } - }); - - _focusNodes[identifier] = focusNode; - return focusNode; - } - - /// Get a registered focus node - static FocusNode? getFocusNode(String identifier) { - return _focusNodes[identifier]; - } - - /// Dispose a focus node - static void disposeFocusNode(String identifier) { - final node = _focusNodes.remove(identifier); - if (node != null) { - _disposedNodes[identifier] = node; - node.dispose(); - } - } - - /// Dispose all focus nodes - static void disposeAll() { - for (final node in _focusNodes.values) { - node.dispose(); - } - _focusNodes.clear(); - _focusHistory.clear(); - _lastFocusedNode = null; - } - - /// Request focus for a specific node - static void requestFocus(String identifier) { - final node = _focusNodes[identifier]; - if (node != null && node.canRequestFocus) { - node.requestFocus(); - HapticFeedback.selectionClick(); - } - } - - /// Unfocus current focus - static void unfocus( - BuildContext context, { - UnfocusDisposition disposition = UnfocusDisposition.scope, - }) { - FocusScope.of(context).unfocus(disposition: disposition); - } - - /// Move focus to next focusable element - static bool nextFocus(BuildContext context) { - return FocusScope.of(context).nextFocus(); - } - - /// Move focus to previous focusable element - static bool previousFocus(BuildContext context) { - return FocusScope.of(context).previousFocus(); - } - - /// Track focus changes - static void _onFocusChanged(FocusNode node) { - _lastFocusedNode = node; - _focusHistory.add(node); - - // Limit history size - if (_focusHistory.length > 10) { - _focusHistory.removeAt(0); - } - } - - /// Restore last focus - static void restoreLastFocus() { - if (_lastFocusedNode != null && _lastFocusedNode!.canRequestFocus) { - _lastFocusedNode!.requestFocus(); - } - } - - /// Get focus history - static List getFocusHistory() { - return List.unmodifiable(_focusHistory); - } - - /// Create a focus trap for modal dialogs - static Widget createFocusTrap({ - required Widget child, - bool autofocus = true, - }) { - return FocusScope(autofocus: autofocus, child: child); - } - - /// Create keyboard navigation handler - static FocusOnKeyEventCallback createKeyboardNavigationHandler({ - VoidCallback? onEnter, - VoidCallback? onEscape, - VoidCallback? onTab, - VoidCallback? onArrowUp, - VoidCallback? onArrowDown, - VoidCallback? onArrowLeft, - VoidCallback? onArrowRight, - }) { - return (FocusNode node, KeyEvent event) { - if (event is! KeyDownEvent) { - return KeyEventResult.ignored; - } - - final key = event.logicalKey; - - if (key == LogicalKeyboardKey.enter || - key == LogicalKeyboardKey.numpadEnter) { - onEnter?.call(); - return KeyEventResult.handled; - } - - if (key == LogicalKeyboardKey.escape) { - onEscape?.call(); - return KeyEventResult.handled; - } - - if (key == LogicalKeyboardKey.tab) { - onTab?.call(); - return KeyEventResult.handled; - } - - if (key == LogicalKeyboardKey.arrowUp) { - onArrowUp?.call(); - return KeyEventResult.handled; - } - - if (key == LogicalKeyboardKey.arrowDown) { - onArrowDown?.call(); - return KeyEventResult.handled; - } - - if (key == LogicalKeyboardKey.arrowLeft) { - onArrowLeft?.call(); - return KeyEventResult.handled; - } - - if (key == LogicalKeyboardKey.arrowRight) { - onArrowRight?.call(); - return KeyEventResult.handled; - } - - return KeyEventResult.ignored; - }; - } -} - -/// Focus manager widget that manages focus for its children -class FocusManager extends StatefulWidget { - final Widget child; - final bool autofocus; - final bool trapFocus; - final FocusOnKeyEventCallback? onKeyEvent; - - const FocusManager({ - super.key, - required this.child, - this.autofocus = false, - this.trapFocus = false, - this.onKeyEvent, - }); - - @override - State createState() => _FocusManagerState(); -} - -class _FocusManagerState extends State { - late FocusScopeNode _focusScopeNode; - - @override - void initState() { - super.initState(); - _focusScopeNode = FocusScopeNode( - debugLabel: 'FocusManager', - onKeyEvent: widget.onKeyEvent, - ); - } - - @override - void dispose() { - _focusScopeNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - Widget child = FocusScope( - node: _focusScopeNode, - autofocus: widget.autofocus, - child: widget.child, - ); - - if (widget.trapFocus) { - child = FocusTraversalGroup( - policy: OrderedTraversalPolicy(), - child: child, - ); - } - - return child; - } -} - -/// Accessible form field with proper focus management -class AccessibleFormField extends StatefulWidget { - final String label; - final String? hint; - final TextEditingController controller; - final String? Function(String?)? validator; - final TextInputType? keyboardType; - final bool obscureText; - final bool autofocus; - final String? semanticLabel; - final String? errorSemanticLabel; - final ValueChanged? onChanged; - final VoidCallback? onEditingComplete; - final ValueChanged? onSubmitted; - final List? inputFormatters; - final int? maxLines; - final int? maxLength; - final bool enabled; - final Widget? suffixIcon; - final Widget? prefixIcon; - final FocusNode? focusNode; - - const AccessibleFormField({ - super.key, - required this.label, - this.hint, - required this.controller, - this.validator, - this.keyboardType, - this.obscureText = false, - this.autofocus = false, - this.semanticLabel, - this.errorSemanticLabel, - this.onChanged, - this.onEditingComplete, - this.onSubmitted, - this.inputFormatters, - this.maxLines = 1, - this.maxLength, - this.enabled = true, - this.suffixIcon, - this.prefixIcon, - this.focusNode, - }); - - @override - State createState() => _AccessibleFormFieldState(); -} - -class _AccessibleFormFieldState extends State { - late FocusNode _focusNode; - String? _errorText; - bool _hasFocus = false; - - @override - void initState() { - super.initState(); - _focusNode = widget.focusNode ?? FocusNode(debugLabel: widget.label); - _focusNode.addListener(_onFocusChanged); - } - - @override - void dispose() { - if (widget.focusNode == null) { - _focusNode.dispose(); - } - super.dispose(); - } - - void _onFocusChanged() { - setState(() { - _hasFocus = _focusNode.hasFocus; - }); - - // Announce focus change for screen readers - if (_hasFocus) { - final announcement = - widget.semanticLabel ?? - '${widget.label} text field. ${widget.hint ?? ''}'; - SemanticsService.announce(announcement, TextDirection.ltr); - } - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return Semantics( - label: widget.semanticLabel ?? widget.label, - hint: widget.hint, - textField: true, - enabled: widget.enabled, - focusable: true, - focused: _hasFocus, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Label - Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: Text( - widget.label, - style: theme.textTheme.bodyMedium?.copyWith( - color: _hasFocus - ? theme.colorScheme.primary - : theme.colorScheme.onSurface, - fontWeight: _hasFocus ? FontWeight.w600 : FontWeight.normal, - ), - ), - ), - - // Text field - TextFormField( - controller: widget.controller, - focusNode: _focusNode, - validator: (value) { - final error = widget.validator?.call(value); - setState(() { - _errorText = error; - }); - - // Announce error for screen readers - if (error != null) { - final errorAnnouncement = - widget.errorSemanticLabel ?? 'Error: $error'; - SemanticsService.announce(errorAnnouncement, TextDirection.ltr); - } - - return error; - }, - keyboardType: widget.keyboardType, - obscureText: widget.obscureText, - autofocus: widget.autofocus, - onChanged: widget.onChanged, - onEditingComplete: widget.onEditingComplete, - onFieldSubmitted: widget.onSubmitted, - inputFormatters: widget.inputFormatters, - maxLines: widget.maxLines, - maxLength: widget.maxLength, - enabled: widget.enabled, - decoration: InputDecoration( - hintText: widget.hint, - errorText: _errorText, - suffixIcon: widget.suffixIcon, - prefixIcon: widget.prefixIcon, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide( - color: theme.colorScheme.primary, - width: 2, - ), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide( - color: theme.colorScheme.error, - width: 2, - ), - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/core/services/navigation_state_service.dart b/lib/core/services/navigation_state_service.dart deleted file mode 100644 index f9a3ac5..0000000 --- a/lib/core/services/navigation_state_service.dart +++ /dev/null @@ -1,430 +0,0 @@ -import 'dart:convert'; -import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import '../utils/debug_logger.dart'; - -/// Navigation state data model -class NavigationState { - final String routeName; - final Map arguments; - final DateTime timestamp; - final String? conversationId; - final int? tabIndex; - - NavigationState({ - required this.routeName, - this.arguments = const {}, - DateTime? timestamp, - this.conversationId, - this.tabIndex, - }) : timestamp = timestamp ?? DateTime.now(); - - Map toJson() => { - 'routeName': routeName, - 'arguments': arguments, - 'timestamp': timestamp.toIso8601String(), - 'conversationId': conversationId, - 'tabIndex': tabIndex, - }; - - factory NavigationState.fromJson(Map json) { - return NavigationState( - routeName: json['routeName'] ?? '/', - arguments: json['arguments'] ?? {}, - timestamp: DateTime.tryParse(json['timestamp'] ?? '') ?? DateTime.now(), - conversationId: json['conversationId'], - tabIndex: json['tabIndex'], - ); - } -} - -/// Service to manage navigation state preservation and restoration -class NavigationStateService { - static final NavigationStateService _instance = - NavigationStateService._internal(); - factory NavigationStateService() => _instance; - NavigationStateService._internal(); - - static const String _navigationStackKey = 'navigation_stack'; - static const String _currentStateKey = 'current_navigation_state'; - static const String _deepLinkStateKey = 'deep_link_state'; - - SharedPreferences? _prefs; - final List _navigationStack = []; - NavigationState? _currentState; - final ValueNotifier _stateNotifier = ValueNotifier(null); - - /// Initialize the service - Future initialize() async { - try { - _prefs = await SharedPreferences.getInstance(); - await _loadNavigationState(); - DebugLogger.navigation('NavigationStateService initialized'); - } catch (e) { - DebugLogger.error('Failed to initialize NavigationStateService', e); - } - } - - /// Get current navigation state as a ValueNotifier for listening to changes - ValueNotifier get stateNotifier => _stateNotifier; - - /// Get current navigation state - NavigationState? get currentState => _currentState; - - /// Get navigation stack - List get navigationStack => - List.unmodifiable(_navigationStack); - - /// Push a new navigation state - Future pushState({ - required String routeName, - Map arguments = const {}, - String? conversationId, - int? tabIndex, - }) async { - try { - final state = NavigationState( - routeName: routeName, - arguments: arguments, - conversationId: conversationId, - tabIndex: tabIndex, - ); - - _navigationStack.add(state); - _currentState = state; - _stateNotifier.value = state; - - await _saveNavigationState(); - - DebugLogger.navigation('Navigation state pushed - ${state.routeName}'); - } catch (e) { - DebugLogger.error('Failed to push navigation state', e); - } - } - - /// Pop the last navigation state - Future popState() async { - try { - if (_navigationStack.isEmpty) return null; - - final poppedState = _navigationStack.removeLast(); - _currentState = _navigationStack.isNotEmpty - ? _navigationStack.last - : null; - _stateNotifier.value = _currentState; - - await _saveNavigationState(); - - DebugLogger.navigation( - 'Navigation state popped - ${poppedState.routeName}', - ); - return poppedState; - } catch (e) { - DebugLogger.error('Failed to pop navigation state', e); - return null; - } - } - - /// Update current state with new information - Future updateCurrentState({ - String? conversationId, - int? tabIndex, - Map? additionalArgs, - }) async { - try { - if (_currentState == null) return; - - final updatedArgs = { - ..._currentState!.arguments, - if (additionalArgs != null) ...additionalArgs, - }; - - final updatedState = NavigationState( - routeName: _currentState!.routeName, - arguments: updatedArgs, - conversationId: conversationId ?? _currentState!.conversationId, - tabIndex: tabIndex ?? _currentState!.tabIndex, - timestamp: _currentState!.timestamp, - ); - - // Update both current state and last item in stack - _currentState = updatedState; - if (_navigationStack.isNotEmpty) { - _navigationStack[_navigationStack.length - 1] = updatedState; - } - - _stateNotifier.value = updatedState; - await _saveNavigationState(); - - DebugLogger.navigation('Navigation state updated'); - } catch (e) { - DebugLogger.error('Failed to update navigation state', e); - } - } - - /// Clear navigation stack but preserve current state - Future clearStack() async { - try { - _navigationStack.clear(); - if (_currentState != null) { - _navigationStack.add(_currentState!); - } - await _saveNavigationState(); - DebugLogger.navigation('Navigation stack cleared'); - } catch (e) { - DebugLogger.error('Failed to clear navigation stack', e); - } - } - - /// Replace entire navigation stack - Future replaceStack(List newStack) async { - try { - _navigationStack.clear(); - _navigationStack.addAll(newStack); - _currentState = newStack.isNotEmpty ? newStack.last : null; - _stateNotifier.value = _currentState; - - await _saveNavigationState(); - DebugLogger.navigation( - 'Navigation stack replaced with ${newStack.length} states', - ); - } catch (e) { - DebugLogger.error('Failed to replace navigation stack', e); - } - } - - /// Handle deep link by preserving navigation context - Future handleDeepLink({ - required String routeName, - Map arguments = const {}, - String? conversationId, - bool preserveStack = true, - }) async { - try { - // Save deep link state for restoration - final deepLinkState = NavigationState( - routeName: routeName, - arguments: arguments, - conversationId: conversationId, - ); - - await _saveDeepLinkState(deepLinkState); - - if (preserveStack) { - // Add to existing stack instead of replacing - await pushState( - routeName: routeName, - arguments: arguments, - conversationId: conversationId, - ); - } else { - // Replace stack with deep link - await replaceStack([deepLinkState]); - } - - DebugLogger.navigation('Deep link handled - $routeName'); - } catch (e) { - DebugLogger.error('Failed to handle deep link', e); - } - } - - /// Get the conversation context from current navigation state - String? getConversationContext() { - return _currentState?.conversationId; - } - - /// Get the current tab index - int? getCurrentTabIndex() { - return _currentState?.tabIndex; - } - - /// Generate breadcrumb navigation based on current stack - List generateBreadcrumbs() { - final breadcrumbs = []; - - for (int i = 0; i < _navigationStack.length; i++) { - final state = _navigationStack[i]; - final isLast = i == _navigationStack.length - 1; - - breadcrumbs.add( - NavigationBreadcrumb( - title: _getRouteTitle(state.routeName), - routeName: state.routeName, - arguments: state.arguments, - isActive: isLast, - canNavigateBack: i > 0, - ), - ); - } - - return breadcrumbs; - } - - /// Check if we can navigate back - bool canGoBack() { - return _navigationStack.length > 1; - } - - /// Get previous state without popping - NavigationState? getPreviousState() { - if (_navigationStack.length < 2) return null; - return _navigationStack[_navigationStack.length - 2]; - } - - /// Restore navigation state on app startup - Future restoreNavigationState(NavigatorState navigator) async { - try { - await _loadNavigationState(); - - if (_currentState != null) { - // Attempt to restore to the last known state - DebugLogger.navigation( - 'Restoring navigation to ${_currentState!.routeName}', - ); - - // This would need to be implemented based on your routing setup - // navigator.pushNamedAndRemoveUntil( - // _currentState!.routeName, - // (route) => false, - // arguments: _currentState!.arguments, - // ); - } - } catch (e) { - DebugLogger.error('Failed to restore navigation state', e); - } - } - - /// Clear all navigation state - Future clearAll() async { - try { - _navigationStack.clear(); - _currentState = null; - _stateNotifier.value = null; - - await _prefs?.remove(_navigationStackKey); - await _prefs?.remove(_currentStateKey); - await _prefs?.remove(_deepLinkStateKey); - - DebugLogger.navigation('All navigation state cleared'); - } catch (e) { - DebugLogger.error('Failed to clear navigation state', e); - } - } - - /// Save navigation state to persistent storage - Future _saveNavigationState() async { - if (_prefs == null) return; - - try { - // Save navigation stack - final stackJson = _navigationStack - .map((state) => state.toJson()) - .toList(); - await _prefs!.setString(_navigationStackKey, jsonEncode(stackJson)); - - // Save current state - if (_currentState != null) { - await _prefs!.setString( - _currentStateKey, - jsonEncode(_currentState!.toJson()), - ); - } else { - await _prefs!.remove(_currentStateKey); - } - } catch (e) { - DebugLogger.error('Failed to save navigation state', e); - } - } - - /// Load navigation state from persistent storage - Future _loadNavigationState() async { - if (_prefs == null) return; - - try { - // Load navigation stack - final stackJsonString = _prefs!.getString(_navigationStackKey); - if (stackJsonString != null) { - final stackJson = jsonDecode(stackJsonString) as List; - _navigationStack.clear(); - for (final stateJson in stackJson) { - if (stateJson is Map) { - _navigationStack.add(NavigationState.fromJson(stateJson)); - } - } - } - - // Load current state - final currentStateJsonString = _prefs!.getString(_currentStateKey); - if (currentStateJsonString != null) { - final currentStateJson = - jsonDecode(currentStateJsonString) as Map; - _currentState = NavigationState.fromJson(currentStateJson); - _stateNotifier.value = _currentState; - } - - DebugLogger.navigation( - 'Navigation state loaded - ${_navigationStack.length} states', - ); - } catch (e) { - DebugLogger.error('Failed to load navigation state', e); - // Clear corrupted state - await clearAll(); - } - } - - /// Save deep link state for restoration - Future _saveDeepLinkState(NavigationState state) async { - if (_prefs == null) return; - - try { - await _prefs!.setString(_deepLinkStateKey, jsonEncode(state.toJson())); - } catch (e) { - DebugLogger.error('Failed to save deep link state', e); - } - } - - /// Get user-friendly title for route name - String _getRouteTitle(String routeName) { - switch (routeName) { - case '/': - case '/home': - return 'Home'; - case '/chat': - return 'Chat'; - case '/settings': - return 'Settings'; - case '/profile': - return 'Profile'; - case '/conversations': - return 'Conversations'; - default: - // Convert route name to title case - return routeName - .replaceAll('/', '') - .split('_') - .map( - (word) => word.isNotEmpty - ? '${word[0].toUpperCase()}${word.substring(1)}' - : '', - ) - .join(' '); - } - } -} - -/// Breadcrumb navigation item -class NavigationBreadcrumb { - final String title; - final String routeName; - final Map arguments; - final bool isActive; - final bool canNavigateBack; - - NavigationBreadcrumb({ - required this.title, - required this.routeName, - required this.arguments, - required this.isActive, - required this.canNavigateBack, - }); -} diff --git a/lib/features/chat/providers/attachment_cache_provider.dart b/lib/features/chat/providers/attachment_cache_provider.dart deleted file mode 100644 index de49e2a..0000000 --- a/lib/features/chat/providers/attachment_cache_provider.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../core/utils/debug_logger.dart'; - -// Global attachment cache state -class AttachmentCacheState { - final Map imageDataCache; - final Map loadingStates; - final Map errorStates; - - AttachmentCacheState({ - required this.imageDataCache, - required this.loadingStates, - required this.errorStates, - }); - - AttachmentCacheState copyWith({ - Map? imageDataCache, - Map? loadingStates, - Map? errorStates, - }) { - return AttachmentCacheState( - imageDataCache: imageDataCache ?? this.imageDataCache, - loadingStates: loadingStates ?? this.loadingStates, - errorStates: errorStates ?? this.errorStates, - ); - } -} - -class AttachmentCacheNotifier extends StateNotifier { - AttachmentCacheNotifier() - : super(AttachmentCacheState( - imageDataCache: {}, - loadingStates: {}, - errorStates: {}, - )); - - void cacheImageData(String attachmentId, String imageData) { - DebugLogger.log('Caching image data for: $attachmentId'); - state = state.copyWith( - imageDataCache: { - ...state.imageDataCache, - attachmentId: imageData, - }, - ); - - // Limit cache size to prevent memory issues - if (state.imageDataCache.length > 100) { - final newCache = Map.from(state.imageDataCache); - final keysToRemove = newCache.keys.take(20).toList(); - for (final key in keysToRemove) { - newCache.remove(key); - state.loadingStates.remove(key); - state.errorStates.remove(key); - } - state = state.copyWith(imageDataCache: newCache); - } - } - - String? getCachedImageData(String attachmentId) { - return state.imageDataCache[attachmentId]; - } - - void setLoadingState(String attachmentId, bool isLoading) { - state = state.copyWith( - loadingStates: { - ...state.loadingStates, - attachmentId: isLoading, - }, - ); - } - - bool isLoading(String attachmentId) { - return state.loadingStates[attachmentId] ?? false; - } - - void setErrorState(String attachmentId, String? error) { - if (error == null) { - final newErrorStates = Map.from(state.errorStates); - newErrorStates.remove(attachmentId); - state = state.copyWith(errorStates: newErrorStates); - } else { - state = state.copyWith( - errorStates: { - ...state.errorStates, - attachmentId: error, - }, - ); - } - } - - String? getErrorState(String attachmentId) { - return state.errorStates[attachmentId]; - } - - void clearCache() { - state = AttachmentCacheState( - imageDataCache: {}, - loadingStates: {}, - errorStates: {}, - ); - } - - void clearAttachmentCache(String attachmentId) { - final newImageCache = Map.from(state.imageDataCache); - final newLoadingStates = Map.from(state.loadingStates); - final newErrorStates = Map.from(state.errorStates); - - newImageCache.remove(attachmentId); - newLoadingStates.remove(attachmentId); - newErrorStates.remove(attachmentId); - - state = AttachmentCacheState( - imageDataCache: newImageCache, - loadingStates: newLoadingStates, - errorStates: newErrorStates, - ); - } -} - -final attachmentCacheProvider = - StateNotifierProvider((ref) { - return AttachmentCacheNotifier(); -}); - -// Helper providers for easier access -final cachedImageDataProvider = Provider.family((ref, attachmentId) { - final cache = ref.watch(attachmentCacheProvider); - return cache.imageDataCache[attachmentId]; -}); - -final attachmentLoadingStateProvider = Provider.family((ref, attachmentId) { - final cache = ref.watch(attachmentCacheProvider); - return cache.loadingStates[attachmentId] ?? false; -}); - -final attachmentErrorStateProvider = Provider.family((ref, attachmentId) { - final cache = ref.watch(attachmentCacheProvider); - return cache.errorStates[attachmentId]; -}); \ No newline at end of file diff --git a/lib/features/chat/widgets/conversation_components.dart b/lib/features/chat/widgets/conversation_components.dart deleted file mode 100644 index d17395b..0000000 --- a/lib/features/chat/widgets/conversation_components.dart +++ /dev/null @@ -1,956 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'dart:io' show Platform; -import 'dart:ui' as ui; - -import '../../../core/models/conversation.dart'; -import '../../../core/providers/app_providers.dart'; -import '../../../shared/theme/app_theme.dart'; -import '../../../shared/theme/theme_extensions.dart'; -import '../../../shared/utils/ui_utils.dart'; -import '../../../shared/widgets/conduit_components.dart'; -import '../providers/chat_providers.dart'; - -// Optimized delete conversation provider with error handling -final deleteConversationProvider = FutureProvider.family(( - ref, - conversationId, -) async { - final api = ref.read(apiServiceProvider); - if (api == null) throw Exception('No API service available'); - - await api.deleteConversation(conversationId); - ref.invalidate(conversationsProvider); -}); - -/// Optimized conversation tile with Conduit design aesthetics -class ModernConversationTile extends ConsumerStatefulWidget { - final Conversation conversation; - final bool isActive; - final Future Function() onTap; - final VoidCallback onDelete; - - const ModernConversationTile({ - super.key, - required this.conversation, - required this.isActive, - required this.onTap, - required this.onDelete, - }); - - @override - ConsumerState createState() => - _ModernConversationTileState(); -} - -class _ModernConversationTileState extends ConsumerState - with SingleTickerProviderStateMixin { - bool _isLoading = false; - late AnimationController _animationController; - late Animation _scaleAnimation; - - @override - void initState() { - super.initState(); - _animationController = AnimationController( - duration: const Duration(milliseconds: 150), - vsync: this, - ); - _scaleAnimation = Tween(begin: 1.0, end: 0.95).animate( - CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), - ); - } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: _scaleAnimation, - builder: (context, child) { - return Transform.scale( - scale: _scaleAnimation.value, - child: Container( - margin: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.xs, - ), - child: Dismissible( - key: Key(widget.conversation.id), - direction: DismissDirection.horizontal, - background: _buildSwipeBackground(DismissDirection.startToEnd), - secondaryBackground: _buildSwipeBackground( - DismissDirection.endToStart, - ), - confirmDismiss: _handleDismiss, - child: _buildTileContent(), - ), - ), - ); - }, - ); - } - - Widget _buildSwipeBackground(DismissDirection direction) { - final isArchive = direction == DismissDirection.startToEnd; - return Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: isArchive - ? [ - AppTheme.brandPrimary.withValues(alpha: 0.1), - AppTheme.brandPrimary.withValues(alpha: 0.2), - ] - : [ - AppTheme.error.withValues(alpha: 0.1), - AppTheme.error.withValues(alpha: 0.2), - ], - ), - borderRadius: BorderRadius.circular(AppBorderRadius.lg), - ), - alignment: isArchive ? Alignment.centerLeft : Alignment.centerRight, - padding: EdgeInsets.symmetric(horizontal: Spacing.lg), - child: Container( - width: Spacing.xxl, - height: Spacing.xxl, - decoration: BoxDecoration( - color: isArchive ? AppTheme.brandPrimary : AppTheme.error, - borderRadius: BorderRadius.circular(AppBorderRadius.md), - boxShadow: ConduitShadows.low, - ), - child: Icon( - isArchive - ? (Platform.isIOS ? CupertinoIcons.archivebox : Icons.archive) - : (Platform.isIOS ? CupertinoIcons.delete : Icons.delete), - color: AppTheme.neutral50, - size: AppTypography.headlineMedium, - ), - ), - ); - } - - Future _handleDismiss(DismissDirection direction) async { - if (direction == DismissDirection.startToEnd) { - await _handleArchive(); - } else { - widget.onDelete(); - } - return false; - } - - Widget _buildTileContent() { - return GestureDetector( - onTapDown: (_) => _animationController.forward(), - onTapUp: (_) => _animationController.reverse(), - onTapCancel: () => _animationController.reverse(), - onTap: _isLoading ? null : _handleTap, - child: Container( - padding: const EdgeInsets.all(Spacing.md), - decoration: BoxDecoration( - gradient: widget.isActive - ? LinearGradient( - colors: [ - AppTheme.brandPrimary.withValues(alpha: 0.15), - AppTheme.brandPrimary.withValues(alpha: 0.08), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ) - : LinearGradient( - colors: [ - AppTheme.neutral700.withValues(alpha: 0.6), - AppTheme.neutral700.withValues(alpha: 0.3), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(AppBorderRadius.lg), - border: Border.all( - color: widget.isActive - ? AppTheme.brandPrimary.withValues(alpha: 0.3) - : AppTheme.neutral600.withValues(alpha: 0.2), - width: widget.isActive ? BorderWidth.medium : BorderWidth.thin, - ), - boxShadow: widget.isActive ? ConduitShadows.low : null, - ), - child: Row( - children: [ - _buildLeadingIcon(), - const SizedBox(width: Spacing.md), - Expanded(child: _buildContent()), - _buildTrailingActions(), - ], - ), - ), - ); - } - - Widget _buildLeadingIcon() { - if (_isLoading) { - return SizedBox( - width: Spacing.xl, - height: Spacing.xl, - child: CircularProgressIndicator.adaptive( - strokeWidth: BorderWidth.thick, - valueColor: AlwaysStoppedAnimation( - widget.isActive ? AppTheme.brandPrimary : AppTheme.neutral300, - ), - ), - ); - } - - return Container( - width: Spacing.xl, - height: Spacing.xl, - decoration: BoxDecoration( - gradient: widget.isActive - ? LinearGradient( - colors: [AppTheme.brandPrimary, AppTheme.brandPrimaryLight], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ) - : LinearGradient( - colors: [ - AppTheme.neutral600.withValues(alpha: 0.8), - AppTheme.neutral500.withValues(alpha: 0.6), - ], - ), - borderRadius: BorderRadius.circular(AppBorderRadius.sm), - ), - child: Stack( - alignment: Alignment.center, - children: [ - Icon( - Platform.isIOS - ? CupertinoIcons.chat_bubble_2_fill - : Icons.chat_rounded, - color: AppTheme.neutral50, - size: Spacing.md, - ), - if (widget.conversation.pinned) - Positioned( - top: Spacing.xxs, - right: Spacing.xxs, - child: Container( - width: Spacing.sm, - height: Spacing.sm, - decoration: const BoxDecoration( - color: AppTheme.warning, - shape: BoxShape.circle, - ), - ), - ), - ], - ), - ); - } - - Widget _buildContent() { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.conversation.title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: widget.isActive ? AppTheme.neutral50 : AppTheme.neutral100, - fontWeight: FontWeight.w600, - fontSize: AppTypography.bodyLarge, - letterSpacing: -0.2, - ), - ), - const SizedBox(height: Spacing.xs), - Row( - children: [ - Icon( - Platform.isIOS ? CupertinoIcons.time : Icons.access_time_rounded, - size: AppTypography.labelMedium, - color: AppTheme.neutral400, - ), - const SizedBox(width: Spacing.xs), - Text( - _formatDate(widget.conversation.updatedAt), - style: const TextStyle( - color: AppTheme.neutral400, - fontSize: AppTypography.labelMedium, - fontWeight: FontWeight.w500, - ), - ), - if (widget.conversation.messages.isNotEmpty) ...[ - Container( - margin: const EdgeInsets.symmetric(horizontal: Spacing.sm), - width: Spacing.xxs, - height: Spacing.xxs, - decoration: const BoxDecoration( - color: AppTheme.neutral400, - shape: BoxShape.circle, - ), - ), - Text( - '${widget.conversation.messages.length} messages', - style: const TextStyle( - color: AppTheme.neutral400, - fontSize: AppTypography.labelMedium, - fontWeight: FontWeight.w500, - ), - ), - ], - ], - ), - if (widget.conversation.tags.isNotEmpty) ...[ - const SizedBox(height: Spacing.sm), - _buildTags(), - ], - ], - ); - } - - Widget _buildTags() { - return Wrap( - spacing: Spacing.xs, - runSpacing: Spacing.xs, - children: widget.conversation.tags.take(3).map((tag) { - return Container( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.xs + Spacing.xxs, - vertical: Spacing.xxs, - ), - decoration: BoxDecoration( - color: AppTheme.brandPrimary.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(AppBorderRadius.xs), - border: Border.all( - color: AppTheme.brandPrimary.withValues(alpha: 0.2), - width: BorderWidth.thin, - ), - ), - child: Text( - tag, - style: const TextStyle( - color: AppTheme.brandPrimary, - fontSize: AppTypography.labelSmall, - fontWeight: FontWeight.w600, - ), - ), - ); - }).toList(), - ); - } - - Widget _buildTrailingActions() { - return PopupMenuButton( - icon: Container( - width: Spacing.xl, - height: Spacing.xl, - decoration: BoxDecoration( - color: AppTheme.neutral700.withValues(alpha: 0.6), - borderRadius: BorderRadius.circular(AppBorderRadius.sm), - ), - child: Icon( - Platform.isIOS ? CupertinoIcons.ellipsis : Icons.more_vert_rounded, - color: AppTheme.neutral300, - size: Spacing.md, - ), - ), - color: AppTheme.neutral800, - elevation: Elevation.high + Spacing.xs, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - side: BorderSide( - color: AppTheme.neutral600.withValues(alpha: 0.3), - width: BorderWidth.thin, - ), - ), - onSelected: _handleMenuAction, - itemBuilder: (context) => _buildMenuItems(), - ); - } - - List> _buildMenuItems() { - return [ - _buildMenuItem( - 'pin', - widget.conversation.pinned - ? (Platform.isIOS - ? CupertinoIcons.pin_slash - : Icons.push_pin_outlined) - : (Platform.isIOS - ? CupertinoIcons.pin_fill - : Icons.push_pin_rounded), - widget.conversation.pinned ? 'Unpin' : 'Pin', - ), - _buildMenuItem( - 'archive', - Platform.isIOS ? CupertinoIcons.archivebox : Icons.archive_rounded, - 'Archive', - ), - _buildMenuItem( - 'share', - Platform.isIOS ? CupertinoIcons.share : Icons.share_rounded, - 'Share', - ), - _buildMenuItem( - 'clone', - Platform.isIOS ? CupertinoIcons.doc_on_doc : Icons.content_copy_rounded, - 'Clone', - ), - PopupMenuItem( - enabled: false, - child: Divider(color: AppTheme.neutral600, height: BorderWidth.regular), - ), - _buildMenuItem( - 'delete', - Platform.isIOS ? CupertinoIcons.delete : Icons.delete_rounded, - 'Delete', - isDestructive: true, - ), - ]; - } - - PopupMenuItem _buildMenuItem( - String value, - IconData icon, - String label, { - bool isDestructive = false, - }) { - return PopupMenuItem( - value: value, - child: Row( - children: [ - Container( - width: Spacing.lg + Spacing.xs, - height: Spacing.lg + Spacing.xs, - decoration: BoxDecoration( - color: isDestructive - ? AppTheme.error.withValues(alpha: 0.1) - : AppTheme.neutral700.withValues(alpha: 0.6), - borderRadius: BorderRadius.circular(AppBorderRadius.xs), - ), - child: Icon( - icon, - size: Spacing.md, - color: isDestructive ? AppTheme.error : AppTheme.neutral200, - ), - ), - const SizedBox(width: Spacing.sm), - Text( - label, - style: TextStyle( - color: isDestructive ? AppTheme.error : AppTheme.neutral50, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ); - } - - Future _handleTap() async { - setState(() => _isLoading = true); - try { - await widget.onTap(); - } finally { - if (mounted) { - setState(() => _isLoading = false); - } - } - } - - Future _handleMenuAction(String action) async { - switch (action) { - case 'pin': - await _handlePin(); - break; - case 'archive': - await _handleArchive(); - break; - case 'share': - await _handleShare(); - break; - case 'clone': - await _handleClone(); - break; - case 'delete': - widget.onDelete(); - break; - } - } - - Future _handlePin() async { - try { - await pinConversation( - ref, - widget.conversation.id, - !widget.conversation.pinned, - ); - if (mounted) { - UiUtils.showMessage( - context, - widget.conversation.pinned - ? 'Conversation unpinned' - : 'Conversation pinned', - ); - } - } catch (e) { - if (mounted) { - UiUtils.showMessage( - context, - 'Failed to ${widget.conversation.pinned ? 'unpin' : 'pin'} conversation', - ); - } - } - } - - Future _handleArchive() async { - try { - await archiveConversation(ref, widget.conversation.id, true); - if (mounted) { - UiUtils.showMessage(context, 'Conversation archived'); - } - } catch (e) { - if (mounted) { - UiUtils.showMessage(context, 'Failed to archive conversation'); - } - } - } - - Future _handleShare() async { - try { - final shareId = await shareConversation(ref, widget.conversation.id); - if (mounted && shareId != null) { - _showShareDialog(shareId); - } - } catch (e) { - if (mounted) { - UiUtils.showMessage(context, 'Failed to share conversation'); - } - } - } - - Future _handleClone() async { - try { - await cloneConversation(ref, widget.conversation.id); - if (mounted) { - Navigator.pop(context); - UiUtils.showMessage(context, 'Conversation cloned'); - } - } catch (e) { - if (mounted) { - UiUtils.showMessage(context, 'Failed to clone conversation'); - } - } - } - - void _showShareDialog(String shareId) { - final shareUrl = - '${ref.read(apiServiceProvider)?.serverConfig.url}/s/$shareId'; - - showDialog( - context: context, - builder: (context) => AlertDialog( - backgroundColor: AppTheme.neutral800, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.lg), - side: BorderSide( - color: AppTheme.neutral600.withValues(alpha: 0.3), - width: BorderWidth.thin, - ), - ), - title: Row( - children: [ - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [AppTheme.brandPrimary, AppTheme.brandPrimaryLight], - ), - borderRadius: BorderRadius.circular(AppBorderRadius.sm), - ), - child: const Icon( - Icons.share_rounded, - color: AppTheme.neutral50, - size: Spacing.md, - ), - ), - const SizedBox(width: Spacing.sm), - const Text( - 'Share Conversation', - style: TextStyle( - color: AppTheme.neutral50, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Anyone with this link can view the conversation:', - style: TextStyle(color: AppTheme.neutral300), - ), - const SizedBox(height: Spacing.md), - Container( - padding: const EdgeInsets.all(Spacing.md), - decoration: BoxDecoration( - color: AppTheme.neutral700.withValues(alpha: 0.6), - borderRadius: BorderRadius.circular(AppBorderRadius.md), - border: Border.all( - color: AppTheme.neutral600.withValues(alpha: 0.3), - width: BorderWidth.thin, - ), - ), - child: SelectableText( - shareUrl, - style: const TextStyle( - fontFamily: 'monospace', - color: AppTheme.neutral50, - fontSize: AppTypography.labelMedium, - ), - ), - ), - ], - ), - actions: [ - ConduitButton( - text: 'Close', - isSecondary: true, - onPressed: () => Navigator.pop(context), - ), - ConduitButton( - text: 'Copy Link', - onPressed: () async { - await Clipboard.setData(ClipboardData(text: shareUrl)); - if (context.mounted) { - UiUtils.showMessage(context, 'Link copied to clipboard'); - Navigator.pop(context); - } - }, - ), - ], - ), - ); - } - - String _formatDate(DateTime date) { - final now = DateTime.now(); - - // Convert to local timezone if needed - final localDate = date.toLocal(); - final localNow = now.toLocal(); - final difference = localNow.difference(localDate); - - // Handle negative differences (future dates) - if (difference.isNegative) { - return 'Just now'; - } - - if (difference.inDays == 0) { - if (difference.inHours == 0) { - if (difference.inMinutes <= 1) { - return 'Just now'; - } - return '${difference.inMinutes}m'; - } - return '${difference.inHours}h'; - } else if (difference.inDays == 1) { - return 'Yesterday'; - } else if (difference.inDays < 7) { - return '${difference.inDays}d'; - } else if (difference.inDays < 365) { - return '${localDate.month}/${localDate.day}'; - } else { - return '${localDate.month}/${localDate.day}/${localDate.year}'; - } - } -} - -/// Optimized archived chats view with improved performance -class ModernArchivedChatsView extends ConsumerWidget { - final ScrollController scrollController; - - const ModernArchivedChatsView({super.key, required this.scrollController}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final archivedConversations = ref.watch(archivedConversationsProvider); - - return Container( - decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [AppTheme.neutral800, AppTheme.neutral900], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - borderRadius: BorderRadius.only( - topLeft: ui.Radius.circular(AppBorderRadius.lg), - topRight: ui.Radius.circular(AppBorderRadius.lg), - ), - border: Border.all( - color: AppTheme.neutral600.withValues(alpha: 0.2), - width: BorderWidth.thin, - ), - ), - child: Column( - children: [ - _buildHandle(), - _buildHeader(context), - const Divider(color: AppTheme.neutral600, height: 1, thickness: 0.5), - Expanded(child: _buildContent(context, archivedConversations, ref)), - ], - ), - ); - } - - Widget _buildHandle() { - return Container( - margin: const EdgeInsets.symmetric(vertical: Spacing.sm), - width: Spacing.xxl, - height: Spacing.xs, - decoration: BoxDecoration( - color: AppTheme.neutral500, - borderRadius: BorderRadius.circular(AppBorderRadius.xs), - ), - ); - } - - Widget _buildHeader(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(Spacing.lg), - child: Row( - children: [ - Container( - width: Spacing.xxl, - height: Spacing.xxl, - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [AppTheme.brandPrimary, AppTheme.brandPrimaryLight], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(AppBorderRadius.md), - ), - child: const Icon( - Icons.archive_rounded, - color: AppTheme.neutral50, - size: AppTypography.headlineMedium, - ), - ), - const SizedBox(width: Spacing.md), - const Expanded( - child: Text( - 'Archived Conversations', - style: TextStyle( - color: AppTheme.neutral50, - fontSize: AppTypography.headlineSmall, - fontWeight: FontWeight.w600, - letterSpacing: -0.3, - ), - ), - ), - ConduitIconButton( - icon: Platform.isIOS ? CupertinoIcons.xmark : Icons.close_rounded, - onPressed: () => Navigator.pop(context), - ), - ], - ), - ); - } - - Widget _buildContent( - BuildContext context, - List conversations, - WidgetRef ref, - ) { - if (conversations.isEmpty) { - return _buildEmptyState(); - } - - return ListView.builder( - controller: scrollController, - padding: const EdgeInsets.all(Spacing.md), - itemCount: conversations.length, - itemBuilder: (context, index) { - final conversation = conversations[index]; - return ModernArchivedConversationTile( - conversation: conversation, - onUnarchive: () => _handleUnarchive(ref, context, conversation.id), - ); - }, - ); - } - - Widget _buildEmptyState() { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: Spacing.xxl + Spacing.xl, - height: Spacing.xxl + Spacing.xl, - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppTheme.neutral600.withValues(alpha: 0.3), - AppTheme.neutral700.withValues(alpha: 0.1), - ], - ), - borderRadius: BorderRadius.circular(AppBorderRadius.round), - ), - child: const Icon( - Icons.archive_rounded, - size: Spacing.xxl, - color: AppTheme.neutral400, - ), - ), - const SizedBox(height: Spacing.lg), - const Text( - 'Nothing archived yet', - style: TextStyle( - color: AppTheme.neutral50, - fontSize: AppTypography.headlineSmall, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: Spacing.sm), - const Text( - 'Conversations you archive will appear here', - style: TextStyle( - color: AppTheme.neutral400, - fontSize: AppTypography.labelLarge, - ), - textAlign: TextAlign.center, - ), - ], - ), - ); - } - - Future _handleUnarchive( - WidgetRef ref, - BuildContext context, - String conversationId, - ) async { - try { - await archiveConversation(ref, conversationId, false); - if (context.mounted) { - UiUtils.showMessage(context, 'Conversation unarchived'); - } - } catch (e) { - if (context.mounted) { - UiUtils.showMessage(context, 'Failed to unarchive conversation'); - } - } - } -} - -/// Optimized archived conversation tile -class ModernArchivedConversationTile extends StatelessWidget { - final Conversation conversation; - final VoidCallback onUnarchive; - - const ModernArchivedConversationTile({ - super.key, - required this.conversation, - required this.onUnarchive, - }); - - @override - Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.only(bottom: Spacing.sm), - child: Container( - padding: const EdgeInsets.all(Spacing.md), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppTheme.neutral700.withValues(alpha: 0.4), - AppTheme.neutral700.withValues(alpha: 0.2), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(AppBorderRadius.lg), - border: Border.all( - color: AppTheme.neutral600.withValues(alpha: 0.2), - width: BorderWidth.thin, - ), - ), - child: Row( - children: [ - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: AppTheme.neutral600.withValues(alpha: 0.6), - borderRadius: BorderRadius.circular(AppBorderRadius.sm), - ), - child: const Icon( - Icons.archive_rounded, - color: AppTheme.neutral300, - size: 16, - ), - ), - const SizedBox(width: Spacing.md), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - conversation.title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: AppTheme.neutral50, - fontWeight: FontWeight.w600, - fontSize: AppTypography.bodyLarge, - ), - ), - const SizedBox(height: Spacing.xs), - Text( - _formatArchivedDate(conversation.updatedAt), - style: const TextStyle( - color: AppTheme.neutral400, - fontSize: AppTypography.labelMedium, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - ConduitIconButton( - icon: Platform.isIOS - ? CupertinoIcons.arrow_up_bin - : Icons.unarchive_rounded, - onPressed: onUnarchive, - tooltip: 'Unarchive', - ), - ], - ), - ), - ); - } - - String _formatArchivedDate(DateTime date) { - final now = DateTime.now(); - final difference = now.difference(date); - - if (difference.inDays == 0) { - return 'Today'; - } else if (difference.inDays == 1) { - return 'Yesterday'; - } else if (difference.inDays < 7) { - return '${difference.inDays} days ago'; - } else { - return '${date.month}/${date.day}/${date.year}'; - } - } -} diff --git a/lib/features/chat/widgets/file_viewer_dialog.dart b/lib/features/chat/widgets/file_viewer_dialog.dart deleted file mode 100644 index d56d2f9..0000000 --- a/lib/features/chat/widgets/file_viewer_dialog.dart +++ /dev/null @@ -1,242 +0,0 @@ -import 'dart:io'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import '../../../shared/theme/theme_extensions.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -import '../../../core/models/file_info.dart'; -import '../../../core/providers/app_providers.dart'; - -class FileViewerDialog extends ConsumerWidget { - final FileInfo fileInfo; - - const FileViewerDialog({super.key, required this.fileInfo}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - // Use themed tokens via extension - final fileContent = ref.watch(fileContentProvider(fileInfo.id)); - - return Dialog.fullscreen( - child: Scaffold( - backgroundColor: context.conduitTheme.surfaceBackground, - appBar: AppBar( - backgroundColor: context.conduitTheme.surfaceBackground, - elevation: 0, - title: Text( - fileInfo.originalFilename, - overflow: TextOverflow.ellipsis, - style: TextStyle(color: context.conduitTheme.textPrimary), - ), - iconTheme: IconThemeData(color: context.conduitTheme.iconPrimary), - leading: IconButton( - icon: Icon(Platform.isIOS ? CupertinoIcons.back : Icons.arrow_back), - onPressed: () => Navigator.pop(context), - ), - actions: [ - IconButton( - icon: Icon(Platform.isIOS ? CupertinoIcons.info : Icons.info), - onPressed: () => _showFileInfo(context), - ), - ], - ), - body: fileContent.when( - data: (content) => _buildContentView(context, content), - loading: () => Center( - child: CircularProgressIndicator( - color: context.conduitTheme.buttonPrimary, - ), - ), - error: (error, _) => Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.error, size: 64, color: context.conduitTheme.error), - const SizedBox(height: Spacing.md), - Text( - 'Failed to load file', - style: TextStyle( - color: context.conduitTheme.error, - fontSize: AppTypography.headlineSmall, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: Spacing.sm), - Text( - error.toString(), - style: TextStyle(color: context.conduitTheme.textSecondary), - textAlign: TextAlign.center, - ), - const SizedBox(height: Spacing.md), - ElevatedButton( - onPressed: () => - ref.invalidate(fileContentProvider(fileInfo.id)), - child: const Text('Retry'), - ), - ], - ), - ), - ), - ), - ); - } - - Widget _buildContentView(BuildContext context, String content) { - final theme = context.conduitTheme; - final isTextFile = _isTextFile(fileInfo.mimeType); - - if (!isTextFile) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - _getFileIcon(fileInfo.mimeType), - size: 64, - color: theme.buttonPrimary, - ), - const SizedBox(height: Spacing.md), - Text( - fileInfo.originalFilename, - style: TextStyle( - color: theme.textPrimary, - fontSize: AppTypography.headlineSmall, - fontWeight: FontWeight.w600, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: Spacing.sm), - Text( - 'File type: ${fileInfo.mimeType}', - style: TextStyle(color: theme.textSecondary), - ), - Text( - 'Size: ${_formatFileSize(fileInfo.size)}', - style: TextStyle(color: theme.textSecondary), - ), - const SizedBox(height: Spacing.md), - Text( - 'Preview not available for this file type', - style: TextStyle(color: theme.textTertiary), - ), - ], - ), - ); - } - - return SingleChildScrollView( - padding: const EdgeInsets.all(Spacing.md), - child: SelectableText( - content, - style: TextStyle( - color: theme.textPrimary, - fontFamily: 'monospace', - fontSize: AppTypography.labelLarge, - ), - ), - ); - } - - void _showFileInfo(BuildContext context) { - showDialog( - context: context, - builder: (context) => AlertDialog( - backgroundColor: context.conduitTheme.surfaceBackground, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.dialog), - ), - title: Text( - 'File Information', - style: TextStyle(color: context.conduitTheme.textPrimary), - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildInfoRow(context, 'Name', fileInfo.originalFilename), - _buildInfoRow(context, 'Size', _formatFileSize(fileInfo.size)), - _buildInfoRow(context, 'Type', fileInfo.mimeType), - _buildInfoRow(context, 'Created', _formatDate(fileInfo.createdAt)), - _buildInfoRow(context, 'Modified', _formatDate(fileInfo.updatedAt)), - if (fileInfo.hash != null) - _buildInfoRow(context, 'Hash', fileInfo.hash!), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text( - 'Close', - style: TextStyle(color: context.conduitTheme.buttonPrimary), - ), - ), - ], - ), - ); - } - - Widget _buildInfoRow(BuildContext context, String label, String value) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: Spacing.xxxl + Spacing.md, - child: Text( - '$label:', - style: TextStyle( - fontWeight: FontWeight.w600, - color: context.conduitTheme.textSecondary, - ), - ), - ), - Expanded( - child: Text( - value, - style: TextStyle(color: context.conduitTheme.textPrimary), - ), - ), - ], - ), - ); - } - - bool _isTextFile(String mimeType) { - return mimeType.startsWith('text/') || - mimeType == 'application/json' || - mimeType == 'application/xml' || - mimeType == 'application/javascript' || - mimeType.contains('yaml') || - mimeType.contains('markdown'); - } - - IconData _getFileIcon(String mimeType) { - if (mimeType.startsWith('image/')) { - return Platform.isIOS ? CupertinoIcons.photo : Icons.image; - } else if (mimeType.startsWith('video/')) { - return Platform.isIOS ? CupertinoIcons.video_camera : Icons.video_file; - } else if (mimeType.startsWith('audio/')) { - return Platform.isIOS ? CupertinoIcons.music_note : Icons.audio_file; - } else if (mimeType.contains('pdf')) { - return Platform.isIOS ? CupertinoIcons.doc : Icons.picture_as_pdf; - } else if (mimeType.startsWith('text/') || mimeType.contains('json')) { - return Platform.isIOS ? CupertinoIcons.doc_text : Icons.description; - } else { - return Platform.isIOS ? CupertinoIcons.doc : Icons.insert_drive_file; - } - } - - String _formatFileSize(int bytes) { - if (bytes < 1024) return '$bytes B'; - if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB'; - if (bytes < 1024 * 1024 * 1024) { - return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB'; - } - return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB'; - } - - String _formatDate(DateTime date) { - return '${date.day}/${date.month}/${date.year} ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}'; - } -} diff --git a/lib/features/chat/widgets/folder_management_dialog.dart b/lib/features/chat/widgets/folder_management_dialog.dart deleted file mode 100644 index 8329db7..0000000 --- a/lib/features/chat/widgets/folder_management_dialog.dart +++ /dev/null @@ -1,938 +0,0 @@ -import 'dart:io'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_animate/flutter_animate.dart'; -import '../../../shared/theme/theme_extensions.dart'; -import '../../../shared/widgets/conduit_components.dart'; -import '../../../shared/utils/ui_utils.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -import '../../../core/models/folder.dart'; -import '../../../core/models/conversation.dart'; -import '../../../core/providers/app_providers.dart'; - -class FolderManagementDialog extends ConsumerStatefulWidget { - final Conversation? conversation; - final BuildContext? parentContext; - - const FolderManagementDialog({super.key, this.conversation, this.parentContext}); - - @override - ConsumerState createState() => - _FolderManagementDialogState(); -} - -class _FolderManagementDialogState - extends ConsumerState { - final _nameController = TextEditingController(); - bool _isCreating = false; - - @override - void dispose() { - _nameController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final folders = ref.watch(foldersProvider); - final isMovingConversation = widget.conversation != null; - - 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, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // Modern Header - _buildModernHeader(context, isMovingConversation), - - // 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: 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, - ), - ), - ); - } - - // 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: ConduitLoadingIndicator( - message: 'Loading folders...', - size: IconSize.xl, - ), - ), - ); - } - - 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: [ - Expanded( - child: ConduitButton( - text: 'Remove from Folder', - onPressed: () => _moveToFolder(null), - isSecondary: true, - icon: Platform.isIOS ? CupertinoIcons.folder_badge_minus : Icons.folder_off_rounded, - ), - ), - const SizedBox(width: Spacing.md), - Expanded( - child: ConduitButton( - text: 'Cancel', - onPressed: () => Navigator.pop(context), - isSecondary: true, - icon: Platform.isIOS ? CupertinoIcons.xmark : Icons.close_rounded, - ), - ), - ], - ), - ); - } - - 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: [ - Icon( - Platform.isIOS ? CupertinoIcons.chat_bubble_2 : Icons.chat_bubble_outline_rounded, - size: IconSize.xs, - color: context.conduitTheme.textTertiary, - ), - const SizedBox(width: Spacing.xs), - Text( - '${folder.conversationIds.length} conversation${folder.conversationIds.length != 1 ? 's' : ''}', - style: AppTypography.bodySmallStyle.copyWith( - color: context.conduitTheme.textSecondary, - ), - ), - 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 _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: 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, - ), - ), - ], - ), - ), - ], - ); - } - - Future _createFolder() async { - final name = _nameController.text.trim(); - if (name.isEmpty) return; - - setState(() => _isCreating = true); - - try { - final api = ref.read(apiServiceProvider); - if (api == null) throw Exception('No API service available'); - - await api.createFolder(name: name); - ref.invalidate(foldersProvider); - _nameController.clear(); - - if (mounted) { - UiUtils.showMessage(widget.parentContext ?? context, 'Folder "$name" created'); - } - } catch (e) { - if (mounted) { - UiUtils.showMessage(widget.parentContext ?? context, 'Error creating folder: $e'); - } - } finally { - if (mounted) { - setState(() => _isCreating = false); - } - } - } - - Future _moveToFolder(String? folderId) async { - if (widget.conversation == null) return; - - try { - final api = ref.read(apiServiceProvider); - if (api == null) throw Exception('No API service available'); - - await api.moveConversationToFolder(widget.conversation!.id, folderId); - ref.invalidate(conversationsProvider); - ref.invalidate(foldersProvider); - - if (mounted) { - Navigator.pop(context); - UiUtils.showMessage( - widget.parentContext ?? context, - folderId != null - ? 'Conversation moved to folder' - : 'Conversation removed from folder', - ); - } - } catch (e) { - if (mounted) { - 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: (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, - ), - ), - ], - ), - ), - ], - ), - ), - - // 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()); - } - }, - ), - ), - - // 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, - ), - ), - ], - ), - ), - ], - ), - ), - ).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 { - final api = ref.read(apiServiceProvider); - if (api != null) { - await api.updateFolder(folder.id, name: result); - ref.invalidate(foldersProvider); - - if (mounted) { - UiUtils.showMessage(widget.parentContext ?? context, 'Folder renamed to "$result"'); - } - } - } catch (e) { - if (mounted) { - UiUtils.showMessage(widget.parentContext ?? context, 'Failed to rename folder: $e'); - } - } - } - - controller.dispose(); - } - - void _deleteFolder(Folder folder) async { - final confirmed = await showDialog( - context: context, - 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, - ), - ), - ], - ), - ), - ], - ), - ), - ).animate().slideY( - begin: 0.1, - duration: AnimationDuration.modalPresentation, - curve: AnimationCurves.modalPresentation, - ).fadeIn( - duration: AnimationDuration.modalPresentation, - curve: AnimationCurves.easeOut, - ), - ), - ); - - if (confirmed == true) { - try { - final api = ref.read(apiServiceProvider); - if (api != null) { - await api.deleteFolder(folder.id); - ref.invalidate(foldersProvider); - ref.invalidate(conversationsProvider); - - if (mounted) { - UiUtils.showMessage(widget.parentContext ?? context, 'Folder "${folder.name}" deleted'); - } - } - } catch (e) { - if (mounted) { - UiUtils.showMessage(widget.parentContext ?? context, 'Failed to delete folder: $e'); - } - } - } - } -} diff --git a/lib/features/chat/widgets/message_batch_widget.dart b/lib/features/chat/widgets/message_batch_widget.dart deleted file mode 100644 index 284088d..0000000 --- a/lib/features/chat/widgets/message_batch_widget.dart +++ /dev/null @@ -1,959 +0,0 @@ -import 'package:flutter/material.dart'; -import '../../../shared/theme/app_theme.dart'; -import '../../../shared/widgets/sheet_handle.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_animate/flutter_animate.dart'; -import 'dart:io' show Platform; -import '../../../shared/theme/theme_extensions.dart'; -import '../../../shared/utils/platform_utils.dart'; - -import '../services/message_batch_service.dart'; -import '../../../core/models/chat_message.dart'; -import '../../../core/providers/app_providers.dart'; -import '../providers/chat_providers.dart'; -import '../../../shared/widgets/themed_dialogs.dart'; - -/// Batch operations toolbar that appears when messages are selected -class MessageBatchToolbar extends ConsumerWidget { - final List selectedMessages; - final VoidCallback? onCancel; - - const MessageBatchToolbar({ - super.key, - required this.selectedMessages, - this.onCancel, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final conduitTheme = context.conduitTheme; - final selectedCount = selectedMessages.length; - - return Container( - height: 80, - padding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), - decoration: BoxDecoration( - color: conduitTheme.cardBackground, - border: Border( - top: BorderSide(color: conduitTheme.cardBorder, width: 1), - ), - boxShadow: ConduitShadows.medium, - ), - child: SafeArea( - child: Row( - children: [ - // Selected count - Expanded( - child: Text( - '$selectedCount message${selectedCount == 1 ? '' : 's'} selected', - style: conduitTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w600, - ), - ), - ), - - // Action buttons - _buildActionButton( - icon: Platform.isIOS - ? CupertinoIcons.doc_on_clipboard - : Icons.copy, - label: 'Copy', - onPressed: () => _showCopyOptions(context, ref), - ), - - const SizedBox(width: Spacing.sm), - - _buildActionButton( - icon: Platform.isIOS ? CupertinoIcons.share : Icons.share, - label: 'Export', - onPressed: () => _showExportOptions(context, ref), - ), - - const SizedBox(width: Spacing.sm), - - _buildActionButton( - icon: Platform.isIOS - ? CupertinoIcons.ellipsis_circle - : Icons.more_vert, - label: 'More', - onPressed: () => _showMoreOptions(context, ref), - ), - - const SizedBox(width: Spacing.sm), - - // Cancel button - GestureDetector( - onTap: () { - PlatformUtils.lightHaptic(); - onCancel?.call(); - }, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), - decoration: BoxDecoration( - color: AppTheme.neutral50.withValues(alpha: Alpha.subtle), - borderRadius: BorderRadius.circular(AppBorderRadius.sm), - ), - child: Text( - 'Cancel', - style: TextStyle( - color: AppTheme.neutral50.withValues(alpha: 0.8), - fontSize: AppTypography.labelLarge, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - ], - ), - ), - ).animate().slideY( - begin: 1, - end: 0, - duration: const Duration(milliseconds: 200), - curve: Curves.easeOut, - ); - } - - Widget _buildActionButton({ - required IconData icon, - required String label, - required VoidCallback onPressed, - }) { - return GestureDetector( - onTap: () { - PlatformUtils.lightHaptic(); - onPressed(); - }, - child: Container( - padding: const EdgeInsets.all(Spacing.sm), - decoration: BoxDecoration( - color: AppTheme.neutral50.withValues(alpha: Alpha.subtle), - borderRadius: BorderRadius.circular(AppBorderRadius.sm), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - icon, - color: AppTheme.neutral50.withValues(alpha: 0.8), - size: IconSize.md, - ), - const SizedBox(height: Spacing.xxs), - Text( - label, - style: TextStyle( - color: AppTheme.neutral50.withValues(alpha: 0.8), - fontSize: AppTypography.labelSmall, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - ); - } - - void _showCopyOptions(BuildContext context, WidgetRef ref) { - showModalBottomSheet( - context: context, - backgroundColor: Colors.transparent, - builder: (context) => CopyOptionsSheet(messages: selectedMessages), - ); - } - - void _showExportOptions(BuildContext context, WidgetRef ref) { - showModalBottomSheet( - context: context, - backgroundColor: Colors.transparent, - builder: (context) => ExportOptionsSheet(messages: selectedMessages), - ); - } - - void _showMoreOptions(BuildContext context, WidgetRef ref) { - showModalBottomSheet( - context: context, - backgroundColor: Colors.transparent, - builder: (context) => MoreOptionsSheet(messages: selectedMessages), - ); - } -} - -/// Copy options bottom sheet -class CopyOptionsSheet extends ConsumerWidget { - final List messages; - - const CopyOptionsSheet({super.key, required this.messages}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final conduitTheme = context.conduitTheme; - - return Container( - decoration: BoxDecoration( - color: context.conduitTheme.surfaceBackground, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(AppBorderRadius.lg), - ), - boxShadow: ConduitShadows.modal, - ), - child: SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Handle bar (standardized) - const SheetHandle(), - - const SizedBox(height: Spacing.lg - Spacing.xs), - - // Title - Text('Copy Messages', style: conduitTheme.headingMedium), - - const SizedBox(height: Spacing.lg - Spacing.xs), - - // Copy options - _buildCopyOption( - context, - ref, - icon: Icons.text_fields, - title: 'Plain Text', - subtitle: 'Copy as plain text', - format: CopyFormat.plain, - ), - - _buildCopyOption( - context, - ref, - icon: Icons.code, - title: 'Markdown', - subtitle: 'Copy with formatting', - format: CopyFormat.markdown, - ), - - _buildCopyOption( - context, - ref, - icon: Icons.data_object, - title: 'JSON', - subtitle: 'Copy as structured data', - format: CopyFormat.json, - ), - - const SizedBox(height: Spacing.lg - Spacing.xs), - ], - ), - ), - ); - } - - Widget _buildCopyOption( - BuildContext context, - WidgetRef ref, { - required IconData icon, - required String title, - required String subtitle, - required CopyFormat format, - }) { - return ListTile( - leading: Icon(icon, color: context.conduitTheme.iconSecondary), - title: Text( - title, - style: context.conduitTheme.bodyLarge?.copyWith( - color: context.conduitTheme.textPrimary, - fontWeight: FontWeight.w500, - ), - ), - subtitle: Text( - subtitle, - style: context.conduitTheme.bodySmall?.copyWith( - color: context.conduitTheme.textSecondary, - ), - ), - onTap: () async { - Navigator.pop(context); - await _copyMessages(context, ref, format); - }, - ); - } - - Future _copyMessages( - BuildContext context, - WidgetRef ref, - CopyFormat format, - ) async { - try { - final batchService = ref.read(messageBatchServiceProvider); - final result = await batchService.copyMessages( - messages: messages, - format: format, - ); - - if (result.success) { - final content = result.data?['content'] as String?; - if (content != null) { - await Clipboard.setData(ClipboardData(text: content)); - - if (context.mounted) {} - } - } else { - if (context.mounted) {} - } - } catch (e) { - if (context.mounted) {} - } - } -} - -/// Export options bottom sheet -class ExportOptionsSheet extends ConsumerWidget { - final List messages; - - const ExportOptionsSheet({super.key, required this.messages}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final conduitTheme = context.conduitTheme; - - return Container( - decoration: BoxDecoration( - color: context.conduitTheme.surfaceBackground, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(AppBorderRadius.lg), - ), - boxShadow: ConduitShadows.modal, - ), - child: SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Handle bar (standardized) - const SheetHandle(), - - const SizedBox(height: Spacing.lg - Spacing.xs), - - // Title - Text('Export Messages', style: conduitTheme.headingMedium), - - const SizedBox(height: Spacing.lg - Spacing.xs), - - // Export options - _buildExportOption( - context, - ref, - icon: Icons.text_fields, - title: 'Text File', - subtitle: 'Export as plain text (.txt)', - format: ExportFormat.text, - ), - - _buildExportOption( - context, - ref, - icon: Icons.code, - title: 'Markdown', - subtitle: 'Export with formatting (.md)', - format: ExportFormat.markdown, - ), - - _buildExportOption( - context, - ref, - icon: Icons.data_object, - title: 'JSON', - subtitle: 'Export as structured data (.json)', - format: ExportFormat.json, - ), - - _buildExportOption( - context, - ref, - icon: Icons.table_chart, - title: 'CSV', - subtitle: 'Export as spreadsheet (.csv)', - format: ExportFormat.csv, - ), - - const SizedBox(height: Spacing.lg - Spacing.xs), - ], - ), - ), - ); - } - - Widget _buildExportOption( - BuildContext context, - WidgetRef ref, { - required IconData icon, - required String title, - required String subtitle, - required ExportFormat format, - }) { - return ListTile( - leading: Icon(icon, color: AppTheme.neutral50.withValues(alpha: 0.8)), - title: Text( - title, - style: const TextStyle( - color: AppTheme.neutral50, - fontWeight: FontWeight.w500, - ), - ), - subtitle: Text( - subtitle, - style: TextStyle( - color: AppTheme.neutral50.withValues(alpha: Alpha.strong), - ), - ), - onTap: () { - Navigator.pop(context); - _showExportDialog(context, ref, format); - }, - ); - } - - void _showExportDialog( - BuildContext context, - WidgetRef ref, - ExportFormat format, - ) { - showDialog( - context: context, - builder: (context) => ExportDialog(messages: messages, format: format), - ); - } -} - -/// More options bottom sheet for additional batch operations -class MoreOptionsSheet extends ConsumerWidget { - final List messages; - - const MoreOptionsSheet({super.key, required this.messages}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final conduitTheme = context.conduitTheme; - - return Container( - decoration: BoxDecoration( - color: conduitTheme.cardBackground, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(AppBorderRadius.lg), - ), - ), - child: SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Handle bar (standardized) - const SheetHandle(), - - const SizedBox(height: Spacing.lg - Spacing.xs), - - // Title - Text('More Actions', style: conduitTheme.headingMedium), - - const SizedBox(height: Spacing.lg - Spacing.xs), - - // More options - ListTile( - leading: Icon( - Icons.label_outline, - color: context.conduitTheme.iconSecondary, - ), - title: Text( - 'Add Tags', - style: context.conduitTheme.bodyLarge?.copyWith( - color: context.conduitTheme.textPrimary, - fontWeight: FontWeight.w500, - ), - ), - subtitle: Text( - 'Tag selected messages', - style: context.conduitTheme.bodySmall?.copyWith( - color: context.conduitTheme.textSecondary, - ), - ), - onTap: () { - Navigator.pop(context); - _showTagDialog(context, ref); - }, - ), - - ListTile( - leading: Icon( - Icons.archive_outlined, - color: context.conduitTheme.iconSecondary, - ), - title: Text( - 'Archive', - style: context.conduitTheme.bodyLarge?.copyWith( - color: context.conduitTheme.textPrimary, - fontWeight: FontWeight.w500, - ), - ), - subtitle: Text( - 'Archive selected messages', - style: context.conduitTheme.bodySmall?.copyWith( - color: context.conduitTheme.textSecondary, - ), - ), - onTap: () { - Navigator.pop(context); - _archiveMessages(context, ref); - }, - ), - - ListTile( - leading: Icon( - Icons.delete_outline, - color: context.conduitTheme.error, - ), - title: Text( - 'Delete', - style: context.conduitTheme.bodyLarge?.copyWith( - color: context.conduitTheme.error, - fontWeight: FontWeight.w500, - ), - ), - subtitle: Text( - 'Delete selected messages', - style: context.conduitTheme.bodySmall?.copyWith( - color: context.conduitTheme.textSecondary, - ), - ), - onTap: () { - Navigator.pop(context); - _showDeleteConfirmation(context, ref); - }, - ), - - const SizedBox(height: Spacing.lg - Spacing.xs), - ], - ), - ), - ); - } - - void _showTagDialog(BuildContext context, WidgetRef ref) async { - final activeConversation = ref.read(activeConversationProvider); - if (activeConversation == null) return; - - final controller = TextEditingController(); - - showDialog( - context: context, - builder: (context) => StatefulBuilder( - builder: (context, setState) => AlertDialog( - backgroundColor: context.conduitTheme.surfaceBackground, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.dialog), - ), - title: Text( - 'Manage Tags', - style: TextStyle(color: context.conduitTheme.textPrimary), - ), - content: SizedBox( - width: double.maxFinite, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Add new tag input - TextField( - controller: controller, - style: TextStyle(color: context.conduitTheme.textPrimary), - decoration: InputDecoration( - hintText: 'Add a tag', - hintStyle: TextStyle( - color: context.conduitTheme.inputPlaceholder, - ), - border: OutlineInputBorder( - borderSide: BorderSide( - color: context.conduitTheme.inputBorder, - ), - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: context.conduitTheme.inputBorder, - ), - ), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: context.conduitTheme.buttonPrimary, - ), - ), - suffixIcon: IconButton( - icon: Icon( - Icons.add, - color: context.conduitTheme.buttonPrimary, - ), - onPressed: () async { - final tag = controller.text.trim(); - if (tag.isNotEmpty) { - try { - final api = ref.read(apiServiceProvider); - if (api != null) { - await api.addTagToConversation( - activeConversation.id, - tag, - ); - controller.clear(); - setState(() {}); // Refresh the dialog - - if (context.mounted) {} - } - } catch (e) { - if (context.mounted) {} - } - } - }, - ), - ), - ), - - const SizedBox(height: Spacing.md), - - // Current tags - FutureBuilder>( - future: _loadConversationTags(ref, activeConversation.id), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return Center( - child: CircularProgressIndicator( - color: context.conduitTheme.buttonPrimary, - ), - ); - } - - final tags = snapshot.data ?? []; - - if (tags.isEmpty) { - return Text( - 'No tags yet', - style: TextStyle( - color: context.conduitTheme.textSecondary, - ), - ); - } - - return Wrap( - spacing: 8, - runSpacing: 8, - children: tags - .map( - (tag) => Chip( - label: Text( - tag, - style: TextStyle( - color: context.conduitTheme.textPrimary, - ), - ), - backgroundColor: context - .conduitTheme - .buttonPrimary - .withValues(alpha: 0.2), - deleteIcon: Icon( - Icons.close, - color: context.conduitTheme.iconSecondary, - size: IconSize.sm, - ), - onDeleted: () async { - try { - final api = ref.read(apiServiceProvider); - if (api != null) { - await api.removeTagFromConversation( - activeConversation.id, - tag, - ); - setState(() {}); // Refresh the dialog - - if (context.mounted) {} - } - } catch (e) { - if (context.mounted) {} - } - }, - ), - ) - .toList(), - ); - }, - ), - ], - ), - ), - actions: [ - TextButton( - onPressed: () { - controller.dispose(); - Navigator.pop(context); - }, - child: Text( - 'Done', - style: TextStyle( - color: AppTheme.neutral50.withValues(alpha: Alpha.strong), - ), - ), - ), - ], - ), - ), - ); - } - - Future> _loadConversationTags( - WidgetRef ref, - String conversationId, - ) async { - try { - final api = ref.read(apiServiceProvider); - if (api != null) { - return await api.getConversationTags(conversationId); - } - } catch (e) { - // Return empty list on error - } - return []; - } - - void _archiveMessages(BuildContext context, WidgetRef ref) async { - final activeConversation = ref.read(activeConversationProvider); - if (activeConversation == null) return; - - final confirmed = await ThemedDialogs.confirm( - context, - title: 'Archive Conversation', - message: - 'Archive this conversation? You can find it in the archived conversations section.', - confirmText: 'Archive', - ); - - if (confirmed == true) { - try { - final api = ref.read(apiServiceProvider); - if (api != null) { - await api.archiveConversation(activeConversation.id, true); - ref.invalidate(conversationsProvider); - ref.invalidate(archivedConversationsProvider); - - if (context.mounted) { - // Navigate back or clear current conversation - Navigator.of(context).popUntil((route) => route.isFirst); - } - } - } catch (e) { - if (context.mounted) {} - } - } - } - - void _showDeleteConfirmation(BuildContext context, WidgetRef ref) async { - final confirmed = await ThemedDialogs.confirm( - context, - title: 'Delete Messages', - message: - 'Are you sure you want to delete ${messages.length} message${messages.length == 1 ? '' : 's'}? This action cannot be undone.', - confirmText: 'Delete', - isDestructive: true, - ); - - if (confirmed == true && context.mounted) { - _deleteMessages(context, ref); - } - } - - void _deleteMessages(BuildContext context, WidgetRef ref) async { - final activeConversation = ref.read(activeConversationProvider); - if (activeConversation == null) return; - - final confirmed = await ThemedDialogs.confirm( - context, - title: 'Delete Conversation', - message: - 'Are you sure you want to delete this conversation?\n\nThis action cannot be undone.', - confirmText: 'Delete', - isDestructive: true, - ); - - if (confirmed == true) { - try { - final api = ref.read(apiServiceProvider); - if (api != null) { - await api.deleteConversation(activeConversation.id); - ref.invalidate(conversationsProvider); - ref.invalidate(archivedConversationsProvider); - - // Clear the current conversation - ref.read(activeConversationProvider.notifier).state = null; - ref.read(chatMessagesProvider.notifier).clearMessages(); - - if (context.mounted) { - // Navigate back to conversation list - Navigator.of(context).popUntil((route) => route.isFirst); - } - } - } catch (e) { - if (context.mounted) {} - } - } - } -} - -/// Export dialog with options -class ExportDialog extends ConsumerStatefulWidget { - final List messages; - final ExportFormat format; - - const ExportDialog({super.key, required this.messages, required this.format}); - - @override - ConsumerState createState() => _ExportDialogState(); -} - -class _ExportDialogState extends ConsumerState { - bool _includeTimestamps = true; - bool _includeMetadata = false; - bool _includeAttachments = true; - bool _isExporting = false; - - @override - Widget build(BuildContext context) { - final conduitTheme = context.conduitTheme; - - return AlertDialog( - backgroundColor: AppTheme.neutral700, - title: Text('Export Options', style: conduitTheme.headingMedium), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Export ${widget.messages.length} messages as ${widget.format.name.toUpperCase()}', - style: conduitTheme.bodyMedium?.copyWith( - color: AppTheme.neutral50.withValues(alpha: 0.8), - ), - ), - - const SizedBox(height: Spacing.lg - Spacing.xs), - - // Export options - CheckboxListTile( - title: const Text( - 'Include timestamps', - style: TextStyle(color: AppTheme.neutral50), - ), - value: _includeTimestamps, - onChanged: (value) => - setState(() => _includeTimestamps = value ?? true), - activeColor: AppTheme.brandPrimary, - ), - - CheckboxListTile( - title: const Text( - 'Include metadata', - style: TextStyle(color: AppTheme.neutral50), - ), - value: _includeMetadata, - onChanged: (value) => - setState(() => _includeMetadata = value ?? false), - activeColor: AppTheme.brandPrimary, - ), - - CheckboxListTile( - title: const Text( - 'Include attachments', - style: TextStyle(color: AppTheme.neutral50), - ), - value: _includeAttachments, - onChanged: (value) => - setState(() => _includeAttachments = value ?? true), - activeColor: AppTheme.brandPrimary, - ), - ], - ), - actions: [ - TextButton( - onPressed: _isExporting ? null : () => Navigator.pop(context), - child: const Text('Cancel'), - ), - TextButton( - onPressed: _isExporting ? null : _performExport, - child: _isExporting - ? const SizedBox( - width: Spacing.md, - height: Spacing.md, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Text('Export'), - ), - ], - ); - } - - Future _performExport() async { - setState(() => _isExporting = true); - - try { - final batchService = ref.read(messageBatchServiceProvider); - final options = ExportOptions( - includeTimestamps: _includeTimestamps, - includeMetadata: _includeMetadata, - includeAttachments: _includeAttachments, - ); - - final result = await batchService.exportMessages( - messages: widget.messages, - format: widget.format, - options: options, - ); - - if (result.success && mounted) { - Navigator.pop(context); - - // In a real app, you would save the file or share it - // For now, we'll copy to clipboard - final content = result.data?['content'] as String?; - if (content != null) { - await Clipboard.setData(ClipboardData(text: content)); - - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Export copied to clipboard (${widget.format.name.toUpperCase()})', - ), - backgroundColor: AppTheme.success, - ), - ); - } - } - } else if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Export failed: ${result.error}'), - backgroundColor: AppTheme.error, - ), - ); - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Export error: $e'), - backgroundColor: AppTheme.error, - ), - ); - } - } finally { - if (mounted) { - setState(() => _isExporting = false); - } - } - } -} diff --git a/lib/features/chat/widgets/tag_management_dialog.dart b/lib/features/chat/widgets/tag_management_dialog.dart deleted file mode 100644 index 54f8807..0000000 --- a/lib/features/chat/widgets/tag_management_dialog.dart +++ /dev/null @@ -1,237 +0,0 @@ -import 'dart:io'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; - -import '../../../shared/theme/theme_extensions.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -import '../../../core/models/conversation.dart'; -import '../../../core/providers/app_providers.dart'; - -class TagManagementDialog extends ConsumerStatefulWidget { - final Conversation conversation; - - const TagManagementDialog({super.key, required this.conversation}); - - @override - ConsumerState createState() => - _TagManagementDialogState(); -} - -class _TagManagementDialogState extends ConsumerState { - final _tagController = TextEditingController(); - bool _isAdding = false; - - @override - void dispose() { - _tagController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final conversationTags = widget.conversation.tags; - - return Dialog( - child: Container( - width: 400, - constraints: const BoxConstraints(maxHeight: 500), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Header - Container( - padding: const EdgeInsets.all(Spacing.md), - decoration: BoxDecoration( - color: theme.colorScheme.primaryContainer, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(AppBorderRadius.lg), - ), - ), - child: Row( - children: [ - Icon( - Platform.isIOS ? CupertinoIcons.tag : Icons.label, - color: theme.colorScheme.onPrimaryContainer, - ), - const SizedBox(width: Spacing.sm), - Text( - 'Manage Tags', - style: theme.textTheme.titleLarge?.copyWith( - color: theme.colorScheme.onPrimaryContainer, - fontWeight: FontWeight.w600, - ), - ), - const Spacer(), - IconButton( - icon: Icon( - Platform.isIOS ? CupertinoIcons.xmark : Icons.close, - color: theme.colorScheme.onPrimaryContainer, - ), - onPressed: () => Navigator.pop(context), - ), - ], - ), - ), - - // Add new tag section - Padding( - padding: const EdgeInsets.all(Spacing.md), - child: Row( - children: [ - Expanded( - child: TextField( - controller: _tagController, - decoration: InputDecoration( - hintText: 'Add new tag', - border: const OutlineInputBorder(), - prefixIcon: Icon( - Platform.isIOS - ? CupertinoIcons.tag_fill - : Icons.label, - ), - ), - onSubmitted: (_) => _addTag(), - ), - ), - const SizedBox(width: Spacing.sm), - ElevatedButton( - onPressed: _isAdding ? null : _addTag, - child: _isAdding - ? const SizedBox( - width: Spacing.md, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Text('Add'), - ), - ], - ), - ), - - const Divider(height: 1), - - // Current tags - Expanded( - child: conversationTags.isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Platform.isIOS - ? CupertinoIcons.tag - : Icons.label_outline, - size: 48, - color: theme.colorScheme.onSurface.withValues( - alpha: 0.3, - ), - ), - const SizedBox(height: Spacing.md), - Text( - 'No tags yet', - style: theme.textTheme.bodyLarge?.copyWith( - color: theme.colorScheme.onSurface.withValues( - alpha: 0.6, - ), - ), - ), - const SizedBox(height: Spacing.sm), - Text( - 'Add tags to organize and find conversations easily', - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurface.withValues( - alpha: 0.5, - ), - ), - textAlign: TextAlign.center, - ), - ], - ), - ) - : ListView.builder( - padding: const EdgeInsets.all(Spacing.md), - itemCount: conversationTags.length, - itemBuilder: (context, index) { - final tag = conversationTags[index]; - return _buildTagChip(context, tag); - }, - ), - ), - - // Bottom actions - Padding( - padding: const EdgeInsets.all(Spacing.md), - child: SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () => Navigator.pop(context), - child: const Text('Done'), - ), - ), - ), - ], - ), - ), - ); - } - - Widget _buildTagChip(BuildContext context, String tag) { - final theme = Theme.of(context); - - return Container( - margin: const EdgeInsets.only(bottom: 8), - child: Chip( - avatar: Icon( - Platform.isIOS ? CupertinoIcons.tag_fill : Icons.label, - size: 16, - color: theme.colorScheme.onPrimaryContainer, - ), - label: Text(tag), - backgroundColor: theme.colorScheme.primaryContainer, - deleteIcon: Icon( - Platform.isIOS ? CupertinoIcons.xmark_circle_fill : Icons.cancel, - size: 18, - ), - onDeleted: () => _removeTag(tag), - ), - ); - } - - Future _addTag() async { - final tag = _tagController.text.trim(); - if (tag.isEmpty || widget.conversation.tags.contains(tag)) return; - - setState(() => _isAdding = true); - - try { - final api = ref.read(apiServiceProvider); - if (api == null) throw Exception('No API service available'); - - await api.addTagToConversation(widget.conversation.id, tag); - ref.invalidate(conversationsProvider); - _tagController.clear(); - - if (mounted) {} - } catch (e) { - if (mounted) {} - } finally { - setState(() => _isAdding = false); - } - } - - Future _removeTag(String tag) async { - try { - final api = ref.read(apiServiceProvider); - if (api == null) throw Exception('No API service available'); - - await api.removeTagFromConversation(widget.conversation.id, tag); - ref.invalidate(conversationsProvider); - - if (mounted) {} - } catch (e) { - if (mounted) {} - } - } -} diff --git a/lib/features/server/providers/server_providers.dart b/lib/features/server/providers/server_providers.dart deleted file mode 100644 index ce59488..0000000 --- a/lib/features/server/providers/server_providers.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../core/models/server_config.dart'; -import '../../../core/providers/app_providers.dart'; - -// Server management providers -final addServerProvider = FutureProvider.family(( - ref, - server, -) async { - final storage = ref.read(optimizedStorageServiceProvider); - final configs = await storage.getServerConfigs(); - - // Add new server - configs.add(server); - - // Save updated list - await storage.saveServerConfigs(configs); - - // Refresh the server list - ref.invalidate(serverConfigsProvider); -}); - -final deleteServerProvider = FutureProvider.family(( - ref, - serverId, -) async { - final storage = ref.read(optimizedStorageServiceProvider); - final configs = await storage.getServerConfigs(); - - // Remove server with matching ID - configs.removeWhere((config) => config.id == serverId); - - // Save updated list - await storage.saveServerConfigs(configs); - - // If this was the active server, clear active server ID - final activeId = await storage.getActiveServerId(); - if (activeId == serverId) { - await storage.setActiveServerId(null); - } - - // Refresh providers - ref.invalidate(serverConfigsProvider); - ref.invalidate(activeServerProvider); -}); - -final setActiveServerProvider = FutureProvider.family(( - ref, - serverId, -) async { - final storage = ref.read(optimizedStorageServiceProvider); - await storage.setActiveServerId(serverId); - - // Refresh active server provider - ref.invalidate(activeServerProvider); -}); diff --git a/lib/shared/utils/keyboard_utils.dart b/lib/shared/utils/keyboard_utils.dart deleted file mode 100644 index 15c623d..0000000 --- a/lib/shared/utils/keyboard_utils.dart +++ /dev/null @@ -1,435 +0,0 @@ -import 'package:flutter/material.dart'; -import '../theme/theme_extensions.dart'; -import 'package:flutter/services.dart'; -import 'dart:io' show Platform; - -/// Enhanced keyboard handling utilities for better UX -class KeyboardUtils { - KeyboardUtils._(); - - /// Dismiss keyboard with haptic feedback - static void dismissKeyboard(BuildContext context) { - final currentFocus = FocusScope.of(context); - if (!currentFocus.hasPrimaryFocus && currentFocus.focusedChild != null) { - FocusManager.instance.primaryFocus?.unfocus(); - - // Add haptic feedback on iOS - if (Platform.isIOS) { - HapticFeedback.lightImpact(); - } - } - } - - /// Force dismiss keyboard immediately - static void forceDismissKeyboard() { - FocusManager.instance.primaryFocus?.unfocus(); - SystemChannels.textInput.invokeMethod('TextInput.hide'); - } - - /// Check if keyboard is currently visible - static bool isKeyboardVisible(BuildContext context) { - return MediaQuery.of(context).viewInsets.bottom > 0; - } - - /// Get keyboard height - static double getKeyboardHeight(BuildContext context) { - return MediaQuery.of(context).viewInsets.bottom; - } - - /// Move focus to next field - static void nextFocus(BuildContext context) { - FocusScope.of(context).nextFocus(); - } - - /// Move focus to previous field - static void previousFocus(BuildContext context) { - FocusScope.of(context).previousFocus(); - } - - /// Request focus for a specific node - static void requestFocus(BuildContext context, FocusNode focusNode) { - FocusScope.of(context).requestFocus(focusNode); - } - - /// Create a tap detector that dismisses keyboard when tapping outside text fields - static Widget dismissKeyboardOnTap({ - required BuildContext context, - required Widget child, - }) { - return GestureDetector( - onTap: () => dismissKeyboard(context), - // Let children handle taps first (e.g., TextField gains focus) - behavior: HitTestBehavior.deferToChild, - child: child, - ); - } -} - -/// Widget that automatically adjusts for keyboard visibility -class KeyboardAware extends StatefulWidget { - final Widget child; - final EdgeInsets? padding; - final bool maintainBottomViewPadding; - final Duration animationDuration; - final Curve animationCurve; - - const KeyboardAware({ - super.key, - required this.child, - this.padding, - this.maintainBottomViewPadding = true, - this.animationDuration = const Duration(milliseconds: 250), - this.animationCurve = Curves.easeInOut, - }); - - @override - State createState() => _KeyboardAwareState(); -} - -class _KeyboardAwareState extends State - with WidgetsBindingObserver { - double _keyboardHeight = 0; - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addObserver(this); - } - - @override - void dispose() { - WidgetsBinding.instance.removeObserver(this); - super.dispose(); - } - - @override - void didChangeMetrics() { - super.didChangeMetrics(); - final newKeyboardHeight = MediaQuery.of(context).viewInsets.bottom; - if (newKeyboardHeight != _keyboardHeight) { - setState(() { - _keyboardHeight = newKeyboardHeight; - }); - } - } - - @override - Widget build(BuildContext context) { - return AnimatedPadding( - duration: widget.animationDuration, - curve: widget.animationCurve, - padding: EdgeInsets.only( - bottom: widget.maintainBottomViewPadding ? _keyboardHeight : 0, - ).add(widget.padding ?? EdgeInsets.zero), - child: widget.child, - ); - } -} - -/// Enhanced text field with better keyboard handling -class EnhancedTextField extends StatefulWidget { - final TextEditingController? controller; - final FocusNode? focusNode; - final String? hintText; - final String? labelText; - final TextInputType? keyboardType; - final TextInputAction? textInputAction; - final ValueChanged? onChanged; - final ValueChanged? onSubmitted; - final VoidCallback? onTap; - final bool obscureText; - final bool enabled; - final int? maxLines; - final int? minLines; - final EdgeInsets? contentPadding; - final Widget? prefixIcon; - final Widget? suffixIcon; - final bool autofocus; - final bool dismissKeyboardOnSubmit; - - const EnhancedTextField({ - super.key, - this.controller, - this.focusNode, - this.hintText, - this.labelText, - this.keyboardType, - this.textInputAction, - this.onChanged, - this.onSubmitted, - this.onTap, - this.obscureText = false, - this.enabled = true, - this.maxLines = 1, - this.minLines, - this.contentPadding, - this.prefixIcon, - this.suffixIcon, - this.autofocus = false, - this.dismissKeyboardOnSubmit = true, - }); - - @override - State createState() => _EnhancedTextFieldState(); -} - -class _EnhancedTextFieldState extends State { - late FocusNode _focusNode; - bool _hasFocus = false; - - @override - void initState() { - super.initState(); - _focusNode = widget.focusNode ?? FocusNode(); - _focusNode.addListener(_onFocusChanged); - } - - @override - void dispose() { - _focusNode.removeListener(_onFocusChanged); - if (widget.focusNode == null) { - _focusNode.dispose(); - } - super.dispose(); - } - - void _onFocusChanged() { - setState(() { - _hasFocus = _focusNode.hasFocus; - }); - } - - void _handleSubmitted(String value) { - widget.onSubmitted?.call(value); - - if (widget.dismissKeyboardOnSubmit) { - KeyboardUtils.dismissKeyboard(context); - } - - // Add haptic feedback - if (Platform.isIOS) { - HapticFeedback.lightImpact(); - } - } - - @override - Widget build(BuildContext context) { - return AnimatedContainer( - duration: const Duration(milliseconds: 200), - decoration: BoxDecoration( - border: Border.all( - color: _hasFocus - ? context.conduitTheme.buttonPrimary - : context.conduitTheme.inputBorder, - width: _hasFocus ? 2 : 1, - ), - borderRadius: BorderRadius.circular(AppBorderRadius.md), - ), - child: TextField( - controller: widget.controller, - focusNode: _focusNode, - obscureText: widget.obscureText, - enabled: widget.enabled, - autofocus: widget.autofocus, - keyboardType: widget.keyboardType, - textInputAction: widget.textInputAction, - maxLines: widget.maxLines, - minLines: widget.minLines, - style: TextStyle( - color: context.conduitTheme.textPrimary, - fontSize: AppTypography.bodyLarge, - ), - decoration: InputDecoration( - hintText: widget.hintText, - labelText: widget.labelText, - hintStyle: TextStyle(color: context.conduitTheme.inputPlaceholder), - labelStyle: TextStyle( - color: _hasFocus - ? context.conduitTheme.buttonPrimary - : context.conduitTheme.textSecondary, - ), - prefixIcon: widget.prefixIcon, - suffixIcon: widget.suffixIcon, - contentPadding: - widget.contentPadding ?? - const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - ), - onChanged: widget.onChanged, - onSubmitted: _handleSubmitted, - onTap: widget.onTap, - ), - ); - } -} - -/// Smart keyboard handler that manages multiple text fields -class SmartKeyboardHandler extends StatefulWidget { - final List focusNodes; - final Widget child; - final VoidCallback? onDone; - - const SmartKeyboardHandler({ - super.key, - required this.focusNodes, - required this.child, - this.onDone, - }); - - @override - State createState() => _SmartKeyboardHandlerState(); -} - -class _SmartKeyboardHandlerState extends State { - int _currentIndex = -1; - - @override - void initState() { - super.initState(); - _setupFocusListeners(); - } - - void _setupFocusListeners() { - for (int i = 0; i < widget.focusNodes.length; i++) { - widget.focusNodes[i].addListener(() => _onFocusChanged(i)); - } - } - - void _onFocusChanged(int index) { - if (widget.focusNodes[index].hasFocus) { - setState(() { - _currentIndex = index; - }); - } - } - - void _moveToNext() { - if (_currentIndex < widget.focusNodes.length - 1) { - KeyboardUtils.requestFocus(context, widget.focusNodes[_currentIndex + 1]); - } else { - KeyboardUtils.dismissKeyboard(context); - widget.onDone?.call(); - } - } - - void _moveToPrevious() { - if (_currentIndex > 0) { - KeyboardUtils.requestFocus(context, widget.focusNodes[_currentIndex - 1]); - } - } - - @override - Widget build(BuildContext context) { - return Focus( - onKeyEvent: (node, event) { - if (event is KeyDownEvent) { - if (event.logicalKey == LogicalKeyboardKey.tab) { - if (HardwareKeyboard.instance.isShiftPressed) { - _moveToPrevious(); - } else { - _moveToNext(); - } - return KeyEventResult.handled; - } - } - return KeyEventResult.ignored; - }, - child: widget.child, - ); - } - - @override - void dispose() { - for (final focusNode in widget.focusNodes) { - focusNode.removeListener(() {}); - } - super.dispose(); - } -} - -/// Keyboard-aware scroll view that adjusts scroll position -class KeyboardAwareScrollView extends StatefulWidget { - final ScrollController? controller; - final Widget child; - final EdgeInsets? padding; - final bool reverse; - final Duration animationDuration; - - const KeyboardAwareScrollView({ - super.key, - this.controller, - required this.child, - this.padding, - this.reverse = false, - this.animationDuration = const Duration(milliseconds: 300), - }); - - @override - State createState() => - _KeyboardAwareScrollViewState(); -} - -class _KeyboardAwareScrollViewState extends State - with WidgetsBindingObserver { - late ScrollController _scrollController; - FocusNode? _currentFocus; - - @override - void initState() { - super.initState(); - _scrollController = widget.controller ?? ScrollController(); - WidgetsBinding.instance.addObserver(this); - } - - @override - void dispose() { - WidgetsBinding.instance.removeObserver(this); - if (widget.controller == null) { - _scrollController.dispose(); - } - super.dispose(); - } - - @override - void didChangeMetrics() { - super.didChangeMetrics(); - _adjustScrollPosition(); - } - - void _adjustScrollPosition() { - final focus = FocusManager.instance.primaryFocus; - if (focus != null && focus != _currentFocus) { - _currentFocus = focus; - - WidgetsBinding.instance.addPostFrameCallback((_) { - if (_scrollController.hasClients) { - final keyboardHeight = MediaQuery.of(context).viewInsets.bottom; - if (keyboardHeight > 0) { - _scrollController.animateTo( - _scrollController.offset + keyboardHeight / 2, - duration: widget.animationDuration, - curve: Curves.easeInOut, - ); - } - } - }); - } - } - - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - controller: _scrollController, - reverse: widget.reverse, - padding: widget.padding, - child: widget.child, - ); - } -} diff --git a/lib/shared/widgets/cached_image.dart b/lib/shared/widgets/cached_image.dart deleted file mode 100644 index d31b9af..0000000 --- a/lib/shared/widgets/cached_image.dart +++ /dev/null @@ -1,143 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:cached_network_image/cached_network_image.dart'; -import '../theme/theme_extensions.dart'; -import 'improved_loading_states.dart'; - -/// Cached network image widget with progressive loading and error handling -class CachedImage extends StatelessWidget { - final String imageUrl; - final double? width; - final double? height; - final BoxFit fit; - final Widget? placeholder; - final Widget? errorWidget; - final Duration fadeInDuration; - final Duration fadeOutDuration; - final bool enableMemoryCache; - final int? maxWidthDiskCache; - final int? maxHeightDiskCache; - - const CachedImage({ - super.key, - required this.imageUrl, - this.width, - this.height, - this.fit = BoxFit.cover, - this.placeholder, - this.errorWidget, - this.fadeInDuration = const Duration(milliseconds: 300), - this.fadeOutDuration = const Duration(milliseconds: 100), - this.enableMemoryCache = true, - this.maxWidthDiskCache, - this.maxHeightDiskCache, - }); - - @override - Widget build(BuildContext context) { - return CachedNetworkImage( - imageUrl: imageUrl, - width: width, - height: height, - fit: fit, - fadeInDuration: fadeInDuration, - fadeOutDuration: fadeOutDuration, - placeholder: placeholder != null - ? (context, url) => placeholder! - : _buildDefaultPlaceholder, - errorWidget: errorWidget != null - ? (context, url, error) => errorWidget! - : _buildDefaultErrorWidget, - memCacheWidth: enableMemoryCache ? width?.toInt() : null, - memCacheHeight: enableMemoryCache ? height?.toInt() : null, - maxWidthDiskCache: maxWidthDiskCache, - maxHeightDiskCache: maxHeightDiskCache, - useOldImageOnUrlChange: true, - filterQuality: FilterQuality.medium, - ); - } - - Widget _buildDefaultPlaceholder(BuildContext context, String url) { - return ShimmerLoader( - width: width ?? double.infinity, - height: height ?? 200, - borderRadius: BorderRadius.circular(8), - ); - } - - Widget _buildDefaultErrorWidget( - BuildContext context, - String url, - dynamic error, - ) { - return Container( - width: width, - height: height, - color: context.conduitTheme.shimmerBase, - child: Icon( - Icons.broken_image, - color: context.conduitTheme.iconSecondary, - size: (width != null && height != null) - ? (width! < height! ? width! * 0.5 : height! * 0.5) - : 24, - ), - ); - } -} - -/// Cached circular avatar with progressive loading -class CachedAvatar extends StatelessWidget { - final String? imageUrl; - final String fallbackText; - final double radius; - final Color? backgroundColor; - final Color? textColor; - - const CachedAvatar({ - super.key, - this.imageUrl, - required this.fallbackText, - this.radius = 20, - this.backgroundColor, - this.textColor, - }); - - @override - Widget build(BuildContext context) { - return CircleAvatar( - radius: radius, - backgroundColor: - backgroundColor ?? context.conduitTheme.surfaceBackground, - child: imageUrl != null - ? ClipOval( - child: CachedNetworkImage( - imageUrl: imageUrl!, - width: radius * 2, - height: radius * 2, - fit: BoxFit.cover, - placeholder: (context, url) => CircularProgressIndicator( - strokeWidth: 2, - color: textColor ?? context.conduitTheme.iconSecondary, - ), - errorWidget: (context, url, error) => Text( - fallbackText.isNotEmpty ? fallbackText[0].toUpperCase() : '?', - style: TextStyle( - color: textColor ?? context.conduitTheme.textPrimary, - fontWeight: FontWeight.bold, - fontSize: radius * 0.6, - ), - ), - memCacheWidth: (radius * 2).toInt(), - memCacheHeight: (radius * 2).toInt(), - ), - ) - : Text( - fallbackText.isNotEmpty ? fallbackText[0].toUpperCase() : '?', - style: TextStyle( - color: textColor ?? context.conduitTheme.textPrimary, - fontWeight: FontWeight.bold, - fontSize: radius * 0.6, - ), - ), - ); - } -} diff --git a/lib/shared/widgets/empty_states.dart b/lib/shared/widgets/empty_states.dart deleted file mode 100644 index 6eb415e..0000000 --- a/lib/shared/widgets/empty_states.dart +++ /dev/null @@ -1,448 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter_animate/flutter_animate.dart'; -import 'dart:io' show Platform; -import '../theme/theme_extensions.dart'; - -import '../services/brand_service.dart'; - -/// Enhanced empty state widgets with illustrations and actions -class ConduitEmptyState extends StatelessWidget { - final String title; - final String? subtitle; - final IconData? icon; - final Widget? illustration; - final List? actions; - final bool isLoading; - - const ConduitEmptyState({ - super.key, - required this.title, - this.subtitle, - this.icon, - this.illustration, - this.actions, - this.isLoading = false, - }); - - @override - Widget build(BuildContext context) { - final conduitTheme = context.conduitTheme; - - return Center( - child: Padding( - padding: const EdgeInsets.all(Spacing.xl), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Illustration or icon - if (illustration != null) - illustration! - else if (icon != null) - Container( - width: IconSize.xxl * 2.5, // 120px equivalent - height: IconSize.xxl * 2.5, // 120px equivalent - decoration: BoxDecoration( - color: conduitTheme.cardBackground, - shape: BoxShape.circle, - border: Border.all(color: conduitTheme.cardBorder, width: 2), - ), - child: Icon( - icon!, - size: IconSize.xxl, - color: context.conduitTheme.iconSecondary, - ), - ) - else - // Default to brand icon when no specific icon or illustration provided - BrandService.createBrandEmptyStateIcon( - size: IconSize.xxl * 2.5, // 120px equivalent - showBackground: true, - ), - - const SizedBox(height: Spacing.xl), - - // Title - Text( - title, - style: conduitTheme.headingMedium, - textAlign: TextAlign.center, - ), - - // Subtitle - if (subtitle != null) ...[ - const SizedBox(height: Spacing.xs), - Text( - subtitle!, - style: conduitTheme.bodyMedium?.copyWith( - color: context.conduitTheme.textSecondary, - ), - textAlign: TextAlign.center, - ), - ], - - // Actions - if (actions != null && actions!.isNotEmpty) ...[ - const SizedBox(height: Spacing.xl), - ...actions!.map( - (action) => Padding( - padding: const EdgeInsets.only(bottom: Spacing.xs), - child: _buildActionButton(context, action), - ), - ), - ], - ], - ), - ), - ).animate().fadeIn( - duration: const Duration(milliseconds: 300), - curve: Curves.easeOut, - ); - } - - Widget _buildActionButton(BuildContext context, EmptyStateAction action) { - return SizedBox( - width: double.infinity, - child: FilledButton( - onPressed: action.onPressed, - style: action.isPrimary - ? FilledButton.styleFrom( - backgroundColor: context.conduitTheme.buttonPrimary, - foregroundColor: context.conduitTheme.buttonPrimaryText, - ) - : FilledButton.styleFrom( - backgroundColor: Colors.transparent, - foregroundColor: context.conduitTheme.textSecondary, - side: BorderSide( - color: context.conduitTheme.dividerColor, - width: 1, - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (action.icon != null) ...[ - Icon(action.icon, size: IconSize.md), - const SizedBox(width: Spacing.sm), - ], - Text(action.label), - ], - ), - ), - ); - } -} - -/// Action for empty states -class EmptyStateAction { - final String label; - final VoidCallback onPressed; - final IconData? icon; - final bool isPrimary; - - const EmptyStateAction({ - required this.label, - required this.onPressed, - this.icon, - this.isPrimary = true, - }); -} - -/// Chat-specific empty state -class ChatEmptyState extends StatelessWidget { - final VoidCallback? onStartChat; - - const ChatEmptyState({super.key, this.onStartChat}); - - @override - Widget build(BuildContext context) { - return ConduitEmptyState( - title: 'Start a conversation', - subtitle: - 'Ask me anything! I\'m here to help with questions, creative tasks, analysis, and more.', - // Remove custom illustration to use default brand icon - icon: BrandService.primaryIcon, - actions: onStartChat != null - ? [ - EmptyStateAction( - label: 'Start chatting', - icon: BrandService.primaryIcon, - onPressed: onStartChat!, - ), - ] - : null, - ); - } -} - -/// Files empty state -class FilesEmptyState extends StatelessWidget { - final VoidCallback? onUploadFile; - - const FilesEmptyState({super.key, this.onUploadFile}); - - @override - Widget build(BuildContext context) { - return ConduitEmptyState( - title: 'No files yet', - subtitle: - 'Upload documents, images, or other files to get started with your knowledge base.', - illustration: Builder( - builder: (context) => _buildFilesIllustration(context), - ), - actions: onUploadFile != null - ? [ - EmptyStateAction( - label: 'Upload files', - icon: Platform.isIOS - ? CupertinoIcons.doc_on_doc - : Icons.upload_file, - onPressed: onUploadFile!, - ), - ] - : null, - ); - } - - Widget _buildFilesIllustration(BuildContext context) { - return SizedBox( - width: 120, - height: 120, - child: Stack( - alignment: Alignment.center, - children: [ - // Background circle - Container( - width: IconSize.xxl * 2.5, // 120px equivalent - height: IconSize.xxl * 2.5, // 120px equivalent - decoration: BoxDecoration( - color: context.conduitTheme.info.withValues(alpha: 0.1), - shape: BoxShape.circle, - ), - ), - // File stack - ...List.generate(3, (index) { - return Positioned( - top: 30 + (index * 8.0), - left: 30 + (index * 4.0), - child: - Container( - width: TouchTarget.minimum, - height: 50, - decoration: BoxDecoration( - color: [ - context.conduitTheme.info, - context.conduitTheme.success, - context.conduitTheme.warning, - ][index], - borderRadius: BorderRadius.circular( - AppBorderRadius.xs, - ), - ), - child: Icon( - [Icons.description, Icons.image, Icons.folder][index], - color: context.conduitTheme.textInverse, - size: IconSize.md, - ), - ) - .animate(delay: Duration(milliseconds: index * 200)) - .fadeIn() - .slideY(begin: 0.3, end: 0), - ); - }), - ], - ), - ); - } -} - -/// Tools empty state -class ToolsEmptyState extends StatelessWidget { - final VoidCallback? onExploreTools; - - const ToolsEmptyState({super.key, this.onExploreTools}); - - @override - Widget build(BuildContext context) { - return ConduitEmptyState( - title: 'Powerful tools await', - subtitle: 'Discover tools to enhance your productivity and creativity.', - illustration: Builder( - builder: (context) => _buildToolsIllustration(context), - ), - actions: onExploreTools != null - ? [ - EmptyStateAction( - label: 'Explore tools', - icon: Platform.isIOS - ? CupertinoIcons.wand_stars - : Icons.auto_awesome, - onPressed: onExploreTools!, - ), - ] - : null, - ); - } - - Widget _buildToolsIllustration(BuildContext context) { - return SizedBox( - width: 120, - height: 120, - child: Stack( - alignment: Alignment.center, - children: [ - // Background circle - Container( - width: IconSize.xxl * 2.5, // 120px equivalent - height: IconSize.xxl * 2.5, // 120px equivalent - decoration: BoxDecoration( - color: context.conduitTheme.buttonPrimary.withValues(alpha: 0.1), - shape: BoxShape.circle, - ), - ), - // Tools arrangement - ...List.generate(6, (index) { - final angle = (index * 60) * (3.14159 / 180); - final radius = 35.0; - return Positioned( - top: 60 + (radius * -cos(angle)) - 15, - left: 60 + (radius * sin(angle)) - 15, - child: - Container( - width: Spacing.xl - Spacing.xxs, // 30px equivalent - height: Spacing.xl - Spacing.xxs, // 30px equivalent - decoration: BoxDecoration( - color: context.conduitTheme.buttonPrimary, - shape: BoxShape.circle, - ), - child: Icon( - [ - Icons.palette, - Icons.calculate, - Icons.code, - Icons.translate, - Icons.music_note, - Icons.analytics, - ][index], - color: context.conduitTheme.textInverse, - size: IconSize.sm, - ), - ) - .animate(delay: Duration(milliseconds: index * 100)) - .fadeIn() - .scale( - begin: const Offset(0.5, 0.5), - end: const Offset(1.0, 1.0), - ), - ); - }), - ], - ), - ); - } -} - -/// Search results empty state -class SearchEmptyState extends StatelessWidget { - final String query; - final VoidCallback? onClearSearch; - - const SearchEmptyState({super.key, required this.query, this.onClearSearch}); - - @override - Widget build(BuildContext context) { - return ConduitEmptyState( - title: 'No results found', - subtitle: 'No results for "$query". Try adjusting your search terms.', - icon: Platform.isIOS ? CupertinoIcons.search : Icons.search_off, - actions: onClearSearch != null - ? [ - EmptyStateAction( - label: 'Clear search', - icon: Platform.isIOS ? CupertinoIcons.clear : Icons.clear, - onPressed: onClearSearch!, - isPrimary: false, - ), - ] - : null, - ); - } -} - -/// Connection error empty state -class ConnectionEmptyState extends StatelessWidget { - final VoidCallback? onRetry; - - const ConnectionEmptyState({super.key, this.onRetry}); - - @override - Widget build(BuildContext context) { - return ConduitEmptyState( - title: 'Connection problem', - subtitle: - 'Unable to load content. Please check your connection and try again.', - icon: Platform.isIOS ? CupertinoIcons.wifi_slash : Icons.wifi_off, - actions: onRetry != null - ? [ - EmptyStateAction( - label: 'Try again', - icon: Platform.isIOS ? CupertinoIcons.refresh : Icons.refresh, - onPressed: onRetry!, - ), - ] - : null, - ); - } -} - -/// Generic empty state with custom illustration -class CustomEmptyState extends StatelessWidget { - final String title; - final String subtitle; - final Widget illustration; - final List? actions; - - const CustomEmptyState({ - super.key, - required this.title, - required this.subtitle, - required this.illustration, - this.actions, - }); - - @override - Widget build(BuildContext context) { - return ConduitEmptyState( - title: title, - subtitle: subtitle, - illustration: illustration, - actions: actions, - ); - } -} - -// Helper function to get cosine -double cos(double radians) { - // Simple cosine approximation for illustration positioning - if (radians == 0) return 1.0; - if (radians == 1.5708) return 0.0; // π/2 - if (radians == 3.14159) return -1.0; // π - if (radians == 4.71239) return 0.0; // 3π/2 - - // Taylor series approximation for other values - double x2 = radians * radians; - return 1 - x2 / 2 + x2 * x2 / 24 - x2 * x2 * x2 / 720; -} - -// Helper function to get sine -double sin(double radians) { - // Simple sine approximation for illustration positioning - if (radians == 0) return 0.0; - if (radians == 1.5708) return 1.0; // π/2 - if (radians == 3.14159) return 0.0; // π - if (radians == 4.71239) return -1.0; // 3π/2 - - // Taylor series approximation for other values - double x2 = radians * radians; - return radians - radians * x2 / 6 + radians * x2 * x2 / 120; -} diff --git a/lib/shared/widgets/error_widgets.dart b/lib/shared/widgets/error_widgets.dart deleted file mode 100644 index ac2a5f1..0000000 --- a/lib/shared/widgets/error_widgets.dart +++ /dev/null @@ -1,397 +0,0 @@ -import 'package:flutter/material.dart'; -import '../theme/theme_extensions.dart'; -import 'conduit_components.dart'; - -/// Enhanced error widget with production-grade design and better hierarchy -class ConduitErrorWidget extends StatelessWidget { - final String title; - final String message; - final String? actionLabel; - final VoidCallback? onAction; - final IconData? icon; - final bool isCompact; - - const ConduitErrorWidget({ - super.key, - required this.title, - required this.message, - this.actionLabel, - this.onAction, - this.icon, - this.isCompact = false, - }); - - @override - Widget build(BuildContext context) { - return Container( - padding: EdgeInsets.all(isCompact ? Spacing.md : Spacing.cardPadding), - decoration: BoxDecoration( - color: context.conduitTheme.errorBackground.withValues( - alpha: Alpha.badgeBackground, - ), - borderRadius: BorderRadius.circular(AppBorderRadius.card), - border: Border.all( - color: context.conduitTheme.error.withValues(alpha: Alpha.subtle), - width: BorderWidth.standard, - ), - boxShadow: ConduitShadows.card, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - icon ?? Icons.error_outline, - size: isCompact ? IconSize.large : IconSize.xl, - color: context.conduitTheme.error, - ), - SizedBox(height: isCompact ? Spacing.sm : Spacing.md), - Text( - title, - style: AppTypography.headlineSmallStyle.copyWith( - color: context.conduitTheme.error, - fontWeight: FontWeight.w600, - ), - textAlign: TextAlign.center, - ), - SizedBox(height: isCompact ? Spacing.xs : Spacing.sm), - Text( - message, - style: AppTypography.standard.copyWith( - color: context.conduitTheme.textSecondary, - ), - textAlign: TextAlign.center, - ), - if (actionLabel != null && onAction != null) ...[ - SizedBox(height: isCompact ? Spacing.md : Spacing.lg), - SizedBox( - width: double.infinity, - child: ConduitButton( - text: actionLabel!, - onPressed: onAction, - isDestructive: true, - isCompact: isCompact, - ), - ), - ], - ], - ), - ); - } -} - -/// Enhanced network error widget with better hierarchy -class NetworkErrorWidget extends StatelessWidget { - final VoidCallback? onRetry; - final String? customMessage; - final bool isCompact; - - const NetworkErrorWidget({ - super.key, - this.onRetry, - this.customMessage, - this.isCompact = false, - }); - - @override - Widget build(BuildContext context) { - return ConduitErrorWidget( - title: 'Connection Error', - message: - customMessage ?? - 'Unable to connect to the server. Please check your internet connection and try again.', - actionLabel: 'Retry', - onAction: onRetry, - icon: Icons.wifi_off, - isCompact: isCompact, - ); - } -} - -/// Enhanced empty state widget with better hierarchy -class EmptyStateWidget extends StatelessWidget { - final String title; - final String message; - final IconData? icon; - final String? actionLabel; - final VoidCallback? onAction; - final bool isCompact; - - const EmptyStateWidget({ - super.key, - required this.title, - required this.message, - this.icon, - this.actionLabel, - this.onAction, - this.isCompact = false, - }); - - @override - Widget build(BuildContext context) { - return Container( - padding: EdgeInsets.all(isCompact ? Spacing.md : Spacing.cardPadding), - decoration: BoxDecoration( - color: context.conduitTheme.cardBackground, - borderRadius: BorderRadius.circular(AppBorderRadius.card), - border: Border.all( - color: context.conduitTheme.cardBorder, - width: BorderWidth.standard, - ), - boxShadow: ConduitShadows.card, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - icon ?? Icons.inbox_outlined, - size: isCompact ? IconSize.large : IconSize.xxl, - 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.xs : Spacing.sm), - Text( - message, - style: AppTypography.standard.copyWith( - color: context.conduitTheme.textSecondary, - ), - textAlign: TextAlign.center, - ), - if (actionLabel != null && onAction != null) ...[ - SizedBox(height: isCompact ? Spacing.md : Spacing.lg), - SizedBox( - width: double.infinity, - child: ConduitButton( - text: actionLabel!, - onPressed: onAction, - isCompact: isCompact, - ), - ), - ], - ], - ), - ); - } -} - -/// Enhanced loading error widget with better hierarchy -class LoadingErrorWidget extends StatelessWidget { - final String message; - final VoidCallback? onRetry; - final bool isCompact; - - const LoadingErrorWidget({ - super.key, - required this.message, - this.onRetry, - this.isCompact = false, - }); - - @override - Widget build(BuildContext context) { - return ConduitErrorWidget( - title: 'Loading Failed', - message: message, - actionLabel: onRetry != null ? 'Try Again' : null, - onAction: onRetry, - icon: Icons.error_outline, - isCompact: isCompact, - ); - } -} - -/// Enhanced validation error widget with better hierarchy -class ValidationErrorWidget extends StatelessWidget { - final String fieldName; - final String message; - final VoidCallback? onFix; - final bool isCompact; - - const ValidationErrorWidget({ - super.key, - required this.fieldName, - required this.message, - this.onFix, - this.isCompact = false, - }); - - @override - Widget build(BuildContext context) { - return ConduitErrorWidget( - title: 'Invalid $fieldName', - message: message, - actionLabel: onFix != null ? 'Fix Now' : null, - onAction: onFix, - icon: Icons.warning_amber_outlined, - isCompact: isCompact, - ); - } -} - -/// Enhanced permission error widget with better hierarchy -class PermissionErrorWidget extends StatelessWidget { - final String permission; - final String message; - final VoidCallback? onGrant; - final bool isCompact; - - const PermissionErrorWidget({ - super.key, - required this.permission, - required this.message, - this.onGrant, - this.isCompact = false, - }); - - @override - Widget build(BuildContext context) { - return ConduitErrorWidget( - title: 'Permission Required', - message: 'This app needs $permission permission to $message.', - actionLabel: onGrant != null ? 'Grant Permission' : null, - onAction: onGrant, - icon: Icons.security, - isCompact: isCompact, - ); - } -} - -/// Enhanced server error widget with better hierarchy -class ServerErrorWidget extends StatelessWidget { - final String error; - final VoidCallback? onRetry; - final bool isCompact; - - const ServerErrorWidget({ - super.key, - required this.error, - this.onRetry, - this.isCompact = false, - }); - - @override - Widget build(BuildContext context) { - return ConduitErrorWidget( - title: 'Server Error', - message: error, - actionLabel: onRetry != null ? 'Retry' : null, - onAction: onRetry, - icon: Icons.cloud_off, - isCompact: isCompact, - ); - } -} - -/// Enhanced file error widget with better hierarchy -class FileErrorWidget extends StatelessWidget { - final String fileName; - final String error; - final VoidCallback? onRetry; - final bool isCompact; - - const FileErrorWidget({ - super.key, - required this.fileName, - required this.error, - this.onRetry, - this.isCompact = false, - }); - - @override - Widget build(BuildContext context) { - return ConduitErrorWidget( - title: 'File Error', - message: 'Failed to process $fileName: $error', - actionLabel: onRetry != null ? 'Try Again' : null, - onAction: onRetry, - icon: Icons.file_present, - isCompact: isCompact, - ); - } -} - -/// Enhanced authentication error widget with better hierarchy -class AuthErrorWidget extends StatelessWidget { - final String message; - final VoidCallback? onLogin; - final bool isCompact; - - const AuthErrorWidget({ - super.key, - required this.message, - this.onLogin, - this.isCompact = false, - }); - - @override - Widget build(BuildContext context) { - return ConduitErrorWidget( - title: 'Authentication Required', - message: message, - actionLabel: onLogin != null ? 'Sign In' : null, - onAction: onLogin, - icon: Icons.lock_outline, - isCompact: isCompact, - ); - } -} - -/// Enhanced offline error widget with better hierarchy -class OfflineErrorWidget extends StatelessWidget { - final String message; - final VoidCallback? onRetry; - final bool isCompact; - - const OfflineErrorWidget({ - super.key, - this.message = - 'You\'re currently offline. Please check your internet connection.', - this.onRetry, - this.isCompact = false, - }); - - @override - Widget build(BuildContext context) { - return ConduitErrorWidget( - title: 'Offline', - message: message, - actionLabel: onRetry != null ? 'Retry' : null, - onAction: onRetry, - icon: Icons.wifi_off, - isCompact: isCompact, - ); - } -} - -/// Enhanced timeout error widget with better hierarchy -class TimeoutErrorWidget extends StatelessWidget { - final String operation; - final VoidCallback? onRetry; - final bool isCompact; - - const TimeoutErrorWidget({ - super.key, - required this.operation, - this.onRetry, - this.isCompact = false, - }); - - @override - Widget build(BuildContext context) { - return ConduitErrorWidget( - title: 'Request Timeout', - message: 'The $operation request timed out. Please try again.', - actionLabel: onRetry != null ? 'Retry' : null, - onAction: onRetry, - icon: Icons.timer_off, - isCompact: isCompact, - ); - } -} diff --git a/pubspec.lock b/pubspec.lock index bb4ef95..45f6c42 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -178,7 +178,7 @@ packages: source: hosted version: "4.10.1" collection: - dependency: "direct main" + dependency: transitive description: name: collection sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" @@ -505,7 +505,7 @@ packages: source: hosted version: "2.3.2" highlight: - dependency: "direct main" + dependency: transitive description: name: highlight sha256: "5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21" diff --git a/pubspec.yaml b/pubspec.yaml index 3ca0dfd..cfc1305 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,7 +25,6 @@ dependencies: # UI Components - Enhanced Markdown markdown_widget: ^2.3.2+8 flutter_highlight: ^0.7.0 - highlight: ^0.7.0 cached_network_image: ^3.3.1 @@ -43,15 +42,14 @@ dependencies: # Utilities path: ^1.9.0 uuid: ^4.5.0 - collection: ^1.18.0 crypto: ^3.0.3 package_info_plus: ^8.0.2 url_launcher: ^6.3.0 - + # Icons & Theming cupertino_icons: ^1.0.8 - freezed_annotation: ^3.0.0 json_annotation: ^4.9.0 + freezed_annotation: ^3.0.0 google_fonts: ^6.2.1 wakelock_plus: ^1.2.10 share_plus: ^11.1.0