sealed class

This commit is contained in:
mertalev 2026-04-01 19:12:55 -04:00
parent 54042ea424
commit 7f7cd5e696
No known key found for this signature in database
GPG Key ID: C50FA35C7B2CBAFB
8 changed files with 118 additions and 124 deletions

View File

@ -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<String, dynamic> 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);
}

View File

@ -0,0 +1,3 @@
extension Let<T extends Object> on T {
R let<R>(R Function(T) transform) => transform(this);
}

View File

@ -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<Map<String, Object?>, 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,
};
}
}

View File

@ -269,9 +269,8 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
Future<List<AssetEdit>> 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();
}
}

View File

@ -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<DriftEditImagePage> with Ti
late final Rect _initialCrop;
void initEditor() {
final existingCrop = widget.edits.firstWhereOrNull((edit) => edit.action == AssetEditAction.crop);
final existingCrop = widget.edits.whereType<CropEdit>().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<DriftEditImagePage> with Ti
final edits = <AssetEdit>[];
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);

View File

@ -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<AssetEditsResponseDto?> editAsset(String assetId, List<AssetEdit> 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<void> 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(),
),
};
}
}

View File

@ -31,27 +31,12 @@ CropParameters convertRectToCropParameters(Rect rect, int originalWidth, int ori
AffineMatrix buildAffineFromEdits(List<AssetEdit> edits) {
return AffineMatrix.compose(
edits.map<AffineMatrix>((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(),
);
}

View File

@ -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<AssetEdit> normalizedToEdits(NormalizedTransform transform) {
List<AssetEdit> 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 = <AssetEdit>[
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 = <AssetEdit>[
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 = <AssetEdit>[
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 = <AssetEdit>[
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 = <AssetEdit>[
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 = <AssetEdit>[
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 = <AssetEdit>[
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 = <AssetEdit>[
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 = <AssetEdit>[
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 = <AssetEdit>[
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 = <AssetEdit>[
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 = <AssetEdit>[
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 = <AssetEdit>[
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 = <AssetEdit>[
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 = <AssetEdit>[
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 = <AssetEdit>[
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 = <AssetEdit>[
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 = <AssetEdit>[
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 = <AssetEdit>[
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 = <AssetEdit>[
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 = <AssetEdit>[
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 = <AssetEdit>[
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 = <AssetEdit>[
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);