1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +00:00

refactor(mobile): use repositories in a number of services (#12891)

* UserService
* PartnerService
* HashService
* MemoryService
* PersonService
* SearchService
* StackService
This commit is contained in:
Fynn Petersen-Frey 2024-09-24 14:50:21 +02:00 committed by GitHub
parent e0fa3cdbc7
commit 202082f62e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 409 additions and 206 deletions

View file

@ -69,14 +69,14 @@ custom_lint:
- integration_test/test_utils/general_helper.dart
- lib/main.dart
- lib/routing/router.dart
- lib/utils/{db,image_url_builder,migration,renderlist_generator}.dart
- lib/utils/{db,migration,renderlist_generator}.dart
- test/**.dart
# refactor to make the providers and services testable
- lib/pages/common/{album_asset_selection,gallery_viewer}.page.dart
- lib/pages/common/album_asset_selection.page.dart
- lib/providers/{archive,asset,authentication,db,favorite,partner,trash,user}.provider.dart
- lib/providers/{album/album,album/shared_album,asset_viewer/asset_stack,asset_viewer/render_list,backup/backup,backup/manual_upload,search/all_motion_photos,search/recently_added_asset}.provider.dart
- lib/services/{asset,background,backup,hash,immich_logger,memory,partner,person,search,stack,sync,user}.service.dart
- lib/widgets/asset_grid/{asset_grid_data_structure,thumbnail_image}.dart
- lib/services/{asset,background,backup,immich_logger,sync}.service.dart
- lib/widgets/asset_grid/asset_grid_data_structure.dart
- import_rule_openapi:
message: openapi must only be used through ApiRepositories
@ -90,18 +90,16 @@ custom_lint:
- test/modules/utils/openapi_patching_test.dart # filename is self-explanatory...
# refactor
- 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/search/{search,search_filter}.provider.dart
- lib/providers/websocket.provider.dart
- lib/routing/auth_guard.dart
- lib/services/{api,asset,backup,memory,oauth,partner,person,search,shared_link,stack,trash,user}.service.dart
- lib/services/{api,asset,backup,memory,oauth,search,shared_link,stack,trash}.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

View file

@ -0,0 +1 @@
const int noDbId = -9223372036854775808; // from Isar

View file

@ -1,5 +1,6 @@
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/device_asset.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
abstract interface class IAssetRepository {
@ -12,6 +13,7 @@ abstract interface class IAssetRepository {
bool? remote,
int limit = 100,
});
Future<List<Asset>> updateAll(List<Asset> assets);
Future<List<Asset>> getMatches({
required List<Asset> assets,
@ -19,4 +21,7 @@ abstract interface class IAssetRepository {
bool? remote,
int limit = 100,
});
Future<List<DeviceAsset?>> getDeviceAssetsById(List<Object> ids);
Future<void> upsertDeviceAssets(List<DeviceAsset> deviceAssets);
}

View file

@ -13,4 +13,6 @@ abstract interface class IAssetApiRepository {
});
// Future<void> delete(String id);
Future<List<Asset>> search({List<String> personIds = const []});
}

View file

@ -0,0 +1,13 @@
import 'package:immich_mobile/entities/user.entity.dart';
abstract interface class IPartnerApiRepository {
Future<List<User>> getAll(Direction direction);
Future<User> create(String id);
Future<User> update(String id, {required bool inTimeline});
Future<void> delete(String id);
}
enum Direction {
sharedWithMe,
sharedByMe,
}

View file

@ -0,0 +1,22 @@
abstract interface class IPersonApiRepository {
Future<List<Person>> getAll();
Future<Person> update(String id, {String? name});
}
class Person {
Person({
required this.id,
required this.isHidden,
required this.name,
required this.thumbnailPath,
this.birthDate,
this.updatedAt,
});
final String id;
final DateTime? birthDate;
final bool isHidden;
final String name;
final String thumbnailPath;
final DateTime? updatedAt;
}

View file

@ -3,4 +3,6 @@ import 'package:immich_mobile/entities/user.entity.dart';
abstract interface class IUserRepository {
Future<List<User>> getByIds(List<String> ids);
Future<User?> get(String id);
Future<List<User>> getAll({bool self = true});
Future<User> update(User user);
}

View file

@ -0,0 +1,11 @@
import 'dart:typed_data';
import 'package:immich_mobile/entities/user.entity.dart';
abstract interface class IUserApiRepository {
Future<List<User>> getAll();
Future<({String profileImagePath})> createProfileImage({
required String name,
required Uint8List data,
});
}

View file

@ -2,7 +2,7 @@
import 'dart:convert';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:openapi/api.dart';
import 'package:immich_mobile/interfaces/person_api.interface.dart';
class SearchLocationFilter {
String? country;
@ -235,7 +235,7 @@ class SearchDisplayFilters {
class SearchFilter {
String? context;
String? filename;
Set<PersonResponseDto> people;
Set<Person> people;
SearchLocationFilter location;
SearchCameraFilter camera;
SearchDateFilter date;
@ -258,7 +258,7 @@ class SearchFilter {
SearchFilter copyWith({
String? context,
String? filename,
Set<PersonResponseDto>? people,
Set<Person>? people,
SearchLocationFilter? location,
SearchCameraFilter? camera,
SearchDateFilter? date,

View file

@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/pages/common/video_viewer.page.dart';
@ -30,7 +31,6 @@ import 'package:immich_mobile/widgets/photo_view/photo_view_gallery.dart';
import 'package:immich_mobile/widgets/photo_view/src/photo_view_computed_scale.dart';
import 'package:immich_mobile/widgets/photo_view/src/photo_view_scale_state.dart';
import 'package:immich_mobile/widgets/photo_view/src/utils/photo_view_hero_attributes.dart';
import 'package:isar/isar.dart';
@RoutePage()
// ignore: must_be_immutable
@ -73,7 +73,7 @@ class GalleryViewerPage extends HookConsumerWidget {
: <Asset>[];
final stackElements = showStack ? [currentAsset, ...stack] : <Asset>[];
// Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id
final isFromDto = currentAsset.id == Isar.autoIncrement;
final isFromDto = currentAsset.id == noDbId;
Asset asset = stackIndex.value == -1
? currentAsset

View file

@ -8,6 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/interfaces/person_api.interface.dart';
import 'package:immich_mobile/models/search/search_filter.model.dart';
import 'package:immich_mobile/providers/search/paginated_search.provider.dart';
import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart';
@ -19,7 +20,6 @@ import 'package:immich_mobile/widgets/search/search_filter/media_type_picker.dar
import 'package:immich_mobile/widgets/search/search_filter/people_picker.dart';
import 'package:immich_mobile/widgets/search/search_filter/search_filter_chip.dart';
import 'package:immich_mobile/widgets/search/search_filter/search_filter_utils.dart';
import 'package:openapi/api.dart';
@RoutePage()
class SearchInputPage extends HookConsumerWidget {
@ -110,7 +110,7 @@ class SearchInputPage extends HookConsumerWidget {
}
showPeoplePicker() {
handleOnSelect(Set<PersonResponseDto> value) {
handleOnSelect(Set<Person> value) {
filter.value = filter.value.copyWith(
people: value,
);

View file

@ -5,5 +5,5 @@ import 'package:immich_mobile/services/user.service.dart';
final otherUsersProvider = FutureProvider.autoDispose<List<User>>((ref) {
UserService userService = ref.watch(userServiceProvider);
return userService.getUsersInDb();
return userService.getUsers();
});

Binary file not shown.

View file

@ -1,14 +1,14 @@
import 'package:immich_mobile/interfaces/person_api.interface.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/services/person.service.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:openapi/api.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'people.provider.g.dart';
@riverpod
Future<List<PersonResponseDto>> getAllPeople(
Future<List<Person>> getAllPeople(
GetAllPeopleRef ref,
) async {
final PersonService personService = ref.read(personServiceProvider);

Binary file not shown.

View file

@ -1,6 +1,11 @@
import 'dart:io';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/android_device_asset.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/device_asset.entity.dart';
import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/providers/db.provider.dart';
@ -69,6 +74,12 @@ class AssetRepository implements IAssetRepository {
return query.limit(limit).findAll();
}
@override
Future<List<Asset>> updateAll(List<Asset> assets) async {
await _db.writeTxn(() => _db.assets.putAll(assets));
return assets;
}
@override
Future<List<Asset>> getMatches({
required List<Asset> assets,
@ -86,6 +97,20 @@ class AssetRepository implements IAssetRepository {
}
return _getMatchesImpl(query, ownerId, assets, limit);
}
@override
Future<List<DeviceAsset?>> getDeviceAssetsById(List<Object> ids) =>
Platform.isAndroid
? _db.androidDeviceAssets.getAll(ids.cast())
: _db.iOSDeviceAssets.getAllById(ids.cast());
@override
Future<void> upsertDeviceAssets(List<DeviceAsset> deviceAssets) =>
_db.writeTxn(
() => Platform.isAndroid
? _db.androidDeviceAssets.putAll(deviceAssets.cast())
: _db.iOSDeviceAssets.putAll(deviceAssets.cast()),
);
}
Future<List<Asset>> _getMatchesImpl(

View file

@ -6,14 +6,18 @@ import 'package:immich_mobile/repositories/base_api.repository.dart';
import 'package:openapi/api.dart';
final assetApiRepositoryProvider = Provider(
(ref) => AssetApiRepository(ref.watch(apiServiceProvider).assetsApi),
(ref) => AssetApiRepository(
ref.watch(apiServiceProvider).assetsApi,
ref.watch(apiServiceProvider).searchApi,
),
);
class AssetApiRepository extends BaseApiRepository
implements IAssetApiRepository {
final AssetsApi _api;
final SearchApi _searchApi;
AssetApiRepository(this._api);
AssetApiRepository(this._api, this._searchApi);
@override
Future<Asset> update(String id, {String? description}) async {
@ -22,4 +26,27 @@ class AssetApiRepository extends BaseApiRepository
);
return Asset.remote(response);
}
@override
Future<List<Asset>> search({List<String> personIds = const []}) async {
// TODO this always fetches all assets, change API and usage to actually do pagination
final List<Asset> result = [];
bool hasNext = true;
int currentPage = 1;
while (hasNext) {
final response = await checkNull(
_searchApi.searchMetadata(
MetadataSearchDto(
personIds: personIds,
page: currentPage,
size: 1000,
),
),
);
result.addAll(response.assets.items.map(Asset.remote));
hasNext = response.assets.nextPage != null;
currentPage++;
}
return result;
}
}

View file

@ -0,0 +1,51 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/base_api.repository.dart';
import 'package:openapi/api.dart';
final partnerApiRepositoryProvider = Provider(
(ref) => PartnerApiRepository(
ref.watch(apiServiceProvider).partnersApi,
),
);
class PartnerApiRepository extends BaseApiRepository
implements IPartnerApiRepository {
final PartnersApi _api;
PartnerApiRepository(this._api);
@override
Future<List<User>> getAll(Direction direction) async {
final response = await checkNull(
_api.getPartners(
direction == Direction.sharedByMe
? PartnerDirection.by
: PartnerDirection.with_,
),
);
return response.map(User.fromPartnerDto).toList();
}
@override
Future<User> create(String id) async {
final dto = await checkNull(_api.createPartner(id));
return User.fromPartnerDto(dto);
}
@override
Future<void> delete(String id) => checkNull(_api.removePartner(id));
@override
Future<User> update(String id, {required bool inTimeline}) async {
final dto = await checkNull(
_api.updatePartner(
id,
UpdatePartnerDto(inTimeline: inTimeline),
),
);
return User.fromPartnerDto(dto);
}
}

View file

@ -0,0 +1,38 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/person_api.interface.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/base_api.repository.dart';
import 'package:openapi/api.dart';
final personApiRepositoryProvider = Provider(
(ref) => PersonApiRepository(ref.watch(apiServiceProvider).peopleApi),
);
class PersonApiRepository extends BaseApiRepository
implements IPersonApiRepository {
final PeopleApi _api;
PersonApiRepository(this._api);
@override
Future<List<Person>> getAll() async {
final dto = await checkNull(_api.getAllPeople());
return dto.people.map(_toPerson).toList();
}
@override
Future<Person> update(String id, {String? name}) async {
final dto = await checkNull(
_api.updatePerson(id, PersonUpdateDto(name: name)),
);
return _toPerson(dto);
}
static Person _toPerson(PersonResponseDto dto) => Person(
birthDate: dto.birthDate,
id: dto.id,
isHidden: dto.isHidden,
name: dto.name,
thumbnailPath: dto.thumbnailPath,
);
}

View file

@ -1,4 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/user.interface.dart';
import 'package:immich_mobile/providers/db.provider.dart';
@ -20,4 +21,19 @@ class UserRepository implements IUserRepository {
@override
Future<User?> get(String id) => _db.users.getById(id);
@override
Future<List<User>> getAll({bool self = true}) {
if (self) {
return _db.users.where().findAll();
}
final int userId = Store.get(StoreKey.currentUser).isarId;
return _db.users.where().isarIdNotEqualTo(userId).findAll();
}
@override
Future<User> update(User user) async {
await _db.writeTxn(() => _db.users.put(user));
return user;
}
}

View file

@ -0,0 +1,41 @@
import 'dart:typed_data';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:http/http.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/user_api.interface.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/base_api.repository.dart';
import 'package:openapi/api.dart';
final userApiRepositoryProvider = Provider(
(ref) => UserApiRepository(
ref.watch(apiServiceProvider).usersApi,
),
);
class UserApiRepository extends BaseApiRepository
implements IUserApiRepository {
final UsersApi _api;
UserApiRepository(this._api);
@override
Future<List<User>> getAll() async {
final dto = await checkNull(_api.searchUsers());
return dto.map(User.fromSimpleUserDto).toList();
}
@override
Future<({String profileImagePath})> createProfileImage({
required String name,
required Uint8List data,
}) async {
final response = await checkNull(
_api.createProfileImage(
MultipartFile.fromBytes('file', data, filename: name),
),
);
return (profileImagePath: response.profileImagePath);
}
}

View file

@ -18,7 +18,9 @@ import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/repositories/backup.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/partner_api.repository.dart';
import 'package:immich_mobile/repositories/user.repository.dart';
import 'package:immich_mobile/repositories/user_api.repository.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';
@ -30,7 +32,6 @@ import 'package:immich_mobile/services/backup.service.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/partner.service.dart';
import 'package:immich_mobile/services/sync.service.dart';
import 'package:immich_mobile/services/user.service.dart';
import 'package:immich_mobile/utils/backup_progress.dart';
@ -362,16 +363,20 @@ class BackgroundService {
apiService.setAccessToken(Store.get(StoreKey.accessToken));
AppSettingsService settingService = AppSettingsService();
AppSettingsService settingsService = AppSettingsService();
PartnerService partnerService = PartnerService(apiService, db);
AlbumRepository albumRepository = AlbumRepository(db);
AssetRepository assetRepository = AssetRepository(db);
BackupRepository backupAlbumRepository = BackupRepository(db);
AlbumMediaRepository albumMediaRepository = AlbumMediaRepository();
FileMediaRepository fileMediaRepository = FileMediaRepository();
UserRepository userRepository = UserRepository(db);
UserApiRepository userApiRepository =
UserApiRepository(apiService.usersApi);
AlbumApiRepository albumApiRepository =
AlbumApiRepository(apiService.albumsApi);
HashService hashService = HashService(db, this, albumMediaRepository);
PartnerApiRepository partnerApiRepository =
PartnerApiRepository(apiService.partnersApi);
HashService hashService =
HashService(assetRepository, this, albumMediaRepository);
EntityService entityService =
EntityService(assetRepository, userRepository);
SyncService syncSerive = SyncService(
@ -381,8 +386,12 @@ class BackgroundService {
albumMediaRepository,
albumApiRepository,
);
UserService userService =
UserService(apiService, db, syncSerive, partnerService);
UserService userService = UserService(
partnerApiRepository,
userApiRepository,
userRepository,
syncSerive,
);
AlbumService albumService = AlbumService(
userService,
syncSerive,

View file

@ -4,20 +4,24 @@ import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/interfaces/album_media.interface.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/repositories/album_media.repository.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/services/background.service.dart';
import 'package:immich_mobile/entities/android_device_asset.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/device_asset.entity.dart';
import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/extensions/string_extensions.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
class HashService {
HashService(this._db, this._backgroundService, this._albumMediaRepository);
final Isar _db;
HashService(
this._assetRepository,
this._backgroundService,
this._albumMediaRepository,
);
final IAssetRepository _assetRepository;
final BackgroundService _backgroundService;
final IAlbumMediaRepository _albumMediaRepository;
final _log = Logger('HashService');
@ -55,7 +59,8 @@ class HashService {
final ids = assets
.map(Platform.isAndroid ? (a) => a.localId!.toInt() : (a) => a.localId!)
.toList();
final List<DeviceAsset?> hashes = await _lookupHashes(ids);
final List<DeviceAsset?> hashes =
await _assetRepository.getDeviceAssetsById(ids);
final List<DeviceAsset> toAdd = [];
final List<String> toHash = [];
@ -106,12 +111,6 @@ class HashService {
return _getHashedAssets(assets, hashes);
}
/// Lookup hashes of assets by their local ID
Future<List<DeviceAsset?>> _lookupHashes(List<Object> ids) =>
Platform.isAndroid
? _db.androidDeviceAssets.getAll(ids.cast())
: _db.iOSDeviceAssets.getAllById(ids.cast());
/// Processes a batch of files and saves any successfully hashed
/// values to the DB table.
Future<void> _processBatch(
@ -131,11 +130,7 @@ class HashService {
final validHashes = anyNull
? toAdd.where((e) => e.hash.length == 20).toList(growable: false)
: toAdd;
await _db.writeTxn(
() => Platform.isAndroid
? _db.androidDeviceAssets.putAll(validHashes.cast())
: _db.iOSDeviceAssets.putAll(validHashes.cast()),
);
await _assetRepository.upsertDeviceAssets(validHashes);
_log.fine("Hashed ${validHashes.length}/${toHash.length} assets");
}
@ -168,7 +163,7 @@ class HashService {
final hashServiceProvider = Provider(
(ref) => HashService(
ref.watch(dbProvider),
ref.watch(assetRepositoryProvider),
ref.watch(backgroundServiceProvider),
ref.watch(albumMediaRepositoryProvider),
),

View file

@ -1,18 +1,17 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/models/memories/memory.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
final memoryServiceProvider = StateProvider<MemoryService>((ref) {
return MemoryService(
ref.watch(apiServiceProvider),
ref.watch(dbProvider),
ref.watch(assetRepositoryProvider),
);
});
@ -20,9 +19,9 @@ class MemoryService {
final log = Logger("MemoryService");
final ApiService _apiService;
final Isar _db;
final IAssetRepository _assetRepository;
MemoryService(this._apiService, this._db);
MemoryService(this._apiService, this._assetRepository);
Future<List<Memory>?> getMemoryLane() async {
try {
@ -39,7 +38,7 @@ class MemoryService {
List<Memory> memories = [];
for (final MemoryLaneResponseDto(:yearsAgo, :assets) in data) {
final dbAssets =
await _db.assets.getAllByRemoteId(assets.map((e) => e.id));
await _assetRepository.getAllByRemoteId(assets.map((e) => e.id));
if (dbAssets.isNotEmpty) {
final String title = yearsAgo <= 1
? 'memories_year_ago'.tr()

View file

@ -1,43 +1,33 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:isar/isar.dart';
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
import 'package:immich_mobile/interfaces/user.interface.dart';
import 'package:immich_mobile/repositories/partner_api.repository.dart';
import 'package:immich_mobile/repositories/user.repository.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
final partnerServiceProvider = Provider(
(ref) => PartnerService(
ref.watch(apiServiceProvider),
ref.watch(dbProvider),
ref.watch(partnerApiRepositoryProvider),
ref.watch(userRepositoryProvider),
),
);
class PartnerService {
final ApiService _apiService;
final Isar _db;
final IPartnerApiRepository _partnerApiRepository;
final IUserRepository _userRepository;
final Logger _log = Logger("PartnerService");
PartnerService(this._apiService, this._db);
Future<List<User>?> getPartners(PartnerDirection direction) async {
try {
final userDtos = await _apiService.partnersApi.getPartners(direction);
if (userDtos != null) {
return userDtos.map((u) => User.fromPartnerDto(u)).toList();
}
} catch (e) {
_log.warning("Failed to get partners for direction $direction", e);
}
return null;
}
PartnerService(
this._partnerApiRepository,
this._userRepository,
);
Future<bool> removePartner(User partner) async {
try {
await _apiService.partnersApi.removePartner(partner.id);
await _partnerApiRepository.delete(partner.id);
partner.isPartnerSharedBy = false;
await _db.writeTxn(() => _db.users.put(partner));
await _userRepository.update(partner);
} catch (e) {
_log.warning("Failed to remove partner ${partner.id}", e);
return false;
@ -47,12 +37,10 @@ class PartnerService {
Future<bool> addPartner(User partner) async {
try {
final dto = await _apiService.partnersApi.createPartner(partner.id);
if (dto != null) {
await _partnerApiRepository.create(partner.id);
partner.isPartnerSharedBy = true;
await _db.writeTxn(() => _db.users.put(partner));
await _userRepository.update(partner);
return true;
}
} catch (e) {
_log.warning("Failed to add partner ${partner.id}", e);
}
@ -61,13 +49,13 @@ class PartnerService {
Future<bool> updatePartner(User partner, {required bool inTimeline}) async {
try {
final dto = await _apiService.partnersApi
.updatePartner(partner.id, UpdatePartnerDto(inTimeline: inTimeline));
if (dto != null) {
partner.inTimeline = dto.inTimeline ?? partner.inTimeline;
await _db.writeTxn(() => _db.users.put(partner));
final dto = await _partnerApiRepository.update(
partner.id,
inTimeline: inTimeline,
);
partner.inTimeline = dto.inTimeline;
await _userRepository.update(partner);
return true;
}
} catch (e) {
_log.warning("Failed to update partner ${partner.id}", e);
}

View file

@ -1,29 +1,37 @@
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:isar/isar.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/asset_api.interface.dart';
import 'package:immich_mobile/interfaces/person_api.interface.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:immich_mobile/repositories/person_api.repository.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'person.service.g.dart';
@riverpod
PersonService personService(PersonServiceRef ref) =>
PersonService(ref.read(apiServiceProvider), ref.read(dbProvider));
PersonService personService(PersonServiceRef ref) => PersonService(
ref.watch(personApiRepositoryProvider),
ref.watch(assetApiRepositoryProvider),
ref.read(assetRepositoryProvider),
);
class PersonService {
final Logger _log = Logger("PersonService");
final ApiService _apiService;
final Isar _db;
final IPersonApiRepository _personApiRepository;
final IAssetApiRepository _assetApiRepository;
final IAssetRepository _assetRepository;
PersonService(this._apiService, this._db);
PersonService(
this._personApiRepository,
this._assetApiRepository,
this._assetRepository,
);
Future<List<PersonResponseDto>> getAllPeople() async {
Future<List<Person>> getAllPeople() async {
try {
final peopleResponseDto = await _apiService.peopleApi.getAllPeople();
return peopleResponseDto?.people ?? [];
return await _personApiRepository.getAll();
} catch (error, stack) {
_log.severe("Error while fetching curated people", error, stack);
return [];
@ -31,50 +39,19 @@ class PersonService {
}
Future<List<Asset>> getPersonAssets(String id) async {
List<Asset> result = [];
var hasNext = true;
var currentPage = 1;
try {
while (hasNext) {
final response = await _apiService.searchApi.searchMetadata(
MetadataSearchDto(
personIds: [id],
page: currentPage,
size: 1000,
),
);
if (response == null) {
break;
}
if (response.assets.nextPage == null) {
hasNext = false;
}
final assets = response.assets.items;
final mapAssets =
await _db.assets.getAllByRemoteId(assets.map((e) => e.id));
result.addAll(mapAssets);
currentPage++;
}
final assets = await _assetApiRepository.search(personIds: [id]);
return await _assetRepository
.getAllByRemoteId(assets.map((a) => a.remoteId!));
} catch (error, stack) {
_log.severe("Error while fetching person assets", error, stack);
}
return result;
return [];
}
Future<PersonResponseDto?> updateName(String id, String name) async {
Future<Person?> updateName(String id, String name) async {
try {
return await _apiService.peopleApi.updatePerson(
id,
PersonUpdateDto(
name: name,
),
);
return await _personApiRepository.update(id, name: name);
} catch (error, stack) {
_log.severe("Error while updating person name", error, stack);
}

Binary file not shown.

View file

@ -1,27 +1,27 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/models/search/search_filter.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
final searchServiceProvider = Provider(
(ref) => SearchService(
ref.watch(apiServiceProvider),
ref.watch(dbProvider),
ref.watch(assetRepositoryProvider),
),
);
class SearchService {
final ApiService _apiService;
final Isar _db;
final IAssetRepository _assetRepository;
final _log = Logger("SearchService");
SearchService(this._apiService, this._db);
SearchService(this._apiService, this._assetRepository);
Future<List<String>?> getSearchSuggestions(
SearchSuggestionType type, {
@ -103,7 +103,7 @@ class SearchService {
return null;
}
return _db.assets
return _assetRepository
.getAllByRemoteId(response.assets.items.map((e) => e.id));
} catch (error, stackTrace) {
_log.severe("Failed to search for assets", error, stackTrace);

View file

@ -1,17 +1,17 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:isar/isar.dart';
import 'package:openapi/api.dart';
class StackService {
StackService(this._api, this._db);
StackService(this._api, this._assetRepository);
final ApiService _api;
final Isar _db;
final IAssetRepository _assetRepository;
Future<StackResponseDto?> getStack(String stackId) async {
try {
@ -61,10 +61,7 @@ class StackService {
removeAssets.add(asset);
}
_db.writeTxn(() async {
await _db.assets.putAll(removeAssets);
});
await _assetRepository.updateAll(removeAssets);
} catch (error) {
debugPrint("Error while deleting stack: $error");
}
@ -74,6 +71,6 @@ class StackService {
final stackServiceProvider = Provider(
(ref) => StackService(
ref.watch(apiServiceProvider),
ref.watch(dbProvider),
ref.watch(assetRepositoryProvider),
),
);

View file

@ -1,68 +1,48 @@
import 'package:collection/collection.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:http/http.dart';
import 'package:image_picker/image_picker.dart';
import 'package:immich_mobile/services/partner.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
import 'package:immich_mobile/interfaces/user.interface.dart';
import 'package:immich_mobile/interfaces/user_api.interface.dart';
import 'package:immich_mobile/repositories/partner_api.repository.dart';
import 'package:immich_mobile/repositories/user.repository.dart';
import 'package:immich_mobile/repositories/user_api.repository.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/sync.service.dart';
import 'package:immich_mobile/utils/diff.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
final userServiceProvider = Provider(
(ref) => UserService(
ref.watch(apiServiceProvider),
ref.watch(dbProvider),
ref.watch(partnerApiRepositoryProvider),
ref.watch(userApiRepositoryProvider),
ref.watch(userRepositoryProvider),
ref.watch(syncServiceProvider),
ref.watch(partnerServiceProvider),
),
);
class UserService {
final ApiService _apiService;
final Isar _db;
final IPartnerApiRepository _partnerApiRepository;
final IUserApiRepository _userApiRepository;
final IUserRepository _userRepository;
final SyncService _syncService;
final PartnerService _partnerService;
final Logger _log = Logger("UserService");
UserService(
this._apiService,
this._db,
this._partnerApiRepository,
this._userApiRepository,
this._userRepository,
this._syncService,
this._partnerService,
);
Future<List<User>?> _getAllUsers() async {
try {
final dto = await _apiService.usersApi.searchUsers();
return dto?.map(User.fromSimpleUserDto).toList();
} catch (e) {
_log.warning("Failed get all users", e);
return null;
}
}
Future<List<User>> getUsers({bool self = false}) =>
_userRepository.getAll(self: self);
Future<List<User>> getUsersInDb({bool self = false}) async {
if (self) {
return _db.users.where().findAll();
}
final int userId = Store.get(StoreKey.currentUser).isarId;
return _db.users.where().isarIdNotEqualTo(userId).findAll();
}
Future<CreateProfileImageResponseDto?> uploadProfileImage(XFile image) async {
Future<({String profileImagePath})?> uploadProfileImage(XFile image) async {
try {
return await _apiService.usersApi.createProfileImage(
MultipartFile.fromBytes(
'file',
await image.readAsBytes(),
filename: image.name,
),
return await _userApiRepository.createProfileImage(
name: image.name,
data: await image.readAsBytes(),
);
} catch (e) {
_log.warning("Failed to upload profile image", e);
@ -71,13 +51,19 @@ class UserService {
}
Future<List<User>?> getUsersFromServer() async {
final List<User>? users = await _getAllUsers();
final List<User>? sharedBy =
await _partnerService.getPartners(PartnerDirection.by);
final List<User>? sharedWith =
await _partnerService.getPartners(PartnerDirection.with_);
List<User>? users;
try {
users = await _userApiRepository.getAll();
} catch (e) {
_log.warning("Failed to fetch users", e);
users = null;
}
final List<User> sharedBy =
await _partnerApiRepository.getAll(Direction.sharedByMe);
final List<User> sharedWith =
await _partnerApiRepository.getAll(Direction.sharedWithMe);
if (users == null || sharedBy == null || sharedWith == null) {
if (users == null) {
_log.warning("Failed to refresh users");
return null;
}

View file

@ -1,7 +1,7 @@
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:isar/isar.dart';
import 'package:openapi/api.dart';
String getThumbnailUrl(
@ -61,7 +61,7 @@ String getOriginalUrlForRemoteId(final String id) {
String getImageCacheKey(final Asset asset) {
// Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id
final isFromDto = asset.id == Isar.autoIncrement;
final isFromDto = asset.id == noDbId;
return '${isFromDto ? asset.remoteId : asset.id}_fullStage';
}

View file

@ -1,11 +1,11 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
import 'package:immich_mobile/utils/storage_indicator.dart';
import 'package:isar/isar.dart';
class ThumbnailImage extends ConsumerWidget {
/// The asset to show the thumbnail image for
@ -46,7 +46,7 @@ class ThumbnailImage extends ConsumerWidget {
? context.primaryColor.darken(amount: 0.6)
: context.primaryColor.lighten(amount: 0.8);
// Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id
final isFromDto = asset.id == Isar.autoIncrement;
final isFromDto = asset.id == noDbId;
Widget buildSelectionIcon(Asset asset) {
if (isSelected) {

View file

@ -3,23 +3,23 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/interfaces/person_api.interface.dart';
import 'package:immich_mobile/providers/search/people.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
class PeoplePicker extends HookConsumerWidget {
const PeoplePicker({super.key, required this.onSelect, this.filter});
final Function(Set<PersonResponseDto>) onSelect;
final Set<PersonResponseDto>? filter;
final Function(Set<Person>) onSelect;
final Set<Person>? filter;
@override
Widget build(BuildContext context, WidgetRef ref) {
var imageSize = 45.0;
final people = ref.watch(getAllPeopleProvider);
final headers = ApiService.getRequestHeaders();
final selectedPeople = useState<Set<PersonResponseDto>>(filter ?? {});
final selectedPeople = useState<Set<Person>>(filter ?? {});
return people.widgetWhen(
onData: (people) {