chore: integrate Hive for local storage management
- Added `hive_ce` and `hive_ce_flutter` dependencies for enhanced local storage capabilities. - Refactored the main application to initialize Hive and migrate existing data. - Updated storage service implementations to utilize Hive for managing application settings and task queues. - Removed the deprecated `StorageService` class to streamline the codebase and improve maintainability.
This commit is contained in:
@@ -2,7 +2,8 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import '../persistence/hive_boxes.dart';
|
||||
import '../utils/debug_logger.dart';
|
||||
|
||||
/// Status of a queued attachment upload
|
||||
@@ -110,12 +111,12 @@ class AttachmentUploadQueue {
|
||||
factory AttachmentUploadQueue() => _instance;
|
||||
AttachmentUploadQueue._internal();
|
||||
|
||||
static const String _prefsKey = 'attachment_upload_queue';
|
||||
static const int _maxRetries = 4;
|
||||
static const Duration _baseRetryDelay = Duration(seconds: 5);
|
||||
static const Duration _maxRetryDelay = Duration(minutes: 5);
|
||||
|
||||
SharedPreferences? _prefs;
|
||||
late final Box<dynamic> _queueBox;
|
||||
bool _initialized = false;
|
||||
final List<QueuedAttachment> _queue = [];
|
||||
Timer? _retryTimer;
|
||||
bool _isProcessing = false;
|
||||
@@ -136,11 +137,14 @@ class AttachmentUploadQueue {
|
||||
}) async {
|
||||
_onUpload = onUpload;
|
||||
_onQueueChanged = onQueueChanged;
|
||||
_prefs ??= await SharedPreferences.getInstance();
|
||||
if (!_initialized) {
|
||||
_queueBox = Hive.box<dynamic>(HiveBoxNames.attachmentQueue);
|
||||
_initialized = true;
|
||||
}
|
||||
await _load();
|
||||
_startPeriodicProcessing();
|
||||
DebugLogger.log(
|
||||
'AttachmentUploadQueue initialized with ${_queue.length} items',
|
||||
'AttachmentUploadQueue initialized with \${_queue.length} items',
|
||||
scope: 'attachments/queue',
|
||||
);
|
||||
}
|
||||
@@ -328,20 +332,33 @@ class AttachmentUploadQueue {
|
||||
|
||||
// Utilities
|
||||
Future<void> _load() async {
|
||||
final jsonStr = (_prefs ?? await SharedPreferences.getInstance()).getString(
|
||||
_prefsKey,
|
||||
);
|
||||
if (jsonStr == null || jsonStr.isEmpty) return;
|
||||
final list = (jsonDecode(jsonStr) as List).cast<Map<String, dynamic>>();
|
||||
final stored = _queueBox.get(HiveStoreKeys.attachmentQueueEntries);
|
||||
if (stored == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<dynamic> rawList;
|
||||
if (stored is String && stored.isNotEmpty) {
|
||||
rawList = (jsonDecode(stored) as List<dynamic>);
|
||||
} else if (stored is List) {
|
||||
rawList = stored;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
_queue
|
||||
..clear()
|
||||
..addAll(list.map(QueuedAttachment.fromJson));
|
||||
..addAll(
|
||||
rawList.map(
|
||||
(item) =>
|
||||
QueuedAttachment.fromJson(Map<String, dynamic>.from(item as Map)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
final prefs = _prefs ?? await SharedPreferences.getInstance();
|
||||
final list = _queue.map((e) => e.toJson()).toList(growable: false);
|
||||
await prefs.setString(_prefsKey, jsonEncode(list));
|
||||
await _queueBox.put(HiveStoreKeys.attachmentQueueEntries, list);
|
||||
}
|
||||
|
||||
void _notify() {
|
||||
|
||||
@@ -1,41 +1,52 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'secure_credential_storage.dart';
|
||||
import '../models/server_config.dart';
|
||||
import 'package:hive_ce/hive.dart';
|
||||
|
||||
import '../models/conversation.dart';
|
||||
import '../models/server_config.dart';
|
||||
import '../persistence/hive_boxes.dart';
|
||||
import '../persistence/persistence_keys.dart';
|
||||
import '../utils/debug_logger.dart';
|
||||
import 'secure_credential_storage.dart';
|
||||
|
||||
/// Optimized storage service with single secure storage implementation
|
||||
/// Eliminates dual storage overhead and improves performance
|
||||
/// Optimized storage service backed by Hive for non-sensitive data and
|
||||
/// FlutterSecureStorage for credentials.
|
||||
class OptimizedStorageService {
|
||||
final SharedPreferences _prefs;
|
||||
final SecureCredentialStorage _secureCredentialStorage;
|
||||
|
||||
OptimizedStorageService({
|
||||
required FlutterSecureStorage secureStorage,
|
||||
required SharedPreferences prefs,
|
||||
}) : _prefs = prefs,
|
||||
required HiveBoxes boxes,
|
||||
}) : _preferencesBox = boxes.preferences,
|
||||
_cachesBox = boxes.caches,
|
||||
_attachmentQueueBox = boxes.attachmentQueue,
|
||||
_metadataBox = boxes.metadata,
|
||||
_secureCredentialStorage = SecureCredentialStorage(
|
||||
instance: secureStorage,
|
||||
);
|
||||
|
||||
// Optimized key names with versioning
|
||||
final Box<dynamic> _preferencesBox;
|
||||
final Box<dynamic> _cachesBox;
|
||||
final Box<dynamic> _attachmentQueueBox;
|
||||
final Box<dynamic> _metadataBox;
|
||||
final SecureCredentialStorage _secureCredentialStorage;
|
||||
|
||||
static const String _authTokenKey = 'auth_token_v3';
|
||||
static const String _activeServerIdKey = 'active_server_id';
|
||||
static const String _rememberCredentialsKey = 'remember_credentials';
|
||||
static const String _themeModeKey = 'theme_mode';
|
||||
static const String _localeCodeKey = 'locale_code_v1';
|
||||
static const String _localConversationsKey = 'local_conversations';
|
||||
static const String _onboardingSeenKey = 'onboarding_seen_v1';
|
||||
static const String _reviewerModeKey = 'reviewer_mode_v1';
|
||||
static const String _activeServerIdKey = PreferenceKeys.activeServerId;
|
||||
static const String _rememberCredentialsKey =
|
||||
PreferenceKeys.rememberCredentials;
|
||||
static const String _themeModeKey = PreferenceKeys.themeMode;
|
||||
static const String _localeCodeKey = PreferenceKeys.localeCode;
|
||||
static const String _localConversationsKey = HiveStoreKeys.localConversations;
|
||||
static const String _onboardingSeenKey = PreferenceKeys.onboardingSeen;
|
||||
static const String _reviewerModeKey = PreferenceKeys.reviewerMode;
|
||||
|
||||
// Cache for frequently accessed data
|
||||
final Map<String, dynamic> _cache = {};
|
||||
static const Duration _cacheTimeout = Duration(minutes: 5);
|
||||
final Map<String, DateTime> _cacheTimestamps = {};
|
||||
static const Duration _cacheTimeout = Duration(minutes: 5);
|
||||
|
||||
/// Auth Token Management (Optimized with caching)
|
||||
// ---------------------------------------------------------------------------
|
||||
// Auth token APIs (secure storage + in-memory cache)
|
||||
// ---------------------------------------------------------------------------
|
||||
Future<void> saveAuthToken(String token) async {
|
||||
try {
|
||||
await _secureCredentialStorage.saveAuthToken(token);
|
||||
@@ -45,9 +56,9 @@ class OptimizedStorageService {
|
||||
'Auth token saved and cached',
|
||||
scope: 'storage/optimized',
|
||||
);
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
DebugLogger.log(
|
||||
'Failed to save auth token: $e',
|
||||
'Failed to save auth token: $error',
|
||||
scope: 'storage/optimized',
|
||||
);
|
||||
rethrow;
|
||||
@@ -55,12 +66,11 @@ class OptimizedStorageService {
|
||||
}
|
||||
|
||||
Future<String?> getAuthToken() async {
|
||||
// Check cache first
|
||||
if (_isCacheValid(_authTokenKey)) {
|
||||
final cachedToken = _cache[_authTokenKey] as String?;
|
||||
if (cachedToken != null) {
|
||||
final cached = _cache[_authTokenKey] as String?;
|
||||
if (cached != null) {
|
||||
DebugLogger.log('Using cached auth token', scope: 'storage/optimized');
|
||||
return cachedToken;
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,9 +81,9 @@ class OptimizedStorageService {
|
||||
_cacheTimestamps[_authTokenKey] = DateTime.now();
|
||||
}
|
||||
return token;
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
DebugLogger.log(
|
||||
'Failed to retrieve auth token: $e',
|
||||
'Failed to retrieve auth token: $error',
|
||||
scope: 'storage/optimized',
|
||||
);
|
||||
return null;
|
||||
@@ -89,15 +99,17 @@ class OptimizedStorageService {
|
||||
'Auth token deleted and cache cleared',
|
||||
scope: 'storage/optimized',
|
||||
);
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
DebugLogger.log(
|
||||
'Failed to delete auth token: $e',
|
||||
'Failed to delete auth token: $error',
|
||||
scope: 'storage/optimized',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Credential Management (Single storage implementation)
|
||||
// ---------------------------------------------------------------------------
|
||||
// Credential APIs (secure storage only)
|
||||
// ---------------------------------------------------------------------------
|
||||
Future<void> saveCredentials({
|
||||
required String serverId,
|
||||
required String username,
|
||||
@@ -110,7 +122,6 @@ class OptimizedStorageService {
|
||||
password: password,
|
||||
);
|
||||
|
||||
// Cache the fact that credentials exist (not the credentials themselves)
|
||||
_cache['has_credentials'] = true;
|
||||
_cacheTimestamps['has_credentials'] = DateTime.now();
|
||||
|
||||
@@ -118,9 +129,9 @@ class OptimizedStorageService {
|
||||
'Credentials saved via optimized storage',
|
||||
scope: 'storage/optimized',
|
||||
);
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
DebugLogger.log(
|
||||
'Failed to save credentials: $e',
|
||||
'Failed to save credentials: $error',
|
||||
scope: 'storage/optimized',
|
||||
);
|
||||
rethrow;
|
||||
@@ -129,17 +140,13 @@ class OptimizedStorageService {
|
||||
|
||||
Future<Map<String, String>?> getSavedCredentials() async {
|
||||
try {
|
||||
// Use single storage implementation - no fallback needed
|
||||
final credentials = await _secureCredentialStorage.getSavedCredentials();
|
||||
|
||||
// Update cache flag
|
||||
_cache['has_credentials'] = credentials != null;
|
||||
_cacheTimestamps['has_credentials'] = DateTime.now();
|
||||
|
||||
return credentials;
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
DebugLogger.log(
|
||||
'Failed to retrieve credentials: $e',
|
||||
'Failed to retrieve credentials: $error',
|
||||
scope: 'storage/optimized',
|
||||
);
|
||||
return null;
|
||||
@@ -155,50 +162,46 @@ class OptimizedStorageService {
|
||||
'Credentials deleted via optimized storage',
|
||||
scope: 'storage/optimized',
|
||||
);
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
DebugLogger.log(
|
||||
'Failed to delete credentials: $e',
|
||||
'Failed to delete credentials: $error',
|
||||
scope: 'storage/optimized',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Quick check if credentials exist (uses cache)
|
||||
Future<bool> hasCredentials() async {
|
||||
if (_isCacheValid('has_credentials')) {
|
||||
return _cache['has_credentials'] == true;
|
||||
}
|
||||
|
||||
final credentials = await getSavedCredentials();
|
||||
return credentials != null;
|
||||
}
|
||||
|
||||
/// Remember Credentials Flag
|
||||
// ---------------------------------------------------------------------------
|
||||
// Preference helpers (Hive-backed)
|
||||
// ---------------------------------------------------------------------------
|
||||
Future<void> setRememberCredentials(bool remember) async {
|
||||
await _prefs.setBool(_rememberCredentialsKey, remember);
|
||||
await _preferencesBox.put(_rememberCredentialsKey, remember);
|
||||
}
|
||||
|
||||
bool getRememberCredentials() {
|
||||
return _prefs.getBool(_rememberCredentialsKey) ?? false;
|
||||
return (_preferencesBox.get(_rememberCredentialsKey) as bool?) ?? false;
|
||||
}
|
||||
|
||||
/// Server Configuration (Optimized)
|
||||
Future<void> saveServerConfigs(List<ServerConfig> configs) async {
|
||||
try {
|
||||
final jsonString = jsonEncode(configs.map((c) => c.toJson()).toList());
|
||||
await _secureCredentialStorage.saveServerConfigs(jsonString);
|
||||
|
||||
// Cache config count for quick checks
|
||||
_cache['server_config_count'] = configs.length;
|
||||
_cacheTimestamps['server_config_count'] = DateTime.now();
|
||||
|
||||
DebugLogger.log(
|
||||
'Server configs saved (${configs.length} configs)',
|
||||
'Server configs saved (${configs.length} entries)',
|
||||
scope: 'storage/optimized',
|
||||
);
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
DebugLogger.log(
|
||||
'Failed to save server configs: $e',
|
||||
'Failed to save server configs: $error',
|
||||
scope: 'storage/optimized',
|
||||
);
|
||||
rethrow;
|
||||
@@ -211,162 +214,151 @@ class OptimizedStorageService {
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
_cache['server_config_count'] = 0;
|
||||
_cacheTimestamps['server_config_count'] = DateTime.now();
|
||||
return [];
|
||||
return const [];
|
||||
}
|
||||
|
||||
final decoded = jsonDecode(jsonString) as List<dynamic>;
|
||||
final configs = decoded
|
||||
.map((item) => ServerConfig.fromJson(item))
|
||||
.toList();
|
||||
|
||||
// Update cache
|
||||
_cache['server_config_count'] = configs.length;
|
||||
_cacheTimestamps['server_config_count'] = DateTime.now();
|
||||
|
||||
return configs;
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
DebugLogger.log(
|
||||
'Failed to retrieve server configs: $e',
|
||||
'Failed to retrieve server configs: $error',
|
||||
scope: 'storage/optimized',
|
||||
);
|
||||
return [];
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Active Server Management
|
||||
Future<void> setActiveServerId(String? serverId) async {
|
||||
if (serverId != null) {
|
||||
await _prefs.setString(_activeServerIdKey, serverId);
|
||||
await _preferencesBox.put(_activeServerIdKey, serverId);
|
||||
} else {
|
||||
await _prefs.remove(_activeServerIdKey);
|
||||
await _preferencesBox.delete(_activeServerIdKey);
|
||||
}
|
||||
|
||||
// Update cache
|
||||
_cache[_activeServerIdKey] = serverId;
|
||||
_cacheTimestamps[_activeServerIdKey] = DateTime.now();
|
||||
}
|
||||
|
||||
Future<String?> getActiveServerId() async {
|
||||
// Check cache first
|
||||
if (_isCacheValid(_activeServerIdKey)) {
|
||||
return _cache[_activeServerIdKey] as String?;
|
||||
}
|
||||
|
||||
final serverId = _prefs.getString(_activeServerIdKey);
|
||||
final serverId = _preferencesBox.get(_activeServerIdKey) as String?;
|
||||
_cache[_activeServerIdKey] = serverId;
|
||||
_cacheTimestamps[_activeServerIdKey] = DateTime.now();
|
||||
|
||||
return serverId;
|
||||
}
|
||||
|
||||
/// Theme Management
|
||||
String? getThemeMode() {
|
||||
return _prefs.getString(_themeModeKey);
|
||||
return _preferencesBox.get(_themeModeKey) as String?;
|
||||
}
|
||||
|
||||
Future<void> setThemeMode(String mode) async {
|
||||
await _prefs.setString(_themeModeKey, mode);
|
||||
await _preferencesBox.put(_themeModeKey, mode);
|
||||
}
|
||||
|
||||
/// Locale Management
|
||||
String? getLocaleCode() {
|
||||
// Returns a locale code like 'en', 'de', 'fr', 'it'. Null means system.
|
||||
return _prefs.getString(_localeCodeKey);
|
||||
return _preferencesBox.get(_localeCodeKey) as String?;
|
||||
}
|
||||
|
||||
Future<void> setLocaleCode(String? code) async {
|
||||
if (code == null || code.isEmpty) {
|
||||
await _prefs.remove(_localeCodeKey);
|
||||
await _preferencesBox.delete(_localeCodeKey);
|
||||
} else {
|
||||
await _prefs.setString(_localeCodeKey, code);
|
||||
await _preferencesBox.put(_localeCodeKey, code);
|
||||
}
|
||||
}
|
||||
|
||||
/// Onboarding
|
||||
Future<bool> getOnboardingSeen() async {
|
||||
return _prefs.getBool(_onboardingSeenKey) ?? false;
|
||||
return (_preferencesBox.get(_onboardingSeenKey) as bool?) ?? false;
|
||||
}
|
||||
|
||||
Future<void> setOnboardingSeen(bool seen) async {
|
||||
await _prefs.setBool(_onboardingSeenKey, seen);
|
||||
await _preferencesBox.put(_onboardingSeenKey, seen);
|
||||
}
|
||||
|
||||
/// Reviewer mode (persisted)
|
||||
Future<bool> getReviewerMode() async {
|
||||
return _prefs.getBool(_reviewerModeKey) ?? false;
|
||||
return (_preferencesBox.get(_reviewerModeKey) as bool?) ?? false;
|
||||
}
|
||||
|
||||
Future<void> setReviewerMode(bool enabled) async {
|
||||
await _prefs.setBool(_reviewerModeKey, enabled);
|
||||
await _preferencesBox.put(_reviewerModeKey, enabled);
|
||||
}
|
||||
|
||||
/// Local Conversations (Optimized with compression)
|
||||
Future<List<Conversation>> getLocalConversations() async {
|
||||
try {
|
||||
final jsonString = _prefs.getString(_localConversationsKey);
|
||||
if (jsonString == null || jsonString.isEmpty) return [];
|
||||
|
||||
final decoded = jsonDecode(jsonString) as List<dynamic>;
|
||||
return decoded.map((item) => Conversation.fromJson(item)).toList();
|
||||
} catch (e) {
|
||||
DebugLogger.log(
|
||||
'Failed to retrieve local conversations: $e',
|
||||
final stored = _cachesBox.get(_localConversationsKey);
|
||||
if (stored == null) {
|
||||
return const [];
|
||||
}
|
||||
if (stored is String) {
|
||||
final decoded = jsonDecode(stored) as List<dynamic>;
|
||||
return decoded.map((item) => Conversation.fromJson(item)).toList();
|
||||
}
|
||||
if (stored is List) {
|
||||
return stored
|
||||
.map(
|
||||
(item) =>
|
||||
Conversation.fromJson(Map<String, dynamic>.from(item as Map)),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
return const [];
|
||||
} catch (error, stack) {
|
||||
DebugLogger.error(
|
||||
'Failed to retrieve local conversations',
|
||||
scope: 'storage/optimized',
|
||||
error: error,
|
||||
stackTrace: stack,
|
||||
);
|
||||
return [];
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveLocalConversations(List<Conversation> conversations) async {
|
||||
try {
|
||||
// Only save essential data to reduce storage size
|
||||
final lightweightConversations = conversations
|
||||
.map(
|
||||
(conv) => {
|
||||
'id': conv.id,
|
||||
'title': conv.title,
|
||||
'updatedAt': conv.updatedAt.toIso8601String(),
|
||||
'messageCount': conv.messages.length,
|
||||
// Don't save full message content locally
|
||||
},
|
||||
)
|
||||
final serialized = conversations
|
||||
.map((conversation) => conversation.toJson())
|
||||
.toList();
|
||||
|
||||
final jsonString = jsonEncode(lightweightConversations);
|
||||
await _prefs.setString(_localConversationsKey, jsonString);
|
||||
|
||||
await _cachesBox.put(_localConversationsKey, serialized);
|
||||
DebugLogger.log(
|
||||
'Saved ${conversations.length} local conversations (lightweight)',
|
||||
'Saved ${conversations.length} local conversations',
|
||||
scope: 'storage/optimized',
|
||||
);
|
||||
} catch (e) {
|
||||
DebugLogger.log(
|
||||
'Failed to save local conversations: $e',
|
||||
} catch (error, stack) {
|
||||
DebugLogger.error(
|
||||
'Failed to save local conversations',
|
||||
scope: 'storage/optimized',
|
||||
error: error,
|
||||
stackTrace: stack,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Batch Operations for Performance
|
||||
// ---------------------------------------------------------------------------
|
||||
// Batch operations
|
||||
// ---------------------------------------------------------------------------
|
||||
Future<void> clearAuthData() async {
|
||||
try {
|
||||
// Clear auth-related data in batch
|
||||
await Future.wait([
|
||||
deleteAuthToken(),
|
||||
deleteSavedCredentials(),
|
||||
_prefs.remove(_rememberCredentialsKey),
|
||||
_prefs.remove(_activeServerIdKey),
|
||||
_preferencesBox.delete(_rememberCredentialsKey),
|
||||
_preferencesBox.delete(_activeServerIdKey),
|
||||
]);
|
||||
|
||||
// Clear related cache entries
|
||||
_cache.removeWhere(
|
||||
(key, value) =>
|
||||
(key, _) =>
|
||||
key.contains('auth') ||
|
||||
key.contains('credentials') ||
|
||||
key.contains('server'),
|
||||
);
|
||||
_cacheTimestamps.removeWhere(
|
||||
(key, value) =>
|
||||
(key, _) =>
|
||||
key.contains('auth') ||
|
||||
key.contains('credentials') ||
|
||||
key.contains('server'),
|
||||
@@ -376,9 +368,9 @@ class OptimizedStorageService {
|
||||
'Auth data cleared in batch operation',
|
||||
scope: 'storage/optimized',
|
||||
);
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
DebugLogger.log(
|
||||
'Failed to clear auth data: $e',
|
||||
'Failed to clear auth data: $error',
|
||||
scope: 'storage/optimized',
|
||||
);
|
||||
}
|
||||
@@ -386,30 +378,48 @@ class OptimizedStorageService {
|
||||
|
||||
Future<void> clearAll() async {
|
||||
try {
|
||||
await Future.wait([_secureCredentialStorage.clearAll(), _prefs.clear()]);
|
||||
await Future.wait([
|
||||
_secureCredentialStorage.clearAll(),
|
||||
_preferencesBox.clear(),
|
||||
_cachesBox.clear(),
|
||||
_attachmentQueueBox.clear(),
|
||||
]);
|
||||
|
||||
_cache.clear();
|
||||
_cacheTimestamps.clear();
|
||||
|
||||
// Preserve migration metadata
|
||||
final migrationVersion =
|
||||
_metadataBox.get(HiveStoreKeys.migrationVersion) as int?;
|
||||
await _metadataBox.clear();
|
||||
if (migrationVersion != null) {
|
||||
await _metadataBox.put(
|
||||
HiveStoreKeys.migrationVersion,
|
||||
migrationVersion,
|
||||
);
|
||||
}
|
||||
|
||||
DebugLogger.log('All storage cleared', scope: 'storage/optimized');
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
DebugLogger.log(
|
||||
'Failed to clear all storage: $e',
|
||||
'Failed to clear all storage: $error',
|
||||
scope: 'storage/optimized',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Storage Health Check
|
||||
Future<bool> isSecureStorageAvailable() async {
|
||||
return await _secureCredentialStorage.isSecureStorageAvailable();
|
||||
return _secureCredentialStorage.isSecureStorageAvailable();
|
||||
}
|
||||
|
||||
/// Cache Management Utilities
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cache helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
bool _isCacheValid(String key) {
|
||||
final timestamp = _cacheTimestamps[key];
|
||||
if (timestamp == null) return false;
|
||||
|
||||
if (timestamp == null) {
|
||||
return false;
|
||||
}
|
||||
return DateTime.now().difference(timestamp) < _cacheTimeout;
|
||||
}
|
||||
|
||||
@@ -419,37 +429,33 @@ class OptimizedStorageService {
|
||||
DebugLogger.log('Storage cache cleared', scope: 'storage/optimized');
|
||||
}
|
||||
|
||||
/// Migration from old storage service (one-time operation)
|
||||
// ---------------------------------------------------------------------------
|
||||
// Legacy migration hooks (no-op)
|
||||
// ---------------------------------------------------------------------------
|
||||
Future<void> migrateFromLegacyStorage() async {
|
||||
try {
|
||||
DebugLogger.log(
|
||||
'Starting migration from legacy storage',
|
||||
scope: 'storage/optimized',
|
||||
);
|
||||
|
||||
// This would be called once during app upgrade
|
||||
// Implementation would depend on the specific migration needs
|
||||
// For now, the SecureCredentialStorage already handles legacy migration
|
||||
|
||||
DebugLogger.log(
|
||||
'Legacy storage migration completed',
|
||||
scope: 'storage/optimized',
|
||||
);
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
DebugLogger.log(
|
||||
'Legacy storage migration failed: $e',
|
||||
'Legacy storage migration failed: $error',
|
||||
scope: 'storage/optimized',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Performance Monitoring
|
||||
Map<String, dynamic> getStorageStats() {
|
||||
return {
|
||||
'cacheSize': _cache.length,
|
||||
'cachedKeys': _cache.keys.toList(),
|
||||
'lastAccess': _cacheTimestamps.entries
|
||||
.map((e) => '${e.key}: ${e.value}')
|
||||
.map((entry) => '${entry.key}: ${entry.value}')
|
||||
.toList(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,246 +1,260 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import '../persistence/hive_boxes.dart';
|
||||
import '../persistence/persistence_keys.dart';
|
||||
import 'animation_service.dart';
|
||||
|
||||
part 'settings_service.g.dart';
|
||||
|
||||
/// Service for managing app-wide settings including accessibility preferences
|
||||
class SettingsService {
|
||||
static const String _reduceMotionKey = 'reduce_motion';
|
||||
static const String _animationSpeedKey = 'animation_speed';
|
||||
static const String _hapticFeedbackKey = 'haptic_feedback';
|
||||
static const String _highContrastKey = 'high_contrast';
|
||||
static const String _largeTextKey = 'large_text';
|
||||
static const String _darkModeKey = 'dark_mode';
|
||||
static const String _defaultModelKey = 'default_model';
|
||||
static const String _reduceMotionKey = PreferenceKeys.reduceMotion;
|
||||
static const String _animationSpeedKey = PreferenceKeys.animationSpeed;
|
||||
static const String _hapticFeedbackKey = PreferenceKeys.hapticFeedback;
|
||||
static const String _highContrastKey = PreferenceKeys.highContrast;
|
||||
static const String _largeTextKey = PreferenceKeys.largeText;
|
||||
static const String _darkModeKey = PreferenceKeys.darkMode;
|
||||
static const String _defaultModelKey = PreferenceKeys.defaultModel;
|
||||
// Model name formatting
|
||||
static const String _omitProviderInModelNameKey =
|
||||
'omit_provider_in_model_name';
|
||||
PreferenceKeys.omitProviderInModelName;
|
||||
// Voice input settings
|
||||
static const String _voiceLocaleKey = 'voice_locale_id';
|
||||
static const String _voiceHoldToTalkKey = 'voice_hold_to_talk';
|
||||
static const String _voiceAutoSendKey = 'voice_auto_send_final';
|
||||
static const String _voiceLocaleKey = PreferenceKeys.voiceLocaleId;
|
||||
static const String _voiceHoldToTalkKey = PreferenceKeys.voiceHoldToTalk;
|
||||
static const String _voiceAutoSendKey = PreferenceKeys.voiceAutoSendFinal;
|
||||
// Realtime transport preference
|
||||
static const String _socketTransportModeKey =
|
||||
'socket_transport_mode'; // 'auto' or 'ws'
|
||||
PreferenceKeys.socketTransportMode; // 'auto' or 'ws'
|
||||
// Quick pill visibility selections (max 2)
|
||||
static const String _quickPillsKey =
|
||||
'quick_pills'; // StringList of identifiers e.g. ['web','image','tools']
|
||||
static const String _quickPillsKey = PreferenceKeys
|
||||
.quickPills; // StringList of identifiers e.g. ['web','image','tools']
|
||||
// Chat input behavior
|
||||
static const String _sendOnEnterKey = 'send_on_enter';
|
||||
static const String _sendOnEnterKey = PreferenceKeys.sendOnEnterKey;
|
||||
|
||||
static Box<dynamic> _preferencesBox() =>
|
||||
Hive.box<dynamic>(HiveBoxNames.preferences);
|
||||
|
||||
/// Get reduced motion preference
|
||||
static Future<bool> getReduceMotion() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_reduceMotionKey) ?? false;
|
||||
static Future<bool> getReduceMotion() {
|
||||
final value = _preferencesBox().get(_reduceMotionKey) as bool?;
|
||||
return Future.value(value ?? false);
|
||||
}
|
||||
|
||||
/// Set reduced motion preference
|
||||
static Future<void> setReduceMotion(bool value) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_reduceMotionKey, value);
|
||||
static Future<void> setReduceMotion(bool value) {
|
||||
return _preferencesBox().put(_reduceMotionKey, value);
|
||||
}
|
||||
|
||||
/// Get animation speed multiplier (0.5 - 2.0)
|
||||
static Future<double> getAnimationSpeed() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getDouble(_animationSpeedKey) ?? 1.0;
|
||||
static Future<double> getAnimationSpeed() {
|
||||
final value = _preferencesBox().get(_animationSpeedKey) as num?;
|
||||
return Future.value((value?.toDouble() ?? 1.0).clamp(0.5, 2.0));
|
||||
}
|
||||
|
||||
/// Set animation speed multiplier
|
||||
static Future<void> setAnimationSpeed(double value) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setDouble(_animationSpeedKey, value.clamp(0.5, 2.0));
|
||||
static Future<void> setAnimationSpeed(double value) {
|
||||
final sanitized = value.clamp(0.5, 2.0).toDouble();
|
||||
return _preferencesBox().put(_animationSpeedKey, sanitized);
|
||||
}
|
||||
|
||||
/// Get haptic feedback preference
|
||||
static Future<bool> getHapticFeedback() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_hapticFeedbackKey) ?? true;
|
||||
static Future<bool> getHapticFeedback() {
|
||||
final value = _preferencesBox().get(_hapticFeedbackKey) as bool?;
|
||||
return Future.value(value ?? true);
|
||||
}
|
||||
|
||||
/// Set haptic feedback preference
|
||||
static Future<void> setHapticFeedback(bool value) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_hapticFeedbackKey, value);
|
||||
static Future<void> setHapticFeedback(bool value) {
|
||||
return _preferencesBox().put(_hapticFeedbackKey, value);
|
||||
}
|
||||
|
||||
/// Get high contrast preference
|
||||
static Future<bool> getHighContrast() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_highContrastKey) ?? false;
|
||||
static Future<bool> getHighContrast() {
|
||||
final value = _preferencesBox().get(_highContrastKey) as bool?;
|
||||
return Future.value(value ?? false);
|
||||
}
|
||||
|
||||
/// Set high contrast preference
|
||||
static Future<void> setHighContrast(bool value) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_highContrastKey, value);
|
||||
static Future<void> setHighContrast(bool value) {
|
||||
return _preferencesBox().put(_highContrastKey, value);
|
||||
}
|
||||
|
||||
/// Get large text preference
|
||||
static Future<bool> getLargeText() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_largeTextKey) ?? false;
|
||||
static Future<bool> getLargeText() {
|
||||
final value = _preferencesBox().get(_largeTextKey) as bool?;
|
||||
return Future.value(value ?? false);
|
||||
}
|
||||
|
||||
/// Set large text preference
|
||||
static Future<void> setLargeText(bool value) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_largeTextKey, value);
|
||||
static Future<void> setLargeText(bool value) {
|
||||
return _preferencesBox().put(_largeTextKey, value);
|
||||
}
|
||||
|
||||
/// Get dark mode preference
|
||||
static Future<bool> getDarkMode() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_darkModeKey) ?? true; // Default to dark
|
||||
static Future<bool> getDarkMode() {
|
||||
final value = _preferencesBox().get(_darkModeKey) as bool?;
|
||||
return Future.value(value ?? true);
|
||||
}
|
||||
|
||||
/// Set dark mode preference
|
||||
static Future<void> setDarkMode(bool value) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_darkModeKey, value);
|
||||
static Future<void> setDarkMode(bool value) {
|
||||
return _preferencesBox().put(_darkModeKey, value);
|
||||
}
|
||||
|
||||
/// Get default model preference
|
||||
static Future<String?> getDefaultModel() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_defaultModelKey);
|
||||
static Future<String?> getDefaultModel() {
|
||||
final value = _preferencesBox().get(_defaultModelKey) as String?;
|
||||
return Future.value(value);
|
||||
}
|
||||
|
||||
/// Set default model preference
|
||||
static Future<void> setDefaultModel(String? modelId) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
static Future<void> setDefaultModel(String? modelId) {
|
||||
final box = _preferencesBox();
|
||||
if (modelId != null) {
|
||||
await prefs.setString(_defaultModelKey, modelId);
|
||||
} else {
|
||||
await prefs.remove(_defaultModelKey);
|
||||
return box.put(_defaultModelKey, modelId);
|
||||
}
|
||||
return box.delete(_defaultModelKey);
|
||||
}
|
||||
|
||||
/// Whether to omit the provider prefix when displaying model names
|
||||
static Future<bool> getOmitProviderInModelName() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_omitProviderInModelNameKey) ?? true; // default: omit
|
||||
static Future<bool> getOmitProviderInModelName() {
|
||||
final value = _preferencesBox().get(_omitProviderInModelNameKey) as bool?;
|
||||
return Future.value(value ?? true);
|
||||
}
|
||||
|
||||
static Future<void> setOmitProviderInModelName(bool value) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_omitProviderInModelNameKey, value);
|
||||
static Future<void> setOmitProviderInModelName(bool value) {
|
||||
return _preferencesBox().put(_omitProviderInModelNameKey, value);
|
||||
}
|
||||
|
||||
/// Load all settings
|
||||
static Future<AppSettings> loadSettings() async {
|
||||
return AppSettings(
|
||||
reduceMotion: await getReduceMotion(),
|
||||
animationSpeed: await getAnimationSpeed(),
|
||||
hapticFeedback: await getHapticFeedback(),
|
||||
highContrast: await getHighContrast(),
|
||||
largeText: await getLargeText(),
|
||||
darkMode: await getDarkMode(),
|
||||
defaultModel: await getDefaultModel(),
|
||||
omitProviderInModelName: await getOmitProviderInModelName(),
|
||||
voiceLocaleId: await getVoiceLocaleId(),
|
||||
voiceHoldToTalk: await getVoiceHoldToTalk(),
|
||||
voiceAutoSendFinal: await getVoiceAutoSendFinal(),
|
||||
socketTransportMode: await getSocketTransportMode(),
|
||||
quickPills: await getQuickPills(),
|
||||
sendOnEnter: await getSendOnEnter(),
|
||||
static Future<AppSettings> loadSettings() {
|
||||
final box = _preferencesBox();
|
||||
return Future.value(
|
||||
AppSettings(
|
||||
reduceMotion: (box.get(_reduceMotionKey) as bool?) ?? false,
|
||||
animationSpeed:
|
||||
(box.get(_animationSpeedKey) as num?)?.toDouble() ?? 1.0,
|
||||
hapticFeedback: (box.get(_hapticFeedbackKey) as bool?) ?? true,
|
||||
highContrast: (box.get(_highContrastKey) as bool?) ?? false,
|
||||
largeText: (box.get(_largeTextKey) as bool?) ?? false,
|
||||
darkMode: (box.get(_darkModeKey) as bool?) ?? true,
|
||||
defaultModel: box.get(_defaultModelKey) as String?,
|
||||
omitProviderInModelName:
|
||||
(box.get(_omitProviderInModelNameKey) as bool?) ?? true,
|
||||
voiceLocaleId: box.get(_voiceLocaleKey) as String?,
|
||||
voiceHoldToTalk: (box.get(_voiceHoldToTalkKey) as bool?) ?? false,
|
||||
voiceAutoSendFinal: (box.get(_voiceAutoSendKey) as bool?) ?? false,
|
||||
socketTransportMode:
|
||||
box.get(_socketTransportModeKey, defaultValue: 'ws') as String,
|
||||
quickPills: List<String>.from(
|
||||
(box.get(_quickPillsKey) as List<dynamic>?) ?? const <String>[],
|
||||
),
|
||||
sendOnEnter: (box.get(_sendOnEnterKey) as bool?) ?? false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Save all settings
|
||||
static Future<void> saveSettings(AppSettings settings) async {
|
||||
await Future.wait([
|
||||
setReduceMotion(settings.reduceMotion),
|
||||
setAnimationSpeed(settings.animationSpeed),
|
||||
setHapticFeedback(settings.hapticFeedback),
|
||||
setHighContrast(settings.highContrast),
|
||||
setLargeText(settings.largeText),
|
||||
setDarkMode(settings.darkMode),
|
||||
setDefaultModel(settings.defaultModel),
|
||||
setOmitProviderInModelName(settings.omitProviderInModelName),
|
||||
setVoiceLocaleId(settings.voiceLocaleId),
|
||||
setVoiceHoldToTalk(settings.voiceHoldToTalk),
|
||||
setVoiceAutoSendFinal(settings.voiceAutoSendFinal),
|
||||
setSocketTransportMode(settings.socketTransportMode),
|
||||
setQuickPills(settings.quickPills),
|
||||
setSendOnEnter(settings.sendOnEnter),
|
||||
]);
|
||||
}
|
||||
final box = _preferencesBox();
|
||||
final updates = <String, Object?>{
|
||||
_reduceMotionKey: settings.reduceMotion,
|
||||
_animationSpeedKey: settings.animationSpeed,
|
||||
_hapticFeedbackKey: settings.hapticFeedback,
|
||||
_highContrastKey: settings.highContrast,
|
||||
_largeTextKey: settings.largeText,
|
||||
_darkModeKey: settings.darkMode,
|
||||
_omitProviderInModelNameKey: settings.omitProviderInModelName,
|
||||
_voiceHoldToTalkKey: settings.voiceHoldToTalk,
|
||||
_voiceAutoSendKey: settings.voiceAutoSendFinal,
|
||||
_socketTransportModeKey: settings.socketTransportMode,
|
||||
_quickPillsKey: settings.quickPills.take(2).toList(),
|
||||
_sendOnEnterKey: settings.sendOnEnter,
|
||||
};
|
||||
|
||||
// Voice input specific settings
|
||||
static Future<String?> getVoiceLocaleId() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_voiceLocaleKey);
|
||||
}
|
||||
await box.putAll(updates);
|
||||
|
||||
static Future<void> setVoiceLocaleId(String? localeId) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (localeId == null || localeId.isEmpty) {
|
||||
await prefs.remove(_voiceLocaleKey);
|
||||
if (settings.defaultModel != null) {
|
||||
await box.put(_defaultModelKey, settings.defaultModel);
|
||||
} else {
|
||||
await prefs.setString(_voiceLocaleKey, localeId);
|
||||
await box.delete(_defaultModelKey);
|
||||
}
|
||||
|
||||
if (settings.voiceLocaleId != null && settings.voiceLocaleId!.isNotEmpty) {
|
||||
await box.put(_voiceLocaleKey, settings.voiceLocaleId);
|
||||
} else {
|
||||
await box.delete(_voiceLocaleKey);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> getVoiceHoldToTalk() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_voiceHoldToTalkKey) ?? false;
|
||||
// Voice input specific settings
|
||||
static Future<String?> getVoiceLocaleId() {
|
||||
final value = _preferencesBox().get(_voiceLocaleKey) as String?;
|
||||
return Future.value(value);
|
||||
}
|
||||
|
||||
static Future<void> setVoiceHoldToTalk(bool value) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_voiceHoldToTalkKey, value);
|
||||
static Future<void> setVoiceLocaleId(String? localeId) {
|
||||
final box = _preferencesBox();
|
||||
if (localeId == null || localeId.isEmpty) {
|
||||
return box.delete(_voiceLocaleKey);
|
||||
}
|
||||
return box.put(_voiceLocaleKey, localeId);
|
||||
}
|
||||
|
||||
static Future<bool> getVoiceAutoSendFinal() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_voiceAutoSendKey) ?? false;
|
||||
static Future<bool> getVoiceHoldToTalk() {
|
||||
final value = _preferencesBox().get(_voiceHoldToTalkKey) as bool?;
|
||||
return Future.value(value ?? false);
|
||||
}
|
||||
|
||||
static Future<void> setVoiceAutoSendFinal(bool value) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_voiceAutoSendKey, value);
|
||||
static Future<void> setVoiceHoldToTalk(bool value) {
|
||||
return _preferencesBox().put(_voiceHoldToTalkKey, value);
|
||||
}
|
||||
|
||||
static Future<bool> getVoiceAutoSendFinal() {
|
||||
final value = _preferencesBox().get(_voiceAutoSendKey) as bool?;
|
||||
return Future.value(value ?? false);
|
||||
}
|
||||
|
||||
static Future<void> setVoiceAutoSendFinal(bool value) {
|
||||
return _preferencesBox().put(_voiceAutoSendKey, value);
|
||||
}
|
||||
|
||||
/// Transport mode: 'auto' (polling+websocket) or 'ws' (websocket only)
|
||||
static Future<String> getSocketTransportMode() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_socketTransportModeKey) ?? 'ws';
|
||||
static Future<String> getSocketTransportMode() {
|
||||
final value = _preferencesBox().get(_socketTransportModeKey) as String?;
|
||||
return Future.value(value ?? 'ws');
|
||||
}
|
||||
|
||||
static Future<void> setSocketTransportMode(String mode) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (mode != 'auto' && mode != 'ws') mode = 'auto';
|
||||
await prefs.setString(_socketTransportModeKey, mode);
|
||||
static Future<void> setSocketTransportMode(String mode) {
|
||||
if (mode != 'auto' && mode != 'ws') {
|
||||
mode = 'auto';
|
||||
}
|
||||
return _preferencesBox().put(_socketTransportModeKey, mode);
|
||||
}
|
||||
|
||||
// Quick Pills (visibility)
|
||||
static Future<List<String>> getQuickPills() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final list = prefs.getStringList(_quickPillsKey);
|
||||
// Default: none selected
|
||||
if (list == null) return const [];
|
||||
// Enforce max 2; accept arbitrary tool IDs plus 'web' and 'image'
|
||||
return list.take(2).toList();
|
||||
static Future<List<String>> getQuickPills() {
|
||||
final stored = _preferencesBox().get(_quickPillsKey) as List<dynamic>?;
|
||||
if (stored == null) {
|
||||
return Future.value(const []);
|
||||
}
|
||||
return Future.value(List<String>.from(stored.take(2)));
|
||||
}
|
||||
|
||||
static Future<void> setQuickPills(List<String> pills) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setStringList(_quickPillsKey, pills.take(2).toList());
|
||||
static Future<void> setQuickPills(List<String> pills) {
|
||||
return _preferencesBox().put(_quickPillsKey, pills.take(2).toList());
|
||||
}
|
||||
|
||||
// Chat input behavior
|
||||
static Future<bool> getSendOnEnter() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_sendOnEnterKey) ?? false;
|
||||
static Future<bool> getSendOnEnter() {
|
||||
final value = _preferencesBox().get(_sendOnEnterKey) as bool?;
|
||||
return Future.value(value ?? false);
|
||||
}
|
||||
|
||||
static Future<void> setSendOnEnter(bool value) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_sendOnEnterKey, value);
|
||||
static Future<void> setSendOnEnter(bool value) {
|
||||
return _preferencesBox().put(_sendOnEnterKey, value);
|
||||
}
|
||||
|
||||
/// Get effective animation duration considering all settings
|
||||
|
||||
@@ -1,416 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/server_config.dart';
|
||||
import '../models/conversation.dart';
|
||||
import 'secure_credential_storage.dart';
|
||||
import '../utils/debug_logger.dart';
|
||||
|
||||
class StorageService {
|
||||
final FlutterSecureStorage _secureStorage;
|
||||
final SharedPreferences _prefs;
|
||||
final SecureCredentialStorage _secureCredentialStorage;
|
||||
|
||||
StorageService({
|
||||
required FlutterSecureStorage secureStorage,
|
||||
required SharedPreferences prefs,
|
||||
}) : _secureStorage = secureStorage,
|
||||
_prefs = prefs,
|
||||
_secureCredentialStorage = SecureCredentialStorage(
|
||||
instance: secureStorage,
|
||||
);
|
||||
|
||||
// Secure storage keys
|
||||
static const String _authTokenKey = 'auth_token';
|
||||
static const String _serverConfigsKey = 'server_configs';
|
||||
static const String _activeServerIdKey = 'active_server_id';
|
||||
static const String _credentialsKey = 'saved_credentials';
|
||||
static const String _rememberCredentialsKey = 'remember_credentials';
|
||||
|
||||
// Shared preferences keys
|
||||
static const String _themeModeKey = 'theme_mode';
|
||||
static const String _localConversationsKey = 'local_conversations';
|
||||
|
||||
// Auth token management - using enhanced secure storage
|
||||
Future<void> saveAuthToken(String token) async {
|
||||
// Try enhanced secure storage first, fallback to legacy if needed
|
||||
try {
|
||||
await _secureCredentialStorage.saveAuthToken(token);
|
||||
} catch (e) {
|
||||
DebugLogger.log(
|
||||
'Enhanced secure storage failed, using fallback: $e',
|
||||
scope: 'storage',
|
||||
);
|
||||
await _secureStorage.write(key: _authTokenKey, value: token);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> getAuthToken() async {
|
||||
// Try enhanced secure storage first, fallback to legacy if needed
|
||||
try {
|
||||
final token = await _secureCredentialStorage.getAuthToken();
|
||||
if (token != null) return token;
|
||||
} catch (e) {
|
||||
DebugLogger.log(
|
||||
'Enhanced secure storage failed, using fallback: $e',
|
||||
scope: 'storage',
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback to legacy storage
|
||||
return await _secureStorage.read(key: _authTokenKey);
|
||||
}
|
||||
|
||||
Future<void> deleteAuthToken() async {
|
||||
// Clear from both storages to ensure complete cleanup
|
||||
try {
|
||||
await _secureCredentialStorage.deleteAuthToken();
|
||||
} catch (e) {
|
||||
DebugLogger.log(
|
||||
'Failed to delete from enhanced storage: $e',
|
||||
scope: 'storage',
|
||||
);
|
||||
}
|
||||
|
||||
await _secureStorage.delete(key: _authTokenKey);
|
||||
}
|
||||
|
||||
// Credential management for auto-login - using enhanced secure storage
|
||||
Future<void> saveCredentials({
|
||||
required String serverId,
|
||||
required String username,
|
||||
required String password,
|
||||
}) async {
|
||||
// Try enhanced secure storage first, fallback to legacy if needed
|
||||
try {
|
||||
// Check if enhanced secure storage is available
|
||||
final isSecureAvailable = await _secureCredentialStorage
|
||||
.isSecureStorageAvailable();
|
||||
if (!isSecureAvailable) {
|
||||
DebugLogger.log(
|
||||
'Enhanced secure storage not available, using legacy storage',
|
||||
scope: 'storage',
|
||||
);
|
||||
throw Exception('Enhanced secure storage not available');
|
||||
}
|
||||
|
||||
await _secureCredentialStorage.saveCredentials(
|
||||
serverId: serverId,
|
||||
username: username,
|
||||
password: password,
|
||||
);
|
||||
DebugLogger.log(
|
||||
'Credentials saved using enhanced secure storage',
|
||||
scope: 'storage',
|
||||
);
|
||||
} catch (e) {
|
||||
DebugLogger.log(
|
||||
'Enhanced secure storage failed, using fallback: $e',
|
||||
scope: 'storage',
|
||||
);
|
||||
|
||||
// Fallback to legacy storage
|
||||
try {
|
||||
final credentials = {
|
||||
'serverId': serverId,
|
||||
'username': username,
|
||||
'password': password,
|
||||
'savedAt': DateTime.now().toIso8601String(),
|
||||
};
|
||||
|
||||
await _secureStorage.write(
|
||||
key: _credentialsKey,
|
||||
value: jsonEncode(credentials),
|
||||
);
|
||||
|
||||
// Verify the fallback save
|
||||
final verifyData = await _secureStorage.read(key: _credentialsKey);
|
||||
if (verifyData == null || verifyData.isEmpty) {
|
||||
throw Exception(
|
||||
'Failed to save credentials even with fallback storage',
|
||||
);
|
||||
}
|
||||
|
||||
DebugLogger.log(
|
||||
'Credentials saved using fallback storage',
|
||||
scope: 'storage',
|
||||
);
|
||||
} catch (fallbackError) {
|
||||
DebugLogger.log(
|
||||
'Both enhanced and fallback credential storage failed: $fallbackError',
|
||||
scope: 'storage',
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, String>?> getSavedCredentials() async {
|
||||
// Try enhanced secure storage first
|
||||
try {
|
||||
final credentials = await _secureCredentialStorage.getSavedCredentials();
|
||||
if (credentials != null) {
|
||||
return credentials;
|
||||
}
|
||||
} catch (e) {
|
||||
DebugLogger.log(
|
||||
'Enhanced secure storage failed, using fallback: $e',
|
||||
scope: 'storage',
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback to legacy storage and migrate if found
|
||||
try {
|
||||
final jsonString = await _secureStorage.read(key: _credentialsKey);
|
||||
if (jsonString == null || jsonString.isEmpty) return null;
|
||||
|
||||
final decoded = jsonDecode(jsonString);
|
||||
if (decoded is! Map<String, dynamic>) return null;
|
||||
|
||||
// Validate that credentials have required fields
|
||||
if (!decoded.containsKey('serverId') ||
|
||||
!decoded.containsKey('username') ||
|
||||
!decoded.containsKey('password')) {
|
||||
DebugLogger.log('Invalid saved credentials format', scope: 'storage');
|
||||
await deleteSavedCredentials();
|
||||
return null;
|
||||
}
|
||||
|
||||
final legacyCredentials = {
|
||||
'serverId': decoded['serverId']?.toString() ?? '',
|
||||
'username': decoded['username']?.toString() ?? '',
|
||||
'password': decoded['password']?.toString() ?? '',
|
||||
'savedAt': decoded['savedAt']?.toString() ?? '',
|
||||
};
|
||||
|
||||
// Attempt to migrate to enhanced storage
|
||||
try {
|
||||
await _secureCredentialStorage.migrateFromOldStorage(legacyCredentials);
|
||||
// If migration successful, clean up legacy storage
|
||||
await _secureStorage.delete(key: _credentialsKey);
|
||||
DebugLogger.log(
|
||||
'Successfully migrated credentials to enhanced storage',
|
||||
scope: 'storage',
|
||||
);
|
||||
} catch (e) {
|
||||
DebugLogger.log('Failed to migrate credentials: $e', scope: 'storage');
|
||||
}
|
||||
|
||||
return legacyCredentials;
|
||||
} catch (e) {
|
||||
DebugLogger.log('Error loading saved credentials: $e', scope: 'storage');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteSavedCredentials() async {
|
||||
// Clear from both storages to ensure complete cleanup
|
||||
try {
|
||||
await _secureCredentialStorage.deleteSavedCredentials();
|
||||
} catch (e) {
|
||||
DebugLogger.log(
|
||||
'Failed to delete from enhanced storage: $e',
|
||||
scope: 'storage',
|
||||
);
|
||||
}
|
||||
|
||||
await _secureStorage.delete(key: _credentialsKey);
|
||||
await setRememberCredentials(false);
|
||||
}
|
||||
|
||||
// Remember credentials preference
|
||||
Future<void> setRememberCredentials(bool remember) async {
|
||||
await _prefs.setBool(_rememberCredentialsKey, remember);
|
||||
}
|
||||
|
||||
bool getRememberCredentials() {
|
||||
return _prefs.getBool(_rememberCredentialsKey) ?? false;
|
||||
}
|
||||
|
||||
// Server configuration management
|
||||
Future<void> saveServerConfigs(List<ServerConfig> configs) async {
|
||||
final json = configs.map((c) => c.toJson()).toList();
|
||||
await _secureStorage.write(key: _serverConfigsKey, value: jsonEncode(json));
|
||||
}
|
||||
|
||||
Future<List<ServerConfig>> getServerConfigs() async {
|
||||
try {
|
||||
final jsonString = await _secureStorage.read(key: _serverConfigsKey);
|
||||
if (jsonString == null || jsonString.isEmpty) return [];
|
||||
|
||||
final decoded = jsonDecode(jsonString);
|
||||
if (decoded is! List) {
|
||||
DebugLogger.log(
|
||||
'Server configs data is not a list, resetting',
|
||||
scope: 'storage',
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
final configs = <ServerConfig>[];
|
||||
for (final item in decoded) {
|
||||
try {
|
||||
if (item is Map<String, dynamic>) {
|
||||
// Validate required fields before parsing
|
||||
if (item.containsKey('id') &&
|
||||
item.containsKey('name') &&
|
||||
item.containsKey('url')) {
|
||||
configs.add(ServerConfig.fromJson(item));
|
||||
} else {
|
||||
DebugLogger.log(
|
||||
'Skipping invalid server config: missing required fields',
|
||||
scope: 'storage',
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
DebugLogger.log(
|
||||
'Failed to parse server config: $e',
|
||||
scope: 'storage',
|
||||
);
|
||||
// Continue with other configs
|
||||
}
|
||||
}
|
||||
|
||||
return configs;
|
||||
} catch (e) {
|
||||
DebugLogger.log('Error loading server configs: $e', scope: 'storage');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setActiveServerId(String? serverId) async {
|
||||
if (serverId == null) {
|
||||
await _secureStorage.delete(key: _activeServerIdKey);
|
||||
} else {
|
||||
await _secureStorage.write(key: _activeServerIdKey, value: serverId);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> getActiveServerId() async {
|
||||
return await _secureStorage.read(key: _activeServerIdKey);
|
||||
}
|
||||
|
||||
// Theme management
|
||||
String? getThemeMode() {
|
||||
return _prefs.getString(_themeModeKey);
|
||||
}
|
||||
|
||||
Future<void> setThemeMode(String mode) async {
|
||||
await _prefs.setString(_themeModeKey, mode);
|
||||
}
|
||||
|
||||
// Local conversation management
|
||||
Future<List<Conversation>> getLocalConversations() async {
|
||||
final jsonString = _prefs.getString(_localConversationsKey);
|
||||
if (jsonString == null || jsonString.isEmpty) return [];
|
||||
|
||||
try {
|
||||
final decoded = jsonDecode(jsonString);
|
||||
if (decoded is! List) {
|
||||
DebugLogger.log(
|
||||
'Local conversations data is not a list, resetting',
|
||||
scope: 'storage',
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
final conversations = <Conversation>[];
|
||||
for (final item in decoded) {
|
||||
try {
|
||||
if (item is Map<String, dynamic>) {
|
||||
// Validate required fields before parsing
|
||||
if (item.containsKey('id') &&
|
||||
item.containsKey('title') &&
|
||||
item.containsKey('createdAt') &&
|
||||
item.containsKey('updatedAt')) {
|
||||
conversations.add(Conversation.fromJson(item));
|
||||
} else {
|
||||
DebugLogger.log(
|
||||
'Skipping invalid conversation: missing required fields',
|
||||
scope: 'storage',
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
DebugLogger.log('Failed to parse conversation: $e', scope: 'storage');
|
||||
// Continue with other conversations
|
||||
}
|
||||
}
|
||||
|
||||
return conversations;
|
||||
} catch (e) {
|
||||
DebugLogger.log(
|
||||
'Error parsing local conversations: $e',
|
||||
scope: 'storage',
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveLocalConversations(List<Conversation> conversations) async {
|
||||
try {
|
||||
final json = conversations.map((c) => c.toJson()).toList();
|
||||
await _prefs.setString(_localConversationsKey, jsonEncode(json));
|
||||
} catch (e) {
|
||||
DebugLogger.log('Error saving local conversations: $e', scope: 'storage');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addLocalConversation(Conversation conversation) async {
|
||||
final conversations = await getLocalConversations();
|
||||
conversations.add(conversation);
|
||||
await saveLocalConversations(conversations);
|
||||
}
|
||||
|
||||
Future<void> updateLocalConversation(Conversation conversation) async {
|
||||
final conversations = await getLocalConversations();
|
||||
final index = conversations.indexWhere((c) => c.id == conversation.id);
|
||||
if (index != -1) {
|
||||
conversations[index] = conversation;
|
||||
await saveLocalConversations(conversations);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteLocalConversation(String conversationId) async {
|
||||
final conversations = await getLocalConversations();
|
||||
conversations.removeWhere((c) => c.id == conversationId);
|
||||
await saveLocalConversations(conversations);
|
||||
}
|
||||
|
||||
// Clear all data
|
||||
Future<void> clearAll() async {
|
||||
// Clear enhanced secure storage
|
||||
try {
|
||||
await _secureCredentialStorage.clearAll();
|
||||
} catch (e) {
|
||||
DebugLogger.log('Failed to clear enhanced storage: $e', scope: 'storage');
|
||||
}
|
||||
|
||||
// Clear legacy storage
|
||||
await _secureStorage.deleteAll();
|
||||
await _prefs.clear();
|
||||
|
||||
DebugLogger.log('All storage cleared', scope: 'storage');
|
||||
}
|
||||
|
||||
// Clear only auth-related data (keeping server configs and other settings)
|
||||
Future<void> clearAuthData() async {
|
||||
await deleteAuthToken();
|
||||
await deleteSavedCredentials();
|
||||
DebugLogger.log('Auth data cleared', scope: 'storage');
|
||||
}
|
||||
|
||||
/// Check if enhanced secure storage is available
|
||||
Future<bool> isEnhancedSecureStorageAvailable() async {
|
||||
try {
|
||||
return await _secureCredentialStorage.isSecureStorageAvailable();
|
||||
} catch (e) {
|
||||
DebugLogger.log(
|
||||
'Failed to check enhanced storage availability: $e',
|
||||
scope: 'storage',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user