diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ba93a31a..dbb91a38 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -16,6 +16,10 @@ jobs: flutter-version: '3.19.5' - name: Install Package Dependencies run: flutter pub get + - name: Format dart code + run: dart format . + - name: Analyze dart code + run: dart analyze . - name: Check publish with dry run run: flutter pub publish --dry-run - name: Publish package diff --git a/CHANGELOG.md b/CHANGELOG.md index d77ee135..f22b3d73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Version 2.6.7 +- **fix**: correct layer interaction to handle multiple layers +- **refactor**: improve code readability for better maintainability + ## Version 2.6.6 - **refactor:** Update editor code examples diff --git a/example/lib/pages/movable_background_image.dart b/example/lib/pages/movable_background_image.dart index 3b63b1c5..e72097b8 100644 --- a/example/lib/pages/movable_background_image.dart +++ b/example/lib/pages/movable_background_image.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:pro_image_editor/models/layer.dart'; import 'package:pro_image_editor/modules/paint_editor/utils/draw/draw_canvas.dart'; import 'package:pro_image_editor/pro_image_editor.dart'; @@ -57,9 +58,13 @@ class _MoveableBackgroundImageExampleState var width = MediaQuery.of(context).size.width; var height = MediaQuery.of(context).size.height; + double imgRatio = 1; // set the aspect ratio from your image. Size editorSize = Size( - width, - height - kToolbarHeight - kBottomNavigationBarHeight, + width - MediaQuery.of(context).padding.horizontal, + height - + kToolbarHeight - + kBottomNavigationBarHeight - + MediaQuery.of(context).padding.vertical, ); await _createTransparentImage(editorSize.aspectRatio); @@ -149,6 +154,9 @@ class _MoveableBackgroundImageExampleState blurEditorConfigs: const BlurEditorConfigs(enabled: false), imageEditorTheme: const ImageEditorTheme( + uiOverlayStyle: SystemUiOverlayStyle( + statusBarColor: Colors.black, + ), background: Colors.transparent, /// Optionally remove background @@ -159,7 +167,10 @@ class _MoveableBackgroundImageExampleState ), stickerEditorConfigs: StickerEditorConfigs( enabled: false, - initWidth: editorSize.width / _initScale, + initWidth: (editorSize.aspectRatio > imgRatio + ? editorSize.height + : editorSize.width) / + _initScale, buildStickers: (setLayer) { // Optionally your code to pick layers return const SizedBox(); diff --git a/example/lib/pages/reorder_layer_example.dart b/example/lib/pages/reorder_layer_example.dart index c860dc56..ddbfdfb4 100644 --- a/example/lib/pages/reorder_layer_example.dart +++ b/example/lib/pages/reorder_layer_example.dart @@ -1,3 +1,4 @@ +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:pro_image_editor/models/layer.dart'; import 'package:pro_image_editor/modules/paint_editor/utils/draw/draw_canvas.dart'; @@ -58,6 +59,7 @@ class _ReorderLayerExampleState extends State onPressed: () { showModalBottomSheet( context: context, + showDragHandle: true, builder: (context) { return ReorderLayerSheet( layers: editorKey.currentState!.activeLayers, @@ -66,7 +68,7 @@ class _ReorderLayerExampleState extends State oldIndex: oldIndex, newIndex: newIndex, ); - Navigator.pop(context); + /* Navigator.pop(context); */ }, ); }, @@ -98,70 +100,67 @@ class ReorderLayerSheet extends StatefulWidget { class _ReorderLayerSheetState extends State { @override Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Padding( - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Text( - 'Reorder', - style: TextStyle(fontSize: 24, fontWeight: FontWeight.w500), - ), - ), - Expanded( - child: ReorderableListView.builder( - itemBuilder: (context, index) { - Layer layer = widget.layers[index]; - return ListTile( - key: ValueKey(layer), - title: layer.runtimeType == TextLayerData - ? Text( - (layer as TextLayerData).text, - style: const TextStyle(fontSize: 20), - ) - : layer.runtimeType == EmojiLayerData - ? Text( - (layer as EmojiLayerData).emoji, - style: const TextStyle(fontSize: 24), - ) - : layer.runtimeType == PaintingLayerData - ? SizedBox( - height: 40, - child: FittedBox( - alignment: Alignment.centerLeft, - child: CustomPaint( - size: (layer as PaintingLayerData).size, - willChange: true, - isComplex: - layer.item.mode == PaintModeE.freeStyle, - painter: DrawCanvas( - item: layer.item, - scale: layer.scale, - enabledHitDetection: false, - freeStyleHighPerformanceScaling: false, - freeStyleHighPerformanceMoving: false, - ), - ), - ), - ) - : layer.runtimeType == StickerLayerData - ? SizedBox( - height: 40, - child: FittedBox( - alignment: Alignment.centerLeft, - child: - (layer as StickerLayerData).sticker, - ), - ) - : Text(layer.id.toString()), - ); - }, - itemCount: widget.layers.length, - onReorder: widget.onReorder, - ), + return ReorderableListView.builder( + header: const Padding( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Text( + 'Reorder', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.w500), ), - ], + ), + footer: const SizedBox(height: 30), + dragStartBehavior: DragStartBehavior.down, + itemBuilder: (context, index) { + Layer layer = widget.layers[index]; + return ListTile( + key: ValueKey(layer), + tileColor: Theme.of(context).cardColor, + title: layer.runtimeType == TextLayerData + ? Text( + (layer as TextLayerData).text, + style: const TextStyle(fontSize: 20), + ) + : layer.runtimeType == EmojiLayerData + ? Text( + (layer as EmojiLayerData).emoji, + style: const TextStyle(fontSize: 24), + ) + : layer.runtimeType == PaintingLayerData + ? SizedBox( + height: 40, + child: FittedBox( + alignment: Alignment.centerLeft, + child: CustomPaint( + size: (layer as PaintingLayerData).size, + willChange: true, + isComplex: + layer.item.mode == PaintModeE.freeStyle, + painter: DrawCanvas( + item: layer.item, + scale: layer.scale, + enabledHitDetection: false, + freeStyleHighPerformanceScaling: false, + freeStyleHighPerformanceMoving: false, + ), + ), + ), + ) + : layer.runtimeType == StickerLayerData + ? SizedBox( + height: 40, + child: FittedBox( + alignment: Alignment.centerLeft, + child: (layer as StickerLayerData).sticker, + ), + ) + : Text( + layer.id.toString(), + ), + trailing: const Icon(Icons.drag_handle_rounded), + ); + }, + itemCount: widget.layers.length, + onReorder: widget.onReorder, ); } } diff --git a/example/lib/pages/stickers_example.dart b/example/lib/pages/stickers_example.dart index a8a9e1ca..ee34de3a 100644 --- a/example/lib/pages/stickers_example.dart +++ b/example/lib/pages/stickers_example.dart @@ -34,6 +34,7 @@ class _StickersExampleState extends State onImageEditingComplete: onImageEditingComplete, onCloseEditor: onCloseEditor, configs: ProImageEditorConfigs( + blurEditorConfigs: const BlurEditorConfigs(enabled: false), stickerEditorConfigs: StickerEditorConfigs( enabled: true, buildStickers: (setLayer) { diff --git a/example/lib/pages/whatsapp_example.dart b/example/lib/pages/whatsapp_example.dart index 60e2a2bc..e3feae82 100644 --- a/example/lib/pages/whatsapp_example.dart +++ b/example/lib/pages/whatsapp_example.dart @@ -176,77 +176,80 @@ class _WhatsAppExampleState extends State }, ), customWidgets: ImageEditorCustomWidgets( - whatsAppBottomWidget: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 7, 16, 12), - child: TextField( - textAlignVertical: TextAlignVertical.center, - decoration: InputDecoration( - filled: true, - isDense: true, - prefixIcon: const Padding( - padding: EdgeInsets.only(left: 7.0), - child: Icon( - Icons.add_photo_alternate_rounded, - size: 24, - color: Colors.white, + whatsAppBottomWidget: LayoutBuilder(builder: (context, constraints) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 7, 16, 12), + child: TextField( + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + filled: true, + isDense: true, + prefixIcon: const Padding( + padding: EdgeInsets.only(left: 7.0), + child: Icon( + Icons.add_photo_alternate_rounded, + size: 24, + color: Colors.white, + ), ), + hintText: 'Add a caption...', + hintStyle: const TextStyle( + color: Color.fromARGB(255, 238, 238, 238), + fontWeight: FontWeight.w400, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(40), + borderSide: BorderSide.none, + ), + fillColor: const Color(0xFF202D35), ), - hintText: 'Add a caption...', - hintStyle: const TextStyle( - color: Color.fromARGB(255, 238, 238, 238), - fontWeight: FontWeight.w400, - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(40), - borderSide: BorderSide.none, - ), - fillColor: const Color(0xFF202D35), ), ), ), - ), - Flexible( - child: Container( - padding: const EdgeInsets.fromLTRB(16, 7, 16, 12), - color: Colors.black38, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - padding: const EdgeInsets.symmetric( - vertical: 4, - horizontal: 10, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: const Color(0xFF202D35), - ), - child: const Text( - 'Alex Frei', - style: TextStyle( - fontSize: 13, + Flexible( + child: Container( + padding: EdgeInsets.fromLTRB(16, 7, 16, + 12 + MediaQuery.of(context).viewInsets.bottom), + color: Colors.black38, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.symmetric( + vertical: 4, + horizontal: 10, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: const Color(0xFF202D35), + ), + child: const Text( + 'Alex Frei', + style: TextStyle( + fontSize: 13, + ), ), ), - ), - IconButton( - onPressed: () { - editorKey.currentState?.doneEditing(); - }, - icon: const Icon(Icons.send), - style: IconButton.styleFrom( - backgroundColor: const Color(0xFF0DA886), - ), - ) - ], + IconButton( + onPressed: () { + editorKey.currentState?.doneEditing(); + }, + icon: const Icon(Icons.send), + style: IconButton.styleFrom( + backgroundColor: const Color(0xFF0DA886), + ), + ) + ], + ), ), - ), - ) - ], - ), + ) + ], + ); + }), ), ), ); diff --git a/lib/mixins/converted_configs.dart b/lib/mixins/converted_configs.dart new file mode 100644 index 00000000..a5a70b93 --- /dev/null +++ b/lib/mixins/converted_configs.dart @@ -0,0 +1,54 @@ +import 'package:pro_image_editor/pro_image_editor.dart'; + +/// A mixin providing access to converted configurations from [ProImageEditorConfigs]. +mixin ImageEditorConvertedConfigs { + /// Returns the main configuration options for the editor. + ProImageEditorConfigs get configs; + + /// Returns the configuration options for the paint editor. + PaintEditorConfigs get paintEditorConfigs => configs.paintEditorConfigs; + + /// Returns the configuration options for the text editor. + TextEditorConfigs get textEditorConfigs => configs.textEditorConfigs; + + /// Returns the configuration options for the crop and rotate editor. + CropRotateEditorConfigs get cropRotateEditorConfigs => + configs.cropRotateEditorConfigs; + + /// Returns the configuration options for the filter editor. + FilterEditorConfigs get filterEditorConfigs => configs.filterEditorConfigs; + + /// Returns the configuration options for the blur editor. + BlurEditorConfigs get blurEditorConfigs => configs.blurEditorConfigs; + + /// Returns the configuration options for the emoji editor. + EmojiEditorConfigs get emojiEditorConfigs => configs.emojiEditorConfigs; + + /// Returns the configuration options for the sticker editor. + StickerEditorConfigs? get stickerEditorConfigs => + configs.stickerEditorConfigs; + + /// Returns the design mode for the image editor. + ImageEditorDesignModeE get designMode => configs.designMode; + + /// Returns the theme settings for the image editor. + ImageEditorTheme get imageEditorTheme => configs.imageEditorTheme; + + /// Returns custom widget configurations for the image editor. + ImageEditorCustomWidgets get customWidgets => configs.customWidgets; + + /// Returns the icons used in the image editor. + ImageEditorIcons get icons => configs.icons; + + /// Returns the internationalization settings for the image editor. + I18n get i18n => configs.i18n; + + /// Returns the initial state history for the image editor. + ImportStateHistory? get initStateHistory => configs.initStateHistory; + + /// Returns helper lines configuration for the image editor. + HelperLines get helperLines => configs.helperLines; + + /// Returns the hero tag used in the image editor. + String get heroTag => configs.heroTag; +} diff --git a/lib/mixins/editor_configs_mixin.dart b/lib/mixins/editor_configs_mixin.dart new file mode 100644 index 00000000..9a6e1fad --- /dev/null +++ b/lib/mixins/editor_configs_mixin.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:pro_image_editor/pro_image_editor.dart'; + +import 'converted_configs.dart'; + +/// A mixin providing access to simple editor configurations. +mixin SimpleConfigsAccess on StatefulWidget { + /// Returns the configuration options for the editor. + ProImageEditorConfigs get configs; +} + +/// A mixin providing access to simple editor configurations within a state. +mixin SimpleConfigsAccessState + on State, ImageEditorConvertedConfigs { + SimpleConfigsAccess get _widget => (widget as SimpleConfigsAccess); + + @override + ProImageEditorConfigs get configs => _widget.configs; +} diff --git a/lib/mixins/main_editor/main_editor_global_keys.dart b/lib/mixins/main_editor/main_editor_global_keys.dart new file mode 100644 index 00000000..9f4cade0 --- /dev/null +++ b/lib/mixins/main_editor/main_editor_global_keys.dart @@ -0,0 +1,28 @@ +import 'package:flutter/widgets.dart'; + +import '../../modules/blur_editor.dart'; +import '../../modules/crop_rotate_editor/crop_rotate_editor.dart'; +import '../../modules/emoji_editor/emoji_editor.dart'; +import '../../modules/filter_editor/filter_editor.dart'; +import '../../modules/paint_editor/paint_editor.dart'; +import '../../modules/text_editor.dart'; + +mixin MainEditorGlobalKeys { + /// A GlobalKey for the Painting Editor, used to access and control the state of the painting editor. + final paintingEditor = GlobalKey(); + + /// A GlobalKey for the Text Editor, used to access and control the state of the text editor. + final textEditor = GlobalKey(); + + /// A GlobalKey for the Crop and Rotate Editor, used to access and control the state of the crop and rotate editor. + final cropRotateEditor = GlobalKey(); + + /// A GlobalKey for the Filter Editor, used to access and control the state of the filter editor. + final filterEditor = GlobalKey(); + + /// A GlobalKey for the Blur Editor, used to access and control the state of the blur editor. + final blurEditor = GlobalKey(); + + /// A GlobalKey for the Emoji Editor, used to access and control the state of the emoji editor. + final emojiEditor = GlobalKey(); +} diff --git a/lib/mixins/standalone_editor.dart b/lib/mixins/standalone_editor.dart new file mode 100644 index 00000000..665e09ed --- /dev/null +++ b/lib/mixins/standalone_editor.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import '../models/crop_rotate_editor/transform_factors.dart'; +import '../models/editor_configs/pro_image_editor_configs.dart'; +import '../models/editor_image.dart'; +import '../models/history/filter_state_history.dart'; +import '../models/init_configs/editor_init_configs.dart'; +import '../models/layer.dart'; +import 'converted_configs.dart'; + +/// A mixin providing access to standalone editor configurations and image. +mixin StandaloneEditor { + /// Returns the initialization configurations for the editor. + T get initConfigs; + + /// Returns the editor image + EditorImage get editorImage; +} + +/// A mixin providing access to standalone editor configurations and image within a state. +mixin StandaloneEditorState on State, ImageEditorConvertedConfigs { + /// Returns the initialization configurations for the editor. + I get initConfigs => (widget as StandaloneEditor).initConfigs; + + /// Returns the image being edited. + EditorImage get editorImage => (widget as StandaloneEditor).editorImage; + + @override + ProImageEditorConfigs get configs => initConfigs.configs; + + /// Returns the theme data for the editor. + ThemeData get theme => initConfigs.theme; + + /// Returns the transformation configurations for the editor. + TransformConfigs? get transformConfigs => initConfigs.transformConfigs; + + /// Returns the layers in the editor. + List? get layers => initConfigs.layers; + + /// Returns the callback function to update the UI. + Function? get onUpdateUI => initConfigs.onUpdateUI; + + /// Returns the applied blur factor. + double get appliedBlurFactor => initConfigs.appliedBlurFactor; + + /// Returns the applied filters. + List get appliedFilters => initConfigs.appliedFilters; + + /// Returns the body size with layers. + Size get bodySizeWithLayers => initConfigs.bodySizeWithLayers; + + /// Returns the image size with layers. + Size get imageSizeWithLayers => initConfigs.imageSizeWithLayers; +} diff --git a/lib/models/helper_lines.dart b/lib/models/editor_configs/helper_lines_configs.dart similarity index 100% rename from lib/models/helper_lines.dart rename to lib/models/editor_configs/helper_lines_configs.dart diff --git a/lib/models/editor_configs/pro_image_editor_configs.dart b/lib/models/editor_configs/pro_image_editor_configs.dart index e61dd5cd..b5df8631 100644 --- a/lib/models/editor_configs/pro_image_editor_configs.dart +++ b/lib/models/editor_configs/pro_image_editor_configs.dart @@ -9,7 +9,7 @@ import 'filter_editor_configs.dart'; import 'blur_editor_configs.dart'; import 'paint_editor_configs.dart'; import 'text_editor_configs.dart'; -import '../helper_lines.dart'; +import 'helper_lines_configs.dart'; import '../i18n/i18n.dart'; import '../icons/icons.dart'; import '../theme/theme.dart'; diff --git a/lib/models/blur_state_history.dart b/lib/models/history/blur_state_history.dart similarity index 100% rename from lib/models/blur_state_history.dart rename to lib/models/history/blur_state_history.dart diff --git a/lib/models/filter_state_history.dart b/lib/models/history/filter_state_history.dart similarity index 100% rename from lib/models/filter_state_history.dart rename to lib/models/history/filter_state_history.dart diff --git a/lib/models/history/state_history.dart b/lib/models/history/state_history.dart index 36d13df4..c242d21f 100644 --- a/lib/models/history/state_history.dart +++ b/lib/models/history/state_history.dart @@ -1,7 +1,7 @@ import '../crop_rotate_editor/transform_factors.dart'; -import '../filter_state_history.dart'; -import '../blur_state_history.dart'; +import 'blur_state_history.dart'; import '../layer.dart'; +import 'filter_state_history.dart'; /// The `EditorStateHistory` class represents changes made to an image in the image /// editor. It contains information about the changes applied to the image, including diff --git a/lib/models/import_export/import_state_history.dart b/lib/models/import_export/import_state_history.dart index 4215bb5f..3d0c28c3 100644 --- a/lib/models/import_export/import_state_history.dart +++ b/lib/models/import_export/import_state_history.dart @@ -7,8 +7,8 @@ import 'package:pro_image_editor/models/import_export/import_state_history_confi import 'package:pro_image_editor/models/layer.dart'; import '../editor_image.dart'; -import '../filter_state_history.dart'; -import '../blur_state_history.dart'; +import '../history/blur_state_history.dart'; +import '../history/filter_state_history.dart'; import '../history/state_history.dart'; /// This class represents the state history of an imported editor session. diff --git a/lib/models/init_configs/blur_editor_init_configs.dart b/lib/models/init_configs/blur_editor_init_configs.dart new file mode 100644 index 00000000..59c405e1 --- /dev/null +++ b/lib/models/init_configs/blur_editor_init_configs.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +import 'editor_init_configs.dart'; + +/// Configuration class for initializing the blur editor. +/// +/// This class extends [EditorInitConfigs] and adds parameters for the image size and whether to return the image as a Uint8List when closing the editor. +class BlurEditorInitConfigs extends EditorInitConfigs { + /// The size of the image. + final Size imageSize; + + /// Determines whether to return the image as a Uint8List when closing the editor. + final bool convertToUint8List; + + /// Creates a new instance of [BlurEditorInitConfigs]. + /// + /// The [theme] parameter specifies the theme data for the editor. + /// The [imageSize] parameter specifies the size of the image. + /// The [convertToUint8List] parameter determines whether to return the image as a Uint8List when closing the editor. + /// The other parameters are inherited from [EditorInitConfigs]. + const BlurEditorInitConfigs({ + super.configs, + super.transformConfigs, + super.layers, + super.onUpdateUI, + super.imageSizeWithLayers, + super.bodySizeWithLayers, + super.appliedFilters, + super.appliedBlurFactor, + required super.theme, + required this.imageSize, + this.convertToUint8List = false, + }); +} diff --git a/lib/models/init_configs/editor_init_configs.dart b/lib/models/init_configs/editor_init_configs.dart new file mode 100644 index 00000000..b81cd29e --- /dev/null +++ b/lib/models/init_configs/editor_init_configs.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import '../../pro_image_editor.dart'; +import '../crop_rotate_editor/transform_factors.dart'; +import '../history/filter_state_history.dart'; +import '../layer.dart'; + +/// Configuration class for initializing the image editor. +/// +/// This class holds various configurations needed to initialize the image editor. +abstract class EditorInitConfigs { + /// The configuration options for the image editor. + final ProImageEditorConfigs configs; + + /// A callback function that can be used to update the UI from custom widgets. + final Function? onUpdateUI; + + /// The size of the image with layers applied. + final Size imageSizeWithLayers; + + /// The size of the body with layers applied. + final Size bodySizeWithLayers; + + /// The list of applied filter history. + final List appliedFilters; + + /// The applied blur factor. + final double appliedBlurFactor; + + /// The transformation configurations for the editor. + final TransformConfigs? transformConfigs; + + /// The theme data for the editor. + final ThemeData theme; + + /// The layers in the editor. + final List? layers; + + /// Creates a new instance of [EditorInitConfigs]. + /// + /// The [theme] parameter specifies the theme data for the editor. + /// The [configs] parameter specifies the configuration options for the image editor. + /// The [onUpdateUI] parameter is a callback function that can be used to update the UI from custom widgets. + /// The [imageSizeWithLayers] parameter specifies the size of the image with layers applied. + /// The [bodySizeWithLayers] parameter specifies the size of the body with layers applied. + /// The [appliedFilters] parameter specifies the list of applied filter history. + /// The [appliedBlurFactor] parameter specifies the applied blur factor. + /// The [transformConfigs] parameter specifies the transformation configurations for the editor. + /// The [layers] parameter specifies the layers in the editor. + const EditorInitConfigs({ + required this.theme, + this.configs = const ProImageEditorConfigs(), + this.onUpdateUI, + this.imageSizeWithLayers = Size.zero, + this.bodySizeWithLayers = Size.zero, + this.transformConfigs, + this.appliedFilters = const [], + this.appliedBlurFactor = 0, + this.layers, + }); +} diff --git a/lib/models/init_configs/filter_editor_init_configs.dart b/lib/models/init_configs/filter_editor_init_configs.dart new file mode 100644 index 00000000..2beda4e2 --- /dev/null +++ b/lib/models/init_configs/filter_editor_init_configs.dart @@ -0,0 +1,27 @@ +import 'editor_init_configs.dart'; + +/// Configuration class for initializing the filter editor. +/// +/// This class extends [EditorInitConfigs] and adds a parameter to determine whether to return the image as a Uint8List when closing the editor. +class FilterEditorInitConfigs extends EditorInitConfigs { + /// Determines whether to return the image as a Uint8List when closing the editor. + final bool convertToUint8List; + + /// Creates a new instance of [FilterEditorInitConfigs]. + /// + /// The [theme] parameter specifies the theme data for the editor. + /// The [convertToUint8List] parameter determines whether to return the image as a Uint8List when closing the editor. + /// The other parameters are inherited from [EditorInitConfigs]. + const FilterEditorInitConfigs({ + super.transformConfigs, + super.configs, + super.onUpdateUI, + super.imageSizeWithLayers, + super.bodySizeWithLayers, + super.layers, + super.appliedFilters, + super.appliedBlurFactor, + required super.theme, + this.convertToUint8List = false, + }); +} diff --git a/lib/models/init_configs/paint_canvas_init_configs.dart b/lib/models/init_configs/paint_canvas_init_configs.dart new file mode 100644 index 00000000..a694731f --- /dev/null +++ b/lib/models/init_configs/paint_canvas_init_configs.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:pro_image_editor/pro_image_editor.dart'; + +/// Configuration class for initializing the paint canvas. +class PaintCanvasInitConfigs { + /// A widget to display as a placeholder. + final Widget? placeholderWidget; + + /// Determines whether the canvas is scalable. + final bool? scalable; + + /// Determines whether to show the color picker. + final bool? showColorPicker; + + /// List of colors to be used in the color picker. + final List? colors; + + /// Callback function to save the canvas. + final Function? save; + + /// Callback function to update the canvas. + final VoidCallback? onUpdate; + + /// Icon widget for undo action. + final Widget? undoIcon; + + /// Icon widget for color selection. + final Widget? colorIcon; + + /// Size of the image. + final Size imageSize; + + /// Theme data for the canvas. + final ThemeData theme; + + /// Internationalization settings. + final I18n i18n; + + /// Theme settings for the image editor. + final ImageEditorTheme imageEditorTheme; + + /// Icons used in the image editor. + final ImageEditorIcons icons; + + /// Design mode for the image editor. + final ImageEditorDesignModeE designMode; + + /// Configuration options for the paint editor. + final PaintEditorConfigs configs; + + /// Creates a new instance of [PaintCanvasInitConfigs]. + /// + /// The [imageSize] parameter specifies the size of the image. + /// The [theme] parameter specifies the theme data for the canvas. + /// The [i18n] parameter specifies internationalization settings. + /// The [imageEditorTheme] parameter specifies theme settings for the image editor. + /// The [icons] parameter specifies icons used in the image editor. + /// The [designMode] parameter specifies the design mode for the image editor. + /// The [configs] parameter specifies configuration options for the paint editor. + const PaintCanvasInitConfigs({ + this.placeholderWidget, + this.scalable, + this.showColorPicker, + this.colors, + this.save, + this.onUpdate, + this.undoIcon, + this.colorIcon, + required this.imageSize, + required this.theme, + required this.i18n, + required this.imageEditorTheme, + required this.icons, + required this.designMode, + required this.configs, + }); +} diff --git a/lib/models/init_configs/paint_editor_init_configs.dart b/lib/models/init_configs/paint_editor_init_configs.dart new file mode 100644 index 00000000..ffbc32df --- /dev/null +++ b/lib/models/init_configs/paint_editor_init_configs.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +import 'editor_init_configs.dart'; + +/// Configuration class for initializing the paint editor. +/// +/// This class extends [EditorInitConfigs] and adds specific parameters related to painting functionality. +class PaintEditorInitConfigs extends EditorInitConfigs { + /// The size of the image. + final Size imageSize; + + /// Additional padding for the editor. + final EdgeInsets? paddingHelper; + + /// Creates a new instance of [PaintEditorInitConfigs]. + /// + /// The [theme] parameter specifies the theme data for the editor. + /// The [imageSize] parameter specifies the size of the image. + /// The [paddingHelper] parameter specifies additional padding for the editor. + /// The other parameters are inherited from [EditorInitConfigs]. + const PaintEditorInitConfigs({ + super.configs, + super.onUpdateUI, + super.transformConfigs, + super.layers, + super.imageSizeWithLayers, + super.bodySizeWithLayers, + super.appliedFilters, + super.appliedBlurFactor, + required super.theme, + required this.imageSize, + this.paddingHelper, + }); +} diff --git a/lib/modules/blur_editor.dart b/lib/modules/blur_editor.dart index 5f812833..d0c4f40f 100644 --- a/lib/modules/blur_editor.dart +++ b/lib/modules/blur_editor.dart @@ -3,17 +3,15 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:pro_image_editor/models/editor_configs/pro_image_editor_configs.dart'; -import 'package:pro_image_editor/models/filter_state_history.dart'; -import 'package:pro_image_editor/models/blur_state_history.dart'; -import 'package:pro_image_editor/utils/helper/editor_mixin.dart'; import 'package:pro_image_editor/widgets/transformed_content_generator.dart'; import 'package:screenshot/screenshot.dart'; import '../models/crop_rotate_editor/transform_factors.dart'; import '../models/editor_image.dart'; -import '../models/layer.dart'; +import '../models/init_configs/blur_editor_init_configs.dart'; import '../models/transform_helper.dart'; +import '../mixins/converted_configs.dart'; +import '../mixins/standalone_editor.dart'; import '../widgets/layer_stack.dart'; import '../widgets/loading_dialog.dart'; import 'filter_editor/widgets/image_with_multiple_filters.dart'; @@ -26,407 +24,110 @@ import 'filter_editor/widgets/image_with_multiple_filters.dart'; /// - `BlurEditor.network`: Loads an image from a network URL. /// - `BlurEditor.memory`: Loads an image from memory as a `Uint8List`. /// - `BlurEditor.autoSource`: Automatically selects the source based on provided parameters. -class BlurEditor extends StatefulWidget with ImageEditorMixin { +class BlurEditor extends StatefulWidget + with StandaloneEditor { @override - final ProImageEditorConfigs configs; - - /// The theme configuration for the editor. - final ThemeData theme; - - /// A byte array representing the image data. - final Uint8List? byteArray; - - /// The file representing the image. - final File? file; - - /// The asset path of the image. - final String? assetPath; - - /// The network URL of the image. - final String? networkUrl; - - /// The size of the image to be edited. - final Size imageSize; - - /// The transform configurations how the image should be initialized. - final TransformConfigs? transformConfigs; - - /// A callback function that can be used to update the UI from custom widgets. - final Function? onUpdateUI; + final BlurEditorInitConfigs initConfigs; + @override + final EditorImage editorImage; - /// Determines whether to return the image as a Uint8List when closing the editor. + /// Constructs a `BlurEditor` widget. /// - /// If set to `true`, when closing the editor, the editor will return the final image - /// as a Uint8List, including all applied blur states. If set to `false`, only - /// the blur states will be returned. - final bool convertToUint8List; - - /// A list of Layer objects representing image layers. - final List? layers; - - /// The rendered image size with layers. - /// Required to calculate the correct layer position. - final Size? imageSizeWithLayers; - - /// The rendered body size with layers. - /// Required to calculate the correct layer position. - final Size? bodySizeWithLayers; - - final List filters; - - final BlurStateHistory currentBlur; - - /// Private constructor for creating a `BlurEditor` widget. + /// The [key] parameter is used to provide a key for the widget. + /// The [editorImage] parameter specifies the image to be edited. + /// The [initConfigs] parameter specifies the initialization configurations for the editor. const BlurEditor._({ super.key, - this.byteArray, - this.file, - this.assetPath, - this.networkUrl, - this.onUpdateUI, - this.convertToUint8List = false, - this.transformConfigs, - this.layers, - this.imageSizeWithLayers, - this.bodySizeWithLayers, - required this.filters, - required this.currentBlur, - required this.theme, - required this.imageSize, - required this.configs, - }) : assert( - byteArray != null || - file != null || - networkUrl != null || - assetPath != null, - 'At least one of bytes, file, networkUrl, or assetPath must not be null.', - ); + required this.editorImage, + required this.initConfigs, + }); - /// Create a BlurEditor widget with an in-memory image represented as a Uint8List. - /// - /// This factory method allows you to create a BlurEditor widget that can be used to apply blur and edit an image represented as a Uint8List in memory. The provided parameters allow you to customize the appearance and behavior of the BlurEditor widget. - /// - /// Parameters: - /// - `byteArray`: A Uint8List representing the image data in memory. - /// - `key`: An optional Key to uniquely identify this widget in the widget tree. - /// - `theme`: An optional ThemeData object that defines the visual styling of the BlurEditor widget. - /// - `configs`: The image editor configs. - /// - `transformConfigs` The transform configurations how the image should be initialized. - /// - `convertToUint8List`: Determines whether to return the image as a Uint8List when closing the editor. - /// - /// Returns: - /// A BlurEditor widget configured with the provided parameters and the in-memory image data. - /// - /// Example Usage: - /// ```dart - /// final Uint8List imageBytes = ... // Load your image data here. - /// final blurEditor = BlurEditor.memory( - /// imageBytes, - /// theme: ThemeData.light(), - /// ); - /// ``` + /// Constructs a `BlurEditor` widget with image data loaded from memory. factory BlurEditor.memory( Uint8List byteArray, { Key? key, - required ThemeData theme, - required Size imageSize, - TransformConfigs? transformConfigs, - List? layers, - ProImageEditorConfigs configs = const ProImageEditorConfigs(), - Function? onUpdateUI, - bool convertToUint8List = false, - Size? imageSizeWithLayers, - Size? bodySizeWithLayers, - List? filters, - BlurStateHistory? currentBlur, + required BlurEditorInitConfigs initConfigs, }) { return BlurEditor._( key: key, - byteArray: byteArray, - theme: theme, - imageSize: imageSize, - transformConfigs: transformConfigs, - configs: configs, - onUpdateUI: onUpdateUI, - imageSizeWithLayers: imageSizeWithLayers, - bodySizeWithLayers: bodySizeWithLayers, - layers: layers, - filters: filters ?? [], - convertToUint8List: convertToUint8List, - currentBlur: currentBlur ?? BlurStateHistory(), + editorImage: EditorImage(byteArray: byteArray), + initConfigs: initConfigs, ); } - /// Create a BlurEditor widget with an image loaded from a File. - /// - /// This factory method allows you to create a BlurEditor widget that can be used to apply blur and edit an image loaded from a File. The provided parameters allow you to customize the appearance and behavior of the BlurEditor widget. - /// - /// Parameters: - /// - `file`: A File object representing the image file to be loaded. - /// - `key`: An optional Key to uniquely identify this widget in the widget tree. - /// - `theme`: An optional ThemeData object that defines the visual styling of the BlurEditor widget. - /// - `configs`: The image editor configs. - /// - `transformConfigs` The transform configurations how the image should be initialized. - /// - `convertToUint8List`: Determines whether to return the image as a Uint8List when closing the editor. - /// - /// Returns: - /// A BlurEditor widget configured with the provided parameters and the image loaded from the File. - /// - /// Example Usage: - /// ```dart - /// final File imageFile = ... // Provide the image file. - /// final blurEditor = BlurEditor.file( - /// imageFile, - /// theme: ThemeData.light(), - /// ); - /// ``` + /// Constructs a `BlurEditor` widget with an image loaded from a file. factory BlurEditor.file( File file, { Key? key, - required ThemeData theme, - required Size imageSize, - TransformConfigs? transformConfigs, - List? layers, - ProImageEditorConfigs configs = const ProImageEditorConfigs(), - Function? onUpdateUI, - bool convertToUint8List = false, - Size? imageSizeWithLayers, - Size? bodySizeWithLayers, - List? filters, - BlurStateHistory? currentBlur, + required BlurEditorInitConfigs initConfigs, }) { return BlurEditor._( key: key, - file: file, - theme: theme, - imageSize: imageSize, - transformConfigs: transformConfigs, - configs: configs, - onUpdateUI: onUpdateUI, - imageSizeWithLayers: imageSizeWithLayers, - bodySizeWithLayers: bodySizeWithLayers, - layers: layers, - filters: filters ?? [], - convertToUint8List: convertToUint8List, - currentBlur: currentBlur ?? BlurStateHistory(), + editorImage: EditorImage(file: file), + initConfigs: initConfigs, ); } - /// Create a BlurEditor widget with an image loaded from an asset. - /// - /// This factory method allows you to create a BlurEditor widget that can be used to apply blur and edit an image loaded from an asset. The provided parameters allow you to customize the appearance and behavior of the BlurEditor widget. - /// - /// Parameters: - /// - `assetPath`: A String representing the asset path of the image to be loaded. - /// - `key`: An optional Key to uniquely identify this widget in the widget tree. - /// - `theme`: An optional ThemeData object that defines the visual styling of the BlurEditor widget. - /// - `configs`: The image editor configs. - /// - `transformConfigs` The transform configurations how the image should be initialized. - /// - `convertToUint8List`: Determines whether to return the image as a Uint8List when closing the editor. - /// - /// Returns: - /// A BlurEditor widget configured with the provided parameters and the image loaded from the asset. - /// - /// Example Usage: - /// ```dart - /// final String assetPath = 'assets/image.png'; // Provide the asset path. - /// final blurEditor = BlurEditor.asset( - /// assetPath, - /// theme: ThemeData.light(), - /// ); - /// ``` + /// Constructs a `BlurEditor` widget with an image loaded from an asset. factory BlurEditor.asset( String assetPath, { Key? key, - required ThemeData theme, - required Size imageSize, - TransformConfigs? transformConfigs, - List? layers, - ProImageEditorConfigs configs = const ProImageEditorConfigs(), - Function? onUpdateUI, - bool convertToUint8List = false, - Size? imageSizeWithLayers, - Size? bodySizeWithLayers, - List? filters, - BlurStateHistory? currentBlur, + required BlurEditorInitConfigs initConfigs, }) { return BlurEditor._( key: key, - assetPath: assetPath, - theme: theme, - imageSize: imageSize, - transformConfigs: transformConfigs, - configs: configs, - onUpdateUI: onUpdateUI, - imageSizeWithLayers: imageSizeWithLayers, - bodySizeWithLayers: bodySizeWithLayers, - layers: layers, - filters: filters ?? [], - convertToUint8List: convertToUint8List, - currentBlur: currentBlur ?? BlurStateHistory(), + editorImage: EditorImage(assetPath: assetPath), + initConfigs: initConfigs, ); } - /// Create a BlurEditor widget with an image loaded from a network URL. - /// - /// This factory method allows you to create a BlurEditor widget that can be used to apply blur and edit an image loaded from a network URL. The provided parameters allow you to customize the appearance and behavior of the BlurEditor widget. - /// - /// Parameters: - /// - `networkUrl`: A String representing the network URL of the image to be loaded. - /// - `key`: An optional Key to uniquely identify this widget in the widget tree. - /// - `theme`: An optional ThemeData object that defines the visual styling of the BlurEditor widget. - /// - `configs`: The image editor configs. - /// - `transformConfigs` The transform configurations how the image should be initialized. - /// - `convertToUint8List`: Determines whether to return the image as a Uint8List when closing the editor. - /// - /// Returns: - /// A BlurEditor widget configured with the provided parameters and the image loaded from the network URL. - /// - /// Example Usage: - /// ```dart - /// final String imageUrl = 'https://example.com/image.jpg'; // Provide the network URL. - /// final blurEditor = BlurEditor.network( - /// imageUrl, - /// theme: ThemeData.light(), - /// ); - /// ``` + /// Constructs a `BlurEditor` widget with an image loaded from a network URL. factory BlurEditor.network( String networkUrl, { Key? key, - required ThemeData theme, - required Size imageSize, - TransformConfigs? transformConfigs, - List? layers, - ProImageEditorConfigs configs = const ProImageEditorConfigs(), - Function? onUpdateUI, - bool convertToUint8List = false, - Size? imageSizeWithLayers, - Size? bodySizeWithLayers, - List? filters, - BlurStateHistory? currentBlur, + required BlurEditorInitConfigs initConfigs, }) { return BlurEditor._( key: key, - networkUrl: networkUrl, - theme: theme, - imageSize: imageSize, - transformConfigs: transformConfigs, - configs: configs, - onUpdateUI: onUpdateUI, - imageSizeWithLayers: imageSizeWithLayers, - bodySizeWithLayers: bodySizeWithLayers, - layers: layers, - filters: filters ?? [], - convertToUint8List: convertToUint8List, - currentBlur: currentBlur ?? BlurStateHistory(), + editorImage: EditorImage(networkUrl: networkUrl), + initConfigs: initConfigs, ); } - /// Create a BlurEditor widget with automatic image source detection. - /// - /// This factory method allows you to create a BlurEditor widget with automatic detection of the image source type (Uint8List, File, asset, or network URL). Based on the provided parameters, it selects the appropriate source type and creates the BlurEditor widget accordingly. - /// - /// Parameters: - /// - `key`: An optional Key to uniquely identify this widget in the widget tree. - /// - `theme`: An optional ThemeData object that defines the visual styling of the BlurEditor widget. - /// - `byteArray`: An optional Uint8List representing the image data in memory. - /// - `file`: An optional File object representing the image file to be loaded. - /// - `assetPath`: An optional String representing the asset path of the image to be loaded. - /// - `networkUrl`: An optional String representing the network URL of the image to be loaded. - /// - `convertToUint8List`: Determines whether to return the image as a Uint8List when closing the editor. - /// - /// Returns: - /// A BlurEditor widget configured with the provided parameters and the detected image source. + /// Constructs a `BlurEditor` widget with an image loaded automatically based on the provided source. /// - /// Example Usage: - /// ```dart - /// // Provide one of the image sources: byteArray, file, assetPath, or networkUrl. - /// final blurEditor = BlurEditor.autoSource( - /// byteArray: imageBytes, - /// theme: ThemeData.light(), - /// ); - /// ``` + /// Either [byteArray], [file], [networkUrl], or [assetPath] must be provided. factory BlurEditor.autoSource({ Key? key, - required ThemeData theme, - TransformConfigs? transformConfigs, - List? layers, - ProImageEditorConfigs configs = const ProImageEditorConfigs(), - required Size imageSize, - Function? onUpdateUI, - bool convertToUint8List = false, - Size? imageSizeWithLayers, - Size? bodySizeWithLayers, - required List filters, + required BlurEditorInitConfigs initConfigs, Uint8List? byteArray, File? file, String? assetPath, String? networkUrl, - required BlurStateHistory currentBlur, }) { if (byteArray != null) { return BlurEditor.memory( byteArray, key: key, - theme: theme, - imageSize: imageSize, - transformConfigs: transformConfigs, - configs: configs, - onUpdateUI: onUpdateUI, - imageSizeWithLayers: imageSizeWithLayers, - bodySizeWithLayers: bodySizeWithLayers, - layers: layers, - filters: filters, - convertToUint8List: convertToUint8List, - currentBlur: currentBlur, + initConfigs: initConfigs, ); } else if (file != null) { return BlurEditor.file( file, key: key, - theme: theme, - imageSize: imageSize, - transformConfigs: transformConfigs, - configs: configs, - onUpdateUI: onUpdateUI, - imageSizeWithLayers: imageSizeWithLayers, - bodySizeWithLayers: bodySizeWithLayers, - layers: layers, - filters: filters, - convertToUint8List: convertToUint8List, - currentBlur: currentBlur, + initConfigs: initConfigs, ); } else if (networkUrl != null) { return BlurEditor.network( networkUrl, key: key, - theme: theme, - imageSize: imageSize, - transformConfigs: transformConfigs, - configs: configs, - onUpdateUI: onUpdateUI, - imageSizeWithLayers: imageSizeWithLayers, - bodySizeWithLayers: bodySizeWithLayers, - layers: layers, - filters: filters, - convertToUint8List: convertToUint8List, - currentBlur: currentBlur, + initConfigs: initConfigs, ); } else if (assetPath != null) { return BlurEditor.asset( assetPath, key: key, - theme: theme, - imageSize: imageSize, - transformConfigs: transformConfigs, - configs: configs, - onUpdateUI: onUpdateUI, - imageSizeWithLayers: imageSizeWithLayers, - bodySizeWithLayers: bodySizeWithLayers, - layers: layers, - filters: filters, - convertToUint8List: convertToUint8List, - currentBlur: currentBlur, + initConfigs: initConfigs, ); } else { throw ArgumentError( @@ -439,12 +140,15 @@ class BlurEditor extends StatefulWidget with ImageEditorMixin { } /// The state class for the `BlurEditor` widget. -class BlurEditorState extends State with ImageEditorStateMixin { +class BlurEditorState extends State + with + ImageEditorConvertedConfigs, + StandaloneEditorState { /// Manages the capturing a screenshot of the image. ScreenshotController screenshotController = ScreenshotController(); /// Represents the selected blur state. - late BlurStateHistory selectedBlur; + late double selectedBlur; /// Represents the dimensions of the body. Size _bodySize = Size.zero; @@ -454,7 +158,7 @@ class BlurEditorState extends State with ImageEditorStateMixin { @override void initState() { - selectedBlur = widget.currentBlur; + selectedBlur = appliedBlurFactor; super.initState(); } @@ -467,13 +171,13 @@ class BlurEditorState extends State with ImageEditorStateMixin { void done() async { if (_createScreenshot) return; - if (widget.convertToUint8List) { + if (initConfigs.convertToUint8List) { _createScreenshot = true; LoadingDialog loading = LoadingDialog() ..show( context, i18n: i18n, - theme: widget.theme, + theme: theme, designMode: designMode, message: i18n.blurEditor.applyBlurDialogMsg, imageEditorTheme: imageEditorTheme, @@ -485,16 +189,15 @@ class BlurEditorState extends State with ImageEditorStateMixin { Navigator.pop(context, data); } } else { - BlurStateHistory blur = selectedBlur; - Navigator.pop(context, blur); + Navigator.pop(context, selectedBlur); } } @override Widget build(BuildContext context) { return Theme( - data: widget.theme.copyWith( - tooltipTheme: widget.theme.tooltipTheme.copyWith(preferBelow: true)), + data: theme.copyWith( + tooltipTheme: theme.tooltipTheme.copyWith(preferBelow: true)), child: AnnotatedRegion( value: imageEditorTheme.uiOverlayStyle, child: Scaffold( @@ -548,31 +251,26 @@ class BlurEditorState extends State with ImageEditorStateMixin { createRectTween: (begin, end) => RectTween(begin: begin, end: end), child: TransformedContentGenerator( - configs: widget.transformConfigs ?? TransformConfigs.empty(), + configs: transformConfigs ?? TransformConfigs.empty(), child: ImageWithMultipleFilters( - width: widget.imageSize.width, - height: widget.imageSize.height, + width: initConfigs.imageSize.width, + height: initConfigs.imageSize.height, designMode: designMode, - image: EditorImage( - assetPath: widget.assetPath, - byteArray: widget.byteArray, - file: widget.file, - networkUrl: widget.networkUrl, - ), - filters: widget.filters, - blur: selectedBlur, + image: editorImage, + filters: appliedFilters, + blurFactor: selectedBlur, ), ), ), - if (blurEditorConfigs.showLayers && widget.layers != null) + if (blurEditorConfigs.showLayers && layers != null) LayerStack( transformHelper: TransformHelper( - mainBodySize: widget.bodySizeWithLayers ?? Size.zero, - mainImageSize: widget.imageSizeWithLayers ?? Size.zero, + mainBodySize: bodySizeWithLayers, + mainImageSize: imageSizeWithLayers, editorBodySize: _bodySize, ), - configs: widget.configs, - layers: widget.layers!, + configs: configs, + layers: layers!, clipBehavior: Clip.none, ), ], @@ -591,11 +289,11 @@ class BlurEditorState extends State with ImageEditorStateMixin { min: 0, max: blurEditorConfigs.maxBlur, divisions: 100, - value: selectedBlur.blur, + value: selectedBlur, onChanged: (value) { - selectedBlur = BlurStateHistory(blur: value); + selectedBlur = value; setState(() {}); - widget.onUpdateUI?.call(); + onUpdateUI?.call(); }, ), ), diff --git a/lib/modules/filter_editor/filter_editor.dart b/lib/modules/filter_editor/filter_editor.dart index b8dbd361..2d057d9a 100644 --- a/lib/modules/filter_editor/filter_editor.dart +++ b/lib/modules/filter_editor/filter_editor.dart @@ -3,8 +3,6 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:pro_image_editor/models/filter_state_history.dart'; -import 'package:pro_image_editor/models/blur_state_history.dart'; import 'package:pro_image_editor/models/transform_helper.dart'; import 'package:pro_image_editor/pro_image_editor.dart'; import 'package:pro_image_editor/widgets/transformed_content_generator.dart'; @@ -12,14 +10,15 @@ import 'package:screenshot/screenshot.dart'; import '../../models/crop_rotate_editor/transform_factors.dart'; import '../../models/editor_image.dart'; -import '../../models/layer.dart'; -import '../../utils/helper/editor_mixin.dart'; +import '../../models/history/filter_state_history.dart'; +import '../../mixins/converted_configs.dart'; +import '../../mixins/standalone_editor.dart'; import '../../widgets/layer_stack.dart'; import '../../widgets/loading_dialog.dart'; import 'widgets/filter_editor_item_list.dart'; import 'widgets/image_with_filter.dart'; -/// The `FilterEditor` widget allows users to apply filters to images. +/// The `FilterEditor` widget allows users to editing images with painting tools. /// /// You can create a `FilterEditor` using one of the factory methods provided: /// - `FilterEditor.file`: Loads an image from a file. @@ -27,326 +26,82 @@ import 'widgets/image_with_filter.dart'; /// - `FilterEditor.network`: Loads an image from a network URL. /// - `FilterEditor.memory`: Loads an image from memory as a `Uint8List`. /// - `FilterEditor.autoSource`: Automatically selects the source based on provided parameters. -class FilterEditor extends StatefulWidget with ImageEditorMixin { +class FilterEditor extends StatefulWidget + with StandaloneEditor { @override - final ProImageEditorConfigs configs; - - /// A byte array representing the image data. - final Uint8List? byteArray; - - /// The file representing the image. - final File? file; - - /// The asset path of the image. - final String? assetPath; - - /// The network URL of the image. - final String? networkUrl; - - /// The transform configurations how the image should be initialized. - final TransformConfigs? transformConfigs; - - /// A callback function that can be used to update the UI from custom widgets. - final Function? onUpdateUI; - - /// The theme configuration for the editor. - final ThemeData theme; + final FilterEditorInitConfigs initConfigs; + @override + final EditorImage editorImage; - /// Determines whether to return the image as a Uint8List when closing the editor. + /// Constructs a `FilterEditor` widget. /// - /// If set to `true`, when closing the editor, the editor will return the final image - /// as a Uint8List, including all applied filter states. If set to `false`, only - /// the filter states will be returned. - final bool convertToUint8List; - - /// A list of Layer objects representing image layers. - final List? layers; - - /// The rendered image size with layers. - /// Required to calculate the correct layer position. - final Size? imageSizeWithLayers; - - /// The rendered body size with layers. - /// Required to calculate the correct layer position. - final Size? bodySizeWithLayers; - - final List? activeFilters; - - final BlurStateHistory? blur; - - /// Private constructor for creating a `FilterEditor` widget. + /// The [key] parameter is used to provide a key for the widget. + /// The [editorImage] parameter specifies the image to be edited. + /// The [initConfigs] parameter specifies the initialization configurations for the editor. const FilterEditor._({ super.key, - this.byteArray, - this.file, - this.assetPath, - this.networkUrl, - this.onUpdateUI, - this.convertToUint8List = false, - this.activeFilters, - this.blur, - this.transformConfigs, - this.imageSizeWithLayers, - this.bodySizeWithLayers, - this.layers, - required this.theme, - required this.configs, - }) : assert( - byteArray != null || - file != null || - networkUrl != null || - assetPath != null, - 'At least one of bytes, file, networkUrl, or assetPath must not be null.', - ); + required this.editorImage, + required this.initConfigs, + }); - /// Create a FilterEditor widget with an in-memory image represented as a Uint8List. - /// - /// This factory method allows you to create a FilterEditor widget that can be used to apply various image filters and edit an image represented as a Uint8List in memory. The provided parameters allow you to customize the appearance and behavior of the FilterEditor widget. - /// - /// Parameters: - /// - `byteArray`: A Uint8List representing the image data in memory. - /// - `key`: An optional Key to uniquely identify this widget in the widget tree. - /// - `theme`: An optional ThemeData object that defines the visual styling of the FilterEditor widget. - /// - `configs`: The image editor configs. - /// - `transformConfigs` The transform configurations how the image should be initialized. - /// - `convertToUint8List`: Determines whether to return the image as a Uint8List when closing the editor. - /// - /// Returns: - /// A FilterEditor widget configured with the provided parameters and the in-memory image data. - /// - /// Example Usage: - /// ```dart - /// final Uint8List imageBytes = ... // Load your image data here. - /// final filterEditor = FilterEditor.memory( - /// imageBytes, - /// theme: ThemeData.light(), - /// ); - /// ``` + /// Constructs a `FilterEditor` widget with image data loaded from memory. factory FilterEditor.memory( Uint8List byteArray, { Key? key, - required ThemeData theme, - TransformConfigs? transformConfigs, - ProImageEditorConfigs configs = const ProImageEditorConfigs(), - Function? onUpdateUI, - bool convertToUint8List = false, - Size? imageSizeWithLayers, - Size? bodySizeWithLayers, - List? layers, - List? activeFilters, - BlurStateHistory? blur, + required FilterEditorInitConfigs initConfigs, }) { return FilterEditor._( key: key, - byteArray: byteArray, - theme: theme, - transformConfigs: transformConfigs, - configs: configs, - onUpdateUI: onUpdateUI, - imageSizeWithLayers: imageSizeWithLayers, - bodySizeWithLayers: bodySizeWithLayers, - layers: layers, - activeFilters: activeFilters, - convertToUint8List: convertToUint8List, - blur: blur, + editorImage: EditorImage(byteArray: byteArray), + initConfigs: initConfigs, ); } - /// Create a FilterEditor widget with an image loaded from a File. - /// - /// This factory method allows you to create a FilterEditor widget that can be used to apply various image filters and edit an image loaded from a File. The provided parameters allow you to customize the appearance and behavior of the FilterEditor widget. - /// - /// Parameters: - /// - `file`: A File object representing the image file to be loaded. - /// - `key`: An optional Key to uniquely identify this widget in the widget tree. - /// - `theme`: An optional ThemeData object that defines the visual styling of the FilterEditor widget. - /// - `configs`: The image editor configs. - /// - `transformConfigs` The transform configurations how the image should be initialized. - /// - `convertToUint8List`: Determines whether to return the image as a Uint8List when closing the editor. - /// - /// Returns: - /// A FilterEditor widget configured with the provided parameters and the image loaded from the File. - /// - /// Example Usage: - /// ```dart - /// final File imageFile = ... // Provide the image file. - /// final filterEditor = FilterEditor.file( - /// imageFile, - /// theme: ThemeData.light(), - /// ); - /// ``` + /// Constructs a `FilterEditor` widget with an image loaded from a file. factory FilterEditor.file( File file, { Key? key, - required ThemeData theme, - TransformConfigs? transformConfigs, - ProImageEditorConfigs configs = const ProImageEditorConfigs(), - Function? onUpdateUI, - bool convertToUint8List = false, - Size? imageSizeWithLayers, - Size? bodySizeWithLayers, - List? layers, - List? activeFilters, - BlurStateHistory? blur, + required FilterEditorInitConfigs initConfigs, }) { return FilterEditor._( key: key, - file: file, - theme: theme, - transformConfigs: transformConfigs, - configs: configs, - onUpdateUI: onUpdateUI, - imageSizeWithLayers: imageSizeWithLayers, - bodySizeWithLayers: bodySizeWithLayers, - layers: layers, - activeFilters: activeFilters, - convertToUint8List: convertToUint8List, - blur: blur, + editorImage: EditorImage(file: file), + initConfigs: initConfigs, ); } - /// Create a FilterEditor widget with an image loaded from an asset. - /// - /// This factory method allows you to create a FilterEditor widget that can be used to apply various image filters and edit an image loaded from an asset. The provided parameters allow you to customize the appearance and behavior of the FilterEditor widget. - /// - /// Parameters: - /// - `assetPath`: A String representing the asset path of the image to be loaded. - /// - `key`: An optional Key to uniquely identify this widget in the widget tree. - /// - `theme`: An optional ThemeData object that defines the visual styling of the FilterEditor widget. - /// - `configs`: The image editor configs. - /// - `transformConfigs` The transform configurations how the image should be initialized. - /// - `convertToUint8List`: Determines whether to return the image as a Uint8List when closing the editor. - /// - /// Returns: - /// A FilterEditor widget configured with the provided parameters and the image loaded from the asset. - /// - /// Example Usage: - /// ```dart - /// final String assetPath = 'assets/image.png'; // Provide the asset path. - /// final filterEditor = FilterEditor.asset( - /// assetPath, - /// theme: ThemeData.light(), - /// ); - /// ``` + /// Constructs a `FilterEditor` widget with an image loaded from an asset. factory FilterEditor.asset( String assetPath, { Key? key, - required ThemeData theme, - TransformConfigs? transformConfigs, - ProImageEditorConfigs configs = const ProImageEditorConfigs(), - Function? onUpdateUI, - bool convertToUint8List = false, - Size? imageSizeWithLayers, - Size? bodySizeWithLayers, - List? layers, - List? activeFilters, - BlurStateHistory? blur, + required FilterEditorInitConfigs initConfigs, }) { return FilterEditor._( key: key, - assetPath: assetPath, - theme: theme, - transformConfigs: transformConfigs, - configs: configs, - onUpdateUI: onUpdateUI, - imageSizeWithLayers: imageSizeWithLayers, - bodySizeWithLayers: bodySizeWithLayers, - layers: layers, - activeFilters: activeFilters, - convertToUint8List: convertToUint8List, - blur: blur, + editorImage: EditorImage(assetPath: assetPath), + initConfigs: initConfigs, ); } - /// Create a FilterEditor widget with an image loaded from a network URL. - /// - /// This factory method allows you to create a FilterEditor widget that can be used to apply various image filters and edit an image loaded from a network URL. The provided parameters allow you to customize the appearance and behavior of the FilterEditor widget. - /// - /// Parameters: - /// - `networkUrl`: A String representing the network URL of the image to be loaded. - /// - `key`: An optional Key to uniquely identify this widget in the widget tree. - /// - `theme`: An optional ThemeData object that defines the visual styling of the FilterEditor widget. - /// - `configs`: The image editor configs. - /// - `transformConfigs` The transform configurations how the image should be initialized. - /// - `convertToUint8List`: Determines whether to return the image as a Uint8List when closing the editor. - /// - /// Returns: - /// A FilterEditor widget configured with the provided parameters and the image loaded from the network URL. - /// - /// Example Usage: - /// ```dart - /// final String imageUrl = 'https://example.com/image.jpg'; // Provide the network URL. - /// final filterEditor = FilterEditor.network( - /// imageUrl, - /// theme: ThemeData.light(), - /// ); - /// ``` + /// Constructs a `FilterEditor` widget with an image loaded from a network URL. factory FilterEditor.network( String networkUrl, { Key? key, - required ThemeData theme, - TransformConfigs? transformConfigs, - ProImageEditorConfigs configs = const ProImageEditorConfigs(), - Function? onUpdateUI, - bool convertToUint8List = false, - Size? imageSizeWithLayers, - Size? bodySizeWithLayers, - List? layers, - List? activeFilters, - BlurStateHistory? blur, + required FilterEditorInitConfigs initConfigs, }) { return FilterEditor._( key: key, - networkUrl: networkUrl, - theme: theme, - transformConfigs: transformConfigs, - configs: configs, - onUpdateUI: onUpdateUI, - imageSizeWithLayers: imageSizeWithLayers, - bodySizeWithLayers: bodySizeWithLayers, - layers: layers, - activeFilters: activeFilters, - blur: blur, - convertToUint8List: convertToUint8List, + editorImage: EditorImage(networkUrl: networkUrl), + initConfigs: initConfigs, ); } - /// Create a FilterEditor widget with automatic image source detection. + /// Constructs a `FilterEditor` widget with an image loaded automatically based on the provided source. /// - /// This factory method allows you to create a FilterEditor widget with automatic detection of the image source type (Uint8List, File, asset, or network URL). Based on the provided parameters, it selects the appropriate source type and creates the FilterEditor widget accordingly. - /// - /// Parameters: - /// - `key`: An optional Key to uniquely identify this widget in the widget tree. - /// - `theme`: An optional ThemeData object that defines the visual styling of the FilterEditor widget. - /// - `byteArray`: An optional Uint8List representing the image data in memory. - /// - `file`: An optional File object representing the image file to be loaded. - /// - `assetPath`: An optional String representing the asset path of the image to be loaded. - /// - `networkUrl`: An optional String representing the network URL of the image to be loaded. - /// - `convertToUint8List`: Determines whether to return the image as a Uint8List when closing the editor. - /// - /// Returns: - /// A FilterEditor widget configured with the provided parameters and the detected image source. - /// - /// Example Usage: - /// ```dart - /// // Provide one of the image sources: byteArray, file, assetPath, or networkUrl. - /// final filterEditor = FilterEditor.autoSource( - /// byteArray: imageBytes, - /// theme: ThemeData.light(), - /// ); - /// ``` + /// Either [byteArray], [file], [networkUrl], or [assetPath] must be provided. factory FilterEditor.autoSource({ Key? key, - required ThemeData theme, - TransformConfigs? transformConfigs, - ProImageEditorConfigs configs = const ProImageEditorConfigs(), - Function? onUpdateUI, - bool convertToUint8List = false, - Size? imageSizeWithLayers, - Size? bodySizeWithLayers, - List? layers, - List? activeFilters, - BlurStateHistory? blur, + required FilterEditorInitConfigs initConfigs, Uint8List? byteArray, File? file, String? assetPath, @@ -356,61 +111,25 @@ class FilterEditor extends StatefulWidget with ImageEditorMixin { return FilterEditor.memory( byteArray, key: key, - theme: theme, - transformConfigs: transformConfigs, - configs: configs, - onUpdateUI: onUpdateUI, - imageSizeWithLayers: imageSizeWithLayers, - bodySizeWithLayers: bodySizeWithLayers, - layers: layers, - activeFilters: activeFilters, - blur: blur, - convertToUint8List: convertToUint8List, + initConfigs: initConfigs, ); } else if (file != null) { return FilterEditor.file( file, key: key, - theme: theme, - transformConfigs: transformConfigs, - configs: configs, - onUpdateUI: onUpdateUI, - imageSizeWithLayers: imageSizeWithLayers, - bodySizeWithLayers: bodySizeWithLayers, - layers: layers, - activeFilters: activeFilters, - blur: blur, - convertToUint8List: convertToUint8List, + initConfigs: initConfigs, ); } else if (networkUrl != null) { return FilterEditor.network( networkUrl, key: key, - theme: theme, - transformConfigs: transformConfigs, - configs: configs, - onUpdateUI: onUpdateUI, - imageSizeWithLayers: imageSizeWithLayers, - bodySizeWithLayers: bodySizeWithLayers, - layers: layers, - activeFilters: activeFilters, - blur: blur, - convertToUint8List: convertToUint8List, + initConfigs: initConfigs, ); } else if (assetPath != null) { return FilterEditor.asset( assetPath, key: key, - theme: theme, - transformConfigs: transformConfigs, - configs: configs, - onUpdateUI: onUpdateUI, - imageSizeWithLayers: imageSizeWithLayers, - bodySizeWithLayers: bodySizeWithLayers, - layers: layers, - activeFilters: activeFilters, - blur: blur, - convertToUint8List: convertToUint8List, + initConfigs: initConfigs, ); } else { throw ArgumentError( @@ -423,7 +142,10 @@ class FilterEditor extends StatefulWidget with ImageEditorMixin { } /// The state class for the `FilterEditor` widget. -class FilterEditorState extends State with ImageEditorStateMixin { +class FilterEditorState extends State + with + ImageEditorConvertedConfigs, + StandaloneEditorState { /// Manages the capturing a screenshot of the image. ScreenshotController screenshotController = ScreenshotController(); @@ -448,13 +170,13 @@ class FilterEditorState extends State with ImageEditorStateMixin { void done() async { if (_createScreenshot) return; - if (widget.convertToUint8List) { + if (widget.initConfigs.convertToUint8List) { _createScreenshot = true; LoadingDialog loading = LoadingDialog() ..show( context, i18n: i18n, - theme: widget.theme, + theme: theme, designMode: designMode, message: i18n.filterEditor.applyFilterDialogMsg, imageEditorTheme: imageEditorTheme, @@ -477,8 +199,8 @@ class FilterEditorState extends State with ImageEditorStateMixin { @override Widget build(BuildContext context) { return Theme( - data: widget.theme.copyWith( - tooltipTheme: widget.theme.tooltipTheme.copyWith(preferBelow: true)), + data: theme.copyWith( + tooltipTheme: theme.tooltipTheme.copyWith(preferBelow: true)), child: AnnotatedRegion( value: imageEditorTheme.uiOverlayStyle, child: Scaffold( @@ -531,32 +253,27 @@ class FilterEditorState extends State with ImageEditorStateMixin { createRectTween: (begin, end) => RectTween(begin: begin, end: end), child: TransformedContentGenerator( - configs: widget.transformConfigs ?? TransformConfigs.empty(), + configs: transformConfigs ?? TransformConfigs.empty(), child: ImageWithFilter( - image: EditorImage( - file: widget.file, - byteArray: widget.byteArray, - networkUrl: widget.networkUrl, - assetPath: widget.assetPath, - ), - activeFilters: widget.activeFilters, + image: editorImage, + activeFilters: appliedFilters, designMode: designMode, filter: selectedFilter, - blur: widget.blur, + blurFactor: appliedBlurFactor, fit: BoxFit.contain, opacity: filterOpacity, ), ), ), - if (filterEditorConfigs.showLayers && widget.layers != null) + if (filterEditorConfigs.showLayers && layers != null) LayerStack( transformHelper: TransformHelper( - mainBodySize: widget.bodySizeWithLayers ?? Size.zero, - mainImageSize: widget.imageSizeWithLayers ?? Size.zero, + mainBodySize: bodySizeWithLayers, + mainImageSize: imageSizeWithLayers, editorBodySize: _bodySize, ), - configs: widget.configs, - layers: widget.layers!, + configs: configs, + layers: layers!, clipBehavior: Clip.none, ), ], @@ -585,24 +302,24 @@ class FilterEditorState extends State with ImageEditorStateMixin { onChanged: (value) { filterOpacity = value; setState(() {}); - widget.onUpdateUI?.call(); + onUpdateUI?.call(); }, ), ), FilterEditorItemList( - byteArray: widget.byteArray, - file: widget.file, - assetPath: widget.assetPath, - networkUrl: widget.networkUrl, - activeFilters: widget.activeFilters, - blur: widget.blur, - configs: widget.configs, - transformConfigs: widget.transformConfigs, + byteArray: widget.editorImage.byteArray, + file: widget.editorImage.file, + assetPath: widget.editorImage.assetPath, + networkUrl: widget.editorImage.networkUrl, + activeFilters: appliedFilters, + blurFactor: appliedBlurFactor, + configs: configs, + transformConfigs: transformConfigs, selectedFilter: selectedFilter, onSelectFilter: (filter) { selectedFilter = filter; setState(() {}); - widget.onUpdateUI?.call(); + onUpdateUI?.call(); }, ), ], diff --git a/lib/modules/filter_editor/widgets/filter_editor_item_list.dart b/lib/modules/filter_editor/widgets/filter_editor_item_list.dart index 52e280fe..02b11e49 100644 --- a/lib/modules/filter_editor/widgets/filter_editor_item_list.dart +++ b/lib/modules/filter_editor/widgets/filter_editor_item_list.dart @@ -5,14 +5,13 @@ import 'package:colorfilter_generator/colorfilter_generator.dart'; import 'package:colorfilter_generator/presets.dart'; import 'package:flutter/material.dart'; import 'package:pro_image_editor/models/editor_configs/pro_image_editor_configs.dart'; -import 'package:pro_image_editor/models/filter_state_history.dart'; import 'package:pro_image_editor/models/theme/theme.dart'; import 'package:pro_image_editor/widgets/pro_image_editor_desktop_mode.dart'; import 'package:pro_image_editor/widgets/transformed_content_generator.dart'; -import '../../../models/blur_state_history.dart'; import '../../../models/crop_rotate_editor/transform_factors.dart'; import '../../../models/editor_image.dart'; +import '../../../models/history/filter_state_history.dart'; import 'image_with_filter.dart'; class FilterEditorItemList extends StatefulWidget { @@ -41,10 +40,8 @@ class FilterEditorItemList extends StatefulWidget { /// If provided, this list contains the history of active filters applied to the image. final List? activeFilters; - /// Specifies the blur state history. - /// - /// If provided, this object contains the history of blur states applied to the image. - final BlurStateHistory? blur; + /// Specifies the blur factor. + final double? blurFactor; /// Specifies the selected filter. /// @@ -66,7 +63,7 @@ class FilterEditorItemList extends StatefulWidget { this.assetPath, this.networkUrl, this.activeFilters, - this.blur, + this.blurFactor, this.itemScaleFactor, this.transformConfigs, required this.selectedFilter, @@ -193,7 +190,7 @@ class _FilterEditorItemListState extends State { size: size, designMode: widget.configs.designMode, filter: filter, - blur: widget.blur, + blurFactor: widget.blurFactor, fit: BoxFit.cover, ), ), @@ -256,7 +253,7 @@ class _FilterEditorItemListState extends State { size: size, designMode: widget.configs.designMode, filter: filter, - blur: widget.blur, + blurFactor: widget.blurFactor, fit: BoxFit.cover, ), ), diff --git a/lib/modules/filter_editor/widgets/image_with_filter.dart b/lib/modules/filter_editor/widgets/image_with_filter.dart index 18e80fa8..a167b164 100644 --- a/lib/modules/filter_editor/widgets/image_with_filter.dart +++ b/lib/modules/filter_editor/widgets/image_with_filter.dart @@ -3,11 +3,10 @@ import 'dart:ui'; import 'package:colorfilter_generator/colorfilter_generator.dart'; import 'package:flutter/material.dart'; import 'package:pro_image_editor/models/editor_image.dart'; -import 'package:pro_image_editor/models/filter_state_history.dart'; -import 'package:pro_image_editor/models/blur_state_history.dart'; import 'package:pro_image_editor/utils/design_mode.dart'; import 'package:pro_image_editor/widgets/auto_image.dart'; +import '../../../models/history/filter_state_history.dart'; import '../utils/generate_filtered_image.dart'; /// A widget that displays an image with a customizable color filter applied to it. @@ -31,7 +30,7 @@ class ImageWithFilter extends StatelessWidget { final List? activeFilters; - final BlurStateHistory? blur; + final double? blurFactor; /// Creates an `ImageWithFilter` widget. /// @@ -44,7 +43,7 @@ class ImageWithFilter extends StatelessWidget { required this.filter, required this.designMode, required this.activeFilters, - required this.blur, + required this.blurFactor, this.size, this.fit, this.opacity = 1, @@ -61,7 +60,7 @@ class ImageWithFilter extends StatelessWidget { ); if (filter.filters.isEmpty && activeFilters == null) { - if (blur == null) { + if (blurFactor == null) { return img; } else { return Stack( @@ -70,7 +69,8 @@ class ImageWithFilter extends StatelessWidget { children: [ img, BackdropFilter( - filter: ImageFilter.blur(sigmaX: blur!.blur, sigmaY: blur!.blur), + filter: + ImageFilter.blur(sigmaX: blurFactor!, sigmaY: blurFactor!), child: Container( decoration: BoxDecoration(color: Colors.white.withOpacity(0.0)), ), @@ -93,7 +93,7 @@ class ImageWithFilter extends StatelessWidget { ); } - if (blur == null) { + if (blurFactor == null) { return Stack( alignment: Alignment.center, fit: StackFit.expand, @@ -110,7 +110,8 @@ class ImageWithFilter extends StatelessWidget { img, filteredImg, BackdropFilter( - filter: ImageFilter.blur(sigmaX: blur!.blur, sigmaY: blur!.blur), + filter: + ImageFilter.blur(sigmaX: blurFactor!, sigmaY: blurFactor!), child: Container( decoration: BoxDecoration(color: Colors.white.withOpacity(0.0)), ), diff --git a/lib/modules/filter_editor/widgets/image_with_multiple_filters.dart b/lib/modules/filter_editor/widgets/image_with_multiple_filters.dart index b08c9234..2856ccb5 100644 --- a/lib/modules/filter_editor/widgets/image_with_multiple_filters.dart +++ b/lib/modules/filter_editor/widgets/image_with_multiple_filters.dart @@ -4,8 +4,7 @@ import 'package:flutter/material.dart'; import 'package:pro_image_editor/models/editor_image.dart'; import 'package:pro_image_editor/pro_image_editor.dart'; -import '../../../models/filter_state_history.dart'; -import '../../../models/blur_state_history.dart'; +import '../../../models/history/filter_state_history.dart'; import '../../../widgets/auto_image.dart'; import '../utils/generate_filtered_image.dart'; @@ -28,8 +27,8 @@ class ImageWithMultipleFilters extends StatelessWidget { /// The editor image to display. final EditorImage image; - /// The blur state history to apply a blur effect on the image. - final BlurStateHistory blur; + /// The blur factor + final double blurFactor; const ImageWithMultipleFilters({ super.key, @@ -38,7 +37,7 @@ class ImageWithMultipleFilters extends StatelessWidget { required this.designMode, required this.filters, required this.image, - required this.blur, + required this.blurFactor, }); @override @@ -66,7 +65,7 @@ class ImageWithMultipleFilters extends StatelessWidget { img, filteredImg, BackdropFilter( - filter: ImageFilter.blur(sigmaX: blur.blur, sigmaY: blur.blur), + filter: ImageFilter.blur(sigmaX: blurFactor, sigmaY: blurFactor), child: Container( width: width, height: height, diff --git a/lib/pro_image_editor_main.dart b/lib/modules/main_editor/main_editor.dart similarity index 53% rename from lib/pro_image_editor_main.dart rename to lib/modules/main_editor/main_editor.dart index c21241e8..b5aa35cc 100644 --- a/lib/pro_image_editor_main.dart +++ b/lib/modules/main_editor/main_editor.dart @@ -4,52 +4,61 @@ import 'dart:io'; import 'package:colorfilter_generator/presets.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:pro_image_editor/designs/whatsapp/whatsapp_appbar.dart'; import 'package:pro_image_editor/models/import_export/utils/export_import_enum.dart'; +import 'package:pro_image_editor/models/init_configs/blur_editor_init_configs.dart'; +import 'package:pro_image_editor/models/init_configs/filter_editor_init_configs.dart'; +import 'package:pro_image_editor/modules/main_editor/utils/layer_manager.dart'; import 'package:pro_image_editor/models/theme/theme_editor_mode.dart'; +import 'package:pro_image_editor/modules/main_editor/utils/main_editor_controllers.dart'; +import 'package:pro_image_editor/modules/main_editor/utils/state_manager.dart'; import 'package:pro_image_editor/modules/sticker_editor.dart'; import 'package:pro_image_editor/utils/design_mode.dart'; -import 'package:pro_image_editor/utils/helper/editor_mixin.dart'; +import 'package:pro_image_editor/mixins/editor_configs_mixin.dart'; import 'package:pro_image_editor/utils/swipe_mode.dart'; import 'package:screenshot/screenshot.dart'; import 'package:vibration/vibration.dart'; -import 'package:image/image.dart' as img; - -import 'designs/whatsapp/whatsapp_filter_button.dart'; -import 'designs/whatsapp/whatsapp_sticker_editor.dart'; -import 'models/crop_rotate_editor/transform_factors.dart'; -import 'models/history/state_history.dart'; -import 'models/history/last_position.dart'; -import 'models/crop_rotate_editor_response.dart'; -import 'models/editor_image.dart'; -import 'models/filter_state_history.dart'; -import 'models/blur_state_history.dart'; -import 'models/import_export/export_state_history.dart'; -import 'models/import_export/export_state_history_configs.dart'; -import 'models/import_export/import_state_history.dart'; -import 'models/layer.dart'; -import 'modules/crop_rotate_editor/crop_rotate_editor.dart'; -import 'modules/emoji_editor/emoji_editor.dart'; -import 'modules/filter_editor/filter_editor.dart'; -import 'modules/filter_editor/widgets/filter_editor_item_list.dart'; -import 'modules/filter_editor/widgets/image_with_multiple_filters.dart'; -import 'modules/blur_editor.dart'; -import 'modules/paint_editor/paint_editor.dart'; -import 'modules/text_editor.dart'; -import 'utils/debounce.dart'; -import 'models/editor_configs/pro_image_editor_configs.dart'; -import 'widgets/adaptive_dialog.dart'; -import 'widgets/flat_icon_text_button.dart'; -import 'widgets/layer_widget.dart'; -import 'widgets/loading_dialog.dart'; -import 'widgets/pro_image_editor_desktop_mode.dart'; -import 'widgets/transformed_content_generator.dart'; - -typedef ImageEditingCompleteCallback = Future Function(Uint8List bytes); -typedef ImageEditingEmptyCallback = void Function(); + +import '../../designs/whatsapp/whatsapp_filter_button.dart'; +import '../../designs/whatsapp/whatsapp_sticker_editor.dart'; +import '../../mixins/main_editor/main_editor_global_keys.dart'; +import '../../utils/image_helpers.dart'; +import '../filter_editor/widgets/image_with_multiple_filters.dart'; +import 'utils/desktop_interaction_manager.dart'; +import 'utils/main_editor_callbacks.dart'; +import 'utils/screen_size_helper.dart'; +import 'utils/whatsapp_helper.dart'; +import '../../models/crop_rotate_editor/transform_factors.dart'; +import '../../models/history/filter_state_history.dart'; +import '../../models/history/state_history.dart'; +import '../../models/history/last_position.dart'; +import '../../models/crop_rotate_editor_response.dart'; +import '../../models/editor_image.dart'; +import '../../models/history/blur_state_history.dart'; +import '../../models/import_export/export_state_history.dart'; +import '../../models/import_export/export_state_history_configs.dart'; +import '../../models/import_export/import_state_history.dart'; +import '../../models/layer.dart'; +import '../../models/init_configs/paint_editor_init_configs.dart'; +import 'utils/layer_interaction_helper.dart'; +import '../crop_rotate_editor/crop_rotate_editor.dart'; +import '../emoji_editor/emoji_editor.dart'; +import '../filter_editor/filter_editor.dart'; +import '../filter_editor/widgets/filter_editor_item_list.dart'; +import '../blur_editor.dart'; +import '../paint_editor/paint_editor.dart'; +import '../text_editor.dart'; +import '../../utils/debounce.dart'; +import '../../models/editor_configs/pro_image_editor_configs.dart'; +import '../../mixins/converted_configs.dart'; +import '../../widgets/adaptive_dialog.dart'; +import '../../widgets/flat_icon_text_button.dart'; +import '../../widgets/layer_widget.dart'; +import '../../widgets/loading_dialog.dart'; +import '../../widgets/pro_image_editor_desktop_mode.dart'; +import '../../widgets/transformed_content_generator.dart'; /// A widget for image editing using ProImageEditor. /// @@ -74,7 +83,7 @@ typedef ImageEditingEmptyCallback = void Function(); /// /// See also: /// - [ProImageEditorConfigs] for configuring image editing options. -class ProImageEditor extends StatefulWidget with ImageEditorMixin { +class ProImageEditor extends StatefulWidget with SimpleConfigsAccess { @override final ProImageEditorConfigs configs; @@ -311,72 +320,39 @@ class ProImageEditor extends StatefulWidget with ImageEditorMixin { } class ProImageEditorState extends State - with ImageEditorStateMixin { - /// A GlobalKey for the Painting Editor, used to access and control the state of the painting editor. - final paintingEditor = GlobalKey(); - - /// A GlobalKey for the Text Editor, used to access and control the state of the text editor. - final textEditor = GlobalKey(); - - /// A GlobalKey for the Crop and Rotate Editor, used to access and control the state of the crop and rotate editor. - final cropRotateEditor = GlobalKey(); - - /// A GlobalKey for the Filter Editor, used to access and control the state of the filter editor. - final filterEditor = GlobalKey(); + with + ImageEditorConvertedConfigs, + SimpleConfigsAccessState, + MainEditorGlobalKeys { + /// Helper class for managing screen sizes and layout calculations. + late final ScreenSizeHelper _screenSize; - /// A GlobalKey for the Blur Editor, used to access and control the state of the blur editor. - final blurEditor = GlobalKey(); + /// Manager class for handling desktop interactions in the image editor. + late final DesktopInteractionManager _desktopInteractionManager; - /// A GlobalKey for the Emoji Editor, used to access and control the state of the emoji editor. - final emojiEditor = GlobalKey(); + /// Controller instances for managing various aspects of the main editor. + final MainEditorControllers _controllers = MainEditorControllers(); - /// Stream controller for tracking mouse movement within the editor. - late StreamController _mouseMoveStream; + /// Manager class for handling layer interactions in the editor. + final LayerManager _layerManager = LayerManager(); - /// Scroll controller for the bottom bar in the editor interface. - late ScrollController _bottomBarScrollCtrl; + /// Manager class for managing the state of the editor. + final StateManager _stateManager = StateManager(); - /// Controller for capturing screenshots of the editor content. - late ScreenshotController _screenshotCtrl; + /// Helper class for managing WhatsApp filters. + final WhatsAppHelper _whatsAppHelper = WhatsAppHelper(); - /// List to track changes made to the image during editing. - List _imgStateHistory = []; - - /// List to store the history of image editor changes. - List stateHistory = []; + /// Helper class for managing interactions with layers in the editor. + final LayerInteractionHelper _layerInteraction = LayerInteractionHelper(); /// The current theme used by the image editor. late ThemeData _theme; - /// Debounce for scaling actions in the editor. - late Debounce _scaleDebounce; - - /// Debounce for handling changes in screen size. - late Debounce _screenSizeDebouncer; - - /// Stores the last recorded screen size. - Size _lastScreenSize = const Size(0, 0); - - /// Stores the last recorded body size. - Size _bodySize = Size.zero; - - /// Stores the last recorded image size. - Size _renderedImageSize = Size.zero; - - /// Getter for the screen size of the device. - Size get _screen => MediaQuery.of(context).size; - - /// Getter for the active layer currently being edited. - Layer get _activeLayer => activeLayers[_selectedLayer]; - /// Temporary layer used during editing. Layer? _tempLayer; - /// Position in the edit history. - int _editPosition = 0; - /// Index of the selected layer. - int _selectedLayer = -1; + int _selectedLayerIndex = -1; /// Flag indicating if the editor has been initialized. bool _inited = false; @@ -384,123 +360,21 @@ class ProImageEditorState extends State /// Flag indicating if the crop tool is active. bool _activeCrop = false; - /// Flag indicating if the scaling tool is active. - bool _activeScale = false; - - /// Flag indicating if the remove button is hovered. - bool hoverRemoveBtn = false; - /// Flag indicating if the image needs decoding. bool _imageNeedDecode = true; - /// Flag indicating if vertical helper lines should be displayed. - bool _showVerticalHelperLine = false; - - /// Flag indicating if horizontal helper lines should be displayed. - bool _showHorizontalHelperLine = false; - - /// Flag indicating if rotation helper lines should be displayed. - bool _showRotationHelperLine = false; - - /// Flag indicating if the device can vibrate. - bool _deviceCanVibrate = false; - - /// Flag indicating if the device can perform custom vibration. - bool _deviceCanCustomVibrate = false; - - /// Flag indicating if rotation helper lines have started. - bool _rotationStartedHelper = false; - - /// Flag indicating if helper lines should be displayed. - bool _showHelperLines = false; - - /// Controls high-performance scaling for free-style drawing. - /// When `true`, enables optimized scaling for improved performance. - bool _freeStyleHighPerformanceScaling = false; - - /// Controls high-performance moving for free-style drawing. - /// When `true`, enables optimized moving for improved performance. - bool _freeStyleHighPerformanceMoving = false; - - /// Enables or disables hit detection. - /// When `true`, allows detecting user interactions with the painted layer. - bool _enabledHitDetection = true; - /// Flag to track if editing is completed. bool _doneEditing = false; /// The pixel ratio of the device's screen. double _pixelRatio = 1; - /// Y-coordinate of the rotation helper line. - double _rotationHelperLineY = 0; - - /// X-coordinate of the rotation helper line. - double _rotationHelperLineX = 0; - - /// Rotation angle of the rotation helper line. - double _rotationHelperLineDeg = 0; - - /// The base scale factor for the editor. - double _baseScaleFactor = 1.0; - - /// The base angle factor for the editor. - double _baseAngleFactor = 0; - - /// X-coordinate where snapping started. - double _snapStartPosX = 0; - - /// Y-coordinate where snapping started. - double _snapStartPosY = 0; - - /// Initial rotation angle when snapping started. - double _snapStartRotation = 0; - - /// Last recorded rotation angle during snapping. - double _snapLastRotation = 0; - - /// Span for detecting hits on layers. - final double _hitSpan = 10; - - /// Width of the image being edited. - double _imageWidth = 0; - - /// Height of the image being edited. - double _imageHeight = 0; - - /// Getter for the screen inner height, excluding top and bottom padding. - double get _screenInnerHeight => - _screen.height - - _screenPadding.top - - _screenPadding.bottom - - _allToolbarHeight; - - /// Getter for the X-coordinate of the middle of the screen. - double get _screenMiddleX => - _screen.width / 2 - (_screenPadding.left + _screenPadding.right) / 2; - - /// Getter for the Y-coordinate of the middle of the screen. - double get _screenMiddleY => - _screen.height / 2 - (_screenPadding.top + _screenPadding.bottom) / 2; - - /// Last recorded X-axis position for layers. - LayerLastPosition _lastPositionX = LayerLastPosition.center; - - /// Last recorded Y-axis position for layers. - LayerLastPosition _lastPositionY = LayerLastPosition.center; - - /// Getter for the screen padding, accounting for safe area insets. - EdgeInsets get _screenPadding => MediaQuery.of(context).padding; - /// Whether an editor is currently open. - bool _openEditor = false; + bool _isEditorOpen = false; /// Whether a dialog is currently open. bool _openDialog = false; - /// Represents the helper value for showing WhatsApp filters. - double _whatsAppFilterShowHelper = 0; - /// Represents the direction of swipe action. SwipeMode _swipeDirection = SwipeMode.none; @@ -513,35 +387,56 @@ class ProImageEditorState extends State /// Store the last device Orientation int _deviceOrientation = 0; + /// Getter for the active layer currently being edited. + Layer get _activeLayer => activeLayers[_selectedLayerIndex]; + + /// Get the list of layers from the current image editor changes. + List get activeLayers => + _stateManager.stateHistory[_stateManager.editPosition].layers; + + /// List to store the history of image editor changes. + List get stateHistory => _stateManager.stateHistory; + + /// Determines whether undo actions can be performed on the current state. + bool get canUndo => _stateManager.editPosition > 0; + + /// Determines whether redo actions can be performed on the current state. + bool get canRedo => + _stateManager.editPosition < _stateManager.stateHistory.length - 1; + @override void initState() { super.initState(); - _mouseMoveStream = StreamController.broadcast(); - _screenshotCtrl = ScreenshotController(); - _scaleDebounce = Debounce(const Duration(milliseconds: 100)); - _screenSizeDebouncer = Debounce(const Duration(milliseconds: 200)); - - _bottomBarScrollCtrl = ScrollController(); + _desktopInteractionManager = DesktopInteractionManager( + configs: configs, + context: context, + onUpdateUI: widget.onUpdateUI, + setState: setState, + ); + _screenSize = ScreenSizeHelper(configs: configs, context: context); + _layerInteraction.scaleDebounce = + Debounce(const Duration(milliseconds: 100)); - _imgStateHistory.add(EditorImage( + _stateManager.imgStateHistory.add(EditorImage( assetPath: widget.assetPath, byteArray: widget.byteArray, file: widget.file, networkUrl: widget.networkUrl, )); - stateHistory.add(EditorStateHistory( + _stateManager.stateHistory.add(EditorStateHistory( transformConfigs: TransformConfigs.empty(), bytesRefIndex: 0, blur: BlurStateHistory(), layers: [], filters: [])); - Vibration.hasVibrator().then((value) => _deviceCanVibrate = value ?? false); - Vibration.hasCustomVibrationsSupport() - .then((value) => _deviceCanCustomVibrate = value ?? false); + Vibration.hasVibrator() + .then((value) => _layerInteraction.deviceCanVibrate = value ?? false); + Vibration.hasCustomVibrationsSupport().then( + (value) => _layerInteraction.deviceCanCustomVibrate = value ?? false); - ServicesBinding.instance.keyboard.addHandler(_onKey); + ServicesBinding.instance.keyboard.addHandler(_onKeyEvent); if (kIsWeb) { _browserContextMenuBeforeEnabled = BrowserContextMenu.enabled; BrowserContextMenu.disableContextMenu(); @@ -550,223 +445,121 @@ class ProImageEditorState extends State @override void dispose() { - _mouseMoveStream.close(); - _bottomBarScrollCtrl.dispose(); - _scaleDebounce.dispose(); - _screenSizeDebouncer.dispose(); + _controllers.dispose(); + _layerInteraction.scaleDebounce.dispose(); + _screenSize.screenSizeDebouncer.dispose(); SystemChrome.setSystemUIOverlayStyle(_theme.brightness == Brightness.dark ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark); SystemChrome.restoreSystemUIOverlays(); - ServicesBinding.instance.keyboard.removeHandler(_onKey); + ServicesBinding.instance.keyboard.removeHandler(_onKeyEvent); if (kIsWeb && _browserContextMenuBeforeEnabled) { BrowserContextMenu.enableContextMenu(); } super.dispose(); } - /// Handles keyboard events. - /// - /// This method responds to key events and performs actions based on the pressed keys. - /// If the 'Escape' key is pressed and the widget is still mounted, it triggers the navigator to pop the current context. - bool _onKey(KeyEvent event) { - final key = event.logicalKey.keyLabel; - - if (mounted && event is KeyDownEvent) { - switch (key) { - case 'Escape': - if (!_activeCrop && !_openDialog) { - if (_openEditor) { - Navigator.pop(context); - } else { - closeEditor(); - } - } - break; - - case 'Subtract': - case 'Numpad Subtract': - case 'Page Down': - case 'Arrow Down': - _keyboardZoom(true); - break; - case 'Add': - case 'Numpad Add': - case 'Page Up': - case 'Arrow Up': - _keyboardZoom(false); - break; - case 'Arrow Left': - _keyboardRotate(true); - break; - case 'Arrow Right': - _keyboardRotate(false); - break; - } - } - - return false; - } - - /// Get the list of layers from the current image editor changes. - List get activeLayers => stateHistory[_editPosition].layers; - - /// Get the list of filters from the current image editor changes. - List get _filters => stateHistory[_editPosition].filters; - - /// Get the transformconfigurations from the crop/ rotate editor. - TransformConfigs get _transformConfigs => - stateHistory[_editPosition].transformConfigs; - - /// Get the blur state from the current image editor changes. - BlurStateHistory get _blur => stateHistory[_editPosition].blur; - - /// Get the current image being edited from the change list. - EditorImage get _image => - _imgStateHistory[stateHistory[_editPosition].bytesRefIndex]; - - /// Returns the total height of all toolbars. - double get _allToolbarHeight => _appBarHeight + _bottomBarHeight; - - /// Returns the height of the app bar. - double get _appBarHeight => - widget.configs.imageEditorTheme.editorMode == ThemeEditorMode.simple - ? kToolbarHeight - : 0; - - /// Returns the height of the bottom bar. - double get _bottomBarHeight => - widget.configs.imageEditorTheme.editorMode == ThemeEditorMode.simple - ? kBottomNavigationBarHeight - : 0; - - /// Clean forward changes in the history. - /// - /// This method removes any changes made after the current edit position in the history. - void _cleanForwardChanges() { - if (stateHistory.length > 1) { - while (_editPosition < stateHistory.length - 1) { - stateHistory.removeLast(); - if (_imgStateHistory.length - 1 > stateHistory.last.bytesRefIndex) { - _imgStateHistory.removeLast(); - } - } - } - _editPosition = stateHistory.length - 1; + bool _onKeyEvent(KeyEvent event) { + return _desktopInteractionManager.onKey( + event, + activeLayer: _activeLayer, + canPressEscape: !_activeCrop && !_openDialog, + isEditorOpen: _isEditorOpen, + onCloseEditor: closeEditor, + ); } /// Add a cropped image to the editor. /// /// This method adds a cropped image to the editor and updates the editing state. void _addCroppedImg(List layers, EditorImage image) { - _cleanForwardChanges(); - _imgStateHistory.add(image); + _stateManager.cleanForwardChanges(); + _stateManager.imgStateHistory.add(image); stateHistory.add( EditorStateHistory( transformConfigs: TransformConfigs.empty(), - bytesRefIndex: _imgStateHistory.length - 1, - blur: _blur, + bytesRefIndex: _stateManager.imgStateHistory.length - 1, + blur: _stateManager.blurStateHistory, layers: layers, - filters: _filters, + filters: _stateManager.filters, ), ); - _editPosition = stateHistory.length - 1; + _stateManager.editPosition = stateHistory.length - 1; } /// Add a new layer to the image editor. /// /// This method adds a new layer to the image editor and updates the editing state. void addLayer(Layer layer, {int removeLayerIndex = -1, EditorImage? image}) { - _cleanForwardChanges(); - if (image != null) _imgStateHistory.add(image); + _stateManager.cleanForwardChanges(); + if (image != null) _stateManager.imgStateHistory.add(image); stateHistory.add( EditorStateHistory( transformConfigs: TransformConfigs.empty(), - bytesRefIndex: _imgStateHistory.length - 1, - blur: _blur, - layers: - List.from(stateHistory.last.layers.map((e) => _copyLayer(e))) - ..add(layer), - filters: _filters, + bytesRefIndex: _stateManager.imgStateHistory.length - 1, + blur: _stateManager.blurStateHistory, + layers: List.from( + stateHistory.last.layers.map((e) => _layerManager.copyLayer(e))) + ..add(layer), + filters: _stateManager.filters, ), ); - _editPosition++; + _stateManager.editPosition++; if (removeLayerIndex >= 0) { activeLayers.removeAt(removeLayerIndex); } } - /// Update the temporary layer in the editor. - /// - /// This method updates the temporary layer in the editor and updates the editing state. - void _updateTempLayer() { - _cleanForwardChanges(); - stateHistory.add( - EditorStateHistory( - transformConfigs: TransformConfigs.empty(), - bytesRefIndex: _imgStateHistory.length - 1, - blur: _blur, - layers: List.from(stateHistory.last.layers.map((e) => _copyLayer(e))), - filters: _filters, - ), - ); - var oldIndex = - activeLayers.indexWhere((element) => element.id == _tempLayer!.id); - if (oldIndex >= 0) { - stateHistory[_editPosition].layers[oldIndex] = _copyLayer(_tempLayer!); - } - _editPosition++; - _tempLayer = null; - } - /// Remove a layer from the editor. /// /// This method removes a layer from the editor and updates the editing state. - void _removeLayer(int layerPos, {Layer? layer}) { - _cleanForwardChanges(); - var layers = List.from(activeLayers.map((e) => _copyLayer(e))); + void removeLayer(int layerPos, {Layer? layer}) { + _stateManager.cleanForwardChanges(); + var layers = + List.from(activeLayers.map((e) => _layerManager.copyLayer(e))); layers.removeAt(layerPos); stateHistory.add( EditorStateHistory( transformConfigs: TransformConfigs.empty(), - bytesRefIndex: _imgStateHistory.length - 1, - blur: _blur, + bytesRefIndex: _stateManager.imgStateHistory.length - 1, + blur: _stateManager.blurStateHistory, layers: layers, - filters: _filters, + filters: _stateManager.filters, ), ); var oldIndex = activeLayers .indexWhere((element) => element.id == (layer?.id ?? _tempLayer!.id)); if (oldIndex >= 0) { - stateHistory[_editPosition].layers[oldIndex] = - _copyLayer(layer ?? _tempLayer!); + stateHistory[_stateManager.editPosition].layers[oldIndex] = + _layerManager.copyLayer(layer ?? _tempLayer!); } - _editPosition++; + _stateManager.editPosition++; } - /// Open a new page on top of the current page. + /// Update the temporary layer in the editor. /// - /// This method navigates to a new page using a fade transition animation. - Future _openPage( - Widget page, { - Duration duration = const Duration(milliseconds: 300), - }) { - return Navigator.push( - context, - PageRouteBuilder( - opaque: false, - transitionDuration: duration, - reverseTransitionDuration: duration, - transitionsBuilder: (context, animation, secondaryAnimation, child) { - return FadeTransition( - opacity: animation, - child: child, - ); - }, - pageBuilder: (context, animation, secondaryAnimation) => page, + /// This method updates the temporary layer in the editor and updates the editing state. + void _updateTempLayer() { + _stateManager.cleanForwardChanges(); + stateHistory.add( + EditorStateHistory( + transformConfigs: TransformConfigs.empty(), + bytesRefIndex: _stateManager.imgStateHistory.length - 1, + blur: _stateManager.blurStateHistory, + layers: List.from( + stateHistory.last.layers.map((e) => _layerManager.copyLayer(e))), + filters: _stateManager.filters, ), ); + var oldIndex = + activeLayers.indexWhere((element) => element.id == _tempLayer!.id); + if (oldIndex >= 0) { + stateHistory[_stateManager.editPosition].layers[oldIndex] = + _layerManager.copyLayer(_tempLayer!); + } + _stateManager.editPosition++; + _tempLayer = null; } /// Decode the image being edited. @@ -774,141 +567,34 @@ class ProImageEditorState extends State /// This method decodes the image if it hasn't been decoded yet and updates its properties. void _decodeImage() async { bool shouldImportStateHistory = - _imageNeedDecode && widget.configs.initStateHistory != null; + _imageNeedDecode && configs.initStateHistory != null; _imageNeedDecode = false; - var decodedImage = await decodeImageFromList(await _image.safeByteArray); + var decodedImage = + await decodeImageFromList(await _stateManager.image.safeByteArray); if (!mounted) return; var w = decodedImage.width; var h = decodedImage.height; - var widthRatio = w.toDouble() / _screen.width; - var heightRatio = h.toDouble() / _screenInnerHeight; + var widthRatio = w.toDouble() / _screenSize.screen.width; + var heightRatio = h.toDouble() / _screenSize.screenInnerHeight; _pixelRatio = max(heightRatio, widthRatio); - _imageWidth = w / _pixelRatio; - _imageHeight = h / _pixelRatio; + _screenSize.imageWidth = w / _pixelRatio; + _screenSize.imageHeight = h / _pixelRatio; _inited = true; if (shouldImportStateHistory) { - importStateHistory(widget.configs.initStateHistory!); + importStateHistory(configs.initStateHistory!); } setState(() {}); widget.onUpdateUI?.call(); } - /// Copy a layer to create a new instance of the same type. - /// - /// This method takes a layer and creates a new instance of the same type. - Layer _copyLayer(Layer layer) { - if (layer is TextLayerData) { - return _createCopyTextLayer(layer); - } else if (layer is EmojiLayerData) { - return _createCopyEmojiLayer(layer); - } else if (layer is PaintingLayerData) { - return _createCopyPaintingLayer(layer); - } else if (layer is StickerLayerData) { - return _createCopyStickerLayer(layer); - } else { - return layer; - } - } - - /// Create a copy of a TextLayerData instance. - TextLayerData _createCopyTextLayer(TextLayerData layer) { - return TextLayerData( - id: layer.id, - text: layer.text, - align: layer.align, - fontScale: layer.fontScale, - background: Color(layer.background.value), - color: Color(layer.color.value), - colorMode: layer.colorMode, - colorPickerPosition: layer.colorPickerPosition, - offset: Offset(layer.offset.dx, layer.offset.dy), - rotation: layer.rotation, - textStyle: layer.textStyle, - scale: layer.scale, - flipX: layer.flipX, - flipY: layer.flipY, - ); - } - - /// Create a copy of an EmojiLayerData instance. - EmojiLayerData _createCopyEmojiLayer(EmojiLayerData layer) { - return EmojiLayerData( - id: layer.id, - emoji: layer.emoji, - offset: Offset(layer.offset.dx, layer.offset.dy), - rotation: layer.rotation, - scale: layer.scale, - flipX: layer.flipX, - flipY: layer.flipY, - ); - } - - /// Create a copy of an EmojiLayerData instance. - StickerLayerData _createCopyStickerLayer(StickerLayerData layer) { - return StickerLayerData( - id: layer.id, - sticker: layer.sticker, - offset: Offset(layer.offset.dx, layer.offset.dy), - rotation: layer.rotation, - scale: layer.scale, - flipX: layer.flipX, - flipY: layer.flipY, - ); - } - - /// Create a copy of a PaintingLayerData instance. - PaintingLayerData _createCopyPaintingLayer(PaintingLayerData layer) { - return PaintingLayerData( - id: layer.id, - offset: Offset(layer.offset.dx, layer.offset.dy), - rotation: layer.rotation, - scale: layer.scale, - flipX: layer.flipX, - flipY: layer.flipY, - item: layer.item.copy(), - rawSize: layer.rawSize, - ); - } - /// Set the temporary layer to a copy of the provided layer. void _setTempLayer(Layer layer) { - _tempLayer = _copyLayer(layer); - } - - /// Vibrates the device briefly if enabled and supported. - /// - /// This function checks if helper lines hit vibration is enabled in the widget's - /// configurations (`widget.configs.helperLines.hitVibration`) and whether the - /// device supports vibration. If both conditions are met, it triggers a brief - /// vibration on the device. - /// - /// If the device supports custom vibrations, it uses the `Vibration.vibrate` - /// method with a duration of 3 milliseconds to produce the vibration. - /// - /// On older Android devices, it initiates vibration using `Vibration.vibrate`, - /// and then, after 3 milliseconds, cancels the vibration using `Vibration.cancel`. - /// - /// This function is used to provide haptic feedback when helper lines are interacted - /// with, enhancing the user experience. - void _lineHitVibrate() { - if (widget.configs.helperLines.hitVibration && _deviceCanVibrate) { - if (_deviceCanCustomVibrate) { - Vibration.vibrate(duration: 3); - } else if (Platform.isAndroid) { - // On old android devices we can stop the vibration after 3 milliseconds - // iOS: only works for custom haptic vibrations using CHHapticEngine. - // This will set `_deviceCanCustomVibrate` anyway to true so it's impossible to fake it. - Vibration.vibrate(); - Future.delayed(const Duration(milliseconds: 3)).whenComplete(() { - Vibration.cancel(); - }); - } - } + _tempLayer = _layerManager.copyLayer(layer); } /// Handle the start of a scaling operation. @@ -917,47 +603,50 @@ class ProImageEditorState extends State void _onScaleStart(ScaleStartDetails details) { _swipeDirection = SwipeMode.none; _swipeStartTime = DateTime.now(); - _snapStartPosX = details.focalPoint.dx; - _snapStartPosY = details.focalPoint.dy; + _layerInteraction.snapStartPosX = details.focalPoint.dx; + _layerInteraction.snapStartPosY = details.focalPoint.dy; - if (_selectedLayer < 0) return; + if (_selectedLayerIndex < 0) return; - var layer = activeLayers[_selectedLayer]; + var layer = activeLayers[_selectedLayerIndex]; _setTempLayer(layer); - _baseScaleFactor = layer.scale; - _baseAngleFactor = layer.rotation; - _snapStartRotation = layer.rotation * 180 / pi; - _snapLastRotation = _snapStartRotation; - _rotationStartedHelper = false; - _showHelperLines = true; - - double posX = layer.offset.dx + screenPaddingHelper.left; - double posY = layer.offset.dy + screenPaddingHelper.top; - - _lastPositionY = posY <= _screenMiddleY - _hitSpan - ? LayerLastPosition.top - : posY >= _screenMiddleY + _hitSpan - ? LayerLastPosition.bottom - : LayerLastPosition.center; - - _lastPositionX = posX <= _screenMiddleX - _hitSpan - ? LayerLastPosition.left - : posX >= _screenMiddleX + _hitSpan - ? LayerLastPosition.right - : LayerLastPosition.center; + _layerInteraction.baseScaleFactor = layer.scale; + _layerInteraction.baseAngleFactor = layer.rotation; + _layerInteraction.snapStartRotation = layer.rotation * 180 / pi; + _layerInteraction.snapLastRotation = _layerInteraction.snapStartRotation; + _layerInteraction.rotationStartedHelper = false; + _layerInteraction.showHelperLines = true; + + double posX = layer.offset.dx + _screenSize.screenPaddingHelper.left; + double posY = layer.offset.dy + _screenSize.screenPaddingHelper.top; + + _layerInteraction.lastPositionY = + posY <= _screenSize.screenMiddleY - _layerInteraction.hitSpan + ? LayerLastPosition.top + : posY >= _screenSize.screenMiddleY + _layerInteraction.hitSpan + ? LayerLastPosition.bottom + : LayerLastPosition.center; + + _layerInteraction.lastPositionX = + posX <= _screenSize.screenMiddleX - _layerInteraction.hitSpan + ? LayerLastPosition.left + : posX >= _screenSize.screenMiddleX + _layerInteraction.hitSpan + ? LayerLastPosition.right + : LayerLastPosition.center; } /// Handle updates during scaling. /// /// This method is called during a scaling operation and updates the selected layer's position and properties. void _onScaleUpdate(ScaleUpdateDetails detail) { - if (_selectedLayer < 0) { - if (widget.configs.imageEditorTheme.editorMode == - ThemeEditorMode.whatsapp) { - _whatsAppFilterShowHelper -= detail.focalPointDelta.dy; - _whatsAppFilterShowHelper = max(0, min(120, _whatsAppFilterShowHelper)); - - double pointerOffset = _snapStartPosY - detail.focalPoint.dy; + if (_selectedLayerIndex < 0) { + if (configs.imageEditorTheme.editorMode == ThemeEditorMode.whatsapp) { + _whatsAppHelper.filterShowHelper -= detail.focalPointDelta.dy; + _whatsAppHelper.filterShowHelper = + max(0, min(120, _whatsAppHelper.filterShowHelper)); + + double pointerOffset = + _layerInteraction.snapStartPosY - detail.focalPoint.dy; if (pointerOffset > 20) { _swipeDirection = SwipeMode.up; } else if (pointerOffset < -20) { @@ -969,129 +658,32 @@ class ProImageEditorState extends State return; } - if (_whatsAppFilterShowHelper > 0) return; + if (_whatsAppHelper.filterShowHelper > 0) return; - _enabledHitDetection = false; + _layerInteraction.enabledHitDetection = false; if (detail.pointerCount == 1) { - if (_activeScale) return; - _freeStyleHighPerformanceMoving = - widget.configs.paintEditorConfigs.freeStyleHighPerformanceMoving ?? + _layerInteraction.freeStyleHighPerformanceMoving = + configs.paintEditorConfigs.freeStyleHighPerformanceMoving ?? isWebMobile; - _activeLayer.offset = Offset( - _activeLayer.offset.dx + detail.focalPointDelta.dx, - _activeLayer.offset.dy + detail.focalPointDelta.dy, + _layerInteraction.calculateMovement( + activeLayer: _activeLayer, + context: context, + detail: detail, + screenMiddleX: _screenSize.screenMiddleX, + screenMiddleY: _screenSize.screenMiddleY, + screenPaddingHelper: _screenSize.screenPaddingHelper, + configEnabledHitVibration: configs.helperLines.hitVibration, ); - - hoverRemoveBtn = detail.focalPoint.dx <= kToolbarHeight && - detail.focalPoint.dy <= - kToolbarHeight + MediaQuery.of(context).viewPadding.top; - - bool vibarate = false; - double posX = _activeLayer.offset.dx + screenPaddingHelper.left; - double posY = _activeLayer.offset.dy + screenPaddingHelper.top; - - bool hitAreaX = detail.focalPoint.dx >= _snapStartPosX - _hitSpan && - detail.focalPoint.dx <= _snapStartPosX + _hitSpan; - bool hitAreaY = detail.focalPoint.dy >= _snapStartPosY - _hitSpan && - detail.focalPoint.dy <= _snapStartPosY + _hitSpan; - - bool helperGoNearLineLeft = - posX >= _screenMiddleX && _lastPositionX == LayerLastPosition.left; - bool helperGoNearLineRight = - posX <= _screenMiddleX && _lastPositionX == LayerLastPosition.right; - bool helperGoNearLineTop = - posY >= _screenMiddleY && _lastPositionY == LayerLastPosition.top; - bool helperGoNearLineBottom = - posY <= _screenMiddleY && _lastPositionY == LayerLastPosition.bottom; - - /// Calc vertical helper line - if ((!_showVerticalHelperLine && - (helperGoNearLineLeft || helperGoNearLineRight)) || - (_showVerticalHelperLine && hitAreaX)) { - if (!_showVerticalHelperLine) { - vibarate = true; - _snapStartPosX = detail.focalPoint.dx; - } - _showVerticalHelperLine = true; - _activeLayer.offset = Offset( - _screenMiddleX - screenPaddingHelper.left, _activeLayer.offset.dy); - _lastPositionX = LayerLastPosition.center; - } else { - _showVerticalHelperLine = false; - _lastPositionX = posX <= _screenMiddleX - ? LayerLastPosition.left - : LayerLastPosition.right; - } - - /// Calc horizontal helper line - if ((!_showHorizontalHelperLine && - (helperGoNearLineTop || helperGoNearLineBottom)) || - (_showHorizontalHelperLine && hitAreaY)) { - if (!_showHorizontalHelperLine) { - vibarate = true; - _snapStartPosY = detail.focalPoint.dy; - } - _showHorizontalHelperLine = true; - _activeLayer.offset = Offset( - _activeLayer.offset.dx, _screenMiddleY - screenPaddingHelper.top); - _lastPositionY = LayerLastPosition.center; - } else { - _showHorizontalHelperLine = false; - _lastPositionY = posY <= _screenMiddleY - ? LayerLastPosition.top - : LayerLastPosition.bottom; - } - - if (vibarate) { - _lineHitVibrate(); - } } else if (detail.pointerCount == 2) { - _freeStyleHighPerformanceScaling = - widget.configs.paintEditorConfigs.freeStyleHighPerformanceScaling ?? + _layerInteraction.freeStyleHighPerformanceScaling = + configs.paintEditorConfigs.freeStyleHighPerformanceScaling ?? !isDesktop; - _activeScale = true; - - _activeLayer.scale = _baseScaleFactor * detail.scale; - _activeLayer.rotation = _baseAngleFactor + detail.rotation; - - var hitSpanX = _hitSpan / 2; - var deg = _activeLayer.rotation * 180 / pi; - var degChange = detail.rotation * 180 / pi; - var degHit = (_snapStartRotation + degChange) % 45; - var hitAreaBelow = degHit <= hitSpanX; - var hitAreaAfter = degHit >= 45 - hitSpanX; - var hitArea = hitAreaBelow || hitAreaAfter; - - if ((!_showRotationHelperLine && - ((degHit > 0 && degHit <= hitSpanX && _snapLastRotation < deg) || - (degHit < 45 && - degHit >= 45 - hitSpanX && - _snapLastRotation > deg))) || - (_showRotationHelperLine && hitArea)) { - if (_rotationStartedHelper) { - _activeLayer.rotation = - (deg - (degHit > 45 - hitSpanX ? degHit - 45 : degHit)) / - 180 * - pi; - _rotationHelperLineDeg = _activeLayer.rotation; - - double posY = _activeLayer.offset.dy + screenPaddingHelper.top; - double posX = _activeLayer.offset.dx + screenPaddingHelper.left; - - _rotationHelperLineX = posX; - _rotationHelperLineY = posY; - if (!_showRotationHelperLine) { - _lineHitVibrate(); - } - _showRotationHelperLine = true; - } - _snapLastRotation = deg; - } else { - _showRotationHelperLine = false; - _rotationStartedHelper = true; - } - - _scaleDebounce(() => _activeScale = false); + _layerInteraction.calculateScale( + activeLayer: _activeLayer, + detail: detail, + screenPaddingHelper: _screenSize.screenPaddingHelper, + configEnabledHitVibration: configs.helperLines.hitVibration, + ); } setState(() {}); widget.onUpdateUI?.call(); @@ -1101,153 +693,39 @@ class ProImageEditorState extends State /// /// This method is called when a scaling operation ends and resets helper lines and flags. void _onScaleEnd(ScaleEndDetails detail) async { - if (_selectedLayer < 0 && - widget.configs.imageEditorTheme.editorMode == - ThemeEditorMode.whatsapp) { - _showHelperLines = false; + if (_selectedLayerIndex < 0 && + configs.imageEditorTheme.editorMode == ThemeEditorMode.whatsapp) { + _layerInteraction.showHelperLines = false; if (_swipeDirection != SwipeMode.none && DateTime.now().difference(_swipeStartTime).inMilliseconds < 200) { if (_swipeDirection == SwipeMode.up) { - _whatsAppFilterSheetAutoAnimation(true); + _whatsAppHelper.filterSheetAutoAnimation(true, setState); } else if (_swipeDirection == SwipeMode.down) { - _whatsAppFilterSheetAutoAnimation(false); + _whatsAppHelper.filterSheetAutoAnimation(false, setState); } } else { - if (_whatsAppFilterShowHelper < 90) { - _whatsAppFilterSheetAutoAnimation(false); + if (_whatsAppHelper.filterShowHelper < 90) { + _whatsAppHelper.filterSheetAutoAnimation(false, setState); } else { - _whatsAppFilterSheetAutoAnimation(true); + _whatsAppHelper.filterSheetAutoAnimation(true, setState); } } - _whatsAppFilterShowHelper = max(0, min(120, _whatsAppFilterShowHelper)); + _whatsAppHelper.filterShowHelper = + max(0, min(120, _whatsAppHelper.filterShowHelper)); setState(() {}); } - if (!hoverRemoveBtn && _tempLayer != null) _updateTempLayer(); + if (!_layerInteraction.hoverRemoveBtn && _tempLayer != null) { + _updateTempLayer(); + } - _enabledHitDetection = true; - _freeStyleHighPerformanceScaling = false; - _freeStyleHighPerformanceMoving = false; - _showHorizontalHelperLine = false; - _showVerticalHelperLine = false; - _showRotationHelperLine = false; - _showHelperLines = false; - hoverRemoveBtn = false; + _layerInteraction.onScaleEnd(); setState(() {}); widget.onUpdateUI?.call(); } - /// Rotate a layer. - /// - /// This method rotates a layer based on various factors, including flip and angle. - void _rotateLayer({ - required Layer layer, - required bool beforeIsFlipX, - required double newImgW, - required double newImgH, - required double rotationScale, - required double rotationRadian, - required double rotationAngle, - }) { - if (beforeIsFlipX) { - layer.rotation -= rotationRadian; - } else { - layer.rotation += rotationRadian; - } - - if (rotationAngle == 90) { - layer.scale /= rotationScale; - layer.offset = Offset( - newImgW - layer.offset.dy / rotationScale, - layer.offset.dx / rotationScale, - ); - } else if (rotationAngle == 180) { - layer.offset = Offset( - newImgW - layer.offset.dx, - newImgH - layer.offset.dy, - ); - } else if (rotationAngle == 270) { - layer.scale /= rotationScale; - layer.offset = Offset( - layer.offset.dy / rotationScale, - newImgH - layer.offset.dx / rotationScale, - ); - } - } - - /// Flip a layer horizontally or vertically. - /// - /// This method flips a layer either horizontally or vertically based on the specified parameters. - void _flipLayer({ - required Layer layer, - required bool flipX, - required bool flipY, - required bool isHalfPi, - }) { - if (flipY) { - if (isHalfPi) { - layer.flipY = !layer.flipY; - } else { - layer.flipX = !layer.flipX; - } - layer.offset = Offset( - _imageWidth - layer.offset.dx, - layer.offset.dy, - ); - } - if (flipX) { - layer.flipX = !layer.flipX; - layer.offset = Offset( - layer.offset.dx, - _imageHeight - layer.offset.dy, - ); - } - } - - /// Handles zooming of a layer. - /// - /// This method calculates the zooming of a layer based on the specified parameters. - /// It checks if the layer should be zoomed and performs the necessary transformations. - /// - /// Returns `true` if the layer was zoomed, otherwise `false`. - bool _zoomedLayer({ - required Layer layer, - required double scale, - required double scaleX, - required double oldFullH, - required double oldFullW, - required Rect cropRect, - required bool isHalfPi, - }) { - var paddingTop = cropRect.top / _pixelRatio; - var paddingLeft = cropRect.left / _pixelRatio; - var paddingRight = oldFullW - cropRect.right; - var paddingBottom = oldFullH - cropRect.bottom; - - // important to check with < 1 and >-1 cuz crop-editor has rounding bugs - if (paddingTop > 0.1 || - paddingTop < -0.1 || - paddingLeft > 0.1 || - paddingLeft < -0.1 || - paddingRight > 0.1 || - paddingRight < -0.1 || - paddingBottom > 0.1 || - paddingBottom < -0.1) { - var initialIconX = (layer.offset.dx - paddingLeft) * scaleX; - var initialIconY = (layer.offset.dy - paddingTop) * scaleX; - layer.offset = Offset( - initialIconX, - initialIconY, - ); - - layer.scale *= scale; - return true; - } - return false; - } - /// Handles tap events on a text layer. /// /// This method opens a text editor for the specified text layer and updates the layer's properties @@ -1255,7 +733,6 @@ class ProImageEditorState extends State /// /// [layerData] - The text layer data to be edited. void _onTextLayerTap(TextLayerData layerData) async { - setState(() => _openEditor = true); TextLayerData? layer = await _openPage( TextEditor( key: textEditor, @@ -1267,7 +744,6 @@ class ProImageEditorState extends State ), duration: const Duration(milliseconds: 50), ); - setState(() => _openEditor = false); if (layer == null || !mounted) return; @@ -1298,39 +774,58 @@ class ProImageEditorState extends State widget.onUpdateUI?.call(); } + /// Open a new page on top of the current page. + /// + /// This method navigates to a new page using a fade transition animation. + Future _openPage( + Widget page, { + Duration duration = const Duration(milliseconds: 300), + }) { + setState(() => _isEditorOpen = true); + return Navigator.push( + context, + PageRouteBuilder( + opaque: false, + transitionDuration: duration, + reverseTransitionDuration: duration, + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return FadeTransition( + opacity: animation, + child: child, + ); + }, + pageBuilder: (context, animation, secondaryAnimation) => page, + ), + ).whenComplete(() { + setState(() => _isEditorOpen = false); + }); + } + /// Opens the painting editor. /// /// This method opens the painting editor and allows the user to draw on the current image. /// After closing the painting editor, any changes made are applied to the image's layers. void openPaintingEditor() async { - setState(() => _openEditor = true); await _openPage>( PaintingEditor.autoSource( key: paintingEditor, - file: _image.file, - byteArray: _image.byteArray, - assetPath: _image.assetPath, - networkUrl: _image.networkUrl, - layers: activeLayers, - theme: _theme, - imageSize: Size(_imageWidth, _imageHeight), - configs: widget.configs, - paddingHelper: EdgeInsets.only( - top: (_screen.height - - _screenPadding.top - - _screenPadding.bottom - - _imageHeight) / - 2 - - _appBarHeight, - left: (_screen.width - - _screenPadding.left - - _screenPadding.right - - _imageWidth) / - 2, + file: _stateManager.image.file, + byteArray: _stateManager.image.byteArray, + assetPath: _stateManager.image.assetPath, + networkUrl: _stateManager.image.networkUrl, + initConfigs: PaintEditorInitConfigs( + layers: activeLayers, + theme: _theme, + imageSize: Size(_screenSize.imageWidth, _screenSize.imageHeight), + configs: widget.configs, + paddingHelper: EdgeInsets.only( + top: _screenSize.screenPaddingHelper.top - _screenSize.appBarHeight, + left: _screenSize.screenPaddingHelper.left, + ), + onUpdateUI: widget.onUpdateUI, + appliedBlurFactor: _stateManager.blurStateHistory.blur, + appliedFilters: _stateManager.filters, ), - onUpdateUI: widget.onUpdateUI, - blur: _blur, - filters: _filters, ), duration: const Duration(milliseconds: 150), ).then((List? paintingLayers) { @@ -1343,14 +838,12 @@ class ProImageEditorState extends State widget.onUpdateUI?.call(); } }); - setState(() => _openEditor = false); } /// Opens the text editor. /// /// This method opens the text editor, allowing the user to add or edit text layers on the image. void openTextEditor() async { - setState(() => _openEditor = true); TextLayerData? layer = await _openPage( TextEditor( key: textEditor, @@ -1360,12 +853,11 @@ class ProImageEditorState extends State ), duration: const Duration(milliseconds: 50), ); - setState(() => _openEditor = false); if (layer == null || !mounted) return; layer.offset = Offset( - _imageWidth / 2, - _imageHeight / 2, + _screenSize.imageWidth / 2, + _screenSize.imageHeight / 2, ); addLayer(layer); @@ -1380,24 +872,26 @@ class ProImageEditorState extends State void openCropEditor() async { if (_activeCrop) return; EditorImage img = EditorImage( - assetPath: _image.assetPath, - byteArray: _image.byteArray, - file: _image.file, - networkUrl: _image.networkUrl, + assetPath: _stateManager.image.assetPath, + byteArray: _stateManager.image.byteArray, + file: _stateManager.image.file, + networkUrl: _stateManager.image.networkUrl, ); Uint8List? bytesWithLayers; - if (activeLayers.isNotEmpty || _filters.isNotEmpty || _blur.blur != 0) { + if (activeLayers.isNotEmpty || + _stateManager.filters.isNotEmpty || + _stateManager.blurStateHistory.blur != 0) { _activeCrop = true; LoadingDialog loading = LoadingDialog() ..show( context, theme: _theme, - imageEditorTheme: widget.configs.imageEditorTheme, - designMode: widget.configs.designMode, - i18n: widget.configs.i18n, - message: widget.configs.i18n.cropRotateEditor.prepareImageDialogMsg, + imageEditorTheme: configs.imageEditorTheme, + designMode: configs.designMode, + i18n: configs.i18n, + message: configs.i18n.cropRotateEditor.prepareImageDialogMsg, ); - bytesWithLayers = await _screenshotCtrl.capture( + bytesWithLayers = await _controllers.screenshot.capture( pixelRatio: _pixelRatio, ); if (mounted) await loading.hide(context); @@ -1405,8 +899,6 @@ class ProImageEditorState extends State _activeCrop = false; if (!mounted) return; - setState(() => _openEditor = true); - _openPage( CropRotateEditor.autoSource( key: cropRotateEditor, @@ -1417,11 +909,10 @@ class ProImageEditorState extends State networkUrl: img.networkUrl, bytesWithLayers: bytesWithLayers, theme: _theme, - imageSize: Size(_imageWidth, _imageHeight), + imageSize: Size(_screenSize.imageWidth, _screenSize.imageHeight), configs: widget.configs, ), ).then((response) async { - setState(() => _openEditor = false); if (response != null) { CropRotateEditorResponse res = response.result; if (res.bytes != null) { @@ -1430,37 +921,38 @@ class ProImageEditorState extends State var w = decodedImage.width; var h = decodedImage.height; - var widthRatio = w.toDouble() / _screen.width; - var heightRatio = h.toDouble() / _screenInnerHeight; + var widthRatio = w.toDouble() / _screenSize.screen.width; + var heightRatio = h.toDouble() / _screenSize.screenInnerHeight; var newPixelRatio = max(heightRatio, widthRatio); var newImgW = w / newPixelRatio; var newImgH = h / newPixelRatio; - var scale = (_imageWidth * _pixelRatio) / w; - var oldFullW = _imageWidth * _pixelRatio; - var oldFullH = _imageHeight * _pixelRatio; + var scale = (_screenSize.imageWidth * _pixelRatio) / w; + var oldFullW = _screenSize.imageWidth * _pixelRatio; + var oldFullH = _screenSize.imageHeight * _pixelRatio; - var rotationScale = _imageWidth / newImgH; + var rotationScale = _screenSize.imageWidth / newImgH; double fitFactor = 1; - bool oldFitWidth = _imageWidth >= _screen.width - 0.1 && - _imageWidth <= _screen.width + 0.1; - bool newFitWidth = - newImgW >= _screen.width - 0.1 && newImgW <= _screen.width + 0.1; + bool oldFitWidth = + _screenSize.imageWidth >= _screenSize.screen.width - 0.1 && + _screenSize.imageWidth <= _screenSize.screen.width + 0.1; + bool newFitWidth = newImgW >= _screenSize.screen.width - 0.1 && + newImgW <= _screenSize.screen.width + 0.1; var scaleX = newFitWidth ? oldFullW / w : oldFullH / h; if (oldFitWidth != newFitWidth) { if (newFitWidth) { - fitFactor = _imageWidth / newImgW; + fitFactor = _screenSize.imageWidth / newImgW; } else { - fitFactor = _imageHeight / newImgH; + fitFactor = _screenSize.imageHeight / newImgH; } } List updatedLayers = []; for (var el in activeLayers) { - var layer = _copyLayer(el); + var layer = _layerManager.copyLayer(el); var beforeIsFlipX = layer.flipX; switch (res.rotationAngle) { case 0: @@ -1478,7 +970,7 @@ class ProImageEditorState extends State break; default: } - bool zoomed = _zoomedLayer( + bool zoomed = _layerInteraction.zoomedLayer( layer: layer, scale: scale, scaleX: scaleX, @@ -1486,14 +978,17 @@ class ProImageEditorState extends State oldFullW: oldFullW, cropRect: res.cropRect, isHalfPi: res.isHalfPi, + pixelRatio: _pixelRatio, ); - _flipLayer( + _layerInteraction.flipLayer( layer: layer, flipX: res.flipX, flipY: res.flipY, isHalfPi: res.isHalfPi, + imageHeight: _screenSize.imageHeight, + imageWidth: _screenSize.imageWidth, ); - _rotateLayer( + _layerInteraction.rotateLayer( layer: layer, beforeIsFlipX: beforeIsFlipX, newImgW: newImgW, @@ -1508,8 +1003,8 @@ class ProImageEditorState extends State _addCroppedImg(updatedLayers, EditorImage(byteArray: res.bytes)); _pixelRatio = max(heightRatio, widthRatio); - _imageWidth = w / _pixelRatio; - _imageHeight = h / _pixelRatio; + _screenSize.imageWidth = w / _pixelRatio; + _screenSize.imageHeight = h / _pixelRatio; setState(() {}); widget.onUpdateUI?.call(); } @@ -1526,45 +1021,90 @@ class ProImageEditorState extends State /// `Uint8List`. If no filter is applied or the operation is canceled, the original image is retained. void openFilterEditor() async { if (!mounted) return; - setState(() => _openEditor = true); FilterStateHistory? filterAppliedImage = await _openPage( FilterEditor.autoSource( key: filterEditor, - file: _image.file, - byteArray: _image.byteArray, - assetPath: _image.assetPath, - networkUrl: _image.networkUrl, - theme: _theme, - configs: widget.configs, - transformConfigs: _transformConfigs, - onUpdateUI: widget.onUpdateUI, - activeFilters: _filters, - layers: activeLayers, - blur: _blur, - imageSizeWithLayers: _renderedImageSize, - bodySizeWithLayers: _bodySize, - convertToUint8List: false, + file: _stateManager.image.file, + byteArray: _stateManager.image.byteArray, + assetPath: _stateManager.image.assetPath, + networkUrl: _stateManager.image.networkUrl, + initConfigs: FilterEditorInitConfigs( + theme: _theme, + configs: widget.configs, + transformConfigs: _stateManager.transformConfigs, + onUpdateUI: widget.onUpdateUI, + layers: activeLayers, + imageSizeWithLayers: _screenSize.renderedImageSize, + bodySizeWithLayers: _screenSize.bodySize, + convertToUint8List: false, + appliedBlurFactor: _stateManager.blurStateHistory.blur, + appliedFilters: _stateManager.filters, + ), ), ); - setState(() => _openEditor = false); if (filterAppliedImage == null) return; - _cleanForwardChanges(); + _stateManager.cleanForwardChanges(); stateHistory.add( EditorStateHistory( transformConfigs: TransformConfigs.empty(), - bytesRefIndex: _imgStateHistory.length - 1, - blur: _blur, + bytesRefIndex: _stateManager.imgStateHistory.length - 1, + blur: _stateManager.blurStateHistory, layers: activeLayers, filters: [ filterAppliedImage, - ..._filters, + ..._stateManager.filters, ], ), ); - _editPosition++; + _stateManager.editPosition++; + + setState(() {}); + widget.onUpdateUI?.call(); + } + + /// Opens the blur editor as a modal bottom sheet. + void openBlurEditor() async { + if (!mounted) return; + double? blur = await _openPage( + BlurEditor.autoSource( + key: blurEditor, + file: _stateManager.image.file, + byteArray: _stateManager.image.byteArray, + assetPath: _stateManager.image.assetPath, + networkUrl: _stateManager.image.networkUrl, + initConfigs: BlurEditorInitConfigs( + theme: _theme, + imageSize: Size(_screenSize.imageWidth, _screenSize.imageHeight), + imageSizeWithLayers: _screenSize.renderedImageSize, + bodySizeWithLayers: _screenSize.bodySize, + layers: activeLayers, + configs: widget.configs, + transformConfigs: _stateManager.transformConfigs, + onUpdateUI: widget.onUpdateUI, + convertToUint8List: false, + appliedBlurFactor: _stateManager.blurStateHistory.blur, + appliedFilters: _stateManager.filters, + ), + ), + ); + + if (blur == null) return; + + _stateManager.cleanForwardChanges(); + + stateHistory.add( + EditorStateHistory( + transformConfigs: TransformConfigs.empty(), + bytesRefIndex: _stateManager.imgStateHistory.length - 1, + blur: BlurStateHistory(blur: blur), + layers: activeLayers, + filters: _stateManager.filters, + ), + ); + _stateManager.editPosition++; setState(() {}); widget.onUpdateUI?.call(); @@ -1579,18 +1119,41 @@ class ProImageEditorState extends State /// Keyboard event handlers are temporarily removed while the emoji editor is active and restored /// after its closure. void openEmojiEditor() async { - ServicesBinding.instance.keyboard.removeHandler(_onKey); + ServicesBinding.instance.keyboard.removeHandler(_onKeyEvent); EmojiLayerData? layer = await showModalBottomSheet( context: context, backgroundColor: Colors.black, builder: (BuildContext context) => EmojiEditor(configs: widget.configs), ); - ServicesBinding.instance.keyboard.addHandler(_onKey); + ServicesBinding.instance.keyboard.addHandler(_onKeyEvent); if (layer == null || !mounted) return; - layer.scale = widget.configs.emojiEditorConfigs.initScale; + layer.scale = configs.emojiEditorConfigs.initScale; layer.offset = Offset( - _imageWidth / 2, - _imageHeight / 2, + _screenSize.imageWidth / 2, + _screenSize.imageHeight / 2, + ); + + addLayer(layer); + + setState(() {}); + widget.onUpdateUI?.call(); + } + + /// Opens the sticker editor as a modal bottom sheet. + void openStickerEditor() async { + ServicesBinding.instance.keyboard.removeHandler(_onKeyEvent); + StickerLayerData? layer = await showModalBottomSheet( + context: context, + backgroundColor: Colors.black, + builder: (BuildContext context) => StickerEditor( + configs: widget.configs, + ), + ); + ServicesBinding.instance.keyboard.addHandler(_onKeyEvent); + if (layer == null || !mounted) return; + layer.offset = Offset( + _screenSize.imageWidth / 2, + _screenSize.imageHeight / 2, ); addLayer(layer); @@ -1611,11 +1174,10 @@ class ProImageEditorState extends State /// /// Finally, the layer is added, the UI is updated, and the widget's [onUpdateUI] callback is called if provided. void openWhatsAppStickerEditor() async { - setState(() => _openEditor = true); - ServicesBinding.instance.keyboard.removeHandler(_onKey); + ServicesBinding.instance.keyboard.removeHandler(_onKeyEvent); Layer? layer; - if (widget.configs.designMode == ImageEditorDesignModeE.material) { + if (configs.designMode == ImageEditorDesignModeE.material) { layer = await _openPage(WhatsAppStickerPage( configs: widget.configs, )); @@ -1644,20 +1206,19 @@ class ProImageEditorState extends State }, ); } - _openEditor = false; - ServicesBinding.instance.keyboard.addHandler(_onKey); + ServicesBinding.instance.keyboard.addHandler(_onKeyEvent); if (layer == null || !mounted) { setState(() {}); return; } if (layer.runtimeType != StickerLayerData) { - layer.scale = widget.configs.emojiEditorConfigs.initScale; + layer.scale = configs.emojiEditorConfigs.initScale; } layer.offset = Offset( - _imageWidth / 2, - _imageHeight / 2, + _screenSize.imageWidth / 2, + _screenSize.imageHeight / 2, ); addLayer(layer); @@ -1666,72 +1227,22 @@ class ProImageEditorState extends State widget.onUpdateUI?.call(); } - /// Opens the sticker editor as a modal bottom sheet. - void openStickerEditor() async { - ServicesBinding.instance.keyboard.removeHandler(_onKey); - StickerLayerData? layer = await showModalBottomSheet( - context: context, - backgroundColor: Colors.black, - builder: (BuildContext context) => StickerEditor( - configs: widget.configs, - ), - ); - ServicesBinding.instance.keyboard.addHandler(_onKey); - if (layer == null || !mounted) return; - layer.offset = Offset( - _imageWidth / 2, - _imageHeight / 2, - ); - - addLayer(layer); - - setState(() {}); - widget.onUpdateUI?.call(); - } - - /// Opens the blur editor as a modal bottom sheet. - void openBlurEditor() async { - if (!mounted) return; - setState(() => _openEditor = true); - BlurStateHistory? blur = await _openPage( - BlurEditor.autoSource( - key: blurEditor, - file: _image.file, - byteArray: _image.byteArray, - assetPath: _image.assetPath, - networkUrl: _image.networkUrl, - theme: _theme, - imageSize: Size(_imageWidth, _imageHeight), - imageSizeWithLayers: _renderedImageSize, - bodySizeWithLayers: _bodySize, - layers: activeLayers, - configs: widget.configs, - transformConfigs: _transformConfigs, - onUpdateUI: widget.onUpdateUI, - filters: _filters, - convertToUint8List: false, - currentBlur: _blur, - ), - ); - setState(() => _openEditor = false); - - if (blur == null) return; - - _cleanForwardChanges(); - - stateHistory.add( - EditorStateHistory( - transformConfigs: TransformConfigs.empty(), - bytesRefIndex: _imgStateHistory.length - 1, - blur: blur, - layers: activeLayers, - filters: _filters, - ), - ); - _editPosition++; - + /// Moves a layer in the list to a new position. + /// + /// [oldIndex] is the current index of the layer. + /// [newIndex] is the desired index to move the layer to. + void moveLayerListPosition({ + required int oldIndex, + required int newIndex, + }) { + if (newIndex > oldIndex) { + var item = activeLayers.removeAt(oldIndex); + activeLayers.insert(newIndex - 1, item); + } else { + var item = activeLayers.removeAt(oldIndex); + activeLayers.insert(newIndex, item); + } setState(() {}); - widget.onUpdateUI?.call(); } /// Undo the last editing action. @@ -1739,9 +1250,9 @@ class ProImageEditorState extends State /// This function allows the user to undo the most recent editing action performed on the image. /// It decreases the edit position, and the image is decoded to reflect the previous state. void undoAction() { - if (_editPosition > 0) { + if (_stateManager.editPosition > 0) { setState(() { - _editPosition--; + _stateManager.editPosition--; _decodeImage(); }); widget.onUpdateUI?.call(); @@ -1754,50 +1265,15 @@ class ProImageEditorState extends State /// `undoAction` function. It increases the edit position, and the image is decoded to reflect /// the next state. void redoAction() { - if (_editPosition < stateHistory.length - 1) { + if (_stateManager.editPosition < stateHistory.length - 1) { setState(() { - _editPosition++; + _stateManager.editPosition++; _decodeImage(); }); widget.onUpdateUI?.call(); } } - /// Function to remove transparent areas from the image - Uint8List? _removeTransparentAreas(Uint8List bytes) { - // Decode the image - img.Image? image = img.decodeImage(bytes); - if (image == null) return null; - - // Determine the bounding box of non-transparent pixels - int minX = image.width, minY = image.height, maxX = 0, maxY = 0; - for (int y = 0; y < image.height; y++) { - for (int x = 0; x < image.width; x++) { - // Extract the alpha component to check for transparency - if (image.getPixel(x, y).a != 0) { - // Check if pixel is not fully transparent - if (x < minX) minX = x; - if (y < minY) minY = y; - if (x > maxX) maxX = x; - if (y > maxY) maxY = y; - } - } - } - - // Check if there are any non-transparent pixels - if (maxX < minX || maxY < minY) { - return Uint8List.fromList(img.encodePng(image)); - } - - // Crop the image to the bounding box - img.Image croppedImage = img.copyCrop(image, - x: minX, y: minY, width: maxX - minX + 1, height: maxY - minY + 1); - - // Encode the cropped image to bytes - Uint8List croppedBytes = Uint8List.fromList(img.encodePng(croppedImage)); - return croppedBytes; - } - /// Complete the editing process and return the edited image. /// /// This function is called when the user is done editing the image. If no changes have been made @@ -1808,7 +1284,7 @@ class ProImageEditorState extends State /// Before returning the edited image, a loading dialog is displayed to indicate that the operation /// is in progress. void doneEditing() async { - if (_editPosition <= 0 && activeLayers.isEmpty) { + if (_stateManager.editPosition <= 0 && activeLayers.isEmpty) { final allowCompleteWithEmptyEditing = widget.allowCompleteWithEmptyEditing; if (!allowCompleteWithEmptyEditing) { @@ -1819,24 +1295,23 @@ class ProImageEditorState extends State LoadingDialog loading = LoadingDialog() ..show( context, - i18n: widget.configs.i18n, + i18n: configs.i18n, theme: _theme, - designMode: widget.configs.designMode, - message: widget.configs.i18n.doneLoadingMsg, - imageEditorTheme: widget.configs.imageEditorTheme, + designMode: configs.designMode, + message: configs.i18n.doneLoadingMsg, + imageEditorTheme: configs.imageEditorTheme, ); Uint8List bytes = Uint8List.fromList([]); try { - bytes = await _screenshotCtrl.capture( - pixelRatio: - widget.configs.removeTransparentAreas ? null : _pixelRatio, + bytes = await _controllers.screenshot.capture( + pixelRatio: configs.removeTransparentAreas ? null : _pixelRatio, ) ?? bytes; } catch (_) {} - if (widget.configs.removeTransparentAreas) { - bytes = _removeTransparentAreas(bytes) ?? bytes; + if (configs.removeTransparentAreas) { + bytes = removeTransparentImgAreas(bytes) ?? bytes; } await widget.onImageEditingComplete(bytes); @@ -1851,7 +1326,7 @@ class ProImageEditorState extends State /// This function allows the user to close the image editor without saving any changes or edits. /// It navigates back to the previous screen or closes the modal editor. void closeEditor() { - if (_editPosition <= 0) { + if (_stateManager.editPosition <= 0) { if (widget.onCloseEditor == null) { Navigator.pop(context); } else { @@ -1870,22 +1345,21 @@ class ProImageEditorState extends State builder: (BuildContext context) => Theme( data: _theme, child: AdaptiveDialog( - designMode: widget.configs.designMode, + designMode: configs.designMode, brightness: _theme.brightness, - imageEditorTheme: widget.configs.imageEditorTheme, - title: Text(widget.configs.i18n.various.closeEditorWarningTitle), - content: Text(widget.configs.i18n.various.closeEditorWarningMessage), + imageEditorTheme: configs.imageEditorTheme, + title: Text(configs.i18n.various.closeEditorWarningTitle), + content: Text(configs.i18n.various.closeEditorWarningMessage), actions: [ AdaptiveDialogAction( - designMode: widget.configs.designMode, + designMode: configs.designMode, onPressed: () => Navigator.pop(context, 'Cancel'), - child: - Text(widget.configs.i18n.various.closeEditorWarningCancelBtn), + child: Text(configs.i18n.various.closeEditorWarningCancelBtn), ), AdaptiveDialogAction( - designMode: widget.configs.designMode, + designMode: configs.designMode, onPressed: () { - _editPosition = 0; + _stateManager.editPosition = 0; Navigator.pop(context, 'OK'); if (widget.onCloseEditor == null) { Navigator.pop(context); @@ -1893,8 +1367,7 @@ class ProImageEditorState extends State widget.onCloseEditor!.call(); } }, - child: Text( - widget.configs.i18n.various.closeEditorWarningConfirmBtn), + child: Text(configs.i18n.various.closeEditorWarningConfirmBtn), ), ], ), @@ -1903,86 +1376,6 @@ class ProImageEditorState extends State _openDialog = false; } - /// Handles Keyboard zoom event - void _keyboardRotate(bool left) { - if (left) { - _activeLayer.rotation -= 0.087266; - } else { - _activeLayer.rotation += 0.087266; - } - setState(() {}); - widget.onUpdateUI?.call(); - } - - /// Handles Keyboard zoom event - void _keyboardZoom(bool zoomIn) { - double factor = _activeLayer is PaintingLayerData - ? 0.1 - : _activeLayer is TextLayerData - ? 0.15 - : widget.configs.textEditorConfigs.initFontSize / 50; - if (zoomIn) { - _activeLayer.scale -= factor; - _activeLayer.scale = max(0.1, _activeLayer.scale); - } else { - _activeLayer.scale += factor; - } - setState(() {}); - widget.onUpdateUI?.call(); - } - - /// Handles mouse scroll events. - void _mouseScroll(PointerSignalEvent event) { - bool shiftDown = HardwareKeyboard.instance.logicalKeysPressed - .contains(LogicalKeyboardKey.shiftLeft) || - HardwareKeyboard.instance.logicalKeysPressed - .contains(LogicalKeyboardKey.shiftRight); - - if (event is PointerScrollEvent && _selectedLayer >= 0) { - if (shiftDown) { - if (event.scrollDelta.dy > 0) { - _activeLayer.rotation -= 0.087266; - } else if (event.scrollDelta.dy < 0) { - _activeLayer.rotation += 0.087266; - } - } else { - double factor = _activeLayer is PaintingLayerData - ? 0.1 - : _activeLayer is TextLayerData - ? 0.15 - : widget.configs.textEditorConfigs.initFontSize / 50; - if (event.scrollDelta.dy > 0) { - _activeLayer.scale -= factor; - _activeLayer.scale = max(0.1, _activeLayer.scale); - } else if (event.scrollDelta.dy < 0) { - _activeLayer.scale += factor; - } - } - setState(() {}); - widget.onUpdateUI?.call(); - } - } - - /// Get the screen padding values. - EdgeInsets get screenPaddingHelper => EdgeInsets.only( - top: (_screen.height - - _screenPadding.top - - _screenPadding.bottom - - _imageHeight) / - 2, - left: (_screen.width - - _screenPadding.left - - _screenPadding.right - - _imageWidth) / - 2, - ); - - /// Determines whether undo actions can be performed on the current state. - bool get canUndo => _editPosition > 0; - - /// Determines whether redo actions can be performed on the current state. - bool get canRedo => _editPosition < stateHistory.length - 1; - /// Imports state history and performs necessary recalculations. /// /// If [ImportStateHistory.configs.recalculateSizeAndPosition] is `true`, it recalculates the position and size of layers. @@ -1999,8 +1392,8 @@ class ProImageEditorState extends State for (var el in import.stateHistory) { for (var layer in el.layers) { // Calculate scaling factors for width and height - double scaleWidth = _imageWidth / imgSize.width; - double scaleHeight = _imageHeight / imgSize.height; + double scaleWidth = _screenSize.imageWidth / imgSize.width; + double scaleHeight = _screenSize.imageHeight / imgSize.height; if (scaleWidth == 0 || scaleWidth.isInfinite) scaleWidth = 1; if (scaleHeight == 0 || scaleHeight.isInfinite) scaleHeight = 1; @@ -2021,11 +1414,11 @@ class ProImageEditorState extends State } if (import.configs.mergeMode == ImportEditorMergeMode.replace) { - _editPosition = import.editorPosition + 1; + _stateManager.editPosition = import.editorPosition + 1; if (import.imgStateHistory.isNotEmpty) { - _imgStateHistory = import.imgStateHistory; + _stateManager.imgStateHistory = import.imgStateHistory; } - stateHistory = [ + _stateManager.stateHistory = [ EditorStateHistory( transformConfigs: TransformConfigs.empty(), bytesRefIndex: 0, @@ -2043,8 +1436,8 @@ class ProImageEditorState extends State } stateHistory.addAll(import.stateHistory); - _imgStateHistory.addAll(import.imgStateHistory); - _editPosition = stateHistory.length - 1; + _stateManager.imgStateHistory.addAll(import.imgStateHistory); + _stateManager.editPosition = stateHistory.length - 1; } setState(() {}); @@ -2060,54 +1453,16 @@ class ProImageEditorState extends State {ExportEditorConfigs configs = const ExportEditorConfigs()}) { return ExportStateHistory( stateHistory, - _imgStateHistory, - Size(_imageWidth, _imageHeight), - _editPosition, + _stateManager.imgStateHistory, + Size(_screenSize.imageWidth, _screenSize.imageHeight), + _stateManager.editPosition, configs: configs, ); } - /// Animates the WhatsApp filter sheet. - /// - /// If [up] is `true`, it animates the sheet upwards. - /// Otherwise, it animates the sheet downwards. - void _whatsAppFilterSheetAutoAnimation(bool up) async { - if (up) { - while (_whatsAppFilterShowHelper < 120) { - _whatsAppFilterShowHelper += 4; - setState(() {}); - await Future.delayed(const Duration(milliseconds: 1)); - } - } else { - while (_whatsAppFilterShowHelper > 0) { - _whatsAppFilterShowHelper -= 4; - setState(() {}); - await Future.delayed(const Duration(milliseconds: 1)); - } - } - } - - /// Moves a layer in the list to a new position. - /// - /// [oldIndex] is the current index of the layer. - /// [newIndex] is the desired index to move the layer to. - void moveLayerListPosition({ - required int oldIndex, - required int newIndex, - }) { - if (newIndex > oldIndex) { - var item = activeLayers.removeAt(oldIndex); - activeLayers.insert(newIndex - 1, item); - } else { - var item = activeLayers.removeAt(oldIndex); - activeLayers.insert(newIndex, item); - } - setState(() {}); - } - @override Widget build(BuildContext context) { - _theme = widget.configs.theme ?? + _theme = configs.theme ?? ThemeData( useMaterial3: true, colorScheme: ColorScheme.fromSeed( @@ -2121,31 +1476,31 @@ class ProImageEditorState extends State _deviceOrientation = orientation.index; } return PopScope( - canPop: _editPosition <= 0 || _doneEditing, + canPop: _stateManager.editPosition <= 0 || _doneEditing, onPopInvoked: (didPop) { - if (_editPosition > 0 && !_doneEditing) { + if (_stateManager.editPosition > 0 && !_doneEditing) { closeWarning(); } }, child: LayoutBuilder(builder: (context, constraints) { // Check if screensize changed to recalculate image size - if (_lastScreenSize.width != constraints.maxWidth || - _lastScreenSize.height != constraints.maxHeight) { - _screenSizeDebouncer(() { + if (_screenSize.lastScreenSize.width != constraints.maxWidth || + _screenSize.lastScreenSize.height != constraints.maxHeight) { + _screenSize.screenSizeDebouncer(() { _decodeImage(); }); - _lastScreenSize = Size( + _screenSize.lastScreenSize = Size( constraints.maxWidth, constraints.maxHeight, ); } return AnnotatedRegion( - value: widget.configs.imageEditorTheme.uiOverlayStyle, + value: configs.imageEditorTheme.uiOverlayStyle, child: Theme( data: _theme, child: SafeArea( child: Scaffold( - backgroundColor: widget.configs.imageEditorTheme.background, + backgroundColor: configs.imageEditorTheme.background, resizeToAvoidBottomInset: false, appBar: _buildAppBar(), body: _buildBody(), @@ -2160,60 +1515,56 @@ class ProImageEditorState extends State } PreferredSizeWidget? _buildAppBar() { - return _selectedLayer >= 0 + return _selectedLayerIndex >= 0 ? null - : widget.configs.customWidgets.appBar ?? - (widget.configs.imageEditorTheme.editorMode == - ThemeEditorMode.simple + : configs.customWidgets.appBar ?? + (configs.imageEditorTheme.editorMode == ThemeEditorMode.simple ? AppBar( automaticallyImplyLeading: false, foregroundColor: - widget.configs.imageEditorTheme.appBarForegroundColor, + configs.imageEditorTheme.appBarForegroundColor, backgroundColor: - widget.configs.imageEditorTheme.appBarBackgroundColor, + configs.imageEditorTheme.appBarBackgroundColor, actions: [ IconButton( - tooltip: widget.configs.i18n.cancel, + tooltip: configs.i18n.cancel, padding: const EdgeInsets.symmetric(horizontal: 8), - icon: Icon(widget.configs.icons.closeEditor), + icon: Icon(configs.icons.closeEditor), onPressed: closeEditor, ), const Spacer(), IconButton( key: const ValueKey('TextEditorMainUndoButton'), - tooltip: widget.configs.i18n.undo, + tooltip: configs.i18n.undo, padding: const EdgeInsets.symmetric(horizontal: 8), icon: Icon( - widget.configs.icons.undoAction, - color: _editPosition > 0 - ? widget.configs.imageEditorTheme - .appBarForegroundColor - : widget.configs.imageEditorTheme - .appBarForegroundColor + configs.icons.undoAction, + color: _stateManager.editPosition > 0 + ? configs.imageEditorTheme.appBarForegroundColor + : configs.imageEditorTheme.appBarForegroundColor .withAlpha(80), ), onPressed: undoAction, ), IconButton( key: const ValueKey('TextEditorMainRedoButton'), - tooltip: widget.configs.i18n.redo, + tooltip: configs.i18n.redo, padding: const EdgeInsets.symmetric(horizontal: 8), icon: Icon( - widget.configs.icons.redoAction, - color: _editPosition < stateHistory.length - 1 - ? widget.configs.imageEditorTheme - .appBarForegroundColor - : widget.configs.imageEditorTheme - .appBarForegroundColor + configs.icons.redoAction, + color: _stateManager.editPosition < + stateHistory.length - 1 + ? configs.imageEditorTheme.appBarForegroundColor + : configs.imageEditorTheme.appBarForegroundColor .withAlpha(80), ), onPressed: redoAction, ), IconButton( key: const ValueKey('TextEditorMainDoneButton'), - tooltip: widget.configs.i18n.done, + tooltip: configs.i18n.done, padding: const EdgeInsets.symmetric(horizontal: 8), - icon: Icon(widget.configs.icons.doneIcon), + icon: Icon(configs.icons.doneIcon), iconSize: 28, onPressed: doneEditing, ), @@ -2226,9 +1577,17 @@ class ProImageEditorState extends State var editorImage = _buildImageWithFilter(); return LayoutBuilder(builder: (context, constraints) { - _bodySize = constraints.biggest; + _screenSize.bodySize = constraints.biggest; return Listener( - onPointerSignal: isDesktop ? _mouseScroll : null, + onPointerSignal: isDesktop + ? (event) { + _desktopInteractionManager.mouseScroll( + event, + activeLayer: _activeLayer, + selectedLayerIndex: _selectedLayerIndex, + ); + } + : null, child: GestureDetector( behavior: HitTestBehavior.translucent, onScaleStart: _onScaleStart, @@ -2243,7 +1602,8 @@ class ProImageEditorState extends State transformHitTests: false, scale: 1 / constraints.maxHeight * - (constraints.maxHeight - _whatsAppFilterShowHelper * 2), + (constraints.maxHeight - + _whatsAppHelper.filterShowHelper * 2), child: Stack( alignment: Alignment.center, fit: StackFit.expand, @@ -2251,18 +1611,17 @@ class ProImageEditorState extends State children: [ Center( child: SizedBox( - height: _imageHeight, - width: _imageWidth, + height: _screenSize.imageHeight, + width: _screenSize.imageWidth, child: StreamBuilder( - stream: _mouseMoveStream.stream, + stream: _controllers.mouseMoveStream.stream, initialData: false, builder: (context, snapshot) { return MouseRegion( hitTestBehavior: HitTestBehavior.translucent, cursor: snapshot.data != true ? SystemMouseCursors.basic - : widget.configs.imageEditorTheme - .layerHoverCursor, + : configs.imageEditorTheme.layerHoverCursor, onHover: isDesktop ? (event) { var hasHit = activeLayers.indexWhere( @@ -2272,20 +1631,19 @@ class ProImageEditorState extends State element.item.hit) >= 0; if (hasHit != snapshot.data) { - _mouseMoveStream.add(hasHit); + _controllers.mouseMoveStream + .add(hasHit); } } : null, child: Screenshot( - controller: _screenshotCtrl, + controller: _controllers.screenshot, child: Stack( alignment: Alignment.center, clipBehavior: Clip.none, children: [ Hero( - tag: !_inited - ? '--' - : widget.configs.heroTag, + tag: !_inited ? '--' : configs.heroTag, createRectTween: (begin, end) => RectTween(begin: begin, end: end), child: Offstage( @@ -2293,7 +1651,8 @@ class ProImageEditorState extends State child: editorImage, ), ), - if (_selectedLayer < 0) _buildLayers(), + if (_selectedLayerIndex < 0) + _buildLayers(), ], ), ), @@ -2303,15 +1662,15 @@ class ProImageEditorState extends State ), // show same image solong decoding that screenshot is ready if (!_inited) editorImage, - if (_selectedLayer >= 0) _buildLayers(), + if (_selectedLayerIndex >= 0) _buildLayers(), _buildHelperLines(), - if (_selectedLayer >= 0) _buildRemoveIcon(), + if (_selectedLayerIndex >= 0) _buildRemoveIcon(), ], ), ), - if (widget.configs.imageEditorTheme.editorMode == + if (configs.imageEditorTheme.editorMode == ThemeEditorMode.whatsapp && - _selectedLayer < 0) + _selectedLayerIndex < 0) ..._buildWhatsAppWidgets() ], ), @@ -2321,7 +1680,8 @@ class ProImageEditorState extends State } List _buildWhatsAppWidgets() { - double opacity = max(0, min(1, 1 - 1 / 120 * _whatsAppFilterShowHelper)); + double opacity = + max(0, min(1, 1 - 1 / 120 * _whatsAppHelper.filterShowHelper)); return [ WhatsAppAppBar( configs: widget.configs, @@ -2332,54 +1692,53 @@ class ProImageEditorState extends State onTapTextEditor: openTextEditor, onTapUndo: undoAction, canUndo: canUndo, - openEditor: _openEditor, + openEditor: _isEditorOpen, ), - if (widget.configs.designMode == ImageEditorDesignModeE.material) + if (configs.designMode == ImageEditorDesignModeE.material) WhatsAppFilterBtn( configs: widget.configs, opacity: opacity, ), - if (widget.configs.customWidgets.whatsAppBottomWidget != null) + if (configs.customWidgets.whatsAppBottomWidget != null) Positioned( bottom: 0, left: 0, right: 0, child: Opacity( opacity: opacity, - child: widget.configs.customWidgets.whatsAppBottomWidget!, + child: configs.customWidgets.whatsAppBottomWidget!, ), ), Positioned( left: 0, right: 0, - bottom: -120 + _whatsAppFilterShowHelper, + bottom: -120 + _whatsAppHelper.filterShowHelper, child: Opacity( - opacity: max(0, min(1, 1 / 120 * _whatsAppFilterShowHelper)), + opacity: max(0, min(1, 1 / 120 * _whatsAppHelper.filterShowHelper)), child: Container( margin: const EdgeInsets.only(top: 7), - color: widget - .configs.imageEditorTheme.filterEditor.whatsAppBottomBarColor, + color: configs.imageEditorTheme.filterEditor.whatsAppBottomBarColor, child: FilterEditorItemList( itemScaleFactor: - max(0, min(1, 1 / 120 * _whatsAppFilterShowHelper)), + max(0, min(1, 1 / 120 * _whatsAppHelper.filterShowHelper)), byteArray: widget.byteArray, file: widget.file, assetPath: widget.assetPath, networkUrl: widget.networkUrl, - activeFilters: const [], - blur: _blur, + blurFactor: _stateManager.blurStateHistory.blur, + activeFilters: _stateManager.filters, configs: widget.configs, - selectedFilter: _filters.isNotEmpty - ? _filters.first.filter + selectedFilter: _stateManager.filters.isNotEmpty + ? _stateManager.filters.first.filter : PresetFilters.none, onSelectFilter: (filter) { - _cleanForwardChanges(); + _stateManager.cleanForwardChanges(); stateHistory.add( EditorStateHistory( transformConfigs: TransformConfigs.empty(), - bytesRefIndex: _imgStateHistory.length - 1, - blur: _blur, + bytesRefIndex: _stateManager.imgStateHistory.length - 1, + blur: _stateManager.blurStateHistory, layers: activeLayers, filters: [ FilterStateHistory( @@ -2389,7 +1748,7 @@ class ProImageEditorState extends State ], ), ); - _editPosition++; + _stateManager.editPosition++; setState(() {}); widget.onUpdateUI?.call(); @@ -2405,29 +1764,28 @@ class ProImageEditorState extends State var bottomTextStyle = const TextStyle(fontSize: 10.0, color: Colors.white); double bottomIconSize = 22.0; - return _selectedLayer >= 0 + return _selectedLayerIndex >= 0 ? null - : widget.configs.customWidgets.bottomNavigationBar ?? - (widget.configs.imageEditorTheme.editorMode == - ThemeEditorMode.simple + : configs.customWidgets.bottomNavigationBar ?? + (configs.imageEditorTheme.editorMode == ThemeEditorMode.simple ? Theme( data: _theme, child: Scrollbar( - controller: _bottomBarScrollCtrl, + controller: _controllers.bottomBarScroll, scrollbarOrientation: ScrollbarOrientation.top, thickness: isDesktop ? null : 0, child: BottomAppBar( - height: _bottomBarHeight, - color: widget - .configs.imageEditorTheme.bottomBarBackgroundColor, + height: _screenSize.bottomBarHeight, + color: + configs.imageEditorTheme.bottomBarBackgroundColor, padding: EdgeInsets.zero, child: Center( child: SingleChildScrollView( - controller: _bottomBarScrollCtrl, + controller: _controllers.bottomBarScroll, scrollDirection: Axis.horizontal, child: ConstrainedBox( constraints: BoxConstraints( - minWidth: min(_screen.width, 600), + minWidth: min(_screenSize.screen.width, 600), maxWidth: 600, ), child: Padding( @@ -2438,121 +1796,112 @@ class ProImageEditorState extends State MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.min, children: [ - if (widget - .configs.paintEditorConfigs.enabled) + if (configs.paintEditorConfigs.enabled) FlatIconTextButton( key: const ValueKey( 'open-painting-editor-btn'), label: Text( - widget.configs.i18n.paintEditor + configs.i18n.paintEditor .bottomNavigationBarText, style: bottomTextStyle), icon: Icon( - widget.configs.icons.paintingEditor + configs.icons.paintingEditor .bottomNavBar, size: bottomIconSize, color: Colors.white, ), onPressed: openPaintingEditor, ), - if (widget - .configs.textEditorConfigs.enabled) + if (configs.textEditorConfigs.enabled) FlatIconTextButton( key: const ValueKey( 'open-text-editor-btn'), label: Text( - widget.configs.i18n.textEditor + configs.i18n.textEditor .bottomNavigationBarText, style: bottomTextStyle), icon: Icon( - widget.configs.icons.textEditor - .bottomNavBar, + configs.icons.textEditor.bottomNavBar, size: bottomIconSize, color: Colors.white, ), onPressed: openTextEditor, ), - if (widget.configs.cropRotateEditorConfigs - .enabled) + if (configs.cropRotateEditorConfigs.enabled) FlatIconTextButton( key: const ValueKey( 'open-crop-rotate-editor-btn'), label: Text( - widget.configs.i18n.cropRotateEditor + configs.i18n.cropRotateEditor .bottomNavigationBarText, style: bottomTextStyle), icon: Icon( - widget.configs.icons.cropRotateEditor + configs.icons.cropRotateEditor .bottomNavBar, size: bottomIconSize, color: Colors.white, ), onPressed: openCropEditor, ), - if (widget - .configs.filterEditorConfigs.enabled) + if (configs.filterEditorConfigs.enabled) FlatIconTextButton( key: const ValueKey( 'open-filter-editor-btn'), label: Text( - widget.configs.i18n.filterEditor + configs.i18n.filterEditor .bottomNavigationBarText, style: bottomTextStyle), icon: Icon( - widget.configs.icons.filterEditor - .bottomNavBar, + configs + .icons.filterEditor.bottomNavBar, size: bottomIconSize, color: Colors.white, ), onPressed: openFilterEditor, ), - if (widget - .configs.blurEditorConfigs.enabled) + if (configs.blurEditorConfigs.enabled) FlatIconTextButton( key: const ValueKey( 'open-blur-editor-btn'), label: Text( - widget.configs.i18n.blurEditor + configs.i18n.blurEditor .bottomNavigationBarText, style: bottomTextStyle), icon: Icon( - widget.configs.icons.blurEditor - .bottomNavBar, + configs.icons.blurEditor.bottomNavBar, size: bottomIconSize, color: Colors.white, ), onPressed: openBlurEditor, ), - if (widget - .configs.emojiEditorConfigs.enabled) + if (configs.emojiEditorConfigs.enabled) FlatIconTextButton( key: const ValueKey( 'open-emoji-editor-btn'), label: Text( - widget.configs.i18n.emojiEditor + configs.i18n.emojiEditor .bottomNavigationBarText, style: bottomTextStyle), icon: Icon( - widget.configs.icons.emojiEditor - .bottomNavBar, + configs + .icons.emojiEditor.bottomNavBar, size: bottomIconSize, color: Colors.white, ), onPressed: openEmojiEditor, ), - if (widget.configs.stickerEditorConfigs - ?.enabled == + if (configs.stickerEditorConfigs?.enabled == true) FlatIconTextButton( key: const ValueKey( 'open-sticker-editor-btn'), label: Text( - widget.configs.i18n.stickerEditor + configs.i18n.stickerEditor .bottomNavigationBarText, style: bottomTextStyle), icon: Icon( - widget.configs.icons.stickerEditor - .bottomNavBar, + configs + .icons.stickerEditor.bottomNavBar, size: bottomIconSize, color: Colors.white, ), @@ -2572,95 +1921,103 @@ class ProImageEditorState extends State Widget _buildLayers() { int loopHelper = 0; - return Stack( - children: activeLayers.map((layerItem) { - var i = loopHelper; - loopHelper++; - - return LayerWidget( - key: ValueKey('${layerItem.id}-$i'), - layerHoverCursor: widget.configs.imageEditorTheme.layerHoverCursor, - padding: _selectedLayer < 0 ? EdgeInsets.zero : screenPaddingHelper, - layerData: layerItem, - textFontSize: widget.configs.textEditorConfigs.initFontSize, - emojiTextStyle: widget.configs.emojiEditorConfigs.textStyle, - enableHitDetection: _enabledHitDetection, - freeStyleHighPerformanceScaling: _freeStyleHighPerformanceScaling, - freeStyleHighPerformanceMoving: _freeStyleHighPerformanceMoving, - designMode: widget.configs.designMode, - stickerInitWidth: - widget.configs.stickerEditorConfigs?.initWidth ?? 100, - onTap: (layer) async { - if (layer is TextLayerData) { - _onTextLayerTap(layer); - } - }, - onTapUp: () { - setState(() { - if (hoverRemoveBtn) _removeLayer(_selectedLayer); - _selectedLayer = -1; - }); - widget.onUpdateUI?.call(); - }, - onTapDown: () { - _selectedLayer = i; - }, - onRemoveTap: () { - setState(() { - _removeLayer( - activeLayers - .indexWhere((element) => element.id == layerItem.id), - layer: layerItem); - }); - widget.onUpdateUI?.call(); - }, - i18n: widget.configs.i18n, - ); - }).toList(), + return IgnorePointer( + ignoring: _selectedLayerIndex >= 0, + child: Stack( + children: activeLayers.map((layerItem) { + var i = loopHelper; + loopHelper++; + + return LayerWidget( + key: ValueKey('${layerItem.id}-$i'), + layerHoverCursor: configs.imageEditorTheme.layerHoverCursor, + padding: _selectedLayerIndex < 0 + ? EdgeInsets.zero + : _screenSize.screenPaddingHelper, + layerData: layerItem, + textFontSize: configs.textEditorConfigs.initFontSize, + emojiTextStyle: configs.emojiEditorConfigs.textStyle, + enableHitDetection: _layerInteraction.enabledHitDetection, + freeStyleHighPerformanceScaling: + _layerInteraction.freeStyleHighPerformanceScaling, + freeStyleHighPerformanceMoving: + _layerInteraction.freeStyleHighPerformanceMoving, + designMode: configs.designMode, + stickerInitWidth: configs.stickerEditorConfigs?.initWidth ?? 100, + onTap: (layer) async { + if (layer is TextLayerData) { + _onTextLayerTap(layer); + } + }, + onTapUp: () { + setState(() { + if (_layerInteraction.hoverRemoveBtn) { + removeLayer(_selectedLayerIndex); + } + _selectedLayerIndex = -1; + }); + widget.onUpdateUI?.call(); + }, + onTapDown: () { + _selectedLayerIndex = i; + }, + onRemoveTap: () { + setState(() { + removeLayer( + activeLayers + .indexWhere((element) => element.id == layerItem.id), + layer: layerItem); + }); + widget.onUpdateUI?.call(); + }, + i18n: configs.i18n, + ); + }).toList(), + ), ); } Widget _buildHelperLines() { - double screenH = _screen.height; - double screenW = _screen.width; + double screenH = _screenSize.screen.height; + double screenW = _screenSize.screen.width; double lineH = 1.25; int duration = 100; - if (!_showHelperLines) return const SizedBox.shrink(); + if (!_layerInteraction.showHelperLines) return const SizedBox.shrink(); return Stack( children: [ - if (widget.configs.helperLines.showVerticalLine) + if (configs.helperLines.showVerticalLine) Align( alignment: Alignment.center, child: AnimatedContainer( duration: Duration(milliseconds: duration), - width: _showVerticalHelperLine ? lineH : 0, + width: _layerInteraction.showVerticalHelperLine ? lineH : 0, height: screenH, - color: widget.configs.imageEditorTheme.helperLine.verticalColor, + color: configs.imageEditorTheme.helperLine.verticalColor, ), ), - if (widget.configs.helperLines.showHorizontalLine) + if (configs.helperLines.showHorizontalLine) Align( alignment: Alignment.center, child: AnimatedContainer( duration: Duration(milliseconds: duration), width: screenW, - height: _showHorizontalHelperLine ? lineH : 0, - color: widget.configs.imageEditorTheme.helperLine.horizontalColor, + height: _layerInteraction.showHorizontalHelperLine ? lineH : 0, + color: configs.imageEditorTheme.helperLine.horizontalColor, ), ), - if (widget.configs.helperLines.showRotateLine) + if (configs.helperLines.showRotateLine) Positioned( - left: _rotationHelperLineX, - top: _rotationHelperLineY, + left: _layerInteraction.rotationHelperLineX, + top: _layerInteraction.rotationHelperLineY, child: FractionalTranslation( translation: const Offset(-0.5, -0.5), child: Transform.rotate( - angle: _rotationHelperLineDeg, + angle: _layerInteraction.rotationHelperLineDeg, child: AnimatedContainer( duration: Duration(milliseconds: duration), - width: _showRotationHelperLine ? lineH : 0, + width: _layerInteraction.showRotationHelperLine ? lineH : 0, height: screenH * 2, - color: widget.configs.imageEditorTheme.helperLine.rotateColor, + color: configs.imageEditorTheme.helperLine.rotateColor, ), ), ), @@ -2670,7 +2027,7 @@ class ProImageEditorState extends State } Widget _buildRemoveIcon() { - return widget.configs.customWidgets.removeLayer ?? + return configs.customWidgets.removeLayer ?? Positioned( top: 0, left: 0, @@ -2680,9 +2037,9 @@ class ProImageEditorState extends State height: kToolbarHeight, width: kToolbarHeight, decoration: BoxDecoration( - color: hoverRemoveBtn + color: _layerInteraction.hoverRemoveBtn ? Colors.red - : (widget.configs.imageEditorTheme.editorMode == + : (configs.imageEditorTheme.editorMode == ThemeEditorMode.simple ? Colors.grey.shade800 : Colors.black12), @@ -2692,7 +2049,7 @@ class ProImageEditorState extends State padding: const EdgeInsets.only(right: 12, bottom: 7), child: Center( child: Icon( - widget.configs.icons.removeElementZone, + configs.icons.removeElementZone, size: 28, ), ), @@ -2703,16 +2060,16 @@ class ProImageEditorState extends State Widget _buildImageWithFilter() { return TransformedContentGenerator( - configs: _transformConfigs, + configs: _stateManager.transformConfigs, child: LayoutBuilder(builder: (context, constraints) { - _renderedImageSize = constraints.biggest; + _screenSize.renderedImageSize = constraints.biggest; return ImageWithMultipleFilters( - width: _imageWidth, - height: _imageHeight, + width: _screenSize.imageWidth, + height: _screenSize.imageHeight, designMode: designMode, - image: _image, - filters: _filters, - blur: _blur, + image: _stateManager.image, + filters: _stateManager.filters, + blurFactor: _stateManager.blurStateHistory.blur, ); }), ); diff --git a/lib/modules/main_editor/utils/desktop_interaction_manager.dart b/lib/modules/main_editor/utils/desktop_interaction_manager.dart new file mode 100644 index 00000000..a574874c --- /dev/null +++ b/lib/modules/main_editor/utils/desktop_interaction_manager.dart @@ -0,0 +1,147 @@ +import 'dart:math'; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:pro_image_editor/models/editor_configs/pro_image_editor_configs.dart'; + +import '../../../models/layer.dart'; + +/// A manager class responsible for handling desktop interactions in the image editor. +/// +/// The `DesktopInteractionManager` class provides methods for responding to keyboard +/// and mouse events on desktop platforms. It enables users to perform actions such +/// as zooming, rotating, and navigating layers using keyboard shortcuts and mouse +/// scroll wheel movements. +class DesktopInteractionManager { + final BuildContext context; + final Function? onUpdateUI; + final Function setState; + final ProImageEditorConfigs configs; + + DesktopInteractionManager({ + required this.context, + required this.onUpdateUI, + required this.setState, + required this.configs, + }); + + /// Handles keyboard events. + /// + /// This method responds to key events and performs actions based on the pressed keys. + /// If the 'Escape' key is pressed and the widget is still mounted, it triggers the navigator to pop the current context. + bool onKey( + KeyEvent event, { + required Layer activeLayer, + required bool canPressEscape, + required bool isEditorOpen, + required Function onCloseEditor, + }) { + final key = event.logicalKey.keyLabel; + + if (context.mounted && event is KeyDownEvent) { + switch (key) { + case 'Escape': + if (canPressEscape) { + if (isEditorOpen) { + Navigator.pop(context); + } else { + onCloseEditor(); + } + } + break; + + case 'Subtract': + case 'Numpad Subtract': + case 'Page Down': + case 'Arrow Down': + _keyboardZoom(zoomIn: true, activeLayer: activeLayer); + break; + case 'Add': + case 'Numpad Add': + case 'Page Up': + case 'Arrow Up': + _keyboardZoom(zoomIn: false, activeLayer: activeLayer); + break; + case 'Arrow Left': + _keyboardRotate(left: true, activeLayer: activeLayer); + break; + case 'Arrow Right': + _keyboardRotate(left: false, activeLayer: activeLayer); + break; + } + } + + return false; + } + + /// Handles Keyboard zoom event + void _keyboardRotate({ + required bool left, + required Layer activeLayer, + }) { + if (left) { + activeLayer.rotation -= 0.087266; + } else { + activeLayer.rotation += 0.087266; + } + setState(() {}); + onUpdateUI?.call(); + } + + /// Handles Keyboard zoom event + void _keyboardZoom({ + required bool zoomIn, + required Layer activeLayer, + }) { + double factor = activeLayer is PaintingLayerData + ? 0.1 + : activeLayer is TextLayerData + ? 0.15 + : configs.textEditorConfigs.initFontSize / 50; + if (zoomIn) { + activeLayer.scale -= factor; + activeLayer.scale = max(0.1, activeLayer.scale); + } else { + activeLayer.scale += factor; + } + setState(() {}); + onUpdateUI?.call(); + } + + /// Handles mouse scroll events. + void mouseScroll( + PointerSignalEvent event, { + required Layer activeLayer, + required int selectedLayerIndex, + }) { + bool shiftDown = HardwareKeyboard.instance.logicalKeysPressed + .contains(LogicalKeyboardKey.shiftLeft) || + HardwareKeyboard.instance.logicalKeysPressed + .contains(LogicalKeyboardKey.shiftRight); + + if (event is PointerScrollEvent && selectedLayerIndex >= 0) { + if (shiftDown) { + if (event.scrollDelta.dy > 0) { + activeLayer.rotation -= 0.087266; + } else if (event.scrollDelta.dy < 0) { + activeLayer.rotation += 0.087266; + } + } else { + double factor = activeLayer is PaintingLayerData + ? 0.1 + : activeLayer is TextLayerData + ? 0.15 + : configs.textEditorConfigs.initFontSize / 50; + if (event.scrollDelta.dy > 0) { + activeLayer.scale -= factor; + activeLayer.scale = max(0.1, activeLayer.scale); + } else if (event.scrollDelta.dy < 0) { + activeLayer.scale += factor; + } + } + setState(() {}); + onUpdateUI?.call(); + } + } +} diff --git a/lib/modules/main_editor/utils/layer_interaction_helper.dart b/lib/modules/main_editor/utils/layer_interaction_helper.dart new file mode 100644 index 00000000..92a23c94 --- /dev/null +++ b/lib/modules/main_editor/utils/layer_interaction_helper.dart @@ -0,0 +1,377 @@ +import 'dart:io'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:vibration/vibration.dart'; + +import '../../../utils/debounce.dart'; +import '../../../models/history/last_position.dart'; +import '../../../models/layer.dart'; + +/// A helper class responsible for managing layer interactions in the editor. +/// +/// The `LayerInteractionHelper` class provides methods for handling various interactions +/// with layers in an image editing environment, including scaling, rotating, flipping, +/// and zooming. It also manages the display of helper lines and provides haptic feedback +/// when interacting with these lines to enhance the user experience. +class LayerInteractionHelper { + /// Debounce for scaling actions in the editor. + late Debounce scaleDebounce; + + /// Y-coordinate of the rotation helper line. + double rotationHelperLineY = 0; + + /// X-coordinate of the rotation helper line. + double rotationHelperLineX = 0; + + /// Rotation angle of the rotation helper line. + double rotationHelperLineDeg = 0; + + /// The base scale factor for the editor. + double baseScaleFactor = 1.0; + + /// The base angle factor for the editor. + double baseAngleFactor = 0; + + /// X-coordinate where snapping started. + double snapStartPosX = 0; + + /// Y-coordinate where snapping started. + double snapStartPosY = 0; + + /// Initial rotation angle when snapping started. + double snapStartRotation = 0; + + /// Last recorded rotation angle during snapping. + double snapLastRotation = 0; + + /// Flag indicating if vertical helper lines should be displayed. + bool showVerticalHelperLine = false; + + /// Flag indicating if horizontal helper lines should be displayed. + bool showHorizontalHelperLine = false; + + /// Flag indicating if rotation helper lines should be displayed. + bool showRotationHelperLine = false; + + /// Flag indicating if the device can vibrate. + bool deviceCanVibrate = false; + + /// Flag indicating if the device can perform custom vibration. + bool deviceCanCustomVibrate = false; + + /// Flag indicating if rotation helper lines have started. + bool rotationStartedHelper = false; + + /// Flag indicating if helper lines should be displayed. + bool showHelperLines = false; + + /// Flag indicating if the remove button is hovered. + bool hoverRemoveBtn = false; + + /// Enables or disables hit detection. + /// When `true`, allows detecting user interactions with the painted layer. + bool enabledHitDetection = true; + + /// Controls high-performance scaling for free-style drawing. + /// When `true`, enables optimized scaling for improved performance. + bool freeStyleHighPerformanceScaling = false; + + /// Controls high-performance moving for free-style drawing. + /// When `true`, enables optimized moving for improved performance. + bool freeStyleHighPerformanceMoving = false; + + /// Flag indicating if the scaling tool is active. + bool _activeScale = false; + + /// Span for detecting hits on layers. + final double hitSpan = 10; + + /// Last recorded X-axis position for layers. + LayerLastPosition lastPositionX = LayerLastPosition.center; + + /// Last recorded Y-axis position for layers. + LayerLastPosition lastPositionY = LayerLastPosition.center; + + calculateMovement({ + required BuildContext context, + required ScaleUpdateDetails detail, + required Layer activeLayer, + required double screenMiddleX, + required double screenMiddleY, + required EdgeInsets screenPaddingHelper, + required bool configEnabledHitVibration, + }) { + if (_activeScale) return; + + activeLayer.offset = Offset( + activeLayer.offset.dx + detail.focalPointDelta.dx, + activeLayer.offset.dy + detail.focalPointDelta.dy, + ); + + hoverRemoveBtn = detail.focalPoint.dx <= kToolbarHeight && + detail.focalPoint.dy <= + kToolbarHeight + MediaQuery.of(context).viewPadding.top; + + bool vibarate = false; + double posX = activeLayer.offset.dx + screenPaddingHelper.left; + double posY = activeLayer.offset.dy + screenPaddingHelper.top; + + bool hitAreaX = detail.focalPoint.dx >= snapStartPosX - hitSpan && + detail.focalPoint.dx <= snapStartPosX + hitSpan; + bool hitAreaY = detail.focalPoint.dy >= snapStartPosY - hitSpan && + detail.focalPoint.dy <= snapStartPosY + hitSpan; + + bool helperGoNearLineLeft = + posX >= screenMiddleX && lastPositionX == LayerLastPosition.left; + bool helperGoNearLineRight = + posX <= screenMiddleX && lastPositionX == LayerLastPosition.right; + bool helperGoNearLineTop = + posY >= screenMiddleY && lastPositionY == LayerLastPosition.top; + bool helperGoNearLineBottom = + posY <= screenMiddleY && lastPositionY == LayerLastPosition.bottom; + + /// Calc vertical helper line + if ((!showVerticalHelperLine && + (helperGoNearLineLeft || helperGoNearLineRight)) || + (showVerticalHelperLine && hitAreaX)) { + if (!showVerticalHelperLine) { + vibarate = true; + snapStartPosX = detail.focalPoint.dx; + } + showVerticalHelperLine = true; + activeLayer.offset = Offset( + screenMiddleX - screenPaddingHelper.left, activeLayer.offset.dy); + lastPositionX = LayerLastPosition.center; + } else { + showVerticalHelperLine = false; + lastPositionX = posX <= screenMiddleX + ? LayerLastPosition.left + : LayerLastPosition.right; + } + + /// Calc horizontal helper line + if ((!showHorizontalHelperLine && + (helperGoNearLineTop || helperGoNearLineBottom)) || + (showHorizontalHelperLine && hitAreaY)) { + if (!showHorizontalHelperLine) { + vibarate = true; + snapStartPosY = detail.focalPoint.dy; + } + showHorizontalHelperLine = true; + activeLayer.offset = Offset( + activeLayer.offset.dx, screenMiddleY - screenPaddingHelper.top); + lastPositionY = LayerLastPosition.center; + } else { + showHorizontalHelperLine = false; + lastPositionY = posY <= screenMiddleY + ? LayerLastPosition.top + : LayerLastPosition.bottom; + } + + if (configEnabledHitVibration && vibarate) { + _lineHitVibrate(); + } + } + + calculateScale({ + required ScaleUpdateDetails detail, + required Layer activeLayer, + required EdgeInsets screenPaddingHelper, + required bool configEnabledHitVibration, + }) { + _activeScale = true; + + activeLayer.scale = baseScaleFactor * detail.scale; + activeLayer.rotation = baseAngleFactor + detail.rotation; + + var hitSpanX = hitSpan / 2; + var deg = activeLayer.rotation * 180 / pi; + var degChange = detail.rotation * 180 / pi; + var degHit = (snapStartRotation + degChange) % 45; + var hitAreaBelow = degHit <= hitSpanX; + var hitAreaAfter = degHit >= 45 - hitSpanX; + var hitArea = hitAreaBelow || hitAreaAfter; + + if ((!showRotationHelperLine && + ((degHit > 0 && degHit <= hitSpanX && snapLastRotation < deg) || + (degHit < 45 && + degHit >= 45 - hitSpanX && + snapLastRotation > deg))) || + (showRotationHelperLine && hitArea)) { + if (rotationStartedHelper) { + activeLayer.rotation = + (deg - (degHit > 45 - hitSpanX ? degHit - 45 : degHit)) / 180 * pi; + rotationHelperLineDeg = activeLayer.rotation; + + double posY = activeLayer.offset.dy + screenPaddingHelper.top; + double posX = activeLayer.offset.dx + screenPaddingHelper.left; + + rotationHelperLineX = posX; + rotationHelperLineY = posY; + if (configEnabledHitVibration && !showRotationHelperLine) { + _lineHitVibrate(); + } + showRotationHelperLine = true; + } + snapLastRotation = deg; + } else { + showRotationHelperLine = false; + rotationStartedHelper = true; + } + + scaleDebounce(() => _activeScale = false); + } + + onScaleEnd() { + enabledHitDetection = true; + freeStyleHighPerformanceScaling = false; + freeStyleHighPerformanceMoving = false; + showHorizontalHelperLine = false; + showVerticalHelperLine = false; + showRotationHelperLine = false; + showHelperLines = false; + hoverRemoveBtn = false; + } + + /// Rotate a layer. + /// + /// This method rotates a layer based on various factors, including flip and angle. + void rotateLayer({ + required Layer layer, + required bool beforeIsFlipX, + required double newImgW, + required double newImgH, + required double rotationScale, + required double rotationRadian, + required double rotationAngle, + }) { + if (beforeIsFlipX) { + layer.rotation -= rotationRadian; + } else { + layer.rotation += rotationRadian; + } + + if (rotationAngle == 90) { + layer.scale /= rotationScale; + layer.offset = Offset( + newImgW - layer.offset.dy / rotationScale, + layer.offset.dx / rotationScale, + ); + } else if (rotationAngle == 180) { + layer.offset = Offset( + newImgW - layer.offset.dx, + newImgH - layer.offset.dy, + ); + } else if (rotationAngle == 270) { + layer.scale /= rotationScale; + layer.offset = Offset( + layer.offset.dy / rotationScale, + newImgH - layer.offset.dx / rotationScale, + ); + } + } + + /// Handles zooming of a layer. + /// + /// This method calculates the zooming of a layer based on the specified parameters. + /// It checks if the layer should be zoomed and performs the necessary transformations. + /// + /// Returns `true` if the layer was zoomed, otherwise `false`. + bool zoomedLayer({ + required Layer layer, + required double scale, + required double scaleX, + required double oldFullH, + required double oldFullW, + required double pixelRatio, + required Rect cropRect, + required bool isHalfPi, + }) { + var paddingTop = cropRect.top / pixelRatio; + var paddingLeft = cropRect.left / pixelRatio; + var paddingRight = oldFullW - cropRect.right; + var paddingBottom = oldFullH - cropRect.bottom; + + // important to check with < 1 and >-1 cuz crop-editor has rounding bugs + if (paddingTop > 0.1 || + paddingTop < -0.1 || + paddingLeft > 0.1 || + paddingLeft < -0.1 || + paddingRight > 0.1 || + paddingRight < -0.1 || + paddingBottom > 0.1 || + paddingBottom < -0.1) { + var initialIconX = (layer.offset.dx - paddingLeft) * scaleX; + var initialIconY = (layer.offset.dy - paddingTop) * scaleX; + layer.offset = Offset( + initialIconX, + initialIconY, + ); + + layer.scale *= scale; + return true; + } + return false; + } + + /// Flip a layer horizontally or vertically. + /// + /// This method flips a layer either horizontally or vertically based on the specified parameters. + void flipLayer({ + required Layer layer, + required bool flipX, + required bool flipY, + required bool isHalfPi, + required double imageWidth, + required double imageHeight, + }) { + if (flipY) { + if (isHalfPi) { + layer.flipY = !layer.flipY; + } else { + layer.flipX = !layer.flipX; + } + layer.offset = Offset( + imageWidth - layer.offset.dx, + layer.offset.dy, + ); + } + if (flipX) { + layer.flipX = !layer.flipX; + layer.offset = Offset( + layer.offset.dx, + imageHeight - layer.offset.dy, + ); + } + } + + /// Vibrates the device briefly if enabled and supported. + /// + /// This function checks if helper lines hit vibration is enabled in the widget's + /// configurations (`widget.configs.helperLines.hitVibration`) and whether the + /// device supports vibration. If both conditions are met, it triggers a brief + /// vibration on the device. + /// + /// If the device supports custom vibrations, it uses the `Vibration.vibrate` + /// method with a duration of 3 milliseconds to produce the vibration. + /// + /// On older Android devices, it initiates vibration using `Vibration.vibrate`, + /// and then, after 3 milliseconds, cancels the vibration using `Vibration.cancel`. + /// + /// This function is used to provide haptic feedback when helper lines are interacted + /// with, enhancing the user experience. + void _lineHitVibrate() { + if (deviceCanVibrate && deviceCanCustomVibrate) { + Vibration.vibrate(duration: 3); + } else if (Platform.isAndroid) { + // On old android devices we can stop the vibration after 3 milliseconds + // iOS: only works for custom haptic vibrations using CHHapticEngine. + // This will set `deviceCanCustomVibrate` anyway to true so it's impossible to fake it. + Vibration.vibrate(); + Future.delayed(const Duration(milliseconds: 3)).whenComplete(() { + Vibration.cancel(); + }); + } + } +} diff --git a/lib/modules/main_editor/utils/layer_manager.dart b/lib/modules/main_editor/utils/layer_manager.dart new file mode 100644 index 00000000..ff073423 --- /dev/null +++ b/lib/modules/main_editor/utils/layer_manager.dart @@ -0,0 +1,88 @@ +import 'dart:ui'; + +import 'package:pro_image_editor/models/layer.dart'; + +/// A class responsible for managing layers in an image editing environment. +/// +/// The `LayerManager` provides methods for copying layers to create new instances +/// of the same type. It supports various types of layers, including text, emoji, +/// painting, and sticker layers. +class LayerManager { + /// Copy a layer to create a new instance of the same type. + /// + /// This method takes a [layer] as input and creates a new instance of the same type. + /// If the layer type is not recognized, it returns the original layer unchanged. + Layer copyLayer(Layer layer) { + if (layer is TextLayerData) { + return createCopyTextLayer(layer); + } else if (layer is EmojiLayerData) { + return createCopyEmojiLayer(layer); + } else if (layer is PaintingLayerData) { + return createCopyPaintingLayer(layer); + } else if (layer is StickerLayerData) { + return createCopyStickerLayer(layer); + } else { + return layer; + } + } + + /// Create a copy of a TextLayerData instance. + TextLayerData createCopyTextLayer(TextLayerData layer) { + return TextLayerData( + id: layer.id, + text: layer.text, + align: layer.align, + fontScale: layer.fontScale, + background: Color(layer.background.value), + color: Color(layer.color.value), + colorMode: layer.colorMode, + colorPickerPosition: layer.colorPickerPosition, + offset: Offset(layer.offset.dx, layer.offset.dy), + rotation: layer.rotation, + textStyle: layer.textStyle, + scale: layer.scale, + flipX: layer.flipX, + flipY: layer.flipY, + ); + } + + /// Create a copy of an EmojiLayerData instance. + EmojiLayerData createCopyEmojiLayer(EmojiLayerData layer) { + return EmojiLayerData( + id: layer.id, + emoji: layer.emoji, + offset: Offset(layer.offset.dx, layer.offset.dy), + rotation: layer.rotation, + scale: layer.scale, + flipX: layer.flipX, + flipY: layer.flipY, + ); + } + + /// Create a copy of an EmojiLayerData instance. + StickerLayerData createCopyStickerLayer(StickerLayerData layer) { + return StickerLayerData( + id: layer.id, + sticker: layer.sticker, + offset: Offset(layer.offset.dx, layer.offset.dy), + rotation: layer.rotation, + scale: layer.scale, + flipX: layer.flipX, + flipY: layer.flipY, + ); + } + + /// Create a copy of a PaintingLayerData instance. + PaintingLayerData createCopyPaintingLayer(PaintingLayerData layer) { + return PaintingLayerData( + id: layer.id, + offset: Offset(layer.offset.dx, layer.offset.dy), + rotation: layer.rotation, + scale: layer.scale, + flipX: layer.flipX, + flipY: layer.flipY, + item: layer.item.copy(), + rawSize: layer.rawSize, + ); + } +} diff --git a/lib/modules/main_editor/utils/main_editor_callbacks.dart b/lib/modules/main_editor/utils/main_editor_callbacks.dart new file mode 100644 index 00000000..a1655ce2 --- /dev/null +++ b/lib/modules/main_editor/utils/main_editor_callbacks.dart @@ -0,0 +1,13 @@ +import 'dart:typed_data'; + +/// A typedef representing a callback function invoked when image editing is complete. +/// +/// This callback typically receives the edited image as bytes in the form of a Uint8List. +/// It should return a Future indicating the completion of any asynchronous operations. +typedef ImageEditingCompleteCallback = Future Function(Uint8List bytes); + +/// A typedef representing a callback function invoked when no image editing is performed. +/// +/// This callback does not receive any parameters and is typically used to handle scenarios +/// where the user cancels or exits the image editing process without making any changes. +typedef ImageEditingEmptyCallback = void Function(); diff --git a/lib/modules/main_editor/utils/main_editor_controllers.dart b/lib/modules/main_editor/utils/main_editor_controllers.dart new file mode 100644 index 00000000..57830cc6 --- /dev/null +++ b/lib/modules/main_editor/utils/main_editor_controllers.dart @@ -0,0 +1,29 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:screenshot/screenshot.dart'; + +/// A class that manages various controllers used in the main editor interface. +class MainEditorControllers { + /// Stream controller for tracking mouse movement within the editor. + late final StreamController mouseMoveStream; + + /// Scroll controller for the bottom bar in the editor interface. + late final ScrollController bottomBarScroll; + + /// Controller for capturing screenshots of the editor content. + late final ScreenshotController screenshot; + + /// Constructs a new instance of [MainEditorControllers]. + MainEditorControllers() { + mouseMoveStream = StreamController.broadcast(); + screenshot = ScreenshotController(); + bottomBarScroll = ScrollController(); + } + + /// Disposes of resources held by the controllers. + void dispose() { + mouseMoveStream.close(); + bottomBarScroll.dispose(); + } +} diff --git a/lib/modules/main_editor/utils/screen_size_helper.dart b/lib/modules/main_editor/utils/screen_size_helper.dart new file mode 100644 index 00000000..cf165910 --- /dev/null +++ b/lib/modules/main_editor/utils/screen_size_helper.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:pro_image_editor/models/theme/theme_editor_mode.dart'; + +import '../../../models/editor_configs/pro_image_editor_configs.dart'; +import '../../../utils/debounce.dart'; + +/// A helper class for managing screen size and padding calculations. +class ScreenSizeHelper { + /// The build context used to obtain screen size information. + final BuildContext context; + + /// Configuration options for the image editor. + final ProImageEditorConfigs configs; + + ScreenSizeHelper({ + required this.context, + required this.configs, + }) { + screenSizeDebouncer = Debounce(const Duration(milliseconds: 200)); + } + + /// Getter for the screen size of the device. + Size get screen => MediaQuery.of(context).size; + + /// Width of the image being edited. + double imageWidth = 0; + + /// Height of the image being edited. + double imageHeight = 0; + + /// Getter for the screen inner height, excluding top and bottom padding. + double get screenInnerHeight => + screen.height - + screenPadding.top - + screenPadding.bottom - + allToolbarHeight; + + /// Getter for the X-coordinate of the middle of the screen. + double get screenMiddleX => + screen.width / 2 - (screenPadding.left + screenPadding.right) / 2; + + /// Getter for the Y-coordinate of the middle of the screen. + double get screenMiddleY => + screen.height / 2 - (screenPadding.top + screenPadding.bottom) / 2; + + /// Returns the total height of all toolbars. + double get allToolbarHeight => appBarHeight + bottomBarHeight; + + /// Returns the height of the app bar. + double get appBarHeight => + configs.imageEditorTheme.editorMode == ThemeEditorMode.simple + ? kToolbarHeight + : 0; + + /// Returns the height of the bottom bar. + double get bottomBarHeight => + configs.imageEditorTheme.editorMode == ThemeEditorMode.simple + ? kBottomNavigationBarHeight + : 0; + + /// Getter for the screen padding, accounting for safe area insets. + EdgeInsets get screenPadding => MediaQuery.of(context).padding; + + /// Get the screen padding values. + EdgeInsets get screenPaddingHelper => EdgeInsets.only( + top: (screen.height - + screenPadding.top - + screenPadding.bottom - + imageHeight) / + 2, + left: (screen.width - + screenPadding.left - + screenPadding.right - + imageWidth) / + 2, + ); + + /// Debounce for handling changes in screen size. + late Debounce screenSizeDebouncer; + + /// Stores the last recorded screen size. + Size lastScreenSize = const Size(0, 0); + + /// Stores the last recorded body size. + Size bodySize = Size.zero; + + /// Stores the last recorded image size. + Size renderedImageSize = Size.zero; +} diff --git a/lib/modules/main_editor/utils/state_manager.dart b/lib/modules/main_editor/utils/state_manager.dart new file mode 100644 index 00000000..16760c2a --- /dev/null +++ b/lib/modules/main_editor/utils/state_manager.dart @@ -0,0 +1,50 @@ +import '../../../models/crop_rotate_editor/transform_factors.dart'; +import '../../../models/editor_image.dart'; +import '../../../models/history/blur_state_history.dart'; +import '../../../models/history/filter_state_history.dart'; +import '../../../models/history/state_history.dart'; +import '../../../models/layer.dart'; + +/// A class for managing the state and history of image editing changes. +class StateManager { + /// Position in the edit history. + int editPosition = 0; + + /// List to track changes made to the image during editing. + List imgStateHistory = []; + + /// List to store the history of image editor changes. + List stateHistory = []; + + /// Get the list of filters from the current image editor changes. + List get filters => stateHistory[editPosition].filters; + + /// Get the transformconfigurations from the crop/ rotate editor. + TransformConfigs get transformConfigs => + stateHistory[editPosition].transformConfigs; + + /// Get the blur state from the current image editor changes. + BlurStateHistory get blurStateHistory => stateHistory[editPosition].blur; + + /// Get the current image being edited from the change list. + EditorImage get image => + imgStateHistory[stateHistory[editPosition].bytesRefIndex]; + + /// Get the list of layers from the current image editor changes. + List get activeLayers => stateHistory[editPosition].layers; + + /// Clean forward changes in the history. + /// + /// This method removes any changes made after the current edit position in the history. + void cleanForwardChanges() { + if (stateHistory.length > 1) { + while (editPosition < stateHistory.length - 1) { + stateHistory.removeLast(); + if (imgStateHistory.length - 1 > stateHistory.last.bytesRefIndex) { + imgStateHistory.removeLast(); + } + } + } + editPosition = stateHistory.length - 1; + } +} diff --git a/lib/modules/main_editor/utils/whatsapp_helper.dart b/lib/modules/main_editor/utils/whatsapp_helper.dart new file mode 100644 index 00000000..6364d2f5 --- /dev/null +++ b/lib/modules/main_editor/utils/whatsapp_helper.dart @@ -0,0 +1,25 @@ +/// A helper class for managing WhatsApp filter animations and values. +class WhatsAppHelper { + /// Represents the helper value for showing WhatsApp filters. + double filterShowHelper = 0; + + /// Animates the WhatsApp filter sheet. + /// + /// If [up] is `true`, it animates the sheet upwards. + /// Otherwise, it animates the sheet downwards. + void filterSheetAutoAnimation(bool up, Function setState) async { + if (up) { + while (filterShowHelper < 120) { + filterShowHelper += 4; + setState(() {}); + await Future.delayed(const Duration(milliseconds: 1)); + } + } else { + while (filterShowHelper > 0) { + filterShowHelper -= 4; + setState(() {}); + await Future.delayed(const Duration(milliseconds: 1)); + } + } + } +} diff --git a/lib/modules/paint_editor/paint_editor.dart b/lib/modules/paint_editor/paint_editor.dart index 7251fa3b..a9e364ae 100644 --- a/lib/modules/paint_editor/paint_editor.dart +++ b/lib/modules/paint_editor/paint_editor.dart @@ -6,19 +6,18 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:pro_image_editor/designs/whatsapp/whatsapp_painting_appbar.dart'; import 'package:pro_image_editor/designs/whatsapp/whatsapp_painting_bottombar.dart'; -import 'package:pro_image_editor/models/editor_configs/pro_image_editor_configs.dart'; +import 'package:pro_image_editor/models/init_configs/paint_canvas_init_configs.dart'; import 'package:pro_image_editor/models/theme/theme.dart'; import 'package:pro_image_editor/widgets/layer_stack.dart'; import '../../models/crop_rotate_editor/transform_factors.dart'; import '../../models/editor_configs/paint_editor_configs.dart'; import '../../models/editor_image.dart'; -import '../../models/filter_state_history.dart'; -import '../../models/blur_state_history.dart'; import '../../models/paint_editor/paint_bottom_bar_item.dart'; -import '../../models/layer.dart'; +import '../../models/init_configs/paint_editor_init_configs.dart'; import '../../utils/design_mode.dart'; -import '../../utils/helper/editor_mixin.dart'; +import '../../mixins/converted_configs.dart'; +import '../../mixins/standalone_editor.dart'; import '../../utils/theme_functions.dart'; import '../../widgets/color_picker/bar_color_picker.dart'; import '../../widgets/color_picker/color_picker_configs.dart'; @@ -30,261 +29,118 @@ import '../filter_editor/widgets/image_with_multiple_filters.dart'; import 'painting_canvas.dart'; import 'utils/paint_editor_enum.dart'; -/// A StatefulWidget that represents an image editor with painting capabilities. -class PaintingEditor extends StatefulWidget with ImageEditorMixin { +/// The `PaintingEditor` widget allows users to editing images with painting tools. +/// +/// You can create a `PaintingEditor` using one of the factory methods provided: +/// - `PaintingEditor.file`: Loads an image from a file. +/// - `PaintingEditor.asset`: Loads an image from an asset. +/// - `PaintingEditor.network`: Loads an image from a network URL. +/// - `PaintingEditor.memory`: Loads an image from memory as a `Uint8List`. +/// - `PaintingEditor.autoSource`: Automatically selects the source based on provided parameters. +class PaintingEditor extends StatefulWidget + with StandaloneEditor { @override - final ProImageEditorConfigs configs; - - /// The theme configuration for the editor. - final ThemeData theme; - - /// A Uint8List representing the image data in memory. - final Uint8List? byteArray; - - /// The asset path of the image. - final String? assetPath; - - /// The network URL of the image. - final String? networkUrl; - - /// A File representing the image file. - final File? file; - - /// The transform configurations how the image should be initialized. - final TransformConfigs? transformConfigs; - - /// A list of Layer objects representing image layers. - final List? layers; - - /// The size of the image. - final Size imageSize; - - /// Additional padding for the editor. - final EdgeInsets? paddingHelper; - - /// A callback function that can be used to update the UI from custom widgets. - final Function? onUpdateUI; - - /// A list of applied filters to the editor. - final List filters; - - /// The blur state to the editor. - final BlurStateHistory blur; + final PaintEditorInitConfigs initConfigs; + @override + final EditorImage editorImage; - /// Constructs a PaintingEditor instance. + /// Constructs a `PaintingEditor` widget. /// - /// The `PaintingEditor._` constructor should not be directly used. Instead, use one of the factory constructors. + /// The [key] parameter is used to provide a key for the widget. + /// The [editorImage] parameter specifies the image to be edited. + /// The [initConfigs] parameter specifies the initialization configurations for the editor. const PaintingEditor._({ super.key, - this.byteArray, - this.assetPath, - this.networkUrl, - this.file, - required this.theme, - this.configs = const ProImageEditorConfigs(), - this.transformConfigs, - required this.imageSize, - this.layers, - this.onUpdateUI, - this.paddingHelper, - required this.filters, - required this.blur, - }) : assert( - byteArray != null || - file != null || - networkUrl != null || - assetPath != null, - 'At least one of bytes, file, networkUrl, or assetPath must not be null.', - ); - - ///Constructor for loading image from memory. + required this.editorImage, + required this.initConfigs, + }); + + /// Constructs a `PaintingEditor` widget with image data loaded from memory. factory PaintingEditor.memory( Uint8List byteArray, { Key? key, - required ThemeData theme, - ProImageEditorConfigs configs = const ProImageEditorConfigs(), - TransformConfigs? transformConfigs, - required Size imageSize, - List? layers, - EdgeInsets? paddingHelper, - Function? onUpdateUI, - List? filters, - BlurStateHistory? blur, + required PaintEditorInitConfigs initConfigs, }) { return PaintingEditor._( key: key, - byteArray: byteArray, - onUpdateUI: onUpdateUI, - theme: theme, - imageSize: imageSize, - layers: layers, - paddingHelper: paddingHelper, - configs: configs, - transformConfigs: transformConfigs, - filters: filters ?? [], - blur: blur ?? BlurStateHistory(), + editorImage: EditorImage(byteArray: byteArray), + initConfigs: initConfigs, ); } - /// Constructor for loading image from [File]. + /// Constructs a `PaintingEditor` widget with an image loaded from a file. factory PaintingEditor.file( File file, { Key? key, - required ThemeData theme, - ProImageEditorConfigs configs = const ProImageEditorConfigs(), - TransformConfigs? transformConfigs, - required Size imageSize, - List? layers, - EdgeInsets? paddingHelper, - Function? onUpdateUI, - List? filters, - BlurStateHistory? blur, + required PaintEditorInitConfigs initConfigs, }) { return PaintingEditor._( key: key, - file: file, - theme: theme, - imageSize: imageSize, - layers: layers, - paddingHelper: paddingHelper, - configs: configs, - transformConfigs: transformConfigs, - onUpdateUI: onUpdateUI, - filters: filters ?? [], - blur: blur ?? BlurStateHistory(), + editorImage: EditorImage(file: file), + initConfigs: initConfigs, ); } - /// Constructor for loading image from assetPath. + /// Constructs a `PaintingEditor` widget with an image loaded from an asset. factory PaintingEditor.asset( String assetPath, { Key? key, - required ThemeData theme, - ProImageEditorConfigs configs = const ProImageEditorConfigs(), - TransformConfigs? transformConfigs, - required Size imageSize, - List? layers, - EdgeInsets? paddingHelper, - Function? onUpdateUI, - List? filters, - BlurStateHistory? blur, + required PaintEditorInitConfigs initConfigs, }) { return PaintingEditor._( key: key, - assetPath: assetPath, - theme: theme, - imageSize: imageSize, - layers: layers, - paddingHelper: paddingHelper, - configs: configs, - transformConfigs: transformConfigs, - onUpdateUI: onUpdateUI, - filters: filters ?? [], - blur: blur ?? BlurStateHistory(), + editorImage: EditorImage(assetPath: assetPath), + initConfigs: initConfigs, ); } - /// Constructor for loading image from network url. + /// Constructs a `PaintingEditor` widget with an image loaded from a network URL. factory PaintingEditor.network( String networkUrl, { Key? key, - required ThemeData theme, - ProImageEditorConfigs configs = const ProImageEditorConfigs(), - TransformConfigs? transformConfigs, - required Size imageSize, - List? layers, - EdgeInsets? paddingHelper, - Function? onUpdateUI, - List? filters, - BlurStateHistory? blur, + required PaintEditorInitConfigs initConfigs, }) { return PaintingEditor._( key: key, - networkUrl: networkUrl, - theme: theme, - imageSize: imageSize, - layers: layers, - paddingHelper: paddingHelper, - configs: configs, - transformConfigs: transformConfigs, - onUpdateUI: onUpdateUI, - filters: filters ?? [], - blur: blur ?? BlurStateHistory(), + editorImage: EditorImage(networkUrl: networkUrl), + initConfigs: initConfigs, ); } - /// Constructor for automatic source selection based on properties + /// Constructs a `PaintingEditor` widget with an image loaded automatically based on the provided source. + /// + /// Either [byteArray], [file], [networkUrl], or [assetPath] must be provided. factory PaintingEditor.autoSource({ Key? key, - required ThemeData theme, - ProImageEditorConfigs configs = const ProImageEditorConfigs(), - TransformConfigs? transformConfigs, - required Size imageSize, Uint8List? byteArray, File? file, String? assetPath, String? networkUrl, - List? layers, - EdgeInsets? paddingHelper, - Function? onUpdateUI, - List? filters, - BlurStateHistory? blur, + required PaintEditorInitConfigs initConfigs, }) { if (byteArray != null) { return PaintingEditor.memory( byteArray, key: key, - theme: theme, - imageSize: imageSize, - layers: layers, - paddingHelper: paddingHelper, - configs: configs, - transformConfigs: transformConfigs, - onUpdateUI: onUpdateUI, - filters: filters, - blur: blur, + initConfigs: initConfigs, ); } else if (file != null) { return PaintingEditor.file( file, key: key, - theme: theme, - imageSize: imageSize, - layers: layers, - paddingHelper: paddingHelper, - configs: configs, - transformConfigs: transformConfigs, - onUpdateUI: onUpdateUI, - filters: filters, - blur: blur, + initConfigs: initConfigs, ); } else if (networkUrl != null) { return PaintingEditor.network( networkUrl, key: key, - theme: theme, - imageSize: imageSize, - layers: layers, - paddingHelper: paddingHelper, - configs: configs, - transformConfigs: transformConfigs, - onUpdateUI: onUpdateUI, - filters: filters, - blur: blur, + initConfigs: initConfigs, ); } else if (assetPath != null) { return PaintingEditor.asset( assetPath, key: key, - theme: theme, - imageSize: imageSize, - layers: layers, - paddingHelper: paddingHelper, - configs: configs, - transformConfigs: transformConfigs, - onUpdateUI: onUpdateUI, - filters: filters, - blur: blur, + initConfigs: initConfigs, ); } else { throw ArgumentError( @@ -297,16 +153,15 @@ class PaintingEditor extends StatefulWidget with ImageEditorMixin { } class PaintingEditorState extends State - with ImageEditorStateMixin { + with + ImageEditorConvertedConfigs, + StandaloneEditorState { /// A global key for accessing the state of the PaintingCanvas widget. final _imageKey = GlobalKey(); /// A global key for accessing the state of the Scaffold widget. final _key = GlobalKey(); - /// An instance of the EditorImage class representing the image to be edited. - late EditorImage _editorImage; - /// A ScrollController for controlling the scrolling behavior of the bottom navigation bar. late ScrollController _bottomBarScrollCtrl; @@ -378,17 +233,10 @@ class PaintingEditorState extends State _fill = paintEditorConfigs.initialFill; _bottomBarScrollCtrl = ScrollController(); - _editorImage = EditorImage( - assetPath: widget.assetPath, - byteArray: widget.byteArray, - file: widget.file, - networkUrl: widget.networkUrl, - ); - /// Important to set state after view init to set action icons WidgetsBinding.instance.addPostFrameCallback((timeStamp) { setState(() {}); - widget.onUpdateUI?.call(); + onUpdateUI?.call(); }); super.initState(); } @@ -409,7 +257,7 @@ class PaintingEditorState extends State void setFill(bool fill) { _imageKey.currentState?.setFill(fill); setState(() {}); - widget.onUpdateUI?.call(); + onUpdateUI?.call(); } /// Toggles the fill mode. @@ -423,21 +271,21 @@ class PaintingEditorState extends State if (_imageKey.currentState != null) { _imageKey.currentState!.mode = mode; } - widget.onUpdateUI?.call(); + onUpdateUI?.call(); } /// Undoes the last action performed in the painting editor. void undoAction() { _imageKey.currentState!.undo(); setState(() {}); - widget.onUpdateUI?.call(); + onUpdateUI?.call(); } /// Redoes the previously undone action in the painting editor. void redoAction() { _imageKey.currentState!.redo(); setState(() {}); - widget.onUpdateUI?.call(); + onUpdateUI?.call(); } /// Closes the editor without applying changes. @@ -457,9 +305,8 @@ class PaintingEditorState extends State return AnnotatedRegion( value: imageEditorTheme.uiOverlayStyle, child: Theme( - data: widget.theme.copyWith( - tooltipTheme: - widget.theme.tooltipTheme.copyWith(preferBelow: true)), + data: theme.copyWith( + tooltipTheme: theme.tooltipTheme.copyWith(preferBelow: true)), child: LayoutBuilder(builder: (context, constraints) { return Scaffold( resizeToAvoidBottomInset: false, @@ -617,7 +464,7 @@ class PaintingEditorState extends State Widget _buildBody() { return SafeArea( child: Theme( - data: widget.theme, + data: theme, child: Material( color: Colors.transparent, textStyle: platformTextStyle(context, designMode), @@ -626,32 +473,27 @@ class PaintingEditorState extends State clipBehavior: Clip.none, children: [ TransformedContentGenerator( - configs: widget.transformConfigs ?? TransformConfigs.empty(), + configs: transformConfigs ?? TransformConfigs.empty(), child: ImageWithMultipleFilters( - width: widget.imageSize.width, - height: widget.imageSize.height, + width: initConfigs.imageSize.width, + height: initConfigs.imageSize.height, designMode: designMode, - image: EditorImage( - assetPath: widget.assetPath, - byteArray: widget.byteArray, - file: widget.file, - networkUrl: widget.networkUrl, - ), - filters: widget.filters, - blur: widget.blur, + image: editorImage, + filters: appliedFilters, + blurFactor: appliedBlurFactor, ), ), - if (widget.layers != null) + if (layers != null) LayerStack( - configs: widget.configs, - layers: widget.layers!, - paddingHelper: widget.paddingHelper, + configs: configs, + layers: layers!, + paddingHelper: initConfigs.paddingHelper, ), _buildPainter(), if (paintEditorConfigs.showColorPicker) _buildColorPicker(), if (imageEditorTheme.editorMode == ThemeEditorMode.whatsapp) ...[ WhatsAppPaintBottomBar( - configs: widget.configs, + configs: configs, strokeWidth: _imageKey.currentState?.strokeWidth ?? 0.0, onSetLineWidth: (val) { setState(() { @@ -660,7 +502,7 @@ class PaintingEditorState extends State }, ), WhatsAppPaintAppBar( - configs: widget.configs, + configs: configs, canUndo: canUndo, onDone: done, onTapUndo: undoAction, @@ -682,7 +524,7 @@ class PaintingEditorState extends State return customWidgets.bottomBarPaintingEditor ?? (imageEditorTheme.editorMode == ThemeEditorMode.simple ? Theme( - data: widget.theme, + data: theme, child: Scrollbar( controller: _bottomBarScrollCtrl, scrollbarOrientation: ScrollbarOrientation.top, @@ -727,7 +569,7 @@ class PaintingEditorState extends State onPressed: () { setMode(item.mode); setState(() {}); - widget.onUpdateUI?.call(); + onUpdateUI?.call(); }, ); }, @@ -749,21 +591,23 @@ class PaintingEditorState extends State Widget _buildPainter() { return PaintingCanvas.autoSource( key: _imageKey, - file: _editorImage.file, - networkUrl: _editorImage.networkUrl, - byteArray: _editorImage.byteArray, - assetPath: _editorImage.assetPath, - i18n: i18n, - icons: icons, - theme: widget.theme, - designMode: designMode, - imageSize: widget.imageSize, - imageEditorTheme: imageEditorTheme, - configs: paintEditorConfigs, - onUpdate: () { - setState(() {}); - widget.onUpdateUI?.call(); - }, + file: widget.editorImage.file, + networkUrl: widget.editorImage.networkUrl, + byteArray: widget.editorImage.byteArray, + assetPath: widget.editorImage.assetPath, + initConfigs: PaintCanvasInitConfigs( + i18n: i18n, + icons: icons, + theme: theme, + designMode: designMode, + imageSize: initConfigs.imageSize, + imageEditorTheme: imageEditorTheme, + configs: paintEditorConfigs, + onUpdate: () { + setState(() {}); + onUpdateUI?.call(); + }, + ), ); } @@ -774,7 +618,7 @@ class PaintingEditorState extends State top: imageEditorTheme.editorMode == ThemeEditorMode.simple ? 10 : 60, right: 0, child: BarColorPicker( - configs: widget.configs, + configs: configs, length: min( imageEditorTheme.editorMode == ThemeEditorMode.simple ? 350 : 200, MediaQuery.of(context).size.height - diff --git a/lib/modules/paint_editor/painting_canvas.dart b/lib/modules/paint_editor/painting_canvas.dart index 1c6dc9c9..6291f589 100644 --- a/lib/modules/paint_editor/painting_canvas.dart +++ b/lib/modules/paint_editor/painting_canvas.dart @@ -3,386 +3,97 @@ import 'dart:math'; import 'package:flutter/material.dart' hide Image, decodeImageFromList; import 'package:flutter/services.dart'; +import 'package:pro_image_editor/pro_image_editor.dart'; -import '../../models/editor_configs/paint_editor_configs.dart'; +import '../../models/editor_image.dart'; +import '../../models/init_configs/paint_canvas_init_configs.dart'; import '../../models/layer.dart'; import '../../models/paint_editor/painted_model.dart'; -import '../../models/theme/theme.dart'; -import '../../models/i18n/i18n.dart'; -import '../../models/icons/icons.dart'; -import '../../utils/design_mode.dart'; import '../../utils/theme_functions.dart'; import '../../widgets/bottom_sheets_header_row.dart'; import 'utils/draw/draw_multiple_canvas.dart'; import 'utils/paint_controller.dart'; -import 'utils/paint_editor_enum.dart'; /// A widget for creating a canvas for painting on images. /// -/// This widget allows you to create a canvas for painting on images loaded from various sources, including network URLs, asset paths, files, or memory (Uint8List). It provides customization options for appearance and behavior. +/// This widget allows you to create a canvas for painting on images loaded from various sources, including network URLs, asset paths, files, or memory (Uint8List). +/// It provides customization options for appearance and behavior. class PaintingCanvas extends StatefulWidget { - /// The network URL of the image (if loading from a network). - final String? networkUrl; + /// The initialization configurations for the canvas. + final PaintCanvasInitConfigs initConfigs; - /// A byte array representing the image data (if loading from memory). - final Uint8List? byteArray; + /// The image being edited on the canvas. + final EditorImage editorImage; - /// The file representing the image (if loading from a file). - final File? file; - - /// The asset path of the image (if loading from assets). - final String? assetPath; - - /// The theme configuration for the editor. - final ThemeData theme; - - /// The design mode of the editor (material or custom). - final ImageEditorDesignModeE designMode; - - /// The internationalization (i18n) configuration for the editor. - final I18n i18n; - - /// Icons used in the editor. - final ImageEditorIcons icons; - - /// The theme configuration specific to the image editor. - final ImageEditorTheme imageEditorTheme; - - /// A callback function called when the painting is updated. - final VoidCallback? onUpdate; - - /// The configuration options for the paint editor. - final PaintEditorConfigs configs; - - /// The size of the image. - final Size imageSize; - - /// Creates a [PaintingCanvas] widget. - /// - /// This constructor is not intended to be used directly. Instead, use one of the factory methods to create an instance of [PaintingCanvas] based on the image source (network, asset, file, or memory). + /// Constructs a `PaintingCanvas` widget. /// - /// See factory methods like [PaintingCanvas.network], [PaintingCanvas.asset], [PaintingCanvas.file], and [PaintingCanvas.memory] for creating instances of this widget with specific image sources. + /// The [key] parameter is used to provide a key for the widget. + /// The [editorImage] parameter specifies the image to be edited on the canvas. + /// The [initConfigs] parameter specifies the initialization configurations for the canvas. const PaintingCanvas._({ super.key, - this.assetPath, - this.networkUrl, - this.byteArray, - this.file, - required this.imageSize, - required this.theme, - required this.designMode, - required this.i18n, - required this.imageEditorTheme, - required this.icons, - required this.configs, - this.onUpdate, + required this.editorImage, + required this.initConfigs, }); - /// Create a [PaintingCanvas] widget with an image loaded from a network URL. - /// - /// Use this factory method to create a [PaintingCanvas] widget for painting on an image loaded from a network URL. Customize the appearance and behavior of the widget using the provided parameters. - /// - /// Parameters: - /// - `networkUrl`: The network URL of the image (required). - /// - `key`: A required Key to uniquely identify this widget in the widget tree. - /// - `imageSize`: The size of the image (required). - /// - `theme`: The theme configuration for the editor (required). - /// - `designMode`: The design mode of the editor (material or custom, required). - /// - `i18n`: The internationalization (i18n) configuration for the editor (required). - /// - `imageEditorTheme`: The theme configuration specific to the image editor (required). - /// - `icons`: Icons used in the editor (required). - /// - `configs`: The configuration options for the paint editor (required). - /// - `onUpdate`: A callback function called when the painting is updated. - /// - /// Returns: - /// A [PaintingCanvas] widget configured with the provided parameters and the image loaded from the network URL. - /// - /// Example Usage: - /// ```dart - /// final String imageUrl = 'https://example.com/image.jpg'; // Provide the network URL. - /// final painter = PaintingCanvas.network( - /// imageUrl, - /// key: GlobalKey(), - /// theme: ThemeData.light(), - /// i18n: I18n(), - /// imageEditorTheme: ImageEditorTheme(), - /// icons: ImageEditorIcons(), - /// designMode: ImageEditorDesignMode.material, - /// ); - /// ``` + /// Constructs a `PaintingCanvas` widget with an image loaded from a network URL. factory PaintingCanvas.network( String networkUrl, { required Key key, - Widget? placeholderWidget, - bool? scalable, - bool? showColorPicker, - List? colors, - Function? save, - VoidCallback? onUpdate, - Widget? undoIcon, - Widget? colorIcon, - required Size imageSize, - required ThemeData theme, - required I18n i18n, - required ImageEditorTheme imageEditorTheme, - required ImageEditorIcons icons, - required ImageEditorDesignModeE designMode, - required PaintEditorConfigs configs, + required PaintCanvasInitConfigs initConfigs, }) { return PaintingCanvas._( key: key, - networkUrl: networkUrl, - onUpdate: onUpdate, - configs: configs, - theme: theme, - imageSize: imageSize, - i18n: i18n, - imageEditorTheme: imageEditorTheme, - icons: icons, - designMode: designMode, + editorImage: EditorImage(networkUrl: networkUrl), + initConfigs: initConfigs, ); } - /// Create a [PaintingCanvas] widget with an image loaded from an asset. - /// - /// Use this factory method to create a [PaintingCanvas] widget for painting on an image loaded from an asset. Customize the appearance and behavior of the widget using the provided parameters. - /// - /// Parameters: - /// - `path`: The asset path of the image (required). - /// - `key`: A required Key to uniquely identify this widget in the widget tree. - /// - `imageSize`: The size of the image (required). - /// - `theme`: The theme configuration for the editor (required). - /// - `designMode`: The design mode of the editor (material or custom, required). - /// - `i18n`: The internationalization (i18n) configuration for the editor (required). - /// - `imageEditorTheme`: The theme configuration specific to the image editor (required). - /// - `icons`: Icons used in the editor (required). - /// - `configs`: The configuration options for the paint editor (required). - /// - `onUpdate`: A callback function called when the painting is updated. - /// - /// Returns: - /// A [PaintingCanvas] widget configured with the provided parameters and the image loaded from the asset. - /// - /// Example Usage: - /// ```dart - /// final String assetPath = 'assets/image.png'; // Provide the asset path. - /// final painter = PaintingCanvas.asset( - /// assetPath, - /// key: GlobalKey(), - /// theme: ThemeData.light(), - /// i18n: I18n(), - /// imageEditorTheme: ImageEditorTheme(), - /// icons: ImageEditorIcons(), - /// designMode: ImageEditorDesignMode.material, - /// ); - /// ``` + /// Constructs a `PaintingCanvas` widget with an image loaded from an asset path. factory PaintingCanvas.asset( String path, { required Key key, - bool? scalable, - bool? showColorPicker, - Widget? placeholderWidget, - Function? save, - List? colors, - VoidCallback? onUpdate, - Widget? undoIcon, - Widget? colorIcon, - required Size imageSize, - required ThemeData theme, - required I18n i18n, - required ImageEditorTheme imageEditorTheme, - required ImageEditorIcons icons, - required ImageEditorDesignModeE designMode, - required PaintEditorConfigs configs, + required PaintCanvasInitConfigs initConfigs, }) { return PaintingCanvas._( key: key, - assetPath: path, - onUpdate: onUpdate, - configs: configs, - theme: theme, - imageSize: imageSize, - i18n: i18n, - imageEditorTheme: imageEditorTheme, - icons: icons, - designMode: designMode, + editorImage: EditorImage(assetPath: path), + initConfigs: initConfigs, ); } - /// Create a [PaintingCanvas] widget with an image loaded from a File. - /// - /// Use this factory method to create a [PaintingCanvas] widget for painting on an image loaded from a File. Customize the appearance and behavior of the widget using the provided parameters. - /// - /// Parameters: - /// - `file`: The File object representing the image file (required). - /// - `key`: A required Key to uniquely identify this widget in the widget tree. - /// - `imageSize`: The size of the image (required). - /// - `theme`: The theme configuration for the editor (required). - /// - `designMode`: The design mode of the editor (material or custom, required). - /// - `i18n`: The internationalization (i18n) configuration for the editor (required). - /// - `imageEditorTheme`: The theme configuration specific to the image editor (required). - /// - `icons`: Icons used in the editor (required). - /// - `configs`: The configuration options for the paint editor (required). - /// - `onUpdate`: A callback function called when the painting is updated. - /// - /// Returns: - /// A [PaintingCanvas] widget configured with the provided parameters and the image loaded from the File. - /// - /// Example Usage: - /// ```dart - /// final File imageFile = File('path/to/image.jpg'); // Provide the image File. - /// final painter = PaintingCanvas.file( - /// imageFile, - /// key: GlobalKey(), - /// theme: ThemeData.light(), - /// i18n: I18n(), - /// imageEditorTheme: ImageEditorTheme(), - /// icons: ImageEditorIcons(), - /// designMode: ImageEditorDesignMode.material, - /// ); - /// ``` + /// Constructs a `PaintingCanvas` widget with an image loaded from a file. factory PaintingCanvas.file( File file, { required Key key, - Function? save, - bool? scalable, - bool? showColorPicker, - Widget? placeholderWidget, - List? colors, - VoidCallback? onUpdate, - Widget? undoIcon, - Widget? colorIcon, - required Size imageSize, - required ThemeData theme, - required I18n i18n, - required ImageEditorTheme imageEditorTheme, - required ImageEditorIcons icons, - required ImageEditorDesignModeE designMode, - required PaintEditorConfigs configs, + required PaintCanvasInitConfigs initConfigs, }) { return PaintingCanvas._( key: key, - file: file, - onUpdate: onUpdate, - configs: configs, - theme: theme, - imageSize: imageSize, - i18n: i18n, - imageEditorTheme: imageEditorTheme, - icons: icons, - designMode: designMode, + editorImage: EditorImage(file: file), + initConfigs: initConfigs, ); } - /// Create a [PaintingCanvas] widget with an image loaded from a Uint8List (memory). - /// - /// Use this factory method to create a [PaintingCanvas] widget for painting on an image loaded from a Uint8List (memory). Customize the appearance and behavior of the widget using the provided parameters. - /// - /// Parameters: - /// - `byteArray`: The Uint8List representing the image data loaded in memory (required). - /// - `key`: A required Key to uniquely identify this widget in the widget tree. - /// - `imageSize`: The size of the image (required). - /// - `theme`: The theme configuration for the editor (required). - /// - `designMode`: The design mode of the editor (material or custom, required). - /// - `i18n`: The internationalization (i18n) configuration for the editor (required). - /// - `imageEditorTheme`: The theme configuration specific to the image editor (required). - /// - `icons`: Icons used in the editor (required). - /// - `configs`: The configuration options for the paint editor (required). - /// - `onUpdate`: A callback function called when the painting is updated. - /// - /// Returns: - /// A [PaintingCanvas] widget configured with the provided parameters and the image loaded from the Uint8List (memory). - /// - /// Example Usage: - /// ```dart - /// final Uint8List imageBytes = Uint8List( /* Image byte data */ ); // Provide the image byte data. - /// final painter = PaintingCanvas.memory( - /// imageBytes, - /// key: GlobalKey(), - /// theme: ThemeData.light(), - /// i18n: I18n(), - /// imageEditorTheme: ImageEditorTheme(), - /// icons: ImageEditorIcons(), - /// designMode: ImageEditorDesignMode.material, - /// ); - /// ``` + /// Constructs a `PaintingCanvas` widget with an image loaded into memory. factory PaintingCanvas.memory( Uint8List byteArray, { required Key key, - bool? scalable, - bool? showColorPicker, - Function? save, - Widget? placeholderWidget, - List? colors, - VoidCallback? onUpdate, - Widget? undoIcon, - Widget? colorIcon, - required Size imageSize, - required ThemeData theme, - required I18n i18n, - required ImageEditorTheme imageEditorTheme, - required ImageEditorIcons icons, - required ImageEditorDesignModeE designMode, - required PaintEditorConfigs configs, + required PaintCanvasInitConfigs initConfigs, }) { return PaintingCanvas._( key: key, - byteArray: byteArray, - onUpdate: onUpdate, - configs: configs, - theme: theme, - imageSize: imageSize, - i18n: i18n, - imageEditorTheme: imageEditorTheme, - icons: icons, - designMode: designMode, + editorImage: EditorImage(byteArray: byteArray), + initConfigs: initConfigs, ); } - /// Create a `PaintingCanvas` widget with automatic image source detection. - /// - /// This factory method allows you to create a `PaintingCanvas` widget with automatic detection of the image source, including options for loading from a Uint8List (memory), File, asset path, or network URL. The provided parameters allow you to customize the appearance and behavior of the `PaintingCanvas` widget. - /// - /// Parameters: - /// - `key`: A required Key to uniquely identify this widget in the widget tree. - /// - `onUpdate`: An optional callback function triggered when the painting is updated. - /// - `imageSize`: The size of the image to be loaded (required). - /// - `theme`: A `ThemeData` object that defines the visual styling of the `PaintingCanvas` widget (required). - /// - `i18n`: An `I18n` object for localization and internationalization (required). - /// - `imageEditorTheme`: An `ImageEditorTheme` object for customizing the overall theme of the editor (required). - /// - `icons`: An `ImageEditorIcons` object for customizing the icons used in the editor (required). - /// - `designMode`: An `ImageEditorDesignMode` enum to specify the design mode (material or custom) of the ImageEditor (required). - /// - `byteArray`: A Uint8List representing the image data loaded in memory (optional). - /// - `file`: A File object representing the image file to be loaded (optional). - /// - `assetPath`: A String specifying the asset path for the image (optional). - /// - `networkUrl`: A String specifying the network URL for the image (optional). - /// - /// Returns: - /// A `PaintingCanvas` widget configured with the provided parameters and the image loaded from the detected source (Uint8List, File, asset, or network URL). + /// Constructs a `PaintingCanvas` widget with an image loaded automatically based on the provided source. /// - /// Example Usage: - /// ```dart - /// final Uint8List imageBytes = Uint8List( /* Image byte data */ ); // Provide the image byte data. - /// final painter = PaintingCanvas.autoSource( - /// key: GlobalKey(), - /// theme: ThemeData.light(), - /// i18n: I18n(), - /// imageEditorTheme: ImageEditorTheme(), - /// icons: ImageEditorIcons(), - /// designMode: ImageEditorDesignMode.material, - /// byteArray: imageBytes, - /// ); - /// // Alternatively, provide a 'file', 'assetPath', or 'networkUrl' instead of 'byteArray' for automatic source detection. - /// ``` + /// Either [byteArray], [file], [networkUrl], or [assetPath] must be provided. factory PaintingCanvas.autoSource({ required Key key, - VoidCallback? onUpdate, - required Size imageSize, - required ThemeData theme, - required I18n i18n, - required ImageEditorTheme imageEditorTheme, - required ImageEditorIcons icons, - required ImageEditorDesignModeE designMode, - required PaintEditorConfigs configs, + required PaintCanvasInitConfigs initConfigs, Uint8List? byteArray, File? file, String? assetPath, @@ -392,53 +103,25 @@ class PaintingCanvas extends StatefulWidget { return PaintingCanvas.memory( byteArray, key: key, - onUpdate: onUpdate, - configs: configs, - theme: theme, - imageSize: imageSize, - i18n: i18n, - imageEditorTheme: imageEditorTheme, - icons: icons, - designMode: designMode, + initConfigs: initConfigs, ); } else if (file != null) { return PaintingCanvas.file( file, key: key, - onUpdate: onUpdate, - configs: configs, - theme: theme, - imageSize: imageSize, - i18n: i18n, - imageEditorTheme: imageEditorTheme, - icons: icons, - designMode: designMode, + initConfigs: initConfigs, ); } else if (networkUrl != null) { return PaintingCanvas.network( networkUrl, key: key, - onUpdate: onUpdate, - configs: configs, - theme: theme, - imageSize: imageSize, - i18n: i18n, - imageEditorTheme: imageEditorTheme, - icons: icons, - designMode: designMode, + initConfigs: initConfigs, ); } else if (assetPath != null) { return PaintingCanvas.asset( assetPath, key: key, - onUpdate: onUpdate, - configs: configs, - theme: theme, - imageSize: imageSize, - i18n: i18n, - imageEditorTheme: imageEditorTheme, - icons: icons, - designMode: designMode, + initConfigs: initConfigs, ); } else { throw ArgumentError( @@ -453,16 +136,18 @@ class PaintingCanvas extends StatefulWidget { class PaintingCanvasState extends State { late final PaintingController _paintCtrl; + PaintEditorConfigs get configs => widget.initConfigs.configs; + @override void initState() { - var w = widget.imageSize.width; - var h = widget.imageSize.height; + var w = widget.initConfigs.imageSize.width; + var h = widget.initConfigs.imageSize.height; _paintCtrl = PaintingController( - fill: widget.configs.initialFill, - mode: widget.configs.initialPaintMode, - strokeWidth: widget.configs.initialStrokeWidth, - color: widget.configs.initialColor, + fill: configs.initialFill, + mode: configs.initialPaintMode, + strokeWidth: configs.initialStrokeWidth, + color: configs.initialColor, strokeMultiplier: (w + h) > 1440 ? (w + h) ~/ 1440 : 1, ); @@ -580,15 +265,15 @@ class PaintingCanvasState extends State { /// This method adds a [PaintedModel] object to the paint history and calls the [onUpdate] callback if provided. void _addPaintHistory(PaintedModel info) { _paintCtrl.addPaintInfo(info); - widget.onUpdate?.call(); + widget.initConfigs.onUpdate?.call(); } /// Set the stroke width. void setStrokeWidth(double value) { _paintCtrl.setStrokeWidth(value); setState(() {}); - if (widget.configs.strokeWidthOnChanged != null) { - widget.configs.strokeWidthOnChanged!(value); + if (configs.strokeWidthOnChanged != null) { + configs.strokeWidthOnChanged!(value); } } @@ -598,40 +283,44 @@ class PaintingCanvasState extends State { void showRangeSlider() { showModalBottomSheet( context: context, - backgroundColor: - widget.imageEditorTheme.paintingEditor.lineWidthBottomSheetColor, + backgroundColor: widget.initConfigs.imageEditorTheme.paintingEditor + .lineWidthBottomSheetColor, builder: (BuildContext context) { - return Material( - color: Colors.transparent, - textStyle: platformTextStyle(context, widget.designMode), - child: SingleChildScrollView( - physics: const ClampingScrollPhysics(), - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - BottomSheetHeaderRow( - title: widget.i18n.paintEditor.lineWidth, - theme: widget.theme, - ), - StatefulBuilder(builder: (context, setState) { - return Slider.adaptive( - max: 40, - min: 2, - divisions: 19, - value: _paintCtrl.strokeWidth, - onChanged: (value) { - setStrokeWidth(value); - }, - ); - }), - ], + return StatefulBuilder(builder: (context, setState) { + return Material( + color: Colors.transparent, + textStyle: + platformTextStyle(context, widget.initConfigs.designMode), + child: SingleChildScrollView( + physics: const ClampingScrollPhysics(), + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + BottomSheetHeaderRow( + title: widget.initConfigs.i18n.paintEditor.lineWidth, + theme: widget.initConfigs.theme, + ), + StatefulBuilder(builder: (context, setState) { + return Slider.adaptive( + max: 40, + min: 2, + divisions: 19, + value: _paintCtrl.strokeWidth, + onChanged: (value) { + setStrokeWidth(value); + setState(() {}); + }, + ); + }), + ], + ), ), ), - ), - ); + ); + }); }, ); } @@ -773,7 +462,7 @@ class PaintingCanvasState extends State { onScaleUpdate: _onScaleUpdate, onScaleEnd: _onScaleEnd, child: CustomPaint( - size: widget.imageSize, + size: widget.initConfigs.imageSize, willChange: true, isComplex: true, painter: DrawImage(paintCtrl: _paintCtrl), diff --git a/lib/modules/sticker_editor.dart b/lib/modules/sticker_editor.dart index d85fecdc..bc34ba76 100644 --- a/lib/modules/sticker_editor.dart +++ b/lib/modules/sticker_editor.dart @@ -2,13 +2,14 @@ import 'package:flutter/material.dart'; import 'package:pro_image_editor/pro_image_editor.dart'; import '../models/layer.dart'; -import '../utils/helper/editor_mixin.dart'; +import '../mixins/converted_configs.dart'; +import '../mixins/editor_configs_mixin.dart'; /// The `StickerEditor` class is responsible for creating a widget that allows users to select emojis. /// /// This widget provides an EmojiPicker that allows users to choose emojis, which are then returned /// as `EmojiLayerData` containing the selected emoji text. -class StickerEditor extends StatefulWidget with ImageEditorMixin { +class StickerEditor extends StatefulWidget with SimpleConfigsAccess { @override final ProImageEditorConfigs configs; @@ -23,7 +24,8 @@ class StickerEditor extends StatefulWidget with ImageEditorMixin { } /// The state class for the `StickerEditor` widget. -class StickerEditorState extends State { +class StickerEditorState extends State + with ImageEditorConvertedConfigs, SimpleConfigsAccessState { /// Closes the editor without applying changes. void close() { Navigator.pop(context); diff --git a/lib/modules/text_editor.dart b/lib/modules/text_editor.dart index ccd69606..dd95a115 100644 --- a/lib/modules/text_editor.dart +++ b/lib/modules/text_editor.dart @@ -5,11 +5,12 @@ import 'package:pro_image_editor/designs/whatsapp/whatsapp_text_appbar.dart'; import 'package:pro_image_editor/models/editor_configs/pro_image_editor_configs.dart'; import 'package:pro_image_editor/models/theme/theme.dart'; import 'package:pro_image_editor/utils/design_mode.dart'; +import 'package:pro_image_editor/mixins/converted_configs.dart'; import 'package:rounded_background_text/rounded_background_text.dart'; import '../designs/whatsapp/whatsapp_text_bottombar.dart'; import '../models/layer.dart'; -import '../utils/helper/editor_mixin.dart'; +import '../mixins/editor_configs_mixin.dart'; import '../utils/theme_functions.dart'; import '../widgets/bottom_sheets_header_row.dart'; import '../widgets/color_picker/bar_color_picker.dart'; @@ -19,7 +20,7 @@ import '../widgets/platform_popup_menu.dart'; import '../widgets/pro_image_editor_desktop_mode.dart'; /// A StatefulWidget that provides a text editing interface for adding and editing text layers. -class TextEditor extends StatefulWidget with ImageEditorMixin { +class TextEditor extends StatefulWidget with SimpleConfigsAccess { @override final ProImageEditorConfigs configs; @@ -52,7 +53,8 @@ class TextEditor extends StatefulWidget with ImageEditorMixin { } /// The state class for the `TextEditor` widget. -class TextEditorState extends State with ImageEditorStateMixin { +class TextEditorState extends State + with ImageEditorConvertedConfigs, SimpleConfigsAccessState { final TextEditingController _textCtrl = TextEditingController(); final FocusNode _focus = FocusNode(); Color _primaryColor = Colors.black; diff --git a/lib/pro_image_editor.dart b/lib/pro_image_editor.dart index 80aad779..ad52d2a5 100644 --- a/lib/pro_image_editor.dart +++ b/lib/pro_image_editor.dart @@ -1,13 +1,13 @@ library pro_image_editor; -export 'pro_image_editor_main.dart' hide ImageEditingCompleteCallback; +export 'modules/main_editor/main_editor.dart'; export 'package:pro_image_editor/utils/converters.dart'; export 'package:pro_image_editor/models/i18n/i18n.dart'; export 'package:pro_image_editor/models/icons/icons.dart'; export 'package:pro_image_editor/models/theme/theme.dart'; -export 'package:pro_image_editor/models/helper_lines.dart'; +export 'package:pro_image_editor/models/editor_configs/helper_lines_configs.dart'; export 'package:pro_image_editor/models/custom_widgets.dart'; export 'package:pro_image_editor/models/editor_configs/pro_image_editor_configs.dart'; export 'package:pro_image_editor/models/editor_configs/paint_editor_configs.dart'; @@ -18,6 +18,10 @@ export 'package:pro_image_editor/models/editor_configs/emoji_editor_configs.dart export 'package:pro_image_editor/models/editor_configs/sticker_editor_configs.dart'; export 'package:pro_image_editor/models/editor_configs/blur_editor_configs.dart'; +export 'package:pro_image_editor/models/init_configs/paint_editor_init_configs.dart'; +export 'package:pro_image_editor/models/init_configs/filter_editor_init_configs.dart'; +export 'package:pro_image_editor/models/init_configs/blur_editor_init_configs.dart'; + export 'package:pro_image_editor/models/import_export/export_state_history_configs.dart'; export 'package:pro_image_editor/models/import_export/import_state_history.dart'; export 'package:pro_image_editor/models/import_export/import_state_history_configs.dart'; diff --git a/lib/utils/helper/editor_mixin.dart b/lib/utils/helper/editor_mixin.dart deleted file mode 100644 index f0730a38..00000000 --- a/lib/utils/helper/editor_mixin.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:pro_image_editor/pro_image_editor.dart'; - -mixin ImageEditorMixin on StatefulWidget { - /// Configuration options for the editor. - ProImageEditorConfigs get configs; -} - -mixin ImageEditorStateMixin on State { - ImageEditorMixin get _widget => (widget as ImageEditorMixin); - - ProImageEditorConfigs get configs => _widget.configs; - - PaintEditorConfigs get paintEditorConfigs => configs.paintEditorConfigs; - TextEditorConfigs get textEditorConfigs => configs.textEditorConfigs; - CropRotateEditorConfigs get cropRotateEditorConfigs => - configs.cropRotateEditorConfigs; - FilterEditorConfigs get filterEditorConfigs => configs.filterEditorConfigs; - BlurEditorConfigs get blurEditorConfigs => configs.blurEditorConfigs; - EmojiEditorConfigs get emojiEditorConfigs => configs.emojiEditorConfigs; - StickerEditorConfigs? get stickerEditorConfigs => - configs.stickerEditorConfigs; - - ImageEditorDesignModeE get designMode => configs.designMode; - ImageEditorTheme get imageEditorTheme => configs.imageEditorTheme; - ImageEditorCustomWidgets get customWidgets => configs.customWidgets; - ImageEditorIcons get icons => configs.icons; - I18n get i18n => configs.i18n; - ImportStateHistory? get initStateHistory => configs.initStateHistory; - HelperLines get helperLines => configs.helperLines; - String get heroTag => configs.heroTag; -} diff --git a/lib/utils/image_helpers.dart b/lib/utils/image_helpers.dart new file mode 100644 index 00000000..69a49311 --- /dev/null +++ b/lib/utils/image_helpers.dart @@ -0,0 +1,37 @@ +import 'dart:typed_data'; +import 'package:image/image.dart' as img; + +/// Function to remove transparent areas from the image +Uint8List? removeTransparentImgAreas(Uint8List bytes) { + // Decode the image + img.Image? image = img.decodeImage(bytes); + if (image == null) return null; + + // Determine the bounding box of non-transparent pixels + int minX = image.width, minY = image.height, maxX = 0, maxY = 0; + for (int y = 0; y < image.height; y++) { + for (int x = 0; x < image.width; x++) { + // Extract the alpha component to check for transparency + if (image.getPixel(x, y).a != 0) { + // Check if pixel is not fully transparent + if (x < minX) minX = x; + if (y < minY) minY = y; + if (x > maxX) maxX = x; + if (y > maxY) maxY = y; + } + } + } + + // Check if there are any non-transparent pixels + if (maxX < minX || maxY < minY) { + return Uint8List.fromList(img.encodePng(image)); + } + + // Crop the image to the bounding box + img.Image croppedImage = img.copyCrop(image, + x: minX, y: minY, width: maxX - minX + 1, height: maxY - minY + 1); + + // Encode the cropped image to bytes + Uint8List croppedBytes = Uint8List.fromList(img.encodePng(croppedImage)); + return croppedBytes; +} diff --git a/lib/widgets/layer_stack.dart b/lib/widgets/layer_stack.dart index 5f4df68c..ea4e251b 100644 --- a/lib/widgets/layer_stack.dart +++ b/lib/widgets/layer_stack.dart @@ -34,7 +34,6 @@ class LayerStack extends StatefulWidget { class _LayerStackState extends State { @override Widget build(BuildContext context) { - // TODO: fix animation bug => layers on main page have full screen size and transform after that down return IgnorePointer( child: Transform.translate( offset: widget.transformHelper.offset, diff --git a/pubspec.yaml b/pubspec.yaml index 10320fb1..219420e4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: pro_image_editor description: "A Flutter image editor: Seamlessly enhance your images with user-friendly editing features." -version: 2.6.6 +version: 2.6.7 homepage: https://github.com/hm21/pro_image_editor/ repository: https://github.com/hm21/pro_image_editor/ issue_tracker: https://github.com/hm21/pro_image_editor/issues/ diff --git a/test/modules/blur_editor_test.dart b/test/modules/blur_editor_test.dart index 6d2b1f5a..ecfe4b41 100644 --- a/test/modules/blur_editor_test.dart +++ b/test/modules/blur_editor_test.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:pro_image_editor/models/init_configs/blur_editor_init_configs.dart'; import 'package:pro_image_editor/modules/blur_editor.dart'; import '../fake/fake_image.dart'; @@ -13,8 +14,10 @@ void main() { home: Scaffold( body: BlurEditor.memory( fakeMemoryImage, - theme: ThemeData.light(), - imageSize: const Size(200, 200), + initConfigs: BlurEditorInitConfigs( + theme: ThemeData.light(), + imageSize: const Size(200, 200), + ), ), ), ), diff --git a/test/modules/filter_editor_test.dart b/test/modules/filter_editor_test.dart index aa56b42d..85d67383 100644 --- a/test/modules/filter_editor_test.dart +++ b/test/modules/filter_editor_test.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:pro_image_editor/models/init_configs/filter_editor_init_configs.dart'; import 'package:pro_image_editor/modules/filter_editor/filter_editor.dart'; import 'package:pro_image_editor/modules/filter_editor/widgets/image_with_filter.dart'; @@ -14,7 +15,9 @@ void main() { home: Scaffold( body: FilterEditor.memory( fakeMemoryImage, - theme: ThemeData.light(), + initConfigs: FilterEditorInitConfigs( + theme: ThemeData.light(), + ), ), ), ), @@ -30,7 +33,9 @@ void main() { home: Scaffold( body: FilterEditor.memory( fakeMemoryImage, - theme: ThemeData.light(), + initConfigs: FilterEditorInitConfigs( + theme: ThemeData.light(), + ), ), ), ), diff --git a/test/modules/paint_editor/paint_editor_test.dart b/test/modules/paint_editor/paint_editor_test.dart index e5a3973a..160a78a0 100644 --- a/test/modules/paint_editor/paint_editor_test.dart +++ b/test/modules/paint_editor/paint_editor_test.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:network_image_mock/network_image_mock.dart'; +import 'package:pro_image_editor/models/init_configs/paint_editor_init_configs.dart'; import 'package:pro_image_editor/modules/paint_editor/paint_editor.dart'; import 'package:pro_image_editor/modules/paint_editor/painting_canvas.dart'; import 'package:pro_image_editor/widgets/color_picker/bar_color_picker.dart'; @@ -14,8 +15,10 @@ void main() { await tester.pumpWidget(MaterialApp( home: PaintingEditor.memory( fakeMemoryImage, - theme: ThemeData(), - imageSize: const Size(200, 200), + initConfigs: PaintEditorInitConfigs( + theme: ThemeData(), + imageSize: const Size(200, 200), + ), ), )); @@ -27,8 +30,10 @@ void main() { await tester.pumpWidget(MaterialApp( home: PaintingEditor.network( fakeNetworkImage, - theme: ThemeData(), - imageSize: const Size(200, 200), + initConfigs: PaintEditorInitConfigs( + theme: ThemeData(), + imageSize: const Size(200, 200), + ), ), )); @@ -40,8 +45,10 @@ void main() { await tester.pumpWidget(MaterialApp( home: PaintingEditor.file( fakeFileImage, - theme: ThemeData(), - imageSize: const Size(200, 200), + initConfigs: PaintEditorInitConfigs( + theme: ThemeData(), + imageSize: const Size(200, 200), + ), ), )); @@ -52,8 +59,10 @@ void main() { await tester.pumpWidget(MaterialApp( home: PaintingEditor.memory( fakeMemoryImage, - theme: ThemeData(), - imageSize: const Size(200, 200), + initConfigs: PaintEditorInitConfigs( + theme: ThemeData(), + imageSize: const Size(200, 200), + ), ), )); @@ -63,8 +72,10 @@ void main() { await tester.pumpWidget(MaterialApp( home: PaintingEditor.memory( fakeMemoryImage, - theme: ThemeData(), - imageSize: const Size(200, 200), + initConfigs: PaintEditorInitConfigs( + theme: ThemeData(), + imageSize: const Size(200, 200), + ), ), )); diff --git a/test/modules/paint_editor/painting_canvas_test.dart b/test/modules/paint_editor/painting_canvas_test.dart index a848bba3..47251c11 100644 --- a/test/modules/paint_editor/painting_canvas_test.dart +++ b/test/modules/paint_editor/painting_canvas_test.dart @@ -4,6 +4,7 @@ import 'package:network_image_mock/network_image_mock.dart'; import 'package:pro_image_editor/models/editor_configs/paint_editor_configs.dart'; import 'package:pro_image_editor/models/i18n/i18n.dart'; import 'package:pro_image_editor/models/icons/icons.dart'; +import 'package:pro_image_editor/models/init_configs/paint_canvas_init_configs.dart'; import 'package:pro_image_editor/models/layer.dart'; import 'package:pro_image_editor/models/theme/theme.dart'; import 'package:pro_image_editor/modules/paint_editor/painting_canvas.dart'; @@ -14,19 +15,22 @@ import '../../fake/fake_image.dart'; void main() { group('PaintingCanvas Tests', () { + PaintCanvasInitConfigs initConfigs = PaintCanvasInitConfigs( + theme: ThemeData(), + imageSize: const Size(200, 200), + i18n: const I18n(), + imageEditorTheme: const ImageEditorTheme(), + icons: const ImageEditorIcons(), + designMode: ImageEditorDesignModeE.material, + configs: const PaintEditorConfigs(), + ); testWidgets('Initializes with memory constructor', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: PaintingCanvas.memory( fakeMemoryImage, key: GlobalKey(), - theme: ThemeData(), - imageSize: const Size(200, 200), - i18n: const I18n(), - imageEditorTheme: const ImageEditorTheme(), - icons: const ImageEditorIcons(), - designMode: ImageEditorDesignModeE.material, - configs: const PaintEditorConfigs(), + initConfigs: initConfigs, ), )); @@ -39,13 +43,7 @@ void main() { home: PaintingCanvas.network( fakeNetworkImage, key: GlobalKey(), - theme: ThemeData(), - imageSize: const Size(200, 200), - i18n: const I18n(), - imageEditorTheme: const ImageEditorTheme(), - icons: const ImageEditorIcons(), - designMode: ImageEditorDesignModeE.material, - configs: const PaintEditorConfigs(), + initConfigs: initConfigs, ), )); @@ -58,13 +56,7 @@ void main() { home: PaintingCanvas.file( fakeFileImage, key: GlobalKey(), - theme: ThemeData(), - imageSize: const Size(200, 200), - i18n: const I18n(), - imageEditorTheme: const ImageEditorTheme(), - icons: const ImageEditorIcons(), - designMode: ImageEditorDesignModeE.material, - configs: const PaintEditorConfigs(), + initConfigs: initConfigs, ), )); @@ -77,13 +69,7 @@ void main() { home: PaintingCanvas.memory( fakeMemoryImage, key: GlobalKey(), - imageSize: const Size(200, 200), - theme: ThemeData.light(), - i18n: const I18n(), - imageEditorTheme: const ImageEditorTheme(), - icons: const ImageEditorIcons(), - designMode: ImageEditorDesignModeE.material, - configs: const PaintEditorConfigs(), + initConfigs: initConfigs, ), )); @@ -114,13 +100,7 @@ void main() { home: PaintingCanvas.memory( fakeMemoryImage, key: GlobalKey(), - imageSize: const Size(200, 200), - theme: ThemeData.light(), - i18n: const I18n(), - imageEditorTheme: const ImageEditorTheme(), - icons: const ImageEditorIcons(), - designMode: ImageEditorDesignModeE.material, - configs: const PaintEditorConfigs(), + initConfigs: initConfigs, ), )); @@ -163,13 +143,7 @@ void main() { home: PaintingCanvas.memory( fakeMemoryImage, key: GlobalKey(), - imageSize: const Size(200, 200), - theme: ThemeData.light(), - i18n: const I18n(), - imageEditorTheme: const ImageEditorTheme(), - icons: const ImageEditorIcons(), - designMode: ImageEditorDesignModeE.material, - configs: const PaintEditorConfigs(), + initConfigs: initConfigs, ), )); diff --git a/test/pro_image_editor_test.dart b/test/pro_image_editor_test.dart index 717f84c9..73240631 100644 --- a/test/pro_image_editor_test.dart +++ b/test/pro_image_editor_test.dart @@ -8,7 +8,7 @@ import 'package:pro_image_editor/modules/filter_editor/filter_editor.dart'; import 'package:pro_image_editor/modules/paint_editor/paint_editor.dart'; import 'package:pro_image_editor/modules/text_editor.dart'; -import 'package:pro_image_editor/pro_image_editor_main.dart'; +import 'package:pro_image_editor/modules/main_editor/main_editor.dart'; import 'package:pro_image_editor/widgets/layer_widget.dart'; import 'fake/fake_image.dart';