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