feat: convert link preview block to url text (#4734)

This commit is contained in:
Lucas.Xu 2024-02-25 21:59:23 +07:00 committed by GitHub
parent b75947b630
commit 93af6e69a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 175 additions and 154 deletions

View File

@ -188,6 +188,7 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
), ),
builder: (context, node, url, title, description, imageUrl) => builder: (context, node, url, title, description, imageUrl) =>
CustomLinkPreviewWidget( CustomLinkPreviewWidget(
node: node,
url: url, url: url,
title: title, title: title,
description: description, description: description,

View File

@ -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),
),
),
);
}
}

View File

@ -1,13 +1,15 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.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/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/util/string_extension.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.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:flowy_infra_ui/widget/ignore_parent_gesture.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -51,18 +53,23 @@ class _ImageMenuState extends State<ImageMenu> {
const HSpace(4), const HSpace(4),
// disable the copy link button if the image is hosted on appflowy cloud // disable the copy link button if the image is hosted on appflowy cloud
// because the url needs the verification token to be accessible // because the url needs the verification token to be accessible
if (!(url?.isAppFlowyCloudUrl ?? false)) if (!(url?.isAppFlowyCloudUrl ?? false)) ...[
_ImageCopyLinkButton( MenuBlockButton(
tooltip: LocaleKeys.editor_copyLink.tr(),
iconData: FlowySvgs.copy_s,
onTap: copyImageLink, onTap: copyImageLink,
), ),
const HSpace(4), const HSpace(4),
],
_ImageAlignButton( _ImageAlignButton(
node: widget.node, node: widget.node,
state: widget.state, state: widget.state,
), ),
const _Divider(), const _Divider(),
_ImageDeleteButton( MenuBlockButton(
onTap: () => deleteImage(), tooltip: LocaleKeys.button_delete.tr(),
iconData: FlowySvgs.delete_s,
onTap: deleteImage,
), ),
const HSpace(4), 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 { class _ImageAlignButton extends StatefulWidget {
const _ImageAlignButton({ const _ImageAlignButton({
required this.node, required this.node,
@ -162,7 +146,11 @@ class _ImageAlignButtonState extends State<_ImageAlignButton> {
margin: const EdgeInsets.all(0), margin: const EdgeInsets.all(0),
direction: PopoverDirection.bottomWithCenterAligned, direction: PopoverDirection.bottomWithCenterAligned,
offset: const Offset(0, 10), offset: const Offset(0, 10),
child: buildAlignIcon(), child: MenuBlockButton(
tooltip: LocaleKeys.document_plugins_optionAction_align.tr(),
iconData: iconFor(align),
onTap: () {},
),
popupBuilder: (_) { popupBuilder: (_) {
preventMenuClose(); preventMenuClose();
return _AlignButtons( return _AlignButtons(
@ -210,19 +198,6 @@ class _ImageAlignButtonState extends State<_ImageAlignButton> {
return FlowySvgs.align_left_s; 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 { class _AlignButtons extends StatelessWidget {
@ -240,19 +215,22 @@ class _AlignButtons extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const HSpace(4), const HSpace(4),
_AlignButton( MenuBlockButton(
icon: FlowySvgs.align_left_s, tooltip: LocaleKeys.document_plugins_optionAction_left,
onTap: () => onAlignChanged('left'), iconData: FlowySvgs.align_left_s,
onTap: () => onAlignChanged(leftAlignmentKey),
), ),
const _Divider(), const _Divider(),
_AlignButton( MenuBlockButton(
icon: FlowySvgs.align_center_s, tooltip: LocaleKeys.document_plugins_optionAction_center,
onTap: () => onAlignChanged('center'), iconData: FlowySvgs.align_center_s,
onTap: () => onAlignChanged(centerAlignmentKey),
), ),
const _Divider(), const _Divider(),
_AlignButton( MenuBlockButton(
icon: FlowySvgs.align_right_s, tooltip: LocaleKeys.document_plugins_optionAction_right,
onTap: () => onAlignChanged('right'), iconData: FlowySvgs.align_right_s,
onTap: () => onAlignChanged(rightAlignmentKey),
), ),
const HSpace(4), 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 { class _Divider extends StatelessWidget {
const _Divider(); const _Divider();

View File

@ -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/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:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.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'; import 'package:url_launcher/url_launcher_string.dart';
class CustomLinkPreviewWidget extends StatelessWidget { class CustomLinkPreviewWidget extends StatelessWidget {
const CustomLinkPreviewWidget({ const CustomLinkPreviewWidget({
super.key, super.key,
required this.node,
required this.url, required this.url,
this.title, this.title,
this.description, this.description,
this.imageUrl, this.imageUrl,
}); });
final Node node;
final String? title; final String? title;
final String? description; final String? description;
final String? imageUrl; final String? imageUrl;
@ -19,9 +30,17 @@ class CustomLinkPreviewWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return InkWell( final documentFontSize = context
onTap: () => launchUrlString(url), .read<EditorState>()
child: Container( .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, clipBehavior: Clip.hardEdge,
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
@ -31,8 +50,9 @@ class CustomLinkPreviewWidget extends StatelessWidget {
6.0, 6.0,
), ),
), ),
child: IntrinsicHeight(
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
if (imageUrl != null) if (imageUrl != null)
ClipRRect( ClipRRect(
@ -42,8 +62,7 @@ class CustomLinkPreviewWidget extends StatelessWidget {
), ),
child: FlowyNetworkImage( child: FlowyNetworkImage(
url: imageUrl!, url: imageUrl!,
width: 180, width: width,
height: 120,
), ),
), ),
Expanded( Expanded(
@ -55,12 +74,15 @@ class CustomLinkPreviewWidget extends StatelessWidget {
children: [ children: [
if (title != null) if (title != null)
Padding( Padding(
padding: const EdgeInsets.only(bottom: 4.0), padding: const EdgeInsets.only(
bottom: 4.0,
right: 10.0,
),
child: FlowyText.medium( child: FlowyText.medium(
title!, title!,
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
fontSize: 16.0, fontSize: fontSize,
), ),
), ),
if (description != null) if (description != null)
@ -70,6 +92,7 @@ class CustomLinkPreviewWidget extends StatelessWidget {
description!, description!,
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
fontSize: fontSize - 4,
), ),
), ),
FlowyText( FlowyText(
@ -77,6 +100,7 @@ class CustomLinkPreviewWidget extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
maxLines: 2, maxLines: 2,
color: Theme.of(context).hintColor, 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,
);
},
),
];
} }
} }

View File

@ -1,5 +1,7 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.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/workspace/presentation/home/toast.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
@ -43,11 +45,24 @@ class _LinkPreviewMenuState extends State<LinkPreviewMenu> {
child: Row( child: Row(
children: [ children: [
const HSpace(4), 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, onTap: copyImageLink,
), ),
const _Divider(), const _Divider(),
_DeleteButton( MenuBlockButton(
tooltip: LocaleKeys.button_delete.tr(),
iconData: FlowySvgs.delete_s,
onTap: deleteLinkPreviewNode, onTap: deleteLinkPreviewNode,
), ),
const HSpace(4), 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 { class _Divider extends StatelessWidget {
const _Divider(); const _Divider();

View File

@ -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);
}

View File

@ -790,7 +790,8 @@
"imageUploadFailed": "Image upload failed" "imageUploadFailed": "Image upload failed"
}, },
"urlPreview": { "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": { "outline": {
"addHeadingToCreateOutline": "Add headings to create a table of contents.", "addHeadingToCreateOutline": "Add headings to create a table of contents.",