mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: keep the toolbar the same height as the keyboard to optimize the editing experience (#3947)
This commit is contained in:
@ -12,9 +12,11 @@ class FlowyEmojiPicker extends StatefulWidget {
|
||||
const FlowyEmojiPicker({
|
||||
super.key,
|
||||
required this.onEmojiSelected,
|
||||
this.emojiPerLine = 9,
|
||||
});
|
||||
|
||||
final EmojiSelectedCallback onEmojiSelected;
|
||||
final int emojiPerLine;
|
||||
|
||||
@override
|
||||
State<FlowyEmojiPicker> createState() => _FlowyEmojiPickerState();
|
||||
@ -61,6 +63,7 @@ class _FlowyEmojiPickerState extends State<FlowyEmojiPicker> {
|
||||
showSectionHeader: true,
|
||||
showTabs: false,
|
||||
defaultSkinTone: lastSelectedEmojiSkinTone ?? EmojiSkinTone.none,
|
||||
perLine: widget.emojiPerLine,
|
||||
),
|
||||
onEmojiSelected: widget.onEmojiSelected,
|
||||
headerBuilder: (context, category) {
|
||||
|
@ -5,14 +5,19 @@ import 'package:go_router/go_router.dart';
|
||||
|
||||
class MobileEmojiPickerScreen extends StatelessWidget {
|
||||
static const routeName = '/emoji_picker';
|
||||
static const pageTitle = 'title';
|
||||
|
||||
const MobileEmojiPickerScreen({
|
||||
super.key,
|
||||
this.title,
|
||||
});
|
||||
|
||||
final String? title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconPickerPage(
|
||||
title: title,
|
||||
onSelected: (result) {
|
||||
context.pop<EmojiPickerResult>(result);
|
||||
},
|
||||
|
@ -105,10 +105,12 @@ class _SearchTextField extends StatefulWidget {
|
||||
|
||||
class _SearchTextFieldState extends State<_SearchTextField> {
|
||||
final TextEditingController controller = TextEditingController();
|
||||
final FocusNode focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
focusNode.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
@ -120,7 +122,7 @@ class _SearchTextFieldState extends State<_SearchTextField> {
|
||||
maxHeight: 32.0,
|
||||
),
|
||||
child: FlowyTextField(
|
||||
autoFocus: true,
|
||||
focusNode: focusNode,
|
||||
hintText: LocaleKeys.emoji_search.tr(),
|
||||
controller: controller,
|
||||
onChanged: widget.onKeywordChanged,
|
||||
@ -145,8 +147,12 @@ class _SearchTextFieldState extends State<_SearchTextField> {
|
||||
margin: EdgeInsets.zero,
|
||||
useIntrinsicWidth: true,
|
||||
onTap: () {
|
||||
controller.clear();
|
||||
widget.onKeywordChanged('');
|
||||
if (controller.text.isNotEmpty) {
|
||||
controller.clear();
|
||||
widget.onKeywordChanged('');
|
||||
} else {
|
||||
focusNode.unfocus();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
|
@ -72,6 +72,7 @@ class _FlowyIconPickerState extends State<FlowyIconPicker>
|
||||
child: TabBarView(
|
||||
children: [
|
||||
FlowyEmojiPicker(
|
||||
emojiPerLine: _getEmojiPerLine(),
|
||||
onEmojiSelected: (_, emoji) {
|
||||
widget.onSelected(
|
||||
EmojiPickerResult(
|
||||
@ -116,6 +117,11 @@ class _FlowyIconPickerState extends State<FlowyIconPicker>
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
int _getEmojiPerLine() {
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
return width ~/ 46.0; // the size of the emoji
|
||||
}
|
||||
}
|
||||
|
||||
class _RemoveIconButton extends StatelessWidget {
|
||||
|
@ -6,26 +6,23 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class IconPickerPage extends StatefulWidget {
|
||||
class IconPickerPage extends StatelessWidget {
|
||||
const IconPickerPage({
|
||||
super.key,
|
||||
this.title,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
final void Function(EmojiPickerResult) onSelected;
|
||||
final String? title;
|
||||
|
||||
@override
|
||||
State<IconPickerPage> createState() => _IconPickerPageState();
|
||||
}
|
||||
|
||||
class _IconPickerPageState extends State<IconPickerPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
titleSpacing: 0,
|
||||
title: FlowyText.semibold(
|
||||
LocaleKeys.titleBar_pageIcon.tr(),
|
||||
title ?? LocaleKeys.titleBar_pageIcon.tr(),
|
||||
fontSize: 14.0,
|
||||
),
|
||||
leading: AppBarBackButton(
|
||||
@ -34,7 +31,7 @@ class _IconPickerPageState extends State<IconPickerPage> {
|
||||
),
|
||||
body: SafeArea(
|
||||
child: FlowyIconPicker(
|
||||
onSelected: widget.onSelected,
|
||||
onSelected: onSelected,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -255,7 +255,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () async {
|
||||
// if the last one isn't a empty node, insert a new empty node.
|
||||
await _ensureLastNodeIsEmptyParagraph();
|
||||
await _focusOnLastEmptyParagraph();
|
||||
},
|
||||
child: VSpace(PlatformExtension.isDesktopOrWeb ? 200 : 400),
|
||||
),
|
||||
@ -266,51 +266,49 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
_setInitialSelection(editorScrollController);
|
||||
|
||||
if (PlatformExtension.isMobile) {
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: MobileFloatingToolbar(
|
||||
editorState: editorState,
|
||||
editorScrollController: editorScrollController,
|
||||
toolbarBuilder: (context, anchor, closeToolbar) {
|
||||
return AdaptiveTextSelectionToolbar.editable(
|
||||
clipboardStatus: ClipboardStatus.pasteable,
|
||||
onCopy: () {
|
||||
copyCommand.execute(editorState);
|
||||
closeToolbar();
|
||||
},
|
||||
onCut: () => cutCommand.execute(editorState),
|
||||
onPaste: () => pasteCommand.execute(editorState),
|
||||
onSelectAll: () => selectAllCommand.execute(editorState),
|
||||
onLiveTextInput: null,
|
||||
anchors: TextSelectionToolbarAnchors(
|
||||
primaryAnchor: anchor,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: editor,
|
||||
),
|
||||
),
|
||||
MobileToolbar(
|
||||
editorState: editorState,
|
||||
toolbarItems: [
|
||||
textDecorationMobileToolbarItem,
|
||||
buildTextAndBackgroundColorMobileToolbarItem(),
|
||||
headingMobileToolbarItem,
|
||||
mobileBlocksToolbarItem,
|
||||
linkMobileToolbarItem,
|
||||
dividerMobileToolbarItem,
|
||||
imageMobileToolbarItem,
|
||||
mathEquationMobileToolbarItem,
|
||||
codeMobileToolbarItem,
|
||||
mobileAlignToolbarItem,
|
||||
mobileIndentToolbarItem,
|
||||
mobileOutdentToolbarItem,
|
||||
undoMobileToolbarItem,
|
||||
redoMobileToolbarItem,
|
||||
],
|
||||
),
|
||||
return MobileToolbarV2(
|
||||
toolbarHeight: 48.0,
|
||||
editorState: editorState,
|
||||
toolbarItems: [
|
||||
customTextDecorationMobileToolbarItem,
|
||||
buildTextAndBackgroundColorMobileToolbarItem(),
|
||||
mobileAddBlockToolbarItem,
|
||||
mobileConvertBlockToolbarItem,
|
||||
linkMobileToolbarItem,
|
||||
imageMobileToolbarItem,
|
||||
mobileAlignToolbarItem,
|
||||
mobileIndentToolbarItem,
|
||||
mobileOutdentToolbarItem,
|
||||
undoMobileToolbarItem,
|
||||
redoMobileToolbarItem,
|
||||
],
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: MobileFloatingToolbar(
|
||||
editorState: editorState,
|
||||
editorScrollController: editorScrollController,
|
||||
toolbarBuilder: (context, anchor, closeToolbar) {
|
||||
return AdaptiveTextSelectionToolbar.editable(
|
||||
clipboardStatus: ClipboardStatus.pasteable,
|
||||
onCopy: () {
|
||||
copyCommand.execute(editorState);
|
||||
closeToolbar();
|
||||
},
|
||||
onCut: () => cutCommand.execute(editorState),
|
||||
onPaste: () => pasteCommand.execute(editorState),
|
||||
onSelectAll: () => selectAllCommand.execute(editorState),
|
||||
onLiveTextInput: null,
|
||||
anchors: TextSelectionToolbarAnchors(
|
||||
primaryAnchor: anchor,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: editor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -482,19 +480,23 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
AppFlowyEditorL10n.current = EditorI18n();
|
||||
}
|
||||
|
||||
Future<void> _ensureLastNodeIsEmptyParagraph() async {
|
||||
Future<void> _focusOnLastEmptyParagraph() async {
|
||||
final editorState = widget.editorState;
|
||||
final root = editorState.document.root;
|
||||
final lastNode = root.children.lastOrNull;
|
||||
final transaction = editorState.transaction;
|
||||
if (lastNode == null ||
|
||||
lastNode.delta?.isEmpty == false ||
|
||||
lastNode.type != ParagraphBlockKeys.type) {
|
||||
final transaction = editorState.transaction;
|
||||
transaction.insertNode([root.children.length], paragraphNode());
|
||||
transaction.afterSelection = Selection.collapsed(
|
||||
Position(path: [root.children.length]),
|
||||
);
|
||||
await editorState.apply(transaction);
|
||||
} else {
|
||||
transaction.afterSelection = Selection.collapsed(
|
||||
Position(path: lastNode.path),
|
||||
);
|
||||
}
|
||||
await editorState.apply(transaction);
|
||||
}
|
||||
}
|
||||
|
@ -229,7 +229,11 @@ class _DocumentHeaderToolbarState extends State<DocumentHeaderToolbar> {
|
||||
Widget child = Container(
|
||||
alignment: Alignment.bottomLeft,
|
||||
width: double.infinity,
|
||||
padding: EditorStyleCustomizer.documentPadding,
|
||||
padding: PlatformExtension.isDesktopOrWeb
|
||||
? EditorStyleCustomizer.documentPadding
|
||||
: EdgeInsets.symmetric(
|
||||
horizontal: EditorStyleCustomizer.documentPadding.left - 6.0,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 28,
|
||||
child: Row(
|
||||
@ -420,48 +424,50 @@ class DocumentCoverState extends State<DocumentCover> {
|
||||
right: 12,
|
||||
child: Row(
|
||||
children: [
|
||||
RoundedTextButton(
|
||||
onPressed: () {
|
||||
showFlowyMobileBottomSheet(
|
||||
context,
|
||||
title: LocaleKeys.document_plugins_cover_changeCover.tr(),
|
||||
builder: (context) {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 340,
|
||||
minHeight: 80,
|
||||
),
|
||||
child: UploadImageMenu(
|
||||
supportTypes: const [
|
||||
UploadImageType.color,
|
||||
UploadImageType.local,
|
||||
UploadImageType.url,
|
||||
UploadImageType.unsplash,
|
||||
],
|
||||
onSelectedLocalImage: (path) async {
|
||||
context.pop();
|
||||
widget.onCoverChanged(CoverType.file, path);
|
||||
},
|
||||
onSelectedAIImage: (_) {
|
||||
throw UnimplementedError();
|
||||
},
|
||||
onSelectedNetworkImage: (url) async {
|
||||
context.pop();
|
||||
widget.onCoverChanged(CoverType.file, url);
|
||||
},
|
||||
onSelectedColor: (color) {
|
||||
context.pop();
|
||||
widget.onCoverChanged(CoverType.color, color);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
fillColor: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
width: 120,
|
||||
height: 32,
|
||||
title: LocaleKeys.document_plugins_cover_changeCover.tr(),
|
||||
IntrinsicWidth(
|
||||
child: RoundedTextButton(
|
||||
onPressed: () {
|
||||
showFlowyMobileBottomSheet(
|
||||
context,
|
||||
title:
|
||||
LocaleKeys.document_plugins_cover_changeCover.tr(),
|
||||
builder: (context) {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 340,
|
||||
minHeight: 80,
|
||||
),
|
||||
child: UploadImageMenu(
|
||||
supportTypes: const [
|
||||
UploadImageType.color,
|
||||
UploadImageType.local,
|
||||
UploadImageType.url,
|
||||
UploadImageType.unsplash,
|
||||
],
|
||||
onSelectedLocalImage: (path) async {
|
||||
context.pop();
|
||||
widget.onCoverChanged(CoverType.file, path);
|
||||
},
|
||||
onSelectedAIImage: (_) {
|
||||
throw UnimplementedError();
|
||||
},
|
||||
onSelectedNetworkImage: (url) async {
|
||||
context.pop();
|
||||
widget.onCoverChanged(CoverType.file, url);
|
||||
},
|
||||
onSelectedColor: (color) {
|
||||
context.pop();
|
||||
widget.onCoverChanged(CoverType.color, color);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
fillColor: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
height: 32,
|
||||
title: LocaleKeys.document_plugins_cover_changeCover.tr(),
|
||||
),
|
||||
),
|
||||
const HSpace(8.0),
|
||||
SizedBox.square(
|
||||
@ -530,14 +536,16 @@ class DocumentCoverState extends State<DocumentCover> {
|
||||
constraints: BoxConstraints.loose(const Size(380, 450)),
|
||||
margin: EdgeInsets.zero,
|
||||
onClose: () => isPopoverOpen = false,
|
||||
child: RoundedTextButton(
|
||||
onPressed: () => popoverController.show(),
|
||||
hoverColor: Theme.of(context).colorScheme.surface,
|
||||
textColor: Theme.of(context).colorScheme.tertiary,
|
||||
fillColor: Theme.of(context).colorScheme.surface.withOpacity(0.5),
|
||||
width: 120,
|
||||
height: 28,
|
||||
title: LocaleKeys.document_plugins_cover_changeCover.tr(),
|
||||
child: IntrinsicWidth(
|
||||
child: RoundedTextButton(
|
||||
height: 28.0,
|
||||
onPressed: () => popoverController.show(),
|
||||
hoverColor: Theme.of(context).colorScheme.surface,
|
||||
textColor: Theme.of(context).colorScheme.tertiary,
|
||||
fillColor:
|
||||
Theme.of(context).colorScheme.surface.withOpacity(0.5),
|
||||
title: LocaleKeys.document_plugins_cover_changeCover.tr(),
|
||||
),
|
||||
),
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
isPopoverOpen = true;
|
||||
@ -575,7 +583,7 @@ class DeleteCoverButton extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyIconButton(
|
||||
hoverColor: Theme.of(context).colorScheme.surface,
|
||||
fillColor: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fillColor: Theme.of(context).colorScheme.surface.withOpacity(0.5),
|
||||
iconPadding: const EdgeInsets.all(5),
|
||||
width: 28,
|
||||
icon: FlowySvg(
|
||||
|
@ -1,5 +1,7 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@ -22,8 +24,8 @@ class _ImagePickerPageState extends State<ImagePickerPage> {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
titleSpacing: 0,
|
||||
title: const FlowyText.semibold(
|
||||
'Page icon',
|
||||
title: FlowyText.semibold(
|
||||
LocaleKeys.titleBar_pageIcon.tr(),
|
||||
fontSize: 14.0,
|
||||
),
|
||||
leading: AppBarBackButton(
|
||||
|
@ -5,8 +5,8 @@ import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
final imageMobileToolbarItem = MobileToolbarItem.action(
|
||||
itemIconBuilder: (_, __) => const FlowySvg(FlowySvgs.m_toolbar_imae_lg),
|
||||
actionHandler: (editorState, selection) async {
|
||||
itemIconBuilder: (_, __, ___) => const FlowySvg(FlowySvgs.m_toolbar_imae_lg),
|
||||
actionHandler: (_, editorState) async {
|
||||
final imagePlaceholderKey = GlobalKey<ImagePlaceholderState>();
|
||||
await editorState.insertEmptyImageBlock(imagePlaceholderKey);
|
||||
|
||||
|
@ -4,10 +4,13 @@ import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
final mathEquationMobileToolbarItem = MobileToolbarItem.action(
|
||||
itemIconBuilder: (_, __) =>
|
||||
const SizedBox(width: 22, child: FlowySvg(FlowySvgs.math_lg)),
|
||||
actionHandler: (editorState, selection) async {
|
||||
if (!selection.isCollapsed) {
|
||||
itemIconBuilder: (_, __, ___) => const SizedBox(
|
||||
width: 22,
|
||||
child: FlowySvg(FlowySvgs.math_lg),
|
||||
),
|
||||
actionHandler: (_, editorState) async {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null || !selection.isCollapsed) {
|
||||
return;
|
||||
}
|
||||
final path = selection.start.path;
|
||||
|
@ -0,0 +1,327 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_blocks_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// convert the current block to other block types
|
||||
// only show in single selection and text type
|
||||
final mobileAddBlockToolbarItem = MobileToolbarItem.withMenu(
|
||||
itemIconBuilder: (_, editorState, ___) {
|
||||
if (!onlyShowInSingleSelectionAndTextType(editorState)) {
|
||||
return null;
|
||||
}
|
||||
return const FlowySvg(
|
||||
FlowySvgs.add_m,
|
||||
size: Size.square(48),
|
||||
);
|
||||
},
|
||||
itemMenuBuilder: (_, editorState, service) {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null) {
|
||||
return null;
|
||||
}
|
||||
return BlocksMenu(
|
||||
items: _addBlockMenuItems,
|
||||
editorState: editorState,
|
||||
service: service,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
final _addBlockMenuItems = [
|
||||
// paragraph
|
||||
BlockMenuItem(
|
||||
blockType: ParagraphBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_text_decoration_m),
|
||||
label: LocaleKeys.editor_text.tr(),
|
||||
isSelected: _unSelectable,
|
||||
onTap: (editorState, selection, service) async {
|
||||
await editorState.insertBlockOrReplaceCurrentBlock(
|
||||
selection,
|
||||
paragraphNode(),
|
||||
);
|
||||
service.closeItemMenu();
|
||||
},
|
||||
),
|
||||
|
||||
// to-do list
|
||||
BlockMenuItem(
|
||||
blockType: TodoListBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_checkbox_m),
|
||||
label: LocaleKeys.editor_checkbox.tr(),
|
||||
isSelected: _unSelectable,
|
||||
onTap: (editorState, selection, service) async {
|
||||
await editorState.insertBlockOrReplaceCurrentBlock(
|
||||
selection,
|
||||
todoListNode(checked: false),
|
||||
);
|
||||
service.closeItemMenu();
|
||||
},
|
||||
),
|
||||
|
||||
// heading 1 - 3
|
||||
BlockMenuItem(
|
||||
blockType: HeadingBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_h1_m),
|
||||
label: LocaleKeys.editor_heading1.tr(),
|
||||
isSelected: _unSelectable,
|
||||
onTap: (editorState, selection, service) async {
|
||||
await editorState.insertBlockOrReplaceCurrentBlock(
|
||||
selection,
|
||||
headingNode(level: 1),
|
||||
);
|
||||
service.closeItemMenu();
|
||||
},
|
||||
),
|
||||
BlockMenuItem(
|
||||
blockType: HeadingBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_h2_m),
|
||||
label: LocaleKeys.editor_heading2.tr(),
|
||||
isSelected: _unSelectable,
|
||||
onTap: (editorState, selection, service) async {
|
||||
await editorState.insertBlockOrReplaceCurrentBlock(
|
||||
selection,
|
||||
headingNode(level: 2),
|
||||
);
|
||||
service.closeItemMenu();
|
||||
},
|
||||
),
|
||||
BlockMenuItem(
|
||||
blockType: HeadingBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_h3_m),
|
||||
label: LocaleKeys.editor_heading3.tr(),
|
||||
isSelected: _unSelectable,
|
||||
onTap: (editorState, selection, service) async {
|
||||
await editorState.insertBlockOrReplaceCurrentBlock(
|
||||
selection,
|
||||
headingNode(level: 3),
|
||||
);
|
||||
service.closeItemMenu();
|
||||
},
|
||||
),
|
||||
|
||||
// bulleted list
|
||||
BlockMenuItem(
|
||||
blockType: BulletedListBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_bulleted_list_m),
|
||||
label: LocaleKeys.editor_bulletedList.tr(),
|
||||
isSelected: _unSelectable,
|
||||
onTap: (editorState, selection, service) async {
|
||||
await editorState.insertBlockOrReplaceCurrentBlock(
|
||||
selection,
|
||||
bulletedListNode(),
|
||||
);
|
||||
service.closeItemMenu();
|
||||
},
|
||||
),
|
||||
|
||||
// numbered list
|
||||
BlockMenuItem(
|
||||
blockType: NumberedListBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_numbered_list_m),
|
||||
label: LocaleKeys.editor_numberedList.tr(),
|
||||
isSelected: _unSelectable,
|
||||
onTap: (editorState, selection, service) async {
|
||||
await editorState.insertBlockOrReplaceCurrentBlock(
|
||||
selection,
|
||||
numberedListNode(),
|
||||
);
|
||||
service.closeItemMenu();
|
||||
},
|
||||
),
|
||||
|
||||
// toggle list
|
||||
BlockMenuItem(
|
||||
blockType: ToggleListBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_toggle_list_m),
|
||||
label: LocaleKeys.document_plugins_toggleList.tr(),
|
||||
isSelected: _unSelectable,
|
||||
onTap: (editorState, selection, service) async {
|
||||
await editorState.insertBlockOrReplaceCurrentBlock(
|
||||
selection,
|
||||
toggleListBlockNode(),
|
||||
);
|
||||
service.closeItemMenu();
|
||||
},
|
||||
),
|
||||
|
||||
// quote
|
||||
BlockMenuItem(
|
||||
blockType: QuoteBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_quote_m),
|
||||
label: LocaleKeys.editor_quote.tr(),
|
||||
isSelected: _unSelectable,
|
||||
onTap: (editorState, selection, service) async {
|
||||
await editorState.insertBlockOrReplaceCurrentBlock(
|
||||
selection,
|
||||
quoteNode(),
|
||||
);
|
||||
service.closeItemMenu();
|
||||
},
|
||||
),
|
||||
|
||||
// callout
|
||||
BlockMenuItem(
|
||||
blockType: CalloutBlockKeys.type,
|
||||
// FIXME: update icon
|
||||
icon: const Icon(Icons.note_rounded),
|
||||
label: LocaleKeys.document_plugins_callout.tr(),
|
||||
isSelected: _unSelectable,
|
||||
onTap: (editorState, selection, service) async {
|
||||
await editorState.insertBlockOrReplaceCurrentBlock(
|
||||
selection,
|
||||
calloutNode(),
|
||||
);
|
||||
service.closeItemMenu();
|
||||
},
|
||||
),
|
||||
|
||||
// code
|
||||
BlockMenuItem(
|
||||
blockType: CodeBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_code_m),
|
||||
label: LocaleKeys.document_selectionMenu_codeBlock.tr(),
|
||||
isSelected: _unSelectable,
|
||||
onTap: (editorState, selection, service) async {
|
||||
await editorState.insertBlockOrReplaceCurrentBlock(
|
||||
selection,
|
||||
codeBlockNode(),
|
||||
);
|
||||
service.closeItemMenu();
|
||||
},
|
||||
),
|
||||
|
||||
// divider
|
||||
BlockMenuItem(
|
||||
blockType: DividerBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_divider_m),
|
||||
label: LocaleKeys.editor_divider.tr(),
|
||||
isSelected: _unSelectable,
|
||||
onTap: (editorState, selection, service) async {
|
||||
await editorState.insertDivider(selection);
|
||||
service.closeItemMenu();
|
||||
},
|
||||
),
|
||||
|
||||
// math equation
|
||||
BlockMenuItem(
|
||||
blockType: MathEquationBlockKeys.type,
|
||||
icon: const FlowySvg(
|
||||
FlowySvgs.math_lg,
|
||||
size: Size.square(22),
|
||||
),
|
||||
label: LocaleKeys.document_plugins_mathEquation_name.tr(),
|
||||
isSelected: _unSelectable,
|
||||
onTap: (editorState, selection, service) async {
|
||||
await editorState.insertMathEquation(selection);
|
||||
service.closeItemMenu();
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
bool _unSelectable(
|
||||
EditorState editorState,
|
||||
Selection selection,
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
extension on EditorState {
|
||||
Future<void> insertBlockOrReplaceCurrentBlock(
|
||||
Selection selection,
|
||||
Node insertedNode,
|
||||
) async {
|
||||
// If the current block is not an empty paragraph block,
|
||||
// then insert a new block below the current block.
|
||||
final node = getNodeAtPath(selection.start.path);
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
final transaction = this.transaction;
|
||||
if (node.type != ParagraphBlockKeys.type ||
|
||||
(node.delta?.isNotEmpty ?? true)) {
|
||||
final path = node.path.next;
|
||||
// insert the block below the current empty paragraph block
|
||||
transaction
|
||||
..insertNode(path, insertedNode)
|
||||
..afterSelection = Selection.collapsed(
|
||||
Position(path: path, offset: 0),
|
||||
);
|
||||
} else {
|
||||
final path = node.path;
|
||||
// replace the current empty paragraph block with the inserted block
|
||||
transaction
|
||||
..insertNode(path, insertedNode)
|
||||
..deleteNode(node)
|
||||
..afterSelection = Selection.collapsed(
|
||||
Position(path: path, offset: 0),
|
||||
);
|
||||
}
|
||||
await apply(transaction);
|
||||
}
|
||||
|
||||
Future<void> insertMathEquation(
|
||||
Selection selection,
|
||||
) async {
|
||||
final path = selection.start.path;
|
||||
final node = getNodeAtPath(path);
|
||||
final delta = node?.delta;
|
||||
if (node == null || delta == null) {
|
||||
return;
|
||||
}
|
||||
final transaction = this.transaction;
|
||||
final insertedNode = mathEquationNode();
|
||||
if (delta.isEmpty) {
|
||||
transaction
|
||||
..insertNode(path, insertedNode)
|
||||
..deleteNode(node);
|
||||
} else {
|
||||
transaction.insertNode(
|
||||
path.next,
|
||||
insertedNode,
|
||||
);
|
||||
}
|
||||
|
||||
await apply(transaction);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
final mathEquationState = getNodeAtPath(path)?.key.currentState;
|
||||
if (mathEquationState != null &&
|
||||
mathEquationState is MathEquationBlockComponentWidgetState) {
|
||||
mathEquationState.showEditingDialog();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> insertDivider(Selection selection) async {
|
||||
// same as the [handler] of [dividerMenuItem] in Desktop
|
||||
|
||||
final path = selection.end.path;
|
||||
final node = getNodeAtPath(path);
|
||||
final delta = node?.delta;
|
||||
if (node == null || delta == null) {
|
||||
return;
|
||||
}
|
||||
final insertedPath = delta.isEmpty ? path : path.next;
|
||||
final transaction = this.transaction;
|
||||
transaction.insertNode(insertedPath, dividerNode());
|
||||
// only insert a new paragraph node when the next node is not a paragraph node
|
||||
// and its delta is not empty.
|
||||
final next = node.next;
|
||||
if (next == null ||
|
||||
next.type != ParagraphBlockKeys.type ||
|
||||
next.delta?.isNotEmpty == true) {
|
||||
transaction.insertNode(
|
||||
insertedPath,
|
||||
paragraphNode(),
|
||||
);
|
||||
}
|
||||
transaction.afterSelection = Selection.collapsed(
|
||||
Position(path: insertedPath.next),
|
||||
);
|
||||
await apply(transaction);
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
final mobileAlignToolbarItem = MobileToolbarItem.withMenu(
|
||||
itemIconBuilder: (_, editorState) {
|
||||
itemIconBuilder: (_, editorState, __) {
|
||||
return onlyShowInTextType(editorState)
|
||||
? const FlowySvg(
|
||||
FlowySvgs.toolbar_align_center_s,
|
||||
@ -14,7 +14,11 @@ final mobileAlignToolbarItem = MobileToolbarItem.withMenu(
|
||||
)
|
||||
: null;
|
||||
},
|
||||
itemMenuBuilder: (editorState, selection, _) {
|
||||
itemMenuBuilder: (_, editorState, ___) {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null) {
|
||||
return null;
|
||||
}
|
||||
return _MobileAlignMenu(
|
||||
editorState: editorState,
|
||||
selection: selection,
|
||||
@ -34,6 +38,7 @@ class _MobileAlignMenu extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.count(
|
||||
padding: EdgeInsets.zero,
|
||||
crossAxisCount: 3,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
|
@ -0,0 +1,90 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BlockMenuItem {
|
||||
const BlockMenuItem({
|
||||
required this.blockType,
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
this.isSelected,
|
||||
});
|
||||
|
||||
// block type
|
||||
final String blockType;
|
||||
final Widget icon;
|
||||
final String label;
|
||||
// callback
|
||||
final void Function(
|
||||
EditorState editorState,
|
||||
Selection selection,
|
||||
// used to control the open or close the menu
|
||||
MobileToolbarWidgetService service,
|
||||
) onTap;
|
||||
|
||||
final bool Function(
|
||||
EditorState editorState,
|
||||
Selection selection,
|
||||
)? isSelected;
|
||||
}
|
||||
|
||||
class BlocksMenu extends StatelessWidget {
|
||||
const BlocksMenu({
|
||||
super.key,
|
||||
required this.editorState,
|
||||
required this.items,
|
||||
required this.service,
|
||||
});
|
||||
|
||||
final EditorState editorState;
|
||||
final List<BlockMenuItem> items;
|
||||
final MobileToolbarWidgetService service;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.count(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
childAspectRatio: 4,
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 36.0,
|
||||
),
|
||||
shrinkWrap: true,
|
||||
children: items.map((item) {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
bool isSelected = false;
|
||||
if (item.isSelected != null) {
|
||||
isSelected = item.isSelected!(editorState, selection);
|
||||
} else {
|
||||
isSelected = _isSelected(editorState, selection, item.blockType);
|
||||
}
|
||||
return MobileToolbarItemMenuBtn(
|
||||
icon: item.icon,
|
||||
label: FlowyText(item.label),
|
||||
isSelected: isSelected,
|
||||
onPressed: () async {
|
||||
item.onTap(editorState, selection, service);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
bool _isSelected(
|
||||
EditorState editorState,
|
||||
Selection selection,
|
||||
String blockType,
|
||||
) {
|
||||
final node = editorState.getNodeAtPath(selection.start.path);
|
||||
final type = node?.type;
|
||||
if (node == null || type == null) {
|
||||
return false;
|
||||
}
|
||||
return type == blockType;
|
||||
}
|
||||
}
|
@ -1,142 +0,0 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.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';
|
||||
|
||||
final mobileBlocksToolbarItem = MobileToolbarItem.withMenu(
|
||||
itemIconBuilder: (_, __) =>
|
||||
const AFMobileIcon(afMobileIcons: AFMobileIcons.list),
|
||||
itemMenuBuilder: (editorState, selection, _) {
|
||||
return _MobileListMenu(
|
||||
editorState: editorState,
|
||||
selection: selection,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
class _MobileListMenu extends StatelessWidget {
|
||||
const _MobileListMenu({
|
||||
required this.editorState,
|
||||
required this.selection,
|
||||
});
|
||||
|
||||
final Selection selection;
|
||||
final EditorState editorState;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.count(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
childAspectRatio: 5,
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
// bulleted list, numbered list
|
||||
_buildListButton(
|
||||
context,
|
||||
BulletedListBlockKeys.type,
|
||||
const AFMobileIcon(afMobileIcons: AFMobileIcons.bulletedList),
|
||||
LocaleKeys.document_plugins_bulletedList.tr(),
|
||||
),
|
||||
_buildListButton(
|
||||
context,
|
||||
NumberedListBlockKeys.type,
|
||||
const AFMobileIcon(afMobileIcons: AFMobileIcons.numberedList),
|
||||
LocaleKeys.document_plugins_numberedList.tr(),
|
||||
),
|
||||
|
||||
// todo list, quote list
|
||||
_buildListButton(
|
||||
context,
|
||||
TodoListBlockKeys.type,
|
||||
const AFMobileIcon(afMobileIcons: AFMobileIcons.checkbox),
|
||||
LocaleKeys.document_plugins_todoList.tr(),
|
||||
),
|
||||
_buildListButton(
|
||||
context,
|
||||
QuoteBlockKeys.type,
|
||||
const AFMobileIcon(afMobileIcons: AFMobileIcons.quote),
|
||||
LocaleKeys.document_plugins_quoteList.tr(),
|
||||
),
|
||||
|
||||
// toggle list, callout
|
||||
_buildListButton(
|
||||
context,
|
||||
ToggleListBlockKeys.type,
|
||||
const FlowySvg(
|
||||
FlowySvgs.toggle_list_s,
|
||||
size: Size.square(24),
|
||||
),
|
||||
LocaleKeys.document_plugins_toggleList.tr(),
|
||||
),
|
||||
_buildListButton(
|
||||
context,
|
||||
CalloutBlockKeys.type,
|
||||
const Icon(Icons.note_rounded),
|
||||
LocaleKeys.document_plugins_callout.tr(),
|
||||
),
|
||||
_buildListButton(
|
||||
context,
|
||||
CodeBlockKeys.type,
|
||||
const Icon(Icons.abc),
|
||||
LocaleKeys.document_selectionMenu_codeBlock.tr(),
|
||||
),
|
||||
// code block
|
||||
_buildListButton(
|
||||
context,
|
||||
CodeBlockKeys.type,
|
||||
const Icon(Icons.abc),
|
||||
LocaleKeys.document_selectionMenu_codeBlock.tr(),
|
||||
),
|
||||
// outline
|
||||
_buildListButton(
|
||||
context,
|
||||
OutlineBlockKeys.type,
|
||||
const Icon(Icons.list_alt),
|
||||
LocaleKeys.document_selectionMenu_outline.tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildListButton(
|
||||
BuildContext context,
|
||||
String listBlockType,
|
||||
Widget icon,
|
||||
String label,
|
||||
) {
|
||||
final node = editorState.getNodeAtPath(selection.start.path);
|
||||
final type = node?.type;
|
||||
if (node == null || type == null) {
|
||||
const SizedBox.shrink();
|
||||
}
|
||||
final isSelected = type == listBlockType;
|
||||
return MobileToolbarItemMenuBtn(
|
||||
icon: icon,
|
||||
label: FlowyText(label),
|
||||
isSelected: isSelected,
|
||||
onPressed: () async {
|
||||
await editorState.formatNode(
|
||||
selection,
|
||||
(node) {
|
||||
final attributes = {
|
||||
ParagraphBlockKeys.delta: (node.delta ?? Delta()).toJson(),
|
||||
if (listBlockType == TodoListBlockKeys.type)
|
||||
TodoListBlockKeys.checked: false,
|
||||
if (listBlockType == CalloutBlockKeys.type)
|
||||
CalloutBlockKeys.icon: '📌',
|
||||
};
|
||||
return node.copyWith(
|
||||
type: isSelected ? ParagraphBlockKeys.type : listBlockType,
|
||||
attributes: attributes,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,251 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_blocks_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// convert the current block to other block types
|
||||
// only show in single selection and text type
|
||||
final mobileConvertBlockToolbarItem = MobileToolbarItem.withMenu(
|
||||
itemIconBuilder: (_, editorState, ___) {
|
||||
if (!onlyShowInSingleSelectionAndTextType(editorState)) {
|
||||
return null;
|
||||
}
|
||||
return const FlowySvg(
|
||||
FlowySvgs.convert_s,
|
||||
size: Size.square(22),
|
||||
);
|
||||
},
|
||||
itemMenuBuilder: (_, editorState, service) {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null) {
|
||||
return null;
|
||||
}
|
||||
return BlocksMenu(
|
||||
items: _convertToBlockMenuItems,
|
||||
editorState: editorState,
|
||||
service: service,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
final _convertToBlockMenuItems = [
|
||||
// paragraph
|
||||
BlockMenuItem(
|
||||
blockType: ParagraphBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_text_decoration_m),
|
||||
label: LocaleKeys.editor_text.tr(),
|
||||
onTap: (editorState, selection, _) => editorState.convertBlockType(
|
||||
selection,
|
||||
ParagraphBlockKeys.type,
|
||||
),
|
||||
),
|
||||
|
||||
// to-do list
|
||||
BlockMenuItem(
|
||||
blockType: TodoListBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_checkbox_m),
|
||||
label: LocaleKeys.editor_checkbox.tr(),
|
||||
onTap: (editorState, selection, _) => editorState.convertBlockType(
|
||||
selection,
|
||||
TodoListBlockKeys.type,
|
||||
extraAttributes: {
|
||||
TodoListBlockKeys.checked: false,
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// heading 1 - 3
|
||||
BlockMenuItem(
|
||||
blockType: HeadingBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_h1_m),
|
||||
label: LocaleKeys.editor_heading1.tr(),
|
||||
isSelected: (editorState, selection) => _isHeadingSelected(
|
||||
editorState,
|
||||
selection,
|
||||
1,
|
||||
),
|
||||
onTap: (editorState, selection, _) {
|
||||
final isSelected = _isHeadingSelected(
|
||||
editorState,
|
||||
selection,
|
||||
1,
|
||||
);
|
||||
editorState.convertBlockType(
|
||||
selection,
|
||||
HeadingBlockKeys.type,
|
||||
isSelected: isSelected,
|
||||
extraAttributes: {
|
||||
HeadingBlockKeys.level: 1,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
BlockMenuItem(
|
||||
blockType: HeadingBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_h2_m),
|
||||
label: LocaleKeys.editor_heading2.tr(),
|
||||
isSelected: (editorState, selection) => _isHeadingSelected(
|
||||
editorState,
|
||||
selection,
|
||||
2,
|
||||
),
|
||||
onTap: (editorState, selection, _) {
|
||||
final isSelected = _isHeadingSelected(
|
||||
editorState,
|
||||
selection,
|
||||
2,
|
||||
);
|
||||
editorState.convertBlockType(
|
||||
selection,
|
||||
HeadingBlockKeys.type,
|
||||
isSelected: isSelected,
|
||||
extraAttributes: {
|
||||
HeadingBlockKeys.level: 2,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
BlockMenuItem(
|
||||
blockType: HeadingBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_h3_m),
|
||||
label: LocaleKeys.editor_heading3.tr(),
|
||||
isSelected: (editorState, selection) => _isHeadingSelected(
|
||||
editorState,
|
||||
selection,
|
||||
3,
|
||||
),
|
||||
onTap: (editorState, selection, _) {
|
||||
final isSelected = _isHeadingSelected(
|
||||
editorState,
|
||||
selection,
|
||||
3,
|
||||
);
|
||||
editorState.convertBlockType(
|
||||
selection,
|
||||
HeadingBlockKeys.type,
|
||||
isSelected: isSelected,
|
||||
extraAttributes: {
|
||||
HeadingBlockKeys.level: 3,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// bulleted list
|
||||
BlockMenuItem(
|
||||
blockType: BulletedListBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_bulleted_list_m),
|
||||
label: LocaleKeys.editor_bulletedList.tr(),
|
||||
onTap: (editorState, selection, _) => editorState.convertBlockType(
|
||||
selection,
|
||||
BulletedListBlockKeys.type,
|
||||
),
|
||||
),
|
||||
|
||||
// numbered list
|
||||
BlockMenuItem(
|
||||
blockType: NumberedListBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_numbered_list_m),
|
||||
label: LocaleKeys.editor_numberedList.tr(),
|
||||
onTap: (editorState, selection, _) => editorState.convertBlockType(
|
||||
selection,
|
||||
NumberedListBlockKeys.type,
|
||||
),
|
||||
),
|
||||
|
||||
// toggle list
|
||||
BlockMenuItem(
|
||||
blockType: ToggleListBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_toggle_list_m),
|
||||
label: LocaleKeys.document_plugins_toggleList.tr(),
|
||||
onTap: (editorState, selection, _) => editorState.convertBlockType(
|
||||
selection,
|
||||
ToggleListBlockKeys.type,
|
||||
),
|
||||
),
|
||||
|
||||
// quote
|
||||
BlockMenuItem(
|
||||
blockType: QuoteBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_quote_m),
|
||||
label: LocaleKeys.editor_quote.tr(),
|
||||
onTap: (editorState, selection, _) => editorState.convertBlockType(
|
||||
selection,
|
||||
QuoteBlockKeys.type,
|
||||
),
|
||||
),
|
||||
|
||||
// callout
|
||||
BlockMenuItem(
|
||||
blockType: CalloutBlockKeys.type,
|
||||
// FIXME: update icon
|
||||
icon: const Icon(Icons.note_rounded),
|
||||
label: LocaleKeys.document_plugins_callout.tr(),
|
||||
onTap: (editorState, selection, _) => editorState.convertBlockType(
|
||||
selection,
|
||||
CalloutBlockKeys.type,
|
||||
extraAttributes: {
|
||||
CalloutBlockKeys.icon: '📌',
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// code
|
||||
BlockMenuItem(
|
||||
blockType: CodeBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_code_m),
|
||||
label: LocaleKeys.document_selectionMenu_codeBlock.tr(),
|
||||
onTap: (editorState, selection, _) => editorState.convertBlockType(
|
||||
selection,
|
||||
CodeBlockKeys.type,
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
extension on EditorState {
|
||||
Future<void> convertBlockType(
|
||||
Selection selection,
|
||||
String newBlockType, {
|
||||
Attributes? extraAttributes,
|
||||
bool? isSelected,
|
||||
}) async {
|
||||
final node = getNodeAtPath(selection.start.path);
|
||||
final type = node?.type;
|
||||
if (node == null || type == null) {
|
||||
assert(false, 'node or type is null');
|
||||
return;
|
||||
}
|
||||
final selected = isSelected ?? type == newBlockType;
|
||||
await formatNode(
|
||||
selection,
|
||||
(node) {
|
||||
final attributes = {
|
||||
ParagraphBlockKeys.delta: (node.delta ?? Delta()).toJson(),
|
||||
// for some block types, they have extra attributes, like todo list has checked attribute, callout has icon attribute, etc.
|
||||
if (!selected && extraAttributes != null) ...extraAttributes,
|
||||
};
|
||||
return node.copyWith(
|
||||
type: selected ? ParagraphBlockKeys.type : newBlockType,
|
||||
attributes: attributes,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool _isHeadingSelected(
|
||||
EditorState editorState,
|
||||
Selection selection,
|
||||
int level,
|
||||
) {
|
||||
final node = editorState.getNodeAtPath(selection.start.path);
|
||||
final type = node?.type;
|
||||
if (node == null || type == null) {
|
||||
return false;
|
||||
}
|
||||
return type == HeadingBlockKeys.type &&
|
||||
node.attributes[HeadingBlockKeys.level] == level;
|
||||
}
|
@ -2,23 +2,23 @@ import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
final mobileIndentToolbarItem = MobileToolbarItem.action(
|
||||
itemIconBuilder: (_, editorState) {
|
||||
itemIconBuilder: (_, editorState, __) {
|
||||
return onlyShowInTextType(editorState)
|
||||
? const Icon(Icons.format_indent_increase_rounded)
|
||||
: null;
|
||||
},
|
||||
actionHandler: (editorState, selection) {
|
||||
actionHandler: (_, editorState) {
|
||||
indentCommand.execute(editorState);
|
||||
},
|
||||
);
|
||||
|
||||
final mobileOutdentToolbarItem = MobileToolbarItem.action(
|
||||
itemIconBuilder: (_, editorState) {
|
||||
itemIconBuilder: (_, editorState, __) {
|
||||
return onlyShowInTextType(editorState)
|
||||
? const Icon(Icons.format_indent_decrease_rounded)
|
||||
: null;
|
||||
},
|
||||
actionHandler: (editorState, selection) {
|
||||
actionHandler: (_, editorState) {
|
||||
outdentCommand.execute(editorState);
|
||||
},
|
||||
);
|
||||
|
@ -0,0 +1,106 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
final customTextDecorationMobileToolbarItem = MobileToolbarItem.withMenu(
|
||||
itemIconBuilder: (_, __, ___) => const AFMobileIcon(
|
||||
afMobileIcons: AFMobileIcons.textDecoration,
|
||||
),
|
||||
itemMenuBuilder: (_, editorState, __) {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return _TextDecorationMenu(editorState, selection);
|
||||
},
|
||||
);
|
||||
|
||||
class _TextDecorationMenu extends StatefulWidget {
|
||||
const _TextDecorationMenu(
|
||||
this.editorState,
|
||||
this.selection,
|
||||
);
|
||||
|
||||
final EditorState editorState;
|
||||
final Selection selection;
|
||||
|
||||
@override
|
||||
State<_TextDecorationMenu> createState() => _TextDecorationMenuState();
|
||||
}
|
||||
|
||||
class _TextDecorationMenuState extends State<_TextDecorationMenu> {
|
||||
final textDecorations = [
|
||||
// BIUS
|
||||
TextDecorationUnit(
|
||||
icon: AFMobileIcons.bold,
|
||||
label: AppFlowyEditorL10n.current.bold,
|
||||
name: AppFlowyRichTextKeys.bold,
|
||||
),
|
||||
TextDecorationUnit(
|
||||
icon: AFMobileIcons.italic,
|
||||
label: AppFlowyEditorL10n.current.italic,
|
||||
name: AppFlowyRichTextKeys.italic,
|
||||
),
|
||||
TextDecorationUnit(
|
||||
icon: AFMobileIcons.underline,
|
||||
label: AppFlowyEditorL10n.current.underline,
|
||||
name: AppFlowyRichTextKeys.underline,
|
||||
),
|
||||
TextDecorationUnit(
|
||||
icon: AFMobileIcons.strikethrough,
|
||||
label: AppFlowyEditorL10n.current.strikethrough,
|
||||
name: AppFlowyRichTextKeys.strikethrough,
|
||||
),
|
||||
|
||||
// Code
|
||||
TextDecorationUnit(
|
||||
icon: AFMobileIcons.code,
|
||||
label: AppFlowyEditorL10n.current.embedCode,
|
||||
name: AppFlowyRichTextKeys.code,
|
||||
),
|
||||
];
|
||||
|
||||
@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,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return MobileToolbarItemMenuBtn(
|
||||
icon: AFMobileIcon(
|
||||
afMobileIcons: currentDecoration.icon,
|
||||
),
|
||||
label: FlowyText(currentDecoration.label),
|
||||
isSelected: isSelected,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
widget.editorState.toggleAttribute(currentDecoration.name);
|
||||
});
|
||||
},
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return GridView.count(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
childAspectRatio: 4,
|
||||
children: bius,
|
||||
);
|
||||
}
|
||||
}
|
@ -26,9 +26,11 @@ export 'inline_math_equation/inline_math_equation.dart';
|
||||
export 'inline_math_equation/inline_math_equation_toolbar_item.dart';
|
||||
export 'math_equation/math_equation_block_component.dart';
|
||||
export 'math_equation/mobile_math_equation_toolbar_item.dart';
|
||||
export 'mobile_toolbar_item/mobile_add_block_toolbar_item.dart';
|
||||
export 'mobile_toolbar_item/mobile_align_toolbar_item.dart';
|
||||
export 'mobile_toolbar_item/mobile_blocks_toolbar_item.dart';
|
||||
export 'mobile_toolbar_item/mobile_convert_block_toolbar_item.dart';
|
||||
export 'mobile_toolbar_item/mobile_indent_toolbar_items.dart';
|
||||
export 'mobile_toolbar_item/mobile_text_decoration_item.dart';
|
||||
export 'openai/widgets/auto_completion_node_widget.dart';
|
||||
export 'openai/widgets/smart_edit_node_widget.dart';
|
||||
export 'openai/widgets/smart_edit_toolbar_item.dart';
|
||||
|
@ -2,8 +2,10 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
final redoMobileToolbarItem = MobileToolbarItem.action(
|
||||
itemIconBuilder: (_, __) => const FlowySvg(FlowySvgs.m_redo_m),
|
||||
actionHandler: (editorState, selection) async {
|
||||
itemIconBuilder: (_, __, ___) => const FlowySvg(
|
||||
FlowySvgs.m_redo_m,
|
||||
),
|
||||
actionHandler: (_, editorState) async {
|
||||
editorState.undoManager.redo();
|
||||
},
|
||||
);
|
||||
|
@ -2,8 +2,10 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
final undoMobileToolbarItem = MobileToolbarItem.action(
|
||||
itemIconBuilder: (_, __) => const FlowySvg(FlowySvgs.m_undo_m),
|
||||
actionHandler: (editorState, selection) async {
|
||||
itemIconBuilder: (_, __, ___) => const FlowySvg(
|
||||
FlowySvgs.m_undo_m,
|
||||
),
|
||||
actionHandler: (_, editorState) async {
|
||||
editorState.undoManager.undo();
|
||||
},
|
||||
);
|
||||
|
@ -83,11 +83,14 @@ class EditorStyleCustomizer {
|
||||
|
||||
EditorStyle mobile() {
|
||||
final theme = Theme.of(context);
|
||||
const fontSize = 14.0;
|
||||
final fontFamily = GoogleFonts.poppins().fontFamily ?? 'Poppins';
|
||||
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
|
||||
final fontFamily = context.read<DocumentAppearanceCubit>().state.fontFamily;
|
||||
final defaultTextDirection =
|
||||
context.read<DocumentAppearanceCubit>().state.defaultTextDirection;
|
||||
final codeFontSize = max(0.0, fontSize - 2);
|
||||
return EditorStyle.mobile(
|
||||
padding: padding,
|
||||
defaultTextDirection: defaultTextDirection,
|
||||
textStyleConfiguration: TextStyleConfiguration(
|
||||
text: baseTextStyle(fontFamily).copyWith(
|
||||
fontSize: fontSize,
|
||||
|
Reference in New Issue
Block a user