mirror of
https://github.com/immich-app/immich.git
synced 2026-07-28 14:47:30 -07:00
refactor: asset update method (#30201)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
co-authored by
shenlong-tanwen
parent
bc6bf388c0
commit
04a38ba91c
@@ -60,6 +60,7 @@ sealed class BaseAsset {
|
|||||||
bool get hasLocal => storage == AssetState.local || storage == AssetState.merged;
|
bool get hasLocal => storage == AssetState.local || storage == AssetState.merged;
|
||||||
bool get isLocalOnly => storage == AssetState.local;
|
bool get isLocalOnly => storage == AssetState.local;
|
||||||
bool get isRemoteOnly => storage == AssetState.remote;
|
bool get isRemoteOnly => storage == AssetState.remote;
|
||||||
|
bool get isMerged => storage == .merged;
|
||||||
|
|
||||||
// Same asset even if localId is known on one side but not the other (heroTag isn't stable then)
|
// Same asset even if localId is known on one side but not the other (heroTag isn't stable then)
|
||||||
bool refersToSameAsset(BaseAsset other) {
|
bool refersToSameAsset(BaseAsset other) {
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ class RemoteAsset extends BaseAsset {
|
|||||||
|
|
||||||
bool get isArchived => visibility == .archive;
|
bool get isArchived => visibility == .archive;
|
||||||
|
|
||||||
|
bool get isLocked => visibility == .locked;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return '''Asset {
|
return '''Asset {
|
||||||
|
|||||||
@@ -1,16 +1,26 @@
|
|||||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/remote_exif.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/asset_api.repository.dart';
|
import 'package:immich_mobile/repositories/asset_api.repository.dart';
|
||||||
|
import 'package:immich_mobile/utils/option.dart';
|
||||||
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
class AssetService {
|
class AssetService {
|
||||||
final RemoteAssetRepository _remoteRepository;
|
final RemoteAssetRepository _remoteRepository;
|
||||||
|
final RemoteExifRepository _exifRepository;
|
||||||
final DriftLocalAssetRepository _localRepository;
|
final DriftLocalAssetRepository _localRepository;
|
||||||
final AssetApiRepository _apiRepository;
|
final AssetApiRepository _apiRepository;
|
||||||
|
|
||||||
const AssetService({required this._remoteRepository, required this._localRepository, required this._apiRepository});
|
const AssetService({
|
||||||
|
required this._remoteRepository,
|
||||||
|
required this._exifRepository,
|
||||||
|
required this._localRepository,
|
||||||
|
required this._apiRepository,
|
||||||
|
});
|
||||||
|
|
||||||
Future<BaseAsset?> getAsset(BaseAsset asset) {
|
Future<BaseAsset?> getAsset(BaseAsset asset) {
|
||||||
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).id;
|
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).id;
|
||||||
@@ -26,10 +36,6 @@ class AssetService {
|
|||||||
return _localRepository.getByChecksum(checksum);
|
return _localRepository.getByChecksum(checksum);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<LocalAsset?> getLocalAsset(String id) {
|
|
||||||
return _localRepository.get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<RemoteAsset?> getRemoteAssetByChecksum(String checksum) {
|
Future<RemoteAsset?> getRemoteAssetByChecksum(String checksum) {
|
||||||
return _remoteRepository.getByChecksum(checksum);
|
return _remoteRepository.getByChecksum(checksum);
|
||||||
}
|
}
|
||||||
@@ -73,6 +79,99 @@ class AssetService {
|
|||||||
return _localRepository.getSourceAlbums(localAssetId, backupSelection: backupSelection);
|
return _localRepository.getSourceAlbums(localAssetId, backupSelection: backupSelection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> restoreTrash(List<String> remoteIds) async {
|
||||||
|
if (remoteIds.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _apiRepository.restoreTrash(remoteIds);
|
||||||
|
await _remoteRepository.restoreTrash(remoteIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> stack(String userId, List<String> remoteIds) async {
|
||||||
|
if (remoteIds.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final stack = await _apiRepository.stack(remoteIds);
|
||||||
|
await _remoteRepository.stack(userId, stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> unstack(List<String> stackIds) async {
|
||||||
|
if (stackIds.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _remoteRepository.unStack(stackIds);
|
||||||
|
await _apiRepository.unStack(stackIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> update(
|
||||||
|
List<String> remoteIds, {
|
||||||
|
Option<bool> isFavorite = const .none(),
|
||||||
|
Option<AssetVisibility> visibility = const .none(),
|
||||||
|
Option<LatLng> location = const .none(),
|
||||||
|
Option<String> dateTime = const .none(),
|
||||||
|
}) async {
|
||||||
|
if (remoteIds.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final parsedDateTime = dateTime.map((dt) => DateTime.parse(dt));
|
||||||
|
final offset = RegExp(r'[+-]\d{2}:\d{2}$').firstMatch(dateTime.unwrapOrNull ?? '')?.group(0);
|
||||||
|
|
||||||
|
await _apiRepository.update(
|
||||||
|
remoteIds,
|
||||||
|
isFavorite: isFavorite,
|
||||||
|
visibility: visibility,
|
||||||
|
location: location,
|
||||||
|
dateTimeOriginal: dateTime,
|
||||||
|
);
|
||||||
|
await _remoteRepository.update(
|
||||||
|
remoteIds,
|
||||||
|
isFavorite: isFavorite,
|
||||||
|
visibility: visibility,
|
||||||
|
createdAt: parsedDateTime,
|
||||||
|
);
|
||||||
|
await _exifRepository.update(
|
||||||
|
remoteIds,
|
||||||
|
location: location,
|
||||||
|
dateTimeOriginal: parsedDateTime,
|
||||||
|
timeZone: .fromNullable(offset).map((o) => 'UTC$o'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> trash(List<String> remoteIds) async {
|
||||||
|
if (remoteIds.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _apiRepository.delete(remoteIds, false);
|
||||||
|
await _remoteRepository.trash(remoteIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> delete(List<String> remoteIds) async {
|
||||||
|
if (remoteIds.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _apiRepository.delete(remoteIds, true);
|
||||||
|
await _remoteRepository.delete(remoteIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> applyEdits(String remoteId, List<AssetEdit> edits) async {
|
||||||
|
if (edits.isEmpty) {
|
||||||
|
await _apiRepository.removeEdits(remoteId);
|
||||||
|
} else {
|
||||||
|
await _apiRepository.editAsset(remoteId, edits);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(shenlong): remove after action migration
|
||||||
|
Future<LocalAsset?> getLocalAsset(String id) {
|
||||||
|
return _localRepository.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> updateFavorite(List<String> remoteIds, bool isFavorite) async {
|
Future<void> updateFavorite(List<String> remoteIds, bool isFavorite) async {
|
||||||
if (remoteIds.isEmpty) {
|
if (remoteIds.isEmpty) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -341,4 +341,10 @@ class RemoteAlbumService {
|
|||||||
|
|
||||||
return sortedAlbums;
|
return sortedAlbums;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<int> removeAssets({required String albumId, required List<String> assetIds}) async {
|
||||||
|
final result = await _albumApiRepository.removeAssets(albumId, assetIds);
|
||||||
|
await _repository.removeAssets(albumId, result.removed);
|
||||||
|
return result.removed.length;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,13 +72,7 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final query = _db.remoteAssetEntity.select()
|
final query = _db.remoteAssetEntity.select()
|
||||||
..where(
|
..where((row) => row.stackId.equals(stackId) & row.id.equals(asset.id).not())
|
||||||
(row) =>
|
|
||||||
row.stackId.equals(stackId) &
|
|
||||||
row.id.equals(asset.id).not() &
|
|
||||||
row.deletedAt.isNull() &
|
|
||||||
row.visibility.equalsValue(AssetVisibility.timeline),
|
|
||||||
)
|
|
||||||
..orderBy([(row) => OrderingTerm.desc(row.createdAt)]);
|
..orderBy([(row) => OrderingTerm.desc(row.createdAt)]);
|
||||||
|
|
||||||
return query.map((row) => row.toDto()).get();
|
return query.map((row) => row.toDto()).get();
|
||||||
@@ -189,38 +183,6 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateLocation(List<String> ids, LatLng location) {
|
|
||||||
return _db.batch((batch) async {
|
|
||||||
for (final id in ids) {
|
|
||||||
batch.update(
|
|
||||||
_db.remoteExifEntity,
|
|
||||||
RemoteExifEntityCompanion(latitude: Value(location.latitude), longitude: Value(location.longitude)),
|
|
||||||
where: (e) => e.assetId.equals(id),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> updateDateTime(List<String> ids, DateTime dateTime, {String? timeZone}) {
|
|
||||||
return _db.batch((batch) async {
|
|
||||||
for (final id in ids) {
|
|
||||||
batch.update(
|
|
||||||
_db.remoteExifEntity,
|
|
||||||
RemoteExifEntityCompanion(
|
|
||||||
dateTimeOriginal: Value(dateTime),
|
|
||||||
timeZone: timeZone == null ? const Value.absent() : Value(timeZone),
|
|
||||||
),
|
|
||||||
where: (e) => e.assetId.equals(id),
|
|
||||||
);
|
|
||||||
batch.update(
|
|
||||||
_db.remoteAssetEntity,
|
|
||||||
RemoteAssetEntityCompanion(createdAt: Value(dateTime)),
|
|
||||||
where: (e) => e.id.equals(id),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> stack(String userId, StackResponse stack) {
|
Future<void> stack(String userId, StackResponse stack) {
|
||||||
return _db.transaction(() async {
|
return _db.transaction(() async {
|
||||||
final stackIds = await _db.managers.stackEntity
|
final stackIds = await _db.managers.stackEntity
|
||||||
@@ -298,10 +260,16 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
|||||||
List<String> remoteIds, {
|
List<String> remoteIds, {
|
||||||
Option<bool> isFavorite = const .none(),
|
Option<bool> isFavorite = const .none(),
|
||||||
Option<AssetVisibility> visibility = const .none(),
|
Option<AssetVisibility> visibility = const .none(),
|
||||||
}) {
|
Option<DateTime> createdAt = const .none(),
|
||||||
|
}) async {
|
||||||
|
if ([isFavorite, visibility, createdAt].every((option) => option.isNone)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final companion = RemoteAssetEntityCompanion(
|
final companion = RemoteAssetEntityCompanion(
|
||||||
visibility: visibility.toDriftValue(),
|
visibility: visibility.toDriftValue(),
|
||||||
isFavorite: isFavorite.toDriftValue(),
|
isFavorite: isFavorite.toDriftValue(),
|
||||||
|
createdAt: createdAt.toDriftValue(),
|
||||||
);
|
);
|
||||||
return _db.batch((batch) {
|
return _db.batch((batch) {
|
||||||
for (final remoteId in remoteIds) {
|
for (final remoteId in remoteIds) {
|
||||||
@@ -309,4 +277,37 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(shenlong): remove after action migration
|
||||||
|
Future<void> updateLocation(List<String> ids, LatLng location) {
|
||||||
|
return _db.batch((batch) async {
|
||||||
|
for (final id in ids) {
|
||||||
|
batch.update(
|
||||||
|
_db.remoteExifEntity,
|
||||||
|
RemoteExifEntityCompanion(latitude: Value(location.latitude), longitude: Value(location.longitude)),
|
||||||
|
where: (e) => e.assetId.equals(id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateDateTime(List<String> ids, DateTime dateTime, {String? timeZone}) {
|
||||||
|
return _db.batch((batch) async {
|
||||||
|
for (final id in ids) {
|
||||||
|
batch.update(
|
||||||
|
_db.remoteExifEntity,
|
||||||
|
RemoteExifEntityCompanion(
|
||||||
|
dateTimeOriginal: Value(dateTime),
|
||||||
|
timeZone: timeZone == null ? const Value.absent() : Value(timeZone),
|
||||||
|
),
|
||||||
|
where: (e) => e.assetId.equals(id),
|
||||||
|
);
|
||||||
|
batch.update(
|
||||||
|
_db.remoteAssetEntity,
|
||||||
|
RemoteAssetEntityCompanion(createdAt: Value(dateTime)),
|
||||||
|
where: (e) => e.id.equals(id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
import 'package:immich_mobile/utils/option.dart';
|
||||||
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
|
class RemoteExifRepository extends DriftDatabaseRepository {
|
||||||
|
final Drift _db;
|
||||||
|
|
||||||
|
const RemoteExifRepository(this._db) : super(_db);
|
||||||
|
|
||||||
|
Future<void> update(
|
||||||
|
List<String> ids, {
|
||||||
|
Option<DateTime> dateTimeOriginal = const .none(),
|
||||||
|
Option<String> timeZone = const .none(),
|
||||||
|
Option<LatLng> location = const .none(),
|
||||||
|
}) async {
|
||||||
|
if ([dateTimeOriginal, timeZone, location].every((option) => option.isNone)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final companion = RemoteExifEntityCompanion(
|
||||||
|
dateTimeOriginal: dateTimeOriginal.toDriftValue(),
|
||||||
|
timeZone: timeZone.toDriftValue(),
|
||||||
|
latitude: location.map((loc) => loc.latitude).toDriftValue(),
|
||||||
|
longitude: location.map((loc) => loc.longitude).toDriftValue(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return _db.batch((batch) {
|
||||||
|
for (final id in ids) {
|
||||||
|
batch.update(_db.remoteExifEntity, companion, where: (a) => a.assetId.equals(id));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ class _ActionWidget extends ConsumerWidget {
|
|||||||
try {
|
try {
|
||||||
await action.onAction(scope);
|
await action.onAction(scope);
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
handleError(scope.context, error, stack: stackTrace, description: 'Action failed: ${action.runtimeType}');
|
handleError(scope.context, stack: stackTrace, description: 'Action failed: ${action.runtimeType}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/domain/services/asset.service.dart';
|
import 'package:immich_mobile/domain/services/asset.service.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/remote_exif.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
@@ -15,6 +16,8 @@ final remoteAssetRepositoryProvider = Provider<RemoteAssetRepository>(
|
|||||||
(ref) => RemoteAssetRepository(ref.watch(driftProvider)),
|
(ref) => RemoteAssetRepository(ref.watch(driftProvider)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final remoteExifRepositoryProvider = Provider((ref) => RemoteExifRepository(ref.watch(driftProvider)));
|
||||||
|
|
||||||
final trashedLocalAssetRepository = Provider<DriftTrashedLocalAssetRepository>(
|
final trashedLocalAssetRepository = Provider<DriftTrashedLocalAssetRepository>(
|
||||||
(ref) => DriftTrashedLocalAssetRepository(ref.watch(driftProvider)),
|
(ref) => DriftTrashedLocalAssetRepository(ref.watch(driftProvider)),
|
||||||
);
|
);
|
||||||
@@ -22,6 +25,7 @@ final trashedLocalAssetRepository = Provider<DriftTrashedLocalAssetRepository>(
|
|||||||
final assetServiceProvider = Provider(
|
final assetServiceProvider = Provider(
|
||||||
(ref) => AssetService(
|
(ref) => AssetService(
|
||||||
remoteRepository: ref.watch(remoteAssetRepositoryProvider),
|
remoteRepository: ref.watch(remoteAssetRepositoryProvider),
|
||||||
|
exifRepository: ref.watch(remoteExifRepositoryProvider),
|
||||||
localRepository: ref.watch(localAssetRepository),
|
localRepository: ref.watch(localAssetRepository),
|
||||||
apiRepository: ref.watch(assetApiRepositoryProvider),
|
apiRepository: ref.watch(assetApiRepositoryProvider),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -43,28 +43,11 @@ class AssetApiRepository extends ApiRepository {
|
|||||||
return response?.count ?? 0;
|
return response?.count ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(shenlong): remove after action migration
|
||||||
Future<void> updateVisibility(List<String> ids, AssetVisibility visibility) async {
|
Future<void> updateVisibility(List<String> ids, AssetVisibility visibility) async {
|
||||||
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, visibility: Optional.present(_mapVisibility(visibility))));
|
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, visibility: Optional.present(_mapVisibility(visibility))));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateFavorite(List<String> ids, bool isFavorite) async {
|
|
||||||
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, isFavorite: Optional.present(isFavorite)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> updateLocation(List<String> ids, LatLng location) async {
|
|
||||||
return _api.updateAssets(
|
|
||||||
AssetBulkUpdateDto(
|
|
||||||
ids: ids,
|
|
||||||
latitude: Optional.present(location.latitude),
|
|
||||||
longitude: Optional.present(location.longitude),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> updateDateTime(List<String> ids, String dateTime) async {
|
|
||||||
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, dateTimeOriginal: Optional.present(dateTime)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<StackResponse> stack(List<String> ids) async {
|
Future<StackResponse> stack(List<String> ids) async {
|
||||||
final responseDto = await checkNull(_stacksApi.createStack(StackCreateDto(assetIds: ids)));
|
final responseDto = await checkNull(_stacksApi.createStack(StackCreateDto(assetIds: ids)));
|
||||||
|
|
||||||
@@ -113,15 +96,39 @@ class AssetApiRepository extends ApiRepository {
|
|||||||
List<String> remoteIds, {
|
List<String> remoteIds, {
|
||||||
Option<bool> isFavorite = const .none(),
|
Option<bool> isFavorite = const .none(),
|
||||||
Option<AssetVisibility> visibility = const .none(),
|
Option<AssetVisibility> visibility = const .none(),
|
||||||
|
Option<String> dateTimeOriginal = const .none(),
|
||||||
|
Option<LatLng> location = const .none(),
|
||||||
}) {
|
}) {
|
||||||
return _api.updateAssets(
|
return _api.updateAssets(
|
||||||
AssetBulkUpdateDto(
|
AssetBulkUpdateDto(
|
||||||
ids: remoteIds,
|
ids: remoteIds,
|
||||||
isFavorite: isFavorite.toOptional(),
|
isFavorite: isFavorite.toOptional(),
|
||||||
visibility: visibility.map(_mapVisibility).toOptional(),
|
visibility: visibility.map(_mapVisibility).toOptional(),
|
||||||
|
dateTimeOriginal: dateTimeOriginal.toOptional(),
|
||||||
|
latitude: location.map((loc) => loc.latitude).toOptional(),
|
||||||
|
longitude: location.map((loc) => loc.longitude).toOptional(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(shenlong): remove after action migration
|
||||||
|
Future<void> updateFavorite(List<String> ids, bool isFavorite) async {
|
||||||
|
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, isFavorite: Optional.present(isFavorite)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateLocation(List<String> ids, LatLng location) async {
|
||||||
|
return _api.updateAssets(
|
||||||
|
AssetBulkUpdateDto(
|
||||||
|
ids: ids,
|
||||||
|
latitude: Optional.present(location.latitude),
|
||||||
|
longitude: Optional.present(location.longitude),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateDateTime(List<String> ids, String dateTime) async {
|
||||||
|
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, dateTimeOriginal: Optional.present(dateTime)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on StackResponseDto {
|
extension on StackResponseDto {
|
||||||
|
|||||||
@@ -15,8 +15,10 @@ extension type const AssetFilter<T extends BaseAsset>(Iterable<T> assets) implem
|
|||||||
remote().where((asset) => asset.visibility != visibility);
|
remote().where((asset) => asset.visibility != visibility);
|
||||||
AssetFilter<RemoteAsset> archived({bool isArchived = true}) =>
|
AssetFilter<RemoteAsset> archived({bool isArchived = true}) =>
|
||||||
remote().where((asset) => asset.isArchived == isArchived);
|
remote().where((asset) => asset.isArchived == isArchived);
|
||||||
|
AssetFilter<RemoteAsset> locked({bool isLocked = true}) => remote().where((asset) => asset.isLocked == isLocked);
|
||||||
AssetFilter<RemoteAsset> stacked({bool isStacked = true}) => remote().where((asset) => asset.isStacked == isStacked);
|
AssetFilter<RemoteAsset> stacked({bool isStacked = true}) => remote().where((asset) => asset.isStacked == isStacked);
|
||||||
|
AssetFilter<RemoteAsset> trashed({bool isTrashed = true}) => remote().where((asset) => asset.isTrashed == isTrashed);
|
||||||
|
|
||||||
AssetFilter<LocalAsset> local() => AssetFilter(assets.whereType<LocalAsset>());
|
AssetFilter<LocalAsset> local() => AssetFilter(assets.whereType<LocalAsset>());
|
||||||
AssetFilter<LocalAsset> backedUp() => local().where((asset) => asset.remoteAssetId != null);
|
AssetFilter<BaseAsset> backedUp({bool isBackedUp = true}) => where((asset) => asset.isMerged == isBackedUp);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import 'package:openapi/api.dart';
|
|||||||
// ignore: depend_on_referenced_packages
|
// ignore: depend_on_referenced_packages
|
||||||
import 'package:stack_trace/stack_trace.dart';
|
import 'package:stack_trace/stack_trace.dart';
|
||||||
|
|
||||||
void handleError(BuildContext context, Object error, {StackTrace? stack, String? description}) {
|
void handleError(Object error, {StackTrace? stack, String? description}) {
|
||||||
String? stackTrace;
|
String? stackTrace;
|
||||||
if (stack != null) {
|
if (stack != null) {
|
||||||
final trace = Trace.from(stack);
|
final trace = Trace.from(stack);
|
||||||
@@ -23,17 +23,13 @@ void handleError(BuildContext context, Object error, {StackTrace? stack, String?
|
|||||||
() => 'Error${description != null ? ' ($description)' : ''}: $error${stackTrace != null ? '\n$stackTrace' : ''}',
|
() => 'Error${description != null ? ' ($description)' : ''}: $error${stackTrace != null ? '\n$stackTrace' : ''}',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!context.mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String message;
|
final String message;
|
||||||
if (serverErrorMessage(error) case String serverMessage) {
|
if (serverErrorMessage(error) case String serverMessage) {
|
||||||
message = serverMessage;
|
message = serverMessage;
|
||||||
} else if (isConnectionError(error)) {
|
} else if (isConnectionError(error)) {
|
||||||
message = context.t.login_form_server_error;
|
message = StaticTranslations.instance.login_form_server_error;
|
||||||
} else {
|
} else {
|
||||||
message = context.t.scaffold_body_error_occurred;
|
message = StaticTranslations.instance.scaffold_body_error_occurred;
|
||||||
}
|
}
|
||||||
|
|
||||||
snackbar.error(message);
|
snackbar.error(message);
|
||||||
|
|||||||
@@ -32,17 +32,6 @@ sealed class Option<T> {
|
|||||||
None() => onNone(),
|
None() => onNone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Option<U> flatMap<U>(Option<U> Function(T value) f) => switch (this) {
|
|
||||||
Some(:final value) => f(value),
|
|
||||||
None() => const Option.none(),
|
|
||||||
};
|
|
||||||
|
|
||||||
void ifPresent(void Function(T value) f) {
|
|
||||||
if (this case Some(:final value)) {
|
|
||||||
f(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => switch (this) {
|
String toString() => switch (this) {
|
||||||
Some(:final value) => 'Some($value)',
|
Some(:final value) => 'Some($value)',
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import 'package:immich_mobile/domain/services/asset.service.dart';
|
|
||||||
import 'package:immich_mobile/domain/services/partner.service.dart';
|
|
||||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
|
||||||
import 'package:immich_mobile/domain/services/user.service.dart';
|
|
||||||
import 'package:immich_mobile/domain/utils/background_sync.dart';
|
|
||||||
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
|
||||||
import 'package:mocktail/mocktail.dart';
|
|
||||||
|
|
||||||
class MockStoreService extends Mock implements StoreService {}
|
|
||||||
|
|
||||||
class MockBackgroundSyncManager extends Mock implements BackgroundSyncManager {}
|
|
||||||
|
|
||||||
class MockNativeSyncApi extends Mock implements NativeSyncApi {}
|
|
||||||
|
|
||||||
class MockAppSettingsService extends Mock implements AppSettingsService {}
|
|
||||||
|
|
||||||
class MockPartnerService extends Mock implements PartnerService {}
|
|
||||||
|
|
||||||
class MockAssetService extends Mock implements AssetService {}
|
|
||||||
|
|
||||||
class MockUserService extends Mock implements UserService {}
|
|
||||||
@@ -16,10 +16,10 @@ import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
|||||||
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
import '../../domain/service.mock.dart';
|
|
||||||
import '../../fixtures/asset.stub.dart';
|
import '../../fixtures/asset.stub.dart';
|
||||||
import '../../infrastructure/repository.mock.dart';
|
import '../../infrastructure/repository.mock.dart';
|
||||||
import '../../repository.mocks.dart';
|
import '../../repository.mocks.dart';
|
||||||
|
import '../../service.mocks.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
late LocalSyncService sut;
|
late LocalSyncService sut;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import 'package:immich_mobile/providers/infrastructure/store.provider.dart';
|
|||||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||||
|
|
||||||
import '../../infrastructure/repository.mock.dart';
|
import '../../infrastructure/repository.mock.dart';
|
||||||
import '../service.mock.dart';
|
import '../../service.mocks.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
// A container with the service's deps overridden but cancellationProvider left
|
// A container with the service's deps overridden but cancellationProvider left
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import 'package:mocktail/mocktail.dart';
|
|||||||
|
|
||||||
import '../../fixtures/user.stub.dart';
|
import '../../fixtures/user.stub.dart';
|
||||||
import '../../infrastructure/repository.mock.dart';
|
import '../../infrastructure/repository.mock.dart';
|
||||||
import '../service.mock.dart';
|
import '../../service.mocks.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
late UserService sut;
|
late UserService sut;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'
|
|||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
|
||||||
import '../../../fixtures/asset.stub.dart';
|
import '../../../fixtures/asset.stub.dart';
|
||||||
|
import '../../../unit/presentation/presentation_context.dart';
|
||||||
|
|
||||||
class _SeededAssetViewerNotifier extends AssetViewerStateNotifier {
|
class _SeededAssetViewerNotifier extends AssetViewerStateNotifier {
|
||||||
@override
|
@override
|
||||||
@@ -32,6 +33,12 @@ TimelineService _stubTimelineService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
late PresentationContext context;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
context = await PresentationContext.create();
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('status bar icons are light while the asset viewer is open in light mode', (tester) async {
|
testWidgets('status bar icons are light while the asset viewer is open in light mode', (tester) async {
|
||||||
// Emulate arriving from a light-themed page whose AppBar set dark status
|
// Emulate arriving from a light-themed page whose AppBar set dark status
|
||||||
// bar icons (the state the viewer is opened from in light mode).
|
// bar icons (the state the viewer is opened from in light mode).
|
||||||
@@ -48,6 +55,7 @@ void main() {
|
|||||||
assetLoader: const CodegenLoader(),
|
assetLoader: const CodegenLoader(),
|
||||||
child: ProviderScope(
|
child: ProviderScope(
|
||||||
overrides: [
|
overrides: [
|
||||||
|
...context.overrides,
|
||||||
timelineServiceProvider.overrideWithValue(_stubTimelineService()),
|
timelineServiceProvider.overrideWithValue(_stubTimelineService()),
|
||||||
assetViewerProvider.overrideWith(_SeededAssetViewerNotifier.new),
|
assetViewerProvider.overrideWith(_SeededAssetViewerNotifier.new),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'
|
|||||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
import '../../domain/service.mock.dart';
|
import '../../service.mocks.dart';
|
||||||
import '../../unit/factories/remote_asset_factory.dart';
|
import '../../unit/factories/remote_asset_factory.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import 'package:immich_mobile/domain/services/tag.service.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/remote_exif.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/asset_api.repository.dart';
|
import 'package:immich_mobile/repositories/asset_api.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/auth.repository.dart';
|
import 'package:immich_mobile/repositories/auth.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/auth_api.repository.dart';
|
import 'package:immich_mobile/repositories/auth_api.repository.dart';
|
||||||
import 'package:immich_mobile/domain/services/tag.service.dart';
|
import 'package:immich_mobile/repositories/download.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/permission.repository.dart';
|
import 'package:immich_mobile/repositories/permission.repository.dart';
|
||||||
|
import 'package:immich_mobile/repositories/toast.repository.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
class MockAssetApiRepository extends Mock implements AssetApiRepository {}
|
class MockAssetApiRepository extends Mock implements AssetApiRepository {}
|
||||||
@@ -17,3 +20,9 @@ class MockAuthApiRepository extends Mock implements AuthApiRepository {}
|
|||||||
class MockAuthRepository extends Mock implements AuthRepository {}
|
class MockAuthRepository extends Mock implements AuthRepository {}
|
||||||
|
|
||||||
class MockTagService extends Mock implements TagService {}
|
class MockTagService extends Mock implements TagService {}
|
||||||
|
|
||||||
|
class MockDownloadRepository extends Mock implements DownloadRepository {}
|
||||||
|
|
||||||
|
class MockRemoteExifRepository extends Mock implements RemoteExifRepository {}
|
||||||
|
|
||||||
|
class MockToastRepository extends Mock implements ToastRepository {}
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
|
import 'package:immich_mobile/domain/services/asset.service.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/partner.service.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/remote_album.service.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/user.service.dart';
|
||||||
|
import 'package:immich_mobile/domain/utils/background_sync.dart';
|
||||||
|
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
|
import 'package:immich_mobile/services/cleanup.service.dart';
|
||||||
|
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
||||||
|
import 'package:immich_mobile/services/gcast.service.dart';
|
||||||
import 'package:immich_mobile/services/network.service.dart';
|
import 'package:immich_mobile/services/network.service.dart';
|
||||||
|
import 'package:immich_mobile/services/server_info.service.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
class MockApiService extends Mock implements ApiService {}
|
class MockApiService extends Mock implements ApiService {}
|
||||||
@@ -8,3 +19,27 @@ class MockApiService extends Mock implements ApiService {}
|
|||||||
class MockNetworkService extends Mock implements NetworkService {}
|
class MockNetworkService extends Mock implements NetworkService {}
|
||||||
|
|
||||||
class MockAppSettingService extends Mock implements AppSettingsService {}
|
class MockAppSettingService extends Mock implements AppSettingsService {}
|
||||||
|
|
||||||
|
class MockStoreService extends Mock implements StoreService {}
|
||||||
|
|
||||||
|
class MockNativeSyncApi extends Mock implements NativeSyncApi {}
|
||||||
|
|
||||||
|
class MockAppSettingsService extends Mock implements AppSettingsService {}
|
||||||
|
|
||||||
|
class MockPartnerService extends Mock implements PartnerService {}
|
||||||
|
|
||||||
|
class MockAssetService extends Mock implements AssetService {}
|
||||||
|
|
||||||
|
class MockUserService extends Mock implements UserService {}
|
||||||
|
|
||||||
|
class MockRemoteAlbumService extends Mock implements RemoteAlbumService {}
|
||||||
|
|
||||||
|
class MockGCastService extends Mock implements GCastService {}
|
||||||
|
|
||||||
|
class MockForegroundUploadService extends Mock implements ForegroundUploadService {}
|
||||||
|
|
||||||
|
class MockServerInfoService extends Mock implements ServerInfoService {}
|
||||||
|
|
||||||
|
class MockCleanupService extends Mock implements CleanupService {}
|
||||||
|
|
||||||
|
class MockBackgroundSyncManager extends Mock implements BackgroundSyncManager {}
|
||||||
|
|||||||
@@ -7,15 +7,12 @@ import 'package:immich_mobile/domain/services/store.service.dart';
|
|||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/download.repository.dart';
|
|
||||||
import 'package:immich_mobile/services/action.service.dart';
|
import 'package:immich_mobile/services/action.service.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
import '../infrastructure/repository.mock.dart';
|
import '../infrastructure/repository.mock.dart';
|
||||||
import '../repository.mocks.dart';
|
import '../repository.mocks.dart';
|
||||||
|
|
||||||
class MockDownloadRepository extends Mock implements DownloadRepository {}
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
late ActionService sut;
|
late ActionService sut;
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import 'package:immich_mobile/services/auth.service.dart';
|
|||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
import '../domain/service.mock.dart';
|
|
||||||
import '../repository.mocks.dart';
|
import '../repository.mocks.dart';
|
||||||
import '../service.mocks.dart';
|
import '../service.mocks.dart';
|
||||||
|
|
||||||
@@ -29,13 +28,7 @@ void main() {
|
|||||||
apiService = MockApiService();
|
apiService = MockApiService();
|
||||||
networkService = MockNetworkService();
|
networkService = MockNetworkService();
|
||||||
backgroundSyncManager = MockBackgroundSyncManager();
|
backgroundSyncManager = MockBackgroundSyncManager();
|
||||||
sut = AuthService(
|
sut = AuthService(authApiRepository, authRepository, apiService, networkService, backgroundSyncManager);
|
||||||
authApiRepository,
|
|
||||||
authRepository,
|
|
||||||
apiService,
|
|
||||||
networkService,
|
|
||||||
backgroundSyncManager,
|
|
||||||
);
|
|
||||||
|
|
||||||
registerFallbackValue(Uri());
|
registerFallbackValue(Uri());
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ import '../../utils.dart';
|
|||||||
class LocalAssetFactory {
|
class LocalAssetFactory {
|
||||||
const LocalAssetFactory();
|
const LocalAssetFactory();
|
||||||
|
|
||||||
static LocalAsset create({String? id, String? name}) {
|
static LocalAsset create({String? id, String? name, String? remoteId}) {
|
||||||
id = TestUtils.uuid(id);
|
id = TestUtils.uuid(id);
|
||||||
|
|
||||||
return LocalAsset(
|
return LocalAsset(
|
||||||
id: id,
|
id: id,
|
||||||
name: name ?? 'local_$id.jpg',
|
name: name ?? 'local_$id.jpg',
|
||||||
|
remoteId: remoteId,
|
||||||
type: AssetType.image,
|
type: AssetType.image,
|
||||||
createdAt: TestUtils.yesterday(),
|
createdAt: TestUtils.yesterday(),
|
||||||
updatedAt: TestUtils.now(),
|
updatedAt: TestUtils.now(),
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
|
|
||||||
|
import '../../utils.dart';
|
||||||
|
|
||||||
|
class RemoteAlbumFactory {
|
||||||
|
const RemoteAlbumFactory();
|
||||||
|
|
||||||
|
static RemoteAlbum create({
|
||||||
|
String? id,
|
||||||
|
String? name,
|
||||||
|
String? ownerId,
|
||||||
|
String? description,
|
||||||
|
DateTime? createdAt,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
String? thumbnailAssetId,
|
||||||
|
bool isActivityEnabled = false,
|
||||||
|
AlbumAssetOrder order = AlbumAssetOrder.desc,
|
||||||
|
int assetCount = 0,
|
||||||
|
String? ownerName,
|
||||||
|
bool isShared = false,
|
||||||
|
}) {
|
||||||
|
id = TestUtils.uuid(id);
|
||||||
|
return RemoteAlbum(
|
||||||
|
id: id,
|
||||||
|
name: name ?? 'remote_album_$id',
|
||||||
|
ownerId: TestUtils.uuid(ownerId),
|
||||||
|
description: description ?? '',
|
||||||
|
createdAt: TestUtils.date(createdAt),
|
||||||
|
updatedAt: TestUtils.date(updatedAt),
|
||||||
|
thumbnailAssetId: thumbnailAssetId,
|
||||||
|
isActivityEnabled: isActivityEnabled,
|
||||||
|
order: order,
|
||||||
|
assetCount: assetCount,
|
||||||
|
ownerName: ownerName ?? 'owner_$id',
|
||||||
|
isShared: isShared,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,8 +10,11 @@ class RemoteAssetFactory {
|
|||||||
String? name,
|
String? name,
|
||||||
String? ownerId,
|
String? ownerId,
|
||||||
bool isFavorite = false,
|
bool isFavorite = false,
|
||||||
AssetVisibility visibility = AssetVisibility.timeline,
|
AssetVisibility visibility = .timeline,
|
||||||
|
AssetType type = .image,
|
||||||
String? stackId,
|
String? stackId,
|
||||||
|
DateTime? deletedAt,
|
||||||
|
String? localId,
|
||||||
}) {
|
}) {
|
||||||
id = TestUtils.uuid(id);
|
id = TestUtils.uuid(id);
|
||||||
|
|
||||||
@@ -20,13 +23,15 @@ class RemoteAssetFactory {
|
|||||||
name: name ?? 'remote_$id.jpg',
|
name: name ?? 'remote_$id.jpg',
|
||||||
ownerId: TestUtils.uuid(ownerId),
|
ownerId: TestUtils.uuid(ownerId),
|
||||||
checksum: 'checksum-$id',
|
checksum: 'checksum-$id',
|
||||||
type: .image,
|
type: type,
|
||||||
createdAt: TestUtils.yesterday(),
|
createdAt: TestUtils.yesterday(),
|
||||||
updatedAt: TestUtils.now(),
|
updatedAt: TestUtils.now(),
|
||||||
isFavorite: isFavorite,
|
isFavorite: isFavorite,
|
||||||
visibility: visibility,
|
visibility: visibility,
|
||||||
stackId: stackId,
|
stackId: stackId,
|
||||||
isEdited: false,
|
isEdited: false,
|
||||||
|
deletedAt: deletedAt,
|
||||||
|
localId: localId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+233
-4
@@ -1,25 +1,43 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/tag.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
||||||
|
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
||||||
|
import 'package:immich_mobile/utils/option.dart';
|
||||||
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
import 'package:mocktail/mocktail.dart' as mock;
|
import 'package:mocktail/mocktail.dart' as mock;
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
import '../domain/service.mock.dart';
|
|
||||||
import '../infrastructure/repository.mock.dart';
|
import '../infrastructure/repository.mock.dart';
|
||||||
|
import '../repository.mocks.dart';
|
||||||
|
import '../service.mocks.dart';
|
||||||
import 'factories/local_album_factory.dart';
|
import 'factories/local_album_factory.dart';
|
||||||
import 'factories/local_asset_factory.dart';
|
import 'factories/local_asset_factory.dart';
|
||||||
|
import 'factories/remote_album_factory.dart';
|
||||||
import 'factories/user_factory.dart';
|
import 'factories/user_factory.dart';
|
||||||
|
|
||||||
class RepositoryMocks {
|
class RepositoryMocks {
|
||||||
final localAlbum = LocalAlbumRepositoryStub(MockLocalAlbumRepository());
|
final localAlbum = LocalAlbumRepositoryStub(MockLocalAlbumRepository());
|
||||||
final localAsset = LocalAssetRepositoryStub(MockDriftLocalAssetRepository());
|
final localAsset = LocalAssetRepositoryStub(MockDriftLocalAssetRepository());
|
||||||
|
final remoteAsset = RemoteAssetRepositoryStub(MockRemoteAssetRepository());
|
||||||
|
final remoteExif = RemoteExifRepositoryStub(MockRemoteExifRepository());
|
||||||
final trashedAsset = MockTrashedLocalAssetRepository();
|
final trashedAsset = MockTrashedLocalAssetRepository();
|
||||||
|
final toast = MockToastRepository();
|
||||||
|
final remoteAlbum = MockRemoteAlbumRepository();
|
||||||
|
final albumApi = MockDriftAlbumApiRepository();
|
||||||
|
|
||||||
final nativeApi = NativeSyncApiStub(MockNativeSyncApi());
|
final nativeApi = NativeSyncApiStub(MockNativeSyncApi());
|
||||||
|
final assetApi = AssetApiRepositoryStub(MockAssetApiRepository());
|
||||||
|
final assetMedia = AssetMediaRepositoryStub(MockAssetMediaRepository());
|
||||||
|
final download = DownloadRepositoryStub(MockDownloadRepository());
|
||||||
|
|
||||||
RepositoryMocks() {
|
RepositoryMocks() {
|
||||||
resetAll();
|
resetAll();
|
||||||
@@ -29,11 +47,34 @@ class RepositoryMocks {
|
|||||||
_registerFallbacks();
|
_registerFallbacks();
|
||||||
localAlbum.reset();
|
localAlbum.reset();
|
||||||
localAsset.reset();
|
localAsset.reset();
|
||||||
|
remoteAsset.reset();
|
||||||
|
remoteExif.reset();
|
||||||
reset(trashedAsset);
|
reset(trashedAsset);
|
||||||
|
reset(remoteAlbum);
|
||||||
|
reset(albumApi);
|
||||||
nativeApi.reset();
|
nativeApi.reset();
|
||||||
|
assetApi.reset();
|
||||||
|
assetMedia.reset();
|
||||||
|
download.reset();
|
||||||
|
reset(toast);
|
||||||
_stubLocalAlbumRepository();
|
_stubLocalAlbumRepository();
|
||||||
_stubLocalAssetRepository();
|
_stubLocalAssetRepository();
|
||||||
|
_stubRemoteAssetRepository();
|
||||||
|
_stubRemoteExifRepository();
|
||||||
_stubNativeSyncApi();
|
_stubNativeSyncApi();
|
||||||
|
_stubAssetApiRepository();
|
||||||
|
_stubAssetMediaRepository();
|
||||||
|
_stubDownloadRepository();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _stubRemoteAssetRepository() {
|
||||||
|
when(remoteAsset.getExif).thenAnswer((_) async => null);
|
||||||
|
when(remoteAsset.getAssetEdits).thenAnswer((_) async => const []);
|
||||||
|
when(remoteAsset.update).thenAnswer((_) async {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _stubRemoteExifRepository() {
|
||||||
|
when(remoteExif.update).thenAnswer((_) async {});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _stubLocalAlbumRepository() {
|
void _stubLocalAlbumRepository() {
|
||||||
@@ -49,12 +90,31 @@ class RepositoryMocks {
|
|||||||
void _stubNativeSyncApi() {
|
void _stubNativeSyncApi() {
|
||||||
when(nativeApi.hashAssets).thenAnswer((_) async => []);
|
when(nativeApi.hashAssets).thenAnswer((_) async => []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _stubAssetApiRepository() {
|
||||||
|
when(assetApi.update).thenAnswer((_) async => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _stubAssetMediaRepository() {
|
||||||
|
when(assetMedia.shareAssets).thenAnswer((_) async => 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _stubDownloadRepository() {
|
||||||
|
when(download.downloadAllAssets).thenAnswer((_) async => const []);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ServiceMocks {
|
class ServiceMocks {
|
||||||
final partner = PartnerServiceStub(MockPartnerService());
|
final partner = PartnerServiceStub(MockPartnerService());
|
||||||
final user = UserServiceStub(MockUserService());
|
final user = UserServiceStub(MockUserService());
|
||||||
final asset = AssetServiceStub(MockAssetService());
|
final asset = AssetServiceStub(MockAssetService());
|
||||||
|
final album = RemoteAlbumServiceStub(MockRemoteAlbumService());
|
||||||
|
final cleanup = CleanupServiceStub(MockCleanupService());
|
||||||
|
final tag = TagServiceStub(MockTagService());
|
||||||
|
final backgroundSync = MockBackgroundSyncManager();
|
||||||
|
final upload = MockForegroundUploadService();
|
||||||
|
final cast = MockGCastService();
|
||||||
|
final serverInfo = MockServerInfoService();
|
||||||
|
|
||||||
ServiceMocks() {
|
ServiceMocks() {
|
||||||
resetAll();
|
resetAll();
|
||||||
@@ -65,9 +125,21 @@ class ServiceMocks {
|
|||||||
partner.reset();
|
partner.reset();
|
||||||
user.reset();
|
user.reset();
|
||||||
asset.reset();
|
asset.reset();
|
||||||
|
album.reset();
|
||||||
|
cleanup.reset();
|
||||||
|
tag.reset();
|
||||||
|
reset(cast);
|
||||||
|
reset(serverInfo);
|
||||||
|
reset(backgroundSync);
|
||||||
|
reset(upload);
|
||||||
_stubUserService();
|
_stubUserService();
|
||||||
_stubPartnerService();
|
_stubPartnerService();
|
||||||
_stubAssetService();
|
_stubAssetService();
|
||||||
|
_stubRemoteAlbumService();
|
||||||
|
_stubCleanupService();
|
||||||
|
_stubTagService();
|
||||||
|
_stubBackgroundSync();
|
||||||
|
_stubForegroundUpload();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _stubUserService() {
|
void _stubUserService() {
|
||||||
@@ -88,7 +160,43 @@ class ServiceMocks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _stubAssetService() {
|
void _stubAssetService() {
|
||||||
when(asset.updateFavorite).thenAnswer((_) async {});
|
when(asset.update).thenAnswer((_) async {});
|
||||||
|
when(asset.stack).thenAnswer((_) async {});
|
||||||
|
when(asset.unstack).thenAnswer((_) async {});
|
||||||
|
when(asset.restoreTrash).thenAnswer((_) async {});
|
||||||
|
when(asset.trash).thenAnswer((_) async {});
|
||||||
|
when(asset.delete).thenAnswer((_) async {});
|
||||||
|
when(asset.applyEdits).thenAnswer((_) async {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _stubRemoteAlbumService() {
|
||||||
|
when(album.removeAssets).thenAnswer((_) async => 0);
|
||||||
|
when(album.updateAlbum).thenAnswer((_) async => RemoteAlbumFactory.create());
|
||||||
|
}
|
||||||
|
|
||||||
|
void _stubCleanupService() {
|
||||||
|
when(cleanup.deleteLocalAssets).thenAnswer((_) async => 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _stubTagService() {
|
||||||
|
when(tag.bulkTagAssets).thenAnswer((_) async => 0);
|
||||||
|
when(tag.upsertTags).thenAnswer((_) async => const []);
|
||||||
|
when(tag.getAllTags).thenAnswer((_) async => const {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _stubBackgroundSync() {
|
||||||
|
when(() => backgroundSync.syncLocal()).thenAnswer((_) async {});
|
||||||
|
when(() => backgroundSync.hashAssets()).thenAnswer((_) async {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _stubForegroundUpload() {
|
||||||
|
when(
|
||||||
|
() => upload.uploadManual(
|
||||||
|
any(),
|
||||||
|
cancelToken: any(named: 'cancelToken'),
|
||||||
|
callbacks: any(named: 'callbacks'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,8 +204,24 @@ void _registerFallbacks() {
|
|||||||
registerFallbackValue(LocalAlbumFactory.create());
|
registerFallbackValue(LocalAlbumFactory.create());
|
||||||
registerFallbackValue(LocalAssetFactory.create());
|
registerFallbackValue(LocalAssetFactory.create());
|
||||||
registerFallbackValue(Uint8List(0));
|
registerFallbackValue(Uint8List(0));
|
||||||
|
registerFallbackValue(AssetVisibility.timeline);
|
||||||
|
registerFallbackValue(const LatLng(0, 0));
|
||||||
|
registerFallbackValue(<AssetEdit>[]);
|
||||||
|
registerFallbackValue(const Option<bool>.none());
|
||||||
|
registerFallbackValue(const Option<AssetVisibility>.none());
|
||||||
|
registerFallbackValue(const Option<LatLng>.none());
|
||||||
|
registerFallbackValue(const Option<String>.none());
|
||||||
|
registerFallbackValue(const Option<DateTime>.none());
|
||||||
|
registerFallbackValue(<BaseAsset>[]);
|
||||||
|
registerFallbackValue(<RemoteAsset>[]);
|
||||||
|
registerFallbackValue(<LocalAsset>[]);
|
||||||
|
registerFallbackValue(ShareAssetType.original);
|
||||||
|
registerFallbackValue(const UploadCallbacks());
|
||||||
|
registerFallbackValue(_FakeBuildContext());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _FakeBuildContext extends Fake implements BuildContext {}
|
||||||
|
|
||||||
extension type const Stub<T extends Mock>(T mockedClass) {
|
extension type const Stub<T extends Mock>(T mockedClass) {
|
||||||
void reset() => mock.reset(mockedClass);
|
void reset() => mock.reset(mockedClass);
|
||||||
}
|
}
|
||||||
@@ -119,6 +243,33 @@ extension type const LocalAssetRepositoryStub(MockDriftLocalAssetRepository repo
|
|||||||
() => repo.updateHashes(any());
|
() => repo.updateHashes(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension type const RemoteAssetRepositoryStub(MockRemoteAssetRepository repo)
|
||||||
|
implements Stub<MockRemoteAssetRepository> {
|
||||||
|
Future<ExifInfo?> Function() get getExif =>
|
||||||
|
() => repo.getExif(any());
|
||||||
|
|
||||||
|
Future<List<AssetEdit>> Function() get getAssetEdits =>
|
||||||
|
() => repo.getAssetEdits(any());
|
||||||
|
|
||||||
|
Future<void> Function() get update =>
|
||||||
|
() => repo.update(
|
||||||
|
any(),
|
||||||
|
isFavorite: any(named: 'isFavorite'),
|
||||||
|
visibility: any(named: 'visibility'),
|
||||||
|
createdAt: any(named: 'createdAt'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
extension type const RemoteExifRepositoryStub(MockRemoteExifRepository repo) implements Stub<MockRemoteExifRepository> {
|
||||||
|
Future<void> Function() get update =>
|
||||||
|
() => repo.update(
|
||||||
|
any(),
|
||||||
|
dateTimeOriginal: any(named: 'dateTimeOriginal'),
|
||||||
|
timeZone: any(named: 'timeZone'),
|
||||||
|
location: any(named: 'location'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
extension type const PartnerServiceStub(MockPartnerService service) implements Stub<MockPartnerService> {
|
extension type const PartnerServiceStub(MockPartnerService service) implements Stub<MockPartnerService> {
|
||||||
Stream<Iterable<User>> Function() get getCandidates =>
|
Stream<Iterable<User>> Function() get getCandidates =>
|
||||||
() => service.getCandidates(any());
|
() => service.getCandidates(any());
|
||||||
@@ -165,11 +316,89 @@ extension type const UserServiceStub(MockUserService service) implements Stub<Mo
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension type const AssetServiceStub(MockAssetService service) implements Stub<MockAssetService> {
|
extension type const AssetServiceStub(MockAssetService service) implements Stub<MockAssetService> {
|
||||||
Future<void> Function() get updateFavorite =>
|
Future<void> Function() get update =>
|
||||||
() => service.updateFavorite(any(), any());
|
() => service.update(
|
||||||
|
any(),
|
||||||
|
isFavorite: any(named: 'isFavorite'),
|
||||||
|
visibility: any(named: 'visibility'),
|
||||||
|
dateTime: any(named: 'dateTime'),
|
||||||
|
location: any(named: 'location'),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<void> Function() get stack =>
|
||||||
|
() => service.stack(any(), any());
|
||||||
|
|
||||||
|
Future<void> Function() get unstack =>
|
||||||
|
() => service.unstack(any());
|
||||||
|
|
||||||
|
Future<void> Function() get restoreTrash =>
|
||||||
|
() => service.restoreTrash(any());
|
||||||
|
|
||||||
|
Future<void> Function() get trash =>
|
||||||
|
() => service.trash(any());
|
||||||
|
|
||||||
|
Future<void> Function() get delete =>
|
||||||
|
() => service.delete(any());
|
||||||
|
|
||||||
|
Future<void> Function() get applyEdits =>
|
||||||
|
() => service.applyEdits(any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
extension type const RemoteAlbumServiceStub(MockRemoteAlbumService service) implements Stub<MockRemoteAlbumService> {
|
||||||
|
Future<int> Function() get removeAssets =>
|
||||||
|
() => service.removeAssets(
|
||||||
|
albumId: any(named: 'albumId'),
|
||||||
|
assetIds: any(named: 'assetIds'),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<RemoteAlbum> Function() get updateAlbum =>
|
||||||
|
() => service.updateAlbum(any(), thumbnailAssetId: any(named: 'thumbnailAssetId'));
|
||||||
|
}
|
||||||
|
|
||||||
|
extension type const CleanupServiceStub(MockCleanupService service) implements Stub<MockCleanupService> {
|
||||||
|
Future<int> Function() get deleteLocalAssets =>
|
||||||
|
() => service.deleteLocalAssets(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
extension type const NativeSyncApiStub(MockNativeSyncApi api) implements Stub<MockNativeSyncApi> {
|
extension type const NativeSyncApiStub(MockNativeSyncApi api) implements Stub<MockNativeSyncApi> {
|
||||||
Future<List<HashResult>> Function() get hashAssets =>
|
Future<List<HashResult>> Function() get hashAssets =>
|
||||||
() => api.hashAssets(any(), allowNetworkAccess: any(named: 'allowNetworkAccess'));
|
() => api.hashAssets(any(), allowNetworkAccess: any(named: 'allowNetworkAccess'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension type const AssetApiRepositoryStub(MockAssetApiRepository api) implements Stub<MockAssetApiRepository> {
|
||||||
|
Future<void> Function() get update =>
|
||||||
|
() => api.update(
|
||||||
|
any(),
|
||||||
|
isFavorite: any(named: 'isFavorite'),
|
||||||
|
visibility: any(named: 'visibility'),
|
||||||
|
dateTimeOriginal: any(named: 'dateTimeOriginal'),
|
||||||
|
location: any(named: 'location'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
extension type const AssetMediaRepositoryStub(MockAssetMediaRepository api) implements Stub<MockAssetMediaRepository> {
|
||||||
|
Future<int> Function() get shareAssets =>
|
||||||
|
() => api.shareAssets(
|
||||||
|
any(),
|
||||||
|
any(),
|
||||||
|
fileType: any(named: 'fileType'),
|
||||||
|
cancelCompleter: any(named: 'cancelCompleter'),
|
||||||
|
onAssetDownloadProgress: any(named: 'onAssetDownloadProgress'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
extension type const DownloadRepositoryStub(MockDownloadRepository repo) implements Stub<MockDownloadRepository> {
|
||||||
|
Future<List<bool>> Function() get downloadAllAssets =>
|
||||||
|
() => repo.downloadAllAssets(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
extension type const TagServiceStub(MockTagService service) implements Stub<MockTagService> {
|
||||||
|
Future<int> Function() get bulkTagAssets =>
|
||||||
|
() => service.bulkTagAssets(any(), any());
|
||||||
|
|
||||||
|
Future<List<Tag>> Function() get upsertTags =>
|
||||||
|
() => service.upsertTags(any());
|
||||||
|
|
||||||
|
Future<Set<Tag>> Function() get getAllTags =>
|
||||||
|
() => service.getAllTags();
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
|||||||
import 'package:immich_mobile/presentation/actions/favorite.action.dart';
|
import 'package:immich_mobile/presentation/actions/favorite.action.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
import '../../../domain/service.mock.dart';
|
import '../../../service.mocks.dart';
|
||||||
import '../../factories/remote_asset_factory.dart';
|
import '../../factories/remote_asset_factory.dart';
|
||||||
import '../presentation_context.dart';
|
import '../presentation_context.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import 'package:immich_mobile/domain/models/user.model.dart';
|
|||||||
import 'package:immich_mobile/presentation/actions/partner.action.dart';
|
import 'package:immich_mobile/presentation/actions/partner.action.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
import '../../../domain/service.mock.dart';
|
import '../../../service.mocks.dart';
|
||||||
import '../../factories/user_factory.dart';
|
import '../../factories/user_factory.dart';
|
||||||
import '../presentation_context.dart';
|
import '../presentation_context.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ import 'package:immich_mobile/presentation/actions/action.dart';
|
|||||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/services/gcast.service.dart';
|
||||||
|
import 'package:immich_mobile/services/server_info.service.dart';
|
||||||
import 'package:immich_ui/immich_ui.dart';
|
import 'package:immich_ui/immich_ui.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
@@ -29,6 +32,7 @@ class PresentationContext {
|
|||||||
service = ServiceMocks(),
|
service = ServiceMocks(),
|
||||||
repository = RepositoryMocks() {
|
repository = RepositoryMocks() {
|
||||||
setup();
|
setup();
|
||||||
|
addTearDown(dispose);
|
||||||
}
|
}
|
||||||
|
|
||||||
static const String serverEndpoint = 'http://localhost:3000';
|
static const String serverEndpoint = 'http://localhost:3000';
|
||||||
@@ -43,6 +47,9 @@ class PresentationContext {
|
|||||||
currentUserProvider.overrideWith((ref) => CurrentUserProvider(service.user.service)),
|
currentUserProvider.overrideWith((ref) => CurrentUserProvider(service.user.service)),
|
||||||
assetServiceProvider.overrideWithValue(service.asset.service),
|
assetServiceProvider.overrideWithValue(service.asset.service),
|
||||||
partnerServiceProvider.overrideWithValue(service.partner.service),
|
partnerServiceProvider.overrideWithValue(service.partner.service),
|
||||||
|
gCastServiceProvider.overrideWithValue(service.cast),
|
||||||
|
serverInfoServiceProvider.overrideWithValue(service.serverInfo),
|
||||||
|
inLockedViewProvider.overrideWithValue(false),
|
||||||
];
|
];
|
||||||
|
|
||||||
static Future<PresentationContext> create() async {
|
static Future<PresentationContext> create() async {
|
||||||
@@ -61,9 +68,7 @@ class PresentationContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
addTearDown(() {
|
service.resetAll();
|
||||||
service.resetAll();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/asset.service.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
|
import '../../infrastructure/repository.mock.dart';
|
||||||
|
import '../../repository.mocks.dart';
|
||||||
|
import '../mocks.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late AssetService sut;
|
||||||
|
late RepositoryMocks mocks;
|
||||||
|
late MockAssetApiRepository apiRepository;
|
||||||
|
late MockRemoteAssetRepository remoteRepository;
|
||||||
|
late MockRemoteExifRepository exifRepository;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
mocks = RepositoryMocks();
|
||||||
|
apiRepository = mocks.assetApi.api;
|
||||||
|
remoteRepository = mocks.remoteAsset.repo;
|
||||||
|
exifRepository = mocks.remoteExif.repo;
|
||||||
|
|
||||||
|
sut = AssetService(
|
||||||
|
remoteRepository: remoteRepository,
|
||||||
|
exifRepository: exifRepository,
|
||||||
|
localRepository: MockDriftLocalAssetRepository(),
|
||||||
|
apiRepository: apiRepository,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('AssetService.updateDateTime', () {
|
||||||
|
const ids = ['asset_id_1'];
|
||||||
|
|
||||||
|
test('sends the picked value to the api with its offset intact', () async {
|
||||||
|
const picked = '2026-06-10T19:15:00.000+06:00';
|
||||||
|
await sut.update(ids, dateTime: const .some(picked));
|
||||||
|
|
||||||
|
verify(() => apiRepository.update(ids, dateTimeOriginal: const .some(picked))).called(1);
|
||||||
|
verify(() => remoteRepository.update(ids, createdAt: .some(DateTime.parse(picked)))).called(1);
|
||||||
|
verify(
|
||||||
|
() => exifRepository.update(
|
||||||
|
ids,
|
||||||
|
dateTimeOriginal: .some(DateTime.parse(picked)),
|
||||||
|
timeZone: const .some('UTC+06:00'),
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles negative offsets', () async {
|
||||||
|
const picked = '2026-01-05T08:00:00.000-05:30';
|
||||||
|
await sut.update(ids, dateTime: const .some(picked));
|
||||||
|
|
||||||
|
verify(() => remoteRepository.update(ids, createdAt: .some(DateTime.parse(picked)))).called(1);
|
||||||
|
verify(
|
||||||
|
() => exifRepository.update(
|
||||||
|
ids,
|
||||||
|
dateTimeOriginal: .some(DateTime.parse(picked)),
|
||||||
|
timeZone: const .some('UTC-05:30'),
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('writes no timezone when the value has no offset', () async {
|
||||||
|
const picked = '2026-06-10T13:15:00.000Z';
|
||||||
|
await sut.update(ids, dateTime: const .some(picked));
|
||||||
|
|
||||||
|
verify(() => remoteRepository.update(ids, createdAt: .some(DateTime.parse(picked)))).called(1);
|
||||||
|
verify(
|
||||||
|
() => exifRepository.update(ids, dateTimeOriginal: .some(DateTime.parse(picked)), timeZone: const .none()),
|
||||||
|
).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('is a no-op when there are no asset ids', () async {
|
||||||
|
await sut.update(const [], dateTime: const .some('2026-06-10T19:15:00.000+06:00'));
|
||||||
|
|
||||||
|
verifyZeroInteractions(apiRepository);
|
||||||
|
verifyZeroInteractions(remoteRepository);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/remote_album.service.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
|
import '../../service.mocks.dart';
|
||||||
|
import '../mocks.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late RemoteAlbumService sut;
|
||||||
|
final mocks = RepositoryMocks();
|
||||||
|
|
||||||
|
setUpAll(() {
|
||||||
|
registerFallbackValue(<String>[]);
|
||||||
|
});
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
sut = RemoteAlbumService(mocks.remoteAlbum, mocks.albumApi, MockForegroundUploadService());
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
mocks.resetAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
group('RemoteAlbumService', () {
|
||||||
|
group('removeAssets', () {
|
||||||
|
test('persists only the assets the server actually removed, not the whole request', () async {
|
||||||
|
const albumId = 'album-1';
|
||||||
|
const requested = ['asset-1', 'asset-2', 'asset-3'];
|
||||||
|
const removed = ['asset-1', 'asset-3'];
|
||||||
|
|
||||||
|
// The server rejected 'asset-2'
|
||||||
|
when(
|
||||||
|
() => mocks.albumApi.removeAssets(albumId, requested),
|
||||||
|
).thenAnswer((_) async => (removed: removed, failed: ['asset-2']));
|
||||||
|
when(() => mocks.remoteAlbum.removeAssets(albumId, any())).thenAnswer((_) async {});
|
||||||
|
|
||||||
|
final count = await sut.removeAssets(albumId: albumId, assetIds: requested);
|
||||||
|
|
||||||
|
final persisted =
|
||||||
|
verify(() => mocks.remoteAlbum.removeAssets(albumId, captureAny())).captured.single as List<String>;
|
||||||
|
expect(persisted, removed);
|
||||||
|
expect(persisted, isNot(contains('asset-2')));
|
||||||
|
|
||||||
|
expect(count, removed.length);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -52,7 +52,7 @@ void main() {
|
|||||||
final offlinePhoto = LocalAssetFactory.create();
|
final offlinePhoto = LocalAssetFactory.create();
|
||||||
final remotePhoto = RemoteAssetFactory.create();
|
final remotePhoto = RemoteAssetFactory.create();
|
||||||
|
|
||||||
final AssetFilter<LocalAsset> syncedPhotos = AssetFilter(<BaseAsset>[
|
final AssetFilter<BaseAsset> syncedPhotos = AssetFilter(<BaseAsset>[
|
||||||
syncedPhoto,
|
syncedPhoto,
|
||||||
offlinePhoto,
|
offlinePhoto,
|
||||||
remotePhoto,
|
remotePhoto,
|
||||||
|
|||||||
Reference in New Issue
Block a user