diff --git a/mobile/lib/presentation/pages/editing/drift_crop.page.dart b/mobile/lib/presentation/pages/editing/drift_crop.page.dart new file mode 100644 index 0000000000..5b14292aa2 --- /dev/null +++ b/mobile/lib/presentation/pages/editing/drift_crop.page.dart @@ -0,0 +1,174 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:crop_image/crop_image.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/utils/hooks/crop_controller_hook.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 DriftCropImagePage extends HookWidget { + final Image image; + final BaseAsset asset; + const DriftCropImagePage({super.key, required this.image, required this.asset}); + + @override + Widget build(BuildContext context) { + final cropController = useCropController(); + final aspectRatio = useState(null); + + return Scaffold( + appBar: AppBar( + backgroundColor: context.scaffoldBackgroundColor, + title: Text("crop".tr()), + leading: CloseButton(color: context.primaryColor), + actions: [ + IconButton( + icon: Icon(Icons.done_rounded, color: context.primaryColor, size: 24), + onPressed: () async { + final croppedImage = await cropController.croppedImage(); + context.pushRoute(DriftEditImageRoute(asset: asset, image: croppedImage, isEdited: true)); + }, + ), + ], + ), + backgroundColor: context.scaffoldBackgroundColor, + body: SafeArea( + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Column( + children: [ + Container( + padding: const EdgeInsets.only(top: 20), + width: constraints.maxWidth * 0.9, + height: constraints.maxHeight * 0.6, + child: CropImage(controller: cropController, image: image, gridColor: Colors.white), + ), + Expanded( + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: context.scaffoldBackgroundColor, + 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: context.themeData.iconTheme.color), + onPressed: () { + cropController.rotateLeft(); + }, + ), + IconButton( + icon: Icon(Icons.rotate_right, color: context.themeData.iconTheme.color), + onPressed: () { + cropController.rotateRight(); + }, + ), + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _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 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) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: Icon(switch (label) { + 'Free' => Icons.crop_free_rounded, + '1:1' => Icons.crop_square_rounded, + '16:9' => Icons.crop_16_9_rounded, + '3:2' => Icons.crop_3_2_rounded, + '7:5' => Icons.crop_7_5_rounded, + _ => Icons.crop_free_rounded, + }, color: aspectRatio.value == ratio ? context.primaryColor : context.themeData.iconTheme.color), + onPressed: () { + cropController.crop = const Rect.fromLTRB(0.1, 0.1, 0.9, 0.9); + aspectRatio.value = ratio; + cropController.aspectRatio = ratio; + }, + ), + Text(label, style: context.textTheme.displayMedium), + ], + ); + } +} diff --git a/mobile/lib/presentation/pages/editing/drift_edit.page.dart b/mobile/lib/presentation/pages/editing/drift_edit.page.dart new file mode 100644 index 0000000000..da62d49b49 --- /dev/null +++ b/mobile/lib/presentation/pages/editing/drift_edit.page.dart @@ -0,0 +1,165 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/background_sync.provider.dart'; +import 'package:immich_mobile/repositories/file_media.repository.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/services/upload.service.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as p; + +/// 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 DriftEditImagePage extends ConsumerWidget { + final BaseAsset asset; + final Image image; + final bool isEdited; + + const DriftEditImagePage({super.key, required this.asset, required this.image, required this.isEdited}); + Future _imageToUint8List(Image image) async { + final Completer 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; + } + + Future _saveEditedImage(BuildContext context, BaseAsset asset, Image image, WidgetRef ref) async { + try { + final Uint8List imageData = await _imageToUint8List(image); + LocalAsset? localAsset; + + try { + localAsset = await ref + .read(fileMediaRepositoryProvider) + .saveLocalAsset(imageData, title: "${p.withoutExtension(asset.name)}_edited.jpg"); + } on PlatformException catch (e) { + // OS might not return the saved image back, so we handle that gracefully + // This can happen if app does not have full library access + Logger("SaveEditedImage").warning("Failed to retrieve the saved image back from OS", e); + } + + ref.read(backgroundSyncProvider).syncLocal(full: true); + context.navigator.popUntil((route) => route.isFirst); + ImmichToast.show(durationInSecond: 3, context: context, msg: 'Image Saved!'); + + if (localAsset == null) { + return; + } + + await ref.read(uploadServiceProvider).manualBackup([localAsset]); + } catch (e) { + ImmichToast.show( + durationInSecond: 6, + context: context, + msg: "error_saving_image".tr(namedArgs: {'error': e.toString()}), + ); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + appBar: AppBar( + title: Text("edit".tr()), + backgroundColor: context.scaffoldBackgroundColor, + leading: IconButton( + icon: Icon(Icons.close_rounded, color: context.primaryColor, size: 24), + onPressed: () => context.navigator.popUntil((route) => route.isFirst), + ), + actions: [ + TextButton( + onPressed: isEdited ? () => _saveEditedImage(context, asset, image, ref) : null, + child: Text("save_to_gallery".tr(), style: TextStyle(color: isEdited ? context.primaryColor : Colors.grey)), + ), + ], + ), + backgroundColor: context.scaffoldBackgroundColor, + body: Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxHeight: context.height * 0.7, maxWidth: context.width * 0.9), + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(7)), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.2), + spreadRadius: 2, + blurRadius: 10, + offset: const Offset(0, 3), + ), + ], + ), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(7)), + child: Image(image: image.image, fit: BoxFit.contain), + ), + ), + ), + ), + bottomNavigationBar: Container( + height: 70, + margin: const EdgeInsets.only(bottom: 60, right: 10, left: 10, top: 10), + decoration: BoxDecoration( + color: context.scaffoldBackgroundColor, + borderRadius: const BorderRadius.all(Radius.circular(30)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: Icon(Icons.crop_rotate_rounded, color: context.themeData.iconTheme.color, size: 25), + onPressed: () { + context.pushRoute(DriftCropImageRoute(asset: asset, image: image)); + }, + ), + Text("crop".tr(), style: context.textTheme.displayMedium), + ], + ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: Icon(Icons.filter, color: context.themeData.iconTheme.color, size: 25), + onPressed: () { + context.pushRoute(DriftFilterImageRoute(asset: asset, image: image)); + }, + ), + Text("filter".tr(), style: context.textTheme.displayMedium), + ], + ), + ], + ), + ), + ); + } +} diff --git a/mobile/lib/presentation/pages/editing/drift_filter.page.dart b/mobile/lib/presentation/pages/editing/drift_filter.page.dart new file mode 100644 index 0000000000..75c3f81de2 --- /dev/null +++ b/mobile/lib/presentation/pages/editing/drift_filter.page.dart @@ -0,0 +1,159 @@ +import 'dart:async'; +import 'dart:ui' as ui; + +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:immich_mobile/constants/filters.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/routing/router.dart'; + +/// A widget for filtering an image. +/// This widget uses [HookWidget] to manage its lifecycle and state. It allows +/// users to add filters to an image and then navigate to the [EditImagePage] with the +/// final composition.' +@RoutePage() +class DriftFilterImagePage extends HookWidget { + final Image image; + final BaseAsset asset; + + const DriftFilterImagePage({super.key, required this.image, required this.asset}); + + @override + Widget build(BuildContext context) { + final colorFilter = useState(filters[0]); + final selectedFilterIndex = useState(0); + + Future createFilteredImage(ui.Image inputImage, ColorFilter filter) { + final completer = Completer(); + final size = Size(inputImage.width.toDouble(), inputImage.height.toDouble()); + final recorder = ui.PictureRecorder(); + final canvas = Canvas(recorder); + + final paint = Paint()..colorFilter = filter; + canvas.drawImage(inputImage, Offset.zero, paint); + + recorder.endRecording().toImage(size.width.round(), size.height.round()).then((image) { + completer.complete(image); + }); + + return completer.future; + } + + void applyFilter(ColorFilter filter, int index) { + colorFilter.value = filter; + selectedFilterIndex.value = index; + } + + Future applyFilterAndConvert(ColorFilter filter) async { + final completer = Completer(); + image.image + .resolve(ImageConfiguration.empty) + .addListener( + ImageStreamListener((ImageInfo info, bool _) { + completer.complete(info.image); + }), + ); + final uiImage = await completer.future; + + final filteredUiImage = await createFilteredImage(uiImage, filter); + final byteData = await filteredUiImage.toByteData(format: ui.ImageByteFormat.png); + final pngBytes = byteData!.buffer.asUint8List(); + + return Image.memory(pngBytes, fit: BoxFit.contain); + } + + return Scaffold( + appBar: AppBar( + backgroundColor: context.scaffoldBackgroundColor, + title: Text("filter".tr()), + leading: CloseButton(color: context.primaryColor), + actions: [ + IconButton( + icon: Icon(Icons.done_rounded, color: context.primaryColor, size: 24), + onPressed: () async { + final filteredImage = await applyFilterAndConvert(colorFilter.value); + context.pushRoute(DriftEditImageRoute(asset: asset, image: filteredImage, isEdited: true)); + }, + ), + ], + ), + backgroundColor: context.scaffoldBackgroundColor, + body: Column( + children: [ + SizedBox( + height: context.height * 0.7, + child: Center( + child: ColorFiltered(colorFilter: colorFilter.value, child: image), + ), + ), + SizedBox( + height: 120, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: filters.length, + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: _FilterButton( + image: image, + label: filterNames[index], + filter: filters[index], + isSelected: selectedFilterIndex.value == index, + onTap: () => applyFilter(filters[index], index), + ), + ); + }, + ), + ), + ], + ), + ); + } +} + +class _FilterButton extends StatelessWidget { + final Image image; + final String label; + final ColorFilter filter; + final bool isSelected; + final VoidCallback onTap; + + const _FilterButton({ + required this.image, + required this.label, + required this.filter, + required this.isSelected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + GestureDetector( + onTap: onTap, + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(10)), + border: isSelected ? Border.all(color: context.primaryColor, width: 3) : null, + ), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(10)), + child: ColorFiltered( + colorFilter: filter, + child: FittedBox(fit: BoxFit.cover, child: image), + ), + ), + ), + ), + const SizedBox(height: 10), + Text(label, style: context.themeData.textTheme.bodyMedium), + ], + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart new file mode 100644 index 0000000000..cde0db8e18 --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/pages/editing/drift_edit.page.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; + +class EditImageActionButton extends ConsumerWidget { + const EditImageActionButton({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final currentAsset = ref.watch(currentAssetNotifier); + + onPress() { + if (currentAsset == null) { + return; + } + + final image = Image(image: getFullImageProvider(currentAsset)); + + context.navigator.push( + MaterialPageRoute( + builder: (context) => DriftEditImagePage(asset: currentAsset, image: image, isEdited: false), + ), + ); + } + + return BaseActionButton( + iconData: Icons.tune, + label: "edit".t(context: context), + onPressed: onPress, + ); + } +} diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart index b220d4e6a5..bb7b06113c 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart @@ -6,6 +6,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/edit_image_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; @@ -38,6 +39,7 @@ class ViewerBottomBar extends ConsumerWidget { final actions = [ const ShareActionButton(source: ActionSource.viewer), if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer), + if (asset.type == AssetType.image) const EditImageActionButton(), if (asset.hasRemote && isOwner) const ArchiveActionButton(source: ActionSource.viewer), asset.isLocalOnly ? const DeleteLocalActionButton(source: ActionSource.viewer) diff --git a/mobile/lib/repositories/file_media.repository.dart b/mobile/lib/repositories/file_media.repository.dart index 6d429e4777..654be78fb4 100644 --- a/mobile/lib/repositories/file_media.repository.dart +++ b/mobile/lib/repositories/file_media.repository.dart @@ -2,7 +2,8 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/entities/asset.entity.dart' hide AssetType; import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:photo_manager/photo_manager.dart' hide AssetType; @@ -15,6 +16,18 @@ class FileMediaRepository { return AssetMediaRepository.toAsset(entity); } + Future saveLocalAsset(Uint8List data, {required String title, String? relativePath}) async { + final entity = await PhotoManager.editor.saveImage(data, filename: title, title: title, relativePath: relativePath); + + return LocalAsset( + id: entity.id, + name: title, + type: AssetType.image, + createdAt: entity.createDateTime, + updatedAt: entity.modifiedDateTime, + ); + } + Future saveImageWithFile(String filePath, {String? title, String? relativePath}) async { final entity = await PhotoManager.editor.saveImageWithPath(filePath, title: title, relativePath: relativePath); return AssetMediaRepository.toAsset(entity); diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index da24617824..5f8e4fe53c 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -101,6 +101,9 @@ import 'package:immich_mobile/presentation/pages/drift_remote_album.page.dart'; import 'package:immich_mobile/presentation/pages/drift_trash.page.dart'; import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart'; import 'package:immich_mobile/presentation/pages/drift_video.page.dart'; +import 'package:immich_mobile/presentation/pages/editing/drift_crop.page.dart'; +import 'package:immich_mobile/presentation/pages/editing/drift_edit.page.dart'; +import 'package:immich_mobile/presentation/pages/editing/drift_filter.page.dart'; import 'package:immich_mobile/presentation/pages/local_timeline.page.dart'; import 'package:immich_mobile/presentation/pages/search/drift_search.page.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart'; @@ -333,6 +336,9 @@ class AppRouter extends RootStackRouter { AutoRoute(page: DriftBackupOptionsRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftAlbumOptionsRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftMapRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: DriftEditImageRoute.page), + AutoRoute(page: DriftCropImageRoute.page), + AutoRoute(page: DriftFilterImageRoute.page), // required to handle all deeplinks in deep_link.service.dart // auto_route_library#1722 RedirectRoute(path: '*', redirectTo: '/'), diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 8c5064e752..1abe49b14f 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -828,6 +828,112 @@ class DriftCreateAlbumRoute extends PageRouteInfo { ); } +/// generated route for +/// [DriftCropImagePage] +class DriftCropImageRoute extends PageRouteInfo { + DriftCropImageRoute({ + Key? key, + required Image image, + required BaseAsset asset, + List? children, + }) : super( + DriftCropImageRoute.name, + args: DriftCropImageRouteArgs(key: key, image: image, asset: asset), + initialChildren: children, + ); + + static const String name = 'DriftCropImageRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return DriftCropImagePage( + key: args.key, + image: args.image, + asset: args.asset, + ); + }, + ); +} + +class DriftCropImageRouteArgs { + const DriftCropImageRouteArgs({ + this.key, + required this.image, + required this.asset, + }); + + final Key? key; + + final Image image; + + final BaseAsset asset; + + @override + String toString() { + return 'DriftCropImageRouteArgs{key: $key, image: $image, asset: $asset}'; + } +} + +/// generated route for +/// [DriftEditImagePage] +class DriftEditImageRoute extends PageRouteInfo { + DriftEditImageRoute({ + Key? key, + required BaseAsset asset, + required Image image, + required bool isEdited, + List? children, + }) : super( + DriftEditImageRoute.name, + args: DriftEditImageRouteArgs( + key: key, + asset: asset, + image: image, + isEdited: isEdited, + ), + initialChildren: children, + ); + + static const String name = 'DriftEditImageRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return DriftEditImagePage( + key: args.key, + asset: args.asset, + image: args.image, + isEdited: args.isEdited, + ); + }, + ); +} + +class DriftEditImageRouteArgs { + const DriftEditImageRouteArgs({ + this.key, + required this.asset, + required this.image, + required this.isEdited, + }); + + final Key? key; + + final BaseAsset asset; + + final Image image; + + final bool isEdited; + + @override + String toString() { + return 'DriftEditImageRouteArgs{key: $key, asset: $asset, image: $image, isEdited: $isEdited}'; + } +} + /// generated route for /// [DriftFavoritePage] class DriftFavoriteRoute extends PageRouteInfo { @@ -844,6 +950,54 @@ class DriftFavoriteRoute extends PageRouteInfo { ); } +/// generated route for +/// [DriftFilterImagePage] +class DriftFilterImageRoute extends PageRouteInfo { + DriftFilterImageRoute({ + Key? key, + required Image image, + required BaseAsset asset, + List? children, + }) : super( + DriftFilterImageRoute.name, + args: DriftFilterImageRouteArgs(key: key, image: image, asset: asset), + initialChildren: children, + ); + + static const String name = 'DriftFilterImageRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return DriftFilterImagePage( + key: args.key, + image: args.image, + asset: args.asset, + ); + }, + ); +} + +class DriftFilterImageRouteArgs { + const DriftFilterImageRouteArgs({ + this.key, + required this.image, + required this.asset, + }); + + final Key? key; + + final Image image; + + final BaseAsset asset; + + @override + String toString() { + return 'DriftFilterImageRouteArgs{key: $key, image: $image, asset: $asset}'; + } +} + /// generated route for /// [DriftLibraryPage] class DriftLibraryRoute extends PageRouteInfo {