feat: add ... button in mobile toolbar (#3970)

* feat: add ... button in mobile toolbar

* fix: the title state should be reset when canceling

* fix: reset hover status after picking emoji

* fix: some emojis missing on linux and android

* fix: unable to press enter key to rename page
This commit is contained in:
Lucas.Xu 2023-11-22 10:49:22 +08:00 committed by GitHub
parent a7364e1f4a
commit 412f34c72a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 346 additions and 49 deletions

View File

@ -2,6 +2,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/plugins/document/document_page.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
@ -120,8 +121,8 @@ class _MobileViewPageState extends State<MobileViewPage> {
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null)
FlowyText(
'$icon ',
EmojiText(
emoji: '$icon ',
fontSize: 22.0,
),
Expanded(

View File

@ -1,6 +1,7 @@
import 'dart:io';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/workspace/application/doc/doc_listener.dart';
@ -108,8 +109,8 @@ class _MobileRecentViewState extends State<MobileRecentView> {
child: Padding(
padding: const EdgeInsets.only(left: 4),
child: icon.isNotEmpty
? FlowyText(
icon,
? EmojiText(
emoji: icon,
fontSize: 30.0,
)
: SizedBox.square(

View File

@ -3,6 +3,7 @@ import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_add_new_page.dart';
import 'package:appflowy/mobile/presentation/page_item/mobile_view_item_add_button.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
@ -355,8 +356,8 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
Widget _buildViewIconButton() {
final icon = widget.view.icon.value.isNotEmpty
? FlowyText(
widget.view.icon.value,
? EmojiText(
emoji: widget.view.icon.value,
fontSize: 24.0,
)
: SizedBox.square(

View File

@ -1,9 +1,12 @@
import 'dart:io';
import 'package:appflowy/plugins/base/emoji/emoji_picker_header.dart';
import 'package:appflowy/plugins/base/emoji/emoji_search_bar.dart';
import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
import 'package:google_fonts/google_fonts.dart';
// use a global value to store the selected emoji to prevent reloading every time.
EmojiData? _cachedEmojiData;
@ -24,6 +27,7 @@ class FlowyEmojiPicker extends StatefulWidget {
class _FlowyEmojiPickerState extends State<FlowyEmojiPicker> {
EmojiData? emojiData;
List<String>? fallbackFontFamily;
@override
void initState() {
@ -42,6 +46,13 @@ class _FlowyEmojiPickerState extends State<FlowyEmojiPicker> {
},
);
}
if (Platform.isAndroid || Platform.isLinux) {
final notoColorEmoji = GoogleFonts.notoColorEmoji().fontFamily;
if (notoColorEmoji != null) {
fallbackFontFamily = [notoColorEmoji];
}
}
}
@override
@ -77,6 +88,7 @@ class _FlowyEmojiPickerState extends State<FlowyEmojiPicker> {
icon: FlowyText(
emoji,
fontSize: 28.0,
fallbackFontFamily: fallbackFontFamily,
),
onPressed: () => callback(emojiId, emoji),
);

View File

@ -0,0 +1,42 @@
import 'dart:io';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
// used to prevent loading font from google fonts every time
List<String>? _cachedFallbackFontFamily;
// Some emojis are not supported by the default font on Android or Linux, fallback to noto color emoji
class EmojiText extends StatelessWidget {
const EmojiText({
super.key,
required this.emoji,
required this.fontSize,
this.textAlign,
});
final String emoji;
final double fontSize;
final TextAlign? textAlign;
@override
Widget build(BuildContext context) {
_loadFallbackFontFamily();
return FlowyText(
emoji,
fontSize: fontSize,
textAlign: textAlign,
fallbackFontFamily: _cachedFallbackFontFamily,
);
}
void _loadFallbackFontFamily() {
if (Platform.isLinux || Platform.isAndroid) {
final notoColorEmoji = GoogleFonts.notoColorEmoji().fontFamily;
if (notoColorEmoji != null) {
_cachedFallbackFontFamily = [notoColorEmoji];
}
}
}
}

View File

@ -1,6 +1,7 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker.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:flowy_infra_ui/style_widget/hover.dart';
@ -119,6 +120,9 @@ class _FlowyIconPickerState extends State<FlowyIconPicker>
}
int _getEmojiPerLine() {
if (PlatformExtension.isDesktopOrWeb) {
return 9;
}
final width = MediaQuery.of(context).size.width;
return width ~/ 46.0; // the size of the emoji
}

View File

@ -22,6 +22,7 @@ List<MobileToolbarItem> getMobileToolbarItems() {
mobileOutdentToolbarItem,
undoMobileToolbarItem,
redoMobileToolbarItem,
mobileBlockSettingsToolbarItem,
];
}

View File

@ -1,4 +1,4 @@
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:flutter/material.dart';
class EmojiIconWidget extends StatefulWidget {
@ -32,8 +32,8 @@ class _EmojiIconWidgetState extends State<EmojiIconWidget> {
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
child: FlowyText(
widget.emoji,
child: EmojiText(
emoji: widget.emoji,
fontSize: widget.emojiSize,
textAlign: TextAlign.center,
),

View File

@ -0,0 +1,106 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
enum MobileBlockActionType {
delete,
duplicate,
insertAbove,
insertBelow,
color;
static List<MobileBlockActionType> get standard => [
MobileBlockActionType.delete,
MobileBlockActionType.duplicate,
MobileBlockActionType.insertAbove,
MobileBlockActionType.insertBelow,
];
static MobileBlockActionType fromActionString(String actionString) {
return MobileBlockActionType.values.firstWhere(
(e) => e.actionString == actionString,
orElse: () => throw Exception('Unknown action string: $actionString'),
);
}
String get actionString => toString();
FlowySvgData get icon {
return switch (this) {
MobileBlockActionType.delete => FlowySvgs.m_delete_m,
MobileBlockActionType.duplicate => FlowySvgs.m_duplicate_m,
MobileBlockActionType.insertAbove => FlowySvgs.arrow_up_s,
MobileBlockActionType.insertBelow => FlowySvgs.arrow_down_s,
MobileBlockActionType.color => FlowySvgs.m_color_m,
};
}
String get i18n {
return switch (this) {
MobileBlockActionType.delete => LocaleKeys.button_delete.tr(),
MobileBlockActionType.duplicate => LocaleKeys.button_duplicate.tr(),
MobileBlockActionType.insertAbove => LocaleKeys.button_insertAbove.tr(),
MobileBlockActionType.insertBelow => LocaleKeys.button_insertBelow.tr(),
MobileBlockActionType.color =>
LocaleKeys.document_plugins_optionAction_color.tr(),
};
}
}
class MobileBlockSettingsScreen extends StatelessWidget {
static const routeName = '/block_settings';
// the action string comes from the enum MobileBlockActionType
// example: MobileBlockActionType.delete.actionString, MobileBlockActionType.duplicate.actionString, etc.
static const supportedActions = 'actions';
const MobileBlockSettingsScreen({
super.key,
required this.actions,
});
final List<MobileBlockActionType> actions;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
titleSpacing: 0,
title: FlowyText.semibold(
LocaleKeys.titleBar_actions.tr(),
fontSize: 14.0,
),
leading: AppBarBackButton(
onTap: () => context.pop(),
),
),
body: SafeArea(
child: ListView.separated(
itemCount: actions.length,
itemBuilder: (context, index) {
final action = actions[index];
return FlowyButton(
text: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 18.0,
),
child: FlowyText(action.i18n),
),
leftIcon: FlowySvg(action.icon),
leftIconSize: const Size.square(24),
onTap: () {},
);
},
separatorBuilder: (context, index) => const Divider(
height: 1.0,
),
),
),
);
}
}

View File

@ -0,0 +1,78 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:go_router/go_router.dart';
final mobileBlockSettingsToolbarItem = MobileToolbarItem.action(
itemIconBuilder: (_, editorState, __) {
return onlyShowInSingleSelectionAndTextType(editorState)
? const FlowySvg(FlowySvgs.three_dots_s)
: null;
},
actionHandler: (_, editorState) async {
// show the settings page
final selection = editorState.selection;
if (selection == null || !selection.isCollapsed) {
return;
}
final node = editorState.getNodeAtPath(selection.start.path);
final context = node?.context;
if (node == null || context == null) {
return;
}
final result = await showFlowyMobileBottomSheet<bool>(
context,
title: LocaleKeys.document_plugins_action.tr(),
builder: (context) {
return BlockActionBottomSheet(
onAction: (action) async {
context.pop(true);
final transaction = editorState.transaction;
switch (action) {
case BlockActionBottomSheetType.delete:
transaction.deleteNode(node);
break;
case BlockActionBottomSheetType.duplicate:
transaction.insertNode(
node.path.next,
node.copyWith(),
);
break;
case BlockActionBottomSheetType.insertAbove:
case BlockActionBottomSheetType.insertBelow:
final path = action == BlockActionBottomSheetType.insertAbove
? node.path
: node.path.next;
transaction
..insertNode(
path,
paragraphNode(),
)
..afterSelection = Selection.collapsed(
Position(
path: path,
),
);
break;
default:
}
if (transaction.operations.isNotEmpty) {
await editorState.apply(transaction);
}
},
);
},
);
if (result != true) {
// restore the selection
editorState.selection = selection;
}
},
);

View File

@ -28,8 +28,9 @@ export 'math_equation/math_equation_block_component.dart';
export 'math_equation/mobile_math_equation_toolbar_item.dart';
export 'mobile_toolbar_item/mobile_add_block_toolbar_item.dart';
export 'mobile_toolbar_item/mobile_align_toolbar_item.dart';
export 'mobile_toolbar_item/mobile_block_settings_toolbar_item.dart';
export 'mobile_toolbar_item/mobile_convert_block_toolbar_item.dart';
export 'mobile_toolbar_item/mobile_indent_toolbar_items.dart';
export 'mobile_toolbar_item/mobile_indent_toolbar_item.dart';
export 'mobile_toolbar_item/mobile_text_decoration_item.dart';
export 'openai/widgets/auto_completion_node_widget.dart';
export 'openai/widgets/smart_edit_node_widget.dart';

View File

@ -13,6 +13,7 @@ import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_language_screen.dart';
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_picker_screen.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_block_settings_screen.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/startup/tasks/app_widget.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
@ -72,6 +73,7 @@ GoRouter generateRouter(Widget child) {
_mobileCodeLanguagePickerPageRoute(),
_mobileLanguagePickerPageRoute(),
_mobileFontPickerPageRoute(),
_mobileBlockSettingsPageRoute(),
],
// Desktop and Mobile
@ -225,6 +227,26 @@ GoRoute _mobileHomeTrashPageRoute() {
);
}
GoRoute _mobileBlockSettingsPageRoute() {
return GoRoute(
parentNavigatorKey: AppGlobals.rootNavKey,
path: MobileBlockSettingsScreen.routeName,
pageBuilder: (context, state) {
final actionsString =
state.uri.queryParameters[MobileBlockSettingsScreen.supportedActions];
final actions = actionsString
?.split(',')
.map(MobileBlockActionType.fromActionString)
.toList();
return MaterialPage(
child: MobileBlockSettingsScreen(
actions: actions ?? MobileBlockActionType.standard,
),
);
},
);
}
GoRoute _mobileEmojiPickerPageRoute() {
return GoRoute(
parentNavigatorKey: AppGlobals.rootNavKey,

View File

@ -1,5 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
@ -301,7 +302,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
style: HoverStyle(
hoverColor: Theme.of(context).colorScheme.secondary,
),
resetHoverOnRebuild: widget.showActions,
resetHoverOnRebuild: widget.showActions || !isIconPickerOpened,
buildWhenOnHover: () =>
!widget.showActions && !_isDragging && !isIconPickerOpened,
builder: (_, onHover) => _buildViewItem(onHover),
@ -356,8 +357,8 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
Widget _buildViewIconButton() {
final icon = widget.view.icon.value.isNotEmpty
? FlowyText(
widget.view.icon.value,
? EmojiText(
emoji: widget.view.icon.value,
fontSize: 18.0,
)
: SizedBox.square(
@ -419,6 +420,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
// + button
Widget _buildViewAddButton(BuildContext context) {
final viewBloc = context.read<ViewBloc>();
return FlowyTooltip(
message: LocaleKeys.menuAppHeader_addPageTooltip.tr(),
child: ViewAddButton(
@ -438,20 +440,20 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
_convertLayoutToHintText(pluginBuilder.layoutType!),
(viewName) {
if (viewName.isNotEmpty) {
context.read<ViewBloc>().add(
ViewEvent.createView(
viewName,
pluginBuilder.layoutType!,
openAfterCreated: openAfterCreated,
),
);
viewBloc.add(
ViewEvent.createView(
viewName,
pluginBuilder.layoutType!,
openAfterCreated: openAfterCreated,
),
);
}
},
);
}
context.read<ViewBloc>().add(
const ViewEvent.setIsExpanded(true),
);
viewBloc.add(
const ViewEvent.setIsExpanded(true),
);
},
),
);

View File

@ -1,3 +1,4 @@
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
import 'package:appflowy/startup/tasks/app_window_size_manager.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
@ -95,12 +96,15 @@ class _ViewTitleBarState extends State<ViewTitleBar> {
continue;
}
children.add(
_ViewTitle(
view: view,
behavior: i == views.length - 1
? _ViewTitleBehavior.editable // only the last one is editable
: _ViewTitleBehavior.uneditable, // others are not editable
onUpdated: () => setState(() => _reloadAncestors()),
FlowyTooltip(
message: view.name,
child: _ViewTitle(
view: view,
behavior: i == views.length - 1
? _ViewTitleBehavior.editable // only the last one is editable
: _ViewTitleBehavior.uneditable, // others are not editable
onUpdated: () => setState(() => _reloadAncestors()),
),
),
);
if (i != views.length - 1) {
@ -190,26 +194,23 @@ class _ViewTitleState extends State<_ViewTitle> {
);
}
final child = FlowyTooltip(
message: name,
child: Row(
children: [
FlowyText.regular(
icon,
fontSize: 18.0,
final child = Row(
children: [
EmojiText(
emoji: icon,
fontSize: 18.0,
),
const HSpace(2.0),
ConstrainedBox(
constraints: BoxConstraints(
maxWidth: widget.maxTitleWidth,
),
const HSpace(2.0),
ConstrainedBox(
constraints: BoxConstraints(
maxWidth: widget.maxTitleWidth,
),
child: FlowyText.regular(
name,
overflow: TextOverflow.ellipsis,
),
child: FlowyText.regular(
name,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
);
if (widget.behavior == _ViewTitleBehavior.uneditable) {
@ -232,6 +233,7 @@ class _ViewTitleState extends State<_ViewTitle> {
offset: const Offset(0, 18),
popupBuilder: (context) {
// icon + textfield
_resetTextEditingController();
return Row(
mainAxisSize: MainAxisSize.min,
children: [
@ -261,8 +263,8 @@ class _ViewTitleState extends State<_ViewTitle> {
viewId: widget.view.id,
name: text,
);
popoverController.close();
}
popoverController.close();
},
),
),

View File

@ -11,6 +11,7 @@ class FlowyText extends StatelessWidget {
final TextDecoration? decoration;
final bool selectable;
final String? fontFamily;
final List<String>? fallbackFontFamily;
const FlowyText(
this.text, {
@ -23,6 +24,7 @@ class FlowyText extends StatelessWidget {
this.decoration,
this.selectable = false,
this.fontFamily,
this.fallbackFontFamily,
Key? key,
}) : super(key: key);
@ -36,6 +38,7 @@ class FlowyText extends StatelessWidget {
this.decoration,
this.selectable = false,
this.fontFamily,
this.fallbackFontFamily,
Key? key,
}) : fontWeight = FontWeight.w400,
super(key: key);
@ -50,6 +53,7 @@ class FlowyText extends StatelessWidget {
this.decoration,
this.selectable = false,
this.fontFamily,
this.fallbackFontFamily,
Key? key,
}) : fontWeight = FontWeight.w500,
super(key: key);
@ -64,10 +68,27 @@ class FlowyText extends StatelessWidget {
this.decoration,
this.selectable = false,
this.fontFamily,
this.fallbackFontFamily,
Key? key,
}) : fontWeight = FontWeight.w600,
super(key: key);
// Some emojis are not supported on Linux and Android, fallback to noto color emoji
const FlowyText.emoji(
this.text, {
this.fontSize,
this.overflow,
this.color,
this.textAlign,
this.maxLines = 1,
this.decoration,
this.selectable = false,
Key? key,
}) : fontWeight = FontWeight.w400,
fontFamily = 'noto color emoji',
fallbackFontFamily = null,
super(key: key);
@override
Widget build(BuildContext context) {
if (selectable) {
@ -81,6 +102,7 @@ class FlowyText extends StatelessWidget {
color: color,
decoration: decoration,
fontFamily: fontFamily,
fontFamilyFallback: fallbackFontFamily,
),
);
} else {
@ -95,6 +117,7 @@ class FlowyText extends StatelessWidget {
color: color,
decoration: decoration,
fontFamily: fontFamily,
fontFamilyFallback: fallbackFontFamily,
),
);
}

View File

@ -1091,6 +1091,7 @@
"titleBar": {
"pageIcon": "Page icon",
"language": "Language",
"font": "Font"
"font": "Font",
"actions": "Actions"
}
}