chore: code review changes

chore: code review
This commit is contained in:
bwees 2026-04-01 14:35:46 -05:00
parent 67e3809921
commit 54042ea424
No known key found for this signature in database
14 changed files with 123 additions and 237 deletions

View File

@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
class AspectRatioPreset {
final double? ratio;
final String label;
final IconData icon;
final bool iconRotated;
AspectRatioPreset({required this.ratio, required this.label, required this.icon, this.iconRotated = false});
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is AspectRatioPreset) {
return ratio == other.ratio && label == other.label;
}
return false;
}
@override
int get hashCode => ratio.hashCode ^ label.hashCode;
}
final aspectRatios = <String, AspectRatioPreset>{
'Free': AspectRatioPreset(ratio: null, label: 'Free', icon: Icons.crop_free_rounded),
'1:1': AspectRatioPreset(ratio: 1.0, label: '1:1', icon: Icons.crop_square_rounded),
'16:9': AspectRatioPreset(ratio: 16 / 9, label: '16:9', icon: Icons.crop_16_9_rounded),
'3:2': AspectRatioPreset(ratio: 3 / 2, label: '3:2', icon: Icons.crop_3_2_rounded),
'7:5': AspectRatioPreset(ratio: 7 / 5, label: '7:5', icon: Icons.crop_7_5_rounded),
'9:16': AspectRatioPreset(ratio: 9 / 16, label: '9:16', icon: Icons.crop_16_9_rounded, iconRotated: true),
'2:3': AspectRatioPreset(ratio: 2 / 3, label: '2:3', icon: Icons.crop_3_2_rounded, iconRotated: true),
'5:7': AspectRatioPreset(ratio: 5 / 7, label: '5:7', icon: Icons.crop_7_5_rounded, iconRotated: true),
};

View File

@ -69,7 +69,7 @@ sealed class BaseAsset {
bool get isLocalOnly => storage == AssetState.local;
bool get isRemoteOnly => storage == AssetState.remote;
bool get isEditable => isImage && !isMotionPhoto && this is RemoteAsset;
bool get isEditable => false;
// Overridden in subclasses
AssetState get storage;

View File

@ -43,6 +43,9 @@ class RemoteAsset extends BaseAsset {
@override
String get heroTag => '${localId ?? checksum}_$id';
@override
bool get isEditable => isImage && !isMotionPhoto && !isAnimatedImage;
@override
String toString() {
return '''Asset {

View File

@ -1,6 +1,5 @@
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_edit.model.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
@ -117,12 +116,4 @@ class AssetService {
Future<List<LocalAlbum>> getSourceAlbums(String localAssetId, {BackupSelection? backupSelection}) {
return _localAssetRepository.getSourceAlbums(localAssetId, backupSelection: backupSelection);
}
Future<List<AssetEdit>> getAssetEdits(String assetId) {
return _remoteAssetRepository.getAssetEdits(assetId);
}
Future<void> editAsset(String assetId, List<AssetEdit> edits) {
return _remoteAssetRepository.editAsset(assetId, edits);
}
}

View File

@ -4,7 +4,6 @@ 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/stack.model.dart';
import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.dart';
import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart' hide ExifInfo;
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
@ -12,7 +11,6 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
import 'package:uuid/uuid.dart';
class RemoteAssetRepository extends DriftDatabaseRepository {
final Drift _db;
@ -269,34 +267,11 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
return _db.managers.remoteAssetEntity.count();
}
Future<List<AssetEdit>> getAssetEdits(String assetId) async {
Future<List<AssetEdit>> getAssetEdits(String assetId) {
final query = _db.assetEditEntity.select()
..where((row) => row.assetId.equals(assetId))
..orderBy([(row) => OrderingTerm.asc(row.sequence)]);
return query.map((row) => row.toDto()).get();
}
Future<void> editAsset(String assetId, List<AssetEdit> edits) async {
await _db.transaction(() async {
await _db.batch((batch) async {
// delete existing edits
batch.deleteWhere(_db.assetEditEntity, (row) => row.assetId.equals(assetId));
// insert new edits
for (var i = 0; i < edits.length; i++) {
final edit = edits[i];
final companion = AssetEditEntityCompanion(
id: Value(const Uuid().v4()),
assetId: Value(assetId),
action: Value(edit.action),
parameters: Value(edit.parameters),
sequence: Value(i),
);
batch.insert(_db.assetEditEntity, companion);
}
});
});
}
}

View File

@ -7,7 +7,7 @@ import 'package:crop_image/crop_image.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/constants/aspect_ratios.dart';
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
@ -20,7 +20,6 @@ import 'package:openapi/api.dart' show CropParameters, RotateParameters, MirrorP
@RoutePage()
class DriftEditImagePage extends ConsumerStatefulWidget {
final Image image;
final BaseAsset asset;
final List<AssetEdit> edits;
final ExifInfo exifInfo;
final Future<void> Function(List<AssetEdit> edits) applyEdits;
@ -28,7 +27,6 @@ class DriftEditImagePage extends ConsumerStatefulWidget {
const DriftEditImagePage({
super.key,
required this.image,
required this.asset,
required this.edits,
required this.exifInfo,
required this.applyEdits,
@ -38,12 +36,10 @@ class DriftEditImagePage extends ConsumerStatefulWidget {
ConsumerState<DriftEditImagePage> createState() => _DriftEditImagePageState();
}
typedef AspectRatio = ({double? ratio, String label});
class _DriftEditImagePageState extends ConsumerState<DriftEditImagePage> with TickerProviderStateMixin {
late final CropController cropController;
Duration _rotationAnimationDuration = const Duration(milliseconds: 250);
Duration _rotationAnimationDuration = const Duration(milliseconds: 0);
int _rotationAngle = 0;
bool _flipHorizontal = false;
@ -56,18 +52,6 @@ class _DriftEditImagePageState extends ConsumerState<DriftEditImagePage> with Ti
bool _isApplyingEdits = false;
bool _hasSheetChanges = false;
late final Rect _initialCrop;
final String _selectedSegment = 'transform';
List<AspectRatio> aspectRatios = [
(ratio: null, label: 'Free'),
(ratio: 1.0, label: '1:1'),
(ratio: 16.0 / 9.0, label: '16:9'),
(ratio: 3.0 / 2.0, label: '3:2'),
(ratio: 7.0 / 5.0, label: '7:5'),
(ratio: 9.0 / 16.0, label: '9:16'),
(ratio: 2.0 / 3.0, label: '2:3'),
(ratio: 5.0 / 7.0, label: '5:7'),
];
void initEditor() {
final existingCrop = widget.edits.firstWhereOrNull((edit) => edit.action == AssetEditAction.crop);
@ -83,9 +67,6 @@ class _DriftEditImagePageState extends ConsumerState<DriftEditImagePage> with Ti
cropController = CropController(defaultCrop: crop);
final transform = normalizeTransformEdits(widget.edits);
// dont animate to initial rotation
_rotationAnimationDuration = const Duration(milliseconds: 0);
_rotationAngle = transform.rotation.toInt();
_flipHorizontal = transform.mirrorHorizontal;
@ -160,7 +141,7 @@ class _DriftEditImagePageState extends ConsumerState<DriftEditImagePage> with Ti
void _rotateLeft() {
setState(() {
_rotationAnimationDuration = const Duration(milliseconds: 150);
_rotationAnimationDuration = const Duration(milliseconds: 250);
_rotationAngle -= 90;
_hasSheetChanges = true;
});
@ -168,7 +149,7 @@ class _DriftEditImagePageState extends ConsumerState<DriftEditImagePage> with Ti
void _rotateRight() {
setState(() {
_rotationAnimationDuration = const Duration(milliseconds: 150);
_rotationAnimationDuration = const Duration(milliseconds: 250);
_rotationAngle += 90;
_hasSheetChanges = true;
});
@ -230,30 +211,29 @@ class _DriftEditImagePageState extends ConsumerState<DriftEditImagePage> with Ti
return isCropped || isRotated || isFlipped;
}
Future<bool> _showDiscardChangesDialog() async {
return await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text('editor_discard_edits_title'.tr()),
content: Text('editor_discard_edits_prompt'.tr()),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
style: ButtonStyle(
foregroundColor: WidgetStateProperty.all(context.themeData.colorScheme.onSurfaceVariant),
),
child: Text('cancel'.tr()),
),
TextButton(onPressed: () => Navigator.of(context).pop(true), child: Text('confirm'.tr())),
],
Future<bool?> _showDiscardChangesDialog() {
return showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text('editor_discard_edits_title'.tr()),
content: Text('editor_discard_edits_prompt'.tr()),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
style: ButtonStyle(
foregroundColor: WidgetStateProperty.all(context.themeData.colorScheme.onSurfaceVariant),
),
child: Text('cancel'.tr()),
),
) ??
false;
TextButton(onPressed: () => Navigator.of(context).pop(true), child: Text('confirm'.tr())),
],
),
);
}
Future<void> _handleClose() async {
if (hasUnsavedChanges) {
final shouldDiscard = await _showDiscardChangesDialog();
final shouldDiscard = await _showDiscardChangesDialog() ?? false;
if (shouldDiscard && mounted) {
Navigator.of(context).pop();
}
@ -268,7 +248,7 @@ class _DriftEditImagePageState extends ConsumerState<DriftEditImagePage> with Ti
canPop: !hasUnsavedChanges,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
final shouldDiscard = await _showDiscardChangesDialog();
final shouldDiscard = await _showDiscardChangesDialog() ?? false;
if (shouldDiscard && mounted) {
Navigator.of(context).pop();
}
@ -319,19 +299,10 @@ class _DriftEditImagePageState extends ConsumerState<DriftEditImagePage> with Ti
padding: const EdgeInsets.all(10),
width: (_rotationAngle % 180 == 0) ? baseWidth : baseHeight,
height: (_rotationAngle % 180 == 0) ? baseHeight : baseWidth,
child: FutureBuilder(
future: resolveImage(widget.image.image),
builder: (context, data) {
if (!data.hasData) {
return const Center(child: CircularProgressIndicator());
}
return CropImage(
controller: cropController,
image: widget.image,
gridColor: Colors.white,
);
},
child: CropImage(
controller: cropController,
image: widget.image,
gridColor: Colors.white,
),
),
),
@ -357,48 +328,18 @@ class _DriftEditImagePageState extends ConsumerState<DriftEditImagePage> with Ti
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedCrossFade(
duration: const Duration(milliseconds: 250),
firstCurve: Curves.easeInOut,
secondCurve: Curves.easeInOut,
sizeCurve: Curves.easeInOut,
crossFadeState: _selectedSegment == 'transform'
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild: _TransformControls(
onRotateLeft: _rotateLeft,
onRotateRight: _rotateRight,
onFlipHorizontal: _flipHorizontally,
onFlipVertical: _flipVertically,
onAspectRatioSelected: _applyAspectRatio,
aspectRatio: _aspectRatio,
),
// this will never show since the segmented button is not shown yet
secondChild: const Text("Filters coming soon!"),
_TransformControls(
onRotateLeft: _rotateLeft,
onRotateRight: _rotateRight,
onFlipHorizontal: _flipHorizontally,
onFlipVertical: _flipVertically,
onAspectRatioSelected: _applyAspectRatio,
aspectRatio: _aspectRatio,
),
Padding(
padding: const EdgeInsets.only(bottom: 36, left: 24, right: 24),
child: Row(
children: [
// SegmentedButton(
// segments: [
// const ButtonSegment<String>(
// value: 'transform',
// label: Text('Transform'),
// icon: Icon(Icons.transform),
// ),
// const ButtonSegment<String>(
// value: 'filters',
// label: Text('Filters'),
// icon: Icon(Icons.color_lens),
// ),
// ],
// selected: {selectedSegment},
// onSelectionChanged: (value) => setState(() {
// selectedSegment = value.first;
// }),
// showSelectedIcon: false,
// ),
const Spacer(),
ImmichTextButton(
labelText: 'reset'.tr(),
@ -424,17 +365,11 @@ class _DriftEditImagePageState extends ConsumerState<DriftEditImagePage> with Ti
}
class _AspectRatioButton extends StatelessWidget {
final double? currentAspectRatio;
final double? ratio;
final String label;
final AspectRatioPreset ratio;
final bool isSelected;
final VoidCallback onPressed;
const _AspectRatioButton({
required this.currentAspectRatio,
required this.ratio,
required this.label,
required this.onPressed,
});
const _AspectRatioButton({required this.ratio, required this.isSelected, required this.onPressed});
@override
Widget build(BuildContext context) {
@ -444,22 +379,12 @@ class _AspectRatioButton extends StatelessWidget {
IconButton(
iconSize: 36,
icon: Transform.rotate(
angle: (ratio ?? 1.0) < 1.0 ? pi / 2 : 0,
child: Icon(switch (label) {
'Free' => Icons.crop_free_rounded,
'1:1' => Icons.crop_square_rounded,
'16:9' => Icons.crop_16_9_rounded,
'3:2' => Icons.crop_3_2_rounded,
'7:5' => Icons.crop_7_5_rounded,
'9:16' => Icons.crop_16_9_rounded,
'2:3' => Icons.crop_3_2_rounded,
'5:7' => Icons.crop_7_5_rounded,
_ => Icons.crop_free_rounded,
}, color: currentAspectRatio == ratio ? context.primaryColor : context.themeData.iconTheme.color),
angle: ratio.iconRotated ? pi / 2 : 0,
child: Icon(ratio.icon, color: isSelected ? context.primaryColor : context.themeData.iconTheme.color),
),
onPressed: onPressed,
),
Text(label, style: context.textTheme.displayMedium),
Text(ratio.label, style: context.textTheme.displayMedium),
],
);
}
@ -473,17 +398,6 @@ class _AspectRatioSelector extends StatelessWidget {
@override
Widget build(BuildContext context) {
final aspectRatios = <String, double?>{
'Free': null,
'1:1': 1.0,
'16:9': 16 / 9,
'3:2': 3 / 2,
'7:5': 7 / 5,
'9:16': 9 / 16,
'2:3': 2 / 3,
'5:7': 5 / 7,
};
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
@ -491,10 +405,9 @@ class _AspectRatioSelector extends StatelessWidget {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: _AspectRatioButton(
currentAspectRatio: currentAspectRatio,
ratio: entry.value,
label: entry.key,
onPressed: () => onAspectRatioSelected(entry.value),
isSelected: currentAspectRatio == entry.value.ratio,
onPressed: () => onAspectRatioSelected(entry.value.ratio),
),
);
}).toList(),

View File

@ -63,7 +63,7 @@ class EditImageActionButton extends ConsumerWidget {
}
await context.pushRoute(
DriftEditImageRoute(asset: currentAsset, image: image, edits: edits, exifInfo: exifInfo, applyEdits: editImage),
DriftEditImageRoute(image: image, edits: edits, exifInfo: exifInfo, applyEdits: editImage),
);
}

View File

@ -13,10 +13,11 @@ import 'package:openapi/api.dart';
class RemoteImageProvider extends CancellableImageProvider<RemoteImageProvider>
with CancellableImageProviderMixin<RemoteImageProvider> {
final String url;
final bool edited;
RemoteImageProvider({required this.url});
RemoteImageProvider({required this.url, this.edited = true});
RemoteImageProvider.thumbnail({required String assetId, required String thumbhash, bool edited = true})
RemoteImageProvider.thumbnail({required String assetId, required String thumbhash, this.edited = true})
: url = getThumbnailUrlForRemoteId(assetId, thumbhash: thumbhash, edited: edited);
@override
@ -45,13 +46,13 @@ class RemoteImageProvider extends CancellableImageProvider<RemoteImageProvider>
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is RemoteImageProvider) {
return url == other.url;
return url == other.url && edited == other.edited;
}
return false;
}
@override
int get hashCode => url.hashCode;
int get hashCode => url.hashCode ^ edited.hashCode;
}
class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImageProvider>
@ -148,7 +149,12 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
}
final previewRequest = request = RemoteImageRequest(
uri: getThumbnailUrlForRemoteId(key.assetId, type: AssetMediaSize.preview, thumbhash: key.thumbhash),
uri: getThumbnailUrlForRemoteId(
key.assetId,
type: AssetMediaSize.preview,
thumbhash: key.thumbhash,
edited: key.edited,
),
);
yield* loadRequest(previewRequest, decode, evictOnError: false);
@ -158,7 +164,9 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
}
// always try original for animated, since previews don't support animation
final originalRequest = request = RemoteImageRequest(uri: getOriginalUrlForRemoteId(key.assetId));
final originalRequest = request = RemoteImageRequest(
uri: getOriginalUrlForRemoteId(key.assetId, edited: key.edited),
);
final codec = await loadCodecRequest(originalRequest);
if (codec == null) {
throw StateError('Failed to load animated codec for asset ${key.assetId}');

View File

@ -213,7 +213,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
timeout,
onTimeout: () {
state.socket?.off(event, handler);
throw TimeoutException("Timeout waiting for event: $event");
completer.completeError(TimeoutException("Timeout waiting for event: $event"));
},
);
}

View File

@ -107,23 +107,17 @@ class AssetApiRepository extends ApiRepository {
return _api.updateAsset(assetId, UpdateAssetDto(rating: rating));
}
Future<void> editAsset(String assetId, List<AssetEdit> edits) async {
Future<AssetEditsResponseDto?> editAsset(String assetId, List<AssetEdit> edits) {
final editDtos = edits
.map((edit) {
if (edit.action == AssetEditAction.other) {
return null;
}
return AssetEditActionItemDto(action: edit.action.toDto()!, parameters: edit.parameters);
})
.whereType<AssetEditActionItemDto>()
.where((edit) => edit.action != AssetEditAction.other)
.map((edit) => AssetEditActionItemDto(action: edit.action.toDto()!, parameters: edit.parameters))
.toList();
await _api.editAsset(assetId, AssetEditsCreateDto(edits: editDtos));
return _api.editAsset(assetId, AssetEditsCreateDto(edits: editDtos));
}
Future<void> removeEdits(String assetId) async {
await _api.removeAssetEdits(assetId);
return _api.removeAssetEdits(assetId);
}
}

View File

@ -1009,7 +1009,6 @@ class DriftEditImageRoute extends PageRouteInfo<DriftEditImageRouteArgs> {
DriftEditImageRoute({
Key? key,
required Image image,
required BaseAsset asset,
required List<AssetEdit> edits,
required ExifInfo exifInfo,
required Future<void> Function(List<AssetEdit>) applyEdits,
@ -1019,7 +1018,6 @@ class DriftEditImageRoute extends PageRouteInfo<DriftEditImageRouteArgs> {
args: DriftEditImageRouteArgs(
key: key,
image: image,
asset: asset,
edits: edits,
exifInfo: exifInfo,
applyEdits: applyEdits,
@ -1036,7 +1034,6 @@ class DriftEditImageRoute extends PageRouteInfo<DriftEditImageRouteArgs> {
return DriftEditImagePage(
key: args.key,
image: args.image,
asset: args.asset,
edits: args.edits,
exifInfo: args.exifInfo,
applyEdits: args.applyEdits,
@ -1049,7 +1046,6 @@ class DriftEditImageRouteArgs {
const DriftEditImageRouteArgs({
this.key,
required this.image,
required this.asset,
required this.edits,
required this.exifInfo,
required this.applyEdits,
@ -1059,8 +1055,6 @@ class DriftEditImageRouteArgs {
final Image image;
final BaseAsset asset;
final List<AssetEdit> edits;
final ExifInfo exifInfo;
@ -1069,7 +1063,7 @@ class DriftEditImageRouteArgs {
@override
String toString() {
return 'DriftEditImageRouteArgs{key: $key, image: $image, asset: $asset, edits: $edits, exifInfo: $exifInfo, applyEdits: $applyEdits}';
return 'DriftEditImageRouteArgs{key: $key, image: $image, edits: $edits, exifInfo: $exifInfo, applyEdits: $applyEdits}';
}
}

View File

@ -253,8 +253,6 @@ class ActionService {
} else {
await _assetApiRepository.editAsset(remoteId, edits);
}
await _remoteAssetRepository.editAsset(remoteId, edits);
}
Future<int> _deleteLocalAssets(List<String> localIds) async {

View File

@ -1,6 +1,4 @@
import 'dart:async';
import 'dart:math';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
@ -17,10 +15,10 @@ Rect convertCropParametersToRect(CropParameters parameters, int originalWidth, i
}
CropParameters convertRectToCropParameters(Rect rect, int originalWidth, int originalHeight) {
final x = (rect.left * originalWidth).round();
final y = (rect.top * originalHeight).round();
final width = (rect.width * originalWidth).round();
final height = (rect.height * originalHeight).round();
final x = (rect.left * originalWidth).truncate();
final y = (rect.top * originalHeight).truncate();
final width = (rect.width * originalWidth).truncate();
final height = (rect.height * originalHeight).truncate();
return CropParameters(
x: max(x, 0).clamp(0, originalWidth),
@ -35,12 +33,22 @@ AffineMatrix buildAffineFromEdits(List<AssetEdit> edits) {
edits.map<AffineMatrix>((edit) {
switch (edit.action) {
case AssetEditAction.rotate:
final angleInDegrees = edit.parameters["angle"] as num;
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 axis = edit.parameters["axis"] as String;
return axis == "horizontal" ? AffineMatrix.flipY() : AffineMatrix.flipX();
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();
}
@ -70,28 +78,3 @@ NormalizedTransform normalizeTransformEdits(List<AssetEdit> edits) {
mirrorVertical: isCloseToZero(a) ? b == c : a == -d,
);
}
/// Helper to resolve an ImageProvider to a ui.Image
Future<ui.Image> resolveImage(ImageProvider provider) {
final completer = Completer<ui.Image>();
final stream = provider.resolve(const ImageConfiguration());
late final ImageStreamListener listener;
listener = ImageStreamListener(
(ImageInfo info, bool sync) {
if (!completer.isCompleted) {
completer.complete(info.image);
}
stream.removeListener(listener);
},
onError: (error, stackTrace) {
if (!completer.isCompleted) {
completer.completeError(error, stackTrace);
}
stream.removeListener(listener);
},
);
stream.addListener(listener);
return completer.future;
}

View File

@ -1,14 +1,8 @@
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:crop_image/crop_image.dart';
import 'dart:ui'; // Import the dart:ui library for Rect
import 'package:crop_image/crop_image.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
/// A hook that provides a [CropController] instance.
CropController useCropController({Rect? initialCrop, CropRotation? initialRotation}) {
return useMemoized(
() => CropController(
defaultCrop: initialCrop ?? const Rect.fromLTRB(0, 0, 1, 1),
rotation: initialRotation ?? CropRotation.up,
),
);
CropController useCropController() {
return useMemoized(() => CropController(defaultCrop: const Rect.fromLTRB(0, 0, 1, 1)));
}