mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
feat(mobile): partner sharing (#2541)
* feat(mobile): partner sharing * getAllAssets for other users * i18n * fix tests * try to fix web tests * shared with/by confusion * error logging * guard against outdated server version
This commit is contained in:
parent
1613ae9185
commit
bcc2c34eef
48 changed files with 1729 additions and 226 deletions
|
|
@ -16,6 +16,7 @@ class ApiService {
|
|||
late AssetApi assetApi;
|
||||
late SearchApi searchApi;
|
||||
late ServerInfoApi serverInfoApi;
|
||||
late PartnerApi partnerApi;
|
||||
|
||||
ApiService() {
|
||||
final endpoint = Store.tryGet(StoreKey.serverEndpoint);
|
||||
|
|
@ -37,6 +38,7 @@ class ApiService {
|
|||
assetApi = AssetApi(_apiClient);
|
||||
serverInfoApi = ServerInfoApi(_apiClient);
|
||||
searchApi = SearchApi(_apiClient);
|
||||
partnerApi = PartnerApi(_apiClient);
|
||||
}
|
||||
|
||||
Future<String> resolveAndSetEndpoint(String serverUrl) async {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ import 'dart:async';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/etag.dart';
|
||||
import 'package:immich_mobile/shared/models/exif_info.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/models/user.dart';
|
||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||
|
|
@ -36,37 +38,47 @@ class AssetService {
|
|||
|
||||
/// Checks the server for updated assets and updates the local database if
|
||||
/// required. Returns `true` if there were any changes.
|
||||
Future<bool> refreshRemoteAssets() async {
|
||||
Future<bool> refreshRemoteAssets([User? user]) async {
|
||||
user ??= Store.get(StoreKey.currentUser);
|
||||
final Stopwatch sw = Stopwatch()..start();
|
||||
final int numOwnedRemoteAssets = await _db.assets
|
||||
.where()
|
||||
.remoteIdIsNotNull()
|
||||
.filter()
|
||||
.ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
|
||||
.ownerIdEqualTo(user!.isarId)
|
||||
.count();
|
||||
final bool changes = await _syncService.syncRemoteAssetsToDb(
|
||||
() async => (await _getRemoteAssets(hasCache: numOwnedRemoteAssets > 0))
|
||||
?.map(Asset.remote)
|
||||
.toList(),
|
||||
user,
|
||||
() async => (await _getRemoteAssets(
|
||||
hasCache: numOwnedRemoteAssets > 0,
|
||||
user: user!,
|
||||
)),
|
||||
);
|
||||
debugPrint("refreshRemoteAssets full took ${sw.elapsedMilliseconds}ms");
|
||||
return changes;
|
||||
}
|
||||
|
||||
/// Returns `null` if the server state did not change, else list of assets
|
||||
Future<List<AssetResponseDto>?> _getRemoteAssets({
|
||||
Future<List<Asset>?> _getRemoteAssets({
|
||||
required bool hasCache,
|
||||
required User user,
|
||||
}) async {
|
||||
try {
|
||||
final etag = hasCache ? Store.tryGet(StoreKey.assetETag) : null;
|
||||
final etag = hasCache ? _db.eTags.getByIdSync(user.id)?.value : null;
|
||||
final (List<AssetResponseDto>? assets, String? newETag) =
|
||||
await _apiService.assetApi.getAllAssetsWithETag(eTag: etag);
|
||||
await _apiService.assetApi
|
||||
.getAllAssetsWithETag(eTag: etag, userId: user.id);
|
||||
if (assets == null) {
|
||||
return null;
|
||||
} else if (assets.isNotEmpty && assets.first.ownerId != user.id) {
|
||||
log.warning("Make sure that server and app versions match!"
|
||||
" The server returned assets for user ${assets.first.ownerId}"
|
||||
" while requesting assets of user ${user.id}");
|
||||
return null;
|
||||
} else if (newETag != etag) {
|
||||
Store.put(StoreKey.assetETag, newETag);
|
||||
_db.writeTxn(() => _db.eTags.put(ETag(id: user.id, value: newETag)));
|
||||
}
|
||||
return assets;
|
||||
return assets.map(Asset.remote).toList();
|
||||
} catch (e, stack) {
|
||||
log.severe('Error while getting remote assets', e, stack);
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -40,7 +40,9 @@ class SyncService {
|
|||
dbUsers,
|
||||
compare: (User a, User b) => a.id.compareTo(b.id),
|
||||
both: (User a, User b) {
|
||||
if (!a.updatedAt.isAtSameMomentAs(b.updatedAt)) {
|
||||
if (!a.updatedAt.isAtSameMomentAs(b.updatedAt) ||
|
||||
a.isPartnerSharedBy != b.isPartnerSharedBy ||
|
||||
a.isPartnerSharedWith != b.isPartnerSharedWith) {
|
||||
toUpsert.add(a);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -61,9 +63,10 @@ class SyncService {
|
|||
/// Syncs remote assets owned by the logged-in user to the DB
|
||||
/// Returns `true` if there were any changes
|
||||
Future<bool> syncRemoteAssetsToDb(
|
||||
User user,
|
||||
FutureOr<List<Asset>?> Function() loadAssets,
|
||||
) =>
|
||||
_lock.run(() => _syncRemoteAssetsToDb(loadAssets));
|
||||
_lock.run(() => _syncRemoteAssetsToDb(user, loadAssets));
|
||||
|
||||
/// Syncs remote albums to the database
|
||||
/// returns `true` if there were any changes
|
||||
|
|
@ -149,13 +152,13 @@ class SyncService {
|
|||
/// Syncs remote assets to the databas
|
||||
/// returns `true` if there were any changes
|
||||
Future<bool> _syncRemoteAssetsToDb(
|
||||
User user,
|
||||
FutureOr<List<Asset>?> Function() loadAssets,
|
||||
) async {
|
||||
final List<Asset>? remote = await loadAssets();
|
||||
if (remote == null) {
|
||||
return false;
|
||||
}
|
||||
final User user = Store.get(StoreKey.currentUser);
|
||||
final List<Asset> inDb = await _db.assets
|
||||
.filter()
|
||||
.ownerIdEqualTo(user.isarId)
|
||||
|
|
@ -349,10 +352,19 @@ class SyncService {
|
|||
);
|
||||
} else if (album.shared) {
|
||||
final User user = Store.get(StoreKey.currentUser);
|
||||
// delete assets in DB unless they belong to this user or are part of some other shared album
|
||||
deleteCandidates.addAll(
|
||||
await album.assets.filter().not().ownerIdEqualTo(user.isarId).findAll(),
|
||||
);
|
||||
// delete assets in DB unless they belong to this user or are part of some other shared album or belong to a partner
|
||||
final userIds = await _db.users
|
||||
.filter()
|
||||
.isPartnerSharedWithEqualTo(true)
|
||||
.isarIdProperty()
|
||||
.findAll();
|
||||
userIds.add(user.isarId);
|
||||
final orphanedAssets = await album.assets
|
||||
.filter()
|
||||
.not()
|
||||
.anyOf(userIds, (q, int id) => q.ownerIdEqualTo(id))
|
||||
.findAll();
|
||||
deleteCandidates.addAll(orphanedAssets);
|
||||
}
|
||||
try {
|
||||
final bool ok = await _db.writeTxn(() => _db.albums.delete(album.id));
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:immich_mobile/modules/partner/services/partner.service.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/models/user.dart';
|
||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||
import 'package:immich_mobile/shared/services/sync.service.dart';
|
||||
import 'package:immich_mobile/utils/diff.dart';
|
||||
import 'package:immich_mobile/utils/files_helper.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final userServiceProvider = Provider(
|
||||
|
|
@ -18,6 +21,7 @@ final userServiceProvider = Provider(
|
|||
ref.watch(apiServiceProvider),
|
||||
ref.watch(dbProvider),
|
||||
ref.watch(syncServiceProvider),
|
||||
ref.watch(partnerServiceProvider),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -25,15 +29,22 @@ class UserService {
|
|||
final ApiService _apiService;
|
||||
final Isar _db;
|
||||
final SyncService _syncService;
|
||||
final PartnerService _partnerService;
|
||||
final Logger _log = Logger("UserService");
|
||||
|
||||
UserService(this._apiService, this._db, this._syncService);
|
||||
UserService(
|
||||
this._apiService,
|
||||
this._db,
|
||||
this._syncService,
|
||||
this._partnerService,
|
||||
);
|
||||
|
||||
Future<List<User>?> _getAllUsers({required bool isAll}) async {
|
||||
try {
|
||||
final dto = await _apiService.userApi.getAllUsers(isAll);
|
||||
return dto?.map(User.fromDto).toList();
|
||||
} catch (e) {
|
||||
debugPrint("Error [getAllUsersInfo] ${e.toString()}");
|
||||
_log.warning("Failed get all users:\n$e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -62,16 +73,45 @@ class UserService {
|
|||
),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint("Error [uploadProfileImage] ${e.toString()}");
|
||||
_log.warning("Failed to upload profile image:\n$e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> refreshUsers() async {
|
||||
final List<User>? users = await _getAllUsers(isAll: true);
|
||||
if (users == null) {
|
||||
final List<User>? sharedBy =
|
||||
await _partnerService.getPartners(PartnerDirection.sharedBy);
|
||||
final List<User>? sharedWith =
|
||||
await _partnerService.getPartners(PartnerDirection.sharedWith);
|
||||
|
||||
if (users == null || sharedBy == null || sharedWith == null) {
|
||||
_log.warning("Failed to refresh users");
|
||||
return false;
|
||||
}
|
||||
|
||||
users.sortBy((u) => u.id);
|
||||
sharedBy.sortBy((u) => u.id);
|
||||
sharedWith.sortBy((u) => u.id);
|
||||
|
||||
diffSortedListsSync(
|
||||
users,
|
||||
sharedBy,
|
||||
compare: (User a, User b) => a.id.compareTo(b.id),
|
||||
both: (User a, User b) => a.isPartnerSharedBy = true,
|
||||
onlyFirst: (_) {},
|
||||
onlySecond: (_) {},
|
||||
);
|
||||
|
||||
diffSortedListsSync(
|
||||
users,
|
||||
sharedWith,
|
||||
compare: (User a, User b) => a.id.compareTo(b.id),
|
||||
both: (User a, User b) => a.isPartnerSharedWith = true,
|
||||
onlyFirst: (_) {},
|
||||
onlySecond: (_) {},
|
||||
);
|
||||
|
||||
return _syncService.syncUsersFromServer(users);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue