mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: improve editing link user experience (#3990)
This commit is contained in:
parent
0427402ba7
commit
8afbf28430
@ -5,13 +5,13 @@ import 'package:flutter/material.dart';
|
||||
class BottomSheetActionWidget extends StatelessWidget {
|
||||
const BottomSheetActionWidget({
|
||||
super.key,
|
||||
required this.svg,
|
||||
this.svg,
|
||||
required this.text,
|
||||
required this.onTap,
|
||||
this.iconColor,
|
||||
});
|
||||
|
||||
final FlowySvgData svg;
|
||||
final FlowySvgData? svg;
|
||||
final String text;
|
||||
final VoidCallback onTap;
|
||||
final Color? iconColor;
|
||||
@ -21,9 +21,23 @@ class BottomSheetActionWidget extends StatelessWidget {
|
||||
final iconColor =
|
||||
this.iconColor ?? Theme.of(context).colorScheme.onBackground;
|
||||
|
||||
if (svg == null) {
|
||||
return OutlinedButton(
|
||||
style: Theme.of(context)
|
||||
.outlinedButtonTheme
|
||||
.style
|
||||
?.copyWith(alignment: Alignment.center),
|
||||
onPressed: onTap,
|
||||
child: FlowyText(
|
||||
text,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return OutlinedButton.icon(
|
||||
icon: FlowySvg(
|
||||
svg,
|
||||
svg!,
|
||||
size: const Size.square(22.0),
|
||||
color: iconColor,
|
||||
),
|
||||
|
@ -0,0 +1,127 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.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:string_validator/string_validator.dart';
|
||||
|
||||
class MobileBottomSheetEditLinkWidget extends StatefulWidget {
|
||||
const MobileBottomSheetEditLinkWidget({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.href,
|
||||
required this.onEdit,
|
||||
});
|
||||
|
||||
final String text;
|
||||
final String? href;
|
||||
final void Function(String text, String href) onEdit;
|
||||
|
||||
@override
|
||||
State<MobileBottomSheetEditLinkWidget> createState() =>
|
||||
_MobileBottomSheetEditLinkWidgetState();
|
||||
}
|
||||
|
||||
class _MobileBottomSheetEditLinkWidgetState
|
||||
extends State<MobileBottomSheetEditLinkWidget> {
|
||||
late final TextEditingController textController;
|
||||
late final TextEditingController hrefController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
textController = TextEditingController(
|
||||
text: widget.text,
|
||||
);
|
||||
hrefController = TextEditingController(
|
||||
text: widget.href,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
textController.dispose();
|
||||
hrefController.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4.0,
|
||||
vertical: 16.0,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildTextField(textController, null),
|
||||
const VSpace(12.0),
|
||||
_buildTextField(hrefController, LocaleKeys.editor_linkTextHint.tr()),
|
||||
const VSpace(12.0),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: BottomSheetActionWidget(
|
||||
text: LocaleKeys.button_cancel.tr(),
|
||||
onTap: () => context.pop(),
|
||||
),
|
||||
),
|
||||
const HSpace(8),
|
||||
Expanded(
|
||||
child: BottomSheetActionWidget(
|
||||
text: LocaleKeys.button_done.tr(),
|
||||
onTap: () {
|
||||
widget.onEdit(textController.text, hrefController.text);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (widget.href != null && isURL(widget.href)) ...[
|
||||
const HSpace(8),
|
||||
Expanded(
|
||||
child: BottomSheetActionWidget(
|
||||
text: LocaleKeys.editor_openLink.tr(),
|
||||
onTap: () {
|
||||
safeLaunchUrl(widget.href!);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextField(
|
||||
TextEditingController controller,
|
||||
String? hintText,
|
||||
) {
|
||||
return SizedBox(
|
||||
height: 44.0,
|
||||
child: FlowyTextField(
|
||||
controller: controller,
|
||||
hintText: hintText,
|
||||
suffixIcon: Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: FlowyButton(
|
||||
text: const FlowySvg(
|
||||
FlowySvgs.close_lg,
|
||||
),
|
||||
margin: EdgeInsets.zero,
|
||||
useIntrinsicWidth: true,
|
||||
onTap: () {
|
||||
controller.clear();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -15,7 +15,6 @@ List<MobileToolbarItem> getMobileToolbarItems() {
|
||||
buildTextAndBackgroundColorMobileToolbarItem(),
|
||||
mobileAddBlockToolbarItem,
|
||||
mobileConvertBlockToolbarItem,
|
||||
linkMobileToolbarItem,
|
||||
imageMobileToolbarItem,
|
||||
mobileAlignToolbarItem,
|
||||
mobileIndentToolbarItem,
|
||||
|
@ -1,19 +1,25 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/utils.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
final customTextDecorationMobileToolbarItem = MobileToolbarItem.withMenu(
|
||||
itemIconBuilder: (_, __, ___) => const FlowySvg(
|
||||
FlowySvgs.text_s,
|
||||
size: Size.square(24),
|
||||
),
|
||||
itemMenuBuilder: (_, editorState, __) {
|
||||
itemMenuBuilder: (_, editorState, service) {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return _TextDecorationMenu(editorState, selection);
|
||||
return _TextDecorationMenu(
|
||||
editorState,
|
||||
selection,
|
||||
service,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@ -21,16 +27,20 @@ class _TextDecorationMenu extends StatefulWidget {
|
||||
const _TextDecorationMenu(
|
||||
this.editorState,
|
||||
this.selection,
|
||||
this.service,
|
||||
);
|
||||
|
||||
final EditorState editorState;
|
||||
final Selection selection;
|
||||
final MobileToolbarWidgetService service;
|
||||
|
||||
@override
|
||||
State<_TextDecorationMenu> createState() => _TextDecorationMenuState();
|
||||
}
|
||||
|
||||
class _TextDecorationMenuState extends State<_TextDecorationMenu> {
|
||||
EditorState get editorState => widget.editorState;
|
||||
|
||||
final textDecorations = [
|
||||
// BIUS
|
||||
TextDecorationUnit(
|
||||
@ -60,41 +70,90 @@ class _TextDecorationMenuState extends State<_TextDecorationMenu> {
|
||||
label: AppFlowyEditorL10n.current.embedCode,
|
||||
name: AppFlowyRichTextKeys.code,
|
||||
),
|
||||
|
||||
// link
|
||||
TextDecorationUnit(
|
||||
icon: AFMobileIcons.link,
|
||||
label: AppFlowyEditorL10n.current.link,
|
||||
name: AppFlowyRichTextKeys.href,
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bius = textDecorations.map((currentDecoration) {
|
||||
// Check current decoration is active or not
|
||||
final selection = widget.selection;
|
||||
final nodes = widget.editorState.getNodesInSelection(selection);
|
||||
final bool isSelected;
|
||||
if (selection.isCollapsed) {
|
||||
isSelected = widget.editorState.toggledStyle.containsKey(
|
||||
currentDecoration.name,
|
||||
);
|
||||
} else {
|
||||
isSelected = nodes.allSatisfyInSelection(selection, (delta) {
|
||||
return delta.everyAttributes(
|
||||
(attributes) => attributes[currentDecoration.name] == true,
|
||||
);
|
||||
});
|
||||
}
|
||||
final children = textDecorations
|
||||
.map((currentDecoration) {
|
||||
// Check current decoration is active or not
|
||||
final selection = widget.selection;
|
||||
|
||||
return MobileToolbarItemMenuBtn(
|
||||
icon: AFMobileIcon(
|
||||
afMobileIcons: currentDecoration.icon,
|
||||
color: MobileToolbarTheme.of(context).iconColor,
|
||||
),
|
||||
label: FlowyText(currentDecoration.label),
|
||||
isSelected: isSelected,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
widget.editorState.toggleAttribute(currentDecoration.name);
|
||||
});
|
||||
},
|
||||
);
|
||||
}).toList();
|
||||
// only show edit link bottom sheet when selection is not collapsed
|
||||
if (selection.isCollapsed &&
|
||||
currentDecoration.name == AppFlowyRichTextKeys.href) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final nodes = editorState.getNodesInSelection(selection);
|
||||
final bool isSelected;
|
||||
if (selection.isCollapsed) {
|
||||
isSelected = editorState.toggledStyle.containsKey(
|
||||
currentDecoration.name,
|
||||
);
|
||||
} else {
|
||||
isSelected = nodes.allSatisfyInSelection(selection, (delta) {
|
||||
return delta.everyAttributes(
|
||||
(attributes) => attributes[currentDecoration.name] == true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return MobileToolbarItemMenuBtn(
|
||||
icon: AFMobileIcon(
|
||||
afMobileIcons: currentDecoration.icon,
|
||||
color: MobileToolbarTheme.of(context).iconColor,
|
||||
),
|
||||
label: FlowyText(currentDecoration.label),
|
||||
isSelected: isSelected,
|
||||
onPressed: () {
|
||||
if (currentDecoration.name == AppFlowyRichTextKeys.href) {
|
||||
if (selection.isCollapsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
_closeKeyboard();
|
||||
|
||||
// show edit link bottom sheet
|
||||
final context = nodes.firstOrNull?.context;
|
||||
if (context != null) {
|
||||
final text = editorState
|
||||
.getTextInSelection(
|
||||
widget.selection,
|
||||
)
|
||||
.join('');
|
||||
final href =
|
||||
editorState.getDeltaAttributeValueInSelection<String>(
|
||||
AppFlowyRichTextKeys.href,
|
||||
widget.selection,
|
||||
);
|
||||
showEditLinkBottomSheet(
|
||||
context,
|
||||
text,
|
||||
href,
|
||||
(context, newText, newHref) {
|
||||
_updateTextAndHref(text, href, newText, newHref);
|
||||
context.pop();
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
setState(() {
|
||||
editorState.toggleAttribute(currentDecoration.name);
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
})
|
||||
.nonNulls
|
||||
.toList();
|
||||
|
||||
return GridView.count(
|
||||
shrinkWrap: true,
|
||||
@ -103,7 +162,49 @@ class _TextDecorationMenuState extends State<_TextDecorationMenu> {
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
childAspectRatio: 4,
|
||||
children: bius,
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
|
||||
void _closeKeyboard() {
|
||||
editorState.updateSelectionWithReason(
|
||||
widget.selection,
|
||||
extraInfo: {
|
||||
disableMobileToolbarKey: true,
|
||||
},
|
||||
);
|
||||
editorState.service.keyboardService?.closeKeyboard();
|
||||
}
|
||||
|
||||
void _updateTextAndHref(
|
||||
String prevText,
|
||||
String? prevHref,
|
||||
String text,
|
||||
String href,
|
||||
) async {
|
||||
final selection = widget.selection;
|
||||
if (!selection.isSingle) {
|
||||
return;
|
||||
}
|
||||
final node = editorState.getNodeAtPath(selection.start.path);
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
final transaction = editorState.transaction;
|
||||
if (prevText != text) {
|
||||
transaction.replaceText(
|
||||
node,
|
||||
selection.startIndex,
|
||||
selection.length,
|
||||
text,
|
||||
);
|
||||
}
|
||||
// if the text is empty, it means the user wants to remove the text
|
||||
if (text.isNotEmpty && prevHref != href) {
|
||||
transaction.formatText(node, selection.startIndex, text.length, {
|
||||
AppFlowyRichTextKeys.href: href.isEmpty ? null : href,
|
||||
});
|
||||
}
|
||||
await editorState.apply(transaction);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,25 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_edit_link_widget.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void showEditLinkBottomSheet(
|
||||
BuildContext context,
|
||||
String text,
|
||||
String? href,
|
||||
void Function(BuildContext context, String text, String href) onEdit,
|
||||
) {
|
||||
assert(text.isNotEmpty);
|
||||
showFlowyMobileBottomSheet(
|
||||
context,
|
||||
title: LocaleKeys.editor_editLink.tr(),
|
||||
builder: (context) {
|
||||
return MobileBottomSheetEditLinkWidget(
|
||||
text: text,
|
||||
href: href,
|
||||
onEdit: (text, href) => onEdit(context, text, href),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
@ -2,6 +2,7 @@ import 'dart:math';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/utils.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';
|
||||
import 'package:appflowy/util/google_font_family_extension.dart';
|
||||
@ -10,6 +11,7 @@ import 'package:collection/collection.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class EditorStyleCustomizer {
|
||||
@ -276,7 +278,23 @@ class EditorStyleCustomizer {
|
||||
text: text.text,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
safeLaunchUrl(href);
|
||||
showEditLinkBottomSheet(
|
||||
context,
|
||||
text.text,
|
||||
href,
|
||||
(linkContext, newText, newHref) {
|
||||
_updateTextAndHref(
|
||||
context,
|
||||
node,
|
||||
index,
|
||||
text.text,
|
||||
href,
|
||||
newText,
|
||||
newHref,
|
||||
);
|
||||
linkContext.pop();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -289,4 +307,37 @@ class EditorStyleCustomizer {
|
||||
textSpan,
|
||||
);
|
||||
}
|
||||
|
||||
void _updateTextAndHref(
|
||||
BuildContext context,
|
||||
Node node,
|
||||
int index,
|
||||
String prevText,
|
||||
String? prevHref,
|
||||
String text,
|
||||
String href,
|
||||
) async {
|
||||
final selection = Selection.single(
|
||||
path: node.path,
|
||||
startOffset: index,
|
||||
endOffset: index + prevText.length,
|
||||
);
|
||||
final editorState = context.read<EditorState>();
|
||||
final transaction = editorState.transaction;
|
||||
if (prevText != text) {
|
||||
transaction.replaceText(
|
||||
node,
|
||||
selection.startIndex,
|
||||
selection.length,
|
||||
text,
|
||||
);
|
||||
}
|
||||
// if the text is empty, it means the user wants to remove the text
|
||||
if (text.isNotEmpty && prevHref != href) {
|
||||
transaction.formatText(node, selection.startIndex, text.length, {
|
||||
AppFlowyRichTextKeys.href: href.isEmpty ? null : href,
|
||||
});
|
||||
}
|
||||
await editorState.apply(transaction);
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
int64_t init_sdk(char *path);
|
||||
int64_t init_sdk(char *data);
|
||||
|
||||
void async_event(int64_t port, const uint8_t *input, uintptr_t len);
|
||||
|
||||
|
@ -54,11 +54,11 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "4f073f3"
|
||||
resolved-ref: "4f073f3381a05a2379144f282c6f65462c4ce9c6"
|
||||
ref: "31acaff"
|
||||
resolved-ref: "31acaff4f0bcd36a2946c9351dc1b5b5601b3994"
|
||||
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
||||
source: git
|
||||
version: "2.0.0-beta.1"
|
||||
version: "2.0.0"
|
||||
appflowy_popover:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1114,6 +1114,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
numerus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: numerus
|
||||
sha256: ada1b55e4a505b8e4578218e57be1ce4b7968bb0e51d824082ac0c74ef18a10d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
octo_image:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -47,7 +47,7 @@ dependencies:
|
||||
appflowy_editor:
|
||||
git:
|
||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||
ref: 'ec3ecf5'
|
||||
ref: '31acaff'
|
||||
|
||||
appflowy_popover:
|
||||
path: packages/appflowy_popover
|
||||
|
Loading…
Reference in New Issue
Block a user