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
+2
View File
@@ -45,3 +45,5 @@ const List<(String, String)> kWidgetNames = [
const double kUploadStatusFailed = -1.0;
const double kUploadStatusCanceled = -2.0;
const int kMinMonthsToEnableScrubberSnap = 12;
+4 -1
View File
@@ -76,7 +76,10 @@ enum StoreKey<T> {
betaTimeline<bool>._(1002),
enableBackup<bool>._(1003),
useWifiForUploadVideos<bool>._(1004),
useWifiForUploadPhotos<bool>._(1005);
useWifiForUploadPhotos<bool>._(1005),
needBetaMigration<bool>._(1006),
// TODO: Remove this after patching open-api
shouldResetSync<bool>._(1007);
const StoreKey._(this.id);
final int id;
+35 -5
View File
@@ -1,7 +1,36 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';
import 'dart:ui';
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
enum AvatarColor {
// do not change this order or reuse indices for other purposes, adding is OK
primary("primary"),
pink("pink"),
red("red"),
yellow("yellow"),
blue("blue"),
green("green"),
purple("purple"),
orange("orange"),
gray("gray"),
amber("amber");
final String value;
const AvatarColor(this.value);
Color toColor({bool isDarkTheme = false}) => switch (this) {
AvatarColor.primary => isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF),
AvatarColor.pink => const Color.fromARGB(255, 244, 114, 182),
AvatarColor.red => const Color.fromARGB(255, 239, 68, 68),
AvatarColor.yellow => const Color.fromARGB(255, 234, 179, 8),
AvatarColor.blue => const Color.fromARGB(255, 59, 130, 246),
AvatarColor.green => const Color.fromARGB(255, 22, 163, 74),
AvatarColor.purple => const Color.fromARGB(255, 147, 51, 234),
AvatarColor.orange => const Color.fromARGB(255, 234, 88, 12),
AvatarColor.gray => const Color.fromARGB(255, 75, 85, 99),
AvatarColor.amber => const Color.fromARGB(255, 217, 119, 6),
};
}
// TODO: Rename to User once Isar is removed
class UserDto {
@@ -9,7 +38,7 @@ class UserDto {
final String email;
final String name;
final bool isAdmin;
final DateTime updatedAt;
final DateTime? updatedAt;
final AvatarColor avatarColor;
@@ -31,8 +60,8 @@ class UserDto {
required this.id,
required this.email,
required this.name,
required this.isAdmin,
required this.updatedAt,
this.isAdmin = false,
this.updatedAt,
required this.profileChangedAt,
this.avatarColor = AvatarColor.primary,
this.memoryEnabled = true,
@@ -99,7 +128,8 @@ profileChangedAt: $profileChangedAt
if (identical(this, other)) return true;
return other.id == id &&
other.updatedAt.isAtSameMomentAs(updatedAt) &&
((updatedAt == null && other.updatedAt == null) ||
(updatedAt != null && other.updatedAt != null && other.updatedAt!.isAtSameMomentAs(updatedAt!))) &&
other.avatarColor == avatarColor &&
other.email == email &&
other.name == name &&
@@ -1,4 +1,4 @@
import 'dart:ui';
import 'package:immich_mobile/domain/models/user.model.dart';
enum UserMetadataKey {
// do not change this order!
@@ -7,36 +7,6 @@ enum UserMetadataKey {
license,
}
enum AvatarColor {
// do not change this order or reuse indices for other purposes, adding is OK
primary("primary"),
pink("pink"),
red("red"),
yellow("yellow"),
blue("blue"),
green("green"),
purple("purple"),
orange("orange"),
gray("gray"),
amber("amber");
final String value;
const AvatarColor(this.value);
Color toColor({bool isDarkTheme = false}) => switch (this) {
AvatarColor.primary => isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF),
AvatarColor.pink => const Color.fromARGB(255, 244, 114, 182),
AvatarColor.red => const Color.fromARGB(255, 239, 68, 68),
AvatarColor.yellow => const Color.fromARGB(255, 234, 179, 8),
AvatarColor.blue => const Color.fromARGB(255, 59, 130, 246),
AvatarColor.green => const Color.fromARGB(255, 22, 163, 74),
AvatarColor.purple => const Color.fromARGB(255, 147, 51, 234),
AvatarColor.orange => const Color.fromARGB(255, 234, 88, 12),
AvatarColor.gray => const Color.fromARGB(255, 75, 85, 99),
AvatarColor.amber => const Color.fromARGB(255, 217, 119, 6),
};
}
class Onboarding {
final bool isOnboarded;
+16 -5
View File
@@ -1,21 +1,20 @@
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
import 'package:platform/platform.dart';
class AssetService {
final RemoteAssetRepository _remoteAssetRepository;
final DriftLocalAssetRepository _localAssetRepository;
final Platform _platform;
const AssetService({
required RemoteAssetRepository remoteAssetRepository,
required DriftLocalAssetRepository localAssetRepository,
}) : _remoteAssetRepository = remoteAssetRepository,
_localAssetRepository = localAssetRepository,
_platform = const LocalPlatform();
_localAssetRepository = localAssetRepository;
Future<BaseAsset?> getAsset(BaseAsset asset) {
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).id;
@@ -27,6 +26,14 @@ class AssetService {
return asset is LocalAsset ? _localAssetRepository.watch(id) : _remoteAssetRepository.watch(id);
}
Future<List<LocalAsset?>> getLocalAssetsByChecksum(String checksum) {
return _localAssetRepository.getByChecksum(checksum);
}
Future<RemoteAsset?> getRemoteAssetByChecksum(String checksum) {
return _remoteAssetRepository.getByChecksum(checksum);
}
Future<RemoteAsset?> getRemoteAsset(String id) {
return _remoteAssetRepository.get(id);
}
@@ -62,7 +69,7 @@ class AssetService {
width = exif?.width ?? asset.width?.toDouble();
height = exif?.height ?? asset.height?.toDouble();
} else if (asset is LocalAsset) {
isFlipped = _platform.isAndroid && (asset.orientation == 90 || asset.orientation == 270);
isFlipped = CurrentPlatform.isAndroid && (asset.orientation == 90 || asset.orientation == 270);
width = asset.width?.toDouble();
height = asset.height?.toDouble();
} else {
@@ -89,4 +96,8 @@ class AssetService {
Future<int> getLocalHashedCount() {
return _localAssetRepository.getHashedCount();
}
Future<List<LocalAlbum>> getSourceAlbums(String localAssetId, {BackupSelection? backupSelection}) {
return _localAssetRepository.getSourceAlbums(localAssetId, backupSelection: backupSelection);
}
}
@@ -1,11 +1,17 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui';
import 'package:background_downloader/background_downloader.dart';
import 'package:cancellation_token_http/http.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/utils/isolate_lock_manager.dart';
import 'package:immich_mobile/domain/services/log.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/network_capability_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/generated/intl_keys.g.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
import 'package:immich_mobile/platform/background_worker_api.g.dart';
@@ -14,16 +20,20 @@ import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/repositories/file_media.repository.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/services/auth.service.dart';
import 'package:immich_mobile/services/localization.service.dart';
import 'package:immich_mobile/services/server_info.service.dart';
import 'package:immich_mobile/services/upload.service.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:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:worker_manager/worker_manager.dart';
class BackgroundWorkerFgService {
final BackgroundWorkerFgHostApi _foregroundHostApi;
@@ -42,8 +52,8 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
final Drift _drift;
final DriftLogger _driftLogger;
final BackgroundWorkerBgHostApi _backgroundHostApi;
final Logger _logger = Logger('BackgroundUploadBgService');
late final IsolateLockManager _lockManager;
final CancellationToken _cancellationToken = CancellationToken();
final Logger _logger = Logger('BackgroundWorkerBgService');
bool _isCleanedUp = false;
@@ -59,7 +69,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
driftProvider.overrideWith(driftOverride(drift)),
],
);
_lockManager = IsolateLockManager(onCloseRequest: _cleanup);
BackgroundWorkerFlutterApi.setUp(this);
}
@@ -67,41 +76,37 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
Future<void> init() async {
try {
await loadTranslations();
HttpSSLOptions.apply(applyNative: false);
await _ref.read(authServiceProvider).setOpenApiServiceEndpoint();
// Initialize the file downloader
await FileDownloader().configure(
globalConfig: [
// maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3
(Config.holdingQueue, (6, 6, 3)),
// On Android, if files are larger than 256MB, run in foreground service
(Config.runInForegroundIfFileLargerThan, 256),
],
);
await FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false);
await FileDownloader().trackTasks();
await Future.wait([
loadTranslations(),
workerManager.init(dynamicSpawning: true),
_ref.read(authServiceProvider).setOpenApiServiceEndpoint(),
// Initialize the file downloader
FileDownloader().configure(
globalConfig: [
// maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3
(Config.holdingQueue, (6, 6, 3)),
// On Android, if files are larger than 256MB, run in foreground service
(Config.runInForegroundIfFileLargerThan, 256),
],
),
FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false),
FileDownloader().trackTasks(),
_ref.read(fileMediaRepositoryProvider).enableBackgroundAccess(),
]);
configureFileDownloaderNotifications();
await _ref.read(fileMediaRepositoryProvider).enableBackgroundAccess();
// Notify the host that the background upload service has been initialized and is ready to use
debugPrint("Acquiring background worker lock");
if (await _lockManager.acquireLock().timeout(
const Duration(seconds: 5),
onTimeout: () {
_lockManager.cancel();
return false;
},
)) {
_logger.info("Acquired background worker lock");
await _backgroundHostApi.onInitialized();
return;
if (Platform.isAndroid) {
await _backgroundHostApi.showNotification(
IntlKeys.uploading_media.t(),
IntlKeys.backup_background_service_in_progress_notification.t(),
);
}
_logger.warning("Failed to acquire background worker lock");
await _cleanup();
await _backgroundHostApi.close();
// Notify the host that the background worker service has been initialized and is ready to use
_backgroundHostApi.onInitialized();
} catch (error, stack) {
_logger.severe("Failed to initialize background worker", error, stack);
await _backgroundHostApi.close();
@@ -115,7 +120,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
final sw = Stopwatch()..start();
await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6));
await _handleBackup(processBulk: false);
await _handleBackup();
sw.stop();
_logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s");
@@ -157,7 +162,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
try {
await _cleanup();
} catch (error, stack) {
debugPrint('Failed to cleanup background worker: $error with stack: $stack');
dPrint(() => 'Failed to cleanup background worker: $error with stack: $stack');
}
}
@@ -167,70 +172,96 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
}
try {
final backgroundSyncManager = _ref.read(backgroundSyncProvider);
_isCleanedUp = true;
_logger.info("Cleaning up background worker");
await _ref.read(backgroundSyncProvider).cancel();
await _ref.read(backgroundSyncProvider).cancelLocal();
if (_isar.isOpen) {
await _isar.close();
}
await _drift.close();
await _driftLogger.close();
_ref.dispose();
_lockManager.releaseLock();
_cancellationToken.cancel();
_logger.info("Cleaning up background worker");
final cleanupFutures = [
workerManager.dispose().catchError((_) async {
// Discard any errors on the dispose call
return;
}),
LogService.I.dispose(),
Store.dispose(),
_drift.close(),
_driftLogger.close(),
backgroundSyncManager.cancel(),
backgroundSyncManager.cancelLocal(),
];
if (_isar.isOpen) {
cleanupFutures.add(_isar.close());
}
await Future.wait(cleanupFutures);
_logger.info("Background worker resources cleaned up");
} catch (error, stack) {
debugPrint('Failed to cleanup background worker: $error with stack: $stack');
dPrint(() => 'Failed to cleanup background worker: $error with stack: $stack');
}
}
Future<void> _handleBackup({bool processBulk = true}) async {
if (!_isBackupEnabled) {
return;
}
Future<void> _handleBackup() async {
await runZonedGuarded(
() async {
if (!_isBackupEnabled || _isCleanedUp) {
_logger.info("[_handleBackup 1] Backup is disabled. Skipping backup routine");
return;
}
final currentUser = _ref.read(currentUserProvider);
if (currentUser == null) {
return;
}
_logger.info("[_handleBackup 2] Enqueuing assets for backup from the background service");
if (processBulk) {
return _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
}
final currentUser = _ref.read(currentUserProvider);
if (currentUser == null) {
_logger.warning("[_handleBackup 3] No current user found. Skipping backup from background");
return;
}
final activeTask = await _ref.read(uploadServiceProvider).getActiveTasks(currentUser.id);
if (activeTask.isNotEmpty) {
await _ref.read(uploadServiceProvider).resumeBackup();
} else {
await _ref.read(uploadServiceProvider).startBackupSerial(currentUser.id);
}
_logger.info("[_handleBackup 4] Resume backup from background");
if (Platform.isIOS) {
return _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
}
final canPing = await _ref.read(serverInfoServiceProvider).ping();
if (!canPing) {
_logger.warning("[_handleBackup 5] Server is not reachable. Skipping backup from background");
return;
}
final networkCapabilities = await _ref.read(connectivityApiProvider).getCapabilities();
return _ref
.read(uploadServiceProvider)
.startBackupWithHttpClient(currentUser.id, networkCapabilities.hasWifi, _cancellationToken);
},
(error, stack) {
dPrint(() => "Error in backup zone $error, $stack");
},
);
}
Future<void> _syncAssets({Duration? hashTimeout}) async {
final futures = <Future<void>>[];
await _ref.read(backgroundSyncProvider).syncLocal();
if (_isCleanedUp) {
return;
}
final localSyncFuture = _ref.read(backgroundSyncProvider).syncLocal().then((_) async {
if (_isCleanedUp) {
return;
}
await _ref.read(backgroundSyncProvider).syncRemote();
if (_isCleanedUp) {
return;
}
var hashFuture = _ref.read(backgroundSyncProvider).hashAssets();
if (hashTimeout != null) {
hashFuture = hashFuture.timeout(
hashTimeout,
onTimeout: () {
// Consume cancellation errors as we want to continue processing
},
);
}
var hashFuture = _ref.read(backgroundSyncProvider).hashAssets();
if (hashTimeout != null) {
hashFuture = hashFuture.timeout(
hashTimeout,
onTimeout: () {
// Consume cancellation errors as we want to continue processing
},
);
}
return hashFuture;
});
futures.add(localSyncFuture);
futures.add(_ref.read(backgroundSyncProvider).syncRemote());
await Future.wait(futures);
await hashFuture;
}
}
@@ -242,6 +273,6 @@ Future<void> backgroundSyncNativeEntrypoint() async {
DartPluginRegistrant.ensureInitialized();
final (isar, drift, logDB) = await Bootstrap.initDB();
await Bootstrap.initDomain(isar, drift, logDB, shouldBufferLogs: false);
await Bootstrap.initDomain(isar, drift, logDB, shouldBufferLogs: false, listenStoreUpdates: false);
await BackgroundWorkerBgService(isar: isar, drift: drift, driftLogger: logDB).init();
}
@@ -1,29 +1,24 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/utils/datetime_helpers.dart';
import 'package:immich_mobile/utils/diff.dart';
import 'package:logging/logging.dart';
import 'package:platform/platform.dart';
class LocalSyncService {
final DriftLocalAlbumRepository _localAlbumRepository;
final NativeSyncApi _nativeSyncApi;
final Platform _platform;
final Logger _log = Logger("DeviceSyncService");
LocalSyncService({
required DriftLocalAlbumRepository localAlbumRepository,
required NativeSyncApi nativeSyncApi,
Platform? platform,
}) : _localAlbumRepository = localAlbumRepository,
_nativeSyncApi = nativeSyncApi,
_platform = platform ?? const LocalPlatform();
LocalSyncService({required DriftLocalAlbumRepository localAlbumRepository, required NativeSyncApi nativeSyncApi})
: _localAlbumRepository = localAlbumRepository,
_nativeSyncApi = nativeSyncApi;
Future<void> sync({bool full = false}) async {
final Stopwatch stopwatch = Stopwatch()..start();
@@ -53,14 +48,14 @@ class LocalSyncService {
final dbAlbums = await _localAlbumRepository.getAll();
// On Android, we need to sync all albums since it is not possible to
// detect album deletions from the native side
if (_platform.isAndroid) {
if (CurrentPlatform.isAndroid) {
for (final album in dbAlbums) {
final deviceIds = await _nativeSyncApi.getAssetIdsForAlbum(album.id);
await _localAlbumRepository.syncDeletes(album.id, deviceIds);
}
}
if (_platform.isIOS) {
if (CurrentPlatform.isIOS) {
// On iOS, we need to full sync albums that are marked as cloud as the delta sync
// does not include changes for cloud albums. If ignoreIcloudAssets is enabled,
// remove the albums from the local database from the previous sync
+7 -8
View File
@@ -1,11 +1,11 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:logging/logging.dart';
/// Service responsible for handling application logging.
@@ -66,13 +66,12 @@ class LogService {
}
void _handleLogRecord(LogRecord r) {
if (kDebugMode) {
debugPrint(
'[${r.level.name}] [${r.time}] [${r.loggerName}] ${r.message}'
'${r.error == null ? '' : '\nError: ${r.error}'}'
'${r.stackTrace == null ? '' : '\nStack: ${r.stackTrace}'}',
);
}
dPrint(
() =>
'[${r.level.name}] [${r.time}] [${r.loggerName}] ${r.message}'
'${r.error == null ? '' : '\nError: ${r.error}'}'
'${r.stackTrace == null ? '' : '\nStack: ${r.stackTrace}'}',
);
final record = LogMessage(
message: r.message,
@@ -1,7 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/repositories/partner.repository.dart';
import 'package:immich_mobile/repositories/partner_api.repository.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class DriftPartnerService {
final DriftPartnerRepository _driftPartnerRepository;
@@ -30,7 +30,7 @@ class DriftPartnerService {
Future<void> toggleShowInTimeline(String partnerId, String userId) async {
final partner = await _driftPartnerRepository.getPartner(partnerId, userId);
if (partner == null) {
debugPrint("Partner not found: $partnerId for user: $userId");
dPrint(() => "Partner not found: $partnerId for user: $userId");
return;
}
+10 -8
View File
@@ -10,7 +10,7 @@ class StoreService {
/// In-memory cache. Keys are [StoreKey.id]
final Map<int, Object?> _cache = {};
late final StreamSubscription<List<StoreDto>> _storeUpdateSubscription;
StreamSubscription<List<StoreDto>>? _storeUpdateSubscription;
StoreService._({required IStoreRepository isarStoreRepository}) : _storeRepository = isarStoreRepository;
@@ -24,15 +24,17 @@ class StoreService {
}
// TODO: Replace the implementation with the one from create after removing the typedef
static Future<StoreService> init({required IStoreRepository storeRepository}) async {
_instance ??= await create(storeRepository: storeRepository);
static Future<StoreService> init({required IStoreRepository storeRepository, bool listenUpdates = true}) async {
_instance ??= await create(storeRepository: storeRepository, listenUpdates: listenUpdates);
return _instance!;
}
static Future<StoreService> create({required IStoreRepository storeRepository}) async {
static Future<StoreService> create({required IStoreRepository storeRepository, bool listenUpdates = true}) async {
final instance = StoreService._(isarStoreRepository: storeRepository);
await instance.populateCache();
instance._storeUpdateSubscription = instance._listenForChange();
if (listenUpdates) {
instance._storeUpdateSubscription = instance._listenForChange();
}
return instance;
}
@@ -50,8 +52,8 @@ class StoreService {
});
/// Disposes the store and cancels the subscription. To reuse the store call init() again
void dispose() async {
await _storeUpdateSubscription.cancel();
Future<void> dispose() async {
await _storeUpdateSubscription?.cancel();
_cache.clear();
}
@@ -90,7 +92,7 @@ class StoreService {
_cache.clear();
}
bool get isBetaTimelineEnabled => tryGet(StoreKey.betaTimeline) ?? false;
bool get isBetaTimelineEnabled => tryGet(StoreKey.betaTimeline) ?? true;
}
class StoreKeyNotFoundException implements Exception {
@@ -1,10 +1,11 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
import 'package:logging/logging.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final syncLinkedAlbumServiceProvider = Provider(
(ref) => SyncLinkedAlbumService(
@@ -19,7 +20,9 @@ class SyncLinkedAlbumService {
final DriftRemoteAlbumRepository _remoteAlbumRepository;
final DriftAlbumApiRepository _albumApiRepository;
const SyncLinkedAlbumService(this._localAlbumRepository, this._remoteAlbumRepository, this._albumApiRepository);
SyncLinkedAlbumService(this._localAlbumRepository, this._remoteAlbumRepository, this._albumApiRepository);
final _log = Logger("SyncLinkedAlbumService");
Future<void> syncLinkedAlbums(String userId) async {
final selectedAlbums = await _localAlbumRepository.getBackupAlbums();
@@ -48,8 +51,12 @@ class SyncLinkedAlbumService {
}
Future<void> manageLinkedAlbums(List<LocalAlbum> localAlbums, String ownerId) async {
for (final album in localAlbums) {
await _processLocalAlbum(album, ownerId);
try {
for (final album in localAlbums) {
await _processLocalAlbum(album, ownerId);
}
} catch (error, stackTrace) {
_log.severe("Error managing linked albums", error, stackTrace);
}
}
@@ -93,7 +100,7 @@ class SyncLinkedAlbumService {
/// Creates a new remote album and links it to the local album
Future<void> _createAndLinkNewRemoteAlbum(LocalAlbum localAlbum) async {
debugPrint("Creating new remote album for local album: ${localAlbum.name}");
dPrint(() => "Creating new remote album for local album: ${localAlbum.name}");
final newRemoteAlbum = await _albumApiRepository.createDriftAlbum(localAlbum.name, assetIds: []);
await _remoteAlbumRepository.create(newRemoteAlbum, []);
return _localAlbumRepository.linkRemoteAlbum(localAlbum.id, newRemoteAlbum.id);
@@ -23,54 +23,18 @@ class SyncStreamService {
bool get isCancelled => _cancelChecker?.call() ?? false;
Future<void> sync() {
Future<void> sync() async {
_logger.info("Remote sync request for user");
// Start the sync stream and handle events
return _syncApiRepository.streamChanges(_handleEvents);
}
Future<void> handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) async {
if (batchData.isEmpty) return;
_logger.info('Processing batch of ${batchData.length} AssetUploadReadyV1 events');
final List<SyncAssetV1> assets = [];
final List<SyncAssetExifV1> exifs = [];
try {
for (final data in batchData) {
if (data is! Map<String, dynamic>) {
continue;
}
final payload = data;
final assetData = payload['asset'];
final exifData = payload['exif'];
if (assetData == null || exifData == null) {
continue;
}
final asset = SyncAssetV1.fromJson(assetData);
final exif = SyncAssetExifV1.fromJson(exifData);
if (asset != null && exif != null) {
assets.add(asset);
exifs.add(exif);
}
}
if (assets.isNotEmpty && exifs.isNotEmpty) {
await _syncStreamRepository.updateAssetsV1(assets, debugLabel: 'websocket-batch');
await _syncStreamRepository.updateAssetsExifV1(exifs, debugLabel: 'websocket-batch');
_logger.info('Successfully processed ${assets.length} assets in batch');
}
} catch (error, stackTrace) {
_logger.severe("Error processing AssetUploadReadyV1 websocket batch events", error, stackTrace);
bool shouldReset = false;
await _syncApiRepository.streamChanges(_handleEvents, onReset: () => shouldReset = true);
if (shouldReset) {
_logger.info("Resetting sync state as requested by server");
await _syncApiRepository.streamChanges(_handleEvents);
}
}
Future<void> _handleEvents(List<SyncEvent> events, Function() abort) async {
Future<void> _handleEvents(List<SyncEvent> events, Function() abort, Function() reset) async {
List<SyncEvent> items = [];
for (final event in events) {
if (isCancelled) {
@@ -83,6 +47,10 @@ class SyncStreamService {
await _processBatch(items);
}
if (event.type == SyncEntityType.syncResetV1) {
reset();
}
items.add(event);
}
@@ -103,6 +71,8 @@ class SyncStreamService {
Future<void> _handleSyncData(SyncEntityType type, Iterable<Object> data) async {
_logger.fine("Processing sync data for $type of length ${data.length}");
switch (type) {
case SyncEntityType.authUserV1:
return _syncStreamRepository.updateAuthUsersV1(data.cast());
case SyncEntityType.userV1:
return _syncStreamRepository.updateUsersV1(data.cast());
case SyncEntityType.userDeleteV1:
@@ -159,6 +129,12 @@ class SyncStreamService {
// to acknowledge that the client has processed all the backfill events
case SyncEntityType.syncAckV1:
return;
// No-op. SyncCompleteV1 is used to signal the completion of the sync process
case SyncEntityType.syncCompleteV1:
return;
// Request to reset the client state. Clear everything related to remote entities
case SyncEntityType.syncResetV1:
return _syncStreamRepository.reset();
case SyncEntityType.memoryV1:
return _syncStreamRepository.updateMemoriesV1(data.cast());
case SyncEntityType.memoryDeleteV1:
@@ -193,4 +169,45 @@ class SyncStreamService {
_logger.warning("Unknown sync data type: $type");
}
}
Future<void> handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) async {
if (batchData.isEmpty) return;
_logger.info('Processing batch of ${batchData.length} AssetUploadReadyV1 events');
final List<SyncAssetV1> assets = [];
final List<SyncAssetExifV1> exifs = [];
try {
for (final data in batchData) {
if (data is! Map<String, dynamic>) {
continue;
}
final payload = data;
final assetData = payload['asset'];
final exifData = payload['exif'];
if (assetData == null || exifData == null) {
continue;
}
final asset = SyncAssetV1.fromJson(assetData);
final exif = SyncAssetExifV1.fromJson(exifData);
if (asset != null && exif != null) {
assets.add(asset);
exifs.add(exif);
}
}
if (assets.isNotEmpty && exifs.isNotEmpty) {
await _syncStreamRepository.updateAssetsV1(assets, debugLabel: 'websocket-batch');
await _syncStreamRepository.updateAssetsExifV1(exifs, debugLabel: 'websocket-batch');
_logger.info('Successfully processed ${assets.length} assets in batch');
}
} catch (error, stackTrace) {
_logger.severe("Error processing AssetUploadReadyV1 websocket batch events", error, stackTrace);
}
}
}
+18 -5
View File
@@ -100,8 +100,14 @@ class BackgroundSyncManager {
// We use a ternary operator to avoid [_deviceAlbumSyncTask] from being
// captured by the closure passed to [runInIsolateGentle].
_deviceAlbumSyncTask = full
? runInIsolateGentle(computation: (ref) => ref.read(localSyncServiceProvider).sync(full: true))
: runInIsolateGentle(computation: (ref) => ref.read(localSyncServiceProvider).sync(full: false));
? runInIsolateGentle(
computation: (ref) => ref.read(localSyncServiceProvider).sync(full: true),
debugLabel: 'local-sync-full-true',
)
: runInIsolateGentle(
computation: (ref) => ref.read(localSyncServiceProvider).sync(full: false),
debugLabel: 'local-sync-full-false',
);
return _deviceAlbumSyncTask!
.whenComplete(() {
@@ -122,7 +128,10 @@ class BackgroundSyncManager {
onHashingStart?.call();
_hashTask = runInIsolateGentle(computation: (ref) => ref.read(hashServiceProvider).hashAssets());
_hashTask = runInIsolateGentle(
computation: (ref) => ref.read(hashServiceProvider).hashAssets(),
debugLabel: 'hash-assets',
);
return _hashTask!
.whenComplete(() {
@@ -142,7 +151,10 @@ class BackgroundSyncManager {
onRemoteSyncStart?.call();
_syncTask = runInIsolateGentle(computation: (ref) => ref.read(syncStreamServiceProvider).sync());
_syncTask = runInIsolateGentle(
computation: (ref) => ref.read(syncStreamServiceProvider).sync(),
debugLabel: 'remote-sync',
);
return _syncTask!
.whenComplete(() {
onRemoteSyncComplete?.call();
@@ -169,7 +181,7 @@ class BackgroundSyncManager {
return _linkedAlbumSyncTask!.future;
}
_linkedAlbumSyncTask = runInIsolateGentle(computation: syncLinkedAlbumsIsolated);
_linkedAlbumSyncTask = runInIsolateGentle(computation: syncLinkedAlbumsIsolated, debugLabel: 'linked-album-sync');
return _linkedAlbumSyncTask!.whenComplete(() {
_linkedAlbumSyncTask = null;
});
@@ -178,4 +190,5 @@ class BackgroundSyncManager {
Cancelable<void> _handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) => runInIsolateGentle(
computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetUploadReadyV1Batch(batchData),
debugLabel: 'websocket-batch',
);
@@ -1,235 +0,0 @@
import 'dart:isolate';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
const String kIsolateLockManagerPort = "immich://isolate_mutex";
enum _LockStatus { active, released }
class _IsolateRequest {
const _IsolateRequest();
}
class _HeartbeatRequest extends _IsolateRequest {
// Port for the receiver to send replies back
final SendPort sendPort;
const _HeartbeatRequest(this.sendPort);
Map<String, dynamic> toJson() {
return {'type': 'heartbeat', 'sendPort': sendPort};
}
}
class _CloseRequest extends _IsolateRequest {
const _CloseRequest();
Map<String, dynamic> toJson() {
return {'type': 'close'};
}
}
class _IsolateResponse {
const _IsolateResponse();
}
class _HeartbeatResponse extends _IsolateResponse {
final _LockStatus status;
const _HeartbeatResponse(this.status);
Map<String, dynamic> toJson() {
return {'type': 'heartbeat', 'status': status.index};
}
}
typedef OnCloseLockHolderRequest = void Function();
class IsolateLockManager {
final String _portName;
bool _hasLock = false;
ReceivePort? _receivePort;
final OnCloseLockHolderRequest? _onCloseRequest;
final Set<SendPort> _waitingIsolates = {};
// Token object - a new one is created for each acquisition attempt
Object? _currentAcquisitionToken;
IsolateLockManager({String? portName, OnCloseLockHolderRequest? onCloseRequest})
: _portName = portName ?? kIsolateLockManagerPort,
_onCloseRequest = onCloseRequest;
Future<bool> acquireLock() async {
if (_hasLock) {
Logger('BackgroundWorkerLockManager').warning("WARNING: [acquireLock] called more than once");
return true;
}
// Create a new token - this invalidates any previous attempt
final token = _currentAcquisitionToken = Object();
final ReceivePort rp = _receivePort = ReceivePort(_portName);
final SendPort sp = rp.sendPort;
while (!IsolateNameServer.registerPortWithName(sp, _portName)) {
// This attempt was superseded by a newer one in the same isolate
if (_currentAcquisitionToken != token) {
return false;
}
await _lockReleasedByHolder(token);
}
_hasLock = true;
rp.listen(_onRequest);
return true;
}
Future<void> _lockReleasedByHolder(Object token) async {
SendPort? holder = IsolateNameServer.lookupPortByName(_portName);
debugPrint("Found lock holder: $holder");
if (holder == null) {
// No holder, try and acquire lock
return;
}
final ReceivePort tempRp = ReceivePort();
final SendPort tempSp = tempRp.sendPort;
final bs = tempRp.asBroadcastStream();
try {
while (true) {
// Send a heartbeat request with the send port to receive reply from the holder
debugPrint("Sending heartbeat request to lock holder");
holder.send(_HeartbeatRequest(tempSp).toJson());
dynamic answer = await bs.first.timeout(const Duration(seconds: 3), onTimeout: () => null);
debugPrint("Received heartbeat response from lock holder: $answer");
// This attempt was superseded by a newer one in the same isolate
if (_currentAcquisitionToken != token) {
break;
}
if (answer == null) {
// Holder failed, most likely killed without calling releaseLock
// Check if a different waiting isolate took the lock
if (holder == IsolateNameServer.lookupPortByName(_portName)) {
// No, remove the stale lock
IsolateNameServer.removePortNameMapping(_portName);
}
break;
}
// Unknown message type received for heartbeat request. Try again
_IsolateResponse? response = _parseResponse(answer);
if (response == null || response is! _HeartbeatResponse) {
break;
}
if (response.status == _LockStatus.released) {
// Holder has released the lock
break;
}
// If the _LockStatus is active, we check again if the task completed
// by sending a released messaged again, if not, send a new heartbeat again
// Check if the holder completed its task after the heartbeat
answer = await bs.first.timeout(
const Duration(seconds: 3),
onTimeout: () => const _HeartbeatResponse(_LockStatus.active).toJson(),
);
response = _parseResponse(answer);
if (response is _HeartbeatResponse && response.status == _LockStatus.released) {
break;
}
}
} catch (e) {
// Timeout or error
} finally {
tempRp.close();
}
return;
}
_IsolateRequest? _parseRequest(dynamic msg) {
if (msg is! Map<String, dynamic>) {
return null;
}
return switch (msg['type']) {
'heartbeat' => _HeartbeatRequest(msg['sendPort']),
'close' => const _CloseRequest(),
_ => null,
};
}
_IsolateResponse? _parseResponse(dynamic msg) {
if (msg is! Map<String, dynamic>) {
return null;
}
return switch (msg['type']) {
'heartbeat' => _HeartbeatResponse(_LockStatus.values[msg['status']]),
_ => null,
};
}
// Executed in the isolate with the lock
void _onRequest(dynamic msg) {
final request = _parseRequest(msg);
if (request == null) {
return;
}
if (request is _HeartbeatRequest) {
// Add the send port to the list of waiting isolates
_waitingIsolates.add(request.sendPort);
request.sendPort.send(const _HeartbeatResponse(_LockStatus.active).toJson());
return;
}
if (request is _CloseRequest) {
_onCloseRequest?.call();
return;
}
}
void releaseLock() {
if (_hasLock) {
IsolateNameServer.removePortNameMapping(_portName);
// Notify waiting isolates
for (final port in _waitingIsolates) {
port.send(const _HeartbeatResponse(_LockStatus.released).toJson());
}
_waitingIsolates.clear();
_hasLock = false;
}
_receivePort?.close();
_receivePort = null;
}
void cancel() {
if (_hasLock) {
return;
}
debugPrint("Cancelling ongoing acquire lock attempts");
// Create a new token to invalidate ongoing acquire lock attempts
_currentAcquisitionToken = Object();
}
void requestHolderToClose() {
if (_hasLock) {
return;
}
IsolateNameServer.lookupPortByName(_portName)?.send(const _CloseRequest().toJson());
}
}
@@ -1,9 +1,10 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/sync_linked_album.service.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/entities/store.entity.dart';
Future<void> syncLinkedAlbumsIsolated(ProviderContainer ref) {
final user = ref.read(currentUserProvider);
final user = Store.tryGet(StoreKey.currentUser);
if (user == null) {
return Future.value();
}
@@ -0,0 +1,8 @@
import 'package:immich_mobile/platform/connectivity_api.g.dart';
extension NetworkCapabilitiesGetters on List<NetworkCapability> {
bool get hasCellular => contains(NetworkCapability.cellular);
bool get hasWifi => contains(NetworkCapability.wifi);
bool get hasVpn => contains(NetworkCapability.vpn);
bool get isUnmetered => contains(NetworkCapability.unmetered);
}
@@ -0,0 +1,9 @@
import 'package:flutter/foundation.dart';
extension CurrentPlatform on TargetPlatform {
@pragma('vm:prefer-inline')
static bool get isIOS => defaultTargetPlatform == TargetPlatform.iOS;
@pragma('vm:prefer-inline')
static bool get isAndroid => defaultTargetPlatform == TargetPlatform.android;
}
@@ -1,6 +1,7 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:intl/message_format.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/utils/debug_print.dart';
extension StringTranslateExtension on String {
String t({BuildContext? context, Map<String, Object>? args}) {
@@ -39,7 +40,7 @@ String _translateHelper(BuildContext? context, String key, [Map<String, Object>?
? MessageFormat(translatedMessage, locale: Intl.defaultLocale ?? 'en').format(args)
: translatedMessage;
} catch (e) {
debugPrint('Translation failed for key "$key". Error: $e');
dPrint(() => 'Translation failed for key "$key". Error: $e');
return key;
}
}
@@ -0,0 +1,27 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
class AuthUserEntity extends Table with DriftDefaultsMixin {
const AuthUserEntity();
TextColumn get id => text()();
TextColumn get name => text()();
TextColumn get email => text()();
BoolColumn get isAdmin => boolean().withDefault(const Constant(false))();
// Profile image
BoolColumn get hasProfileImage => boolean().withDefault(const Constant(false))();
DateTimeColumn get profileChangedAt => dateTime().withDefault(currentDateAndTime)();
IntColumn get avatarColor => intEnum<AvatarColor>()();
// Quota
IntColumn get quotaSizeInBytes => integer().withDefault(const Constant(0))();
IntColumn get quotaUsageInBytes => integer().withDefault(const Constant(0))();
// Locked Folder
TextColumn get pinCode => text().nullable()();
@override
Set<Column> get primaryKey => {id};
}
@@ -0,0 +1,933 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart'
as i1;
import 'package:immich_mobile/domain/models/user.model.dart' as i2;
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.dart'
as i3;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
typedef $$AuthUserEntityTableCreateCompanionBuilder =
i1.AuthUserEntityCompanion Function({
required String id,
required String name,
required String email,
i0.Value<bool> isAdmin,
i0.Value<bool> hasProfileImage,
i0.Value<DateTime> profileChangedAt,
required i2.AvatarColor avatarColor,
i0.Value<int> quotaSizeInBytes,
i0.Value<int> quotaUsageInBytes,
i0.Value<String?> pinCode,
});
typedef $$AuthUserEntityTableUpdateCompanionBuilder =
i1.AuthUserEntityCompanion Function({
i0.Value<String> id,
i0.Value<String> name,
i0.Value<String> email,
i0.Value<bool> isAdmin,
i0.Value<bool> hasProfileImage,
i0.Value<DateTime> profileChangedAt,
i0.Value<i2.AvatarColor> avatarColor,
i0.Value<int> quotaSizeInBytes,
i0.Value<int> quotaUsageInBytes,
i0.Value<String?> pinCode,
});
class $$AuthUserEntityTableFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$AuthUserEntityTable> {
$$AuthUserEntityTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnFilters<String> get id => $composableBuilder(
column: $table.id,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<String> get name => $composableBuilder(
column: $table.name,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<String> get email => $composableBuilder(
column: $table.email,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<bool> get isAdmin => $composableBuilder(
column: $table.isAdmin,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<bool> get hasProfileImage => $composableBuilder(
column: $table.hasProfileImage,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<DateTime> get profileChangedAt => $composableBuilder(
column: $table.profileChangedAt,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnWithTypeConverterFilters<i2.AvatarColor, i2.AvatarColor, int>
get avatarColor => $composableBuilder(
column: $table.avatarColor,
builder: (column) => i0.ColumnWithTypeConverterFilters(column),
);
i0.ColumnFilters<int> get quotaSizeInBytes => $composableBuilder(
column: $table.quotaSizeInBytes,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<int> get quotaUsageInBytes => $composableBuilder(
column: $table.quotaUsageInBytes,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<String> get pinCode => $composableBuilder(
column: $table.pinCode,
builder: (column) => i0.ColumnFilters(column),
);
}
class $$AuthUserEntityTableOrderingComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$AuthUserEntityTable> {
$$AuthUserEntityTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<String> get id => $composableBuilder(
column: $table.id,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<String> get name => $composableBuilder(
column: $table.name,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<String> get email => $composableBuilder(
column: $table.email,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<bool> get isAdmin => $composableBuilder(
column: $table.isAdmin,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<bool> get hasProfileImage => $composableBuilder(
column: $table.hasProfileImage,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<DateTime> get profileChangedAt => $composableBuilder(
column: $table.profileChangedAt,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<int> get avatarColor => $composableBuilder(
column: $table.avatarColor,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<int> get quotaSizeInBytes => $composableBuilder(
column: $table.quotaSizeInBytes,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<int> get quotaUsageInBytes => $composableBuilder(
column: $table.quotaUsageInBytes,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<String> get pinCode => $composableBuilder(
column: $table.pinCode,
builder: (column) => i0.ColumnOrderings(column),
);
}
class $$AuthUserEntityTableAnnotationComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$AuthUserEntityTable> {
$$AuthUserEntityTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumn<String> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
i0.GeneratedColumn<String> get name =>
$composableBuilder(column: $table.name, builder: (column) => column);
i0.GeneratedColumn<String> get email =>
$composableBuilder(column: $table.email, builder: (column) => column);
i0.GeneratedColumn<bool> get isAdmin =>
$composableBuilder(column: $table.isAdmin, builder: (column) => column);
i0.GeneratedColumn<bool> get hasProfileImage => $composableBuilder(
column: $table.hasProfileImage,
builder: (column) => column,
);
i0.GeneratedColumn<DateTime> get profileChangedAt => $composableBuilder(
column: $table.profileChangedAt,
builder: (column) => column,
);
i0.GeneratedColumnWithTypeConverter<i2.AvatarColor, int> get avatarColor =>
$composableBuilder(
column: $table.avatarColor,
builder: (column) => column,
);
i0.GeneratedColumn<int> get quotaSizeInBytes => $composableBuilder(
column: $table.quotaSizeInBytes,
builder: (column) => column,
);
i0.GeneratedColumn<int> get quotaUsageInBytes => $composableBuilder(
column: $table.quotaUsageInBytes,
builder: (column) => column,
);
i0.GeneratedColumn<String> get pinCode =>
$composableBuilder(column: $table.pinCode, builder: (column) => column);
}
class $$AuthUserEntityTableTableManager
extends
i0.RootTableManager<
i0.GeneratedDatabase,
i1.$AuthUserEntityTable,
i1.AuthUserEntityData,
i1.$$AuthUserEntityTableFilterComposer,
i1.$$AuthUserEntityTableOrderingComposer,
i1.$$AuthUserEntityTableAnnotationComposer,
$$AuthUserEntityTableCreateCompanionBuilder,
$$AuthUserEntityTableUpdateCompanionBuilder,
(
i1.AuthUserEntityData,
i0.BaseReferences<
i0.GeneratedDatabase,
i1.$AuthUserEntityTable,
i1.AuthUserEntityData
>,
),
i1.AuthUserEntityData,
i0.PrefetchHooks Function()
> {
$$AuthUserEntityTableTableManager(
i0.GeneratedDatabase db,
i1.$AuthUserEntityTable table,
) : super(
i0.TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
i1.$$AuthUserEntityTableFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
i1.$$AuthUserEntityTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () => i1
.$$AuthUserEntityTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback:
({
i0.Value<String> id = const i0.Value.absent(),
i0.Value<String> name = const i0.Value.absent(),
i0.Value<String> email = const i0.Value.absent(),
i0.Value<bool> isAdmin = const i0.Value.absent(),
i0.Value<bool> hasProfileImage = const i0.Value.absent(),
i0.Value<DateTime> profileChangedAt = const i0.Value.absent(),
i0.Value<i2.AvatarColor> avatarColor = const i0.Value.absent(),
i0.Value<int> quotaSizeInBytes = const i0.Value.absent(),
i0.Value<int> quotaUsageInBytes = const i0.Value.absent(),
i0.Value<String?> pinCode = const i0.Value.absent(),
}) => i1.AuthUserEntityCompanion(
id: id,
name: name,
email: email,
isAdmin: isAdmin,
hasProfileImage: hasProfileImage,
profileChangedAt: profileChangedAt,
avatarColor: avatarColor,
quotaSizeInBytes: quotaSizeInBytes,
quotaUsageInBytes: quotaUsageInBytes,
pinCode: pinCode,
),
createCompanionCallback:
({
required String id,
required String name,
required String email,
i0.Value<bool> isAdmin = const i0.Value.absent(),
i0.Value<bool> hasProfileImage = const i0.Value.absent(),
i0.Value<DateTime> profileChangedAt = const i0.Value.absent(),
required i2.AvatarColor avatarColor,
i0.Value<int> quotaSizeInBytes = const i0.Value.absent(),
i0.Value<int> quotaUsageInBytes = const i0.Value.absent(),
i0.Value<String?> pinCode = const i0.Value.absent(),
}) => i1.AuthUserEntityCompanion.insert(
id: id,
name: name,
email: email,
isAdmin: isAdmin,
hasProfileImage: hasProfileImage,
profileChangedAt: profileChangedAt,
avatarColor: avatarColor,
quotaSizeInBytes: quotaSizeInBytes,
quotaUsageInBytes: quotaUsageInBytes,
pinCode: pinCode,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
),
);
}
typedef $$AuthUserEntityTableProcessedTableManager =
i0.ProcessedTableManager<
i0.GeneratedDatabase,
i1.$AuthUserEntityTable,
i1.AuthUserEntityData,
i1.$$AuthUserEntityTableFilterComposer,
i1.$$AuthUserEntityTableOrderingComposer,
i1.$$AuthUserEntityTableAnnotationComposer,
$$AuthUserEntityTableCreateCompanionBuilder,
$$AuthUserEntityTableUpdateCompanionBuilder,
(
i1.AuthUserEntityData,
i0.BaseReferences<
i0.GeneratedDatabase,
i1.$AuthUserEntityTable,
i1.AuthUserEntityData
>,
),
i1.AuthUserEntityData,
i0.PrefetchHooks Function()
>;
class $AuthUserEntityTable extends i3.AuthUserEntity
with i0.TableInfo<$AuthUserEntityTable, i1.AuthUserEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
$AuthUserEntityTable(this.attachedDatabase, [this._alias]);
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
@override
late final i0.GeneratedColumn<String> id = i0.GeneratedColumn<String>(
'id',
aliasedName,
false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
);
static const i0.VerificationMeta _nameMeta = const i0.VerificationMeta(
'name',
);
@override
late final i0.GeneratedColumn<String> name = i0.GeneratedColumn<String>(
'name',
aliasedName,
false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
);
static const i0.VerificationMeta _emailMeta = const i0.VerificationMeta(
'email',
);
@override
late final i0.GeneratedColumn<String> email = i0.GeneratedColumn<String>(
'email',
aliasedName,
false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
);
static const i0.VerificationMeta _isAdminMeta = const i0.VerificationMeta(
'isAdmin',
);
@override
late final i0.GeneratedColumn<bool> isAdmin = i0.GeneratedColumn<bool>(
'is_admin',
aliasedName,
false,
type: i0.DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'CHECK ("is_admin" IN (0, 1))',
),
defaultValue: const i4.Constant(false),
);
static const i0.VerificationMeta _hasProfileImageMeta =
const i0.VerificationMeta('hasProfileImage');
@override
late final i0.GeneratedColumn<bool> hasProfileImage =
i0.GeneratedColumn<bool>(
'has_profile_image',
aliasedName,
false,
type: i0.DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'CHECK ("has_profile_image" IN (0, 1))',
),
defaultValue: const i4.Constant(false),
);
static const i0.VerificationMeta _profileChangedAtMeta =
const i0.VerificationMeta('profileChangedAt');
@override
late final i0.GeneratedColumn<DateTime> profileChangedAt =
i0.GeneratedColumn<DateTime>(
'profile_changed_at',
aliasedName,
false,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: i4.currentDateAndTime,
);
@override
late final i0.GeneratedColumnWithTypeConverter<i2.AvatarColor, int>
avatarColor =
i0.GeneratedColumn<int>(
'avatar_color',
aliasedName,
false,
type: i0.DriftSqlType.int,
requiredDuringInsert: true,
).withConverter<i2.AvatarColor>(
i1.$AuthUserEntityTable.$converteravatarColor,
);
static const i0.VerificationMeta _quotaSizeInBytesMeta =
const i0.VerificationMeta('quotaSizeInBytes');
@override
late final i0.GeneratedColumn<int> quotaSizeInBytes = i0.GeneratedColumn<int>(
'quota_size_in_bytes',
aliasedName,
false,
type: i0.DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: const i4.Constant(0),
);
static const i0.VerificationMeta _quotaUsageInBytesMeta =
const i0.VerificationMeta('quotaUsageInBytes');
@override
late final i0.GeneratedColumn<int> quotaUsageInBytes =
i0.GeneratedColumn<int>(
'quota_usage_in_bytes',
aliasedName,
false,
type: i0.DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: const i4.Constant(0),
);
static const i0.VerificationMeta _pinCodeMeta = const i0.VerificationMeta(
'pinCode',
);
@override
late final i0.GeneratedColumn<String> pinCode = i0.GeneratedColumn<String>(
'pin_code',
aliasedName,
true,
type: i0.DriftSqlType.string,
requiredDuringInsert: false,
);
@override
List<i0.GeneratedColumn> get $columns => [
id,
name,
email,
isAdmin,
hasProfileImage,
profileChangedAt,
avatarColor,
quotaSizeInBytes,
quotaUsageInBytes,
pinCode,
];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'auth_user_entity';
@override
i0.VerificationContext validateIntegrity(
i0.Insertable<i1.AuthUserEntityData> instance, {
bool isInserting = false,
}) {
final context = i0.VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
} else if (isInserting) {
context.missing(_idMeta);
}
if (data.containsKey('name')) {
context.handle(
_nameMeta,
name.isAcceptableOrUnknown(data['name']!, _nameMeta),
);
} else if (isInserting) {
context.missing(_nameMeta);
}
if (data.containsKey('email')) {
context.handle(
_emailMeta,
email.isAcceptableOrUnknown(data['email']!, _emailMeta),
);
} else if (isInserting) {
context.missing(_emailMeta);
}
if (data.containsKey('is_admin')) {
context.handle(
_isAdminMeta,
isAdmin.isAcceptableOrUnknown(data['is_admin']!, _isAdminMeta),
);
}
if (data.containsKey('has_profile_image')) {
context.handle(
_hasProfileImageMeta,
hasProfileImage.isAcceptableOrUnknown(
data['has_profile_image']!,
_hasProfileImageMeta,
),
);
}
if (data.containsKey('profile_changed_at')) {
context.handle(
_profileChangedAtMeta,
profileChangedAt.isAcceptableOrUnknown(
data['profile_changed_at']!,
_profileChangedAtMeta,
),
);
}
if (data.containsKey('quota_size_in_bytes')) {
context.handle(
_quotaSizeInBytesMeta,
quotaSizeInBytes.isAcceptableOrUnknown(
data['quota_size_in_bytes']!,
_quotaSizeInBytesMeta,
),
);
}
if (data.containsKey('quota_usage_in_bytes')) {
context.handle(
_quotaUsageInBytesMeta,
quotaUsageInBytes.isAcceptableOrUnknown(
data['quota_usage_in_bytes']!,
_quotaUsageInBytesMeta,
),
);
}
if (data.containsKey('pin_code')) {
context.handle(
_pinCodeMeta,
pinCode.isAcceptableOrUnknown(data['pin_code']!, _pinCodeMeta),
);
}
return context;
}
@override
Set<i0.GeneratedColumn> get $primaryKey => {id};
@override
i1.AuthUserEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.AuthUserEntityData(
id: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}id'],
)!,
name: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}name'],
)!,
email: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}email'],
)!,
isAdmin: attachedDatabase.typeMapping.read(
i0.DriftSqlType.bool,
data['${effectivePrefix}is_admin'],
)!,
hasProfileImage: attachedDatabase.typeMapping.read(
i0.DriftSqlType.bool,
data['${effectivePrefix}has_profile_image'],
)!,
profileChangedAt: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime,
data['${effectivePrefix}profile_changed_at'],
)!,
avatarColor: i1.$AuthUserEntityTable.$converteravatarColor.fromSql(
attachedDatabase.typeMapping.read(
i0.DriftSqlType.int,
data['${effectivePrefix}avatar_color'],
)!,
),
quotaSizeInBytes: attachedDatabase.typeMapping.read(
i0.DriftSqlType.int,
data['${effectivePrefix}quota_size_in_bytes'],
)!,
quotaUsageInBytes: attachedDatabase.typeMapping.read(
i0.DriftSqlType.int,
data['${effectivePrefix}quota_usage_in_bytes'],
)!,
pinCode: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}pin_code'],
),
);
}
@override
$AuthUserEntityTable createAlias(String alias) {
return $AuthUserEntityTable(attachedDatabase, alias);
}
static i0.JsonTypeConverter2<i2.AvatarColor, int, int> $converteravatarColor =
const i0.EnumIndexConverter<i2.AvatarColor>(i2.AvatarColor.values);
@override
bool get withoutRowId => true;
@override
bool get isStrict => true;
}
class AuthUserEntityData extends i0.DataClass
implements i0.Insertable<i1.AuthUserEntityData> {
final String id;
final String name;
final String email;
final bool isAdmin;
final bool hasProfileImage;
final DateTime profileChangedAt;
final i2.AvatarColor avatarColor;
final int quotaSizeInBytes;
final int quotaUsageInBytes;
final String? pinCode;
const AuthUserEntityData({
required this.id,
required this.name,
required this.email,
required this.isAdmin,
required this.hasProfileImage,
required this.profileChangedAt,
required this.avatarColor,
required this.quotaSizeInBytes,
required this.quotaUsageInBytes,
this.pinCode,
});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['id'] = i0.Variable<String>(id);
map['name'] = i0.Variable<String>(name);
map['email'] = i0.Variable<String>(email);
map['is_admin'] = i0.Variable<bool>(isAdmin);
map['has_profile_image'] = i0.Variable<bool>(hasProfileImage);
map['profile_changed_at'] = i0.Variable<DateTime>(profileChangedAt);
{
map['avatar_color'] = i0.Variable<int>(
i1.$AuthUserEntityTable.$converteravatarColor.toSql(avatarColor),
);
}
map['quota_size_in_bytes'] = i0.Variable<int>(quotaSizeInBytes);
map['quota_usage_in_bytes'] = i0.Variable<int>(quotaUsageInBytes);
if (!nullToAbsent || pinCode != null) {
map['pin_code'] = i0.Variable<String>(pinCode);
}
return map;
}
factory AuthUserEntityData.fromJson(
Map<String, dynamic> json, {
i0.ValueSerializer? serializer,
}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return AuthUserEntityData(
id: serializer.fromJson<String>(json['id']),
name: serializer.fromJson<String>(json['name']),
email: serializer.fromJson<String>(json['email']),
isAdmin: serializer.fromJson<bool>(json['isAdmin']),
hasProfileImage: serializer.fromJson<bool>(json['hasProfileImage']),
profileChangedAt: serializer.fromJson<DateTime>(json['profileChangedAt']),
avatarColor: i1.$AuthUserEntityTable.$converteravatarColor.fromJson(
serializer.fromJson<int>(json['avatarColor']),
),
quotaSizeInBytes: serializer.fromJson<int>(json['quotaSizeInBytes']),
quotaUsageInBytes: serializer.fromJson<int>(json['quotaUsageInBytes']),
pinCode: serializer.fromJson<String?>(json['pinCode']),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<String>(id),
'name': serializer.toJson<String>(name),
'email': serializer.toJson<String>(email),
'isAdmin': serializer.toJson<bool>(isAdmin),
'hasProfileImage': serializer.toJson<bool>(hasProfileImage),
'profileChangedAt': serializer.toJson<DateTime>(profileChangedAt),
'avatarColor': serializer.toJson<int>(
i1.$AuthUserEntityTable.$converteravatarColor.toJson(avatarColor),
),
'quotaSizeInBytes': serializer.toJson<int>(quotaSizeInBytes),
'quotaUsageInBytes': serializer.toJson<int>(quotaUsageInBytes),
'pinCode': serializer.toJson<String?>(pinCode),
};
}
i1.AuthUserEntityData copyWith({
String? id,
String? name,
String? email,
bool? isAdmin,
bool? hasProfileImage,
DateTime? profileChangedAt,
i2.AvatarColor? avatarColor,
int? quotaSizeInBytes,
int? quotaUsageInBytes,
i0.Value<String?> pinCode = const i0.Value.absent(),
}) => i1.AuthUserEntityData(
id: id ?? this.id,
name: name ?? this.name,
email: email ?? this.email,
isAdmin: isAdmin ?? this.isAdmin,
hasProfileImage: hasProfileImage ?? this.hasProfileImage,
profileChangedAt: profileChangedAt ?? this.profileChangedAt,
avatarColor: avatarColor ?? this.avatarColor,
quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes,
quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes,
pinCode: pinCode.present ? pinCode.value : this.pinCode,
);
AuthUserEntityData copyWithCompanion(i1.AuthUserEntityCompanion data) {
return AuthUserEntityData(
id: data.id.present ? data.id.value : this.id,
name: data.name.present ? data.name.value : this.name,
email: data.email.present ? data.email.value : this.email,
isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin,
hasProfileImage: data.hasProfileImage.present
? data.hasProfileImage.value
: this.hasProfileImage,
profileChangedAt: data.profileChangedAt.present
? data.profileChangedAt.value
: this.profileChangedAt,
avatarColor: data.avatarColor.present
? data.avatarColor.value
: this.avatarColor,
quotaSizeInBytes: data.quotaSizeInBytes.present
? data.quotaSizeInBytes.value
: this.quotaSizeInBytes,
quotaUsageInBytes: data.quotaUsageInBytes.present
? data.quotaUsageInBytes.value
: this.quotaUsageInBytes,
pinCode: data.pinCode.present ? data.pinCode.value : this.pinCode,
);
}
@override
String toString() {
return (StringBuffer('AuthUserEntityData(')
..write('id: $id, ')
..write('name: $name, ')
..write('email: $email, ')
..write('isAdmin: $isAdmin, ')
..write('hasProfileImage: $hasProfileImage, ')
..write('profileChangedAt: $profileChangedAt, ')
..write('avatarColor: $avatarColor, ')
..write('quotaSizeInBytes: $quotaSizeInBytes, ')
..write('quotaUsageInBytes: $quotaUsageInBytes, ')
..write('pinCode: $pinCode')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(
id,
name,
email,
isAdmin,
hasProfileImage,
profileChangedAt,
avatarColor,
quotaSizeInBytes,
quotaUsageInBytes,
pinCode,
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.AuthUserEntityData &&
other.id == this.id &&
other.name == this.name &&
other.email == this.email &&
other.isAdmin == this.isAdmin &&
other.hasProfileImage == this.hasProfileImage &&
other.profileChangedAt == this.profileChangedAt &&
other.avatarColor == this.avatarColor &&
other.quotaSizeInBytes == this.quotaSizeInBytes &&
other.quotaUsageInBytes == this.quotaUsageInBytes &&
other.pinCode == this.pinCode);
}
class AuthUserEntityCompanion
extends i0.UpdateCompanion<i1.AuthUserEntityData> {
final i0.Value<String> id;
final i0.Value<String> name;
final i0.Value<String> email;
final i0.Value<bool> isAdmin;
final i0.Value<bool> hasProfileImage;
final i0.Value<DateTime> profileChangedAt;
final i0.Value<i2.AvatarColor> avatarColor;
final i0.Value<int> quotaSizeInBytes;
final i0.Value<int> quotaUsageInBytes;
final i0.Value<String?> pinCode;
const AuthUserEntityCompanion({
this.id = const i0.Value.absent(),
this.name = const i0.Value.absent(),
this.email = const i0.Value.absent(),
this.isAdmin = const i0.Value.absent(),
this.hasProfileImage = const i0.Value.absent(),
this.profileChangedAt = const i0.Value.absent(),
this.avatarColor = const i0.Value.absent(),
this.quotaSizeInBytes = const i0.Value.absent(),
this.quotaUsageInBytes = const i0.Value.absent(),
this.pinCode = const i0.Value.absent(),
});
AuthUserEntityCompanion.insert({
required String id,
required String name,
required String email,
this.isAdmin = const i0.Value.absent(),
this.hasProfileImage = const i0.Value.absent(),
this.profileChangedAt = const i0.Value.absent(),
required i2.AvatarColor avatarColor,
this.quotaSizeInBytes = const i0.Value.absent(),
this.quotaUsageInBytes = const i0.Value.absent(),
this.pinCode = const i0.Value.absent(),
}) : id = i0.Value(id),
name = i0.Value(name),
email = i0.Value(email),
avatarColor = i0.Value(avatarColor);
static i0.Insertable<i1.AuthUserEntityData> custom({
i0.Expression<String>? id,
i0.Expression<String>? name,
i0.Expression<String>? email,
i0.Expression<bool>? isAdmin,
i0.Expression<bool>? hasProfileImage,
i0.Expression<DateTime>? profileChangedAt,
i0.Expression<int>? avatarColor,
i0.Expression<int>? quotaSizeInBytes,
i0.Expression<int>? quotaUsageInBytes,
i0.Expression<String>? pinCode,
}) {
return i0.RawValuesInsertable({
if (id != null) 'id': id,
if (name != null) 'name': name,
if (email != null) 'email': email,
if (isAdmin != null) 'is_admin': isAdmin,
if (hasProfileImage != null) 'has_profile_image': hasProfileImage,
if (profileChangedAt != null) 'profile_changed_at': profileChangedAt,
if (avatarColor != null) 'avatar_color': avatarColor,
if (quotaSizeInBytes != null) 'quota_size_in_bytes': quotaSizeInBytes,
if (quotaUsageInBytes != null) 'quota_usage_in_bytes': quotaUsageInBytes,
if (pinCode != null) 'pin_code': pinCode,
});
}
i1.AuthUserEntityCompanion copyWith({
i0.Value<String>? id,
i0.Value<String>? name,
i0.Value<String>? email,
i0.Value<bool>? isAdmin,
i0.Value<bool>? hasProfileImage,
i0.Value<DateTime>? profileChangedAt,
i0.Value<i2.AvatarColor>? avatarColor,
i0.Value<int>? quotaSizeInBytes,
i0.Value<int>? quotaUsageInBytes,
i0.Value<String?>? pinCode,
}) {
return i1.AuthUserEntityCompanion(
id: id ?? this.id,
name: name ?? this.name,
email: email ?? this.email,
isAdmin: isAdmin ?? this.isAdmin,
hasProfileImage: hasProfileImage ?? this.hasProfileImage,
profileChangedAt: profileChangedAt ?? this.profileChangedAt,
avatarColor: avatarColor ?? this.avatarColor,
quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes,
quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes,
pinCode: pinCode ?? this.pinCode,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (id.present) {
map['id'] = i0.Variable<String>(id.value);
}
if (name.present) {
map['name'] = i0.Variable<String>(name.value);
}
if (email.present) {
map['email'] = i0.Variable<String>(email.value);
}
if (isAdmin.present) {
map['is_admin'] = i0.Variable<bool>(isAdmin.value);
}
if (hasProfileImage.present) {
map['has_profile_image'] = i0.Variable<bool>(hasProfileImage.value);
}
if (profileChangedAt.present) {
map['profile_changed_at'] = i0.Variable<DateTime>(profileChangedAt.value);
}
if (avatarColor.present) {
map['avatar_color'] = i0.Variable<int>(
i1.$AuthUserEntityTable.$converteravatarColor.toSql(avatarColor.value),
);
}
if (quotaSizeInBytes.present) {
map['quota_size_in_bytes'] = i0.Variable<int>(quotaSizeInBytes.value);
}
if (quotaUsageInBytes.present) {
map['quota_usage_in_bytes'] = i0.Variable<int>(quotaUsageInBytes.value);
}
if (pinCode.present) {
map['pin_code'] = i0.Variable<String>(pinCode.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('AuthUserEntityCompanion(')
..write('id: $id, ')
..write('name: $name, ')
..write('email: $email, ')
..write('isAdmin: $isAdmin, ')
..write('hasProfileImage: $hasProfileImage, ')
..write('profileChangedAt: $profileChangedAt, ')
..write('avatarColor: $avatarColor, ')
..write('quotaSizeInBytes: $quotaSizeInBytes, ')
..write('quotaUsageInBytes: $quotaUsageInBytes, ')
..write('pinCode: $pinCode')
..write(')'))
.toString();
}
}
@@ -1,6 +1,5 @@
import 'package:drift/drift.dart' hide Index;
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:isar/isar.dart';
@@ -44,7 +43,7 @@ class User {
static User fromDto(UserDto dto) => User(
id: dto.id,
updatedAt: dto.updatedAt,
updatedAt: dto.updatedAt ?? DateTime(2025),
email: dto.email,
name: dto.name,
isAdmin: dto.isAdmin,
@@ -81,13 +80,12 @@ class UserEntity extends Table with DriftDefaultsMixin {
TextColumn get id => text()();
TextColumn get name => text()();
BoolColumn get isAdmin => boolean().withDefault(const Constant(false))();
TextColumn get email => text()();
// Profile image
BoolColumn get hasProfileImage => boolean().withDefault(const Constant(false))();
DateTimeColumn get profileChangedAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
IntColumn get avatarColor => intEnum<AvatarColor>().withDefault(const Constant(0))();
@override
Set<Column> get primaryKey => {id};
+74 -129
View File
@@ -3,28 +3,27 @@
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
as i1;
import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as i2;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
import 'package:immich_mobile/domain/models/user.model.dart' as i2;
import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as i3;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
typedef $$UserEntityTableCreateCompanionBuilder =
i1.UserEntityCompanion Function({
required String id,
required String name,
i0.Value<bool> isAdmin,
required String email,
i0.Value<bool> hasProfileImage,
i0.Value<DateTime> profileChangedAt,
i0.Value<DateTime> updatedAt,
i0.Value<i2.AvatarColor> avatarColor,
});
typedef $$UserEntityTableUpdateCompanionBuilder =
i1.UserEntityCompanion Function({
i0.Value<String> id,
i0.Value<String> name,
i0.Value<bool> isAdmin,
i0.Value<String> email,
i0.Value<bool> hasProfileImage,
i0.Value<DateTime> profileChangedAt,
i0.Value<DateTime> updatedAt,
i0.Value<i2.AvatarColor> avatarColor,
});
class $$UserEntityTableFilterComposer
@@ -46,11 +45,6 @@ class $$UserEntityTableFilterComposer
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<bool> get isAdmin => $composableBuilder(
column: $table.isAdmin,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<String> get email => $composableBuilder(
column: $table.email,
builder: (column) => i0.ColumnFilters(column),
@@ -66,9 +60,10 @@ class $$UserEntityTableFilterComposer
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<DateTime> get updatedAt => $composableBuilder(
column: $table.updatedAt,
builder: (column) => i0.ColumnFilters(column),
i0.ColumnWithTypeConverterFilters<i2.AvatarColor, i2.AvatarColor, int>
get avatarColor => $composableBuilder(
column: $table.avatarColor,
builder: (column) => i0.ColumnWithTypeConverterFilters(column),
);
}
@@ -91,11 +86,6 @@ class $$UserEntityTableOrderingComposer
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<bool> get isAdmin => $composableBuilder(
column: $table.isAdmin,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<String> get email => $composableBuilder(
column: $table.email,
builder: (column) => i0.ColumnOrderings(column),
@@ -111,8 +101,8 @@ class $$UserEntityTableOrderingComposer
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<DateTime> get updatedAt => $composableBuilder(
column: $table.updatedAt,
i0.ColumnOrderings<int> get avatarColor => $composableBuilder(
column: $table.avatarColor,
builder: (column) => i0.ColumnOrderings(column),
);
}
@@ -132,9 +122,6 @@ class $$UserEntityTableAnnotationComposer
i0.GeneratedColumn<String> get name =>
$composableBuilder(column: $table.name, builder: (column) => column);
i0.GeneratedColumn<bool> get isAdmin =>
$composableBuilder(column: $table.isAdmin, builder: (column) => column);
i0.GeneratedColumn<String> get email =>
$composableBuilder(column: $table.email, builder: (column) => column);
@@ -148,8 +135,11 @@ class $$UserEntityTableAnnotationComposer
builder: (column) => column,
);
i0.GeneratedColumn<DateTime> get updatedAt =>
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
i0.GeneratedColumnWithTypeConverter<i2.AvatarColor, int> get avatarColor =>
$composableBuilder(
column: $table.avatarColor,
builder: (column) => column,
);
}
class $$UserEntityTableTableManager
@@ -191,37 +181,33 @@ class $$UserEntityTableTableManager
({
i0.Value<String> id = const i0.Value.absent(),
i0.Value<String> name = const i0.Value.absent(),
i0.Value<bool> isAdmin = const i0.Value.absent(),
i0.Value<String> email = const i0.Value.absent(),
i0.Value<bool> hasProfileImage = const i0.Value.absent(),
i0.Value<DateTime> profileChangedAt = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
i0.Value<i2.AvatarColor> avatarColor = const i0.Value.absent(),
}) => i1.UserEntityCompanion(
id: id,
name: name,
isAdmin: isAdmin,
email: email,
hasProfileImage: hasProfileImage,
profileChangedAt: profileChangedAt,
updatedAt: updatedAt,
avatarColor: avatarColor,
),
createCompanionCallback:
({
required String id,
required String name,
i0.Value<bool> isAdmin = const i0.Value.absent(),
required String email,
i0.Value<bool> hasProfileImage = const i0.Value.absent(),
i0.Value<DateTime> profileChangedAt = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
i0.Value<i2.AvatarColor> avatarColor = const i0.Value.absent(),
}) => i1.UserEntityCompanion.insert(
id: id,
name: name,
isAdmin: isAdmin,
email: email,
hasProfileImage: hasProfileImage,
profileChangedAt: profileChangedAt,
updatedAt: updatedAt,
avatarColor: avatarColor,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
@@ -253,7 +239,7 @@ typedef $$UserEntityTableProcessedTableManager =
i0.PrefetchHooks Function()
>;
class $UserEntityTable extends i2.UserEntity
class $UserEntityTable extends i3.UserEntity
with i0.TableInfo<$UserEntityTable, i1.UserEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
@@ -279,21 +265,6 @@ class $UserEntityTable extends i2.UserEntity
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
);
static const i0.VerificationMeta _isAdminMeta = const i0.VerificationMeta(
'isAdmin',
);
@override
late final i0.GeneratedColumn<bool> isAdmin = i0.GeneratedColumn<bool>(
'is_admin',
aliasedName,
false,
type: i0.DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'CHECK ("is_admin" IN (0, 1))',
),
defaultValue: const i3.Constant(false),
);
static const i0.VerificationMeta _emailMeta = const i0.VerificationMeta(
'email',
);
@@ -318,7 +289,7 @@ class $UserEntityTable extends i2.UserEntity
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'CHECK ("has_profile_image" IN (0, 1))',
),
defaultValue: const i3.Constant(false),
defaultValue: const i4.Constant(false),
);
static const i0.VerificationMeta _profileChangedAtMeta =
const i0.VerificationMeta('profileChangedAt');
@@ -330,30 +301,26 @@ class $UserEntityTable extends i2.UserEntity
false,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: i3.currentDateAndTime,
defaultValue: i4.currentDateAndTime,
);
static const i0.VerificationMeta _updatedAtMeta = const i0.VerificationMeta(
'updatedAt',
);
@override
late final i0.GeneratedColumn<DateTime> updatedAt =
i0.GeneratedColumn<DateTime>(
'updated_at',
aliasedName,
false,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: i3.currentDateAndTime,
);
late final i0.GeneratedColumnWithTypeConverter<i2.AvatarColor, int>
avatarColor = i0.GeneratedColumn<int>(
'avatar_color',
aliasedName,
false,
type: i0.DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: const i4.Constant(0),
).withConverter<i2.AvatarColor>(i1.$UserEntityTable.$converteravatarColor);
@override
List<i0.GeneratedColumn> get $columns => [
id,
name,
isAdmin,
email,
hasProfileImage,
profileChangedAt,
updatedAt,
avatarColor,
];
@override
String get aliasedName => _alias ?? actualTableName;
@@ -380,12 +347,6 @@ class $UserEntityTable extends i2.UserEntity
} else if (isInserting) {
context.missing(_nameMeta);
}
if (data.containsKey('is_admin')) {
context.handle(
_isAdminMeta,
isAdmin.isAcceptableOrUnknown(data['is_admin']!, _isAdminMeta),
);
}
if (data.containsKey('email')) {
context.handle(
_emailMeta,
@@ -412,12 +373,6 @@ class $UserEntityTable extends i2.UserEntity
),
);
}
if (data.containsKey('updated_at')) {
context.handle(
_updatedAtMeta,
updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta),
);
}
return context;
}
@@ -435,10 +390,6 @@ class $UserEntityTable extends i2.UserEntity
i0.DriftSqlType.string,
data['${effectivePrefix}name'],
)!,
isAdmin: attachedDatabase.typeMapping.read(
i0.DriftSqlType.bool,
data['${effectivePrefix}is_admin'],
)!,
email: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}email'],
@@ -451,10 +402,12 @@ class $UserEntityTable extends i2.UserEntity
i0.DriftSqlType.dateTime,
data['${effectivePrefix}profile_changed_at'],
)!,
updatedAt: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime,
data['${effectivePrefix}updated_at'],
)!,
avatarColor: i1.$UserEntityTable.$converteravatarColor.fromSql(
attachedDatabase.typeMapping.read(
i0.DriftSqlType.int,
data['${effectivePrefix}avatar_color'],
)!,
),
);
}
@@ -463,6 +416,8 @@ class $UserEntityTable extends i2.UserEntity
return $UserEntityTable(attachedDatabase, alias);
}
static i0.JsonTypeConverter2<i2.AvatarColor, int, int> $converteravatarColor =
const i0.EnumIndexConverter<i2.AvatarColor>(i2.AvatarColor.values);
@override
bool get withoutRowId => true;
@override
@@ -473,30 +428,31 @@ class UserEntityData extends i0.DataClass
implements i0.Insertable<i1.UserEntityData> {
final String id;
final String name;
final bool isAdmin;
final String email;
final bool hasProfileImage;
final DateTime profileChangedAt;
final DateTime updatedAt;
final i2.AvatarColor avatarColor;
const UserEntityData({
required this.id,
required this.name,
required this.isAdmin,
required this.email,
required this.hasProfileImage,
required this.profileChangedAt,
required this.updatedAt,
required this.avatarColor,
});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['id'] = i0.Variable<String>(id);
map['name'] = i0.Variable<String>(name);
map['is_admin'] = i0.Variable<bool>(isAdmin);
map['email'] = i0.Variable<String>(email);
map['has_profile_image'] = i0.Variable<bool>(hasProfileImage);
map['profile_changed_at'] = i0.Variable<DateTime>(profileChangedAt);
map['updated_at'] = i0.Variable<DateTime>(updatedAt);
{
map['avatar_color'] = i0.Variable<int>(
i1.$UserEntityTable.$converteravatarColor.toSql(avatarColor),
);
}
return map;
}
@@ -508,11 +464,12 @@ class UserEntityData extends i0.DataClass
return UserEntityData(
id: serializer.fromJson<String>(json['id']),
name: serializer.fromJson<String>(json['name']),
isAdmin: serializer.fromJson<bool>(json['isAdmin']),
email: serializer.fromJson<String>(json['email']),
hasProfileImage: serializer.fromJson<bool>(json['hasProfileImage']),
profileChangedAt: serializer.fromJson<DateTime>(json['profileChangedAt']),
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
avatarColor: i1.$UserEntityTable.$converteravatarColor.fromJson(
serializer.fromJson<int>(json['avatarColor']),
),
);
}
@override
@@ -521,36 +478,34 @@ class UserEntityData extends i0.DataClass
return <String, dynamic>{
'id': serializer.toJson<String>(id),
'name': serializer.toJson<String>(name),
'isAdmin': serializer.toJson<bool>(isAdmin),
'email': serializer.toJson<String>(email),
'hasProfileImage': serializer.toJson<bool>(hasProfileImage),
'profileChangedAt': serializer.toJson<DateTime>(profileChangedAt),
'updatedAt': serializer.toJson<DateTime>(updatedAt),
'avatarColor': serializer.toJson<int>(
i1.$UserEntityTable.$converteravatarColor.toJson(avatarColor),
),
};
}
i1.UserEntityData copyWith({
String? id,
String? name,
bool? isAdmin,
String? email,
bool? hasProfileImage,
DateTime? profileChangedAt,
DateTime? updatedAt,
i2.AvatarColor? avatarColor,
}) => i1.UserEntityData(
id: id ?? this.id,
name: name ?? this.name,
isAdmin: isAdmin ?? this.isAdmin,
email: email ?? this.email,
hasProfileImage: hasProfileImage ?? this.hasProfileImage,
profileChangedAt: profileChangedAt ?? this.profileChangedAt,
updatedAt: updatedAt ?? this.updatedAt,
avatarColor: avatarColor ?? this.avatarColor,
);
UserEntityData copyWithCompanion(i1.UserEntityCompanion data) {
return UserEntityData(
id: data.id.present ? data.id.value : this.id,
name: data.name.present ? data.name.value : this.name,
isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin,
email: data.email.present ? data.email.value : this.email,
hasProfileImage: data.hasProfileImage.present
? data.hasProfileImage.value
@@ -558,7 +513,9 @@ class UserEntityData extends i0.DataClass
profileChangedAt: data.profileChangedAt.present
? data.profileChangedAt.value
: this.profileChangedAt,
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
avatarColor: data.avatarColor.present
? data.avatarColor.value
: this.avatarColor,
);
}
@@ -567,11 +524,10 @@ class UserEntityData extends i0.DataClass
return (StringBuffer('UserEntityData(')
..write('id: $id, ')
..write('name: $name, ')
..write('isAdmin: $isAdmin, ')
..write('email: $email, ')
..write('hasProfileImage: $hasProfileImage, ')
..write('profileChangedAt: $profileChangedAt, ')
..write('updatedAt: $updatedAt')
..write('avatarColor: $avatarColor')
..write(')'))
.toString();
}
@@ -580,11 +536,10 @@ class UserEntityData extends i0.DataClass
int get hashCode => Object.hash(
id,
name,
isAdmin,
email,
hasProfileImage,
profileChangedAt,
updatedAt,
avatarColor,
);
@override
bool operator ==(Object other) =>
@@ -592,78 +547,70 @@ class UserEntityData extends i0.DataClass
(other is i1.UserEntityData &&
other.id == this.id &&
other.name == this.name &&
other.isAdmin == this.isAdmin &&
other.email == this.email &&
other.hasProfileImage == this.hasProfileImage &&
other.profileChangedAt == this.profileChangedAt &&
other.updatedAt == this.updatedAt);
other.avatarColor == this.avatarColor);
}
class UserEntityCompanion extends i0.UpdateCompanion<i1.UserEntityData> {
final i0.Value<String> id;
final i0.Value<String> name;
final i0.Value<bool> isAdmin;
final i0.Value<String> email;
final i0.Value<bool> hasProfileImage;
final i0.Value<DateTime> profileChangedAt;
final i0.Value<DateTime> updatedAt;
final i0.Value<i2.AvatarColor> avatarColor;
const UserEntityCompanion({
this.id = const i0.Value.absent(),
this.name = const i0.Value.absent(),
this.isAdmin = const i0.Value.absent(),
this.email = const i0.Value.absent(),
this.hasProfileImage = const i0.Value.absent(),
this.profileChangedAt = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(),
this.avatarColor = const i0.Value.absent(),
});
UserEntityCompanion.insert({
required String id,
required String name,
this.isAdmin = const i0.Value.absent(),
required String email,
this.hasProfileImage = const i0.Value.absent(),
this.profileChangedAt = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(),
this.avatarColor = const i0.Value.absent(),
}) : id = i0.Value(id),
name = i0.Value(name),
email = i0.Value(email);
static i0.Insertable<i1.UserEntityData> custom({
i0.Expression<String>? id,
i0.Expression<String>? name,
i0.Expression<bool>? isAdmin,
i0.Expression<String>? email,
i0.Expression<bool>? hasProfileImage,
i0.Expression<DateTime>? profileChangedAt,
i0.Expression<DateTime>? updatedAt,
i0.Expression<int>? avatarColor,
}) {
return i0.RawValuesInsertable({
if (id != null) 'id': id,
if (name != null) 'name': name,
if (isAdmin != null) 'is_admin': isAdmin,
if (email != null) 'email': email,
if (hasProfileImage != null) 'has_profile_image': hasProfileImage,
if (profileChangedAt != null) 'profile_changed_at': profileChangedAt,
if (updatedAt != null) 'updated_at': updatedAt,
if (avatarColor != null) 'avatar_color': avatarColor,
});
}
i1.UserEntityCompanion copyWith({
i0.Value<String>? id,
i0.Value<String>? name,
i0.Value<bool>? isAdmin,
i0.Value<String>? email,
i0.Value<bool>? hasProfileImage,
i0.Value<DateTime>? profileChangedAt,
i0.Value<DateTime>? updatedAt,
i0.Value<i2.AvatarColor>? avatarColor,
}) {
return i1.UserEntityCompanion(
id: id ?? this.id,
name: name ?? this.name,
isAdmin: isAdmin ?? this.isAdmin,
email: email ?? this.email,
hasProfileImage: hasProfileImage ?? this.hasProfileImage,
profileChangedAt: profileChangedAt ?? this.profileChangedAt,
updatedAt: updatedAt ?? this.updatedAt,
avatarColor: avatarColor ?? this.avatarColor,
);
}
@@ -676,9 +623,6 @@ class UserEntityCompanion extends i0.UpdateCompanion<i1.UserEntityData> {
if (name.present) {
map['name'] = i0.Variable<String>(name.value);
}
if (isAdmin.present) {
map['is_admin'] = i0.Variable<bool>(isAdmin.value);
}
if (email.present) {
map['email'] = i0.Variable<String>(email.value);
}
@@ -688,8 +632,10 @@ class UserEntityCompanion extends i0.UpdateCompanion<i1.UserEntityData> {
if (profileChangedAt.present) {
map['profile_changed_at'] = i0.Variable<DateTime>(profileChangedAt.value);
}
if (updatedAt.present) {
map['updated_at'] = i0.Variable<DateTime>(updatedAt.value);
if (avatarColor.present) {
map['avatar_color'] = i0.Variable<int>(
i1.$UserEntityTable.$converteravatarColor.toSql(avatarColor.value),
);
}
return map;
}
@@ -699,11 +645,10 @@ class UserEntityCompanion extends i0.UpdateCompanion<i1.UserEntityData> {
return (StringBuffer('UserEntityCompanion(')
..write('id: $id, ')
..write('name: $name, ')
..write('isAdmin: $isAdmin, ')
..write('email: $email, ')
..write('hasProfileImage: $hasProfileImage, ')
..write('profileChangedAt: $profileChangedAt, ')
..write('updatedAt: $updatedAt')
..write('avatarColor: $avatarColor')
..write(')'))
.toString();
}
@@ -4,7 +4,6 @@ import 'package:drift/drift.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/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
@@ -138,22 +137,4 @@ class DriftBackupRepository extends DriftDatabaseRepository {
return query.map((localAsset) => localAsset.toDto()).get();
}
FutureOr<List<LocalAlbum>> getSourceAlbums(String localAssetId) {
final query = _db.localAlbumEntity.select()
..where(
(lae) =>
existsQuery(
_db.localAlbumAssetEntity.selectOnly()
..addColumns([_db.localAlbumAssetEntity.albumId])
..where(
_db.localAlbumAssetEntity.albumId.equalsExp(lae.id) &
_db.localAlbumAssetEntity.assetId.equals(localAssetId),
),
) &
lae.backupSelection.equalsValue(BackupSelection.selected),
)
..orderBy([(lae) => OrderingTerm.asc(lae.name)]);
return query.map((localAlbum) => localAlbum.toDto()).get();
}
}
@@ -5,6 +5,7 @@ import 'package:drift_flutter/drift_flutter.dart';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.dart';
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart';
@@ -43,6 +44,7 @@ class IsarDatabaseRepository implements IDatabaseRepository {
@DriftDatabase(
tables: [
AuthUserEntity,
UserEntity,
UserMetadataEntity,
PartnerEntity,
@@ -67,8 +69,31 @@ class Drift extends $Drift implements IDatabaseRepository {
Drift([QueryExecutor? executor])
: super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true)));
Future<void> reset() async {
// https://github.com/simolus3/drift/commit/bd80a46264b6dd833ef4fd87fffc03f5a832ab41#diff-3f879e03b4a35779344ef16170b9353608dd9c42385f5402ec6035aac4dd8a04R76-R94
await exclusively(() async {
// https://stackoverflow.com/a/65743498/25690041
await customStatement('PRAGMA writable_schema = 1;');
await customStatement('DELETE FROM sqlite_master;');
await customStatement('VACUUM;');
await customStatement('PRAGMA writable_schema = 0;');
await customStatement('PRAGMA integrity_check');
await customStatement('PRAGMA user_version = 0');
await beforeOpen(
// ignore: invalid_use_of_internal_member
resolvedEngine.executor,
OpeningDetails(null, schemaVersion),
);
await customStatement('PRAGMA user_version = $schemaVersion');
// Refresh all stream queries
notifyUpdates({for (final table in allTables) TableUpdate.onTable(table)});
});
}
@override
int get schemaVersion => 9;
int get schemaVersion => 10;
@override
MigrationStrategy get migration => MigrationStrategy(
@@ -126,6 +151,11 @@ class Drift extends $Drift implements IDatabaseRepository {
from8To9: (m, v9) async {
await m.addColumn(v9.localAlbumEntity, v9.localAlbumEntity.linkedRemoteAlbumId);
},
from9To10: (m, v10) async {
await m.createTable(v10.authUserEntity);
await m.addColumn(v10.userEntity, v10.userEntity.avatarColor);
await m.alterTable(TableMigration(v10.userEntity));
},
),
);
@@ -141,7 +171,7 @@ class Drift extends $Drift implements IDatabaseRepository {
await customStatement('PRAGMA foreign_keys = ON');
await customStatement('PRAGMA synchronous = NORMAL');
await customStatement('PRAGMA journal_mode = WAL');
await customStatement('PRAGMA busy_timeout = 500');
await customStatement('PRAGMA busy_timeout = 30000');
},
);
}
@@ -15,29 +15,31 @@ import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.d
as i6;
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'
as i7;
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart'
as i8;
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
as i9;
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
as i10;
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'
as i11;
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'
as i12;
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'
as i13;
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'
as i14;
import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'
as i15;
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'
as i16;
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart'
as i17;
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'
as i18;
import 'package:drift/internal/modular.dart' as i19;
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
as i19;
import 'package:drift/internal/modular.dart' as i20;
abstract class $Drift extends i0.GeneratedDatabase {
$Drift(i0.QueryExecutor e) : super(e);
@@ -54,27 +56,30 @@ abstract class $Drift extends i0.GeneratedDatabase {
.$LocalAlbumEntityTable(this);
late final i7.$LocalAlbumAssetEntityTable localAlbumAssetEntity = i7
.$LocalAlbumAssetEntityTable(this);
late final i8.$UserMetadataEntityTable userMetadataEntity = i8
.$UserMetadataEntityTable(this);
late final i9.$PartnerEntityTable partnerEntity = i9.$PartnerEntityTable(
late final i8.$AuthUserEntityTable authUserEntity = i8.$AuthUserEntityTable(
this,
);
late final i10.$RemoteExifEntityTable remoteExifEntity = i10
.$RemoteExifEntityTable(this);
late final i11.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity = i11
.$RemoteAlbumAssetEntityTable(this);
late final i12.$RemoteAlbumUserEntityTable remoteAlbumUserEntity = i12
.$RemoteAlbumUserEntityTable(this);
late final i13.$MemoryEntityTable memoryEntity = i13.$MemoryEntityTable(this);
late final i14.$MemoryAssetEntityTable memoryAssetEntity = i14
.$MemoryAssetEntityTable(this);
late final i15.$PersonEntityTable personEntity = i15.$PersonEntityTable(this);
late final i16.$AssetFaceEntityTable assetFaceEntity = i16
.$AssetFaceEntityTable(this);
late final i17.$StoreEntityTable storeEntity = i17.$StoreEntityTable(this);
i18.MergedAssetDrift get mergedAssetDrift => i19.ReadDatabaseContainer(
late final i9.$UserMetadataEntityTable userMetadataEntity = i9
.$UserMetadataEntityTable(this);
late final i10.$PartnerEntityTable partnerEntity = i10.$PartnerEntityTable(
this,
).accessor<i18.MergedAssetDrift>(i18.MergedAssetDrift.new);
);
late final i11.$RemoteExifEntityTable remoteExifEntity = i11
.$RemoteExifEntityTable(this);
late final i12.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity = i12
.$RemoteAlbumAssetEntityTable(this);
late final i13.$RemoteAlbumUserEntityTable remoteAlbumUserEntity = i13
.$RemoteAlbumUserEntityTable(this);
late final i14.$MemoryEntityTable memoryEntity = i14.$MemoryEntityTable(this);
late final i15.$MemoryAssetEntityTable memoryAssetEntity = i15
.$MemoryAssetEntityTable(this);
late final i16.$PersonEntityTable personEntity = i16.$PersonEntityTable(this);
late final i17.$AssetFaceEntityTable assetFaceEntity = i17
.$AssetFaceEntityTable(this);
late final i18.$StoreEntityTable storeEntity = i18.$StoreEntityTable(this);
i19.MergedAssetDrift get mergedAssetDrift => i20.ReadDatabaseContainer(
this,
).accessor<i19.MergedAssetDrift>(i19.MergedAssetDrift.new);
@override
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
@@ -92,6 +97,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
i2.uQRemoteAssetsOwnerChecksum,
i2.uQRemoteAssetsOwnerLibraryChecksum,
i2.idxRemoteAssetChecksum,
authUserEntity,
userMetadataEntity,
partnerEntity,
remoteExifEntity,
@@ -102,7 +108,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
personEntity,
assetFaceEntity,
storeEntity,
i10.idxLatLng,
i11.idxLatLng,
];
@override
i0.StreamQueryUpdateRules
@@ -305,27 +311,29 @@ class $DriftManager {
i6.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity);
i7.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i7
.$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity);
i8.$$UserMetadataEntityTableTableManager get userMetadataEntity =>
i8.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
i9.$$PartnerEntityTableTableManager get partnerEntity =>
i9.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
i10.$$RemoteExifEntityTableTableManager get remoteExifEntity =>
i10.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity);
i11.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity =>
i11.$$RemoteAlbumAssetEntityTableTableManager(
i8.$$AuthUserEntityTableTableManager get authUserEntity =>
i8.$$AuthUserEntityTableTableManager(_db, _db.authUserEntity);
i9.$$UserMetadataEntityTableTableManager get userMetadataEntity =>
i9.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
i10.$$PartnerEntityTableTableManager get partnerEntity =>
i10.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
i11.$$RemoteExifEntityTableTableManager get remoteExifEntity =>
i11.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity);
i12.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity =>
i12.$$RemoteAlbumAssetEntityTableTableManager(
_db,
_db.remoteAlbumAssetEntity,
);
i12.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i12
i13.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i13
.$$RemoteAlbumUserEntityTableTableManager(_db, _db.remoteAlbumUserEntity);
i13.$$MemoryEntityTableTableManager get memoryEntity =>
i13.$$MemoryEntityTableTableManager(_db, _db.memoryEntity);
i14.$$MemoryAssetEntityTableTableManager get memoryAssetEntity =>
i14.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity);
i15.$$PersonEntityTableTableManager get personEntity =>
i15.$$PersonEntityTableTableManager(_db, _db.personEntity);
i16.$$AssetFaceEntityTableTableManager get assetFaceEntity =>
i16.$$AssetFaceEntityTableTableManager(_db, _db.assetFaceEntity);
i17.$$StoreEntityTableTableManager get storeEntity =>
i17.$$StoreEntityTableTableManager(_db, _db.storeEntity);
i14.$$MemoryEntityTableTableManager get memoryEntity =>
i14.$$MemoryEntityTableTableManager(_db, _db.memoryEntity);
i15.$$MemoryAssetEntityTableTableManager get memoryAssetEntity =>
i15.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity);
i16.$$PersonEntityTableTableManager get personEntity =>
i16.$$PersonEntityTableTableManager(_db, _db.personEntity);
i17.$$AssetFaceEntityTableTableManager get assetFaceEntity =>
i17.$$AssetFaceEntityTableTableManager(_db, _db.assetFaceEntity);
i18.$$StoreEntityTableTableManager get storeEntity =>
i18.$$StoreEntityTableTableManager(_db, _db.storeEntity);
}
@@ -3820,6 +3820,456 @@ i1.GeneratedColumn<String> _column_90(String aliasedName) =>
'REFERENCES remote_album_entity (id) ON DELETE SET NULL',
),
);
final class Schema10 extends i0.VersionedSchema {
Schema10({required super.database}) : super(version: 10);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
userEntity,
remoteAssetEntity,
stackEntity,
localAssetEntity,
remoteAlbumEntity,
localAlbumEntity,
localAlbumAssetEntity,
idxLocalAssetChecksum,
idxRemoteAssetOwnerChecksum,
uQRemoteAssetsOwnerChecksum,
uQRemoteAssetsOwnerLibraryChecksum,
idxRemoteAssetChecksum,
authUserEntity,
userMetadataEntity,
partnerEntity,
remoteExifEntity,
remoteAlbumAssetEntity,
remoteAlbumUserEntity,
memoryEntity,
memoryAssetEntity,
personEntity,
assetFaceEntity,
storeEntity,
idxLatLng,
];
late final Shape20 userEntity = Shape20(
source: i0.VersionedTable(
entityName: 'user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_3,
_column_84,
_column_85,
_column_91,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape17 remoteAssetEntity = Shape17(
source: i0.VersionedTable(
entityName: 'remote_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_1,
_column_8,
_column_9,
_column_5,
_column_10,
_column_11,
_column_12,
_column_0,
_column_13,
_column_14,
_column_15,
_column_16,
_column_17,
_column_18,
_column_19,
_column_20,
_column_21,
_column_86,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape3 stackEntity = Shape3(
source: i0.VersionedTable(
entityName: 'stack_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [_column_0, _column_9, _column_5, _column_15, _column_75],
attachedDatabase: database,
),
alias: null,
);
late final Shape2 localAssetEntity = Shape2(
source: i0.VersionedTable(
entityName: 'local_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_1,
_column_8,
_column_9,
_column_5,
_column_10,
_column_11,
_column_12,
_column_0,
_column_22,
_column_14,
_column_23,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape9 remoteAlbumEntity = Shape9(
source: i0.VersionedTable(
entityName: 'remote_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_56,
_column_9,
_column_5,
_column_15,
_column_57,
_column_58,
_column_59,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape19 localAlbumEntity = Shape19(
source: i0.VersionedTable(
entityName: 'local_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_5,
_column_31,
_column_32,
_column_90,
_column_33,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape7 localAlbumAssetEntity = Shape7(
source: i0.VersionedTable(
entityName: 'local_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_34, _column_35],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxLocalAssetChecksum = i1.Index(
'idx_local_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
);
final i1.Index idxRemoteAssetOwnerChecksum = i1.Index(
'idx_remote_asset_owner_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
);
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
'UQ_remote_assets_owner_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
);
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
'UQ_remote_assets_owner_library_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
);
final i1.Index idxRemoteAssetChecksum = i1.Index(
'idx_remote_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
);
late final Shape21 authUserEntity = Shape21(
source: i0.VersionedTable(
entityName: 'auth_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_3,
_column_2,
_column_84,
_column_85,
_column_92,
_column_93,
_column_7,
_column_94,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape4 userMetadataEntity = Shape4(
source: i0.VersionedTable(
entityName: 'user_metadata_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
columns: [_column_25, _column_26, _column_27],
attachedDatabase: database,
),
alias: null,
);
late final Shape5 partnerEntity = Shape5(
source: i0.VersionedTable(
entityName: 'partner_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
columns: [_column_28, _column_29, _column_30],
attachedDatabase: database,
),
alias: null,
);
late final Shape8 remoteExifEntity = Shape8(
source: i0.VersionedTable(
entityName: 'remote_exif_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id)'],
columns: [
_column_36,
_column_37,
_column_38,
_column_39,
_column_40,
_column_41,
_column_11,
_column_10,
_column_42,
_column_43,
_column_44,
_column_45,
_column_46,
_column_47,
_column_48,
_column_49,
_column_50,
_column_51,
_column_52,
_column_53,
_column_54,
_column_55,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape7 remoteAlbumAssetEntity = Shape7(
source: i0.VersionedTable(
entityName: 'remote_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_36, _column_60],
attachedDatabase: database,
),
alias: null,
);
late final Shape10 remoteAlbumUserEntity = Shape10(
source: i0.VersionedTable(
entityName: 'remote_album_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
columns: [_column_60, _column_25, _column_61],
attachedDatabase: database,
),
alias: null,
);
late final Shape11 memoryEntity = Shape11(
source: i0.VersionedTable(
entityName: 'memory_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_9,
_column_5,
_column_18,
_column_15,
_column_8,
_column_62,
_column_63,
_column_64,
_column_65,
_column_66,
_column_67,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape12 memoryAssetEntity = Shape12(
source: i0.VersionedTable(
entityName: 'memory_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
columns: [_column_36, _column_68],
attachedDatabase: database,
),
alias: null,
);
late final Shape14 personEntity = Shape14(
source: i0.VersionedTable(
entityName: 'person_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_9,
_column_5,
_column_15,
_column_1,
_column_69,
_column_71,
_column_72,
_column_73,
_column_74,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape15 assetFaceEntity = Shape15(
source: i0.VersionedTable(
entityName: 'asset_face_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_36,
_column_76,
_column_77,
_column_78,
_column_79,
_column_80,
_column_81,
_column_82,
_column_83,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape18 storeEntity = Shape18(
source: i0.VersionedTable(
entityName: 'store_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [_column_87, _column_88, _column_89],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxLatLng = i1.Index(
'idx_lat_lng',
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
);
}
class Shape20 extends i0.VersionedTable {
Shape20({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get email =>
columnsByName['email']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get hasProfileImage =>
columnsByName['has_profile_image']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<DateTime> get profileChangedAt =>
columnsByName['profile_changed_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<int> get avatarColor =>
columnsByName['avatar_color']! as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<int> _column_91(String aliasedName) =>
i1.GeneratedColumn<int>(
'avatar_color',
aliasedName,
false,
type: i1.DriftSqlType.int,
defaultValue: const CustomExpression('0'),
);
class Shape21 extends i0.VersionedTable {
Shape21({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get email =>
columnsByName['email']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get isAdmin =>
columnsByName['is_admin']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get hasProfileImage =>
columnsByName['has_profile_image']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<DateTime> get profileChangedAt =>
columnsByName['profile_changed_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<int> get avatarColor =>
columnsByName['avatar_color']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get quotaSizeInBytes =>
columnsByName['quota_size_in_bytes']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get quotaUsageInBytes =>
columnsByName['quota_usage_in_bytes']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get pinCode =>
columnsByName['pin_code']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<int> _column_92(String aliasedName) =>
i1.GeneratedColumn<int>(
'avatar_color',
aliasedName,
false,
type: i1.DriftSqlType.int,
);
i1.GeneratedColumn<int> _column_93(String aliasedName) =>
i1.GeneratedColumn<int>(
'quota_size_in_bytes',
aliasedName,
false,
type: i1.DriftSqlType.int,
defaultValue: const CustomExpression('0'),
);
i1.GeneratedColumn<String> _column_94(String aliasedName) =>
i1.GeneratedColumn<String>(
'pin_code',
aliasedName,
true,
type: i1.DriftSqlType.string,
);
i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@@ -3829,6 +4279,7 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
@@ -3872,6 +4323,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema);
await from8To9(migrator, schema);
return 9;
case 9:
final schema = Schema10(database: database);
final migrator = i1.Migrator(database, schema);
await from9To10(migrator, schema);
return 10;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
@@ -3887,6 +4343,7 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
}) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from1To2: from1To2,
@@ -3897,5 +4354,6 @@ i1.OnUpgrade stepByStep({
from6To7: from6To7,
from7To8: from7To8,
from8To9: from8To9,
from9To10: from9To10,
),
);
@@ -1,22 +1,20 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:platform/platform.dart';
enum SortLocalAlbumsBy { id, backupSelection, isIosSharedAlbum, name, assetCount, newestAsset }
class DriftLocalAlbumRepository extends DriftDatabaseRepository {
final Drift _db;
final Platform _platform;
const DriftLocalAlbumRepository(this._db, {Platform? platform})
: _platform = platform ?? const LocalPlatform(),
super(_db);
const DriftLocalAlbumRepository(this._db) : super(_db);
Future<List<LocalAlbum>> getAll({Set<SortLocalAlbumsBy> sortBy = const {}}) {
final assetCount = _db.localAlbumAssetEntity.assetId.count();
@@ -61,7 +59,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
// Remove all assets that are only in this particular album
// We cannot remove all assets in the album because they might be in other albums in iOS
// That is not the case on Android since asset <-> album has one:one mapping
final assetsToDelete = _platform.isIOS ? await _getUniqueAssetsInAlbum(albumId) : await getAssetIds(albumId);
final assetsToDelete = CurrentPlatform.isIOS ? await _getUniqueAssetsInAlbum(albumId) : await getAssetIds(albumId);
await _deleteAssets(assetsToDelete);
await _db.managers.localAlbumEntity
@@ -144,7 +142,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
}
});
if (_platform.isAndroid) {
if (CurrentPlatform.isAndroid) {
// On Android, an asset can only be in one album
// So, get the albums that are marked for deletion
// and delete all the assets that are in those albums
@@ -265,7 +263,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
return Future.value();
}
if (_platform.isAndroid) {
if (CurrentPlatform.isAndroid) {
return _deleteAssets(assetIds);
}
@@ -1,6 +1,8 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
@@ -26,6 +28,12 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
Future<LocalAsset?> get(String id) => _assetSelectable(id).getSingleOrNull();
Future<List<LocalAsset?>> getByChecksum(String checksum) {
final query = _db.localAssetEntity.select()..where((lae) => lae.checksum.equals(checksum));
return query.map((row) => row.toDto()).get();
}
Stream<LocalAsset?> watch(String id) => _assetSelectable(id).watchSingleOrNull();
Future<void> updateHashes(Iterable<LocalAsset> hashes) {
@@ -69,4 +77,23 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
Future<int> getHashedCount() {
return _db.managers.localAssetEntity.filter((e) => e.checksum.isNull().not()).count();
}
Future<List<LocalAlbum>> getSourceAlbums(String localAssetId, {BackupSelection? backupSelection}) {
final query = _db.localAlbumEntity.select()
..where(
(lae) => existsQuery(
_db.localAlbumAssetEntity.selectOnly()
..addColumns([_db.localAlbumAssetEntity.albumId])
..where(
_db.localAlbumAssetEntity.albumId.equalsExp(lae.id) &
_db.localAlbumAssetEntity.assetId.equals(localAssetId),
),
),
)
..orderBy([(lae) => OrderingTerm.asc(lae.name)]);
if (backupSelection != null) {
query.where((lae) => lae.backupSelection.equalsValue(backupSelection));
}
return query.map((localAlbum) => localAlbum.toDto()).get();
}
}
@@ -202,14 +202,13 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
id: user.id,
email: user.email,
name: user.name,
isAdmin: user.isAdmin,
updatedAt: user.updatedAt,
memoryEnabled: true,
inTimeline: false,
isPartnerSharedBy: false,
isPartnerSharedWith: false,
profileChangedAt: user.profileChangedAt,
hasProfileImage: user.hasProfileImage,
avatarColor: user.avatarColor,
),
)
.get();
@@ -55,6 +55,12 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
return _assetSelectable(id).getSingleOrNull();
}
Future<RemoteAsset?> getByChecksum(String checksum) {
final query = _db.remoteAssetEntity.select()..where((row) => row.checksum.equals(checksum));
return query.map((row) => row.toDto()).getSingleOrNull();
}
Future<List<RemoteAsset>> getStackChildren(RemoteAsset asset) {
if (asset.stackId == null) {
return Future.value([]);
@@ -173,7 +173,7 @@ class DriftStoreRepository extends DriftDatabaseRepository implements IStoreRepo
const (bool) => entity.intValue == 1,
const (DateTime) => entity.intValue == null ? null : DateTime.fromMillisecondsSinceEpoch(entity.intValue!),
const (UserDto) =>
entity.stringValue == null ? null : await DriftUserRepository(_db).get(entity.stringValue!),
entity.stringValue == null ? null : await DriftAuthUserRepository(_db).get(entity.stringValue!),
_ => null,
}
as T?;
@@ -184,7 +184,7 @@ class DriftStoreRepository extends DriftDatabaseRepository implements IStoreRepo
const (String) => (null, value as String),
const (bool) => ((value as bool) ? 1 : 0, null),
const (DateTime) => ((value as DateTime).millisecondsSinceEpoch, null),
const (UserDto) => (null, (await DriftUserRepository(_db).upsert(value as UserDto)).id),
const (UserDto) => (null, (await DriftAuthUserRepository(_db).upsert(value as UserDto)).id),
_ => throw UnsupportedError("Unsupported primitive type: ${key.type} for key: ${key.name}"),
};
return StoreEntityCompanion(id: Value(key.id), intValue: Value(intValue), stringValue: Value(strValue));
@@ -3,7 +3,9 @@ import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
@@ -18,7 +20,8 @@ class SyncApiRepository {
}
Future<void> streamChanges(
Function(List<SyncEvent>, Function() abort) onData, {
Future<void> Function(List<SyncEvent>, Function() abort, Function() reset) onData, {
Function()? onReset,
int batchSize = kSyncEventBatchSize,
http.Client? httpClient,
}) async {
@@ -32,11 +35,13 @@ class SyncApiRepository {
await _api.applyToParams([], headerParams);
headers.addAll(headerParams);
final shouldReset = Store.get(StoreKey.shouldResetSync, false);
final request = http.Request('POST', Uri.parse(endpoint));
request.headers.addAll(headers);
request.body = jsonEncode(
SyncStreamDto(
types: [
SyncRequestType.authUsersV1,
SyncRequestType.usersV1,
SyncRequestType.assetsV1,
SyncRequestType.assetExifsV1,
@@ -56,6 +61,7 @@ class SyncApiRepository {
SyncRequestType.peopleV1,
SyncRequestType.assetFacesV1,
],
reset: shouldReset,
).toJson(),
);
@@ -69,6 +75,8 @@ class SyncApiRepository {
shouldAbort = true;
}
final reset = onReset ?? () {};
try {
final response = await client.send(request);
@@ -77,6 +85,9 @@ class SyncApiRepository {
throw ApiException(response.statusCode, 'Failed to get sync stream: $errorBody');
}
// Reset after successful stream start
await Store.put(StoreKey.shouldResetSync, false);
await for (final chunk in response.stream.transform(utf8.decoder)) {
if (shouldAbort) {
break;
@@ -91,12 +102,12 @@ class SyncApiRepository {
continue;
}
await onData(_parseLines(lines), abort);
await onData(_parseLines(lines), abort, reset);
lines.clear();
}
if (lines.isNotEmpty && !shouldAbort) {
await onData(_parseLines(lines), abort);
await onData(_parseLines(lines), abort, reset);
}
} catch (error, stack) {
_logger.severe("Error processing stream", error, stack);
@@ -130,6 +141,7 @@ class SyncApiRepository {
}
const _kResponseMap = <SyncEntityType, Function(Object)>{
SyncEntityType.authUserV1: SyncAuthUserV1.fromJson,
SyncEntityType.userV1: SyncUserV1.fromJson,
SyncEntityType.userDeleteV1: SyncUserDeleteV1.fromJson,
SyncEntityType.partnerV1: SyncPartnerV1.fromJson,
@@ -156,7 +168,8 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
SyncEntityType.albumToAssetV1: SyncAlbumToAssetV1.fromJson,
SyncEntityType.albumToAssetBackfillV1: SyncAlbumToAssetV1.fromJson,
SyncEntityType.albumToAssetDeleteV1: SyncAlbumToAssetDeleteV1.fromJson,
SyncEntityType.syncAckV1: _SyncAckV1.fromJson,
SyncEntityType.syncAckV1: _SyncEmptyDto.fromJson,
SyncEntityType.syncResetV1: _SyncEmptyDto.fromJson,
SyncEntityType.memoryV1: SyncMemoryV1.fromJson,
SyncEntityType.memoryDeleteV1: SyncMemoryDeleteV1.fromJson,
SyncEntityType.memoryToAssetV1: SyncMemoryAssetV1.fromJson,
@@ -172,8 +185,9 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
SyncEntityType.personDeleteV1: SyncPersonDeleteV1.fromJson,
SyncEntityType.assetFaceV1: SyncAssetFaceV1.fromJson,
SyncEntityType.assetFaceDeleteV1: SyncAssetFaceDeleteV1.fromJson,
SyncEntityType.syncCompleteV1: _SyncEmptyDto.fromJson,
};
class _SyncAckV1 {
static _SyncAckV1? fromJson(dynamic _) => _SyncAckV1();
class _SyncEmptyDto {
static _SyncEmptyDto? fromJson(dynamic _) => _SyncEmptyDto();
}
@@ -1,11 +1,14 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:drift/drift.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/domain/models/memory.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart';
@@ -29,6 +32,65 @@ class SyncStreamRepository extends DriftDatabaseRepository {
SyncStreamRepository(super.db) : _db = db;
Future<void> reset() async {
_logger.fine("SyncResetV1 received. Resetting remote entities");
try {
await _db.exclusively(() async {
// foreign_keys PRAGMA is no-op within transactions
// https://www.sqlite.org/pragma.html#pragma_foreign_keys
await _db.customStatement('PRAGMA foreign_keys = OFF');
await transaction(() async {
await _db.assetFaceEntity.deleteAll();
await _db.memoryAssetEntity.deleteAll();
await _db.memoryEntity.deleteAll();
await _db.partnerEntity.deleteAll();
await _db.personEntity.deleteAll();
await _db.remoteAlbumAssetEntity.deleteAll();
await _db.remoteAlbumEntity.deleteAll();
await _db.remoteAlbumUserEntity.deleteAll();
await _db.remoteAssetEntity.deleteAll();
await _db.remoteExifEntity.deleteAll();
await _db.stackEntity.deleteAll();
await _db.userEntity.deleteAll();
await _db.userMetadataEntity.deleteAll();
});
await _db.customStatement('PRAGMA foreign_keys = ON');
});
} catch (error, stack) {
_logger.severe('Error: SyncResetV1', error, stack);
rethrow;
}
}
Future<void> updateAuthUsersV1(Iterable<SyncAuthUserV1> data) async {
try {
await _db.batch((batch) {
for (final user in data) {
final companion = AuthUserEntityCompanion(
name: Value(user.name),
email: Value(user.email),
hasProfileImage: Value(user.hasProfileImage),
profileChangedAt: Value(user.profileChangedAt),
avatarColor: Value(user.avatarColor?.toAvatarColor() ?? AvatarColor.primary),
isAdmin: Value(user.isAdmin),
pinCode: Value(user.pinCode),
quotaSizeInBytes: Value(user.quotaSizeInBytes ?? 0),
quotaUsageInBytes: Value(user.quotaUsageInBytes),
);
batch.insert(
_db.authUserEntity,
companion.copyWith(id: Value(user.id)),
onConflict: DoUpdate((_) => companion),
);
}
});
} catch (error, stack) {
_logger.severe('Error: SyncAuthUserV1', error, stack);
rethrow;
}
}
Future<void> deleteUsersV1(Iterable<SyncUserDeleteV1> data) async {
try {
await _db.userEntity.deleteWhere((row) => row.id.isIn(data.map((e) => e.userId)));
@@ -47,6 +109,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
email: Value(user.email),
hasProfileImage: Value(user.hasProfileImage),
profileChangedAt: Value(user.profileChangedAt),
avatarColor: Value(user.avatarColor?.toAvatarColor() ?? AvatarColor.primary),
);
batch.insert(_db.userEntity, companion.copyWith(id: Value(user.id)), onConflict: DoUpdate((_) => companion));
@@ -573,3 +636,7 @@ extension on String {
}
}
}
extension on UserAvatarColor {
AvatarColor? toAvatarColor() => AvatarColor.values.firstWhereOrNull((c) => c.name == value);
}
@@ -42,14 +42,10 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
throw UnsupportedError("GroupAssetsBy.none is not supported for watchMainBucket");
}
return _db.mergedAssetDrift
.mergedBucket(userIds: userIds, groupBy: groupBy.index)
.map((row) {
final date = row.bucketDate.dateFmt(groupBy);
return TimeBucket(date: date, assetCount: row.assetCount);
})
.watch()
.throttle(const Duration(seconds: 3), trailing: true);
return _db.mergedAssetDrift.mergedBucket(userIds: userIds, groupBy: groupBy.index).map((row) {
final date = row.bucketDate.dateFmt(groupBy);
return TimeBucket(date: date, assetCount: row.assetCount);
}).watch();
}
Future<List<BaseAsset>> _getMainBucketAssets(List<String> userIds, {required int offset, required int count}) {
@@ -595,7 +591,7 @@ extension on String {
GroupAssetsBy.none => throw ArgumentError("GroupAssetsBy.none is not supported for date formatting"),
};
try {
return DateFormat(format).parse(this);
return DateFormat(format, 'en').parse(this);
} catch (e) {
throw FormatException("Invalid date format: $this", e);
}
@@ -2,8 +2,8 @@ import 'package:drift/drift.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity;
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user_metadata.repository.dart';
import 'package:isar/isar.dart';
@@ -68,12 +68,12 @@ class IsarUserRepository extends IsarDatabaseRepository {
}
}
class DriftUserRepository extends DriftDatabaseRepository {
class DriftAuthUserRepository extends DriftDatabaseRepository {
final Drift _db;
const DriftUserRepository(super.db) : _db = db;
const DriftAuthUserRepository(super.db) : _db = db;
Future<UserDto?> get(String id) async {
final user = await _db.managers.userEntity.filter((user) => user.id.equals(id)).getSingleOrNull();
final user = await _db.managers.authUserEntity.filter((user) => user.id.equals(id)).getSingleOrNull();
if (user == null) return null;
@@ -84,43 +84,30 @@ class DriftUserRepository extends DriftDatabaseRepository {
}
Future<UserDto> upsert(UserDto user) async {
await _db.userEntity.insertOnConflictUpdate(
UserEntityCompanion(
await _db.authUserEntity.insertOnConflictUpdate(
AuthUserEntityCompanion(
id: Value(user.id),
isAdmin: Value(user.isAdmin),
updatedAt: Value(user.updatedAt),
name: Value(user.name),
email: Value(user.email),
hasProfileImage: Value(user.hasProfileImage),
profileChangedAt: Value(user.profileChangedAt),
isAdmin: Value(user.isAdmin),
quotaSizeInBytes: Value(user.quotaSizeInBytes),
quotaUsageInBytes: Value(user.quotaUsageInBytes),
avatarColor: Value(user.avatarColor),
),
);
return user;
}
Future<List<UserDto>> getAll() async {
final users = await _db.userEntity.select().get();
final List<UserDto> result = [];
for (final user in users) {
final query = _db.userMetadataEntity.select()..where((e) => e.userId.equals(user.id));
final metadata = await query.map((row) => row.toDto()).get();
result.add(user.toDto(metadata));
}
return result;
}
}
extension on UserEntityData {
extension on AuthUserEntityData {
UserDto toDto([List<UserMetadata>? metadata]) {
AvatarColor avatarColor = AvatarColor.primary;
bool memoryEnabled = true;
if (metadata != null) {
for (final meta in metadata) {
if (meta.key == UserMetadataKey.preferences && meta.preferences != null) {
avatarColor = meta.preferences?.userAvatarColor ?? AvatarColor.primary;
memoryEnabled = meta.preferences?.memoriesEnabled ?? true;
}
}
@@ -130,12 +117,13 @@ extension on UserEntityData {
id: id,
email: email,
name: name,
isAdmin: isAdmin,
updatedAt: updatedAt,
profileChangedAt: profileChangedAt,
hasProfileImage: hasProfileImage,
avatarColor: avatarColor,
memoryEnabled: memoryEnabled,
isAdmin: isAdmin,
quotaSizeInBytes: quotaSizeInBytes,
quotaUsageInBytes: quotaUsageInBytes,
);
}
}
@@ -1,5 +1,4 @@
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
import 'package:openapi/api.dart';
// TODO: Move to repository once all classes are refactored
+13 -12
View File
@@ -17,9 +17,9 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/generated/codegen_loader.g.dart';
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/locale_provider.dart';
import 'package:immich_mobile/providers/routes.provider.dart';
import 'package:immich_mobile/providers/theme.provider.dart';
@@ -39,6 +39,7 @@ import 'package:intl/date_symbol_data_local.dart';
import 'package:logging/logging.dart';
import 'package:timezone/data/latest.dart';
import 'package:worker_manager/worker_manager.dart';
import 'package:immich_mobile/utils/debug_print.dart';
void main() async {
ImmichWidgetsBinding();
@@ -69,9 +70,9 @@ Future<void> initApp() async {
if (kReleaseMode && Platform.isAndroid) {
try {
await FlutterDisplayMode.setHighRefreshRate();
debugPrint("Enabled high refresh mode");
dPrint(() => "Enabled high refresh mode");
} catch (e) {
debugPrint("Error setting high refresh rate: $e");
dPrint(() => "Error setting high refresh rate: $e");
}
}
@@ -126,23 +127,23 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.resumed:
debugPrint("[APP STATE] resumed");
dPrint(() => "[APP STATE] resumed");
ref.read(appStateProvider.notifier).handleAppResume();
break;
case AppLifecycleState.inactive:
debugPrint("[APP STATE] inactive");
dPrint(() => "[APP STATE] inactive");
ref.read(appStateProvider.notifier).handleAppInactivity();
break;
case AppLifecycleState.paused:
debugPrint("[APP STATE] paused");
dPrint(() => "[APP STATE] paused");
ref.read(appStateProvider.notifier).handleAppPause();
break;
case AppLifecycleState.detached:
debugPrint("[APP STATE] detached");
dPrint(() => "[APP STATE] detached");
ref.read(appStateProvider.notifier).handleAppDetached();
break;
case AppLifecycleState.hidden:
debugPrint("[APP STATE] hidden");
dPrint(() => "[APP STATE] hidden");
ref.read(appStateProvider.notifier).handleAppHidden();
break;
}
@@ -200,14 +201,14 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
@override
initState() {
super.initState();
initApp().then((_) => debugPrint("App Init Completed"));
initApp().then((_) => dPrint(() => "App Init Completed"));
WidgetsBinding.instance.addPostFrameCallback((_) {
// needs to be delayed so that EasyLocalization is working
if (Store.isBetaTimelineEnabled) {
ref.read(backgroundServiceProvider).disableService();
ref.read(driftBackgroundUploadFgService).enable();
ref.read(backgroundWorkerFgServiceProvider).enable();
} else {
ref.read(driftBackgroundUploadFgService).disable();
ref.read(backgroundWorkerFgServiceProvider).disable();
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
}
});
@@ -239,7 +240,7 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
theme: getThemeData(colorScheme: immichTheme.light, locale: context.locale),
routerConfig: router.config(
deepLinkBuilder: _deepLinkBuilder,
navigatorObservers: () => [AppNavigationObserver(ref: ref), HeroController()],
navigatorObservers: () => [AppNavigationObserver(ref: ref)],
),
),
);
@@ -171,7 +171,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
album.activityEnabled = value;
}
},
activeColor: activityEnabled.value ? context.primaryColor : context.themeData.disabledColor,
activeThumbColor: activityEnabled.value ? context.primaryColor : context.themeData.disabledColor,
dense: true,
title: Text(
"comments_and_likes",
@@ -205,9 +205,9 @@ class BackupControllerPage extends HookConsumerWidget {
}
buildBackgroundBackupInfo() {
return const ListTile(
leading: Icon(Icons.info_outline_rounded),
title: Text("Background backup is currently running, cannot start manual backup"),
return ListTile(
leading: const Icon(Icons.info_outline_rounded),
title: Text('background_backup_running_error'.tr()),
);
}
+54 -10
View File
@@ -5,15 +5,19 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.widget.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_album.provider.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/backup/backup_info_card.dart';
@RoutePage()
@@ -25,6 +29,8 @@ class DriftBackupPage extends ConsumerStatefulWidget {
}
class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
Timer? _countPoller;
@override
void initState() {
super.initState();
@@ -33,22 +39,40 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
return;
}
if (ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup)) {
_startCountPolling();
}
ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
}
Future<void> startBackup() async {
final currentUser = ref.read(currentUserProvider);
if (currentUser == null) {
return;
}
void _startCountPolling() {
_countPoller?.cancel();
_countPoller = Timer.periodic(const Duration(seconds: 5), (timer) async {
if (!mounted) {
timer.cancel();
return;
}
await ref.read(backgroundSyncProvider).syncRemote();
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
await ref.read(driftBackupProvider.notifier).startBackup(currentUser.id);
final currentUser = ref.read(currentUserProvider);
if (currentUser == null) {
timer.cancel();
return;
}
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
});
}
Future<void> stopBackup() async {
await ref.read(driftBackupProvider.notifier).cancel();
void _stopCountPolling() {
_countPoller?.cancel();
_countPoller = null;
}
@override
void dispose() {
_stopCountPolling();
super.dispose();
}
@override
@@ -58,6 +82,26 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
.where((album) => album.backupSelection == BackupSelection.selected)
.toList();
final backupNotifier = ref.read(driftBackupProvider.notifier);
final backgroundManager = ref.read(backgroundSyncProvider);
Future<void> startBackup() async {
final currentUser = Store.tryGet(StoreKey.currentUser);
if (currentUser == null) {
return;
}
await backgroundManager.syncRemote();
await backupNotifier.getBackupStatus(currentUser.id);
await backupNotifier.startBackup(currentUser.id);
_startCountPolling();
}
Future<void> stopBackup() async {
await backupNotifier.cancel();
_stopCountPolling();
}
return Scaffold(
appBar: AppBar(
elevation: 0,
@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
@@ -249,7 +250,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
mainAxisSize: MainAxisSize.max,
children: [
const CircularProgressIndicator(strokeWidth: 4),
Text("Creating linked albums...", style: context.textTheme.labelLarge),
Text('creating_linked_albums'.tr(), style: context.textTheme.labelLarge),
],
),
),
@@ -1,4 +1,5 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
@@ -58,8 +59,10 @@ class DriftBackupAssetDetailPage extends ConsumerWidget {
overflow: TextOverflow.ellipsis,
);
},
error: (error, stackTrace) =>
Text('Error: $error', style: TextStyle(color: context.colorScheme.error)),
error: (error, stackTrace) => Text(
'error_saving_image'.tr(args: [error.toString()]),
style: TextStyle(color: context.colorScheme.error),
),
loading: () => const SizedBox(height: 16, width: 16, child: CircularProgressIndicator.adaptive()),
),
],
@@ -83,7 +86,7 @@ class DriftBackupAssetDetailPage extends ConsumerWidget {
);
},
error: (Object error, StackTrace stackTrace) {
return Center(child: Text('Error: $error'));
return Center(child: Text('error_saving_image'.tr(args: [error.toString()])));
},
loading: () {
return const SizedBox(height: 48, width: 48, child: Center(child: CircularProgressIndicator.adaptive()));
+2 -2
View File
@@ -1,4 +1,5 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -8,7 +9,6 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/immich_logger.service.dart';
import 'package:intl/intl.dart';
@RoutePage()
class AppLogPage extends HookConsumerWidget {
@@ -49,7 +49,7 @@ class AppLogPage extends HookConsumerWidget {
return Scaffold(
appBar: AppBar(
title: const Text("Logs", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0)),
title: Text('logs'.tr(), style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0)),
scrolledUnderElevation: 1,
elevation: 2,
actions: [
@@ -1,4 +1,5 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -36,7 +37,7 @@ class AppLogDetailPage extends HookConsumerWidget {
context.scaffoldMessenger.showSnackBar(
SnackBar(
content: Text(
"Copied to clipboard",
"copied_to_clipboard".tr(),
style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor),
),
),
@@ -97,7 +98,7 @@ class AppLogDetailPage extends HookConsumerWidget {
}
return Scaffold(
appBar: AppBar(title: const Text("Log Detail")),
appBar: AppBar(title: Text("log_detail_title".tr())),
body: SafeArea(
child: ListView(
children: [
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -13,6 +15,7 @@ import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/providers/websocket.provider.dart';
import 'package:immich_mobile/services/background.service.dart';
@@ -41,49 +44,13 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
Future<void> _handleMigration() async {
try {
if (widget.switchingToBeta) {
final assetNotifier = ref.read(assetProvider.notifier);
if (assetNotifier.mounted) {
assetNotifier.dispose();
}
final albumNotifier = ref.read(albumProvider.notifier);
if (albumNotifier.mounted) {
albumNotifier.dispose();
}
// Cancel uploads
await Store.put(StoreKey.backgroundBackup, false);
ref
.read(backupProvider.notifier)
.configureBackgroundBackup(enabled: false, onBatteryInfo: () {}, onError: (_) {});
ref.read(backupProvider.notifier).setAutoBackup(false);
ref.read(backupProvider.notifier).cancelBackup();
ref.read(manualUploadProvider.notifier).cancelBackup();
// Start listening to new websocket events
ref.read(websocketProvider.notifier).stopListenToOldEvents();
ref.read(websocketProvider.notifier).startListeningToBetaEvents();
final permission = await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
if (permission.isGranted) {
await ref.read(backgroundSyncProvider).syncLocal(full: true);
await migrateDeviceAssetToSqlite(ref.read(isarProvider), ref.read(driftProvider));
await migrateBackupAlbumsToSqlite(ref.read(isarProvider), ref.read(driftProvider));
await migrateStoreToSqlite(ref.read(isarProvider), ref.read(driftProvider));
await ref.read(backgroundServiceProvider).disableService();
}
} else {
await ref.read(backgroundSyncProvider).cancel();
ref.read(websocketProvider.notifier).stopListeningToBetaEvents();
ref.read(websocketProvider.notifier).startListeningToOldEvents();
ref.read(readonlyModeProvider.notifier).setReadonlyMode(false);
await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider));
await ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
await ref.read(driftBackgroundUploadFgService).disable();
}
await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
await _performMigrationLogic().timeout(
const Duration(minutes: 3),
onTimeout: () async {
await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
},
);
if (mounted) {
setState(() {
@@ -101,6 +68,54 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
}
}
Future<void> _performMigrationLogic() async {
if (widget.switchingToBeta) {
final assetNotifier = ref.read(assetProvider.notifier);
if (assetNotifier.mounted) {
assetNotifier.dispose();
}
final albumNotifier = ref.read(albumProvider.notifier);
if (albumNotifier.mounted) {
albumNotifier.dispose();
}
// Cancel uploads
await Store.put(StoreKey.backgroundBackup, false);
ref
.read(backupProvider.notifier)
.configureBackgroundBackup(enabled: false, onBatteryInfo: () {}, onError: (_) {});
ref.read(backupProvider.notifier).setAutoBackup(false);
ref.read(backupProvider.notifier).cancelBackup();
ref.read(manualUploadProvider.notifier).cancelBackup();
// Start listening to new websocket events
ref.read(websocketProvider.notifier).stopListenToOldEvents();
ref.read(websocketProvider.notifier).startListeningToBetaEvents();
await ref.read(driftProvider).reset();
await Store.put(StoreKey.shouldResetSync, true);
final permission = await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
if (permission.isGranted) {
await ref.read(backgroundSyncProvider).syncLocal(full: true);
await migrateDeviceAssetToSqlite(ref.read(isarProvider), ref.read(driftProvider));
await migrateBackupAlbumsToSqlite(ref.read(isarProvider), ref.read(driftProvider));
await migrateStoreToSqlite(ref.read(isarProvider), ref.read(driftProvider));
await ref.read(backgroundServiceProvider).disableService();
}
} else {
await ref.read(backgroundSyncProvider).cancel();
ref.read(websocketProvider.notifier).stopListeningToBetaEvents();
ref.read(websocketProvider.notifier).startListeningToOldEvents();
ref.read(readonlyModeProvider.notifier).setReadonlyMode(false);
await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider));
await ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
await ref.read(backgroundWorkerFgServiceProvider).disable();
}
await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
}
@override
Widget build(BuildContext context) {
return Scaffold(
+17 -29
View File
@@ -11,8 +11,7 @@ import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_se
import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart';
import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart';
import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart';
import 'package:immich_mobile/widgets/settings/beta_sync_settings/beta_sync_settings.dart';
import 'package:immich_mobile/widgets/settings/beta_timeline_list_tile.dart';
import 'package:immich_mobile/widgets/settings/beta_sync_settings/sync_status_and_actions.dart';
import 'package:immich_mobile/widgets/settings/language_settings.dart';
import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart';
import 'package:immich_mobile/widgets/settings/notification_setting.dart';
@@ -20,7 +19,6 @@ import 'package:immich_mobile/widgets/settings/preference_settings/preference_se
import 'package:immich_mobile/widgets/settings/settings_card.dart';
enum SettingSection {
beta('beta_sync', Icons.sync_outlined, "beta_sync_subtitle"),
advanced('advanced', Icons.build_outlined, "advanced_settings_tile_subtitle"),
assetViewer('asset_viewer_settings_title', Icons.image_outlined, "asset_viewer_settings_subtitle"),
backup('backup', Icons.cloud_upload_outlined, "backup_settings_subtitle"),
@@ -28,14 +26,14 @@ enum SettingSection {
networking('networking_settings', Icons.wifi, "networking_subtitle"),
notifications('notifications', Icons.notifications_none_rounded, "setting_notifications_subtitle"),
preferences('preferences_settings_title', Icons.interests_outlined, "preferences_settings_subtitle"),
timeline('asset_list_settings_title', Icons.auto_awesome_mosaic_outlined, "asset_list_settings_subtitle");
timeline('asset_list_settings_title', Icons.auto_awesome_mosaic_outlined, "asset_list_settings_subtitle"),
beta('sync_status', Icons.sync_outlined, "sync_status_subtitle");
final String title;
final String subtitle;
final IconData icon;
Widget get widget => switch (this) {
SettingSection.beta => const _BetaLandscapeToggle(),
SettingSection.advanced => const AdvancedSettings(),
SettingSection.assetViewer => const AssetViewerSettings(),
SettingSection.backup =>
@@ -45,6 +43,7 @@ enum SettingSection {
SettingSection.notifications => const NotificationSetting(),
SettingSection.preferences => const PreferenceSetting(),
SettingSection.timeline => const AssetListSettings(),
SettingSection.beta => const SyncStatusAndActions(),
};
const SettingSection(this.title, this.icon, this.subtitle);
@@ -59,7 +58,7 @@ class SettingsPage extends StatelessWidget {
context.locale;
return Scaffold(
appBar: AppBar(centerTitle: false, title: const Text('settings').tr()),
body: context.isMobile ? const _MobileLayout() : const _TabletLayout(),
body: context.isMobile ? const SafeArea(child: _MobileLayout()) : const SafeArea(child: _TabletLayout()),
);
}
}
@@ -72,13 +71,12 @@ class _MobileLayout extends StatelessWidget {
.expand(
(setting) => setting == SettingSection.beta
? [
const BetaTimelineListTile(),
if (Store.isBetaTimelineEnabled)
SettingsCard(
icon: Icons.sync_outlined,
title: 'beta_sync'.tr(),
subtitle: 'beta_sync_subtitle'.tr(),
settingRoute: const BetaSyncSettingsRoute(),
title: 'sync_status'.tr(),
subtitle: 'sync_status_subtitle'.tr(),
settingRoute: const SyncStatusRoute(),
),
]
: [
@@ -93,7 +91,7 @@ class _MobileLayout extends StatelessWidget {
.toList();
return ListView(
physics: const ClampingScrollPhysics(),
padding: const EdgeInsets.only(top: 10.0, bottom: 56),
padding: const EdgeInsets.only(top: 10.0, bottom: 16),
children: [...settings],
);
}
@@ -134,21 +132,6 @@ class _TabletLayout extends HookWidget {
}
}
class _BetaLandscapeToggle extends HookWidget {
const _BetaLandscapeToggle();
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const SizedBox(height: 100, child: BetaTimelineListTile()),
if (Store.isBetaTimelineEnabled) const Expanded(child: BetaSyncSettings()),
],
);
}
}
@RoutePage()
class SettingsSubPage extends StatelessWidget {
const SettingsSubPage(this.section, {super.key});
@@ -158,9 +141,14 @@ class SettingsSubPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
context.locale;
return Scaffold(
appBar: AppBar(centerTitle: false, title: Text(section.title).tr()),
body: section.widget,
return SafeArea(
bottom: true,
top: false,
right: true,
child: Scaffold(
appBar: AppBar(centerTitle: false, title: Text(section.title).tr()),
body: section.widget,
),
);
}
}
+60 -35
View File
@@ -1,14 +1,12 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/utils/isolate_lock_manager.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/websocket.provider.dart';
@@ -29,19 +27,11 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
@override
void initState() {
super.initState();
final lockManager = ref.read(isolateLockManagerProvider(kIsolateLockManagerPort));
lockManager.requestHolderToClose();
lockManager
.acquireLock()
.timeout(const Duration(seconds: 5))
.whenComplete(
() => ref
.read(authProvider.notifier)
.setOpenApiServiceEndpoint()
.then(logConnectionInfo)
.whenComplete(() => resumeSession()),
);
ref
.read(authProvider.notifier)
.setOpenApiServiceEndpoint()
.then(logConnectionInfo)
.whenComplete(() => resumeSession());
}
void logConnectionInfo(String? endpoint) {
@@ -60,32 +50,56 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
if (accessToken != null && serverUrl != null && endpoint != null) {
final infoProvider = ref.read(serverInfoProvider.notifier);
final wsProvider = ref.read(websocketProvider.notifier);
unawaited(
ref.read(authProvider.notifier).saveAuthInfo(accessToken: accessToken).then(
(a) {
try {
wsProvider.connect();
infoProvider.getServerInfo();
} catch (e) {
log.severe('Failed establishing connection to the server: $e');
final backgroundManager = ref.read(backgroundSyncProvider);
final backupProvider = ref.read(driftBackupProvider.notifier);
ref.read(authProvider.notifier).saveAuthInfo(accessToken: accessToken).then(
(_) async {
try {
wsProvider.connect();
infoProvider.getServerInfo();
if (Store.isBetaTimelineEnabled) {
await Future.wait([backgroundManager.syncLocal(), backgroundManager.syncRemote()]);
await Future.wait([
backgroundManager.hashAssets().then((_) {
_resumeBackup(backupProvider);
}),
_resumeBackup(backupProvider),
]);
if (Store.get(StoreKey.syncAlbums, false)) {
await backgroundManager.syncLinkedAlbum();
}
}
},
onError: (exception) => {
log.severe('Failed to update auth info with access token: $accessToken'),
ref.read(authProvider.notifier).logout(),
context.replaceRoute(const LoginRoute()),
},
),
} catch (e) {
log.severe('Failed establishing connection to the server: $e');
}
},
onError: (exception) => {
log.severe('Failed to update auth info with access token: $accessToken'),
ref.read(authProvider.notifier).logout(),
context.replaceRoute(const LoginRoute()),
},
);
} else {
log.severe('Missing crucial offline login info - Logging out completely');
unawaited(ref.read(authProvider.notifier).logout());
unawaited(context.replaceRoute(const LoginRoute()));
ref.read(authProvider.notifier).logout();
context.replaceRoute(const LoginRoute());
return;
}
// clean install - change the default of the flag
// current install not using beta timeline
if (context.router.current.name == SplashScreenRoute.name) {
unawaited(context.replaceRoute(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute()));
final needBetaMigration = Store.get(StoreKey.needBetaMigration, false);
if (needBetaMigration) {
await Store.put(StoreKey.needBetaMigration, false);
context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: true)]);
return;
}
context.replaceRoute(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute());
}
if (Store.isBetaTimelineEnabled) {
@@ -95,7 +109,18 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
final hasPermission = await ref.read(galleryPermissionNotifier.notifier).hasPermission;
if (hasPermission) {
// Resume backup (if enable) then navigate
await ref.watch(backupProvider.notifier).resumeBackup();
ref.watch(backupProvider.notifier).resumeBackup();
}
}
Future<void> _resumeBackup(DriftBackupNotifier notifier) async {
final isEnableBackup = Store.get(StoreKey.enableBackup, false);
if (isEnableBackup) {
final currentUser = Store.tryGet(StoreKey.currentUser);
if (currentUser != null) {
notifier.handleBackupResume(currentUser.id);
}
}
}
@@ -7,8 +7,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
@@ -17,11 +15,7 @@ import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.da
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
import 'package:immich_mobile/providers/tab.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/providers/websocket.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/utils/migration.dart';
@RoutePage()
class TabShellPage extends ConsumerStatefulWidget {
@@ -32,28 +26,6 @@ class TabShellPage extends ConsumerStatefulWidget {
}
class _TabShellPageState extends ConsumerState<TabShellPage> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
ref.read(websocketProvider.notifier).connect();
final isEnableBackup = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
await runNewSync(ref, full: true).then((_) async {
if (isEnableBackup) {
final currentUser = ref.read(currentUserProvider);
if (currentUser == null) {
return;
}
await ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
}
});
});
}
@override
Widget build(BuildContext context) {
final isScreenLandscape = context.orientation == Orientation.landscape;
@@ -134,7 +134,7 @@ class _SharedToPartnerList extends ConsumerWidget {
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text("Error loading partners: $error")),
error: (error, stack) => Center(child: Text('error_loading_partners'.tr(args: [error.toString()]))),
);
}
}
@@ -85,7 +85,7 @@ class PlacesCollectionPage extends HookConsumerWidget {
},
);
},
error: (error, stask) => const Text('Error getting places'),
error: (error, stask) => Text('error_getting_places'.tr()),
loading: () => const Center(child: CircularProgressIndicator()),
),
],
@@ -112,7 +112,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
return SwitchListTile.adaptive(
value: showMetadata.value,
onChanged: newShareLink.value.isEmpty ? (value) => showMetadata.value = value : null,
activeColor: colorScheme.primary,
activeThumbColor: colorScheme.primary,
dense: true,
title: Text("show_metadata", style: themeData.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold)).tr(),
);
@@ -122,7 +122,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
return SwitchListTile.adaptive(
value: allowDownload.value,
onChanged: newShareLink.value.isEmpty ? (value) => allowDownload.value = value : null,
activeColor: colorScheme.primary,
activeThumbColor: colorScheme.primary,
dense: true,
title: Text(
"allow_public_user_to_download",
@@ -135,7 +135,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
return SwitchListTile.adaptive(
value: allowUpload.value,
onChanged: newShareLink.value.isEmpty ? (value) => allowUpload.value = value : null,
activeColor: colorScheme.primary,
activeThumbColor: colorScheme.primary,
dense: true,
title: Text(
"allow_public_user_to_upload",
@@ -148,7 +148,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
return SwitchListTile.adaptive(
value: editExpiry.value,
onChanged: newShareLink.value.isEmpty ? (value) => editExpiry.value = value : null,
activeColor: colorScheme.primary,
activeThumbColor: colorScheme.primary,
dense: true,
title: Text(
"change_expiration_time",
+1 -1
View File
@@ -435,7 +435,7 @@ class SearchPage extends HookConsumerWidget {
}
},
icon: const Icon(Icons.more_vert_rounded),
tooltip: 'Show text search menu',
tooltip: 'show_text_search_menu'.tr(),
);
},
menuChildren: [
@@ -1,25 +1,25 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/widgets/settings/beta_sync_settings/beta_sync_settings.dart';
import 'package:immich_mobile/widgets/settings/beta_sync_settings/sync_status_and_actions.dart';
@RoutePage()
class BetaSyncSettingsPage extends StatelessWidget {
const BetaSyncSettingsPage({super.key});
class SyncStatusPage extends StatelessWidget {
const SyncStatusPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 0,
title: const Text("beta_sync").t(context: context),
title: const Text("sync_status").t(context: context),
leading: IconButton(
onPressed: () => context.maybePop(true),
splashRadius: 24,
icon: const Icon(Icons.arrow_back_ios_rounded),
),
),
body: const BetaSyncSettings(),
body: const SyncStatusAndActions(),
);
}
}
+23
View File
@@ -142,6 +142,29 @@ class BackgroundWorkerBgHostApi {
}
}
Future<void> showNotification(String title, String content) async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.showNotification$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[title, content]);
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else {
return;
}
}
Future<void> close() async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close$pigeonVar_messageChannelSuffix';
+87
View File
@@ -0,0 +1,87 @@
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
import 'dart:async';
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
import 'package:flutter/services.dart';
PlatformException _createConnectionError(String channelName) {
return PlatformException(
code: 'channel-error',
message: 'Unable to establish connection on channel: "$channelName".',
);
}
enum NetworkCapability { cellular, wifi, vpn, unmetered }
class _PigeonCodec extends StandardMessageCodec {
const _PigeonCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is int) {
buffer.putUint8(4);
buffer.putInt64(value);
} else if (value is NetworkCapability) {
buffer.putUint8(129);
writeValue(buffer, value.index);
} else {
super.writeValue(buffer, value);
}
}
@override
Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) {
case 129:
final int? value = readValue(buffer) as int?;
return value == null ? null : NetworkCapability.values[value];
default:
return super.readValueOfType(type, buffer);
}
}
}
class ConnectivityApi {
/// Constructor for [ConnectivityApi]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform.
ConnectivityApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
: pigeonVar_binaryMessenger = binaryMessenger,
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
final BinaryMessenger? pigeonVar_binaryMessenger;
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
final String pigeonVar_messageChannelSuffix;
Future<List<NetworkCapability>> getCapabilities() async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as List<Object?>?)!.cast<NetworkCapability>();
}
}
}
@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:drift/drift.dart' hide Column;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
@@ -135,7 +136,7 @@ class FeatInDevPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Features in Development'), centerTitle: true),
appBar: AppBar(title: Text('features_in_development'.tr()), centerTitle: true),
body: Column(
children: [
Flexible(
@@ -15,6 +15,7 @@ class MainTimelinePage extends ConsumerWidget {
return Timeline(
topSliverWidget: const SliverToBoxAdapter(child: DriftMemoryLane()),
topSliverWidgetHeight: hasMemories ? 200 : 0,
showStorageIndicator: true,
);
}
}
@@ -1,5 +1,6 @@
import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
@@ -55,7 +56,7 @@ class LocalMediaSummaryPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Local Media Summary')),
appBar: AppBar(title: Text('local_media_summary'.tr())),
body: Consumer(
builder: (ctx, ref, __) {
final db = ref.watch(driftProvider);
@@ -78,7 +79,7 @@ class LocalMediaSummaryPage extends StatelessWidget {
const Divider(),
Padding(
padding: const EdgeInsets.only(left: 15),
child: Text("Album summary", style: ctx.textTheme.titleMedium),
child: Text("album_summary".tr(), style: ctx.textTheme.titleMedium),
),
],
),
@@ -135,7 +136,7 @@ class RemoteMediaSummaryPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Remote Media Summary')),
appBar: AppBar(title: Text('remote_media_summary'.tr())),
body: Consumer(
builder: (ctx, ref, __) {
final db = ref.watch(driftProvider);
@@ -158,7 +159,7 @@ class RemoteMediaSummaryPage extends StatelessWidget {
const Divider(),
Padding(
padding: const EdgeInsets.only(left: 15),
child: Text("Album summary", style: ctx.textTheme.titleMedium),
child: Text("album_summary".tr(), style: ctx.textTheme.titleMedium),
),
],
),
@@ -210,7 +210,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget {
activityEnabled.value = value;
await ref.read(remoteAlbumProvider.notifier).setActivityStatus(album.id, value);
},
activeColor: activityEnabled.value ? context.primaryColor : context.themeData.disabledColor,
activeThumbColor: activityEnabled.value ? context.primaryColor : context.themeData.disabledColor,
dense: true,
title: Text(
"comments_and_likes",
@@ -0,0 +1,352 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
@RoutePage()
class AssetTroubleshootPage extends ConsumerWidget {
final BaseAsset asset;
const AssetTroubleshootPage({super.key, required this.asset});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(title: Text('asset_troubleshoot'.tr())),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: _AssetDetailsView(asset: asset),
),
),
);
}
}
class _AssetDetailsView extends ConsumerWidget {
final BaseAsset asset;
const _AssetDetailsView({required this.asset});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_AssetPropertiesSection(asset: asset),
const SizedBox(height: 16),
Text(
'matching_assets'.tr(),
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
if (asset.checksum != null) ...[
_LocalAssetsSection(asset: asset),
const SizedBox(height: 16),
_RemoteAssetSection(asset: asset),
] else ...[
_PropertySectionCard(
title: 'Local Assets',
properties: [_PropertyItem(label: 'Status', value: 'no_checksum_local'.tr())],
),
const SizedBox(height: 16),
_PropertySectionCard(
title: 'Remote Assets',
properties: [_PropertyItem(label: 'Status', value: 'no_checksum_remote'.tr())],
),
],
],
);
}
}
class _AssetPropertiesSection extends ConsumerStatefulWidget {
final BaseAsset asset;
const _AssetPropertiesSection({required this.asset});
@override
ConsumerState createState() => _AssetPropertiesSectionState();
}
class _AssetPropertiesSectionState extends ConsumerState<_AssetPropertiesSection> {
List<_PropertyItem> properties = [];
@override
void initState() {
super.initState();
_buildAssetProperties(widget.asset).whenComplete(() {
if (mounted) {
setState(() {});
}
});
}
@override
Widget build(BuildContext context) {
final title = _getAssetTypeTitle(widget.asset);
return _PropertySectionCard(title: title, properties: properties);
}
Future<void> _buildAssetProperties(BaseAsset asset) async {
_addCommonProperties();
if (asset is LocalAsset) {
await _addLocalAssetProperties(asset);
} else if (asset is RemoteAsset) {
await _addRemoteAssetProperties(asset);
}
}
void _addCommonProperties() {
final asset = widget.asset;
properties.addAll([
_PropertyItem(label: 'Name', value: asset.name),
_PropertyItem(label: 'Checksum', value: asset.checksum),
_PropertyItem(label: 'Type', value: asset.type.toString()),
_PropertyItem(label: 'Created At', value: asset.createdAt.toString()),
_PropertyItem(label: 'Updated At', value: asset.updatedAt.toString()),
_PropertyItem(label: 'Width', value: asset.width?.toString()),
_PropertyItem(label: 'Height', value: asset.height?.toString()),
_PropertyItem(
label: 'Duration',
value: asset.durationInSeconds != null ? '${asset.durationInSeconds} seconds' : null,
),
_PropertyItem(label: 'Is Favorite', value: asset.isFavorite.toString()),
_PropertyItem(label: 'Live Photo Video ID', value: asset.livePhotoVideoId),
]);
}
Future<void> _addLocalAssetProperties(LocalAsset asset) async {
properties.insertAll(0, [
_PropertyItem(label: 'Local ID', value: asset.id),
_PropertyItem(label: 'Remote ID', value: asset.remoteId),
]);
properties.insert(4, _PropertyItem(label: 'Orientation', value: asset.orientation.toString()));
final albums = await ref.read(assetServiceProvider).getSourceAlbums(asset.id);
properties.add(_PropertyItem(label: 'Album', value: albums.map((a) => a.name).join(', ')));
}
Future<void> _addRemoteAssetProperties(RemoteAsset asset) async {
properties.insertAll(0, [
_PropertyItem(label: 'Remote ID', value: asset.id),
_PropertyItem(label: 'Local ID', value: asset.localId),
_PropertyItem(label: 'Owner ID', value: asset.ownerId),
]);
final additionalProps = <_PropertyItem>[
_PropertyItem(label: 'Thumb Hash', value: asset.thumbHash),
_PropertyItem(label: 'Visibility', value: asset.visibility.toString()),
_PropertyItem(label: 'Stack ID', value: asset.stackId),
];
properties.insertAll(4, additionalProps);
final exif = await ref.read(assetServiceProvider).getExif(asset);
if (exif != null) {
_addExifProperties(exif);
} else {
properties.add(const _PropertyItem(label: 'EXIF', value: null));
}
}
void _addExifProperties(ExifInfo exif) {
properties.addAll([
_PropertyItem(
label: 'File Size',
value: exif.fileSize != null ? '${(exif.fileSize! / 1024 / 1024).toStringAsFixed(2)} MB' : null,
),
_PropertyItem(label: 'Description', value: exif.description),
_PropertyItem(label: 'EXIF Width', value: exif.width?.toString()),
_PropertyItem(label: 'EXIF Height', value: exif.height?.toString()),
_PropertyItem(label: 'Date Taken', value: exif.dateTimeOriginal?.toString()),
_PropertyItem(label: 'Time Zone', value: exif.timeZone),
_PropertyItem(label: 'Camera Make', value: exif.make),
_PropertyItem(label: 'Camera Model', value: exif.model),
_PropertyItem(label: 'Lens', value: exif.lens),
_PropertyItem(label: 'F-Number', value: exif.f != null ? 'f/${exif.fNumber}' : null),
_PropertyItem(label: 'Focal Length', value: exif.mm != null ? '${exif.focalLength}mm' : null),
_PropertyItem(label: 'ISO', value: exif.iso?.toString()),
_PropertyItem(label: 'Exposure Time', value: exif.exposureTime.isNotEmpty ? exif.exposureTime : null),
_PropertyItem(
label: 'GPS Coordinates',
value: exif.hasCoordinates ? '${exif.latitude}, ${exif.longitude}' : null,
),
_PropertyItem(
label: 'Location',
value: [exif.city, exif.state, exif.country].where((e) => e != null && e.isNotEmpty).join(', '),
),
]);
}
String _getAssetTypeTitle(BaseAsset asset) {
if (asset is LocalAsset) return 'Local Asset';
if (asset is RemoteAsset) return 'Remote Asset';
return 'Base Asset';
}
}
class _LocalAssetsSection extends ConsumerWidget {
final BaseAsset asset;
const _LocalAssetsSection({required this.asset});
@override
Widget build(BuildContext context, WidgetRef ref) {
final assetService = ref.watch(assetServiceProvider);
return FutureBuilder<List<LocalAsset?>>(
future: assetService.getLocalAssetsByChecksum(asset.checksum!),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const _PropertySectionCard(
title: 'Local Assets',
properties: [_PropertyItem(label: 'Status', value: 'Loading...')],
);
}
if (snapshot.hasError) {
return _PropertySectionCard(
title: 'Local Assets',
properties: [_PropertyItem(label: 'Error', value: snapshot.error.toString())],
);
}
final localAssets = snapshot.data?.cast<LocalAsset>() ?? [];
if (asset is LocalAsset) {
localAssets.removeWhere((a) => a.id == (asset as LocalAsset).id);
if (localAssets.isEmpty) {
return const SizedBox.shrink();
}
}
if (localAssets.isEmpty) {
return _PropertySectionCard(
title: 'Local Assets',
properties: [_PropertyItem(label: 'Status', value: 'no_local_assets_found'.tr())],
);
}
return Column(
children: [
if (localAssets.length > 1)
_PropertySectionCard(
title: 'Local Assets Summary',
properties: [_PropertyItem(label: 'Total Count', value: localAssets.length.toString())],
),
...localAssets.map((localAsset) {
return Padding(
padding: const EdgeInsets.only(top: 16),
child: _AssetPropertiesSection(asset: localAsset),
);
}),
],
);
},
);
}
}
class _RemoteAssetSection extends ConsumerWidget {
final BaseAsset asset;
const _RemoteAssetSection({required this.asset});
@override
Widget build(BuildContext context, WidgetRef ref) {
final assetService = ref.watch(assetServiceProvider);
if (asset is RemoteAsset) {
return const SizedBox.shrink();
}
return FutureBuilder<RemoteAsset?>(
future: assetService.getRemoteAssetByChecksum(asset.checksum!),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const _PropertySectionCard(
title: 'Remote Assets',
properties: [_PropertyItem(label: 'Status', value: 'Loading...')],
);
}
if (snapshot.hasError) {
return _PropertySectionCard(
title: 'Remote Assets',
properties: [_PropertyItem(label: 'Error', value: snapshot.error.toString())],
);
}
final remoteAsset = snapshot.data;
if (remoteAsset == null) {
return _PropertySectionCard(
title: 'Remote Assets',
properties: [_PropertyItem(label: 'Status', value: 'no_remote_assets_found'.tr())],
);
}
return _AssetPropertiesSection(asset: remoteAsset);
},
);
}
}
class _PropertySectionCard extends StatelessWidget {
final String title;
final List<_PropertyItem> properties;
const _PropertySectionCard({required this.title, required this.properties});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(vertical: 8),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
...properties,
],
),
),
);
}
}
class _PropertyItem extends StatelessWidget {
final String label;
final String? value;
const _PropertyItem({required this.label, this.value});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 120,
child: Text('$label:', style: const TextStyle(fontWeight: FontWeight.w500)),
),
Expanded(
child: Text(
value ?? 'not_available'.tr(),
style: TextStyle(color: Theme.of(context).colorScheme.secondary),
),
),
],
),
);
}
}
@@ -1,4 +1,5 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
@@ -302,7 +303,9 @@ class _LocalAlbumsCollectionCard extends ConsumerWidget {
}).toList();
},
error: (error, _) {
return [Center(child: Text('Error: $error'))];
return [
Center(child: Text('error_saving_image'.tr(args: [error.toString()]))),
];
},
loading: () {
return [const Center(child: CircularProgressIndicator())];
@@ -1,4 +1,5 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
@@ -46,9 +47,9 @@ class _AlbumList extends ConsumerWidget {
),
data: (albums) {
if (albums.isEmpty) {
return const SliverToBoxAdapter(
return SliverToBoxAdapter(
child: Center(
child: Padding(padding: EdgeInsets.all(20.0), child: Text('No albums found')),
child: Padding(padding: const EdgeInsets.all(20.0), child: Text('no_albums_yet'.tr())),
),
);
}
@@ -10,6 +10,7 @@ import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
import 'package:immich_mobile/utils/debug_print.dart';
@RoutePage()
class DriftPartnerDetailPage extends StatelessWidget {
@@ -68,7 +69,7 @@ class _InfoBoxState extends ConsumerState<_InfoBox> {
_inTimeline = !_inTimeline;
});
} catch (error, stack) {
debugPrint("Failed to toggle in timeline: $error $stack");
dPrint(() => "Failed to toggle in timeline: $error $stack");
ImmichToast.show(
context: context,
toastType: ToastType.error,
@@ -3,9 +3,8 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
@@ -26,11 +25,9 @@ final driftUsersProvider = FutureProvider.autoDispose<List<UserDto>>((ref) async
id: entity.id,
name: entity.name,
email: entity.email,
isAdmin: entity.isAdmin,
updatedAt: entity.updatedAt,
isPartnerSharedBy: false,
isPartnerSharedWith: false,
avatarColor: AvatarColor.primary,
avatarColor: entity.avatarColor,
memoryEnabled: true,
inTimeline: true,
profileChangedAt: entity.profileChangedAt,
@@ -26,6 +26,7 @@ class LocalTimelinePage extends StatelessWidget {
child: Timeline(
appBar: MesmerizingSliverAppBar(title: album.name),
bottomSheet: const LocalAlbumBottomSheet(),
showStorageIndicator: true,
),
);
}
@@ -440,7 +440,7 @@ class DriftSearchPage extends HookConsumerWidget {
}
},
icon: const Icon(Icons.more_vert_rounded),
tooltip: 'Show text search menu',
tooltip: 'show_text_search_menu'.tr(),
);
},
menuChildren: [
@@ -633,6 +633,7 @@ class _SearchResultGrid extends ConsumerWidget {
groupBy: GroupAssetsBy.none,
appBar: null,
bottomSheet: const GeneralBottomSheet(minChildSize: 0.20),
withScrubber: false,
),
),
),
@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
class AdvancedInfoActionButton extends ConsumerWidget {
final ActionSource source;
const AdvancedInfoActionButton({super.key, required this.source});
void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) {
return;
}
ref.read(actionProvider.notifier).troubleshoot(source, context);
}
@override
Widget build(BuildContext context, WidgetRef ref) {
return BaseActionButton(
maxWidth: 115.0,
iconData: Icons.help_outline_rounded,
label: "troubleshoot".t(context: context),
onPressed: () => _onTap(context, ref),
);
}
}
@@ -1,4 +1,5 @@
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
@@ -58,7 +59,7 @@ class LikeActivityActionButton extends ConsumerWidget {
label: "like".t(context: context),
menuItem: menuItem,
),
error: (error, stack) => Text("Error: $error"),
error: (error, stack) => Text('error_saving_image'.tr(args: [error.toString()])),
);
}
}
@@ -40,6 +40,7 @@ class AlbumSelector extends ConsumerStatefulWidget {
class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
bool isGrid = false;
final searchController = TextEditingController();
final menuController = MenuController();
final searchFocusNode = FocusNode();
List<RemoteAlbum> sortedAlbums = [];
List<RemoteAlbum> shownAlbums = [];
@@ -157,34 +158,45 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
await sortAlbums();
});
return MultiSliver(
children: [
_SearchBar(
searchController: searchController,
searchFocusNode: searchFocusNode,
onSearch: onSearch,
filterMode: filter.mode,
onClearSearch: clearSearch,
),
_QuickFilterButtonRow(
filterMode: filter.mode,
onChangeFilter: changeFilter,
onSearch: onSearch,
searchController: searchController,
),
_QuickSortAndViewMode(isGrid: isGrid, onToggleViewMode: toggleViewMode, onSortChanged: changeSort),
isGrid
? _AlbumGrid(albums: shownAlbums, userId: userId, onAlbumSelected: widget.onAlbumSelected)
: _AlbumList(albums: shownAlbums, userId: userId, onAlbumSelected: widget.onAlbumSelected),
],
return PopScope(
onPopInvokedWithResult: (didPop, _) {
menuController.close();
},
child: MultiSliver(
children: [
_SearchBar(
searchController: searchController,
searchFocusNode: searchFocusNode,
onSearch: onSearch,
filterMode: filter.mode,
onClearSearch: clearSearch,
),
_QuickFilterButtonRow(
filterMode: filter.mode,
onChangeFilter: changeFilter,
onSearch: onSearch,
searchController: searchController,
),
_QuickSortAndViewMode(
isGrid: isGrid,
onToggleViewMode: toggleViewMode,
onSortChanged: changeSort,
controller: menuController,
),
isGrid
? _AlbumGrid(albums: shownAlbums, userId: userId, onAlbumSelected: widget.onAlbumSelected)
: _AlbumList(albums: shownAlbums, userId: userId, onAlbumSelected: widget.onAlbumSelected),
],
),
);
}
}
class _SortButton extends ConsumerStatefulWidget {
const _SortButton(this.onSortChanged);
const _SortButton(this.onSortChanged, {this.controller});
final Future<void> Function(AlbumSort) onSortChanged;
final MenuController? controller;
@override
ConsumerState<_SortButton> createState() => _SortButtonState();
@@ -220,6 +232,7 @@ class _SortButtonState extends ConsumerState<_SortButton> {
@override
Widget build(BuildContext context) {
return MenuAnchor(
controller: widget.controller,
style: MenuStyle(
elevation: const WidgetStatePropertyAll(1),
shape: WidgetStateProperty.all(
@@ -449,10 +462,16 @@ class _QuickFilterButton extends StatelessWidget {
}
class _QuickSortAndViewMode extends StatelessWidget {
const _QuickSortAndViewMode({required this.isGrid, required this.onToggleViewMode, required this.onSortChanged});
const _QuickSortAndViewMode({
required this.isGrid,
required this.onToggleViewMode,
required this.onSortChanged,
this.controller,
});
final bool isGrid;
final VoidCallback onToggleViewMode;
final MenuController? controller;
final Future<void> Function(AlbumSort) onSortChanged;
@override
@@ -463,7 +482,7 @@ class _QuickSortAndViewMode extends StatelessWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_SortButton(onSortChanged),
_SortButton(onSortChanged, controller: controller),
IconButton(
icon: Icon(isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined, size: 24),
onPressed: onToggleViewMode,
@@ -485,9 +504,9 @@ class _AlbumList extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
if (albums.isEmpty) {
return const SliverToBoxAdapter(
return SliverToBoxAdapter(
child: Center(
child: Padding(padding: EdgeInsets.all(20.0), child: Text('No albums found')),
child: Padding(padding: const EdgeInsets.all(20.0), child: Text('album_search_not_found'.tr())),
),
);
}
@@ -580,9 +599,9 @@ class _AlbumGrid extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (albums.isEmpty) {
return const SliverToBoxAdapter(
return SliverToBoxAdapter(
child: Center(
child: Padding(padding: EdgeInsets.all(20.0), child: Text('No albums found')),
child: Padding(padding: const EdgeInsets.all(20.0), child: Text('album_search_not_found'.tr())),
),
);
}
@@ -10,6 +10,7 @@ import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/extensions/scroll_extensions.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.provider.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.widget.dart';
@@ -30,7 +31,6 @@ import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
import 'package:immich_mobile/widgets/photo_view/photo_view.dart';
import 'package:immich_mobile/widgets/photo_view/photo_view_gallery.dart';
import 'package:platform/platform.dart';
@RoutePage()
class AssetViewerPage extends StatelessWidget {
@@ -53,10 +53,9 @@ class AssetViewerPage extends StatelessWidget {
class AssetViewer extends ConsumerStatefulWidget {
final int initialIndex;
final Platform? platform;
final int? heroOffset;
const AssetViewer({super.key, required this.initialIndex, this.platform, this.heroOffset});
const AssetViewer({super.key, required this.initialIndex, this.heroOffset});
@override
ConsumerState createState() => _AssetViewerState();
@@ -86,7 +85,6 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
PhotoViewControllerBase? viewController;
StreamSubscription? reloadSubscription;
late Platform platform;
late final int heroOffset;
late PhotoViewControllerValue initialPhotoViewState;
bool? hasDraggedDown;
@@ -114,7 +112,6 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
super.initState();
assert(ref.read(currentAssetNotifier) != null, "Current asset should not be null when opening the AssetViewer");
pageController = PageController(initialPage: widget.initialIndex);
platform = widget.platform ?? const LocalPlatform();
totalAssets = ref.read(timelineServiceProvider).totalAssets;
bottomSheetController = DraggableScrollableController();
WidgetsBinding.instance.addPostFrameCallback(_onAssetInit);
@@ -638,7 +635,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
gaplessPlayback: true,
loadingBuilder: _placeholderBuilder,
pageController: pageController,
scrollPhysics: platform.isIOS
scrollPhysics: CurrentPlatform.isIOS
? const FastScrollPhysics() // Use bouncing physics for iOS
: const FastClampingScrollPhysics(), // Use heavy physics for Android
itemCount: totalAssets,
@@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart';
@@ -14,6 +15,7 @@ import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
import 'package:immich_mobile/providers/routes.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
@@ -41,6 +43,7 @@ class AssetDetailBottomSheet extends ConsumerWidget {
final isInLockedView = ref.watch(inLockedViewProvider);
final currentAlbum = ref.watch(currentRemoteAlbumProvider);
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting);
final buttonContext = ActionButtonContext(
asset: asset,
@@ -49,6 +52,7 @@ class AssetDetailBottomSheet extends ConsumerWidget {
isTrashEnabled: isTrashEnable,
isInLockedView: isInLockedView,
currentAlbum: currentAlbum,
advancedTroubleshooting: advancedTroubleshooting,
source: ActionSource.viewer,
);
@@ -122,6 +126,10 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
return [fNumber, exposureTime, focalLength, iso].where((spec) => spec != null && spec.isNotEmpty).join(_kSeparator);
}
Future<void> _editDateTime(BuildContext context, WidgetRef ref) async {
await ref.read(actionProvider.notifier).editDateTime(ActionSource.viewer, context);
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final asset = ref.watch(currentAssetNotifier);
@@ -132,10 +140,6 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull;
final cameraTitle = _getCameraInfoTitle(exifInfo);
Future<void> editDateTime() async {
await ref.read(actionProvider.notifier).editDateTime(ActionSource.viewer, context);
}
return SliverList.list(
children: [
// Asset Date and Time
@@ -143,7 +147,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
title: _getDateTime(context, asset),
titleStyle: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
trailing: asset.hasRemote ? const Icon(Icons.edit, size: 18) : null,
onTap: asset.hasRemote ? () async => await editDateTime() : null,
onTap: asset.hasRemote ? () async => await _editDateTime(context, ref) : null,
),
if (exifInfo != null) _SheetAssetDescription(exif: exifInfo),
const SheetPeopleDetails(),
@@ -14,9 +14,10 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_act
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/providers/routes.provider.dart';
import 'package:immich_mobile/providers/tab.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
@@ -38,8 +39,9 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
final previousRouteName = ref.watch(previousRouteNameProvider);
final tabRoute = ref.watch(tabProvider);
final showViewInTimelineButton =
previousRouteName != TabShellRoute.name &&
(previousRouteName != TabShellRoute.name || tabRoute == TabEnum.search) &&
previousRouteName != AssetViewerRoute.name &&
previousRouteName != null;
@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart';
@@ -43,7 +43,7 @@ class ArchiveBottomSheet extends ConsumerWidget {
const EditDateTimeActionButton(source: ActionSource.timeline),
const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(source: ActionSource.timeline),
const StackActionButton(source: ActionSource.timeline),
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
],
if (multiselect.hasLocal) ...[
const DeleteLocalActionButton(source: ActionSource.timeline),
@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart';
@@ -43,7 +43,7 @@ class FavoriteBottomSheet extends ConsumerWidget {
const EditDateTimeActionButton(source: ActionSource.timeline),
const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(source: ActionSource.timeline),
const StackActionButton(source: ActionSource.timeline),
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
],
if (multiselect.hasLocal) ...[
const DeleteLocalActionButton(source: ActionSource.timeline),
@@ -4,6 +4,8 @@ import 'package:hooks_riverpod/hooks_riverpod.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/domain/models/setting.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';
@@ -21,6 +23,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
@@ -51,6 +54,7 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
Widget build(BuildContext context) {
final multiselect = ref.watch(multiSelectProvider);
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting);
Future<void> addAssetsToAlbum(RemoteAlbum album) async {
final selectedAssets = multiselect.selectedAssets;
@@ -88,19 +92,22 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
maxChildSize: 0.85,
shouldCloseOnMinExtent: false,
actions: [
if (multiselect.selectedAssets.length == 1 && advancedTroubleshooting) ...[
const AdvancedInfoActionButton(source: ActionSource.timeline),
],
const ShareActionButton(source: ActionSource.timeline),
if (multiselect.hasRemote) ...[
const ShareLinkActionButton(source: ActionSource.timeline),
const ArchiveActionButton(source: ActionSource.timeline),
const FavoriteActionButton(source: ActionSource.timeline),
const DownloadActionButton(source: ActionSource.timeline),
const EditDateTimeActionButton(source: ActionSource.timeline),
const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(source: ActionSource.timeline),
const StackActionButton(source: ActionSource.timeline),
isTrashEnable
? const TrashActionButton(source: ActionSource.timeline)
: const DeletePermanentActionButton(source: ActionSource.timeline),
const FavoriteActionButton(source: ActionSource.timeline),
const ArchiveActionButton(source: ActionSource.timeline),
const EditDateTimeActionButton(source: ActionSource.timeline),
const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(source: ActionSource.timeline),
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
const DeleteActionButton(source: ActionSource.timeline),
],
if (multiselect.hasLocal || multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
@@ -1,12 +1,12 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/translate_extensions.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/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart';
@@ -100,7 +100,7 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
const EditDateTimeActionButton(source: ActionSource.timeline),
const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(source: ActionSource.timeline),
const StackActionButton(source: ActionSource.timeline),
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
],
if (multiselect.hasLocal) ...[
const DeleteLocalActionButton(source: ActionSource.timeline),
@@ -98,7 +98,7 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
final operation = cachedOperation;
if (operation != null) {
this.cachedOperation = null;
cachedOperation = null;
operation.cancel();
}
}
@@ -8,6 +8,7 @@ import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/infrastructure/people.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:scroll_date_picker/scroll_date_picker.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class DriftPersonBirthdayEditForm extends ConsumerStatefulWidget {
final DriftPerson person;
@@ -36,7 +37,7 @@ class _DriftPersonNameEditFormState extends ConsumerState<DriftPersonBirthdayEdi
context.pop<DateTime>(_selectedDate);
}
} catch (error) {
debugPrint('Error updating birthday: $error');
dPrint(() => 'Error updating birthday: $error');
if (!context.mounted) {
return;
@@ -7,6 +7,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/infrastructure/people.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class DriftPersonNameEditForm extends ConsumerStatefulWidget {
final DriftPerson person;
@@ -34,7 +35,7 @@ class _DriftPersonNameEditFormState extends ConsumerState<DriftPersonNameEditFor
context.pop<String>(newName);
}
} catch (error) {
debugPrint('Error updating name: $error');
dPrint(() => 'Error updating name: $error');
if (!context.mounted) {
return;
@@ -3,14 +3,15 @@ import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
import 'package:intl/intl.dart' hide TextDirection;
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:intl/intl.dart' hide TextDirection;
/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged
/// for quick navigation of the BoxScrollView.
@@ -45,7 +46,7 @@ class Scrubber extends ConsumerStatefulWidget {
}
List<_Segment> _buildSegments({required List<Segment> layoutSegments, required double timelineHeight}) {
const double offsetThreshold = 20.0;
const double offsetThreshold = 40.0;
final segments = <_Segment>[];
if (layoutSegments.isEmpty || layoutSegments.first.bucket is! TimeBucket) {
@@ -79,6 +80,7 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
double _thumbTopOffset = 0.0;
bool _isDragging = false;
List<_Segment> _segments = [];
int _monthCount = 0;
late AnimationController _thumbAnimationController;
Timer? _fadeOutTimer;
@@ -105,6 +107,7 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
_thumbAnimationController = AnimationController(vsync: this, duration: kTimelineScrubberFadeInDuration);
_thumbAnimation = CurvedAnimation(parent: _thumbAnimationController, curve: Curves.fastEaseInToSlowEaseOut);
_labelAnimationController = AnimationController(vsync: this, duration: kTimelineScrubberFadeInDuration);
_monthCount = getMonthCount();
_labelAnimation = CurvedAnimation(parent: _labelAnimationController, curve: Curves.fastOutSlowIn);
}
@@ -121,6 +124,7 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
if (oldWidget.layoutSegments.lastOrNull?.endOffset != widget.layoutSegments.lastOrNull?.endOffset) {
_segments = _buildSegments(layoutSegments: widget.layoutSegments, timelineHeight: _scrubberHeight);
_monthCount = getMonthCount();
}
}
@@ -140,6 +144,10 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
});
}
int getMonthCount() {
return _segments.map((e) => "${e.date.month}_${e.date.year}").toSet().length;
}
bool _onScrollNotification(ScrollNotification notification) {
if (_isDragging) {
// If the user is dragging the thumb, we don't want to update the position
@@ -169,7 +177,10 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
}
void _onDragStart(DragStartDetails _) {
ref.read(timelineStateProvider.notifier).setScrubbing(true);
if (_monthCount >= kMinMonthsToEnableScrubberSnap) {
ref.read(timelineStateProvider.notifier).setScrubbing(true);
}
setState(() {
_isDragging = true;
_labelAnimationController.forward();
@@ -191,13 +202,22 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
final nearestMonthSegment = _findNearestMonthSegment(dragPosition);
if (nearestMonthSegment != null) {
_snapToSegment(nearestMonthSegment);
final label = nearestMonthSegment.scrollLabel;
if (_lastLabel != label) {
ref.read(hapticFeedbackProvider.notifier).selectionClick();
_lastLabel = label;
}
}
if (_monthCount < kMinMonthsToEnableScrubberSnap) {
// If there are less than kMinMonthsToEnableScrubberSnap months, we don't need to snap to segments
setState(() {
_thumbTopOffset = dragPosition;
_scrollController.jumpTo((dragPosition / _scrubberHeight) * _scrollController.position.maxScrollExtent);
});
} else if (nearestMonthSegment != null) {
_snapToSegment(nearestMonthSegment);
}
}
/// Calculate the drag position relative to the scrubber area
@@ -370,7 +390,7 @@ class _SegmentWidget extends StatelessWidget {
Widget build(BuildContext context) {
return IgnorePointer(
child: Container(
margin: const EdgeInsets.only(right: 12.0),
margin: const EdgeInsets.only(right: 36.0),
child: Material(
color: context.colorScheme.surface,
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
@@ -32,17 +32,17 @@ class Timeline extends StatelessWidget {
super.key,
this.topSliverWidget,
this.topSliverWidgetHeight,
this.showStorageIndicator,
this.showStorageIndicator = false,
this.withStack = false,
this.appBar = const ImmichSliverAppBar(floating: true, pinned: false, snap: false),
this.bottomSheet = const GeneralBottomSheet(),
this.bottomSheet = const GeneralBottomSheet(minChildSize: 0.18),
this.groupBy,
this.withScrubber = true,
});
final Widget? topSliverWidget;
final double? topSliverWidgetHeight;
final bool? showStorageIndicator;
final bool showStorageIndicator;
final Widget? appBar;
final Widget? bottomSheet;
final bool withStack;
@@ -1,8 +1,8 @@
import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/log.service.dart';
import 'package:immich_mobile/domain/utils/isolate_lock_manager.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/models/backup/backup_state.model.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
@@ -19,7 +19,6 @@ import 'package:immich_mobile/providers/memory.provider.dart';
import 'package:immich_mobile/providers/notification_permission.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/tab.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/providers/websocket.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/services/background.service.dart';
@@ -125,93 +124,58 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
}
}
Future<void> _safeRun(Future<void> action, String debugName) async {
if (!_shouldContinueOperation()) {
return;
}
try {
await action;
} catch (e, stackTrace) {
_log.warning("Error during $debugName operation", e, stackTrace);
}
}
Future<void> _handleBetaTimelineResume() async {
_ref.read(backupProvider.notifier).cancelBackup();
final lockManager = _ref.read(isolateLockManagerProvider(kIsolateLockManagerPort));
// Give isolates time to complete any ongoing database transactions
await Future.delayed(const Duration(milliseconds: 500));
lockManager.requestHolderToClose();
// Add timeout to prevent deadlock on lock acquisition
try {
await lockManager.acquireLock().timeout(
const Duration(seconds: 10),
onTimeout: () {
_log.warning("Lock acquisition timed out, proceeding without lock");
throw TimeoutException("Lock acquisition timed out", const Duration(seconds: 10));
},
);
} catch (e) {
_log.warning("Failed to acquire lock: $e");
return;
}
final backgroundManager = _ref.read(backgroundSyncProvider);
final isAlbumLinkedSyncEnable = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
try {
// Run operations sequentially with state checks and error handling for each
if (_shouldContinueOperation()) {
try {
await backgroundManager.syncLocal();
} catch (e, stackTrace) {
_log.warning("Failed syncLocal: $e", e, stackTrace);
}
}
await Future.wait([
_safeRun(backgroundManager.syncLocal(), "syncLocal"),
_safeRun(backgroundManager.syncRemote(), "syncRemote"),
]);
// Check if app is still active before hashing
if (_shouldContinueOperation()) {
try {
await backgroundManager.hashAssets();
} catch (e, stackTrace) {
_log.warning("Failed hashAssets: $e", e, stackTrace);
}
}
await Future.wait([
_safeRun(backgroundManager.hashAssets(), "hashAssets").then((_) {
_resumeBackup();
}),
_resumeBackup(),
]);
// Check if app is still active before remote sync
if (_shouldContinueOperation()) {
try {
await backgroundManager.syncRemote();
} catch (e, stackTrace) {
_log.warning("Failed syncRemote: $e", e, stackTrace);
}
if (isAlbumLinkedSyncEnable && _shouldContinueOperation()) {
try {
await backgroundManager.syncLinkedAlbum();
} catch (e, stackTrace) {
_log.warning("Failed syncLinkedAlbum: $e", e, stackTrace);
}
}
}
// Handle backup resume only if still active
if (_shouldContinueOperation()) {
final isEnableBackup = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
if (isEnableBackup) {
final currentUser = _ref.read(currentUserProvider);
if (currentUser != null) {
try {
await _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
_log.fine("Completed backup resume");
} catch (e, stackTrace) {
_log.warning("Failed backup resume: $e", e, stackTrace);
}
}
}
if (isAlbumLinkedSyncEnable) {
await _safeRun(backgroundManager.syncLinkedAlbum(), "syncLinkedAlbum");
}
} catch (e, stackTrace) {
_log.severe("Error during background sync", e, stackTrace);
} finally {
// Ensure lock is released even if operations fail
try {
lockManager.releaseLock();
_log.fine("Lock released after background sync operations");
} catch (lockError) {
_log.warning("Failed to release lock after error: $lockError");
}
}
Future<void> _resumeBackup() async {
final isEnableBackup = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
if (isEnableBackup) {
final currentUser = Store.tryGet(StoreKey.currentUser);
if (currentUser != null) {
await _safeRun(
_ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id),
"handleBackupResume",
);
}
}
}
@@ -263,28 +227,6 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
if (_ref.read(backupProvider.notifier).backupProgress != BackUpProgressEnum.manualInProgress) {
_ref.read(backupProvider.notifier).cancelBackup();
}
} else {
final backgroundManager = _ref.read(backgroundSyncProvider);
// Cancel operations with extended timeout to allow database transactions to complete
try {
await Future.wait([
backgroundManager.cancel().timeout(const Duration(seconds: 10)),
backgroundManager.cancelLocal().timeout(const Duration(seconds: 10)),
]).timeout(const Duration(seconds: 15));
// Give additional time for isolates to clean up database connections
await Future.delayed(const Duration(milliseconds: 1000));
} catch (e) {
_log.warning("Timeout during background cancellation: $e");
}
// Always release the lock, even if cancellation failed
try {
_ref.read(isolateLockManagerProvider(kIsolateLockManagerPort)).releaseLock();
} catch (e) {
_log.warning("Failed to release lock on pause: $e");
}
}
_ref.read(websocketProvider.notifier).disconnect();
@@ -312,7 +254,6 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
} catch (_) {}
if (Store.isBetaTimelineEnabled) {
_ref.read(isolateLockManagerProvider(kIsolateLockManagerPort)).releaseLock();
return;
}
+2 -2
View File
@@ -1,4 +1,3 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
@@ -13,6 +12,7 @@ import 'package:immich_mobile/services/etag.service.dart';
import 'package:immich_mobile/services/exif.service.dart';
import 'package:immich_mobile/services/sync.service.dart';
import 'package:logging/logging.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final assetProvider = StateNotifierProvider<AssetNotifier, bool>((ref) {
return AssetNotifier(
@@ -68,7 +68,7 @@ class AssetNotifier extends StateNotifier<bool> {
}
final bool newRemote = await _assetService.refreshRemoteAssets();
final bool newLocal = await _albumService.refreshDeviceAlbums();
debugPrint("changedUsers: $changedUsers, newRemote: $newRemote, newLocal: $newLocal");
dPrint(() => "changedUsers: $changedUsers, newRemote: $newRemote, newLocal: $newLocal");
if (newRemote) {
_ref.invalidate(memoryFutureProvider);
}
+2 -5
View File
@@ -1,4 +1,3 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_udid/flutter_udid.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
@@ -18,6 +17,7 @@ import 'package:immich_mobile/services/widget.service.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
return AuthNotifier(
@@ -150,10 +150,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
_log.severe("Error getting user information from the server [API EXCEPTION]", stackTrace);
} catch (error, stackTrace) {
_log.severe("Error getting user information from the server [CATCH ALL]", error, stackTrace);
if (kDebugMode) {
debugPrint("Error getting user information from the server [CATCH ALL] $error $stackTrace");
}
dPrint(() => "Error getting user information from the server [CATCH ALL] $error $stackTrace");
}
// If the user is null, the login was not successful
@@ -1,6 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/utils/background_sync.dart';
import 'package:immich_mobile/domain/utils/isolate_lock_manager.dart';
import 'package:immich_mobile/providers/sync_status.provider.dart';
final backgroundSyncProvider = Provider<BackgroundSyncManager>((ref) {
@@ -19,7 +18,3 @@ final backgroundSyncProvider = Provider<BackgroundSyncManager>((ref) {
ref.onDispose(manager.cancel);
return manager;
});
final isolateLockManagerProvider = Provider.family<IsolateLockManager, String>((ref, name) {
return IsolateLockManager(portName: name);
});
@@ -2,11 +2,8 @@ import 'dart:io';
import 'package:cancellation_token_http/http.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/background_worker.service.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
@@ -18,7 +15,6 @@ import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
import 'package:immich_mobile/models/server_info/server_disk_info.model.dart';
import 'package:immich_mobile/platform/background_worker_api.g.dart';
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
@@ -35,8 +31,7 @@ import 'package:immich_mobile/utils/diff.dart';
import 'package:logging/logging.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
final driftBackgroundUploadFgService = Provider((ref) => BackgroundWorkerFgService(BackgroundWorkerFgHostApi()));
import 'package:immich_mobile/utils/debug_print.dart';
final backupProvider = StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
return BackupNotifier(
@@ -290,7 +285,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
state = state.copyWith(selectedBackupAlbums: selectedAlbums, excludedBackupAlbums: excludedAlbums);
log.info("_getBackupAlbumsInfo: Found ${availableAlbums.length} available albums");
debugPrint("_getBackupAlbumsInfo takes ${stopwatch.elapsedMilliseconds}ms");
dPrint(() => "_getBackupAlbumsInfo takes ${stopwatch.elapsedMilliseconds}ms");
}
///
@@ -432,7 +427,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
/// Invoke backup process
Future<void> startBackupProcess() async {
debugPrint("Start backup process");
dPrint(() => "Start backup process");
assert(state.backupProgress == BackUpProgressEnum.idle);
state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress);
@@ -4,15 +4,15 @@ import 'dart:convert';
import 'package:background_downloader/background_downloader.dart';
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/services/upload.service.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:logging/logging.dart';
class EnqueueStatus {
@@ -234,10 +234,6 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
switch (update.status) {
case TaskStatus.complete:
if (update.task.group == kBackupGroup) {
state = state.copyWith(backupCount: state.backupCount + 1, remainderCount: state.remainderCount - 1);
}
// Remove the completed task from the upload items
if (state.uploadItems.containsKey(taskId)) {
Future.delayed(const Duration(milliseconds: 1000), () {
@@ -329,16 +325,16 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
}
Future<void> cancel() async {
debugPrint("Canceling backup tasks...");
dPrint(() => "Canceling backup tasks...");
state = state.copyWith(enqueueCount: 0, enqueueTotalCount: 0, isCanceling: true);
final activeTaskCount = await _uploadService.cancelBackup();
if (activeTaskCount > 0) {
debugPrint("$activeTaskCount tasks left, continuing to cancel...");
dPrint(() => "$activeTaskCount tasks left, continuing to cancel...");
await cancel();
} else {
debugPrint("All tasks canceled successfully.");
dPrint(() => "All tasks canceled successfully.");
// Clear all upload items when cancellation is complete
state = state.copyWith(isCanceling: false, uploadItems: {});
}
@@ -380,5 +376,5 @@ final driftCandidateBackupAlbumInfoProvider = FutureProvider.autoDispose.family<
ref,
assetId,
) {
return ref.read(backupRepositoryProvider).getSourceAlbums(assetId);
return ref.read(localAssetRepository).getSourceAlbums(assetId, backupSelection: BackupSelection.selected);
});
@@ -1,4 +1,3 @@
import 'dart:async';
import 'dart:io';
import 'package:cancellation_token_http/http.dart';
@@ -31,6 +30,7 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:logging/logging.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
import 'package:immich_mobile/utils/debug_print.dart';
final manualUploadProvider = StateNotifierProvider<ManualUploadNotifier, ManualUploadState>((ref) {
return ManualUploadNotifier(
@@ -217,7 +217,7 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
);
if (uploadAssets.isEmpty) {
debugPrint("[_startUpload] No Assets to upload - Abort Process");
dPrint(() => "[_startUpload] No Assets to upload - Abort Process");
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
return false;
}
@@ -294,11 +294,11 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
);
}
} else {
unawaited(openAppSettings());
debugPrint("[_startUpload] Do not have permission to the gallery");
openAppSettings();
dPrint(() => "[_startUpload] Do not have permission to the gallery");
}
} catch (e) {
debugPrint("ERROR _startUpload: ${e.toString()}");
dPrint(() => "ERROR _startUpload: ${e.toString()}");
hasErrors = true;
} finally {
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
@@ -341,7 +341,7 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
// waits until it has stopped to start the backup.
final bool hasLock = await ref.read(backgroundServiceProvider).acquireLock();
if (!hasLock) {
debugPrint("[uploadAssets] could not acquire lock, exiting");
dPrint(() => "[uploadAssets] could not acquire lock, exiting");
ImmichToast.show(
context: context,
msg: "failed".tr(),
@@ -356,18 +356,18 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
// check if backup is already in process - then return
if (_backupProvider.backupProgress == BackUpProgressEnum.manualInProgress) {
debugPrint("[uploadAssets] Manual upload is already running - abort");
dPrint(() => "[uploadAssets] Manual upload is already running - abort");
showInProgress = true;
}
if (_backupProvider.backupProgress == BackUpProgressEnum.inProgress) {
debugPrint("[uploadAssets] Auto Backup is already in progress - abort");
dPrint(() => "[uploadAssets] Auto Backup is already in progress - abort");
showInProgress = true;
return false;
}
if (_backupProvider.backupProgress == BackUpProgressEnum.inBackground) {
debugPrint("[uploadAssets] Background backup is running - abort");
dPrint(() => "[uploadAssets] Background backup is running - abort");
showInProgress = true;
}
@@ -1,5 +1,4 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:background_downloader/background_downloader.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/constants/enums.dart';
@@ -8,6 +7,7 @@ import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/action.service.dart';
import 'package:immich_mobile/services/download.service.dart';
import 'package:immich_mobile/services/timeline.service.dart';
@@ -64,7 +64,7 @@ class ActionNotifier extends Notifier<void> {
void _downloadLivePhotoCallback(TaskStatusUpdate update) async {
if (update.status == TaskStatus.complete) {
final livePhotosId = LivePhotosMetadata.fromJson(update.task.metaData).id;
unawaited(_downloadService.saveLivePhotos(update.task, livePhotosId));
_downloadService.saveLivePhotos(update.task, livePhotosId);
}
}
@@ -117,6 +117,16 @@ class ActionNotifier extends Notifier<void> {
};
}
Future<ActionResult> troubleshoot(ActionSource source, BuildContext context) async {
final assets = _getAssets(source);
if (assets.length > 1) {
return ActionResult(count: assets.length, success: false, error: 'Cannot troubleshoot multiple assets');
}
context.pushRoute(AssetTroubleshootRoute(asset: assets.first));
return ActionResult(count: assets.length, success: true);
}
Future<ActionResult> shareLink(ActionSource source, BuildContext context) async {
final ids = _getRemoteIdsForSource(source);
try {
@@ -1,7 +1,14 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/services/background_worker.service.dart';
import 'package:immich_mobile/platform/background_worker_api.g.dart';
import 'package:immich_mobile/platform/connectivity_api.g.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/platform/thumbnail_api.g.dart';
final backgroundWorkerFgServiceProvider = Provider((_) => BackgroundWorkerFgService(BackgroundWorkerFgHostApi()));
final nativeSyncApiProvider = Provider<NativeSyncApi>((_) => NativeSyncApi());
final connectivityApiProvider = Provider<ConnectivityApi>((_) => ConnectivityApi());
final thumbnailApi = ThumbnailApi();
+4 -3
View File
@@ -7,11 +7,12 @@ import 'package:immich_mobile/theme/theme_data.dart';
import 'package:immich_mobile/theme/dynamic_theme.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final immichThemeModeProvider = StateProvider<ThemeMode>((ref) {
final themeMode = ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.themeMode);
debugPrint("Current themeMode $themeMode");
dPrint(() => "Current themeMode $themeMode");
if (themeMode == ThemeMode.light.name) {
return ThemeMode.light;
@@ -26,12 +27,12 @@ final immichThemePresetProvider = StateProvider<ImmichColorPreset>((ref) {
final appSettingsProvider = ref.watch(appSettingsServiceProvider);
final primaryColorPreset = appSettingsProvider.getSetting(AppSettingsEnum.primaryColor);
debugPrint("Current theme preset $primaryColorPreset");
dPrint(() => "Current theme preset $primaryColorPreset");
try {
return ImmichColorPreset.values.firstWhere((e) => e.name == primaryColorPreset);
} catch (e) {
debugPrint("Theme preset $primaryColorPreset not found. Applying default preset.");
dPrint(() => "Theme preset $primaryColorPreset not found. Applying default preset.");
appSettingsProvider.setSetting(AppSettingsEnum.primaryColor, defaultColorPresetName);
return defaultColorPreset;
}
@@ -1,10 +1,10 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/utils/debug_print.dart';
enum UploadProfileStatus { idle, loading, success, failure }
@@ -67,7 +67,7 @@ class UploadProfileImageNotifier extends StateNotifier<UploadProfileImageState>
var profileImagePath = await _userService.createProfileImage(file.name, await file.readAsBytes());
if (profileImagePath != null) {
debugPrint("Successfully upload profile image");
dPrint(() => "Successfully upload profile image");
state = state.copyWith(status: UploadProfileStatus.success, profileImagePath: profileImagePath);
return true;
}
+11 -9
View File
@@ -2,8 +2,6 @@ import 'dart:async';
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
@@ -20,6 +18,7 @@ import 'package:immich_mobile/utils/debounce.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
import 'package:socket_io_client/socket_io_client.dart';
import 'package:immich_mobile/utils/debug_print.dart';
enum PendingAction { assetDelete, assetUploaded, assetHidden, assetTrash }
@@ -105,7 +104,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
headers["Authorization"] = "Basic ${base64.encode(utf8.encode(endpoint.userInfo))}";
}
debugPrint("Attempting to connect to websocket");
dPrint(() => "Attempting to connect to websocket");
// Configure socket transports must be specified
Socket socket = io(
endpoint.origin,
@@ -121,12 +120,12 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
);
socket.onConnect((_) {
debugPrint("Established Websocket Connection");
dPrint(() => "Established Websocket Connection");
state = WebsocketState(isConnected: true, socket: socket, pendingChanges: state.pendingChanges);
});
socket.onDisconnect((_) {
debugPrint("Disconnect to Websocket Connection");
dPrint(() => "Disconnect to Websocket Connection");
state = WebsocketState(isConnected: false, socket: null, pendingChanges: state.pendingChanges);
});
@@ -150,13 +149,13 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
socket.on('on_config_update', _handleOnConfigUpdate);
socket.on('on_new_release', _handleReleaseUpdates);
} catch (e) {
debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}");
dPrint(() => "[WEBSOCKET] Catch Websocket Error - ${e.toString()}");
}
}
}
void disconnect() {
debugPrint("Attempting to disconnect from websocket");
dPrint(() => "Attempting to disconnect from websocket");
_batchedAssetUploadReady.clear();
@@ -200,7 +199,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
}
void listenUploadEvent() {
debugPrint("Start listening to event on_upload_success");
dPrint(() => "Start listening to event on_upload_success");
state.socket?.on('on_upload_success', _handleOnUploadSuccess);
}
@@ -321,10 +320,13 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
return;
}
final isSyncAlbumEnabled = Store.get(StoreKey.syncAlbums, false);
try {
unawaited(
_ref.read(backgroundSyncProvider).syncWebsocketBatch(_batchedAssetUploadReady.toList()).then((_) {
return _ref.read(backgroundSyncProvider).syncLinkedAlbum();
if (isSyncAlbumEnabled) {
_ref.read(backgroundSyncProvider).syncLinkedAlbum();
}
}),
);
} catch (error) {
+2 -20
View File
@@ -1,6 +1,5 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/album.entity.dart';
@@ -10,6 +9,7 @@ import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
@@ -25,25 +25,7 @@ class AuthRepository extends DatabaseRepository {
const AuthRepository(super.db, this._drift);
Future<void> clearLocalData() async {
// Drift deletions - child entities first (those with foreign keys)
await Future.wait([
_drift.memoryAssetEntity.deleteAll(),
_drift.remoteAlbumAssetEntity.deleteAll(),
_drift.remoteAlbumUserEntity.deleteAll(),
_drift.remoteExifEntity.deleteAll(),
_drift.userMetadataEntity.deleteAll(),
_drift.partnerEntity.deleteAll(),
_drift.stackEntity.deleteAll(),
_drift.assetFaceEntity.deleteAll(),
]);
// Drift deletions - parent entities
await Future.wait([
_drift.memoryEntity.deleteAll(),
_drift.personEntity.deleteAll(),
_drift.remoteAlbumEntity.deleteAll(),
_drift.remoteAssetEntity.deleteAll(),
_drift.userEntity.deleteAll(),
]);
await SyncStreamRepository(_drift).reset();
return db.writeTxn(() {
return Future.wait([
+70 -4
View File
@@ -1,7 +1,21 @@
import 'dart:convert';
import 'dart:io';
import 'package:background_downloader/background_downloader.dart';
import 'package:flutter/material.dart';
import 'package:cancellation_token_http/http.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:logging/logging.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class UploadTaskWithFile {
final File file;
final UploadTask task;
const UploadTaskWithFile({required this.file, required this.task});
}
final uploadRepositoryProvider = Provider((ref) => UploadRepository());
@@ -31,7 +45,7 @@ class UploadRepository {
return FileDownloader().enqueue(task);
}
Future<void> enqueueBackgroundAll(List<UploadTask> tasks) {
Future<List<bool>> enqueueBackgroundAll(List<UploadTask> tasks) {
return FileDownloader().enqueueAll(tasks);
}
@@ -65,13 +79,65 @@ class UploadRepository {
FileDownloader().database.allRecordsWithStatus(TaskStatus.paused, group: kBackupGroup),
]);
debugPrint("""
dPrint(
() =>
"""
Upload Info:
Enqueued: ${enqueuedTasks.length}
Running: ${runningTasks.length}
Canceled: ${canceledTasks.length}
Waiting: ${waitingTasks.length}
Paused: ${pausedTasks.length}
""");
""",
);
}
Future<void> backupWithDartClient(Iterable<UploadTaskWithFile> tasks, CancellationToken cancelToken) async {
final httpClient = Client();
final String savedEndpoint = Store.get(StoreKey.serverEndpoint);
Logger logger = Logger('UploadRepository');
for (final candidate in tasks) {
if (cancelToken.isCancelled) {
logger.warning("Backup was cancelled by the user");
break;
}
try {
final fileStream = candidate.file.openRead();
final assetRawUploadData = MultipartFile(
"assetData",
fileStream,
candidate.file.lengthSync(),
filename: candidate.task.filename,
);
final baseRequest = MultipartRequest('POST', Uri.parse('$savedEndpoint/assets'));
baseRequest.headers.addAll(candidate.task.headers);
baseRequest.fields.addAll(candidate.task.fields);
baseRequest.files.add(assetRawUploadData);
final response = await httpClient.send(baseRequest, cancellationToken: cancelToken);
final responseBody = jsonDecode(await response.stream.bytesToString());
if (![200, 201].contains(response.statusCode)) {
final error = responseBody;
logger.warning(
"Error(${error['statusCode']}) uploading ${candidate.task.filename} | Created on ${candidate.task.fields["fileCreatedAt"]} | ${error['error']}",
);
continue;
}
} on CancelledException {
logger.warning("Backup was cancelled by the user");
break;
} catch (error, stackTrace) {
logger.warning("Error backup asset: ${error.toString()}: $stackTrace");
continue;
}
}
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/utils/debug_print.dart';
/// Guards against duplicate navigation to this route
class DuplicateGuard extends AutoRouteGuard {
@@ -8,7 +8,7 @@ class DuplicateGuard extends AutoRouteGuard {
void onNavigation(NavigationResolver resolver, StackRouter router) async {
// Duplicate navigation
if (resolver.route.name == router.current.name) {
debugPrint('DuplicateGuard: Preventing duplicate route navigation for ${resolver.route.name}');
dPrint(() => 'DuplicateGuard: Preventing duplicate route navigation for ${resolver.route.name}');
resolver.next(false);
} else {
resolver.next(true);
+4 -2
View File
@@ -76,7 +76,7 @@ import 'package:immich_mobile/pages/search/map/map_location_picker.page.dart';
import 'package:immich_mobile/pages/search/person_result.page.dart';
import 'package:immich_mobile/pages/search/recently_taken.page.dart';
import 'package:immich_mobile/pages/search/search.page.dart';
import 'package:immich_mobile/pages/settings/beta_sync_settings.page.dart';
import 'package:immich_mobile/pages/settings/sync_status.page.dart';
import 'package:immich_mobile/pages/share_intent/share_intent.page.dart';
import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart';
import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart';
@@ -86,6 +86,7 @@ import 'package:immich_mobile/presentation/pages/drift_album.page.dart';
import 'package:immich_mobile/presentation/pages/drift_album_options.page.dart';
import 'package:immich_mobile/presentation/pages/drift_archive.page.dart';
import 'package:immich_mobile/presentation/pages/drift_asset_selection_timeline.page.dart';
import 'package:immich_mobile/presentation/pages/drift_asset_troubleshoot.page.dart';
import 'package:immich_mobile/presentation/pages/drift_create_album.page.dart';
import 'package:immich_mobile/presentation/pages/drift_favorite.page.dart';
import 'package:immich_mobile/presentation/pages/drift_library.page.dart';
@@ -332,7 +333,7 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: ChangeExperienceRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftPartnerRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftUploadDetailRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: BetaSyncSettingsRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: SyncStatusRoute.page, guards: [_duplicateGuard]),
AutoRoute(page: DriftPeopleCollectionRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftPersonRoute.page, guards: [_authGuard]),
AutoRoute(page: DriftBackupOptionsRoute.page, guards: [_authGuard, _duplicateGuard]),
@@ -343,6 +344,7 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: DriftFilterImageRoute.page),
AutoRoute(page: DriftActivitiesRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftBackupAssetDetailRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: AssetTroubleshootRoute.page, guards: [_authGuard, _duplicateGuard]),
// required to handle all deeplinks in deep_link.service.dart
// auto_route_library#1722
RedirectRoute(path: '*', redirectTo: '/'),
+53 -16
View File
@@ -403,6 +403,43 @@ class ArchiveRoute extends PageRouteInfo<void> {
);
}
/// generated route for
/// [AssetTroubleshootPage]
class AssetTroubleshootRoute extends PageRouteInfo<AssetTroubleshootRouteArgs> {
AssetTroubleshootRoute({
Key? key,
required BaseAsset asset,
List<PageRouteInfo>? children,
}) : super(
AssetTroubleshootRoute.name,
args: AssetTroubleshootRouteArgs(key: key, asset: asset),
initialChildren: children,
);
static const String name = 'AssetTroubleshootRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
final args = data.argsAs<AssetTroubleshootRouteArgs>();
return AssetTroubleshootPage(key: args.key, asset: args.asset);
},
);
}
class AssetTroubleshootRouteArgs {
const AssetTroubleshootRouteArgs({this.key, required this.asset});
final Key? key;
final BaseAsset asset;
@override
String toString() {
return 'AssetTroubleshootRouteArgs{key: $key, asset: $asset}';
}
}
/// generated route for
/// [AssetViewerPage]
class AssetViewerRoute extends PageRouteInfo<AssetViewerRouteArgs> {
@@ -509,22 +546,6 @@ class BackupOptionsRoute extends PageRouteInfo<void> {
);
}
/// generated route for
/// [BetaSyncSettingsPage]
class BetaSyncSettingsRoute extends PageRouteInfo<void> {
const BetaSyncSettingsRoute({List<PageRouteInfo>? children})
: super(BetaSyncSettingsRoute.name, initialChildren: children);
static const String name = 'BetaSyncSettingsRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const BetaSyncSettingsPage();
},
);
}
/// generated route for
/// [ChangeExperiencePage]
class ChangeExperienceRoute extends PageRouteInfo<ChangeExperienceRouteArgs> {
@@ -2629,6 +2650,22 @@ class SplashScreenRoute extends PageRouteInfo<void> {
);
}
/// generated route for
/// [SyncStatusPage]
class SyncStatusRoute extends PageRouteInfo<void> {
const SyncStatusRoute({List<PageRouteInfo>? children})
: super(SyncStatusRoute.name, initialChildren: children);
static const String name = 'SyncStatusRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const SyncStatusPage();
},
);
}
/// generated route for
/// [TabControllerPage]
class TabControllerRoute extends PageRouteInfo<void> {
+12 -12
View File
@@ -3,7 +3,6 @@ import 'dart:collection';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
@@ -24,6 +23,7 @@ import 'package:immich_mobile/services/entity.service.dart';
import 'package:immich_mobile/services/sync.service.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:logging/logging.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final albumServiceProvider = Provider(
(ref) => AlbumService(
@@ -124,7 +124,7 @@ class AlbumService {
} finally {
_localCompleter.complete(changes);
}
debugPrint("refreshDeviceAlbums took ${sw.elapsedMilliseconds}ms");
dPrint(() => "refreshDeviceAlbums took ${sw.elapsedMilliseconds}ms");
return changes;
}
@@ -172,7 +172,7 @@ class AlbumService {
} finally {
_remoteCompleter.complete(changes);
}
debugPrint("refreshRemoteAlbums took ${sw.elapsedMilliseconds}ms");
dPrint(() => "refreshRemoteAlbums took ${sw.elapsedMilliseconds}ms");
return changes;
}
@@ -220,7 +220,7 @@ class AlbumService {
return AlbumAddAssetsResponse(alreadyInAlbum: result.duplicates, successfullyAdded: addedAssets.length);
} catch (e) {
debugPrint("Error addAssets ${e.toString()}");
dPrint(() => "Error addAssets ${e.toString()}");
}
return null;
}
@@ -242,7 +242,7 @@ class AlbumService {
await _albumRepository.update(album);
return true;
} catch (e) {
debugPrint("Error setActivityEnabled ${e.toString()}");
dPrint(() => "Error setActivityEnabled ${e.toString()}");
}
return false;
}
@@ -271,7 +271,7 @@ class AlbumService {
}
return true;
} catch (e) {
debugPrint("Error deleteAlbum ${e.toString()}");
dPrint(() => "Error deleteAlbum ${e.toString()}");
}
return false;
}
@@ -281,7 +281,7 @@ class AlbumService {
await _albumApiRepository.removeUser(album.remoteId!, userId: "me");
return true;
} catch (e) {
debugPrint("Error leaveAlbum ${e.toString()}");
dPrint(() => "Error leaveAlbum ${e.toString()}");
return false;
}
}
@@ -293,7 +293,7 @@ class AlbumService {
await _updateAssets(album.id, remove: toRemove.toList());
return true;
} catch (e) {
debugPrint("Error removeAssetFromAlbum ${e.toString()}");
dPrint(() => "Error removeAssetFromAlbum ${e.toString()}");
}
return false;
}
@@ -310,7 +310,7 @@ class AlbumService {
return true;
} catch (error) {
debugPrint("Error removeUser ${error.toString()}");
dPrint(() => "Error removeUser ${error.toString()}");
return false;
}
}
@@ -327,7 +327,7 @@ class AlbumService {
return true;
} catch (error) {
debugPrint("Error addUsers ${error.toString()}");
dPrint(() => "Error addUsers ${error.toString()}");
}
return false;
}
@@ -340,7 +340,7 @@ class AlbumService {
await _albumRepository.update(album);
return true;
} catch (e) {
debugPrint("Error changeTitleAlbum ${e.toString()}");
dPrint(() => "Error changeTitleAlbum ${e.toString()}");
return false;
}
}
@@ -353,7 +353,7 @@ class AlbumService {
await _albumRepository.update(album);
return true;
} catch (e) {
debugPrint("Error changeDescriptionAlbum ${e.toString()}");
dPrint(() => "Error changeDescriptionAlbum ${e.toString()}");
return false;
}
}
+4 -4
View File
@@ -3,14 +3,14 @@ import 'dart:convert';
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/utils/url_helper.dart';
import 'package:immich_mobile/utils/user_agent.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
import 'package:immich_mobile/utils/user_agent.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class ApiService implements Authentication {
late ApiClient _apiClient;
@@ -86,7 +86,7 @@ class ApiService implements Authentication {
setEndpoint(endpoint);
// Save in local database for next startup
await Store.put(StoreKey.serverEndpoint, endpoint);
Store.put(StoreKey.serverEndpoint, endpoint);
return endpoint;
}
@@ -155,7 +155,7 @@ class ApiService implements Authentication {
return endpoint;
}
} catch (e) {
debugPrint("Could not locate /.well-known/immich at $baseUrl");
dPrint(() => "Could not locate /.well-known/immich at $baseUrl");
}
return "";

Some files were not shown because too many files have changed in this diff Show More