mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-12-15 17:24:08 +00:00
chore: support pasting image links that have no file extension (#7262)
* test: speed up copypasta tests * chore: strip away markdown syntax if message is image only * chore: paste image urls with no file extension * test: add integration test * test: group tests * chore: apply code suggestions to 3 files
This commit is contained in:
parent
0b0e10baa8
commit
c5a91e10df
File diff suppressed because one or more lines are too long
@ -13,6 +13,7 @@ import 'package:appflowy/plugins/ai_chat/application/chat_select_sources_cubit.d
|
||||
import 'package:appflowy/plugins/document/application/prelude.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/shared/markdown_to_document.dart';
|
||||
import 'package:appflowy/shared/patterns/common_patterns.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||
@ -170,7 +171,7 @@ class CopyButton extends StatelessWidget {
|
||||
);
|
||||
await getIt<ClipboardService>().setData(
|
||||
ClipboardServiceData(
|
||||
plainText: textMessage.text,
|
||||
plainText: _getTrimmedPlainText(textMessage.text),
|
||||
inAppJson: jsonEncode(document.toJson()),
|
||||
),
|
||||
);
|
||||
@ -184,6 +185,16 @@ class CopyButton extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getTrimmedPlainText(String plainText) {
|
||||
// match and capture inner url as group
|
||||
final matches = singleLineMarkdownImageRegex.allMatches(plainText);
|
||||
if (matches.length != 1) {
|
||||
return plainText;
|
||||
}
|
||||
|
||||
return matches.first[1] ?? plainText;
|
||||
}
|
||||
}
|
||||
|
||||
class RegenerateButton extends StatelessWidget {
|
||||
|
||||
@ -8,12 +8,12 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_p
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart';
|
||||
import 'package:appflowy/shared/clipboard_state.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/default_extensions.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:string_validator/string_validator.dart';
|
||||
|
||||
/// - support
|
||||
@ -154,17 +154,14 @@ Future<bool> _pasteAsLinkPreview(
|
||||
EditorState editorState,
|
||||
String? text,
|
||||
) async {
|
||||
// 1. the url should contains a protocol
|
||||
// 2. the url should not be an image url
|
||||
if (text == null ||
|
||||
text.isImageUrl() ||
|
||||
!isURL(text, {'require_protocol': true})) {
|
||||
// the url should contain a protocol
|
||||
if (text == null || !isURL(text, {'require_protocol': true})) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final selection = editorState.selection;
|
||||
// Apply the update only when the selection is collapsed
|
||||
// and at the start of the current line
|
||||
// and at the start of the current line
|
||||
if (selection == null ||
|
||||
!selection.isCollapsed ||
|
||||
selection.startIndex != 0) {
|
||||
@ -173,44 +170,52 @@ Future<bool> _pasteAsLinkPreview(
|
||||
|
||||
final node = editorState.getNodeAtPath(selection.start.path);
|
||||
// Apply the update only when the current node is a paragraph
|
||||
// and the paragraph is empty
|
||||
// and the paragraph is empty
|
||||
if (node == null ||
|
||||
node.type != ParagraphBlockKeys.type ||
|
||||
node.delta?.toPlainText().isNotEmpty == true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 1. insert the text with link format
|
||||
// 2. convert it the link preview node
|
||||
final textTransaction = editorState.transaction;
|
||||
textTransaction.insertText(
|
||||
node,
|
||||
0,
|
||||
text,
|
||||
attributes: {AppFlowyRichTextKeys.href: text},
|
||||
);
|
||||
final bool isImageUrl;
|
||||
try {
|
||||
isImageUrl = await _isImageUrl(text);
|
||||
} catch (e) {
|
||||
Log.info('unable to get content header');
|
||||
return false;
|
||||
}
|
||||
|
||||
// insert the text with link format
|
||||
final textTransaction = editorState.transaction
|
||||
..insertText(
|
||||
node,
|
||||
0,
|
||||
text,
|
||||
attributes: {AppFlowyRichTextKeys.href: text},
|
||||
);
|
||||
await editorState.apply(
|
||||
textTransaction,
|
||||
skipHistoryDebounce: true,
|
||||
);
|
||||
|
||||
final linkPreviewTransaction = editorState.transaction;
|
||||
final insertedNodes = [
|
||||
linkPreviewNode(url: text),
|
||||
// convert it to image or link preview node
|
||||
final replacementInsertedNodes = [
|
||||
isImageUrl ? imageNode(url: text) : linkPreviewNode(url: text),
|
||||
// if the next node is null, insert a empty paragraph node
|
||||
if (node.next == null) paragraphNode(),
|
||||
];
|
||||
linkPreviewTransaction.insertNodes(
|
||||
selection.start.path,
|
||||
insertedNodes,
|
||||
);
|
||||
linkPreviewTransaction.deleteNode(node);
|
||||
linkPreviewTransaction.afterSelection = Selection.collapsed(
|
||||
Position(
|
||||
path: node.path.next,
|
||||
),
|
||||
);
|
||||
await editorState.apply(linkPreviewTransaction);
|
||||
|
||||
final replacementTransaction = editorState.transaction
|
||||
..insertNodes(
|
||||
selection.start.path,
|
||||
replacementInsertedNodes,
|
||||
)
|
||||
..deleteNode(node)
|
||||
..afterSelection = Selection.collapsed(
|
||||
Position(path: node.path.next),
|
||||
);
|
||||
|
||||
await editorState.apply(replacementTransaction);
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -235,3 +240,16 @@ Future<void> doPlainPaste(EditorState editorState) async {
|
||||
Log.info('unable to parse the clipboard content');
|
||||
return;
|
||||
}
|
||||
|
||||
Future<bool> _isImageUrl(String text) async {
|
||||
final response = await http.head(Uri.parse(text));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final contentType = response.headers['content-type'];
|
||||
if (contentType != null) {
|
||||
return contentType.startsWith('image/');
|
||||
}
|
||||
}
|
||||
|
||||
throw 'bad status code';
|
||||
}
|
||||
|
||||
@ -13,6 +13,9 @@ const _imgUrlPattern =
|
||||
r'(https?:\/\/)([^\s(["<,>/]*)(\/)[^\s[",><]*(.png|.jpg|.jpeg|.gif|.webm|.webp|.bmp)(\?[^\s[",><]*)?';
|
||||
final imgUrlRegex = RegExp(_imgUrlPattern);
|
||||
|
||||
const _singleLineMarkdownImagePattern = "^!\\[.*\\]\\(($_hrefPattern)\\)\$";
|
||||
final singleLineMarkdownImageRegex = RegExp(_singleLineMarkdownImagePattern);
|
||||
|
||||
/// This pattern allows for both HTTP and HTTPS Scheme
|
||||
/// It allows for query parameters
|
||||
/// It only allows the following video extensions:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user