refactor: more logs
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import 'package:flutter/foundation.dart' hide debugPrint;
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
// Types are used through app_providers.dart
|
// Types are used through app_providers.dart
|
||||||
import '../providers/app_providers.dart';
|
import '../providers/app_providers.dart';
|
||||||
@@ -7,11 +7,6 @@ import 'token_validator.dart';
|
|||||||
import 'auth_cache_manager.dart';
|
import 'auth_cache_manager.dart';
|
||||||
import '../utils/debug_logger.dart';
|
import '../utils/debug_logger.dart';
|
||||||
|
|
||||||
void debugPrint(String? message, {int? wrapWidth}) {
|
|
||||||
if (message == null) return;
|
|
||||||
DebugLogger.fromLegacy(message, scope: 'auth/state');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Comprehensive auth state representation
|
/// Comprehensive auth state representation
|
||||||
@immutable
|
@immutable
|
||||||
class AuthState {
|
class AuthState {
|
||||||
@@ -153,7 +148,7 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('ERROR: Auth initialization failed: $e');
|
DebugLogger.error('auth-init-failed', scope: 'auth/state', error: e);
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
status: AuthStatus.error,
|
status: AuthStatus.error,
|
||||||
error: 'Failed to initialize auth: $e',
|
error: 'Failed to initialize auth: $e',
|
||||||
@@ -244,7 +239,7 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
throw Exception('Invalid API key or insufficient permissions');
|
throw Exception('Invalid API key or insufficient permissions');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('ERROR: API key login failed: $e');
|
DebugLogger.error('api-key-login-failed', scope: 'auth/state', error: e);
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
status: AuthStatus.error,
|
status: AuthStatus.error,
|
||||||
error: e.toString(),
|
error: e.toString(),
|
||||||
@@ -325,7 +320,7 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
DebugLogger.auth('Login successful');
|
DebugLogger.auth('Login successful');
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('ERROR: Login failed: $e');
|
DebugLogger.error('login-failed', scope: 'auth/state', error: e);
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
status: AuthStatus.error,
|
status: AuthStatus.error,
|
||||||
error: e.toString(),
|
error: e.toString(),
|
||||||
@@ -433,7 +428,7 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
return await login(username, password, rememberCredentials: false);
|
return await login(username, password, rememberCredentials: false);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('ERROR: Silent login failed: $e');
|
DebugLogger.error('silent-login-failed', scope: 'auth/state', error: e);
|
||||||
|
|
||||||
// Clear invalid credentials on auth errors
|
// Clear invalid credentials on auth errors
|
||||||
if (e.toString().contains('401') ||
|
if (e.toString().contains('401') ||
|
||||||
@@ -496,7 +491,11 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
try {
|
try {
|
||||||
await api.logout();
|
await api.logout();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Warning: Server logout failed: $e');
|
DebugLogger.warning(
|
||||||
|
'server-logout-failed',
|
||||||
|
scope: 'auth/state',
|
||||||
|
data: {'error': e.toString()},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,7 +515,7 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
|
|
||||||
DebugLogger.auth('Logout complete');
|
DebugLogger.auth('Logout complete');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('ERROR: Logout failed: $e');
|
DebugLogger.error('logout-failed', scope: 'auth/state', error: e);
|
||||||
// Even if logout fails, clear local state
|
// Even if logout fails, clear local state
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
status: AuthStatus.unauthenticated,
|
status: AuthStatus.unauthenticated,
|
||||||
@@ -551,7 +550,11 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
// Fall back to server data loading
|
// Fall back to server data loading
|
||||||
await _loadServerUserData();
|
await _loadServerUserData();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Warning: Failed to load user data: $e');
|
DebugLogger.warning(
|
||||||
|
'user-data-load-failed',
|
||||||
|
scope: 'auth/state',
|
||||||
|
data: {'error': e.toString()},
|
||||||
|
);
|
||||||
// Don't update state on user data load failure
|
// Don't update state on user data load failure
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -563,9 +566,7 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
if (api != null && state.isAuthenticated) {
|
if (api != null && state.isAuthenticated) {
|
||||||
// Check if we already have user data from token validation
|
// Check if we already have user data from token validation
|
||||||
if (state.user != null) {
|
if (state.user != null) {
|
||||||
debugPrint(
|
DebugLogger.auth('user-data-present-from-token', scope: 'auth/state');
|
||||||
'DEBUG: User data already available from token validation',
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -574,7 +575,11 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
DebugLogger.auth('Loaded complete user data from server');
|
DebugLogger.auth('Loaded complete user data from server');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Warning: Failed to load server user data: $e');
|
DebugLogger.warning(
|
||||||
|
'server-user-data-load-failed',
|
||||||
|
scope: 'auth/state',
|
||||||
|
data: {'error': e.toString()},
|
||||||
|
);
|
||||||
// Don't update state on server data load failure - keep JWT data if available
|
// Don't update state on server data load failure - keep JWT data if available
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -605,21 +610,25 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
// Fast format validation first
|
// Fast format validation first
|
||||||
final formatResult = TokenValidator.validateTokenFormat(token);
|
final formatResult = TokenValidator.validateTokenFormat(token);
|
||||||
if (!formatResult.isValid) {
|
if (!formatResult.isValid) {
|
||||||
debugPrint('DEBUG: Token format invalid: ${formatResult.message}');
|
DebugLogger.warning(
|
||||||
|
'token-format-invalid',
|
||||||
|
scope: 'auth/state',
|
||||||
|
data: {'message': formatResult.message},
|
||||||
|
);
|
||||||
TokenValidationCache.cacheResult(token, formatResult);
|
TokenValidationCache.cacheResult(token, formatResult);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If format is valid but token is expiring soon, try server validation
|
// If format is valid but token is expiring soon, try server validation
|
||||||
if (formatResult.isExpiringSoon) {
|
if (formatResult.isExpiringSoon) {
|
||||||
debugPrint('DEBUG: Token expiring soon, validating with server');
|
DebugLogger.auth('token-expiring-soon', scope: 'auth/state');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server validation (async with timeout)
|
// Server validation (async with timeout)
|
||||||
try {
|
try {
|
||||||
final api = ref.read(apiServiceProvider);
|
final api = ref.read(apiServiceProvider);
|
||||||
if (api == null) {
|
if (api == null) {
|
||||||
debugPrint('DEBUG: No API service available for token validation');
|
DebugLogger.warning('token-validation-no-api', scope: 'auth/state');
|
||||||
return formatResult.isValid; // Fall back to format validation
|
return formatResult.isValid; // Fall back to format validation
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -650,7 +659,11 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
);
|
);
|
||||||
return serverResult.isValid;
|
return serverResult.isValid;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('DEBUG: Token server validation failed: $e');
|
DebugLogger.warning(
|
||||||
|
'token-validation-failed',
|
||||||
|
scope: 'auth/state',
|
||||||
|
data: {'error': e.toString()},
|
||||||
|
);
|
||||||
// On network error, fall back to format validation if it was valid
|
// On network error, fall back to format validation if it was valid
|
||||||
return formatResult.isValid;
|
return formatResult.isValid;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,6 @@ import 'dart:convert';
|
|||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import '../utils/debug_logger.dart';
|
import '../utils/debug_logger.dart';
|
||||||
|
|
||||||
void debugPrint(String? message, {int? wrapWidth}) {
|
|
||||||
if (message == null) return;
|
|
||||||
DebugLogger.fromLegacy(message, scope: 'auth/token-validator');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// JWT token validation utilities
|
/// JWT token validation utilities
|
||||||
class TokenValidator {
|
class TokenValidator {
|
||||||
static const Duration _validationTimeout = Duration(seconds: 5);
|
static const Duration _validationTimeout = Duration(seconds: 5);
|
||||||
@@ -68,8 +63,10 @@ class TokenValidator {
|
|||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// If we can't decode JWT, treat as opaque token
|
// If we can't decode JWT, treat as opaque token
|
||||||
debugPrint(
|
DebugLogger.warning(
|
||||||
'DEBUG: Could not decode JWT payload, treating as opaque token: $e',
|
'jwt-decode-failed',
|
||||||
|
scope: 'auth/token-validator',
|
||||||
|
data: {'error': e.toString()},
|
||||||
);
|
);
|
||||||
return TokenValidationResult.valid('Opaque token format valid');
|
return TokenValidationResult.valid('Opaque token format valid');
|
||||||
}
|
}
|
||||||
@@ -153,7 +150,11 @@ class TokenValidator {
|
|||||||
'iat': payload['iat'], // Issued at
|
'iat': payload['iat'], // Issued at
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('DEBUG: Could not extract user info from token: $e');
|
DebugLogger.warning(
|
||||||
|
'token-user-info-failed',
|
||||||
|
scope: 'auth/token-validator',
|
||||||
|
data: {'error': e.toString()},
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/foundation.dart' hide debugPrint;
|
import 'package:flutter/foundation.dart';
|
||||||
import 'api_error.dart';
|
import 'api_error.dart';
|
||||||
import 'error_parser.dart';
|
import 'error_parser.dart';
|
||||||
import '../utils/debug_logger.dart';
|
import '../utils/debug_logger.dart';
|
||||||
|
|
||||||
void debugPrint(String? message, {int? wrapWidth}) {
|
|
||||||
if (message == null) return;
|
|
||||||
DebugLogger.fromLegacy(message, scope: 'api/error-handler');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Comprehensive API error handler with structured error parsing
|
/// Comprehensive API error handler with structured error parsing
|
||||||
/// Handles all types of API errors and converts them to standardized format
|
/// Handles all types of API errors and converts them to standardized format
|
||||||
class ApiErrorHandler {
|
class ApiErrorHandler {
|
||||||
@@ -39,7 +34,10 @@ class ApiErrorHandler {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Fallback error if transformation itself fails
|
// Fallback error if transformation itself fails
|
||||||
debugPrint('ApiErrorHandler: Error transforming exception: $e');
|
DebugLogger.log(
|
||||||
|
'ApiErrorHandler: Error transforming exception: $e',
|
||||||
|
scope: 'api/error-handler',
|
||||||
|
);
|
||||||
return ApiError.unknown(
|
return ApiError.unknown(
|
||||||
message: 'A system error occurred',
|
message: 'A system error occurred',
|
||||||
originalError: error,
|
originalError: error,
|
||||||
@@ -318,21 +316,33 @@ class ApiErrorHandler {
|
|||||||
String httpMethod,
|
String httpMethod,
|
||||||
) {
|
) {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
debugPrint('🔴 API Error Details:');
|
DebugLogger.log('🔴 API Error Details:', scope: 'api/error-handler');
|
||||||
debugPrint(' Method: ${httpMethod.toUpperCase()}');
|
DebugLogger.log(
|
||||||
debugPrint(' Endpoint: $requestPath');
|
' Method: ${httpMethod.toUpperCase()}',
|
||||||
debugPrint(' Type: ${dioError.type}');
|
scope: 'api/error-handler',
|
||||||
debugPrint(' Status: ${dioError.response?.statusCode}');
|
);
|
||||||
|
DebugLogger.log(' Endpoint: $requestPath', scope: 'api/error-handler');
|
||||||
|
DebugLogger.log(' Type: ${dioError.type}', scope: 'api/error-handler');
|
||||||
|
DebugLogger.log(
|
||||||
|
' Status: ${dioError.response?.statusCode}',
|
||||||
|
scope: 'api/error-handler',
|
||||||
|
);
|
||||||
|
|
||||||
if (dioError.response?.data != null) {
|
if (dioError.response?.data != null) {
|
||||||
DebugLogger.error('Response data available (truncated for security)');
|
DebugLogger.error('Response data available (truncated for security)');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dioError.requestOptions.data != null) {
|
if (dioError.requestOptions.data != null) {
|
||||||
debugPrint(' Request Data: ${dioError.requestOptions.data}');
|
DebugLogger.log(
|
||||||
|
' Request Data: ${dioError.requestOptions.data}',
|
||||||
|
scope: 'api/error-handler',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint(' Error: ${dioError.message}');
|
DebugLogger.log(
|
||||||
|
' Error: ${dioError.message}',
|
||||||
|
scope: 'api/error-handler',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// In production, you would send this to your error tracking service
|
// In production, you would send this to your error tracking service
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/foundation.dart' hide debugPrint;
|
import 'package:flutter/foundation.dart';
|
||||||
import 'api_error_handler.dart';
|
import 'api_error_handler.dart';
|
||||||
import 'api_error.dart';
|
import 'api_error.dart';
|
||||||
import '../utils/debug_logger.dart';
|
import '../utils/debug_logger.dart';
|
||||||
|
|
||||||
void debugPrint(String? message, {int? wrapWidth}) {
|
|
||||||
if (message == null) return;
|
|
||||||
DebugLogger.fromLegacy(message, scope: 'api/error-interceptor');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Dio interceptor for automatic error handling and transformation
|
/// Dio interceptor for automatic error handling and transformation
|
||||||
/// Converts all HTTP errors into standardized ApiError format
|
/// Converts all HTTP errors into standardized ApiError format
|
||||||
class ApiErrorInterceptor extends Interceptor {
|
class ApiErrorInterceptor extends Interceptor {
|
||||||
@@ -51,7 +46,11 @@ class ApiErrorInterceptor extends Interceptor {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Fallback if error transformation fails
|
// Fallback if error transformation fails
|
||||||
debugPrint('ApiErrorInterceptor: Failed to transform error: $e');
|
DebugLogger.error(
|
||||||
|
'transform-failed',
|
||||||
|
scope: 'api/error-interceptor',
|
||||||
|
error: e,
|
||||||
|
);
|
||||||
handler.next(err);
|
handler.next(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,7 +70,16 @@ class ApiErrorInterceptor extends Interceptor {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (logErrors) {
|
if (logErrors) {
|
||||||
debugPrint('🟡 API Error in successful response: $apiError');
|
DebugLogger.warning(
|
||||||
|
'successful-response-error',
|
||||||
|
scope: 'api/error-interceptor',
|
||||||
|
data: {
|
||||||
|
'endpoint': apiError.endpoint,
|
||||||
|
'method': apiError.method,
|
||||||
|
'status': apiError.statusCode,
|
||||||
|
'message': apiError.message,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the error for later handling
|
// Store the error for later handling
|
||||||
@@ -122,74 +130,59 @@ class ApiErrorInterceptor extends Interceptor {
|
|||||||
void _logApiError(ApiError apiError, DioException originalError) {
|
void _logApiError(ApiError apiError, DioException originalError) {
|
||||||
if (!kDebugMode) return;
|
if (!kDebugMode) return;
|
||||||
|
|
||||||
final typeIcon = _getErrorTypeIcon(apiError.type);
|
final payload = <String, Object?>{
|
||||||
debugPrint('$typeIcon API Error [${apiError.type.name.toUpperCase()}]');
|
'type': apiError.type.name,
|
||||||
debugPrint(' Method: ${apiError.method?.toUpperCase() ?? 'UNKNOWN'}');
|
'endpoint': apiError.endpoint,
|
||||||
debugPrint(' Endpoint: ${apiError.endpoint ?? 'unknown'}');
|
'method': apiError.method,
|
||||||
debugPrint(' Status: ${apiError.statusCode ?? 'N/A'}');
|
'status': apiError.statusCode,
|
||||||
debugPrint(' Message: ${apiError.message}');
|
'message': apiError.message,
|
||||||
|
if (apiError.technical != null) 'technical': apiError.technical,
|
||||||
|
if (apiError.retryAfter != null)
|
||||||
|
'retryAfterSeconds': apiError.retryAfter!.inSeconds,
|
||||||
|
'originalType': originalError.type.name,
|
||||||
|
};
|
||||||
|
|
||||||
if (apiError.hasFieldErrors) {
|
if (apiError.hasFieldErrors) {
|
||||||
debugPrint(' Field Errors:');
|
payload['fieldErrors'] = {
|
||||||
for (final entry in apiError.fieldErrors.entries) {
|
for (final entry in apiError.fieldErrors.entries)
|
||||||
final field = entry.key;
|
entry.key: entry.value,
|
||||||
final errors = entry.value;
|
};
|
||||||
debugPrint(' $field: ${errors.join(', ')}');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (apiError.technical != null) {
|
|
||||||
debugPrint(' Technical: ${apiError.technical}');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (apiError.retryAfter != null) {
|
|
||||||
debugPrint(' Retry After: ${apiError.retryAfter!.inSeconds}s');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log original error type for debugging
|
|
||||||
debugPrint(' Original Type: ${originalError.type}');
|
|
||||||
|
|
||||||
// Log request details if available
|
|
||||||
final requestData = originalError.requestOptions.data;
|
final requestData = originalError.requestOptions.data;
|
||||||
if (requestData != null && requestData.toString().length < 500) {
|
if (requestData != null && requestData.toString().length < 500) {
|
||||||
debugPrint(' Request: $requestData');
|
payload['request'] = requestData;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log response data if available and not too large
|
|
||||||
final responseData = originalError.response?.data;
|
final responseData = originalError.response?.data;
|
||||||
if (responseData != null && responseData.toString().length < 1000) {
|
if (responseData != null && responseData.toString().length < 1000) {
|
||||||
DebugLogger.error('Response data available (truncated for security)');
|
payload['response'] = responseData;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Get emoji icon for error type
|
final headers = originalError.requestOptions.headers;
|
||||||
String _getErrorTypeIcon(ApiErrorType type) {
|
if (headers.isNotEmpty) {
|
||||||
switch (type) {
|
payload['requestHeaders'] = {
|
||||||
case ApiErrorType.network:
|
for (final entry in headers.entries) entry.key: entry.value.toString(),
|
||||||
return '🌐';
|
};
|
||||||
case ApiErrorType.timeout:
|
|
||||||
return '⏱️';
|
|
||||||
case ApiErrorType.authentication:
|
|
||||||
return '🔐';
|
|
||||||
case ApiErrorType.authorization:
|
|
||||||
return '🚫';
|
|
||||||
case ApiErrorType.validation:
|
|
||||||
return '✏️';
|
|
||||||
case ApiErrorType.badRequest:
|
|
||||||
return '❌';
|
|
||||||
case ApiErrorType.notFound:
|
|
||||||
return '🔍';
|
|
||||||
case ApiErrorType.server:
|
|
||||||
return '🔥';
|
|
||||||
case ApiErrorType.rateLimit:
|
|
||||||
return '🐌';
|
|
||||||
case ApiErrorType.cancelled:
|
|
||||||
return '🛑';
|
|
||||||
case ApiErrorType.security:
|
|
||||||
return '🔒';
|
|
||||||
case ApiErrorType.unknown:
|
|
||||||
return '❓';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final responseHeaders = originalError.response?.headers;
|
||||||
|
if (responseHeaders != null && responseHeaders.map.isNotEmpty) {
|
||||||
|
payload['responseHeaders'] = responseHeaders.map;
|
||||||
|
}
|
||||||
|
|
||||||
|
final requestDuration =
|
||||||
|
originalError.response?.requestOptions.extra['requestDuration'];
|
||||||
|
if (requestDuration is Duration) {
|
||||||
|
payload['requestDurationMs'] = requestDuration.inMilliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugLogger.error(
|
||||||
|
'api-error',
|
||||||
|
scope: 'api/error-interceptor',
|
||||||
|
data: payload,
|
||||||
|
error: apiError,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract ApiError from DioException if available
|
/// Extract ApiError from DioException if available
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/foundation.dart' hide debugPrint;
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'api_error.dart';
|
import 'api_error.dart';
|
||||||
import 'api_error_handler.dart';
|
import 'api_error_handler.dart';
|
||||||
@@ -9,11 +9,6 @@ import '../../shared/theme/theme_extensions.dart';
|
|||||||
import 'package:conduit/l10n/app_localizations.dart';
|
import 'package:conduit/l10n/app_localizations.dart';
|
||||||
import '../utils/debug_logger.dart';
|
import '../utils/debug_logger.dart';
|
||||||
|
|
||||||
void debugPrint(String? message, {int? wrapWidth}) {
|
|
||||||
if (message == null) return;
|
|
||||||
DebugLogger.fromLegacy(message, scope: 'api/error-service');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Enhanced error service with comprehensive error handling capabilities
|
/// Enhanced error service with comprehensive error handling capabilities
|
||||||
/// Provides unified error management across the application
|
/// Provides unified error management across the application
|
||||||
class EnhancedErrorService {
|
class EnhancedErrorService {
|
||||||
@@ -306,16 +301,31 @@ class EnhancedErrorService {
|
|||||||
}) {
|
}) {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
final timestamp = DateTime.now().toIso8601String();
|
final timestamp = DateTime.now().toIso8601String();
|
||||||
debugPrint('🔴 ERROR [$timestamp] ${context ?? 'Unknown Context'}');
|
DebugLogger.log(
|
||||||
debugPrint(' Message: ${getUserMessage(error)}');
|
'🔴 ERROR [$timestamp] ${context ?? 'Unknown Context'}',
|
||||||
debugPrint(' Technical: ${getTechnicalDetails(error)}');
|
scope: 'api/error-service',
|
||||||
|
);
|
||||||
|
DebugLogger.log(
|
||||||
|
' Message: ${getUserMessage(error)}',
|
||||||
|
scope: 'api/error-service',
|
||||||
|
);
|
||||||
|
DebugLogger.log(
|
||||||
|
' Technical: ${getTechnicalDetails(error)}',
|
||||||
|
scope: 'api/error-service',
|
||||||
|
);
|
||||||
|
|
||||||
if (additionalData != null && additionalData.isNotEmpty) {
|
if (additionalData != null && additionalData.isNotEmpty) {
|
||||||
debugPrint(' Additional Data: $additionalData');
|
DebugLogger.log(
|
||||||
|
' Additional Data: $additionalData',
|
||||||
|
scope: 'api/error-service',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stackTrace != null) {
|
if (stackTrace != null) {
|
||||||
debugPrint(' Stack Trace: $stackTrace');
|
DebugLogger.log(
|
||||||
|
' Stack Trace: $stackTrace',
|
||||||
|
scope: 'api/error-service',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import 'api_error.dart';
|
import 'api_error.dart';
|
||||||
import '../utils/debug_logger.dart';
|
import '../utils/debug_logger.dart';
|
||||||
|
|
||||||
void debugPrint(String? message, {int? wrapWidth}) {
|
|
||||||
if (message == null) return;
|
|
||||||
DebugLogger.fromLegacy(message, scope: 'api/error-parser');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Comprehensive error response parser
|
/// Comprehensive error response parser
|
||||||
/// Handles various API error response formats and extracts structured information
|
/// Handles various API error response formats and extracts structured information
|
||||||
class ErrorParser {
|
class ErrorParser {
|
||||||
@@ -29,7 +24,10 @@ class ErrorParser {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('ErrorParser: Error parsing response: $e');
|
DebugLogger.log(
|
||||||
|
'ErrorParser: Error parsing response: $e',
|
||||||
|
scope: 'api/error-parser',
|
||||||
|
);
|
||||||
return ParsedErrorResponse(
|
return ParsedErrorResponse(
|
||||||
message: 'Failed to parse error response',
|
message: 'Failed to parse error response',
|
||||||
metadata: {
|
metadata: {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,11 +5,6 @@ import 'package:dio/dio.dart';
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import '../utils/debug_logger.dart';
|
import '../utils/debug_logger.dart';
|
||||||
|
|
||||||
void debugPrint(String? message, {int? wrapWidth}) {
|
|
||||||
if (message == null) return;
|
|
||||||
DebugLogger.fromLegacy(message, scope: 'attachments/queue');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Status of a queued attachment upload
|
/// Status of a queued attachment upload
|
||||||
enum QueuedAttachmentStatus { pending, uploading, completed, failed, cancelled }
|
enum QueuedAttachmentStatus { pending, uploading, completed, failed, cancelled }
|
||||||
|
|
||||||
@@ -144,8 +139,9 @@ class AttachmentUploadQueue {
|
|||||||
_prefs ??= await SharedPreferences.getInstance();
|
_prefs ??= await SharedPreferences.getInstance();
|
||||||
await _load();
|
await _load();
|
||||||
_startPeriodicProcessing();
|
_startPeriodicProcessing();
|
||||||
debugPrint(
|
DebugLogger.log(
|
||||||
'DEBUG: AttachmentUploadQueue initialized with ${_queue.length} items',
|
'AttachmentUploadQueue initialized with ${_queue.length} items',
|
||||||
|
scope: 'attachments/queue',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,8 +219,9 @@ class AttachmentUploadQueue {
|
|||||||
|
|
||||||
await _save();
|
await _save();
|
||||||
_notify();
|
_notify();
|
||||||
debugPrint(
|
DebugLogger.log(
|
||||||
'DEBUG: Attachment ${item.id} uploaded successfully (fileId=$fileId)',
|
'Attachment ${item.id} uploaded successfully (fileId=$fileId)',
|
||||||
|
scope: 'attachments/queue',
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
final retries = item.retryCount + 1;
|
final retries = item.retryCount + 1;
|
||||||
@@ -239,8 +236,9 @@ class AttachmentUploadQueue {
|
|||||||
);
|
);
|
||||||
await _save();
|
await _save();
|
||||||
_notify();
|
_notify();
|
||||||
debugPrint(
|
DebugLogger.log(
|
||||||
'WARNING: Attachment ${item.id} failed after $_maxRetries attempts',
|
'WARNING: Attachment ${item.id} failed after $_maxRetries attempts',
|
||||||
|
scope: 'attachments/queue',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -257,8 +255,9 @@ class AttachmentUploadQueue {
|
|||||||
);
|
);
|
||||||
await _save();
|
await _save();
|
||||||
_notify();
|
_notify();
|
||||||
debugPrint(
|
DebugLogger.log(
|
||||||
'DEBUG: Scheduled retry for attachment ${item.id} in ${delay.inSeconds}s',
|
'Scheduled retry for attachment ${item.id} in ${delay.inSeconds}s',
|
||||||
|
scope: 'attachments/queue',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,6 @@ import '../models/server_config.dart';
|
|||||||
import '../models/conversation.dart';
|
import '../models/conversation.dart';
|
||||||
import '../utils/debug_logger.dart';
|
import '../utils/debug_logger.dart';
|
||||||
|
|
||||||
void debugPrint(String? message, {int? wrapWidth}) {
|
|
||||||
if (message == null) return;
|
|
||||||
DebugLogger.fromLegacy(message, scope: 'storage/optimized');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Optimized storage service with single secure storage implementation
|
/// Optimized storage service with single secure storage implementation
|
||||||
/// Eliminates dual storage overhead and improves performance
|
/// Eliminates dual storage overhead and improves performance
|
||||||
class OptimizedStorageService {
|
class OptimizedStorageService {
|
||||||
@@ -46,9 +41,15 @@ class OptimizedStorageService {
|
|||||||
await _secureCredentialStorage.saveAuthToken(token);
|
await _secureCredentialStorage.saveAuthToken(token);
|
||||||
_cache[_authTokenKey] = token;
|
_cache[_authTokenKey] = token;
|
||||||
_cacheTimestamps[_authTokenKey] = DateTime.now();
|
_cacheTimestamps[_authTokenKey] = DateTime.now();
|
||||||
debugPrint('DEBUG: Auth token saved and cached');
|
DebugLogger.log(
|
||||||
|
'Auth token saved and cached',
|
||||||
|
scope: 'storage/optimized',
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('ERROR: Failed to save auth token: $e');
|
DebugLogger.log(
|
||||||
|
'Failed to save auth token: $e',
|
||||||
|
scope: 'storage/optimized',
|
||||||
|
);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,7 +59,7 @@ class OptimizedStorageService {
|
|||||||
if (_isCacheValid(_authTokenKey)) {
|
if (_isCacheValid(_authTokenKey)) {
|
||||||
final cachedToken = _cache[_authTokenKey] as String?;
|
final cachedToken = _cache[_authTokenKey] as String?;
|
||||||
if (cachedToken != null) {
|
if (cachedToken != null) {
|
||||||
debugPrint('DEBUG: Using cached auth token');
|
DebugLogger.log('Using cached auth token', scope: 'storage/optimized');
|
||||||
return cachedToken;
|
return cachedToken;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,7 +72,10 @@ class OptimizedStorageService {
|
|||||||
}
|
}
|
||||||
return token;
|
return token;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('ERROR: Failed to retrieve auth token: $e');
|
DebugLogger.log(
|
||||||
|
'Failed to retrieve auth token: $e',
|
||||||
|
scope: 'storage/optimized',
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,9 +85,15 @@ class OptimizedStorageService {
|
|||||||
await _secureCredentialStorage.deleteAuthToken();
|
await _secureCredentialStorage.deleteAuthToken();
|
||||||
_cache.remove(_authTokenKey);
|
_cache.remove(_authTokenKey);
|
||||||
_cacheTimestamps.remove(_authTokenKey);
|
_cacheTimestamps.remove(_authTokenKey);
|
||||||
debugPrint('DEBUG: Auth token deleted and cache cleared');
|
DebugLogger.log(
|
||||||
|
'Auth token deleted and cache cleared',
|
||||||
|
scope: 'storage/optimized',
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('ERROR: Failed to delete auth token: $e');
|
DebugLogger.log(
|
||||||
|
'Failed to delete auth token: $e',
|
||||||
|
scope: 'storage/optimized',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,9 +114,15 @@ class OptimizedStorageService {
|
|||||||
_cache['has_credentials'] = true;
|
_cache['has_credentials'] = true;
|
||||||
_cacheTimestamps['has_credentials'] = DateTime.now();
|
_cacheTimestamps['has_credentials'] = DateTime.now();
|
||||||
|
|
||||||
debugPrint('DEBUG: Credentials saved via optimized storage');
|
DebugLogger.log(
|
||||||
|
'Credentials saved via optimized storage',
|
||||||
|
scope: 'storage/optimized',
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('ERROR: Failed to save credentials: $e');
|
DebugLogger.log(
|
||||||
|
'Failed to save credentials: $e',
|
||||||
|
scope: 'storage/optimized',
|
||||||
|
);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,7 +138,10 @@ class OptimizedStorageService {
|
|||||||
|
|
||||||
return credentials;
|
return credentials;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('ERROR: Failed to retrieve credentials: $e');
|
DebugLogger.log(
|
||||||
|
'Failed to retrieve credentials: $e',
|
||||||
|
scope: 'storage/optimized',
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,9 +151,15 @@ class OptimizedStorageService {
|
|||||||
await _secureCredentialStorage.deleteSavedCredentials();
|
await _secureCredentialStorage.deleteSavedCredentials();
|
||||||
_cache.remove('has_credentials');
|
_cache.remove('has_credentials');
|
||||||
_cacheTimestamps.remove('has_credentials');
|
_cacheTimestamps.remove('has_credentials');
|
||||||
debugPrint('DEBUG: Credentials deleted via optimized storage');
|
DebugLogger.log(
|
||||||
|
'Credentials deleted via optimized storage',
|
||||||
|
scope: 'storage/optimized',
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('ERROR: Failed to delete credentials: $e');
|
DebugLogger.log(
|
||||||
|
'Failed to delete credentials: $e',
|
||||||
|
scope: 'storage/optimized',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,9 +192,15 @@ class OptimizedStorageService {
|
|||||||
_cache['server_config_count'] = configs.length;
|
_cache['server_config_count'] = configs.length;
|
||||||
_cacheTimestamps['server_config_count'] = DateTime.now();
|
_cacheTimestamps['server_config_count'] = DateTime.now();
|
||||||
|
|
||||||
debugPrint('DEBUG: Server configs saved (${configs.length} configs)');
|
DebugLogger.log(
|
||||||
|
'Server configs saved (${configs.length} configs)',
|
||||||
|
scope: 'storage/optimized',
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('ERROR: Failed to save server configs: $e');
|
DebugLogger.log(
|
||||||
|
'Failed to save server configs: $e',
|
||||||
|
scope: 'storage/optimized',
|
||||||
|
);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,7 +225,10 @@ class OptimizedStorageService {
|
|||||||
|
|
||||||
return configs;
|
return configs;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('ERROR: Failed to retrieve server configs: $e');
|
DebugLogger.log(
|
||||||
|
'Failed to retrieve server configs: $e',
|
||||||
|
scope: 'storage/optimized',
|
||||||
|
);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -275,7 +309,10 @@ class OptimizedStorageService {
|
|||||||
final decoded = jsonDecode(jsonString) as List<dynamic>;
|
final decoded = jsonDecode(jsonString) as List<dynamic>;
|
||||||
return decoded.map((item) => Conversation.fromJson(item)).toList();
|
return decoded.map((item) => Conversation.fromJson(item)).toList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('ERROR: Failed to retrieve local conversations: $e');
|
DebugLogger.log(
|
||||||
|
'Failed to retrieve local conversations: $e',
|
||||||
|
scope: 'storage/optimized',
|
||||||
|
);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -298,11 +335,15 @@ class OptimizedStorageService {
|
|||||||
final jsonString = jsonEncode(lightweightConversations);
|
final jsonString = jsonEncode(lightweightConversations);
|
||||||
await _prefs.setString(_localConversationsKey, jsonString);
|
await _prefs.setString(_localConversationsKey, jsonString);
|
||||||
|
|
||||||
debugPrint(
|
DebugLogger.log(
|
||||||
'DEBUG: Saved ${conversations.length} local conversations (lightweight)',
|
'Saved ${conversations.length} local conversations (lightweight)',
|
||||||
|
scope: 'storage/optimized',
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('ERROR: Failed to save local conversations: $e');
|
DebugLogger.log(
|
||||||
|
'Failed to save local conversations: $e',
|
||||||
|
scope: 'storage/optimized',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,9 +372,15 @@ class OptimizedStorageService {
|
|||||||
key.contains('server'),
|
key.contains('server'),
|
||||||
);
|
);
|
||||||
|
|
||||||
debugPrint('DEBUG: Auth data cleared in batch operation');
|
DebugLogger.log(
|
||||||
|
'Auth data cleared in batch operation',
|
||||||
|
scope: 'storage/optimized',
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('ERROR: Failed to clear auth data: $e');
|
DebugLogger.log(
|
||||||
|
'Failed to clear auth data: $e',
|
||||||
|
scope: 'storage/optimized',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,9 +391,12 @@ class OptimizedStorageService {
|
|||||||
_cache.clear();
|
_cache.clear();
|
||||||
_cacheTimestamps.clear();
|
_cacheTimestamps.clear();
|
||||||
|
|
||||||
debugPrint('DEBUG: All storage cleared');
|
DebugLogger.log('All storage cleared', scope: 'storage/optimized');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('ERROR: Failed to clear all storage: $e');
|
DebugLogger.log(
|
||||||
|
'Failed to clear all storage: $e',
|
||||||
|
scope: 'storage/optimized',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,21 +416,30 @@ class OptimizedStorageService {
|
|||||||
void clearCache() {
|
void clearCache() {
|
||||||
_cache.clear();
|
_cache.clear();
|
||||||
_cacheTimestamps.clear();
|
_cacheTimestamps.clear();
|
||||||
debugPrint('DEBUG: Storage cache cleared');
|
DebugLogger.log('Storage cache cleared', scope: 'storage/optimized');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Migration from old storage service (one-time operation)
|
/// Migration from old storage service (one-time operation)
|
||||||
Future<void> migrateFromLegacyStorage() async {
|
Future<void> migrateFromLegacyStorage() async {
|
||||||
try {
|
try {
|
||||||
debugPrint('DEBUG: Starting migration from legacy storage');
|
DebugLogger.log(
|
||||||
|
'Starting migration from legacy storage',
|
||||||
|
scope: 'storage/optimized',
|
||||||
|
);
|
||||||
|
|
||||||
// This would be called once during app upgrade
|
// This would be called once during app upgrade
|
||||||
// Implementation would depend on the specific migration needs
|
// Implementation would depend on the specific migration needs
|
||||||
// For now, the SecureCredentialStorage already handles legacy migration
|
// For now, the SecureCredentialStorage already handles legacy migration
|
||||||
|
|
||||||
debugPrint('DEBUG: Legacy storage migration completed');
|
DebugLogger.log(
|
||||||
|
'Legacy storage migration completed',
|
||||||
|
scope: 'storage/optimized',
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('ERROR: Legacy storage migration failed: $e');
|
DebugLogger.log(
|
||||||
|
'Legacy storage migration failed: $e',
|
||||||
|
scope: 'storage/optimized',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart' hide debugPrint;
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:share_handler/share_handler.dart' as sh;
|
import 'package:share_handler/share_handler.dart' as sh;
|
||||||
|
|
||||||
@@ -85,7 +85,10 @@ final shareReceiverInitializerProvider = Provider<void>((ref) {
|
|||||||
maybeProcessPending();
|
maybeProcessPending();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('ShareReceiver: failed to get initial shared media: $e');
|
DebugLogger.log(
|
||||||
|
'ShareReceiver: failed to get initial shared media: $e',
|
||||||
|
scope: 'share',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -98,7 +101,10 @@ final shareReceiverInitializerProvider = Provider<void>((ref) {
|
|||||||
maybeProcessPending();
|
maybeProcessPending();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('ShareReceiver: failed to parse shared media: $e');
|
DebugLogger.log(
|
||||||
|
'ShareReceiver: failed to parse shared media: $e',
|
||||||
|
scope: 'share',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -197,11 +203,9 @@ Future<void> _processPayload(Ref ref, SharedPayload payload) async {
|
|||||||
// Do NOT create a server chat here. The chat is created on first send
|
// Do NOT create a server chat here. The chat is created on first send
|
||||||
// (with server syncing + title generation) in chat_providers.dart.
|
// (with server syncing + title generation) in chat_providers.dart.
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('ShareReceiver: failed to process payload: $e');
|
DebugLogger.log(
|
||||||
|
'ShareReceiver: failed to process payload: $e',
|
||||||
|
scope: 'share',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void debugPrint(String? message, {int? wrapWidth}) {
|
|
||||||
if (message == null) return;
|
|
||||||
DebugLogger.fromLegacy(message, scope: 'share');
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,11 +2,6 @@ import 'package:socket_io_client/socket_io_client.dart' as io;
|
|||||||
import '../models/server_config.dart';
|
import '../models/server_config.dart';
|
||||||
import '../utils/debug_logger.dart';
|
import '../utils/debug_logger.dart';
|
||||||
|
|
||||||
void debugPrint(String? message, {int? wrapWidth}) {
|
|
||||||
if (message == null) return;
|
|
||||||
DebugLogger.fromLegacy(message, scope: 'socket');
|
|
||||||
}
|
|
||||||
|
|
||||||
class SocketService {
|
class SocketService {
|
||||||
final ServerConfig serverConfig;
|
final ServerConfig serverConfig;
|
||||||
final bool websocketOnly;
|
final bool websocketOnly;
|
||||||
@@ -97,7 +92,7 @@ class SocketService {
|
|||||||
_socket = io.io(base, builder.build());
|
_socket = io.io(base, builder.build());
|
||||||
|
|
||||||
_socket!.on('connect', (_) {
|
_socket!.on('connect', (_) {
|
||||||
debugPrint('Socket connected: ${_socket!.id}');
|
DebugLogger.log('Socket connected: ${_socket!.id}', scope: 'socket');
|
||||||
if (_authToken != null && _authToken!.isNotEmpty) {
|
if (_authToken != null && _authToken!.isNotEmpty) {
|
||||||
_socket!.emit('user-join', {
|
_socket!.emit('user-join', {
|
||||||
'auth': {'token': _authToken},
|
'auth': {'token': _authToken},
|
||||||
@@ -106,15 +101,18 @@ class SocketService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
_socket!.on('connect_error', (err) {
|
_socket!.on('connect_error', (err) {
|
||||||
debugPrint('Socket connect_error: $err');
|
DebugLogger.log('Socket connect_error: $err', scope: 'socket');
|
||||||
});
|
});
|
||||||
|
|
||||||
_socket!.on('reconnect_attempt', (attempt) {
|
_socket!.on('reconnect_attempt', (attempt) {
|
||||||
debugPrint('Socket reconnect_attempt: $attempt');
|
DebugLogger.log('Socket reconnect_attempt: $attempt', scope: 'socket');
|
||||||
});
|
});
|
||||||
|
|
||||||
_socket!.on('reconnect', (attempt) {
|
_socket!.on('reconnect', (attempt) {
|
||||||
debugPrint('Socket reconnected after $attempt attempts');
|
DebugLogger.log(
|
||||||
|
'Socket reconnected after $attempt attempts',
|
||||||
|
scope: 'socket',
|
||||||
|
);
|
||||||
if (_authToken != null && _authToken!.isNotEmpty) {
|
if (_authToken != null && _authToken!.isNotEmpty) {
|
||||||
// Best-effort rejoin
|
// Best-effort rejoin
|
||||||
_socket!.emit('user-join', {
|
_socket!.emit('user-join', {
|
||||||
@@ -124,11 +122,11 @@ class SocketService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
_socket!.on('reconnect_failed', (_) {
|
_socket!.on('reconnect_failed', (_) {
|
||||||
debugPrint('Socket reconnect_failed');
|
DebugLogger.log('Socket reconnect_failed', scope: 'socket');
|
||||||
});
|
});
|
||||||
|
|
||||||
_socket!.on('disconnect', (reason) {
|
_socket!.on('disconnect', (reason) {
|
||||||
debugPrint('Socket disconnected: $reason');
|
DebugLogger.log('Socket disconnected: $reason', scope: 'socket');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,6 @@ import '../models/conversation.dart';
|
|||||||
import 'secure_credential_storage.dart';
|
import 'secure_credential_storage.dart';
|
||||||
import '../utils/debug_logger.dart';
|
import '../utils/debug_logger.dart';
|
||||||
|
|
||||||
void debugPrint(String? message, {int? wrapWidth}) {
|
|
||||||
if (message == null) return;
|
|
||||||
DebugLogger.fromLegacy(message, scope: 'storage');
|
|
||||||
}
|
|
||||||
|
|
||||||
class StorageService {
|
class StorageService {
|
||||||
final FlutterSecureStorage _secureStorage;
|
final FlutterSecureStorage _secureStorage;
|
||||||
final SharedPreferences _prefs;
|
final SharedPreferences _prefs;
|
||||||
@@ -42,7 +37,10 @@ class StorageService {
|
|||||||
try {
|
try {
|
||||||
await _secureCredentialStorage.saveAuthToken(token);
|
await _secureCredentialStorage.saveAuthToken(token);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Warning: Enhanced secure storage failed, using fallback: $e');
|
DebugLogger.log(
|
||||||
|
'Enhanced secure storage failed, using fallback: $e',
|
||||||
|
scope: 'storage',
|
||||||
|
);
|
||||||
await _secureStorage.write(key: _authTokenKey, value: token);
|
await _secureStorage.write(key: _authTokenKey, value: token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,7 +51,10 @@ class StorageService {
|
|||||||
final token = await _secureCredentialStorage.getAuthToken();
|
final token = await _secureCredentialStorage.getAuthToken();
|
||||||
if (token != null) return token;
|
if (token != null) return token;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Warning: Enhanced secure storage failed, using fallback: $e');
|
DebugLogger.log(
|
||||||
|
'Enhanced secure storage failed, using fallback: $e',
|
||||||
|
scope: 'storage',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to legacy storage
|
// Fallback to legacy storage
|
||||||
@@ -65,7 +66,10 @@ class StorageService {
|
|||||||
try {
|
try {
|
||||||
await _secureCredentialStorage.deleteAuthToken();
|
await _secureCredentialStorage.deleteAuthToken();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Warning: Failed to delete from enhanced storage: $e');
|
DebugLogger.log(
|
||||||
|
'Failed to delete from enhanced storage: $e',
|
||||||
|
scope: 'storage',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _secureStorage.delete(key: _authTokenKey);
|
await _secureStorage.delete(key: _authTokenKey);
|
||||||
@@ -83,8 +87,9 @@ class StorageService {
|
|||||||
final isSecureAvailable = await _secureCredentialStorage
|
final isSecureAvailable = await _secureCredentialStorage
|
||||||
.isSecureStorageAvailable();
|
.isSecureStorageAvailable();
|
||||||
if (!isSecureAvailable) {
|
if (!isSecureAvailable) {
|
||||||
debugPrint(
|
DebugLogger.log(
|
||||||
'DEBUG: Enhanced secure storage not available, using legacy storage',
|
'Enhanced secure storage not available, using legacy storage',
|
||||||
|
scope: 'storage',
|
||||||
);
|
);
|
||||||
throw Exception('Enhanced secure storage not available');
|
throw Exception('Enhanced secure storage not available');
|
||||||
}
|
}
|
||||||
@@ -94,9 +99,15 @@ class StorageService {
|
|||||||
username: username,
|
username: username,
|
||||||
password: password,
|
password: password,
|
||||||
);
|
);
|
||||||
debugPrint('DEBUG: Credentials saved using enhanced secure storage');
|
DebugLogger.log(
|
||||||
|
'Credentials saved using enhanced secure storage',
|
||||||
|
scope: 'storage',
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Warning: Enhanced secure storage failed, using fallback: $e');
|
DebugLogger.log(
|
||||||
|
'Enhanced secure storage failed, using fallback: $e',
|
||||||
|
scope: 'storage',
|
||||||
|
);
|
||||||
|
|
||||||
// Fallback to legacy storage
|
// Fallback to legacy storage
|
||||||
try {
|
try {
|
||||||
@@ -120,10 +131,14 @@ class StorageService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint('DEBUG: Credentials saved using fallback storage');
|
DebugLogger.log(
|
||||||
|
'Credentials saved using fallback storage',
|
||||||
|
scope: 'storage',
|
||||||
|
);
|
||||||
} catch (fallbackError) {
|
} catch (fallbackError) {
|
||||||
debugPrint(
|
DebugLogger.log(
|
||||||
'ERROR: Both enhanced and fallback credential storage failed: $fallbackError',
|
'Both enhanced and fallback credential storage failed: $fallbackError',
|
||||||
|
scope: 'storage',
|
||||||
);
|
);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
@@ -138,7 +153,10 @@ class StorageService {
|
|||||||
return credentials;
|
return credentials;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Warning: Enhanced secure storage failed, using fallback: $e');
|
DebugLogger.log(
|
||||||
|
'Enhanced secure storage failed, using fallback: $e',
|
||||||
|
scope: 'storage',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to legacy storage and migrate if found
|
// Fallback to legacy storage and migrate if found
|
||||||
@@ -153,7 +171,7 @@ class StorageService {
|
|||||||
if (!decoded.containsKey('serverId') ||
|
if (!decoded.containsKey('serverId') ||
|
||||||
!decoded.containsKey('username') ||
|
!decoded.containsKey('username') ||
|
||||||
!decoded.containsKey('password')) {
|
!decoded.containsKey('password')) {
|
||||||
debugPrint('Warning: Invalid saved credentials format');
|
DebugLogger.log('Invalid saved credentials format', scope: 'storage');
|
||||||
await deleteSavedCredentials();
|
await deleteSavedCredentials();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -170,16 +188,17 @@ class StorageService {
|
|||||||
await _secureCredentialStorage.migrateFromOldStorage(legacyCredentials);
|
await _secureCredentialStorage.migrateFromOldStorage(legacyCredentials);
|
||||||
// If migration successful, clean up legacy storage
|
// If migration successful, clean up legacy storage
|
||||||
await _secureStorage.delete(key: _credentialsKey);
|
await _secureStorage.delete(key: _credentialsKey);
|
||||||
debugPrint(
|
DebugLogger.log(
|
||||||
'DEBUG: Successfully migrated credentials to enhanced storage',
|
'Successfully migrated credentials to enhanced storage',
|
||||||
|
scope: 'storage',
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Warning: Failed to migrate credentials: $e');
|
DebugLogger.log('Failed to migrate credentials: $e', scope: 'storage');
|
||||||
}
|
}
|
||||||
|
|
||||||
return legacyCredentials;
|
return legacyCredentials;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Error loading saved credentials: $e');
|
DebugLogger.log('Error loading saved credentials: $e', scope: 'storage');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,7 +208,10 @@ class StorageService {
|
|||||||
try {
|
try {
|
||||||
await _secureCredentialStorage.deleteSavedCredentials();
|
await _secureCredentialStorage.deleteSavedCredentials();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Warning: Failed to delete from enhanced storage: $e');
|
DebugLogger.log(
|
||||||
|
'Failed to delete from enhanced storage: $e',
|
||||||
|
scope: 'storage',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _secureStorage.delete(key: _credentialsKey);
|
await _secureStorage.delete(key: _credentialsKey);
|
||||||
@@ -218,7 +240,10 @@ class StorageService {
|
|||||||
|
|
||||||
final decoded = jsonDecode(jsonString);
|
final decoded = jsonDecode(jsonString);
|
||||||
if (decoded is! List) {
|
if (decoded is! List) {
|
||||||
debugPrint('Warning: Server configs data is not a list, resetting');
|
DebugLogger.log(
|
||||||
|
'Server configs data is not a list, resetting',
|
||||||
|
scope: 'storage',
|
||||||
|
);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,20 +257,24 @@ class StorageService {
|
|||||||
item.containsKey('url')) {
|
item.containsKey('url')) {
|
||||||
configs.add(ServerConfig.fromJson(item));
|
configs.add(ServerConfig.fromJson(item));
|
||||||
} else {
|
} else {
|
||||||
debugPrint(
|
DebugLogger.log(
|
||||||
'Warning: Skipping invalid server config: missing required fields',
|
'Skipping invalid server config: missing required fields',
|
||||||
|
scope: 'storage',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Warning: Failed to parse server config: $e');
|
DebugLogger.log(
|
||||||
|
'Failed to parse server config: $e',
|
||||||
|
scope: 'storage',
|
||||||
|
);
|
||||||
// Continue with other configs
|
// Continue with other configs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return configs;
|
return configs;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Error loading server configs: $e');
|
DebugLogger.log('Error loading server configs: $e', scope: 'storage');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,8 +308,9 @@ class StorageService {
|
|||||||
try {
|
try {
|
||||||
final decoded = jsonDecode(jsonString);
|
final decoded = jsonDecode(jsonString);
|
||||||
if (decoded is! List) {
|
if (decoded is! List) {
|
||||||
debugPrint(
|
DebugLogger.log(
|
||||||
'Warning: Local conversations data is not a list, resetting',
|
'Local conversations data is not a list, resetting',
|
||||||
|
scope: 'storage',
|
||||||
);
|
);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -296,20 +326,24 @@ class StorageService {
|
|||||||
item.containsKey('updatedAt')) {
|
item.containsKey('updatedAt')) {
|
||||||
conversations.add(Conversation.fromJson(item));
|
conversations.add(Conversation.fromJson(item));
|
||||||
} else {
|
} else {
|
||||||
debugPrint(
|
DebugLogger.log(
|
||||||
'Warning: Skipping invalid conversation: missing required fields',
|
'Skipping invalid conversation: missing required fields',
|
||||||
|
scope: 'storage',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Warning: Failed to parse conversation: $e');
|
DebugLogger.log('Failed to parse conversation: $e', scope: 'storage');
|
||||||
// Continue with other conversations
|
// Continue with other conversations
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return conversations;
|
return conversations;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Error parsing local conversations: $e');
|
DebugLogger.log(
|
||||||
|
'Error parsing local conversations: $e',
|
||||||
|
scope: 'storage',
|
||||||
|
);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -319,7 +353,7 @@ class StorageService {
|
|||||||
final json = conversations.map((c) => c.toJson()).toList();
|
final json = conversations.map((c) => c.toJson()).toList();
|
||||||
await _prefs.setString(_localConversationsKey, jsonEncode(json));
|
await _prefs.setString(_localConversationsKey, jsonEncode(json));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Error saving local conversations: $e');
|
DebugLogger.log('Error saving local conversations: $e', scope: 'storage');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,21 +384,21 @@ class StorageService {
|
|||||||
try {
|
try {
|
||||||
await _secureCredentialStorage.clearAll();
|
await _secureCredentialStorage.clearAll();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Warning: Failed to clear enhanced storage: $e');
|
DebugLogger.log('Failed to clear enhanced storage: $e', scope: 'storage');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear legacy storage
|
// Clear legacy storage
|
||||||
await _secureStorage.deleteAll();
|
await _secureStorage.deleteAll();
|
||||||
await _prefs.clear();
|
await _prefs.clear();
|
||||||
|
|
||||||
debugPrint('DEBUG: All storage cleared');
|
DebugLogger.log('All storage cleared', scope: 'storage');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear only auth-related data (keeping server configs and other settings)
|
// Clear only auth-related data (keeping server configs and other settings)
|
||||||
Future<void> clearAuthData() async {
|
Future<void> clearAuthData() async {
|
||||||
await deleteAuthToken();
|
await deleteAuthToken();
|
||||||
await deleteSavedCredentials();
|
await deleteSavedCredentials();
|
||||||
debugPrint('DEBUG: Auth data cleared');
|
DebugLogger.log('Auth data cleared', scope: 'storage');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if enhanced secure storage is available
|
/// Check if enhanced secure storage is available
|
||||||
@@ -372,7 +406,10 @@ class StorageService {
|
|||||||
try {
|
try {
|
||||||
return await _secureCredentialStorage.isSecureStorageAvailable();
|
return await _secureCredentialStorage.isSecureStorageAvailable();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Warning: Failed to check enhanced storage availability: $e');
|
DebugLogger.log(
|
||||||
|
'Failed to check enhanced storage availability: $e',
|
||||||
|
scope: 'storage',
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart' hide debugPrint;
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../../core/models/chat_message.dart';
|
import '../../core/models/chat_message.dart';
|
||||||
import '../../core/services/persistent_streaming_service.dart';
|
import '../../core/services/persistent_streaming_service.dart';
|
||||||
@@ -14,11 +14,6 @@ import '../../shared/widgets/themed_dialogs.dart';
|
|||||||
import '../../shared/theme/theme_extensions.dart';
|
import '../../shared/theme/theme_extensions.dart';
|
||||||
import '../utils/debug_logger.dart';
|
import '../utils/debug_logger.dart';
|
||||||
|
|
||||||
void debugPrint(String? message, {int? wrapWidth}) {
|
|
||||||
if (message == null) return;
|
|
||||||
DebugLogger.fromLegacy(message, scope: 'streaming/helper');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep local verbosity toggle for socket logs
|
// Keep local verbosity toggle for socket logs
|
||||||
const bool kSocketVerboseLogging = false;
|
const bool kSocketVerboseLogging = false;
|
||||||
|
|
||||||
@@ -83,7 +78,10 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
|||||||
),
|
),
|
||||||
controller: persistentController,
|
controller: persistentController,
|
||||||
recoveryCallback: () async {
|
recoveryCallback: () async {
|
||||||
debugPrint('DEBUG: Attempting to recover interrupted stream');
|
DebugLogger.log(
|
||||||
|
'Attempting to recover interrupted stream',
|
||||||
|
scope: 'streaming/helper',
|
||||||
|
);
|
||||||
},
|
},
|
||||||
metadata: {
|
metadata: {
|
||||||
'conversationId': activeConversationId,
|
'conversationId': activeConversationId,
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
import 'package:flutter/foundation.dart' hide debugPrint;
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:conduit/l10n/app_localizations.dart';
|
import 'package:conduit/l10n/app_localizations.dart';
|
||||||
import '../../shared/theme/theme_extensions.dart';
|
import '../../shared/theme/theme_extensions.dart';
|
||||||
import 'navigation_service.dart';
|
import 'navigation_service.dart';
|
||||||
import '../utils/debug_logger.dart';
|
import '../utils/debug_logger.dart';
|
||||||
|
|
||||||
void debugPrint(String? message, {int? wrapWidth}) {
|
|
||||||
if (message == null) return;
|
|
||||||
DebugLogger.fromLegacy(message, scope: 'errors/user-friendly');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// User-friendly error messages and recovery actions
|
/// User-friendly error messages and recovery actions
|
||||||
class UserFriendlyErrorHandler {
|
class UserFriendlyErrorHandler {
|
||||||
static final UserFriendlyErrorHandler _instance =
|
static final UserFriendlyErrorHandler _instance =
|
||||||
@@ -347,9 +342,12 @@ class UserFriendlyErrorHandler {
|
|||||||
/// Log technical error details for debugging
|
/// Log technical error details for debugging
|
||||||
void _logError(dynamic error) {
|
void _logError(dynamic error) {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
debugPrint('ERROR: $error');
|
DebugLogger.log('$error', scope: 'errors/user-friendly');
|
||||||
if (error is Error) {
|
if (error is Error) {
|
||||||
debugPrint('STACK TRACE: ${error.stackTrace}');
|
DebugLogger.log(
|
||||||
|
'STACK TRACE: ${error.stackTrace}',
|
||||||
|
scope: 'errors/user-friendly',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,11 +16,6 @@ import '../../tools/providers/tools_providers.dart';
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import '../../../core/utils/debug_logger.dart';
|
import '../../../core/utils/debug_logger.dart';
|
||||||
|
|
||||||
void debugPrint(String? message, {int? wrapWidth}) {
|
|
||||||
if (message == null) return;
|
|
||||||
DebugLogger.fromLegacy(message, scope: 'chat/providers');
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool kSocketVerboseLogging = false;
|
const bool kSocketVerboseLogging = false;
|
||||||
|
|
||||||
// Chat messages for current conversation
|
// Chat messages for current conversation
|
||||||
@@ -105,7 +100,10 @@ class ChatMessagesNotifier extends Notifier<List<ChatMessage>> {
|
|||||||
previous,
|
previous,
|
||||||
next,
|
next,
|
||||||
) {
|
) {
|
||||||
debugPrint('Conversation changed: ${previous?.id} -> ${next?.id}');
|
DebugLogger.log(
|
||||||
|
'Conversation changed: ${previous?.id} -> ${next?.id}',
|
||||||
|
scope: 'chat/providers',
|
||||||
|
);
|
||||||
|
|
||||||
// Only react when the conversation actually changes
|
// Only react when the conversation actually changes
|
||||||
if (previous?.id == next?.id) {
|
if (previous?.id == next?.id) {
|
||||||
@@ -1886,7 +1884,10 @@ Please try sending the message again, or try without attachments.''',
|
|||||||
);
|
);
|
||||||
ref.read(chatMessagesProvider.notifier).addMessage(errorMessage);
|
ref.read(chatMessagesProvider.notifier).addMessage(errorMessage);
|
||||||
} else if (e.toString().contains('404')) {
|
} else if (e.toString().contains('404')) {
|
||||||
debugPrint('DEBUG: Model or endpoint not found (404)');
|
DebugLogger.log(
|
||||||
|
'Model or endpoint not found (404)',
|
||||||
|
scope: 'chat/providers',
|
||||||
|
);
|
||||||
final errorMessage = ChatMessage(
|
final errorMessage = ChatMessage(
|
||||||
id: const Uuid().v4(),
|
id: const Uuid().v4(),
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
@@ -2005,7 +2006,10 @@ Future<void> pinConversation(
|
|||||||
.set(activeConversation!.copyWith(pinned: pinned));
|
.set(activeConversation!.copyWith(pinned: pinned));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Error ${pinned ? 'pinning' : 'unpinning'} conversation: $e');
|
DebugLogger.log(
|
||||||
|
'Error ${pinned ? 'pinning' : 'unpinning'} conversation: $e',
|
||||||
|
scope: 'chat/providers',
|
||||||
|
);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2033,8 +2037,9 @@ Future<void> archiveConversation(
|
|||||||
// Refresh conversations list to reflect the change
|
// Refresh conversations list to reflect the change
|
||||||
ref.invalidate(conversationsProvider);
|
ref.invalidate(conversationsProvider);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint(
|
DebugLogger.log(
|
||||||
'Error ${archived ? 'archiving' : 'unarchiving'} conversation: $e',
|
'Error ${archived ? 'archiving' : 'unarchiving'} conversation: $e',
|
||||||
|
scope: 'chat/providers',
|
||||||
);
|
);
|
||||||
|
|
||||||
// If server operation failed and we archived locally, restore the conversation
|
// If server operation failed and we archived locally, restore the conversation
|
||||||
@@ -2060,7 +2065,7 @@ Future<String?> shareConversation(WidgetRef ref, String conversationId) async {
|
|||||||
|
|
||||||
return shareId;
|
return shareId;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Error sharing conversation: $e');
|
DebugLogger.log('Error sharing conversation: $e', scope: 'chat/providers');
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2081,7 +2086,7 @@ Future<void> cloneConversation(WidgetRef ref, String conversationId) async {
|
|||||||
// Refresh conversations list to show the new conversation
|
// Refresh conversations list to show the new conversation
|
||||||
ref.invalidate(conversationsProvider);
|
ref.invalidate(conversationsProvider);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Error cloning conversation: $e');
|
DebugLogger.log('Error cloning conversation: $e', scope: 'chat/providers');
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:flutter/material.dart' hide debugPrint;
|
import 'package:flutter/material.dart';
|
||||||
import 'package:conduit/l10n/app_localizations.dart';
|
import 'package:conduit/l10n/app_localizations.dart';
|
||||||
import '../../../core/widgets/error_boundary.dart';
|
import '../../../core/widgets/error_boundary.dart';
|
||||||
import '../../../shared/widgets/optimized_list.dart';
|
import '../../../shared/widgets/optimized_list.dart';
|
||||||
@@ -48,11 +48,6 @@ import '../../../shared/widgets/model_avatar.dart';
|
|||||||
import '../../../core/services/platform_service.dart' as ps;
|
import '../../../core/services/platform_service.dart' as ps;
|
||||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
||||||
|
|
||||||
void debugPrint(String? message, {int? wrapWidth}) {
|
|
||||||
if (message == null) return;
|
|
||||||
DebugLogger.fromLegacy(message, scope: 'chat/page');
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChatPage extends ConsumerStatefulWidget {
|
class ChatPage extends ConsumerStatefulWidget {
|
||||||
const ChatPage({super.key});
|
const ChatPage({super.key});
|
||||||
|
|
||||||
@@ -251,7 +246,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ref.read(activeConversationProvider.notifier).set(welcomeConv);
|
ref.read(activeConversationProvider.notifier).set(welcomeConv);
|
||||||
debugPrint('Auto-loaded demo conversation');
|
DebugLogger.log('Auto-loaded demo conversation', scope: 'chat/page');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,7 +261,10 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint('Failed to auto-load demo conversation');
|
DebugLogger.log(
|
||||||
|
'Failed to auto-load demo conversation',
|
||||||
|
scope: 'chat/page',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -439,18 +437,19 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
debugPrint('Enqueue upload failed: $e');
|
DebugLogger.log('Enqueue upload failed: $e', scope: 'chat/page');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
debugPrint('File selection failed: $e');
|
DebugLogger.log('File selection failed: $e', scope: 'chat/page');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleImageAttachment({bool fromCamera = false}) async {
|
void _handleImageAttachment({bool fromCamera = false}) async {
|
||||||
debugPrint(
|
DebugLogger.log(
|
||||||
'DEBUG: Starting image attachment process - fromCamera: $fromCamera',
|
'Starting image attachment process - fromCamera: $fromCamera',
|
||||||
|
scope: 'chat/page',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if selected model supports vision
|
// Check if selected model supports vision
|
||||||
@@ -462,23 +461,26 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
|
|
||||||
final fileService = ref.read(fileAttachmentServiceProvider);
|
final fileService = ref.read(fileAttachmentServiceProvider);
|
||||||
if (fileService == null) {
|
if (fileService == null) {
|
||||||
debugPrint('DEBUG: File service is null - cannot proceed');
|
DebugLogger.log(
|
||||||
|
'File service is null - cannot proceed',
|
||||||
|
scope: 'chat/page',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
debugPrint('DEBUG: Picking image...');
|
DebugLogger.log('Picking image...', scope: 'chat/page');
|
||||||
final image = fromCamera
|
final image = fromCamera
|
||||||
? await fileService.takePhoto()
|
? await fileService.takePhoto()
|
||||||
: await fileService.pickImage();
|
: await fileService.pickImage();
|
||||||
if (image == null) {
|
if (image == null) {
|
||||||
debugPrint('DEBUG: No image selected');
|
DebugLogger.log('No image selected', scope: 'chat/page');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint('DEBUG: Image selected: ${image.path}');
|
DebugLogger.log('Image selected: ${image.path}', scope: 'chat/page');
|
||||||
final imageSize = await image.length();
|
final imageSize = await image.length();
|
||||||
debugPrint('DEBUG: Image size: $imageSize bytes');
|
DebugLogger.log('Image size: $imageSize bytes', scope: 'chat/page');
|
||||||
|
|
||||||
// Validate file size (default 20MB limit like OpenWebUI)
|
// Validate file size (default 20MB limit like OpenWebUI)
|
||||||
if (!validateFileSize(imageSize, 20)) {
|
if (!validateFileSize(imageSize, 20)) {
|
||||||
@@ -495,10 +497,10 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
|
|
||||||
// Add image to the attachment list
|
// Add image to the attachment list
|
||||||
ref.read(attachedFilesProvider.notifier).addFiles([image]);
|
ref.read(attachedFilesProvider.notifier).addFiles([image]);
|
||||||
debugPrint('DEBUG: Image added to attachment list');
|
DebugLogger.log('Image added to attachment list', scope: 'chat/page');
|
||||||
|
|
||||||
// Enqueue upload via task queue for unified retry/progress
|
// Enqueue upload via task queue for unified retry/progress
|
||||||
debugPrint('DEBUG: Enqueueing image upload...');
|
DebugLogger.log('Enqueueing image upload...', scope: 'chat/page');
|
||||||
final activeConv = ref.read(activeConversationProvider);
|
final activeConv = ref.read(activeConversationProvider);
|
||||||
try {
|
try {
|
||||||
await ref
|
await ref
|
||||||
@@ -510,10 +512,10 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
fileSize: imageSize,
|
fileSize: imageSize,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('DEBUG: Enqueue image upload failed: $e');
|
DebugLogger.log('Enqueue image upload failed: $e', scope: 'chat/page');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('DEBUG: Image attachment error: $e');
|
DebugLogger.log('Image attachment error: $e', scope: 'chat/page');
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -886,7 +888,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
userMessage.attachmentIds,
|
userMessage.attachmentIds,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Regenerate failed: $e');
|
DebugLogger.log('Regenerate failed: $e', scope: 'chat/page');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1389,8 +1391,9 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
.read(activeConversationProvider.notifier)
|
.read(activeConversationProvider.notifier)
|
||||||
.set(full);
|
.set(full);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint(
|
DebugLogger.log(
|
||||||
'DEBUG: Failed to refresh conversation: $e',
|
'Failed to refresh conversation: $e',
|
||||||
|
scope: 'chat/page',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2101,7 +2104,7 @@ class _VoiceInputSheetState extends ConsumerState<_VoiceInputSheet> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
onDone: () {
|
onDone: () {
|
||||||
debugPrint('DEBUG: VoiceInputSheet stream done');
|
DebugLogger.log('VoiceInputSheet stream done', scope: 'chat/page');
|
||||||
setState(() {
|
setState(() {
|
||||||
_isListening = false;
|
_isListening = false;
|
||||||
});
|
});
|
||||||
@@ -2112,7 +2115,10 @@ class _VoiceInputSheetState extends ConsumerState<_VoiceInputSheet> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (error) {
|
onError: (error) {
|
||||||
debugPrint('DEBUG: VoiceInputSheet stream error: $error');
|
DebugLogger.log(
|
||||||
|
'VoiceInputSheet stream error: $error',
|
||||||
|
scope: 'chat/page',
|
||||||
|
);
|
||||||
setState(() {
|
setState(() {
|
||||||
_isListening = false;
|
_isListening = false;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:flutter/material.dart' hide debugPrint;
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/foundation.dart' show listEquals;
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
@@ -22,11 +21,6 @@ import 'package:url_launcher/url_launcher_string.dart';
|
|||||||
import '../providers/chat_providers.dart' show sendMessage;
|
import '../providers/chat_providers.dart' show sendMessage;
|
||||||
import '../../../core/utils/debug_logger.dart';
|
import '../../../core/utils/debug_logger.dart';
|
||||||
|
|
||||||
void debugPrint(String? message, {int? wrapWidth}) {
|
|
||||||
if (message == null) return;
|
|
||||||
DebugLogger.fromLegacy(message, scope: 'chat/assistant');
|
|
||||||
}
|
|
||||||
|
|
||||||
class AssistantMessageWidget extends ConsumerStatefulWidget {
|
class AssistantMessageWidget extends ConsumerStatefulWidget {
|
||||||
final dynamic message;
|
final dynamic message;
|
||||||
final bool isStreaming;
|
final bool isStreaming;
|
||||||
@@ -76,7 +70,10 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
|||||||
try {
|
try {
|
||||||
await sendMessage(ref, trimmed, null);
|
await sendMessage(ref, trimmed, null);
|
||||||
} catch (err, stack) {
|
} catch (err, stack) {
|
||||||
debugPrint('Failed to send follow-up: $err');
|
DebugLogger.log(
|
||||||
|
'Failed to send follow-up: $err',
|
||||||
|
scope: 'chat/assistant',
|
||||||
|
);
|
||||||
debugPrintStack(stackTrace: stack);
|
debugPrintStack(stackTrace: stack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -660,15 +657,6 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
|||||||
const SizedBox(height: Spacing.md),
|
const SizedBox(height: Spacing.md),
|
||||||
CitationListView(sources: widget.message.sources),
|
CitationListView(sources: widget.message.sources),
|
||||||
],
|
],
|
||||||
|
|
||||||
if (hasFollowUps) ...[
|
|
||||||
const SizedBox(height: Spacing.md),
|
|
||||||
FollowUpSuggestionBar(
|
|
||||||
suggestions: widget.message.followUps,
|
|
||||||
onSelected: _handleFollowUpTap,
|
|
||||||
isBusy: widget.isStreaming,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -677,6 +665,14 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
|||||||
if (!widget.isStreaming) ...[
|
if (!widget.isStreaming) ...[
|
||||||
const SizedBox(height: Spacing.sm),
|
const SizedBox(height: Spacing.sm),
|
||||||
_buildActionButtons(),
|
_buildActionButtons(),
|
||||||
|
if (hasFollowUps) ...[
|
||||||
|
const SizedBox(height: Spacing.md),
|
||||||
|
FollowUpSuggestionBar(
|
||||||
|
suggestions: widget.message.followUps,
|
||||||
|
onSelected: _handleFollowUpTap,
|
||||||
|
isBusy: widget.isStreaming,
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -1283,6 +1279,124 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _AssistantResponseSection extends StatelessWidget {
|
||||||
|
const _AssistantResponseSection({
|
||||||
|
required this.title,
|
||||||
|
required this.child,
|
||||||
|
this.icon,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final Widget child;
|
||||||
|
final IconData? icon;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = context.conduitTheme;
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (icon != null) ...[
|
||||||
|
Icon(icon, size: 16, color: theme.buttonPrimary),
|
||||||
|
const SizedBox(width: Spacing.xs),
|
||||||
|
],
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
color: theme.textSecondary,
|
||||||
|
fontSize: AppTypography.bodySmall,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
letterSpacing: 0.15,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: Spacing.xs),
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(Spacing.sm),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.cardBackground,
|
||||||
|
borderRadius: BorderRadius.circular(AppBorderRadius.card),
|
||||||
|
border: Border.all(
|
||||||
|
color: theme.cardBorder.withValues(alpha: 0.6),
|
||||||
|
width: BorderWidth.thin,
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: colorScheme.shadow.withValues(alpha: 0.05),
|
||||||
|
blurRadius: 16,
|
||||||
|
offset: const Offset(0, 6),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AssistantSuggestionChip extends StatelessWidget {
|
||||||
|
const _AssistantSuggestionChip({
|
||||||
|
required this.label,
|
||||||
|
this.icon,
|
||||||
|
this.onPressed,
|
||||||
|
this.enabled = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final IconData? icon;
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
final bool enabled;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = context.conduitTheme;
|
||||||
|
final effectiveOnPressed = enabled ? onPressed : null;
|
||||||
|
final iconColor = enabled
|
||||||
|
? theme.textSecondary
|
||||||
|
: theme.textSecondary.withValues(alpha: 0.5);
|
||||||
|
|
||||||
|
final background = theme.cardBackground.withValues(
|
||||||
|
alpha: enabled ? 0.95 : 0.85,
|
||||||
|
);
|
||||||
|
final borderColor = theme.cardBorder.withValues(
|
||||||
|
alpha: enabled ? 0.6 : 0.35,
|
||||||
|
);
|
||||||
|
|
||||||
|
return RawChip(
|
||||||
|
avatar: icon != null ? Icon(icon, size: 16, color: iconColor) : null,
|
||||||
|
label: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: enabled ? theme.textPrimary : theme.textSecondary,
|
||||||
|
fontSize: AppTypography.labelMedium,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
letterSpacing: 0.2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: effectiveOnPressed,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: Spacing.sm,
|
||||||
|
vertical: Spacing.xxs,
|
||||||
|
),
|
||||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
backgroundColor: background,
|
||||||
|
disabledColor: background,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppBorderRadius.pill),
|
||||||
|
side: BorderSide(color: borderColor, width: BorderWidth.thin),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class StatusHistoryTimeline extends StatelessWidget {
|
class StatusHistoryTimeline extends StatelessWidget {
|
||||||
const StatusHistoryTimeline({super.key, required this.updates});
|
const StatusHistoryTimeline({super.key, required this.updates});
|
||||||
|
|
||||||
@@ -1290,39 +1404,24 @@ class StatusHistoryTimeline extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = context.conduitTheme;
|
|
||||||
if (updates.isEmpty) {
|
if (updates.isEmpty) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Container(
|
return _AssistantResponseSection(
|
||||||
width: double.infinity,
|
title: 'Status updates',
|
||||||
padding: const EdgeInsets.all(Spacing.sm),
|
icon: Icons.sync_alt,
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.surfaceContainer.withValues(alpha: 0.6),
|
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
|
||||||
border: Border.all(
|
|
||||||
color: theme.dividerColor.withValues(alpha: 0.6),
|
|
||||||
width: BorderWidth.thin,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
const SizedBox(height: Spacing.xs),
|
||||||
'Status updates',
|
for (var index = 0; index < updates.length; index++)
|
||||||
style: TextStyle(
|
Padding(
|
||||||
color: theme.textPrimary,
|
padding: EdgeInsets.only(
|
||||||
fontWeight: FontWeight.w600,
|
bottom: index == updates.length - 1 ? 0 : Spacing.xs,
|
||||||
fontSize: AppTypography.bodyLarge,
|
),
|
||||||
|
child: _StatusHistoryEntry(update: updates[index]),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const SizedBox(height: Spacing.sm),
|
|
||||||
...List.generate(updates.length, (index) {
|
|
||||||
final update = updates[index];
|
|
||||||
final isLast = index == updates.length - 1;
|
|
||||||
return _StatusHistoryEntry(update: update, isLast: isLast);
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1330,10 +1429,9 @@ class StatusHistoryTimeline extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _StatusHistoryEntry extends StatelessWidget {
|
class _StatusHistoryEntry extends StatelessWidget {
|
||||||
const _StatusHistoryEntry({required this.update, required this.isLast});
|
const _StatusHistoryEntry({required this.update});
|
||||||
|
|
||||||
final ChatStatusUpdate update;
|
final ChatStatusUpdate update;
|
||||||
final bool isLast;
|
|
||||||
|
|
||||||
Color _indicatorColor(ConduitThemeExtension theme) {
|
Color _indicatorColor(ConduitThemeExtension theme) {
|
||||||
if (update.done == false) {
|
if (update.done == false) {
|
||||||
@@ -1372,142 +1470,159 @@ class _StatusHistoryEntry extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Padding(
|
return Container(
|
||||||
padding: const EdgeInsets.only(bottom: Spacing.sm),
|
width: double.infinity,
|
||||||
child: Row(
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: Spacing.sm,
|
||||||
|
vertical: Spacing.sm,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.cardBackground.withValues(alpha: 0.92),
|
||||||
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||||
|
border: Border.all(
|
||||||
|
color: theme.cardBorder.withValues(alpha: 0.5),
|
||||||
|
width: BorderWidth.thin,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Column(
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Icon(_indicatorIcon(), size: 18, color: indicatorColor),
|
Icon(_indicatorIcon(), size: 16, color: indicatorColor),
|
||||||
if (!isLast)
|
const SizedBox(width: Spacing.sm),
|
||||||
Container(
|
Expanded(
|
||||||
margin: const EdgeInsets.only(top: Spacing.xxs),
|
child: Column(
|
||||||
width: 2,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
height: 32,
|
children: [
|
||||||
color: theme.dividerColor.withValues(alpha: 0.5),
|
Text(
|
||||||
|
description,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: AppTypography.bodySmall,
|
||||||
|
color: theme.textSecondary,
|
||||||
|
fontWeight: update.done == true
|
||||||
|
? FontWeight.w600
|
||||||
|
: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (update.count != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: Spacing.xxs),
|
||||||
|
child: Text(
|
||||||
|
update.count == 1
|
||||||
|
? 'Retrieved 1 source'
|
||||||
|
: 'Retrieved ${update.count} sources',
|
||||||
|
style: TextStyle(
|
||||||
|
color: theme.textSecondary,
|
||||||
|
fontSize: AppTypography.labelSmall,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (timestamp != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: Spacing.xxs),
|
||||||
|
child: Text(
|
||||||
|
_formatTimestamp(timestamp),
|
||||||
|
style: TextStyle(
|
||||||
|
color: theme.textSecondary.withValues(alpha: 0.8),
|
||||||
|
fontSize: AppTypography.labelSmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(width: Spacing.sm),
|
if (queries.isNotEmpty)
|
||||||
Expanded(
|
Padding(
|
||||||
child: Column(
|
padding: const EdgeInsets.only(top: Spacing.sm),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Wrap(
|
||||||
children: [
|
spacing: Spacing.xs,
|
||||||
Text(
|
runSpacing: Spacing.xs,
|
||||||
description,
|
children: queries.map((query) {
|
||||||
style: TextStyle(
|
return _AssistantSuggestionChip(
|
||||||
fontSize: AppTypography.bodyMedium,
|
label: query,
|
||||||
color: theme.textPrimary,
|
icon: Icons.search,
|
||||||
fontWeight: update.done == true
|
onPressed: () {
|
||||||
? FontWeight.w600
|
_launchUri(
|
||||||
: FontWeight.w500,
|
'https://www.google.com/search?q=${Uri.encodeComponent(query)}',
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
if (update.count != null)
|
);
|
||||||
Padding(
|
}).toList(),
|
||||||
padding: const EdgeInsets.only(top: Spacing.xxs),
|
),
|
||||||
child: Text(
|
),
|
||||||
update.count == 1
|
if (update.urls.isNotEmpty)
|
||||||
? 'Retrieved 1 source'
|
Padding(
|
||||||
: 'Retrieved ${update.count} sources',
|
padding: const EdgeInsets.only(top: Spacing.sm),
|
||||||
style: TextStyle(
|
child: Wrap(
|
||||||
color: theme.textSecondary,
|
spacing: Spacing.xs,
|
||||||
fontSize: AppTypography.labelSmall,
|
runSpacing: Spacing.xs,
|
||||||
),
|
children: update.urls.map((url) {
|
||||||
),
|
final host = Uri.tryParse(url)?.host ?? 'Link';
|
||||||
),
|
return _AssistantSuggestionChip(
|
||||||
if (timestamp != null)
|
label: host,
|
||||||
Padding(
|
icon: Icons.open_in_new,
|
||||||
padding: const EdgeInsets.only(top: Spacing.xxs),
|
onPressed: () => _launchUri(url),
|
||||||
child: Text(
|
);
|
||||||
_formatTimestamp(timestamp),
|
}).toList(),
|
||||||
style: TextStyle(
|
),
|
||||||
color: theme.textSecondary,
|
),
|
||||||
fontSize: AppTypography.labelSmall,
|
if (update.items.isNotEmpty)
|
||||||
),
|
Padding(
|
||||||
),
|
padding: const EdgeInsets.only(top: Spacing.sm),
|
||||||
),
|
child: Column(
|
||||||
if (queries.isNotEmpty)
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Padding(
|
children: update.items.map((item) {
|
||||||
padding: const EdgeInsets.only(top: Spacing.xxs),
|
final title = item.title?.isNotEmpty == true
|
||||||
child: Wrap(
|
? item.title!
|
||||||
spacing: Spacing.xs,
|
: item.link ?? 'Result';
|
||||||
runSpacing: Spacing.xs,
|
return Padding(
|
||||||
children: queries.map((query) {
|
padding: const EdgeInsets.only(bottom: Spacing.xs),
|
||||||
return ActionChip(
|
child: InkWell(
|
||||||
label: Text(query),
|
onTap: item.link != null
|
||||||
avatar: const Icon(Icons.search, size: 16),
|
? () => _launchUri(item.link!)
|
||||||
onPressed: () {
|
: null,
|
||||||
_launchUri(
|
borderRadius: BorderRadius.circular(AppBorderRadius.sm),
|
||||||
'https://www.google.com/search?q=${Uri.encodeComponent(query)}',
|
child: Padding(
|
||||||
);
|
padding: const EdgeInsets.symmetric(
|
||||||
},
|
vertical: Spacing.xxs,
|
||||||
);
|
),
|
||||||
}).toList(),
|
child: Row(
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
),
|
children: [
|
||||||
if (update.urls.isNotEmpty)
|
Icon(
|
||||||
Padding(
|
Icons.link,
|
||||||
padding: const EdgeInsets.only(top: Spacing.xxs),
|
size: 16,
|
||||||
child: Wrap(
|
color: theme.textSecondary,
|
||||||
spacing: Spacing.xs,
|
),
|
||||||
runSpacing: Spacing.xs,
|
const SizedBox(width: Spacing.xs),
|
||||||
children: update.urls.map((url) {
|
Expanded(
|
||||||
return OutlinedButton.icon(
|
child: Text(
|
||||||
onPressed: () => _launchUri(url),
|
title,
|
||||||
icon: const Icon(Icons.open_in_new, size: 16),
|
style: TextStyle(
|
||||||
label: Text(
|
color: item.link != null
|
||||||
Uri.tryParse(url)?.host ?? 'Link',
|
? theme.buttonPrimary
|
||||||
overflow: TextOverflow.ellipsis,
|
: theme.textSecondary,
|
||||||
),
|
decoration: item.link != null
|
||||||
);
|
? TextDecoration.underline
|
||||||
}).toList(),
|
: TextDecoration.none,
|
||||||
),
|
fontSize: AppTypography.bodySmall,
|
||||||
),
|
fontWeight: FontWeight.w500,
|
||||||
if (update.items.isNotEmpty)
|
),
|
||||||
Padding(
|
),
|
||||||
padding: const EdgeInsets.only(top: Spacing.xxs),
|
),
|
||||||
child: Column(
|
],
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
),
|
||||||
children: update.items.map((item) {
|
),
|
||||||
final title = item.title?.isNotEmpty == true
|
),
|
||||||
? item.title!
|
);
|
||||||
: item.link ?? 'Result';
|
}).toList(),
|
||||||
return Padding(
|
),
|
||||||
padding: const EdgeInsets.only(bottom: Spacing.xxs),
|
|
||||||
child: InkWell(
|
|
||||||
onTap: item.link != null
|
|
||||||
? () => _launchUri(item.link!)
|
|
||||||
: null,
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.link, size: 16),
|
|
||||||
const SizedBox(width: Spacing.xxs),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
title,
|
|
||||||
style: TextStyle(
|
|
||||||
color: item.link != null
|
|
||||||
? theme.buttonPrimary
|
|
||||||
: theme.textSecondary,
|
|
||||||
decoration: item.link != null
|
|
||||||
? TextDecoration.underline
|
|
||||||
: TextDecoration.none,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1772,7 +1887,7 @@ class CitationListView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FollowUpSuggestionBar extends StatefulWidget {
|
class FollowUpSuggestionBar extends StatelessWidget {
|
||||||
const FollowUpSuggestionBar({
|
const FollowUpSuggestionBar({
|
||||||
super.key,
|
super.key,
|
||||||
required this.suggestions,
|
required this.suggestions,
|
||||||
@@ -1784,149 +1899,37 @@ class FollowUpSuggestionBar extends StatefulWidget {
|
|||||||
final ValueChanged<String> onSelected;
|
final ValueChanged<String> onSelected;
|
||||||
final bool isBusy;
|
final bool isBusy;
|
||||||
|
|
||||||
@override
|
|
||||||
State<FollowUpSuggestionBar> createState() => _FollowUpSuggestionBarState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FollowUpSuggestionBarState extends State<FollowUpSuggestionBar>
|
|
||||||
with SingleTickerProviderStateMixin {
|
|
||||||
late final AnimationController _controller;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_controller = AnimationController(
|
|
||||||
vsync: this,
|
|
||||||
duration: const Duration(milliseconds: 520),
|
|
||||||
);
|
|
||||||
if (widget.suggestions.isNotEmpty) {
|
|
||||||
_controller.forward();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(covariant FollowUpSuggestionBar oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
if (!listEquals(oldWidget.suggestions, widget.suggestions)) {
|
|
||||||
if (widget.suggestions.isEmpty) {
|
|
||||||
_controller.reset();
|
|
||||||
} else {
|
|
||||||
_controller.forward(from: 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_controller.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = context.conduitTheme;
|
final trimmedSuggestions = suggestions
|
||||||
if (widget.suggestions.isEmpty) {
|
.map((s) => s.trim())
|
||||||
|
.where((s) => s.isNotEmpty)
|
||||||
|
.toList(growable: false);
|
||||||
|
|
||||||
|
if (trimmedSuggestions.isEmpty) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
final Animation<double> headerAnimation = CurvedAnimation(
|
return _AssistantResponseSection(
|
||||||
parent: _controller,
|
title: 'Suggested next steps',
|
||||||
curve: const Interval(0, 0.35, curve: Curves.easeOutCubic),
|
icon: Icons.auto_awesome,
|
||||||
);
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
return Column(
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
const SizedBox(height: Spacing.xs),
|
||||||
children: [
|
Wrap(
|
||||||
AnimatedBuilder(
|
spacing: Spacing.xs,
|
||||||
animation: headerAnimation,
|
runSpacing: Spacing.xs,
|
||||||
builder: (context, child) {
|
children: [
|
||||||
return Opacity(
|
for (final suggestion in trimmedSuggestions)
|
||||||
opacity: headerAnimation.value,
|
_AssistantSuggestionChip(
|
||||||
child: Transform.translate(
|
label: suggestion,
|
||||||
offset: Offset(0, (1 - headerAnimation.value) * 10),
|
onPressed: isBusy ? null : () => onSelected(suggestion),
|
||||||
child: child,
|
enabled: !isBusy,
|
||||||
),
|
),
|
||||||
);
|
],
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
'Try next',
|
|
||||||
style: TextStyle(
|
|
||||||
color: theme.textPrimary,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: AppTypography.bodyLarge,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
const SizedBox(height: Spacing.xs),
|
|
||||||
Wrap(
|
|
||||||
spacing: Spacing.xs,
|
|
||||||
runSpacing: Spacing.xs,
|
|
||||||
children: [
|
|
||||||
for (var i = 0; i < widget.suggestions.length; i++)
|
|
||||||
_AnimatedSuggestionChip(
|
|
||||||
controller: _controller,
|
|
||||||
index: i,
|
|
||||||
total: widget.suggestions.length,
|
|
||||||
isBusy: widget.isBusy,
|
|
||||||
suggestion: widget.suggestions[i],
|
|
||||||
onSelected: widget.onSelected,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AnimatedSuggestionChip extends StatelessWidget {
|
|
||||||
const _AnimatedSuggestionChip({
|
|
||||||
required this.controller,
|
|
||||||
required this.index,
|
|
||||||
required this.total,
|
|
||||||
required this.isBusy,
|
|
||||||
required this.suggestion,
|
|
||||||
required this.onSelected,
|
|
||||||
});
|
|
||||||
|
|
||||||
final AnimationController controller;
|
|
||||||
final int index;
|
|
||||||
final int total;
|
|
||||||
final bool isBusy;
|
|
||||||
final String suggestion;
|
|
||||||
final ValueChanged<String> onSelected;
|
|
||||||
|
|
||||||
Interval _intervalForIndex() {
|
|
||||||
if (total <= 1) {
|
|
||||||
return const Interval(0.0, 0.8, curve: Curves.easeOutCubic);
|
|
||||||
}
|
|
||||||
final double step = 0.6 / total;
|
|
||||||
final double start = (index * step).clamp(0.0, 0.8);
|
|
||||||
final double end = (start + 0.4).clamp(0.2, 1.0);
|
|
||||||
return Interval(start, end, curve: Curves.easeOutCubic);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final animation = CurvedAnimation(
|
|
||||||
parent: controller,
|
|
||||||
curve: _intervalForIndex(),
|
|
||||||
);
|
|
||||||
|
|
||||||
return AnimatedBuilder(
|
|
||||||
animation: animation,
|
|
||||||
builder: (context, child) {
|
|
||||||
final double t = animation.value;
|
|
||||||
return Opacity(
|
|
||||||
opacity: t,
|
|
||||||
child: Transform.translate(
|
|
||||||
offset: Offset(0, (1 - t) * 12),
|
|
||||||
child: Transform.scale(scale: 0.95 + (t * 0.05), child: child),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: FilledButton.tonal(
|
|
||||||
onPressed: isBusy ? null : () => onSelected(suggestion),
|
|
||||||
child: Text(suggestion),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1937,6 +1940,6 @@ Future<void> _launchUri(String url) async {
|
|||||||
try {
|
try {
|
||||||
await launchUrlString(url, mode: LaunchMode.externalApplication);
|
await launchUrlString(url, mode: LaunchMode.externalApplication);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
debugPrint('Unable to open url $url: $err');
|
DebugLogger.log('Unable to open url $url: $err', scope: 'chat/assistant');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:flutter/material.dart' hide debugPrint;
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
@@ -14,11 +14,6 @@ import '../../../core/providers/app_providers.dart';
|
|||||||
import '../../auth/providers/unified_auth_providers.dart';
|
import '../../auth/providers/unified_auth_providers.dart';
|
||||||
import '../../../core/utils/debug_logger.dart';
|
import '../../../core/utils/debug_logger.dart';
|
||||||
|
|
||||||
void debugPrint(String? message, {int? wrapWidth}) {
|
|
||||||
if (message == null) return;
|
|
||||||
DebugLogger.fromLegacy(message, scope: 'chat/image-attachment');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple global cache to prevent reloading
|
// Simple global cache to prevent reloading
|
||||||
final _globalImageCache = <String, String>{};
|
final _globalImageCache = <String, String>{};
|
||||||
final _globalLoadingStates = <String, bool>{};
|
final _globalLoadingStates = <String, bool>{};
|
||||||
@@ -696,7 +691,10 @@ class FullScreenImageViewer extends ConsumerWidget {
|
|||||||
await SharePlus.instance.share(ShareParams(files: [XFile(file.path)]));
|
await SharePlus.instance.share(ShareParams(files: [XFile(file.path)]));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Swallowing UI feedback per requirements; keep a log for debugging
|
// Swallowing UI feedback per requirements; keep a log for debugging
|
||||||
debugPrint('Failed to share image: $e');
|
DebugLogger.log(
|
||||||
|
'Failed to share image: $e',
|
||||||
|
scope: 'chat/image-attachment',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:developer' as developer;
|
import 'dart:developer' as developer;
|
||||||
import 'package:flutter/material.dart' hide debugPrint;
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'core/widgets/error_boundary.dart';
|
import 'core/widgets/error_boundary.dart';
|
||||||
@@ -22,11 +22,6 @@ import 'core/providers/app_startup_providers.dart';
|
|||||||
|
|
||||||
developer.TimelineTask? _startupTimeline;
|
developer.TimelineTask? _startupTimeline;
|
||||||
|
|
||||||
void debugPrint(String? message, {int? wrapWidth}) {
|
|
||||||
if (message == null) return;
|
|
||||||
DebugLogger.fromLegacy(message, scope: 'app/main');
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runZonedGuarded(
|
runZonedGuarded(
|
||||||
() async {
|
() async {
|
||||||
@@ -41,7 +36,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
final stack = details.stack;
|
final stack = details.stack;
|
||||||
if (stack != null) {
|
if (stack != null) {
|
||||||
debugPrint(stack.toString());
|
debugPrintStack(stackTrace: stack);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
WidgetsBinding.instance.platformDispatcher.onError = (error, stack) {
|
WidgetsBinding.instance.platformDispatcher.onError = (error, stack) {
|
||||||
@@ -51,7 +46,7 @@ void main() {
|
|||||||
error: error,
|
error: error,
|
||||||
stackTrace: stack,
|
stackTrace: stack,
|
||||||
);
|
);
|
||||||
debugPrint(stack.toString());
|
debugPrintStack(stackTrace: stack);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -108,7 +103,7 @@ void main() {
|
|||||||
error: error,
|
error: error,
|
||||||
stackTrace: stack,
|
stackTrace: stack,
|
||||||
);
|
);
|
||||||
debugPrint(stack.toString());
|
debugPrintStack(stackTrace: stack);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,6 @@ import 'outbound_task.dart';
|
|||||||
import 'task_worker.dart';
|
import 'task_worker.dart';
|
||||||
import '../../../core/utils/debug_logger.dart';
|
import '../../../core/utils/debug_logger.dart';
|
||||||
|
|
||||||
void debugPrint(String? message, {int? wrapWidth}) {
|
|
||||||
if (message == null) return;
|
|
||||||
DebugLogger.fromLegacy(message, scope: 'tasks/queue');
|
|
||||||
}
|
|
||||||
|
|
||||||
final taskQueueProvider =
|
final taskQueueProvider =
|
||||||
NotifierProvider<TaskQueueNotifier, List<OutboundTask>>(
|
NotifierProvider<TaskQueueNotifier, List<OutboundTask>>(
|
||||||
TaskQueueNotifier.new,
|
TaskQueueNotifier.new,
|
||||||
@@ -61,7 +56,7 @@ class TaskQueueNotifier extends Notifier<List<OutboundTask>> {
|
|||||||
// Kick processing after load
|
// Kick processing after load
|
||||||
_process();
|
_process();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('DEBUG: Failed to load task queue: $e');
|
DebugLogger.log('Failed to load task queue: $e', scope: 'tasks/queue');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +78,7 @@ class TaskQueueNotifier extends Notifier<List<OutboundTask>> {
|
|||||||
final raw = retained.map((t) => t.toJson()).toList(growable: false);
|
final raw = retained.map((t) => t.toJson()).toList(growable: false);
|
||||||
await prefs.setString(_prefsKey, jsonEncode(raw));
|
await prefs.setString(_prefsKey, jsonEncode(raw));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('DEBUG: Failed to persist task queue: $e');
|
DebugLogger.log('Failed to persist task queue: $e', scope: 'tasks/queue');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,7 +257,10 @@ class TaskQueueNotifier extends Notifier<List<OutboundTask>> {
|
|||||||
t,
|
t,
|
||||||
];
|
];
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint('Task failed (${task.runtimeType}): $e\n$st');
|
DebugLogger.log(
|
||||||
|
'Task failed (${task.runtimeType}): $e\n$st',
|
||||||
|
scope: 'tasks/queue',
|
||||||
|
);
|
||||||
state = [
|
state = [
|
||||||
for (final t in state)
|
for (final t in state)
|
||||||
if (t.id == task.id)
|
if (t.id == task.id)
|
||||||
|
|||||||
Reference in New Issue
Block a user