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) =>
CustomLinkPreviewWidget(
node: node,
url: url,
title: title,
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/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),
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();

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_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,20 +30,29 @@ class CustomLinkPreviewWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () => launchUrlString(url),
child: Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.onSurface,
),
borderRadius: BorderRadius.circular(
6.0,
),
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(
color: Theme.of(context).colorScheme.onSurface,
),
borderRadius: BorderRadius.circular(
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,
);
},
),
];
}
}

View File

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

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"
},
"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.",