mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: convert link preview block to url text (#4734)
This commit is contained in:
parent
b75947b630
commit
93af6e69a1
@ -188,6 +188,7 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
|
||||
),
|
||||
builder: (context, node, url, title, description, imageUrl) =>
|
||||
CustomLinkPreviewWidget(
|
||||
node: node,
|
||||
url: url,
|
||||
title: title,
|
||||
description: description,
|
||||
|
@ -0,0 +1,32 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MenuBlockButton extends StatelessWidget {
|
||||
const MenuBlockButton({
|
||||
super.key,
|
||||
required this.tooltip,
|
||||
required this.iconData,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final VoidCallback? onTap;
|
||||
final String tooltip;
|
||||
final FlowySvgData iconData;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
onTap: onTap,
|
||||
text: FlowyTooltip(
|
||||
message: tooltip,
|
||||
child: FlowySvg(
|
||||
iconData,
|
||||
size: const Size.square(16),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,13 +1,15 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/align_toolbar_item/align_toolbar_item.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/util/string_extension.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.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:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -51,18 +53,23 @@ class _ImageMenuState extends State<ImageMenu> {
|
||||
const HSpace(4),
|
||||
// disable the copy link button if the image is hosted on appflowy cloud
|
||||
// because the url needs the verification token to be accessible
|
||||
if (!(url?.isAppFlowyCloudUrl ?? false))
|
||||
_ImageCopyLinkButton(
|
||||
if (!(url?.isAppFlowyCloudUrl ?? false)) ...[
|
||||
MenuBlockButton(
|
||||
tooltip: LocaleKeys.editor_copyLink.tr(),
|
||||
iconData: FlowySvgs.copy_s,
|
||||
onTap: copyImageLink,
|
||||
),
|
||||
const HSpace(4),
|
||||
],
|
||||
_ImageAlignButton(
|
||||
node: widget.node,
|
||||
state: widget.state,
|
||||
),
|
||||
const _Divider(),
|
||||
_ImageDeleteButton(
|
||||
onTap: () => deleteImage(),
|
||||
MenuBlockButton(
|
||||
tooltip: LocaleKeys.button_delete.tr(),
|
||||
iconData: FlowySvgs.delete_s,
|
||||
onTap: deleteImage,
|
||||
),
|
||||
const HSpace(4),
|
||||
],
|
||||
@ -90,29 +97,6 @@ class _ImageMenuState extends State<ImageMenu> {
|
||||
}
|
||||
}
|
||||
|
||||
class _ImageCopyLinkButton extends StatelessWidget {
|
||||
const _ImageCopyLinkButton({
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
onTap: onTap,
|
||||
text: FlowyTooltip(
|
||||
message: LocaleKeys.editor_copyLink.tr(),
|
||||
child: const FlowySvg(
|
||||
FlowySvgs.copy_s,
|
||||
size: Size.square(16),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ImageAlignButton extends StatefulWidget {
|
||||
const _ImageAlignButton({
|
||||
required this.node,
|
||||
@ -162,7 +146,11 @@ class _ImageAlignButtonState extends State<_ImageAlignButton> {
|
||||
margin: const EdgeInsets.all(0),
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
offset: const Offset(0, 10),
|
||||
child: buildAlignIcon(),
|
||||
child: MenuBlockButton(
|
||||
tooltip: LocaleKeys.document_plugins_optionAction_align.tr(),
|
||||
iconData: iconFor(align),
|
||||
onTap: () {},
|
||||
),
|
||||
popupBuilder: (_) {
|
||||
preventMenuClose();
|
||||
return _AlignButtons(
|
||||
@ -210,19 +198,6 @@ class _ImageAlignButtonState extends State<_ImageAlignButton> {
|
||||
return FlowySvgs.align_left_s;
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildAlignIcon() {
|
||||
return FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyTooltip(
|
||||
message: LocaleKeys.document_plugins_optionAction_align.tr(),
|
||||
child: FlowySvg(
|
||||
iconFor(align),
|
||||
size: const Size.square(16),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AlignButtons extends StatelessWidget {
|
||||
@ -240,19 +215,22 @@ class _AlignButtons extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const HSpace(4),
|
||||
_AlignButton(
|
||||
icon: FlowySvgs.align_left_s,
|
||||
onTap: () => onAlignChanged('left'),
|
||||
MenuBlockButton(
|
||||
tooltip: LocaleKeys.document_plugins_optionAction_left,
|
||||
iconData: FlowySvgs.align_left_s,
|
||||
onTap: () => onAlignChanged(leftAlignmentKey),
|
||||
),
|
||||
const _Divider(),
|
||||
_AlignButton(
|
||||
icon: FlowySvgs.align_center_s,
|
||||
onTap: () => onAlignChanged('center'),
|
||||
MenuBlockButton(
|
||||
tooltip: LocaleKeys.document_plugins_optionAction_center,
|
||||
iconData: FlowySvgs.align_center_s,
|
||||
onTap: () => onAlignChanged(centerAlignmentKey),
|
||||
),
|
||||
const _Divider(),
|
||||
_AlignButton(
|
||||
icon: FlowySvgs.align_right_s,
|
||||
onTap: () => onAlignChanged('right'),
|
||||
MenuBlockButton(
|
||||
tooltip: LocaleKeys.document_plugins_optionAction_right,
|
||||
iconData: FlowySvgs.align_right_s,
|
||||
onTap: () => onAlignChanged(rightAlignmentKey),
|
||||
),
|
||||
const HSpace(4),
|
||||
],
|
||||
@ -261,51 +239,6 @@ class _AlignButtons extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _AlignButton extends StatelessWidget {
|
||||
const _AlignButton({
|
||||
required this.icon,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final FlowySvgData icon;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
onTap: onTap,
|
||||
text: FlowySvg(
|
||||
icon,
|
||||
size: const Size.square(16),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ImageDeleteButton extends StatelessWidget {
|
||||
const _ImageDeleteButton({
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
onTap: onTap,
|
||||
text: FlowyTooltip(
|
||||
message: LocaleKeys.button_delete.tr(),
|
||||
child: const FlowySvg(
|
||||
FlowySvgs.delete_s,
|
||||
size: Size.square(16),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Divider extends StatelessWidget {
|
||||
const _Divider();
|
||||
|
||||
|
@ -1,17 +1,28 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart';
|
||||
import 'package:appflowy/shared/appflowy_network_image.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class CustomLinkPreviewWidget extends StatelessWidget {
|
||||
const CustomLinkPreviewWidget({
|
||||
super.key,
|
||||
required this.node,
|
||||
required this.url,
|
||||
this.title,
|
||||
this.description,
|
||||
this.imageUrl,
|
||||
});
|
||||
|
||||
final Node node;
|
||||
final String? title;
|
||||
final String? description;
|
||||
final String? imageUrl;
|
||||
@ -19,9 +30,17 @@ class CustomLinkPreviewWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: () => launchUrlString(url),
|
||||
child: Container(
|
||||
final documentFontSize = context
|
||||
.read<EditorState>()
|
||||
.editorStyle
|
||||
.textStyleConfiguration
|
||||
.text
|
||||
.fontSize ??
|
||||
16.0;
|
||||
final (fontSize, width) = PlatformExtension.isDesktopOrWeb
|
||||
? (documentFontSize, 180.0)
|
||||
: (documentFontSize - 2, 120.0);
|
||||
final Widget child = Container(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
@ -31,8 +50,9 @@ class CustomLinkPreviewWidget extends StatelessWidget {
|
||||
6.0,
|
||||
),
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (imageUrl != null)
|
||||
ClipRRect(
|
||||
@ -42,8 +62,7 @@ class CustomLinkPreviewWidget extends StatelessWidget {
|
||||
),
|
||||
child: FlowyNetworkImage(
|
||||
url: imageUrl!,
|
||||
width: 180,
|
||||
height: 120,
|
||||
width: width,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
@ -55,12 +74,15 @@ class CustomLinkPreviewWidget extends StatelessWidget {
|
||||
children: [
|
||||
if (title != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4.0),
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 4.0,
|
||||
right: 10.0,
|
||||
),
|
||||
child: FlowyText.medium(
|
||||
title!,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
fontSize: 16.0,
|
||||
fontSize: fontSize,
|
||||
),
|
||||
),
|
||||
if (description != null)
|
||||
@ -70,6 +92,7 @@ class CustomLinkPreviewWidget extends StatelessWidget {
|
||||
description!,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
fontSize: fontSize - 4,
|
||||
),
|
||||
),
|
||||
FlowyText(
|
||||
@ -77,6 +100,7 @@ class CustomLinkPreviewWidget extends StatelessWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
color: Theme.of(context).hintColor,
|
||||
fontSize: fontSize - 4,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -86,5 +110,40 @@ class CustomLinkPreviewWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (PlatformExtension.isDesktopOrWeb) {
|
||||
return InkWell(
|
||||
onTap: () => launchUrlString(url),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
return MobileBlockActionButtons(
|
||||
node: node,
|
||||
editorState: context.read<EditorState>(),
|
||||
extendActionWidgets: _buildExtendActionWidgets(context),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
// only used on mobile platform
|
||||
List<Widget> _buildExtendActionWidgets(BuildContext context) {
|
||||
return [
|
||||
FlowyOptionTile.text(
|
||||
showTopBorder: false,
|
||||
text: LocaleKeys.document_plugins_urlPreview_convertToLink.tr(),
|
||||
leftIcon: const FlowySvg(
|
||||
FlowySvgs.m_aa_link_s,
|
||||
size: Size.square(20),
|
||||
),
|
||||
onTap: () {
|
||||
context.pop();
|
||||
convertUrlPreviewNodeToLink(
|
||||
context.read<EditorState>(),
|
||||
node,
|
||||
);
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
@ -43,11 +45,24 @@ class _LinkPreviewMenuState extends State<LinkPreviewMenu> {
|
||||
child: Row(
|
||||
children: [
|
||||
const HSpace(4),
|
||||
_CopyLinkButton(
|
||||
MenuBlockButton(
|
||||
tooltip: LocaleKeys.document_plugins_urlPreview_convertToLink.tr(),
|
||||
iconData: FlowySvgs.m_aa_link_s,
|
||||
onTap: () => convertUrlPreviewNodeToLink(
|
||||
context.read<EditorState>(),
|
||||
widget.node,
|
||||
),
|
||||
),
|
||||
const HSpace(4),
|
||||
MenuBlockButton(
|
||||
tooltip: LocaleKeys.editor_copyLink.tr(),
|
||||
iconData: FlowySvgs.copy_s,
|
||||
onTap: copyImageLink,
|
||||
),
|
||||
const _Divider(),
|
||||
_DeleteButton(
|
||||
MenuBlockButton(
|
||||
tooltip: LocaleKeys.button_delete.tr(),
|
||||
iconData: FlowySvgs.delete_s,
|
||||
onTap: deleteLinkPreviewNode,
|
||||
),
|
||||
const HSpace(4),
|
||||
@ -77,44 +92,6 @@ class _LinkPreviewMenuState extends State<LinkPreviewMenu> {
|
||||
}
|
||||
}
|
||||
|
||||
class _CopyLinkButton extends StatelessWidget {
|
||||
const _CopyLinkButton({
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: const FlowySvg(
|
||||
FlowySvgs.copy_s,
|
||||
size: Size.square(16),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DeleteButton extends StatelessWidget {
|
||||
const _DeleteButton({
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: const FlowySvg(
|
||||
FlowySvgs.delete_s,
|
||||
size: Size.square(16),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Divider extends StatelessWidget {
|
||||
const _Divider();
|
||||
|
||||
|
@ -0,0 +1,18 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
|
||||
void convertUrlPreviewNodeToLink(EditorState editorState, Node node) {
|
||||
assert(node.type == LinkPreviewBlockKeys.type);
|
||||
final url = node.attributes[ImageBlockKeys.url];
|
||||
final transaction = editorState.transaction;
|
||||
transaction
|
||||
..insertNode(node.path, paragraphNode(text: url))
|
||||
..deleteNode(node);
|
||||
transaction.afterSelection = Selection.collapsed(
|
||||
Position(
|
||||
path: node.path,
|
||||
offset: url.length,
|
||||
),
|
||||
);
|
||||
editorState.apply(transaction);
|
||||
}
|
@ -790,7 +790,8 @@
|
||||
"imageUploadFailed": "Image upload failed"
|
||||
},
|
||||
"urlPreview": {
|
||||
"copiedToPasteBoard": "The link has been copied to the clipboard"
|
||||
"copiedToPasteBoard": "The link has been copied to the clipboard",
|
||||
"convertToLink": "Convert to embed link"
|
||||
},
|
||||
"outline": {
|
||||
"addHeadingToCreateOutline": "Add headings to create a table of contents.",
|
||||
|
Loading…
Reference in New Issue
Block a user