diff --git a/.github/workflows/android_ci.yaml.bak b/.github/workflows/android_ci.yaml.bak index 8732558927d28..0cb110bae436a 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 6ec1c766822eb..3e64bcd83f304 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" @@ -172,7 +172,7 @@ jobs: sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list sudo apt-get update - sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev + sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev libmpv-dev mpv fi shell: bash @@ -272,7 +272,7 @@ jobs: sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list sudo apt-get update - sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev + sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev libmpv-dev mpv shell: bash - name: Enable Flutter Desktop @@ -319,6 +319,12 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 + - name: Install video dependency + run: | + sudo apt-get update + sudo apt-get -y install libmpv-dev mpv + shell: bash + - name: Flutter Integration Test 1 uses: ./.github/actions/flutter_integration_test with: @@ -343,6 +349,12 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 + - name: Install video dependency + run: | + sudo apt-get update + sudo apt-get -y install libmpv-dev mpv + shell: bash + - name: Flutter Integration Test 2 uses: ./.github/actions/flutter_integration_test with: @@ -367,6 +379,12 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 + - name: Install video dependency + run: | + sudo apt-get update + sudo apt-get -y install libmpv-dev mpv + shell: bash + - name: Flutter Integration Test 3 uses: ./.github/actions/flutter_integration_test with: diff --git a/.github/workflows/ios_ci.yaml b/.github/workflows/ios_ci.yaml index c32a7f93c77f1..d24eaed1f3b10 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 ee065c6e9e933..e91d95f969ff1 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 4d8e9cbad87d9..12e728698fc8d 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 e612d832f79d2..0cc8d05b68553 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -105,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 5a962f3cbf590..a71ffec1fcf34 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.8" +APPFLOWY_VERSION = "0.5.9" FLUTTER_DESKTOP_FEATURES = "dart" PRODUCT_NAME = "AppFlowy" MACOSX_DEPLOYMENT_TARGET = "11.0" diff --git a/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml b/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml index 351994354de95..279b17320c5fe 100644 --- a/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml +++ b/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml @@ -58,4 +58,11 @@ + + + \ No newline at end of file diff --git a/frontend/appflowy_flutter/integration_test/cloud/workspace/collaborative_workspace_test.dart b/frontend/appflowy_flutter/integration_test/cloud/workspace/collaborative_workspace_test.dart index 11af75af1d17e..9886c2228e315 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/workspace/collaborative_workspace_test.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/workspace/collaborative_workspace_test.dart @@ -2,8 +2,6 @@ import 'dart:io'; -import 'package:flutter/material.dart'; - import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart'; @@ -12,14 +10,15 @@ import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'; import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/uuid.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:path/path.dart' as p; diff --git a/frontend/appflowy_flutter/integration_test/desktop/board/board_test_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/board/board_test_runner.dart index 932c266bda0ac..a786367fff2cc 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/board/board_test_runner.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/board/board_test_runner.dart @@ -1,10 +1,10 @@ import 'package:integration_test/integration_test.dart'; -import 'board_row_test.dart' as board_row_test; import 'board_add_row_test.dart' as board_add_row_test; import 'board_group_test.dart' as board_group_test; +import 'board_row_test.dart' as board_row_test; -void startTesting() { +void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); // Board integration tests diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_alignment_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_alignment_test.dart index de7401652e170..d95d907881e92 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 0bbd64c82b836..f1699108405cf 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 0000000000000..d4cc11d7f0518 --- /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 42462c26585a3..239e7e09a8e23 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 2b42ef74513ad..f7a88d7bec54b 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_cover_image_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_cover_image_test.dart @@ -130,24 +130,24 @@ void main() { final searchEmojiTextField = find.byWidgetPredicate( (widget) => widget is TextField && - widget.decoration!.hintText == LocaleKeys.emoji_search.tr(), + widget.decoration!.hintText == LocaleKeys.search_label.tr(), ); await tester.enterText( searchEmojiTextField, - 'hand', + 'punch', ); // change skin tone await tester.editor.changeEmojiSkinTone(EmojiSkinTone.dark); // select an icon with skin tone - const hand = '👋🏿'; - await tester.tapEmoji(hand); - tester.expectToSeeDocumentIcon(hand); + const punch = '👊🏿'; + await tester.tapEmoji(punch); + tester.expectToSeeDocumentIcon(punch); tester.expectViewHasIcon( gettingStarted, ViewLayoutPB.Document, - hand, + punch, ); }); }); diff --git a/frontend/appflowy_flutter/integration_test/desktop/settings/notifications_settings_test.dart b/frontend/appflowy_flutter/integration_test/desktop/settings/notifications_settings_test.dart index 46550aa81a4f4..958910dd80e58 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/settings/notifications_settings_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/settings/notifications_settings_test.dart @@ -8,8 +8,8 @@ import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('board add row test', () { - testWidgets('Add card from header', (tester) async { + group('notification test', () { + testWidgets('enable notification', (tester) async { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); @@ -17,7 +17,7 @@ void main() { await tester.openSettingsPage(SettingsPage.notifications); await tester.pumpAndSettle(); - final switchFinder = find.byType(Switch); + final switchFinder = find.byType(Switch).first; // Defaults to enabled Switch switchWidget = tester.widget(switchFinder); diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart index ff0df1c7dac1a..f2a1fae8ae9f2 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_folder.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -12,8 +12,8 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('sidebar expand test', () { - bool isExpanded({required FolderCategoryType type}) { - if (type == FolderCategoryType.private) { + bool isExpanded({required FolderSpaceType type}) { + if (type == FolderSpaceType.private) { return find .descendant( of: find.byType(PrivateSectionFolder), @@ -30,19 +30,19 @@ void main() { await tester.tapAnonymousSignInButton(); // first time is expanded - expect(isExpanded(type: FolderCategoryType.private), true); + expect(isExpanded(type: FolderSpaceType.private), true); // collapse the personal folder await tester.tapButton( find.byTooltip(LocaleKeys.sideBar_clickToHidePrivate.tr()), ); - expect(isExpanded(type: FolderCategoryType.private), false); + expect(isExpanded(type: FolderSpaceType.private), false); // expand the personal folder await tester.tapButton( find.byTooltip(LocaleKeys.sideBar_clickToHidePrivate.tr()), ); - expect(isExpanded(type: FolderCategoryType.private), true); + expect(isExpanded(type: FolderSpaceType.private), true); }); }); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart index 072764217c862..729ee62a3ed40 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart @@ -1,5 +1,5 @@ import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_favorite_folder.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; @@ -46,7 +46,7 @@ void main() { await tester.favoriteViewByName(names[1]); expect( tester.findFavoritePageName(names[1]), - findsNWidgets(2), + findsNWidgets(1), ); await tester.unfavoriteViewByName(gettingStarted); @@ -120,9 +120,9 @@ void main() { (widget) => widget is SingleInnerViewItem && widget.view.isFavorite && - widget.categoryType == FolderCategoryType.favorite, + widget.spaceType == FolderSpaceType.favorite, ), - findsNWidgets(6), + findsNWidgets(3), ); await tester.hoverOnPageName( @@ -135,7 +135,7 @@ void main() { expect( tester.findAllFavoritePages(), - findsNWidgets(3), + findsNWidgets(2), ); await tester.hoverOnPageName( @@ -168,7 +168,7 @@ void main() { widget.isSelected != null && widget.isSelected!(), ), - findsNWidgets(2), + findsNWidgets(1), ); }, ); diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart index 35bcf599ab735..3bc41d78c0d55 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart @@ -4,7 +4,7 @@ import 'sidebar_favorites_test.dart' as sidebar_favorite_test; import 'sidebar_icon_test.dart' as sidebar_icon_test; import 'sidebar_test.dart' as sidebar_test; -void startTesting() { +void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); // Sidebar integration tests diff --git a/frontend/appflowy_flutter/integration_test/desktop_runner_3.dart b/frontend/appflowy_flutter/integration_test/desktop_runner_3.dart index 72bf8a4faef65..f1025b8f1e92d 100644 --- a/frontend/appflowy_flutter/integration_test/desktop_runner_3.dart +++ b/frontend/appflowy_flutter/integration_test/desktop_runner_3.dart @@ -27,7 +27,7 @@ Future runIntegration3OnDesktop() async { settings_test_runner.main(); share_markdown_test.main(); import_files_test.main(); - sidebar_test_runner.startTesting(); - board_test_runner.startTesting(); + sidebar_test_runner.main(); + board_test_runner.main(); tabs_test.main(); } diff --git a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart index 08d6fd0ec6217..8bc72369943a8 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/editor_test_operations.dart b/frontend/appflowy_flutter/integration_test/shared/editor_test_operations.dart index 0bdcf06367bbe..4eff62321a4b7 100644 --- a/frontend/appflowy_flutter/integration_test/shared/editor_test_operations.dart +++ b/frontend/appflowy_flutter/integration_test/shared/editor_test_operations.dart @@ -81,15 +81,12 @@ class EditorOperations { /// Taps the 'Remove Icon' button in the cover toolbar and the icon popover Future tapRemoveIconButton({bool isInPicker = false}) async { - Finder button = - find.text(LocaleKeys.document_plugins_cover_removeIcon.tr()); - if (isInPicker) { - button = find.descendant( - of: find.byType(FlowyIconPicker), - matching: button, - ); - } - + final Finder button = !isInPicker + ? find.text(LocaleKeys.document_plugins_cover_removeIcon.tr()) + : find.descendant( + of: find.byType(FlowyIconPicker), + matching: find.text(LocaleKeys.button_remove.tr()), + ); await tester.tapButton(button); } diff --git a/frontend/appflowy_flutter/integration_test/shared/expectation.dart b/frontend/appflowy_flutter/integration_test/shared/expectation.dart index aeb3f04cb8873..c4b54a0fda5de 100644 --- a/frontend/appflowy_flutter/integration_test/shared/expectation.dart +++ b/frontend/appflowy_flutter/integration_test/shared/expectation.dart @@ -165,7 +165,7 @@ extension Expectation on WidgetTester { (widget) => widget is SingleInnerViewItem && widget.view.isFavorite && - widget.categoryType == FolderCategoryType.favorite && + widget.spaceType == FolderSpaceType.favorite && widget.view.name == name && widget.view.layout == layout, skipOffstage: false, @@ -175,7 +175,7 @@ extension Expectation on WidgetTester { (widget) => widget is SingleInnerViewItem && widget.view.isFavorite && - widget.categoryType == FolderCategoryType.favorite, + widget.spaceType == FolderSpaceType.favorite, ); Finder findPageName( diff --git a/frontend/appflowy_flutter/integration_test/shared/keyboard.dart b/frontend/appflowy_flutter/integration_test/shared/keyboard.dart index d792b92c662b2..567e7e548cf7f 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 dd13bd088f750..3b25c32111341 100644 --- a/frontend/appflowy_flutter/integration_test/shared/settings.dart +++ b/frontend/appflowy_flutter/integration_test/shared/settings.dart @@ -1,7 +1,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart'; import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart'; @@ -12,7 +12,6 @@ import 'package:flowy_infra_ui/style_widget/text_field.dart'; import 'package:flutter_test/flutter_test.dart'; import '../desktop/board/board_hide_groups_test.dart'; - import 'base.dart'; import 'common_operations.dart'; diff --git a/frontend/appflowy_flutter/integration_test/shared/workspace.dart b/frontend/appflowy_flutter/integration_test/shared/workspace.dart index 5137944364957..4d20d88ce193f 100644 --- a/frontend/appflowy_flutter/integration_test/shared/workspace.dart +++ b/frontend/appflowy_flutter/integration_test/shared/workspace.dart @@ -1,14 +1,14 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/base/icon/icon_picker.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'base.dart'; +import 'util.dart'; extension AppFlowyWorkspace on WidgetTester { /// Open workspace menu @@ -36,12 +36,19 @@ extension AppFlowyWorkspace on WidgetTester { matching: find.byType(WorkspaceMoreActionList), ); expect(moreButton, findsOneWidget); - await tapButton(moreButton); - await tapButton(find.findTextInFlowyText(LocaleKeys.button_rename.tr())); - final input = find.byType(TextFormField); - expect(input, findsOneWidget); - await enterText(input, name); - await tapButton(find.text(LocaleKeys.button_ok.tr())); + await hoverOnWidget( + moreButton, + onHover: () async { + await tapButton(moreButton); + await tapButton( + find.findTextInFlowyText(LocaleKeys.button_rename.tr()), + ); + final input = find.byType(TextFormField); + expect(input, findsOneWidget); + await enterText(input, name); + await tapButton(find.text(LocaleKeys.button_ok.tr())); + }, + ); } Future changeWorkspaceIcon(String icon) async { diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index e62299792dc3a..5a2d069c36e5b 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -58,6 +58,12 @@ PODS: - Flutter - keyboard_height_plugin (0.0.1): - Flutter + - media_kit_libs_ios_video (1.0.4): + - Flutter + - media_kit_native_event_loop (1.0.0): + - Flutter + - media_kit_video (0.0.1): + - Flutter - package_info_plus (0.4.5): - Flutter - path_provider_foundation (0.0.1): @@ -66,6 +72,8 @@ PODS: - permission_handler_apple (9.3.0): - Flutter - ReachabilitySwift (5.0.0) + - screen_brightness_ios (0.1.0): + - Flutter - SDWebImage (5.14.2): - SDWebImage/Core (= 5.14.2) - SDWebImage/Core (5.14.2) @@ -83,6 +91,10 @@ PODS: - Toast (4.0.0) - url_launcher_ios (0.0.1): - Flutter + - volume_controller (0.0.1): + - Flutter + - wakelock_plus (0.0.1): + - Flutter DEPENDENCIES: - app_links (from `.symlinks/plugins/app_links/ios`) @@ -98,14 +110,20 @@ DEPENDENCIES: - integration_test (from `.symlinks/plugins/integration_test/ios`) - irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`) - keyboard_height_plugin (from `.symlinks/plugins/keyboard_height_plugin/ios`) + - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) + - media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`) + - media_kit_video (from `.symlinks/plugins/media_kit_video/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite (from `.symlinks/plugins/sqflite/darwin`) - super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - volume_controller (from `.symlinks/plugins/volume_controller/ios`) + - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) SPEC REPOS: trunk: @@ -143,12 +161,20 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/irondash_engine_context/ios" keyboard_height_plugin: :path: ".symlinks/plugins/keyboard_height_plugin/ios" + media_kit_libs_ios_video: + :path: ".symlinks/plugins/media_kit_libs_ios_video/ios" + media_kit_native_event_loop: + :path: ".symlinks/plugins/media_kit_native_event_loop/ios" + media_kit_video: + :path: ".symlinks/plugins/media_kit_video/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" + screen_brightness_ios: + :path: ".symlinks/plugins/screen_brightness_ios/ios" share_plus: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: @@ -159,6 +185,10 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/super_native_extensions/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" + volume_controller: + :path: ".symlinks/plugins/volume_controller/ios" + wakelock_plus: + :path: ".symlinks/plugins/wakelock_plus/ios" SPEC CHECKSUMS: app_links: 5ef33d0d295a89d9d16bb81b0e3b0d5f70d6c875 @@ -173,13 +203,17 @@ 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 + media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 + media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a + media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 + screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625 SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84 share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 @@ -188,6 +222,8 @@ SPEC CHECKSUMS: SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812 + volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9 + wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca diff --git a/frontend/appflowy_flutter/ios/Runner/Info.plist b/frontend/appflowy_flutter/ios/Runner/Info.plist index 8c605b9d3a026..5ec528b05e431 100644 --- a/frontend/appflowy_flutter/ios/Runner/Info.plist +++ b/frontend/appflowy_flutter/ios/Runner/Info.plist @@ -66,5 +66,10 @@ UIViewControllerBasedStatusBarAppearance + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + \ No newline at end of file diff --git a/frontend/appflowy_flutter/lib/core/config/kv_keys.dart b/frontend/appflowy_flutter/lib/core/config/kv_keys.dart index ff97b6124111e..00e79153e47ce 100644 --- a/frontend/appflowy_flutter/lib/core/config/kv_keys.dart +++ b/frontend/appflowy_flutter/lib/core/config/kv_keys.dart @@ -65,6 +65,11 @@ class KVKeys { /// {'feature_flag_1': true, 'feature_flag_2': false} static const String featureFlag = 'featureFlag'; + /// The key for saving show notification icon option + /// + /// The value is a boolean string + static const String showNotificationIcon = 'showNotificationIcon'; + /// The key for saving the last opened workspace id /// /// The workspace id is a string. @@ -75,4 +80,15 @@ class KVKeys { /// /// The value is a double string. static const String scaleFactor = 'scaleFactor'; + + /// The key for saving the last opened space + /// + /// The value is a int string. + static const String lastOpenedSpace = 'lastOpenedSpace'; + + /// The key for saving the space order + /// + /// The value is a json string with the following format: + /// [0, 1, 2] + static const String spaceOrder = 'spaceOrder'; } diff --git a/frontend/appflowy_flutter/lib/core/frameless_window.dart b/frontend/appflowy_flutter/lib/core/frameless_window.dart index 7877615a9878d..fcd955fc93879 100644 --- a/frontend/appflowy_flutter/lib/core/frameless_window.dart +++ b/frontend/appflowy_flutter/lib/core/frameless_window.dart @@ -3,12 +3,13 @@ import 'dart:io'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/window_title_bar.dart'; +import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class CocoaWindowChannel { @@ -102,13 +103,25 @@ class MoveWindowDetectorState extends State { return const SizedBox.shrink(); } + final color = Theme.of(context).isLightMode ? Colors.white : Colors.black; + final textSpan = TextSpan( + children: [ + TextSpan( + text: '${LocaleKeys.sideBar_openSidebar.tr()}\n', + style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: color), + ), + TextSpan( + text: Platform.isMacOS ? '⌘+.' : 'Ctrl+\\', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Theme.of(context).hintColor), + ), + ], + ); + return FlowyTooltip( - richMessage: TextSpan( - children: [ - TextSpan(text: '${LocaleKeys.sideBar_closeSidebar.tr()}\n'), - const TextSpan(text: 'Ctrl+\\'), - ], - ), + richMessage: textSpan, child: FlowyIconButton( hoverColor: Colors.transparent, onPressed: () => context diff --git a/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart b/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart index da9f4649c36ae..9d18bb14f7e97 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/main.dart b/frontend/appflowy_flutter/lib/main.dart index 9f140489c41ed..bee524b5742a4 100644 --- a/frontend/appflowy_flutter/lib/main.dart +++ b/frontend/appflowy_flutter/lib/main.dart @@ -1,9 +1,12 @@ import 'package:scaled_app/scaled_app.dart'; +import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; + import 'startup/startup.dart'; Future main() async { ScaledWidgetsFlutterBinding.ensureInitialized(scaleFactor: (_) => 1.0); + VideoBlockKit.ensureInitialized(); await runAppFlowy(); } diff --git a/frontend/appflowy_flutter/lib/mobile/application/recent/recent_view_bloc.dart b/frontend/appflowy_flutter/lib/mobile/application/recent/recent_view_bloc.dart index 410bc68c4e325..547c81f00b988 100644 --- a/frontend/appflowy_flutter/lib/mobile/application/recent/recent_view_bloc.dart +++ b/frontend/appflowy_flutter/lib/mobile/application/recent/recent_view_bloc.dart @@ -57,7 +57,19 @@ class RecentViewBloc extends Bloc { } }, ); + + // only document supports the cover + if (view.layout != ViewLayoutPB.Document) { + emit( + state.copyWith( + name: view.name, + icon: view.icon.value, + ), + ); + } + final cover = getCoverV2(); + if (cover != null) { emit( state.copyWith( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/type_option_menu_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/type_option_menu_item.dart index 04149c823814a..497f76935444d 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/type_option_menu_item.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/type_option_menu_item.dart @@ -1,5 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; class TypeOptionMenuItemValue { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/app_bar_buttons.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/app_bar_buttons.dart index d712aa5aecc98..1301719c41f30 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 b94242eceb232..6394ca9647193 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart @@ -38,6 +38,7 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget { case MobileViewBottomSheetBodyAction.removeFromFavorites: context.pop(); context.read().add(FavoriteEvent.toggle(view)); + break; case MobileViewBottomSheetBodyAction.undo: EditorNotification.undo().post(); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart index 3e594b47f9307..6b54b1fda3b0d 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/bottom_sheet_add_new_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_add_new_page.dart index 37a5cb0221bac..18c5c3a6df3ff 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_add_new_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_add_new_page.dart @@ -23,7 +23,7 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget { text: LocaleKeys.document_menuName.tr(), leftIcon: const FlowySvg( FlowySvgs.document_s, - size: Size.square(20), + size: Size.square(18), ), showTopBorder: false, onTap: () => onAction(ViewLayoutPB.Document), @@ -32,7 +32,7 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget { text: LocaleKeys.grid_menuName.tr(), leftIcon: const FlowySvg( FlowySvgs.grid_s, - size: Size.square(20), + size: Size.square(18), ), showTopBorder: false, onTap: () => onAction(ViewLayoutPB.Grid), @@ -41,7 +41,7 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget { text: LocaleKeys.board_menuName.tr(), leftIcon: const FlowySvg( FlowySvgs.board_s, - size: Size.square(20), + size: Size.square(18), ), showTopBorder: false, onTap: () => onAction(ViewLayoutPB.Board), @@ -49,8 +49,8 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget { FlowyOptionTile.text( text: LocaleKeys.calendar_menuName.tr(), leftIcon: const FlowySvg( - FlowySvgs.date_s, - size: Size.square(20), + FlowySvgs.calendar_s, + size: Size.square(18), ), showTopBorder: false, onTap: () => onAction(ViewLayoutPB.Calendar), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart index c1e2560e48421..75b0151a3a518 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart @@ -1,9 +1,17 @@ +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'; +import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart'; +import 'package:appflowy/startup/tasks/app_widget.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; +import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.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/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:fluttertoast/fluttertoast.dart'; enum MobileBottomSheetType { view, @@ -14,11 +22,13 @@ class MobileViewItemBottomSheet extends StatefulWidget { const MobileViewItemBottomSheet({ super.key, required this.view, + required this.actions, this.defaultType = MobileBottomSheetType.view, }); final ViewPB view; final MobileBottomSheetType defaultType; + final List actions; @override State createState() => @@ -27,12 +37,14 @@ class MobileViewItemBottomSheet extends StatefulWidget { class _MobileViewItemBottomSheetState extends State { MobileBottomSheetType type = MobileBottomSheetType.view; + final fToast = FToast(); @override void initState() { super.initState(); type = widget.defaultType; + fToast.init(AppGlobals.context); } @override @@ -40,6 +52,7 @@ class _MobileViewItemBottomSheetState extends State { switch (type) { case MobileBottomSheetType.view: return MobileViewItemBottomSheetBody( + actions: widget.actions, isFavorite: widget.view.isFavorite, onAction: (action) { switch (action) { @@ -59,7 +72,6 @@ class _MobileViewItemBottomSheetState extends State { case MobileViewItemBottomSheetBodyAction.delete: Navigator.pop(context); context.read().add(const ViewEvent.delete()); - break; case MobileViewItemBottomSheetBodyAction.addToFavorites: case MobileViewItemBottomSheetBodyAction.removeFromFavorites: @@ -68,6 +80,11 @@ class _MobileViewItemBottomSheetState extends State { .read() .add(FavoriteEvent.toggle(widget.view)); break; + case MobileViewItemBottomSheetBodyAction.removeFromRecent: + _removeFromRecent(context); + break; + case MobileViewItemBottomSheetBodyAction.divider: + break; } }, ); @@ -83,4 +100,74 @@ class _MobileViewItemBottomSheetState extends State { ); } } + + Future _removeFromRecent(BuildContext context) async { + final viewId = context.read().view.id; + final recentViewsBloc = context.read(); + Navigator.pop(context); + + await _showConfirmDialog( + onDelete: () { + recentViewsBloc.add(RecentViewsEvent.removeRecentViews([viewId])); + + fToast.showToast( + child: const _RemoveToast(), + positionedToastBuilder: (context, child) { + return Positioned.fill( + top: 450, + child: child, + ); + }, + ); + }, + ); + } + + Future _showConfirmDialog({required VoidCallback onDelete}) async { + await showFlowyCupertinoConfirmDialog( + title: LocaleKeys.sideBar_removePageFromRecent.tr(), + leftButton: FlowyText.regular( + LocaleKeys.button_cancel.tr(), + color: const Color(0xFF1456F0), + ), + rightButton: FlowyText.medium( + LocaleKeys.button_delete.tr(), + color: const Color(0xFFFE0220), + ), + onRightButtonPressed: (context) { + onDelete(); + Navigator.pop(context); + }, + ); + } +} + +class _RemoveToast extends StatelessWidget { + const _RemoveToast(); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 13.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.0), + color: const Color(0xE5171717), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const FlowySvg( + FlowySvgs.success_s, + blendMode: null, + ), + const HSpace(8.0), + FlowyText.regular( + LocaleKeys.sideBar_removeSuccess.tr(), + fontSize: 16.0, + color: Colors.white, + ), + ], + ), + ); + } } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart index 624ae33b9fbc1..d9c6382288153 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart'; +import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -11,6 +11,8 @@ enum MobileViewItemBottomSheetBodyAction { delete, addToFavorites, removeFromFavorites, + divider, + removeFromRecent, } class MobileViewItemBottomSheetBody extends StatelessWidget { @@ -18,63 +20,124 @@ class MobileViewItemBottomSheetBody extends StatelessWidget { super.key, this.isFavorite = false, required this.onAction, + required this.actions, }); final bool isFavorite; final void Function(MobileViewItemBottomSheetBodyAction action) onAction; + final List actions; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - MobileQuickActionButton( + children: + actions.map((action) => _buildActionButton(context, action)).toList(), + ); + } + + Widget _buildActionButton( + BuildContext context, + MobileViewItemBottomSheetBodyAction action, + ) { + switch (action) { + case MobileViewItemBottomSheetBodyAction.rename: + return FlowyOptionTile.text( text: LocaleKeys.button_rename.tr(), - icon: FlowySvgs.m_rename_s, - onTap: () => onAction( - MobileViewItemBottomSheetBodyAction.rename, + leftIcon: const FlowySvg( + FlowySvgs.view_item_rename_s, + size: Size.square(18), ), - ), - _divider(), - MobileQuickActionButton( - text: isFavorite - ? LocaleKeys.button_removeFromFavorites.tr() - : LocaleKeys.button_addToFavorites.tr(), - icon: isFavorite - ? FlowySvgs.m_favorite_selected_lg - : FlowySvgs.m_favorite_unselected_lg, - iconColor: isFavorite ? Colors.yellow : null, + showTopBorder: false, + showBottomBorder: false, onTap: () => onAction( - isFavorite - ? MobileViewItemBottomSheetBodyAction.removeFromFavorites - : MobileViewItemBottomSheetBodyAction.addToFavorites, + MobileViewItemBottomSheetBodyAction.rename, ), - ), - _divider(), - MobileQuickActionButton( + ); + case MobileViewItemBottomSheetBodyAction.duplicate: + return FlowyOptionTile.text( text: LocaleKeys.button_duplicate.tr(), - icon: FlowySvgs.m_duplicate_s, + leftIcon: const FlowySvg( + FlowySvgs.duplicate_s, + size: Size.square(18), + ), + showTopBorder: false, + showBottomBorder: false, onTap: () => onAction( MobileViewItemBottomSheetBodyAction.duplicate, ), - ), - _divider(), - MobileQuickActionButton( + ); + + case MobileViewItemBottomSheetBodyAction.share: + return FlowyOptionTile.text( + text: LocaleKeys.button_share.tr(), + leftIcon: const FlowySvg( + FlowySvgs.share_s, + size: Size.square(18), + ), + showTopBorder: false, + showBottomBorder: false, + onTap: () => onAction( + MobileViewItemBottomSheetBodyAction.share, + ), + ); + case MobileViewItemBottomSheetBodyAction.delete: + return FlowyOptionTile.text( text: LocaleKeys.button_delete.tr(), textColor: Theme.of(context).colorScheme.error, - icon: FlowySvgs.m_delete_s, - iconColor: Theme.of(context).colorScheme.error, + leftIcon: FlowySvg( + FlowySvgs.delete_s, + size: const Size.square(18), + color: Theme.of(context).colorScheme.error, + ), + showTopBorder: false, + showBottomBorder: false, onTap: () => onAction( MobileViewItemBottomSheetBodyAction.delete, ), - ), - _divider(), - ], - ); - } + ); + case MobileViewItemBottomSheetBodyAction.addToFavorites: + return FlowyOptionTile.text( + text: LocaleKeys.button_addToFavorites.tr(), + leftIcon: const FlowySvg( + FlowySvgs.favorite_s, + size: Size.square(18), + ), + showTopBorder: false, + showBottomBorder: false, + onTap: () => onAction( + MobileViewItemBottomSheetBodyAction.addToFavorites, + ), + ); + case MobileViewItemBottomSheetBodyAction.removeFromFavorites: + return FlowyOptionTile.text( + text: LocaleKeys.button_removeFromFavorites.tr(), + leftIcon: const FlowySvg( + FlowySvgs.favorite_section_remove_from_favorite_s, + size: Size.square(18), + ), + showTopBorder: false, + showBottomBorder: false, + onTap: () => onAction( + MobileViewItemBottomSheetBodyAction.removeFromFavorites, + ), + ); + case MobileViewItemBottomSheetBodyAction.removeFromRecent: + return FlowyOptionTile.text( + text: LocaleKeys.button_removeFromRecent.tr(), + leftIcon: const FlowySvg( + FlowySvgs.remove_from_recent_s, + size: Size.square(18), + ), + showTopBorder: false, + showBottomBorder: false, + onTap: () => onAction( + MobileViewItemBottomSheetBodyAction.removeFromRecent, + ), + ); - Widget _divider() => const Divider( - height: 8.5, - thickness: 0.5, - ); + case MobileViewItemBottomSheetBodyAction.divider: + return const Divider(height: 0.5); + } + } } 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 f27c5b3b6f585..327db627c4c4f 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( @@ -64,8 +65,21 @@ enum MobilePaneActionType { ], child: BlocBuilder( builder: (context, state) { + final isFavorite = state.view.isFavorite; return MobileViewItemBottomSheet( view: viewBloc.state.view, + actions: [ + isFavorite + ? MobileViewItemBottomSheetBodyAction + .removeFromFavorites + : MobileViewItemBottomSheetBodyAction + .addToFavorites, + MobileViewItemBottomSheetBodyAction.divider, + MobileViewItemBottomSheetBodyAction.rename, + MobileViewItemBottomSheetBodyAction.duplicate, + MobileViewItemBottomSheetBodyAction.divider, + MobileViewItemBottomSheetBodyAction.delete, + ], ); }, ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart index 718ac5c4e6c15..be815b6550c6a 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart @@ -1,6 +1,6 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart'; import 'package:appflowy/plugins/base/drag_handler.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; extension BottomSheetPaddingExtension on BuildContext { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_page.dart index 7938f474628bb..a1fc2a70a3697 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 76deaa6f0a6d3..184bd901c15c2 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 2e2367b9bbb00..0b0c16f951e36 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 e3cad565abf28..08f6c0b48bd5f 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 e62ddeb872416..cc6c9b43aac5d 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/field/mobile_field_bottom_sheets.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_bottom_sheets.dart index b799d339da1ad..7d81801d736b6 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'; @@ -41,7 +42,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 909018d1b1a29..8dd224390c3de 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 9ef8ddefb1f05..763da369184ee 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 a2771ece26e75..4d8acbbeba024 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/favorite/mobile_favorite_folder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart index f9fcba5754209..ca012891f6066 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_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/home/favorite_folder/mobile_home_favorite_folder.dart'; @@ -10,6 +8,7 @@ import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.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'; @@ -17,14 +16,15 @@ class MobileFavoritePageFolder extends StatelessWidget { const MobileFavoritePageFolder({ super.key, required this.userProfile, - required this.workspaceId, }); final UserProfilePB userProfile; - final String workspaceId; @override Widget build(BuildContext context) { + final workspaceId = + context.read().state.currentWorkspace?.workspaceId ?? + ''; return MultiBlocProvider( providers: [ BlocProvider( @@ -67,7 +67,8 @@ class MobileFavoritePageFolder extends StatelessWidget { MobileFavoriteFolder( showHeader: false, forceExpanded: true, - views: favoriteState.views, + views: + favoriteState.views.map((e) => e.item).toList(), ), const VSpace(100.0), ], diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart index 7afc740b45d9d..e6d2d895b1461 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart @@ -64,8 +64,6 @@ class MobileFavoriteScreen extends StatelessWidget { builder: (context, state) { return MobileFavoritePage( userProfile: userProfile, - workspaceId: state.currentWorkspace?.workspaceId ?? - workspaceSetting.workspaceId, ); }, ), @@ -81,11 +79,9 @@ class MobileFavoritePage extends StatelessWidget { const MobileFavoritePage({ super.key, required this.userProfile, - required this.workspaceId, }); final UserProfilePB userProfile; - final String workspaceId; @override Widget build(BuildContext context) { @@ -108,7 +104,6 @@ class MobileFavoritePage extends StatelessWidget { Expanded( child: MobileFavoritePageFolder( userProfile: userProfile, - workspaceId: workspaceId, ), ), ], diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/favorite_space.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/favorite_space.dart new file mode 100644 index 0000000000000..684442a74a7e5 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/favorite_space.dart @@ -0,0 +1,126 @@ +import 'package:appflowy/mobile/application/mobile_router.dart'; +import 'package:appflowy/mobile/presentation/home/shared/empty_placeholder.dart'; +import 'package:appflowy/mobile/presentation/home/shared/mobile_view_card.dart'; +import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; +import 'package:appflowy/workspace/application/user/prelude.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MobileFavoriteSpace extends StatefulWidget { + const MobileFavoriteSpace({ + super.key, + required this.userProfile, + }); + + final UserProfilePB userProfile; + + @override + State createState() => _MobileFavoriteSpaceState(); +} + +class _MobileFavoriteSpaceState extends State + with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + final workspaceId = + context.read().state.currentWorkspace?.workspaceId ?? + ''; + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (_) => SidebarSectionsBloc() + ..add( + SidebarSectionsEvent.initial(widget.userProfile, workspaceId), + ), + ), + BlocProvider( + create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()), + ), + ], + child: BlocListener( + listener: (context, state) => + context.read().add(const FavoriteEvent.initial()), + child: MultiBlocListener( + listeners: [ + BlocListener( + listenWhen: (p, c) => + p.lastCreatedRootView?.id != c.lastCreatedRootView?.id, + listener: (context, state) => + context.pushView(state.lastCreatedRootView!), + ), + ], + child: Builder( + builder: (context) { + final favoriteState = context.watch().state; + + if (favoriteState.isLoading) { + return const SizedBox.shrink(); + } + + if (favoriteState.views.isEmpty) { + return const EmptySpacePlaceholder( + type: MobileViewCardType.favorite, + ); + } + + return _FavoriteViews( + favoriteViews: favoriteState.views.reversed.toList(), + ); + }, + ), + ), + ), + ); + } +} + +class _FavoriteViews extends StatelessWidget { + const _FavoriteViews({ + required this.favoriteViews, + }); + + final List favoriteViews; + + @override + Widget build(BuildContext context) { + return Scrollbar( + child: ListView.separated( + key: const PageStorageKey('favorite_views_page_storage_key'), + padding: const EdgeInsets.symmetric( + horizontal: HomeSpaceViewSizes.mHorizontalPadding, + ), + itemBuilder: (context, index) { + final view = favoriteViews[index]; + return Container( + padding: const EdgeInsets.symmetric(vertical: 24.0), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor, + width: 0.5, + ), + ), + ), + child: MobileViewCard( + key: ValueKey(view.item.id), + view: view.item, + timestamp: view.timestamp, + type: MobileViewCardType.favorite, + ), + ); + }, + separatorBuilder: (context, index) => const HSpace(8), + itemCount: favoriteViews.length, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart index c56d369676d4b..d683cf3507bc7 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart'; import 'package:appflowy/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder_header.dart'; @@ -7,6 +5,7 @@ import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MobileFavoriteFolder extends StatelessWidget { @@ -28,7 +27,7 @@ class MobileFavoriteFolder extends StatelessWidget { } return BlocProvider( - create: (context) => FolderBloc(type: FolderCategoryType.favorite) + create: (context) => FolderBloc(type: FolderSpaceType.favorite) ..add( const FolderEvent.initial(), ), @@ -55,9 +54,9 @@ class MobileFavoriteFolder extends StatelessWidget { ...views.map( (view) => MobileViewItem( key: ValueKey( - '${FolderCategoryType.favorite.name} ${view.id}', + '${FolderSpaceType.favorite.name} ${view.id}', ), - categoryType: FolderCategoryType.favorite, + spaceType: FolderSpaceType.favorite, isDraggable: false, isFirstChild: view.id == views.first.id, isFeedback: false, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/home_space/home_space.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/home_space/home_space.dart new file mode 100644 index 0000000000000..6ba27a6766128 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/home_space/home_space.dart @@ -0,0 +1,44 @@ +import 'package:appflowy/mobile/presentation/home/mobile_folders.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MobileHomeSpace extends StatefulWidget { + const MobileHomeSpace({super.key, required this.userProfile}); + + final UserProfilePB userProfile; + + @override + State createState() => _MobileHomeSpaceState(); +} + +class _MobileHomeSpaceState extends State + with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + final workspaceId = + context.read().state.currentWorkspace?.workspaceId ?? + ''; + return Scrollbar( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: HomeSpaceViewSizes.mHorizontalPadding, + vertical: HomeSpaceViewSizes.mVerticalPadding, + ), + child: MobileFolders( + user: widget.userProfile, + workspaceId: workspaceId, + showFavorite: false, + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart index 7631383faabea..100649701d93f 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart @@ -1,5 +1,7 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/mobile_router.dart'; +import 'package:appflowy/mobile/presentation/home/home.dart'; import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; @@ -11,6 +13,7 @@ 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'; +import 'package:go_router/go_router.dart'; // Contains Public And Private Sections class MobileFolders extends StatelessWidget { @@ -70,24 +73,25 @@ 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, ), ], - const VSpace(8.0), + const VSpace(4.0), + const _TrashButton(), ], ), ); @@ -97,3 +101,28 @@ class MobileFolders extends StatelessWidget { ); } } + +class _TrashButton extends StatelessWidget { + const _TrashButton(); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 52, + child: FlowyButton( + expand: true, + margin: const EdgeInsets.symmetric(vertical: 8), + leftIcon: const FlowySvg( + FlowySvgs.m_delete_s, + ), + leftIconSize: const Size.square(18), + iconPadding: 10.0, + text: FlowyText.regular( + LocaleKeys.trash_text.tr(), + fontSize: 16.0, + ), + onTap: () => context.push(MobileHomeTrashPage.routeName), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart index 69759fc508b63..e0b7c626d91a9 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart @@ -1,23 +1,20 @@ import 'dart:io'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/home/home.dart'; -import 'package:appflowy/mobile/presentation/home/mobile_folders.dart'; import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart'; -import 'package:appflowy/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart'; +import 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart'; +import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; +import 'package:appflowy/workspace/application/recent/cached_recent_service.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.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'; -import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; class MobileHomeScreen extends StatelessWidget { @@ -84,66 +81,49 @@ class MobileHomePage extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (_) => UserWorkspaceBloc(userProfile: userProfile) - ..add( - const UserWorkspaceEvent.initial(), + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (_) => UserWorkspaceBloc(userProfile: userProfile) + ..add( + const UserWorkspaceEvent.initial(), + ), + ), + BlocProvider( + create: (context) => + FavoriteBloc()..add(const FavoriteEvent.initial()), ), - child: BlocBuilder( + ], + child: BlocConsumer( buildWhen: (previous, current) => previous.currentWorkspace?.workspaceId != current.currentWorkspace?.workspaceId, + listener: (context, state) => getIt().reset(), builder: (context, state) { if (state.currentWorkspace == null) { return const SizedBox.shrink(); } + return Column( children: [ // Header Padding( padding: EdgeInsets.only( - left: 16, - right: 16, + left: HomeSpaceViewSizes.mHorizontalPadding, + right: 8.0, top: Platform.isAndroid ? 8.0 : 0.0, ), child: MobileHomePageHeader( userProfile: userProfile, ), ), - const Divider(), - // Folder Expanded( - child: Scrollbar( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - // Recent files - const MobileRecentFolder(), - - // Folders - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: MobileFolders( - user: userProfile, - workspaceId: - state.currentWorkspace?.workspaceId ?? - workspaceSetting.workspaceId, - showFavorite: false, - ), - ), - const SizedBox(height: 8), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 24), - child: _TrashButton(), - ), - ], - ), - ), + child: BlocProvider( + create: (context) => + SpaceOrderBloc()..add(const SpaceOrderEvent.initial()), + child: MobileSpaceTab( + userProfile: userProfile, ), ), ), @@ -154,25 +134,3 @@ class MobileHomePage extends StatelessWidget { ); } } - -class _TrashButton extends StatelessWidget { - const _TrashButton(); - - @override - Widget build(BuildContext context) { - return FlowyButton( - expand: true, - margin: const EdgeInsets.symmetric(vertical: 8), - leftIcon: FlowySvg( - FlowySvgs.m_delete_m, - color: Theme.of(context).colorScheme.onSurface, - ), - leftIconSize: const Size.square(24), - text: FlowyText.medium( - LocaleKeys.trash_text.tr(), - fontSize: 18.0, - ), - onTap: () => context.push(MobileHomeTrashPage.routeName), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart index 1759d32aaff5c..5ae2ff1430aed 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'; @@ -36,7 +35,7 @@ class MobileHomePageHeader extends StatelessWidget { final isCollaborativeWorkspace = context.read().state.isCollabWorkspaceOn; return ConstrainedBox( - constraints: const BoxConstraints(minHeight: 52), + constraints: const BoxConstraints(minHeight: 56), child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -45,11 +44,14 @@ class MobileHomePageHeader extends StatelessWidget { ? _MobileWorkspace(userProfile: userProfile) : _MobileUser(userProfile: userProfile), ), - IconButton( - onPressed: () => context.push( + GestureDetector( + onTap: () => context.push( MobileHomeSettingPage.routeName, ), - icon: const FlowySvg(FlowySvgs.m_setting_m), + child: const Padding( + padding: EdgeInsets.all(8.0), + child: FlowySvg(FlowySvgs.m_setting_m), + ), ), ], ), @@ -120,12 +122,12 @@ class _MobileWorkspace extends StatelessWidget { }, child: Row( children: [ - const HSpace(2.0), SizedBox.square( dimension: 34.0, child: WorkspaceIcon( workspace: currentWorkspace, iconSize: 26, + fontSize: 16.0, enableEdit: false, onSelected: (result) => context.read().add( UserWorkspaceEvent.updateWorkspaceIcon( @@ -142,7 +144,7 @@ class _MobileWorkspace extends StatelessWidget { children: [ Row( children: [ - FlowyText.medium( + FlowyText.semibold( currentWorkspace.name, fontSize: 16.0, overflow: TextOverflow.ellipsis, @@ -151,7 +153,7 @@ class _MobileWorkspace extends StatelessWidget { const FlowySvg(FlowySvgs.list_dropdown_s), ], ), - FlowyText.medium( + FlowyText.regular( userProfile.email.isNotEmpty ? userProfile.email : userProfile.name, 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 a9c2f1b9331fb..2ee9e14175685 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 4dc9f28155b9a..a2b9ae52c780c 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'; @@ -38,7 +38,8 @@ class _MobileRecentFolderState extends State { builder: (context, state) { final ids = {}; - List recentViews = state.views.reversed.toList(); + List recentViews = + state.views.reversed.map((e) => e.item).toList(); recentViews.retainWhere((element) => ids.add(element.id)); // only keep the first 20 items. @@ -91,7 +92,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/recent_folder/recent_space.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/recent_space.dart new file mode 100644 index 0000000000000..3cab831c23400 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/recent_space.dart @@ -0,0 +1,96 @@ +import 'package:appflowy/mobile/presentation/home/shared/empty_placeholder.dart'; +import 'package:appflowy/mobile/presentation/home/shared/mobile_view_card.dart'; +import 'package:appflowy/workspace/application/recent/prelude.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MobileRecentSpace extends StatefulWidget { + const MobileRecentSpace({super.key}); + + @override + State createState() => _MobileRecentSpaceState(); +} + +class _MobileRecentSpaceState extends State + with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return BlocProvider( + create: (context) => + RecentViewsBloc()..add(const RecentViewsEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + if (state.isLoading) { + return const SizedBox.shrink(); + } + + final recentViews = _filterRecentViews(state.views); + + if (recentViews.isEmpty) { + return const Center( + child: EmptySpacePlaceholder(type: MobileViewCardType.recent), + ); + } + + return _RecentViews(recentViews: recentViews); + }, + ), + ); + } + + List _filterRecentViews(List recentViews) { + final ids = {}; + final filteredRecentViews = recentViews.reversed.toList(); + filteredRecentViews.retainWhere((e) => ids.add(e.item.id)); + return filteredRecentViews; + } +} + +class _RecentViews extends StatelessWidget { + const _RecentViews({ + required this.recentViews, + }); + + final List recentViews; + + @override + Widget build(BuildContext context) { + return Scrollbar( + child: ListView.separated( + key: const PageStorageKey('recent_views_page_storage_key'), + padding: const EdgeInsets.symmetric( + horizontal: HomeSpaceViewSizes.mHorizontalPadding, + ), + itemBuilder: (context, index) { + final sectionView = recentViews[index]; + return Container( + padding: const EdgeInsets.symmetric(vertical: 24.0), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor, + width: 0.5, + ), + ), + ), + child: MobileViewCard( + key: ValueKey(sectionView.item.id), + view: sectionView.item, + timestamp: sectionView.timestamp, + type: MobileViewCardType.recent, + ), + ); + }, + separatorBuilder: (context, index) => const HSpace(8), + itemCount: recentViews.length, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart index c9ea1453c99f5..7a14a4d48d158 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,16 +1,13 @@ -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'; import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart'; 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 +15,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(), ), @@ -36,53 +33,42 @@ class MobileSectionFolder extends StatelessWidget { builder: (context, state) { return Column( children: [ - MobileSectionFolderHeader( - title: title, - isExpanded: context.read().state.isExpanded, - onPressed: () => context - .read() - .add(const FolderEvent.expandOrUnExpand()), - onAdded: () { - context.read().add( - SidebarSectionsEvent.createRootViewInSection( - name: - LocaleKeys.menuAppHeader_defaultNewPageName.tr(), - index: 0, - viewSection: categoryType.toViewSectionPB, - ), - ); - context.read().add( - const FolderEvent.expandOrUnExpand(isExpanded: true), - ); - }, - ), - const VSpace(8.0), - const Divider( - height: 1, + SizedBox( + height: HomeSpaceViewSizes.mViewHeight, + child: MobileSectionFolderHeader( + title: title, + isExpanded: context.read().state.isExpanded, + onPressed: () => context + .read() + .add(const FolderEvent.expandOrUnExpand()), + onAdded: () { + context.read().add( + SidebarSectionsEvent.createRootViewInSection( + name: LocaleKeys.menuAppHeader_defaultNewPageName + .tr(), + index: 0, + viewSection: spaceType.toViewSectionPB, + ), + ); + context.read().add( + const FolderEvent.expandOrUnExpand(isExpanded: true), + ); + }, + ), ), if (state.isExpanded) ...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) { - final view = context.read().state.view; - return buildEndActionPane(context, [ - MobilePaneActionType.delete, - view.isFavorite - ? MobilePaneActionType.removeFromFavorites - : MobilePaneActionType.addToFavorites, - MobilePaneActionType.more, - ]); - }, ), ), ], diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart index 3ba15df25d56a..49a9829d8a03b 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart @@ -1,4 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -29,24 +30,23 @@ class _MobileSectionFolderHeaderState extends State { @override Widget build(BuildContext context) { - const iconSize = 32.0; return Row( children: [ Expanded( child: FlowyButton( - text: FlowyText.semibold( + text: FlowyText.medium( widget.title, - fontSize: 20.0, + fontSize: 16.0, ), margin: const EdgeInsets.symmetric(vertical: 8), expandText: false, + iconPadding: 2, mainAxisAlignment: MainAxisAlignment.start, rightIcon: AnimatedRotation( duration: const Duration(milliseconds: 200), turns: _turns, - child: const Icon( - Icons.keyboard_arrow_down_rounded, - color: Colors.grey, + child: const FlowySvg( + FlowySvgs.m_spaces_expand_s, ), ), onTap: () { @@ -60,12 +60,10 @@ class _MobileSectionFolderHeaderState extends State { FlowyIconButton( key: mobileCreateNewPageButtonKey, hoverColor: Theme.of(context).colorScheme.secondaryContainer, - iconPadding: const EdgeInsets.all(2), - height: iconSize, - width: iconSize, + height: HomeSpaceViewSizes.mViewButtonDimension, + width: HomeSpaceViewSizes.mViewButtonDimension, icon: const FlowySvg( - FlowySvgs.add_s, - size: Size.square(iconSize), + FlowySvgs.m_space_add_s, ), onPressed: widget.onAdded, ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/empty_placeholder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/empty_placeholder.dart new file mode 100644 index 0000000000000..e59fa87538b90 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/empty_placeholder.dart @@ -0,0 +1,55 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/home/shared/mobile_view_card.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +class EmptySpacePlaceholder extends StatelessWidget { + const EmptySpacePlaceholder({super.key, required this.type}); + + final MobileViewCardType type; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 48.0), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const FlowySvg( + FlowySvgs.m_empty_page_xl, + ), + const VSpace(16.0), + FlowyText.medium( + _emptyPageText, + fontSize: 18.0, + textAlign: TextAlign.center, + ), + const VSpace(8.0), + FlowyText.regular( + _emptyPageSubText, + fontSize: 17.0, + maxLines: 10, + textAlign: TextAlign.center, + lineHeight: 1.3, + color: Theme.of(context).hintColor, + ), + ], + ), + ); + } + + String get _emptyPageText => switch (type) { + MobileViewCardType.recent => LocaleKeys.sideBar_emptyRecent.tr(), + MobileViewCardType.favorite => LocaleKeys.sideBar_emptyFavorite.tr(), + }; + + String get _emptyPageSubText => switch (type) { + MobileViewCardType.recent => + LocaleKeys.sideBar_emptyRecentDescription.tr(), + MobileViewCardType.favorite => + LocaleKeys.sideBar_emptyFavoriteDescription.tr(), + }; +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/mobile_view_card.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/mobile_view_card.dart new file mode 100644 index 0000000000000..c4498a27cdf63 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/mobile_view_card.dart @@ -0,0 +1,399 @@ +import 'dart:io'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/application/mobile_router.dart'; +import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; +import 'package:appflowy/mobile/application/recent/recent_view_bloc.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/shared/appflowy_network_image.dart'; +import 'package:appflowy/shared/flowy_gradient_colors.dart'; +import 'package:appflowy/util/string_extension.dart'; +import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; +import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:fixnum/fixnum.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:google_fonts/google_fonts.dart'; +import 'package:provider/provider.dart'; +import 'package:string_validator/string_validator.dart'; + +enum MobileViewCardType { + recent, + favorite; + + String get lastOperationHintText => switch (this) { + MobileViewCardType.recent => LocaleKeys.sideBar_lastViewed.tr(), + MobileViewCardType.favorite => LocaleKeys.sideBar_favoriteAt.tr(), + }; +} + +class MobileViewCard extends StatelessWidget { + const MobileViewCard({ + super.key, + required this.view, + this.timestamp, + required this.type, + }); + + final ViewPB view; + final Int64? timestamp; + final MobileViewCardType type; + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => ViewBloc(view: view, shouldLoadChildViews: false) + ..add(const ViewEvent.initial()), + ), + BlocProvider( + create: (context) => + RecentViewBloc(view: view)..add(const RecentViewEvent.initial()), + ), + ], + child: BlocBuilder( + builder: (context, state) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTapUp: (_) => context.pushView(view), + onLongPressUp: () => _showActionSheet(context), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded(child: _buildDescription(context, state)), + const HSpace(20.0), + SizedBox( + width: 84, + height: 60, + child: _buildCover(context, state), + ), + ], + ), + ); + }, + ), + ); + } + + Widget _buildDescription(BuildContext context, RecentViewState state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // page icon & page title + _buildTitle(context, state), + const VSpace(12.0), + // author & last viewed + _buildNameAndLastViewed(context, state), + ], + ); + } + + Widget _buildNameAndLastViewed(BuildContext context, RecentViewState state) { + final supportAvatar = isURL(state.icon); + if (!supportAvatar) { + return _buildLastViewed(context); + } + return Row( + children: [ + _buildAvatar(context, state), + Flexible(child: _buildAuthor(context, state)), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 3.0), + child: FlowySvg(FlowySvgs.dot_s), + ), + _buildLastViewed(context), + ], + ); + } + + Widget _buildAvatar(BuildContext context, RecentViewState state) { + final userProfile = Provider.of(context); + final iconUrl = userProfile?.iconUrl; + if (iconUrl == null || + iconUrl.isEmpty || + view.createdBy != userProfile?.id) { + return const SizedBox.shrink(); + } + + return Padding( + padding: const EdgeInsets.only(top: 2, bottom: 2, right: 8), + child: ClipRRect( + borderRadius: BorderRadius.circular(8.0), + child: SizedBox.square( + dimension: 16.0, + child: FlowyNetworkImage( + url: iconUrl, + ), + ), + ), + ); + } + + Widget _buildCover(BuildContext context, RecentViewState state) { + return ClipRRect( + borderRadius: BorderRadius.circular(8), + child: _ViewCover( + coverTypeV1: state.coverTypeV1, + coverTypeV2: state.coverTypeV2, + value: state.coverValue, + ), + ); + } + + Widget _buildTitle(BuildContext context, RecentViewState state) { + final name = state.name; + final icon = state.icon; + final fontFamily = Platform.isAndroid || Platform.isLinux + ? GoogleFonts.notoColorEmoji().fontFamily + : null; + return RichText( + maxLines: 3, + overflow: TextOverflow.ellipsis, + text: TextSpan( + children: [ + TextSpan( + text: icon, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 17.0, + fontWeight: FontWeight.w600, + fontFamily: fontFamily, + ), + ), + if (icon.isNotEmpty) const WidgetSpan(child: HSpace(2.0)), + TextSpan( + text: name, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 16.0, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } + + Widget _buildAuthor(BuildContext context, RecentViewState state) { + return FlowyText.regular( + // view.createdBy.toString(), + 'Lucas', + fontSize: 12.0, + color: Theme.of(context).hintColor, + overflow: TextOverflow.ellipsis, + ); + } + + Widget _buildLastViewed(BuildContext context) { + if (timestamp == null) { + return const SizedBox.shrink(); + } + final date = _formatTimestamp( + timestamp!.toInt() * 1000, + ); + return FlowyText.regular( + date, + fontSize: 12.0, + color: Theme.of(context).hintColor, + ); + } + + String _formatTimestamp(int timestamp) { + final now = DateTime.now(); + final dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp); + final difference = now.difference(dateTime); + final String date; + + if (difference.inMinutes < 1) { + date = LocaleKeys.sideBar_justNow.tr(); + } else if (difference.inHours < 1) { + // Less than 1 hour + date = LocaleKeys.sideBar_minutesAgo + .tr(namedArgs: {'count': difference.inMinutes.toString()}); + } else if (difference.inHours >= 1 && difference.inHours < 24) { + // Between 1 hour and 24 hours + date = DateFormat('h:mm a').format(dateTime); + } else if (difference.inDays >= 1 && dateTime.year == now.year) { + // More than 24 hours but within the current year + date = DateFormat('M/d, h:mm a').format(dateTime); + } else { + // Other cases (previous years) + date = DateFormat('M/d/yyyy, h:mm a').format(dateTime); + } + + if (difference.inHours >= 1) { + return '${type.lastOperationHintText} $date'; + } + + return date; + } + + Future _showActionSheet(BuildContext context) async { + final viewBloc = context.read(); + final favoriteBloc = context.read(); + final recentViewsBloc = context.read(); + await showMobileBottomSheet( + context, + showDragHandle: true, + showDivider: false, + backgroundColor: AFThemeExtension.of(context).background, + useRootNavigator: true, + builder: (context) { + return MultiBlocProvider( + providers: [ + BlocProvider.value(value: viewBloc), + BlocProvider.value(value: favoriteBloc), + if (recentViewsBloc != null) + BlocProvider.value(value: recentViewsBloc), + ], + child: BlocBuilder( + builder: (context, state) { + final isFavorite = state.view.isFavorite; + return MobileViewItemBottomSheet( + view: viewBloc.state.view, + actions: _buildActions(isFavorite), + ); + }, + ), + ); + }, + ); + } + + List _buildActions(bool isFavorite) { + switch (type) { + case MobileViewCardType.recent: + return [ + isFavorite + ? MobileViewItemBottomSheetBodyAction.removeFromFavorites + : MobileViewItemBottomSheetBodyAction.addToFavorites, + MobileViewItemBottomSheetBodyAction.divider, + MobileViewItemBottomSheetBodyAction.duplicate, + MobileViewItemBottomSheetBodyAction.divider, + MobileViewItemBottomSheetBodyAction.removeFromRecent, + ]; + case MobileViewCardType.favorite: + return [ + MobileViewItemBottomSheetBodyAction.removeFromFavorites, + MobileViewItemBottomSheetBodyAction.divider, + MobileViewItemBottomSheetBodyAction.duplicate, + ]; + } + } +} + +class _ViewCover extends StatelessWidget { + const _ViewCover({ + required this.coverTypeV1, + this.coverTypeV2, + this.value, + }); + + final CoverType coverTypeV1; + final PageStyleCoverImageType? coverTypeV2; + final String? value; + + @override + Widget build(BuildContext context) { + final placeholder = Container( + color: const Color(0xFFE1FBFF), + ); + final value = this.value; + if (value == null) { + return placeholder; + } + if (coverTypeV2 != null) { + return _buildCoverV2(context, value, placeholder); + } + return _buildCoverV1(context, value, placeholder); + } + + Widget _buildCoverV2(BuildContext context, String value, Widget placeholder) { + final type = coverTypeV2; + if (type == null) { + return placeholder; + } + if (type == PageStyleCoverImageType.customImage || + type == PageStyleCoverImageType.unsplashImage) { + final userProfilePB = Provider.of(context); + return FlowyNetworkImage( + url: value, + userProfilePB: userProfilePB, + ); + } + + if (type == PageStyleCoverImageType.builtInImage) { + return Image.asset( + PageStyleCoverImageType.builtInImagePath(value), + fit: BoxFit.cover, + ); + } + + if (type == PageStyleCoverImageType.pureColor) { + final color = value.coverColor(context); + if (color != null) { + return ColoredBox( + color: color, + ); + } + } + + if (type == PageStyleCoverImageType.gradientColor) { + return Container( + decoration: BoxDecoration( + gradient: FlowyGradientColor.fromId(value).linear, + ), + ); + } + + if (type == PageStyleCoverImageType.localImage) { + return Image.file( + File(value), + fit: BoxFit.cover, + ); + } + + return placeholder; + } + + Widget _buildCoverV1(BuildContext context, String value, Widget placeholder) { + switch (coverTypeV1) { + case CoverType.file: + if (isURL(value)) { + final userProfilePB = Provider.of(context); + return FlowyNetworkImage( + url: value, + userProfilePB: userProfilePB, + ); + } + final imageFile = File(value); + if (!imageFile.existsSync()) { + return placeholder; + } + return Image.file( + imageFile, + ); + case CoverType.asset: + return Image.asset( + value, + fit: BoxFit.cover, + ); + case CoverType.color: + final color = value.tryToColor() ?? Colors.white; + return Container( + color: color, + ); + case CoverType.none: + return placeholder; + } + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/_round_underline_tab_indicator.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/_round_underline_tab_indicator.dart new file mode 100644 index 0000000000000..1a3eb121f3b77 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/_round_underline_tab_indicator.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; + +class RoundUnderlineTabIndicator extends Decoration { + const RoundUnderlineTabIndicator({ + this.borderRadius, + this.borderSide = const BorderSide(width: 2.0, color: Colors.white), + this.insets = EdgeInsets.zero, + required this.width, + }); + + final BorderRadius? borderRadius; + final BorderSide borderSide; + final EdgeInsetsGeometry insets; + final double width; + + @override + Decoration? lerpFrom(Decoration? a, double t) { + if (a is UnderlineTabIndicator) { + return UnderlineTabIndicator( + borderSide: BorderSide.lerp(a.borderSide, borderSide, t), + insets: EdgeInsetsGeometry.lerp(a.insets, insets, t)!, + ); + } + return super.lerpFrom(a, t); + } + + @override + Decoration? lerpTo(Decoration? b, double t) { + if (b is UnderlineTabIndicator) { + return UnderlineTabIndicator( + borderSide: BorderSide.lerp(borderSide, b.borderSide, t), + insets: EdgeInsetsGeometry.lerp(insets, b.insets, t)!, + ); + } + return super.lerpTo(b, t); + } + + @override + BoxPainter createBoxPainter([VoidCallback? onChanged]) { + return _UnderlinePainter(this, borderRadius, onChanged); + } + + Rect _indicatorRectFor(Rect rect, TextDirection textDirection) { + final Rect indicator = insets.resolve(textDirection).deflateRect(rect); + final center = indicator.center.dx; + return Rect.fromLTWH( + center - width / 2.0, + indicator.bottom - borderSide.width, + width, + borderSide.width, + ); + } + + @override + Path getClipPath(Rect rect, TextDirection textDirection) { + if (borderRadius != null) { + return Path() + ..addRRect( + borderRadius!.toRRect(_indicatorRectFor(rect, textDirection)), + ); + } + return Path()..addRect(_indicatorRectFor(rect, textDirection)); + } +} + +class _UnderlinePainter extends BoxPainter { + _UnderlinePainter( + this.decoration, + this.borderRadius, + super.onChanged, + ); + + final RoundUnderlineTabIndicator decoration; + final BorderRadius? borderRadius; + + @override + void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { + assert(configuration.size != null); + final Rect rect = offset & configuration.size!; + final TextDirection textDirection = configuration.textDirection!; + final Paint paint; + if (borderRadius != null) { + paint = Paint()..color = decoration.borderSide.color; + final Rect indicator = decoration._indicatorRectFor(rect, textDirection); + final RRect rrect = RRect.fromRectAndCorners( + indicator, + topLeft: borderRadius!.topLeft, + topRight: borderRadius!.topRight, + bottomRight: borderRadius!.bottomRight, + bottomLeft: borderRadius!.bottomLeft, + ); + canvas.drawRRect(rrect, paint); + } else { + paint = decoration.borderSide.toPaint()..strokeCap = StrokeCap.round; + final Rect indicator = decoration + ._indicatorRectFor(rect, textDirection) + .deflate(decoration.borderSide.width / 2.0); + canvas.drawLine(indicator.bottomLeft, indicator.bottomRight, paint); + } + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/_tab_bar.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/_tab_bar.dart new file mode 100644 index 0000000000000..adf13ed66782d --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/_tab_bar.dart @@ -0,0 +1,57 @@ +import 'package:appflowy/mobile/presentation/home/tab/_round_underline_tab_indicator.dart'; +import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:reorderable_tabbar/reorderable_tabbar.dart'; + +class MobileSpaceTabBar extends StatelessWidget { + const MobileSpaceTabBar({ + super.key, + this.height = 38.0, + required this.tabController, + required this.tabs, + required this.onReorder, + }); + + final double height; + final List tabs; + final TabController tabController; + final OnReorder onReorder; + + @override + Widget build(BuildContext context) { + final baseStyle = Theme.of(context).textTheme.bodyMedium; + final labelStyle = baseStyle?.copyWith( + fontWeight: FontWeight.w500, + fontSize: 15.0, + ); + final unselectedLabelStyle = baseStyle?.copyWith( + fontWeight: FontWeight.w400, + fontSize: 15.0, + ); + + return Container( + height: height, + padding: const EdgeInsets.only(left: 8.0), + child: ReorderableTabBar( + controller: tabController, + tabs: tabs.map((e) => Tab(text: e.tr)).toList(), + indicatorSize: TabBarIndicatorSize.label, + indicatorColor: Theme.of(context).primaryColor, + isScrollable: true, + labelStyle: labelStyle, + labelColor: baseStyle?.color, + labelPadding: const EdgeInsets.symmetric(horizontal: 12.0), + unselectedLabelStyle: unselectedLabelStyle, + overlayColor: WidgetStateProperty.all(Colors.transparent), + indicator: RoundUnderlineTabIndicator( + width: 28.0, + borderSide: BorderSide( + color: Theme.of(context).primaryColor, + width: 3, + ), + ), + onReorder: onReorder, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart new file mode 100644 index 0000000000000..097bd22910826 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart @@ -0,0 +1,107 @@ +import 'package:appflowy/mobile/presentation/home/favorite_folder/favorite_space.dart'; +import 'package:appflowy/mobile/presentation/home/home_space/home_space.dart'; +import 'package:appflowy/mobile/presentation/home/recent_folder/recent_space.dart'; +import 'package:appflowy/mobile/presentation/home/tab/_tab_bar.dart'; +import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:provider/provider.dart'; + +class MobileSpaceTab extends StatefulWidget { + const MobileSpaceTab({super.key, required this.userProfile}); + + final UserProfilePB userProfile; + + @override + State createState() => _MobileSpaceTabState(); +} + +class _MobileSpaceTabState extends State + with SingleTickerProviderStateMixin { + TabController? tabController; + + @override + void dispose() { + tabController?.removeListener(_onTabChange); + tabController?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Provider.value( + value: widget.userProfile, + child: BlocBuilder( + builder: (context, state) { + if (state.isLoading) { + return const SizedBox.shrink(); + } + + _initTabController(state); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MobileSpaceTabBar( + tabController: tabController!, + tabs: state.tabsOrder, + onReorder: (from, to) { + context.read().add( + SpaceOrderEvent.reorder(from, to), + ); + }, + ), + const HSpace(12.0), + Expanded( + child: TabBarView( + controller: tabController, + children: _buildTabs(state), + ), + ), + ], + ); + }, + ), + ); + } + + void _initTabController(SpaceOrderState state) { + if (tabController != null) { + return; + } + tabController = TabController( + length: state.tabsOrder.length, + vsync: this, + initialIndex: state.tabsOrder.indexOf(state.defaultTab), + ); + tabController?.addListener(_onTabChange); + } + + void _onTabChange() { + if (tabController == null) { + return; + } + context.read().add( + SpaceOrderEvent.open( + tabController!.index, + ), + ); + } + + List _buildTabs(SpaceOrderState state) { + return state.tabsOrder.map((tab) { + switch (tab) { + case MobileSpaceTabType.recent: + return const MobileRecentSpace(); + case MobileSpaceTabType.spaces: + return MobileHomeSpace(userProfile: widget.userProfile); + case MobileSpaceTabType.favorites: + return MobileFavoriteSpace(userProfile: widget.userProfile); + default: + throw Exception('Unknown tab type: $tab'); + } + }).toList(); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/space_order_bloc.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/space_order_bloc.dart new file mode 100644 index 0000000000000..e3c1439dd45e6 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/space_order_bloc.dart @@ -0,0 +1,127 @@ +import 'dart:convert'; + +import 'package:appflowy/core/config/kv.dart'; +import 'package:appflowy/core/config/kv_keys.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:bloc/bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'space_order_bloc.freezed.dart'; + +enum MobileSpaceTabType { + // DO NOT CHANGE THE ORDER + spaces, + recent, + favorites; + + String get tr { + switch (this) { + case MobileSpaceTabType.recent: + return LocaleKeys.sideBar_RecentSpace.tr(); + case MobileSpaceTabType.spaces: + return LocaleKeys.sideBar_Spaces.tr(); + case MobileSpaceTabType.favorites: + return LocaleKeys.sideBar_favoriteSpace.tr(); + } + } +} + +class SpaceOrderBloc extends Bloc { + SpaceOrderBloc() : super(const SpaceOrderState()) { + on( + (event, emit) async { + await event.when( + initial: () async { + final tabsOrder = await _getTabsOrder(); + final defaultTab = await _getDefaultTab(); + emit( + state.copyWith( + tabsOrder: tabsOrder, + defaultTab: defaultTab, + isLoading: false, + ), + ); + }, + open: (index) async { + final tab = state.tabsOrder[index]; + await _setDefaultTab(tab); + }, + reorder: (from, to) async { + final tabsOrder = List.of(state.tabsOrder); + tabsOrder.insert(to, tabsOrder.removeAt(from)); + await _setTabsOrder(tabsOrder); + emit(state.copyWith(tabsOrder: tabsOrder)); + }, + ); + }, + ); + } + + final _storage = getIt(); + + Future _getDefaultTab() async { + try { + return await _storage.getWithFormat( + KVKeys.lastOpenedSpace, (value) { + return MobileSpaceTabType.values[int.parse(value)]; + }) ?? + MobileSpaceTabType.spaces; + } catch (e) { + return MobileSpaceTabType.spaces; + } + } + + Future _setDefaultTab(MobileSpaceTabType tab) async { + await _storage.set( + KVKeys.lastOpenedSpace, + tab.index.toString(), + ); + } + + Future> _getTabsOrder() async { + try { + return await _storage.getWithFormat>( + KVKeys.spaceOrder, (value) { + final order = jsonDecode(value).cast(); + if (order.isEmpty) { + return MobileSpaceTabType.values; + } + return order + .map((e) => MobileSpaceTabType.values[e]) + .cast() + .toList(); + }) ?? + MobileSpaceTabType.values; + } catch (e) { + return MobileSpaceTabType.values; + } + } + + Future _setTabsOrder(List tabsOrder) async { + await _storage.set( + KVKeys.spaceOrder, + jsonEncode(tabsOrder.map((e) => e.index).toList()), + ); + } +} + +@freezed +class SpaceOrderEvent with _$SpaceOrderEvent { + const factory SpaceOrderEvent.initial() = Initial; + const factory SpaceOrderEvent.open(int index) = Open; + const factory SpaceOrderEvent.reorder(int from, int to) = Reorder; +} + +@freezed +class SpaceOrderState with _$SpaceOrderState { + const factory SpaceOrderState({ + @Default(MobileSpaceTabType.spaces) MobileSpaceTabType defaultTab, + @Default(MobileSpaceTabType.values) List tabsOrder, + @Default(true) bool isLoading, + }) = _SpaceOrderState; + + factory SpaceOrderState.initial() => const SpaceOrderState(); +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart index 4745b248c3b49..496c5607a583f 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; @@ -9,6 +7,7 @@ import 'package:appflowy/workspace/presentation/settings/widgets/members/workspa import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; // Only works on mobile. @@ -106,6 +105,7 @@ class _WorkspaceMenuItem extends StatelessWidget { leftIcon: WorkspaceIcon( enableEdit: false, iconSize: 26, + fontSize: 16.0, workspace: workspace, onSelected: (result) => context.read().add( UserWorkspaceEvent.updateWorkspaceIcon( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart b/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart index 101a546294b75..3115428eb5264 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart @@ -20,49 +20,40 @@ class MobileBottomNavigationBar extends StatelessWidget { return Scaffold( body: navigationShell, - bottomNavigationBar: BottomNavigationBar( - showSelectedLabels: false, - showUnselectedLabels: false, - enableFeedback: true, - type: BottomNavigationBarType.fixed, - items: [ - BottomNavigationBarItem( - // There is no text shown on the bottom navigation bar, but Exception will be thrown if label is null here. - label: 'home', - icon: const FlowySvg(FlowySvgs.m_home_unselected_lg), - activeIcon: FlowySvg( - FlowySvgs.m_home_selected_lg, - color: style.colorScheme.primary, + bottomNavigationBar: Theme( + data: Theme.of(context).copyWith( + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + ), + child: BottomNavigationBar( + showSelectedLabels: false, + showUnselectedLabels: false, + enableFeedback: false, + type: BottomNavigationBarType.fixed, + elevation: 0, + items: [ + const BottomNavigationBarItem( + label: 'home', + icon: FlowySvg(FlowySvgs.m_home_unselected_m), + activeIcon: + FlowySvg(FlowySvgs.m_home_selected_m, blendMode: null), ), - ), - const BottomNavigationBarItem( - label: 'favorite', - icon: FlowySvg(FlowySvgs.m_favorite_unselected_lg), - activeIcon: FlowySvg( - FlowySvgs.m_favorite_selected_lg, - blendMode: null, + const BottomNavigationBarItem( + label: 'add', + icon: FlowySvg(FlowySvgs.m_home_add_m), ), - ), - // Enable this when search is ready. - // BottomNavigationBarItem( - // label: 'search', - // icon: const FlowySvg(FlowySvgs.m_search_lg), - // activeIcon: FlowySvg( - // FlowySvgs.m_search_lg, - // color: style.colorScheme.primary, - // ), - // ), - BottomNavigationBarItem( - label: 'notification', - icon: const FlowySvg(FlowySvgs.m_notification_unselected_lg), - activeIcon: FlowySvg( - FlowySvgs.m_notification_selected_lg, - color: style.colorScheme.primary, + BottomNavigationBarItem( + label: 'notification', + icon: const FlowySvg(FlowySvgs.m_home_notification_m), + activeIcon: FlowySvg( + FlowySvgs.m_home_notification_m, + color: style.colorScheme.primary, + ), ), - ), - ], - currentIndex: navigationShell.currentIndex, - onTap: (int bottomBarIndex) => _onTap(context, bottomBarIndex), + ], + currentIndex: navigationShell.currentIndex, + onTap: (int bottomBarIndex) => _onTap(context, bottomBarIndex), + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart index 34fd5176133f3..7bd7a50157869 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,31 +1,30 @@ -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/application/mobile_router.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/page_item/mobile_view_item_add_button.dart'; -import 'package:appflowy/plugins/base/emoji/emoji_text.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/view/view_bloc.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/view/draggable_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_bloc/flutter_bloc.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; typedef ViewItemOnSelected = void Function(ViewPB); typedef ActionPaneBuilder = ActionPane Function(BuildContext context); -const _itemHeight = 48.0; - class MobileViewItem extends StatelessWidget { const MobileViewItem({ 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, @@ -178,48 +177,10 @@ class InnerMobileViewItem extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ child, - const Divider( - height: 1, - ), ...children, ], ); - } else { - child = Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - child, - const Divider( - height: 1, - ), - Container( - height: _itemHeight, - alignment: Alignment.centerLeft, - child: Padding( - padding: EdgeInsets.only(left: (level + 2) * leftPadding), - child: FlowyText.medium( - LocaleKeys.noPagesInside.tr(), - color: Colors.grey, - ), - ), - ), - const Divider( - height: 1, - ), - ], - ); } - } else { - child = Column( - mainAxisSize: MainAxisSize.min, - children: [ - child, - const Divider( - height: 1, - ), - ], - ); } // wrap the child with DraggableItem if isDraggable is true @@ -227,7 +188,6 @@ class InnerMobileViewItem extends StatelessWidget { child = DraggableViewItem( isFirstChild: isFirstChild, view: view, - // FIXME: use better color centerHighlightColor: Colors.blue.shade200, topHighlightColor: Colors.blue.shade200, bottomHighlightColor: Colors.blue.shade200, @@ -235,7 +195,7 @@ class InnerMobileViewItem extends StatelessWidget { return MobileViewItem( view: view, parentView: parentView, - categoryType: categoryType, + spaceType: spaceType, level: level, onSelected: onSelected, isDraggable: false, @@ -262,7 +222,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 +242,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; @@ -297,15 +257,14 @@ class _SingleMobileInnerViewItemState extends State { final children = [ // expand icon _buildLeftIcon(), - const HSpace(4), // icon _buildViewIcon(), const HSpace(8), // title Expanded( - child: FlowyText.medium( + child: FlowyText.regular( widget.view.name, - fontSize: 18.0, + fontSize: 16.0, overflow: TextOverflow.ellipsis, ), ), @@ -314,10 +273,11 @@ class _SingleMobileInnerViewItemState extends State { // hover action // ··· more action button - // children.add(_buildViewMoreActionButton(context)); + children.add(_buildViewMoreButton(context)); // only support add button for document layout if (!widget.isFeedback && widget.view.layout == ViewLayoutPB.Document) { // + button + children.add(_buildViewAddButton(context)); } @@ -325,7 +285,7 @@ class _SingleMobileInnerViewItemState extends State { borderRadius: BorderRadius.circular(4.0), onTap: () => widget.onSelected(widget.view), child: SizedBox( - height: _itemHeight, + height: HomeSpaceViewSizes.mViewHeight, child: Padding( padding: EdgeInsets.only(left: widget.level * widget.leftPadding), child: Row( @@ -350,12 +310,12 @@ class _SingleMobileInnerViewItemState extends State { Widget _buildViewIcon() { final icon = widget.view.icon.value.isNotEmpty - ? EmojiText( - emoji: widget.view.icon.value, - fontSize: 24.0, + ? FlowyText.emoji( + widget.view.icon.value, + fontSize: 20.0, ) : SizedBox.square( - dimension: 26.0, + dimension: 18.0, child: widget.view.defaultIcon(), ); return icon; @@ -365,18 +325,20 @@ class _SingleMobileInnerViewItemState extends State { // show > if the view is expandable. // show · if the view can't contain child views. Widget _buildLeftIcon() { - if (isReferencedDatabaseView(widget.view, widget.parentView)) { - return const _DotIconWidget(); + if (context.read().state.view.childViews.isEmpty) { + return HSpace(widget.leftPadding); } return GestureDetector( - child: AnimatedRotation( - duration: const Duration(milliseconds: 250), - turns: widget.isExpanded ? 0 : -0.25, - child: const Icon( - Icons.keyboard_arrow_down_rounded, - size: 28, - ), + behavior: HitTestBehavior.opaque, + child: Padding( + padding: const EdgeInsets.only(right: 6.0, top: 6.0, bottom: 6.0), + child: FlowySvg( + widget.isExpanded + ? FlowySvgs.m_expand_s + : FlowySvgs.m_collapse_s, + blendMode: null, + ), ), onTap: () { context @@ -407,10 +369,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, ), ); }, @@ -420,23 +381,49 @@ class _SingleMobileInnerViewItemState extends State { }, ); } -} -class _DotIconWidget extends StatelessWidget { - const _DotIconWidget(); + // + button + Widget _buildViewMoreButton(BuildContext context) { + return MobileViewMoreButton(onPressed: () => _showMoreActions(context)); + } - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(6.0), - child: Container( - width: 4, - height: 4, - decoration: BoxDecoration( - color: Theme.of(context).iconTheme.color, - borderRadius: BorderRadius.circular(2), - ), - ), + Future _showMoreActions(BuildContext context) async { + final viewBloc = context.read(); + final favoriteBloc = context.read(); + await showMobileBottomSheet( + context, + showHeader: true, + title: widget.view.name, + showDragHandle: true, + showCloseButton: true, + useRootNavigator: true, + builder: (context) { + return MultiBlocProvider( + providers: [ + BlocProvider.value(value: viewBloc), + BlocProvider.value(value: favoriteBloc), + ], + child: BlocBuilder( + builder: (context, state) { + final isFavorite = state.view.isFavorite; + return MobileViewItemBottomSheet( + view: viewBloc.state.view, + actions: [ + isFavorite + ? MobileViewItemBottomSheetBodyAction.removeFromFavorites + : MobileViewItemBottomSheetBodyAction.addToFavorites, + MobileViewItemBottomSheetBodyAction.divider, + MobileViewItemBottomSheetBodyAction.rename, + MobileViewItemBottomSheetBodyAction.divider, + MobileViewItemBottomSheetBodyAction.duplicate, + MobileViewItemBottomSheetBodyAction.divider, + MobileViewItemBottomSheetBodyAction.delete, + ], + ); + }, + ), + ); + }, ); } } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item_add_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item_add_button.dart index eb2f8ea9f8876..77bb57773fc0a 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item_add_button.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item_add_button.dart @@ -1,9 +1,8 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -const _iconSize = 32.0; - class MobileViewAddButton extends StatelessWidget { const MobileViewAddButton({ super.key, @@ -15,12 +14,31 @@ class MobileViewAddButton extends StatelessWidget { @override Widget build(BuildContext context) { return FlowyIconButton( - iconPadding: const EdgeInsets.all(2), - width: _iconSize, - height: _iconSize, + width: HomeSpaceViewSizes.mViewButtonDimension, + height: HomeSpaceViewSizes.mViewButtonDimension, + icon: const FlowySvg( + FlowySvgs.m_space_add_s, + ), + onPressed: onPressed, + ); + } +} + +class MobileViewMoreButton extends StatelessWidget { + const MobileViewMoreButton({ + super.key, + required this.onPressed, + }); + + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return FlowyIconButton( + width: HomeSpaceViewSizes.mViewButtonDimension, + height: HomeSpaceViewSizes.mViewButtonDimension, icon: const FlowySvg( - FlowySvgs.add_s, - size: Size.square(_iconSize), + FlowySvgs.m_space_more_s, ), onPressed: onPressed, ); 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 39a0fdae4c055..e61281c5c4cff 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 e3526c3df03cd..3bdb836a71861 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 1291804af6e7b..8893eab1059b5 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 64dd62729c7f8..390f0824de590 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 17b61849da07e..5be3d3e78c610 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 @@ -1,7 +1,6 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; class MobileQuickActionButton extends StatelessWidget { const MobileQuickActionButton({ @@ -29,23 +28,23 @@ 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, + height: 52, padding: const EdgeInsets.symmetric(horizontal: 12), child: Row( children: [ FlowySvg( icon, - size: const Size.square(20), + size: const Size.square(18), color: enable ? iconColor : Theme.of(context).disabledColor, ), const HSpace(12), Expanded( - child: FlowyText( + child: FlowyText.regular( text, - fontSize: 15, + fontSize: 16, color: enable ? textColor : Theme.of(context).disabledColor, ), ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart index 5a481eaa6867f..321632a36a490 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart @@ -1,6 +1,8 @@ import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/startup/tasks/app_widget.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'; enum ConfirmDialogActionAlignment { @@ -85,3 +87,46 @@ Future showFlowyMobileConfirmDialog( }, ); } + +Future showFlowyCupertinoConfirmDialog({ + BuildContext? context, + required String title, + required Widget leftButton, + required Widget rightButton, + void Function(BuildContext context)? onLeftButtonPressed, + void Function(BuildContext context)? onRightButtonPressed, +}) { + return showDialog( + context: context ?? AppGlobals.context, + builder: (context) => CupertinoAlertDialog( + title: FlowyText.medium( + title, + fontSize: 18, + maxLines: 10, + lineHeight: 1.3, + ), + actions: [ + CupertinoDialogAction( + onPressed: () { + if (onLeftButtonPressed != null) { + onLeftButtonPressed(context); + } else { + Navigator.of(context).pop(); + } + }, + child: leftButton, + ), + CupertinoDialogAction( + onPressed: () { + if (onRightButtonPressed != null) { + onRightButtonPressed(context); + } else { + Navigator.of(context).pop(); + } + }, + child: rightButton, + ), + ], + ), + ); +} 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 1297bccc379fc..d7a7b9ceb22a9 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart @@ -1,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 9619f00d30dc6..9f05c80f09ada 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_header.dart @@ -16,9 +16,14 @@ class FlowyEmojiHeader extends StatelessWidget { if (PlatformExtension.isDesktopOrWeb) { return Container( height: 22, - padding: const EdgeInsets.symmetric(horizontal: 8.0), color: Theme.of(context).cardColor, - child: FlowyText.regular(category.id), + child: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: FlowyText.regular( + category.id, + color: Theme.of(context).hintColor, + ), + ), ); } else { return Column( diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart index 1b01e6aee8d05..c6cec89ecc8bc 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart @@ -42,7 +42,7 @@ class _FlowyEmojiSearchBarState extends State { Widget build(BuildContext context) { return Padding( padding: EdgeInsets.symmetric( - vertical: 8.0, + vertical: 12.0, horizontal: PlatformExtension.isDesktopOrWeb ? 0.0 : 8.0, ), child: Row( @@ -52,16 +52,15 @@ class _FlowyEmojiSearchBarState extends State { onKeywordChanged: widget.onKeywordChanged, ), ), - const HSpace(6.0), + const HSpace(8.0), _RandomEmojiButton( emojiData: widget.emojiData, onRandomEmojiSelected: widget.onRandomEmojiSelected, ), - const HSpace(6.0), + const HSpace(8.0), FlowyEmojiSkinToneSelector( onEmojiSkinToneChanged: widget.onSkinToneChanged, ), - const HSpace(6.0), ], ), ); @@ -79,20 +78,30 @@ class _RandomEmojiButton extends StatelessWidget { @override Widget build(BuildContext context) { - return FlowyTooltip( - message: LocaleKeys.emoji_random.tr(), - child: FlowyButton( - useIntrinsicWidth: true, - text: const Icon( - Icons.shuffle_rounded, + return Container( + width: 36, + height: 36, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: const BorderSide(color: Color(0x1E171717)), + borderRadius: BorderRadius.circular(8), + ), + ), + child: FlowyTooltip( + message: LocaleKeys.emoji_random.tr(), + child: FlowyButton( + useIntrinsicWidth: true, + text: const FlowySvg( + FlowySvgs.icon_shuffle_s, + ), + onTap: () { + final random = emojiData.random; + onRandomEmojiSelected( + random.$1, + random.$2, + ); + }, ), - onTap: () { - final random = emojiData.random; - onRandomEmojiSelected( - random.$1, - random.$2, - ); - }, ), ); } @@ -123,32 +132,35 @@ class _SearchTextFieldState extends State<_SearchTextField> { @override Widget build(BuildContext context) { - return ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: 32.0, - ), + return SizedBox( + height: 36.0, child: FlowyTextField( focusNode: focusNode, - hintText: LocaleKeys.emoji_search.tr(), + hintText: LocaleKeys.search_label.tr(), + hintStyle: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 14.0, + fontWeight: FontWeight.w400, + color: Theme.of(context).hintColor, + ), controller: controller, onChanged: widget.onKeywordChanged, prefixIcon: const Padding( padding: EdgeInsets.only( - left: 8.0, - right: 4.0, + left: 14.0, + right: 8.0, ), child: FlowySvg( FlowySvgs.search_s, ), ), prefixIconConstraints: const BoxConstraints( - maxHeight: 18.0, + maxHeight: 20.0, ), suffixIcon: Padding( padding: const EdgeInsets.all(4.0), child: FlowyButton( text: const FlowySvg( - FlowySvgs.close_lg, + FlowySvgs.m_app_bar_close_s, ), margin: EdgeInsets.zero, useIntrinsicWidth: true, diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart index e8da112660b47..3add90773de81 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart @@ -57,7 +57,7 @@ class _FlowyEmojiSkinToneSelectorState child: FlowyTooltip( message: LocaleKeys.emoji_selectSkinTone.tr(), child: _buildIconButton( - lastSelectedEmojiSkinTone?.icon ?? '✋', + lastSelectedEmojiSkinTone?.icon ?? '👋', () => controller.show(), ), ), @@ -65,19 +65,22 @@ class _FlowyEmojiSkinToneSelectorState } Widget _buildIconButton(String icon, VoidCallback onPressed) { - return FlowyIconButton( - key: emojiSkinToneKey(icon), - icon: Padding( - // add a left padding to align the emoji center - padding: const EdgeInsets.only( - left: 3.0, - ), - child: FlowyText( + return Container( + width: 36, + height: 36, + decoration: BoxDecoration( + border: Border.all(color: const Color(0x1E171717)), + borderRadius: BorderRadius.circular(8), + ), + child: FlowyButton( + key: emojiSkinToneKey(icon), + margin: EdgeInsets.zero, + text: FlowyText.emoji( icon, - fontSize: 22.0, + fontSize: 24.0, ), + onTap: onPressed, ), - onPressed: onPressed, ); } } @@ -86,17 +89,17 @@ extension EmojiSkinToneIcon on EmojiSkinTone { String get icon { switch (this) { case EmojiSkinTone.none: - return '✋'; + return '👋'; case EmojiSkinTone.light: - return '✋🏻'; + return '👋🏻'; case EmojiSkinTone.mediumLight: - return '✋🏼'; + return '👋🏼'; case EmojiSkinTone.medium: - return '✋🏽'; + return '👋🏽'; case EmojiSkinTone.mediumDark: - return '✋🏾'; + return '👋🏾'; case EmojiSkinTone.dark: - return '✋🏿'; + return '👋🏿'; } } } diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_text.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_text.dart index e9fed800d48df..5481c7676ad55 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_text.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_text.dart @@ -29,6 +29,7 @@ class EmojiText extends StatelessWidget { emoji, fontSize: fontSize, textAlign: textAlign, + strutStyle: const StrutStyle(forceStrutHeight: true), fallbackFontFamily: _cachedFallbackFontFamily, lineHeight: lineHeight, ); diff --git a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart index a77b4b2f2719b..08e63251e0257 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart @@ -1,13 +1,10 @@ -import 'package:flutter/material.dart'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/icon.pbenum.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flutter/material.dart'; extension ToProto on FlowyIconType { ViewIconTypePB toProto() { @@ -54,57 +51,28 @@ class FlowyIconPicker extends StatelessWidget { @override Widget build(BuildContext context) { - // ONLY supports emoji picker for now - return DefaultTabController( - length: 1, + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ + const VSpace(8.0), Row( children: [ - _buildTabs(context), + FlowyText(LocaleKeys.newSettings_workplace_chooseAnIcon.tr()), const Spacer(), _RemoveIconButton( onTap: () => onSelected(EmojiPickerResult.none()), ), ], ), - const Divider(height: 2), + const VSpace(12.0), + const Divider(height: 0.5), Expanded( - child: TabBarView( - children: [ - FlowyEmojiPicker( - emojiPerLine: _getEmojiPerLine(context), - onEmojiSelected: (_, emoji) => - onSelected(EmojiPickerResult.emoji(emoji)), - ), - ], - ), - ), - ], - ), - ); - } - - Widget _buildTabs(BuildContext context) { - return Align( - alignment: Alignment.centerLeft, - child: TabBar( - indicatorSize: TabBarIndicatorSize.label, - isScrollable: true, - overlayColor: 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 64415ff85bd25..a1f3a78f75e85 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 5d1a29141e4ea..6dfa3313f56b9 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 6baf2ecd2f595..e105914908aef 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 c38c7647baf41..ff695eea0e811 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 29af8e4355477..7c18bb927f8ad 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 55394ec33c887..ddddd90fb4d38 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 d8aa978e12518..787e08760b0c6 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart @@ -236,7 +236,8 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder { final String? initialRowId; @override - Widget get leftBarItem => ViewTitleBar(view: notifier.view); + Widget get leftBarItem => + ViewTitleBar(key: ValueKey(notifier.view.id), view: notifier.view); @override Widget tabBarItem(String pluginId) => ViewTabBarItem(view: notifier.view); @@ -278,7 +279,7 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder { ] : [], DatabaseShareButton(key: ValueKey(view.id), view: view), - const HSpace(4), + const HSpace(10), ViewFavoriteButton(view: view), const HSpace(4), MoreViewActions(view: view, isDocument: false), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/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 0e411440efc92..5c31d41b30eee 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 4dffae30221c5..cddb8219431a4 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 23b5c31c755c2..2e9e4b1a24987 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 dd981795d2a54..67f9f1c53f9a5 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 eebb3e1c75f43..cdbcef64c74b6 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 f97eabe8306bd..f87b22549272c 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 3ab883329a20c..18c9428eded92 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 70383361d7ed1..63ea44008efe0 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 3094c97887bde..d3a41c6c3fc02 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/row/row_property.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart index 969c4e6e0c054..8762918141b67 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 d5f3ce293a1b1..f3a548932dff6 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 d6c84e39fe481..7ede902085e73 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/share_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/share_button.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/share_bloc.dart'; import 'package:appflowy/startup/startup.dart'; @@ -13,6 +11,7 @@ import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:flowy_infra_ui/widget/rounded_button.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class DatabaseShareButton extends StatelessWidget { @@ -39,11 +38,7 @@ class DatabaseShareButton extends StatelessWidget { ); }, child: BlocBuilder( - builder: (context, state) => ConstrainedBox( - constraints: const BoxConstraints.expand( - height: 30, - width: 100, - ), + builder: (context, state) => IntrinsicWidth( child: DatabaseShareActionList(view: view), ), ), @@ -106,6 +101,8 @@ class DatabaseShareActionListState extends State { onPointerDown: (_) => controller.show(), child: RoundedTextButton( title: LocaleKeys.shareAction_buttonText.tr(), + padding: const EdgeInsets.symmetric(horizontal: 12.0), + fontSize: 14.0, textColor: Theme.of(context).colorScheme.onPrimary, onPressed: () {}, ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/document.dart b/frontend/appflowy_flutter/lib/plugins/document/document.dart index 13242bbe0a9e8..bd9e4891d297c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document.dart @@ -1,7 +1,5 @@ library document_plugin; -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; @@ -22,6 +20,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class DocumentPluginBuilder extends PluginBuilder { @@ -130,7 +129,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder } @override - Widget get leftBarItem => ViewTitleBar(view: view); + Widget get leftBarItem => ViewTitleBar(key: ValueKey(view.id), view: view); @override Widget tabBarItem(String pluginId) => ViewTabBarItem(view: notifier.view); @@ -162,7 +161,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder key: ValueKey('share_button_${view.id}'), view: view, ), - const HSpace(4), + const HSpace(10), ViewFavoriteButton( key: ValueKey('favorite_button_${view.id}'), view: view, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/banner.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/banner.dart index 3936bf69684f6..e5fd6b6b8be17 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 39cd608d0e01b..966a5685f5973 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart @@ -5,6 +5,8 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mo import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_block_copy_button.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/video/video_menu.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/video/video_placeholder.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; @@ -53,9 +55,8 @@ Map getEditorBuilderMap({ configuration: configuration.copyWith( placeholderText: (_) => LocaleKeys.blockPlaceholders_todoList.tr(), ), - iconBuilder: PlatformExtension.isMobile - ? (_, node, onCheck) => TodoListIcon(node: node, onCheck: onCheck) - : null, + iconBuilder: (_, node, onCheck) => + TodoListIcon(node: node, onCheck: onCheck), toggleChildrenTriggers: [ LogicalKeyboardKey.shift, LogicalKeyboardKey.shiftLeft, @@ -66,18 +67,14 @@ Map getEditorBuilderMap({ configuration: configuration.copyWith( placeholderText: (_) => LocaleKeys.blockPlaceholders_bulletList.tr(), ), - iconBuilder: PlatformExtension.isMobile - ? (_, node) => BulletedListIcon(node: node) - : null, + iconBuilder: (_, node) => BulletedListIcon(node: node), ), NumberedListBlockKeys.type: NumberedListBlockComponentBuilder( configuration: configuration.copyWith( placeholderText: (_) => LocaleKeys.blockPlaceholders_numberList.tr(), ), - iconBuilder: PlatformExtension.isMobile - ? (_, node, textDirection) => - NumberedListIcon(node: node, textDirection: textDirection) - : null, + iconBuilder: (_, node, textDirection) => + NumberedListIcon(node: node, textDirection: textDirection), ), QuoteBlockKeys.type: QuoteBlockComponentBuilder( configuration: configuration.copyWith( @@ -99,9 +96,13 @@ Map getEditorBuilderMap({ return const EdgeInsets.only(top: 12.0, bottom: 4.0); }, - placeholderText: (node) => LocaleKeys.blockPlaceholders_heading.tr( - args: [node.attributes[HeadingBlockKeys.level].toString()], - ), + placeholderText: (node) { + int level = node.attributes[HeadingBlockKeys.level] ?? 6; + level = level.clamp(1, 6); + return LocaleKeys.blockPlaceholders_heading.tr( + args: [level.toString()], + ); + }, ), textStyleBuilder: (level) => styleCustomizer.headingStyleBuilder(level), ), @@ -110,7 +111,7 @@ Map getEditorBuilderMap({ showMenu: true, menuBuilder: (Node node, CustomImageBlockComponentState state) => Positioned( - top: 0, + top: 10, right: 10, child: ImageMenu(node: node, state: state), ), @@ -163,7 +164,10 @@ Map getEditorBuilderMap({ ), ), CalloutBlockKeys.type: CalloutBlockComponentBuilder( - configuration: configuration, + configuration: configuration.copyWith( + textStyle: (_) => styleCustomizer.calloutBlockStyleBuilder(), + placeholderTextStyle: (_) => styleCustomizer.calloutBlockStyleBuilder(), + ), defaultColor: calloutBGColor, ), DividerBlockKeys.type: DividerBlockComponentBuilder( @@ -180,7 +184,6 @@ Map getEditorBuilderMap({ configuration: configuration, ), CodeBlockKeys.type: CodeBlockComponentBuilder( - editorState: editorState, configuration: configuration.copyWith( textStyle: (_) => styleCustomizer.codeBlockStyleBuilder(), placeholderTextStyle: (_) => styleCustomizer.codeBlockStyleBuilder(), @@ -228,6 +231,16 @@ Map getEditorBuilderMap({ errorBlockComponentBuilderKey: ErrorBlockComponentBuilder( configuration: configuration, ), + VideoBlockKeys.type: VideoBlockComponentBuilder( + configuration: configuration, + showMenu: true, + menuBuilder: (node, state) => Positioned( + top: 10, + right: 10, + child: VideoMenu(node: node, state: state), + ), + placeholderBuilder: (node) => VideoPlaceholder(node: node), + ), }; final builders = { 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 0b7a473176ac3..996aa4ced1ce0 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. @@ -385,6 +388,7 @@ class _AppFlowyEditorPageState extends State { emojiMenuItem, autoGeneratorMenuItem, dateMenuItem, + videoBlockItem(LocaleKeys.document_plugins_video_label.tr()), ]; } @@ -392,12 +396,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 +405,7 @@ class _AppFlowyEditorPageState extends State { final customizeShortcuts = await settingsShortcutService.getCustomizeShortcuts(); await settingsShortcutService.updateCommandShortcuts( - commandShortcutEvents, + cmdShortcutEvents, customizeShortcuts, ); } @@ -437,7 +435,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 eb4487fa490f1..b80bb034a641e 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 09906e1429d36..d5e99e13f8fd3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart @@ -141,7 +141,7 @@ enum OptionDepthType { class DividerOptionAction extends CustomActionCell { @override - Widget buildWithContext(BuildContext context) { + Widget buildWithContext(BuildContext context, PopoverController controller) { return const Divider( height: 1.0, thickness: 1.0, @@ -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 df78f6261efa2..2a1101794a6ec 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart @@ -1,11 +1,10 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; class EmojiPickerButton extends StatelessWidget { @@ -19,6 +18,7 @@ class EmojiPickerButton extends StatelessWidget { this.offset, this.direction, this.title, + this.showBorder = true, }); final String emoji; @@ -30,6 +30,7 @@ class EmojiPickerButton extends StatelessWidget { final Offset? offset; final PopoverDirection? direction; final String? title; + final bool showBorder; @override Widget build(BuildContext context) { @@ -51,22 +52,28 @@ class EmojiPickerButton extends StatelessWidget { onExit: () {}, ), ), - child: emoji.isEmpty && defaultIcon != null - ? FlowyButton( - useIntrinsicWidth: true, - text: defaultIcon!, - onTap: popoverController.show, - ) - : FlowyTextButton( - emoji, - overflow: TextOverflow.visible, - fontSize: emojiSize, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(minWidth: 35.0), - fillColor: Colors.transparent, - mainAxisAlignment: MainAxisAlignment.center, - onPressed: popoverController.show, - ), + child: Container( + width: 30.0, + height: 30.0, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: showBorder + ? Border.all( + color: Theme.of(context).dividerColor, + ) + : null, + ), + child: FlowyButton( + margin: emoji.isEmpty && defaultIcon != null + ? EdgeInsets.zero + : const EdgeInsets.only(left: 2.0), + expandText: false, + text: emoji.isEmpty && defaultIcon != null + ? defaultIcon! + : FlowyText.emoji(emoji, fontSize: emojiSize), + onTap: popoverController.show, + ), + ), ); } return FlowyTextButton( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/bulleted_list/bulleted_list_icon.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/bulleted_list/bulleted_list_icon.dart index d0551b03af616..abb166ec12890 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/bulleted_list/bulleted_list_icon.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/bulleted_list/bulleted_list_icon.dart @@ -37,7 +37,9 @@ class BulletedListIcon extends StatelessWidget { @override Widget build(BuildContext context) { - final iconPadding = context.read().state.iconPadding; + final iconPadding = PlatformExtension.isMobile + ? context.read().state.iconPadding + : 0.0; return Container( constraints: const BoxConstraints( minWidth: 22, 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 323c15be1e297..97ac54e9f1930 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/cover/document_immersive_cover.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart index 741b3c16ee4a4..92f04719a4cec 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart @@ -1,12 +1,14 @@ import 'dart:io'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; -import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; -import 'package:appflowy/plugins/base/icon/icon_picker.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/plugins/document/application/prelude.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/build_context_extension.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/icon/icon_selector.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon_bloc.dart'; import 'package:appflowy/shared/appflowy_network_image.dart'; import 'package:appflowy/shared/flowy_gradient_colors.dart'; import 'package:appflowy/shared/google_fonts_extension.dart'; @@ -16,11 +18,13 @@ import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:auto_size_text_field/auto_size_text_field.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/widget/ignore_parent_gesture.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; double kDocumentCoverHeight = 98.0; double kDocumentTitlePadding = 20.0; @@ -133,9 +137,11 @@ class _DocumentImmersiveCoverState extends State { if (documentFontFamily != null && fontFamily != documentFontFamily) { fontFamily = getGoogleFontSafely(documentFontFamily).fontFamily; } - return TextField( + + return AutoSizeTextField( controller: textEditingController, focusNode: focusNode, + minFontSize: 18.0, decoration: const InputDecoration( border: InputBorder.none, enabledBorder: InputBorder.none, @@ -151,6 +157,7 @@ class _DocumentImmersiveCoverState extends State { fontFamily: fontFamily, color: state.cover.isNone || state.cover.isPresets ? null : Colors.white, + overflow: TextOverflow.ellipsis, ), onChanged: _rename, onSubmitted: _rename, @@ -167,12 +174,40 @@ class _DocumentImmersiveCoverState extends State { ), ), onTap: () async { - final result = await context.push( - MobileEmojiPickerScreen.routeName, + final pageStyleIconBloc = PageStyleIconBloc(view: widget.view) + ..add(const PageStyleIconEvent.initial()); + await showMobileBottomSheet( + context, + showDragHandle: true, + showDivider: false, + showDoneButton: true, + showHeader: true, + title: LocaleKeys.titleBar_pageIcon.tr(), + backgroundColor: AFThemeExtension.of(context).background, + enableDraggableScrollable: true, + minChildSize: 0.6, + initialChildSize: 0.61, + showRemoveButton: true, + onRemove: () { + pageStyleIconBloc.add( + const PageStyleIconEvent.updateIcon('', true), + ); + }, + scrollableWidgetBuilder: (_, controller) { + return BlocProvider.value( + value: pageStyleIconBloc, + child: Expanded( + child: Scrollbar( + controller: controller, + child: IconSelector( + scrollController: controller, + ), + ), + ), + ); + }, + builder: (_) => const SizedBox.shrink(), ); - if (result != null && context.mounted) { - context.read().add(ViewEvent.updateIcon(result.emoji)); - } }, ); } 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 d16c035115b3c..ba6f01e9088d5 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( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/find_and_replace/find_and_replace_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/find_and_replace/find_and_replace_menu.dart index ce756b9ffd538..7c84f2c31b34c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/find_and_replace/find_and_replace_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/find_and_replace/find_and_replace_menu.dart @@ -2,7 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/text_input.dart'; import 'package:flutter/material.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart index 7a606f33ef930..3a22909744287 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart @@ -300,9 +300,10 @@ class _DocumentHeaderToolbarState extends State { : (CoverType.color, '0xffe8e0ff'), ), useIntrinsicWidth: true, - leftIcon: const FlowySvg(FlowySvgs.image_s), + leftIcon: const FlowySvg(FlowySvgs.add_cover_s), text: FlowyText.small( LocaleKeys.document_plugins_cover_addCover.tr(), + color: Theme.of(context).hintColor, ), ), ); @@ -311,28 +312,24 @@ class _DocumentHeaderToolbarState extends State { if (widget.hasIcon) { children.add( FlowyButton( - leftIconSize: const Size.square(18), onTap: () => widget.onIconOrCoverChanged(icon: ""), useIntrinsicWidth: true, - leftIcon: const Icon( - Icons.emoji_emotions_outlined, - size: 18, - ), + leftIcon: const FlowySvg(FlowySvgs.add_icon_s), + iconPadding: 4.0, text: FlowyText.small( LocaleKeys.document_plugins_cover_removeIcon.tr(), + color: Theme.of(context).hintColor, ), ), ); } else { Widget child = FlowyButton( - leftIconSize: const Size.square(18), useIntrinsicWidth: true, - leftIcon: const Icon( - Icons.emoji_emotions_outlined, - size: 18, - ), + leftIcon: const FlowySvg(FlowySvgs.add_icon_s), + iconPadding: 4.0, text: FlowyText.small( LocaleKeys.document_plugins_cover_addIcon.tr(), + color: Theme.of(context).hintColor, ), onTap: PlatformExtension.isDesktop ? null diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/icon/icon_selector.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/icon/icon_selector.dart new file mode 100644 index 0000000000000..5da4528a3c382 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/icon/icon_selector.dart @@ -0,0 +1,159 @@ +import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_search_text_field.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon_bloc.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_emoji_mart/flutter_emoji_mart.dart'; + +class IconSelector extends StatefulWidget { + const IconSelector({ + super.key, + required this.scrollController, + }); + + final ScrollController scrollController; + + @override + State createState() => _IconSelectorState(); +} + +class _IconSelectorState extends State { + EmojiData? emojiData; + List availableEmojis = []; + + PageStyleIconBloc? pageStyleIconBloc; + + @override + void initState() { + super.initState(); + + // load the emoji data from cache if it's available + if (kCachedEmojiData != null) { + emojiData = kCachedEmojiData; + availableEmojis = _setupAvailableEmojis(emojiData!); + } else { + EmojiData.builtIn().then( + (value) { + kCachedEmojiData = value; + setState(() { + emojiData = value; + availableEmojis = _setupAvailableEmojis(value); + }); + }, + ); + } + + pageStyleIconBloc = context.read(); + } + + @override + void dispose() { + pageStyleIconBloc?.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (emojiData == null) { + return const Center(child: CircularProgressIndicator()); + } + + return RepaintBoundary( + child: BlocBuilder( + builder: (_, state) => Column( + children: [ + _buildSearchBar(context), + Expanded( + child: GridView.count( + crossAxisCount: 7, + controller: widget.scrollController, + children: [ + for (final emoji in availableEmojis) + _buildEmoji(context, emoji, state.icon), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildEmoji( + BuildContext context, + String emoji, + String? selectedEmoji, + ) { + Widget child = SizedBox.square( + dimension: 24.0, + child: Center( + child: FlowyText.emoji( + emoji, + fontSize: 24, + ), + ), + ); + + if (emoji == selectedEmoji) { + child = Center( + child: Container( + width: 40, + height: 40, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: const BorderSide( + width: 1.40, + strokeAlign: BorderSide.strokeAlignOutside, + color: Color(0xFF00BCF0), + ), + borderRadius: BorderRadius.circular(10), + ), + ), + child: child, + ), + ); + } + + return GestureDetector( + onTap: () { + context.read().add( + PageStyleIconEvent.updateIcon(emoji, true), + ); + }, + child: child, + ); + } + + List _setupAvailableEmojis(EmojiData emojiData) { + final categories = emojiData.categories; + availableEmojis = categories + .map((e) => e.emojiIds.map((e) => emojiData.getEmojiById(e))) + .expand((e) => e) + .toList(); + return availableEmojis; + } + + Widget _buildSearchBar(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 12.0, + ), + child: FlowyMobileSearchTextField( + onChanged: (keyword) { + if (emojiData == null) { + return; + } + + final filtered = emojiData!.filterByKeyword(keyword); + final availableEmojis = _setupAvailableEmojis(filtered); + + setState(() { + this.availableEmojis = availableEmojis; + }); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart index c389977333061..722a79c2f549a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart @@ -1,5 +1,7 @@ import 'dart:io'; +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'; @@ -13,7 +15,6 @@ import 'package:appflowy/util/string_extension.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide ResizableImage; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import 'package:string_validator/string_validator.dart'; @@ -109,10 +110,7 @@ class CustomImageBlockComponentBuilder extends BlockComponentBuilder { node: node, showActions: showActions(node), configuration: configuration, - actionBuilder: (context, state) => actionBuilder( - blockComponentContext, - state, - ), + actionBuilder: (_, state) => actionBuilder(blockComponentContext, state), showMenu: showMenu, menuBuilder: menuBuilder, ); 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 f193f91617698..5d51c1b390e7e 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 36d90ac6fb0b2..eda320bdb399d 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 @@ -112,7 +112,7 @@ class _UnsplashImageWidgetState extends State { } } -class _UnsplashImages extends StatelessWidget { +class _UnsplashImages extends StatefulWidget { const _UnsplashImages({ required this.type, required this.photos, @@ -123,17 +123,24 @@ class _UnsplashImages extends StatelessWidget { final List photos; final OnSelectUnsplashImage onSelectUnsplashImage; + @override + State<_UnsplashImages> createState() => _UnsplashImagesState(); +} + +class _UnsplashImagesState extends State<_UnsplashImages> { + int _selectedPhotoIndex = -1; + @override Widget build(BuildContext context) { - final crossAxisCount = switch (type) { + final crossAxisCount = switch (widget.type) { UnsplashImageType.halfScreen => 3, UnsplashImageType.fullScreen => 2, }; - final mainAxisSpacing = switch (type) { + final mainAxisSpacing = switch (widget.type) { UnsplashImageType.halfScreen => 16.0, UnsplashImageType.fullScreen => 16.0, }; - final crossAxisSpacing = switch (type) { + final crossAxisSpacing = switch (widget.type) { UnsplashImageType.halfScreen => 10.0, UnsplashImageType.fullScreen => 16.0, }; @@ -142,17 +149,23 @@ class _UnsplashImages extends StatelessWidget { mainAxisSpacing: mainAxisSpacing, crossAxisSpacing: crossAxisSpacing, childAspectRatio: 4 / 3, - children: photos - .map( - (photo) => _UnsplashImage( - type: type, - photo: photo, - onTap: () => onSelectUnsplashImage( - photo.urls.regular.toString(), - ), - ), - ) - .toList(), + children: widget.photos.asMap().entries.map((entry) { + final index = entry.key; + final photo = entry.value; + return _UnsplashImage( + type: widget.type, + photo: photo, + onTap: () { + widget.onSelectUnsplashImage( + photo.urls.regular.toString(), + ); + setState(() { + _selectedPhotoIndex = index; + }); + }, + isSelected: index == _selectedPhotoIndex, + ); + }).toList(), ); } } @@ -162,11 +175,13 @@ class _UnsplashImage extends StatelessWidget { required this.type, required this.photo, required this.onTap, + required this.isSelected, }); final UnsplashImageType type; final Photo photo; final VoidCallback onTap; + final bool isSelected; @override Widget build(BuildContext context) { @@ -177,7 +192,19 @@ class _UnsplashImage extends StatelessWidget { return GestureDetector( onTap: onTap, - child: child, + child: isSelected + ? Container( + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: const BorderSide(width: 1.50, color: Color(0xFF00BCF0)), + borderRadius: BorderRadius.circular(8.0), + ), + ), + padding: const EdgeInsets.all(2.0), + child: child, + ) + : child, ); } 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 3403a1ff31b54..017e5a94b287d 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 4c9de6b07d404..0679f879962a8 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 543cee120757b..7ce143acba4d3 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 56a9739531fa5..c7d298ff09add 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 21f3b7ea68bd4..ad4d52381287b 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 2de13ed4590e2..438aa1264b6d9 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/mobile_toolbar_v3/add_block_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_toolbar_item.dart index d0be5af466aee..7ce86c3035747 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_toolbar_item.dart @@ -197,6 +197,15 @@ class _AddBlockMenu extends StatelessWidget { }, ), + // video + TypeOptionMenuItemValue( + value: VideoBlockKeys.type, + backgroundColor: colorMap[VideoBlockKeys.type]!, + text: LocaleKeys.document_plugins_video_label.tr(), + icon: FlowySvgs.m_add_block_video_s, + onTap: (_, __) => _insertBlock(videoBlockNode()), + ), + // date TypeOptionMenuItemValue( value: ParagraphBlockKeys.type, @@ -287,6 +296,7 @@ class _AddBlockMenu extends StatelessWidget { NumberedListBlockKeys.type: const Color(0xFFA35F94), ToggleListBlockKeys.type: const Color(0xFFA35F94), ImageBlockKeys.type: const Color(0xFFBAAC74), + VideoBlockKeys.type: const Color(0xFFBAAC74), MentionBlockKeys.type: const Color(0xFF40AAB8), DividerBlockKeys.type: const Color(0xFF4BB299), CalloutBlockKeys.type: const Color(0xFF66599B), @@ -303,6 +313,7 @@ class _AddBlockMenu extends StatelessWidget { NumberedListBlockKeys.type: const Color(0xFFFFB9EF), ToggleListBlockKeys.type: const Color(0xFFFFB9EF), ImageBlockKeys.type: const Color(0xFFFDEDA7), + VideoBlockKeys.type: const Color(0xFFFDEDA7), MentionBlockKeys.type: const Color(0xFF91EAF5), DividerBlockKeys.type: const Color(0xFF98F4CD), CalloutBlockKeys.type: const Color(0xFFCABDFF), 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 214fb7456f763..fd0aa86fa7df7 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,7 +189,7 @@ class PageStyleCoverImage extends StatelessWidget { ); }, title: LocaleKeys.pageStyle_presets.tr(), - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: AFThemeExtension.of(context).background, builder: (_) { return BlocProvider.value( value: pageStyleBloc, @@ -256,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( @@ -266,7 +270,7 @@ class PageStyleCoverImage extends StatelessWidget { showHeader: true, showRemoveButton: true, title: LocaleKeys.pageStyle_unsplash.tr(), - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: backgroundColor, onRemove: () { pageStyleBloc.add( DocumentPageStyleEvent.updateCoverImage( @@ -277,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 a3ccc974f7de6..3f3ed875228dd 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 @@ -1,16 +1,15 @@ 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'; -import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_search_text_field.dart'; -import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/icon/icon_selector.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon_bloc.dart'; 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'; -import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; import 'package:go_router/go_router.dart'; class PageStyleIcon extends StatefulWidget { @@ -77,8 +76,7 @@ class _PageStyleIconState extends State { showDoneButton: true, showHeader: true, title: LocaleKeys.titleBar_pageIcon.tr(), - backgroundColor: Theme.of(context).colorScheme.background, - isScrollControlled: true, + backgroundColor: AFThemeExtension.of(context).background, enableDraggableScrollable: true, minChildSize: 0.6, initialChildSize: 0.61, @@ -94,7 +92,7 @@ class _PageStyleIconState extends State { child: Expanded( child: Scrollbar( controller: controller, - child: _IconSelector( + child: IconSelector( scrollController: controller, ), ), @@ -105,154 +103,3 @@ class _PageStyleIconState extends State { ); } } - -class _IconSelector extends StatefulWidget { - const _IconSelector({ - required this.scrollController, - }); - - final ScrollController scrollController; - - @override - State<_IconSelector> createState() => _IconSelectorState(); -} - -class _IconSelectorState extends State<_IconSelector> { - EmojiData? emojiData; - List availableEmojis = []; - - PageStyleIconBloc? pageStyleIconBloc; - - @override - void initState() { - super.initState(); - - // load the emoji data from cache if it's available - if (kCachedEmojiData != null) { - emojiData = kCachedEmojiData; - availableEmojis = _setupAvailableEmojis(emojiData!); - } else { - EmojiData.builtIn().then( - (value) { - kCachedEmojiData = value; - setState(() { - emojiData = value; - availableEmojis = _setupAvailableEmojis(value); - }); - }, - ); - } - - pageStyleIconBloc = context.read(); - } - - @override - void dispose() { - pageStyleIconBloc?.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - if (emojiData == null) { - return const Center(child: CircularProgressIndicator()); - } - - return RepaintBoundary( - child: BlocBuilder( - builder: (_, state) => Column( - children: [ - _buildSearchBar(context), - Expanded( - child: GridView.count( - crossAxisCount: 7, - controller: widget.scrollController, - children: [ - for (final emoji in availableEmojis) - _buildEmoji(context, emoji, state.icon), - ], - ), - ), - ], - ), - ), - ); - } - - Widget _buildEmoji( - BuildContext context, - String emoji, - String? selectedEmoji, - ) { - Widget child = SizedBox.square( - dimension: 24.0, - child: Center( - child: FlowyText.emoji( - emoji, - fontSize: 24, - ), - ), - ); - - if (emoji == selectedEmoji) { - child = Center( - child: Container( - width: 40, - height: 40, - decoration: ShapeDecoration( - shape: RoundedRectangleBorder( - side: const BorderSide( - width: 1.40, - strokeAlign: BorderSide.strokeAlignOutside, - color: Color(0xFF00BCF0), - ), - borderRadius: BorderRadius.circular(10), - ), - ), - child: child, - ), - ); - } - - return GestureDetector( - onTap: () { - context.read().add( - PageStyleIconEvent.updateIcon(emoji, true), - ); - }, - child: child, - ); - } - - List _setupAvailableEmojis(EmojiData emojiData) { - final categories = emojiData.categories; - availableEmojis = categories - .map((e) => e.emojiIds.map((e) => emojiData.getEmojiById(e))) - .expand((e) => e) - .toList(); - return availableEmojis; - } - - Widget _buildSearchBar(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 12.0, - ), - child: FlowyMobileSearchTextField( - onChanged: (keyword) { - if (emojiData == null) { - return; - } - - final filtered = emojiData!.filterByKeyword(keyword); - final availableEmojis = _setupAvailableEmojis(filtered); - - setState(() { - this.availableEmojis = availableEmojis; - }); - }, - ), - ); - } -} 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 054f544c8f4ac..211e287d159c2 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,8 +209,7 @@ class _FontButton extends StatelessWidget { showDoneButton: true, showHeader: true, title: LocaleKeys.titleBar_font.tr(), - 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 afb2b63f49f07..6d0597319c470 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_plugins/todo_list/todo_list_icon.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/todo_list/todo_list_icon.dart index deb45c2182599..85972a3c2ce71 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/todo_list/todo_list_icon.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/todo_list/todo_list_icon.dart @@ -17,7 +17,9 @@ class TodoListIcon extends StatelessWidget { @override Widget build(BuildContext context) { - final iconPadding = context.read().state.iconPadding; + final iconPadding = PlatformExtension.isMobile + ? context.read().state.iconPadding + : 0.0; final checked = node.attributes[TodoListBlockKeys.checked] ?? false; return GestureDetector( behavior: HitTestBehavior.opaque, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/video/upload_video_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/video/upload_video_menu.dart new file mode 100644 index 0000000000000..2b2752e3f5a3f --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/video/upload_video_menu.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/shared/patterns/common_patterns.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide ColorOption; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; + +class UploadVideoMenu extends StatefulWidget { + const UploadVideoMenu({ + super.key, + required this.onUrlSubmitted, + this.onSelectedColor, + }); + + final void Function(String url) onUrlSubmitted; + final void Function(String color)? onSelectedColor; + + @override + State createState() => _UploadVideoMenuState(); +} + +class _UploadVideoMenuState extends State { + @override + Widget build(BuildContext context) { + final constraints = + PlatformExtension.isMobile ? const BoxConstraints(minHeight: 92) : null; + + return Container( + padding: const EdgeInsets.all(8.0), + constraints: constraints, + child: _EmbedUrl(onSubmit: widget.onUrlSubmitted), + ); + } +} + +class _EmbedUrl extends StatefulWidget { + const _EmbedUrl({required this.onSubmit}); + + final void Function(String url) onSubmit; + + @override + State<_EmbedUrl> createState() => _EmbedUrlState(); +} + +class _EmbedUrlState extends State<_EmbedUrl> { + bool isUrlValid = true; + String inputText = ''; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + FlowyTextField( + hintText: LocaleKeys.document_plugins_video_placeholder.tr(), + onChanged: (value) => inputText = value, + onEditingComplete: submit, + ), + if (!isUrlValid) ...[ + const VSpace(8), + FlowyText( + LocaleKeys.document_plugins_video_invalidVideoUrl.tr(), + color: Theme.of(context).colorScheme.error, + ), + ], + const VSpace(8), + SizedBox( + width: 160, + child: FlowyButton( + showDefaultBoxDecorationOnMobile: true, + margin: const EdgeInsets.all(8.0), + text: FlowyText( + LocaleKeys.document_plugins_video_insertVideo.tr(), + textAlign: TextAlign.center, + ), + onTap: submit, + ), + ), + ], + ); + } + + void submit() { + if (checkUrlValidity(inputText)) { + return widget.onSubmit(inputText); + } + + setState(() => isUrlValid = false); + } + + bool checkUrlValidity(String url) => videoUrlRegex.hasMatch(url); +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/video/video_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/video/video_menu.dart new file mode 100644 index 0000000000000..a82156586e354 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/video/video_menu.dart @@ -0,0 +1,314 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; +import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/align_toolbar_item/align_toolbar_item.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; +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_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +class VideoMenu extends StatefulWidget { + const VideoMenu({ + super.key, + required this.node, + required this.state, + }); + + final Node node; + final VideoBlockComponentState state; + + @override + State createState() => _VideoMenuState(); +} + +class _VideoMenuState extends State { + late final String? url = widget.node.attributes[VideoBlockKeys.url]; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Container( + height: 32, + decoration: BoxDecoration( + color: theme.cardColor, + borderRadius: BorderRadius.circular(4.0), + boxShadow: [ + BoxShadow( + blurRadius: 5, + spreadRadius: 1, + color: Colors.black.withOpacity(0.1), + ), + ], + ), + child: PlatformExtension.isMobile + ? MenuBlockButton( + tooltip: LocaleKeys.button_edit.tr(), + iconData: FlowySvgs.more_s, + onTap: showMobileMenu, + ) + : Row( + children: [ + const HSpace(4), + MenuBlockButton( + tooltip: LocaleKeys.editor_copyLink.tr(), + iconData: FlowySvgs.copy_s, + onTap: copyVideoLink, + ), + const HSpace(4), + _VideoAlignButton( + node: widget.node, + state: widget.state, + ), + const _Divider(), + MenuBlockButton( + tooltip: LocaleKeys.button_delete.tr(), + iconData: FlowySvgs.delete_s, + onTap: deleteVideo, + ), + const HSpace(4), + ], + ), + ); + } + + void copyVideoLink() { + if (url != null) { + Clipboard.setData(ClipboardData(text: url!)); + showSnackBarMessage( + context, + LocaleKeys.document_plugins_video_copiedToPasteBoard.tr(), + ); + } + } + + void showMobileMenu() { + final editorState = context.read() + ..updateSelectionWithReason(null, extraInfo: {}); + final src = widget.node.attributes[VideoBlockKeys.url]; + showMobileBottomSheet( + context, + showHeader: true, + showCloseButton: true, + showDragHandle: true, + title: LocaleKeys.document_plugins_action.tr(), + builder: (context) { + return BlockActionBottomSheet( + extendActionWidgets: [ + FlowyOptionTile.text( + showTopBorder: false, + text: LocaleKeys.editor_copyLink.tr(), + leftIcon: const FlowySvg( + FlowySvgs.m_field_copy_s, + ), + onTap: () async { + context.pop(); + showSnackBarMessage( + context, + LocaleKeys.document_plugins_video_copiedToPasteBoard.tr(), + ); + await getIt().setPlainText(src); + }, + ), + ], + onAction: (action) async { + context.pop(); + + final transaction = editorState.transaction; + switch (action) { + case BlockActionBottomSheetType.delete: + transaction.deleteNode(widget.node); + break; + case BlockActionBottomSheetType.duplicate: + transaction.insertNode( + widget.node.path.next, + widget.node.copyWith(), + ); + break; + case BlockActionBottomSheetType.insertAbove: + case BlockActionBottomSheetType.insertBelow: + final path = action == BlockActionBottomSheetType.insertAbove + ? widget.node.path + : widget.node.path.next; + transaction + ..insertNode(path, paragraphNode()) + ..afterSelection = Selection.collapsed(Position(path: path)); + break; + default: + } + + if (transaction.operations.isNotEmpty) { + await editorState.apply(transaction); + } + }, + ); + }, + ); + } + + Future deleteVideo() async { + final node = widget.node; + final editorState = context.read(); + final transaction = editorState.transaction; + transaction.deleteNode(node); + transaction.afterSelection = null; + await editorState.apply(transaction); + } +} + +class _VideoAlignButton extends StatefulWidget { + const _VideoAlignButton({ + required this.node, + required this.state, + }); + + final Node node; + final VideoBlockComponentState state; + + @override + State<_VideoAlignButton> createState() => _VideoAlignButtonState(); +} + +const interceptorKey = 'video-align'; + +class _VideoAlignButtonState extends State<_VideoAlignButton> { + final gestureInterceptor = SelectionGestureInterceptor( + key: interceptorKey, + canTap: (_) => false, + ); + + String get align => + widget.node.attributes[VideoBlockKeys.alignment] ?? centerAlignmentKey; + final popoverController = PopoverController(); + late final EditorState editorState; + + @override + void initState() { + super.initState(); + editorState = context.read(); + } + + @override + void dispose() { + allowMenuClose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return IgnoreParentGestureWidget( + child: AppFlowyPopover( + onClose: allowMenuClose, + controller: popoverController, + windowPadding: const EdgeInsets.all(0), + margin: const EdgeInsets.all(0), + direction: PopoverDirection.bottomWithCenterAligned, + offset: const Offset(0, 10), + child: MenuBlockButton( + tooltip: LocaleKeys.document_plugins_optionAction_align.tr(), + iconData: iconFor(align), + ), + popupBuilder: (_) { + preventMenuClose(); + return _AlignButtons(onAlignChanged: onAlignChanged); + }, + ), + ); + } + + void onAlignChanged(String align) { + popoverController.close(); + + final transaction = editorState.transaction; + transaction.updateNode(widget.node, {VideoBlockKeys.alignment: align}); + editorState.apply(transaction); + + allowMenuClose(); + } + + void preventMenuClose() { + widget.state.preventClose = true; + editorState.service.selectionService + .registerGestureInterceptor(gestureInterceptor); + } + + void allowMenuClose() { + widget.state.preventClose = false; + editorState.service.selectionService + .unregisterGestureInterceptor(interceptorKey); + } + + FlowySvgData iconFor(String alignment) { + switch (alignment) { + case leftAlignmentKey: + return FlowySvgs.align_left_s; + case rightAlignmentKey: + return FlowySvgs.align_right_s; + case centerAlignmentKey: + default: + return FlowySvgs.align_center_s; + } + } +} + +class _AlignButtons extends StatelessWidget { + const _AlignButtons({required this.onAlignChanged}); + + final Function(String align) onAlignChanged; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 32, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const HSpace(4), + MenuBlockButton( + tooltip: LocaleKeys.document_plugins_optionAction_left, + iconData: FlowySvgs.align_left_s, + onTap: () => onAlignChanged(leftAlignmentKey), + ), + const _Divider(), + MenuBlockButton( + tooltip: LocaleKeys.document_plugins_optionAction_center, + iconData: FlowySvgs.align_center_s, + onTap: () => onAlignChanged(centerAlignmentKey), + ), + const _Divider(), + MenuBlockButton( + tooltip: LocaleKeys.document_plugins_optionAction_right, + iconData: FlowySvgs.align_right_s, + onTap: () => onAlignChanged(rightAlignmentKey), + ), + const HSpace(4), + ], + ), + ); + } +} + +class _Divider extends StatelessWidget { + const _Divider(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8), + child: Container(width: 1, color: Colors.grey), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/video/video_placeholder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/video/video_placeholder.dart new file mode 100644 index 0000000000000..a7d853b1451bf --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/video/video_placeholder.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; +import 'package:appflowy/plugins/document/application/prelude.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/video/upload_video_menu.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log, UploadImageMenu; +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_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:string_validator/string_validator.dart'; + +class VideoPlaceholder extends StatefulWidget { + const VideoPlaceholder({super.key, required this.node}); + + final Node node; + + @override + State createState() => VideoPlaceholderState(); +} + +class VideoPlaceholderState extends State { + final controller = PopoverController(); + final documentService = DocumentService(); + late final editorState = context.read(); + + bool showLoading = false; + + @override + Widget build(BuildContext context) { + final Widget child = DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(4), + ), + child: FlowyHover( + style: HoverStyle(borderRadius: BorderRadius.circular(4)), + child: SizedBox( + height: 52, + child: Row( + children: [ + const HSpace(10), + const Icon(Icons.featured_video_outlined, size: 24), + const HSpace(10), + FlowyText(LocaleKeys.document_plugins_video_emptyLabel.tr()), + ], + ), + ), + ), + ); + + if (PlatformExtension.isDesktopOrWeb) { + return AppFlowyPopover( + controller: controller, + direction: PopoverDirection.bottomWithCenterAligned, + constraints: const BoxConstraints( + maxWidth: 540, + maxHeight: 360, + minHeight: 80, + ), + clickHandler: PopoverClickHandler.gestureDetector, + popupBuilder: (_) => UploadVideoMenu( + onUrlSubmitted: (url) { + controller.close(); + WidgetsBinding.instance.addPostFrameCallback( + (_) async => updateSrc(url), + ); + }, + ), + child: child, + ); + } else { + return MobileBlockActionButtons( + node: widget.node, + editorState: editorState, + child: GestureDetector( + onTap: () { + editorState.updateSelectionWithReason(null, extraInfo: {}); + showUploadVideoMenu(); + }, + child: child, + ), + ); + } + } + + void showUploadVideoMenu() { + if (PlatformExtension.isDesktopOrWeb) { + controller.show(); + } else { + showMobileBottomSheet( + context, + title: LocaleKeys.document_plugins_video_label.tr(), + showHeader: true, + showCloseButton: true, + showDragHandle: true, + builder: (context) => Container( + margin: const EdgeInsets.only(top: 12.0), + constraints: const BoxConstraints( + maxHeight: 340, + minHeight: 80, + ), + child: UploadVideoMenu( + onUrlSubmitted: (url) async { + context.pop(); + await updateSrc(url); + }, + ), + ), + ); + } + } + + Future updateSrc(String url) async { + if (url.isEmpty || !isURL(url)) { + // show error + showSnackBarMessage( + context, + LocaleKeys.document_imageBlock_error_invalidImage.tr(), + ); + return; + } + + final transaction = editorState.transaction; + transaction.updateNode(widget.node, { + VideoBlockKeys.url: url, + }); + await editorState.apply(transaction); + } +} 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 6196388bd196c..0b3aca1ce7fd3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; @@ -15,6 +13,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,6 +41,7 @@ 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; @@ -58,10 +58,12 @@ class EditorStyleCustomizer { DefaultAppearanceSettings.getDefaultSelectionColor(context), defaultTextDirection: appearance.defaultTextDirection, textStyleConfiguration: TextStyleConfiguration( + lineHeight: 1.2, + applyHeightToFirstAscent: true, + applyHeightToLastDescent: true, 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, @@ -79,7 +81,7 @@ class EditorStyleCustomizer { ), code: GoogleFonts.robotoMono( textStyle: baseTextStyle(fontFamily).copyWith( - fontSize: fontSize - 2, + fontSize: fontSize, fontWeight: FontWeight.normal, color: Colors.red, backgroundColor: theme.colorScheme.inverseSurface.withOpacity(0.8), @@ -93,6 +95,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; @@ -103,15 +106,14 @@ class EditorStyleCustomizer { final textScaleFactor = context.read().state.textScaleFactor; final baseTextStyle = this.baseTextStyle(fontFamily); - final codeFontSize = max(0.0, fontSize - 2); return EditorStyle.mobile( 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), @@ -125,7 +127,7 @@ class EditorStyleCustomizer { ), code: GoogleFonts.robotoMono( textStyle: baseTextStyle.copyWith( - fontSize: codeFontSize, + fontSize: fontSize, fontWeight: FontWeight.normal, fontStyle: FontStyle.italic, color: Colors.red, @@ -177,7 +179,15 @@ class EditorStyleCustomizer { return baseTextStyle(fontFamily).copyWith( fontSize: fontSize, height: 1.5, - color: Theme.of(context).colorScheme.onBackground, + color: AFThemeExtension.of(context).onBackground, + ); + } + + TextStyle calloutBlockStyleBuilder() { + final fontSize = context.read().state.fontSize; + return baseTextStyle(null).copyWith( + fontSize: fontSize, + height: 1.5, ); } @@ -187,16 +197,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, @@ -205,10 +216,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 fa15bc10d5824..eb82f3d1faec3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart @@ -41,12 +41,9 @@ class DocumentShareButton extends StatelessWidget { ); }, child: BlocBuilder( - builder: (context, state) => ConstrainedBox( - constraints: const BoxConstraints.expand( - height: 30, - width: 100, - ), - child: ShareActionList(view: view), + builder: (context, state) => SizedBox( + height: 32.0, + child: IntrinsicWidth(child: ShareActionList(view: view)), ), ), ), @@ -120,7 +117,9 @@ class ShareActionListState extends State { onPointerDown: (_) => controller.show(), child: RoundedTextButton( title: LocaleKeys.shareAction_buttonText.tr(), + padding: const EdgeInsets.symmetric(horizontal: 12.0), onPressed: () {}, + fontSize: 14.0, textColor: Theme.of(context).colorScheme.onPrimary, ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart index 0bbfd3359d423..4da27109d2f0c 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart @@ -1,7 +1,5 @@ import 'dart:async'; -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart'; @@ -20,6 +18,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; +import 'package:flutter/material.dart'; // const _channel = "InlinePageReference"; @@ -65,8 +64,11 @@ class InlinePageReferenceService extends InlineActionsDelegate { _recentViewsInitialized = true; - final views = - (await _recentService.recentViews()).reversed.toSet().toList(); + final views = (await _recentService.recentViews()) + .reversed + .map((e) => e.item) + .toSet() + .toList(); // Filter by viewLayout views.retainWhere( diff --git a/frontend/appflowy_flutter/lib/shared/patterns/common_patterns.dart b/frontend/appflowy_flutter/lib/shared/patterns/common_patterns.dart index fce937908af56..ea5da4f44a6e8 100644 --- a/frontend/appflowy_flutter/lib/shared/patterns/common_patterns.dart +++ b/frontend/appflowy_flutter/lib/shared/patterns/common_patterns.dart @@ -13,6 +13,15 @@ const _imgUrlPattern = r'(https?:\/\/)([^\s(["<,>/]*)(\/)[^\s[",><]*(.png|.jpg|.gif|.webm)(\?[^\s[",><]*)?'; final imgUrlRegex = RegExp(_imgUrlPattern); +/// This pattern allows for both HTTP and HTTPS Scheme +/// It allows for query parameters +/// It only allows the following video extensions: +/// .mp4, .mov, .avi, .webm, .flv, .m4v (mpeg), .mpeg, .h264, +/// +const _videoUrlPattern = + r'(https?:\/\/)([^\s(["<,>/]*)(\/)[^\s[",><]*(.mp4|.mov|.avi|.webm|.flv|.m4v|.mpeg|.h264)(\?[^\s[",><]*)?'; +final videoUrlRegex = RegExp(_videoUrlPattern); + const _appflowyCloudUrlPattern = r'^(https:\/\/)(.*)(\.appflowy\.cloud\/)(.*)'; final appflowyCloudUrlRegex = RegExp(_appflowyCloudUrlPattern); diff --git a/frontend/appflowy_flutter/lib/shared/window_title_bar.dart b/frontend/appflowy_flutter/lib/shared/window_title_bar.dart index 51a8b51c86ada..164038358865e 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/startup/tasks/app_widget.dart b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart index 9e719d6ecc769..f90685f030707 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart @@ -253,9 +253,9 @@ class _ApplicationWidgetState extends State { } class AppGlobals { - // static GlobalKey scaffoldMessengerKey = GlobalKey(); static GlobalKey rootNavKey = GlobalKey(); static NavigatorState get nav => rootNavKey.currentState!; + static BuildContext get context => rootNavKey.currentContext!; } class ApplicationBlocObserver extends BlocObserver { diff --git a/frontend/appflowy_flutter/lib/user/application/user_listener.dart b/frontend/appflowy_flutter/lib/user/application/user_listener.dart index b3e7d6fbe0306..81a081b4e3349 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 ce446320a311d..09d24ecd836ea 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 0000000000000..c7b56699d312d --- /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 7b9e86e16b952..8bc1e549eebf5 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,37 +33,53 @@ 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.toList(); + final pinnedViews = + views.where((v) => v.item.isPinned).toList(); + final unpinnedViews = + views.where((v) => !v.item.isPinned).toList(); + return state.copyWith( + isLoading: false, + views: views, + pinnedViews: pinnedViews, + unpinnedViews: unpinnedViews, + ); + }, (error) => state.copyWith( + isLoading: false, views: [], ), ), ); }, 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,15 +101,18 @@ 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 views, + @Default([]) List pinnedViews, + @Default([]) List unpinnedViews, + @Default(true) bool isLoading, }) = _FavoriteState; - factory FavoriteState.initial() => const FavoriteState( - views: [], - ); + factory FavoriteState.initial() => const FavoriteState(); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_service.dart b/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_service.dart index d9343e2ee386d..71bb8423df2dc 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_service.dart @@ -1,10 +1,15 @@ +import 'dart:convert'; + +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; +import 'package:collection/collection.dart'; class FavoriteService { - Future> readFavorites() { + Future> readFavorites() { return FolderEventReadFavorites().send(); } @@ -15,4 +20,33 @@ class FavoriteService { final id = RepeatedViewIdPB.create()..items.add(viewId); return FolderEventToggleFavorite(id).send(); } + + Future> pinFavorite(ViewPB view) async { + return pinOrUnpinFavorite(view, true); + } + + Future> unpinFavorite(ViewPB view) async { + return pinOrUnpinFavorite(view, false); + } + + Future> pinOrUnpinFavorite( + ViewPB view, + bool isPinned, + ) async { + try { + final current = view.extra.isNotEmpty ? jsonDecode(view.extra) : {}; + final merged = mergeMaps( + current, + {ViewExtKeys.isPinnedKey: isPinned}, + ); + await ViewBackendService.updateView( + viewId: view.id, + extra: jsonEncode(merged), + ); + } catch (e) { + return FlowyResult.failure(FlowyError(msg: 'Failed to pin favorite: $e')); + } + + return FlowyResult.success(null); + } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/recent/cached_recent_service.dart b/frontend/appflowy_flutter/lib/workspace/application/recent/cached_recent_service.dart index bfd4d654a36dc..f4245c310094b 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/recent/cached_recent_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/recent/cached_recent_service.dart @@ -1,13 +1,12 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; - import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/recent/recent_listener.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/view.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flutter/foundation.dart'; /// This is a lazy-singleton to share recent views across the application. /// @@ -23,21 +22,23 @@ class CachedRecentService { Completer _completer = Completer(); - ValueNotifier> notifier = ValueNotifier(const []); + ValueNotifier> notifier = ValueNotifier(const []); - List get _recentViews => notifier.value; - set _recentViews(List value) => notifier.value = value; + List get _recentViews => notifier.value; + set _recentViews(List value) => notifier.value = value; final _listener = RecentViewsListener(); - Future> recentViews() async { + Future> recentViews() async { if (_isInitialized) return _recentViews; _isInitialized = true; _listener.start(recentViewsUpdated: _recentViewsUpdated); - final result = await _readRecentViews(); - _recentViews = result.toNullable()?.items ?? const []; + _recentViews = await _readRecentViews().fold( + (s) => s.items, + (_) => [], + ); _completer.complete(); return _recentViews; @@ -47,15 +48,24 @@ class CachedRecentService { Future> updateRecentViews( List viewIds, bool addInRecent, - ) async => - FolderEventUpdateRecentViews( - UpdateRecentViewPayloadPB( - viewIds: viewIds, - addInRecent: addInRecent, - ), - ).send(); - - Future> _readRecentViews() => + ) async { + final List duplicatedViewIds = []; + for (final viewId in viewIds) { + for (final view in _recentViews) { + if (view.item.id == viewId) { + duplicatedViewIds.add(viewId); + } + } + } + return FolderEventUpdateRecentViews( + UpdateRecentViewPayloadPB( + viewIds: addInRecent ? viewIds : duplicatedViewIds, + addInRecent: addInRecent, + ), + ).send(); + } + + Future> _readRecentViews() => FolderEventReadRecentViews().send(); bool _isInitialized = false; @@ -74,11 +84,12 @@ class CachedRecentService { void _recentViewsUpdated( FlowyResult result, - ) { + ) async { final viewIds = result.toNullable(); if (viewIds != null) { - _readRecentViews().then( - (views) => _recentViews = views.toNullable()?.items ?? const [], + _recentViews = await _readRecentViews().fold( + (s) => s.items, + (_) => [], ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/recent/recent_views_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/recent/recent_views_bloc.dart index b43edfa2b1a0e..d67c24e854d2b 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/recent/recent_views_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/recent/recent_views_bloc.dart @@ -35,7 +35,12 @@ class RecentViewsBloc extends Bloc { await _service.updateRecentViews(e.viewIds, false); }, fetchRecentViews: (e) async { - emit(state.copyWith(views: await _service.recentViews())); + emit( + state.copyWith( + isLoading: false, + views: await _service.recentViews(), + ), + ); }, resetRecentViews: (e) async { await _service.reset(); @@ -63,8 +68,10 @@ class RecentViewsEvent with _$RecentViewsEvent { @freezed class RecentViewsState with _$RecentViewsState { - const factory RecentViewsState({required List views}) = - _RecentViewsState; + const factory RecentViewsState({ + required List views, + @Default(true) bool isLoading, + }) = _RecentViewsState; factory RecentViewsState.initial() => const RecentViewsState(views: []); } 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 2fba33263ff16..0c6fe9aaed05b 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/base_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/base_appearance.dart @@ -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 001de8af4e531..1727eedd55eca 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 19cd87f4f4653..09db07ed11188 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 5da3caa5b9990..0dfa2807b9ac3 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/notifications/notification_settings_cubit.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/notifications/notification_settings_cubit.dart index aae0a6360983f..ea6b3b6f0153e 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/notifications/notification_settings_cubit.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/notifications/notification_settings_cubit.dart @@ -1,5 +1,8 @@ import 'dart:async'; +import 'package:appflowy/core/config/kv.dart'; +import 'package:appflowy/core/config/kv_keys.dart'; +import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/user_settings_service.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart'; @@ -10,23 +13,30 @@ part 'notification_settings_cubit.freezed.dart'; class NotificationSettingsCubit extends Cubit { NotificationSettingsCubit() : super(NotificationSettingsState.initial()) { - UserSettingsBackendService() - .getNotificationSettings() - .then((notificationSettings) { - _notificationSettings = notificationSettings; - emit( - state.copyWith( - isNotificationsEnabled: _notificationSettings.notificationsEnabled, - ), - ); - _initCompleter.complete(); - }); + _initialize(); } final Completer _initCompleter = Completer(); late final NotificationSettingsPB _notificationSettings; + Future _initialize() async { + _notificationSettings = + await UserSettingsBackendService().getNotificationSettings(); + + final showNotificationSetting = await getIt() + .getWithFormat(KVKeys.showNotificationIcon, (v) => bool.parse(v)); + + emit( + state.copyWith( + isNotificationsEnabled: _notificationSettings.notificationsEnabled, + isShowNotificationsIconEnabled: showNotificationSetting ?? true, + ), + ); + + _initCompleter.complete(); + } + Future toggleNotificationsEnabled() async { await _initCompleter.future; @@ -41,9 +51,24 @@ class NotificationSettingsCubit extends Cubit { await _saveNotificationSettings(); } + Future toogleShowNotificationIconEnabled() async { + await _initCompleter.future; + + emit( + state.copyWith( + isShowNotificationsIconEnabled: !state.isShowNotificationsIconEnabled, + ), + ); + } + Future _saveNotificationSettings() async { await _initCompleter.future; + await getIt().set( + KVKeys.showNotificationIcon, + state.isShowNotificationsIconEnabled.toString(), + ); + final result = await UserSettingsBackendService() .setNotificationSettings(_notificationSettings); result.fold( @@ -59,8 +84,12 @@ class NotificationSettingsState with _$NotificationSettingsState { const factory NotificationSettingsState({ required bool isNotificationsEnabled, + required bool isShowNotificationsIconEnabled, }) = _NotificationSettingsState; factory NotificationSettingsState.initial() => - const NotificationSettingsState(isNotificationsEnabled: true); + const NotificationSettingsState( + isNotificationsEnabled: true, + isShowNotificationsIconEnabled: true, + ); } 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 790375fc2030e..0fdaa491285be 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 5c02ea6b11496..d7980e031a563 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 4dd934f60b152..c85f3bd0b0952 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/sidebar/folder/folder_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/sidebar/folder/folder_bloc.dart @@ -9,18 +9,18 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'folder_bloc.freezed.dart'; -enum FolderCategoryType { +enum FolderSpaceType { favorite, private, public; ViewSectionPB get toViewSectionPB { switch (this) { - case FolderCategoryType.private: + case FolderSpaceType.private: return ViewSectionPB.Private; - case FolderCategoryType.public: + case FolderSpaceType.public: return ViewSectionPB.Public; - case FolderCategoryType.favorite: + case FolderSpaceType.favorite: throw UnimplementedError(); } } @@ -28,7 +28,7 @@ enum FolderCategoryType { class FolderBloc extends Bloc { FolderBloc({ - required FolderCategoryType type, + required FolderSpaceType type, }) : super(FolderState.initial(type)) { on((event, emit) async { await event.map( @@ -84,12 +84,12 @@ class FolderEvent with _$FolderEvent { @freezed class FolderState with _$FolderState { const factory FolderState({ - required FolderCategoryType type, + required FolderSpaceType type, required bool isExpanded, }) = _FolderState; factory FolderState.initial( - FolderCategoryType type, + FolderSpaceType type, ) => FolderState( type: type, diff --git a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart index c0a885c19b08f..ec1a2c43d19a5 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart @@ -1,5 +1,3 @@ -import 'package:flutter/foundation.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/user/application/user_listener.dart'; @@ -12,6 +10,7 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:protobuf/protobuf.dart'; @@ -51,6 +50,7 @@ class UserWorkspaceBloc extends Bloc { 'init workspace, current workspace: ${currentWorkspace?.workspaceId}, ' 'workspaces: ${workspaces.map((e) => e.workspaceId)}, isCollabWorkspaceOn: $isCollabWorkspaceOn', ); + final members = await _fetchMembers(currentWorkspace?.workspaceId); if (currentWorkspace != null && result.$3 == true) { Log.info('init open workspace: ${currentWorkspace.workspaceId}'); await _userService.openWorkspace(currentWorkspace.workspaceId); @@ -61,6 +61,7 @@ class UserWorkspaceBloc extends Bloc { workspaces: workspaces, isCollabWorkspaceOn: isCollabWorkspaceOn, actionResult: null, + members: members, ), ); }, @@ -198,6 +199,7 @@ class UserWorkspaceBloc extends Bloc { ), (e) => state.currentWorkspace, ); + final members = await _fetchMembers(currentWorkspace?.workspaceId); result ..onSuccess((s) { @@ -212,6 +214,7 @@ class UserWorkspaceBloc extends Bloc { emit( state.copyWith( currentWorkspace: currentWorkspace, + members: members, actionResult: UserWorkspaceActionResult( actionType: UserWorkspaceActionType.open, isLoading: false, @@ -415,6 +418,17 @@ class UserWorkspaceBloc extends Bloc { ..name = workspace.name ..createdAtTimestamp = workspace.createTime; } + + Future> _fetchMembers( + String? workspaceId, + ) async { + if (workspaceId == null) { + return []; + } + return _userService + .getWorkspaceMembers(workspaceId) + .fold((s) => s.items, (_) => []); + } } @freezed @@ -477,6 +491,7 @@ class UserWorkspaceState with _$UserWorkspaceState { @Default([]) List workspaces, @Default(null) UserWorkspaceActionResult? actionResult, @Default(false) bool isCollabWorkspaceOn, + @Default([]) List members, }) = _UserWorkspaceState; factory UserWorkspaceState.initial() => const UserWorkspaceState(); diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart index 1cfb89c8f937c..bf243f8a1c7b9 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart @@ -19,7 +19,7 @@ import 'package:protobuf/protobuf.dart'; part 'view_bloc.freezed.dart'; class ViewBloc extends Bloc { - ViewBloc({required this.view}) + ViewBloc({required this.view, this.shouldLoadChildViews = true}) : viewBackendSvc = ViewBackendService(), listener = ViewListener(viewId: view.id), favoriteListener = FavoriteListener(), @@ -31,6 +31,7 @@ class ViewBloc extends Bloc { final ViewBackendService viewBackendSvc; final ViewListener listener; final FavoriteListener favoriteListener; + final bool shouldLoadChildViews; @override Future close() async { @@ -74,8 +75,10 @@ class ViewBloc extends Bloc { }, ); final isExpanded = await _getViewIsExpanded(view); - emit(state.copyWith(isExpanded: isExpanded)); - await _loadViewsWhenExpanded(emit, isExpanded); + emit(state.copyWith(isExpanded: isExpanded, view: view)); + if (shouldLoadChildViews) { + await _loadChildViews(emit); + } }, setIsEditing: (e) { emit(state.copyWith(isEditing: e.isEditing)); @@ -222,6 +225,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 +279,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 +424,7 @@ class ViewEvent with _$ViewEvent { bool isPublic, ) = UpdateViewVisibility; const factory ViewEvent.updateIcon(String? icon) = UpdateIcon; + const factory ViewEvent.collapseAllPages() = CollapseAllPages; } @freezed diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart index ca3bb62fbb52f..c845b2ddcc6e2 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart @@ -32,13 +32,16 @@ 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 { Widget defaultIcon() => FlowySvg( switch (layout) { ViewLayoutPB.Board => FlowySvgs.board_s, - ViewLayoutPB.Calendar => FlowySvgs.date_s, + ViewLayoutPB.Calendar => FlowySvgs.calendar_s, ViewLayoutPB.Grid => FlowySvgs.grid_s, ViewLayoutPB.Document => FlowySvgs.document_s, _ => FlowySvgs.document_s, @@ -96,6 +99,16 @@ extension ViewExtension on ViewPB { FlowySvgData get iconData => layout.icon; + bool get isPinned { + try { + final ext = jsonDecode(extra); + final isPinned = ext[ViewExtKeys.isPinnedKey] ?? false; + return isPinned; + } catch (e) { + return false; + } + } + PageStyleCover? get cover { if (layout != ViewLayoutPB.Document) { return null; diff --git a/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bar_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bar_bloc.dart new file mode 100644 index 0000000000000..491ff367861e8 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bar_bloc.dart @@ -0,0 +1,48 @@ +import 'package:appflowy/workspace/application/view/prelude.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'view_title_bar_bloc.freezed.dart'; + +class ViewTitleBarBloc extends Bloc { + ViewTitleBarBloc({ + required this.view, + }) : super(ViewTitleBarState.initial()) { + on( + (event, emit) async { + await event.when( + initial: () async { + add(const ViewTitleBarEvent.reload()); + }, + reload: () async { + final List ancestors = + await ViewBackendService.getViewAncestors(view.id).fold( + (s) => s.items, + (f) => [], + ); + emit(state.copyWith(ancestors: ancestors)); + }, + ); + }, + ); + } + + final ViewPB view; +} + +@freezed +class ViewTitleBarEvent with _$ViewTitleBarEvent { + const factory ViewTitleBarEvent.initial() = Initial; + const factory ViewTitleBarEvent.reload() = Reload; +} + +@freezed +class ViewTitleBarState with _$ViewTitleBarState { + const factory ViewTitleBarState({ + required List ancestors, + }) = _ViewTitleBarState; + + factory ViewTitleBarState.initial() => const ViewTitleBarState(ancestors: []); +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bloc.dart new file mode 100644 index 0000000000000..737539763aa9f --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bloc.dart @@ -0,0 +1,73 @@ +import 'package:appflowy/workspace/application/view/prelude.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'view_title_bloc.freezed.dart'; + +class ViewTitleBloc extends Bloc { + ViewTitleBloc({ + required this.view, + }) : viewListener = ViewListener(viewId: view.id), + super(ViewTitleState.initial()) { + on( + (event, emit) async { + await event.when( + initial: () async { + emit( + state.copyWith( + name: view.name, + icon: view.icon.value, + ), + ); + + viewListener.start( + onViewUpdated: (view) { + add( + ViewTitleEvent.updateNameOrIcon( + view.name, + view.icon.value, + ), + ); + }, + ); + }, + updateNameOrIcon: (name, icon) async { + emit( + state.copyWith( + name: name, + icon: icon, + ), + ); + }, + ); + }, + ); + } + + final ViewPB view; + final ViewListener viewListener; + + @override + Future close() { + viewListener.stop(); + return super.close(); + } +} + +@freezed +class ViewTitleEvent with _$ViewTitleEvent { + const factory ViewTitleEvent.initial() = Initial; + const factory ViewTitleEvent.updateNameOrIcon(String name, String icon) = + UpdateNameOrIcon; +} + +@freezed +class ViewTitleState with _$ViewTitleState { + const factory ViewTitleState({ + required String name, + required String icon, + }) = _ViewTitleState; + + factory ViewTitleState.initial() => const ViewTitleState(name: '', icon: ''); +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_views_list.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_views_list.dart index 2087d1e476142..d8c257e897781 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_views_list.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_views_list.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/recent/recent_views_bloc.dart'; @@ -8,6 +6,7 @@ import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_v import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class RecentViewsList extends StatelessWidget { @@ -24,7 +23,7 @@ class RecentViewsList extends StatelessWidget { builder: (context, state) { // We remove duplicates by converting the list to a set first final List recentViews = - state.views.reversed.toSet().toList(); + state.views.reversed.map((e) => e.item).toSet().toList(); return ListView.separated( shrinkWrap: true, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_sizes.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_sizes.dart index fd15cf8d23831..b35ae64ac5774 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,27 @@ 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; + + // mobile, m represents mobile + static const double mViewHeight = 48.0; + static const double mViewButtonDimension = 34.0; + static const double mHorizontalPadding = 20.0; + static const double mVerticalPadding = 12.0; } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart index 2a0eb6ec7d353..8fdfb9bb44d3b 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 ed5a667e91862..b2d9945b2bb62 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'; @@ -141,6 +141,16 @@ class _HomeHotKeysState extends State { keyDownHandler: (_) => _scaleWithStep(-0.1), ), + // Reset app scaling + HotKeyItem( + hotKey: HotKey( + KeyCode.digit0, + modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control], + scope: HotKeyScope.inapp, + ), + keyDownHandler: (_) => _scaleToSize(1), + ), + // Open settings dialog openSettingsHotKey(context, widget.userProfile), ]; @@ -182,7 +192,11 @@ class _HomeHotKeysState extends State { Log.info('scale the app from $currentScaleFactor to $textScale'); - ScaledWidgetsFlutterBinding.instance.scaleFactor = (_) => textScale; - await windowSizeManager.setScaleFactor(textScale); + await _scaleToSize(textScale); + } + + Future _scaleToSize(double size) async { + ScaledWidgetsFlutterBinding.instance.scaleFactor = (_) => size; + await windowSizeManager.setScaleFactor(size); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart new file mode 100644 index 0000000000000..1e6279f3041ce --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.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/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((e) => e.item) + .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, + shouldLoadChildViews: 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 tabsBloc = context.read(); + 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 MultiBlocProvider( + providers: [ + BlocProvider.value(value: favoriteBloc), + BlocProvider.value(value: tabsBloc), + ], + child: const FavoriteMenu(minWidth: minWidth), + ); + }, + margin: EdgeInsets.zero, + child: FlowyButton( + onTap: () {}, + margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 7.0), + leftIcon: const FlowySvg( + FlowySvgs.workspace_three_dots_s, + ), + text: FlowyText.regular(LocaleKeys.button_more.tr()), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu.dart new file mode 100644 index 0000000000000..91f178af609bf --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu.dart @@ -0,0 +1,218 @@ +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/application/tabs/tabs_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:appflowy_popover/appflowy_popover.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) { + context.read().openPlugin(view); + PopoverContainer.maybeOf(context)?.close(); + }, + 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 StatefulWidget { + const _FavoriteSearchField({ + required this.width, + required this.onSearch, + }); + + final double width; + final void Function(BuildContext context, String text) onSearch; + + @override + State<_FavoriteSearchField> createState() => _FavoriteSearchFieldState(); +} + +class _FavoriteSearchFieldState extends State<_FavoriteSearchField> { + final focusNode = FocusNode(); + + @override + void initState() { + super.initState(); + focusNode.requestFocus(); + } + + @override + void dispose() { + focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + height: 30, + width: widget.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) => widget.onSearch(context, text), + padding: EdgeInsets.zero, + focusNode: focusNode, + placeholder: LocaleKeys.search_label.tr(), + prefixIcon: const FlowySvg(FlowySvgs.m_search_m), + prefixInsets: const EdgeInsets.only(left: 12.0, right: 8.0), + suffixIcon: const Icon(Icons.close), + suffixInsets: const EdgeInsets.only(right: 8.0), + itemSize: 16.0, + decoration: const BoxDecoration( + color: Colors.transparent, + ), + placeholderStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).hintColor, + fontWeight: FontWeight.w400, + ), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w400, + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu_bloc.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu_bloc.dart new file mode 100644 index 0000000000000..eb5d27183644a --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu_bloc.dart @@ -0,0 +1,115 @@ +import 'package:appflowy/workspace/application/favorite/favorite_service.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(todayViews, query); + thisWeekViews = _filter(thisWeekViews, query); + otherViews = _filter(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) => 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).toList(); + final List todayViews = []; + final List thisWeekViews = []; + final List otherViews = []; + for (final favoriteView in source.items) { + final view = favoriteView.item; + final date = DateTime.fromMillisecondsSinceEpoch( + favoriteView.timestamp.toInt() * 1000, + ); + final diff = DateTime.now().difference(date).inDays; + if (diff == 0) { + todayViews.add(view); + } else if (diff < 7) { + thisWeekViews.add(view); + } else { + otherViews.add(view); + } + } + return (views, todayViews, thisWeekViews, otherViews); + } +} + +@freezed +class FavoriteMenuEvent with _$FavoriteMenuEvent { + const factory FavoriteMenuEvent.initial() = Initial; + const factory FavoriteMenuEvent.search(String query) = Search; +} + +@freezed +class FavoriteMenuState with _$FavoriteMenuState { + const factory FavoriteMenuState({ + @Default([]) List views, + @Default([]) List queriedViews, + @Default([]) List todayViews, + @Default([]) List thisWeekViews, + @Default([]) List otherViews, + }) = _FavoriteMenuState; + + factory FavoriteMenuState.initial() => const FavoriteMenuState(); +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart new file mode 100644 index 0000000000000..40c21676268c2 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart @@ -0,0 +1,67 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; +import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; +import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart'; +import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class FavoriteMoreActions extends StatelessWidget { + const FavoriteMoreActions({super.key, required this.view}); + + final ViewPB view; + + @override + Widget build(BuildContext context) { + return FlowyTooltip( + message: LocaleKeys.menuAppHeader_moreButtonToolTip.tr(), + child: ViewMoreActionButton( + view: view, + spaceType: FolderSpaceType.favorite, + onEditing: (value) => + context.read().add(ViewEvent.setIsEditing(value)), + onAction: (action, _) { + switch (action) { + case ViewMoreActionType.favorite: + case ViewMoreActionType.unFavorite: + context.read().add(FavoriteEvent.toggle(view)); + PopoverContainer.maybeOf(context)?.closeAll(); + break; + case ViewMoreActionType.rename: + NavigatorTextFieldDialog( + title: LocaleKeys.disclosureAction_rename.tr(), + autoSelectAllText: true, + value: view.name, + maxLength: 256, + onConfirm: (newValue, _) { + // can not use bloc here because it has been disposed. + ViewBackendService.updateView( + viewId: view.id, + name: newValue, + ); + }, + ).show(context); + PopoverContainer.maybeOf(context)?.closeAll(); + break; + + case ViewMoreActionType.openInNewTab: + context.read().openTab(view); + break; + case ViewMoreActionType.delete: + case ViewMoreActionType.duplicate: + default: + throw UnsupportedError('$action is not supported'); + } + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.dart new file mode 100644 index 0000000000000..2a849b7e8d0ae --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.dart @@ -0,0 +1,40 @@ +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: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: () { + view.isPinned + ? context.read().add(FavoriteEvent.unpin(view)) + : context.read().add(FavoriteEvent.pin(view)); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_bloc.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_bloc.dart new file mode 100644 index 0000000000000..e4a335e34bac7 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_bloc.dart @@ -0,0 +1,59 @@ +import 'package:appflowy/workspace/application/favorite/favorite_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'favorite_pin_bloc.freezed.dart'; + +class FavoritePinBloc extends Bloc { + FavoritePinBloc() : super(FavoritePinState.initial()) { + on( + (event, emit) async { + await event.when( + initial: () async { + final List views = await _service + .readFavorites() + .fold((s) => s.items.map((v) => v.item).toList(), (f) => []); + emit(state.copyWith(views: views, queriedViews: views)); + }, + search: (query) async { + if (query.isEmpty) { + emit(state.copyWith(queriedViews: state.views)); + return; + } + + final queriedViews = state.views + .where( + (view) => + view.name.toLowerCase().contains(query.toLowerCase()), + ) + .toList(); + emit(state.copyWith(queriedViews: queriedViews)); + }, + ); + }, + ); + } + + final FavoriteService _service = FavoriteService(); +} + +@freezed +class FavoritePinEvent with _$FavoritePinEvent { + const factory FavoritePinEvent.initial() = Initial; + const factory FavoritePinEvent.search(String query) = Search; +} + +@freezed +class FavoritePinState with _$FavoritePinState { + const factory FavoritePinState({ + @Default([]) List views, + @Default([]) List queriedViews, + @Default([]) List> todayViews, + @Default([]) List> lastWeekViews, + @Default([]) List> otherViews, + }) = _FavoritePinState; + + factory FavoritePinState.initial() => const FavoritePinState(); +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_favorite_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_favorite_folder.dart deleted file mode 100644 index 364e12644c3b4..0000000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_favorite_folder.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; -import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class FavoriteFolder extends StatelessWidget { - const FavoriteFolder({ - super.key, - required this.views, - }); - - final List views; - - @override - Widget build(BuildContext context) { - if (views.isEmpty) { - return const SizedBox.shrink(); - } - - return BlocProvider( - create: (context) => FolderBloc(type: FolderCategoryType.favorite) - ..add( - const FolderEvent.initial(), - ), - child: BlocBuilder( - builder: (context, state) { - return Column( - children: [ - FavoriteHeader( - onPressed: () => context - .read() - .add(const FolderEvent.expandOrUnExpand()), - onAdded: () => context - .read() - .add(const FolderEvent.expandOrUnExpand(isExpanded: true)), - ), - if (state.isExpanded) - ...views.map( - (view) => ViewItem( - key: ValueKey( - '${FolderCategoryType.favorite.name} ${view.id}', - ), - categoryType: FolderCategoryType.favorite, - isDraggable: false, - isFirstChild: view.id == views.first.id, - isFeedback: false, - view: view, - level: 0, - onSelected: (view, _) { - if (HardwareKeyboard.instance.isControlPressed) { - context.read().openTab(view); - } - - context.read().openPlugin(view); - }, - onTertiarySelected: (view, _) => - context.read().openTab(view), - ), - ), - ], - ); - }, - ), - ); - } -} - -class FavoriteHeader extends StatefulWidget { - const FavoriteHeader({ - super.key, - required this.onPressed, - required this.onAdded, - }); - - final VoidCallback onPressed; - final VoidCallback onAdded; - - @override - State createState() => _FavoriteHeaderState(); -} - -class _FavoriteHeaderState extends State { - bool onHover = false; - - @override - Widget build(BuildContext context) { - const iconSize = 26.0; - return MouseRegion( - onEnter: (event) => setState(() => onHover = true), - onExit: (event) => setState(() => onHover = false), - child: Row( - children: [ - FlowyTextButton( - LocaleKeys.sideBar_favorites.tr(), - fontColor: AFThemeExtension.of(context).textColor, - fontHoverColor: Theme.of(context).colorScheme.onSurface, - tooltip: LocaleKeys.sideBar_clickToHideFavorites.tr(), - constraints: const BoxConstraints(maxHeight: iconSize), - padding: const EdgeInsets.all(4), - fillColor: Colors.transparent, - onPressed: widget.onPressed, - ), - ], - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart index 422003fdd9b3a..c5042c4de4cff 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart @@ -1,8 +1,7 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; class FolderHeader extends StatefulWidget { const FolderHeader({ @@ -25,42 +24,34 @@ class FolderHeader extends StatefulWidget { } class _FolderHeaderState extends State { - bool onHover = false; + final isHovered = ValueNotifier(false); + + @override + void dispose() { + isHovered.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { - const iconSize = 26.0; - const textPadding = 4.0; return MouseRegion( - onEnter: (event) => setState(() => onHover = true), - onExit: (event) => setState(() => onHover = false), - child: Row( - children: [ - FlowyTextButton( - widget.title, - tooltip: widget.expandButtonTooltip, - constraints: const BoxConstraints( - minHeight: iconSize + textPadding * 2, - ), - fontColor: AFThemeExtension.of(context).textColor, - fontHoverColor: Theme.of(context).colorScheme.onSurface, - padding: const EdgeInsets.all(textPadding), - fillColor: Colors.transparent, - onPressed: widget.onPressed, + onEnter: (_) => isHovered.value = true, + onExit: (_) => isHovered.value = false, + child: FlowyButton( + onTap: widget.onPressed, + margin: const EdgeInsets.symmetric(horizontal: 6.0), + rightIcon: ValueListenableBuilder( + valueListenable: isHovered, + builder: (context, onHover, child) => + Opacity(opacity: onHover ? 1 : 0, child: child), + child: FlowyIconButton( + tooltipText: widget.addButtonTooltip, + icon: const FlowySvg(FlowySvgs.view_item_add_s), + onPressed: widget.onAdded, ), - if (onHover) ...[ - const Spacer(), - FlowyIconButton( - tooltipText: widget.addButtonTooltip, - hoverColor: Theme.of(context).colorScheme.secondaryContainer, - iconPadding: const EdgeInsets.all(2), - height: iconSize, - width: iconSize, - icon: const FlowySvg(FlowySvgs.add_s), - onPressed: widget.onAdded, - ), - ], - ], + ), + iconPadding: 10.0, + text: FlowyText(widget.title), ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart index ecd7b5f9dea05..bd83e06934310 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart @@ -3,20 +3,22 @@ import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/rename_view_dialog.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class SectionFolder extends StatelessWidget { +class SectionFolder extends StatefulWidget { const SectionFolder({ super.key, required this.title, - required this.categoryType, + required this.spaceType, required this.views, this.isHoverEnabled = true, required this.expandButtonTooltip, @@ -24,101 +26,140 @@ class SectionFolder extends StatelessWidget { }); final String title; - final FolderCategoryType categoryType; + final FolderSpaceType spaceType; final List views; final bool isHoverEnabled; final String expandButtonTooltip; final String addButtonTooltip; + @override + State createState() => _SectionFolderState(); +} + +class _SectionFolderState extends State { + final ValueNotifier isHovered = ValueNotifier(false); + + @override + void dispose() { + isHovered.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => FolderBloc(type: categoryType) - ..add( - const FolderEvent.initial(), + return MouseRegion( + onEnter: (_) => isHovered.value = true, + onExit: (_) => isHovered.value = false, + child: BlocProvider( + create: (context) => FolderBloc(type: widget.spaceType) + ..add( + const FolderEvent.initial(), + ), + child: BlocBuilder( + builder: (context, state) { + return Column( + children: [ + _buildHeader(context), + // Pages + const VSpace(4.0), + ..._buildViews(context, state, isHovered), + // Add a placeholder if there are no views + _buildDraggablePlaceholder(context), + ], + ); + }, ), - child: BlocBuilder( - builder: (context, state) { - return Column( - children: [ - FolderHeader( - title: title, - expandButtonTooltip: expandButtonTooltip, - addButtonTooltip: addButtonTooltip, - onPressed: () => context - .read() - .add(const FolderEvent.expandOrUnExpand()), - onAdded: () { - createViewAndShowRenameDialogIfNeeded( - context, - LocaleKeys.newPageText.tr(), - (viewName, _) { - if (viewName.isNotEmpty) { - context.read().add( - SidebarSectionsEvent.createRootViewInSection( - name: viewName, - index: 0, - viewSection: categoryType.toViewSectionPB, - ), - ); + ), + ); + } - context.read().add( - const FolderEvent.expandOrUnExpand( - isExpanded: true, - ), - ); - } - }, + Widget _buildHeader(BuildContext context) { + return FolderHeader( + title: widget.title, + expandButtonTooltip: widget.expandButtonTooltip, + addButtonTooltip: widget.addButtonTooltip, + onPressed: () => + context.read().add(const FolderEvent.expandOrUnExpand()), + onAdded: () { + createViewAndShowRenameDialogIfNeeded( + context, + LocaleKeys.newPageText.tr(), + (viewName, _) { + if (viewName.isNotEmpty) { + context.read().add( + SidebarSectionsEvent.createRootViewInSection( + name: viewName, + index: 0, + viewSection: widget.spaceType.toViewSectionPB, + ), ); - }, - ), - if (state.isExpanded) - ...views.map( - (view) => ViewItem( - key: ValueKey( - '${categoryType.name} ${view.id}', + + context.read().add( + const FolderEvent.expandOrUnExpand( + isExpanded: true, ), - categoryType: categoryType, - isFirstChild: view.id == views.first.id, - view: view, - level: 0, - leftPadding: 16, - isFeedback: false, - onSelected: (view, viewContext) { - if (HardwareKeyboard.instance.isControlPressed) { - context.read().openTab(view); - } + ); + } + }, + ); + }, + ); + } + + Iterable _buildViews( + BuildContext context, + FolderState state, + ValueNotifier isHovered, + ) { + if (!state.isExpanded) { + return []; + } - context.read().openPlugin(view); - }, - onTertiarySelected: (view, viewContext) => - context.read().openTab(view), - isHoverEnabled: isHoverEnabled, - ), - ), - if (views.isEmpty) - ViewItem( - categoryType: categoryType, - view: ViewPB( - parentViewId: context - .read() - .state - .currentWorkspace - ?.workspaceId ?? - '', - ), - level: 0, - leftPadding: 16, - isFeedback: false, - onSelected: (_, __) {}, - onTertiarySelected: (_, __) {}, - isHoverEnabled: isHoverEnabled, - isPlaceholder: true, - ), - ], - ); + return widget.views.map( + (view) => ViewItem( + key: ValueKey('${widget.spaceType.name} ${view.id}'), + spaceType: widget.spaceType, + isFirstChild: view.id == widget.views.first.id, + view: view, + level: 0, + leftPadding: HomeSpaceViewSizes.leftPadding, + isFeedback: false, + isHovered: isHovered, + onSelected: (viewContext, view) { + if (HardwareKeyboard.instance.isControlPressed) { + context.read().openTab(view); + } + + context.read().openPlugin(view); }, + onTertiarySelected: (viewContext, view) => + context.read().openTab(view), + isHoverEnabled: widget.isHoverEnabled, + ), + ); + } + + Widget _buildDraggablePlaceholder(BuildContext context) { + if (widget.views.isNotEmpty) { + return const SizedBox.shrink(); + } + return ViewItem( + spaceType: widget.spaceType, + view: ViewPB( + parentViewId: context + .read() + .state + .currentWorkspace + ?.workspaceId ?? + '', ), + level: 0, + leftPadding: HomeSpaceViewSizes.leftPadding, + isFeedback: false, + onSelected: (_, __) {}, + onTertiarySelected: (_, __) {}, + isHoverEnabled: widget.isHoverEnabled, + isPlaceholder: true, ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart new file mode 100644 index 0000000000000..6c16609a33ee5 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart @@ -0,0 +1,71 @@ +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()), + // Enable it when the widget button is ready + // 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 67% rename from frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart index 4ef548050775b..c7dc54126e8cb 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 @@ -3,11 +3,13 @@ import 'dart:io' show Platform; import 'package:appflowy/core/frameless_window.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; 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'; @@ -50,34 +52,46 @@ class SidebarTopMenu extends StatelessWidget { ? FlowySvgs.flowy_logo_dark_mode_xl : FlowySvgs.flowy_logo_text_xl; - return FlowySvg( - svgData, - size: const Size(92, 17), - blendMode: null, + return Padding( + padding: const EdgeInsets.only(top: 12.0, left: 4), + child: FlowySvg( + svgData, + size: const Size(92, 17), + blendMode: null, + ), ); } Widget _buildCollapseMenuButton(BuildContext context) { + final color = Theme.of(context).isLightMode ? Colors.white : Colors.black; final textSpan = TextSpan( children: [ TextSpan( text: '${LocaleKeys.sideBar_closeSidebar.tr()}\n', + style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: color), ), TextSpan( text: Platform.isMacOS ? '⌘+.' : 'Ctrl+\\', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Theme.of(context).hintColor), ), ], ); - return FlowyTooltip( - richMessage: textSpan, - child: FlowyIconButton( - width: PlatformExtension.isWindows ? 30 : 28, - hoverColor: Colors.transparent, - onPressed: () => context - .read() - .add(const HomeSettingEvent.collapseMenu()), - iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4), - icon: const FlowySvg(FlowySvgs.hide_menu_m), + + return Padding( + padding: const EdgeInsets.only(top: 12.0), + child: FlowyTooltip( + richMessage: textSpan, + child: FlowyIconButton( + width: 24, + onPressed: () => context + .read() + .add(const HomeSettingEvent.collapseMenu()), + iconPadding: const EdgeInsets.all(2), + icon: const FlowySvg(FlowySvgs.hide_menu_s), + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_user.dart similarity index 93% rename from frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_user.dart index dc089e27a2034..20465d0faa1ab 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,14 +28,16 @@ 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), const NotificationButton(), ], ), 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 91% rename from frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart index dbbf3f0d0eff6..0e49c2acd6274 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,8 +37,10 @@ class SidebarFolder extends StatelessWidget { return const SizedBox.shrink(); } return Padding( - padding: const EdgeInsets.only(bottom: 10), - child: FavoriteFolder(views: state.views), + padding: const EdgeInsets.only(top: 16.0, bottom: 10), + child: FavoriteFolder( + views: state.views.map((e) => e.item).toList(), + ), ); }, ), @@ -85,7 +86,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 +96,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 +106,7 @@ class PersonalSectionFolder extends SectionFolder { PersonalSectionFolder({super.key, required super.views}) : super( title: LocaleKeys.sideBar_personal.tr(), - categoryType: FolderCategoryType.public, + spaceType: FolderSpaceType.public, expandButtonTooltip: LocaleKeys.sideBar_clickToHidePersonal.tr(), addButtonTooltip: LocaleKeys.sideBar_addAPage.tr(), ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart new file mode 100644 index 0000000000000..f58a1d43c9e0c --- /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 0e2d020f677ee..8c6d9c5cf7ac8 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart @@ -1,10 +1,9 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy/workspace/presentation/home/hotkeys.dart'; import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart'; import 'package:appflowy_backend/log.dart'; @@ -12,7 +11,9 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show UserProfilePB; import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; @@ -47,15 +48,14 @@ class UserSettingButton extends StatelessWidget { @override Widget build(BuildContext context) { - return FlowyTooltip( - message: LocaleKeys.settings_menu_open.tr(), - child: IconButton( - onPressed: () => showSettingsDialog(context, userProfile), - icon: SizedBox.square( - dimension: 20, - child: FlowySvg( - FlowySvgs.settings_m, - color: Theme.of(context).colorScheme.tertiary, + return SizedBox.square( + dimension: HomeSizes.workspaceSectionHeight, + child: FlowyTooltip( + message: LocaleKeys.settings_menu_open.tr(), + child: FlowyButton( + onTap: () => showSettingsDialog(context, userProfile), + text: const FlowySvg( + FlowySvgs.settings_s, ), ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart index 1e5aec2f0e27a..31cdf736857d2 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,8 +222,9 @@ class _SidebarState extends State<_Sidebar> { // top menu const Padding(padding: menuHorizontalInset, child: SidebarTopMenu()), // user or workspace, setting - Padding( - padding: menuHorizontalInset, + Container( + height: HomeSizes.workspaceSectionHeight, + padding: menuHorizontalInset - const EdgeInsets.only(right: 6), child: // if the workspaces are empty, show the user profile instead userState.isCollabWorkspaceOn && userState.workspaces.isNotEmpty @@ -231,12 +232,15 @@ class _SidebarState extends State<_Sidebar> { : SidebarUser(userProfile: widget.userProfile), ), if (FeatureFlag.search.isOn) ...[ - const VSpace(8), - const Padding( + const VSpace(6), + Container( padding: menuHorizontalInset, - child: _SidebarSearchButton(), + height: HomeSizes.searchSectionHeight, + child: const _SidebarSearchButton(), ), ], + // new page button + const SidebarNewPageButton(), // scrollable document list Expanded( child: Padding( @@ -256,11 +260,14 @@ class _SidebarState extends State<_Sidebar> { // trash const Padding( padding: menuHorizontalInset, - child: SidebarTrashButton(), + child: Divider(height: 1.0, color: Color(0x141F2329)), + ), + const VSpace(14), + const Padding( + padding: menuHorizontalInset, + child: SidebarFooter(), ), const VSpace(10), - // new page button - const SidebarNewPageButton(), ], ), ); @@ -289,7 +296,8 @@ class _SidebarSearchButton extends StatelessWidget { return FlowyButton( onTap: () => CommandPalette.of(context).toggle(), leftIcon: const FlowySvg(FlowySvgs.search_s), - text: FlowyText(LocaleKeys.search_label.tr()), + iconPadding: 10.0, + text: FlowyText.regular(LocaleKeys.search_label.tr()), ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart deleted file mode 100644 index eac80118b4562..0000000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/extension.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class SidebarNewPageButton extends StatelessWidget { - const SidebarNewPageButton({ - super.key, - }); - - @override - Widget build(BuildContext context) { - final child = FlowyTextButton( - LocaleKeys.newPageText.tr(), - fillColor: Colors.transparent, - hoverColor: Colors.transparent, - fontColor: Theme.of(context).colorScheme.tertiary, - onPressed: () async => createViewAndShowRenameDialogIfNeeded( - context, - LocaleKeys.newPageText.tr(), - (viewName, _) { - if (viewName.isNotEmpty) { - // if the workspace is collaborative, create the view in the private section by default. - final section = - context.read().state.isCollabWorkspaceOn - ? ViewSectionPB.Private - : ViewSectionPB.Public; - context.read().add( - SidebarSectionsEvent.createRootViewInSection( - name: viewName, - viewSection: section, - ), - ); - } - }, - ), - heading: Container( - width: 16, - height: 16, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).colorScheme.surface, - ), - child: FlowySvg( - FlowySvgs.new_app_s, - color: Theme.of(context).colorScheme.primary, - ), - ), - padding: const EdgeInsets.all(0), - ); - - return SizedBox( - height: 60, - child: TopBorder( - color: Theme.of(context).dividerColor, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 18), - child: child, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_trash.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_trash.dart deleted file mode 100644 index b4a6eb344a129..0000000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_trash.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/startup/plugin/plugin.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; - -class SidebarTrashButton extends StatelessWidget { - const SidebarTrashButton({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: getIt().notifier, - builder: (context, value, child) { - return FlowyHover( - style: HoverStyle( - hoverColor: AFThemeExtension.of(context).greySelect, - ), - isSelected: () => getIt().latestOpenView == null, - child: SizedBox( - height: 26, - child: InkWell( - onTap: () { - getIt().latestOpenView = null; - getIt().add( - TabsEvent.openPlugin( - plugin: makePlugin(pluginType: PluginType.trash), - ), - ); - }, - child: _buildTextButton(context), - ), - ), - ); - }, - ); - } - - Widget _buildTextButton(BuildContext context) { - return Row( - children: [ - const HSpace(6), - const FlowySvg( - FlowySvgs.trash_m, - size: Size(16, 16), - ), - const HSpace(6), - FlowyText.medium( - LocaleKeys.trash_text.tr(), - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart index 39301799d62ba..53baa2599e40c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart @@ -16,6 +16,7 @@ enum WorkspaceMoreAction { rename, delete, leave, + divider, } class WorkspaceMoreActionList extends StatelessWidget { @@ -32,6 +33,7 @@ class WorkspaceMoreActionList extends StatelessWidget { final actions = []; if (myRole.isOwner) { actions.add(WorkspaceMoreAction.rename); + actions.add(WorkspaceMoreAction.divider); actions.add(WorkspaceMoreAction.delete); } else if (myRole.canLeave) { actions.add(WorkspaceMoreAction.leave); @@ -40,20 +42,23 @@ class WorkspaceMoreActionList extends StatelessWidget { return const SizedBox.shrink(); } return PopoverActionList<_WorkspaceMoreActionWrapper>( - direction: PopoverDirection.bottomWithCenterAligned, + direction: PopoverDirection.bottomWithLeftAligned, actions: actions .map((e) => _WorkspaceMoreActionWrapper(e, workspace)) .toList(), + constraints: const BoxConstraints(minWidth: 220), buildChild: (controller) { - return FlowyButton( - margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - useIntrinsicWidth: true, - text: const FlowySvg( - FlowySvgs.three_dots_vertical_s, + return SizedBox.square( + dimension: 24.0, + child: FlowyButton( + margin: const EdgeInsets.symmetric(horizontal: 4.0), + text: const FlowySvg( + FlowySvgs.workspace_three_dots_s, + ), + onTap: () { + controller.show(); + }, ), - onTap: () { - controller.show(); - }, ); }, onSelected: (action, controller) {}, @@ -68,20 +73,37 @@ class _WorkspaceMoreActionWrapper extends CustomActionCell { final UserWorkspacePB workspace; @override - Widget buildWithContext(BuildContext context) { + Widget buildWithContext(BuildContext context, PopoverController controller) { + if (inner == WorkspaceMoreAction.divider) { + return const Divider(); + } + + return _buildActionButton(context, controller); + } + + Widget _buildActionButton( + BuildContext context, + PopoverController controller, + ) { return FlowyButton( - text: FlowyText( + leftIcon: buildLeftIcon(context), + iconPadding: 10.0, + text: FlowyText.regular( name, - color: inner == WorkspaceMoreAction.delete + fontSize: 14.0, + color: [WorkspaceMoreAction.delete, WorkspaceMoreAction.leave] + .contains(inner) ? Theme.of(context).colorScheme.error : null, ), - margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0), + margin: const EdgeInsets.all(6), onTap: () async { PopoverContainer.of(context).closeAll(); final workspaceBloc = context.read(); switch (inner) { + case WorkspaceMoreAction.divider: + break; case WorkspaceMoreAction.delete: await NavigatorAlertDialog( title: LocaleKeys.workspace_deleteWorkspaceHintText.tr(), @@ -93,7 +115,7 @@ class _WorkspaceMoreActionWrapper extends CustomActionCell { ).show(context); case WorkspaceMoreAction.rename: await NavigatorTextFieldDialog( - title: LocaleKeys.workspace_create.tr(), + title: LocaleKeys.workspace_renameWorkspace.tr(), value: workspace.name, hintText: '', autoSelectAllText: true, @@ -132,6 +154,27 @@ class _WorkspaceMoreActionWrapper extends CustomActionCell { return LocaleKeys.button_rename.tr(); case WorkspaceMoreAction.leave: return LocaleKeys.workspace_leaveCurrentWorkspace.tr(); + case WorkspaceMoreAction.divider: + return ''; + } + } + + Widget buildLeftIcon(BuildContext context) { + switch (inner) { + case WorkspaceMoreAction.delete: + return FlowySvg( + FlowySvgs.delete_s, + color: Theme.of(context).colorScheme.error, + ); + case WorkspaceMoreAction.rename: + return const FlowySvg(FlowySvgs.view_item_rename_s); + case WorkspaceMoreAction.leave: + return FlowySvg( + FlowySvgs.logout_s, + color: Theme.of(context).colorScheme.error, + ); + case WorkspaceMoreAction.divider: + return const SizedBox.shrink(); } } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart index f3f78ee99b926..d179c4ce0f76e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart @@ -1,6 +1,5 @@ import 'dart:math'; -import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/util/color_generator/color_generator.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; @@ -14,12 +13,14 @@ class WorkspaceIcon extends StatefulWidget { required this.workspace, required this.enableEdit, required this.iconSize, + required this.fontSize, required this.onSelected, }); final UserWorkspacePB workspace; final double iconSize; final bool enableEdit; + final double fontSize; final void Function(EmojiPickerResult) onSelected; @override @@ -35,24 +36,24 @@ class _WorkspaceIconState extends State { ? Container( width: widget.iconSize, alignment: Alignment.center, - child: EmojiText( - emoji: widget.workspace.icon, + child: FlowyText.emoji( + widget.workspace.icon, fontSize: widget.iconSize, ), ) : Container( alignment: Alignment.center, width: widget.iconSize, - height: max(widget.iconSize, 26), + height: min(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 efc4372ee82b4..8a1b172aff241 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,9 @@ -import 'package:flutter/material.dart'; +import 'dart:io'; 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 +15,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 +41,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 +53,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 +70,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 +90,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 +104,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: 44, + 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 +156,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 +184,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,12 +239,12 @@ 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, + mainAxisAlignment: MainAxisAlignment.center, children: [ // workspace name FlowyText.medium( @@ -213,8 +253,9 @@ class _WorkspaceInfo extends StatelessWidget { overflow: TextOverflow.ellipsis, withTooltip: true, ), + if (Platform.isMacOS) const VSpace(2.0), // workspace members count - FlowyText( + FlowyText.regular( state.isLoading ? '' : LocaleKeys.settings_appearance_members_membersCount @@ -263,3 +304,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 76% rename from frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart index 141d7d7f4c740..d1a09afc28fc2 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,7 +49,6 @@ class _SidebarWorkspaceState extends State { ), ), UserSettingButton(userProfile: widget.userProfile), - const HSpace(4), const NotificationButton(), ], ); @@ -144,7 +142,7 @@ class _SidebarWorkspaceState extends State { } } -class SidebarSwitchWorkspaceButton extends StatelessWidget { +class SidebarSwitchWorkspaceButton extends StatefulWidget { const SidebarSwitchWorkspaceButton({ super.key, required this.userProfile, @@ -154,16 +152,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 +189,7 @@ class SidebarSwitchWorkspaceButton extends StatelessWidget { } Log.info('open workspace menu'); return WorkspacesMenu( - userProfile: userProfile, + userProfile: widget.userProfile, currentWorkspace: currentWorkspace, workspaces: workspaces, ); @@ -185,34 +198,43 @@ class SidebarSwitchWorkspaceButton extends StatelessWidget { ); }, child: FlowyButton( - margin: const EdgeInsets.symmetric(vertical: 8), - text: Row( - children: [ - const HSpace(2.0), - SizedBox.square( - dimension: 30.0, - child: WorkspaceIcon( - workspace: currentWorkspace, - iconSize: 20, + margin: EdgeInsets.zero, + text: SizedBox( + height: 30, + child: Row( + children: [ + const HSpace(6.0), + WorkspaceIcon( + 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( - child: FlowyText.medium( - currentWorkspace.name, - overflow: TextOverflow.ellipsis, - withTooltip: true, + const HSpace(10), + Flexible( + child: FlowyText.medium( + widget.currentWorkspace.name, + overflow: TextOverflow.ellipsis, + withTooltip: true, + ), ), - ), - const FlowySvg(FlowySvgs.drop_menu_show_m), - ], + const HSpace(4), + ValueListenableBuilder( + valueListenable: _isWorkSpaceMenuExpanded, + builder: (context, value, _) => FlowySvg( + value + ? FlowySvgs.workspace_drop_down_menu_hide_s + : FlowySvgs.workspace_drop_down_menu_show_s, + ), + ), + ], + ), ), ), ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart index 658d60bfe7b7d..881d926df322e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart @@ -15,6 +15,8 @@ enum DraggableHoverPosition { bottom, } +const kDraggableViewItemDividerHeight = 2.0; + class DraggableViewItem extends StatefulWidget { const DraggableViewItem({ super.key, @@ -45,8 +47,7 @@ class DraggableViewItem extends StatefulWidget { class _DraggableViewItemState extends State { DraggableHoverPosition position = DraggableHoverPosition.none; - - final _dividerHeight = 2.0; + final hoverColor = const Color(0xFF00C8FF); @override Widget build(BuildContext context) { @@ -100,29 +101,26 @@ class _DraggableViewItemState extends State { // only show the top border when the draggable item is the first child if (widget.isFirstChild) Divider( - height: _dividerHeight, - thickness: _dividerHeight, + height: kDraggableViewItemDividerHeight, + thickness: kDraggableViewItemDividerHeight, color: position == DraggableHoverPosition.top - ? widget.topHighlightColor ?? - Theme.of(context).colorScheme.secondary + ? widget.topHighlightColor ?? hoverColor : Colors.transparent, ), DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(6.0), color: position == DraggableHoverPosition.center - ? widget.centerHighlightColor ?? - Theme.of(context).colorScheme.secondary.withOpacity(0.5) + ? widget.centerHighlightColor ?? hoverColor.withOpacity(0.5) : Colors.transparent, ), child: widget.child, ), Divider( - height: _dividerHeight, - thickness: _dividerHeight, + height: kDraggableViewItemDividerHeight, + thickness: kDraggableViewItemDividerHeight, color: position == DraggableHoverPosition.bottom - ? widget.bottomHighlightColor ?? - Theme.of(context).colorScheme.secondary + ? widget.bottomHighlightColor ?? hoverColor : Colors.transparent, ), ], @@ -137,10 +135,10 @@ class _DraggableViewItemState extends State { top: 0, left: 0, right: 0, - height: _dividerHeight, + height: kDraggableViewItemDividerHeight, child: Divider( - height: _dividerHeight, - thickness: _dividerHeight, + height: kDraggableViewItemDividerHeight, + thickness: kDraggableViewItemDividerHeight, color: position == DraggableHoverPosition.top ? widget.topHighlightColor ?? Theme.of(context).colorScheme.secondary @@ -161,10 +159,10 @@ class _DraggableViewItemState extends State { bottom: 0, left: 0, right: 0, - height: _dividerHeight, + height: kDraggableViewItemDividerHeight, child: Divider( - height: _dividerHeight, - thickness: _dividerHeight, + height: kDraggableViewItemDividerHeight, + thickness: kDraggableViewItemDividerHeight, color: position == DraggableHoverPosition.bottom ? widget.bottomHighlightColor ?? Theme.of(context).colorScheme.secondary diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_action_type.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_action_type.dart index 9fda07d7d277d..5b04e5050103e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_action_type.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_action_type.dart @@ -10,8 +10,13 @@ enum ViewMoreActionType { duplicate, copyLink, // not supported yet. rename, - moveTo, // not supported yet. + moveTo, openInNewTab, + changeIcon, + collapseAllPages, // including sub pages + divider, + lastModified, + created, } extension ViewMoreActionTypeExtension on ViewMoreActionType { @@ -33,27 +38,63 @@ extension ViewMoreActionTypeExtension on ViewMoreActionType { return LocaleKeys.disclosureAction_moveTo.tr(); case ViewMoreActionType.openInNewTab: return LocaleKeys.disclosureAction_openNewTab.tr(); + case ViewMoreActionType.changeIcon: + return LocaleKeys.disclosureAction_changeIcon.tr(); + case ViewMoreActionType.collapseAllPages: + return LocaleKeys.disclosureAction_collapseAllPages.tr(); + case ViewMoreActionType.divider: + case ViewMoreActionType.lastModified: + case ViewMoreActionType.created: + return ''; } } - Widget icon(Color iconColor) { + Widget get leftIcon { switch (this) { case ViewMoreActionType.delete: - return const FlowySvg(FlowySvgs.delete_s); + return const FlowySvg(FlowySvgs.trash_s, blendMode: null); case ViewMoreActionType.favorite: - return const FlowySvg(FlowySvgs.unfavorite_s); - case ViewMoreActionType.unFavorite: return const FlowySvg(FlowySvgs.favorite_s); + case ViewMoreActionType.unFavorite: + return const FlowySvg(FlowySvgs.unfavorite_s); case ViewMoreActionType.duplicate: - return const FlowySvg(FlowySvgs.copy_s); + return const FlowySvg(FlowySvgs.duplicate_s); case ViewMoreActionType.copyLink: return const Icon(Icons.copy); case ViewMoreActionType.rename: - return const FlowySvg(FlowySvgs.edit_s); + return const FlowySvg(FlowySvgs.view_item_rename_s); + case ViewMoreActionType.moveTo: + return const FlowySvg(FlowySvgs.move_to_s); + case ViewMoreActionType.openInNewTab: + return const FlowySvg(FlowySvgs.view_item_open_in_new_tab_s); + case ViewMoreActionType.changeIcon: + return const FlowySvg(FlowySvgs.change_icon_s); + case ViewMoreActionType.collapseAllPages: + return const FlowySvg(FlowySvgs.collapse_all_page_s); + case ViewMoreActionType.divider: + case ViewMoreActionType.lastModified: + case ViewMoreActionType.created: + return const SizedBox.shrink(); + } + } + + Widget get rightIcon { + switch (this) { + case ViewMoreActionType.changeIcon: case ViewMoreActionType.moveTo: - return const Icon(Icons.move_to_inbox); + return const FlowySvg(FlowySvgs.view_item_right_arrow_s); + case ViewMoreActionType.favorite: + case ViewMoreActionType.unFavorite: + case ViewMoreActionType.duplicate: + case ViewMoreActionType.copyLink: + case ViewMoreActionType.rename: case ViewMoreActionType.openInNewTab: - return const FlowySvg(FlowySvgs.full_view_s); + case ViewMoreActionType.collapseAllPages: + case ViewMoreActionType.divider: + case ViewMoreActionType.delete: + case ViewMoreActionType.lastModified: + case ViewMoreActionType.created: + return const SizedBox.shrink(); } } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_add_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_add_button.dart index e020751e0bee6..1cfff3c6e6509 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_add_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_add_button.dart @@ -1,15 +1,14 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/document.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/import/import_panel.dart'; - import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:easy_localization/easy_localization.dart'; class ViewAddButton extends StatelessWidget { const ViewAddButton({ @@ -51,13 +50,12 @@ class ViewAddButton extends StatelessWidget { return PopoverActionList( direction: PopoverDirection.bottomWithLeftAligned, actions: _actions, - offset: const Offset(0, 8), + offset: const Offset(0, 4), buildChild: (popover) { return FlowyIconButton( hoverColor: Colors.transparent, - iconPadding: const EdgeInsets.all(2), - width: 26, - icon: const FlowySvg(FlowySvgs.add_s), + width: 24, + icon: const FlowySvg(FlowySvgs.view_item_add_s), onPressed: () { onEditing(true); popover.show(); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart index 057b3d8f99188..a6c88b3059484 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,20 @@ 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, + this.shouldLoadChildViews = true, }); 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,10 +97,25 @@ 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; + + final bool shouldLoadChildViews; + @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => ViewBloc(view: view)..add(const ViewEvent.initial()), + create: (_) => + ViewBloc(view: view, shouldLoadChildViews: shouldLoadChildViews) + ..add(const ViewEvent.initial()), child: BlocConsumer( listenWhen: (p, c) => c.lastCreatedView != null && @@ -100,7 +127,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 +140,10 @@ class ViewItem extends StatelessWidget { height: height, isHoverEnabled: isHoverEnabled, isPlaceholder: isPlaceholder, + isHovered: isHovered, + shouldRenderChildren: shouldRenderChildren, + leftIconBuilder: leftIconBuilder, + rightIconsBuilder: rightIconsBuilder, ); }, ), @@ -128,7 +159,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 +172,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 +199,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 +211,7 @@ class InnerViewItem extends StatelessWidget { parentView: parentView, level: level, showActions: showActions, - categoryType: categoryType, + spaceType: spaceType, onSelected: onSelected, onTertiarySelected: onTertiarySelected, isExpanded: isExpanded, @@ -181,56 +220,40 @@ class InnerViewItem extends StatelessWidget { isFeedback: isFeedback, height: height, isPlaceholder: isPlaceholder, + isHovered: isHovered, + leftIconBuilder: leftIconBuilder, + rightIconsBuilder: rightIconsBuilder, ); // if the view is expanded and has child views, render its child views - if (isExpanded) { - if (childViews.isNotEmpty) { - final children = childViews.map((childView) { - return ViewItem( - key: ValueKey('${categoryType.name} ${childView.id}'), - parentView: view, - categoryType: categoryType, - isFirstChild: childView.id == childViews.first.id, - view: childView, - level: level + 1, - onSelected: onSelected, - onTertiarySelected: onTertiarySelected, - isDraggable: isDraggable, - leftPadding: leftPadding, - isFeedback: isFeedback, - isPlaceholder: isPlaceholder, - ); - }).toList(); - - child = Column( - mainAxisSize: MainAxisSize.min, - children: [ - child, - ...children, - ], - ); - } else { - child = Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - child, - Container( - height: height, - alignment: Alignment.centerLeft, - child: Padding( - // add 2px to make the text align with the view item - padding: EdgeInsets.only(left: (level + 1) * leftPadding + 2), - child: FlowyText.medium( - LocaleKeys.noPagesInside.tr(), - color: Theme.of(context).hintColor, - ), - ), - ), - ], + if (isExpanded && shouldRenderChildren && childViews.isNotEmpty) { + final children = childViews.map((childView) { + return ViewItem( + key: ValueKey('${spaceType.name} ${childView.id}'), + parentView: view, + spaceType: spaceType, + isFirstChild: childView.id == childViews.first.id, + view: childView, + level: level + 1, + onSelected: onSelected, + onTertiarySelected: onTertiarySelected, + isDraggable: isDraggable, + leftPadding: leftPadding, + isFeedback: isFeedback, + isPlaceholder: isPlaceholder, + isHovered: isHovered, + leftIconBuilder: leftIconBuilder, + rightIconsBuilder: rightIconsBuilder, ); - } + }).toList(); + + child = Column( + mainAxisSize: MainAxisSize.min, + children: [ + child, + ...children, + ], + ); } // wrap the child with DraggableItem if isDraggable is true @@ -246,16 +269,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 +297,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 +313,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 +331,7 @@ class InnerViewItem extends StatelessWidget { context.read().add( ViewEvent.updateViewVisibility( from, - categoryType == FolderCategoryType.public, + spaceType == FolderSpaceType.public, ), ); } @@ -312,7 +346,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 +354,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 +372,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 +422,15 @@ 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), + // const SizedBox( + // width: 6.0, + // height: 1, + // ), // title Expanded( child: FlowyText.regular( @@ -398,25 +442,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 +497,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 +541,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 +597,7 @@ class _SingleInnerViewItemState extends State { viewName, pluginBuilder.layoutType!, openAfterCreated: openAfterCreated, - section: widget.categoryType.toViewSectionPB, + section: widget.spaceType.toViewSectionPB, ), ); } @@ -554,9 +618,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 +649,20 @@ class _SingleInnerViewItemState extends State { case ViewMoreActionType.openInNewTab: context.read().openTab(widget.view); break; + case ViewMoreActionType.collapseAllPages: + context.read().add(const ViewEvent.collapseAllPages()); + break; + case ViewMoreActionType.changeIcon: + if (data is! EmojiPickerResult) { + return; + } + final result = data; + ViewBackendService.updateViewIcon( + viewId: widget.view.id, + viewIcon: result.emoji, + iconType: result.type.toProto(), + ); + break; default: throw UnsupportedError('$action is not supported'); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart index 0cefde700e8a0..68d179da50709 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart @@ -1,11 +1,12 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/base/icon/icon_picker.dart'; +import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:flutter/material.dart'; - import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; /// ··· button beside the view name class ViewMoreActionButton extends StatelessWidget { @@ -14,59 +15,179 @@ class ViewMoreActionButton extends StatelessWidget { required this.view, required this.onEditing, required this.onAction, + required this.spaceType, }); final ViewPB view; final void Function(bool value) onEditing; - final void Function(ViewMoreActionType) onAction; + final void Function(ViewMoreActionType type, dynamic data) onAction; + final FolderSpaceType spaceType; @override Widget build(BuildContext context) { - final supportedActionTypes = [ - ViewMoreActionType.rename, - ViewMoreActionType.delete, - ViewMoreActionType.duplicate, - ViewMoreActionType.openInNewTab, - view.isFavorite - ? ViewMoreActionType.unFavorite - : ViewMoreActionType.favorite, - ]; + final wrappers = _buildActionTypeWrappers(); return PopoverActionList( - direction: PopoverDirection.bottomWithCenterAligned, + direction: PopoverDirection.bottomWithLeftAligned, offset: const Offset(0, 8), - actions: supportedActionTypes - .map((e) => ViewMoreActionTypeWrapper(e)) - .toList(), + actions: wrappers, + constraints: const BoxConstraints( + minWidth: 260, + ), buildChild: (popover) { return FlowyIconButton( - hoverColor: Colors.transparent, - iconPadding: const EdgeInsets.all(2), - width: 26, - icon: const FlowySvg(FlowySvgs.details_s), + width: 24, + icon: const FlowySvg(FlowySvgs.workspace_three_dots_s), onPressed: () { onEditing(true); popover.show(); }, ); }, - onSelected: (action, popover) { - onEditing(false); - onAction(action.inner); - popover.close(); - }, + onSelected: (_, __) {}, onClosed: () => onEditing(false), ); } + + List _buildActionTypeWrappers() { + final actionTypes = _buildActionTypes(); + return actionTypes + .map( + (e) => ViewMoreActionTypeWrapper(e, (controller, data) { + onEditing(false); + onAction(e, data); + controller.close(); + }), + ) + .toList(); + } + + List _buildActionTypes() { + final List actionTypes = []; + switch (spaceType) { + case FolderSpaceType.favorite: + actionTypes.addAll([ + ViewMoreActionType.unFavorite, + ViewMoreActionType.divider, + ViewMoreActionType.rename, + ViewMoreActionType.openInNewTab, + ]); + break; + default: + actionTypes.addAll([ + view.isFavorite + ? ViewMoreActionType.unFavorite + : ViewMoreActionType.favorite, + ViewMoreActionType.divider, + ViewMoreActionType.rename, + ViewMoreActionType.changeIcon, + ViewMoreActionType.duplicate, + ViewMoreActionType.delete, + ViewMoreActionType.divider, + ViewMoreActionType.collapseAllPages, + ViewMoreActionType.divider, + ViewMoreActionType.openInNewTab, + ]); + } + return actionTypes; + } } -class ViewMoreActionTypeWrapper extends ActionCell { - ViewMoreActionTypeWrapper(this.inner); +class ViewMoreActionTypeWrapper extends CustomActionCell { + ViewMoreActionTypeWrapper(this.inner, this.onTap); final ViewMoreActionType inner; + final void Function(PopoverController controller, dynamic data) onTap; @override - Widget? leftIcon(Color iconColor) => inner.icon(iconColor); + Widget buildWithContext(BuildContext context, PopoverController controller) { + if (inner == ViewMoreActionType.divider) { + return _buildDivider(); + } else if (inner == ViewMoreActionType.lastModified) { + return _buildLastModified(context); + } else if (inner == ViewMoreActionType.created) { + return _buildCreated(context); + } else if (inner == ViewMoreActionType.changeIcon) { + return _buildEmojiActionButton(context, controller); + } else { + return _buildNormalActionButton(context, controller); + } + } - @override - String get name => inner.name; + Widget _buildNormalActionButton( + BuildContext context, + PopoverController controller, + ) { + return _buildActionButton(context, () => onTap(controller, null)); + } + + Widget _buildEmojiActionButton( + BuildContext context, + PopoverController controller, + ) { + final child = _buildActionButton(context, null); + + return AppFlowyPopover( + constraints: BoxConstraints.loose(const Size(364, 356)), + clickHandler: PopoverClickHandler.gestureDetector, + popupBuilder: (_) => FlowyIconPicker( + onSelected: (result) => onTap(controller, result), + ), + child: child, + ); + } + + Widget _buildDivider() { + return const Padding( + padding: EdgeInsets.all(8.0), + child: Divider(height: 1.0), + ); + } + + Widget _buildLastModified(BuildContext context) { + return Container( + height: 40, + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: const Column( + crossAxisAlignment: CrossAxisAlignment.start, + ), + ); + } + + Widget _buildCreated(BuildContext context) { + return Container( + height: 40, + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: const Column( + crossAxisAlignment: CrossAxisAlignment.start, + ), + ); + } + + Widget _buildActionButton( + BuildContext context, + VoidCallback? onTap, + ) { + return Container( + height: 34, + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: FlowyButton( + margin: const EdgeInsets.symmetric(horizontal: 6), + leftIcon: inner.leftIcon, + rightIcon: inner.rightIcon, + iconPadding: 10.0, + text: SizedBox( + height: 18.0, + child: FlowyText.regular( + inner.name, + color: inner == ViewMoreActionType.delete + ? Theme.of(context).colorScheme.error + : null, + ), + ), + onTap: onTap, + ), + ); + } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart index c046c8379f0de..4201ec5396477 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart @@ -1,17 +1,17 @@ import 'dart:io'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; import 'package:appflowy/workspace/presentation/home/home_stack.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -65,23 +65,39 @@ class FlowyNavigation extends StatelessWidget { buildWhen: (p, c) => p.isMenuCollapsed != c.isMenuCollapsed, builder: (context, state) { if (!PlatformExtension.isWindows && state.isMenuCollapsed) { - return RotationTransition( - turns: const AlwaysStoppedAnimation(180 / 360), - child: FlowyTooltip( - richMessage: sidebarTooltipTextSpan( - context, - LocaleKeys.sideBar_openSidebar.tr(), + final color = + Theme.of(context).isLightMode ? Colors.white : Colors.black; + final textSpan = TextSpan( + children: [ + TextSpan( + text: '${LocaleKeys.sideBar_openSidebar.tr()}\n', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: color), + ), + TextSpan( + text: Platform.isMacOS ? '⌘+.' : 'Ctrl+\\', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Theme.of(context).hintColor), ), - child: FlowyIconButton( - width: 24, - hoverColor: Colors.transparent, - onPressed: () => context - .read() - .add(const HomeSettingEvent.collapseMenu()), - iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2), - icon: FlowySvg( - FlowySvgs.hide_menu_m, - color: Theme.of(context).iconTheme.color, + ], + ); + return Padding( + padding: const EdgeInsets.only(right: 8.0), + child: RotationTransition( + turns: const AlwaysStoppedAnimation(180 / 360), + 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/tabs/flowy_tab.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/flowy_tab.dart index 81601cbfd491b..4760378f4b7c4 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/flowy_tab.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/flowy_tab.dart @@ -33,7 +33,7 @@ class _FlowyTabState extends State { onExit: (_) => _setHovering(), child: Container( width: HomeSizes.tabBarWidth, - height: HomeSizes.tabBarHeigth, + height: HomeSizes.tabBarHeight, decoration: BoxDecoration( color: _getBackgroundColor(), ), @@ -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 436e58f1bf7e5..064d64477f62b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/tabs_manager.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/tabs_manager.dart @@ -57,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 6e9d575dce12b..60c19a305146d 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 8ee7a0f102670..988ca40fca663 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 a7925dc3f7984..08f421951b2c6 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 @@ -3,6 +3,8 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; +import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy/workspace/presentation/notifications/notification_dialog.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -24,23 +26,35 @@ class NotificationButton extends StatelessWidget { return BlocProvider.value( value: getIt(), - child: BlocBuilder( - builder: (context, state) => FlowyTooltip( - message: LocaleKeys.notificationHub_title.tr(), - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: AppFlowyPopover( - mutex: mutex, - direction: PopoverDirection.bottomWithLeftAligned, - constraints: const BoxConstraints(maxHeight: 500, maxWidth: 425), - windowPadding: EdgeInsets.zero, - margin: EdgeInsets.zero, - popupBuilder: (_) => - NotificationDialog(views: views, mutex: mutex), - child: _buildNotificationIcon(context, state.hasUnreads), - ), - ), - ), + child: BlocBuilder( + builder: (notificationSettingsContext, notificationSettingsState) { + return BlocBuilder( + builder: (context, state) => notificationSettingsState + .isShowNotificationsIconEnabled + ? FlowyTooltip( + message: LocaleKeys.notificationHub_title.tr(), + child: AppFlowyPopover( + mutex: mutex, + direction: PopoverDirection.bottomWithLeftAligned, + constraints: + const BoxConstraints(maxHeight: 500, maxWidth: 425), + windowPadding: EdgeInsets.zero, + margin: EdgeInsets.zero, + popupBuilder: (_) => + NotificationDialog(views: views, mutex: mutex), + child: SizedBox.square( + dimension: HomeSizes.workspaceSectionHeight, + child: FlowyButton( + useIntrinsicWidth: true, + text: + _buildNotificationIcon(context, state.hasUnreads), + ), + ), + ), + ) + : const SizedBox.shrink(), + ); + }, ), ); } @@ -48,10 +62,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 9ee584e5dc1f2..68829d7b662fe 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart @@ -1,6 +1,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 17d76b4fe1c46..4ad5d00e1f599 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 c1d167844ada0..aa67ea315817d 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart @@ -1,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 { ), ); } +} + +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( - BuildContext context, { - required String current, + 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 ef4c374239893..31e5959002779 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 12bf1c1480dd6..ba6ae1416f8f4 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 67c0dc4cf98e6..42f407f97a4c8 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 066d63f380e50..4ef6e00994306 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 43f9438ff77fc..e4e15da75dcbc 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart @@ -1,9 +1,8 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; class FeatureFlagsPage extends StatelessWidget { const FeatureFlagsPage({ @@ -50,7 +49,8 @@ class _FeatureFlagItemState extends State<_FeatureFlagItem> { subtitle: FlowyText.small(widget.featureFlag.description, maxLines: 3), trailing: Switch.adaptive( value: widget.featureFlag.isOn, - onChanged: (value) => setState(() => widget.featureFlag.update(value)), + onChanged: (value) => + setState(() async => widget.featureFlag.update(value)), ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_exporter_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_exporter_widget.dart index 751599def3e06..decf74f874042 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_exporter_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_exporter_widget.dart @@ -1,7 +1,5 @@ import 'dart:io'; -import 'package:flutter/material.dart'; - import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/export/document_exporter.dart'; import 'package:appflowy/workspace/application/settings/settings_file_exporter_cubit.dart'; @@ -16,7 +14,8 @@ import 'package:appflowy_result/appflowy_result.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:path/path.dart' as p; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart index e104879752da6..6b9e55fc7d508 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 6805633f74e0f..a75cd87171a25 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 99d272d1229c2..10e69be87a663 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 c014cdf516ea4..29fab1805f40d 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 ed9b4dcd89eb4..8066d9b65e11d 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 0148db171b1b0..e44af72edd080 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/settings_notifications_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_notifications_view.dart index 1b3250ee55c6e..f7dcb635074cf 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_notifications_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_notifications_view.dart @@ -33,6 +33,23 @@ class SettingsNotificationsView extends StatelessWidget { ), ], ), + SettingListTile( + label: LocaleKeys + .settings_notifications_showNotificationsIcon_label + .tr(), + hint: LocaleKeys.settings_notifications_showNotificationsIcon_hint + .tr(), + trailing: [ + Switch( + value: state.isShowNotificationsIconEnabled, + splashRadius: 0, + activeColor: Theme.of(context).colorScheme.primary, + onChanged: (_) => context + .read() + .toogleShowNotificationIconEnabled(), + ), + ], + ), ], ); }, 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 260918c7de31c..f3cd25afde555 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 379c78acd5f0d..edb382d6eefe2 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 628232bd71f51..d57d2d2a00f8d 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 643189a38faaa..5e0ad15f385a9 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 967f5b0b0f65e..0113d26a37f5c 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 513f72b4ed89d..66cad4ab38de3 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 4c52e6c27851f..970ee82631148 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 { @@ -22,7 +20,7 @@ class ViewFavoriteButton extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - final isFavorite = state.views.any((v) => v.id == view.id); + final isFavorite = state.views.any((v) => v.item.id == view.id); return Listener( onPointerDown: (_) => context.read().add(FavoriteEvent.toggle(view)), @@ -35,9 +33,8 @@ class ViewFavoriteButton extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(6), child: FlowySvg( - isFavorite ? FlowySvgs.favorite_s : FlowySvgs.unfavorite_s, - size: const Size(18, 18), - color: AFThemeExtension.of(context).warning, + isFavorite ? FlowySvgs.unfavorite_s : FlowySvgs.favorite_s, + size: const Size.square(18), ), ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart index e72dfa098cc25..1c8c8d20ff080 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart @@ -1,6 +1,3 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; @@ -14,6 +11,8 @@ import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -147,7 +146,7 @@ class _DebugToast { class FlowyVersionDescription extends CustomActionCell { @override - Widget buildWithContext(BuildContext context) { + Widget buildWithContext(BuildContext context, PopoverController controller) { return FutureBuilder( future: PackageInfo.fromPlatform(), builder: (BuildContext context, AsyncSnapshot snapshot) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart index 3d0416f5897ba..e4bf942c6af04 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; @@ -14,6 +12,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MoreViewActions extends StatefulWidget { @@ -101,8 +100,8 @@ class _MoreViewActionsState extends State { builder: (context, isHovering) => Padding( padding: const EdgeInsets.all(6), child: FlowySvg( - FlowySvgs.three_dots_vertical_s, - size: const Size.square(16), + FlowySvgs.three_dots_s, + size: const Size.square(18), color: isHovering ? Theme.of(context).colorScheme.onSecondary : Theme.of(context).iconTheme.color, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart index bb285a79170f7..d4103cc6bf18f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart @@ -1,5 +1,5 @@ import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flutter/material.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -83,7 +83,7 @@ class _PopoverActionListState ); } else { final custom = action as CustomActionCell; - return custom.buildWithContext(context); + return custom.buildWithContext(context, popoverController); } }).toList(); @@ -121,7 +121,7 @@ abstract class PopoverActionCell extends PopoverAction { } abstract class CustomActionCell extends PopoverAction { - Widget buildWithContext(BuildContext context); + Widget buildWithContext(BuildContext context, PopoverController controller); } abstract class PopoverAction {} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart index b12ae6644a98e..ab67ecb5b405c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart @@ -1,10 +1,10 @@ -import 'package:flutter/widgets.dart'; - import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/style_widget/text_field.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; class RenameViewPopover extends StatefulWidget { const RenameViewPopover({ @@ -51,17 +51,20 @@ class _RenameViewPopoverState extends State { mainAxisSize: MainAxisSize.min, children: [ if (widget.showIconChanger) ...[ - EmojiPickerButton( - emoji: widget.emoji, - defaultIcon: widget.icon, - direction: PopoverDirection.bottomWithCenterAligned, - offset: const Offset(0, 18), - onSubmitted: _updateViewIcon, + SizedBox( + width: 30.0, + child: EmojiPickerButton( + emoji: widget.emoji, + defaultIcon: widget.icon, + direction: PopoverDirection.bottomWithCenterAligned, + offset: const Offset(0, 18), + onSubmitted: _updateViewIcon, + ), ), const HSpace(6), ], SizedBox( - height: 36.0, + height: 32.0, width: 220, child: FlowyTextField( controller: _controller, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart index 4b6708c151aa1..357e565559d10 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart @@ -1,78 +1,107 @@ -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'; +import 'package:string_validator/string_validator.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(); - const initialsCount = 2; + return _buildEmptyAvatar(context); + } else if (isURL(iconUrl)) { + return _buildUrlAvatar(context); + } else { + return _buildEmojiAvatar(context); + } + } - // Taking the first letters of the name components and limiting to 2 elements - final nameInitials = nameOrDefault - .split(' ') - .where((element) => element.isNotEmpty) - .take(initialsCount) - .map((element) => element[0].toUpperCase()) - .join(); + Widget _buildEmptyAvatar(BuildContext context) { + final String nameOrDefault = _userName(name); + final Color color = ColorGenerator(name).toColor(); + const initialsCount = 2; + + // Taking the first letters of the name components and limiting to 2 elements + final nameInitials = nameOrDefault + .split(' ') + .where((element) => element.isNotEmpty) + .take(initialsCount) + .map((element) => element[0].toUpperCase()) + .join(); + + return Container( + width: size, + height: size, + alignment: Alignment.center, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + border: isHovering + ? Border.all( + color: _darken(color), + width: 4, + ) + : null, + ), + child: FlowyText.regular( + nameInitials, + color: Colors.black, + fontSize: fontSize, + ), + ); + } - return Container( - width: size, - height: size, - alignment: Alignment.center, + Widget _buildUrlAvatar(BuildContext context) { + return SizedBox.square( + dimension: size, + child: DecoratedBox( decoration: BoxDecoration( - color: color, shape: BoxShape.circle, border: isHovering ? Border.all( - color: _darken(color), + color: Theme.of(context).colorScheme.primary, width: 4, ) : null, ), - child: FlowyText.semibold( - nameInitials, - color: Colors.black, - fontSize: isLarge - ? nameInitials.length == initialsCount - ? 20 - : 26 - : nameInitials.length == initialsCount - ? 12 - : 14, + child: ClipRRect( + borderRadius: Corners.s5Border, + child: CircleAvatar( + backgroundColor: Colors.transparent, + child: Image.network( + iconUrl, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => + _buildEmptyAvatar(context), + ), + ), ), - ); - } + ), + ); + } + Widget _buildEmojiAvatar(BuildContext context) { return SizedBox.square( dimension: size, child: DecoratedBox( @@ -94,7 +123,7 @@ class UserAvatar extends StatelessWidget { FlowySvgData('emoji/$iconUrl'), blendMode: null, ) - : EmojiText(emoji: iconUrl, fontSize: isLarge ? 36 : 18), + : FlowyText.emoji(iconUrl, fontSize: fontSize), ), ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart index 9d65cc06a112f..b3eddfb52b508 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,59 @@ class _ViewTitle extends StatefulWidget { class _ViewTitleState extends State<_ViewTitle> { final popoverController = PopoverController(); final textEditingController = TextEditingController(); - late final viewListener = ViewListener(viewId: widget.view.id); - - String name = ''; - String icon = ''; - String inputtingName = ''; - - @override - void initState() { - super.initState(); - - name = widget.view.name; - icon = widget.view.icon.value; - - _resetTextEditingController(); - viewListener.start( - onViewUpdated: (view) { - if (name != view.name || icon != view.icon.value) { - widget.onUpdated(); - } - setState(() { - name = view.name; - icon = view.icon.value; - _resetTextEditingController(); - }); - }, - ); - } @override void dispose() { textEditingController.dispose(); popoverController.close(); - viewListener.stop(); super.dispose(); } @override Widget build(BuildContext context) { - // root view - if (widget.view.parentViewId.isEmpty) { - return Row( - children: [ - FlowyText.regular(name), - const HSpace(4.0), - ], - ); - } - - final child = SingleChildScrollView( - child: Row( - children: [ - EmojiText( - emoji: icon, - fontSize: 18.0, - ), - const HSpace(2.0), - ConstrainedBox( - constraints: BoxConstraints( - maxWidth: max(0, widget.maxTitleWidth), - ), - child: FlowyText.regular( - name, - overflow: TextOverflow.ellipsis, - ), - ), - ], + final isEditable = widget.behavior == _ViewTitleBehavior.editable; + + return BlocProvider( + create: (_) => + ViewTitleBloc(view: widget.view)..add(const ViewTitleEvent.initial()), + child: BlocConsumer( + listener: (_, state) { + _resetTextEditingController(state); + widget.onUpdated(); + }, + builder: (context, state) { + // root view + if (widget.view.parentViewId.isEmpty) { + return Row( + children: [ + FlowyText.regular(state.name), + const HSpace(4.0), + ], + ); + } else if (isEditable) { + return _buildEditableViewTitle(context, state); + } else { + return _buildUnEditableViewTitle(context, state); + } + }, ), ); + } - if (widget.behavior == _ViewTitleBehavior.uneditable) { - return Listener( - onPointerDown: (_) => context.read().openPlugin(widget.view), - child: FlowyButton( - useIntrinsicWidth: true, - onTap: () {}, - text: child, - ), - ); - } + Widget _buildUnEditableViewTitle(BuildContext context, ViewTitleState state) { + return Listener( + onPointerDown: (_) => context.read().openPlugin(widget.view), + child: FlowyButton( + useIntrinsicWidth: true, + margin: const EdgeInsets.symmetric(horizontal: 6.0), + onTap: () {}, + text: _buildIconAndName(state), + ), + ); + } + Widget _buildEditableViewTitle(BuildContext context, ViewTitleState state) { return AppFlowyPopover( constraints: const BoxConstraints( maxWidth: 300, @@ -268,32 +182,55 @@ class _ViewTitleState extends State<_ViewTitle> { ), controller: popoverController, direction: PopoverDirection.bottomWithLeftAligned, - offset: const Offset(0, 18), + offset: const Offset(0, 6), popupBuilder: (context) { // icon + textfield - _resetTextEditingController(); + _resetTextEditingController(state); return RenameViewPopover( viewId: widget.view.id, name: widget.view.name, popoverController: popoverController, icon: widget.view.defaultIcon(), - emoji: icon, + emoji: state.icon, ); }, child: FlowyButton( useIntrinsicWidth: true, - text: child, + margin: const EdgeInsets.symmetric(horizontal: 6.0), + text: _buildIconAndName(state), + ), + ); + } + + Widget _buildIconAndName(ViewTitleState state) { + return SingleChildScrollView( + child: Row( + children: [ + if (state.icon.isNotEmpty) ...[ + FlowyText.emoji( + state.icon, + fontSize: 14.0, + ), + const HSpace(6.0), + ], + ConstrainedBox( + constraints: const BoxConstraints(), + child: FlowyText.regular( + state.name, + overflow: TextOverflow.ellipsis, + ), + ), + ], ), ); } - void _resetTextEditingController() { - inputtingName = name; + void _resetTextEditingController(ViewTitleState state) { textEditingController - ..text = name + ..text = state.name ..selection = TextSelection( baseOffset: 0, - extentOffset: name.length, + extentOffset: state.name.length, ); } } diff --git a/frontend/appflowy_flutter/macos/Runner/MainFlutterWindow.swift b/frontend/appflowy_flutter/macos/Runner/MainFlutterWindow.swift index 8e357d7ca1b7d..65498b121f3bf 100644 --- a/frontend/appflowy_flutter/macos/Runner/MainFlutterWindow.swift +++ b/frontend/appflowy_flutter/macos/Runner/MainFlutterWindow.swift @@ -1,7 +1,7 @@ import Cocoa import FlutterMacOS -private let kTrafficLightOffetTop = 22 +private let kTrafficLightOffetTop = 14 class MainFlutterWindow: NSWindow { func registerMethodChannel(flutterViewController: FlutterViewController) { @@ -17,7 +17,7 @@ class MainFlutterWindow: NSWindow { let nY = position[1] as! NSNumber let x = nX.doubleValue let y = nY.doubleValue - + self.setFrameOrigin(NSPoint(x: x, y: y)) result(nil) return @@ -30,7 +30,7 @@ class MainFlutterWindow: NSWindow { result(nil) return } - + result(FlutterMethodNotImplemented) }) } @@ -51,9 +51,9 @@ class MainFlutterWindow: NSWindow { let zoomButton = self.standardWindowButton(ButtonType.zoomButton)! let titlebarView = closeButton.superview! - self.layoutTrafficLightButton(titlebarView: titlebarView, button: closeButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 20) - self.layoutTrafficLightButton(titlebarView: titlebarView, button: minButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 38) - self.layoutTrafficLightButton(titlebarView: titlebarView, button: zoomButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 56) + self.layoutTrafficLightButton(titlebarView: titlebarView, button: closeButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 12) + self.layoutTrafficLightButton(titlebarView: titlebarView, button: minButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 30) + self.layoutTrafficLightButton(titlebarView: titlebarView, button: zoomButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 48) let customToolbar = NSTitlebarAccessoryViewController() let newView = NSView() diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart index d9420944fb12b..405db2bee8de1 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart @@ -1,8 +1,7 @@ +import 'package:appflowy_popover/src/layout.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:appflowy_popover/src/layout.dart'; - import 'mask.dart'; import 'mutex.dart'; @@ -291,6 +290,13 @@ class PopoverContainer extends StatefulWidget { context.findAncestorStateOfType(); return result!; } + + static PopoverContainerState? maybeOf(BuildContext context) { + if (context is StatefulElement && context.state is PopoverContainerState) { + return context.state as PopoverContainerState; + } + return context.findAncestorStateOfType(); + } } class PopoverContainerState extends State { diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/default_colorscheme.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/default_colorscheme.dart index 9f0cc50c93853..8f7120fa846f5 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,13 @@ 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 _darkMain2 = Color(0xFF00BCF0); const _darkInput = Color(0xFF282E3A); class DefaultColorScheme extends FlowyColorScheme { @@ -103,7 +104,7 @@ class DefaultColorScheme extends FlowyColorScheme { tint7: const Color(0x5900BD2A), tint8: const Color(0x80008890), tint9: const Color(0x4d0029FF), - main1: _darkMain1, + main1: _darkMain2, main2: const Color(0xFF00B7EA), shadow: const Color(0xFF0F131C), sidebarBg: const Color(0xFF232B38), @@ -114,7 +115,7 @@ class DefaultColorScheme extends FlowyColorScheme { secondaryText: _darkShader5, input: _darkInput, hint: const Color(0xFF59647a), - primary: _darkMain1, + primary: _darkMain2, onPrimary: _darkShader1, hoverBG1: _darkMain1, hoverBG2: _darkMain1, 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 2eeb901ef4762..70128c44bd6a3 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 3cd07738f89f8..8c7793d7cc878 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 59a2bc653362f..7bbcbf09499c2 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/basis.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/basis.dart @@ -1,8 +1,3 @@ -import 'package:flutter/material.dart'; - // MARK: - Shared Builder - -typedef WidgetBuilder = Widget Function(); - typedef IndexedCallback = void Function(int index); typedef IndexedValueCallback = void Function(T value, int index); diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/flowy_infra_ui.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/flowy_infra_ui.dart index cb147e2782a6f..bbdeda8de3d4d 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'; -export 'style_widget/text.dart'; -export 'style_widget/text_field.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_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'; +export 'style_widget/scrolling/styled_scroll_bar.dart'; +export 'style_widget/text.dart'; +export 'style_widget/text_field.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 832eca88e09c0..acd628a22204f 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart @@ -1,12 +1,11 @@ import 'dart:io'; -import 'package:flutter/material.dart'; - import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; class FlowyButton extends StatelessWidget { final Widget text; @@ -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 1a4b96ecb53b9..0902d78835805 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/decoration.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/decoration.dart @@ -7,10 +7,11 @@ class FlowyDecoration { double spreadRadius = 0, double blurRadius = 20, Offset offset = Offset.zero, + double borderRadius = 6, }) { return BoxDecoration( color: boxColor, - borderRadius: const BorderRadius.all(Radius.circular(6)), + borderRadius: BorderRadius.all(Radius.circular(borderRadius)), boxShadow: [ BoxShadow( color: boxShadow, diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/snap_bar.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/snap_bar.dart index 778aee0a74ff2..8752a5985fd18 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 65d0c19c59e5c..10065b3dc7a4a 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 @@ -1,6 +1,9 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +const String _emojiFontFamily = 'noto color emoji'; class FlowyText extends StatelessWidget { final String text; @@ -16,6 +19,8 @@ class FlowyText extends StatelessWidget { final List? fallbackFontFamily; final double? lineHeight; final bool withTooltip; + final StrutStyle? strutStyle; + final bool isEmoji; const FlowyText( this.text, { @@ -32,6 +37,8 @@ class FlowyText extends StatelessWidget { this.fallbackFontFamily, this.lineHeight, this.withTooltip = false, + this.isEmoji = false, + this.strutStyle, }); FlowyText.small( @@ -47,6 +54,8 @@ class FlowyText extends StatelessWidget { this.fallbackFontFamily, this.lineHeight, this.withTooltip = false, + this.isEmoji = false, + this.strutStyle, }) : fontWeight = FontWeight.w400, fontSize = (Platform.isIOS || Platform.isAndroid) ? 14 : 12; @@ -64,6 +73,8 @@ class FlowyText extends StatelessWidget { this.fallbackFontFamily, this.lineHeight, this.withTooltip = false, + this.isEmoji = false, + this.strutStyle, }) : fontWeight = FontWeight.w400; const FlowyText.medium( @@ -80,6 +91,8 @@ class FlowyText extends StatelessWidget { this.fallbackFontFamily, this.lineHeight, this.withTooltip = false, + this.isEmoji = false, + this.strutStyle, }) : fontWeight = FontWeight.w500; const FlowyText.semibold( @@ -96,6 +109,8 @@ class FlowyText extends StatelessWidget { this.fallbackFontFamily, this.lineHeight, this.withTooltip = false, + this.isEmoji = false, + this.strutStyle, }) : fontWeight = FontWeight.w600; // Some emojis are not supported on Linux and Android, fallback to noto color emoji @@ -105,34 +120,54 @@ 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), + this.isEmoji = true, + this.fontFamily, }) : fontWeight = FontWeight.w400, - fontFamily = 'noto color emoji', fallbackFontFamily = null; @override Widget build(BuildContext context) { Widget child; + var fontFamily = this.fontFamily; + var fallbackFontFamily = this.fallbackFontFamily; + if (isEmoji && (Platform.isLinux || Platform.isAndroid)) { + fontFamily = _loadEmojiFontFamilyIfNeeded(); + if (fontFamily != null && fallbackFontFamily == null) { + fallbackFontFamily = [fontFamily]; + } + } + + var fontSize = + this.fontSize ?? Theme.of(context).textTheme.bodyMedium!.fontSize!; + if (Platform.isLinux && fontFamily == _emojiFontFamily) { + fontSize = fontSize * 0.8; + } + + 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 +175,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, ); } @@ -161,4 +188,12 @@ class FlowyText extends StatelessWidget { return child; } + + String? _loadEmojiFontFamilyIfNeeded() { + if (Platform.isLinux || Platform.isAndroid) { + return GoogleFonts.notoColorEmoji().fontFamily; + } + + return null; + } } 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 ac44197abeb3b..c79f430942b4e 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/flowy_tooltip.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart index 47a684cf01ea0..57874fad642ee 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart @@ -22,8 +22,13 @@ class FlowyTooltip extends StatelessWidget { @override Widget build(BuildContext context) { + final isLightMode = Theme.of(context).brightness == Brightness.light; return Tooltip( margin: margin, + decoration: BoxDecoration( + color: isLightMode ? const Color(0xE5171717) : const Color(0xE5E5E5E5), + borderRadius: BorderRadius.circular(8.0), + ), waitDuration: _tooltipWaitDuration, message: message, richMessage: richMessage, 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 11b71b7d283cb..a73d96f454a69 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/packages/flowy_infra_ui/pubspec.yaml b/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml index 3d73a51d7b438..5eb1ba066e775 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: loading_indicator: ^3.1.0 async: url_launcher: ^6.1.11 + google_fonts: ^6.1.0 # Federated Platform Interface flowy_infra_ui_platform_interface: diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index b46a113767248..e4ddf13bfca75 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -53,19 +53,19 @@ 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: name: appflowy_editor_plugins - sha256: "9d91f65e564f85ffc98a407524371beeb1fd40aabd621b00ba8a722058636094" + sha256: "46c899acc22245798e5beed255852455d9d85c3fae8b275ef4db257810b1c59d" url: "https://pub.dev" source: hosted - version: "0.0.2" + version: "0.0.6" appflowy_popover: dependency: "direct main" description: @@ -104,6 +104,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + auto_size_text_field: + dependency: "direct main" + description: + name: auto_size_text_field + sha256: c4ba8714ba4216ca122acac1573581dac499f3162c9218a28b573dca73721b3f + url: "https://pub.dev" + source: hosted + version: "2.2.3" avatar_stack: dependency: "direct main" description: @@ -868,6 +876,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image: + dependency: transitive + description: + name: image + sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" + url: "https://pub.dev" + source: hosted + version: "4.1.7" image_gallery_saver: dependency: "direct main" description: @@ -949,10 +965,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 +1053,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: @@ -1145,14 +1161,86 @@ packages: url: "https://pub.dev" source: hosted version: "0.8.0" + media_kit: + dependency: transitive + description: + name: media_kit + sha256: "3289062540e3b8b9746e5c50d95bd78a9289826b7227e253dff806d002b9e67a" + url: "https://pub.dev" + source: hosted + version: "1.1.10+1" + media_kit_libs_android_video: + dependency: transitive + description: + name: media_kit_libs_android_video + sha256: "9dd8012572e4aff47516e55f2597998f0a378e3d588d0fad0ca1f11a53ae090c" + url: "https://pub.dev" + source: hosted + version: "1.3.6" + media_kit_libs_ios_video: + dependency: transitive + description: + name: media_kit_libs_ios_video + sha256: b5382994eb37a4564c368386c154ad70ba0cc78dacdd3fb0cd9f30db6d837991 + url: "https://pub.dev" + source: hosted + version: "1.1.4" + media_kit_libs_linux: + dependency: transitive + description: + name: media_kit_libs_linux + sha256: e186891c31daa6bedab4d74dcdb4e8adfccc7d786bfed6ad81fe24a3b3010310 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + media_kit_libs_macos_video: + dependency: transitive + description: + name: media_kit_libs_macos_video + sha256: f26aa1452b665df288e360393758f84b911f70ffb3878032e1aabba23aa1032d + url: "https://pub.dev" + source: hosted + version: "1.1.4" + media_kit_libs_video: + dependency: transitive + description: + name: media_kit_libs_video + sha256: "3688e0c31482074578652bf038ce6301a5d21e1eda6b54fc3117ffeb4bdba067" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + media_kit_libs_windows_video: + dependency: transitive + description: + name: media_kit_libs_windows_video + sha256: "7bace5f35d9afcc7f9b5cdadb7541d2191a66bb3fc71bfa11c1395b3360f6122" + url: "https://pub.dev" + source: hosted + version: "1.0.9" + media_kit_native_event_loop: + dependency: transitive + description: + name: media_kit_native_event_loop + sha256: a605cf185499d14d58935b8784955a92a4bf0ff4e19a23de3d17a9106303930e + url: "https://pub.dev" + source: hosted + version: "1.0.8" + media_kit_video: + dependency: transitive + description: + name: media_kit_video + sha256: c048d11a19e379aebbe810647636e3fc6d18374637e2ae12def4ff8a4b99a882 + url: "https://pub.dev" + source: hosted + version: "1.2.4" meta: 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: @@ -1489,6 +1577,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + reorderable_tabbar: + dependency: "direct main" + description: + name: reorderable_tabbar + sha256: dd19d7b6f60f0dec4be02ba0a2c860f9acbe5a392cb8b5b8c1417cbfcbfe923f + url: "https://pub.dev" + source: hosted + version: "1.0.6" reorderables: dependency: "direct main" description: @@ -1521,14 +1617,70 @@ packages: url: "https://pub.dev" source: hosted version: "0.27.7" + safe_local_storage: + dependency: transitive + description: + name: safe_local_storage + sha256: ede4eb6cb7d88a116b3d3bf1df70790b9e2038bc37cb19112e381217c74d9440 + url: "https://pub.dev" + source: hosted + version: "1.0.2" scaled_app: 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_brightness: + dependency: transitive + description: + name: screen_brightness + sha256: ed8da4a4511e79422fc1aa88138e920e4008cd312b72cdaa15ccb426c0faaedd + url: "https://pub.dev" + source: hosted + version: "0.2.2+1" + screen_brightness_android: + dependency: transitive + description: + name: screen_brightness_android + sha256: "3df10961e3a9e968a5e076fe27e7f4741fa8a1d3950bdeb48cf121ed529d0caf" + url: "https://pub.dev" + source: hosted + version: "0.1.0+2" + screen_brightness_ios: + dependency: transitive + description: + name: screen_brightness_ios + sha256: "99adc3ca5490b8294284aad5fcc87f061ad685050e03cf45d3d018fe398fd9a2" + url: "https://pub.dev" + source: hosted + version: "0.1.0" + screen_brightness_macos: + dependency: transitive + description: + name: screen_brightness_macos + sha256: "64b34e7e3f4900d7687c8e8fb514246845a73ecec05ab53483ed025bd4a899fd" + url: "https://pub.dev" + source: hosted + version: "0.1.0+1" + screen_brightness_platform_interface: + dependency: transitive + description: + name: screen_brightness_platform_interface + sha256: b211d07f0c96637a15fb06f6168617e18030d5d74ad03795dd8547a52717c171 + url: "https://pub.dev" + source: hosted + version: "0.1.0" + screen_brightness_windows: + dependency: transitive + description: + name: screen_brightness_windows + sha256: "9261bf33d0fc2707d8cf16339ce25768100a65e70af0fcabaf032fc12408ba86" + url: "https://pub.dev" + source: hosted + version: "0.1.3" screen_retriever: dependency: transitive description: @@ -1856,14 +2008,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0+1" + tab_indicator_styler: + dependency: transitive + description: + name: tab_indicator_styler + sha256: "9e7e90367e20f71f3882fc6578fdcced35ab1c66ab20fcb623cdcc20d2796c76" + url: "https://pub.dev" + source: hosted + version: "2.0.0" table_calendar: 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 +2036,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: @@ -1968,6 +2128,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + uri_parser: + dependency: transitive + description: + name: uri_parser + sha256: "6543c9fd86d2862fac55d800a43e67c0dcd1a41677cb69c2f8edfe73bbcf1835" + url: "https://pub.dev" + source: hosted + version: "2.0.2" url_launcher: dependency: "direct main" description: @@ -2101,10 +2269,34 @@ 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" + volume_controller: + dependency: transitive + description: + name: volume_controller + sha256: "189bdc7a554f476b412e4c8b2f474562b09d74bc458c23667356bce3ca1d48c9" + url: "https://pub.dev" + source: hosted + version: "2.0.7" + wakelock_plus: + dependency: transitive + description: + name: wakelock_plus + sha256: "104d94837bb28c735894dcd592877e990149c380e6358b00c04398ca1426eed4" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + wakelock_plus_platform_interface: + dependency: transitive + description: + name: wakelock_plus_platform_interface + sha256: "422d1cdbb448079a8a62a5a770b69baa489f8f7ca21aef47800c726d404f9d16" + url: "https://pub.dev" + source: hosted + version: "1.2.1" watcher: dependency: transitive description: @@ -2203,4 +2395,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 8ad4bd55be041..22382d9e48524 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.8 +version: 0.5.9 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. @@ -47,7 +47,7 @@ dependencies: ref: 8a6434ae3d02624b614a010af80f775db11bf22e appflowy_result: path: packages/appflowy_result - appflowy_editor_plugins: ^0.0.2 + appflowy_editor_plugins: ^0.0.6 appflowy_editor: @@ -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,9 @@ 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 + auto_size_text_field: ^2.2.3 + reorderable_tabbar: ^1.0.6 dev_dependencies: flutter_lints: ^3.0.1 @@ -172,7 +174,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 0000000000000..34472193f97da --- /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.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index 9206c93d27a23..6b3a6710773c5 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 diff --git a/frontend/appflowy_tauri/src-tauri/src/init.rs b/frontend/appflowy_tauri/src-tauri/src/init.rs index 1702c0292326f..7591ba37ffb13 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_web_app/index.html b/frontend/appflowy_web_app/index.html index 5480f3785904e..3d8bc89cd60b0 100644 --- a/frontend/appflowy_web_app/index.html +++ b/frontend/appflowy_web_app/index.html @@ -3,15 +3,44 @@ - + + AppFlowy
+ diff --git a/frontend/appflowy_web_app/package.json b/frontend/appflowy_web_app/package.json index 2dafe5e66d93e..8c8fd676272ee 100644 --- a/frontend/appflowy_web_app/package.json +++ b/frontend/appflowy_web_app/package.json @@ -12,7 +12,7 @@ "lint": "pnpm run sync:i18n && tsc --noEmit --project tsconfig.web.json && eslint --ext .js,.ts,.tsx . --ignore-path .eslintignore.web", "start": "vite preview --port 3000", "tauri:dev": "tauri dev", - "css:variables": "node style-dictionary/config.cjs", + "css:variables": "node scripts/generateTailwindColors.cjs", "sync:i18n": "node scripts/i18n.cjs", "link:client-api": "rm -rf node_modules/.vite && node scripts/create-symlink.cjs", "analyze": "cross-env ANALYZE_MODE=true vite build", @@ -38,6 +38,7 @@ "@types/react-swipeable-views": "^0.13.4", "async-retry": "^1.3.3", "axios": "^1.6.8", + "colorthief": "^2.4.0", "dayjs": "^1.11.9", "decimal.js": "^10.4.3", "emoji-mart": "^5.5.2", @@ -63,6 +64,7 @@ "react-big-calendar": "^1.8.5", "react-color": "^2.19.3", "react-custom-scrollbars": "^4.2.1", + "react-custom-scrollbars-2": "^4.5.0", "react-datepicker": "^4.23.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.13", @@ -88,7 +90,6 @@ "unsplash-js": "^7.0.19", "utf8": "^3.0.0", "validator": "^13.11.0", - "valtio": "^1.12.1", "vite-plugin-wasm": "^3.3.0", "y-indexeddb": "9.0.12", "yjs": "^13.6.14" @@ -107,6 +108,7 @@ "@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", diff --git a/frontend/appflowy_web_app/pnpm-lock.yaml b/frontend/appflowy_web_app/pnpm-lock.yaml index 770298d3b921f..03e1e7e411b51 100644 --- a/frontend/appflowy_web_app/pnpm-lock.yaml +++ b/frontend/appflowy_web_app/pnpm-lock.yaml @@ -10,49 +10,52 @@ dependencies: version: 0.0.3 '@atlaskit/primitives': specifier: ^5.5.3 - version: 5.5.3(@types/react@18.2.66)(react@18.2.0) + version: 5.7.0(@types/react@18.2.66)(react@18.2.0) '@emoji-mart/data': specifier: ^1.1.2 - version: 1.1.2 + version: 1.2.1 '@emoji-mart/react': specifier: ^1.1.1 - version: 1.1.1(emoji-mart@5.5.2)(react@18.2.0) + version: 1.1.1(emoji-mart@5.6.0)(react@18.2.0) '@emotion/react': specifier: ^11.10.6 - version: 11.10.6(@types/react@18.2.66)(react@18.2.0) + version: 11.11.4(@types/react@18.2.66)(react@18.2.0) '@emotion/styled': specifier: ^11.10.6 - version: 11.10.6(@emotion/react@11.10.6)(@types/react@18.2.66)(react@18.2.0) + version: 11.11.5(@emotion/react@11.11.4)(@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) + version: 5.15.18(@mui/material@6.0.0-alpha.2)(@types/react@18.2.66)(react@18.2.0) '@mui/material': specifier: 6.0.0-alpha.2 - 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) + version: 6.0.0-alpha.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@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.20.0(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@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) + version: 2.0.0(react-redux@8.1.3)(react@18.2.0) '@slate-yjs/core': specifier: ^1.0.2 - version: 1.0.2(slate@0.101.4)(yjs@13.6.14) + version: 1.0.2(slate@0.101.5)(yjs@13.6.15) '@tauri-apps/api': specifier: ^1.5.3 - version: 1.5.3 + version: 1.5.6 '@types/react-swipeable-views': specifier: ^0.13.4 - version: 0.13.4 + version: 0.13.5 async-retry: specifier: ^1.3.3 version: 1.3.3 axios: specifier: ^1.6.8 - version: 1.6.8 + version: 1.7.2 + colorthief: + specifier: ^2.4.0 + version: 2.4.0 dayjs: specifier: ^1.11.9 version: 1.11.9 @@ -61,25 +64,25 @@ dependencies: version: 10.4.3 emoji-mart: specifier: ^5.5.2 - version: 5.5.2 + version: 5.6.0 emoji-regex: specifier: ^10.2.1 - version: 10.2.1 + version: 10.3.0 events: specifier: ^3.3.0 version: 3.3.0 google-protobuf: specifier: ^3.15.12 - version: 3.16.0 + version: 3.21.2 i18next: specifier: ^22.4.10 - version: 22.4.10 + version: 22.5.1 i18next-browser-languagedetector: specifier: ^7.0.1 - version: 7.0.1 + version: 7.2.1 i18next-resources-to-backend: specifier: ^1.1.4 - version: 1.1.4 + version: 1.2.1 is-hotkey: specifier: ^0.2.0 version: 0.2.0 @@ -88,16 +91,16 @@ dependencies: version: 29.5.0(@types/node@20.11.30) js-base64: specifier: ^3.7.5 - version: 3.7.5 + version: 3.7.7 katex: specifier: ^0.16.7 - version: 0.16.7 + version: 0.16.10 lodash-es: specifier: ^4.17.21 version: 4.17.21 nanoid: specifier: ^4.0.0 - version: 4.0.0 + version: 4.0.2 numeral: specifier: ^2.0.6 version: 2.0.6 @@ -121,16 +124,19 @@ dependencies: version: 13.1.1(react-dom@18.2.0)(react@18.2.0) react-big-calendar: specifier: ^1.8.5 - version: 1.8.5(react-dom@18.2.0)(react@18.2.0) + version: 1.12.2(react-dom@18.2.0)(react@18.2.0) react-color: specifier: ^2.19.3 version: 2.19.3(react@18.2.0) react-custom-scrollbars: specifier: ^4.2.1 version: 4.2.1(react-dom@18.2.0)(react@18.2.0) + react-custom-scrollbars-2: + specifier: ^4.5.0 + version: 4.5.0(react-dom@18.2.0)(react@18.2.0) react-datepicker: specifier: ^4.23.0 - version: 4.23.0(react-dom@18.2.0)(react@18.2.0) + version: 4.25.0(react-dom@18.2.0)(react@18.2.0) react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) @@ -142,7 +148,7 @@ dependencies: version: 2.4.1(csstype@3.1.3)(react-dom@18.2.0)(react@18.2.0) react-i18next: specifier: ^14.1.0 - version: 14.1.0(i18next@22.4.10)(react-dom@18.2.0)(react@18.2.0) + version: 14.1.2(i18next@22.5.1)(react-dom@18.2.0)(react@18.2.0) react-katex: specifier: ^3.0.1 version: 3.0.1(prop-types@15.8.1)(react@18.2.0) @@ -151,10 +157,10 @@ dependencies: 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) + version: 8.1.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1) react-router-dom: specifier: ^6.22.3 - version: 6.22.3(react-dom@18.2.0)(react@18.2.0) + version: 6.23.1(react-dom@18.2.0)(react@18.2.0) react-swipeable-views: specifier: ^0.14.0 version: 0.14.0(react@18.2.0) @@ -163,7 +169,7 @@ dependencies: version: 4.4.5(react-dom@18.2.0)(react@18.2.0) react-virtualized-auto-sizer: specifier: ^1.0.20 - version: 1.0.20(react-dom@18.2.0)(react@18.2.0) + version: 1.0.24(react-dom@18.2.0)(react@18.2.0) react-vtree: specifier: ^2.0.4 version: 2.0.4(@types/react-window@1.8.8)(react-dom@18.2.0)(react-window@1.8.10)(react@18.2.0) @@ -172,7 +178,7 @@ dependencies: version: 1.8.10(react-dom@18.2.0)(react@18.2.0) react18-input-otp: specifier: ^1.1.2 - version: 1.1.2(react-dom@18.2.0)(react@18.2.0) + version: 1.1.4(react-dom@18.2.0)(react@18.2.0) redux: specifier: ^4.2.1 version: 4.2.1 @@ -181,16 +187,16 @@ dependencies: version: 7.8.0 sass: specifier: ^1.70.0 - version: 1.70.0 + version: 1.77.2 slate: specifier: ^0.101.4 - version: 0.101.4 + version: 0.101.5 slate-history: specifier: ^0.100.0 - version: 0.100.0(slate@0.101.4) + version: 0.100.0(slate@0.101.5) slate-react: specifier: ^0.101.3 - version: 0.101.3(react-dom@18.2.0)(react@18.2.0)(slate@0.101.4) + version: 0.101.6(react-dom@18.2.0)(react@18.2.0)(slate@0.101.5) ts-results: specifier: ^3.3.0 version: 3.3.0 @@ -202,19 +208,16 @@ dependencies: version: 3.0.0 validator: specifier: ^13.11.0 - version: 13.11.0 - valtio: - specifier: ^1.12.1 - version: 1.12.1(@types/react@18.2.66)(react@18.2.0) + version: 13.12.0 vite-plugin-wasm: specifier: ^3.3.0 version: 3.3.0(vite@5.2.0) y-indexeddb: specifier: 9.0.12 - version: 9.0.12(yjs@13.6.14) + version: 9.0.12(yjs@13.6.15) yjs: specifier: ^13.6.14 - version: 13.6.14 + version: 13.6.15 devDependencies: '@svgr/plugin-svgo': @@ -256,6 +259,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 @@ -363,7 +369,7 @@ devDependencies: version: 9.0.0 vite: specifier: ^5.2.0 - version: 5.2.0(@types/node@20.11.30)(sass@1.70.0) + version: 5.2.0(@types/node@20.11.30)(sass@1.77.2) vite-plugin-compression2: specifier: ^1.0.0 version: 1.0.0 @@ -407,27 +413,27 @@ packages: tslib: 2.6.2 dev: false - /@atlaskit/analytics-next@9.2.3(react@18.2.0): - resolution: {integrity: sha512-zTtM6xHTNNMO0dxiPrsxMko6BawEh4wN0wWSvMdQXdnusvvWNEE8rnpjn23kS4M08bYWIEstRbgoPLx6VQisug==} + /@atlaskit/analytics-next@9.3.0(react@18.2.0): + resolution: {integrity: sha512-mR5CndP92k2gFl8sWu4DJZZEpEQ4bnp5Z3fWCZE1oySiOKK8iM+KzKH4FMCaSUGOhWW6/5VeuXCcXvaogaAmsA==} peerDependencies: react: ^16.8.0 dependencies: '@atlaskit/analytics-next-stable-react-context': 1.0.1(react@18.2.0) '@atlaskit/platform-feature-flags': 0.2.5 - '@babel/runtime': 7.24.4 + '@babel/runtime': 7.24.1 prop-types: 15.8.1 react: 18.2.0 use-memo-one: 1.1.3(react@18.2.0) dev: false - /@atlaskit/app-provider@1.3.0(react@18.2.0): - resolution: {integrity: sha512-0bHfIbXyCbYjuv0zMyHPyuCcyW5IIVCBsNbYEbEl9w5P9hChPyewNrz1VkNPRQBg18DF1zxIIsNyZOEuQjvs7Q==} + /@atlaskit/app-provider@1.3.2(react@18.2.0): + resolution: {integrity: sha512-tvyMNrydTyu5yJK78zUjqbJwgNRdW5nQ31imWFav5PvWXwc36lfGiTXRX/JIxJNBC3rBJ0gLAyrrb9YMzyWcTw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ~18.2.0 dependencies: - '@atlaskit/tokens': 1.43.0(react@18.2.0) - '@babel/runtime': 7.24.4 - bind-event-listener: 2.1.1 + '@atlaskit/tokens': 1.49.1(react@18.2.0) + '@babel/runtime': 7.24.1 + bind-event-listener: 3.0.0 react: 18.2.0 transitivePeerDependencies: - supports-color @@ -438,21 +444,21 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ~18.2.0 dependencies: - '@atlaskit/tokens': 1.43.0(react@18.2.0) - '@babel/runtime': 7.24.4 + '@atlaskit/tokens': 1.49.1(react@18.2.0) + '@babel/runtime': 7.24.1 '@compiled/react': 0.17.1(react@18.2.0) react: 18.2.0 transitivePeerDependencies: - supports-color dev: false - /@atlaskit/ds-lib@2.3.0(react@18.2.0): - resolution: {integrity: sha512-ULU9ZTVBvlQ9QUwKXhCju3/fUWT79zdX5omNYOIL3nUrAtBCPX7inNpIJ/D4Lx35O6XSWJRKuNFL5tR75FEYKQ==} + /@atlaskit/ds-lib@2.3.1(react@18.2.0): + resolution: {integrity: sha512-DVUE3hYLhdEZy4NnsxqiCqKC5Ym3CM/DGRQlnSPcABFNL0N0FfTXso3pLpkJnMZBtEnd2pn13mPJ2VQlSISRuw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ~18.2.0 dependencies: - '@babel/runtime': 7.24.4 - bind-event-listener: 2.1.1 + '@babel/runtime': 7.24.1 + bind-event-listener: 3.0.0 react: 18.2.0 dev: false @@ -461,32 +467,32 @@ packages: peerDependencies: react: ^16.8.0 dependencies: - '@babel/runtime': 7.24.4 + '@babel/runtime': 7.24.1 react: 18.2.0 dev: false /@atlaskit/platform-feature-flags@0.2.5: resolution: {integrity: sha512-0fD2aDxn2mE59D4acUhVib+YF2HDYuuPH50aYwpQdcV/CsVkAaJsMKy8WhWSulcRFeMYp72kfIfdy0qGdRB7Uw==} dependencies: - '@babel/runtime': 7.24.4 + '@babel/runtime': 7.24.1 dev: false - /@atlaskit/primitives@5.5.3(@types/react@18.2.66)(react@18.2.0): - resolution: {integrity: sha512-UnKx0N/Jeo5Z61wFm5U2OkJM3bcb1qpKUHYrxzmSVe+QfQvBbJ/U5882WMnwQYq9rqYR2NcFvRNyT8Lf/L4FRQ==} + /@atlaskit/primitives@5.7.0(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-eCLyHN1BllNpwqA2YqCmYpqwoiNVcW3R6bHrpKmsW8uvPE/+Bd45hOiPwvCPJUPyK1ZNMfnkegWKkoxcmjMYIQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@atlaskit/analytics-next': 9.2.3(react@18.2.0) - '@atlaskit/app-provider': 1.3.0(react@18.2.0) + '@atlaskit/analytics-next': 9.3.0(react@18.2.0) + '@atlaskit/app-provider': 1.3.2(react@18.2.0) '@atlaskit/css': 0.1.0(react@18.2.0) - '@atlaskit/ds-lib': 2.3.0(react@18.2.0) + '@atlaskit/ds-lib': 2.3.1(react@18.2.0) '@atlaskit/interaction-context': 2.1.4(react@18.2.0) - '@atlaskit/tokens': 1.43.0(react@18.2.0) - '@atlaskit/visually-hidden': 1.2.5(@types/react@18.2.66)(react@18.2.0) - '@babel/runtime': 7.24.4 - '@emotion/react': 11.10.6(@types/react@18.2.66)(react@18.2.0) + '@atlaskit/tokens': 1.49.1(react@18.2.0) + '@atlaskit/visually-hidden': 1.3.0(@types/react@18.2.66)(react@18.2.0) + '@babel/runtime': 7.24.1 + '@emotion/react': 11.11.4(@types/react@18.2.66)(react@18.2.0) '@emotion/serialize': 1.1.4 - bind-event-listener: 2.1.1 + bind-event-listener: 3.0.0 react: 18.2.0 tiny-invariant: 1.3.3 transitivePeerDependencies: @@ -494,29 +500,29 @@ packages: - supports-color dev: false - /@atlaskit/tokens@1.43.0(react@18.2.0): - resolution: {integrity: sha512-3rRxGRnJGQBVKGqNqy+Zuad3xuDZ7uD+aFGRcU2OpLuIpiFLX95agDZ9w0HGzNiDw9eWi2f1j8Uzq06AyaRqTw==} + /@atlaskit/tokens@1.49.1(react@18.2.0): + resolution: {integrity: sha512-3SuhRMPUTU6b+nv0zVoGsNoqrUMtwQ/4iBbKhwaylRITanFxlxBwzW8XCCnn4sp1S2JupiT5BksI0h6jRoKN9Q==} peerDependencies: - react: ^16.8.0 + react: ^16.8.0 || ^17.0.0 || ~18.2.0 dependencies: - '@atlaskit/ds-lib': 2.3.0(react@18.2.0) + '@atlaskit/ds-lib': 2.3.1(react@18.2.0) '@atlaskit/platform-feature-flags': 0.2.5 - '@babel/runtime': 7.24.4 + '@babel/runtime': 7.24.1 '@babel/traverse': 7.24.1 '@babel/types': 7.24.0 - bind-event-listener: 2.1.1 + bind-event-listener: 3.0.0 react: 18.2.0 transitivePeerDependencies: - supports-color dev: false - /@atlaskit/visually-hidden@1.2.5(@types/react@18.2.66)(react@18.2.0): - resolution: {integrity: sha512-khxJB6456uMS1IkwnsH0ZTYGVAnE83aoGarz388I0zPydOZdNeeEdwsRPFOn9mua88Ez+rL0OEWV22k6XRUf6A==} + /@atlaskit/visually-hidden@1.3.0(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-iOHCxRnhNV3gnqOHuyLOnsFibfHpr1T28XUPYZjtN9bDQbn1GdSDYLoIHnLK+2enqdILirsuUWi93mWM3dCCwg==} peerDependencies: - react: ^16.8.0 + react: ^16.8.0 || ^17.0.0 || ~18.2.0 dependencies: - '@babel/runtime': 7.24.4 - '@emotion/react': 11.10.6(@types/react@18.2.66)(react@18.2.0) + '@babel/runtime': 7.24.1 + '@emotion/react': 11.11.4(@types/react@18.2.66)(react@18.2.0) react: 18.2.0 transitivePeerDependencies: - '@types/react' @@ -811,8 +817,8 @@ packages: dependencies: regenerator-runtime: 0.14.1 - /@babel/runtime@7.24.4: - resolution: {integrity: sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==} + /@babel/runtime@7.24.6: + resolution: {integrity: sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw==} engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.14.1 @@ -910,17 +916,17 @@ packages: - supports-color dev: true - /@emoji-mart/data@1.1.2: - resolution: {integrity: sha512-1HP8BxD2azjqWJvxIaWAMyTySeZY0Osr83ukYjltPVkNXeJvTz7yDrPLBtnrD5uqJ3tg4CcLuuBW09wahqL/fg==} + /@emoji-mart/data@1.2.1: + resolution: {integrity: sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==} dev: false - /@emoji-mart/react@1.1.1(emoji-mart@5.5.2)(react@18.2.0): + /@emoji-mart/react@1.1.1(emoji-mart@5.6.0)(react@18.2.0): resolution: {integrity: sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g==} peerDependencies: emoji-mart: ^5.2 react: ^16.8 || ^17 || ^18 dependencies: - emoji-mart: 5.5.2 + emoji-mart: 5.6.0 react: 18.2.0 dev: false @@ -964,8 +970,8 @@ packages: resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==} dev: false - /@emotion/react@11.10.6(@types/react@18.2.66)(react@18.2.0): - resolution: {integrity: sha512-6HT8jBmcSkfzO7mc+N1L9uwvOnlcGoix8Zn7srt+9ga0MjREo6lRpuVX0kzo6Jp6oTqDhREOFsygN6Ew4fEQbw==} + /@emotion/react@11.11.4(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==} peerDependencies: '@types/react': '*' react: '>=16.8.0' @@ -999,8 +1005,8 @@ packages: resolution: {integrity: sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==} dev: false - /@emotion/styled@11.10.6(@emotion/react@11.10.6)(@types/react@18.2.66)(react@18.2.0): - resolution: {integrity: sha512-OXtBzOmDSJo5Q0AFemHCfl+bUueT8BIcPSxu0EGTpGk6DmI5dnhSzQANm1e1ze0YZL7TDyAyy6s/b/zmGOS3Og==} + /@emotion/styled@11.11.5(@emotion/react@11.11.4)(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-/ZjjnaNKvuMPxcIiUkf/9SHoG4Q196DRl1w82hQ3WCsjo1IUR8uaGWrC6a87CrYAW0Kb/pK7hk8BnLgLRi9KoQ==} peerDependencies: '@emotion/react': ^11.0.0-rc.0 '@types/react': '*' @@ -1012,7 +1018,7 @@ packages: '@babel/runtime': 7.24.1 '@emotion/babel-plugin': 11.11.0 '@emotion/is-prop-valid': 1.2.2 - '@emotion/react': 11.10.6(@types/react@18.2.66)(react@18.2.0) + '@emotion/react': 11.11.4(@types/react@18.2.66)(react@18.2.0) '@emotion/serialize': 1.1.4 '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) '@emotion/utils': 1.2.1 @@ -1261,32 +1267,32 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /@floating-ui/core@1.6.0: - resolution: {integrity: sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==} + /@floating-ui/core@1.6.2: + resolution: {integrity: sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==} dependencies: - '@floating-ui/utils': 0.2.1 + '@floating-ui/utils': 0.2.2 dev: false - /@floating-ui/dom@1.6.3: - resolution: {integrity: sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==} + /@floating-ui/dom@1.6.5: + resolution: {integrity: sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==} dependencies: - '@floating-ui/core': 1.6.0 - '@floating-ui/utils': 0.2.1 + '@floating-ui/core': 1.6.2 + '@floating-ui/utils': 0.2.2 dev: false - /@floating-ui/react-dom@2.0.8(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==} + /@floating-ui/react-dom@2.1.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-lNzj5EQmEKn5FFKc04+zasr09h/uX8RtJRNj5gUXsSQIXHVWTVh+hVAg1vOMCexkX8EgvemMvIFpQfkosnVNyA==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' dependencies: - '@floating-ui/dom': 1.6.3 + '@floating-ui/dom': 1.6.5 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@floating-ui/utils@0.2.1: - resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==} + /@floating-ui/utils@0.2.2: + resolution: {integrity: sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==} dev: false /@humanwhocodes/config-array@0.11.14: @@ -1579,6 +1585,10 @@ packages: resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} dev: false + /@lokesh.dhakar/quantize@1.3.0: + resolution: {integrity: sha512-4KBSyaMj65d8A+2vnzLxtHFu4OmBU4IKO0yLxZ171Itdf9jGV4w+WbG7VsKts2jUdRkFSzsZqpZOz6hTB3qGAw==} + dev: false + /@mui/base@5.0.0-beta.40(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==} engines: {node: '>=12.0.0'} @@ -1591,12 +1601,12 @@ packages: optional: true dependencies: '@babel/runtime': 7.24.1 - '@floating-ui/react-dom': 2.0.8(react-dom@18.2.0)(react@18.2.0) + '@floating-ui/react-dom': 2.1.0(react-dom@18.2.0)(react@18.2.0) '@mui/types': 7.2.14(@types/react@18.2.66) '@mui/utils': 5.15.14(@types/react@18.2.66)(react@18.2.0) '@popperjs/core': 2.11.8 '@types/react': 18.2.66 - clsx: 2.1.0 + clsx: 2.1.1 prop-types: 15.8.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -1613,24 +1623,24 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.4 - '@floating-ui/react-dom': 2.0.8(react-dom@18.2.0)(react@18.2.0) + '@babel/runtime': 7.24.6 + '@floating-ui/react-dom': 2.1.0(react-dom@18.2.0)(react@18.2.0) '@mui/types': 7.2.14(@types/react@18.2.66) - '@mui/utils': 6.0.0-alpha.1(@types/react@18.2.66)(react@18.2.0) + '@mui/utils': 6.0.0-alpha.8(@types/react@18.2.66)(react@18.2.0) '@popperjs/core': 2.11.8 '@types/react': 18.2.66 - clsx: 2.1.0 + clsx: 2.1.1 prop-types: 15.8.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@mui/core-downloads-tracker@6.0.0-alpha.2: - resolution: {integrity: sha512-6JSNsfMePoLZ0tKZzQ2i+W8mcZCbP9snFNf/pQGOsMK2igkPGqOfqgdsda2OJrD5tP9VGjEDrrmYGHuX3ir29g==} + /@mui/core-downloads-tracker@6.0.0-dev.240424162023-9968b4889d: + resolution: {integrity: sha512-doh3M3U7HUGSBIWGe1yvesSbfDguMRjP0N09ogWSBM2hovXAlgULhMgcRTepAZLLwfRxFII0bCohq6B9NqoKuw==} dev: false - /@mui/icons-material@5.11.11(@mui/material@6.0.0-alpha.2)(@types/react@18.2.66)(react@18.2.0): - resolution: {integrity: sha512-Eell3ADmQVE8HOpt/LZ3zIma8JSvPh3XgnhwZLT0k5HRqZcd6F/QDHc7xsWtgz09t+UEFvOYJXjtrwKmLdwwpw==} + /@mui/icons-material@5.15.18(@mui/material@6.0.0-alpha.2)(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-jGhyw02TSLM0NgW+MDQRLLRUD/K4eN9rlK2pTBTL1OtzyZmQ8nB060zK1wA0b7cVrIiG+zyrRmNAvGWXwm2N9Q==} engines: {node: '>=12.0.0'} peerDependencies: '@mui/material': ^5.0.0 @@ -1641,12 +1651,12 @@ packages: optional: true dependencies: '@babel/runtime': 7.24.1 - '@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/material': 6.0.0-alpha.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) '@types/react': 18.2.66 react: 18.2.0 dev: false - /@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/material@6.0.0-alpha.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-p1GpE1a7dQTns0yp0anSNX/Bh1xafTdUCt0roTyqEuL/3hCBKTURE/9/CDttwwQ+Q8oDm5KcsdtXJXJh1ts6Kw==} engines: {node: '>=12.0.0'} peerDependencies: @@ -1663,17 +1673,17 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.4 - '@emotion/react': 11.10.6(@types/react@18.2.66)(react@18.2.0) - '@emotion/styled': 11.10.6(@emotion/react@11.10.6)(@types/react@18.2.66)(react@18.2.0) + '@babel/runtime': 7.24.6 + '@emotion/react': 11.11.4(@types/react@18.2.66)(react@18.2.0) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.2.66)(react@18.2.0) '@mui/base': 5.0.0-beta.42(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) - '@mui/core-downloads-tracker': 6.0.0-alpha.2 - '@mui/system': 6.0.0-alpha.1(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react@18.2.0) + '@mui/core-downloads-tracker': 6.0.0-dev.240424162023-9968b4889d + '@mui/system': 6.0.0-dev.240424162023-9968b4889d(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.66)(react@18.2.0) '@mui/types': 7.2.14(@types/react@18.2.66) - '@mui/utils': 6.0.0-alpha.1(@types/react@18.2.66)(react@18.2.0) + '@mui/utils': 6.0.0-alpha.8(@types/react@18.2.66)(react@18.2.0) '@types/react': 18.2.66 '@types/react-transition-group': 4.4.10 - clsx: 2.1.0 + clsx: 2.1.1 csstype: 3.1.3 prop-types: 15.8.1 react: 18.2.0 @@ -1692,15 +1702,15 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.4 + '@babel/runtime': 7.24.6 '@mui/utils': 5.15.14(@types/react@18.2.66)(react@18.2.0) '@types/react': 18.2.66 prop-types: 15.8.1 react: 18.2.0 dev: false - /@mui/private-theming@6.0.0-alpha.1(@types/react@18.2.66)(react@18.2.0): - resolution: {integrity: sha512-S+vlRxja0ncLT1KK+Qkqy6ZNpIsw5ZxoTd70KR8foLtPQXs0XGWQkq9IotbcZnYyKUUhM/5Cqo3w8Q/zNNVzBQ==} + /@mui/private-theming@6.0.0-alpha.8(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-0iN+hK/OZTaiVfjFYDgWEc/frRB7Z1hfBsSJBniM4KPZnrdeHIArP+3TdYzRT0avh30O2KNkBNk0GG95BnUVEg==} engines: {node: '>=12.0.0'} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 @@ -1709,14 +1719,14 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.4 - '@mui/utils': 6.0.0-alpha.1(@types/react@18.2.66)(react@18.2.0) + '@babel/runtime': 7.24.6 + '@mui/utils': 6.0.0-alpha.8(@types/react@18.2.66)(react@18.2.0) '@types/react': 18.2.66 prop-types: 15.8.1 react: 18.2.0 dev: false - /@mui/styled-engine@5.15.14(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(react@18.2.0): + /@mui/styled-engine@5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.2.0): resolution: {integrity: sha512-RILkuVD8gY6PvjZjqnWhz8fu68dVkqhM5+jYWfB5yhlSQKg+2rHkmEwm75XIeAqI3qwOndK6zELK5H6Zxn4NHw==} engines: {node: '>=12.0.0'} peerDependencies: @@ -1729,17 +1739,17 @@ packages: '@emotion/styled': optional: true dependencies: - '@babel/runtime': 7.24.4 + '@babel/runtime': 7.24.6 '@emotion/cache': 11.11.0 - '@emotion/react': 11.10.6(@types/react@18.2.66)(react@18.2.0) - '@emotion/styled': 11.10.6(@emotion/react@11.10.6)(@types/react@18.2.66)(react@18.2.0) + '@emotion/react': 11.11.4(@types/react@18.2.66)(react@18.2.0) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.2.66)(react@18.2.0) csstype: 3.1.3 prop-types: 15.8.1 react: 18.2.0 dev: false - /@mui/styled-engine@6.0.0-alpha.1(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(react@18.2.0): - resolution: {integrity: sha512-lJmMF1D3SD9unSh5gjXYU3ChiCrNGrpT36cPOmGv7wiH2qLMov5Wo23nK7UfuaQVA4c4/JAS43SLT2O2z6hx4Q==} + /@mui/styled-engine@6.0.0-alpha.8(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.2.0): + resolution: {integrity: sha512-7zJYgbjZRQpGN1SGmLDOgRpJZB26JjPSeqml5m+jA4wAsIONm2im+GHfki4nE3ay0uj1S555OMeNpaQ+sG9LkA==} engines: {node: '>=12.0.0'} peerDependencies: '@emotion/react': ^11.4.1 @@ -1751,17 +1761,17 @@ packages: '@emotion/styled': optional: true dependencies: - '@babel/runtime': 7.24.4 + '@babel/runtime': 7.24.6 '@emotion/cache': 11.11.0 - '@emotion/react': 11.10.6(@types/react@18.2.66)(react@18.2.0) - '@emotion/styled': 11.10.6(@emotion/react@11.10.6)(@types/react@18.2.66)(react@18.2.0) + '@emotion/react': 11.11.4(@types/react@18.2.66)(react@18.2.0) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.2.66)(react@18.2.0) csstype: 3.1.3 prop-types: 15.8.1 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.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-aulox6N1dnu5PABsfxVGOZffDVmlxPOVgj56HrUnJE8MCSh8lOvvkd47cebIVQQYAjpwieXQXiDPj5pwM40jTQ==} engines: {node: '>=12.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -1776,22 +1786,22 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.4 - '@emotion/react': 11.10.6(@types/react@18.2.66)(react@18.2.0) - '@emotion/styled': 11.10.6(@emotion/react@11.10.6)(@types/react@18.2.66)(react@18.2.0) + '@babel/runtime': 7.24.6 + '@emotion/react': 11.11.4(@types/react@18.2.66)(react@18.2.0) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.2.66)(react@18.2.0) '@mui/private-theming': 5.15.14(@types/react@18.2.66)(react@18.2.0) - '@mui/styled-engine': 5.15.14(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(react@18.2.0) + '@mui/styled-engine': 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.2.0) '@mui/types': 7.2.14(@types/react@18.2.66) '@mui/utils': 5.15.14(@types/react@18.2.66)(react@18.2.0) '@types/react': 18.2.66 - clsx: 2.1.0 + clsx: 2.1.1 csstype: 3.1.3 prop-types: 15.8.1 react: 18.2.0 dev: false - /@mui/system@6.0.0-alpha.1(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react@18.2.0): - resolution: {integrity: sha512-NqOFHCT/lvi4v696b0pEEz2Jc3MgjjGRsbFNN+IW5EuIInHr/JEznnPw0GpEU8tgU544MdfMTlFpDgR1LmvmLA==} + /@mui/system@6.0.0-dev.240424162023-9968b4889d(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-Y3yCFUHN1xMK62hJJBqzZb1YQvHNaHc7JUX01eU6QTPojtIbGMF2jCOP/EQw77/byahNbxeLoAIQx10F0IR3Rw==} engines: {node: '>=12.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -1806,15 +1816,15 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.4 - '@emotion/react': 11.10.6(@types/react@18.2.66)(react@18.2.0) - '@emotion/styled': 11.10.6(@emotion/react@11.10.6)(@types/react@18.2.66)(react@18.2.0) - '@mui/private-theming': 6.0.0-alpha.1(@types/react@18.2.66)(react@18.2.0) - '@mui/styled-engine': 6.0.0-alpha.1(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(react@18.2.0) + '@babel/runtime': 7.24.6 + '@emotion/react': 11.11.4(@types/react@18.2.66)(react@18.2.0) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.2.66)(react@18.2.0) + '@mui/private-theming': 6.0.0-alpha.8(@types/react@18.2.66)(react@18.2.0) + '@mui/styled-engine': 6.0.0-alpha.8(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.2.0) '@mui/types': 7.2.14(@types/react@18.2.66) - '@mui/utils': 6.0.0-alpha.1(@types/react@18.2.66)(react@18.2.0) + '@mui/utils': 6.0.0-alpha.8(@types/react@18.2.66)(react@18.2.0) '@types/react': 18.2.66 - clsx: 2.1.0 + clsx: 2.1.1 csstype: 3.1.3 prop-types: 15.8.1 react: 18.2.0 @@ -1849,8 +1859,8 @@ packages: react-is: 18.2.0 dev: false - /@mui/utils@6.0.0-alpha.1(@types/react@18.2.66)(react@18.2.0): - resolution: {integrity: sha512-BvTVWvW6JDxMjDfEfTTEb6/CLgj87HyS3hYA4hbk0LdFsXeRnFSK6yDj8Dw3gxvXBymQ26kHDyFK3pEoAFlurw==} + /@mui/utils@6.0.0-alpha.8(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-X5lg0bh8B6uYt/0HXV+t82HXLTOVFEKcIBmIbJ5El1h9ykXaRTenr8mORxt5UC5w9DHFhkRoI8XiM5qyDuSJVw==} engines: {node: '>=12.0.0'} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 @@ -1859,7 +1869,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.4 + '@babel/runtime': 7.24.6 '@types/prop-types': 15.7.12 '@types/react': 18.2.66 prop-types: 15.8.1 @@ -1867,15 +1877,15 @@ 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): - resolution: {integrity: sha512-8lEVEOtCQssKWel4Ey1pRulGPXUQ73TnkHKzHWsjdv03FjiUs3eYB+Ej0Uk5yWPmsqlShWhOzOlOGDpzsYJsUg==} + /@mui/x-date-pickers-pro@6.20.0(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@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-lXbO8xCKRvIKgu2R8EAGhYwN1BkMS9GxTeinKZg5lCGvxTrd5pErQ4xpOxYVQq7wNLphDiY7I/Xf88VekKTNLQ==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.9.0 '@emotion/styled': ^11.8.1 '@mui/material': ^5.8.6 '@mui/system': ^5.8.0 - date-fns: ^2.25.0 + date-fns: ^2.25.0 || ^3.2.0 date-fns-jalali: ^2.13.0-0 dayjs: ^1.10.7 luxon: ^3.0.2 @@ -1905,15 +1915,15 @@ packages: optional: true dependencies: '@babel/runtime': 7.24.1 - '@emotion/react': 11.10.6(@types/react@18.2.66)(react@18.2.0) - '@emotion/styled': 11.10.6(@emotion/react@11.10.6)(@types/react@18.2.66)(react@18.2.0) + '@emotion/react': 11.11.4(@types/react@18.2.66)(react@18.2.0) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@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/material': 6.0.0-alpha.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@mui/system': 5.15.15(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@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.20.0(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@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 + clsx: 2.1.1 dayjs: 1.11.9 prop-types: 15.8.1 react: 18.2.0 @@ -1923,15 +1933,15 @@ 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): - resolution: {integrity: sha512-HJq4uoFQSu5isa/mesWw2BKh8KBRYUQb+KaSlVlWfJNgP3YhPvWZ6yqCNYyxOAiPMxb0n3nBjS9ErO27OHjFMA==} + /@mui/x-date-pickers@6.20.0(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@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-q/x3rNmPYMXnx75+3s9pQb1YDtws9y5bwxpxeB3EW88oCp33eS7bvJpeuoCA1LzW/PpVfIRhi5RCyAvrEeTL7Q==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.9.0 '@emotion/styled': ^11.8.1 '@mui/material': ^5.8.6 '@mui/system': ^5.8.0 - date-fns: ^2.25.0 + date-fns: ^2.25.0 || ^3.2.0 date-fns-jalali: ^2.13.0-0 dayjs: ^1.10.7 luxon: ^3.0.2 @@ -1961,14 +1971,14 @@ packages: optional: true dependencies: '@babel/runtime': 7.24.1 - '@emotion/react': 11.10.6(@types/react@18.2.66)(react@18.2.0) - '@emotion/styled': 11.10.6(@emotion/react@11.10.6)(@types/react@18.2.66)(react@18.2.0) + '@emotion/react': 11.11.4(@types/react@18.2.66)(react@18.2.0) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@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/material': 6.0.0-alpha.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@mui/system': 5.15.15(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@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 + clsx: 2.1.1 dayjs: 1.11.9 prop-types: 15.8.1 react: 18.2.0 @@ -2026,7 +2036,7 @@ packages: /@popperjs/core@2.11.8: resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - /@reduxjs/toolkit@2.0.0(react-redux@8.0.5)(react@18.2.0): + /@reduxjs/toolkit@2.0.0(react-redux@8.1.3)(react@18.2.0): resolution: {integrity: sha512-Kq/a+aO28adYdPoNEu9p800MYPKoUc0tlkYfv035Ief9J7MPq8JvmT7UdpYhvXsoMtOdt567KwZjc9H3Rf8yjg==} peerDependencies: react: ^16.9.0 || ^17.0.0 || ^18 @@ -2037,16 +2047,16 @@ packages: react-redux: optional: true dependencies: - immer: 10.0.4 + immer: 10.1.1 react: 18.2.0 - react-redux: 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) + react-redux: 8.1.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1) redux: 5.0.1 redux-thunk: 3.1.0(redux@5.0.1) - reselect: 5.0.1 + reselect: 5.1.0 dev: false - /@remix-run/router@1.15.3: - resolution: {integrity: sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==} + /@remix-run/router@1.16.1: + resolution: {integrity: sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==} engines: {node: '>=14.0.0'} dev: false @@ -2205,15 +2215,15 @@ packages: dependencies: '@sinonjs/commons': 3.0.1 - /@slate-yjs/core@1.0.2(slate@0.101.4)(yjs@13.6.14): + /@slate-yjs/core@1.0.2(slate@0.101.5)(yjs@13.6.15): resolution: {integrity: sha512-X0hLFJbQu9c1ItWBaNuEn0pqcXYK76KCp8C4Gvy/VaTQVMo1VgAb2WiiJ0Je/AyuIYEPPSTNVOcyrGHwgA7e6Q==} peerDependencies: slate: '>=0.70.0' yjs: ^13.5.29 dependencies: - slate: 0.101.4 - y-protocols: 1.0.6(yjs@13.6.14) - yjs: 13.6.14 + slate: 0.101.5 + y-protocols: 1.0.6(yjs@13.6.15) + yjs: 13.6.15 dev: false /@svgr/babel-plugin-add-jsx-attribute@7.0.0(@babel/core@7.24.3): @@ -2455,8 +2465,8 @@ packages: - typescript dev: true - /@tauri-apps/api@1.5.3: - resolution: {integrity: sha512-zxnDjHHKjOsrIzZm6nO5Xapb/BxqUq1tc7cGkFXsFkGTsSWgCPH1D8mm0XS9weJY2OaR73I3k3S+b7eSzJDfqA==} + /@tauri-apps/api@1.5.6: + resolution: {integrity: sha512-LH5ToovAHnDVe5Qa9f/+jW28I6DeMhos8bNDtBOmmnaDpPmJmYLyHdeDblAWWWYc7KKRDg9/66vMuKyq0WIeFA==} engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'} dev: false @@ -2618,6 +2628,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==} @@ -2723,6 +2737,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: @@ -2774,8 +2796,8 @@ packages: redux: 4.2.1 dev: false - /@types/react-swipeable-views@0.13.4: - resolution: {integrity: sha512-hQV9Oq6oa+9HKdnGd43xkckElwf5dThOiegtQxqE7qX761oHhxnZO07fz6IsKSnUy9J3tzlRQBu3sNyvC8+kYw==} + /@types/react-swipeable-views@0.13.5: + resolution: {integrity: sha512-ni6WjO7gBq2xB2Y/ZiRdQOgjGOxIik5ow2s7xKieDq8DxsXTdV46jJslSBVK2yoIJHf6mG3uqNTwxwgzbXRRzg==} dependencies: '@types/react': 18.2.66 dev: false @@ -3023,7 +3045,7 @@ packages: '@babel/plugin-transform-react-jsx-source': 7.24.1(@babel/core@7.24.3) '@types/babel__core': 7.20.5 react-refresh: 0.14.0 - vite: 5.2.0(@types/node@20.11.30)(sass@1.70.0) + vite: 5.2.0(@types/node@20.11.30)(sass@1.77.2) transitivePeerDependencies: - supports-color dev: true @@ -3106,7 +3128,6 @@ packages: fast-json-stable-stringify: 2.1.0 json-schema-traverse: 0.4.1 uri-js: 4.4.1 - dev: true /ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} @@ -3250,12 +3271,10 @@ packages: resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} dependencies: safer-buffer: 2.1.2 - dev: true /assert-plus@1.0.0: resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} engines: {node: '>=0.8'} - dev: true /astral-regex@2.0.0: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} @@ -3305,14 +3324,12 @@ packages: /aws-sign2@0.7.0: resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} - dev: true /aws4@1.12.0: resolution: {integrity: sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==} - dev: true - /axios@1.6.8: - resolution: {integrity: sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==} + /axios@1.7.2: + resolution: {integrity: sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==} dependencies: follow-redirects: 1.15.6 form-data: 4.0.0 @@ -3442,14 +3459,13 @@ packages: resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} dependencies: tweetnacl: 0.14.5 - dev: true /binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - /bind-event-listener@2.1.1: - resolution: {integrity: sha512-O+a5c0D2se/u2VlBJmPRn45IB6R4mYMh1ok3dWxrIZ2pmLqzggBhb875mbq73508ylzofc0+hT9W41x4Y2s8lg==} + /bind-event-listener@3.0.0: + resolution: {integrity: sha512-PJvH288AWQhKs2v9zyfYdPzlPqf5bXbGMmhmUIY9x4dAUGIWgomO771oBQNwJnMQSnUIXhKu6sgzpBRXTlvb8Q==} dev: false /blob-util@2.0.2: @@ -3576,7 +3592,6 @@ packages: /caseless@0.12.0: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} - dev: true /chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} @@ -3696,8 +3711,8 @@ packages: engines: {node: '>=6'} dev: false - /clsx@2.1.0: - resolution: {integrity: sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==} + /clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} dev: false @@ -3729,6 +3744,13 @@ packages: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} dev: true + /colorthief@2.4.0: + resolution: {integrity: sha512-0U48RGNRo5fVO+yusBwgp+d3augWSorXabnqXUu9SabEhCpCgZJEUjUTTI41OOBBYuMMxawa3177POT6qLfLeQ==} + dependencies: + '@lokesh.dhakar/quantize': 1.3.0 + get-pixels: 3.3.3 + dev: false + /combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -3778,7 +3800,6 @@ packages: /core-util-is@1.0.2: resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} - dev: true /core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -3917,6 +3938,12 @@ packages: /csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + /cwise-compiler@1.1.3: + resolution: {integrity: sha512-WXlK/m+Di8DMMcCjcWr4i+XzcQra9eCdXIJrgh4TUgh0pIS/yJduLxS9JgefsHJ/YVLdgPtXm9r62W92MvanEQ==} + dependencies: + uniq: 1.0.1 + dev: false + /cypress@13.7.2: resolution: {integrity: sha512-FF5hFI5wlRIHY8urLZjJjj/YvfCBrRpglbZCLr/cYcL9MdDe0+5usa8kTIrDHthlEc9lwihbkb5dmwqBDNS2yw==} engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0} @@ -3972,7 +3999,10 @@ packages: engines: {node: '>=0.10'} dependencies: assert-plus: 1.0.0 - dev: true + + /data-uri-to-buffer@0.0.3: + resolution: {integrity: sha512-Cp+jOa8QJef5nXS5hU7M1DWzXPEIoVR3kbV0dQuVGwROZg8bGf1DcCnkmajBTnvghTtSNMUdRrPjgaT6ZQucbw==} + dev: false /data-urls@3.0.2: resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} @@ -4112,14 +4142,6 @@ packages: engines: {node: '>=6'} dev: false - /derive-valtio@0.1.0(valtio@1.12.1): - resolution: {integrity: sha512-OCg2UsLbXK7GmmpzMXhYkdO64vhJ1ROUUGaTFyHjVwEdMEcTTRj7W1TxLbSBxdY8QLBPCcp66MTyaSy0RpO17A==} - peerDependencies: - valtio: '*' - dependencies: - valtio: 1.12.1(@types/react@18.2.66)(react@18.2.0) - dev: false - /detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -4258,7 +4280,6 @@ packages: dependencies: jsbn: 0.1.1 safer-buffer: 2.1.2 - dev: true /electron-to-chromium@1.4.722: resolution: {integrity: sha512-5nLE0TWFFpZ80Crhtp4pIp8LXCztjYX41yUcV6b+bKR2PqzjskTMOOlBi1VjBHlvHwS+4gar7kNKOrsbsewEZQ==} @@ -4267,12 +4288,12 @@ packages: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} - /emoji-mart@5.5.2: - resolution: {integrity: sha512-Sqc/nso4cjxhOwWJsp9xkVm8OF5c+mJLZJFoFfzRuKO+yWiN7K8c96xmtughYb0d/fZ8UC6cLIQ/p4BR6Pv3/A==} + /emoji-mart@5.6.0: + resolution: {integrity: sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==} dev: false - /emoji-regex@10.2.1: - resolution: {integrity: sha512-97g6QgOk8zlDRdgq1WxwgTMgEWGVAQvB5Fdpgc1MkNy56la5SKP9GsMXKDOdqwn90/41a8yPwIGk1Y6WVbeMQA==} + /emoji-regex@10.3.0: + resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==} dev: false /emoji-regex@8.0.0: @@ -4683,11 +4704,9 @@ packages: /extsprintf@1.3.0: resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} engines: {'0': node >=0.6.0} - dev: true /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: true /fast-diff@1.1.2: resolution: {integrity: sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==} @@ -4818,7 +4837,6 @@ packages: /forever-agent@0.6.1: resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} - dev: true /form-data@2.3.3: resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} @@ -4827,7 +4845,6 @@ packages: asynckit: 0.4.0 combined-stream: 1.0.8 mime-types: 2.1.35 - dev: true /form-data@4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} @@ -4912,6 +4929,22 @@ packages: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} + /get-pixels@3.3.3: + resolution: {integrity: sha512-5kyGBn90i9tSMUVHTqkgCHsoWoR+/lGbl4yC83Gefyr0HLIhgSWEx/2F/3YgsZ7UpYNuM6pDhDK7zebrUJ5nXg==} + dependencies: + data-uri-to-buffer: 0.0.3 + jpeg-js: 0.4.4 + mime-types: 2.1.35 + ndarray: 1.0.19 + ndarray-pack: 1.2.1 + node-bitmap: 0.0.1 + omggif: 1.0.10 + parse-data-uri: 0.2.0 + pngjs: 3.4.0 + request: 2.88.2 + through: 2.3.8 + dev: false + /get-stream@5.2.0: resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} engines: {node: '>=8'} @@ -4942,7 +4975,6 @@ packages: resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} dependencies: assert-plus: 1.0.0 - dev: true /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -5028,8 +5060,8 @@ packages: csstype: 3.1.3 dev: false - /google-protobuf@3.16.0: - resolution: {integrity: sha512-gBY66yYL1wbQMU2r1POkXSXkm035Ni0wFv3vx0K9IEUsJLP9G5rAcFVn0xUXfZneRu6MmDjaw93pt/DE56VOyw==} + /google-protobuf@3.21.2: + resolution: {integrity: sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==} dev: false /gopd@1.0.1: @@ -5056,6 +5088,20 @@ packages: through2: 2.0.5 dev: true + /har-schema@2.0.0: + resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==} + engines: {node: '>=4'} + dev: false + + /har-validator@5.1.5: + resolution: {integrity: sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==} + engines: {node: '>=6'} + deprecated: this library is no longer supported + dependencies: + ajv: 6.12.6 + har-schema: 2.0.0 + dev: false + /has-bigints@1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} dev: true @@ -5133,6 +5179,15 @@ packages: - supports-color dev: true + /http-signature@1.2.0: + resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} + engines: {node: '>=0.8', npm: '>=1.3.7'} + dependencies: + assert-plus: 1.0.0 + jsprim: 1.4.2 + sshpk: 1.18.0 + dev: false + /http-signature@1.3.6: resolution: {integrity: sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==} engines: {node: '>=0.10'} @@ -5161,20 +5216,20 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} - /i18next-browser-languagedetector@7.0.1: - resolution: {integrity: sha512-Pa5kFwaczXJAeHE56CHG2aWzFBMJNUNghf0Pm4SwSrEMps/PTKqW90EYWlIvhuYStf3Sn1K0vw+gH3+TLdkH1g==} + /i18next-browser-languagedetector@7.2.1: + resolution: {integrity: sha512-h/pM34bcH6tbz8WgGXcmWauNpQupCGr25XPp9cZwZInR9XHSjIFDYp1SIok7zSPsTOMxdvuLyu86V+g2Kycnfw==} dependencies: '@babel/runtime': 7.24.1 dev: false - /i18next-resources-to-backend@1.1.4: - resolution: {integrity: sha512-hMyr9AOmIea17AOaVe1srNxK/l3mbk81P7Uf3fdcjlw3ehZy3UNTd0OP3EEi6yu4J02kf9jzhCcjokz6AFlEOg==} + /i18next-resources-to-backend@1.2.1: + resolution: {integrity: sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw==} dependencies: '@babel/runtime': 7.24.1 dev: false - /i18next@22.4.10: - resolution: {integrity: sha512-3EqgGK6fAJRjnGgfkNSStl4mYLCjUoJID338yVyLMj5APT67HUtWoqSayZewiiC5elzMUB1VEUwcmSCoeQcNEA==} + /i18next@22.5.1: + resolution: {integrity: sha512-8TGPgM3pAD+VRsMtUMNknRz3kzqwp/gPALrWMsDnmC1mKqJwpWyooQRLMcbTwq8z8YwSmuj+ZYvc+xCuEpkssA==} dependencies: '@babel/runtime': 7.24.1 dev: false @@ -5195,12 +5250,12 @@ packages: engines: {node: '>= 4'} dev: true - /immer@10.0.4: - resolution: {integrity: sha512-cuBuGK40P/sk5IzWa9QPUaAdvPHjkk1c+xYsd9oZw+YQQEV+10G0P5uMpGctZZKnyQ+ibRO08bD25nWLmYi2pw==} + /immer@10.1.1: + resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} dev: false - /immutable@4.3.5: - resolution: {integrity: sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==} + /immutable@4.3.6: + resolution: {integrity: sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==} /import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} @@ -5255,6 +5310,10 @@ packages: loose-envify: 1.4.0 dev: false + /iota-array@1.0.0: + resolution: {integrity: sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA==} + dev: false + /is-arguments@1.1.1: resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} engines: {node: '>= 0.4'} @@ -5294,6 +5353,10 @@ packages: has-tostringtag: 1.0.2 dev: true + /is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + dev: false + /is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} @@ -5440,7 +5503,6 @@ packages: /is-typedarray@1.0.0: resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} - dev: true /is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} @@ -5477,7 +5539,6 @@ packages: /isstream@0.1.2: resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} - dev: true /istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} @@ -5948,8 +6009,12 @@ packages: - supports-color - ts-node - /js-base64@3.7.5: - resolution: {integrity: sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==} + /jpeg-js@0.4.4: + resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} + dev: false + + /js-base64@3.7.7: + resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==} dev: false /js-tokens@4.0.0: @@ -5971,7 +6036,6 @@ packages: /jsbn@0.1.1: resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} - dev: true /jsdom@20.0.3: resolution: {integrity: sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==} @@ -6028,11 +6092,9 @@ packages: /json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - dev: true /json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} - dev: true /json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -6040,7 +6102,6 @@ packages: /json-stringify-safe@5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - dev: true /json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} @@ -6059,6 +6120,16 @@ packages: graceful-fs: 4.2.11 dev: true + /jsprim@1.4.2: + resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==} + engines: {node: '>=0.6.0'} + dependencies: + assert-plus: 1.0.0 + extsprintf: 1.3.0 + json-schema: 0.4.0 + verror: 1.10.0 + dev: false + /jsprim@2.0.2: resolution: {integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==} engines: {'0': node >=0.6.0} @@ -6079,8 +6150,8 @@ packages: object.values: 1.2.0 dev: true - /katex@0.16.7: - resolution: {integrity: sha512-Xk9C6oGKRwJTfqfIbtr0Kes9OSv6IFsuhFGc7tW4urlpMJtuh+7YhzU6YEG9n8gmWKcMAFzkp7nr+r69kV0zrA==} + /katex@0.16.10: + resolution: {integrity: sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==} hasBin: true dependencies: commander: 8.3.0 @@ -6121,8 +6192,8 @@ packages: type-check: 0.4.0 dev: true - /lib0@0.2.93: - resolution: {integrity: sha512-M5IKsiFJYulS+8Eal8f+zAqf5ckm1vffW0fFDxfgxJ+uiVopvDdd3PxJmz0GsVi3YNO7QCFSq0nAsiDmNhLj9Q==} + /lib0@0.2.94: + resolution: {integrity: sha512-hZ3p54jL4Wpu7IOg26uC7dnEWiMyNlUrb9KoG7+xYs45WkQwpVvKFndVq2+pqLYKe1u8Fp3+zAfZHVvTK34PvQ==} engines: {node: '>=16'} hasBin: true dependencies: @@ -6374,8 +6445,8 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - /nanoid@4.0.0: - resolution: {integrity: sha512-IgBP8piMxe/gf73RTQx7hmnhwz0aaEXYakvqZyE302IXW3HyVNhdNGC+O2MwMAVhLEnvXlvKtGbtJf6wvHihCg==} + /nanoid@4.0.2: + resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==} engines: {node: ^14 || ^16 || >=18} hasBin: true dev: false @@ -6383,6 +6454,20 @@ packages: /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + /ndarray-pack@1.2.1: + resolution: {integrity: sha512-51cECUJMT0rUZNQa09EoKsnFeDL4x2dHRT0VR5U2H5ZgEcm95ZDWcMA5JShroXjHOejmAD/fg8+H+OvUnVXz2g==} + dependencies: + cwise-compiler: 1.1.3 + ndarray: 1.0.19 + dev: false + + /ndarray@1.0.19: + resolution: {integrity: sha512-B4JHA4vdyZU30ELBw3g7/p9bZupyew5a7tX1Y/gGeF2hafrPaQZhgrGQfsvgfYbgdFZjYwuEcnaobeM/WMW+HQ==} + dependencies: + iota-array: 1.0.0 + is-buffer: 1.1.6 + dev: false + /no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} dependencies: @@ -6390,6 +6475,11 @@ packages: tslib: 2.6.2 dev: true + /node-bitmap@0.0.1: + resolution: {integrity: sha512-Jx5lPaaLdIaOsj2mVLWMWulXF6GQVdyLvNSxmiYCvZ8Ma2hfKX0POoR2kgKOqz+oFsRreq0yYZjQ2wjE9VNzCA==} + engines: {node: '>=v0.6.5'} + dev: false + /node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} @@ -6425,6 +6515,10 @@ packages: resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==} dev: true + /oauth-sign@0.9.0: + resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} + dev: false + /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -6497,6 +6591,10 @@ packages: es-object-atoms: 1.0.0 dev: true + /omggif@1.0.10: + resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==} + dev: false + /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: @@ -6589,6 +6687,12 @@ packages: dependencies: callsites: 3.1.0 + /parse-data-uri@0.2.0: + resolution: {integrity: sha512-uOtts8NqDcaCt1rIsO3VFDRsAfgE4c6osG4d9z3l4dCBlxYFzni6Di/oNU270SDrjkfZuUvLZx1rxMyqh46Y9w==} + dependencies: + data-uri-to-buffer: 0.0.3 + dev: false + /parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -6682,6 +6786,11 @@ packages: dependencies: find-up: 4.1.0 + /pngjs@3.4.0: + resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==} + engines: {node: '>=4.0.0'} + dev: false + /possible-typed-array-names@1.0.0: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} @@ -6878,10 +6987,6 @@ packages: hasBin: true dev: false - /proxy-compare@2.5.1: - resolution: {integrity: sha512-oyfc0Tx87Cpwva5ZXezSp5V9vht1c7dZBhvuV/y3ctkgMVUmiAGDVeeB0dKhGSyT0v1ZTEQYpe/RXlBVBNuCLA==} - dev: false - /proxy-from-env@1.0.0: resolution: {integrity: sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==} dev: true @@ -6892,7 +6997,6 @@ packages: /psl@1.9.0: resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} - dev: true /pump@2.0.1: resolution: {integrity: sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==} @@ -6919,7 +7023,6 @@ packages: /punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - dev: true /pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} @@ -6931,6 +7034,11 @@ packages: side-channel: 1.0.6 dev: true + /qs@6.5.3: + resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==} + engines: {node: '>=0.6'} + dev: false + /querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} dev: true @@ -7014,8 +7122,8 @@ packages: - react-native dev: false - /react-big-calendar@1.8.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-cra8WPfoTSQthFfqxi0k9xm/Shv5jWSw19LkNzpSJcnQhP6XGes/eJjd8P8g/iwaJjXIWPpg3+HB5wO5wabRyA==} + /react-big-calendar@1.12.2(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-cPVcwH5V1YiC6QKaV4afvpuZ2DtP8+TocnZY98nGodqq8bfjVDiP3Ch+TewBZzj9mg7JbewHdufDZXZBqQl1lw==} peerDependencies: react: ^16.14.0 || ^17 || ^18 react-dom: ^16.14.0 || ^17 || ^18 @@ -7055,6 +7163,19 @@ packages: tinycolor2: 1.6.0 dev: false + /react-custom-scrollbars-2@4.5.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-/z0nWAeXfMDr4+OXReTpYd1Atq9kkn4oI3qxq3iMXGQx1EEfwETSqB8HTAvg1X7dEqcCachbny1DRNGlqX5bDQ==} + peerDependencies: + react: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + dom-css: 2.1.0 + prop-types: 15.8.1 + raf: 3.4.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react-custom-scrollbars@4.2.1(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-VtJTUvZ7kPh/auZWIbBRceGPkE30XBYe+HktFxuMWBR2eVQQ+Ur6yFJMoaYcNpyGq22uYJ9Wx4UAEcC0K+LNPQ==} peerDependencies: @@ -7068,8 +7189,8 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /react-datepicker@4.23.0(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-w+msqlOZ14v6H1UknTKtZw/dw9naFMgAOspf59eY130gWpvy5dvKj/bgsFICDdvxB7PtKWxDcbGlAqCloY1d2A==} + /react-datepicker@4.25.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-zB7CSi44SJ0sqo8hUQ3BF1saE/knn7u25qEMTO1CQGofY1VAKahO8k9drZtp0cfW1DMfoYLR3uSY1/uMvbEzbg==} peerDependencies: react: ^16.9.0 || ^17 || ^18 react-dom: ^16.9.0 || ^17 || ^18 @@ -7080,7 +7201,7 @@ packages: prop-types: 15.8.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-onclickoutside: 6.13.0(react-dom@18.2.0)(react@18.2.0) + react-onclickoutside: 6.13.1(react-dom@18.2.0)(react@18.2.0) react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0) dev: false @@ -7130,8 +7251,8 @@ packages: - csstype dev: false - /react-i18next@14.1.0(i18next@22.4.10)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-3KwX6LHpbvGQ+sBEntjV4sYW3Zovjjl3fpoHbUwSgFHf0uRBcbeCBLR5al6ikncI5+W0EFb71QXZmfop+J6NrQ==} + /react-i18next@14.1.2(i18next@22.5.1)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-FSIcJy6oauJbGEXfhUgVeLzvWBhIBIS+/9c6Lj4niwKZyGaGb4V4vUbATXSlsHJDXXB+ociNxqFNiFuV1gmoqg==} peerDependencies: i18next: '>= 23.2.3' react: '>= 16.8.0' @@ -7145,7 +7266,7 @@ packages: dependencies: '@babel/runtime': 7.24.1 html-parse-stringify: 3.0.1 - i18next: 22.4.10 + i18next: 22.5.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -7166,7 +7287,7 @@ packages: prop-types: ^15.8.1 react: '>=15.3.2 <=18' dependencies: - katex: 0.16.7 + katex: 0.16.10 prop-types: 15.8.1 react: 18.2.0 dev: false @@ -7181,7 +7302,7 @@ packages: react: '>0.13.0' react-dom: '>0.13.0' dependencies: - '@babel/runtime': 7.24.4 + '@babel/runtime': 7.24.1 get-node-dimensions: 1.2.1 prop-types: 15.8.1 react: 18.2.0 @@ -7189,8 +7310,8 @@ packages: 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==} + /react-onclickoutside@6.13.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-LdrrxK/Yh9zbBQdFbMTXPp3dTSN9B+9YJQucdDu3JNKRrbdU+H+/TVONJoWtOwy4II8Sqf1y/DTI6w/vGPYW0w==} peerDependencies: react: ^15.5.x || ^16.x || ^17.x || ^18.x react-dom: ^15.5.x || ^16.x || ^17.x || ^18.x @@ -7252,15 +7373,15 @@ packages: react-is: 17.0.2 dev: false - /react-redux@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): - resolution: {integrity: sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==} + /react-redux@8.1.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1): + resolution: {integrity: sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==} peerDependencies: '@types/react': ^16.8 || ^17.0 || ^18.0 '@types/react-dom': ^16.8 || ^17.0 || ^18.0 react: ^16.8 || ^17.0 || ^18.0 react-dom: ^16.8 || ^17.0 || ^18.0 react-native: '>=0.59' - redux: ^4 + redux: ^4 || ^5.0.0-beta.0 peerDependenciesMeta: '@types/react': optional: true @@ -7283,7 +7404,7 @@ packages: react-dom: 18.2.0(react@18.2.0) react-is: 18.2.0 redux: 4.2.1 - use-sync-external-store: 1.2.0(react@18.2.0) + use-sync-external-store: 1.2.2(react@18.2.0) dev: false /react-refresh@0.14.0: @@ -7291,26 +7412,26 @@ packages: engines: {node: '>=0.10.0'} dev: true - /react-router-dom@6.22.3(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw==} + /react-router-dom@6.23.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==} engines: {node: '>=14.0.0'} peerDependencies: react: '>=16.8' react-dom: '>=16.8' dependencies: - '@remix-run/router': 1.15.3 + '@remix-run/router': 1.16.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-router: 6.22.3(react@18.2.0) + react-router: 6.23.1(react@18.2.0) dev: false - /react-router@6.22.3(react@18.2.0): - resolution: {integrity: sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ==} + /react-router@6.23.1(react@18.2.0): + resolution: {integrity: sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==} engines: {node: '>=14.0.0'} peerDependencies: react: '>=16.8' dependencies: - '@remix-run/router': 1.15.3 + '@remix-run/router': 1.16.1 react: 18.2.0 dev: false @@ -7364,11 +7485,11 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /react-virtualized-auto-sizer@1.0.20(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-OdIyHwj4S4wyhbKHOKM1wLSj/UDXm839Z3Cvfg2a9j+He6yDa6i5p0qQvEiCnyQlGO/HyfSnigQwuxvYalaAXA==} + /react-virtualized-auto-sizer@1.0.24(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-3kCn7N9NEb3FlvJrSHWGQ4iVl+ydQObq2fHMn12i5wbtm74zHOPhz/i64OL3c1S1vi9i2GXtZqNqUJTQ+BnNfg==} peerDependencies: - react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc - react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc + react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 + react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 dependencies: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -7402,8 +7523,8 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /react18-input-otp@1.1.2(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-E21NiPh/KH67Bq/uEAm78E8H+croiGAyX5WcXfX49qh0im1iKrk/3RCKCTESG6WUoJYyh/fj5JY0UrHm+Mm0eQ==} + /react18-input-otp@1.1.4(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-35xvmTeuPWIxd0Z0Opx4z3OoMaTmKN4ubirQCx1YMZiNoe+2h1hsOSUco4aKPlGXWZCtXrfOFieAh46vqiK9mA==} peerDependencies: react: 16.2.0 - 18 react-dom: 16.2.0 - 18 @@ -7491,6 +7612,33 @@ packages: throttleit: 1.0.1 dev: true + /request@2.88.2: + resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==} + engines: {node: '>= 6'} + deprecated: request has been deprecated, see https://github.com/request/request/issues/3142 + dependencies: + aws-sign2: 0.7.0 + aws4: 1.12.0 + caseless: 0.12.0 + combined-stream: 1.0.8 + extend: 3.0.2 + forever-agent: 0.6.1 + form-data: 2.3.3 + har-validator: 5.1.5 + http-signature: 1.2.0 + is-typedarray: 1.0.0 + isstream: 0.1.2 + json-stringify-safe: 5.0.1 + mime-types: 2.1.35 + oauth-sign: 0.9.0 + performance-now: 2.1.0 + qs: 6.5.3 + safe-buffer: 5.1.2 + tough-cookie: 2.5.0 + tunnel-agent: 0.6.0 + uuid: 3.4.0 + dev: false + /require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -7499,8 +7647,8 @@ packages: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} dev: true - /reselect@5.0.1: - resolution: {integrity: sha512-D72j2ubjgHpvuCiORWkOUxndHJrxDaSolheiz5CO+roz8ka97/4msh2E8F5qay4GawR5vzBt5MkbDHT+Rdy/Wg==} + /reselect@5.1.0: + resolution: {integrity: sha512-aw7jcGLDpSgNDyWBQLv2cedml85qd95/iszJjN988zX1t7AVRJi19d9kto5+W7oCfQ94gyo40dVbT6g2k4/kXg==} dev: false /resize-observer-polyfill@1.5.1: @@ -7641,7 +7789,6 @@ packages: /safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - dev: true /safe-regex-test@1.0.3: resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} @@ -7654,15 +7801,14 @@ packages: /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - dev: true - /sass@1.70.0: - resolution: {integrity: sha512-uUxNQ3zAHeAx5nRFskBnrWzDUJrrvpCPD5FNAoRvTi0WwremlheES3tg+56PaVtCs5QDRX5CBLxxKMDJMEa1WQ==} + /sass@1.77.2: + resolution: {integrity: sha512-eb4GZt1C3avsX3heBNlrc7I09nyT00IUuo4eFhAbeXWU2fvA7oXI53SxODVAA+zgZCk9aunAZgO+losjR3fAwA==} engines: {node: '>=14.0.0'} hasBin: true dependencies: chokidar: 3.6.0 - immutable: 4.3.5 + immutable: 4.3.6 source-map-js: 1.2.0 /saxes@6.0.0: @@ -7770,17 +7916,17 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - /slate-history@0.100.0(slate@0.101.4): + /slate-history@0.100.0(slate@0.101.5): resolution: {integrity: sha512-x5rUuWLNtH97hs9PrFovGgt3Qc5zkTm/5mcUB+0NR/TK923eLax4HsL6xACLHMs245nI6aJElyM1y6hN0y5W/Q==} peerDependencies: slate: '>=0.65.3' dependencies: is-plain-object: 5.0.0 - slate: 0.101.4 + slate: 0.101.5 dev: false - /slate-react@0.101.3(react-dom@18.2.0)(react@18.2.0)(slate@0.101.4): - resolution: {integrity: sha512-KMXK9FLeS7HYhhoVcI8SUi4Qp1I9C1lTQ2EgbPH95sVXfH/vq+hbhurEGIGCe0VQ9Opj4rSKJIv/g7De1+nJMA==} + /slate-react@0.101.6(react-dom@18.2.0)(react@18.2.0)(slate@0.101.5): + resolution: {integrity: sha512-aMtp9FY127hKWTkCcTBonfKIwKJC2ESPqFdw2o/RuOk3RMQRwsWay8XTOHx8OBGOHanI2fsKaTAPF5zxOLA1Qg==} peerDependencies: react: '>=18.2.0' react-dom: '>=18.2.0' @@ -7796,14 +7942,14 @@ packages: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) scroll-into-view-if-needed: 3.1.0 - slate: 0.101.4 + slate: 0.101.5 tiny-invariant: 1.3.1 dev: false - /slate@0.101.4: - resolution: {integrity: sha512-8LazZrNDsYFKDg1wpb0HouAfX5Pw/UmOZ/vIrtqD2GSCDZvraOkV2nVJ9Ery8kIlsU1jeybwgcaCy4KkVwfvEg==} + /slate@0.101.5: + resolution: {integrity: sha512-ZZt1ia8ayRqxtpILRMi2a4MfdvwdTu64CorxTVq9vNSd0GQ/t3YDkze6wKjdeUtENmBlq5wNIDInZbx38Hfu5Q==} dependencies: - immer: 10.0.4 + immer: 10.1.1 is-plain-object: 5.0.0 tiny-warning: 1.0.3 dev: false @@ -7881,7 +8027,6 @@ packages: jsbn: 0.1.1 safer-buffer: 2.1.2 tweetnacl: 0.14.5 - dev: true /stack-utils@2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} @@ -8143,7 +8288,6 @@ packages: /through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - dev: true /tiny-invariant@1.3.1: resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} @@ -8199,6 +8343,14 @@ packages: engines: {node: '>=6'} dev: true + /tough-cookie@2.5.0: + resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} + engines: {node: '>=0.8'} + dependencies: + psl: 1.9.0 + punycode: 2.3.1 + dev: false + /tough-cookie@4.1.3: resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} engines: {node: '>=6'} @@ -8348,11 +8500,9 @@ packages: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} dependencies: safe-buffer: 5.1.2 - dev: true /tweetnacl@0.14.5: resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} - dev: true /type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} @@ -8452,6 +8602,10 @@ packages: /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + /uniq@1.0.1: + resolution: {integrity: sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA==} + dev: false + /universalify@0.2.0: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} @@ -8498,7 +8652,6 @@ packages: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} dependencies: punycode: 2.3.1 - dev: true /url-parse@1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} @@ -8515,8 +8668,8 @@ packages: react: 18.2.0 dev: false - /use-sync-external-store@1.2.0(react@18.2.0): - resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} + /use-sync-external-store@1.2.2(react@18.2.0): + resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: @@ -8531,6 +8684,12 @@ packages: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: true + /uuid@3.4.0: + resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} + deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. + hasBin: true + dev: false + /uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -8553,30 +8712,11 @@ packages: '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 - /validator@13.11.0: - resolution: {integrity: sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==} + /validator@13.12.0: + resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} engines: {node: '>= 0.10'} dev: false - /valtio@1.12.1(@types/react@18.2.66)(react@18.2.0): - resolution: {integrity: sha512-R0V4H86Xi2Pp7pmxN/EtV4Q6jr6PMN3t1IwxEvKUp6160r8FimvPh941oWyeK1iec/DTsh9Jb3Q+GputMS8SYg==} - engines: {node: '>=12.20.0'} - peerDependencies: - '@types/react': '>=16.8' - react: '>=16.8' - peerDependenciesMeta: - '@types/react': - optional: true - react: - optional: true - dependencies: - '@types/react': 18.2.66 - derive-valtio: 0.1.0(valtio@1.12.1) - proxy-compare: 2.5.1 - react: 18.2.0 - use-sync-external-store: 1.2.0(react@18.2.0) - dev: false - /verror@1.10.0: resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} engines: {'0': node >=0.6.0} @@ -8584,7 +8724,6 @@ packages: assert-plus: 1.0.0 core-util-is: 1.0.2 extsprintf: 1.3.0 - dev: true /vite-plugin-compression2@1.0.0: resolution: {integrity: sha512-42XNp6FjxE0JIecxj1fdi770pLhYm3MJhBUAod9EszTgDg9C4LDOgBzWcj/0K52KfJrpRXwUsWV6kqTDuoCfLA==} @@ -8614,7 +8753,7 @@ packages: '@rollup/pluginutils': 5.1.0 '@svgr/core': 7.0.0(typescript@4.9.5) '@svgr/plugin-jsx': 7.0.0 - vite: 5.2.0(@types/node@20.11.30)(sass@1.70.0) + vite: 5.2.0(@types/node@20.11.30)(sass@1.77.2) transitivePeerDependencies: - rollup - supports-color @@ -8632,7 +8771,7 @@ packages: kolorist: 1.8.0 sirv: 2.0.4 ufo: 1.5.3 - vite: 5.2.0(@types/node@20.11.30)(sass@1.70.0) + vite: 5.2.0(@types/node@20.11.30)(sass@1.77.2) transitivePeerDependencies: - rollup - supports-color @@ -8644,7 +8783,7 @@ packages: vite: '>=5.0.0' dependencies: chalk: 5.3.0 - vite: 5.2.0(@types/node@20.11.30)(sass@1.70.0) + vite: 5.2.0(@types/node@20.11.30)(sass@1.77.2) dev: true /vite-plugin-wasm@3.3.0(vite@5.2.0): @@ -8652,10 +8791,10 @@ packages: peerDependencies: vite: ^2 || ^3 || ^4 || ^5 dependencies: - vite: 5.2.0(@types/node@20.11.30)(sass@1.70.0) + vite: 5.2.0(@types/node@20.11.30)(sass@1.77.2) dev: false - /vite@5.2.0(@types/node@20.11.30)(sass@1.70.0): + /vite@5.2.0(@types/node@20.11.30)(sass@1.77.2): resolution: {integrity: sha512-xMSLJNEjNk/3DJRgWlPADDwaU9AgYRodDH2t6oENhJnIlmU9Hx1Q6VpjyXua/JdMw1WJRbnAgHJ9xgET9gnIAg==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -8687,7 +8826,7 @@ packages: esbuild: 0.20.2 postcss: 8.4.38 rollup: 4.13.2 - sass: 1.70.0 + sass: 1.77.2 optionalDependencies: fsevents: 2.3.3 @@ -8829,24 +8968,24 @@ packages: engines: {node: '>=0.4'} dev: true - /y-indexeddb@9.0.12(yjs@13.6.14): + /y-indexeddb@9.0.12(yjs@13.6.15): resolution: {integrity: sha512-9oCFRSPPzBK7/w5vOkJBaVCQZKHXB/v6SIT+WYhnJxlEC61juqG0hBrAf+y3gmSMLFLwICNH9nQ53uscuse6Hg==} engines: {node: '>=16.0.0', npm: '>=8.0.0'} peerDependencies: yjs: ^13.0.0 dependencies: - lib0: 0.2.93 - yjs: 13.6.14 + lib0: 0.2.94 + yjs: 13.6.15 dev: false - /y-protocols@1.0.6(yjs@13.6.14): + /y-protocols@1.0.6(yjs@13.6.15): resolution: {integrity: sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==} engines: {node: '>=16.0.0', npm: '>=8.0.0'} peerDependencies: yjs: ^13.0.0 dependencies: - lib0: 0.2.93 - yjs: 13.6.14 + lib0: 0.2.94 + yjs: 13.6.15 dev: false /y18n@5.0.8: @@ -8886,11 +9025,11 @@ packages: fd-slicer: 1.1.0 dev: true - /yjs@13.6.14: - resolution: {integrity: sha512-D+7KcUr0j+vBCUSKXXEWfA+bG4UQBviAwP3gYBhkstkgwy5+8diOPMx0iqLIOxNo/HxaREUimZRxqHGAHCL2BQ==} + /yjs@13.6.15: + resolution: {integrity: sha512-moFv4uNYhp8BFxIk3AkpoAnnjts7gwdpiG8RtyFiKbMtxKCS0zVZ5wPaaGpwC3V2N/K8TK8MwtSI3+WO9CHWjQ==} engines: {node: '>=16.0.0', npm: '>=8.0.0'} dependencies: - lib0: 0.2.93 + lib0: 0.2.94 dev: false /yn@3.1.1: diff --git a/frontend/appflowy_web_app/scripts/generateTailwindColors.cjs b/frontend/appflowy_web_app/scripts/generateTailwindColors.cjs new file mode 100644 index 0000000000000..83f5bb25d5114 --- /dev/null +++ b/frontend/appflowy_web_app/scripts/generateTailwindColors.cjs @@ -0,0 +1,61 @@ +const fs = require('fs'); +const path = require('path'); + +// Read CSS file +const cssFilePath = path.join(__dirname, '../src/styles/variables/light.variables.css'); +const cssContent = fs.readFileSync(cssFilePath, 'utf-8'); + +// Extract color variables +const shadowVariables = cssContent.match(/--shadow:\s.*;/g); +const colorVariables = cssContent.match(/--[\w-]+:\s*#[0-9a-fA-F]{6}/g); + +if (!colorVariables) { + console.error('No color variables found in CSS file.'); + process.exit(1); +} + +const shadows = shadowVariables.reduce((shadows, variable) => { + const [name, value] = variable.split(':').map(str => str.trim()); + const formattedName = name.replace('--', '').replace(/-/g, '_'); + const key = 'md'; + + shadows[key] = `var(${name})`; + return shadows; +}, {}); +// Generate Tailwind CSS colors configuration +// Replace -- with _ and - with _ in color variable names +const tailwindColors = colorVariables.reduce((colors, variable) => { + const [name, value] = variable.split(':').map(str => str.trim()); + const formattedName = name.replace('--', '').replace(/-/g, '_'); + const category = formattedName.split('_')[0]; + const key = formattedName.replace(`${category}_`, ''); + + if (!colors[category]) { + colors[category] = {}; + } + colors[category][key] = `var(${name})`; + return colors; +}, {}); + +const tailwindColorsFormatted = JSON.stringify(tailwindColors, null, 2) + .replace(/_/g, '-'); +const header = `/**\n` + '* Do not edit directly\n' + `* Generated on ${new Date().toUTCString()}\n` + `* Generated from $pnpm css:variables \n` + `*/\n\n`; + +// Write Tailwind CSS colors configuration to file +const tailwindColorTemplate = ` +${header} +module.exports = ${tailwindColorsFormatted}; +`; + +const tailwindShadowTemplate = ` +${header} +module.exports = ${JSON.stringify(shadows, null, 2).replace(/_/g, '-')}; +`; + +const tailwindConfigFilePath = path.join(__dirname, '../tailwind/colors.cjs'); +fs.writeFileSync(tailwindConfigFilePath, tailwindColorTemplate, 'utf-8'); + +const tailwindShadowFilePath = path.join(__dirname, '../tailwind/box-shadow.cjs'); +fs.writeFileSync(tailwindShadowFilePath, tailwindShadowTemplate, 'utf-8'); + +console.log('Tailwind CSS colors configuration generated successfully.'); diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.toml b/frontend/appflowy_web_app/src-tauri/Cargo.toml index 45d822b46ed08..54e09acf6c172 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 diff --git a/frontend/appflowy_web_app/src-tauri/src/init.rs b/frontend/appflowy_web_app/src-tauri/src/init.rs index 923ab0ff8fe73..42c857abdf33b 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 9a2bcfe1861b1..4f61246aabe1c 100644 --- a/frontend/appflowy_web_app/src/application/collab.type.ts +++ b/frontend/appflowy_web_app/src/application/collab.type.ts @@ -27,6 +27,8 @@ export enum BlockType { DividerBlock = 'divider', ImageBlock = 'image', GridBlock = 'grid', + BoardBlock = 'board', + CalendarBlock = 'calendar', OutlineBlock = 'outline', TableBlock = 'table', TableCell = 'table/cell', @@ -111,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', @@ -132,15 +138,15 @@ export interface FolderMeta { current_workspace: string; } -export enum CoverType { +export enum DocCoverType { Color = 'CoverType.color', Image = 'CoverType.file', Asset = 'CoverType.asset', } -export type PageCover = { +export type DocCover = { image_type?: ImageType; - cover_selection_type?: CoverType; + cover_selection_type?: DocCoverType; cover_selection?: string; } | null; @@ -160,6 +166,7 @@ export enum YjsEditorKey { // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values database_row = 'data', user_awareness = 'user_awareness', + empty = 'empty', // document blocks = 'blocks', @@ -193,6 +200,10 @@ export enum YjsFolderKey { id = 'id', name = 'name', icon = 'icon', + extra = 'extra', + cover = 'cover', + line_height_layout = 'line_height_layout', + font_layout = 'font_layout', type = 'ty', value = 'value', layout = 'layout', @@ -241,6 +252,13 @@ export enum YjsDatabaseKey { 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 { @@ -324,7 +342,7 @@ export interface YView extends Y.Map { get(key: YjsFolderKey.name): string; // eslint-disable-next-line @typescript-eslint/unified-signatures - get(key: YjsFolderKey.icon): string; + get(key: YjsFolderKey.icon | YjsFolderKey.extra): string; // eslint-disable-next-line @typescript-eslint/unified-signatures get(key: YjsFolderKey.layout): string; @@ -425,18 +443,55 @@ export type YDatabaseFieldOrders = Y.Array; // [ { id: FieldId } ] export type YDatabaseRowOrders = Y.Array; // [ { id: RowId, height: number } ] -export type YDatabaseGroups = Y.Array; +export type YDatabaseGroups = Y.Array; export type YDatabaseFilters = Y.Array; export type YDatabaseSorts = Y.Array; -export type YDatabaseLayoutSettings = Y.Map; - 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; @@ -557,3 +612,15 @@ export const databaseLayoutMap = { [DatabaseViewLayout.Board]: 'board', [DatabaseViewLayout.Calendar]: 'calendar', }; + +export enum FontLayout { + small = 'small', + normal = 'normal', + large = 'large', +} + +export enum LineHeightLayout { + small = 'small', + normal = 'normal', + large = 'large', +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/const.ts b/frontend/appflowy_web_app/src/application/database-yjs/const.ts index b082acc6a4b42..b8ebe46a3a9b8 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/const.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/const.ts @@ -1,2 +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 index 8717aa0ffe9d8..8e7aa4b867430 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/context.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/context.ts @@ -1,34 +1,51 @@ import { YDatabase, YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; -import { filterBy } from '@/application/database-yjs/filter'; import { Row } from '@/application/database-yjs/selector'; -import { sortBy } from '@/application/database-yjs/sort'; -import { createContext, useContext, useEffect, useState } from 'react'; +import { createContext, useContext } from 'react'; import * as Y from 'yjs'; -import debounce from 'lodash-es/debounce'; export interface DatabaseContextState { readOnly: boolean; - doc: YDoc; + databaseDoc: YDoc; viewId: string; rowDocMap: Y.Map; + isDatabaseRowPage?: boolean; + navigateToRow?: (rowId: string) => void; } export const DatabaseContext = createContext(null); export const useDatabase = () => { const database = useContext(DatabaseContext) - ?.doc?.getMap(YjsEditorKey.data_section) + ?.databaseDoc?.getMap(YjsEditorKey.data_section) .get(YjsEditorKey.database) as YDatabase; return database; }; -export const useRowMeta = (rowId: string) => { - const rows = useContext(DatabaseContext)?.rowDocMap; - const rowMetaDoc = rows?.get(rowId); - const rowMeta = rowMetaDoc?.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow; +export function useDatabaseViewId() { + return useContext(DatabaseContext)?.viewId; +} + +export const useNavigateToRow = () => { + return useContext(DatabaseContext)?.navigateToRow; +}; + +export const useRowDocMap = () => { + return useContext(DatabaseContext)?.rowDocMap; +}; + +export const useIsDatabaseRowPage = () => { + return useContext(DatabaseContext)?.isDatabaseRowPage; +}; + +export const useRow = (rowId: string) => { + const rows = useRowDocMap(); + + return rows?.get(rowId)?.getMap(YjsEditorKey.data_section); +}; - return rowMeta; +export const useRowData = (rowId: string) => { + return useRow(rowId)?.get(YjsEditorKey.database_row) as YDatabaseRow; }; export const useViewId = () => { @@ -56,72 +73,16 @@ export function useDatabaseFields() { return database.get(YjsDatabaseKey.fields); } -export interface GridRowsState { +export interface RowsState { rowOrders: Row[]; } -export const GridRowsContext = createContext(null); - -export function useGridRowsContext() { - return useContext(GridRowsContext); -} +export const RowsContext = createContext(null); -export function useGridRows() { - return useGridRowsContext()?.rowOrders; +export function useRowsContext() { + return useContext(RowsContext); } -export function useGridRowOrders() { - 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; - - console.log('sort or filter changed'); - 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 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 index f5d4aeac61a52..c8ac7da5b0064 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/database.type.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/database.type.ts @@ -49,3 +49,24 @@ export interface Filter { 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/checklist/parse.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/parse.ts index 6dd14c71e0ef0..c93fee7a3815f 100644 --- 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 @@ -9,7 +9,7 @@ export interface ChecklistCellData { export function parseChecklistData(data: string): ChecklistCellData | null { try { const { options, selected_option_ids } = JSON.parse(data); - const percentage = (selected_option_ids.length / options.length) * 100; + const percentage = selected_option_ids.length / options.length; return { percentage, diff --git a/frontend/appflowy_web_app/src/application/database-yjs/filter.ts b/frontend/appflowy_web_app/src/application/database-yjs/filter.ts index 73a8663371721..5f0919c4d98ef 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/filter.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/filter.ts @@ -181,10 +181,10 @@ export function checklistFilterCheck(data: string, content: string, condition: n const percentage = parseChecklistData(data)?.percentage ?? 0; if (condition === ChecklistFilterCondition.IsComplete) { - return percentage === 100; + return percentage === 1; } - return percentage !== 100; + return percentage !== 1; } export function selectOptionFilterCheck(data: string, content: string, condition: number) { 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 0000000000000..709053aa32cd5 --- /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 index 708ae080d2c02..1d5aa0ce3deb8 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/index.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/index.ts @@ -4,5 +4,3 @@ export * from './context'; export * from './selector'; export * from './database.type'; export * from './const'; -export * from './filter'; -export * from './sort'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/selector.ts b/frontend/appflowy_web_app/src/application/database-yjs/selector.ts index c3222fdf654ed..697cd8560ee15 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/selector.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/selector.ts @@ -1,9 +1,34 @@ -import { FieldId, SortId, YDatabaseField, YjsDatabaseKey } from '@/application/collab.type'; -import { MIN_COLUMN_WIDTH } from '@/application/database-yjs/const'; -import { useDatabase, useGridRows, useViewId } from '@/application/database-yjs/context'; -import { parseFilter } from '@/application/database-yjs/filter'; -import { FieldType, FieldVisibility, Filter, SortCondition } from './database.type'; -import { useEffect, useMemo, useState } from 'react'; +import { + FieldId, + SortId, + YDatabaseField, + YDoc, + YjsDatabaseKey, + YjsEditorKey, + YjsFolderKey, +} from '@/application/collab.type'; +import { getCell, metaIdFromRowId, MIN_COLUMN_WIDTH } from '@/application/database-yjs/const'; +import { + useDatabase, + useDatabaseFields, + useDatabaseView, + useIsDatabaseRowPage, + useRowDocMap, + 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 { throttle } from 'lodash-es'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import Y from 'yjs'; +import { CalendarLayoutSetting, FieldType, FieldVisibility, Filter, RowMetaKey, SortCondition } from './database.type'; export interface Column { fieldId: string; @@ -19,11 +44,53 @@ export interface Row { const defaultVisible = [FieldVisibility.AlwaysShown, FieldVisibility.HideWhenEmpty]; -export function useGridColumnsSelector(viewId: string, visibilitys: FieldVisibility[] = defaultVisible) { +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); @@ -39,11 +106,15 @@ export function useGridColumnsSelector(viewId: string, visibilitys: FieldVisibil return { fieldId, width: parseInt(setting?.get(YjsDatabaseKey.width)) || MIN_COLUMN_WIDTH, - visibility: parseInt(setting?.get(YjsDatabaseKey.visibility)) as FieldVisibility, - wrap: setting?.get(YjsDatabaseKey.wrap), + visibility: Number( + setting?.get(YjsDatabaseKey.visibility) || FieldVisibility.AlwaysShown + ) as FieldVisibility, + wrap: setting?.get(YjsDatabaseKey.wrap) ?? true, }; }) - .filter((column) => visibilitys.includes(column.visibility)); + .filter((column) => { + return visibilitys.includes(column.visibility); + }); }; const observerEvent = () => setColumns(getColumns()); @@ -62,8 +133,8 @@ export function useGridColumnsSelector(viewId: string, visibilitys: FieldVisibil return columns; } -export function useGridRowsSelector() { - const rowOrders = useGridRows(); +export function useRowsSelector() { + const rowOrders = useRows(); return useMemo(() => rowOrders ?? [], [rowOrders]); } @@ -81,10 +152,10 @@ export function useFieldSelector(fieldId: string) { setField(field || null); const observerEvent = () => setClock((prev) => prev + 1); - field.observe(observerEvent); + field?.observe(observerEvent); return () => { - field.unobserve(observerEvent); + field?.unobserve(observerEvent); }; }, [database, fieldId]); @@ -225,3 +296,467 @@ export function useSortSelector(sortId: SortId) { 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 = useRowDocMap(); + 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 = () => { + if (rows.size !== rowOrders?.length) return; + + 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(); + + fields.observeDeep(onConditionsChange); + return () => { + fields.unobserveDeep(onConditionsChange); + }; + }, [fieldId, fields, rowOrders, rows]); + + const visibleColumns = columns.filter((column) => column.visible); + + return { + fieldId, + groupResult, + columns: visibleColumns, + notFound, + }; +} + +export function useRowOrdersSelector() { + const isDatabaseRowPage = useIsDatabaseRowPage(); + const { rows, clock } = useRowDocMapSelector(); + const [rowOrders, setRowOrders] = useState(); + const view = useDatabaseView(); + const sorts = view?.get(YjsDatabaseKey.sorts); + const fields = useDatabaseFields(); + const filters = view?.get(YjsDatabaseKey.filters); + const onConditionsChange = useCallback(() => { + const originalRowOrders = view?.get(YjsDatabaseKey.row_orders).toJSON(); + + if (!originalRowOrders || !rows) return; + + if (originalRowOrders.length !== rows.size && !isDatabaseRowPage) 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); + } + }, [fields, filters, rows, sorts, view, isDatabaseRowPage]); + + useEffect(() => { + onConditionsChange(); + }, [onConditionsChange, clock]); + + useEffect(() => { + const throttleChange = throttle(onConditionsChange, 200); + + sorts?.observeDeep(throttleChange); + filters?.observeDeep(throttleChange); + fields?.observeDeep(throttleChange); + + return () => { + sorts?.unobserveDeep(throttleChange); + filters?.unobserveDeep(throttleChange); + fields?.unobserveDeep(throttleChange); + }; + }, [onConditionsChange, fields, filters, sorts]); + + return rowOrders; +} + +export function useRowDocMapSelector() { + const rowMap = useRowDocMap(); + const [clock, setClock] = useState(0); + + useEffect(() => { + if (!rowMap) return; + const observerEvent = () => setClock((prev) => prev + 1); + + const rowIds = Array.from(rowMap?.keys() || []); + + rowMap.observe(observerEvent); + + const observers = rowIds.map((rowId) => { + return observeDeepRow(rowId, rowMap, observerEvent); + }); + + return () => { + rowMap.unobserve(observerEvent); + observers.forEach((observer) => observer()); + }; + }, [rowMap]); + + return { + rows: rowMap, + clock, + }; +} + +export function observeDeepRow( + rowId: string, + rowMap: Y.Map, + observerEvent: () => void, + key: YjsEditorKey.meta | YjsEditorKey.database_row = YjsEditorKey.database_row +) { + const rowSharedRoot = rowMap?.get(rowId)?.getMap(YjsEditorKey.data_section); + const row = rowSharedRoot?.get(key); + + rowSharedRoot?.observe(observerEvent); + row?.observeDeep(observerEvent); + return () => { + rowSharedRoot?.unobserve(observerEvent); + row?.unobserveDeep(observerEvent); + }; +} + +export function useRowDataSelector(rowId: string) { + const rowMap = useRowDocMap(); + const rowSharedRoot = rowMap?.get(rowId)?.getMap(YjsEditorKey.data_section); + const row = rowSharedRoot?.get(YjsEditorKey.database_row); + + const [clock, setClock] = useState(0); + + useEffect(() => { + if (!rowMap) return; + const onChange = () => { + setClock((prev) => prev + 1); + }; + + const observer = observeDeepRow(rowId, rowMap, onChange); + + rowMap.observe(onChange); + + return () => { + rowMap.unobserve(onChange); + observer(); + }; + }, [rowId, rowMap]); + + return { + row, + clock, + }; +} + +export function useCellSelector({ rowId, fieldId }: { rowId: string; fieldId: string }) { + const { row } = useRowDataSelector(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 = useRowDocMap(); + 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; +} + +const metaIdMapFromRowIdMap = new Map>(); + +function getMetaIdMap(rowId: string) { + const hasMetaIdMap = metaIdMapFromRowIdMap.has(rowId); + + if (!hasMetaIdMap) { + const parser = metaIdFromRowId(rowId); + const map = new Map(); + + map.set(RowMetaKey.IconId, parser(RowMetaKey.IconId)); + map.set(RowMetaKey.CoverId, parser(RowMetaKey.CoverId)); + map.set(RowMetaKey.DocumentId, parser(RowMetaKey.DocumentId)); + map.set(RowMetaKey.IsDocumentEmpty, parser(RowMetaKey.IsDocumentEmpty)); + metaIdMapFromRowIdMap.set(rowId, map); + return map; + } + + return metaIdMapFromRowIdMap.get(rowId) as Map; +} + +export const useRowMetaSelector = (rowId: string) => { + const [meta, setMeta] = useState(); + const rowMap = useRowDocMap(); + + const updateMeta = useCallback(() => { + const metaKeyMap = getMetaIdMap(rowId); + + const iconKey = metaKeyMap.get(RowMetaKey.IconId) ?? ''; + const coverKey = metaKeyMap.get(RowMetaKey.CoverId) ?? ''; + const documentId = metaKeyMap.get(RowMetaKey.DocumentId) ?? ''; + const isEmptyDocumentKey = metaKeyMap.get(RowMetaKey.IsDocumentEmpty) ?? ''; + const rowSharedRoot = rowMap?.get(rowId)?.getMap(YjsEditorKey.data_section); + const yMeta = rowSharedRoot?.get(YjsEditorKey.meta); + + if (!yMeta) return; + const metaJson = yMeta.toJSON(); + + const icon = metaJson[iconKey]; + const cover = metaJson[coverKey]; + const isEmptyDocument = metaJson[isEmptyDocumentKey]; + + setMeta({ + icon, + cover, + documentId, + isEmptyDocument, + }); + }, [rowId, rowMap]); + + useEffect(() => { + if (!rowMap) return; + updateMeta(); + const observer = observeDeepRow(rowId, rowMap, updateMeta, YjsEditorKey.meta); + + rowMap.observe(updateMeta); + + return () => { + rowMap.unobserve(updateMeta); + observer(); + }; + }, [rowId, rowMap, updateMeta]); + + 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 index 355d4b4ad93a8..f229ce8f74e31 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/sort.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/sort.ts @@ -62,6 +62,7 @@ export function parseCellDataForSort(field: YDatabaseField, data: string | boole switch (fieldType) { case FieldType.RichText: case FieldType.URL: + return data ? data : '\uFFFF'; case FieldType.Number: return data; case FieldType.Checkbox: diff --git a/frontend/appflowy_web_app/src/application/folder-yjs/context.ts b/frontend/appflowy_web_app/src/application/folder-yjs/context.ts index 57c4d171df436..02a2cfcba46e2 100644 --- a/frontend/appflowy_web_app/src/application/folder-yjs/context.ts +++ b/frontend/appflowy_web_app/src/application/folder-yjs/context.ts @@ -1,8 +1,49 @@ -import { YFolder } from '@/application/collab.type'; -import { createContext, useContext } from 'react'; +import { ViewLayout, YFolder, YjsFolderKey } from '@/application/collab.type'; +import { createContext, useCallback, useContext } from 'react'; +import { useParams } from 'react-router-dom'; -export const FolderContext = createContext(null); +export interface Crumb { + viewId: string; + rowId?: string; + name: string; + icon: string; +} + +export const FolderContext = createContext<{ + folder: YFolder | null; + onNavigateToView?: (viewId: string) => void; + crumbs?: Crumb[]; + setCrumbs?: React.Dispatch>; +} | null>(null); export const useFolderContext = () => { - return useContext(FolderContext); + return useContext(FolderContext)?.folder; +}; + +export const useViewLayout = () => { + const folder = useFolderContext(); + const { objectId } = useParams(); + const views = folder?.get(YjsFolderKey.views); + const view = objectId ? views?.get(objectId) : null; + + return Number(view?.get(YjsFolderKey.layout)) as ViewLayout; +}; + +export const useNavigateToView = () => { + return useContext(FolderContext)?.onNavigateToView; +}; + +export const useCrumbs = () => { + return useContext(FolderContext)?.crumbs; +}; + +export const usePushCrumb = () => { + const { setCrumbs } = useContext(FolderContext) || {}; + + return useCallback( + (crumb: Crumb) => { + setCrumbs?.((prevCrumbs) => [...prevCrumbs, crumb]); + }, + [setCrumbs] + ); }; diff --git a/frontend/appflowy_web_app/src/application/folder-yjs/folder.type.ts b/frontend/appflowy_web_app/src/application/folder-yjs/folder.type.ts new file mode 100644 index 0000000000000..2a3bd0a2e7e11 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/folder-yjs/folder.type.ts @@ -0,0 +1,8 @@ +export enum CoverType { + NormalColor = 'color', + GradientColor = 'gradient', + BuildInImage = 'none', + CustomImage = 'custom', + LocalImage = 'local', + UpsplashImage = 'unsplash', +} 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 295315874b97c..7ef758ba5eeb6 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, }; } @@ -48,10 +54,10 @@ export function useViewSelector(viewId: string) { setView(view || null); const observerEvent = () => setClock((prev) => prev + 1); - view.observe(observerEvent); + view?.observe(observerEvent); return () => { - view.unobserve(observerEvent); + view?.unobserve(observerEvent); }; }, [folder, viewId]); 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 index a1bfcdbf21752..a105df001539f 100644 --- 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 @@ -11,20 +11,34 @@ import * as Y from 'yjs'; export class JSDatabaseService implements DatabaseService { private loadedDatabaseId: Set = new Set(); + private cacheDatabaseRowDocMap: Map = new Map(); + constructor() { // } async getDatabase( workspaceId: string, - databaseId: 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); + + const rootRowsDoc = + this.cacheDatabaseRowDocMap.get(databaseId) ?? + new Y.Doc({ + guid: databaseId, + }); + + if (!this.cacheDatabaseRowDocMap.has(databaseId)) { + this.cacheDatabaseRowDocMap.set(databaseId, rootRowsDoc); + } + + const rowsFolder: Y.Map = rootRowsDoc.getMap(); + let databaseDoc: YDoc | undefined = undefined; if (isLoaded) { @@ -36,42 +50,66 @@ export class JSDatabaseService implements DatabaseService { 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 rowIds = rowOrders.toJSON() as { + const rowOrdersIds = rowOrders.toJSON() as { id: string; }[]; - if (!rowIds) { + if (!rowOrdersIds) { throw new Error('Database rows not found'); } + const ids = rowIds ? rowIds : rowOrdersIds.map((item) => item.id); + if (isLoaded) { - for (const row of rowIds) { - const { doc } = await getCollabStorage(row.id, CollabType.DatabaseRow); + for (const id of ids) { + const { doc } = await getCollabStorage(id, CollabType.DatabaseRow); - rowsFolder.set(row.id, doc); + if (!rowsFolder.has(id)) { + rowsFolder.set(id, doc); + } } } else { - const rows = await this.loadDatabaseRows( - workspaceId, - rowIds.map((item) => item.id) - ); - - rows.forEach((row, id) => { - rowsFolder.set(id, row); + void this.loadDatabaseRows(workspaceId, ids, (id, row) => { + if (!rowsFolder.has(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), + (rowId: string, rowDoc) => { + if (!rowsFolder.has(rowId)) { + rowsFolder.set(rowId, rowDoc); + } + } + ); + } + }); + } + return { databaseDoc, - rows: rowsFolder as Y.Map, + rows: rowsFolder, }; } async openDatabase( workspaceId: string, - viewId: string + viewId: string, + rowIds?: string[] ): Promise<{ databaseDoc: YDoc; rows: Y.Map; @@ -112,28 +150,8 @@ export class JSDatabaseService implements DatabaseService { throw new Error('Database not found'); } - const { databaseDoc, rows } = await this.getDatabase(workspaceId, databaseMeta.database_id); - const database = databaseDoc.getMap(YjsEditorKey.data_section)?.get(YjsEditorKey.database) as YDatabase; - const rowOrders = database.get(YjsDatabaseKey.views)?.get(viewId)?.get(YjsDatabaseKey.row_orders); + const { databaseDoc, rows } = await this.getDatabase(workspaceId, databaseMeta.database_id, 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) => { - rows.set(id, row); - }); - }); - } - }); const handleUpdate = (update: Uint8Array, origin: CollabOrigin) => { if (origin === CollabOrigin.LocalSync) { // Send the update to the server @@ -142,6 +160,7 @@ export class JSDatabaseService implements DatabaseService { }; databaseDoc.on('update', handleUpdate); + console.log('Database loaded', rows.toJSON()); return { databaseDoc, @@ -149,9 +168,7 @@ export class JSDatabaseService implements DatabaseService { }; } - async loadDatabaseRows(workspaceId: string, rowIds: string[]) { - const rows = new Map(); - + async loadDatabaseRows(workspaceId: string, rowIds: string[], rowCallback: (rowId: string, rowDoc: YDoc) => void) { try { await batchCollabs( workspaceId, @@ -159,12 +176,14 @@ export class JSDatabaseService implements DatabaseService { object_id: id, collab_type: CollabType.DatabaseRow, })), - (id, rowDoc) => rows.set(id, rowDoc) + rowCallback ); } catch (e) { console.error(e); } + } - return rows; + async closeDatabase(databaseId: string) { + this.cacheDatabaseRowDocMap.delete(databaseId); } } 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 bf5f0c7aa1c55..f17ac5053128a 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 @@ -10,6 +10,7 @@ import * as Y from 'yjs'; export async function openCollabDB(docName: string): Promise { const name = `${databasePrefix}_${docName}`; const doc = new Y.Doc(); + const provider = new IndexeddbPersistence(name, doc); let resolve: (value: unknown) => void; @@ -26,14 +27,6 @@ export async function openCollabDB(docName: string): Promise { return doc as YDoc; } -export async function deleteCollabDB(docName: string) { - const name = `${databasePrefix}_${docName}`; - const doc = new Y.Doc(); - const provider = new IndexeddbPersistence(name, doc); - - await provider.destroy(); -} - export function getDBName(id: string, type: string) { const { uuid } = getAuthInfo() || {}; 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 index 27ce771d74448..bdf57518c0e24 100644 --- 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 @@ -1,7 +1,7 @@ -import { CollabType, YDoc, YjsEditorKey } from '@/application/collab.type'; +import { CollabType, YDoc, YjsEditorKey, YSharedRoot } from '@/application/collab.type'; import { getDBName, openCollabDB } from '@/application/services/js-services/db'; import { APIService } from '@/application/services/js-services/wasm'; -import { applyDocument } from '@/application/ydoc/apply'; +import { applyYDoc } from '@/application/ydoc/apply'; export function fetchCollab(workspaceId: string, id: string, type: CollabType) { return APIService.getCollab(workspaceId, id, type); @@ -30,11 +30,28 @@ function collabTypeToDBType(type: CollabType) { } } +const collabSharedRootKeyMap = { + [CollabType.Folder]: YjsEditorKey.folder, + [CollabType.Document]: YjsEditorKey.document, + [CollabType.Database]: YjsEditorKey.database, + [CollabType.WorkspaceDatabase]: YjsEditorKey.workspace_database, + [CollabType.DatabaseRow]: YjsEditorKey.database_row, + [CollabType.UserAwareness]: YjsEditorKey.user_awareness, + [CollabType.Empty]: YjsEditorKey.empty, +}; + 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); + let localExist = false; + const existData = doc.share.has(YjsEditorKey.data_section); + + if (existData) { + const data = doc.getMap(YjsEditorKey.data_section) as YSharedRoot; + + localExist = data.has(collabSharedRootKeyMap[type] as string); + } return { doc, @@ -47,7 +64,7 @@ export async function getCollabStorageWithAPICall(workspaceId: string, id: strin const asyncApply = async () => { const res = await fetchCollab(workspaceId, id, type); - applyDocument(doc, res.state); + applyYDoc(doc, res.state); }; // If the document exists locally, apply the state asynchronously, @@ -74,28 +91,27 @@ export async function batchCollabs( for (const item of params) { const { object_id, collab_type } = item; - const { doc } = await getCollabStorage(object_id, collab_type); + const { doc, localExist } = await getCollabStorage(object_id, collab_type); - if (rowCallback) { + if (rowCallback && localExist) { rowCallback(object_id, doc); } } - // Async fetch collab data and apply to Y.Doc - void (async () => { - const res = await batchFetchCollab(workspaceId, params); + 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]; + 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; - } + if (type === undefined || !data) { + continue; + } - const { doc } = await getCollabStorage(id, type); + const { doc } = await getCollabStorage(id, type); - applyDocument(doc, data); - } - })(); + applyYDoc(doc, data); + + rowCallback?.(id, doc); + } } 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 7e170b683bc06..9dd910d6d5922 100644 --- a/frontend/appflowy_web_app/src/application/services/services.type.ts +++ b/frontend/appflowy_web_app/src/application/services/services.type.ts @@ -37,18 +37,21 @@ export interface DocumentService { export interface DatabaseService { openDatabase: ( workspaceId: string, - viewId: string + viewId: string, + rowIds?: string[] ) => Promise<{ databaseDoc: YDoc; rows: Y.Map; }>; getDatabase: ( workspaceId: string, - databaseId: string + databaseId: string, + rowIds?: string[] ) => Promise<{ databaseDoc: YDoc; rows: Y.Map; }>; + closeDatabase: (databaseId: string) => Promise; } export interface UserService { 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 index 8644914ca70b7..7707c3b779f15 100644 --- 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 @@ -7,6 +7,10 @@ export class TauriDatabaseService implements DatabaseService { // } + async closeDatabase(_databaseId: string) { + return Promise.reject('Not implemented'); + } + async openDatabase( _workspaceId: string, _viewId: string 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 efa20446226f8..9784292884b94 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 @@ -57,15 +57,34 @@ export const YjsEditor = { export function withYjs( editor: T, doc: Y.Doc, - localOrigin: CollabOrigin = CollabOrigin.Local + { + localOrigin, + }: { + localOrigin: CollabOrigin; + } ): 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); + + 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) => { @@ -87,17 +106,8 @@ export function withYjs( 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); }; 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 5a2fd6670cda9..f6e9abaf4032f 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,7 @@ import { Operation, Node } from 'slate'; import * as Y from 'yjs'; -export function applySlateOp(ydoc: Y.Doc, slateRoot: Node, op: Operation) { - console.log('applySlateOp', op); +// transform slate op to yjs op and apply it to ydoc +export function applySlateOp(_ydoc: Y.Doc, _slateRoot: Node, _op: Operation) { + // console.log('applySlateOp', op); } diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/convert.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/convert.ts index ae8b6698e6a7f..65689002ad147 100644 --- a/frontend/appflowy_web_app/src/application/slate-yjs/utils/convert.ts +++ b/frontend/appflowy_web_app/src/application/slate-yjs/utils/convert.ts @@ -22,8 +22,7 @@ interface BlockJson { external_id?: string; } -export function yDocToSlateContent(doc: YDoc, includeRoot?: boolean): Element | undefined { - console.log(doc); +export function yDocToSlateContent(doc: YDoc): Element | undefined { const sharedRoot = doc.getMap(YjsEditorKey.data_section) as YSharedRoot; console.log(sharedRoot.toJSON()); @@ -107,13 +106,6 @@ export function yDocToSlateContent(doc: YDoc, includeRoot?: boolean): Element | if (!result) return; - if (!includeRoot) { - return result; - } - - const { children, ...rootNode } = result; - - // load font family if (fontFamilys.length > 0) { window.WebFont?.load({ google: { @@ -122,21 +114,7 @@ export function yDocToSlateContent(doc: YDoc, includeRoot?: boolean): Element | }); } - return { - children: [ - { - ...rootNode, - children: [ - { - textId: pageId, - type: YjsEditorKey.text, - children: [{ text: '' }], - }, - ], - }, - ...children, - ], - }; + return result; } export function blockToSlateNode(block: BlockJson): Element { 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 8be1dbc29792d..0565e8de9278c 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 10af76fcde8ef..c3f3bfd9035bd 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 fd50bb6df8418..cab9831833673 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 dfe5c029e9933..2fc6deca73c9c 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 @@ -2,7 +2,6 @@ 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/ydoc/apply/__tests__/document.test.ts b/frontend/appflowy_web_app/src/application/ydoc/apply/__tests__/document.test.ts index 512c28ae6a931..8a332f4b60fb8 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 60d02d04509e9..0000000000000 --- 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 8147823035501..b19cb43328203 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/components/_shared/context-provider/FolderProvider.tsx b/frontend/appflowy_web_app/src/components/_shared/context-provider/FolderProvider.tsx index be466fdc4976b..37bb03533b0e6 100644 --- a/frontend/appflowy_web_app/src/components/_shared/context-provider/FolderProvider.tsx +++ b/frontend/appflowy_web_app/src/components/_shared/context-provider/FolderProvider.tsx @@ -1,9 +1,23 @@ import { YFolder } from '@/application/collab.type'; -import { FolderContext } from '@/application/folder-yjs'; +import { Crumb, FolderContext } from '@/application/folder-yjs'; -export const FolderProvider: React.FC<{ folder: YFolder | null; children?: React.ReactNode }> = ({ - folder, - children, -}) => { - return {children}; +export const FolderProvider: React.FC<{ + folder: YFolder | null; + children?: React.ReactNode; + onNavigateToView?: (viewId: string) => void; + crumbs?: Crumb[]; + setCrumbs?: React.Dispatch>; +}> = ({ folder, children, onNavigateToView, crumbs, setCrumbs }) => { + return ( + + {children} + + ); }; 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 789642420df57..15682f1c8211d 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 9216a92c698f3..f8a6412d06d3f 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 @@ -2,7 +2,7 @@ import { Button, Dialog, DialogActions, DialogContent, DialogContentText, Dialog import React from 'react'; import { useNavigate } from 'react-router-dom'; -export function RecordNotFound({ open, workspaceId }: { workspaceId: string; open: boolean }) { +export function RecordNotFound({ open, workspaceId, title }: { workspaceId: string; open: boolean; title?: string }) { const navigate = useNavigate(); return ( @@ -10,13 +10,13 @@ export function RecordNotFound({ open, workspaceId }: { workspaceId: string; ope Oops.. something went wrong - Sorry, the page you are looking for does not exist. + {title ? title : 'The record you are looking for does not exist.'} + + ); +} + +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 0000000000000..5e2eaa61d2299 --- /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 0000000000000..fd2861e4e1d7b --- /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 0000000000000..7c6430332bc60 --- /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 index 1012dd454396d..2e752f8f6eb5c 100644 --- 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 @@ -26,7 +26,7 @@ export function useDateTypeCellDispatcher(fieldId: string) { const getDateTimeStr = useCallback( (timeStamp: string, includeTime?: boolean) => { - if (!typeOptionValue) return null; + if (!typeOptionValue || !timeStamp) return null; const timeFormat = getTimeFormat(typeOptionValue.timeFormat); const dateFormat = getDateFormat(typeOptionValue.dateFormat); const format = [dateFormat]; 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 index ee3cde673bd2c..d234397606c04 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/Cell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/Cell.tsx @@ -1,31 +1,27 @@ -import { FieldId, YjsDatabaseKey } from '@/application/collab.type'; +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/RowCreateModifiedTime'; +import { RowCreateModifiedTime } from '@/components/database/components/cell/created-modified'; import React, { FC, useMemo } from 'react'; -import RichTextCell from '@/components/database/components/cell/TextCell'; -import UrlCell from '@/components/database/components/cell/UrlCell'; -import NumberCell from '@/components/database/components/cell/NumberCell'; -import CheckboxCell from '@/components/database/components/cell/CheckboxCell'; -import SelectCell from '@/components/database/components/cell/SelectionCell'; -import DateTimeCell from '@/components/database/components/cell/DateTimeCell'; -import ChecklistCell from '@/components/database/components/cell/ChecklistCell'; -import { Cell as CellValue } from '@/components/database/components/cell/cell.type'; -import RelationCell from '@/components/database/components/cell/RelationCell'; +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 interface CellProps { - rowId: string; - fieldId: FieldId; - cell?: CellValue; -} - -export function Cell({ cell, rowId, fieldId }: CellProps) { +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 RichTextCell; + return TextCell; case FieldType.URL: return UrlCell; case FieldType.Number: @@ -34,7 +30,7 @@ export function Cell({ cell, rowId, fieldId }: CellProps) { return CheckboxCell; case FieldType.SingleSelect: case FieldType.MultiSelect: - return SelectCell; + return SelectOptionCell; case FieldType.DateTime: return DateTimeCell; case FieldType.Checklist: @@ -42,21 +38,21 @@ export function Cell({ cell, rowId, fieldId }: CellProps) { case FieldType.Relation: return RelationCell; default: - return RichTextCell; + return TextCell; } - }, [fieldType]) as FC<{ cell?: CellValue; rowId: string; fieldId: FieldId }>; + }, [fieldType]) as FC>; if (fieldType === FieldType.CreatedTime || fieldType === FieldType.LastEditedTime) { const attrName = fieldType === FieldType.CreatedTime ? YjsDatabaseKey.created_at : YjsDatabaseKey.last_modified; - return ; + return ; } - if (cell?.fieldType !== fieldType) { + if (cell && cell.fieldType !== fieldType) { return null; } - return ; + return ; } export default Cell; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/CheckboxCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/CheckboxCell.tsx deleted file mode 100644 index 558c424f62616..0000000000000 --- a/frontend/appflowy_web_app/src/components/database/components/cell/CheckboxCell.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { FieldId } from '@/application/collab.type'; -import { ReactComponent as CheckboxCheckSvg } from '$icons/16x/check_filled.svg'; -import { ReactComponent as CheckboxUncheckSvg } from '$icons/16x/uncheck.svg'; -import { CheckboxCell } from '@/components/database/components/cell/cell.type'; - -export default function ({ cell }: { cell?: CheckboxCell; rowId: string; fieldId: FieldId }) { - const checked = cell?.data; - - return ( -
- {checked ? : } -
- ); -} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/ChecklistCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/ChecklistCell.tsx deleted file mode 100644 index 32d97d758fe8a..0000000000000 --- a/frontend/appflowy_web_app/src/components/database/components/cell/ChecklistCell.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { FieldId } from '@/application/collab.type'; -import { parseChecklistData } from '@/application/database-yjs'; -import { ChecklistCell } from '@/components/database/components/cell/cell.type'; -import LinearProgressWithLabel from '@/components/_shared/progress/LinearProgressWithLabel'; -import React, { useMemo } from 'react'; - -export default function ({ cell }: { cell?: ChecklistCell; rowId: string; fieldId: FieldId }) { - const data = useMemo(() => { - return parseChecklistData(cell?.data ?? ''); - }, [cell?.data]); - - const options = data?.options; - const selectedOptions = data?.selectedOptionIds; - - if (!data || !options || !selectedOptions) return null; - return ( -
- -
- ); -} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/NumberCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/NumberCell.tsx deleted file mode 100644 index 851e14a34e996..0000000000000 --- a/frontend/appflowy_web_app/src/components/database/components/cell/NumberCell.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { FieldId } from '@/application/collab.type'; -import { currencyFormaterMap, NumberFormat, useFieldSelector, parseNumberTypeOptions } from '@/application/database-yjs'; -import { UrlCell } from '@/components/database/components/cell/cell.type'; -import React, { useMemo } from 'react'; -import Decimal from 'decimal.js'; - -export default function ({ cell, fieldId }: { cell?: UrlCell; rowId: string; fieldId: FieldId }) { - 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]); - - return
{value}
; -} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/RelationCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/RelationCell.tsx deleted file mode 100644 index 56c1e8d27be25..0000000000000 --- a/frontend/appflowy_web_app/src/components/database/components/cell/RelationCell.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { - FieldId, - YDatabaseField, - YDatabaseFields, - YDatabaseRow, - YDoc, - YjsDatabaseKey, - YjsEditorKey, -} from '@/application/collab.type'; -import { useFieldSelector, parseRelationTypeOption } from '@/application/database-yjs'; -import { useId } from '@/components/_shared/context-provider/IdProvider'; -import { AFConfigContext } from '@/components/app/AppConfig'; -import { parseYDatabaseCellToCell } from '@/components/database/components/cell/cell.parse'; -import { RelationCell, RelationCellData } from '@/components/database/components/cell/cell.type'; -import React, { useContext, useEffect, useMemo, useState } from 'react'; -import * as Y from 'yjs'; - -export default function ({ cell, fieldId }: { cell?: RelationCell; fieldId: string; rowId: string }) { - 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).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]); - - return ( -
- {rowIds.map((rowId) => { - const rowDoc = rows?.get(rowId); - - return ( -
- {rowDoc && databasePrimaryFieldId && ( - - )} -
- ); - })} -
- ); -} - -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/SelectionCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/SelectionCell.tsx deleted file mode 100644 index a915d31a9b91d..0000000000000 --- a/frontend/appflowy_web_app/src/components/database/components/cell/SelectionCell.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { FieldId } from '@/application/collab.type'; -import { useFieldSelector, parseSelectOptionTypeOptions } from '@/application/database-yjs'; -import { Tag } from '@/components/_shared/tag'; -import { SelectOptionColorMap } from '@/components/database/components/cell/cell.const'; -import { SelectCell } from '@/components/database/components/cell/cell.type'; -import React, { useCallback, useMemo } from 'react'; - -export default function ({ cell, fieldId }: { cell?: SelectCell; rowId: string; fieldId: FieldId }) { - const selectOptionIds = useMemo(() => 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] - ); - - return ( -
- {selectOptionIds ? renderSelectedOptions(selectOptionIds) : null} -
- ); -} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/TextCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/TextCell.tsx deleted file mode 100644 index f9c8749258ce7..0000000000000 --- a/frontend/appflowy_web_app/src/components/database/components/cell/TextCell.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { FieldId } from '@/application/collab.type'; -import { useReadOnly } from '@/application/database-yjs'; -import { TextCell } from '@/components/database/components/cell/cell.type'; -import React from 'react'; - -function TextCellComponent({ cell }: { cell?: TextCell; rowId: string; fieldId: FieldId }) { - const readOnly = useReadOnly(); - - return
{cell?.data}
; -} - -export default TextCellComponent; 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 index 185cca9409814..1c82465a84821 100644 --- 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 @@ -1,6 +1,7 @@ -import { RowId } from '@/application/collab.type'; -import { DateFormat, SelectOption, TimeFormat } from '@/application/database-yjs'; +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 { @@ -32,7 +33,7 @@ export interface UrlCell extends Cell { export type SelectionId = string; -export interface SelectCell extends Cell { +export interface SelectOptionCell extends Cell { fieldType: FieldType.SingleSelect | FieldType.MultiSelect; data: SelectionId; } @@ -51,11 +52,6 @@ export interface DateTimeCell extends Cell { reminderId?: string; } -export interface TimeStampCell extends Cell { - fieldType: FieldType.LastEditedTime | FieldType.CreatedTime; - data: TimestampCellData; -} - export interface DateTimeCellData { date?: string; time?: string; @@ -67,11 +63,6 @@ export interface DateTimeCellData { isRange?: boolean; } -export interface TimestampCellData { - dataTime?: string; - timestamp?: number; -} - export interface ChecklistCell extends Cell { fieldType: FieldType.Checklist; data: string; @@ -84,7 +75,12 @@ export interface RelationCell extends Cell { export type RelationCellData = RowId[]; -export interface ChecklistCellData { - selected_option_ids?: string[]; - options?: SelectOption[]; +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 0000000000000..c2b3e7ac68a3b --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/checkbox/CheckboxCell.tsx @@ -0,0 +1,16 @@ +import { ReactComponent as CheckboxCheckSvg } from '$icons/16x/check_filled.svg'; +import { ReactComponent as CheckboxUncheckSvg } from '$icons/16x/uncheck.svg'; +import { FieldType } from '@/application/database-yjs'; +import { CellProps, CheckboxCell as CheckboxCellType } from '@/components/database/components/cell/cell.type'; + +export function CheckboxCell({ cell, style }: CellProps) { + const checked = cell?.data; + + if (cell?.fieldType !== FieldType.Checkbox) return null; + + 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 0000000000000..f1cb1ac4bf47b --- /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 0000000000000..618562b37307e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/checklist/ChecklistCell.tsx @@ -0,0 +1,27 @@ +import { FieldType, 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 (cell?.fieldType !== FieldType.Checklist) return null; + + 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 0000000000000..b12d47b6c533e --- /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/RowCreateModifiedTime.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/RowCreateModifiedTime.tsx similarity index 60% rename from frontend/appflowy_web_app/src/components/database/components/cell/RowCreateModifiedTime.tsx rename to frontend/appflowy_web_app/src/components/database/components/cell/created-modified/RowCreateModifiedTime.tsx index d685b53cf953a..56e7027567655 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/RowCreateModifiedTime.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/RowCreateModifiedTime.tsx @@ -1,43 +1,48 @@ import { YjsDatabaseKey } from '@/application/collab.type'; -import { useRowMeta } from '@/application/database-yjs'; +import { useRowDataSelector } from '@/application/database-yjs'; import { useDateTypeCellDispatcher } from '@/components/database/components/cell/Cell.hooks'; import React, { useEffect, useMemo, useState } from 'react'; -function RowCreateModifiedTime({ +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 rowMeta = useRowMeta(rowId); + const { row: rowData } = useRowDataSelector(rowId); const [value, setValue] = useState(null); useEffect(() => { - if (!rowMeta) return; + if (!rowData) return; const observeHandler = () => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - setValue(rowMeta.get(attrName)); + setValue(rowData.get(attrName)); }; observeHandler(); - rowMeta.observe(observeHandler); + rowData.observe(observeHandler); return () => { - rowMeta.unobserve(observeHandler); + rowData.unobserve(observeHandler); }; - }, [rowMeta, attrName]); + }, [rowData, attrName]); const time = useMemo(() => { if (!value) return null; return getDateTimeStr(value, false); }, [value, getDateTimeStr]); - return
{time}
; + 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 0000000000000..ed951f35213b7 --- /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/DateTimeCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/date/DateTimeCell.tsx similarity index 62% rename from frontend/appflowy_web_app/src/components/database/components/cell/DateTimeCell.tsx rename to frontend/appflowy_web_app/src/components/database/components/cell/date/DateTimeCell.tsx index 490a2bd95efd5..c64c0f2d01230 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/DateTimeCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/date/DateTimeCell.tsx @@ -1,10 +1,10 @@ -import { FieldId } from '@/application/collab.type'; +import { FieldType } from '@/application/database-yjs'; import { useDateTypeCellDispatcher } from '@/components/database/components/cell/Cell.hooks'; -import { DateTimeCell } from '@/components/database/components/cell/cell.type'; +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 default function ({ cell, fieldId }: { cell?: DateTimeCell; rowId: string; fieldId: FieldId }) { +export function DateTimeCell({ cell, fieldId, style, placeholder }: CellProps) { const { getDateTimeStr } = useDateTypeCellDispatcher(fieldId); const startDateTime = useMemo(() => { @@ -21,13 +21,20 @@ export default function ({ cell, fieldId }: { cell?: DateTimeCell; rowId: string }, [cell, getDateTimeStr]); const dateStr = useMemo(() => { - return [startDateTime, endDateTime].filter(Boolean).join(' -> '); + return [startDateTime, endDateTime].filter(Boolean).join(' - '); }, [startDateTime, endDateTime]); const hasReminder = !!cell?.reminderId; + if (cell?.fieldType !== FieldType.DateTime) return null; + 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 0000000000000..e05bb1674a61d --- /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/number/NumberCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/number/NumberCell.tsx new file mode 100644 index 0000000000000..f7312bfbd8034 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/number/NumberCell.tsx @@ -0,0 +1,42 @@ +import { + currencyFormaterMap, + NumberFormat, + useFieldSelector, + parseNumberTypeOptions, + FieldType, +} 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 || cell.fieldType !== FieldType.Number) 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 0000000000000..3e1686c783062 --- /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 0000000000000..09287d48b56f9 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/primary/PrimaryCell.tsx @@ -0,0 +1,73 @@ +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 OpenAction from '@/components/database/components/database-row/OpenAction'; +import { getPlatform } from '@/utils/platform'; +import React, { useEffect, useMemo, useState } from 'react'; + +export function PrimaryCell(props: CellProps) { + const { rowId } = props; + const meta = useRowMetaSelector(rowId); + const icon = meta?.icon; + + const [hover, setHover] = useState(false); + + 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); + } + }; + + const onMouseLeave = () => { + setHover(false); + }; + + table.addEventListener('mousemove', onMouseMove); + table.addEventListener('mouseleave', onMouseLeave); + return () => { + table.removeEventListener('mousemove', onMouseMove); + table.removeEventListener('mouseleave', onMouseLeave); + }; + }, [rowId]); + + const isMobile = useMemo(() => { + return getPlatform().isMobile; + }, []); + + const navigateToRow = useNavigateToRow(); + + return ( +
{ + if (isMobile) { + navigateToRow?.(rowId); + } + }} + className={'primary-cell relative flex min-h-full w-full items-center gap-2'} + > + {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 0000000000000..c854b4e3365a0 --- /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 0000000000000..47ac405966b15 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationCell.tsx @@ -0,0 +1,17 @@ +import { FieldType } from '@/application/database-yjs'; +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?.fieldType !== FieldType.Relation) return null; + + 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 0000000000000..1a998c512237a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationItems.tsx @@ -0,0 +1,79 @@ +import { YDatabaseField, YDatabaseFields, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; +import { + DatabaseContextState, + parseRelationTypeOption, + useDatabase, + useFieldSelector, + useNavigateToRow, +} 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'; + +function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId: string; style?: React.CSSProperties }) { + const { field } = useFieldSelector(fieldId); + const currentDatabaseId = useDatabase()?.get(YjsDatabaseKey.id); + const workspaceId = useId()?.workspaceId; + const rowIds = useMemo(() => { + return (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(); + + const navigateToRow = useNavigateToRow(); + + useEffect(() => { + if (!workspaceId || !databaseId || !rowIds.length) 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]); + + useEffect(() => { + return () => { + if (currentDatabaseId !== databaseId && databaseId) { + void databaseService?.closeDatabase(databaseId); + } + }; + }, [currentDatabaseId, databaseId, databaseService]); + + return ( +
+ {rowIds.map((rowId) => { + const rowDoc = rows?.get(rowId); + + return ( +
{ + e.stopPropagation(); + navigateToRow?.(rowId); + }} + className={'w-full cursor-pointer underline'} + > + {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 0000000000000..0c33397eb8512 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationPrimaryValue.tsx @@ -0,0 +1,42 @@ +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); + const [row, setRow] = useState(null); + + useEffect(() => { + const data = rowDoc.getMap(YjsEditorKey.data_section); + + const onRowChange = () => { + setRow(data?.get(YjsEditorKey.database_row) as YDatabaseRow); + }; + + onRowChange(); + data?.observe(onRowChange); + return () => { + data?.unobserve(onRowChange); + }; + }, [rowDoc]); + + useEffect(() => { + if (!row) return; + 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); + }; + }, [row, 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 0000000000000..95a0aa3668c90 --- /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 0000000000000..4d3318297fd26 --- /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 0000000000000..40df2f3d7d69c --- /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 0000000000000..b4d048b14fed7 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/text/TextCell.tsx @@ -0,0 +1,19 @@ +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, placeholder }: CellProps) { + const readOnly = useReadOnly(); + + if (!cell?.data) + return placeholder ? ( +
+ {placeholder} +
+ ) : 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 0000000000000..64bcb41a7f242 --- /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/UrlCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/url/UrlCell.tsx similarity index 59% rename from frontend/appflowy_web_app/src/components/database/components/cell/UrlCell.tsx rename to frontend/appflowy_web_app/src/components/database/components/cell/url/UrlCell.tsx index e2d3d2c87f483..0ee2c1d5bf4f3 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/UrlCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/url/UrlCell.tsx @@ -1,16 +1,15 @@ -import { FieldId } from '@/application/collab.type'; import { useReadOnly } from '@/application/database-yjs'; -import { UrlCell } from '@/components/database/components/cell/cell.type'; +import { CellProps, UrlCell as UrlCellType } from '@/components/database/components/cell/cell.type'; import { openUrl, processUrl } from '@/utils/url'; import React, { useMemo } from 'react'; -export default function ({ cell }: { cell?: UrlCell; rowId: string; fieldId: FieldId }) { +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']; + const classList = ['select-text', 'w-fit', 'flex', 'w-full', 'items-center']; if (isUrl) { classList.push('text-content-blue-400', 'underline', 'cursor-pointer'); @@ -21,11 +20,20 @@ export default function ({ cell }: { cell?: UrlCell; rowId: string; fieldId: Fie return classList.join(' '); }, [isUrl]); + if (!cell?.data) + return placeholder ? ( +
+ {placeholder} +
+ ) : null; + return (
{ + style={style} + onClick={(e) => { if (!isUrl || !cell) return; if (readOnly) { + e.stopPropagation(); void openUrl(cell.data, '_blank'); } }} 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 0000000000000..9f45924c9717c --- /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 0000000000000..d4d7020523bba --- /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 index 6b4a83659774c..a7559374c2572 100644 --- a/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseActions.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseActions.tsx @@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next'; export function DatabaseActions() { const { t } = useTranslation(); + const sorts = useSortsSelector(); const filter = useFiltersSelector(); const conditionsContext = useConditionsContext(); 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 index fc36c470d62ab..7c74e0fb8a51d 100644 --- a/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseConditions.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseConditions.tsx @@ -18,7 +18,7 @@ export function DatabaseConditions() { borderTopWidth: expanded ? '1px' : '0', }} className={ - 'database-conditions relative mx-24 transform overflow-hidden border-t border-line-divider transition-all max-md:mx-4' + 'database-conditions relative mx-16 transform overflow-hidden border-t border-line-divider transition-all max-md:mx-4' } > 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 0000000000000..59405d6c152e7 --- /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 0000000000000..ac9e15c59008a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/database-row/DatabaseRowSubDocument.tsx @@ -0,0 +1,52 @@ +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 CircularProgress from '@mui/material/CircularProgress'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; + +export function DatabaseRowSubDocument({ rowId }: { rowId: string }) { + const { workspaceId } = useId() || {}; + const meta = useRowMetaSelector(rowId); + const documentId = meta?.documentId; + + console.log('documentId', documentId); + const [loading, setLoading] = useState(true); + const [doc, setDoc] = useState(null); + + 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); + + console.log('doc', doc); + setDoc(doc); + } catch (e) { + console.error(e); + // haven't created by client, ignore error and show empty + } + }, [documentService, workspaceId, documentId]); + + useEffect(() => { + setLoading(true); + void handleOpenDocument().then(() => setLoading(false)); + }, [handleOpenDocument]); + + if (loading) { + return ( +
+ +
+ ); + } + + if (!doc) return null; + + return ; +} + +export default DatabaseRowSubDocument; diff --git a/frontend/appflowy_web_app/src/components/database/components/database-row/OpenAction.tsx b/frontend/appflowy_web_app/src/components/database/components/database-row/OpenAction.tsx new file mode 100644 index 0000000000000..94b6977bca73c --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/database-row/OpenAction.tsx @@ -0,0 +1,27 @@ +import { ReactComponent as ExpandMoreIcon } from '$icons/16x/full_view.svg'; +import { useTranslation } from 'react-i18next'; +import { useNavigateToRow } from '@/application/database-yjs'; +import { Tooltip } from '@mui/material'; +import React from 'react'; + +function OpenAction({ rowId }: { rowId: string }) { + const navigateToRow = useNavigateToRow(); + + const { t } = useTranslation(); + + return ( + + + + ); +} + +export default OpenAction; 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 0000000000000..a5f4ddd8aa5c2 --- /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 0000000000000..2575aa47ca368 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/field/CardField.tsx @@ -0,0 +1,46 @@ +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 = {}; + + if (isPrimary) { + Object.assign(styleProperties, { + fontSize: '1.25em', + 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/grid-cell/GridCell.tsx b/frontend/appflowy_web_app/src/components/database/components/grid-cell/GridCell.tsx deleted file mode 100644 index b9a5017b382e6..0000000000000 --- a/frontend/appflowy_web_app/src/components/database/components/grid-cell/GridCell.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { FieldId, YjsDatabaseKey } from '@/application/collab.type'; -import { useRowMeta } from '@/application/database-yjs'; -import { useFieldSelector } from '@/application/database-yjs/selector'; -import { Cell } from '@/components/database/components/cell'; -import { parseYDatabaseCellToCell } from '@/components/database/components/cell/cell.parse'; -import React, { useEffect, useState } 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 = React.useRef(null); - const field = useFieldSelector(fieldId); - const row = useRowMeta(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]); - - useEffect(() => { - const el = ref.current; - - if (!el) return; - - const observer = new ResizeObserver(() => { - if (onResize) { - onResize(rowIndex, columnIndex, { - width: el.offsetWidth, - height: el.offsetHeight, - }); - } - }); - - observer.observe(el); - - return () => { - observer.disconnect(); - }; - }, [columnIndex, onResize, rowIndex]); - - if (!field) return null; - return ( -
- -
- ); -} - -export default GridCell; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-column/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid-column/index.ts deleted file mode 100644 index 6de83c7026da5..0000000000000 --- a/frontend/appflowy_web_app/src/components/database/components/grid-column/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './GridColumn'; -export * from './useRenderColumns'; diff --git a/frontend/appflowy_web_app/src/components/database/components/calculation-cell/CalculationCell.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-calculation-cell/CalculationCell.tsx similarity index 83% rename from frontend/appflowy_web_app/src/components/database/components/calculation-cell/CalculationCell.tsx rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-calculation-cell/CalculationCell.tsx index eeefee18bbc87..1ddb4e2d32ae6 100644 --- a/frontend/appflowy_web_app/src/components/database/components/calculation-cell/CalculationCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-calculation-cell/CalculationCell.tsx @@ -1,11 +1,21 @@ import { CalculationType } from '@/application/database-yjs/database.type'; -import { CalulationCell } from '@/components/database/components/calculation-cell/cell.type'; import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -export function CalculationCell({ cell }: { cell?: CalulationCell }) { +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 ''; diff --git a/frontend/appflowy_web_app/src/components/database/components/calculation-cell/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/grid-calculation-cell/index.ts similarity index 100% rename from frontend/appflowy_web_app/src/components/database/components/calculation-cell/index.ts rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-calculation-cell/index.ts 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 0000000000000..0d3c7dfc11c02 --- /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-cell/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/grid-cell/index.ts similarity index 100% rename from frontend/appflowy_web_app/src/components/database/components/grid-cell/index.ts rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-cell/index.ts diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-column/GridColumn.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/GridColumn.tsx similarity index 55% rename from frontend/appflowy_web_app/src/components/database/components/grid-column/GridColumn.tsx rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-column/GridColumn.tsx index 88c6fae84e966..56845e8425c74 100644 --- a/frontend/appflowy_web_app/src/components/database/components/grid-column/GridColumn.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/GridColumn.tsx @@ -2,6 +2,7 @@ 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 { Tooltip } from '@mui/material'; import React, { useMemo } from 'react'; export function GridColumn({ column, index }: { column: Column; index: number }) { @@ -16,19 +17,21 @@ export function GridColumn({ column, index }: { column: Column; index: number }) }, [field]); return ( -
-
- + +
+
+ +
+
{name}
-
{name}
-
+ ); } 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 0000000000000..3c71a6b89912f --- /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-column/useRenderColumns.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/useRenderFields.tsx similarity index 73% rename from frontend/appflowy_web_app/src/components/database/components/grid-column/useRenderColumns.tsx rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-column/useRenderFields.tsx index c0041b5c5eee2..7bacc7b882366 100644 --- a/frontend/appflowy_web_app/src/components/database/components/grid-column/useRenderColumns.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/useRenderFields.tsx @@ -1,6 +1,6 @@ import { FieldId } from '@/application/collab.type'; import { FieldVisibility } from '@/application/database-yjs/database.type'; -import { useGridColumnsSelector } from '@/application/database-yjs/selector'; +import { useFieldsSelector } from '@/application/database-yjs/selector'; import { useCallback, useMemo } from 'react'; export enum GridColumnType { @@ -9,8 +9,6 @@ export enum GridColumnType { NewProperty, } -const defaultVisibilitys = [FieldVisibility.AlwaysShown, FieldVisibility.HideWhenEmpty]; - export type RenderColumn = { type: GridColumnType; visibility?: FieldVisibility; @@ -19,12 +17,11 @@ export type RenderColumn = { wrap?: boolean; }; -export function useRenderColumns(viewId: string) { - const columns = useGridColumnsSelector(viewId, defaultVisibilitys); +export function useRenderFields() { + const fields = useFieldsSelector(); - console.log('columns', columns); const renderColumns = useMemo(() => { - const fields = columns.map((column) => ({ + const data = fields.map((column) => ({ ...column, type: GridColumnType.Field, })); @@ -32,19 +29,19 @@ export function useRenderColumns(viewId: string) { return [ { type: GridColumnType.Action, - width: 96, + width: 64, }, - ...fields, + ...data, { type: GridColumnType.NewProperty, width: 150, }, { type: GridColumnType.Action, - width: 96, + width: 64, }, ].filter(Boolean) as RenderColumn[]; - }, [columns]); + }, [fields]); const columnWidth = useCallback( (index: number, containerWidth: number) => { @@ -67,7 +64,7 @@ export function useRenderColumns(viewId: string) { ); return { - columns: renderColumns, + fields: renderColumns, columnWidth, }; } diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-header/GridHeader.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-header/GridHeader.tsx similarity index 100% rename from frontend/appflowy_web_app/src/components/database/components/grid-header/GridHeader.tsx rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-header/GridHeader.tsx diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-header/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/grid-header/index.ts similarity index 100% rename from frontend/appflowy_web_app/src/components/database/components/grid-header/index.ts rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-header/index.ts diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-row/GridCalculateRowCell.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridCalculateRowCell.tsx similarity index 83% rename from frontend/appflowy_web_app/src/components/database/components/grid-row/GridCalculateRowCell.tsx rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridCalculateRowCell.tsx index 650ed3bfbe52d..4d7abb7a2ced7 100644 --- a/frontend/appflowy_web_app/src/components/database/components/grid-row/GridCalculateRowCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridCalculateRowCell.tsx @@ -1,8 +1,7 @@ import { YjsDatabaseKey } from '@/application/collab.type'; import { useDatabaseView } from '@/application/database-yjs'; import { CalculationType } from '@/application/database-yjs/database.type'; -import { CalculationCell } from '@/components/database/components/calculation-cell'; -import { CalulationCell } from '@/components/database/components/calculation-cell/cell.type'; +import { CalculationCell, ICalculationCell } from '../grid-calculation-cell'; import React, { useEffect, useState } from 'react'; export interface GridCalculateRowCellProps { @@ -11,7 +10,7 @@ export interface GridCalculateRowCellProps { export function GridCalculateRowCell({ fieldId }: GridCalculateRowCellProps) { const calculations = useDatabaseView()?.get(YjsDatabaseKey.calculations); - const [calculation, setCalculation] = useState(); + const [calculation, setCalculation] = useState(); useEffect(() => { if (!calculations) return; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-row/GridRowCell.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridRowCell.tsx similarity index 81% rename from frontend/appflowy_web_app/src/components/database/components/grid-row/GridRowCell.tsx rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridRowCell.tsx index ef4be6840635b..11f14135e352a 100644 --- a/frontend/appflowy_web_app/src/components/database/components/grid-row/GridRowCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridRowCell.tsx @@ -1,6 +1,6 @@ -import { GridColumnType } from '@/components/database/components/grid-column'; +import { GridColumnType } from '../grid-column'; import React from 'react'; -import GridCell from 'src/components/database/components/grid-cell/GridCell'; +import GridCell from '../grid-cell/GridCell'; export interface GridRowCellProps { rowId: string; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-row/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/index.ts similarity index 100% rename from frontend/appflowy_web_app/src/components/database/components/grid-row/index.ts rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-row/index.ts diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-row/useRenderRows.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/useRenderRows.tsx similarity index 76% rename from frontend/appflowy_web_app/src/components/database/components/grid-row/useRenderRows.tsx rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-row/useRenderRows.tsx index e5038cafff141..8b2e6597b8dc4 100644 --- a/frontend/appflowy_web_app/src/components/database/components/grid-row/useRenderRows.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/useRenderRows.tsx @@ -1,6 +1,5 @@ -import { useReadOnly } from '@/application/database-yjs'; -import { DEFAULT_ROW_HEIGHT } from '@/application/database-yjs/const'; -import { useGridRowsSelector } from '@/application/database-yjs/selector'; +import { DEFAULT_ROW_HEIGHT, useReadOnly, useRowsSelector } from '@/application/database-yjs'; + import { useMemo } from 'react'; export enum RenderRowType { @@ -16,7 +15,7 @@ export type RenderRow = { }; export function useRenderRows() { - const rows = useGridRowsSelector(); + const rows = useRowsSelector(); const readOnly = useReadOnly(); const renderRows = useMemo(() => { diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-table/GridTable.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx similarity index 93% rename from frontend/appflowy_web_app/src/components/database/components/grid-table/GridTable.tsx rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx index dd3ed13bfe8ea..34f5a97b75075 100644 --- a/frontend/appflowy_web_app/src/components/database/components/grid-table/GridTable.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx @@ -1,12 +1,7 @@ import { DEFAULT_ROW_HEIGHT } from '@/application/database-yjs/const'; import { AFScroller } from '@/components/_shared/scroller'; -import { GridColumnType, RenderColumn } from '@/components/database/components/grid-column'; -import { - GridCalculateRowCell, - GridRowCell, - RenderRowType, - useRenderRows, -} from '@/components/database/components/grid-row'; +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'; @@ -27,7 +22,6 @@ export const GridTable = ({ scrollLeft, columnWidth, columns, onScrollLeft }: Gr useEffect(() => { if (ref.current) { - console.log(ref.current, scrollLeft); ref.current.scrollTo({ scrollLeft }); } }, [scrollLeft]); @@ -99,10 +93,10 @@ export const GridTable = ({ scrollLeft, columnWidth, columns, onScrollLeft }: Gr const row = data.rows[rowIndex]; const column = data.columns[columnIndex] as RenderColumn; - const classList = ['flex', 'items-center', 'overflow-hidden']; + const classList = ['flex', 'items-center', 'overflow-hidden', 'grid-row-cell']; if (column.wrap) { - classList.push('whitespace-pre-wrap', 'break-words'); + classList.push('wrap-cell'); } else { classList.push('whitespace-nowrap'); } @@ -118,6 +112,7 @@ export const GridTable = ({ scrollLeft, columnWidth, columns, onScrollLeft }: Gr if (row.type === RenderRowType.Row) { return (
@@ -158,6 +153,7 @@ export const GridTable = ({ scrollLeft, columnWidth, columns, onScrollLeft }: Gr columnCount={columns.length} columnWidth={(index) => columnWidth(index, width)} rowHeight={rowHeight} + className={'grid-table'} overscanRowCount={5} overscanColumnCount={5} style={{ diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-table/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/index.ts similarity index 100% rename from frontend/appflowy_web_app/src/components/database/components/grid-table/index.ts rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-table/index.ts 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 0000000000000..2e9a6988f4f49 --- /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 0000000000000..f84da67aa2edf --- /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 0000000000000..330e5ba1cf65f --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/header/DatabaseRowHeader.tsx @@ -0,0 +1,36 @@ +import { useCellSelector, useDatabaseViewId, usePrimaryFieldId, useRowMetaSelector } from '@/application/database-yjs'; +import { FolderContext } from '@/application/folder-yjs'; +import Title from '@/components/database/components/header/Title'; +import React, { useContext, useEffect } from 'react'; + +function DatabaseRowHeader({ rowId }: { rowId: string }) { + const fieldId = usePrimaryFieldId() || ''; + const setCrumbs = useContext(FolderContext)?.setCrumbs; + const viewId = useDatabaseViewId(); + + const meta = useRowMetaSelector(rowId); + const cell = useCellSelector({ + rowId, + fieldId, + }); + + useEffect(() => { + if (!viewId) return; + setCrumbs?.((prev) => { + const lastCrumb = prev[prev.length - 1]; + const crumb = { + viewId, + rowId, + name: cell?.data as string, + icon: meta?.icon || '', + }; + + if (lastCrumb?.rowId === rowId) return [...prev.slice(0, -1), crumb]; + return [...prev, crumb]; + }); + }, [cell, meta, rowId, setCrumbs, viewId]); + + 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 0000000000000..8bb19791964c4 --- /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 0000000000000..452eceafe15b9 --- /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 0000000000000..b1e4662b2d675 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/property/Property.tsx @@ -0,0 +1,77 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType, useCellSelector, useFieldSelector } from '@/application/database-yjs'; +import { Cell as CellType, CellProps } 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 { TextCell } from '@/components/database/components/cell/text'; +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; + case FieldType.RichText: + return TextCell; + default: + return TextProperty; + } + }, [fieldType]) as FC<CellProps<CellType>>; + + const style = useMemo( + () => ({ + fontSize: '12px', + }), + [] + ); + + 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 0000000000000..c930365b37aa9 --- /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 items-center overflow-x-hidden 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 0000000000000..fabe862659ad4 --- /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 py-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 0000000000000..413d3c884b6e3 --- /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 0000000000000..1a4ad04e85cfc --- /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 0000000000000..64589c0ea3538 --- /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 0000000000000..c10e4ed3d0eb1 --- /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/tabs/DatabaseTabs.tsx b/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx index 65a1b238bb7b8..84e2a355ef230 100644 --- a/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx @@ -1,7 +1,9 @@ -import { ViewLayout, YjsFolderKey, YView } from '@/application/collab.type'; +import { DatabaseViewLayout, ViewLayout, YjsDatabaseKey, YjsFolderKey, YView } from '@/application/collab.type'; +import { useDatabaseView } from '@/application/database-yjs'; import { useFolderContext } from '@/application/folder-yjs'; import { useId } from '@/components/_shared/context-provider/IdProvider'; import { DatabaseActions } from '@/components/database/components/conditions'; +import { Tooltip } from '@mui/material'; import { forwardRef, FunctionComponent, SVGProps, useCallback, useEffect, useMemo } from 'react'; import { ViewTabs, ViewTab } from './ViewTabs'; import { useTranslation } from 'react-i18next'; @@ -31,6 +33,9 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>( const objectId = useId().objectId; const { t } = useTranslation(); const folder = useFolderContext(); + const view = useDatabaseView(); + const layout = Number(view?.get(YjsDatabaseKey.layout)) as DatabaseViewLayout; + const handleChange = (_: React.SyntheticEvent, newValue: string) => { setSelectedViewId?.(newValue); }; @@ -50,9 +55,21 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>( [folder] ); + const className = useMemo(() => { + const classList = [ + 'mx-16 -mb-[0.5px] flex items-center overflow-hidden border-line-divider text-text-title max-md:mx-4', + ]; + + if (layout === DatabaseViewLayout.Calendar) { + classList.push('border-b'); + } + + return classList.join(' '); + }, [layout]); + if (viewIds.length === 0) return null; return ( - <div ref={ref} className='mx-24 flex items-center overflow-hidden text-text-title max-md:mx-4'> + <div ref={ref} className={className}> <div style={{ width: 'calc(100% - 120px)', @@ -66,7 +83,7 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>( value={isSelected ? selectedViewId : objectId} onChange={handleChange} > - {viewIds.map((viewId, index) => { + {viewIds.map((viewId) => { const view = getFolderView(viewId); if (!view) return null; @@ -77,20 +94,21 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>( return ( <ViewTab key={viewId} - style={{ - borderRight: index === viewIds.length - 1 ? 'none' : '1px solid var(--line-divider)', - }} icon={<Icon className={'h-4 w-4'} />} iconPosition='start' color='inherit' - label={name || t('grid.title.placeholder')} + label={ + <Tooltip title={name} placement={'right'}> + <span className={'max-w-[120px] truncate'}>{name || t('grid.title.placeholder')}</span> + </Tooltip> + } value={viewId} /> ); })} </ViewTabs> </div> - <DatabaseActions /> + {layout !== DatabaseViewLayout.Calendar ? <DatabaseActions /> : null} </div> ); } diff --git a/frontend/appflowy_web_app/src/components/database/grid/Grid.tsx b/frontend/appflowy_web_app/src/components/database/grid/Grid.tsx index eacb2b725ad44..ebc7d866ea1f3 100644 --- a/frontend/appflowy_web_app/src/components/database/grid/Grid.tsx +++ b/frontend/appflowy_web_app/src/components/database/grid/Grid.tsx @@ -1,16 +1,19 @@ -import { GridRowsContext, useDatabase, useGridRowOrders, useViewId } from '@/application/database-yjs'; -import { useRenderColumns } from '@/components/database/components/grid-column'; +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'; -import { GridHeader } from 'src/components/database/components/grid-header'; -import { GridTable } from 'src/components/database/components/grid-table'; +import React, { useEffect, useState } from 'react'; export function Grid() { const database = useDatabase(); - const [scrollLeft, setScrollLeft] = useState(0); const viewId = useViewId() || ''; - const { columns, columnWidth } = useRenderColumns(viewId); - const rowOrders = useGridRowOrders(); + const [scrollLeft, setScrollLeft] = useState(0); + + const { fields, columnWidth } = useRenderFields(); + const rowOrders = useRowOrdersSelector(); + + useEffect(() => { + setScrollLeft(0); + }, [viewId]); if (!database || !rowOrders) { return ( @@ -21,24 +24,24 @@ export function Grid() { } return ( - <GridRowsContext.Provider + <RowsContext.Provider value={{ rowOrders, }} > <div className={'flex w-full flex-1 flex-col'}> - <GridHeader scrollLeft={scrollLeft} columnWidth={columnWidth} columns={columns} onScrollLeft={setScrollLeft} /> + <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={columns} + columns={fields} onScrollLeft={setScrollLeft} /> </div> </div> - </GridRowsContext.Provider> + </RowsContext.Provider> ); } diff --git a/frontend/appflowy_web_app/src/components/document/Document.tsx b/frontend/appflowy_web_app/src/components/document/Document.tsx index 82e20bed4dc8e..313ccb1480b87 100644 --- a/frontend/appflowy_web_app/src/components/document/Document.tsx +++ b/frontend/appflowy_web_app/src/components/document/Document.tsx @@ -1,17 +1,29 @@ import { YDoc } from '@/application/collab.type'; import { useId } from '@/components/_shared/context-provider/IdProvider'; +import { usePageInfo } from '@/components/_shared/page/usePageInfo'; +import ComponentLoading from '@/components/_shared/progress/ComponentLoading'; import { AFConfigContext } from '@/components/app/AppConfig'; import { DocumentHeader } from '@/components/document/document_header'; import { Editor } from '@/components/editor'; +import { EditorLayoutStyle } from '@/components/editor/EditorContext'; import { Log } from '@/utils/log'; -import React, { useCallback, useContext, useEffect, useState } from 'react'; +import CircularProgress from '@mui/material/CircularProgress'; +import React, { Suspense, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import RecordNotFound from 'src/components/_shared/not-found/RecordNotFound'; export const Document = () => { const { objectId: documentId, workspaceId } = useId() || {}; const [doc, setDoc] = useState<YDoc | null>(null); const [notFound, setNotFound] = useState<boolean>(false); + const extra = usePageInfo(documentId).extra; + const layoutStyle: EditorLayoutStyle = useMemo(() => { + return { + font: extra?.font || '', + fontLayout: extra?.fontLayout, + lineHeightLayout: extra?.lineHeightLayout, + }; + }, [extra]); const documentService = useContext(AFConfigContext)?.service?.documentService; const handleOpenDocument = useCallback(async () => { @@ -32,22 +44,70 @@ export const Document = () => { void handleOpenDocument(); }, [handleOpenDocument]); + const style = useMemo(() => { + const fontSizeMap = { + small: '14px', + normal: '16px', + large: '20px', + }; + + return { + fontFamily: layoutStyle.font, + fontSize: fontSizeMap[layoutStyle.fontLayout], + }; + }, [layoutStyle]); + + const layoutClassName = useMemo(() => { + const classList = []; + + if (layoutStyle.fontLayout === 'large') { + classList.push('font-large'); + } else if (layoutStyle.fontLayout === 'small') { + classList.push('font-small'); + } + + if (layoutStyle.lineHeightLayout === 'large') { + classList.push('line-height-large'); + } else if (layoutStyle.lineHeightLayout === 'small') { + classList.push('line-height-small'); + } + + return classList.join(' '); + }, [layoutStyle]); + + useEffect(() => { + if (!layoutStyle.font) return; + void window.WebFont?.load({ + google: { + families: [layoutStyle.font], + }, + }); + }, [layoutStyle.font]); + if (!documentId) return null; return ( <> - {doc && ( - <div className={'relative w-full'}> + {doc ? ( + <div style={style} className={`relative w-full ${layoutClassName}`}> <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} /> - </div> + <Suspense fallback={<ComponentLoading />}> + <div className={'max-w-screen w-[964px] min-w-0'}> + <Editor doc={doc} readOnly={true} layoutStyle={layoutStyle} /> + </div> + </Suspense> </div> </div> + ) : ( + <div className={'flex h-full w-full items-center justify-center'}> + <CircularProgress /> + </div> )} <RecordNotFound open={notFound} workspaceId={workspaceId} /> </> ); }; + +export default Document; diff --git a/frontend/appflowy_web_app/src/components/document/document_header/DocumentCover.tsx b/frontend/appflowy_web_app/src/components/document/document_header/DocumentCover.tsx index 08ee25ef87bcb..53d0315271b27 100644 --- a/frontend/appflowy_web_app/src/components/document/document_header/DocumentCover.tsx +++ b/frontend/appflowy_web_app/src/components/document/document_header/DocumentCover.tsx @@ -1,11 +1,20 @@ -import { CoverType, YDoc } from '@/application/collab.type'; +import { DocCoverType, YDoc } from '@/application/collab.type'; +import { CoverType } from '@/application/folder-yjs/folder.type'; +import { useId } from '@/components/_shared/context-provider/IdProvider'; +import { usePageInfo } from '@/components/_shared/page/usePageInfo'; import { useBlockCover } from '@/components/document/document_header/useBlockCover'; +import { showColorsForImage } from '@/components/document/document_header/utils'; import { renderColor } from '@/utils/color'; import React, { useCallback } from 'react'; import DefaultImage from './default_cover.jpg'; -function DocumentCover({ doc }: { doc: YDoc }) { +function DocumentCover({ doc, onTextColor }: { doc: YDoc; onTextColor: (color: string) => void }) { + const viewId = useId().objectId; + const { extra } = usePageInfo(viewId); + + const pageCover = extra.cover; const { cover } = useBlockCover(doc); + const renderCoverColor = useCallback((color: string) => { return ( <div @@ -17,21 +26,47 @@ function DocumentCover({ doc }: { doc: YDoc }) { ); }, []); - const renderCoverImage = useCallback((url: string) => { - return <img draggable={false} src={url} alt={''} className={'h-full w-full object-cover'} />; - }, []); - - const { cover_selection_type: type, cover_selection: value = '' } = cover || {}; + const renderCoverImage = useCallback( + (url: string) => { + return ( + <img + onLoad={(e) => { + void showColorsForImage(e.currentTarget).then((res) => { + onTextColor(res); + }); + }} + draggable={false} + src={url} + alt={''} + className={'h-full w-full object-cover'} + /> + ); + }, + [onTextColor] + ); - return value ? ( - <div className={`relative mb-[-80px] flex h-[255px] w-full`}> - <> - {type === CoverType.Asset ? renderCoverImage(DefaultImage) : null} - {type === CoverType.Color ? renderCoverColor(value) : null} - {type === CoverType.Image ? renderCoverImage(value) : null} - </> + if (!pageCover && !cover?.cover_selection) return null; + return ( + <div className={`relative flex h-[255px] w-full max-sm:h-[180px]`}> + {pageCover ? ( + <> + {[CoverType.NormalColor, CoverType.GradientColor].includes(pageCover.type) + ? renderCoverColor(pageCover.value) + : null} + {CoverType.BuildInImage === pageCover.type ? renderCoverImage(DefaultImage) : null} + {[CoverType.CustomImage, CoverType.UpsplashImage].includes(pageCover.type) + ? renderCoverImage(pageCover.value) + : null} + </> + ) : cover?.cover_selection ? ( + <> + {cover.cover_selection_type === DocCoverType.Asset ? renderCoverImage(DefaultImage) : null} + {cover.cover_selection_type === DocCoverType.Color ? renderCoverColor(cover.cover_selection) : null} + {cover.cover_selection_type === DocCoverType.Image ? renderCoverImage(cover.cover_selection) : null} + </> + ) : null} </div> - ) : null; + ); } export default DocumentCover; diff --git a/frontend/appflowy_web_app/src/components/document/document_header/DocumentHeader.tsx b/frontend/appflowy_web_app/src/components/document/document_header/DocumentHeader.tsx index 0610b8a834bbf..3e7fd5ee28d14 100644 --- a/frontend/appflowy_web_app/src/components/document/document_header/DocumentHeader.tsx +++ b/frontend/appflowy_web_app/src/components/document/document_header/DocumentHeader.tsx @@ -1,12 +1,12 @@ import { YDoc, YjsFolderKey } from '@/application/collab.type'; import { useViewSelector } from '@/application/folder-yjs'; import DocumentCover from '@/components/document/document_header/DocumentCover'; -import React, { memo, useMemo, useRef } from 'react'; +import React, { memo, useMemo, useRef, useState } from 'react'; export function DocumentHeader({ viewId, doc }: { viewId: string; doc: YDoc }) { const ref = useRef<HTMLDivElement>(null); const { view } = useViewSelector(viewId); - + const [textColor, setTextColor] = useState<string>('var(--text-title)'); const icon = view?.get(YjsFolderKey.icon); const iconObject = useMemo(() => { try { @@ -17,21 +17,30 @@ export function DocumentHeader({ viewId, doc }: { viewId: string; doc: YDoc }) { }, [icon]); return ( - <div ref={ref} className={'document-header select-none'}> - <div className={'flex flex-col justify-end'}> - <div className={'view-banner flex w-full flex-col overflow-hidden'}> - <DocumentCover doc={doc} /> + <div ref={ref} className={'document-header mb-[10px] select-none'}> + <div className={'view-banner relative flex w-full flex-col overflow-hidden'}> + <DocumentCover onTextColor={setTextColor} doc={doc} /> - <div className={`relative min-h-[65px] w-[964px] min-w-0 max-w-full px-16 pt-10 max-md:px-4`}> - <div - style={{ - position: 'relative', - bottom: '50%', - }} - > - <div className={`view-icon`}>{iconObject?.value}</div> + <div className={`relative mx-16 w-[964px] min-w-0 max-w-full overflow-visible max-md:mx-4`}> + <div + style={{ + position: 'absolute', + bottom: '100%', + width: '100%', + }} + className={'flex items-center gap-2 px-14 pb-10 text-4xl max-md:px-2 max-md:pb-6 max-sm:text-[7vw]'} + > + <div className={`view-icon`}>{iconObject?.value}</div> + <div className={'flex flex-1 items-center gap-2 overflow-hidden'}> + <div + style={{ + color: textColor, + }} + className={'font-bold leading-[1.5em]'} + > + {view?.get(YjsFolderKey.name)} + </div> </div> - <div className={'py-2'}></div> </div> </div> </div> diff --git a/frontend/appflowy_web_app/src/components/document/document_header/useBlockCover.ts b/frontend/appflowy_web_app/src/components/document/document_header/useBlockCover.ts index 589a1e4169c5c..ba6226a6e832a 100644 --- a/frontend/appflowy_web_app/src/components/document/document_header/useBlockCover.ts +++ b/frontend/appflowy_web_app/src/components/document/document_header/useBlockCover.ts @@ -1,4 +1,4 @@ -import { PageCover, YBlocks, YDoc, YDocument, YjsEditorKey } from '@/application/collab.type'; +import { DocCover, YBlocks, YDoc, YDocument, YjsEditorKey } from '@/application/collab.type'; import { useEffect, useMemo, useState } from 'react'; export function useBlockCover(doc: YDoc) { @@ -22,7 +22,7 @@ export function useBlockCover(doc: YDoc) { }; }, [doc]); - const coverObj: PageCover = useMemo(() => { + const coverObj: DocCover = useMemo(() => { try { return JSON.parse(cover || ''); } catch (e) { diff --git a/frontend/appflowy_web_app/src/components/document/document_header/utils.ts b/frontend/appflowy_web_app/src/components/document/document_header/utils.ts new file mode 100644 index 0000000000000..fe2c0acbe0090 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/document/document_header/utils.ts @@ -0,0 +1,28 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-expect-error +import ColorThief from 'colorthief'; + +const colorThief = new ColorThief(); + +export function calculateTextColor(rgb: [number, number, number]): string { + const [r, g, b] = rgb; + const brightness = (r * 299 + g * 587 + b * 114) / 1000; + + return brightness > 125 ? 'black' : 'white'; +} + +export async function showColorsForImage(image: HTMLImageElement) { + const img = new Image(); + + img.crossOrigin = 'Anonymous'; // Handle CORS + img.src = image.src; + + await new Promise((resolve, reject) => { + img.onload = resolve; + img.onerror = reject; + }); + + const dominantColor = colorThief.getColor(img); + + return calculateTextColor(dominantColor); +} diff --git a/frontend/appflowy_web_app/src/components/editor/CollaborativeEditor.tsx b/frontend/appflowy_web_app/src/components/editor/CollaborativeEditor.tsx index f55dde01a4c7e..8820296780a10 100644 --- a/frontend/appflowy_web_app/src/components/editor/CollaborativeEditor.tsx +++ b/frontend/appflowy_web_app/src/components/editor/CollaborativeEditor.tsx @@ -1,8 +1,5 @@ -import { CollabOrigin, YjsFolderKey } from '@/application/collab.type'; -import { useViewSelector } from '@/application/folder-yjs'; +import { CollabOrigin } from '@/application/collab.type'; 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'; @@ -16,15 +13,20 @@ const defaultInitialValue: Descendant[] = []; function CollaborativeEditor({ doc }: { doc: Y.Doc }) { const context = useEditorContext(); // if readOnly, collabOrigin is Local, otherwise RemoteSync - const collabOrigin = context.readOnly ? CollabOrigin.Local : CollabOrigin.LocalSync; + const localOrigin = context.readOnly ? CollabOrigin.Local : CollabOrigin.LocalSync; const editor = useMemo( - () => doc && (withPlugins(withReact(withYjs(createEditor(), doc, collabOrigin))) as YjsEditor), - [doc, collabOrigin] + () => + doc && + (withPlugins( + withReact( + withYjs(createEditor(), doc, { + localOrigin, + }) + ) + ) as YjsEditor), + [doc, localOrigin] ); - const [connected, setIsConnected] = useState(false); - const viewId = useId()?.objectId || ''; - const { view } = useViewSelector(viewId); - const title = view?.get(YjsFolderKey.name); + const [, setIsConnected] = useState(false); useEffect(() => { if (!editor) return; @@ -36,11 +38,6 @@ function CollaborativeEditor({ doc }: { doc: Y.Doc }) { }; }, [editor]); - useEffect(() => { - if (!editor || !connected) return; - CustomEditor.setDocumentTitle(editor, title || ''); - }, [editor, title, connected]); - return ( <Slate editor={editor} initialValue={defaultInitialValue}> <EditorEditable editor={editor} /> 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 12c2feb435335..766e1e6d29e09 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 7777973061dea..bbb30d2dc0b55 100644 --- a/frontend/appflowy_web_app/src/components/editor/Editor.tsx +++ b/frontend/appflowy_web_app/src/components/editor/Editor.tsx @@ -1,12 +1,18 @@ import { YDoc } from '@/application/collab.type'; import CollaborativeEditor from '@/components/editor/CollaborativeEditor'; -import { EditorContextProvider } from '@/components/editor/EditorContext'; +import { defaultLayoutStyle, EditorContextProvider, EditorLayoutStyle } from '@/components/editor/EditorContext'; import React from 'react'; import './editor.scss'; -export const Editor = ({ readOnly, doc }: { readOnly: boolean; doc: YDoc }) => { +export interface EditorProps { + readOnly: boolean; + doc: YDoc; + layoutStyle?: EditorLayoutStyle; +} + +export const Editor = ({ readOnly, doc, layoutStyle = defaultLayoutStyle }: EditorProps) => { return ( - <EditorContextProvider readOnly={readOnly}> + <EditorContextProvider layoutStyle={layoutStyle} readOnly={readOnly}> <CollaborativeEditor doc={doc} /> </EditorContextProvider> ); diff --git a/frontend/appflowy_web_app/src/components/editor/EditorContext.tsx b/frontend/appflowy_web_app/src/components/editor/EditorContext.tsx index 7b4162891e990..c360c969dddc3 100644 --- a/frontend/appflowy_web_app/src/components/editor/EditorContext.tsx +++ b/frontend/appflowy_web_app/src/components/editor/EditorContext.tsx @@ -1,11 +1,26 @@ +import { FontLayout, LineHeightLayout } from '@/application/collab.type'; import { createContext, useContext } from 'react'; +export interface EditorLayoutStyle { + fontLayout: FontLayout; + font: string; + lineHeightLayout: LineHeightLayout; +} + +export const defaultLayoutStyle: EditorLayoutStyle = { + fontLayout: FontLayout.normal, + font: '', + lineHeightLayout: LineHeightLayout.normal, +}; + interface EditorContextState { readOnly: boolean; + layoutStyle: EditorLayoutStyle; } export const EditorContext = createContext<EditorContextState>({ readOnly: true, + layoutStyle: defaultLayoutStyle, }); export const EditorContextProvider = ({ children, ...props }: EditorContextState & { children: React.ReactNode }) => { 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 a60255f951230..4b4f02ea6b03f 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 6f4f9b53edb55..0c72c971d88d5 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 4a3b0be961a3e..be86b4bd98eeb 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/DatabaseBlock.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/DatabaseBlock.tsx new file mode 100644 index 0000000000000..5e9fc6a37d518 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/DatabaseBlock.tsx @@ -0,0 +1,94 @@ +import { ReactComponent as ExpandMoreIcon } from '$icons/16x/full_view.svg'; +import { useNavigateToView } from '@/application/folder-yjs'; +import { IdProvider, useId } from '@/components/_shared/context-provider/IdProvider'; +import { Database } from '@/components/database'; +import { DatabaseNode, EditorElementProps } from '@/components/editor/editor.type'; +import { Tooltip } from '@mui/material'; +import React, { forwardRef, memo, useCallback, useMemo, useState } 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 navigateToView = useNavigateToView(); + const [isHovering, setIsHovering] = useState(false); + + 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]); + + const handleNavigateToRow = useCallback( + (viewId: string, rowId: string) => { + const url = `/view/${workspaceId}/${viewId}?r=${rowId}`; + + window.open(url, '_blank'); + }, + [workspaceId] + ); + + return ( + <> + <div + {...attributes} + className={`relative w-full cursor-pointer py-2`} + onMouseEnter={() => setIsHovering(true)} + onMouseLeave={() => setIsHovering(false)} + > + <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 relative flex w-full flex-col px-3`}> + {viewId ? ( + <IdProvider workspaceId={workspaceId} objectId={viewId}> + <Database onNavigateToRow={handleNavigateToRow} /> + {isHovering && ( + <div className={'absolute right-4 top-1'}> + <Tooltip placement={'bottom'} title={t('tooltip.openAsPage')}> + <button + color={'primary'} + className={'rounded border border-line-divider bg-bg-body p-1 hover:bg-fill-list-hover'} + onClick={() => { + navigateToView?.(viewId); + }} + > + <ExpandMoreIcon /> + </button> + </Tooltip> + </div> + )} + </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/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/index.ts new file mode 100644 index 0000000000000..8eaf47802595a --- /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/heading/utils.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/heading/utils.ts index bab542fc8469c..22fe53980abe0 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/heading/utils.ts +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/heading/utils.ts @@ -1,17 +1,17 @@ export function getHeadingCssProperty(level: number) { switch (level) { case 1: - return 'text-3xl pt-[10px] pb-[8px] font-bold'; + return 'text-3xl pt-[10px] max-md:pt-[1.5vw] pb-[4px] max-md:pb-[1vw] font-bold max-sm:text-[6vw]'; case 2: - return 'text-2xl pt-[8px] pb-[6px] font-bold'; + return 'text-2xl pt-[8px] max-md:pt-[1vw] pb-[2px] max-md:pb-[0.5vw] font-bold max-sm:text-[5vw]'; case 3: - return 'text-xl pt-[4px] font-bold'; + return 'text-xl pt-[4px] font-bold max-sm:text-[4vw]'; case 4: return 'text-lg pt-[4px] font-bold'; case 5: - return 'text-base pt-[4px] font-bold'; + return 'pt-[4px] font-bold'; case 6: - return 'text-sm pt-[4px] font-bold'; + return 'pt-[4px] font-bold'; default: return ''; } 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 db00aeb777bfe..86eb6117adce1 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 new file mode 100644 index 0000000000000..d10b1720205a4 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/math-equation/index.ts @@ -0,0 +1,3 @@ +import { lazy } from 'react'; + +export const MathEquation = lazy(() => import('./MathEquation')); 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 deleted file mode 100644 index 27b52f50f2416..0000000000000 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/math_equation/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { lazy } from 'react'; - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-expect-error -export const MathEquation = lazy(() => import('./MathEquation?chunkName=formula')); 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/Placeholder.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/text/Placeholder.tsx index 44c1391e13b90..08a66de9d21c8 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/text/Placeholder.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/text/Placeholder.tsx @@ -10,6 +10,7 @@ function Placeholder({ node, ...attributes }: { node: Element; className?: strin const { t } = useTranslation(); const { readOnly } = useEditorContext(); const editor = useSlate(); + const selected = useSelected() && !readOnly && !!editor.selection && Range.isCollapsed(editor.selection); const [isComposing, setIsComposing] = useState(false); const block = useMemo(() => { @@ -33,7 +34,7 @@ function Placeholder({ node, ...attributes }: { node: Element; className?: strin const unSelectedPlaceholder = useMemo(() => { switch (block?.type) { case BlockType.Paragraph: { - if (editor.children.length === 1) { + if (editor.children.length === 1 && !readOnly) { return t('editor.slashPlaceHolder'); } @@ -73,7 +74,7 @@ function Placeholder({ node, ...attributes }: { node: Element; className?: strin default: return ''; } - }, [block, t, editor.children.length]); + }, [readOnly, block, t, editor.children.length]); const selectedPlaceholder = useMemo(() => { switch (block?.type) { @@ -122,7 +123,7 @@ function Placeholder({ node, ...attributes }: { node: Element; className?: strin return ( <span - data-placeholder={selected ? selectedPlaceholder : unSelectedPlaceholder} + data-placeholder={selected && !readOnly ? selectedPlaceholder : unSelectedPlaceholder} contentEditable={false} {...attributes} className={className} 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 119c6fe9fe671..2ab5996b09622 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/text/Text.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/text/Text.tsx index 854789969c3f8..cfe167b6e22bb 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/text/Text.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/text/Text.tsx @@ -11,13 +11,13 @@ export const Text = memo( const { hasStartIcon, renderIcon } = useStartIcon(node); const editor = useSlateStatic(); const isEmpty = editor.isEmpty(node); - const className = useMemo( - () => - `text-element relative my-1 flex w-full whitespace-pre-wrap break-all px-1 ${classNameProp ?? ''} ${ - hasStartIcon ? 'has-start-icon' : '' - }`, - [classNameProp, hasStartIcon] - ); + const className = useMemo(() => { + const classList = ['text-element', 'relative', 'flex', 'w-full', 'whitespace-pre-wrap', 'break-all', 'px-1']; + + if (classNameProp) classList.push(classNameProp); + if (hasStartIcon) classList.push('has-start-icon'); + return classList.join(' '); + }, [classNameProp, hasStartIcon]); return ( <span {...attributes} ref={ref} className={className}> 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 100% 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 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 99% 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 index ab3a437b783c8..f0356d4cbfe5f 100644 --- 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 @@ -7,7 +7,7 @@ export const TodoList = memo( const className = useMemo(() => { return `flex w-full flex-col ${checked ? 'checked' : ''} ${attributes.className ?? ''}`; }, [attributes.className, checked]); - + return ( <div {...attributes} ref={ref} className={className}> {children} 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 100% 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 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 f535bde10c222..93712b01ef00f 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,27 +1,31 @@ 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 { ElementFallbackRender } from '@/components/error/ElementFallbackRender'; +import { Skeleton } from '@mui/material'; +import { ErrorBoundary } from 'react-error-boundary'; +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'; import { EditorElementProps, TextNode } from '@/components/editor/editor.type'; import { renderColor } from '@/utils/color'; -import React, { FC, useMemo } from 'react'; +import React, { FC, Suspense, useMemo } from 'react'; import { RenderElementProps } from 'slate-react'; +import { DatabaseBlock } from 'src/components/editor/components/blocks/database'; export const Element = ({ element: node, @@ -64,6 +68,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; } @@ -113,10 +121,14 @@ export const Element = ({ } return ( - <div {...attributes} data-block-type={node.type} className={className}> - <Component style={style} className={`flex w-full flex-col`} node={node}> - {children} - </Component> - </div> + <Suspense fallback={<Skeleton width={'100%'} height={24} />}> + <ErrorBoundary fallbackRender={ElementFallbackRender}> + <div {...attributes} data-block-type={node.type} className={className}> + <Component style={style} className={`flex w-full flex-col`} node={node}> + {children} + </Component> + </div> + </ErrorBoundary> + </Suspense> ); }; 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 714b41db97828..38ba65e3f2ab2 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/MentionPage.tsx b/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionPage.tsx index e4df335a3bb99..40e6d31e239d1 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionPage.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionPage.tsx @@ -1,20 +1,15 @@ -import { layoutMap, ViewLayout, YjsFolderKey } from '@/application/collab.type'; -import { useId } from '@/components/_shared/context-provider/IdProvider'; +import { useNavigateToView } from '@/application/folder-yjs'; import { usePageInfo } from '@/components/_shared/page/usePageInfo'; import React from 'react'; -import { useNavigate } from 'react-router-dom'; function MentionPage({ pageId }: { pageId: string }) { - const navigate = useNavigate(); - const { workspaceId } = useId(); - const { view, icon, name } = usePageInfo(pageId); + const onNavigateToView = useNavigateToView(); + const { icon, name } = usePageInfo(pageId); return ( <span onClick={() => { - const layout = parseInt(view?.get(YjsFolderKey.layout) ?? '0') as ViewLayout; - - navigate(`/workspace/${workspaceId}/${layoutMap[layout]}/${pageId}`); + onNavigateToView?.(pageId); }} className={`mention-inline px-1 underline`} contentEditable={false} diff --git a/frontend/appflowy_web_app/src/components/editor/editor.scss b/frontend/appflowy_web_app/src/components/editor/editor.scss index 338e21de7bfb1..71832ec330d60 100644 --- a/frontend/appflowy_web_app/src/components/editor/editor.scss +++ b/frontend/appflowy_web_app/src/components/editor/editor.scss @@ -52,6 +52,7 @@ [role=textbox] { .text-element { + @apply my-1; &::selection { @apply bg-transparent; } @@ -197,21 +198,18 @@ 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; } } -.grid-block .grid-scroll-container::-webkit-scrollbar { - width: 0; - height: 0; -} - .image-render { .image-resizer { @apply absolute w-[10px] top-0 z-10 flex h-full cursor-col-resize items-center justify-end; @@ -238,7 +236,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; } } } @@ -267,7 +265,39 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { } .mention-content { - @apply ml-5; + @apply ml-6; + } + +} + +.text-block-icon { + @apply flex items-center justify-center; +} + + +.font-small { + .text-element { + line-height: 1.7; + } +} + + +.font-large { + .text-element { + line-height: 1.2; + } +} + +.line-height-large { + .text-element { + margin-top: 6px; + margin-bottom: 6px; } +} -} \ No newline at end of file +.line-height-small { + .text-element { + margin-top: 0px; + margin-bottom: 0px; + } +} 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 fec9ffbcbf146..d21f75cd3a2fb 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/ElementFallbackRender.tsx b/frontend/appflowy_web_app/src/components/error/ElementFallbackRender.tsx new file mode 100644 index 0000000000000..ddfdac39f1bc6 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/error/ElementFallbackRender.tsx @@ -0,0 +1,11 @@ +import { Alert } from '@mui/material'; +import { FallbackProps } from 'react-error-boundary'; + +export function ElementFallbackRender({ error }: FallbackProps) { + return ( + <Alert severity={'error'} variant={'standard'} className={'my-2'}> + <p>Something went wrong:</p> + <pre>{error.message}</pre> + </Alert> + ); +} diff --git a/frontend/appflowy_web_app/src/components/folder/ViewItem.tsx b/frontend/appflowy_web_app/src/components/folder/ViewItem.tsx index 7465857b17a1f..49feb382e2213 100644 --- a/frontend/appflowy_web_app/src/components/folder/ViewItem.tsx +++ b/frontend/appflowy_web_app/src/components/folder/ViewItem.tsx @@ -1,19 +1,15 @@ -import { layoutMap, ViewLayout, YjsFolderKey } from '@/application/collab.type'; +import { useNavigateToView } from '@/application/folder-yjs'; 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(); - const { pathname } = useLocation(); + const onNavigateToView = useNavigateToView(); return ( <div className={'cursor-pointer border-b border-line-border py-4 px-2'}> <Page - onClick={(view) => { - const layout = parseInt(view?.get(YjsFolderKey.layout) ?? '0') as ViewLayout; - - navigate(`${pathname}/${layoutMap[layout]}/${id}`); + onClick={() => { + onNavigateToView?.(id); }} id={id} /> diff --git a/frontend/appflowy_web_app/src/components/layout/Header.tsx b/frontend/appflowy_web_app/src/components/layout/Header.tsx index 5c874db35c8fd..df87892b420dd 100644 --- a/frontend/appflowy_web_app/src/components/layout/Header.tsx +++ b/frontend/appflowy_web_app/src/components/layout/Header.tsx @@ -2,10 +2,9 @@ import { downloadPage, openAppFlowySchema, openUrl } from '@/utils/url'; import { Button } from '@mui/material'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useParams } from 'react-router-dom'; -import Page from 'src/components/_shared/page/Page'; import { ReactComponent as Logo } from '@/assets/logo.svg'; import Popover, { PopoverOrigin } from '@mui/material/Popover'; +import Breadcrumb from 'src/components/layout/breadcrumb/Breadcrumb'; const popoverOrigin: { anchorOrigin: PopoverOrigin; @@ -22,14 +21,13 @@ const popoverOrigin: { }; function Header() { - const { objectId } = useParams(); const { t } = useTranslation(); const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null); return ( <div className={'appflowy-top-bar flex h-[64px] p-4'}> - <div className={'flex flex-1 items-center justify-between'}> - <div className={'flex-1'}>{objectId && <Page id={objectId} />}</div> + <div className={'flex w-full items-center justify-between overflow-hidden'}> + <Breadcrumb /> <Button className={'border-line-border'} diff --git a/frontend/appflowy_web_app/src/components/layout/Layout.hooks.ts b/frontend/appflowy_web_app/src/components/layout/Layout.hooks.ts new file mode 100644 index 0000000000000..0313015d3fab6 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/layout/Layout.hooks.ts @@ -0,0 +1,96 @@ +import { YFolder, YjsEditorKey, YjsFolderKey } from '@/application/collab.type'; +import { Crumb } from '@/application/folder-yjs'; +import { AFConfigContext } from '@/components/app/AppConfig'; +import { useCallback, useContext, useEffect, useState } from 'react'; +import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; + +export function useLayout() { + const { workspaceId, objectId } = useParams(); + const [search] = useSearchParams(); + const folderService = useContext(AFConfigContext)?.service?.folderService; + const [folder, setFolder] = useState<YFolder | null>(null); + const views = folder?.get(YjsFolderKey.views); + const view = objectId ? views?.get(objectId) : null; + const [crumbs, setCrumbs] = useState<Crumb[]>([]); + + const getFolder = useCallback( + async (workspaceId: string) => { + const folder = (await folderService?.openWorkspace(workspaceId)) + ?.getMap(YjsEditorKey.data_section) + .get(YjsEditorKey.folder); + + if (!folder) return; + + console.log(folder.toJSON()); + setFolder(folder); + }, + [folderService] + ); + + useEffect(() => { + if (!workspaceId) return; + + void getFolder(workspaceId); + }, [getFolder, workspaceId]); + + const navigate = useNavigate(); + + const handleNavigateToView = useCallback( + (viewId: string) => { + const view = folder?.get(YjsFolderKey.views)?.get(viewId); + + if (!view) return; + navigate(`/view/${workspaceId}/${viewId}`); + }, + [folder, navigate, workspaceId] + ); + + const onChangeBreadcrumb = useCallback(() => { + if (!view) return; + const queue = [view]; + let parentId = view.get(YjsFolderKey.bid); + + while (parentId) { + const parent = views?.get(parentId); + + if (!parent) break; + + queue.unshift(parent); + parentId = parent?.get(YjsFolderKey.bid); + } + + setCrumbs( + queue + .map((view) => { + let icon = view.get(YjsFolderKey.icon); + + try { + icon = JSON.parse(icon || '')?.value; + } catch (e) { + // do nothing + } + + return { + viewId: view.get(YjsFolderKey.id), + name: view.get(YjsFolderKey.name), + icon: icon || '', + }; + }) + .slice(1) + ); + }, [view, views]); + + useEffect(() => { + onChangeBreadcrumb(); + + view?.observe(onChangeBreadcrumb); + views?.observe(onChangeBreadcrumb); + + return () => { + view?.unobserve(onChangeBreadcrumb); + views?.unobserve(onChangeBreadcrumb); + }; + }, [search, onChangeBreadcrumb, view, views]); + + return { folder, handleNavigateToView, crumbs, setCrumbs }; +} diff --git a/frontend/appflowy_web_app/src/components/layout/Layout.tsx b/frontend/appflowy_web_app/src/components/layout/Layout.tsx index c10048aa8213f..dc3f075f69e7a 100644 --- a/frontend/appflowy_web_app/src/components/layout/Layout.tsx +++ b/frontend/appflowy_web_app/src/components/layout/Layout.tsx @@ -1,36 +1,23 @@ -import { YFolder, YjsEditorKey } from '@/application/collab.type'; import { FolderProvider } from '@/components/_shared/context-provider/FolderProvider'; -import { AFConfigContext } from '@/components/app/AppConfig'; import Header from '@/components/layout/Header'; import { AFScroller } from '@/components/_shared/scroller'; -import React, { useCallback, useContext, useEffect, useState } from 'react'; -import { useParams } from 'react-router-dom'; +import { useLayout } from '@/components/layout/Layout.hooks'; +import React from 'react'; import './layout.scss'; +import { ReactComponent as Logo } from '@/assets/logo.svg'; function Layout({ children }: { children: React.ReactNode }) { - const { workspaceId } = useParams(); - const folderService = useContext(AFConfigContext)?.service?.folderService; - const [folder, setFolder] = useState<YFolder | null>(null); - const getFolder = useCallback( - async (workspaceId: string) => { - const folder = (await folderService?.openWorkspace(workspaceId)) - ?.getMap(YjsEditorKey.data_section) - .get(YjsEditorKey.folder); + const { folder, handleNavigateToView, crumbs, setCrumbs } = useLayout(); - if (!folder) return; + if (!folder) + return ( + <div className={'flex h-screen w-screen items-center justify-center'}> + <Logo className={'h-20 w-20'} /> + </div> + ); - setFolder(folder); - }, - [folderService] - ); - - useEffect(() => { - if (!workspaceId) return; - - void getFolder(workspaceId); - }, [getFolder, workspaceId]); return ( - <FolderProvider folder={folder}> + <FolderProvider setCrumbs={setCrumbs} crumbs={crumbs} onNavigateToView={handleNavigateToView} folder={folder}> <Header /> <AFScroller overflowXHidden diff --git a/frontend/appflowy_web_app/src/components/layout/breadcrumb/Breadcrumb.tsx b/frontend/appflowy_web_app/src/components/layout/breadcrumb/Breadcrumb.tsx new file mode 100644 index 0000000000000..b5a56b4527a5e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/layout/breadcrumb/Breadcrumb.tsx @@ -0,0 +1,24 @@ +import { useCrumbs } from '@/application/folder-yjs'; +import Item from '@/components/layout/breadcrumb/Item'; +import React, { useMemo } from 'react'; + +export function Breadcrumb() { + const crumbs = useCrumbs(); + + const renderCrumb = useMemo(() => { + return crumbs?.map((crumb, index) => { + const isLast = index === crumbs.length - 1; + + return ( + <React.Fragment key={crumb.viewId}> + <Item crumb={crumb} disableClick={isLast} /> + {!isLast && <span>/</span>} + </React.Fragment> + ); + }); + }, [crumbs]); + + return <div className={'flex flex-1 items-center gap-2 overflow-hidden'}>{renderCrumb}</div>; +} + +export default Breadcrumb; diff --git a/frontend/appflowy_web_app/src/components/layout/breadcrumb/Item.tsx b/frontend/appflowy_web_app/src/components/layout/breadcrumb/Item.tsx new file mode 100644 index 0000000000000..cf9f697ff6644 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/layout/breadcrumb/Item.tsx @@ -0,0 +1,27 @@ +import { Crumb, useNavigateToView } from '@/application/folder-yjs'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +function Item({ crumb, disableClick = false }: { crumb: Crumb; disableClick?: boolean }) { + const { viewId, icon, name } = crumb; + + const { t } = useTranslation(); + const onNavigateToView = useNavigateToView(); + + return ( + <div + className={`flex items-center gap-1 ${!disableClick ? 'cursor-pointer' : 'flex-1 overflow-hidden'}`} + onClick={() => { + if (disableClick) return; + onNavigateToView?.(viewId); + }} + > + {icon} + <span className={!disableClick ? 'underline' : 'flex-1 truncate'}> + {name || t('menuAppHeader.defaultNewPageName')} + </span> + </div> + ); +} + +export default Item; diff --git a/frontend/appflowy_web_app/src/components/layout/breadcrumb/index.ts b/frontend/appflowy_web_app/src/components/layout/breadcrumb/index.ts new file mode 100644 index 0000000000000..116446358b6e1 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/layout/breadcrumb/index.ts @@ -0,0 +1 @@ +export * from './Breadcrumb'; diff --git a/frontend/appflowy_web_app/src/components/layout/layout.scss b/frontend/appflowy_web_app/src/components/layout/layout.scss index bac3baae6965e..16a9a92a08c9a 100644 --- a/frontend/appflowy_web_app/src/components/layout/layout.scss +++ b/frontend/appflowy_web_app/src/components/layout/layout.scss @@ -16,51 +16,45 @@ box-shadow: var(--line-border) 0px 0px 0px 1px inset !important; } -.appflowy-date-picker-calendar { - width: 100%; +@mixin hidden-scrollbar { + &::-webkit-scrollbar { + display: none; + } + -ms-overflow-style: none; + scrollbar-width: none; // For Firefox } -.grid-sticky-header::-webkit-scrollbar { - width: 0; - height: 0; -} +body { + &[data-os="windows"]:not([data-browser="firefox"]) { + .appflowy-custom-scroller { + @include hidden-scrollbar; + } + } -.grid-scroll-container::-webkit-scrollbar { - width: 0; - height: 0; + .grid-sticky-header { + @include hidden-scrollbar; + } } -.appflowy-scroll-container { - &::-webkit-scrollbar { - width: 0; - height: 0; - } +.appflowy-date-picker-calendar { + width: 100%; } + .appflowy-scrollbar-thumb-horizontal, .appflowy-scrollbar-thumb-vertical { background-color: var(--scrollbar-thumb); border-radius: 4px; opacity: 60%; } -.workspaces, .database-conditions, .grid-scroll-table { - ::-webkit-scrollbar { - width: 0; - height: 0; - } -} - .view-icon { - @apply flex w-fit cursor-pointer rounded-lg py-2 text-6xl; + @apply flex w-fit leading-[1.5em] cursor-pointer rounded-lg py-2 text-[1.5em]; 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 { @@ -79,3 +73,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 index e1fbfb8067c50..f5b4839924f80 100644 --- a/frontend/appflowy_web_app/src/pages/DatabasePage.tsx +++ b/frontend/appflowy_web_app/src/pages/DatabasePage.tsx @@ -1,10 +1,25 @@ -import { Database } from '@/components/database'; +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} />; + } -function DatabasePage () { return ( - <Database /> + <div className={'relative flex h-full w-full flex-col'}> + <DatabaseHeader viewId={objectId} /> + <Database /> + </div> ); } -export default DatabasePage; \ No newline at end of file +export default DatabasePage; diff --git a/frontend/appflowy_web_app/src/pages/LoginPage.tsx b/frontend/appflowy_web_app/src/pages/LoginPage.tsx index 5d2dcceee4fff..a4ded1d5e38aa 100644 --- a/frontend/appflowy_web_app/src/pages/LoginPage.tsx +++ b/frontend/appflowy_web_app/src/pages/LoginPage.tsx @@ -13,7 +13,7 @@ function LoginPage() { const workspaceId = currentUser.user?.workspaceId; if (!redirect || redirect === '/') { - return navigate(`/workspace/${workspaceId}`); + return navigate(`/view/${workspaceId}`); } navigate(`${redirect}`); diff --git a/frontend/appflowy_web_app/src/pages/ProductPage.tsx b/frontend/appflowy_web_app/src/pages/ProductPage.tsx index f7b5615a77bcb..385e7ea3f5d69 100644 --- a/frontend/appflowy_web_app/src/pages/ProductPage.tsx +++ b/frontend/appflowy_web_app/src/pages/ProductPage.tsx @@ -1,44 +1,33 @@ -import { CollabType } from '@/application/collab.type'; +import { ViewLayout } from '@/application/collab.type'; +import { useViewLayout } from '@/application/folder-yjs'; import { IdProvider } from '@/components/_shared/context-provider/IdProvider'; -import DatabasePage from '@/pages/DatabasePage'; -import React, { useMemo } from 'react'; +import React, { lazy, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import DocumentPage from '@/pages/DocumentPage'; -enum URL_COLLAB_TYPE { - DOCUMENT = 'document', - GRID = 'grid', - BOARD = 'board', - CALENDAR = 'calendar', -} - -const collabTypeMap: Record<string, CollabType> = { - [URL_COLLAB_TYPE.DOCUMENT]: CollabType.Document, - [URL_COLLAB_TYPE.GRID]: CollabType.WorkspaceDatabase, - [URL_COLLAB_TYPE.BOARD]: CollabType.WorkspaceDatabase, - [URL_COLLAB_TYPE.CALENDAR]: CollabType.WorkspaceDatabase, -}; +const DatabasePage = lazy(() => import('./DatabasePage')); function ProductPage() { - const { workspaceId, type, objectId } = useParams(); + const { workspaceId, objectId } = useParams(); + const type = useViewLayout(); + const PageComponent = useMemo(() => { switch (type) { - case URL_COLLAB_TYPE.DOCUMENT: + case ViewLayout.Document: return DocumentPage; - case URL_COLLAB_TYPE.GRID: - case URL_COLLAB_TYPE.BOARD: - case URL_COLLAB_TYPE.CALENDAR: + case ViewLayout.Grid: + case ViewLayout.Board: + case ViewLayout.Calendar: return DatabasePage; default: return null; } }, [type]); - console.log(workspaceId, type, objectId); - if (!workspaceId || !type || !objectId) return null; + if (!workspaceId || !objectId) return null; return ( - <IdProvider workspaceId={workspaceId} objectId={objectId} collabType={collabTypeMap[type]}> + <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 b255483f4edc4..82a597519f7d5 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; } @@ -63,3 +48,12 @@ th { @apply text-left font-normal; } +html { + font-size: 16px; +} + +@media (max-width: 600px) { + html { + font-size: 14px; + } +} \ No newline at end of file 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 6753969ca0f9a..d52ac6e96dbb9 100644 --- a/frontend/appflowy_web_app/src/styles/variables/dark.variables.css +++ b/frontend/appflowy_web_app/src/styles/variables/dark.variables.css @@ -1,121 +1,51 @@ -/** -* Do not edit directly -* 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: #e5e5e5; - --base-light-neutral-200: #e2e4eb; - --base-light-neutral-300: #f2f2f2; - --base-light-neutral-400: #e0e0e0; - --base-light-neutral-500: #bdbdbd; - --base-light-neutral-600: #828282; - --base-light-neutral-700: #4f4f4f; - --base-light-neutral-800: #333333; - --base-light-neutral-900: #1f2329; - --base-light-neutral-1000: #000000; - --base-light-neutral-00: #ffffff; - --base-light-blue-50: #f2fcff; - --base-light-blue-100: #e0f8ff; - --base-light-blue-200: #a6ecff; - --base-light-blue-300: #52d1f4; - --base-light-blue-400: #00bcf0; - --base-light-blue-500: #05ade2; - --base-light-blue-600: #009fd1; - --base-light-color-deep-red: #fb006d; - --base-light-color-deep-yellow: #ffd667; - --base-light-color-deep-green: #66cf80; - --base-light-color-deep-blue: #00bcf0; - --base-light-color-light-purple: #e8e0ff; - --base-light-color-light-pink: #ffe7ee; - --base-light-color-light-orange: #ffefe3; - --base-light-color-light-yellow: #fff2cd; - --base-light-color-light-lime: #f5ffdc; - --base-light-color-light-green: #ddffd6; - --base-light-color-light-aqua: #defff1; - --base-light-color-light-blue: #e1fbff; - --base-light-color-light-red: #ffdddd; - --base-black-neutral-100: #252F41; - --base-black-neutral-200: #313c51; - --base-black-neutral-300: #3c4557; - --base-black-neutral-400: #525A69; - --base-black-neutral-500: #59647a; - --base-black-neutral-600: #87A0BF; - --base-black-neutral-700: #99a6b8; - --base-black-neutral-800: #e2e9f2; - --base-black-neutral-900: #eff4fb; - --base-black-neutral-1000: #ffffff; - --base-black-neutral-n50: #232b38; - --base-black-neutral-n00: #1a202c; - --base-black-blue-50: #232b38; - --base-black-blue-100: #005174; - --base-black-blue-200: #a6ecff; - --base-black-blue-300: #52d1f4; - --base-black-blue-400: #00bcf0; - --base-black-blue-500: #05ade2; - --base-black-blue-600: #009fd1; - --base-black-color-deep-red: #d32772; - --base-black-color-deep-yellow: #e9b320; - --base-black-color-deep-green: #3ba856; - --base-black-color-deep-blue: #2e9dbb; - --base-black-color-light-purple: #4D4078; - --base-black-color-light-blue: #2C3B58; - --base-black-color-light-green: #3C5133; - --base-black-color-light-yellow: #695E3E; - --base-black-color-light-pink: #5E3C5E; - --base-black-color-light-red: #56363F; - --base-black-color-light-aqua: #1B3849; - --base-black-color-light-lime: #394027; - --base-black-color-light-orange: #5E3C3C; - --base-else-brand: #2c144b; - --text-title: #e2e9f2; - --text-caption: #87A0BF; - --text-placeholder: #3c4557; - --text-link-default: #00bcf0; - --text-link-hover: #52d1f4; - --text-link-pressed: #009fd1; - --text-link-disabled: #005174; - --icon-primary: #e2e9f2; - --icon-secondary: #59647a; - --icon-disabled: #525A69; - --icon-on-toolbar: white; - --line-border: #59647a; - --line-divider: #252F41; - --line-on-toolbar: #99a6b8; - --fill-default: #00bcf0; - --fill-hover: #005174; - --fill-toolbar: #0F111C; - --fill-selector: #232b38; - --fill-list-active: #3c4557; - --fill-list-hover: #005174; - --content-blue-400: #00bcf0; - --content-blue-300: #52d1f4; - --content-blue-600: #009fd1; - --content-blue-100: #005174; - --content-on-fill: #1a202c; - --content-on-tag: #99a6b8; - --content-blue-50: #232b38; - --bg-body: #1a202c; - --bg-base: #232b38; - --bg-mask: rgba(0,0,0,0.7); - --bg-tips: #005174; - --bg-brand: #2c144b; - --function-error: #d32772; - --function-warning: #e9b320; - --function-success: #3ba856; - --function-info: #2e9dbb; - --tint-red: #56363F; - --tint-green: #3C5133; - --tint-purple: #4D4078; - --tint-blue: #2C3B58; - --tint-yellow: #695E3E; - --tint-pink: #5E3C5E; - --tint-lime: #394027; - --tint-aqua: #1B3849; - --tint-orange: #5E3C3C; - --shadow: 0px 0px 25px 0px rgba(0,0,0,0.3); - --scrollbar-track: #252F41; - --scrollbar-thumb: #3c4557; + --text-title: #e2e9f2; + --text-caption: #87A0BF; + --text-placeholder: #3c4557; + --text-link-default: #00bcf0; + --text-link-hover: #52d1f4; + --text-link-pressed: #009fd1; + --text-link-disabled: #005174; + --icon-primary: #e2e9f2; + --icon-secondary: #59647a; + --icon-disabled: #525A69; + --icon-on-toolbar: white; + --line-border: #59647a; + --line-divider: #252F41; + --line-on-toolbar: #99a6b8; + --fill-default: #00bcf0; + --fill-hover: #005174; + --fill-toolbar: #0F111C; + --fill-selector: #232b38; + --fill-list-active: #3c4557; + --fill-list-hover: #005174; + --content-blue-400: #00bcf0; + --content-blue-300: #52d1f4; + --content-blue-600: #009fd1; + --content-blue-100: #005174; + --content-on-fill: #1a202c; + --content-on-tag: #99a6b8; + --content-blue-50: #232b38; + --bg-body: #1a202c; + --bg-base: #232b38; + --bg-mask: rgba(0, 0, 0, 0.7); + --bg-tips: #005174; + --bg-brand: #2c144b; + --function-error: #d32772; + --function-warning: #e9b320; + --function-success: #3ba856; + --function-info: #2e9dbb; + --tint-red: #56363F; + --tint-green: #3C5133; + --tint-purple: #4D4078; + --tint-blue: #2C3B58; + --tint-yellow: #695E3E; + --tint-pink: #5E3C5E; + --tint-lime: #394027; + --tint-aqua: #1B3849; + --tint-orange: #5E3C3C; + --shadow: 0px 0px 25px 0px rgba(0, 0, 0, 0.3); + --scrollbar-track: #252F41; + --scrollbar-thumb: #3c4557; } \ No newline at end of file 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 b1494114bdc2a..bae74c9b3ef71 100644 --- a/frontend/appflowy_web_app/src/styles/variables/light.variables.css +++ b/frontend/appflowy_web_app/src/styles/variables/light.variables.css @@ -1,124 +1,54 @@ -/** -* Do not edit directly -* 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: #e5e5e5; - --base-light-neutral-200: #e2e4eb; - --base-light-neutral-300: #f2f2f2; - --base-light-neutral-400: #e0e0e0; - --base-light-neutral-500: #bdbdbd; - --base-light-neutral-600: #828282; - --base-light-neutral-700: #4f4f4f; - --base-light-neutral-800: #333333; - --base-light-neutral-900: #1f2329; - --base-light-neutral-1000: #000000; - --base-light-neutral-00: #ffffff; - --base-light-blue-50: #f2fcff; - --base-light-blue-100: #e0f8ff; - --base-light-blue-200: #a6ecff; - --base-light-blue-300: #52d1f4; - --base-light-blue-400: #00bcf0; - --base-light-blue-500: #05ade2; - --base-light-blue-600: #009fd1; - --base-light-color-deep-red: #fb006d; - --base-light-color-deep-yellow: #ffd667; - --base-light-color-deep-green: #66cf80; - --base-light-color-deep-blue: #00bcf0; - --base-light-color-light-purple: #e8e0ff; - --base-light-color-light-pink: #ffe7ee; - --base-light-color-light-orange: #ffefe3; - --base-light-color-light-yellow: #fff2cd; - --base-light-color-light-lime: #f5ffdc; - --base-light-color-light-green: #ddffd6; - --base-light-color-light-aqua: #defff1; - --base-light-color-light-blue: #e1fbff; - --base-light-color-light-red: #ffdddd; - --base-black-neutral-100: #252F41; - --base-black-neutral-200: #313c51; - --base-black-neutral-300: #3c4557; - --base-black-neutral-400: #525A69; - --base-black-neutral-500: #59647a; - --base-black-neutral-600: #87A0BF; - --base-black-neutral-700: #99a6b8; - --base-black-neutral-800: #e2e9f2; - --base-black-neutral-900: #eff4fb; - --base-black-neutral-1000: #ffffff; - --base-black-neutral-n50: #232b38; - --base-black-neutral-n00: #1a202c; - --base-black-blue-50: #232b38; - --base-black-blue-100: #005174; - --base-black-blue-200: #a6ecff; - --base-black-blue-300: #52d1f4; - --base-black-blue-400: #00bcf0; - --base-black-blue-500: #05ade2; - --base-black-blue-600: #009fd1; - --base-black-color-deep-red: #d32772; - --base-black-color-deep-yellow: #e9b320; - --base-black-color-deep-green: #3ba856; - --base-black-color-deep-blue: #2e9dbb; - --base-black-color-light-purple: #4D4078; - --base-black-color-light-blue: #2C3B58; - --base-black-color-light-green: #3C5133; - --base-black-color-light-yellow: #695E3E; - --base-black-color-light-pink: #5E3C5E; - --base-black-color-light-red: #56363F; - --base-black-color-light-aqua: #1B3849; - --base-black-color-light-lime: #394027; - --base-black-color-light-orange: #5E3C3C; - --base-else-brand: #2c144b; - --text-title: #333333; - --text-caption: #828282; - --text-placeholder: #bdbdbd; - --text-disabled: #e0e0e0; - --text-link-default: #00bcf0; - --text-link-hover: #52d1f4; - --text-link-pressed: #009fd1; - --text-link-disabled: #e0f8ff; - --icon-primary: #333333; - --icon-secondary: #59647a; - --icon-disabled: #e0e0e0; - --icon-on-toolbar: #ffffff; - --line-border: #bdbdbd; - --line-divider: #e5e5e5; - --line-on-toolbar: #4f4f4f; - --fill-toolbar: #333333; - --fill-default: #00bcf0; - --fill-hover: #52d1f4; - --fill-pressed: #009fd1; - --fill-active: #e0f8ff; - --fill-list-hover: #e0f8ff; - --fill-list-active: #f9fafd; - --content-blue-400: #00bcf0; - --content-blue-300: #52d1f4; - --content-blue-600: #009fd1; - --content-blue-100: #e0f8ff; - --content-blue-50: #f2fcff; - --content-on-fill-hover: #00bcf0; - --content-on-fill: #ffffff; - --content-on-tag: #4f4f4f; - --bg-body: #ffffff; - --bg-base: #f9fafd; - --bg-mask: rgba(0,0,0,0.55); - --bg-tips: #e0f8ff; - --bg-brand: #2c144b; - --function-error: #fb006d; - --function-waring: #ffd667; - --function-success: #66cf80; - --function-info: #00bcf0; - --tint-purple: #e8e0ff; - --tint-pink: #ffe7ee; - --tint-red: #ffdddd; - --tint-lime: #f5ffdc; - --tint-green: #ddffd6; - --tint-aqua: #defff1; - --tint-blue: #e1fbff; - --tint-orange: #ffefe3; - --tint-yellow: #fff2cd; - --shadow: 0px 0px 10px 0px rgba(0,0,0,0.1); - --scrollbar-thumb: #bdbdbd; - --scrollbar-track: #e5e5e5; + --text-title: #333333; + --text-caption: #828282; + --text-placeholder: #bdbdbd; + --text-disabled: #e0e0e0; + --text-link-default: #00bcf0; + --text-link-hover: #52d1f4; + --text-link-pressed: #009fd1; + --text-link-disabled: #e0f8ff; + --icon-primary: #333333; + --icon-secondary: #59647a; + --icon-disabled: #e0e0e0; + --icon-on-toolbar: #ffffff; + --line-border: #bdbdbd; + --line-divider: #e5e5e5; + --line-on-toolbar: #4f4f4f; + --fill-toolbar: #333333; + --fill-default: #00bcf0; + --fill-hover: #52d1f4; + --fill-pressed: #009fd1; + --fill-active: #e0f8ff; + --fill-list-hover: #e0f8ff; + --fill-list-active: #f9fafd; + --content-blue-400: #00bcf0; + --content-blue-300: #52d1f4; + --content-blue-600: #009fd1; + --content-blue-100: #e0f8ff; + --content-blue-50: #f2fcff; + --content-on-fill-hover: #00bcf0; + --content-on-fill: #ffffff; + --content-on-tag: #4f4f4f; + --bg-body: #ffffff; + --bg-base: #f9fafd; + --bg-mask: rgba(0, 0, 0, 0.55); + --bg-tips: #e0f8ff; + --bg-brand: #2c144b; + --function-error: #fb006d; + --function-waring: #ffd667; + --function-success: #66cf80; + --function-info: #00bcf0; + --tint-purple: #e8e0ff; + --tint-pink: #ffe7ee; + --tint-red: #ffdddd; + --tint-lime: #f5ffdc; + --tint-green: #ddffd6; + --tint-aqua: #defff1; + --tint-blue: #e1fbff; + --tint-orange: #ffefe3; + --tint-yellow: #fff2cd; + --shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.1); + --scrollbar-thumb: #bdbdbd; + --scrollbar-track: #e5e5e5; } \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/utils/platform.ts b/frontend/appflowy_web_app/src/utils/platform.ts index 285e38bc7364a..fc3a5387504c8 100644 --- a/frontend/appflowy_web_app/src/utils/platform.ts +++ b/frontend/appflowy_web_app/src/utils/platform.ts @@ -1,5 +1,6 @@ export function getPlatform() { return { isTauri: !!import.meta.env.TAURI_PLATFORM, + isMobile: window.navigator.userAgent.match(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i), }; } diff --git a/frontend/appflowy_web_app/style-dictionary/config.cjs b/frontend/appflowy_web_app/style-dictionary/config.cjs deleted file mode 100644 index 10d7084060925..0000000000000 --- a/frontend/appflowy_web_app/style-dictionary/config.cjs +++ /dev/null @@ -1,114 +0,0 @@ -const StyleDictionary = require('style-dictionary'); -const fs = require('fs'); -const path = require('path'); - -// Add comment header to generated files -StyleDictionary.registerFormat({ - name: 'css/variables', - formatter: function(dictionary, config) { - const header = `/**\n` + '* Do not edit directly\n' + `* Generated on ${new Date().toUTCString()}\n` + `* Generated from $pnpm css:variables \n` + `*/\n\n`; - const allProperties = dictionary.allProperties; - const properties = allProperties.map(prop => { - const { name, value } = prop; - return ` --${name}: ${value};` - }).join('\n'); - // generate tailwind config - generateTailwindConfig(allProperties); - return header + `:root${this.selector} {\n${properties}\n}` - } -}); - -// expand shadow tokens into a single string -StyleDictionary.registerTransform({ - name: 'shadow/spreadShadow', - type: 'value', - matcher: function (prop) { - return prop.type === 'boxShadow'; - }, - transformer: function (prop) { - // destructure shadow values from original token value - const { x, y, blur, spread, color } = prop.original.value; - - return `${x}px ${y}px ${blur}px ${spread}px ${color}`; - }, -}); - -const transforms = ['attribute/cti', 'name/cti/kebab', 'shadow/spreadShadow']; - -// Generate Light CSS variables -StyleDictionary.extend({ - source: ['./style-dictionary/tokens/base.json', './style-dictionary/tokens/light.json'], - platforms: { - css: { - transformGroup: 'css', - buildPath: './src/styles/variables/', - files: [ - { - format: 'css/variables', - destination: 'light.variables.css', - selector: '', - options: { - outputReferences: true - } - }, - ], - transforms, - }, - }, -}).buildAllPlatforms(); - -// Generate Dark CSS variables -StyleDictionary.extend({ - source: ['./style-dictionary/tokens/base.json', './style-dictionary/tokens/dark.json'], - platforms: { - css: { - transformGroup: 'css', - buildPath: './src/styles/variables/', - files: [ - { - format: 'css/variables', - destination: 'dark.variables.css', - selector: '[data-dark-mode=true]', - }, - ], - transforms, - }, - }, -}).buildAllPlatforms(); - - -function set(obj, path, value) { - const lastKey = path.pop(); - const lastObj = path.reduce((obj, key) => - obj[key] = obj[key] || {}, - obj); - lastObj[lastKey] = value; -} - -function writeFile (file, data) { - const header = `/**\n` + '* Do not edit directly\n' + `* Generated on ${new Date().toUTCString()}\n` + `* Generated from $pnpm css:variables \n` + `*/\n\n`; - const exportString = `module.exports = ${JSON.stringify(data, null, 2)}`; - fs.writeFileSync(path.join(__dirname, file), header + exportString); -} - -function generateTailwindConfig(allProperties) { - const tailwindColors = {}; - const tailwindBoxShadow = {}; - allProperties.forEach(prop => { - const { path, type, name, value } = prop; - if (path[0] === 'Base') { - return; - } - if (type === 'color') { - if (name.includes('fill')) { - console.log(prop); - } - set(tailwindColors, path, `var(--${name})`); - } - if (type === 'boxShadow') { - set(tailwindBoxShadow, ['md'], `var(--${name})`); - } - }); - writeFile('./tailwind/colors.cjs', tailwindColors); - writeFile('./tailwind/box-shadow.cjs', tailwindBoxShadow); -} \ No newline at end of file diff --git a/frontend/appflowy_web_app/style-dictionary/tokens/base.json b/frontend/appflowy_web_app/style-dictionary/tokens/base.json deleted file mode 100644 index f92d39267f284..0000000000000 --- a/frontend/appflowy_web_app/style-dictionary/tokens/base.json +++ /dev/null @@ -1,290 +0,0 @@ -{ - "Base": { - "Light": { - "neutral": { - "50": { - "value": "#f9fafd", - "type": "color" - }, - "100": { - "value": "#e5e5e5", - "type": "color" - }, - "200": { - "value": "#e2e4eb", - "type": "color" - }, - "300": { - "value": "#f2f2f2", - "type": "color" - }, - "400": { - "value": "#e0e0e0", - "type": "color" - }, - "500": { - "value": "#bdbdbd", - "type": "color" - }, - "600": { - "value": "#828282", - "type": "color" - }, - "700": { - "value": "#4f4f4f", - "type": "color" - }, - "800": { - "value": "#333333", - "type": "color" - }, - "900": { - "value": "#1f2329", - "type": "color" - }, - "1000": { - "value": "#000000", - "type": "color" - }, - "00": { - "value": "#ffffff", - "type": "color" - } - }, - "blue": { - "50": { - "value": "#f2fcff", - "type": "color" - }, - "100": { - "value": "#e0f8ff", - "type": "color" - }, - "200": { - "value": "#a6ecff", - "type": "color" - }, - "300": { - "value": "#52d1f4", - "type": "color" - }, - "400": { - "value": "#00bcf0", - "type": "color" - }, - "500": { - "value": "#05ade2", - "type": "color" - }, - "600": { - "value": "#009fd1", - "type": "color" - } - }, - "color": { - "deep": { - "red": { - "value": "#fb006d", - "type": "color" - }, - "yellow": { - "value": "#ffd667", - "type": "color" - }, - "green": { - "value": "#66cf80", - "type": "color" - }, - "blue": { - "value": "#00bcf0", - "type": "color" - } - }, - "light": { - "purple": { - "value": "#e8e0ff", - "type": "color" - }, - "pink": { - "value": "#ffe7ee", - "type": "color" - }, - "orange": { - "value": "#ffefe3", - "type": "color" - }, - "yellow": { - "value": "#fff2cd", - "type": "color" - }, - "lime": { - "value": "#f5ffdc", - "type": "color" - }, - "green": { - "value": "#ddffd6", - "type": "color" - }, - "aqua": { - "value": "#defff1", - "type": "color" - }, - "blue": { - "value": "#e1fbff", - "type": "color" - }, - "red": { - "value": "#ffdddd", - "type": "color" - } - } - } - }, - "black": { - "neutral": { - "100": { - "value": "#252F41", - "type": "color" - }, - "200": { - "value": "#313c51", - "type": "color" - }, - "300": { - "value": "#3c4557", - "type": "color" - }, - "400": { - "value": "#525A69", - "type": "color" - }, - "500": { - "value": "#59647a", - "type": "color" - }, - "600": { - "value": "#87A0BF", - "type": "color" - }, - "700": { - "value": "#99a6b8", - "type": "color" - }, - "800": { - "value": "#e2e9f2", - "type": "color" - }, - "900": { - "value": "#eff4fb", - "type": "color" - }, - "1000": { - "value": "#ffffff", - "type": "color" - }, - "N50": { - "value": "#232b38", - "type": "color" - }, - "N00": { - "value": "#1a202c", - "type": "color" - } - }, - "blue": { - "50": { - "value": "#232b38", - "type": "color" - }, - "100": { - "value": "#005174", - "type": "color" - }, - "200": { - "value": "#a6ecff", - "type": "color" - }, - "300": { - "value": "#52d1f4", - "type": "color" - }, - "400": { - "value": "#00bcf0", - "type": "color" - }, - "500": { - "value": "#05ade2", - "type": "color" - }, - "600": { - "value": "#009fd1", - "type": "color" - } - }, - "color": { - "deep": { - "red": { - "value": "#d32772", - "type": "color" - }, - "yellow": { - "value": "#e9b320", - "type": "color" - }, - "green": { - "value": "#3ba856", - "type": "color" - }, - "blue": { - "value": "#2e9dbb", - "type": "color" - } - }, - "light": { - "purple": { - "value": "#4D4078", - "type": "color" - }, - "blue": { - "value": "#2C3B58", - "type": "color" - }, - "green": { - "value": "#3C5133", - "type": "color" - }, - "yellow": { - "value": "#695E3E", - "type": "color" - }, - "pink": { - "value": "#5E3C5E", - "type": "color" - }, - "red": { - "value": "#56363F", - "type": "color" - }, - "aqua": { - "value": "#1B3849", - "type": "color" - }, - "lime": { - "value": "#394027", - "type": "color" - }, - "orange": { - "value": "#5E3C3C", - "type": "color" - } - } - } - }, - "else": { - "brand": { - "value": "#2c144b", - "type": "color" - } - } - } -} \ No newline at end of file diff --git a/frontend/appflowy_web_app/style-dictionary/tokens/dark.json b/frontend/appflowy_web_app/style-dictionary/tokens/dark.json deleted file mode 100644 index c67af7c9ec704..0000000000000 --- a/frontend/appflowy_web_app/style-dictionary/tokens/dark.json +++ /dev/null @@ -1,221 +0,0 @@ -{ - "text": { - "title": { - "value": "{Base.black.neutral.800}", - "type": "color" - }, - "caption": { - "value": "{Base.black.neutral.600}", - "type": "color" - }, - "placeholder": { - "value": "{Base.black.neutral.300}", - "type": "color" - }, - "link-default": { - "value": "{Base.black.blue.400}", - "type": "color" - }, - "link-hover": { - "value": "{Base.black.blue.300}", - "type": "color" - }, - "link-pressed": { - "value": "{Base.black.blue.600}", - "type": "color" - }, - "link-disabled": { - "value": "{Base.black.blue.100}", - "type": "color" - } - }, - "icon": { - "primary": { - "value": "{Base.black.neutral.800}", - "type": "color" - }, - "secondary": { - "value": "{Base.black.neutral.500}", - "type": "color" - }, - "disabled": { - "value": "{Base.black.neutral.400}", - "type": "color" - }, - "on-toolbar": { - "value": "white", - "type": "color" - } - }, - "line": { - "border": { - "value": "{Base.black.neutral.500}", - "type": "color" - }, - "divider": { - "value": "{Base.black.neutral.100}", - "type": "color" - }, - "on-toolbar": { - "value": "{Base.black.neutral.700}", - "type": "color" - } - }, - "fill": { - "default": { - "value": "{Base.black.blue.400}", - "type": "color" - }, - "hover": { - "value": "{Base.black.blue.100}", - "type": "color" - }, - "toolbar": { - "value": "#0F111C", - "type": "color" - }, - "selector": { - "value": "{Base.black.blue.50}", - "type": "color" - }, - "list": { - "active": { - "value": "{Base.black.neutral.300}", - "type": "color" - }, - "hover": { - "value": "{Base.black.blue.100}", - "type": "color" - } - } - }, - "content": { - "blue-400": { - "value": "{Base.black.blue.400}", - "type": "color" - }, - "blue-300": { - "value": "{Base.black.blue.300}", - "type": "color" - }, - "blue-600": { - "value": "{Base.black.blue.600}", - "type": "color" - }, - "blue-100": { - "value": "{Base.black.blue.100}", - "type": "color" - }, - "on-fill": { - "value": "{Base.black.neutral.N00}", - "type": "color" - }, - "on-tag": { - "value": "{Base.black.neutral.700}", - "type": "color" - }, - "blue-50": { - "value": "{Base.black.blue.50}", - "type": "color" - } - }, - "bg": { - "body": { - "value": "{Base.black.neutral.N00}", - "type": "color" - }, - "base": { - "value": "{Base.black.blue.50}", - "type": "color" - }, - "mask": { - "value": "rgba(0,0,0,0.7)", - "type": "color" - }, - "tips": { - "value": "{Base.black.blue.100}", - "type": "color" - }, - "brand": { - "value": "{Base.else.brand}", - "type": "color" - } - }, - "function": { - "error": { - "value": "{Base.black.color.deep.red}", - "type": "color" - }, - "warning": { - "value": "{Base.black.color.deep.yellow}", - "type": "color" - }, - "success": { - "value": "#3ba856", - "type": "color" - }, - "info": { - "value": "#2e9dbb", - "type": "color" - } - }, - "tint": { - "red": { - "value": "{Base.black.color.light.red}", - "type": "color" - }, - "green": { - "value": "{Base.black.color.light.green}", - "type": "color" - }, - "purple": { - "value": "{Base.black.color.light.purple}", - "type": "color" - }, - "blue": { - "value": "{Base.black.color.light.blue}", - "type": "color" - }, - "yellow": { - "value": "{Base.black.color.light.yellow}", - "type": "color" - }, - "pink": { - "value": "{Base.black.color.light.pink}", - "type": "color" - }, - "lime": { - "value": "{Base.black.color.light.lime}", - "type": "color" - }, - "aqua": { - "value": "{Base.black.color.light.aqua}", - "type": "color" - }, - "orange": { - "value": "{Base.black.color.light.orange}", - "type": "color" - } - }, - "shadow": { - "value": { - "x": "0", - "y": "0", - "blur": "25", - "spread": "0", - "color": "rgba(0,0,0,0.3)", - "type": "innerShadow" - }, - "type": "boxShadow" - }, - "scrollbar": { - "track": { - "value": "{Base.black.neutral.100}", - "type": "color" - }, - "thumb": { - "value": "{Base.black.neutral.300}", - "type": "color" - } - } -} \ No newline at end of file diff --git a/frontend/appflowy_web_app/style-dictionary/tokens/light.json b/frontend/appflowy_web_app/style-dictionary/tokens/light.json deleted file mode 100644 index 173f3d35aafb3..0000000000000 --- a/frontend/appflowy_web_app/style-dictionary/tokens/light.json +++ /dev/null @@ -1,233 +0,0 @@ -{ - "text": { - "title": { - "value": "{Base.Light.neutral.800}", - "type": "color" - }, - "caption": { - "value": "{Base.Light.neutral.600}", - "type": "color" - }, - "placeholder": { - "value": "{Base.Light.neutral.500}", - "type": "color" - }, - "disabled": { - "value": "{Base.Light.neutral.400}", - "type": "color" - }, - "link-default": { - "value": "{Base.Light.blue.400}", - "type": "color" - }, - "link-hover": { - "value": "{Base.Light.blue.300}", - "type": "color" - }, - "link-pressed": { - "value": "{Base.Light.blue.600}", - "type": "color" - }, - "link-disabled": { - "value": "{Base.Light.blue.100}", - "type": "color" - } - }, - "icon": { - "primary": { - "value": "{Base.Light.neutral.800}", - "type": "color" - }, - "secondary": { - "value": "{Base.black.neutral.500}", - "type": "color" - }, - "disabled": { - "value": "{Base.Light.neutral.400}", - "type": "color" - }, - "on-toolbar": { - "value": "{Base.Light.neutral.00}", - "type": "color" - } - }, - "line": { - "border": { - "value": "{Base.Light.neutral.500}", - "type": "color" - }, - "divider": { - "value": "{Base.Light.neutral.100}", - "type": "color" - }, - "on-toolbar": { - "value": "{Base.Light.neutral.700}", - "type": "color" - } - }, - "fill": { - "toolbar": { - "value": "{Base.Light.neutral.800}", - "type": "color" - }, - "default": { - "value": "{Base.Light.blue.400}", - "type": "color" - }, - "hover": { - "value": "{Base.Light.blue.300}", - "type": "color" - }, - "pressed": { - "value": "{Base.Light.blue.600}", - "type": "color" - }, - "active": { - "value": "{Base.Light.blue.100}", - "type": "color" - }, - "list": { - "hover": { - "value": "{Base.Light.blue.100}", - "type": "color" - }, - "active": { - "value": "{Base.Light.neutral.100}", - "type": "color" - } - } - }, - "content": { - "blue-400": { - "value": "{Base.Light.blue.400}", - "type": "color" - }, - "blue-300": { - "value": "{Base.Light.blue.300}", - "type": "color" - }, - "blue-600": { - "value": "{Base.Light.blue.600}", - "type": "color" - }, - "blue-100": { - "value": "{Base.Light.blue.100}", - "type": "color" - }, - "blue-50": { - "value": "{Base.Light.blue.50}", - "type": "color" - }, - "on-fill-hover": { - "value": "{Base.Light.blue.400}", - "type": "color" - }, - "on-fill": { - "value": "{Base.Light.neutral.00}", - "type": "color" - }, - "on-tag": { - "value": "{Base.Light.neutral.700}", - "type": "color" - } - }, - "bg": { - "body": { - "value": "{Base.Light.neutral.00}", - "type": "color" - }, - "base": { - "value": "{Base.Light.neutral.50}", - "type": "color" - }, - "mask": { - "value": "rgba(0,0,0,0.55)", - "type": "color" - }, - "tips": { - "value": "{Base.Light.blue.100}", - "type": "color" - }, - "brand": { - "value": "{Base.else.brand}", - "type": "color" - } - }, - "function": { - "error": { - "value": "{Base.Light.color.deep.red}", - "type": "color" - }, - "waring": { - "value": "{Base.Light.color.deep.yellow}", - "type": "color" - }, - "success": { - "value": "{Base.Light.color.deep.green}", - "type": "color" - }, - "info": { - "value": "{Base.Light.color.deep.blue}", - "type": "color" - } - }, - "tint": { - "purple": { - "value": "{Base.Light.color.light.purple}", - "type": "color" - }, - "pink": { - "value": "{Base.Light.color.light.pink}", - "type": "color" - }, - "red": { - "value": "{Base.Light.color.light.red}", - "type": "color" - }, - "lime": { - "value": "{Base.Light.color.light.lime}", - "type": "color" - }, - "green": { - "value": "{Base.Light.color.light.green}", - "type": "color" - }, - "aqua": { - "value": "{Base.Light.color.light.aqua}", - "type": "color" - }, - "blue": { - "value": "{Base.Light.color.light.blue}", - "type": "color" - }, - "orange": { - "value": "{Base.Light.color.light.orange}", - "type": "color" - }, - "yellow": { - "value": "{Base.Light.color.light.yellow}", - "type": "color" - } - }, - "shadow": { - "value": { - "x": "0", - "y": "0", - "blur": "10", - "spread": "0", - "color": "rgba(0,0,0,0.1)", - "type": "dropShadow" - }, - "type": "boxShadow" - }, - "scrollbar": { - "thumb": { - "value": "{Base.Light.neutral.500}", - "type": "color" - }, - "track": { - "value": "{Base.Light.neutral.100}", - "type": "color" - } - } -} \ No newline at end of file diff --git a/frontend/appflowy_web_app/tailwind.config.cjs b/frontend/appflowy_web_app/tailwind.config.cjs index 06390d938f697..8589a0f4b6776 100644 --- a/frontend/appflowy_web_app/tailwind.config.cjs +++ b/frontend/appflowy_web_app/tailwind.config.cjs @@ -1,5 +1,5 @@ -const colors = require('./style-dictionary/tailwind/colors.cjs'); -const boxShadow = require('./style-dictionary/tailwind/box-shadow.cjs'); +const colors = require('./tailwind/colors.cjs'); +const boxShadow = require('./tailwind/box-shadow.cjs'); /** @type {import('tailwindcss').Config} */ module.exports = { diff --git a/frontend/appflowy_web_app/style-dictionary/tailwind/box-shadow.cjs b/frontend/appflowy_web_app/tailwind/box-shadow.cjs similarity index 69% rename from frontend/appflowy_web_app/style-dictionary/tailwind/box-shadow.cjs rename to frontend/appflowy_web_app/tailwind/box-shadow.cjs index 9de67fc1bea17..d72c2552272df 100644 --- a/frontend/appflowy_web_app/style-dictionary/tailwind/box-shadow.cjs +++ b/frontend/appflowy_web_app/tailwind/box-shadow.cjs @@ -1,9 +1,11 @@ + /** * Do not edit directly -* Generated on Thu, 09 May 2024 03:26:45 GMT +* Generated on Mon, 27 May 2024 06:26:20 GMT * Generated from $pnpm css:variables */ + module.exports = { "md": "var(--shadow)" -} \ No newline at end of file +}; diff --git a/frontend/appflowy_web_app/style-dictionary/tailwind/colors.cjs b/frontend/appflowy_web_app/tailwind/colors.cjs similarity index 73% rename from frontend/appflowy_web_app/style-dictionary/tailwind/colors.cjs rename to frontend/appflowy_web_app/tailwind/colors.cjs index 63e679a90aac7..27a3b07c30bea 100644 --- a/frontend/appflowy_web_app/style-dictionary/tailwind/colors.cjs +++ b/frontend/appflowy_web_app/tailwind/colors.cjs @@ -1,14 +1,17 @@ + /** * Do not edit directly -* Generated on Thu, 09 May 2024 03:26:45 GMT +* Generated on Mon, 27 May 2024 06:26:20 GMT * Generated from $pnpm css:variables */ + module.exports = { "text": { "title": "var(--text-title)", "caption": "var(--text-caption)", "placeholder": "var(--text-placeholder)", + "disabled": "var(--text-disabled)", "link-default": "var(--text-link-default)", "link-hover": "var(--text-link-hover)", "link-pressed": "var(--text-link-pressed)", @@ -26,50 +29,49 @@ module.exports = { "on-toolbar": "var(--line-on-toolbar)" }, "fill": { + "toolbar": "var(--fill-toolbar)", "default": "var(--fill-default)", "hover": "var(--fill-hover)", - "toolbar": "var(--fill-toolbar)", - "selector": "var(--fill-selector)", - "list": { - "active": "var(--fill-list-active)", - "hover": "var(--fill-list-hover)" - } + "pressed": "var(--fill-pressed)", + "active": "var(--fill-active)", + "list-hover": "var(--fill-list-hover)", + "list-active": "var(--fill-list-active)" }, "content": { "blue-400": "var(--content-blue-400)", "blue-300": "var(--content-blue-300)", "blue-600": "var(--content-blue-600)", "blue-100": "var(--content-blue-100)", + "blue-50": "var(--content-blue-50)", + "on-fill-hover": "var(--content-on-fill-hover)", "on-fill": "var(--content-on-fill)", - "on-tag": "var(--content-on-tag)", - "blue-50": "var(--content-blue-50)" + "on-tag": "var(--content-on-tag)" }, "bg": { "body": "var(--bg-body)", "base": "var(--bg-base)", - "mask": "var(--bg-mask)", "tips": "var(--bg-tips)", "brand": "var(--bg-brand)" }, "function": { "error": "var(--function-error)", - "warning": "var(--function-warning)", + "waring": "var(--function-waring)", "success": "var(--function-success)", "info": "var(--function-info)" }, "tint": { - "red": "var(--tint-red)", - "green": "var(--tint-green)", "purple": "var(--tint-purple)", - "blue": "var(--tint-blue)", - "yellow": "var(--tint-yellow)", "pink": "var(--tint-pink)", + "red": "var(--tint-red)", "lime": "var(--tint-lime)", + "green": "var(--tint-green)", "aqua": "var(--tint-aqua)", - "orange": "var(--tint-orange)" + "blue": "var(--tint-blue)", + "orange": "var(--tint-orange)", + "yellow": "var(--tint-yellow)" }, "scrollbar": { - "track": "var(--scrollbar-track)", - "thumb": "var(--scrollbar-thumb)" + "thumb": "var(--scrollbar-thumb)", + "track": "var(--scrollbar-track)" } -} \ No newline at end of file +}; diff --git a/frontend/appflowy_web_app/vite.config.ts b/frontend/appflowy_web_app/vite.config.ts index 0e4cfebb4b1a5..8431b8bd2f933 100644 --- a/frontend/appflowy_web_app/vite.config.ts +++ b/frontend/appflowy_web_app/vite.config.ts @@ -71,6 +71,9 @@ export default defineConfig({ cors: false, }, envPrefix: ['AF', 'TAURI_'], + esbuild: { + pure: !isDev ? ['console.log', 'console.debug', 'console.info'] : [], + }, build: !!process.env.TAURI_PLATFORM ? { // Tauri supports es2021 @@ -82,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 diff --git a/frontend/resources/flowy_icons/16x/add_cover.svg b/frontend/resources/flowy_icons/16x/add_cover.svg new file mode 100644 index 0000000000000..ac83855416ba8 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/add_cover.svg @@ -0,0 +1,7 @@ +<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 0000000000000..e49b54ec141df --- /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 0000000000000..83dabfe0a15bb --- /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/board.svg b/frontend/resources/flowy_icons/16x/board.svg index 550d045178da5..bde3149c31182 100644 --- a/frontend/resources/flowy_icons/16x/board.svg +++ b/frontend/resources/flowy_icons/16x/board.svg @@ -1,6 +1,4 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M12.8 2H3.2C2.53726 2 2 2.55964 2 3.25V5.75C2 6.44036 2.53726 7 3.2 7H12.8C13.4627 7 14 6.44036 14 5.75V3.25C14 2.55964 13.4627 2 12.8 2Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M12.8 9H3.2C2.53726 9 2 9.55964 2 10.25V12.75C2 13.4404 2.53726 14 3.2 14H12.8C13.4627 14 14 13.4404 14 12.75V10.25C14 9.55964 13.4627 9 12.8 9Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> -<circle cx="4.5" cy="4.5" r="0.5" fill="#333333"/> -<circle cx="4.5" cy="11.5" r="0.5" fill="#333333"/> +<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M7.90313 14.7769V3.22312C7.90313 2.12625 7.43513 1.6875 6.27244 1.6875H3.31819C2.1555 1.6875 1.6875 2.12625 1.6875 3.22312V14.7769C1.6875 15.8737 2.1555 16.3125 3.31819 16.3125H6.27244C7.43513 16.3125 7.90313 15.8737 7.90313 14.7769Z" stroke="#171717" stroke-opacity="0.7" stroke-width="1.09687" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M16.3133 9.65812V3.22312C16.3133 2.12625 15.8453 1.6875 14.6826 1.6875H11.7283C10.5657 1.6875 10.0977 2.12625 10.0977 3.22312V9.65812C10.0977 10.755 10.5657 11.1937 11.7283 11.1937H14.6826C15.8453 11.1937 16.3133 10.755 16.3133 9.65812Z" stroke="#171717" stroke-opacity="0.7" stroke-width="1.09687" stroke-linecap="round" stroke-linejoin="round"/> </svg> diff --git a/frontend/resources/flowy_icons/16x/calendar.svg b/frontend/resources/flowy_icons/16x/calendar.svg new file mode 100644 index 0000000000000..34f184b4d2afa --- /dev/null +++ b/frontend/resources/flowy_icons/16x/calendar.svg @@ -0,0 +1,6 @@ +<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M5.83008 1.125V3.5" stroke="#171717" stroke-opacity="0.7" stroke-width="1.125" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M12.1699 1.125V3.5" stroke="#171717" stroke-opacity="0.7" stroke-width="1.125" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M2.26758 6.73877H15.7259" stroke="#171717" stroke-opacity="0.7" stroke-width="1.125" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> +<rect x="2.0625" y="2.4375" width="13.875" height="13.875" rx="2.4375" stroke="#171717" stroke-opacity="0.7" stroke-width="1.125"/> +</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 0000000000000..38c7e41710654 --- /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 0000000000000..2760daaaef83a --- /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/document.svg b/frontend/resources/flowy_icons/16x/document.svg index abb59d5c145df..d73738329c993 100644 --- a/frontend/resources/flowy_icons/16x/document.svg +++ b/frontend/resources/flowy_icons/16x/document.svg @@ -1,3 +1,4 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path fill-rule="evenodd" clip-rule="evenodd" d="M4.5 2C3.67157 2 3 2.67157 3 3.5V12.5C3 13.3284 3.67157 14 4.5 14H11.5C12.3284 14 13 13.3284 13 12.5V7.16667C13 6.62574 12.8246 6.09941 12.5 5.66667L10.5 3C10.0279 2.37049 9.28689 2 8.5 2H4.5ZM4 3.5C4 3.22386 4.22386 3 4.5 3H8.5V6C8.5 6.82843 9.17157 7.5 10 7.5H12V12.5C12 12.7761 11.7761 13 11.5 13H4.5C4.22386 13 4 12.7761 4 12.5V3.5ZM11.8437 6.5C11.8032 6.41843 11.7552 6.34029 11.7 6.26667L9.7 3.6C9.64012 3.52016 9.57303 3.44726 9.5 3.38194V6C9.5 6.27614 9.72386 6.5 10 6.5H11.8437Z" fill="#333333"/> +<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M3.11523 4.5C3.11523 3.15381 4.20654 2.0625 5.55273 2.0625H9.96357C10.1512 2.0625 10.3292 2.14513 10.4504 2.28838L12.3569 4.5432L14.7021 7.6626C14.8242 7.82503 14.8902 8.02274 14.8902 8.22596V13.5C14.8902 14.8462 13.7989 15.9375 12.4527 15.9375H5.55274C4.20654 15.9375 3.11523 14.8462 3.11523 13.5V4.5Z" stroke="#171717" stroke-opacity="0.7" stroke-width="1.125"/> +<path d="M10.0527 2.25V6.525C10.0527 7.35343 10.7243 8.025 11.5527 8.025H14.9277" stroke="#171717" stroke-opacity="0.7" stroke-width="1.05"/> </svg> diff --git a/frontend/resources/flowy_icons/16x/dot.svg b/frontend/resources/flowy_icons/16x/dot.svg new file mode 100644 index 0000000000000..70d26f3988ce8 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/dot.svg @@ -0,0 +1,3 @@ +<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg"> +<circle opacity="0.4" cx="4" cy="4" r="1" fill="#171717"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/duplicate.svg b/frontend/resources/flowy_icons/16x/duplicate.svg new file mode 100644 index 0000000000000..883082258975b --- /dev/null +++ b/frontend/resources/flowy_icons/16x/duplicate.svg @@ -0,0 +1,4 @@ +<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 8ad54bbbb5733..addd7d4915b1e 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 0000000000000..8296f888f34f7 --- /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 0000000000000..0402120e41a12 --- /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 0000000000000..b984afe0171ff --- /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 0000000000000..3e72f90f4b49d --- /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/grid.svg b/frontend/resources/flowy_icons/16x/grid.svg index 8164b24e6adc6..e1b7c1f148d57 100644 --- a/frontend/resources/flowy_icons/16x/grid.svg +++ b/frontend/resources/flowy_icons/16x/grid.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 2H3C2.44772 2 2 2.44772 2 3V6C2 6.55228 2.44772 7 3 7H6C6.55228 7 7 6.55228 7 6V3C7 2.44772 6.55228 2 6 2Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M13 2H10C9.44772 2 9 2.44772 9 3V6C9 6.55228 9.44772 7 10 7H13C13.5523 7 14 6.55228 14 6V3C14 2.44772 13.5523 2 13 2Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M13 9H10C9.44772 9 9 9.44772 9 10V13C9 13.5523 9.44772 14 10 14H13C13.5523 14 14 13.5523 14 13V10C14 9.44772 13.5523 9 13 9Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M6 9H3C2.44772 9 2 9.44772 2 10V13C2 13.5523 2.44772 14 3 14H6C6.55228 14 7 13.5523 7 13V10C7 9.44772 6.55228 9 6 9Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> +<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect x="2.03571" y="2.03571" width="13.9286" height="13.9286" rx="2.32143" stroke="#171717" stroke-opacity="0.7" stroke-width="1.07143"/> +<path d="M6.64453 2.03564V15.9642" stroke="#171717" stroke-opacity="0.7" stroke-width="1.07143" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M2.57227 6.50146H15.4294" stroke="#171717" stroke-opacity="0.7" stroke-width="1.07143" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M2.57227 11.501H15.4294" stroke="#171717" stroke-opacity="0.7" stroke-width="1.07143" stroke-linecap="round" stroke-linejoin="round"/> </svg> diff --git a/frontend/resources/flowy_icons/16x/hide_menu.svg b/frontend/resources/flowy_icons/16x/hide_menu.svg index ce88af8ea7c69..9e301210c45d0 100644 --- a/frontend/resources/flowy_icons/16x/hide_menu.svg +++ b/frontend/resources/flowy_icons/16x/hide_menu.svg @@ -1,6 +1,6 @@ -<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 0000000000000..9953ebe30c450 --- /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/m_add_block_video.svg b/frontend/resources/flowy_icons/16x/m_add_block_video.svg new file mode 100644 index 0000000000000..5bd19d0a3e290 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_add_block_video.svg @@ -0,0 +1,2 @@ + +<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#e8eaed"><path d="m384-312 264-168-264-168v336ZM168-192q-29.7 0-50.85-21.16Q96-234.32 96-264.04v-432.24Q96-726 117.15-747T168-768h624q29.7 0 50.85 21.16Q864-725.68 864-695.96v432.24Q864-234 842.85-213T792-192H168Zm0-72h624v-432H168v432Zm0 0v-432 432Z"/></svg> \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/m_collapse.svg b/frontend/resources/flowy_icons/16x/m_collapse.svg new file mode 100644 index 0000000000000..bf88470d69f1c --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_collapse.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="M10.9667 7.59993C11.2333 7.79993 11.2333 8.19993 10.9667 8.39993L6.3 11.8999C5.97038 12.1471 5.5 11.912 5.5 11.4999L5.5 4.49993C5.5 4.08791 5.97038 3.85272 6.3 4.09993L10.9667 7.59993Z" fill="#8F959E"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/m_expand.svg b/frontend/resources/flowy_icons/16x/m_expand.svg new file mode 100644 index 0000000000000..48c7bc5ebdd3b --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_expand.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="M8.39909 10.9667C8.19909 11.2333 7.79909 11.2333 7.59909 10.9667L4.09909 6.3C3.85188 5.97038 4.08707 5.5 4.49909 5.5L11.4991 5.5C11.9111 5.5 12.1463 5.97038 11.8991 6.3L8.39909 10.9667Z" fill="#8F959E"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/m_space_add.svg b/frontend/resources/flowy_icons/16x/m_space_add.svg new file mode 100644 index 0000000000000..dcab9d1eb5bc9 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_space_add.svg @@ -0,0 +1,6 @@ +<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.5"> +<rect x="2.25" y="8.3252" width="13.5" height="1.35" rx="0.675" fill="#171717"/> +<rect x="8.32422" y="15.75" width="13.5" height="1.35" rx="0.675" transform="rotate(-90 8.32422 15.75)" fill="#171717"/> +</g> +</svg> diff --git a/frontend/resources/flowy_icons/16x/m_space_more.svg b/frontend/resources/flowy_icons/16x/m_space_more.svg new file mode 100644 index 0000000000000..ea557e960c0f7 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_space_more.svg @@ -0,0 +1,7 @@ +<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="M4.76211 9.0002C4.76211 9.68365 4.20806 10.2377 3.52461 10.2377C2.84116 10.2377 2.28711 9.68365 2.28711 9.0002C2.28711 8.31674 2.84116 7.7627 3.52461 7.7627C4.20806 7.7627 4.76211 8.31674 4.76211 9.0002Z" fill="#171717"/> +<path d="M10.2367 9.0002C10.2367 9.68365 9.68267 10.2377 8.99922 10.2377C8.31577 10.2377 7.76172 9.68365 7.76172 9.0002C7.76172 8.31674 8.31577 7.7627 8.99922 7.7627C9.68267 7.7627 10.2367 8.31674 10.2367 9.0002Z" fill="#171717"/> +<path d="M15.7113 9.0002C15.7113 9.68365 15.1573 10.2377 14.4738 10.2377C13.7904 10.2377 13.2363 9.68365 13.2363 9.0002C13.2363 8.31674 13.7904 7.7627 14.4738 7.7627C15.1573 7.7627 15.7113 8.31674 15.7113 9.0002Z" fill="#171717"/> +</g> +</svg> diff --git a/frontend/resources/flowy_icons/16x/m_spaces_expand.svg b/frontend/resources/flowy_icons/16x/m_spaces_expand.svg new file mode 100644 index 0000000000000..af15050d18dd2 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_spaces_expand.svg @@ -0,0 +1,8 @@ +<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> +<mask id="path-2-outside-1_136_3860" maskUnits="userSpaceOnUse" x="4" y="7" width="10" height="6" fill="black"> +<rect fill="white" x="4" y="7" width="10" height="6"/> +<path d="M5.14645 7.64645C5.34171 7.45118 5.65829 7.45118 5.85355 7.64645L9 10.7929L12.1464 7.64645C12.3417 7.45118 12.6583 7.45118 12.8536 7.64645C13.0488 7.84171 13.0488 8.15829 12.8536 8.35355L9.35355 11.8536C9.25979 11.9473 9.13261 12 9 12C8.86739 12 8.74021 11.9473 8.64645 11.8536L5.14645 8.35355C4.95118 8.15829 4.95118 7.84171 5.14645 7.64645Z"/> +</mask> +<path d="M5.14645 7.64645C5.34171 7.45118 5.65829 7.45118 5.85355 7.64645L9 10.7929L12.1464 7.64645C12.3417 7.45118 12.6583 7.45118 12.8536 7.64645C13.0488 7.84171 13.0488 8.15829 12.8536 8.35355L9.35355 11.8536C9.25979 11.9473 9.13261 12 9 12C8.86739 12 8.74021 11.9473 8.64645 11.8536L5.14645 8.35355C4.95118 8.15829 4.95118 7.84171 5.14645 7.64645Z" fill="#171717"/> +<path d="M5.85355 7.64645L5.78284 7.71716H5.78284L5.85355 7.64645ZM5.14645 7.64645L5.21716 7.71716L5.14645 7.64645ZM9 10.7929L9.07071 10.8636C9.05196 10.8824 9.02652 10.8929 9 10.8929C8.97348 10.8929 8.94804 10.8824 8.92929 10.8636L9 10.7929ZM12.1464 7.64645L12.2172 7.71716L12.1464 7.64645ZM12.8536 7.64645L12.7828 7.71716L12.8536 7.64645ZM12.8536 8.35355L12.7828 8.28284L12.8536 8.35355ZM9.35355 11.8536L9.42426 11.9243V11.9243L9.35355 11.8536ZM8.64645 11.8536L8.71716 11.7828V11.7828L8.64645 11.8536ZM5.14645 8.35355L5.07574 8.42426H5.07574L5.14645 8.35355ZM5.78284 7.71716C5.62663 7.56095 5.37337 7.56095 5.21716 7.71716L5.07574 7.57574C5.31005 7.34142 5.68995 7.34142 5.92426 7.57574L5.78284 7.71716ZM8.92929 10.8636L5.78284 7.71716L5.92426 7.57574L9.07071 10.7222L8.92929 10.8636ZM12.2172 7.71716L9.07071 10.8636L8.92929 10.7222L12.0757 7.57574L12.2172 7.71716ZM12.7828 7.71716C12.6266 7.56095 12.3734 7.56095 12.2172 7.71716L12.0757 7.57574C12.3101 7.34142 12.6899 7.34142 12.9243 7.57574L12.7828 7.71716ZM12.7828 8.28284C12.9391 8.12663 12.9391 7.87337 12.7828 7.71716L12.9243 7.57574C13.1586 7.81005 13.1586 8.18995 12.9243 8.42426L12.7828 8.28284ZM9.28284 11.7828L12.7828 8.28284L12.9243 8.42426L9.42426 11.9243L9.28284 11.7828ZM9 11.9C9.10609 11.9 9.20783 11.8579 9.28284 11.7828L9.42426 11.9243C9.31174 12.0368 9.15913 12.1 9 12.1V11.9ZM8.71716 11.7828C8.79217 11.8579 8.89391 11.9 9 11.9V12.1C8.84087 12.1 8.68826 12.0368 8.57574 11.9243L8.71716 11.7828ZM5.21716 8.28284L8.71716 11.7828L8.57574 11.9243L5.07574 8.42426L5.21716 8.28284ZM5.21716 7.71716C5.06095 7.87337 5.06095 8.12663 5.21716 8.28284L5.07574 8.42426C4.84142 8.18995 4.84142 7.81005 5.07574 7.57574L5.21716 7.71716Z" fill="#171717" fill-opacity="0.8" mask="url(#path-2-outside-1_136_3860)"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/more.svg b/frontend/resources/flowy_icons/16x/more.svg index b191e64a10343..da54e4b6e635d 100644 --- a/frontend/resources/flowy_icons/16x/more.svg +++ b/frontend/resources/flowy_icons/16x/more.svg @@ -1,3 +1,7 @@ <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 0000000000000..1c7d6144eede3 --- /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 0000000000000..feb63cb9b319a --- /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/remove_from_recent.svg b/frontend/resources/flowy_icons/16x/remove_from_recent.svg new file mode 100644 index 0000000000000..b12c8054aaebf --- /dev/null +++ b/frontend/resources/flowy_icons/16x/remove_from_recent.svg @@ -0,0 +1,4 @@ +<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M8.93945 16.5C13.0645 16.5 16.4395 13.125 16.4395 9C16.4395 4.875 13.0645 1.5 8.93945 1.5C4.81445 1.5 1.43945 4.875 1.43945 9C1.43945 13.125 4.81445 16.5 8.93945 16.5Z" stroke="#171717" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M5.93945 9H11.9395" stroke="#171717" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/search.svg b/frontend/resources/flowy_icons/16x/search.svg index 1efb2d475c980..ed7e6de9d2710 100644 --- a/frontend/resources/flowy_icons/16x/search.svg +++ b/frontend/resources/flowy_icons/16x/search.svg @@ -1,4 +1,6 @@ <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 f9896aad5250c..bcc96b817be53 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/show_menu.svg b/frontend/resources/flowy_icons/16x/show_menu.svg index 8baf55bffdd3e..67edfc4d17374 100644 --- a/frontend/resources/flowy_icons/16x/show_menu.svg +++ b/frontend/resources/flowy_icons/16x/show_menu.svg @@ -1,6 +1,4 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M10 5L13 8L10 11" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> -<rect x="3" y="5" width="4" height="1" rx="0.5" fill="#333333"/> -<rect x="3" y="7.5" width="6" height="1" rx="0.5" fill="#333333"/> -<rect x="3" y="10" width="4" height="1" rx="0.5" fill="#333333"/> +<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M9.75 14.25L15 9L9.75 3.75" stroke="#171717" stroke-width="1.2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M3.5625 14.25L8.8125 9L3.5625 3.75" stroke="#171717" stroke-width="1.2" 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 0000000000000..f412bb86fdc77 --- /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 0000000000000..2dbb6f52cfaab --- /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/success.svg b/frontend/resources/flowy_icons/16x/success.svg new file mode 100644 index 0000000000000..771d8d7f5c115 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/success.svg @@ -0,0 +1,4 @@ +<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M10.5013 18.3334C15.1037 18.3334 18.8346 14.6025 18.8346 10.0001C18.8346 5.39771 15.1037 1.66675 10.5013 1.66675C5.89893 1.66675 2.16797 5.39771 2.16797 10.0001C2.16797 14.6025 5.89893 18.3334 10.5013 18.3334Z" stroke="white" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M6.75 10.625L9.58883 13.4524L15.0833 7.5" stroke="white" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/three-dots.svg b/frontend/resources/flowy_icons/16x/three-dots.svg index 4d37a346a0ce4..07aa7ca7062f9 100644 --- a/frontend/resources/flowy_icons/16x/three-dots.svg +++ b/frontend/resources/flowy_icons/16x/three-dots.svg @@ -1,3 +1,5 @@ -<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 0000000000000..5f92484836db6 --- /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 0000000000000..487a57001f292 --- /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 0ccfc1edff86e..b984afe0171ff 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 0000000000000..9373ef6dd11fa --- /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 0000000000000..5fd5a1e719573 --- /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 0000000000000..87f9f11949d6b --- /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 0000000000000..c890184915301 --- /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 0000000000000..de5db7fd68fad --- /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 0000000000000..8971910251e9d --- /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 0000000000000..d264b4734e278 --- /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 0000000000000..4682de7aa7e14 --- /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 0000000000000..6e44da6b1454d --- /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 0000000000000..73d86f8e0242b --- /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 0000000000000..93aa13693a6fb --- /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/m_home_add.svg b/frontend/resources/flowy_icons/24x/m_home_add.svg new file mode 100644 index 0000000000000..bf93cf5e68712 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_home_add.svg @@ -0,0 +1,5 @@ +<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M12.333 22C17.833 22 22.333 17.5 22.333 12C22.333 6.5 17.833 2 12.333 2C6.83301 2 2.33301 6.5 2.33301 12C2.33301 17.5 6.83301 22 12.333 22Z" stroke="#171717" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M8.33301 12H16.333" stroke="#171717" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M12.333 16V8" stroke="#171717" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/frontend/resources/flowy_icons/24x/m_home_notification.svg b/frontend/resources/flowy_icons/24x/m_home_notification.svg new file mode 100644 index 0000000000000..6d4b883701b3a --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_home_notification.svg @@ -0,0 +1,5 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M12.0196 2.91016C8.7096 2.91016 6.0196 5.60016 6.0196 8.91016V11.8002C6.0196 12.4102 5.7596 13.3402 5.4496 13.8602L4.2996 15.7702C3.5896 16.9502 4.0796 18.2602 5.3796 18.7002C9.6896 20.1402 14.3396 20.1402 18.6496 18.7002C19.8596 18.3002 20.3896 16.8702 19.7296 15.7702L18.5796 13.8602C18.2796 13.3402 18.0196 12.4102 18.0196 11.8002V8.91016C18.0196 5.61016 15.3196 2.91016 12.0196 2.91016Z" stroke="#171717" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round"/> +<path d="M13.8699 3.19994C13.5599 3.10994 13.2399 3.03994 12.9099 2.99994C11.9499 2.87994 11.0299 2.94994 10.1699 3.19994C10.4599 2.45994 11.1799 1.93994 12.0199 1.93994C12.8599 1.93994 13.5799 2.45994 13.8699 3.19994Z" stroke="#171717" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M15.0195 19.0601C15.0195 20.7101 13.6695 22.0601 12.0195 22.0601C11.1995 22.0601 10.4395 21.7201 9.89953 21.1801C9.35953 20.6401 9.01953 19.8801 9.01953 19.0601" stroke="#171717" stroke-width="1.5" stroke-miterlimit="10"/> +</svg> diff --git a/frontend/resources/flowy_icons/24x/m_home_selected.svg b/frontend/resources/flowy_icons/24x/m_home_selected.svg new file mode 100644 index 0000000000000..b92efcd4531a9 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_home_selected.svg @@ -0,0 +1,6 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M7.24 2H5.34C3.15 2 2 3.15 2 5.33V7.23C2 9.41 3.15 10.56 5.33 10.56H7.23C9.41 10.56 10.56 9.41 10.56 7.23V5.33C10.57 3.15 9.42 2 7.24 2Z" fill="#00C8FF"/> +<path d="M18.6704 2H16.7704C14.5904 2 13.4404 3.15 13.4404 5.33V7.23C13.4404 9.41 14.5904 10.56 16.7704 10.56H18.6704C20.8504 10.56 22.0004 9.41 22.0004 7.23V5.33C22.0004 3.15 20.8504 2 18.6704 2Z" fill="#00C8FF"/> +<path d="M18.6704 13.4302H16.7704C14.5904 13.4302 13.4404 14.5802 13.4404 16.7602V18.6602C13.4404 20.8402 14.5904 21.9902 16.7704 21.9902H18.6704C20.8504 21.9902 22.0004 20.8402 22.0004 18.6602V16.7602C22.0004 14.5802 20.8504 13.4302 18.6704 13.4302Z" fill="#00C8FF"/> +<path d="M7.24 13.4302H5.34C3.15 13.4302 2 14.5802 2 16.7602V18.6602C2 20.8502 3.15 22.0002 5.33 22.0002H7.23C9.41 22.0002 10.56 20.8502 10.56 18.6702V16.7702C10.57 14.5802 9.42 13.4302 7.24 13.4302Z" fill="#00C8FF"/> +</svg> diff --git a/frontend/resources/flowy_icons/24x/m_home_unselected.svg b/frontend/resources/flowy_icons/24x/m_home_unselected.svg new file mode 100644 index 0000000000000..7c34f3371f34d --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_home_unselected.svg @@ -0,0 +1,6 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M10.06 5.32771V5.33V7.23C10.06 8.23613 9.79557 8.92232 9.35895 9.35895C8.92232 9.79557 8.23613 10.06 7.23 10.06H5.33C4.32387 10.06 3.63768 9.79557 3.20105 9.35895C2.76443 8.92232 2.5 8.23613 2.5 7.23V5.33C2.5 4.32387 2.76441 3.63797 3.20191 3.20145C3.63959 2.76474 4.32828 2.5 5.34 2.5H7.24C8.24631 2.5 8.93213 2.76451 9.3673 3.20066C9.80228 3.63661 10.0646 4.322 10.06 5.32771Z" stroke="#191919"/> +<path d="M18.6704 2H16.7704C14.5904 2 13.4404 3.15 13.4404 5.33V7.23C13.4404 9.41 14.5904 10.56 16.7704 10.56H18.6704C20.8504 10.56 22.0004 9.41 22.0004 7.23V5.33C22.0004 3.15 20.8504 2 18.6704 2Z" fill="#191919"/> +<path d="M18.6704 13.4302H16.7704C14.5904 13.4302 13.4404 14.5802 13.4404 16.7602V18.6602C13.4404 20.8402 14.5904 21.9902 16.7704 21.9902H18.6704C20.8504 21.9902 22.0004 20.8402 22.0004 18.6602V16.7602C22.0004 14.5802 20.8504 13.4302 18.6704 13.4302Z" fill="#191919"/> +<path d="M7.24 13.4302H5.34C3.15 13.4302 2 14.5802 2 16.7602V18.6602C2 20.8502 3.15 22.0002 5.33 22.0002H7.23C9.41 22.0002 10.56 20.8502 10.56 18.6702V16.7702C10.57 14.5802 9.42 13.4302 7.24 13.4302Z" fill="#191919"/> +</svg> diff --git a/frontend/resources/flowy_icons/24x/m_setting.svg b/frontend/resources/flowy_icons/24x/m_setting.svg index d8be6a36b7fb2..a32f3bc7b3d90 100644 --- a/frontend/resources/flowy_icons/24x/m_setting.svg +++ b/frontend/resources/flowy_icons/24x/m_setting.svg @@ -1,3 +1,6 @@ -<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M8.96704 2.45522C7.54904 2.90522 6.32605 3.63923 5.24805 4.64323C4.91505 4.95323 4.81305 5.46123 5.02905 5.86123C5.83005 7.34223 4.99204 8.92723 3.18504 9.01823C2.74304 9.04023 2.35205 9.36823 2.24805 9.79923C2.06905 10.5442 1.99805 11.1682 1.99805 11.9862C1.99805 12.6732 2.07204 13.4512 2.21704 14.1432C2.30704 14.5752 2.68305 14.8862 3.12305 14.9242C4.94105 15.0812 5.84105 16.4682 5.02905 18.2362C4.84905 18.6292 4.93105 19.0992 5.24805 19.3932C6.31005 20.3752 7.53004 21.0682 8.96704 21.5182C9.37704 21.6462 9.84004 21.4912 10.092 21.1432C11.204 19.6042 12.817 19.5992 13.873 21.1432C14.122 21.5062 14.578 21.6812 14.998 21.5492C16.385 21.1122 17.677 20.3692 18.748 19.3932C19.078 19.0922 19.165 18.6052 18.967 18.2052C18.135 16.5262 19.0921 14.9852 20.8101 14.9552C21.2661 14.9472 21.6721 14.6482 21.7791 14.2052C21.9521 13.4882 21.998 12.8642 21.998 11.9862C21.998 11.2322 21.909 10.4892 21.748 9.76823C21.646 9.31123 21.2471 8.98723 20.7791 8.98623C19.0881 8.98323 18.14 7.32123 18.967 5.86123C19.197 5.45523 19.1251 4.95723 18.7791 4.64323C17.6891 3.65323 16.36 2.87922 14.967 2.45522C14.539 2.32522 14.084 2.48523 13.842 2.86123C12.876 4.36223 11.072 4.38823 10.123 2.89323C9.88005 2.50923 9.39904 2.31822 8.96704 2.45522ZM15.045 4.57923C15.728 4.86523 16.267 5.16423 16.886 5.63323C16.16 7.93023 17.391 10.3162 19.941 10.8972C20.004 11.3102 19.998 11.5602 19.998 11.9862C19.998 12.4952 20.0051 12.6742 19.9471 13.0422C17.4081 13.5682 16.152 15.8872 16.859 18.3382C16.251 18.7792 15.816 19.0852 15.053 19.3802C13.261 17.5562 10.7731 17.4762 8.94305 19.3922C8.22905 19.0782 7.68006 18.7992 7.12506 18.3302C7.81306 15.8412 6.65005 13.6922 4.06805 13.0402C3.95305 12.5842 3.99905 11.2962 4.06505 10.9052C6.73505 10.2652 7.78705 7.90223 7.12405 5.62623C7.71005 5.18523 8.23706 4.86423 8.92206 4.58623C10.6471 6.34123 13.237 6.51623 15.045 4.57923ZM11.998 7.98623C9.78905 7.98623 7.99805 9.77723 7.99805 11.9862C7.99805 14.1962 9.78905 15.9862 11.998 15.9862C14.207 15.9862 15.998 14.1962 15.998 11.9862C15.998 9.77723 14.207 7.98623 11.998 7.98623ZM11.998 9.98623C13.103 9.98623 13.998 10.8822 13.998 11.9862C13.998 13.0912 13.103 13.9862 11.998 13.9862C10.893 13.9862 9.99805 13.0912 9.99805 11.9862C9.99805 10.8822 10.893 9.98623 11.998 9.98623Z" fill="#2F3030"/> +<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.7"> +<path d="M10 12.5C11.3807 12.5 12.5 11.3807 12.5 10C12.5 8.61929 11.3807 7.5 10 7.5C8.61929 7.5 7.5 8.61929 7.5 10C7.5 11.3807 8.61929 12.5 10 12.5Z" stroke="#171717" stroke-width="1.25" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M1.66602 10.7334V9.26669C1.66602 8.40003 2.37435 7.68336 3.24935 7.68336C4.75768 7.68336 5.37435 6.6167 4.61602 5.30836C4.18268 4.55836 4.44102 3.58336 5.19935 3.15003L6.64102 2.32503C7.29935 1.93336 8.14935 2.1667 8.54102 2.82503L8.63268 2.98336C9.38268 4.2917 10.616 4.2917 11.3743 2.98336L11.466 2.82503C11.8577 2.1667 12.7077 1.93336 13.366 2.32503L14.8077 3.15003C15.566 3.58336 15.8243 4.55836 15.391 5.30836C14.6327 6.6167 15.2493 7.68336 16.7577 7.68336C17.6243 7.68336 18.341 8.39169 18.341 9.26669V10.7334C18.341 11.6 17.6327 12.3167 16.7577 12.3167C15.2493 12.3167 14.6327 13.3834 15.391 14.6917C15.8243 15.45 15.566 16.4167 14.8077 16.85L13.366 17.675C12.7077 18.0667 11.8577 17.8334 11.466 17.175L11.3743 17.0167C10.6243 15.7084 9.39102 15.7084 8.63268 17.0167L8.54102 17.175C8.14935 17.8334 7.29935 18.0667 6.64102 17.675L5.19935 16.85C4.44102 16.4167 4.18268 15.4417 4.61602 14.6917C5.37435 13.3834 4.75768 12.3167 3.24935 12.3167C2.37435 12.3167 1.66602 11.6 1.66602 10.7334Z" stroke="#171717" stroke-width="1.25" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> +</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 0000000000000..d6c6b6d8096d1 --- /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/flowy_icons/40x/m_empty_page.svg b/frontend/resources/flowy_icons/40x/m_empty_page.svg new file mode 100644 index 0000000000000..c64507dd0ac2c --- /dev/null +++ b/frontend/resources/flowy_icons/40x/m_empty_page.svg @@ -0,0 +1,6 @@ +<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.4"> +<path d="M18 44H30C40 44 44 40 44 30V18C44 8 40 4 30 4H18C8 4 4 8 4 18V30C4 40 8 44 18 44Z" stroke="#171717" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M4 26.0005H11.52C13.04 26.0005 14.42 26.8605 15.1 28.2205L16.88 31.8005C18 34.0005 20 34.0005 20.48 34.0005H27.54C29.06 34.0005 30.44 33.1405 31.12 31.7805L32.9 28.2005C33.58 26.8405 34.96 25.9805 36.48 25.9805H43.96" stroke="#171717" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +</g> +</svg> diff --git a/frontend/resources/translations/ar-SA.json b/frontend/resources/translations/ar-SA.json index 077ca23e034e4..136bfb3fa1ed8 100644 --- a/frontend/resources/translations/ar-SA.json +++ b/frontend/resources/translations/ar-SA.json @@ -1169,4 +1169,4 @@ "addField": "إضافة حقل", "userIcon": "رمز المستخدم" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/ca-ES.json b/frontend/resources/translations/ca-ES.json index a9a86fb5fbca2..d00c2c4114586 100644 --- a/frontend/resources/translations/ca-ES.json +++ b/frontend/resources/translations/ca-ES.json @@ -812,4 +812,4 @@ "deleteContentTitle": "Esteu segur que voleu suprimir {pageType}?", "deleteContentCaption": "si suprimiu aquest {pageType}, podeu restaurar-lo des de la paperera." } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/ckb-KU.json b/frontend/resources/translations/ckb-KU.json index 1bbd3ccd25eab..cb8befcd8a9de 100644 --- a/frontend/resources/translations/ckb-KU.json +++ b/frontend/resources/translations/ckb-KU.json @@ -946,4 +946,4 @@ "frequentlyUsed": "زۆرجار بەکارت هێناوە" } } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/cs-CZ.json b/frontend/resources/translations/cs-CZ.json index 76453591a5fc2..dab1d101e23b6 100644 --- a/frontend/resources/translations/cs-CZ.json +++ b/frontend/resources/translations/cs-CZ.json @@ -1094,4 +1094,4 @@ "font": "Písmo", "actions": "Příkazy" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/de-DE.json b/frontend/resources/translations/de-DE.json index 11c8da8e62f1f..f0c08ca52e3f8 100644 --- a/frontend/resources/translations/de-DE.json +++ b/frontend/resources/translations/de-DE.json @@ -47,6 +47,7 @@ "unmatchedPasswordError": "Passwörter stimmen nicht überein", "syncPromptMessage": "Synchronisation kann ein paar Minuten dauern. Diese Seite bitte nicht schließen", "or": "ODER", + "signInWith": "Anmeldeoptionen:", "signInWithGoogle": "Mit Google anmelden", "signInWithGithub": "Mit Github anmelden", "signInWithDiscord": "Mit Discord anmelden", @@ -64,38 +65,36 @@ "alreadyHaveAnAccount": "Du hast bereits ein Konto?", "logIn": "Anmeldung", "generalError": "Etwas ist schiefgelaufen. Bitte versuche es später noch einmal", - "limitRateError": "Aus Sicherheitsgründen kannst du nur alle 60 Sekunden einen Authentifizierungslink anfordern", - "LogInWithGoogle": "Mit Google-Account anmelden", - "LogInWithGithub": "Mit GitHub-Account anmelden", - "LogInWithDiscord": "Mit Discord-Account anmelden" + "limitRateError": "Aus Sicherheitsgründen kannst du nur alle 60 Sekunden einen Authentifizierungslink anfordern" }, "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 @:appName-Instanzen zu schließen & versuche es erneut.", + "chooseWorkspace": "Arbeitsbereich wählen", + "create": "Arbeitsbereich erstellen", + "reset": "Arbeitsbereich zurücksetzen", + "renameWorkspace": "Arbeitsbereich umbenennen", + "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", @@ -139,7 +138,9 @@ "openNewTab": "In einem neuen Tab öffnen", "moveTo": "Verschieben nach", "addToFavorites": "Zu Favoriten hinzufügen", - "copyLink": "Link kopieren" + "copyLink": "Link kopieren", + "changeIcon": "Symbol ändern", + "collapseAllPages": "Alle Seiten einklappen" }, "blankPageTitle": "Leere Seite", "newPageText": "Neue Seite", @@ -245,6 +246,9 @@ "addAPageToPrivate": "Eine Seite zum privaten Bereich hinzufügen.", "addAPageToWorkspace": "Eine Seite zum Arbeitsbereich hinzufügen", "recent": "Zuletzt", + "today": "Heute", + "thisWeek": "Diese Woche", + "others": "Andere", "public": "Öffentlich", "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." @@ -302,7 +306,8 @@ "back": "Zurück", "signInGoogle": "Mit einem Google Benutzerkonto anmelden", "signInGithub": "Mit einem Github Benutzerkonto anmelden", - "signInDiscord": "Mit einem Discord Benutzerkonto anmelden" + "signInDiscord": "Mit einem Discord Benutzerkonto anmelden", + "more": "Mehr" }, "label": { "welcome": "Willkommen!", @@ -358,18 +363,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 +387,7 @@ "description": "Wähle ein voreingestelltes Design aus oder lade dein eigenes benutzerdefiniertes Design hoch." }, "workspaceFont": { - "title": "Schriftart Arbeitsbereich" + "title": "Schriftart" }, "textDirection": { "title": "Textrichtung", @@ -428,7 +434,7 @@ "manageDataPage": { "menuLabel": "Daten verwalten", "title": "Daten verwalten", - "description": "Verwalte den lokalen Datenspeicher oder importiere deine vorhandenen Daten in @:appName. Du kannst deine Daten mit End-to-End-Verschlüsselung absichern.", + "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", @@ -437,7 +443,8 @@ "open": "Ordner öffnen", "openTooltip": "Aktuellen Speicherort des Datenordners öffnen", "copy": "Pfad kopieren", - "copiedHint": "Link kopiert!" + "copiedHint": "Link kopiert!", + "resetTooltip": "Auf Standardspeicherort zurücksetzen" }, "resetDialog": { "title": "Bist du sicher?", @@ -446,8 +453,8 @@ }, "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", + "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": { @@ -549,8 +556,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", @@ -611,7 +618,7 @@ "label": "Mitglieder", "user": "Nutzer", "role": "Rolle", - "removeFromWorkspace": "Vom Workspace entfernen", + "removeFromWorkspace": "Vom Arbeitsbereich entfernen", "owner": "Besitzer", "guest": "Gast", "member": "Mitglied", @@ -654,7 +661,7 @@ "openFolderDesc": "Öffnen und speichern im vorhandenen @:appName-Ordner", "folderHintText": "Ordnernamen", "location": "Ein neuen Ordner erstellen", - "locationDesc": "Einen Namen für den @:appName-Datenordner festlegen", + "locationDesc": "Einen Namen für den @:appName Datenordner festlegen", "browser": "Durchsuchen", "create": "Erstellen", "set": "Festlegen", @@ -742,7 +749,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", @@ -1210,6 +1221,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": { @@ -1257,7 +1269,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", @@ -1267,7 +1281,13 @@ "today": "Heute", "jumpToday": "Springe zu Heute", "previousMonth": "Vorheriger Monat", - "nextMonth": "Nächster Monat" + "nextMonth": "Nächster Monat", + "views": { + "day": "Tag", + "week": "Woche", + "month": "Monat", + "year": "Jahr" + } }, "mobileEventScreen": { "emptyTitle": "Noch keine Events", @@ -1287,7 +1307,8 @@ }, "unscheduledEventsTitle": "Ungeplante Events", "clickToAdd": "Klicken Sie, um es zum Kalender hinzuzufügen", - "name": "Kalendereinstellungen" + "name": "Kalendereinstellungen", + "clickToOpen": "Hier klicken, um den Eintrag zu öffnen" }, "referencedCalendarPrefix": "Sicht von", "quickJumpYear": "Spring zu", @@ -1578,7 +1599,9 @@ }, "favorite": { "noFavorite": "Leere Favoritenseite", - "noFavoriteHintText": "Nach links wischen, um es den Favoriten hinzuzufügen" + "noFavoriteHintText": "Nach links wischen, um es den Favoriten hinzuzufügen", + "removeFromSidebar": "Aus der Seitenleiste entfernen", + "addToSidebar": "An Seitenleiste anheften" }, "cardDetails": { "notesPlaceholder": "'/'-Taste, um einen Block einzufügen oder Text eingeben" @@ -1618,7 +1641,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": { @@ -1627,10 +1650,11 @@ "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", + "chooseAnIcon": "Symbol auswählen", "appearance": { "name": "Aussehen", "themeMode": { @@ -1676,4 +1700,4 @@ "betaTooltip": "Wir unterstützen derzeit nur die Suche nach Seiten", "fromTrashHint": "Aus dem Mülleimer" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/el-GR.json b/frontend/resources/translations/el-GR.json index 0ee0bc10cd219..633a4adf65095 100644 --- a/frontend/resources/translations/el-GR.json +++ b/frontend/resources/translations/el-GR.json @@ -201,8 +201,8 @@ "addBlockBelow": "Add a block below" }, "sideBar": { - "closeSidebar": "Close side bar", - "openSidebar": "Open side bar", + "closeSidebar": "Close sidebar", + "openSidebar": "Open sidebar", "personal": "Personal", "favorites": "Favorites", "clickToHidePersonal": "Click to hide personal section", diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 1e752eb702e21..ba26e5c405eef 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -70,6 +70,7 @@ "chooseWorkspace": "Choose your workspace", "create": "Create workspace", "reset": "Reset workspace", + "renameWorkspace": "Rename workspace", "resetWorkspacePrompt": "Resetting the workspace will delete all pages and data within it. Are you sure you want to reset the workspace? Alternatively, you can contact the support team to restore the workspace", "hint": "workspace", "notFoundError": "Workspace not found", @@ -136,7 +137,9 @@ "openNewTab": "Open in a new tab", "moveTo": "Move to", "addToFavorites": "Add to Favorites", - "copyLink": "Copy Link" + "copyLink": "Copy Link", + "changeIcon": "Change icon", + "collapseAllPages": "Collapse all subpages" }, "blankPageTitle": "Blank page", "newPageText": "New page", @@ -226,8 +229,8 @@ "genSummary": "Generate summary" }, "sideBar": { - "closeSidebar": "Close side bar", - "openSidebar": "Open side bar", + "closeSidebar": "Close sidebar", + "openSidebar": "Open sidebar", "personal": "Personal", "private": "Private", "workspace": "Workspace", @@ -239,7 +242,23 @@ "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", + "justNow": "just now", + "minutesAgo": "{count} minutes ago", + "lastViewed": "Last viewed", + "favoriteAt": "Favorited at", + "emptyRecent": "No Recent Documents", + "emptyRecentDescription": "As you view documents, they will appear here for easy retrieval", + "emptyFavorite": "No Favorite Documents", + "emptyFavoriteDescription": "Start exploring and mark documents as favorites. They’ll be listed here for quick access!", + "removePageFromRecent": "Remove this page from the Recent?", + "removeSuccess": "Removed successfully", + "favoriteSpace": "Favorites", + "RecentSpace": "Recent", + "Spaces": "Spaces" }, "notifications": { "export": { @@ -278,6 +297,7 @@ "update": "Update", "share": "Share", "removeFromFavorites": "Remove from favorites", + "removeFromRecent": "Remove from recent", "addToFavorites": "Add to favorites", "rename": "Rename", "helpCenter": "Help Center", @@ -294,7 +314,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 +374,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", @@ -429,7 +451,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?", @@ -524,6 +547,10 @@ "enableNotifications": { "label": "Enable notifications", "hint": "Turn off to stop local notifications from appearing." + }, + "showNotificationsIcon": { + "label": "Show notifications icon", + "hint": "Turn off to hide notification icons in the app." } }, "appearance": { @@ -1113,7 +1140,16 @@ "newDatabase": "New Database", "linkToDatabase": "Link to Database" }, - "date": "Date" + "date": "Date", + "video": { + "label": "Video", + "emptyLabel": "Add a video", + "placeholder": "Enter a link to a video", + "copiedToPasteBoard": "The video link has been copied to the clipboard", + "insertVideo": "Insert video", + "invalidVideoUrl": "Invalid video url, must be a valid file.", + "supportedFormats": "Supported formats: MP4, WebM, MOV, AVI, FLV, MPEG/M4V, H.264" + } }, "outlineBlock": { "placeholder": "Table of Contents" @@ -1253,7 +1289,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", @@ -1263,7 +1301,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", @@ -1283,7 +1327,8 @@ }, "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", @@ -1574,7 +1619,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" @@ -1624,9 +1671,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": { @@ -1672,4 +1720,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 1393cbc7e1cfc..6177cd53778cb 100644 --- a/frontend/resources/translations/es-VE.json +++ b/frontend/resources/translations/es-VE.json @@ -42,7 +42,7 @@ "emailHint": "Correo", "passwordHint": "Contraseña", "dontHaveAnAccount": "¿No posee credenciales?", - "createAccount": "Crear cuenta", + "createAccount": "Crear una cuenta", "repeatPasswordEmptyError": "La contraseña no puede estar en blanco", "unmatchedPasswordError": "Las contraseñas no coinciden", "syncPromptMessage": "La sincronización de los datos puede tardar un poco. Por favor no cierres esta página", @@ -64,12 +64,7 @@ "alreadyHaveAnAccount": "¿Ya tienes cuenta?", "logIn": "Iniciar sesión", "generalError": "Algo ha salido mal. Por favor, inténtalo más tarde", - "limitRateError": "Por razones de seguridad, solo puedes solicitar un enlace mágico cada 60 segundos", - "LogInWithGoogle": "Iniciar sesión con Google", - "LogInWithGithub": "Iniciar sesión con Github", - "LogInWithDiscord": "Iniciar sesión con Discord", - "loginAsGuestButtonText": "Empezar", - "logInWithMagicLink": "Iniciar sesión con Enlace Mágico" + "limitRateError": "Por razones de seguridad, solo puedes solicitar un enlace mágico cada 60 segundos" }, "workspace": { "chooseWorkspace": "Elige tu espacio de trabajo", @@ -425,7 +420,7 @@ "dark": "Modo Oscuro", "system": "Adapt to System" }, - "fontScaleFactor": "Escala de la fuente", + "fontScaleFactor": "Factor de escala de fuente", "documentSettings": { "cursorColor": "Color del cursor del documento", "selectionColor": "Color de selección de documento", @@ -483,7 +478,7 @@ "enableRTLToolbarItems": "Habilitar elementos de la barra de herramientas RTL", "members": { "title": "Configuración de miembros", - "inviteMembers": "Invitar miembros", + "inviteMembers": "Invitar a los miembros", "sendInvite": "Enviar invitación", "copyInviteLink": "Copiar enlace de invitación", "label": "Miembros", @@ -494,8 +489,7 @@ "guest": "Invitado", "member": "Miembro", "memberHintText": "Un miembro puede leer, comentar y editar páginas. Invitar a miembros e invitados.", - "guestHintText": "Un Invitado puede leer, reaccionar, comentar y editar ciertas páginas con permiso.", - "emailInvalidError": "Email no válido, compruébalo y vuelve a intentarlo.", + "emailInvalidError": "Correo electrónico no válido, compruébalo y vuelve a intentarlo.", "emailSent": "Email enviado, por favor revisa la bandeja de entrada", "members": "miembros", "membersCount": { @@ -577,7 +571,6 @@ "codeBlockOutdentLines": "Eliminar dos espacios al inicio de la línea en el bloque de código", "codeBlockAddTwoSpaces": "Insertar dos espacios en la posición del cursor en el bloque de código", "codeBlockSelectAll": "Seleccionar todo el contenido dentro de un bloque de código", - "codeBlockPasteText": "Pegar texto en bloque de código", "textAlignLeft": "Alinear texto a la izquierda", "textAlignCenter": "Alinear el texto al centro", "textAlignRight": "Alinear el texto a la derecha" @@ -595,7 +588,7 @@ "userAgreement": "Acuerdo del Usuario", "termsAndConditions": "Términos y condiciones", "userprofileError": "No se pudo cargar el perfil de usuario", - "userprofileErrorDescription": "Intenta cerrar sesión y volver a entrar para comprobar si el problema persiste.", + "userprofileErrorDescription": "Intente cerrar sesión y volver a iniciarla para comprobar si el problema persiste.", "selectLayout": "Seleccionar diseño", "selectStartingDay": "Seleccione el día de inicio", "version": "Versión" @@ -678,8 +671,8 @@ "choicechipPrefix": { "before": "Antes", "after": "Después", - "onOrBefore": "En o antes de", - "onOrAfter": "En o después de", + "onOrBefore": "En o antes", + "onOrAfter": "Sobre o después", "isEmpty": "Está vacio", "isNotEmpty": "No está vacío" } @@ -1543,4 +1536,4 @@ "betaTooltip": "Actualmente solo admitimos la búsqueda de páginas.", "fromTrashHint": "De la papelera" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/eu-ES.json b/frontend/resources/translations/eu-ES.json index a070a59c54ea9..e7029a0d38b15 100644 --- a/frontend/resources/translations/eu-ES.json +++ b/frontend/resources/translations/eu-ES.json @@ -601,4 +601,4 @@ "deleteContentTitle": "Ziur {pageType} ezabatu nahi duzula?", "deleteContentCaption": "{pageType} hau ezabatzen baduzu, zaborrontzitik leheneratu dezakezu." } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/fa.json b/frontend/resources/translations/fa.json index 1f9b3526deed3..147bca02b6710 100644 --- a/frontend/resources/translations/fa.json +++ b/frontend/resources/translations/fa.json @@ -674,4 +674,4 @@ "frequentlyUsed": "استفاده‌شده" } } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/fr-CA.json b/frontend/resources/translations/fr-CA.json index 9fd37affceeee..f5bbb4b2db313 100644 --- a/frontend/resources/translations/fr-CA.json +++ b/frontend/resources/translations/fr-CA.json @@ -1262,4 +1262,4 @@ "userIcon": "Icône utilisateur" }, "noLogFiles": "Il n'y a pas de log" -} +} \ No newline at end of file diff --git a/frontend/resources/translations/fr-FR.json b/frontend/resources/translations/fr-FR.json index c24ce58aa7a3b..e258e9099b6e9 100644 --- a/frontend/resources/translations/fr-FR.json +++ b/frontend/resources/translations/fr-FR.json @@ -1573,4 +1573,4 @@ "loadingTooltip": "Nous recherchons des résultats...", "betaTooltip": "Nous ne prenons actuellement en charge que la recherche de pages" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/hu-HU.json b/frontend/resources/translations/hu-HU.json index 7c25d96d1450a..1a60a1c6f5d8d 100644 --- a/frontend/resources/translations/hu-HU.json +++ b/frontend/resources/translations/hu-HU.json @@ -599,4 +599,4 @@ "deleteContentTitle": "Biztosan törli a következőt: {pageType}?", "deleteContentCaption": "ha törli ezt a {pageType} oldalt, visszaállíthatja a kukából." } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/id-ID.json b/frontend/resources/translations/id-ID.json index 90c1835f57462..286f5a3e06618 100644 --- a/frontend/resources/translations/id-ID.json +++ b/frontend/resources/translations/id-ID.json @@ -1022,4 +1022,4 @@ "noFavorite": "Tidak ada halaman favorit", "noFavoriteHintText": "Geser halaman ke kiri untuk menambahkannya ke favorit Anda" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/it-IT.json b/frontend/resources/translations/it-IT.json index afed35474914c..74282485d2433 100644 --- a/frontend/resources/translations/it-IT.json +++ b/frontend/resources/translations/it-IT.json @@ -1262,4 +1262,4 @@ "userIcon": "Icona utente" }, "noLogFiles": "Non ci sono file di log" -} +} \ No newline at end of file diff --git a/frontend/resources/translations/ja-JP.json b/frontend/resources/translations/ja-JP.json index 5dc6c6328572b..00738dde42956 100644 --- a/frontend/resources/translations/ja-JP.json +++ b/frontend/resources/translations/ja-JP.json @@ -686,4 +686,4 @@ "deleteContentTitle": "{pageType} を削除してもよろしいですか?", "deleteContentCaption": "この {pageType} を削除しても、ゴミ箱から復元できます。" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/ko-KR.json b/frontend/resources/translations/ko-KR.json index ecfc4dfe5bab1..b0b7a472b4c91 100644 --- a/frontend/resources/translations/ko-KR.json +++ b/frontend/resources/translations/ko-KR.json @@ -598,4 +598,4 @@ "deleteContentTitle": "{pageType}을(를) 삭제하시겠습니까?", "deleteContentCaption": "이 {pageType}을(를) 삭제하면 휴지통에서 복원할 수 있습니다." } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/pl-PL.json b/frontend/resources/translations/pl-PL.json index 5ee2bc91e5f91..389eaa068d93d 100644 --- a/frontend/resources/translations/pl-PL.json +++ b/frontend/resources/translations/pl-PL.json @@ -1077,4 +1077,4 @@ "language": "Język", "font": "Czcionka" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/pt-BR.json b/frontend/resources/translations/pt-BR.json index de9dca7320b15..dc1340d3b8a31 100644 --- a/frontend/resources/translations/pt-BR.json +++ b/frontend/resources/translations/pt-BR.json @@ -1219,4 +1219,4 @@ "addField": "Adicionar campo", "userIcon": "Ícone do usuário" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/pt-PT.json b/frontend/resources/translations/pt-PT.json index 699be162e8e85..1b5ee1fcd1257 100644 --- a/frontend/resources/translations/pt-PT.json +++ b/frontend/resources/translations/pt-PT.json @@ -857,4 +857,4 @@ "noResult": "Nenhum resultado", "caseSensitive": "Maiúsculas e minúsculas" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/ru-RU.json b/frontend/resources/translations/ru-RU.json index 8034301d0e45d..89b81271dc3ec 100644 --- a/frontend/resources/translations/ru-RU.json +++ b/frontend/resources/translations/ru-RU.json @@ -1311,4 +1311,4 @@ "userIcon": "Пользовательская иконка" }, "noLogFiles": "Нет файлов журналов" -} +} \ No newline at end of file diff --git a/frontend/resources/translations/sv-SE.json b/frontend/resources/translations/sv-SE.json index 2231ec75fb7b3..50be68350d11d 100644 --- a/frontend/resources/translations/sv-SE.json +++ b/frontend/resources/translations/sv-SE.json @@ -668,4 +668,4 @@ "deleteContentTitle": "Är du säker på att du vill ta bort {pageType}?", "deleteContentCaption": "om du tar bort denna {pageType} kan du återställa den från papperskorgen." } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/tr-TR.json b/frontend/resources/translations/tr-TR.json index 8648129b27e31..66dd2e0d69700 100644 --- a/frontend/resources/translations/tr-TR.json +++ b/frontend/resources/translations/tr-TR.json @@ -1477,4 +1477,4 @@ "betaTooltip": "Şu anda yalnızca sayfaları aramayı destekliyoruz", "fromTrashHint": "Çöp kutusundan" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/uk-UA.json b/frontend/resources/translations/uk-UA.json index 3c36875f96fdb..9e0cdf64f41b5 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 8be445a75df46..f0b4c0b10d972 100644 --- a/frontend/resources/translations/vi-VN.json +++ b/frontend/resources/translations/vi-VN.json @@ -868,4 +868,4 @@ "font": "Phông chữ", "date": "Ngày" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/vi.json b/frontend/resources/translations/vi.json index b921c1844e2e7..4d1716447aa81 100644 --- a/frontend/resources/translations/vi.json +++ b/frontend/resources/translations/vi.json @@ -6,4 +6,4 @@ "failedToLoad": "Không tải được chế độ xem bảng" } } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/zh-CN.json b/frontend/resources/translations/zh-CN.json index ed9cf5e0bd7a5..72240f857a66e 100644 --- a/frontend/resources/translations/zh-CN.json +++ b/frontend/resources/translations/zh-CN.json @@ -1357,4 +1357,4 @@ "addField": "添加字段", "userIcon": "用户图标" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/zh-TW.json b/frontend/resources/translations/zh-TW.json index c6f0520f05078..e49eca0e7ea36 100644 --- a/frontend/resources/translations/zh-TW.json +++ b/frontend/resources/translations/zh-TW.json @@ -1479,4 +1479,4 @@ "betaLabel": "BETA", "betaTooltip": "目前我們只支援搜尋頁面" } -} +} \ No newline at end of file diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 0560f222de22e..d9a9d047e5fd5 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -1218,6 +1218,7 @@ dependencies = [ "lib-log", "parking_lot 0.12.1", "protobuf", + "semver", "serde", "serde_json", "serde_repr", @@ -1509,6 +1510,7 @@ dependencies = [ "parking_lot 0.12.1", "protobuf", "rand 0.8.5", + "semver", "serde", "serde_json", "strum", @@ -2035,6 +2037,7 @@ dependencies = [ "postgrest", "rand 0.8.5", "reqwest", + "semver", "serde", "serde_json", "thiserror", @@ -4800,9 +4803,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/dart-ffi/Cargo.toml b/frontend/rust-lib/dart-ffi/Cargo.toml index 4e5148da95e14..e17ede23e57cd 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 48f3e485a24ac..a7f3e2ee193d8 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 26d561e993707..d427a8f16c626 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/lib.rs b/frontend/rust-lib/event-integration-test/src/lib.rs index 180623d8d5b4b..cddd304dde6ef 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/script.rs b/frontend/rust-lib/event-integration-test/tests/folder/local_test/script.rs index eb311cfea7ea0..fe55ad1bd2f89 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 @@ -158,7 +158,9 @@ impl FolderTest { assert_eq!(self.child_view, view, "View not equal"); }, FolderScript::ReadView(view_id) => { - let view = read_view(sdk, &view_id).await; + let mut view = read_view(sdk, &view_id).await; + // Ignore the last edited time + view.last_edited = 0; self.child_view = view; }, FolderScript::UpdateView { @@ -196,7 +198,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 +377,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 53919ad9b012f..fd8fbe335fcc4 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 b0c0acbdcf613..2959ae7f21183 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 36addf0fe72f8..2ba29b83fdd55 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-folder/src/entities/view.rs b/frontend/rust-lib/flowy-folder/src/entities/view.rs index 004f793e11b69..876b8a69f338a 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/view.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/view.rs @@ -60,6 +60,18 @@ pub struct ViewPB { #[pb(index = 9, one_of)] pub extra: Option<String>, + + // user_id + #[pb(index = 10, one_of)] + pub created_by: Option<i64>, + + // timestamp + #[pb(index = 11)] + pub last_edited: i64, + + // user_id + #[pb(index = 12, one_of)] + pub last_edited_by: Option<i64>, } pub fn view_pb_without_child_views(view: View) -> ViewPB { @@ -73,6 +85,9 @@ pub fn view_pb_without_child_views(view: View) -> ViewPB { icon: view.icon.clone().map(|icon| icon.into()), is_favorite: view.is_favorite, extra: view.extra, + created_by: view.created_by, + last_edited: view.last_edited_time, + last_edited_by: view.last_edited_by, } } @@ -87,6 +102,9 @@ pub fn view_pb_without_child_views_from_arc(view: Arc<View>) -> ViewPB { icon: view.icon.clone().map(|icon| icon.into()), is_favorite: view.is_favorite, extra: view.extra.clone(), + created_by: view.created_by, + last_edited: view.last_edited_time, + last_edited_by: view.last_edited_by, } } @@ -105,6 +123,9 @@ pub fn view_pb_with_child_views(view: Arc<View>, child_views: Vec<Arc<View>>) -> icon: view.icon.clone().map(|icon| icon.into()), is_favorite: view.is_favorite, extra: view.extra.clone(), + created_by: view.created_by, + last_edited: view.last_edited_time, + last_edited_by: view.last_edited_by, } } @@ -152,6 +173,26 @@ pub struct RepeatedViewPB { pub items: Vec<ViewPB>, } +#[derive(Eq, PartialEq, Debug, Default, ProtoBuf, Clone)] +pub struct RepeatedFavoriteViewPB { + #[pb(index = 1)] + pub items: Vec<SectionViewPB>, +} + +#[derive(Eq, PartialEq, Debug, Default, ProtoBuf, Clone)] +pub struct RepeatedRecentViewPB { + #[pb(index = 1)] + pub items: Vec<SectionViewPB>, +} + +#[derive(Eq, PartialEq, Debug, Default, ProtoBuf, Clone)] +pub struct SectionViewPB { + #[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 d6d36b683ebb7..888c26d2a6d56 100644 --- a/frontend/rust-lib/flowy-folder/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder/src/event_handler.rs @@ -278,31 +278,37 @@ 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(SectionViewPB { + item: view, + timestamp: item.timestamp, + }); } } - data_result_ok(RepeatedViewPB { items: views }) + data_result_ok(RepeatedFavoriteViewPB { items: views }) } #[tracing::instrument(level = "debug", skip(folder), err)] pub(crate) async fn read_recent_views_handler( folder: AFPluginState<Weak<FolderManager>>, -) -> DataResult<RepeatedViewPB, FlowyError> { +) -> DataResult<RepeatedRecentViewPB, FlowyError> { let folder = upgrade_folder(folder)?; let recent_items = folder.get_my_recent_sections().await; let mut views = vec![]; for item in recent_items { if let Ok(view) = folder.get_view_pb(&item.id).await { - views.push(view); + views.push(SectionViewPB { + item: view, + timestamp: item.timestamp, + }); } } - data_result_ok(RepeatedViewPB { items: views }) + data_result_ok(RepeatedRecentViewPB { items: views }) } #[tracing::instrument(level = "debug", skip(folder), err)] diff --git a/frontend/rust-lib/flowy-folder/src/event_map.rs b/frontend/rust-lib/flowy-folder/src/event_map.rs index 31034bd1437f5..febfc49b5ef12 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")] @@ -155,7 +155,7 @@ pub enum FolderEvent { #[event(input = "UpdateViewIconPayloadPB")] UpdateViewIcon = 35, - #[event(output = "RepeatedViewPB")] + #[event(output = "RepeatedRecentViewPB")] ReadRecentViews = 36, // used for add or remove recent views, like history diff --git a/frontend/rust-lib/flowy-folder/src/notification.rs b/frontend/rust-lib/flowy-folder/src/notification.rs index c57450a5d6d4a..1ddcebcafdd50 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 b64b30fa496df..d11cb2b594a46 100644 --- a/frontend/rust-lib/flowy-server/Cargo.toml +++ b/frontend/rust-lib/flowy-server/Cargo.toml @@ -48,6 +48,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 1fd6a5b03fe31..c758be253a869 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 224e10cd958f4..71dacfab048ed 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 0624ec053b52c..8ebe5fc8ef52a 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 @@ -48,7 +48,7 @@ RUN dart pub global activate protoc_plugin 21.1.2 # Install build dependencies for AppFlowy RUN yay -S --noconfirm jemalloc4 cargo-make cargo-binstall -RUN sudo pacman -S --noconfirm git libkeybinder3 sqlite clang rsync libnotify rocksdb zstd +RUN sudo pacman -S --noconfirm git libkeybinder3 sqlite clang rsync libnotify rocksdb zstd mpv RUN sudo ln -s /usr/bin/sha1sum /usr/bin/shasum RUN source ~/.cargo/env && cargo binstall duckscript_cli -y diff --git a/frontend/scripts/install_dev_env/install_ios.sh b/frontend/scripts/install_dev_env/install_ios.sh index 0ce5cfb5d428a..653eb8f1b374d 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 d1f85445a2673..b02b31d62c42a 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 @@ -78,7 +78,17 @@ if command apt-get &>/dev/null; then elif command dnf &>/dev/null; then sudo dnf install libnotify-dev else - echo 'Your system is not supported, please install libnotify manually.' + echo 'Your system is not supported, please install libnotify-dev manually.' +fi + +# For Video Block support +printMessage "Installing libmpv-dev mpv" +if command apt-get &>/dev/null; then + sudo apt-get install libmpv-dev mpv +elif command dnf &>/dev/null; then + sudo dnf install libmpv-dev mpv +else + echo 'Your system is not supported, please install libmpv-dev mpv manually.' fi # Add the githooks directory to your git configuration diff --git a/frontend/scripts/install_dev_env/install_macos.sh b/frontend/scripts/install_dev_env/install_macos.sh index 10f894e13c525..8613b904c6559 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 aef80844a0e0a..1d68a677ae032 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/linux_distribution/appimage/AppImageBuilder.yml b/frontend/scripts/linux_distribution/appimage/AppImageBuilder.yml index c20581440154c..5049ff5eb3c18 100644 --- a/frontend/scripts/linux_distribution/appimage/AppImageBuilder.yml +++ b/frontend/scripts/linux_distribution/appimage/AppImageBuilder.yml @@ -19,47 +19,55 @@ AppDir: exec_args: $@ apt: arch: - - amd64 + - amd64 allow_unauthenticated: true sources: - - sourceline: deb http://id.archive.ubuntu.com/ubuntu/ jammy main restricted - - sourceline: deb http://id.archive.ubuntu.com/ubuntu/ jammy-updates main restricted - - sourceline: deb http://id.archive.ubuntu.com/ubuntu/ jammy universe - - sourceline: deb http://id.archive.ubuntu.com/ubuntu/ jammy-updates universe - - sourceline: deb http://id.archive.ubuntu.com/ubuntu/ jammy multiverse - - sourceline: deb http://id.archive.ubuntu.com/ubuntu/ jammy-updates multiverse - - sourceline: deb http://id.archive.ubuntu.com/ubuntu/ jammy-backports main restricted - universe multiverse - - sourceline: deb http://security.ubuntu.com/ubuntu jammy-security main restricted - - sourceline: deb http://security.ubuntu.com/ubuntu jammy-security universe - - sourceline: deb http://security.ubuntu.com/ubuntu jammy-security multiverse - - sourceline: deb https://ppa.launchpadcontent.net/touchegg/stable/ubuntu/ jammy - main - - sourceline: deb https://packagecloud.io/slacktechnologies/slack/debian/ jessie - main - - sourceline: deb [arch=amd64 signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] - https://cli.github.com/packages stable main - - sourceline: deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x - jammy main - - sourceline: deb [arch=amd64,arm64,armhf] http://packages.microsoft.com/repos/code - stable main - - sourceline: deb [arch=amd64] https://dl.google.com/linux/chrome/deb/ stable - main + - sourceline: deb http://id.archive.ubuntu.com/ubuntu/ jammy main restricted + - sourceline: deb http://id.archive.ubuntu.com/ubuntu/ jammy-updates main restricted + - sourceline: deb http://id.archive.ubuntu.com/ubuntu/ jammy universe + - sourceline: deb http://id.archive.ubuntu.com/ubuntu/ jammy-updates universe + - sourceline: deb http://id.archive.ubuntu.com/ubuntu/ jammy multiverse + - sourceline: deb http://id.archive.ubuntu.com/ubuntu/ jammy-updates multiverse + - sourceline: + deb http://id.archive.ubuntu.com/ubuntu/ jammy-backports main restricted + universe multiverse + - sourceline: deb http://security.ubuntu.com/ubuntu jammy-security main restricted + - sourceline: deb http://security.ubuntu.com/ubuntu jammy-security universe + - sourceline: deb http://security.ubuntu.com/ubuntu jammy-security multiverse + - sourceline: + deb https://ppa.launchpadcontent.net/touchegg/stable/ubuntu/ jammy + main + - sourceline: + deb https://packagecloud.io/slacktechnologies/slack/debian/ jessie + main + - sourceline: + deb [arch=amd64 signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] + https://cli.github.com/packages stable main + - sourceline: + deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x + jammy main + - sourceline: + deb [arch=amd64,arm64,armhf] http://packages.microsoft.com/repos/code + stable main + - sourceline: + deb [arch=amd64] https://dl.google.com/linux/chrome/deb/ stable + main include: - - libc6:amd64 - - libnotify4:amd64 - - libkeybinder-3.0-0:amd64 - - libwayland-cursor0:amd64 - - libwayland-client0:amd64 - - libwayland-egl1:amd64 + - libc6:amd64 + - libnotify4:amd64 + - libkeybinder-3.0-0:amd64 + - libwayland-cursor0:amd64 + - libwayland-client0:amd64 + - libwayland-egl1:amd64 + - mpv:amd64 files: include: [] exclude: - - usr/share/man - - usr/share/doc/*/README.* - - usr/share/doc/*/changelog.* - - usr/share/doc/*/NEWS.* - - usr/share/doc/*/TODO.* + - usr/share/man + - usr/share/doc/*/README.* + - usr/share/doc/*/changelog.* + - usr/share/doc/*/NEWS.* + - usr/share/doc/*/TODO.* test: fedora-30: image: appimagecrafters/tests-env:fedora-30 @@ -78,4 +86,4 @@ AppDir: command: ./AppRun AppImage: arch: x86_64 - update-information: guess \ No newline at end of file + update-information: guess diff --git a/frontend/scripts/tool/update_collab_source.sh b/frontend/scripts/tool/update_collab_source.sh index 29892de1de9d2..094e5caf14abd 100755 --- a/frontend/scripts/tool/update_collab_source.sh +++ b/frontend/scripts/tool/update_collab_source.sh @@ -17,7 +17,7 @@ switch_deps() { # Switch to local paths for crate in collab collab-folder collab-document collab-database collab-plugins collab-user collab-entity collab-sync-protocol collab-persistence; do sed -i '' \ - -e "s#${crate} = { git = \"https://github.com/AppFlowy-IO/AppFlowy-Collab\", rev = \"[a-f0-9]*\" }#${crate} = { path = \"$repo_path/$crate\" }#g" \ + -e "s#${crate} = { .*git = \"https://github.com/AppFlowy-IO/AppFlowy-Collab\".* }#${crate} = { path = \"$repo_path/$crate\" }#g" \ "$cargo_toml" done echo "Switched to local paths in $cargo_toml." @@ -38,4 +38,4 @@ fi # Switch dependencies in both Cargo.toml files switch_deps "$CARGO_TOML_1" "$REPO_RELATIVE_PATH_1" -switch_deps "$CARGO_TOML_2" "$REPO_RELATIVE_PATH_2" +switch_deps "$CARGO_TOML_2" "$REPO_RELATIVE_PATH_2" \ No newline at end of file