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/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