merge main

This commit is contained in:
shenlong-tanwen
2025-09-17 16:08:34 +05:30
528 changed files with 17365 additions and 6390 deletions
+8 -1
View File
@@ -1,6 +1,8 @@
import 'package:flutter/widgets.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
@@ -15,7 +17,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_act
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
class ActionButtonContext {
final BaseAsset asset;
@@ -24,6 +25,7 @@ class ActionButtonContext {
final bool isTrashEnabled;
final bool isInLockedView;
final RemoteAlbum? currentAlbum;
final bool advancedTroubleshooting;
final ActionSource source;
const ActionButtonContext({
@@ -33,11 +35,13 @@ class ActionButtonContext {
required this.isTrashEnabled,
required this.isInLockedView,
required this.currentAlbum,
required this.advancedTroubleshooting,
required this.source,
});
}
enum ActionButtonType {
advancedInfo,
share,
shareLink,
archive,
@@ -55,6 +59,7 @@ enum ActionButtonType {
bool shouldShow(ActionButtonContext context) {
return switch (this) {
ActionButtonType.advancedInfo => context.advancedTroubleshooting,
ActionButtonType.share => true,
ActionButtonType.shareLink =>
!context.isInLockedView && //
@@ -115,6 +120,7 @@ enum ActionButtonType {
Widget buildButton(ActionButtonContext context) {
return switch (this) {
ActionButtonType.advancedInfo => AdvancedInfoActionButton(source: context.source),
ActionButtonType.share => ShareActionButton(source: context.source),
ActionButtonType.shareLink => ShareLinkActionButton(source: context.source),
ActionButtonType.archive => ArchiveActionButton(source: context.source),
@@ -138,6 +144,7 @@ enum ActionButtonType {
class ActionButtonBuilder {
static const List<ActionButtonType> _actionTypes = [
ActionButtonType.advancedInfo,
ActionButtonType.share,
ActionButtonType.shareLink,
ActionButtonType.likeActivity,
+9 -3
View File
@@ -89,11 +89,17 @@ abstract final class Bootstrap {
return (isar, drift, logDb);
}
static Future<void> initDomain(Isar db, Drift drift, DriftLogger logDb, {bool shouldBufferLogs = true}) async {
final isBeta = await IsarStoreRepository(db).tryGet(StoreKey.betaTimeline) ?? false;
static Future<void> initDomain(
Isar db,
Drift drift,
DriftLogger logDb, {
bool listenStoreUpdates = true,
bool shouldBufferLogs = true,
}) async {
final isBeta = await IsarStoreRepository(db).tryGet(StoreKey.betaTimeline) ?? true;
final IStoreRepository storeRepo = isBeta ? DriftStoreRepository(drift) : IsarStoreRepository(db);
await StoreService.init(storeRepository: storeRepo);
await StoreService.init(storeRepository: storeRepo, listenUpdates: listenStoreUpdates);
await LogService.init(
logRepository: LogRepository(logDb),
+8
View File
@@ -0,0 +1,8 @@
import 'package:flutter/foundation.dart';
@pragma('vm:prefer-inline')
void dPrint(String Function() message) {
if (kDebugMode) {
debugPrint(message());
}
}
+54 -45
View File
@@ -1,14 +1,15 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/services/log.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/utils/bootstrap.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:immich_mobile/utils/http_ssl_options.dart';
import 'package:logging/logging.dart';
import 'package:worker_manager/worker_manager.dart';
@@ -31,55 +32,63 @@ Cancelable<T?> runInIsolateGentle<T>({
}
return workerManager.executeGentle((cancelledChecker) async {
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
DartPluginRegistrant.ensureInitialized();
await runZonedGuarded(
() async {
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
DartPluginRegistrant.ensureInitialized();
final (isar, drift, logDb) = await Bootstrap.initDB();
await Bootstrap.initDomain(isar, drift, logDb, shouldBufferLogs: false);
final ref = ProviderContainer(
overrides: [
// TODO: Remove once isar is removed
dbProvider.overrideWithValue(isar),
isarProvider.overrideWithValue(isar),
cancellationProvider.overrideWithValue(cancelledChecker),
driftProvider.overrideWith(driftOverride(drift)),
],
);
final (isar, drift, logDb) = await Bootstrap.initDB();
await Bootstrap.initDomain(isar, drift, logDb, shouldBufferLogs: false, listenStoreUpdates: false);
final ref = ProviderContainer(
overrides: [
// TODO: Remove once isar is removed
dbProvider.overrideWithValue(isar),
isarProvider.overrideWithValue(isar),
cancellationProvider.overrideWithValue(cancelledChecker),
driftProvider.overrideWith(driftOverride(drift)),
],
);
Logger log = Logger("IsolateLogger");
Logger log = Logger("IsolateLogger");
try {
HttpSSLOptions.apply(applyNative: false);
return await computation(ref);
} on CanceledError {
log.warning("Computation cancelled ${debugLabel == null ? '' : ' for $debugLabel'}");
} catch (error, stack) {
log.severe("Error in runInIsolateGentle ${debugLabel == null ? '' : ' for $debugLabel'}", error, stack);
} finally {
try {
await LogService.I.dispose();
await logDb.close();
await ref.read(driftProvider).close();
// Close Isar safely
try {
final isar = ref.read(isarProvider);
if (isar.isOpen) {
await isar.close();
}
} catch (e) {
debugPrint("Error closing Isar: $e");
}
HttpSSLOptions.apply(applyNative: false);
return await computation(ref);
} on CanceledError {
log.warning("Computation cancelled ${debugLabel == null ? '' : ' for $debugLabel'}");
} catch (error, stack) {
log.severe("Error in runInIsolateGentle ${debugLabel == null ? '' : ' for $debugLabel'}", error, stack);
} finally {
try {
ref.dispose();
ref.dispose();
} catch (error, stack) {
debugPrint("Error closing resources in isolate: $error, $stack");
} finally {
ref.dispose();
// Delay to ensure all resources are released
await Future.delayed(const Duration(seconds: 2));
}
}
await Store.dispose();
await LogService.I.dispose();
await logDb.close();
await drift.close();
// Close Isar safely
try {
if (isar.isOpen) {
await isar.close();
}
} catch (e) {
dPrint(() => "Error closing Isar: $e");
}
} catch (error, stack) {
dPrint(() => "Error closing resources in isolate: $error, $stack");
} finally {
ref.dispose();
// Delay to ensure all resources are released
await Future.delayed(const Duration(seconds: 2));
}
}
return null;
},
(error, stack) {
dPrint(() => "Error in isolate $debugLabel zone: $error, $stack");
},
);
return null;
});
}
+67 -47
View File
@@ -4,8 +4,6 @@ import 'dart:io';
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/album.entity.dart';
@@ -23,22 +21,18 @@ import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:immich_mobile/utils/diff.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
// ignore: import_rule_photo_manager
import 'package:photo_manager/photo_manager.dart';
const int targetVersion = 14;
const int targetVersion = 16;
Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
final hasVersion = Store.tryGet(StoreKey.version) != null;
final int version = Store.get(StoreKey.version, targetVersion);
if (version < 9) {
await Store.put(StoreKey.version, targetVersion);
final value = await db.storeValues.get(StoreKey.currentUser.id);
@@ -68,6 +62,31 @@ Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
await Store.populateCache();
}
// Handle migration only for this version
// TODO: remove when old timeline is removed
final needBetaMigration = Store.tryGet(StoreKey.needBetaMigration);
if (version == 15 && needBetaMigration == null) {
// Check both databases directly instead of relying on cache
final isBeta = Store.tryGet(StoreKey.betaTimeline);
final isNewInstallation = await _isNewInstallation(db, drift);
// For new installations, no migration needed
// For existing installations, only migrate if beta timeline is not enabled (null or false)
if (isNewInstallation || isBeta == true) {
await Store.put(StoreKey.needBetaMigration, false);
await Store.put(StoreKey.betaTimeline, true);
} else {
await drift.reset();
await Store.put(StoreKey.needBetaMigration, true);
}
}
if (version < 16) {
await SyncStreamRepository(drift).reset();
await Store.put(StoreKey.shouldResetSync, true);
}
if (targetVersion >= 12) {
await Store.put(StoreKey.version, targetVersion);
return;
@@ -80,6 +99,35 @@ Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
}
}
Future<bool> _isNewInstallation(Isar db, Drift drift) async {
try {
final isarUserCount = await db.users.count();
if (isarUserCount > 0) {
return false;
}
final isarAssetCount = await db.assets.count();
if (isarAssetCount > 0) {
return false;
}
final driftStoreCount = await drift.storeEntity.select().get().then((list) => list.length);
if (driftStoreCount > 0) {
return false;
}
final driftAssetCount = await drift.localAssetEntity.select().get().then((list) => list.length);
if (driftAssetCount > 0) {
return false;
}
return true;
} catch (error) {
dPrint(() => "[MIGRATION] Error checking if new installation: $error");
return false;
}
}
Future<void> _migrateTo(Isar db, int version) async {
await Store.delete(StoreKey.assetETag);
await db.writeTxn(() async {
@@ -101,10 +149,7 @@ Future<void> _migrateDeviceAsset(Isar db) async {
final PermissionState ps = await PhotoManager.requestPermissionExtend();
if (!ps.hasAccess) {
if (kDebugMode) {
debugPrint("[MIGRATION] Photo library permission not granted. Skipping device asset migration.");
}
dPrint(() => "[MIGRATION] Photo library permission not granted. Skipping device asset migration.");
return;
}
@@ -124,8 +169,8 @@ Future<void> _migrateDeviceAsset(Isar db) async {
localAssets = allDeviceAssets.map((a) => _DeviceAsset(assetId: a.id, dateTime: a.modifiedDateTime)).toList();
}
debugPrint("[MIGRATION] Device Asset Ids length - ${ids.length}");
debugPrint("[MIGRATION] Local Asset Ids length - ${localAssets.length}");
dPrint(() => "[MIGRATION] Device Asset Ids length - ${ids.length}");
dPrint(() => "[MIGRATION] Local Asset Ids length - ${localAssets.length}");
ids.sort((a, b) => a.assetId.compareTo(b.assetId));
localAssets.sort((a, b) => a.assetId.compareTo(b.assetId));
final List<DeviceAssetEntity> toAdd = [];
@@ -140,20 +185,14 @@ Future<void> _migrateDeviceAsset(Isar db) async {
return false;
},
onlyFirst: (deviceAsset) {
if (kDebugMode) {
debugPrint('[MIGRATION] Local asset not found in DeviceAsset: ${deviceAsset.assetId}');
}
dPrint(() => '[MIGRATION] Local asset not found in DeviceAsset: ${deviceAsset.assetId}');
},
onlySecond: (asset) {
if (kDebugMode) {
debugPrint('[MIGRATION] Local asset not found in DeviceAsset: ${asset.assetId}');
}
dPrint(() => '[MIGRATION] Local asset not found in DeviceAsset: ${asset.assetId}');
},
);
if (kDebugMode) {
debugPrint("[MIGRATION] Total number of device assets migrated - ${toAdd.length}");
}
dPrint(() => "[MIGRATION] Total number of device assets migrated - ${toAdd.length}");
await db.writeTxn(() async {
await db.deviceAssetEntitys.putAll(toAdd);
@@ -173,7 +212,7 @@ Future<void> migrateDeviceAssetToSqlite(Isar db, Drift drift) async {
}
});
} catch (error) {
debugPrint("[MIGRATION] Error while migrating device assets to SQLite: $error");
dPrint(() => "[MIGRATION] Error while migrating device assets to SQLite: $error");
}
}
@@ -221,7 +260,7 @@ Future<void> migrateBackupAlbumsToSqlite(Isar db, Drift drift) async {
}
});
} catch (error) {
debugPrint("[MIGRATION] Error while migrating backup albums to SQLite: $error");
dPrint(() => "[MIGRATION] Error while migrating backup albums to SQLite: $error");
}
}
@@ -239,7 +278,7 @@ Future<void> migrateStoreToSqlite(Isar db, Drift drift) async {
}
});
} catch (error) {
debugPrint("[MIGRATION] Error while migrating store values to SQLite: $error");
dPrint(() => "[MIGRATION] Error while migrating store values to SQLite: $error");
}
}
@@ -254,7 +293,7 @@ Future<void> migrateStoreToIsar(Isar db, Drift drift) async {
await db.storeValues.putAll(driftStoreValues);
});
} catch (error) {
debugPrint("[MIGRATION] Error while migrating store values to Isar: $error");
dPrint(() => "[MIGRATION] Error while migrating store values to Isar: $error");
}
}
@@ -265,22 +304,3 @@ class _DeviceAsset {
const _DeviceAsset({required this.assetId, this.hash, this.dateTime});
}
Future<List<void>> runNewSync(WidgetRef ref, {bool full = false}) {
ref.read(backupProvider.notifier).cancelBackup();
final backgroundManager = ref.read(backgroundSyncProvider);
final isAlbumLinkedSyncEnable = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
return Future.wait([
backgroundManager.syncLocal(full: full).then((_) {
Logger("runNewSync").fine("Hashing assets after syncLocal");
return backgroundManager.hashAssets();
}),
backgroundManager.syncRemote().then((_) {
if (isAlbumLinkedSyncEnable) {
return backgroundManager.syncLinkedAlbum();
}
}),
]);
}