From a8f136eda2b1da2b7c0b6de0295646e4bf1ac5d0 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 27 May 2024 08:51:49 +0800 Subject: [PATCH] feat: sidebar UI Revamp on Desktop (#5343) --- .../collaborative_workspace_test.dart | 5 +- .../desktop/board/board_test_runner.dart | 4 +- .../document/document_more_actions_test.dart | 2 + .../document_with_cover_image_test.dart | 12 +- .../desktop/sidebar/sidebar_expand_test.dart | 12 +- .../sidebar/sidebar_favorites_test.dart | 12 +- .../desktop/sidebar/sidebar_test_runner.dart | 2 +- .../integration_test/desktop_runner_3.dart | 4 +- .../shared/common_operations.dart | 16 +- .../shared/editor_test_operations.dart | 15 +- .../integration_test/shared/expectation.dart | 4 +- .../integration_test/shared/settings.dart | 3 +- .../integration_test/shared/workspace.dart | 23 +- .../base/type_option_menu_item.dart | 2 +- .../base/view_page/more_bottom_sheet.dart | 1 + .../show_mobile_bottom_sheet.dart | 2 +- .../mobile_home_favorite_folder.dart | 9 +- .../presentation/home/mobile_folders.dart | 6 +- .../home/mobile_home_page_header.dart | 4 +- .../mobile_home_section_folder.dart | 18 +- .../workspace_menu_bottom_sheet.dart | 4 +- .../page_item/mobile_view_item.dart | 32 +- .../lib/plugins/base/emoji/emoji_picker.dart | 33 +- .../base/emoji/emoji_picker_header.dart | 9 +- .../plugins/base/emoji/emoji_search_bar.dart | 64 ++-- .../plugins/base/emoji/emoji_skin_tone.dart | 37 ++- .../lib/plugins/base/emoji/emoji_text.dart | 1 + .../lib/plugins/base/icon/icon_picker.dart | 64 +--- .../database/tab_bar/tab_bar_view.dart | 5 +- .../database/widgets/share_button.dart | 11 +- .../lib/plugins/document/document.dart | 7 +- .../document/presentation/editor_page.dart | 6 - .../editor_plugins/actions/option_action.dart | 2 +- .../base/emoji_picker_button.dart | 43 ++- .../find_and_replace_menu.dart | 2 +- .../header/document_header_node_widget.dart | 19 +- .../image/unsplash_image_widget.dart | 2 +- .../presentation/share/share_button.dart | 11 +- .../application/favorite/favorite_bloc.dart | 46 ++- .../favorite/favorite_service.dart | 36 +- .../settings/appearance/base_appearance.dart | 2 +- .../sidebar/folder/folder_bloc.dart | 14 +- .../workspace/application/view/view_bloc.dart | 36 +- .../workspace/application/view/view_ext.dart | 13 + .../view_title/view_title_bar_bloc.dart | 48 +++ .../view_title/view_title_bloc.dart | 73 +++++ .../presentation/home/home_sizes.dart | 15 +- .../presentation/home/home_stack.dart | 13 +- .../workspace/presentation/home/hotkeys.dart | 2 +- .../sidebar/favorites/favorite_folder.dart | 183 +++++++++++ .../menu/sidebar/favorites/favorite_menu.dart | 193 +++++++++++ .../sidebar/favorites/favorite_menu_bloc.dart | 124 +++++++ .../favorites/favorite_more_actions.dart | 67 ++++ .../favorites/favorite_pin_action.dart | 43 +++ .../sidebar/favorites/favorite_pin_bloc.dart | 59 ++++ .../menu/sidebar/folder/_favorite_folder.dart | 115 ------- .../menu/sidebar/folder/_folder_header.dart | 59 ++-- .../menu/sidebar/folder/_section_folder.dart | 209 +++++++----- .../menu/sidebar/footer/sidebar_footer.dart | 70 ++++ .../{ => header}/sidebar_top_menu.dart | 32 +- .../sidebar/{ => header}/sidebar_user.dart | 10 +- .../{ => shared}/rename_view_dialog.dart | 0 .../sidebar/{ => shared}/sidebar_folder.dart | 13 +- .../shared/sidebar_new_page_button.dart | 62 ++++ .../sidebar/{ => shared}/sidebar_setting.dart | 22 +- .../home/menu/sidebar/sidebar.dart | 40 ++- .../menu/sidebar/sidebar_new_page_button.dart | 70 ---- .../home/menu/sidebar/sidebar_trash.dart | 63 ---- .../workspace/_sidebar_workspace_actions.dart | 71 +++- .../workspace/_sidebar_workspace_icon.dart | 14 +- .../workspace/_sidebar_workspace_menu.dart | 237 +++++++++---- .../{ => workspace}/sidebar_workspace.dart | 72 ++-- .../home/menu/view/draggable_view_item.dart | 34 +- .../home/menu/view/view_action_type.dart | 59 +++- .../home/menu/view/view_add_button.dart | 12 +- .../home/menu/view/view_item.dart | 272 +++++++++------ .../menu/view/view_more_action_button.dart | 185 +++++++++-- .../presentation/home/tabs/flowy_tab.dart | 2 +- .../presentation/home/tabs/tabs_manager.dart | 2 +- .../widgets/notification_button.dart | 6 +- .../settings/pages/settings_account_view.dart | 8 +- .../pages/settings_workspace_view.dart | 8 +- .../feature_flags/feature_flag_page.dart | 6 +- .../files/settings_file_exporter_widget.dart | 5 +- .../presentation/widgets/favorite_button.dart | 9 +- .../widgets/float_bubble/question_bubble.dart | 7 +- .../more_view_actions/more_view_actions.dart | 7 +- .../presentation/widgets/pop_up_action.dart | 6 +- .../widgets/rename_view_popover.dart | 21 +- .../presentation/widgets/user_avatar.dart | 27 +- .../presentation/widgets/view_title_bar.dart | 310 +++++++----------- .../macos/Runner/MainFlutterWindow.swift | 12 +- .../appflowy_popover/lib/src/popover.dart | 10 +- .../packages/flowy_infra_ui/lib/basis.dart | 5 - .../lib/style_widget/button.dart | 3 +- .../lib/style_widget/decoration.dart | 3 +- .../flowy_infra_ui/lib/style_widget/text.dart | 40 +-- .../resources/flowy_icons/16x/add_cover.svg | 7 + .../resources/flowy_icons/16x/add_icon.svg | 5 + .../flowy_icons/16x/add_workspace.svg | 6 + .../resources/flowy_icons/16x/change_icon.svg | 4 + .../flowy_icons/16x/collapse_all_page.svg | 14 + .../resources/flowy_icons/16x/duplicate.svg | 4 + .../resources/flowy_icons/16x/favorite.svg | 2 +- .../flowy_icons/16x/favorite_header_icon.svg | 3 + .../flowy_icons/16x/favorite_section_pin.svg | 5 + .../favorite_section_remove_from_favorite.svg | 5 + .../16x/favorite_section_unpin.svg | 5 + .../resources/flowy_icons/16x/hide_menu.svg | 10 +- .../flowy_icons/16x/icon_shuffle.svg | 7 + frontend/resources/flowy_icons/16x/more.svg | 8 +- .../resources/flowy_icons/16x/move_to.svg | 5 + .../flowy_icons/16x/notification.svg | 5 + frontend/resources/flowy_icons/16x/search.svg | 6 +- .../resources/flowy_icons/16x/settings.svg | 4 +- .../flowy_icons/16x/sidebar_footer_trash.svg | 9 + .../flowy_icons/16x/sidebar_footer_widget.svg | 15 + .../resources/flowy_icons/16x/three-dots.svg | 6 +- .../flowy_icons/16x/title_bar_divider.svg | 5 + frontend/resources/flowy_icons/16x/trash.svg | 7 + .../resources/flowy_icons/16x/unfavorite.svg | 4 +- .../flowy_icons/16x/view_item_add.svg | 6 + .../flowy_icons/16x/view_item_expand.svg | 5 + .../16x/view_item_open_in_new_tab.svg | 5 + .../flowy_icons/16x/view_item_rename.svg | 5 + .../flowy_icons/16x/view_item_right_arrow.svg | 10 + .../flowy_icons/16x/view_item_unexpand.svg | 5 + .../16x/workspace_drop_down_menu_hide.svg | 8 + .../16x/workspace_drop_down_menu_show.svg | 8 + .../flowy_icons/16x/workspace_logout.svg | 5 + .../flowy_icons/16x/workspace_selected.svg | 10 + .../flowy_icons/16x/workspace_three_dots.svg | 7 + frontend/resources/translations/en.json | 18 +- .../tests/folder/local_test/script.rs | 6 +- .../flowy-folder/src/entities/view.rs | 14 + .../flowy-folder/src/event_handler.rs | 9 +- .../rust-lib/flowy-folder/src/event_map.rs | 2 +- frontend/scripts/tool/update_collab_source.sh | 4 +- 138 files changed, 2672 insertions(+), 1299 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bar_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_bloc.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_favorite_folder.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart rename frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/{ => header}/sidebar_top_menu.dart (74%) rename frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/{ => header}/sidebar_user.dart (93%) rename frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/{ => shared}/rename_view_dialog.dart (100%) rename frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/{ => shared}/sidebar_folder.dart (94%) create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart rename frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/{ => shared}/sidebar_setting.dart (86%) delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_trash.dart rename frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/{ => workspace}/sidebar_workspace.dart (79%) create mode 100644 frontend/resources/flowy_icons/16x/add_cover.svg create mode 100644 frontend/resources/flowy_icons/16x/add_icon.svg create mode 100644 frontend/resources/flowy_icons/16x/add_workspace.svg create mode 100644 frontend/resources/flowy_icons/16x/change_icon.svg create mode 100644 frontend/resources/flowy_icons/16x/collapse_all_page.svg create mode 100644 frontend/resources/flowy_icons/16x/duplicate.svg create mode 100644 frontend/resources/flowy_icons/16x/favorite_header_icon.svg create mode 100644 frontend/resources/flowy_icons/16x/favorite_section_pin.svg create mode 100644 frontend/resources/flowy_icons/16x/favorite_section_remove_from_favorite.svg create mode 100644 frontend/resources/flowy_icons/16x/favorite_section_unpin.svg create mode 100644 frontend/resources/flowy_icons/16x/icon_shuffle.svg create mode 100644 frontend/resources/flowy_icons/16x/move_to.svg create mode 100644 frontend/resources/flowy_icons/16x/notification.svg create mode 100644 frontend/resources/flowy_icons/16x/sidebar_footer_trash.svg create mode 100644 frontend/resources/flowy_icons/16x/sidebar_footer_widget.svg create mode 100644 frontend/resources/flowy_icons/16x/title_bar_divider.svg create mode 100644 frontend/resources/flowy_icons/16x/trash.svg create mode 100644 frontend/resources/flowy_icons/16x/view_item_add.svg create mode 100644 frontend/resources/flowy_icons/16x/view_item_expand.svg create mode 100644 frontend/resources/flowy_icons/16x/view_item_open_in_new_tab.svg create mode 100644 frontend/resources/flowy_icons/16x/view_item_rename.svg create mode 100644 frontend/resources/flowy_icons/16x/view_item_right_arrow.svg create mode 100644 frontend/resources/flowy_icons/16x/view_item_unexpand.svg create mode 100644 frontend/resources/flowy_icons/16x/workspace_drop_down_menu_hide.svg create mode 100644 frontend/resources/flowy_icons/16x/workspace_drop_down_menu_show.svg create mode 100644 frontend/resources/flowy_icons/16x/workspace_logout.svg create mode 100644 frontend/resources/flowy_icons/16x/workspace_selected.svg create mode 100644 frontend/resources/flowy_icons/16x/workspace_three_dots.svg diff --git a/frontend/appflowy_flutter/integration_test/cloud/workspace/collaborative_workspace_test.dart b/frontend/appflowy_flutter/integration_test/cloud/workspace/collaborative_workspace_test.dart index 11af75af1d17e..9886c2228e315 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/workspace/collaborative_workspace_test.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/workspace/collaborative_workspace_test.dart @@ -2,8 +2,6 @@ import 'dart:io'; -import 'package:flutter/material.dart'; - import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart'; @@ -12,14 +10,15 @@ import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'; import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/uuid.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:path/path.dart' as p; diff --git a/frontend/appflowy_flutter/integration_test/desktop/board/board_test_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/board/board_test_runner.dart index 932c266bda0ac..a786367fff2cc 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/board/board_test_runner.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/board/board_test_runner.dart @@ -1,10 +1,10 @@ import 'package:integration_test/integration_test.dart'; -import 'board_row_test.dart' as board_row_test; import 'board_add_row_test.dart' as board_add_row_test; import 'board_group_test.dart' as board_group_test; +import 'board_row_test.dart' as board_row_test; -void startTesting() { +void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); // Board integration tests diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_more_actions_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_more_actions_test.dart index 646a4eb565885..d4cc11d7f0518 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_more_actions_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_more_actions_test.dart @@ -19,12 +19,14 @@ void main() { // Duplicate await tester.openMoreViewActions(); await tester.duplicateByMoreViewActions(); + await tester.pumpAndSettle(); expect(pageFinder, findsNWidgets(2)); // Delete await tester.openMoreViewActions(); await tester.deleteByMoreViewActions(); + await tester.pumpAndSettle(); expect(pageFinder, findsNWidgets(1)); }); diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_cover_image_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_cover_image_test.dart index 2b42ef74513ad..f7a88d7bec54b 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_cover_image_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_cover_image_test.dart @@ -130,24 +130,24 @@ void main() { final searchEmojiTextField = find.byWidgetPredicate( (widget) => widget is TextField && - widget.decoration!.hintText == LocaleKeys.emoji_search.tr(), + widget.decoration!.hintText == LocaleKeys.search_label.tr(), ); await tester.enterText( searchEmojiTextField, - 'hand', + 'punch', ); // change skin tone await tester.editor.changeEmojiSkinTone(EmojiSkinTone.dark); // select an icon with skin tone - const hand = '👋🏿'; - await tester.tapEmoji(hand); - tester.expectToSeeDocumentIcon(hand); + const punch = '👊🏿'; + await tester.tapEmoji(punch); + tester.expectToSeeDocumentIcon(punch); tester.expectViewHasIcon( gettingStarted, ViewLayoutPB.Document, - hand, + punch, ); }); }); diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart index ff0df1c7dac1a..f2a1fae8ae9f2 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_folder.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -12,8 +12,8 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('sidebar expand test', () { - bool isExpanded({required FolderCategoryType type}) { - if (type == FolderCategoryType.private) { + bool isExpanded({required FolderSpaceType type}) { + if (type == FolderSpaceType.private) { return find .descendant( of: find.byType(PrivateSectionFolder), @@ -30,19 +30,19 @@ void main() { await tester.tapAnonymousSignInButton(); // first time is expanded - expect(isExpanded(type: FolderCategoryType.private), true); + expect(isExpanded(type: FolderSpaceType.private), true); // collapse the personal folder await tester.tapButton( find.byTooltip(LocaleKeys.sideBar_clickToHidePrivate.tr()), ); - expect(isExpanded(type: FolderCategoryType.private), false); + expect(isExpanded(type: FolderSpaceType.private), false); // expand the personal folder await tester.tapButton( find.byTooltip(LocaleKeys.sideBar_clickToHidePrivate.tr()), ); - expect(isExpanded(type: FolderCategoryType.private), true); + expect(isExpanded(type: FolderSpaceType.private), true); }); }); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart index 072764217c862..729ee62a3ed40 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart @@ -1,5 +1,5 @@ import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_favorite_folder.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; @@ -46,7 +46,7 @@ void main() { await tester.favoriteViewByName(names[1]); expect( tester.findFavoritePageName(names[1]), - findsNWidgets(2), + findsNWidgets(1), ); await tester.unfavoriteViewByName(gettingStarted); @@ -120,9 +120,9 @@ void main() { (widget) => widget is SingleInnerViewItem && widget.view.isFavorite && - widget.categoryType == FolderCategoryType.favorite, + widget.spaceType == FolderSpaceType.favorite, ), - findsNWidgets(6), + findsNWidgets(3), ); await tester.hoverOnPageName( @@ -135,7 +135,7 @@ void main() { expect( tester.findAllFavoritePages(), - findsNWidgets(3), + findsNWidgets(2), ); await tester.hoverOnPageName( @@ -168,7 +168,7 @@ void main() { widget.isSelected != null && widget.isSelected!(), ), - findsNWidgets(2), + findsNWidgets(1), ); }, ); diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart index 35bcf599ab735..3bc41d78c0d55 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart @@ -4,7 +4,7 @@ import 'sidebar_favorites_test.dart' as sidebar_favorite_test; import 'sidebar_icon_test.dart' as sidebar_icon_test; import 'sidebar_test.dart' as sidebar_test; -void startTesting() { +void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); // Sidebar integration tests diff --git a/frontend/appflowy_flutter/integration_test/desktop_runner_3.dart b/frontend/appflowy_flutter/integration_test/desktop_runner_3.dart index 72bf8a4faef65..f1025b8f1e92d 100644 --- a/frontend/appflowy_flutter/integration_test/desktop_runner_3.dart +++ b/frontend/appflowy_flutter/integration_test/desktop_runner_3.dart @@ -27,7 +27,7 @@ Future runIntegration3OnDesktop() async { settings_test_runner.main(); share_markdown_test.main(); import_files_test.main(); - sidebar_test_runner.startTesting(); - board_test_runner.startTesting(); + sidebar_test_runner.main(); + board_test_runner.main(); tabs_test.main(); } diff --git a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart index a2a6318c3dd83..8bc72369943a8 100644 --- a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart +++ b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart @@ -1,10 +1,5 @@ import 'dart:io'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; @@ -16,9 +11,9 @@ import 'package:appflowy/shared/feature_flags.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/sidebar/sidebar_workspace.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.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'; import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart'; @@ -34,6 +29,10 @@ import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'emoji.dart'; @@ -60,6 +59,7 @@ extension CommonOperations on WidgetTester { /// Tap the + button on the home page. Future tapAddViewButton({ String name = gettingStarted, + ViewLayoutPB layout = ViewLayoutPB.Document, }) async { await hoverOnPageName( name, @@ -279,7 +279,7 @@ extension CommonOperations on WidgetTester { bool openAfterCreated = true, }) async { // create a new page - await tapAddViewButton(name: parentName ?? gettingStarted); + await tapAddViewButton(name: parentName ?? gettingStarted, layout: layout); await tapButtonWithName(layout.menuName); final settingsOrFailure = await getIt().getWithFormat( KVKeys.showRenameDialogWhenCreatingNewFile, diff --git a/frontend/appflowy_flutter/integration_test/shared/editor_test_operations.dart b/frontend/appflowy_flutter/integration_test/shared/editor_test_operations.dart index 0bdcf06367bbe..4eff62321a4b7 100644 --- a/frontend/appflowy_flutter/integration_test/shared/editor_test_operations.dart +++ b/frontend/appflowy_flutter/integration_test/shared/editor_test_operations.dart @@ -81,15 +81,12 @@ class EditorOperations { /// Taps the 'Remove Icon' button in the cover toolbar and the icon popover Future tapRemoveIconButton({bool isInPicker = false}) async { - Finder button = - find.text(LocaleKeys.document_plugins_cover_removeIcon.tr()); - if (isInPicker) { - button = find.descendant( - of: find.byType(FlowyIconPicker), - matching: button, - ); - } - + final Finder button = !isInPicker + ? find.text(LocaleKeys.document_plugins_cover_removeIcon.tr()) + : find.descendant( + of: find.byType(FlowyIconPicker), + matching: find.text(LocaleKeys.button_remove.tr()), + ); await tester.tapButton(button); } diff --git a/frontend/appflowy_flutter/integration_test/shared/expectation.dart b/frontend/appflowy_flutter/integration_test/shared/expectation.dart index aeb3f04cb8873..c4b54a0fda5de 100644 --- a/frontend/appflowy_flutter/integration_test/shared/expectation.dart +++ b/frontend/appflowy_flutter/integration_test/shared/expectation.dart @@ -165,7 +165,7 @@ extension Expectation on WidgetTester { (widget) => widget is SingleInnerViewItem && widget.view.isFavorite && - widget.categoryType == FolderCategoryType.favorite && + widget.spaceType == FolderSpaceType.favorite && widget.view.name == name && widget.view.layout == layout, skipOffstage: false, @@ -175,7 +175,7 @@ extension Expectation on WidgetTester { (widget) => widget is SingleInnerViewItem && widget.view.isFavorite && - widget.categoryType == FolderCategoryType.favorite, + widget.spaceType == FolderSpaceType.favorite, ); Finder findPageName( diff --git a/frontend/appflowy_flutter/integration_test/shared/settings.dart b/frontend/appflowy_flutter/integration_test/shared/settings.dart index dd13bd088f750..3b25c32111341 100644 --- a/frontend/appflowy_flutter/integration_test/shared/settings.dart +++ b/frontend/appflowy_flutter/integration_test/shared/settings.dart @@ -1,7 +1,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart'; import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart'; @@ -12,7 +12,6 @@ import 'package:flowy_infra_ui/style_widget/text_field.dart'; import 'package:flutter_test/flutter_test.dart'; import '../desktop/board/board_hide_groups_test.dart'; - import 'base.dart'; import 'common_operations.dart'; diff --git a/frontend/appflowy_flutter/integration_test/shared/workspace.dart b/frontend/appflowy_flutter/integration_test/shared/workspace.dart index 5137944364957..4d20d88ce193f 100644 --- a/frontend/appflowy_flutter/integration_test/shared/workspace.dart +++ b/frontend/appflowy_flutter/integration_test/shared/workspace.dart @@ -1,14 +1,14 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/base/icon/icon_picker.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'base.dart'; +import 'util.dart'; extension AppFlowyWorkspace on WidgetTester { /// Open workspace menu @@ -36,12 +36,19 @@ extension AppFlowyWorkspace on WidgetTester { matching: find.byType(WorkspaceMoreActionList), ); expect(moreButton, findsOneWidget); - await tapButton(moreButton); - await tapButton(find.findTextInFlowyText(LocaleKeys.button_rename.tr())); - final input = find.byType(TextFormField); - expect(input, findsOneWidget); - await enterText(input, name); - await tapButton(find.text(LocaleKeys.button_ok.tr())); + await hoverOnWidget( + moreButton, + onHover: () async { + await tapButton(moreButton); + await tapButton( + find.findTextInFlowyText(LocaleKeys.button_rename.tr()), + ); + final input = find.byType(TextFormField); + expect(input, findsOneWidget); + await enterText(input, name); + await tapButton(find.text(LocaleKeys.button_ok.tr())); + }, + ); } Future changeWorkspaceIcon(String icon) async { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/type_option_menu_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/type_option_menu_item.dart index 04149c823814a..497f76935444d 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/type_option_menu_item.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/type_option_menu_item.dart @@ -1,5 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; class TypeOptionMenuItemValue { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart index b94242eceb232..6394ca9647193 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart @@ -38,6 +38,7 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget { case MobileViewBottomSheetBodyAction.removeFromFavorites: context.pop(); context.read().add(FavoriteEvent.toggle(view)); + break; case MobileViewBottomSheetBodyAction.undo: EditorNotification.undo().post(); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart index 718ac5c4e6c15..be815b6550c6a 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart @@ -1,6 +1,6 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart'; import 'package:appflowy/plugins/base/drag_handler.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; extension BottomSheetPaddingExtension on BuildContext { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart index c56d369676d4b..d683cf3507bc7 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart'; import 'package:appflowy/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder_header.dart'; @@ -7,6 +5,7 @@ import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MobileFavoriteFolder extends StatelessWidget { @@ -28,7 +27,7 @@ class MobileFavoriteFolder extends StatelessWidget { } return BlocProvider( - create: (context) => FolderBloc(type: FolderCategoryType.favorite) + create: (context) => FolderBloc(type: FolderSpaceType.favorite) ..add( const FolderEvent.initial(), ), @@ -55,9 +54,9 @@ class MobileFavoriteFolder extends StatelessWidget { ...views.map( (view) => MobileViewItem( key: ValueKey( - '${FolderCategoryType.favorite.name} ${view.id}', + '${FolderSpaceType.favorite.name} ${view.id}', ), - categoryType: FolderCategoryType.favorite, + spaceType: FolderSpaceType.favorite, isDraggable: false, isFirstChild: view.id == views.first.id, isFeedback: false, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart index 7631383faabea..6e695ea0e4f14 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart @@ -70,20 +70,20 @@ class MobileFolders extends StatelessWidget { ? [ MobileSectionFolder( title: LocaleKeys.sideBar_workspace.tr(), - categoryType: FolderCategoryType.public, + spaceType: FolderSpaceType.public, views: state.section.publicViews, ), const VSpace(8.0), MobileSectionFolder( title: LocaleKeys.sideBar_private.tr(), - categoryType: FolderCategoryType.private, + spaceType: FolderSpaceType.private, views: state.section.privateViews, ), ] : [ MobileSectionFolder( title: LocaleKeys.sideBar_personal.tr(), - categoryType: FolderCategoryType.public, + spaceType: FolderSpaceType.public, views: state.section.publicViews, ), ], diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart index 1759d32aaff5c..be47ef1b32f3b 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; @@ -15,6 +13,7 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sid import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -126,6 +125,7 @@ class _MobileWorkspace extends StatelessWidget { child: WorkspaceIcon( workspace: currentWorkspace, iconSize: 26, + fontSize: 16.0, enableEdit: false, onSelected: (result) => context.read().add( UserWorkspaceEvent.updateWorkspaceIcon( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart index c9ea1453c99f5..4d9d109d3f816 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart'; @@ -8,9 +6,11 @@ import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart'; import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MobileSectionFolder extends StatelessWidget { @@ -18,17 +18,17 @@ class MobileSectionFolder extends StatelessWidget { super.key, required this.title, required this.views, - required this.categoryType, + required this.spaceType, }); final String title; final List views; - final FolderCategoryType categoryType; + final FolderSpaceType spaceType; @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => FolderBloc(type: categoryType) + create: (context) => FolderBloc(type: spaceType) ..add( const FolderEvent.initial(), ), @@ -48,7 +48,7 @@ class MobileSectionFolder extends StatelessWidget { name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(), index: 0, - viewSection: categoryType.toViewSectionPB, + viewSection: spaceType.toViewSectionPB, ), ); context.read().add( @@ -64,13 +64,13 @@ class MobileSectionFolder extends StatelessWidget { ...views.map( (view) => MobileViewItem( key: ValueKey( - '${FolderCategoryType.private.name} ${view.id}', + '${FolderSpaceType.private.name} ${view.id}', ), - categoryType: categoryType, + spaceType: spaceType, isFirstChild: view.id == views.first.id, view: view, level: 0, - leftPadding: 16, + leftPadding: HomeSpaceViewSizes.leftPadding, isFeedback: false, onSelected: context.pushView, endActionPane: (context) { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart index 4745b248c3b49..496c5607a583f 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; @@ -9,6 +7,7 @@ import 'package:appflowy/workspace/presentation/settings/widgets/members/workspa import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; // Only works on mobile. @@ -106,6 +105,7 @@ class _WorkspaceMenuItem extends StatelessWidget { leftIcon: WorkspaceIcon( enableEdit: false, iconSize: 26, + fontSize: 16.0, workspace: workspace, onSelected: (result) => context.read().add( UserWorkspaceEvent.updateWorkspaceIcon( 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 34fd5176133f3..862ce794b6936 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 @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; @@ -12,6 +10,7 @@ import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_it import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; @@ -25,7 +24,7 @@ class MobileViewItem extends StatelessWidget { super.key, required this.view, this.parentView, - required this.categoryType, + required this.spaceType, required this.level, this.leftPadding = 10, required this.onSelected, @@ -39,7 +38,7 @@ class MobileViewItem extends StatelessWidget { final ViewPB view; final ViewPB? parentView; - final FolderCategoryType categoryType; + final FolderSpaceType spaceType; // indicate the level of the view item // used to calculate the left padding @@ -80,7 +79,7 @@ class MobileViewItem extends StatelessWidget { view: state.view, parentView: parentView, childViews: state.view.childViews, - categoryType: categoryType, + spaceType: spaceType, level: level, leftPadding: leftPadding, showActions: true, @@ -104,7 +103,7 @@ class InnerMobileViewItem extends StatelessWidget { required this.view, required this.parentView, required this.childViews, - required this.categoryType, + required this.spaceType, this.isDraggable = true, this.isExpanded = true, required this.level, @@ -120,7 +119,7 @@ class InnerMobileViewItem extends StatelessWidget { final ViewPB view; final ViewPB? parentView; final List childViews; - final FolderCategoryType categoryType; + final FolderSpaceType spaceType; final bool isDraggable; final bool isExpanded; @@ -144,7 +143,7 @@ class InnerMobileViewItem extends StatelessWidget { parentView: parentView, level: level, showActions: showActions, - categoryType: categoryType, + spaceType: spaceType, onSelected: onSelected, isExpanded: isExpanded, isDraggable: isDraggable, @@ -159,9 +158,9 @@ class InnerMobileViewItem extends StatelessWidget { if (childViews.isNotEmpty) { final children = childViews.map((childView) { return MobileViewItem( - key: ValueKey('${categoryType.name} ${childView.id}'), + key: ValueKey('${spaceType.name} ${childView.id}'), parentView: view, - categoryType: categoryType, + spaceType: spaceType, isFirstChild: childView.id == childViews.first.id, view: childView, level: level + 1, @@ -235,7 +234,7 @@ class InnerMobileViewItem extends StatelessWidget { return MobileViewItem( view: view, parentView: parentView, - categoryType: categoryType, + spaceType: spaceType, level: level, onSelected: onSelected, isDraggable: false, @@ -262,7 +261,7 @@ class SingleMobileInnerViewItem extends StatefulWidget { required this.level, required this.leftPadding, this.isDraggable = true, - required this.categoryType, + required this.spaceType, required this.showActions, required this.onSelected, required this.isFeedback, @@ -282,7 +281,7 @@ class SingleMobileInnerViewItem extends StatefulWidget { final bool isDraggable; final bool showActions; final ViewItemOnSelected onSelected; - final FolderCategoryType categoryType; + final FolderSpaceType spaceType; final ActionPaneBuilder? startActionPane; final ActionPaneBuilder? endActionPane; @@ -407,10 +406,9 @@ class _SingleMobileInnerViewItemState extends State { ViewEvent.createView( LocaleKeys.menuAppHeader_defaultNewPageName.tr(), layout, - section: - widget.categoryType != FolderCategoryType.favorite - ? widget.categoryType.toViewSectionPB - : null, + section: widget.spaceType != FolderSpaceType.favorite + ? widget.spaceType.toViewSectionPB + : null, ), ); }, 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 86c3b1e6252cd..d7a7b9ceb22a9 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart @@ -1,13 +1,10 @@ -import 'dart:io'; - import 'package:appflowy/plugins/base/emoji/emoji_picker_header.dart'; import 'package:appflowy/plugins/base/emoji/emoji_search_bar.dart'; import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; -import 'package:google_fonts/google_fonts.dart'; // use a global value to store the selected emoji to prevent reloading every time. EmojiData? kCachedEmojiData; @@ -28,7 +25,6 @@ class FlowyEmojiPicker extends StatefulWidget { class _FlowyEmojiPickerState extends State { EmojiData? emojiData; - List? fallbackFontFamily; @override void initState() { @@ -47,13 +43,6 @@ class _FlowyEmojiPickerState extends State { }, ); } - - if (Platform.isAndroid || Platform.isLinux) { - final notoColorEmoji = GoogleFonts.notoColorEmoji().fontFamily; - if (notoColorEmoji != null) { - fallbackFontFamily = [notoColorEmoji]; - } - } } @override @@ -83,16 +72,18 @@ class _FlowyEmojiPickerState extends State { ); }, itemBuilder: (context, emojiId, emoji, callback) { - return FlowyIconButton( - iconPadding: PlatformExtension.isWindows - ? const EdgeInsets.only(bottom: 2.0) - : const EdgeInsets.all(2), - icon: FlowyText( - emoji, - fontSize: 28.0, - fallbackFontFamily: fallbackFontFamily, + return SizedBox( + width: 36, + height: 36, + child: FlowyButton( + margin: EdgeInsets.zero, + radius: Corners.s8Border, + text: FlowyText.emoji( + emoji, + fontSize: 24.0, + ), + onTap: () => callback(emojiId, emoji), ), - onPressed: () => callback(emojiId, emoji), ); }, searchBarBuilder: (context, keyword, skinTone) { 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 index 9619f00d30dc6..9f05c80f09ada 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_header.dart @@ -16,9 +16,14 @@ class FlowyEmojiHeader extends StatelessWidget { if (PlatformExtension.isDesktopOrWeb) { return Container( height: 22, - padding: const EdgeInsets.symmetric(horizontal: 8.0), color: Theme.of(context).cardColor, - child: FlowyText.regular(category.id), + child: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: FlowyText.regular( + category.id, + color: Theme.of(context).hintColor, + ), + ), ); } else { return Column( 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 index 1b01e6aee8d05..c6cec89ecc8bc 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart @@ -42,7 +42,7 @@ class _FlowyEmojiSearchBarState extends State { Widget build(BuildContext context) { return Padding( padding: EdgeInsets.symmetric( - vertical: 8.0, + vertical: 12.0, horizontal: PlatformExtension.isDesktopOrWeb ? 0.0 : 8.0, ), child: Row( @@ -52,16 +52,15 @@ class _FlowyEmojiSearchBarState extends State { onKeywordChanged: widget.onKeywordChanged, ), ), - const HSpace(6.0), + const HSpace(8.0), _RandomEmojiButton( emojiData: widget.emojiData, onRandomEmojiSelected: widget.onRandomEmojiSelected, ), - const HSpace(6.0), + const HSpace(8.0), FlowyEmojiSkinToneSelector( onEmojiSkinToneChanged: widget.onSkinToneChanged, ), - const HSpace(6.0), ], ), ); @@ -79,20 +78,30 @@ class _RandomEmojiButton extends StatelessWidget { @override Widget build(BuildContext context) { - return FlowyTooltip( - message: LocaleKeys.emoji_random.tr(), - child: FlowyButton( - useIntrinsicWidth: true, - text: const Icon( - Icons.shuffle_rounded, + return Container( + width: 36, + height: 36, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: const BorderSide(color: Color(0x1E171717)), + borderRadius: BorderRadius.circular(8), + ), + ), + child: FlowyTooltip( + message: LocaleKeys.emoji_random.tr(), + child: FlowyButton( + useIntrinsicWidth: true, + text: const FlowySvg( + FlowySvgs.icon_shuffle_s, + ), + onTap: () { + final random = emojiData.random; + onRandomEmojiSelected( + random.$1, + random.$2, + ); + }, ), - onTap: () { - final random = emojiData.random; - onRandomEmojiSelected( - random.$1, - random.$2, - ); - }, ), ); } @@ -123,32 +132,35 @@ class _SearchTextFieldState extends State<_SearchTextField> { @override Widget build(BuildContext context) { - return ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: 32.0, - ), + return SizedBox( + height: 36.0, child: FlowyTextField( focusNode: focusNode, - hintText: LocaleKeys.emoji_search.tr(), + hintText: LocaleKeys.search_label.tr(), + hintStyle: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 14.0, + fontWeight: FontWeight.w400, + color: Theme.of(context).hintColor, + ), controller: controller, onChanged: widget.onKeywordChanged, prefixIcon: const Padding( padding: EdgeInsets.only( - left: 8.0, - right: 4.0, + left: 14.0, + right: 8.0, ), child: FlowySvg( FlowySvgs.search_s, ), ), prefixIconConstraints: const BoxConstraints( - maxHeight: 18.0, + maxHeight: 20.0, ), suffixIcon: Padding( padding: const EdgeInsets.all(4.0), child: FlowyButton( text: const FlowySvg( - FlowySvgs.close_lg, + FlowySvgs.m_app_bar_close_s, ), margin: EdgeInsets.zero, useIntrinsicWidth: true, 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 index e8da112660b47..3add90773de81 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart @@ -57,7 +57,7 @@ class _FlowyEmojiSkinToneSelectorState child: FlowyTooltip( message: LocaleKeys.emoji_selectSkinTone.tr(), child: _buildIconButton( - lastSelectedEmojiSkinTone?.icon ?? '✋', + lastSelectedEmojiSkinTone?.icon ?? '👋', () => controller.show(), ), ), @@ -65,19 +65,22 @@ class _FlowyEmojiSkinToneSelectorState } Widget _buildIconButton(String icon, VoidCallback onPressed) { - return FlowyIconButton( - key: emojiSkinToneKey(icon), - icon: Padding( - // add a left padding to align the emoji center - padding: const EdgeInsets.only( - left: 3.0, - ), - child: FlowyText( + return Container( + width: 36, + height: 36, + decoration: BoxDecoration( + border: Border.all(color: const Color(0x1E171717)), + borderRadius: BorderRadius.circular(8), + ), + child: FlowyButton( + key: emojiSkinToneKey(icon), + margin: EdgeInsets.zero, + text: FlowyText.emoji( icon, - fontSize: 22.0, + fontSize: 24.0, ), + onTap: onPressed, ), - onPressed: onPressed, ); } } @@ -86,17 +89,17 @@ extension EmojiSkinToneIcon on EmojiSkinTone { String get icon { switch (this) { case EmojiSkinTone.none: - return '✋'; + return '👋'; case EmojiSkinTone.light: - return '✋🏻'; + return '👋🏻'; case EmojiSkinTone.mediumLight: - return '✋🏼'; + return '👋🏼'; case EmojiSkinTone.medium: - return '✋🏽'; + return '👋🏽'; case EmojiSkinTone.mediumDark: - return '✋🏾'; + return '👋🏾'; case EmojiSkinTone.dark: - return '✋🏿'; + return '👋🏿'; } } } diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_text.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_text.dart index e9fed800d48df..5481c7676ad55 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_text.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_text.dart @@ -29,6 +29,7 @@ class EmojiText extends StatelessWidget { emoji, fontSize: fontSize, textAlign: textAlign, + strutStyle: const StrutStyle(forceStrutHeight: true), fallbackFontFamily: _cachedFallbackFontFamily, lineHeight: lineHeight, ); diff --git a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart index 1ecea2b7878f1..08e63251e0257 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart @@ -1,13 +1,10 @@ -import 'package:flutter/material.dart'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/icon.pbenum.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flutter/material.dart'; extension ToProto on FlowyIconType { ViewIconTypePB toProto() { @@ -54,57 +51,28 @@ class FlowyIconPicker extends StatelessWidget { @override Widget build(BuildContext context) { - // ONLY supports emoji picker for now - return DefaultTabController( - length: 1, + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ + const VSpace(8.0), Row( children: [ - _buildTabs(context), + FlowyText(LocaleKeys.newSettings_workplace_chooseAnIcon.tr()), const Spacer(), _RemoveIconButton( onTap: () => onSelected(EmojiPickerResult.none()), ), ], ), - const Divider(height: 2), + const VSpace(12.0), + const Divider(height: 0.5), Expanded( - child: TabBarView( - children: [ - FlowyEmojiPicker( - emojiPerLine: _getEmojiPerLine(context), - onEmojiSelected: (_, emoji) => - onSelected(EmojiPickerResult.emoji(emoji)), - ), - ], - ), - ), - ], - ), - ); - } - - Widget _buildTabs(BuildContext context) { - return Align( - alignment: Alignment.centerLeft, - child: TabBar( - indicatorSize: TabBarIndicatorSize.label, - isScrollable: true, - overlayColor: WidgetStatePropertyAll( - 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()), + child: FlowyEmojiPicker( + emojiPerLine: _getEmojiPerLine(context), + onEmojiSelected: (_, emoji) => + onSelected(EmojiPickerResult.emoji(emoji)), ), ), ], @@ -117,7 +85,7 @@ class FlowyIconPicker extends StatelessWidget { return 9; } final width = MediaQuery.of(context).size.width; - return width ~/ 46.0; // the size of the emoji + return width ~/ 40.0; // the size of the emoji } } @@ -129,14 +97,14 @@ class _RemoveIconButton extends StatelessWidget { @override Widget build(BuildContext context) { return SizedBox( - height: 28, + height: 24, child: FlowyButton( onTap: onTap, useIntrinsicWidth: true, - text: FlowyText.small( - LocaleKeys.document_plugins_cover_removeIcon.tr(), + text: FlowyText.regular( + LocaleKeys.button_remove.tr(), + color: Theme.of(context).hintColor, ), - leftIcon: const FlowySvg(FlowySvgs.delete_s), ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart index d8aa978e12518..787e08760b0c6 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart @@ -236,7 +236,8 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder { final String? initialRowId; @override - Widget get leftBarItem => ViewTitleBar(view: notifier.view); + Widget get leftBarItem => + ViewTitleBar(key: ValueKey(notifier.view.id), view: notifier.view); @override Widget tabBarItem(String pluginId) => ViewTabBarItem(view: notifier.view); @@ -278,7 +279,7 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder { ] : [], DatabaseShareButton(key: ValueKey(view.id), view: view), - const HSpace(4), + const HSpace(10), ViewFavoriteButton(view: view), const HSpace(4), MoreViewActions(view: view, isDocument: false), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/share_button.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/share_button.dart index d6c84e39fe481..7ede902085e73 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/share_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/share_button.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/share_bloc.dart'; import 'package:appflowy/startup/startup.dart'; @@ -13,6 +11,7 @@ import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:flowy_infra_ui/widget/rounded_button.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class DatabaseShareButton extends StatelessWidget { @@ -39,11 +38,7 @@ class DatabaseShareButton extends StatelessWidget { ); }, child: BlocBuilder( - builder: (context, state) => ConstrainedBox( - constraints: const BoxConstraints.expand( - height: 30, - width: 100, - ), + builder: (context, state) => IntrinsicWidth( child: DatabaseShareActionList(view: view), ), ), @@ -106,6 +101,8 @@ class DatabaseShareActionListState extends State { onPointerDown: (_) => controller.show(), child: RoundedTextButton( title: LocaleKeys.shareAction_buttonText.tr(), + padding: const EdgeInsets.symmetric(horizontal: 12.0), + fontSize: 14.0, textColor: Theme.of(context).colorScheme.onPrimary, onPressed: () {}, ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/document.dart b/frontend/appflowy_flutter/lib/plugins/document/document.dart index 13242bbe0a9e8..bd9e4891d297c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document.dart @@ -1,7 +1,5 @@ library document_plugin; -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; @@ -22,6 +20,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class DocumentPluginBuilder extends PluginBuilder { @@ -130,7 +129,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder } @override - Widget get leftBarItem => ViewTitleBar(view: view); + Widget get leftBarItem => ViewTitleBar(key: ValueKey(view.id), view: view); @override Widget tabBarItem(String pluginId) => ViewTabBarItem(view: notifier.view); @@ -162,7 +161,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder key: ValueKey('share_button_${view.id}'), view: view, ), - const HSpace(4), + const HSpace(10), ViewFavoriteButton( key: ValueKey('favorite_button_${view.id}'), view: view, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index 9642a4a8cc9c5..2d125ba647827 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -392,12 +392,6 @@ class _AppFlowyEditorPageState extends State { if (widget.editorState.document.isEmpty) { return (true, Selection.collapsed(Position(path: [0]))); } - final nodes = - widget.editorState.document.root.children.where((e) => e.delta != null); - final isAllEmpty = nodes.isNotEmpty && nodes.every((e) => e.delta!.isEmpty); - if (isAllEmpty) { - return (true, Selection.collapsed(Position(path: nodes.first.path))); - } return const (false, null); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart index cfff8d4938595..d5e99e13f8fd3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart @@ -141,7 +141,7 @@ enum OptionDepthType { class DividerOptionAction extends CustomActionCell { @override - Widget buildWithContext(BuildContext context) { + Widget buildWithContext(BuildContext context, PopoverController controller) { return const Divider( height: 1.0, thickness: 1.0, 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 df78f6261efa2..2a1101794a6ec 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 @@ -1,11 +1,10 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; class EmojiPickerButton extends StatelessWidget { @@ -19,6 +18,7 @@ class EmojiPickerButton extends StatelessWidget { this.offset, this.direction, this.title, + this.showBorder = true, }); final String emoji; @@ -30,6 +30,7 @@ class EmojiPickerButton extends StatelessWidget { final Offset? offset; final PopoverDirection? direction; final String? title; + final bool showBorder; @override Widget build(BuildContext context) { @@ -51,22 +52,28 @@ class EmojiPickerButton extends StatelessWidget { onExit: () {}, ), ), - child: emoji.isEmpty && defaultIcon != null - ? FlowyButton( - useIntrinsicWidth: true, - text: defaultIcon!, - onTap: popoverController.show, - ) - : FlowyTextButton( - emoji, - overflow: TextOverflow.visible, - fontSize: emojiSize, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(minWidth: 35.0), - fillColor: Colors.transparent, - mainAxisAlignment: MainAxisAlignment.center, - onPressed: popoverController.show, - ), + child: Container( + width: 30.0, + height: 30.0, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: showBorder + ? Border.all( + color: Theme.of(context).dividerColor, + ) + : null, + ), + child: FlowyButton( + margin: emoji.isEmpty && defaultIcon != null + ? EdgeInsets.zero + : const EdgeInsets.only(left: 2.0), + expandText: false, + text: emoji.isEmpty && defaultIcon != null + ? defaultIcon! + : FlowyText.emoji(emoji, fontSize: emojiSize), + onTap: popoverController.show, + ), + ), ); } return FlowyTextButton( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/find_and_replace/find_and_replace_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/find_and_replace/find_and_replace_menu.dart index ce756b9ffd538..7c84f2c31b34c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/find_and_replace/find_and_replace_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/find_and_replace/find_and_replace_menu.dart @@ -2,7 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/text_input.dart'; import 'package:flutter/material.dart'; 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 7a606f33ef930..3a22909744287 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 @@ -300,9 +300,10 @@ class _DocumentHeaderToolbarState extends State { : (CoverType.color, '0xffe8e0ff'), ), useIntrinsicWidth: true, - leftIcon: const FlowySvg(FlowySvgs.image_s), + leftIcon: const FlowySvg(FlowySvgs.add_cover_s), text: FlowyText.small( LocaleKeys.document_plugins_cover_addCover.tr(), + color: Theme.of(context).hintColor, ), ), ); @@ -311,28 +312,24 @@ class _DocumentHeaderToolbarState extends State { if (widget.hasIcon) { children.add( FlowyButton( - leftIconSize: const Size.square(18), onTap: () => widget.onIconOrCoverChanged(icon: ""), useIntrinsicWidth: true, - leftIcon: const Icon( - Icons.emoji_emotions_outlined, - size: 18, - ), + leftIcon: const FlowySvg(FlowySvgs.add_icon_s), + iconPadding: 4.0, text: FlowyText.small( LocaleKeys.document_plugins_cover_removeIcon.tr(), + color: Theme.of(context).hintColor, ), ), ); } else { Widget child = FlowyButton( - leftIconSize: const Size.square(18), useIntrinsicWidth: true, - leftIcon: const Icon( - Icons.emoji_emotions_outlined, - size: 18, - ), + leftIcon: const FlowySvg(FlowySvgs.add_icon_s), + iconPadding: 4.0, text: FlowyText.small( LocaleKeys.document_plugins_cover_addIcon.tr(), + color: Theme.of(context).hintColor, ), onTap: PlatformExtension.isDesktop ? null diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart index 36d90ac6fb0b2..84ab2f7380e59 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart @@ -148,7 +148,7 @@ class _UnsplashImages extends StatelessWidget { type: type, photo: photo, onTap: () => onSelectUnsplashImage( - photo.urls.regular.toString(), + photo.urls.full.toString(), ), ), ) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart index fa15bc10d5824..eb82f3d1faec3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart @@ -41,12 +41,9 @@ class DocumentShareButton extends StatelessWidget { ); }, child: BlocBuilder( - builder: (context, state) => ConstrainedBox( - constraints: const BoxConstraints.expand( - height: 30, - width: 100, - ), - child: ShareActionList(view: view), + builder: (context, state) => SizedBox( + height: 32.0, + child: IntrinsicWidth(child: ShareActionList(view: view)), ), ), ), @@ -120,7 +117,9 @@ class ShareActionListState extends State { onPointerDown: (_) => controller.show(), child: RoundedTextButton( title: LocaleKeys.shareAction_buttonText.tr(), + padding: const EdgeInsets.symmetric(horizontal: 12.0), onPressed: () {}, + fontSize: 14.0, textColor: Theme.of(context).colorScheme.onPrimary, ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_bloc.dart index 7b9e86e16b952..6ce62acae82d7 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_bloc.dart @@ -1,4 +1,5 @@ import 'package:appflowy/workspace/application/favorite/favorite_service.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; @@ -32,25 +33,23 @@ class FavoriteBloc extends Bloc { _listener.start( favoritesUpdated: _onFavoritesUpdated, ); - final result = await _service.readFavorites(); - emit( - result.fold( - (view) => state.copyWith( - views: view.items, - ), - (error) => state.copyWith( - views: [], - ), - ), - ); + add(const FavoriteEvent.fetchFavorites()); }, fetchFavorites: () async { final result = await _service.readFavorites(); emit( result.fold( - (view) => state.copyWith( - views: view.items, - ), + (favoriteViews) { + final views = favoriteViews.items.map((v) => v.item).toList(); + final pinnedViews = views.where((v) => v.isPinned).toList(); + final unpinnedViews = + views.where((v) => !v.isPinned).toList(); + return state.copyWith( + views: views, + pinnedViews: pinnedViews, + unpinnedViews: unpinnedViews, + ); + }, (error) => state.copyWith( views: [], ), @@ -58,11 +57,26 @@ class FavoriteBloc extends Bloc { ); }, toggle: (view) async { + if (view.isFavorite) { + await _service.unpinFavorite(view); + } else if (state.pinnedViews.length < 3) { + // pin the view if there are less than 3 pinned views + await _service.pinFavorite(view); + } + await _service.toggleFavorite( view.id, !view.isFavorite, ); }, + pin: (view) async { + await _service.pinFavorite(view); + add(const FavoriteEvent.fetchFavorites()); + }, + unpin: (view) async { + await _service.unpinFavorite(view); + add(const FavoriteEvent.fetchFavorites()); + }, ); }, ); @@ -84,12 +98,16 @@ class FavoriteEvent with _$FavoriteEvent { const factory FavoriteEvent.initial() = Initial; const factory FavoriteEvent.toggle(ViewPB view) = ToggleFavorite; const factory FavoriteEvent.fetchFavorites() = FetchFavorites; + const factory FavoriteEvent.pin(ViewPB view) = PinFavorite; + const factory FavoriteEvent.unpin(ViewPB view) = UnpinFavorite; } @freezed class FavoriteState with _$FavoriteState { const factory FavoriteState({ required List views, + @Default([]) List pinnedViews, + @Default([]) List unpinnedViews, }) = _FavoriteState; factory FavoriteState.initial() => const FavoriteState( diff --git a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_service.dart b/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_service.dart index d9343e2ee386d..71bb8423df2dc 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_service.dart @@ -1,10 +1,15 @@ +import 'dart:convert'; + +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; +import 'package:collection/collection.dart'; class FavoriteService { - Future> readFavorites() { + Future> readFavorites() { return FolderEventReadFavorites().send(); } @@ -15,4 +20,33 @@ class FavoriteService { final id = RepeatedViewIdPB.create()..items.add(viewId); return FolderEventToggleFavorite(id).send(); } + + Future> pinFavorite(ViewPB view) async { + return pinOrUnpinFavorite(view, true); + } + + Future> unpinFavorite(ViewPB view) async { + return pinOrUnpinFavorite(view, false); + } + + Future> pinOrUnpinFavorite( + ViewPB view, + bool isPinned, + ) async { + try { + final current = view.extra.isNotEmpty ? jsonDecode(view.extra) : {}; + final merged = mergeMaps( + current, + {ViewExtKeys.isPinnedKey: isPinned}, + ); + await ViewBackendService.updateView( + viewId: view.id, + extra: jsonEncode(merged), + ); + } catch (e) { + return FlowyResult.failure(FlowyError(msg: 'Failed to pin favorite: $e')); + } + + return FlowyResult.success(null); + } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/base_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/base_appearance.dart index a70073236af0f..0c6fe9aaed05b 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/base_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/base_appearance.dart @@ -33,7 +33,7 @@ abstract class BaseAppearance { double? letterSpacing, double? lineHeight, }) { - fontSize = fontSize ?? FontSizes.s12; + fontSize = fontSize ?? FontSizes.s14; fontWeight = fontWeight ?? (PlatformExtension.isDesktopOrWeb ? FontWeight.w500 : FontWeight.w400); letterSpacing = fontSize * (letterSpacing ?? 0.005); diff --git a/frontend/appflowy_flutter/lib/workspace/application/sidebar/folder/folder_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/sidebar/folder/folder_bloc.dart index 4dd934f60b152..c85f3bd0b0952 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/sidebar/folder/folder_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/sidebar/folder/folder_bloc.dart @@ -9,18 +9,18 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'folder_bloc.freezed.dart'; -enum FolderCategoryType { +enum FolderSpaceType { favorite, private, public; ViewSectionPB get toViewSectionPB { switch (this) { - case FolderCategoryType.private: + case FolderSpaceType.private: return ViewSectionPB.Private; - case FolderCategoryType.public: + case FolderSpaceType.public: return ViewSectionPB.Public; - case FolderCategoryType.favorite: + case FolderSpaceType.favorite: throw UnimplementedError(); } } @@ -28,7 +28,7 @@ enum FolderCategoryType { class FolderBloc extends Bloc { FolderBloc({ - required FolderCategoryType type, + required FolderSpaceType type, }) : super(FolderState.initial(type)) { on((event, emit) async { await event.map( @@ -84,12 +84,12 @@ class FolderEvent with _$FolderEvent { @freezed class FolderState with _$FolderState { const factory FolderState({ - required FolderCategoryType type, + required FolderSpaceType type, required bool isExpanded, }) = _FolderState; factory FolderState.initial( - FolderCategoryType type, + FolderSpaceType type, ) => FolderState( type: type, diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart index 1cfb89c8f937c..ebf1cf2b3f78d 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart @@ -75,7 +75,7 @@ class ViewBloc extends Bloc { ); final isExpanded = await _getViewIsExpanded(view); emit(state.copyWith(isExpanded: isExpanded)); - await _loadViewsWhenExpanded(emit, isExpanded); + await _loadChildViews(emit); }, setIsEditing: (e) { emit(state.copyWith(isEditing: e.isEditing)); @@ -222,6 +222,12 @@ class ViewBloc extends Bloc { viewIcon: value.icon ?? '', ); }, + collapseAllPages: (value) async { + for (final childView in view.childViews) { + await _setViewIsExpanded(childView, false); + } + add(const ViewEvent.setIsExpanded(false)); + }, ); }, ); @@ -270,6 +276,33 @@ class ViewBloc extends Bloc { ); } + Future _loadChildViews( + Emitter emit, + ) async { + final viewsOrFailed = + await ViewBackendService.getChildViews(viewId: state.view.id); + + viewsOrFailed.fold( + (childViews) { + state.view.freeze(); + final viewWithChildViews = state.view.rebuild((b) { + b.childViews.clear(); + b.childViews.addAll(childViews); + }); + emit( + state.copyWith( + view: viewWithChildViews, + ), + ); + }, + (error) => emit( + state.copyWith( + successOrFailure: FlowyResult.failure(error), + ), + ), + ); + } + Future _setViewIsExpanded(ViewPB view, bool isExpanded) async { final result = await getIt().get(KVKeys.expandedViews); final Map map; @@ -388,6 +421,7 @@ class ViewEvent with _$ViewEvent { bool isPublic, ) = UpdateViewVisibility; const factory ViewEvent.updateIcon(String? icon) = UpdateIcon; + const factory ViewEvent.collapseAllPages() = CollapseAllPages; } @freezed diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart index ca3bb62fbb52f..c3b17cb7426c7 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart @@ -32,6 +32,9 @@ class ViewExtKeys { static String coverKey = 'cover'; static String coverTypeKey = 'type'; static String coverValueKey = 'value'; + + // is pinned + static String isPinnedKey = 'is_pinned'; } extension ViewExtension on ViewPB { @@ -96,6 +99,16 @@ extension ViewExtension on ViewPB { FlowySvgData get iconData => layout.icon; + bool get isPinned { + try { + final ext = jsonDecode(extra); + final isPinned = ext[ViewExtKeys.isPinnedKey] ?? false; + return isPinned; + } catch (e) { + return false; + } + } + PageStyleCover? get cover { if (layout != ViewLayoutPB.Document) { return null; diff --git a/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bar_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bar_bloc.dart new file mode 100644 index 0000000000000..491ff367861e8 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bar_bloc.dart @@ -0,0 +1,48 @@ +import 'package:appflowy/workspace/application/view/prelude.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'view_title_bar_bloc.freezed.dart'; + +class ViewTitleBarBloc extends Bloc { + ViewTitleBarBloc({ + required this.view, + }) : super(ViewTitleBarState.initial()) { + on( + (event, emit) async { + await event.when( + initial: () async { + add(const ViewTitleBarEvent.reload()); + }, + reload: () async { + final List ancestors = + await ViewBackendService.getViewAncestors(view.id).fold( + (s) => s.items, + (f) => [], + ); + emit(state.copyWith(ancestors: ancestors)); + }, + ); + }, + ); + } + + final ViewPB view; +} + +@freezed +class ViewTitleBarEvent with _$ViewTitleBarEvent { + const factory ViewTitleBarEvent.initial() = Initial; + const factory ViewTitleBarEvent.reload() = Reload; +} + +@freezed +class ViewTitleBarState with _$ViewTitleBarState { + const factory ViewTitleBarState({ + required List ancestors, + }) = _ViewTitleBarState; + + factory ViewTitleBarState.initial() => const ViewTitleBarState(ancestors: []); +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bloc.dart new file mode 100644 index 0000000000000..737539763aa9f --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bloc.dart @@ -0,0 +1,73 @@ +import 'package:appflowy/workspace/application/view/prelude.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'view_title_bloc.freezed.dart'; + +class ViewTitleBloc extends Bloc { + ViewTitleBloc({ + required this.view, + }) : viewListener = ViewListener(viewId: view.id), + super(ViewTitleState.initial()) { + on( + (event, emit) async { + await event.when( + initial: () async { + emit( + state.copyWith( + name: view.name, + icon: view.icon.value, + ), + ); + + viewListener.start( + onViewUpdated: (view) { + add( + ViewTitleEvent.updateNameOrIcon( + view.name, + view.icon.value, + ), + ); + }, + ); + }, + updateNameOrIcon: (name, icon) async { + emit( + state.copyWith( + name: name, + icon: icon, + ), + ); + }, + ); + }, + ); + } + + final ViewPB view; + final ViewListener viewListener; + + @override + Future close() { + viewListener.stop(); + return super.close(); + } +} + +@freezed +class ViewTitleEvent with _$ViewTitleEvent { + const factory ViewTitleEvent.initial() = Initial; + const factory ViewTitleEvent.updateNameOrIcon(String name, String icon) = + UpdateNameOrIcon; +} + +@freezed +class ViewTitleState with _$ViewTitleState { + const factory ViewTitleState({ + required String name, + required String icon, + }) = _ViewTitleState; + + factory ViewTitleState.initial() => const ViewTitleState(name: '', icon: ''); +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_sizes.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_sizes.dart index fd15cf8d23831..bb7eb3600b3d1 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_sizes.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_sizes.dart @@ -1,12 +1,21 @@ class HomeSizes { static const double menuAddButtonHeight = 60; - static const double topBarHeight = 60; + static const double topBarHeight = 44; static const double editPanelTopBarHeight = 60; static const double editPanelWidth = 400; - static const double tabBarHeigth = 40; + static const double tabBarHeight = 40; static const double tabBarWidth = 200; + static const double workspaceSectionHeight = 32; + static const double searchSectionHeight = 30; + static const double newPageSectionHeight = 30; } class HomeInsets { - static const double topBarTitlePadding = 12; + static const double topBarTitleHorizontalPadding = 12; + static const double topBarTitleVerticalPadding = 12; +} + +class HomeSpaceViewSizes { + static const double leftPadding = 16.0; + static const double viewHeight = 30.0; } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart index 2a0eb6ec7d353..b54a170e93e00 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart @@ -1,6 +1,3 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; - import 'package:appflowy/core/frameless_window.dart'; import 'package:appflowy/plugins/blank/blank.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; @@ -13,6 +10,8 @@ import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; import 'package:time/time.dart'; @@ -275,14 +274,12 @@ class HomeTopBar extends StatelessWidget { return Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.onSecondaryContainer, - border: Border( - bottom: BorderSide(color: Theme.of(context).dividerColor), - ), ), - height: HomeSizes.topBarHeight, + height: HomeSizes.topBarHeight + HomeInsets.topBarTitleVerticalPadding, child: Padding( padding: const EdgeInsets.symmetric( - horizontal: HomeInsets.topBarTitlePadding, + horizontal: HomeInsets.topBarTitleHorizontalPadding, + vertical: HomeInsets.topBarTitleVerticalPadding, ), child: Row( children: [ diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart index ed5a667e91862..e979f102e3361 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart @@ -6,7 +6,7 @@ import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:flutter/material.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart new file mode 100644 index 0000000000000..16415e5e932f7 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart @@ -0,0 +1,183 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.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/presentation/home/home_sizes.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_menu.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.dart'; +import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/decoration.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class FavoriteFolder extends StatefulWidget { + const FavoriteFolder({ + super.key, + required this.views, + }); + + final List views; + + @override + State createState() => _FavoriteFolderState(); +} + +class _FavoriteFolderState extends State { + final isHovered = ValueNotifier(false); + + @override + void dispose() { + isHovered.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (widget.views.isEmpty) { + return const SizedBox.shrink(); + } + + return BlocProvider( + create: (context) => FolderBloc(type: FolderSpaceType.favorite) + ..add(const FolderEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + return MouseRegion( + onEnter: (_) => isHovered.value = true, + onExit: (_) => isHovered.value = false, + child: Column( + children: [ + FavoriteHeader( + onPressed: () => context + .read() + .add(const FolderEvent.expandOrUnExpand()), + ), + // pages + ..._buildViews(context, state, isHovered), + if (state.isExpanded) ...[ + // more button + const VSpace(2), + const FavoriteMoreButton(), + ], + ], + ), + ); + }, + ), + ); + } + + Iterable _buildViews( + BuildContext context, + FolderState state, + ValueNotifier isHovered, + ) { + if (!state.isExpanded) { + return []; + } + + return context.read().state.pinnedViews.map( + (view) => ViewItem( + key: ValueKey( + '${FolderSpaceType.favorite.name} ${view.id}', + ), + spaceType: FolderSpaceType.favorite, + isDraggable: false, + isFirstChild: view.id == widget.views.first.id, + isFeedback: false, + view: view, + leftPadding: HomeSpaceViewSizes.leftPadding, + leftIconBuilder: (_, __) => + const HSpace(HomeSpaceViewSizes.leftPadding), + level: 0, + isHovered: isHovered, + rightIconsBuilder: (context, view) => [ + FavoriteMoreActions(view: view), + const HSpace(8.0), + FavoritePinAction(view: view), + const HSpace(4.0), + ], + shouldRenderChildren: false, + onTertiarySelected: (_, view) => + context.read().openTab(view), + onSelected: (_, view) { + if (HardwareKeyboard.instance.isControlPressed) { + context.read().openTab(view); + } + + context.read().openPlugin(view); + }, + ), + ); + } +} + +class FavoriteHeader extends StatelessWidget { + const FavoriteHeader({ + super.key, + required this.onPressed, + }); + + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return FlowyButton( + onTap: onPressed, + margin: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 7.0), + leftIcon: const FlowySvg( + FlowySvgs.favorite_header_icon_s, + blendMode: null, + ), + iconPadding: 10.0, + text: FlowyText.regular(LocaleKeys.sideBar_favorites.tr()), + ); + } +} + +class FavoriteMoreButton extends StatelessWidget { + const FavoriteMoreButton({super.key}); + + @override + Widget build(BuildContext context) { + final favoriteBloc = context.watch(); + final unpinnedViews = favoriteBloc.state.unpinnedViews; + // only show the more button if there are unpinned views + if (unpinnedViews.isEmpty) { + return const SizedBox.shrink(); + } + + const minWidth = 260.0; + return AppFlowyPopover( + constraints: const BoxConstraints( + minWidth: minWidth, + ), + decoration: FlowyDecoration.decoration( + Theme.of(context).cardColor, + Theme.of(context).colorScheme.shadow, + borderRadius: 10.0, + ), + popupBuilder: (_) { + return BlocProvider.value( + value: favoriteBloc, + child: const FavoriteMenu(minWidth: minWidth), + ); + }, + margin: EdgeInsets.zero, + child: FlowyButton( + onTap: () {}, + margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 7.0), + leftIcon: const FlowySvg( + FlowySvgs.workspace_three_dots_s, + ), + text: FlowyText.regular(LocaleKeys.button_more.tr()), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu.dart new file mode 100644 index 0000000000000..4ec75da1d2eaf --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu.dart @@ -0,0 +1,193 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_menu_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.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'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +const double _kHorizontalPadding = 10.0; +const double _kVerticalPadding = 10.0; + +class FavoriteMenu extends StatelessWidget { + const FavoriteMenu({super.key, required this.minWidth}); + + final double minWidth; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only( + left: _kHorizontalPadding, + right: _kHorizontalPadding, + top: _kVerticalPadding, + bottom: _kVerticalPadding, + ), + child: BlocProvider( + create: (context) => + FavoriteMenuBloc()..add(const FavoriteMenuEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + if (state.views.isEmpty) { + return const SizedBox.shrink(); + } + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const VSpace(4), + _FavoriteSearchField( + width: minWidth - 2 * _kHorizontalPadding, + onSearch: (context, text) { + context + .read() + .add(FavoriteMenuEvent.search(text)); + }, + ), + const VSpace(12), + _buildViews(context, state), + ], + ); + }, + ), + ), + ); + } + + Widget _buildViews(BuildContext context, FavoriteMenuState state) { + return Container( + width: minWidth - 2 * _kHorizontalPadding, + constraints: const BoxConstraints( + maxHeight: 300, + ), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ..._buildGroups( + context, + state.todayViews, + LocaleKeys.sideBar_today.tr(), + ), + ..._buildGroups( + context, + state.thisWeekViews, + LocaleKeys.sideBar_thisWeek.tr(), + ), + ..._buildGroups( + context, + state.otherViews, + LocaleKeys.sideBar_others.tr(), + ), + ], + ), + ), + ); + } + + List _buildGroups( + BuildContext context, + List views, + String title, + ) { + return [ + if (views.isNotEmpty) ...[ + SizedBox( + height: 24, + child: FlowyText( + title, + fontSize: 12.0, + color: Theme.of(context).hintColor, + ), + ), + const VSpace(2), + _buildGroupedViews(context, views), + const VSpace(8), + const Divider(height: 1), + const VSpace(8), + ], + ]; + } + + Widget _buildGroupedViews(BuildContext context, List views) { + return Column( + mainAxisSize: MainAxisSize.min, + children: views + .map( + (e) => ViewItem( + key: ValueKey(e.id), + view: e, + spaceType: FolderSpaceType.favorite, + level: 0, + onSelected: (view, _) {}, + isFeedback: false, + isDraggable: false, + shouldRenderChildren: false, + leftIconBuilder: (_, __) => const HSpace(4.0), + rightIconsBuilder: (_, view) => [ + FavoriteMoreActions(view: view), + const HSpace(6.0), + FavoritePinAction(view: view), + const HSpace(4.0), + ], + ), + ) + .toList(), + ); + } +} + +class _FavoriteSearchField extends StatelessWidget { + const _FavoriteSearchField({ + required this.width, + required this.onSearch, + }); + + final double width; + final void Function(BuildContext context, String text) onSearch; + + @override + Widget build(BuildContext context) { + return Container( + height: 30, + width: width, + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: const BorderSide( + width: 1.20, + strokeAlign: BorderSide.strokeAlignOutside, + color: Color(0xFF00BCF0), + ), + borderRadius: BorderRadius.circular(8), + ), + ), + child: CupertinoSearchTextField( + onChanged: (text) => onSearch(context, text), + padding: EdgeInsets.zero, + placeholder: LocaleKeys.search_label.tr(), + prefixIcon: const FlowySvg(FlowySvgs.m_search_m), + prefixInsets: const EdgeInsets.only(left: 12.0, right: 8.0), + suffixIcon: const Icon(Icons.close), + suffixInsets: const EdgeInsets.only(right: 8.0), + itemSize: 16.0, + decoration: const BoxDecoration( + color: Colors.transparent, + ), + placeholderStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).hintColor, + fontWeight: FontWeight.w400, + ), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w400, + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu_bloc.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu_bloc.dart new file mode 100644 index 0000000000000..4ad963f50d5e0 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu_bloc.dart @@ -0,0 +1,124 @@ +import 'package:appflowy/workspace/application/favorite/favorite_service.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'favorite_menu_bloc.freezed.dart'; + +class FavoriteMenuBloc extends Bloc { + FavoriteMenuBloc() : super(FavoriteMenuState.initial()) { + on( + (event, emit) async { + await event.when( + initial: () async { + final favoriteViews = await _service.readFavorites(); + List views = []; + List todayViews = []; + List thisWeekViews = []; + List otherViews = []; + + favoriteViews.onSuccess((s) { + _source = s; + (views, todayViews, thisWeekViews, otherViews) = _getViews(s); + }); + + emit( + state.copyWith( + views: views, + queriedViews: views, + todayViews: todayViews, + thisWeekViews: thisWeekViews, + otherViews: otherViews, + ), + ); + }, + search: (query) async { + if (_source == null) { + return; + } + var (views, todayViews, thisWeekViews, otherViews) = + _getViews(_source!); + var queriedViews = views; + + if (query.isNotEmpty) { + queriedViews = _filter(views, query); + todayViews = _filter(state.todayViews, query); + thisWeekViews = _filter(state.thisWeekViews, query); + otherViews = _filter(state.otherViews, query); + } + + emit( + state.copyWith( + views: views, + queriedViews: queriedViews, + todayViews: todayViews, + thisWeekViews: thisWeekViews, + otherViews: otherViews, + ), + ); + }, + ); + }, + ); + } + + final FavoriteService _service = FavoriteService(); + RepeatedFavoriteViewPB? _source; + + List _filter(List views, String query) { + return views + .where( + (view) => view.name.toLowerCase().contains(query.toLowerCase()), + ) + .toList(); + } + + // all, today, last week, other + (List, List, List, List) _getViews( + RepeatedFavoriteViewPB source, + ) { + final List views = + source.items.map((v) => v.item).where((e) => !e.isPinned).toList(); + final List todayViews = []; + final List thisWeekViews = []; + final List otherViews = []; + for (final favoriteView in source.items) { + final view = favoriteView.item; + if (view.isPinned) { + continue; + } + final date = DateTime.fromMillisecondsSinceEpoch( + favoriteView.timestamp.toInt() * 1000, + ); + final diff = DateTime.now().difference(date).inDays; + if (diff == 0) { + todayViews.add(view); + } else if (diff < 7) { + thisWeekViews.add(view); + } else { + otherViews.add(view); + } + } + return (views, todayViews, thisWeekViews, otherViews); + } +} + +@freezed +class FavoriteMenuEvent with _$FavoriteMenuEvent { + const factory FavoriteMenuEvent.initial() = Initial; + const factory FavoriteMenuEvent.search(String query) = Search; +} + +@freezed +class FavoriteMenuState with _$FavoriteMenuState { + const factory FavoriteMenuState({ + @Default([]) List views, + @Default([]) List queriedViews, + @Default([]) List todayViews, + @Default([]) List thisWeekViews, + @Default([]) List otherViews, + }) = _FavoriteMenuState; + + factory FavoriteMenuState.initial() => const FavoriteMenuState(); +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart new file mode 100644 index 0000000000000..40c21676268c2 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart @@ -0,0 +1,67 @@ +import 'package:appflowy/generated/locale_keys.g.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/view_service.dart'; +import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart'; +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-folder/view.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class FavoriteMoreActions extends StatelessWidget { + const FavoriteMoreActions({super.key, required this.view}); + + final ViewPB view; + + @override + Widget build(BuildContext context) { + return FlowyTooltip( + message: LocaleKeys.menuAppHeader_moreButtonToolTip.tr(), + child: ViewMoreActionButton( + view: view, + spaceType: FolderSpaceType.favorite, + onEditing: (value) => + context.read().add(ViewEvent.setIsEditing(value)), + onAction: (action, _) { + switch (action) { + case ViewMoreActionType.favorite: + case ViewMoreActionType.unFavorite: + context.read().add(FavoriteEvent.toggle(view)); + PopoverContainer.maybeOf(context)?.closeAll(); + break; + case ViewMoreActionType.rename: + NavigatorTextFieldDialog( + title: LocaleKeys.disclosureAction_rename.tr(), + autoSelectAllText: true, + value: view.name, + maxLength: 256, + onConfirm: (newValue, _) { + // can not use bloc here because it has been disposed. + ViewBackendService.updateView( + viewId: view.id, + name: newValue, + ); + }, + ).show(context); + PopoverContainer.maybeOf(context)?.closeAll(); + break; + + case ViewMoreActionType.openInNewTab: + context.read().openTab(view); + break; + case ViewMoreActionType.delete: + case ViewMoreActionType.duplicate: + default: + throw UnsupportedError('$action is not supported'); + } + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.dart new file mode 100644 index 0000000000000..53a53e5ae94c7 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.dart @@ -0,0 +1,43 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/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/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class FavoritePinAction extends StatelessWidget { + const FavoritePinAction({super.key, required this.view}); + + final ViewPB view; + + @override + Widget build(BuildContext context) { + final tooltip = view.isPinned + ? LocaleKeys.favorite_removeFromSidebar.tr() + : LocaleKeys.favorite_addToSidebar.tr(); + final icon = FlowySvg( + view.isPinned + ? FlowySvgs.favorite_section_pin_s + : FlowySvgs.favorite_section_unpin_s, + ); + return FlowyTooltip( + message: tooltip, + child: FlowyIconButton( + width: 24, + icon: icon, + onPressed: () { + PopoverContainer.maybeOf(context)?.closeAll(); + + view.isPinned + ? context.read().add(FavoriteEvent.unpin(view)) + : context.read().add(FavoriteEvent.pin(view)); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_bloc.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_bloc.dart new file mode 100644 index 0000000000000..e4a335e34bac7 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_bloc.dart @@ -0,0 +1,59 @@ +import 'package:appflowy/workspace/application/favorite/favorite_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'favorite_pin_bloc.freezed.dart'; + +class FavoritePinBloc extends Bloc { + FavoritePinBloc() : super(FavoritePinState.initial()) { + on( + (event, emit) async { + await event.when( + initial: () async { + final List views = await _service + .readFavorites() + .fold((s) => s.items.map((v) => v.item).toList(), (f) => []); + emit(state.copyWith(views: views, queriedViews: views)); + }, + search: (query) async { + if (query.isEmpty) { + emit(state.copyWith(queriedViews: state.views)); + return; + } + + final queriedViews = state.views + .where( + (view) => + view.name.toLowerCase().contains(query.toLowerCase()), + ) + .toList(); + emit(state.copyWith(queriedViews: queriedViews)); + }, + ); + }, + ); + } + + final FavoriteService _service = FavoriteService(); +} + +@freezed +class FavoritePinEvent with _$FavoritePinEvent { + const factory FavoritePinEvent.initial() = Initial; + const factory FavoritePinEvent.search(String query) = Search; +} + +@freezed +class FavoritePinState with _$FavoritePinState { + const factory FavoritePinState({ + @Default([]) List views, + @Default([]) List queriedViews, + @Default([]) List> todayViews, + @Default([]) List> lastWeekViews, + @Default([]) List> otherViews, + }) = _FavoritePinState; + + factory FavoritePinState.initial() => const FavoritePinState(); +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_favorite_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_favorite_folder.dart deleted file mode 100644 index 364e12644c3b4..0000000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_favorite_folder.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; -import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class FavoriteFolder extends StatelessWidget { - const FavoriteFolder({ - super.key, - required this.views, - }); - - final List views; - - @override - Widget build(BuildContext context) { - if (views.isEmpty) { - return const SizedBox.shrink(); - } - - return BlocProvider( - create: (context) => FolderBloc(type: FolderCategoryType.favorite) - ..add( - const FolderEvent.initial(), - ), - child: BlocBuilder( - builder: (context, state) { - return Column( - children: [ - FavoriteHeader( - onPressed: () => context - .read() - .add(const FolderEvent.expandOrUnExpand()), - onAdded: () => context - .read() - .add(const FolderEvent.expandOrUnExpand(isExpanded: true)), - ), - if (state.isExpanded) - ...views.map( - (view) => ViewItem( - key: ValueKey( - '${FolderCategoryType.favorite.name} ${view.id}', - ), - categoryType: FolderCategoryType.favorite, - isDraggable: false, - isFirstChild: view.id == views.first.id, - isFeedback: false, - view: view, - level: 0, - onSelected: (view, _) { - if (HardwareKeyboard.instance.isControlPressed) { - context.read().openTab(view); - } - - context.read().openPlugin(view); - }, - onTertiarySelected: (view, _) => - context.read().openTab(view), - ), - ), - ], - ); - }, - ), - ); - } -} - -class FavoriteHeader extends StatefulWidget { - const FavoriteHeader({ - super.key, - required this.onPressed, - required this.onAdded, - }); - - final VoidCallback onPressed; - final VoidCallback onAdded; - - @override - State createState() => _FavoriteHeaderState(); -} - -class _FavoriteHeaderState extends State { - bool onHover = false; - - @override - Widget build(BuildContext context) { - const iconSize = 26.0; - return MouseRegion( - onEnter: (event) => setState(() => onHover = true), - onExit: (event) => setState(() => onHover = false), - child: Row( - children: [ - FlowyTextButton( - LocaleKeys.sideBar_favorites.tr(), - fontColor: AFThemeExtension.of(context).textColor, - fontHoverColor: Theme.of(context).colorScheme.onSurface, - tooltip: LocaleKeys.sideBar_clickToHideFavorites.tr(), - constraints: const BoxConstraints(maxHeight: iconSize), - padding: const EdgeInsets.all(4), - fillColor: Colors.transparent, - onPressed: widget.onPressed, - ), - ], - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart index 422003fdd9b3a..c5042c4de4cff 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart @@ -1,8 +1,7 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; class FolderHeader extends StatefulWidget { const FolderHeader({ @@ -25,42 +24,34 @@ class FolderHeader extends StatefulWidget { } class _FolderHeaderState extends State { - bool onHover = false; + final isHovered = ValueNotifier(false); + + @override + void dispose() { + isHovered.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { - const iconSize = 26.0; - const textPadding = 4.0; return MouseRegion( - onEnter: (event) => setState(() => onHover = true), - onExit: (event) => setState(() => onHover = false), - child: Row( - children: [ - FlowyTextButton( - widget.title, - tooltip: widget.expandButtonTooltip, - constraints: const BoxConstraints( - minHeight: iconSize + textPadding * 2, - ), - fontColor: AFThemeExtension.of(context).textColor, - fontHoverColor: Theme.of(context).colorScheme.onSurface, - padding: const EdgeInsets.all(textPadding), - fillColor: Colors.transparent, - onPressed: widget.onPressed, + onEnter: (_) => isHovered.value = true, + onExit: (_) => isHovered.value = false, + child: FlowyButton( + onTap: widget.onPressed, + margin: const EdgeInsets.symmetric(horizontal: 6.0), + rightIcon: ValueListenableBuilder( + valueListenable: isHovered, + builder: (context, onHover, child) => + Opacity(opacity: onHover ? 1 : 0, child: child), + child: FlowyIconButton( + tooltipText: widget.addButtonTooltip, + icon: const FlowySvg(FlowySvgs.view_item_add_s), + onPressed: widget.onAdded, ), - if (onHover) ...[ - const Spacer(), - FlowyIconButton( - tooltipText: widget.addButtonTooltip, - hoverColor: Theme.of(context).colorScheme.secondaryContainer, - iconPadding: const EdgeInsets.all(2), - height: iconSize, - width: iconSize, - icon: const FlowySvg(FlowySvgs.add_s), - onPressed: widget.onAdded, - ), - ], - ], + ), + iconPadding: 10.0, + text: FlowyText(widget.title), ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart index ecd7b5f9dea05..bd83e06934310 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart @@ -3,20 +3,22 @@ import 'package:appflowy/workspace/application/menu/sidebar_sections_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/user/user_workspace_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/rename_view_dialog.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class SectionFolder extends StatelessWidget { +class SectionFolder extends StatefulWidget { const SectionFolder({ super.key, required this.title, - required this.categoryType, + required this.spaceType, required this.views, this.isHoverEnabled = true, required this.expandButtonTooltip, @@ -24,101 +26,140 @@ class SectionFolder extends StatelessWidget { }); final String title; - final FolderCategoryType categoryType; + final FolderSpaceType spaceType; final List views; final bool isHoverEnabled; final String expandButtonTooltip; final String addButtonTooltip; + @override + State createState() => _SectionFolderState(); +} + +class _SectionFolderState extends State { + final ValueNotifier isHovered = ValueNotifier(false); + + @override + void dispose() { + isHovered.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => FolderBloc(type: categoryType) - ..add( - const FolderEvent.initial(), + return MouseRegion( + onEnter: (_) => isHovered.value = true, + onExit: (_) => isHovered.value = false, + child: BlocProvider( + create: (context) => FolderBloc(type: widget.spaceType) + ..add( + const FolderEvent.initial(), + ), + child: BlocBuilder( + builder: (context, state) { + return Column( + children: [ + _buildHeader(context), + // Pages + const VSpace(4.0), + ..._buildViews(context, state, isHovered), + // Add a placeholder if there are no views + _buildDraggablePlaceholder(context), + ], + ); + }, ), - child: BlocBuilder( - builder: (context, state) { - return Column( - children: [ - FolderHeader( - title: title, - expandButtonTooltip: expandButtonTooltip, - addButtonTooltip: addButtonTooltip, - onPressed: () => context - .read() - .add(const FolderEvent.expandOrUnExpand()), - onAdded: () { - createViewAndShowRenameDialogIfNeeded( - context, - LocaleKeys.newPageText.tr(), - (viewName, _) { - if (viewName.isNotEmpty) { - context.read().add( - SidebarSectionsEvent.createRootViewInSection( - name: viewName, - index: 0, - viewSection: categoryType.toViewSectionPB, - ), - ); + ), + ); + } - context.read().add( - const FolderEvent.expandOrUnExpand( - isExpanded: true, - ), - ); - } - }, + Widget _buildHeader(BuildContext context) { + return FolderHeader( + title: widget.title, + expandButtonTooltip: widget.expandButtonTooltip, + addButtonTooltip: widget.addButtonTooltip, + onPressed: () => + context.read().add(const FolderEvent.expandOrUnExpand()), + onAdded: () { + createViewAndShowRenameDialogIfNeeded( + context, + LocaleKeys.newPageText.tr(), + (viewName, _) { + if (viewName.isNotEmpty) { + context.read().add( + SidebarSectionsEvent.createRootViewInSection( + name: viewName, + index: 0, + viewSection: widget.spaceType.toViewSectionPB, + ), ); - }, - ), - if (state.isExpanded) - ...views.map( - (view) => ViewItem( - key: ValueKey( - '${categoryType.name} ${view.id}', + + context.read().add( + const FolderEvent.expandOrUnExpand( + isExpanded: true, ), - categoryType: categoryType, - isFirstChild: view.id == views.first.id, - view: view, - level: 0, - leftPadding: 16, - isFeedback: false, - onSelected: (view, viewContext) { - if (HardwareKeyboard.instance.isControlPressed) { - context.read().openTab(view); - } + ); + } + }, + ); + }, + ); + } + + Iterable _buildViews( + BuildContext context, + FolderState state, + ValueNotifier isHovered, + ) { + if (!state.isExpanded) { + return []; + } - context.read().openPlugin(view); - }, - onTertiarySelected: (view, viewContext) => - context.read().openTab(view), - isHoverEnabled: isHoverEnabled, - ), - ), - if (views.isEmpty) - ViewItem( - categoryType: categoryType, - view: ViewPB( - parentViewId: context - .read() - .state - .currentWorkspace - ?.workspaceId ?? - '', - ), - level: 0, - leftPadding: 16, - isFeedback: false, - onSelected: (_, __) {}, - onTertiarySelected: (_, __) {}, - isHoverEnabled: isHoverEnabled, - isPlaceholder: true, - ), - ], - ); + return widget.views.map( + (view) => ViewItem( + key: ValueKey('${widget.spaceType.name} ${view.id}'), + spaceType: widget.spaceType, + isFirstChild: view.id == widget.views.first.id, + view: view, + level: 0, + leftPadding: HomeSpaceViewSizes.leftPadding, + isFeedback: false, + isHovered: isHovered, + onSelected: (viewContext, view) { + if (HardwareKeyboard.instance.isControlPressed) { + context.read().openTab(view); + } + + context.read().openPlugin(view); }, + onTertiarySelected: (viewContext, view) => + context.read().openTab(view), + isHoverEnabled: widget.isHoverEnabled, + ), + ); + } + + Widget _buildDraggablePlaceholder(BuildContext context) { + if (widget.views.isNotEmpty) { + return const SizedBox.shrink(); + } + return ViewItem( + spaceType: widget.spaceType, + view: ViewPB( + parentViewId: context + .read() + .state + .currentWorkspace + ?.workspaceId ?? + '', ), + level: 0, + leftPadding: HomeSpaceViewSizes.leftPadding, + isFeedback: false, + onSelected: (_, __) {}, + onTertiarySelected: (_, __) {}, + isHoverEnabled: widget.isHoverEnabled, + isPlaceholder: true, ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart new file mode 100644 index 0000000000000..c10d14105a920 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart @@ -0,0 +1,70 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/startup/plugin/plugin.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; +import 'package:flutter/material.dart'; + +class SidebarFooter extends StatelessWidget { + const SidebarFooter({super.key}); + + @override + Widget build(BuildContext context) { + return const Row( + children: [ + Expanded(child: SidebarTrashButton()), + SizedBox( + height: 16, + child: VerticalDivider(width: 1, color: Color(0x141F2329)), + ), + Expanded(child: SidebarWidgetButton()), + ], + ); + } +} + +class SidebarTrashButton extends StatelessWidget { + const SidebarTrashButton({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: getIt().notifier, + builder: (context, value, child) { + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + getIt().latestOpenView = null; + getIt().add( + TabsEvent.openPlugin( + plugin: makePlugin(pluginType: PluginType.trash), + ), + ); + }, + child: const FlowySvg(FlowySvgs.sidebar_footer_trash_s), + ), + ); + }, + ); + } +} + +class SidebarWidgetButton extends StatelessWidget { + const SidebarWidgetButton({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () {}, + child: const FlowySvg(FlowySvgs.sidebar_footer_widget_s), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart similarity index 74% rename from frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart index 4ef548050775b..051386b7b2444 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart @@ -8,6 +8,7 @@ import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; @@ -62,22 +63,33 @@ class SidebarTopMenu extends StatelessWidget { children: [ TextSpan( text: '${LocaleKeys.sideBar_closeSidebar.tr()}\n', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Colors.white), ), TextSpan( text: Platform.isMacOS ? '⌘+.' : 'Ctrl+\\', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Theme.of(context).hintColor), ), ], ); - return FlowyTooltip( - richMessage: textSpan, - child: FlowyIconButton( - width: PlatformExtension.isWindows ? 30 : 28, - hoverColor: Colors.transparent, - onPressed: () => context - .read() - .add(const HomeSettingEvent.collapseMenu()), - iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4), - icon: const FlowySvg(FlowySvgs.hide_menu_m), + + return Padding( + padding: const EdgeInsets.only(top: 12.0), + child: FlowyTooltip( + richMessage: textSpan, + child: FlowyIconButton( + width: 24, + onPressed: () => context + .read() + .add(const HomeSettingEvent.collapseMenu()), + iconPadding: const EdgeInsets.all(2), + icon: const FlowySvg(FlowySvgs.hide_menu_s), + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_user.dart similarity index 93% rename from frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_user.dart index dc089e27a2034..8d7b0efd509d3 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_user.dart @@ -1,8 +1,6 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart'; import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart'; import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' @@ -10,6 +8,7 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; // keep this widget in case we need to roll back (lucas.xu) @@ -29,11 +28,14 @@ class SidebarUser extends StatelessWidget { child: BlocBuilder( builder: (context, state) => Row( children: [ + const HSpace(6), UserAvatar( iconUrl: state.userProfile.iconUrl, name: state.userProfile.name, + size: 16.0, + fontSize: 10.0, ), - const HSpace(8), + const HSpace(10), Expanded(child: _buildUserName(context, state)), UserSettingButton(userProfile: state.userProfile), const HSpace(4), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/rename_view_dialog.dart similarity index 100% rename from frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/rename_view_dialog.dart diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart similarity index 94% rename from frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart index dbbf3f0d0eff6..93bcccdf0606f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; @@ -7,11 +5,12 @@ import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_favorite_folder.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class SidebarFolder extends StatelessWidget { @@ -38,7 +37,7 @@ class SidebarFolder extends StatelessWidget { return const SizedBox.shrink(); } return Padding( - padding: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.only(top: 16.0, bottom: 10), child: FavoriteFolder(views: state.views), ); }, @@ -85,7 +84,7 @@ class PrivateSectionFolder extends SectionFolder { PrivateSectionFolder({super.key, required super.views}) : super( title: LocaleKeys.sideBar_private.tr(), - categoryType: FolderCategoryType.private, + spaceType: FolderSpaceType.private, expandButtonTooltip: LocaleKeys.sideBar_clickToHidePrivate.tr(), addButtonTooltip: LocaleKeys.sideBar_addAPageToPrivate.tr(), ); @@ -95,7 +94,7 @@ class PublicSectionFolder extends SectionFolder { PublicSectionFolder({super.key, required super.views}) : super( title: LocaleKeys.sideBar_workspace.tr(), - categoryType: FolderCategoryType.public, + spaceType: FolderSpaceType.public, expandButtonTooltip: LocaleKeys.sideBar_clickToHideWorkspace.tr(), addButtonTooltip: LocaleKeys.sideBar_addAPageToWorkspace.tr(), ); @@ -105,7 +104,7 @@ class PersonalSectionFolder extends SectionFolder { PersonalSectionFolder({super.key, required super.views}) : super( title: LocaleKeys.sideBar_personal.tr(), - categoryType: FolderCategoryType.public, + spaceType: FolderSpaceType.public, expandButtonTooltip: LocaleKeys.sideBar_clickToHidePersonal.tr(), addButtonTooltip: LocaleKeys.sideBar_addAPage.tr(), ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart new file mode 100644 index 0000000000000..fa8d7785cfb3e --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart @@ -0,0 +1,62 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/rename_view_dialog.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SidebarNewPageButton extends StatelessWidget { + const SidebarNewPageButton({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12), + height: HomeSizes.newPageSectionHeight, + child: FlowyButton( + onTap: () async => _createNewPage(context), + leftIcon: FlowySvg( + FlowySvgs.new_app_s, + color: Theme.of(context).colorScheme.primary, + ), + iconPadding: 10.0, + text: SizedBox( + height: 18.0, + child: FlowyText.regular( + LocaleKeys.newPageText.tr(), + ), + ), + ), + ); + } + + Future _createNewPage(BuildContext context) async { + return createViewAndShowRenameDialogIfNeeded( + context, + LocaleKeys.newPageText.tr(), + (viewName, _) { + if (viewName.isNotEmpty) { + // if the workspace is collaborative, create the view in the private section by default. + final section = + context.read().state.isCollabWorkspaceOn + ? ViewSectionPB.Private + : ViewSectionPB.Public; + context.read().add( + SidebarSectionsEvent.createRootViewInSection( + name: viewName, + viewSection: section, + index: 0, + ), + ); + } + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart similarity index 86% rename from frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_setting.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart index 0e2d020f677ee..8c6d9c5cf7ac8 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart @@ -1,10 +1,9 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy/workspace/presentation/home/hotkeys.dart'; import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart'; import 'package:appflowy_backend/log.dart'; @@ -12,7 +11,9 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show UserProfilePB; import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; @@ -47,15 +48,14 @@ class UserSettingButton extends StatelessWidget { @override Widget build(BuildContext context) { - return FlowyTooltip( - message: LocaleKeys.settings_menu_open.tr(), - child: IconButton( - onPressed: () => showSettingsDialog(context, userProfile), - icon: SizedBox.square( - dimension: 20, - child: FlowySvg( - FlowySvgs.settings_m, - color: Theme.of(context).colorScheme.tertiary, + return SizedBox.square( + dimension: HomeSizes.workspaceSectionHeight, + child: FlowyTooltip( + message: LocaleKeys.settings_menu_open.tr(), + child: FlowyButton( + onTap: () => showSettingsDialog(context, userProfile), + text: const FlowySvg( + FlowySvgs.settings_s, ), ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart index 9c20ac015bbd3..323ec1428cf54 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart @@ -1,7 +1,5 @@ import 'dart:async'; -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/feature_flags.dart'; @@ -17,12 +15,13 @@ import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_folder.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_trash.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_user.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/header/sidebar_user.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show UserProfilePB; @@ -31,6 +30,7 @@ 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:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; /// Home Sidebar is the left side bar of the home page. @@ -222,7 +222,8 @@ class _SidebarState extends State<_Sidebar> { // top menu const Padding(padding: menuHorizontalInset, child: SidebarTopMenu()), // user or workspace, setting - Padding( + Container( + height: HomeSizes.workspaceSectionHeight, padding: menuHorizontalInset, child: // if the workspaces are empty, show the user profile instead @@ -231,12 +232,15 @@ class _SidebarState extends State<_Sidebar> { : SidebarUser(userProfile: widget.userProfile), ), if (FeatureFlag.search.isOn) ...[ - const VSpace(8), - const Padding( + const VSpace(6), + Container( padding: menuHorizontalInset, - child: _SidebarSearchButton(), + height: HomeSizes.searchSectionHeight, + child: const _SidebarSearchButton(), ), ], + // new page button + const SidebarNewPageButton(), // scrollable document list Expanded( child: Padding( @@ -256,11 +260,14 @@ class _SidebarState extends State<_Sidebar> { // trash const Padding( padding: menuHorizontalInset, - child: SidebarTrashButton(), + child: Divider(height: 1.0, color: Color(0x141F2329)), + ), + const VSpace(14), + const Padding( + padding: menuHorizontalInset, + child: SidebarFooter(), ), const VSpace(10), - // new page button - const SidebarNewPageButton(), ], ), ); @@ -289,7 +296,8 @@ class _SidebarSearchButton extends StatelessWidget { return FlowyButton( onTap: () => CommandPalette.of(context).toggle(), leftIcon: const FlowySvg(FlowySvgs.search_s), - text: FlowyText(LocaleKeys.search_label.tr()), + iconPadding: 10.0, + text: FlowyText.regular(LocaleKeys.search_label.tr()), ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart deleted file mode 100644 index eac80118b4562..0000000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/extension.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class SidebarNewPageButton extends StatelessWidget { - const SidebarNewPageButton({ - super.key, - }); - - @override - Widget build(BuildContext context) { - final child = FlowyTextButton( - LocaleKeys.newPageText.tr(), - fillColor: Colors.transparent, - hoverColor: Colors.transparent, - fontColor: Theme.of(context).colorScheme.tertiary, - onPressed: () async => createViewAndShowRenameDialogIfNeeded( - context, - LocaleKeys.newPageText.tr(), - (viewName, _) { - if (viewName.isNotEmpty) { - // if the workspace is collaborative, create the view in the private section by default. - final section = - context.read().state.isCollabWorkspaceOn - ? ViewSectionPB.Private - : ViewSectionPB.Public; - context.read().add( - SidebarSectionsEvent.createRootViewInSection( - name: viewName, - viewSection: section, - ), - ); - } - }, - ), - heading: Container( - width: 16, - height: 16, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).colorScheme.surface, - ), - child: FlowySvg( - FlowySvgs.new_app_s, - color: Theme.of(context).colorScheme.primary, - ), - ), - padding: const EdgeInsets.all(0), - ); - - return SizedBox( - height: 60, - child: TopBorder( - color: Theme.of(context).dividerColor, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 18), - child: child, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_trash.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_trash.dart deleted file mode 100644 index b4a6eb344a129..0000000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_trash.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/startup/plugin/plugin.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; - -class SidebarTrashButton extends StatelessWidget { - const SidebarTrashButton({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: getIt().notifier, - builder: (context, value, child) { - return FlowyHover( - style: HoverStyle( - hoverColor: AFThemeExtension.of(context).greySelect, - ), - isSelected: () => getIt().latestOpenView == null, - child: SizedBox( - height: 26, - child: InkWell( - onTap: () { - getIt().latestOpenView = null; - getIt().add( - TabsEvent.openPlugin( - plugin: makePlugin(pluginType: PluginType.trash), - ), - ); - }, - child: _buildTextButton(context), - ), - ), - ); - }, - ); - } - - Widget _buildTextButton(BuildContext context) { - return Row( - children: [ - const HSpace(6), - const FlowySvg( - FlowySvgs.trash_m, - size: Size(16, 16), - ), - const HSpace(6), - FlowyText.medium( - LocaleKeys.trash_text.tr(), - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart index 39301799d62ba..53baa2599e40c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart @@ -16,6 +16,7 @@ enum WorkspaceMoreAction { rename, delete, leave, + divider, } class WorkspaceMoreActionList extends StatelessWidget { @@ -32,6 +33,7 @@ class WorkspaceMoreActionList extends StatelessWidget { final actions = []; if (myRole.isOwner) { actions.add(WorkspaceMoreAction.rename); + actions.add(WorkspaceMoreAction.divider); actions.add(WorkspaceMoreAction.delete); } else if (myRole.canLeave) { actions.add(WorkspaceMoreAction.leave); @@ -40,20 +42,23 @@ class WorkspaceMoreActionList extends StatelessWidget { return const SizedBox.shrink(); } return PopoverActionList<_WorkspaceMoreActionWrapper>( - direction: PopoverDirection.bottomWithCenterAligned, + direction: PopoverDirection.bottomWithLeftAligned, actions: actions .map((e) => _WorkspaceMoreActionWrapper(e, workspace)) .toList(), + constraints: const BoxConstraints(minWidth: 220), buildChild: (controller) { - return FlowyButton( - margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - useIntrinsicWidth: true, - text: const FlowySvg( - FlowySvgs.three_dots_vertical_s, + return SizedBox.square( + dimension: 24.0, + child: FlowyButton( + margin: const EdgeInsets.symmetric(horizontal: 4.0), + text: const FlowySvg( + FlowySvgs.workspace_three_dots_s, + ), + onTap: () { + controller.show(); + }, ), - onTap: () { - controller.show(); - }, ); }, onSelected: (action, controller) {}, @@ -68,20 +73,37 @@ class _WorkspaceMoreActionWrapper extends CustomActionCell { final UserWorkspacePB workspace; @override - Widget buildWithContext(BuildContext context) { + Widget buildWithContext(BuildContext context, PopoverController controller) { + if (inner == WorkspaceMoreAction.divider) { + return const Divider(); + } + + return _buildActionButton(context, controller); + } + + Widget _buildActionButton( + BuildContext context, + PopoverController controller, + ) { return FlowyButton( - text: FlowyText( + leftIcon: buildLeftIcon(context), + iconPadding: 10.0, + text: FlowyText.regular( name, - color: inner == WorkspaceMoreAction.delete + fontSize: 14.0, + color: [WorkspaceMoreAction.delete, WorkspaceMoreAction.leave] + .contains(inner) ? Theme.of(context).colorScheme.error : null, ), - margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0), + margin: const EdgeInsets.all(6), onTap: () async { PopoverContainer.of(context).closeAll(); final workspaceBloc = context.read(); switch (inner) { + case WorkspaceMoreAction.divider: + break; case WorkspaceMoreAction.delete: await NavigatorAlertDialog( title: LocaleKeys.workspace_deleteWorkspaceHintText.tr(), @@ -93,7 +115,7 @@ class _WorkspaceMoreActionWrapper extends CustomActionCell { ).show(context); case WorkspaceMoreAction.rename: await NavigatorTextFieldDialog( - title: LocaleKeys.workspace_create.tr(), + title: LocaleKeys.workspace_renameWorkspace.tr(), value: workspace.name, hintText: '', autoSelectAllText: true, @@ -132,6 +154,27 @@ class _WorkspaceMoreActionWrapper extends CustomActionCell { return LocaleKeys.button_rename.tr(); case WorkspaceMoreAction.leave: return LocaleKeys.workspace_leaveCurrentWorkspace.tr(); + case WorkspaceMoreAction.divider: + return ''; + } + } + + Widget buildLeftIcon(BuildContext context) { + switch (inner) { + case WorkspaceMoreAction.delete: + return FlowySvg( + FlowySvgs.delete_s, + color: Theme.of(context).colorScheme.error, + ); + case WorkspaceMoreAction.rename: + return const FlowySvg(FlowySvgs.view_item_rename_s); + case WorkspaceMoreAction.leave: + return FlowySvg( + FlowySvgs.logout_s, + color: Theme.of(context).colorScheme.error, + ); + case WorkspaceMoreAction.divider: + return const SizedBox.shrink(); } } } 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 101c12c3a6db2..ea0826e82a64f 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 @@ -1,6 +1,5 @@ import 'dart:math'; -import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/util/color_generator/color_generator.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; @@ -14,12 +13,14 @@ class WorkspaceIcon extends StatefulWidget { required this.workspace, required this.enableEdit, required this.iconSize, + required this.fontSize, required this.onSelected, }); final UserWorkspacePB workspace; final double iconSize; final bool enableEdit; + final double fontSize; final void Function(EmojiPickerResult) onSelected; @override @@ -35,10 +36,9 @@ class _WorkspaceIconState extends State { ? Container( width: widget.iconSize, alignment: Alignment.center, - child: EmojiText( - emoji: widget.workspace.icon, + child: FlowyText.emoji( + widget.workspace.icon, fontSize: widget.iconSize, - lineHeight: 1, ), ) : Container( @@ -47,13 +47,13 @@ class _WorkspaceIconState extends State { height: max(widget.iconSize, 26), decoration: BoxDecoration( color: ColorGenerator(widget.workspace.name).toColor(), - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(4), ), child: FlowyText( widget.workspace.name.isEmpty ? '' : widget.workspace.name.substring(0, 1), - fontSize: 16, + fontSize: widget.fontSize, color: Colors.black, ), ); @@ -63,7 +63,7 @@ class _WorkspaceIconState extends State { offset: const Offset(0, 8), controller: controller, direction: PopoverDirection.bottomWithLeftAligned, - constraints: BoxConstraints.loose(const Size(360, 380)), + constraints: BoxConstraints.loose(const Size(364, 356)), clickHandler: PopoverClickHandler.gestureDetector, popupBuilder: (_) => FlowyIconPicker( onSelected: (result) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart index efc4372ee82b4..0572ae107ce1c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart @@ -1,7 +1,7 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; @@ -13,6 +13,7 @@ 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/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @visibleForTesting @@ -38,7 +39,7 @@ class WorkspacesMenu extends StatelessWidget { children: [ // user email Padding( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), + padding: const EdgeInsets.symmetric(horizontal: 4.0), child: Row( children: [ Expanded( @@ -50,18 +51,16 @@ class WorkspacesMenu extends StatelessWidget { ), ), const HSpace(4.0), - FlowyButton( - key: createWorkspaceButtonKey, - useIntrinsicWidth: true, - text: const FlowySvg(FlowySvgs.add_m), - onTap: () { - _showCreateWorkspaceDialog(context); - PopoverContainer.of(context).closeAll(); - }, - ), + const _WorkspaceMoreButton(), + const HSpace(8.0), ], ), ), + const Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: Divider(height: 1.0), + ), + // workspace list for (final workspace in workspaces) ...[ WorkspaceMenuItem( key: ValueKey(workspace.workspaceId), @@ -69,8 +68,11 @@ class WorkspacesMenu extends StatelessWidget { userProfile: userProfile, isSelected: workspace.workspaceId == currentWorkspace.workspaceId, ), - const VSpace(4.0), + const VSpace(6.0), ], + // add new workspace + const _CreateWorkspaceButton(), + const VSpace(6.0), ], ); } @@ -86,20 +88,9 @@ class WorkspacesMenu extends StatelessWidget { return LocaleKeys.defaultUsername.tr(); } - - Future _showCreateWorkspaceDialog(BuildContext context) async { - if (context.mounted) { - final workspaceBloc = context.read(); - await CreateWorkspaceDialog( - onConfirm: (name) { - workspaceBloc.add(UserWorkspaceEvent.createWorkspace(name)); - }, - ).show(context); - } - } } -class WorkspaceMenuItem extends StatelessWidget { +class WorkspaceMenuItem extends StatefulWidget { const WorkspaceMenuItem({ super.key, required this.workspace, @@ -111,32 +102,50 @@ class WorkspaceMenuItem extends StatelessWidget { final UserWorkspacePB workspace; final bool isSelected; + @override + State createState() => _WorkspaceMenuItemState(); +} + +class _WorkspaceMenuItemState extends State { + final ValueNotifier isHovered = ValueNotifier(false); + + @override + void dispose() { + isHovered.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => - WorkspaceMemberBloc(userProfile: userProfile, workspace: workspace) - ..add(const WorkspaceMemberEvent.initial()), + create: (_) => WorkspaceMemberBloc( + userProfile: widget.userProfile, + workspace: widget.workspace, + )..add(const WorkspaceMemberEvent.initial()), child: BlocBuilder( builder: (context, state) { // settings right icon inside the flowy button will // cause the popover dismiss intermediately when click the right icon. // so using the stack to put the right icon on the flowy button. return SizedBox( - height: 52, - child: Stack( - alignment: Alignment.center, - children: [ - _WorkspaceInfo( - isSelected: isSelected, - workspace: workspace, - ), - Positioned(left: 8, child: _buildLeftIcon(context)), - Positioned( - right: 12.0, - child: Align(child: _buildRightIcon(context)), - ), - ], + height: 40, + child: MouseRegion( + onEnter: (_) => isHovered.value = true, + onExit: (_) => isHovered.value = false, + child: Stack( + alignment: Alignment.center, + children: [ + _WorkspaceInfo( + isSelected: widget.isSelected, + workspace: widget.workspace, + ), + Positioned(left: 4, child: _buildLeftIcon(context)), + Positioned( + right: 4.0, + child: Align(child: _buildRightIcon(context, isHovered)), + ), + ], + ), ), ); }, @@ -145,17 +154,26 @@ class WorkspaceMenuItem extends StatelessWidget { } Widget _buildLeftIcon(BuildContext context) { - return SizedBox.square( - dimension: 32, + return Container( + width: 32.0, + height: 32.0, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: const Color(0x01717171).withOpacity(0.12), + width: 0.8, + ), + ), child: FlowyTooltip( message: LocaleKeys.document_plugins_cover_changeIcon.tr(), child: WorkspaceIcon( - workspace: workspace, - iconSize: 26, + workspace: widget.workspace, + iconSize: 22, + fontSize: 16, enableEdit: true, onSelected: (result) => context.read().add( UserWorkspaceEvent.updateWorkspaceIcon( - workspace.workspaceId, + widget.workspace.workspaceId, result.emoji, ), ), @@ -164,19 +182,39 @@ class WorkspaceMenuItem extends StatelessWidget { ); } - Widget _buildRightIcon(BuildContext context) { + Widget _buildRightIcon(BuildContext context, ValueNotifier isHovered) { // only the owner can update or delete workspace. - // only show the more action button when the workspace is selected. - if (!isSelected || context.read().state.isLoading) { + if (context.read().state.isLoading) { return const SizedBox.shrink(); } return Row( children: [ - WorkspaceMoreActionList(workspace: workspace), - const FlowySvg( - FlowySvgs.blue_check_s, + ValueListenableBuilder( + valueListenable: isHovered, + builder: (context, value, child) { + return Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Opacity( + opacity: value ? 1.0 : 0.0, + child: child, + ), + ); + }, + child: WorkspaceMoreActionList(workspace: widget.workspace), ), + const HSpace(8.0), + if (widget.isSelected) ...[ + const Padding( + padding: EdgeInsets.all(5.0), + child: FlowySvg( + FlowySvgs.workspace_selected_s, + blendMode: null, + size: Size.square(14.0), + ), + ), + const HSpace(8.0), + ], ], ); } @@ -199,10 +237,9 @@ class _WorkspaceInfo extends StatelessWidget { return FlowyButton( onTap: () => _openWorkspace(context), iconPadding: 10.0, - margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), leftIconSize: const Size.square(32), leftIcon: const SizedBox.square(dimension: 32), - rightIcon: const HSpace(42.0), + rightIcon: const HSpace(32.0), text: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -213,8 +250,9 @@ class _WorkspaceInfo extends StatelessWidget { overflow: TextOverflow.ellipsis, withTooltip: true, ), + const VSpace(2.0), // workspace members count - FlowyText( + FlowyText.regular( state.isLoading ? '' : LocaleKeys.settings_appearance_members_membersCount @@ -263,3 +301,90 @@ class CreateWorkspaceDialog extends StatelessWidget { ); } } + +class _CreateWorkspaceButton extends StatelessWidget { + const _CreateWorkspaceButton(); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 40, + child: FlowyButton( + key: createWorkspaceButtonKey, + onTap: () { + _showCreateWorkspaceDialog(context); + PopoverContainer.of(context).closeAll(); + }, + margin: const EdgeInsets.symmetric(horizontal: 4.0), + text: Row( + children: [ + _buildLeftIcon(context), + const HSpace(10.0), + FlowyText.regular(LocaleKeys.workspace_create.tr()), + ], + ), + ), + ); + } + + Widget _buildLeftIcon(BuildContext context) { + return Container( + width: 32.0, + height: 32.0, + padding: const EdgeInsets.all(7.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: const Color(0x01717171).withOpacity(0.12), + width: 0.8, + ), + ), + child: const FlowySvg(FlowySvgs.add_workspace_s), + ); + } + + Future _showCreateWorkspaceDialog(BuildContext context) async { + if (context.mounted) { + final workspaceBloc = context.read(); + await CreateWorkspaceDialog( + onConfirm: (name) { + workspaceBloc.add(UserWorkspaceEvent.createWorkspace(name)); + }, + ).show(context); + } + } +} + +class _WorkspaceMoreButton extends StatelessWidget { + const _WorkspaceMoreButton(); + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + direction: PopoverDirection.bottomWithLeftAligned, + offset: const Offset(0, 6), + popupBuilder: (_) => FlowyButton( + margin: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 7.0), + leftIcon: const FlowySvg(FlowySvgs.workspace_logout_s), + iconPadding: 10.0, + text: FlowyText.regular(LocaleKeys.button_logout.tr()), + onTap: () async { + await getIt().signOut(); + await runAppFlowy(); + }, + ), + child: SizedBox.square( + dimension: 24.0, + child: FlowyButton( + useIntrinsicWidth: true, + margin: EdgeInsets.zero, + text: const FlowySvg( + FlowySvgs.workspace_three_dots_s, + size: Size.square(16.0), + ), + onTap: () {}, + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart similarity index 79% rename from frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart index 141d7d7f4c740..bb4d18ab46510 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart @@ -1,10 +1,8 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; @@ -16,6 +14,7 @@ import 'package:appflowy_backend/protobuf/flowy-user/user_profile.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:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class SidebarWorkspace extends StatefulWidget { @@ -50,8 +49,9 @@ class _SidebarWorkspaceState extends State { ), ), UserSettingButton(userProfile: widget.userProfile), - const HSpace(4), + const HSpace(8), const NotificationButton(), + const HSpace(4), ], ); }, @@ -144,7 +144,7 @@ class _SidebarWorkspaceState extends State { } } -class SidebarSwitchWorkspaceButton extends StatelessWidget { +class SidebarSwitchWorkspaceButton extends StatefulWidget { const SidebarSwitchWorkspaceButton({ super.key, required this.userProfile, @@ -154,16 +154,31 @@ class SidebarSwitchWorkspaceButton extends StatelessWidget { final UserWorkspacePB currentWorkspace; final UserProfilePB userProfile; + @override + State createState() => + _SidebarSwitchWorkspaceButtonState(); +} + +class _SidebarSwitchWorkspaceButtonState + extends State { + final ValueNotifier _isWorkSpaceMenuExpanded = ValueNotifier(false); + @override Widget build(BuildContext context) { return AppFlowyPopover( - direction: PopoverDirection.bottomWithCenterAligned, - offset: const Offset(0, 10), - constraints: const BoxConstraints(maxWidth: 260, maxHeight: 600), - onOpen: () => context - .read() - .add(const UserWorkspaceEvent.fetchWorkspaces()), - onClose: () => Log.info('close workspace menu'), + direction: PopoverDirection.bottomWithLeftAligned, + offset: const Offset(0, 5), + constraints: const BoxConstraints(maxWidth: 300, maxHeight: 600), + onOpen: () { + _isWorkSpaceMenuExpanded.value = true; + context + .read() + .add(const UserWorkspaceEvent.fetchWorkspaces()); + }, + onClose: () { + _isWorkSpaceMenuExpanded.value = false; + Log.info('close workspace menu'); + }, popupBuilder: (_) { return BlocProvider.value( value: context.read(), @@ -176,7 +191,7 @@ class SidebarSwitchWorkspaceButton extends StatelessWidget { } Log.info('open workspace menu'); return WorkspacesMenu( - userProfile: userProfile, + userProfile: widget.userProfile, currentWorkspace: currentWorkspace, workspaces: workspaces, ); @@ -185,33 +200,42 @@ class SidebarSwitchWorkspaceButton extends StatelessWidget { ); }, child: FlowyButton( - margin: const EdgeInsets.symmetric(vertical: 8), + margin: EdgeInsets.zero, text: Row( children: [ - const HSpace(2.0), - SizedBox.square( - dimension: 30.0, + const HSpace(6.0), + SizedBox( + width: 16.0, child: WorkspaceIcon( - workspace: currentWorkspace, - iconSize: 20, + workspace: widget.currentWorkspace, + iconSize: 16, + fontSize: 10, enableEdit: false, onSelected: (result) => context.read().add( UserWorkspaceEvent.updateWorkspaceIcon( - currentWorkspace.workspaceId, + widget.currentWorkspace.workspaceId, result.emoji, ), ), ), ), - const HSpace(6), - Expanded( + const HSpace(10), + Flexible( child: FlowyText.medium( - currentWorkspace.name, + widget.currentWorkspace.name, overflow: TextOverflow.ellipsis, withTooltip: true, ), ), - const FlowySvg(FlowySvgs.drop_menu_show_m), + const HSpace(4), + ValueListenableBuilder( + valueListenable: _isWorkSpaceMenuExpanded, + builder: (context, value, _) => FlowySvg( + value + ? FlowySvgs.workspace_drop_down_menu_hide_s + : FlowySvgs.workspace_drop_down_menu_show_s, + ), + ), ], ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart index 658d60bfe7b7d..881d926df322e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart @@ -15,6 +15,8 @@ enum DraggableHoverPosition { bottom, } +const kDraggableViewItemDividerHeight = 2.0; + class DraggableViewItem extends StatefulWidget { const DraggableViewItem({ super.key, @@ -45,8 +47,7 @@ class DraggableViewItem extends StatefulWidget { class _DraggableViewItemState extends State { DraggableHoverPosition position = DraggableHoverPosition.none; - - final _dividerHeight = 2.0; + final hoverColor = const Color(0xFF00C8FF); @override Widget build(BuildContext context) { @@ -100,29 +101,26 @@ class _DraggableViewItemState extends State { // only show the top border when the draggable item is the first child if (widget.isFirstChild) Divider( - height: _dividerHeight, - thickness: _dividerHeight, + height: kDraggableViewItemDividerHeight, + thickness: kDraggableViewItemDividerHeight, color: position == DraggableHoverPosition.top - ? widget.topHighlightColor ?? - Theme.of(context).colorScheme.secondary + ? widget.topHighlightColor ?? hoverColor : Colors.transparent, ), DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(6.0), color: position == DraggableHoverPosition.center - ? widget.centerHighlightColor ?? - Theme.of(context).colorScheme.secondary.withOpacity(0.5) + ? widget.centerHighlightColor ?? hoverColor.withOpacity(0.5) : Colors.transparent, ), child: widget.child, ), Divider( - height: _dividerHeight, - thickness: _dividerHeight, + height: kDraggableViewItemDividerHeight, + thickness: kDraggableViewItemDividerHeight, color: position == DraggableHoverPosition.bottom - ? widget.bottomHighlightColor ?? - Theme.of(context).colorScheme.secondary + ? widget.bottomHighlightColor ?? hoverColor : Colors.transparent, ), ], @@ -137,10 +135,10 @@ class _DraggableViewItemState extends State { top: 0, left: 0, right: 0, - height: _dividerHeight, + height: kDraggableViewItemDividerHeight, child: Divider( - height: _dividerHeight, - thickness: _dividerHeight, + height: kDraggableViewItemDividerHeight, + thickness: kDraggableViewItemDividerHeight, color: position == DraggableHoverPosition.top ? widget.topHighlightColor ?? Theme.of(context).colorScheme.secondary @@ -161,10 +159,10 @@ class _DraggableViewItemState extends State { bottom: 0, left: 0, right: 0, - height: _dividerHeight, + height: kDraggableViewItemDividerHeight, child: Divider( - height: _dividerHeight, - thickness: _dividerHeight, + height: kDraggableViewItemDividerHeight, + thickness: kDraggableViewItemDividerHeight, color: position == DraggableHoverPosition.bottom ? widget.bottomHighlightColor ?? Theme.of(context).colorScheme.secondary diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_action_type.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_action_type.dart index 9fda07d7d277d..5b04e5050103e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_action_type.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_action_type.dart @@ -10,8 +10,13 @@ enum ViewMoreActionType { duplicate, copyLink, // not supported yet. rename, - moveTo, // not supported yet. + moveTo, openInNewTab, + changeIcon, + collapseAllPages, // including sub pages + divider, + lastModified, + created, } extension ViewMoreActionTypeExtension on ViewMoreActionType { @@ -33,27 +38,63 @@ extension ViewMoreActionTypeExtension on ViewMoreActionType { return LocaleKeys.disclosureAction_moveTo.tr(); case ViewMoreActionType.openInNewTab: return LocaleKeys.disclosureAction_openNewTab.tr(); + case ViewMoreActionType.changeIcon: + return LocaleKeys.disclosureAction_changeIcon.tr(); + case ViewMoreActionType.collapseAllPages: + return LocaleKeys.disclosureAction_collapseAllPages.tr(); + case ViewMoreActionType.divider: + case ViewMoreActionType.lastModified: + case ViewMoreActionType.created: + return ''; } } - Widget icon(Color iconColor) { + Widget get leftIcon { switch (this) { case ViewMoreActionType.delete: - return const FlowySvg(FlowySvgs.delete_s); + return const FlowySvg(FlowySvgs.trash_s, blendMode: null); case ViewMoreActionType.favorite: - return const FlowySvg(FlowySvgs.unfavorite_s); - case ViewMoreActionType.unFavorite: return const FlowySvg(FlowySvgs.favorite_s); + case ViewMoreActionType.unFavorite: + return const FlowySvg(FlowySvgs.unfavorite_s); case ViewMoreActionType.duplicate: - return const FlowySvg(FlowySvgs.copy_s); + return const FlowySvg(FlowySvgs.duplicate_s); case ViewMoreActionType.copyLink: return const Icon(Icons.copy); case ViewMoreActionType.rename: - return const FlowySvg(FlowySvgs.edit_s); + return const FlowySvg(FlowySvgs.view_item_rename_s); + case ViewMoreActionType.moveTo: + return const FlowySvg(FlowySvgs.move_to_s); + case ViewMoreActionType.openInNewTab: + return const FlowySvg(FlowySvgs.view_item_open_in_new_tab_s); + case ViewMoreActionType.changeIcon: + return const FlowySvg(FlowySvgs.change_icon_s); + case ViewMoreActionType.collapseAllPages: + return const FlowySvg(FlowySvgs.collapse_all_page_s); + case ViewMoreActionType.divider: + case ViewMoreActionType.lastModified: + case ViewMoreActionType.created: + return const SizedBox.shrink(); + } + } + + Widget get rightIcon { + switch (this) { + case ViewMoreActionType.changeIcon: case ViewMoreActionType.moveTo: - return const Icon(Icons.move_to_inbox); + return const FlowySvg(FlowySvgs.view_item_right_arrow_s); + case ViewMoreActionType.favorite: + case ViewMoreActionType.unFavorite: + case ViewMoreActionType.duplicate: + case ViewMoreActionType.copyLink: + case ViewMoreActionType.rename: case ViewMoreActionType.openInNewTab: - return const FlowySvg(FlowySvgs.full_view_s); + case ViewMoreActionType.collapseAllPages: + case ViewMoreActionType.divider: + case ViewMoreActionType.delete: + case ViewMoreActionType.lastModified: + case ViewMoreActionType.created: + return const SizedBox.shrink(); } } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_add_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_add_button.dart index e020751e0bee6..1cfff3c6e6509 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_add_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_add_button.dart @@ -1,15 +1,14 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/document.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/import/import_panel.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:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:easy_localization/easy_localization.dart'; class ViewAddButton extends StatelessWidget { const ViewAddButton({ @@ -51,13 +50,12 @@ class ViewAddButton extends StatelessWidget { return PopoverActionList( direction: PopoverDirection.bottomWithLeftAligned, actions: _actions, - offset: const Offset(0, 8), + offset: const Offset(0, 4), buildChild: (popover) { return FlowyIconButton( hoverColor: Colors.transparent, - iconPadding: const EdgeInsets.all(2), - width: 26, - icon: const FlowySvg(FlowySvgs.add_s), + width: 24, + icon: const FlowySvg(FlowySvgs.view_item_add_s), onPressed: () { onEditing(true); popover.show(); 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 057b3d8f99188..0ebb919318072 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,8 +1,5 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; @@ -11,8 +8,9 @@ import 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_b import 'package:appflowy/workspace/application/tabs/tabs_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/home_sizes.dart'; import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/rename_view_dialog.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'; import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart'; @@ -26,16 +24,25 @@ 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:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -typedef ViewItemOnSelected = void Function(ViewPB, BuildContext); +typedef ViewItemOnSelected = void Function(BuildContext context, ViewPB view); +typedef ViewItemLeftIconBuilder = Widget Function( + BuildContext context, + ViewPB view, +); +typedef ViewItemRightIconsBuilder = List Function( + BuildContext context, + ViewPB view, +); class ViewItem extends StatelessWidget { const ViewItem({ super.key, required this.view, this.parentView, - required this.categoryType, + required this.spaceType, required this.level, this.leftPadding = 10, required this.onSelected, @@ -43,15 +50,19 @@ class ViewItem extends StatelessWidget { this.isFirstChild = false, this.isDraggable = true, required this.isFeedback, - this.height = 28.0, + this.height = HomeSpaceViewSizes.viewHeight, this.isHoverEnabled = true, this.isPlaceholder = false, + this.isHovered, + this.shouldRenderChildren = true, + this.leftIconBuilder, + this.rightIconsBuilder, }); final ViewPB view; final ViewPB? parentView; - final FolderCategoryType categoryType; + final FolderSpaceType spaceType; // indicate the level of the view item // used to calculate the left padding @@ -85,6 +96,17 @@ class ViewItem extends StatelessWidget { // placeholder widget to receive the drop event when moving view across sections. final bool isPlaceholder; + // used for control the expand/collapse icon + final ValueNotifier? isHovered; + + // render the child views of the view + final bool shouldRenderChildren; + + // custom the left icon widget, if it's null, the default expand/collapse icon will be used + final ViewItemLeftIconBuilder? leftIconBuilder; + // custom the right icon widget, if it's null, the default ... and + button will be used + final ViewItemRightIconsBuilder? rightIconsBuilder; + @override Widget build(BuildContext context) { return BlocProvider( @@ -100,7 +122,7 @@ class ViewItem extends StatelessWidget { view: state.view, parentView: parentView, childViews: state.view.childViews, - categoryType: categoryType, + spaceType: spaceType, level: level, leftPadding: leftPadding, showActions: state.isEditing, @@ -113,6 +135,10 @@ class ViewItem extends StatelessWidget { height: height, isHoverEnabled: isHoverEnabled, isPlaceholder: isPlaceholder, + isHovered: isHovered, + shouldRenderChildren: shouldRenderChildren, + leftIconBuilder: leftIconBuilder, + rightIconsBuilder: rightIconsBuilder, ); }, ), @@ -128,7 +154,7 @@ class InnerViewItem extends StatelessWidget { required this.view, required this.parentView, required this.childViews, - required this.categoryType, + required this.spaceType, this.isDraggable = true, this.isExpanded = true, required this.level, @@ -141,12 +167,16 @@ class InnerViewItem extends StatelessWidget { required this.height, this.isHoverEnabled = true, this.isPlaceholder = false, + this.isHovered, + this.shouldRenderChildren = true, + required this.leftIconBuilder, + required this.rightIconsBuilder, }); final ViewPB view; final ViewPB? parentView; final List childViews; - final FolderCategoryType categoryType; + final FolderSpaceType spaceType; final bool isDraggable; final bool isExpanded; @@ -164,6 +194,10 @@ class InnerViewItem extends StatelessWidget { final bool isHoverEnabled; final bool isPlaceholder; + final ValueNotifier? isHovered; + final bool shouldRenderChildren; + final ViewItemLeftIconBuilder? leftIconBuilder; + final ViewItemRightIconsBuilder? rightIconsBuilder; @override Widget build(BuildContext context) { @@ -172,7 +206,7 @@ class InnerViewItem extends StatelessWidget { parentView: parentView, level: level, showActions: showActions, - categoryType: categoryType, + spaceType: spaceType, onSelected: onSelected, onTertiarySelected: onTertiarySelected, isExpanded: isExpanded, @@ -181,56 +215,40 @@ class InnerViewItem extends StatelessWidget { isFeedback: isFeedback, height: height, isPlaceholder: isPlaceholder, + isHovered: isHovered, + leftIconBuilder: leftIconBuilder, + rightIconsBuilder: rightIconsBuilder, ); // if the view is expanded and has child views, render its child views - if (isExpanded) { - if (childViews.isNotEmpty) { - final children = childViews.map((childView) { - return ViewItem( - key: ValueKey('${categoryType.name} ${childView.id}'), - parentView: view, - categoryType: categoryType, - isFirstChild: childView.id == childViews.first.id, - view: childView, - level: level + 1, - onSelected: onSelected, - onTertiarySelected: onTertiarySelected, - isDraggable: isDraggable, - leftPadding: leftPadding, - isFeedback: isFeedback, - isPlaceholder: isPlaceholder, - ); - }).toList(); - - child = Column( - mainAxisSize: MainAxisSize.min, - children: [ - child, - ...children, - ], - ); - } else { - child = Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - child, - Container( - height: height, - alignment: Alignment.centerLeft, - child: Padding( - // add 2px to make the text align with the view item - padding: EdgeInsets.only(left: (level + 1) * leftPadding + 2), - child: FlowyText.medium( - LocaleKeys.noPagesInside.tr(), - color: Theme.of(context).hintColor, - ), - ), - ), - ], + if (isExpanded && shouldRenderChildren && childViews.isNotEmpty) { + final children = childViews.map((childView) { + return ViewItem( + key: ValueKey('${spaceType.name} ${childView.id}'), + parentView: view, + spaceType: spaceType, + isFirstChild: childView.id == childViews.first.id, + view: childView, + level: level + 1, + onSelected: onSelected, + onTertiarySelected: onTertiarySelected, + isDraggable: isDraggable, + leftPadding: leftPadding, + isFeedback: isFeedback, + isPlaceholder: isPlaceholder, + isHovered: isHovered, + leftIconBuilder: leftIconBuilder, + rightIconsBuilder: rightIconsBuilder, ); - } + }).toList(); + + child = Column( + mainAxisSize: MainAxisSize.min, + children: [ + child, + ...children, + ], + ); } // wrap the child with DraggableItem if isDraggable is true @@ -246,16 +264,27 @@ class InnerViewItem extends StatelessWidget { ? (from, to) => _moveViewCrossSection(context, from, to) : null, feedback: (context) { - return ViewItem( - view: view, - parentView: parentView, - categoryType: categoryType, - level: level, - onSelected: onSelected, - onTertiarySelected: onTertiarySelected, - isDraggable: false, - leftPadding: leftPadding, - isFeedback: true, + return Container( + width: 250, + decoration: BoxDecoration( + color: Brightness.light == Theme.of(context).brightness + ? Colors.white + : Colors.black54, + borderRadius: BorderRadius.circular(8), + ), + child: ViewItem( + view: view, + parentView: parentView, + spaceType: spaceType, + level: level, + onSelected: onSelected, + onTertiarySelected: onTertiarySelected, + isDraggable: false, + leftPadding: leftPadding, + isFeedback: true, + leftIconBuilder: leftIconBuilder, + rightIconsBuilder: rightIconsBuilder, + ), ); }, child: child, @@ -263,7 +292,7 @@ class InnerViewItem extends StatelessWidget { } else { // keep the same height of the DraggableItem child = Padding( - padding: const EdgeInsets.only(top: 2.0), + padding: const EdgeInsets.only(top: kDraggableViewItemDividerHeight), child: child, ); } @@ -279,10 +308,10 @@ class InnerViewItem extends StatelessWidget { if (isReferencedDatabaseView(view, parentView)) { return; } - final fromSection = categoryType == FolderCategoryType.public + final fromSection = spaceType == FolderSpaceType.public ? ViewSectionPB.Private : ViewSectionPB.Public; - final toSection = categoryType == FolderCategoryType.public + final toSection = spaceType == FolderSpaceType.public ? ViewSectionPB.Public : ViewSectionPB.Private; context.read().add( @@ -297,7 +326,7 @@ class InnerViewItem extends StatelessWidget { context.read().add( ViewEvent.updateViewVisibility( from, - categoryType == FolderCategoryType.public, + spaceType == FolderSpaceType.public, ), ); } @@ -312,7 +341,7 @@ class SingleInnerViewItem extends StatefulWidget { required this.level, required this.leftPadding, this.isDraggable = true, - required this.categoryType, + required this.spaceType, required this.showActions, required this.onSelected, this.onTertiarySelected, @@ -320,6 +349,9 @@ class SingleInnerViewItem extends StatefulWidget { required this.height, this.isHoverEnabled = true, this.isPlaceholder = false, + this.isHovered, + required this.leftIconBuilder, + required this.rightIconsBuilder, }); final ViewPB view; @@ -335,11 +367,14 @@ class SingleInnerViewItem extends StatefulWidget { final bool showActions; final ViewItemOnSelected onSelected; final ViewItemOnSelected? onTertiarySelected; - final FolderCategoryType categoryType; + final FolderSpaceType spaceType; final double height; final bool isHoverEnabled; final bool isPlaceholder; + final ValueNotifier? isHovered; + final ViewItemLeftIconBuilder? leftIconBuilder; + final ViewItemRightIconsBuilder? rightIconsBuilder; @override State createState() => _SingleInnerViewItemState(); @@ -382,11 +417,11 @@ class _SingleInnerViewItemState extends State { Widget _buildViewItem(bool onHover, [bool isSelected = false]) { final children = [ - // expand icon - _buildLeftIcon(), + // expand icon or placeholder + widget.leftIconBuilder?.call(context, widget.view) ?? _buildLeftIcon(), // icon _buildViewIconButton(), - const HSpace(5), + const HSpace(6), // title Expanded( child: FlowyText.regular( @@ -398,25 +433,32 @@ class _SingleInnerViewItemState extends State { // hover action if (widget.showActions || onHover) { - // ··· more action button - children.add(_buildViewMoreActionButton(context)); - // only support add button for document layout - if (widget.view.layout == ViewLayoutPB.Document) { - // + button - children.add(_buildViewAddButton(context)); + if (widget.rightIconsBuilder != null) { + children.addAll(widget.rightIconsBuilder!(context, widget.view)); + } else { + // ··· more action button + children.add(_buildViewMoreActionButton(context)); + children.add(const HSpace(8.0)); + // only support add button for document layout + if (widget.view.layout == ViewLayoutPB.Document) { + // + button + children.add(_buildViewAddButton(context)); + } + children.add(const HSpace(4.0)); } } final child = GestureDetector( behavior: HitTestBehavior.translucent, - onTap: () => widget.onSelected(widget.view, context), + onTap: () => widget.onSelected(context, widget.view), onTertiaryTapDown: (_) => - widget.onTertiarySelected?.call(widget.view, context), + widget.onTertiarySelected?.call(context, widget.view), child: SizedBox( height: widget.height, child: Padding( padding: EdgeInsets.only(left: widget.level * widget.leftPadding), child: Row( + mainAxisAlignment: MainAxisAlignment.center, children: children, ), ), @@ -446,26 +488,24 @@ class _SingleInnerViewItemState extends State { Widget _buildViewIconButton() { final icon = widget.view.icon.value.isNotEmpty - ? EmojiText( - emoji: widget.view.icon.value, - fontSize: 18.0, + ? FlowyText.emoji( + widget.view.icon.value, + fontSize: 16.0, ) - : SizedBox.square( - dimension: 20.0, - child: widget.view.defaultIcon(), - ); + : widget.view.defaultIcon(); + return AppFlowyPopover( offset: const Offset(20, 0), controller: controller, direction: PopoverDirection.rightWithCenterAligned, - constraints: BoxConstraints.loose(const Size(360, 380)), + constraints: BoxConstraints.loose(const Size(364, 356)), 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, + child: SizedBox(width: 16.0, child: icon), ), ), popupBuilder: (context) { @@ -492,18 +532,33 @@ class _SingleInnerViewItemState extends State { return const _DotIconWidget(); } - final svg = widget.isExpanded - ? FlowySvgs.drop_menu_show_m - : FlowySvgs.drop_menu_hide_m; - return GestureDetector( + if (context.read().state.view.childViews.isEmpty) { + return HSpace(widget.leftPadding); + } + + final child = GestureDetector( child: FlowySvg( - svg, + widget.isExpanded + ? FlowySvgs.view_item_expand_s + : FlowySvgs.view_item_unexpand_s, size: const Size.square(16.0), ), onTap: () => context .read() .add(ViewEvent.setIsExpanded(!widget.isExpanded)), ); + + if (widget.isHovered != null) { + return ValueListenableBuilder( + valueListenable: widget.isHovered!, + builder: (_, isHovered, child) { + return Opacity(opacity: isHovered ? 1.0 : 0.0, child: child); + }, + child: child, + ); + } + + return child; } // + button @@ -533,7 +588,7 @@ class _SingleInnerViewItemState extends State { viewName, pluginBuilder.layoutType!, openAfterCreated: openAfterCreated, - section: widget.categoryType.toViewSectionPB, + section: widget.spaceType.toViewSectionPB, ), ); } @@ -554,9 +609,10 @@ class _SingleInnerViewItemState extends State { message: LocaleKeys.menuAppHeader_moreButtonToolTip.tr(), child: ViewMoreActionButton( view: widget.view, + spaceType: widget.spaceType, onEditing: (value) => context.read().add(ViewEvent.setIsEditing(value)), - onAction: (action) { + onAction: (action, data) { switch (action) { case ViewMoreActionType.favorite: case ViewMoreActionType.unFavorite: @@ -584,6 +640,20 @@ class _SingleInnerViewItemState extends State { case ViewMoreActionType.openInNewTab: context.read().openTab(widget.view); break; + case ViewMoreActionType.collapseAllPages: + context.read().add(const ViewEvent.collapseAllPages()); + break; + case ViewMoreActionType.changeIcon: + if (data is! EmojiPickerResult) { + return; + } + final result = data; + ViewBackendService.updateViewIcon( + viewId: widget.view.id, + viewIcon: result.emoji, + iconType: result.type.toProto(), + ); + break; default: throw UnsupportedError('$action is not supported'); } 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 0cefde700e8a0..68d179da50709 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 @@ -1,11 +1,12 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/base/icon/icon_picker.dart'; +import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:flutter/material.dart'; - import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; /// ··· button beside the view name class ViewMoreActionButton extends StatelessWidget { @@ -14,59 +15,179 @@ class ViewMoreActionButton extends StatelessWidget { required this.view, required this.onEditing, required this.onAction, + required this.spaceType, }); final ViewPB view; final void Function(bool value) onEditing; - final void Function(ViewMoreActionType) onAction; + final void Function(ViewMoreActionType type, dynamic data) onAction; + final FolderSpaceType spaceType; @override Widget build(BuildContext context) { - final supportedActionTypes = [ - ViewMoreActionType.rename, - ViewMoreActionType.delete, - ViewMoreActionType.duplicate, - ViewMoreActionType.openInNewTab, - view.isFavorite - ? ViewMoreActionType.unFavorite - : ViewMoreActionType.favorite, - ]; + final wrappers = _buildActionTypeWrappers(); return PopoverActionList( - direction: PopoverDirection.bottomWithCenterAligned, + direction: PopoverDirection.bottomWithLeftAligned, offset: const Offset(0, 8), - actions: supportedActionTypes - .map((e) => ViewMoreActionTypeWrapper(e)) - .toList(), + actions: wrappers, + constraints: const BoxConstraints( + minWidth: 260, + ), buildChild: (popover) { return FlowyIconButton( - hoverColor: Colors.transparent, - iconPadding: const EdgeInsets.all(2), - width: 26, - icon: const FlowySvg(FlowySvgs.details_s), + width: 24, + icon: const FlowySvg(FlowySvgs.workspace_three_dots_s), onPressed: () { onEditing(true); popover.show(); }, ); }, - onSelected: (action, popover) { - onEditing(false); - onAction(action.inner); - popover.close(); - }, + onSelected: (_, __) {}, onClosed: () => onEditing(false), ); } + + List _buildActionTypeWrappers() { + final actionTypes = _buildActionTypes(); + return actionTypes + .map( + (e) => ViewMoreActionTypeWrapper(e, (controller, data) { + onEditing(false); + onAction(e, data); + controller.close(); + }), + ) + .toList(); + } + + List _buildActionTypes() { + final List actionTypes = []; + switch (spaceType) { + case FolderSpaceType.favorite: + actionTypes.addAll([ + ViewMoreActionType.unFavorite, + ViewMoreActionType.divider, + ViewMoreActionType.rename, + ViewMoreActionType.openInNewTab, + ]); + break; + default: + actionTypes.addAll([ + view.isFavorite + ? ViewMoreActionType.unFavorite + : ViewMoreActionType.favorite, + ViewMoreActionType.divider, + ViewMoreActionType.rename, + ViewMoreActionType.changeIcon, + ViewMoreActionType.duplicate, + ViewMoreActionType.delete, + ViewMoreActionType.divider, + ViewMoreActionType.collapseAllPages, + ViewMoreActionType.divider, + ViewMoreActionType.openInNewTab, + ]); + } + return actionTypes; + } } -class ViewMoreActionTypeWrapper extends ActionCell { - ViewMoreActionTypeWrapper(this.inner); +class ViewMoreActionTypeWrapper extends CustomActionCell { + ViewMoreActionTypeWrapper(this.inner, this.onTap); final ViewMoreActionType inner; + final void Function(PopoverController controller, dynamic data) onTap; @override - Widget? leftIcon(Color iconColor) => inner.icon(iconColor); + Widget buildWithContext(BuildContext context, PopoverController controller) { + if (inner == ViewMoreActionType.divider) { + return _buildDivider(); + } else if (inner == ViewMoreActionType.lastModified) { + return _buildLastModified(context); + } else if (inner == ViewMoreActionType.created) { + return _buildCreated(context); + } else if (inner == ViewMoreActionType.changeIcon) { + return _buildEmojiActionButton(context, controller); + } else { + return _buildNormalActionButton(context, controller); + } + } - @override - String get name => inner.name; + Widget _buildNormalActionButton( + BuildContext context, + PopoverController controller, + ) { + return _buildActionButton(context, () => onTap(controller, null)); + } + + Widget _buildEmojiActionButton( + BuildContext context, + PopoverController controller, + ) { + final child = _buildActionButton(context, null); + + return AppFlowyPopover( + constraints: BoxConstraints.loose(const Size(364, 356)), + clickHandler: PopoverClickHandler.gestureDetector, + popupBuilder: (_) => FlowyIconPicker( + onSelected: (result) => onTap(controller, result), + ), + child: child, + ); + } + + Widget _buildDivider() { + return const Padding( + padding: EdgeInsets.all(8.0), + child: Divider(height: 1.0), + ); + } + + Widget _buildLastModified(BuildContext context) { + return Container( + height: 40, + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: const Column( + crossAxisAlignment: CrossAxisAlignment.start, + ), + ); + } + + Widget _buildCreated(BuildContext context) { + return Container( + height: 40, + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: const Column( + crossAxisAlignment: CrossAxisAlignment.start, + ), + ); + } + + Widget _buildActionButton( + BuildContext context, + VoidCallback? onTap, + ) { + return Container( + height: 34, + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: FlowyButton( + margin: const EdgeInsets.symmetric(horizontal: 6), + leftIcon: inner.leftIcon, + rightIcon: inner.rightIcon, + iconPadding: 10.0, + text: SizedBox( + height: 18.0, + child: FlowyText.regular( + inner.name, + color: inner == ViewMoreActionType.delete + ? Theme.of(context).colorScheme.error + : null, + ), + ), + onTap: onTap, + ), + ); + } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/flowy_tab.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/flowy_tab.dart index ee4f1d6d00b19..4760378f4b7c4 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/flowy_tab.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/flowy_tab.dart @@ -33,7 +33,7 @@ class _FlowyTabState extends State { onExit: (_) => _setHovering(), child: Container( width: HomeSizes.tabBarWidth, - height: HomeSizes.tabBarHeigth, + height: HomeSizes.tabBarHeight, decoration: BoxDecoration( color: _getBackgroundColor(), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/tabs_manager.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/tabs_manager.dart index 6e24544820617..064d64477f62b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/tabs_manager.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/tabs_manager.dart @@ -57,7 +57,7 @@ class _TabsManagerState extends State return Container( alignment: Alignment.bottomLeft, - height: HomeSizes.tabBarHeigth, + height: HomeSizes.tabBarHeight, decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHighest, ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_button.dart index a7925dc3f7984..049ee6481b595 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_button.dart @@ -48,10 +48,8 @@ class NotificationButton extends StatelessWidget { Widget _buildNotificationIcon(BuildContext context, bool hasUnreads) { return Stack( children: [ - FlowySvg( - FlowySvgs.clock_alarm_s, - size: const Size.square(24), - color: Theme.of(context).colorScheme.tertiary, + const FlowySvg( + FlowySvgs.notification_s, ), if (hasUnreads) Positioned( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart index ebbb4f0e074e5..68829d7b662fe 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart @@ -1,6 +1,3 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; @@ -23,6 +20,8 @@ import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class SettingsAccountView extends StatefulWidget { @@ -342,7 +341,8 @@ class _UserProfileSettingState extends State { child: UserAvatar( iconUrl: widget.iconUrl, name: widget.name, - isLarge: true, + size: 48, + fontSize: 24, isHovering: isHovering, ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart index 8bf9415e3f034..aa67ea315817d 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart @@ -1,6 +1,3 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; @@ -43,6 +40,8 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -377,7 +376,8 @@ class _WorkspaceIconSetting extends StatelessWidget { child: WorkspaceIcon( workspace: workspace!, iconSize: workspace!.icon.isNotEmpty == true ? 46 : 20, - enableEdit: enableEdit, + fontSize: 16.0, + enableEdit: true, onSelected: (r) => context .read() .add(WorkspaceSettingsEvent.updateWorkspaceIcon(r.emoji)), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart index 43f9438ff77fc..e4e15da75dcbc 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart @@ -1,9 +1,8 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; class FeatureFlagsPage extends StatelessWidget { const FeatureFlagsPage({ @@ -50,7 +49,8 @@ class _FeatureFlagItemState extends State<_FeatureFlagItem> { subtitle: FlowyText.small(widget.featureFlag.description, maxLines: 3), trailing: Switch.adaptive( value: widget.featureFlag.isOn, - onChanged: (value) => setState(() => widget.featureFlag.update(value)), + onChanged: (value) => + setState(() async => widget.featureFlag.update(value)), ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_exporter_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_exporter_widget.dart index 751599def3e06..decf74f874042 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_exporter_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_exporter_widget.dart @@ -1,7 +1,5 @@ import 'dart:io'; -import 'package:flutter/material.dart'; - import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/export/document_exporter.dart'; import 'package:appflowy/workspace/application/settings/settings_file_exporter_cubit.dart'; @@ -16,7 +14,8 @@ import 'package:appflowy_result/appflowy_result.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:path/path.dart' as p; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/favorite_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/favorite_button.dart index 4c52e6c27851f..e25c81b182acf 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/favorite_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/favorite_button.dart @@ -1,13 +1,11 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class ViewFavoriteButton extends StatelessWidget { @@ -35,9 +33,8 @@ class ViewFavoriteButton extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(6), child: FlowySvg( - isFavorite ? FlowySvgs.favorite_s : FlowySvgs.unfavorite_s, - size: const Size(18, 18), - color: AFThemeExtension.of(context).warning, + isFavorite ? FlowySvgs.unfavorite_s : FlowySvgs.favorite_s, + size: const Size.square(18), ), ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart index e72dfa098cc25..1c8c8d20ff080 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart @@ -1,6 +1,3 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; @@ -14,6 +11,8 @@ import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -147,7 +146,7 @@ class _DebugToast { class FlowyVersionDescription extends CustomActionCell { @override - Widget buildWithContext(BuildContext context) { + Widget buildWithContext(BuildContext context, PopoverController controller) { return FutureBuilder( future: PackageInfo.fromPlatform(), builder: (BuildContext context, AsyncSnapshot snapshot) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart index 3d0416f5897ba..e4bf942c6af04 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; @@ -14,6 +12,7 @@ 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:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MoreViewActions extends StatefulWidget { @@ -101,8 +100,8 @@ class _MoreViewActionsState extends State { builder: (context, isHovering) => Padding( padding: const EdgeInsets.all(6), child: FlowySvg( - FlowySvgs.three_dots_vertical_s, - size: const Size.square(16), + FlowySvgs.three_dots_s, + size: const Size.square(18), color: isHovering ? Theme.of(context).colorScheme.onSecondary : Theme.of(context).iconTheme.color, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart index bb285a79170f7..d4103cc6bf18f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart @@ -1,5 +1,5 @@ import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flutter/material.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -83,7 +83,7 @@ class _PopoverActionListState ); } else { final custom = action as CustomActionCell; - return custom.buildWithContext(context); + return custom.buildWithContext(context, popoverController); } }).toList(); @@ -121,7 +121,7 @@ abstract class PopoverActionCell extends PopoverAction { } abstract class CustomActionCell extends PopoverAction { - Widget buildWithContext(BuildContext context); + Widget buildWithContext(BuildContext context, PopoverController controller); } abstract class PopoverAction {} 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 b12ae6644a98e..ab67ecb5b405c 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 @@ -1,10 +1,10 @@ -import 'package:flutter/widgets.dart'; - import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/style_widget/text_field.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; class RenameViewPopover extends StatefulWidget { const RenameViewPopover({ @@ -51,17 +51,20 @@ class _RenameViewPopoverState extends State { mainAxisSize: MainAxisSize.min, children: [ if (widget.showIconChanger) ...[ - EmojiPickerButton( - emoji: widget.emoji, - defaultIcon: widget.icon, - direction: PopoverDirection.bottomWithCenterAligned, - offset: const Offset(0, 18), - onSubmitted: _updateViewIcon, + SizedBox( + width: 30.0, + child: EmojiPickerButton( + emoji: widget.emoji, + defaultIcon: widget.icon, + direction: PopoverDirection.bottomWithCenterAligned, + offset: const Offset(0, 18), + onSubmitted: _updateViewIcon, + ), ), const HSpace(6), ], SizedBox( - height: 36.0, + height: 32.0, width: 220, child: FlowyTextField( controller: _controller, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart index 4b6708c151aa1..5ad85efe3156c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart @@ -1,37 +1,32 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; import 'package:appflowy/util/built_in_svgs.dart'; import 'package:appflowy/util/color_generator/color_generator.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; - -const double _smallSize = 28; -const double _largeSize = 64; +import 'package:flutter/material.dart'; class UserAvatar extends StatelessWidget { const UserAvatar({ super.key, required this.iconUrl, required this.name, - this.isLarge = false, + required this.size, + required this.fontSize, this.isHovering = false, }); final String iconUrl; final String name; - final bool isLarge; + final double size; + final double fontSize; // If true, a border will be applied on top of the avatar final bool isHovering; @override Widget build(BuildContext context) { - final size = isLarge ? _largeSize : _smallSize; - if (iconUrl.isEmpty) { final String nameOrDefault = _userName(name); final Color color = ColorGenerator(name).toColor(); @@ -59,16 +54,10 @@ class UserAvatar extends StatelessWidget { ) : null, ), - child: FlowyText.semibold( + child: FlowyText.regular( nameInitials, color: Colors.black, - fontSize: isLarge - ? nameInitials.length == initialsCount - ? 20 - : 26 - : nameInitials.length == initialsCount - ? 12 - : 14, + fontSize: fontSize, ), ); } @@ -94,7 +83,7 @@ class UserAvatar extends StatelessWidget { FlowySvgData('emoji/$iconUrl'), blendMode: null, ) - : EmojiText(emoji: iconUrl, fontSize: isLarge ? 36 : 18), + : FlowyText.emoji(iconUrl, fontSize: fontSize), ), ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart index 9d65cc06a112f..18b206bd9e147 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart @@ -1,23 +1,18 @@ -import 'dart:math'; - -import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; -import 'package:appflowy/startup/tasks/app_window_size_manager.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/application/view/view_listener.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy/workspace/application/view_title/view_title_bar_bloc.dart'; +import 'package:appflowy/workspace/application/view_title/view_title_bloc.dart'; import 'package:appflowy/workspace/presentation/widgets/rename_view_popover.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:appflowy_result/appflowy_result.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -// workspace name / ... / view_title -class ViewTitleBar extends StatefulWidget { +// workspace name > ... > view_title +class ViewTitleBar extends StatelessWidget { const ViewTitleBar({ super.key, required this.view, @@ -25,133 +20,83 @@ class ViewTitleBar extends StatefulWidget { final ViewPB view; - @override - State createState() => _ViewTitleBarState(); -} - -class _ViewTitleBarState extends State { - late Future> ancestors; - late String viewId; - - @override - void initState() { - super.initState(); - - viewId = widget.view.id; - _reloadAncestors(viewId); - } - - @override - void didUpdateWidget(covariant ViewTitleBar oldWidget) { - super.didUpdateWidget(oldWidget); - - if (oldWidget.view.id != widget.view.id) { - viewId = widget.view.id; - _reloadAncestors(viewId); - } - } - + // late Future> ancestors; @override Widget build(BuildContext context) { - return FutureBuilder>( - future: ancestors, - builder: (context, snapshot) { - final ancestors = snapshot.data; - if (ancestors == null || - snapshot.connectionState != ConnectionState.done) { - return const SizedBox.shrink(); - } - const maxWidth = WindowSizeManager.minWindowWidth / 2.0; - final replacement = Row( - // refresh the view title bar when the ancestors changed - key: ValueKey(ancestors.hashCode), - children: _buildViewTitles(context, ancestors), - ); - return LayoutBuilder( - builder: (context, constraints) { - return Visibility( - visible: constraints.maxWidth < maxWidth, - replacement: replacement, - // if the width is too small, only show one view title bar without the ancestors - child: _ViewTitle( - key: ValueKey(ancestors.last), - view: ancestors.last, - maxTitleWidth: constraints.maxWidth, - onUpdated: () => setState(() => _reloadAncestors(viewId)), - ), - ); - }, - ); - }, + return BlocProvider( + create: (_) => + ViewTitleBarBloc(view: view)..add(const ViewTitleBarEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + final ancestors = state.ancestors; + if (ancestors.isEmpty) { + return const SizedBox.shrink(); + } + return SingleChildScrollView( + child: SizedBox( + height: 24, + child: Row(children: _buildViewTitles(context, ancestors)), + ), + ); + }, + ), ); } List _buildViewTitles(BuildContext context, List views) { // if the level is too deep, only show the last two view, the first one view and the root view + // for example: + // if the views are [root, view1, view2, view3, view4, view5], only show [root, view1, ..., view4, view5] + // if the views are [root, view1, view2, view3], show [root, view1, view2, view3] + const lowerBound = 2; + final upperBound = views.length - 2; bool hasAddedEllipsis = false; final children = []; - for (var i = 0; i < views.length; i++) { + if (views.length <= 1) { + return []; + } + + // ignore the workspace name, use section name instead in the future + // skip the workspace view + for (var i = 1; i < views.length; i++) { final view = views[i]; - if (i >= 1 && i < views.length - 2) { + if (i >= lowerBound && i < upperBound) { if (!hasAddedEllipsis) { hasAddedEllipsis = true; - children.add( - const FlowyText.regular(' ... /'), - ); + children.addAll([ + const FlowyText.regular(' ... '), + const FlowySvg(FlowySvgs.title_bar_divider_s), + ]); } continue; } - Widget child; - if (i == 0) { - final currentWorkspace = - context.read().state.currentWorkspace; - final icon = currentWorkspace?.icon ?? ''; - final name = currentWorkspace?.name ?? view.name; - // the first one is the workspace name - child = FlowyTooltip( - message: name, - child: Row( - children: [ - EmojiText( - emoji: icon, - fontSize: 18.0, - ), - const HSpace(2.0), - FlowyText.regular(name), - const HSpace(4.0), - ], - ), - ); - } else { - child = FlowyTooltip( - message: view.name, - child: _ViewTitle( - view: view, - behavior: i == views.length - 1 - ? _ViewTitleBehavior.editable // only the last one is editable - : _ViewTitleBehavior.uneditable, // others are not editable - onUpdated: () => setState(() => _reloadAncestors(viewId)), - ), - ); - } + final child = FlowyTooltip( + message: view.name, + child: _ViewTitle( + view: view, + behavior: i == views.length - 1 + ? _ViewTitleBehavior.editable // only the last one is editable + : _ViewTitleBehavior.uneditable, // others are not editable + onUpdated: () { + context + .read() + .add(const ViewTitleBarEvent.reload()); + }, + ), + ); children.add(child); if (i != views.length - 1) { // if not the last one, add a divider - children.add(const FlowyText.regular('/')); + children.add(const FlowySvg(FlowySvgs.title_bar_divider_s)); } } return children; } - - void _reloadAncestors(String viewId) { - ancestors = ViewBackendService.getViewAncestors(viewId) - .fold((s) => s.items, (f) => []); - } } enum _ViewTitleBehavior { @@ -161,16 +106,13 @@ enum _ViewTitleBehavior { class _ViewTitle extends StatefulWidget { const _ViewTitle({ - super.key, required this.view, this.behavior = _ViewTitleBehavior.editable, - this.maxTitleWidth = 180, required this.onUpdated, }); final ViewPB view; final _ViewTitleBehavior behavior; - final double maxTitleWidth; final VoidCallback onUpdated; @override @@ -180,87 +122,58 @@ class _ViewTitle extends StatefulWidget { class _ViewTitleState extends State<_ViewTitle> { final popoverController = PopoverController(); final textEditingController = TextEditingController(); - late final viewListener = ViewListener(viewId: widget.view.id); - - String name = ''; - String icon = ''; - String inputtingName = ''; - - @override - void initState() { - super.initState(); - - name = widget.view.name; - icon = widget.view.icon.value; - - _resetTextEditingController(); - viewListener.start( - onViewUpdated: (view) { - if (name != view.name || icon != view.icon.value) { - widget.onUpdated(); - } - setState(() { - name = view.name; - icon = view.icon.value; - _resetTextEditingController(); - }); - }, - ); - } @override void dispose() { textEditingController.dispose(); popoverController.close(); - viewListener.stop(); super.dispose(); } @override Widget build(BuildContext context) { - // root view - if (widget.view.parentViewId.isEmpty) { - return Row( - children: [ - FlowyText.regular(name), - const HSpace(4.0), - ], - ); - } - - final child = SingleChildScrollView( - child: Row( - children: [ - EmojiText( - emoji: icon, - fontSize: 18.0, - ), - const HSpace(2.0), - ConstrainedBox( - constraints: BoxConstraints( - maxWidth: max(0, widget.maxTitleWidth), - ), - child: FlowyText.regular( - name, - overflow: TextOverflow.ellipsis, - ), - ), - ], + final isEditable = widget.behavior == _ViewTitleBehavior.editable; + + return BlocProvider( + create: (_) => + ViewTitleBloc(view: widget.view)..add(const ViewTitleEvent.initial()), + child: BlocConsumer( + listener: (_, state) { + _resetTextEditingController(state); + widget.onUpdated(); + }, + builder: (context, state) { + // root view + if (widget.view.parentViewId.isEmpty) { + return Row( + children: [ + FlowyText.regular(state.name), + const HSpace(4.0), + ], + ); + } else if (isEditable) { + return _buildEditableViewTitle(context, state); + } else { + return _buildUnEditableViewTitle(context, state); + } + }, ), ); + } - if (widget.behavior == _ViewTitleBehavior.uneditable) { - return Listener( - onPointerDown: (_) => context.read().openPlugin(widget.view), - child: FlowyButton( - useIntrinsicWidth: true, - onTap: () {}, - text: child, - ), - ); - } + Widget _buildUnEditableViewTitle(BuildContext context, ViewTitleState state) { + return Listener( + onPointerDown: (_) => context.read().openPlugin(widget.view), + child: FlowyButton( + useIntrinsicWidth: true, + onTap: () {}, + text: _buildIconAndName(state), + ), + ); + } + Widget _buildEditableViewTitle(BuildContext context, ViewTitleState state) { return AppFlowyPopover( constraints: const BoxConstraints( maxWidth: 300, @@ -268,32 +181,55 @@ class _ViewTitleState extends State<_ViewTitle> { ), controller: popoverController, direction: PopoverDirection.bottomWithLeftAligned, - offset: const Offset(0, 18), + offset: const Offset(0, 6), popupBuilder: (context) { // icon + textfield - _resetTextEditingController(); + _resetTextEditingController(state); return RenameViewPopover( viewId: widget.view.id, name: widget.view.name, popoverController: popoverController, icon: widget.view.defaultIcon(), - emoji: icon, + emoji: state.icon, ); }, child: FlowyButton( useIntrinsicWidth: true, - text: child, + margin: const EdgeInsets.symmetric(horizontal: 6.0), + text: _buildIconAndName(state), + ), + ); + } + + Widget _buildIconAndName(ViewTitleState state) { + return SingleChildScrollView( + child: Row( + children: [ + if (state.icon.isNotEmpty) ...[ + FlowyText.emoji( + state.icon, + fontSize: 14.0, + ), + const HSpace(6.0), + ], + ConstrainedBox( + constraints: const BoxConstraints(), + child: FlowyText.regular( + state.name, + overflow: TextOverflow.ellipsis, + ), + ), + ], ), ); } - void _resetTextEditingController() { - inputtingName = name; + void _resetTextEditingController(ViewTitleState state) { textEditingController - ..text = name + ..text = state.name ..selection = TextSelection( baseOffset: 0, - extentOffset: name.length, + extentOffset: state.name.length, ); } } diff --git a/frontend/appflowy_flutter/macos/Runner/MainFlutterWindow.swift b/frontend/appflowy_flutter/macos/Runner/MainFlutterWindow.swift index 8e357d7ca1b7d..65498b121f3bf 100644 --- a/frontend/appflowy_flutter/macos/Runner/MainFlutterWindow.swift +++ b/frontend/appflowy_flutter/macos/Runner/MainFlutterWindow.swift @@ -1,7 +1,7 @@ import Cocoa import FlutterMacOS -private let kTrafficLightOffetTop = 22 +private let kTrafficLightOffetTop = 14 class MainFlutterWindow: NSWindow { func registerMethodChannel(flutterViewController: FlutterViewController) { @@ -17,7 +17,7 @@ class MainFlutterWindow: NSWindow { let nY = position[1] as! NSNumber let x = nX.doubleValue let y = nY.doubleValue - + self.setFrameOrigin(NSPoint(x: x, y: y)) result(nil) return @@ -30,7 +30,7 @@ class MainFlutterWindow: NSWindow { result(nil) return } - + result(FlutterMethodNotImplemented) }) } @@ -51,9 +51,9 @@ class MainFlutterWindow: NSWindow { let zoomButton = self.standardWindowButton(ButtonType.zoomButton)! let titlebarView = closeButton.superview! - self.layoutTrafficLightButton(titlebarView: titlebarView, button: closeButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 20) - self.layoutTrafficLightButton(titlebarView: titlebarView, button: minButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 38) - self.layoutTrafficLightButton(titlebarView: titlebarView, button: zoomButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 56) + self.layoutTrafficLightButton(titlebarView: titlebarView, button: closeButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 12) + self.layoutTrafficLightButton(titlebarView: titlebarView, button: minButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 30) + self.layoutTrafficLightButton(titlebarView: titlebarView, button: zoomButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 48) let customToolbar = NSTitlebarAccessoryViewController() let newView = NSView() diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart index d9420944fb12b..405db2bee8de1 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart @@ -1,8 +1,7 @@ +import 'package:appflowy_popover/src/layout.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:appflowy_popover/src/layout.dart'; - import 'mask.dart'; import 'mutex.dart'; @@ -291,6 +290,13 @@ class PopoverContainer extends StatefulWidget { context.findAncestorStateOfType(); return result!; } + + static PopoverContainerState? maybeOf(BuildContext context) { + if (context is StatefulElement && context.state is PopoverContainerState) { + return context.state as PopoverContainerState; + } + return context.findAncestorStateOfType(); + } } class PopoverContainerState extends State { diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/basis.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/basis.dart index 59a2bc653362f..7bbcbf09499c2 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/basis.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/basis.dart @@ -1,8 +1,3 @@ -import 'package:flutter/material.dart'; - // MARK: - Shared Builder - -typedef WidgetBuilder = Widget Function(); - typedef IndexedCallback = void Function(int index); typedef IndexedValueCallback = void Function(T value, int index); diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart index f402c171d2107..acd628a22204f 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart @@ -1,12 +1,11 @@ import 'dart:io'; -import 'package:flutter/material.dart'; - import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; class FlowyButton extends StatelessWidget { final Widget text; diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/decoration.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/decoration.dart index 1a4b96ecb53b9..0902d78835805 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/decoration.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/decoration.dart @@ -7,10 +7,11 @@ class FlowyDecoration { double spreadRadius = 0, double blurRadius = 20, Offset offset = Offset.zero, + double borderRadius = 6, }) { return BoxDecoration( color: boxColor, - borderRadius: const BorderRadius.all(Radius.circular(6)), + borderRadius: BorderRadius.all(Radius.circular(borderRadius)), boxShadow: [ BoxShadow( color: boxShadow, diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart index 65d0c19c59e5c..88ebc735b7b07 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart @@ -16,6 +16,7 @@ class FlowyText extends StatelessWidget { final List? fallbackFontFamily; final double? lineHeight; final bool withTooltip; + final StrutStyle? strutStyle; const FlowyText( this.text, { @@ -32,6 +33,7 @@ class FlowyText extends StatelessWidget { this.fallbackFontFamily, this.lineHeight, this.withTooltip = false, + this.strutStyle, }); FlowyText.small( @@ -47,6 +49,7 @@ class FlowyText extends StatelessWidget { this.fallbackFontFamily, this.lineHeight, this.withTooltip = false, + this.strutStyle, }) : fontWeight = FontWeight.w400, fontSize = (Platform.isIOS || Platform.isAndroid) ? 14 : 12; @@ -64,6 +67,7 @@ class FlowyText extends StatelessWidget { this.fallbackFontFamily, this.lineHeight, this.withTooltip = false, + this.strutStyle, }) : fontWeight = FontWeight.w400; const FlowyText.medium( @@ -80,6 +84,7 @@ class FlowyText extends StatelessWidget { this.fallbackFontFamily, this.lineHeight, this.withTooltip = false, + this.strutStyle, }) : fontWeight = FontWeight.w500; const FlowyText.semibold( @@ -96,6 +101,7 @@ class FlowyText extends StatelessWidget { this.fallbackFontFamily, this.lineHeight, this.withTooltip = false, + this.strutStyle, }) : fontWeight = FontWeight.w600; // Some emojis are not supported on Linux and Android, fallback to noto color emoji @@ -105,12 +111,13 @@ class FlowyText extends StatelessWidget { this.fontSize, this.overflow, this.color, - this.textAlign, + this.textAlign = TextAlign.center, this.maxLines = 1, this.decoration, this.selectable = false, this.lineHeight, this.withTooltip = false, + this.strutStyle = const StrutStyle(forceStrutHeight: true), }) : fontWeight = FontWeight.w400, fontFamily = 'noto color emoji', fallbackFontFamily = null; @@ -119,20 +126,23 @@ class FlowyText extends StatelessWidget { Widget build(BuildContext context) { Widget child; + final textStyle = Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: fontSize, + fontWeight: fontWeight, + color: color, + decoration: decoration, + fontFamily: fontFamily, + fontFamilyFallback: fallbackFontFamily, + height: lineHeight, + ); + if (selectable) { child = SelectableText( text, maxLines: maxLines, textAlign: textAlign, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: fontSize, - fontWeight: fontWeight, - color: color, - decoration: decoration, - fontFamily: fontFamily, - fontFamilyFallback: fallbackFontFamily, - height: lineHeight, - ), + strutStyle: strutStyle, + style: textStyle, ); } else { child = Text( @@ -140,15 +150,7 @@ class FlowyText extends StatelessWidget { maxLines: maxLines, textAlign: textAlign, overflow: overflow ?? TextOverflow.clip, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: fontSize, - fontWeight: fontWeight, - color: color, - decoration: decoration, - fontFamily: fontFamily, - fontFamilyFallback: fallbackFontFamily, - height: lineHeight, - ), + style: textStyle, ); } diff --git a/frontend/resources/flowy_icons/16x/add_cover.svg b/frontend/resources/flowy_icons/16x/add_cover.svg new file mode 100644 index 0000000000000..ac83855416ba8 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/add_cover.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/resources/flowy_icons/16x/add_icon.svg b/frontend/resources/flowy_icons/16x/add_icon.svg new file mode 100644 index 0000000000000..e49b54ec141df --- /dev/null +++ b/frontend/resources/flowy_icons/16x/add_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/16x/add_workspace.svg b/frontend/resources/flowy_icons/16x/add_workspace.svg new file mode 100644 index 0000000000000..83dabfe0a15bb --- /dev/null +++ b/frontend/resources/flowy_icons/16x/add_workspace.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/resources/flowy_icons/16x/change_icon.svg b/frontend/resources/flowy_icons/16x/change_icon.svg new file mode 100644 index 0000000000000..38c7e41710654 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/change_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/16x/collapse_all_page.svg b/frontend/resources/flowy_icons/16x/collapse_all_page.svg new file mode 100644 index 0000000000000..2760daaaef83a --- /dev/null +++ b/frontend/resources/flowy_icons/16x/collapse_all_page.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/frontend/resources/flowy_icons/16x/duplicate.svg b/frontend/resources/flowy_icons/16x/duplicate.svg new file mode 100644 index 0000000000000..883082258975b --- /dev/null +++ b/frontend/resources/flowy_icons/16x/duplicate.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/16x/favorite.svg b/frontend/resources/flowy_icons/16x/favorite.svg index 8ad54bbbb5733..addd7d4915b1e 100644 --- a/frontend/resources/flowy_icons/16x/favorite.svg +++ b/frontend/resources/flowy_icons/16x/favorite.svg @@ -1,3 +1,3 @@ - + diff --git a/frontend/resources/flowy_icons/16x/favorite_header_icon.svg b/frontend/resources/flowy_icons/16x/favorite_header_icon.svg new file mode 100644 index 0000000000000..8296f888f34f7 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/favorite_header_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/16x/favorite_section_pin.svg b/frontend/resources/flowy_icons/16x/favorite_section_pin.svg new file mode 100644 index 0000000000000..0402120e41a12 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/favorite_section_pin.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/16x/favorite_section_remove_from_favorite.svg b/frontend/resources/flowy_icons/16x/favorite_section_remove_from_favorite.svg new file mode 100644 index 0000000000000..b984afe0171ff --- /dev/null +++ b/frontend/resources/flowy_icons/16x/favorite_section_remove_from_favorite.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/16x/favorite_section_unpin.svg b/frontend/resources/flowy_icons/16x/favorite_section_unpin.svg new file mode 100644 index 0000000000000..3e72f90f4b49d --- /dev/null +++ b/frontend/resources/flowy_icons/16x/favorite_section_unpin.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/16x/hide_menu.svg b/frontend/resources/flowy_icons/16x/hide_menu.svg index ce88af8ea7c69..9e301210c45d0 100644 --- a/frontend/resources/flowy_icons/16x/hide_menu.svg +++ b/frontend/resources/flowy_icons/16x/hide_menu.svg @@ -1,6 +1,6 @@ - - - - - + + + + + diff --git a/frontend/resources/flowy_icons/16x/icon_shuffle.svg b/frontend/resources/flowy_icons/16x/icon_shuffle.svg new file mode 100644 index 0000000000000..9953ebe30c450 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/icon_shuffle.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/resources/flowy_icons/16x/more.svg b/frontend/resources/flowy_icons/16x/more.svg index b191e64a10343..da54e4b6e635d 100644 --- a/frontend/resources/flowy_icons/16x/more.svg +++ b/frontend/resources/flowy_icons/16x/more.svg @@ -1,3 +1,7 @@ - - + + + + + + diff --git a/frontend/resources/flowy_icons/16x/move_to.svg b/frontend/resources/flowy_icons/16x/move_to.svg new file mode 100644 index 0000000000000..1c7d6144eede3 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/move_to.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/16x/notification.svg b/frontend/resources/flowy_icons/16x/notification.svg new file mode 100644 index 0000000000000..feb63cb9b319a --- /dev/null +++ b/frontend/resources/flowy_icons/16x/notification.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/16x/search.svg b/frontend/resources/flowy_icons/16x/search.svg index 1efb2d475c980..ed7e6de9d2710 100644 --- a/frontend/resources/flowy_icons/16x/search.svg +++ b/frontend/resources/flowy_icons/16x/search.svg @@ -1,4 +1,6 @@ - - + + + + diff --git a/frontend/resources/flowy_icons/16x/settings.svg b/frontend/resources/flowy_icons/16x/settings.svg index f9896aad5250c..bcc96b817be53 100644 --- a/frontend/resources/flowy_icons/16x/settings.svg +++ b/frontend/resources/flowy_icons/16x/settings.svg @@ -1,4 +1,4 @@ - - + + diff --git a/frontend/resources/flowy_icons/16x/sidebar_footer_trash.svg b/frontend/resources/flowy_icons/16x/sidebar_footer_trash.svg new file mode 100644 index 0000000000000..f412bb86fdc77 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/sidebar_footer_trash.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/resources/flowy_icons/16x/sidebar_footer_widget.svg b/frontend/resources/flowy_icons/16x/sidebar_footer_widget.svg new file mode 100644 index 0000000000000..2dbb6f52cfaab --- /dev/null +++ b/frontend/resources/flowy_icons/16x/sidebar_footer_widget.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/frontend/resources/flowy_icons/16x/three-dots.svg b/frontend/resources/flowy_icons/16x/three-dots.svg index 4d37a346a0ce4..07aa7ca7062f9 100644 --- a/frontend/resources/flowy_icons/16x/three-dots.svg +++ b/frontend/resources/flowy_icons/16x/three-dots.svg @@ -1,3 +1,5 @@ - - + + + + diff --git a/frontend/resources/flowy_icons/16x/title_bar_divider.svg b/frontend/resources/flowy_icons/16x/title_bar_divider.svg new file mode 100644 index 0000000000000..5f92484836db6 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/title_bar_divider.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/16x/trash.svg b/frontend/resources/flowy_icons/16x/trash.svg new file mode 100644 index 0000000000000..487a57001f292 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/trash.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/resources/flowy_icons/16x/unfavorite.svg b/frontend/resources/flowy_icons/16x/unfavorite.svg index 0ccfc1edff86e..b984afe0171ff 100644 --- a/frontend/resources/flowy_icons/16x/unfavorite.svg +++ b/frontend/resources/flowy_icons/16x/unfavorite.svg @@ -1,3 +1,5 @@ - + + + diff --git a/frontend/resources/flowy_icons/16x/view_item_add.svg b/frontend/resources/flowy_icons/16x/view_item_add.svg new file mode 100644 index 0000000000000..9373ef6dd11fa --- /dev/null +++ b/frontend/resources/flowy_icons/16x/view_item_add.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/resources/flowy_icons/16x/view_item_expand.svg b/frontend/resources/flowy_icons/16x/view_item_expand.svg new file mode 100644 index 0000000000000..5fd5a1e719573 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/view_item_expand.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/16x/view_item_open_in_new_tab.svg b/frontend/resources/flowy_icons/16x/view_item_open_in_new_tab.svg new file mode 100644 index 0000000000000..87f9f11949d6b --- /dev/null +++ b/frontend/resources/flowy_icons/16x/view_item_open_in_new_tab.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/16x/view_item_rename.svg b/frontend/resources/flowy_icons/16x/view_item_rename.svg new file mode 100644 index 0000000000000..c890184915301 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/view_item_rename.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/16x/view_item_right_arrow.svg b/frontend/resources/flowy_icons/16x/view_item_right_arrow.svg new file mode 100644 index 0000000000000..de5db7fd68fad --- /dev/null +++ b/frontend/resources/flowy_icons/16x/view_item_right_arrow.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/resources/flowy_icons/16x/view_item_unexpand.svg b/frontend/resources/flowy_icons/16x/view_item_unexpand.svg new file mode 100644 index 0000000000000..8971910251e9d --- /dev/null +++ b/frontend/resources/flowy_icons/16x/view_item_unexpand.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/16x/workspace_drop_down_menu_hide.svg b/frontend/resources/flowy_icons/16x/workspace_drop_down_menu_hide.svg new file mode 100644 index 0000000000000..d264b4734e278 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/workspace_drop_down_menu_hide.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/16x/workspace_drop_down_menu_show.svg b/frontend/resources/flowy_icons/16x/workspace_drop_down_menu_show.svg new file mode 100644 index 0000000000000..4682de7aa7e14 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/workspace_drop_down_menu_show.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/16x/workspace_logout.svg b/frontend/resources/flowy_icons/16x/workspace_logout.svg new file mode 100644 index 0000000000000..6e44da6b1454d --- /dev/null +++ b/frontend/resources/flowy_icons/16x/workspace_logout.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/16x/workspace_selected.svg b/frontend/resources/flowy_icons/16x/workspace_selected.svg new file mode 100644 index 0000000000000..73d86f8e0242b --- /dev/null +++ b/frontend/resources/flowy_icons/16x/workspace_selected.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/resources/flowy_icons/16x/workspace_three_dots.svg b/frontend/resources/flowy_icons/16x/workspace_three_dots.svg new file mode 100644 index 0000000000000..93aa13693a6fb --- /dev/null +++ b/frontend/resources/flowy_icons/16x/workspace_three_dots.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 3f9f4178a06dc..04ab72f795f92 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -70,6 +70,7 @@ "chooseWorkspace": "Choose your workspace", "create": "Create workspace", "reset": "Reset workspace", + "renameWorkspace": "Rename workspace", "resetWorkspacePrompt": "Resetting the workspace will delete all pages and data within it. Are you sure you want to reset the workspace? Alternatively, you can contact the support team to restore the workspace", "hint": "workspace", "notFoundError": "Workspace not found", @@ -136,7 +137,9 @@ "openNewTab": "Open in a new tab", "moveTo": "Move to", "addToFavorites": "Add to Favorites", - "copyLink": "Copy Link" + "copyLink": "Copy Link", + "changeIcon":"Change icon", + "collapseAllPages": "Collapse all pages" }, "blankPageTitle": "Blank page", "newPageText": "New page", @@ -239,7 +242,10 @@ "addAPage": "Add a page", "addAPageToPrivate": "Add a page to private space", "addAPageToWorkspace": "Add a page to workspace", - "recent": "Recent" + "recent": "Recent", + "today": "Today", + "thisWeek": "This week", + "others": "Others" }, "notifications": { "export": { @@ -294,7 +300,8 @@ "back": "Back", "signInGoogle": "Sign in with Google", "signInGithub": "Sign in with Github", - "signInDiscord": "Sign in with Discord" + "signInDiscord": "Sign in with Discord", + "more": "More" }, "label": { "welcome": "Welcome!", @@ -1584,7 +1591,9 @@ }, "favorite": { "noFavorite": "No favorite page", - "noFavoriteHintText": "Swipe the page to the left to add it to your favorites" + "noFavoriteHintText": "Swipe the page to the left to add it to your favorites", + "removeFromSidebar": "Remove from sidebar", + "addToSidebar": "Pin to sidebar" }, "cardDetails": { "notesPlaceholder": "Enter a / to insert a block, or start typing" @@ -1637,6 +1646,7 @@ "workplaceIconSubtitle": "Upload an image or use an emoji for your workspace. Icon will show in your sidebar and notifications.", "renameError": "Failed to rename workplace", "updateIconError": "Failed to update icon", + "chooseAnIcon": "Choose an icon", "appearance": { "name": "Appearance", "themeMode": { diff --git a/frontend/rust-lib/event-integration-test/tests/folder/local_test/script.rs b/frontend/rust-lib/event-integration-test/tests/folder/local_test/script.rs index eb311cfea7ea0..0a2f34ca0a65e 100644 --- a/frontend/rust-lib/event-integration-test/tests/folder/local_test/script.rs +++ b/frontend/rust-lib/event-integration-test/tests/folder/local_test/script.rs @@ -196,7 +196,7 @@ impl FolderTest { }, FolderScript::ReadFavorites => { let favorites = read_favorites(sdk).await; - self.favorites = favorites.to_vec(); + self.favorites = favorites.items.iter().map(|x| x.item.clone()).collect(); }, } } @@ -375,10 +375,10 @@ pub async fn toggle_favorites(sdk: &EventIntegrationTest, view_id: Vec) .await; } -pub async fn read_favorites(sdk: &EventIntegrationTest) -> RepeatedViewPB { +pub async fn read_favorites(sdk: &EventIntegrationTest) -> RepeatedFavoriteViewPB { EventBuilder::new(sdk.clone()) .event(ReadFavorites) .async_send() .await - .parse::() + .parse::() } diff --git a/frontend/rust-lib/flowy-folder/src/entities/view.rs b/frontend/rust-lib/flowy-folder/src/entities/view.rs index 004f793e11b69..466d30d06d288 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/view.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/view.rs @@ -152,6 +152,20 @@ pub struct RepeatedViewPB { pub items: Vec, } +#[derive(Eq, PartialEq, Debug, Default, ProtoBuf, Clone)] +pub struct RepeatedFavoriteViewPB { + #[pb(index = 1)] + pub items: Vec, +} + +#[derive(Eq, PartialEq, Debug, Default, ProtoBuf, Clone)] +pub struct FavoriteViewPB { + #[pb(index = 1)] + pub item: ViewPB, + #[pb(index = 2)] + pub timestamp: i64, +} + impl std::convert::From> for RepeatedViewPB { fn from(items: Vec) -> Self { RepeatedViewPB { items } diff --git a/frontend/rust-lib/flowy-folder/src/event_handler.rs b/frontend/rust-lib/flowy-folder/src/event_handler.rs index d6d36b683ebb7..06daab5a2fd8f 100644 --- a/frontend/rust-lib/flowy-folder/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder/src/event_handler.rs @@ -278,16 +278,19 @@ pub(crate) async fn duplicate_view_handler( #[tracing::instrument(level = "debug", skip(folder), err)] pub(crate) async fn read_favorites_handler( folder: AFPluginState>, -) -> DataResult { +) -> DataResult { let folder = upgrade_folder(folder)?; let favorite_items = folder.get_all_favorites().await; let mut views = vec![]; for item in favorite_items { if let Ok(view) = folder.get_view_pb(&item.id).await { - views.push(view); + views.push(FavoriteViewPB { + item: view, + timestamp: item.timestamp, + }); } } - data_result_ok(RepeatedViewPB { items: views }) + data_result_ok(RepeatedFavoriteViewPB { items: views }) } #[tracing::instrument(level = "debug", skip(folder), err)] diff --git a/frontend/rust-lib/flowy-folder/src/event_map.rs b/frontend/rust-lib/flowy-folder/src/event_map.rs index 31034bd1437f5..2901d19a635ec 100644 --- a/frontend/rust-lib/flowy-folder/src/event_map.rs +++ b/frontend/rust-lib/flowy-folder/src/event_map.rs @@ -146,7 +146,7 @@ pub enum FolderEvent { #[event(input = "MoveNestedViewPayloadPB")] MoveNestedView = 32, - #[event(output = "RepeatedViewPB")] + #[event(output = "RepeatedFavoriteViewPB")] ReadFavorites = 33, #[event(input = "RepeatedViewIdPB")] diff --git a/frontend/scripts/tool/update_collab_source.sh b/frontend/scripts/tool/update_collab_source.sh index 29892de1de9d2..094e5caf14abd 100755 --- a/frontend/scripts/tool/update_collab_source.sh +++ b/frontend/scripts/tool/update_collab_source.sh @@ -17,7 +17,7 @@ switch_deps() { # Switch to local paths for crate in collab collab-folder collab-document collab-database collab-plugins collab-user collab-entity collab-sync-protocol collab-persistence; do sed -i '' \ - -e "s#${crate} = { git = \"https://github.com/AppFlowy-IO/AppFlowy-Collab\", rev = \"[a-f0-9]*\" }#${crate} = { path = \"$repo_path/$crate\" }#g" \ + -e "s#${crate} = { .*git = \"https://github.com/AppFlowy-IO/AppFlowy-Collab\".* }#${crate} = { path = \"$repo_path/$crate\" }#g" \ "$cargo_toml" done echo "Switched to local paths in $cargo_toml." @@ -38,4 +38,4 @@ fi # Switch dependencies in both Cargo.toml files switch_deps "$CARGO_TOML_1" "$REPO_RELATIVE_PATH_1" -switch_deps "$CARGO_TOML_2" "$REPO_RELATIVE_PATH_2" +switch_deps "$CARGO_TOML_2" "$REPO_RELATIVE_PATH_2" \ No newline at end of file