mirror of
https://github.com/immich-app/immich.git
synced 2025-01-28 06:32:44 +01:00
refactor(mobile): more repositories (#12879)
* ExifInfoRepository * ActivityApiRepository * initial AssetApiRepository
This commit is contained in:
parent
56f680ce04
commit
e0fa3cdbc7
20 changed files with 392 additions and 193 deletions
mobile
analysis_options.yaml
lib
interfaces
models/activities
providers
repositories
activity_api.repository.dartalbum_api.repository.dartasset.repository.dartasset_api.repository.dartbase_api.repository.dartexif_info.repository.dart
services
activity.service.dartasset.service.dartasset_description.service.dartbackup_verification.service.dart
widgets/asset_viewer
test/modules/activity
|
@ -64,7 +64,7 @@ custom_lint:
|
||||||
allowed:
|
allowed:
|
||||||
# required / wanted
|
# required / wanted
|
||||||
- lib/entities/*.entity.dart
|
- lib/entities/*.entity.dart
|
||||||
- lib/repositories/{album,asset,backup,user}.repository.dart
|
- lib/repositories/{album,asset,backup,exif_info,user}.repository.dart
|
||||||
# acceptable exceptions for the time being
|
# acceptable exceptions for the time being
|
||||||
- integration_test/test_utils/general_helper.dart
|
- integration_test/test_utils/general_helper.dart
|
||||||
- lib/main.dart
|
- lib/main.dart
|
||||||
|
@ -75,7 +75,7 @@ custom_lint:
|
||||||
- lib/pages/common/{album_asset_selection,gallery_viewer}.page.dart
|
- lib/pages/common/{album_asset_selection,gallery_viewer}.page.dart
|
||||||
- lib/providers/{archive,asset,authentication,db,favorite,partner,trash,user}.provider.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/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,asset_description,background,backup,backup_verification,hash,immich_logger,memory,partner,person,search,stack,sync,user}.service.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/widgets/asset_grid/{asset_grid_data_structure,thumbnail_image}.dart
|
||||||
|
|
||||||
- import_rule_openapi:
|
- import_rule_openapi:
|
||||||
|
@ -83,13 +83,12 @@ custom_lint:
|
||||||
restrict: package:openapi
|
restrict: package:openapi
|
||||||
allowed:
|
allowed:
|
||||||
# requried / wanted
|
# requried / wanted
|
||||||
- lib/repositories/album_api.repository.dart
|
- lib/repositories/*_api.repository.dart
|
||||||
# acceptable exceptions for the time being
|
# acceptable exceptions for the time being
|
||||||
- lib/entities/{album,asset,exif_info,user}.entity.dart # to convert DTOs to entities
|
- 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
|
- lib/utils/{image_url_builder,openapi_patching}.dart # utils are fine
|
||||||
- test/modules/utils/openapi_patching_test.dart # filename is self-explanatory...
|
- test/modules/utils/openapi_patching_test.dart # filename is self-explanatory...
|
||||||
# refactor
|
# refactor
|
||||||
- lib/models/activities/activity.model.dart
|
|
||||||
- lib/models/map/map_marker.model.dart
|
- lib/models/map/map_marker.model.dart
|
||||||
- lib/models/search/search_filter.model.dart
|
- lib/models/search/search_filter.model.dart
|
||||||
- lib/models/server_info/server_{config,disk_info,features,version}.model.dart
|
- lib/models/server_info/server_{config,disk_info,features,version}.model.dart
|
||||||
|
@ -102,7 +101,7 @@ custom_lint:
|
||||||
- lib/providers/search/{people,search,search_filter}.provider.dart
|
- lib/providers/search/{people,search,search_filter}.provider.dart
|
||||||
- lib/providers/websocket.provider.dart
|
- lib/providers/websocket.provider.dart
|
||||||
- lib/routing/auth_guard.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/services/{api,asset,backup,memory,oauth,partner,person,search,shared_link,stack,trash,user}.service.dart
|
||||||
- lib/widgets/album/album_thumbnail_listtile.dart
|
- lib/widgets/album/album_thumbnail_listtile.dart
|
||||||
- lib/widgets/forms/login/login_form.dart
|
- lib/widgets/forms/login/login_form.dart
|
||||||
- lib/widgets/search/search_filter/{camera_picker,location_picker,people_picker}.dart
|
- lib/widgets/search/search_filter/{camera_picker,location_picker,people_picker}.dart
|
||||||
|
|
16
mobile/lib/interfaces/activity_api.interface.dart
Normal file
16
mobile/lib/interfaces/activity_api.interface.dart
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import 'package:immich_mobile/models/activities/activity.model.dart';
|
||||||
|
|
||||||
|
abstract interface class IActivityApiRepository {
|
||||||
|
Future<List<Activity>> getAll(
|
||||||
|
String albumId, {
|
||||||
|
String? assetId,
|
||||||
|
});
|
||||||
|
Future<Activity> create(
|
||||||
|
String albumId,
|
||||||
|
ActivityType type, {
|
||||||
|
String? assetId,
|
||||||
|
String? comment,
|
||||||
|
});
|
||||||
|
Future<void> delete(String id);
|
||||||
|
Future<ActivityStats> getStats(String albumId, {String? assetId});
|
||||||
|
}
|
|
@ -7,4 +7,16 @@ abstract interface class IAssetRepository {
|
||||||
Future<List<Asset>> getAllByRemoteId(Iterable<String> ids);
|
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);
|
||||||
|
Future<List<Asset>> getAll({
|
||||||
|
required int ownerId,
|
||||||
|
bool? remote,
|
||||||
|
int limit = 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<List<Asset>> getMatches({
|
||||||
|
required List<Asset> assets,
|
||||||
|
required int ownerId,
|
||||||
|
bool? remote,
|
||||||
|
int limit = 100,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
16
mobile/lib/interfaces/asset_api.interface.dart
Normal file
16
mobile/lib/interfaces/asset_api.interface.dart
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
|
|
||||||
|
abstract interface class IAssetApiRepository {
|
||||||
|
// Future<Asset> get(String id);
|
||||||
|
|
||||||
|
// Future<List<Asset>> getAll();
|
||||||
|
|
||||||
|
// Future<Asset> create(Asset asset);
|
||||||
|
|
||||||
|
Future<Asset> update(
|
||||||
|
String id, {
|
||||||
|
String? description,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Future<void> delete(String id);
|
||||||
|
}
|
9
mobile/lib/interfaces/exif_info.interface.dart
Normal file
9
mobile/lib/interfaces/exif_info.interface.dart
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
||||||
|
|
||||||
|
abstract interface class IExifInfoRepository {
|
||||||
|
Future<ExifInfo?> get(int id);
|
||||||
|
|
||||||
|
Future<ExifInfo> update(ExifInfo exifInfo);
|
||||||
|
|
||||||
|
Future<void> delete(int id);
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
enum ActivityType { comment, like }
|
enum ActivityType { comment, like }
|
||||||
|
|
||||||
|
@ -38,16 +37,6 @@ class Activity {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Activity.fromDto(ActivityResponseDto dto)
|
|
||||||
: id = dto.id,
|
|
||||||
assetId = dto.assetId,
|
|
||||||
comment = dto.comment,
|
|
||||||
createdAt = dto.createdAt,
|
|
||||||
type = dto.type == ReactionType.comment
|
|
||||||
? ActivityType.comment
|
|
||||||
: ActivityType.like,
|
|
||||||
user = User.fromSimpleUserDto(dto.user);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'Activity(id: $id, assetId: $assetId, comment: $comment, createdAt: $createdAt, type: $type, user: $user)';
|
return 'Activity(id: $id, assetId: $assetId, comment: $comment, createdAt: $createdAt, type: $type, user: $user)';
|
||||||
|
@ -75,3 +64,9 @@ class Activity {
|
||||||
user.hashCode;
|
user.hashCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ActivityStats {
|
||||||
|
final int comments;
|
||||||
|
|
||||||
|
const ActivityStats({required this.comments});
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
|
import 'package:immich_mobile/repositories/activity_api.repository.dart';
|
||||||
import 'package:immich_mobile/services/activity.service.dart';
|
import 'package:immich_mobile/services/activity.service.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'activity_service.provider.g.dart';
|
part 'activity_service.provider.g.dart';
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
ActivityService activityService(ActivityServiceRef ref) =>
|
ActivityService activityService(ActivityServiceRef ref) =>
|
||||||
ActivityService(ref.watch(apiServiceProvider));
|
ActivityService(ref.watch(activityApiRepositoryProvider));
|
||||||
|
|
|
@ -11,7 +11,7 @@ class ActivityStatistics extends _$ActivityStatistics {
|
||||||
ref
|
ref
|
||||||
.watch(activityServiceProvider)
|
.watch(activityServiceProvider)
|
||||||
.getStatistics(albumId, assetId: assetId)
|
.getStatistics(albumId, assetId: assetId)
|
||||||
.then((comments) => state = comments);
|
.then((stats) => state = stats.comments);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
67
mobile/lib/repositories/activity_api.repository.dart
Normal file
67
mobile/lib/repositories/activity_api.repository.dart
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/activity_api.interface.dart';
|
||||||
|
import 'package:immich_mobile/models/activities/activity.model.dart';
|
||||||
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
|
import 'package:immich_mobile/repositories/base_api.repository.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
final activityApiRepositoryProvider = Provider(
|
||||||
|
(ref) => ActivityApiRepository(ref.watch(apiServiceProvider).activitiesApi),
|
||||||
|
);
|
||||||
|
|
||||||
|
class ActivityApiRepository extends BaseApiRepository
|
||||||
|
implements IActivityApiRepository {
|
||||||
|
final ActivitiesApi _api;
|
||||||
|
|
||||||
|
ActivityApiRepository(this._api);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Activity>> getAll(String albumId, {String? assetId}) async {
|
||||||
|
final response =
|
||||||
|
await checkNull(_api.getActivities(albumId, assetId: assetId));
|
||||||
|
return response.map(_toActivity).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Activity> create(
|
||||||
|
String albumId,
|
||||||
|
ActivityType type, {
|
||||||
|
String? assetId,
|
||||||
|
String? comment,
|
||||||
|
}) async {
|
||||||
|
final dto = ActivityCreateDto(
|
||||||
|
albumId: albumId,
|
||||||
|
type: type == ActivityType.comment
|
||||||
|
? ReactionType.comment
|
||||||
|
: ReactionType.like,
|
||||||
|
assetId: assetId,
|
||||||
|
comment: comment,
|
||||||
|
);
|
||||||
|
final response = await checkNull(_api.createActivity(dto));
|
||||||
|
return _toActivity(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> delete(String id) {
|
||||||
|
return checkNull(_api.deleteActivity(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ActivityStats> getStats(String albumId, {String? assetId}) async {
|
||||||
|
final response =
|
||||||
|
await checkNull(_api.getActivityStatistics(albumId, assetId: assetId));
|
||||||
|
return ActivityStats(comments: response.comments);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Activity _toActivity(ActivityResponseDto dto) => Activity(
|
||||||
|
id: dto.id,
|
||||||
|
createdAt: dto.createdAt,
|
||||||
|
type: dto.type == ReactionType.comment
|
||||||
|
? ActivityType.comment
|
||||||
|
: ActivityType.like,
|
||||||
|
user: User.fromSimpleUserDto(dto.user),
|
||||||
|
assetId: dto.assetId,
|
||||||
|
comment: dto.comment,
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,30 +1,31 @@
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
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/album.entity.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
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/interfaces/album_api.interface.dart';
|
import 'package:immich_mobile/interfaces/album_api.interface.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
|
import 'package:immich_mobile/repositories/base_api.repository.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
final albumApiRepositoryProvider = Provider(
|
final albumApiRepositoryProvider = Provider(
|
||||||
(ref) => AlbumApiRepository(ref.watch(apiServiceProvider).albumsApi),
|
(ref) => AlbumApiRepository(ref.watch(apiServiceProvider).albumsApi),
|
||||||
);
|
);
|
||||||
|
|
||||||
class AlbumApiRepository implements IAlbumApiRepository {
|
class AlbumApiRepository extends BaseApiRepository
|
||||||
|
implements IAlbumApiRepository {
|
||||||
final AlbumsApi _api;
|
final AlbumsApi _api;
|
||||||
|
|
||||||
AlbumApiRepository(this._api);
|
AlbumApiRepository(this._api);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Album> get(String id) async {
|
Future<Album> get(String id) async {
|
||||||
final dto = await _checkNull(_api.getAlbumInfo(id));
|
final dto = await checkNull(_api.getAlbumInfo(id));
|
||||||
return _toAlbum(dto);
|
return _toAlbum(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<Album>> getAll({bool? shared}) async {
|
Future<List<Album>> getAll({bool? shared}) async {
|
||||||
final dtos = await _checkNull(_api.getAllAlbums(shared: shared));
|
final dtos = await checkNull(_api.getAllAlbums(shared: shared));
|
||||||
return dtos.map(_toAlbum).toList().cast();
|
return dtos.map(_toAlbum).toList().cast();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +38,7 @@ class AlbumApiRepository implements IAlbumApiRepository {
|
||||||
final users = sharedUserIds.map(
|
final users = sharedUserIds.map(
|
||||||
(id) => AlbumUserCreateDto(userId: id, role: AlbumUserRole.editor),
|
(id) => AlbumUserCreateDto(userId: id, role: AlbumUserRole.editor),
|
||||||
);
|
);
|
||||||
final responseDto = await _checkNull(
|
final responseDto = await checkNull(
|
||||||
_api.createAlbum(
|
_api.createAlbum(
|
||||||
CreateAlbumDto(
|
CreateAlbumDto(
|
||||||
albumName: name,
|
albumName: name,
|
||||||
|
@ -57,7 +58,7 @@ class AlbumApiRepository implements IAlbumApiRepository {
|
||||||
String? description,
|
String? description,
|
||||||
bool? activityEnabled,
|
bool? activityEnabled,
|
||||||
}) async {
|
}) async {
|
||||||
final response = await _checkNull(
|
final response = await checkNull(
|
||||||
_api.updateAlbumInfo(
|
_api.updateAlbumInfo(
|
||||||
albumId,
|
albumId,
|
||||||
UpdateAlbumDto(
|
UpdateAlbumDto(
|
||||||
|
@ -81,7 +82,7 @@ class AlbumApiRepository implements IAlbumApiRepository {
|
||||||
String albumId,
|
String albumId,
|
||||||
Iterable<String> assetIds,
|
Iterable<String> assetIds,
|
||||||
) async {
|
) async {
|
||||||
final response = await _checkNull(
|
final response = await checkNull(
|
||||||
_api.addAssetsToAlbum(
|
_api.addAssetsToAlbum(
|
||||||
albumId,
|
albumId,
|
||||||
BulkIdsDto(ids: assetIds.toList()),
|
BulkIdsDto(ids: assetIds.toList()),
|
||||||
|
@ -106,7 +107,7 @@ class AlbumApiRepository implements IAlbumApiRepository {
|
||||||
String albumId,
|
String albumId,
|
||||||
Iterable<String> assetIds,
|
Iterable<String> assetIds,
|
||||||
) async {
|
) async {
|
||||||
final response = await _checkNull(
|
final response = await checkNull(
|
||||||
_api.removeAssetFromAlbum(
|
_api.removeAssetFromAlbum(
|
||||||
albumId,
|
albumId,
|
||||||
BulkIdsDto(ids: assetIds.toList()),
|
BulkIdsDto(ids: assetIds.toList()),
|
||||||
|
@ -127,7 +128,7 @@ class AlbumApiRepository implements IAlbumApiRepository {
|
||||||
Future<Album> addUsers(String albumId, Iterable<String> userIds) async {
|
Future<Album> addUsers(String albumId, Iterable<String> userIds) async {
|
||||||
final albumUsers =
|
final albumUsers =
|
||||||
userIds.map((userId) => AlbumUserAddDto(userId: userId)).toList();
|
userIds.map((userId) => AlbumUserAddDto(userId: userId)).toList();
|
||||||
final response = await _checkNull(
|
final response = await checkNull(
|
||||||
_api.addUsersToAlbum(
|
_api.addUsersToAlbum(
|
||||||
albumId,
|
albumId,
|
||||||
AddUsersDto(albumUsers: albumUsers),
|
AddUsersDto(albumUsers: albumUsers),
|
||||||
|
@ -141,12 +142,6 @@ class AlbumApiRepository implements IAlbumApiRepository {
|
||||||
return _api.removeUserFromAlbum(albumId, 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) {
|
static Album _toAlbum(AlbumResponseDto dto) {
|
||||||
final Album album = Album(
|
final Album album = Album(
|
||||||
remoteId: dto.id,
|
remoteId: dto.id,
|
||||||
|
|
|
@ -35,4 +35,84 @@ class AssetRepository implements IAssetRepository {
|
||||||
@override
|
@override
|
||||||
Future<List<Asset>> getAllByRemoteId(Iterable<String> ids) =>
|
Future<List<Asset>> getAllByRemoteId(Iterable<String> ids) =>
|
||||||
_db.assets.getAllByRemoteId(ids);
|
_db.assets.getAllByRemoteId(ids);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Asset>> getAll({
|
||||||
|
required int ownerId,
|
||||||
|
bool? remote,
|
||||||
|
int limit = 100,
|
||||||
|
}) {
|
||||||
|
if (remote == null) {
|
||||||
|
return _db.assets
|
||||||
|
.where()
|
||||||
|
.ownerIdEqualToAnyChecksum(ownerId)
|
||||||
|
.limit(limit)
|
||||||
|
.findAll();
|
||||||
|
}
|
||||||
|
final QueryBuilder<Asset, Asset, QAfterFilterCondition> query;
|
||||||
|
if (remote) {
|
||||||
|
query = _db.assets
|
||||||
|
.where()
|
||||||
|
.localIdIsNull()
|
||||||
|
.filter()
|
||||||
|
.remoteIdIsNotNull()
|
||||||
|
.ownerIdEqualTo(ownerId);
|
||||||
|
} else {
|
||||||
|
query = _db.assets
|
||||||
|
.where()
|
||||||
|
.remoteIdIsNull()
|
||||||
|
.filter()
|
||||||
|
.localIdIsNotNull()
|
||||||
|
.ownerIdEqualTo(ownerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.limit(limit).findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Asset>> getMatches({
|
||||||
|
required List<Asset> assets,
|
||||||
|
required int ownerId,
|
||||||
|
bool? remote,
|
||||||
|
int limit = 100,
|
||||||
|
}) {
|
||||||
|
final QueryBuilder<Asset, Asset, QAfterFilterCondition> query;
|
||||||
|
if (remote == null) {
|
||||||
|
query = _db.assets.filter().remoteIdIsNotNull().or().localIdIsNotNull();
|
||||||
|
} else if (remote) {
|
||||||
|
query = _db.assets.where().localIdIsNull().filter().remoteIdIsNotNull();
|
||||||
|
} else {
|
||||||
|
query = _db.assets.where().remoteIdIsNull().filter().localIdIsNotNull();
|
||||||
|
}
|
||||||
|
return _getMatchesImpl(query, ownerId, assets, limit);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<Asset>> _getMatchesImpl(
|
||||||
|
QueryBuilder<Asset, Asset, QAfterFilterCondition> query,
|
||||||
|
int ownerId,
|
||||||
|
List<Asset> assets,
|
||||||
|
int limit,
|
||||||
|
) =>
|
||||||
|
query
|
||||||
|
.ownerIdEqualTo(ownerId)
|
||||||
|
.anyOf(
|
||||||
|
assets,
|
||||||
|
(q, Asset a) => q
|
||||||
|
.fileNameEqualTo(a.fileName)
|
||||||
|
.and()
|
||||||
|
.durationInSecondsEqualTo(a.durationInSeconds)
|
||||||
|
.and()
|
||||||
|
.fileCreatedAtBetween(
|
||||||
|
a.fileCreatedAt.subtract(const Duration(hours: 12)),
|
||||||
|
a.fileCreatedAt.add(const Duration(hours: 12)),
|
||||||
|
)
|
||||||
|
.and()
|
||||||
|
.not()
|
||||||
|
.checksumEqualTo(a.checksum),
|
||||||
|
)
|
||||||
|
.sortByFileName()
|
||||||
|
.thenByFileCreatedAt()
|
||||||
|
.thenByFileModifiedAt()
|
||||||
|
.limit(limit)
|
||||||
|
.findAll();
|
||||||
|
|
25
mobile/lib/repositories/asset_api.repository.dart
Normal file
25
mobile/lib/repositories/asset_api.repository.dart
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/asset_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 assetApiRepositoryProvider = Provider(
|
||||||
|
(ref) => AssetApiRepository(ref.watch(apiServiceProvider).assetsApi),
|
||||||
|
);
|
||||||
|
|
||||||
|
class AssetApiRepository extends BaseApiRepository
|
||||||
|
implements IAssetApiRepository {
|
||||||
|
final AssetsApi _api;
|
||||||
|
|
||||||
|
AssetApiRepository(this._api);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Asset> update(String id, {String? description}) async {
|
||||||
|
final response = await checkNull(
|
||||||
|
_api.updateAsset(id, UpdateAssetDto(description: description)),
|
||||||
|
);
|
||||||
|
return Asset.remote(response);
|
||||||
|
}
|
||||||
|
}
|
11
mobile/lib/repositories/base_api.repository.dart
Normal file
11
mobile/lib/repositories/base_api.repository.dart
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:immich_mobile/constants/errors.dart';
|
||||||
|
|
||||||
|
abstract class BaseApiRepository {
|
||||||
|
@protected
|
||||||
|
Future<T> checkNull<T>(Future<T?> future) async {
|
||||||
|
final response = await future;
|
||||||
|
if (response == null) throw NoResponseDtoError();
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
28
mobile/lib/repositories/exif_info.repository.dart
Normal file
28
mobile/lib/repositories/exif_info.repository.dart
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/exif_info.interface.dart';
|
||||||
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
|
final exifInfoRepositoryProvider =
|
||||||
|
Provider((ref) => ExifInfoRepository(ref.watch(dbProvider)));
|
||||||
|
|
||||||
|
class ExifInfoRepository implements IExifInfoRepository {
|
||||||
|
final Isar _db;
|
||||||
|
|
||||||
|
ExifInfoRepository(
|
||||||
|
this._db,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> delete(int id) => _db.exifInfos.delete(id);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ExifInfo?> get(int id) => _db.exifInfos.get(id);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ExifInfo> update(ExifInfo exifInfo) async {
|
||||||
|
await _db.writeTxn(() => _db.exifInfos.put(exifInfo));
|
||||||
|
return exifInfo;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,41 +1,31 @@
|
||||||
import 'package:immich_mobile/constants/errors.dart';
|
import 'package:immich_mobile/interfaces/activity_api.interface.dart';
|
||||||
import 'package:immich_mobile/mixins/error_logger.mixin.dart';
|
import 'package:immich_mobile/mixins/error_logger.mixin.dart';
|
||||||
import 'package:immich_mobile/models/activities/activity.model.dart';
|
import 'package:immich_mobile/models/activities/activity.model.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
|
|
||||||
class ActivityService with ErrorLoggerMixin {
|
class ActivityService with ErrorLoggerMixin {
|
||||||
final ApiService _apiService;
|
final IActivityApiRepository _activityApiRepository;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final Logger logger = Logger("ActivityService");
|
final Logger logger = Logger("ActivityService");
|
||||||
|
|
||||||
ActivityService(this._apiService);
|
ActivityService(this._activityApiRepository);
|
||||||
|
|
||||||
Future<List<Activity>> getAllActivities(
|
Future<List<Activity>> getAllActivities(
|
||||||
String albumId, {
|
String albumId, {
|
||||||
String? assetId,
|
String? assetId,
|
||||||
}) async {
|
}) async {
|
||||||
return logError(
|
return logError(
|
||||||
() async {
|
() => _activityApiRepository.getAll(albumId, assetId: assetId),
|
||||||
final list = await _apiService.activitiesApi
|
|
||||||
.getActivities(albumId, assetId: assetId);
|
|
||||||
return list != null ? list.map(Activity.fromDto).toList() : [];
|
|
||||||
},
|
|
||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
errorMessage: "Failed to get all activities for album $albumId",
|
errorMessage: "Failed to get all activities for album $albumId",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> getStatistics(String albumId, {String? assetId}) async {
|
Future<ActivityStats> getStatistics(String albumId, {String? assetId}) async {
|
||||||
return logError(
|
return logError(
|
||||||
() async {
|
() => _activityApiRepository.getStats(albumId, assetId: assetId),
|
||||||
final dto = await _apiService.activitiesApi
|
defaultValue: const ActivityStats(comments: 0),
|
||||||
.getActivityStatistics(albumId, assetId: assetId);
|
|
||||||
return dto?.comments ?? 0;
|
|
||||||
},
|
|
||||||
defaultValue: 0,
|
|
||||||
errorMessage: "Failed to statistics for album $albumId",
|
errorMessage: "Failed to statistics for album $albumId",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -43,7 +33,7 @@ class ActivityService with ErrorLoggerMixin {
|
||||||
Future<bool> removeActivity(String id) async {
|
Future<bool> removeActivity(String id) async {
|
||||||
return logError(
|
return logError(
|
||||||
() async {
|
() async {
|
||||||
await _apiService.activitiesApi.deleteActivity(id);
|
await _activityApiRepository.delete(id);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
|
@ -58,22 +48,12 @@ class ActivityService with ErrorLoggerMixin {
|
||||||
String? comment,
|
String? comment,
|
||||||
}) async {
|
}) async {
|
||||||
return guardError(
|
return guardError(
|
||||||
() async {
|
() => _activityApiRepository.create(
|
||||||
final dto = await _apiService.activitiesApi.createActivity(
|
albumId,
|
||||||
ActivityCreateDto(
|
type,
|
||||||
albumId: albumId,
|
assetId: assetId,
|
||||||
type: type == ActivityType.comment
|
comment: comment,
|
||||||
? ReactionType.comment
|
),
|
||||||
: ReactionType.like,
|
|
||||||
assetId: assetId,
|
|
||||||
comment: comment,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (dto != null) {
|
|
||||||
return Activity.fromDto(dto);
|
|
||||||
}
|
|
||||||
throw NoResponseDtoError();
|
|
||||||
},
|
|
||||||
errorMessage: "Failed to create $type for album $albumId",
|
errorMessage: "Failed to create $type for album $albumId",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,13 @@ import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/entities/etag.entity.dart';
|
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/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/asset_api.interface.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/exif_info.interface.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/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
|
import 'package:immich_mobile/repositories/asset_api.repository.dart';
|
||||||
|
import 'package:immich_mobile/repositories/exif_info.repository.dart';
|
||||||
import 'package:immich_mobile/services/album.service.dart';
|
import 'package:immich_mobile/services/album.service.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/services/backup.service.dart';
|
import 'package:immich_mobile/services/backup.service.dart';
|
||||||
|
@ -24,6 +28,8 @@ import 'package:openapi/api.dart';
|
||||||
|
|
||||||
final assetServiceProvider = Provider(
|
final assetServiceProvider = Provider(
|
||||||
(ref) => AssetService(
|
(ref) => AssetService(
|
||||||
|
ref.watch(assetApiRepositoryProvider),
|
||||||
|
ref.watch(exifInfoRepositoryProvider),
|
||||||
ref.watch(apiServiceProvider),
|
ref.watch(apiServiceProvider),
|
||||||
ref.watch(syncServiceProvider),
|
ref.watch(syncServiceProvider),
|
||||||
ref.watch(userServiceProvider),
|
ref.watch(userServiceProvider),
|
||||||
|
@ -34,6 +40,8 @@ final assetServiceProvider = Provider(
|
||||||
);
|
);
|
||||||
|
|
||||||
class AssetService {
|
class AssetService {
|
||||||
|
final IAssetApiRepository _assetApiRepository;
|
||||||
|
final IExifInfoRepository _exifInfoRepository;
|
||||||
final ApiService _apiService;
|
final ApiService _apiService;
|
||||||
final SyncService _syncService;
|
final SyncService _syncService;
|
||||||
final UserService _userService;
|
final UserService _userService;
|
||||||
|
@ -43,6 +51,8 @@ class AssetService {
|
||||||
final Isar _db;
|
final Isar _db;
|
||||||
|
|
||||||
AssetService(
|
AssetService(
|
||||||
|
this._assetApiRepository,
|
||||||
|
this._exifInfoRepository,
|
||||||
this._apiService,
|
this._apiService,
|
||||||
this._syncService,
|
this._syncService,
|
||||||
this._userService,
|
this._userService,
|
||||||
|
@ -342,4 +352,46 @@ class AssetService {
|
||||||
log.severe("Error while syncing uploaded asset to albums", error, stack);
|
log.severe("Error while syncing uploaded asset to albums", error, stack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setDescription(
|
||||||
|
Asset asset,
|
||||||
|
String newDescription,
|
||||||
|
) async {
|
||||||
|
final remoteAssetId = asset.remoteId;
|
||||||
|
final localExifId = asset.exifInfo?.id;
|
||||||
|
|
||||||
|
// Guard [remoteAssetId] and [localExifId] null
|
||||||
|
if (remoteAssetId == null || localExifId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await _assetApiRepository.update(
|
||||||
|
remoteAssetId,
|
||||||
|
description: newDescription,
|
||||||
|
);
|
||||||
|
|
||||||
|
final description = result.exifInfo?.description;
|
||||||
|
|
||||||
|
if (description != null) {
|
||||||
|
var exifInfo = await _exifInfoRepository.get(localExifId);
|
||||||
|
|
||||||
|
if (exifInfo != null) {
|
||||||
|
exifInfo.description = description;
|
||||||
|
await _exifInfoRepository.update(exifInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> getDescription(Asset asset) async {
|
||||||
|
final localExifId = asset.exifInfo?.id;
|
||||||
|
|
||||||
|
// Guard [remoteAssetId] and [localExifId] null
|
||||||
|
if (localExifId == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
final exifInfo = await _exifInfoRepository.get(localExifId);
|
||||||
|
|
||||||
|
return exifInfo?.description ?? "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,66 +0,0 @@
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
|
||||||
import 'package:immich_mobile/entities/exif_info.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:openapi/api.dart';
|
|
||||||
|
|
||||||
class AssetDescriptionService {
|
|
||||||
AssetDescriptionService(this._db, this._api);
|
|
||||||
|
|
||||||
final Isar _db;
|
|
||||||
final ApiService _api;
|
|
||||||
|
|
||||||
Future<void> setDescription(
|
|
||||||
Asset asset,
|
|
||||||
String newDescription,
|
|
||||||
) async {
|
|
||||||
final remoteAssetId = asset.remoteId;
|
|
||||||
final localExifId = asset.exifInfo?.id;
|
|
||||||
|
|
||||||
// Guard [remoteAssetId] and [localExifId] null
|
|
||||||
if (remoteAssetId == null || localExifId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final result = await _api.assetsApi.updateAsset(
|
|
||||||
remoteAssetId,
|
|
||||||
UpdateAssetDto(description: newDescription),
|
|
||||||
);
|
|
||||||
|
|
||||||
final description = result?.exifInfo?.description;
|
|
||||||
|
|
||||||
if (description != null) {
|
|
||||||
var exifInfo = await _db.exifInfos.get(localExifId);
|
|
||||||
|
|
||||||
if (exifInfo != null) {
|
|
||||||
exifInfo.description = description;
|
|
||||||
await _db.writeTxn(
|
|
||||||
() => _db.exifInfos.put(exifInfo),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String getAssetDescription(Asset asset) {
|
|
||||||
final localExifId = asset.exifInfo?.id;
|
|
||||||
|
|
||||||
// Guard [remoteAssetId] and [localExifId] null
|
|
||||||
if (localExifId == null) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
final exifInfo = _db.exifInfos.getSync(localExifId);
|
|
||||||
|
|
||||||
return exifInfo?.description ?? "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final assetDescriptionServiceProvider = Provider(
|
|
||||||
(ref) => AssetDescriptionService(
|
|
||||||
ref.watch(dbProvider),
|
|
||||||
ref.watch(apiServiceProvider),
|
|
||||||
),
|
|
||||||
);
|
|
|
@ -8,41 +8,46 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.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/interfaces/asset.interface.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/exif_info.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/file_media.interface.dart';
|
import 'package:immich_mobile/interfaces/file_media.interface.dart';
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/repositories/asset.repository.dart';
|
||||||
|
import 'package:immich_mobile/repositories/exif_info.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/utils/diff.dart';
|
import 'package:immich_mobile/utils/diff.dart';
|
||||||
import 'package:isar/isar.dart';
|
|
||||||
|
|
||||||
/// Finds duplicates originating from missing EXIF information
|
/// Finds duplicates originating from missing EXIF information
|
||||||
class BackupVerificationService {
|
class BackupVerificationService {
|
||||||
final Isar _db;
|
|
||||||
final IFileMediaRepository _fileMediaRepository;
|
final IFileMediaRepository _fileMediaRepository;
|
||||||
|
final IAssetRepository _assetRepository;
|
||||||
|
final IExifInfoRepository _exifInfoRepository;
|
||||||
|
|
||||||
BackupVerificationService(this._db, this._fileMediaRepository);
|
BackupVerificationService(
|
||||||
|
this._fileMediaRepository,
|
||||||
|
this._assetRepository,
|
||||||
|
this._exifInfoRepository,
|
||||||
|
);
|
||||||
|
|
||||||
/// Returns at most [limit] assets that were backed up without exif
|
/// Returns at most [limit] assets that were backed up without exif
|
||||||
Future<List<Asset>> findWronglyBackedUpAssets({int limit = 100}) async {
|
Future<List<Asset>> findWronglyBackedUpAssets({int limit = 100}) async {
|
||||||
final owner = Store.get(StoreKey.currentUser).isarId;
|
final owner = Store.get(StoreKey.currentUser).isarId;
|
||||||
final List<Asset> onlyLocal = await _db.assets
|
final List<Asset> onlyLocal = await _assetRepository.getAll(
|
||||||
.where()
|
ownerId: owner,
|
||||||
.remoteIdIsNull()
|
remote: false,
|
||||||
.filter()
|
limit: limit,
|
||||||
.ownerIdEqualTo(owner)
|
|
||||||
.localIdIsNotNull()
|
|
||||||
.findAll();
|
|
||||||
final List<Asset> remoteMatches = await _getMatches(
|
|
||||||
_db.assets.where().localIdIsNull().filter().remoteIdIsNotNull(),
|
|
||||||
owner,
|
|
||||||
onlyLocal,
|
|
||||||
limit,
|
|
||||||
);
|
);
|
||||||
final List<Asset> localMatches = await _getMatches(
|
final List<Asset> remoteMatches = await _assetRepository.getMatches(
|
||||||
_db.assets.where().remoteIdIsNull().filter().localIdIsNotNull(),
|
assets: onlyLocal,
|
||||||
owner,
|
ownerId: owner,
|
||||||
remoteMatches,
|
remote: true,
|
||||||
limit,
|
limit: limit,
|
||||||
|
);
|
||||||
|
final List<Asset> localMatches = await _assetRepository.getMatches(
|
||||||
|
assets: remoteMatches,
|
||||||
|
ownerId: owner,
|
||||||
|
remote: false,
|
||||||
|
limit: limit,
|
||||||
);
|
);
|
||||||
|
|
||||||
final List<Asset> deleteCandidates = [], originals = [];
|
final List<Asset> deleteCandidates = [], originals = [];
|
||||||
|
@ -52,7 +57,7 @@ class BackupVerificationService {
|
||||||
localMatches,
|
localMatches,
|
||||||
compare: (a, b) => a.fileName.compareTo(b.fileName),
|
compare: (a, b) => a.fileName.compareTo(b.fileName),
|
||||||
both: (a, b) async {
|
both: (a, b) async {
|
||||||
a.exifInfo = await _db.exifInfos.get(a.id);
|
a.exifInfo = await _exifInfoRepository.get(a.id);
|
||||||
deleteCandidates.add(a);
|
deleteCandidates.add(a);
|
||||||
originals.add(b);
|
originals.add(b);
|
||||||
return false;
|
return false;
|
||||||
|
@ -192,35 +197,6 @@ class BackupVerificationService {
|
||||||
return bytes.buffer.asUint64List(start);
|
return bytes.buffer.asUint64List(start);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<List<Asset>> _getMatches(
|
|
||||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> query,
|
|
||||||
int ownerId,
|
|
||||||
List<Asset> assets,
|
|
||||||
int limit,
|
|
||||||
) =>
|
|
||||||
query
|
|
||||||
.ownerIdEqualTo(ownerId)
|
|
||||||
.anyOf(
|
|
||||||
assets,
|
|
||||||
(q, Asset a) => q
|
|
||||||
.fileNameEqualTo(a.fileName)
|
|
||||||
.and()
|
|
||||||
.durationInSecondsEqualTo(a.durationInSeconds)
|
|
||||||
.and()
|
|
||||||
.fileCreatedAtBetween(
|
|
||||||
a.fileCreatedAt.subtract(const Duration(hours: 12)),
|
|
||||||
a.fileCreatedAt.add(const Duration(hours: 12)),
|
|
||||||
)
|
|
||||||
.and()
|
|
||||||
.not()
|
|
||||||
.checksumEqualTo(a.checksum),
|
|
||||||
)
|
|
||||||
.sortByFileName()
|
|
||||||
.thenByFileCreatedAt()
|
|
||||||
.thenByFileModifiedAt()
|
|
||||||
.limit(limit)
|
|
||||||
.findAll();
|
|
||||||
|
|
||||||
static bool _sameExceptTimeZone(DateTime a, DateTime b) {
|
static bool _sameExceptTimeZone(DateTime a, DateTime b) {
|
||||||
final ms = a.isAfter(b)
|
final ms = a.isAfter(b)
|
||||||
? a.millisecondsSinceEpoch - b.millisecondsSinceEpoch
|
? a.millisecondsSinceEpoch - b.millisecondsSinceEpoch
|
||||||
|
@ -233,7 +209,8 @@ class BackupVerificationService {
|
||||||
|
|
||||||
final backupVerificationServiceProvider = Provider(
|
final backupVerificationServiceProvider = Provider(
|
||||||
(ref) => BackupVerificationService(
|
(ref) => BackupVerificationService(
|
||||||
ref.watch(dbProvider),
|
|
||||||
ref.watch(fileMediaRepositoryProvider),
|
ref.watch(fileMediaRepositoryProvider),
|
||||||
|
ref.watch(assetRepositoryProvider),
|
||||||
|
ref.watch(exifInfoRepositoryProvider),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,7 +8,7 @@ import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/services/asset_description.service.dart';
|
import 'package:immich_mobile/services/asset.service.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
@ -29,14 +29,16 @@ class DescriptionInput extends HookConsumerWidget {
|
||||||
final focusNode = useFocusNode();
|
final focusNode = useFocusNode();
|
||||||
final isFocus = useState(false);
|
final isFocus = useState(false);
|
||||||
final isTextEmpty = useState(controller.text.isEmpty);
|
final isTextEmpty = useState(controller.text.isEmpty);
|
||||||
final descriptionProvider = ref.watch(assetDescriptionServiceProvider);
|
final assetService = ref.watch(assetServiceProvider);
|
||||||
final owner = ref.watch(currentUserProvider);
|
final owner = ref.watch(currentUserProvider);
|
||||||
final hasError = useState(false);
|
final hasError = useState(false);
|
||||||
final assetWithExif = ref.watch(assetDetailProvider(asset));
|
final assetWithExif = ref.watch(assetDetailProvider(asset));
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
controller.text = descriptionProvider.getAssetDescription(asset);
|
assetService
|
||||||
|
.getDescription(asset)
|
||||||
|
.then((value) => controller.text = value);
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
[assetWithExif.value],
|
[assetWithExif.value],
|
||||||
|
@ -45,7 +47,7 @@ class DescriptionInput extends HookConsumerWidget {
|
||||||
submitDescription(String description) async {
|
submitDescription(String description) async {
|
||||||
hasError.value = false;
|
hasError.value = false;
|
||||||
try {
|
try {
|
||||||
await descriptionProvider.setDescription(
|
await assetService.setDescription(
|
||||||
asset,
|
asset,
|
||||||
description,
|
description,
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/models/activities/activity.model.dart';
|
||||||
import 'package:immich_mobile/providers/activity_service.provider.dart';
|
import 'package:immich_mobile/providers/activity_service.provider.dart';
|
||||||
import 'package:immich_mobile/providers/activity_statistics.provider.dart';
|
import 'package:immich_mobile/providers/activity_statistics.provider.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
@ -25,7 +26,7 @@ void main() {
|
||||||
test('Returns the proper count family', () async {
|
test('Returns the proper count family', () async {
|
||||||
when(
|
when(
|
||||||
() => activityMock.getStatistics('test-album', assetId: 'test-asset'),
|
() => activityMock.getStatistics('test-album', assetId: 'test-asset'),
|
||||||
).thenAnswer((_) async => 5);
|
).thenAnswer((_) async => const ActivityStats(comments: 5));
|
||||||
|
|
||||||
// Read here to make the getStatistics call
|
// Read here to make the getStatistics call
|
||||||
container.read(activityStatisticsProvider('test-album', 'test-asset'));
|
container.read(activityStatisticsProvider('test-album', 'test-asset'));
|
||||||
|
@ -50,7 +51,7 @@ void main() {
|
||||||
test('Adds activity', () async {
|
test('Adds activity', () async {
|
||||||
when(
|
when(
|
||||||
() => activityMock.getStatistics('test-album'),
|
() => activityMock.getStatistics('test-album'),
|
||||||
).thenAnswer((_) async => 10);
|
).thenAnswer((_) async => const ActivityStats(comments: 10));
|
||||||
|
|
||||||
final provider = activityStatisticsProvider('test-album');
|
final provider = activityStatisticsProvider('test-album');
|
||||||
container.listen(
|
container.listen(
|
||||||
|
@ -71,7 +72,7 @@ void main() {
|
||||||
test('Removes activity', () async {
|
test('Removes activity', () async {
|
||||||
when(
|
when(
|
||||||
() => activityMock.getStatistics('new-album', assetId: 'test-asset'),
|
() => activityMock.getStatistics('new-album', assetId: 'test-asset'),
|
||||||
).thenAnswer((_) async => 10);
|
).thenAnswer((_) async => const ActivityStats(comments: 10));
|
||||||
|
|
||||||
final provider = activityStatisticsProvider('new-album', 'test-asset');
|
final provider = activityStatisticsProvider('new-album', 'test-asset');
|
||||||
container.listen(
|
container.listen(
|
||||||
|
|
Loading…
Reference in a new issue