From e81b61c98b348b20c7c9d92f5038d4c491c3b2c3 Mon Sep 17 00:00:00 2001 From: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Date: Mon, 2 Sep 2024 02:16:47 +0530 Subject: [PATCH] add full sync --- .../lib/domain/entities/asset.entity.dart | 10 +-- .../domain/entities/local_asset.entity.dart | 5 +- .../domain/entities/remote_asset.entity.dart | 6 +- .../domain/interfaces/database.interface.dart | 10 --- .../lib/domain/interfaces/log.interface.dart | 2 +- .../interfaces/remote_asset.interface.dart | 6 ++ .../lib/domain/interfaces/user.interface.dart | 4 +- .../models/{ => asset}/asset.model.dart | 34 +++------ .../models/{ => asset}/local_asset.model.dart | 14 +--- .../{ => asset}/remote_asset.model.dart | 41 ++++++++--- .../repositories/database.repository.dart | 47 ++---------- .../domain/repositories/log.repository.dart | 24 +++---- .../repositories/remote_asset.repository.dart | 44 ++++++++++++ .../domain/repositories/store.repository.dart | 2 +- .../domain/repositories/user.repository.dart | 36 +++++----- .../domain/services/server_info.service.dart | 7 +- .../lib/domain/services/sync.service.dart | 71 +++++++++++++++++++ .../lib/domain/services/user.service.dart | 9 +-- .../lib/domain/utils/store_converters.dart | 2 +- mobile-v2/lib/main.dart | 8 +++ .../modules/home/pages/home.page.dart | 9 ++- .../login/states/login_page.state.dart | 6 +- .../router/pages/tab_controller.page.dart | 2 +- mobile-v2/lib/service_locator.dart | 47 +++++++----- mobile-v2/lib/utils/constants/globals.dart | 3 + .../utils/extensions/string.extension.dart | 3 + mobile-v2/lib/utils/immich_api_client.dart | 50 +++++++++++++ mobile-v2/lib/utils/log_manager.dart | 1 + mobile-v2/pubspec.lock | 8 +++ mobile-v2/pubspec.yaml | 1 + 30 files changed, 333 insertions(+), 179 deletions(-) delete mode 100644 mobile-v2/lib/domain/interfaces/database.interface.dart create mode 100644 mobile-v2/lib/domain/interfaces/remote_asset.interface.dart rename mobile-v2/lib/domain/models/{ => asset}/asset.model.dart (73%) rename mobile-v2/lib/domain/models/{ => asset}/local_asset.model.dart (82%) rename mobile-v2/lib/domain/models/{ => asset}/remote_asset.model.dart (56%) create mode 100644 mobile-v2/lib/domain/repositories/remote_asset.repository.dart create mode 100644 mobile-v2/lib/domain/services/sync.service.dart create mode 100644 mobile-v2/lib/utils/extensions/string.extension.dart diff --git a/mobile-v2/lib/domain/entities/asset.entity.dart b/mobile-v2/lib/domain/entities/asset.entity.dart index e2cf15f725..be4d45a530 100644 --- a/mobile-v2/lib/domain/entities/asset.entity.dart +++ b/mobile-v2/lib/domain/entities/asset.entity.dart @@ -1,19 +1,15 @@ import 'package:drift/drift.dart'; -import 'package:immich_mobile/domain/models/asset.model.dart'; +import 'package:immich_mobile/domain/models/asset/asset.model.dart'; class Asset extends Table { const Asset(); - - IntColumn get id => integer().autoIncrement()(); - TextColumn get name => text()(); TextColumn get checksum => text().unique()(); - IntColumn get height => integer()(); - IntColumn get width => integer()(); + IntColumn get height => integer().nullable()(); + IntColumn get width => integer().nullable()(); IntColumn get type => intEnum()(); DateTimeColumn get createdTime => dateTime()(); DateTimeColumn get modifiedTime => dateTime().withDefault(currentDateAndTime)(); IntColumn get duration => integer().withDefault(const Constant(0))(); - BoolColumn get isLivePhoto => boolean().withDefault(const Constant(false))(); } diff --git a/mobile-v2/lib/domain/entities/local_asset.entity.dart b/mobile-v2/lib/domain/entities/local_asset.entity.dart index aa2643962f..71e7dd702f 100644 --- a/mobile-v2/lib/domain/entities/local_asset.entity.dart +++ b/mobile-v2/lib/domain/entities/local_asset.entity.dart @@ -4,5 +4,8 @@ import 'package:immich_mobile/domain/entities/asset.entity.dart'; class LocalAsset extends Asset { const LocalAsset(); - TextColumn get localId => text().unique()(); + TextColumn get localId => text()(); + + @override + Set get primaryKey => {localId}; } diff --git a/mobile-v2/lib/domain/entities/remote_asset.entity.dart b/mobile-v2/lib/domain/entities/remote_asset.entity.dart index 4cf596742b..37171fdd30 100644 --- a/mobile-v2/lib/domain/entities/remote_asset.entity.dart +++ b/mobile-v2/lib/domain/entities/remote_asset.entity.dart @@ -4,5 +4,9 @@ import 'package:immich_mobile/domain/entities/asset.entity.dart'; class RemoteAsset extends Asset { const RemoteAsset(); - TextColumn get remoteId => text().unique()(); + TextColumn get remoteId => text()(); + TextColumn get livePhotoVideoId => text().nullable()(); + + @override + Set get primaryKey => {remoteId}; } diff --git a/mobile-v2/lib/domain/interfaces/database.interface.dart b/mobile-v2/lib/domain/interfaces/database.interface.dart deleted file mode 100644 index 6cd5c0950f..0000000000 --- a/mobile-v2/lib/domain/interfaces/database.interface.dart +++ /dev/null @@ -1,10 +0,0 @@ -abstract class IDatabaseRepository { - /// Current version of the DB to aid with migration - int get schemaVersion; - - /// Initializes the DB and returns the corresponding object - T init(); - - /// Check and migrate the DB to the latest schema - void migrateDB(); -} diff --git a/mobile-v2/lib/domain/interfaces/log.interface.dart b/mobile-v2/lib/domain/interfaces/log.interface.dart index 89135826d3..5fc8e858a2 100644 --- a/mobile-v2/lib/domain/interfaces/log.interface.dart +++ b/mobile-v2/lib/domain/interfaces/log.interface.dart @@ -4,7 +4,7 @@ import 'package:immich_mobile/domain/models/log.model.dart'; abstract class ILogRepository { /// Fetches all logs - FutureOr> fetchLogs(); + FutureOr> fetchAll(); /// Inserts a new log into the DB FutureOr add(LogMessage log); diff --git a/mobile-v2/lib/domain/interfaces/remote_asset.interface.dart b/mobile-v2/lib/domain/interfaces/remote_asset.interface.dart new file mode 100644 index 0000000000..b21ae549d1 --- /dev/null +++ b/mobile-v2/lib/domain/interfaces/remote_asset.interface.dart @@ -0,0 +1,6 @@ +import 'package:immich_mobile/domain/models/asset/remote_asset.model.dart'; + +abstract class IRemoteAssetRepository { + /// Batch insert asset + Future addAll(Iterable assets); +} diff --git a/mobile-v2/lib/domain/interfaces/user.interface.dart b/mobile-v2/lib/domain/interfaces/user.interface.dart index 9d5c3c6bac..9f133b2200 100644 --- a/mobile-v2/lib/domain/interfaces/user.interface.dart +++ b/mobile-v2/lib/domain/interfaces/user.interface.dart @@ -4,8 +4,8 @@ import 'package:immich_mobile/domain/models/user.model.dart'; abstract class IUserRepository { /// Fetches user - FutureOr getUser(String userId); + FutureOr fetch(String userId); /// Insert user - FutureOr insertUser(User user); + FutureOr add(User user); } diff --git a/mobile-v2/lib/domain/models/asset.model.dart b/mobile-v2/lib/domain/models/asset/asset.model.dart similarity index 73% rename from mobile-v2/lib/domain/models/asset.model.dart rename to mobile-v2/lib/domain/models/asset/asset.model.dart index cda67c4059..9d9b25b557 100644 --- a/mobile-v2/lib/domain/models/asset.model.dart +++ b/mobile-v2/lib/domain/models/asset/asset.model.dart @@ -7,32 +7,27 @@ enum AssetType { } class Asset { - final int id; final String name; final String checksum; - final int height; - final int width; + final int? height; + final int? width; final AssetType type; final DateTime createdTime; final DateTime modifiedTime; final int duration; - final bool isLivePhoto; const Asset({ - required this.id, required this.name, required this.checksum, - required this.height, - required this.width, + this.height, + this.width, required this.type, required this.createdTime, required this.modifiedTime, required this.duration, - required this.isLivePhoto, }); Asset copyWith({ - int? id, String? name, String? checksum, int? height, @@ -41,10 +36,8 @@ class Asset { DateTime? createdTime, DateTime? modifiedTime, int? duration, - bool? isLivePhoto, }) { return Asset( - id: id ?? this.id, name: name ?? this.name, checksum: checksum ?? this.checksum, height: height ?? this.height, @@ -53,52 +46,45 @@ class Asset { createdTime: createdTime ?? this.createdTime, modifiedTime: modifiedTime ?? this.modifiedTime, duration: duration ?? this.duration, - isLivePhoto: isLivePhoto ?? this.isLivePhoto, ); } @override String toString() => """ { - "id": $id, "name": "$name", "checksum": "$checksum", - "height": $height, - "width": $width, + "height": ${height ?? "-"}, + "width": ${width ?? "-"}, "type": "$type", "createdTime": "$createdTime", "modifiedTime": "$modifiedTime", "duration": "$duration", - "isLivePhoto": "$isLivePhoto", }"""; @override bool operator ==(covariant Asset other) { if (identical(this, other)) return true; - return other.id == id && - other.name == name && + return other.name == name && other.checksum == checksum && other.height == height && other.width == width && other.type == type && other.createdTime == createdTime && other.modifiedTime == modifiedTime && - other.duration == duration && - other.isLivePhoto == isLivePhoto; + other.duration == duration; } @override int get hashCode { - return id.hashCode ^ - name.hashCode ^ + return name.hashCode ^ checksum.hashCode ^ height.hashCode ^ width.hashCode ^ type.hashCode ^ createdTime.hashCode ^ modifiedTime.hashCode ^ - duration.hashCode ^ - isLivePhoto.hashCode; + duration.hashCode; } } diff --git a/mobile-v2/lib/domain/models/local_asset.model.dart b/mobile-v2/lib/domain/models/asset/local_asset.model.dart similarity index 82% rename from mobile-v2/lib/domain/models/local_asset.model.dart rename to mobile-v2/lib/domain/models/asset/local_asset.model.dart index 4962bc5a08..d78074606f 100644 --- a/mobile-v2/lib/domain/models/local_asset.model.dart +++ b/mobile-v2/lib/domain/models/asset/local_asset.model.dart @@ -1,5 +1,5 @@ import 'package:flutter/foundation.dart'; -import 'package:immich_mobile/domain/models/asset.model.dart'; +import 'package:immich_mobile/domain/models/asset/asset.model.dart'; @immutable class LocalAsset extends Asset { @@ -7,7 +7,6 @@ class LocalAsset extends Asset { const LocalAsset({ required this.localId, - required super.id, required super.name, required super.checksum, required super.height, @@ -16,23 +15,20 @@ class LocalAsset extends Asset { required super.createdTime, required super.modifiedTime, required super.duration, - required super.isLivePhoto, }); @override String toString() => """ { - "id": $id, "localId": "$localId", "name": "$name", "checksum": "$checksum", - "height": $height, - "width": $width, + "height": ${height ?? "-"}, + "width": ${width ?? "-"}, "type": "$type", "createdTime": "$createdTime", "modifiedTime": "$modifiedTime", "duration": "$duration", - "isLivePhoto": "$isLivePhoto", }"""; @override @@ -47,7 +43,6 @@ class LocalAsset extends Asset { @override LocalAsset copyWith({ - int? id, String? localId, String? name, String? checksum, @@ -57,10 +52,8 @@ class LocalAsset extends Asset { DateTime? createdTime, DateTime? modifiedTime, int? duration, - bool? isLivePhoto, }) { return LocalAsset( - id: id ?? this.id, localId: localId ?? this.localId, name: name ?? this.name, checksum: checksum ?? this.checksum, @@ -70,7 +63,6 @@ class LocalAsset extends Asset { createdTime: createdTime ?? this.createdTime, modifiedTime: modifiedTime ?? this.modifiedTime, duration: duration ?? this.duration, - isLivePhoto: isLivePhoto ?? this.isLivePhoto, ); } } diff --git a/mobile-v2/lib/domain/models/remote_asset.model.dart b/mobile-v2/lib/domain/models/asset/remote_asset.model.dart similarity index 56% rename from mobile-v2/lib/domain/models/remote_asset.model.dart rename to mobile-v2/lib/domain/models/asset/remote_asset.model.dart index f30c0b5440..cd88c1adff 100644 --- a/mobile-v2/lib/domain/models/remote_asset.model.dart +++ b/mobile-v2/lib/domain/models/asset/remote_asset.model.dart @@ -1,13 +1,15 @@ import 'package:flutter/foundation.dart'; -import 'package:immich_mobile/domain/models/asset.model.dart'; +import 'package:immich_mobile/domain/models/asset/asset.model.dart'; +import 'package:immich_mobile/utils/extensions/string.extension.dart'; +import 'package:openapi/api.dart'; @immutable class RemoteAsset extends Asset { final String remoteId; + final String? livePhotoVideoId; const RemoteAsset({ required this.remoteId, - required super.id, required super.name, required super.checksum, required super.height, @@ -16,23 +18,22 @@ class RemoteAsset extends Asset { required super.createdTime, required super.modifiedTime, required super.duration, - required super.isLivePhoto, + this.livePhotoVideoId, }); @override String toString() => """ { - "id": $id, "remoteId": "$remoteId", "name": "$name", "checksum": "$checksum", - "height": $height, - "width": $width, + "height": ${height ?? "-"}, + "width": ${width ?? "-"}, "type": "$type", "createdTime": "$createdTime", "modifiedTime": "$modifiedTime", "duration": "$duration", - "isLivePhoto": "$isLivePhoto", + "livePhotoVideoId": "${livePhotoVideoId ?? "-"}", }"""; @override @@ -47,7 +48,6 @@ class RemoteAsset extends Asset { @override RemoteAsset copyWith({ - int? id, String? remoteId, String? name, String? checksum, @@ -57,10 +57,9 @@ class RemoteAsset extends Asset { DateTime? createdTime, DateTime? modifiedTime, int? duration, - bool? isLivePhoto, + String? livePhotoVideoId, }) { return RemoteAsset( - id: id ?? this.id, remoteId: remoteId ?? this.remoteId, name: name ?? this.name, checksum: checksum ?? this.checksum, @@ -70,7 +69,27 @@ class RemoteAsset extends Asset { createdTime: createdTime ?? this.createdTime, modifiedTime: modifiedTime ?? this.modifiedTime, duration: duration ?? this.duration, - isLivePhoto: isLivePhoto ?? this.isLivePhoto, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, ); } + + factory RemoteAsset.fromDto(AssetResponseDto dto) => RemoteAsset( + remoteId: dto.id, + createdTime: dto.fileCreatedAt, + duration: dto.duration.tryParseInt() ?? 0, + height: dto.exifInfo?.exifImageHeight?.toInt(), + width: dto.exifInfo?.exifImageWidth?.toInt(), + checksum: dto.checksum, + name: dto.originalFileName, + livePhotoVideoId: dto.livePhotoVideoId, + modifiedTime: dto.fileModifiedAt, + type: _toAssetType(dto.type), + ); } + +AssetType _toAssetType(AssetTypeEnum type) => switch (type) { + AssetTypeEnum.AUDIO => AssetType.audio, + AssetTypeEnum.IMAGE => AssetType.image, + AssetTypeEnum.VIDEO => AssetType.video, + _ => AssetType.other, + }; diff --git a/mobile-v2/lib/domain/repositories/database.repository.dart b/mobile-v2/lib/domain/repositories/database.repository.dart index de9f7ab792..ebd905cc46 100644 --- a/mobile-v2/lib/domain/repositories/database.repository.dart +++ b/mobile-v2/lib/domain/repositories/database.repository.dart @@ -1,9 +1,7 @@ -import 'dart:io'; - import 'package:drift/drift.dart'; -import 'package:drift/native.dart'; // ignore: depend_on_referenced_packages import 'package:drift_dev/api/migrations.dart'; +import 'package:drift_flutter/drift_flutter.dart'; import 'package:flutter/foundation.dart'; import 'package:immich_mobile/domain/entities/album.entity.dart'; import 'package:immich_mobile/domain/entities/local_asset.entity.dart'; @@ -11,54 +9,17 @@ import 'package:immich_mobile/domain/entities/log.entity.dart'; import 'package:immich_mobile/domain/entities/remote_asset.entity.dart'; import 'package:immich_mobile/domain/entities/store.entity.dart'; import 'package:immich_mobile/domain/entities/user.entity.dart'; -import 'package:immich_mobile/domain/interfaces/database.interface.dart'; -import 'package:path/path.dart' as p; -import 'package:path_provider/path_provider.dart'; -import 'package:sqlite3/sqlite3.dart'; -import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; import 'database.repository.drift.dart'; @DriftDatabase(tables: [Logs, Store, LocalAlbum, LocalAsset, User, RemoteAsset]) -class DriftDatabaseRepository extends $DriftDatabaseRepository - implements IDatabaseRepository { - DriftDatabaseRepository() : super(_openConnection()); - - static LazyDatabase _openConnection() { - return LazyDatabase(() async { - final dbFolder = await getApplicationDocumentsDirectory(); - final file = File(p.join(dbFolder.path, 'db.sqlite')); - - // Work around limitations on old Android versions - // https://github.com/simolus3/sqlite3.dart/tree/main/sqlite3_flutter_libs#problems-on-android-6 - if (Platform.isAndroid) { - await applyWorkaroundToOpenSqlite3OnOldAndroidVersions(); - } - - // Make sqlite3 pick a more suitable location for temporary files - the - // one from the system may be inaccessible due to sandboxing. - // https://github.com/simolus3/moor/issues/876#issuecomment-710013503 - final cachebase = (await getTemporaryDirectory()).path; - // We can't access /tmp on Android, which sqlite3 would try by default. - // Explicitly tell it about the correct temporary directory. - sqlite3.tempDirectory = cachebase; - - return NativeDatabase.createInBackground(file); - }); - } - - @override - GeneratedDatabase init() => this; +class DriftDatabaseRepository extends $DriftDatabaseRepository { + DriftDatabaseRepository([QueryExecutor? executor]) + : super(executor ?? driftDatabase(name: 'db')); @override int get schemaVersion => 1; - @override - // ignore: no-empty-block - void migrateDB() { - // Migrations are handled automatically using the migrator field - } - @override MigrationStrategy get migration => MigrationStrategy( onCreate: (m) => m.createAll(), diff --git a/mobile-v2/lib/domain/repositories/log.repository.dart b/mobile-v2/lib/domain/repositories/log.repository.dart index 59efeb0c70..90192e5e1a 100644 --- a/mobile-v2/lib/domain/repositories/log.repository.dart +++ b/mobile-v2/lib/domain/repositories/log.repository.dart @@ -13,8 +13,8 @@ class LogDriftRepository implements ILogRepository { const LogDriftRepository(this.db); @override - Future> fetchLogs() async { - return await db.managers.logs.map((l) => l.toModel()).get(); + Future> fetchAll() async { + return await db.managers.logs.map(_toModel).get(); } @override @@ -82,15 +82,13 @@ class LogDriftRepository implements ILogRepository { } } -extension _LogToLogMessage on Log { - LogMessage toModel() { - return LogMessage( - content: content, - createdAt: createdAt, - level: level, - error: error, - logger: logger, - stack: stack, - ); - } +LogMessage _toModel(Log log) { + return LogMessage( + content: log.content, + createdAt: log.createdAt, + level: log.level, + error: log.error, + logger: log.logger, + stack: log.stack, + ); } diff --git a/mobile-v2/lib/domain/repositories/remote_asset.repository.dart b/mobile-v2/lib/domain/repositories/remote_asset.repository.dart new file mode 100644 index 0000000000..1b64225029 --- /dev/null +++ b/mobile-v2/lib/domain/repositories/remote_asset.repository.dart @@ -0,0 +1,44 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/entities/remote_asset.entity.drift.dart'; +import 'package:immich_mobile/domain/interfaces/remote_asset.interface.dart'; +import 'package:immich_mobile/domain/models/asset/remote_asset.model.dart'; +import 'package:immich_mobile/domain/repositories/database.repository.dart'; +import 'package:immich_mobile/utils/mixins/log_context.mixin.dart'; + +class RemoteAssetDriftRepository + with LogContext + implements IRemoteAssetRepository { + final DriftDatabaseRepository _db; + + const RemoteAssetDriftRepository(this._db); + + @override + Future addAll(Iterable assets) async { + try { + await _db.batch((batch) => batch.insertAllOnConflictUpdate( + _db.remoteAsset, + assets.map(_toEntity), + )); + + return true; + } catch (e, s) { + log.severe("Cannot insert remote assets into table", e, s); + return false; + } + } +} + +RemoteAssetCompanion _toEntity(RemoteAsset asset) { + return RemoteAssetCompanion.insert( + name: asset.name, + checksum: asset.checksum, + height: Value(asset.height), + width: Value(asset.width), + type: asset.type, + createdTime: asset.createdTime, + remoteId: asset.remoteId, + duration: Value(asset.duration), + modifiedTime: Value(asset.modifiedTime), + livePhotoVideoId: Value(asset.livePhotoVideoId), + ); +} diff --git a/mobile-v2/lib/domain/repositories/store.repository.dart b/mobile-v2/lib/domain/repositories/store.repository.dart index 64e22d7a9d..da1c1c2153 100644 --- a/mobile-v2/lib/domain/repositories/store.repository.dart +++ b/mobile-v2/lib/domain/repositories/store.repository.dart @@ -63,7 +63,7 @@ class StoreDriftRepository with LogContext implements IStoreRepository { @override FutureOr clearStore() async { await db.managers.store.delete(); - ; + } FutureOr _getValueFromStoreData( diff --git a/mobile-v2/lib/domain/repositories/user.repository.dart b/mobile-v2/lib/domain/repositories/user.repository.dart index 1e39513d3d..4792878470 100644 --- a/mobile-v2/lib/domain/repositories/user.repository.dart +++ b/mobile-v2/lib/domain/repositories/user.repository.dart @@ -13,15 +13,15 @@ class UserDriftRepository with LogContext implements IUserRepository { const UserDriftRepository(this.db); @override - FutureOr getUser(String userId) async { + FutureOr fetch(String userId) async { return await db.managers.user .filter((f) => f.id.equals(userId)) - .map((u) => u.toModel()) + .map(_toModel) .getSingleOrNull(); } @override - FutureOr insertUser(User user) async { + FutureOr add(User user) async { try { await db.into(db.user).insertOnConflictUpdate( UserCompanion.insert( @@ -46,20 +46,18 @@ class UserDriftRepository with LogContext implements IUserRepository { } } -extension _UserDataToUser on UserData { - User toModel() { - return User( - id: id, - email: email, - avatarColor: avatarColor, - inTimeline: inTimeline, - isAdmin: isAdmin, - memoryEnabled: memoryEnabled, - name: name, - profileImagePath: profileImagePath, - quotaSizeInBytes: quotaSizeInBytes, - quotaUsageInBytes: quotaUsageInBytes, - updatedAt: updatedAt, - ); - } +User _toModel(UserData user) { + return User( + id: user.id, + email: user.email, + avatarColor: user.avatarColor, + inTimeline: user.inTimeline, + isAdmin: user.isAdmin, + memoryEnabled: user.memoryEnabled, + name: user.name, + profileImagePath: user.profileImagePath, + quotaSizeInBytes: user.quotaSizeInBytes, + quotaUsageInBytes: user.quotaUsageInBytes, + updatedAt: user.updatedAt, + ); } diff --git a/mobile-v2/lib/domain/services/server_info.service.dart b/mobile-v2/lib/domain/services/server_info.service.dart index 0ff11d226c..73700785e3 100644 --- a/mobile-v2/lib/domain/services/server_info.service.dart +++ b/mobile-v2/lib/domain/services/server_info.service.dart @@ -1,15 +1,12 @@ import 'package:immich_mobile/domain/models/server-info/server_config.model.dart'; import 'package:immich_mobile/domain/models/server-info/server_features.model.dart'; -import 'package:immich_mobile/utils/immich_api_client.dart'; import 'package:immich_mobile/utils/mixins/log_context.mixin.dart'; import 'package:openapi/api.dart'; class ServerInfoService with LogContext { - final ImmichApiClient _api; + final ServerApi _serverInfo; - ServerApi get _serverInfo => _api.getServerApi(); - - ServerInfoService(this._api); + const ServerInfoService(this._serverInfo); Future getServerFeatures() async { try { diff --git a/mobile-v2/lib/domain/services/sync.service.dart b/mobile-v2/lib/domain/services/sync.service.dart new file mode 100644 index 0000000000..4c2d29075e --- /dev/null +++ b/mobile-v2/lib/domain/services/sync.service.dart @@ -0,0 +1,71 @@ +import 'package:drift/isolate.dart'; +import 'package:immich_mobile/domain/interfaces/remote_asset.interface.dart'; +import 'package:immich_mobile/domain/models/asset/remote_asset.model.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/domain/repositories/database.repository.dart'; +import 'package:immich_mobile/service_locator.dart'; +import 'package:immich_mobile/utils/constants/globals.dart'; +import 'package:immich_mobile/utils/immich_api_client.dart'; +import 'package:immich_mobile/utils/log_manager.dart'; +import 'package:immich_mobile/utils/mixins/log_context.mixin.dart'; +import 'package:logging/logging.dart'; +import 'package:openapi/api.dart'; + +class SyncService with LogContext { + final ImmichApiClient _appClient; + final DriftDatabaseRepository _db; + + SyncService(this._appClient, this._db); + + Future doFullSyncForUserDrift( + User user, { + DateTime? updatedUtil, + int? limit, + }) async { + final clientData = _appClient.clientData; + try { + await _db.computeWithDatabase( + connect: (connection) => DriftDatabaseRepository(connection), + computation: (database) async { + ServiceLocator.configureServicesForIsolate(database: database); + LogManager.I.init(); + final logger = Logger("SyncService "); + final syncClient = + ImmichApiClient.clientData(clientData).getSyncApi(); + + final chunkSize = limit ?? kFullSyncChunkSize; + final updatedTill = updatedUtil ?? DateTime.now().toUtc(); + updatedUtil ??= DateTime.now().toUtc(); + String? lastAssetId; + + while (true) { + logger.info( + "Requesting more chunks from lastId - ${lastAssetId ?? ""}", + ); + + final assets = await syncClient.getFullSyncForUser(AssetFullSyncDto( + limit: chunkSize, + updatedUntil: updatedTill, + lastId: lastAssetId, + userId: user.id, + )); + if (assets == null) { + break; + } + + await di() + .addAll(assets.map(RemoteAsset.fromDto)); + + lastAssetId = assets.lastOrNull?.id; + if (assets.length != chunkSize) break; + } + + return true; + }, + ); + } catch (e, s) { + log.severe("Error performing full sync for user - ${user.name}", e, s); + } + return false; + } +} diff --git a/mobile-v2/lib/domain/services/user.service.dart b/mobile-v2/lib/domain/services/user.service.dart index ab58a018d5..4640f02ed9 100644 --- a/mobile-v2/lib/domain/services/user.service.dart +++ b/mobile-v2/lib/domain/services/user.service.dart @@ -1,14 +1,11 @@ import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/utils/immich_api_client.dart'; import 'package:immich_mobile/utils/mixins/log_context.mixin.dart'; import 'package:openapi/api.dart'; class UserService with LogContext { - final ImmichApiClient _api; + final UsersApi _userApi; - UsersApi get _userApi => _api.getUsersApi(); - - UserService(this._api); + const UserService(this._userApi); Future getMyUser() async { try { @@ -21,7 +18,7 @@ class UserService with LogContext { final preferencesDto = await _userApi.getMyPreferences(); return User.fromAdminDto(userDto, preferencesDto); } catch (e, s) { - log.severe("Error while fetching server features", e, s); + log.severe("Error while fetching my user", e, s); } return null; } diff --git a/mobile-v2/lib/domain/utils/store_converters.dart b/mobile-v2/lib/domain/utils/store_converters.dart index 29ef7e8b26..b527a76c39 100644 --- a/mobile-v2/lib/domain/utils/store_converters.dart +++ b/mobile-v2/lib/domain/utils/store_converters.dart @@ -50,7 +50,7 @@ class StoreUserConverter extends IStoreConverter { @override Future fromPrimitive(String value) async { - return await di().getUser(value); + return await di().fetch(value); } @override diff --git a/mobile-v2/lib/main.dart b/mobile-v2/lib/main.dart index 13cc9c8651..979446091d 100644 --- a/mobile-v2/lib/main.dart +++ b/mobile-v2/lib/main.dart @@ -3,6 +3,8 @@ import 'package:immich_mobile/i18n/strings.g.dart'; import 'package:immich_mobile/immich_app.dart'; import 'package:immich_mobile/service_locator.dart'; import 'package:immich_mobile/utils/log_manager.dart'; +// ignore: depend_on_referenced_packages +import 'package:stack_trace/stack_trace.dart' as stack_trace; void main() { WidgetsFlutterBinding.ensureInitialized(); @@ -13,5 +15,11 @@ void main() { // Init localization LocaleSettings.useDeviceLocale(); + FlutterError.demangleStackTrace = (StackTrace stack) { + if (stack is stack_trace.Trace) return stack.vmTrace; + if (stack is stack_trace.Chain) return stack.toTrace().vmTrace; + return stack; + }; + runApp(const ImmichApp()); } diff --git a/mobile-v2/lib/presentation/modules/home/pages/home.page.dart b/mobile-v2/lib/presentation/modules/home/pages/home.page.dart index df15cd414d..444398bf0f 100644 --- a/mobile-v2/lib/presentation/modules/home/pages/home.page.dart +++ b/mobile-v2/lib/presentation/modules/home/pages/home.page.dart @@ -1,6 +1,8 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; -import 'package:immich_mobile/presentation/router/router.dart'; +import 'package:immich_mobile/domain/services/sync.service.dart'; +import 'package:immich_mobile/presentation/modules/common/states/current_user.state.dart'; +import 'package:immich_mobile/service_locator.dart'; @RoutePage() class HomePage extends StatelessWidget { @@ -10,8 +12,9 @@ class HomePage extends StatelessWidget { Widget build(BuildContext context) { return Center( child: ElevatedButton( - onPressed: () => context.router.navigate(const SettingsRoute()), - child: const Text('Settings'), + onPressed: () => di() + .doFullSyncForUserDrift(di().state), + child: const Text('Sync'), ), ); } diff --git a/mobile-v2/lib/presentation/modules/login/states/login_page.state.dart b/mobile-v2/lib/presentation/modules/login/states/login_page.state.dart index d54cae4103..34a5b5ed2a 100644 --- a/mobile-v2/lib/presentation/modules/login/states/login_page.state.dart +++ b/mobile-v2/lib/presentation/modules/login/states/login_page.state.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/domain/interfaces/store.interface.dart'; import 'package:immich_mobile/domain/interfaces/user.interface.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/login.service.dart'; +import 'package:immich_mobile/domain/services/sync.service.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/i18n/strings.g.dart'; import 'package:immich_mobile/presentation/modules/common/states/server_info/server_feature_config.state.dart'; @@ -65,6 +66,7 @@ class LoginPageCubit extends Cubit with LogContext { di().set(StoreKey.serverEndpoint, url); ServiceLocator.registerPostValidationServices(url); + ServiceLocator.registerPostGlobalStates(); // Fetch server features await di().getFeatures(); @@ -132,7 +134,9 @@ class LoginPageCubit extends Cubit with LogContext { // Register user ServiceLocator.registerCurrentUser(user); - await di().insertUser(user); + await di().add(user); + // Sync assets in background + unawaited(di().doFullSyncForUserDrift(user)); emit(state.copyWith( isValidationInProgress: false, diff --git a/mobile-v2/lib/presentation/router/pages/tab_controller.page.dart b/mobile-v2/lib/presentation/router/pages/tab_controller.page.dart index 802b573921..7756c0c8d8 100644 --- a/mobile-v2/lib/presentation/router/pages/tab_controller.page.dart +++ b/mobile-v2/lib/presentation/router/pages/tab_controller.page.dart @@ -23,7 +23,7 @@ class TabControllerPage extends StatelessWidget { // Pop-back to photos tab or if already in photos tab, close the app return PopScope( canPop: tabsRouter.activeIndex == 0, - onPopInvoked: (didPop) => + onPopInvokedWithResult: (didPop, _) => !didPop ? tabsRouter.setActiveIndex(0) : null, child: _TabControllerAdaptiveScaffold( body: (ctxx) => child, diff --git a/mobile-v2/lib/service_locator.dart b/mobile-v2/lib/service_locator.dart index 51bc0b41ba..7b0298abfe 100644 --- a/mobile-v2/lib/service_locator.dart +++ b/mobile-v2/lib/service_locator.dart @@ -1,15 +1,18 @@ import 'package:get_it/get_it.dart'; import 'package:immich_mobile/domain/interfaces/log.interface.dart'; +import 'package:immich_mobile/domain/interfaces/remote_asset.interface.dart'; import 'package:immich_mobile/domain/interfaces/store.interface.dart'; import 'package:immich_mobile/domain/interfaces/user.interface.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/repositories/database.repository.dart'; import 'package:immich_mobile/domain/repositories/log.repository.dart'; +import 'package:immich_mobile/domain/repositories/remote_asset.repository.dart'; import 'package:immich_mobile/domain/repositories/store.repository.dart'; import 'package:immich_mobile/domain/repositories/user.repository.dart'; import 'package:immich_mobile/domain/services/app_setting.service.dart'; import 'package:immich_mobile/domain/services/login.service.dart'; import 'package:immich_mobile/domain/services/server_info.service.dart'; +import 'package:immich_mobile/domain/services/sync.service.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/presentation/modules/common/states/current_user.state.dart'; import 'package:immich_mobile/presentation/modules/common/states/server_info/server_feature_config.state.dart'; @@ -23,41 +26,49 @@ class ServiceLocator { const ServiceLocator._internal(); static void configureServices() { - // Register DB di.registerSingleton(DriftDatabaseRepository()); - _registerPreValidationServices(); + _registerRepositories(); + _registerPreGlobalStates(); } - static void _registerPreValidationServices() { - // ====== DOMAIN + static void configureServicesForIsolate({ + required DriftDatabaseRepository database, + }) { + di.registerSingleton(database); + _registerRepositories(); + } - // Init store + static void _registerRepositories() { + /// Repositories di.registerFactory(() => StoreDriftRepository(di())); - // Logs di.registerFactory(() => LogDriftRepository(di())); - // App Settings di.registerFactory(() => AppSettingService(di())); - // User Repo di.registerFactory(() => UserDriftRepository(di())); - // Login Service + di.registerFactory( + () => RemoteAssetDriftRepository(di()), + ); + + /// Services di.registerFactory(() => const LoginService()); + } - // ====== PRESENTATION - - // App router + static void _registerPreGlobalStates() { di.registerSingleton(AppRouter()); - // Global states di.registerLazySingleton(() => AppThemeCubit(di())); } static void registerPostValidationServices(String endpoint) { di.registerSingleton(ImmichApiClient(endpoint: endpoint)); + di.registerFactory(() => UserService( + di().getUsersApi(), + )); + di.registerFactory(() => ServerInfoService( + di().getServerApi(), + )); + di.registerFactory(() => SyncService(di(), di())); + } - // ====== DOMAIN - di.registerFactory(() => UserService(di())); - di.registerFactory(() => ServerInfoService(di())); - - // ====== PRESENTATION + static void registerPostGlobalStates() { di.registerLazySingleton( () => ServerFeatureConfigCubit(di()), ); diff --git a/mobile-v2/lib/utils/constants/globals.dart b/mobile-v2/lib/utils/constants/globals.dart index 27eef57d8e..3da227d922 100644 --- a/mobile-v2/lib/utils/constants/globals.dart +++ b/mobile-v2/lib/utils/constants/globals.dart @@ -3,6 +3,9 @@ import 'package:flutter/material.dart'; /// Log messages stored in the DB const int kLogMessageLimit = 500; +/// Chunked asset sync size +const int kFullSyncChunkSize = 10000; + /// Headers // Auth header const String kImmichHeaderAuthKey = "x-immich-user-token"; diff --git a/mobile-v2/lib/utils/extensions/string.extension.dart b/mobile-v2/lib/utils/extensions/string.extension.dart new file mode 100644 index 0000000000..93498fd984 --- /dev/null +++ b/mobile-v2/lib/utils/extensions/string.extension.dart @@ -0,0 +1,3 @@ +extension StringNumberUtils on String { + int? tryParseInt() => int.tryParse(this); +} diff --git a/mobile-v2/lib/utils/immich_api_client.dart b/mobile-v2/lib/utils/immich_api_client.dart index 1e3b3167ff..0899779e85 100644 --- a/mobile-v2/lib/utils/immich_api_client.dart +++ b/mobile-v2/lib/utils/immich_api_client.dart @@ -1,6 +1,8 @@ +import 'dart:convert'; import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/foundation.dart'; import 'package:http/http.dart'; import 'package:immich_mobile/domain/interfaces/store.interface.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; @@ -10,9 +12,21 @@ import 'package:immich_mobile/utils/constants/globals.dart'; import 'package:immich_mobile/utils/mixins/log_context.mixin.dart'; import 'package:openapi/api.dart'; +@immutable +class ImmichApiClientData { + final String endpoint; + final Map headersMap; + + const ImmichApiClientData({required this.endpoint, required this.headersMap}); +} + class ImmichApiClient extends ApiClient with LogContext { ImmichApiClient({required String endpoint}) : super(basePath: endpoint); + /// Used to recreate the client in Isolates + ImmichApiClientData get clientData => + ImmichApiClientData(endpoint: basePath, headersMap: defaultHeaderMap); + Future init({String? accessToken}) async { final token = accessToken ?? (await di().get(StoreKey.accessToken)); @@ -33,6 +47,15 @@ class ImmichApiClient extends ApiClient with LogContext { addDefaultHeader(kImmichHeaderDeviceType, Platform.operatingSystem); } + factory ImmichApiClient.clientData(ImmichApiClientData data) { + final client = ImmichApiClient(endpoint: data.endpoint); + + for (final entry in data.headersMap.entries) { + client.addDefaultHeader(entry.key, entry.value); + } + return client; + } + @override Future invokeAPI( String path, @@ -85,8 +108,35 @@ class ImmichApiClient extends ApiClient with LogContext { return ApiClient.fromJson(value, targetType, growable: growable); } + @override + // ignore: avoid-dynamic + Future deserializeAsync( + String value, + String targetType, { + bool growable = false, + }) => + deserialize(value, targetType, growable: growable); + + @override + // ignore: avoid-dynamic + Future deserialize( + String value, + String targetType, { + bool growable = false, + }) async { + targetType = targetType.replaceAll(' ', ''); + return targetType == 'String' + ? value + : fromJson( + await compute((String j) => json.decode(j), value), + targetType, + growable: growable, + ); + } + UsersApi getUsersApi() => UsersApi(this); ServerApi getServerApi() => ServerApi(this); AuthenticationApi getAuthenticationApi() => AuthenticationApi(this); OAuthApi getOAuthApi() => OAuthApi(this); + SyncApi getSyncApi() => SyncApi(this); } diff --git a/mobile-v2/lib/utils/log_manager.dart b/mobile-v2/lib/utils/log_manager.dart index 2dab6667c0..de40ea9e78 100644 --- a/mobile-v2/lib/utils/log_manager.dart +++ b/mobile-v2/lib/utils/log_manager.dart @@ -27,6 +27,7 @@ class LogManager { debugPrint('[${record.level.name}] [${record.time}] ${record.message}'); if (record.error != null && record.stackTrace != null) { debugPrint('${record.error}'); + debugPrintStack(stackTrace: record.stackTrace); } return true; }()); diff --git a/mobile-v2/pubspec.lock b/mobile-v2/pubspec.lock index ec7a233e70..edc4c34633 100644 --- a/mobile-v2/pubspec.lock +++ b/mobile-v2/pubspec.lock @@ -278,6 +278,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.20.1" + drift_flutter: + dependency: "direct main" + description: + name: drift_flutter + sha256: c670c947fe17ad149678a43fdbbfdb69321f0c83d315043e34e8ad2729e11f49 + url: "https://pub.dev" + source: hosted + version: "0.2.0" dynamic_color: dependency: "direct main" description: diff --git a/mobile-v2/pubspec.yaml b/mobile-v2/pubspec.yaml index 35d7d75e4b..7b1c8a8f72 100644 --- a/mobile-v2/pubspec.yaml +++ b/mobile-v2/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: flutter_bloc: ^8.1.6 # Database drift: ^2.20.0 + drift_flutter: ^0.2.0 sqlite3: ^2.4.6 sqlite3_flutter_libs: ^0.5.24 # Network