mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-01 19:17:12 -04: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/splash_screen.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/favorite.page.dart'; | ||||
| import 'package:immich_mobile/pages/library/library.page.dart'; | ||||
| @ -133,6 +135,8 @@ class AppRouter extends _$AppRouter { | ||||
|       page: CreateAlbumRoute.page, | ||||
|       guards: [_authGuard, _duplicateGuard], | ||||
|     ), | ||||
|     AutoRoute(page: EditImageRoute.page), | ||||
|     AutoRoute(page: CropImageRoute.page), | ||||
|     AutoRoute(page: FavoritesRoute.page, guards: [_authGuard, _duplicateGuard]), | ||||
|     AutoRoute(page: AllVideosRoute.page, guards: [_authGuard, _duplicateGuard]), | ||||
|     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) { | ||||
|       return AutoRoutePage<dynamic>( | ||||
|         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 | ||||
| /// [FailedBackupStatusPage] | ||||
| 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/user.provider.dart'; | ||||
| import 'package:immich_mobile/widgets/common/immich_toast.dart'; | ||||
| import 'package:immich_mobile/pages/editing/edit.page.dart'; | ||||
| 
 | ||||
| class BottomGalleryBar extends ConsumerWidget { | ||||
|   final Asset asset; | ||||
| @ -69,6 +70,12 @@ class BottomGalleryBar extends ConsumerWidget { | ||||
|         label: '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) | ||||
|         asset.isArchived | ||||
|             ? BottomNavigationBarItem( | ||||
| @ -280,6 +287,24 @@ class BottomGalleryBar extends ConsumerWidget { | ||||
|       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() { | ||||
|       ref.read(assetProvider.notifier).toggleArchive([asset]); | ||||
|       if (isParent) { | ||||
| @ -343,6 +368,7 @@ class BottomGalleryBar extends ConsumerWidget { | ||||
| 
 | ||||
|     List<Function(int)> actionslist = [ | ||||
|       (_) => shareAsset(), | ||||
|       if (asset.isImage) (_) => handleEdit(), | ||||
|       if (isOwner) (_) => handleArchive(), | ||||
|       if (isOwner && stack.isNotEmpty) (_) => showStackActionItems(), | ||||
|       if (isOwner) (_) => handleDelete(), | ||||
|  | ||||
| @ -273,6 +273,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     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: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|  | ||||
| @ -62,6 +62,8 @@ dependencies: | ||||
|   thumbhash: 0.1.0+1 | ||||
|   async: ^2.11.0 | ||||
| 
 | ||||
|   #image editing packages | ||||
|   crop_image: ^1.0.13 | ||||
|   openapi: | ||||
|     path: openapi | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user