diff --git a/.github/workflows/android_ci.yaml.bak b/.github/workflows/android_ci.yaml.bak index 8732558927..0cb110bae4 100644 --- a/.github/workflows/android_ci.yaml.bak +++ b/.github/workflows/android_ci.yaml.bak @@ -19,7 +19,7 @@ # env: # CARGO_TERM_COLOR: always -# FLUTTER_VERSION: "3.19.0" +# FLUTTER_VERSION: "3.22.0" # RUST_TOOLCHAIN: "1.77.2" # CARGO_MAKE_VERSION: "0.36.6" diff --git a/.github/workflows/flutter_ci.yaml b/.github/workflows/flutter_ci.yaml index 6ec1c76682..b944187ec4 100644 --- a/.github/workflows/flutter_ci.yaml +++ b/.github/workflows/flutter_ci.yaml @@ -25,7 +25,7 @@ on: env: CARGO_TERM_COLOR: always - FLUTTER_VERSION: "3.19.0" + FLUTTER_VERSION: "3.22.0" RUST_TOOLCHAIN: "1.77.2" CARGO_MAKE_VERSION: "0.36.6" diff --git a/.github/workflows/ios_ci.yaml b/.github/workflows/ios_ci.yaml index c32a7f93c7..d24eaed1f3 100644 --- a/.github/workflows/ios_ci.yaml +++ b/.github/workflows/ios_ci.yaml @@ -20,7 +20,7 @@ on: - "!frontend/appflowy_web_app/**" env: - FLUTTER_VERSION: "3.19.0" + FLUTTER_VERSION: "3.22.0" RUST_TOOLCHAIN: "1.77.2" concurrency: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ee065c6e9e..e91d95f969 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,7 @@ on: - "*" env: - FLUTTER_VERSION: "3.19.0" + FLUTTER_VERSION: "3.22.0" RUST_TOOLCHAIN: "1.77.2" jobs: diff --git a/.github/workflows/rust_coverage.yml b/.github/workflows/rust_coverage.yml index 4d8e9cbad8..12e728698f 100644 --- a/.github/workflows/rust_coverage.yml +++ b/.github/workflows/rust_coverage.yml @@ -10,7 +10,7 @@ on: env: CARGO_TERM_COLOR: always - FLUTTER_VERSION: "3.19.0" + FLUTTER_VERSION: "3.22.0" RUST_TOOLCHAIN: "1.77.2" jobs: diff --git a/CHANGELOG.md b/CHANGELOG.md index 550cd2bbce..0cc8d05b68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,21 @@ # Release Notes +## Version 0.5.8 - 05/20/2024 +### New Features +- Improvement to the Callout block to insert new lines +- New settings page "Manage data" replaced the "Files" page +- New settings page "Workspace" replaced the "Appearance" and "Language" pages +- A custom implementation of a title bar for Windows users +- Added support for selecting Cards in kanban and performing grouped keyboard shortcuts +- Added support for default system font family +- Support for scaling the application up/down using a keyboard shortcut (CMD/CTRL + PLUS/MINUS) + +### Bug Fixes +- Resolved and refined the UI on Mobile +- Resolved issue with text editing in database +- Improved appearance of empty text cells in kanban/calendar +- Resolved an issue where a page's more actions (delete, duplicate) did not work properly +- Resolved and inconsistency in padding on get started screen on Desktop + ## Version 0.5.7 - 05/10/2024 ### Bug Fixes - Resolved page opening issue on Android. @@ -88,7 +105,7 @@ - Fixed a bug where newly created rows were not being automatically sorted. - Fixed issues related to deleting a sorting field or sort not removing existing sorts properly. ### Notes -- Windows 7, Windows 8, and iOS 11 are not yet supported due to the upgrade to Flutter 3.19.0. +- Windows 7, Windows 8, and iOS 11 are not yet supported due to the upgrade to Flutter 3.22.0. ## Version 0.4.9 - 02/17/2024 ### Bug Fixes diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml index d5fad27238..5a962f3cbf 100644 --- a/frontend/Makefile.toml +++ b/frontend/Makefile.toml @@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true CARGO_MAKE_CRATE_FS_NAME = "dart_ffi" CARGO_MAKE_CRATE_NAME = "dart-ffi" LIB_NAME = "dart_ffi" -APPFLOWY_VERSION = "0.5.7" +APPFLOWY_VERSION = "0.5.8" FLUTTER_DESKTOP_FEATURES = "dart" PRODUCT_NAME = "AppFlowy" MACOSX_DEPLOYMENT_TARGET = "11.0" 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 11af75af1d..9886c2228e 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_field_test.dart b/frontend/appflowy_flutter/integration_test/desktop/board/board_field_test.dart new file mode 100644 index 0000000000..3fe48b5f6f --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/desktop/board/board_field_test.dart @@ -0,0 +1,37 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../../shared/database_test_op.dart'; +import '../../shared/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('board field test', () { + testWidgets('change field type whithin card #5360', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + + await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board); + const name = 'Card 1'; + final card1 = find.text(name); + await tester.tapButton(card1); + + const fieldName = "test change field"; + await tester.createField( + FieldType.RichText, + fieldName, + layout: ViewLayoutPB.Board, + ); + await tester.tapButton(card1); + await tester.changeFieldTypeOfFieldWithName( + fieldName, + FieldType.Checkbox, + layout: ViewLayoutPB.Board, + ); + await tester.hoverOnWidget(find.text('Card 2')); + }); + }); +} 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 932c266bda..a786367fff 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_alignment_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_alignment_test.dart index de7401652e..d95d907881 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_alignment_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_alignment_test.dart @@ -66,6 +66,7 @@ void main() { LogicalKeyboardKey.keyR, ], tester: tester, + withKeyUp: true, ); expect(first.attributes[blockComponentAlign], rightAlignmentKey); @@ -77,6 +78,7 @@ void main() { LogicalKeyboardKey.keyE, ], tester: tester, + withKeyUp: true, ); expect(first.attributes[blockComponentAlign], centerAlignmentKey); @@ -88,6 +90,7 @@ void main() { LogicalKeyboardKey.keyL, ], tester: tester, + withKeyUp: true, ); expect(first.attributes[blockComponentAlign], leftAlignmentKey); }); diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_inline_page_reference_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_inline_page_reference_test.dart index 0bbd64c82b..f169910840 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_inline_page_reference_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_inline_page_reference_test.dart @@ -111,6 +111,7 @@ Future triggerReferenceDocumentBySlashMenu(WidgetTester tester) async { LogicalKeyboardKey.enter, ], tester: tester, + withKeyUp: true, ); await tester.pumpAndSettle(); @@ -129,6 +130,7 @@ Future enterDocumentText(WidgetTester tester) async { LogicalKeyboardKey.keyT, ], tester: tester, + withKeyUp: true, ); await tester.pumpAndSettle(); } 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 new file mode 100644 index 0000000000..d4cc11d7f0 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_more_actions_test.dart @@ -0,0 +1,34 @@ +import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../../shared/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('MoreViewActions', () { + testWidgets('can duplicate and delete from menu', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + await tester.pumpAndSettle(); + + final pageFinder = find.byType(ViewItem); + expect(pageFinder, findsNWidgets(1)); + + // 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_test_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner.dart index 42462c2658..239e7e09a8 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner.dart @@ -6,6 +6,9 @@ import 'document_copy_and_paste_test.dart' as document_copy_and_paste_test; import 'document_create_and_delete_test.dart' as document_create_and_delete_test; import 'document_option_action_test.dart' as document_option_action_test; +import 'document_inline_page_reference_test.dart' + as document_inline_page_reference_test; +import 'document_more_actions_test.dart' as document_more_actions_test; import 'document_text_direction_test.dart' as document_text_direction_test; import 'document_with_cover_image_test.dart' as document_with_cover_image_test; import 'document_with_database_test.dart' as document_with_database_test; @@ -16,8 +19,6 @@ import 'document_with_inline_page_test.dart' as document_with_inline_page_test; import 'document_with_outline_block_test.dart' as document_with_outline_block; import 'document_with_toggle_list_test.dart' as document_with_toggle_list_test; import 'edit_document_test.dart' as document_edit_test; -import 'document_inline_page_reference_test.dart' - as document_inline_page_reference_test; void startTesting() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -38,4 +39,5 @@ void startTesting() { document_option_action_test.main(); document_with_image_block_test.main(); document_inline_page_reference_test.main(); + document_more_actions_test.main(); } 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 2b42ef7451..f7a88d7bec 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 ff0df1c7da..f2a1fae8ae 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 072764217c..729ee62a3e 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 35bcf599ab..3bc41d78c0 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 72bf8a4fae..f1025b8f1e 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 08d6fd0ec6..8bc7236994 100644 --- a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart +++ b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart @@ -11,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'; @@ -22,6 +22,8 @@ import 'package:appflowy/workspace/presentation/notifications/widgets/flowy_tab. import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart'; import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; +import 'package:appflowy/workspace/presentation/widgets/more_view_actions/more_view_actions.dart'; +import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart'; import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; @@ -57,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, @@ -276,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, @@ -564,6 +567,44 @@ extension CommonOperations on WidgetTester { ); await tapButton(button); } + + Future openMoreViewActions() async { + final button = find.byType(MoreViewActions); + await tap(button); + await pumpAndSettle(); + } + + /// Presses on the Duplicate ViewAction in the [MoreViewActions] popup. + /// + /// [openMoreViewActions] must be called beforehand! + /// + Future duplicateByMoreViewActions() async { + final button = find.descendant( + of: find.byType(ListView), + matching: find.byWidgetPredicate( + (widget) => + widget is ViewAction && widget.type == ViewActionType.duplicate, + ), + ); + await tap(button); + await pump(); + } + + /// Presses on the Delete ViewAction in the [MoreViewActions] popup. + /// + /// [openMoreViewActions] must be called beforehand! + /// + Future deleteByMoreViewActions() async { + final button = find.descendant( + of: find.byType(ListView), + matching: find.byWidgetPredicate( + (widget) => + widget is ViewAction && widget.type == ViewActionType.delete, + ), + ); + await tap(button); + await pump(); + } } extension SettingsFinder on CommonFinders { diff --git a/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart b/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart index 8697b832c0..8277b4db97 100644 --- a/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart @@ -661,10 +661,13 @@ extension AppFlowyDatabaseTest on WidgetTester { Future changeFieldTypeOfFieldWithName( String name, - FieldType type, - ) async { + FieldType type, { + ViewLayoutPB layout = ViewLayoutPB.Grid, + }) async { await tapGridFieldWithName(name); - await tapEditFieldButton(); + if (layout == ViewLayoutPB.Grid) { + await tapEditFieldButton(); + } await tapSwitchFieldTypeButton(); await selectFieldType(type); @@ -881,8 +884,14 @@ extension AppFlowyDatabaseTest on WidgetTester { await tapButtonWithName(LocaleKeys.grid_row_delete.tr()); } - Future createField(FieldType fieldType, String name) async { - await scrollToRight(find.byType(GridPage)); + Future createField( + FieldType fieldType, + String name, { + ViewLayoutPB layout = ViewLayoutPB.Grid, + }) async { + if (layout == ViewLayoutPB.Grid) { + await scrollToRight(find.byType(GridPage)); + } await tapNewPropertyButton(); await renameField(name); await tapSwitchFieldTypeButton(); 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 0bdcf06367..4eff62321a 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 aeb3f04cb8..c4b54a0fda 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/keyboard.dart b/frontend/appflowy_flutter/integration_test/shared/keyboard.dart index d792b92c66..567e7e548c 100644 --- a/frontend/appflowy_flutter/integration_test/shared/keyboard.dart +++ b/frontend/appflowy_flutter/integration_test/shared/keyboard.dart @@ -5,10 +5,18 @@ class FlowyTestKeyboard { static Future simulateKeyDownEvent( List keys, { required flutter_test.WidgetTester tester, + bool withKeyUp = false, }) async { for (final LogicalKeyboardKey key in keys) { await flutter_test.simulateKeyDownEvent(key); await tester.pumpAndSettle(); } + + if (withKeyUp) { + for (final LogicalKeyboardKey key in keys) { + await flutter_test.simulateKeyUpEvent(key); + await tester.pumpAndSettle(); + } + } } } diff --git a/frontend/appflowy_flutter/integration_test/shared/settings.dart b/frontend/appflowy_flutter/integration_test/shared/settings.dart index dd13bd088f..3b25c32111 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 5137944364..4d20d88ce1 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/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index e62299792d..93a8eb77e1 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -173,7 +173,7 @@ SPEC CHECKSUMS: fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265 image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425 - integration_test: 13825b8a9334a850581300559b8839134b124670 + integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4 irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9 keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86 package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c diff --git a/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart b/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart index da9f4649c3..9d18bb14f7 100644 --- a/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart +++ b/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart @@ -481,7 +481,7 @@ class _AFDropdownMenuState extends State> { ButtonStyle effectiveStyle = entry.style ?? defaultStyle; final Color focusedBackgroundColor = effectiveStyle.foregroundColor - ?.resolve({MaterialState.focused}) ?? + ?.resolve({WidgetState.focused}) ?? Theme.of(context).colorScheme.onSurface; Widget label = entry.labelWidget ?? Text(entry.label); @@ -499,7 +499,7 @@ class _AFDropdownMenuState extends State> { // color will also change to foregroundColor.withOpacity(0.12). effectiveStyle = entry.enabled && i == focusedIndex ? effectiveStyle.copyWith( - backgroundColor: MaterialStatePropertyAll( + backgroundColor: WidgetStatePropertyAll( focusedBackgroundColor.withOpacity(0.12), ), ) @@ -628,17 +628,17 @@ class _AFDropdownMenuState extends State> { final double? anchorWidth = getWidth(_anchorKey); if (widget.width != null) { effectiveMenuStyle = effectiveMenuStyle.copyWith( - minimumSize: MaterialStatePropertyAll(Size(widget.width!, 0.0)), + minimumSize: WidgetStatePropertyAll(Size(widget.width!, 0.0)), ); } else if (anchorWidth != null) { effectiveMenuStyle = effectiveMenuStyle.copyWith( - minimumSize: MaterialStatePropertyAll(Size(anchorWidth, 0.0)), + minimumSize: WidgetStatePropertyAll(Size(anchorWidth, 0.0)), ); } if (widget.menuHeight != null) { effectiveMenuStyle = effectiveMenuStyle.copyWith( - maximumSize: MaterialStatePropertyAll( + maximumSize: WidgetStatePropertyAll( Size(double.infinity, widget.menuHeight!), ), ); @@ -1029,8 +1029,8 @@ class _DropdownMenuDefaultsM3 extends DropdownMenuThemeData { @override MenuStyle get menuStyle { return const MenuStyle( - minimumSize: MaterialStatePropertyAll(Size(_kMinimumWidth, 0.0)), - maximumSize: MaterialStatePropertyAll(Size.infinite), + minimumSize: WidgetStatePropertyAll(Size(_kMinimumWidth, 0.0)), + maximumSize: WidgetStatePropertyAll(Size.infinite), visualDensity: VisualDensity.standard, ); } 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 04149c8238..497f769354 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/app_bar_buttons.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/app_bar_buttons.dart index d712aa5aec..1301719c41 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/app_bar_buttons.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/app_bar_buttons.dart @@ -12,6 +12,7 @@ import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/view/prelude.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:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -92,7 +93,7 @@ class MobileViewPageMoreButton extends StatelessWidget { context, showDragHandle: true, showDivider: false, - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: AFThemeExtension.of(context).background, builder: (_) => MultiBlocProvider( providers: [ BlocProvider.value(value: context.read()), @@ -144,7 +145,7 @@ class MobileViewPageLayoutButton extends StatelessWidget { showDoneButton: true, showHeader: true, title: LocaleKeys.pageStyle_title.tr(), - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: AFThemeExtension.of(context).background, builder: (_) => MultiBlocProvider( providers: [ BlocProvider.value(value: context.read()), 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 b94242eceb..6394ca9647 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/bottom_sheet_action_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart index 3e594b47f9..6b54b1fda3 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart @@ -1,4 +1,5 @@ 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'; @@ -19,7 +20,7 @@ class BottomSheetActionWidget extends StatelessWidget { @override Widget build(BuildContext context) { final iconColor = - this.iconColor ?? Theme.of(context).colorScheme.onBackground; + this.iconColor ?? AFThemeExtension.of(context).onBackground; if (svg == null) { return OutlinedButton( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart index f27c5b3b6f..80330de3c7 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart @@ -3,6 +3,7 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/page_item/mobile_slide_action_button.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; @@ -54,7 +55,7 @@ enum MobilePaneActionType { context, showDragHandle: true, showDivider: false, - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: AFThemeExtension.of(context).background, useRootNavigator: true, builder: (context) { return MultiBlocProvider( 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 718ac5c4e6..be815b6550 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/database/board/mobile_board_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_page.dart index 7938f47462..a1fc2a70a3 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_page.dart @@ -15,6 +15,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_board/appflowy_board.dart'; import 'package:easy_localization/easy_localization.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_bloc/flutter_bloc.dart'; @@ -265,7 +266,7 @@ class _BoardContentState extends State<_BoardContent> { BoxDecoration _makeBoxDecoration(BuildContext context) { final themeMode = context.read().state.themeMode; return BoxDecoration( - color: Theme.of(context).colorScheme.background, + color: AFThemeExtension.of(context).background, borderRadius: const BorderRadius.all(Radius.circular(8)), border: themeMode == ThemeMode.light ? Border.fromBorderSide( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_board_trailing.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_board_trailing.dart index 76deaa6f0a..184bd901c1 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_board_trailing.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_board_trailing.dart @@ -60,7 +60,7 @@ class _MobileBoardTrailingState extends State { child: IconButton( icon: Icon( Icons.close, - color: style.colorScheme.onBackground, + color: style.colorScheme.onSurface, ), onPressed: () => setState(() => _textController.clear()), @@ -86,7 +86,7 @@ class _MobileBoardTrailingState extends State { child: Text( LocaleKeys.button_cancel.tr(), style: style.textTheme.titleSmall?.copyWith( - color: style.colorScheme.onBackground, + color: style.colorScheme.onSurface, ), ), onPressed: () => setState(() => isEditing = false), @@ -96,7 +96,7 @@ class _MobileBoardTrailingState extends State { LocaleKeys.button_add.tr(), style: style.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.bold, - color: style.colorScheme.onBackground, + color: style.colorScheme.onSurface, ), ), onPressed: () { @@ -117,14 +117,14 @@ class _MobileBoardTrailingState extends State { ) : ElevatedButton.icon( style: ElevatedButton.styleFrom( - foregroundColor: style.colorScheme.onBackground, + foregroundColor: style.colorScheme.onSurface, backgroundColor: style.colorScheme.secondary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ).copyWith( overlayColor: - MaterialStateProperty.all(Theme.of(context).hoverColor), + WidgetStateProperty.all(Theme.of(context).hoverColor), ), icon: const Icon(Icons.add), label: Text( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_hidden_groups_column.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_hidden_groups_column.dart index 2e2367b9bb..0b0c16f951 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_hidden_groups_column.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_hidden_groups_column.dart @@ -9,6 +9,7 @@ import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/text_c import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.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_bloc/flutter_bloc.dart'; @@ -168,7 +169,7 @@ class MobileHiddenGroup extends StatelessWidget { return TextButton( style: TextButton.styleFrom( textStyle: Theme.of(context).textTheme.bodyMedium, - foregroundColor: Theme.of(context).colorScheme.onBackground, + foregroundColor: AFThemeExtension.of(context).onBackground, visualDensity: VisualDensity.compact, ), child: CardCellBuilder( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart index e3cad565ab..08f6c0b48b 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart @@ -20,6 +20,7 @@ import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart' import 'package:appflowy/plugins/database/widgets/row/row_property.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; import 'package:easy_localization/easy_localization.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_bloc/flutter_bloc.dart'; @@ -131,7 +132,7 @@ class _MobileRowDetailPageState extends State { void _showCardActions(BuildContext context) { showMobileBottomSheet( context, - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: AFThemeExtension.of(context).background, showDragHandle: true, builder: (_) => Column( mainAxisSize: MainAxisSize.min, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_create_field_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_create_field_button.dart index e62ddeb872..cc6c9b43aa 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_create_field_button.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_create_field_button.dart @@ -22,17 +22,17 @@ class MobileRowDetailCreateFieldButton extends StatelessWidget { constraints: const BoxConstraints(minWidth: double.infinity), child: TextButton.icon( style: Theme.of(context).textButtonTheme.style?.copyWith( - shape: MaterialStateProperty.all( + shape: WidgetStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(12.0), ), ), - overlayColor: MaterialStateProperty.all( + overlayColor: WidgetStateProperty.all( Theme.of(context).hoverColor, ), alignment: AlignmentDirectional.centerStart, splashFactory: NoSplash.splashFactory, - padding: const MaterialStatePropertyAll( + padding: const WidgetStatePropertyAll( EdgeInsets.symmetric(vertical: 14, horizontal: 6), ), ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/mobile_card_content.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/mobile_card_content.dart index 057d3937cb..48d8b2f097 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/mobile_card_content.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/mobile_card_content.dart @@ -1,5 +1,5 @@ -import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/widgets/card/card.dart'; +import 'package:appflowy/plugins/database/widgets/card/card_bloc.dart'; import 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart'; import 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; @@ -16,7 +16,7 @@ class MobileCardContent extends StatelessWidget { final RowMetaPB rowMeta; final CardCellBuilder cellBuilder; - final List cells; + final List cells; final RowCardStyleConfiguration styleConfiguration; @override @@ -26,9 +26,9 @@ class MobileCardContent extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: cells.map( - (cellContext) { + (cellMeta) { return cellBuilder.build( - cellContext: cellContext, + cellContext: cellMeta.cellContext(), styleMap: mobileBoardCardCellStyleMap(context), hasNotes: !rowMeta.isDocumentEmpty, ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_bottom_sheets.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_bottom_sheets.dart index a4ef722ea7..266a06de7f 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_bottom_sheets.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_bottom_sheets.dart @@ -7,6 +7,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.da import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -40,7 +41,7 @@ Future showFieldTypeGridBottomSheet( showCloseButton: true, elevation: 20, title: title, - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: AFThemeExtension.of(context).background, enableDraggableScrollable: true, builder: (context) { final typeOptionMenuItemValue = mobileSupportedFieldTypes diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_sort_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_sort_bottom_sheet.dart index 909018d1b1..8dd224390c 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_sort_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_sort_bottom_sheet.dart @@ -438,7 +438,7 @@ class _SortDetailContent extends StatelessWidget { color: Theme.of(context).colorScheme.surface, ), splashFactory: NoSplash.splashFactory, - overlayColor: const MaterialStatePropertyAll( + overlayColor: const WidgetStatePropertyAll( Colors.transparent, ), onTap: (index) { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_list.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_list.dart index 9ef8ddefb1..763da36918 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_list.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_list.dart @@ -11,6 +11,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.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_bloc/flutter_bloc.dart'; @@ -183,7 +184,7 @@ class MobileDatabaseViewListButton extends StatelessWidget { showMobileBottomSheet( context, showDragHandle: true, - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: AFThemeExtension.of(context).background, builder: (_) { return BlocProvider( create: (_) => diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_screen.dart index a2771ece26..4d8acbbeba 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_screen.dart @@ -234,7 +234,6 @@ class DatabaseViewSettingTile extends StatelessWidget { showHeader: true, showBackButton: true, title: LocaleKeys.grid_settings_properties.tr(), - showDivider: true, builder: (_) { return BlocProvider.value( value: context.read(), 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 c56d369676..d683cf3507 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 7631383faa..6e695ea0e4 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 1759d32aaf..be47ef1b32 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/mobile_home_trash_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart index a9c2f1b933..2ee9e14175 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart @@ -178,7 +178,7 @@ class _DeletedFilesListView extends StatelessWidget { title: Text( deletedFile.name, style: theme.textTheme.labelMedium - ?.copyWith(color: theme.colorScheme.onBackground), + ?.copyWith(color: theme.colorScheme.onSurface), ), horizontalTitleGap: 0, tileColor: theme.colorScheme.onSurface.withOpacity(0.1), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart index 4dc9f28155..fa585903f3 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.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'; @@ -9,7 +7,9 @@ import 'package:appflowy/workspace/application/recent/prelude.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:easy_localization/easy_localization.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_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -91,7 +91,7 @@ class _RecentViews extends StatelessWidget { context, showDivider: false, showDragHandle: true, - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: AFThemeExtension.of(context).background, builder: (_) { return Column( children: [ 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 c9ea1453c9..4d9d109d3f 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 4745b248c3..496c5607a5 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 34fd517613..862ce794b6 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/mobile/presentation/setting/appearance/rtl_setting.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/rtl_setting.dart index 39a0fdae4c..e61281c5c4 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/rtl_setting.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/rtl_setting.dart @@ -37,7 +37,6 @@ class RTLSetting extends StatelessWidget { showHeader: true, showDragHandle: true, showDivider: false, - showCloseButton: false, title: LocaleKeys.settings_appearance_textDirection_label.tr(), builder: (context) { final layoutDirection = diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/text_scale_setting.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/text_scale_setting.dart index e3526c3df0..3bdb836a71 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/text_scale_setting.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/text_scale_setting.dart @@ -45,7 +45,6 @@ class TextScaleSetting extends StatelessWidget { showHeader: true, showDragHandle: true, showDivider: false, - showCloseButton: false, title: LocaleKeys.settings_appearance_fontScaleFactor.tr(), builder: (context) { return FontSizeStepper( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/theme_setting.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/theme_setting.dart index 1291804af6..8893eab105 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/theme_setting.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/theme_setting.dart @@ -38,7 +38,6 @@ class ThemeSetting extends StatelessWidget { showHeader: true, showDragHandle: true, showDivider: false, - showCloseButton: false, title: LocaleKeys.settings_appearance_themeMode_label.tr(), builder: (context) { final themeMode = diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_picker_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_picker_screen.dart index 64dd62729c..390f0824de 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_picker_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_picker_screen.dart @@ -29,9 +29,7 @@ class FontPickerScreen extends StatelessWidget { } class LanguagePickerPage extends StatefulWidget { - const LanguagePickerPage({ - super.key, - }); + const LanguagePickerPage({super.key}); @override State createState() => _LanguagePickerPageState(); @@ -43,7 +41,6 @@ class _LanguagePickerPageState extends State { @override void initState() { super.initState(); - availableFonts = _availableFonts; } @@ -90,7 +87,6 @@ class _FontSelectorState extends State { @override void initState() { super.initState(); - availableFonts = _availableFonts; } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart index 17b61849da..ba2d2b00cc 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart @@ -29,7 +29,7 @@ class MobileQuickActionButton extends StatelessWidget { onTap: enable ? onTap : null, borderRadius: BorderRadius.circular(12), overlayColor: - enable ? null : const MaterialStatePropertyAll(Colors.transparent), + enable ? null : const WidgetStatePropertyAll(Colors.transparent), splashColor: Colors.transparent, child: Container( height: 44, 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 1297bccc37..d7a7b9ceb2 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart @@ -1,12 +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: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; @@ -27,7 +25,6 @@ class FlowyEmojiPicker extends StatefulWidget { class _FlowyEmojiPickerState extends State { EmojiData? emojiData; - List? fallbackFontFamily; @override void initState() { @@ -46,13 +43,6 @@ class _FlowyEmojiPickerState extends State { }, ); } - - if (Platform.isAndroid || Platform.isLinux) { - final notoColorEmoji = GoogleFonts.notoColorEmoji().fontFamily; - if (notoColorEmoji != null) { - fallbackFontFamily = [notoColorEmoji]; - } - } } @override @@ -82,14 +72,18 @@ class _FlowyEmojiPickerState extends State { ); }, itemBuilder: (context, emojiId, emoji, callback) { - return FlowyIconButton( - iconPadding: const EdgeInsets.all(2.0), - 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 9619f00d30..9f05c80f09 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 1b01e6aee8..c6cec89ecc 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 e8da112660..3add90773d 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( - icon, - fontSize: 22.0, - ), + 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: 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 e9fed800d4..5481c7676a 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 a77b4b2f27..08e63251e0 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: MaterialStatePropertyAll( - Theme.of(context).colorScheme.secondary, - ), - padding: EdgeInsets.zero, - tabs: [ - FlowyHover( - style: const HoverStyle(borderRadius: BorderRadius.zero), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 8.0, - ), - child: FlowyText(LocaleKeys.emoji_emojiTab.tr()), + 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/board/presentation/board_page.dart b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart index 64415ff85b..a1f3a78f75 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart @@ -20,6 +20,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_board/appflowy_board.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.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/error_page.dart'; @@ -671,7 +672,7 @@ class _BoardCardState extends State<_BoardCard> { ? const Color(0x0F1F2329) : const Color(0x0FEFF4FB), foregroundColorOnHover: - Theme.of(context).colorScheme.onBackground, + AFThemeExtension.of(context).onBackground, ), ), onStartEditing: () => diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_day.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_day.dart index 5d1a29141e..6dfa3313f5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_day.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_day.dart @@ -238,7 +238,7 @@ class NewEventButton extends StatelessWidget { child: FlowyIconButton( onPressed: onCreate, icon: const FlowySvg(FlowySvgs.add_s), - fillColor: Theme.of(context).colorScheme.background, + fillColor: Theme.of(context).colorScheme.surface, hoverColor: AFThemeExtension.of(context).lightGreyHover, width: 22, tooltipText: LocaleKeys.calendar_newEventButtonTooltip.tr(), @@ -289,8 +289,8 @@ class _DayBadge extends StatelessWidget { @override Widget build(BuildContext context) { - Color dayTextColor = Theme.of(context).colorScheme.onBackground; - Color monthTextColor = Theme.of(context).colorScheme.onBackground; + Color dayTextColor = AFThemeExtension.of(context).onBackground; + Color monthTextColor = AFThemeExtension.of(context).onBackground; final String monthString = DateFormat("MMM ", context.locale.toLanguageTag()).format(date); final String dayString = date.day.toString(); diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_card.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_card.dart index 6baf2ecd2f..e105914908 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_card.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_card.dart @@ -8,6 +8,7 @@ import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flutter/material.dart'; @@ -15,7 +16,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import '../application/calendar_bloc.dart'; - import 'calendar_event_editor.dart'; class EventCard extends StatefulWidget { @@ -102,7 +102,7 @@ class _EventCardState extends State { hoverColor: Theme.of(context).brightness == Brightness.light ? const Color(0x0F1F2329) : const Color(0x0FEFF4FB), - foregroundColorOnHover: Theme.of(context).colorScheme.onBackground, + foregroundColorOnHover: AFThemeExtension.of(context).onBackground, ), ), onStartEditing: () {}, diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart index c38c7647ba..ff695eea0e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.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'; @@ -21,12 +19,12 @@ import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.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:go_router/go_router.dart'; import '../../application/row/row_controller.dart'; import '../../widgets/row/row_detail.dart'; - import 'calendar_day.dart'; import 'layout/sizes.dart'; import 'toolbar/calendar_setting_bar.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/mobile_fab.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/mobile_fab.dart index 29af8e4355..7c18bb927f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/mobile_fab.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/mobile_fab.dart @@ -43,7 +43,7 @@ Widget getGridFabs(BuildContext context) { .read() .add(const GridEvent.createRow(openRowDetail: true)); }, - overlayColor: const MaterialStatePropertyAll(Color(0xFF009FD1)), + overlayColor: const WidgetStatePropertyAll(Color(0xFF009FD1)), boxShadow: const BoxShadow( offset: Offset(0, 8), color: Color(0x6612BFEF), @@ -75,7 +75,7 @@ class MobileGridFab extends StatelessWidget { final VoidCallback onTap; final FlowySvgData icon; final Size iconSize; - final MaterialStateProperty? overlayColor; + final WidgetStateProperty? overlayColor; @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/mobile/mobile_tab_bar_header.dart b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/mobile/mobile_tab_bar_header.dart index 55394ec33c..ddddd90fb4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/mobile/mobile_tab_bar_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/mobile/mobile_tab_bar_header.dart @@ -77,22 +77,22 @@ class _DatabaseViewSelectorButton extends StatelessWidget { return TextButton( style: ButtonStyle( - padding: const MaterialStatePropertyAll( + padding: const WidgetStatePropertyAll( EdgeInsets.fromLTRB(12, 8, 8, 8), ), - maximumSize: const MaterialStatePropertyAll(Size(200, 48)), - minimumSize: const MaterialStatePropertyAll(Size(48, 0)), - shape: const MaterialStatePropertyAll( + maximumSize: const WidgetStatePropertyAll(Size(200, 48)), + minimumSize: const WidgetStatePropertyAll(Size(48, 0)), + shape: const WidgetStatePropertyAll( RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(12)), ), ), - backgroundColor: MaterialStatePropertyAll( + backgroundColor: WidgetStatePropertyAll( Theme.of(context).brightness == Brightness.light ? const Color(0x0F212729) : const Color(0x0FFFFFFF), ), - overlayColor: MaterialStatePropertyAll( + overlayColor: WidgetStatePropertyAll( Theme.of(context).colorScheme.secondary, ), ), @@ -119,7 +119,6 @@ class _DatabaseViewSelectorButton extends StatelessWidget { showTransitionMobileBottomSheet( context, showDivider: false, - initialStop: 1.0, builder: (_) { return MultiBlocProvider( providers: [ 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 d8aa978e12..787e08760b 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/card/card.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card.dart index b2daf625f6..d5c73a1179 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card.dart @@ -1,6 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/mobile/presentation/database/card/card.dart'; -import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/field/field_controller.dart'; import 'package:appflowy/plugins/database/application/row/row_cache.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/row/action.dart'; @@ -186,7 +185,7 @@ class _CardContent extends StatelessWidget { final RowMetaPB rowMeta; final CardCellBuilder cellBuilder; - final List cells; + final List cells; final RowCardStyleConfiguration styleConfiguration; @override @@ -210,9 +209,9 @@ class _CardContent extends StatelessWidget { List _makeCells( BuildContext context, RowMetaPB rowMeta, - List cells, + List cells, ) { - return cells.mapIndexed((int index, CellContext cellContext) { + return cells.mapIndexed((int index, CellMeta cellMeta) { EditableCardNotifier? cellNotifier; if (index == 0) { @@ -225,7 +224,7 @@ class _CardContent extends StatelessWidget { } return cellBuilder.build( - cellContext: cellContext, + cellContext: cellMeta.cellContext(), cellNotifier: cellNotifier, styleMap: styleConfiguration.cellStyleMap, hasNotes: !rowMeta.isDocumentEmpty, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card_bloc.dart index 3cb47d9f43..5bd4d6f505 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card_bloc.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:appflowy/plugins/database/application/row/row_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:flutter/foundation.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; @@ -7,7 +9,6 @@ import 'package:appflowy/plugins/database/application/field/field_controller.dar import 'package:appflowy/plugins/database/application/row/row_cache.dart'; import 'package:appflowy/plugins/database/domain/row_listener.dart'; import 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -104,7 +105,7 @@ class CardBloc extends Bloc { } } -List _makeCells( +List _makeCells( FieldController fieldController, String? groupFieldId, List cellContexts, @@ -116,7 +117,15 @@ List _makeCells( !(fieldInfo.visibility?.isVisibleState() ?? false) || (groupFieldId != null && cellContext.fieldId == groupFieldId); }); - return cellContexts.toList(); + return cellContexts + .map( + (cellCtx) => CellMeta( + fieldId: cellCtx.fieldId, + rowId: cellCtx.rowId, + fieldType: fieldController.getField(cellCtx.fieldId)!.fieldType, + ), + ) + .toList(); } @freezed @@ -124,17 +133,30 @@ class CardEvent with _$CardEvent { const factory CardEvent.initial() = _InitialRow; const factory CardEvent.setIsEditing(bool isEditing) = _IsEditing; const factory CardEvent.didReceiveCells( - List cells, + List cells, ChangedReason reason, ) = _DidReceiveCells; const factory CardEvent.didUpdateRowMeta(RowMetaPB rowMeta) = _DidUpdateRowMeta; } +@freezed +class CellMeta with _$CellMeta { + const CellMeta._(); + + const factory CellMeta({ + required String fieldId, + required RowId rowId, + required FieldType fieldType, + }) = _DatabaseCellMeta; + + CellContext cellContext() => CellContext(fieldId: fieldId, rowId: rowId); +} + @freezed class CardState with _$CardState { const factory CardState({ - required List cells, + required List cells, required RowMetaPB rowMeta, required bool isEditing, ChangedReason? changeReason, @@ -142,7 +164,7 @@ class CardState with _$CardState { factory CardState.initial( RowMetaPB rowMeta, - List cells, + List cells, bool isEditing, ) => CardState( diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_relation_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_relation_cell.dart index 0e411440ef..5c31d41b30 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_relation_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_relation_cell.dart @@ -42,7 +42,6 @@ class MobileGridRelationCellSkin extends IEditableRelationCellSkin { onTap: () { showMobileBottomSheet( context, - padding: EdgeInsets.zero, backgroundColor: Theme.of(context).colorScheme.secondaryContainer, builder: (context) { return const FlowyText("Coming soon"); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_url_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_url_cell.dart index 4dffae3022..cddb821943 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_url_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_url_cell.dart @@ -2,6 +2,7 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_she import 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -51,7 +52,7 @@ class MobileGridURLCellSkin extends IEditableURLCellSkin { showMobileBottomSheet( context, showDragHandle: true, - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: AFThemeExtension.of(context).background, builder: (context) => BlocProvider.value( value: bloc, child: MobileURLEditor( diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checkbox_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checkbox_cell.dart index 23b5c31c75..2e9e4b1a24 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checkbox_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checkbox_cell.dart @@ -1,6 +1,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/checkbox_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flutter/material.dart'; import '../editable_cell_skeleton/checkbox.dart'; @@ -31,7 +32,7 @@ class MobileRowDetailCheckboxCellSkin extends IEditableCheckboxCellSkin { alignment: AlignmentDirectional.centerStart, child: FlowySvg( state.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s, - color: Theme.of(context).colorScheme.onBackground, + color: AFThemeExtension.of(context).onBackground, blendMode: BlendMode.dst, size: const Size.square(24), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checklist_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checklist_cell.dart index dd981795d2..67f9f1c53f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checklist_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checklist_cell.dart @@ -1,11 +1,12 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/checklist_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/mobile_checklist_cell_editor.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.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_bloc/flutter_bloc.dart'; @@ -25,7 +26,7 @@ class MobileRowDetailChecklistCellSkin extends IEditableChecklistCellSkin { borderRadius: const BorderRadius.all(Radius.circular(14)), onTap: () => showMobileBottomSheet( context, - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: AFThemeExtension.of(context).background, builder: (context) { return BlocProvider.value( value: bloc, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_relation_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_relation_cell.dart index eebb3e1c75..cdbcef64c7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_relation_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_relation_cell.dart @@ -19,7 +19,6 @@ class MobileRowDetailRelationCellSkin extends IEditableRelationCellSkin { borderRadius: const BorderRadius.all(Radius.circular(14)), onTap: () => showMobileBottomSheet( context, - padding: EdgeInsets.zero, builder: (context) { return const FlowyText("Coming soon"); }, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_url_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_url_cell.dart index f97eabe830..f87b225492 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_url_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_url_cell.dart @@ -1,12 +1,12 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flowy_infra/theme_extension.dart'; import '../editable_cell_skeleton/url.dart'; @@ -28,7 +28,7 @@ class MobileRowDetailURLCellSkin extends IEditableURLCellSkin { onTap: () => showMobileBottomSheet( context, showDragHandle: true, - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: AFThemeExtension.of(context).background, builder: (_) { return BlocProvider.value( value: bloc, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart index 3ab883329a..18c9428ede 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart @@ -423,7 +423,7 @@ class _DeleteTaskButton extends StatefulWidget { } class _DeleteTaskButtonState extends State<_DeleteTaskButton> { - final _materialStatesController = MaterialStatesController(); + final _materialStatesController = WidgetStatesController(); @override void dispose() { @@ -438,16 +438,16 @@ class _DeleteTaskButtonState extends State<_DeleteTaskButton> { onHover: (_) => setState(() {}), onFocusChange: (_) => setState(() {}), style: ButtonStyle( - fixedSize: const MaterialStatePropertyAll(Size.square(32)), - minimumSize: const MaterialStatePropertyAll(Size.square(32)), - maximumSize: const MaterialStatePropertyAll(Size.square(32)), - overlayColor: MaterialStateProperty.resolveWith((state) { - if (state.contains(MaterialState.focused)) { + fixedSize: const WidgetStatePropertyAll(Size.square(32)), + minimumSize: const WidgetStatePropertyAll(Size.square(32)), + maximumSize: const WidgetStatePropertyAll(Size.square(32)), + overlayColor: WidgetStateProperty.resolveWith((state) { + if (state.contains(WidgetState.focused)) { return AFThemeExtension.of(context).greyHover; } return Colors.transparent; }), - shape: const MaterialStatePropertyAll( + shape: const WidgetStatePropertyAll( RoundedRectangleBorder(borderRadius: Corners.s6Border), ), ), @@ -455,8 +455,8 @@ class _DeleteTaskButtonState extends State<_DeleteTaskButton> { child: FlowySvg( FlowySvgs.delete_s, color: _materialStatesController.value - .contains(MaterialState.hovered) || - _materialStatesController.value.contains(MaterialState.focused) + .contains(WidgetState.hovered) || + _materialStatesController.value.contains(WidgetState.focused) ? Theme.of(context).colorScheme.error : null, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart index 70383361d7..63ea44008e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart @@ -422,7 +422,7 @@ class _UnselectRowButton extends StatefulWidget { } class _UnselectRowButtonState extends State<_UnselectRowButton> { - final _materialStatesController = MaterialStatesController(); + final _materialStatesController = WidgetStatesController(); @override void dispose() { @@ -437,26 +437,25 @@ class _UnselectRowButtonState extends State<_UnselectRowButton> { onHover: (_) => setState(() {}), onFocusChange: (_) => setState(() {}), style: ButtonStyle( - fixedSize: const MaterialStatePropertyAll(Size.square(32)), - minimumSize: const MaterialStatePropertyAll(Size.square(32)), - maximumSize: const MaterialStatePropertyAll(Size.square(32)), - overlayColor: MaterialStateProperty.resolveWith((state) { - if (state.contains(MaterialState.focused)) { + fixedSize: const WidgetStatePropertyAll(Size.square(32)), + minimumSize: const WidgetStatePropertyAll(Size.square(32)), + maximumSize: const WidgetStatePropertyAll(Size.square(32)), + overlayColor: WidgetStateProperty.resolveWith((state) { + if (state.contains(WidgetState.focused)) { return AFThemeExtension.of(context).greyHover; } return Colors.transparent; }), - shape: const MaterialStatePropertyAll( + shape: const WidgetStatePropertyAll( RoundedRectangleBorder(borderRadius: Corners.s6Border), ), ), statesController: _materialStatesController, child: Container( - color: _materialStatesController.value - .contains(MaterialState.hovered) || - _materialStatesController.value.contains(MaterialState.focused) + color: _materialStatesController.value.contains(WidgetState.hovered) || + _materialStatesController.value.contains(WidgetState.focused) ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onBackground, + : AFThemeExtension.of(context).onBackground, width: 12, height: 1, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_cell_editor.dart index 3094c97887..d3a41c6c3f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_cell_editor.dart @@ -380,7 +380,7 @@ class _SelectOptionCellState extends State<_SelectOptionCell> { icon: FlowySvg( FlowySvgs.three_dots_s, size: const Size.square(16), - color: Theme.of(context).colorScheme.onBackground, + color: AFThemeExtension.of(context).onBackground, ), ), ], @@ -462,7 +462,7 @@ class SelectOptionTagCell extends StatelessWidget { child: FlowySvg( FlowySvgs.drag_element_s, size: const Size.square(14), - color: Theme.of(context).colorScheme.onBackground, + color: AFThemeExtension.of(context).onBackground, ), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart index bcb1867086..02bc0700a4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart @@ -20,7 +20,7 @@ const List _supportedFieldTypes = [ FieldType.LastEditedTime, FieldType.CreatedTime, FieldType.Relation, - // FieldType.Summary, + FieldType.Summary, ]; class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart index 969c4e6e0c..8762918141 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart @@ -314,17 +314,17 @@ class ToggleHiddenFieldsVisibilityButton extends StatelessWidget { constraints: const BoxConstraints(minWidth: double.infinity), child: TextButton.icon( style: Theme.of(context).textButtonTheme.style?.copyWith( - shape: MaterialStateProperty.all( + shape: WidgetStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(12.0), ), ), - overlayColor: MaterialStateProperty.all( + overlayColor: WidgetStateProperty.all( Theme.of(context).hoverColor, ), alignment: AlignmentDirectional.centerStart, splashFactory: NoSplash.splashFactory, - padding: const MaterialStatePropertyAll( + padding: const WidgetStatePropertyAll( EdgeInsets.symmetric(vertical: 14, horizontal: 6), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/mobile_database_controls.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/mobile_database_controls.dart index d5f3ce293a..f3a548932d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/mobile_database_controls.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/mobile_database_controls.dart @@ -9,6 +9,7 @@ import 'package:appflowy/plugins/database/grid/application/sort/sort_editor_bloc import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:easy_localization/easy_localization.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_bloc/flutter_bloc.dart'; @@ -128,7 +129,6 @@ void _showDatabaseFieldListFromToolbar( showHeader: true, showBackButton: true, title: LocaleKeys.grid_settings_properties.tr(), - showDivider: true, builder: (_) { return BlocProvider.value( value: context.read(), @@ -150,7 +150,7 @@ void _showEditSortPanelFromToolbar( showDragHandle: true, showDivider: false, useSafeArea: false, - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: AFThemeExtension.of(context).background, builder: (_) { return BlocProvider.value( value: context.read(), 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 d6c84e39fe..7ede902085 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/application/document_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart index 038405c957..121582e1f3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart @@ -20,7 +20,6 @@ import 'package:appflowy/util/debounce.dart'; import 'package:appflowy/util/throttle.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; @@ -76,13 +75,15 @@ class DocumentBloc extends Bloc { StreamSubscription? _transactionSubscription; - final _updateSelectionDebounce = Debounce(); - final _syncThrottle = Throttler(duration: const Duration(milliseconds: 500)); + bool isClosing = false; + + static const _syncDuration = Duration(milliseconds: 250); + final _updateSelectionDebounce = Debounce(duration: _syncDuration); + final _syncThrottle = Throttler(duration: _syncDuration); // The conflict handle logic is not fully implemented yet // use the syncTimer to force to reload the document state when the conflict happens. Timer? _syncTimer; - bool _shouldSync = false; bool get isLocalMode { final userProfilePB = state.userProfilePB; @@ -92,6 +93,10 @@ class DocumentBloc extends Bloc { @override Future close() async { + isClosing = true; + _updateSelectionDebounce.dispose(); + _syncThrottle.dispose(); + await _documentService.syncAwarenessStates(documentId: documentId); await _documentListener.stop(); await _syncStateListener.stop(); await _viewListener?.stop(); @@ -110,7 +115,6 @@ class DocumentBloc extends Bloc { ) async { await event.when( initial: () async { - _resetSyncTimer(); final result = await _fetchDocumentState(); _onViewChanged(); _onDocumentChanged(); @@ -205,19 +209,6 @@ class DocumentBloc extends Bloc { ); } - void _resetSyncTimer() { - _syncTimer?.cancel(); - _syncTimer = null; - _syncTimer = Timer.periodic(const Duration(seconds: 10), (_) { - if (!_shouldSync) { - return; - } - Log.debug('auto sync document'); - // unawaited(_documentCollabAdapter.forceReload()); - _shouldSync = false; - }); - } - /// Fetch document Future> _fetchDocumentState() async { final result = await _documentService.openDocument(documentId: documentId); @@ -257,10 +248,6 @@ class DocumentBloc extends Bloc { // ignore: invalid_use_of_visible_for_testing_member emit(state.copyWith(isDocumentEmpty: editorState.document.isEmpty)); } - - // reset the sync timer - _shouldSync = true; - _resetSyncTimer(); }, ); @@ -322,8 +309,6 @@ class DocumentBloc extends Bloc { } unawaited(_documentCollabAdapter.syncV3(docEvent: docEvent)); - - _resetSyncTimer(); } Future _onAwarenessStatesUpdate( @@ -347,13 +332,15 @@ class DocumentBloc extends Bloc { } void _throttleSyncDoc(DocEventPB docEvent) { - _shouldSync = true; _syncThrottle.call(() { _onDocumentStateUpdate(docEvent); }); } Future _onSelectionUpdate() async { + if (isClosing) { + return; + } final user = state.userProfilePB; final deviceId = ApplicationInfo.deviceId; if (!FeatureFlag.syncDocument.isOn || user == null) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_collab_adapter.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_collab_adapter.dart index 43292817d1..8d8841cc3c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_collab_adapter.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_collab_adapter.dart @@ -183,7 +183,7 @@ class DocumentCollabAdapter { for (final state in values) { // the following code is only for version 1 if (state.version != 1 || state.metadata.isEmpty) { - return; + continue; } final uid = state.user.uid.toString(); final did = state.user.deviceId; @@ -244,9 +244,8 @@ class DocumentCollabAdapter { ); remoteSelections.add(remoteSelection); } - if (remoteSelections.isNotEmpty) { - editorState.remoteSelections.value = remoteSelections; - } + + editorState.remoteSelections.value = remoteSelections; } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart index e60782605a..da99886014 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart' @@ -10,7 +11,13 @@ import 'package:appflowy_editor/appflowy_editor.dart' Delta, ParagraphBlockKeys, NodeIterator, - NodeExternalValues; + NodeExternalValues, + HeadingBlockKeys, + QuoteBlockKeys, + NumberedListBlockKeys, + BulletedListBlockKeys, + blockComponentDelta; +import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:collection/collection.dart'; import 'package:nanoid/nanoid.dart'; @@ -144,21 +151,25 @@ extension BlockToNode on BlockPB { final deltaString = meta.textMap[externalId]; if (deltaString != null) { final delta = jsonDecode(deltaString); - map['delta'] = delta; - // map.putIfAbsent( - // 'delta', - // () => delta, - // ); + map[blockComponentDelta] = delta; } } } + Attributes adapterCallback(Attributes map) => map + ..putIfAbsent( + blockComponentDelta, + () => Delta().toJson(), + ); + final adapter = { - ParagraphBlockKeys.type: (Attributes map) => map - ..putIfAbsent( - 'delta', - () => Delta().toJson(), - ), + ParagraphBlockKeys.type: adapterCallback, + HeadingBlockKeys.type: adapterCallback, + CodeBlockKeys.type: adapterCallback, + QuoteBlockKeys.type: adapterCallback, + NumberedListBlockKeys.type: adapterCallback, + BulletedListBlockKeys.type: adapterCallback, + ToggleListBlockKeys.type: adapterCallback, }; return adapter[ty]?.call(map) ?? map; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/document.dart b/frontend/appflowy_flutter/lib/plugins/document/document.dart index 13242bbe0a..bd9e4891d2 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/banner.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/banner.dart index 3936bf6968..e5fd6b6b8b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/banner.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/banner.dart @@ -23,7 +23,7 @@ class DocumentBanner extends StatelessWidget { constraints: const BoxConstraints(minHeight: 60), child: Container( width: double.infinity, - color: colorScheme.surfaceVariant, + color: colorScheme.surfaceContainerHighest, child: FittedBox( fit: BoxFit.scaleDown, child: Row( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart index 8f18cb1d58..39cd608d0e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart @@ -1,6 +1,3 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_page.dart'; @@ -13,6 +10,8 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:easy_localization/easy_localization.dart' hide TextDirection; import 'package:flowy_infra/theme_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; Map getEditorBuilderMap({ @@ -93,8 +92,9 @@ Map getEditorBuilderMap({ final factor = pageStyle.fontLayout.factor; final headingPaddings = pageStyle.lineHeightLayout.headingPaddings .map((e) => e * factor); - final level = node.attributes[HeadingBlockKeys.level] ?? 6; - return EdgeInsets.only(top: headingPaddings.elementAt(level)); + int level = node.attributes[HeadingBlockKeys.level] ?? 6; + level = level.clamp(1, 6); + return EdgeInsets.only(top: headingPaddings.elementAt(level - 1)); } return const EdgeInsets.only(top: 12.0, bottom: 4.0); 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 0b7a473176..13d2ba6ab9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -56,7 +56,18 @@ final List commandShortcutEvents = [ customPasteCommand, customCutCommand, ...customTextAlignCommands, - ...standardCommandShortcutEvents, + + // remove standard shortcuts for copy, cut, paste, todo + ...standardCommandShortcutEvents + ..removeWhere( + (shortcut) => [ + copyCommand, + cutCommand, + pasteCommand, + toggleTodoListCommand, + ].contains(shortcut), + ), + emojiShortcutEvent, ]; @@ -90,7 +101,6 @@ class AppFlowyEditorPage extends StatefulWidget { final String Function(Node)? placeholderText; /// Used to provide an initial selection on Page-load - /// final Selection? initialSelection; final bool useViewInfoBloc; @@ -111,15 +121,8 @@ class _AppFlowyEditorPageState extends State { ], ); - late final List commandShortcutEvents = [ - toggleToggleListCommand, - ...localizedCodeBlockCommands, - customCopyCommand, - customPasteCommand, - customCutCommand, - ...customTextAlignCommands, - ...standardCommandShortcutEvents, - emojiShortcutEvent, + late final List cmdShortcutEvents = [ + ...commandShortcutEvents, ..._buildFindAndReplaceCommands(), ]; @@ -309,7 +312,7 @@ class _AppFlowyEditorPageState extends State { ), // customize the shortcuts characterShortcutEvents: characterShortcutEvents, - commandShortcutEvents: commandShortcutEvents, + commandShortcutEvents: cmdShortcutEvents, // customize the context menu items contextMenuItems: customContextMenuItems, // customize the header and footer. @@ -392,12 +395,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); } @@ -407,7 +404,7 @@ class _AppFlowyEditorPageState extends State { final customizeShortcuts = await settingsShortcutService.getCustomizeShortcuts(); await settingsShortcutService.updateCommandShortcuts( - commandShortcutEvents, + cmdShortcutEvents, customizeShortcuts, ); } @@ -437,7 +434,7 @@ class _AppFlowyEditorPageState extends State { Material( child: DecoratedBox( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, + color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(4), ), child: FindAndReplaceMenuWidget( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart index eb4487fa49..b80bb034a6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart @@ -74,7 +74,6 @@ class MobileBlockActionButtons extends StatelessWidget { context, showHeader: true, showCloseButton: true, - showDivider: true, showDragHandle: true, title: LocaleKeys.document_plugins_action.tr(), builder: (context) { 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 09906e1429..d5e99e13f8 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, @@ -300,7 +300,7 @@ class ColorOptionAction extends PopoverActionCell { colors: colors, selected: selectedColor, border: Border.all( - color: Theme.of(context).colorScheme.onBackground, + color: AFThemeExtension.of(context).onBackground, ), onTap: (option, index) async { final transaction = editorState.transaction; 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 df78f6261e..2a1101794a 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/code_block/code_block_language_selector.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_language_selector.dart index 323c15be1e..97ac54e9f1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_language_selector.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_language_selector.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_item_list_menu.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart'; @@ -8,7 +6,9 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; CodeBlockLanguagePickerBuilder codeBlockLanguagePickerBuilder = ( @@ -70,7 +70,7 @@ class _CodeBlockLanguageSelectorState widget.language?.capitalize() ?? LocaleKeys.document_codeBlock_language_auto.tr(), constraints: const BoxConstraints(minWidth: 50), - fontColor: Theme.of(context).colorScheme.onBackground, + fontColor: AFThemeExtension.of(context).onBackground, padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 4), fillColor: Colors.transparent, hoverColor: Theme.of(context).colorScheme.secondaryContainer, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart index 3fdf332eb3..ba6f01e908 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart @@ -61,7 +61,7 @@ class _ErrorBlockComponentWidgetState extends State Widget build(BuildContext context) { Widget child = DecoratedBox( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, + color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(4), ), child: FlowyButton( @@ -74,17 +74,9 @@ class _ErrorBlockComponentWidgetState extends State ClipboardServiceData(plainText: jsonEncode(node.toJson())), ); }, - text: SizedBox( - height: 52, - child: Row( - children: [ - const HSpace(4), - FlowyText( - LocaleKeys.document_errorBlock_theBlockIsNotSupported.tr(), - ), - ], - ), - ), + text: PlatformExtension.isDesktopOrWeb + ? _buildDesktopErrorBlock(context) + : _buildMobileErrorBlock(context), ), ); @@ -111,4 +103,44 @@ class _ErrorBlockComponentWidgetState extends State return child; } + + Widget _buildDesktopErrorBlock(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + const HSpace(4), + FlowyText.regular( + LocaleKeys.document_errorBlock_theBlockIsNotSupported.tr(), + ), + const HSpace(4), + FlowyText.regular( + '(${LocaleKeys.document_errorBlock_clickToCopyTheBlockContent.tr()})', + color: Theme.of(context).hintColor, + ), + ], + ), + ); + } + + Widget _buildMobileErrorBlock(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText.regular( + LocaleKeys.document_errorBlock_theBlockIsNotSupported.tr(), + ), + const VSpace(6), + FlowyText.regular( + '(${LocaleKeys.document_errorBlock_clickToCopyTheBlockContent.tr()})', + color: Theme.of(context).hintColor, + fontSize: 12.0, + ), + ], + ), + ); + } } 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 ce756b9ffd..7c84f2c31b 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 7a606f33ef..3a22909744 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/image_placeholder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart index f193f91617..5d51c1b390 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart @@ -49,7 +49,7 @@ class ImagePlaceholderState extends State { Widget build(BuildContext context) { final Widget child = DecoratedBox( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, + color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(4), ), child: FlowyHover( 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 36d90ac6fb..84ab2f7380 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/editor_plugins/image/unsupport_image_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsupport_image_widget.dart index 3403a1ff31..017e5a94b2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsupport_image_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsupport_image_widget.dart @@ -14,7 +14,7 @@ class UnSupportImageWidget extends StatelessWidget { Widget build(BuildContext context) { return DecoratedBox( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, + color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(4), ), child: FlowyHover( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart index 4c9de6b07d..0679f87996 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart @@ -107,7 +107,7 @@ class _UploadImageMenuState extends State { }), indicatorSize: TabBarIndicatorSize.label, isScrollable: true, - overlayColor: MaterialStatePropertyAll( + overlayColor: WidgetStatePropertyAll( PlatformExtension.isDesktop ? Theme.of(context).colorScheme.secondary : Colors.transparent, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation.dart index 543cee1207..7ce143acba 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation.dart @@ -77,7 +77,7 @@ class _InlineMathEquationState extends State { ), fontSize: 14.0, color: widget.textStyle?.color ?? - theme.colorScheme.onBackground, + theme.colorScheme.onSurface, ), ), const HSpace(2), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart index 56a9739531..c7d298ff09 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart @@ -124,7 +124,7 @@ class MathEquationBlockComponentWidgetState decoration: BoxDecoration( color: formula.isNotEmpty ? Colors.transparent - : Theme.of(context).colorScheme.surfaceVariant, + : Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(4), ), child: FlowyHover( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/utils.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/utils.dart index 21f3b7ea68..ad4d523812 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/utils.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/utils.dart @@ -10,8 +10,6 @@ Future showEditLinkBottomSheet( ) { return showMobileBottomSheet( context, - showHeader: false, - showCloseButton: false, showDragHandle: true, padding: const EdgeInsets.symmetric(horizontal: 16), builder: (context) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_list.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_list.dart index 2de13ed459..438aa1264b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_list.dart @@ -22,8 +22,6 @@ Future showTextColorAndBackgroundColorPicker( await showMobileBottomSheet( context, showHeader: true, - showCloseButton: false, - showDivider: true, showDragHandle: true, showDoneButton: true, barrierColor: Colors.transparent, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart index 3055c7c440..fd0aa86fa7 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart @@ -20,6 +20,7 @@ import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/snap_bar.dart'; import 'package:flutter/material.dart'; @@ -188,8 +189,7 @@ class PageStyleCoverImage extends StatelessWidget { ); }, title: LocaleKeys.pageStyle_presets.tr(), - barrierColor: Colors.transparent, - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: AFThemeExtension.of(context).background, builder: (_) { return BlocProvider.value( value: pageStyleBloc, @@ -257,6 +257,9 @@ class PageStyleCoverImage extends StatelessWidget { void _showUnsplash(BuildContext context) { final pageStyleBloc = context.read(); + final backgroundColor = AFThemeExtension.of(context).background; + final maxHeight = MediaQuery.of(context).size.height * 0.6; + context.pop(); showMobileBottomSheet( @@ -267,8 +270,7 @@ class PageStyleCoverImage extends StatelessWidget { showHeader: true, showRemoveButton: true, title: LocaleKeys.pageStyle_unsplash.tr(), - barrierColor: Colors.transparent, - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: backgroundColor, onRemove: () { pageStyleBloc.add( DocumentPageStyleEvent.updateCoverImage( @@ -279,11 +281,11 @@ class PageStyleCoverImage extends StatelessWidget { builder: (_) { return ConstrainedBox( constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.6, + maxHeight: maxHeight, minHeight: 80, ), child: BlocProvider.value( - value: context.read(), + value: pageStyleBloc, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: UnsplashImageWidget( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart index ae975048d6..434dba7337 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart @@ -7,6 +7,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_util.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/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -77,9 +78,7 @@ class _PageStyleIconState extends State { showDoneButton: true, showHeader: true, title: LocaleKeys.titleBar_pageIcon.tr(), - barrierColor: Colors.transparent, - backgroundColor: Theme.of(context).colorScheme.background, - isScrollControlled: true, + backgroundColor: AFThemeExtension.of(context).background, enableDraggableScrollable: true, minChildSize: 0.6, initialChildSize: 0.61, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart index 7b8d5cfb92..211e287d15 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart @@ -8,6 +8,7 @@ import 'package:appflowy/shared/feedback_gesture_detector.dart'; import 'package:appflowy/util/font_family_extension.dart'; import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:easy_localization/easy_localization.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_bloc/flutter_bloc.dart'; @@ -208,9 +209,7 @@ class _FontButton extends StatelessWidget { showDoneButton: true, showHeader: true, title: LocaleKeys.titleBar_font.tr(), - barrierColor: Colors.transparent, - backgroundColor: Theme.of(context).colorScheme.background, - isScrollControlled: true, + backgroundColor: AFThemeExtension.of(context).background, enableDraggableScrollable: true, minChildSize: 0.6, initialChildSize: 0.61, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_option_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_option_action.dart index afb2b63f49..6d0597319c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_option_action.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_option_action.dart @@ -137,7 +137,7 @@ class TableColorOptionAction extends PopoverActionCell { colors: colors, selected: selectedColor, border: Border.all( - color: Theme.of(context).colorScheme.onBackground, + color: AFThemeExtension.of(context).onBackground, ), onTap: (option, index) async { final backgroundColor = diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index 6d81e8eb8f..271aa0e340 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -15,6 +15,7 @@ import 'package:appflowy/workspace/application/settings/appearance/appearance_cu import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:collection/collection.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -42,9 +43,14 @@ class EditorStyleCustomizer { EditorStyle desktop() { final theme = Theme.of(context); + final afThemeExtension = AFThemeExtension.of(context); + final appearanceFont = context.read().state.font; final appearance = context.read().state; final fontSize = appearance.fontSize; - final fontFamily = appearance.fontFamily; + String fontFamily = appearance.fontFamily; + if (fontFamily.isEmpty && appearanceFont.isNotEmpty) { + fontFamily = appearanceFont; + } return EditorStyle.desktop( padding: padding, @@ -56,8 +62,7 @@ class EditorStyleCustomizer { textStyleConfiguration: TextStyleConfiguration( text: baseTextStyle(fontFamily).copyWith( fontSize: fontSize, - color: theme.colorScheme.onBackground, - height: 1.5, + color: afThemeExtension.onBackground, ), bold: baseTextStyle(fontFamily, fontWeight: FontWeight.bold).copyWith( fontWeight: FontWeight.w600, @@ -89,6 +94,7 @@ class EditorStyleCustomizer { } EditorStyle mobile() { + final afThemeExtension = AFThemeExtension.of(context); final pageStyle = context.read().state; final theme = Theme.of(context); final fontSize = pageStyle.fontLayout.fontSize; @@ -104,10 +110,10 @@ class EditorStyleCustomizer { padding: padding, defaultTextDirection: defaultTextDirection, textStyleConfiguration: TextStyleConfiguration( + lineHeight: lineHeight, text: baseTextStyle.copyWith( fontSize: fontSize, - color: theme.colorScheme.onBackground, - height: lineHeight, + color: afThemeExtension.onBackground, ), bold: baseTextStyle.copyWith(fontWeight: FontWeight.w600), italic: baseTextStyle.copyWith(fontStyle: FontStyle.italic), @@ -173,7 +179,7 @@ class EditorStyleCustomizer { return baseTextStyle(fontFamily).copyWith( fontSize: fontSize, height: 1.5, - color: Theme.of(context).colorScheme.onBackground, + color: AFThemeExtension.of(context).onBackground, ); } @@ -183,16 +189,17 @@ class EditorStyleCustomizer { fontFamily: defaultFontFamily, fontSize: fontSize, height: 1.5, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.6), + color: AFThemeExtension.of(context).onBackground.withOpacity(0.6), ); } SelectionMenuStyle selectionMenuStyleBuilder() { final theme = Theme.of(context); + final afThemeExtension = AFThemeExtension.of(context); return SelectionMenuStyle( selectionMenuBackgroundColor: theme.cardColor, - selectionMenuItemTextColor: theme.colorScheme.onBackground, - selectionMenuItemIconColor: theme.colorScheme.onBackground, + selectionMenuItemTextColor: afThemeExtension.onBackground, + selectionMenuItemIconColor: afThemeExtension.onBackground, selectionMenuItemSelectedIconColor: theme.colorScheme.onSurface, selectionMenuItemSelectedTextColor: theme.colorScheme.onSurface, selectionMenuItemSelectedColor: theme.hoverColor, @@ -201,10 +208,11 @@ class EditorStyleCustomizer { InlineActionsMenuStyle inlineActionsMenuStyleBuilder() { final theme = Theme.of(context); + final afThemeExtension = AFThemeExtension.of(context); return InlineActionsMenuStyle( backgroundColor: theme.cardColor, - groupTextColor: theme.colorScheme.onBackground.withOpacity(.8), - menuItemTextColor: theme.colorScheme.onBackground, + groupTextColor: afThemeExtension.onBackground.withOpacity(.8), + menuItemTextColor: afThemeExtension.onBackground, menuItemSelectedColor: theme.colorScheme.secondary, menuItemSelectedTextColor: theme.colorScheme.onSurface, ); 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 fa15bc10d5..eb82f3d1fa 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/shared/window_title_bar.dart b/frontend/appflowy_flutter/lib/shared/window_title_bar.dart index 51a8b51c86..1640383588 100644 --- a/frontend/appflowy_flutter/lib/shared/window_title_bar.dart +++ b/frontend/appflowy_flutter/lib/shared/window_title_bar.dart @@ -73,7 +73,7 @@ class _WindowTitleBarState extends State { return Container( height: 40, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, + color: Theme.of(context).colorScheme.surfaceContainerHighest, ), child: DragToMoveArea( child: Row( diff --git a/frontend/appflowy_flutter/lib/user/application/user_listener.dart b/frontend/appflowy_flutter/lib/user/application/user_listener.dart index b3e7d6fbe0..81a081b4e3 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_listener.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_listener.dart @@ -113,7 +113,7 @@ typedef WorkspaceSettingNotifyValue class UserWorkspaceListener { UserWorkspaceListener(); - PublishNotifier? _settingChangedNotifier = + final PublishNotifier _settingChangedNotifier = PublishNotifier(); FolderNotificationListener? _listener; @@ -122,7 +122,7 @@ class UserWorkspaceListener { void Function(WorkspaceSettingNotifyValue)? onSettingUpdated, }) { if (onSettingUpdated != null) { - _settingChangedNotifier?.addPublishListener(onSettingUpdated); + _settingChangedNotifier.addPublishListener(onSettingUpdated); } // The "current-workspace" is predefined in the backend. Do not try to @@ -140,13 +140,11 @@ class UserWorkspaceListener { switch (ty) { case FolderNotification.DidUpdateWorkspaceSetting: result.fold( - (payload) => _settingChangedNotifier?.value = + (payload) => _settingChangedNotifier.value = FlowyResult.success(WorkspaceSettingPB.fromBuffer(payload)), - (error) => - _settingChangedNotifier?.value = FlowyResult.failure(error), + (error) => _settingChangedNotifier.value = FlowyResult.failure(error), ); break; - default: break; } @@ -154,8 +152,6 @@ class UserWorkspaceListener { Future stop() async { await _listener?.stop(); - - _settingChangedNotifier?.dispose(); - _settingChangedNotifier = null; + _settingChangedNotifier.dispose(); } } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart index ce446320a3..09d24ecd83 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart @@ -154,20 +154,20 @@ class _DesktopSignInButton extends StatelessWidget { ), ), style: ButtonStyle( - overlayColor: MaterialStateProperty.resolveWith( + overlayColor: WidgetStateProperty.resolveWith( (states) { - if (states.contains(MaterialState.hovered)) { + if (states.contains(WidgetState.hovered)) { return style.colorScheme.onSecondaryContainer; } return null; }, ), - shape: MaterialStateProperty.all( + shape: WidgetStateProperty.all( const RoundedRectangleBorder( borderRadius: Corners.s6Border, ), ), - side: MaterialStateProperty.all( + side: WidgetStateProperty.all( BorderSide( color: style.dividerColor, ), diff --git a/frontend/appflowy_flutter/lib/util/theme_extension.dart b/frontend/appflowy_flutter/lib/util/theme_extension.dart new file mode 100644 index 0000000000..c7b56699d3 --- /dev/null +++ b/frontend/appflowy_flutter/lib/util/theme_extension.dart @@ -0,0 +1,5 @@ +import 'package:flutter/material.dart'; + +extension IsLightMode on ThemeData { + bool get isLightMode => brightness == Brightness.light; +} 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 7b9e86e16b..6ce62acae8 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 d9343e2ee3..71bb8423df 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 2fba33263f..0c6fe9aaed 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 @@ -1,9 +1,8 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/shared/google_fonts_extension.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme.dart'; +import 'package:flutter/material.dart'; // the default font family is empty, so we can use the default font family of the platform // the system will choose the default font family of the platform @@ -20,10 +19,10 @@ const builtInCodeFontFamily = 'RobotoMono'; abstract class BaseAppearance { final white = const Color(0xFFFFFFFF); - final Set scrollbarInteractiveStates = { - MaterialState.pressed, - MaterialState.hovered, - MaterialState.dragged, + final Set scrollbarInteractiveStates = { + WidgetState.pressed, + WidgetState.hovered, + WidgetState.dragged, }; TextStyle getFontStyle({ @@ -34,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/settings/appearance/desktop_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart index 001de8af4e..1727eedd55 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart @@ -34,8 +34,6 @@ class DesktopAppearance extends BaseAppearance { // Editor: toolbarColor onTertiary: theme.toolbarColor, tertiaryContainer: theme.questionBubbleBG, - background: theme.surface, - onBackground: theme.text, surface: theme.surface, // text&icon color when it is hovered onSurface: theme.hoverFG, @@ -44,7 +42,7 @@ class DesktopAppearance extends BaseAppearance { onError: theme.onPrimary, error: theme.red, outline: theme.shader4, - surfaceVariant: theme.sidebarBg, + surfaceContainerHighest: theme.sidebarBg, shadow: theme.shadow, ); @@ -76,13 +74,13 @@ class DesktopAppearance extends BaseAppearance { contentTextStyle: TextStyle(color: colorScheme.onSurface), ), scrollbarTheme: ScrollbarThemeData( - thumbColor: MaterialStateProperty.resolveWith((states) { + thumbColor: WidgetStateProperty.resolveWith((states) { if (states.any(scrollbarInteractiveStates.contains)) { return theme.shader7; } return theme.shader5; }), - thickness: MaterialStateProperty.resolveWith((states) { + thickness: WidgetStateProperty.resolveWith((states) { if (states.any(scrollbarInteractiveStates.contains)) { return 4; } @@ -144,6 +142,8 @@ class DesktopAppearance extends BaseAppearance { fontWeight: FontWeight.w400, fontColor: theme.hint, ), + onBackground: theme.text, + background: theme.surface, ), ], ); diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart index 19cd87f4f4..09db07ed11 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart @@ -1,11 +1,10 @@ -import 'package:flutter/material.dart'; - // ThemeData in mobile import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme_extension.dart'; +import 'package:flutter/material.dart'; class MobileAppearance extends BaseAppearance { static const _primaryColor = Color(0xFF00BCF0); //primary 100 @@ -49,14 +48,12 @@ class MobileAppearance extends BaseAppearance { tertiary: const Color(0xff858585), // for light text error: const Color(0xffFB006D), onError: const Color(0xffFB006D), - background: Colors.white, - onBackground: _onBackgroundColor, outline: const Color(0xffe3e3e3), outlineVariant: const Color(0xffCBD5E0).withOpacity(0.24), //Snack bar surface: Colors.white, onSurface: _onSurfaceColor, // text/body color - surfaceVariant: const Color.fromARGB(255, 216, 216, 216), + surfaceContainerHighest: const Color.fromARGB(255, 216, 216, 216), ) : ColorScheme( brightness: brightness, @@ -67,8 +64,6 @@ class MobileAppearance extends BaseAppearance { tertiary: const Color(0xff858585), // temp error: const Color(0xffFB006D), onError: const Color(0xffFB006D), - background: const Color(0xff121212), // temp - onBackground: Colors.white, outline: _hintColorInDarkMode, outlineVariant: Colors.black, //Snack bar @@ -78,6 +73,10 @@ class MobileAppearance extends BaseAppearance { final hintColor = brightness == Brightness.light ? const Color(0x991F2329) : _hintColorInDarkMode; + final onBackground = + brightness == Brightness.light ? _onBackgroundColor : Colors.white; + final background = + brightness == Brightness.light ? Colors.white : const Color(0xff121212); return ThemeData( useMaterial3: false, @@ -86,14 +85,14 @@ class MobileAppearance extends BaseAppearance { dividerColor: colorTheme.outline, //caption hintColor: hintColor, disabledColor: colorTheme.outline, - scaffoldBackgroundColor: colorTheme.background, + scaffoldBackgroundColor: background, appBarTheme: AppBarTheme( toolbarHeight: 44.0, - foregroundColor: colorTheme.onBackground, - backgroundColor: colorTheme.background, + foregroundColor: onBackground, + backgroundColor: background, centerTitle: false, titleTextStyle: TextStyle( - color: colorTheme.onBackground, + color: onBackground, fontSize: 18, fontWeight: FontWeight.w600, letterSpacing: 0.05, @@ -101,8 +100,8 @@ class MobileAppearance extends BaseAppearance { shadowColor: colorTheme.outlineVariant, ), radioTheme: RadioThemeData( - fillColor: MaterialStateProperty.resolveWith((states) { - if (states.contains(MaterialState.selected)) { + fillColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { return colorTheme.primary; } return colorTheme.outline; @@ -111,20 +110,20 @@ class MobileAppearance extends BaseAppearance { // button elevatedButtonTheme: ElevatedButtonThemeData( style: ButtonStyle( - fixedSize: MaterialStateProperty.all(const Size.fromHeight(48)), - elevation: MaterialStateProperty.all(0), - textStyle: MaterialStateProperty.all( + fixedSize: WidgetStateProperty.all(const Size.fromHeight(48)), + elevation: WidgetStateProperty.all(0), + textStyle: WidgetStateProperty.all( TextStyle( fontSize: 14, fontFamily: fontStyle.fontFamily, fontWeight: FontWeight.w600, ), ), - shadowColor: MaterialStateProperty.all(null), - foregroundColor: MaterialStateProperty.all(Colors.white), - backgroundColor: MaterialStateProperty.resolveWith( - (Set states) { - if (states.contains(MaterialState.disabled)) { + shadowColor: WidgetStateProperty.all(null), + foregroundColor: WidgetStateProperty.all(Colors.white), + backgroundColor: WidgetStateProperty.resolveWith( + (Set states) { + if (states.contains(WidgetState.disabled)) { return _primaryColor; } return colorTheme.primary; @@ -134,29 +133,29 @@ class MobileAppearance extends BaseAppearance { ), outlinedButtonTheme: OutlinedButtonThemeData( style: ButtonStyle( - textStyle: MaterialStateProperty.all( + textStyle: WidgetStateProperty.all( TextStyle( fontSize: 14, fontFamily: fontStyle.fontFamily, fontWeight: FontWeight.w500, ), ), - foregroundColor: MaterialStateProperty.all(colorTheme.onBackground), - backgroundColor: MaterialStateProperty.all(colorTheme.background), - shape: MaterialStateProperty.all( + foregroundColor: WidgetStateProperty.all(onBackground), + backgroundColor: WidgetStateProperty.all(background), + shape: WidgetStateProperty.all( RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), ), - side: MaterialStateProperty.all( + side: WidgetStateProperty.all( BorderSide(color: colorTheme.outline, width: 0.5), ), - padding: MaterialStateProperty.all( + padding: WidgetStateProperty.all( const EdgeInsets.symmetric(horizontal: 8, vertical: 12), ), ), ), textButtonTheme: TextButtonThemeData( style: ButtonStyle( - textStyle: MaterialStateProperty.all(fontStyle), + textStyle: WidgetStateProperty.all(fontStyle), ), ), // text @@ -170,7 +169,7 @@ class MobileAppearance extends BaseAppearance { letterSpacing: 0.16, ), displayMedium: fontStyle.copyWith( - color: colorTheme.onBackground, + color: onBackground, fontSize: 32, fontWeight: FontWeight.w600, height: 1.20, @@ -178,33 +177,33 @@ class MobileAppearance extends BaseAppearance { ), // H1 Semi 26 displaySmall: fontStyle.copyWith( - color: colorTheme.onBackground, + color: onBackground, fontWeight: FontWeight.w600, height: 1.10, letterSpacing: 0.13, ), // body2 14 Regular bodyMedium: fontStyle.copyWith( - color: colorTheme.onBackground, + color: onBackground, fontWeight: FontWeight.w400, letterSpacing: 0.07, ), // Trash empty title labelLarge: fontStyle.copyWith( - color: colorTheme.onBackground, + color: onBackground, fontSize: 22, fontWeight: FontWeight.w600, letterSpacing: -0.3, ), // setting item title labelMedium: fontStyle.copyWith( - color: colorTheme.onSurface, + color: onBackground, fontSize: 18, fontWeight: FontWeight.w500, ), // setting group title labelSmall: fontStyle.copyWith( - color: colorTheme.onBackground, + color: onBackground, fontSize: 16, fontWeight: FontWeight.w600, letterSpacing: 0.05, @@ -273,6 +272,8 @@ class MobileAppearance extends BaseAppearance { fontWeight: FontWeight.w400, color: theme.hint, ), + onBackground: onBackground, + background: background, ), ToolbarColorExtension.fromBrightness(brightness), ], diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/time_format_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/time_format_ext.dart index 5da3caa5b9..0dfa2807b9 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/time_format_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/time_format_ext.dart @@ -8,6 +8,6 @@ extension TimeFormatter on UserTimeFormatPB { } final _toFormat = { - UserTimeFormatPB.TwelveHour: DateFormat.Hm(), - UserTimeFormatPB.TwentyFourHour: DateFormat.jm(), + UserTimeFormatPB.TwentyFourHour: DateFormat.Hm(), + UserTimeFormatPB.TwelveHour: DateFormat.jm(), }; diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart index 790375fc20..0fdaa49128 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart @@ -32,6 +32,7 @@ class ShortcutsCubit extends Cubit { error: '', ), ); + try { final customizeShortcuts = await service.getCustomizeShortcuts(); await service.updateCommandShortcuts( @@ -40,7 +41,9 @@ class ShortcutsCubit extends Cubit { ); //sort the shortcuts - commandShortcutEvents.sort((a, b) => a.key.compareTo(b.key)); + commandShortcutEvents.sort( + (a, b) => a.key.toLowerCase().compareTo(b.key.toLowerCase()), + ); emit( state.copyWith( @@ -104,11 +107,11 @@ class ShortcutsCubit extends Cubit { } } - ///Checks if the new command is conflicting with other shortcut - ///We also check using the key, whether this command is a codeblock - ///shortcut, if so we only check a conflict with other codeblock shortcut. + /// Checks if the new command is conflicting with other shortcut + /// We also check using the key, whether this command is a codeblock + /// shortcut, if so we only check a conflict with other codeblock shortcut. String getConflict(CommandShortcutEvent currentShortcut, String command) { - //check if currentShortcut is a codeblock shortcut. + // check if currentShortcut is a codeblock shortcut. final isCodeBlockCommand = currentShortcut.isCodeBlockCommand; for (final e in state.commandShortcutEvents) { diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart index 5c02ea6b11..d7980e031a 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart @@ -56,7 +56,13 @@ class WorkspaceSettingsBloc ?.role ?? AFRolePB.Guest; - emit(state.copyWith(members: members, myRole: role)); + emit( + state.copyWith( + workspace: currentWorkspaceInList, + members: members, + myRole: role, + ), + ); } catch (e) { Log.error('Failed to get or create current workspace'); } 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 4dd934f60b..c85f3bd0b0 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 1cfb89c8f9..ebf1cf2b3f 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 ca3bb62fbb..c3b17cb742 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 0000000000..491ff36786 --- /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 0000000000..737539763a --- /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 fd15cf8d23..bb7eb3600b 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 2a0eb6ec7d..8fdfb9bb44 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'; @@ -274,15 +273,13 @@ class HomeTopBar extends StatelessWidget { Widget build(BuildContext context) { return Container( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.onSecondaryContainer, - border: Border( - bottom: BorderSide(color: Theme.of(context).dividerColor), - ), + color: Theme.of(context).colorScheme.surface, ), - 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 ed5a667e91..e979f102e3 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 0000000000..16415e5e93 --- /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 0000000000..4ec75da1d2 --- /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 0000000000..4ad963f50d --- /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 0000000000..40c2167626 --- /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 0000000000..53a53e5ae9 --- /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 0000000000..e4a335e34b --- /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 364e12644c..0000000000 --- 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 422003fdd9..c5042c4de4 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 ecd7b5f9de..bd83e06934 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, - ), - ); - } - }, - ); - }, - ), - if (state.isExpanded) - ...views.map( - (view) => ViewItem( - key: ValueKey( - '${categoryType.name} ${view.id}', - ), - 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); - } - - 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, - ), - ], - ); - }, ), ); } + + 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, + ), + ); + + context.read().add( + const FolderEvent.expandOrUnExpand( + isExpanded: true, + ), + ); + } + }, + ); + }, + ); + } + + Iterable _buildViews( + BuildContext context, + FolderState state, + ValueNotifier isHovered, + ) { + if (!state.isExpanded) { + return []; + } + + 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 0000000000..c10d14105a --- /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 4ef5480507..051386b7b2 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 dc089e27a2..8d7b0efd50 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 dbbf3f0d0e..93bcccdf06 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 0000000000..f58a1d43c9 --- /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: const FlowySvg( + FlowySvgs.new_app_s, + blendMode: null, + ), + 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 0e2d020f67..8c6d9c5cf7 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 1e5aec2f0e..323ec1428c 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. @@ -211,7 +211,7 @@ class _SidebarState extends State<_Sidebar> { final userState = context.read().state; return DecoratedBox( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, + color: Theme.of(context).colorScheme.surfaceContainerHighest, border: Border( right: BorderSide(color: Theme.of(context).dividerColor), ), @@ -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 eac80118b4..0000000000 --- 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 b4a6eb344a..0000000000 --- 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 39301799d6..53baa2599e 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 100b8d6099..ea0826e82a 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,12 +1,11 @@ import 'dart:math'; -import 'package:flutter/material.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'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; class WorkspaceIcon extends StatefulWidget { const WorkspaceIcon({ @@ -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,7 +36,7 @@ class _WorkspaceIconState extends State { ? Container( width: widget.iconSize, alignment: Alignment.center, - child: FlowyText( + child: FlowyText.emoji( widget.workspace.icon, fontSize: widget.iconSize, ), @@ -46,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, ), ); @@ -62,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 efc4372ee8..0572ae107c 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 141d7d7f4c..bb4d18ab46 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 658d60bfe7..881d926df3 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 9fda07d7d2..5b04e50501 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 Icon(Icons.move_to_inbox); + return const FlowySvg(FlowySvgs.move_to_s); case ViewMoreActionType.openInNewTab: - return const FlowySvg(FlowySvgs.full_view_s); + 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 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: + 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 e020751e0b..1cfff3c6e6 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 057b3d8f99..0ebb919318 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(); + 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, - ], - ); - } 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, - ), - ), - ), - ], - ); - } + 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 0cefde700e..68d179da50 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 81601cbfd4..4760378f4b 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(), ), @@ -86,7 +86,7 @@ class _FlowyTabState extends State { return AFThemeExtension.of(context).lightGreyHover; } - return Theme.of(context).colorScheme.surfaceVariant; + return Theme.of(context).colorScheme.surfaceContainerHighest; } void _closeTab([TapUpDetails? details]) => context 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 436e58f1bf..064d64477f 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,9 +57,9 @@ class _TabsManagerState extends State return Container( alignment: Alignment.bottomLeft, - height: HomeSizes.tabBarHeigth, + height: HomeSizes.tabBarHeight, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, + color: Theme.of(context).colorScheme.surfaceContainerHighest, ), /// TODO(Xazin): Custom Reorderable TabBar diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart index 6e9d575dce..60c19a3051 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart @@ -57,7 +57,7 @@ void showSnackBarMessage( }) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - backgroundColor: Theme.of(context).colorScheme.surfaceVariant, + backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, duration: duration, action: !showCancel ? null diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/inbox_action_bar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/inbox_action_bar.dart index 8ee7a0f102..988ca40fca 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/inbox_action_bar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/inbox_action_bar.dart @@ -81,30 +81,30 @@ class _ToggleUnreadsButtonState extends State<_ToggleUnreadsButton> { showSelectedIcon: false, style: ButtonStyle( tapTargetSize: MaterialTapTargetSize.shrinkWrap, - side: MaterialStatePropertyAll( + side: WidgetStatePropertyAll( BorderSide(color: Theme.of(context).dividerColor), ), - shape: const MaterialStatePropertyAll( + shape: const WidgetStatePropertyAll( RoundedRectangleBorder( borderRadius: Corners.s6Border, ), ), - foregroundColor: MaterialStateProperty.resolveWith( + foregroundColor: WidgetStateProperty.resolveWith( (state) { - if (state.contains(MaterialState.selected)) { + if (state.contains(WidgetState.selected)) { return Theme.of(context).colorScheme.onPrimary; } return AFThemeExtension.of(context).textColor; }, ), - backgroundColor: MaterialStateProperty.resolveWith( + backgroundColor: WidgetStateProperty.resolveWith( (state) { - if (state.contains(MaterialState.selected)) { + if (state.contains(WidgetState.selected)) { return Theme.of(context).colorScheme.primary; } - if (state.contains(MaterialState.hovered)) { + if (state.contains(WidgetState.hovered)) { return AFThemeExtension.of(context).lightGreyHover; } 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 a7925dc3f7..049ee6481b 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 9ee584e5dc..68829d7b66 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,4 @@ -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'; import 'package:appflowy/plugins/base/icon/icon_picker.dart'; @@ -22,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 { @@ -78,46 +78,46 @@ class _SettingsAccountViewState extends State { ], ), - // Enable when/if we need change email feature - // // Only show change email if the user is authenticated and not using local auth - // if (isAuthEnabled && - // state.userProfile.authenticator != AuthenticatorPB.Local) ...[ - // const SettingsCategorySpacer(), - // SettingsCategory( - // title: LocaleKeys.settings_accountPage_email_title.tr(), - // children: [ - // SingleSettingAction( - // label: state.userProfile.email, - // buttonLabel: LocaleKeys - // .settings_accountPage_email_actions_change - // .tr(), - // onPressed: () => SettingsAlertDialog( - // title: LocaleKeys - // .settings_accountPage_email_actions_change - // .tr(), - // confirmLabel: LocaleKeys.button_save.tr(), - // confirm: () { - // context.read().add( - // SettingsUserEvent.updateUserEmail( - // _emailController.text, - // ), - // ); - // Navigator.of(context).pop(); - // }, - // children: [ - // SettingsInputField( - // label: LocaleKeys.settings_accountPage_email_title - // .tr(), - // value: state.userProfile.email, - // hideActions: true, - // textController: _emailController, - // ), - // ], - // ).show(context), - // ), - // ], - // ), - // ], + // Only show email if the user is authenticated and not using local auth + if (isAuthEnabled && + state.userProfile.authenticator != AuthenticatorPB.Local) ...[ + SettingsCategory( + title: LocaleKeys.settings_accountPage_email_title.tr(), + children: [ + FlowyText.regular(state.userProfile.email), + // Enable when/if we need change email feature + // SingleSettingAction( + // label: state.userProfile.email, + // buttonLabel: LocaleKeys + // .settings_accountPage_email_actions_change + // .tr(), + // onPressed: () => SettingsAlertDialog( + // title: LocaleKeys + // .settings_accountPage_email_actions_change + // .tr(), + // confirmLabel: LocaleKeys.button_save.tr(), + // confirm: () { + // context.read().add( + // SettingsUserEvent.updateUserEmail( + // _emailController.text, + // ), + // ); + // Navigator.of(context).pop(); + // }, + // children: [ + // SettingsInputField( + // label: LocaleKeys.settings_accountPage_email_title + // .tr(), + // value: state.userProfile.email, + // hideActions: true, + // textController: _emailController, + // ), + // ], + // ).show(context), + // ), + ], + ), + ], /// Enable when we have change password feature and 2FA // const SettingsCategorySpacer(), @@ -341,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_manage_data_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart index 17d76b4fe1..4ad5d00e1f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart @@ -9,6 +9,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/appflowy_cache_manager.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/tasks/rust_sdk.dart'; +import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/workspace/application/settings/setting_file_importer_bloc.dart'; import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; @@ -55,6 +56,9 @@ class SettingsManageDataView extends StatelessWidget { actions: [ if (state.mapOrNull(didReceivedPath: (_) => true) == true) SettingAction( + tooltip: LocaleKeys + .settings_manageDataPage_dataStorage_actions_resetTooltip + .tr(), icon: const FlowySvg(FlowySvgs.restore_s), label: LocaleKeys.settings_common_reset.tr(), onPressed: () => SettingsAlertDialog( @@ -375,6 +379,8 @@ class _CurrentPathState extends State<_CurrentPath> { @override Widget build(BuildContext context) { + final isLM = Theme.of(context).isLightMode; + return Column( children: [ Row( @@ -392,7 +398,9 @@ class _CurrentPathState extends State<_CurrentPath> { maxLines: 2, overflow: TextOverflow.ellipsis, decoration: isHovering ? TextDecoration.underline : null, - color: const Color(0xFF005483), + color: isLM + ? const Color(0xFF005483) + : Theme.of(context).colorScheme.primary, ), ), ), 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 c1d167844a..aa67ea3158 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,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/plugins/document/application/document_appearance_cubit.dart'; @@ -18,12 +16,12 @@ import 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu import 'package:appflowy/workspace/presentation/settings/shared/document_color_setting_button.dart'; import 'package:appflowy/workspace/presentation/settings/shared/setting_action.dart'; import 'package:appflowy/workspace/presentation/settings/shared/setting_list_tile.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/settings_actionable_input.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_dashed_divider.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_input_field.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_radio_select.dart'; import 'package:appflowy/workspace/presentation/settings/shared/single_setting_action.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart'; @@ -41,39 +39,24 @@ import 'package:flowy_infra/theme_extension.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/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'; -class SettingsWorkspaceView extends StatefulWidget { +class SettingsWorkspaceView extends StatelessWidget { const SettingsWorkspaceView({super.key, required this.userProfile}); final UserProfilePB userProfile; - @override - State createState() => _SettingsWorkspaceViewState(); -} - -class _SettingsWorkspaceViewState extends State { - final TextEditingController _workspaceNameController = - TextEditingController(); - - @override - void dispose() { - _workspaceNameController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { return BlocProvider( create: (context) => WorkspaceSettingsBloc() - ..add(WorkspaceSettingsEvent.initial(userProfile: widget.userProfile)), + ..add(WorkspaceSettingsEvent.initial(userProfile: userProfile)), child: BlocConsumer( listener: (context, state) { - if ((state.workspace?.name ?? '') != _workspaceNameController.text) { - _workspaceNameController.text = state.workspace?.name ?? ''; - } - if (state.deleteWorkspace) { context.read().add( UserWorkspaceEvent.deleteWorkspace( @@ -97,44 +80,11 @@ class _SettingsWorkspaceViewState extends State { description: LocaleKeys.settings_workspacePage_description.tr(), children: [ // We don't allow changing workspace name/icon for local/offline - if (state.workspace != null && - widget.userProfile.authenticator != - AuthenticatorPB.Local) ...[ + if (userProfile.authenticator != AuthenticatorPB.Local) ...[ SettingsCategory( title: LocaleKeys.settings_workspacePage_workspaceName_title .tr(), - children: [ - SettingsActionableInput( - controller: _workspaceNameController, - onSave: (value) => _saveWorkspaceName( - context, - current: state.workspace!.name, - name: value, - ), - actions: [ - SizedBox( - height: 48, - child: FlowyTextButton( - LocaleKeys.button_save.tr(), - padding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 12, - ), - fontWeight: FontWeight.w600, - radius: BorderRadius.circular(12), - fillColor: Theme.of(context).colorScheme.primary, - hoverColor: const Color(0xFF005483), - fontHoverColor: Colors.white, - onPressed: () => _saveWorkspaceName( - context, - current: state.workspace!.name, - name: _workspaceNameController.text, - ), - ), - ), - ], - ), - ], + children: const [_WorkspaceNameSetting()], ), SettingsCategory( title: LocaleKeys.settings_workspacePage_workspaceIcon_title @@ -143,7 +93,10 @@ class _SettingsWorkspaceViewState extends State { .settings_workspacePage_workspaceIcon_description .tr(), children: [ - _WorkspaceIconSetting(workspace: state.workspace!), + _WorkspaceIconSetting( + enableEdit: state.myRole.isOwner, + workspace: state.workspace, + ), ], ), ], @@ -171,7 +124,7 @@ class _SettingsWorkspaceViewState extends State { title: LocaleKeys.settings_workspacePage_textDirection_title.tr(), children: const [ - _TextDirectionSelect(), + TextDirectionSelect(), EnableRTLItemsSwitcher(), ], ), @@ -195,9 +148,7 @@ class _SettingsWorkspaceViewState extends State { title: LocaleKeys.settings_workspacePage_language_title.tr(), children: const [LanguageDropdown()], ), - if (state.workspace != null && - widget.userProfile.authenticator != - AuthenticatorPB.Local) ...[ + if (userProfile.authenticator != AuthenticatorPB.Local) ...[ SingleSettingAction( label: LocaleKeys.settings_workspacePage_manageWorkspace_title .tr(), @@ -244,17 +195,115 @@ class _SettingsWorkspaceViewState extends State { ), ); } +} - void _saveWorkspaceName( - BuildContext context, { - required String current, +class _WorkspaceNameSetting extends StatefulWidget { + const _WorkspaceNameSetting(); + + @override + State<_WorkspaceNameSetting> createState() => _WorkspaceNameSettingState(); +} + +class _WorkspaceNameSettingState extends State<_WorkspaceNameSetting> { + final TextEditingController workspaceNameController = TextEditingController(); + late final FocusNode focusNode; + bool isEditing = false; + + @override + void initState() { + super.initState(); + focusNode = FocusNode( + onKeyEvent: (_, event) { + if (event is KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.escape && + isEditing && + mounted) { + setState(() => isEditing = false); + return KeyEventResult.handled; + } + + return KeyEventResult.ignored; + }, + )..addListener(() { + if (!focusNode.hasFocus && isEditing && mounted) { + _saveWorkspaceName(name: workspaceNameController.text); + setState(() => isEditing = false); + } + }); + } + + @override + void dispose() { + focusNode.dispose(); + workspaceNameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (_, state) { + final newName = state.workspace?.name; + if (newName != null && newName != workspaceNameController.text) { + workspaceNameController.text = newName; + } + }, + builder: (_, state) { + if (isEditing) { + return Flexible( + child: SettingsInputField( + textController: workspaceNameController, + value: workspaceNameController.text, + focusNode: focusNode..requestFocus(), + onCancel: () => setState(() => isEditing = false), + onSave: (_) { + _saveWorkspaceName(name: workspaceNameController.text); + setState(() => isEditing = false); + }, + ), + ); + } + + return Row( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 2.5), + child: FlowyText.regular( + workspaceNameController.text, + fontSize: 14, + ), + ), + if (state.myRole.isOwner) ...[ + const HSpace(4), + FlowyTooltip( + message: LocaleKeys + .settings_workspacePage_workspaceName_editTooltip + .tr(), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => setState(() => isEditing = true), + child: const FlowyHover( + resetHoverOnRebuild: false, + child: Padding( + padding: EdgeInsets.all(4), + child: FlowySvg(FlowySvgs.edit_s), + ), + ), + ), + ), + ], + ], + ); + }, + ); + } + + void _saveWorkspaceName({ required String name, }) { - if (name.isNotEmpty && name != current) { + if (name.isNotEmpty) { context.read().add( - WorkspaceSettingsEvent.updateWorkspaceName( - _workspaceNameController.text, - ), + WorkspaceSettingsEvent.updateWorkspaceName(name), ); if (context.mounted) { @@ -300,12 +349,21 @@ class LanguageDropdown extends StatelessWidget { } class _WorkspaceIconSetting extends StatelessWidget { - const _WorkspaceIconSetting({required this.workspace}); + const _WorkspaceIconSetting({required this.enableEdit, this.workspace}); - final UserWorkspacePB workspace; + final bool enableEdit; + final UserWorkspacePB? workspace; @override Widget build(BuildContext context) { + if (workspace == null) { + return const SizedBox( + height: 64, + width: 64, + child: CircularProgressIndicator(), + ); + } + return Container( height: 64, width: 64, @@ -316,8 +374,9 @@ class _WorkspaceIconSetting extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(1), child: WorkspaceIcon( - workspace: workspace, - iconSize: workspace.icon.isNotEmpty == true ? 46 : 20, + workspace: workspace!, + iconSize: workspace!.icon.isNotEmpty == true ? 46 : 20, + fontSize: 16.0, enableEdit: true, onSelected: (r) => context .read() @@ -328,19 +387,25 @@ class _WorkspaceIconSetting extends StatelessWidget { } } -class _TextDirectionSelect extends StatelessWidget { - const _TextDirectionSelect(); +@visibleForTesting +class TextDirectionSelect extends StatelessWidget { + const TextDirectionSelect({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - final selectedItem = state.textDirection ?? AppFlowyTextDirection.auto; + final selectedItem = state.textDirection ?? AppFlowyTextDirection.ltr; return SettingsRadioSelect( - onChanged: (item) => context - .read() - .setTextDirection(item.value), + onChanged: (item) { + context + .read() + .setTextDirection(item.value); + context + .read() + .syncDefaultTextDirection(item.value.name); + }, items: [ SettingsRadioItem( value: AppFlowyTextDirection.ltr, @@ -508,6 +573,7 @@ class _DateTimeFormatLabel extends StatelessWidget { now.timeZoneName, ], ), + maxLines: 2, fontSize: 16, color: AFThemeExtension.of(context).secondaryTextColor, ); @@ -712,6 +778,9 @@ class AppearanceSelector extends StatelessWidget { ), ), ), + child: t != themeMode + ? null + : const _SelectedModeIndicator(), ), const VSpace(6), FlowyText.regular(getLabel(t), textAlign: TextAlign.center), @@ -735,6 +804,38 @@ class AppearanceSelector extends StatelessWidget { }; } +class _SelectedModeIndicator extends StatelessWidget { + const _SelectedModeIndicator(); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Positioned( + top: 4, + left: 4, + child: Material( + shape: const CircleBorder(), + elevation: 2, + child: Container( + decoration: const BoxDecoration( + shape: BoxShape.circle, + ), + height: 16, + width: 16, + child: const FlowySvg( + FlowySvgs.settings_selected_theme_m, + size: Size.square(16), + blendMode: BlendMode.dstIn, + ), + ), + ), + ), + ], + ); + } +} + class _FontSelectorDropdown extends StatelessWidget { const _FontSelectorDropdown(); @@ -777,6 +878,7 @@ class _FontSelectorDropdown extends StatelessWidget { selectedValue: appearance.font, value: font, label: font.fontFamilyDisplayName, + fontFamily: font, ), ) .toList(), @@ -834,7 +936,7 @@ class _CursorColorValueWidget extends StatelessWidget { FlowyText( LocaleKeys.appName.tr(), // To avoid the text color changes when it is hovered in dark mode - color: Theme.of(context).colorScheme.onBackground, + color: AFThemeExtension.of(context).onBackground, ), ], ); @@ -885,7 +987,7 @@ class _SelectionColorValueWidget extends StatelessWidget { @override Widget build(BuildContext context) { // To avoid the text color changes when it is hovered in dark mode - final textColor = Theme.of(context).colorScheme.onBackground; + final textColor = AFThemeExtension.of(context).onBackground; return Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart index ef4c374239..31e5959002 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart @@ -1,3 +1,5 @@ +import 'package:appflowy/shared/google_fonts_extension.dart'; +import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:flutter/material.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; @@ -10,23 +12,33 @@ DropdownMenuEntry buildDropdownMenuEntry( T? selectedValue, Widget? leadingWidget, Widget? trailingWidget, + String? fontFamily, }) { + final fontFamilyUsed = fontFamily != null + ? getGoogleFontSafely(fontFamily).fontFamily ?? defaultFontFamily + : defaultFontFamily; + return DropdownMenuEntry( style: ButtonStyle( foregroundColor: - MaterialStatePropertyAll(Theme.of(context).colorScheme.primary), - padding: MaterialStateProperty.all( + WidgetStatePropertyAll(Theme.of(context).colorScheme.primary), + padding: WidgetStateProperty.all( const EdgeInsets.symmetric(horizontal: 6, vertical: 4), ), - minimumSize: const MaterialStatePropertyAll(Size(double.infinity, 29)), - maximumSize: const MaterialStatePropertyAll(Size(double.infinity, 29)), + minimumSize: const WidgetStatePropertyAll(Size(double.infinity, 29)), + maximumSize: const WidgetStatePropertyAll(Size(double.infinity, 29)), ), value: value, label: label, leadingIcon: leadingWidget, labelWidget: Padding( padding: const EdgeInsets.symmetric(vertical: 4), - child: FlowyText.medium(label, fontSize: 14, textAlign: TextAlign.start), + child: FlowyText.medium( + label, + fontSize: 14, + textAlign: TextAlign.start, + fontFamily: fontFamilyUsed, + ), ), trailingIcon: Row( children: [ diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_value_dropdown.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_value_dropdown.dart index 12bf1c1480..ba6ae1416f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_value_dropdown.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_value_dropdown.dart @@ -1,7 +1,7 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; class SettingValueDropDown extends StatefulWidget { const SettingValueDropDown({ @@ -45,7 +45,7 @@ class _SettingValueDropDownState extends State { child: widget.child ?? FlowyTextButton( widget.currentValue, - fontColor: Theme.of(context).colorScheme.onBackground, + fontColor: AFThemeExtension.maybeOf(context)?.onBackground, fillColor: Colors.transparent, onPressed: () {}, ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart index 67c0dc4cf9..42f407f97a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart @@ -1,9 +1,13 @@ +import 'package:appflowy/shared/google_fonts_extension.dart'; +import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; +import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:flutter/material.dart'; import 'package:appflowy/flutter/af_dropdown_menu.dart'; import 'package:collection/collection.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class SettingsDropdown extends StatefulWidget { const SettingsDropdown({ @@ -37,6 +41,10 @@ class _SettingsDropdownState extends State> { @override Widget build(BuildContext context) { + final fontFamily = context.read().state.font; + final fontFamilyUsed = + getGoogleFontSafely(fontFamily).fontFamily ?? defaultFontFamily; + return Row( children: [ Expanded( @@ -45,16 +53,20 @@ class _SettingsDropdownState extends State> { expandedInsets: widget.expandWidth ? EdgeInsets.zero : null, initialSelection: widget.selectedOption, dropdownMenuEntries: widget.options, + textStyle: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith(fontFamily: fontFamilyUsed), menuStyle: MenuStyle( maximumSize: - const MaterialStatePropertyAll(Size(double.infinity, 250)), - elevation: const MaterialStatePropertyAll(10), + const WidgetStatePropertyAll(Size(double.infinity, 250)), + elevation: const WidgetStatePropertyAll(10), shadowColor: - MaterialStatePropertyAll(Colors.black.withOpacity(0.4)), - backgroundColor: MaterialStatePropertyAll( + WidgetStatePropertyAll(Colors.black.withOpacity(0.4)), + backgroundColor: WidgetStatePropertyAll( Theme.of(context).cardColor, ), - padding: const MaterialStatePropertyAll( + padding: const WidgetStatePropertyAll( EdgeInsets.symmetric(horizontal: 6, vertical: 8), ), alignment: Alignment.bottomLeft, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/flowy_emoji_picker_config.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/flowy_emoji_picker_config.dart index 066d63f380..4ef6e00994 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/flowy_emoji_picker_config.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/flowy_emoji_picker_config.dart @@ -29,6 +29,6 @@ EmojiPickerConfig buildFlowyEmojiPickerConfig(BuildContext context) { noRecentsText: LocaleKeys.emoji_noRecent.tr(), noRecentsStyle: style.textTheme.bodyMedium, noEmojiFoundText: LocaleKeys.emoji_noEmojiFound.tr(), - scrollBarHandleColor: style.colorScheme.onBackground, + scrollBarHandleColor: style.colorScheme.onSurface, ); } 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 43f9438ff7..e4e15da75d 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 751599def3..decf74f874 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/settings/widgets/members/workspace_member_page.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart index e104879752..6b9e55fc7d 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart @@ -5,7 +5,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/af_role_pb_extension.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; @@ -37,8 +36,6 @@ class WorkspaceMembersPage extends StatelessWidget { title: LocaleKeys.settings_appearance_members_title.tr(), children: [ if (state.myRole.canInvite) const _InviteMember(), - if (state.myRole.canInvite && state.members.isNotEmpty) - const SettingsCategorySpacer(), if (state.members.isNotEmpty) _MemberList( members: state.members, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart index 6805633f74..a75cd87171 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart @@ -18,6 +18,7 @@ import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flowy_infra/theme_extension.dart'; class AppFlowyCloudViewSetting extends StatelessWidget { const AppFlowyCloudViewSetting({ @@ -289,7 +290,7 @@ class CloudURLInputState extends State { .copyWith(fontWeight: FontWeight.w400, fontSize: 16), enabledBorder: UnderlineInputBorder( borderSide: - BorderSide(color: Theme.of(context).colorScheme.onBackground), + BorderSide(color: AFThemeExtension.of(context).onBackground), ), focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: Theme.of(context).colorScheme.primary), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_cloud.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_cloud.dart index 99d272d122..10e69be87a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_cloud.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_cloud.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/env/env.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; @@ -15,7 +13,9 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.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_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -127,7 +127,7 @@ class CloudTypeSwitcher extends StatelessWidget { child: FlowyTextButton( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 6), titleFromCloudType(cloudType), - fontColor: Theme.of(context).colorScheme.onBackground, + fontColor: AFThemeExtension.of(context).onBackground, fillColor: Colors.transparent, onPressed: () {}, ), @@ -159,7 +159,6 @@ class CloudTypeSwitcher extends StatelessWidget { showHeader: true, showDragHandle: true, showDivider: false, - showCloseButton: false, title: LocaleKeys.settings_menu_cloudServerType.tr(), builder: (context) { return Column( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_cloud.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_cloud.dart index c014cdf516..29fab1805f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_cloud.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_cloud.dart @@ -1,7 +1,3 @@ -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/supabase_cloud_setting_bloc.dart'; @@ -15,9 +11,13 @@ import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class SettingSupabaseCloudView extends StatelessWidget { @@ -293,7 +293,7 @@ class SupabaseInputState extends State { .copyWith(fontWeight: FontWeight.w400, fontSize: 16), enabledBorder: UnderlineInputBorder( borderSide: - BorderSide(color: Theme.of(context).colorScheme.onBackground), + BorderSide(color: AFThemeExtension.of(context).onBackground), ), focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: Theme.of(context).colorScheme.primary), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart index ed9b4dcd89..8066d9b65e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart @@ -84,7 +84,7 @@ class ShortcutsListView extends StatelessWidget { } } -class ShortcutsListTile extends StatelessWidget { +class ShortcutsListTile extends StatefulWidget { const ShortcutsListTile({ super.key, required this.shortcutEvent, @@ -92,6 +92,25 @@ class ShortcutsListTile extends StatelessWidget { final CommandShortcutEvent shortcutEvent; + @override + State createState() => _ShortcutsListTileState(); +} + +class _ShortcutsListTileState extends State { + late final TextEditingController controller; + + @override + void initState() { + controller = TextEditingController(text: widget.shortcutEvent.command); + super.initState(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Column( @@ -100,16 +119,16 @@ class ShortcutsListTile extends StatelessWidget { children: [ Expanded( child: FlowyText.medium( - key: Key(shortcutEvent.key), - shortcutEvent.description!.capitalize(), + key: Key(widget.shortcutEvent.key), + widget.shortcutEvent.description!.capitalize(), overflow: TextOverflow.ellipsis, ), ), FlowyTextButton( - shortcutEvent.command, + widget.shortcutEvent.command, fontColor: AFThemeExtension.of(context).textColor, fillColor: Colors.transparent, - onPressed: () => showKeyListenerDialog(context), + onPressed: () => showKeyListenerDialog(context, controller), ), ], ), @@ -120,8 +139,10 @@ class ShortcutsListTile extends StatelessWidget { ); } - void showKeyListenerDialog(BuildContext widgetContext) { - final controller = TextEditingController(text: shortcutEvent.command); + void showKeyListenerDialog( + BuildContext widgetContext, + TextEditingController controller, + ) { showDialog( context: widgetContext, builder: (builderContext) { @@ -131,9 +152,10 @@ class ShortcutsListTile extends StatelessWidget { content: KeyboardListener( focusNode: FocusNode(), onKeyEvent: (key) { + if (key is! KeyDownEvent) return; if (key.logicalKey == LogicalKeyboardKey.enter && !HardwareKeyboard.instance.isShiftPressed) { - if (controller.text == shortcutEvent.command) { + if (controller.text == widget.shortcutEvent.command) { _dismiss(builderContext); } if (formKey.currentState!.validate()) { @@ -166,12 +188,12 @@ class ShortcutsListTile extends StatelessWidget { ), ); }, - ).then((_) => controller.dispose()); + ); } String? _validateForConflicts(BuildContext context, String command) { final conflict = BlocProvider.of(context).getConflict( - shortcutEvent, + widget.shortcutEvent, command, ); if (conflict.isEmpty) return null; @@ -182,7 +204,7 @@ class ShortcutsListTile extends StatelessWidget { } void _updateKey(BuildContext context, String command) { - shortcutEvent.updateCommand(command: command); + widget.shortcutEvent.updateCommand(command: command); BlocProvider.of(context).updateAllShortcuts(); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart index 0148db171b..e44af72edd 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart @@ -32,7 +32,7 @@ class SettingsMenu extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 8) + const EdgeInsets.only(left: 8, right: 4), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, + color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: const BorderRadius.only( topLeft: Radius.circular(8), bottomLeft: Radius.circular(8), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_decoration.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_decoration.dart index 260918c7de..f3cd25afde 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_decoration.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_decoration.dart @@ -1,4 +1,5 @@ import 'package:dotted_border/dotted_border.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flutter/material.dart'; import 'theme_upload_view.dart'; @@ -15,7 +16,7 @@ class ThemeUploadDecoration extends StatelessWidget { borderRadius: BorderRadius.circular(ThemeUploadWidget.borderRadius), color: Theme.of(context).colorScheme.surface, border: Border.all( - color: Theme.of(context).colorScheme.onBackground.withOpacity( + color: AFThemeExtension.of(context).onBackground.withOpacity( ThemeUploadWidget.fadeOpacity, ), ), @@ -26,7 +27,7 @@ class ThemeUploadDecoration extends StatelessWidget { dashPattern: const [6, 6], color: Theme.of(context) .colorScheme - .onBackground + .onSurface .withOpacity(ThemeUploadWidget.fadeOpacity), radius: const Radius.circular(ThemeUploadWidget.borderRadius), child: ClipRRect( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_failure_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_failure_widget.dart index 379c78acd5..edb382d6ee 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_failure_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_failure_widget.dart @@ -1,5 +1,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; @@ -24,7 +25,7 @@ class ThemeUploadFailureWidget extends StatelessWidget { FlowySvg( FlowySvgs.close_m, size: ThemeUploadWidget.iconSize, - color: Theme.of(context).colorScheme.onBackground, + color: AFThemeExtension.of(context).onBackground, ), FlowyText.medium( errorMessage, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_learn_more_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_learn_more_button.dart index 628232bd71..d57d2d2a00 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_learn_more_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_learn_more_button.dart @@ -8,6 +8,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; +import 'package:flowy_infra/theme_extension.dart'; class ThemeUploadLearnMoreButton extends StatelessWidget { const ThemeUploadLearnMoreButton({super.key}); @@ -21,7 +22,7 @@ class ThemeUploadLearnMoreButton extends StatelessWidget { height: ThemeUploadWidget.buttonSize.height, child: IntrinsicWidth( child: SecondaryButton( - outlineColor: Theme.of(context).colorScheme.onBackground, + outlineColor: AFThemeExtension.of(context).onBackground, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: FlowyText.medium( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_loading_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_loading_widget.dart index 643189a38f..5e0ad15f38 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_loading_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_loading_widget.dart @@ -13,7 +13,7 @@ class ThemeUploadLoadingWidget extends StatelessWidget { padding: ThemeUploadWidget.padding, color: Theme.of(context) .colorScheme - .background + .surface .withOpacity(ThemeUploadWidget.fadeOpacity), constraints: const BoxConstraints.expand(), child: Column( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/upload_new_theme_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/upload_new_theme_widget.dart index 967f5b0b0f..0113d26a37 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/upload_new_theme_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/upload_new_theme_widget.dart @@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -13,7 +14,7 @@ class UploadNewThemeWidget extends StatelessWidget { return Container( color: Theme.of(context) .colorScheme - .background + .surface .withOpacity(ThemeUploadWidget.fadeOpacity), padding: ThemeUploadWidget.padding, child: Column( @@ -23,7 +24,7 @@ class UploadNewThemeWidget extends StatelessWidget { FlowySvg( FlowySvgs.folder_m, size: ThemeUploadWidget.iconSize, - color: Theme.of(context).colorScheme.onBackground, + color: AFThemeExtension.of(context).onBackground, ), FlowyText.medium( LocaleKeys.settings_appearance_themeUpload_description.tr(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart index 513f72b4ed..66cad4ab38 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart @@ -214,7 +214,7 @@ class NavigatorOkCancelDialog extends StatelessWidget { ), VSpace(Insets.sm * 1.5), Container( - color: Theme.of(context).colorScheme.surfaceVariant, + color: Theme.of(context).colorScheme.surfaceContainerHighest, height: 1, ), VSpace(Insets.m * 1.5), 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 4c52e6c278..e25c81b182 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 e72dfa098c..1c8c8d20ff 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 3d0416f589..e4bf942c6a 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 bb285a7917..d4103cc6bf 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 b12ae6644a..ab67ecb5b4 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 4b6708c151..5ad85efe31 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 9d65cc06a1..18b206bd9e 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 isEditable = widget.behavior == _ViewTitleBehavior.editable; - 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, - ), - ), - ], + 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), ), ); } - void _resetTextEditingController() { - inputtingName = name; + 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(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 8e357d7ca1..65498b121f 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 d9420944fb..405db2bee8 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/lib/colorscheme/default_colorscheme.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/default_colorscheme.dart index 9f0cc50c93..b1c80fab64 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/default_colorscheme.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/default_colorscheme.dart @@ -13,12 +13,12 @@ const _lightShader5 = Color(0xFFe0e0e0); const _lightShader6 = Color(0xFFf2f2f2); const _lightMain1 = Color(0xFF00bcf0); const _lightTint9 = Color(0xFFe1fbFF); -const _darkShader1 = Color(0xFF131720); +const _darkShader1 = Color(0xE5FFFFFF); const _darkShader2 = Color(0xFF1A202C); const _darkShader3 = Color(0xFF363D49); const _darkShader5 = Color(0xFFBBC3CD); const _darkShader6 = Color(0xFFF2F2F2); -const _darkMain1 = Color(0xFF00BCF0); +const _darkMain1 = Color(0x19FFFFFF); const _darkInput = Color(0xFF282E3A); class DefaultColorScheme extends FlowyColorScheme { diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart index 2eeb901ef4..70128c44bd 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart @@ -5,6 +5,9 @@ class AFThemeExtension extends ThemeExtension { static AFThemeExtension of(BuildContext context) => Theme.of(context).extension()!; + static AFThemeExtension? maybeOf(BuildContext context) => + Theme.of(context).extension(); + const AFThemeExtension({ required this.warning, required this.success, @@ -32,6 +35,8 @@ class AFThemeExtension extends ThemeExtension { required this.progressBarBGColor, required this.toggleButtonBGColor, required this.gridRowCountColor, + required this.background, + required this.onBackground, }); final Color? warning; @@ -64,6 +69,9 @@ class AFThemeExtension extends ThemeExtension { final TextStyle callout; final TextStyle caption; + final Color background; + final Color onBackground; + @override AFThemeExtension copyWith({ Color? warning, @@ -92,6 +100,8 @@ class AFThemeExtension extends ThemeExtension { TextStyle? code, TextStyle? callout, TextStyle? caption, + Color? background, + Color? onBackground, }) => AFThemeExtension( warning: warning ?? this.warning, @@ -121,6 +131,8 @@ class AFThemeExtension extends ThemeExtension { code: code ?? this.code, callout: callout ?? this.callout, caption: caption ?? this.caption, + onBackground: onBackground ?? this.onBackground, + background: background ?? this.background, ); @override @@ -165,6 +177,8 @@ class AFThemeExtension extends ThemeExtension { code: other.code, callout: other.callout, caption: other.caption, + onBackground: Color.lerp(onBackground, other.onBackground, t)!, + background: Color.lerp(background, other.background, t)!, ); } } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/example/pubspec.yaml b/frontend/appflowy_flutter/packages/flowy_infra_ui/example/pubspec.yaml index 3cd07738f8..8c7793d7cc 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/example/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/example/pubspec.yaml @@ -4,7 +4,7 @@ description: Demonstrates how to use the flowy_infra_ui plugin. publish_to: 'none' # Remove this line if you wish to publish to pub.dev environment: - flutter: ">=3.19.0" + flutter: ">=3.22.0" sdk: ">=3.1.5 <4.0.0" dependencies: 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 59a2bc6533..7bbcbf0949 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/flowy_infra_ui.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/flowy_infra_ui.dart index cb147e2782..bbdeda8de3 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/flowy_infra_ui.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/flowy_infra_ui.dart @@ -1,22 +1,19 @@ // Basis +export '/widget/separated_flex.dart'; +export '/widget/spacing.dart'; export 'basis.dart'; - -// Keyboard -export 'src/keyboard/keyboard_visibility_detector.dart'; - +export 'src/flowy_overlay/appflowy_popover.dart'; +export 'src/flowy_overlay/flowy_dialog.dart'; // Overlay export 'src/flowy_overlay/flowy_overlay.dart'; export 'src/flowy_overlay/list_overlay.dart'; export 'src/flowy_overlay/option_overlay.dart'; -export 'src/flowy_overlay/flowy_dialog.dart'; -export 'src/flowy_overlay/appflowy_popover.dart'; +// Keyboard +export 'src/keyboard/keyboard_visibility_detector.dart'; +export 'style_widget/button.dart'; +export 'style_widget/color_picker.dart'; +export 'style_widget/icon_button.dart'; +export 'style_widget/scrolling/styled_list.dart'; +export 'style_widget/scrolling/styled_scroll_bar.dart'; export 'style_widget/text.dart'; export 'style_widget/text_field.dart'; - -export 'style_widget/button.dart'; -export 'style_widget/icon_button.dart'; -export 'style_widget/scrolling/styled_scroll_bar.dart'; -export '/widget/spacing.dart'; -export '/widget/separated_flex.dart'; -export 'style_widget/scrolling/styled_list.dart'; -export 'style_widget/color_picker.dart'; 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 832eca88e0..acd628a222 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; @@ -128,7 +127,7 @@ class FlowyButton extends StatelessWidget { (Platform.isIOS || Platform.isAndroid) ? BoxDecoration( border: Border.all( - color: Theme.of(context).colorScheme.surfaceVariant, + color: Theme.of(context).colorScheme.surfaceContainerHighest, width: 1.0, )) : null); @@ -210,12 +209,12 @@ class FlowyTextButton extends StatelessWidget { onPressed: onPressed ?? () {}, focusNode: FocusNode(skipTraversal: onPressed == null), style: ButtonStyle( - overlayColor: const MaterialStatePropertyAll(Colors.transparent), + overlayColor: const WidgetStatePropertyAll(Colors.transparent), splashFactory: NoSplash.splashFactory, tapTargetSize: MaterialTapTargetSize.shrinkWrap, - padding: MaterialStateProperty.all(padding), - elevation: MaterialStateProperty.all(0), - shape: MaterialStateProperty.all( + padding: WidgetStateProperty.all(padding), + elevation: WidgetStateProperty.all(0), + shape: WidgetStateProperty.all( RoundedRectangleBorder( side: BorderSide( color: isDangerous @@ -225,7 +224,7 @@ class FlowyTextButton extends StatelessWidget { borderRadius: radius ?? Corners.s6Border, ), ), - textStyle: MaterialStateProperty.all( + textStyle: WidgetStateProperty.all( TextStyle( fontWeight: fontWeight ?? FontWeight.w500, fontSize: fontSize, @@ -233,9 +232,9 @@ class FlowyTextButton extends StatelessWidget { fontFamily: fontFamily, ), ), - backgroundColor: MaterialStateProperty.resolveWith( + backgroundColor: WidgetStateProperty.resolveWith( (states) { - if (states.contains(MaterialState.hovered)) { + if (states.contains(WidgetState.hovered)) { return hoverColor ?? (isDangerous ? Theme.of(context).colorScheme.error @@ -248,9 +247,9 @@ class FlowyTextButton extends StatelessWidget { : Theme.of(context).colorScheme.secondaryContainer); }, ), - foregroundColor: MaterialStateProperty.resolveWith( + foregroundColor: WidgetStateProperty.resolveWith( (states) { - if (states.contains(MaterialState.hovered)) { + if (states.contains(WidgetState.hovered)) { return fontHoverColor ?? (fontColor ?? Theme.of(context).colorScheme.onSurface); } 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 1a4b96ecb5..0902d78835 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/snap_bar.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/snap_bar.dart index 778aee0a74..8752a5985f 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/snap_bar.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/snap_bar.dart @@ -9,7 +9,7 @@ void showSnapBar(BuildContext context, String title, {VoidCallback? onClosed}) { ScaffoldMessenger.of(context) .showSnackBar( SnackBar( - backgroundColor: Theme.of(context).colorScheme.surfaceVariant, + backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, duration: const Duration(milliseconds: 8000), content: FlowyText( title, 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 65d0c19c59..88ebc735b7 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/appflowy_flutter/packages/flowy_infra_ui/lib/widget/error_page.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/error_page.dart index ac44197abe..c79f430942 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/error_page.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/error_page.dart @@ -1,3 +1,4 @@ +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_svg/flowy_svg.dart'; import 'package:flutter/material.dart'; @@ -155,7 +156,7 @@ class StackTracePreview extends StatelessWidget { Align( alignment: Alignment.centerRight, child: FlowyButton( - hoverColor: Theme.of(context).colorScheme.onBackground, + hoverColor: AFThemeExtension.of(context).onBackground, text: const FlowyText( "Copy", ), diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_button.dart index 11b71b7d28..a73d96f454 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_button.dart @@ -83,7 +83,7 @@ class RoundedImageButton extends StatelessWidget { child: TextButton( onPressed: press, style: ButtonStyle( - shape: MaterialStateProperty.all( + shape: WidgetStateProperty.all( RoundedRectangleBorder(borderRadius: borderRadius))), child: child, ), diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index b46a113767..dab9ec0475 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -53,11 +53,11 @@ packages: dependency: "direct main" description: path: "." - ref: b827d08 - resolved-ref: b827d089b6e97762806075953a433cfcbe697a73 + ref: "0c79b870586f4bc5c23b61b327c51fe6a8856b47" + resolved-ref: "0c79b870586f4bc5c23b61b327c51fe6a8856b47" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git - version: "2.4.0" + version: "2.5.1" appflowy_editor_plugins: dependency: "direct main" description: @@ -949,10 +949,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" intl_utils: dependency: transitive description: @@ -1037,26 +1037,26 @@ packages: dependency: "direct main" description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" linked_scroll_controller: dependency: "direct main" description: @@ -1149,10 +1149,10 @@ packages: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" mime: dependency: transitive description: @@ -1525,10 +1525,10 @@ packages: dependency: "direct main" description: name: scaled_app - sha256: "3415fad16d1cf283112988985ccd14c4cd28bf48cbe6432d59e158f3b632d58d" + sha256: a2ad9f22cf2200a5ce455b59c5ea7bfb09a84acfc52452d1db54f4958c99d76a url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" screen_retriever: dependency: transitive description: @@ -1860,10 +1860,10 @@ packages: dependency: "direct main" description: name: table_calendar - sha256: "1e3521a3e6d3fc7f645a58b135ab663d458ab12504f1ea7f9b4b81d47086c478" + sha256: b759eb6caa88dda8e51c70ee43c19d1682f8244458f84cced9138ee35b2ce416 url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.1.1" term_glyph: dependency: transitive description: @@ -1876,26 +1876,26 @@ packages: dependency: transitive description: name: test - sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f + sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" url: "https://pub.dev" source: hosted - version: "1.24.9" + version: "1.25.2" test_api: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.0" test_core: dependency: transitive description: name: test_core - sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a + sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" url: "https://pub.dev" source: hosted - version: "0.5.9" + version: "0.6.0" textstyle_extensions: dependency: transitive description: @@ -2101,10 +2101,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.1" watcher: dependency: transitive description: @@ -2203,4 +2203,4 @@ packages: version: "2.0.0" sdks: dart: ">=3.3.0 <4.0.0" - flutter: ">=3.19.0" + flutter: ">=3.22.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index fa2e0f5c9f..35050161f7 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -15,10 +15,10 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.5.7 +version: 0.5.8 environment: - flutter: ">=3.19.0" + flutter: ">=3.22.0" sdk: ">=3.3.0 <4.0.0" # Dependencies specify other packages that your package needs in order to work. @@ -55,7 +55,7 @@ dependencies: path: packages/appflowy_popover # third party packages - intl: ^0.18.0 + intl: ^0.19.0 time: ^2.1.3 equatable: ^2.0.5 freezed_annotation: ^2.2.0 @@ -135,7 +135,7 @@ dependencies: numerus: ^2.1.2 flutter_animate: ^4.5.0 permission_handler: ^11.3.1 - scaled_app: ^2.2.0 + scaled_app: ^2.3.0 dev_dependencies: flutter_lints: ^3.0.1 @@ -172,7 +172,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "b827d08" + ref: "0c79b870586f4bc5c23b61b327c51fe6a8856b47" sheet: git: diff --git a/frontend/appflowy_flutter/test/widget_test/direction_setting_test.dart b/frontend/appflowy_flutter/test/widget_test/direction_setting_test.dart new file mode 100644 index 0000000000..34472193f9 --- /dev/null +++ b/frontend/appflowy_flutter/test/widget_test/direction_setting_test.dart @@ -0,0 +1,159 @@ +import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_radio_select.dart'; +import 'package:flowy_infra/theme.dart'; +import 'package:flutter/material.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; +import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:bloc_test/bloc_test.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart'; +import 'package:appflowy/user/application/user_settings_service.dart'; + +import '../util.dart'; + +class MockAppearanceSettingsBloc + extends MockBloc + implements AppearanceSettingsCubit {} + +class MockDocumentAppearanceCubit extends Mock + implements DocumentAppearanceCubit {} + +class MockDocumentAppearance extends Mock implements DocumentAppearance {} + +void main() { + late AppearanceSettingsPB appearanceSettings; + late DateTimeSettingsPB dateTimeSettings; + + setUp(() async { + await AppFlowyUnitTest.ensureInitialized(); + appearanceSettings = + await UserSettingsBackendService().getAppearanceSetting(); + dateTimeSettings = await UserSettingsBackendService().getDateTimeSettings(); + }); + + testWidgets('TextDirectionSelect update default text direction setting', + (WidgetTester tester) async { + final appearanceSettingsState = AppearanceSettingsState.initial( + AppTheme.fallback, + appearanceSettings.themeMode, + appearanceSettings.font, + appearanceSettings.monospaceFont, + appearanceSettings.layoutDirection, + appearanceSettings.textDirection, + appearanceSettings.enableRtlToolbarItems, + appearanceSettings.locale, + appearanceSettings.isMenuCollapsed, + appearanceSettings.menuOffset, + dateTimeSettings.dateFormat, + dateTimeSettings.timeFormat, + dateTimeSettings.timezoneId, + appearanceSettings.documentSetting.cursorColor.isEmpty + ? null + : Color( + int.parse(appearanceSettings.documentSetting.cursorColor), + ), + appearanceSettings.documentSetting.selectionColor.isEmpty + ? null + : Color( + int.parse( + appearanceSettings.documentSetting.selectionColor, + ), + ), + 1.0, + ); + final mockAppearanceSettingsBloc = MockAppearanceSettingsBloc(); + when(() => mockAppearanceSettingsBloc.state).thenReturn( + appearanceSettingsState, + ); + + final mockDocumentAppearanceCubit = MockDocumentAppearanceCubit(); + when(() => mockDocumentAppearanceCubit.stream).thenAnswer( + (_) => Stream.fromIterable([MockDocumentAppearance()]), + ); + + await tester.pumpWidget( + MultiBlocProvider( + providers: [ + BlocProvider.value( + value: mockAppearanceSettingsBloc, + ), + BlocProvider.value( + value: mockDocumentAppearanceCubit, + ), + ], + child: MaterialApp( + theme: appearanceSettingsState.lightTheme, + home: MultiBlocProvider( + providers: [ + BlocProvider.value( + value: mockAppearanceSettingsBloc, + ), + BlocProvider.value( + value: mockDocumentAppearanceCubit, + ), + ], + child: const Scaffold( + body: TextDirectionSelect(), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect( + find.text( + LocaleKeys.settings_workspacePage_textDirection_leftToRight.tr(), + ), + findsOne, + ); + expect( + find.text( + LocaleKeys.settings_workspacePage_textDirection_rightToLeft.tr(), + ), + findsOne, + ); + expect( + find.text( + LocaleKeys.settings_workspacePage_textDirection_auto.tr(), + ), + findsOne, + ); + + final radioSelectFinder = + find.byType(SettingsRadioSelect); + expect(radioSelectFinder, findsOne); + + when( + () => mockAppearanceSettingsBloc.setTextDirection( + any(), + ), + ).thenAnswer((_) async => {}); + when( + () => mockDocumentAppearanceCubit.syncDefaultTextDirection( + any(), + ), + ).thenAnswer((_) async {}); + + final radioSelect = tester.widget(radioSelectFinder) + as SettingsRadioSelect; + final rtlSelect = radioSelect.items + .firstWhere((select) => select.value == AppFlowyTextDirection.rtl); + radioSelect.onChanged(rtlSelect); + + verify( + () => mockAppearanceSettingsBloc.setTextDirection( + any(), + ), + ).called(1); + verify( + () => mockDocumentAppearanceCubit.syncDefaultTextDirection( + any(), + ), + ).called(1); + }); +} diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 3eea9e18ae..2a1c42eaa1 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -860,7 +860,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "async-trait", @@ -884,7 +884,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "async-trait", @@ -914,7 +914,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "collab", @@ -933,7 +933,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "bytes", @@ -948,7 +948,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "chrono", @@ -986,7 +986,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "async-stream", @@ -1067,7 +1067,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "collab", @@ -1296,7 +1296,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa 1.0.6", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] @@ -4729,7 +4729,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" dependencies = [ "bytes", "heck 0.4.1", - "itertools 0.10.5", + "itertools 0.11.0", "log", "multimap", "once_cell", @@ -4750,7 +4750,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.11.0", "proc-macro2", "quote", "syn 2.0.47", @@ -7968,9 +7968,9 @@ dependencies = [ [[package]] name = "yrs" -version = "0.18.7" +version = "0.18.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d58fbc807677598fedfab76f99f6e1aa5c644411255002b5438ea0ab14672398" +checksum = "da227d69095141c331d9b60c11496d0a3c6505cd9f8e200898b197219e8e394f" dependencies = [ "arc-swap", "atomic_refcell", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index f323062ffc..6b3a671077 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -94,6 +94,7 @@ flowy-notification = { path = "../../rust-lib/flowy-notification", features = [ uuid = "1.5.0" tauri-plugin-deep-link = "0.1.2" dotenv = "0.15.0" +semver = "1.0.23" [features] # by default Tauri runs in production mode @@ -104,10 +105,10 @@ default = ["custom-protocol"] custom-protocol = ["tauri/custom-protocol"] [patch.crates-io] -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } \ No newline at end of file +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } \ No newline at end of file diff --git a/frontend/appflowy_tauri/src-tauri/src/init.rs b/frontend/appflowy_tauri/src-tauri/src/init.rs index 1702c02923..7591ba37ff 100644 --- a/frontend/appflowy_tauri/src-tauri/src/init.rs +++ b/frontend/appflowy_tauri/src-tauri/src/init.rs @@ -34,7 +34,8 @@ pub fn init_flowy_core() -> AppFlowyCore { .version .clone() .map(|v| v.to_string()) - .unwrap_or_else(|| "0.0.0".to_string()); + .unwrap_or_else(|| "0.5.8".to_string()); + let app_version = semver::Version::parse(&app_version).unwrap_or_else(|_| semver::Version::new(0, 5, 8)); let mut data_path = tauri::api::path::app_local_data_dir(&config).unwrap(); if cfg!(debug_assertions) { data_path.push("data_dev"); diff --git a/frontend/appflowy_tauri/style-dictionary/tokens/base.json b/frontend/appflowy_tauri/style-dictionary/tokens/base.json index 4e31b0523d..fb58a867b1 100644 --- a/frontend/appflowy_tauri/style-dictionary/tokens/base.json +++ b/frontend/appflowy_tauri/style-dictionary/tokens/base.json @@ -7,7 +7,7 @@ "type": "color" }, "100": { - "value": "#edeef2", + "value": "#dadbdd", "type": "color" }, "200": { diff --git a/frontend/appflowy_web/wasm-libs/Cargo.lock b/frontend/appflowy_web/wasm-libs/Cargo.lock index a7d441c1fc..8024a9ba1f 100644 --- a/frontend/appflowy_web/wasm-libs/Cargo.lock +++ b/frontend/appflowy_web/wasm-libs/Cargo.lock @@ -638,7 +638,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" dependencies = [ "anyhow", "async-trait", @@ -662,7 +662,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" dependencies = [ "anyhow", "collab", @@ -681,7 +681,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" dependencies = [ "anyhow", "bytes", @@ -696,7 +696,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" dependencies = [ "anyhow", "chrono", @@ -734,7 +734,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" dependencies = [ "anyhow", "async-stream", @@ -814,7 +814,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" dependencies = [ "anyhow", "collab", @@ -966,7 +966,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] @@ -2790,7 +2790,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros", + "phf_macros 0.8.0", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -2810,6 +2810,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ + "phf_macros 0.11.2", "phf_shared 0.11.2", ] @@ -2877,6 +2878,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "phf_shared" version = "0.8.0" @@ -4909,9 +4923,9 @@ dependencies = [ [[package]] name = "yrs" -version = "0.18.7" +version = "0.18.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d58fbc807677598fedfab76f99f6e1aa5c644411255002b5438ea0ab14672398" +checksum = "da227d69095141c331d9b60c11496d0a3c6505cd9f8e200898b197219e8e394f" dependencies = [ "arc-swap", "atomic_refcell", @@ -4995,4 +5009,4 @@ dependencies = [ [[patch.unused]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" diff --git a/frontend/appflowy_web/wasm-libs/Cargo.toml b/frontend/appflowy_web/wasm-libs/Cargo.toml index 1b6cb52c67..e6637b4d31 100644 --- a/frontend/appflowy_web/wasm-libs/Cargo.toml +++ b/frontend/appflowy_web/wasm-libs/Cargo.toml @@ -70,10 +70,10 @@ opt-level = 3 codegen-units = 1 [patch.crates-io] -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } \ No newline at end of file +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/fixtures/user_workspace.json b/frontend/appflowy_web_app/cypress/fixtures/user_workspace.json new file mode 100644 index 0000000000..6961f6f1c4 --- /dev/null +++ b/frontend/appflowy_web_app/cypress/fixtures/user_workspace.json @@ -0,0 +1,61 @@ +{ + "data": { + "user_profile": { + "uid": 304120109071339520, + "uuid": "cbff060a-196d-415a-aa80-759c01886466", + "email": "lu@appflowy.io", + "password": "", + "name": "Kilu", + "metadata": { + "icon_url": "🇽🇰" + }, + "encryption_sign": null, + "latest_workspace_id": "9eebea03-3ed5-4298-86b2-a7f77856d48b", + "updated_at": 1715847453 + }, + "visiting_workspace": { + "workspace_id": "9eebea03-3ed5-4298-86b2-a7f77856d48b", + "database_storage_id": "375874be-7a4f-4b7c-8b89-1dc9a39838f4", + "owner_uid": 304120109071339520, + "owner_name": "Kilu", + "workspace_type": 0, + "workspace_name": "Kilu Works", + "created_at": "2024-03-13T07:23:10.275174Z", + "icon": "😆" + }, + "workspaces": [ + { + "workspace_id": "81570fa8-8be9-4b2d-9f1c-1ef4f34079a8", + "database_storage_id": "6c1f1a2c-e8d5-4bc2-917f-495bce862abb", + "owner_uid": 311828434584080384, + "owner_name": "Zack Zi Xiang Fu", + "workspace_type": 0, + "workspace_name": "My Workspace", + "created_at": "2024-04-03T13:53:18.295918Z", + "icon": "" + }, + { + "workspace_id": "fcb503f9-9287-4de4-8de0-ea191e680968", + "database_storage_id": "ae1b82a5-2b93-45c7-901a-f9357c544534", + "owner_uid": 276169796100296704, + "owner_name": "Annie Anqi Wang", + "workspace_type": 0, + "workspace_name": "AppFlowy Test", + "created_at": "2023-12-27T04:18:36.372013Z", + "icon": "" + }, + { + "workspace_id": "9eebea03-3ed5-4298-86b2-a7f77856d48b", + "database_storage_id": "375874be-7a4f-4b7c-8b89-1dc9a39838f4", + "owner_uid": 304120109071339520, + "owner_name": "Kilu", + "workspace_type": 0, + "workspace_name": "Kilu Works", + "created_at": "2024-03-13T07:23:10.275174Z", + "icon": "😆" + } + ] + }, + "code": 0, + "message": "Operation completed successfully." +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/support/commands.ts b/frontend/appflowy_web_app/cypress/support/commands.ts index 6146bd1c01..b275a842c5 100644 --- a/frontend/appflowy_web_app/cypress/support/commands.ts +++ b/frontend/appflowy_web_app/cypress/support/commands.ts @@ -37,6 +37,7 @@ Cypress.Commands.add('mockAPI', () => { cy.intercept('POST', '/gotrue/token?grant_type=refresh_token', json).as('refreshToken'); }); cy.intercept('GET', '/api/user/profile', { fixture: 'user' }).as('getUserProfile'); + cy.intercept('GET', '/api/user/workspace', { fixture: 'user_workspace' }).as('getUserWorkspace'); }); // Example use: diff --git a/frontend/appflowy_web_app/index.html b/frontend/appflowy_web_app/index.html index 3548e9b85d..5480f37859 100644 --- a/frontend/appflowy_web_app/index.html +++ b/frontend/appflowy_web_app/index.html @@ -3,7 +3,9 @@ - + AppFlowy diff --git a/frontend/appflowy_web_app/package.json b/frontend/appflowy_web_app/package.json index 1acc7d6e82..6eeb31ee09 100644 --- a/frontend/appflowy_web_app/package.json +++ b/frontend/appflowy_web_app/package.json @@ -22,12 +22,13 @@ "test:unit": "jest" }, "dependencies": { - "@appflowyinc/client-api-wasm": "0.0.2-alpha.2", + "@appflowyinc/client-api-wasm": "0.0.3", "@atlaskit/primitives": "^5.5.3", "@emoji-mart/data": "^1.1.2", "@emoji-mart/react": "^1.1.1", "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", + "@jest/globals": "^29.7.0", "@mui/icons-material": "^5.11.11", "@mui/material": "6.0.0-alpha.2", "@mui/x-date-pickers-pro": "^6.18.2", @@ -35,9 +36,10 @@ "@slate-yjs/core": "^1.0.2", "@tauri-apps/api": "^1.5.3", "@types/react-swipeable-views": "^0.13.4", + "async-retry": "^1.3.3", "axios": "^1.6.8", "dayjs": "^1.11.9", - "dexie": "^4.0.1", + "decimal.js": "^10.4.3", "emoji-mart": "^5.5.2", "emoji-regex": "^10.2.1", "events": "^3.3.0", @@ -51,6 +53,7 @@ "katex": "^0.16.7", "lodash-es": "^4.17.21", "nanoid": "^4.0.0", + "numeral": "^2.0.6", "prismjs": "^1.29.0", "protoc-gen-ts": "0.8.7", "quill": "^1.3.7", @@ -66,6 +69,7 @@ "react-hot-toast": "^2.4.1", "react-i18next": "^14.1.0", "react-katex": "^3.0.1", + "react-measure": "^2.5.2", "react-redux": "^8.0.5", "react-router-dom": "^6.22.3", "react-swipeable-views": "^0.14.0", @@ -98,15 +102,18 @@ "@types/katex": "^0.16.0", "@types/lodash-es": "^4.17.11", "@types/node": "^20.11.30", + "@types/numeral": "^2.0.5", "@types/prismjs": "^1.26.0", "@types/quill": "^2.0.10", "@types/react": "^18.2.66", "@types/react-beautiful-dnd": "^13.1.3", + "@types/react-big-calendar": "^1.8.9", "@types/react-color": "^3.0.6", "@types/react-custom-scrollbars": "^4.0.13", "@types/react-datepicker": "^4.19.3", "@types/react-dom": "^18.2.22", "@types/react-katex": "^3.0.0", + "@types/react-measure": "^2.0.12", "@types/react-transition-group": "^4.4.6", "@types/react-window": "^1.8.8", "@types/utf8": "^3.0.1", diff --git a/frontend/appflowy_web_app/pnpm-lock.yaml b/frontend/appflowy_web_app/pnpm-lock.yaml index b9fe83de2f..7796ae7db8 100644 --- a/frontend/appflowy_web_app/pnpm-lock.yaml +++ b/frontend/appflowy_web_app/pnpm-lock.yaml @@ -6,8 +6,8 @@ settings: dependencies: '@appflowyinc/client-api-wasm': - specifier: 0.0.2-alpha.2 - version: 0.0.2-alpha.2 + specifier: 0.0.3 + version: 0.0.3 '@atlaskit/primitives': specifier: ^5.5.3 version: 5.5.3(@types/react@18.2.66)(react@18.2.0) @@ -23,6 +23,9 @@ dependencies: '@emotion/styled': specifier: ^11.10.6 version: 11.10.6(@emotion/react@11.10.6)(@types/react@18.2.66)(react@18.2.0) + '@jest/globals': + specifier: ^29.7.0 + version: 29.7.0 '@mui/icons-material': specifier: ^5.11.11 version: 5.11.11(@mui/material@6.0.0-alpha.2)(@types/react@18.2.66)(react@18.2.0) @@ -31,7 +34,7 @@ dependencies: version: 6.0.0-alpha.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) '@mui/x-date-pickers-pro': specifier: ^6.18.2 - version: 6.18.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@mui/material@6.0.0-alpha.2)(@mui/system@5.14.4)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0) + version: 6.18.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@mui/material@6.0.0-alpha.2)(@mui/system@5.15.15)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0) '@reduxjs/toolkit': specifier: 2.0.0 version: 2.0.0(react-redux@8.0.5)(react@18.2.0) @@ -44,15 +47,18 @@ dependencies: '@types/react-swipeable-views': specifier: ^0.13.4 version: 0.13.4 + async-retry: + specifier: ^1.3.3 + version: 1.3.3 axios: specifier: ^1.6.8 version: 1.6.8 dayjs: specifier: ^1.11.9 version: 1.11.9 - dexie: - specifier: ^4.0.1 - version: 4.0.1 + decimal.js: + specifier: ^10.4.3 + version: 10.4.3 emoji-mart: specifier: ^5.5.2 version: 5.5.2 @@ -92,6 +98,9 @@ dependencies: nanoid: specifier: ^4.0.0 version: 4.0.0 + numeral: + specifier: ^2.0.6 + version: 2.0.6 prismjs: specifier: ^1.29.0 version: 1.29.0 @@ -137,6 +146,9 @@ dependencies: react-katex: specifier: ^3.0.1 version: 3.0.1(prop-types@15.8.1)(react@18.2.0) + react-measure: + specifier: ^2.5.2 + version: 2.5.2(react-dom@18.2.0)(react@18.2.0) react-redux: specifier: ^8.0.5 version: 8.0.5(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1) @@ -229,6 +241,9 @@ devDependencies: '@types/node': specifier: ^20.11.30 version: 20.11.30 + '@types/numeral': + specifier: ^2.0.5 + version: 2.0.5 '@types/prismjs': specifier: ^1.26.0 version: 1.26.0 @@ -241,6 +256,9 @@ devDependencies: '@types/react-beautiful-dnd': specifier: ^13.1.3 version: 13.1.3 + '@types/react-big-calendar': + specifier: ^1.8.9 + version: 1.8.9 '@types/react-color': specifier: ^3.0.6 version: 3.0.6 @@ -256,6 +274,9 @@ devDependencies: '@types/react-katex': specifier: ^3.0.0 version: 3.0.0 + '@types/react-measure': + specifier: ^2.0.12 + version: 2.0.12 '@types/react-transition-group': specifier: ^4.4.6 version: 4.4.6 @@ -376,8 +397,8 @@ packages: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 - /@appflowyinc/client-api-wasm@0.0.2-alpha.2: - resolution: {integrity: sha512-BcRK06zHHJdaGNYohYxGaR2xPfQ1RwU48jMzdMZDf2HXVLU2WWQ6cYfuM4lrsK+O3QEfJdeEL2fntnQDaaeQng==} + /@appflowyinc/client-api-wasm@0.0.3: + resolution: {integrity: sha512-ARjLhiDZ8MiZ9egWDbAX9VAdXXS30av+InCPLrS/iqCMYrhuuU9rxS9jQeNEB7jucFrj158gBRusimFN7P/lyw==} dev: false /@atlaskit/analytics-next-stable-react-context@1.0.1(react@18.2.0): @@ -1742,8 +1763,8 @@ packages: react: 18.2.0 dev: false - /@mui/system@5.14.4(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react@18.2.0): - resolution: {integrity: sha512-oPgfWS97QNfHcDBapdkZIs4G5i85BJt69Hp6wbXF6s7vi3Evcmhdk8AbCRW6n0sX4vTj8oe0mh0RIm1G2A1KDA==} + /@mui/system@5.15.15(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-aulox6N1dnu5PABsfxVGOZffDVmlxPOVgj56HrUnJE8MCSh8lOvvkd47cebIVQQYAjpwieXQXiDPj5pwM40jTQ==} engines: {node: '>=12.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -1849,7 +1870,7 @@ packages: react-is: 18.2.0 dev: false - /@mui/x-date-pickers-pro@6.18.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@mui/material@6.0.0-alpha.2)(@mui/system@5.14.4)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0): + /@mui/x-date-pickers-pro@6.18.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@mui/material@6.0.0-alpha.2)(@mui/system@5.15.15)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-8lEVEOtCQssKWel4Ey1pRulGPXUQ73TnkHKzHWsjdv03FjiUs3eYB+Ej0Uk5yWPmsqlShWhOzOlOGDpzsYJsUg==} engines: {node: '>=14.0.0'} peerDependencies: @@ -1891,9 +1912,9 @@ packages: '@emotion/styled': 11.10.6(@emotion/react@11.10.6)(@types/react@18.2.66)(react@18.2.0) '@mui/base': 5.0.0-beta.40(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) '@mui/material': 6.0.0-alpha.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) - '@mui/system': 5.14.4(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react@18.2.0) + '@mui/system': 5.15.15(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react@18.2.0) '@mui/utils': 5.15.14(@types/react@18.2.66)(react@18.2.0) - '@mui/x-date-pickers': 6.18.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@mui/material@6.0.0-alpha.2)(@mui/system@5.14.4)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0) + '@mui/x-date-pickers': 6.18.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@mui/material@6.0.0-alpha.2)(@mui/system@5.15.15)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0) '@mui/x-license-pro': 6.10.2(@types/react@18.2.66)(react@18.2.0) clsx: 2.1.0 dayjs: 1.11.9 @@ -1905,7 +1926,7 @@ packages: - '@types/react' dev: false - /@mui/x-date-pickers@6.18.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@mui/material@6.0.0-alpha.2)(@mui/system@5.14.4)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0): + /@mui/x-date-pickers@6.18.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@mui/material@6.0.0-alpha.2)(@mui/system@5.15.15)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-HJq4uoFQSu5isa/mesWw2BKh8KBRYUQb+KaSlVlWfJNgP3YhPvWZ6yqCNYyxOAiPMxb0n3nBjS9ErO27OHjFMA==} engines: {node: '>=14.0.0'} peerDependencies: @@ -1947,7 +1968,7 @@ packages: '@emotion/styled': 11.10.6(@emotion/react@11.10.6)(@types/react@18.2.66)(react@18.2.0) '@mui/base': 5.0.0-beta.40(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) '@mui/material': 6.0.0-alpha.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) - '@mui/system': 5.14.4(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react@18.2.0) + '@mui/system': 5.15.15(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react@18.2.0) '@mui/utils': 5.15.14(@types/react@18.2.66)(react@18.2.0) '@types/react-transition-group': 4.4.10 clsx: 2.1.0 @@ -2600,6 +2621,10 @@ packages: dependencies: '@babel/types': 7.24.0 + /@types/date-arithmetic@4.1.4: + resolution: {integrity: sha512-p9eZ2X9B80iKiTW4ukVj8B4K6q9/+xFtQ5MGYA5HWToY9nL4EkhV9+6ftT2VHpVMEZb5Tv00Iel516bVdO+yRw==} + dev: true + /@types/estree@1.0.5: resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} @@ -2677,6 +2702,10 @@ packages: dependencies: undici-types: 5.26.5 + /@types/numeral@2.0.5: + resolution: {integrity: sha512-kH8I7OSSwQu9DS9JYdFWbuvhVzvFRoCPCkGxNwoGgaPeDfEPJlcxNvEOypZhQ3XXHsGbfIuYcxcJxKUfJHnRfw==} + dev: true + /@types/parse-json@4.0.2: resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} dev: false @@ -2701,6 +2730,14 @@ packages: '@types/react': 18.2.66 dev: true + /@types/react-big-calendar@1.8.9: + resolution: {integrity: sha512-HIHLUxR3PzWHrFdZ00VnCMvDjAh5uzlL0vMC2b7tL3bKaAJsqq9T8h+x0GVeDbZfMfHAd1cs5tZBhVvourNJXQ==} + dependencies: + '@types/date-arithmetic': 4.1.4 + '@types/prop-types': 15.7.12 + '@types/react': 18.2.66 + dev: true + /@types/react-color@3.0.6: resolution: {integrity: sha512-OzPIO5AyRmLA7PlOyISlgabpYUa3En74LP8mTMa0veCA719SvYQov4WLMsHvCgXP+L+KI9yGhYnqZafVGG0P4w==} dependencies: @@ -2737,6 +2774,12 @@ packages: '@types/react': 18.2.66 dev: true + /@types/react-measure@2.0.12: + resolution: {integrity: sha512-Y6V11CH6bU7RhqrIdENPwEUZlPXhfXNGylMNnGwq5TAEs2wDoBA3kSVVM/EQ8u72sz5r9ja+7W8M8PIVcS841Q==} + dependencies: + '@types/react': 18.2.66 + dev: true + /@types/react-redux@7.1.33: resolution: {integrity: sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==} dependencies: @@ -3234,6 +3277,12 @@ packages: engines: {node: '>=8'} dev: true + /async-retry@1.3.3: + resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + dependencies: + retry: 0.13.1 + dev: false + /async@3.2.5: resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} dev: true @@ -4015,7 +4064,6 @@ packages: /decimal.js@10.4.3: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} - dev: true /dedent@1.5.1: resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} @@ -4101,10 +4149,6 @@ packages: minimist: 1.2.8 dev: true - /dexie@4.0.1: - resolution: {integrity: sha512-wSNn+TcCh+DuE2pdg058K3MhxA4g+IiZlW7yGz4cMd/t3z2rJXZcV3HDxZljbrICU2Iq0qY4UHnbolTMK/+bcA==} - dev: false - /didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} dev: true @@ -4875,6 +4919,10 @@ packages: has-symbols: 1.0.3 hasown: 2.0.2 + /get-node-dimensions@1.2.1: + resolution: {integrity: sha512-2MSPMu7S1iOTL+BOa6K1S62hB2zUAYNF/lV0gSVlOaacd087lc6nR1H1r0e3B1CerTo+RceOmi1iJW+vp21xcQ==} + dev: false + /get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} @@ -6384,6 +6432,10 @@ packages: boolbase: 1.0.0 dev: true + /numeral@2.0.6: + resolution: {integrity: sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA==} + dev: false + /nwsapi@2.2.7: resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==} dev: true @@ -7138,6 +7190,20 @@ packages: resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} dev: false + /react-measure@2.5.2(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-M+rpbTLWJ3FD6FXvYV6YEGvQ5tMayQ3fGrZhRPHrE9bVlBYfDCLuDcgNttYfk8IqfOI03jz6cbpqMRTUclQnaA==} + peerDependencies: + react: '>0.13.0' + react-dom: '>0.13.0' + dependencies: + '@babel/runtime': 7.24.4 + get-node-dimensions: 1.2.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + resize-observer-polyfill: 1.5.1 + dev: false + /react-onclickoutside@6.13.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A==} peerDependencies: @@ -7452,6 +7518,10 @@ packages: resolution: {integrity: sha512-D72j2ubjgHpvuCiORWkOUxndHJrxDaSolheiz5CO+roz8ka97/4msh2E8F5qay4GawR5vzBt5MkbDHT+Rdy/Wg==} dev: false + /resize-observer-polyfill@1.5.1: + resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} + dev: false + /resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} @@ -7495,6 +7565,11 @@ packages: signal-exit: 3.0.7 dev: true + /retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + dev: false + /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.lock b/frontend/appflowy_web_app/src-tauri/Cargo.lock index 2fcd8de4d4..2bb44433ed 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.lock +++ b/frontend/appflowy_web_app/src-tauri/Cargo.lock @@ -843,7 +843,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "async-trait", @@ -867,7 +867,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "async-trait", @@ -897,7 +897,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "collab", @@ -916,7 +916,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "bytes", @@ -931,7 +931,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "chrono", @@ -969,7 +969,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "async-stream", @@ -1050,7 +1050,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "collab", @@ -8272,9 +8272,9 @@ dependencies = [ [[package]] name = "yrs" -version = "0.18.7" +version = "0.18.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d58fbc807677598fedfab76f99f6e1aa5c644411255002b5438ea0ab14672398" +checksum = "da227d69095141c331d9b60c11496d0a3c6505cd9f8e200898b197219e8e394f" dependencies = [ "arc-swap", "atomic_refcell", diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.toml b/frontend/appflowy_web_app/src-tauri/Cargo.toml index 63bc2269e3..54e09acf6c 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.toml +++ b/frontend/appflowy_web_app/src-tauri/Cargo.toml @@ -93,6 +93,7 @@ flowy-notification = { path = "../../rust-lib/flowy-notification", features = [ uuid = "1.5.0" tauri-plugin-deep-link = "0.1.2" dotenv = "0.15.0" +semver = "1.0.23" [features] # by default Tauri runs in production mode @@ -103,10 +104,10 @@ default = ["custom-protocol"] custom-protocol = ["tauri/custom-protocol"] [patch.crates-io] -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } \ No newline at end of file +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } \ No newline at end of file diff --git a/frontend/appflowy_web_app/src-tauri/src/init.rs b/frontend/appflowy_web_app/src-tauri/src/init.rs index 923ab0ff8f..42c857abdf 100644 --- a/frontend/appflowy_web_app/src-tauri/src/init.rs +++ b/frontend/appflowy_web_app/src-tauri/src/init.rs @@ -30,6 +30,7 @@ pub fn init_flowy_core() -> AppFlowyCore { let config: tauri_utils::config::Config = serde_json::from_str(config_json).unwrap(); let app_version = config.package.version.clone().map(|v| v.to_string()).unwrap_or_else(|| "0.0.0".to_string()); + let app_version = semver::Version::parse(&app_version).unwrap_or_else(|_| semver::Version::new(0, 5, 8)); let mut data_path = tauri::api::path::app_local_data_dir(&config).unwrap(); if cfg!(debug_assertions) { data_path.push("data_dev"); diff --git a/frontend/appflowy_web_app/src/application/collab.type.ts b/frontend/appflowy_web_app/src/application/collab.type.ts index 0df2729749..affc19e921 100644 --- a/frontend/appflowy_web_app/src/application/collab.type.ts +++ b/frontend/appflowy_web_app/src/application/collab.type.ts @@ -1,4 +1,4 @@ -import Y from 'yjs'; +import * as Y from 'yjs'; export type BlockId = string; @@ -8,6 +8,10 @@ export type ChildrenId = string; export type ViewId = string; +export type RowId = string; + +export type CellId = string; + export enum BlockType { Paragraph = 'paragraph', Page = 'page', @@ -23,6 +27,8 @@ export enum BlockType { DividerBlock = 'divider', ImageBlock = 'image', GridBlock = 'grid', + BoardBlock = 'board', + CalendarBlock = 'calendar', OutlineBlock = 'outline', TableBlock = 'table', TableCell = 'table/cell', @@ -107,6 +113,10 @@ export interface TableCellBlockData extends BlockData { width: number; } +export interface DatabaseNodeData extends BlockData { + view_id: ViewId; +} + export enum MentionType { PageRef = 'page', Date = 'date', @@ -192,6 +202,58 @@ export enum YjsFolderKey { type = 'ty', value = 'value', layout = 'layout', + bid = 'bid', +} + +export enum YjsDatabaseKey { + views = 'views', + id = 'id', + metas = 'metas', + fields = 'fields', + is_primary = 'is_primary', + last_modified = 'last_modified', + created_at = 'created_at', + name = 'name', + type = 'ty', + type_option = 'type_option', + content = 'content', + data = 'data', + iid = 'iid', + database_id = 'database_id', + field_orders = 'field_orders', + field_settings = 'field_settings', + visibility = 'visibility', + wrap = 'wrap', + width = 'width', + filters = 'filters', + groups = 'groups', + layout = 'layout', + layout_settings = 'layout_settings', + modified_at = 'modified_at', + row_orders = 'row_orders', + sorts = 'sorts', + height = 'height', + cells = 'cells', + field_type = 'field_type', + end_timestamp = 'end_timestamp', + include_time = 'include_time', + is_range = 'is_range', + reminder_id = 'reminder_id', + time_format = 'time_format', + date_format = 'date_format', + calculations = 'calculations', + field_id = 'field_id', + calculation_value = 'calculation_value', + condition = 'condition', + format = 'format', + filter_type = 'filter_type', + visible = 'visible', + hide_ungrouped_column = 'hide_ungrouped_column', + collapse_hidden_groups = 'collapse_hidden_groups', + first_day_of_week = 'first_day_of_week', + show_week_numbers = 'show_week_numbers', + show_weekends = 'show_weekends', + layout_ty = 'layout_ty', } export interface YDoc extends Y.Doc { @@ -199,11 +261,54 @@ export interface YDoc extends Y.Doc { getMap(key: YjsEditorKey.data_section): YSharedRoot | any; } +export interface YDatabaseRow extends Y.Map { + get(key: YjsDatabaseKey.id): RowId; + + get(key: YjsDatabaseKey.height): string; + + get(key: YjsDatabaseKey.visibility): boolean; + + get(key: YjsDatabaseKey.created_at): CreatedAt; + + get(key: YjsDatabaseKey.last_modified): LastModified; + + get(key: YjsDatabaseKey.cells): YDatabaseCells; +} + +export interface YDatabaseCells extends Y.Map { + get(key: FieldId): YDatabaseCell; +} + +export type EndTimestamp = string; +export type ReminderId = string; + +export interface YDatabaseCell extends Y.Map { + get(key: YjsDatabaseKey.created_at): CreatedAt; + + get(key: YjsDatabaseKey.last_modified): LastModified; + + get(key: YjsDatabaseKey.field_type): string; + + get(key: YjsDatabaseKey.data): object | string | boolean | number; + + get(key: YjsDatabaseKey.end_timestamp): EndTimestamp; + + get(key: YjsDatabaseKey.include_time): boolean; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.is_range): boolean; + + get(key: YjsDatabaseKey.reminder_id): ReminderId; +} + export interface YSharedRoot extends Y.Map { get(key: YjsEditorKey.document): YDocument; - // eslint-disable-next-line @typescript-eslint/unified-signatures get(key: YjsEditorKey.folder): YFolder; + + get(key: YjsEditorKey.database): YDatabase; + + get(key: YjsEditorKey.database_row): YDatabaseRow; } export interface YFolder extends Y.Map { @@ -226,6 +331,9 @@ export interface YViews extends Y.Map { export interface YView extends Y.Map { get(key: YjsFolderKey.id): ViewId; + get(key: YjsFolderKey.bid): string; + + // eslint-disable-next-line @typescript-eslint/unified-signatures get(key: YjsFolderKey.name): string; // eslint-disable-next-line @typescript-eslint/unified-signatures @@ -271,6 +379,203 @@ export interface YTextMap extends Y.Map { get(key: ExternalId): Y.Text; } +export interface YDatabase extends Y.Map { + get(key: YjsDatabaseKey.views): YDatabaseViews; + + get(key: YjsDatabaseKey.metas): YDatabaseMetas; + + get(key: YjsDatabaseKey.fields): YDatabaseFields; + + get(key: YjsDatabaseKey.id): string; +} + +export interface YDatabaseViews extends Y.Map { + get(key: ViewId): YDatabaseView; +} + +export type DatabaseId = string; +export type CreatedAt = string; +export type LastModified = string; +export type ModifiedAt = string; +export type FieldId = string; + +export enum DatabaseViewLayout { + Grid = 0, + Board = 1, + Calendar = 2, +} + +export interface YDatabaseView extends Y.Map { + get(key: YjsDatabaseKey.database_id): DatabaseId; + + get(key: YjsDatabaseKey.name): string; + + get(key: YjsDatabaseKey.created_at): CreatedAt; + + get(key: YjsDatabaseKey.modified_at): ModifiedAt; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.layout): string; + + get(key: YjsDatabaseKey.layout_settings): YDatabaseLayoutSettings; + + get(key: YjsDatabaseKey.filters): YDatabaseFilters; + + get(key: YjsDatabaseKey.groups): YDatabaseGroups; + + get(key: YjsDatabaseKey.sorts): YDatabaseSorts; + + get(key: YjsDatabaseKey.field_settings): YDatabaseFieldSettings; + + get(key: YjsDatabaseKey.field_orders): YDatabaseFieldOrders; + + get(key: YjsDatabaseKey.row_orders): YDatabaseRowOrders; + + get(key: YjsDatabaseKey.calculations): YDatabaseCalculations; +} + +export type YDatabaseFieldOrders = Y.Array; // [ { id: FieldId } ] + +export type YDatabaseRowOrders = Y.Array; // [ { id: RowId, height: number } ] + +export type YDatabaseGroups = Y.Array; + +export type YDatabaseFilters = Y.Array; + +export type YDatabaseSorts = Y.Array; + +export type YDatabaseCalculations = Y.Array; + +export type SortId = string; + +export type GroupId = string; + +export interface YDatabaseLayoutSettings extends Y.Map { + // DatabaseViewLayout.Board + get(key: '1'): YDatabaseBoardLayoutSetting; + + // DatabaseViewLayout.Calendar + get(key: '2'): YDatabaseCalendarLayoutSetting; +} + +export interface YDatabaseBoardLayoutSetting extends Y.Map { + get(key: YjsDatabaseKey.hide_ungrouped_column | YjsDatabaseKey.collapse_hidden_groups): boolean; +} + +export interface YDatabaseCalendarLayoutSetting extends Y.Map { + get(key: YjsDatabaseKey.first_day_of_week | YjsDatabaseKey.field_id | YjsDatabaseKey.layout_ty): string; + + get(key: YjsDatabaseKey.show_week_numbers | YjsDatabaseKey.show_weekends): boolean; +} + +export interface YDatabaseGroup extends Y.Map { + get(key: YjsDatabaseKey.id): GroupId; + + get(key: YjsDatabaseKey.field_id): FieldId; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.content): string; + + get(key: YjsDatabaseKey.groups): YDatabaseGroupColumns; +} + +export type YDatabaseGroupColumns = Y.Array; + +export interface YDatabaseGroupColumn extends Y.Map { + get(key: YjsDatabaseKey.id): string; + + get(key: YjsDatabaseKey.visible): boolean; +} + +export interface YDatabaseRowOrder extends Y.Map { + get(key: YjsDatabaseKey.id): SortId; + + get(key: YjsDatabaseKey.height): number; +} + +export interface YDatabaseSort extends Y.Map { + get(key: YjsDatabaseKey.id): SortId; + + get(key: YjsDatabaseKey.field_id): FieldId; + + get(key: YjsDatabaseKey.condition): string; +} + +export type FilterId = string; + +export interface YDatabaseFilter extends Y.Map { + get(key: YjsDatabaseKey.id): FilterId; + + get(key: YjsDatabaseKey.field_id): FieldId; + + get(key: YjsDatabaseKey.type | YjsDatabaseKey.condition | YjsDatabaseKey.content | YjsDatabaseKey.filter_type): string; +} + +export interface YDatabaseCalculation extends Y.Map { + get(key: YjsDatabaseKey.field_id): FieldId; + + get(key: YjsDatabaseKey.id | YjsDatabaseKey.type | YjsDatabaseKey.calculation_value): string; +} + +export interface YDatabaseFieldSettings extends Y.Map { + get(key: FieldId): YDatabaseFieldSetting; +} + +export interface YDatabaseFieldSetting extends Y.Map { + get(key: YjsDatabaseKey.visibility): string; + + get(key: YjsDatabaseKey.wrap): boolean; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.width): string; +} + +export interface YDatabaseMetas extends Y.Map { + get(key: YjsDatabaseKey.iid): string; +} + +export interface YDatabaseFields extends Y.Map { + get(key: FieldId): YDatabaseField; +} + +export interface YDatabaseField extends Y.Map { + get(key: YjsDatabaseKey.name): string; + + get(key: YjsDatabaseKey.id): FieldId; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.type): string; + + get(key: YjsDatabaseKey.type_option): YDatabaseFieldTypeOption; + + get(key: YjsDatabaseKey.is_primary): boolean; + + get(key: YjsDatabaseKey.last_modified): LastModified; +} + +export interface YDatabaseFieldTypeOption extends Y.Map { + // key is the field type + get(key: string): YMapFieldTypeOption; +} + +export interface YMapFieldTypeOption extends Y.Map { + get(key: YjsDatabaseKey.content): string; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.data): string; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.time_format): string; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.date_format): string; + + get(key: YjsDatabaseKey.database_id): DatabaseId; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.format): string; +} + export enum CollabType { Document = 0, Database = 1, @@ -282,8 +587,12 @@ export enum CollabType { } export enum CollabOrigin { + // from local changes and never sync to remote. used for read-only mode Local = 'local', + // from remote changes and never sync to remote. Remote = 'remote', + // from local changes and sync to remote. used for collaborative mode + LocalSync = 'local_sync', } export const layoutMap = { @@ -292,3 +601,9 @@ export const layoutMap = { [ViewLayout.Board]: 'board', [ViewLayout.Calendar]: 'calendar', }; + +export const databaseLayoutMap = { + [DatabaseViewLayout.Grid]: 'grid', + [DatabaseViewLayout.Board]: 'board', + [DatabaseViewLayout.Calendar]: 'calendar', +}; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/const.ts b/frontend/appflowy_web_app/src/application/database-yjs/const.ts new file mode 100644 index 0000000000..b8ebe46a3a --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/const.ts @@ -0,0 +1,24 @@ +import { YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; +import { RowMetaKey } from '@/application/database-yjs/database.type'; +import * as Y from 'yjs'; +import { v5 as uuidv5, parse as uuidParse } from 'uuid'; + +export const DEFAULT_ROW_HEIGHT = 37; +export const MIN_COLUMN_WIDTH = 100; + +export const getCell = (rowId: string, fieldId: string, rowMetas: Y.Map) => { + const rowMeta = rowMetas.get(rowId); + const meta = rowMeta?.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow; + + return meta?.get(YjsDatabaseKey.cells)?.get(fieldId); +}; + +export const getCellData = (rowId: string, fieldId: string, rowMetas: Y.Map) => { + return getCell(rowId, fieldId, rowMetas)?.get(YjsDatabaseKey.data); +}; + +export const metaIdFromRowId = (rowId: string) => { + const namespace = uuidParse(rowId); + + return (key: RowMetaKey) => uuidv5(key, namespace).toString(); +}; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/context.ts b/frontend/appflowy_web_app/src/application/database-yjs/context.ts new file mode 100644 index 0000000000..96a0f067c4 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/context.ts @@ -0,0 +1,75 @@ +import { YDatabase, YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; +import { Row } from '@/application/database-yjs/selector'; +import { createContext, useContext } from 'react'; +import * as Y from 'yjs'; + +export interface DatabaseContextState { + readOnly: boolean; + doc: YDoc; + viewId: string; + rowDocMap: Y.Map; + navigateToRow?: (rowId: string) => void; +} + +export const DatabaseContext = createContext(null); + +export const useDatabase = () => { + const database = useContext(DatabaseContext) + ?.doc?.getMap(YjsEditorKey.data_section) + .get(YjsEditorKey.database) as YDatabase; + + return database; +}; + +export const useNavigateToRow = () => { + return useContext(DatabaseContext)?.navigateToRow; +}; + +export const useRow = (rowId: string) => { + const rows = useContext(DatabaseContext)?.rowDocMap; + + return rows?.get(rowId)?.getMap(YjsEditorKey.data_section); +}; + +export const useRowData = (rowId: string) => { + return useRow(rowId)?.get(YjsEditorKey.database_row) as YDatabaseRow; +}; + +export const useViewId = () => { + const context = useContext(DatabaseContext); + + return context?.viewId; +}; + +export const useReadOnly = () => { + const context = useContext(DatabaseContext); + + return context?.readOnly; +}; + +export const useDatabaseView = () => { + const database = useDatabase(); + const viewId = useViewId(); + + return viewId ? database.get(YjsDatabaseKey.views)?.get(viewId) : undefined; +}; + +export function useDatabaseFields() { + const database = useDatabase(); + + return database.get(YjsDatabaseKey.fields); +} + +export interface RowsState { + rowOrders: Row[]; +} + +export const RowsContext = createContext(null); + +export function useRowsContext() { + return useContext(RowsContext); +} + +export function useRows() { + return useRowsContext()?.rowOrders; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/database.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/database.type.ts new file mode 100644 index 0000000000..c8ac7da5b0 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/database.type.ts @@ -0,0 +1,72 @@ +import { FieldId } from '@/application/collab.type'; + +export enum FieldVisibility { + AlwaysShown = 0, + HideWhenEmpty = 1, + AlwaysHidden = 2, +} + +export enum FieldType { + RichText = 0, + Number = 1, + DateTime = 2, + SingleSelect = 3, + MultiSelect = 4, + Checkbox = 5, + URL = 6, + Checklist = 7, + LastEditedTime = 8, + CreatedTime = 9, + Relation = 10, +} + +export enum CalculationType { + Average = 0, + Max = 1, + Median = 2, + Min = 3, + Sum = 4, + Count = 5, + CountEmpty = 6, + CountNonEmpty = 7, +} + +export enum SortCondition { + Ascending = 0, + Descending = 1, +} + +export enum FilterType { + Data = 0, + And = 1, + Or = 2, +} + +export interface Filter { + fieldId: FieldId; + filterType: FilterType; + condition: number; + id: string; + content: string; +} + +export enum CalendarLayout { + MonthLayout = 0, + WeekLayout = 1, + DayLayout = 2, +} + +export interface CalendarLayoutSetting { + fieldId: string; + firstDayOfWeek: number; + showWeekNumbers: boolean; + showWeekends: boolean; + layout: CalendarLayout; +} + +export enum RowMetaKey { + DocumentId = 'document_id', + IconId = 'icon_id', + CoverId = 'cover_id', + IsDocumentEmpty = 'is_document_empty', +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/checkbox/checkbox.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/checkbox/checkbox.type.ts new file mode 100644 index 0000000000..b9da4341f6 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/checkbox/checkbox.type.ts @@ -0,0 +1,10 @@ +import { Filter } from '@/application/database-yjs'; + +export enum CheckboxFilterCondition { + IsChecked = 0, + IsUnChecked = 1, +} + +export interface CheckboxFilter extends Filter { + condition: CheckboxFilterCondition; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/checkbox/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/checkbox/index.ts new file mode 100644 index 0000000000..9ccd409dc8 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/checkbox/index.ts @@ -0,0 +1 @@ +export * from './checkbox.type'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/checklist.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/checklist.type.ts new file mode 100644 index 0000000000..2b504ded8a --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/checklist.type.ts @@ -0,0 +1,10 @@ +import { Filter } from '@/application/database-yjs'; + +export enum ChecklistFilterCondition { + IsComplete = 0, + IsIncomplete = 1, +} + +export interface ChecklistFilter extends Filter { + condition: ChecklistFilterCondition; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/index.ts new file mode 100644 index 0000000000..15d37f912b --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/index.ts @@ -0,0 +1,2 @@ +export * from './checklist.type'; +export * from './parse'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/parse.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/parse.ts new file mode 100644 index 0000000000..c93fee7a38 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/parse.ts @@ -0,0 +1,22 @@ +import { SelectOption } from '../select-option'; + +export interface ChecklistCellData { + selectedOptionIds?: string[]; + options?: SelectOption[]; + percentage: number; +} + +export function parseChecklistData(data: string): ChecklistCellData | null { + try { + const { options, selected_option_ids } = JSON.parse(data); + const percentage = selected_option_ids.length / options.length; + + return { + percentage, + options, + selectedOptionIds: selected_option_ids, + }; + } catch (e) { + return null; + } +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/date/date.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/date/date.type.ts new file mode 100644 index 0000000000..0db15f21eb --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/date/date.type.ts @@ -0,0 +1,32 @@ +import { Filter } from '@/application/database-yjs'; + +export enum TimeFormat { + TwelveHour = 0, + TwentyFourHour = 1, +} + +export enum DateFormat { + Local = 0, + US = 1, + ISO = 2, + Friendly = 3, + DayMonthYear = 4, +} + +export enum DateFilterCondition { + DateIs = 0, + DateBefore = 1, + DateAfter = 2, + DateOnOrBefore = 3, + DateOnOrAfter = 4, + DateWithIn = 5, + DateIsEmpty = 6, + DateIsNotEmpty = 7, +} + +export interface DateFilter extends Filter { + condition: DateFilterCondition; + start?: number; + end?: number; + timestamp?: number; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/date/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/date/index.ts new file mode 100644 index 0000000000..106279c949 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/date/index.ts @@ -0,0 +1,2 @@ +export * from './date.type'; +export * from './utils'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/date/utils.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/date/utils.ts new file mode 100644 index 0000000000..985402768b --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/date/utils.ts @@ -0,0 +1,29 @@ +import { TimeFormat, DateFormat } from '@/application/database-yjs'; + +export function getTimeFormat(timeFormat?: TimeFormat) { + switch (timeFormat) { + case TimeFormat.TwelveHour: + return 'h:mm A'; + case TimeFormat.TwentyFourHour: + return 'HH:mm'; + default: + return 'HH:mm'; + } +} + +export function getDateFormat(dateFormat?: DateFormat) { + switch (dateFormat) { + case DateFormat.Friendly: + return 'MMM DD, YYYY'; + case DateFormat.ISO: + return 'YYYY-MM-DD'; + case DateFormat.US: + return 'YYYY/MM/DD'; + case DateFormat.Local: + return 'MM/DD/YYYY'; + case DateFormat.DayMonthYear: + return 'DD/MM/YYYY'; + default: + return 'YYYY-MM-DD'; + } +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/index.ts new file mode 100644 index 0000000000..5505f0e4ed --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/index.ts @@ -0,0 +1,8 @@ +export * from './type_option'; +export * from './date'; +export * from './number'; +export * from './select-option'; +export * from './text'; +export * from './checkbox'; +export * from './checklist'; +export * from './relation'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/number/__tests__/format.test.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/__tests__/format.test.ts new file mode 100644 index 0000000000..e165752348 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/__tests__/format.test.ts @@ -0,0 +1,628 @@ +import { currencyFormaterMap } from '../format'; +import { NumberFormat } from '../number.type'; +import { expect } from '@jest/globals'; + +const testCases = [0, 1, 0.5, 0.5666, 1000, 10000, 1000000, 10000000, 1000000.0]; +describe('currencyFormaterMap', () => { + test('should return the correct formatter for Num', () => { + const formater = currencyFormaterMap[NumberFormat.Num]; + const result = ['0', '1', '0.5', '0.5666', '1,000', '10,000', '1,000,000', '10,000,000', '1,000,000']; + testCases.forEach((testCase) => { + expect(formater(testCase)).toBe(result[testCases.indexOf(testCase)]); + }); + }); + + test('should return the correct formatter for Percent', () => { + const formater = currencyFormaterMap[NumberFormat.Percent]; + const result = ['0%', '1%', '0.5%', '0.57%', '1,000%', '10,000%', '1,000,000%', '10,000,000%', '1,000,000%']; + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for USD', () => { + const formater = currencyFormaterMap[NumberFormat.USD]; + const result = ['$0', '$1', '$0.5', '$0.57', '$1,000', '$10,000', '$1,000,000', '$10,000,000', '$1,000,000']; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for CanadianDollar', () => { + const formater = currencyFormaterMap[NumberFormat.CanadianDollar]; + const result = [ + 'CA$0', + 'CA$1', + 'CA$0.5', + 'CA$0.57', + 'CA$1,000', + 'CA$10,000', + 'CA$1,000,000', + 'CA$10,000,000', + 'CA$1,000,000', + ]; + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for EUR', () => { + const formater = currencyFormaterMap[NumberFormat.EUR]; + + const result = ['€0', '€1', '€0.5', '€0.57', '€1,000', '€10,000', '€1,000,000', '€10,000,000', '€1,000,000']; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Pound', () => { + const formater = currencyFormaterMap[NumberFormat.Pound]; + + const result = ['£0', '£1', '£0.5', '£0.57', '£1,000', '£10,000', '£1,000,000', '£10,000,000', '£1,000,000']; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Yen', () => { + const formater = currencyFormaterMap[NumberFormat.Yen]; + + const result = [ + '¥0', + '¥1', + '¥0.5', + '¥0.57', + '¥1,000', + '¥10,000', + '¥1,000,000', + '¥10,000,000', + '¥1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Ruble', () => { + const formater = currencyFormaterMap[NumberFormat.Ruble]; + + const result = [ + '0 RUB', + '1 RUB', + '0,5 RUB', + '0,57 RUB', + '1 000 RUB', + '10 000 RUB', + '1 000 000 RUB', + '10 000 000 RUB', + '1 000 000 RUB', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Rupee', () => { + const formater = currencyFormaterMap[NumberFormat.Rupee]; + + const result = ['₹0', '₹1', '₹0.5', '₹0.57', '₹1,000', '₹10,000', '₹10,00,000', '₹1,00,00,000', '₹10,00,000']; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Won', () => { + const formater = currencyFormaterMap[NumberFormat.Won]; + + const result = ['₩0', '₩1', '₩0.5', '₩0.57', '₩1,000', '₩10,000', '₩1,000,000', '₩10,000,000', '₩1,000,000']; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Yuan', () => { + const formater = currencyFormaterMap[NumberFormat.Yuan]; + + const result = [ + 'CN¥0', + 'CN¥1', + 'CN¥0.5', + 'CN¥0.57', + 'CN¥1,000', + 'CN¥10,000', + 'CN¥1,000,000', + 'CN¥10,000,000', + 'CN¥1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Real', () => { + const formater = currencyFormaterMap[NumberFormat.Real]; + + const result = [ + 'R$ 0', + 'R$ 1', + 'R$ 0,5', + 'R$ 0,57', + 'R$ 1.000', + 'R$ 10.000', + 'R$ 1.000.000', + 'R$ 10.000.000', + 'R$ 1.000.000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Lira', () => { + const formater = currencyFormaterMap[NumberFormat.Lira]; + + const result = [ + 'TRY 0', + 'TRY 1', + 'TRY 0,5', + 'TRY 0,57', + 'TRY 1.000', + 'TRY 10.000', + 'TRY 1.000.000', + 'TRY 10.000.000', + 'TRY 1.000.000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Rupiah', () => { + const formater = currencyFormaterMap[NumberFormat.Rupiah]; + + const result = [ + 'IDR 0', + 'IDR 1', + 'IDR 0,5', + 'IDR 0,57', + 'IDR 1.000', + 'IDR 10.000', + 'IDR 1.000.000', + 'IDR 10.000.000', + 'IDR 1.000.000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Franc', () => { + const formater = currencyFormaterMap[NumberFormat.Franc]; + + const result = [ + 'CHF 0', + 'CHF 1', + 'CHF 0.5', + 'CHF 0.57', + `CHF 1’000`, + `CHF 10’000`, + `CHF 1’000’000`, + `CHF 10’000’000`, + `CHF 1’000’000`, + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for HongKongDollar', () => { + const formater = currencyFormaterMap[NumberFormat.HongKongDollar]; + + const result = [ + 'HK$0', + 'HK$1', + 'HK$0.5', + 'HK$0.57', + 'HK$1,000', + 'HK$10,000', + 'HK$1,000,000', + 'HK$10,000,000', + 'HK$1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for NewZealandDollar', () => { + const formater = currencyFormaterMap[NumberFormat.NewZealandDollar]; + + const result = [ + 'NZ$0', + 'NZ$1', + 'NZ$0.5', + 'NZ$0.57', + 'NZ$1,000', + 'NZ$10,000', + 'NZ$1,000,000', + 'NZ$10,000,000', + 'NZ$1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Krona', () => { + const formater = currencyFormaterMap[NumberFormat.Krona]; + + const result = [ + '0 SEK', + '1 SEK', + '0,5 SEK', + '0,57 SEK', + '1 000 SEK', + '10 000 SEK', + '1 000 000 SEK', + '10 000 000 SEK', + '1 000 000 SEK', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for NorwegianKrone', () => { + const formater = currencyFormaterMap[NumberFormat.NorwegianKrone]; + + const result = [ + 'NOK 0', + 'NOK 1', + 'NOK 0,5', + 'NOK 0,57', + 'NOK 1 000', + 'NOK 10 000', + 'NOK 1 000 000', + 'NOK 10 000 000', + 'NOK 1 000 000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for MexicanPeso', () => { + const formater = currencyFormaterMap[NumberFormat.MexicanPeso]; + + const result = [ + 'MX$0', + 'MX$1', + 'MX$0.5', + 'MX$0.57', + 'MX$1,000', + 'MX$10,000', + 'MX$1,000,000', + 'MX$10,000,000', + 'MX$1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Rand', () => { + const formater = currencyFormaterMap[NumberFormat.Rand]; + + const result = [ + 'ZAR 0', + 'ZAR 1', + 'ZAR 0,5', + 'ZAR 0,57', + 'ZAR 1 000', + 'ZAR 10 000', + 'ZAR 1 000 000', + 'ZAR 10 000 000', + 'ZAR 1 000 000', + ]; + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for NewTaiwanDollar', () => { + const formater = currencyFormaterMap[NumberFormat.NewTaiwanDollar]; + + const result = [ + 'NT$0', + 'NT$1', + 'NT$0.5', + 'NT$0.57', + 'NT$1,000', + 'NT$10,000', + 'NT$1,000,000', + 'NT$10,000,000', + 'NT$1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for DanishKrone', () => { + const formater = currencyFormaterMap[NumberFormat.DanishKrone]; + + const result = [ + '0 DKK', + '1 DKK', + '0,5 DKK', + '0,57 DKK', + '1.000 DKK', + '10.000 DKK', + '1.000.000 DKK', + '10.000.000 DKK', + '1.000.000 DKK', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for Baht', () => { + const formater = currencyFormaterMap[NumberFormat.Baht]; + + const result = [ + 'THB 0', + 'THB 1', + 'THB 0.5', + 'THB 0.57', + 'THB 1,000', + 'THB 10,000', + 'THB 1,000,000', + 'THB 10,000,000', + 'THB 1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for Forint', () => { + const formater = currencyFormaterMap[NumberFormat.Forint]; + + const result = [ + '0 HUF', + '1 HUF', + '0,5 HUF', + '0,57 HUF', + '1 000 HUF', + '10 000 HUF', + '1 000 000 HUF', + '10 000 000 HUF', + '1 000 000 HUF', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Koruna', () => { + const formater = currencyFormaterMap[NumberFormat.Koruna]; + + const result = [ + '0 CZK', + '1 CZK', + '0,5 CZK', + '0,57 CZK', + '1 000 CZK', + '10 000 CZK', + '1 000 000 CZK', + '10 000 000 CZK', + '1 000 000 CZK', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Shekel', () => { + const formater = currencyFormaterMap[NumberFormat.Shekel]; + + const result = [ + '‏0 ‏₪', + '‏1 ‏₪', + '‏0.5 ‏₪', + '‏0.57 ‏₪', + '‏1,000 ‏₪', + '‏10,000 ‏₪', + '‏1,000,000 ‏₪', + '‏10,000,000 ‏₪', + '‏1,000,000 ‏₪', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for ChileanPeso', () => { + const formater = currencyFormaterMap[NumberFormat.ChileanPeso]; + + const result = [ + 'CLP 0', + 'CLP 1', + 'CLP 0,5', + 'CLP 0,57', + 'CLP 1.000', + 'CLP 10.000', + 'CLP 1.000.000', + 'CLP 10.000.000', + 'CLP 1.000.000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for PhilippinePeso', () => { + const formater = currencyFormaterMap[NumberFormat.PhilippinePeso]; + + const result = ['₱0', '₱1', '₱0.5', '₱0.57', '₱1,000', '₱10,000', '₱1,000,000', '₱10,000,000', '₱1,000,000']; + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for Dirham', () => { + const formater = currencyFormaterMap[NumberFormat.Dirham]; + + const result = [ + '‏0 AED', + '‏1 AED', + '‏0.5 AED', + '‏0.57 AED', + '‏1,000 AED', + '‏10,000 AED', + '‏1,000,000 AED', + '‏10,000,000 AED', + '‏1,000,000 AED', + ]; + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for ColombianPeso', () => { + const formater = currencyFormaterMap[NumberFormat.ColombianPeso]; + + const result = [ + 'COP 0', + 'COP 1', + 'COP 0,5', + 'COP 0,57', + 'COP 1.000', + 'COP 10.000', + 'COP 1.000.000', + 'COP 10.000.000', + 'COP 1.000.000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for Riyal', () => { + const formater = currencyFormaterMap[NumberFormat.Riyal]; + + const result = [ + 'SAR 0', + 'SAR 1', + 'SAR 0.5', + 'SAR 0.57', + 'SAR 1,000', + 'SAR 10,000', + 'SAR 1,000,000', + 'SAR 10,000,000', + 'SAR 1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Ringgit', () => { + const formater = currencyFormaterMap[NumberFormat.Ringgit]; + + const result = [ + 'RM 0', + 'RM 1', + 'RM 0.5', + 'RM 0.57', + 'RM 1,000', + 'RM 10,000', + 'RM 1,000,000', + 'RM 10,000,000', + 'RM 1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Leu', () => { + const formater = currencyFormaterMap[NumberFormat.Leu]; + + const result = [ + '0 RON', + '1 RON', + '0,5 RON', + '0,57 RON', + '1.000 RON', + '10.000 RON', + '1.000.000 RON', + '10.000.000 RON', + '1.000.000 RON', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for ArgentinePeso', () => { + const formater = currencyFormaterMap[NumberFormat.ArgentinePeso]; + + const result = [ + 'ARS 0', + 'ARS 1', + 'ARS 0,5', + 'ARS 0,57', + 'ARS 1.000', + 'ARS 10.000', + 'ARS 1.000.000', + 'ARS 10.000.000', + 'ARS 1.000.000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for UruguayanPeso', () => { + const formater = currencyFormaterMap[NumberFormat.UruguayanPeso]; + + const result = [ + 'UYU 0', + 'UYU 1', + 'UYU 0,5', + 'UYU 0,57', + 'UYU 1.000', + 'UYU 10.000', + 'UYU 1.000.000', + 'UYU 10.000.000', + 'UYU 1.000.000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); +}); diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/number/format.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/format.ts new file mode 100644 index 0000000000..589f6ac3ec --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/format.ts @@ -0,0 +1,229 @@ +import { NumberFormat } from './number.type'; + +const commonProps = { + minimumFractionDigits: 0, + maximumFractionDigits: 2, + style: 'currency', + currencyDisplay: 'symbol', + useGrouping: true, +}; + +export const currencyFormaterMap: Record string> = { + [NumberFormat.Num]: (n: number) => + new Intl.NumberFormat('en-US', { + style: 'decimal', + minimumFractionDigits: 0, + maximumFractionDigits: 20, + }).format(n), + [NumberFormat.Percent]: (n: number) => + new Intl.NumberFormat('en-US', { + ...commonProps, + style: 'decimal', + }).format(n) + '%', + [NumberFormat.USD]: (n: number) => + new Intl.NumberFormat('en-US', { + ...commonProps, + currency: 'USD', + }).format(n), + [NumberFormat.CanadianDollar]: (n: number) => + new Intl.NumberFormat('en-CA', { + ...commonProps, + currency: 'CAD', + }) + .format(n) + .replace('$', 'CA$'), + [NumberFormat.EUR]: (n: number) => + new Intl.NumberFormat('en-IE', { + ...commonProps, + currency: 'EUR', + }).format(n), + [NumberFormat.Pound]: (n: number) => + new Intl.NumberFormat('en-GB', { + ...commonProps, + currency: 'GBP', + }).format(n), + [NumberFormat.Yen]: (n: number) => + new Intl.NumberFormat('ja-JP', { + ...commonProps, + currency: 'JPY', + }).format(n), + [NumberFormat.Ruble]: (n: number) => + new Intl.NumberFormat('ru-RU', { + ...commonProps, + currency: 'RUB', + currencyDisplay: 'code', + }) + .format(n) + .replaceAll(' ', ' '), + [NumberFormat.Rupee]: (n: number) => + new Intl.NumberFormat('hi-IN', { + ...commonProps, + currency: 'INR', + }).format(n), + [NumberFormat.Won]: (n: number) => + new Intl.NumberFormat('ko-KR', { + ...commonProps, + currency: 'KRW', + }).format(n), + [NumberFormat.Yuan]: (n: number) => + new Intl.NumberFormat('zh-CN', { + ...commonProps, + currency: 'CNY', + }) + .format(n) + .replace('¥', 'CN¥'), + [NumberFormat.Real]: (n: number) => + new Intl.NumberFormat('pt-BR', { + ...commonProps, + currency: 'BRL', + }) + .format(n) + .replaceAll(' ', ' '), + [NumberFormat.Lira]: (n: number) => + new Intl.NumberFormat('tr-TR', { + ...commonProps, + currency: 'TRY', + currencyDisplay: 'code', + }) + .format(n) + .replaceAll(' ', ' '), + [NumberFormat.Rupiah]: (n: number) => + new Intl.NumberFormat('id-ID', { + ...commonProps, + currency: 'IDR', + currencyDisplay: 'code', + }) + .format(n) + .replaceAll(' ', ' '), + [NumberFormat.Franc]: (n: number) => + new Intl.NumberFormat('de-CH', { + ...commonProps, + currency: 'CHF', + }) + .format(n) + .replaceAll(' ', ' '), + [NumberFormat.HongKongDollar]: (n: number) => + new Intl.NumberFormat('zh-HK', { + ...commonProps, + currency: 'HKD', + }).format(n), + [NumberFormat.NewZealandDollar]: (n: number) => + new Intl.NumberFormat('en-NZ', { + ...commonProps, + currency: 'NZD', + }) + .format(n) + .replace('$', 'NZ$'), + [NumberFormat.Krona]: (n: number) => + new Intl.NumberFormat('sv-SE', { + ...commonProps, + currency: 'SEK', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.NorwegianKrone]: (n: number) => + new Intl.NumberFormat('nb-NO', { + ...commonProps, + currency: 'NOK', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.MexicanPeso]: (n: number) => + new Intl.NumberFormat('es-MX', { + ...commonProps, + currency: 'MXN', + }) + .format(n) + .replace('$', 'MX$'), + [NumberFormat.Rand]: (n: number) => + new Intl.NumberFormat('en-ZA', { + ...commonProps, + currency: 'ZAR', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.NewTaiwanDollar]: (n: number) => + new Intl.NumberFormat('zh-TW', { + ...commonProps, + currency: 'TWD', + }) + .format(n) + .replace('$', 'NT$'), + [NumberFormat.DanishKrone]: (n: number) => + new Intl.NumberFormat('da-DK', { + ...commonProps, + currency: 'DKK', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.Baht]: (n: number) => + new Intl.NumberFormat('th-TH', { + ...commonProps, + currency: 'THB', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.Forint]: (n: number) => + new Intl.NumberFormat('hu-HU', { + ...commonProps, + currency: 'HUF', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.Koruna]: (n: number) => + new Intl.NumberFormat('cs-CZ', { + ...commonProps, + currency: 'CZK', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.Shekel]: (n: number) => + new Intl.NumberFormat('he-IL', { + ...commonProps, + currency: 'ILS', + }).format(n), + [NumberFormat.ChileanPeso]: (n: number) => + new Intl.NumberFormat('es-CL', { + ...commonProps, + currency: 'CLP', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.PhilippinePeso]: (n: number) => + new Intl.NumberFormat('fil-PH', { + ...commonProps, + currency: 'PHP', + }).format(n), + [NumberFormat.Dirham]: (n: number) => + new Intl.NumberFormat('ar-AE', { + ...commonProps, + currency: 'AED', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.ColombianPeso]: (n: number) => + new Intl.NumberFormat('es-CO', { + ...commonProps, + currency: 'COP', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.Riyal]: (n: number) => + new Intl.NumberFormat('en-US', { + ...commonProps, + currency: 'SAR', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.Ringgit]: (n: number) => + new Intl.NumberFormat('ms-MY', { + ...commonProps, + currency: 'MYR', + }).format(n), + [NumberFormat.Leu]: (n: number) => + new Intl.NumberFormat('ro-RO', { + ...commonProps, + currency: 'RON', + }).format(n), + [NumberFormat.ArgentinePeso]: (n: number) => + new Intl.NumberFormat('es-AR', { + ...commonProps, + currency: 'ARS', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.UruguayanPeso]: (n: number) => + new Intl.NumberFormat('es-UY', { + ...commonProps, + currency: 'UYU', + currencyDisplay: 'code', + }).format(n), +}; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/number/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/index.ts new file mode 100644 index 0000000000..27ca7cd8d8 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/index.ts @@ -0,0 +1,3 @@ +export * from './format'; +export * from './number.type'; +export * from './parse'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/number/number.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/number.type.ts new file mode 100644 index 0000000000..9140531325 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/number.type.ts @@ -0,0 +1,56 @@ +import { Filter } from '@/application/database-yjs'; + +export enum NumberFormat { + Num = 0, + USD = 1, + CanadianDollar = 2, + EUR = 4, + Pound = 5, + Yen = 6, + Ruble = 7, + Rupee = 8, + Won = 9, + Yuan = 10, + Real = 11, + Lira = 12, + Rupiah = 13, + Franc = 14, + HongKongDollar = 15, + NewZealandDollar = 16, + Krona = 17, + NorwegianKrone = 18, + MexicanPeso = 19, + Rand = 20, + NewTaiwanDollar = 21, + DanishKrone = 22, + Baht = 23, + Forint = 24, + Koruna = 25, + Shekel = 26, + ChileanPeso = 27, + PhilippinePeso = 28, + Dirham = 29, + ColombianPeso = 30, + Riyal = 31, + Ringgit = 32, + Leu = 33, + ArgentinePeso = 34, + UruguayanPeso = 35, + Percent = 36, +} + +export enum NumberFilterCondition { + Equal = 0, + NotEqual = 1, + GreaterThan = 2, + LessThan = 3, + GreaterThanOrEqualTo = 4, + LessThanOrEqualTo = 5, + NumberIsEmpty = 6, + NumberIsNotEmpty = 7, +} + +export interface NumberFilter extends Filter { + condition: NumberFilterCondition; + content: string; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/number/parse.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/parse.ts new file mode 100644 index 0000000000..9abac198b4 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/parse.ts @@ -0,0 +1,11 @@ +import { YDatabaseField } from '@/application/collab.type'; +import { getTypeOptions } from '../type_option'; +import { NumberFormat } from './number.type'; + +export function parseNumberTypeOptions(field: YDatabaseField) { + const numberTypeOption = getTypeOptions(field)?.toJSON(); + + return { + format: parseInt(numberTypeOption.format) as NumberFormat, + }; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/index.ts new file mode 100644 index 0000000000..4b94064b52 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/index.ts @@ -0,0 +1,2 @@ +export * from './parse'; +export * from './relation.type'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/parse.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/parse.ts new file mode 100644 index 0000000000..c5820576cd --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/parse.ts @@ -0,0 +1,9 @@ +import { YDatabaseField } from '@/application/collab.type'; +import { RelationTypeOption } from './relation.type'; +import { getTypeOptions } from '../type_option'; + +export function parseRelationTypeOption(field: YDatabaseField) { + const relationTypeOption = getTypeOptions(field)?.toJSON(); + + return relationTypeOption as RelationTypeOption; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/relation.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/relation.type.ts new file mode 100644 index 0000000000..31021afc38 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/relation.type.ts @@ -0,0 +1,9 @@ +import { Filter } from '@/application/database-yjs'; + +export interface RelationTypeOption { + database_id: string; +} + +export interface RelationFilter extends Filter { + condition: number; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/index.ts new file mode 100644 index 0000000000..a569b2ca47 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/index.ts @@ -0,0 +1,2 @@ +export * from './select_option.type'; +export * from './parse'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/parse.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/parse.ts new file mode 100644 index 0000000000..7840278a34 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/parse.ts @@ -0,0 +1,28 @@ +import { YDatabaseField, YjsDatabaseKey } from '@/application/collab.type'; +import { getTypeOptions } from '../type_option'; +import { SelectTypeOption } from './select_option.type'; + +export function parseSelectOptionTypeOptions(field: YDatabaseField) { + const content = getTypeOptions(field)?.get(YjsDatabaseKey.content); + + if (!content) return null; + + try { + return JSON.parse(content) as SelectTypeOption; + } catch (e) { + return null; + } +} + +export function parseSelectOptionCellData(field: YDatabaseField, data: string) { + const typeOption = parseSelectOptionTypeOptions(field); + const selectedIds = typeof data === 'string' ? data.split(',') : []; + + return selectedIds + .map((id) => { + const option = typeOption?.options?.find((option) => option.id === id); + + return option?.name ?? ''; + }) + .join(', '); +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/select_option.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/select_option.type.ts new file mode 100644 index 0000000000..343941d588 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/select_option.type.ts @@ -0,0 +1,38 @@ +import { Filter } from '@/application/database-yjs'; + +export enum SelectOptionColor { + Purple = 'Purple', + Pink = 'Pink', + LightPink = 'LightPink', + Orange = 'Orange', + Yellow = 'Yellow', + Lime = 'Lime', + Green = 'Green', + Aqua = 'Aqua', + Blue = 'Blue', +} + +export enum SelectOptionFilterCondition { + OptionIs = 0, + OptionIsNot = 1, + OptionContains = 2, + OptionDoesNotContain = 3, + OptionIsEmpty = 4, + OptionIsNotEmpty = 5, +} + +export interface SelectOptionFilter extends Filter { + condition: SelectOptionFilterCondition; + optionIds: string[]; +} + +export interface SelectOption { + id: string; + name: string; + color: SelectOptionColor; +} + +export interface SelectTypeOption { + disable_color: boolean; + options: SelectOption[]; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/text/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/text/index.ts new file mode 100644 index 0000000000..7d0a52cd9d --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/text/index.ts @@ -0,0 +1 @@ +export * from './text.type'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/text/text.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/text/text.type.ts new file mode 100644 index 0000000000..c2f230c738 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/text/text.type.ts @@ -0,0 +1,17 @@ +import { Filter } from '@/application/database-yjs'; + +export enum TextFilterCondition { + TextIs = 0, + TextIsNot = 1, + TextContains = 2, + TextDoesNotContain = 3, + TextStartsWith = 4, + TextEndsWith = 5, + TextIsEmpty = 6, + TextIsNotEmpty = 7, +} + +export interface TextFilter extends Filter { + condition: TextFilterCondition; + content: string; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/type_option.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/type_option.ts new file mode 100644 index 0000000000..bf9c80706f --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/type_option.ts @@ -0,0 +1,8 @@ +import { YDatabaseField, YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType } from '@/application/database-yjs'; + +export function getTypeOptions(field: YDatabaseField) { + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + + return field?.get(YjsDatabaseKey.type_option)?.get(String(fieldType)); +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/filter.ts b/frontend/appflowy_web_app/src/application/database-yjs/filter.ts new file mode 100644 index 0000000000..5f0919c4d9 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/filter.ts @@ -0,0 +1,223 @@ +import { + YDatabaseFields, + YDatabaseFilter, + YDatabaseFilters, + YDatabaseRow, + YDoc, + YjsDatabaseKey, + YjsEditorKey, +} from '@/application/collab.type'; +import { FieldType } from '@/application/database-yjs/database.type'; +import { + CheckboxFilter, + CheckboxFilterCondition, + ChecklistFilter, + ChecklistFilterCondition, + DateFilter, + NumberFilter, + NumberFilterCondition, + parseChecklistData, + SelectOptionFilter, + SelectOptionFilterCondition, + TextFilter, + TextFilterCondition, +} from '@/application/database-yjs/fields'; +import { Row } from '@/application/database-yjs/selector'; +import Decimal from 'decimal.js'; +import * as Y from 'yjs'; +import { every, filter, some } from 'lodash-es'; + +export function parseFilter(fieldType: FieldType, filter: YDatabaseFilter) { + const fieldId = filter.get(YjsDatabaseKey.field_id); + const filterType = Number(filter.get(YjsDatabaseKey.filter_type)); + const id = filter.get(YjsDatabaseKey.id); + const content = filter.get(YjsDatabaseKey.content); + const condition = Number(filter.get(YjsDatabaseKey.condition)); + + const value = { + fieldId, + filterType, + condition, + id, + content, + }; + + switch (fieldType) { + case FieldType.URL: + case FieldType.RichText: + return value as TextFilter; + case FieldType.Number: + return value as NumberFilter; + case FieldType.Checklist: + return value as ChecklistFilter; + case FieldType.Checkbox: + return value as CheckboxFilter; + case FieldType.SingleSelect: + case FieldType.MultiSelect: + // eslint-disable-next-line no-case-declarations + const options = content.split(','); + + return { + ...value, + optionIds: options, + } as SelectOptionFilter; + case FieldType.DateTime: + case FieldType.CreatedTime: + case FieldType.LastEditedTime: + return value as DateFilter; + } + + return value; +} + +function createPredicate(conditions: ((row: Row) => boolean)[]) { + return function (item: Row) { + return every(conditions, (condition) => condition(item)); + }; +} + +export function filterBy(rows: Row[], filters: YDatabaseFilters, fields: YDatabaseFields, rowMetas: Y.Map) { + const filterArray = filters.toArray(); + const conditions = filterArray.map((filter) => { + return (row: { id: string }) => { + const fieldId = filter.get(YjsDatabaseKey.field_id); + const field = fields.get(fieldId); + const fieldType = Number(field.get(YjsDatabaseKey.type)); + const rowId = row.id; + const rowMeta = rowMetas.get(rowId); + + if (!rowMeta) return false; + const filterValue = parseFilter(fieldType, filter); + const meta = rowMeta.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow; + + if (!meta) return false; + + const cells = meta.get(YjsDatabaseKey.cells); + const cell = cells.get(fieldId); + + if (!cell) return false; + const { condition, content } = filterValue; + + switch (fieldType) { + case FieldType.URL: + case FieldType.RichText: + return textFilterCheck(cell.get(YjsDatabaseKey.data) as string, content, condition); + case FieldType.Number: + return numberFilterCheck(cell.get(YjsDatabaseKey.data) as string, content, condition); + case FieldType.Checkbox: + return checkboxFilterCheck(cell.get(YjsDatabaseKey.data) as string, condition); + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return selectOptionFilterCheck(cell.get(YjsDatabaseKey.data) as string, content, condition); + case FieldType.Checklist: + return checklistFilterCheck(cell.get(YjsDatabaseKey.data) as string, content, condition); + default: + return true; + } + }; + }); + const predicate = createPredicate(conditions); + + return filter(rows, predicate); +} + +export function textFilterCheck(data: string, content: string, condition: TextFilterCondition) { + switch (condition) { + case TextFilterCondition.TextContains: + return data.includes(content); + case TextFilterCondition.TextDoesNotContain: + return !data.includes(content); + case TextFilterCondition.TextIs: + return data === content; + case TextFilterCondition.TextIsNot: + return data !== content; + case TextFilterCondition.TextIsEmpty: + return data === ''; + case TextFilterCondition.TextIsNotEmpty: + return data !== ''; + default: + return false; + } +} + +export function numberFilterCheck(data: string, content: string, condition: number) { + const decimal = new Decimal(data).toNumber(); + const filterDecimal = new Decimal(content).toNumber(); + + switch (condition) { + case NumberFilterCondition.Equal: + return decimal === filterDecimal; + case NumberFilterCondition.NotEqual: + return decimal !== filterDecimal; + case NumberFilterCondition.GreaterThan: + return decimal > filterDecimal; + case NumberFilterCondition.GreaterThanOrEqualTo: + return decimal >= filterDecimal; + case NumberFilterCondition.LessThan: + return decimal < filterDecimal; + case NumberFilterCondition.LessThanOrEqualTo: + return decimal <= filterDecimal; + case NumberFilterCondition.NumberIsEmpty: + return data === ''; + case NumberFilterCondition.NumberIsNotEmpty: + return data !== ''; + default: + return false; + } +} + +export function checkboxFilterCheck(data: string, condition: number) { + switch (condition) { + case CheckboxFilterCondition.IsChecked: + return data === 'Yes'; + case CheckboxFilterCondition.IsUnChecked: + return data !== 'Yes'; + default: + return false; + } +} + +export function checklistFilterCheck(data: string, content: string, condition: number) { + const percentage = parseChecklistData(data)?.percentage ?? 0; + + if (condition === ChecklistFilterCondition.IsComplete) { + return percentage === 1; + } + + return percentage !== 1; +} + +export function selectOptionFilterCheck(data: string, content: string, condition: number) { + const selectedOptionIds = data.split(','); + const filterOptionIds = content.split(','); + + switch (condition) { + // Ensure all filterOptionIds are included in selectedOptionIds + case SelectOptionFilterCondition.OptionIs: + return every(filterOptionIds, (option) => selectedOptionIds.includes(option)); + + // Ensure none of the filterOptionIds are included in selectedOptionIds + case SelectOptionFilterCondition.OptionIsNot: + return every(filterOptionIds, (option) => !selectedOptionIds.includes(option)); + + // Ensure at least one of the filterOptionIds is included in selectedOptionIds + case SelectOptionFilterCondition.OptionContains: + return some(filterOptionIds, (option) => selectedOptionIds.includes(option)); + + // Ensure at least one of the filterOptionIds is not included in selectedOptionIds + case SelectOptionFilterCondition.OptionDoesNotContain: + return some(filterOptionIds, (option) => !selectedOptionIds.includes(option)); + + // Ensure selectedOptionIds is empty + case SelectOptionFilterCondition.OptionIsEmpty: + return selectedOptionIds.length === 0; + + // Ensure selectedOptionIds is not empty + case SelectOptionFilterCondition.OptionIsNotEmpty: + return selectedOptionIds.length !== 0; + + // Default case, if no conditions match + default: + return false; + } +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/group.ts b/frontend/appflowy_web_app/src/application/database-yjs/group.ts new file mode 100644 index 0000000000..709053aa32 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/group.ts @@ -0,0 +1,54 @@ +import { YDatabaseField, YDoc, YjsDatabaseKey } from '@/application/collab.type'; +import { getCellData } from '@/application/database-yjs/const'; +import { FieldType } from '@/application/database-yjs/database.type'; +import { parseSelectOptionTypeOptions } from '@/application/database-yjs/fields'; +import { Row } from '@/application/database-yjs/selector'; +import * as Y from 'yjs'; + +export function groupByField(rows: Row[], rowMetas: Y.Map, field: YDatabaseField) { + const fieldType = Number(field.get(YjsDatabaseKey.type)); + const isSelectOptionField = [FieldType.SingleSelect, FieldType.MultiSelect].includes(fieldType); + + if (!isSelectOptionField) return; + return groupBySelectOption(rows, rowMetas, field); +} + +export function groupBySelectOption(rows: Row[], rowMetas: Y.Map, field: YDatabaseField) { + const fieldId = field.get(YjsDatabaseKey.id); + const result = new Map(); + const typeOption = parseSelectOptionTypeOptions(field); + + if (!typeOption) { + return; + } + + if (typeOption.options.length === 0) { + result.set(fieldId, rows); + return result; + } + + rows.forEach((row) => { + const cellData = getCellData(row.id, fieldId, rowMetas); + + const selectedIds = (cellData as string)?.split(',') ?? []; + + if (selectedIds.length === 0) { + const group = result.get(fieldId) ?? []; + + group.push(row); + result.set(fieldId, group); + return; + } + + selectedIds.forEach((id) => { + const option = typeOption.options.find((option) => option.id === id); + const groupName = option?.id ?? fieldId; + const group = result.get(groupName) ?? []; + + group.push(row); + result.set(groupName, group); + }); + }); + + return result; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/index.ts new file mode 100644 index 0000000000..1d5aa0ce3d --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/index.ts @@ -0,0 +1,6 @@ +export * from './context'; +export * from './fields'; +export * from './context'; +export * from './selector'; +export * from './database.type'; +export * from './const'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/selector.ts b/frontend/appflowy_web_app/src/application/database-yjs/selector.ts new file mode 100644 index 0000000000..d42399d882 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/selector.ts @@ -0,0 +1,644 @@ +import { FieldId, SortId, YDatabaseField, YjsDatabaseKey, YjsEditorKey, YjsFolderKey } from '@/application/collab.type'; +import { getCell, metaIdFromRowId, MIN_COLUMN_WIDTH } from '@/application/database-yjs/const'; +import { + DatabaseContext, + useDatabase, + useDatabaseFields, + useDatabaseView, + useRow, + useRowData, + useRows, + useViewId, +} from '@/application/database-yjs/context'; +import { filterBy, parseFilter } from '@/application/database-yjs/filter'; +import { groupByField } from '@/application/database-yjs/group'; +import { sortBy } from '@/application/database-yjs/sort'; +import { useViewsIdSelector } from '@/application/folder-yjs'; +import { useId } from '@/components/_shared/context-provider/IdProvider'; +import { parseYDatabaseCellToCell } from '@/components/database/components/cell/cell.parse'; +import { DateTimeCell } from '@/components/database/components/cell/cell.type'; +import dayjs from 'dayjs'; +import debounce from 'lodash-es/debounce'; +import React, { useContext, useEffect, useMemo, useState } from 'react'; +import { CalendarLayoutSetting, FieldType, FieldVisibility, Filter, RowMetaKey, SortCondition } from './database.type'; + +export interface Column { + fieldId: string; + width: number; + visibility: FieldVisibility; + wrap?: boolean; +} + +export interface Row { + id: string; + height: number; +} + +const defaultVisible = [FieldVisibility.AlwaysShown, FieldVisibility.HideWhenEmpty]; + +export function useDatabaseViewsSelector() { + const database = useDatabase(); + const { objectId: currentViewId } = useId(); + const { viewsId: visibleViewsId, views: folderViews } = useViewsIdSelector(); + const views = database?.get(YjsDatabaseKey.views); + const [viewIds, setViewIds] = useState([]); + const childViews = useMemo(() => { + return viewIds.map((viewId) => views?.get(viewId)); + }, [viewIds, views]); + + useEffect(() => { + if (!views) return; + + const observerEvent = () => { + setViewIds( + Array.from(views.keys()).filter((id) => { + const view = folderViews?.get(id); + + return ( + visibleViewsId.includes(id) && + (view?.get(YjsFolderKey.bid) === currentViewId || view?.get(YjsFolderKey.id) === currentViewId) + ); + }) + ); + }; + + observerEvent(); + views.observe(observerEvent); + + return () => { + views.unobserve(observerEvent); + }; + }, [visibleViewsId, views, folderViews, currentViewId]); + + return { + childViews, + viewIds, + }; +} + +export function useFieldsSelector(visibilitys: FieldVisibility[] = defaultVisible) { + const viewId = useViewId(); + const database = useDatabase(); + const [columns, setColumns] = useState([]); + + useEffect(() => { + if (!viewId) return; + const view = database?.get(YjsDatabaseKey.views)?.get(viewId); + const fields = database?.get(YjsDatabaseKey.fields); + const fieldsOrder = view?.get(YjsDatabaseKey.field_orders); + const fieldSettings = view?.get(YjsDatabaseKey.field_settings); + const getColumns = () => { + if (!fields || !fieldsOrder || !fieldSettings) return []; + const fieldIds = fieldsOrder.toJSON().map((item) => item.id) as string[]; + + return fieldIds + .map((fieldId) => { + const setting = fieldSettings.get(fieldId); + + return { + fieldId, + width: parseInt(setting?.get(YjsDatabaseKey.width)) || MIN_COLUMN_WIDTH, + visibility: Number( + setting?.get(YjsDatabaseKey.visibility) || FieldVisibility.AlwaysShown + ) as FieldVisibility, + wrap: setting?.get(YjsDatabaseKey.wrap) ?? true, + }; + }) + .filter((column) => { + return visibilitys.includes(column.visibility); + }); + }; + + const observerEvent = () => setColumns(getColumns()); + + setColumns(getColumns()); + + fieldsOrder?.observe(observerEvent); + fieldSettings?.observe(observerEvent); + + return () => { + fieldsOrder?.unobserve(observerEvent); + fieldSettings?.unobserve(observerEvent); + }; + }, [database, viewId, visibilitys]); + + return columns; +} + +export function useRowsSelector() { + const rowOrders = useRows(); + + return useMemo(() => rowOrders ?? [], [rowOrders]); +} + +export function useFieldSelector(fieldId: string) { + const database = useDatabase(); + const [field, setField] = useState(null); + const [clock, setClock] = useState(0); + + useEffect(() => { + if (!database) return; + + const field = database.get(YjsDatabaseKey.fields)?.get(fieldId); + + setField(field || null); + const observerEvent = () => setClock((prev) => prev + 1); + + field?.observe(observerEvent); + + return () => { + field?.unobserve(observerEvent); + }; + }, [database, fieldId]); + + return { + field, + clock, + }; +} + +export function useFiltersSelector() { + const database = useDatabase(); + const viewId = useViewId(); + const [filters, setFilters] = useState([]); + + useEffect(() => { + if (!viewId) return; + const view = database?.get(YjsDatabaseKey.views)?.get(viewId); + const filterOrders = view?.get(YjsDatabaseKey.filters); + + if (!filterOrders) return; + + const getFilters = () => { + return filterOrders.toJSON().map((item) => item.id); + }; + + const observerEvent = () => setFilters(getFilters()); + + setFilters(getFilters()); + + filterOrders.observe(observerEvent); + + return () => { + filterOrders.unobserve(observerEvent); + }; + }, [database, viewId]); + + return filters; +} + +export function useFilterSelector(filterId: string) { + const database = useDatabase(); + const viewId = useViewId(); + const fields = database?.get(YjsDatabaseKey.fields); + const [filterValue, setFilterValue] = useState(null); + + useEffect(() => { + if (!viewId) return; + const view = database?.get(YjsDatabaseKey.views)?.get(viewId); + const filter = view + ?.get(YjsDatabaseKey.filters) + .toArray() + .find((filter) => filter.get(YjsDatabaseKey.id) === filterId); + const field = fields?.get(filter?.get(YjsDatabaseKey.field_id) as FieldId); + + const observerEvent = () => { + if (!filter || !field) return; + const fieldType = Number(field.get(YjsDatabaseKey.type)) as FieldType; + + setFilterValue(parseFilter(fieldType, filter)); + }; + + observerEvent(); + field?.observe(observerEvent); + filter?.observe(observerEvent); + return () => { + field?.unobserve(observerEvent); + filter?.unobserve(observerEvent); + }; + }, [fields, viewId, filterId, database]); + return filterValue; +} + +export function useSortsSelector() { + const database = useDatabase(); + const viewId = useViewId(); + const [sorts, setSorts] = useState([]); + + useEffect(() => { + if (!viewId) return; + const view = database?.get(YjsDatabaseKey.views)?.get(viewId); + const sortOrders = view?.get(YjsDatabaseKey.sorts); + + if (!sortOrders) return; + + const getSorts = () => { + return sortOrders.toJSON().map((item) => item.id); + }; + + const observerEvent = () => setSorts(getSorts()); + + setSorts(getSorts()); + + sortOrders.observe(observerEvent); + + return () => { + sortOrders.unobserve(observerEvent); + }; + }, [database, viewId]); + + return sorts; +} + +export interface Sort { + fieldId: FieldId; + condition: SortCondition; + id: SortId; +} + +export function useSortSelector(sortId: SortId) { + const database = useDatabase(); + const viewId = useViewId(); + const [sortValue, setSortValue] = useState(null); + const views = database?.get(YjsDatabaseKey.views); + + useEffect(() => { + if (!viewId) return; + const view = views?.get(viewId); + const sort = view + ?.get(YjsDatabaseKey.sorts) + .toArray() + .find((sort) => sort.get(YjsDatabaseKey.id) === sortId); + + const observerEvent = () => { + setSortValue({ + fieldId: sort?.get(YjsDatabaseKey.field_id) as FieldId, + condition: Number(sort?.get(YjsDatabaseKey.condition)), + id: sort?.get(YjsDatabaseKey.id) as SortId, + }); + }; + + observerEvent(); + sort?.observe(observerEvent); + + return () => { + sort?.unobserve(observerEvent); + }; + }, [viewId, sortId, views]); + + return sortValue; +} + +export function useGroupsSelector() { + const database = useDatabase(); + const viewId = useViewId(); + const [groups, setGroups] = useState([]); + + useEffect(() => { + if (!viewId) return; + const view = database?.get(YjsDatabaseKey.views)?.get(viewId); + const groupOrders = view?.get(YjsDatabaseKey.groups); + + if (!groupOrders) return; + + const getGroups = () => { + return groupOrders.toJSON().map((item) => item.id); + }; + + const observerEvent = () => setGroups(getGroups()); + + setGroups(getGroups()); + + groupOrders.observe(observerEvent); + + return () => { + groupOrders.unobserve(observerEvent); + }; + }, [database, viewId]); + + return groups; +} + +export interface GroupColumn { + id: string; + visible: boolean; +} + +export function useGroup(groupId: string) { + const database = useDatabase(); + const viewId = useViewId() as string; + const view = database?.get(YjsDatabaseKey.views)?.get(viewId); + const group = view + ?.get(YjsDatabaseKey.groups) + ?.toArray() + .find((group) => group.get(YjsDatabaseKey.id) === groupId); + const groupColumns = group?.get(YjsDatabaseKey.groups); + const [fieldId, setFieldId] = useState(null); + const [columns, setColumns] = useState([]); + + useEffect(() => { + if (!viewId) return; + + const observerEvent = () => { + setFieldId(group?.get(YjsDatabaseKey.field_id) as string); + }; + + observerEvent(); + group?.observe(observerEvent); + + const observerColumns = () => { + if (!groupColumns) return; + setColumns(groupColumns.toJSON()); + }; + + observerColumns(); + groupColumns?.observe(observerColumns); + + return () => { + group?.unobserve(observerEvent); + groupColumns?.unobserve(observerColumns); + }; + }, [database, viewId, groupId, group, groupColumns]); + + return { + columns, + fieldId, + }; +} + +export function useRowsByGroup(groupId: string) { + const { columns, fieldId } = useGroup(groupId); + const rows = useContext(DatabaseContext)?.rowDocMap; + const rowOrders = useRowOrdersSelector(); + const fields = useDatabaseFields(); + const [notFound, setNotFound] = useState(false); + const [groupResult, setGroupResult] = useState>(new Map()); + + useEffect(() => { + if (!fieldId || !rowOrders || !rows) return; + + const onConditionsChange = () => { + const newResult = new Map(); + + const field = fields.get(fieldId); + + if (!field) { + setNotFound(true); + setGroupResult(newResult); + return; + } + + const groupResult = groupByField(rowOrders, rows, field); + + if (!groupResult) { + setGroupResult(newResult); + return; + } + + setGroupResult(groupResult); + }; + + onConditionsChange(); + + const debounceConditionsChange = debounce(onConditionsChange, 200); + + fields.observeDeep(debounceConditionsChange); + return () => { + fields.unobserveDeep(debounceConditionsChange); + }; + }, [fieldId, fields, rowOrders, rows]); + + const visibleColumns = columns.filter((column) => column.visible); + + return { + fieldId, + groupResult, + columns: visibleColumns, + notFound, + }; +} + +export function useRowOrdersSelector() { + const rows = useContext(DatabaseContext)?.rowDocMap; + const [rowOrders, setRowOrders] = useState(); + const view = useDatabaseView(); + const sorts = view?.get(YjsDatabaseKey.sorts); + const fields = useDatabaseFields(); + const filters = view?.get(YjsDatabaseKey.filters); + + useEffect(() => { + const onConditionsChange = () => { + const originalRowOrders = view?.get(YjsDatabaseKey.row_orders).toJSON(); + + if (!originalRowOrders || !rows) return; + + if (sorts?.length === 0 && filters?.length === 0) { + setRowOrders(originalRowOrders); + return; + } + + let rowOrders: Row[] | undefined; + + if (sorts?.length) { + rowOrders = sortBy(originalRowOrders, sorts, fields, rows); + } + + if (filters?.length) { + rowOrders = filterBy(rowOrders ?? originalRowOrders, filters, fields, rows); + } + + if (rowOrders) { + setRowOrders(rowOrders); + } else { + setRowOrders(originalRowOrders); + } + }; + + const debounceConditionsChange = debounce(onConditionsChange, 200); + + onConditionsChange(); + sorts?.observeDeep(debounceConditionsChange); + filters?.observeDeep(debounceConditionsChange); + fields?.observeDeep(debounceConditionsChange); + rows?.observeDeep(debounceConditionsChange); + + return () => { + sorts?.unobserveDeep(debounceConditionsChange); + filters?.unobserveDeep(debounceConditionsChange); + fields?.unobserveDeep(debounceConditionsChange); + rows?.observeDeep(debounceConditionsChange); + }; + }, [fields, rows, sorts, filters, view]); + + return rowOrders; +} + +export function useCellSelector({ rowId, fieldId }: { rowId: string; fieldId: string }) { + const row = useRowData(rowId); + const cell = row?.get(YjsDatabaseKey.cells)?.get(fieldId); + const [cellValue, setCellValue] = useState(() => (cell ? parseYDatabaseCellToCell(cell) : undefined)); + + useEffect(() => { + if (!cell) return; + setCellValue(parseYDatabaseCellToCell(cell)); + const observerEvent = () => setCellValue(parseYDatabaseCellToCell(cell)); + + cell.observe(observerEvent); + + return () => { + cell.unobserve(observerEvent); + }; + }, [cell]); + + return cellValue; +} + +export interface CalendarEvent { + start?: Date; + end?: Date; + id: string; +} + +export function useCalendarEventsSelector() { + const setting = useCalendarLayoutSetting(); + const filedId = setting.fieldId; + const { field } = useFieldSelector(filedId); + const rowOrders = useRowOrdersSelector(); + const rows = useContext(DatabaseContext)?.rowDocMap; + const [events, setEvents] = useState([]); + const [emptyEvents, setEmptyEvents] = useState([]); + + useEffect(() => { + if (!field || !rowOrders || !rows) return; + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + + if (fieldType !== FieldType.DateTime) return; + const newEvents: CalendarEvent[] = []; + const emptyEvents: CalendarEvent[] = []; + + rowOrders?.forEach((row) => { + const cell = getCell(row.id, filedId, rows); + + if (!cell) { + emptyEvents.push({ + id: `${row.id}:${filedId}`, + }); + return; + } + + const value = parseYDatabaseCellToCell(cell) as DateTimeCell; + + if (!value || !value.data) { + emptyEvents.push({ + id: `${row.id}:${filedId}`, + }); + return; + } + + const getDate = (timestamp: string) => { + const dayjsResult = timestamp.length === 10 ? dayjs.unix(Number(timestamp)) : dayjs(timestamp); + + return dayjsResult.toDate(); + }; + + newEvents.push({ + id: `${row.id}:${filedId}`, + start: getDate(value.data), + end: value.endTimestamp && value.isRange ? getDate(value.endTimestamp) : getDate(value.data), + }); + }); + + setEvents(newEvents); + setEmptyEvents(emptyEvents); + }, [field, rowOrders, rows, filedId]); + + return { events, emptyEvents }; +} + +export function useCalendarLayoutSetting() { + const view = useDatabaseView(); + const layoutSetting = view?.get(YjsDatabaseKey.layout_settings)?.get('2'); + const [setting, setSetting] = useState({ + fieldId: '', + firstDayOfWeek: 0, + showWeekNumbers: true, + showWeekends: true, + layout: 0, + }); + + useEffect(() => { + const observerHandler = () => { + setSetting({ + fieldId: layoutSetting?.get(YjsDatabaseKey.field_id) as string, + firstDayOfWeek: Number(layoutSetting?.get(YjsDatabaseKey.first_day_of_week)), + showWeekNumbers: Boolean(layoutSetting?.get(YjsDatabaseKey.show_week_numbers)), + showWeekends: Boolean(layoutSetting?.get(YjsDatabaseKey.show_weekends)), + layout: Number(layoutSetting?.get(YjsDatabaseKey.layout_ty)), + }); + }; + + observerHandler(); + layoutSetting?.observe(observerHandler); + return () => { + layoutSetting?.unobserve(observerHandler); + }; + }, [layoutSetting]); + + return setting; +} + +export function usePrimaryFieldId() { + const database = useDatabase(); + const [primaryFieldId, setPrimaryFieldId] = React.useState(null); + + useEffect(() => { + const fields = database?.get(YjsDatabaseKey.fields); + const primaryFieldId = Array.from(fields?.keys() || []).find((fieldId) => { + return fields?.get(fieldId)?.get(YjsDatabaseKey.is_primary); + }); + + setPrimaryFieldId(primaryFieldId || null); + }, [database]); + + return primaryFieldId; +} + +export interface RowMeta { + documentId: string; + cover: string; + icon: string; + isEmptyDocument: boolean; +} + +export const useRowMetaSelector = (rowId: string) => { + const [meta, setMeta] = useState(); + const yMeta = useRow(rowId)?.get(YjsEditorKey.meta); + + useEffect(() => { + if (!yMeta) return; + const onChange = () => { + const metaJson = yMeta.toJSON(); + const getData = metaIdFromRowId(rowId); + const icon = metaJson[getData(RowMetaKey.IconId)]; + const cover = metaJson[getData(RowMetaKey.CoverId)]; + const documentId = getData(RowMetaKey.DocumentId); + const isEmptyDocument = metaJson[getData(RowMetaKey.IsDocumentEmpty)]; + + return setMeta({ + icon, + cover, + documentId, + isEmptyDocument, + }); + }; + + onChange(); + + yMeta.observe(onChange); + return () => { + yMeta.unobserve(onChange); + }; + }, [rowId, yMeta]); + + return meta; +}; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/sort.ts b/frontend/appflowy_web_app/src/application/database-yjs/sort.ts new file mode 100644 index 0000000000..f229ce8f74 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/sort.ts @@ -0,0 +1,80 @@ +import { + YDatabaseField, + YDatabaseFields, + YDatabaseRow, + YDatabaseSorts, + YDoc, + YjsDatabaseKey, + YjsEditorKey, +} from '@/application/collab.type'; +import { FieldType, SortCondition } from '@/application/database-yjs/database.type'; +import { parseChecklistData, parseSelectOptionCellData } from '@/application/database-yjs/fields'; +import { Row } from '@/application/database-yjs/selector'; +import orderBy from 'lodash-es/orderBy'; +import * as Y from 'yjs'; + +export function sortBy(rows: Row[], sorts: YDatabaseSorts, fields: YDatabaseFields, rowMetas: Y.Map) { + const sortArray = sorts.toArray(); + const iteratees = sortArray.map((sort) => { + return (row: { id: string }) => { + const fieldId = sort.get(YjsDatabaseKey.field_id); + const field = fields.get(fieldId); + const fieldType = Number(field.get(YjsDatabaseKey.type)); + + const rowId = row.id; + const rowMeta = rowMetas.get(rowId); + + const defaultData = parseCellDataForSort(field, ''); + + if (!rowMeta) return defaultData; + const meta = rowMeta.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow; + + if (!meta) return defaultData; + if (fieldType === FieldType.LastEditedTime) { + return meta.get(YjsDatabaseKey.last_modified); + } + + if (fieldType === FieldType.CreatedTime) { + return meta.get(YjsDatabaseKey.created_at); + } + + const cells = meta.get(YjsDatabaseKey.cells); + const cell = cells.get(fieldId); + + if (!cell) return defaultData; + + return parseCellDataForSort(field, cell.get(YjsDatabaseKey.data) ?? ''); + }; + }); + const orders = sortArray.map((sort) => { + const condition = Number(sort.get(YjsDatabaseKey.condition)); + + if (condition === SortCondition.Descending) return 'desc'; + return 'asc'; + }); + + return orderBy(rows, iteratees, orders); +} + +export function parseCellDataForSort(field: YDatabaseField, data: string | boolean | number | object) { + const fieldType = Number(field.get(YjsDatabaseKey.type)); + + switch (fieldType) { + case FieldType.RichText: + case FieldType.URL: + return data ? data : '\uFFFF'; + case FieldType.Number: + return data; + case FieldType.Checkbox: + return data === 'Yes'; + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return parseSelectOptionCellData(field, typeof data === 'string' ? data : ''); + case FieldType.Checklist: + return parseChecklistData(typeof data === 'string' ? data : '')?.percentage ?? 0; + case FieldType.DateTime: + return Number(data); + case FieldType.Relation: + return ''; + } +} diff --git a/frontend/appflowy_web_app/src/application/document.type.ts b/frontend/appflowy_web_app/src/application/document.type.ts deleted file mode 100644 index da559c5bde..0000000000 --- a/frontend/appflowy_web_app/src/application/document.type.ts +++ /dev/null @@ -1,176 +0,0 @@ -import Y from 'yjs'; - -export type BlockId = string; - -export type ExternalId = string; - -export type ChildrenId = string; - -export enum BlockType { - Paragraph = 'paragraph', - Page = 'page', - HeadingBlock = 'heading', - TodoListBlock = 'todo_list', - BulletedListBlock = 'bulleted_list', - NumberedListBlock = 'numbered_list', - ToggleListBlock = 'toggle_list', - CodeBlock = 'code', - EquationBlock = 'math_equation', - QuoteBlock = 'quote', - CalloutBlock = 'callout', - DividerBlock = 'divider', - ImageBlock = 'image', - GridBlock = 'grid', - OutlineBlock = 'outline', - TableBlock = 'table', - TableCell = 'table/cell', -} - -export enum InlineBlockType { - Formula = 'formula', - Mention = 'mention', -} - -export enum AlignType { - Left = 'left', - Center = 'center', - Right = 'right', -} - -export interface BlockData { - bg_color?: string; - font_color?: string; - align?: AlignType; -} - -export interface HeadingBlockData extends BlockData { - level: number; -} - -export interface NumberedListBlockData extends BlockData { - number: number; -} - -export interface TodoListBlockData extends BlockData { - checked: boolean; -} - -export interface ToggleListBlockData extends BlockData { - collapsed: boolean; -} - -export interface CodeBlockData extends BlockData { - language: string; -} - -export interface CalloutBlockData extends BlockData { - icon: string; -} - -export interface MathEquationBlockData extends BlockData { - formula?: string; -} - -export enum ImageType { - Local = 0, - Internal = 1, - External = 2, -} - -export interface ImageBlockData extends BlockData { - url?: string; - width?: number; - align?: AlignType; - image_type?: ImageType; - height?: number; -} - -export interface OutlineBlockData extends BlockData { - depth?: number; -} - -export interface TableBlockData extends BlockData { - colDefaultWidth: number; - colMinimumWidth: number; - colsHeight: number; - colsLen: number; - rowDefaultHeight: number; - rowsLen: number; -} - -export interface TableCellBlockData extends BlockData { - colPosition: number; - height: number; - rowPosition: number; - width: number; -} - -export enum MentionType { - PageRef = 'page', - Date = 'date', -} - -export interface Mention { - // inline page ref id - page_id?: string; - // reminder date ref id - date?: string; - - type: MentionType; -} - -export enum YjsEditorKey { - data_section = 'data', - document = 'document', - database = 'database', - workspace_database = 'databases', - folder = 'folder', - // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values - database_row = 'data', - user_awareness = 'user_awareness', - blocks = 'blocks', - page_id = 'page_id', - meta = 'meta', - children_map = 'children_map', - text_map = 'text_map', - text = 'text', - delta = 'delta', - - block_id = 'id', - block_type = 'ty', - // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values - block_data = 'data', - block_parent = 'parent', - block_children = 'children', - block_external_id = 'external_id', - block_external_type = 'external_type', -} - -export interface YDoc extends Y.Doc { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - get(key: YjsEditorKey.data_section | string): YSharedRoot | any; -} - -export interface YSharedRoot extends Y.Map { - get(key: YjsEditorKey.document): YDocument; -} - -export interface YDocument extends Y.Map { - get(key: YjsEditorKey.blocks | YjsEditorKey.page_id | YjsEditorKey.meta): YBlocks | YMeta | string; -} - -export interface YBlocks extends Y.Map { - get(key: BlockId): Y.Map; -} - -export interface YMeta extends Y.Map { - get(key: YjsEditorKey.children_map | YjsEditorKey.text_map): YChildrenMap | YTextMap; -} - -export interface YChildrenMap extends Y.Map { - get(key: ChildrenId): Y.Array; -} - -export interface YTextMap extends Y.Map { - get(key: ExternalId): Y.Text; -} diff --git a/frontend/appflowy_web_app/src/application/folder-yjs/selector.ts b/frontend/appflowy_web_app/src/application/folder-yjs/selector.ts index 295315874b..e5eb68203e 100644 --- a/frontend/appflowy_web_app/src/application/folder-yjs/selector.ts +++ b/frontend/appflowy_web_app/src/application/folder-yjs/selector.ts @@ -5,33 +5,39 @@ import { useEffect, useState } from 'react'; export function useViewsIdSelector() { const folder = useFolderContext(); const [viewsId, setViewsId] = useState([]); + const views = folder?.get(YjsFolderKey.views); + const trash = folder?.get(YjsFolderKey.section)?.get(YjsFolderKey.trash); + const meta = folder?.get(YjsFolderKey.meta); useEffect(() => { - if (!folder) return; + if (!views) return; - const views = folder.get(YjsFolderKey.views); - const trash = folder.get(YjsFolderKey.section)?.get(YjsFolderKey.trash); - const meta = folder.get(YjsFolderKey.meta); + const trashUid = trash ? Array.from(trash.keys())[0] : null; + const userTrash = trashUid ? trash?.get(trashUid) : null; - console.log('folder', folder.toJSON()); const collectIds = () => { - return Array.from(views.keys()).filter( - (id) => !trash?.has(id) && id !== meta?.get(YjsFolderKey.current_workspace) - ); + const trashIds = userTrash?.toJSON()?.map((item) => item.id) || []; + + return Array.from(views.keys()).filter((id) => { + return !trashIds.includes(id) && id !== meta?.get(YjsFolderKey.current_workspace); + }); }; setViewsId(collectIds()); const observerEvent = () => setViewsId(collectIds()); - folder.observe(observerEvent); + views.observe(observerEvent); + userTrash?.observe(observerEvent); return () => { - folder.unobserve(observerEvent); + views.unobserve(observerEvent); + userTrash?.unobserve(observerEvent); }; - }, [folder]); + }, [views, trash, meta]); return { viewsId, + views, }; } diff --git a/frontend/appflowy_web_app/src/application/services/js-services/database.service.ts b/frontend/appflowy_web_app/src/application/services/js-services/database.service.ts new file mode 100644 index 0000000000..e80915c393 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/database.service.ts @@ -0,0 +1,172 @@ +import { CollabOrigin, CollabType, YDatabase, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; +import { + batchCollabs, + getCollabStorage, + getCollabStorageWithAPICall, + getUserWorkspace, +} from '@/application/services/js-services/storage'; +import { DatabaseService } from '@/application/services/services.type'; +import * as Y from 'yjs'; + +export class JSDatabaseService implements DatabaseService { + private loadedDatabaseId: Set = new Set(); + + constructor() { + // + } + + async getDatabase( + workspaceId: string, + databaseId: string, + rowIds?: string[] + ): Promise<{ + databaseDoc: YDoc; + rows: Y.Map; + }> { + const rootRowsDoc = new Y.Doc(); + const rowsFolder = rootRowsDoc.getMap(); + const isLoaded = this.loadedDatabaseId.has(databaseId); + let databaseDoc: YDoc | undefined = undefined; + + if (isLoaded) { + databaseDoc = (await getCollabStorage(databaseId, CollabType.Database)).doc; + } else { + databaseDoc = await getCollabStorageWithAPICall(workspaceId, databaseId, CollabType.Database); + } + + const database = databaseDoc.getMap(YjsEditorKey.data_section)?.get(YjsEditorKey.database) as YDatabase; + const viewId = database.get(YjsDatabaseKey.metas)?.get(YjsDatabaseKey.iid)?.toString(); + const rowOrders = database.get(YjsDatabaseKey.views)?.get(viewId)?.get(YjsDatabaseKey.row_orders); + const rowOrdersIds = rowOrders.toJSON() as { + id: string; + }[]; + + if (!rowOrdersIds) { + throw new Error('Database rows not found'); + } + + const ids = rowIds ? rowIds : rowOrdersIds.map((item) => item.id); + + if (isLoaded) { + for (const id of ids) { + const { doc } = await getCollabStorage(id, CollabType.DatabaseRow); + + rowsFolder.set(id, doc); + } + } else { + const rows = await this.loadDatabaseRows(workspaceId, ids); + + rows.forEach((row, id) => { + rowsFolder.set(id, row); + }); + } + + this.loadedDatabaseId.add(databaseId); + + if (!rowIds) { + // Update rows if new rows are added + rowOrders?.observe((event) => { + if (event.changes.added.size > 0) { + const rowIds = rowOrders.toJSON() as { + id: string; + }[]; + + console.log('Update rows', rowIds); + void this.loadDatabaseRows( + workspaceId, + rowIds.map((item) => item.id) + ).then((newRows) => { + newRows.forEach((row, id) => { + rowsFolder.set(id, row); + }); + }); + } + }); + } + + return { + databaseDoc, + rows: rowsFolder as Y.Map, + }; + } + + async openDatabase( + workspaceId: string, + viewId: string, + rowIds?: string[] + ): Promise<{ + databaseDoc: YDoc; + rows: Y.Map; + }> { + const userWorkspace = await getUserWorkspace(); + + if (!userWorkspace) { + throw new Error('User workspace not found'); + } + + const workspaceDatabaseId = userWorkspace.workspaces.find( + (workspace) => workspace.id === workspaceId + )?.workspaceDatabaseId; + + if (!workspaceDatabaseId) { + throw new Error('Workspace database not found'); + } + + const workspaceDatabase = await getCollabStorageWithAPICall( + workspaceId, + workspaceDatabaseId, + CollabType.WorkspaceDatabase + ); + + const databases = workspaceDatabase + .getMap(YjsEditorKey.data_section) + .get(YjsEditorKey.workspace_database) + .toJSON() as { + views: string[]; + database_id: string; + }[]; + + const databaseMeta = databases.find((item) => { + return item.views.some((databaseViewId: string) => databaseViewId === viewId); + }); + + if (!databaseMeta) { + throw new Error('Database not found'); + } + + const { databaseDoc, rows } = await this.getDatabase(workspaceId, databaseMeta.database_id, rowIds); + + const handleUpdate = (update: Uint8Array, origin: CollabOrigin) => { + if (origin === CollabOrigin.LocalSync) { + // Send the update to the server + console.log('update', update); + } + }; + + databaseDoc.on('update', handleUpdate); + + return { + databaseDoc, + rows, + }; + } + + async loadDatabaseRows(workspaceId: string, rowIds: string[]) { + const rows = new Map(); + + try { + await batchCollabs( + workspaceId, + rowIds.map((id) => ({ + object_id: id, + collab_type: CollabType.DatabaseRow, + })), + (id, rowDoc) => rows.set(id, rowDoc) + ); + } catch (e) { + console.error(e); + } + + return rows; + } +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/db/index.ts b/frontend/appflowy_web_app/src/application/services/js-services/db/index.ts index ebe8870c15..bf5f0c7aa1 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/db/index.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/db/index.ts @@ -1,41 +1,8 @@ import { YDoc } from '@/application/collab.type'; -import { getAuthInfo } from '@/application/services/js-services/storage'; -import * as Y from 'yjs'; -import { IndexeddbPersistence } from 'y-indexeddb'; import { databasePrefix } from '@/application/constants'; -import BaseDexie from 'dexie'; -import { usersSchema, UsersTable } from './tables/users'; - -const version = 1; - -type DexieTables = UsersTable; -export type Dexie = BaseDexie & T; - -let db: Dexie | undefined; - -export function getDB() { - const authInfo = getAuthInfo(); - - if (!db && authInfo?.uuid) { - return openDB(authInfo?.uuid); - } - - return db; -} - -export function openDB(uuid: string) { - const dbName = `${databasePrefix}_${uuid}`; - - if (db && db.name === dbName) { - return db; - } - - db = new BaseDexie(dbName) as Dexie; - const schema = Object.assign({}, usersSchema); - - db.version(version).stores(schema); - return db; -} +import { getAuthInfo } from '@/application/services/js-services/storage'; +import { IndexeddbPersistence } from 'y-indexeddb'; +import * as Y from 'yjs'; /** * Open the collaboration database, and return a function to close it @@ -66,3 +33,10 @@ export async function deleteCollabDB(docName: string) { await provider.destroy(); } + +export function getDBName(id: string, type: string) { + const { uuid } = getAuthInfo() || {}; + + if (!uuid) throw new Error('No user found'); + return `${uuid}_${type}_${id}`; +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/db/tables/users.ts b/frontend/appflowy_web_app/src/application/services/js-services/db/tables/users.ts deleted file mode 100644 index 1da8f20b0c..0000000000 --- a/frontend/appflowy_web_app/src/application/services/js-services/db/tables/users.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Table } from 'dexie'; -import { UserProfile } from '@/application/user.type'; - -export type UsersTable = { - users: Table; -}; - -export const usersSchema = { - users: 'uuid, uid, email, name, workspaceId, iconUrl', -}; \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/application/services/js-services/document.service.ts b/frontend/appflowy_web_app/src/application/services/js-services/document.service.ts index 1af92df8a0..e93809449d 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/document.service.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/document.service.ts @@ -1,41 +1,20 @@ import { CollabOrigin, CollabType, YDoc } from '@/application/collab.type'; -import { getDocumentStorage } from '@/application/services/js-services/storage/document'; +import { getCollabStorageWithAPICall } from '@/application/services/js-services/storage'; import { DocumentService } from '@/application/services/services.type'; -import { APIService } from 'src/application/services/js-services/wasm'; -import { applyDocument } from 'src/application/ydoc/apply'; export class JSDocumentService implements DocumentService { constructor() { // } - fetchDocument(workspaceId: string, docId: string) { - return APIService.getCollab(workspaceId, docId, CollabType.Document); - } - async openDocument(workspaceId: string, docId: string): Promise { - const { doc, localExist } = await getDocumentStorage(docId); - const asyncApply = async () => { - const res = await this.fetchDocument(workspaceId, docId); - - applyDocument(doc, res.state); - }; - - // If the document exists locally, apply the state asynchronously, - // otherwise, apply the state synchronously - if (localExist) { - void asyncApply(); - } else { - await asyncApply(); - } + const doc = await getCollabStorageWithAPICall(workspaceId, docId, CollabType.Document); const handleUpdate = (update: Uint8Array, origin: CollabOrigin) => { - if (origin === CollabOrigin.Remote) { - return; + if (origin === CollabOrigin.LocalSync) { + // Send the update to the server + console.log('update', update); } - - // Send the update to the server - console.log('update', update); }; doc.on('update', handleUpdate); diff --git a/frontend/appflowy_web_app/src/application/services/js-services/folder.service.ts b/frontend/appflowy_web_app/src/application/services/js-services/folder.service.ts index 796cd078d6..c475cfa935 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/folder.service.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/folder.service.ts @@ -1,41 +1,19 @@ import { CollabOrigin, CollabType, YDoc } from '@/application/collab.type'; -import { getFolderStorage } from '@/application/services/js-services/storage/folder'; +import { getCollabStorageWithAPICall } from '@/application/services/js-services/storage'; import { FolderService } from '@/application/services/services.type'; -import { APIService } from 'src/application/services/js-services/wasm'; -import { applyDocument } from 'src/application/ydoc/apply'; export class JSFolderService implements FolderService { constructor() { // } - fetchFolder(workspaceId: string) { - return APIService.getCollab(workspaceId, workspaceId, CollabType.Folder); - } - async openWorkspace(workspaceId: string): Promise { - const { doc, localExist } = await getFolderStorage(workspaceId); - const asyncApply = async () => { - const res = await this.fetchFolder(workspaceId); - - applyDocument(doc, res.state); - }; - - // If the document exists locally, apply the state asynchronously, - // otherwise, apply the state synchronously - if (localExist) { - void asyncApply(); - } else { - await asyncApply(); - } - + const doc = await getCollabStorageWithAPICall(workspaceId, workspaceId, CollabType.Folder); const handleUpdate = (update: Uint8Array, origin: CollabOrigin) => { - if (origin === CollabOrigin.Remote) { - return; + if (origin === CollabOrigin.LocalSync) { + // Send the update to the server + console.log('update', update); } - - // Send the update to the server - console.log('update', update); }; doc.on('update', handleUpdate); diff --git a/frontend/appflowy_web_app/src/application/services/js-services/index.ts b/frontend/appflowy_web_app/src/application/services/js-services/index.ts index 3410c8d27e..d31b7f117a 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/index.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/index.ts @@ -1,7 +1,9 @@ +import { JSDatabaseService } from '@/application/services/js-services/database.service'; import { AFService, AFServiceConfig, AuthService, + DatabaseService, DocumentService, FolderService, UserService, @@ -22,6 +24,8 @@ export class AFClientService implements AFService { folderService: FolderService; + databaseService: DatabaseService; + private deviceId: string = nanoid(8); private clientId: string = 'web'; @@ -45,5 +49,6 @@ export class AFClientService implements AFService { this.userService = new JSUserService(); this.documentService = new JSDocumentService(); this.folderService = new JSFolderService(); + this.databaseService = new JSDatabaseService(); } } diff --git a/frontend/appflowy_web_app/src/application/services/js-services/storage/auth.ts b/frontend/appflowy_web_app/src/application/services/js-services/storage/auth.ts index bb19f590bc..dd8d3d1d99 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/storage/auth.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/storage/auth.ts @@ -1,11 +1,3 @@ -import { getAuthInfo } from '@/application/services/js-services/storage/token'; -import { openDB } from '@/application/services/js-services/db'; - export async function signInSuccess() { - const authInfo = getAuthInfo(); - - if (authInfo) { - // Open the database - openDB(authInfo.uuid); - } + // Do nothing } diff --git a/frontend/appflowy_web_app/src/application/services/js-services/storage/collab.ts b/frontend/appflowy_web_app/src/application/services/js-services/storage/collab.ts new file mode 100644 index 0000000000..bdc1392024 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/storage/collab.ts @@ -0,0 +1,101 @@ +import { CollabType, YDoc, YjsEditorKey } from '@/application/collab.type'; +import { getDBName, openCollabDB } from '@/application/services/js-services/db'; +import { APIService } from '@/application/services/js-services/wasm'; +import { applyYDoc } from '@/application/ydoc/apply'; + +export function fetchCollab(workspaceId: string, id: string, type: CollabType) { + return APIService.getCollab(workspaceId, id, type); +} + +export function batchFetchCollab(workspaceId: string, params: { object_id: string; collab_type: CollabType }[]) { + return APIService.batchGetCollab(workspaceId, params); +} + +function collabTypeToDBType(type: CollabType) { + switch (type) { + case CollabType.Folder: + return 'folder'; + case CollabType.Document: + return 'document'; + case CollabType.Database: + return 'database'; + case CollabType.WorkspaceDatabase: + return 'databases'; + case CollabType.DatabaseRow: + return 'database_row'; + case CollabType.UserAwareness: + return 'user_awareness'; + default: + return ''; + } +} + +export async function getCollabStorage(id: string, type: CollabType) { + const name = getDBName(id, collabTypeToDBType(type)); + + const doc = await openCollabDB(name); + const localExist = doc.share.has(YjsEditorKey.data_section); + + return { + doc, + localExist, + }; +} + +export async function getCollabStorageWithAPICall(workspaceId: string, id: string, type: CollabType) { + const { doc, localExist } = await getCollabStorage(id, type); + const asyncApply = async () => { + const res = await fetchCollab(workspaceId, id, type); + + applyYDoc(doc, res.state); + }; + + // If the document exists locally, apply the state asynchronously, + // otherwise, apply the state synchronously + if (localExist) { + void asyncApply(); + } else { + await asyncApply(); + } + + return doc; +} + +export async function batchCollabs( + workspaceId: string, + params: { + object_id: string; + collab_type: CollabType; + }[], + rowCallback?: (id: string, doc: YDoc) => void +) { + console.log('Fetching collab data:', params); + // Create or get Y.Doc from local storage + for (const item of params) { + const { object_id, collab_type } = item; + + const { doc } = await getCollabStorage(object_id, collab_type); + + if (rowCallback) { + rowCallback(object_id, doc); + } + } + + // Async fetch collab data and apply to Y.Doc + void (async () => { + const res = await batchFetchCollab(workspaceId, params); + + for (const id of Object.keys(res)) { + const type = params.find((param) => param.object_id === id)?.collab_type; + const data = res[id]; + + if (type === undefined || !data) { + continue; + } + + const { doc } = await getCollabStorage(id, type); + + applyYDoc(doc, data); + } + })(); +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/storage/document.ts b/frontend/appflowy_web_app/src/application/services/js-services/storage/document.ts deleted file mode 100644 index 0c1278d216..0000000000 --- a/frontend/appflowy_web_app/src/application/services/js-services/storage/document.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { YjsEditorKey } from '@/application/collab.type'; -import { openCollabDB } from '@/application/services/js-services/db'; -import { getAuthInfo } from '@/application/services/js-services/storage/token'; - -export async function getDocumentStorage(docId: string) { - const docName = getDocName(docId); - const doc = await openCollabDB(docName); - const localExist = doc.share.has(YjsEditorKey.data_section); - - return { - doc, - localExist, - }; -} - -export function getDocName(docId: string) { - const { uuid } = getAuthInfo() || {}; - - if (!uuid) throw new Error('No user found'); - return `${uuid}_document_${docId}`; -} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/storage/folder.ts b/frontend/appflowy_web_app/src/application/services/js-services/storage/folder.ts deleted file mode 100644 index 8d70df8d0a..0000000000 --- a/frontend/appflowy_web_app/src/application/services/js-services/storage/folder.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { YjsEditorKey } from '@/application/collab.type'; -import { openCollabDB } from '@/application/services/js-services/db'; -import { getAuthInfo } from '@/application/services/js-services/storage/token'; - -export async function getFolderStorage(workspaceId: string) { - const docName = getDocName(workspaceId); - const doc = await openCollabDB(docName); - const localExist = doc.share.has(YjsEditorKey.data_section); - - return { - doc, - localExist, - }; -} - -export function getDocName(workspaceId: string) { - const { uuid } = getAuthInfo() || {}; - - if (!uuid) throw new Error('No user found'); - return `${uuid}_folder_${workspaceId}`; -} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/storage/index.ts b/frontend/appflowy_web_app/src/application/services/js-services/storage/index.ts index d983c71b07..f0b9cab2d6 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/storage/index.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/storage/index.ts @@ -1,2 +1,4 @@ -export * from './token'; -export * from './user'; \ No newline at end of file +export * from './token'; +export * from './user'; +export * from './collab'; +export * from './auth'; diff --git a/frontend/appflowy_web_app/src/application/services/js-services/storage/user.ts b/frontend/appflowy_web_app/src/application/services/js-services/storage/user.ts index 0194bb8e0f..db9626ae8e 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/storage/user.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/storage/user.ts @@ -1,18 +1,36 @@ -import { UserProfile } from '@/application/user.type'; -import { getDB } from '@/application/services/js-services/db'; -import { getAuthInfo } from '@/application/services/js-services/storage/token'; +import { UserProfile, UserWorkspace } from '@/application/user.type'; -const primaryKeyName = 'uid'; +const userKey = 'user'; +const workspaceKey = 'workspace'; export async function getSignInUser(): Promise { - const db = getDB(); - const authInfo = getAuthInfo(); + const userStr = localStorage.getItem(userKey); - return db?.users.get(authInfo?.uuid); + try { + return userStr ? JSON.parse(userStr) : undefined; + } catch (e) { + return undefined; + } } export async function setSignInUser(profile: UserProfile) { - const db = getDB(); + const userStr = JSON.stringify(profile); - return db?.users.put(profile, primaryKeyName); + localStorage.setItem(userKey, userStr); +} + +export async function getUserWorkspace(): Promise { + const str = localStorage.getItem(workspaceKey); + + try { + return str ? JSON.parse(str) : undefined; + } catch (e) { + return undefined; + } +} + +export async function setUserWorkspace(workspace: UserWorkspace) { + const str = JSON.stringify(workspace); + + localStorage.setItem(workspaceKey, str); } diff --git a/frontend/appflowy_web_app/src/application/services/js-services/user.service.ts b/frontend/appflowy_web_app/src/application/services/js-services/user.service.ts index 88e8ba996a..c4853f850d 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/user.service.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/user.service.ts @@ -1,7 +1,14 @@ import { UserService } from '@/application/services/services.type'; -import { UserProfile } from '@/application/user.type'; +import { UserProfile, UserWorkspace } from '@/application/user.type'; import { APIService } from 'src/application/services/js-services/wasm'; -import { getAuthInfo, getSignInUser, invalidToken, setSignInUser } from '@/application/services/js-services/storage'; +import { + getAuthInfo, + getSignInUser, + getUserWorkspace, + invalidToken, + setSignInUser, + setUserWorkspace, +} from '@/application/services/js-services/storage'; import { asyncDataDecorator } from '@/application/services/js-services/decorator'; async function getUser() { @@ -22,10 +29,17 @@ export class JSUserService implements UserService { return Promise.reject('Not authenticated'); } + await this.getUserWorkspace(); + return null!; } async checkUser(): Promise { return (await getSignInUser()) !== undefined; } + + @asyncDataDecorator(getUserWorkspace, setUserWorkspace, APIService.getUserWorkspace) + async getUserWorkspace(): Promise { + return null!; + } } diff --git a/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts b/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts index 48a76d1837..f3fecb1215 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts @@ -1,6 +1,6 @@ import { CollabType } from '@/application/collab.type'; import { ClientAPI } from '@appflowyinc/client-api-wasm'; -import { UserProfile } from '@/application/user.type'; +import { UserProfile, UserWorkspace } from '@/application/user.type'; import { AFCloudConfig } from '@/application/services/services.type'; import { invalidToken, readTokenStr, writeToken } from '@/application/services/js-services/storage'; @@ -77,3 +77,45 @@ export async function getCollab(workspaceId: string, object_id: string, collabTy state, }; } + +export async function batchGetCollab( + workspaceId: string, + params: { + object_id: string; + collab_type: CollabType; + }[] +) { + const res = (await client.batch_get_collab( + workspaceId, + params.map((param) => ({ + object_id: param.object_id, + collab_type: Number(param.collab_type) as 0 | 1 | 2 | 3 | 4 | 5, + })) + )) as unknown as Map; + + const result: Record = {}; + + res.forEach((value, key) => { + result[key] = new Uint8Array(value.doc_state); + }); + return result; +} + +export async function getUserWorkspace(): Promise { + const res = await client.get_user_workspace(); + + return { + visitingWorkspaceId: res.visiting_workspace_id, + workspaces: res.workspaces.map((workspace) => ({ + id: workspace.workspace_id, + name: workspace.workspace_name, + icon: workspace.icon, + owner: { + id: Number(workspace.owner_uid), + name: workspace.owner_name, + }, + type: workspace.workspace_type, + workspaceDatabaseId: workspace.database_storage_id, + })), + }; +} diff --git a/frontend/appflowy_web_app/src/application/services/services.type.ts b/frontend/appflowy_web_app/src/application/services/services.type.ts index d7d3ad069c..1d6a7d45b0 100644 --- a/frontend/appflowy_web_app/src/application/services/services.type.ts +++ b/frontend/appflowy_web_app/src/application/services/services.type.ts @@ -1,5 +1,6 @@ import { YDoc } from '@/application/collab.type'; import { ProviderType, SignUpWithEmailPasswordParams, UserProfile } from '@/application/user.type'; +import * as Y from 'yjs'; export interface AFService { getDeviceID: () => string; @@ -8,6 +9,7 @@ export interface AFService { userService: UserService; documentService: DocumentService; folderService: FolderService; + databaseService: DatabaseService; } export interface AFServiceConfig { @@ -32,6 +34,25 @@ export interface DocumentService { openDocument: (workspaceId: string, docId: string) => Promise; } +export interface DatabaseService { + openDatabase: ( + workspaceId: string, + viewId: string, + rowIds?: string[] + ) => Promise<{ + databaseDoc: YDoc; + rows: Y.Map; + }>; + getDatabase: ( + workspaceId: string, + databaseId: string, + rowIds?: string[] + ) => Promise<{ + databaseDoc: YDoc; + rows: Y.Map; + }>; +} + export interface UserService { getUserProfile: () => Promise; checkUser: () => Promise; diff --git a/frontend/appflowy_web_app/src/application/services/tauri-services/database.service.ts b/frontend/appflowy_web_app/src/application/services/tauri-services/database.service.ts new file mode 100644 index 0000000000..8644914ca7 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/tauri-services/database.service.ts @@ -0,0 +1,29 @@ +import { YDoc } from '@/application/collab.type'; +import { DatabaseService } from '@/application/services/services.type'; +import * as Y from 'yjs'; + +export class TauriDatabaseService implements DatabaseService { + constructor() { + // + } + + async openDatabase( + _workspaceId: string, + _viewId: string + ): Promise<{ + databaseDoc: YDoc; + rows: Y.Map; + }> { + return Promise.reject('Not implemented'); + } + + async getDatabase( + _workspaceId: string, + _databaseId: string + ): Promise<{ + databaseDoc: YDoc; + rows: Y.Map; + }> { + return Promise.reject('Not implemented'); + } +} diff --git a/frontend/appflowy_web_app/src/application/services/tauri-services/document.service.ts b/frontend/appflowy_web_app/src/application/services/tauri-services/document.service.ts index 8bcede6523..9ae2987350 100644 --- a/frontend/appflowy_web_app/src/application/services/tauri-services/document.service.ts +++ b/frontend/appflowy_web_app/src/application/services/tauri-services/document.service.ts @@ -1,5 +1,5 @@ import { DocumentService } from '@/application/services/services.type'; -import Y from 'yjs'; +import * as Y from 'yjs'; export class TauriDocumentService implements DocumentService { async openDocument(_id: string): Promise { diff --git a/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts b/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts index 0f162ba36f..8908c002ee 100644 --- a/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts +++ b/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts @@ -2,11 +2,13 @@ import { AFService, AFServiceConfig, AuthService, + DatabaseService, DocumentService, FolderService, UserService, } from '@/application/services/services.type'; import { TauriAuthService } from '@/application/services/tauri-services/auth.service'; +import { TauriDatabaseService } from '@/application/services/tauri-services/database.service'; import { TauriFolderService } from '@/application/services/tauri-services/folder.service'; import { TauriUserService } from '@/application/services/tauri-services/user.service'; import { TauriDocumentService } from '@/application/services/tauri-services/document.service'; @@ -21,6 +23,8 @@ export class AFClientService implements AFService { folderService: FolderService; + databaseService: DatabaseService; + private deviceId: string = nanoid(8); private clientId: string = 'web'; @@ -41,5 +45,6 @@ export class AFClientService implements AFService { this.userService = new TauriUserService(); this.documentService = new TauriDocumentService(); this.folderService = new TauriFolderService(); + this.databaseService = new TauriDatabaseService(); } } diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/plugins/withYjs.ts b/frontend/appflowy_web_app/src/application/slate-yjs/plugins/withYjs.ts index 1484813ab1..dd3ec137d7 100644 --- a/frontend/appflowy_web_app/src/application/slate-yjs/plugins/withYjs.ts +++ b/frontend/appflowy_web_app/src/application/slate-yjs/plugins/withYjs.ts @@ -54,14 +54,39 @@ export const YjsEditor = { }, }; -export function withYjs(editor: T, doc: Y.Doc): T & YjsEditor { +export function withYjs( + editor: T, + doc: Y.Doc, + { + localOrigin, + includeRoot = true, + }: { + localOrigin: CollabOrigin; + includeRoot?: boolean; + } +): T & YjsEditor { const e = editor as T & YjsEditor; const { apply, onChange } = e; e.sharedRoot = doc.getMap(YjsEditorKey.data_section) as YSharedRoot; + + const initializeDocumentContent = () => { + const content = yDocToSlateContent(doc, includeRoot); + + if (!content) { + return; + } + + e.children = content.children; + Editor.normalize(editor, { force: true }); + }; + e.applyRemoteEvents = (events: Array>, _: Transaction) => { YjsEditor.flushLocalChanges(e); + // TODO: handle remote events + // This is a temporary implementation to apply remote events to slate + initializeDocumentContent(); Editor.withoutNormalizing(editor, () => { events.forEach((event) => { translateYjsEvent(e.sharedRoot, editor, event).forEach((op) => { @@ -73,11 +98,9 @@ export function withYjs(editor: T, doc: Y.Doc): T & YjsEditor }; const handleYEvents = (events: Array>, transaction: Transaction) => { - if (transaction.origin === CollabOrigin.Local) { - return; + if (transaction.origin === CollabOrigin.Remote) { + YjsEditor.applyRemoteEvents(e, events, transaction); } - - YjsEditor.applyRemoteEvents(e, events, transaction); }; e.connect = () => { @@ -85,17 +108,8 @@ export function withYjs(editor: T, doc: Y.Doc): T & YjsEditor throw new Error('Already connected'); } - const content = yDocToSlateContent(doc, true); - - if (!content) { - return; - } - - console.log(content); - + initializeDocumentContent(); e.sharedRoot.observeDeep(handleYEvents); - e.children = content.children; - Editor.normalize(editor, { force: true }); connectSet.add(e); }; @@ -123,7 +137,7 @@ export function withYjs(editor: T, doc: Y.Doc): T & YjsEditor changes.forEach((change) => { applySlateOp(doc, { children: change.slateContent }, change.op); }); - }, CollabOrigin.Local); + }, localOrigin); }; e.apply = (op) => { diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/applySlateOpts.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/applySlateOpts.ts index edb14cfa0a..5a2fd6670c 100644 --- a/frontend/appflowy_web_app/src/application/slate-yjs/utils/applySlateOpts.ts +++ b/frontend/appflowy_web_app/src/application/slate-yjs/utils/applySlateOpts.ts @@ -1,6 +1,6 @@ import { Operation, Node } from 'slate'; -import Y from 'yjs'; +import * as Y from 'yjs'; -export function applySlateOp (ydoc: Y.Doc, slateRoot: Node, op: Operation) { +export function applySlateOp(ydoc: Y.Doc, slateRoot: Node, op: Operation) { console.log('applySlateOp', op); -} \ No newline at end of file +} diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/arrayEvent.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/arrayEvent.ts index 8be1dbc297..0565e8de92 100644 --- a/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/arrayEvent.ts +++ b/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/arrayEvent.ts @@ -3,10 +3,9 @@ import * as Y from 'yjs'; import { Editor, Operation } from 'slate'; export function translateYArrayEvent( - sharedRoot: YSharedRoot, - editor: Editor, - event: Y.YEvent> + _sharedRoot: YSharedRoot, + _editor: Editor, + _event: Y.YEvent> ): Operation[] { - console.log('translateYArrayEvent', sharedRoot, editor, event); return []; } diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/index.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/index.ts index 10af76fcde..c3f3bfd903 100644 --- a/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/index.ts +++ b/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/index.ts @@ -13,7 +13,6 @@ import { translateYTextEvent } from 'src/application/slate-yjs/utils/translateYj * @param op */ export function translateYjsEvent(sharedRoot: YSharedRoot, editor: Editor, event: Y.YEvent): Operation[] { - console.log('translateYjsEvent', event); if (event instanceof Y.YMapEvent) { return translateYMapEvent(sharedRoot, editor, event); } diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/mapEvent.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/mapEvent.ts index fd50bb6df8..cab9831833 100644 --- a/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/mapEvent.ts +++ b/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/mapEvent.ts @@ -3,10 +3,9 @@ import * as Y from 'yjs'; import { Editor, Operation } from 'slate'; export function translateYMapEvent( - sharedRoot: YSharedRoot, - editor: Editor, - event: Y.YEvent> + _sharedRoot: YSharedRoot, + _editor: Editor, + _event: Y.YEvent> ): Operation[] { - console.log('translateYMapEvent', sharedRoot, editor, event); return []; } diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/textEvent.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/textEvent.ts index 3dce8a3d59..2fc6deca73 100644 --- a/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/textEvent.ts +++ b/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/textEvent.ts @@ -1,8 +1,7 @@ -import { YSharedRoot } from '@/application/document.type'; +import { YSharedRoot } from '@/application/collab.type'; import * as Y from 'yjs'; import { Editor, Operation } from 'slate'; -export function translateYTextEvent(sharedRoot: YSharedRoot, editor: Editor, event: Y.YEvent): Operation[] { - console.log('translateYTextEvent', sharedRoot, editor, event); +export function translateYTextEvent(_sharedRoot: YSharedRoot, _editor: Editor, _event: Y.YEvent): Operation[] { return []; } diff --git a/frontend/appflowy_web_app/src/application/user.type.ts b/frontend/appflowy_web_app/src/application/user.type.ts index be64d574b4..e2c3bcdb43 100644 --- a/frontend/appflowy_web_app/src/application/user.type.ts +++ b/frontend/appflowy_web_app/src/application/user.type.ts @@ -18,6 +18,11 @@ export interface UserProfile { workspaceId?: string; } +export interface UserWorkspace { + visitingWorkspaceId: string; + workspaces: Workspace[]; +} + export interface Workspace { id: string; name: string; @@ -26,6 +31,8 @@ export interface Workspace { id: number; name: string; }; + type: number; + workspaceDatabaseId: string; } export interface SignUpWithEmailPasswordParams { diff --git a/frontend/appflowy_web_app/src/application/ydoc/apply/__tests__/document.test.ts b/frontend/appflowy_web_app/src/application/ydoc/apply/__tests__/document.test.ts index 512c28ae6a..8a332f4b60 100644 --- a/frontend/appflowy_web_app/src/application/ydoc/apply/__tests__/document.test.ts +++ b/frontend/appflowy_web_app/src/application/ydoc/apply/__tests__/document.test.ts @@ -1,5 +1,5 @@ import { YjsEditorKey } from '@/application/collab.type'; -import { applyDocument } from '@/application/ydoc/apply'; +import { applyYDoc } from '@/application/ydoc/apply'; import * as Y from 'yjs'; import * as docJson from '../../../../../cypress/fixtures/simple_doc.json'; @@ -11,7 +11,7 @@ describe('apply document', () => { data.set(YjsEditorKey.document, document); const state = new Uint8Array(docJson.data.doc_state); - applyDocument(collab, state); + applyYDoc(collab, state); }); }); diff --git a/frontend/appflowy_web_app/src/application/ydoc/apply/document.ts b/frontend/appflowy_web_app/src/application/ydoc/apply/document.ts deleted file mode 100644 index 60d02d0450..0000000000 --- a/frontend/appflowy_web_app/src/application/ydoc/apply/document.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { CollabOrigin } from '@/application/collab.type'; -import * as Y from 'yjs'; - -/** - * Apply doc state from server to client - * Note: origin is always remote - * @param doc local Y.Doc - * @param state state from server - */ -export function applyDocument(doc: Y.Doc, state: Uint8Array) { - Y.transact( - doc, - () => { - Y.applyUpdate(doc, state); - }, - CollabOrigin.Remote - ); -} diff --git a/frontend/appflowy_web_app/src/application/ydoc/apply/index.ts b/frontend/appflowy_web_app/src/application/ydoc/apply/index.ts index 8147823035..b19cb43328 100644 --- a/frontend/appflowy_web_app/src/application/ydoc/apply/index.ts +++ b/frontend/appflowy_web_app/src/application/ydoc/apply/index.ts @@ -1 +1,18 @@ -export * from 'src/application/ydoc/apply/document'; +import { CollabOrigin } from '@/application/collab.type'; +import * as Y from 'yjs'; + +/** + * Apply doc state from server to client + * Note: origin is always remote + * @param doc local Y.Doc + * @param state state from server + */ +export function applyYDoc(doc: Y.Doc, state: Uint8Array) { + Y.transact( + doc, + () => { + Y.applyUpdate(doc, state); + }, + CollabOrigin.Remote + ); +} diff --git a/frontend/appflowy_web_app/src/assets/add.svg b/frontend/appflowy_web_app/src/assets/add.svg deleted file mode 100644 index 049be05cec..0000000000 --- a/frontend/appflowy_web_app/src/assets/add.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/align-center.svg b/frontend/appflowy_web_app/src/assets/align-center.svg deleted file mode 100644 index f4f4999514..0000000000 --- a/frontend/appflowy_web_app/src/assets/align-center.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/appflowy_web_app/src/assets/align-left.svg b/frontend/appflowy_web_app/src/assets/align-left.svg deleted file mode 100644 index 23957285c7..0000000000 --- a/frontend/appflowy_web_app/src/assets/align-left.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/appflowy_web_app/src/assets/align-right.svg b/frontend/appflowy_web_app/src/assets/align-right.svg deleted file mode 100644 index bca2d14fc7..0000000000 --- a/frontend/appflowy_web_app/src/assets/align-right.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/appflowy_web_app/src/assets/arrow-left.svg b/frontend/appflowy_web_app/src/assets/arrow-left.svg deleted file mode 100644 index e4ab9068be..0000000000 --- a/frontend/appflowy_web_app/src/assets/arrow-left.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/arrow-right.svg b/frontend/appflowy_web_app/src/assets/arrow-right.svg deleted file mode 100644 index dc40ae52a6..0000000000 --- a/frontend/appflowy_web_app/src/assets/arrow-right.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/board.svg b/frontend/appflowy_web_app/src/assets/board.svg deleted file mode 100644 index 0bb0e3fabe..0000000000 --- a/frontend/appflowy_web_app/src/assets/board.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/bold.svg b/frontend/appflowy_web_app/src/assets/bold.svg deleted file mode 100644 index 878b6329b3..0000000000 --- a/frontend/appflowy_web_app/src/assets/bold.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/clock_alarm.svg b/frontend/appflowy_web_app/src/assets/clock_alarm.svg deleted file mode 100644 index 33a5585ceb..0000000000 --- a/frontend/appflowy_web_app/src/assets/clock_alarm.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/close.svg b/frontend/appflowy_web_app/src/assets/close.svg deleted file mode 100644 index b519b419c0..0000000000 --- a/frontend/appflowy_web_app/src/assets/close.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/copy.svg b/frontend/appflowy_web_app/src/assets/copy.svg deleted file mode 100644 index e21e6cb082..0000000000 --- a/frontend/appflowy_web_app/src/assets/copy.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/dark-logo.svg b/frontend/appflowy_web_app/src/assets/dark-logo.svg deleted file mode 100644 index 80d8c4132e..0000000000 --- a/frontend/appflowy_web_app/src/assets/dark-logo.svg +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/database/checkbox-check.svg b/frontend/appflowy_web_app/src/assets/database/checkbox-check.svg deleted file mode 100644 index d2fc54c4b7..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/checkbox-check.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/database/checkbox-uncheck.svg b/frontend/appflowy_web_app/src/assets/database/checkbox-uncheck.svg deleted file mode 100644 index 3b3e17dd31..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/checkbox-uncheck.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-attach.svg b/frontend/appflowy_web_app/src/assets/database/field-type-attach.svg deleted file mode 100644 index f00f5c7aa2..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-attach.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-checkbox.svg b/frontend/appflowy_web_app/src/assets/database/field-type-checkbox.svg deleted file mode 100644 index 37f52c47ed..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-checkbox.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-checklist.svg b/frontend/appflowy_web_app/src/assets/database/field-type-checklist.svg deleted file mode 100644 index 3a88d236a1..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-checklist.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-date.svg b/frontend/appflowy_web_app/src/assets/database/field-type-date.svg deleted file mode 100644 index 78243f1e75..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-date.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-last-edited-time.svg b/frontend/appflowy_web_app/src/assets/database/field-type-last-edited-time.svg deleted file mode 100644 index 634af3e361..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-last-edited-time.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-multi-select.svg b/frontend/appflowy_web_app/src/assets/database/field-type-multi-select.svg deleted file mode 100644 index 97a2e9c434..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-multi-select.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-number.svg b/frontend/appflowy_web_app/src/assets/database/field-type-number.svg deleted file mode 100644 index 9d8b98d10d..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-number.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-person.svg b/frontend/appflowy_web_app/src/assets/database/field-type-person.svg deleted file mode 100644 index 2fc04be065..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-person.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-relation.svg b/frontend/appflowy_web_app/src/assets/database/field-type-relation.svg deleted file mode 100644 index f82a41d226..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-relation.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-single-select.svg b/frontend/appflowy_web_app/src/assets/database/field-type-single-select.svg deleted file mode 100644 index 8ccbc9a2e3..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-single-select.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-text.svg b/frontend/appflowy_web_app/src/assets/database/field-type-text.svg deleted file mode 100644 index 7befa5080f..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-text.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-url.svg b/frontend/appflowy_web_app/src/assets/database/field-type-url.svg deleted file mode 100644 index f00f5c7aa2..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-url.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/date.svg b/frontend/appflowy_web_app/src/assets/date.svg deleted file mode 100644 index 78243f1e75..0000000000 --- a/frontend/appflowy_web_app/src/assets/date.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/delete.svg b/frontend/appflowy_web_app/src/assets/delete.svg deleted file mode 100644 index 9e51636798..0000000000 --- a/frontend/appflowy_web_app/src/assets/delete.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/details.svg b/frontend/appflowy_web_app/src/assets/details.svg deleted file mode 100644 index 22c6830916..0000000000 --- a/frontend/appflowy_web_app/src/assets/details.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/appflowy_web_app/src/assets/document.svg b/frontend/appflowy_web_app/src/assets/document.svg deleted file mode 100644 index b00e1cfb38..0000000000 --- a/frontend/appflowy_web_app/src/assets/document.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/drag.svg b/frontend/appflowy_web_app/src/assets/drag.svg deleted file mode 100644 index 627c959f9f..0000000000 --- a/frontend/appflowy_web_app/src/assets/drag.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/dropdown.svg b/frontend/appflowy_web_app/src/assets/dropdown.svg deleted file mode 100644 index 95e4964b53..0000000000 --- a/frontend/appflowy_web_app/src/assets/dropdown.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/edit.svg b/frontend/appflowy_web_app/src/assets/edit.svg deleted file mode 100644 index ae93287114..0000000000 --- a/frontend/appflowy_web_app/src/assets/edit.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/eye_close.svg b/frontend/appflowy_web_app/src/assets/eye_close.svg deleted file mode 100644 index 116c715ca8..0000000000 --- a/frontend/appflowy_web_app/src/assets/eye_close.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/eye_open.svg b/frontend/appflowy_web_app/src/assets/eye_open.svg deleted file mode 100644 index fa3017c04d..0000000000 --- a/frontend/appflowy_web_app/src/assets/eye_open.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/grid.svg b/frontend/appflowy_web_app/src/assets/grid.svg deleted file mode 100644 index c397af8130..0000000000 --- a/frontend/appflowy_web_app/src/assets/grid.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/h1.svg b/frontend/appflowy_web_app/src/assets/h1.svg deleted file mode 100644 index b33bd52135..0000000000 --- a/frontend/appflowy_web_app/src/assets/h1.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/h2.svg b/frontend/appflowy_web_app/src/assets/h2.svg deleted file mode 100644 index 7449c57391..0000000000 --- a/frontend/appflowy_web_app/src/assets/h2.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/h3.svg b/frontend/appflowy_web_app/src/assets/h3.svg deleted file mode 100644 index 0976945974..0000000000 --- a/frontend/appflowy_web_app/src/assets/h3.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/hide-menu.svg b/frontend/appflowy_web_app/src/assets/hide-menu.svg deleted file mode 100644 index ce88af8ea7..0000000000 --- a/frontend/appflowy_web_app/src/assets/hide-menu.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/hide.svg b/frontend/appflowy_web_app/src/assets/hide.svg deleted file mode 100644 index 22001ef65d..0000000000 --- a/frontend/appflowy_web_app/src/assets/hide.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/image.svg b/frontend/appflowy_web_app/src/assets/image.svg deleted file mode 100644 index 0739605066..0000000000 --- a/frontend/appflowy_web_app/src/assets/image.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/appflowy_web_app/src/assets/images/default_cover.jpg b/frontend/appflowy_web_app/src/assets/images/default_cover.jpg deleted file mode 100644 index aeaa6a0f29..0000000000 Binary files a/frontend/appflowy_web_app/src/assets/images/default_cover.jpg and /dev/null differ diff --git a/frontend/appflowy_web_app/src/assets/inline-code.svg b/frontend/appflowy_web_app/src/assets/inline-code.svg deleted file mode 100644 index 3585603096..0000000000 --- a/frontend/appflowy_web_app/src/assets/inline-code.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/italic.svg b/frontend/appflowy_web_app/src/assets/italic.svg deleted file mode 100644 index b295c230f0..0000000000 --- a/frontend/appflowy_web_app/src/assets/italic.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/left.svg b/frontend/appflowy_web_app/src/assets/left.svg deleted file mode 100644 index 0f771a3858..0000000000 --- a/frontend/appflowy_web_app/src/assets/left.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/appflowy_web_app/src/assets/light-logo.svg b/frontend/appflowy_web_app/src/assets/light-logo.svg deleted file mode 100644 index f5cd761ba7..0000000000 --- a/frontend/appflowy_web_app/src/assets/light-logo.svg +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/link.svg b/frontend/appflowy_web_app/src/assets/link.svg deleted file mode 100644 index 5fbcc8d787..0000000000 --- a/frontend/appflowy_web_app/src/assets/link.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/list-dropdown.svg b/frontend/appflowy_web_app/src/assets/list-dropdown.svg deleted file mode 100644 index 4a8424c5f8..0000000000 --- a/frontend/appflowy_web_app/src/assets/list-dropdown.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/list.svg b/frontend/appflowy_web_app/src/assets/list.svg deleted file mode 100644 index 97a2e9c434..0000000000 --- a/frontend/appflowy_web_app/src/assets/list.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/mention.svg b/frontend/appflowy_web_app/src/assets/mention.svg deleted file mode 100644 index b98318132c..0000000000 --- a/frontend/appflowy_web_app/src/assets/mention.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/more.svg b/frontend/appflowy_web_app/src/assets/more.svg deleted file mode 100644 index b191e64a10..0000000000 --- a/frontend/appflowy_web_app/src/assets/more.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/numbers.svg b/frontend/appflowy_web_app/src/assets/numbers.svg deleted file mode 100644 index 9d8b98d10d..0000000000 --- a/frontend/appflowy_web_app/src/assets/numbers.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/open.svg b/frontend/appflowy_web_app/src/assets/open.svg deleted file mode 100644 index b443c8b993..0000000000 --- a/frontend/appflowy_web_app/src/assets/open.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/quote.svg b/frontend/appflowy_web_app/src/assets/quote.svg deleted file mode 100644 index 57839231ff..0000000000 --- a/frontend/appflowy_web_app/src/assets/quote.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/react.svg b/frontend/appflowy_web_app/src/assets/react.svg deleted file mode 100644 index 6c87de9bb3..0000000000 --- a/frontend/appflowy_web_app/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/right.svg b/frontend/appflowy_web_app/src/assets/right.svg deleted file mode 100644 index 7d738f4e69..0000000000 --- a/frontend/appflowy_web_app/src/assets/right.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/appflowy_web_app/src/assets/search.svg b/frontend/appflowy_web_app/src/assets/search.svg deleted file mode 100644 index a8a92df509..0000000000 --- a/frontend/appflowy_web_app/src/assets/search.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/select-check.svg b/frontend/appflowy_web_app/src/assets/select-check.svg deleted file mode 100644 index 05caec861a..0000000000 --- a/frontend/appflowy_web_app/src/assets/select-check.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/settings.svg b/frontend/appflowy_web_app/src/assets/settings.svg deleted file mode 100644 index 92140a3c23..0000000000 --- a/frontend/appflowy_web_app/src/assets/settings.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/settings/account.svg b/frontend/appflowy_web_app/src/assets/settings/account.svg deleted file mode 100644 index fddfca7575..0000000000 --- a/frontend/appflowy_web_app/src/assets/settings/account.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/settings/check_circle.svg b/frontend/appflowy_web_app/src/assets/settings/check_circle.svg deleted file mode 100644 index c6fa56067b..0000000000 --- a/frontend/appflowy_web_app/src/assets/settings/check_circle.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/settings/dark.png b/frontend/appflowy_web_app/src/assets/settings/dark.png deleted file mode 100644 index 15a2db5eb8..0000000000 Binary files a/frontend/appflowy_web_app/src/assets/settings/dark.png and /dev/null differ diff --git a/frontend/appflowy_web_app/src/assets/settings/light.png b/frontend/appflowy_web_app/src/assets/settings/light.png deleted file mode 100644 index 09b2d9c475..0000000000 Binary files a/frontend/appflowy_web_app/src/assets/settings/light.png and /dev/null differ diff --git a/frontend/appflowy_web_app/src/assets/settings/workplace.svg b/frontend/appflowy_web_app/src/assets/settings/workplace.svg deleted file mode 100644 index 2076ea3e2c..0000000000 --- a/frontend/appflowy_web_app/src/assets/settings/workplace.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/show-menu.svg b/frontend/appflowy_web_app/src/assets/show-menu.svg deleted file mode 100644 index 8baf55bffd..0000000000 --- a/frontend/appflowy_web_app/src/assets/show-menu.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/sort.svg b/frontend/appflowy_web_app/src/assets/sort.svg deleted file mode 100644 index e3b6a49a56..0000000000 --- a/frontend/appflowy_web_app/src/assets/sort.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/strikethrough.svg b/frontend/appflowy_web_app/src/assets/strikethrough.svg deleted file mode 100644 index c118422a15..0000000000 --- a/frontend/appflowy_web_app/src/assets/strikethrough.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/text.svg b/frontend/appflowy_web_app/src/assets/text.svg deleted file mode 100644 index 7befa5080f..0000000000 --- a/frontend/appflowy_web_app/src/assets/text.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/todo-list.svg b/frontend/appflowy_web_app/src/assets/todo-list.svg deleted file mode 100644 index 37f52c47ed..0000000000 --- a/frontend/appflowy_web_app/src/assets/todo-list.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/underline.svg b/frontend/appflowy_web_app/src/assets/underline.svg deleted file mode 100644 index f5d53f0ec2..0000000000 --- a/frontend/appflowy_web_app/src/assets/underline.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/up.svg b/frontend/appflowy_web_app/src/assets/up.svg deleted file mode 100644 index bd8f3067d3..0000000000 --- a/frontend/appflowy_web_app/src/assets/up.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/components/_shared/context-provider/IdProvider.tsx b/frontend/appflowy_web_app/src/components/_shared/context-provider/IdProvider.tsx index 789642420d..15682f1c82 100644 --- a/frontend/appflowy_web_app/src/components/_shared/context-provider/IdProvider.tsx +++ b/frontend/appflowy_web_app/src/components/_shared/context-provider/IdProvider.tsx @@ -1,4 +1,3 @@ -import { CollabType } from '@/application/collab.type'; import { useContext, createContext } from 'react'; export const IdContext = createContext(null); @@ -6,7 +5,6 @@ export const IdContext = createContext(null); interface IdProviderProps { workspaceId: string; objectId: string; - collabType: CollabType; } export const IdProvider = ({ children, ...props }: IdProviderProps & { children: React.ReactNode }) => { diff --git a/frontend/appflowy_web_app/src/components/_shared/not-found/RecordNotFound.tsx b/frontend/appflowy_web_app/src/components/_shared/not-found/RecordNotFound.tsx index 00441e5281..9216a92c69 100644 --- a/frontend/appflowy_web_app/src/components/_shared/not-found/RecordNotFound.tsx +++ b/frontend/appflowy_web_app/src/components/_shared/not-found/RecordNotFound.tsx @@ -10,7 +10,7 @@ export function RecordNotFound({ open, workspaceId }: { workspaceId: string; ope Oops.. something went wrong - Sorry, the document you are looking for does not exist. + Sorry, the page you are looking for does not exist. diff --git a/frontend/appflowy_web_app/src/components/_shared/page/usePageInfo.tsx b/frontend/appflowy_web_app/src/components/_shared/page/usePageInfo.tsx index 4fec272b79..be0dc61dc7 100644 --- a/frontend/appflowy_web_app/src/components/_shared/page/usePageInfo.tsx +++ b/frontend/appflowy_web_app/src/components/_shared/page/usePageInfo.tsx @@ -1,10 +1,10 @@ import { ViewLayout, YjsFolderKey, YView } from '@/application/collab.type'; import { useViewSelector } from '@/application/folder-yjs'; import React, { useMemo } from 'react'; -import { ReactComponent as DocumentSvg } from '@/assets/document.svg'; -import { ReactComponent as GridSvg } from '@/assets/grid.svg'; -import { ReactComponent as BoardSvg } from '@/assets/board.svg'; -import { ReactComponent as CalendarSvg } from '@/assets/date.svg'; +import { ReactComponent as DocumentSvg } from '$icons/16x/document.svg'; +import { ReactComponent as GridSvg } from '$icons/16x/grid.svg'; +import { ReactComponent as BoardSvg } from '$icons/16x/board.svg'; +import { ReactComponent as CalendarSvg } from '$icons/16x/date.svg'; import { useTranslation } from 'react-i18next'; export function usePageInfo(id: string) { diff --git a/frontend/appflowy_web_app/src/components/_shared/popover/Popover.tsx b/frontend/appflowy_web_app/src/components/_shared/popover/Popover.tsx new file mode 100644 index 0000000000..f91ac8284e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/popover/Popover.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Popover as PopoverComponent, PopoverProps as PopoverComponentProps } from '@mui/material'; + +const defaultProps: Partial = { + keepMounted: false, + disableRestoreFocus: true, + anchorOrigin: { + vertical: 'bottom', + horizontal: 'left', + }, +}; + +export function Popover({ children, ...props }: PopoverComponentProps) { + return ( + + {children} + + ); +} diff --git a/frontend/appflowy_web_app/src/components/_shared/popover/RichTooltip.tsx b/frontend/appflowy_web_app/src/components/_shared/popover/RichTooltip.tsx new file mode 100644 index 0000000000..437b08eaf5 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/popover/RichTooltip.tsx @@ -0,0 +1,63 @@ +import { Box, ClickAwayListener, Fade, Paper, Popper, PopperPlacementType } from '@mui/material'; +import React, { ReactElement, useEffect } from 'react'; + +interface Props { + content: ReactElement; + children: ReactElement; + open: boolean; + onClose: () => void; + placement?: PopperPlacementType; +} + +export const RichTooltip = ({ placement = 'top', open, onClose, content, children }: Props) => { + const [childNode, setChildNode] = React.useState(null); + const [, setTransitioning] = React.useState(false); + + useEffect(() => { + if (open) { + setTransitioning(true); + } + }, [open]); + return ( + <> + {React.cloneElement(children, { ...children.props, ref: setChildNode })} + + {({ TransitionProps }) => ( + { + setTransitioning(false); + }} + > + + + + {content} + + + + + )} + + + ); +}; + +export default RichTooltip; diff --git a/frontend/appflowy_web_app/src/components/_shared/popover/index.ts b/frontend/appflowy_web_app/src/components/_shared/popover/index.ts new file mode 100644 index 0000000000..f1c61c79c4 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/popover/index.ts @@ -0,0 +1,2 @@ +export * from './Popover'; +export * from './RichTooltip'; diff --git a/frontend/appflowy_web_app/src/components/_shared/progress/LinearProgressWithLabel.tsx b/frontend/appflowy_web_app/src/components/_shared/progress/LinearProgressWithLabel.tsx new file mode 100644 index 0000000000..f12cfe4c01 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/progress/LinearProgressWithLabel.tsx @@ -0,0 +1,47 @@ +import React, { useMemo } from 'react'; + +function LinearProgressWithLabel({ + value, + count, + selectedCount, +}: { + value: number; + count: number; + selectedCount: number; +}) { + const result = useMemo(() => `${Math.round(value * 100)}%`, [value]); + + const options = useMemo(() => { + return Array.from({ length: count }, (_, i) => ({ + id: i, + checked: i < selectedCount, + })); + }, [count, selectedCount]); + + const isSplit = count < 6; + + return ( +
+
+ {options.map((option) => ( + + ))} +
+
{result}
+
+ ); +} + +export default LinearProgressWithLabel; diff --git a/frontend/appflowy_web_app/src/components/_shared/scroller/AFScroller.tsx b/frontend/appflowy_web_app/src/components/_shared/scroller/AFScroller.tsx index 0527b6cc26..9d07c8b908 100644 --- a/frontend/appflowy_web_app/src/components/_shared/scroller/AFScroller.tsx +++ b/frontend/appflowy_web_app/src/components/_shared/scroller/AFScroller.tsx @@ -7,49 +7,67 @@ export interface AFScrollerProps { overflowYHidden?: boolean; className?: string; style?: React.CSSProperties; + onScroll?: (e: React.UIEvent) => void; } -export const AFScroller = ({ style, children, overflowXHidden, overflowYHidden, className }: AFScrollerProps) => { - return ( -
} - renderThumbVertical={(props) =>
} - {...(overflowXHidden && { - renderTrackHorizontal: (props) => ( + +export const AFScroller = React.forwardRef( + ({ onScroll, style, children, overflowXHidden, overflowYHidden, className }: AFScrollerProps, ref) => { + return ( + { + if (!el) return; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const scrollEl = el.container?.firstChild as HTMLElement; + + if (!scrollEl) return; + if (typeof ref === 'function') { + ref(scrollEl); + } else if (ref) { + ref.current = scrollEl; + } + }} + renderThumbHorizontal={(props) =>
} + renderThumbVertical={(props) =>
} + {...(overflowXHidden && { + renderTrackHorizontal: (props) => ( +
+ ), + })} + {...(overflowYHidden && { + renderTrackVertical: (props) => ( +
+ ), + })} + style={style} + renderView={(props) => (
- ), - })} - {...(overflowYHidden && { - renderTrackVertical: (props) => ( -
- ), - })} - style={style} - renderView={(props) => ( -
- )} - > - {children} - - ); -}; + )} + > + {children} + + ); + } +); diff --git a/frontend/appflowy_web_app/src/components/_shared/tag/Tag.tsx b/frontend/appflowy_web_app/src/components/_shared/tag/Tag.tsx new file mode 100644 index 0000000000..fbd9ac486d --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/tag/Tag.tsx @@ -0,0 +1,29 @@ +import { FC, useMemo } from 'react'; + +export interface TagProps { + color?: string; + label?: string; + size?: 'small' | 'medium'; +} + +export const Tag: FC = ({ color, size = 'small', label }) => { + const className = useMemo(() => { + const classList = ['rounded-md', 'font-medium', 'text-xs', 'leading-[18px]']; + + if (color) classList.push(`text-text-title`); + if (size === 'small') classList.push('text-xs', 'px-2', 'py-[2px]'); + if (size === 'medium') classList.push('text-sm', 'px-3', 'py-1'); + return classList.join(' '); + }, [color, size]); + + return ( +
+ {label} +
+ ); +}; diff --git a/frontend/appflowy_web_app/src/components/_shared/tag/index.ts b/frontend/appflowy_web_app/src/components/_shared/tag/index.ts new file mode 100644 index 0000000000..9790fcbf11 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/tag/index.ts @@ -0,0 +1 @@ +export * from './Tag'; diff --git a/frontend/appflowy_web_app/src/components/app/App.tsx b/frontend/appflowy_web_app/src/components/app/App.tsx index 1504c99f07..b2ee81eb20 100644 --- a/frontend/appflowy_web_app/src/components/app/App.tsx +++ b/frontend/appflowy_web_app/src/components/app/App.tsx @@ -10,7 +10,7 @@ const AppMain = withAppWrapper(() => { }> } /> - } /> + } /> } /> diff --git a/frontend/appflowy_web_app/src/components/app/AppTheme.tsx b/frontend/appflowy_web_app/src/components/app/AppTheme.tsx index 2d00bec2a3..179b371125 100644 --- a/frontend/appflowy_web_app/src/components/app/AppTheme.tsx +++ b/frontend/appflowy_web_app/src/components/app/AppTheme.tsx @@ -72,6 +72,7 @@ function AppTheme({ children }: { children: React.ReactNode }) { styleOverrides: { root: { backgroundImage: 'none', + boxShadow: 'var(--shadow)', }, }, }, @@ -100,6 +101,14 @@ function AppTheme({ children }: { children: React.ReactNode }) { }, }, MuiInputBase: { + defaultProps: { + sx: { + '&.Mui-disabled, .Mui-disabled': { + color: 'var(--text-caption)', + WebkitTextFillColor: 'var(--text-caption) !important', + }, + }, + }, styleOverrides: { input: { backgroundColor: 'transparent !important', diff --git a/frontend/appflowy_web_app/src/components/auth/Welcome.cy.tsx b/frontend/appflowy_web_app/src/components/auth/Welcome.cy.tsx index f0f83d366a..768cf3587b 100644 --- a/frontend/appflowy_web_app/src/components/auth/Welcome.cy.tsx +++ b/frontend/appflowy_web_app/src/components/auth/Welcome.cy.tsx @@ -9,7 +9,7 @@ describe('', () => { it('renders', () => { const AppWrapper = withAppWrapper(Welcome); - + cy.mount(); }); @@ -29,6 +29,7 @@ describe('', () => { cy.wait('@loginSuccess'); cy.wait('@verifyToken'); cy.wait('@getUserProfile'); + cy.wait('@getUserWorkspace'); cy.get('@dialog').should('not.exist'); }); }); diff --git a/frontend/appflowy_web_app/src/components/auth/auth.hooks.ts b/frontend/appflowy_web_app/src/components/auth/auth.hooks.ts index cb972283bf..affe339c81 100644 --- a/frontend/appflowy_web_app/src/components/auth/auth.hooks.ts +++ b/frontend/appflowy_web_app/src/components/auth/auth.hooks.ts @@ -56,6 +56,7 @@ export const useAuth = () => { throw new Error('Failed to check user'); } + console.log('userProfile', userProfile); await setUser(userProfile); return userProfile; diff --git a/frontend/appflowy_web_app/src/components/database/Database.tsx b/frontend/appflowy_web_app/src/components/database/Database.tsx new file mode 100644 index 0000000000..fe35a99cc6 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/Database.tsx @@ -0,0 +1,87 @@ +import { YDoc, YjsEditorKey } from '@/application/collab.type'; +import { useId } from '@/components/_shared/context-provider/IdProvider'; +import RecordNotFound from '@/components/_shared/not-found/RecordNotFound'; +import { AFConfigContext } from '@/components/app/AppConfig'; +import DatabaseViews from '@/components/database/DatabaseViews'; +import { DatabaseContextProvider } from '@/components/database/DatabaseContext'; +import { Log } from '@/utils/log'; +import CircularProgress from '@mui/material/CircularProgress'; +import React, { memo, useCallback, useContext, useEffect, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import * as Y from 'yjs'; + +export const Database = memo(() => { + const { objectId, workspaceId } = useId() || {}; + const [search, setSearch] = useSearchParams(); + + const viewId = search.get('v'); + const [doc, setDoc] = useState(null); + const [rows, setRows] = useState | null>(null); // Map(false); + const databaseService = useContext(AFConfigContext)?.service?.databaseService; + + const handleOpenDatabase = useCallback(async () => { + if (!databaseService || !workspaceId || !objectId) return; + + try { + setDoc(null); + const { databaseDoc, rows } = await databaseService.openDatabase(workspaceId, objectId); + + console.log('databaseDoc', databaseDoc.getMap(YjsEditorKey.data_section).toJSON()); + console.log('rows', rows); + + setDoc(databaseDoc); + setRows(rows); + } catch (e) { + Log.error(e); + setNotFound(true); + } + }, [databaseService, workspaceId, objectId]); + + useEffect(() => { + setNotFound(false); + void handleOpenDatabase(); + }, [handleOpenDatabase]); + + const handleChangeView = useCallback( + (viewId: string) => { + setSearch({ v: viewId }); + }, + [setSearch] + ); + + const navigateToRow = useCallback( + (rowId: string) => { + setSearch({ r: rowId }); + }, + [setSearch] + ); + + if (notFound || !objectId) { + return ; + } + + if (!rows || !doc) { + return ( +
+ +
+ ); + } + + return ( +
+ + + +
+ ); +}); + +export default Database; diff --git a/frontend/appflowy_web_app/src/components/database/DatabaseContext.tsx b/frontend/appflowy_web_app/src/components/database/DatabaseContext.tsx new file mode 100644 index 0000000000..8adc87d4e6 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/DatabaseContext.tsx @@ -0,0 +1,10 @@ +import { DatabaseContext, DatabaseContextState } from '@/application/database-yjs'; + +export const DatabaseContextProvider = ({ + children, + ...props +}: DatabaseContextState & { + children: React.ReactNode; +}) => { + return {children}; +}; diff --git a/frontend/appflowy_web_app/src/components/database/DatabaseRow.tsx b/frontend/appflowy_web_app/src/components/database/DatabaseRow.tsx new file mode 100644 index 0000000000..6928cc2992 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/DatabaseRow.tsx @@ -0,0 +1,79 @@ +import { YDoc, YjsEditorKey } from '@/application/collab.type'; +import { useId } from '@/components/_shared/context-provider/IdProvider'; +import { AFConfigContext } from '@/components/app/AppConfig'; +import { DatabaseRowProperties, DatabaseRowSubDocument } from '@/components/database/components/database-row'; +import DatabaseRowHeader from '@/components/database/components/header/DatabaseRowHeader'; +import { DatabaseContextProvider } from '@/components/database/DatabaseContext'; +import { Log } from '@/utils/log'; +import { Divider } from '@mui/material'; +import CircularProgress from '@mui/material/CircularProgress'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; +import RecordNotFound from 'src/components/_shared/not-found/RecordNotFound'; +import * as Y from 'yjs'; + +function DatabaseRow({ rowId }: { rowId: string }) { + const { objectId, workspaceId } = useId() || {}; + const [doc, setDoc] = useState(null); + const [rows, setRows] = useState | null>(null); // Map(false); + const handleOpenDatabaseRow = useCallback(async () => { + if (!databaseService || !workspaceId || !objectId) return; + + try { + setDoc(null); + const { databaseDoc, rows } = await databaseService.openDatabase(workspaceId, objectId, [rowId]); + + console.log('database', databaseDoc.getMap(YjsEditorKey.data_section).toJSON()); + console.log('row', rows.get(rowId)?.getMap(YjsEditorKey.data_section).toJSON()); + + const row = rows.get(rowId); + + if (!row) { + setNotFound(true); + return; + } + + setDoc(databaseDoc); + setRows(rows); + } catch (e) { + Log.error(e); + setNotFound(true); + } + }, [databaseService, workspaceId, objectId, rowId]); + + useEffect(() => { + setNotFound(false); + void handleOpenDatabaseRow(); + }, [handleOpenDatabaseRow]); + + if (notFound || !objectId) { + return ; + } + + if (!rows || !doc) { + return ( +
+ +
+ ); + } + + return ( +
+
+ + + +
+ + + +
+
+
+
+ ); +} + +export default DatabaseRow; diff --git a/frontend/appflowy_web_app/src/components/database/DatabaseTitle.tsx b/frontend/appflowy_web_app/src/components/database/DatabaseTitle.tsx new file mode 100644 index 0000000000..fb996978ff --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/DatabaseTitle.tsx @@ -0,0 +1,19 @@ +import { usePageInfo } from '@/components/_shared/page/usePageInfo'; +import React from 'react'; + +function DatabaseTitle({ viewId }: { viewId: string }) { + const { name, icon } = usePageInfo(viewId); + + return ( +
+
+
+
{icon}
+
{name}
+
+
+
+ ); +} + +export default DatabaseTitle; diff --git a/frontend/appflowy_web_app/src/components/database/DatabaseViews.tsx b/frontend/appflowy_web_app/src/components/database/DatabaseViews.tsx new file mode 100644 index 0000000000..8c02c124b0 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/DatabaseViews.tsx @@ -0,0 +1,66 @@ +import { DatabaseViewLayout, YjsDatabaseKey } from '@/application/collab.type'; +import { useDatabaseViewsSelector } from '@/application/database-yjs'; +import { Board } from '@/components/database/board'; +import { Calendar } from '@/components/database/calendar'; +import { DatabaseConditionsContext } from '@/components/database/components/conditions/context'; +import { DatabaseTabs } from '@/components/database/components/tabs'; +import { Grid } from '@/components/database/grid'; +import React, { useCallback, useMemo, useState } from 'react'; +import DatabaseConditions from 'src/components/database/components/conditions/DatabaseConditions'; + +function DatabaseViews({ + onChangeView, + currentViewId, +}: { + onChangeView: (viewId: string) => void; + currentViewId: string; +}) { + const { childViews, viewIds } = useDatabaseViewsSelector(); + + const value = useMemo(() => { + return Math.max( + 0, + viewIds.findIndex((id) => id === currentViewId) + ); + }, [currentViewId, viewIds]); + + const [conditionsExpanded, setConditionsExpanded] = useState(false); + const toggleExpanded = useCallback(() => { + setConditionsExpanded((prev) => !prev); + }, []); + + const activeView = useMemo(() => { + return childViews[value]; + }, [childViews, value]); + + const view = useMemo(() => { + if (!activeView) return null; + const layout = Number(activeView.get(YjsDatabaseKey.layout)) as DatabaseViewLayout; + + switch (layout) { + case DatabaseViewLayout.Grid: + return ; + case DatabaseViewLayout.Board: + return ; + case DatabaseViewLayout.Calendar: + return ; + } + }, [activeView]); + + return ( + <> + + + + +
{view}
+ + ); +} + +export default DatabaseViews; diff --git a/frontend/appflowy_web_app/src/components/database/board/Board.tsx b/frontend/appflowy_web_app/src/components/database/board/Board.tsx new file mode 100644 index 0000000000..27c43bf8b3 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/board/Board.tsx @@ -0,0 +1,34 @@ +import { useDatabase, useGroupsSelector } from '@/application/database-yjs'; +import { Group } from '@/components/database/components/board'; +import { CircularProgress } from '@mui/material'; +import React from 'react'; +import { DragDropContext } from 'react-beautiful-dnd'; + +export function Board() { + const database = useDatabase(); + const groups = useGroupsSelector(); + + if (!database) { + return ( +
+ +
+ ); + } + + return ( + { + // + }} + > +
+ {groups.map((groupId) => ( + + ))} +
+
+ ); +} + +export default Board; diff --git a/frontend/appflowy_web_app/src/components/database/board/index.ts b/frontend/appflowy_web_app/src/components/database/board/index.ts new file mode 100644 index 0000000000..9294d869ce --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/board/index.ts @@ -0,0 +1 @@ +export * from './Board'; diff --git a/frontend/appflowy_web_app/src/components/database/calendar/Calendar.hooks.ts b/frontend/appflowy_web_app/src/components/database/calendar/Calendar.hooks.ts new file mode 100644 index 0000000000..b3ec014505 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/calendar/Calendar.hooks.ts @@ -0,0 +1,41 @@ +import { useCalendarEventsSelector, useCalendarLayoutSetting } from '@/application/database-yjs'; +import { useCallback, useEffect, useMemo } from 'react'; +import { dayjsLocalizer } from 'react-big-calendar'; +import dayjs from 'dayjs'; +import en from 'dayjs/locale/en'; + +export function useCalendarSetup() { + const layoutSetting = useCalendarLayoutSetting(); + const { events, emptyEvents } = useCalendarEventsSelector(); + + const dayPropGetter = useCallback((date: Date) => { + const day = date.getDay(); + + return { + className: `day-${day}`, + }; + }, []); + + useEffect(() => { + dayjs.locale({ + ...en, + weekStart: layoutSetting.firstDayOfWeek, + }); + }, [layoutSetting]); + + const localizer = useMemo(() => dayjsLocalizer(dayjs), []); + + const formats = useMemo(() => { + return { + weekdayFormat: 'ddd', + }; + }, []); + + return { + localizer, + formats, + dayPropGetter, + events, + emptyEvents, + }; +} diff --git a/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx b/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx new file mode 100644 index 0000000000..046d558254 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx @@ -0,0 +1,33 @@ +import { useCalendarSetup } from '@/components/database/calendar/Calendar.hooks'; +import { Toolbar, Event } from '@/components/database/components/calendar'; +import React from 'react'; +import { Calendar as BigCalendar } from 'react-big-calendar'; +import './calendar.scss'; + +export function Calendar() { + const { dayPropGetter, localizer, formats, events, emptyEvents } = useCalendarSetup(); + + return ( +
+ , + eventWrapper: Event, + }} + style={{ + marginBottom: '24px', + }} + events={events} + views={['month']} + localizer={localizer} + formats={formats} + dayPropGetter={dayPropGetter} + showMultiDayTimes={true} + step={1} + showAllEvents={true} + /> +
+ ); +} + +export default Calendar; diff --git a/frontend/appflowy_web_app/src/components/database/calendar/calendar.scss b/frontend/appflowy_web_app/src/components/database/calendar/calendar.scss new file mode 100644 index 0000000000..4d2877154a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/calendar/calendar.scss @@ -0,0 +1,92 @@ +$today-highlight-bg: transparent; +@import 'react-big-calendar/lib/sass/styles'; +@import 'react-big-calendar/lib/addons/dragAndDrop/styles'; // if using DnD + +.rbc-calendar { + font-size: 12px; +} + +.rbc-button-link { + @apply rounded-full w-[20px] h-[20px] my-1.5; +} + + +.rbc-date-cell, .rbc-header { + min-width: 120px; + max-width: 180px; +} + +.rbc-date-cell.rbc-now { + + color: var(--content-on-fill); + + .rbc-button-link { + background-color: var(--function-error); + } +} + +.rbc-month-view { + border: none; + @apply h-full overflow-auto; + + .rbc-month-row { + border: 1px solid var(--line-divider); + border-top: none; + + } + + &::-webkit-scrollbar { + width: 4px; + height: 4px; + } + + + &:hover { + + &::-webkit-scrollbar-thumb, & *::-webkit-scrollbar-thumb { + border-radius: 4px; + background-color: var(--scrollbar-thumb); + } + } + + +} + + +.rbc-month-header { + height: 40px; + position: sticky; + top: 0; + background: var(--bg-body); + z-index: 50; + + .rbc-header { + border: none; + border-bottom: 1px solid var(--line-divider); + @apply flex items-end py-2 justify-center font-normal text-text-caption bg-bg-body; + + } +} + +.rbc-month-row .rbc-row-bg { + .rbc-off-range-bg { + background-color: transparent; + color: var(--text-caption); + } + + .rbc-day-bg.day-0, .rbc-day-bg.day-6 { + background-color: var(--fill-list-active); + } +} + +.rbc-month-row { + display: inline-table !important; + flex: 0 0 0 !important; + min-height: 120px !important; +} + +.event-properties { + .property-label { + @apply text-text-caption; + } +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/components/database/calendar/index.ts b/frontend/appflowy_web_app/src/components/database/calendar/index.ts new file mode 100644 index 0000000000..a723380592 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/calendar/index.ts @@ -0,0 +1 @@ +export * from './Calendar'; diff --git a/frontend/appflowy_web_app/src/components/database/components/board/card/Card.tsx b/frontend/appflowy_web_app/src/components/database/components/board/card/Card.tsx new file mode 100644 index 0000000000..6f3b5e7b76 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/card/Card.tsx @@ -0,0 +1,50 @@ +import { useFieldsSelector } from '@/application/database-yjs'; +import CardField from '@/components/database/components/field/CardField'; +import React, { useEffect, useMemo } from 'react'; + +export interface CardProps { + groupFieldId: string; + rowId: string; + onResize?: (height: number) => void; + isDragging?: boolean; +} + +export function Card({ groupFieldId, rowId, onResize, isDragging }: CardProps) { + const fields = useFieldsSelector(); + const showFields = useMemo(() => fields.filter((field) => field.fieldId !== groupFieldId), [fields, groupFieldId]); + + const ref = React.useRef(null); + + useEffect(() => { + if (isDragging) return; + const el = ref.current; + + if (!el) return; + + const observer = new ResizeObserver(() => { + onResize?.(el.offsetHeight); + }); + + observer.observe(el); + + return () => { + observer.disconnect(); + }; + }, [onResize, isDragging]); + + return ( +
+ {showFields.map((field, index) => { + return ; + })} +
+ ); +} + +export default Card; diff --git a/frontend/appflowy_web_app/src/components/database/components/board/card/index.ts b/frontend/appflowy_web_app/src/components/database/components/board/card/index.ts new file mode 100644 index 0000000000..ca0b060473 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/card/index.ts @@ -0,0 +1 @@ +export * from './Card'; diff --git a/frontend/appflowy_web_app/src/components/database/components/board/column/Column.tsx b/frontend/appflowy_web_app/src/components/database/components/board/column/Column.tsx new file mode 100644 index 0000000000..765bbb0a19 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/column/Column.tsx @@ -0,0 +1,130 @@ +import { Row } from '@/application/database-yjs'; +import { AFScroller } from '@/components/_shared/scroller'; +import { Tag } from '@/components/_shared/tag'; +import ListItem from '@/components/database/components/board/column/ListItem'; +import { useRenderColumn } from '@/components/database/components/board/column/useRenderColumn'; +import { useMeasureHeight } from '@/components/database/components/cell/useMeasure'; +import React, { useCallback, useEffect, useMemo } from 'react'; +import { Draggable, DraggableProvided, Droppable } from 'react-beautiful-dnd'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { VariableSizeList } from 'react-window'; + +export interface ColumnProps { + id: string; + rows?: Row[]; + fieldId: string; + provided: DraggableProvided; +} + +export function Column({ id, rows, fieldId, provided }: ColumnProps) { + const { header } = useRenderColumn(id, fieldId); + const ref = React.useRef(null); + const forceUpdate = useCallback((index: number) => { + ref.current?.resetAfterIndex(index, true); + }, []); + + useEffect(() => { + forceUpdate(0); + }, [rows, forceUpdate]); + + const measureRows = useMemo( + () => + rows?.map((row) => { + return { + rowId: row.id, + }; + }) || [], + [rows] + ); + const { rowHeight, onResize } = useMeasureHeight({ forceUpdate, rows: measureRows }); + + const Row = useCallback( + ({ index, style, data }: { index: number; style: React.CSSProperties; data: Row[] }) => { + const item = data[index]; + + // We are rendering an extra item for the placeholder + if (!item) { + return null; + } + + const onResizeCallback = (height: number) => { + onResize(index, 0, { + width: 0, + height: height + 8, + }); + }; + + return ( + + {(provided) => ( + + )} + + ); + }, + [fieldId, onResize] + ); + + const getItemSize = useCallback( + (index: number) => { + if (!rows || index >= rows.length) return 0; + const row = rows[index]; + + if (!row) return 0; + return rowHeight(index); + }, + [rowHeight, rows] + ); + + if (!rows) return
; + return ( +
+
+ +
+ +
+ ( + + )} + > + {(provided, snapshot) => { + // Add an extra item to our list to make space for a dragging item + // Usually the DroppableProvided.placeholder does this, but that won't + // work in a virtual list + const itemCount = snapshot.isUsingPlaceholder ? rows.length + 1 : rows.length; + + return ( + + {({ height, width }: { height: number; width: number }) => { + return ( + + {Row} + + ); + }} + + ); + }} + +
+
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/board/column/ListItem.tsx b/frontend/appflowy_web_app/src/components/database/components/board/column/ListItem.tsx new file mode 100644 index 0000000000..ac1e3bb82b --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/column/ListItem.tsx @@ -0,0 +1,74 @@ +import { Row } from '@/application/database-yjs'; +import React from 'react'; +import { DraggableProvided, DraggingStyle, NotDraggingStyle } from 'react-beautiful-dnd'; +import Card from 'src/components/database/components/board/card/Card'; + +export const ListItem = ({ + provided, + item, + style, + onResize, + fieldId, + isDragging, +}: { + provided: DraggableProvided; + item: Row; + style?: React.CSSProperties; + fieldId: string; + onResize?: (height: number) => void; + isDragging?: boolean; +}) => { + return ( +
+ +
+ ); +}; + +function getStyle({ + draggableStyle, + virtualStyle, + isDragging, +}: { + draggableStyle?: DraggingStyle | NotDraggingStyle; + virtualStyle?: React.CSSProperties; + isDragging?: boolean; +}) { + // If you don't want any spacing between your items + // then you could just return this. + // I do a little bit of magic to have some nice visual space + // between the row items + const combined = { + ...virtualStyle, + ...draggableStyle, + } as { + height: number; + left: number; + width: number; + }; + + // Being lazy: this is defined in our css file + const grid = 1; + + // when dragging we want to use the draggable style for placement, otherwise use the virtual style + + return { + ...combined, + height: isDragging ? combined.height : combined.height - grid, + left: isDragging ? combined.left : combined.left + grid, + width: isDragging ? (draggableStyle as DraggingStyle)?.width : `calc(${combined.width} - ${grid * 2}px)`, + marginBottom: grid, + }; +} + +export default ListItem; diff --git a/frontend/appflowy_web_app/src/components/database/components/board/column/index.ts b/frontend/appflowy_web_app/src/components/database/components/board/column/index.ts new file mode 100644 index 0000000000..f59b699c20 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/column/index.ts @@ -0,0 +1 @@ +export * from './Column'; diff --git a/frontend/appflowy_web_app/src/components/database/components/board/column/useRenderColumn.ts b/frontend/appflowy_web_app/src/components/database/components/board/column/useRenderColumn.ts new file mode 100644 index 0000000000..c845d4b5a3 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/column/useRenderColumn.ts @@ -0,0 +1,31 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType, parseSelectOptionTypeOptions, useFieldSelector } from '@/application/database-yjs'; +import { SelectOptionColorMap } from '@/components/database/components/cell/cell.const'; +import { useMemo } from 'react'; + +export function useRenderColumn(id: string, fieldId: string) { + const { field } = useFieldSelector(fieldId); + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + const fieldName = field?.get(YjsDatabaseKey.name) || ''; + const header = useMemo(() => { + if (!field) return null; + switch (fieldType) { + case FieldType.SingleSelect: + case FieldType.MultiSelect: { + const option = parseSelectOptionTypeOptions(field)?.options.find((option) => option.id === id); + + return { + name: option?.name || `No ${fieldName}`, + color: option?.color ? SelectOptionColorMap[option?.color] : 'transparent', + }; + } + + default: + return null; + } + }, [field, fieldName, fieldType, id]); + + return { + header, + }; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/board/group/Group.tsx b/frontend/appflowy_web_app/src/components/database/components/board/group/Group.tsx new file mode 100644 index 0000000000..0f67403beb --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/group/Group.tsx @@ -0,0 +1,61 @@ +import { useRowsByGroup } from '@/application/database-yjs'; +import { AFScroller } from '@/components/_shared/scroller'; +import React from 'react'; +import { Draggable, Droppable } from 'react-beautiful-dnd'; +import { useTranslation } from 'react-i18next'; +import { Column } from '../column'; + +export interface GroupProps { + groupId: string; +} + +export const Group = ({ groupId }: GroupProps) => { + const { columns, groupResult, fieldId, notFound } = useRowsByGroup(groupId); + + const { t } = useTranslation(); + + if (notFound) { + return ( +
+
{t('board.noGroup')}
+
{t('board.noGroupDesc')}
+
+ ); + } + + if (columns.length === 0 || !fieldId) return null; + return ( + + + {(provided) => { + return ( +
+ {columns.map((data, index) => ( + + {(provided) => { + return ( + + ); + }} + + ))} + {provided.placeholder} +
+ ); + }} +
+
+ ); +}; + +export default Group; diff --git a/frontend/appflowy_web_app/src/components/database/components/board/group/index.ts b/frontend/appflowy_web_app/src/components/database/components/board/group/index.ts new file mode 100644 index 0000000000..8401278d65 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/group/index.ts @@ -0,0 +1 @@ +export * from './Group'; diff --git a/frontend/appflowy_web_app/src/components/database/components/board/index.ts b/frontend/appflowy_web_app/src/components/database/components/board/index.ts new file mode 100644 index 0000000000..8a78f59377 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/index.ts @@ -0,0 +1 @@ +export * from './group'; diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/event/Event.tsx b/frontend/appflowy_web_app/src/components/database/components/calendar/event/Event.tsx new file mode 100644 index 0000000000..1e8d33ebcd --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/event/Event.tsx @@ -0,0 +1,34 @@ +import { CalendarEvent, useFieldsSelector } from '@/application/database-yjs'; +import { RichTooltip } from '@/components/_shared/popover'; +import EventPaper from '@/components/database/components/calendar/event/EventPaper'; +import CardField from '@/components/database/components/field/CardField'; +import React, { useMemo } from 'react'; +import { EventWrapperProps } from 'react-big-calendar'; + +export function Event({ event }: EventWrapperProps) { + const { id } = event; + const [rowId, fieldId] = id.split(':'); + const fields = useFieldsSelector(); + const showFields = useMemo(() => fields.filter((field) => field.fieldId !== fieldId), [fields, fieldId]); + + const [open, setOpen] = React.useState(false); + + return ( +
+ } open={open} placement='right' onClose={() => setOpen(false)}> +
setOpen((prev) => !prev)} + className={ + 'flex min-h-[24px] cursor-pointer flex-col gap-2 rounded-md border border-line-border bg-bg-body p-2 shadow-sm hover:bg-fill-list-active hover:shadow' + } + > + {showFields.map((field) => { + return ; + })} +
+
+
+ ); +} + +export default Event; diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/event/EventPaper.tsx b/frontend/appflowy_web_app/src/components/database/components/calendar/event/EventPaper.tsx new file mode 100644 index 0000000000..3bc31dd942 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/event/EventPaper.tsx @@ -0,0 +1,39 @@ +import { useFieldsSelector, useNavigateToRow } from '@/application/database-yjs'; +import { Property } from '@/components/database/components/property'; +import { Tooltip } from '@mui/material'; +import React from 'react'; +import { ReactComponent as ExpandMoreIcon } from '$icons/16x/full_view.svg'; +import { useTranslation } from 'react-i18next'; + +function EventPaper({ rowId }: { rowId: string }) { + const fields = useFieldsSelector(); + const navigateToRow = useNavigateToRow(); + const { t } = useTranslation(); + + return ( +
+
+
+ + + +
+
+ {fields.map((field) => { + return ; + })} +
+
+
+ ); +} + +export default EventPaper; diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/event/index.ts b/frontend/appflowy_web_app/src/components/database/components/calendar/event/index.ts new file mode 100644 index 0000000000..e59a119814 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/event/index.ts @@ -0,0 +1 @@ +export * from './Event'; diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/index.ts b/frontend/appflowy_web_app/src/components/database/components/calendar/index.ts new file mode 100644 index 0000000000..7b631093dc --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/index.ts @@ -0,0 +1,2 @@ +export * from './toolbar'; +export * from './event'; diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDate.tsx b/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDate.tsx new file mode 100644 index 0000000000..7965bc33b7 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDate.tsx @@ -0,0 +1,46 @@ +import { CalendarEvent } from '@/application/database-yjs'; +import { RichTooltip } from '@/components/_shared/popover'; +import NoDateRow from '@/components/database/components/calendar/toolbar/NoDateRow'; +import Button from '@mui/material/Button'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function NoDate({ emptyEvents }: { emptyEvents: CalendarEvent[] }) { + const [open, setOpen] = React.useState(false); + const { t } = useTranslation(); + const content = useMemo(() => { + return ( +
+
{t('calendar.settings.clickToOpen')}
+ {emptyEvents.map((event) => { + const rowId = event.id.split(':')[0]; + + return ; + })} +
+ ); + }, [emptyEvents, t]); + + return ( + { + setOpen(false); + }} + > + + + ); +} + +export default NoDate; diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDateRow.tsx b/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDateRow.tsx new file mode 100644 index 0000000000..5e2eaa61d2 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDateRow.tsx @@ -0,0 +1,39 @@ +import { useCellSelector, useNavigateToRow, usePrimaryFieldId } from '@/application/database-yjs'; +import { Cell } from '@/components/database/components/cell'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +function NoDateRow({ rowId }: { rowId: string }) { + const navigateToRow = useNavigateToRow(); + const primaryFieldId = usePrimaryFieldId(); + const cell = useCellSelector({ + rowId, + fieldId: primaryFieldId || '', + }); + const { t } = useTranslation(); + + if (!primaryFieldId || !cell?.data) { + return
{t('grid.row.titlePlaceholder')}
; + } + + return ( +
{ + navigateToRow?.(rowId); + }} + className={'w-full hover:text-fill-default'} + > + +
+ ); +} + +export default NoDateRow; diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/Toolbar.tsx b/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/Toolbar.tsx new file mode 100644 index 0000000000..ed082d4afe --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/Toolbar.tsx @@ -0,0 +1,59 @@ +import { CalendarEvent } from '@/application/database-yjs'; +import NoDate from '@/components/database/components/calendar/toolbar/NoDate'; +import { IconButton } from '@mui/material'; +import Button from '@mui/material/Button'; +import dayjs from 'dayjs'; +import React, { useMemo } from 'react'; +import { ToolbarProps } from 'react-big-calendar'; +import { ReactComponent as LeftArrow } from '$icons/16x/arrow_left.svg'; +import { ReactComponent as RightArrow } from '$icons/16x/arrow_right.svg'; +import { ReactComponent as DownArrow } from '$icons/16x/arrow_down.svg'; + +import { useTranslation } from 'react-i18next'; + +export function Toolbar({ + onNavigate, + date, + emptyEvents, +}: ToolbarProps & { + emptyEvents: CalendarEvent[]; +}) { + const dateStr = useMemo(() => dayjs(date).format('MMM YYYY'), [date]); + const { t } = useTranslation(); + + return ( +
+
{dateStr}
+
+ onNavigate('PREV')}> + + + + onNavigate('NEXT')}> + + + + +
+
+ ); +} + +export default Toolbar; diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/index.ts b/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/index.ts new file mode 100644 index 0000000000..7c6430332b --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/index.ts @@ -0,0 +1 @@ +export * from './Toolbar'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/Cell.hooks.ts b/frontend/appflowy_web_app/src/components/database/components/cell/Cell.hooks.ts new file mode 100644 index 0000000000..2e752f8f6e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/Cell.hooks.ts @@ -0,0 +1,47 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType } from '@/application/database-yjs/database.type'; +import { useFieldSelector } from '@/application/database-yjs/selector'; +import { DateFormat, TimeFormat, getDateFormat, getTimeFormat } from '@/application/database-yjs'; +import { renderDate } from '@/utils/time'; +import { useCallback, useMemo } from 'react'; + +export function useCellTypeOption(fieldId: string) { + const { field } = useFieldSelector(fieldId); + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + + return useMemo(() => { + return field?.get(YjsDatabaseKey.type_option)?.get(String(fieldType)); + }, [fieldType, field]); +} + +export function useDateTypeCellDispatcher(fieldId: string) { + const typeOption = useCellTypeOption(fieldId); + const typeOptionValue = useMemo(() => { + if (!typeOption) return null; + return { + timeFormat: parseInt(typeOption.get(YjsDatabaseKey.time_format)) as TimeFormat, + dateFormat: parseInt(typeOption.get(YjsDatabaseKey.date_format)) as DateFormat, + }; + }, [typeOption]); + + const getDateTimeStr = useCallback( + (timeStamp: string, includeTime?: boolean) => { + if (!typeOptionValue || !timeStamp) return null; + const timeFormat = getTimeFormat(typeOptionValue.timeFormat); + const dateFormat = getDateFormat(typeOptionValue.dateFormat); + const format = [dateFormat]; + + if (includeTime) { + format.push(timeFormat); + } + + return renderDate(timeStamp, format.join(' '), true); + }, + [typeOptionValue] + ); + + return { + getDateTimeStr, + typeOptionValue, + }; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/Cell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/Cell.tsx new file mode 100644 index 0000000000..d234397606 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/Cell.tsx @@ -0,0 +1,58 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType } from '@/application/database-yjs/database.type'; +import { useFieldSelector } from '@/application/database-yjs/selector'; +import { RowCreateModifiedTime } from '@/components/database/components/cell/created-modified'; +import React, { FC, useMemo } from 'react'; +import { TextCell } from '@/components/database/components/cell/text'; +import { UrlCell } from '@/components/database/components/cell/url'; +import { NumberCell } from '@/components/database/components/cell/number'; +import { CheckboxCell } from '@/components/database/components/cell/checkbox'; +import { SelectOptionCell } from '@/components/database/components/cell/select-option'; +import { DateTimeCell } from '@/components/database/components/cell/date'; +import { ChecklistCell } from '@/components/database/components/cell/checklist'; +import { CellProps, Cell as CellType } from '@/components/database/components/cell/cell.type'; +import { RelationCell } from '@/components/database/components/cell/relation'; + +export function Cell(props: CellProps) { + const { cell, rowId, fieldId, style } = props; + const { field } = useFieldSelector(fieldId); + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + + const Component = useMemo(() => { + switch (fieldType) { + case FieldType.RichText: + return TextCell; + case FieldType.URL: + return UrlCell; + case FieldType.Number: + return NumberCell; + case FieldType.Checkbox: + return CheckboxCell; + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return SelectOptionCell; + case FieldType.DateTime: + return DateTimeCell; + case FieldType.Checklist: + return ChecklistCell; + case FieldType.Relation: + return RelationCell; + default: + return TextCell; + } + }, [fieldType]) as FC>; + + if (fieldType === FieldType.CreatedTime || fieldType === FieldType.LastEditedTime) { + const attrName = fieldType === FieldType.CreatedTime ? YjsDatabaseKey.created_at : YjsDatabaseKey.last_modified; + + return ; + } + + if (cell && cell.fieldType !== fieldType) { + return null; + } + + return ; +} + +export default Cell; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/cell.const.ts b/frontend/appflowy_web_app/src/components/database/components/cell/cell.const.ts new file mode 100644 index 0000000000..d9e3564096 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/cell.const.ts @@ -0,0 +1,25 @@ +import { SelectOptionColor } from '@/application/database-yjs'; + +export const SelectOptionColorMap = { + [SelectOptionColor.Purple]: '--tint-purple', + [SelectOptionColor.Pink]: '--tint-pink', + [SelectOptionColor.LightPink]: '--tint-red', + [SelectOptionColor.Orange]: '--tint-orange', + [SelectOptionColor.Yellow]: '--tint-yellow', + [SelectOptionColor.Lime]: '--tint-lime', + [SelectOptionColor.Green]: '--tint-green', + [SelectOptionColor.Aqua]: '--tint-aqua', + [SelectOptionColor.Blue]: '--tint-blue', +}; + +export const SelectOptionColorTextMap = { + [SelectOptionColor.Purple]: 'purpleColor', + [SelectOptionColor.Pink]: 'pinkColor', + [SelectOptionColor.LightPink]: 'lightPinkColor', + [SelectOptionColor.Orange]: 'orangeColor', + [SelectOptionColor.Yellow]: 'yellowColor', + [SelectOptionColor.Lime]: 'limeColor', + [SelectOptionColor.Green]: 'greenColor', + [SelectOptionColor.Aqua]: 'aquaColor', + [SelectOptionColor.Blue]: 'blueColor', +} as const; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/cell.parse.ts b/frontend/appflowy_web_app/src/components/database/components/cell/cell.parse.ts new file mode 100644 index 0000000000..4124381c06 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/cell.parse.ts @@ -0,0 +1,46 @@ +import { YDatabaseCell, YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType } from '@/application/database-yjs/database.type'; +import { Cell, CheckboxCell, DateTimeCell } from './cell.type'; + +export function parseYDatabaseCommonCellToCell(cell: YDatabaseCell): Cell { + return { + createdAt: Number(cell.get(YjsDatabaseKey.created_at)), + lastModified: Number(cell.get(YjsDatabaseKey.last_modified)), + fieldType: parseInt(cell.get(YjsDatabaseKey.field_type)) as FieldType, + data: cell.get(YjsDatabaseKey.data), + }; +} + +export function parseYDatabaseCellToCell(cell: YDatabaseCell): Cell { + const fieldType = parseInt(cell.get(YjsDatabaseKey.field_type)); + + if (fieldType === FieldType.DateTime) { + return parseYDatabaseDateTimeCellToCell(cell); + } + + if (fieldType === FieldType.Checkbox) { + return parseYDatabaseCheckboxCellToCell(cell); + } + + return parseYDatabaseCommonCellToCell(cell); +} + +export function parseYDatabaseDateTimeCellToCell(cell: YDatabaseCell): DateTimeCell { + return { + ...parseYDatabaseCommonCellToCell(cell), + data: cell.get(YjsDatabaseKey.data) as string, + fieldType: FieldType.DateTime, + endTimestamp: cell.get(YjsDatabaseKey.end_timestamp), + includeTime: cell.get(YjsDatabaseKey.include_time), + isRange: cell.get(YjsDatabaseKey.is_range), + reminderId: cell.get(YjsDatabaseKey.reminder_id), + }; +} + +export function parseYDatabaseCheckboxCellToCell(cell: YDatabaseCell): CheckboxCell { + return { + ...parseYDatabaseCommonCellToCell(cell), + data: cell.get(YjsDatabaseKey.data) === 'Yes', + fieldType: FieldType.Checkbox, + }; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/cell.type.ts b/frontend/appflowy_web_app/src/components/database/components/cell/cell.type.ts new file mode 100644 index 0000000000..1c82465a84 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/cell.type.ts @@ -0,0 +1,86 @@ +import { FieldId, RowId } from '@/application/collab.type'; +import { DateFormat, TimeFormat } from '@/application/database-yjs'; +import { FieldType } from '@/application/database-yjs/database.type'; +import React from 'react'; +import { YArray } from 'yjs/dist/src/types/YArray'; + +export interface Cell { + createdAt: number; + lastModified: number; + fieldType: FieldType; + data: unknown; +} + +export interface TextCell extends Cell { + fieldType: FieldType.RichText; + data: string; +} + +export interface NumberCell extends Cell { + fieldType: FieldType.Number; + data: string; +} + +export interface CheckboxCell extends Cell { + fieldType: FieldType.Checkbox; + data: boolean; +} + +export interface UrlCell extends Cell { + fieldType: FieldType.URL; + data: string; +} + +export type SelectionId = string; + +export interface SelectOptionCell extends Cell { + fieldType: FieldType.SingleSelect | FieldType.MultiSelect; + data: SelectionId; +} + +export interface DataTimeTypeOption { + timeFormat: TimeFormat; + dateFormat: DateFormat; +} + +export interface DateTimeCell extends Cell { + fieldType: FieldType.DateTime; + data: string; + endTimestamp?: string; + includeTime?: boolean; + isRange?: boolean; + reminderId?: string; +} + +export interface DateTimeCellData { + date?: string; + time?: string; + timestamp?: number; + includeTime?: boolean; + endDate?: string; + endTime?: string; + endTimestamp?: number; + isRange?: boolean; +} + +export interface ChecklistCell extends Cell { + fieldType: FieldType.Checklist; + data: string; +} + +export interface RelationCell extends Cell { + fieldType: FieldType.Relation; + data: YArray; +} + +export type RelationCellData = RowId[]; + +export interface CellProps { + cell?: T; + rowId: string; + fieldId: FieldId; + style?: React.CSSProperties; + readOnly?: boolean; + placeholder?: string; + className?: string; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/checkbox/CheckboxCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/checkbox/CheckboxCell.tsx new file mode 100644 index 0000000000..cc665474ec --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/checkbox/CheckboxCell.tsx @@ -0,0 +1,13 @@ +import { ReactComponent as CheckboxCheckSvg } from '$icons/16x/check_filled.svg'; +import { ReactComponent as CheckboxUncheckSvg } from '$icons/16x/uncheck.svg'; +import { CellProps, CheckboxCell as CheckboxCellType } from '@/components/database/components/cell/cell.type'; + +export function CheckboxCell({ cell, style }: CellProps) { + const checked = cell?.data; + + return ( +
+ {checked ? : } +
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/checkbox/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/checkbox/index.ts new file mode 100644 index 0000000000..f1cb1ac4bf --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/checkbox/index.ts @@ -0,0 +1 @@ +export * from './CheckboxCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/checklist/ChecklistCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/checklist/ChecklistCell.tsx new file mode 100644 index 0000000000..3d6b53e9fc --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/checklist/ChecklistCell.tsx @@ -0,0 +1,25 @@ +import { parseChecklistData } from '@/application/database-yjs'; +import { CellProps, ChecklistCell as ChecklistCellType } from '@/components/database/components/cell/cell.type'; +import LinearProgressWithLabel from '@/components/_shared/progress/LinearProgressWithLabel'; +import React, { useMemo } from 'react'; + +export function ChecklistCell({ cell, style, placeholder }: CellProps) { + const data = useMemo(() => { + return parseChecklistData(cell?.data ?? ''); + }, [cell?.data]); + + const options = data?.options; + const selectedOptions = data?.selectedOptionIds; + + if (!data || !options || !selectedOptions) + return placeholder ? ( +
+ {placeholder} +
+ ) : null; + return ( +
+ +
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/checklist/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/checklist/index.ts new file mode 100644 index 0000000000..b12d47b6c5 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/checklist/index.ts @@ -0,0 +1 @@ +export * from './ChecklistCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/RowCreateModifiedTime.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/RowCreateModifiedTime.tsx new file mode 100644 index 0000000000..d3ec145c67 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/RowCreateModifiedTime.tsx @@ -0,0 +1,50 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { useRowData } from '@/application/database-yjs'; +import { useDateTypeCellDispatcher } from '@/components/database/components/cell/Cell.hooks'; +import React, { useEffect, useMemo, useState } from 'react'; + +export function RowCreateModifiedTime({ + rowId, + fieldId, + attrName, + style, +}: { + rowId: string; + fieldId: string; + style?: React.CSSProperties; + attrName: YjsDatabaseKey.last_modified | YjsDatabaseKey.created_at; +}) { + const { getDateTimeStr } = useDateTypeCellDispatcher(fieldId); + const rowData = useRowData(rowId); + const [value, setValue] = useState(null); + + useEffect(() => { + if (!rowData) return; + const observeHandler = () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + setValue(rowData.get(attrName)); + }; + + observeHandler(); + + rowData.observe(observeHandler); + return () => { + rowData.unobserve(observeHandler); + }; + }, [rowData, attrName]); + + const time = useMemo(() => { + if (!value) return null; + return getDateTimeStr(value, false); + }, [value, getDateTimeStr]); + + if (!time) return null; + return ( +
+ {time} +
+ ); +} + +export default RowCreateModifiedTime; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/index.ts new file mode 100644 index 0000000000..ed951f3521 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/index.ts @@ -0,0 +1 @@ +export * from './RowCreateModifiedTime'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/date/DateTimeCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/date/DateTimeCell.tsx new file mode 100644 index 0000000000..324737df4d --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/date/DateTimeCell.tsx @@ -0,0 +1,40 @@ +import { useDateTypeCellDispatcher } from '@/components/database/components/cell/Cell.hooks'; +import { CellProps, DateTimeCell as DateTimeCellType } from '@/components/database/components/cell/cell.type'; +import React, { useMemo } from 'react'; +import { ReactComponent as ReminderSvg } from '$icons/16x/clock_alarm.svg'; + +export function DateTimeCell({ cell, fieldId, style, placeholder }: CellProps) { + const { getDateTimeStr } = useDateTypeCellDispatcher(fieldId); + + const startDateTime = useMemo(() => { + return getDateTimeStr(cell?.data || '', cell?.includeTime); + }, [cell, getDateTimeStr]); + + const endDateTime = useMemo(() => { + if (!cell) return null; + const { endTimestamp, isRange } = cell; + + if (!isRange) return null; + + return getDateTimeStr(endTimestamp || '', cell?.includeTime); + }, [cell, getDateTimeStr]); + + const dateStr = useMemo(() => { + return [startDateTime, endDateTime].filter(Boolean).join(' -> '); + }, [startDateTime, endDateTime]); + + const hasReminder = !!cell?.reminderId; + + if (!cell?.data) + return placeholder ? ( +
+ {placeholder} +
+ ) : null; + return ( +
+ {hasReminder && } + {dateStr} +
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/date/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/date/index.ts new file mode 100644 index 0000000000..e05bb1674a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/date/index.ts @@ -0,0 +1 @@ +export * from './DateTimeCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/index.ts new file mode 100644 index 0000000000..2440976340 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/index.ts @@ -0,0 +1 @@ +export * from './Cell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/number/NumberCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/number/NumberCell.tsx new file mode 100644 index 0000000000..56ac39ef8d --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/number/NumberCell.tsx @@ -0,0 +1,36 @@ +import { currencyFormaterMap, NumberFormat, useFieldSelector, parseNumberTypeOptions } from '@/application/database-yjs'; +import { CellProps, NumberCell as NumberCellType } from '@/components/database/components/cell/cell.type'; +import React, { useMemo } from 'react'; +import Decimal from 'decimal.js'; + +export function NumberCell({ cell, fieldId, style, placeholder }: CellProps) { + const { field } = useFieldSelector(fieldId); + + const format = useMemo(() => (field ? parseNumberTypeOptions(field).format : NumberFormat.Num), [field]); + + const className = useMemo(() => { + const classList = ['select-text', 'cursor-text']; + + return classList.join(' '); + }, []); + + const value = useMemo(() => { + if (!cell) return ''; + const numberFormater = currencyFormaterMap[format]; + + if (!numberFormater) return cell.data; + return numberFormater(new Decimal(cell.data).toNumber()); + }, [cell, format]); + + if (value === undefined) + return placeholder ? ( +
+ {placeholder} +
+ ) : null; + return ( +
+ {value} +
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/number/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/number/index.ts new file mode 100644 index 0000000000..3e1686c783 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/number/index.ts @@ -0,0 +1 @@ +export * from './NumberCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/primary/PrimaryCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/primary/PrimaryCell.tsx new file mode 100644 index 0000000000..b9014f02f7 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/primary/PrimaryCell.tsx @@ -0,0 +1,65 @@ +import { ReactComponent as ExpandMoreIcon } from '$icons/16x/full_view.svg'; +import { useNavigateToRow, useRowMetaSelector } from '@/application/database-yjs'; +import { TextCell as CellType, CellProps } from '@/components/database/components/cell/cell.type'; +import { TextCell } from '@/components/database/components/cell/text'; +import { Tooltip } from '@mui/material'; +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +export function PrimaryCell(props: CellProps) { + const navigateToRow = useNavigateToRow(); + const { rowId } = props; + const icon = useRowMetaSelector(rowId)?.icon; + + const [hover, setHover] = useState(false); + const { t } = useTranslation(); + + useEffect(() => { + const table = document.querySelector('.grid-table'); + + if (!table) { + return; + } + + const onMouseMove = (e: Event) => { + const target = e.target as HTMLElement; + + if (target.closest('.grid-row-cell')?.getAttribute('data-row-id') === rowId) { + setHover(true); + } else { + setHover(false); + } + }; + + table.addEventListener('mousemove', onMouseMove); + return () => { + table.removeEventListener('mousemove', onMouseMove); + }; + }, [rowId]); + return ( +
+ {icon &&
{icon}
} +
+ +
+ + {hover && ( + + + + )} +
+ ); +} + +export default PrimaryCell; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/primary/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/primary/index.ts new file mode 100644 index 0000000000..c854b4e336 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/primary/index.ts @@ -0,0 +1 @@ +export * from './PrimaryCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationCell.tsx new file mode 100644 index 0000000000..5bd8eb7b87 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationCell.tsx @@ -0,0 +1,13 @@ +import { CellProps, RelationCell as RelationCellType } from '@/components/database/components/cell/cell.type'; +import RelationItems from '@/components/database/components/cell/relation/RelationItems'; +import React from 'react'; + +export function RelationCell({ cell, fieldId, style, placeholder }: CellProps) { + if (!cell?.data) + return placeholder ? ( +
+ {placeholder} +
+ ) : null; + return ; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationItems.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationItems.tsx new file mode 100644 index 0000000000..6d68eee4af --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationItems.tsx @@ -0,0 +1,54 @@ +import { YDatabaseField, YDatabaseFields, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; +import { parseRelationTypeOption, useFieldSelector } from '@/application/database-yjs'; +import { useId } from '@/components/_shared/context-provider/IdProvider'; +import { AFConfigContext } from '@/components/app/AppConfig'; +import { RelationCell, RelationCellData } from '@/components/database/components/cell/cell.type'; +import { RelationPrimaryValue } from '@/components/database/components/cell/relation/RelationPrimaryValue'; +import React, { useContext, useEffect, useMemo, useState } from 'react'; +import * as Y from 'yjs'; + +function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId: string; style?: React.CSSProperties }) { + const { field } = useFieldSelector(fieldId); + const workspaceId = useId()?.workspaceId; + const rowIds = useMemo(() => (cell.data.toJSON() as RelationCellData) ?? [], [cell.data]); + const databaseId = rowIds.length > 0 && field ? parseRelationTypeOption(field).database_id : undefined; + const databaseService = useContext(AFConfigContext)?.service?.databaseService; + const [databasePrimaryFieldId, setDatabasePrimaryFieldId] = useState(undefined); + const [rows, setRows] = useState | null>(); + + useEffect(() => { + if (!workspaceId || !databaseId) return; + void databaseService?.getDatabase(workspaceId, databaseId, rowIds).then(({ databaseDoc: doc, rows }) => { + const fields = doc + .getMap(YjsEditorKey.data_section) + .get(YjsEditorKey.database) + .get(YjsDatabaseKey.fields) as YDatabaseFields; + + fields.forEach((field, fieldId) => { + if ((field as YDatabaseField).get(YjsDatabaseKey.is_primary)) { + setDatabasePrimaryFieldId(fieldId); + } + }); + + setRows(rows); + }); + }, [workspaceId, databaseId, databaseService, rowIds]); + + return ( +
+ {rowIds.map((rowId) => { + const rowDoc = rows?.get(rowId); + + return ( +
+ {rowDoc && databasePrimaryFieldId && ( + + )} +
+ ); + })} +
+ ); +} + +export default RelationItems; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationPrimaryValue.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationPrimaryValue.tsx new file mode 100644 index 0000000000..174a3693f0 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationPrimaryValue.tsx @@ -0,0 +1,27 @@ +import { FieldId, YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; +import { parseYDatabaseCellToCell } from '@/components/database/components/cell/cell.parse'; +import React, { useEffect, useState } from 'react'; + +export function RelationPrimaryValue({ rowDoc, fieldId }: { rowDoc: YDoc; fieldId: FieldId }) { + const [text, setText] = useState(null); + + useEffect(() => { + const row = rowDoc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow; + const cells = row.get(YjsDatabaseKey.cells); + const primaryCell = cells.get(fieldId); + + if (!primaryCell) return; + const observeHandler = () => { + setText(parseYDatabaseCellToCell(primaryCell).data as string); + }; + + observeHandler(); + + primaryCell.observe(observeHandler); + return () => { + primaryCell.unobserve(observeHandler); + }; + }, [rowDoc, fieldId]); + + return
{text}
; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/relation/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/relation/index.ts new file mode 100644 index 0000000000..95a0aa3668 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/relation/index.ts @@ -0,0 +1 @@ +export * from './RelationCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/select-option/SelectOptionCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/select-option/SelectOptionCell.tsx new file mode 100644 index 0000000000..4d3318297f --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/select-option/SelectOptionCell.tsx @@ -0,0 +1,41 @@ +import { useFieldSelector, parseSelectOptionTypeOptions } from '@/application/database-yjs'; +import { Tag } from '@/components/_shared/tag'; +import { SelectOptionColorMap } from '@/components/database/components/cell/cell.const'; +import { CellProps, SelectOptionCell as SelectOptionCellType } from '@/components/database/components/cell/cell.type'; +import React, { useCallback, useMemo } from 'react'; + +export function SelectOptionCell({ cell, fieldId, style, placeholder }: CellProps) { + const selectOptionIds = useMemo(() => (!cell?.data ? [] : cell?.data.split(',')), [cell]); + const { field } = useFieldSelector(fieldId); + const typeOption = useMemo(() => { + if (!field) return null; + return parseSelectOptionTypeOptions(field); + }, [field]); + + const renderSelectedOptions = useCallback( + (selected: string[]) => + selected.map((id) => { + const option = typeOption?.options?.find((option) => option.id === id); + + if (!option) return null; + return ; + }), + [typeOption] + ); + + if (!typeOption || !selectOptionIds?.length) + return placeholder ? ( +
+ {placeholder} +
+ ) : null; + + return ( +
+ {renderSelectedOptions(selectOptionIds)} +
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/select-option/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/select-option/index.ts new file mode 100644 index 0000000000..40df2f3d7d --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/select-option/index.ts @@ -0,0 +1 @@ +export * from './SelectOptionCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/text/TextCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/text/TextCell.tsx new file mode 100644 index 0000000000..4d882b8c28 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/text/TextCell.tsx @@ -0,0 +1,14 @@ +import { useReadOnly } from '@/application/database-yjs'; +import { CellProps, TextCell as TextCellType } from '@/components/database/components/cell/cell.type'; +import React from 'react'; + +export function TextCell({ cell, style }: CellProps) { + const readOnly = useReadOnly(); + + if (!cell?.data) return null; + return ( +
+ {cell?.data} +
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/text/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/text/index.ts new file mode 100644 index 0000000000..64bcb41a7f --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/text/index.ts @@ -0,0 +1 @@ +export * from './TextCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/url/UrlCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/url/UrlCell.tsx new file mode 100644 index 0000000000..63e3c4cb10 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/url/UrlCell.tsx @@ -0,0 +1,44 @@ +import { useReadOnly } from '@/application/database-yjs'; +import { CellProps, UrlCell as UrlCellType } from '@/components/database/components/cell/cell.type'; +import { openUrl, processUrl } from '@/utils/url'; +import React, { useMemo } from 'react'; + +export function UrlCell({ cell, style, placeholder }: CellProps) { + const readOnly = useReadOnly(); + + const isUrl = useMemo(() => (cell ? processUrl(cell.data) : false), [cell]); + + const className = useMemo(() => { + const classList = ['select-text', 'w-fit', 'flex', 'w-full', 'items-center']; + + if (isUrl) { + classList.push('text-content-blue-400', 'underline', 'cursor-pointer'); + } else { + classList.push('cursor-text'); + } + + return classList.join(' '); + }, [isUrl]); + + if (!cell?.data) + return placeholder ? ( +
+ {placeholder} +
+ ) : null; + + return ( +
{ + if (!isUrl || !cell) return; + if (readOnly) { + void openUrl(cell.data, '_blank'); + } + }} + className={className} + > + {cell?.data} +
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/url/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/url/index.ts new file mode 100644 index 0000000000..9f45924c97 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/url/index.ts @@ -0,0 +1 @@ +export * from './UrlCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/useMeasure.ts b/frontend/appflowy_web_app/src/components/database/components/cell/useMeasure.ts new file mode 100644 index 0000000000..d4d7020523 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/useMeasure.ts @@ -0,0 +1,53 @@ +import { DEFAULT_ROW_HEIGHT } from '@/application/database-yjs'; +import { useCallback, useRef } from 'react'; + +export function useMeasureHeight({ + forceUpdate, + rows, +}: { + forceUpdate: (index: number) => void; + rows: { + rowId?: string; + }[]; +}) { + const heightRef = useRef<{ [rowId: string]: number }>({}); + const rowHeight = useCallback( + (index: number) => { + const row = rows[index]; + + if (!row || !row.rowId) return DEFAULT_ROW_HEIGHT; + + return heightRef.current[row.rowId] || DEFAULT_ROW_HEIGHT; + }, + [rows] + ); + + const setRowHeight = useCallback( + (index: number, height: number) => { + const row = rows[index]; + const rowId = row.rowId; + + if (!row || !rowId) return; + const oldHeight = heightRef.current[rowId]; + + heightRef.current[rowId] = Math.max(oldHeight || DEFAULT_ROW_HEIGHT, height); + + if (oldHeight !== height) { + forceUpdate(index); + } + }, + [forceUpdate, rows] + ); + + const onResize = useCallback( + (rowIndex: number, columnIndex: number, size: { width: number; height: number }) => { + setRowHeight(rowIndex, size.height); + }, + [setRowHeight] + ); + + return { + rowHeight, + onResize, + }; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseActions.tsx b/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseActions.tsx new file mode 100644 index 0000000000..e095a0eacc --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseActions.tsx @@ -0,0 +1,42 @@ +import { DatabaseViewLayout, YjsDatabaseKey } from '@/application/collab.type'; +import { useDatabaseView, useFiltersSelector, useSortsSelector } from '@/application/database-yjs'; +import { useConditionsContext } from '@/components/database/components/conditions/context'; +import { TextButton } from '@/components/database/components/tabs/TextButton'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +export function DatabaseActions() { + const { t } = useTranslation(); + const view = useDatabaseView(); + const layout = Number(view?.get(YjsDatabaseKey.layout)) as DatabaseViewLayout; + const sorts = useSortsSelector(); + const filter = useFiltersSelector(); + const conditionsContext = useConditionsContext(); + + if (layout === DatabaseViewLayout.Calendar) { + return null; + } + + return ( +
+ { + conditionsContext?.toggleExpanded(); + }} + color={filter.length > 0 ? 'primary' : 'inherit'} + > + {t('grid.settings.filter')} + + { + conditionsContext?.toggleExpanded(); + }} + color={sorts.length > 0 ? 'primary' : 'inherit'} + > + {t('grid.settings.sort')} + +
+ ); +} + +export default DatabaseActions; diff --git a/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseConditions.tsx b/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseConditions.tsx new file mode 100644 index 0000000000..7c74e0fb8a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseConditions.tsx @@ -0,0 +1,33 @@ +import { useFiltersSelector, useSortsSelector } from '@/application/database-yjs'; +import { AFScroller } from '@/components/_shared/scroller'; +import { useConditionsContext } from '@/components/database/components/conditions/context'; +import React from 'react'; +import Filters from 'src/components/database/components/filters/Filters'; +import Sorts from 'src/components/database/components/sorts/Sorts'; + +export function DatabaseConditions() { + const conditionsContext = useConditionsContext(); + const expanded = conditionsContext?.expanded ?? false; + const sorts = useSortsSelector(); + const filters = useFiltersSelector(); + + return ( +
+ + + {sorts.length > 0 && filters.length > 0 &&
} + + +
+ ); +} + +export default DatabaseConditions; diff --git a/frontend/appflowy_web_app/src/components/database/components/conditions/context.ts b/frontend/appflowy_web_app/src/components/database/components/conditions/context.ts new file mode 100644 index 0000000000..aadb5007af --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/conditions/context.ts @@ -0,0 +1,12 @@ +import { createContext, useContext } from 'react'; + +interface DatabaseConditionsContextType { + expanded: boolean; + toggleExpanded: () => void; +} + +export function useConditionsContext() { + return useContext(DatabaseConditionsContext); +} + +export const DatabaseConditionsContext = createContext(undefined); diff --git a/frontend/appflowy_web_app/src/components/database/components/conditions/index.ts b/frontend/appflowy_web_app/src/components/database/components/conditions/index.ts new file mode 100644 index 0000000000..7b30286c5c --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/conditions/index.ts @@ -0,0 +1,2 @@ +export * from './DatabaseActions'; +export * from './DatabaseConditions'; diff --git a/frontend/appflowy_web_app/src/components/database/components/database-row/DatabaseRowProperties.tsx b/frontend/appflowy_web_app/src/components/database/components/database-row/DatabaseRowProperties.tsx new file mode 100644 index 0000000000..59405d6c15 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/database-row/DatabaseRowProperties.tsx @@ -0,0 +1,18 @@ +import { useFieldsSelector, usePrimaryFieldId } from '@/application/database-yjs'; +import { Property } from '@/components/database/components/property'; +import React from 'react'; + +export function DatabaseRowProperties({ rowId }: { rowId: string }) { + const primaryFieldId = usePrimaryFieldId(); + const fields = useFieldsSelector().filter((column) => column.fieldId !== primaryFieldId); + + return ( +
+ {fields.map((field) => { + return ; + })} +
+ ); +} + +export default DatabaseRowProperties; diff --git a/frontend/appflowy_web_app/src/components/database/components/database-row/DatabaseRowSubDocument.tsx b/frontend/appflowy_web_app/src/components/database/components/database-row/DatabaseRowSubDocument.tsx new file mode 100644 index 0000000000..b2dd815e7a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/database-row/DatabaseRowSubDocument.tsx @@ -0,0 +1,53 @@ +import { YDoc } from '@/application/collab.type'; +import { useRowMetaSelector } from '@/application/database-yjs'; +import { useId } from '@/components/_shared/context-provider/IdProvider'; +import { AFConfigContext } from '@/components/app/AppConfig'; +import { Editor } from '@/components/editor'; +import { Log } from '@/utils/log'; +import CircularProgress from '@mui/material/CircularProgress'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; +import RecordNotFound from '@/components/_shared/not-found/RecordNotFound'; + +export function DatabaseRowSubDocument({ rowId }: { rowId: string }) { + const { workspaceId } = useId() || {}; + const documentId = useRowMetaSelector(rowId)?.documentId; + + const [doc, setDoc] = useState(null); + const [notFound, setNotFound] = useState(false); + + const documentService = useContext(AFConfigContext)?.service?.documentService; + + const handleOpenDocument = useCallback(async () => { + if (!documentService || !workspaceId || !documentId) return; + try { + setDoc(null); + const doc = await documentService.openDocument(workspaceId, documentId); + + setDoc(doc); + } catch (e) { + Log.error(e); + setNotFound(true); + } + }, [documentService, workspaceId, documentId]); + + useEffect(() => { + setNotFound(false); + void handleOpenDocument(); + }, [handleOpenDocument]); + + if (notFound || !documentId) { + return ; + } + + if (!doc) { + return ( +
+ +
+ ); + } + + return ; +} + +export default DatabaseRowSubDocument; diff --git a/frontend/appflowy_web_app/src/components/database/components/database-row/index.ts b/frontend/appflowy_web_app/src/components/database/components/database-row/index.ts new file mode 100644 index 0000000000..a5f4ddd8aa --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/database-row/index.ts @@ -0,0 +1,2 @@ +export * from './DatabaseRowProperties'; +export * from './DatabaseRowSubDocument'; diff --git a/frontend/appflowy_web_app/src/components/database/components/field/CardField.tsx b/frontend/appflowy_web_app/src/components/database/components/field/CardField.tsx new file mode 100644 index 0000000000..585a3d2ce0 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/field/CardField.tsx @@ -0,0 +1,48 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { useCellSelector, useFieldSelector } from '@/application/database-yjs'; +import Cell from '@/components/database/components/cell/Cell'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function CardField({ rowId, fieldId, index }: { rowId: string; fieldId: string; index: number }) { + const { t } = useTranslation(); + const { field } = useFieldSelector(fieldId); + const cell = useCellSelector({ + rowId, + fieldId, + }); + + const isPrimary = field?.get(YjsDatabaseKey.is_primary); + const style = useMemo(() => { + const styleProperties = { + fontSize: '12px', + }; + + if (isPrimary) { + Object.assign(styleProperties, { + fontSize: '14px', + fontWeight: 500, + }); + } + + if (index !== 0) { + Object.assign(styleProperties, { + marginTop: '8px', + }); + } + + return styleProperties; + }, [index, isPrimary]); + + if (isPrimary && !cell?.data) { + return ( +
+ {t('grid.row.titlePlaceholder')} +
+ ); + } + + return ; +} + +export default CardField; diff --git a/frontend/appflowy_web_app/src/components/database/components/field/FieldDisplay.tsx b/frontend/appflowy_web_app/src/components/database/components/field/FieldDisplay.tsx new file mode 100644 index 0000000000..3ff135e8f7 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/field/FieldDisplay.tsx @@ -0,0 +1,20 @@ +import { FieldId, YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType, useFieldSelector } from '@/application/database-yjs'; +import { FieldTypeIcon } from '@/components/database/components/field/FieldTypeIcon'; +import React from 'react'; + +export function FieldDisplay({ fieldId }: { fieldId: FieldId }) { + const { field } = useFieldSelector(fieldId); + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + + if (!field) return null; + + return ( +
+ + {field?.get(YjsDatabaseKey.name)} +
+ ); +} + +export default FieldDisplay; diff --git a/frontend/appflowy_web_app/src/components/database/components/field/FieldTypeIcon.tsx b/frontend/appflowy_web_app/src/components/database/components/field/FieldTypeIcon.tsx new file mode 100644 index 0000000000..3749e21afd --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/field/FieldTypeIcon.tsx @@ -0,0 +1,33 @@ +import { FieldType } from '@/application/database-yjs/database.type'; +import { FC, memo } from 'react'; +import { ReactComponent as TextSvg } from '$icons/16x/text.svg'; +import { ReactComponent as NumberSvg } from '$icons/16x/number.svg'; +import { ReactComponent as DateSvg } from '$icons/16x/date.svg'; +import { ReactComponent as SingleSelectSvg } from '$icons/16x/single_select.svg'; +import { ReactComponent as MultiSelectSvg } from '$icons/16x/multiselect.svg'; +import { ReactComponent as ChecklistSvg } from '$icons/16x/checklist.svg'; +import { ReactComponent as CheckboxSvg } from '$icons/16x/checkbox.svg'; +import { ReactComponent as URLSvg } from '$icons/16x/url.svg'; +import { ReactComponent as LastEditedTimeSvg } from '$icons/16x/last_modified.svg'; +import { ReactComponent as CreatedSvg } from '$icons/16x/created_at.svg'; +import { ReactComponent as RelationSvg } from '$icons/16x/relation.svg'; + +export const FieldTypeSvgMap: Record>> = { + [FieldType.RichText]: TextSvg, + [FieldType.Number]: NumberSvg, + [FieldType.DateTime]: DateSvg, + [FieldType.SingleSelect]: SingleSelectSvg, + [FieldType.MultiSelect]: MultiSelectSvg, + [FieldType.Checkbox]: CheckboxSvg, + [FieldType.URL]: URLSvg, + [FieldType.Checklist]: ChecklistSvg, + [FieldType.LastEditedTime]: LastEditedTimeSvg, + [FieldType.CreatedTime]: CreatedSvg, + [FieldType.Relation]: RelationSvg, +}; + +export const FieldTypeIcon: FC<{ type: FieldType; className?: string }> = memo(({ type, ...props }) => { + const Svg = FieldTypeSvgMap[type]; + + return ; +}); diff --git a/frontend/appflowy_web_app/src/components/database/components/field/index.ts b/frontend/appflowy_web_app/src/components/database/components/field/index.ts new file mode 100644 index 0000000000..85ff96da07 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/field/index.ts @@ -0,0 +1,2 @@ +export * from './FieldTypeIcon'; +export * from './FieldDisplay'; diff --git a/frontend/appflowy_web_app/src/components/database/components/field/select-option/SelectOptionList.tsx b/frontend/appflowy_web_app/src/components/database/components/field/select-option/SelectOptionList.tsx new file mode 100644 index 0000000000..353ef5d349 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/field/select-option/SelectOptionList.tsx @@ -0,0 +1,30 @@ +import { parseSelectOptionTypeOptions, SelectOption, useFieldSelector } from '@/application/database-yjs'; +import { Tag } from '@/components/_shared/tag'; +import { SelectOptionColorMap } from '@/components/database/components/cell/cell.const'; +import React, { useCallback, useMemo } from 'react'; +import { ReactComponent as CheckIcon } from '$icons/16x/check.svg'; + +export function SelectOptionList({ fieldId, selectedIds }: { fieldId: string; selectedIds: string[] }) { + const { field } = useFieldSelector(fieldId); + const typeOption = useMemo(() => { + if (!field) return null; + return parseSelectOptionTypeOptions(field); + }, [field]); + + const renderOption = useCallback( + (option: SelectOption) => { + const isSelected = selectedIds.includes(option.id); + + return ( +
+ + {isSelected && } +
+ ); + }, + [selectedIds] + ); + + if (!field || !typeOption) return null; + return
{typeOption.options.map(renderOption)}
; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/field/select-option/index.ts b/frontend/appflowy_web_app/src/components/database/components/field/select-option/index.ts new file mode 100644 index 0000000000..20465070b4 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/field/select-option/index.ts @@ -0,0 +1 @@ +export * from './SelectOptionList'; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/Filter.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/Filter.tsx new file mode 100644 index 0000000000..3fe0c4daf3 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/Filter.tsx @@ -0,0 +1,57 @@ +import { useFilterSelector } from '@/application/database-yjs'; +import { Popover } from '@/components/_shared/popover'; +import { FilterContentOverview } from './overview'; +import React, { useState } from 'react'; +import { FieldDisplay } from '@/components/database/components/field'; +import { ReactComponent as ArrowDownSvg } from '$icons/16x/arrow_down.svg'; +import { FilterMenu } from './filter-menu'; + +function Filter({ filterId }: { filterId: string }) { + const filter = useFilterSelector(filterId); + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + if (!filter) return null; + + return ( + <> +
{ + setAnchorEl(e.currentTarget); + }} + className={ + 'flex cursor-pointer flex-nowrap items-center gap-1 rounded-full border border-line-divider py-1 px-2 hover:border-fill-default hover:text-fill-default hover:shadow-sm' + } + > +
+ +
+ +
+ +
+ +
+ {open && ( + { + setAnchorEl(null); + }} + slotProps={{ + paper: { + style: { + maxHeight: '260px', + }, + }, + }} + > + + + )} + + ); +} + +export default Filter; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/Filters.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/Filters.tsx new file mode 100644 index 0000000000..41f54f8cac --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/Filters.tsx @@ -0,0 +1,32 @@ +import { useFiltersSelector, useReadOnly } from '@/application/database-yjs'; +import Filter from '@/components/database/components/filters/Filter'; +import Button from '@mui/material/Button'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as AddFilterSvg } from '$icons/16x/add.svg'; + +export function Filters() { + const filters = useFiltersSelector(); + const { t } = useTranslation(); + const readOnly = useReadOnly(); + + return ( + <> + {filters.map((filterId) => ( + + ))} + + + ); +} + +export default Filters; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/CheckboxFilterMenu.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/CheckboxFilterMenu.tsx new file mode 100644 index 0000000000..851e811499 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/CheckboxFilterMenu.tsx @@ -0,0 +1,33 @@ +import { CheckboxFilter, CheckboxFilterCondition } from '@/application/database-yjs'; +import FieldMenuTitle from '@/components/database/components/filters/filter-menu/FieldMenuTitle'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function CheckboxFilterMenu({ filter }: { filter: CheckboxFilter }) { + const { t } = useTranslation(); + + const conditions = useMemo( + () => [ + { + value: CheckboxFilterCondition.IsChecked, + text: t('grid.checkboxFilter.isChecked'), + }, + { + value: CheckboxFilterCondition.IsUnChecked, + text: t('grid.checkboxFilter.isUnchecked'), + }, + ], + [t] + ); + const selectedCondition = useMemo(() => { + return conditions.find((c) => c.value === filter.condition); + }, [filter.condition, conditions]); + + return ( +
+ +
+ ); +} + +export default CheckboxFilterMenu; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/ChecklistFilterMenu.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/ChecklistFilterMenu.tsx new file mode 100644 index 0000000000..5d6398b242 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/ChecklistFilterMenu.tsx @@ -0,0 +1,33 @@ +import { ChecklistFilter, ChecklistFilterCondition } from '@/application/database-yjs'; +import FieldMenuTitle from '@/components/database/components/filters/filter-menu/FieldMenuTitle'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function ChecklistFilterMenu({ filter }: { filter: ChecklistFilter }) { + const { t } = useTranslation(); + + const conditions = useMemo( + () => [ + { + value: ChecklistFilterCondition.IsComplete, + text: t('grid.checklistFilter.isComplete'), + }, + { + value: ChecklistFilterCondition.IsIncomplete, + text: t('grid.checklistFilter.isIncomplted'), + }, + ], + [t] + ); + const selectedCondition = useMemo(() => { + return conditions.find((c) => c.value === filter.condition); + }, [filter.condition, conditions]); + + return ( +
+ +
+ ); +} + +export default ChecklistFilterMenu; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/FieldMenuTitle.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/FieldMenuTitle.tsx new file mode 100644 index 0000000000..e5784b44f5 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/FieldMenuTitle.tsx @@ -0,0 +1,23 @@ +import { ReactComponent as ArrowDownSvg } from '$icons/16x/arrow_down.svg'; +import { FieldDisplay } from '@/components/database/components/field'; +import React from 'react'; + +function FieldMenuTitle({ fieldId, selectedConditionText }: { fieldId: string; selectedConditionText: string }) { + return ( +
+
+ +
+
+
+
+ {selectedConditionText} +
+ +
+
+
+ ); +} + +export default FieldMenuTitle; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/FilterMenu.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/FilterMenu.tsx new file mode 100644 index 0000000000..720dac3d3d --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/FilterMenu.tsx @@ -0,0 +1,39 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType, Filter, SelectOptionFilter, useFieldSelector } from '@/application/database-yjs'; +import CheckboxFilterMenu from './CheckboxFilterMenu'; +import ChecklistFilterMenu from './ChecklistFilterMenu'; +import MultiSelectOptionFilterMenu from './MultiSelectOptionFilterMenu'; +import NumberFilterMenu from './NumberFilterMenu'; +import SingleSelectOptionFilterMenu from './SingleSelectOptionFilterMenu'; +import TextFilterMenu from './TextFilterMenu'; +import React, { useMemo } from 'react'; + +export function FilterMenu({ filter }: { filter: Filter }) { + const { field } = useFieldSelector(filter?.fieldId); + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + + const menu = useMemo(() => { + if (!field) return null; + switch (fieldType) { + case FieldType.RichText: + case FieldType.URL: + return ; + case FieldType.Checkbox: + return ; + case FieldType.Checklist: + return ; + case FieldType.Number: + return ; + case FieldType.MultiSelect: + return ; + case FieldType.SingleSelect: + return ; + default: + return null; + } + }, [field, fieldType, filter]); + + return menu; +} + +export default FilterMenu; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/MultiSelectOptionFilterMenu.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/MultiSelectOptionFilterMenu.tsx new file mode 100644 index 0000000000..68def09bb8 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/MultiSelectOptionFilterMenu.tsx @@ -0,0 +1,56 @@ +import { SelectOptionFilter, SelectOptionFilterCondition } from '@/application/database-yjs'; +import { SelectOptionList } from '@/components/database/components/field/select-option'; +import FieldMenuTitle from './FieldMenuTitle'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function MultiSelectOptionFilterMenu({ filter }: { filter: SelectOptionFilter }) { + const { t } = useTranslation(); + const conditions = useMemo(() => { + return [ + { + value: SelectOptionFilterCondition.OptionIs, + text: t('grid.selectOptionFilter.is'), + }, + { + value: SelectOptionFilterCondition.OptionIsNot, + text: t('grid.selectOptionFilter.isNot'), + }, + { + value: SelectOptionFilterCondition.OptionContains, + text: t('grid.selectOptionFilter.contains'), + }, + { + value: SelectOptionFilterCondition.OptionDoesNotContain, + text: t('grid.selectOptionFilter.doesNotContain'), + }, + { + value: SelectOptionFilterCondition.OptionIsEmpty, + text: t('grid.selectOptionFilter.isEmpty'), + }, + { + value: SelectOptionFilterCondition.OptionIsNotEmpty, + text: t('grid.selectOptionFilter.isNotEmpty'), + }, + ]; + }, [t]); + + const selectedCondition = useMemo(() => { + return conditions.find((c) => c.value === filter.condition); + }, [filter.condition, conditions]); + + const displaySelectOptionList = useMemo(() => { + return ![SelectOptionFilterCondition.OptionIsEmpty, SelectOptionFilterCondition.OptionIsNotEmpty].includes( + filter.condition + ); + }, [filter.condition]); + + return ( +
+ + {displaySelectOptionList && } +
+ ); +} + +export default MultiSelectOptionFilterMenu; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/NumberFilterMenu.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/NumberFilterMenu.tsx new file mode 100644 index 0000000000..fdd8963ef2 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/NumberFilterMenu.tsx @@ -0,0 +1,74 @@ +import { NumberFilter, NumberFilterCondition, useReadOnly } from '@/application/database-yjs'; +import FieldMenuTitle from '@/components/database/components/filters/filter-menu/FieldMenuTitle'; +import { TextField } from '@mui/material'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function NumberFilterMenu({ filter }: { filter: NumberFilter }) { + const { t } = useTranslation(); + const readOnly = useReadOnly(); + const conditions = useMemo(() => { + return [ + { + value: NumberFilterCondition.Equal, + text: t('grid.numberFilter.equal'), + }, + { + value: NumberFilterCondition.NotEqual, + text: t('grid.numberFilter.notEqual'), + }, + { + value: NumberFilterCondition.GreaterThan, + text: t('grid.numberFilter.greaterThan'), + }, + { + value: NumberFilterCondition.LessThan, + text: t('grid.numberFilter.lessThan'), + }, + { + value: NumberFilterCondition.GreaterThanOrEqualTo, + text: t('grid.numberFilter.greaterThanOrEqualTo'), + }, + { + value: NumberFilterCondition.LessThanOrEqualTo, + text: t('grid.numberFilter.lessThanOrEqualTo'), + }, + { + value: NumberFilterCondition.NumberIsEmpty, + text: t('grid.textFilter.isEmpty'), + }, + { + value: NumberFilterCondition.NumberIsNotEmpty, + text: t('grid.textFilter.isNotEmpty'), + }, + ]; + }, [t]); + + const selectedCondition = useMemo(() => { + return conditions.find((c) => c.value === filter.condition); + }, [filter.condition, conditions]); + + const displayTextField = useMemo(() => { + return ![NumberFilterCondition.NumberIsEmpty, NumberFilterCondition.NumberIsNotEmpty].includes(filter.condition); + }, [filter.condition]); + + return ( +
+ + {displayTextField && ( + + )} +
+ ); +} + +export default NumberFilterMenu; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/SingleSelectOptionFilterMenu.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/SingleSelectOptionFilterMenu.tsx new file mode 100644 index 0000000000..217ad8d1ae --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/SingleSelectOptionFilterMenu.tsx @@ -0,0 +1,48 @@ +import { SelectOptionFilter, SelectOptionFilterCondition } from '@/application/database-yjs'; +import { SelectOptionList } from '@/components/database/components/field/select-option'; +import FieldMenuTitle from '@/components/database/components/filters/filter-menu/FieldMenuTitle'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function SingleSelectOptionFilterMenu({ filter }: { filter: SelectOptionFilter }) { + const { t } = useTranslation(); + const conditions = useMemo(() => { + return [ + { + value: SelectOptionFilterCondition.OptionIs, + text: t('grid.selectOptionFilter.is'), + }, + { + value: SelectOptionFilterCondition.OptionIsNot, + text: t('grid.selectOptionFilter.isNot'), + }, + { + value: SelectOptionFilterCondition.OptionIsEmpty, + text: t('grid.selectOptionFilter.isEmpty'), + }, + { + value: SelectOptionFilterCondition.OptionIsNotEmpty, + text: t('grid.selectOptionFilter.isNotEmpty'), + }, + ]; + }, [t]); + + const selectedCondition = useMemo(() => { + return conditions.find((c) => c.value === filter.condition); + }, [filter.condition, conditions]); + + const displaySelectOptionList = useMemo(() => { + return ![SelectOptionFilterCondition.OptionIsEmpty, SelectOptionFilterCondition.OptionIsNotEmpty].includes( + filter.condition + ); + }, [filter.condition]); + + return ( +
+ + {displaySelectOptionList && } +
+ ); +} + +export default SingleSelectOptionFilterMenu; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/TextFilterMenu.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/TextFilterMenu.tsx new file mode 100644 index 0000000000..f3ca7690af --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/TextFilterMenu.tsx @@ -0,0 +1,74 @@ +import { TextFilter, TextFilterCondition, useReadOnly } from '@/application/database-yjs'; +import FieldMenuTitle from '@/components/database/components/filters/filter-menu/FieldMenuTitle'; +import { TextField } from '@mui/material'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function TextFilterMenu({ filter }: { filter: TextFilter }) { + const { t } = useTranslation(); + const readOnly = useReadOnly(); + const conditions = useMemo(() => { + return [ + { + value: TextFilterCondition.TextContains, + text: t('grid.textFilter.contains'), + }, + { + value: TextFilterCondition.TextDoesNotContain, + text: t('grid.textFilter.doesNotContain'), + }, + { + value: TextFilterCondition.TextStartsWith, + text: t('grid.textFilter.startWith'), + }, + { + value: TextFilterCondition.TextEndsWith, + text: t('grid.textFilter.endsWith'), + }, + { + value: TextFilterCondition.TextIs, + text: t('grid.textFilter.is'), + }, + { + value: TextFilterCondition.TextIsNot, + text: t('grid.textFilter.isNot'), + }, + { + value: TextFilterCondition.TextIsEmpty, + text: t('grid.textFilter.isEmpty'), + }, + { + value: TextFilterCondition.TextIsNotEmpty, + text: t('grid.textFilter.isNotEmpty'), + }, + ]; + }, [t]); + + const selectedCondition = useMemo(() => { + return conditions.find((c) => c.value === filter.condition); + }, [filter.condition, conditions]); + + const displayTextField = useMemo(() => { + return ![TextFilterCondition.TextIsEmpty, TextFilterCondition.TextIsNotEmpty].includes(filter.condition); + }, [filter.condition]); + + return ( +
+ + {displayTextField && ( + + )} +
+ ); +} + +export default TextFilterMenu; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/index.ts b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/index.ts new file mode 100644 index 0000000000..fc54ea0f3a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/index.ts @@ -0,0 +1 @@ +export * from './FilterMenu'; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/index.ts b/frontend/appflowy_web_app/src/components/database/components/filters/index.ts new file mode 100644 index 0000000000..c7b59bcd2f --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/index.ts @@ -0,0 +1 @@ +export * from './Filters'; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/overview/DateFilterContentOverview.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/overview/DateFilterContentOverview.tsx new file mode 100644 index 0000000000..d3a30e1844 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/overview/DateFilterContentOverview.tsx @@ -0,0 +1,51 @@ +import { DateFilter, DateFilterCondition } from '@/application/database-yjs'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import dayjs from 'dayjs'; + +function DateFilterContentOverview({ filter }: { filter: DateFilter }) { + const { t } = useTranslation(); + + const value = useMemo(() => { + if (!filter.timestamp) return ''; + + let startStr = ''; + let endStr = ''; + + if (filter.start) { + const end = filter.end ?? filter.start; + const moreThanOneYear = dayjs.unix(end).diff(dayjs.unix(filter.start), 'year') > 1; + const format = moreThanOneYear ? 'MMM D, YYYY' : 'MMM D'; + + startStr = dayjs.unix(filter.start).format(format); + endStr = dayjs.unix(end).format(format); + } + + const timestamp = dayjs.unix(filter.timestamp).format('MMM D'); + + switch (filter.condition) { + case DateFilterCondition.DateIs: + return `: ${timestamp}`; + case DateFilterCondition.DateBefore: + return `: ${t('grid.dateFilter.choicechipPrefix.before')} ${timestamp}`; + case DateFilterCondition.DateAfter: + return `: ${t('grid.dateFilter.choicechipPrefix.after')} ${timestamp}`; + case DateFilterCondition.DateOnOrBefore: + return `: ${t('grid.dateFilter.choicechipPrefix.onOrBefore')} ${timestamp}`; + case DateFilterCondition.DateOnOrAfter: + return `: ${t('grid.dateFilter.choicechipPrefix.onOrAfter')} ${timestamp}`; + case DateFilterCondition.DateWithIn: + return `: ${startStr} - ${endStr}`; + case DateFilterCondition.DateIsEmpty: + return `: ${t('grid.dateFilter.choicechipPrefix.isEmpty')}`; + case DateFilterCondition.DateIsNotEmpty: + return `: ${t('grid.dateFilter.choicechipPrefix.isNotEmpty')}`; + default: + return ''; + } + }, [filter, t]); + + return <>{value}; +} + +export default DateFilterContentOverview; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/overview/FilterContentOverview.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/overview/FilterContentOverview.tsx new file mode 100644 index 0000000000..9f6d1ea188 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/overview/FilterContentOverview.tsx @@ -0,0 +1,59 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { + CheckboxFilterCondition, + ChecklistFilterCondition, + FieldType, + Filter, + SelectOptionFilter, + useFieldSelector, +} from '@/application/database-yjs'; +import DateFilterContentOverview from '@/components/database/components/filters/overview/DateFilterContentOverview'; +import NumberFilterContentOverview from '@/components/database/components/filters/overview/NumberFilterContentOverview'; +import SelectFilterContentOverview from '@/components/database/components/filters/overview/SelectFilterContentOverview'; +import TextFilterContentOverview from '@/components/database/components/filters/overview/TextFilterContentOverview'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export function FilterContentOverview({ filter }: { filter: Filter }) { + const { field } = useFieldSelector(filter?.fieldId); + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + const { t } = useTranslation(); + + return useMemo(() => { + if (!field) return null; + switch (fieldType) { + case FieldType.RichText: + case FieldType.URL: + return ; + case FieldType.Number: + return ; + case FieldType.DateTime: + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + return ; + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return ; + case FieldType.Checkbox: + return ( + <> + : {t('grid.checkboxFilter.choicechipPrefix.is')}{' '} + {filter.condition === CheckboxFilterCondition.IsChecked + ? t('grid.checkboxFilter.isChecked') + : t('grid.checkboxFilter.isUnchecked')} + + ); + case FieldType.Checklist: + return ( + <> + :{' '} + {filter.condition === ChecklistFilterCondition.IsComplete + ? t('grid.checklistFilter.isComplete') + : t('grid.checklistFilter.isIncomplted')} + + ); + default: + return null; + } + }, [field, fieldType, filter, t]); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/overview/NumberFilterContentOverview.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/overview/NumberFilterContentOverview.tsx new file mode 100644 index 0000000000..64864541e7 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/overview/NumberFilterContentOverview.tsx @@ -0,0 +1,38 @@ +import { NumberFilter, NumberFilterCondition } from '@/application/database-yjs'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function NumberFilterContentOverview({ filter }: { filter: NumberFilter }) { + const { t } = useTranslation(); + + const value = useMemo(() => { + if (!filter.content) { + return ''; + } + + const content = parseInt(filter.content); + + switch (filter.condition) { + case NumberFilterCondition.Equal: + return `= ${content}`; + case NumberFilterCondition.NotEqual: + return `!= ${content}`; + case NumberFilterCondition.GreaterThan: + return `> ${content}`; + case NumberFilterCondition.GreaterThanOrEqualTo: + return `>= ${content}`; + case NumberFilterCondition.LessThan: + return `< ${content}`; + case NumberFilterCondition.LessThanOrEqualTo: + return `<= ${content}`; + case NumberFilterCondition.NumberIsEmpty: + return t('grid.textFilter.isEmpty'); + case NumberFilterCondition.NumberIsNotEmpty: + return t('grid.textFilter.isNotEmpty'); + } + }, [filter.condition, filter.content, t]); + + return <>{value}; +} + +export default NumberFilterContentOverview; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/overview/SelectFilterContentOverview.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/overview/SelectFilterContentOverview.tsx new file mode 100644 index 0000000000..64e8ddc00c --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/overview/SelectFilterContentOverview.tsx @@ -0,0 +1,42 @@ +import { YDatabaseField } from '@/application/collab.type'; +import { + parseSelectOptionTypeOptions, + SelectOptionFilter, + SelectOptionFilterCondition, +} from '@/application/database-yjs'; +import React, { useMemo } from 'react'; + +import { useTranslation } from 'react-i18next'; + +function SelectFilterContentOverview({ filter, field }: { filter: SelectOptionFilter; field: YDatabaseField }) { + const typeOption = parseSelectOptionTypeOptions(field); + const { t } = useTranslation(); + const value = useMemo(() => { + if (!filter.optionIds?.length) return ''; + + const options = filter.optionIds + .map((optionId) => { + const option = typeOption?.options?.find((option) => option.id === optionId); + + return option?.name; + }) + .join(', '); + + switch (filter.condition) { + case SelectOptionFilterCondition.OptionIs: + return `: ${options}`; + case SelectOptionFilterCondition.OptionIsNot: + return `: ${t('grid.textFilter.choicechipPrefix.isNot')} ${options}`; + case SelectOptionFilterCondition.OptionIsEmpty: + return `: ${t('grid.textFilter.choicechipPrefix.isEmpty')}`; + case SelectOptionFilterCondition.OptionIsNotEmpty: + return `: ${t('grid.textFilter.choicechipPrefix.isNotEmpty')}`; + default: + return ''; + } + }, [filter.condition, filter.optionIds, t, typeOption?.options]); + + return <>{value}; +} + +export default SelectFilterContentOverview; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/overview/TextFilterContentOverview.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/overview/TextFilterContentOverview.tsx new file mode 100644 index 0000000000..fc03b39c96 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/overview/TextFilterContentOverview.tsx @@ -0,0 +1,33 @@ +import { TextFilter, TextFilterCondition } from '@/application/database-yjs'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function TextFilterContentOverview({ filter }: { filter: TextFilter }) { + const { t } = useTranslation(); + + const value = useMemo(() => { + if (!filter.content) return ''; + switch (filter.condition) { + case TextFilterCondition.TextContains: + case TextFilterCondition.TextIs: + return `: ${filter.content}`; + case TextFilterCondition.TextDoesNotContain: + case TextFilterCondition.TextIsNot: + return `: ${t('grid.textFilter.choicechipPrefix.isNot')} ${filter.content}`; + case TextFilterCondition.TextStartsWith: + return `: ${t('grid.textFilter.choicechipPrefix.startWith')} ${filter.content}`; + case TextFilterCondition.TextEndsWith: + return `: ${t('grid.textFilter.choicechipPrefix.endWith')} ${filter.content}`; + case TextFilterCondition.TextIsEmpty: + return `: ${t('grid.textFilter.choicechipPrefix.isEmpty')}`; + case TextFilterCondition.TextIsNotEmpty: + return `: ${t('grid.textFilter.choicechipPrefix.isNotEmpty')}`; + default: + return ''; + } + }, [t, filter]); + + return <>{value}; +} + +export default TextFilterContentOverview; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/overview/index.ts b/frontend/appflowy_web_app/src/components/database/components/filters/overview/index.ts new file mode 100644 index 0000000000..47e041409e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/overview/index.ts @@ -0,0 +1 @@ +export * from './FilterContentOverview'; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/package.json b/frontend/appflowy_web_app/src/components/database/components/filters/package.json new file mode 100644 index 0000000000..e56f3198c9 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/package.json @@ -0,0 +1,14 @@ +{ + "name": "filters", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/qinluhe/AppFlowy.git" + }, + "private": true +} diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-calculation-cell/CalculationCell.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-calculation-cell/CalculationCell.tsx new file mode 100644 index 0000000000..1ddb4e2d32 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-calculation-cell/CalculationCell.tsx @@ -0,0 +1,50 @@ +import { CalculationType } from '@/application/database-yjs/database.type'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export interface ICalculationCell { + value: string; + fieldId: string; + id: string; + type: CalculationType; +} + +export interface CalculationCellProps { + cell?: ICalculationCell; +} + +export function CalculationCell({ cell }: CalculationCellProps) { + const { t } = useTranslation(); + + const prefix = useMemo(() => { + if (!cell) return ''; + + switch (cell.type) { + case CalculationType.Average: + return t('grid.calculationTypeLabel.average'); + case CalculationType.Max: + return t('grid.calculationTypeLabel.max'); + case CalculationType.Count: + return t('grid.calculationTypeLabel.count'); + case CalculationType.Min: + return t('grid.calculationTypeLabel.min'); + case CalculationType.Sum: + return t('grid.calculationTypeLabel.sum'); + case CalculationType.CountEmpty: + return t('grid.calculationTypeLabel.countEmptyShort'); + case CalculationType.CountNonEmpty: + return t('grid.calculationTypeLabel.countNonEmptyShort'); + default: + return ''; + } + }, [cell, t]); + + return ( +
+ {prefix} + {cell?.value ?? ''} +
+ ); +} + +export default CalculationCell; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-calculation-cell/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/grid-calculation-cell/index.ts new file mode 100644 index 0000000000..9bf73af548 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-calculation-cell/index.ts @@ -0,0 +1 @@ +export * from './CalculationCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-cell/GridCell.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-cell/GridCell.tsx new file mode 100644 index 0000000000..0d3c7dfc11 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-cell/GridCell.tsx @@ -0,0 +1,62 @@ +import { FieldId, YjsDatabaseKey } from '@/application/collab.type'; +import { useCellSelector } from '@/application/database-yjs'; +import { useFieldSelector } from '@/application/database-yjs/selector'; +import { Cell } from '@/components/database/components/cell'; +import { CellProps, Cell as CellType } from '@/components/database/components/cell/cell.type'; +import { PrimaryCell } from '@/components/database/components/cell/primary'; +import React, { useEffect, useMemo, useRef } from 'react'; + +export interface GridCellProps { + rowId: string; + fieldId: FieldId; + columnIndex: number; + rowIndex: number; + onResize?: (rowIndex: number, columnIndex: number, size: { width: number; height: number }) => void; +} + +export function GridCell({ onResize, rowId, fieldId, columnIndex, rowIndex }: GridCellProps) { + const ref = useRef(null); + const { field } = useFieldSelector(fieldId); + const isPrimary = field?.get(YjsDatabaseKey.is_primary); + const cell = useCellSelector({ + rowId, + fieldId, + }); + + useEffect(() => { + const el = ref.current; + + if (!el || !cell) return; + + const observer = new ResizeObserver(() => { + onResize?.(rowIndex, columnIndex, { + width: el.offsetWidth, + height: el.offsetHeight, + }); + }); + + observer.observe(el); + + return () => { + observer.disconnect(); + }; + }, [columnIndex, onResize, rowIndex, cell]); + + const Component = useMemo(() => { + if (isPrimary) { + return PrimaryCell; + } + + return Cell; + }, [isPrimary]) as React.FC>; + + if (!field) return null; + + return ( +
+ +
+ ); +} + +export default GridCell; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-cell/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/grid-cell/index.ts new file mode 100644 index 0000000000..2b6d663ef5 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-cell/index.ts @@ -0,0 +1 @@ +export * from './GridCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/GridColumn.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/GridColumn.tsx new file mode 100644 index 0000000000..88c6fae84e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/GridColumn.tsx @@ -0,0 +1,35 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType } from '@/application/database-yjs/database.type'; +import { Column, useFieldSelector } from '@/application/database-yjs/selector'; +import { FieldTypeIcon } from '@/components/database/components/field'; +import React, { useMemo } from 'react'; + +export function GridColumn({ column, index }: { column: Column; index: number }) { + const { field } = useFieldSelector(column.fieldId); + const name = field?.get(YjsDatabaseKey.name); + const type = useMemo(() => { + const type = field?.get(YjsDatabaseKey.type); + + if (!type) return FieldType.RichText; + + return parseInt(type) as FieldType; + }, [field]); + + return ( +
+
+ +
+
{name}
+
+ ); +} + +export default GridColumn; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/index.ts new file mode 100644 index 0000000000..3c71a6b899 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/index.ts @@ -0,0 +1,2 @@ +export * from './GridColumn'; +export * from 'src/components/database/components/grid/grid-column/useRenderFields'; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/useRenderFields.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/useRenderFields.tsx new file mode 100644 index 0000000000..7bacc7b882 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/useRenderFields.tsx @@ -0,0 +1,70 @@ +import { FieldId } from '@/application/collab.type'; +import { FieldVisibility } from '@/application/database-yjs/database.type'; +import { useFieldsSelector } from '@/application/database-yjs/selector'; +import { useCallback, useMemo } from 'react'; + +export enum GridColumnType { + Action, + Field, + NewProperty, +} + +export type RenderColumn = { + type: GridColumnType; + visibility?: FieldVisibility; + fieldId?: FieldId; + width: number; + wrap?: boolean; +}; + +export function useRenderFields() { + const fields = useFieldsSelector(); + + const renderColumns = useMemo(() => { + const data = fields.map((column) => ({ + ...column, + type: GridColumnType.Field, + })); + + return [ + { + type: GridColumnType.Action, + width: 64, + }, + ...data, + { + type: GridColumnType.NewProperty, + width: 150, + }, + { + type: GridColumnType.Action, + width: 64, + }, + ].filter(Boolean) as RenderColumn[]; + }, [fields]); + + const columnWidth = useCallback( + (index: number, containerWidth: number) => { + const { type, width } = renderColumns[index]; + + if (type === GridColumnType.NewProperty) { + const totalWidth = renderColumns.reduce((acc, column) => acc + column.width, 0); + const remainingWidth = containerWidth - totalWidth; + + return remainingWidth > 0 ? remainingWidth + width : width; + } + + if (type === GridColumnType.Action && containerWidth < 800) { + return 16; + } + + return width; + }, + [renderColumns] + ); + + return { + fields: renderColumns, + columnWidth, + }; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-header/GridHeader.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-header/GridHeader.tsx new file mode 100644 index 0000000000..64d0c39117 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-header/GridHeader.tsx @@ -0,0 +1,73 @@ +import React, { useCallback, useEffect, useRef } from 'react'; +import { GridChildComponentProps, VariableSizeGrid } from 'react-window'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { GridColumnType, RenderColumn, GridColumn } from '../grid-column'; + +export interface GridHeaderProps { + onScrollLeft: (left: number) => void; + columnWidth: (index: number, totalWidth: number) => number; + columns: RenderColumn[]; + scrollLeft?: number; +} + +export const GridHeader = ({ scrollLeft, onScrollLeft, columnWidth, columns }: GridHeaderProps) => { + const ref = useRef(null); + const Cell = useCallback(({ columnIndex, style, data }: GridChildComponentProps) => { + const column = data[columnIndex]; + + // Placeholder for Action toolbar + if (!column || column.type === GridColumnType.Action) return
; + + if (column.type === GridColumnType.Field) { + return ( +
+ +
+ ); + } + + return
; + }, []); + + useEffect(() => { + if (ref.current) { + ref.current.scrollTo({ scrollLeft }); + } + }, [scrollLeft]); + + useEffect(() => { + if (ref.current) { + ref.current?.resetAfterIndices({ columnIndex: 0, rowIndex: 0 }); + } + }, [columns]); + + return ( +
+ + {({ height, width }: { height: number; width: number }) => { + return ( + 36} + rowCount={1} + columnCount={columns.length} + columnWidth={(index) => columnWidth(index, width)} + ref={ref} + onScroll={(props) => { + onScrollLeft(props.scrollLeft); + }} + itemData={columns} + style={{ overscrollBehavior: 'none' }} + > + {Cell} + + ); + }} + +
+ ); +}; + +export default GridHeader; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-header/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/grid-header/index.ts new file mode 100644 index 0000000000..44d8082bd7 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-header/index.ts @@ -0,0 +1 @@ +export * from './GridHeader'; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridCalculateRowCell.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridCalculateRowCell.tsx new file mode 100644 index 0000000000..4d7abb7a2c --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridCalculateRowCell.tsx @@ -0,0 +1,40 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { useDatabaseView } from '@/application/database-yjs'; +import { CalculationType } from '@/application/database-yjs/database.type'; +import { CalculationCell, ICalculationCell } from '../grid-calculation-cell'; +import React, { useEffect, useState } from 'react'; + +export interface GridCalculateRowCellProps { + fieldId: string; +} + +export function GridCalculateRowCell({ fieldId }: GridCalculateRowCellProps) { + const calculations = useDatabaseView()?.get(YjsDatabaseKey.calculations); + const [calculation, setCalculation] = useState(); + + useEffect(() => { + if (!calculations) return; + const observerHandle = () => { + calculations.forEach((calculation) => { + if (calculation.get(YjsDatabaseKey.field_id) === fieldId) { + setCalculation({ + id: calculation.get(YjsDatabaseKey.id), + fieldId: calculation.get(YjsDatabaseKey.field_id), + value: calculation.get(YjsDatabaseKey.calculation_value), + type: Number(calculation.get(YjsDatabaseKey.type)) as CalculationType, + }); + } + }); + }; + + observerHandle(); + calculations.observeDeep(observerHandle); + + return () => { + calculations.unobserveDeep(observerHandle); + }; + }, [calculations, fieldId]); + return ; +} + +export default GridCalculateRowCell; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridRowCell.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridRowCell.tsx new file mode 100644 index 0000000000..11f14135e3 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridRowCell.tsx @@ -0,0 +1,28 @@ +import { GridColumnType } from '../grid-column'; +import React from 'react'; +import GridCell from '../grid-cell/GridCell'; + +export interface GridRowCellProps { + rowId: string; + fieldId?: string; + type: GridColumnType; + columnIndex: number; + rowIndex: number; + onResize?: (rowIndex: number, columnIndex: number, size: { width: number; height: number }) => void; +} + +export function GridRowCell({ onResize, rowIndex, columnIndex, rowId, fieldId, type }: GridRowCellProps) { + if (type === GridColumnType.Field && fieldId) { + return ( + + ); + } + + if (type === GridColumnType.Action) { + return null; + } + + return null; +} + +export default GridRowCell; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/index.ts new file mode 100644 index 0000000000..365c3f467e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/index.ts @@ -0,0 +1,3 @@ +export * from './GridCalculateRowCell'; +export * from './GridRowCell'; +export * from './useRenderRows'; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/useRenderRows.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/useRenderRows.tsx new file mode 100644 index 0000000000..8b2e6597b8 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/useRenderRows.tsx @@ -0,0 +1,43 @@ +import { DEFAULT_ROW_HEIGHT, useReadOnly, useRowsSelector } from '@/application/database-yjs'; + +import { useMemo } from 'react'; + +export enum RenderRowType { + Row = 'row', + NewRow = 'new-row', + CalculateRow = 'calculate-row', +} + +export type RenderRow = { + type: RenderRowType; + rowId?: string; + height?: number; +}; + +export function useRenderRows() { + const rows = useRowsSelector(); + const readOnly = useReadOnly(); + + const renderRows = useMemo(() => { + return [ + ...rows.map((row) => ({ + type: RenderRowType.Row, + rowId: row.id, + height: row.height, + })), + + !readOnly && { + type: RenderRowType.NewRow, + height: DEFAULT_ROW_HEIGHT, + }, + { + type: RenderRowType.CalculateRow, + height: DEFAULT_ROW_HEIGHT, + }, + ].filter(Boolean) as RenderRow[]; + }, [readOnly, rows]); + + return { + rows: renderRows, + }; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx new file mode 100644 index 0000000000..ee39bfb957 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx @@ -0,0 +1,174 @@ +import { DEFAULT_ROW_HEIGHT } from '@/application/database-yjs/const'; +import { AFScroller } from '@/components/_shared/scroller'; +import { GridColumnType, RenderColumn } from '../grid-column'; +import { GridCalculateRowCell, GridRowCell, RenderRowType, useRenderRows } from '../grid-row'; +import React, { useCallback, useEffect, useRef } from 'react'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { GridChildComponentProps, VariableSizeGrid } from 'react-window'; + +export interface GridTableProps { + onScrollLeft: (left: number) => void; + columnWidth: (index: number, totalWidth: number) => number; + + columns: RenderColumn[]; + scrollLeft?: number; + viewId: string; +} + +export const GridTable = ({ scrollLeft, columnWidth, columns, onScrollLeft }: GridTableProps) => { + const ref = useRef(null); + const { rows } = useRenderRows(); + const rowHeights = useRef<{ [key: string]: number }>({}); + + useEffect(() => { + if (ref.current) { + console.log(ref.current, scrollLeft); + ref.current.scrollTo({ scrollLeft }); + } + }, [scrollLeft]); + + useEffect(() => { + if (ref.current) { + ref.current.resetAfterIndices({ columnIndex: 0, rowIndex: 0 }); + } + }, [columns]); + + const rowHeight = useCallback( + (index: number) => { + const row = rows[index]; + + if (!row || !row.rowId) return DEFAULT_ROW_HEIGHT; + + return rowHeights.current[row.rowId] || DEFAULT_ROW_HEIGHT; + }, + [rows] + ); + + const setRowHeight = useCallback( + (index: number, height: number) => { + const row = rows[index]; + const rowId = row.rowId; + + if (!row || !rowId) return; + const oldHeight = rowHeights.current[rowId]; + + rowHeights.current[rowId] = Math.max(oldHeight || DEFAULT_ROW_HEIGHT, height); + if (oldHeight !== height) { + ref.current?.resetAfterRowIndex(index, true); + } + }, + [rows] + ); + + const onResize = useCallback( + (rowIndex: number, columnIndex: number, size: { width: number; height: number }) => { + setRowHeight(rowIndex, size.height); + }, + [setRowHeight] + ); + + const getItemKey = useCallback( + ({ columnIndex, rowIndex }: { columnIndex: number; rowIndex: number }) => { + const row = rows[rowIndex]; + const column = columns[columnIndex]; + const fieldId = column.fieldId; + + if (row.type === RenderRowType.Row) { + if (fieldId) { + return `${row.rowId}:${fieldId}`; + } + + return `${rowIndex}:${columnIndex}`; + } + + if (fieldId) { + return `${row.type}:${fieldId}`; + } + + return `${rowIndex}:${columnIndex}`; + }, + [columns, rows] + ); + const Cell = useCallback( + ({ columnIndex, rowIndex, style, data }: GridChildComponentProps) => { + const row = data.rows[rowIndex]; + const column = data.columns[columnIndex] as RenderColumn; + + const classList = ['flex', 'items-center', 'overflow-hidden', 'grid-row-cell']; + + if (column.wrap) { + classList.push('wrap-cell'); + } else { + classList.push('whitespace-nowrap'); + } + + if (column.type === GridColumnType.Field) { + classList.push('border-b', 'border-l', 'border-line-divider', 'px-2'); + } + + if (column.type === GridColumnType.NewProperty) { + classList.push('border-b', 'border-line-divider', 'px-2'); + } + + if (row.type === RenderRowType.Row) { + return ( +
+ +
+ ); + } + + if (row.type === RenderRowType.CalculateRow && column.fieldId) { + return ( +
+ +
+ ); + } + + return
; + }, + [onResize] + ); + + return ( + + {({ height, width }: { height: number; width: number }) => ( + onScrollLeft(scrollLeft)} + rowCount={rows.length} + columnCount={columns.length} + columnWidth={(index) => columnWidth(index, width)} + rowHeight={rowHeight} + className={'grid-table'} + overscanRowCount={5} + overscanColumnCount={5} + style={{ + overscrollBehavior: 'none', + }} + itemKey={getItemKey} + itemData={{ columns, rows }} + outerElementType={AFScroller} + > + {Cell} + + )} + + ); +}; + +export default GridTable; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/index.ts new file mode 100644 index 0000000000..49518fa391 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/index.ts @@ -0,0 +1 @@ +export * from './GridTable'; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/index.ts new file mode 100644 index 0000000000..2e9a6988f4 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/index.ts @@ -0,0 +1,3 @@ +export * from './grid-table'; +export * from './grid-header'; +export * from './grid-column'; diff --git a/frontend/appflowy_web_app/src/components/database/components/header/DatabaseHeader.tsx b/frontend/appflowy_web_app/src/components/database/components/header/DatabaseHeader.tsx new file mode 100644 index 0000000000..f84da67aa2 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/header/DatabaseHeader.tsx @@ -0,0 +1,11 @@ +import { usePageInfo } from '@/components/_shared/page/usePageInfo'; +import Title from './Title'; +import React from 'react'; + +export function DatabaseHeader({ viewId }: { viewId: string }) { + const { name, icon } = usePageInfo(viewId); + + return ; +} + +export default DatabaseHeader; diff --git a/frontend/appflowy_web_app/src/components/database/components/header/DatabaseRowHeader.tsx b/frontend/appflowy_web_app/src/components/database/components/header/DatabaseRowHeader.tsx new file mode 100644 index 0000000000..cbbfcaee49 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/header/DatabaseRowHeader.tsx @@ -0,0 +1,16 @@ +import { useCellSelector, usePrimaryFieldId, useRowMetaSelector } from '@/application/database-yjs'; +import Title from '@/components/database/components/header/Title'; +import React from 'react'; + +function DatabaseRowHeader({ rowId }: { rowId: string }) { + const fieldId = usePrimaryFieldId() || ''; + const meta = useRowMetaSelector(rowId); + const cell = useCellSelector({ + rowId, + fieldId, + }); + + return <Title icon={meta?.icon} name={cell?.data as string} />; +} + +export default DatabaseRowHeader; diff --git a/frontend/appflowy_web_app/src/components/database/components/header/Title.tsx b/frontend/appflowy_web_app/src/components/database/components/header/Title.tsx new file mode 100644 index 0000000000..8bb1979196 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/header/Title.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +export function Title({ icon, name }: { icon?: string; name?: string }) { + const { t } = useTranslation(); + + return ( + <div className={'flex w-full flex-col py-4'}> + <div className={'flex w-full items-center px-16 max-md:px-4'}> + <div className={'flex items-center gap-2 text-3xl'}> + <div>{icon}</div> + <div className={'font-bold'}>{name || t('document.title.placeholder')}</div> + </div> + </div> + </div> + ); +} + +export default Title; diff --git a/frontend/appflowy_web_app/src/components/database/components/header/index.ts b/frontend/appflowy_web_app/src/components/database/components/header/index.ts new file mode 100644 index 0000000000..452eceafe1 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/header/index.ts @@ -0,0 +1,2 @@ +export * from './DatabaseHeader'; +export * from './DatabaseRowHeader'; diff --git a/frontend/appflowy_web_app/src/components/database/components/property/Property.tsx b/frontend/appflowy_web_app/src/components/database/components/property/Property.tsx new file mode 100644 index 0000000000..f194a65a72 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/property/Property.tsx @@ -0,0 +1,78 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType, useCellSelector, useFieldSelector } from '@/application/database-yjs'; +import { Cell as CellType, CellProps, TextCell } from '@/components/database/components/cell/cell.type'; +import { CheckboxCell } from '@/components/database/components/cell/checkbox'; +import { RowCreateModifiedTime } from '@/components/database/components/cell/created-modified'; +import { DateTimeCell } from '@/components/database/components/cell/date'; +import { NumberCell } from '@/components/database/components/cell/number'; +import { RelationCell } from '@/components/database/components/cell/relation'; +import { SelectOptionCell } from '@/components/database/components/cell/select-option'; +import { UrlCell } from '@/components/database/components/cell/url'; +import PropertyWrapper from '@/components/database/components/property/PropertyWrapper'; +import { TextProperty } from '@/components/database/components/property/text'; +import { ChecklistProperty } from 'src/components/database/components/property/cheklist'; + +import React, { FC, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export function Property({ fieldId, rowId }: { fieldId: string; rowId: string }) { + const cell = useCellSelector({ + fieldId, + rowId, + }); + + const { field } = useFieldSelector(fieldId); + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + + const { t } = useTranslation(); + const Component = useMemo(() => { + switch (fieldType) { + case FieldType.URL: + return UrlCell; + case FieldType.Number: + return NumberCell; + case FieldType.Checkbox: + return CheckboxCell; + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return SelectOptionCell; + case FieldType.DateTime: + return DateTimeCell; + case FieldType.Checklist: + return ChecklistProperty; + case FieldType.Relation: + return RelationCell; + default: + return TextProperty; + } + }, [fieldType]) as FC<CellProps<CellType>>; + + const style = useMemo( + () => ({ + fontSize: '12px', + }), + [] + ); + + if (fieldType === FieldType.RichText) { + return <TextProperty cell={cell as TextCell} fieldId={fieldId} rowId={rowId} />; + } + + if (fieldType === FieldType.CreatedTime || fieldType === FieldType.LastEditedTime) { + const attrName = fieldType === FieldType.CreatedTime ? YjsDatabaseKey.created_at : YjsDatabaseKey.last_modified; + + return ( + <PropertyWrapper fieldId={fieldId}> + <RowCreateModifiedTime style={style} rowId={rowId} fieldId={fieldId} attrName={attrName} /> + </PropertyWrapper> + ); + } + + return ( + <PropertyWrapper fieldId={fieldId}> + <Component cell={cell} style={style} placeholder={t('grid.row.textPlaceholder')} fieldId={fieldId} rowId={rowId} /> + </PropertyWrapper> + ); +} + +export default Property; diff --git a/frontend/appflowy_web_app/src/components/database/components/property/PropertyWrapper.tsx b/frontend/appflowy_web_app/src/components/database/components/property/PropertyWrapper.tsx new file mode 100644 index 0000000000..9e970abaee --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/property/PropertyWrapper.tsx @@ -0,0 +1,15 @@ +import { FieldDisplay } from '@/components/database/components/field'; +import React from 'react'; + +function PropertyWrapper({ fieldId, children }: { fieldId: string; children: React.ReactNode }) { + return ( + <div className={'flex min-h-[28px] w-full gap-2'}> + <div className={'property-label flex h-[28px] w-[30%] items-center'}> + <FieldDisplay fieldId={fieldId} /> + </div> + <div className={'flex flex-1 flex-wrap pr-1'}>{children}</div> + </div> + ); +} + +export default PropertyWrapper; diff --git a/frontend/appflowy_web_app/src/components/database/components/property/cheklist/ChecklistProperty.tsx b/frontend/appflowy_web_app/src/components/database/components/property/cheklist/ChecklistProperty.tsx new file mode 100644 index 0000000000..6e47ada5fe --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/property/cheklist/ChecklistProperty.tsx @@ -0,0 +1,34 @@ +import { parseChecklistData } from '@/application/database-yjs'; +import { CellProps, ChecklistCell as CellType } from '@/components/database/components/cell/cell.type'; +import { ChecklistCell } from '@/components/database/components/cell/checklist'; +import React, { useMemo } from 'react'; +import { ReactComponent as CheckboxCheckSvg } from '$icons/16x/check_filled.svg'; +import { ReactComponent as CheckboxUncheckSvg } from '$icons/16x/uncheck.svg'; + +export function ChecklistProperty(props: CellProps<CellType>) { + const { cell } = props; + const data = useMemo(() => { + return parseChecklistData(cell?.data ?? ''); + }, [cell?.data]); + + const options = data?.options; + const selectedOptions = data?.selectedOptionIds; + + return ( + <div className={'flex w-full flex-col gap-2'}> + <ChecklistCell {...props} /> + {options?.map((option) => { + const isSelected = selectedOptions?.includes(option.id); + + return ( + <div key={option.id} className={'flex items-center gap-2 text-xs font-medium'}> + {isSelected ? <CheckboxCheckSvg className={'h-4 w-4'} /> : <CheckboxUncheckSvg className={'h-4 w-4'} />} + <div>{option.name}</div> + </div> + ); + })} + </div> + ); +} + +export default ChecklistProperty; diff --git a/frontend/appflowy_web_app/src/components/database/components/property/cheklist/index.ts b/frontend/appflowy_web_app/src/components/database/components/property/cheklist/index.ts new file mode 100644 index 0000000000..413d3c884b --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/property/cheklist/index.ts @@ -0,0 +1 @@ +export * from './ChecklistProperty'; diff --git a/frontend/appflowy_web_app/src/components/database/components/property/index.ts b/frontend/appflowy_web_app/src/components/database/components/property/index.ts new file mode 100644 index 0000000000..1a4ad04e85 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/property/index.ts @@ -0,0 +1 @@ +export * from './Property'; diff --git a/frontend/appflowy_web_app/src/components/database/components/property/text/TextProperty.tsx b/frontend/appflowy_web_app/src/components/database/components/property/text/TextProperty.tsx new file mode 100644 index 0000000000..64589c0ea3 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/property/text/TextProperty.tsx @@ -0,0 +1,29 @@ +import { CellProps, TextCell } from '@/components/database/components/cell/cell.type'; +import { TextField } from '@mui/material'; +import React from 'react'; + +export function TextProperty({ cell }: CellProps<TextCell>) { + return ( + <TextField + value={cell?.data} + inputProps={{ + readOnly: true, + }} + fullWidth + size={'small'} + sx={{ + '& .MuiInputBase-root': { + fontSize: '0.875rem', + borderRadius: '8px', + }, + + '& .MuiInputBase-input': { + padding: '4px 8px', + fontWeight: 500, + }, + }} + /> + ); +} + +export default TextProperty; diff --git a/frontend/appflowy_web_app/src/components/database/components/property/text/index.ts b/frontend/appflowy_web_app/src/components/database/components/property/text/index.ts new file mode 100644 index 0000000000..c10e4ed3d0 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/property/text/index.ts @@ -0,0 +1 @@ +export * from './TextProperty'; diff --git a/frontend/appflowy_web_app/src/components/database/components/sorts/Sort.tsx b/frontend/appflowy_web_app/src/components/database/components/sorts/Sort.tsx new file mode 100644 index 0000000000..37575224ac --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/sorts/Sort.tsx @@ -0,0 +1,20 @@ +import { useSortSelector } from '@/application/database-yjs'; +import SortCondition from '@/components/database/components/sorts/SortCondition'; +import React from 'react'; +import { FieldDisplay } from 'src/components/database/components/field'; + +function Sort({ sortId }: { sortId: string }) { + const sort = useSortSelector(sortId); + + if (!sort) return null; + return ( + <div className={'flex items-center gap-1.5'}> + <div className={'w-[120px] max-w-[250px] overflow-hidden rounded-full border border-line-divider py-1 px-2 '}> + <FieldDisplay fieldId={sort.fieldId} /> + </div> + <SortCondition sort={sort} /> + </div> + ); +} + +export default Sort; diff --git a/frontend/appflowy_web_app/src/components/database/components/sorts/SortCondition.tsx b/frontend/appflowy_web_app/src/components/database/components/sorts/SortCondition.tsx new file mode 100644 index 0000000000..78457da1ca --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/sorts/SortCondition.tsx @@ -0,0 +1,30 @@ +import { Sort } from '@/application/database-yjs'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as ArrowDownSvg } from '$icons/16x/arrow_down.svg'; + +function SortCondition({ sort }: { sort: Sort }) { + const condition = sort.condition; + const { t } = useTranslation(); + const conditionText = useMemo(() => { + switch (condition) { + case 0: + return t('grid.sort.ascending'); + case 1: + return t('grid.sort.descending'); + } + }, [condition, t]); + + return ( + <div + className={ + 'flex w-[120px] max-w-[250px] items-center justify-between gap-1.5 rounded-full border border-line-divider py-1 px-2 font-medium ' + } + > + <span className={'text-xs'}>{conditionText}</span> + <ArrowDownSvg className={'text-text-caption'} /> + </div> + ); +} + +export default SortCondition; diff --git a/frontend/appflowy_web_app/src/components/database/components/sorts/SortList.tsx b/frontend/appflowy_web_app/src/components/database/components/sorts/SortList.tsx new file mode 100644 index 0000000000..a657b4a0b9 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/sorts/SortList.tsx @@ -0,0 +1,17 @@ +import { useSortsSelector } from '@/application/database-yjs'; +import Sort from '@/components/database/components/sorts/Sort'; +import React from 'react'; + +function SortList() { + const sorts = useSortsSelector(); + + return ( + <div className={'flex w-fit flex-col gap-2 p-2'}> + {sorts.map((sortId) => ( + <Sort sortId={sortId} key={sortId} /> + ))} + </div> + ); +} + +export default SortList; diff --git a/frontend/appflowy_web_app/src/components/database/components/sorts/Sorts.tsx b/frontend/appflowy_web_app/src/components/database/components/sorts/Sorts.tsx new file mode 100644 index 0000000000..a00aeea20c --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/sorts/Sorts.tsx @@ -0,0 +1,43 @@ +import { useSortsSelector } from '@/application/database-yjs'; +import { Popover } from '@/components/_shared/popover'; +import SortList from '@/components/database/components/sorts/SortList'; +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as SortSvg } from '$icons/16x/sort_ascending.svg'; +import { ReactComponent as ArrowDownSvg } from '$icons/16x/arrow_down.svg'; + +export function Sorts() { + const { t } = useTranslation(); + const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); + const open = Boolean(anchorEl); + const sorts = useSortsSelector(); + + if (sorts.length === 0) return null; + return ( + <> + <div + onClick={(e) => { + setAnchorEl(e.currentTarget); + }} + className='flex cursor-pointer items-center gap-1 rounded-full border border-line-divider px-2 py-1 text-xs hover:border-fill-default hover:text-fill-default hover:shadow-sm' + > + <SortSvg /> + {t('grid.settings.sort')} + <ArrowDownSvg /> + </div> + {open && ( + <Popover + open={open} + anchorEl={anchorEl} + onClose={() => { + setAnchorEl(null); + }} + > + <SortList /> + </Popover> + )} + </> + ); +} + +export default Sorts; diff --git a/frontend/appflowy_web_app/src/components/database/components/sorts/index.ts b/frontend/appflowy_web_app/src/components/database/components/sorts/index.ts new file mode 100644 index 0000000000..467acd9081 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/sorts/index.ts @@ -0,0 +1 @@ +export * from './Sorts'; diff --git a/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx b/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx new file mode 100644 index 0000000000..8e33c7eaba --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx @@ -0,0 +1,97 @@ +import { ViewLayout, YjsFolderKey, YView } from '@/application/collab.type'; +import { useFolderContext } from '@/application/folder-yjs'; +import { useId } from '@/components/_shared/context-provider/IdProvider'; +import { DatabaseActions } from '@/components/database/components/conditions'; +import { forwardRef, FunctionComponent, SVGProps, useCallback, useEffect, useMemo } from 'react'; +import { ViewTabs, ViewTab } from './ViewTabs'; +import { useTranslation } from 'react-i18next'; + +import { ReactComponent as GridSvg } from '$icons/16x/grid.svg'; +import { ReactComponent as BoardSvg } from '$icons/16x/board.svg'; +import { ReactComponent as CalendarSvg } from '$icons/16x/date.svg'; +import { ReactComponent as DocumentSvg } from '$icons/16x/document.svg'; + +export interface DatabaseTabBarProps { + viewIds: string[]; + selectedViewId?: string; + setSelectedViewId?: (viewId: string) => void; +} + +const DatabaseIcons: { + [key in ViewLayout]: FunctionComponent<SVGProps<SVGSVGElement> & { title?: string | undefined }>; +} = { + [ViewLayout.Document]: DocumentSvg, + [ViewLayout.Grid]: GridSvg, + [ViewLayout.Board]: BoardSvg, + [ViewLayout.Calendar]: CalendarSvg, +}; + +export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>( + ({ viewIds, selectedViewId, setSelectedViewId }, ref) => { + const objectId = useId().objectId; + const { t } = useTranslation(); + const folder = useFolderContext(); + const handleChange = (_: React.SyntheticEvent, newValue: string) => { + setSelectedViewId?.(newValue); + }; + + useEffect(() => { + if (selectedViewId === undefined) { + setSelectedViewId?.(objectId); + } + }, [selectedViewId, setSelectedViewId, objectId]); + const isSelected = useMemo(() => viewIds.some((viewId) => viewId === selectedViewId), [viewIds, selectedViewId]); + + const getFolderView = useCallback( + (viewId: string) => { + if (!folder) return null; + return folder.get(YjsFolderKey.views)?.get(viewId) as YView | null; + }, + [folder] + ); + + if (viewIds.length === 0) return null; + return ( + <div + ref={ref} + className='mx-16 -mb-[0.5px] flex items-center overflow-hidden border-b border-line-divider text-text-title max-md:mx-4' + > + <div + style={{ + width: 'calc(100% - 120px)', + }} + className='flex items-center ' + > + <ViewTabs + scrollButtons={false} + variant='scrollable' + allowScrollButtonsMobile + value={isSelected ? selectedViewId : objectId} + onChange={handleChange} + > + {viewIds.map((viewId) => { + const view = getFolderView(viewId); + + if (!view) return null; + const layout = Number(view.get(YjsFolderKey.layout)) as ViewLayout; + const Icon = DatabaseIcons[layout]; + const name = view.get(YjsFolderKey.name); + + return ( + <ViewTab + key={viewId} + icon={<Icon className={'h-4 w-4'} />} + iconPosition='start' + color='inherit' + label={<span className={'max-w-[120px] truncate'}>{name || t('grid.title.placeholder')}</span>} + value={viewId} + /> + ); + })} + </ViewTabs> + </div> + <DatabaseActions /> + </div> + ); + } +); diff --git a/frontend/appflowy_web_app/src/components/database/components/tabs/TextButton.tsx b/frontend/appflowy_web_app/src/components/database/components/tabs/TextButton.tsx new file mode 100644 index 0000000000..7bbf91cf65 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/tabs/TextButton.tsx @@ -0,0 +1,18 @@ +import { Button, ButtonProps, styled } from '@mui/material'; + +export const TextButton = styled((props: ButtonProps) => ( + <Button + {...props} + sx={{ + '&.MuiButton-colorInherit': { + color: 'var(--text-caption)', + }, + }} + /> +))<ButtonProps>(() => ({ + padding: '4px 6px', + fontSize: '0.75rem', + lineHeight: '1rem', + fontWeight: 400, + minWidth: 'unset', +})); diff --git a/frontend/appflowy_web_app/src/components/database/components/tabs/ViewTabs.tsx b/frontend/appflowy_web_app/src/components/database/components/tabs/ViewTabs.tsx new file mode 100644 index 0000000000..a9c58e42c7 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/tabs/ViewTabs.tsx @@ -0,0 +1,52 @@ +import { styled, Tab, TabProps, Tabs, TabsProps } from '@mui/material'; +import { HTMLAttributes } from 'react'; + +export const ViewTabs = styled((props: TabsProps) => <Tabs {...props} />)({ + minHeight: '28px', + + '& .MuiTabs-scroller': { + paddingBottom: '2px', + }, +}); + +export const ViewTab = styled((props: TabProps) => <Tab disableRipple {...props} />)({ + padding: '0 12px', + minHeight: '24px', + fontSize: '12px', + minWidth: 'unset', + margin: '4px 0', + borderRadius: 0, + '&:hover': { + backgroundColor: 'transparent !important', + color: 'inherit', + }, + '&.Mui-selected': { + color: 'inherit', + backgroundColor: 'transparent', + }, +}); + +interface TabPanelProps extends HTMLAttributes<HTMLDivElement> { + children?: React.ReactNode; + index: number; + value: number; +} + +export function TabPanel(props: TabPanelProps) { + const { children, value, index, ...other } = props; + + const isActivated = value === index; + + return ( + <div + role='tabpanel' + hidden={!isActivated} + id={`full-width-tabpanel-${index}`} + aria-labelledby={`full-width-tab-${index}`} + dir={'ltr'} + {...other} + > + {isActivated ? children : null} + </div> + ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/tabs/index.ts b/frontend/appflowy_web_app/src/components/database/components/tabs/index.ts new file mode 100644 index 0000000000..8d2722a633 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/tabs/index.ts @@ -0,0 +1,2 @@ +export * from './DatabaseTabs'; +export * from './ViewTabs'; diff --git a/frontend/appflowy_web_app/src/components/database/grid/Grid.tsx b/frontend/appflowy_web_app/src/components/database/grid/Grid.tsx new file mode 100644 index 0000000000..954f9cd7c3 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/grid/Grid.tsx @@ -0,0 +1,44 @@ +import { RowsContext, useDatabase, useRowOrdersSelector, useViewId } from '@/application/database-yjs'; +import { useRenderFields, GridHeader, GridTable } from '@/components/database/components/grid'; +import { CircularProgress } from '@mui/material'; +import React, { useState } from 'react'; + +export function Grid() { + const database = useDatabase(); + const viewId = useViewId() || ''; + const [scrollLeft, setScrollLeft] = useState(0); + + const { fields, columnWidth } = useRenderFields(); + const rowOrders = useRowOrdersSelector(); + + if (!database || !rowOrders) { + return ( + <div className={'flex w-full flex-1 flex-col items-center justify-center'}> + <CircularProgress /> + </div> + ); + } + + return ( + <RowsContext.Provider + value={{ + rowOrders, + }} + > + <div className={'flex w-full flex-1 flex-col'}> + <GridHeader scrollLeft={scrollLeft} columnWidth={columnWidth} columns={fields} onScrollLeft={setScrollLeft} /> + <div className={'grid-scroll-table w-full flex-1'}> + <GridTable + viewId={viewId} + scrollLeft={scrollLeft} + columnWidth={columnWidth} + columns={fields} + onScrollLeft={setScrollLeft} + /> + </div> + </div> + </RowsContext.Provider> + ); +} + +export default Grid; diff --git a/frontend/appflowy_web_app/src/components/database/grid/index.ts b/frontend/appflowy_web_app/src/components/database/grid/index.ts new file mode 100644 index 0000000000..762542e7cb --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/grid/index.ts @@ -0,0 +1 @@ +export * from './Grid'; diff --git a/frontend/appflowy_web_app/src/components/database/index.ts b/frontend/appflowy_web_app/src/components/database/index.ts new file mode 100644 index 0000000000..8ef9c34dc1 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/index.ts @@ -0,0 +1,3 @@ +import { lazy } from 'react'; + +export const Database = lazy(() => import('./Database')); diff --git a/frontend/appflowy_web_app/src/components/document/Document.tsx b/frontend/appflowy_web_app/src/components/document/Document.tsx index 82e20bed4d..219b76e7e5 100644 --- a/frontend/appflowy_web_app/src/components/document/Document.tsx +++ b/frontend/appflowy_web_app/src/components/document/Document.tsx @@ -41,7 +41,7 @@ export const Document = () => { <DocumentHeader doc={doc} viewId={documentId} /> <div className={'flex w-full justify-center'}> <div className={'max-w-screen w-[964px] min-w-0'}> - <Editor doc={doc} readOnly={true} /> + <Editor doc={doc} readOnly={true} includeRoot={true} /> </div> </div> </div> @@ -51,3 +51,5 @@ export const Document = () => { </> ); }; + +export default Document; diff --git a/frontend/appflowy_web_app/src/components/editor/CollaborativeEditor.tsx b/frontend/appflowy_web_app/src/components/editor/CollaborativeEditor.tsx index 83e36b88f8..7e5ebbc28c 100644 --- a/frontend/appflowy_web_app/src/components/editor/CollaborativeEditor.tsx +++ b/frontend/appflowy_web_app/src/components/editor/CollaborativeEditor.tsx @@ -1,9 +1,10 @@ -import { YjsFolderKey } from '@/application/collab.type'; +import { CollabOrigin, YjsFolderKey } from '@/application/collab.type'; import { useViewSelector } from '@/application/folder-yjs'; import { withYjs, YjsEditor } from '@/application/slate-yjs/plugins/withYjs'; import { useId } from '@/components/_shared/context-provider/IdProvider'; import { CustomEditor } from '@/components/editor/command'; import EditorEditable from '@/components/editor/Editable'; +import { useEditorContext } from '@/components/editor/EditorContext'; import { withPlugins } from '@/components/editor/plugins'; import React, { useEffect, useMemo, useState } from 'react'; import { createEditor, Descendant } from 'slate'; @@ -12,12 +13,27 @@ import * as Y from 'yjs'; const defaultInitialValue: Descendant[] = []; -function CollaborativeEditor({ doc }: { doc: Y.Doc }) { - const editor = useMemo(() => doc && (withPlugins(withReact(withYjs(createEditor(), doc))) as YjsEditor), [doc]); - const [connected, setIsConnected] = useState(false); +function CollaborativeEditor({ doc, includeRoot = true }: { doc: Y.Doc; includeRoot?: boolean }) { const viewId = useId()?.objectId || ''; const { view } = useViewSelector(viewId); - const title = view?.get(YjsFolderKey.name); + const title = includeRoot ? view?.get(YjsFolderKey.name) : undefined; + const context = useEditorContext(); + // if readOnly, collabOrigin is Local, otherwise RemoteSync + const localOrigin = context.readOnly ? CollabOrigin.Local : CollabOrigin.LocalSync; + const editor = useMemo( + () => + doc && + (withPlugins( + withReact( + withYjs(createEditor(), doc, { + localOrigin, + includeRoot, + }) + ) + ) as YjsEditor), + [doc, localOrigin, includeRoot] + ); + const [connected, setIsConnected] = useState(false); useEffect(() => { if (!editor) return; @@ -30,8 +46,8 @@ function CollaborativeEditor({ doc }: { doc: Y.Doc }) { }, [editor]); useEffect(() => { - if (!editor || !connected) return; - CustomEditor.setDocumentTitle(editor, title || ''); + if (!editor || !connected || title === undefined) return; + CustomEditor.setDocumentTitle(editor, title); }, [editor, title, connected]); return ( diff --git a/frontend/appflowy_web_app/src/components/editor/Editor.cy.tsx b/frontend/appflowy_web_app/src/components/editor/Editor.cy.tsx index 12c2feb435..766e1e6d29 100644 --- a/frontend/appflowy_web_app/src/components/editor/Editor.cy.tsx +++ b/frontend/appflowy_web_app/src/components/editor/Editor.cy.tsx @@ -1,6 +1,6 @@ import { YDoc } from '@/application/collab.type'; import { DocumentTest } from '@/../cypress/support/document'; -import { applyDocument } from '@/application/ydoc/apply'; +import { applyYDoc } from '@/application/ydoc/apply'; import React from 'react'; import * as Y from 'yjs'; import { Editor } from './Editor'; @@ -20,7 +20,7 @@ describe('<Editor />', () => { const doc = new Y.Doc(); const state = new Uint8Array(docJson.data.doc_state); - applyDocument(doc, state); + applyYDoc(doc, state); renderEditor(doc); }); }); diff --git a/frontend/appflowy_web_app/src/components/editor/Editor.tsx b/frontend/appflowy_web_app/src/components/editor/Editor.tsx index 7777973061..601b5d44b2 100644 --- a/frontend/appflowy_web_app/src/components/editor/Editor.tsx +++ b/frontend/appflowy_web_app/src/components/editor/Editor.tsx @@ -4,10 +4,18 @@ import { EditorContextProvider } from '@/components/editor/EditorContext'; import React from 'react'; import './editor.scss'; -export const Editor = ({ readOnly, doc }: { readOnly: boolean; doc: YDoc }) => { +export const Editor = ({ + readOnly, + doc, + includeRoot = true, +}: { + readOnly: boolean; + doc: YDoc; + includeRoot?: boolean; +}) => { return ( <EditorContextProvider readOnly={readOnly}> - <CollaborativeEditor doc={doc} /> + <CollaborativeEditor doc={doc} includeRoot={includeRoot} /> </EditorContextProvider> ); }; diff --git a/frontend/appflowy_web_app/src/components/editor/command/index.ts b/frontend/appflowy_web_app/src/components/editor/command/index.ts index dc4668760c..d4ee1a6e28 100644 --- a/frontend/appflowy_web_app/src/components/editor/command/index.ts +++ b/frontend/appflowy_web_app/src/components/editor/command/index.ts @@ -24,7 +24,10 @@ export const CustomEditor = { } if (node.type === InlineBlockType.Mention && (node.data as Mention)?.type === MentionType.Date) { - return renderDate((node.data as Mention).date || ''); + const date = (node.data as Mention).date || ''; + const isUnix = date?.length === 10; + + return renderDate(date, 'MMM DD, YYYY', isUnix); } } diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted_list/BulletedList.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted-list/BulletedList.tsx similarity index 100% rename from frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted_list/BulletedList.tsx rename to frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted-list/BulletedList.tsx diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted_list/BulletedListIcon.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted-list/BulletedListIcon.tsx similarity index 100% rename from frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted_list/BulletedListIcon.tsx rename to frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted-list/BulletedListIcon.tsx diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted_list/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted-list/index.ts similarity index 100% rename from frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted_list/index.ts rename to frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted-list/index.ts diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/callout/Callout.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/callout/Callout.tsx index a60255f951..4b4f02ea6b 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/callout/Callout.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/callout/Callout.tsx @@ -10,7 +10,10 @@ export const Callout = memo( <CalloutIcon node={node} /> </div> <div ref={ref} className={`${attributes.className ?? ''} w-full bg-bg-body py-2`}> - <div {...attributes} className={`flex w-full flex-col rounded bg-content-blue-50 py-2 pl-10`}> + <div + {...attributes} + className={`flex w-full flex-col rounded border border-line-divider bg-fill-list-active py-2 pl-10`} + > {children} </div> </div> diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/callout/CalloutIcon.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/callout/CalloutIcon.tsx index 6f4f9b53ed..0c72c971d8 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/callout/CalloutIcon.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/callout/CalloutIcon.tsx @@ -6,7 +6,7 @@ function CalloutIcon({ node }: { node: CalloutNode }) { return ( <> - <span contentEditable={false} ref={ref} className={`h-8 w-8 p-1`}> + <span contentEditable={false} ref={ref} className={`flex h-8 w-8 items-center p-1`}> {node.data.icon} </span> </> diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/Code.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/Code.tsx index 4a3b0be961..be86b4bd98 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/Code.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/Code.tsx @@ -15,7 +15,7 @@ export const CodeBlock = memo( <div {...attributes} ref={ref} className={`${attributes.className ?? ''} flex w-full bg-bg-body py-2`}> <pre spellCheck={false} - className={`flex w-full rounded border border-solid border-line-divider bg-content-blue-50 p-5 pt-20`} + className={`flex w-full rounded border border-line-divider bg-fill-list-active p-5 pt-20`} > <code>{children}</code> </pre> diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/database/BoardBlock.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/BoardBlock.tsx new file mode 100644 index 0000000000..88e3790551 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/BoardBlock.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +function BoardBlock() { + return <div></div>; +} + +export default BoardBlock; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/database/CalendarBlock.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/CalendarBlock.tsx new file mode 100644 index 0000000000..19c2b32bf0 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/CalendarBlock.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +function CalendarBlock() { + return <div></div>; +} + +export default CalendarBlock; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/database/DatabaseBlock.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/DatabaseBlock.tsx new file mode 100644 index 0000000000..b9bc174969 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/DatabaseBlock.tsx @@ -0,0 +1,60 @@ +import { IdProvider, useId } from '@/components/_shared/context-provider/IdProvider'; +import { Database } from '@/components/database'; +import { DatabaseNode, EditorElementProps } from '@/components/editor/editor.type'; +import React, { forwardRef, memo, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { BlockType } from '@/application/collab.type'; + +export const DatabaseBlock = memo( + forwardRef<HTMLDivElement, EditorElementProps<DatabaseNode>>(({ node, children, ...attributes }, ref) => { + const { t } = useTranslation(); + const viewId = node.data.view_id; + const workspaceId = useId()?.workspaceId; + const type = node.type; + + const style = useMemo(() => { + const style = {}; + + switch (type) { + case BlockType.GridBlock: + Object.assign(style, { + height: 360, + }); + break; + case BlockType.CalendarBlock: + case BlockType.BoardBlock: + Object.assign(style, { + height: 560, + }); + } + + return style; + }, [type]); + + return ( + <> + <div {...attributes} className={`relative w-full cursor-pointer py-2`}> + <div ref={ref} className={'absolute left-0 top-0 h-full w-full caret-transparent'}> + {children} + </div> + <div contentEditable={false} style={style} className={`container-bg flex w-full flex-col px-3`}> + {viewId ? ( + <IdProvider workspaceId={workspaceId} objectId={viewId}> + <Database /> + </IdProvider> + ) : ( + <div + className={'mt-[10%] flex h-full w-full flex-col items-center gap-2 px-16 text-text-caption max-md:px-4'} + > + <div className={'text-sm font-medium'}>{t('document.plugins.database.noDataSource')}</div> + <div className={'text-xs'}>{t('grid.relation.noDatabaseSelected')}</div> + </div> + )} + </div> + </div> + </> + ); + }) +); + +export default DatabaseBlock; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/database/GridBlock.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/GridBlock.tsx new file mode 100644 index 0000000000..eaf2742ceb --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/GridBlock.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +function GridBlock() { + return <div></div>; +} + +export default GridBlock; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/database/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/index.ts new file mode 100644 index 0000000000..8eaf478025 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/index.ts @@ -0,0 +1 @@ +export * from './DatabaseBlock'; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/image/ImageEmpty.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/image/ImageEmpty.tsx index ab95cbe37d..32ea2881f9 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/image/ImageEmpty.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/image/ImageEmpty.tsx @@ -1,6 +1,6 @@ import { ImageBlockNode } from '@/components/editor/editor.type'; import React from 'react'; -import { ReactComponent as ImageIcon } from '@/assets/image.svg'; +import { ReactComponent as ImageIcon } from '$icons/16x/image.svg'; import { useTranslation } from 'react-i18next'; function ImageEmpty(_: { containerRef: React.RefObject<HTMLDivElement>; onEscape: () => void; node: ImageBlockNode }) { diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/math_equation/MathEquation.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/math-equation/MathEquation.tsx similarity index 96% rename from frontend/appflowy_web_app/src/components/editor/components/blocks/math_equation/MathEquation.tsx rename to frontend/appflowy_web_app/src/components/editor/components/blocks/math-equation/MathEquation.tsx index db00aeb777..86eb6117ad 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/math_equation/MathEquation.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/math-equation/MathEquation.tsx @@ -20,7 +20,7 @@ export const MathEquation = memo( > <div contentEditable={false} - className={`container-bg w-full select-none rounded border border-line-divider bg-content-blue-50 px-3`} + className={`container-bg w-full select-none rounded border border-line-divider bg-fill-list-active px-3`} > {formula ? ( <KatexMath latex={formula} /> diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/math_equation/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/math-equation/index.ts similarity index 100% rename from frontend/appflowy_web_app/src/components/editor/components/blocks/math_equation/index.ts rename to frontend/appflowy_web_app/src/components/editor/components/blocks/math-equation/index.ts diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/numbered_list/NumberListIcon.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/numbered-list/NumberListIcon.tsx similarity index 100% rename from frontend/appflowy_web_app/src/components/editor/components/blocks/numbered_list/NumberListIcon.tsx rename to frontend/appflowy_web_app/src/components/editor/components/blocks/numbered-list/NumberListIcon.tsx diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/numbered_list/NumberedList.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/numbered-list/NumberedList.tsx similarity index 100% rename from frontend/appflowy_web_app/src/components/editor/components/blocks/numbered_list/NumberedList.tsx rename to frontend/appflowy_web_app/src/components/editor/components/blocks/numbered-list/NumberedList.tsx diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/numbered_list/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/numbered-list/index.ts similarity index 100% rename from frontend/appflowy_web_app/src/components/editor/components/blocks/numbered_list/index.ts rename to frontend/appflowy_web_app/src/components/editor/components/blocks/numbered-list/index.ts diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/text/StartIcon.hooks.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/text/StartIcon.hooks.tsx index 119c6fe9fe..2ab5996b09 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/text/StartIcon.hooks.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/text/StartIcon.hooks.tsx @@ -1,12 +1,12 @@ import { BlockType } from '@/application/collab.type'; -import { BulletedListIcon } from '@/components/editor/components/blocks/bulleted_list'; -import { NumberListIcon } from '@/components/editor/components/blocks/numbered_list'; -import ToggleIcon from '@/components/editor/components/blocks/toggle_list/ToggleIcon'; +import { BulletedListIcon } from '@/components/editor/components/blocks/bulleted-list'; +import { NumberListIcon } from '@/components/editor/components/blocks/numbered-list'; +import ToggleIcon from '@/components/editor/components/blocks/toggle-list/ToggleIcon'; import { TextNode } from '@/components/editor/editor.type'; import React, { FC, useCallback, useMemo } from 'react'; import { ReactEditor, useSlate } from 'slate-react'; import { Editor, Element } from 'slate'; -import CheckboxIcon from '../todo_list/CheckboxIcon'; +import CheckboxIcon from '@/components/editor/components/blocks/todo-list/CheckboxIcon'; export function useStartIcon(node: TextNode) { const editor = useSlate(); @@ -37,7 +37,7 @@ export function useStartIcon(node: TextNode) { return null; } - return <Component className={`text-block-icon relative`} block={block} />; + return <Component className={`text-block-icon relative h-[24px] w-[24px]`} block={block} />; }, [Component, block]); return { diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/todo_list/CheckboxIcon.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/todo-list/CheckboxIcon.tsx similarity index 76% rename from frontend/appflowy_web_app/src/components/editor/components/blocks/todo_list/CheckboxIcon.tsx rename to frontend/appflowy_web_app/src/components/editor/components/blocks/todo-list/CheckboxIcon.tsx index aad5969e86..007903b89a 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/todo_list/CheckboxIcon.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/todo-list/CheckboxIcon.tsx @@ -1,7 +1,7 @@ import { TodoListNode } from '@/components/editor/editor.type'; import React from 'react'; -import { ReactComponent as CheckboxCheckSvg } from '@/assets/database/checkbox-check.svg'; -import { ReactComponent as CheckboxUncheckSvg } from '@/assets/database/checkbox-uncheck.svg'; +import { ReactComponent as CheckboxCheckSvg } from '$icons/16x/check_filled.svg'; +import { ReactComponent as CheckboxUncheckSvg } from '$icons/16x/uncheck.svg'; function CheckboxIcon({ block, className }: { block: TodoListNode; className: string }) { const { checked } = block.data; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/todo_list/TodoList.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/todo-list/TodoList.tsx similarity index 100% rename from frontend/appflowy_web_app/src/components/editor/components/blocks/todo_list/TodoList.tsx rename to frontend/appflowy_web_app/src/components/editor/components/blocks/todo-list/TodoList.tsx diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/todo_list/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/todo-list/index.ts similarity index 100% rename from frontend/appflowy_web_app/src/components/editor/components/blocks/todo_list/index.ts rename to frontend/appflowy_web_app/src/components/editor/components/blocks/todo-list/index.ts diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle_list/ToggleIcon.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle-list/ToggleIcon.tsx similarity index 90% rename from frontend/appflowy_web_app/src/components/editor/components/blocks/toggle_list/ToggleIcon.tsx rename to frontend/appflowy_web_app/src/components/editor/components/blocks/toggle-list/ToggleIcon.tsx index 4e2735d128..e7a2d288f2 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle_list/ToggleIcon.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle-list/ToggleIcon.tsx @@ -1,6 +1,6 @@ import { ToggleListNode } from '@/components/editor/editor.type'; import React from 'react'; -import { ReactComponent as RightSvg } from '@/assets/more.svg'; +import { ReactComponent as RightSvg } from '$icons/16x/more.svg'; function ToggleIcon({ block, className }: { block: ToggleListNode; className: string }) { const { collapsed } = block.data; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle_list/ToggleList.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle-list/ToggleList.tsx similarity index 100% rename from frontend/appflowy_web_app/src/components/editor/components/blocks/toggle_list/ToggleList.tsx rename to frontend/appflowy_web_app/src/components/editor/components/blocks/toggle-list/ToggleList.tsx diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle_list/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle-list/index.ts similarity index 100% rename from frontend/appflowy_web_app/src/components/editor/components/blocks/toggle_list/index.ts rename to frontend/appflowy_web_app/src/components/editor/components/blocks/toggle-list/index.ts diff --git a/frontend/appflowy_web_app/src/components/editor/components/element/Element.tsx b/frontend/appflowy_web_app/src/components/editor/components/element/Element.tsx index f535bde10c..46a784dd37 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/element/Element.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/element/Element.tsx @@ -1,20 +1,20 @@ import { BlockData, BlockType, InlineBlockType, YjsEditorKey } from '@/application/collab.type'; -import { BulletedList } from '@/components/editor/components/blocks/bulleted_list'; +import { BulletedList } from '@/components/editor/components/blocks/bulleted-list'; import { Callout } from '@/components/editor/components/blocks/callout'; import { CodeBlock } from '@/components/editor/components/blocks/code'; import { DividerNode } from '@/components/editor/components/blocks/divider'; import { Heading } from '@/components/editor/components/blocks/heading'; import { ImageBlock } from '@/components/editor/components/blocks/image'; -import { MathEquation } from '@/components/editor/components/blocks/math_equation'; -import { NumberedList } from '@/components/editor/components/blocks/numbered_list'; +import { MathEquation } from '@/components/editor/components/blocks/math-equation'; +import { NumberedList } from '@/components/editor/components/blocks/numbered-list'; import { Outline } from '@/components/editor/components/blocks/outline'; import { Page } from '@/components/editor/components/blocks/page'; import { Paragraph } from '@/components/editor/components/blocks/paragraph'; import { Quote } from '@/components/editor/components/blocks/quote'; import { TableBlock, TableCellBlock } from '@/components/editor/components/blocks/table'; import { Text } from '@/components/editor/components/blocks/text'; -import { TodoList } from '@/components/editor/components/blocks/todo_list'; -import { ToggleList } from '@/components/editor/components/blocks/toggle_list'; +import { TodoList } from 'src/components/editor/components/blocks/todo-list'; +import { ToggleList } from 'src/components/editor/components/blocks/toggle-list'; import { UnSupportedBlock } from '@/components/editor/components/element/UnSupportedBlock'; import { Formula } from '@/components/editor/components/leaf/formula'; import { Mention } from '@/components/editor/components/leaf/mention'; @@ -22,6 +22,7 @@ import { EditorElementProps, TextNode } from '@/components/editor/editor.type'; import { renderColor } from '@/utils/color'; import React, { FC, useMemo } from 'react'; import { RenderElementProps } from 'slate-react'; +import { DatabaseBlock } from 'src/components/editor/components/blocks/database'; export const Element = ({ element: node, @@ -64,6 +65,10 @@ export const Element = ({ return TableBlock; case BlockType.TableCell: return TableCellBlock; + case BlockType.GridBlock: + case BlockType.BoardBlock: + case BlockType.CalendarBlock: + return DatabaseBlock; default: return UnSupportedBlock; } diff --git a/frontend/appflowy_web_app/src/components/editor/components/leaf/Leaf.tsx b/frontend/appflowy_web_app/src/components/editor/components/leaf/Leaf.tsx index 714b41db97..38ba65e3f2 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/leaf/Leaf.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/leaf/Leaf.tsx @@ -10,9 +10,7 @@ export function Leaf({ attributes, children, leaf }: RenderLeafProps) { const classList = [leaf.prism_token, leaf.prism_token && 'token', leaf.class_name].filter(Boolean); if (leaf.code) { - newChildren = ( - <span className={'bg-fill-list-active bg-opacity-50 text-xs font-medium text-[#EB5757]'}>{newChildren}</span> - ); + newChildren = <span className={'bg-line-divider font-medium text-[#EB5757]'}>{newChildren}</span>; } if (leaf.underline) { diff --git a/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionDate.tsx b/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionDate.tsx index fe1e844a4a..c430968b52 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionDate.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionDate.tsx @@ -1,11 +1,11 @@ import { renderDate } from '@/utils/time'; import React, { useMemo } from 'react'; -import { ReactComponent as DateSvg } from '@/assets/date.svg'; -import { ReactComponent as ReminderSvg } from '@/assets/clock_alarm.svg'; +import { ReactComponent as DateSvg } from '$icons/16x/date.svg'; +import { ReactComponent as ReminderSvg } from '$icons/16x/clock_alarm.svg'; function MentionDate({ date, reminder }: { date: string; reminder?: { id: string; option: string } }) { const dateFormat = useMemo(() => { - return renderDate(date); + return renderDate(date, 'MMM D, YYYY'); }, [date]); return ( diff --git a/frontend/appflowy_web_app/src/components/editor/editor.scss b/frontend/appflowy_web_app/src/components/editor/editor.scss index 338e21de7b..abf09c726e 100644 --- a/frontend/appflowy_web_app/src/components/editor/editor.scss +++ b/frontend/appflowy_web_app/src/components/editor/editor.scss @@ -197,12 +197,14 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { .bulleted-icon { &:after { content: attr(data-letter); + font-weight: 500; } } .numbered-icon { &:after { content: attr(data-number) "."; + font-weight: 500; } } @@ -238,7 +240,7 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { &:hover { .container-bg { - background: var(--content-blue-100) !important; + background: var(--fill-list-hover) !important; } } } @@ -270,4 +272,8 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { @apply ml-5; } +} + +.text-block-icon { + @apply flex items-center justify-center; } \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/components/editor/editor.type.ts b/frontend/appflowy_web_app/src/components/editor/editor.type.ts index fec9ffbcbf..d21f75cd3a 100644 --- a/frontend/appflowy_web_app/src/components/editor/editor.type.ts +++ b/frontend/appflowy_web_app/src/components/editor/editor.type.ts @@ -16,6 +16,7 @@ import { TableCellBlockData, BlockId, BlockData, + DatabaseNodeData, } from '@/application/collab.type'; import { HTMLAttributes } from 'react'; import { Element } from 'slate'; @@ -120,6 +121,12 @@ export interface TableCellNode extends BlockNode { data: TableCellBlockData; } +export interface DatabaseNode extends BlockNode { + type: BlockType.GridBlock | BlockType.BoardBlock | BlockType.CalendarBlock; + blockId: string; + data: DatabaseNodeData; +} + export interface EditorElementProps<T = Element> extends HTMLAttributes<HTMLDivElement> { node: T; } diff --git a/frontend/appflowy_web_app/src/components/error/ErrorModal.tsx b/frontend/appflowy_web_app/src/components/error/ErrorModal.tsx index c4382c8182..dc8d664a01 100644 --- a/frontend/appflowy_web_app/src/components/error/ErrorModal.tsx +++ b/frontend/appflowy_web_app/src/components/error/ErrorModal.tsx @@ -1,6 +1,6 @@ import { ReactComponent as InformationSvg } from '@/assets/information.svg'; -import { ReactComponent as CloseSvg } from '@/assets/close.svg'; -import { Button } from "@mui/material"; +import { ReactComponent as CloseSvg } from '$icons/16x/close.svg'; +import { Button } from '@mui/material'; export const ErrorModal = ({ message, onClose }: { message: string; onClose: () => void }) => { return ( @@ -22,9 +22,11 @@ export const ErrorModal = ({ message, onClose }: { message: string; onClose: () <h1 className={'text-xl'}>Oops.. something went wrong</h1> <h2>{message}</h2> - <Button onClick={() => { - window.location.reload(); - }}> + <Button + onClick={() => { + window.location.reload(); + }} + > Reload </Button> </div> diff --git a/frontend/appflowy_web_app/src/components/folder/ViewItem.tsx b/frontend/appflowy_web_app/src/components/folder/ViewItem.tsx index 7465857b17..c95cd305f9 100644 --- a/frontend/appflowy_web_app/src/components/folder/ViewItem.tsx +++ b/frontend/appflowy_web_app/src/components/folder/ViewItem.tsx @@ -1,7 +1,7 @@ import { layoutMap, ViewLayout, YjsFolderKey } from '@/application/collab.type'; import React from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; -import Page from 'src/components/_shared/page/Page'; +import Page from '@/components/_shared/page/Page'; function ViewItem({ id }: { id: string }) { const navigate = useNavigate(); diff --git a/frontend/appflowy_web_app/src/components/layout/Layout.tsx b/frontend/appflowy_web_app/src/components/layout/Layout.tsx index c10048aa82..46e4619cf8 100644 --- a/frontend/appflowy_web_app/src/components/layout/Layout.tsx +++ b/frontend/appflowy_web_app/src/components/layout/Layout.tsx @@ -19,6 +19,7 @@ function Layout({ children }: { children: React.ReactNode }) { if (!folder) return; + console.log(folder.toJSON()); setFolder(folder); }, [folderService] diff --git a/frontend/appflowy_web_app/src/components/layout/layout.scss b/frontend/appflowy_web_app/src/components/layout/layout.scss index 4133489130..2aa965dd15 100644 --- a/frontend/appflowy_web_app/src/components/layout/layout.scss +++ b/frontend/appflowy_web_app/src/components/layout/layout.scss @@ -35,6 +35,7 @@ .appflowy-scroll-container { &::-webkit-scrollbar { width: 0; + height: 0; } } @@ -44,28 +45,20 @@ opacity: 60%; } -.workspaces { - ::-webkit-scrollbar { - width: 0px; - } -} - -.MuiPopover-root, .MuiPaper-root { +.workspaces, .database-conditions, .grid-scroll-table, .grid-board, .MuiPaper-root, .appflowy-database { ::-webkit-scrollbar { width: 0; height: 0; } } + .view-icon { @apply flex w-fit cursor-pointer rounded-lg py-2 text-6xl; font-family: "Apple Color Emoji", "Segoe UI Emoji", NotoColorEmoji, "Noto Color Emoji", "Segoe UI Symbol", "Android Emoji", EmojiSymbols; line-height: 1em; white-space: nowrap; - //&:hover { - // background-color: rgba(156, 156, 156, 0.20); - //} } .theme-mode-item { @@ -84,3 +77,36 @@ @apply items-center; } } + +.tooltip-arrow { + overflow: hidden; + position: absolute; + width: 1em; + height: 0.71em; + color: var(--bg-body); + + &:before { + content: '""'; + margin: auto; + display: block; + width: 100%; + height: 100%; + box-shadow: var(--shadow); + background-color: var(--bg-body); + transform: rotate(45deg); + } +} + +.grid-row-cell.wrap-cell { + .text-cell { + @apply py-2 break-words whitespace-pre-wrap; + } + + .relation-cell { + @apply py-2 break-words whitespace-pre-wrap flex-wrap; + } + + .select-option-cell { + @apply flex-wrap py-2; + } +} diff --git a/frontend/appflowy_web_app/src/pages/DatabasePage.tsx b/frontend/appflowy_web_app/src/pages/DatabasePage.tsx new file mode 100644 index 0000000000..f5b4839924 --- /dev/null +++ b/frontend/appflowy_web_app/src/pages/DatabasePage.tsx @@ -0,0 +1,25 @@ +import { useId } from '@/components/_shared/context-provider/IdProvider'; +import { DatabaseHeader } from '@/components/database/components/header'; +import React from 'react'; +import { useSearchParams } from 'react-router-dom'; +import DatabaseRow from '@/components/database/DatabaseRow'; +import Database from '@/components/database/Database'; + +function DatabasePage() { + const objectId = useId()?.objectId; + const [search] = useSearchParams(); + const rowId = search.get('r'); + + if (rowId) { + return <DatabaseRow rowId={rowId} />; + } + + return ( + <div className={'relative flex h-full w-full flex-col'}> + <DatabaseHeader viewId={objectId} /> + <Database /> + </div> + ); +} + +export default DatabasePage; diff --git a/frontend/appflowy_web_app/src/pages/ProductPage.tsx b/frontend/appflowy_web_app/src/pages/ProductPage.tsx index 8080e339ef..0cd4d16cac 100644 --- a/frontend/appflowy_web_app/src/pages/ProductPage.tsx +++ b/frontend/appflowy_web_app/src/pages/ProductPage.tsx @@ -1,34 +1,37 @@ -import { CollabType } from '@/application/collab.type'; import { IdProvider } from '@/components/_shared/context-provider/IdProvider'; -import React, { useMemo } from 'react'; +import React, { lazy, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import DocumentPage from '@/pages/DocumentPage'; +const DatabasePage = lazy(() => import('./DatabasePage')); + enum URL_COLLAB_TYPE { DOCUMENT = 'document', - DATABASE = 'database', + GRID = 'grid', + BOARD = 'board', + CALENDAR = 'calendar', } -const collabTypeMap: Record<string, CollabType> = { - [URL_COLLAB_TYPE.DOCUMENT]: CollabType.Document, - [URL_COLLAB_TYPE.DATABASE]: CollabType.Database, -}; - function ProductPage() { - const { workspaceId, collabType, objectId } = useParams(); + const { workspaceId, type, objectId } = useParams(); const PageComponent = useMemo(() => { - switch (collabType) { + switch (type) { case URL_COLLAB_TYPE.DOCUMENT: return DocumentPage; + case URL_COLLAB_TYPE.GRID: + case URL_COLLAB_TYPE.BOARD: + case URL_COLLAB_TYPE.CALENDAR: + return DatabasePage; default: return null; } - }, [collabType]); + }, [type]); - if (!workspaceId || !collabType || !objectId) return null; + console.log(workspaceId, type, objectId); + if (!workspaceId || !type || !objectId) return null; return ( - <IdProvider workspaceId={workspaceId} objectId={objectId} collabType={collabTypeMap[collabType]}> + <IdProvider workspaceId={workspaceId} objectId={objectId}> {PageComponent && <PageComponent />} </IdProvider> ); diff --git a/frontend/appflowy_web_app/src/styles/template.css b/frontend/appflowy_web_app/src/styles/template.css index b255483f4e..b6f7dd3361 100644 --- a/frontend/appflowy_web_app/src/styles/template.css +++ b/frontend/appflowy_web_app/src/styles/template.css @@ -31,21 +31,6 @@ textarea { } -::-webkit-scrollbar { - width: 8px; -} - - -:root[data-dark-mode=true] body { - scrollbar-color: #fff var(--bg-body); -} - -body { - scrollbar-track-color: var(--bg-body); - scrollbar-shadow-color: var(--bg-body); -} - - .btn { @apply rounded-xl border border-line-divider px-4 py-3; } diff --git a/frontend/appflowy_web_app/src/styles/variables/dark.variables.css b/frontend/appflowy_web_app/src/styles/variables/dark.variables.css index b82d97e5be..6753969ca0 100644 --- a/frontend/appflowy_web_app/src/styles/variables/dark.variables.css +++ b/frontend/appflowy_web_app/src/styles/variables/dark.variables.css @@ -1,12 +1,12 @@ /** * Do not edit directly -* Generated on Mon, 25 Mar 2024 05:19:13 GMT +* Generated on Thu, 09 May 2024 03:26:45 GMT * Generated from $pnpm css:variables */ :root[data-dark-mode=true] { --base-light-neutral-50: #f9fafd; - --base-light-neutral-100: #edeef2; + --base-light-neutral-100: #e5e5e5; --base-light-neutral-200: #e2e4eb; --base-light-neutral-300: #f2f2f2; --base-light-neutral-400: #e0e0e0; diff --git a/frontend/appflowy_web_app/src/styles/variables/light.variables.css b/frontend/appflowy_web_app/src/styles/variables/light.variables.css index 0477655f66..b1494114bd 100644 --- a/frontend/appflowy_web_app/src/styles/variables/light.variables.css +++ b/frontend/appflowy_web_app/src/styles/variables/light.variables.css @@ -1,12 +1,12 @@ /** * Do not edit directly -* Generated on Mon, 25 Mar 2024 05:19:13 GMT +* Generated on Thu, 09 May 2024 03:26:45 GMT * Generated from $pnpm css:variables */ :root { --base-light-neutral-50: #f9fafd; - --base-light-neutral-100: #edeef2; + --base-light-neutral-100: #e5e5e5; --base-light-neutral-200: #e2e4eb; --base-light-neutral-300: #f2f2f2; --base-light-neutral-400: #e0e0e0; @@ -83,7 +83,7 @@ --icon-disabled: #e0e0e0; --icon-on-toolbar: #ffffff; --line-border: #bdbdbd; - --line-divider: #edeef2; + --line-divider: #e5e5e5; --line-on-toolbar: #4f4f4f; --fill-toolbar: #333333; --fill-default: #00bcf0; @@ -91,7 +91,7 @@ --fill-pressed: #009fd1; --fill-active: #e0f8ff; --fill-list-hover: #e0f8ff; - --fill-list-active: #edeef2; + --fill-list-active: #f9fafd; --content-blue-400: #00bcf0; --content-blue-300: #52d1f4; --content-blue-600: #009fd1; @@ -120,5 +120,5 @@ --tint-yellow: #fff2cd; --shadow: 0px 0px 10px 0px rgba(0,0,0,0.1); --scrollbar-thumb: #bdbdbd; - --scrollbar-track: #edeef2; + --scrollbar-track: #e5e5e5; } \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/utils/time.ts b/frontend/appflowy_web_app/src/utils/time.ts index 3b6920fb34..792b72ee61 100644 --- a/frontend/appflowy_web_app/src/utils/time.ts +++ b/frontend/appflowy_web_app/src/utils/time.ts @@ -1,10 +1,6 @@ import dayjs from 'dayjs'; -export enum DateFormat { - Date = 'MMM D, YYYY', - DateTime = 'MMM D, YYYY h:mm A', -} - -export function renderDate(date: string, format: DateFormat = DateFormat.Date): string { +export function renderDate(date: string, format: string, isUnix?: boolean): string { + if (isUnix) return dayjs.unix(Number(date)).format(format); return dayjs(date).format(format); } diff --git a/frontend/appflowy_web_app/src/utils/url.ts b/frontend/appflowy_web_app/src/utils/url.ts index 8d67f3583f..a10cf9ca85 100644 --- a/frontend/appflowy_web_app/src/utils/url.ts +++ b/frontend/appflowy_web_app/src/utils/url.ts @@ -1,12 +1,14 @@ import { getPlatform } from '@/utils/platform'; -import validator from 'validator'; +import isURL from 'validator/lib/isURL'; +import isIP from 'validator/lib/isIP'; +import isFQDN from 'validator/lib/isFQDN'; export const downloadPage = 'https://appflowy.io/download'; export const openAppFlowySchema = 'appflowy-flutter://'; export function isValidUrl(input: string) { - return validator.isURL(input, { require_protocol: true, require_host: false }); + return isURL(input, { require_protocol: true, require_host: false }); } // Process the URL to make sure it's a valid URL @@ -20,7 +22,7 @@ export function processUrl(input: string) { const domain = input.split('/')[0]; - if (validator.isIP(domain) || validator.isFQDN(domain)) { + if (isIP(domain) || isFQDN(domain)) { processedUrl = `https://${input}`; if (isValidUrl(processedUrl)) { return processedUrl; diff --git a/frontend/appflowy_web_app/style-dictionary/tailwind/box-shadow.cjs b/frontend/appflowy_web_app/style-dictionary/tailwind/box-shadow.cjs index 00647333e2..9de67fc1be 100644 --- a/frontend/appflowy_web_app/style-dictionary/tailwind/box-shadow.cjs +++ b/frontend/appflowy_web_app/style-dictionary/tailwind/box-shadow.cjs @@ -1,6 +1,6 @@ /** * Do not edit directly -* Generated on Mon, 25 Mar 2024 05:19:13 GMT +* Generated on Thu, 09 May 2024 03:26:45 GMT * Generated from $pnpm css:variables */ diff --git a/frontend/appflowy_web_app/style-dictionary/tailwind/colors.cjs b/frontend/appflowy_web_app/style-dictionary/tailwind/colors.cjs index 798741f06c..63e679a90a 100644 --- a/frontend/appflowy_web_app/style-dictionary/tailwind/colors.cjs +++ b/frontend/appflowy_web_app/style-dictionary/tailwind/colors.cjs @@ -1,6 +1,6 @@ /** * Do not edit directly -* Generated on Mon, 25 Mar 2024 05:19:13 GMT +* Generated on Thu, 09 May 2024 03:26:45 GMT * Generated from $pnpm css:variables */ diff --git a/frontend/appflowy_web_app/style-dictionary/tokens/base.json b/frontend/appflowy_web_app/style-dictionary/tokens/base.json index 4e31b0523d..f92d39267f 100644 --- a/frontend/appflowy_web_app/style-dictionary/tokens/base.json +++ b/frontend/appflowy_web_app/style-dictionary/tokens/base.json @@ -7,7 +7,7 @@ "type": "color" }, "100": { - "value": "#edeef2", + "value": "#e5e5e5", "type": "color" }, "200": { diff --git a/frontend/appflowy_web_app/tsconfig.json b/frontend/appflowy_web_app/tsconfig.json index de30c24901..05dcd8d587 100644 --- a/frontend/appflowy_web_app/tsconfig.json +++ b/frontend/appflowy_web_app/tsconfig.json @@ -27,7 +27,7 @@ "node", "jest" ], - "baseUrl": "./", + "baseUrl": ".", "paths": { "@/*": [ "src/*" @@ -37,6 +37,9 @@ ], "$client-services": [ "src/application/services/js-services" + ], + "$icons/*": [ + "../resources/flowy-flowy_icons/*" ] } }, diff --git a/frontend/appflowy_web_app/vite.config.ts b/frontend/appflowy_web_app/vite.config.ts index b2621799ed..87a5e284bf 100644 --- a/frontend/appflowy_web_app/vite.config.ts +++ b/frontend/appflowy_web_app/vite.config.ts @@ -5,7 +5,9 @@ import wasm from 'vite-plugin-wasm'; import { visualizer } from 'rollup-plugin-visualizer'; import usePluginImport from 'vite-plugin-importer'; import { totalBundleSize } from 'vite-plugin-total-bundle-size'; +import path from 'path'; +const resourcesPath = path.resolve(__dirname, '../resources'); const isDev = process.env.NODE_ENV === 'development'; // https://vitejs.dev/config/ export default defineConfig({ @@ -69,6 +71,9 @@ export default defineConfig({ cors: false, }, envPrefix: ['AF', 'TAURI_'], + esbuild: { + drop: isDev ? [] : ['console', 'debugger'], + }, build: !!process.env.TAURI_PLATFORM ? { // Tauri supports es2021 @@ -80,15 +85,6 @@ export default defineConfig({ } : { target: `esnext`, - terserOptions: !isDev - ? { - compress: { - keep_infinity: true, - drop_console: true, - drop_debugger: true, - }, - } - : {}, reportCompressedSize: true, sourcemap: isDev, rollupOptions: !isDev @@ -104,8 +100,8 @@ export default defineConfig({ id.includes('/react-is@') || id.includes('/yjs@') || id.includes('/y-indexeddb@') || - id.includes('/dexie@') || - id.includes('/redux') + id.includes('/redux') || + id.includes('/react-custom-scrollbars') ) { return 'common'; } @@ -124,6 +120,7 @@ export default defineConfig({ ? `${__dirname}/src/application/services/tauri-services` : `${__dirname}/src/application/services/js-services`, }, + { find: '$icons', replacement: `${resourcesPath}/flowy_icons/` }, ], }, 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 0000000000..ac83855416 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/add_cover.svg @@ -0,0 +1,7 @@ +<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.3"> +<path d="M1.72052 13.1735L1.70719 13.1868C1.52719 12.7935 1.41385 12.3468 1.36719 11.8535C1.41385 12.3402 1.54052 12.7802 1.72052 13.1735Z" fill="#171717"/> +<path d="M6.00073 7.41943C6.87702 7.41943 7.5874 6.70905 7.5874 5.83276C7.5874 4.95647 6.87702 4.24609 6.00073 4.24609C5.12444 4.24609 4.41406 4.95647 4.41406 5.83276C4.41406 6.70905 5.12444 7.41943 6.00073 7.41943Z" fill="#171717"/> +<path d="M10.792 1.83398H5.20536C2.7787 1.83398 1.33203 3.28065 1.33203 5.70732V11.294C1.33203 12.0207 1.4587 12.654 1.70536 13.1873C2.2787 14.454 3.50536 15.1673 5.20536 15.1673H10.792C13.2187 15.1673 14.6654 13.7207 14.6654 11.294V9.76732V5.70732C14.6654 3.28065 13.2187 1.83398 10.792 1.83398ZM13.5787 8.83398C13.0587 8.38732 12.2187 8.38732 11.6987 8.83398L8.92537 11.214C8.40537 11.6607 7.56536 11.6607 7.04536 11.214L6.8187 11.0273C6.34536 10.614 5.59203 10.574 5.0587 10.934L2.56536 12.6073C2.4187 12.234 2.33203 11.8007 2.33203 11.294V5.70732C2.33203 3.82732 3.32536 2.83398 5.20536 2.83398H10.792C12.672 2.83398 13.6654 3.82732 13.6654 5.70732V8.90732L13.5787 8.83398Z" fill="#171717"/> +</g> +</svg> 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 0000000000..e49b54ec14 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/add_icon.svg @@ -0,0 +1,5 @@ +<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.3"> +<path d="M12.739 3.80656C10.1323 1.1999 5.899 1.1999 3.29233 3.80656C0.639001 6.4599 0.685667 10.7866 3.42567 13.3866C5.959 15.7799 10.0657 15.7799 12.599 13.3866C15.3457 10.7866 15.3923 6.4599 12.739 3.80656ZM10.919 11.5999C10.119 12.3599 9.06567 12.7399 8.01233 12.7399C6.959 12.7399 5.90567 12.3599 5.10567 11.5999C4.90567 11.4066 4.899 11.0932 5.08567 10.8932C5.279 10.6932 5.59233 10.6866 5.79233 10.8732C7.01233 12.0266 9.00567 12.0332 10.2323 10.8732C10.4323 10.6866 10.7523 10.6932 10.939 10.8932C11.1323 11.0932 11.119 11.4066 10.919 11.5999Z" fill="#171717"/> +</g> +</svg> 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 0000000000..83dabfe0a1 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/add_workspace.svg @@ -0,0 +1,6 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.5"> +<rect x="2" y="7.3999" width="12" height="1.2" rx="0.6" fill="#171717"/> +<rect x="7.40234" y="14" width="12" height="1.2" rx="0.6" transform="rotate(-90 7.40234 14)" fill="#171717"/> +</g> +</svg> 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 0000000000..38c7e41710 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/change_icon.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z" stroke="#171717" stroke-width="1.02857" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M5.14453 10.4004C5.77453 11.3064 6.83053 11.9004 8.01853 11.9004C9.20653 11.9004 10.2565 11.3064 10.8925 10.4004" stroke="#171717" stroke-width="1.02857" stroke-linecap="round" stroke-linejoin="round"/> +</svg> 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 0000000000..2760daaaef --- /dev/null +++ b/frontend/resources/flowy_icons/16x/collapse_all_page.svg @@ -0,0 +1,14 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<mask id="path-1-outside-1_517_23658" maskUnits="userSpaceOnUse" x="3.69922" y="1.62109" width="9" height="6" fill="black"> +<rect fill="white" x="3.69922" y="1.62109" width="9" height="6"/> +<path d="M11.5638 2.6337C11.3831 2.45486 11.0903 2.45486 10.9097 2.6337L7.99922 5.51551L5.08876 2.6337C4.90814 2.45486 4.6153 2.45486 4.43468 2.6337C4.25406 2.81254 4.25406 3.1025 4.43468 3.28134L7.67218 6.48696C7.75892 6.57285 7.87656 6.62109 7.99922 6.62109C8.12188 6.62109 8.23952 6.57285 8.32626 6.48696L11.5638 3.28134C11.7444 3.1025 11.7444 2.81254 11.5638 2.6337Z"/> +</mask> +<path d="M11.5638 2.6337C11.3831 2.45486 11.0903 2.45486 10.9097 2.6337L7.99922 5.51551L5.08876 2.6337C4.90814 2.45486 4.6153 2.45486 4.43468 2.6337C4.25406 2.81254 4.25406 3.1025 4.43468 3.28134L7.67218 6.48696C7.75892 6.57285 7.87656 6.62109 7.99922 6.62109C8.12188 6.62109 8.23952 6.57285 8.32626 6.48696L11.5638 3.28134C11.7444 3.1025 11.7444 2.81254 11.5638 2.6337Z" fill="#171717"/> +<path d="M10.9097 2.6337L10.9881 2.71286L10.9097 2.6337ZM11.5638 2.6337L11.4854 2.71286H11.4854L11.5638 2.6337ZM7.99922 5.51551L7.92084 5.59467C7.96425 5.63765 8.03418 5.63765 8.07759 5.59467L7.99922 5.51551ZM5.08876 2.6337L5.01038 2.71286L5.08876 2.6337ZM4.43468 2.6337L4.51306 2.71286V2.71286L4.43468 2.6337ZM4.43468 3.28134L4.51306 3.20218V3.20218L4.43468 3.28134ZM7.67218 6.48696L7.59381 6.56612V6.56612L7.67218 6.48696ZM8.32626 6.48696L8.24788 6.40781V6.40781L8.32626 6.48696ZM11.5638 3.28134L11.6421 3.36049H11.6421L11.5638 3.28134ZM10.9881 2.71286C11.1253 2.577 11.3482 2.577 11.4854 2.71286L11.6421 2.55455C11.4181 2.33273 11.0553 2.33273 10.8313 2.55455L10.9881 2.71286ZM8.07759 5.59467L10.9881 2.71286L10.8313 2.55455L7.92084 5.43636L8.07759 5.59467ZM5.01038 2.71286L7.92084 5.59467L8.07759 5.43636L5.16713 2.55455L5.01038 2.71286ZM4.51306 2.71286C4.65026 2.577 4.87317 2.577 5.01038 2.71286L5.16713 2.55455C4.9431 2.33273 4.58033 2.33273 4.35631 2.55455L4.51306 2.71286ZM4.51306 3.20218C4.37646 3.06693 4.37646 2.84811 4.51306 2.71286L4.35631 2.55455C4.13167 2.77698 4.13167 3.13807 4.35631 3.36049L4.51306 3.20218ZM7.75056 6.40781L4.51306 3.20218L4.35631 3.36049L7.59381 6.56612L7.75056 6.40781ZM7.99922 6.5097C7.90575 6.5097 7.81632 6.47292 7.75056 6.40781L7.59381 6.56612C7.70151 6.67277 7.84737 6.73249 7.99922 6.73249V6.5097ZM8.24788 6.40781C8.18212 6.47292 8.09269 6.5097 7.99922 6.5097V6.73249C8.15107 6.73249 8.29692 6.67277 8.40463 6.56612L8.24788 6.40781ZM11.4854 3.20218L8.24788 6.40781L8.40463 6.56612L11.6421 3.36049L11.4854 3.20218ZM11.4854 2.71286C11.622 2.84811 11.622 3.06693 11.4854 3.20218L11.6421 3.36049C11.8668 3.13807 11.8668 2.77698 11.6421 2.55455L11.4854 2.71286Z" fill="#171717" mask="url(#path-1-outside-1_517_23658)"/> +<mask id="path-3-outside-2_517_23658" maskUnits="userSpaceOnUse" x="3.30078" y="8.37891" width="9" height="6" fill="black"> +<rect fill="white" x="3.30078" y="8.37891" width="9" height="6"/> +<path d="M4.43624 13.3663C4.61686 13.5451 4.9097 13.5451 5.09032 13.3663L8.00078 10.4845L10.9112 13.3663C11.0919 13.5451 11.3847 13.5451 11.5653 13.3663C11.7459 13.1875 11.7459 12.8975 11.5653 12.7187L8.32782 9.51304C8.24108 9.42715 8.12344 9.37891 8.00078 9.37891C7.87812 9.37891 7.76048 9.42715 7.67374 9.51304L4.43624 12.7187C4.25563 12.8975 4.25563 13.1875 4.43624 13.3663Z"/> +</mask> +<path d="M4.43624 13.3663C4.61686 13.5451 4.9097 13.5451 5.09032 13.3663L8.00078 10.4845L10.9112 13.3663C11.0919 13.5451 11.3847 13.5451 11.5653 13.3663C11.7459 13.1875 11.7459 12.8975 11.5653 12.7187L8.32782 9.51304C8.24108 9.42715 8.12344 9.37891 8.00078 9.37891C7.87812 9.37891 7.76048 9.42715 7.67374 9.51304L4.43624 12.7187C4.25563 12.8975 4.25563 13.1875 4.43624 13.3663Z" fill="#171717"/> +<path d="M5.09032 13.3663L5.01194 13.2871L5.09032 13.3663ZM4.43624 13.3663L4.51462 13.2871H4.51462L4.43624 13.3663ZM8.00078 10.4845L8.07916 10.4053C8.03575 10.3623 7.96582 10.3623 7.92241 10.4053L8.00078 10.4845ZM10.9112 13.3663L10.9896 13.2871L10.9112 13.3663ZM11.5653 13.3663L11.4869 13.2871V13.2871L11.5653 13.3663ZM11.5653 12.7187L11.4869 12.7978V12.7978L11.5653 12.7187ZM8.32782 9.51304L8.40619 9.43388V9.43388L8.32782 9.51304ZM7.67374 9.51304L7.75212 9.59219V9.59219L7.67374 9.51304ZM4.43624 12.7187L4.35787 12.6395H4.35787L4.43624 12.7187ZM5.01194 13.2871C4.87474 13.423 4.65183 13.423 4.51462 13.2871L4.35787 13.4455C4.5819 13.6673 4.94467 13.6673 5.16869 13.4455L5.01194 13.2871ZM7.92241 10.4053L5.01194 13.2871L5.16869 13.4455L8.07916 10.5636L7.92241 10.4053ZM10.9896 13.2871L8.07916 10.4053L7.92241 10.5636L10.8329 13.4455L10.9896 13.2871ZM11.4869 13.2871C11.3497 13.423 11.1268 13.423 10.9896 13.2871L10.8329 13.4455C11.0569 13.6673 11.4197 13.6673 11.6437 13.4455L11.4869 13.2871ZM11.4869 12.7978C11.6235 12.9331 11.6235 13.1519 11.4869 13.2871L11.6437 13.4455C11.8683 13.223 11.8683 12.8619 11.6437 12.6395L11.4869 12.7978ZM8.24944 9.59219L11.4869 12.7978L11.6437 12.6395L8.40619 9.43388L8.24944 9.59219ZM8.00078 9.4903C8.09425 9.4903 8.18368 9.52708 8.24944 9.59219L8.40619 9.43388C8.29849 9.32723 8.15263 9.26751 8.00078 9.26751V9.4903ZM7.75212 9.59219C7.81788 9.52708 7.90731 9.4903 8.00078 9.4903V9.26751C7.84893 9.26751 7.70308 9.32723 7.59537 9.43388L7.75212 9.59219ZM4.51462 12.7978L7.75212 9.59219L7.59537 9.43388L4.35787 12.6395L4.51462 12.7978ZM4.51462 13.2871C4.37802 13.1519 4.37802 12.9331 4.51462 12.7978L4.35787 12.6395C4.13323 12.8619 4.13323 13.223 4.35787 13.4455L4.51462 13.2871Z" fill="#171717" mask="url(#path-3-outside-2_517_23658)"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/duplicate.svg b/frontend/resources/flowy_icons/16x/duplicate.svg new file mode 100644 index 0000000000..8830822589 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/duplicate.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M10.6654 8.60065V11.4007C10.6654 13.734 9.73203 14.6673 7.3987 14.6673H4.5987C2.26536 14.6673 1.33203 13.734 1.33203 11.4007V8.60065C1.33203 6.26732 2.26536 5.33398 4.5987 5.33398H7.3987C9.73203 5.33398 10.6654 6.26732 10.6654 8.60065Z" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M14.6654 4.60065V7.40065C14.6654 9.73398 13.732 10.6673 11.3987 10.6673H10.6654V8.60065C10.6654 6.26732 9.73203 5.33398 7.3987 5.33398H5.33203V4.60065C5.33203 2.26732 6.26536 1.33398 8.5987 1.33398H11.3987C13.732 1.33398 14.6654 2.26732 14.6654 4.60065Z" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/favorite.svg b/frontend/resources/flowy_icons/16x/favorite.svg index 8ad54bbbb5..addd7d4915 100644 --- a/frontend/resources/flowy_icons/16x/favorite.svg +++ b/frontend/resources/flowy_icons/16x/favorite.svg @@ -1,3 +1,3 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M8 3L9.3905 5.96215L12.5 6.44006L10.25 8.74448L10.781 12L8 10.4621L5.219 12L5.75 8.74448L3.5 6.44006L6.6095 5.96215L8 3Z" fill="#FFD667" stroke="#FFD667" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M9.15132 2.33977L10.3247 4.68643C10.4847 5.0131 10.9113 5.32643 11.2713 5.38643L13.398 5.73977C14.758 5.96643 15.078 6.9531 14.098 7.92643L12.4447 9.57977C12.1647 9.85977 12.0113 10.3998 12.098 10.7864L12.5713 12.8331C12.9447 14.4531 12.0847 15.0798 10.6513 14.2331L8.65799 13.0531C8.29799 12.8398 7.70465 12.8398 7.33799 13.0531L5.34465 14.2331C3.91799 15.0798 3.05132 14.4464 3.42465 12.8331L3.89799 10.7864C3.98465 10.3998 3.83132 9.85977 3.55132 9.57977L1.89799 7.92643C0.924653 6.9531 1.23799 5.96643 2.59799 5.73977L4.72465 5.38643C5.07799 5.32643 5.50465 5.0131 5.66465 4.68643L6.83799 2.33977C7.47799 1.06643 8.51799 1.06643 9.15132 2.33977Z" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> </svg> 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 0000000000..8296f888f3 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/favorite_header_icon.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M9.15132 2.34025L10.3247 4.68692C10.4847 5.01359 10.9113 5.32692 11.2713 5.38692L13.398 5.74025C14.758 5.96692 15.078 6.95359 14.098 7.92692L12.4447 9.58025C12.1647 9.86025 12.0113 10.4003 12.098 10.7869L12.5713 12.8336C12.9447 14.4536 12.0847 15.0803 10.6513 14.2336L8.65799 13.0536C8.29799 12.8403 7.70465 12.8403 7.33799 13.0536L5.34465 14.2336C3.91799 15.0803 3.05132 14.4469 3.42465 12.8336L3.89799 10.7869C3.98465 10.4003 3.83132 9.86025 3.55132 9.58025L1.89799 7.92692C0.924653 6.95359 1.23799 5.96692 2.59799 5.74025L4.72465 5.38692C5.07799 5.32692 5.50465 5.01359 5.66465 4.68692L6.83799 2.34025C7.47799 1.06692 8.51799 1.06692 9.15132 2.34025Z" fill="#FFBA00"/> +</svg> 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 0000000000..0402120e41 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/favorite_section_pin.svg @@ -0,0 +1,5 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.5"> +<path d="M8.38274 2.1912C8.61919 1.95467 8.99451 1.91977 9.2623 2.18766L14.145 7.07205C14.3765 7.30363 14.411 7.68431 14.1468 7.94865C13.9854 8.11011 13.5919 8.23414 13.2876 7.92972L12.5611 7.20735L10.6204 9.1481C10.5628 9.20571 10.5477 9.30158 10.5477 9.38042L10.2799 13.2031C10.2799 13.285 10.2475 13.3638 10.1899 13.4214L9.84485 13.7657C9.60524 14.0063 9.21857 14.0071 8.98279 13.7713L6.20394 10.9936L3.37596 13.8224C3.1392 14.0592 2.75536 14.0592 2.51861 13.8224L2.5096 13.8134C2.27301 13.5767 2.27282 13.193 2.50916 12.9561L5.33522 10.1233L2.5568 7.34311C2.31101 7.09723 2.31763 6.71613 2.55769 6.47598L2.90214 6.13221C2.95976 6.07459 3.03787 6.04124 3.11932 6.04124L6.94541 5.77806C7.02695 5.77806 7.13094 5.77049 7.18854 5.71288L9.12891 3.77213L8.40493 3.04532C8.17437 2.81725 8.15367 2.42036 8.38274 2.1912Z" fill="#171717"/> +</g> +</svg> 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 0000000000..b984afe017 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/favorite_section_remove_from_favorite.svg @@ -0,0 +1,5 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M3.52189 12.4331L3.90189 10.7864C3.98856 10.3998 3.83523 9.85977 3.55523 9.57977L1.90189 7.92643C0.928559 6.9531 1.24189 5.96643 2.60189 5.73977L4.72856 5.38643C5.08189 5.32643 5.50856 5.0131 5.66856 4.68643L6.84189 2.33977C7.47523 1.06643 8.51523 1.06643 9.15523 2.33977L10.3286 4.68643C10.4019 4.83977 10.5419 4.98643 10.6952 5.1131" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M13.401 5.73975C14.761 5.96641 15.081 6.95308 14.101 7.92641L12.4477 9.57975C12.1677 9.85975 12.0143 10.3997 12.101 10.7864L12.5743 12.8331C12.9477 14.4531 12.0877 15.0797 10.6543 14.2331L8.66099 13.0531C8.30099 12.8397 7.70766 12.8397 7.34099 13.0531L5.34766 14.2331" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M14.6693 1.33301L1.33594 14.6663" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> +</svg> 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 0000000000..3e72f90f4b --- /dev/null +++ b/frontend/resources/flowy_icons/16x/favorite_section_unpin.svg @@ -0,0 +1,5 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.5"> +<path d="M9.2623 2.18766C8.99451 1.91977 8.61919 1.95467 8.38274 2.1912C8.15367 2.42036 8.17437 2.81725 8.40493 3.04532L9.12891 3.77213L7.18854 5.71288C7.13094 5.77049 7.02695 5.77806 6.94541 5.77806L3.11932 6.04124C3.03787 6.04124 2.95976 6.07459 2.90214 6.13221L2.55769 6.47598C2.31763 6.71613 2.31101 7.09723 2.5568 7.34311L5.33522 10.1233L2.50916 12.9561C2.27282 13.193 2.27301 13.5767 2.5096 13.8134L2.51861 13.8224C2.75536 14.0592 3.1392 14.0592 3.37596 13.8224L6.20394 10.9936L8.98279 13.7713C9.21857 14.0071 9.60524 14.0063 9.84485 13.7657L10.1899 13.4214C10.2475 13.3638 10.2799 13.285 10.2799 13.2031L10.5477 9.38042C10.5477 9.30159 10.5628 9.20571 10.6204 9.1481L12.5611 7.20735L13.2876 7.92972C13.5919 8.23414 13.9854 8.11011 14.1468 7.94865C14.411 7.68431 14.3765 7.30363 14.145 7.07205L9.2623 2.18766ZM10.0059 4.64872L11.7235 6.36508L9.40296 8.69111L9.14988 12.226L4.1365 7.20788L7.69325 6.96485L10.0059 4.64872Z" fill="#171717"/> +</g> +</svg> diff --git a/frontend/resources/flowy_icons/16x/hide_menu.svg b/frontend/resources/flowy_icons/16x/hide_menu.svg index ce88af8ea7..9e301210c4 100644 --- a/frontend/resources/flowy_icons/16x/hide_menu.svg +++ b/frontend/resources/flowy_icons/16x/hide_menu.svg @@ -1,6 +1,6 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M6 5L3 8L6 11" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> -<rect width="4" height="1" rx="0.5" transform="matrix(-1 0 0 1 13 5)" fill="#333333"/> -<rect width="6" height="1" rx="0.5" transform="matrix(-1 0 0 1 13 7.5)" fill="#333333"/> -<rect width="4" height="1" rx="0.5" transform="matrix(-1 0 0 1 13 10)" fill="#333333"/> +<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.5"> +<path d="M8.25 3.75L3 9L8.25 14.25" stroke="#171717" stroke-width="1.2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M14.4375 3.75L9.1875 9L14.4375 14.25" stroke="#171717" stroke-width="1.2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> +</g> </svg> 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 0000000000..9953ebe30c --- /dev/null +++ b/frontend/resources/flowy_icons/16x/icon_shuffle.svg @@ -0,0 +1,7 @@ +<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M2.75 16.4824L5.08749 16.4916C5.92166 16.4916 6.70083 16.0791 7.15916 15.3916L13.0167 6.60992C13.475 5.92242 14.2542 5.50075 15.0883 5.50991L19.2592 5.52826" stroke="#171717" stroke-width="1.375" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M17.418 18.3158L19.2513 16.4824" stroke="#171717" stroke-width="1.375" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M8.14918 7.90182L7.15916 6.52682C6.69166 5.87599 5.93999 5.49099 5.14249 5.50016L2.75 5.50934" stroke="#171717" stroke-width="1.375" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M11.8867 14.0996L13.0051 15.5388C13.4726 16.1438 14.2059 16.5013 14.9759 16.5013L19.2567 16.4829" stroke="#171717" stroke-width="1.375" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M19.2513 5.51693L17.418 3.68359" stroke="#171717" stroke-width="1.375" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/more.svg b/frontend/resources/flowy_icons/16x/more.svg index b191e64a10..da54e4b6e6 100644 --- a/frontend/resources/flowy_icons/16x/more.svg +++ b/frontend/resources/flowy_icons/16x/more.svg @@ -1,3 +1,7 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M9.39568 7.6963L6.91032 5.56599C6.65085 5.34358 6.25 5.52795 6.25 5.86969L6.25 10.1303C6.25 10.4721 6.65085 10.6564 6.91032 10.434L9.39568 8.3037C9.58192 8.14406 9.58192 7.85594 9.39568 7.6963Z" fill="#333333"/> -</svg> + <g opacity="0.6"> + <path d="M4.23516 7.99941C4.23516 8.60693 3.74267 9.09941 3.13516 9.09941C2.52764 9.09941 2.03516 8.60693 2.03516 7.99941C2.03516 7.3919 2.52764 6.89941 3.13516 6.89941C3.74267 6.89941 4.23516 7.3919 4.23516 7.99941Z" fill="#171717"/> + <path d="M9.10234 7.99941C9.10234 8.60693 8.60986 9.09941 8.00234 9.09941C7.39483 9.09941 6.90234 8.60693 6.90234 7.99941C6.90234 7.3919 7.39483 6.89941 8.00234 6.89941C8.60986 6.89941 9.10234 7.3919 9.10234 7.99941Z" fill="#171717"/> + <path d="M13.9695 7.99941C13.9695 8.60693 13.477 9.09941 12.8695 9.09941C12.262 9.09941 11.7695 8.60693 11.7695 7.99941C11.7695 7.3919 12.262 6.89941 12.8695 6.89941C13.477 6.89941 13.9695 7.3919 13.9695 7.99941Z" fill="#171717"/> + </g> + </svg> 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 0000000000..1c7d6144ee --- /dev/null +++ b/frontend/resources/flowy_icons/16x/move_to.svg @@ -0,0 +1,5 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M7.9987 14.6673C11.6806 14.6673 14.6654 11.6825 14.6654 8.00065C14.6654 4.31875 11.6806 1.33398 7.9987 1.33398C4.3168 1.33398 1.33203 4.31875 1.33203 8.00065C1.33203 11.6825 4.3168 14.6673 7.9987 14.6673Z" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M5.66797 8H9.66797" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M8.33203 10L10.332 8L8.33203 6" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/notification.svg b/frontend/resources/flowy_icons/16x/notification.svg new file mode 100644 index 0000000000..feb63cb9b3 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/notification.svg @@ -0,0 +1,5 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M8.01242 1.93994C5.80575 1.93994 4.01242 3.73327 4.01242 5.93994V7.86661C4.01242 8.27328 3.83908 8.89327 3.63242 9.23994L2.86575 10.5133C2.39242 11.2999 2.71908 12.1733 3.58575 12.4666C6.45908 13.4266 9.55908 13.4266 12.4324 12.4666C13.2391 12.1999 13.5924 11.2466 13.1524 10.5133L12.3857 9.23994C12.1857 8.89327 12.0124 8.27328 12.0124 7.86661V5.93994C12.0124 3.73994 10.2124 1.93994 8.01242 1.93994Z" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round"/> +<path d="M9.24792 2.13346C9.04125 2.07346 8.82792 2.02679 8.60792 2.00012C7.96792 1.92012 7.35458 1.96679 6.78125 2.13346C6.97458 1.64012 7.45458 1.29346 8.01458 1.29346C8.57458 1.29346 9.05458 1.64012 9.24792 2.13346Z" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M10.0117 12.7065C10.0117 13.8065 9.11172 14.7065 8.01172 14.7065C7.46505 14.7065 6.95838 14.4799 6.59838 14.1199C6.23838 13.7599 6.01172 13.2532 6.01172 12.7065" stroke="#171717" stroke-miterlimit="10"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/search.svg b/frontend/resources/flowy_icons/16x/search.svg index 1efb2d475c..ed7e6de9d2 100644 --- a/frontend/resources/flowy_icons/16x/search.svg +++ b/frontend/resources/flowy_icons/16x/search.svg @@ -1,4 +1,6 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<circle cx="7.5" cy="7.5" r="5" stroke="#333333"/> -<path d="M12.6464 13.354C12.8416 13.5493 13.1582 13.5493 13.3535 13.3541C13.5488 13.1588 13.5488 12.8422 13.3536 12.647L12.6464 13.354ZM10.6464 11.3535L12.6464 13.354L13.3536 12.647L11.3536 10.6465L10.6464 11.3535Z" fill="#333333"/> +<g opacity="0.7"> +<path d="M7.66927 13.9999C11.1671 13.9999 14.0026 11.1644 14.0026 7.66659C14.0026 4.16878 11.1671 1.33325 7.66927 1.33325C4.17147 1.33325 1.33594 4.16878 1.33594 7.66659C1.33594 11.1644 4.17147 13.9999 7.66927 13.9999Z" stroke="#171717" stroke-width="1.03333" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M14.6693 14.6666L13.3359 13.3333" stroke="#171717" stroke-width="1.03333" stroke-linecap="round" stroke-linejoin="round"/> +</g> </svg> diff --git a/frontend/resources/flowy_icons/16x/settings.svg b/frontend/resources/flowy_icons/16x/settings.svg index f9896aad52..bcc96b817b 100644 --- a/frontend/resources/flowy_icons/16x/settings.svg +++ b/frontend/resources/flowy_icons/16x/settings.svg @@ -1,4 +1,4 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M7.01471 2.14621C7.62441 1.7942 8.37559 1.7942 8.98529 2.14621L12.5769 4.21982C13.1866 4.57183 13.5622 5.22237 13.5622 5.92639V10.0736C13.5622 10.7776 13.1866 11.4282 12.5769 11.7802L8.98529 13.8538C8.37559 14.2058 7.62441 14.2058 7.01471 13.8538L3.42312 11.7802C2.81341 11.4282 2.43782 10.7776 2.43782 10.0736V5.92639C2.43782 5.22237 2.81341 4.57183 3.42312 4.21982L7.01471 2.14621Z" stroke="#333333"/> -<circle cx="8" cy="8" r="2.5" stroke="#333333"/> +<path d="M8 10C9.10457 10 10 9.10457 10 8C10 6.89543 9.10457 6 8 6C6.89543 6 6 6.89543 6 8C6 9.10457 6.89543 10 8 10Z" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M1.33203 8.58679V7.41345C1.33203 6.72012 1.8987 6.14679 2.5987 6.14679C3.80536 6.14679 4.2987 5.29345 3.69203 4.24679C3.34536 3.64679 3.55203 2.86679 4.1587 2.52012L5.31203 1.86012C5.8387 1.54679 6.5187 1.73345 6.83203 2.26012L6.90536 2.38679C7.50536 3.43345 8.49203 3.43345 9.0987 2.38679L9.17203 2.26012C9.48536 1.73345 10.1654 1.54679 10.692 1.86012L11.8454 2.52012C12.452 2.86679 12.6587 3.64679 12.312 4.24679C11.7054 5.29345 12.1987 6.14679 13.4054 6.14679C14.0987 6.14679 14.672 6.71345 14.672 7.41345V8.58679C14.672 9.28012 14.1054 9.85345 13.4054 9.85345C12.1987 9.85345 11.7054 10.7068 12.312 11.7535C12.6587 12.3601 12.452 13.1335 11.8454 13.4801L10.692 14.1401C10.1654 14.4535 9.48536 14.2668 9.17203 13.7401L9.0987 13.6135C8.4987 12.5668 7.51203 12.5668 6.90536 13.6135L6.83203 13.7401C6.5187 14.2668 5.8387 14.4535 5.31203 14.1401L4.1587 13.4801C3.55203 13.1335 3.34536 12.3535 3.69203 11.7535C4.2987 10.7068 3.80536 9.85345 2.5987 9.85345C1.8987 9.85345 1.33203 9.28012 1.33203 8.58679Z" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> </svg> 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 0000000000..f412bb86fd --- /dev/null +++ b/frontend/resources/flowy_icons/16x/sidebar_footer_trash.svg @@ -0,0 +1,9 @@ +<svg width="19" height="18" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.6"> +<path d="M16.5 4.48535C14.0025 4.23785 11.49 4.11035 8.985 4.11035C7.5 4.11035 6.015 4.18535 4.53 4.33535L3 4.48535" stroke="#171717" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M7.125 3.7275L7.29 2.745C7.41 2.0325 7.5 1.5 8.7675 1.5H10.7325C12 1.5 12.0975 2.0625 12.21 2.7525L12.375 3.7275" stroke="#171717" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M14.8844 6.85449L14.3969 14.407C14.3144 15.5845 14.2469 16.4995 12.1544 16.4995H7.33937C5.24687 16.4995 5.17938 15.5845 5.09688 14.407L4.60938 6.85449" stroke="#171717" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M8.49609 12.375H10.9936" stroke="#171717" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M7.875 9.375H11.625" stroke="#171717" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/> +</g> +</svg> 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 0000000000..2dbb6f52cf --- /dev/null +++ b/frontend/resources/flowy_icons/16x/sidebar_footer_widget.svg @@ -0,0 +1,15 @@ +<svg width="19" height="18" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.6"> +<g clip-path="url(#clip0_611_31756)"> +<path d="M6.25 12H4.3225C2.605 12 1.75 11.145 1.75 9.4275V4.0725C1.75 2.355 2.605 1.5 4.3225 1.5H7.75C9.4675 1.5 10.3225 2.355 10.3225 4.0725" stroke="#171717" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M14.1758 16.5H10.7483C9.03078 16.5 8.17578 15.645 8.17578 13.9275V8.5725C8.17578 6.855 9.03078 6 10.7483 6H14.1758C15.8933 6 16.7483 6.855 16.7483 8.5725V13.9275C16.7483 15.645 15.8933 16.5 14.1758 16.5Z" stroke="#171717" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M11.4023 11.25H13.8473" stroke="#171717" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M12.625 12.4723V10.0273" stroke="#171717" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/> +</g> +</g> +<defs> +<clipPath id="clip0_611_31756"> +<rect width="18" height="18" fill="white" transform="translate(0.25)"/> +</clipPath> +</defs> +</svg> diff --git a/frontend/resources/flowy_icons/16x/three-dots.svg b/frontend/resources/flowy_icons/16x/three-dots.svg index 4d37a346a0..07aa7ca706 100644 --- a/frontend/resources/flowy_icons/16x/three-dots.svg +++ b/frontend/resources/flowy_icons/16x/three-dots.svg @@ -1,3 +1,5 @@ -<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M4.4 10.1001C3.35025 10.1001 2.5 10.9503 2.5 12.0001C2.5 13.0498 3.35025 13.9001 4.4 13.9001C5.44975 13.9001 6.3 13.0498 6.3 12.0001C6.3 10.9503 5.44975 10.1001 4.4 10.1001ZM12 10.1001C10.9502 10.1001 10.1 10.9503 10.1 12.0001C10.1 13.0498 10.9502 13.9001 12 13.9001C13.0498 13.9001 13.9 13.0498 13.9 12.0001C13.9 10.9503 13.0498 10.1001 12 10.1001ZM19.6 10.1001C18.5502 10.1001 17.7 10.9503 17.7 12.0001C17.7 13.0498 18.5502 13.9001 19.6 13.9001C20.6497 13.9001 21.5 13.0498 21.5 12.0001C21.5 10.9503 20.6497 10.1001 19.6 10.1001Z" fill="#C5C7CB"/> +<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M4.76406 8.99995C4.76406 9.6834 4.21001 10.2375 3.52656 10.2375C2.84311 10.2375 2.28906 9.6834 2.28906 8.99995C2.28906 8.3165 2.84311 7.76245 3.52656 7.76245C4.21001 7.76245 4.76406 8.3165 4.76406 8.99995Z" fill="#171717"/> +<path d="M10.2406 8.99995C10.2406 9.6834 9.68658 10.2375 9.00312 10.2375C8.31967 10.2375 7.76562 9.6834 7.76562 8.99995C7.76562 8.3165 8.31967 7.76245 9.00312 7.76245C9.68658 7.76245 10.2406 8.3165 10.2406 8.99995Z" fill="#171717"/> +<path d="M15.7133 8.99995C15.7133 9.6834 15.1592 10.2375 14.4758 10.2375C13.7923 10.2375 13.2383 9.6834 13.2383 8.99995C13.2383 8.3165 13.7923 7.76245 14.4758 7.76245C15.1592 7.76245 15.7133 8.3165 15.7133 8.99995Z" fill="#171717"/> </svg> 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 0000000000..5f92484836 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/title_bar_divider.svg @@ -0,0 +1,5 @@ +<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.5"> +<path d="M4.5 9.375L7.875 6L4.5 2.625" stroke="#171717" stroke-width="0.9" stroke-linecap="round" stroke-linejoin="round"/> +</g> +</svg> diff --git a/frontend/resources/flowy_icons/16x/trash.svg b/frontend/resources/flowy_icons/16x/trash.svg new file mode 100644 index 0000000000..487a57001f --- /dev/null +++ b/frontend/resources/flowy_icons/16x/trash.svg @@ -0,0 +1,7 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M14 3.98763C11.78 3.76763 9.54667 3.6543 7.32 3.6543C6 3.6543 4.68 3.72096 3.36 3.8543L2 3.98763" stroke="#FB006D" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M5.66797 3.31398L5.81464 2.44065C5.9213 1.80732 6.0013 1.33398 7.12797 1.33398H8.87464C10.0013 1.33398 10.088 1.83398 10.188 2.44732L10.3346 3.31398" stroke="#FB006D" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M12.5669 6.09375L12.1336 12.8071C12.0603 13.8537 12.0003 14.6671 10.1403 14.6671H5.86026C4.00026 14.6671 3.94026 13.8537 3.86693 12.8071L3.43359 6.09375" stroke="#FB006D" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M6.88672 11H9.10672" stroke="#FB006D" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M6.33203 8.33398H9.66536" stroke="#FB006D" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/unfavorite.svg b/frontend/resources/flowy_icons/16x/unfavorite.svg index 0ccfc1edff..b984afe017 100644 --- a/frontend/resources/flowy_icons/16x/unfavorite.svg +++ b/frontend/resources/flowy_icons/16x/unfavorite.svg @@ -1,3 +1,5 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M8 3L9.3905 5.96215L12.5 6.44006L10.25 8.74448L10.781 12L8 10.4621L5.219 12L5.75 8.74448L3.5 6.44006L6.6095 5.96215L8 3Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M3.52189 12.4331L3.90189 10.7864C3.98856 10.3998 3.83523 9.85977 3.55523 9.57977L1.90189 7.92643C0.928559 6.9531 1.24189 5.96643 2.60189 5.73977L4.72856 5.38643C5.08189 5.32643 5.50856 5.0131 5.66856 4.68643L6.84189 2.33977C7.47523 1.06643 8.51523 1.06643 9.15523 2.33977L10.3286 4.68643C10.4019 4.83977 10.5419 4.98643 10.6952 5.1131" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M13.401 5.73975C14.761 5.96641 15.081 6.95308 14.101 7.92641L12.4477 9.57975C12.1677 9.85975 12.0143 10.3997 12.101 10.7864L12.5743 12.8331C12.9477 14.4531 12.0877 15.0797 10.6543 14.2331L8.66099 13.0531C8.30099 12.8397 7.70766 12.8397 7.34099 13.0531L5.34766 14.2331" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M14.6693 1.33301L1.33594 14.6663" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> </svg> 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 0000000000..9373ef6dd1 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/view_item_add.svg @@ -0,0 +1,6 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.5"> +<rect x="2" y="7.40039" width="12" height="1.2" rx="0.6" fill="#171717"/> +<rect x="7.40234" y="14" width="12" height="1.2" rx="0.6" transform="rotate(-90 7.40234 14)" fill="#171717"/> +</g> +</svg> 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 0000000000..5fd5a1e719 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/view_item_expand.svg @@ -0,0 +1,5 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.45"> +<path d="M7.83607 10.0988L5.51977 6.9139C5.30345 6.61646 5.51592 6.19922 5.8837 6.19922H10.5163C10.8841 6.19922 11.0966 6.61646 10.8802 6.9139L8.56393 10.0988C8.38422 10.3459 8.01578 10.3459 7.83607 10.0988Z" fill="#171717"/> +</g> +</svg> 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 0000000000..87f9f11949 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/view_item_open_in_new_tab.svg @@ -0,0 +1,5 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M8.62891 7.36667L13.8222 2.17334" stroke="#171717" stroke-width="1.06667" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M14.3291 4.70699V1.66699H11.2891" stroke="#171717" stroke-width="1.06667" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M7.36406 1.66699H6.0974C2.93073 1.66699 1.66406 2.93366 1.66406 6.10033V9.90033C1.66406 13.067 2.93073 14.3337 6.0974 14.3337H9.8974C13.0641 14.3337 14.3307 13.067 14.3307 9.90033V8.63366" stroke="#171717" stroke-width="1.06667" stroke-linecap="round" stroke-linejoin="round"/> +</svg> 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 0000000000..c890184915 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/view_item_rename.svg @@ -0,0 +1,5 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M8.83958 2.40031L3.36624 8.19364C3.15958 8.41364 2.95958 8.84697 2.91958 9.14697L2.67291 11.307C2.58624 12.087 3.14624 12.6203 3.91958 12.487L6.06624 12.1203C6.36624 12.067 6.78624 11.847 6.99291 11.6203L12.4662 5.82697C13.4129 4.82697 13.8396 3.68697 12.3662 2.29364C10.8996 0.913641 9.78624 1.40031 8.83958 2.40031Z" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M7.92578 3.3667C8.21245 5.2067 9.70578 6.61337 11.5591 6.80003" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M2 14.6665H14" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> +</svg> 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 0000000000..de5db7fd68 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/view_item_right_arrow.svg @@ -0,0 +1,10 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.4"> +<mask id="path-1-outside-1_517_47838" maskUnits="userSpaceOnUse" x="5.19922" y="2.20117" width="7" height="11" fill="black"> +<rect fill="white" x="5.19922" y="2.20117" width="7" height="11"/> +<path d="M6.03945 3.37691C5.80803 3.61122 5.80803 3.99112 6.03945 4.22544L9.76857 8.00117L6.03945 11.7769C5.80803 12.0112 5.80803 12.3911 6.03945 12.6254C6.27087 12.8598 6.64608 12.8598 6.8775 12.6254L11.0257 8.42544C11.1368 8.31291 11.1992 8.1603 11.1992 8.00117C11.1992 7.84204 11.1368 7.68943 11.0257 7.57691L6.8775 3.37691C6.64608 3.14259 6.27087 3.14259 6.03945 3.37691Z"/> +</mask> +<path d="M6.03945 3.37691C5.80803 3.61122 5.80803 3.99112 6.03945 4.22544L9.76857 8.00117L6.03945 11.7769C5.80803 12.0112 5.80803 12.3911 6.03945 12.6254C6.27087 12.8598 6.64608 12.8598 6.8775 12.6254L11.0257 8.42544C11.1368 8.31291 11.1992 8.1603 11.1992 8.00117C11.1992 7.84204 11.1368 7.68943 11.0257 7.57691L6.8775 3.37691C6.64608 3.14259 6.27087 3.14259 6.03945 3.37691Z" fill="#171717"/> +<path d="M6.03945 4.22544L6.13432 4.13174L6.13432 4.13174L6.03945 4.22544ZM6.03945 3.37691L6.13432 3.4706L6.13432 3.4706L6.03945 3.37691ZM9.76857 8.00117L9.86344 8.09487C9.91473 8.04293 9.91473 7.95941 9.86344 7.90748L9.76857 8.00117ZM6.03945 11.7769L6.13432 11.8706L6.13432 11.8706L6.03945 11.7769ZM6.03945 12.6254L6.13432 12.5317L6.13432 12.5317L6.03945 12.6254ZM6.8775 12.6254L6.78264 12.5317L6.78264 12.5317L6.8775 12.6254ZM11.0257 8.42544L11.1205 8.51913L11.1205 8.51913L11.0257 8.42544ZM11.0257 7.57691L10.9308 7.6706L10.9308 7.6706L11.0257 7.57691ZM6.8775 3.37691L6.97237 3.28321L6.97237 3.28321L6.8775 3.37691ZM6.13432 4.13174C5.95419 3.94936 5.95419 3.65298 6.13432 3.4706L5.94459 3.28321C5.66187 3.56946 5.66187 4.03288 5.94459 4.31913L6.13432 4.13174ZM9.86344 7.90748L6.13432 4.13174L5.94459 4.31913L9.67371 8.09487L9.86344 7.90748ZM6.13432 11.8706L9.86344 8.09487L9.67371 7.90748L5.94459 11.6832L6.13432 11.8706ZM6.13432 12.5317C5.95419 12.3494 5.95419 12.053 6.13432 11.8706L5.94459 11.6832C5.66187 11.9695 5.66187 12.4329 5.94459 12.7191L6.13432 12.5317ZM6.78264 12.5317C6.60342 12.7132 6.31354 12.7132 6.13432 12.5317L5.94459 12.7191C6.22821 13.0063 6.68875 13.0063 6.97237 12.7191L6.78264 12.5317ZM10.9308 8.33174L6.78264 12.5317L6.97237 12.7191L11.1205 8.51913L10.9308 8.33174ZM11.0659 8.00117C11.0659 8.12547 11.0171 8.24435 10.9308 8.33174L11.1205 8.51913C11.2565 8.38148 11.3326 8.19513 11.3326 8.00117H11.0659ZM10.9308 7.6706C11.0171 7.75799 11.0659 7.87687 11.0659 8.00117H11.3326C11.3326 7.80721 11.2565 7.62086 11.1205 7.48321L10.9308 7.6706ZM6.78264 3.4706L10.9308 7.6706L11.1205 7.48321L6.97237 3.28321L6.78264 3.4706ZM6.13432 3.4706C6.31354 3.28914 6.60342 3.28914 6.78264 3.4706L6.97237 3.28321C6.68875 2.99605 6.22821 2.99605 5.94459 3.28321L6.13432 3.4706Z" fill="#171717" mask="url(#path-1-outside-1_517_47838)"/> +</g> +</svg> 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 0000000000..8971910251 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/view_item_unexpand.svg @@ -0,0 +1,5 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.45"> +<path d="M10.3338 8.33716L7.06207 10.7166C6.78588 10.9174 6.39844 10.7202 6.39844 10.3786V5.61979C6.39844 5.27828 6.78588 5.08099 7.06207 5.28186L10.3338 7.66128C10.5632 7.82815 10.5632 8.17028 10.3338 8.33716Z" fill="#171717"/> +</g> +</svg> 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 0000000000..d264b4734e --- /dev/null +++ b/frontend/resources/flowy_icons/16x/workspace_drop_down_menu_hide.svg @@ -0,0 +1,8 @@ +<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg"> +<mask id="path-1-outside-1_598_58130" maskUnits="userSpaceOnUse" x="1.33203" y="3" width="8" height="5" fill="black"> +<rect fill="white" x="1.33203" y="3" width="8" height="5"/> +<path d="M8.20999 7.62796C8.04727 7.79068 7.78346 7.79068 7.62074 7.62796L4.9987 5.00592L2.37666 7.62796C2.21394 7.79068 1.95012 7.79068 1.7874 7.62796C1.62468 7.46524 1.62468 7.20142 1.7874 7.03871L4.70407 4.12204C4.78221 4.0439 4.88819 4 4.9987 4C5.1092 4 5.21519 4.0439 5.29333 4.12204L8.20999 7.03871C8.37271 7.20142 8.37271 7.46524 8.20999 7.62796Z"/> +</mask> +<path d="M8.20999 7.62796C8.04727 7.79068 7.78346 7.79068 7.62074 7.62796L4.9987 5.00592L2.37666 7.62796C2.21394 7.79068 1.95012 7.79068 1.7874 7.62796C1.62468 7.46524 1.62468 7.20142 1.7874 7.03871L4.70407 4.12204C4.78221 4.0439 4.88819 4 4.9987 4C5.1092 4 5.21519 4.0439 5.29333 4.12204L8.20999 7.03871C8.37271 7.20142 8.37271 7.46524 8.20999 7.62796Z" fill="#171717"/> +<path d="M7.62074 7.62796L7.67966 7.56904L7.62074 7.62796ZM8.20999 7.62796L8.15107 7.56904H8.15107L8.20999 7.62796ZM4.9987 5.00592L4.93977 4.947C4.9554 4.93137 4.9766 4.92259 4.9987 4.92259C5.0208 4.92259 5.042 4.93137 5.05762 4.947L4.9987 5.00592ZM2.37666 7.62796L2.31773 7.56904L2.37666 7.62796ZM1.7874 7.62796L1.84633 7.56904L1.7874 7.62796ZM1.7874 7.03871L1.84633 7.09763L1.7874 7.03871ZM4.70407 4.12204L4.64514 4.06311V4.06311L4.70407 4.12204ZM5.29333 4.12204L5.2344 4.18096L5.2344 4.18096L5.29333 4.12204ZM8.20999 7.03871L8.26892 6.97978L8.20999 7.03871ZM7.67966 7.56904C7.80984 7.69921 8.02089 7.69921 8.15107 7.56904L8.26892 7.68689C8.07366 7.88215 7.75707 7.88215 7.56181 7.68689L7.67966 7.56904ZM5.05762 4.947L7.67966 7.56904L7.56181 7.68689L4.93977 5.06485L5.05762 4.947ZM2.31773 7.56904L4.93977 4.947L5.05762 5.06485L2.43558 7.68689L2.31773 7.56904ZM1.84633 7.56904C1.9765 7.69921 2.18756 7.69921 2.31773 7.56904L2.43558 7.68689C2.24032 7.88215 1.92374 7.88215 1.72848 7.68689L1.84633 7.56904ZM1.84633 7.09763C1.71615 7.22781 1.71615 7.43886 1.84633 7.56904L1.72848 7.68689C1.53322 7.49163 1.53322 7.17504 1.72848 6.97978L1.84633 7.09763ZM4.763 4.18096L1.84633 7.09763L1.72848 6.97978L4.64514 4.06311L4.763 4.18096ZM4.9987 4.08333C4.91029 4.08333 4.82551 4.11845 4.763 4.18096L4.64514 4.06311C4.73891 3.96935 4.86609 3.91667 4.9987 3.91667V4.08333ZM5.2344 4.18096C5.17189 4.11845 5.0871 4.08333 4.9987 4.08333V3.91667C5.13131 3.91667 5.25848 3.96935 5.35225 4.06311L5.2344 4.18096ZM8.15107 7.09763L5.2344 4.18096L5.35225 4.06311L8.26892 6.97978L8.15107 7.09763ZM8.15107 7.56904C8.28124 7.43886 8.28124 7.22781 8.15107 7.09763L8.26892 6.97978C8.46418 7.17504 8.46418 7.49163 8.26892 7.68689L8.15107 7.56904Z" fill="#171717" fill-opacity="0.8" mask="url(#path-1-outside-1_598_58130)"/> +</svg> 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 0000000000..4682de7aa7 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/workspace_drop_down_menu_show.svg @@ -0,0 +1,8 @@ +<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg"> +<mask id="path-1-outside-1_628_27017" maskUnits="userSpaceOnUse" x="0.667969" y="4" width="8" height="5" fill="black"> +<rect fill="white" x="0.667969" y="4" width="8" height="5"/> +<path d="M1.79001 4.37204C1.95273 4.20932 2.21654 4.20932 2.37926 4.37204L5.0013 6.99408L7.62334 4.37204C7.78606 4.20932 8.04988 4.20932 8.2126 4.37204C8.37532 4.53476 8.37532 4.79858 8.2126 4.96129L5.29593 7.87796C5.21779 7.9561 5.11181 8 5.0013 8C4.8908 8 4.78481 7.9561 4.70667 7.87796L1.79001 4.96129C1.62729 4.79858 1.62729 4.53476 1.79001 4.37204Z"/> +</mask> +<path d="M1.79001 4.37204C1.95273 4.20932 2.21654 4.20932 2.37926 4.37204L5.0013 6.99408L7.62334 4.37204C7.78606 4.20932 8.04988 4.20932 8.2126 4.37204C8.37532 4.53476 8.37532 4.79858 8.2126 4.96129L5.29593 7.87796C5.21779 7.9561 5.11181 8 5.0013 8C4.8908 8 4.78481 7.9561 4.70667 7.87796L1.79001 4.96129C1.62729 4.79858 1.62729 4.53476 1.79001 4.37204Z" fill="#171717"/> +<path d="M2.37926 4.37204L2.32034 4.43096L2.37926 4.37204ZM1.79001 4.37204L1.84893 4.43096H1.84893L1.79001 4.37204ZM5.0013 6.99408L5.06023 7.053C5.0446 7.06863 5.0234 7.07741 5.0013 7.07741C4.9792 7.07741 4.958 7.06863 4.94238 7.053L5.0013 6.99408ZM7.62334 4.37204L7.68227 4.43096L7.62334 4.37204ZM8.2126 4.37204L8.15367 4.43096L8.2126 4.37204ZM8.2126 4.96129L8.15367 4.90237L8.2126 4.96129ZM5.29593 7.87796L5.35486 7.93689V7.93689L5.29593 7.87796ZM4.70667 7.87796L4.7656 7.81904L4.7656 7.81904L4.70667 7.87796ZM1.79001 4.96129L1.73108 5.02022L1.79001 4.96129ZM2.32034 4.43096C2.19016 4.30079 1.97911 4.30079 1.84893 4.43096L1.73108 4.31311C1.92634 4.11785 2.24293 4.11785 2.43819 4.31311L2.32034 4.43096ZM4.94238 7.053L2.32034 4.43096L2.43819 4.31311L5.06023 6.93515L4.94238 7.053ZM7.68227 4.43096L5.06023 7.053L4.94238 6.93515L7.56442 4.31311L7.68227 4.43096ZM8.15367 4.43096C8.0235 4.30079 7.81244 4.30079 7.68227 4.43096L7.56442 4.31311C7.75968 4.11785 8.07626 4.11785 8.27152 4.31311L8.15367 4.43096ZM8.15367 4.90237C8.28385 4.77219 8.28385 4.56114 8.15367 4.43096L8.27152 4.31311C8.46678 4.50837 8.46678 4.82496 8.27152 5.02022L8.15367 4.90237ZM5.237 7.81904L8.15367 4.90237L8.27152 5.02022L5.35486 7.93689L5.237 7.81904ZM5.0013 7.91667C5.08971 7.91667 5.17449 7.88155 5.237 7.81904L5.35486 7.93689C5.26109 8.03065 5.13391 8.08333 5.0013 8.08333V7.91667ZM4.7656 7.81904C4.82811 7.88155 4.9129 7.91667 5.0013 7.91667V8.08333C4.86869 8.08333 4.74152 8.03065 4.64775 7.93689L4.7656 7.81904ZM1.84893 4.90237L4.7656 7.81904L4.64775 7.93689L1.73108 5.02022L1.84893 4.90237ZM1.84893 4.43096C1.71876 4.56114 1.71876 4.77219 1.84893 4.90237L1.73108 5.02022C1.53582 4.82496 1.53582 4.50837 1.73108 4.31311L1.84893 4.43096Z" fill="#171717" fill-opacity="0.8" mask="url(#path-1-outside-1_628_27017)"/> +</svg> 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 0000000000..6e44da6b14 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/workspace_logout.svg @@ -0,0 +1,5 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M12.1094 9.84729L14.001 7.95563L12.1094 6.06396" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M6.43359 7.95557H13.9485" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M7.91144 13.8229C4.64537 13.8229 2 11.6061 2 7.91144C2 4.21679 4.64537 2 7.91144 2" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> +</svg> 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 0000000000..73d86f8e02 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/workspace_selected.svg @@ -0,0 +1,10 @@ +<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g id="Frame 1948756070" clip-path="url(#clip0_648_32496)"> +<path id="Vector" d="M0.699219 7.70904L4.5196 11.4628L13.4042 2.80029" stroke="#00C6F1" stroke-width="1.68" stroke-linecap="round" stroke-linejoin="round"/> +</g> +<defs> +<clipPath id="clip0_648_32496"> +<rect width="14" height="14" fill="white"/> +</clipPath> +</defs> +</svg> 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 0000000000..93aa13693a --- /dev/null +++ b/frontend/resources/flowy_icons/16x/workspace_three_dots.svg @@ -0,0 +1,7 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.6"> +<path d="M4.23516 7.9999C4.23516 8.60742 3.74267 9.0999 3.13516 9.0999C2.52764 9.0999 2.03516 8.60742 2.03516 7.9999C2.03516 7.39239 2.52764 6.8999 3.13516 6.8999C3.74267 6.8999 4.23516 7.39239 4.23516 7.9999Z" fill="#171717"/> +<path d="M9.10234 7.9999C9.10234 8.60742 8.60986 9.0999 8.00234 9.0999C7.39483 9.0999 6.90234 8.60742 6.90234 7.9999C6.90234 7.39239 7.39483 6.8999 8.00234 6.8999C8.60986 6.8999 9.10234 7.39239 9.10234 7.9999Z" fill="#171717"/> +<path d="M13.9695 7.9999C13.9695 8.60742 13.477 9.0999 12.8695 9.0999C12.262 9.0999 11.7695 8.60742 11.7695 7.9999C11.7695 7.39239 12.262 6.8999 12.8695 6.8999C13.477 6.8999 13.9695 7.39239 13.9695 7.9999Z" fill="#171717"/> +</g> +</svg> diff --git a/frontend/resources/flowy_icons/24x/settings_selected_theme.svg b/frontend/resources/flowy_icons/24x/settings_selected_theme.svg new file mode 100644 index 0000000000..d6c6b6d809 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/settings_selected_theme.svg @@ -0,0 +1,10 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<circle cx="12" cy="12" r="12" fill="#14AE5C"/> +<circle cx="12" cy="12" r="9" fill="white"/> +<mask id="mask0_3623_112333" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24"> +<rect width="24" height="24" fill="#66CF80"/> +</mask> +<g mask="url(#mask0_3623_112333)"> +<path d="M10.598 16.6L17.648 9.55L16.248 8.15L10.598 13.8L7.74805 10.95L6.34805 12.35L10.598 16.6ZM11.998 22C10.6147 22 9.31471 21.7375 8.09805 21.2125C6.88138 20.6875 5.82305 19.975 4.92305 19.075C4.02305 18.175 3.31055 17.1167 2.78555 15.9C2.26055 14.6833 1.99805 13.3833 1.99805 12C1.99805 10.6167 2.26055 9.31667 2.78555 8.1C3.31055 6.88333 4.02305 5.825 4.92305 4.925C5.82305 4.025 6.88138 3.3125 8.09805 2.7875C9.31471 2.2625 10.6147 2 11.998 2C13.3814 2 14.6814 2.2625 15.898 2.7875C17.1147 3.3125 18.173 4.025 19.073 4.925C19.973 5.825 20.6855 6.88333 21.2105 8.1C21.7355 9.31667 21.998 10.6167 21.998 12C21.998 13.3833 21.7355 14.6833 21.2105 15.9C20.6855 17.1167 19.973 18.175 19.073 19.075C18.173 19.975 17.1147 20.6875 15.898 21.2125C14.6814 21.7375 13.3814 22 11.998 22Z" fill="#66CF80"/> +</g> +</svg> diff --git a/frontend/resources/translations/ar-SA.json b/frontend/resources/translations/ar-SA.json index 409cdfdc28..136bfb3fa1 100644 --- a/frontend/resources/translations/ar-SA.json +++ b/frontend/resources/translations/ar-SA.json @@ -56,7 +56,7 @@ "resetWorkspacePrompt": "ستؤدي إعادة تعيين مساحة العمل إلى حذف جميع الصفحات والبيانات الموجودة بداخلها. هل أنت متأكد أنك تريد إعادة تعيين مساحة العمل؟ وبدلاً من ذلك، يمكنك الاتصال بفريق الدعم لاستعادة مساحة العمل", "hint": "مساحة العمل", "notFoundError": "مساحة العمل غير موجودة", - "failedToLoad": "هناك خطأ ما! فشل تحميل مساحة العمل. حاول إغلاق أي مثيل مفتوح لـ AppFlowy وحاول مرة أخرى.", + "failedToLoad": "هناك خطأ ما! فشل تحميل مساحة العمل. حاول إغلاق أي مثيل مفتوح لـ @:appName وحاول مرة أخرى.", "errorActions": { "reportIssue": "بلغ عن خطأ", "reachOut": "تواصل مع ديسكورد" @@ -279,7 +279,7 @@ "cloudSupabaseUrl": "رابط Supabase", "cloudSupabaseAnonKey": "مفتاح Supabase الخفي", "cloudSupabaseAnonKeyCanNotBeEmpty": "لا يمكن أن يكون المفتاح المجهول فارغًا إذا لم يكن عنوان URL الخاص بـ Supabase فارغًا", - "cloudAppFlowy": "سحابة AppFlowy", + "cloudAppFlowy": "سحابة @:appName", "clickToCopy": "انقر للنسخ", "selfHostStart": "إذا لم يكن لديك خادم، يرجى الرجوع إلى", "selfHostContent": "مستند", @@ -298,7 +298,7 @@ "historicalUserList": "سجل تسجيل دخول المستخدم", "historicalUserListTooltip": "تعرض هذه القائمة حساباتك المجهولة. يمكنك النقر على الحساب لعرض تفاصيله. يتم إنشاء الحسابات المجهولة بالنقر فوق الزر \"البدء\".", "openHistoricalUser": "انقر لفتح الحساب الخفي", - "customPathPrompt": "قد يؤدي تخزين مجلد بيانات AppFlowy في مجلد متزامن على السحابة مثل Google Drive إلى مخاطر. إذا تم الوصول إلى قاعدة البيانات الموجودة في هذا المجلد أو تعديلها من مواقع متعددة في نفس الوقت، فقد يؤدي ذلك إلى حدوث تعارضات في المزامنة وتلف محتمل للبيانات", + "customPathPrompt": "قد يؤدي تخزين مجلد بيانات @:appName في مجلد متزامن على السحابة مثل Google Drive إلى مخاطر. إذا تم الوصول إلى قاعدة البيانات الموجودة في هذا المجلد أو تعديلها من مواقع متعددة في نفس الوقت، فقد يؤدي ذلك إلى حدوث تعارضات في المزامنة وتلف محتمل للبيانات", "supabaseSetting": "إعداد Supabase", "cloudSetting": "إعداد السحابة" }, @@ -337,7 +337,7 @@ "themeUpload": { "button": "رفع", "uploadTheme": "تحميل الموضوع", - "description": "قم بتحميل قالب AppFlowy الخاص بك باستخدام الزر أدناه.", + "description": "قم بتحميل قالب @:appName الخاص بك باستخدام الزر أدناه.", "loading": "يرجى الانتظار بينما نقوم بالتحقق من صحة السمة الخاصة بك وتحميلها ...", "uploadSuccess": "تم تحميل موضوعك بنجاح", "deletionFailure": "فشل حذف الموضوع. حاول حذفه يدويًا.", @@ -370,7 +370,7 @@ "defaultLocation": "أين يتم تخزين بياناتك الآن", "exportData": "قم بتصدير بياناتك", "doubleTapToCopy": "انقر نقرًا مزدوجًا لنسخ المسار", - "restoreLocation": "استعادة المسار الافتراضي AppFlowy", + "restoreLocation": "استعادة المسار الافتراضي @:appName", "customizeLocation": "افتح مجلدًا آخر", "restartApp": "يرجى إعادة تشغيل التطبيق لتصبح التغييرات سارية المفعول.", "exportDatabase": "تصدير قاعدة البيانات", @@ -382,10 +382,10 @@ "defineWhereYourDataIsStored": "حدد مكان تخزين بياناتك", "open": "يفتح", "openFolder": "افتح مجلدًا موجودًا", - "openFolderDesc": "اقرأها واكتبها في مجلد AppFlowy الموجود لديك", + "openFolderDesc": "اقرأها واكتبها في مجلد @:appName الموجود لديك", "folderHintText": "إسم الملف", "location": "إنشاء مجلد جديد", - "locationDesc": "اختر اسمًا لمجلد بيانات AppFlowy", + "locationDesc": "اختر اسمًا لمجلد بيانات @:appName", "browser": "تصفح", "create": "يخلق", "set": "تعيين", @@ -396,7 +396,7 @@ "change": "يتغير", "openLocationTooltips": "افتح دليل بيانات آخر", "openCurrentDataFolder": "افتح دليل البيانات الحالي", - "recoverLocationTooltips": "إعادة التعيين إلى دليل البيانات الافتراضي لـ AppFlowy", + "recoverLocationTooltips": "إعادة التعيين إلى دليل البيانات الافتراضي لـ @:appName", "exportFileSuccess": "تم تصدير الملف بنجاح!", "exportFileFail": "فشل تصدير الملف!", "export": "يصدّر" @@ -904,7 +904,7 @@ "quickJumpYear": "انتقل إلى" }, "errorDialog": { - "title": "خطأ AppFlowy", + "title": "خطأ @:appName", "howToFixFallback": "نأسف للإزعاج! قم بإرسال مشكلة على صفحة GitHub الخاصة بنا والتي تصف الخطأ الخاص بك.", "github": "عرض على جيثب" }, diff --git a/frontend/resources/translations/ca-ES.json b/frontend/resources/translations/ca-ES.json index 2e98070d46..d00c2c4114 100644 --- a/frontend/resources/translations/ca-ES.json +++ b/frontend/resources/translations/ca-ES.json @@ -270,7 +270,7 @@ "invalidCloudURLScheme": "Esquema no vàlid", "cloudServerType": "Servidor al núvol", "cloudLocal": "Local", - "cloudAppFlowy": "AppFlowy Cloud Beta", + "cloudAppFlowy": "@:appName Cloud Beta", "clickToCopy": "Feu clic per copiar", "selfHostContent": "document", "cloudURLHint": "Introduïu l'URL base del vostre servidor", @@ -278,7 +278,7 @@ "inputEncryptPrompt": "Introduïu el vostre secret de xifratge per a", "clickToCopySecret": "Feu clic per copiar el secret", "inputTextFieldHint": "El teu secret", - "importSuccess": "S'ha importat correctament la carpeta de dades d'AppFlowy", + "importSuccess": "S'ha importat correctament la carpeta de dades d'@:appName", "supabaseSetting": "Configuració Supabase" }, "notifications": { @@ -319,7 +319,7 @@ "themeUpload": { "button": "Carrega", "uploadTheme": "Carrega el tema", - "description": "Carregueu el vostre propi tema AppFlowy amb el botó següent.", + "description": "Carregueu el vostre propi tema @:appName amb el botó següent.", "loading": "Si us plau, espereu mentre validem i carreguem el vostre tema...", "uploadSuccess": "El teu tema s'ha penjat correctament", "deletionFailure": "No s'ha pogut suprimir el tema. Intenta esborrar-lo manualment.", @@ -347,7 +347,7 @@ "defaultLocation": "Llegir fitxers i ubicació d'emmagatzematge de dades", "exportData": "Exporteu les vostres dades", "doubleTapToCopy": "Fes doble toc per copiar el camí", - "restoreLocation": "Restaura al camí predeterminat d'AppFlowy", + "restoreLocation": "Restaura al camí predeterminat d'@:appName", "customizeLocation": "Obriu una altra carpeta", "restartApp": "Si us plau, reinicieu l'aplicació perquè els canvis tinguin efecte.", "exportDatabase": "Exportar la base de dades", @@ -359,10 +359,10 @@ "defineWhereYourDataIsStored": "Definiu on s'emmagatzemen les vostres dades", "open": "Obert", "openFolder": "Obre una carpeta existent", - "openFolderDesc": "Llegiu-lo i escriviu-lo a la vostra carpeta AppFlowy existent", + "openFolderDesc": "Llegiu-lo i escriviu-lo a la vostra carpeta @:appName existent", "folderHintText": "nom de la carpeta", "location": "Creació d'una carpeta nova", - "locationDesc": "Trieu un nom per a la vostra carpeta de dades d'AppFlowy", + "locationDesc": "Trieu un nom per a la vostra carpeta de dades d'@:appName", "browser": "Navega", "create": "Crear", "set": "Conjunt", @@ -373,7 +373,7 @@ "change": "Canviar", "openLocationTooltips": "Obriu un altre directori de dades", "openCurrentDataFolder": "Obre el directori de dades actual", - "recoverLocationTooltips": "Restableix al directori de dades predeterminat d'AppFlowy", + "recoverLocationTooltips": "Restableix al directori de dades predeterminat d'@:appName", "exportFileSuccess": "Exporta el fitxer correctament!", "exportFileFail": "Ha fallat l'exportació del fitxer!", "export": "Exporta" @@ -791,7 +791,7 @@ "referencedCalendarPrefix": "Vista de" }, "errorDialog": { - "title": "Error d'AppFlowy", + "title": "Error d'@:appName", "howToFixFallback": "Lamentem les molèsties! Envieu un problema a la nostra pàgina de GitHub que descrigui el vostre error.", "github": "Veure a GitHub" }, diff --git a/frontend/resources/translations/ckb-KU.json b/frontend/resources/translations/ckb-KU.json index 1260ecf330..cb8befcd8a 100644 --- a/frontend/resources/translations/ckb-KU.json +++ b/frontend/resources/translations/ckb-KU.json @@ -64,7 +64,7 @@ "resetWorkspacePrompt": "ڕێستکردنی شوێنی کارەکە هەموو لاپەڕە و داتاکانی ناوی دەسڕێتەوە. ئایا دڵنیای کە دەتەوێت شوێنی کارەکە ڕێست بکەیتەوە؟ یان دەتوانیت پەیوەندی بە تیمی پشتگیرییەوە بکەیت بۆ گەڕاندنەوەی شوێنی کارەکە", "hint": "شوێنی کارکردن", "notFoundError": "هیچ شوێنێکی کار نەدۆزراوە", - "failedToLoad": "هەندێ شت بە هەڵە ڕۆیشت! شکستی هێنا لە بارکردنی شوێنی کارکردن. هەوڵبدە هەر نموونەیەکی کراوەی AppFlowy دابخەیت و دووبارە هەوڵبدەرەوە.", + "failedToLoad": "هەندێ شت بە هەڵە ڕۆیشت! شکستی هێنا لە بارکردنی شوێنی کارکردن. هەوڵبدە هەر نموونەیەکی کراوەی @:appName دابخەیت و دووبارە هەوڵبدەرەوە.", "errorActions": { "reportIssue": "ڕاپۆرت کردنی کێشەیەک", "reportIssueOnGithub": "ڕاپۆرت کردنی کێشەیەک لەسەر گیتهابەوە ", @@ -358,12 +358,12 @@ "historicalUserList": "مێژووی چوونەژوورەوەی بەکارهێنەر", "historicalUserListTooltip": "ئەم لیستە ئەکاونتە بێناوەکانت پیشان دەدات. دەتوانیت کلیک لەسەر ئەکاونتێک بکەیت بۆ بینینی وردەکارییەکانی. ئەکاونتی بێناو بە کلیک کردن لەسەر دوگمەی دەستپێکردن دروست دەکرێت", "openHistoricalUser": "بۆ کردنەوەی ئەکاونتی بێناو کلیک بکە", - "customPathPrompt": "هەڵگرتنی فۆڵدەری داتاکانی AppFlowy لە فۆڵدەرێکی هاوکاتی کڵاود وەک گووگڵ درایڤ دەتوانێت مەترسی دروست بکات. ئەگەر بنکەدراوەی ناو ئەم فۆڵدەرە لە یەک کاتدا لە چەندین شوێنەوە دەستی پێ بگات یان دەستکاری بکرێت، لەوانەیە ببێتە هۆی ناکۆکی هاوکاتکردن و ئەگەری تێکچوونی داتاکان", - "importAppFlowyData": "هێنانی داتا لە فۆڵدەری دەرەکی AppFlowy", + "customPathPrompt": "هەڵگرتنی فۆڵدەری داتاکانی @:appName لە فۆڵدەرێکی هاوکاتی کڵاود وەک گووگڵ درایڤ دەتوانێت مەترسی دروست بکات. ئەگەر بنکەدراوەی ناو ئەم فۆڵدەرە لە یەک کاتدا لە چەندین شوێنەوە دەستی پێ بگات یان دەستکاری بکرێت، لەوانەیە ببێتە هۆی ناکۆکی هاوکاتکردن و ئەگەری تێکچوونی داتاکان", + "importAppFlowyData": "هێنانی داتا لە فۆڵدەری دەرەکی @:appName", "importingAppFlowyDataTip": "هێنانی داتا لە قۆناغی جێبەجێکردندایە. تکایە ئەپەکە دامەخە", - "importAppFlowyDataDescription": "داتا لە فۆڵدەری داتای دەرەکی AppFlowy کۆپی بکە و هاوردە بکە بۆ ناو فۆڵدەری داتاکانی AppFlowy ی ئێستا", - "importSuccess": "بە سەرکەوتوویی فۆڵدەری داتاکانی AppFlowy هاوردە کرد", - "importFailed": "هاوردەکردنی فۆڵدەری داتاکانی AppFlowy شکستی هێنا", + "importAppFlowyDataDescription": "داتا لە فۆڵدەری داتای دەرەکی @:appName کۆپی بکە و هاوردە بکە بۆ ناو فۆڵدەری داتاکانی @:appName ی ئێستا", + "importSuccess": "بە سەرکەوتوویی فۆڵدەری داتاکانی @:appName هاوردە کرد", + "importFailed": "هاوردەکردنی فۆڵدەری داتاکانی @:appName شکستی هێنا", "importGuide": "بۆ زانیاری زیاتر، تکایە بەڵگەنامەی ئاماژەپێکراو بپشکنە" }, "notifications": { @@ -413,7 +413,7 @@ "themeUpload": { "button": "بارکردن", "uploadTheme": "بارکردنی تێم", - "description": "بە بەکارهێنانی دوگمەی خوارەوە تێمی AppFlowy ـەکەت باربکە.", + "description": "بە بەکارهێنانی دوگمەی خوارەوە تێمی @:appName ـەکەت باربکە.", "loading": "تکایە چاوەڕوان بن تا ئێمە تێمی قاڵبەکەت پشتڕاست دەکەینەوە و بار دەکەین...", "uploadSuccess": "تێمی قاڵبەکەت بە سەرکەوتوویی بارکرا", "deletionFailure": "تێمەکە نەسڕدرایەوە. هەوڵبدە بە دەستی لابەریت.", @@ -444,7 +444,7 @@ "defaultLocation": "خوێندنەوەی پەڕگەکان و شوێنی هەڵگرتنی داتاکان", "exportData": "دەرچوون لە داتاکانتەوە بەدەست بهێنە", "doubleTapToCopy": "بۆ کۆپیکردن دووجار کلیک بکە", - "restoreLocation": "گەڕاندنەوە بۆ ڕێڕەوی پێشوەختەی AppFlowy", + "restoreLocation": "گەڕاندنەوە بۆ ڕێڕەوی پێشوەختەی @:appName", "customizeLocation": "فۆڵدەرێکی دیکە بکەرەوە", "restartApp": "تکایە ئەپەکە دابخە و بیکەرەوە بۆ ئەوەی گۆڕانکارییەکان جێبەجێ بکرێن.", "exportDatabase": "هەناردە کردنی بنکەدراوە", @@ -456,10 +456,10 @@ "defineWhereYourDataIsStored": "پێناسە بکە کە داتاکانت لە کوێ هەڵدەگیرێن", "open": "کردنەوە", "openFolder": "فۆڵدەرێکی هەبوو بکەرەوە", - "openFolderDesc": "خوێندن و نووسین بۆ فۆڵدەری AppFlowy ی ئێستات", + "openFolderDesc": "خوێندن و نووسین بۆ فۆڵدەری @:appName ی ئێستات", "folderHintText": "ناوی فۆڵدەر", "location": "دروستکردنی فۆڵدەرێکی نوێ", - "locationDesc": "ناوێک بۆ فۆڵدەری داتاکانی AppFlowy هەڵبژێرە", + "locationDesc": "ناوێک بۆ فۆڵدەری داتاکانی @:appName هەڵبژێرە", "browser": "وێبگەڕ", "create": "دروستکردن", "set": "دانان", @@ -893,7 +893,7 @@ "referencedCalendarPrefix": "دیمەنی..." }, "errorDialog": { - "title": "هەڵەی⛔️ AppFlowy", + "title": "هەڵەی⛔️ @:appName", "howToFixFallback": "ببورن بۆ کێشەکە🥺️! پرسەکە و وەسفەکەی لە لاپەڕەی GitHub ـمان بنێرن.", "github": "بینین لە GitHub" }, diff --git a/frontend/resources/translations/cs-CZ.json b/frontend/resources/translations/cs-CZ.json index b6e1282376..dab1d101e2 100644 --- a/frontend/resources/translations/cs-CZ.json +++ b/frontend/resources/translations/cs-CZ.json @@ -55,7 +55,7 @@ "resetWorkspacePrompt": "Obnovením pracovního prostoru smažete všechny stránky a data v nich. Opravdu chcete obnovit pracovní prostor? Pro obnovení pracovního prostoru můžete kontaktovat podporu.", "hint": "pracovní plocha", "notFoundError": "Pracovní prostor nenalezen", - "failedToLoad": "Něco se pokazilo! Nepodařilo se načíst pracovní prostor. Zkuste zavřít a znovu otevřít AppFlowy a zkuste to znovu.", + "failedToLoad": "Něco se pokazilo! Nepodařilo se načíst pracovní prostor. Zkuste zavřít a znovu otevřít @:appName a zkuste to znovu.", "errorActions": { "reportIssue": "Nahlásit problém", "reachOut": "Ozvat se na Discordu" @@ -265,7 +265,7 @@ "enableSync": "Zapnout synchronizaci", "enableEncrypt": "Šifrovat data", "cloudURL": "URL adresa serveru", - "cloudAppFlowy": "AppFlowy Cloud Beta", + "cloudAppFlowy": "@:appName Cloud Beta", "enableEncryptPrompt": "Zapněte šifrování a zabezpečte svá ", "inputEncryptPrompt": "Vložte prosím Váš šifrovací klíč k", "clickToCopySecret": "Kliknutím zkopírujete šifrovací klíč", @@ -273,7 +273,7 @@ "historicalUserList": "Historie přihlášení uživatele", "historicalUserListTooltip": "V tomto seznamu vidíte anonymní účty. Kliknutím na účet zobrazíte jeho detaily. Anonymní účty vznikají kliknutím na tlačítko \"Začínáme\"", "openHistoricalUser": "Kliknutím založíte anonymní účet", - "customPathPrompt": "Uložením složky s daty AppFlowy ve složce synchronizovanéí jako např. Google Drive může nést rizika. Pokud se databáze v složce navštíví nebo změní ", + "customPathPrompt": "Uložením složky s daty @:appName ve složce synchronizovanéí jako např. Google Drive může nést rizika. Pokud se databáze v složce navštíví nebo změní ", "cloudSetting": "Nastavení cloudu" }, "notifications": { @@ -311,7 +311,7 @@ "themeUpload": { "button": "Nahrát", "uploadTheme": "Nahrát motiv vzhledu", - "description": "Nahrajte vlastní motiv vzhledu pro AppFlowy stisknutím tlačítka níže.", + "description": "Nahrajte vlastní motiv vzhledu pro @:appName stisknutím tlačítka níže.", "loading": "Prosím počkejte dokud nedokončíme kontrolu a nahrávání vašeho motivu vzhledu...", "uploadSuccess": "Váš motiv vzhledu byl úspěšně nahrán", "deletionFailure": "Nepodařilo se smazat motiv vzhledu. Zkuste ho smazat ručně.", @@ -342,7 +342,7 @@ "defaultLocation": "Umístění pro čtení a ukládání dat", "exportData": "Exportovat data", "doubleTapToCopy": "Dvojitým klepnutím zkopírujete cestu", - "restoreLocation": "Obnovit výchozí AppFlowy cestu", + "restoreLocation": "Obnovit výchozí @:appName cestu", "customizeLocation": "OtevřítProsím tre další složku", "restartApp": "Aby se projevily změny, restartujte prosím aplikaci.", "exportDatabase": "Exportovat databázi", @@ -354,10 +354,10 @@ "defineWhereYourDataIsStored": "Vyberte kde jsou ukládána Vaše data", "open": "Otevřít", "openFolder": "Otevřít existující složku", - "openFolderDesc": "Číst a zapisovat do existující AppFlowy složky", + "openFolderDesc": "Číst a zapisovat do existující @:appName složky", "folderHintText": "název složky", "location": "Vytváření nové složky", - "locationDesc": "Vyberte název pro složku, kam bude AppFlowy ukládat Vaše data", + "locationDesc": "Vyberte název pro složku, kam bude @:appName ukládat Vaše data", "browser": "Procházet", "create": "Vytvořit", "set": "Nastavit", @@ -836,7 +836,7 @@ "referencedCalendarPrefix": "Pohled na" }, "errorDialog": { - "title": "Chyba AppFlowy", + "title": "Chyba @:appName", "howToFixFallback": "Omlouváme se za nepříjemnost! Pošlete hlášení na náš GitHub, kde popíšete chybu na kterou jste narazili.", "github": "Zobrazit na GitHubu" }, diff --git a/frontend/resources/translations/de-DE.json b/frontend/resources/translations/de-DE.json index d8b12aea2e..3f4732539b 100644 --- a/frontend/resources/translations/de-DE.json +++ b/frontend/resources/translations/de-DE.json @@ -70,32 +70,32 @@ "LogInWithDiscord": "Mit Discord-Account anmelden" }, "workspace": { - "chooseWorkspace": "Workspace wählen", - "create": "Workspace erstellen", - "reset": "Workspace zurücksetzen", - "resetWorkspacePrompt": "Das Zurücksetzen des Workspace löscht alle enthaltenen Seiten und Daten. Bist du sicher, dass du den Arbeitsbereich zurücksetzen möchstest? ", - "hint": "Workspace", - "notFoundError": "Workspace nicht gefunden", - "failedToLoad": "Etwas ist schief gelaufen! Der Workspace konnte nicht geladen werden. Versuche, alle AppFlowy-Instanzen zu schließen & versuche es erneut.", + "chooseWorkspace": "Arbeitsbereich wählen", + "create": "Arbeitsbereich erstellen", + "reset": "Arbeitsbereich zurücksetzen", + "resetWorkspacePrompt": "Das Zurücksetzen des Arbeitsbereiches löscht alle enthaltenen Seiten und Daten. Bist du sicher, dass du den Arbeitsbereich zurücksetzen möchtest? ", + "hint": "Arbeitsbereich", + "notFoundError": "Arbeitsbereich nicht gefunden", + "failedToLoad": "Etwas ist schief gelaufen! Der Arbeitsbereich konnte nicht geladen werden. Versuche, alle @:appName Instanzen zu schließen und versuche es erneut.", "errorActions": { "reportIssue": "Problem melden", - "reportIssueOnGithub": "Melde ein Problem auf Github", + "reportIssueOnGithub": "Melde ein Problem auf GitHub", "exportLogFiles": "Exportiere Log-Dateien", "reachOut": "Kontaktiere uns auf Discord" }, "menuTitle": "Arbeitsbereiche", - "deleteWorkspaceHintText": "Sicher, dass du dein Workspace löschen möchtest?\nDies kann nicht mehr Rückgängig gemacht werden.", - "createSuccess": "Workspace erfolgreich erstellt", - "createFailed": "Der Workspace konnte nicht erstellt werden", - "createLimitExceeded": "Du hast die für dein Benutzerkonto maximal zulässige Anzahl an Arbeitsbereichen erreicht. Benötigst du zum fortsetzen deiner Arbeit noch weitere Arbeitsbereiche, erstelle auf Github bitte eine entsprechende Anfrage.", - "deleteSuccess": "Workspace erfolgreich gelöscht", - "deleteFailed": "Der Workspace konnte nicht gelöscht werden", - "openSuccess": "Workspace erfolgreich geöffnet", - "openFailed": "Der Workspace konnte nicht geöffnet werden", - "renameSuccess": "Workspace erfolgreich umbenannt", - "renameFailed": "Der Workspace konnte nicht umbenannt werden", - "updateIconSuccess": "Workspace erfolgreich zurückgesetzt", - "updateIconFailed": "Der Workspace konnte nicht zurückgesetzt werden", + "deleteWorkspaceHintText": "Sicher, dass du deinen Arbeitsbereich löschen möchtest? Dies kann nicht mehr Rückgängig gemacht werden.", + "createSuccess": "Arbeitsbereich erfolgreich erstellt", + "createFailed": "Der Arbeitsbereich konnte nicht erstellt werden", + "createLimitExceeded": "Du hast die für dein Benutzerkonto maximal zulässige Anzahl an Arbeitsbereichen erreicht. Benötigst du zum fortsetzen deiner Arbeit noch weitere Arbeitsbereiche, erstelle auf GitHub bitte eine entsprechende Anfrage.", + "deleteSuccess": "Arbeitsbereich erfolgreich gelöscht", + "deleteFailed": "Der Arbeitsbereich konnte nicht gelöscht werden", + "openSuccess": "Arbeitsbereich erfolgreich geöffnet", + "openFailed": "Der Arbeitsbereich konnte nicht geöffnet werden", + "renameSuccess": "Arbeitsbereich erfolgreich umbenannt", + "renameFailed": "Der Arbeitsbereich konnte nicht umbenannt werden", + "updateIconSuccess": "Arbeitsbereich erfolgreich zurückgesetzt", + "updateIconFailed": "Der Arbeitsbereich konnte nicht zurückgesetzt werden", "cannotDeleteTheOnlyWorkspace": "Der einzig vorhandene Arbeitsbereich kann nicht gelöscht werden", "fetchWorkspacesFailed": "Arbeitsbereiche konnten nicht abgerufen werden!", "leaveCurrentWorkspace": "Arbeitsbereich verlassen", @@ -246,7 +246,7 @@ "addAPageToWorkspace": "Eine Seite zum Arbeitsbereich hinzufügen", "recent": "Zuletzt", "public": "Öffentlich", - "clickToHidePublic": "Hier klicken, um den öffentlichen Raum auszublenden.\nVon dir hier erstellte Seiten sind für jedes Mitglied sichtbar.", + "clickToHidePublic": "Hier klicken, um den öffentlichen Bereich auszublenden.\nHier erstellte Seiten sind für jedes Mitglied sichtbar.", "addAPageToPublic": "Eine Seite zum öffentlichen Bereich hinzufügen." }, "notifications": { @@ -329,10 +329,10 @@ "accountPage": { "menuLabel": "Mein Konto", "title": "Mein Konto", - "description": "Passe dein Profil an, verwalte deine Sicherheitseinstellungen und AI-API-Schlüssel oder melde dich bei deinem Konto an.", + "description": "Passe dein Profil an, verwalte deine Sicherheitseinstellungen und KI API-Schlüssel oder melde dich bei deinem Konto an.", "general": { "title": "Kontoname und Profilbild", - "changeProfilePicture": "Ändern" + "changeProfilePicture": "Profilbild ändern" }, "email": { "title": "E-Mail", @@ -358,18 +358,19 @@ "workspacePage": { "menuLabel": "Arbeitsbereich", "title": "Arbeitsbereich", - "description": "Passe das Erscheinungsbild, das Design, die Schriftart, das Textlayout, das Datums-/Zeitformat und die Sprache deines Arbeitsbereichs an.", + "description": "Passe das Erscheinungsbild, das Design, die Schriftart, das Textlayout, das Datums-/Zeitformat und die Sprache deines Arbeitsbereiches an.", "workspaceName": { - "title": "Name des Arbeitsbereichs", - "savedMessage": "Name des Arbeitsbereichs gespeichert" + "title": "Name des Arbeitsbereiches", + "savedMessage": "Name des Arbeitsbereiches gespeichert", + "editTooltip": "Name des Arbeitsbereiches ändern" }, "workspaceIcon": { - "title": "Arbeitsbereich-Symbol", - "description": "Passe das Erscheinungsbild, das Design, die Schriftart, das Textlayout, das Datum, die Uhrzeit und die Sprache deines Arbeitsbereichs an." + "title": "Symbol", + "description": "Lade ein Bild hoch oder verwende ein Emoji für deinen Arbeitsbereich. Das Symbol wird in deiner Seitenleiste und in deinen Benachrichtigungen angezeigt." }, "appearance": { "title": "Aussehen", - "description": "Passe das Erscheinungsbild, das Design, die Schriftart, das Textlayout, das Datums-/Zeitformat und die Sprache deines Arbeitsbereichs an.", + "description": "Passe das Erscheinungsbild, das Design, die Schriftart, das Textlayout, das Datums-/Zeitformat und die Sprache deines Arbeitsbereiches an.", "options": { "system": "Auto", "light": "Hell", @@ -381,7 +382,7 @@ "description": "Wähle ein voreingestelltes Design aus oder lade dein eigenes benutzerdefiniertes Design hoch." }, "workspaceFont": { - "title": "Schriftart Arbeitsbereich" + "title": "Schriftart" }, "textDirection": { "title": "Textrichtung", @@ -404,7 +405,7 @@ "local": "Lokal", "us": "US", "iso": "ISO", - "friendly": "leserlich", + "friendly": "Leserlich", "dmy": "T/M/J" } }, @@ -425,6 +426,53 @@ "deleteWorkspace": "Arbeitsbereich löschen" } }, + "manageDataPage": { + "menuLabel": "Daten verwalten", + "title": "Daten verwalten", + "description": "Verwalte den lokalen Datenspeicher oder importiere deine vorhandenen Daten in @:appName. Du kannst deine Daten mit Ende-zu-Ende-Verschlüsselung absichern.", + "dataStorage": { + "title": "Speicherort", + "tooltip": "Das Verzeichnis, in dem deine Dateien gespeichert sind", + "actions": { + "change": "Pfad ändern", + "open": "Ordner öffnen", + "openTooltip": "Aktuellen Speicherort des Datenordners öffnen", + "copy": "Pfad kopieren", + "copiedHint": "Link kopiert!", + "resetTooltip": "Auf Standardspeicherort zurücksetzen" + }, + "resetDialog": { + "title": "Bist du sicher?", + "description": "Durch das Zurücksetzen des Pfads auf das Standardverzeichnis werden deine Daten nicht gelöscht. Wenn du deine aktuellen Daten erneut importieren möchtest, solltest du zuerst den Pfad deines aktuellen Speicherorts kopieren." + } + }, + "importData": { + "title": "Daten importieren", + "tooltip": "Daten aus @:appName Backups-/Datenordnern importieren", + "description": "Daten aus einem externen @:appName Datenordner kopieren und in den aktuellen @:appName Datenordner importieren", + "action": "Ordner durchsuchen" + }, + "encryption": { + "title": "Verschlüsselung", + "tooltip": "Verwalte, wie deine Daten gespeichert und verschlüsselt werden", + "descriptionNoEncryption": "Durch das Einschalten der Verschlüsselung werden alle Daten verschlüsselt. Dieser Vorgang kann nicht rückgängig gemacht werden.", + "descriptionEncrypted": "Deine Daten sind verschlüsselt.", + "action": "Daten verschlüsseln", + "dialog": { + "title": "Alle deine Daten verschlüsseln?", + "description": "Durch die Verschlüsselung all deiner Daten bleiben diese sicher und geschützt. Diese Aktion kann NICHT rückgängig gemacht werden. Möchtest du wirklich fortfahren?" + } + }, + "cache": { + "title": "Cache leeren", + "description": "Wenn Bilder nicht geladen werden oder Schriftarten nicht richtig angezeigt werden, versuche den Cache zu leeren. Deine Benutzerdaten werden dadurch nicht gelöscht.", + "dialog": { + "title": "Bist du sicher?", + "description": "Durch das Leeren des Caches werden Bilder und Schriftarten beim Laden erneut heruntergeladen. Deine Daten werden durch diese Aktion weder entfernt noch geändert.", + "successHint": "Cache geleert!" + } + } + }, "common": { "reset": "Zurücksetzen" }, @@ -452,18 +500,18 @@ "cloudSupabaseUrlCanNotBeEmpty": "Die Supabase-URL darf nicht leer sein", "cloudSupabaseAnonKey": "Supabase anonymer Schlüssel", "cloudSupabaseAnonKeyCanNotBeEmpty": "Der anonyme Schlüssel darf nicht leer sein", - "cloudAppFlowy": "AppFlowy Cloud [BETA]", - "cloudAppFlowySelfHost": "AppFlowy Cloud Self-hosted", + "cloudAppFlowy": "@:appName Cloud Beta", + "cloudAppFlowySelfHost": "@:appName Cloud Self-hosted", "appFlowyCloudUrlCanNotBeEmpty": "Die Cloud-URL darf nicht leer sein", "clickToCopy": "Klicken, um zu kopieren", - "selfHostStart": "Falls du keinen Server hast, nehme lieber folgende", + "selfHostStart": "Falls du keinen Server hast, konsultiere bitte", "selfHostContent": "Dokument", - "selfHostEnd": "für Hilfe, um einen einen eigenen Server aufzusetzen", + "selfHostEnd": "um einen einen eigenen Server aufzusetzen", "cloudURLHint": "Eingabe der Basis- URL Ihres Servers", "cloudWSURL": "Websocket URL", "cloudWSURLHint": "Eingbe der Websocket Adresse Ihres Servers", "restartApp": "Neustart", - "restartAppTip": "Programm neustarten, um die Änderungen zu übernehmen. Bitte bachten, dass der aktuelle Account eventuell ausgeloggt wird.", + "restartAppTip": "Programm neustarten, um die Änderungen zu übernehmen. Bitte beachten, dass der aktuelle Account eventuell ausgeloggt wird.", "changeServerTip": "Nach dem Wechsel des Servers muss auf die Schaltfläche „Neustart“ geklickt werden, damit die Änderungen wirksam werden", "enableEncryptPrompt": "Verschlüsselung aktivieren, um deine Daten mit dem Secret Key zu verschlüsseln. Verwahre den Schlüssel sicher! \nEinmal aktiviert kann es nicht mehr rückgängig gemacht werden.\nFalls der Schlüssel verloren geht sind die Daten unwiderbringlich verloren.\nKlicken, um zu kopieren.", "inputEncryptPrompt": "Bitte den Encryption Secret Code eingeben", @@ -474,12 +522,12 @@ "historicalUserList": "Anmeldeverlauf", "historicalUserListTooltip": "Diese Liste zeigt deine anonymen Accounts. Du kannst einen Account anklicken, um mehr Informationen zu sehen.\nAnonyme Accounts werden über den 'Erste Schritte' Button erstellt.", "openHistoricalUser": "Klicken, um einen anonymen Account zu öffnen", - "customPathPrompt": "Den AppFlowy Daten-Ordner in einem mit der Cloud synchronisierten Ordner (z.B. Google Drive) zu speichern, könnte Risiken bergen. Falls die Datenbank innerhalb dieses Ordners gleichzeitig von mehreren Orten zugegriffen oder verändert wird könnte es zu Synchronisationskonflikten und potentiellen Daten-Beschädigung führen", - "importAppFlowyData": "Daten von einem externen AppFlowy Ordner importieren.", + "customPathPrompt": "Den @:appName Daten-Ordner in einem mit der Cloud synchronisierten Ordner (z.B. Google Drive) zu speichern, könnte Risiken bergen. Falls die Datenbank innerhalb dieses Ordners gleichzeitig von mehreren Orten zugegriffen oder verändert wird könnte dies zu Synchronisationskonflikten und potentiellen Daten-Beschädigungen führen", + "importAppFlowyData": "Daten von einem externen @:appName Ordner importieren.", "importingAppFlowyDataTip": "Der Datenimport läuft. Bitte die App nicht schließen oder in den Hintergrund setzten", - "importAppFlowyDataDescription": "Daten von einem externen AppFlowy Ordner kopieren und in den aktuellen AppFlowy Datenordner importieren.", - "importSuccess": "Der AppFlowy Dateienordner wurde erfolgreich importiert", - "importFailed": "Der AppFlowy Dateienordner-Import ist fehlgeschlagen", + "importAppFlowyDataDescription": "Daten von einem externen @:appName Ordner kopieren und in den aktuellen @:appName Datenordner importieren.", + "importSuccess": "Der @:appName Dateienordner wurde erfolgreich importiert", + "importFailed": "Der @:appName Dateienordner-Import ist fehlgeschlagen", "importGuide": "Für weitere Details, bitte das verlinkte Dokument prüfen" }, "notifications": { @@ -503,8 +551,8 @@ }, "fontScaleFactor": "Schriftgröße", "documentSettings": { - "cursorColor": "Dokument Cursor-Farbe", - "selectionColor": "Dokument Auswahl-Farbe", + "cursorColor": "Cursor-Farbe", + "selectionColor": "Auswahl-Farbe", "hexEmptyError": "Hex-Farbe darf nicht leer sein", "hexLengthError": "Hex-Wert muss 6 Zeichen lang sein", "hexInvalidError": "Ungültiger Hex-Wert", @@ -531,13 +579,13 @@ "themeUpload": { "button": "Hochladen", "uploadTheme": "Theme hochladen", - "description": "Lade eigenes AppFlowy-Theme über die Schaltfläche unten hoch.", + "description": "Lade eigenes @:appName-Theme über die untere Schaltfläche hoch.", "loading": "Bitte warte einen Moment . . .\nWir validieren gerade dein Theme und laden es hoch.", "uploadSuccess": "Das Theme wurde erfolgreich hochgeladen", "deletionFailure": "Das Theme konnte nicht gelöscht werden. Versuche, es manuell zu löschen.", "filePickerDialogTitle": "Wähle eine .flowy_plugin-Datei", "urlUploadFailure": "URL konnte nicht geöffnet werden: {}", - "failure": "Das hochgeladene Theme hatte ein ungültiges Format." + "failure": "Das hochgeladene Theme hat ein ungültiges Format." }, "theme": "Theme", "builtInsLabel": "Integrierte Theme", @@ -547,7 +595,7 @@ "local": "Lokal", "us": "US", "iso": "ISO", - "friendly": "Freundlich", + "friendly": "Leserlich", "dmy": "TT/MM/JJJJ" }, "timeFormat": { @@ -556,7 +604,7 @@ "twentyFourHour": "24 Stunden" }, "showNamingDialogWhenCreatingPage": "Zeige Bennenungsfenster, wenn eine neue Seite erstellt wird", - "enableRTLToolbarItems": "Aktivieren Sie RTL-Symbolleiste", + "enableRTLToolbarItems": "RTL-Symbolleistenelemente aktivieren", "members": { "title": "Mitglieder-Einstellungen", "inviteMembers": "Mitglieder einladen", @@ -565,7 +613,7 @@ "label": "Mitglieder", "user": "Nutzer", "role": "Rolle", - "removeFromWorkspace": "Vom Workspace entfernen", + "removeFromWorkspace": "Vom Arbeitsbereich entfernen", "owner": "Besitzer", "guest": "Gast", "member": "Mitglied", @@ -590,10 +638,10 @@ }, "files": { "copy": "Kopieren", - "defaultLocation": "Dateien und Speicherort", + "defaultLocation": "@:appName Datenverzeichnis", "exportData": "Daten exportieren", "doubleTapToCopy": "Zweimal tippen, um den Pfad zu kopieren", - "restoreLocation": "AppFlowy-Standardpfad wiederherstellen", + "restoreLocation": "@:appName-Standardpfad wiederherstellen", "customizeLocation": "Einen anderen Ordner öffnen", "restartApp": "Bitte starten Sie die App neu, damit die Änderungen wirksam werden.", "exportDatabase": "Datenbank exportieren", @@ -605,10 +653,10 @@ "defineWhereYourDataIsStored": "Wo sind die Daten gespeichert?", "open": "Offen", "openFolder": "Einen vorhandenen Ordner öffnen", - "openFolderDesc": "Öffnen und speichern im vorhandenen AppFlowy-Ordner", + "openFolderDesc": "Öffnen und speichern im vorhandenen @:appName-Ordner", "folderHintText": "Ordnernamen", "location": "Ein neuen Ordner erstellen", - "locationDesc": "Einen Namen für den AppFlowy-Datenordner festlegen", + "locationDesc": "Einen Namen für den @:appName Datenordner festlegen", "browser": "Durchsuchen", "create": "Erstellen", "set": "Festlegen", @@ -619,7 +667,7 @@ "change": "Ändern", "openLocationTooltips": "Win anderes Datenverzeichnis öffnen", "openCurrentDataFolder": "Aktuelles Datenverzeichnis öffnen", - "recoverLocationTooltips": "Zurücksetzen auf das Standarddatenverzeichnis von AppFlowy", + "recoverLocationTooltips": "Zurücksetzen auf das Standarddatenverzeichnis von @:appName", "exportFileSuccess": "Datei erfolgreich exportiert!", "exportFileFail": "Datei-Export fehlgeschlagen!", "export": "Export", @@ -696,7 +744,11 @@ "typeAValue": "Einen Wert eingeben...", "layout": "Layout", "databaseLayout": "Layout", - "viewList": "Datenbank-Ansichten", + "viewList": { + "zero": "0 Aufrufe", + "one": "{count} Aufruf", + "other": "{count} Aufrufe" + }, "editView": "Ansicht editieren", "boardSettings": "Board-Einstellungen", "calendarSettings": "Kalender-Einstellungen", @@ -1164,6 +1216,7 @@ }, "errorBlock": { "theBlockIsNotSupported": "Die aktuelle Version unterstützt diesen Block nicht.", + "clickToCopyTheBlockContent": "Hier klicken, um den Blockinhalt zu kopieren", "blockContentHasBeenCopied": "Der Blockinhalt wurde kopiert." }, "mobilePageSelector": { @@ -1211,7 +1264,9 @@ "showGroup": "Zeige die Gruppe", "showGroupContent": "Sicher, dass diese Gruppe auf dem Board angezeigt werden soll?", "failedToLoad": "Boardansicht konnte nicht geladen werden" - } + }, + "noGroup": "Keine Gruppierung nach Eigenschaft", + "noGroupDesc": "Board-Ansichten benötigen eine Eigenschaft zum Gruppieren, um angezeigt zu werden" }, "calendar": { "menuName": "Kalender", @@ -1248,7 +1303,7 @@ "duplicateEvent": "Doppeltes Ereignis" }, "errorDialog": { - "title": "AppFlowy-Fehler", + "title": "@:appName-Fehler", "howToFixFallback": "Wir entschuldigen uns für die Unannehmlichkeiten! Reiche auf unserer GitHub-Seite ein Problem ein, das Ihren Fehler beschreibt.", "github": "Auf GitHub ansehen" }, @@ -1489,7 +1544,7 @@ "opacity": "Transparenz", "resetToDefaultColor": "Auf Standardfarben zurücksetzen", "ltr": "LTR (Links nach rechts)", - "rtl": "RTL (Rechts nach lins)", + "rtl": "RTL (rechts nach links)", "auto": "Auto", "cut": "Ausschneiden", "copy": "Kopieren", @@ -1572,7 +1627,7 @@ "deleteMyAccount": "Mein Benutzerkonto löschen", "dialogTitle": "Benutzerkonto löschen", "dialogContent1": "Bist du sicher, dass du dein Benutzerkonto unwiderruflich löschen möchtest?", - "dialogContent2": "Diese Aktion kann nicht rückgängig gemacht werden und führt dazu, dass der Zugriff auf alle Teambereiche aufgehoben wird, Ihr gesamtes Benutzerkonto, einschließlich privater Arbeitsbereiche, gelöscht wird und Sie aus allen freigegebenen Arbeitsbereichen entfernt werden." + "dialogContent2": "Diese Aktion kann nicht rückgängig gemacht werden und führt dazu, dass der Zugriff auf alle Teambereiche aufgehoben wird, dein gesamtes Benutzerkonto, einschließlich privater Arbeitsbereiche, gelöscht wird und du aus allen freigegebenen Arbeitsbereichen entfernt wirst." } }, "workplace": { @@ -1581,8 +1636,8 @@ "subtitle": "Passe das Erscheinungsbild, das Design, die Schriftart, das Textlayout, das Datum, die Uhrzeit und die Sprache deines Arbeitsbereiches an.", "workplaceName": "Name des Arbeitsbereiches", "workplaceNamePlaceholder": "Gib den Namen des Arbeitsbereiches ein", - "workplaceIcon": "Symbol für den Arbeitsbereich", - "workplaceIconSubtitle": "Lade ein Bild hoch oder verwende ein Emoji für deinen Arbeitsbereich. Das Symbol wird in deiner Seitenleiste und in deinen Benachrichtigungen angezeigt", + "workplaceIcon": "Symbol", + "workplaceIconSubtitle": "Lade ein Bild hoch oder verwende ein Emoji für deinen Arbeitsbereich. Das Symbol wird in deiner Seitenleiste und in deinen Benachrichtigungen angezeigt.", "renameError": "Umbenennen des Arbeitsbereiches fehlgeschlagen", "updateIconError": "Symbol konnte nicht aktualisiert werden", "appearance": { @@ -1616,7 +1671,7 @@ "none": "Keines", "photoPermissionDescription": "Erlaube den Zugriff auf die Fotobibliothek zum Hochladen von Bildern.", "openSettings": "Einstellungen öffnen", - "photoPermissionTitle": "AppFlowy möchte auf deine Fotobibliothek zugreifen", + "photoPermissionTitle": "@:appName möchte auf deine Fotobibliothek zugreifen", "doNotAllow": "Nicht zulassen", "image": "Bild" }, diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 88ebedb546..04ab72f795 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -70,10 +70,11 @@ "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", - "failedToLoad": "Something went wrong! Failed to load the workspace. Try to close any open instance of AppFlowy and try again.", + "failedToLoad": "Something went wrong! Failed to load the workspace. Try to close any open instance of @:appName and try again.", "errorActions": { "reportIssue": "Report an issue", "reportIssueOnGithub": "Report an issue on Github", @@ -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!", @@ -353,11 +360,12 @@ "description": "Customize your workspace appearance, theme, font, text layout, date-/time-format, and language.", "workspaceName": { "title": "Workspace name", - "savedMessage": "Saved workspace name" + "savedMessage": "Saved workspace name", + "editTooltip": "Edit workspace name" }, "workspaceIcon": { "title": "Workspace icon", - "description": "Customize your workspace appearance, theme, font, text layout, date, time, and language." + "description": "Upload an image or use an emoji for your workspace. Icon will show in your sidebar and notifications." }, "appearance": { "title": "Appearance", @@ -420,7 +428,7 @@ "manageDataPage": { "menuLabel": "Manage data", "title": "Manage data", - "description": "Manage data local storage or Import your existing data into Appflowy. You can secure your data with end to end encryption.", + "description": "Manage data local storage or Import your existing data into @:appName. You can secure your data with end to end encryption.", "dataStorage": { "title": "File storage location", "tooltip": "The location where your files are stored", @@ -429,7 +437,8 @@ "open": "Open folder", "openTooltip": "Open current data folder location", "copy": "Copy path", - "copiedHint": "Link copied!" + "copiedHint": "Path copied!", + "resetTooltip": "Reset to default location" }, "resetDialog": { "title": "Are you sure?", @@ -438,8 +447,8 @@ }, "importData": { "title": "Import data", - "tooltip": "Import data from AppFlowy backups/data folders", - "description": "Copy data from an external AppFlowy data folder and import it into the current AppFlowy data folder", + "tooltip": "Import data from @:appName backups/data folders", + "description": "Copy data from an external @:appName data folder and import it into the current @:appName data folder", "action": "Browse folder" }, "encryption": { @@ -490,8 +499,8 @@ "cloudSupabaseUrlCanNotBeEmpty": "The supabase url can't be empty", "cloudSupabaseAnonKey": "Supabase anon key", "cloudSupabaseAnonKeyCanNotBeEmpty": "The anon key can't be empty", - "cloudAppFlowy": "AppFlowy Cloud Beta", - "cloudAppFlowySelfHost": "AppFlowy Cloud Self-hosted", + "cloudAppFlowy": "@:appName Cloud Beta", + "cloudAppFlowySelfHost": "@:appName Cloud Self-hosted", "appFlowyCloudUrlCanNotBeEmpty": "The cloud url can't be empty", "clickToCopy": "Click to copy", "selfHostStart": "If you don't have a server, please refer to the", @@ -512,12 +521,12 @@ "historicalUserList": "User login history", "historicalUserListTooltip": "This list displays your anonymous accounts. You can click on an account to view its details. Anonymous accounts are created by clicking the 'Get Started' button", "openHistoricalUser": "Click to open the anonymous account", - "customPathPrompt": "Storing the AppFlowy data folder in a cloud-synced folder such as Google Drive can pose risks. If the database within this folder is accessed or modified from multiple locations at the same time, it may result in synchronization conflicts and potential data corruption", - "importAppFlowyData": "Import Data from External AppFlowy Folder", + "customPathPrompt": "Storing the @:appName data folder in a cloud-synced folder such as Google Drive can pose risks. If the database within this folder is accessed or modified from multiple locations at the same time, it may result in synchronization conflicts and potential data corruption", + "importAppFlowyData": "Import Data from External @:appName Folder", "importingAppFlowyDataTip": "Data import is in progress. Please do not close the app", - "importAppFlowyDataDescription": "Copy data from an external AppFlowy data folder and import it into the current AppFlowy data folder", - "importSuccess": "Successfully imported the AppFlowy data folder", - "importFailed": "Importing the AppFlowy data folder failed", + "importAppFlowyDataDescription": "Copy data from an external @:appName data folder and import it into the current AppFlowy data folder", + "importSuccess": "Successfully imported the @:appName data folder", + "importFailed": "Importing the @:appName data folder failed", "importGuide": "For further details, please check the referenced document" }, "notifications": { @@ -569,7 +578,7 @@ "themeUpload": { "button": "Upload", "uploadTheme": "Upload theme", - "description": "Upload your own AppFlowy theme using the button below.", + "description": "Upload your own @:appName theme using the button below.", "loading": "Please wait while we validate and upload your theme...", "uploadSuccess": "Your theme was uploaded successfully", "deletionFailure": "Failed to delete the theme. Try to delete it manually.", @@ -630,7 +639,7 @@ "defaultLocation": "Read files and data storage location", "exportData": "Export your data", "doubleTapToCopy": "Double tap to copy the path", - "restoreLocation": "Restore to AppFlowy default path", + "restoreLocation": "Restore to @:appName default path", "customizeLocation": "Open another folder", "restartApp": "Please restart app for the changes to take effect.", "exportDatabase": "Export database", @@ -642,10 +651,10 @@ "defineWhereYourDataIsStored": "Define where your data is stored", "open": "Open", "openFolder": "Open an existing folder", - "openFolderDesc": "Read and write it to your existing AppFlowy folder", + "openFolderDesc": "Read and write it to your existing @:appName folder", "folderHintText": "folder name", "location": "Creating a new folder", - "locationDesc": "Pick a name for your AppFlowy data folder", + "locationDesc": "Pick a name for your @:appName data folder", "browser": "Browse", "create": "Create", "set": "Set", @@ -656,7 +665,7 @@ "change": "Change", "openLocationTooltips": "Open another data directory", "openCurrentDataFolder": "Open current data directory", - "recoverLocationTooltips": "Reset to AppFlowy's default data directory", + "recoverLocationTooltips": "Reset to @:appName's default data directory", "exportFileSuccess": "Export file successfully!", "exportFileFail": "Export file failed!", "export": "Export", @@ -1204,7 +1213,8 @@ "resetToDefaultFont": "Reset to default" }, "errorBlock": { - "theBlockIsNotSupported": "The current version does not support this block.", + "theBlockIsNotSupported": "Unable to parse the block content", + "clickToCopyTheBlockContent": "Click to copy the block content", "blockContentHasBeenCopied": "The block content has been copied." }, "mobilePageSelector": { @@ -1251,7 +1261,9 @@ "showGroup": "Show group", "showGroupContent": "Are you sure you want to show this group on the board?", "failedToLoad": "Failed to load board view" - } + }, + "noGroup": "No group by property", + "noGroupDesc": "Board views require a property to group by in order to display" }, "calendar": { "menuName": "Calendar", @@ -1261,7 +1273,13 @@ "today": "Today", "jumpToday": "Jump to Today", "previousMonth": "Previous Month", - "nextMonth": "Next Month" + "nextMonth": "Next Month", + "views": { + "day": "Day", + "week": "Week", + "month": "Month", + "year": "Year" + } }, "mobileEventScreen": { "emptyTitle": "No events yet", @@ -1281,14 +1299,15 @@ }, "unscheduledEventsTitle": "Unscheduled events", "clickToAdd": "Click to add to the calendar", - "name": "Calendar settings" + "name": "Calendar settings", + "clickToOpen": "Click to open the record" }, "referencedCalendarPrefix": "View of", "quickJumpYear": "Jump to", "duplicateEvent": "Duplicate event" }, "errorDialog": { - "title": "AppFlowy Error", + "title": "@:appName Error", "howToFixFallback": "We're sorry for the inconvenience! Submit an issue on our GitHub page that describes your error.", "github": "View on GitHub" }, @@ -1572,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" @@ -1622,9 +1643,10 @@ "workplaceName": "Workplace name", "workplaceNamePlaceholder": "Enter workplace name", "workplaceIcon": "Workplace icon", - "workplaceIconSubtitle": "Upload an image or use an emoji for your workspace. Icon will show in your sidebar and notifications", + "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": { @@ -1656,7 +1678,7 @@ "none": "None", "photoPermissionDescription": "Allow access to the photo library for uploading images.", "openSettings": "Open Settings", - "photoPermissionTitle": "AppFlowy Would Like to Access Your Photo Library", + "photoPermissionTitle": "@:appName would like to access your photo library", "doNotAllow": "Don't Allow", "image": "Image" }, @@ -1670,4 +1692,4 @@ "betaTooltip": "We currently only support searching for pages", "fromTrashHint": "From trash" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/es-VE.json b/frontend/resources/translations/es-VE.json index 35f5541197..57cabeb120 100644 --- a/frontend/resources/translations/es-VE.json +++ b/frontend/resources/translations/es-VE.json @@ -78,7 +78,7 @@ "resetWorkspacePrompt": "Al restablecer el espacio de trabajo se eliminarán todas las páginas y datos que contiene. ¿Está seguro de que desea restablecer el espacio de trabajo? Alternativamente, puede comunicarse con el equipo de soporte para restaurar el espacio de trabajo.", "hint": "Espacio de trabajo", "notFoundError": "Espacio de trabajo no encontrado", - "failedToLoad": "¡Algo salió mal! No se pudo cargar el espacio de trabajo. Intente cerrar cualquier instancia abierta de AppFlowy y vuelva a intentarlo.", + "failedToLoad": "¡Algo salió mal! No se pudo cargar el espacio de trabajo. Intente cerrar cualquier instancia abierta de @:appName y vuelva a intentarlo.", "errorActions": { "reportIssue": "Reportar un problema", "reportIssueOnGithub": "Informar un problema en Github", @@ -375,8 +375,8 @@ "cloudSupabaseUrlCanNotBeEmpty": "La URL de supabase no puede estar vacía.", "cloudSupabaseAnonKey": "Supabase clave anon", "cloudSupabaseAnonKeyCanNotBeEmpty": "La clave anon no puede estar vacía si la URL de supabase no está vacía", - "cloudAppFlowy": "Nube AppFlowy", - "cloudAppFlowySelfHost": "AppFlowy Cloud autohospedado", + "cloudAppFlowy": "Nube @:appName", + "cloudAppFlowySelfHost": "@:appName Cloud autohospedado", "appFlowyCloudUrlCanNotBeEmpty": "La URL de la nube no puede estar vacía", "clickToCopy": "Haga clic para copiar", "selfHostStart": "Si no tiene un servidor, consulte la", @@ -397,12 +397,12 @@ "historicalUserList": "Historial de inicio de sesión del usuario", "historicalUserListTooltip": "Esta lista muestra tus cuentas anónimas. Puedes hacer clic en una cuenta para ver sus detalles. Las cuentas anónimas se crean haciendo clic en el botón \"Comenzar\".", "openHistoricalUser": "Haga clic para abrir la cuenta anónima", - "customPathPrompt": "Almacenar la carpeta de datos de AppFlowy en una carpeta sincronizada en la nube, como Google Drive, puede presentar riesgos. Si se accede a la base de datos dentro de esta carpeta o se modifica desde varias ubicaciones al mismo tiempo, se pueden producir conflictos de sincronización y posibles daños en los datos", - "importAppFlowyData": "Importar datos desde una carpeta externa de AppFlowy", + "customPathPrompt": "Almacenar la carpeta de datos de @:appName en una carpeta sincronizada en la nube, como Google Drive, puede presentar riesgos. Si se accede a la base de datos dentro de esta carpeta o se modifica desde varias ubicaciones al mismo tiempo, se pueden producir conflictos de sincronización y posibles daños en los datos", + "importAppFlowyData": "Importar datos desde una carpeta externa de @:appName", "importingAppFlowyDataTip": "La importación de datos está en curso. Por favor no cierres la aplicación.", - "importAppFlowyDataDescription": "Copia los datos de una carpeta de datos externa de AppFlowy e impórtalos a la carpeta de datos actual de AppFlowy", - "importSuccess": "Importó exitosamente la carpeta de datos de AppFlowy", - "importFailed": "Error al importar la carpeta de datos de AppFlowy", + "importAppFlowyDataDescription": "Copia los datos de una carpeta de datos externa de @:appName e impórtalos a la carpeta de datos actual de @:appName", + "importSuccess": "Importó exitosamente la carpeta de datos de @:appName", + "importFailed": "Error al importar la carpeta de datos de @:appName", "importGuide": "Para obtener más detalles, consulte el documento de referencia.", "supabaseSetting": "Ajuste de base superior" }, @@ -455,7 +455,7 @@ "themeUpload": { "button": "Subir", "uploadTheme": "Subir tema", - "description": "Cargue su propio tema AppFlowy usando el botón de abajo.", + "description": "Cargue su propio tema @:appName usando el botón de abajo.", "loading": "Espere mientras validamos y cargamos su tema...", "uploadSuccess": "Su tema se ha subido con éxito", "deletionFailure": "No se pudo eliminar el tema. Intenta eliminarlo manualmente.", @@ -517,7 +517,7 @@ "defaultLocation": "Leer archivos y ubicación de almacenamiento de datos", "exportData": "Exporta tus datos", "doubleTapToCopy": "Toca dos veces para copiar la ruta", - "restoreLocation": "Restaurar a la ruta predeterminada de AppFlowy", + "restoreLocation": "Restaurar a la ruta predeterminada de @:appName", "customizeLocation": "Abrir otra carpeta", "restartApp": "Reinicie la aplicación para que los cambios surtan efecto.", "exportDatabase": "Exportar base de datos", @@ -529,10 +529,10 @@ "defineWhereYourDataIsStored": "Defina dónde se almacenan sus datos", "open": "Abierto", "openFolder": "Abrir una carpeta existente", - "openFolderDesc": "Léalo y escríbalo en su carpeta AppFlowy existente", + "openFolderDesc": "Léalo y escríbalo en su carpeta @:appName existente", "folderHintText": "nombre de la carpeta", "location": "Creando una nueva carpeta", - "locationDesc": "Elija un nombre para su carpeta de datos de AppFlowy", + "locationDesc": "Elija un nombre para su carpeta de datos de @:appName", "browser": "Navegar", "create": "Crear", "set": "Colocar", @@ -543,7 +543,7 @@ "change": "Cambiar", "openLocationTooltips": "Abrir otro directorio de datos", "openCurrentDataFolder": "Abrir el directorio de datos actual", - "recoverLocationTooltips": "Restablecer al directorio de datos predeterminado de AppFlowy", + "recoverLocationTooltips": "Restablecer al directorio de datos predeterminado de @:appName", "exportFileSuccess": "¡Exportar archivo con éxito!", "exportFileFail": "¡Error en la exportación del archivo!", "export": "Exportar", diff --git a/frontend/resources/translations/eu-ES.json b/frontend/resources/translations/eu-ES.json index 496a262596..e7029a0d38 100644 --- a/frontend/resources/translations/eu-ES.json +++ b/frontend/resources/translations/eu-ES.json @@ -222,7 +222,7 @@ }, "themeUpload": { "button": "Kargatu", - "description": "Kargatu zure AppFlowy gaia beheko botoia erabiliz.", + "description": "Kargatu zure @:appName gaia beheko botoia erabiliz.", "loading": "Mesedez, itxaron zure gaia balioztatzen eta kargatzen dugun bitartean...", "uploadSuccess": "Zure gaia behar bezala kargatu da", "deletionFailure": "Ezin izan da gaia ezabatu. Saiatu eskuz ezabatzen.", @@ -239,7 +239,7 @@ "defaultLocation": "Non gordetzen diren zure datuak", "exportData": "Esportatu zure datuak", "doubleTapToCopy": "Sakatu birritan bidea kopiatzeko", - "restoreLocation": "Berrezarri AppFlowy-ren biden lehenetsira", + "restoreLocation": "Berrezarri @:appName-ren biden lehenetsira", "customizeLocation": "Beste karpeta bat ireki", "restartApp": "Mesedez, berrabiarazi aplikazioa aldaketak indarrean egon daitezen.", "exportDatabase": "Datubasea exportatu", @@ -251,10 +251,10 @@ "defineWhereYourDataIsStored": "Zure datuak non gordetzen diren zehaztu", "open": "Oreki", "openFolder": "Ireki karpeta bat", - "openFolderDesc": "Irakurri eta idatzi zure AppFlowy karpetan...", + "openFolderDesc": "Irakurri eta idatzi zure @:appName karpetan...", "folderHintText": "karpetaren izena", "location": "Karpeta berria sortzen", - "locationDesc": "Aukeratu izen bat AppFlowy datuen karpetarako", + "locationDesc": "Aukeratu izen bat @:appName datuen karpetarako", "browser": "Bilatu", "create": "Sortu", "set": "Ezarri", @@ -265,7 +265,7 @@ "change": "Aldatu", "openLocationTooltips": "Ireki beste datu-direktorio bat", "openCurrentDataFolder": "Ireki uneko datuen direktorioa", - "recoverLocationTooltips": "Berrezarri AppFlowyren datu-direktorio lehenetsira", + "recoverLocationTooltips": "Berrezarri @:appNameren datu-direktorio lehenetsira", "exportFileSuccess": "Esportatu fitxategia behar bezala!", "exportFileFail": "Ezin izan da esportatu fitxategia!", "export": "Esportatu" @@ -580,7 +580,7 @@ "referencedCalendarPrefix": "-ren ikuspegia" }, "errorDialog": { - "title": "AppFlowy errorea", + "title": "@:appName errorea", "howToFixFallback": "Sentitzen dugu eragozpenak! Bidali zure errorea deskribatzen duen arazo bat gure GitHub orrian.", "github": "Ikusi GitHub-en" }, diff --git a/frontend/resources/translations/fa.json b/frontend/resources/translations/fa.json index 56e97333d7..147bca02b6 100644 --- a/frontend/resources/translations/fa.json +++ b/frontend/resources/translations/fa.json @@ -245,7 +245,7 @@ }, "themeUpload": { "button": "بارگذاری", - "description": "تم قالب AppFlowy خود را با استفاده از دکمه زیر آپلود کنید.", + "description": "تم قالب @:appName خود را با استفاده از دکمه زیر آپلود کنید.", "loading": "لطفاً منتظر بمانید تا تم قالب شما را اعتبارسنجی و آپلود کنیم...", "uploadSuccess": "تم قالب شما با موفقیت آپلود شد", "deletionFailure": "تم حذف نشد. سعی کنید آن را به صورت دستی حذف کنید.", @@ -262,7 +262,7 @@ "defaultLocation": "خواندن فایل‌ها و مکان ذخیره داده‌ها", "exportData": "از داده‌های خود خروجی بگیرید", "doubleTapToCopy": "برای کپی کردن دوبار کلیک کنید", - "restoreLocation": "بازیابی به مسیر پیش فرض AppFlowy", + "restoreLocation": "بازیابی به مسیر پیش فرض @:appName", "customizeLocation": "پوشه دیگری باز کنید", "restartApp": "لطفاً برنامه را مجدداً راه اندازی کنید تا تغییرات اعمال شوند.", "exportDatabase": "از پایگاه داده‌ها خروجی بگیرید", @@ -274,10 +274,10 @@ "defineWhereYourDataIsStored": "محل ذخیره داده های خود را مشخص کنید", "open": "باز کردن", "openFolder": "باز کردن یک پوشه موجود", - "openFolderDesc": "خواندن و نوشتن آن در یک پوشه AppFlowy موجود", + "openFolderDesc": "خواندن و نوشتن آن در یک پوشه @:appName موجود", "folderHintText": "نام پوشه", "location": "ایجاد یک پوشه جدید", - "locationDesc": "یک نام برای پوشه داده AppFlowy خود انتخاب کنید", + "locationDesc": "یک نام برای پوشه داده @:appName خود انتخاب کنید", "browser": "مرورگر", "create": "ایجاد کردن", "set": "تنظیم کردن", @@ -288,7 +288,7 @@ "change": "تغییر", "openLocationTooltips": "باز کردن یک فهرست پوشه دیگر", "openCurrentDataFolder": "باز کردن فهرست پوشه فعلی", - "recoverLocationTooltips": "بازنشانی به فهرست داده های پیش فرض AppFlowy", + "recoverLocationTooltips": "بازنشانی به فهرست داده های پیش فرض @:appName", "exportFileSuccess": "خروجی گرفتن از فایل با موفقیت انجام شد.", "exportFileFail": "خروجی گرفتن از فایل انجام نشد!", "export": "خروجی گرفتن" @@ -621,7 +621,7 @@ "referencedCalendarPrefix": "نمای" }, "errorDialog": { - "title": "خطای AppFlowy", + "title": "خطای @:appName", "howToFixFallback": "بابت مشکل پیش آمده متأسفیم! مشکل و شرح آن را در صفحه GitHub ما ارسال کنید.", "github": "مشاهده در GitHub" }, diff --git a/frontend/resources/translations/fr-CA.json b/frontend/resources/translations/fr-CA.json index a6799575f1..f5bbb4b2db 100644 --- a/frontend/resources/translations/fr-CA.json +++ b/frontend/resources/translations/fr-CA.json @@ -58,7 +58,7 @@ "resetWorkspacePrompt": "La réinitialisation de l'espace de travail supprimera toutes les pages et données qu'elles contiennent. Êtes-vous sûr de vouloir réinitialiser l'espace de travail ? Alternativement, vous pouvez contacter l'équipe d'assistance pour restaurer l'espace de travail", "hint": "Espace de travail", "notFoundError": "Espace de travail introuvable", - "failedToLoad": "Quelque chose s'est mal passé ! Échec du chargement de l'espace de travail. Essayez de fermer toute instance ouverte d'AppFlowy et réessayez.", + "failedToLoad": "Quelque chose s'est mal passé ! Échec du chargement de l'espace de travail. Essayez de fermer toute instance ouverte d'@:appName et réessayez.", "errorActions": { "reportIssue": "Signaler un problème", "reportIssueOnGithub": "Signaler un bug sur Github", @@ -283,8 +283,8 @@ "cloudSupabaseUrlCanNotBeEmpty": "L'URL Supabase ne peut pas être vide", "cloudSupabaseAnonKey": "Clé anonyme Supabase", "cloudSupabaseAnonKeyCanNotBeEmpty": "La clé anonyme ne peut pas être vide si l'URL de Supabase n'est pas vide", - "cloudAppFlowy": "AppFlowy Cloud Bêta", - "cloudAppFlowySelfHost": "AppFlowy Cloud auto-hébergé", + "cloudAppFlowy": "@:appName Cloud Bêta", + "cloudAppFlowySelfHost": "@:appName Cloud auto-hébergé", "appFlowyCloudUrlCanNotBeEmpty": "L'URL cloud ne peut pas être vide", "clickToCopy": "Cliquez pour copier", "selfHostStart": "Si vous n'avez pas de serveur, veuillez vous référer au", @@ -305,12 +305,12 @@ "historicalUserList": "Historique de connexion d'utilisateurs", "historicalUserListTooltip": "Cette liste affiche vos comptes anonymes. Vous pouvez cliquer sur un compte pour afficher ses détails. Les comptes anonymes sont créés en cliquant sur le bouton « Commencer »", "openHistoricalUser": "Cliquez pour ouvrir le compte anonyme", - "customPathPrompt": "Le stockage du dossier de données AppFlowy dans un dossier synchronisé avec le cloud tel que Google Drive peut présenter des risques. Si la base de données de ce dossier est consultée ou modifiée à partir de plusieurs emplacements en même temps, cela peut entraîner des conflits de synchronisation et une corruption potentielle des données.", - "importAppFlowyData": "Importer des données à partir du dossier AppFlowy externe", + "customPathPrompt": "Le stockage du dossier de données @:appName dans un dossier synchronisé avec le cloud tel que Google Drive peut présenter des risques. Si la base de données de ce dossier est consultée ou modifiée à partir de plusieurs emplacements en même temps, cela peut entraîner des conflits de synchronisation et une corruption potentielle des données.", + "importAppFlowyData": "Importer des données à partir du dossier @:appName externe", "importingAppFlowyDataTip": "L'importation des données est en cours. Veuillez ne pas fermer l'application", - "importAppFlowyDataDescription": "Copiez les données d'un dossier de données AppFlowy externe et importez-les dans le dossier de données AppFlowy actuel", - "importSuccess": "Importation réussie du dossier de données AppFlowy", - "importFailed": "L'importation du dossier de données AppFlowy a échoué", + "importAppFlowyDataDescription": "Copiez les données d'un dossier de données @:appName externe et importez-les dans le dossier de données @:appName actuel", + "importSuccess": "Importation réussie du dossier de données @:appName", + "importFailed": "L'importation du dossier de données @:appName a échoué", "importGuide": "Pour plus de détails, veuillez consulter le document référencé", "supabaseSetting": "Paramètre Supabase" }, @@ -361,7 +361,7 @@ "themeUpload": { "button": "Téléverser", "uploadTheme": "Téléverser le thème", - "description": "Téléversez votre propre thème AppFlowy en utilisant le bouton ci-dessous.", + "description": "Téléversez votre propre thème @:appName en utilisant le bouton ci-dessous.", "loading": "Veuillez patienter pendant que nous validons et téléchargeons votre thème...", "uploadSuccess": "Votre thème a été téléversé avec succès", "deletionFailure": "Échec de la suppression du thème. Essayez de le supprimer manuellement.", @@ -392,7 +392,7 @@ "defaultLocation": "Lire les fichiers et l'emplacement de stockage des données", "exportData": "Exportez vos données", "doubleTapToCopy": "Appuyez deux fois pour copier le chemin", - "restoreLocation": "Restaurer le chemin par défaut d'AppFlowy", + "restoreLocation": "Restaurer le chemin par défaut d'@:appName", "customizeLocation": "Ouvrir un autre dossier", "restartApp": "Veuillez redémarrer l'application pour que les modifications prennent effet.", "exportDatabase": "Exporter la base de données", @@ -404,10 +404,10 @@ "defineWhereYourDataIsStored": "Définissez où vos données sont stockées", "open": "Ouvrir", "openFolder": "Ouvrir un dossier existant", - "openFolderDesc": "Lisez-le et écrivez-le dans votre dossier AppFlowy existant", + "openFolderDesc": "Lisez-le et écrivez-le dans votre dossier @:appName existant", "folderHintText": "Nom de dossier", "location": "Création d'un nouveau dossier", - "locationDesc": "Choisissez un nom pour votre dossier de données AppFlowy", + "locationDesc": "Choisissez un nom pour votre dossier de données @:appName", "browser": "Parcourir", "create": "Créer", "set": "Définir", @@ -418,7 +418,7 @@ "change": "Changer", "openLocationTooltips": "Ouvrir un autre répertoire de données", "openCurrentDataFolder": "Ouvrir le répertoire de données actuel", - "recoverLocationTooltips": "Réinitialiser au répertoire de données par défaut d'AppFlowy", + "recoverLocationTooltips": "Réinitialiser au répertoire de données par défaut d'@:appName", "exportFileSuccess": "Exporter le fichier avec succès !", "exportFileFail": "Échec de l'export du fichier !", "export": "Exporter" @@ -963,7 +963,7 @@ "quickJumpYear": "Sauter à" }, "errorDialog": { - "title": "Erreur AppFlowy", + "title": "Erreur @:appName", "howToFixFallback": "Nous sommes désolés pour le désagrément ! Soumettez un problème sur notre page GitHub qui décrit votre erreur.", "github": "Afficher sur GitHub" }, diff --git a/frontend/resources/translations/fr-FR.json b/frontend/resources/translations/fr-FR.json index dc4e2348fb..e258e9099b 100644 --- a/frontend/resources/translations/fr-FR.json +++ b/frontend/resources/translations/fr-FR.json @@ -42,6 +42,7 @@ "emailHint": "Courriel", "passwordHint": "Mot de passe", "dontHaveAnAccount": "Vous n'avez pas encore de compte ?", + "createAccount": "Créer un compte", "repeatPasswordEmptyError": "Vous n'avez pas ressaisi votre mot de passe", "unmatchedPasswordError": "Les deux mots de passe ne sont pas identiques", "syncPromptMessage": "La synchronisation des données peut prendre un certain temps. Merci de ne pas fermer pas cette page.", @@ -51,6 +52,7 @@ "pleaseInputYourEmail": "Veuillez entrer votre adresse e-mail", "magicLinkSent": "Lien magique envoyé à votre email, veuillez vérifier votre boîte de réception", "invalidEmail": "S'il vous plaît, mettez une adresse email valide", + "logIn": "Connexion", "LogInWithGoogle": "Se connecter avec Google", "LogInWithGithub": "Se connecter avec Github", "LogInWithDiscord": "Se connecter avec Discord", @@ -64,7 +66,7 @@ "resetWorkspacePrompt": "La réinitialisation de l'espace de travail supprimera toutes les pages et données qu'elles contiennent. Êtes-vous sûr de vouloir réinitialiser l'espace de travail ? Alternativement, vous pouvez contacter l'équipe d'assistance pour restaurer l'espace de travail", "hint": "Espace de travail", "notFoundError": "Espace de travail introuvable", - "failedToLoad": "Quelque chose s'est mal passé ! Échec du chargement de l'espace de travail. Essayez de fermer toute instance ouverte d'AppFlowy et réessayez.", + "failedToLoad": "Quelque chose s'est mal passé ! Échec du chargement de l'espace de travail. Essayez de fermer toute instance ouverte d'@:appName et réessayez.", "errorActions": { "reportIssue": "Signaler un problème", "reportIssueOnGithub": "Signaler un bug sur Github", @@ -93,6 +95,7 @@ "buttonText": "Partager", "workInProgress": "Bientôt disponible", "markdown": "Markdown", + "html": "HTML", "clipboard": "Copier dans le presse-papier", "csv": "CSV", "copyLink": "Copier le lien" @@ -170,7 +173,7 @@ "shortcuts": "Raccourcis", "whatsNew": "Nouveautés", "help": "Aide et Support", - "markdown": "Réduction", + "markdown": "Rédaction", "debug": { "name": "Informations de Débogage", "success": "Informations de débogage copiées dans le presse-papiers !", @@ -310,6 +313,108 @@ }, "settings": { "title": "Paramètres", + "accountPage": { + "menuLabel": "Mon compte", + "title": "Mon compte", + "email": { + "title": "Email", + "actions": { + "change": "Modifier l'email" + } + }, + "login": { + "loginLabel": "Connexion", + "logoutLabel": "Déconnexion" + } + }, + "workspacePage": { + "menuLabel": "Espace de travail", + "title": "Espace de travail", + "description": "Personnalisez l'apparence, le thème, la police, la disposition du texte, le format de la date/heure et la langue de votre espace de travail.", + "workspaceName": { + "title": "Nom de l'espace de travail", + "savedMessage": "Nom de l'espace de travail enregistré" + }, + "workspaceIcon": { + "title": "Icône de l'espace de travail", + "description": "Personnalisez l'apparence, le thème, la police, la disposition du texte, la date, l'heure et la langue de votre espace de travail." + }, + "appearance": { + "title": "Apparence", + "description": "Personnalisez l'apparence, le thème, la police, la disposition du texte, la date, l'heure et la langue de votre espace de travail.", + "options": { + "system": "Auto", + "light": "Clair", + "dark": "Foncé" + } + }, + "theme": { + "title": "Thème", + "description": "Sélectionnez un thème prédéfini ou téléchargez votre propre thème personnalisé." + }, + "workspaceFont": { + "title": "Police de caractère de l'espace de travail" + }, + "textDirection": { + "title": "Sens du texte", + "leftToRight": "De gauche à droite", + "rightToLeft": "De droite à gauche", + "auto": "Auto" + }, + "layoutDirection": { + "leftToRight": "De gauche à droite", + "rightToLeft": "De droite à gauche" + }, + "dateTime": { + "title": "Date et heure", + "24HourTime": "Heure sur 24 heures", + "dateFormat": { + "label": "Format de date", + "local": "Locale", + "us": "US", + "iso": "ISO", + "dmy": "J/M/A" + } + }, + "language": { + "title": "Langue" + }, + "deleteWorkspacePrompt": { + "title": "Supprimer l'espace de travail", + "content": "Êtes-vous sûr de vouloir supprimer cet espace de travail ? Cette action ne peut pas être annulée." + }, + "leaveWorkspacePrompt": { + "title": "Quitter l'espace de travail", + "content": "Êtes-vous sûr de vouloir quitter cet espace de travail ? Vous allez perdre l’accès à toutes les pages et données qu’il contient." + }, + "manageWorkspace": { + "title": "Gérer l'espace de travail", + "leaveWorkspace": "Quitter l'espace de travail", + "deleteWorkspace": "Supprimer l'espace de travail" + } + }, + "manageDataPage": { + "menuLabel": "Gérer les données", + "title": "Gérer les données", + "dataStorage": { + "actions": { + "change": "Changer de chemin", + "open": "Ouvrir le répertoire", + "copy": "Copier le chemin", + "copiedHint": "Lien copié !" + }, + "resetDialog": { + "title": "Êtes-vous sûr ?" + } + }, + "importData": { + "title": "Importer des données", + "action": "Parcourir le dossier" + }, + "encryption": { + "title": "Chiffrement" + } + }, "menu": { "appearance": "Apparence", "language": "Langue", @@ -334,8 +439,8 @@ "cloudSupabaseUrlCanNotBeEmpty": "L'URL Supabase ne peut pas être vide", "cloudSupabaseAnonKey": "Clé anonyme Supabase", "cloudSupabaseAnonKeyCanNotBeEmpty": "La clé anonyme ne peut pas être vide si l'URL de Supabase n'est pas vide", - "cloudAppFlowy": "AppFlowy Cloud Bêta", - "cloudAppFlowySelfHost": "AppFlowy Cloud auto-hébergé", + "cloudAppFlowy": "@:appName Cloud Bêta", + "cloudAppFlowySelfHost": "@:appName Cloud auto-hébergé", "appFlowyCloudUrlCanNotBeEmpty": "L'URL cloud ne peut pas être vide", "clickToCopy": "Cliquez pour copier", "selfHostStart": "Si vous n'avez pas de serveur, veuillez vous référer au", @@ -356,12 +461,12 @@ "historicalUserList": "Historique de connexion d'utilisateurs", "historicalUserListTooltip": "Cette liste affiche vos comptes anonymes. Vous pouvez cliquer sur un compte pour afficher ses détails. Les comptes anonymes sont créés en cliquant sur le bouton « Commencer »", "openHistoricalUser": "Cliquez pour ouvrir le compte anonyme", - "customPathPrompt": "Le stockage du dossier de données AppFlowy dans un dossier synchronisé avec le cloud tel que Google Drive peut présenter des risques. Si la base de données de ce dossier est consultée ou modifiée à partir de plusieurs emplacements en même temps, cela peut entraîner des conflits de synchronisation et une corruption potentielle des données.", - "importAppFlowyData": "Importer des données à partir du dossier AppFlowy externe", + "customPathPrompt": "Le stockage du dossier de données @:appName dans un dossier synchronisé avec le cloud tel que Google Drive peut présenter des risques. Si la base de données de ce dossier est consultée ou modifiée à partir de plusieurs emplacements en même temps, cela peut entraîner des conflits de synchronisation et une corruption potentielle des données.", + "importAppFlowyData": "Importer des données à partir du dossier @:appName externe", "importingAppFlowyDataTip": "L'importation des données est en cours. Veuillez ne pas fermer l'application", - "importAppFlowyDataDescription": "Copiez les données d'un dossier de données AppFlowy externe et importez-les dans le dossier de données AppFlowy actuel", - "importSuccess": "Importation réussie du dossier de données AppFlowy", - "importFailed": "L'importation du dossier de données AppFlowy a échoué", + "importAppFlowyDataDescription": "Copiez les données d'un dossier de données @:appName externe et importez-les dans le dossier de données @:appName actuel", + "importSuccess": "Importation réussie du dossier de données @:appName", + "importFailed": "L'importation du dossier de données @:appName a échoué", "importGuide": "Pour plus de détails, veuillez consulter le document référencé", "supabaseSetting": "Paramètre Supabase" }, @@ -413,7 +518,7 @@ "themeUpload": { "button": "Téléverser", "uploadTheme": "Téléverser le thème", - "description": "Téléversez votre propre thème AppFlowy en utilisant le bouton ci-dessous.", + "description": "Téléversez votre propre thème @:appName en utilisant le bouton ci-dessous.", "loading": "Veuillez patienter pendant que nous validons et téléchargeons votre thème...", "uploadSuccess": "Votre thème a été téléversé avec succès", "deletionFailure": "Échec de la suppression du thème. Essayez de le supprimer manuellement.", @@ -473,7 +578,7 @@ "defaultLocation": "Lire les fichiers et l'emplacement de stockage des données", "exportData": "Exportez vos données", "doubleTapToCopy": "Appuyez deux fois pour copier le chemin", - "restoreLocation": "Restaurer le chemin par défaut d'AppFlowy", + "restoreLocation": "Restaurer le chemin par défaut d'@:appName", "customizeLocation": "Ouvrir un autre dossier", "restartApp": "Veuillez redémarrer l'application pour que les modifications prennent effet.", "exportDatabase": "Exporter la base de données", @@ -485,10 +590,10 @@ "defineWhereYourDataIsStored": "Définissez où vos données sont stockées", "open": "Ouvrir", "openFolder": "Ouvrir un dossier existant", - "openFolderDesc": "Lisez-le et écrivez-le dans votre dossier AppFlowy existant", + "openFolderDesc": "Lisez-le et écrivez-le dans votre dossier @:appName existant", "folderHintText": "Nom de dossier", "location": "Création d'un nouveau dossier", - "locationDesc": "Choisissez un nom pour votre dossier de données AppFlowy", + "locationDesc": "Choisissez un nom pour votre dossier de données @:appName", "browser": "Parcourir", "create": "Créer", "set": "Définir", @@ -499,7 +604,7 @@ "change": "Changer", "openLocationTooltips": "Ouvrir un autre répertoire de données", "openCurrentDataFolder": "Ouvrir le répertoire de données actuel", - "recoverLocationTooltips": "Réinitialiser au répertoire de données par défaut d'AppFlowy", + "recoverLocationTooltips": "Réinitialiser au répertoire de données par défaut d'@:appName", "exportFileSuccess": "Exporter le fichier avec succès !", "exportFileFail": "Échec de l'export du fichier !", "export": "Exporter", @@ -1113,7 +1218,7 @@ "duplicateEvent": "Événement en double" }, "errorDialog": { - "title": "Erreur AppFlowy", + "title": "Erreur @:appName", "howToFixFallback": "Nous sommes désolés pour le désagrément ! Soumettez un problème sur notre page GitHub qui décrit votre erreur.", "github": "Afficher sur GitHub" }, diff --git a/frontend/resources/translations/hu-HU.json b/frontend/resources/translations/hu-HU.json index bfecc2756f..1a60a1c6f5 100644 --- a/frontend/resources/translations/hu-HU.json +++ b/frontend/resources/translations/hu-HU.json @@ -226,7 +226,7 @@ }, "themeUpload": { "button": "Feltöltés", - "description": "Töltse fel saját AppFlowy témáját az alábbi gomb segítségével.", + "description": "Töltse fel saját @:appName témáját az alábbi gomb segítségével.", "loading": "Kérjük, várjon, amíg ellenőrizzük és feltöltjük a témát...", "uploadSuccess": "A témát sikeresen feltöltötte", "deletionFailure": "Nem sikerült törölni a témát. Próbálja meg manuálisan törölni.", @@ -243,7 +243,7 @@ "defaultLocation": "Fájlok és adattárolási hely olvasása", "exportData": "Exportálja adatait", "doubleTapToCopy": "Koppintson duplán az útvonal másolásához", - "restoreLocation": "Visszaállítás az AppFlowy alapértelmezett elérési útjára", + "restoreLocation": "Visszaállítás az @:appName alapértelmezett elérési útjára", "customizeLocation": "Nyisson meg egy másik mappát", "restartApp": "Kérjük, indítsa újra az alkalmazást, hogy a változtatások életbe lépjenek.", "exportDatabase": "Adatbázis exportálása", @@ -255,10 +255,10 @@ "defineWhereYourDataIsStored": "Határozza meg, hol tárolják adatait", "open": "Nyisd ki", "openFolder": "Nyisson meg egy meglévő mappát", - "openFolderDesc": "Olvassa el és írja be a meglévő AppFlowy mappájába", + "openFolderDesc": "Olvassa el és írja be a meglévő @:appName mappájába", "folderHintText": "mappa neve", "location": "Új mappa létrehozása", - "locationDesc": "Válasszon nevet az AppFlowy adatmappájának", + "locationDesc": "Válasszon nevet az @:appName adatmappájának", "browser": "Tallózás", "create": "Teremt", "set": "Készlet", @@ -269,7 +269,7 @@ "change": "változás", "openLocationTooltips": "Nyisson meg egy másik adatkönyvtárat", "openCurrentDataFolder": "Nyissa meg az aktuális adatkönyvtárat", - "recoverLocationTooltips": "Állítsa vissza az AppFlowy alapértelmezett adatkönyvtárát", + "recoverLocationTooltips": "Állítsa vissza az @:appName alapértelmezett adatkönyvtárát", "exportFileSuccess": "A fájl exportálása sikeres volt!", "exportFileFail": "A fájl exportálása nem sikerült!", "export": "Export" @@ -578,7 +578,7 @@ "referencedCalendarPrefix": "Nézet" }, "errorDialog": { - "title": "AppFlowy hiba", + "title": "@:appName hiba", "howToFixFallback": "Elnézést kérünk a kellemetlenségért! Nyújtsa be a problémát a GitHub-oldalunkon, amely leírja a hibát.", "github": "Megtekintés a GitHubon" }, diff --git a/frontend/resources/translations/id-ID.json b/frontend/resources/translations/id-ID.json index f1f5c6a1f1..286f5a3e06 100644 --- a/frontend/resources/translations/id-ID.json +++ b/frontend/resources/translations/id-ID.json @@ -58,7 +58,7 @@ "resetWorkspacePrompt": "Mengatur ulang area kerja akan menghapus semua halaman dan data di dalamnya. Apakah anda yakin ingin Mengatur ulang area kerja? Selain itu, anda bisa menghubungi tim dukungan untuk mengembalikan area kerja", "hint": "Area kerja", "notFoundError": "Area kerja tidak ditemukan", - "failedToLoad": "Ada yang tidak beres! Gagal memuat area kerja. Coba tutup AppFlowy yang terbuka dan coba lagi.", + "failedToLoad": "Ada yang tidak beres! Gagal memuat area kerja. Coba tutup @:appName yang terbuka dan coba lagi.", "errorActions": { "reportIssue": "Melaporkan isu", "reachOut": "Hubungi di Discord" @@ -309,7 +309,7 @@ "themeUpload": { "button": "Mengunggah", "uploadTheme": "Unggah tema", - "description": "Unggah tema AppFlowy Anda sendiri menggunakan tombol di bawah ini.", + "description": "Unggah tema @:appName Anda sendiri menggunakan tombol di bawah ini.", "loading": "Harap tunggu sementara kami memvalidasi dan mengunggah tema Anda...", "uploadSuccess": "Tema Anda berhasil diunggah", "deletionFailure": "Gagal menghapus tema. Cobalah untuk menghapusnya secara manual.", @@ -340,7 +340,7 @@ "defaultLocation": "Baca file dan lokasi penyimpanan data", "exportData": "Ekspor data Anda", "doubleTapToCopy": "Ketuk dua kali untuk menyalin jalur", - "restoreLocation": "Pulihkan ke jalur default AppFlowy", + "restoreLocation": "Pulihkan ke jalur default @:appName", "customizeLocation": "Buka folder lain", "restartApp": "Harap mulai ulang aplikasi agar perubahan diterapkan.", "exportDatabase": "Ekspor basis data", @@ -352,10 +352,10 @@ "defineWhereYourDataIsStored": "Tentukan di mana data Anda disimpan", "open": "Membuka", "openFolder": "Buka folder yang ada", - "openFolderDesc": "Baca dan tulis ke folder AppFlowy Anda yang sudah ada", + "openFolderDesc": "Baca dan tulis ke folder @:appName Anda yang sudah ada", "folderHintText": "nama folder", "location": "Membuat folder baru", - "locationDesc": "Pilih nama untuk folder data AppFlowy Anda", + "locationDesc": "Pilih nama untuk folder data @:appName Anda", "browser": "Jelajahi", "create": "Membuat", "set": "Mengatur", @@ -366,7 +366,7 @@ "change": "Mengubah", "openLocationTooltips": "Buka direktori data lain", "openCurrentDataFolder": "Buka direktori data saat ini", - "recoverLocationTooltips": "Setel ulang ke direktori data default AppFlowy", + "recoverLocationTooltips": "Setel ulang ke direktori data default @:appName", "exportFileSuccess": "Ekspor file berhasil!", "exportFileFail": "File ekspor gagal!", "export": "Ekspor" @@ -788,7 +788,7 @@ "referencedCalendarPrefix": "Pemandangan dari" }, "errorDialog": { - "title": "Kesalahan AppFlowy", + "title": "Kesalahan @:appName", "howToFixFallback": "Kami mohon maaf atas ketidaknyamanan ini! Kirimkan masalah di halaman GitHub kami yang menjelaskan kesalahan Anda.", "github": "Lihat di GitHub" }, diff --git a/frontend/resources/translations/it-IT.json b/frontend/resources/translations/it-IT.json index 4093c824ff..74282485d2 100644 --- a/frontend/resources/translations/it-IT.json +++ b/frontend/resources/translations/it-IT.json @@ -56,7 +56,7 @@ "resetWorkspacePrompt": "Il ripristino dello spazio di lavoro eliminerà tutte le pagine e i dati al suo interno. Sei sicuro di voler ripristinare lo spazio di lavoro? In alternativa, puoi contattare il team di supporto per ristabilire lo spazio di lavoro", "hint": "spazio di lavoro", "notFoundError": "Spazio di lavoro non trovato", - "failedToLoad": "Qualcosa è andato storto! Impossibile caricare lo spazio di lavoro. Prova a chiudere qualsiasi istanza aperta di AppFlowy e riprova.", + "failedToLoad": "Qualcosa è andato storto! Impossibile caricare lo spazio di lavoro. Prova a chiudere qualsiasi istanza aperta di @:appName e riprova.", "errorActions": { "reportIssue": "Segnala un problema", "reportIssueOnGithub": "Segnalate un problema su Github", @@ -283,8 +283,8 @@ "cloudSupabase": "Supabase", "cloudSupabaseUrl": "URL di Supabase", "cloudSupabaseUrlCanNotBeEmpty": "L'url di supabase non può essere vuoto", - "cloudAppFlowy": "AppFlowy Cloud", - "cloudAppFlowySelfHost": "AppFlowy Cloud Self-hosted (autogestito)", + "cloudAppFlowy": "@:appName Cloud Beta", + "cloudAppFlowySelfHost": "@:appName Cloud Self-hosted (autogestito)", "appFlowyCloudUrlCanNotBeEmpty": "L'url del cloud non può essere vuoto", "clickToCopy": "Fare clic per copiare", "selfHostStart": "Se non disponi di un server, fai riferimento a", @@ -305,12 +305,12 @@ "historicalUserList": "Cronologia di accesso dell'utente", "historicalUserListTooltip": "Questo elenco mostra i tuoi account anonimi. È possibile fare clic su un account per visualizzarne i dettagli. Gli account anonimi vengono creati facendo clic sul pulsante \"Inizia\".", "openHistoricalUser": "Fare clic per aprire l'account anonimo", - "customPathPrompt": "L'archiviazione della cartella dati di AppFlowy in una cartella sincronizzata sul cloud come Google Drive può comportare rischi. Se si accede o si modifica il database all'interno di questa cartella da più posizioni contemporaneamente, potrebbero verificarsi conflitti di sincronizzazione e potenziale danneggiamento dei dati", - "importAppFlowyData": "Importa dati dalla cartella AppFlowy esterna", + "customPathPrompt": "L'archiviazione della cartella dati di @:appName in una cartella sincronizzata sul cloud come Google Drive può comportare rischi. Se si accede o si modifica il database all'interno di questa cartella da più posizioni contemporaneamente, potrebbero verificarsi conflitti di sincronizzazione e potenziale danneggiamento dei dati", + "importAppFlowyData": "Importa dati dalla cartella @:appName esterna", "importingAppFlowyDataTip": "L'importazione dei dati è in corso. Non chiudere l'applicazione", - "importAppFlowyDataDescription": "Copia i dati da una cartella dati AppFlowy esterna e importali nella cartella dati AppFlowy corrente", - "importSuccess": "Importazione della cartella dati AppFlowy riuscita", - "importFailed": "L'importazione della cartella dati di AppFlowy non è riuscita", + "importAppFlowyDataDescription": "Copia i dati da una cartella dati @:appName esterna e importali nella cartella dati @:appName corrente", + "importSuccess": "Importazione della cartella dati @:appName riuscita", + "importFailed": "L'importazione della cartella dati di @:appName non è riuscita", "importGuide": "Per ulteriori dettagli si prega di consultare il documento di riferimento", "supabaseSetting": "Impostazione Supabase" }, @@ -360,7 +360,7 @@ "themeUpload": { "button": "Caricamento", "uploadTheme": "Carica tema", - "description": "Carica il tuo tema AppFlowy utilizzando il pulsante in basso.", + "description": "Carica il tuo tema @:appName utilizzando il pulsante in basso.", "loading": "Attendi mentre convalidiamo e carichiamo il tuo tema...", "uploadSuccess": "Il tuo tema è stato caricato correttamente", "deletionFailure": "Impossibile eliminare il tema. Prova a eliminarlo manualmente.", @@ -391,7 +391,7 @@ "defaultLocation": "Leggi i file e la posizione di archiviazione dei dati", "exportData": "Esporta i tuoi dati", "doubleTapToCopy": "Tocca due volte per copiare il percorso", - "restoreLocation": "Ripristina nel percorso predefinito di AppFlowy", + "restoreLocation": "Ripristina nel percorso predefinito di @:appName", "customizeLocation": "Apri un'altra cartella", "restartApp": "Riavvia l'app per rendere effettive le modifiche.", "exportDatabase": "Esporta banca dati", @@ -403,10 +403,10 @@ "defineWhereYourDataIsStored": "Definisci dove sono archiviati i tuoi dati", "open": "Aprire", "openFolder": "Apri una cartella esistente", - "openFolderDesc": "Leggilo e scrivilo nella tua cartella AppFlowy esistente", + "openFolderDesc": "Leggilo e scrivilo nella tua cartella @:appName esistente", "folderHintText": "nome della cartella", "location": "Creazione di una nuova cartella", - "locationDesc": "Scegli un nome per la cartella dei dati di AppFlowy", + "locationDesc": "Scegli un nome per la cartella dei dati di @:appName", "browser": "Navigare", "create": "Creare", "set": "Impostato", @@ -417,7 +417,7 @@ "change": "Modifica", "openLocationTooltips": "Apri un'altra directory di dati", "openCurrentDataFolder": "Apre la directory dei dati corrente", - "recoverLocationTooltips": "Ripristina la directory dei dati predefinita di AppFlowy", + "recoverLocationTooltips": "Ripristina la directory dei dati predefinita di @:appName", "exportFileSuccess": "Esporta file con successo!", "exportFileFail": "File di esportazione non riuscito!", "export": "Esportare" @@ -969,7 +969,7 @@ "quickJumpYear": "Salta a" }, "errorDialog": { - "title": "Errore AppFlow", + "title": "Errore @:appName", "howToFixFallback": "Ci scusiamo per l'inconveniente! Invia un problema sulla nostra pagina GitHub che descriva il tuo errore.", "github": "Visualizza su GitHub" }, diff --git a/frontend/resources/translations/ja-JP.json b/frontend/resources/translations/ja-JP.json index 704552b55d..00738dde42 100644 --- a/frontend/resources/translations/ja-JP.json +++ b/frontend/resources/translations/ja-JP.json @@ -255,8 +255,8 @@ "cloudSupabaseUrlCanNotBeEmpty": "Supabase URLは空白にはできません", "cloudSupabaseAnonKey": "Supabase anon key", "cloudSupabaseAnonKeyCanNotBeEmpty": "Supabase anon keyは空白にはできません", - "cloudAppFlowy": "AppFlowy Cloud Beta", - "cloudAppFlowySelfHost": "AppFlowy Cloud セルフホスト", + "cloudAppFlowy": "@:appName Cloud Beta", + "cloudAppFlowySelfHost": "@:appName Cloud セルフホスト", "appFlowyCloudUrlCanNotBeEmpty": "クラウドURLは空白にはできません", "clickToCopy": "クリックしてコピー", "selfHostStart": "サーバーが準備できていない場合、", @@ -284,7 +284,7 @@ "themeUpload": { "button": "アップロード", "uploadTheme": "テーマをアップロード", - "description": "下のボタンを使用して、独自のAppFlowyテーマをアップロードします。", + "description": "下のボタンを使用して、独自の@:appNameテーマをアップロードします。", "loading": "テーマを検証してアップロードするまでお待ちください...", "uploadSuccess": "テーマは正常にアップロードされました", "deletionFailure": "テーマの削除に失敗しました。手動で削除してみてください。", @@ -309,7 +309,7 @@ "defaultLocation": "ファイルの読み取りとデータの保存場所", "exportData": "データをエクスポートする", "doubleTapToCopy": "ダブルタップしてパスをコピーします", - "restoreLocation": "AppFlowyのデフォルトパスに戻す", + "restoreLocation": "@:appNameのデフォルトパスに戻す", "customizeLocation": "別のフォルダーを開く", "restartApp": "変更を有効にするには、アプリを再起動してください。", "exportDatabase": "データベースのエクスポート", @@ -321,10 +321,10 @@ "defineWhereYourDataIsStored": "データの保存場所を定義する", "open": "開ける", "openFolder": "既存のフォルダーを開く", - "openFolderDesc": "既存のAppFlowyフォルダに読み書きします", + "openFolderDesc": "既存の@:appNameフォルダに読み書きします", "folderHintText": "フォルダ名", "location": "新しいフォルダーの作成", - "locationDesc": "AppFlowyデータフォルダーの名前を選択します", + "locationDesc": "@:appNameデータフォルダーの名前を選択します", "browser": "ブラウズ", "create": "作成", "set": "設定", @@ -335,7 +335,7 @@ "change": "変化", "openLocationTooltips": "別のデータ ディレクトリを開く", "openCurrentDataFolder": "現在のデータディレクトリを開く", - "recoverLocationTooltips": "AppFlowyのデフォルトのデータディレクトリにリセットします", + "recoverLocationTooltips": "@:appNameのデフォルトのデータディレクトリにリセットします", "exportFileSuccess": "ファイルのエクスポートに成功しました。", "exportFileFail": "ファイルのエクスポートに失敗しました!", "export": "書き出す" @@ -665,7 +665,7 @@ "referencedCalendarPrefix": "のビュー" }, "errorDialog": { - "title": "AppFlowyエラー", + "title": "@:appNameエラー", "howToFixFallback": "ご不便をおかけして申し訳ございません。エラーを説明した issue を GitHub ページに送信してください。", "github": "GitHub で見る" }, diff --git a/frontend/resources/translations/ko-KR.json b/frontend/resources/translations/ko-KR.json index 03174e2ed2..b0b7a472b4 100644 --- a/frontend/resources/translations/ko-KR.json +++ b/frontend/resources/translations/ko-KR.json @@ -222,7 +222,7 @@ }, "themeUpload": { "button": "업로드", - "description": "아래 버튼을 사용하여 나만의 AppFlowy 테마를 업로드하세요.", + "description": "아래 버튼을 사용하여 나만의 @:appName 테마를 업로드하세요.", "loading": "테마를 확인하고 업로드하는 동안 잠시 기다려 주십시오...", "uploadSuccess": "테마가 성공적으로 업로드되었습니다.", "deletionFailure": "테마를 삭제하지 못했습니다. 수동으로 삭제해 보십시오.", @@ -241,7 +241,7 @@ "defaultLocation": "파일 및 데이터 저장 위치 읽기", "exportData": "데이터 내보내기", "doubleTapToCopy": "경로를 복사하려면 두 번 탭하세요.", - "restoreLocation": "AppFlowy 기본 경로로 복원", + "restoreLocation": "@:appName 기본 경로로 복원", "customizeLocation": "다른 폴더 열기", "restartApp": "변경 사항을 적용하려면 앱을 다시 시작하십시오.", "exportDatabase": "데이터베이스 내보내기", @@ -253,10 +253,10 @@ "defineWhereYourDataIsStored": "데이터가 저장되는 위치 정의", "open": "열려 있는", "openFolder": "기존 폴더 열기", - "openFolderDesc": "기존 AppFlowy 폴더에서 읽고 쓰기", + "openFolderDesc": "기존 @:appName 폴더에서 읽고 쓰기", "folderHintText": "폴더 이름", "location": "새 폴더 만들기", - "locationDesc": "AppFlowy 데이터 폴더의 이름을 선택하세요", + "locationDesc": "@:appName 데이터 폴더의 이름을 선택하세요", "browser": "검색", "create": "만들다", "set": "세트", @@ -267,7 +267,7 @@ "change": "변화", "openLocationTooltips": "다른 데이터 디렉토리 열기", "openCurrentDataFolder": "현재 데이터 디렉토리 열기", - "recoverLocationTooltips": "AppFlowy의 기본 데이터 디렉터리로 재설정", + "recoverLocationTooltips": "@:appName의 기본 데이터 디렉터리로 재설정", "exportFileSuccess": "파일을 성공적으로 내보냈습니다!", "exportFileFail": "파일 내보내기 실패!", "export": "내보내다" @@ -577,7 +577,7 @@ "referencedCalendarPrefix": "관점" }, "errorDialog": { - "title": "AppFlowy 오류", + "title": "@:appName 오류", "howToFixFallback": "불편을 끼쳐드려 죄송합니다! 오류를 설명하는 문제를 GitHub 페이지에 제출하세요.", "github": "GitHub에서 보기" }, diff --git a/frontend/resources/translations/pl-PL.json b/frontend/resources/translations/pl-PL.json index 43d17c717e..389eaa068d 100644 --- a/frontend/resources/translations/pl-PL.json +++ b/frontend/resources/translations/pl-PL.json @@ -56,7 +56,7 @@ "resetWorkspacePrompt": "Zresetowanie przestrzeni roboczej spowoduje usunięcie wszystkich znajdujących się w niej stron i danych. Czy na pewno chcesz zresetować przestrzeń roboczą? Alternatywnie możesz skontaktować się z zespołem pomocy technicznej, aby przywrócić przestrzeń roboczą", "hint": "przestrzeń robocza", "notFoundError": "Przestrzeni nie znaleziono", - "failedToLoad": "Coś poszło nie tak! Wczytywanie przestrzeni roboczej nie powiodło się. Spróbuj wyłączyć wszystkie otwarte instancje AppFlowy i spróbuj ponownie.", + "failedToLoad": "Coś poszło nie tak! Wczytywanie przestrzeni roboczej nie powiodło się. Spróbuj wyłączyć wszystkie otwarte instancje @:appName i spróbuj ponownie.", "errorActions": { "reportIssue": "Zgłoś problem", "reachOut": "Skontaktuj się na Discord" @@ -310,7 +310,7 @@ }, "themeUpload": { "button": "Prześlij", - "description": "Prześlij własny motyw AppFlowy za pomocą przycisku poniżej.", + "description": "Prześlij własny motyw @:appName za pomocą przycisku poniżej.", "loading": "Poczekaj, aż zweryfikujemy i prześlemy Twój motyw...", "uploadSuccess": "Twój motyw został przesłany pomyślnie", "deletionFailure": "Nie udało się usunąć motywu. Spróbuj usunąć go ręcznie.", @@ -341,7 +341,7 @@ "defaultLocation": "Ścieżka katalogu z plikami", "exportData": "Eksportuj swoje dane", "doubleTapToCopy": "Kliknij dwukrotnie, aby skopiować ścieżkę", - "restoreLocation": "Przywróć domyślną ścieżkę AppFlowy", + "restoreLocation": "Przywróć domyślną ścieżkę @:appName", "customizeLocation": "Otwórz inny folder", "restartApp": "Uruchom ponownie aplikację, aby zmiany zaczęły obowiązywać.", "exportDatabase": "Eksportuj bazę danych", @@ -353,10 +353,10 @@ "defineWhereYourDataIsStored": "Zdefiniuj miejsce przechowywania Twoich danych", "open": "Otwórz", "openFolder": "Otwórz istniejący folder", - "openFolderDesc": "Przeczytaj i zapisz go w istniejącym folderze AppFlowy", + "openFolderDesc": "Przeczytaj i zapisz go w istniejącym folderze @:appName", "folderHintText": "Nazwa folderu", "location": "Tworzenie nowego folderu", - "locationDesc": "Wybierz nazwę folderu danych AppFlowy", + "locationDesc": "Wybierz nazwę folderu danych @:appName", "browser": "Przeglądaj", "create": "Stwórz", "set": "Ustaw", @@ -367,7 +367,7 @@ "change": "Zmień", "openLocationTooltips": "Otwórz inny katalog danych", "openCurrentDataFolder": "Otwórz bieżący katalog danych", - "recoverLocationTooltips": "Zresetuj do domyślnego katalogu danych AppFlowy", + "recoverLocationTooltips": "Zresetuj do domyślnego katalogu danych @:appName", "exportFileSuccess": "Eksportowanie pliku zakończono pomyślnie!", "exportFileFail": "Eksport pliku nie powiódł się!", "export": "Eksport" @@ -821,7 +821,7 @@ "referencedCalendarPrefix": "Widok" }, "errorDialog": { - "title": "Błąd AppFlowy", + "title": "Błąd @:appName", "howToFixFallback": "Przepraszamy za niedogodności! Zgłoś problem na naszej stronie GitHub, który opisuje Twój błąd.", "github": "Zobacz na GitHubie" }, diff --git a/frontend/resources/translations/pt-BR.json b/frontend/resources/translations/pt-BR.json index 046eee783e..dc1340d3b8 100644 --- a/frontend/resources/translations/pt-BR.json +++ b/frontend/resources/translations/pt-BR.json @@ -58,7 +58,7 @@ "resetWorkspacePrompt": "A redefinição do espaço de trabalho excluirá todas as páginas e dados contidos nele. Tem certeza de que deseja redefinir o espaço de trabalho? Alternativamente, você pode entrar em contato com a equipe de suporte para restaurar o espaço de trabalho", "hint": "Espaço de trabalho", "notFoundError": "Espaço de trabalho não encontrado", - "failedToLoad": "Algo deu errado! Falha ao carregar o espaço de trabalho. Tente fechar qualquer instância aberta do AppFlowy e tente novamente.", + "failedToLoad": "Algo deu errado! Falha ao carregar o espaço de trabalho. Tente fechar qualquer instância aberta do @:appName e tente novamente.", "errorActions": { "reportIssue": "Reporte um problema", "reachOut": "Entre em contato no Discord" @@ -280,7 +280,7 @@ "cloudSupabaseUrl": "URL da Supabase", "cloudSupabaseAnonKey": "Chave anônima Supabase", "cloudSupabaseAnonKeyCanNotBeEmpty": "A chave anon não pode estar vazia se o URL da supabase não estiver vazio", - "cloudAppFlowy": "AppFlowy Cloud", + "cloudAppFlowy": "@:appName Cloud Beta", "clickToCopy": "Clique para copiar", "selfHostStart": "Se você não possui um servidor, consulte o", "selfHostContent": "documento", @@ -299,11 +299,11 @@ "historicalUserList": "Histórico de login do usuário", "historicalUserListTooltip": "Esta lista exibe suas contas anônimas. Você pode clicar em uma conta para ver seus detalhes. Contas anônimas são criadas clicando no botão ‘Começar’", "openHistoricalUser": "Clique para abrir a conta anônima", - "customPathPrompt": "Armazenar a pasta de dados do AppFlowy em uma pasta sincronizada na nuvem, como o Google Drive, pode representar riscos. Se o banco de dados nesta pasta for acessado ou modificado de vários locais ao mesmo tempo, isso poderá resultar em conflitos de sincronização e possível corrupção de dados", - "importAppFlowyData": "Importar dados da pasta AppFlowy externa", - "importAppFlowyDataDescription": "Copie dados de uma pasta de dados externa do AppFlowy e importe-os para a pasta de dados atual do AppFlowy", - "importSuccess": "Importou com sucesso a pasta de dados do AppFlowy", - "importFailed": "Falha ao importar a pasta de dados do AppFlowy", + "customPathPrompt": "Armazenar a pasta de dados do @:appName em uma pasta sincronizada na nuvem, como o Google Drive, pode representar riscos. Se o banco de dados nesta pasta for acessado ou modificado de vários locais ao mesmo tempo, isso poderá resultar em conflitos de sincronização e possível corrupção de dados", + "importAppFlowyData": "Importar dados da pasta @:appName externa", + "importAppFlowyDataDescription": "Copie dados de uma pasta de dados externa do @:appName e importe-os para a pasta de dados atual do @:appName", + "importSuccess": "Importou com sucesso a pasta de dados do @:appName", + "importFailed": "Falha ao importar a pasta de dados do @:appName", "importGuide": "Para mais detalhes, consulte o documento referenciado", "supabaseSetting": "Configuração de Supabase" }, @@ -354,7 +354,7 @@ "themeUpload": { "button": "Carregar", "uploadTheme": "Carregar tema", - "description": "Carregue seu próprio tema AppFlowy usando o botão abaixo.", + "description": "Carregue seu próprio tema @:appName usando o botão abaixo.", "loading": "Aguarde enquanto validamos e carregamos seu tema...", "uploadSuccess": "Seu tema foi carregado com sucesso", "deletionFailure": "Falha ao excluir o tema. Tente excluí-lo manualmente.", @@ -385,7 +385,7 @@ "defaultLocation": "Onde os seus dados ficam armazenados", "exportData": "Exporte seus dados", "doubleTapToCopy": "Clique duas vezes para copiar o caminho", - "restoreLocation": "Restaurar para o caminho padrão do AppFlowy", + "restoreLocation": "Restaurar para o caminho padrão do @:appName", "customizeLocation": "Abrir outra pasta", "restartApp": "Reinicie o aplicativo para que as alterações entrem em vigor.", "exportDatabase": "Exportar banco de dados", @@ -397,10 +397,10 @@ "defineWhereYourDataIsStored": "Defina onde seus dados são armazenados", "open": "Abrir", "openFolder": "Abra uma pasta existente", - "openFolderDesc": "Gravar na pasta AppFlowy existente ...", + "openFolderDesc": "Gravar na pasta @:appName existente ...", "folderHintText": "nome da pasta", "location": "Criando nova pasta", - "locationDesc": "Escolha um nome para sua pasta de dados do AppFlowy", + "locationDesc": "Escolha um nome para sua pasta de dados do @:appName", "browser": "Navegar", "create": "Criar", "set": "Definir", @@ -411,7 +411,7 @@ "change": "Mudar", "openLocationTooltips": "Abra outro diretório de dados", "openCurrentDataFolder": "Abra o diretório de dados atual", - "recoverLocationTooltips": "Redefinir para o diretório de dados padrão do AppFlowy", + "recoverLocationTooltips": "Redefinir para o diretório de dados padrão do @:appName", "exportFileSuccess": "Exportar arquivo com sucesso!", "exportFileFail": "Falha na exportação do arquivo!", "export": "Exportar" @@ -945,7 +945,7 @@ "quickJumpYear": "Ir para" }, "errorDialog": { - "title": "Erro do AppFlowy", + "title": "Erro do @:appName", "howToFixFallback": "Lamentamos o inconveniente! Envie um problema em nossa página do GitHub que descreva seu erro.", "github": "Ver no GitHub" }, diff --git a/frontend/resources/translations/pt-PT.json b/frontend/resources/translations/pt-PT.json index ab4bd7d438..1b5ee1fcd1 100644 --- a/frontend/resources/translations/pt-PT.json +++ b/frontend/resources/translations/pt-PT.json @@ -56,7 +56,7 @@ "resetWorkspacePrompt": "Reinciar do espaço de trabalho excluirá todas as páginas e dados contidos. Tem certeza de que deseja reiniciar o espaço de trabalho? Alternativamente, podes entrar em contato com a equipe de suporte para restaurar o espaço de trabalho", "hint": "ambiente de trabalho", "notFoundError": "Ambiente de trabalho não encontrada", - "failedToLoad": "Algo correu mal! Falha ao carregar o espaço de trabalho. Tente fechar qualquer instância aberta do AppFlowy e tente novamente.", + "failedToLoad": "Algo correu mal! Falha ao carregar o espaço de trabalho. Tente fechar qualquer instância aberta do @:appName e tente novamente.", "errorActions": { "reportIssue": "Relatar problema", "reachOut": "Entre em contacto no Discord" @@ -293,7 +293,7 @@ "themeUpload": { "button": "Carregar", "uploadTheme": "Carregar tema", - "description": "Carregue seu próprio tema AppFlowy usando o botão abaixo.", + "description": "Carregue seu próprio tema @:appName usando o botão abaixo.", "loading": "Aguarde enquanto validamos e carregamos seu tema...", "uploadSuccess": "Seu tema foi carregado com sucesso", "deletionFailure": "Falha ao excluir o tema. Tente excluí-lo manualmente.", @@ -326,7 +326,7 @@ "defaultLocation": "Leia arquivos e local de armazenamento de dados", "exportData": "Exporte seus dados", "doubleTapToCopy": "Toque duas vezes para copiar o caminho", - "restoreLocation": "Restaurar para o caminho padrão do AppFlowy", + "restoreLocation": "Restaurar para o caminho padrão do @:appName", "customizeLocation": "Abra outra pasta", "restartApp": "Reinicie o aplicativo para que as alterações entrem em vigor.", "exportDatabase": "Exportar banco de dados", @@ -338,10 +338,10 @@ "defineWhereYourDataIsStored": "Defina onde seus dados são armazenados", "open": "Abrir", "openFolder": "Abra uma pasta existente", - "openFolderDesc": "Leia e grave-o em sua pasta AppFlowy existente", + "openFolderDesc": "Leia e grave-o em sua pasta @:appName existente", "folderHintText": "nome da pasta", "location": "Criando uma nova pasta", - "locationDesc": "Escolha um nome para sua pasta de dados do AppFlowy", + "locationDesc": "Escolha um nome para sua pasta de dados do @:appName", "browser": "Navegar", "create": "Criar", "set": "Definir", @@ -352,7 +352,7 @@ "change": "Mudar", "openLocationTooltips": "Abra outro diretório de dados", "openCurrentDataFolder": "Abra o diretório de dados atual", - "recoverLocationTooltips": "Redefinir para o diretório de dados padrão do AppFlowy", + "recoverLocationTooltips": "Redefinir para o diretório de dados padrão do @:appName", "exportFileSuccess": "Exportar arquivo com sucesso!", "exportFileFail": "Falha na exportação do arquivo!", "export": "Exportar" @@ -754,7 +754,7 @@ "referencedCalendarPrefix": "Vista de" }, "errorDialog": { - "title": "Erro do AppFlowy", + "title": "Erro do @:appName", "howToFixFallback": "Lamentamos o inconveniente! Envie um problema em nossa página do GitHub que descreva seu erro.", "github": "Ver no GitHub" }, diff --git a/frontend/resources/translations/ru-RU.json b/frontend/resources/translations/ru-RU.json index 72e7a7424e..89b81271dc 100644 --- a/frontend/resources/translations/ru-RU.json +++ b/frontend/resources/translations/ru-RU.json @@ -58,7 +58,7 @@ "resetWorkspacePrompt": "Сброс рабочего пространства приведет к удалению всех страниц и данных внутри него. Вы уверены, что хотите сбросить рабочее пространство? В качестве альтернативы вы можете обратиться в службу поддержки для восстановления рабочего пространства", "hint": "рабочее пространство", "notFoundError": "Рабочее пространство не найдено", - "failedToLoad": "Что-то пошло не так! Не удалось загрузить рабочее пространство. Попробуйте закрыть все открытые экземпляры AppFlowy и повторите попытку.", + "failedToLoad": "Что-то пошло не так! Не удалось загрузить рабочее пространство. Попробуйте закрыть все открытые экземпляры @:appName и повторите попытку.", "errorActions": { "reportIssue": "Сообщить о проблеме", "reportIssueOnGithub": "Сообщить о проблеме на Github", @@ -292,8 +292,8 @@ "cloudSupabaseUrlCanNotBeEmpty": "URL-адрес Supabase не может быть пустым.", "cloudSupabaseAnonKey": "Анонимный ключ Supabase", "cloudSupabaseAnonKeyCanNotBeEmpty": "Анонимный ключ не может быть пустым, если URL Supabase не пуст", - "cloudAppFlowy": "AppFlowy Cloud", - "cloudAppFlowySelfHost": "AppFlowy Cloud на своём сервере", + "cloudAppFlowy": "@:appName Cloud Beta", + "cloudAppFlowySelfHost": "@:appName Cloud на своём сервере", "appFlowyCloudUrlCanNotBeEmpty": "URL облака не может быть пустым.", "clickToCopy": "Нажмите, чтобы скопировать", "selfHostStart": "Если у вас нет сервера, пожалуйста, обратитесь к", @@ -314,12 +314,12 @@ "historicalUserList": "История входа пользователя", "historicalUserListTooltip": "В этом списке отображаются ваши анонимные аккаунты. Вы можете нажать на аккаунт, чтобы посмотреть данные. Анонимный аккаунт создаётся нажатием кнопки «Начать».", "openHistoricalUser": "Нажмите, чтобы открыть анонимный аккаунт", - "customPathPrompt": "Хранение папки данных AppFlowy в папке с облачной синхронизацией, например на Google Диске, может представлять риск. Если база данных в этой папке будет доступна или изменена с нескольких мест одновременно, это может привести к конфликтам синхронизации и потенциальному повреждению данных", - "importAppFlowyData": "Импортировать данные из внешней папки AppFlowy", + "customPathPrompt": "Хранение папки данных @:appName в папке с облачной синхронизацией, например на Google Диске, может представлять риск. Если база данных в этой папке будет доступна или изменена с нескольких мест одновременно, это может привести к конфликтам синхронизации и потенциальному повреждению данных", + "importAppFlowyData": "Импортировать данные из внешней папки @:appName", "importingAppFlowyDataTip": "Выполняется импорт данных. Пожалуйста, не закрывайте приложение", - "importAppFlowyDataDescription": "Скопируйте данные из внешней папки данных AppFlowy и импортируйте их в текущую папку данных AppFlowy.", - "importSuccess": "Папка данных AppFlowy успешно импортирована", - "importFailed": "Не удалось импортировать папку данных AppFlowy", + "importAppFlowyDataDescription": "Скопируйте данные из внешней папки данных @:appName и импортируйте их в текущую папку данных @:appName.", + "importSuccess": "Папка данных @:appName успешно импортирована", + "importFailed": "Не удалось импортировать папку данных @:appName", "importGuide": "Для получения более подробной информации, пожалуйста, проверьте указанный документ.", "supabaseSetting": "Настройка Supabase" }, @@ -371,7 +371,7 @@ "themeUpload": { "button": "Загрузить", "uploadTheme": "Загрузить тему", - "description": "Загрузите собственную тему AppFlowy, используя кнопку ниже.", + "description": "Загрузите собственную тему @:appName, используя кнопку ниже.", "loading": "Подождите, пока мы проверим и загрузим вашу тему...", "uploadSuccess": "Ваша тема была успешно загружена", "deletionFailure": "Не удалось удалить тему. Попробуйте удалить её вручную.", @@ -403,7 +403,7 @@ "defaultLocation": "Путь до хранилища", "exportData": "Экспорт данных", "doubleTapToCopy": "Нажмите дважды, чтобы скопировать путь", - "restoreLocation": "Восстановить путь AppFlowy по умолчанию", + "restoreLocation": "Восстановить путь @:appName по умолчанию", "customizeLocation": "Выбрать другую папку", "restartApp": "Пожалуйста, перезапустите приложение, чтобы изменения вступили в силу.", "exportDatabase": "Экспорт базы данных", @@ -415,10 +415,10 @@ "defineWhereYourDataIsStored": "Указать хранилище данных", "open": "Открыть", "openFolder": "Открыть существующую папку", - "openFolderDesc": "Чтение и запись в существующую папку AppFlowy ...", + "openFolderDesc": "Чтение и запись в существующую папку @:appName ...", "folderHintText": "имя папки", "location": "Создание новой папки", - "locationDesc": "Выбрать имя папки данных AppFlowy", + "locationDesc": "Выбрать имя папки данных @:appName", "browser": "Обзор", "create": "Создать", "set": "Установить", diff --git a/frontend/resources/translations/sv-SE.json b/frontend/resources/translations/sv-SE.json index f35966d390..50be68350d 100644 --- a/frontend/resources/translations/sv-SE.json +++ b/frontend/resources/translations/sv-SE.json @@ -272,12 +272,12 @@ "historicalUserList": "Användarinloggningshistorik", "historicalUserListTooltip": "Den här listan visar dina anonyma konton. Du kan klicka på ett konto för att se dess detaljer. Anonyma konton skapas genom att klicka på knappen \"Kom igång\".", "openHistoricalUser": "Klicka för att öppna det anonyma kontot", - "customPathPrompt": "Att lagra AppFlowy-datamappen i en molnsynkroniserad mapp som Google Drive kan innebära risker. Om databasen i den här mappen nås eller ändras från flera platser samtidigt, kan det resultera i synkroniseringskonflikter och potentiell datakorruption", - "importAppFlowyData": "Importera data från extern AppFlowy-mapp", + "customPathPrompt": "Att lagra @:appName-datamappen i en molnsynkroniserad mapp som Google Drive kan innebära risker. Om databasen i den här mappen nås eller ändras från flera platser samtidigt, kan det resultera i synkroniseringskonflikter och potentiell datakorruption", + "importAppFlowyData": "Importera data från extern @:appName-mapp", "importingAppFlowyDataTip": "Dataimport pågår. Stäng inte appen", - "importAppFlowyDataDescription": "Kopiera data från en extern AppFlowy-datamapp och importera den till den aktuella AppFlowy-datamappen", - "importSuccess": "AppFlowy-datamappen har importerats", - "importFailed": "Det gick inte att importera AppFlowy-datamappen", + "importAppFlowyDataDescription": "Kopiera data från en extern @:appName-datamapp och importera den till den aktuella @:appName-datamappen", + "importSuccess": "@:appName-datamappen har importerats", + "importFailed": "Det gick inte att importera @:appName-datamappen", "importGuide": "För ytterligare information, snälla se det refererade dokumentet", "supabaseSetting": "Supabase-inställning" }, @@ -294,7 +294,7 @@ }, "themeUpload": { "button": "Ladda upp", - "description": "Ladda upp ditt eget AppFlowy-tema med knappen nedan.", + "description": "Ladda upp ditt eget @:appName-tema med knappen nedan.", "loading": "Vänta medan vi validerar och laddar upp ditt tema...", "uploadSuccess": "Ditt tema laddades upp", "deletionFailure": "Det gick inte att ta bort temat. Försök att radera det manuellt.", @@ -311,7 +311,7 @@ "defaultLocation": "Läs filer och datalagringsplats", "exportData": "Exportera dina data", "doubleTapToCopy": "Dubbeltryck för att kopiera sökvägen", - "restoreLocation": "Återställ till AppFlowy standardsökväg", + "restoreLocation": "Återställ till @:appName standardsökväg", "customizeLocation": "Öppna en annan mapp", "restartApp": "Starta om appen för att ändringarna ska träda i kraft.", "exportDatabase": "Exportera databas", @@ -323,10 +323,10 @@ "defineWhereYourDataIsStored": "Definiera var din data lagras", "open": "Öppen", "openFolder": "Öppna en befintlig mapp", - "openFolderDesc": "Läs och skriv det till din befintliga AppFlowy-mapp", + "openFolderDesc": "Läs och skriv det till din befintliga @:appName-mapp", "folderHintText": "mappnamn", "location": "Skapar en ny mapp", - "locationDesc": "Välj ett namn för din AppFlowy-datamapp", + "locationDesc": "Välj ett namn för din @:appName-datamapp", "browser": "Bläddra", "create": "Skapa", "set": "Uppsättning", @@ -337,7 +337,7 @@ "change": "Förändra", "openLocationTooltips": "Öppna en annan datakatalog", "openCurrentDataFolder": "Öppna aktuell datakatalog", - "recoverLocationTooltips": "Återställ till AppFlowys standarddatakatalog", + "recoverLocationTooltips": "Återställ till @:appNames standarddatakatalog", "exportFileSuccess": "Exporterade filen framgångsrikt!", "exportFileFail": "Export av fil misslyckades!", "export": "Exportera" @@ -647,7 +647,7 @@ "referencedCalendarPrefix": "Utsikt över" }, "errorDialog": { - "title": "AppFlowy-fel", + "title": "@:appName-fel", "howToFixFallback": "Vi ber om ursäkt för besväret! Skapa en felrapport på vår GitHub-sida som beskriver ditt fel.", "github": "Visa på GitHub" }, diff --git a/frontend/resources/translations/tr-TR.json b/frontend/resources/translations/tr-TR.json index acfacef891..66dd2e0d69 100644 --- a/frontend/resources/translations/tr-TR.json +++ b/frontend/resources/translations/tr-TR.json @@ -63,7 +63,7 @@ "resetWorkspacePrompt": "Çalışma alanını sıfırlamak, içindeki tüm sayfaları ve verileri silecektir. Çalışma alanını sıfırlamak istediğinizden emin misiniz? Alternatif olarak, çalışma alanını geri yüklemek için destek ekibiyle iletişime geçebilirsiniz", "hint": "çalışma alanı", "notFoundError": "Çalışma alanı bulunamadı", - "failedToLoad": "Bir şeyler yanlış gitti! Çalışma alanı yüklenemedi. Açık olan tüm AppFlowy örneklerini kapatıp tekrar deneyin.", + "failedToLoad": "Bir şeyler yanlış gitti! Çalışma alanı yüklenemedi. Açık olan tüm @:appName örneklerini kapatıp tekrar deneyin.", "errorActions": { "reportIssue": "Sorun bildir", "reportIssueOnGithub": "GitHub'da sorun bildir", @@ -330,8 +330,8 @@ "cloudSupabaseUrlCanNotBeEmpty": "Supabase url'si boş olamaz", "cloudSupabaseAnonKey": "Supabase anonim anahtarı", "cloudSupabaseAnonKeyCanNotBeEmpty": "Anonim anahtar boş olamaz", - "cloudAppFlowy": "AppFlowy Bulutu Beta", - "cloudAppFlowySelfHost": "AppFlowy Bulutu Kendi Sunucunuzda", + "cloudAppFlowy": "@:appName Bulutu Beta", + "cloudAppFlowySelfHost": "@:appName Bulutu Kendi Sunucunuzda", "appFlowyCloudUrlCanNotBeEmpty": "Bulut url'si boş olamaz", "clickToCopy": "Kopyalamak için tıklayın", "selfHostStart": "Bir sunucunuz yoksa, lütfen", @@ -352,12 +352,12 @@ "historicalUserList": "Kullanıcı giriş geçmişi", "historicalUserListTooltip": "Bu liste anonim hesaplarınızı görüntüler. Ayrıntılarını görüntülemek için bir hesaba tıklayabilirsiniz. Anonim hesaplar 'Başlayın' düğmesine tıklanarak oluşturulur", "openHistoricalUser": "Anonim hesabı açmak için tıklayın", - "customPathPrompt": "AppFlowy veri klasörünü Google Drive gibi bulutla senkronize edilmiş bir klasörde saklamak risk oluşturabilir. Bu klasördeki veritabanına aynı anda birden fazla konumdan erişilir veya değiştirilirse, senkronizasyon çakışmaları ve olası veri bozulmasıyla sonuçlanabilir", - "importAppFlowyData": "Harici AppFlowy Klasöründen Veri Al", + "customPathPrompt": "@:appName veri klasörünü Google Drive gibi bulutla senkronize edilmiş bir klasörde saklamak risk oluşturabilir. Bu klasördeki veritabanına aynı anda birden fazla konumdan erişilir veya değiştirilirse, senkronizasyon çakışmaları ve olası veri bozulmasıyla sonuçlanabilir", + "importAppFlowyData": "Harici @:appName Klasöründen Veri Al", "importingAppFlowyDataTip": "Veri aktarımı devam ediyor. Lütfen uygulamayı kapatmayın", - "importAppFlowyDataDescription": "Harici bir AppFlowy veri klasöründen veri kopyalayın ve geçerli AppFlowy veri klasörüne aktarın", - "importSuccess": "AppFlowy veri klasörü başarıyla alındı", - "importFailed": "AppFlowy veri klasörü alınamadı", + "importAppFlowyDataDescription": "Harici bir @:appName veri klasöründen veri kopyalayın ve geçerli @:appName veri klasörüne aktarın", + "importSuccess": "@:appName veri klasörü başarıyla alındı", + "importFailed": "@:appName veri klasörü alınamadı", "importGuide": "Daha fazla bilgi için lütfen referans belgeyi kontrol edin" }, "notifications": { @@ -408,7 +408,7 @@ "themeUpload": { "button": "Yükle", "uploadTheme": "Temayı yükle", - "description": "Aşağıdaki düğmeyi kullanarak kendi AppFlowy temanızı yükleyin.", + "description": "Aşağıdaki düğmeyi kullanarak kendi @:appName temanızı yükleyin.", "loading": "Lütfen temanızı doğrularken ve yüklerken bekleyin...", "uploadSuccess": "Temanız başarıyla yüklendi", "deletionFailure": "Temayı silinemedi. Manuel olarak silmeyi deneyin.", @@ -467,7 +467,7 @@ "defaultLocation": "Dosyaları ve verileri okuma konumu", "exportData": "Verilerinizi dışa aktarın", "doubleTapToCopy": "Yolu kopyalamak için iki kez dokunun", - "restoreLocation": "AppFlowy varsayılan yoluna geri yükle", + "restoreLocation": "@:appName varsayılan yoluna geri yükle", "customizeLocation": "Başka bir klasör aç", "restartApp": "Değişikliklerin etkili olması için lütfen uygulamayı yeniden başlatın.", "exportDatabase": "Veritabanını dışa aktar", @@ -479,10 +479,10 @@ "defineWhereYourDataIsStored": "Verilerinizin nerede saklandığını tanımlayın", "open": "Aç", "openFolder": "Mevcut bir klasörü aç", - "openFolderDesc": "Mevcut AppFlowy klasörünüze okuyun ve yazın", + "openFolderDesc": "Mevcut @:appName klasörünüze okuyun ve yazın", "folderHintText": "klasör adı", "location": "Yeni bir klasör oluşturuluyor", - "locationDesc": "AppFlowy veri klasörünüz için bir ad seçin", + "locationDesc": "@:appName veri klasörünüz için bir ad seçin", "browser": "Gözat", "create": "Oluştur", "set": "Ayarla", @@ -493,7 +493,7 @@ "change": "Değiştir", "openLocationTooltips": "Başka bir veri dizini aç", "openCurrentDataFolder": "Geçerli veri dizinini aç", - "recoverLocationTooltips": "AppFlowy'nin varsayılan veri dizinine sıfırla", + "recoverLocationTooltips": "@:appName'nin varsayılan veri dizinine sıfırla", "exportFileSuccess": "Dosya başarıyla dışa aktarıldı!", "exportFileFail": "Dosya dışa aktarılamadı!", "export": "Dışa Aktar", @@ -1116,7 +1116,7 @@ "duplicateEvent": "Etkinliği kopyala" }, "errorDialog": { - "title": "AppFlowy Hatası", + "title": "@:appName Hatası", "howToFixFallback": "Verdiğimiz rahatsızlıktan dolayı özür dileriz! Lütfen GitHub sayfamızda hatanızı açıklayan bir sorun bildirin.", "github": "GitHub'da Görüntüle" }, diff --git a/frontend/resources/translations/uk-UA.json b/frontend/resources/translations/uk-UA.json index 3c36875f96..9e0cdf64f4 100644 --- a/frontend/resources/translations/uk-UA.json +++ b/frontend/resources/translations/uk-UA.json @@ -43,10 +43,10 @@ "unmatchedPasswordError": "Повторний пароль не співпадає з паролем", "syncPromptMessage": "Синхронізація даних може зайняти трохи часу. Будь ласка, не закривайте цю сторінку", "or": "АБО", + "signInWith": "Увійти за допомогою:", "LogInWithGoogle": "Увійти за допомогою Google", "LogInWithGithub": "Увійти за допомогою Github", - "LogInWithDiscord": "Увійти за допомогою Discord", - "signInWith": "Увійти за допомогою:" + "LogInWithDiscord": "Увійти за допомогою Discord" }, "workspace": { "chooseWorkspace": "Виберіть свій робочий простір", @@ -194,6 +194,7 @@ }, "button": { "ok": "OK", + "done": "Готово", "cancel": "Скасувати", "signIn": "Увійти", "signOut": "Вийти", @@ -210,7 +211,6 @@ "edit": "Редагувати", "delete": "Видалити", "duplicate": "Дублювати", - "done": "Готово", "putback": "Повернути" }, "label": { @@ -285,12 +285,12 @@ "button": "Завантажити", "uploadTheme": "Завантажити тему", "description": "Завантажте свою власну тему AppFlowy, скориставшись кнопкою нижче.", - "failure": "Тему, яка була завантажена, має неправильний формат.", "loading": "Будь ласка, зачекайте, поки ми перевіряємо та завантажуємо вашу тему...", "uploadSuccess": "Вашу тему успішно завантажено", "deletionFailure": "Не вдалося видалити тему. Спробуйте видалити її вручну.", "filePickerDialogTitle": "Виберіть файл .flowy_plugin", - "urlUploadFailure": "Не вдалося відкрити URL: {}" + "urlUploadFailure": "Не вдалося відкрити URL: {}", + "failure": "Тему, яка була завантажена, має неправильний формат." }, "theme": "Тема", "builtInsLabel": "Вбудовані теми", diff --git a/frontend/resources/translations/vi-VN.json b/frontend/resources/translations/vi-VN.json index 9b89f50dbd..f0b4c0b10d 100644 --- a/frontend/resources/translations/vi-VN.json +++ b/frontend/resources/translations/vi-VN.json @@ -60,7 +60,7 @@ "resetWorkspacePrompt": "Đặt lại không gian làm việc sẽ xóa tất cả các trang và dữ liệu trong đó. Bạn có chắc chắn muốn đặt lại không gian làm việc? Ngoài ra, bạn có thể liên hệ với nhóm hỗ trợ để khôi phục không gian làm việc", "hint": "không gian làm việc", "notFoundError": "Không tìm thấy không gian làm việc", - "failedToLoad": "Đã xảy ra lỗi! Không tải được không gian làm việc. Hãy thử đóng mọi phiên bản đang mở của AppFlowy và thử lại.", + "failedToLoad": "Đã xảy ra lỗi! Không tải được không gian làm việc. Hãy thử đóng mọi phiên bản đang mở của @:appName và thử lại.", "errorActions": { "reportIssue": "Báo cáo một vấn đề", "reportIssueOnGithub": "Báo cáo sự cố trên Github", @@ -314,7 +314,7 @@ "cloudSupabaseUrl": "Supabase URL", "cloudSupabaseAnonKey": "Supabase anon key", "cloudSupabaseAnonKeyCanNotBeEmpty": "Anon key không được để trống nếu url supabase không trống", - "cloudAppFlowy": "AppFlowy Cloud Beta", + "cloudAppFlowy": "@:appName Cloud Beta", "clickToCopy": "Bấm để sao chép", "selfHostStart": "Nếu bạn không có máy chủ, vui lòng tham khảo", "selfHostContent": "tài liệu", @@ -332,11 +332,11 @@ "inputTextFieldHint": "Bí mật của bạn", "historicalUserList": "Lịch sử đăng nhập", "openHistoricalUser": "Ấn để mở tài khoản ẩn danh", - "importAppFlowyData": "Nhập dữ liệu từ thư mục AppFlowy bên ngoài", + "importAppFlowyData": "Nhập dữ liệu từ thư mục @:appName bên ngoài", "importingAppFlowyDataTip": "Quá trình nhập dữ liệu đang diễn ra. Vui lòng không đóng ứng dụng", - "importAppFlowyDataDescription": "Sao chép dữ liệu từ thư mục dữ liệu AppFlowy bên ngoài và nhập dữ liệu đó vào thư mục dữ liệu AppFlowy hiện tại", - "importSuccess": "Đã nhập thành công thư mục dữ liệu AppFlowy", - "importFailed": "Nhập thư mục dữ liệu AppFlowy không thành công", + "importAppFlowyDataDescription": "Sao chép dữ liệu từ thư mục dữ liệu @:appName bên ngoài và nhập dữ liệu đó vào thư mục dữ liệu @:appName hiện tại", + "importSuccess": "Đã nhập thành công thư mục dữ liệu @:appName", + "importFailed": "Nhập thư mục dữ liệu @:appName không thành công", "importGuide": "Để biết thêm chi tiết, vui lòng kiểm tra tài liệu được tham chiếu" }, "notifications": { @@ -430,7 +430,7 @@ "defaultLocation": "Đọc tập tin và vị trí lưu trữ dữ liệu", "exportData": "Xuất dữ liệu của bạn", "doubleTapToCopy": "Nhấn đúp để sao chép đường dẫn", - "restoreLocation": "Khôi phục về đường dẫn mặc định của AppFlowy", + "restoreLocation": "Khôi phục về đường dẫn mặc định của @:appName", "customizeLocation": "Mở thư mục khác", "restartApp": "Vui lòng khởi động lại ứng dụng để những thay đổi có hiệu lực.", "exportDatabase": "Xuất cơ sở dữ liệu", @@ -442,10 +442,10 @@ "defineWhereYourDataIsStored": "Xác định nơi dữ liệu của bạn được lưu trữ", "open": "Mở", "openFolder": "Mở một thư mục hiện có", - "openFolderDesc": "Đọc và ghi nó vào thư mục AppFlowy hiện có của bạn", + "openFolderDesc": "Đọc và ghi nó vào thư mục @:appName hiện có của bạn", "folderHintText": "tên thư mục", "location": "Tạo một thư mục mới", - "locationDesc": "Chọn tên cho thư mục dữ liệu AppFlowy của bạn", + "locationDesc": "Chọn tên cho thư mục dữ liệu @:appName của bạn", "browser": "Duyệt", "create": "Tạo", "folderPath": "Đường dẫn lưu trữ thư mục của bạn", @@ -455,7 +455,7 @@ "change": "Thay đổi", "openLocationTooltips": "Mở thư mục dữ liệu khác", "openCurrentDataFolder": "Mở thư mục dữ liệu hiện tại", - "recoverLocationTooltips": "Đặt lại về thư mục dữ liệu mặc định của AppFlowy", + "recoverLocationTooltips": "Đặt lại về thư mục dữ liệu mặc định của @:appName", "exportFileSuccess": "Xuất tập tin thành công!", "exportFileFail": "Xuất tập tin thất bại!", "export": "Xuất" @@ -765,7 +765,7 @@ } }, "errorDialog": { - "title": "Lỗi của AppFlowy", + "title": "Lỗi của @:appName", "howToFixFallback": "Chúng tôi xin lỗi vì sự cố này! Vui lòng mở sự cố trên GitHub để báo lỗi.", "github": "Xem trên GitHub" }, diff --git a/frontend/resources/translations/zh-CN.json b/frontend/resources/translations/zh-CN.json index a1f9d3807b..72240f857a 100644 --- a/frontend/resources/translations/zh-CN.json +++ b/frontend/resources/translations/zh-CN.json @@ -64,7 +64,7 @@ "resetWorkspacePrompt": "重置工作区将删除其中的所有页面和数据。您确定要重置工作区吗?您也可以联系技术支持团队来恢复工作区", "hint": "工作区", "notFoundError": "找不到工作区", - "failedToLoad": "出了些问题!我们无法加载工作区。请尝试关闭所有打开的 AppFlowy 实例,然后重试。", + "failedToLoad": "出了些问题!我们无法加载工作区。请尝试关闭所有打开的 @:appName 实例,然后重试。", "errorActions": { "reportIssue": "上报问题", "reportIssueOnGithub": "在 Github 上报告问题", @@ -333,8 +333,8 @@ "cloudSupabaseUrlCanNotBeEmpty": "supabase url 不能为空", "cloudSupabaseAnonKey": "Supabase Anon key", "cloudSupabaseAnonKeyCanNotBeEmpty": "如果 Supabase url 不为空,则 Anon key 不能为空", - "cloudAppFlowy": "AppFlowy Cloud", - "cloudAppFlowySelfHost": "AppFlowy Cloud 自托管", + "cloudAppFlowy": "@:appName Cloud Beta", + "cloudAppFlowySelfHost": "@:appName Cloud 自托管", "appFlowyCloudUrlCanNotBeEmpty": "云地址不能为空", "clickToCopy": "点击复制", "selfHostStart": "如果您没有服务器,请参阅", @@ -355,12 +355,12 @@ "historicalUserList": "用户登录历史记录", "historicalUserListTooltip": "此列表显示您的匿名帐户。您可以单击某个帐户来查看其详细信息。单击“开始”按钮即可创建匿名帐户", "openHistoricalUser": "点击开设匿名账户", - "customPathPrompt": "将 AppFlowy 数据文件夹存储在云同步文件夹(例如 Google Drive)中可能会带来风险。如果同时从多个位置访问或修改此文件夹中的数据库,可能会导致同步冲突和潜在的数据损坏", - "importAppFlowyData": "从外部 AppFlowy 文件夹导入数据", + "customPathPrompt": "将 @:appName 数据文件夹存储在云同步文件夹(例如 Google Drive)中可能会带来风险。如果同时从多个位置访问或修改此文件夹中的数据库,可能会导致同步冲突和潜在的数据损坏", + "importAppFlowyData": "从外部 @:appName 文件夹导入数据", "importingAppFlowyDataTip": "数据导入正在进行中。请不要关闭应用程序", - "importAppFlowyDataDescription": "从外部 AppFlowy 数据文件夹复制数据并将其导入到当前 AppFlowy 数据文件夹中", - "importSuccess": "成功导入AppFlowy数据文件夹", - "importFailed": "导入 AppFlowy 数据文件夹失败", + "importAppFlowyDataDescription": "从外部 @:appName 数据文件夹复制数据并将其导入到当前 @:appName 数据文件夹中", + "importSuccess": "成功导入@:appName数据文件夹", + "importFailed": "导入 @:appName 数据文件夹失败", "importGuide": "有关详细信息,请参阅参考文档", "supabaseSetting": "Supabase 设置", "cloudSetting": "云设置" @@ -413,7 +413,7 @@ "themeUpload": { "button": "上传", "uploadTheme": "上传主题", - "description": "使用下面的按钮上传您自己的 AppFlowy 主题。", + "description": "使用下面的按钮上传您自己的 @:appName 主题。", "loading": "我们正在验证并上传您的主题,请稍候...", "uploadSuccess": "您的主题已上传成功", "deletionFailure": "删除主题失败,请尝试手动删除。", @@ -463,7 +463,7 @@ "defaultLocation": "读取文件和数据存储位置", "exportData": "导出您的数据", "doubleTapToCopy": "双击复制路径", - "restoreLocation": "恢复为 AppFlowy 默认路径", + "restoreLocation": "恢复为 @:appName 默认路径", "customizeLocation": "打开另一个文件夹", "restartApp": "请重启 App 使设置生效", "exportDatabase": "导出数据库", @@ -475,10 +475,10 @@ "defineWhereYourDataIsStored": "定义数据存储位置", "open": "打开", "openFolder": "打开现有文件夹", - "openFolderDesc": "读取并将其写入您现有的 AppFlowy 文件夹", + "openFolderDesc": "读取并将其写入您现有的 @:appName 文件夹", "folderHintText": "文件夹名", "location": "正在新建文件夹", - "locationDesc": "为您的 AppFlowy 数据文件夹选择一个名称", + "locationDesc": "为您的 @:appName 数据文件夹选择一个名称", "browser": "浏览", "create": "新建", "set": "设置", @@ -489,7 +489,7 @@ "change": "更改", "openLocationTooltips": "打开另一个数据目录", "openCurrentDataFolder": "打开当前数据目录", - "recoverLocationTooltips": "恢复为 AppFlowy 默认数据目录", + "recoverLocationTooltips": "恢复为 @:appName 默认数据目录", "exportFileSuccess": "导出成功!", "exportFileFail": "导出失败!", "export": "导出", @@ -1093,7 +1093,7 @@ "quickJumpYear": "跳转到" }, "errorDialog": { - "title": "AppFlowy 错误", + "title": "@:appName 错误", "howToFixFallback": "对于给您带来的不便, 我们深表歉意! 请在我们的 GitHub 页面上提交 issue 并描述您遇到的错误。", "github": "在 GitHub 查看" }, diff --git a/frontend/resources/translations/zh-TW.json b/frontend/resources/translations/zh-TW.json index a7f81d0205..e49eca0e7e 100644 --- a/frontend/resources/translations/zh-TW.json +++ b/frontend/resources/translations/zh-TW.json @@ -75,7 +75,7 @@ "resetWorkspacePrompt": "重設工作區將刪除其中所有頁面和資料。你確定要重設工作區嗎?或者,你可以聯絡支援團隊來恢復工作區。", "hint": "工作區", "notFoundError": "找不到工作區", - "failedToLoad": "出了些問題!無法載入工作區。請嘗試關閉 AppFlowy 的任何開啟執行個體,然後再試一次。", + "failedToLoad": "出了些問題!無法載入工作區。請嘗試關閉 @:appName 的任何開啟執行個體,然後再試一次。", "errorActions": { "reportIssue": "回報問題", "reportIssueOnGithub": "在 Github 提交 issue", @@ -368,8 +368,8 @@ "cloudSupabaseUrlCanNotBeEmpty": "Supabase 網址不能為空", "cloudSupabaseAnonKey": "Supabase 匿名金鑰", "cloudSupabaseAnonKeyCanNotBeEmpty": "如果 Supabase 網址不為空,則匿名金鑰不得為空", - "cloudAppFlowy": "AppFlowy 雲端測試版 (Beta)", - "cloudAppFlowySelfHost": "自架 AppFlowy 雲端伺服器", + "cloudAppFlowy": "@:appName 雲端測試版 (Beta)", + "cloudAppFlowySelfHost": "自架 @:appName 雲端伺服器", "appFlowyCloudUrlCanNotBeEmpty": "雲端網址不能為空", "clickToCopy": "點選以複製", "selfHostStart": "若您尚未設定伺服器,請參閱", @@ -390,12 +390,12 @@ "historicalUserList": "使用者登入歷史", "historicalUserListTooltip": "此列表顯示您的匿名帳號。您可以點選帳號以檢視其詳細資訊。透過點選「開始使用」按鈕來建立匿名帳號", "openHistoricalUser": "點選以開啟匿名帳號", - "customPathPrompt": "將 AppFlowy 資料資料夾儲存在如 Google 雲端硬碟等雲端同步資料夾中可能會帶來風險。如果該資料庫在多處同時被存取或修改,可能會導致同步衝突和潛在的資料損壞", - "importAppFlowyData": "從外部 AppFlowy 資料夾匯入資料", + "customPathPrompt": "將 @:appName 資料資料夾儲存在如 Google 雲端硬碟等雲端同步資料夾中可能會帶來風險。如果該資料庫在多處同時被存取或修改,可能會導致同步衝突和潛在的資料損壞", + "importAppFlowyData": "從外部 @:appName 資料夾匯入資料", "importingAppFlowyDataTip": "資料正在匯入中。請勿關閉應用程式", - "importAppFlowyDataDescription": "從外部 AppFlowy 資料夾複製資料並匯入到目前的 AppFlowy 資料夾", - "importSuccess": "成功匯入 AppFlowy 資料夾", - "importFailed": "匯入 AppFlowy 資料夾失敗", + "importAppFlowyDataDescription": "從外部 @:appName 資料夾複製資料並匯入到目前的 @:appName 資料夾", + "importSuccess": "成功匯入 @:appName 資料夾", + "importFailed": "匯入 @:appName 資料夾失敗", "importGuide": "欲瞭解更多詳細資訊,請查閱參考文件", "supabaseSetting": "supabase 設定" }, @@ -448,7 +448,7 @@ "themeUpload": { "button": "上傳", "uploadTheme": "上傳主題", - "description": "使用下方的按鈕上傳您自己的 AppFlowy 主題。", + "description": "使用下方的按鈕上傳您自己的 @:appName 主題。", "loading": "我們正在驗證並上傳您的主題,請稍候...", "uploadSuccess": "您的主題已成功上傳", "deletionFailure": "刪除主題失敗。請嘗試手動刪除。", @@ -499,10 +499,10 @@ }, "files": { "copy": "複製", - "defaultLocation": "AppFlowy 資料儲存位置", + "defaultLocation": "@:appName 資料儲存位置", "exportData": "匯出您的資料", "doubleTapToCopy": "點選兩下以複製路徑", - "restoreLocation": "恢復為 AppFlowy 預設路徑", + "restoreLocation": "恢復為 @:appName 預設路徑", "customizeLocation": "開啟其他資料夾", "restartApp": "請重新啟動應用程式以使變更生效。", "exportDatabase": "匯出資料庫", @@ -514,10 +514,10 @@ "defineWhereYourDataIsStored": "定義您的資料儲存位置", "open": "開啟", "openFolder": "開啟一個已經存在的資料夾", - "openFolderDesc": "讀取並寫入到現有的 AppFlowy 資料夾", + "openFolderDesc": "讀取並寫入到現有的 @:appName 資料夾", "folderHintText": "資料夾名稱", "location": "建立新資料夾", - "locationDesc": "為您的 AppFlowy 資料夾選擇一個名稱", + "locationDesc": "為您的 @:appName 資料夾選擇一個名稱", "browser": "瀏覽", "create": "建立", "set": "設定", @@ -528,7 +528,7 @@ "change": "更改", "openLocationTooltips": "開啟另一個資料目錄", "openCurrentDataFolder": "開啟目前資料目錄", - "recoverLocationTooltips": "重設為 AppFlowy 的預設資料目錄", + "recoverLocationTooltips": "重設為 @:appName 的預設資料目錄", "exportFileSuccess": "匯出檔案成功!", "exportFileFail": "匯出檔案失敗!", "export": "匯出", @@ -1105,7 +1105,7 @@ "quickJumpYear": "跳到" }, "errorDialog": { - "title": "AppFlowy 錯誤", + "title": "@:appName 錯誤", "howToFixFallback": "對於給您帶來的不便,我們深表歉意!在我們的 GitHub 頁面上提交描述您的錯誤的問題。", "github": "在 GitHub 上檢視" }, @@ -1469,7 +1469,7 @@ "none": "無", "photoPermissionDescription": "允許存取圖片庫以上傳圖片", "openSettings": "打開設定", - "photoPermissionTitle": "AppFlowy 希望存取您的圖片庫", + "photoPermissionTitle": "@:appName 希望存取您的圖片庫", "doNotAllow": "不允許" }, "commandPalette": { diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index e697105f42..581f1fd642 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -753,7 +753,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "async-trait", @@ -777,7 +777,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "async-trait", @@ -807,7 +807,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "collab", @@ -826,7 +826,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "bytes", @@ -841,7 +841,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "chrono", @@ -879,7 +879,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "async-stream", @@ -960,7 +960,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "collab", @@ -1232,6 +1232,7 @@ dependencies = [ "lib-log", "parking_lot 0.12.1", "protobuf", + "semver", "serde", "serde_json", "serde_repr", @@ -1523,6 +1524,7 @@ dependencies = [ "parking_lot 0.12.1", "protobuf", "rand 0.8.5", + "semver", "serde", "serde_json", "strum", @@ -2050,6 +2052,7 @@ dependencies = [ "postgrest", "rand 0.8.5", "reqwest", + "semver", "serde", "serde_json", "thiserror", @@ -4815,9 +4818,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 1964f0460a..1c51d68a70 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -90,7 +90,7 @@ yrs = "0.18.8" # Run the script.add_workspace_members: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "6cc46e3779f7174284a0b52cbc87fb1cf98b0b9f" } +client-api = { version = "0.2" } appflowy-cloud-billing-client = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud-Billing-Client", rev = "1d5297d2cfc8813b7f6a97cc74cb4ebef78c4b30" } [profile.dev] @@ -118,6 +118,8 @@ lto = false incremental = false [patch.crates-io] +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "6cc46e3779f7174284a0b52cbc87fb1cf98b0b9f" } + # TODO(Lucas.Xu) Upgrade to the latest version of RocksDB once PR(https://github.com/rust-rocksdb/rust-rocksdb/pull/869) is merged. # Currently, using the following revision id. This commit is patched to fix the 32-bit build issue and it's checked out from 0.21.0, not 0.22.0. rocksdb = { git = "https://github.com/LucasXu0/rust-rocksdb", rev = "21cf4a23ec131b9d82dc94e178fe8efc0c147b09" } @@ -130,10 +132,10 @@ rocksdb = { git = "https://github.com/LucasXu0/rust-rocksdb", rev = "21cf4a23ec1 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } diff --git a/frontend/rust-lib/dart-ffi/Cargo.toml b/frontend/rust-lib/dart-ffi/Cargo.toml index 4e5148da95..e17ede23e5 100644 --- a/frontend/rust-lib/dart-ffi/Cargo.toml +++ b/frontend/rust-lib/dart-ffi/Cargo.toml @@ -25,6 +25,7 @@ lazy_static = "1.4.0" parking_lot.workspace = true tracing.workspace = true lib-log.workspace = true +semver = "1.0.22" # workspace lib-dispatch = { workspace = true } diff --git a/frontend/rust-lib/dart-ffi/src/lib.rs b/frontend/rust-lib/dart-ffi/src/lib.rs index 48f3e485a2..a7f3e2ee19 100644 --- a/frontend/rust-lib/dart-ffi/src/lib.rs +++ b/frontend/rust-lib/dart-ffi/src/lib.rs @@ -1,11 +1,11 @@ #![allow(clippy::not_unsafe_ptr_arg_deref)] use allo_isolate::Isolate; -use std::sync::Arc; -use std::{ffi::CStr, os::raw::c_char}; - use lazy_static::lazy_static; use parking_lot::Mutex; +use semver::Version; +use std::sync::Arc; +use std::{ffi::CStr, os::raw::c_char}; use tracing::{debug, error, info, trace, warn}; use flowy_core::config::AppFlowyCoreConfig; @@ -67,8 +67,16 @@ pub extern "C" fn init_sdk(_port: i64, data: *mut c_char) -> i64 { let _ = save_appflowy_cloud_config(&configuration.root, &configuration.appflowy_cloud_config); } + let mut app_version = + Version::parse(&configuration.app_version).unwrap_or_else(|_| Version::new(0, 5, 8)); + + let min_version = Version::new(0, 5, 8); + if app_version < min_version { + app_version = min_version; + } + let config = AppFlowyCoreConfig::new( - configuration.app_version, + app_version, configuration.custom_app_path, configuration.origin_app_path, configuration.device_id, diff --git a/frontend/rust-lib/event-integration-test/Cargo.toml b/frontend/rust-lib/event-integration-test/Cargo.toml index 26d561e993..d427a8f16c 100644 --- a/frontend/rust-lib/event-integration-test/Cargo.toml +++ b/frontend/rust-lib/event-integration-test/Cargo.toml @@ -24,6 +24,7 @@ flowy-notification = { workspace = true } anyhow.workspace = true flowy-storage = { workspace = true } flowy-search = { workspace = true } +semver = "1.0.23" serde.workspace = true serde_json.workspace = true diff --git a/frontend/rust-lib/event-integration-test/src/folder_event.rs b/frontend/rust-lib/event-integration-test/src/folder_event.rs index 280e91e008..194d15a54c 100644 --- a/frontend/rust-lib/event-integration-test/src/folder_event.rs +++ b/frontend/rust-lib/event-integration-test/src/folder_event.rs @@ -139,6 +139,24 @@ impl EventIntegrationTest { } } + /// Create orphan views in the folder. + /// Orphan view: the parent_view_id equal to the view_id + /// Normally, the orphan view will be created in nested database + pub async fn create_orphan_view(&self, name: &str, view_id: &str, layout: ViewLayoutPB) { + let payload = CreateOrphanViewPayloadPB { + name: name.to_string(), + desc: "".to_string(), + layout, + view_id: view_id.to_string(), + initial_data: vec![], + }; + EventBuilder::new(self.clone()) + .event(FolderEvent::CreateOrphanView) + .payload(payload) + .async_send() + .await; + } + pub fn get_folder_data(&self) -> FolderData { let mutex_folder = self.appflowy_core.folder_manager.get_mutex_folder().clone(); let folder_lock_guard = mutex_folder.read(); @@ -240,6 +258,18 @@ impl EventIntegrationTest { .await .parse::<ViewPB>() } + + pub async fn get_view_ancestors(&self, view_id: &str) -> Vec<ViewPB> { + EventBuilder::new(self.clone()) + .event(FolderEvent::GetViewAncestors) + .payload(ViewIdPB { + value: view_id.to_string(), + }) + .async_send() + .await + .parse::<RepeatedViewPB>() + .items + } } pub struct ViewTest { diff --git a/frontend/rust-lib/event-integration-test/src/lib.rs b/frontend/rust-lib/event-integration-test/src/lib.rs index 180623d8d5..cddd304dde 100644 --- a/frontend/rust-lib/event-integration-test/src/lib.rs +++ b/frontend/rust-lib/event-integration-test/src/lib.rs @@ -10,6 +10,7 @@ use std::time::Duration; use nanoid::nanoid; use parking_lot::RwLock; +use semver::Version; use tokio::select; use tokio::time::sleep; @@ -55,7 +56,7 @@ impl EventIntegrationTest { let device_id = uuid::Uuid::new_v4().to_string(); let config = AppFlowyCoreConfig::new( - "".to_string(), + Version::new(0, 5, 8), path.clone(), path, device_id, diff --git a/frontend/rust-lib/event-integration-test/tests/folder/local_test/folder_test.rs b/frontend/rust-lib/event-integration-test/tests/folder/local_test/folder_test.rs index 7523f5ab0f..d0a4a28429 100644 --- a/frontend/rust-lib/event-integration-test/tests/folder/local_test/folder_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/folder/local_test/folder_test.rs @@ -1,6 +1,8 @@ use collab_folder::ViewLayout; +use event_integration_test::EventIntegrationTest; use flowy_folder::entities::icon::{ViewIconPB, ViewIconTypePB}; +use flowy_folder::entities::ViewLayoutPB; use crate::folder::local_test::script::FolderScript::*; use crate::folder::local_test::script::FolderTest; @@ -331,3 +333,17 @@ async fn move_view_event_test() { assert_eq!(after_view_ids[0], view_ids[1]); assert_eq!(after_view_ids[1], view_ids[0]); } + +#[tokio::test] +async fn create_orphan_child_view_and_get_its_ancestors_test() { + let test = EventIntegrationTest::new_anon().await; + let name = "Orphan View"; + let view_id = "20240521"; + test + .create_orphan_view(name, view_id, ViewLayoutPB::Grid) + .await; + let ancestors = test.get_view_ancestors(view_id).await; + assert_eq!(ancestors.len(), 1); + assert_eq!(ancestors[0].name, "Orphan View"); + assert_eq!(ancestors[0].id, view_id); +} 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 eb311cfea7..0a2f34ca0a 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<String>) .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::<RepeatedViewPB>() + .parse::<RepeatedFavoriteViewPB>() } diff --git a/frontend/rust-lib/flowy-core/src/config.rs b/frontend/rust-lib/flowy-core/src/config.rs index 53919ad9b0..fd8fbe335f 100644 --- a/frontend/rust-lib/flowy-core/src/config.rs +++ b/frontend/rust-lib/flowy-core/src/config.rs @@ -2,6 +2,7 @@ use std::fmt; use std::path::Path; use base64::Engine; +use semver::Version; use tracing::{error, info}; use flowy_server_pub::af_cloud_config::AFCloudConfiguration; @@ -15,7 +16,7 @@ use crate::integrate::log::create_log_filter; #[derive(Clone)] pub struct AppFlowyCoreConfig { /// Different `AppFlowyCoreConfig` instance should have different name - pub(crate) app_version: String, + pub(crate) app_version: Version, pub name: String, pub(crate) device_id: String, pub platform: String, @@ -75,7 +76,7 @@ fn make_user_data_folder(root: &str, url: &str) -> String { impl AppFlowyCoreConfig { pub fn new( - app_version: String, + app_version: Version, custom_application_path: String, application_path: String, device_id: String, diff --git a/frontend/rust-lib/flowy-core/src/integrate/server.rs b/frontend/rust-lib/flowy-core/src/integrate/server.rs index b0c0acbdcf..2959ae7f21 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/server.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/server.rs @@ -133,7 +133,7 @@ impl ServerProvider { config, *self.user_enable_sync.read(), self.config.device_id.clone(), - &self.config.app_version, + self.config.app_version.clone(), self.user.clone(), )); diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index 36addf0fe7..2ba29b83fd 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -3,7 +3,6 @@ use flowy_search::folder::indexer::FolderIndexManagerImpl; use flowy_search::services::manager::SearchManager; use flowy_storage::ObjectStorageService; -use semver::Version; use std::sync::{Arc, Weak}; use std::time::Duration; use sysinfo::System; @@ -106,13 +105,12 @@ impl AppFlowyCore { let task_dispatcher = Arc::new(RwLock::new(task_scheduler)); runtime.spawn(TaskRunner::run(task_dispatcher.clone())); - let app_version = Version::parse(&config.app_version).unwrap_or_else(|_| Version::new(0, 5, 4)); let user_config = UserConfig::new( &config.name, &config.storage_path, &config.application_path, &config.device_id, - app_version, + config.app_version.clone(), ); let authenticate_user = Arc::new(AuthenticateUser::new( diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index 21858ad201..283ad989c1 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -347,6 +347,12 @@ impl DatabaseManager { }) .await .map_err(internal_error)??; + + // Currently, we only support importing up to 500 rows. We can support more rows in the future. + if !cfg!(debug_assertions) && params.rows.len() > 500 { + return Err(FlowyError::internal().with_context("The number of rows exceeds the limit")); + } + let result = ImportResult { database_id: params.database_id.clone(), view_id: params.inline_view_id.clone(), @@ -424,15 +430,19 @@ impl DatabaseManager { if let Some(row) = database.get_row(&view_id, &row_id) { let fields = database.get_fields(&view_id, None); for field in fields { - if let Some(cell) = row.cells.get(&field.id) { - summary_row_content.insert(field.name.clone(), stringify_cell(cell, &field)); + // When summarizing a row, skip the content in the "AI summary" cell; it does not need to + // be summarized. + if field.id != field_id { + if let Some(cell) = row.cells.get(&field.id) { + summary_row_content.insert(field.name.clone(), stringify_cell(cell, &field)); + } } } } // Call the cloud service to summarize the row. trace!( - "[AI]: summarize row:{}, content:{:?}", + "[AI]:summarize row:{}, content:{:?}", row_id, summary_row_content ); diff --git a/frontend/rust-lib/flowy-folder/src/entities/view.rs b/frontend/rust-lib/flowy-folder/src/entities/view.rs index 004f793e11..466d30d06d 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<ViewPB>, } +#[derive(Eq, PartialEq, Debug, Default, ProtoBuf, Clone)] +pub struct RepeatedFavoriteViewPB { + #[pb(index = 1)] + pub items: Vec<FavoriteViewPB>, +} + +#[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<Vec<ViewPB>> for RepeatedViewPB { fn from(items: Vec<ViewPB>) -> 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 d6d36b683e..06daab5a2f 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<Weak<FolderManager>>, -) -> DataResult<RepeatedViewPB, FlowyError> { +) -> DataResult<RepeatedFavoriteViewPB, FlowyError> { 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 31034bd143..2901d19a63 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/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index 2433a181a5..35d2ffc72b 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -530,6 +530,10 @@ impl FolderManager { while let Some(view) = self.with_folder(|| None, |folder| folder.views.get_view(&parent_view_id)) { + // If the view is already in the ancestors list, then break the loop + if ancestors.iter().any(|v: &ViewPB| v.id == view.id) { + break; + } ancestors.push(view_pb_without_child_views(view.as_ref().clone())); parent_view_id = view.parent_view_id.clone(); } diff --git a/frontend/rust-lib/flowy-folder/src/notification.rs b/frontend/rust-lib/flowy-folder/src/notification.rs index c57450a5d6..1ddcebcafd 100644 --- a/frontend/rust-lib/flowy-folder/src/notification.rs +++ b/frontend/rust-lib/flowy-folder/src/notification.rs @@ -12,7 +12,7 @@ pub enum FolderNotification { Unknown = 0, /// Trigger after creating a workspace DidCreateWorkspace = 1, - // /// Trigger after updating a workspace + /// Trigger after updating a workspace DidUpdateWorkspace = 2, DidUpdateWorkspaceViews = 3, diff --git a/frontend/rust-lib/flowy-server/Cargo.toml b/frontend/rust-lib/flowy-server/Cargo.toml index 86297f99fd..b71776b02b 100644 --- a/frontend/rust-lib/flowy-server/Cargo.toml +++ b/frontend/rust-lib/flowy-server/Cargo.toml @@ -49,6 +49,7 @@ tokio-stream = { workspace = true, features = ["sync"] } lib-dispatch = { workspace = true } yrs.workspace = true rand = "0.8.5" +semver = "1.0.23" [dependencies.client-api] workspace = true diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs index 1fd6a5b03f..c758be253a 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs @@ -12,6 +12,7 @@ use client_api::ws::{ use client_api::{Client, ClientConfiguration}; use flowy_storage::ObjectStorageService; use rand::Rng; +use semver::Version; use tokio::select; use tokio::sync::{watch, Mutex}; use tokio_stream::wrappers::WatchStream; @@ -53,7 +54,7 @@ impl AppFlowyCloudServer { config: AFCloudConfiguration, enable_sync: bool, mut device_id: String, - client_version: &str, + client_version: Version, user: Arc<dyn ServerUser>, ) -> Self { // The device id can't be empty, so we generate a new one if it is. @@ -70,7 +71,7 @@ impl AppFlowyCloudServer { ClientConfiguration::default() .with_compression_buffer_size(10240) .with_compression_quality(8), - client_version, + &client_version.to_string(), ); let token_state_rx = api_client.subscribe_token_state(); let enable_sync = Arc::new(AtomicBool::new(enable_sync)); diff --git a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs index 224e10cd95..71dacfab04 100644 --- a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs +++ b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs @@ -1,4 +1,5 @@ use client_api::ClientConfiguration; +use semver::Version; use std::collections::HashMap; use std::sync::Arc; @@ -32,7 +33,7 @@ pub fn af_cloud_server(config: AFCloudConfiguration) -> Arc<AppFlowyCloudServer> config, true, fake_device_id, - "0.5.1", + Version::new(0, 5, 8), Arc::new(FakeServerUserImpl), )) } diff --git a/frontend/scripts/docker-buildfiles/Dockerfile b/frontend/scripts/docker-buildfiles/Dockerfile index 0624ec053b..8aad410e94 100644 --- a/frontend/scripts/docker-buildfiles/Dockerfile +++ b/frontend/scripts/docker-buildfiles/Dockerfile @@ -39,7 +39,7 @@ RUN source ~/.cargo/env && \ RUN sudo pacman -S --noconfirm git tar gtk3 RUN curl -sSfL \ --output flutter.tar.xz \ - https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.19.0-stable.tar.xz && \ + https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.22.0-stable.tar.xz && \ tar -xf flutter.tar.xz && \ rm flutter.tar.xz RUN flutter config --enable-linux-desktop diff --git a/frontend/scripts/install_dev_env/install_ios.sh b/frontend/scripts/install_dev_env/install_ios.sh index 0ce5cfb5d4..653eb8f1b3 100644 --- a/frontend/scripts/install_dev_env/install_ios.sh +++ b/frontend/scripts/install_dev_env/install_ios.sh @@ -44,9 +44,9 @@ printMessage "Setting up Flutter" # Get the current Flutter version FLUTTER_VERSION=$(flutter --version | grep -oE 'Flutter [^ ]+' | grep -oE '[^ ]+$') -# Check if the current version is 3.19.0 -if [ "$FLUTTER_VERSION" = "3.19.0" ]; then - echo "Flutter version is already 3.19.0" +# Check if the current version is 3.22.0 +if [ "$FLUTTER_VERSION" = "3.22.0" ]; then + echo "Flutter version is already 3.22.0" else # Get the path to the Flutter SDK FLUTTER_PATH=$(which flutter) @@ -55,12 +55,12 @@ else current_dir=$(pwd) cd $FLUTTER_PATH - # Use git to checkout version 3.19.0 of Flutter - git checkout 3.19.0 + # Use git to checkout version 3.22.0 of Flutter + git checkout 3.22.0 # Get back to current working directory cd "$current_dir" - echo "Switched to Flutter version 3.19.0" + echo "Switched to Flutter version 3.22.0" fi # Enable linux desktop diff --git a/frontend/scripts/install_dev_env/install_linux.sh b/frontend/scripts/install_dev_env/install_linux.sh index d1f85445a2..22b491321b 100755 --- a/frontend/scripts/install_dev_env/install_linux.sh +++ b/frontend/scripts/install_dev_env/install_linux.sh @@ -38,9 +38,9 @@ fi printMessage "Setting up Flutter" # Get the current Flutter version FLUTTER_VERSION=$(flutter --version | grep -oP 'Flutter \K\S+') -# Check if the current version is 3.19.0 -if [ "$FLUTTER_VERSION" = "3.19.0" ]; then - echo "Flutter version is already 3.19.0" +# Check if the current version is 3.22.0 +if [ "$FLUTTER_VERSION" = "3.22.0" ]; then + echo "Flutter version is already 3.22.0" else # Get the path to the Flutter SDK FLUTTER_PATH=$(which flutter) @@ -49,12 +49,12 @@ else current_dir=$(pwd) cd $FLUTTER_PATH - # Use git to checkout version 3.19.0 of Flutter - git checkout 3.19.0 + # Use git to checkout version 3.22.0 of Flutter + git checkout 3.22.0 # Get back to current working directory cd "$current_dir" - echo "Switched to Flutter version 3.19.0" + echo "Switched to Flutter version 3.22.0" fi # Enable linux desktop diff --git a/frontend/scripts/install_dev_env/install_macos.sh b/frontend/scripts/install_dev_env/install_macos.sh index 10f894e13c..8613b904c6 100755 --- a/frontend/scripts/install_dev_env/install_macos.sh +++ b/frontend/scripts/install_dev_env/install_macos.sh @@ -41,9 +41,9 @@ printMessage "Setting up Flutter" # Get the current Flutter version FLUTTER_VERSION=$(flutter --version | grep -oE 'Flutter [^ ]+' | grep -oE '[^ ]+$') -# Check if the current version is 3.19.0 -if [ "$FLUTTER_VERSION" = "3.19.0" ]; then - echo "Flutter version is already 3.19.0" +# Check if the current version is 3.22.0 +if [ "$FLUTTER_VERSION" = "3.22.0" ]; then + echo "Flutter version is already 3.22.0" else # Get the path to the Flutter SDK FLUTTER_PATH=$(which flutter) @@ -52,12 +52,12 @@ else current_dir=$(pwd) cd $FLUTTER_PATH - # Use git to checkout version 3.19.0 of Flutter - git checkout 3.19.0 + # Use git to checkout version 3.22.0 of Flutter + git checkout 3.22.0 # Get back to current working directory cd "$current_dir" - echo "Switched to Flutter version 3.19.0" + echo "Switched to Flutter version 3.22.0" fi # Enable linux desktop diff --git a/frontend/scripts/install_dev_env/install_windows.sh b/frontend/scripts/install_dev_env/install_windows.sh index aef80844a0..1d68a677ae 100644 --- a/frontend/scripts/install_dev_env/install_windows.sh +++ b/frontend/scripts/install_dev_env/install_windows.sh @@ -48,9 +48,9 @@ fi printMessage "Setting up Flutter" # Get the current Flutter version FLUTTER_VERSION=$(flutter --version | grep -oP 'Flutter \K\S+') -# Check if the current version is 3.19.0 -if [ "$FLUTTER_VERSION" = "3.19.0" ]; then - echo "Flutter version is already 3.19.0" +# Check if the current version is 3.22.0 +if [ "$FLUTTER_VERSION" = "3.22.0" ]; then + echo "Flutter version is already 3.22.0" else # Get the path to the Flutter SDK FLUTTER_PATH=$(which flutter) @@ -59,12 +59,12 @@ else current_dir=$(pwd) cd $FLUTTER_PATH - # Use git to checkout version 3.19.0 of Flutter - git checkout 3.19.0 + # Use git to checkout version 3.22.0 of Flutter + git checkout 3.22.0 # Get back to current working directory cd "$current_dir" - echo "Switched to Flutter version 3.19.0" + echo "Switched to Flutter version 3.22.0" fi # Add pub cache and cargo to PATH diff --git a/frontend/scripts/tool/update_collab_source.sh b/frontend/scripts/tool/update_collab_source.sh index 29892de1de..094e5caf14 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 diff --git a/project.inlang/settings.json b/project.inlang/settings.json index c49b392792..2d09236b50 100644 --- a/project.inlang/settings.json +++ b/project.inlang/settings.json @@ -24,6 +24,7 @@ "ru-RU", "sv-SE", "tr-TR", + "uk-UA", "vi", "vi-VN", "zh-CN",