feat: support mention page on mobile (#5158)

* feat: support mention page on mobile

* chore: clean up toggle notifier

* fix: changes after merge

* fix: depends on inherited widget error

* fix: amend after merge

* feat: add icon to search

* chore: slight style changes

* chore: revert podfile change

* ci: fix disposal
This commit is contained in:
Mathias Mogensen 2024-05-02 02:10:56 +02:00 committed by GitHub
parent 6bfac6b80a
commit e1e8747f15
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 425 additions and 333 deletions

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/grid/application/row/row_document_bloc.dart'; import 'package:appflowy/plugins/database/grid/application/row/row_document_bloc.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart';
@ -8,7 +10,6 @@ import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class RowDocument extends StatelessWidget { class RowDocument extends StatelessWidget {
@ -82,9 +83,7 @@ class _RowEditorState extends State<RowEditor> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MultiBlocProvider( return MultiBlocProvider(
providers: [ providers: [BlocProvider.value(value: documentBloc)],
BlocProvider.value(value: documentBloc),
],
child: BlocListener<DocumentBloc, DocumentState>( child: BlocListener<DocumentBloc, DocumentState>(
listenWhen: (previous, current) => listenWhen: (previous, current) =>
previous.isDocumentEmpty != current.isDocumentEmpty, previous.isDocumentEmpty != current.isDocumentEmpty,

View File

@ -1,5 +1,7 @@
library document_plugin; library document_plugin;
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
@ -20,7 +22,6 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class DocumentPluginBuilder extends PluginBuilder { class DocumentPluginBuilder extends PluginBuilder {

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart';
@ -16,7 +18,6 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class DocumentPage extends StatefulWidget { class DocumentPage extends StatefulWidget {
@ -153,9 +154,9 @@ class _DocumentPageState extends State<DocumentPage>
onRestore: () => context.read<DocumentBloc>().add( onRestore: () => context.read<DocumentBloc>().add(
const DocumentEvent.restorePage(), const DocumentEvent.restorePage(),
), ),
onDelete: () => context.read<DocumentBloc>().add( onDelete: () => context
const DocumentEvent.deletePermanently(), .read<DocumentBloc>()
), .add(const DocumentEvent.deletePermanently()),
); );
} }
@ -178,12 +179,10 @@ class _DocumentPageState extends State<DocumentPage>
node: page, node: page,
editorState: editorState, editorState: editorState,
view: widget.view, view: widget.view,
onIconChanged: (icon) async { onIconChanged: (icon) async => ViewBackendService.updateViewIcon(
await ViewBackendService.updateViewIcon(
viewId: widget.view.id, viewId: widget.view.id,
viewIcon: icon, viewIcon: icon,
); ),
},
); );
} }
@ -196,12 +195,11 @@ class _DocumentPageState extends State<DocumentPage>
undoCommand.execute(editorState); undoCommand.execute(editorState);
} else if (type == EditorNotificationType.redo) { } else if (type == EditorNotificationType.redo) {
redoCommand.execute(editorState); redoCommand.execute(editorState);
} else if (type == EditorNotificationType.exitEditing) { } else if (type == EditorNotificationType.exitEditing &&
if (editorState.selection != null) { editorState.selection != null) {
editorState.selection = null; editorState.selection = null;
} }
} }
}
void _onNotificationAction( void _onNotificationAction(
BuildContext context, BuildContext context,

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:avatar_stack/avatar_stack.dart'; import 'package:avatar_stack/avatar_stack.dart';
import 'package:avatar_stack/positions.dart'; import 'package:avatar_stack/positions.dart';
import 'package:flutter/material.dart';
class CollaboratorAvatarStack extends StatelessWidget { class CollaboratorAvatarStack extends StatelessWidget {
const CollaboratorAvatarStack({ const CollaboratorAvatarStack({
@ -17,21 +18,13 @@ class CollaboratorAvatarStack extends StatelessWidget {
}); });
final List<Widget> avatars; final List<Widget> avatars;
final Positions? settings; final Positions? settings;
final InfoWidgetBuilder? infoWidgetBuilder; final InfoWidgetBuilder? infoWidgetBuilder;
final double? width; final double? width;
final double? height; final double? height;
final double? borderWidth; final double? borderWidth;
final Color? borderColor; final Color? borderColor;
final Color? backgroundColor; final Color? backgroundColor;
final Widget Function(int value, BorderSide border) plusWidgetBuilder; final Widget Function(int value, BorderSide border) plusWidgetBuilder;
@override @override

View File

@ -1,3 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_page.dart'; import 'package:appflowy/plugins/document/presentation/editor_page.dart';
@ -10,8 +13,6 @@ import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:easy_localization/easy_localization.dart' hide TextDirection; import 'package:easy_localization/easy_localization.dart' hide TextDirection;
import 'package:flowy_infra/theme_extension.dart'; 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:flutter_bloc/flutter_bloc.dart';
Map<String, BlockComponentBuilder> getEditorBuilderMap({ Map<String, BlockComponentBuilder> getEditorBuilderMap({
@ -23,25 +24,17 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
ShowPlaceholder? showParagraphPlaceholder, ShowPlaceholder? showParagraphPlaceholder,
String Function(Node)? placeholderText, String Function(Node)? placeholderText,
}) { }) {
final standardActions = [ final standardActions = [OptionAction.delete, OptionAction.duplicate];
OptionAction.delete,
OptionAction.duplicate,
// OptionAction.divider,
// OptionAction.moveUp,
// OptionAction.moveDown,
];
final calloutBGColor = AFThemeExtension.of(context).calloutBGColor; final calloutBGColor = AFThemeExtension.of(context).calloutBGColor;
final configuration = BlockComponentConfiguration( final configuration = BlockComponentConfiguration(
// use EdgeInsets.zero to remove the default padding. // use EdgeInsets.zero to remove the default padding.
padding: (node) { padding: (_) {
if (PlatformExtension.isMobile) { if (PlatformExtension.isMobile) {
final pageStyle = context.read<DocumentPageStyleBloc>().state; final pageStyle = context.read<DocumentPageStyleBloc>().state;
final factor = pageStyle.fontLayout.factor; final factor = pageStyle.fontLayout.factor;
final padding = pageStyle.lineHeightLayout.padding * factor; final padding = pageStyle.lineHeightLayout.padding * factor;
return EdgeInsets.only( return EdgeInsets.only(top: padding);
top: padding,
);
} }
return const EdgeInsets.symmetric(vertical: 5.0); return const EdgeInsets.symmetric(vertical: 5.0);
@ -62,10 +55,7 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
placeholderText: (_) => LocaleKeys.blockPlaceholders_todoList.tr(), placeholderText: (_) => LocaleKeys.blockPlaceholders_todoList.tr(),
), ),
iconBuilder: PlatformExtension.isMobile iconBuilder: PlatformExtension.isMobile
? (context, node, onCheck) => TodoListIcon( ? (_, node, onCheck) => TodoListIcon(node: node, onCheck: onCheck)
node: node,
onCheck: onCheck,
)
: null, : null,
toggleChildrenTriggers: [ toggleChildrenTriggers: [
LogicalKeyboardKey.shift, LogicalKeyboardKey.shift,
@ -78,9 +68,7 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
placeholderText: (_) => LocaleKeys.blockPlaceholders_bulletList.tr(), placeholderText: (_) => LocaleKeys.blockPlaceholders_bulletList.tr(),
), ),
iconBuilder: PlatformExtension.isMobile iconBuilder: PlatformExtension.isMobile
? (context, node) => BulletedListIcon( ? (_, node) => BulletedListIcon(node: node)
node: node,
)
: null, : null,
), ),
NumberedListBlockKeys.type: NumberedListBlockComponentBuilder( NumberedListBlockKeys.type: NumberedListBlockComponentBuilder(
@ -88,10 +76,8 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
placeholderText: (_) => LocaleKeys.blockPlaceholders_numberList.tr(), placeholderText: (_) => LocaleKeys.blockPlaceholders_numberList.tr(),
), ),
iconBuilder: PlatformExtension.isMobile iconBuilder: PlatformExtension.isMobile
? (context, node, textDirection) => NumberedListIcon( ? (_, node, textDirection) =>
node: node, NumberedListIcon(node: node, textDirection: textDirection)
textDirection: textDirection,
)
: null, : null,
), ),
QuoteBlockKeys.type: QuoteBlockComponentBuilder( QuoteBlockKeys.type: QuoteBlockComponentBuilder(
@ -108,9 +94,7 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
final headingPaddings = pageStyle.lineHeightLayout.headingPaddings final headingPaddings = pageStyle.lineHeightLayout.headingPaddings
.map((e) => e * factor); .map((e) => e * factor);
final level = node.attributes[HeadingBlockKeys.level] ?? 6; final level = node.attributes[HeadingBlockKeys.level] ?? 6;
return EdgeInsets.only( return EdgeInsets.only(top: headingPaddings.elementAt(level));
top: headingPaddings.elementAt(level),
);
} }
return const EdgeInsets.only(top: 12.0, bottom: 4.0); return const EdgeInsets.only(top: 12.0, bottom: 4.0);
@ -128,10 +112,7 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
Positioned( Positioned(
top: 0, top: 0,
right: 10, right: 10,
child: ImageMenu( child: ImageMenu(node: node, state: state),
node: node,
state: state,
),
), ),
), ),
TableBlockKeys.type: TableBlockComponentBuilder( TableBlockKeys.type: TableBlockComponentBuilder(
@ -188,14 +169,12 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
DividerBlockKeys.type: DividerBlockComponentBuilder( DividerBlockKeys.type: DividerBlockComponentBuilder(
configuration: configuration, configuration: configuration,
height: 28.0, height: 28.0,
wrapper: (context, node, child) { wrapper: (_, node, child) => MobileBlockActionButtons(
return MobileBlockActionButtons(
showThreeDots: false, showThreeDots: false,
node: node, node: node,
editorState: editorState, editorState: editorState,
child: child, child: child,
); ),
},
), ),
MathEquationBlockKeys.type: MathEquationBlockComponentBuilder( MathEquationBlockKeys.type: MathEquationBlockComponentBuilder(
configuration: configuration, configuration: configuration,
@ -223,10 +202,7 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
configuration: configuration.copyWith( configuration: configuration.copyWith(
placeholderTextStyle: (_) => placeholderTextStyle: (_) =>
styleCustomizer.outlineBlockPlaceholderStyleBuilder(), styleCustomizer.outlineBlockPlaceholderStyleBuilder(),
padding: (_) => const EdgeInsets.only( padding: (_) => const EdgeInsets.only(top: 12.0, bottom: 4.0),
top: 12.0,
bottom: 4.0,
),
), ),
), ),
LinkPreviewBlockKeys.type: LinkPreviewBlockComponentBuilder( LinkPreviewBlockKeys.type: LinkPreviewBlockComponentBuilder(
@ -238,12 +214,9 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
menuBuilder: (context, node, state) => Positioned( menuBuilder: (context, node, state) => Positioned(
top: 10, top: 10,
right: 0, right: 0,
child: LinkPreviewMenu( child: LinkPreviewMenu(node: node, state: state),
node: node,
state: state,
), ),
), builder: (_, node, url, title, description, imageUrl) =>
builder: (context, node, url, title, description, imageUrl) =>
CustomLinkPreviewWidget( CustomLinkPreviewWidget(
node: node, node: node,
url: url, url: url,
@ -283,27 +256,11 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
ToggleListBlockKeys.type, ToggleListBlockKeys.type,
]; ];
final supportAlignBuilderType = [ final supportAlignBuilderType = [ImageBlockKeys.type];
ImageBlockKeys.type, final supportDepthBuilderType = [OutlineBlockKeys.type];
]; final colorAction = [OptionAction.divider, OptionAction.color];
final alignAction = [OptionAction.divider, OptionAction.align];
final supportDepthBuilderType = [ final depthAction = [OptionAction.depth];
OutlineBlockKeys.type,
];
final colorAction = [
OptionAction.divider,
OptionAction.color,
];
final alignAction = [
OptionAction.divider,
OptionAction.align,
];
final depthAction = [
OptionAction.depth,
];
final List<OptionAction> actions = [ final List<OptionAction> actions = [
...standardActions, ...standardActions,

View File

@ -1,46 +1,30 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
enum EditorNotificationType { import 'package:appflowy_editor/appflowy_editor.dart';
none,
undo, enum EditorNotificationType { none, undo, redo, exitEditing }
redo,
exitEditing,
}
class EditorNotification { class EditorNotification {
const EditorNotification({ const EditorNotification({required this.type});
required this.type,
});
EditorNotification.undo() : type = EditorNotificationType.undo; EditorNotification.undo() : type = EditorNotificationType.undo;
EditorNotification.redo() : type = EditorNotificationType.redo; EditorNotification.redo() : type = EditorNotificationType.redo;
EditorNotification.exitEditing() : type = EditorNotificationType.exitEditing; EditorNotification.exitEditing() : type = EditorNotificationType.exitEditing;
static final PropertyValueNotifier<EditorNotificationType> _notifier = static final PropertyValueNotifier<EditorNotificationType> _notifier =
PropertyValueNotifier( PropertyValueNotifier(EditorNotificationType.none);
EditorNotificationType.none,
);
final EditorNotificationType type; final EditorNotificationType type;
void post() { void post() => _notifier.value = type;
_notifier.value = type;
}
static void addListener(ValueChanged<EditorNotificationType> listener) { static void addListener(ValueChanged<EditorNotificationType> listener) {
_notifier.addListener(() { _notifier.addListener(() => listener(_notifier.value));
listener(_notifier.value);
});
} }
static void removeListener(ValueChanged<EditorNotificationType> listener) { static void removeListener(ValueChanged<EditorNotificationType> listener) {
_notifier.removeListener(() { _notifier.removeListener(() => listener(_notifier.value));
listener(_notifier.value);
});
} }
static void dispose() { static void dispose() => _notifier.dispose();
_notifier.dispose();
}
} }

View File

@ -1,5 +1,8 @@
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_configuration.dart'; import 'package:appflowy/plugins/document/presentation/editor_configuration.dart';
@ -26,8 +29,6 @@ import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
final codeBlockLocalization = CodeBlockLocalizations( final codeBlockLocalization = CodeBlockLocalizations(
@ -210,9 +211,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
if (widget.useViewInfoBloc) { if (widget.useViewInfoBloc) {
viewInfoBloc.add( viewInfoBloc.add(
ViewInfoEvent.registerEditorState( ViewInfoEvent.registerEditorState(editorState: widget.editorState),
editorState: widget.editorState,
),
); );
} }
@ -326,25 +325,25 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
final editorState = widget.editorState; final editorState = widget.editorState;
if (PlatformExtension.isMobile) { if (PlatformExtension.isMobile) {
return AppFlowyMobileToolbar( return BlocProvider.value(
value: documentBloc,
child: AppFlowyMobileToolbar(
toolbarHeight: 42.0, toolbarHeight: 42.0,
editorState: editorState, editorState: editorState,
toolbarItemsBuilder: (selection) => buildMobileToolbarItems( toolbarItemsBuilder: (selection) =>
editorState, buildMobileToolbarItems(editorState, selection),
selection,
),
child: MobileFloatingToolbar( child: MobileFloatingToolbar(
editorState: editorState, editorState: editorState,
editorScrollController: editorScrollController, editorScrollController: editorScrollController,
toolbarBuilder: (context, anchor, closeToolbar) { toolbarBuilder: (_, anchor, closeToolbar) =>
return CustomMobileFloatingToolbar( CustomMobileFloatingToolbar(
editorState: editorState, editorState: editorState,
anchor: anchor, anchor: anchor,
closeToolbar: closeToolbar, closeToolbar: closeToolbar,
); ),
},
child: editor, child: editor,
), ),
),
); );
} }
@ -362,9 +361,8 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
List<SelectionMenuItem> _customSlashMenuItems() { List<SelectionMenuItem> _customSlashMenuItems() {
final items = [...standardSelectionMenuItems]; final items = [...standardSelectionMenuItems];
final imageItem = items.firstWhereOrNull( final imageItem = items
(element) => element.name == AppFlowyEditorL10n.current.image, .firstWhereOrNull((e) => e.name == AppFlowyEditorL10n.current.image);
);
if (imageItem != null) { if (imageItem != null) {
final imageItemIndex = items.indexOf(imageItem); final imageItemIndex = items.indexOf(imageItem);
if (imageItemIndex != -1) { if (imageItemIndex != -1) {
@ -393,15 +391,11 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
(bool, Selection?) _computeAutoFocusParameters() { (bool, Selection?) _computeAutoFocusParameters() {
if (widget.editorState.document.isEmpty) { if (widget.editorState.document.isEmpty) {
return ( return (true, Selection.collapsed(Position(path: [0])));
true,
Selection.collapsed(Position(path: [0])),
);
} }
final nodes = widget.editorState.document.root.children final nodes =
.where((element) => element.delta != null); widget.editorState.document.root.children.where((e) => e.delta != null);
final isAllEmpty = final isAllEmpty = nodes.isNotEmpty && nodes.every((e) => e.delta!.isEmpty);
nodes.isNotEmpty && nodes.every((element) => element.delta!.isEmpty);
if (isAllEmpty) { if (isAllEmpty) {
return (true, Selection.collapsed(Position(path: nodes.first.path))); return (true, Selection.collapsed(Position(path: nodes.first.path)));
} }
@ -458,12 +452,8 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
} }
void _customizeBlockComponentBackgroundColorDecorator() { void _customizeBlockComponentBackgroundColorDecorator() {
blockComponentBackgroundColorDecorator = blockComponentBackgroundColorDecorator = (Node node, String colorString) =>
(Node node, String colorString) => buildEditorCustomizedColor( buildEditorCustomizedColor(context, node, colorString);
context,
node,
colorString,
);
} }
void _initEditorL10n() => AppFlowyEditorL10n.current = EditorI18n(); void _initEditorL10n() => AppFlowyEditorL10n.current = EditorI18n();
@ -528,7 +518,5 @@ bool showInAnyTextType(EditorState editorState) {
} }
final nodes = editorState.getNodesInSelection(selection); final nodes = editorState.getNodesInSelection(selection);
return nodes.any( return nodes.any((node) => toolbarItemWhiteList.contains(node.type));
(node) => toolbarItemWhiteList.contains(node.type),
);
} }

View File

@ -1,9 +1,10 @@
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
enum MentionType { enum MentionType {
@ -69,18 +70,21 @@ class MentionBlock extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final type = MentionType.fromString(mention[MentionBlockKeys.type]); final type = MentionType.fromString(mention[MentionBlockKeys.type]);
final editorState = context.read<EditorState>();
switch (type) { switch (type) {
case MentionType.page: case MentionType.page:
final String pageId = mention[MentionBlockKeys.pageId]; final String pageId = mention[MentionBlockKeys.pageId];
return MentionPageBlock( return MentionPageBlock(
key: ValueKey(pageId), key: ValueKey(pageId),
editorState: editorState,
pageId: pageId, pageId: pageId,
node: node,
textStyle: textStyle, textStyle: textStyle,
index: index,
); );
case MentionType.date: case MentionType.date:
final String date = mention[MentionBlockKeys.date]; final String date = mention[MentionBlockKeys.date];
final editorState = context.read<EditorState>();
final reminderOption = ReminderOption.values.firstWhereOrNull( final reminderOption = ReminderOption.values.firstWhereOrNull(
(o) => o.name == mention[MentionBlockKeys.reminderOption], (o) => o.name == mention[MentionBlockKeys.reminderOption],
); );

View File

@ -1,6 +1,11 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mobile_page_selector_sheet.dart';
import 'package:appflowy/plugins/trash/application/trash_service.dart'; import 'package:appflowy/plugins/trash/application/trash_service.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
@ -9,26 +14,58 @@ import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart' import 'package:appflowy_editor/appflowy_editor.dart'
show EditorState, PlatformExtension; show
Delta,
EditorState,
Node,
PlatformExtension,
TextInsert,
TextTransaction,
paragraphNode;
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
final pageMemorizer = <String, ViewPB?>{}; final pageMemorizer = <String, ViewPB?>{};
Node pageMentionNode(String viewId) {
return paragraphNode(
delta: Delta(
operations: [
TextInsert(
'\$',
attributes: {
MentionBlockKeys.mention: {
MentionBlockKeys.type: MentionType.page.name,
MentionBlockKeys.pageId: viewId,
},
},
),
],
),
);
}
class MentionPageBlock extends StatefulWidget { class MentionPageBlock extends StatefulWidget {
const MentionPageBlock({ const MentionPageBlock({
super.key, super.key,
required this.editorState,
required this.pageId, required this.pageId,
required this.node,
required this.textStyle, required this.textStyle,
required this.index,
}); });
final EditorState editorState;
final String pageId; final String pageId;
final Node node;
final TextStyle? textStyle; final TextStyle? textStyle;
// Used to update the block
final int index;
@override @override
State<MentionPageBlock> createState() => _MentionPageBlockState(); State<MentionPageBlock> createState() => _MentionPageBlockState();
} }
@ -71,14 +108,15 @@ class _MentionPageBlockState extends State<MentionPageBlock> {
if (view == null) { if (view == null) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
// updateSelection();
final iconSize = widget.textStyle?.fontSize ?? 16.0; final iconSize = widget.textStyle?.fontSize ?? 16.0;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 2), padding: const EdgeInsets.symmetric(horizontal: 2),
child: FlowyHover( child: FlowyHover(
cursor: SystemMouseCursors.click, cursor: SystemMouseCursors.click,
child: GestureDetector( child: GestureDetector(
onTap: () => openPage(widget.pageId), onTap: handleTap,
onDoubleTap: handleDoubleTap,
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -112,24 +150,51 @@ class _MentionPageBlockState extends State<MentionPageBlock> {
); );
} }
void openPage(String pageId) async { Future<void> handleTap() async {
final view = await fetchView(pageId); final view = await fetchView(widget.pageId);
if (view == null) { if (view == null) {
Log.error('Page($pageId) not found'); Log.error('Page(${widget.pageId}) not found');
return; return;
} }
if (PlatformExtension.isDesktopOrWeb) {
getIt<TabsBloc>().add( if (PlatformExtension.isMobile && mounted) {
TabsEvent.openPlugin(
plugin: view.plugin(),
view: view,
),
);
} else {
if (mounted) {
await context.pushView(view); await context.pushView(view);
} else {
getIt<TabsBloc>().add(
TabsEvent.openPlugin(plugin: view.plugin(), view: view),
);
} }
} }
Future<void> handleDoubleTap() async {
if (!PlatformExtension.isMobile) {
return;
}
final currentViewId = context.read<DocumentBloc>().documentId;
final viewId = await showPageSelectorSheet(
context,
currentViewId: currentViewId,
selectedViewId: widget.pageId,
);
if (viewId != null) {
// Update this nodes pageId
final transaction = widget.editorState.transaction
..formatText(
widget.node,
widget.index,
1,
{
MentionBlockKeys.mention: {
MentionBlockKeys.type: MentionType.page.name,
MentionBlockKeys.pageId: viewId,
},
},
);
await widget.editorState.apply(transaction, withUpdateSelection: false);
}
} }
Future<ViewPB?> fetchView(String pageId) async { Future<ViewPB?> fetchView(String pageId) async {

View File

@ -0,0 +1,135 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/flowy_search_text_field.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
Future<String?> showPageSelectorSheet(
BuildContext context, {
String? currentViewId,
String? selectedViewId,
}) async {
return showMobileBottomSheet<String>(
context,
title: LocaleKeys.document_mobilePageSelector_title.tr(),
showHeader: true,
showCloseButton: true,
showDragHandle: true,
builder: (context) => Container(
margin: const EdgeInsets.only(top: 12.0),
constraints: const BoxConstraints(
maxHeight: 340,
minHeight: 80,
),
child: _MobilePageSelectorBody(
currentViewId: currentViewId,
selectedViewId: selectedViewId,
),
),
);
}
class _MobilePageSelectorBody extends StatefulWidget {
const _MobilePageSelectorBody({this.currentViewId, this.selectedViewId});
final String? currentViewId;
final String? selectedViewId;
@override
State<_MobilePageSelectorBody> createState() =>
_MobilePageSelectorBodyState();
}
class _MobilePageSelectorBodyState extends State<_MobilePageSelectorBody> {
final searchController = TextEditingController();
late final Future<List<ViewPB>> _viewsFuture = _fetchViews();
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
height: 44.0,
child: FlowySearchTextField(
controller: searchController,
onChanged: (_) => setState(() {}),
),
),
FutureBuilder(
future: _viewsFuture,
builder: (_, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator.adaptive());
}
if (snapshot.hasError || snapshot.data == null) {
return Center(
child: FlowyText(
LocaleKeys.document_mobilePageSelector_failedToLoad.tr(),
),
);
}
final views = snapshot.data!;
if (widget.currentViewId != null) {
views.removeWhere((v) => v.id == widget.currentViewId);
}
final filtered = views.where(
(v) =>
searchController.text.isEmpty ||
v.name
.toLowerCase()
.contains(searchController.text.toLowerCase()),
);
if (filtered.isEmpty) {
return Center(
child: FlowyText(
LocaleKeys.document_mobilePageSelector_noPagesFound.tr(),
),
);
}
return Flexible(
child: ListView(
children: filtered
.map(
(view) => FlowyOptionTile.checkbox(
leftIcon: view.icon.value.isNotEmpty
? EmojiText(
emoji: view.icon.value,
fontSize: 18,
textAlign: TextAlign.center,
lineHeight: 1.3,
)
: FlowySvg(
view.layout.icon,
size: const Size.square(20),
),
text: view.name,
isSelected: view.id == widget.selectedViewId,
onTap: () => Navigator.of(context).pop(view.id),
),
)
.toList(),
),
);
},
),
],
);
}
Future<List<ViewPB>> _fetchViews() async =>
(await ViewBackendService.getAllViews()).toNullable()?.items ?? [];
}

View File

@ -6,8 +6,11 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/type_option_menu_item.dart'; import 'package:appflowy/mobile/presentation/base/type_option_menu_item.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mobile_page_selector_sheet.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_add_block_toolbar_item.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_add_block_toolbar_item.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
@ -15,6 +18,7 @@ import 'package:appflowy/startup/tasks/app_widget.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
final addBlockToolbarItem = AppFlowyMobileToolbarItem( final addBlockToolbarItem = AppFlowyMobileToolbarItem(
@ -41,15 +45,12 @@ final addBlockToolbarItem = AppFlowyMobileToolbarItem(
keepEditorFocusNotifier.increase(); keepEditorFocusNotifier.increase();
final didAddBlock = await showAddBlockMenu( final didAddBlock = await showAddBlockMenu(
AppGlobals.rootNavKey.currentContext!, AppGlobals.rootNavKey.currentContext!,
documentBloc: context.read<DocumentBloc>(),
editorState: editorState, editorState: editorState,
selection: selection!, selection: selection!,
); );
if (didAddBlock != true) { if (didAddBlock != true) {
unawaited( unawaited(editorState.updateSelectionWithReason(selection));
editorState.updateSelectionWithReason(
selection,
),
);
} }
}); });
}, },
@ -59,6 +60,7 @@ final addBlockToolbarItem = AppFlowyMobileToolbarItem(
Future<bool?> showAddBlockMenu( Future<bool?> showAddBlockMenu(
BuildContext context, { BuildContext context, {
required DocumentBloc documentBloc,
required EditorState editorState, required EditorState editorState,
required Selection selection, required Selection selection,
}) async { }) async {
@ -73,15 +75,16 @@ Future<bool?> showAddBlockMenu(
backgroundColor: theme.toolbarMenuBackgroundColor, backgroundColor: theme.toolbarMenuBackgroundColor,
elevation: 20, elevation: 20,
enableDraggableScrollable: true, enableDraggableScrollable: true,
builder: (context) { builder: (_) => Padding(
return Padding(
padding: EdgeInsets.all(16 * context.scale), padding: EdgeInsets.all(16 * context.scale),
child: BlocProvider.value(
value: documentBloc,
child: _AddBlockMenu( child: _AddBlockMenu(
selection: selection, selection: selection,
editorState: editorState, editorState: editorState,
), ),
); ),
}, ),
); );
} }
@ -96,20 +99,21 @@ class _AddBlockMenu extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return TypeOptionMenu<String>( return BlocProvider.value(
value: context.read<DocumentBloc>(),
child: TypeOptionMenu<String>(
values: buildTypeOptionMenuItemValues(context), values: buildTypeOptionMenuItemValues(context),
scaleFactor: context.scale, scaleFactor: context.scale,
),
); );
} }
Future<void> _insertBlock(Node node) async { Future<void> _insertBlock(Node node) async {
AppGlobals.rootNavKey.currentContext?.pop(true); AppGlobals.rootNavKey.currentContext?.pop(true);
Future.delayed(const Duration(milliseconds: 100), () { Future.delayed(
editorState.insertBlockAfterCurrentSelection( const Duration(milliseconds: 100),
selection, () => editorState.insertBlockAfterCurrentSelection(selection, node),
node,
); );
});
} }
List<TypeOptionMenuItemValue<String>> buildTypeOptionMenuItemValues( List<TypeOptionMenuItemValue<String>> buildTypeOptionMenuItemValues(
@ -208,11 +212,36 @@ class _AddBlockMenu extends StatelessWidget {
// date // date
TypeOptionMenuItemValue( TypeOptionMenuItemValue(
value: ParagraphBlockKeys.type, value: ParagraphBlockKeys.type,
backgroundColor: colorMap['date']!, backgroundColor: colorMap[MentionBlockKeys.type]!,
text: LocaleKeys.editor_date.tr(), text: LocaleKeys.editor_date.tr(),
icon: FlowySvgs.m_add_block_date_s, icon: FlowySvgs.m_add_block_date_s,
onTap: (_, __) => _insertBlock(dateMentionNode()), onTap: (_, __) => _insertBlock(dateMentionNode()),
), ),
// page
TypeOptionMenuItemValue(
value: ParagraphBlockKeys.type,
backgroundColor: colorMap[MentionBlockKeys.type]!,
text: LocaleKeys.editor_page.tr(),
icon: FlowySvgs.document_s,
onTap: (_, __) async {
AppGlobals.rootNavKey.currentContext?.pop(true);
final currentViewId = context.read<DocumentBloc>().documentId;
final viewId = await showPageSelectorSheet(
context,
currentViewId: currentViewId,
);
if (viewId != null) {
Future.delayed(const Duration(milliseconds: 100), () {
editorState.insertBlockAfterCurrentSelection(
selection,
pageMentionNode(viewId),
);
});
}
},
),
// divider // divider
TypeOptionMenuItemValue( TypeOptionMenuItemValue(
@ -270,7 +299,7 @@ class _AddBlockMenu extends StatelessWidget {
NumberedListBlockKeys.type: const Color(0xFFA35F94), NumberedListBlockKeys.type: const Color(0xFFA35F94),
ToggleListBlockKeys.type: const Color(0xFFA35F94), ToggleListBlockKeys.type: const Color(0xFFA35F94),
ImageBlockKeys.type: const Color(0xFFBAAC74), ImageBlockKeys.type: const Color(0xFFBAAC74),
'date': const Color(0xFF40AAB8), MentionBlockKeys.type: const Color(0xFF40AAB8),
DividerBlockKeys.type: const Color(0xFF4BB299), DividerBlockKeys.type: const Color(0xFF4BB299),
CalloutBlockKeys.type: const Color(0xFF66599B), CalloutBlockKeys.type: const Color(0xFF66599B),
CodeBlockKeys.type: const Color(0xFF66599B), CodeBlockKeys.type: const Color(0xFF66599B),
@ -286,7 +315,7 @@ class _AddBlockMenu extends StatelessWidget {
NumberedListBlockKeys.type: const Color(0xFFFFB9EF), NumberedListBlockKeys.type: const Color(0xFFFFB9EF),
ToggleListBlockKeys.type: const Color(0xFFFFB9EF), ToggleListBlockKeys.type: const Color(0xFFFFB9EF),
ImageBlockKeys.type: const Color(0xFFFDEDA7), ImageBlockKeys.type: const Color(0xFFFDEDA7),
'date': const Color(0xFF91EAF5), MentionBlockKeys.type: const Color(0xFF91EAF5),
DividerBlockKeys.type: const Color(0xFF98F4CD), DividerBlockKeys.type: const Color(0xFF98F4CD),
CalloutBlockKeys.type: const Color(0xFFCABDFF), CalloutBlockKeys.type: const Color(0xFFCABDFF),
CodeBlockKeys.type: const Color(0xFFCABDFF), CodeBlockKeys.type: const Color(0xFFCABDFF),

View File

@ -1,6 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_close_keyboard_or_menu_button.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_close_keyboard_or_menu_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar_item.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar_item.dart';
@ -8,8 +12,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_too
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
@ -66,9 +69,7 @@ class _AppFlowyMobileToolbarState extends State<AppFlowyMobileToolbar> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
children: [ children: [
Expanded( Expanded(child: widget.child),
child: widget.child,
),
// add a bottom offset to make sure the toolbar is above the keyboard // add a bottom offset to make sure the toolbar is above the keyboard
ValueListenableBuilder( ValueListenableBuilder(
valueListenable: isKeyboardShow, valueListenable: isKeyboardShow,
@ -108,11 +109,14 @@ class _AppFlowyMobileToolbarState extends State<AppFlowyMobileToolbar> {
} }
return RepaintBoundary( return RepaintBoundary(
child: BlocProvider.value(
value: context.read<DocumentBloc>(),
child: _MobileToolbar( child: _MobileToolbar(
editorState: widget.editorState, editorState: widget.editorState,
toolbarItems: widget.toolbarItemsBuilder(selection), toolbarItems: widget.toolbarItemsBuilder(selection),
toolbarHeight: widget.toolbarHeight, toolbarHeight: widget.toolbarHeight,
), ),
),
); );
}, },
); );

View File

@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class MobileToolbarMenuItemWrapper extends StatelessWidget { class MobileToolbarMenuItemWrapper extends StatelessWidget {
const MobileToolbarMenuItemWrapper({ const MobileToolbarMenuItemWrapper({
@ -66,10 +67,7 @@ class MobileToolbarMenuItemWrapper extends StatelessWidget {
final radius = Radius.circular(12 * scale); final radius = Radius.circular(12 * scale);
final Widget child; final Widget child;
if (icon != null) { if (icon != null) {
child = FlowySvg( child = FlowySvg(icon!, color: iconColor);
icon!,
color: iconColor,
);
} else if (text != null) { } else if (text != null) {
child = Padding( child = Padding(
padding: textPadding * scale, padding: textPadding * scale,
@ -137,16 +135,12 @@ class ScaledVerticalDivider extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return HSpace( return HSpace(1.5 * context.scale);
1.5 * context.scale,
);
} }
} }
class ScaledVSpace extends StatelessWidget { class ScaledVSpace extends StatelessWidget {
const ScaledVSpace({ const ScaledVSpace({super.key});
super.key,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -166,10 +160,7 @@ final _blocksCanContainChildren = [
]; ];
extension MobileToolbarEditorState on EditorState { extension MobileToolbarEditorState on EditorState {
bool isBlockTypeSelected( bool isBlockTypeSelected(String blockType, {int? level}) {
String blockType, {
int? level,
}) {
final selection = this.selection; final selection = this.selection;
if (selection == null) { if (selection == null) {
return false; return false;
@ -186,9 +177,7 @@ extension MobileToolbarEditorState on EditorState {
return type == blockType; return type == blockType;
} }
bool isTextDecorationSelected( bool isTextDecorationSelected(String richTextKey) {
String richTextKey,
) {
final selection = this.selection; final selection = this.selection;
if (selection == null) { if (selection == null) {
return false; return false;
@ -207,18 +196,16 @@ extension MobileToolbarEditorState on EditorState {
start: selection.start.copyWith( start: selection.start.copyWith(
offset: selection.startIndex - 1, offset: selection.startIndex - 1,
), ),
), (delta) { ),
return delta.everyAttributes( (delta) => delta.everyAttributes(
(attributes) => attributes[richTextKey] == true, (attributes) => attributes[richTextKey] == true,
),
); );
});
} }
} }
} else { } else {
isSelected = nodes.allSatisfyInSelection(selection, (delta) { isSelected = nodes.allSatisfyInSelection(selection, (delta) {
return delta.everyAttributes( return delta.everyAttributes((attr) => attr[richTextKey] == true);
(attributes) => attributes[richTextKey] == true,
);
}); });
} }
return isSelected; return isSelected;
@ -321,9 +308,7 @@ extension MobileToolbarEditorState on EditorState {
text.isNotEmpty && text.isNotEmpty &&
selection.isCollapsed) { selection.isCollapsed) {
final attributes = href != null && href.isNotEmpty final attributes = href != null && href.isNotEmpty
? { ? {AppFlowyRichTextKeys.href: href}
AppFlowyRichTextKeys.href: href,
}
: null; : null;
transaction.insertText( transaction.insertText(
node, node,
@ -348,9 +333,7 @@ extension MobileToolbarEditorState on EditorState {
node, node,
selection.startIndex, selection.startIndex,
text.length, text.length,
{ {AppFlowyRichTextKeys.href: href?.isEmpty == true ? null : href},
AppFlowyRichTextKeys.href: href?.isEmpty == true ? null : href,
},
); );
} }

View File

@ -1,5 +1,8 @@
import 'dart:math'; import 'dart:math';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
@ -13,17 +16,12 @@ import 'package:appflowy/workspace/application/settings/appearance/appearance_cu
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
class EditorStyleCustomizer { class EditorStyleCustomizer {
EditorStyleCustomizer({ EditorStyleCustomizer({required this.context, required this.padding});
required this.context,
required this.padding,
});
final BuildContext context; final BuildContext context;
final EdgeInsets padding; final EdgeInsets padding;
@ -63,9 +61,7 @@ class EditorStyleCustomizer {
bold: baseTextStyle(fontFamily, fontWeight: FontWeight.bold).copyWith( bold: baseTextStyle(fontFamily, fontWeight: FontWeight.bold).copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
italic: baseTextStyle(fontFamily).copyWith( italic: baseTextStyle(fontFamily).copyWith(fontStyle: FontStyle.italic),
fontStyle: FontStyle.italic,
),
underline: baseTextStyle(fontFamily).copyWith( underline: baseTextStyle(fontFamily).copyWith(
decoration: TextDecoration.underline, decoration: TextDecoration.underline,
), ),
@ -110,15 +106,9 @@ class EditorStyleCustomizer {
color: theme.colorScheme.onBackground, color: theme.colorScheme.onBackground,
height: lineHeight, height: lineHeight,
), ),
bold: baseTextStyle.copyWith( bold: baseTextStyle.copyWith(fontWeight: FontWeight.w600),
fontWeight: FontWeight.w600, italic: baseTextStyle.copyWith(fontStyle: FontStyle.italic),
), underline: baseTextStyle.copyWith(decoration: TextDecoration.underline),
italic: baseTextStyle.copyWith(
fontStyle: FontStyle.italic,
),
underline: baseTextStyle.copyWith(
decoration: TextDecoration.underline,
),
strikethrough: baseTextStyle.copyWith( strikethrough: baseTextStyle.copyWith(
decoration: TextDecoration.lineThrough, decoration: TextDecoration.lineThrough,
), ),
@ -175,25 +165,23 @@ class EditorStyleCustomizer {
} }
TextStyle codeBlockStyleBuilder() { TextStyle codeBlockStyleBuilder() {
final theme = Theme.of(context);
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize; final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
final fontFamily = final fontFamily =
context.read<DocumentAppearanceCubit>().state.codeFontFamily; context.read<DocumentAppearanceCubit>().state.codeFontFamily;
return baseTextStyle(fontFamily).copyWith( return baseTextStyle(fontFamily).copyWith(
fontSize: fontSize, fontSize: fontSize,
height: 1.5, height: 1.5,
color: theme.colorScheme.onBackground, color: Theme.of(context).colorScheme.onBackground,
); );
} }
TextStyle outlineBlockPlaceholderStyleBuilder() { TextStyle outlineBlockPlaceholderStyleBuilder() {
final theme = Theme.of(context);
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize; final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
return TextStyle( return TextStyle(
fontFamily: builtInFontFamily(), fontFamily: builtInFontFamily(),
fontSize: fontSize, fontSize: fontSize,
height: 1.5, height: 1.5,
color: theme.colorScheme.onBackground.withOpacity(0.6), color: Theme.of(context).colorScheme.onBackground.withOpacity(0.6),
); );
} }
@ -220,38 +208,22 @@ class EditorStyleCustomizer {
); );
} }
FloatingToolbarStyle floatingToolbarStyleBuilder() { FloatingToolbarStyle floatingToolbarStyleBuilder() => FloatingToolbarStyle(
final theme = Theme.of(context); backgroundColor: Theme.of(context).colorScheme.onTertiary,
return FloatingToolbarStyle(
backgroundColor: theme.colorScheme.onTertiary,
); );
}
TextStyle baseTextStyle( TextStyle baseTextStyle(String? fontFamily, {FontWeight? fontWeight}) {
String? fontFamily, {
FontWeight? fontWeight,
}) {
if (fontFamily == null) { if (fontFamily == null) {
return TextStyle( return TextStyle(fontWeight: fontWeight);
fontWeight: fontWeight,
);
} }
try { try {
return GoogleFonts.getFont( return GoogleFonts.getFont(fontFamily, fontWeight: fontWeight);
fontFamily,
fontWeight: fontWeight,
);
} on Exception { } on Exception {
if ([builtInFontFamily(), builtInCodeFontFamily].contains(fontFamily)) { if ([builtInFontFamily(), builtInCodeFontFamily].contains(fontFamily)) {
return TextStyle( return TextStyle(fontFamily: fontFamily, fontWeight: fontWeight);
fontFamily: fontFamily,
fontWeight: fontWeight,
);
} }
return TextStyle( return TextStyle(fontWeight: fontWeight);
fontWeight: fontWeight,
);
} }
} }
@ -281,7 +253,7 @@ class EditorStyleCustomizer {
), ),
); );
} }
} catch (e) { } catch (_) {
// ignore // ignore
} }
} }
@ -334,18 +306,13 @@ class EditorStyleCustomizer {
..onTap = () { ..onTap = () {
final editorState = context.read<EditorState>(); final editorState = context.read<EditorState>();
if (editorState.selection == null) { if (editorState.selection == null) {
afLaunchUrlString( afLaunchUrlString(href, addingHttpSchemeWhenFailed: true);
href,
addingHttpSchemeWhenFailed: true,
);
return; return;
} }
editorState.updateSelectionWithReason( editorState.updateSelectionWithReason(
editorState.selection, editorState.selection,
extraInfo: { extraInfo: {selectionExtraInfoDisableMobileToolbarKey: true},
selectionExtraInfoDisableMobileToolbarKey: true,
},
); );
showEditLinkBottomSheet( showEditLinkBottomSheet(

View File

@ -204,9 +204,9 @@ class _ApplicationWidgetState extends State<ApplicationWidget> {
), ),
child: overlayManagerBuilder( child: overlayManagerBuilder(
context, context,
FeatureFlag.search.isOn !PlatformExtension.isMobile && FeatureFlag.search.isOn
? CommandPalette( ? CommandPalette(
toggleNotifier: _commandPaletteNotifier, notifier: _commandPaletteNotifier,
child: child, child: child,
) )
: child, : child,

View File

@ -16,18 +16,12 @@ class CommandPalette extends InheritedWidget {
CommandPalette({ CommandPalette({
super.key, super.key,
required Widget? child, required Widget? child,
required ValueNotifier<bool> toggleNotifier, required this.notifier,
}) : _toggleNotifier = toggleNotifier, }) : super(
super( child: _CommandPaletteController(notifier: notifier, child: child),
child: _CommandPaletteController(
toggleNotifier: toggleNotifier,
child: child,
),
); );
final ValueNotifier<bool> _toggleNotifier; final ValueNotifier<bool> notifier;
void toggle() => _toggleNotifier.value = !_toggleNotifier.value;
static CommandPalette of(BuildContext context) { static CommandPalette of(BuildContext context) {
final CommandPalette? result = final CommandPalette? result =
@ -38,6 +32,8 @@ class CommandPalette extends InheritedWidget {
return result!; return result!;
} }
void toggle() => notifier.value = !notifier.value;
@override @override
bool updateShouldNotify(covariant InheritedWidget oldWidget) => false; bool updateShouldNotify(covariant InheritedWidget oldWidget) => false;
} }
@ -48,12 +44,12 @@ class _ToggleCommandPaletteIntent extends Intent {
class _CommandPaletteController extends StatefulWidget { class _CommandPaletteController extends StatefulWidget {
const _CommandPaletteController({ const _CommandPaletteController({
required this.toggleNotifier,
required this.child, required this.child,
required this.notifier,
}); });
final Widget? child; final Widget? child;
final ValueNotifier<bool> toggleNotifier; final ValueNotifier<bool> notifier;
@override @override
State<_CommandPaletteController> createState() => State<_CommandPaletteController> createState() =>
@ -61,26 +57,9 @@ class _CommandPaletteController extends StatefulWidget {
} }
class _CommandPaletteControllerState extends State<_CommandPaletteController> { class _CommandPaletteControllerState extends State<_CommandPaletteController> {
late ValueNotifier<bool> _toggleNotifier = widget.toggleNotifier; late final ValueNotifier<bool> _toggleNotifier = widget.notifier;
bool _isOpen = false; bool _isOpen = false;
@override
void didUpdateWidget(covariant _CommandPaletteController oldWidget) {
if (oldWidget.toggleNotifier != widget.toggleNotifier) {
_toggleNotifier.removeListener(_onToggle);
_toggleNotifier.dispose();
_toggleNotifier = widget.toggleNotifier;
// If widget is changed, eg. on theme mode hotkey used
// while modal is shown, set the value before listening
_toggleNotifier.value = _isOpen;
_toggleNotifier.addListener(_onToggle);
}
super.didUpdateWidget(oldWidget);
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();

View File

@ -1082,6 +1082,11 @@
"errorBlock": { "errorBlock": {
"theBlockIsNotSupported": "The current version does not support this block.", "theBlockIsNotSupported": "The current version does not support this block.",
"blockContentHasBeenCopied": "The block content has been copied." "blockContentHasBeenCopied": "The block content has been copied."
},
"mobilePageSelector": {
"title": "Select page",
"failedToLoad": "Failed to load page list",
"noPagesFound": "No pages found"
} }
}, },
"board": { "board": {
@ -1328,6 +1333,7 @@
"color": "Color", "color": "Color",
"image": "Image", "image": "Image",
"date": "Date", "date": "Date",
"page": "Page",
"italic": "Italic", "italic": "Italic",
"link": "Link", "link": "Link",
"numberedList": "Numbered List", "numberedList": "Numbered List",