feat: support share page and block (#6431)

* feat: support share page

* feat: support copy share link

* chore: replace share icon

* chore: update translations

* chore: optimize code

* test: add share link test

* feat: support copy block link

* test: add copy link to block test

* chore: refactor share code

* fix: doc bloc not found issue
This commit is contained in:
Lucas 2024-10-03 14:31:04 +08:00 committed by GitHub
parent c1cf58b99e
commit 97913c390b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 508 additions and 34 deletions

View File

@ -1,15 +1,14 @@
import 'document/document_delete_block_test.dart' as document_delete_block_test;
import 'document/document_drag_block_test.dart' as document_drag_block_test;
import 'document/document_option_actions_test.dart'
as document_option_actions_test;
import 'sidebar/sidebar_move_page_test.dart' as sidebar_move_page_test;
import 'uncategorized/anon_user_continue_test.dart' as anon_user_continue_test;
import 'uncategorized/appflowy_cloud_auth_test.dart'
as appflowy_cloud_auth_test;
import 'uncategorized/empty_test.dart' as preset_af_cloud_env_test;
import 'uncategorized/user_setting_sync_test.dart' as user_sync_test;
import 'workspace/change_name_and_icon_test.dart'
as change_workspace_name_and_icon_test;
import 'workspace/collaborative_workspace_test.dart'
as collaboration_workspace_test;
import 'workspace/workspace_settings_test.dart' as workspace_settings_test;
import 'workspace/workspace_test_runner.dart' as workspace_test_runner;
Future<void> main() async {
preset_af_cloud_env_test.main();
@ -18,12 +17,12 @@ Future<void> main() async {
anon_user_continue_test.main();
// workspace
collaboration_workspace_test.main();
change_workspace_name_and_icon_test.main();
workspace_settings_test.main();
workspace_test_runner.startTesting();
// document
document_option_actions_test.main();
document_drag_block_test.main();
document_delete_block_test.main();
// sidebar
sidebar_move_page_test.main();

View File

@ -1,5 +1,8 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@ -9,7 +12,7 @@ import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('document drag block: ', () {
group('document option actions:', () {
testWidgets('drag block to the top', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
@ -63,5 +66,36 @@ void main() {
final afterMoveBlock = tester.editor.getNodeAtPath([9, 0]);
expect(afterMoveBlock.delta, beforeMoveBlock.delta);
});
testWidgets('copy block link', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// open getting started page
await tester.openPage(Constants.gettingStartedPageName);
// hover and click on the option menu button beside the block component.
await tester.editor.hoverAndClickOptionMenuButton([0]);
// click the copy link to block option
await tester.tap(
find.findTextInFlowyText(
LocaleKeys.document_plugins_optionAction_copyLinkToBlock.tr(),
),
);
await tester.pumpAndSettle(Durations.short1);
// check the clipboard
final content = await Clipboard.getData(Clipboard.kTextPlain);
expect(
content?.text,
matches(
r'^https:\/\/appflowy\.com\/app\/[a-f0-9-]{36}\/[a-f0-9-]{36}\?blockId=[A-Za-z0-9_-]+$',
),
);
});
});
}

View File

@ -0,0 +1,101 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../../shared/constants.dart';
import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('document option actions:', () {
testWidgets('drag block to the top', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// open getting started page
await tester.openPage(Constants.gettingStartedPageName);
// before move
final beforeMoveBlock = tester.editor.getNodeAtPath([1]);
// move the desktop guide to the top, above the getting started
await tester.editor.dragBlock(
[1],
const Offset(20, -80),
);
// wait for the move animation to complete
await tester.pumpAndSettle(Durations.short1);
// check if the block is moved to the top
final afterMoveBlock = tester.editor.getNodeAtPath([0]);
expect(afterMoveBlock.delta, beforeMoveBlock.delta);
});
testWidgets('drag block to other block\'s child', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// open getting started page
await tester.openPage(Constants.gettingStartedPageName);
// before move
final beforeMoveBlock = tester.editor.getNodeAtPath([10]);
// move the checkbox to the child of the block at path [9]
await tester.editor.dragBlock(
[10],
const Offset(80, -30),
);
// wait for the move animation to complete
await tester.pumpAndSettle(Durations.short1);
// check if the block is moved to the child of the block at path [9]
final afterMoveBlock = tester.editor.getNodeAtPath([9, 0]);
expect(afterMoveBlock.delta, beforeMoveBlock.delta);
});
testWidgets('copy block link', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// open getting started page
await tester.openPage(Constants.gettingStartedPageName);
// hover and click on the option menu button beside the block component.
await tester.editor.hoverAndClickOptionMenuButton([0]);
// click the copy link to block option
await tester.tap(
find.findTextInFlowyText(
LocaleKeys.document_plugins_optionAction_copyLinkToBlock.tr(),
),
);
await tester.pumpAndSettle(Durations.short1);
// check the clipboard
final content = await Clipboard.getData(Clipboard.kTextPlain);
expect(
content?.text,
matches(
r'^https:\/\/appflowy\.com\/app\/[a-f0-9-]{36}\/[a-f0-9-]{36}\?blockId=[A-Za-z0-9_-]+$',
),
);
});
});
}

View File

@ -0,0 +1,76 @@
// ignore_for_file: unused_import
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/plugins/shared/share/constants.dart';
import 'package:appflowy/plugins/shared/share/share_menu.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../../../shared/constants.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
import '../../../shared/workspace.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Share menu:', () {
testWidgets('share tab', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
const pageName = 'Document';
await tester.createNewPageInSpace(
spaceName: Constants.generalSpaceName,
layout: ViewLayoutPB.Document,
pageName: pageName,
);
// click the share button
await tester.tapShareButton();
// expect the share menu is shown
final shareMenu = find.byType(ShareMenu);
expect(shareMenu, findsOneWidget);
// click the copy link button
final copyLinkButton = find.textContaining(
LocaleKeys.button_copyLink.tr(),
);
await tester.tapButton(copyLinkButton);
// read the clipboard content
final clipboardContent = await getIt<ClipboardService>().getData();
final plainText = clipboardContent.plainText;
expect(
plainText,
startsWith(ShareConstants.shareBaseUrl),
);
final shareValues = plainText!
.replaceAll('${ShareConstants.shareBaseUrl}/', '')
.split('/');
final workspaceId = shareValues[0];
expect(workspaceId, isNotEmpty);
final pageId = shareValues[1];
expect(pageId, isNotEmpty);
});
});
}

View File

@ -0,0 +1,15 @@
import 'package:integration_test/integration_test.dart';
import 'change_name_and_icon_test.dart' as change_name_and_icon_test;
import 'collaborative_workspace_test.dart' as collaborative_workspace_test;
import 'share_menu_test.dart' as share_menu_test;
import 'workspace_settings_test.dart' as workspace_settings_test;
void startTesting() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
workspace_settings_test.main();
share_menu_test.main();
collaborative_workspace_test.main();
change_name_and_icon_test.main();
}

View File

@ -1,6 +1,3 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart';
@ -16,6 +13,8 @@ import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:easy_localization/easy_localization.dart' hide TextDirection;
import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:universal_platform/universal_platform.dart';
@ -29,7 +28,13 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
String Function(Node)? placeholderText,
EdgeInsets? customHeadingPadding,
}) {
final standardActions = [OptionAction.delete, OptionAction.duplicate];
final standardActions = [
OptionAction.delete,
OptionAction.duplicate,
// filter out the copy link to block option if in local mode
if (context.read<DocumentBloc?>()?.isLocalMode != true)
OptionAction.copyLinkToBlock,
];
final calloutBGColor = AFThemeExtension.of(context).calloutBGColor;
final configuration = BlockComponentConfiguration(

View File

@ -1,19 +1,25 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option_action.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart';
import 'package:appflowy/plugins/shared/share/constants.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.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/style_widget/snap_bar.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:toastification/toastification.dart';
import 'drag_to_reorder/draggable_option_button.dart';
@ -121,6 +127,9 @@ class _BlockOptionButtonState extends State<BlockOptionButton> {
case OptionAction.moveDown:
transaction.moveNode(node.path.next.next, node);
break;
case OptionAction.copyLinkToBlock:
await _copyLinkToBlock(context, node);
break;
case OptionAction.align:
case OptionAction.color:
case OptionAction.divider:
@ -234,6 +243,44 @@ class _BlockOptionButtonState extends State<BlockOptionButton> {
);
}
Future<void> _copyLinkToBlock(BuildContext context, Node node) async {
final viewId = context.read<DocumentBloc>().documentId;
final workspace = await FolderEventReadCurrentWorkspace().send();
final workspaceId = workspace.fold(
(l) => l.id,
(r) => '',
);
if (workspaceId.isEmpty || viewId.isEmpty) {
Log.error('Failed to get workspace id: $workspaceId or view id: $viewId');
if (context.mounted) {
showToastNotification(
context,
message: LocaleKeys.shareAction_copyLinkToBlockFailed.tr(),
type: ToastificationType.error,
);
}
return;
}
final link = ShareConstants.buildShareUrl(
workspaceId: workspaceId,
viewId: viewId,
blockId: node.id,
);
await getIt<ClipboardService>().setData(
ClipboardServiceData(plainText: link),
);
if (context.mounted) {
showToastNotification(
context,
message: LocaleKeys.shareAction_copyLinkToBlockSuccess.tr(),
);
}
}
/// Handles duplicating a SubPage.
///
/// If the duplication fails for any reason, this method will return false, and inserting

View File

@ -18,6 +18,7 @@ enum OptionAction {
turnInto,
moveUp,
moveDown,
copyLinkToBlock,
/// callout background color
color,
@ -28,7 +29,7 @@ enum OptionAction {
FlowySvgData get svg {
switch (this) {
case OptionAction.delete:
return FlowySvgs.delete_s;
return FlowySvgs.trash_s;
case OptionAction.duplicate:
return FlowySvgs.copy_s;
case OptionAction.turnInto:
@ -45,6 +46,8 @@ enum OptionAction {
return FlowySvgs.m_aa_bulleted_list_s;
case OptionAction.depth:
return FlowySvgs.tag_s;
case OptionAction.copyLinkToBlock:
return FlowySvgs.share_tab_copy_s;
}
}
@ -66,6 +69,8 @@ enum OptionAction {
return LocaleKeys.document_plugins_optionAction_align.tr();
case OptionAction.depth:
return LocaleKeys.document_plugins_optionAction_depth.tr();
case OptionAction.copyLinkToBlock:
return LocaleKeys.document_plugins_optionAction_copyLinkToBlock.tr();
case OptionAction.divider:
throw UnsupportedError('Divider does not have description');
}
@ -142,9 +147,12 @@ enum OptionDepthType {
class DividerOptionAction extends CustomActionCell {
@override
Widget buildWithContext(BuildContext context, PopoverController controller) {
return const Divider(
height: 1.0,
thickness: 1.0,
return const Padding(
padding: EdgeInsets.symmetric(vertical: 4.0),
child: Divider(
height: 1.0,
thickness: 1.0,
),
);
}
}

View File

@ -0,0 +1,23 @@
class ShareConstants {
static const String publishBaseUrl = 'https://appflowy.com';
static const String shareBaseUrl = 'https://appflowy.com/app';
static String buildPublishUrl({
required String nameSpace,
required String publishName,
}) {
return '$publishBaseUrl/$nameSpace/$publishName';
}
static String buildShareUrl({
required String workspaceId,
required String viewId,
String? blockId,
}) {
final url = '$shareBaseUrl/$workspaceId/$viewId';
if (blockId == null || blockId.isEmpty) {
return url;
}
return '$url?blockId=$blockId';
}
}

View File

@ -13,9 +13,9 @@ import 'package:appflowy_result/appflowy_result.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'share_bloc.freezed.dart';
import 'constants.dart';
const _url = 'https://appflowy.com';
part 'share_bloc.freezed.dart';
class ShareBloc extends Bloc<ShareEvent, ShareState> {
ShareBloc({
@ -27,7 +27,7 @@ class ShareBloc extends Bloc<ShareEvent, ShareState> {
viewListener = ViewListener(viewId: view.id)
..start(
onViewUpdated: (value) {
add(ShareEvent.updateViewName(value.name));
add(ShareEvent.updateViewName(value.name, value.id));
},
onViewMoveToTrash: (p0) {
add(const ShareEvent.setPublishStatus(false));
@ -70,7 +70,10 @@ class ShareBloc extends Bloc<ShareEvent, ShareState> {
isPublished: true,
publishResult: FlowySuccess(null),
unpublishResult: null,
url: '$_url/${result.namespace}/$publishName',
url: ShareConstants.buildPublishUrl(
nameSpace: result.namespace,
publishName: publishName,
),
),
);
@ -113,8 +116,8 @@ class ShareBloc extends Bloc<ShareEvent, ShareState> {
),
);
},
updateViewName: (viewName) async {
emit(state.copyWith(viewName: viewName));
updateViewName: (viewName, viewId) async {
emit(state.copyWith(viewName: viewName, viewId: viewId));
},
setPublishStatus: (isPublished) {
emit(
@ -131,13 +134,23 @@ class ShareBloc extends Bloc<ShareEvent, ShareState> {
(v) => v.authenticator == AuthenticatorPB.AppFlowyCloud,
(p) => false,
);
String workspaceId = state.workspaceId;
if (workspaceId.isEmpty) {
workspaceId = await UserBackendService.getCurrentWorkspace()
.fold((s) => s.id, (f) => '');
}
publishInfo.fold((s) {
emit(
state.copyWith(
isPublished: true,
url: '$_url/${s.namespace}/${s.publishName}',
url: ShareConstants.buildPublishUrl(
nameSpace: s.namespace,
publishName: s.publishName,
),
viewName: view.name,
enablePublish: enablePublish,
workspaceId: workspaceId,
viewId: view.id,
),
);
}, (f) {
@ -147,6 +160,8 @@ class ShareBloc extends Bloc<ShareEvent, ShareState> {
url: '',
viewName: view.name,
enablePublish: enablePublish,
workspaceId: workspaceId,
viewId: view.id,
),
);
});
@ -261,7 +276,8 @@ class ShareEvent with _$ShareEvent {
List<String> selectedViewIds,
) = _Publish;
const factory ShareEvent.unPublish() = _UnPublish;
const factory ShareEvent.updateViewName(String name) = _UpdateViewName;
const factory ShareEvent.updateViewName(String name, String viewId) =
_UpdateViewName;
const factory ShareEvent.updatePublishStatus() = _UpdatePublishStatus;
const factory ShareEvent.setPublishStatus(bool isPublished) =
_SetPublishStatus;
@ -278,6 +294,8 @@ class ShareState with _$ShareState {
FlowyResult<ShareType, FlowyError>? exportResult,
FlowyResult<void, FlowyError>? publishResult,
FlowyResult<void, FlowyError>? unpublishResult,
required String viewId,
required String workspaceId,
}) = _ShareState;
factory ShareState.initial() => const ShareState(
@ -286,5 +304,7 @@ class ShareState with _$ShareState {
enablePublish: true,
url: '',
viewName: '',
viewId: '',
workspaceId: '',
);
}

View File

@ -46,7 +46,11 @@ class ShareButton extends StatelessWidget {
child: BlocBuilder<ShareBloc, ShareState>(
builder: (context, state) {
final tabs = [
if (state.enablePublish) ShareMenuTab.publish,
if (state.enablePublish) ...[
// share the same permission with publish
ShareMenuTab.share,
ShareMenuTab.publish,
],
ShareMenuTab.exportAs,
];

View File

@ -3,6 +3,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/home/tab/_round_underline_tab_indicator.dart';
import 'package:appflowy/plugins/shared/share/export_tab.dart';
import 'package:appflowy/plugins/shared/share/share_bloc.dart';
import 'package:appflowy/plugins/shared/share/share_tab.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@ -121,10 +122,8 @@ class _ShareMenuState extends State<ShareMenu>
return const PublishTab();
case ShareMenuTab.exportAs:
return const ExportTab();
default:
return const Center(
child: FlowyText('🏡 under construction'),
);
case ShareMenuTab.share:
return const ShareTab();
}
}
}

View File

@ -0,0 +1,124 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/plugins/shared/share/share_bloc.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'constants.dart';
class ShareTab extends StatelessWidget {
const ShareTab({
super.key,
});
@override
Widget build(BuildContext context) {
return const Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
VSpace(18),
_ShareTabHeader(),
VSpace(2),
_ShareTabDescription(),
VSpace(14),
_ShareTabContent(),
],
);
}
}
class _ShareTabHeader extends StatelessWidget {
const _ShareTabHeader();
@override
Widget build(BuildContext context) {
return Row(
children: [
const FlowySvg(FlowySvgs.share_tab_icon_s),
const HSpace(6),
FlowyText.medium(
LocaleKeys.shareAction_shareTabTitle.tr(),
figmaLineHeight: 18.0,
),
],
);
}
}
class _ShareTabDescription extends StatelessWidget {
const _ShareTabDescription();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 2.0),
child: FlowyText.regular(
LocaleKeys.shareAction_shareTabDescription.tr(),
fontSize: 13.0,
figmaLineHeight: 18.0,
color: Theme.of(context).hintColor,
),
);
}
}
class _ShareTabContent extends StatelessWidget {
const _ShareTabContent();
@override
Widget build(BuildContext context) {
return BlocBuilder<ShareBloc, ShareState>(
builder: (context, state) {
final shareUrl = ShareConstants.buildShareUrl(
workspaceId: state.workspaceId,
viewId: state.viewId,
);
return Row(
children: [
Expanded(
child: SizedBox(
height: 36,
child: FlowyTextField(
text: shareUrl, // todo: add workspace id + view id
readOnly: true,
borderRadius: BorderRadius.circular(10),
),
),
),
const HSpace(8.0),
PrimaryRoundedButton(
margin: const EdgeInsets.symmetric(
vertical: 9.0,
horizontal: 14.0,
),
text: LocaleKeys.button_copyLink.tr(),
figmaLineHeight: 18.0,
leftIcon: FlowySvg(
FlowySvgs.share_tab_copy_s,
color: Theme.of(context).colorScheme.onPrimary,
),
onTap: () => _copy(context, shareUrl),
),
],
);
},
);
}
void _copy(BuildContext context, String url) {
getIt<ClipboardService>().setData(
ClipboardServiceData(plainText: url),
);
showToastNotification(
context,
message: LocaleKeys.grid_url_copy.tr(),
);
}
}

View File

@ -17,6 +17,7 @@ class PrimaryRoundedButton extends StatelessWidget {
this.useIntrinsicWidth = true,
this.lineHeight,
this.figmaLineHeight,
this.leftIcon,
});
final String text;
@ -31,11 +32,13 @@ class PrimaryRoundedButton extends StatelessWidget {
final bool useIntrinsicWidth;
final double? lineHeight;
final double? figmaLineHeight;
final Widget? leftIcon;
@override
Widget build(BuildContext context) {
return FlowyButton(
useIntrinsicWidth: useIntrinsicWidth,
leftIcon: leftIcon,
text: FlowyText(
text,
fontSize: fontSize ?? 14.0,

View File

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.07617 4.30773L8.42791 2.95589C9.70243 1.68137 11.7688 1.68137 13.0434 2.95589C14.3179 4.23042 14.3179 6.29682 13.0434 7.57135L11.6916 8.92318" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.92308 11.6924L7.57135 13.0442C6.29682 14.3187 4.23042 14.3187 2.9559 13.0442C1.68137 11.7697 1.68137 9.70326 2.9559 8.42873L4.30762 7.0769" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.84666 6.15381L6.1543 9.84617" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 701 B

View File

@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.10573 7.24659C6.03906 7.23992 5.95906 7.23992 5.88573 7.24659C4.29906 7.19325 3.03906 5.89325 3.03906 4.29325C3.03906 2.65992 4.35906 1.33325 5.99906 1.33325C7.6324 1.33325 8.95906 2.65992 8.95906 4.29325C8.9524 5.89325 7.6924 7.19325 6.10573 7.24659Z" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.9402 2.66675C12.2335 2.66675 13.2735 3.71341 13.2735 5.00008C13.2735 6.26008 12.2735 7.28675 11.0268 7.33341C10.9735 7.32675 10.9135 7.32675 10.8535 7.33341" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.7725 9.70675C1.15917 10.7867 1.15917 12.5467 2.7725 13.6201C4.60583 14.8467 7.6125 14.8467 9.44583 13.6201C11.0592 12.5401 11.0592 10.7801 9.44583 9.70675C7.61917 8.48675 4.6125 8.48675 2.7725 9.70675Z" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.2266 13.3333C12.7066 13.2333 13.1599 13.0399 13.5332 12.7533C14.5732 11.9733 14.5732 10.6866 13.5332 9.90659C13.1666 9.62659 12.7199 9.43992 12.2466 9.33325" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -121,7 +121,11 @@
"exportAsTab": "Export as",
"publishTab": "Publish",
"shareTab": "Share",
"publishOnAppFlowy": "Publish on AppFlowy"
"publishOnAppFlowy": "Publish on AppFlowy",
"shareTabTitle": "Invite to collaborate",
"shareTabDescription": "For easy collaboration with anyone",
"copyLinkToBlockSuccess": "Copied block link to clipboard",
"copyLinkToBlockFailed": "Failed to copy block link to clipboard"
},
"moreAction": {
"small": "small",
@ -1665,7 +1669,8 @@
"center": "Center",
"right": "Right",
"defaultColor": "Default",
"depth": "Depth"
"depth": "Depth",
"copyLinkToBlock": "Copy link to block"
},
"image": {
"addAnImage": "Add images",