mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -05:00 
			
		
		
		
	feat(mobile): adds crop and rotate to mobile (#10989)
* Added Crop Feature * Using LayoutBuilder Fix * Using Immich Colors * Using Immich Text Theme * Chnaging dynamic datatype to nullable * Fix for the retrivel of the image from the cropscreen * Using Hooks State * Small edits * Finals edits * Saving to the mobile * Commented final code * Commented final code * Comments and AutoRoute * Fix AutoRoute Final * Naming tools and Action when made no edits * Updating timeline after edit * chore: lint * format * Light Mode Compatible * fix duplicate page name * Fix Routing * Hiding the Button * lint * remove unused code --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
		
							parent
							
								
									bc8e236598
								
							
						
					
					
						commit
						15503784c8
					
				
							
								
								
									
										203
									
								
								mobile/lib/pages/editing/crop.page.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								mobile/lib/pages/editing/crop.page.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,203 @@
 | 
				
			|||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:crop_image/crop_image.dart';
 | 
				
			||||||
 | 
					import 'package:flutter_hooks/flutter_hooks.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/routing/router.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/utils/hooks/crop_controller_hook.dart';
 | 
				
			||||||
 | 
					import 'edit.page.dart';
 | 
				
			||||||
 | 
					import 'package:auto_route/auto_route.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A widget for cropping an image.
 | 
				
			||||||
 | 
					/// This widget uses [HookWidget] to manage its lifecycle and state. It allows
 | 
				
			||||||
 | 
					/// users to crop an image and then navigate to the [EditImagePage] with the
 | 
				
			||||||
 | 
					/// cropped image.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@RoutePage()
 | 
				
			||||||
 | 
					class CropImagePage extends HookWidget {
 | 
				
			||||||
 | 
					  final Image image;
 | 
				
			||||||
 | 
					  const CropImagePage({super.key, required this.image});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    final cropController = useCropController();
 | 
				
			||||||
 | 
					    final aspectRatio = useState<double?>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Scaffold(
 | 
				
			||||||
 | 
					      appBar: AppBar(
 | 
				
			||||||
 | 
					        backgroundColor: Theme.of(context).bottomAppBarTheme.color,
 | 
				
			||||||
 | 
					        leading: CloseButton(color: Theme.of(context).iconTheme.color),
 | 
				
			||||||
 | 
					        actions: [
 | 
				
			||||||
 | 
					          IconButton(
 | 
				
			||||||
 | 
					            icon: Icon(
 | 
				
			||||||
 | 
					              Icons.done_rounded,
 | 
				
			||||||
 | 
					              color: Theme.of(context).iconTheme.color,
 | 
				
			||||||
 | 
					              size: 24,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            onPressed: () async {
 | 
				
			||||||
 | 
					              final croppedImage = await cropController.croppedImage();
 | 
				
			||||||
 | 
					              context.pushRoute(EditImageRoute(image: croppedImage));
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      body: LayoutBuilder(
 | 
				
			||||||
 | 
					        builder: (BuildContext context, BoxConstraints constraints) {
 | 
				
			||||||
 | 
					          return Column(
 | 
				
			||||||
 | 
					            children: [
 | 
				
			||||||
 | 
					              Container(
 | 
				
			||||||
 | 
					                padding: const EdgeInsets.only(top: 20),
 | 
				
			||||||
 | 
					                width: double.infinity,
 | 
				
			||||||
 | 
					                height: constraints.maxHeight * 0.6,
 | 
				
			||||||
 | 
					                child: CropImage(
 | 
				
			||||||
 | 
					                  controller: cropController,
 | 
				
			||||||
 | 
					                  image: image,
 | 
				
			||||||
 | 
					                  gridColor: Colors.white,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              Expanded(
 | 
				
			||||||
 | 
					                child: Container(
 | 
				
			||||||
 | 
					                  width: double.infinity,
 | 
				
			||||||
 | 
					                  decoration: BoxDecoration(
 | 
				
			||||||
 | 
					                    color: Theme.of(context).bottomAppBarTheme.color,
 | 
				
			||||||
 | 
					                    borderRadius: const BorderRadius.only(
 | 
				
			||||||
 | 
					                      topLeft: Radius.circular(20),
 | 
				
			||||||
 | 
					                      topRight: Radius.circular(20),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  child: Center(
 | 
				
			||||||
 | 
					                    child: Column(
 | 
				
			||||||
 | 
					                      mainAxisAlignment: MainAxisAlignment.center,
 | 
				
			||||||
 | 
					                      children: [
 | 
				
			||||||
 | 
					                        Padding(
 | 
				
			||||||
 | 
					                          padding: const EdgeInsets.only(
 | 
				
			||||||
 | 
					                            left: 20,
 | 
				
			||||||
 | 
					                            right: 20,
 | 
				
			||||||
 | 
					                            bottom: 10,
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                          child: Row(
 | 
				
			||||||
 | 
					                            mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
				
			||||||
 | 
					                            children: [
 | 
				
			||||||
 | 
					                              IconButton(
 | 
				
			||||||
 | 
					                                icon: Icon(
 | 
				
			||||||
 | 
					                                  Icons.rotate_left,
 | 
				
			||||||
 | 
					                                  color: Theme.of(context).iconTheme.color,
 | 
				
			||||||
 | 
					                                ),
 | 
				
			||||||
 | 
					                                onPressed: () {
 | 
				
			||||||
 | 
					                                  cropController.rotateLeft();
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                              IconButton(
 | 
				
			||||||
 | 
					                                icon: Icon(
 | 
				
			||||||
 | 
					                                  Icons.rotate_right,
 | 
				
			||||||
 | 
					                                  color: Theme.of(context).iconTheme.color,
 | 
				
			||||||
 | 
					                                ),
 | 
				
			||||||
 | 
					                                onPressed: () {
 | 
				
			||||||
 | 
					                                  cropController.rotateRight();
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                            ],
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                        Row(
 | 
				
			||||||
 | 
					                          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
 | 
				
			||||||
 | 
					                          children: <Widget>[
 | 
				
			||||||
 | 
					                            _AspectRatioButton(
 | 
				
			||||||
 | 
					                              cropController: cropController,
 | 
				
			||||||
 | 
					                              aspectRatio: aspectRatio,
 | 
				
			||||||
 | 
					                              ratio: null,
 | 
				
			||||||
 | 
					                              label: 'Free',
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            _AspectRatioButton(
 | 
				
			||||||
 | 
					                              cropController: cropController,
 | 
				
			||||||
 | 
					                              aspectRatio: aspectRatio,
 | 
				
			||||||
 | 
					                              ratio: 1.0,
 | 
				
			||||||
 | 
					                              label: '1:1',
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            _AspectRatioButton(
 | 
				
			||||||
 | 
					                              cropController: cropController,
 | 
				
			||||||
 | 
					                              aspectRatio: aspectRatio,
 | 
				
			||||||
 | 
					                              ratio: 16.0 / 9.0,
 | 
				
			||||||
 | 
					                              label: '16:9',
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            _AspectRatioButton(
 | 
				
			||||||
 | 
					                              cropController: cropController,
 | 
				
			||||||
 | 
					                              aspectRatio: aspectRatio,
 | 
				
			||||||
 | 
					                              ratio: 3.0 / 2.0,
 | 
				
			||||||
 | 
					                              label: '3:2',
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            _AspectRatioButton(
 | 
				
			||||||
 | 
					                              cropController: cropController,
 | 
				
			||||||
 | 
					                              aspectRatio: aspectRatio,
 | 
				
			||||||
 | 
					                              ratio: 7.0 / 5.0,
 | 
				
			||||||
 | 
					                              label: '7:5',
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                          ],
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ],
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _AspectRatioButton extends StatelessWidget {
 | 
				
			||||||
 | 
					  final CropController cropController;
 | 
				
			||||||
 | 
					  final ValueNotifier<double?> aspectRatio;
 | 
				
			||||||
 | 
					  final double? ratio;
 | 
				
			||||||
 | 
					  final String label;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const _AspectRatioButton({
 | 
				
			||||||
 | 
					    required this.cropController,
 | 
				
			||||||
 | 
					    required this.aspectRatio,
 | 
				
			||||||
 | 
					    required this.ratio,
 | 
				
			||||||
 | 
					    required this.label,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    IconData iconData;
 | 
				
			||||||
 | 
					    switch (label) {
 | 
				
			||||||
 | 
					      case 'Free':
 | 
				
			||||||
 | 
					        iconData = Icons.crop_free_rounded;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case '1:1':
 | 
				
			||||||
 | 
					        iconData = Icons.crop_square_rounded;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case '16:9':
 | 
				
			||||||
 | 
					        iconData = Icons.crop_16_9_rounded;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case '3:2':
 | 
				
			||||||
 | 
					        iconData = Icons.crop_3_2_rounded;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case '7:5':
 | 
				
			||||||
 | 
					        iconData = Icons.crop_7_5_rounded;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        iconData = Icons.crop_free_rounded;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Column(
 | 
				
			||||||
 | 
					      mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
 | 
					      children: [
 | 
				
			||||||
 | 
					        IconButton(
 | 
				
			||||||
 | 
					          icon: Icon(
 | 
				
			||||||
 | 
					            iconData,
 | 
				
			||||||
 | 
					            color: aspectRatio.value == ratio
 | 
				
			||||||
 | 
					                ? Colors.indigo
 | 
				
			||||||
 | 
					                : Theme.of(context).iconTheme.color,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          onPressed: () {
 | 
				
			||||||
 | 
					            aspectRatio.value = ratio;
 | 
				
			||||||
 | 
					            cropController.aspectRatio = ratio;
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        Text(label, style: Theme.of(context).textTheme.bodyMedium),
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										158
									
								
								mobile/lib/pages/editing/edit.page.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								mobile/lib/pages/editing/edit.page.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,158 @@
 | 
				
			|||||||
 | 
					import 'dart:io';
 | 
				
			||||||
 | 
					import 'dart:typed_data';
 | 
				
			||||||
 | 
					import 'dart:async';
 | 
				
			||||||
 | 
					import 'dart:ui';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:fluttertoast/fluttertoast.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/entities/asset.entity.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/widgets/common/immich_image.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/widgets/common/immich_toast.dart';
 | 
				
			||||||
 | 
					import 'package:auto_route/auto_route.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/routing/router.dart';
 | 
				
			||||||
 | 
					import 'package:photo_manager/photo_manager.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/providers/album/album.provider.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A stateless widget that provides functionality for editing an image.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// This widget allows users to edit an image provided either as an [Asset] or
 | 
				
			||||||
 | 
					/// directly as an [Image]. It ensures that exactly one of these is provided.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// It also includes a conversion method to convert an [Image] to a [Uint8List] to save the image on the user's phone
 | 
				
			||||||
 | 
					/// They automatically navigate to the [HomePage] with the edited image saved and they eventually get backed up to the server.
 | 
				
			||||||
 | 
					@immutable
 | 
				
			||||||
 | 
					@RoutePage()
 | 
				
			||||||
 | 
					class EditImagePage extends ConsumerWidget {
 | 
				
			||||||
 | 
					  final Asset? asset;
 | 
				
			||||||
 | 
					  final Image? image;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const EditImagePage({
 | 
				
			||||||
 | 
					    super.key,
 | 
				
			||||||
 | 
					    this.image,
 | 
				
			||||||
 | 
					    this.asset,
 | 
				
			||||||
 | 
					  }) : assert(
 | 
				
			||||||
 | 
					          (image != null && asset == null) || (image == null && asset != null),
 | 
				
			||||||
 | 
					          'Must supply one of asset or image',
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<Uint8List> _imageToUint8List(Image image) async {
 | 
				
			||||||
 | 
					    final Completer<Uint8List> completer = Completer();
 | 
				
			||||||
 | 
					    image.image.resolve(const ImageConfiguration()).addListener(
 | 
				
			||||||
 | 
					          ImageStreamListener(
 | 
				
			||||||
 | 
					            (ImageInfo info, bool _) {
 | 
				
			||||||
 | 
					              info.image
 | 
				
			||||||
 | 
					                  .toByteData(format: ImageByteFormat.png)
 | 
				
			||||||
 | 
					                  .then((byteData) {
 | 
				
			||||||
 | 
					                if (byteData != null) {
 | 
				
			||||||
 | 
					                  completer.complete(byteData.buffer.asUint8List());
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                  completer.completeError('Failed to convert image to bytes');
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              });
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            onError: (exception, stackTrace) =>
 | 
				
			||||||
 | 
					                completer.completeError(exception),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    return completer.future;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
 | 
					    final ImageProvider provider = (asset != null)
 | 
				
			||||||
 | 
					        ? ImmichImage.imageProvider(asset: asset!)
 | 
				
			||||||
 | 
					        : (image != null)
 | 
				
			||||||
 | 
					            ? image!.image
 | 
				
			||||||
 | 
					            : throw Exception('Invalid image source type');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final Image imageWidget = (asset != null)
 | 
				
			||||||
 | 
					        ? Image(image: ImmichImage.imageProvider(asset: asset!))
 | 
				
			||||||
 | 
					        : (image != null)
 | 
				
			||||||
 | 
					            ? image!
 | 
				
			||||||
 | 
					            : throw Exception('Invalid image source type');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Scaffold(
 | 
				
			||||||
 | 
					      appBar: AppBar(
 | 
				
			||||||
 | 
					        backgroundColor: Theme.of(context).appBarTheme.backgroundColor,
 | 
				
			||||||
 | 
					        leading: IconButton(
 | 
				
			||||||
 | 
					          icon: Icon(
 | 
				
			||||||
 | 
					            Icons.close_rounded,
 | 
				
			||||||
 | 
					            color: Theme.of(context).iconTheme.color,
 | 
				
			||||||
 | 
					            size: 24,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          onPressed: () =>
 | 
				
			||||||
 | 
					              Navigator.of(context).popUntil((route) => route.isFirst),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        actions: <Widget>[
 | 
				
			||||||
 | 
					          if (image != null)
 | 
				
			||||||
 | 
					            TextButton(
 | 
				
			||||||
 | 
					              onPressed: () async {
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                  final Uint8List imageData = await _imageToUint8List(image!);
 | 
				
			||||||
 | 
					                  ImmichToast.show(
 | 
				
			||||||
 | 
					                    durationInSecond: 3,
 | 
				
			||||||
 | 
					                    context: context,
 | 
				
			||||||
 | 
					                    msg: 'Image Saved!',
 | 
				
			||||||
 | 
					                    gravity: ToastGravity.CENTER,
 | 
				
			||||||
 | 
					                  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  await PhotoManager.editor
 | 
				
			||||||
 | 
					                      .saveImage(imageData, title: "_edited.jpg");
 | 
				
			||||||
 | 
					                  await ref.read(albumProvider.notifier).getDeviceAlbums();
 | 
				
			||||||
 | 
					                  Navigator.of(context).popUntil((route) => route.isFirst);
 | 
				
			||||||
 | 
					                } catch (e) {
 | 
				
			||||||
 | 
					                  ImmichToast.show(
 | 
				
			||||||
 | 
					                    durationInSecond: 6,
 | 
				
			||||||
 | 
					                    context: context,
 | 
				
			||||||
 | 
					                    msg: 'Error: ${e.toString()}',
 | 
				
			||||||
 | 
					                    gravity: ToastGravity.BOTTOM,
 | 
				
			||||||
 | 
					                  );
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              child: Text(
 | 
				
			||||||
 | 
					                'Save to gallery',
 | 
				
			||||||
 | 
					                style: Theme.of(context).textTheme.displayMedium,
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      body: Column(
 | 
				
			||||||
 | 
					        children: <Widget>[
 | 
				
			||||||
 | 
					          Expanded(
 | 
				
			||||||
 | 
					            child: Image(image: provider),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          Container(
 | 
				
			||||||
 | 
					            height: 80,
 | 
				
			||||||
 | 
					            color: Theme.of(context).bottomAppBarTheme.color,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      bottomNavigationBar: Container(
 | 
				
			||||||
 | 
					        height: 80,
 | 
				
			||||||
 | 
					        margin: const EdgeInsets.only(bottom: 20, right: 10, left: 10, top: 10),
 | 
				
			||||||
 | 
					        decoration: BoxDecoration(
 | 
				
			||||||
 | 
					          color: Theme.of(context).bottomAppBarTheme.color,
 | 
				
			||||||
 | 
					          borderRadius: BorderRadius.circular(30),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        child: Column(
 | 
				
			||||||
 | 
					          mainAxisAlignment: MainAxisAlignment.center,
 | 
				
			||||||
 | 
					          children: <Widget>[
 | 
				
			||||||
 | 
					            IconButton(
 | 
				
			||||||
 | 
					              icon: Icon(
 | 
				
			||||||
 | 
					                Platform.isAndroid
 | 
				
			||||||
 | 
					                    ? Icons.crop_rotate_rounded
 | 
				
			||||||
 | 
					                    : Icons.crop_rotate_rounded,
 | 
				
			||||||
 | 
					                color: Theme.of(context).iconTheme.color,
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              onPressed: () {
 | 
				
			||||||
 | 
					                context.pushRoute(CropImageRoute(image: imageWidget));
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            Text('Crop', style: Theme.of(context).textTheme.displayMedium),
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -28,6 +28,8 @@ import 'package:immich_mobile/pages/common/headers_settings.page.dart';
 | 
				
			|||||||
import 'package:immich_mobile/pages/common/settings.page.dart';
 | 
					import 'package:immich_mobile/pages/common/settings.page.dart';
 | 
				
			||||||
import 'package:immich_mobile/pages/common/splash_screen.page.dart';
 | 
					import 'package:immich_mobile/pages/common/splash_screen.page.dart';
 | 
				
			||||||
import 'package:immich_mobile/pages/common/tab_controller.page.dart';
 | 
					import 'package:immich_mobile/pages/common/tab_controller.page.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/pages/editing/edit.page.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/pages/editing/crop.page.dart';
 | 
				
			||||||
import 'package:immich_mobile/pages/library/archive.page.dart';
 | 
					import 'package:immich_mobile/pages/library/archive.page.dart';
 | 
				
			||||||
import 'package:immich_mobile/pages/library/favorite.page.dart';
 | 
					import 'package:immich_mobile/pages/library/favorite.page.dart';
 | 
				
			||||||
import 'package:immich_mobile/pages/library/library.page.dart';
 | 
					import 'package:immich_mobile/pages/library/library.page.dart';
 | 
				
			||||||
@ -133,6 +135,8 @@ class AppRouter extends _$AppRouter {
 | 
				
			|||||||
      page: CreateAlbumRoute.page,
 | 
					      page: CreateAlbumRoute.page,
 | 
				
			||||||
      guards: [_authGuard, _duplicateGuard],
 | 
					      guards: [_authGuard, _duplicateGuard],
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
 | 
					    AutoRoute(page: EditImageRoute.page),
 | 
				
			||||||
 | 
					    AutoRoute(page: CropImageRoute.page),
 | 
				
			||||||
    AutoRoute(page: FavoritesRoute.page, guards: [_authGuard, _duplicateGuard]),
 | 
					    AutoRoute(page: FavoritesRoute.page, guards: [_authGuard, _duplicateGuard]),
 | 
				
			||||||
    AutoRoute(page: AllVideosRoute.page, guards: [_authGuard, _duplicateGuard]),
 | 
					    AutoRoute(page: AllVideosRoute.page, guards: [_authGuard, _duplicateGuard]),
 | 
				
			||||||
    AutoRoute(
 | 
					    AutoRoute(
 | 
				
			||||||
 | 
				
			|||||||
@ -165,6 +165,28 @@ abstract class _$AppRouter extends RootStackRouter {
 | 
				
			|||||||
        ),
 | 
					        ),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    CropImageRoute.name: (routeData) {
 | 
				
			||||||
 | 
					      final args = routeData.argsAs<CropImageRouteArgs>();
 | 
				
			||||||
 | 
					      return AutoRoutePage<dynamic>(
 | 
				
			||||||
 | 
					        routeData: routeData,
 | 
				
			||||||
 | 
					        child: CropImagePage(
 | 
				
			||||||
 | 
					          key: args.key,
 | 
				
			||||||
 | 
					          image: args.image,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    EditImageRoute.name: (routeData) {
 | 
				
			||||||
 | 
					      final args = routeData.argsAs<EditImageRouteArgs>(
 | 
				
			||||||
 | 
					          orElse: () => const EditImageRouteArgs());
 | 
				
			||||||
 | 
					      return AutoRoutePage<dynamic>(
 | 
				
			||||||
 | 
					        routeData: routeData,
 | 
				
			||||||
 | 
					        child: EditImagePage(
 | 
				
			||||||
 | 
					          key: args.key,
 | 
				
			||||||
 | 
					          image: args.image,
 | 
				
			||||||
 | 
					          asset: args.asset,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    FailedBackupStatusRoute.name: (routeData) {
 | 
					    FailedBackupStatusRoute.name: (routeData) {
 | 
				
			||||||
      return AutoRoutePage<dynamic>(
 | 
					      return AutoRoutePage<dynamic>(
 | 
				
			||||||
        routeData: routeData,
 | 
					        routeData: routeData,
 | 
				
			||||||
@ -836,6 +858,87 @@ class CreateAlbumRouteArgs {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// generated route for
 | 
				
			||||||
 | 
					/// [CropImagePage]
 | 
				
			||||||
 | 
					class CropImageRoute extends PageRouteInfo<CropImageRouteArgs> {
 | 
				
			||||||
 | 
					  CropImageRoute({
 | 
				
			||||||
 | 
					    Key? key,
 | 
				
			||||||
 | 
					    required Image image,
 | 
				
			||||||
 | 
					    List<PageRouteInfo>? children,
 | 
				
			||||||
 | 
					  }) : super(
 | 
				
			||||||
 | 
					          CropImageRoute.name,
 | 
				
			||||||
 | 
					          args: CropImageRouteArgs(
 | 
				
			||||||
 | 
					            key: key,
 | 
				
			||||||
 | 
					            image: image,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          initialChildren: children,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static const String name = 'CropImageRoute';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static const PageInfo<CropImageRouteArgs> page =
 | 
				
			||||||
 | 
					      PageInfo<CropImageRouteArgs>(name);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CropImageRouteArgs {
 | 
				
			||||||
 | 
					  const CropImageRouteArgs({
 | 
				
			||||||
 | 
					    this.key,
 | 
				
			||||||
 | 
					    required this.image,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final Key? key;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final Image image;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String toString() {
 | 
				
			||||||
 | 
					    return 'CropImageRouteArgs{key: $key, image: $image}';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// generated route for
 | 
				
			||||||
 | 
					/// [EditImagePage]
 | 
				
			||||||
 | 
					class EditImageRoute extends PageRouteInfo<EditImageRouteArgs> {
 | 
				
			||||||
 | 
					  EditImageRoute({
 | 
				
			||||||
 | 
					    Key? key,
 | 
				
			||||||
 | 
					    Image? image,
 | 
				
			||||||
 | 
					    Asset? asset,
 | 
				
			||||||
 | 
					    List<PageRouteInfo>? children,
 | 
				
			||||||
 | 
					  }) : super(
 | 
				
			||||||
 | 
					          EditImageRoute.name,
 | 
				
			||||||
 | 
					          args: EditImageRouteArgs(
 | 
				
			||||||
 | 
					            key: key,
 | 
				
			||||||
 | 
					            image: image,
 | 
				
			||||||
 | 
					            asset: asset,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          initialChildren: children,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static const String name = 'EditImageRoute';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static const PageInfo<EditImageRouteArgs> page =
 | 
				
			||||||
 | 
					      PageInfo<EditImageRouteArgs>(name);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class EditImageRouteArgs {
 | 
				
			||||||
 | 
					  const EditImageRouteArgs({
 | 
				
			||||||
 | 
					    this.key,
 | 
				
			||||||
 | 
					    this.image,
 | 
				
			||||||
 | 
					    this.asset,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final Key? key;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final Image? image;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final Asset? asset;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String toString() {
 | 
				
			||||||
 | 
					    return 'EditImageRouteArgs{key: $key, image: $image, asset: $asset}';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// generated route for
 | 
					/// generated route for
 | 
				
			||||||
/// [FailedBackupStatusPage]
 | 
					/// [FailedBackupStatusPage]
 | 
				
			||||||
class FailedBackupStatusRoute extends PageRouteInfo<void> {
 | 
					class FailedBackupStatusRoute extends PageRouteInfo<void> {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										12
									
								
								mobile/lib/utils/hooks/crop_controller_hook.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								mobile/lib/utils/hooks/crop_controller_hook.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					import 'package:flutter_hooks/flutter_hooks.dart';
 | 
				
			||||||
 | 
					import 'package:crop_image/crop_image.dart';
 | 
				
			||||||
 | 
					import 'dart:ui'; // Import the dart:ui library for Rect
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A hook that provides a [CropController] instance.
 | 
				
			||||||
 | 
					CropController useCropController() {
 | 
				
			||||||
 | 
					  return useMemoized(
 | 
				
			||||||
 | 
					    () => CropController(
 | 
				
			||||||
 | 
					      defaultCrop: const Rect.fromLTRB(0.1, 0.1, 0.9, 0.9),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -21,6 +21,7 @@ import 'package:immich_mobile/providers/asset.provider.dart';
 | 
				
			|||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
 | 
					import 'package:immich_mobile/providers/server_info.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/providers/user.provider.dart';
 | 
					import 'package:immich_mobile/providers/user.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
 | 
					import 'package:immich_mobile/widgets/common/immich_toast.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/pages/editing/edit.page.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BottomGalleryBar extends ConsumerWidget {
 | 
					class BottomGalleryBar extends ConsumerWidget {
 | 
				
			||||||
  final Asset asset;
 | 
					  final Asset asset;
 | 
				
			||||||
@ -69,6 +70,12 @@ class BottomGalleryBar extends ConsumerWidget {
 | 
				
			|||||||
        label: 'control_bottom_app_bar_share'.tr(),
 | 
					        label: 'control_bottom_app_bar_share'.tr(),
 | 
				
			||||||
        tooltip: 'control_bottom_app_bar_share'.tr(),
 | 
					        tooltip: 'control_bottom_app_bar_share'.tr(),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
 | 
					      if (asset.isImage)
 | 
				
			||||||
 | 
					        BottomNavigationBarItem(
 | 
				
			||||||
 | 
					          icon: const Icon(Icons.edit_outlined),
 | 
				
			||||||
 | 
					          label: 'control_bottom_app_bar_edit'.tr(),
 | 
				
			||||||
 | 
					          tooltip: 'control_bottom_app_bar_edit'.tr(),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
      if (isOwner)
 | 
					      if (isOwner)
 | 
				
			||||||
        asset.isArchived
 | 
					        asset.isArchived
 | 
				
			||||||
            ? BottomNavigationBarItem(
 | 
					            ? BottomNavigationBarItem(
 | 
				
			||||||
@ -280,6 +287,24 @@ class BottomGalleryBar extends ConsumerWidget {
 | 
				
			|||||||
      ref.read(imageViewerStateProvider.notifier).shareAsset(asset, context);
 | 
					      ref.read(imageViewerStateProvider.notifier).shareAsset(asset, context);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    void handleEdit() async {
 | 
				
			||||||
 | 
					      if (asset.isOffline) {
 | 
				
			||||||
 | 
					        ImmichToast.show(
 | 
				
			||||||
 | 
					          durationInSecond: 1,
 | 
				
			||||||
 | 
					          context: context,
 | 
				
			||||||
 | 
					          msg: 'asset_action_edit_err_offline'.tr(),
 | 
				
			||||||
 | 
					          gravity: ToastGravity.BOTTOM,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      Navigator.of(context).push(
 | 
				
			||||||
 | 
					        MaterialPageRoute(
 | 
				
			||||||
 | 
					          builder: (context) =>
 | 
				
			||||||
 | 
					              EditImagePage(asset: asset), // Send the Asset object
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    handleArchive() {
 | 
					    handleArchive() {
 | 
				
			||||||
      ref.read(assetProvider.notifier).toggleArchive([asset]);
 | 
					      ref.read(assetProvider.notifier).toggleArchive([asset]);
 | 
				
			||||||
      if (isParent) {
 | 
					      if (isParent) {
 | 
				
			||||||
@ -343,6 +368,7 @@ class BottomGalleryBar extends ConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    List<Function(int)> actionslist = [
 | 
					    List<Function(int)> actionslist = [
 | 
				
			||||||
      (_) => shareAsset(),
 | 
					      (_) => shareAsset(),
 | 
				
			||||||
 | 
					      if (asset.isImage) (_) => handleEdit(),
 | 
				
			||||||
      if (isOwner) (_) => handleArchive(),
 | 
					      if (isOwner) (_) => handleArchive(),
 | 
				
			||||||
      if (isOwner && stack.isNotEmpty) (_) => showStackActionItems(),
 | 
					      if (isOwner && stack.isNotEmpty) (_) => showStackActionItems(),
 | 
				
			||||||
      if (isOwner) (_) => handleDelete(),
 | 
					      if (isOwner) (_) => handleDelete(),
 | 
				
			||||||
 | 
				
			|||||||
@ -273,6 +273,14 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.1.1"
 | 
					    version: "3.1.1"
 | 
				
			||||||
 | 
					  crop_image:
 | 
				
			||||||
 | 
					    dependency: "direct main"
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: crop_image
 | 
				
			||||||
 | 
					      sha256: "6cf20655ecbfba99c369d43ec7adcfa49bf135af88fb75642173d6224a95d3f1"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "1.0.13"
 | 
				
			||||||
  cross_file:
 | 
					  cross_file:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
 | 
				
			|||||||
@ -62,6 +62,8 @@ dependencies:
 | 
				
			|||||||
  thumbhash: 0.1.0+1
 | 
					  thumbhash: 0.1.0+1
 | 
				
			||||||
  async: ^2.11.0
 | 
					  async: ^2.11.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  #image editing packages
 | 
				
			||||||
 | 
					  crop_image: ^1.0.13
 | 
				
			||||||
  openapi:
 | 
					  openapi:
 | 
				
			||||||
    path: openapi
 | 
					    path: openapi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user