import 'dart:async'; import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/constants/colors.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/domain/models/metadata_key.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; const int targetVersion = 26; Future migrateDatabaseIfNeeded(Drift drift) async { final int version = Store.get(StoreKey.version, targetVersion); if (version < 25) { await _migrateTo25(); } if (version < 26) { await _migrateTo26(drift); } await Store.put(StoreKey.version, targetVersion); return; } Future _migrateTo25() async { final accessToken = Store.tryGet(StoreKey.accessToken); if (accessToken == null || accessToken.isEmpty) return; final serverUrls = ApiService.getServerUrls(); if (serverUrls.isEmpty) return; await NetworkRepository.setHeaders(ApiService.getRequestHeaders(), serverUrls, token: accessToken); } Future _migrateTo26(Drift drift) async { final migrator = _StoreMigrator(drift); await migrator.migrateEnumName(StoreKey.legacyThemeMode, MetadataKey.themeMode, ThemeMode.values); await migrator.migrateEnumIndex(StoreKey.legacyLogLevel, MetadataKey.logLevel, LogLevel.values); await migrator.migrateEnumName(StoreKey.legacyPrimaryColor, MetadataKey.themePrimaryColor, ImmichColorPreset.values); await migrator.migrateBool(StoreKey.legacyDynamicTheme, MetadataKey.themeDynamic); await migrator.migrateBool(StoreKey.legacyColorfulInterface, MetadataKey.themeColorfulInterface); final cleanupKeepAlbumIds = await migrator.readLegacyStoreString(StoreKey.legacyCleanupKeepAlbumIds.id); if (cleanupKeepAlbumIds != null) { final ids = cleanupKeepAlbumIds.split(',').where((id) => id.isNotEmpty).toList(); await drift.metadataEntity.insertOnConflictUpdate( MetadataEntityCompanion.insert( key: MetadataKey.cleanupKeepAlbumIds.key, value: MetadataKey.cleanupKeepAlbumIds.encode(ids), updatedAt: Value(DateTime.now()), ), ); await migrator.deleteLegacyStoreRows([StoreKey.legacyCleanupKeepAlbumIds.id]); } await migrator.migrateBool(StoreKey.legacyCleanupKeepFavorites, MetadataKey.cleanupKeepFavorites); await migrator.migrateEnumIndex( StoreKey.legacyCleanupKeepMediaType, MetadataKey.cleanupKeepMediaType, AssetKeepType.values, ); await migrator.migrateInt(StoreKey.legacyCleanupCutoffDaysAgo, MetadataKey.cleanupCutoffDaysAgo); await migrator.migrateBool(StoreKey.legacyCleanupDefaultsInitialized, MetadataKey.cleanupDefaultsInitialized); await migrator.complete(); } class _StoreMigrator { final Drift _db; final Map, Object> _cache = {}; final List _migratedStoreIds = []; _StoreMigrator(this._db); Future migrateEnumIndex(StoreKey legacyKey, MetadataKey newKey, List values) async { final index = await readLegacyStoreInt(legacyKey.id); if (index == null) return; final enumValue = values.elementAtOrNull(index) ?? newKey.defaultValue; _cache[newKey] = enumValue; _migratedStoreIds.add(legacyKey.id); } Future migrateEnumName( StoreKey legacyKey, MetadataKey newKey, List values, ) async { final name = await readLegacyStoreString(legacyKey.id); if (name == null) return; final enumValue = values.firstWhere((e) => e.name == name, orElse: () => newKey.defaultValue); _cache[newKey] = enumValue; _migratedStoreIds.add(legacyKey.id); } Future migrateBool(StoreKey legacyKey, MetadataKey newKey) async { final intValue = await readLegacyStoreInt(legacyKey.id); if (intValue == null) return; final boolValue = intValue != 0; _cache[newKey] = boolValue; _migratedStoreIds.add(legacyKey.id); } Future migrateInt(StoreKey legacyKey, MetadataKey newKey) async { final intValue = await readLegacyStoreInt(legacyKey.id); if (intValue == null) return; _cache[newKey] = intValue; _migratedStoreIds.add(legacyKey.id); } Future complete() async { await _db.batch((batch) { for (final entry in _cache.entries) { batch.insert( _db.metadataEntity, MetadataEntityCompanion(key: Value(entry.key.key), value: Value(entry.key.encode(entry.value))), mode: InsertMode.insertOrReplace, ); } }); await deleteLegacyStoreRows(_migratedStoreIds); } Future readLegacyStoreString(int id) async { final row = await (_db.storeEntity.select()..where((t) => t.id.equals(id))).getSingleOrNull(); return row?.stringValue; } Future readLegacyStoreInt(int id) async { final row = await (_db.storeEntity.select()..where((t) => t.id.equals(id))).getSingleOrNull(); return row?.intValue; } Future deleteLegacyStoreRows(List ids) async { if (ids.isEmpty) return; await (_db.storeEntity.delete()..where((t) => t.id.isIn(ids))).go(); } }