diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c8009aeb86..eea2f7edfe 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,7 +59,6 @@ jobs: with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} - cache: true - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -70,11 +69,6 @@ jobs: components: rustfmt profile: minimal - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: appflowy-lib-cache - key: ${{ matrix.job.os }}-${{ matrix.job.target }} - - name: Install prerequisites working-directory: frontend run: | @@ -151,7 +145,6 @@ jobs: with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} - cache: true - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -162,11 +155,6 @@ jobs: components: rustfmt profile: minimal - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: appflowy-lib-cache - key: ${{ matrix.job.os }}-${{ matrix.job.target }} - - name: Install prerequisites working-directory: frontend run: | @@ -257,7 +245,6 @@ jobs: with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} - cache: true - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable @@ -266,11 +253,6 @@ jobs: targets: ${{ matrix.job.targets }} components: rustfmt - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: appflowy-lib-cache - key: ${{ matrix.job.os }}-${{ matrix.job.target }} - - name: Install prerequisites working-directory: frontend run: | @@ -366,7 +348,6 @@ jobs: with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} - cache: true - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -377,11 +358,6 @@ jobs: components: rustfmt profile: minimal - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: appflowy-lib-cache - key: ${{ matrix.job.os }}-${{ matrix.job.target }} - - name: Install prerequisites working-directory: frontend run: | diff --git a/frontend/appflowy_flutter/integration_test/database_row_page_test.dart b/frontend/appflowy_flutter/integration_test/database_row_page_test.dart index 62d5c81be3..9a771eb876 100644 --- a/frontend/appflowy_flutter/integration_test/database_row_page_test.dart +++ b/frontend/appflowy_flutter/integration_test/database_row_page_test.dart @@ -42,7 +42,6 @@ void main() { await tester.hoverRowBanner(); await tester.openEmojiPicker(); - await tester.switchToEmojiList(); await tester.tapEmoji('๐Ÿ˜€'); // After select the emoji, the EmojiButton will show up @@ -60,12 +59,10 @@ void main() { await tester.openFirstRowDetailPage(); await tester.hoverRowBanner(); await tester.openEmojiPicker(); - await tester.switchToEmojiList(); await tester.tapEmoji('๐Ÿ˜€'); // Update existing selected emoji await tester.tapButton(find.byType(EmojiButton)); - await tester.switchToEmojiList(); await tester.tapEmoji('๐Ÿ˜…'); // The emoji already displayed in the row banner @@ -89,7 +86,6 @@ void main() { await tester.openFirstRowDetailPage(); await tester.hoverRowBanner(); await tester.openEmojiPicker(); - await tester.switchToEmojiList(); await tester.tapEmoji('๐Ÿ˜€'); // Remove the emoji diff --git a/frontend/appflowy_flutter/integration_test/document/document_with_cover_image_test.dart b/frontend/appflowy_flutter/integration_test/document/document_with_cover_image_test.dart index 5bcf4b7b43..360202e016 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_with_cover_image_test.dart +++ b/frontend/appflowy_flutter/integration_test/document/document_with_cover_image_test.dart @@ -1,4 +1,8 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:emoji_mart/emoji_mart.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -61,7 +65,6 @@ void main() { // Insert a document icon await tester.editor.tapAddIconButton(); - await tester.switchToEmojiList(); await tester.tapEmoji('๐Ÿ˜€'); tester.expectToSeeDocumentIcon('๐Ÿ˜€'); @@ -73,13 +76,11 @@ void main() { // Add the icon back for further testing await tester.editor.hoverOnCoverToolbar(); await tester.editor.tapAddIconButton(); - await tester.switchToEmojiList(); await tester.tapEmoji('๐Ÿ˜€'); tester.expectToSeeDocumentIcon('๐Ÿ˜€'); // Change the document icon await tester.editor.tapOnIconWidget(); - await tester.switchToEmojiList(); await tester.tapEmoji('๐Ÿ˜…'); tester.expectToSeeDocumentIcon('๐Ÿ˜…'); @@ -102,7 +103,6 @@ void main() { // Insert a document icon await tester.editor.tapAddIconButton(); - await tester.switchToEmojiList(); await tester.tapEmoji('๐Ÿ˜€'); // Insert a document cover @@ -116,5 +116,46 @@ void main() { await tester.editor.hoverOnCoverToolbar(); tester.expectToSeeEmptyDocumentHeaderToolbar(); }); + + testWidgets('shuffle icon', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.editor.hoverOnCoverToolbar(); + await tester.editor.tapAddIconButton(); + + // click the shuffle button + await tester.tapButton( + find.byTooltip(LocaleKeys.emoji_random.tr()), + ); + tester.expectDocumentIconNotNull(); + }); + + testWidgets('change skin tone', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.editor.hoverOnCoverToolbar(); + await tester.editor.tapAddIconButton(); + + final searchEmojiTextField = find.byWidgetPredicate( + (widget) => + widget is TextField && + widget.decoration!.hintText == LocaleKeys.emoji_search.tr(), + ); + await tester.enterText( + searchEmojiTextField, + 'hand', + ); + + // change skin tone + await tester.editor.changeEmojiSkinTone(EmojiSkinTone.dark); + + // select an icon with skin tone + const hand = '๐Ÿ‘‹๐Ÿฟ'; + await tester.tapEmoji(hand); + tester.expectToSeeDocumentIcon(hand); + tester.isPageWithIcon(gettingStarted, hand); + }); }); } diff --git a/frontend/appflowy_flutter/integration_test/util/base.dart b/frontend/appflowy_flutter/integration_test/util/base.dart index bfe6a8d833..b28188629b 100644 --- a/frontend/appflowy_flutter/integration_test/util/base.dart +++ b/frontend/appflowy_flutter/integration_test/util/base.dart @@ -1,9 +1,11 @@ import 'dart:async'; import 'dart:io'; +import 'package:appflowy/env/env.dart'; import 'package:appflowy/startup/entry_point.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/presentation/presentation.dart'; +import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; import 'package:flowy_infra/uuid.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -81,9 +83,15 @@ extension AppFlowyTestBase on WidgetTester { } Future waitUntilSignInPageShow() async { - final finder = find.byType(GoButton); - await pumpUntilFound(finder); - expect(finder, findsOneWidget); + if (isCloudEnabled) { + final finder = find.byType(SignInAnonymousButton); + await pumpUntilFound(finder); + expect(finder, findsOneWidget); + } else { + final finder = find.byType(GoButton); + await pumpUntilFound(finder); + expect(finder, findsOneWidget); + } } Future pumpUntilFound( diff --git a/frontend/appflowy_flutter/integration_test/util/common_operations.dart b/frontend/appflowy_flutter/integration_test/util/common_operations.dart index 15a1d74f3a..7094f4a333 100644 --- a/frontend/appflowy_flutter/integration_test/util/common_operations.dart +++ b/frontend/appflowy_flutter/integration_test/util/common_operations.dart @@ -7,6 +7,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/share/share_button.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/presentation/screens/screens.dart'; +import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart'; @@ -27,8 +28,15 @@ import 'util.dart'; extension CommonOperations on WidgetTester { /// Tap the GetStart button on the launch page. Future tapGoButton() async { + // local version final goButton = find.byType(GoButton); - await tapButton(goButton); + if (goButton.evaluate().isNotEmpty) { + await tapButton(goButton); + } else { + // cloud version + final anonymousButton = find.byType(SignInAnonymousButton); + await tapButton(anonymousButton); + } if (Platform.isWindows) { await pumpAndSettle(const Duration(milliseconds: 200)); diff --git a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart index 89b94e6a2c..b91511e5d3 100644 --- a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart @@ -38,7 +38,6 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_header.dart'; import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart'; import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart'; -import 'package:appflowy/plugins/database_view/widgets/setting/setting_property_list.dart'; import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart'; @@ -53,7 +52,7 @@ import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_property.dart'; import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart'; +import 'package:appflowy/plugins/database_view/widgets/setting/setting_property_list.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; @@ -618,7 +617,6 @@ extension AppFlowyDatabaseTest on WidgetTester { Future openEmojiPicker() async { await tapButton(find.byType(EmojiPickerButton)); - await tapButton(find.byType(EmojiSelectionMenu)); } Future tapDateCellInRowDetailPage() async { diff --git a/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart b/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart index 5b3e8c0777..51e99ceddf 100644 --- a/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart +++ b/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart @@ -1,15 +1,18 @@ import 'dart:ui'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart'; +import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/custom_cover_picker.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_popover.dart'; import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:easy_localization/easy_localization.dart'; +import 'package:emoji_mart/emoji_mart.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -54,7 +57,18 @@ class EditorOperations { await tester.tapButtonWithName( LocaleKeys.document_plugins_cover_addIcon.tr(), ); - expect(find.byType(EmojiPopover), findsOneWidget); + expect(find.byType(FlowyEmojiPicker), findsOneWidget); + } + + /// Taps on the 'Skin tone' button + /// + /// Must call [tapAddIconButton] first. + Future changeEmojiSkinTone(EmojiSkinTone skinTone) async { + await tester.tapButton( + find.byTooltip(LocaleKeys.emoji_selectSkinTone.tr()), + ); + final skinToneButton = find.text(EmojiSkinToneWrapper(skinTone).name); + await tester.tapButton(skinToneButton); } /// Taps the 'Remove Icon' button in the cover toolbar and the icon popover @@ -62,7 +76,10 @@ class EditorOperations { Finder button = find.text(LocaleKeys.document_plugins_cover_removeIcon.tr()); if (isInPicker) { - button = find.descendant(of: find.byType(EmojiPopover), matching: button); + button = find.descendant( + of: find.byType(FlowyIconPicker), + matching: button, + ); } await tester.tapButton(button); diff --git a/frontend/appflowy_flutter/integration_test/util/emoji.dart b/frontend/appflowy_flutter/integration_test/util/emoji.dart index 616f3da6ec..a3bfbc02f6 100644 --- a/frontend/appflowy_flutter/integration_test/util/emoji.dart +++ b/frontend/appflowy_flutter/integration_test/util/emoji.dart @@ -1,15 +1,8 @@ -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'base.dart'; extension EmojiTestExtension on WidgetTester { - /// Must call [openEmojiPicker] first - Future switchToEmojiList() async { - final icon = find.byIcon(Icons.tag_faces); - await tapButton(icon); - } - Future tapEmoji(String emoji) async { final emojiWidget = find.text(emoji); await tapButton(emojiWidget); diff --git a/frontend/appflowy_flutter/integration_test/util/expectation.dart b/frontend/appflowy_flutter/integration_test/util/expectation.dart index 56bd2c6804..ec2f63d30c 100644 --- a/frontend/appflowy_flutter/integration_test/util/expectation.dart +++ b/frontend/appflowy_flutter/integration_test/util/expectation.dart @@ -108,6 +108,13 @@ extension Expectation on WidgetTester { expect(iconWidget, findsOneWidget); } + void expectDocumentIconNotNull() { + final iconWidget = find.byWidgetPredicate( + (widget) => widget is EmojiIconWidget && widget.emoji.isNotEmpty, + ); + expect(iconWidget, findsOneWidget); + } + void expectToSeeDocumentCover(CoverType type) { final findCover = find.byWidgetPredicate( (widget) => widget is DocumentCover && widget.coverType == type, @@ -193,4 +200,13 @@ extension Expectation on WidgetTester { matching: findPageName(name, layout: layout), ); } + + void isPageWithIcon(String name, String emoji) { + final pageName = findPageName(name); + final icon = find.descendant( + of: pageName, + matching: find.text(emoji), + ); + expect(icon, findsOneWidget); + } } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart index e63008f25f..ec85278ea3 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart @@ -106,12 +106,22 @@ class _MobileViewPageState extends State { } Widget _buildApp(ViewPB? view, List actions, Widget child) { + final icon = view?.icon.value; return Scaffold( appBar: AppBar( titleSpacing: 0, - title: FlowyText.semibold( - view?.name ?? widget.title ?? '', - fontSize: 14.0, + title: Row( + children: [ + if (icon != null) + FlowyText( + '$icon ', + fontSize: 22.0, + ), + FlowyText.regular( + view?.name ?? widget.title ?? '', + fontSize: 14.0, + ), + ], ), leading: AppBarBackButton( onTap: () => context.pop(), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart index d24806f246..18cc9a1bb9 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart @@ -303,11 +303,8 @@ class _SingleMobileInnerViewItemState extends State { _buildLeftIcon(), const HSpace(4), // icon - SizedBox.square( - dimension: 22, - child: widget.view.defaultIcon(), - ), - const HSpace(12), + _buildViewIconButton(), + const HSpace(8), // title Expanded( child: FlowyText.regular( @@ -356,6 +353,19 @@ class _SingleMobileInnerViewItemState extends State { return child; } + Widget _buildViewIconButton() { + final icon = widget.view.icon.value.isNotEmpty + ? FlowyText( + widget.view.icon.value, + fontSize: 24.0, + ) + : SizedBox.square( + dimension: 26.0, + child: widget.view.defaultIcon(), + ); + return icon; + } + // > button or ยท button // show > if the view is expandable. // show ยท if the view can't contain child views. diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart new file mode 100644 index 0000000000..fb9a55652f --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart @@ -0,0 +1,95 @@ +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:emoji_mart/emoji_mart.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +// use a global value to store the selected emoji to prevent reloading every time. +EmojiData? _cachedEmojiData; + +class FlowyEmojiPicker extends StatefulWidget { + const FlowyEmojiPicker({ + super.key, + required this.onEmojiSelected, + }); + + final EmojiSelectedCallback onEmojiSelected; + + @override + State createState() => _FlowyEmojiPickerState(); +} + +class _FlowyEmojiPickerState extends State { + EmojiData? emojiData; + + @override + void initState() { + super.initState(); + + // load the emoji data from cache if it's available + if (_cachedEmojiData != null) { + emojiData = _cachedEmojiData; + } else { + EmojiData.builtIn().then( + (value) { + _cachedEmojiData = value; + setState(() { + emojiData = value; + }); + }, + ); + } + } + + @override + Widget build(BuildContext context) { + if (emojiData == null) { + return const Center( + child: SizedBox.square( + dimension: 24.0, + child: CircularProgressIndicator( + strokeWidth: 2.0, + ), + ), + ); + } + + return EmojiPicker( + emojiData: emojiData!, + configuration: EmojiPickerConfiguration( + showSectionHeader: true, + showTabs: false, + defaultSkinTone: lastSelectedEmojiSkinTone ?? EmojiSkinTone.none, + ), + onEmojiSelected: widget.onEmojiSelected, + headerBuilder: (context, category) { + return FlowyEmojiHeader( + category: category, + ); + }, + itemBuilder: (context, emojiId, emoji, callback) { + return FlowyIconButton( + iconPadding: const EdgeInsets.all(2.0), + icon: FlowyText( + emoji, + fontSize: 28.0, + ), + onPressed: () => callback(emojiId, emoji), + ); + }, + searchBarBuilder: (context, keyword, skinTone) { + return FlowyEmojiSearchBar( + emojiData: emojiData!, + onKeywordChanged: (value) { + keyword.value = value; + }, + onSkinToneChanged: (value) { + skinTone.value = value; + }, + onRandomEmojiSelected: widget.onEmojiSelected, + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_header.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_header.dart new file mode 100644 index 0000000000..19b3ad939a --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_header.dart @@ -0,0 +1,48 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:emoji_mart/emoji_mart.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +class FlowyEmojiHeader extends StatelessWidget { + const FlowyEmojiHeader({ + super.key, + required this.category, + }); + + final Category category; + + @override + Widget build(BuildContext context) { + if (PlatformExtension.isDesktopOrWeb) { + return Container( + height: 22, + padding: const EdgeInsets.symmetric(horizontal: 8.0), + color: Theme.of(context).cardColor, + child: FlowyText.regular(category.id), + ); + } else { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 40, + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 8.0), + color: Theme.of(context).cardColor, + child: Padding( + padding: const EdgeInsets.only( + top: 14.0, + bottom: 4.0, + ), + child: FlowyText.regular(category.id), + ), + ), + const Divider( + height: 1, + thickness: 1, + ), + ], + ); + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_i18n.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_i18n.dart new file mode 100644 index 0000000000..0c4fb066aa --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_i18n.dart @@ -0,0 +1,41 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:emoji_mart/emoji_mart.dart'; + +class FlowyEmojiPickerI18n extends EmojiPickerI18n { + @override + String get activity => LocaleKeys.emoji_categories_activities.tr(); + + @override + String get flags => LocaleKeys.emoji_categories_flags.tr(); + + @override + String get foods => LocaleKeys.emoji_categories_food.tr(); + + @override + String get frequent => LocaleKeys.emoji_categories_frequentlyUsed.tr(); + + @override + String get nature => LocaleKeys.emoji_categories_nature.tr(); + + @override + String get objects => LocaleKeys.emoji_categories_objects.tr(); + + @override + String get people => LocaleKeys.emoji_categories_smileys.tr(); + + @override + String get places => LocaleKeys.emoji_categories_places.tr(); + + @override + String get search => LocaleKeys.emoji_search.tr(); + + @override + String get symbols => LocaleKeys.emoji_categories_symbols.tr(); + + @override + String get searchHintText => LocaleKeys.emoji_search.tr(); + + @override + String get searchNoResult => LocaleKeys.emoji_noEmojiFound.tr(); +} 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 new file mode 100644 index 0000000000..fa578bb132 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_screen.dart @@ -0,0 +1,22 @@ +import 'package:appflowy/plugins/base/icon/icon_picker_page.dart'; +import 'package:flutter/material.dart'; + +class MobileEmojiPickerScreen extends StatelessWidget { + static const routeName = '/emoji_picker'; + static const viewId = 'id'; + + const MobileEmojiPickerScreen({ + super.key, + required this.id, + }); + + /// view id + final String id; + + @override + Widget build(BuildContext context) { + return IconPickerPage( + id: id, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart new file mode 100644 index 0000000000..c7cf0a0943 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart @@ -0,0 +1,156 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:emoji_mart/emoji_mart.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; + +typedef EmojiKeywordChangedCallback = void Function(String keyword); +typedef EmojiSkinToneChanged = void Function(EmojiSkinTone skinTone); + +class FlowyEmojiSearchBar extends StatefulWidget { + const FlowyEmojiSearchBar({ + super.key, + required this.emojiData, + required this.onKeywordChanged, + required this.onSkinToneChanged, + required this.onRandomEmojiSelected, + }); + + final EmojiData emojiData; + final EmojiKeywordChangedCallback onKeywordChanged; + final EmojiSkinToneChanged onSkinToneChanged; + final EmojiSelectedCallback onRandomEmojiSelected; + + @override + State createState() => _FlowyEmojiSearchBarState(); +} + +class _FlowyEmojiSearchBarState extends State { + final TextEditingController controller = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric( + vertical: 8.0, + horizontal: PlatformExtension.isDesktopOrWeb ? 0.0 : 8.0, + ), + child: Row( + children: [ + Expanded( + child: _SearchTextField( + onKeywordChanged: widget.onKeywordChanged, + ), + ), + const HSpace(6.0), + _RandomEmojiButton( + emojiData: widget.emojiData, + onRandomEmojiSelected: widget.onRandomEmojiSelected, + ), + const HSpace(6.0), + FlowyEmojiSkinToneSelector( + onEmojiSkinToneChanged: widget.onSkinToneChanged, + ), + const HSpace(6.0), + ], + ), + ); + } +} + +class _RandomEmojiButton extends StatelessWidget { + const _RandomEmojiButton({ + required this.emojiData, + required this.onRandomEmojiSelected, + }); + + final EmojiData emojiData; + final EmojiSelectedCallback onRandomEmojiSelected; + + @override + Widget build(BuildContext context) { + return FlowyTooltip( + message: LocaleKeys.emoji_random.tr(), + child: FlowyButton( + useIntrinsicWidth: true, + text: const Icon( + Icons.shuffle_rounded, + ), + onTap: () { + final random = emojiData.random; + onRandomEmojiSelected( + random.$1, + random.$2, + ); + }, + ), + ); + } +} + +class _SearchTextField extends StatefulWidget { + const _SearchTextField({ + required this.onKeywordChanged, + }); + + final EmojiKeywordChangedCallback onKeywordChanged; + + @override + State<_SearchTextField> createState() => _SearchTextFieldState(); +} + +class _SearchTextFieldState extends State<_SearchTextField> { + final TextEditingController controller = TextEditingController(); + + @override + void dispose() { + controller.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 32.0, + ), + child: FlowyTextField( + autoFocus: true, + hintText: LocaleKeys.emoji_search.tr(), + controller: controller, + onChanged: widget.onKeywordChanged, + prefixIcon: const Padding( + padding: EdgeInsets.only( + left: 8.0, + right: 4.0, + ), + child: FlowySvg( + FlowySvgs.search_s, + ), + ), + prefixIconConstraints: const BoxConstraints( + maxHeight: 18.0, + ), + suffixIcon: Padding( + padding: const EdgeInsets.all(4.0), + child: FlowyButton( + text: const FlowySvg( + FlowySvgs.close_lg, + ), + margin: EdgeInsets.zero, + useIntrinsicWidth: true, + onTap: () { + controller.clear(); + widget.onKeywordChanged(''); + }, + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart new file mode 100644 index 0000000000..eebc73ed05 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart @@ -0,0 +1,112 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:emoji_mart/emoji_mart.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; + +// use a temporary global value to store last selected skin tone +EmojiSkinTone? lastSelectedEmojiSkinTone; + +class FlowyEmojiSkinToneSelector extends StatefulWidget { + const FlowyEmojiSkinToneSelector({ + super.key, + required this.onEmojiSkinToneChanged, + }); + + final EmojiSkinToneChanged onEmojiSkinToneChanged; + + @override + State createState() => + _FlowyEmojiSkinToneSelectorState(); +} + +class _FlowyEmojiSkinToneSelectorState + extends State { + EmojiSkinTone skinTone = EmojiSkinTone.none; + + @override + Widget build(BuildContext context) { + return PopoverActionList( + direction: PopoverDirection.bottomWithCenterAligned, + offset: const Offset(0, 8), + actions: EmojiSkinTone.values + .map((action) => EmojiSkinToneWrapper(action)) + .toList(), + buildChild: (controller) { + return FlowyTooltip( + message: LocaleKeys.emoji_selectSkinTone.tr(), + child: FlowyIconButton( + icon: Padding( + // add a left padding to align the emoji center + padding: const EdgeInsets.only( + left: 3.0, + ), + child: FlowyText( + lastSelectedEmojiSkinTone?.icon ?? 'โœ‹', + fontSize: 22.0, + ), + ), + onPressed: () => controller.show(), + ), + ); + }, + onSelected: (action, controller) async { + widget.onEmojiSkinToneChanged(action.inner); + setState(() { + lastSelectedEmojiSkinTone = action.inner; + }); + controller.close(); + }, + ); + } +} + +class EmojiSkinToneWrapper extends ActionCell { + EmojiSkinToneWrapper(this.inner); + + final EmojiSkinTone inner; + + Widget? icon(Color iconColor) => null; + + @override + String get name { + final String i18n; + switch (inner) { + case EmojiSkinTone.none: + i18n = LocaleKeys.emoji_skinTone_default.tr(); + case EmojiSkinTone.light: + i18n = LocaleKeys.emoji_skinTone_light.tr(); + case EmojiSkinTone.mediumLight: + i18n = LocaleKeys.emoji_skinTone_mediumLight.tr(); + case EmojiSkinTone.medium: + i18n = LocaleKeys.emoji_skinTone_medium.tr(); + case EmojiSkinTone.mediumDark: + i18n = LocaleKeys.emoji_skinTone_mediumDark.tr(); + case EmojiSkinTone.dark: + i18n = LocaleKeys.emoji_skinTone_dark.tr(); + } + return '${inner.icon} $i18n'; + } +} + +extension on EmojiSkinTone { + String get icon { + switch (this) { + case EmojiSkinTone.none: + return 'โœ‹'; + case EmojiSkinTone.light: + return 'โœ‹๐Ÿป'; + case EmojiSkinTone.mediumLight: + return 'โœ‹๐Ÿผ'; + case EmojiSkinTone.medium: + return 'โœ‹๐Ÿฝ'; + case EmojiSkinTone.mediumDark: + return 'โœ‹๐Ÿพ'; + case EmojiSkinTone.dark: + return 'โœ‹๐Ÿฟ'; + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart new file mode 100644 index 0000000000..e60555a1ef --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart @@ -0,0 +1,121 @@ +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:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flutter/material.dart'; + +enum FlowyIconType { + emoji, + icon, + custom; +} + +class FlowyIconPicker extends StatefulWidget { + const FlowyIconPicker({ + super.key, + required this.onSelected, + }); + + final void Function(FlowyIconType type, String value) onSelected; + + @override + State createState() => _FlowyIconPickerState(); +} + +class _FlowyIconPickerState extends State + with SingleTickerProviderStateMixin { + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + // ONLY supports emoji picker for now + return DefaultTabController( + length: 1, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + _buildTabs(context), + const Spacer(), + _RemoveIconButton( + onTap: () { + widget.onSelected(FlowyIconType.icon, ''); + }, + ), + ], + ), + const Divider( + height: 2, + ), + Expanded( + child: TabBarView( + children: [ + FlowyEmojiPicker( + onEmojiSelected: (_, emoji) { + widget.onSelected(FlowyIconType.emoji, emoji); + }, + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildTabs(BuildContext context) { + return Align( + alignment: Alignment.centerLeft, + child: TabBar( + indicatorSize: TabBarIndicatorSize.label, + isScrollable: true, + overlayColor: MaterialStatePropertyAll( + Theme.of(context).colorScheme.secondary, + ), + padding: EdgeInsets.zero, + tabs: [ + FlowyHover( + style: const HoverStyle(borderRadius: BorderRadius.zero), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 8.0, + ), + child: FlowyText( + LocaleKeys.emoji_emojiTab.tr(), + ), + ), + ) + ], + ), + ); + } +} + +class _RemoveIconButton extends StatelessWidget { + const _RemoveIconButton({ + required this.onTap, + }); + + final VoidCallback onTap; + @override + Widget build(BuildContext context) { + return SizedBox( + height: 28, + child: FlowyButton( + onTap: onTap, + useIntrinsicWidth: true, + text: FlowyText( + LocaleKeys.document_plugins_cover_removeIcon.tr(), + ), + leftIcon: const FlowySvg(FlowySvgs.delete_s), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker_page.dart b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker_page.dart new file mode 100644 index 0000000000..43cc4c67de --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker_page.dart @@ -0,0 +1,48 @@ +import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart'; +import 'package:appflowy/plugins/base/icon/icon_picker.dart'; +import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class IconPickerPage extends StatefulWidget { + const IconPickerPage({ + super.key, + required this.id, + }); + + /// view id + final String id; + + @override + State createState() => _IconPickerPageState(); +} + +class _IconPickerPageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + titleSpacing: 0, + title: const FlowyText.semibold( + 'Page icon', + fontSize: 14.0, + ), + leading: AppBarBackButton( + onTap: () => context.pop(), + ), + ), + body: SafeArea( + child: FlowyIconPicker( + onSelected: (_, emoji) { + ViewBackendService.updateViewIcon( + viewId: widget.id, + viewIcon: emoji, + ); + context.pop(); + }, + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_banner.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_banner.dart index 8a99d638ee..8de5322693 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_banner.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_banner.dart @@ -186,10 +186,9 @@ class _BannerTitleState extends State<_BannerTitle> { controller: widget.popoverController, triggerActions: PopoverTriggerFlags.none, direction: PopoverDirection.bottomWithLeftAligned, + constraints: const BoxConstraints(maxWidth: 380, maxHeight: 300), popupBuilder: (popoverContext) => _buildEmojiPicker((emoji) { - context - .read() - .add(RowBannerEvent.setIcon(emoji.emoji)); + context.read().add(RowBannerEvent.setIcon(emoji)); widget.popoverController.close(); }), child: Row(children: children), @@ -199,7 +198,7 @@ class _BannerTitleState extends State<_BannerTitle> { } } -typedef OnSubmittedEmoji = void Function(Emoji emoji); +typedef OnSubmittedEmoji = void Function(String emoji); const _kBannerActionHeight = 40.0; class EmojiButton extends StatelessWidget { @@ -286,12 +285,9 @@ class RemoveEmojiButton extends StatelessWidget { } Widget _buildEmojiPicker(OnSubmittedEmoji onSubmitted) { - return SizedBox( - height: 250, - child: EmojiSelectionMenu( - onSubmitted: onSubmitted, - onExit: () {}, - ), + return EmojiSelectionMenu( + onSubmitted: onSubmitted, + onExit: () {}, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index 1c424a2c83..607f139d32 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -12,6 +12,7 @@ import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/util/base64_string.dart'; import 'package:appflowy/workspace/application/notifications/notification_action.dart'; import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart'; +import 'package:appflowy/workspace/application/view/prelude.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart' @@ -111,9 +112,7 @@ class _DocumentPageState extends State { styleCustomizer: EditorStyleCustomizer( context: context, // the 44 is the width of the left action list - padding: PlatformExtension.isMobile - ? const EdgeInsets.only(left: 20, right: 20) - : const EdgeInsets.only(left: 40, right: 40 + 44), + padding: EditorStyleCustomizer.documentPadding, ), header: _buildCoverAndIcon(context), ); @@ -140,6 +139,13 @@ class _DocumentPageState extends State { return DocumentHeaderNodeWidget( node: page, editorState: editorState!, + view: widget.view, + onIconChanged: (icon) async { + await ViewBackendService.updateViewIcon( + viewId: widget.view.id, + viewIcon: icon, + ); + }, ); } 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 8b4bf37ffb..2d4e48379b 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 @@ -15,7 +15,7 @@ class EmojiPickerButton extends StatelessWidget { final String emoji; final double emojiSize; final Size emojiPickerSize; - final void Function(Emoji emoji, PopoverController controller) onSubmitted; + final void Function(String emoji, PopoverController controller) onSubmitted; final PopoverController popoverController = PopoverController(); @override diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart index 3d67ac106b..7ede37eb72 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart @@ -186,7 +186,7 @@ class _CalloutBlockComponentWidgetState ), // force to refresh the popover state emoji: emoji, onSubmitted: (emoji, controller) { - setEmoji(emoji.emoji); + setEmoji(emoji); controller.close(); }, ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart index c958aac802..358c85ff2d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart @@ -2,17 +2,23 @@ import 'dart:io'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; +import 'package:appflowy/plugins/base/icon/icon_picker.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; +import 'package:appflowy/plugins/document/presentation/editor_style.dart'; +import 'package:appflowy/workspace/application/view/view_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_listener.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/rounded_button.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; import 'cover_editor.dart'; -import 'emoji_icon_widget.dart'; -import 'emoji_popover.dart'; const double kCoverHeight = 250.0; const double kIconHeight = 60.0; @@ -45,13 +51,17 @@ enum CoverType { class DocumentHeaderNodeWidget extends StatefulWidget { const DocumentHeaderNodeWidget({ + super.key, required this.node, required this.editorState, - super.key, + required this.onIconChanged, + required this.view, }); final Node node; final EditorState editorState; + final void Function(String icon) onIconChanged; + final ViewPB view; @override State createState() => @@ -64,19 +74,33 @@ class _DocumentHeaderNodeWidgetState extends State { ); String? get coverDetails => widget.node.attributes[DocumentHeaderBlockKeys.coverDetails]; - String get icon => widget.node.attributes[DocumentHeaderBlockKeys.icon]; - bool get hasIcon => - widget.node.attributes[DocumentHeaderBlockKeys.icon]?.isNotEmpty ?? false; + String? get icon => widget.node.attributes[DocumentHeaderBlockKeys.icon]; + bool get hasIcon => viewIcon.isNotEmpty; bool get hasCover => coverType != CoverType.none; + String viewIcon = ''; + late final ViewListener viewListener; + @override void initState() { super.initState(); + final value = widget.view.icon.value; + viewIcon = value.isNotEmpty ? value : icon ?? ''; widget.node.addListener(_reload); + viewListener = ViewListener( + viewId: widget.view.id, + )..start( + onViewUpdated: (p0) { + setState(() { + viewIcon = p0.icon.value; + }); + }, + ); } @override void dispose() { + viewListener.stop(); widget.node.removeListener(_reload); super.dispose(); } @@ -108,7 +132,7 @@ class _DocumentHeaderNodeWidgetState extends State { ), if (hasIcon) Positioned( - left: 80, + left: PlatformExtension.isDesktopOrWeb ? 80 : 20, // if hasCover, there shouldn't be icons present so the icon can // be closer to the bottom. bottom: @@ -116,8 +140,10 @@ class _DocumentHeaderNodeWidgetState extends State { child: DocumentIcon( editorState: widget.editorState, node: widget.node, - icon: icon, - onIconChanged: (icon) => _saveCover(icon: icon), + icon: viewIcon, + onIconChanged: (icon) async { + _saveCover(icon: icon); + }, ), ), ], @@ -153,6 +179,7 @@ class _DocumentHeaderNodeWidgetState extends State { } if (icon != null) { attributes[DocumentHeaderBlockKeys.icon] = icon; + widget.onIconChanged(icon); } transaction.updateNode(widget.node, attributes); @@ -188,29 +215,42 @@ class _DocumentHeaderToolbarState extends State { final PopoverController _popoverController = PopoverController(); + @override + void initState() { + super.initState(); + + isHidden = PlatformExtension.isDesktopOrWeb; + } + @override Widget build(BuildContext context) { - return MouseRegion( - onEnter: (event) => setHidden(false), - onExit: (event) { - if (!isPopoverOpen) { - setHidden(true); - } - }, - opaque: false, - child: Container( - alignment: Alignment.bottomLeft, - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 40), - child: SizedBox( - height: 28, - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: buildRowChildren(), - ), + Widget child = Container( + alignment: Alignment.bottomLeft, + width: double.infinity, + padding: EditorStyleCustomizer.documentPadding, + child: SizedBox( + height: 28, + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: buildRowChildren(), ), ), ); + + if (PlatformExtension.isDesktopOrWeb) { + child = MouseRegion( + onEnter: (event) => setHidden(false), + onExit: (event) { + if (!isPopoverOpen) { + setHidden(true); + } + }, + opaque: false, + child: child, + ); + } + + return child; } List buildRowChildren() { @@ -251,42 +291,50 @@ class _DocumentHeaderToolbarState extends State { ), ); } else { - children.add( - AppFlowyPopover( + Widget child = FlowyButton( + leftIconSize: const Size.square(18), + useIntrinsicWidth: true, + leftIcon: const Icon( + Icons.emoji_emotions_outlined, + size: 18, + ), + text: FlowyText.regular( + LocaleKeys.document_plugins_cover_addIcon.tr(), + ), + onTap: PlatformExtension.isDesktop + ? null + : () => context.push( + Uri( + path: MobileEmojiPickerScreen.routeName, + queryParameters: { + MobileEmojiPickerScreen.viewId: + context.read().state.view.id, + }, + ).toString(), + ), + ); + + if (PlatformExtension.isDesktop) { + child = AppFlowyPopover( onClose: () => isPopoverOpen = false, controller: _popoverController, offset: const Offset(0, 8), direction: PopoverDirection.bottomWithCenterAligned, - constraints: BoxConstraints.loose(const Size(300, 250)), - child: FlowyButton( - leftIconSize: const Size.square(18), - useIntrinsicWidth: true, - leftIcon: const Icon( - Icons.emoji_emotions_outlined, - size: 18, - ), - text: FlowyText.regular( - LocaleKeys.document_plugins_cover_addIcon.tr(), - ), - ), + constraints: BoxConstraints.loose(const Size(360, 380)), + child: child, popupBuilder: (BuildContext popoverContext) { isPopoverOpen = true; - return EmojiPopover( - showRemoveButton: widget.hasIcon, - removeIcon: () { - widget.onCoverChanged(icon: ""); - _popoverController.close(); - }, - node: widget.node, - editorState: widget.editorState, - onEmojiChanged: (Emoji emoji) { - widget.onCoverChanged(icon: emoji.emoji); + return FlowyIconPicker( + onSelected: (type, value) { + widget.onCoverChanged(icon: value); _popoverController.close(); }, ); }, - ), - ); + ); + } + + children.add(child); } return children; @@ -471,27 +519,41 @@ class _DocumentIconState extends State { @override Widget build(BuildContext context) { - return AppFlowyPopover( - direction: PopoverDirection.bottomWithCenterAligned, - controller: _popoverController, - offset: const Offset(0, 8), - constraints: BoxConstraints.loose(const Size(320, 380)), - child: EmojiIconWidget(emoji: widget.icon), - popupBuilder: (BuildContext popoverContext) { - return EmojiPopover( - node: widget.node, - showRemoveButton: true, - removeIcon: () { - widget.onIconChanged(""); - _popoverController.close(); - }, - editorState: widget.editorState, - onEmojiChanged: (Emoji emoji) { - widget.onIconChanged(emoji.emoji); - _popoverController.close(); - }, - ); - }, + Widget child = EmojiIconWidget( + emoji: widget.icon, ); + + if (PlatformExtension.isDesktopOrWeb) { + child = AppFlowyPopover( + direction: PopoverDirection.bottomWithCenterAligned, + controller: _popoverController, + offset: const Offset(0, 8), + constraints: BoxConstraints.loose(const Size(360, 380)), + child: child, + popupBuilder: (BuildContext popoverContext) { + return FlowyIconPicker( + onSelected: (type, value) { + widget.onIconChanged(value); + _popoverController.close(); + }, + ); + }, + ); + } else { + child = GestureDetector( + child: child, + onTap: () => context.push( + Uri( + path: MobileEmojiPickerScreen.routeName, + queryParameters: { + MobileEmojiPickerScreen.viewId: + context.read().state.view.id, + }, + ).toString(), + ), + ); + } + + return child; } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_popover.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_popover.dart deleted file mode 100644 index 4dfd9ece38..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_popover.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; - -import 'package:flutter/material.dart'; - -/// Add icon menu in Header -class EmojiPopover extends StatefulWidget { - final EditorState editorState; - final Node node; - final void Function(Emoji emoji) onEmojiChanged; - final VoidCallback removeIcon; - final bool showRemoveButton; - - const EmojiPopover({ - super.key, - required this.editorState, - required this.node, - required this.onEmojiChanged, - required this.removeIcon, - required this.showRemoveButton, - }); - - @override - State createState() => _EmojiPopoverState(); -} - -class _EmojiPopoverState extends State { - @override - Widget build(BuildContext context) { - return Column( - children: [ - if (widget.showRemoveButton) - Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: Align( - alignment: Alignment.centerRight, - child: DeleteButton(onTap: widget.removeIcon), - ), - ), - Expanded( - child: EmojiPicker( - onEmojiSelected: (category, emoji) { - widget.onEmojiChanged(emoji); - }, - config: buildFlowyEmojiPickerConfig(context), - ), - ), - ], - ); - } -} - -class DeleteButton extends StatelessWidget { - final VoidCallback onTap; - const DeleteButton({required this.onTap, Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 28, - child: FlowyButton( - onTap: onTap, - useIntrinsicWidth: true, - text: FlowyText( - LocaleKeys.document_plugins_cover_removeIcon.tr(), - ), - leftIcon: const FlowySvg(FlowySvgs.delete_s), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index 503332dfef..d7f925d00f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -29,6 +29,10 @@ class EditorStyleCustomizer { throw UnimplementedError(); } + static EdgeInsets get documentPadding => PlatformExtension.isMobile + ? const EdgeInsets.only(left: 20, right: 20) + : const EdgeInsets.only(left: 40, right: 40 + 44); + EditorStyle desktop() { final theme = Theme.of(context); final fontSize = context.read().state.fontSize; diff --git a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart index 134fd36fd8..a9415f14d6 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart @@ -3,6 +3,7 @@ import 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dar import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart'; import 'package:appflowy/mobile/presentation/favorite/mobile_favorite_page.dart'; import 'package:appflowy/mobile/presentation/presentation.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_picker_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'; @@ -47,6 +48,9 @@ GoRouter generateRouter(Widget child) { // trash _mobileHomeTrashPageRoute(), + + // emoji picker + _mobileEmojiPickerPageRoute(), ], // Desktop and Mobile @@ -200,6 +204,21 @@ GoRoute _mobileHomeTrashPageRoute() { ); } +GoRoute _mobileEmojiPickerPageRoute() { + return GoRoute( + parentNavigatorKey: AppGlobals.rootNavKey, + path: MobileEmojiPickerScreen.routeName, + pageBuilder: (context, state) { + final id = state.uri.queryParameters[MobileEmojiPickerScreen.viewId]!; + return MaterialPage( + child: MobileEmojiPickerScreen( + id: id, + ), + ); + }, + ); +} + GoRoute _desktopHomeScreenRoute() { return GoRoute( path: DesktopHomeScreen.routeName, diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart index 3c27ea599a..8536d50fdc 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart @@ -1,10 +1,9 @@ import 'dart:async'; -import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'; -import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart'; +import 'package:dartz/dartz.dart'; class ViewBackendService { static Future> createView({ @@ -149,9 +148,24 @@ class ViewBackendService { if (isFavorite != null) { payload.isFavorite = isFavorite; } + return FolderEventUpdateView(payload).send(); } + static Future> updateViewIcon({ + required String viewId, + required String viewIcon, + }) { + final icon = ViewIconPB() + ..ty = ViewIconTypePB.Emoji + ..value = viewIcon; + final payload = UpdateViewIconPayloadPB.create() + ..viewId = viewId + ..icon = icon; + + return FolderEventUpdateViewIcon(payload).send(); + } + // deprecated static Future> moveView({ required String viewId, 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 e3ea73cfe4..05ce8902e2 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 @@ -1,10 +1,11 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.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'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_bloc.dart'; +import 'package:appflowy/workspace/application/view/prelude.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart'; @@ -14,6 +15,7 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.d import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.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'; @@ -36,6 +38,7 @@ class ViewItem extends StatelessWidget { this.isFirstChild = false, this.isDraggable = true, required this.isFeedback, + this.height = 28.0, }); final ViewPB view; @@ -67,6 +70,8 @@ class ViewItem extends StatelessWidget { // identify if the view item is rendered as feedback widget inside DraggableItem final bool isFeedback; + final double height; + @override Widget build(BuildContext context) { return BlocProvider( @@ -96,6 +101,7 @@ class ViewItem extends StatelessWidget { isFirstChild: isFirstChild, isDraggable: isDraggable, isFeedback: isFeedback, + height: height, ); }, ), @@ -121,6 +127,7 @@ class InnerViewItem extends StatelessWidget { this.onTertiarySelected, this.isFirstChild = false, required this.isFeedback, + required this.height, }); final ViewPB view; @@ -140,6 +147,7 @@ class InnerViewItem extends StatelessWidget { final bool showActions; final ViewItemOnSelected onSelected; final ViewItemOnSelected? onTertiarySelected; + final double height; @override Widget build(BuildContext context) { @@ -155,6 +163,7 @@ class InnerViewItem extends StatelessWidget { isDraggable: isDraggable, leftPadding: leftPadding, isFeedback: isFeedback, + height: height, ); // if the view is expanded and has child views, render its child views @@ -233,6 +242,7 @@ class SingleInnerViewItem extends StatefulWidget { required this.onSelected, this.onTertiarySelected, required this.isFeedback, + required this.height, }); final ViewPB view; @@ -249,12 +259,16 @@ class SingleInnerViewItem extends StatefulWidget { final ViewItemOnSelected onSelected; final ViewItemOnSelected? onTertiarySelected; final FolderCategoryType categoryType; + final double height; @override State createState() => _SingleInnerViewItemState(); } class _SingleInnerViewItemState extends State { + final controller = PopoverController(); + bool isIconPickerOpened = false; + @override Widget build(BuildContext context) { if (widget.isFeedback) { @@ -266,7 +280,8 @@ class _SingleInnerViewItemState extends State { hoverColor: Theme.of(context).colorScheme.secondary, ), resetHoverOnRebuild: widget.showActions, - buildWhenOnHover: () => !widget.showActions && !_isDragging, + buildWhenOnHover: () => + !widget.showActions && !_isDragging && !isIconPickerOpened, builder: (_, onHover) => _buildViewItem(onHover), isSelected: () => widget.showActions || @@ -279,10 +294,7 @@ class _SingleInnerViewItemState extends State { // expand icon _buildLeftIcon(), // icon - SizedBox.square( - dimension: 16, - child: widget.view.defaultIcon(), - ), + _buildViewIconButton(), const HSpace(5), // title Expanded( @@ -309,7 +321,7 @@ class _SingleInnerViewItemState extends State { onTap: () => widget.onSelected(widget.view), onTertiaryTapDown: (_) => widget.onTertiarySelected?.call(widget.view), child: SizedBox( - height: 26, + height: widget.height, child: Padding( padding: EdgeInsets.only(left: widget.level * widget.leftPadding), child: Row( @@ -320,6 +332,47 @@ class _SingleInnerViewItemState extends State { ); } + Widget _buildViewIconButton() { + final icon = widget.view.icon.value.isNotEmpty + ? FlowyText( + widget.view.icon.value, + fontSize: 18.0, + ) + : SizedBox.square( + dimension: 20.0, + child: widget.view.defaultIcon(), + ); + return AppFlowyPopover( + offset: const Offset(20, 0), + controller: controller, + direction: PopoverDirection.rightWithCenterAligned, + constraints: BoxConstraints.loose(const Size(360, 380)), + onClose: () => setState(() { + isIconPickerOpened = false; + }), + child: GestureDetector( + // prevent the tap event from being passed to the parent widget + onTap: () {}, + child: FlowyTooltip( + message: LocaleKeys.document_plugins_cover_changeIcon.tr(), + child: icon, + ), + ), + popupBuilder: (context) { + isIconPickerOpened = true; + return FlowyIconPicker( + onSelected: (_, emoji) { + ViewBackendService.updateViewIcon( + viewId: widget.view.id, + viewIcon: emoji, + ); + controller.close(); + }, + ); + }, + ); + } + // > button or ยท button // show > if the view is expandable. // show ยท if the view can't contain child views. 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 65ad4c642e..d7a1a26657 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,11 +1,10 @@ +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:flowy_infra_ui/style_widget/decoration.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'emoji_picker.dart'; - SelectionMenuItem emojiMenuItem = SelectionMenuItem( name: 'Emoji', icon: (editorState, onSelected, style) => SelectableIconWidget( @@ -54,7 +53,7 @@ void showEmojiPickerMenu( ), child: EmojiSelectionMenu( onSubmitted: (emoji) { - editorState.insertTextAtCurrentSelection(emoji.emoji); + editorState.insertTextAtCurrentSelection(emoji); }, onExit: () { // close emoji panel @@ -73,7 +72,7 @@ class EmojiSelectionMenu extends StatefulWidget { required this.onExit, }) : super(key: key); - final void Function(Emoji emoji) onSubmitted; + final void Function(String emoji) onSubmitted; final void Function() onExit; @override @@ -111,9 +110,10 @@ class _EmojiSelectionMenuState extends State { @override Widget build(BuildContext context) { - return EmojiPicker( - onEmojiSelected: (category, emoji) => widget.onSubmitted(emoji), - config: buildFlowyEmojiPickerConfig(context), + return FlowyEmojiPicker( + onEmojiSelected: (_, emoji) { + widget.onSubmitted(emoji); + }, ); } } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart index 24135d9228..efad4a6f30 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart @@ -22,6 +22,10 @@ class FlowyTextField extends StatefulWidget { final String? errorText; final int maxLines; final bool showCounter; + final Widget? prefixIcon; + final Widget? suffixIcon; + final BoxConstraints? prefixIconConstraints; + final BoxConstraints? suffixIconConstraints; const FlowyTextField({ super.key, @@ -42,6 +46,10 @@ class FlowyTextField extends StatefulWidget { this.errorText, this.maxLines = 1, this.showCounter = true, + this.prefixIcon, + this.suffixIcon, + this.prefixIconConstraints, + this.suffixIconConstraints, }); @override @@ -55,6 +63,8 @@ class FlowyTextFieldState extends State { @override void initState() { + super.initState(); + focusNode = widget.focusNode ?? FocusNode(); focusNode.addListener(notifyDidEndEditing); @@ -67,10 +77,10 @@ class FlowyTextFieldState extends State { WidgetsBinding.instance.addPostFrameCallback((_) { focusNode.requestFocus(); controller.selection = TextSelection.fromPosition( - TextPosition(offset: controller.text.length)); + TextPosition(offset: controller.text.length), + ); }); } - super.initState(); } void _debounceOnChangedText(Duration duration, String text) { @@ -113,6 +123,7 @@ class FlowyTextFieldState extends State { maxLength: widget.maxLength, maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds, style: widget.textStyle ?? Theme.of(context).textTheme.bodySmall, + textAlignVertical: TextAlignVertical.center, decoration: InputDecoration( constraints: BoxConstraints( maxHeight: widget.errorText?.isEmpty ?? true ? 32 : 58), @@ -158,6 +169,10 @@ class FlowyTextFieldState extends State { ), borderRadius: Corners.s8Border, ), + prefixIcon: widget.prefixIcon, + suffixIcon: widget.suffixIcon, + prefixIconConstraints: widget.prefixIconConstraints, + suffixIconConstraints: widget.suffixIconConstraints, ), ); } diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 397d874f6e..74658d9c67 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -53,12 +53,11 @@ packages: appflowy_editor: dependency: "direct main" description: - path: "." - ref: "7336274" - resolved-ref: "7336274ff90402c8dd790b029e00cac60c580f28" - url: "https://github.com/AppFlowy-IO/appflowy-editor.git" - source: git - version: "1.5.0" + name: appflowy_editor + sha256: d3112408f28ca3b7b8d3d1ecc90a0c1ba7c1fe807ab285c07b1e9d312b1d3cad + url: "https://pub.dev" + source: hosted + version: "1.5.1" appflowy_popover: dependency: "direct main" description: @@ -346,6 +345,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0+3" + easy_debounce: + dependency: transitive + description: + name: easy_debounce + sha256: f082609cfb8f37defb9e37fc28bc978c6712dedf08d4c5a26f820fa10165a236 + url: "https://pub.dev" + source: hosted + version: "2.0.3" easy_localization: dependency: "direct main" description: @@ -362,6 +369,15 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.2" + emoji_mart: + dependency: "direct main" + description: + path: "." + ref: "067f718" + resolved-ref: "067f7188965c8fcb7be02ce174ce2b6757f288ee" + url: "https://github.com/LucasXu0/emoji_mart.git" + source: git + version: "0.0.1" envied: dependency: "direct main" description: @@ -533,6 +549,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + flutter_sticky_header: + dependency: transitive + description: + name: flutter_sticky_header + sha256: "017f398fbb45a589e01491861ca20eb6570a763fd9f3888165a978e11248c709" + url: "https://pub.dev" + source: hosted + version: "0.6.5" flutter_svg: dependency: "direct main" description: @@ -571,10 +595,10 @@ packages: dependency: "direct main" description: name: freezed_annotation - sha256: aeac15850ef1b38ee368d4c53ba9a847e900bb2c53a4db3f6881cbb3cb684338 + sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.4.1" frontend_server_client: dependency: transitive description: @@ -1390,6 +1414,14 @@ packages: description: flutter source: sdk version: "0.0.99" + sliver_tools: + dependency: transitive + description: + name: sliver_tools + sha256: eae28220badfb9d0559207badcbbc9ad5331aac829a88cb0964d330d2a4636a6 + url: "https://pub.dev" + source: hosted + version: "0.2.12" source_gen: dependency: transitive description: @@ -1728,6 +1760,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.7" + value_layout_builder: + dependency: transitive + description: + name: value_layout_builder + sha256: "98202ec1807e94ac72725b7f0d15027afde513c55c69ff3f41bcfccb950831bc" + url: "https://pub.dev" + source: hosted + version: "0.3.1" vector_graphics: dependency: transitive description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index abe808cd1c..24f060e392 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -44,10 +44,7 @@ dependencies: git: url: https://github.com/AppFlowy-IO/appflowy-board.git ref: 6aba8dd - appflowy_editor: - git: - url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "7336274" + appflowy_editor: ^1.5.1 appflowy_popover: path: packages/appflowy_popover @@ -110,6 +107,10 @@ dependencies: go_router: ^10.1.2 string_validator: ^1.0.0 unsplash_client: ^2.1.1 + emoji_mart: + git: + url: https://github.com/LucasXu0/emoji_mart.git + ref: "067f718" # Notifications # TODO: Consider implementing custom package diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 4adb7acc98..5aea6e5b44 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -619,13 +619,14 @@ "add": "Add", "back": "Back", "saveToGallery": "Save to gallery", - "removeIcon": "Remove Icon", + "removeIcon": "Remove icon", "pasteImageUrl": "Paste image URL", "or": "OR", "pickFromFiles": "Pick from files", "couldNotFetchImage": "Could not fetch image", "imageSavingFailed": "Image Saving Failed", - "addIcon": "Add Icon", + "addIcon": "Add icon", + "changeIcon": "Change icon", "coverRemoveAlert": "It will be removed from cover after it is deleted.", "alertDialogConfirmation": "Are you sure, you want to continue?" }, @@ -818,6 +819,7 @@ "gray": "Gray" }, "emoji": { + "emojiTab": "Emoji", "search": "Search emoji", "noRecent": "No recent emoji", "noEmojiFound": "No emoji found", @@ -837,6 +839,14 @@ "flags": "Flags", "nature": "Nature", "frequentlyUsed": "Frequently Used" + }, + "skinTone": { + "default": "Default", + "light": "Light", + "mediumLight": "Medium-Light", + "medium": "Medium", + "mediumDark": "Medium-Dark", + "dark": "Dark" } }, "inlineActions": {