diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_calendar_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_calendar_test.dart index d6df648bb3..3a565cbee9 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_calendar_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_calendar_test.dart @@ -1,4 +1,5 @@ import 'package:appflowy/plugins/database/calendar/presentation/calendar_event_editor.dart'; +import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; @@ -9,7 +10,14 @@ import '../../shared/database_test_op.dart'; import '../../shared/util.dart'; void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + setUpAll(() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + RecentIcons.enable = false; + }); + + tearDownAll(() { + RecentIcons.enable = true; + }); group('calendar', () { testWidgets('update calendar layout', (tester) async { diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart index 1422aa8aee..6ce248a8a1 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart @@ -1,12 +1,12 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; import 'package:appflowy/plugins/database/widgets/field/type_option_editor/select/select_option.dart'; +import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -14,7 +14,14 @@ import '../../shared/database_test_op.dart'; import '../../shared/util.dart'; void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + setUpAll(() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + RecentIcons.enable = false; + }); + + tearDownAll(() { + RecentIcons.enable = true; + }); group('grid edit field test:', () { testWidgets('rename existing field', (tester) async { diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_view_item_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_view_item_test.dart index 1400ccebe3..f2b721e686 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_view_item_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_view_item_test.dart @@ -1,6 +1,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; +import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -12,7 +13,14 @@ import '../../shared/emoji.dart'; import '../../shared/util.dart'; void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + setUpAll(() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + RecentIcons.enable = false; + }); + + tearDownAll(() { + RecentIcons.enable = true; + }); group('Sidebar view item tests', () { testWidgets('Access view item context menu by right click', (tester) async { diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart index 517a0d68fa..0f218641da 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart @@ -13,6 +13,18 @@ import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; EmojiData? kCachedEmojiData; const _kRecentEmojiCategoryId = 'Recent'; +class EmojiPickerResult { + EmojiPickerResult({ + required this.emojiId, + required this.emoji, + this.isRandom = false, + }); + + final String emojiId; + final String emoji; + final bool isRandom; +} + class FlowyEmojiPicker extends StatefulWidget { const FlowyEmojiPicker({ super.key, @@ -21,7 +33,7 @@ class FlowyEmojiPicker extends StatefulWidget { this.ensureFocus = false, }); - final EmojiSelectedCallback onEmojiSelected; + final ValueChanged onEmojiSelected; final int emojiPerLine; final bool ensureFocus; @@ -70,7 +82,9 @@ class _FlowyEmojiPickerState extends State { defaultSkinTone: lastSelectedEmojiSkinTone ?? EmojiSkinTone.none, ), onEmojiSelected: (id, emoji) { - widget.onEmojiSelected.call(id, emoji); + widget.onEmojiSelected.call( + EmojiPickerResult(emojiId: id, emoji: emoji), + ); RecentIcons.putEmoji(id); }, padding: const EdgeInsets.symmetric(horizontal: 16.0), @@ -106,7 +120,12 @@ class _FlowyEmojiPickerState extends State { onSkinToneChanged: (value) { skinTone.value = value; }, - onRandomEmojiSelected: widget.onEmojiSelected, + onRandomEmojiSelected: (id, emoji) { + widget.onEmojiSelected.call( + EmojiPickerResult(emojiId: id, emoji: emoji, isRandom: true), + ); + RecentIcons.putEmoji(id); + }, ), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_screen.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_screen.dart index 513b0fd224..cf32cad611 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_screen.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_screen.dart @@ -11,14 +11,17 @@ class MobileEmojiPickerScreen extends StatelessWidget { const MobileEmojiPickerScreen({ super.key, this.title, + this.selectedType, this.tabs = const [PickerTabType.emoji, PickerTabType.icon], }); + final PickerTabType? selectedType; final String? title; final List tabs; static const routeName = '/emoji_picker'; static const pageTitle = 'title'; + static const iconSelectedType = 'iconSelectedType'; static const selectTabs = 'tabs'; @override @@ -30,8 +33,9 @@ class MobileEmojiPickerScreen extends StatelessWidget { body: SafeArea( child: FlowyIconEmojiPicker( tabs: tabs, + initialType: selectedType, onSelectedEmoji: (r) { - context.pop(r); + context.pop(r.data); }, ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title.dart b/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title.dart index 04b8a30905..7c7a408b17 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title.dart @@ -244,9 +244,9 @@ class _RenameRowPopoverState extends State { direction: PopoverDirection.bottomWithCenterAligned, offset: const Offset(0, 18), defaultIcon: const FlowySvg(FlowySvgs.document_s), - onSubmitted: (emoji, _) { - widget.onUpdateIcon(emoji); - PopoverContainer.of(context).close(); + onSubmitted: (r, _) { + widget.onUpdateIcon(r.data); + if (!r.keepOpen) PopoverContainer.of(context).close(); }, ), const HSpace(6), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart index e2113cc1fb..894536dbcc 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart @@ -26,8 +26,10 @@ class EmojiPickerButton extends StatelessWidget { final EmojiIconData emoji; final double emojiSize; final Size emojiPickerSize; - final void Function(EmojiIconData emoji, PopoverController? controller) - onSubmitted; + final void Function( + SelectedEmojiIconResult result, + PopoverController? controller, + ) onSubmitted; final PopoverController popoverController = PopoverController(); final Widget? defaultIcon; final Offset? offset; @@ -85,8 +87,10 @@ class _DesktopEmojiPickerButton extends StatelessWidget { final EmojiIconData emoji; final double emojiSize; final Size emojiPickerSize; - final void Function(EmojiIconData emoji, PopoverController? controller) - onSubmitted; + final void Function( + SelectedEmojiIconResult result, + PopoverController? controller, + ) onSubmitted; final PopoverController popoverController = PopoverController(); final Widget? defaultIcon; final Offset? offset; @@ -113,6 +117,7 @@ class _DesktopEmojiPickerButton extends StatelessWidget { height: emojiPickerSize.height, padding: const EdgeInsets.all(4.0), child: FlowyIconEmojiPicker( + initialType: emoji.type.toPickerTabType(), onSelectedEmoji: (r) { onSubmitted(r, popoverController); }, @@ -156,8 +161,10 @@ class _MobileEmojiPickerButton extends StatelessWidget { final EmojiIconData emoji; final double emojiSize; - final void Function(EmojiIconData emoji, PopoverController? controller) - onSubmitted; + final void Function( + SelectedEmojiIconResult result, + PopoverController? controller, + ) onSubmitted; final String? title; final bool enable; final EdgeInsets? margin; @@ -177,11 +184,14 @@ class _MobileEmojiPickerButton extends StatelessWidget { final result = await context.push( Uri( path: MobileEmojiPickerScreen.routeName, - queryParameters: {MobileEmojiPickerScreen.pageTitle: title}, + queryParameters: { + MobileEmojiPickerScreen.pageTitle: title, + MobileEmojiPickerScreen.iconSelectedType: emoji.type.name, + }, ).toString(), ); if (result != null) { - onSubmitted(result, null); + onSubmitted(result.toSelectedResult(), null); } } : null, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart index 05c891bd1e..7485bfe018 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart @@ -227,11 +227,12 @@ class _DocumentImmersiveCoverState extends State { value: pageStyleIconBloc, child: Expanded( child: FlowyIconEmojiPicker( + initialType: icon.type.toPickerTabType(), onSelectedEmoji: (r) { pageStyleIconBloc.add( - PageStyleIconEvent.updateIcon(r, true), + PageStyleIconEvent.updateIcon(r.data, true), ); - Navigator.pop(context); + if (!r.keepOpen) Navigator.pop(context); }, ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart index 54feee3d90..d28020d0fe 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart @@ -468,9 +468,9 @@ class _DocumentHeaderToolbarState extends State { popupBuilder: (BuildContext popoverContext) { isPopoverOpen = true; return FlowyIconEmojiPicker( - onSelectedEmoji: (result) { - widget.onIconOrCoverChanged(icon: result); - _popoverController.close(); + onSelectedEmoji: (r) { + widget.onIconOrCoverChanged(icon: r.data); + if (!r.keepOpen) _popoverController.close(); }, ); }, @@ -838,9 +838,7 @@ class _DocumentIconState extends State { @override Widget build(BuildContext context) { - Widget child = EmojiIconWidget( - emoji: widget.icon, - ); + Widget child = EmojiIconWidget(emoji: widget.icon); if (UniversalPlatform.isDesktopOrWeb) { child = AppFlowyPopover( @@ -852,9 +850,10 @@ class _DocumentIconState extends State { child: child, popupBuilder: (BuildContext popoverContext) { return FlowyIconEmojiPicker( - onSelectedEmoji: (result) { - widget.onChangeIcon(result); - _popoverController.close(); + initialType: widget.icon.type.toPickerTabType(), + onSelectedEmoji: (r) { + widget.onChangeIcon(r.data); + if (!r.keepOpen) _popoverController.close(); }, ); }, @@ -864,7 +863,12 @@ class _DocumentIconState extends State { child: child, onTap: () async { final result = await context.push( - MobileEmojiPickerScreen.routeName, + Uri( + path: MobileEmojiPickerScreen.routeName, + queryParameters: { + MobileEmojiPickerScreen.iconSelectedType: widget.icon.type.name, + }, + ).toString(), ); if (result != null) { widget.onChangeIcon(result); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart index 77b0c158f5..a2f3d2aeeb 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; @@ -67,24 +68,35 @@ class RawEmojiIconWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final defaultEmoji = EmojiText( - emoji: '❓', - fontSize: emojiSize, - textAlign: TextAlign.center, + final defaultEmoji = SizedBox( + width: emojiSize, + child: EmojiText( + emoji: '❓', + fontSize: emojiSize, + textAlign: TextAlign.center, + ), ); try { switch (emoji.type) { case FlowyIconType.emoji: - return EmojiText( - emoji: emoji.emoji, - fontSize: emojiSize, - textAlign: TextAlign.center, + return SizedBox( + width: emojiSize, + child: EmojiText( + emoji: emoji.emoji, + fontSize: emojiSize, + textAlign: TextAlign.center, + ), ); case FlowyIconType.icon: final iconData = IconsData.fromJson(jsonDecode(emoji.emoji)); + + /// Under the same width conditions, icons on macOS seem to appear + /// larger than emojis, so 0.9 is used here to slightly reduce the + /// size of the icons + final iconSize = Platform.isMacOS ? emojiSize * 0.9 : emojiSize; return IconWidget( data: iconData, - size: emojiSize, + size: iconSize, ); default: return defaultEmoji; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart index 6b640be1c4..6647796833 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart @@ -35,7 +35,7 @@ class _PageStyleIconState extends State { builder: (context, state) { final icon = state.icon ?? EmojiIconData.none(); return GestureDetector( - onTap: () => _showIconSelector(context), + onTap: () => _showIconSelector(context, icon), behavior: HitTestBehavior.opaque, child: Container( height: 52, @@ -66,7 +66,7 @@ class _PageStyleIconState extends State { ); } - void _showIconSelector(BuildContext context) { + void _showIconSelector(BuildContext context, EmojiIconData icon) { Navigator.pop(context); final pageStyleIconBloc = PageStyleIconBloc(view: widget.view) ..add(const PageStyleIconEvent.initial()); @@ -85,11 +85,12 @@ class _PageStyleIconState extends State { value: pageStyleIconBloc, child: Expanded( child: FlowyIconEmojiPicker( + initialType: icon.type.toPickerTabType(), onSelectedEmoji: (r) { pageStyleIconBloc.add( - PageStyleIconEvent.updateIcon(r, true), + PageStyleIconEvent.updateIcon(r.data, true), ); - Navigator.pop(ctx); + if (!r.keepOpen) Navigator.pop(ctx); }, ), ), diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/emoji_search_bar.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/emoji_search_bar.dart index 5604da1b33..4520a2b118 100644 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/emoji_search_bar.dart +++ b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/emoji_search_bar.dart @@ -34,6 +34,7 @@ class FlowyEmojiSearchBar extends StatefulWidget { class _FlowyEmojiSearchBarState extends State { final TextEditingController controller = TextEditingController(); + EmojiSkinTone skinTone = lastSelectedEmojiSkinTone ?? EmojiSkinTone.none; @override void dispose() { @@ -58,12 +59,18 @@ class _FlowyEmojiSearchBarState extends State { ), const HSpace(8.0), _RandomEmojiButton( + skinTone: skinTone, emojiData: widget.emojiData, onRandomEmojiSelected: widget.onRandomEmojiSelected, ), const HSpace(8.0), FlowyEmojiSkinToneSelector( - onEmojiSkinToneChanged: widget.onSkinToneChanged, + onEmojiSkinToneChanged: (v) { + setState(() { + skinTone = v; + }); + widget.onSkinToneChanged.call(v); + }, ), ], ), @@ -73,10 +80,12 @@ class _FlowyEmojiSearchBarState extends State { class _RandomEmojiButton extends StatelessWidget { const _RandomEmojiButton({ + required this.skinTone, required this.emojiData, required this.onRandomEmojiSelected, }); + final EmojiSkinTone skinTone; final EmojiData emojiData; final EmojiSelectedCallback onRandomEmojiSelected; @@ -100,9 +109,14 @@ class _RandomEmojiButton extends StatelessWidget { ), onTap: () { final random = emojiData.random; + final emojiId = random.$1; + final emoji = emojiData.getEmojiById( + emojiId, + skinTone: skinTone, + ); onRandomEmojiSelected( - random.$1, - random.$2, + emojiId, + emoji, ); }, ), @@ -131,6 +145,9 @@ class _SearchTextFieldState extends State<_SearchTextField> { @override void initState() { super.initState(); + + /// Sometimes focus is lost due to the [SelectionGestureInterceptor] in [KeyboardServiceWidgetState] + /// this is to ensure that focus can be regained within a short period of time if (widget.ensureFocus) { Future.delayed(const Duration(milliseconds: 200), () { if (!mounted || focusNode.hasFocus) return; diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart index 727d0b8fba..132e79e8e8 100644 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart +++ b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart @@ -6,6 +6,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/icon.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart' hide Icon; +import 'package:flutter/services.dart'; import 'package:universal_platform/universal_platform.dart'; extension ToProto on FlowyIconType { @@ -46,6 +47,10 @@ enum FlowyIconType { custom; } +extension FlowyIconTypeToPickerTabType on FlowyIconType { + PickerTabType? toPickerTabType() => name.toPickerTabType(); +} + class EmojiIconData { factory EmojiIconData.none() => const EmojiIconData(FlowyIconType.icon, ''); @@ -78,17 +83,35 @@ class EmojiIconData { bool get isNotEmpty => emoji.isNotEmpty; } +class SelectedEmojiIconResult { + SelectedEmojiIconResult(this.data, this.keepOpen); + + final EmojiIconData data; + final bool keepOpen; + + FlowyIconType get type => data.type; + + String get emoji => data.emoji; +} + +extension EmojiIconDataToSelectedResultExtension on EmojiIconData { + SelectedEmojiIconResult toSelectedResult({bool keepOpen = false}) => + SelectedEmojiIconResult(this, keepOpen); +} + class FlowyIconEmojiPicker extends StatefulWidget { const FlowyIconEmojiPicker({ super.key, this.onSelectedEmoji, + this.initialType, this.enableBackgroundColorSelection = true, this.tabs = const [PickerTabType.emoji, PickerTabType.icon], }); - final ValueChanged? onSelectedEmoji; + final ValueChanged? onSelectedEmoji; final bool enableBackgroundColorSelection; final List tabs; + final PickerTabType? initialType; @override State createState() => _FlowyIconEmojiPickerState(); @@ -96,12 +119,23 @@ class FlowyIconEmojiPicker extends StatefulWidget { class _FlowyIconEmojiPickerState extends State with SingleTickerProviderStateMixin { - late final controller = TabController( - length: widget.tabs.length, - vsync: this, - ); + late TabController controller; int currentIndex = 0; + @override + void initState() { + super.initState(); + final initialType = widget.initialType; + if (initialType != null) { + currentIndex = widget.tabs.indexOf(initialType); + } + controller = TabController( + initialIndex: currentIndex, + length: widget.tabs.length, + vsync: this, + ); + } + @override void dispose() { controller.dispose(); @@ -127,7 +161,8 @@ class _FlowyIconEmojiPickerState extends State ), _RemoveIconButton( onTap: () { - widget.onSelectedEmoji?.call(EmojiIconData.none()); + widget.onSelectedEmoji + ?.call(EmojiIconData.none().toSelectedResult()); }, ), ], @@ -155,9 +190,12 @@ class _FlowyIconEmojiPickerState extends State return FlowyEmojiPicker( ensureFocus: true, emojiPerLine: _getEmojiPerLine(context), - onEmojiSelected: (_, emoji) => widget.onSelectedEmoji?.call( - EmojiIconData.emoji(emoji), - ), + onEmojiSelected: (r) { + widget.onSelectedEmoji?.call( + EmojiIconData.emoji(r.emoji).toSelectedResult(keepOpen: r.isRandom), + ); + SystemChannels.textInput.invokeMethod('TextInput.hide'); + }, ); } @@ -171,9 +209,13 @@ class _FlowyIconEmojiPickerState extends State Widget _buildIconPicker() { return FlowyIconPicker( + ensureFocus: true, enableBackgroundColorSelection: widget.enableBackgroundColorSelection, - onSelectedIcon: (result) { - widget.onSelectedEmoji?.call(result.toEmojiIconData()); + onSelectedIcon: (r) { + widget.onSelectedEmoji?.call( + r.data.toEmojiIconData().toSelectedResult(keepOpen: r.isRandom), + ); + SystemChannels.textInput.invokeMethod('TextInput.hide'); }, ); } diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_picker.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_picker.dart index 75f5633ee5..4e34badacd 100644 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_picker.dart +++ b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_picker.dart @@ -75,17 +75,31 @@ Future> loadIconGroups() async { } } +class IconPickerResult { + IconPickerResult(this.data, this.isRandom); + + final IconsData data; + final bool isRandom; +} + +extension IconsDataToIconPickerResultExtension on IconsData { + IconPickerResult toResult({bool isRandom = false}) => + IconPickerResult(this, isRandom); +} + class FlowyIconPicker extends StatefulWidget { const FlowyIconPicker({ super.key, required this.onSelectedIcon, required this.enableBackgroundColorSelection, this.iconPerLine = 9, + this.ensureFocus = false, }); final bool enableBackgroundColorSelection; - final ValueChanged onSelectedIcon; + final ValueChanged onSelectedIcon; final int iconPerLine; + final bool ensureFocus; @override State createState() => _FlowyIconPickerState(); @@ -142,6 +156,7 @@ class _FlowyIconPickerState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: IconSearchBar( + ensureFocus: widget.ensureFocus, onRandomTap: () { final value = kIconGroups?.randomIcon(); if (value == null) { @@ -154,8 +169,9 @@ class _FlowyIconPickerState extends State { value.$2.content, value.$2.name, color, - ), + ).toResult(isRandom: true), ); + RecentIcons.putIcon(value.$2); }, onKeywordChanged: (keyword) => { debounce.call(() { @@ -193,14 +209,14 @@ class _FlowyIconPickerState extends State { iconGroups: filteredIconGroups, enableBackgroundColorSelection: widget.enableBackgroundColorSelection, - onSelectedIcon: widget.onSelectedIcon, + onSelectedIcon: (r) => widget.onSelectedIcon.call(r.toResult()), iconPerLine: widget.iconPerLine, ); } return IconPicker( iconGroups: iconGroups, enableBackgroundColorSelection: widget.enableBackgroundColorSelection, - onSelectedIcon: widget.onSelectedIcon, + onSelectedIcon: (r) => widget.onSelectedIcon.call(r.toResult()), iconPerLine: widget.iconPerLine, ); }, @@ -278,6 +294,7 @@ class _IconPickerState extends State { crossAxisCount: widget.iconPerLine, ), itemCount: iconGroup.icons.length, + physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, itemBuilder: (context, index) { final icon = iconGroup.icons[index]; diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_search_bar.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_search_bar.dart index dc079bbc4e..a12be47684 100644 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_search_bar.dart +++ b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_search_bar.dart @@ -16,9 +16,11 @@ class IconSearchBar extends StatefulWidget { super.key, required this.onRandomTap, required this.onKeywordChanged, + this.ensureFocus = false, }); final VoidCallback onRandomTap; + final bool ensureFocus; final IconKeywordChangedCallback onKeywordChanged; @override @@ -46,6 +48,7 @@ class _IconSearchBarState extends State { Expanded( child: _SearchTextField( onKeywordChanged: widget.onKeywordChanged, + ensureFocus: widget.ensureFocus, ), ), const HSpace(8.0), @@ -93,9 +96,11 @@ class _RandomIconButton extends StatelessWidget { class _SearchTextField extends StatefulWidget { const _SearchTextField({ required this.onKeywordChanged, + this.ensureFocus = false, }); final IconKeywordChangedCallback onKeywordChanged; + final bool ensureFocus; @override State<_SearchTextField> createState() => _SearchTextFieldState(); @@ -105,6 +110,20 @@ class _SearchTextFieldState extends State<_SearchTextField> { final TextEditingController controller = TextEditingController(); final FocusNode focusNode = FocusNode(); + @override + void initState() { + super.initState(); + + /// Sometimes focus is lost due to the [SelectionGestureInterceptor] in [KeyboardServiceWidgetState] + /// this is to ensure that focus can be regained within a short period of time + if (widget.ensureFocus) { + Future.delayed(const Duration(milliseconds: 200), () { + if (!mounted || focusNode.hasFocus) return; + focusNode.requestFocus(); + }); + } + } + @override void dispose() { controller.dispose(); diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/tab.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/tab.dart index 56a363132c..b74d1145c6 100644 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/tab.dart +++ b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/tab.dart @@ -15,6 +15,16 @@ enum PickerTabType { } } +extension StringToPickerTabType on String { + PickerTabType? toPickerTabType() { + try { + return PickerTabType.values.byName(this); + } on ArgumentError { + return null; + } + } +} + class PickerTab extends StatelessWidget { const PickerTab({ super.key, diff --git a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart index 27bfdcee5b..bcca2d0a69 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart @@ -285,14 +285,21 @@ GoRoute _mobileEmojiPickerPageRoute() { state.uri.queryParameters[MobileEmojiPickerScreen.pageTitle]; final selectTabs = state.uri.queryParameters[MobileEmojiPickerScreen.selectTabs] ?? ''; + final selectedType = state + .uri.queryParameters[MobileEmojiPickerScreen.iconSelectedType] + ?.toPickerTabType(); final tabs = selectTabs .split('-') .map((e) => PickerTabType.values.byName(e)) .toList(); return MaterialExtendedPage( child: tabs.isEmpty - ? MobileEmojiPickerScreen(title: title) - : MobileEmojiPickerScreen(title: title, tabs: tabs), + ? MobileEmojiPickerScreen(title: title, selectedType: selectedType) + : MobileEmojiPickerScreen( + title: title, + selectedType: selectedType, + tabs: tabs, + ), ); }, ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart index 12dec42026..1f9f4b03b8 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart @@ -96,9 +96,9 @@ class _WorkspaceIconState extends State { margin: const EdgeInsets.all(0), popupBuilder: (_) => FlowyIconEmojiPicker( tabs: const [PickerTabType.emoji], - onSelectedEmoji: (result) { - widget.onSelected(result); - controller.close(); + onSelectedEmoji: (r) { + widget.onSelected(r.data); + if (!r.keepOpen) controller.close(); }, ), child: MouseRegion( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart index d6b5dfe297..3f39b976d2 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart @@ -641,12 +641,13 @@ class _SingleInnerViewItemState extends State { popupBuilder: (context) { isIconPickerOpened = true; return FlowyIconEmojiPicker( - onSelectedEmoji: (result) { + initialType: iconData.type.toPickerTabType(), + onSelectedEmoji: (r) { ViewBackendService.updateViewIcon( viewId: widget.view.id, - viewIcon: result, + viewIcon: r.data, ); - controller.close(); + if (!r.keepOpen) controller.close(); }, ); }, @@ -770,13 +771,12 @@ class _SingleInnerViewItemState extends State { context.read().add(const ViewEvent.collapseAllPages()); break; case ViewMoreActionType.changeIcon: - if (data is! EmojiIconData) { + if (data is! SelectedEmojiIconResult) { return; } - final result = data; await ViewBackendService.updateViewIcon( viewId: widget.view.id, - viewIcon: result, + viewIcon: data.data, ); break; case ViewMoreActionType.moveTo: diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart index ba9a946cc2..186c593572 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart @@ -58,7 +58,11 @@ class ViewMoreActionPopover extends StatelessWidget { (e) => ViewMoreActionTypeWrapper(e, view, (controller, data) { onEditing(false); onAction(e, data); - controller.close(); + bool enableClose = true; + if (data is SelectedEmojiIconResult) { + if (data.keepOpen) enableClose = false; + } + if (enableClose) controller.close(); }), ) .toList(); @@ -172,6 +176,7 @@ class ViewMoreActionTypeWrapper extends CustomActionCell { margin: const EdgeInsets.all(0), clickHandler: PopoverClickHandler.gestureDetector, popupBuilder: (_) => FlowyIconEmojiPicker( + initialType: sourceView.icon.toEmojiIconData().type.toPickerTabType(), onSelectedEmoji: (result) => onTap(controller, result), ), child: child, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/publish_info_view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/publish_info_view_item.dart index f2a3980bf6..3ba2c7e75e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/publish_info_view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/publish_info_view_item.dart @@ -1,4 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; +import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy/util/string_extension.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; @@ -51,13 +53,9 @@ class PublishInfoViewItem extends StatelessWidget { } Widget _buildIcon() { - final icon = publishInfoView.view.icon.value; + final icon = publishInfoView.view.icon.toEmojiIconData(); return icon.isNotEmpty - ? FlowyText.emoji( - icon, - fontSize: 16.0, - figmaLineHeight: 18.0, - ) + ? RawEmojiIconWidget(emoji: icon, emojiSize: 16.0) : publishInfoView.view.defaultIcon(); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart index 6cdccb3b3b..ab952386bd 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart @@ -1,12 +1,11 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/decoration.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; SelectionMenuItem emojiMenuItem = SelectionMenuItem( getName: LocaleKeys.document_plugins_emoji.tr, @@ -109,7 +108,7 @@ class _EmojiSelectionMenuState extends State { @override Widget build(BuildContext context) { return FlowyEmojiPicker( - onEmojiSelected: (_, emoji) => widget.onSubmitted(emoji), + onEmojiSelected: (r) => widget.onSubmitted(r.emoji), ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart index ccea2f895c..cae65cafca 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart @@ -91,13 +91,15 @@ class _RenameViewPopoverState extends State { } Future _updateViewIcon( - EmojiIconData emoji, + SelectedEmojiIconResult r, PopoverController? _, ) async { await ViewBackendService.updateViewIcon( viewId: widget.viewId, - viewIcon: emoji, + viewIcon: r.data, ); - widget.popoverController.close(); + if (!r.keepOpen) { + widget.popoverController.close(); + } } } diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index c0df44a207..b108a0c23a 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -757,8 +757,8 @@ packages: dependency: "direct main" description: path: "." - ref: "8a9fa49" - resolved-ref: "8a9fa491cb3b86baf78b0a33c2c37a29d1cae028" + ref: "355aa56" + resolved-ref: "355aa56e9c74a91e00370a882739e0bb98c30bd8" url: "https://github.com/LucasXu0/emoji_mart.git" source: git version: "1.0.2" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 60ab502b2b..9433511050 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -73,7 +73,7 @@ dependencies: flutter_emoji_mart: git: url: https://github.com/LucasXu0/emoji_mart.git - ref: "8a9fa49" + ref: "355aa56" flutter_math_fork: ^0.7.3 flutter_slidable: ^3.0.0