From 7f7cd5e696017c67ca295d64fd1127bb1b1c3070 Mon Sep 17 00:00:00 2001 From: mertalev <101130780+mertalev@users.noreply.github.com> Date: Wed, 1 Apr 2026 19:12:55 -0400 Subject: [PATCH] sealed class --- .../lib/domain/models/asset_edit.model.dart | 32 +++--- mobile/lib/extensions/object_extensions.dart | 3 + .../entities/asset_edit.entity.dart | 11 +- .../repositories/remote_asset.repository.dart | 5 +- .../presentation/pages/drift_edit.page.dart | 33 ++---- .../repositories/asset_api.repository.dart | 30 ++++-- mobile/lib/utils/editor.utils.dart | 27 ++--- mobile/test/utils/editor_test.dart | 101 +++++++++--------- 8 files changed, 118 insertions(+), 124 deletions(-) create mode 100644 mobile/lib/extensions/object_extensions.dart diff --git a/mobile/lib/domain/models/asset_edit.model.dart b/mobile/lib/domain/models/asset_edit.model.dart index b3266dba46..9809b9c606 100644 --- a/mobile/lib/domain/models/asset_edit.model.dart +++ b/mobile/lib/domain/models/asset_edit.model.dart @@ -1,21 +1,25 @@ -import "package:openapi/api.dart" as api show AssetEditAction; +import "package:openapi/api.dart" show CropParameters, RotateParameters, MirrorParameters; enum AssetEditAction { rotate, crop, mirror, other } -extension AssetEditActionExtension on AssetEditAction { - api.AssetEditAction? toDto() { - return switch (this) { - AssetEditAction.rotate => api.AssetEditAction.rotate, - AssetEditAction.crop => api.AssetEditAction.crop, - AssetEditAction.mirror => api.AssetEditAction.mirror, - AssetEditAction.other => null, - }; - } +sealed class AssetEdit { + const AssetEdit(); } -class AssetEdit { - final AssetEditAction action; - final Map parameters; +class CropEdit extends AssetEdit { + final CropParameters parameters; - const AssetEdit({required this.action, required this.parameters}); + const CropEdit(this.parameters); +} + +class RotateEdit extends AssetEdit { + final RotateParameters parameters; + + const RotateEdit(this.parameters); +} + +class MirrorEdit extends AssetEdit { + final MirrorParameters parameters; + + const MirrorEdit(this.parameters); } diff --git a/mobile/lib/extensions/object_extensions.dart b/mobile/lib/extensions/object_extensions.dart new file mode 100644 index 0000000000..4e76532137 --- /dev/null +++ b/mobile/lib/extensions/object_extensions.dart @@ -0,0 +1,3 @@ +extension Let on T { + R let(R Function(T) transform) => transform(this); +} diff --git a/mobile/lib/infrastructure/entities/asset_edit.entity.dart b/mobile/lib/infrastructure/entities/asset_edit.entity.dart index 22d059bdb4..87a05ab8fe 100644 --- a/mobile/lib/infrastructure/entities/asset_edit.entity.dart +++ b/mobile/lib/infrastructure/entities/asset_edit.entity.dart @@ -1,8 +1,10 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/asset_edit.model.dart'; +import 'package:immich_mobile/extensions/object_extensions.dart'; import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; +import 'package:openapi/api.dart' hide AssetEditAction; @TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)') class AssetEditEntity extends Table with DriftDefaultsMixin { @@ -27,7 +29,12 @@ final JsonTypeConverter2, Uint8List, Object?> editParameter ); extension AssetEditEntityDataDomainEx on AssetEditEntityData { - AssetEdit toDto() { - return AssetEdit(action: action, parameters: parameters); + AssetEdit? toDto() { + return switch (action) { + AssetEditAction.crop => CropParameters.fromJson(parameters)?.let(CropEdit.new), + AssetEditAction.rotate => RotateParameters.fromJson(parameters)?.let(RotateEdit.new), + AssetEditAction.mirror => MirrorParameters.fromJson(parameters)?.let(MirrorEdit.new), + AssetEditAction.other => null, + }; } } diff --git a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart index 002160f549..00c0b81850 100644 --- a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart @@ -269,9 +269,8 @@ class RemoteAssetRepository extends DriftDatabaseRepository { Future> getAssetEdits(String assetId) { final query = _db.assetEditEntity.select() - ..where((row) => row.assetId.equals(assetId)) + ..where((row) => row.assetId.equals(assetId) & row.action.equals(AssetEditAction.other.index).not()) ..orderBy([(row) => OrderingTerm.asc(row.sequence)]); - - return query.map((row) => row.toDto()).get(); + return query.map((row) => row.toDto()!).get(); } } diff --git a/mobile/lib/presentation/pages/drift_edit.page.dart b/mobile/lib/presentation/pages/drift_edit.page.dart index f21e7654f6..2ca199b273 100644 --- a/mobile/lib/presentation/pages/drift_edit.page.dart +++ b/mobile/lib/presentation/pages/drift_edit.page.dart @@ -15,7 +15,7 @@ import 'package:immich_mobile/providers/theme.provider.dart'; import 'package:immich_mobile/theme/theme_data.dart'; import 'package:immich_mobile/utils/editor.utils.dart'; import 'package:immich_ui/immich_ui.dart'; -import 'package:openapi/api.dart' show CropParameters, RotateParameters, MirrorParameters, MirrorAxis; +import 'package:openapi/api.dart' show RotateParameters, MirrorParameters, MirrorAxis; @RoutePage() class DriftEditImagePage extends ConsumerStatefulWidget { @@ -54,14 +54,10 @@ class _DriftEditImagePageState extends ConsumerState with Ti late final Rect _initialCrop; void initEditor() { - final existingCrop = widget.edits.firstWhereOrNull((edit) => edit.action == AssetEditAction.crop); + final existingCrop = widget.edits.whereType().firstOrNull; Rect crop = existingCrop != null && originalWidth != null && originalHeight != null - ? convertCropParametersToRect( - CropParameters.fromJson(existingCrop.parameters)!, - originalWidth!, - originalHeight!, - ) + ? convertCropParametersToRect(existingCrop.parameters, originalWidth!, originalHeight!) : const Rect.fromLTRB(0, 0, 1, 1); cropController = CropController(defaultCrop: crop); @@ -90,34 +86,19 @@ class _DriftEditImagePageState extends ConsumerState with Ti final edits = []; if (cropParameters.width != originalWidth || cropParameters.height != originalHeight) { - edits.add(AssetEdit(action: AssetEditAction.crop, parameters: cropParameters.toJson())); + edits.add(CropEdit(cropParameters)); } if (_flipHorizontal) { - edits.add( - AssetEdit( - action: AssetEditAction.mirror, - parameters: MirrorParameters(axis: MirrorAxis.horizontal).toJson(), - ), - ); + edits.add(MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal))); } if (_flipVertical) { - edits.add( - AssetEdit( - action: AssetEditAction.mirror, - parameters: MirrorParameters(axis: MirrorAxis.vertical).toJson(), - ), - ); + edits.add(MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical))); } if (normalizedRotation != 0) { - edits.add( - AssetEdit( - action: AssetEditAction.rotate, - parameters: RotateParameters(angle: normalizedRotation).toJson(), - ), - ); + edits.add(RotateEdit(RotateParameters(angle: normalizedRotation))); } await widget.applyEdits(edits); diff --git a/mobile/lib/repositories/asset_api.repository.dart b/mobile/lib/repositories/asset_api.repository.dart index 35f5a2952f..d66b39ecde 100644 --- a/mobile/lib/repositories/asset_api.repository.dart +++ b/mobile/lib/repositories/asset_api.repository.dart @@ -1,13 +1,13 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:http/http.dart'; import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/asset_edit.model.dart'; +import 'package:immich_mobile/domain/models/asset_edit.model.dart' hide AssetEditAction; import 'package:immich_mobile/domain/models/stack.model.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/repositories/api.repository.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; -import 'package:openapi/api.dart' hide AssetEditAction; +import 'package:openapi/api.dart'; final assetApiRepositoryProvider = Provider( (ref) => AssetApiRepository( @@ -108,12 +108,7 @@ class AssetApiRepository extends ApiRepository { } Future editAsset(String assetId, List edits) { - final editDtos = edits - .where((edit) => edit.action != AssetEditAction.other) - .map((edit) => AssetEditActionItemDto(action: edit.action.toDto()!, parameters: edit.parameters)) - .toList(); - - return _api.editAsset(assetId, AssetEditsCreateDto(edits: editDtos)); + return _api.editAsset(assetId, AssetEditsCreateDto(edits: edits.map((e) => e.toApi()).toList())); } Future removeEdits(String assetId) async { @@ -126,3 +121,22 @@ extension on StackResponseDto { return StackResponse(id: id, primaryAssetId: primaryAssetId, assetIds: assets.map((asset) => asset.id).toList()); } } + +extension on AssetEdit { + AssetEditActionItemDto toApi() { + return switch (this) { + CropEdit(:final parameters) => AssetEditActionItemDto( + action: AssetEditAction.crop, + parameters: parameters.toJson(), + ), + RotateEdit(:final parameters) => AssetEditActionItemDto( + action: AssetEditAction.rotate, + parameters: parameters.toJson(), + ), + MirrorEdit(:final parameters) => AssetEditActionItemDto( + action: AssetEditAction.mirror, + parameters: parameters.toJson(), + ), + }; + } +} diff --git a/mobile/lib/utils/editor.utils.dart b/mobile/lib/utils/editor.utils.dart index edf5079b7f..fa2dedf383 100644 --- a/mobile/lib/utils/editor.utils.dart +++ b/mobile/lib/utils/editor.utils.dart @@ -31,27 +31,12 @@ CropParameters convertRectToCropParameters(Rect rect, int originalWidth, int ori AffineMatrix buildAffineFromEdits(List edits) { return AffineMatrix.compose( edits.map((edit) { - switch (edit.action) { - case AssetEditAction.rotate: - final parameters = RotateParameters.fromJson(edit.parameters); - if (parameters == null) { - throw ArgumentError("Unable to parse rotate parameters from edit: ${edit.parameters}"); - } - - final angleInDegrees = parameters.angle; - final angleInRadians = angleInDegrees * pi / 180; - return AffineMatrix.rotate(angleInRadians); - - case AssetEditAction.mirror: - final parameters = MirrorParameters.fromJson(edit.parameters); - if (parameters == null) { - throw ArgumentError("Unable to parse mirror parameters from edit: ${edit.parameters}"); - } - - return parameters.axis == MirrorAxis.horizontal ? AffineMatrix.flipY() : AffineMatrix.flipX(); - default: - return AffineMatrix.identity(); - } + return switch (edit) { + RotateEdit(:final parameters) => AffineMatrix.rotate(parameters.angle * pi / 180), + MirrorEdit(:final parameters) => + parameters.axis == MirrorAxis.horizontal ? AffineMatrix.flipY() : AffineMatrix.flipX(), + CropEdit() => AffineMatrix.identity(), + }; }).toList(), ); } diff --git a/mobile/test/utils/editor_test.dart b/mobile/test/utils/editor_test.dart index 835ff77781..16f1c08d05 100644 --- a/mobile/test/utils/editor_test.dart +++ b/mobile/test/utils/editor_test.dart @@ -1,20 +1,21 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/utils/editor.utils.dart'; +import 'package:openapi/api.dart' show MirrorAxis, MirrorParameters, RotateParameters; List normalizedToEdits(NormalizedTransform transform) { List edits = []; if (transform.mirrorHorizontal) { - edits.add(const AssetEdit(action: AssetEditAction.mirror, parameters: {"axis": "horizontal"})); + edits.add(MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal))); } if (transform.mirrorVertical) { - edits.add(const AssetEdit(action: AssetEditAction.mirror, parameters: {"axis": "vertical"})); + edits.add(MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical))); } if (transform.rotation != 0) { - edits.add(AssetEdit(action: AssetEditAction.rotate, parameters: {"angle": transform.rotation})); + edits.add(RotateEdit(RotateParameters(angle: transform.rotation))); } return edits; @@ -43,7 +44,7 @@ void main() { test('should handle a single 90° rotation', () { final edits = [ - const AssetEdit(action: AssetEditAction.rotate, parameters: {"angle": 90}), + RotateEdit(RotateParameters(angle: 90)), ]; final result = normalizeTransformEdits(edits); @@ -54,7 +55,7 @@ void main() { test('should handle a single 180° rotation', () { final edits = [ - const AssetEdit(action: AssetEditAction.rotate, parameters: {"angle": 180}), + RotateEdit(RotateParameters(angle: 180)), ]; final result = normalizeTransformEdits(edits); @@ -65,7 +66,7 @@ void main() { test('should handle a single 270° rotation', () { final edits = [ - const AssetEdit(action: AssetEditAction.rotate, parameters: {"angle": 270}), + RotateEdit(RotateParameters(angle: 270)), ]; final result = normalizeTransformEdits(edits); @@ -76,7 +77,7 @@ void main() { test('should handle a single horizontal mirror', () { final edits = [ - const AssetEdit(action: AssetEditAction.mirror, parameters: {"axis": "horizontal"}), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), ]; final result = normalizeTransformEdits(edits); @@ -87,7 +88,7 @@ void main() { test('should handle a single vertical mirror', () { final edits = [ - const AssetEdit(action: AssetEditAction.mirror, parameters: {"axis": "vertical"}), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), ]; final result = normalizeTransformEdits(edits); @@ -98,8 +99,8 @@ void main() { test('should handle 90° rotation + horizontal mirror', () { final edits = [ - const AssetEdit(action: AssetEditAction.rotate, parameters: {"angle": 90}), - const AssetEdit(action: AssetEditAction.mirror, parameters: {"axis": "horizontal"}), + RotateEdit(RotateParameters(angle: 90)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), ]; final result = normalizeTransformEdits(edits); @@ -110,8 +111,8 @@ void main() { test('should handle 90° rotation + vertical mirror', () { final edits = [ - const AssetEdit(action: AssetEditAction.rotate, parameters: {"angle": 90}), - const AssetEdit(action: AssetEditAction.mirror, parameters: {"axis": "vertical"}), + RotateEdit(RotateParameters(angle: 90)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), ]; final result = normalizeTransformEdits(edits); @@ -122,9 +123,9 @@ void main() { test('should handle 90° rotation + both mirrors', () { final edits = [ - const AssetEdit(action: AssetEditAction.rotate, parameters: {"angle": 90}), - const AssetEdit(action: AssetEditAction.mirror, parameters: {"axis": "horizontal"}), - const AssetEdit(action: AssetEditAction.mirror, parameters: {"axis": "vertical"}), + RotateEdit(RotateParameters(angle: 90)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), ]; final result = normalizeTransformEdits(edits); @@ -135,8 +136,8 @@ void main() { test('should handle 180° rotation + horizontal mirror', () { final edits = [ - const AssetEdit(action: AssetEditAction.rotate, parameters: {"angle": 180}), - const AssetEdit(action: AssetEditAction.mirror, parameters: {"axis": "horizontal"}), + RotateEdit(RotateParameters(angle: 180)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), ]; final result = normalizeTransformEdits(edits); @@ -147,8 +148,8 @@ void main() { test('should handle 180° rotation + vertical mirror', () { final edits = [ - const AssetEdit(action: AssetEditAction.rotate, parameters: {"angle": 180}), - const AssetEdit(action: AssetEditAction.mirror, parameters: {"axis": "vertical"}), + RotateEdit(RotateParameters(angle: 180)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), ]; final result = normalizeTransformEdits(edits); @@ -159,9 +160,9 @@ void main() { test('should handle 180° rotation + both mirrors', () { final edits = [ - const AssetEdit(action: AssetEditAction.rotate, parameters: {"angle": 180}), - const AssetEdit(action: AssetEditAction.mirror, parameters: {"axis": "horizontal"}), - const AssetEdit(action: AssetEditAction.mirror, parameters: {"axis": "vertical"}), + RotateEdit(RotateParameters(angle: 180)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), ]; final result = normalizeTransformEdits(edits); @@ -172,8 +173,8 @@ void main() { test('should handle 270° rotation + horizontal mirror', () { final edits = [ - const AssetEdit(action: AssetEditAction.rotate, parameters: {"angle": 270}), - const AssetEdit(action: AssetEditAction.mirror, parameters: {"axis": "horizontal"}), + RotateEdit(RotateParameters(angle: 270)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), ]; final result = normalizeTransformEdits(edits); @@ -184,8 +185,8 @@ void main() { test('should handle 270° rotation + vertical mirror', () { final edits = [ - const AssetEdit(action: AssetEditAction.rotate, parameters: {"angle": 270}), - const AssetEdit(action: AssetEditAction.mirror, parameters: {"axis": "vertical"}), + RotateEdit(RotateParameters(angle: 270)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), ]; final result = normalizeTransformEdits(edits); @@ -196,9 +197,9 @@ void main() { test('should handle 270° rotation + both mirrors', () { final edits = [ - const AssetEdit(action: AssetEditAction.rotate, parameters: {"angle": 270}), - const AssetEdit(action: AssetEditAction.mirror, parameters: {"axis": "horizontal"}), - const AssetEdit(action: AssetEditAction.mirror, parameters: {"axis": "vertical"}), + RotateEdit(RotateParameters(angle: 270)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), ]; final result = normalizeTransformEdits(edits); @@ -209,8 +210,8 @@ void main() { test('should handle horizontal mirror + 90° rotation', () { final edits = [ - const AssetEdit(action: AssetEditAction.mirror, parameters: {"axis": "horizontal"}), - const AssetEdit(action: AssetEditAction.rotate, parameters: {"angle": 90}), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + RotateEdit(RotateParameters(angle: 90)), ]; final result = normalizeTransformEdits(edits); @@ -221,8 +222,8 @@ void main() { test('should handle horizontal mirror + 180° rotation', () { final edits = [ - const AssetEdit(action: AssetEditAction.mirror, parameters: {"axis": "horizontal"}), - const AssetEdit(action: AssetEditAction.rotate, parameters: {"angle": 180}), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + RotateEdit(RotateParameters(angle: 180)), ]; final result = normalizeTransformEdits(edits); @@ -233,8 +234,8 @@ void main() { test('should handle horizontal mirror + 270° rotation', () { final edits = [ - const AssetEdit(action: AssetEditAction.mirror, parameters: {"axis": "horizontal"}), - const AssetEdit(action: AssetEditAction.rotate, parameters: {"angle": 270}), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + RotateEdit(RotateParameters(angle: 270)), ]; final result = normalizeTransformEdits(edits); @@ -245,8 +246,8 @@ void main() { test('should handle vertical mirror + 90° rotation', () { final edits = [ - const AssetEdit(action: AssetEditAction.mirror, parameters: {"axis": "vertical"}), - const AssetEdit(action: AssetEditAction.rotate, parameters: {"angle": 90}), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + RotateEdit(RotateParameters(angle: 90)), ]; final result = normalizeTransformEdits(edits); @@ -257,8 +258,8 @@ void main() { test('should handle vertical mirror + 180° rotation', () { final edits = [ - const AssetEdit(action: AssetEditAction.mirror, parameters: {"axis": "vertical"}), - const AssetEdit(action: AssetEditAction.rotate, parameters: {"angle": 180}), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + RotateEdit(RotateParameters(angle: 180)), ]; final result = normalizeTransformEdits(edits); @@ -269,8 +270,8 @@ void main() { test('should handle vertical mirror + 270° rotation', () { final edits = [ - const AssetEdit(action: AssetEditAction.mirror, parameters: {"axis": "vertical"}), - const AssetEdit(action: AssetEditAction.rotate, parameters: {"angle": 270}), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + RotateEdit(RotateParameters(angle: 270)), ]; final result = normalizeTransformEdits(edits); @@ -281,9 +282,9 @@ void main() { test('should handle both mirrors + 90° rotation', () { final edits = [ - const AssetEdit(action: AssetEditAction.mirror, parameters: {"axis": "horizontal"}), - const AssetEdit(action: AssetEditAction.mirror, parameters: {"axis": "vertical"}), - const AssetEdit(action: AssetEditAction.rotate, parameters: {"angle": 90}), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + RotateEdit(RotateParameters(angle: 90)), ]; final result = normalizeTransformEdits(edits); @@ -294,9 +295,9 @@ void main() { test('should handle both mirrors + 180° rotation', () { final edits = [ - const AssetEdit(action: AssetEditAction.mirror, parameters: {"axis": "horizontal"}), - const AssetEdit(action: AssetEditAction.mirror, parameters: {"axis": "vertical"}), - const AssetEdit(action: AssetEditAction.rotate, parameters: {"angle": 180}), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + RotateEdit(RotateParameters(angle: 180)), ]; final result = normalizeTransformEdits(edits); @@ -307,9 +308,9 @@ void main() { test('should handle both mirrors + 270° rotation', () { final edits = [ - const AssetEdit(action: AssetEditAction.mirror, parameters: {"axis": "horizontal"}), - const AssetEdit(action: AssetEditAction.mirror, parameters: {"axis": "vertical"}), - const AssetEdit(action: AssetEditAction.rotate, parameters: {"angle": 270}), + MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal)), + MirrorEdit(MirrorParameters(axis: MirrorAxis.vertical)), + RotateEdit(RotateParameters(angle: 270)), ]; final result = normalizeTransformEdits(edits);