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:
46
lib/core/persistence/hive_bootstrap.dart
Normal file
46
lib/core/persistence/hive_bootstrap.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
lib/core/persistence/hive_boxes.dart
Normal file
35
lib/core/persistence/hive_boxes.dart
Normal 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;
|
||||||
|
}
|
||||||
29
lib/core/persistence/persistence_keys.dart
Normal file
29
lib/core/persistence/persistence_keys.dart
Normal 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';
|
||||||
|
}
|
||||||
210
lib/core/persistence/persistence_migrator.dart
Normal file
210
lib/core/persistence/persistence_migrator.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
lib/core/persistence/persistence_providers.dart
Normal file
10
lib/core/persistence/persistence_providers.dart
Normal 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.');
|
||||||
@@ -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),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
24
pubspec.lock
24
pubspec.lock
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user