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/plugins/database/grid/application/row/row_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:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class RowDocument extends StatelessWidget {
@ -82,9 +83,7 @@ class _RowEditorState extends State<RowEditor> {
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider.value(value: documentBloc),
],
providers: [BlocProvider.value(value: documentBloc)],
child: BlocListener<DocumentBloc, DocumentState>(
listenWhen: (previous, current) =>
previous.isDocumentEmpty != current.isDocumentEmpty,

View File

@ -1,5 +1,7 @@
library document_plugin;
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.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:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
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/mobile/application/page_style/document_page_style_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:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class DocumentPage extends StatefulWidget {
@ -153,9 +154,9 @@ class _DocumentPageState extends State<DocumentPage>
onRestore: () => context.read<DocumentBloc>().add(
const DocumentEvent.restorePage(),
),
onDelete: () => context.read<DocumentBloc>().add(
const DocumentEvent.deletePermanently(),
),
onDelete: () => context
.read<DocumentBloc>()
.add(const DocumentEvent.deletePermanently()),
);
}
@ -178,12 +179,10 @@ class _DocumentPageState extends State<DocumentPage>
node: page,
editorState: editorState,
view: widget.view,
onIconChanged: (icon) async {
await ViewBackendService.updateViewIcon(
viewId: widget.view.id,
viewIcon: icon,
);
},
onIconChanged: (icon) async => ViewBackendService.updateViewIcon(
viewId: widget.view.id,
viewIcon: icon,
),
);
}
@ -196,10 +195,9 @@ class _DocumentPageState extends State<DocumentPage>
undoCommand.execute(editorState);
} else if (type == EditorNotificationType.redo) {
redoCommand.execute(editorState);
} else if (type == EditorNotificationType.exitEditing) {
if (editorState.selection != null) {
editorState.selection = null;
}
} else if (type == EditorNotificationType.exitEditing &&
editorState.selection != null) {
editorState.selection = null;
}
}

View File

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

View File

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

View File

@ -1,5 +1,8 @@
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/plugins/document/application/document_bloc.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:flowy_infra/theme_extension.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';
final codeBlockLocalization = CodeBlockLocalizations(
@ -210,9 +211,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
if (widget.useViewInfoBloc) {
viewInfoBloc.add(
ViewInfoEvent.registerEditorState(
editorState: widget.editorState,
),
ViewInfoEvent.registerEditorState(editorState: widget.editorState),
);
}
@ -326,24 +325,24 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
final editorState = widget.editorState;
if (PlatformExtension.isMobile) {
return AppFlowyMobileToolbar(
toolbarHeight: 42.0,
editorState: editorState,
toolbarItemsBuilder: (selection) => buildMobileToolbarItems(
editorState,
selection,
),
child: MobileFloatingToolbar(
return BlocProvider.value(
value: documentBloc,
child: AppFlowyMobileToolbar(
toolbarHeight: 42.0,
editorState: editorState,
editorScrollController: editorScrollController,
toolbarBuilder: (context, anchor, closeToolbar) {
return CustomMobileFloatingToolbar(
toolbarItemsBuilder: (selection) =>
buildMobileToolbarItems(editorState, selection),
child: MobileFloatingToolbar(
editorState: editorState,
editorScrollController: editorScrollController,
toolbarBuilder: (_, anchor, closeToolbar) =>
CustomMobileFloatingToolbar(
editorState: editorState,
anchor: anchor,
closeToolbar: closeToolbar,
);
},
child: editor,
),
child: editor,
),
),
);
}
@ -362,9 +361,8 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
List<SelectionMenuItem> _customSlashMenuItems() {
final items = [...standardSelectionMenuItems];
final imageItem = items.firstWhereOrNull(
(element) => element.name == AppFlowyEditorL10n.current.image,
);
final imageItem = items
.firstWhereOrNull((e) => e.name == AppFlowyEditorL10n.current.image);
if (imageItem != null) {
final imageItemIndex = items.indexOf(imageItem);
if (imageItemIndex != -1) {
@ -393,15 +391,11 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
(bool, Selection?) _computeAutoFocusParameters() {
if (widget.editorState.document.isEmpty) {
return (
true,
Selection.collapsed(Position(path: [0])),
);
return (true, Selection.collapsed(Position(path: [0])));
}
final nodes = widget.editorState.document.root.children
.where((element) => element.delta != null);
final isAllEmpty =
nodes.isNotEmpty && nodes.every((element) => element.delta!.isEmpty);
final nodes =
widget.editorState.document.root.children.where((e) => e.delta != null);
final isAllEmpty = nodes.isNotEmpty && nodes.every((e) => e.delta!.isEmpty);
if (isAllEmpty) {
return (true, Selection.collapsed(Position(path: nodes.first.path)));
}
@ -458,12 +452,8 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
}
void _customizeBlockComponentBackgroundColorDecorator() {
blockComponentBackgroundColorDecorator =
(Node node, String colorString) => buildEditorCustomizedColor(
context,
node,
colorString,
);
blockComponentBackgroundColorDecorator = (Node node, String colorString) =>
buildEditorCustomizedColor(context, node, colorString);
}
void _initEditorL10n() => AppFlowyEditorL10n.current = EditorI18n();
@ -528,7 +518,5 @@ bool showInAnyTextType(EditorState editorState) {
}
final nodes = editorState.getNodesInSelection(selection);
return nodes.any(
(node) => toolbarItemWhiteList.contains(node.type),
);
return nodes.any((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_page_block.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
enum MentionType {
@ -69,18 +70,21 @@ class MentionBlock extends StatelessWidget {
@override
Widget build(BuildContext context) {
final type = MentionType.fromString(mention[MentionBlockKeys.type]);
final editorState = context.read<EditorState>();
switch (type) {
case MentionType.page:
final String pageId = mention[MentionBlockKeys.pageId];
return MentionPageBlock(
key: ValueKey(pageId),
editorState: editorState,
pageId: pageId,
node: node,
textStyle: textStyle,
index: index,
);
case MentionType.date:
final String date = mention[MentionBlockKeys.date];
final editorState = context.read<EditorState>();
final reminderOption = ReminderOption.values.firstWhereOrNull(
(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/mobile/application/mobile_router.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/startup/startup.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/protobuf/flowy-folder/protobuf.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:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
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 {
const MentionPageBlock({
super.key,
required this.editorState,
required this.pageId,
required this.node,
required this.textStyle,
required this.index,
});
final EditorState editorState;
final String pageId;
final Node node;
final TextStyle? textStyle;
// Used to update the block
final int index;
@override
State<MentionPageBlock> createState() => _MentionPageBlockState();
}
@ -71,14 +108,15 @@ class _MentionPageBlockState extends State<MentionPageBlock> {
if (view == null) {
return const SizedBox.shrink();
}
// updateSelection();
final iconSize = widget.textStyle?.fontSize ?? 16.0;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 2),
child: FlowyHover(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () => openPage(widget.pageId),
onTap: handleTap,
onDoubleTap: handleDoubleTap,
behavior: HitTestBehavior.translucent,
child: Row(
mainAxisSize: MainAxisSize.min,
@ -112,23 +150,50 @@ class _MentionPageBlockState extends State<MentionPageBlock> {
);
}
void openPage(String pageId) async {
final view = await fetchView(pageId);
Future<void> handleTap() async {
final view = await fetchView(widget.pageId);
if (view == null) {
Log.error('Page($pageId) not found');
Log.error('Page(${widget.pageId}) not found');
return;
}
if (PlatformExtension.isDesktopOrWeb) {
getIt<TabsBloc>().add(
TabsEvent.openPlugin(
plugin: view.plugin(),
view: view,
),
);
if (PlatformExtension.isMobile && mounted) {
await context.pushView(view);
} else {
if (mounted) {
await context.pushView(view);
}
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);
}
}

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/mobile/presentation/base/type_option_menu_item.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/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_v3/aa_menu/_toolbar_theme.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_plugins/appflowy_editor_plugins.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
final addBlockToolbarItem = AppFlowyMobileToolbarItem(
@ -41,15 +45,12 @@ final addBlockToolbarItem = AppFlowyMobileToolbarItem(
keepEditorFocusNotifier.increase();
final didAddBlock = await showAddBlockMenu(
AppGlobals.rootNavKey.currentContext!,
documentBloc: context.read<DocumentBloc>(),
editorState: editorState,
selection: selection!,
);
if (didAddBlock != true) {
unawaited(
editorState.updateSelectionWithReason(
selection,
),
);
unawaited(editorState.updateSelectionWithReason(selection));
}
});
},
@ -59,6 +60,7 @@ final addBlockToolbarItem = AppFlowyMobileToolbarItem(
Future<bool?> showAddBlockMenu(
BuildContext context, {
required DocumentBloc documentBloc,
required EditorState editorState,
required Selection selection,
}) async {
@ -73,15 +75,16 @@ Future<bool?> showAddBlockMenu(
backgroundColor: theme.toolbarMenuBackgroundColor,
elevation: 20,
enableDraggableScrollable: true,
builder: (context) {
return Padding(
padding: EdgeInsets.all(16 * context.scale),
builder: (_) => Padding(
padding: EdgeInsets.all(16 * context.scale),
child: BlocProvider.value(
value: documentBloc,
child: _AddBlockMenu(
selection: selection,
editorState: editorState,
),
);
},
),
),
);
}
@ -96,20 +99,21 @@ class _AddBlockMenu extends StatelessWidget {
@override
Widget build(BuildContext context) {
return TypeOptionMenu<String>(
values: buildTypeOptionMenuItemValues(context),
scaleFactor: context.scale,
return BlocProvider.value(
value: context.read<DocumentBloc>(),
child: TypeOptionMenu<String>(
values: buildTypeOptionMenuItemValues(context),
scaleFactor: context.scale,
),
);
}
Future<void> _insertBlock(Node node) async {
AppGlobals.rootNavKey.currentContext?.pop(true);
Future.delayed(const Duration(milliseconds: 100), () {
editorState.insertBlockAfterCurrentSelection(
selection,
node,
);
});
Future.delayed(
const Duration(milliseconds: 100),
() => editorState.insertBlockAfterCurrentSelection(selection, node),
);
}
List<TypeOptionMenuItemValue<String>> buildTypeOptionMenuItemValues(
@ -208,11 +212,36 @@ class _AddBlockMenu extends StatelessWidget {
// date
TypeOptionMenuItemValue(
value: ParagraphBlockKeys.type,
backgroundColor: colorMap['date']!,
backgroundColor: colorMap[MentionBlockKeys.type]!,
text: LocaleKeys.editor_date.tr(),
icon: FlowySvgs.m_add_block_date_s,
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
TypeOptionMenuItemValue(
@ -270,7 +299,7 @@ class _AddBlockMenu extends StatelessWidget {
NumberedListBlockKeys.type: const Color(0xFFA35F94),
ToggleListBlockKeys.type: const Color(0xFFA35F94),
ImageBlockKeys.type: const Color(0xFFBAAC74),
'date': const Color(0xFF40AAB8),
MentionBlockKeys.type: const Color(0xFF40AAB8),
DividerBlockKeys.type: const Color(0xFF4BB299),
CalloutBlockKeys.type: const Color(0xFF66599B),
CodeBlockKeys.type: const Color(0xFF66599B),
@ -286,7 +315,7 @@ class _AddBlockMenu extends StatelessWidget {
NumberedListBlockKeys.type: const Color(0xFFFFB9EF),
ToggleListBlockKeys.type: const Color(0xFFFFB9EF),
ImageBlockKeys.type: const Color(0xFFFDEDA7),
'date': const Color(0xFF91EAF5),
MentionBlockKeys.type: const Color(0xFF91EAF5),
DividerBlockKeys.type: const Color(0xFF98F4CD),
CalloutBlockKeys.type: const Color(0xFFCABDFF),
CodeBlockKeys.type: const Color(0xFFCABDFF),

View File

@ -1,6 +1,10 @@
import 'dart:async';
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/_toolbar_theme.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:collection/collection.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:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
@ -66,9 +69,7 @@ class _AppFlowyMobileToolbarState extends State<AppFlowyMobileToolbar> {
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: widget.child,
),
Expanded(child: widget.child),
// add a bottom offset to make sure the toolbar is above the keyboard
ValueListenableBuilder(
valueListenable: isKeyboardShow,
@ -108,10 +109,13 @@ class _AppFlowyMobileToolbarState extends State<AppFlowyMobileToolbar> {
}
return RepaintBoundary(
child: _MobileToolbar(
editorState: widget.editorState,
toolbarItems: widget.toolbarItemsBuilder(selection),
toolbarHeight: widget.toolbarHeight,
child: BlocProvider.value(
value: context.read<DocumentBloc>(),
child: _MobileToolbar(
editorState: widget.editorState,
toolbarItems: widget.toolbarItemsBuilder(selection),
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/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class MobileToolbarMenuItemWrapper extends StatelessWidget {
const MobileToolbarMenuItemWrapper({
@ -66,10 +67,7 @@ class MobileToolbarMenuItemWrapper extends StatelessWidget {
final radius = Radius.circular(12 * scale);
final Widget child;
if (icon != null) {
child = FlowySvg(
icon!,
color: iconColor,
);
child = FlowySvg(icon!, color: iconColor);
} else if (text != null) {
child = Padding(
padding: textPadding * scale,
@ -137,16 +135,12 @@ class ScaledVerticalDivider extends StatelessWidget {
@override
Widget build(BuildContext context) {
return HSpace(
1.5 * context.scale,
);
return HSpace(1.5 * context.scale);
}
}
class ScaledVSpace extends StatelessWidget {
const ScaledVSpace({
super.key,
});
const ScaledVSpace({super.key});
@override
Widget build(BuildContext context) {
@ -166,10 +160,7 @@ final _blocksCanContainChildren = [
];
extension MobileToolbarEditorState on EditorState {
bool isBlockTypeSelected(
String blockType, {
int? level,
}) {
bool isBlockTypeSelected(String blockType, {int? level}) {
final selection = this.selection;
if (selection == null) {
return false;
@ -186,9 +177,7 @@ extension MobileToolbarEditorState on EditorState {
return type == blockType;
}
bool isTextDecorationSelected(
String richTextKey,
) {
bool isTextDecorationSelected(String richTextKey) {
final selection = this.selection;
if (selection == null) {
return false;
@ -203,22 +192,20 @@ extension MobileToolbarEditorState on EditorState {
if (selection.startIndex != 0) {
// get previous index text style
isSelected = nodes.allSatisfyInSelection(
selection.copyWith(
start: selection.start.copyWith(
offset: selection.startIndex - 1,
),
), (delta) {
return delta.everyAttributes(
selection.copyWith(
start: selection.start.copyWith(
offset: selection.startIndex - 1,
),
),
(delta) => delta.everyAttributes(
(attributes) => attributes[richTextKey] == true,
);
});
),
);
}
}
} else {
isSelected = nodes.allSatisfyInSelection(selection, (delta) {
return delta.everyAttributes(
(attributes) => attributes[richTextKey] == true,
);
return delta.everyAttributes((attr) => attr[richTextKey] == true);
});
}
return isSelected;
@ -321,9 +308,7 @@ extension MobileToolbarEditorState on EditorState {
text.isNotEmpty &&
selection.isCollapsed) {
final attributes = href != null && href.isNotEmpty
? {
AppFlowyRichTextKeys.href: href,
}
? {AppFlowyRichTextKeys.href: href}
: null;
transaction.insertText(
node,
@ -348,9 +333,7 @@ extension MobileToolbarEditorState on EditorState {
node,
selection.startIndex,
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 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.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_editor/appflowy_editor.dart' hide Log;
import 'package:collection/collection.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
class EditorStyleCustomizer {
EditorStyleCustomizer({
required this.context,
required this.padding,
});
EditorStyleCustomizer({required this.context, required this.padding});
final BuildContext context;
final EdgeInsets padding;
@ -63,9 +61,7 @@ class EditorStyleCustomizer {
bold: baseTextStyle(fontFamily, fontWeight: FontWeight.bold).copyWith(
fontWeight: FontWeight.w600,
),
italic: baseTextStyle(fontFamily).copyWith(
fontStyle: FontStyle.italic,
),
italic: baseTextStyle(fontFamily).copyWith(fontStyle: FontStyle.italic),
underline: baseTextStyle(fontFamily).copyWith(
decoration: TextDecoration.underline,
),
@ -110,15 +106,9 @@ class EditorStyleCustomizer {
color: theme.colorScheme.onBackground,
height: lineHeight,
),
bold: baseTextStyle.copyWith(
fontWeight: FontWeight.w600,
),
italic: baseTextStyle.copyWith(
fontStyle: FontStyle.italic,
),
underline: baseTextStyle.copyWith(
decoration: TextDecoration.underline,
),
bold: baseTextStyle.copyWith(fontWeight: FontWeight.w600),
italic: baseTextStyle.copyWith(fontStyle: FontStyle.italic),
underline: baseTextStyle.copyWith(decoration: TextDecoration.underline),
strikethrough: baseTextStyle.copyWith(
decoration: TextDecoration.lineThrough,
),
@ -175,25 +165,23 @@ class EditorStyleCustomizer {
}
TextStyle codeBlockStyleBuilder() {
final theme = Theme.of(context);
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
final fontFamily =
context.read<DocumentAppearanceCubit>().state.codeFontFamily;
return baseTextStyle(fontFamily).copyWith(
fontSize: fontSize,
height: 1.5,
color: theme.colorScheme.onBackground,
color: Theme.of(context).colorScheme.onBackground,
);
}
TextStyle outlineBlockPlaceholderStyleBuilder() {
final theme = Theme.of(context);
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
return TextStyle(
fontFamily: builtInFontFamily(),
fontSize: fontSize,
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() {
final theme = Theme.of(context);
return FloatingToolbarStyle(
backgroundColor: theme.colorScheme.onTertiary,
);
}
TextStyle baseTextStyle(
String? fontFamily, {
FontWeight? fontWeight,
}) {
if (fontFamily == null) {
return TextStyle(
fontWeight: fontWeight,
FloatingToolbarStyle floatingToolbarStyleBuilder() => FloatingToolbarStyle(
backgroundColor: Theme.of(context).colorScheme.onTertiary,
);
TextStyle baseTextStyle(String? fontFamily, {FontWeight? fontWeight}) {
if (fontFamily == null) {
return TextStyle(fontWeight: fontWeight);
}
try {
return GoogleFonts.getFont(
fontFamily,
fontWeight: fontWeight,
);
return GoogleFonts.getFont(fontFamily, fontWeight: fontWeight);
} on Exception {
if ([builtInFontFamily(), builtInCodeFontFamily].contains(fontFamily)) {
return TextStyle(
fontFamily: fontFamily,
fontWeight: fontWeight,
);
return TextStyle(fontFamily: fontFamily, fontWeight: fontWeight);
}
return TextStyle(
fontWeight: fontWeight,
);
return TextStyle(fontWeight: fontWeight);
}
}
@ -281,7 +253,7 @@ class EditorStyleCustomizer {
),
);
}
} catch (e) {
} catch (_) {
// ignore
}
}
@ -334,18 +306,13 @@ class EditorStyleCustomizer {
..onTap = () {
final editorState = context.read<EditorState>();
if (editorState.selection == null) {
afLaunchUrlString(
href,
addingHttpSchemeWhenFailed: true,
);
afLaunchUrlString(href, addingHttpSchemeWhenFailed: true);
return;
}
editorState.updateSelectionWithReason(
editorState.selection,
extraInfo: {
selectionExtraInfoDisableMobileToolbarKey: true,
},
extraInfo: {selectionExtraInfoDisableMobileToolbarKey: true},
);
showEditLinkBottomSheet(

View File

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

View File

@ -16,18 +16,12 @@ class CommandPalette extends InheritedWidget {
CommandPalette({
super.key,
required Widget? child,
required ValueNotifier<bool> toggleNotifier,
}) : _toggleNotifier = toggleNotifier,
super(
child: _CommandPaletteController(
toggleNotifier: toggleNotifier,
child: child,
),
required this.notifier,
}) : super(
child: _CommandPaletteController(notifier: notifier, child: child),
);
final ValueNotifier<bool> _toggleNotifier;
void toggle() => _toggleNotifier.value = !_toggleNotifier.value;
final ValueNotifier<bool> notifier;
static CommandPalette of(BuildContext context) {
final CommandPalette? result =
@ -38,6 +32,8 @@ class CommandPalette extends InheritedWidget {
return result!;
}
void toggle() => notifier.value = !notifier.value;
@override
bool updateShouldNotify(covariant InheritedWidget oldWidget) => false;
}
@ -48,12 +44,12 @@ class _ToggleCommandPaletteIntent extends Intent {
class _CommandPaletteController extends StatefulWidget {
const _CommandPaletteController({
required this.toggleNotifier,
required this.child,
required this.notifier,
});
final Widget? child;
final ValueNotifier<bool> toggleNotifier;
final ValueNotifier<bool> notifier;
@override
State<_CommandPaletteController> createState() =>
@ -61,26 +57,9 @@ class _CommandPaletteController extends StatefulWidget {
}
class _CommandPaletteControllerState extends State<_CommandPaletteController> {
late ValueNotifier<bool> _toggleNotifier = widget.toggleNotifier;
late final ValueNotifier<bool> _toggleNotifier = widget.notifier;
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
void initState() {
super.initState();

View File

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