mirror of
https://github.com/immich-app/immich
synced 2026-08-22 13:13:05 +00:00
415 lines
15 KiB
Dart
415 lines
15 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
|
|
import 'package:collection/collection.dart';
|
|
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/app_metadata_key.dart';
|
|
import 'package:immich_mobile/domain/models/config/app_config.dart';
|
|
import 'package:immich_mobile/domain/models/log.model.dart';
|
|
import 'package:immich_mobile/domain/models/session.model.dart';
|
|
import 'package:immich_mobile/domain/models/settings_key.dart';
|
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
|
import 'package:immich_mobile/domain/services/feature_message.service.dart';
|
|
import 'package:immich_mobile/infrastructure/entities/app_metadata.entity.drift.dart';
|
|
import 'package:immich_mobile/infrastructure/entities/session.entity.drift.dart';
|
|
import 'package:immich_mobile/infrastructure/entities/settings.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/infrastructure/repositories/session.repository.dart';
|
|
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
|
|
import 'package:immich_mobile/infrastructure/store.dart';
|
|
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
|
|
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
|
|
|
Future<void> migrateDatabaseIfNeeded(Drift drift) async {
|
|
final isFreshInstall = await drift.appMetadataEntity
|
|
.count(where: (row) => row.key.equals(AppMetadataKey.version.name))
|
|
.map((count) => count == 0)
|
|
.getSingle();
|
|
final metadata = drift.appMetadataRepository;
|
|
final version = await metadata.get(.version);
|
|
|
|
if (version < 29) {
|
|
final legacyStore = await _readLegacyStore(drift);
|
|
|
|
if (version < 25) {
|
|
await _migrateTo25(drift, legacyStore);
|
|
}
|
|
if (version < 26) {
|
|
await _migrateTo26(drift, legacyStore);
|
|
}
|
|
if (version < 27) {
|
|
await _migrateTo27(drift, legacyStore);
|
|
}
|
|
if (version < 28) {
|
|
await _migrateTo28(drift, legacyStore);
|
|
}
|
|
await _migrateTo29(drift, legacyStore);
|
|
}
|
|
|
|
if (isFreshInstall) {
|
|
await FeatureMessageService(SettingsRepository.instance).markSeen();
|
|
}
|
|
|
|
await metadata.set(.version, kCurrentVersion);
|
|
return;
|
|
}
|
|
|
|
Future<Map<int, Object?>> _readLegacyStore(Drift drift) async {
|
|
final rows = await drift.storeEntity.select().get();
|
|
return {for (final row in rows) row.id: row.stringValue ?? row.intValue};
|
|
}
|
|
|
|
Future<void> _migrateTo25(Drift drift, Map<int, Object?> legacyStore) async {
|
|
final migrator = _StoreMigrator<SettingsKey>._(
|
|
drift,
|
|
legacyStore,
|
|
encode: (key, value) => key.encode(value),
|
|
readDefault: (key) => defaultConfig.read(key),
|
|
insertRow: (batch, name, value) => batch.insert(
|
|
drift.settingsEntity,
|
|
SettingsEntityCompanion(key: Value(name), value: Value(value)),
|
|
mode: InsertMode.insertOrReplace,
|
|
),
|
|
);
|
|
|
|
final accessToken = migrator.readLegacyStoreString(.legacyAccessToken);
|
|
if (accessToken == null || accessToken.isEmpty) {
|
|
return;
|
|
}
|
|
|
|
final urls = <String>[];
|
|
final serverEndpoint = migrator.readLegacyStoreString(.legacyServerEndpoint);
|
|
if (serverEndpoint != null && serverEndpoint.isNotEmpty) {
|
|
urls.add(serverEndpoint);
|
|
}
|
|
final localEndpoint = migrator.readLegacyStoreString(.legacyLocalEndpoint);
|
|
if (localEndpoint != null && localEndpoint.isNotEmpty) {
|
|
urls.add(localEndpoint);
|
|
}
|
|
final externalJson = migrator.readLegacyStoreString(.legacyExternalEndpointList);
|
|
if (externalJson != null) {
|
|
final List<dynamic> list = jsonDecode(externalJson);
|
|
for (final entry in list) {
|
|
final url = AuxilaryEndpoint.fromJson(entry).url;
|
|
if (url.isNotEmpty) {
|
|
urls.add(url);
|
|
}
|
|
}
|
|
}
|
|
if (urls.isEmpty) {
|
|
return;
|
|
}
|
|
|
|
final customHeadersStr = migrator.readLegacyStoreString(.legacyCustomHeaders) ?? "";
|
|
final headers = customHeadersStr.isEmpty
|
|
? const <String, String>{}
|
|
: (jsonDecode(customHeadersStr) as Map).cast<String, String>();
|
|
|
|
await NetworkRepository.setHeaders(headers, urls, token: accessToken);
|
|
}
|
|
|
|
Future<void> _migrateTo26(Drift drift, Map<int, Object?> legacyStore) async {
|
|
final migrator = _StoreMigrator<SettingsKey>._(
|
|
drift,
|
|
legacyStore,
|
|
encode: (key, value) => key.encode(value),
|
|
readDefault: (key) => defaultConfig.read(key),
|
|
insertRow: (batch, name, value) => batch.insert(
|
|
drift.settingsEntity,
|
|
SettingsEntityCompanion(key: Value(name), value: Value(value)),
|
|
mode: InsertMode.insertOrReplace,
|
|
),
|
|
);
|
|
await migrator.migrateEnumIndex(.legacyLogLevel, .logLevel, LogLevel.values);
|
|
// Theme
|
|
await migrator.migrateEnumName(.legacyThemeMode, .themeMode, ThemeMode.values);
|
|
await migrator.migrateEnumName(.legacyPrimaryColor, .themePrimaryColor, ImmichColorPreset.values);
|
|
await migrator.migrateBool(.legacyDynamicTheme, .themeDynamic);
|
|
await migrator.migrateBool(.legacyColorfulInterface, .themeColorfulInterface);
|
|
// Cleanup
|
|
final cleanupKeepAlbumIds = migrator.readLegacyStoreString(.legacyCleanupKeepAlbumIds);
|
|
if (cleanupKeepAlbumIds != null) {
|
|
final ids = cleanupKeepAlbumIds.split(',').where((id) => id.isNotEmpty).toList();
|
|
migrator.stage(.legacyCleanupKeepAlbumIds, .cleanupKeepAlbumIds, ids);
|
|
}
|
|
await migrator.migrateBool(.legacyCleanupKeepFavorites, .cleanupKeepFavorites);
|
|
await migrator.migrateEnumIndex(.legacyCleanupKeepMediaType, .cleanupKeepMediaType, AssetKeepType.values);
|
|
await migrator.migrateInt(.legacyCleanupCutoffDaysAgo, .cleanupCutoffDaysAgo);
|
|
await migrator.migrateBool(.legacyCleanupDefaultsInitialized, .cleanupDefaultsInitialized);
|
|
// Map
|
|
await migrator.migrateBool(.legacyMapShowFavoriteOnly, .mapShowFavoriteOnly);
|
|
await migrator.migrateInt(.legacyMapRelativeDate, .mapRelativeDate);
|
|
await migrator.migrateBool(.legacyMapIncludeArchived, .mapIncludeArchived);
|
|
await migrator.migrateEnumIndex(.legacyMapThemeMode, .mapThemeMode, ThemeMode.values);
|
|
await migrator.migrateBool(.legacyMapwithPartners, .mapWithPartners);
|
|
// Timeline
|
|
await migrator.migrateInt(.legacyTilesPerRow, .timelineTilesPerRow);
|
|
await migrator.migrateEnumIndex(.legacyGroupAssetsBy, .timelineGroupAssetsBy, GroupAssetsBy.values);
|
|
await migrator.migrateBool(.legacyStorageIndicator, .timelineStorageIndicator);
|
|
// Image
|
|
await migrator.migrateBool(.legacyPreferRemoteImage, .imagePreferRemote);
|
|
await migrator.migrateBool(.legacyLoadOriginal, .imageLoadOriginal);
|
|
// Viewer
|
|
await migrator.migrateBool(.legacyLoopVideo, .viewerLoopVideo);
|
|
await migrator.migrateBool(.legacyLoadOriginalVideo, .viewerLoadOriginalVideo);
|
|
await migrator.migrateBool(.legacyAutoPlayVideo, .viewerAutoPlayVideo);
|
|
await migrator.migrateBool(.legacyTapToNavigate, .viewerTapToNavigate);
|
|
// Network
|
|
await migrator.migrateBool(.legacyAutoEndpointSwitching, .networkAutoEndpointSwitching);
|
|
final preferredWifiName = migrator.readLegacyStoreString(.legacyPreferredWifiName);
|
|
migrator.stage(.legacyPreferredWifiName, .networkPreferredWifiName, preferredWifiName);
|
|
final localEndpoint = migrator.readLegacyStoreString(.legacyLocalEndpoint);
|
|
migrator.stage(.legacyLocalEndpoint, .networkLocalEndpoint, localEndpoint);
|
|
await _migrateExternalEndpointList(migrator);
|
|
await _migrateCustomHeaders(migrator);
|
|
// Album
|
|
await _migrateAlbumSortMode(migrator);
|
|
await migrator.migrateBool(.legacySelectedAlbumSortReverse, .albumIsReverse);
|
|
await migrator.migrateBool(.legacyAlbumGridView, .albumIsGrid);
|
|
// Backup
|
|
await migrator.migrateBool(.legacyEnableBackup, .backupEnabled);
|
|
await migrator.migrateBool(.legacyUseWifiForUploadVideos, .backupUseCellularForVideos);
|
|
await migrator.migrateBool(.legacyUseWifiForUploadPhotos, .backupUseCellularForPhotos);
|
|
await migrator.migrateBool(.legacyBackupRequireCharging, .backupRequireCharging);
|
|
await migrator.migrateInt(.legacyBackupTriggerDelay, .backupTriggerDelay);
|
|
await migrator.migrateBool(.legacySyncAlbums, .backupSyncAlbums);
|
|
await migrator.complete();
|
|
}
|
|
|
|
Future<void> _migrateTo27(Drift drift, Map<int, Object?> legacyStore) async {
|
|
final migrator = _StoreMigrator<SessionKey>._(
|
|
drift,
|
|
legacyStore,
|
|
encode: (key, value) => key.encode(value),
|
|
readDefault: (key) => defaultSession.read(key),
|
|
insertRow: (batch, name, value) => batch.insert(
|
|
drift.sessionEntity,
|
|
SessionEntityCompanion(key: Value(name), value: Value(value)),
|
|
mode: InsertMode.insertOrReplace,
|
|
),
|
|
);
|
|
await migrator.migrateString(.legacyServerUrl, .serverUrl);
|
|
await migrator.migrateString(.legacyAccessToken, .accessToken);
|
|
await migrator.migrateString(.legacyServerEndpoint, .serverEndpoint);
|
|
await migrator.complete();
|
|
|
|
await SessionRepository.instance.refresh();
|
|
}
|
|
|
|
Future<void> _migrateTo28(Drift drift, Map<int, Object?> legacyStore) async {
|
|
final migrator = _StoreMigrator<SettingsKey>._(
|
|
drift,
|
|
legacyStore,
|
|
encode: (key, value) => key.encode(value),
|
|
readDefault: (key) => defaultConfig.read(key),
|
|
insertRow: (batch, name, value) => batch.insert(
|
|
drift.settingsEntity,
|
|
SettingsEntityCompanion(key: Value(name), value: Value(value)),
|
|
mode: InsertMode.insertOrReplace,
|
|
),
|
|
);
|
|
await migrator.migrateBool(.legacyAdvancedTroubleshooting, .advancedTroubleshooting);
|
|
await migrator.migrateBool(.legacyEnableHapticFeedback, .advancedEnableHapticFeedback);
|
|
await migrator.migrateBool(.legacyReadonlyModeEnabled, .advancedReadonlyModeEnabled);
|
|
await migrator.complete();
|
|
|
|
await SettingsRepository.instance.refresh();
|
|
}
|
|
|
|
Future<void> _migrateTo29(Drift drift, Map<int, Object?> legacyStore) async {
|
|
final migrator = _StoreMigrator<AppMetadataKey>._(
|
|
drift,
|
|
legacyStore,
|
|
encode: (key, value) => key.encode(value),
|
|
readDefault: (_) => null,
|
|
insertRow: (batch, name, value) => batch.insert(
|
|
drift.appMetadataEntity,
|
|
AppMetadataEntityCompanion(key: Value(name), value: Value(value)),
|
|
mode: InsertMode.insertOrReplace,
|
|
),
|
|
);
|
|
|
|
final rawStatus = migrator.readLegacyStoreString(.legacySyncMigrationStatus);
|
|
if (rawStatus != null) {
|
|
final decoded = jsonDecode(rawStatus);
|
|
final migrations = decoded is List ? decoded.whereType<String>().toList() : <String>[];
|
|
migrator.stage(.legacySyncMigrationStatus, .syncMigrationStatus, migrations);
|
|
}
|
|
|
|
await migrator.migrateBool(.legacyManageLocalMediaAndroid, .manageLocalMediaAndroid);
|
|
await migrator.complete();
|
|
}
|
|
|
|
Future<void> _migrateAlbumSortMode(_StoreMigrator<SettingsKey> migrator) async {
|
|
final raw = migrator.readLegacyStoreInt(.legacySelectedAlbumSortOrder);
|
|
final mode = AlbumSortMode.values.firstWhereOrNull((e) => raw != null && e.storeIndex == raw);
|
|
if (mode == null) {
|
|
return;
|
|
}
|
|
|
|
migrator.stage(.legacySelectedAlbumSortOrder, .albumSortMode, mode);
|
|
}
|
|
|
|
Future<void> _migrateExternalEndpointList(_StoreMigrator<SettingsKey> migrator) async {
|
|
final raw = migrator.readLegacyStoreString(.legacyExternalEndpointList);
|
|
if (raw == null) {
|
|
return;
|
|
}
|
|
|
|
final urls = <String>[];
|
|
try {
|
|
final decoded = jsonDecode(raw);
|
|
if (decoded is List) {
|
|
for (final entry in decoded) {
|
|
final url = AuxilaryEndpoint.fromJson(entry).url;
|
|
if (url.isNotEmpty) {
|
|
urls.add(url);
|
|
}
|
|
}
|
|
}
|
|
} on FormatException {
|
|
// ignore invalid entries
|
|
}
|
|
|
|
migrator.stage(.legacyExternalEndpointList, SettingsKey.networkExternalEndpointList, urls);
|
|
}
|
|
|
|
Future<void> _migrateCustomHeaders(_StoreMigrator<SettingsKey> migrator) async {
|
|
final raw = migrator.readLegacyStoreString(.legacyCustomHeaders);
|
|
if (raw == null) {
|
|
return;
|
|
}
|
|
|
|
final headers = <String, String>{};
|
|
try {
|
|
final decoded = jsonDecode(raw);
|
|
if (decoded is Map) {
|
|
decoded.forEach((key, value) {
|
|
if (key is String && value is String) {
|
|
headers[key] = value;
|
|
}
|
|
});
|
|
}
|
|
} on FormatException {
|
|
// ignore invalid entries
|
|
}
|
|
|
|
migrator.stage(.legacyCustomHeaders, SettingsKey.networkCustomHeaders, headers);
|
|
}
|
|
|
|
class _StoreMigrator<K extends Enum> {
|
|
_StoreMigrator._(
|
|
this._db,
|
|
this._legacyStore, {
|
|
required this.encode,
|
|
required this.readDefault,
|
|
required this.insertRow,
|
|
});
|
|
|
|
final Drift _db;
|
|
final Map<int, Object?> _legacyStore;
|
|
final String Function(K key, Object value) encode;
|
|
final Object? Function(K key) readDefault;
|
|
final void Function(Batch batch, String name, String? value) insertRow;
|
|
final Map<K, Object?> _cache = {};
|
|
final List<int> _migratedStoreIds = [];
|
|
|
|
Future<void> migrateEnumIndex<T extends Enum>(LegacyStoreKey legacyKey, K newKey, List<T> values) async {
|
|
final index = readLegacyStoreInt(legacyKey);
|
|
if (index == null) {
|
|
return;
|
|
}
|
|
|
|
final enumValue = values.elementAtOrNull(index);
|
|
if (enumValue == null) {
|
|
return;
|
|
}
|
|
|
|
_cache[newKey] = enumValue;
|
|
_migratedStoreIds.add(legacyKey.id);
|
|
}
|
|
|
|
Future<void> migrateEnumName<T extends Enum>(LegacyStoreKey legacyKey, K newKey, List<T> values) async {
|
|
final name = readLegacyStoreString(legacyKey);
|
|
if (name == null) {
|
|
return;
|
|
}
|
|
|
|
final enumValue = values.firstWhereOrNull((e) => e.name == name);
|
|
if (enumValue == null) {
|
|
return;
|
|
}
|
|
|
|
_cache[newKey] = enumValue;
|
|
_migratedStoreIds.add(legacyKey.id);
|
|
}
|
|
|
|
Future<void> migrateBool(LegacyStoreKey legacyKey, K newKey) async {
|
|
final intValue = readLegacyStoreInt(legacyKey);
|
|
if (intValue == null) {
|
|
return;
|
|
}
|
|
|
|
_cache[newKey] = intValue != 0;
|
|
_migratedStoreIds.add(legacyKey.id);
|
|
}
|
|
|
|
Future<void> migrateInt(LegacyStoreKey legacyKey, K newKey) async {
|
|
final intValue = readLegacyStoreInt(legacyKey);
|
|
if (intValue == null) {
|
|
return;
|
|
}
|
|
|
|
_cache[newKey] = intValue;
|
|
_migratedStoreIds.add(legacyKey.id);
|
|
}
|
|
|
|
Future<void> migrateString(LegacyStoreKey legacyKey, K newKey) async {
|
|
final value = readLegacyStoreString(legacyKey);
|
|
if (value == null || value.isEmpty) {
|
|
return;
|
|
}
|
|
|
|
_cache[newKey] = value;
|
|
_migratedStoreIds.add(legacyKey.id);
|
|
}
|
|
|
|
Future<void> migrateNullableString(LegacyStoreKey legacyKey, K newKey) async {
|
|
_cache[newKey] = readLegacyStoreString(legacyKey);
|
|
_migratedStoreIds.add(legacyKey.id);
|
|
}
|
|
|
|
void stage(LegacyStoreKey legacyKey, K newKey, Object? value) {
|
|
_cache[newKey] = value;
|
|
_migratedStoreIds.add(legacyKey.id);
|
|
}
|
|
|
|
Future<void> complete() async {
|
|
await _db.batch((batch) {
|
|
for (final entry in _cache.entries) {
|
|
if (entry.value == readDefault(entry.key)) {
|
|
continue;
|
|
}
|
|
|
|
final value = entry.value;
|
|
insertRow(batch, entry.key.name, value == null ? null : encode(entry.key, value));
|
|
}
|
|
});
|
|
await deleteLegacyStoreRows(_migratedStoreIds);
|
|
}
|
|
|
|
String? readLegacyStoreString(LegacyStoreKey key) => _legacyStore[key.id] as String?;
|
|
|
|
int? readLegacyStoreInt(LegacyStoreKey key) => _legacyStore[key.id] as int?;
|
|
|
|
Future<void> deleteLegacyStoreRows(List<int> ids) async {
|
|
if (ids.isEmpty) {
|
|
return;
|
|
}
|
|
await (_db.storeEntity.delete()..where((t) => t.id.isIn(ids))).go();
|
|
}
|
|
}
|