mirror of
https://github.com/immich-app/immich.git
synced 2024-12-29 15:11:58 +00:00
refactor(mobile): album api repository for album service (#12791)
* refactor(mobile): album api repository for album service
This commit is contained in:
parent
94fc1f213a
commit
3868736799
20 changed files with 751 additions and 223 deletions
|
@ -46,21 +46,46 @@ custom_lint:
|
||||||
- avoid_public_notifier_properties: false
|
- avoid_public_notifier_properties: false
|
||||||
- avoid_manual_providers_as_generated_provider_dependency: false
|
- avoid_manual_providers_as_generated_provider_dependency: false
|
||||||
- unsupported_provider_value: false
|
- unsupported_provider_value: false
|
||||||
- photo_manager:
|
- import_rule_photo_manager:
|
||||||
exclude:
|
message: photo_manager must only be used in MediaRepositories
|
||||||
|
restrict: package:photo_manager
|
||||||
|
allowed:
|
||||||
# required / wanted
|
# required / wanted
|
||||||
- album_media.repository.dart
|
- 'lib/repositories/{album,asset,file}_media.repository.dart'
|
||||||
- asset_media.repository.dart
|
|
||||||
- file_media.repository.dart
|
|
||||||
# acceptable exceptions for the time being
|
# acceptable exceptions for the time being
|
||||||
- asset.entity.dart # to provide local AssetEntity for now
|
- lib/entities/asset.entity.dart # to provide local AssetEntity for now
|
||||||
- immich_local_image_provider.dart # accesses thumbnails via PhotoManager
|
- lib/providers/image/immich_local_{image,thumbnail}_provider.dart # accesses thumbnails via PhotoManager
|
||||||
- immich_local_thumbnail_provider.dart # accesses thumbnails via PhotoManager
|
|
||||||
# refactor to make the providers and services testable
|
# refactor to make the providers and services testable
|
||||||
- backup.provider.dart # uses only PMProgressHandler
|
- lib/providers/backup/{backup,manual_upload}.provider.dart # uses only PMProgressHandler
|
||||||
- manual_upload.provider.dart # uses only PMProgressHandler
|
- lib/services/{background,backup}.service.dart # uses only PMProgressHandler
|
||||||
- background.service.dart # uses only PMProgressHandler
|
- import_rule_openapi:
|
||||||
- backup.service.dart # uses only PMProgressHandler
|
message: openapi must only be used through ApiRepositories
|
||||||
|
restrict: package:openapi
|
||||||
|
allowed:
|
||||||
|
# requried / wanted
|
||||||
|
- lib/repositories/album_api.repository.dart
|
||||||
|
# acceptable exceptions for the time being
|
||||||
|
- lib/entities/{album,asset,exif_info,user}.entity.dart # to convert DTOs to entities
|
||||||
|
- lib/utils/{image_url_builder,openapi_patching}.dart # utils are fine
|
||||||
|
- test/modules/utils/openapi_patching_test.dart # filename is self-explanatory...
|
||||||
|
# refactor
|
||||||
|
- lib/models/activities/activity.model.dart
|
||||||
|
- lib/models/map/map_marker.model.dart
|
||||||
|
- lib/models/search/search_filter.model.dart
|
||||||
|
- lib/models/server_info/server_{config,disk_info,features,version}.model.dart
|
||||||
|
- lib/models/shared_link/shared_link.model.dart
|
||||||
|
- lib/pages/search/search_input.page.dart
|
||||||
|
- lib/providers/asset_viewer/asset_people.provider.dart
|
||||||
|
- lib/providers/authentication.provider.dart
|
||||||
|
- lib/providers/image/immich_remote_{image,thumbnail}_provider.dart
|
||||||
|
- lib/providers/map/map_state.provider.dart
|
||||||
|
- lib/providers/search/{people,search,search_filter}.provider.dart
|
||||||
|
- lib/providers/websocket.provider.dart
|
||||||
|
- lib/routing/auth_guard.dart
|
||||||
|
- lib/services/{activity,api,asset,asset_description,backup,memory,oauth,partner,person,search,shared_link,stack,trash,user}.service.dart
|
||||||
|
- lib/widgets/album/album_thumbnail_listtile.dart
|
||||||
|
- lib/widgets/forms/login/login_form.dart
|
||||||
|
- lib/widgets/search/search_filter/{camera_picker,location_picker,people_picker}.dart
|
||||||
|
|
||||||
dart_code_metrics:
|
dart_code_metrics:
|
||||||
metrics:
|
metrics:
|
||||||
|
|
|
@ -1,48 +1,85 @@
|
||||||
import 'dart:collection';
|
|
||||||
|
|
||||||
import 'package:analyzer/error/listener.dart';
|
import 'package:analyzer/error/listener.dart';
|
||||||
import 'package:analyzer/error/error.dart' show ErrorSeverity;
|
import 'package:analyzer/error/error.dart' show ErrorSeverity;
|
||||||
import 'package:custom_lint_builder/custom_lint_builder.dart';
|
import 'package:custom_lint_builder/custom_lint_builder.dart';
|
||||||
|
// ignore: depend_on_referenced_packages
|
||||||
|
import 'package:glob/glob.dart';
|
||||||
|
|
||||||
PluginBase createPlugin() => ImmichLinter();
|
PluginBase createPlugin() => ImmichLinter();
|
||||||
|
|
||||||
class ImmichLinter extends PluginBase {
|
class ImmichLinter extends PluginBase {
|
||||||
@override
|
@override
|
||||||
List<LintRule> getLintRules(CustomLintConfigs configs) => [
|
List<LintRule> getLintRules(CustomLintConfigs configs) {
|
||||||
PhotoManagerRule(configs.rules[PhotoManagerRule._code.name]),
|
final List<LintRule> rules = [];
|
||||||
];
|
for (final entry in configs.rules.entries) {
|
||||||
}
|
if (entry.value.enabled && entry.key.startsWith("import_rule_")) {
|
||||||
|
final code = makeCode(entry.key, entry.value);
|
||||||
class PhotoManagerRule extends DartLintRule {
|
final allowedPaths = getStrings(entry.value, "allowed");
|
||||||
PhotoManagerRule(LintOptions? options) : super(code: _code) {
|
final forbiddenPaths = getStrings(entry.value, "forbidden");
|
||||||
final excludeOption = options?.json["exclude"];
|
final restrict = getStrings(entry.value, "restrict");
|
||||||
if (excludeOption is String) {
|
rules.add(ImportRule(code, buildGlob(allowedPaths),
|
||||||
_excludePaths.add(excludeOption);
|
buildGlob(forbiddenPaths), restrict));
|
||||||
} else if (excludeOption is List) {
|
|
||||||
_excludePaths.addAll(excludeOption.map((option) => option));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
|
||||||
final Set<String> _excludePaths = HashSet();
|
static makeCode(String name, LintOptions options) => LintCode(
|
||||||
|
name: name,
|
||||||
static const _code = LintCode(
|
problemMessage: options.json["message"] as String,
|
||||||
name: 'photo_manager',
|
|
||||||
problemMessage:
|
|
||||||
'photo_manager library must only be used in MediaRepository',
|
|
||||||
errorSeverity: ErrorSeverity.WARNING,
|
errorSeverity: ErrorSeverity.WARNING,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static List<String> getStrings(LintOptions options, String field) {
|
||||||
|
final List<String> result = [];
|
||||||
|
final excludeOption = options.json[field];
|
||||||
|
if (excludeOption is String) {
|
||||||
|
result.add(excludeOption);
|
||||||
|
} else if (excludeOption is List) {
|
||||||
|
result.addAll(excludeOption.map((option) => option));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Glob? buildGlob(List<String> globs) {
|
||||||
|
if (globs.isEmpty) return null;
|
||||||
|
if (globs.length == 1) return Glob(globs[0], caseSensitive: true);
|
||||||
|
return Glob("{${globs.join(",")}}", caseSensitive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore: must_be_immutable
|
||||||
|
class ImportRule extends DartLintRule {
|
||||||
|
ImportRule(LintCode code, this._allowed, this._forbidden, this._restrict)
|
||||||
|
: super(code: code);
|
||||||
|
|
||||||
|
final Glob? _allowed;
|
||||||
|
final Glob? _forbidden;
|
||||||
|
final List<String> _restrict;
|
||||||
|
int _rootOffset = -1;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void run(
|
void run(
|
||||||
CustomLintResolver resolver,
|
CustomLintResolver resolver,
|
||||||
ErrorReporter reporter,
|
ErrorReporter reporter,
|
||||||
CustomLintContext context,
|
CustomLintContext context,
|
||||||
) {
|
) {
|
||||||
if (_excludePaths.contains(resolver.source.shortName)) return;
|
if (_rootOffset == -1) {
|
||||||
|
const project = "/immich/mobile/";
|
||||||
|
_rootOffset = resolver.path.indexOf(project) + project.length;
|
||||||
|
}
|
||||||
|
final path = resolver.path.substring(_rootOffset);
|
||||||
|
|
||||||
|
if ((_allowed != null && _allowed!.matches(path)) &&
|
||||||
|
(_forbidden == null || !_forbidden!.matches(path))) return;
|
||||||
|
|
||||||
context.registry.addImportDirective((node) {
|
context.registry.addImportDirective((node) {
|
||||||
if (node.uri.stringValue?.startsWith("package:photo_manager") == true) {
|
final uri = node.uri.stringValue;
|
||||||
|
if (uri == null) return;
|
||||||
|
for (final restricted in _restrict) {
|
||||||
|
if (uri.startsWith(restricted) == true) {
|
||||||
reporter.atNode(node, code);
|
reporter.atNode(node, code);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -159,7 +159,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.4"
|
version: "2.4.4"
|
||||||
glob:
|
glob:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: glob
|
name: glob
|
||||||
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
|
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
|
||||||
|
|
|
@ -8,6 +8,7 @@ dependencies:
|
||||||
analyzer: ^6.8.0
|
analyzer: ^6.8.0
|
||||||
analyzer_plugin: ^0.11.3
|
analyzer_plugin: ^0.11.3
|
||||||
custom_lint_builder: ^0.6.4
|
custom_lint_builder: ^0.6.4
|
||||||
|
glob: ^2.1.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
lints: ^4.0.0
|
lints: ^4.0.0
|
||||||
|
|
|
@ -3,6 +3,8 @@ import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/utils/datetime_comparison.dart';
|
import 'package:immich_mobile/utils/datetime_comparison.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
// ignore: implementation_imports
|
||||||
|
import 'package:isar/src/common/isar_links_common.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
part 'album.entity.g.dart';
|
part 'album.entity.g.dart';
|
||||||
|
@ -23,6 +25,7 @@ class Album {
|
||||||
required this.activityEnabled,
|
required this.activityEnabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// fields stored in DB
|
||||||
Id id = Isar.autoIncrement;
|
Id id = Isar.autoIncrement;
|
||||||
@Index(unique: false, replace: false, type: IndexType.hash)
|
@Index(unique: false, replace: false, type: IndexType.hash)
|
||||||
String? remoteId;
|
String? remoteId;
|
||||||
|
@ -41,9 +44,17 @@ class Album {
|
||||||
final IsarLinks<User> sharedUsers = IsarLinks<User>();
|
final IsarLinks<User> sharedUsers = IsarLinks<User>();
|
||||||
final IsarLinks<Asset> assets = IsarLinks<Asset>();
|
final IsarLinks<Asset> assets = IsarLinks<Asset>();
|
||||||
|
|
||||||
|
// transient fields
|
||||||
@ignore
|
@ignore
|
||||||
bool isAll = false;
|
bool isAll = false;
|
||||||
|
|
||||||
|
@ignore
|
||||||
|
String? remoteThumbnailAssetId;
|
||||||
|
|
||||||
|
@ignore
|
||||||
|
int remoteAssetCount = 0;
|
||||||
|
|
||||||
|
// getters
|
||||||
@ignore
|
@ignore
|
||||||
bool get isRemote => remoteId != null;
|
bool get isRemote => remoteId != null;
|
||||||
|
|
||||||
|
@ -74,6 +85,18 @@ class Album {
|
||||||
@ignore
|
@ignore
|
||||||
String get eTagKeyAssetCount => "device-album-$localId-asset-count";
|
String get eTagKeyAssetCount => "device-album-$localId-asset-count";
|
||||||
|
|
||||||
|
// the following getter are needed because Isar links do not make data
|
||||||
|
// accessible in an object freshly created (not loaded from DB)
|
||||||
|
|
||||||
|
@ignore
|
||||||
|
Iterable<User> get remoteUsers => sharedUsers.isEmpty
|
||||||
|
? (sharedUsers as IsarLinksCommon<User>).addedObjects
|
||||||
|
: sharedUsers;
|
||||||
|
|
||||||
|
@ignore
|
||||||
|
Iterable<Asset> get remoteAssets =>
|
||||||
|
assets.isEmpty ? (assets as IsarLinksCommon<Asset>).addedObjects : assets;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(other) {
|
bool operator ==(other) {
|
||||||
if (other is! Album) return false;
|
if (other is! Album) return false;
|
||||||
|
@ -129,6 +152,7 @@ class Album {
|
||||||
endDate: dto.endDate,
|
endDate: dto.endDate,
|
||||||
activityEnabled: dto.isActivityEnabled,
|
activityEnabled: dto.isActivityEnabled,
|
||||||
);
|
);
|
||||||
|
a.remoteAssetCount = dto.assetCount;
|
||||||
a.owner.value = await db.users.getById(dto.ownerId);
|
a.owner.value = await db.users.getById(dto.ownerId);
|
||||||
if (dto.albumThumbnailAssetId != null) {
|
if (dto.albumThumbnailAssetId != null) {
|
||||||
a.thumbnail.value = await db.assets
|
a.thumbnail.value = await db.assets
|
||||||
|
@ -164,7 +188,3 @@ extension AssetsHelper on IsarCollection<Album> {
|
||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AlbumResponseDtoHelper on AlbumResponseDto {
|
|
||||||
List<Asset> getAssets() => assets.map(Asset.remote).toList();
|
|
||||||
}
|
|
||||||
|
|
40
mobile/lib/interfaces/album_api.interface.dart
Normal file
40
mobile/lib/interfaces/album_api.interface.dart
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
|
|
||||||
|
abstract interface class IAlbumApiRepository {
|
||||||
|
Future<Album> get(String id);
|
||||||
|
|
||||||
|
Future<List<Album>> getAll({bool? shared});
|
||||||
|
|
||||||
|
Future<Album> create(
|
||||||
|
String name, {
|
||||||
|
required Iterable<String> assetIds,
|
||||||
|
Iterable<String> sharedUserIds = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<Album> update(
|
||||||
|
String albumId, {
|
||||||
|
String? name,
|
||||||
|
String? thumbnailAssetId,
|
||||||
|
String? description,
|
||||||
|
bool? activityEnabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> delete(String albumId);
|
||||||
|
|
||||||
|
Future<({List<String> added, List<String> duplicates})> addAssets(
|
||||||
|
String albumId,
|
||||||
|
Iterable<String> assetIds,
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<({List<String> removed, List<String> failed})> removeAssets(
|
||||||
|
String albumId,
|
||||||
|
Iterable<String> assetIds,
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<Album> addUsers(
|
||||||
|
String albumId,
|
||||||
|
Iterable<String> userIds,
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<void> removeUser(String albumId, {required String userId});
|
||||||
|
}
|
|
@ -3,6 +3,8 @@ import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
|
|
||||||
abstract interface class IAssetRepository {
|
abstract interface class IAssetRepository {
|
||||||
|
Future<Asset?> getByRemoteId(String id);
|
||||||
|
Future<List<Asset>> getAllByRemoteId(Iterable<String> ids);
|
||||||
Future<List<Asset>> getByAlbum(Album album, {User? notOwnedBy});
|
Future<List<Asset>> getByAlbum(Album album, {User? notOwnedBy});
|
||||||
Future<void> deleteById(List<int> ids);
|
Future<void> deleteById(List<int> ids);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,4 +2,5 @@ import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
|
|
||||||
abstract interface class IUserRepository {
|
abstract interface class IUserRepository {
|
||||||
Future<List<User>> getByIds(List<String> ids);
|
Future<List<User>> getByIds(List<String> ids);
|
||||||
|
Future<User?> get(String id);
|
||||||
}
|
}
|
||||||
|
|
172
mobile/lib/repositories/album_api.repository.dart
Normal file
172
mobile/lib/repositories/album_api.repository.dart
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/errors.dart';
|
||||||
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/album_api.interface.dart';
|
||||||
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
final albumApiRepositoryProvider = Provider(
|
||||||
|
(ref) => AlbumApiRepository(ref.watch(apiServiceProvider).albumsApi),
|
||||||
|
);
|
||||||
|
|
||||||
|
class AlbumApiRepository implements IAlbumApiRepository {
|
||||||
|
final AlbumsApi _api;
|
||||||
|
|
||||||
|
AlbumApiRepository(this._api);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Album> get(String id) async {
|
||||||
|
final dto = await _checkNull(_api.getAlbumInfo(id));
|
||||||
|
return _toAlbum(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Album>> getAll({bool? shared}) async {
|
||||||
|
final dtos = await _checkNull(_api.getAllAlbums(shared: shared));
|
||||||
|
return dtos.map(_toAlbum).toList().cast();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Album> create(
|
||||||
|
String name, {
|
||||||
|
required Iterable<String> assetIds,
|
||||||
|
Iterable<String> sharedUserIds = const [],
|
||||||
|
}) async {
|
||||||
|
final users = sharedUserIds.map(
|
||||||
|
(id) => AlbumUserCreateDto(userId: id, role: AlbumUserRole.editor),
|
||||||
|
);
|
||||||
|
final responseDto = await _checkNull(
|
||||||
|
_api.createAlbum(
|
||||||
|
CreateAlbumDto(
|
||||||
|
albumName: name,
|
||||||
|
assetIds: assetIds.toList(),
|
||||||
|
albumUsers: users.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return _toAlbum(responseDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Album> update(
|
||||||
|
String albumId, {
|
||||||
|
String? name,
|
||||||
|
String? thumbnailAssetId,
|
||||||
|
String? description,
|
||||||
|
bool? activityEnabled,
|
||||||
|
}) async {
|
||||||
|
final response = await _checkNull(
|
||||||
|
_api.updateAlbumInfo(
|
||||||
|
albumId,
|
||||||
|
UpdateAlbumDto(
|
||||||
|
albumName: name,
|
||||||
|
albumThumbnailAssetId: thumbnailAssetId,
|
||||||
|
description: description,
|
||||||
|
isActivityEnabled: activityEnabled,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return _toAlbum(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> delete(String albumId) {
|
||||||
|
return _api.deleteAlbum(albumId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<({List<String> added, List<String> duplicates})> addAssets(
|
||||||
|
String albumId,
|
||||||
|
Iterable<String> assetIds,
|
||||||
|
) async {
|
||||||
|
final response = await _checkNull(
|
||||||
|
_api.addAssetsToAlbum(
|
||||||
|
albumId,
|
||||||
|
BulkIdsDto(ids: assetIds.toList()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<String> added = [];
|
||||||
|
final List<String> duplicates = [];
|
||||||
|
|
||||||
|
for (final result in response) {
|
||||||
|
if (result.success) {
|
||||||
|
added.add(result.id);
|
||||||
|
} else if (result.error == BulkIdResponseDtoErrorEnum.duplicate) {
|
||||||
|
duplicates.add(result.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (added: added, duplicates: duplicates);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<({List<String> removed, List<String> failed})> removeAssets(
|
||||||
|
String albumId,
|
||||||
|
Iterable<String> assetIds,
|
||||||
|
) async {
|
||||||
|
final response = await _checkNull(
|
||||||
|
_api.removeAssetFromAlbum(
|
||||||
|
albumId,
|
||||||
|
BulkIdsDto(ids: assetIds.toList()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final List<String> removed = [], failed = [];
|
||||||
|
for (final dto in response) {
|
||||||
|
if (dto.success) {
|
||||||
|
removed.add(dto.id);
|
||||||
|
} else {
|
||||||
|
failed.add(dto.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (removed: removed, failed: failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Album> addUsers(String albumId, Iterable<String> userIds) async {
|
||||||
|
final albumUsers =
|
||||||
|
userIds.map((userId) => AlbumUserAddDto(userId: userId)).toList();
|
||||||
|
final response = await _checkNull(
|
||||||
|
_api.addUsersToAlbum(
|
||||||
|
albumId,
|
||||||
|
AddUsersDto(albumUsers: albumUsers),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return _toAlbum(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> removeUser(String albumId, {required String userId}) {
|
||||||
|
return _api.removeUserFromAlbum(albumId, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<T> _checkNull<T>(Future<T?> future) async {
|
||||||
|
final response = await future;
|
||||||
|
if (response == null) throw NoResponseDtoError();
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Album _toAlbum(AlbumResponseDto dto) {
|
||||||
|
final Album album = Album(
|
||||||
|
remoteId: dto.id,
|
||||||
|
name: dto.albumName,
|
||||||
|
createdAt: dto.createdAt,
|
||||||
|
modifiedAt: dto.updatedAt,
|
||||||
|
lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp,
|
||||||
|
shared: dto.shared,
|
||||||
|
startDate: dto.startDate,
|
||||||
|
endDate: dto.endDate,
|
||||||
|
activityEnabled: dto.isActivityEnabled,
|
||||||
|
);
|
||||||
|
album.remoteAssetCount = dto.assetCount;
|
||||||
|
album.owner.value = User.fromSimpleUserDto(dto.owner);
|
||||||
|
album.remoteThumbnailAssetId = dto.albumThumbnailAssetId;
|
||||||
|
final users = dto.albumUsers
|
||||||
|
.map((albumUser) => User.fromSimpleUserDto(albumUser.user));
|
||||||
|
album.sharedUsers.addAll(users);
|
||||||
|
final assets = dto.assets.map(Asset.remote).toList();
|
||||||
|
album.assets.addAll(assets);
|
||||||
|
return album;
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,4 +28,11 @@ class AssetRepository implements IAssetRepository {
|
||||||
@override
|
@override
|
||||||
Future<void> deleteById(List<int> ids) =>
|
Future<void> deleteById(List<int> ids) =>
|
||||||
_db.writeTxn(() => _db.assets.deleteAll(ids));
|
_db.writeTxn(() => _db.assets.deleteAll(ids));
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Asset?> getByRemoteId(String id) => _db.assets.getByRemoteId(id);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Asset>> getAllByRemoteId(Iterable<String> ids) =>
|
||||||
|
_db.assets.getAllByRemoteId(ids);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,4 +17,7 @@ class UserRepository implements IUserRepository {
|
||||||
@override
|
@override
|
||||||
Future<List<User>> getByIds(List<String> ids) async =>
|
Future<List<User>> getByIds(List<String> ids) async =>
|
||||||
(await _db.users.getAllById(ids)).cast();
|
(await _db.users.getAllById(ids)).cast();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<User?> get(String id) => _db.users.getById(id);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,63 +6,61 @@ import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/interfaces/album.interface.dart';
|
import 'package:immich_mobile/interfaces/album.interface.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/album_api.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/backup.interface.dart';
|
import 'package:immich_mobile/interfaces/backup.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/user.interface.dart';
|
|
||||||
import 'package:immich_mobile/models/albums/album_add_asset_response.model.dart';
|
import 'package:immich_mobile/models/albums/album_add_asset_response.model.dart';
|
||||||
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
|
||||||
import 'package:immich_mobile/repositories/album.repository.dart';
|
import 'package:immich_mobile/repositories/album.repository.dart';
|
||||||
|
import 'package:immich_mobile/repositories/album_api.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/asset.repository.dart';
|
import 'package:immich_mobile/repositories/asset.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/backup.repository.dart';
|
import 'package:immich_mobile/repositories/backup.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/user.repository.dart';
|
import 'package:immich_mobile/services/entity.service.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
|
||||||
import 'package:immich_mobile/services/sync.service.dart';
|
import 'package:immich_mobile/services/sync.service.dart';
|
||||||
import 'package:immich_mobile/services/user.service.dart';
|
import 'package:immich_mobile/services/user.service.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
final albumServiceProvider = Provider(
|
final albumServiceProvider = Provider(
|
||||||
(ref) => AlbumService(
|
(ref) => AlbumService(
|
||||||
ref.watch(apiServiceProvider),
|
|
||||||
ref.watch(userServiceProvider),
|
ref.watch(userServiceProvider),
|
||||||
ref.watch(syncServiceProvider),
|
ref.watch(syncServiceProvider),
|
||||||
|
ref.watch(entityServiceProvider),
|
||||||
ref.watch(albumRepositoryProvider),
|
ref.watch(albumRepositoryProvider),
|
||||||
ref.watch(assetRepositoryProvider),
|
ref.watch(assetRepositoryProvider),
|
||||||
ref.watch(userRepositoryProvider),
|
|
||||||
ref.watch(backupRepositoryProvider),
|
ref.watch(backupRepositoryProvider),
|
||||||
ref.watch(albumMediaRepositoryProvider),
|
ref.watch(albumMediaRepositoryProvider),
|
||||||
|
ref.watch(albumApiRepositoryProvider),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
class AlbumService {
|
class AlbumService {
|
||||||
final ApiService _apiService;
|
|
||||||
final UserService _userService;
|
final UserService _userService;
|
||||||
final SyncService _syncService;
|
final SyncService _syncService;
|
||||||
|
final EntityService _entityService;
|
||||||
final IAlbumRepository _albumRepository;
|
final IAlbumRepository _albumRepository;
|
||||||
final IAssetRepository _assetRepository;
|
final IAssetRepository _assetRepository;
|
||||||
final IUserRepository _userRepository;
|
|
||||||
final IBackupRepository _backupAlbumRepository;
|
final IBackupRepository _backupAlbumRepository;
|
||||||
final IAlbumMediaRepository _albumMediaRepository;
|
final IAlbumMediaRepository _albumMediaRepository;
|
||||||
|
final IAlbumApiRepository _albumApiRepository;
|
||||||
final Logger _log = Logger('AlbumService');
|
final Logger _log = Logger('AlbumService');
|
||||||
Completer<bool> _localCompleter = Completer()..complete(false);
|
Completer<bool> _localCompleter = Completer()..complete(false);
|
||||||
Completer<bool> _remoteCompleter = Completer()..complete(false);
|
Completer<bool> _remoteCompleter = Completer()..complete(false);
|
||||||
|
|
||||||
AlbumService(
|
AlbumService(
|
||||||
this._apiService,
|
|
||||||
this._userService,
|
this._userService,
|
||||||
this._syncService,
|
this._syncService,
|
||||||
|
this._entityService,
|
||||||
this._albumRepository,
|
this._albumRepository,
|
||||||
this._assetRepository,
|
this._assetRepository,
|
||||||
this._userRepository,
|
|
||||||
this._backupAlbumRepository,
|
this._backupAlbumRepository,
|
||||||
this._albumMediaRepository,
|
this._albumMediaRepository,
|
||||||
|
this._albumApiRepository,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Checks all selected device albums for changes of albums and their assets
|
/// Checks all selected device albums for changes of albums and their assets
|
||||||
|
@ -164,17 +162,11 @@ class AlbumService {
|
||||||
bool changes = false;
|
bool changes = false;
|
||||||
try {
|
try {
|
||||||
await _userService.refreshUsers();
|
await _userService.refreshUsers();
|
||||||
final List<AlbumResponseDto>? serverAlbums = await _apiService.albumsApi
|
final List<Album> serverAlbums =
|
||||||
.getAllAlbums(shared: isShared ? true : null);
|
await _albumApiRepository.getAll(shared: isShared ? true : null);
|
||||||
if (serverAlbums == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
changes = await _syncService.syncRemoteAlbumsToDb(
|
changes = await _syncService.syncRemoteAlbumsToDb(
|
||||||
serverAlbums,
|
serverAlbums,
|
||||||
isShared: isShared,
|
isShared: isShared,
|
||||||
loadDetails: (dto) async => dto.assetCount == dto.assets.length
|
|
||||||
? dto
|
|
||||||
: (await _apiService.albumsApi.getAlbumInfo(dto.id)) ?? dto,
|
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
_remoteCompleter.complete(changes);
|
_remoteCompleter.complete(changes);
|
||||||
|
@ -188,30 +180,13 @@ class AlbumService {
|
||||||
Iterable<Asset> assets, [
|
Iterable<Asset> assets, [
|
||||||
Iterable<User> sharedUsers = const [],
|
Iterable<User> sharedUsers = const [],
|
||||||
]) async {
|
]) async {
|
||||||
try {
|
final Album album = await _albumApiRepository.create(
|
||||||
AlbumResponseDto? remote = await _apiService.albumsApi.createAlbum(
|
albumName,
|
||||||
CreateAlbumDto(
|
assetIds: assets.map((asset) => asset.remoteId!),
|
||||||
albumName: albumName,
|
sharedUserIds: sharedUsers.map((user) => user.id),
|
||||||
assetIds: assets.map((asset) => asset.remoteId!).toList(),
|
|
||||||
albumUsers: sharedUsers
|
|
||||||
.map(
|
|
||||||
(e) => AlbumUserCreateDto(
|
|
||||||
userId: e.id,
|
|
||||||
role: AlbumUserRole.editor,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
if (remote != null) {
|
await _entityService.fillAlbumWithDatabaseEntities(album);
|
||||||
final Album album = await Album.remote(remote);
|
return _albumRepository.create(album);
|
||||||
await _albumRepository.create(album);
|
|
||||||
return album;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint("Error createSharedAlbum ${e.toString()}");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -243,32 +218,21 @@ class AlbumService {
|
||||||
Album album,
|
Album album,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
var response = await _apiService.albumsApi.addAssetsToAlbum(
|
final result = await _albumApiRepository.addAssets(
|
||||||
album.remoteId!,
|
album.remoteId!,
|
||||||
BulkIdsDto(ids: assets.map((asset) => asset.remoteId!).toList()),
|
assets.map((asset) => asset.remoteId!),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response != null) {
|
final List<Asset> addedAssets = result.added
|
||||||
List<Asset> successAssets = [];
|
.map((id) => assets.firstWhere((asset) => asset.remoteId == id))
|
||||||
List<String> duplicatedAssets = [];
|
.toList();
|
||||||
|
|
||||||
for (final result in response) {
|
await _updateAssets(album.id, add: addedAssets);
|
||||||
if (result.success) {
|
|
||||||
successAssets
|
|
||||||
.add(assets.firstWhere((asset) => asset.remoteId == result.id));
|
|
||||||
} else if (!result.success &&
|
|
||||||
result.error == BulkIdResponseDtoErrorEnum.duplicate) {
|
|
||||||
duplicatedAssets.add(result.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await _updateAssets(album.id, add: successAssets);
|
|
||||||
|
|
||||||
return AlbumAddAssetsResponse(
|
return AlbumAddAssetsResponse(
|
||||||
alreadyInAlbum: duplicatedAssets,
|
alreadyInAlbum: result.duplicates,
|
||||||
successfullyAdded: successAssets.length,
|
successfullyAdded: addedAssets.length,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error addAdditionalAssetToAlbum ${e.toString()}");
|
debugPrint("Error addAdditionalAssetToAlbum ${e.toString()}");
|
||||||
}
|
}
|
||||||
|
@ -293,20 +257,11 @@ class AlbumService {
|
||||||
Album album,
|
Album album,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
final List<AlbumUserAddDto> albumUsers = sharedUserIds
|
final updatedAlbum =
|
||||||
.map((userId) => AlbumUserAddDto(userId: userId))
|
await _albumApiRepository.addUsers(album.remoteId!, sharedUserIds);
|
||||||
.toList();
|
await _entityService.fillAlbumWithDatabaseEntities(updatedAlbum);
|
||||||
|
await _albumRepository.update(updatedAlbum);
|
||||||
final result = await _apiService.albumsApi.addUsersToAlbum(
|
|
||||||
album.remoteId!,
|
|
||||||
AddUsersDto(albumUsers: albumUsers),
|
|
||||||
);
|
|
||||||
if (result != null) {
|
|
||||||
album.sharedUsers.addAll(await _userRepository.getByIds(sharedUserIds));
|
|
||||||
album.shared = result.shared;
|
|
||||||
await _albumRepository.update(album);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error addAdditionalUserToAlbum ${e.toString()}");
|
debugPrint("Error addAdditionalUserToAlbum ${e.toString()}");
|
||||||
}
|
}
|
||||||
|
@ -315,15 +270,13 @@ class AlbumService {
|
||||||
|
|
||||||
Future<bool> setActivityEnabled(Album album, bool enabled) async {
|
Future<bool> setActivityEnabled(Album album, bool enabled) async {
|
||||||
try {
|
try {
|
||||||
final result = await _apiService.albumsApi.updateAlbumInfo(
|
final updatedAlbum = await _albumApiRepository.update(
|
||||||
album.remoteId!,
|
album.remoteId!,
|
||||||
UpdateAlbumDto(isActivityEnabled: enabled),
|
activityEnabled: enabled,
|
||||||
);
|
);
|
||||||
if (result != null) {
|
await _entityService.fillAlbumWithDatabaseEntities(updatedAlbum);
|
||||||
album.activityEnabled = enabled;
|
await _albumRepository.update(updatedAlbum);
|
||||||
await _albumRepository.update(album);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error setActivityEnabled ${e.toString()}");
|
debugPrint("Error setActivityEnabled ${e.toString()}");
|
||||||
}
|
}
|
||||||
|
@ -334,7 +287,7 @@ class AlbumService {
|
||||||
try {
|
try {
|
||||||
final user = Store.get(StoreKey.currentUser);
|
final user = Store.get(StoreKey.currentUser);
|
||||||
if (album.owner.value?.isarId == user.isarId) {
|
if (album.owner.value?.isarId == user.isarId) {
|
||||||
await _apiService.albumsApi.deleteAlbum(album.remoteId!);
|
await _albumApiRepository.delete(album.remoteId!);
|
||||||
}
|
}
|
||||||
if (album.shared) {
|
if (album.shared) {
|
||||||
final foreignAssets =
|
final foreignAssets =
|
||||||
|
@ -365,7 +318,7 @@ class AlbumService {
|
||||||
|
|
||||||
Future<bool> leaveAlbum(Album album) async {
|
Future<bool> leaveAlbum(Album album) async {
|
||||||
try {
|
try {
|
||||||
await _apiService.albumsApi.removeUserFromAlbum(album.remoteId!, "me");
|
await _albumApiRepository.removeUser(album.remoteId!, userId: "me");
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error leaveAlbum ${e.toString()}");
|
debugPrint("Error leaveAlbum ${e.toString()}");
|
||||||
|
@ -378,21 +331,14 @@ class AlbumService {
|
||||||
Iterable<Asset> assets,
|
Iterable<Asset> assets,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
final response = await _apiService.albumsApi.removeAssetFromAlbum(
|
final result = await _albumApiRepository.removeAssets(
|
||||||
album.remoteId!,
|
album.remoteId!,
|
||||||
BulkIdsDto(
|
assets.map((asset) => asset.remoteId!),
|
||||||
ids: assets.map((asset) => asset.remoteId!).toList(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
if (response != null) {
|
final toRemove = result.removed
|
||||||
final toRemove = response.every((e) => e.success)
|
.map((id) => assets.firstWhere((asset) => asset.remoteId == id));
|
||||||
? assets
|
|
||||||
: response
|
|
||||||
.where((e) => e.success)
|
|
||||||
.map((e) => assets.firstWhere((a) => a.remoteId == e.id));
|
|
||||||
await _updateAssets(album.id, remove: toRemove.toList());
|
await _updateAssets(album.id, remove: toRemove.toList());
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error removeAssetFromAlbum ${e.toString()}");
|
debugPrint("Error removeAssetFromAlbum ${e.toString()}");
|
||||||
}
|
}
|
||||||
|
@ -404,9 +350,9 @@ class AlbumService {
|
||||||
User user,
|
User user,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
await _apiService.albumsApi.removeUserFromAlbum(
|
await _albumApiRepository.removeUser(
|
||||||
album.remoteId!,
|
album.remoteId!,
|
||||||
user.id,
|
userId: user.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
album.sharedUsers.remove(user);
|
album.sharedUsers.remove(user);
|
||||||
|
@ -427,15 +373,12 @@ class AlbumService {
|
||||||
String newAlbumTitle,
|
String newAlbumTitle,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
await _apiService.albumsApi.updateAlbumInfo(
|
album = await _albumApiRepository.update(
|
||||||
album.remoteId!,
|
album.remoteId!,
|
||||||
UpdateAlbumDto(
|
name: newAlbumTitle,
|
||||||
albumName: newAlbumTitle,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
album.name = newAlbumTitle;
|
await _entityService.fillAlbumWithDatabaseEntities(album);
|
||||||
await _albumRepository.update(album);
|
await _albumRepository.update(album);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error changeTitleAlbum ${e.toString()}");
|
debugPrint("Error changeTitleAlbum ${e.toString()}");
|
||||||
|
@ -456,12 +399,8 @@ class AlbumService {
|
||||||
for (final albumName in albumNames) {
|
for (final albumName in albumNames) {
|
||||||
Album? album = await getAlbumByName(albumName, true);
|
Album? album = await getAlbumByName(albumName, true);
|
||||||
album ??= await createAlbum(albumName, []);
|
album ??= await createAlbum(albumName, []);
|
||||||
|
|
||||||
if (album != null && album.remoteId != null) {
|
if (album != null && album.remoteId != null) {
|
||||||
await _apiService.albumsApi.addAssetsToAlbum(
|
await _albumApiRepository.addAssets(album.remoteId!, assetIds);
|
||||||
album.remoteId!,
|
|
||||||
BulkIdsDto(ids: assetIds),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,12 +13,14 @@ import 'package:immich_mobile/main.dart';
|
||||||
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
||||||
import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
|
import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
|
||||||
import 'package:immich_mobile/repositories/album.repository.dart';
|
import 'package:immich_mobile/repositories/album.repository.dart';
|
||||||
|
import 'package:immich_mobile/repositories/album_api.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/asset.repository.dart';
|
import 'package:immich_mobile/repositories/asset.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/backup.repository.dart';
|
import 'package:immich_mobile/repositories/backup.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/user.repository.dart';
|
import 'package:immich_mobile/repositories/user.repository.dart';
|
||||||
import 'package:immich_mobile/services/album.service.dart';
|
import 'package:immich_mobile/services/album.service.dart';
|
||||||
|
import 'package:immich_mobile/services/entity.service.dart';
|
||||||
import 'package:immich_mobile/services/hash.service.dart';
|
import 'package:immich_mobile/services/hash.service.dart';
|
||||||
import 'package:immich_mobile/services/localization.service.dart';
|
import 'package:immich_mobile/services/localization.service.dart';
|
||||||
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||||
|
@ -363,23 +365,33 @@ class BackgroundService {
|
||||||
PartnerService partnerService = PartnerService(apiService, db);
|
PartnerService partnerService = PartnerService(apiService, db);
|
||||||
AlbumRepository albumRepository = AlbumRepository(db);
|
AlbumRepository albumRepository = AlbumRepository(db);
|
||||||
AssetRepository assetRepository = AssetRepository(db);
|
AssetRepository assetRepository = AssetRepository(db);
|
||||||
UserRepository userRepository = UserRepository(db);
|
|
||||||
BackupRepository backupAlbumRepository = BackupRepository(db);
|
BackupRepository backupAlbumRepository = BackupRepository(db);
|
||||||
AlbumMediaRepository albumMediaRepository = AlbumMediaRepository();
|
AlbumMediaRepository albumMediaRepository = AlbumMediaRepository();
|
||||||
FileMediaRepository fileMediaRepository = FileMediaRepository();
|
FileMediaRepository fileMediaRepository = FileMediaRepository();
|
||||||
|
UserRepository userRepository = UserRepository(db);
|
||||||
|
AlbumApiRepository albumApiRepository =
|
||||||
|
AlbumApiRepository(apiService.albumsApi);
|
||||||
HashService hashService = HashService(db, this, albumMediaRepository);
|
HashService hashService = HashService(db, this, albumMediaRepository);
|
||||||
SyncService syncSerive = SyncService(db, hashService, albumMediaRepository);
|
EntityService entityService =
|
||||||
|
EntityService(assetRepository, userRepository);
|
||||||
|
SyncService syncSerive = SyncService(
|
||||||
|
db,
|
||||||
|
hashService,
|
||||||
|
entityService,
|
||||||
|
albumMediaRepository,
|
||||||
|
albumApiRepository,
|
||||||
|
);
|
||||||
UserService userService =
|
UserService userService =
|
||||||
UserService(apiService, db, syncSerive, partnerService);
|
UserService(apiService, db, syncSerive, partnerService);
|
||||||
AlbumService albumService = AlbumService(
|
AlbumService albumService = AlbumService(
|
||||||
apiService,
|
|
||||||
userService,
|
userService,
|
||||||
syncSerive,
|
syncSerive,
|
||||||
|
entityService,
|
||||||
albumRepository,
|
albumRepository,
|
||||||
assetRepository,
|
assetRepository,
|
||||||
userRepository,
|
|
||||||
backupAlbumRepository,
|
backupAlbumRepository,
|
||||||
albumMediaRepository,
|
albumMediaRepository,
|
||||||
|
albumApiRepository,
|
||||||
);
|
);
|
||||||
BackupService backupService = BackupService(
|
BackupService backupService = BackupService(
|
||||||
apiService,
|
apiService,
|
||||||
|
|
52
mobile/lib/services/entity.service.dart
Normal file
52
mobile/lib/services/entity.service.dart
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/user.interface.dart';
|
||||||
|
import 'package:immich_mobile/repositories/asset.repository.dart';
|
||||||
|
import 'package:immich_mobile/repositories/user.repository.dart';
|
||||||
|
|
||||||
|
class EntityService {
|
||||||
|
final IAssetRepository _assetRepository;
|
||||||
|
final IUserRepository _userRepository;
|
||||||
|
EntityService(
|
||||||
|
this._assetRepository,
|
||||||
|
this._userRepository,
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<Album> fillAlbumWithDatabaseEntities(Album album) async {
|
||||||
|
final ownerId = album.ownerId;
|
||||||
|
if (ownerId != null) {
|
||||||
|
// replace owner with user from database
|
||||||
|
album.owner.value = await _userRepository.get(ownerId);
|
||||||
|
}
|
||||||
|
final thumbnailAssetId =
|
||||||
|
album.remoteThumbnailAssetId ?? album.thumbnail.value?.remoteId;
|
||||||
|
if (thumbnailAssetId != null) {
|
||||||
|
// set thumbnail with asset from database
|
||||||
|
album.thumbnail.value =
|
||||||
|
await _assetRepository.getByRemoteId(thumbnailAssetId);
|
||||||
|
}
|
||||||
|
if (album.remoteUsers.isNotEmpty) {
|
||||||
|
// replace all users with users from database
|
||||||
|
final users = await _userRepository
|
||||||
|
.getByIds(album.remoteUsers.map((user) => user.id).toList());
|
||||||
|
album.sharedUsers.clear();
|
||||||
|
album.sharedUsers.addAll(users);
|
||||||
|
}
|
||||||
|
if (album.remoteAssets.isNotEmpty) {
|
||||||
|
// replace all assets with assets from database
|
||||||
|
final assets = await _assetRepository
|
||||||
|
.getAllByRemoteId(album.remoteAssets.map((asset) => asset.remoteId!));
|
||||||
|
album.assets.clear();
|
||||||
|
album.assets.addAll(assets);
|
||||||
|
}
|
||||||
|
return album;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final entityServiceProvider = Provider(
|
||||||
|
(ref) => EntityService(
|
||||||
|
ref.watch(assetRepositoryProvider),
|
||||||
|
ref.watch(userRepositoryProvider),
|
||||||
|
),
|
||||||
|
);
|
|
@ -8,9 +8,12 @@ import 'package:immich_mobile/entities/etag.entity.dart';
|
||||||
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/album_api.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
|
import 'package:immich_mobile/repositories/album_api.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||||
|
import 'package:immich_mobile/services/entity.service.dart';
|
||||||
import 'package:immich_mobile/services/hash.service.dart';
|
import 'package:immich_mobile/services/hash.service.dart';
|
||||||
import 'package:immich_mobile/utils/async_mutex.dart';
|
import 'package:immich_mobile/utils/async_mutex.dart';
|
||||||
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
||||||
|
@ -18,24 +21,33 @@ import 'package:immich_mobile/utils/datetime_comparison.dart';
|
||||||
import 'package:immich_mobile/utils/diff.dart';
|
import 'package:immich_mobile/utils/diff.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
final syncServiceProvider = Provider(
|
final syncServiceProvider = Provider(
|
||||||
(ref) => SyncService(
|
(ref) => SyncService(
|
||||||
ref.watch(dbProvider),
|
ref.watch(dbProvider),
|
||||||
ref.watch(hashServiceProvider),
|
ref.watch(hashServiceProvider),
|
||||||
|
ref.watch(entityServiceProvider),
|
||||||
ref.watch(albumMediaRepositoryProvider),
|
ref.watch(albumMediaRepositoryProvider),
|
||||||
|
ref.watch(albumApiRepositoryProvider),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
class SyncService {
|
class SyncService {
|
||||||
final Isar _db;
|
final Isar _db;
|
||||||
final HashService _hashService;
|
final HashService _hashService;
|
||||||
|
final EntityService _entityService;
|
||||||
final IAlbumMediaRepository _albumMediaRepository;
|
final IAlbumMediaRepository _albumMediaRepository;
|
||||||
|
final IAlbumApiRepository _albumApiRepository;
|
||||||
final AsyncMutex _lock = AsyncMutex();
|
final AsyncMutex _lock = AsyncMutex();
|
||||||
final Logger _log = Logger('SyncService');
|
final Logger _log = Logger('SyncService');
|
||||||
|
|
||||||
SyncService(this._db, this._hashService, this._albumMediaRepository);
|
SyncService(
|
||||||
|
this._db,
|
||||||
|
this._hashService,
|
||||||
|
this._entityService,
|
||||||
|
this._albumMediaRepository,
|
||||||
|
this._albumApiRepository,
|
||||||
|
);
|
||||||
|
|
||||||
// public methods:
|
// public methods:
|
||||||
|
|
||||||
|
@ -65,11 +77,10 @@ class SyncService {
|
||||||
/// Syncs remote albums to the database
|
/// Syncs remote albums to the database
|
||||||
/// returns `true` if there were any changes
|
/// returns `true` if there were any changes
|
||||||
Future<bool> syncRemoteAlbumsToDb(
|
Future<bool> syncRemoteAlbumsToDb(
|
||||||
List<AlbumResponseDto> remote, {
|
List<Album> remote, {
|
||||||
required bool isShared,
|
required bool isShared,
|
||||||
required FutureOr<AlbumResponseDto> Function(AlbumResponseDto) loadDetails,
|
|
||||||
}) =>
|
}) =>
|
||||||
_lock.run(() => _syncRemoteAlbumsToDb(remote, isShared, loadDetails));
|
_lock.run(() => _syncRemoteAlbumsToDb(remote, isShared));
|
||||||
|
|
||||||
/// Syncs all device albums and their assets to the database
|
/// Syncs all device albums and their assets to the database
|
||||||
/// Returns `true` if there were any changes
|
/// Returns `true` if there were any changes
|
||||||
|
@ -289,11 +300,10 @@ class SyncService {
|
||||||
/// Syncs remote albums to the database
|
/// Syncs remote albums to the database
|
||||||
/// returns `true` if there were any changes
|
/// returns `true` if there were any changes
|
||||||
Future<bool> _syncRemoteAlbumsToDb(
|
Future<bool> _syncRemoteAlbumsToDb(
|
||||||
List<AlbumResponseDto> remote,
|
List<Album> remoteAlbums,
|
||||||
bool isShared,
|
bool isShared,
|
||||||
FutureOr<AlbumResponseDto> Function(AlbumResponseDto) loadDetails,
|
|
||||||
) async {
|
) async {
|
||||||
remote.sortBy((e) => e.id);
|
remoteAlbums.sortBy((e) => e.remoteId!);
|
||||||
|
|
||||||
final baseQuery = _db.albums.where().remoteIdIsNotNull().filter();
|
final baseQuery = _db.albums.where().remoteIdIsNotNull().filter();
|
||||||
final QueryBuilder<Album, Album, QAfterFilterCondition> query;
|
final QueryBuilder<Album, Album, QAfterFilterCondition> query;
|
||||||
|
@ -310,14 +320,14 @@ class SyncService {
|
||||||
final List<Asset> existing = [];
|
final List<Asset> existing = [];
|
||||||
|
|
||||||
final bool changes = await diffSortedLists(
|
final bool changes = await diffSortedLists(
|
||||||
remote,
|
remoteAlbums,
|
||||||
dbAlbums,
|
dbAlbums,
|
||||||
compare: (AlbumResponseDto a, Album b) => a.id.compareTo(b.remoteId!),
|
compare: (remoteAlbum, dbAlbum) =>
|
||||||
both: (AlbumResponseDto a, Album b) =>
|
remoteAlbum.remoteId!.compareTo(dbAlbum.remoteId!),
|
||||||
_syncRemoteAlbum(a, b, toDelete, existing, loadDetails),
|
both: (remoteAlbum, dbAlbum) =>
|
||||||
onlyFirst: (AlbumResponseDto a) =>
|
_syncRemoteAlbum(remoteAlbum, dbAlbum, toDelete, existing),
|
||||||
_addAlbumFromServer(a, existing, loadDetails),
|
onlyFirst: (remoteAlbum) => _addAlbumFromServer(remoteAlbum, existing),
|
||||||
onlySecond: (Album a) => _removeAlbumFromDb(a, toDelete),
|
onlySecond: (dbAlbum) => _removeAlbumFromDb(dbAlbum, toDelete),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isShared && toDelete.isNotEmpty) {
|
if (isShared && toDelete.isNotEmpty) {
|
||||||
|
@ -338,26 +348,22 @@ class SyncService {
|
||||||
/// syncing changes from local back to server)
|
/// syncing changes from local back to server)
|
||||||
/// accumulates
|
/// accumulates
|
||||||
Future<bool> _syncRemoteAlbum(
|
Future<bool> _syncRemoteAlbum(
|
||||||
AlbumResponseDto dto,
|
Album dto,
|
||||||
Album album,
|
Album album,
|
||||||
List<Asset> deleteCandidates,
|
List<Asset> deleteCandidates,
|
||||||
List<Asset> existing,
|
List<Asset> existing,
|
||||||
FutureOr<AlbumResponseDto> Function(AlbumResponseDto) loadDetails,
|
|
||||||
) async {
|
) async {
|
||||||
if (!_hasAlbumResponseDtoChanged(dto, album)) {
|
if (!_hasRemoteAlbumChanged(dto, album)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// loadDetails (/api/album/:id) will not include lastModifiedAssetTimestamp,
|
// loadDetails (/api/album/:id) will not include lastModifiedAssetTimestamp,
|
||||||
// i.e. it will always be null. Save it here.
|
// i.e. it will always be null. Save it here.
|
||||||
final originalDto = dto;
|
final originalDto = dto;
|
||||||
dto = await loadDetails(dto);
|
dto = await _albumApiRepository.get(dto.remoteId!);
|
||||||
if (dto.assetCount != dto.assets.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
final assetsInDb =
|
final assetsInDb =
|
||||||
await album.assets.filter().sortByOwnerId().thenByChecksum().findAll();
|
await album.assets.filter().sortByOwnerId().thenByChecksum().findAll();
|
||||||
assert(assetsInDb.isSorted(Asset.compareByOwnerChecksum), "inDb unsorted!");
|
assert(assetsInDb.isSorted(Asset.compareByOwnerChecksum), "inDb unsorted!");
|
||||||
final List<Asset> assetsOnRemote = dto.getAssets();
|
final List<Asset> assetsOnRemote = dto.remoteAssets.toList();
|
||||||
assetsOnRemote.sort(Asset.compareByOwnerChecksum);
|
assetsOnRemote.sort(Asset.compareByOwnerChecksum);
|
||||||
final (toAdd, toUpdate, toUnlink) = _diffAssets(
|
final (toAdd, toUpdate, toUnlink) = _diffAssets(
|
||||||
assetsOnRemote,
|
assetsOnRemote,
|
||||||
|
@ -368,15 +374,16 @@ class SyncService {
|
||||||
// update shared users
|
// update shared users
|
||||||
final List<User> sharedUsers = album.sharedUsers.toList(growable: false);
|
final List<User> sharedUsers = album.sharedUsers.toList(growable: false);
|
||||||
sharedUsers.sort((a, b) => a.id.compareTo(b.id));
|
sharedUsers.sort((a, b) => a.id.compareTo(b.id));
|
||||||
dto.albumUsers.sort((a, b) => a.user.id.compareTo(b.user.id));
|
final List<User> users = dto.remoteUsers.toList()
|
||||||
|
..sort((a, b) => a.id.compareTo(b.id));
|
||||||
final List<String> userIdsToAdd = [];
|
final List<String> userIdsToAdd = [];
|
||||||
final List<User> usersToUnlink = [];
|
final List<User> usersToUnlink = [];
|
||||||
diffSortedListsSync(
|
diffSortedListsSync(
|
||||||
dto.albumUsers,
|
users,
|
||||||
sharedUsers,
|
sharedUsers,
|
||||||
compare: (AlbumUserResponseDto a, User b) => a.user.id.compareTo(b.id),
|
compare: (User a, User b) => a.id.compareTo(b.id),
|
||||||
both: (a, b) => false,
|
both: (a, b) => false,
|
||||||
onlyFirst: (AlbumUserResponseDto a) => userIdsToAdd.add(a.user.id),
|
onlyFirst: (User a) => userIdsToAdd.add(a.id),
|
||||||
onlySecond: (User a) => usersToUnlink.add(a),
|
onlySecond: (User a) => usersToUnlink.add(a),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -386,19 +393,19 @@ class SyncService {
|
||||||
final assetsToLink = existingInDb + updated;
|
final assetsToLink = existingInDb + updated;
|
||||||
final usersToLink = (await _db.users.getAllById(userIdsToAdd)).cast<User>();
|
final usersToLink = (await _db.users.getAllById(userIdsToAdd)).cast<User>();
|
||||||
|
|
||||||
album.name = dto.albumName;
|
album.name = dto.name;
|
||||||
album.shared = dto.shared;
|
album.shared = dto.shared;
|
||||||
album.createdAt = dto.createdAt;
|
album.createdAt = dto.createdAt;
|
||||||
album.modifiedAt = dto.updatedAt;
|
album.modifiedAt = dto.modifiedAt;
|
||||||
album.startDate = dto.startDate;
|
album.startDate = dto.startDate;
|
||||||
album.endDate = dto.endDate;
|
album.endDate = dto.endDate;
|
||||||
album.lastModifiedAssetTimestamp = originalDto.lastModifiedAssetTimestamp;
|
album.lastModifiedAssetTimestamp = originalDto.lastModifiedAssetTimestamp;
|
||||||
album.shared = dto.shared;
|
album.shared = dto.shared;
|
||||||
album.activityEnabled = dto.isActivityEnabled;
|
album.activityEnabled = dto.activityEnabled;
|
||||||
if (album.thumbnail.value?.remoteId != dto.albumThumbnailAssetId) {
|
if (album.thumbnail.value?.remoteId != dto.remoteThumbnailAssetId) {
|
||||||
album.thumbnail.value = await _db.assets
|
album.thumbnail.value = await _db.assets
|
||||||
.where()
|
.where()
|
||||||
.remoteIdEqualTo(dto.albumThumbnailAssetId)
|
.remoteIdEqualTo(dto.remoteThumbnailAssetId)
|
||||||
.findFirst();
|
.findFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -434,27 +441,26 @@ class SyncService {
|
||||||
/// (shared) assets to the database beforehand
|
/// (shared) assets to the database beforehand
|
||||||
/// accumulates assets already existing in the database
|
/// accumulates assets already existing in the database
|
||||||
Future<void> _addAlbumFromServer(
|
Future<void> _addAlbumFromServer(
|
||||||
AlbumResponseDto dto,
|
Album album,
|
||||||
List<Asset> existing,
|
List<Asset> existing,
|
||||||
FutureOr<AlbumResponseDto> Function(AlbumResponseDto) loadDetails,
|
|
||||||
) async {
|
) async {
|
||||||
if (dto.assetCount != dto.assets.length) {
|
if (album.remoteAssetCount != album.remoteAssets.length) {
|
||||||
dto = await loadDetails(dto);
|
album = await _albumApiRepository.get(album.remoteId!);
|
||||||
}
|
}
|
||||||
if (dto.assetCount == dto.assets.length) {
|
if (album.remoteAssetCount == album.remoteAssets.length) {
|
||||||
// in case an album contains assets not yet present in local DB:
|
// in case an album contains assets not yet present in local DB:
|
||||||
// put missing album assets into local DB
|
// put missing album assets into local DB
|
||||||
final (existingInDb, updated) =
|
final (existingInDb, updated) =
|
||||||
await _linkWithExistingFromDb(dto.getAssets());
|
await _linkWithExistingFromDb(album.remoteAssets.toList());
|
||||||
existing.addAll(existingInDb);
|
existing.addAll(existingInDb);
|
||||||
await upsertAssetsWithExif(updated);
|
await upsertAssetsWithExif(updated);
|
||||||
|
|
||||||
final Album a = await Album.remote(dto);
|
await _entityService.fillAlbumWithDatabaseEntities(album);
|
||||||
await _db.writeTxn(() => _db.albums.store(a));
|
await _db.writeTxn(() => _db.albums.store(album));
|
||||||
} else {
|
} else {
|
||||||
_log.warning(
|
_log.warning(
|
||||||
"Failed to add album from server: assetCount ${dto.assetCount} != "
|
"Failed to add album from server: assetCount ${album.remoteAssetCount} != "
|
||||||
"asset array length ${dto.assets.length} for album ${dto.albumName}");
|
"asset array length ${album.remoteAssets.length} for album ${album.name}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -919,17 +925,17 @@ class SyncService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// returns `true` if the albums differ on the surface
|
/// returns `true` if the albums differ on the surface
|
||||||
bool _hasAlbumResponseDtoChanged(AlbumResponseDto dto, Album a) {
|
bool _hasRemoteAlbumChanged(Album remoteAlbum, Album dbAlbum) {
|
||||||
return dto.assetCount != a.assetCount ||
|
return remoteAlbum.remoteAssetCount != dbAlbum.assetCount ||
|
||||||
dto.albumName != a.name ||
|
remoteAlbum.name != dbAlbum.name ||
|
||||||
dto.albumThumbnailAssetId != a.thumbnail.value?.remoteId ||
|
remoteAlbum.remoteThumbnailAssetId != dbAlbum.thumbnail.value?.remoteId ||
|
||||||
dto.shared != a.shared ||
|
remoteAlbum.shared != dbAlbum.shared ||
|
||||||
dto.albumUsers.length != a.sharedUsers.length ||
|
remoteAlbum.remoteUsers.length != dbAlbum.sharedUsers.length ||
|
||||||
!dto.updatedAt.isAtSameMomentAs(a.modifiedAt) ||
|
!remoteAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) ||
|
||||||
!isAtSameMomentAs(dto.startDate, a.startDate) ||
|
!isAtSameMomentAs(remoteAlbum.startDate, dbAlbum.startDate) ||
|
||||||
!isAtSameMomentAs(dto.endDate, a.endDate) ||
|
!isAtSameMomentAs(remoteAlbum.endDate, dbAlbum.endDate) ||
|
||||||
!isAtSameMomentAs(
|
!isAtSameMomentAs(
|
||||||
dto.lastModifiedAssetTimestamp,
|
remoteAlbum.lastModifiedAssetTimestamp,
|
||||||
a.lastModifiedAssetTimestamp,
|
dbAlbum.lastModifiedAssetTimestamp,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,8 +39,10 @@ void main() {
|
||||||
group('Test SyncService grouped', () {
|
group('Test SyncService grouped', () {
|
||||||
late final Isar db;
|
late final Isar db;
|
||||||
final MockHashService hs = MockHashService();
|
final MockHashService hs = MockHashService();
|
||||||
|
final MockEntityService entityService = MockEntityService();
|
||||||
final MockAlbumMediaRepository albumMediaRepository =
|
final MockAlbumMediaRepository albumMediaRepository =
|
||||||
MockAlbumMediaRepository();
|
MockAlbumMediaRepository();
|
||||||
|
final MockAlbumApiRepository albumApiRepository = MockAlbumApiRepository();
|
||||||
final owner = User(
|
final owner = User(
|
||||||
id: "1",
|
id: "1",
|
||||||
updatedAt: DateTime.now(),
|
updatedAt: DateTime.now(),
|
||||||
|
@ -48,6 +50,7 @@ void main() {
|
||||||
name: "first last",
|
name: "first last",
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
);
|
);
|
||||||
|
late SyncService s;
|
||||||
setUpAll(() async {
|
setUpAll(() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
db = await TestUtils.initIsar();
|
db = await TestUtils.initIsar();
|
||||||
|
@ -68,9 +71,15 @@ void main() {
|
||||||
db.assets.clearSync();
|
db.assets.clearSync();
|
||||||
db.assets.putAllSync(initialAssets);
|
db.assets.putAllSync(initialAssets);
|
||||||
});
|
});
|
||||||
|
s = SyncService(
|
||||||
|
db,
|
||||||
|
hs,
|
||||||
|
entityService,
|
||||||
|
albumMediaRepository,
|
||||||
|
albumApiRepository,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
test('test inserting existing assets', () async {
|
test('test inserting existing assets', () async {
|
||||||
SyncService s = SyncService(db, hs, albumMediaRepository);
|
|
||||||
final List<Asset> remoteAssets = [
|
final List<Asset> remoteAssets = [
|
||||||
makeAsset(checksum: "a", remoteId: "0-1"),
|
makeAsset(checksum: "a", remoteId: "0-1"),
|
||||||
makeAsset(checksum: "b", remoteId: "2-1"),
|
makeAsset(checksum: "b", remoteId: "2-1"),
|
||||||
|
@ -88,7 +97,6 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('test inserting new assets', () async {
|
test('test inserting new assets', () async {
|
||||||
SyncService s = SyncService(db, hs, albumMediaRepository);
|
|
||||||
final List<Asset> remoteAssets = [
|
final List<Asset> remoteAssets = [
|
||||||
makeAsset(checksum: "a", remoteId: "0-1"),
|
makeAsset(checksum: "a", remoteId: "0-1"),
|
||||||
makeAsset(checksum: "b", remoteId: "2-1"),
|
makeAsset(checksum: "b", remoteId: "2-1"),
|
||||||
|
@ -109,7 +117,6 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('test syncing duplicate assets', () async {
|
test('test syncing duplicate assets', () async {
|
||||||
SyncService s = SyncService(db, hs, albumMediaRepository);
|
|
||||||
final List<Asset> remoteAssets = [
|
final List<Asset> remoteAssets = [
|
||||||
makeAsset(checksum: "a", remoteId: "0-1"),
|
makeAsset(checksum: "a", remoteId: "0-1"),
|
||||||
makeAsset(checksum: "b", remoteId: "1-1"),
|
makeAsset(checksum: "b", remoteId: "1-1"),
|
||||||
|
@ -157,7 +164,6 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('test efficient sync', () async {
|
test('test efficient sync', () async {
|
||||||
SyncService s = SyncService(db, hs, albumMediaRepository);
|
|
||||||
final List<Asset> toUpsert = [
|
final List<Asset> toUpsert = [
|
||||||
makeAsset(checksum: "a", remoteId: "0-1"), // changed
|
makeAsset(checksum: "a", remoteId: "0-1"), // changed
|
||||||
makeAsset(checksum: "f", remoteId: "0-2"), // new
|
makeAsset(checksum: "f", remoteId: "0-2"), // new
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:immich_mobile/interfaces/album.interface.dart';
|
import 'package:immich_mobile/interfaces/album.interface.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/album_api.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/asset_media.interface.dart';
|
import 'package:immich_mobile/interfaces/asset_media.interface.dart';
|
||||||
|
@ -20,3 +21,5 @@ class MockAlbumMediaRepository extends Mock implements IAlbumMediaRepository {}
|
||||||
class MockAssetMediaRepository extends Mock implements IAssetMediaRepository {}
|
class MockAssetMediaRepository extends Mock implements IAssetMediaRepository {}
|
||||||
|
|
||||||
class MockFileMediaRepository extends Mock implements IFileMediaRepository {}
|
class MockFileMediaRepository extends Mock implements IFileMediaRepository {}
|
||||||
|
|
||||||
|
class MockAlbumApiRepository extends Mock implements IAlbumApiRepository {}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
|
import 'package:immich_mobile/services/entity.service.dart';
|
||||||
import 'package:immich_mobile/services/hash.service.dart';
|
import 'package:immich_mobile/services/hash.service.dart';
|
||||||
import 'package:immich_mobile/services/sync.service.dart';
|
import 'package:immich_mobile/services/sync.service.dart';
|
||||||
import 'package:immich_mobile/services/user.service.dart';
|
import 'package:immich_mobile/services/user.service.dart';
|
||||||
|
@ -11,3 +12,5 @@ class MockUserService extends Mock implements UserService {}
|
||||||
class MockSyncService extends Mock implements SyncService {}
|
class MockSyncService extends Mock implements SyncService {}
|
||||||
|
|
||||||
class MockHashService extends Mock implements HashService {}
|
class MockHashService extends Mock implements HashService {}
|
||||||
|
|
||||||
|
class MockEntityService extends Mock implements EntityService {}
|
||||||
|
|
|
@ -3,39 +3,41 @@ import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||||
import 'package:immich_mobile/services/album.service.dart';
|
import 'package:immich_mobile/services/album.service.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
import '../fixtures/album.stub.dart';
|
import '../fixtures/album.stub.dart';
|
||||||
|
import '../fixtures/asset.stub.dart';
|
||||||
|
import '../fixtures/user.stub.dart';
|
||||||
import '../repository.mocks.dart';
|
import '../repository.mocks.dart';
|
||||||
import '../service.mocks.dart';
|
import '../service.mocks.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
late AlbumService sut;
|
late AlbumService sut;
|
||||||
late MockApiService apiService;
|
|
||||||
late MockUserService userService;
|
late MockUserService userService;
|
||||||
late MockSyncService syncService;
|
late MockSyncService syncService;
|
||||||
|
late MockEntityService entityService;
|
||||||
late MockAlbumRepository albumRepository;
|
late MockAlbumRepository albumRepository;
|
||||||
late MockAssetRepository assetRepository;
|
late MockAssetRepository assetRepository;
|
||||||
late MockUserRepository userRepository;
|
|
||||||
late MockBackupRepository backupRepository;
|
late MockBackupRepository backupRepository;
|
||||||
late MockAlbumMediaRepository albumMediaRepository;
|
late MockAlbumMediaRepository albumMediaRepository;
|
||||||
|
late MockAlbumApiRepository albumApiRepository;
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
apiService = MockApiService();
|
|
||||||
userService = MockUserService();
|
userService = MockUserService();
|
||||||
syncService = MockSyncService();
|
syncService = MockSyncService();
|
||||||
|
entityService = MockEntityService();
|
||||||
albumRepository = MockAlbumRepository();
|
albumRepository = MockAlbumRepository();
|
||||||
assetRepository = MockAssetRepository();
|
assetRepository = MockAssetRepository();
|
||||||
userRepository = MockUserRepository();
|
|
||||||
backupRepository = MockBackupRepository();
|
backupRepository = MockBackupRepository();
|
||||||
albumMediaRepository = MockAlbumMediaRepository();
|
albumMediaRepository = MockAlbumMediaRepository();
|
||||||
|
albumApiRepository = MockAlbumApiRepository();
|
||||||
|
|
||||||
sut = AlbumService(
|
sut = AlbumService(
|
||||||
apiService,
|
|
||||||
userService,
|
userService,
|
||||||
syncService,
|
syncService,
|
||||||
|
entityService,
|
||||||
albumRepository,
|
albumRepository,
|
||||||
assetRepository,
|
assetRepository,
|
||||||
userRepository,
|
|
||||||
backupRepository,
|
backupRepository,
|
||||||
albumMediaRepository,
|
albumMediaRepository,
|
||||||
|
albumApiRepository,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -70,4 +72,125 @@ void main() {
|
||||||
verifyNoMoreInteractions(syncService);
|
verifyNoMoreInteractions(syncService);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
group('refreshRemoteAlbums', () {
|
||||||
|
test('isShared: false', () async {
|
||||||
|
when(() => userService.refreshUsers()).thenAnswer((_) async => true);
|
||||||
|
when(() => albumApiRepository.getAll(shared: null))
|
||||||
|
.thenAnswer((_) async => [AlbumStub.oneAsset, AlbumStub.twoAsset]);
|
||||||
|
when(
|
||||||
|
() => syncService.syncRemoteAlbumsToDb(
|
||||||
|
[AlbumStub.oneAsset, AlbumStub.twoAsset],
|
||||||
|
isShared: false,
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => true);
|
||||||
|
final result = await sut.refreshRemoteAlbums(isShared: false);
|
||||||
|
expect(result, true);
|
||||||
|
verify(() => userService.refreshUsers()).called(1);
|
||||||
|
verify(() => albumApiRepository.getAll(shared: null)).called(1);
|
||||||
|
verify(
|
||||||
|
() => syncService.syncRemoteAlbumsToDb(
|
||||||
|
[AlbumStub.oneAsset, AlbumStub.twoAsset],
|
||||||
|
isShared: false,
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
verifyNoMoreInteractions(userService);
|
||||||
|
verifyNoMoreInteractions(albumApiRepository);
|
||||||
|
verifyNoMoreInteractions(syncService);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('createAlbum', () {
|
||||||
|
test('shared with assets', () async {
|
||||||
|
when(
|
||||||
|
() => albumApiRepository.create(
|
||||||
|
"name",
|
||||||
|
assetIds: any(named: "assetIds"),
|
||||||
|
sharedUserIds: any(named: "sharedUserIds"),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => AlbumStub.oneAsset);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => entityService.fillAlbumWithDatabaseEntities(AlbumStub.oneAsset),
|
||||||
|
).thenAnswer((_) async => AlbumStub.oneAsset);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => albumRepository.create(AlbumStub.oneAsset),
|
||||||
|
).thenAnswer((_) async => AlbumStub.twoAsset);
|
||||||
|
|
||||||
|
final result =
|
||||||
|
await sut.createAlbum("name", [AssetStub.image1], [UserStub.user1]);
|
||||||
|
expect(result, AlbumStub.twoAsset);
|
||||||
|
verify(
|
||||||
|
() => albumApiRepository.create(
|
||||||
|
"name",
|
||||||
|
assetIds: [AssetStub.image1.remoteId!],
|
||||||
|
sharedUserIds: [UserStub.user1.id],
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
verify(
|
||||||
|
() => entityService.fillAlbumWithDatabaseEntities(AlbumStub.oneAsset),
|
||||||
|
).called(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('addAdditionalAssetToAlbum', () {
|
||||||
|
test('one added, one duplicate', () async {
|
||||||
|
when(
|
||||||
|
() => albumApiRepository.addAssets(AlbumStub.oneAsset.remoteId!, any()),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async => (
|
||||||
|
added: [AssetStub.image2.remoteId!],
|
||||||
|
duplicates: [AssetStub.image1.remoteId!]
|
||||||
|
),
|
||||||
|
);
|
||||||
|
when(
|
||||||
|
() => albumRepository.getById(AlbumStub.oneAsset.id),
|
||||||
|
).thenAnswer((_) async => AlbumStub.oneAsset);
|
||||||
|
when(
|
||||||
|
() => albumRepository.addAssets(AlbumStub.oneAsset, [AssetStub.image2]),
|
||||||
|
).thenAnswer((_) async {});
|
||||||
|
when(
|
||||||
|
() => albumRepository.removeAssets(AlbumStub.oneAsset, []),
|
||||||
|
).thenAnswer((_) async {});
|
||||||
|
when(
|
||||||
|
() => albumRepository.recalculateMetadata(AlbumStub.oneAsset),
|
||||||
|
).thenAnswer((_) async => AlbumStub.oneAsset);
|
||||||
|
when(
|
||||||
|
() => albumRepository.update(AlbumStub.oneAsset),
|
||||||
|
).thenAnswer((_) async => AlbumStub.oneAsset);
|
||||||
|
|
||||||
|
final result = await sut.addAdditionalAssetToAlbum(
|
||||||
|
[AssetStub.image1, AssetStub.image2],
|
||||||
|
AlbumStub.oneAsset,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result != null, true);
|
||||||
|
expect(result!.alreadyInAlbum, [AssetStub.image1.remoteId!]);
|
||||||
|
expect(result.successfullyAdded, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('addAdditionalUserToAlbum', () {
|
||||||
|
test('one added', () async {
|
||||||
|
when(
|
||||||
|
() =>
|
||||||
|
albumApiRepository.addUsers(AlbumStub.emptyAlbum.remoteId!, any()),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async => AlbumStub.sharedWithUser,
|
||||||
|
);
|
||||||
|
when(
|
||||||
|
() => entityService
|
||||||
|
.fillAlbumWithDatabaseEntities(AlbumStub.sharedWithUser),
|
||||||
|
).thenAnswer((_) async => AlbumStub.sharedWithUser);
|
||||||
|
when(
|
||||||
|
() => albumRepository.update(AlbumStub.sharedWithUser),
|
||||||
|
).thenAnswer((_) async => AlbumStub.sharedWithUser);
|
||||||
|
|
||||||
|
final result = await sut.addAdditionalUserToAlbum(
|
||||||
|
[UserStub.user2.id],
|
||||||
|
AlbumStub.emptyAlbum,
|
||||||
|
);
|
||||||
|
expect(result, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
76
mobile/test/services/entity.service_test.dart
Normal file
76
mobile/test/services/entity.service_test.dart
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
|
import 'package:immich_mobile/services/entity.service.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import '../fixtures/asset.stub.dart';
|
||||||
|
import '../fixtures/user.stub.dart';
|
||||||
|
import '../repository.mocks.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late EntityService sut;
|
||||||
|
late MockAssetRepository assetRepository;
|
||||||
|
late MockUserRepository userRepository;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
assetRepository = MockAssetRepository();
|
||||||
|
userRepository = MockUserRepository();
|
||||||
|
sut = EntityService(assetRepository, userRepository);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('fillAlbumWithDatabaseEntities', () {
|
||||||
|
test('remote album with owner, thumbnail, sharedUsers and assets',
|
||||||
|
() async {
|
||||||
|
final Album album = Album(
|
||||||
|
name: "album-with-two-assets-and-two-users",
|
||||||
|
localId: "album-with-two-assets-and-two-users-local",
|
||||||
|
remoteId: "album-with-two-assets-and-two-users-remote",
|
||||||
|
createdAt: DateTime(2001),
|
||||||
|
modifiedAt: DateTime(2010),
|
||||||
|
shared: true,
|
||||||
|
activityEnabled: true,
|
||||||
|
startDate: DateTime(2019),
|
||||||
|
endDate: DateTime(2020),
|
||||||
|
)
|
||||||
|
..remoteThumbnailAssetId = AssetStub.image1.remoteId
|
||||||
|
..assets.addAll([AssetStub.image1, AssetStub.image1])
|
||||||
|
..owner.value = UserStub.user1
|
||||||
|
..sharedUsers.addAll([UserStub.admin, UserStub.admin]);
|
||||||
|
|
||||||
|
when(() => userRepository.get(album.ownerId!))
|
||||||
|
.thenAnswer((_) async => UserStub.admin);
|
||||||
|
|
||||||
|
when(() => assetRepository.getByRemoteId(AssetStub.image1.remoteId!))
|
||||||
|
.thenAnswer((_) async => AssetStub.image1);
|
||||||
|
|
||||||
|
when(() => userRepository.getByIds(any()))
|
||||||
|
.thenAnswer((_) async => [UserStub.user1, UserStub.user2]);
|
||||||
|
|
||||||
|
when(() => assetRepository.getAllByRemoteId(any()))
|
||||||
|
.thenAnswer((_) async => [AssetStub.image1, AssetStub.image2]);
|
||||||
|
|
||||||
|
await sut.fillAlbumWithDatabaseEntities(album);
|
||||||
|
expect(album.owner.value, UserStub.admin);
|
||||||
|
expect(album.thumbnail.value, AssetStub.image1);
|
||||||
|
expect(album.remoteUsers.toSet(), {UserStub.user1, UserStub.user2});
|
||||||
|
expect(album.remoteAssets.toSet(), {AssetStub.image1, AssetStub.image2});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('remote album without any info', () async {
|
||||||
|
makeEmptyAlbum() => Album(
|
||||||
|
name: "album-without-info",
|
||||||
|
localId: "album-without-info-local",
|
||||||
|
remoteId: "album-without-info-remote",
|
||||||
|
createdAt: DateTime(2001),
|
||||||
|
modifiedAt: DateTime(2010),
|
||||||
|
shared: false,
|
||||||
|
activityEnabled: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
final album = makeEmptyAlbum();
|
||||||
|
await sut.fillAlbumWithDatabaseEntities(album);
|
||||||
|
verifyNoMoreInteractions(assetRepository);
|
||||||
|
verifyNoMoreInteractions(userRepository);
|
||||||
|
expect(album, makeEmptyAlbum());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in a new issue