Ver código fonte

feat:IInline math equation (#2949)

Lucas.Xu 1 ano atrás
pai
commit
ff9b3c56c5
43 arquivos alterados com 500 adições e 120 exclusões
  1. 1 1
      .github/workflows/flutter_ci.yaml
  2. 19 18
      .github/workflows/integration_test.yml
  3. 1 0
      frontend/appflowy_flutter/assets/images/editor/math.svg
  4. 1 0
      frontend/appflowy_flutter/assets/translations/en.json
  5. 6 6
      frontend/appflowy_flutter/integration_test/database_calendar_test.dart
  6. 2 1
      frontend/appflowy_flutter/integration_test/database_row_page_test.dart
  7. 2 0
      frontend/appflowy_flutter/integration_test/database_share_test.dart
  8. 1 1
      frontend/appflowy_flutter/integration_test/document/document_create_and_delete_test.dart
  9. 22 0
      frontend/appflowy_flutter/integration_test/document/document_test_runner.dart
  10. 0 0
      frontend/appflowy_flutter/integration_test/document/document_with_cover_image_test.dart
  11. 65 0
      frontend/appflowy_flutter/integration_test/document/document_with_inline_math_equation_test.dart
  12. BIN
      frontend/appflowy_flutter/integration_test/document/document_with_inline_math_equation_test_1.png
  13. BIN
      frontend/appflowy_flutter/integration_test/document/document_with_inline_math_equation_test_2.png
  14. 9 16
      frontend/appflowy_flutter/integration_test/runner.dart
  15. 25 7
      frontend/appflowy_flutter/integration_test/util/base.dart
  16. 11 4
      frontend/appflowy_flutter/integration_test/util/database_test_op.dart
  17. 10 0
      frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart
  18. 3 2
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart
  19. 1 1
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart
  20. 1 15
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart
  21. 1 1
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_popover.dart
  22. 1 1
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_menu.dart
  23. 173 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation.dart
  24. 52 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation_toolbar_item.dart
  25. 0 2
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_page/inline_page_reference.dart
  26. 13 11
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart
  27. 23 3
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart
  28. 3 2
      frontend/appflowy_flutter/lib/startup/startup.dart
  29. 2 5
      frontend/appflowy_flutter/lib/startup/tasks/windows.dart
  30. 1 1
      frontend/appflowy_flutter/lib/user/presentation/splash_screen.dart
  31. 1 0
      frontend/appflowy_flutter/lib/workspace/application/settings/application_data_storage.dart
  32. 9 2
      frontend/appflowy_flutter/packages/flowy_infra/lib/image.dart
  33. 3 3
      frontend/appflowy_flutter/pubspec.lock
  34. 1 1
      frontend/appflowy_flutter/pubspec.yaml
  35. 3 1
      frontend/appflowy_tauri/src-tauri/src/main.rs
  36. 10 0
      frontend/rust-lib/Cargo.lock
  37. 3 1
      frontend/rust-lib/dart-ffi/src/lib.rs
  38. 11 0
      frontend/rust-lib/flowy-notification/src/lib.rs
  39. 7 4
      frontend/scripts/code_generation/freezed/generate_freezed.sh
  40. 0 0
      frontend/scripts/code_generation/generate.sh
  41. 0 2
      frontend/scripts/code_generation/language_files/generate_language_files.cmd
  42. 0 2
      frontend/scripts/code_generation/language_files/generate_language_files.sh
  43. 3 6
      frontend/scripts/makefile/flutter.toml

+ 1 - 1
.github/workflows/flutter_ci.yaml

@@ -134,6 +134,6 @@ jobs:
             fail_ci_if_error: true
             verbose: true
             os: ${{ matrix.os }}
-          attempt_limit: 5
+          attempt_limit: 20
           attempt_delay: 10000
 

+ 19 - 18
.github/workflows/integration_test.yml

@@ -29,25 +29,31 @@ concurrency:
   cancel-in-progress: true
 
 jobs:
-  tests:
+  build:
     if: github.event.pull_request.draft != true
     strategy:
+      fail-fast: false
       matrix:
         os: [ubuntu-latest, windows-latest]
-
+        include:
+          - os: ubuntu-latest
+            flutter_profile: development-linux-x86_64
+            target: x86_64-unknown-linux-gnu
+          - os: windows-latest
+            flutter_profile: development-windows-x86
+            target: x86_64-pc-windows-msvc
     runs-on: ${{ matrix.os }}
 
     steps:
-      - uses: actions/checkout@v2
-      - uses: actions-rs/toolchain@v1
-        with:
-          toolchain: "stable-2022-04-07"
+      - name: Checkout source code
+        uses: actions/checkout@v2
 
       - name: Install Rust toolchain
         id: rust_toolchain
         uses: actions-rs/toolchain@v1
         with:
           toolchain: ${{ env.RUST_TOOLCHAIN }}
+          target: ${{ matrix.target }}
           override: true
           profile: minimal
 
@@ -64,6 +70,7 @@ jobs:
           prefix-key: ${{ matrix.os }}
           workspaces: |
             frontend/rust-lib
+          cache-all-crates: true
 
       - uses: davidB/rust-cargo-make@v1
         with:
@@ -87,30 +94,24 @@ jobs:
           cargo make appflowy-flutter-deps-tools
         shell: bash
 
-      - name: Config Flutter
+      - name: Enable Flutter Desktop
         run: |
           if [ "$RUNNER_OS" == "Linux" ]; then
             flutter config --enable-linux-desktop
           elif [ "$RUNNER_OS" == "macOS" ]; then
             flutter config --enable-macos-desktop
           elif [ "$RUNNER_OS" == "Windows" ]; then
+            git config --system core.longpaths true
             flutter config --enable-windows-desktop
           fi
         shell: bash
 
-      - name: Build Test lib
+      - name: Build AppFlowy
         working-directory: frontend
         run: |
-          if [ "$RUNNER_OS" == "Linux" ]; then
-            cargo make --profile development-linux-x86_64 appflowy-dev
-          elif [ "$RUNNER_OS" == "macOS" ]; then
-            cargo make --profile development-mac-x86_64 appflowy-dev
-          elif [ "$RUNNER_OS" == "Windows" ]; then
-            cargo make --profile development-windows-x86 appflowy-dev
-          fi
-        shell: bash
+          cargo make --profile ${{ matrix.flutter_profile }} appflowy-dev
 
-      - name: Run AppFlowy tests
+      - name: Run Flutter integration tests
         working-directory: frontend/appflowy_flutter
         run: |
           if [ "$RUNNER_OS" == "Linux" ]; then
@@ -135,5 +136,5 @@ jobs:
             fail_ci_if_error: true
             verbose: true
             os: ${{ matrix.os }}
-          attempt_limit: 5
+          attempt_limit: 20
           attempt_delay: 10000

+ 1 - 0
frontend/appflowy_flutter/assets/images/editor/math.svg

@@ -0,0 +1 @@
+<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30" style="width: 16px; height: 16px; display: block; fill: inherit; flex-shrink: 0; backface-visibility: hidden;" width="30"  height="30" ><path d="M6.04883 27.3232C7.24219 27.3232 7.86426 26.4854 8.20703 25.3936L13.9834 6.79492H27.5039C28.2275 6.79492 28.7227 6.35059 28.7227 5.65234C28.7227 4.97949 28.2275 4.53516 27.5039 4.53516H13.958C12.6631 4.53516 12.0791 5.01758 11.7363 6.13477L6.18848 24.3525H5.97266L3.58594 15.9355C3.38281 15.2373 3.00195 14.9072 2.40527 14.9072C1.73242 14.9072 1.2373 15.3896 1.2373 16.0117C1.2373 16.2656 1.30078 16.4941 1.35156 16.6846L4.04297 25.5332C4.36035 26.5615 4.93164 27.3232 6.04883 27.3232ZM16.3955 24.7334C16.8652 24.7334 17.1064 24.5684 17.4619 24.0732L20.4707 19.8203H20.5215L23.5049 24.0732C23.873 24.5684 24.1143 24.7334 24.5713 24.7334C25.2061 24.7334 25.6758 24.3018 25.6758 23.7051C25.6758 23.4258 25.6123 23.1973 25.4219 22.9561L21.9307 18.208L25.4473 13.4346C25.6377 13.168 25.7139 12.9395 25.7139 12.6855C25.7139 12.127 25.2568 11.6953 24.6475 11.6953C24.2031 11.6953 23.9365 11.8477 23.6064 12.3174L20.7246 16.5957H20.6611L17.6904 12.3047C17.373 11.8477 17.0938 11.6953 16.624 11.6953C16.0146 11.6953 15.5068 12.165 15.5068 12.7363C15.5068 13.0537 15.583 13.2568 15.8115 13.5488L19.1504 18.1445L15.6084 23.0322C15.418 23.2861 15.3672 23.4639 15.3672 23.7559C15.3672 24.3018 15.8115 24.7334 16.3955 24.7334Z"  fill="#2F3030"></path></svg>

+ 1 - 0
frontend/appflowy_flutter/assets/translations/en.json

@@ -426,6 +426,7 @@
       "smartEditCouldNotFetchKey": "Could not fetch OpenAI key",
       "smartEditDisabled": "Connect OpenAI in Settings",
       "discardResponse": "Do you want to discard the AI responses?",
+      "createInlineMathEquation": "Create equation",
       "cover": {
         "changeCover": "Change Cover",
         "colors": "Colors",

+ 6 - 6
frontend/appflowy_flutter/integration_test/database_calendar_test.dart

@@ -107,7 +107,7 @@ void main() {
 
       // Make sure that the event is edited
       tester.assertNumberOfEventsInCalendar(1, title: 'hello world');
-      tester.assertNumberofEventsOnSpecificDay(2, DateTime.now());
+      tester.assertNumberOfEventsOnSpecificDay(2, DateTime.now());
 
       // Click on the event
       await tester.openCalendarEvent(index: 1);
@@ -119,7 +119,7 @@ void main() {
 
       // Check that there are 2 events
       tester.assertNumberOfEventsInCalendar(2, title: 'hello world');
-      tester.assertNumberofEventsOnSpecificDay(3, DateTime.now());
+      tester.assertNumberOfEventsOnSpecificDay(3, DateTime.now());
 
       // Delete an event
       await tester.openCalendarEvent(index: 1);
@@ -127,7 +127,7 @@ void main() {
 
       // Check that there is 1 event
       tester.assertNumberOfEventsInCalendar(1, title: 'hello world');
-      tester.assertNumberofEventsOnSpecificDay(2, DateTime.now());
+      tester.assertNumberOfEventsOnSpecificDay(2, DateTime.now());
     });
 
     testWidgets('rescheduling events', (tester) async {
@@ -150,7 +150,7 @@ void main() {
       // Make sure that the event has been rescheduled to the new date
       final sameDayNextWeek = firstOfThisMonth.add(const Duration(days: 7));
       tester.assertNumberOfEventsInCalendar(1);
-      tester.assertNumberofEventsOnSpecificDay(1, sameDayNextWeek);
+      tester.assertNumberOfEventsOnSpecificDay(1, sameDayNextWeek);
 
       // Delete the event
       await tester.openCalendarEvent(index: 0, date: sameDayNextWeek);
@@ -162,7 +162,7 @@ void main() {
       await tester.dismissRowDetailPage();
 
       // Make sure that the event is today
-      tester.assertNumberofEventsOnSpecificDay(1, today);
+      tester.assertNumberOfEventsOnSpecificDay(1, today);
 
       // Click on the event
       await tester.openCalendarEvent(index: 0);
@@ -183,7 +183,7 @@ void main() {
 
       // Make sure that the event is edited
       tester.assertNumberOfEventsInCalendar(1);
-      tester.assertNumberofEventsOnSpecificDay(1, newDate);
+      tester.assertNumberOfEventsOnSpecificDay(1, newDate);
     });
   });
 }

+ 2 - 1
frontend/appflowy_flutter/integration_test/database_row_page_test.dart

@@ -172,9 +172,10 @@ void main() {
       // Focus on the editor
       final textBlock = find.byType(TextBlockComponentWidget);
       await tester.tapAt(tester.getCenter(textBlock));
+      await tester.pumpAndSettle();
 
       // Input some text
-      const inputText = 'Hello world';
+      const inputText = 'Hello World';
       await tester.ime.insertText(inputText);
       expect(
         find.textContaining(inputText, findRichText: true),

+ 2 - 0
frontend/appflowy_flutter/integration_test/database_share_test.dart

@@ -10,6 +10,8 @@ void main() {
   group('database', () {
     testWidgets('import v0.2.0 database data', (tester) async {
       await tester.openV020database();
+      // wait the database data is loaded
+      await tester.pumpAndSettle(const Duration(microseconds: 500));
 
       // check the text cell
       final textCells = <String>['A', 'B', 'C', 'D', 'E', '', '', '', '', ''];

+ 1 - 1
frontend/appflowy_flutter/integration_test/document/document_test.dart → frontend/appflowy_flutter/integration_test/document/document_create_and_delete_test.dart

@@ -9,7 +9,7 @@ import '../util/util.dart';
 void main() {
   IntegrationTestWidgetsFlutterBinding.ensureInitialized();
 
-  group('document', () {
+  group('create and delete the document', () {
     testWidgets('create a new document when launching app in first time',
         (tester) async {
       await tester.initializeAppFlowy();

+ 22 - 0
frontend/appflowy_flutter/integration_test/document/document_test_runner.dart

@@ -0,0 +1,22 @@
+import 'package:integration_test/integration_test.dart';
+
+import 'document_create_and_delete_test.dart'
+    as document_create_and_delete_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;
+import 'document_with_inline_math_equation_test.dart'
+    as document_with_inline_math_equation_test;
+import 'document_with_inline_page_test.dart' as document_with_inline_page_test;
+import 'edit_document_test.dart' as document_edit_test;
+
+void startTesting() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+  // Document integration tests
+  document_create_and_delete_test.main();
+  document_edit_test.main();
+  document_with_database_test.main();
+  document_with_inline_page_test.main();
+  document_with_inline_math_equation_test.main();
+  document_with_cover_image_test.main();
+}

+ 0 - 0
frontend/appflowy_flutter/integration_test/document/cover_image_test.dart → frontend/appflowy_flutter/integration_test/document/document_with_cover_image_test.dart


+ 65 - 0
frontend/appflowy_flutter/integration_test/document/document_with_inline_math_equation_test.dart

@@ -0,0 +1,65 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra_ui/style_widget/button.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+
+import '../util/ime.dart';
+import '../util/util.dart';
+
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+  TestWidgetsFlutterBinding.ensureInitialized();
+
+  group('inline math equation in document', () {
+    testWidgets('insert an inline math equation', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      // create a new document
+      await tester.createNewPageWithName(
+        ViewLayoutPB.Document,
+        LocaleKeys.document_plugins_createInlineMathEquation.tr(),
+      );
+
+      // tap the first line of the document
+      await tester.editor.tapLineOfEditorAt(0);
+      // insert a inline page
+      const formula = 'E = MC ^ 2';
+      await tester.ime.insertText(formula);
+      await tester.editor.updateSelection(
+        Selection.single(path: [0], startOffset: 0, endOffset: formula.length),
+      );
+
+      // tap the inline math equation button
+      final inlineMathEquationButton = find.byTooltip(
+        LocaleKeys.document_plugins_createInlineMathEquation.tr(),
+      );
+      await tester.tapButton(inlineMathEquationButton);
+
+      // expect to see the math equation block
+      final inlineMathEquation = find.byType(InlineMathEquation);
+      expect(inlineMathEquation, findsOneWidget);
+
+      // tap it and update the content
+      await tester.tapButton(inlineMathEquation);
+      final textFormField = find.descendant(
+        of: find.byType(MathInputTextField),
+        matching: find.byType(TextFormField),
+      );
+      const newFormula = 'E = MC ^ 3';
+      await tester.enterText(textFormField, newFormula);
+      await tester.tapButton(
+        find.descendant(
+          of: find.byType(MathInputTextField),
+          matching: find.byType(FlowyButton),
+        ),
+      );
+      await tester.pumpAndSettle();
+    });
+  });
+}

BIN
frontend/appflowy_flutter/integration_test/document/document_with_inline_math_equation_test_1.png


BIN
frontend/appflowy_flutter/integration_test/document/document_with_inline_math_equation_test_2.png


+ 9 - 16
frontend/appflowy_flutter/integration_test/runner.dart

@@ -1,23 +1,19 @@
 import 'package:integration_test/integration_test.dart';
 
-import 'switch_folder_test.dart' as switch_folder_test;
-import 'document/document_test.dart' as document_test;
-import 'document/cover_image_test.dart' as cover_image_test;
-import 'share_markdown_test.dart' as share_markdown_test;
-import 'import_files_test.dart' as import_files_test;
-import 'document/document_with_database_test.dart'
-    as document_with_database_test;
-import 'document/edit_document_test.dart' as edit_document_test;
+import 'database_calendar_test.dart' as database_calendar_test;
 import 'database_cell_test.dart' as database_cell_test;
 import 'database_field_test.dart' as database_field_test;
-import 'database_share_test.dart' as database_share_test;
+import 'database_filter_test.dart' as database_filter_test;
 import 'database_row_page_test.dart' as database_row_page_test;
 import 'database_row_test.dart' as database_row_test;
 import 'database_setting_test.dart' as database_setting_test;
-import 'database_filter_test.dart' as database_filter_test;
-import 'database_view_test.dart' as database_view_test;
-import 'database_calendar_test.dart' as database_calendar_test;
+import 'database_share_test.dart' as database_share_test;
 import 'database_sort_test.dart' as database_sort_test;
+import 'database_view_test.dart' as database_view_test;
+import 'document/document_test_runner.dart' as document_test_runner;
+import 'import_files_test.dart' as import_files_test;
+import 'share_markdown_test.dart' as share_markdown_test;
+import 'switch_folder_test.dart' as switch_folder_test;
 
 /// The main task runner for all integration tests in AppFlowy.
 ///
@@ -33,10 +29,7 @@ void main() {
   import_files_test.main();
 
   // Document integration tests
-  cover_image_test.main();
-  document_test.main();
-  document_with_database_test.main();
-  edit_document_test.main();
+  document_test_runner.startTesting();
 
   // Database integration tests
   database_cell_test.main();

+ 25 - 7
frontend/appflowy_flutter/integration_test/util/base.dart

@@ -10,6 +10,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:path_provider/path_provider.dart';
+import 'package:path/path.dart' as p;
 
 class FlowyTestContext {
   FlowyTestContext({
@@ -23,7 +24,10 @@ extension AppFlowyTestBase on WidgetTester {
   Future<FlowyTestContext> initializeAppFlowy({
     // use to append after the application data directory
     String? pathExtension,
+    Size windowsSize = const Size(1600, 1200),
   }) async {
+    binding.setSurfaceSize(windowsSize);
+
     mockHotKeyManagerHandlers();
     final directory = await mockApplicationDataStorage(
       pathExtension: pathExtension,
@@ -60,7 +64,7 @@ extension AppFlowyTestBase on WidgetTester {
     final dir = await getTemporaryDirectory();
 
     // Use a random uuid to avoid conflict.
-    String path = '${dir.path}/appflowy_integration_test/${uuid()}';
+    String path = p.join(dir.path, 'appflowy_integration_test', uuid());
     if (pathExtension != null && pathExtension.isNotEmpty) {
       path = '$path/$pathExtension';
     }
@@ -78,7 +82,7 @@ extension AppFlowyTestBase on WidgetTester {
     Finder finder, {
     int? pointer,
     int buttons = kPrimaryButton,
-    bool warnIfMissed = true,
+    bool warnIfMissed = false,
     int milliseconds = 500,
   }) async {
     await tap(
@@ -123,6 +127,18 @@ extension AppFlowyTestBase on WidgetTester {
     return;
   }
 
+  Future<void> doubleTapAt(
+    Offset location, {
+    int? pointer,
+    int buttons = kPrimaryButton,
+    int milliseconds = 500,
+  }) async {
+    await tapAt(location, pointer: pointer, buttons: buttons);
+    await pump(kDoubleTapMinTime);
+    await tapAt(location, pointer: pointer, buttons: buttons);
+    await pumpAndSettle(Duration(milliseconds: milliseconds));
+  }
+
   Future<void> doubleTapButton(
     Finder finder, {
     int? pointer,
@@ -130,20 +146,22 @@ extension AppFlowyTestBase on WidgetTester {
     bool warnIfMissed = true,
     int milliseconds = 500,
   }) async {
-    await tapButton(
+    await tap(
       finder,
       pointer: pointer,
       buttons: buttons,
       warnIfMissed: warnIfMissed,
-      milliseconds: kDoubleTapMinTime.inMilliseconds,
     );
-    await tapButton(
+
+    await pump(kDoubleTapMinTime);
+
+    await tap(
       finder,
-      pointer: pointer,
       buttons: buttons,
+      pointer: pointer,
       warnIfMissed: warnIfMissed,
-      milliseconds: milliseconds,
     );
+    await pumpAndSettle(Duration(milliseconds: milliseconds));
   }
 
   Future<void> wait(int milliseconds) async {

+ 11 - 4
frontend/appflowy_flutter/integration_test/util/database_test_op.dart

@@ -453,8 +453,15 @@ extension AppFlowyDatabaseTest on WidgetTester {
   }
 
   Future<void> dismissRowDetailPage() async {
-    await sendKeyEvent(LogicalKeyboardKey.escape);
+    // use tap empty area instead of clicking ESC to dismiss the row detail page
+    // sometimes, the ESC key is not working.
+    await simulateKeyEvent(LogicalKeyboardKey.escape);
     await pumpAndSettle();
+    final findRowDetailPage = find.byType(RowDetailPage);
+    if (findRowDetailPage.evaluate().isNotEmpty) {
+      await tapAt(const Offset(0, 0));
+      await pumpAndSettle();
+    }
   }
 
   Future<void> editTitleInRowDetailPage(String title) async {
@@ -1031,7 +1038,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
     expect(findEvents, findsNWidgets(number));
   }
 
-  void assertNumberofEventsOnSpecificDay(
+  void assertNumberOfEventsOnSpecificDay(
     int number,
     DateTime date, {
     String? title,
@@ -1058,8 +1065,8 @@ extension AppFlowyDatabaseTest on WidgetTester {
     final todayCell = find.byWidgetPredicate(
       (widget) => widget is CalendarDayCard && isSameDay(date, widget.date),
     );
-
-    await doubleTapButton(todayCell);
+    final location = getTopLeft(todayCell).translate(10, 10);
+    await doubleTapAt(location);
   }
 
   Future<void> openCalendarEvent({required index, DateTime? date}) async {

+ 10 - 0
frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart

@@ -165,4 +165,14 @@ class EditorOperations {
     );
     await tester.tapButton(atMenuItem);
   }
+
+  /// Update the editor's selection
+  Future<void> updateSelection(Selection selection) async {
+    final editorState = getCurrentEditorState();
+    editorState.updateSelectionWithReason(
+      selection,
+      reason: SelectionUpdateReason.uiEvent,
+    );
+    await tester.pumpAndSettle(const Duration(milliseconds: 200));
+  }
 }

+ 3 - 2
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart

@@ -49,9 +49,10 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
     quoteItem,
     bulletedListItem,
     numberedListItem,
+    inlineMathEquationItem,
     linkItem,
-    textColorItem,
-    highlightColorItem,
+    buildTextColorItem(),
+    buildHighlightColorItem(),
   ];
 
   late final List<SelectionMenuItem> slashMenuItems;

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart

@@ -1,7 +1,7 @@
 import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
 import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
-import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg;
+import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/image.dart';

+ 1 - 15
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart

@@ -2,7 +2,7 @@ import 'dart:io';
 
 import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
-import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg;
+import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/image.dart';
@@ -43,20 +43,6 @@ enum CoverType {
   }
 }
 
-class DocumentHeaderNodeWidgetBuilder implements NodeWidgetBuilder {
-  @override
-  Widget build(NodeWidgetContext<Node> context) {
-    return DocumentHeaderNodeWidget(
-      key: context.node.key,
-      node: context.node,
-      editorState: context.editorState,
-    );
-  }
-
-  @override
-  NodeValidator<Node> get nodeValidator => (_) => true;
-}
-
 class DocumentHeaderNodeWidget extends StatefulWidget {
   const DocumentHeaderNodeWidget({
     required this.node,

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_popover.dart

@@ -1,6 +1,6 @@
 import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
-import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg;
+import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra_ui/style_widget/button.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_menu.dart

@@ -1,5 +1,5 @@
 import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg;
+import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/image.dart';

+ 173 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation.dart

@@ -0,0 +1,173 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flowy_infra_ui/style_widget/text_input.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_math_fork/flutter_math.dart';
+import 'package:provider/provider.dart';
+
+class InlineMathEquationKeys {
+  const InlineMathEquationKeys._();
+
+  static const formula = 'formula';
+}
+
+class InlineMathEquation extends StatefulWidget {
+  const InlineMathEquation({
+    super.key,
+    required this.formula,
+    required this.node,
+    required this.index,
+    this.textStyle,
+  });
+
+  final Node node;
+  final int index;
+  final String formula;
+  final TextStyle? textStyle;
+
+  @override
+  State<InlineMathEquation> createState() => _InlineMathEquationState();
+}
+
+class _InlineMathEquationState extends State<InlineMathEquation> {
+  final popoverController = PopoverController();
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = Theme.of(context);
+    return _IgnoreParentPointer(
+      child: AppFlowyPopover(
+        controller: popoverController,
+        direction: PopoverDirection.bottomWithLeftAligned,
+        popupBuilder: (_) {
+          return MathInputTextField(
+            initialText: widget.formula,
+            onSubmit: (value) async {
+              popoverController.close();
+              if (value == widget.formula) {
+                return;
+              }
+              final editorState = context.read<EditorState>();
+              final transaction = editorState.transaction
+                ..formatText(widget.node, widget.index, 1, {
+                  InlineMathEquationKeys.formula: value,
+                });
+              await editorState.apply(transaction);
+            },
+          );
+        },
+        offset: const Offset(0, 10),
+        child: Padding(
+          padding: const EdgeInsets.symmetric(vertical: 8.0),
+          child: MouseRegion(
+            cursor: SystemMouseCursors.click,
+            child: Row(
+              mainAxisSize: MainAxisSize.min,
+              children: [
+                const HSpace(2),
+                Math.tex(
+                  widget.formula,
+                  options: MathOptions(
+                    style: MathStyle.text,
+                    mathFontOptions: const FontOptions(
+                      fontShape: FontStyle.italic,
+                    ),
+                    fontSize: 14.0,
+                    color: widget.textStyle?.color ??
+                        theme.colorScheme.onBackground,
+                  ),
+                ),
+                const HSpace(2),
+              ],
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+}
+
+class MathInputTextField extends StatefulWidget {
+  const MathInputTextField({
+    super.key,
+    required this.initialText,
+    required this.onSubmit,
+  });
+
+  final String initialText;
+  final void Function(String value) onSubmit;
+
+  @override
+  State<MathInputTextField> createState() => _MathInputTextFieldState();
+}
+
+class _MathInputTextFieldState extends State<MathInputTextField> {
+  late final TextEditingController textEditingController;
+
+  @override
+  void initState() {
+    super.initState();
+
+    textEditingController = TextEditingController(
+      text: widget.initialText,
+    );
+    textEditingController.selection = TextSelection(
+      baseOffset: 0,
+      extentOffset: widget.initialText.length,
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      width: 240,
+      child: Row(
+        mainAxisSize: MainAxisSize.min,
+        children: [
+          Expanded(
+            child: FlowyFormTextInput(
+              autoFocus: true,
+              textAlign: TextAlign.left,
+              controller: textEditingController,
+              contentPadding: const EdgeInsets.symmetric(
+                vertical: 8.0,
+                horizontal: 4.0,
+              ),
+              onEditingComplete: () =>
+                  widget.onSubmit(textEditingController.text),
+            ),
+          ),
+          const HSpace(4.0),
+          FlowyButton(
+            text: FlowyText(LocaleKeys.button_Done.tr()),
+            useIntrinsicWidth: true,
+            onTap: () => widget.onSubmit(textEditingController.text),
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class _IgnoreParentPointer extends StatelessWidget {
+  const _IgnoreParentPointer({
+    required this.child,
+  });
+
+  final Widget child;
+
+  @override
+  Widget build(BuildContext context) {
+    return GestureDetector(
+      behavior: HitTestBehavior.opaque,
+      onTap: () {},
+      onTapDown: (_) {},
+      onDoubleTap: () {},
+      onLongPress: () {},
+      child: child,
+    );
+  }
+}

+ 52 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation_toolbar_item.dart

@@ -0,0 +1,52 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/image.dart';
+import 'package:flutter/material.dart';
+
+final ToolbarItem inlineMathEquationItem = ToolbarItem(
+  id: 'editor.inline_math_equation',
+  group: 2,
+  isActive: onlyShowInSingleSelectionAndTextType,
+  builder: (context, editorState) {
+    final selection = editorState.selection!;
+    final nodes = editorState.getNodesInSelection(selection);
+    final isHighlight = nodes.allSatisfyInSelection(selection, (delta) {
+      return delta.everyAttributes(
+        (attributes) => attributes[InlineMathEquationKeys.formula] != null,
+      );
+    });
+    return IconItemWidget(
+      iconBuilder: (_) => svgWidget(
+        'editor/math',
+        size: const Size.square(16),
+        color: Colors.white,
+      ),
+      isHighlight: isHighlight,
+      tooltip: LocaleKeys.document_plugins_createInlineMathEquation.tr(),
+      onPressed: () async {
+        final selection = editorState.selection;
+        if (selection == null || selection.isCollapsed) {
+          return;
+        }
+        final node = editorState.getNodeAtPath(selection.start.path);
+        if (node == null) {
+          return;
+        }
+        final text = editorState.getTextInSelection(selection).join();
+        final transaction = editorState.transaction
+          ..replaceText(
+            node,
+            selection.startIndex,
+            selection.length,
+            '\$',
+            attributes: {
+              InlineMathEquationKeys.formula: text,
+            },
+          );
+        await editorState.apply(transaction);
+      },
+    );
+  },
+);

+ 0 - 2
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_page/inline_page_reference.dart

@@ -24,8 +24,6 @@ class MentionBlockKeys {
   static const mention = 'mention';
   static const type = 'type'; // MentionType, String
   static const pageId = 'page_id';
-  static const pageType = 'page_type';
-  static const pageName = 'page_name';
 }
 
 class InlinePageReferenceService {

+ 13 - 11
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart

@@ -1,22 +1,24 @@
+export 'actions/block_action_list.dart';
+export 'actions/option_action.dart';
 export 'callout/callout_block_component.dart';
 export 'code_block/code_block_component.dart';
 export 'code_block/code_block_shortcut_event.dart';
-export 'header/cover_editor_bloc.dart';
-export 'header/document_header_node_widget.dart';
-export 'header/custom_cover_picker.dart';
-export 'emoji_picker/emoji_menu_item.dart';
-export 'extensions/flowy_tint_extension.dart';
+export 'database/database_view_block_component.dart';
 export 'database/inline_database_menu_item.dart';
 export 'database/referenced_database_menu_item.dart';
-export 'database/database_view_block_component.dart';
+export 'emoji_picker/emoji_menu_item.dart';
+export 'extensions/flowy_tint_extension.dart';
+export 'header/cover_editor_bloc.dart';
+export 'header/custom_cover_picker.dart';
+export 'header/document_header_node_widget.dart';
+export 'image/image_menu.dart';
+export 'image/image_selection_menu.dart';
+export 'inline_math_equation/inline_math_equation.dart';
+export 'inline_math_equation/inline_math_equation_toolbar_item.dart';
 export 'math_equation/math_equation_block_component.dart';
 export 'openai/widgets/auto_completion_node_widget.dart';
 export 'openai/widgets/smart_edit_node_widget.dart';
 export 'openai/widgets/smart_edit_toolbar_item.dart';
+export 'outline/outline_block_component.dart';
 export 'toggle/toggle_block_component.dart';
 export 'toggle/toggle_block_shortcut_event.dart';
-export 'outline/outline_block_component.dart';
-export 'image/image_menu.dart';
-export 'image/image_selection_menu.dart';
-export 'actions/option_action.dart';
-export 'actions/block_action_list.dart';

+ 23 - 3
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart

@@ -1,7 +1,8 @@
+import 'package:appflowy/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation.dart';
 import 'package:appflowy/plugins/document/presentation/editor_plugins/inline_page/inline_page_reference.dart';
 import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
 import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
-import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg, Log;
+import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
 import 'package:collection/collection.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
@@ -173,13 +174,17 @@ class EditorStyleCustomizer {
   }
 
   InlineSpan customizeAttributeDecorator(
-    TextInsert textInsert,
+    Node node,
+    int index,
+    TextInsert text,
     TextSpan textSpan,
   ) {
-    final attributes = textInsert.attributes;
+    final attributes = text.attributes;
     if (attributes == null) {
       return textSpan;
     }
+
+    // customize the inline mention block, like inline page
     final mention = attributes[MentionBlockKeys.mention] as Map?;
     if (mention != null) {
       final type = mention[MentionBlockKeys.type];
@@ -193,6 +198,21 @@ class EditorStyleCustomizer {
         );
       }
     }
+
+    // customize the inline math equation block
+    final formula = attributes[InlineMathEquationKeys.formula] as String?;
+    if (formula != null) {
+      return WidgetSpan(
+        alignment: PlaceholderAlignment.middle,
+        child: InlineMathEquation(
+          node: node,
+          index: index,
+          formula: formula,
+          textStyle: style().textStyleConfiguration.text,
+        ),
+      );
+    }
+
     return textSpan;
   }
 }

+ 3 - 2
frontend/appflowy_flutter/lib/startup/startup.dart

@@ -46,8 +46,9 @@ class FlowyRunner {
     final launcher = getIt<AppLauncher>();
     launcher.addTasks(
       [
-        // handle platform errors.
-        const PlatformErrorCatcherTask(),
+        // this task should be first task, for handling platform errors.
+        // don't catch errors in test mode
+        if (!mode.isUnitTest) const PlatformErrorCatcherTask(),
         // localization
         const InitLocalizationTask(),
         // init the app window

+ 2 - 5
frontend/appflowy_flutter/lib/startup/tasks/windows.dart

@@ -19,17 +19,14 @@ class InitAppWindowTask extends LaunchTask with WindowListener {
   @override
   Future<void> initialize(LaunchContext context) async {
     // Don't initialize on mobile or web.
-    if (!defaultTargetPlatform.isDesktop) {
+    if (!defaultTargetPlatform.isDesktop || context.env.isIntegrationTest) {
       return;
     }
 
     await windowManager.ensureInitialized();
     windowManager.addListener(this);
 
-    Size windowSize = await WindowSizeManager().getSize();
-    if (context.env.isIntegrationTest) {
-      windowSize = const Size(1600, 1200);
-    }
+    final windowSize = await WindowSizeManager().getSize();
 
     final windowOptions = WindowOptions(
       size: windowSize,

+ 1 - 1
frontend/appflowy_flutter/lib/user/presentation/splash_screen.dart

@@ -96,7 +96,7 @@ class SplashScreen extends StatelessWidget {
   }
 
   Future<void> _registerIfNeeded() async {
-    final result = await UserEventCheckUser().send();
+    final result = await UserEventGetUserProfile().send();
     if (!result.isLeft()) {
       await getIt<AuthService>().signUpAsGuest();
     }

+ 1 - 0
frontend/appflowy_flutter/lib/workspace/application/settings/application_data_storage.dart

@@ -98,6 +98,7 @@ class MockApplicationDataStorage extends ApplicationDataStorage {
     final path = initialPath;
     if (path != null) {
       initialPath = null;
+      await super.setPath(path);
       return Future.value(path);
     }
     return super.getPath();

+ 9 - 2
frontend/appflowy_flutter/packages/flowy_infra/lib/image.dart

@@ -5,16 +5,23 @@ import 'package:flutter_svg/flutter_svg.dart';
 ///
 /// Get the hover color from ThemeData
 class FlowySvg extends StatelessWidget {
-  const FlowySvg({super.key, this.size, required this.name});
+  const FlowySvg({
+    super.key,
+    required this.name,
+    this.size,
+    this.color,
+  });
+
   final String name;
   final Size? size;
+  final Color? color;
 
   @override
   Widget build(BuildContext context) {
     return svgWidget(
       name,
       size: size,
-      color: Theme.of(context).iconTheme.color,
+      color: color ?? Theme.of(context).iconTheme.color,
     );
   }
 }

+ 3 - 3
frontend/appflowy_flutter/pubspec.lock

@@ -53,11 +53,11 @@ packages:
     dependency: "direct main"
     description:
       path: "."
-      ref: "572a174"
-      resolved-ref: "572a174892267e2f78f9c3d7f1fe4ca71c9be0db"
+      ref: c5b5e64
+      resolved-ref: c5b5e641fe11ae634f02db112e71f40a119e9c44
       url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
     source: git
-    version: "1.0.4"
+    version: "1.1.0"
   appflowy_popover:
     dependency: "direct main"
     description:

+ 1 - 1
frontend/appflowy_flutter/pubspec.yaml

@@ -46,7 +46,7 @@ dependencies:
   appflowy_editor:
     git:
       url: https://github.com/AppFlowy-IO/appflowy-editor.git
-      ref: 572a174
+      ref: c5b5e64
   appflowy_popover:
     path: packages/appflowy_popover
 

+ 3 - 1
frontend/appflowy_tauri/src-tauri/src/main.rs

@@ -7,7 +7,7 @@ mod init;
 mod notification;
 mod request;
 
-use flowy_notification::register_notification_sender;
+use flowy_notification::{register_notification_sender, unregister_all_notification_sender};
 use init::*;
 use notification::*;
 use request::*;
@@ -22,6 +22,8 @@ fn main() {
     .on_menu_event(|_menu| {})
     .on_page_load(|window, _payload| {
       let app_handler = window.app_handle();
+      // Make sure hot reload won't register the notification sender twice
+      unregister_all_notification_sender();
       register_notification_sender(TSNotificationSender::new(app_handler.clone()));
       // tauri::async_runtime::spawn(async move {});
       window.listen_global(AF_EVENT, move |event| {

+ 10 - 0
frontend/rust-lib/Cargo.lock

@@ -85,6 +85,7 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
 [[package]]
 name = "appflowy-integrate"
 version = "0.1.0"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2134c0#2134c0f27b8a9f3077e25ae928f2420c926506cc"
 dependencies = [
  "anyhow",
  "collab",
@@ -896,6 +897,7 @@ dependencies = [
 [[package]]
 name = "collab"
 version = "0.1.0"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2134c0#2134c0f27b8a9f3077e25ae928f2420c926506cc"
 dependencies = [
  "anyhow",
  "bytes",
@@ -913,6 +915,7 @@ dependencies = [
 [[package]]
 name = "collab-client-ws"
 version = "0.1.0"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2134c0#2134c0f27b8a9f3077e25ae928f2420c926506cc"
 dependencies = [
  "bytes",
  "collab-sync",
@@ -930,6 +933,7 @@ dependencies = [
 [[package]]
 name = "collab-database"
 version = "0.1.0"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2134c0#2134c0f27b8a9f3077e25ae928f2420c926506cc"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -956,6 +960,7 @@ dependencies = [
 [[package]]
 name = "collab-derive"
 version = "0.1.0"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2134c0#2134c0f27b8a9f3077e25ae928f2420c926506cc"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -967,6 +972,7 @@ dependencies = [
 [[package]]
 name = "collab-document"
 version = "0.1.0"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2134c0#2134c0f27b8a9f3077e25ae928f2420c926506cc"
 dependencies = [
  "anyhow",
  "collab",
@@ -985,6 +991,7 @@ dependencies = [
 [[package]]
 name = "collab-folder"
 version = "0.1.0"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2134c0#2134c0f27b8a9f3077e25ae928f2420c926506cc"
 dependencies = [
  "anyhow",
  "chrono",
@@ -1004,6 +1011,7 @@ dependencies = [
 [[package]]
 name = "collab-persistence"
 version = "0.1.0"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2134c0#2134c0f27b8a9f3077e25ae928f2420c926506cc"
 dependencies = [
  "bincode",
  "chrono",
@@ -1023,6 +1031,7 @@ dependencies = [
 [[package]]
 name = "collab-plugins"
 version = "0.1.0"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2134c0#2134c0f27b8a9f3077e25ae928f2420c926506cc"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -1056,6 +1065,7 @@ dependencies = [
 [[package]]
 name = "collab-sync"
 version = "0.1.0"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2134c0#2134c0f27b8a9f3077e25ae928f2420c926506cc"
 dependencies = [
  "bytes",
  "collab",

+ 3 - 1
frontend/rust-lib/dart-ffi/src/lib.rs

@@ -6,7 +6,7 @@ use lazy_static::lazy_static;
 use parking_lot::RwLock;
 
 use flowy_core::*;
-use flowy_notification::register_notification_sender;
+use flowy_notification::{register_notification_sender, unregister_all_notification_sender};
 use lib_dispatch::prelude::ToBytes;
 use lib_dispatch::prelude::*;
 
@@ -90,6 +90,8 @@ pub extern "C" fn sync_event(input: *const u8, len: usize) -> *const u8 {
 
 #[no_mangle]
 pub extern "C" fn set_stream_port(port: i64) -> i32 {
+  // Make sure hot reload won't register the notification sender twice
+  unregister_all_notification_sender();
   register_notification_sender(DartNotificationSender::new(port));
   0
 }

+ 11 - 0
frontend/rust-lib/flowy-notification/src/lib.rs

@@ -14,6 +14,10 @@ lazy_static! {
   static ref NOTIFICATION_SENDER: RwLock<Vec<Box<dyn NotificationSender>>> = RwLock::new(vec![]);
 }
 
+/// Register a notification sender. The sender will be alive until the process exits.
+/// Flutter integration test or Tauri hot reload might cause register multiple times.
+/// So before register a new sender, you might need to unregister the old one. Currently,
+/// Just remove all senders by calling `unregister_all_notification_sender`.
 pub fn register_notification_sender<T: NotificationSender>(sender: T) {
   let box_sender = Box::new(sender);
   match NOTIFICATION_SENDER.write() {
@@ -22,6 +26,13 @@ pub fn register_notification_sender<T: NotificationSender>(sender: T) {
   }
 }
 
+pub fn unregister_all_notification_sender() {
+  match NOTIFICATION_SENDER.write() {
+    Ok(mut write_guard) => write_guard.clear(),
+    Err(err) => tracing::error!("Failed to remove all notification senders: {:?}", err),
+  }
+}
+
 pub trait NotificationSender: Send + Sync + 'static {
   fn send_subject(&self, subject: SubscribeObject) -> Result<(), String>;
 }

+ 7 - 4
frontend/scripts/code_generation/freezed/generate_freezed.sh

@@ -10,12 +10,14 @@ cd ../../../appflowy_flutter
 
 # Navigate to the appflowy_flutter directory and generate files
 echo "Generating files for appflowy_flutter"
-flutter clean >/dev/null 2>&1 && flutter packages pub get >/dev/null 2>&1 && dart run build_runner clean && dart run build_runner build -d
+# flutter clean >/dev/null 2>&1 && flutter packages pub get >/dev/null 2>&1 && dart run build_runner clean &&
+flutter packages pub get >/dev/null 2>&1
+dart run build_runner build -d
 echo "Done generating files for appflowy_flutter"
 
 echo "Generating files for packages"
 cd packages
-for d in */ ; do
+for d in */; do
   # Navigate into the subdirectory
   cd "$d"
 
@@ -23,7 +25,8 @@ for d in */ ; do
   if [ -f "pubspec.yaml" ]; then
     echo "Generating freezed files in $d..."
     echo "Please wait while we clean the project and fetch the dependencies."
-    flutter clean >/dev/null 2>&1 && flutter packages pub get >/dev/null 2>&1 && dart run build_runner clean && dart run build_runner build -d
+    flutter packages pub get >/dev/null 2>&1
+    dart run build_runner build -d
     echo "Done running build command in $d"
   else
     echo "No pubspec.yaml found in $d, it can\'t be a Dart project. Skipping."
@@ -34,4 +37,4 @@ for d in */ ; do
 done
 
 # Return to the original directory
-cd "$original_dir"
+cd "$original_dir"

+ 0 - 0
frontend/scripts/code_generation/generate.sh


+ 0 - 2
frontend/scripts/code_generation/language_files/generate_language_files.cmd

@@ -10,8 +10,6 @@ cd /d "%~dp0"
 
 cd ..\..\..\appflowy_flutter
 
-call flutter clean
-
 call flutter packages pub get
 
 echo Specifying source directory for AppFlowy Localizations.

+ 0 - 2
frontend/scripts/code_generation/language_files/generate_language_files.sh

@@ -10,8 +10,6 @@ cd "$(dirname "$0")"
 # Navigate to the project root
 cd ../../../appflowy_flutter
 
-flutter clean
-
 flutter packages pub get
 
 echo "Specifying source directory for AppFlowy Localizations."

+ 3 - 6
frontend/scripts/makefile/flutter.toml

@@ -167,7 +167,6 @@ script = [
 [tasks.flutter-build]
 script = ["""
   cd appflowy_flutter/
-  flutter clean
   flutter pub get
   flutter build ${TARGET_OS} --${BUILD_FLAG}
   """]
@@ -176,7 +175,6 @@ script_runner = "@shell"
 [tasks.flutter-build.windows]
 script = ["""
   cd appflowy_flutter
-  exec cmd.exe /c flutter clean
   exec cmd.exe /c flutter pub get
   exec cmd.exe /c flutter build ${TARGET_OS} --${BUILD_FLAG} --build-name=${APP_VERSION}
   """]
@@ -186,16 +184,15 @@ script_runner = "@duckscript"
 script_runner = "@shell"
 script = [
   """
-  chmod +x scripts/code_generation/generate.sh
-  """,
-  "scripts/code_generation/generate.sh"
+  sh scripts/code_generation/generate.sh
+  """
 ]
 
 [tasks.code_generation.windows]
 script_runner = "@duckscript"
 script = [
   """
-  exec "scripts/code_generation/generate.cmd"
+  exec scripts/code_generation/generate.cmd
   """,
 ]