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; static bool _migrationComplete = false; final HiveBoxes _boxes; Future migrateIfNeeded() async { // Fast path: if we already checked migration in this app session, skip if (_migrationComplete) { return; } final currentVersion = _boxes.metadata.get(HiveStoreKeys.migrationVersion) as int?; if (currentVersion != null && currentVersion >= _targetVersion) { _migrationComplete = true; 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); _migrationComplete = true; await _cleanupLegacyKeys(prefs); DebugLogger.log('Migration completed', scope: 'persistence/migration'); } catch (error, stack) { DebugLogger.error( 'Migration failed', scope: 'persistence/migration', error: error, stackTrace: stack, ); } } Future _migratePreferences(SharedPreferences prefs) async { final updates = {}; void copyBool(String key) { final value = prefs.getBool(key); if (value != null) updates[key] = value; } void copyDouble(String key) { final value = prefs.getDouble(key); if (value != null) updates[key] = value; } void copyString(String key) { final value = prefs.getString(key); if (value != null && value.isNotEmpty) updates[key] = value; } void copyStringList(String key) { final value = prefs.getStringList(key); if (value != null && value.isNotEmpty) { updates[key] = List.from(value); } } copyBool(PreferenceKeys.reduceMotion); copyDouble(PreferenceKeys.animationSpeed); copyBool(PreferenceKeys.hapticFeedback); copyBool(PreferenceKeys.highContrast); copyBool(PreferenceKeys.largeText); copyBool(PreferenceKeys.darkMode); copyString(PreferenceKeys.defaultModel); 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.themePalette); copyString(PreferenceKeys.localeCode); copyBool(PreferenceKeys.onboardingSeen); copyBool(PreferenceKeys.reviewerMode); if (updates.isNotEmpty) { await _boxes.preferences.putAll(updates); } } Future _migrateCaches(SharedPreferences prefs) async { final jsonString = prefs.getString(HiveStoreKeys.localConversations); if (jsonString == null || jsonString.isEmpty) { return; } try { final decoded = jsonDecode(jsonString); if (decoded is List) { final list = decoded .map((entry) => Map.from(entry as Map)) .toList(growable: false); await _boxes.caches.put(HiveStoreKeys.localConversations, list); } } catch (error, stack) { DebugLogger.error( 'Failed to migrate local conversations', scope: 'persistence/migration', error: error, stackTrace: stack, ); } } Future _migrateAttachmentQueue(SharedPreferences prefs) async { final jsonString = prefs.getString( LegacyPreferenceKeys.attachmentUploadQueue, ); if (jsonString == null || jsonString.isEmpty) { return; } try { final decoded = jsonDecode(jsonString); if (decoded is List) { final list = decoded .map((entry) => Map.from(entry as Map)) .toList(growable: false); await _boxes.attachmentQueue.put( HiveStoreKeys.attachmentQueueEntries, list, ); } } catch (error, stack) { DebugLogger.error( 'Failed to migrate attachment queue', scope: 'persistence/migration', error: error, stackTrace: stack, ); } } Future _migrateTaskQueue(SharedPreferences prefs) async { final jsonString = prefs.getString(LegacyPreferenceKeys.taskQueue); if (jsonString == null || jsonString.isEmpty) { return; } try { final decoded = jsonDecode(jsonString); if (decoded is List) { final list = decoded .map((entry) => Map.from(entry as Map)) .toList(growable: false); await _boxes.caches.put(HiveStoreKeys.taskQueue, list); } } catch (error, stack) { DebugLogger.error( 'Failed to migrate outbound task queue', scope: 'persistence/migration', error: error, stackTrace: stack, ); } } Future _cleanupLegacyKeys(SharedPreferences prefs) async { final keysToRemove = [ PreferenceKeys.reduceMotion, PreferenceKeys.animationSpeed, PreferenceKeys.hapticFeedback, PreferenceKeys.highContrast, PreferenceKeys.largeText, PreferenceKeys.darkMode, PreferenceKeys.defaultModel, PreferenceKeys.voiceLocaleId, PreferenceKeys.voiceHoldToTalk, PreferenceKeys.voiceAutoSendFinal, PreferenceKeys.socketTransportMode, PreferenceKeys.quickPills, PreferenceKeys.sendOnEnterKey, PreferenceKeys.rememberCredentials, PreferenceKeys.activeServerId, PreferenceKeys.themeMode, PreferenceKeys.themePalette, PreferenceKeys.localeCode, PreferenceKeys.onboardingSeen, PreferenceKeys.reviewerMode, HiveStoreKeys.localConversations, HiveStoreKeys.attachmentQueueEntries, LegacyPreferenceKeys.attachmentUploadQueue, LegacyPreferenceKeys.taskQueue, ]; for (final key in keysToRemove) { await prefs.remove(key); } } }