refactor: cleanup unsued files

This commit is contained in:
cogwheel0
2025-08-23 11:54:41 +05:30
parent 7f30b728ab
commit b898adbe40
19 changed files with 9 additions and 6490 deletions

View File

@@ -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;
}
}

View File

@@ -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<DeepLinkService>((ref) => DeepLinkService());

View File

@@ -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<ErrorRecoveryAction> 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<void> 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<bool> 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,
],
],
),
),
);
},
);
}
}

View File

@@ -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<String, RetryConfig> _retryConfigs = {};
final Map<String, DateTime> _lastRetryTimes = {};
ErrorRecoveryService(Dio dio);
/// Execute an operation with automatic retry and recovery
Future<T> executeWithRecovery<T>({
required String operationId,
required Future<T> 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<void> execute(dynamic error, int attempt);
}
/// Reconnect to server recovery action
class ReconnectAction extends RecoveryAction {
final Future<void> Function() reconnectFunction;
ReconnectAction(this.reconnectFunction);
@override
Future<void> 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<void> Function() refreshFunction;
RefreshTokenAction(this.refreshFunction);
@override
Future<void> 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<void> Function() clearCacheFunction;
ClearCacheAction(this.clearCacheFunction);
@override
Future<void> 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<ErrorRecoveryService>((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<ErrorBoundary> createState() => _ErrorBoundaryState();
}
class _ErrorBoundaryState extends State<ErrorBoundary> {
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<ErrorDetector> createState() => _ErrorDetectorState();
}
class _ErrorDetectorState extends State<ErrorDetector> {
@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);
};
}
}

View File

@@ -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<String, FocusNode> _focusNodes = {};
static final Map<String, FocusNode> _disposedNodes = {};
static FocusNode? _lastFocusedNode;
static final List<FocusNode> _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<FocusNode> 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<FocusManager> createState() => _FocusManagerState();
}
class _FocusManagerState extends State<FocusManager> {
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<String>? onChanged;
final VoidCallback? onEditingComplete;
final ValueChanged<String>? onSubmitted;
final List<TextInputFormatter>? 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<AccessibleFormField> createState() => _AccessibleFormFieldState();
}
class _AccessibleFormFieldState extends State<AccessibleFormField> {
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,
),
),
),
),
],
),
);
}
}

View File

@@ -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<String, dynamic> 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<String, dynamic> toJson() => {
'routeName': routeName,
'arguments': arguments,
'timestamp': timestamp.toIso8601String(),
'conversationId': conversationId,
'tabIndex': tabIndex,
};
factory NavigationState.fromJson(Map<String, dynamic> 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<NavigationState> _navigationStack = [];
NavigationState? _currentState;
final ValueNotifier<NavigationState?> _stateNotifier = ValueNotifier(null);
/// Initialize the service
Future<void> 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<NavigationState?> get stateNotifier => _stateNotifier;
/// Get current navigation state
NavigationState? get currentState => _currentState;
/// Get navigation stack
List<NavigationState> get navigationStack =>
List.unmodifiable(_navigationStack);
/// Push a new navigation state
Future<void> pushState({
required String routeName,
Map<String, dynamic> 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<NavigationState?> 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<void> updateCurrentState({
String? conversationId,
int? tabIndex,
Map<String, dynamic>? additionalArgs,
}) async {
try {
if (_currentState == null) return;
final updatedArgs = <String, dynamic>{
..._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<void> 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<void> replaceStack(List<NavigationState> 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<void> handleDeepLink({
required String routeName,
Map<String, dynamic> 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<NavigationBreadcrumb> generateBreadcrumbs() {
final breadcrumbs = <NavigationBreadcrumb>[];
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<void> 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<void> 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<void> _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<void> _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<String, dynamic>) {
_navigationStack.add(NavigationState.fromJson(stateJson));
}
}
}
// Load current state
final currentStateJsonString = _prefs!.getString(_currentStateKey);
if (currentStateJsonString != null) {
final currentStateJson =
jsonDecode(currentStateJsonString) as Map<String, dynamic>;
_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<void> _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<String, dynamic> arguments;
final bool isActive;
final bool canNavigateBack;
NavigationBreadcrumb({
required this.title,
required this.routeName,
required this.arguments,
required this.isActive,
required this.canNavigateBack,
});
}

View File

@@ -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<String, String> imageDataCache;
final Map<String, bool> loadingStates;
final Map<String, String> errorStates;
AttachmentCacheState({
required this.imageDataCache,
required this.loadingStates,
required this.errorStates,
});
AttachmentCacheState copyWith({
Map<String, String>? imageDataCache,
Map<String, bool>? loadingStates,
Map<String, String>? errorStates,
}) {
return AttachmentCacheState(
imageDataCache: imageDataCache ?? this.imageDataCache,
loadingStates: loadingStates ?? this.loadingStates,
errorStates: errorStates ?? this.errorStates,
);
}
}
class AttachmentCacheNotifier extends StateNotifier<AttachmentCacheState> {
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<String, String>.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<String, String>.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<String, String>.from(state.imageDataCache);
final newLoadingStates = Map<String, bool>.from(state.loadingStates);
final newErrorStates = Map<String, String>.from(state.errorStates);
newImageCache.remove(attachmentId);
newLoadingStates.remove(attachmentId);
newErrorStates.remove(attachmentId);
state = AttachmentCacheState(
imageDataCache: newImageCache,
loadingStates: newLoadingStates,
errorStates: newErrorStates,
);
}
}
final attachmentCacheProvider =
StateNotifierProvider<AttachmentCacheNotifier, AttachmentCacheState>((ref) {
return AttachmentCacheNotifier();
});
// Helper providers for easier access
final cachedImageDataProvider = Provider.family<String?, String>((ref, attachmentId) {
final cache = ref.watch(attachmentCacheProvider);
return cache.imageDataCache[attachmentId];
});
final attachmentLoadingStateProvider = Provider.family<bool, String>((ref, attachmentId) {
final cache = ref.watch(attachmentCacheProvider);
return cache.loadingStates[attachmentId] ?? false;
});
final attachmentErrorStateProvider = Provider.family<String?, String>((ref, attachmentId) {
final cache = ref.watch(attachmentCacheProvider);
return cache.errorStates[attachmentId];
});

View File

@@ -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<void, String>((
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<void> Function() onTap;
final VoidCallback onDelete;
const ModernConversationTile({
super.key,
required this.conversation,
required this.isActive,
required this.onTap,
required this.onDelete,
});
@override
ConsumerState<ModernConversationTile> createState() =>
_ModernConversationTileState();
}
class _ModernConversationTileState extends ConsumerState<ModernConversationTile>
with SingleTickerProviderStateMixin {
bool _isLoading = false;
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 150),
vsync: this,
);
_scaleAnimation = Tween<double>(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<bool?> _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<Color>(
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<String>(
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<PopupMenuItem<String>> _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<String>(
enabled: false,
child: Divider(color: AppTheme.neutral600, height: BorderWidth.regular),
),
_buildMenuItem(
'delete',
Platform.isIOS ? CupertinoIcons.delete : Icons.delete_rounded,
'Delete',
isDestructive: true,
),
];
}
PopupMenuItem<String> _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<void> _handleTap() async {
setState(() => _isLoading = true);
try {
await widget.onTap();
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
}
}
Future<void> _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<void> _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<void> _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<void> _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<void> _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<Conversation> 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<void> _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}';
}
}
}

View File

@@ -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')}';
}
}

View File

@@ -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<FolderManagementDialog> createState() =>
_FolderManagementDialogState();
}
class _FolderManagementDialogState
extends ConsumerState<FolderManagementDialog> {
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<Folder> 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<String>(
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<void> _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<void> _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<String>(
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<bool>(
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');
}
}
}
}
}

View File

@@ -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<ChatMessage> 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<ChatMessage> 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<void> _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<ChatMessage> 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<ChatMessage> 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<List<String>>(
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<List<String>> _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<ChatMessage> messages;
final ExportFormat format;
const ExportDialog({super.key, required this.messages, required this.format});
@override
ConsumerState<ExportDialog> createState() => _ExportDialogState();
}
class _ExportDialogState extends ConsumerState<ExportDialog> {
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<void> _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);
}
}
}
}

View File

@@ -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<TagManagementDialog> createState() =>
_TagManagementDialogState();
}
class _TagManagementDialogState extends ConsumerState<TagManagementDialog> {
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<void> _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<void> _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) {}
}
}
}

View File

@@ -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<void, ServerConfig>((
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<void, String>((
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<void, String>((
ref,
serverId,
) async {
final storage = ref.read(optimizedStorageServiceProvider);
await storage.setActiveServerId(serverId);
// Refresh active server provider
ref.invalidate(activeServerProvider);
});

View File

@@ -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<KeyboardAware> createState() => _KeyboardAwareState();
}
class _KeyboardAwareState extends State<KeyboardAware>
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<String>? onChanged;
final ValueChanged<String>? 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<EnhancedTextField> createState() => _EnhancedTextFieldState();
}
class _EnhancedTextFieldState extends State<EnhancedTextField> {
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<FocusNode> focusNodes;
final Widget child;
final VoidCallback? onDone;
const SmartKeyboardHandler({
super.key,
required this.focusNodes,
required this.child,
this.onDone,
});
@override
State<SmartKeyboardHandler> createState() => _SmartKeyboardHandlerState();
}
class _SmartKeyboardHandlerState extends State<SmartKeyboardHandler> {
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<KeyboardAwareScrollView> createState() =>
_KeyboardAwareScrollViewState();
}
class _KeyboardAwareScrollViewState extends State<KeyboardAwareScrollView>
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,
);
}
}

View File

@@ -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,
),
),
);
}
}

View File

@@ -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<EmptyStateAction>? 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<EmptyStateAction>? 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;
}

View File

@@ -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,
);
}
}

View File

@@ -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"

View File

@@ -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