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
17 changed files with 346 additions and 49 deletions

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';