From d1af172fb7a0529630173d6e23725631a7248ca6 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Fri, 26 Jul 2024 00:08:55 +0200 Subject: [PATCH] feat: drop images into editor (#5813) --- .../lib/plugins/document/document_page.dart | 73 +++++++++++++++++-- .../document/presentation/editor_page.dart | 4 + .../copy_and_paste/paste_from_image.dart | 40 ++++++++++ frontend/appflowy_flutter/pubspec.lock | 22 +++--- frontend/appflowy_flutter/pubspec.yaml | 7 +- 5 files changed, 125 insertions(+), 21 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index 097a23e5c9..00fdd1b118 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -6,7 +6,10 @@ import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/presentation/banner.dart'; import 'package:appflowy/plugins/document/presentation/editor_notification.dart'; import 'package:appflowy/plugins/document/presentation/editor_page.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy/startup/startup.dart'; @@ -16,10 +19,17 @@ import 'package:appflowy/workspace/application/view/prelude.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide Log; +import 'package:desktop_drop/desktop_drop.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +const _excludeFromDropTarget = [ + ImageBlockKeys.type, + CustomImageBlockKeys.type, + MultiImageBlockKeys.type, +]; + class DocumentPage extends StatefulWidget { const DocumentPage({ super.key, @@ -125,15 +135,62 @@ class _DocumentPageState extends State }, ); } else { - child = AppFlowyEditorPage( - editorState: state.editorState!, - styleCustomizer: EditorStyleCustomizer( - context: context, - // the 44 is the width of the left action list - padding: EditorStyleCustomizer.documentPadding, + child = DropTarget( + onDragExited: (_) => + state.editorState!.selectionService.removeDropTarget(), + onDragUpdated: (details) { + final data = state.editorState!.selectionService + .getDropTargetRenderData(details.globalPosition); + + if (data != null && + data.dropTarget != null && + + // We implement custom Drop logic for image blocks, this is + // how we can exclude them from the Drop Target + !_excludeFromDropTarget.contains(data.cursorNode?.type)) { + // Render the drop target + state.editorState!.selectionService + .renderDropTargetForOffset(details.globalPosition); + } else { + state.editorState!.selectionService.removeDropTarget(); + } + }, + onDragDone: (details) async { + state.editorState!.selectionService.removeDropTarget(); + + final data = state.editorState!.selectionService + .getDropTargetRenderData(details.globalPosition); + + if (data != null) { + if (data.cursorNode != null) { + if ([ + ImageBlockKeys.type, + CustomImageBlockKeys.type, + MultiImageBlockKeys.type, + ].contains(data.cursorNode?.type)) { + return; + } + + final isLocalMode = context.read().isLocalMode; + await editorState!.dropImages( + data.dropTarget!, + details.files, + widget.view.id, + isLocalMode, + ); + } + } + }, + child: AppFlowyEditorPage( + editorState: state.editorState!, + styleCustomizer: EditorStyleCustomizer( + context: context, + // the 44 is the width of the left action list + padding: EditorStyleCustomizer.documentPadding, + ), + header: _buildCoverAndIcon(context, state), + initialSelection: widget.initialSelection, ), - header: _buildCoverAndIcon(context, state), - initialSelection: widget.initialSelection, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index ed6cc04d8e..78f8e20fee 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -353,6 +353,10 @@ class _AppFlowyEditorPageState extends State { }, child: VSpace(PlatformExtension.isDesktopOrWeb ? 200 : 400), ), + dropTargetStyle: AppFlowyDropTargetStyle( + color: Theme.of(context).colorScheme.primary.withOpacity(0.8), + margin: const EdgeInsets.only(left: 44), + ), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart index c6c8d2162c..989798dcf2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart @@ -3,12 +3,16 @@ import 'dart:typed_data'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart'; +import 'package:appflowy/shared/patterns/common_patterns.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/settings/application_data_storage.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide Log; +import 'package:cross_file/cross_file.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/uuid.dart'; import 'package:path/path.dart' as p; @@ -21,6 +25,42 @@ extension PasteFromImage on EditorState { 'gif', ]; + Future dropImages( + Node dropNode, + List files, + String documentId, + bool isLocalMode, + ) async { + final imageFiles = files.where( + (file) => + file.mimeType?.startsWith('image/') ?? + false || imgExtensionRegex.hasMatch(file.name), + ); + + for (final file in imageFiles) { + String? path; + CustomImageType? type; + if (isLocalMode) { + path = await saveImageToLocalStorage(file.path); + type = CustomImageType.local; + } else { + (path, _) = await saveImageToCloudStorage(file.path, documentId); + type = CustomImageType.internal; + } + + if (path == null) { + continue; + } + + final t = transaction + ..insertNode( + dropNode.path, + customImageNode(url: path, type: type), + ); + await apply(t); + } + } + Future pasteImage( String format, Uint8List imageBytes, diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 5f143adc84..8011ba4a3c 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -53,8 +53,8 @@ packages: dependency: "direct main" description: path: "." - ref: "61d4363" - resolved-ref: "61d4363b4675f7ef91ef5003f2f88bbcb4d9dfa9" + ref: "268aae9" + resolved-ref: "268aae905b18efc8a3a9c88dc75ebd19b314bd43" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "3.1.0" @@ -387,7 +387,7 @@ packages: source: hosted version: "1.7.2" cross_file: - dependency: transitive + dependency: "direct main" description: name: cross_file sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" @@ -1118,18 +1118,18 @@ packages: dependency: transitive description: name: irondash_engine_context - sha256: "4f5e2629296430cce08cdff42e47cef07b8f74a64fdbdfb0525d147bc1a969a2" + sha256: cd7b769db11a2b5243b037c8a9b1ecaef02e1ae27a2d909ffa78c1dad747bb10 url: "https://pub.dev" source: hosted - version: "0.5.2" + version: "0.5.4" irondash_message_channel: dependency: transitive description: name: irondash_message_channel - sha256: dd581214215dca054bd9873209d690ec3609288c28774cb509dbd86b21180cf8 + sha256: b4101669776509c76133b8917ab8cfc704d3ad92a8c450b92934dd8884a2f060 url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.7.0" isolates: dependency: "direct main" description: @@ -2053,18 +2053,18 @@ packages: dependency: "direct main" description: name: super_clipboard - sha256: "15d25eb88df8e904e0c2ef77378c6010cc57bbfc0b6f91f2416d08fad5fcca92" + sha256: c72d2ae8c3a66b20a104523add86b7c2813b1d4cced893a9764b84fb97ac8e2a url: "https://pub.dev" source: hosted - version: "0.8.5" + version: "0.8.18" super_native_extensions: dependency: transitive description: name: super_native_extensions - sha256: "530a2118d032483b192713c68ed7105fe64418f22492165f87ed01f9b01d4965" + sha256: b03f19e54744b65940a7c2cb4f93abd4819b5355aa3464d7b3c9a013b6b76db1 url: "https://pub.dev" source: hosted - version: "0.8.12" + version: "0.8.18" sync_http: dependency: transitive description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 2f7cc3cfbc..508d66dbb2 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -142,10 +142,13 @@ dependencies: shimmer: ^3.0.0 isolates: ^3.0.3+8 markdown_widget: ^2.3.2+6 - desktop_drop: ^0.4.4 markdown: logger: ^2.4.0 + # Desktop Drop uses Cross File (XFile) data type + desktop_drop: ^0.4.4 + cross_file: ^0.3.4+1 + # Window Manager for MacOS and Linux window_manager: ^0.3.9 @@ -191,7 +194,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "61d4363" + ref: "268aae9" appflowy_editor_plugins: git: