mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
@ -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),
|
||||
);
|
||||
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ List<MobileToolbarItem> getMobileToolbarItems() {
|
||||
mobileOutdentToolbarItem,
|
||||
undoMobileToolbarItem,
|
||||
redoMobileToolbarItem,
|
||||
mobileBlockSettingsToolbarItem,
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
},
|
||||
);
|
@ -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';
|
||||
|
Reference in New Issue
Block a user