From 80129c5711d58f79361b35a87e410a21be9a4013 Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:55:44 +0530 Subject: [PATCH] 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. --- lib/core/persistence/hive_bootstrap.dart | 46 ++ lib/core/persistence/hive_boxes.dart | 35 ++ lib/core/persistence/persistence_keys.dart | 29 ++ .../persistence/persistence_migrator.dart | 210 +++++++++ .../persistence/persistence_providers.dart | 10 + lib/core/providers/app_providers.dart | 17 +- .../services/attachment_upload_queue.dart | 43 +- .../services/optimized_storage_service.dart | 288 ++++++------ lib/core/services/settings_service.dart | 302 +++++++------ lib/core/services/storage_service.dart | 416 ------------------ lib/main.dart | 15 +- lib/shared/services/tasks/task_queue.dart | 27 +- pubspec.lock | 24 + pubspec.yaml | 2 + 14 files changed, 723 insertions(+), 741 deletions(-) create mode 100644 lib/core/persistence/hive_bootstrap.dart create mode 100644 lib/core/persistence/hive_boxes.dart create mode 100644 lib/core/persistence/persistence_keys.dart create mode 100644 lib/core/persistence/persistence_migrator.dart create mode 100644 lib/core/persistence/persistence_providers.dart delete mode 100644 lib/core/services/storage_service.dart diff --git a/lib/core/persistence/hive_bootstrap.dart b/lib/core/persistence/hive_bootstrap.dart new file mode 100644 index 0000000..024b69c --- /dev/null +++ b/lib/core/persistence/hive_bootstrap.dart @@ -0,0 +1,46 @@ +import 'package:hive_ce_flutter/hive_flutter.dart'; + +import 'hive_boxes.dart'; + +/// Sets up Hive and exposes lazily opened boxes used across the app. +class HiveBootstrap { + HiveBootstrap._(); + + static final HiveBootstrap instance = HiveBootstrap._(); + + HiveBoxes? _boxes; + + /// Ensures Hive is initialized and all required boxes are open. + Future ensureInitialized() async { + if (_boxes != null) { + return _boxes!; + } + + await Hive.initFlutter('conduit_hive'); + + final preferences = await Hive.openBox(HiveBoxNames.preferences); + final caches = await Hive.openBox(HiveBoxNames.caches); + final attachmentQueue = await Hive.openBox( + HiveBoxNames.attachmentQueue, + ); + final metadata = await Hive.openBox(HiveBoxNames.metadata); + + _boxes = HiveBoxes( + preferences: preferences, + caches: caches, + attachmentQueue: attachmentQueue, + metadata: metadata, + ); + + return _boxes!; + } + + /// Access the cached boxes after [ensureInitialized] has completed. + HiveBoxes get boxes { + final cached = _boxes; + if (cached == null) { + throw StateError('HiveBootstrap.ensureInitialized must run first.'); + } + return cached; + } +} diff --git a/lib/core/persistence/hive_boxes.dart b/lib/core/persistence/hive_boxes.dart new file mode 100644 index 0000000..28b461f --- /dev/null +++ b/lib/core/persistence/hive_boxes.dart @@ -0,0 +1,35 @@ +import 'package:hive_ce/hive.dart'; + +/// Logical names for Hive boxes used by the app. +final class HiveBoxNames { + static const String preferences = 'preferences_v1'; + static const String caches = 'caches_v1'; + static const String attachmentQueue = 'attachment_queue_v1'; + static const String metadata = 'metadata_v1'; +} + +/// Well-known keys stored inside Hive boxes. +final class HiveStoreKeys { + // Metadata + static const String migrationVersion = 'migration_version'; + + // Cache entries + static const String localConversations = 'local_conversations'; + static const String attachmentQueueEntries = 'attachment_queue_entries'; + static const String taskQueue = 'outbound_task_queue_v1'; +} + +/// Grouped Hive boxes that remain open for the app lifecycle. +class HiveBoxes { + HiveBoxes({ + required this.preferences, + required this.caches, + required this.attachmentQueue, + required this.metadata, + }); + + final Box preferences; + final Box caches; + final Box attachmentQueue; + final Box metadata; +} diff --git a/lib/core/persistence/persistence_keys.dart b/lib/core/persistence/persistence_keys.dart new file mode 100644 index 0000000..2e4b9af --- /dev/null +++ b/lib/core/persistence/persistence_keys.dart @@ -0,0 +1,29 @@ +/// Keys previously stored in SharedPreferences. Centralized so Hive-based +/// storage and migration logic stay aligned. +final class PreferenceKeys { + static const String reduceMotion = 'reduce_motion'; + static const String animationSpeed = 'animation_speed'; + static const String hapticFeedback = 'haptic_feedback'; + static const String highContrast = 'high_contrast'; + static const String largeText = 'large_text'; + static const String darkMode = 'dark_mode'; + static const String defaultModel = 'default_model'; + static const String omitProviderInModelName = 'omit_provider_in_model_name'; + static const String voiceLocaleId = 'voice_locale_id'; + static const String voiceHoldToTalk = 'voice_hold_to_talk'; + static const String voiceAutoSendFinal = 'voice_auto_send_final'; + static const String socketTransportMode = 'socket_transport_mode'; + static const String quickPills = 'quick_pills'; + static const String sendOnEnterKey = 'send_on_enter'; + static const String rememberCredentials = 'remember_credentials'; + static const String activeServerId = 'active_server_id'; + static const String themeMode = 'theme_mode'; + static const String localeCode = 'locale_code_v1'; + static const String onboardingSeen = 'onboarding_seen_v1'; + static const String reviewerMode = 'reviewer_mode_v1'; +} + +final class LegacyPreferenceKeys { + static const String attachmentUploadQueue = 'attachment_upload_queue'; + static const String taskQueue = 'outbound_task_queue_v1'; +} diff --git a/lib/core/persistence/persistence_migrator.dart b/lib/core/persistence/persistence_migrator.dart new file mode 100644 index 0000000..cf0b5b4 --- /dev/null +++ b/lib/core/persistence/persistence_migrator.dart @@ -0,0 +1,210 @@ +import 'dart:convert'; + +import 'package:shared_preferences/shared_preferences.dart'; + +import '../utils/debug_logger.dart'; +import 'hive_boxes.dart'; +import 'persistence_keys.dart'; + +/// Handles one-time migration from SharedPreferences to Hive-backed storage. +class PersistenceMigrator { + PersistenceMigrator({required HiveBoxes hiveBoxes}) : _boxes = hiveBoxes; + + static const int _targetVersion = 1; + + final HiveBoxes _boxes; + + Future migrateIfNeeded() async { + final currentVersion = + _boxes.metadata.get(HiveStoreKeys.migrationVersion) as int?; + if (currentVersion != null && currentVersion >= _targetVersion) { + return; + } + + DebugLogger.log( + 'Starting SharedPreferences → Hive migration', + scope: 'persistence/migration', + ); + + try { + final prefs = await SharedPreferences.getInstance(); + await _migratePreferences(prefs); + await _migrateCaches(prefs); + await _migrateAttachmentQueue(prefs); + await _migrateTaskQueue(prefs); + + await _boxes.metadata.put(HiveStoreKeys.migrationVersion, _targetVersion); + + await _cleanupLegacyKeys(prefs); + DebugLogger.log('Migration completed', scope: 'persistence/migration'); + } catch (error, stack) { + DebugLogger.error( + 'Migration failed', + scope: 'persistence/migration', + error: error, + stackTrace: stack, + ); + } + } + + Future _migratePreferences(SharedPreferences prefs) async { + final updates = {}; + + void copyBool(String key) { + final value = prefs.getBool(key); + if (value != null) updates[key] = value; + } + + void copyDouble(String key) { + final value = prefs.getDouble(key); + if (value != null) updates[key] = value; + } + + void copyString(String key) { + final value = prefs.getString(key); + if (value != null && value.isNotEmpty) updates[key] = value; + } + + void copyStringList(String key) { + final value = prefs.getStringList(key); + if (value != null && value.isNotEmpty) { + updates[key] = List.from(value); + } + } + + copyBool(PreferenceKeys.reduceMotion); + copyDouble(PreferenceKeys.animationSpeed); + copyBool(PreferenceKeys.hapticFeedback); + copyBool(PreferenceKeys.highContrast); + copyBool(PreferenceKeys.largeText); + copyBool(PreferenceKeys.darkMode); + copyString(PreferenceKeys.defaultModel); + copyBool(PreferenceKeys.omitProviderInModelName); + copyString(PreferenceKeys.voiceLocaleId); + copyBool(PreferenceKeys.voiceHoldToTalk); + copyBool(PreferenceKeys.voiceAutoSendFinal); + copyString(PreferenceKeys.socketTransportMode); + copyStringList(PreferenceKeys.quickPills); + copyBool(PreferenceKeys.sendOnEnterKey); + copyBool(PreferenceKeys.rememberCredentials); + copyString(PreferenceKeys.activeServerId); + copyString(PreferenceKeys.themeMode); + copyString(PreferenceKeys.localeCode); + copyBool(PreferenceKeys.onboardingSeen); + copyBool(PreferenceKeys.reviewerMode); + + if (updates.isNotEmpty) { + await _boxes.preferences.putAll(updates); + } + } + + Future _migrateCaches(SharedPreferences prefs) async { + final jsonString = prefs.getString(HiveStoreKeys.localConversations); + if (jsonString == null || jsonString.isEmpty) { + return; + } + + try { + final decoded = jsonDecode(jsonString); + if (decoded is List) { + final list = decoded + .map((entry) => Map.from(entry as Map)) + .toList(growable: false); + await _boxes.caches.put(HiveStoreKeys.localConversations, list); + } + } catch (error, stack) { + DebugLogger.error( + 'Failed to migrate local conversations', + scope: 'persistence/migration', + error: error, + stackTrace: stack, + ); + } + } + + Future _migrateAttachmentQueue(SharedPreferences prefs) async { + final jsonString = prefs.getString( + LegacyPreferenceKeys.attachmentUploadQueue, + ); + if (jsonString == null || jsonString.isEmpty) { + return; + } + + try { + final decoded = jsonDecode(jsonString); + if (decoded is List) { + final list = decoded + .map((entry) => Map.from(entry as Map)) + .toList(growable: false); + await _boxes.attachmentQueue.put( + HiveStoreKeys.attachmentQueueEntries, + list, + ); + } + } catch (error, stack) { + DebugLogger.error( + 'Failed to migrate attachment queue', + scope: 'persistence/migration', + error: error, + stackTrace: stack, + ); + } + } + + Future _migrateTaskQueue(SharedPreferences prefs) async { + final jsonString = prefs.getString(LegacyPreferenceKeys.taskQueue); + if (jsonString == null || jsonString.isEmpty) { + return; + } + + try { + final decoded = jsonDecode(jsonString); + if (decoded is List) { + final list = decoded + .map((entry) => Map.from(entry as Map)) + .toList(growable: false); + await _boxes.caches.put(HiveStoreKeys.taskQueue, list); + } + } catch (error, stack) { + DebugLogger.error( + 'Failed to migrate outbound task queue', + scope: 'persistence/migration', + error: error, + stackTrace: stack, + ); + } + } + + Future _cleanupLegacyKeys(SharedPreferences prefs) async { + final keysToRemove = [ + PreferenceKeys.reduceMotion, + PreferenceKeys.animationSpeed, + PreferenceKeys.hapticFeedback, + PreferenceKeys.highContrast, + PreferenceKeys.largeText, + PreferenceKeys.darkMode, + PreferenceKeys.defaultModel, + PreferenceKeys.omitProviderInModelName, + PreferenceKeys.voiceLocaleId, + PreferenceKeys.voiceHoldToTalk, + PreferenceKeys.voiceAutoSendFinal, + PreferenceKeys.socketTransportMode, + PreferenceKeys.quickPills, + PreferenceKeys.sendOnEnterKey, + PreferenceKeys.rememberCredentials, + PreferenceKeys.activeServerId, + PreferenceKeys.themeMode, + PreferenceKeys.localeCode, + PreferenceKeys.onboardingSeen, + PreferenceKeys.reviewerMode, + HiveStoreKeys.localConversations, + HiveStoreKeys.attachmentQueueEntries, + LegacyPreferenceKeys.attachmentUploadQueue, + LegacyPreferenceKeys.taskQueue, + ]; + + for (final key in keysToRemove) { + await prefs.remove(key); + } + } +} diff --git a/lib/core/persistence/persistence_providers.dart b/lib/core/persistence/persistence_providers.dart new file mode 100644 index 0000000..dd55349 --- /dev/null +++ b/lib/core/persistence/persistence_providers.dart @@ -0,0 +1,10 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import 'hive_boxes.dart'; + +part 'persistence_providers.g.dart'; + +/// Provides access to eagerly opened Hive boxes. Must be overridden in [main]. +@Riverpod(keepAlive: true) +HiveBoxes hiveBoxes(Ref ref) => + throw UnimplementedError('Hive boxes must be provided during bootstrap.'); diff --git a/lib/core/providers/app_providers.dart b/lib/core/providers/app_providers.dart index 033b45c..59825fc 100644 --- a/lib/core/providers/app_providers.dart +++ b/lib/core/providers/app_providers.dart @@ -4,9 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import '../services/storage_service.dart'; -// (removed duplicate) import '../services/optimized_storage_service.dart'; +import '../persistence/persistence_providers.dart'; import '../services/api_service.dart'; import '../auth/auth_state_manager.dart'; import '../../features/auth/providers/unified_auth_providers.dart'; @@ -29,10 +27,6 @@ import '../models/socket_event.dart'; part 'app_providers.g.dart'; // Storage providers -final sharedPreferencesProvider = Provider((ref) { - throw UnimplementedError(); -}); - final secureStorageProvider = Provider((ref) { // Single, shared instance with explicit platform options return const FlutterSecureStorage( @@ -50,20 +44,13 @@ final secureStorageProvider = Provider((ref) { ); }); -final storageServiceProvider = Provider((ref) { - return StorageService( - secureStorage: ref.watch(secureStorageProvider), - prefs: ref.watch(sharedPreferencesProvider), - ); -}); - // Optimized storage service provider final optimizedStorageServiceProvider = Provider(( ref, ) { return OptimizedStorageService( secureStorage: ref.watch(secureStorageProvider), - prefs: ref.watch(sharedPreferencesProvider), + boxes: ref.watch(hiveBoxesProvider), ); }); diff --git a/lib/core/services/attachment_upload_queue.dart b/lib/core/services/attachment_upload_queue.dart index 12ca333..bf08acb 100644 --- a/lib/core/services/attachment_upload_queue.dart +++ b/lib/core/services/attachment_upload_queue.dart @@ -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 _queueBox; + bool _initialized = false; final List _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(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 _load() async { - final jsonStr = (_prefs ?? await SharedPreferences.getInstance()).getString( - _prefsKey, - ); - if (jsonStr == null || jsonStr.isEmpty) return; - final list = (jsonDecode(jsonStr) as List).cast>(); + final stored = _queueBox.get(HiveStoreKeys.attachmentQueueEntries); + if (stored == null) { + return; + } + + List rawList; + if (stored is String && stored.isNotEmpty) { + rawList = (jsonDecode(stored) as List); + } else if (stored is List) { + rawList = stored; + } else { + return; + } + _queue ..clear() - ..addAll(list.map(QueuedAttachment.fromJson)); + ..addAll( + rawList.map( + (item) => + QueuedAttachment.fromJson(Map.from(item as Map)), + ), + ); } Future _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() { diff --git a/lib/core/services/optimized_storage_service.dart b/lib/core/services/optimized_storage_service.dart index 4008b7f..b66fded 100644 --- a/lib/core/services/optimized_storage_service.dart +++ b/lib/core/services/optimized_storage_service.dart @@ -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 _preferencesBox; + final Box _cachesBox; + final Box _attachmentQueueBox; + final Box _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 _cache = {}; - static const Duration _cacheTimeout = Duration(minutes: 5); final Map _cacheTimestamps = {}; + static const Duration _cacheTimeout = Duration(minutes: 5); - /// Auth Token Management (Optimized with caching) + // --------------------------------------------------------------------------- + // Auth token APIs (secure storage + in-memory cache) + // --------------------------------------------------------------------------- Future 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 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 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?> 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 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 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 saveServerConfigs(List 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; 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 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 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 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 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 getOnboardingSeen() async { - return _prefs.getBool(_onboardingSeenKey) ?? false; + return (_preferencesBox.get(_onboardingSeenKey) as bool?) ?? false; } Future setOnboardingSeen(bool seen) async { - await _prefs.setBool(_onboardingSeenKey, seen); + await _preferencesBox.put(_onboardingSeenKey, seen); } - /// Reviewer mode (persisted) Future getReviewerMode() async { - return _prefs.getBool(_reviewerModeKey) ?? false; + return (_preferencesBox.get(_reviewerModeKey) as bool?) ?? false; } Future setReviewerMode(bool enabled) async { - await _prefs.setBool(_reviewerModeKey, enabled); + await _preferencesBox.put(_reviewerModeKey, enabled); } - /// Local Conversations (Optimized with compression) Future> getLocalConversations() async { try { - final jsonString = _prefs.getString(_localConversationsKey); - if (jsonString == null || jsonString.isEmpty) return []; - - final decoded = jsonDecode(jsonString) as List; - 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; + return decoded.map((item) => Conversation.fromJson(item)).toList(); + } + if (stored is List) { + return stored + .map( + (item) => + Conversation.fromJson(Map.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 saveLocalConversations(List 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 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 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 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 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 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(), }; } diff --git a/lib/core/services/settings_service.dart b/lib/core/services/settings_service.dart index 9b72fbe..64bf5ef 100644 --- a/lib/core/services/settings_service.dart +++ b/lib/core/services/settings_service.dart @@ -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 _preferencesBox() => + Hive.box(HiveBoxNames.preferences); /// Get reduced motion preference - static Future getReduceMotion() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getBool(_reduceMotionKey) ?? false; + static Future getReduceMotion() { + final value = _preferencesBox().get(_reduceMotionKey) as bool?; + return Future.value(value ?? false); } /// Set reduced motion preference - static Future setReduceMotion(bool value) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool(_reduceMotionKey, value); + static Future setReduceMotion(bool value) { + return _preferencesBox().put(_reduceMotionKey, value); } /// Get animation speed multiplier (0.5 - 2.0) - static Future getAnimationSpeed() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getDouble(_animationSpeedKey) ?? 1.0; + static Future 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 setAnimationSpeed(double value) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setDouble(_animationSpeedKey, value.clamp(0.5, 2.0)); + static Future setAnimationSpeed(double value) { + final sanitized = value.clamp(0.5, 2.0).toDouble(); + return _preferencesBox().put(_animationSpeedKey, sanitized); } /// Get haptic feedback preference - static Future getHapticFeedback() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getBool(_hapticFeedbackKey) ?? true; + static Future getHapticFeedback() { + final value = _preferencesBox().get(_hapticFeedbackKey) as bool?; + return Future.value(value ?? true); } /// Set haptic feedback preference - static Future setHapticFeedback(bool value) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool(_hapticFeedbackKey, value); + static Future setHapticFeedback(bool value) { + return _preferencesBox().put(_hapticFeedbackKey, value); } /// Get high contrast preference - static Future getHighContrast() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getBool(_highContrastKey) ?? false; + static Future getHighContrast() { + final value = _preferencesBox().get(_highContrastKey) as bool?; + return Future.value(value ?? false); } /// Set high contrast preference - static Future setHighContrast(bool value) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool(_highContrastKey, value); + static Future setHighContrast(bool value) { + return _preferencesBox().put(_highContrastKey, value); } /// Get large text preference - static Future getLargeText() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getBool(_largeTextKey) ?? false; + static Future getLargeText() { + final value = _preferencesBox().get(_largeTextKey) as bool?; + return Future.value(value ?? false); } /// Set large text preference - static Future setLargeText(bool value) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool(_largeTextKey, value); + static Future setLargeText(bool value) { + return _preferencesBox().put(_largeTextKey, value); } /// Get dark mode preference - static Future getDarkMode() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getBool(_darkModeKey) ?? true; // Default to dark + static Future getDarkMode() { + final value = _preferencesBox().get(_darkModeKey) as bool?; + return Future.value(value ?? true); } /// Set dark mode preference - static Future setDarkMode(bool value) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool(_darkModeKey, value); + static Future setDarkMode(bool value) { + return _preferencesBox().put(_darkModeKey, value); } /// Get default model preference - static Future getDefaultModel() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getString(_defaultModelKey); + static Future getDefaultModel() { + final value = _preferencesBox().get(_defaultModelKey) as String?; + return Future.value(value); } /// Set default model preference - static Future setDefaultModel(String? modelId) async { - final prefs = await SharedPreferences.getInstance(); + static Future 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 getOmitProviderInModelName() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getBool(_omitProviderInModelNameKey) ?? true; // default: omit + static Future getOmitProviderInModelName() { + final value = _preferencesBox().get(_omitProviderInModelNameKey) as bool?; + return Future.value(value ?? true); } - static Future setOmitProviderInModelName(bool value) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool(_omitProviderInModelNameKey, value); + static Future setOmitProviderInModelName(bool value) { + return _preferencesBox().put(_omitProviderInModelNameKey, value); } /// Load all settings - static Future 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 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.from( + (box.get(_quickPillsKey) as List?) ?? const [], + ), + sendOnEnter: (box.get(_sendOnEnterKey) as bool?) ?? false, + ), ); } /// Save all settings static Future 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 = { + _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 getVoiceLocaleId() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getString(_voiceLocaleKey); - } + await box.putAll(updates); - static Future 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 getVoiceHoldToTalk() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getBool(_voiceHoldToTalkKey) ?? false; + // Voice input specific settings + static Future getVoiceLocaleId() { + final value = _preferencesBox().get(_voiceLocaleKey) as String?; + return Future.value(value); } - static Future setVoiceHoldToTalk(bool value) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool(_voiceHoldToTalkKey, value); + static Future setVoiceLocaleId(String? localeId) { + final box = _preferencesBox(); + if (localeId == null || localeId.isEmpty) { + return box.delete(_voiceLocaleKey); + } + return box.put(_voiceLocaleKey, localeId); } - static Future getVoiceAutoSendFinal() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getBool(_voiceAutoSendKey) ?? false; + static Future getVoiceHoldToTalk() { + final value = _preferencesBox().get(_voiceHoldToTalkKey) as bool?; + return Future.value(value ?? false); } - static Future setVoiceAutoSendFinal(bool value) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool(_voiceAutoSendKey, value); + static Future setVoiceHoldToTalk(bool value) { + return _preferencesBox().put(_voiceHoldToTalkKey, value); + } + + static Future getVoiceAutoSendFinal() { + final value = _preferencesBox().get(_voiceAutoSendKey) as bool?; + return Future.value(value ?? false); + } + + static Future setVoiceAutoSendFinal(bool value) { + return _preferencesBox().put(_voiceAutoSendKey, value); } /// Transport mode: 'auto' (polling+websocket) or 'ws' (websocket only) - static Future getSocketTransportMode() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getString(_socketTransportModeKey) ?? 'ws'; + static Future getSocketTransportMode() { + final value = _preferencesBox().get(_socketTransportModeKey) as String?; + return Future.value(value ?? 'ws'); } - static Future setSocketTransportMode(String mode) async { - final prefs = await SharedPreferences.getInstance(); - if (mode != 'auto' && mode != 'ws') mode = 'auto'; - await prefs.setString(_socketTransportModeKey, mode); + static Future setSocketTransportMode(String mode) { + if (mode != 'auto' && mode != 'ws') { + mode = 'auto'; + } + return _preferencesBox().put(_socketTransportModeKey, mode); } // Quick Pills (visibility) - static Future> 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> getQuickPills() { + final stored = _preferencesBox().get(_quickPillsKey) as List?; + if (stored == null) { + return Future.value(const []); + } + return Future.value(List.from(stored.take(2))); } - static Future setQuickPills(List pills) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setStringList(_quickPillsKey, pills.take(2).toList()); + static Future setQuickPills(List pills) { + return _preferencesBox().put(_quickPillsKey, pills.take(2).toList()); } // Chat input behavior - static Future getSendOnEnter() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getBool(_sendOnEnterKey) ?? false; + static Future getSendOnEnter() { + final value = _preferencesBox().get(_sendOnEnterKey) as bool?; + return Future.value(value ?? false); } - static Future setSendOnEnter(bool value) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool(_sendOnEnterKey, value); + static Future setSendOnEnter(bool value) { + return _preferencesBox().put(_sendOnEnterKey, value); } /// Get effective animation duration considering all settings diff --git a/lib/core/services/storage_service.dart b/lib/core/services/storage_service.dart deleted file mode 100644 index 8adda7c..0000000 --- a/lib/core/services/storage_service.dart +++ /dev/null @@ -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 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 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 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 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?> 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) 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 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 setRememberCredentials(bool remember) async { - await _prefs.setBool(_rememberCredentialsKey, remember); - } - - bool getRememberCredentials() { - return _prefs.getBool(_rememberCredentialsKey) ?? false; - } - - // Server configuration management - Future saveServerConfigs(List configs) async { - final json = configs.map((c) => c.toJson()).toList(); - await _secureStorage.write(key: _serverConfigsKey, value: jsonEncode(json)); - } - - Future> 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 = []; - for (final item in decoded) { - try { - if (item is Map) { - // 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 setActiveServerId(String? serverId) async { - if (serverId == null) { - await _secureStorage.delete(key: _activeServerIdKey); - } else { - await _secureStorage.write(key: _activeServerIdKey, value: serverId); - } - } - - Future getActiveServerId() async { - return await _secureStorage.read(key: _activeServerIdKey); - } - - // Theme management - String? getThemeMode() { - return _prefs.getString(_themeModeKey); - } - - Future setThemeMode(String mode) async { - await _prefs.setString(_themeModeKey, mode); - } - - // Local conversation management - Future> 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 = []; - for (final item in decoded) { - try { - if (item is Map) { - // 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 saveLocalConversations(List 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 addLocalConversation(Conversation conversation) async { - final conversations = await getLocalConversations(); - conversations.add(conversation); - await saveLocalConversations(conversations); - } - - Future 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 deleteLocalConversation(String conversationId) async { - final conversations = await getLocalConversations(); - conversations.removeWhere((c) => c.id == conversationId); - await saveLocalConversations(conversations); - } - - // Clear all data - Future 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 clearAuthData() async { - await deleteAuthToken(); - await deleteSavedCredentials(); - DebugLogger.log('Auth data cleared', scope: 'storage'); - } - - /// Check if enhanced secure storage is available - Future isEnhancedSecureStorageAvailable() async { - try { - return await _secureCredentialStorage.isSecureStorageAvailable(); - } catch (e) { - DebugLogger.log( - 'Failed to check enhanced storage availability: $e', - scope: 'storage', - ); - return false; - } - } -} diff --git a/lib/main.dart b/lib/main.dart index 80a2b9b..6d4b79d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,9 +5,11 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'core/widgets/error_boundary.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'core/providers/app_providers.dart'; +import 'core/persistence/hive_bootstrap.dart'; +import 'core/persistence/persistence_migrator.dart'; +import 'core/persistence/persistence_providers.dart'; import 'core/router/app_router.dart'; import 'shared/theme/app_theme.dart'; import 'shared/widgets/offline_indicator.dart'; @@ -62,8 +64,6 @@ void main() { _startupTimeline?.instant('edge_to_edge_enabled'); }); - final sharedPrefs = await SharedPreferences.getInstance(); - _startupTimeline!.instant('shared_prefs_ready'); const secureStorage = FlutterSecureStorage( aOptions: AndroidOptions( encryptedSharedPreferences: true, @@ -78,6 +78,13 @@ void main() { ); _startupTimeline!.instant('secure_storage_ready'); + final hiveBoxes = await HiveBootstrap.instance.ensureInitialized(); + _startupTimeline!.instant('hive_ready'); + + final migrator = PersistenceMigrator(hiveBoxes: hiveBoxes); + await migrator.migrateIfNeeded(); + _startupTimeline!.instant('migration_complete'); + // Finish timeline after first frame paints WidgetsBinding.instance.addPostFrameCallback((_) { _startupTimeline?.instant('first_frame_rendered'); @@ -88,8 +95,8 @@ void main() { runApp( ProviderScope( overrides: [ - sharedPreferencesProvider.overrideWithValue(sharedPrefs), secureStorageProvider.overrideWithValue(secureStorage), + hiveBoxesProvider.overrideWithValue(hiveBoxes), ], child: const ConduitApp(), ), diff --git a/lib/shared/services/tasks/task_queue.dart b/lib/shared/services/tasks/task_queue.dart index dbaaf10..2a2a041 100644 --- a/lib/shared/services/tasks/task_queue.dart +++ b/lib/shared/services/tasks/task_queue.dart @@ -4,7 +4,8 @@ import 'dart:convert'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:uuid/uuid.dart'; -import '../../../core/providers/app_providers.dart'; +import '../../../core/persistence/persistence_providers.dart'; +import '../../../core/persistence/hive_boxes.dart'; import 'outbound_task.dart'; import 'task_worker.dart'; import '../../../core/utils/debug_logger.dart'; @@ -15,7 +16,7 @@ final taskQueueProvider = ); class TaskQueueNotifier extends Notifier> { - static const _prefsKey = 'outbound_task_queue_v1'; + static const _storageKey = HiveStoreKeys.taskQueue; final _uuid = const Uuid(); bool _bootstrapScheduled = false; @@ -34,10 +35,20 @@ class TaskQueueNotifier extends Notifier> { Future _load() async { try { - final prefs = ref.read(sharedPreferencesProvider); - final jsonStr = prefs.getString(_prefsKey); - if (jsonStr == null || jsonStr.isEmpty) return; - final raw = (jsonDecode(jsonStr) as List).cast>(); + final boxes = ref.read(hiveBoxesProvider); + final stored = boxes.caches.get(_storageKey); + if (stored == null) return; + + List> raw; + if (stored is String && stored.isNotEmpty) { + raw = (jsonDecode(stored) as List).cast>(); + } else if (stored is List) { + raw = stored + .map((entry) => Map.from(entry as Map)) + .toList(growable: false); + } else { + return; + } final tasks = raw.map(OutboundTask.fromJson).toList(); // Only restore non-completed tasks state = tasks @@ -62,7 +73,7 @@ class TaskQueueNotifier extends Notifier> { Future _save() async { try { - final prefs = ref.read(sharedPreferencesProvider); + final boxes = ref.read(hiveBoxesProvider); final retained = [ for (final task in state) if (task.status == TaskStatus.queued || @@ -76,7 +87,7 @@ class TaskQueueNotifier extends Notifier> { } final raw = retained.map((t) => t.toJson()).toList(growable: false); - await prefs.setString(_prefsKey, jsonEncode(raw)); + await boxes.caches.put(_storageKey, raw); } catch (e) { DebugLogger.log('Failed to persist task queue: $e', scope: 'tasks/queue'); } diff --git a/pubspec.lock b/pubspec.lock index b14826b..15a7ac4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -597,6 +597,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + hive_ce: + dependency: "direct main" + description: + name: hive_ce + sha256: d678b1b2e315c18cd7ed8fd79eda25d70a1f3852d6988bfe5461cffe260c60aa + url: "https://pub.dev" + source: hosted + version: "2.14.0" + hive_ce_flutter: + dependency: "direct main" + description: + name: hive_ce_flutter + sha256: f5bd57fda84402bca7557fedb8c629c96c8ea10fab4a542968d7b60864ca02cc + url: "https://pub.dev" + source: hosted + version: "2.3.2" hotreloader: dependency: transitive description: @@ -725,6 +741,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + isolate_channel: + dependency: transitive + description: + name: isolate_channel + sha256: f3d36f783b301e6b312c3450eeb2656b0e7d1db81331af2a151d9083a3f6b18d + url: "https://pub.dev" + source: hosted + version: "0.2.2+1" js: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 14da8c7..cfb7a0a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,6 +25,8 @@ dependencies: # Storage flutter_secure_storage: ^9.2.2 + hive_ce: ^2.14.0 + hive_ce_flutter: ^2.3.2 shared_preferences: ^2.3.2 # UI Components - Enhanced Markdown