diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index 3c1edb37a3..f56a5a28bb 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -7,12 +7,14 @@ import 'package:appflowy/plugins/document/presentation/banner.dart'; import 'package:appflowy/plugins/document/presentation/editor_drop_manager.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_file.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/shared/patterns/common_patterns.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; @@ -20,6 +22,7 @@ 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:cross_file/cross_file.dart'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; @@ -195,9 +198,27 @@ class _DocumentPageState extends State } final isLocalMode = context.read().isLocalMode; + + final List imageFiles = []; + final List otherfiles = []; + for (final file in details.files) { + if (file.mimeType?.startsWith('image/') ?? + false || imgExtensionRegex.hasMatch(file.name)) { + imageFiles.add(file); + } else { + otherfiles.add(file); + } + } + await editorState!.dropImages( data.dropTarget!, - details.files, + imageFiles, + widget.view.id, + isLocalMode, + ); + await editorState!.dropFiles( + data.dropTarget!, + otherfiles, widget.view.id, isLocalMode, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_file.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_file.dart new file mode 100644 index 0000000000..1ca77a96d8 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_file.dart @@ -0,0 +1,40 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_block.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:cross_file/cross_file.dart'; + +extension PasteFromFile on EditorState { + Future dropFiles( + Node dropNode, + List files, + String documentId, + bool isLocalMode, + ) async { + for (final file in files) { + String? path; + FileUrlType? type; + if (isLocalMode) { + path = await saveFileToLocalStorage(file.path); + type = FileUrlType.local; + } else { + (path, _) = await saveFileToCloudStorage(file.path, documentId); + type = FileUrlType.cloud; + } + + if (path == null) { + continue; + } + + final t = transaction + ..insertNode( + dropNode.path, + fileNode( + url: path, + type: type, + name: file.name, + ), + ); + await apply(t); + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_component.dart index a2cbb4258b..704f9ddcbe 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_component.dart @@ -90,12 +90,15 @@ enum FileUrlType { Node fileNode({ required String url, FileUrlType type = FileUrlType.local, + String? name, }) { return Node( type: FileBlockKeys.type, attributes: { FileBlockKeys.url: url, FileBlockKeys.urlType: type.toIntValue(), + FileBlockKeys.name: name, + FileBlockKeys.uploadedAt: DateTime.now().millisecondsSinceEpoch, }, ); } @@ -149,7 +152,6 @@ class FileBlockComponentState extends State final showActionsNotifier = ValueNotifier(false); final controller = PopoverController(); final menuController = PopoverController(); - final menuMutex = PopoverMutex(); late final editorState = Provider.of(context, listen: false); @@ -157,26 +159,6 @@ class FileBlockComponentState extends State bool isDragging = false; bool isHovering = false; - @override - void initState() { - super.initState(); - - final url = node.attributes[FileBlockKeys.url] as String?; - if (url != null && url.isNotEmpty) { - // If the name attribute is not set, extract the file name from the url. - final name = node.attributes[FileBlockKeys.name] as String?; - if (name == null || name.isEmpty) { - final name = Uri.tryParse(url)?.pathSegments.last ?? url; - final attributes = node.attributes; - attributes[FileBlockKeys.name] = name; - - final transaction = editorState.transaction; - transaction.updateNode(node, attributes); - editorState.apply(transaction); - } - } - } - @override void didChangeDependencies() { dropManagerState = context.read(); @@ -202,8 +184,9 @@ class FileBlockComponentState extends State opaque: false, child: GestureDetector( behavior: HitTestBehavior.translucent, - onTap: - url != null && url.isNotEmpty ? () => afLaunchUrlString(url) : null, + onTap: url != null && url.isNotEmpty + ? () => afLaunchUrlString(url) + : controller.show, child: DecoratedBox( decoration: BoxDecoration( color: isHovering @@ -328,7 +311,6 @@ class FileBlockComponentState extends State onTap: menuController.show, child: AppFlowyPopover( controller: menuController, - mutex: menuMutex, triggerActions: PopoverTriggerFlags.none, direction: PopoverDirection.bottomWithRightAligned, onClose: () { @@ -406,6 +388,7 @@ class FileBlockComponentState extends State FileBlockKeys.url: url, FileBlockKeys.urlType: urlType.toIntValue(), FileBlockKeys.name: name, + FileBlockKeys.uploadedAt: DateTime.now().millisecondsSinceEpoch, }); await editorState.apply(transaction); } @@ -428,6 +411,7 @@ class FileBlockComponentState extends State FileBlockKeys.url: url, FileBlockKeys.urlType: FileUrlType.network.toIntValue(), FileBlockKeys.name: name, + FileBlockKeys.uploadedAt: DateTime.now().millisecondsSinceEpoch, }); await editorState.apply(transaction); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_menu.dart index 089eb8d022..872f0d61d0 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_menu.dart @@ -3,12 +3,15 @@ import 'package:flutter/material.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_block.dart'; +import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; +import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class FileBlockMenu extends StatefulWidget { const FileBlockMenu({ @@ -41,9 +44,25 @@ class _FileBlockMenuState extends State { ); } + @override + void dispose() { + errorMessage.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { + final uploadedAtInMS = + widget.node.attributes[FileBlockKeys.uploadedAt] as int?; + final uploadedAt = uploadedAtInMS != null + ? DateTime.fromMillisecondsSinceEpoch(uploadedAtInMS) + : null; + final dateFormat = context.read().state.dateFormat; + final urlType = + FileUrlType.fromIntValue(widget.node.attributes[FileBlockKeys.urlType]); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ HoverButton( @@ -84,6 +103,25 @@ class _FileBlockMenuState extends State { widget.controller.close(); }, ), + if (uploadedAt != null) ...[ + const Divider(height: 12), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: FlowyText.regular( + [FileUrlType.cloud, FileUrlType.local].contains(urlType) + ? LocaleKeys.document_plugins_file_uploadedAt.tr( + args: [dateFormat.formatDate(uploadedAt, false)], + ) + : LocaleKeys.document_plugins_file_linkedAt.tr( + args: [dateFormat.formatDate(uploadedAt, false)], + ), + fontSize: 14, + maxLines: 2, + color: Theme.of(context).hintColor, + ), + ), + const VSpace(2), + ], ], ); } @@ -133,6 +171,7 @@ class _RenameTextFieldState extends State<_RenameTextField> { @override void dispose() { widget.errorMessage.removeListener(_setState); + widget.nameController.dispose(); super.dispose(); } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index f634e19b3f..6b4d7575d9 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -1580,7 +1580,9 @@ "title": "Rename file", "description": "Enter the new name for this file", "nameEmptyError": "File name cannot be left empty." - } + }, + "uploadedAt": "Uploaded on {}", + "linkedAt": "Link added on {}" } }, "outlineBlock": {