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:
cogwheel0
2025-10-01 16:55:44 +05:30
parent 7d17c97d7e
commit 80129c5711
14 changed files with 723 additions and 741 deletions

View File

@@ -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<HiveBoxes> ensureInitialized() async {
if (_boxes != null) {
return _boxes!;
}
await Hive.initFlutter('conduit_hive');
final preferences = await Hive.openBox<dynamic>(HiveBoxNames.preferences);
final caches = await Hive.openBox<dynamic>(HiveBoxNames.caches);
final attachmentQueue = await Hive.openBox<dynamic>(
HiveBoxNames.attachmentQueue,
);
final metadata = await Hive.openBox<dynamic>(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;
}
}

View File

@@ -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<dynamic> preferences;
final Box<dynamic> caches;
final Box<dynamic> attachmentQueue;
final Box<dynamic> metadata;
}

View File

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

View File

@@ -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<void> 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<void> _migratePreferences(SharedPreferences prefs) async {
final updates = <String, Object?>{};
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<String>.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<void> _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<String, dynamic>.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<void> _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<String, dynamic>.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<void> _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<String, dynamic>.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<void> _cleanupLegacyKeys(SharedPreferences prefs) async {
final keysToRemove = <String>[
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);
}
}
}

View File

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

View File

@@ -4,9 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart'; import '../persistence/persistence_providers.dart';
import '../services/storage_service.dart';
// (removed duplicate) import '../services/optimized_storage_service.dart';
import '../services/api_service.dart'; import '../services/api_service.dart';
import '../auth/auth_state_manager.dart'; import '../auth/auth_state_manager.dart';
import '../../features/auth/providers/unified_auth_providers.dart'; import '../../features/auth/providers/unified_auth_providers.dart';
@@ -29,10 +27,6 @@ import '../models/socket_event.dart';
part 'app_providers.g.dart'; part 'app_providers.g.dart';
// Storage providers // Storage providers
final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
throw UnimplementedError();
});
final secureStorageProvider = Provider<FlutterSecureStorage>((ref) { final secureStorageProvider = Provider<FlutterSecureStorage>((ref) {
// Single, shared instance with explicit platform options // Single, shared instance with explicit platform options
return const FlutterSecureStorage( return const FlutterSecureStorage(
@@ -50,20 +44,13 @@ final secureStorageProvider = Provider<FlutterSecureStorage>((ref) {
); );
}); });
final storageServiceProvider = Provider<StorageService>((ref) {
return StorageService(
secureStorage: ref.watch(secureStorageProvider),
prefs: ref.watch(sharedPreferencesProvider),
);
});
// Optimized storage service provider // Optimized storage service provider
final optimizedStorageServiceProvider = Provider<OptimizedStorageService>(( final optimizedStorageServiceProvider = Provider<OptimizedStorageService>((
ref, ref,
) { ) {
return OptimizedStorageService( return OptimizedStorageService(
secureStorage: ref.watch(secureStorageProvider), secureStorage: ref.watch(secureStorageProvider),
prefs: ref.watch(sharedPreferencesProvider), boxes: ref.watch(hiveBoxesProvider),
); );
}); });

View File

@@ -2,7 +2,8 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:dio/dio.dart'; 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'; import '../utils/debug_logger.dart';
/// Status of a queued attachment upload /// Status of a queued attachment upload
@@ -110,12 +111,12 @@ class AttachmentUploadQueue {
factory AttachmentUploadQueue() => _instance; factory AttachmentUploadQueue() => _instance;
AttachmentUploadQueue._internal(); AttachmentUploadQueue._internal();
static const String _prefsKey = 'attachment_upload_queue';
static const int _maxRetries = 4; static const int _maxRetries = 4;
static const Duration _baseRetryDelay = Duration(seconds: 5); static const Duration _baseRetryDelay = Duration(seconds: 5);
static const Duration _maxRetryDelay = Duration(minutes: 5); static const Duration _maxRetryDelay = Duration(minutes: 5);
SharedPreferences? _prefs; late final Box<dynamic> _queueBox;
bool _initialized = false;
final List<QueuedAttachment> _queue = []; final List<QueuedAttachment> _queue = [];
Timer? _retryTimer; Timer? _retryTimer;
bool _isProcessing = false; bool _isProcessing = false;
@@ -136,11 +137,14 @@ class AttachmentUploadQueue {
}) async { }) async {
_onUpload = onUpload; _onUpload = onUpload;
_onQueueChanged = onQueueChanged; _onQueueChanged = onQueueChanged;
_prefs ??= await SharedPreferences.getInstance(); if (!_initialized) {
_queueBox = Hive.box<dynamic>(HiveBoxNames.attachmentQueue);
_initialized = true;
}
await _load(); await _load();
_startPeriodicProcessing(); _startPeriodicProcessing();
DebugLogger.log( DebugLogger.log(
'AttachmentUploadQueue initialized with ${_queue.length} items', 'AttachmentUploadQueue initialized with \${_queue.length} items',
scope: 'attachments/queue', scope: 'attachments/queue',
); );
} }
@@ -328,20 +332,33 @@ class AttachmentUploadQueue {
// Utilities // Utilities
Future<void> _load() async { Future<void> _load() async {
final jsonStr = (_prefs ?? await SharedPreferences.getInstance()).getString( final stored = _queueBox.get(HiveStoreKeys.attachmentQueueEntries);
_prefsKey, if (stored == null) {
); return;
if (jsonStr == null || jsonStr.isEmpty) return; }
final list = (jsonDecode(jsonStr) as List).cast<Map<String, dynamic>>();
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 _queue
..clear() ..clear()
..addAll(list.map(QueuedAttachment.fromJson)); ..addAll(
rawList.map(
(item) =>
QueuedAttachment.fromJson(Map<String, dynamic>.from(item as Map)),
),
);
} }
Future<void> _save() async { Future<void> _save() async {
final prefs = _prefs ?? await SharedPreferences.getInstance();
final list = _queue.map((e) => e.toJson()).toList(growable: false); final list = _queue.map((e) => e.toJson()).toList(growable: false);
await prefs.setString(_prefsKey, jsonEncode(list)); await _queueBox.put(HiveStoreKeys.attachmentQueueEntries, list);
} }
void _notify() { void _notify() {

View File

@@ -1,41 +1,52 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:hive_ce/hive.dart';
import 'secure_credential_storage.dart';
import '../models/server_config.dart';
import '../models/conversation.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 '../utils/debug_logger.dart';
import 'secure_credential_storage.dart';
/// Optimized storage service with single secure storage implementation /// Optimized storage service backed by Hive for non-sensitive data and
/// Eliminates dual storage overhead and improves performance /// FlutterSecureStorage for credentials.
class OptimizedStorageService { class OptimizedStorageService {
final SharedPreferences _prefs;
final SecureCredentialStorage _secureCredentialStorage;
OptimizedStorageService({ OptimizedStorageService({
required FlutterSecureStorage secureStorage, required FlutterSecureStorage secureStorage,
required SharedPreferences prefs, required HiveBoxes boxes,
}) : _prefs = prefs, }) : _preferencesBox = boxes.preferences,
_cachesBox = boxes.caches,
_attachmentQueueBox = boxes.attachmentQueue,
_metadataBox = boxes.metadata,
_secureCredentialStorage = SecureCredentialStorage( _secureCredentialStorage = SecureCredentialStorage(
instance: secureStorage, 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 _authTokenKey = 'auth_token_v3';
static const String _activeServerIdKey = 'active_server_id'; static const String _activeServerIdKey = PreferenceKeys.activeServerId;
static const String _rememberCredentialsKey = 'remember_credentials'; static const String _rememberCredentialsKey =
static const String _themeModeKey = 'theme_mode'; PreferenceKeys.rememberCredentials;
static const String _localeCodeKey = 'locale_code_v1'; static const String _themeModeKey = PreferenceKeys.themeMode;
static const String _localConversationsKey = 'local_conversations'; static const String _localeCodeKey = PreferenceKeys.localeCode;
static const String _onboardingSeenKey = 'onboarding_seen_v1'; static const String _localConversationsKey = HiveStoreKeys.localConversations;
static const String _reviewerModeKey = 'reviewer_mode_v1'; static const String _onboardingSeenKey = PreferenceKeys.onboardingSeen;
static const String _reviewerModeKey = PreferenceKeys.reviewerMode;
// Cache for frequently accessed data
final Map<String, dynamic> _cache = {}; final Map<String, dynamic> _cache = {};
static const Duration _cacheTimeout = Duration(minutes: 5);
final Map<String, DateTime> _cacheTimestamps = {}; 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 { Future<void> saveAuthToken(String token) async {
try { try {
await _secureCredentialStorage.saveAuthToken(token); await _secureCredentialStorage.saveAuthToken(token);
@@ -45,9 +56,9 @@ class OptimizedStorageService {
'Auth token saved and cached', 'Auth token saved and cached',
scope: 'storage/optimized', scope: 'storage/optimized',
); );
} catch (e) { } catch (error) {
DebugLogger.log( DebugLogger.log(
'Failed to save auth token: $e', 'Failed to save auth token: $error',
scope: 'storage/optimized', scope: 'storage/optimized',
); );
rethrow; rethrow;
@@ -55,12 +66,11 @@ class OptimizedStorageService {
} }
Future<String?> getAuthToken() async { Future<String?> getAuthToken() async {
// Check cache first
if (_isCacheValid(_authTokenKey)) { if (_isCacheValid(_authTokenKey)) {
final cachedToken = _cache[_authTokenKey] as String?; final cached = _cache[_authTokenKey] as String?;
if (cachedToken != null) { if (cached != null) {
DebugLogger.log('Using cached auth token', scope: 'storage/optimized'); DebugLogger.log('Using cached auth token', scope: 'storage/optimized');
return cachedToken; return cached;
} }
} }
@@ -71,9 +81,9 @@ class OptimizedStorageService {
_cacheTimestamps[_authTokenKey] = DateTime.now(); _cacheTimestamps[_authTokenKey] = DateTime.now();
} }
return token; return token;
} catch (e) { } catch (error) {
DebugLogger.log( DebugLogger.log(
'Failed to retrieve auth token: $e', 'Failed to retrieve auth token: $error',
scope: 'storage/optimized', scope: 'storage/optimized',
); );
return null; return null;
@@ -89,15 +99,17 @@ class OptimizedStorageService {
'Auth token deleted and cache cleared', 'Auth token deleted and cache cleared',
scope: 'storage/optimized', scope: 'storage/optimized',
); );
} catch (e) { } catch (error) {
DebugLogger.log( DebugLogger.log(
'Failed to delete auth token: $e', 'Failed to delete auth token: $error',
scope: 'storage/optimized', scope: 'storage/optimized',
); );
} }
} }
/// Credential Management (Single storage implementation) // ---------------------------------------------------------------------------
// Credential APIs (secure storage only)
// ---------------------------------------------------------------------------
Future<void> saveCredentials({ Future<void> saveCredentials({
required String serverId, required String serverId,
required String username, required String username,
@@ -110,7 +122,6 @@ class OptimizedStorageService {
password: password, password: password,
); );
// Cache the fact that credentials exist (not the credentials themselves)
_cache['has_credentials'] = true; _cache['has_credentials'] = true;
_cacheTimestamps['has_credentials'] = DateTime.now(); _cacheTimestamps['has_credentials'] = DateTime.now();
@@ -118,9 +129,9 @@ class OptimizedStorageService {
'Credentials saved via optimized storage', 'Credentials saved via optimized storage',
scope: 'storage/optimized', scope: 'storage/optimized',
); );
} catch (e) { } catch (error) {
DebugLogger.log( DebugLogger.log(
'Failed to save credentials: $e', 'Failed to save credentials: $error',
scope: 'storage/optimized', scope: 'storage/optimized',
); );
rethrow; rethrow;
@@ -129,17 +140,13 @@ class OptimizedStorageService {
Future<Map<String, String>?> getSavedCredentials() async { Future<Map<String, String>?> getSavedCredentials() async {
try { try {
// Use single storage implementation - no fallback needed
final credentials = await _secureCredentialStorage.getSavedCredentials(); final credentials = await _secureCredentialStorage.getSavedCredentials();
// Update cache flag
_cache['has_credentials'] = credentials != null; _cache['has_credentials'] = credentials != null;
_cacheTimestamps['has_credentials'] = DateTime.now(); _cacheTimestamps['has_credentials'] = DateTime.now();
return credentials; return credentials;
} catch (e) { } catch (error) {
DebugLogger.log( DebugLogger.log(
'Failed to retrieve credentials: $e', 'Failed to retrieve credentials: $error',
scope: 'storage/optimized', scope: 'storage/optimized',
); );
return null; return null;
@@ -155,50 +162,46 @@ class OptimizedStorageService {
'Credentials deleted via optimized storage', 'Credentials deleted via optimized storage',
scope: 'storage/optimized', scope: 'storage/optimized',
); );
} catch (e) { } catch (error) {
DebugLogger.log( DebugLogger.log(
'Failed to delete credentials: $e', 'Failed to delete credentials: $error',
scope: 'storage/optimized', scope: 'storage/optimized',
); );
} }
} }
/// Quick check if credentials exist (uses cache)
Future<bool> hasCredentials() async { Future<bool> hasCredentials() async {
if (_isCacheValid('has_credentials')) { if (_isCacheValid('has_credentials')) {
return _cache['has_credentials'] == true; return _cache['has_credentials'] == true;
} }
final credentials = await getSavedCredentials(); final credentials = await getSavedCredentials();
return credentials != null; return credentials != null;
} }
/// Remember Credentials Flag // ---------------------------------------------------------------------------
// Preference helpers (Hive-backed)
// ---------------------------------------------------------------------------
Future<void> setRememberCredentials(bool remember) async { Future<void> setRememberCredentials(bool remember) async {
await _prefs.setBool(_rememberCredentialsKey, remember); await _preferencesBox.put(_rememberCredentialsKey, remember);
} }
bool getRememberCredentials() { bool getRememberCredentials() {
return _prefs.getBool(_rememberCredentialsKey) ?? false; return (_preferencesBox.get(_rememberCredentialsKey) as bool?) ?? false;
} }
/// Server Configuration (Optimized)
Future<void> saveServerConfigs(List<ServerConfig> configs) async { Future<void> saveServerConfigs(List<ServerConfig> configs) async {
try { try {
final jsonString = jsonEncode(configs.map((c) => c.toJson()).toList()); final jsonString = jsonEncode(configs.map((c) => c.toJson()).toList());
await _secureCredentialStorage.saveServerConfigs(jsonString); await _secureCredentialStorage.saveServerConfigs(jsonString);
// Cache config count for quick checks
_cache['server_config_count'] = configs.length; _cache['server_config_count'] = configs.length;
_cacheTimestamps['server_config_count'] = DateTime.now(); _cacheTimestamps['server_config_count'] = DateTime.now();
DebugLogger.log( DebugLogger.log(
'Server configs saved (${configs.length} configs)', 'Server configs saved (${configs.length} entries)',
scope: 'storage/optimized', scope: 'storage/optimized',
); );
} catch (e) { } catch (error) {
DebugLogger.log( DebugLogger.log(
'Failed to save server configs: $e', 'Failed to save server configs: $error',
scope: 'storage/optimized', scope: 'storage/optimized',
); );
rethrow; rethrow;
@@ -211,162 +214,151 @@ class OptimizedStorageService {
if (jsonString == null || jsonString.isEmpty) { if (jsonString == null || jsonString.isEmpty) {
_cache['server_config_count'] = 0; _cache['server_config_count'] = 0;
_cacheTimestamps['server_config_count'] = DateTime.now(); _cacheTimestamps['server_config_count'] = DateTime.now();
return []; return const [];
} }
final decoded = jsonDecode(jsonString) as List<dynamic>; final decoded = jsonDecode(jsonString) as List<dynamic>;
final configs = decoded final configs = decoded
.map((item) => ServerConfig.fromJson(item)) .map((item) => ServerConfig.fromJson(item))
.toList(); .toList();
// Update cache
_cache['server_config_count'] = configs.length; _cache['server_config_count'] = configs.length;
_cacheTimestamps['server_config_count'] = DateTime.now(); _cacheTimestamps['server_config_count'] = DateTime.now();
return configs; return configs;
} catch (e) { } catch (error) {
DebugLogger.log( DebugLogger.log(
'Failed to retrieve server configs: $e', 'Failed to retrieve server configs: $error',
scope: 'storage/optimized', scope: 'storage/optimized',
); );
return []; return const [];
} }
} }
/// Active Server Management
Future<void> setActiveServerId(String? serverId) async { Future<void> setActiveServerId(String? serverId) async {
if (serverId != null) { if (serverId != null) {
await _prefs.setString(_activeServerIdKey, serverId); await _preferencesBox.put(_activeServerIdKey, serverId);
} else { } else {
await _prefs.remove(_activeServerIdKey); await _preferencesBox.delete(_activeServerIdKey);
} }
// Update cache
_cache[_activeServerIdKey] = serverId; _cache[_activeServerIdKey] = serverId;
_cacheTimestamps[_activeServerIdKey] = DateTime.now(); _cacheTimestamps[_activeServerIdKey] = DateTime.now();
} }
Future<String?> getActiveServerId() async { Future<String?> getActiveServerId() async {
// Check cache first
if (_isCacheValid(_activeServerIdKey)) { if (_isCacheValid(_activeServerIdKey)) {
return _cache[_activeServerIdKey] as String?; return _cache[_activeServerIdKey] as String?;
} }
final serverId = _preferencesBox.get(_activeServerIdKey) as String?;
final serverId = _prefs.getString(_activeServerIdKey);
_cache[_activeServerIdKey] = serverId; _cache[_activeServerIdKey] = serverId;
_cacheTimestamps[_activeServerIdKey] = DateTime.now(); _cacheTimestamps[_activeServerIdKey] = DateTime.now();
return serverId; return serverId;
} }
/// Theme Management
String? getThemeMode() { String? getThemeMode() {
return _prefs.getString(_themeModeKey); return _preferencesBox.get(_themeModeKey) as String?;
} }
Future<void> setThemeMode(String mode) async { Future<void> setThemeMode(String mode) async {
await _prefs.setString(_themeModeKey, mode); await _preferencesBox.put(_themeModeKey, mode);
} }
/// Locale Management
String? getLocaleCode() { String? getLocaleCode() {
// Returns a locale code like 'en', 'de', 'fr', 'it'. Null means system. return _preferencesBox.get(_localeCodeKey) as String?;
return _prefs.getString(_localeCodeKey);
} }
Future<void> setLocaleCode(String? code) async { Future<void> setLocaleCode(String? code) async {
if (code == null || code.isEmpty) { if (code == null || code.isEmpty) {
await _prefs.remove(_localeCodeKey); await _preferencesBox.delete(_localeCodeKey);
} else { } else {
await _prefs.setString(_localeCodeKey, code); await _preferencesBox.put(_localeCodeKey, code);
} }
} }
/// Onboarding
Future<bool> getOnboardingSeen() async { Future<bool> getOnboardingSeen() async {
return _prefs.getBool(_onboardingSeenKey) ?? false; return (_preferencesBox.get(_onboardingSeenKey) as bool?) ?? false;
} }
Future<void> setOnboardingSeen(bool seen) async { Future<void> setOnboardingSeen(bool seen) async {
await _prefs.setBool(_onboardingSeenKey, seen); await _preferencesBox.put(_onboardingSeenKey, seen);
} }
/// Reviewer mode (persisted)
Future<bool> getReviewerMode() async { Future<bool> getReviewerMode() async {
return _prefs.getBool(_reviewerModeKey) ?? false; return (_preferencesBox.get(_reviewerModeKey) as bool?) ?? false;
} }
Future<void> setReviewerMode(bool enabled) async { 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 { Future<List<Conversation>> getLocalConversations() async {
try { try {
final jsonString = _prefs.getString(_localConversationsKey); final stored = _cachesBox.get(_localConversationsKey);
if (jsonString == null || jsonString.isEmpty) return []; if (stored == null) {
return const [];
final decoded = jsonDecode(jsonString) as List<dynamic>; }
return decoded.map((item) => Conversation.fromJson(item)).toList(); if (stored is String) {
} catch (e) { final decoded = jsonDecode(stored) as List<dynamic>;
DebugLogger.log( return decoded.map((item) => Conversation.fromJson(item)).toList();
'Failed to retrieve local conversations: $e', }
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', scope: 'storage/optimized',
error: error,
stackTrace: stack,
); );
return []; return const [];
} }
} }
Future<void> saveLocalConversations(List<Conversation> conversations) async { Future<void> saveLocalConversations(List<Conversation> conversations) async {
try { try {
// Only save essential data to reduce storage size final serialized = conversations
final lightweightConversations = conversations .map((conversation) => conversation.toJson())
.map(
(conv) => {
'id': conv.id,
'title': conv.title,
'updatedAt': conv.updatedAt.toIso8601String(),
'messageCount': conv.messages.length,
// Don't save full message content locally
},
)
.toList(); .toList();
await _cachesBox.put(_localConversationsKey, serialized);
final jsonString = jsonEncode(lightweightConversations);
await _prefs.setString(_localConversationsKey, jsonString);
DebugLogger.log( DebugLogger.log(
'Saved ${conversations.length} local conversations (lightweight)', 'Saved ${conversations.length} local conversations',
scope: 'storage/optimized', scope: 'storage/optimized',
); );
} catch (e) { } catch (error, stack) {
DebugLogger.log( DebugLogger.error(
'Failed to save local conversations: $e', 'Failed to save local conversations',
scope: 'storage/optimized', scope: 'storage/optimized',
error: error,
stackTrace: stack,
); );
} }
} }
/// Batch Operations for Performance // ---------------------------------------------------------------------------
// Batch operations
// ---------------------------------------------------------------------------
Future<void> clearAuthData() async { Future<void> clearAuthData() async {
try { try {
// Clear auth-related data in batch
await Future.wait([ await Future.wait([
deleteAuthToken(), deleteAuthToken(),
deleteSavedCredentials(), deleteSavedCredentials(),
_prefs.remove(_rememberCredentialsKey), _preferencesBox.delete(_rememberCredentialsKey),
_prefs.remove(_activeServerIdKey), _preferencesBox.delete(_activeServerIdKey),
]); ]);
// Clear related cache entries
_cache.removeWhere( _cache.removeWhere(
(key, value) => (key, _) =>
key.contains('auth') || key.contains('auth') ||
key.contains('credentials') || key.contains('credentials') ||
key.contains('server'), key.contains('server'),
); );
_cacheTimestamps.removeWhere( _cacheTimestamps.removeWhere(
(key, value) => (key, _) =>
key.contains('auth') || key.contains('auth') ||
key.contains('credentials') || key.contains('credentials') ||
key.contains('server'), key.contains('server'),
@@ -376,9 +368,9 @@ class OptimizedStorageService {
'Auth data cleared in batch operation', 'Auth data cleared in batch operation',
scope: 'storage/optimized', scope: 'storage/optimized',
); );
} catch (e) { } catch (error) {
DebugLogger.log( DebugLogger.log(
'Failed to clear auth data: $e', 'Failed to clear auth data: $error',
scope: 'storage/optimized', scope: 'storage/optimized',
); );
} }
@@ -386,30 +378,48 @@ class OptimizedStorageService {
Future<void> clearAll() async { Future<void> clearAll() async {
try { try {
await Future.wait([_secureCredentialStorage.clearAll(), _prefs.clear()]); await Future.wait([
_secureCredentialStorage.clearAll(),
_preferencesBox.clear(),
_cachesBox.clear(),
_attachmentQueueBox.clear(),
]);
_cache.clear(); _cache.clear();
_cacheTimestamps.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'); DebugLogger.log('All storage cleared', scope: 'storage/optimized');
} catch (e) { } catch (error) {
DebugLogger.log( DebugLogger.log(
'Failed to clear all storage: $e', 'Failed to clear all storage: $error',
scope: 'storage/optimized', scope: 'storage/optimized',
); );
} }
} }
/// Storage Health Check
Future<bool> isSecureStorageAvailable() async { Future<bool> isSecureStorageAvailable() async {
return await _secureCredentialStorage.isSecureStorageAvailable(); return _secureCredentialStorage.isSecureStorageAvailable();
} }
/// Cache Management Utilities // ---------------------------------------------------------------------------
// Cache helpers
// ---------------------------------------------------------------------------
bool _isCacheValid(String key) { bool _isCacheValid(String key) {
final timestamp = _cacheTimestamps[key]; final timestamp = _cacheTimestamps[key];
if (timestamp == null) return false; if (timestamp == null) {
return false;
}
return DateTime.now().difference(timestamp) < _cacheTimeout; return DateTime.now().difference(timestamp) < _cacheTimeout;
} }
@@ -419,37 +429,33 @@ class OptimizedStorageService {
DebugLogger.log('Storage cache cleared', scope: 'storage/optimized'); 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 { Future<void> migrateFromLegacyStorage() async {
try { try {
DebugLogger.log( DebugLogger.log(
'Starting migration from legacy storage', 'Starting migration from legacy storage',
scope: 'storage/optimized', 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( DebugLogger.log(
'Legacy storage migration completed', 'Legacy storage migration completed',
scope: 'storage/optimized', scope: 'storage/optimized',
); );
} catch (e) { } catch (error) {
DebugLogger.log( DebugLogger.log(
'Legacy storage migration failed: $e', 'Legacy storage migration failed: $error',
scope: 'storage/optimized', scope: 'storage/optimized',
); );
} }
} }
/// Performance Monitoring
Map<String, dynamic> getStorageStats() { Map<String, dynamic> getStorageStats() {
return { return {
'cacheSize': _cache.length, 'cacheSize': _cache.length,
'cachedKeys': _cache.keys.toList(), 'cachedKeys': _cache.keys.toList(),
'lastAccess': _cacheTimestamps.entries 'lastAccess': _cacheTimestamps.entries
.map((e) => '${e.key}: ${e.value}') .map((entry) => '${entry.key}: ${entry.value}')
.toList(), .toList(),
}; };
} }

View File

@@ -1,246 +1,260 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.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'; import 'animation_service.dart';
part 'settings_service.g.dart'; part 'settings_service.g.dart';
/// Service for managing app-wide settings including accessibility preferences /// Service for managing app-wide settings including accessibility preferences
class SettingsService { class SettingsService {
static const String _reduceMotionKey = 'reduce_motion'; static const String _reduceMotionKey = PreferenceKeys.reduceMotion;
static const String _animationSpeedKey = 'animation_speed'; static const String _animationSpeedKey = PreferenceKeys.animationSpeed;
static const String _hapticFeedbackKey = 'haptic_feedback'; static const String _hapticFeedbackKey = PreferenceKeys.hapticFeedback;
static const String _highContrastKey = 'high_contrast'; static const String _highContrastKey = PreferenceKeys.highContrast;
static const String _largeTextKey = 'large_text'; static const String _largeTextKey = PreferenceKeys.largeText;
static const String _darkModeKey = 'dark_mode'; static const String _darkModeKey = PreferenceKeys.darkMode;
static const String _defaultModelKey = 'default_model'; static const String _defaultModelKey = PreferenceKeys.defaultModel;
// Model name formatting // Model name formatting
static const String _omitProviderInModelNameKey = static const String _omitProviderInModelNameKey =
'omit_provider_in_model_name'; PreferenceKeys.omitProviderInModelName;
// Voice input settings // Voice input settings
static const String _voiceLocaleKey = 'voice_locale_id'; static const String _voiceLocaleKey = PreferenceKeys.voiceLocaleId;
static const String _voiceHoldToTalkKey = 'voice_hold_to_talk'; static const String _voiceHoldToTalkKey = PreferenceKeys.voiceHoldToTalk;
static const String _voiceAutoSendKey = 'voice_auto_send_final'; static const String _voiceAutoSendKey = PreferenceKeys.voiceAutoSendFinal;
// Realtime transport preference // Realtime transport preference
static const String _socketTransportModeKey = static const String _socketTransportModeKey =
'socket_transport_mode'; // 'auto' or 'ws' PreferenceKeys.socketTransportMode; // 'auto' or 'ws'
// Quick pill visibility selections (max 2) // Quick pill visibility selections (max 2)
static const String _quickPillsKey = static const String _quickPillsKey = PreferenceKeys
'quick_pills'; // StringList of identifiers e.g. ['web','image','tools'] .quickPills; // StringList of identifiers e.g. ['web','image','tools']
// Chat input behavior // 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 /// Get reduced motion preference
static Future<bool> getReduceMotion() async { static Future<bool> getReduceMotion() {
final prefs = await SharedPreferences.getInstance(); final value = _preferencesBox().get(_reduceMotionKey) as bool?;
return prefs.getBool(_reduceMotionKey) ?? false; return Future.value(value ?? false);
} }
/// Set reduced motion preference /// Set reduced motion preference
static Future<void> setReduceMotion(bool value) async { static Future<void> setReduceMotion(bool value) {
final prefs = await SharedPreferences.getInstance(); return _preferencesBox().put(_reduceMotionKey, value);
await prefs.setBool(_reduceMotionKey, value);
} }
/// Get animation speed multiplier (0.5 - 2.0) /// Get animation speed multiplier (0.5 - 2.0)
static Future<double> getAnimationSpeed() async { static Future<double> getAnimationSpeed() {
final prefs = await SharedPreferences.getInstance(); final value = _preferencesBox().get(_animationSpeedKey) as num?;
return prefs.getDouble(_animationSpeedKey) ?? 1.0; return Future.value((value?.toDouble() ?? 1.0).clamp(0.5, 2.0));
} }
/// Set animation speed multiplier /// Set animation speed multiplier
static Future<void> setAnimationSpeed(double value) async { static Future<void> setAnimationSpeed(double value) {
final prefs = await SharedPreferences.getInstance(); final sanitized = value.clamp(0.5, 2.0).toDouble();
await prefs.setDouble(_animationSpeedKey, value.clamp(0.5, 2.0)); return _preferencesBox().put(_animationSpeedKey, sanitized);
} }
/// Get haptic feedback preference /// Get haptic feedback preference
static Future<bool> getHapticFeedback() async { static Future<bool> getHapticFeedback() {
final prefs = await SharedPreferences.getInstance(); final value = _preferencesBox().get(_hapticFeedbackKey) as bool?;
return prefs.getBool(_hapticFeedbackKey) ?? true; return Future.value(value ?? true);
} }
/// Set haptic feedback preference /// Set haptic feedback preference
static Future<void> setHapticFeedback(bool value) async { static Future<void> setHapticFeedback(bool value) {
final prefs = await SharedPreferences.getInstance(); return _preferencesBox().put(_hapticFeedbackKey, value);
await prefs.setBool(_hapticFeedbackKey, value);
} }
/// Get high contrast preference /// Get high contrast preference
static Future<bool> getHighContrast() async { static Future<bool> getHighContrast() {
final prefs = await SharedPreferences.getInstance(); final value = _preferencesBox().get(_highContrastKey) as bool?;
return prefs.getBool(_highContrastKey) ?? false; return Future.value(value ?? false);
} }
/// Set high contrast preference /// Set high contrast preference
static Future<void> setHighContrast(bool value) async { static Future<void> setHighContrast(bool value) {
final prefs = await SharedPreferences.getInstance(); return _preferencesBox().put(_highContrastKey, value);
await prefs.setBool(_highContrastKey, value);
} }
/// Get large text preference /// Get large text preference
static Future<bool> getLargeText() async { static Future<bool> getLargeText() {
final prefs = await SharedPreferences.getInstance(); final value = _preferencesBox().get(_largeTextKey) as bool?;
return prefs.getBool(_largeTextKey) ?? false; return Future.value(value ?? false);
} }
/// Set large text preference /// Set large text preference
static Future<void> setLargeText(bool value) async { static Future<void> setLargeText(bool value) {
final prefs = await SharedPreferences.getInstance(); return _preferencesBox().put(_largeTextKey, value);
await prefs.setBool(_largeTextKey, value);
} }
/// Get dark mode preference /// Get dark mode preference
static Future<bool> getDarkMode() async { static Future<bool> getDarkMode() {
final prefs = await SharedPreferences.getInstance(); final value = _preferencesBox().get(_darkModeKey) as bool?;
return prefs.getBool(_darkModeKey) ?? true; // Default to dark return Future.value(value ?? true);
} }
/// Set dark mode preference /// Set dark mode preference
static Future<void> setDarkMode(bool value) async { static Future<void> setDarkMode(bool value) {
final prefs = await SharedPreferences.getInstance(); return _preferencesBox().put(_darkModeKey, value);
await prefs.setBool(_darkModeKey, value);
} }
/// Get default model preference /// Get default model preference
static Future<String?> getDefaultModel() async { static Future<String?> getDefaultModel() {
final prefs = await SharedPreferences.getInstance(); final value = _preferencesBox().get(_defaultModelKey) as String?;
return prefs.getString(_defaultModelKey); return Future.value(value);
} }
/// Set default model preference /// Set default model preference
static Future<void> setDefaultModel(String? modelId) async { static Future<void> setDefaultModel(String? modelId) {
final prefs = await SharedPreferences.getInstance(); final box = _preferencesBox();
if (modelId != null) { if (modelId != null) {
await prefs.setString(_defaultModelKey, modelId); return box.put(_defaultModelKey, modelId);
} else {
await prefs.remove(_defaultModelKey);
} }
return box.delete(_defaultModelKey);
} }
/// Whether to omit the provider prefix when displaying model names /// Whether to omit the provider prefix when displaying model names
static Future<bool> getOmitProviderInModelName() async { static Future<bool> getOmitProviderInModelName() {
final prefs = await SharedPreferences.getInstance(); final value = _preferencesBox().get(_omitProviderInModelNameKey) as bool?;
return prefs.getBool(_omitProviderInModelNameKey) ?? true; // default: omit return Future.value(value ?? true);
} }
static Future<void> setOmitProviderInModelName(bool value) async { static Future<void> setOmitProviderInModelName(bool value) {
final prefs = await SharedPreferences.getInstance(); return _preferencesBox().put(_omitProviderInModelNameKey, value);
await prefs.setBool(_omitProviderInModelNameKey, value);
} }
/// Load all settings /// Load all settings
static Future<AppSettings> loadSettings() async { static Future<AppSettings> loadSettings() {
return AppSettings( final box = _preferencesBox();
reduceMotion: await getReduceMotion(), return Future.value(
animationSpeed: await getAnimationSpeed(), AppSettings(
hapticFeedback: await getHapticFeedback(), reduceMotion: (box.get(_reduceMotionKey) as bool?) ?? false,
highContrast: await getHighContrast(), animationSpeed:
largeText: await getLargeText(), (box.get(_animationSpeedKey) as num?)?.toDouble() ?? 1.0,
darkMode: await getDarkMode(), hapticFeedback: (box.get(_hapticFeedbackKey) as bool?) ?? true,
defaultModel: await getDefaultModel(), highContrast: (box.get(_highContrastKey) as bool?) ?? false,
omitProviderInModelName: await getOmitProviderInModelName(), largeText: (box.get(_largeTextKey) as bool?) ?? false,
voiceLocaleId: await getVoiceLocaleId(), darkMode: (box.get(_darkModeKey) as bool?) ?? true,
voiceHoldToTalk: await getVoiceHoldToTalk(), defaultModel: box.get(_defaultModelKey) as String?,
voiceAutoSendFinal: await getVoiceAutoSendFinal(), omitProviderInModelName:
socketTransportMode: await getSocketTransportMode(), (box.get(_omitProviderInModelNameKey) as bool?) ?? true,
quickPills: await getQuickPills(), voiceLocaleId: box.get(_voiceLocaleKey) as String?,
sendOnEnter: await getSendOnEnter(), 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 /// Save all settings
static Future<void> saveSettings(AppSettings settings) async { static Future<void> saveSettings(AppSettings settings) async {
await Future.wait([ final box = _preferencesBox();
setReduceMotion(settings.reduceMotion), final updates = <String, Object?>{
setAnimationSpeed(settings.animationSpeed), _reduceMotionKey: settings.reduceMotion,
setHapticFeedback(settings.hapticFeedback), _animationSpeedKey: settings.animationSpeed,
setHighContrast(settings.highContrast), _hapticFeedbackKey: settings.hapticFeedback,
setLargeText(settings.largeText), _highContrastKey: settings.highContrast,
setDarkMode(settings.darkMode), _largeTextKey: settings.largeText,
setDefaultModel(settings.defaultModel), _darkModeKey: settings.darkMode,
setOmitProviderInModelName(settings.omitProviderInModelName), _omitProviderInModelNameKey: settings.omitProviderInModelName,
setVoiceLocaleId(settings.voiceLocaleId), _voiceHoldToTalkKey: settings.voiceHoldToTalk,
setVoiceHoldToTalk(settings.voiceHoldToTalk), _voiceAutoSendKey: settings.voiceAutoSendFinal,
setVoiceAutoSendFinal(settings.voiceAutoSendFinal), _socketTransportModeKey: settings.socketTransportMode,
setSocketTransportMode(settings.socketTransportMode), _quickPillsKey: settings.quickPills.take(2).toList(),
setQuickPills(settings.quickPills), _sendOnEnterKey: settings.sendOnEnter,
setSendOnEnter(settings.sendOnEnter), };
]);
}
// Voice input specific settings await box.putAll(updates);
static Future<String?> getVoiceLocaleId() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(_voiceLocaleKey);
}
static Future<void> setVoiceLocaleId(String? localeId) async { if (settings.defaultModel != null) {
final prefs = await SharedPreferences.getInstance(); await box.put(_defaultModelKey, settings.defaultModel);
if (localeId == null || localeId.isEmpty) {
await prefs.remove(_voiceLocaleKey);
} else { } 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 { // Voice input specific settings
final prefs = await SharedPreferences.getInstance(); static Future<String?> getVoiceLocaleId() {
return prefs.getBool(_voiceHoldToTalkKey) ?? false; final value = _preferencesBox().get(_voiceLocaleKey) as String?;
return Future.value(value);
} }
static Future<void> setVoiceHoldToTalk(bool value) async { static Future<void> setVoiceLocaleId(String? localeId) {
final prefs = await SharedPreferences.getInstance(); final box = _preferencesBox();
await prefs.setBool(_voiceHoldToTalkKey, value); if (localeId == null || localeId.isEmpty) {
return box.delete(_voiceLocaleKey);
}
return box.put(_voiceLocaleKey, localeId);
} }
static Future<bool> getVoiceAutoSendFinal() async { static Future<bool> getVoiceHoldToTalk() {
final prefs = await SharedPreferences.getInstance(); final value = _preferencesBox().get(_voiceHoldToTalkKey) as bool?;
return prefs.getBool(_voiceAutoSendKey) ?? false; return Future.value(value ?? false);
} }
static Future<void> setVoiceAutoSendFinal(bool value) async { static Future<void> setVoiceHoldToTalk(bool value) {
final prefs = await SharedPreferences.getInstance(); return _preferencesBox().put(_voiceHoldToTalkKey, value);
await prefs.setBool(_voiceAutoSendKey, 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) /// Transport mode: 'auto' (polling+websocket) or 'ws' (websocket only)
static Future<String> getSocketTransportMode() async { static Future<String> getSocketTransportMode() {
final prefs = await SharedPreferences.getInstance(); final value = _preferencesBox().get(_socketTransportModeKey) as String?;
return prefs.getString(_socketTransportModeKey) ?? 'ws'; return Future.value(value ?? 'ws');
} }
static Future<void> setSocketTransportMode(String mode) async { static Future<void> setSocketTransportMode(String mode) {
final prefs = await SharedPreferences.getInstance(); if (mode != 'auto' && mode != 'ws') {
if (mode != 'auto' && mode != 'ws') mode = 'auto'; mode = 'auto';
await prefs.setString(_socketTransportModeKey, mode); }
return _preferencesBox().put(_socketTransportModeKey, mode);
} }
// Quick Pills (visibility) // Quick Pills (visibility)
static Future<List<String>> getQuickPills() async { static Future<List<String>> getQuickPills() {
final prefs = await SharedPreferences.getInstance(); final stored = _preferencesBox().get(_quickPillsKey) as List<dynamic>?;
final list = prefs.getStringList(_quickPillsKey); if (stored == null) {
// Default: none selected return Future.value(const []);
if (list == null) return const []; }
// Enforce max 2; accept arbitrary tool IDs plus 'web' and 'image' return Future.value(List<String>.from(stored.take(2)));
return list.take(2).toList();
} }
static Future<void> setQuickPills(List<String> pills) async { static Future<void> setQuickPills(List<String> pills) {
final prefs = await SharedPreferences.getInstance(); return _preferencesBox().put(_quickPillsKey, pills.take(2).toList());
await prefs.setStringList(_quickPillsKey, pills.take(2).toList());
} }
// Chat input behavior // Chat input behavior
static Future<bool> getSendOnEnter() async { static Future<bool> getSendOnEnter() {
final prefs = await SharedPreferences.getInstance(); final value = _preferencesBox().get(_sendOnEnterKey) as bool?;
return prefs.getBool(_sendOnEnterKey) ?? false; return Future.value(value ?? false);
} }
static Future<void> setSendOnEnter(bool value) async { static Future<void> setSendOnEnter(bool value) {
final prefs = await SharedPreferences.getInstance(); return _preferencesBox().put(_sendOnEnterKey, value);
await prefs.setBool(_sendOnEnterKey, value);
} }
/// Get effective animation duration considering all settings /// Get effective animation duration considering all settings

View File

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

View File

@@ -5,9 +5,11 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'core/widgets/error_boundary.dart'; import 'core/widgets/error_boundary.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.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/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 'core/router/app_router.dart';
import 'shared/theme/app_theme.dart'; import 'shared/theme/app_theme.dart';
import 'shared/widgets/offline_indicator.dart'; import 'shared/widgets/offline_indicator.dart';
@@ -62,8 +64,6 @@ void main() {
_startupTimeline?.instant('edge_to_edge_enabled'); _startupTimeline?.instant('edge_to_edge_enabled');
}); });
final sharedPrefs = await SharedPreferences.getInstance();
_startupTimeline!.instant('shared_prefs_ready');
const secureStorage = FlutterSecureStorage( const secureStorage = FlutterSecureStorage(
aOptions: AndroidOptions( aOptions: AndroidOptions(
encryptedSharedPreferences: true, encryptedSharedPreferences: true,
@@ -78,6 +78,13 @@ void main() {
); );
_startupTimeline!.instant('secure_storage_ready'); _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 // Finish timeline after first frame paints
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_startupTimeline?.instant('first_frame_rendered'); _startupTimeline?.instant('first_frame_rendered');
@@ -88,8 +95,8 @@ void main() {
runApp( runApp(
ProviderScope( ProviderScope(
overrides: [ overrides: [
sharedPreferencesProvider.overrideWithValue(sharedPrefs),
secureStorageProvider.overrideWithValue(secureStorage), secureStorageProvider.overrideWithValue(secureStorage),
hiveBoxesProvider.overrideWithValue(hiveBoxes),
], ],
child: const ConduitApp(), child: const ConduitApp(),
), ),

View File

@@ -4,7 +4,8 @@ import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:uuid/uuid.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 'outbound_task.dart';
import 'task_worker.dart'; import 'task_worker.dart';
import '../../../core/utils/debug_logger.dart'; import '../../../core/utils/debug_logger.dart';
@@ -15,7 +16,7 @@ final taskQueueProvider =
); );
class TaskQueueNotifier extends Notifier<List<OutboundTask>> { class TaskQueueNotifier extends Notifier<List<OutboundTask>> {
static const _prefsKey = 'outbound_task_queue_v1'; static const _storageKey = HiveStoreKeys.taskQueue;
final _uuid = const Uuid(); final _uuid = const Uuid();
bool _bootstrapScheduled = false; bool _bootstrapScheduled = false;
@@ -34,10 +35,20 @@ class TaskQueueNotifier extends Notifier<List<OutboundTask>> {
Future<void> _load() async { Future<void> _load() async {
try { try {
final prefs = ref.read(sharedPreferencesProvider); final boxes = ref.read(hiveBoxesProvider);
final jsonStr = prefs.getString(_prefsKey); final stored = boxes.caches.get(_storageKey);
if (jsonStr == null || jsonStr.isEmpty) return; if (stored == null) return;
final raw = (jsonDecode(jsonStr) as List).cast<Map<String, dynamic>>();
List<Map<String, dynamic>> raw;
if (stored is String && stored.isNotEmpty) {
raw = (jsonDecode(stored) as List).cast<Map<String, dynamic>>();
} else if (stored is List) {
raw = stored
.map((entry) => Map<String, dynamic>.from(entry as Map))
.toList(growable: false);
} else {
return;
}
final tasks = raw.map(OutboundTask.fromJson).toList(); final tasks = raw.map(OutboundTask.fromJson).toList();
// Only restore non-completed tasks // Only restore non-completed tasks
state = tasks state = tasks
@@ -62,7 +73,7 @@ class TaskQueueNotifier extends Notifier<List<OutboundTask>> {
Future<void> _save() async { Future<void> _save() async {
try { try {
final prefs = ref.read(sharedPreferencesProvider); final boxes = ref.read(hiveBoxesProvider);
final retained = [ final retained = [
for (final task in state) for (final task in state)
if (task.status == TaskStatus.queued || if (task.status == TaskStatus.queued ||
@@ -76,7 +87,7 @@ class TaskQueueNotifier extends Notifier<List<OutboundTask>> {
} }
final raw = retained.map((t) => t.toJson()).toList(growable: false); final raw = retained.map((t) => t.toJson()).toList(growable: false);
await prefs.setString(_prefsKey, jsonEncode(raw)); await boxes.caches.put(_storageKey, raw);
} catch (e) { } catch (e) {
DebugLogger.log('Failed to persist task queue: $e', scope: 'tasks/queue'); DebugLogger.log('Failed to persist task queue: $e', scope: 'tasks/queue');
} }

View File

@@ -597,6 +597,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.2" 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: hotreloader:
dependency: transitive dependency: transitive
description: description:
@@ -725,6 +741,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.5" 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: js:
dependency: transitive dependency: transitive
description: description:

View File

@@ -25,6 +25,8 @@ dependencies:
# Storage # Storage
flutter_secure_storage: ^9.2.2 flutter_secure_storage: ^9.2.2
hive_ce: ^2.14.0
hive_ce_flutter: ^2.3.2
shared_preferences: ^2.3.2 shared_preferences: ^2.3.2
# UI Components - Enhanced Markdown # UI Components - Enhanced Markdown