Browse Source

chore: Merge branch 'main' into develop

nathan 2 years ago
parent
commit
ad99998d33
100 changed files with 1982 additions and 949 deletions
  1. 2 0
      .github/workflows/flutter_ci.yaml
  2. 0 8
      .github/workflows/release.yml
  3. 3 1
      .github/workflows/translation_notify.yml
  4. 1 1
      frontend/Makefile.toml
  5. BIN
      frontend/appflowy_flutter/assets/test/workspaces/ai_workspace.zip
  6. BIN
      frontend/appflowy_flutter/assets/test/workspaces/cover_image.zip
  7. 422 0
      frontend/appflowy_flutter/assets/translations/ar-SA.json
  8. 11 3
      frontend/appflowy_flutter/assets/translations/en.json
  9. 1 1
      frontend/appflowy_flutter/assets/translations/id-ID.json
  10. 1 1
      frontend/appflowy_flutter/assets/translations/ja-JP.json
  11. 1 1
      frontend/appflowy_flutter/assets/translations/ko-KR.json
  12. 1 1
      frontend/appflowy_flutter/assets/translations/pt-BR.json
  13. 15 5
      frontend/appflowy_flutter/assets/translations/ru-RU.json
  14. 1 1
      frontend/appflowy_flutter/assets/translations/sv.json
  15. 1 1
      frontend/appflowy_flutter/assets/translations/zh-CN.json
  16. 1 1
      frontend/appflowy_flutter/assets/translations/zh-TW.json
  17. 54 0
      frontend/appflowy_flutter/integration_test/cover_image_test.dart
  18. 108 0
      frontend/appflowy_flutter/integration_test/open_ai_smart_menu_test.dart
  19. 2 0
      frontend/appflowy_flutter/integration_test/runner.dart
  20. 3 1
      frontend/appflowy_flutter/integration_test/util/data.dart
  21. 76 0
      frontend/appflowy_flutter/integration_test/util/mock/mock_openai_repository.dart
  22. 1 0
      frontend/appflowy_flutter/lib/core/helpers/helpers.dart
  23. 21 0
      frontend/appflowy_flutter/lib/core/helpers/url_validator.dart
  24. 2 6
      frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart
  25. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller_builder.dart
  26. 13 1
      frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_loader.dart
  27. 6 1
      frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_persistence.dart
  28. 9 3
      frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart
  29. 2 1
      frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart
  30. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart
  31. 5 5
      frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart
  32. 107 51
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart
  33. 200 108
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart
  34. 37 12
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart
  35. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart
  36. 18 7
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart
  37. 1 2
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_editor.dart
  38. 2 6
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_list.dart
  39. 2 11
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart
  40. 1 0
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart
  41. 2 43
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart
  42. 4 9
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/number.dart
  43. 6 7
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/select_option_editor.dart
  44. 67 28
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart
  45. 27 40
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart
  46. 15 9
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart
  47. 54 9
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart
  48. 10 7
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/checkbox_card_cell.dart
  49. 12 1
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/date_card_cell.dart
  50. 23 3
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/number_card_cell.dart
  51. 5 3
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart
  52. 16 3
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart
  53. 11 3
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/url_card_cell.dart
  54. 2 2
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/container/card_container.dart
  55. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart
  56. 1 4
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart
  57. 11 10
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/number_cell/number_cell_bloc.dart
  58. 7 1
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/text_cell/text_cell.dart
  59. 163 105
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart
  60. 1 12
      frontend/appflowy_flutter/lib/plugins/document/document_page.dart
  61. 47 16
      frontend/appflowy_flutter/lib/plugins/document/editor_styles.dart
  62. 12 3
      frontend/appflowy_flutter/lib/plugins/document/presentation/more/font_size_switcher.dart
  63. 1 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/more/more_button.dart
  64. 7 6
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/callout/callout_node_widget.dart
  65. 3 3
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/code_block/code_block_node_widget.dart
  66. 1 1
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/code_block/code_block_shortcut_event.dart
  67. 3 6
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/change_cover_popover.dart
  68. 55 42
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_image_picker.dart
  69. 63 36
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_node_widget.dart
  70. 0 1
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/emoji_popover.dart
  71. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/divider/divider_node_widget.dart
  72. 1 1
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/divider/divider_shortcut_event.dart
  73. 1 1
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/emoji_menu_item.dart
  74. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/emoji_picker.dart
  75. 1 1
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/src/config.dart
  76. 8 8
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/src/default_emoji_picker_view.dart
  77. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/src/emoji_lists.dart
  78. 13 13
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/src/emoji_picker.dart
  79. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/src/emoji_picker_builder.dart
  80. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/src/emoji_view_state.dart
  81. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/src/models/category_models.dart
  82. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/src/models/emoji_model.dart
  83. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/src/models/recent_emoji_model.dart
  84. 1 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/extensions/flowy_tint_extension.dart
  85. 0 168
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/horizontal_rule_node_widget.dart
  86. 0 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/infra/svg.dart
  87. 19 9
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/math_equation/math_equation_node_widget.dart
  88. 1 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart
  89. 10 1
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart
  90. 71 51
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart
  91. 21 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/plugins.dart
  92. 6 2
      frontend/appflowy_flutter/lib/plugins/trash/src/sizes.dart
  93. 6 10
      frontend/appflowy_flutter/lib/plugins/trash/src/trash_cell.dart
  94. 2 8
      frontend/appflowy_flutter/lib/plugins/trash/trash_page.dart
  95. 21 2
      frontend/appflowy_flutter/lib/startup/deps_resolver.dart
  96. 1 0
      frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart
  97. 20 10
      frontend/appflowy_flutter/lib/workspace/application/appearance.dart
  98. 2 0
      frontend/appflowy_flutter/lib/workspace/application/settings/settings_location_cubit.dart
  99. 4 4
      frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart
  100. 9 3
      frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart

+ 2 - 0
.github/workflows/flutter_ci.yaml

@@ -8,6 +8,7 @@ on:
       - "release/*"
     paths:
       - "frontend/**"
+      - "!frontend/appflowy_tauri/**"
 
   pull_request:
     branches:
@@ -16,6 +17,7 @@ on:
       - "release/*"
     paths:
       - "frontend/**"
+      - "!frontend/appflowy_tauri/**"
 
 env:
   FLUTTER_VERSION: "3.7.5"

+ 0 - 8
.github/workflows/release.yml

@@ -236,14 +236,6 @@ jobs:
               extra-build-args: "",
               flutter_profile: production-linux-x86_64,
             }
-          # - { arch: aarch64, target: aarch64-unknown-linux-gnu, os: ubuntu-20.04, extra-build-args: "", flutter_profile: production-linux-aarch64 }
-          - {
-              arch: x86_64,
-              target: x86_64-unknown-linux-gnu,
-              os: ubuntu-18.04,
-              extra-build-args: "",
-              flutter_profile: production-linux-x86_64,
-            }
     steps:
       - name: Checkout source code
         uses: actions/checkout@v3

+ 3 - 1
.github/workflows/translation_notify.yml

@@ -13,4 +13,6 @@ jobs:
         env:
           DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
         with:
-          args: '@appflowytranslators English UI strings has been updated.'
+          args: |
+            @appflowytranslators English UI strings has been updated.
+            Link to changes: ${{github.event.compare}}

+ 1 - 1
frontend/Makefile.toml

@@ -23,7 +23,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
 CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
 CARGO_MAKE_CRATE_NAME = "dart-ffi"
 LIB_NAME = "dart_ffi"
-CURRENT_APP_VERSION = "0.1.2"
+CURRENT_APP_VERSION = "0.1.3"
 FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
 PRODUCT_NAME = "AppFlowy"
 # CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html

BIN
frontend/appflowy_flutter/assets/test/workspaces/ai_workspace.zip


BIN
frontend/appflowy_flutter/assets/test/workspaces/cover_image.zip


+ 422 - 0
frontend/appflowy_flutter/assets/translations/ar-SA.json

@@ -0,0 +1,422 @@
+{
+    "appName": "AppFlowy",
+    "defaultUsername": "أنا",
+    "welcomeText": "مرحبًا بك في @: appName",
+    "githubStarText": "نجمة على GitHub",
+    "subscribeNewsletterText": "اشترك في النشرة الإخبارية",
+    "letsGoButtonText": "بداية سريعة",
+    "title": "عنوان",
+    "signUp": {
+        "buttonText": "اشتراك",
+        "title": "قم بالتسجيل في @: appName",
+        "getStartedText": "البدء",
+        "emptyPasswordError": "لا يمكن أن تكون كلمة المرور فارغة",
+        "repeatPasswordEmptyError": "إعادة كلمة المرور لا يمكن أن تكون فارغة",
+        "unmatchedPasswordError": "تكرار كلمة المرور ليس هو نفسه كلمة المرور",
+        "alreadyHaveAnAccount": "هل لديك حساب؟",
+        "emailHint": "بريد إلكتروني",
+        "passwordHint": "كلمة المرور",
+        "repeatPasswordHint": "اعد كلمة السر"
+    },
+    "signIn": {
+        "loginTitle": "تسجيل الدخول إلى @: appName",
+        "loginButtonText": "تسجيل الدخول",
+        "buttonText": "تسجيل الدخول",
+        "forgotPassword": "هل نسيت كلمة السر؟",
+        "emailHint": "بريد إلكتروني",
+        "passwordHint": "كلمة المرور",
+        "dontHaveAnAccount": "ليس لديك حساب؟",
+        "repeatPasswordEmptyError": "إعادة كلمة المرور لا يمكن أن تكون فارغة",
+        "unmatchedPasswordError": "تكرار كلمة المرور ليس هو نفسه كلمة المرور"
+    },
+    "workspace": {
+        "create": "قم بإنشاء مساحة عمل",
+        "hint": "مساحة العمل",
+        "notFoundError": "مساحة العمل غير موجودة"
+    },
+    "shareAction": {
+        "buttonText": "مشاركه",
+        "workInProgress": "قريباً",
+        "markdown": "Markdown",
+        "copyLink": "نسخ الرابط"
+    },
+    "moreAction": {
+        "small": "صغير",
+        "medium": "متوسط",
+        "large": "كبير",
+        "fontSize": "حجم الخط",
+        "import": "استيراد"
+    },
+    "disclosureAction": {
+        "rename": "إعادة تسمية",
+        "delete": "يمسح",
+        "duplicate": "كرر"
+    },
+    "blankPageTitle": "صفحة فارغة",
+    "newPageText": "صفحة جديدة",
+    "trash": {
+        "text": "المهملات",
+        "restoreAll": "استعادة الكل",
+        "deleteAll": "حذف الكل",
+        "pageHeader": {
+            "fileName": "اسم الملف",
+            "lastModified": "آخر تعديل",
+            "created": "تم انشاؤها"
+        }
+    },
+    "deletePagePrompt": {
+        "text": "هذه الصفحة في المهملات",
+        "restore": "استعادة الصفحة",
+        "deletePermanent": "الحذف بشكل نهائي"
+    },
+    "dialogCreatePageNameHint": "اسم الصفحة",
+    "questionBubble": {
+        "shortcuts": "الاختصارات",
+        "whatsNew": "ما هو الجديد؟",
+        "help": "المساعدة والدعم",
+        "markdown": "Markdown",
+        "debug": {
+            "name": "معلومات التصحيح",
+            "success": "تم نسخ معلومات التصحيح إلى الحافظة!",
+            "fail": "تعذر نسخ معلومات التصحيح إلى الحافظة"
+        }
+    },
+    "menuAppHeader": {
+        "addPageTooltip": "أضف صفحة في الداخل بسرعة",
+        "defaultNewPageName": "بدون عنوان",
+        "renameDialog": "إعادة تسمية"
+    },
+    "toolbar": {
+        "undo": "الغاء التحميل",
+        "redo": "إعادة",
+        "bold": "عريض",
+        "italic": "مائل",
+        "underline": "تسطير",
+        "strike": "يتوسطه خط",
+        "numList": "قائمة مرقمة",
+        "bulletList": "قائمة نقطية",
+        "checkList": "قائمة تدقيق",
+        "inlineCode": "رمز مضمّن",
+        "quote": "كتلة اقتباس",
+        "header": "رأس",
+        "highlight": "تسليط الضوء",
+        "color": "لون"
+    },
+    "tooltip": {
+        "lightMode": "قم بالتبديل إلى وضع الإضاءة",
+        "darkMode": "قم بالتبديل إلى الوضع الداكن",
+        "openAsPage": "فتح كصفحة",
+        "addNewRow": "أضف صفًا جديدًا",
+        "openMenu": "انقر لفتح القائمة",
+        "viewDataBase": "عرض قاعدة البيانات",
+        "referencePage": "تمت الإشارة إلى هذا {name}"
+    },
+    "sideBar": {
+        "closeSidebar": "إغلاق الشريط الجانبي",
+        "openSidebar": "فتح الشريط الجانبي"
+    },
+    "notifications": {
+        "export": {
+            "markdown": "تم تصدير ملاحظة إلى Markdown",
+            "path": "Documents/flowy"
+        }
+    },
+    "contactsPage": {
+        "title": "جهات الاتصال",
+        "whatsHappening": "ماذا يحدث هذا الاسبوع؟",
+        "addContact": "إضافة جهة اتصال",
+        "editContact": "تحرير جهة الاتصال"
+    },
+    "button": {
+        "OK": "نعم",
+        "Done": "منتهي",
+        "Cancel": "إلغاء",
+        "signIn": "تسجيل الدخول",
+        "signOut": "خروج",
+        "complete": "مكتمل",
+        "save": "حفظ",
+        "generate": "يولد",
+        "esc": "خروج",
+        "keep": "ابقاء",
+        "tryAgain": "حاول ثانية",
+        "discard": "تجاهل",
+        "replace": "يستبدل",
+        "insertBelow": "إدراج أدناه"
+    },
+    "label": {
+        "welcome": "مرحباً!",
+        "firstName": "الاسم الأول",
+        "middleName": "الاسم الأوسط",
+        "lastName": "اسم العائلة",
+        "stepX": "الخطوة {X}"
+    },
+    "oAuth": {
+        "err": {
+            "failedTitle": "غير قادر على الاتصال بحسابك.",
+            "failedMsg": "يرجى التأكد من إكمال عملية تسجيل الدخول في متصفحك."
+        },
+        "google": {
+            "title": "تسجيل الدخول إلى GOOGLE",
+            "instruction1": "لاستيراد جهات اتصال Google الخاصة بك ، ستحتاج إلى ترخيص هذا التطبيق باستخدام متصفح الويب الخاص بك.",
+            "instruction2": "انسخ هذا الرمز إلى الحافظة الخاصة بك عن طريق النقر فوق الرمز أو تحديد النص:",
+            "instruction3": "انتقل إلى الرابط التالي في متصفح الويب الخاص بك ، وأدخل الرمز أعلاه:",
+            "instruction4": "اضغط على الزر أدناه عند الانتهاء من التسجيل:"
+        }
+    },
+    "settings": {
+        "title": "إعدادات",
+        "menu": {
+            "appearance": "مظهر",
+            "language": "لغة",
+            "user": "مستخدم",
+            "files": "الملفات",
+            "open": "أفتح الإعدادات"
+        },
+        "appearance": {
+            "themeMode": {
+                "label": "وضع السمة",
+                "light": "وضع الضوء",
+                "dark": "الوضع الداكن",
+                "system": "التكيف مع النظام"
+            },
+            "theme": "سمة"
+        },
+        "files": {
+            "defaultLocation": "أين يتم تخزين بياناتك الآن",
+            "doubleTapToCopy": "انقر نقرًا مزدوجًا لنسخ المسار",
+            "restoreLocation": "استعادة المسار الافتراضي AppFlowy",
+            "customizeLocation": "افتح مجلدًا آخر",
+            "restartApp": "يرجى إعادة تشغيل التطبيق لتصبح التغييرات سارية المفعول.",
+            "exportDatabase": "تصدير قاعدة البيانات",
+            "selectFiles": "حدد الملفات التي تريد تصديرها",
+            "createNewFolder": "انشاء مجلد جديد",
+            "createNewFolderDesc": "أخبرنا بالمكان الذي تريد تخزين بياناتك فيه",
+            "open": "يفتح",
+            "openFolder": "افتح مجلدًا موجودًا",
+            "openFolderDesc": "اقرأها واكتبها في مجلد AppFlowy الموجود لديك",
+            "folderHintText": "إسم الملف",
+            "location": "إنشاء مجلد جديد",
+            "locationDesc": "اختر اسمًا لمجلد بيانات AppFlowy",
+            "browser": "تصفح",
+            "create": "يخلق",
+            "folderPath": "مسار لتخزين المجلد الخاص بك",
+            "locationCannotBeEmpty": "لا يمكن أن يكون المسار فارغًا",
+            "pathCopiedSnackbar": "تم نسخ مسار تخزين الملفات إلى الحافظة!"
+        },
+        "user": {
+            "name": "اسم",
+            "icon": "أيقونة",
+            "selectAnIcon": "حدد أيقونة",
+            "pleaseInputYourOpenAIKey": "الرجاء إدخال مفتاح OpenAI الخاص بك"
+        }
+    },
+    "grid": {
+        "settings": {
+            "filter": "منقي",
+            "sort": "نوع",
+            "sortBy": "ترتيب حسب",
+            "Properties": "ملكيات",
+            "group": "مجموعة",
+            "addFilter": "أضف عامل تصفية",
+            "deleteFilter": "حذف عامل التصفية",
+            "filterBy": "مصنف بواسطة...",
+            "typeAValue": "اكتب قيمة ...",
+            "layout": "تَخطِيط"
+        },
+        "textFilter": {
+            "contains": "يتضمن",
+            "doesNotContain": "لا يحتوي",
+            "endsWith": "ينتهي بـ",
+            "startWith": "ابدا ب",
+            "is": "يكون",
+            "isNot": "ليس",
+            "isEmpty": "فارغ",
+            "isNotEmpty": "ليس فارغا",
+            "choicechipPrefix": {
+                "isNot": "لا",
+                "startWith": "ابدا ب",
+                "endWith": "ينتهي بـ",
+                "isEmpty": "فارغ",
+                "isNotEmpty": "ليس فارغا"
+            }
+        },
+        "checkboxFilter": {
+            "isChecked": "التحقق",
+            "isUnchecked": "لم يتم التحقق منه",
+            "choicechipPrefix": {
+                "is": "يكون"
+            }
+        },
+        "checklistFilter": {
+            "isComplete": "كاملة",
+            "isIncomplted": "غير مكتمل"
+        },
+        "singleSelectOptionFilter": {
+            "is": "يكون",
+            "isNot": "ليس",
+            "isEmpty": "فارغ",
+            "isNotEmpty": "ليس فارغا"
+        },
+        "multiSelectOptionFilter": {
+            "contains": "يتضمن",
+            "doesNotContain": "لا يحتوي",
+            "isEmpty": "فارغ",
+            "isNotEmpty": "ليس فارغا"
+        },
+        "field": {
+            "hide": "يخفي",
+            "insertLeft": "أدخل اليسار",
+            "insertRight": "أدخل اليمين",
+            "duplicate": "ينسخ",
+            "delete": "يمسح",
+            "textFieldName": "نص",
+            "checkboxFieldName": "خانة اختيار",
+            "dateFieldName": "تاريخ",
+            "numberFieldName": "أعداد",
+            "singleSelectFieldName": "يختار",
+            "multiSelectFieldName": "تحديد متعدد",
+            "urlFieldName": "URL",
+            "checklistFieldName": "قائمة تدقيق",
+            "numberFormat": "تنسيق الأرقام",
+            "dateFormat": "صيغة التاريخ",
+            "includeTime": "أضف الوقت",
+            "dateFormatFriendly": "شهر يوم سنه",
+            "dateFormatISO": "سنة شهر يوم",
+            "dateFormatLocal": "شهر يوم سنه",
+            "dateFormatUS": "سنة شهر يوم",
+            "dateFormatDayMonthYear": "يوم شهر سنة",
+            "timeFormat": "تنسيق الوقت",
+            "invalidTimeFormat": "تنسيق غير صالح",
+            "timeFormatTwelveHour": "12 ساعة",
+            "timeFormatTwentyFourHour": "24 ساعة",
+            "addSelectOption": "أضف خيارًا",
+            "optionTitle": "خيارات",
+            "addOption": "إضافة خيار",
+            "editProperty": "تحرير الملكية",
+            "newProperty": "خاصية جديدة",
+            "deleteFieldPromptMessage": "هل أنت متأكد؟ سيتم حذف هذه الخاصية"
+        },
+        "sort": {
+            "ascending": "تصاعدي",
+            "descending": "تنازلي",
+            "deleteSort": "حذف الفرز",
+            "addSort": "أضف نوعًا"
+        },
+        "row": {
+            "duplicate": "مكرره",
+            "delete": "يمسح",
+            "textPlaceholder": "فارغ",
+            "copyProperty": "نسخ الممتلكات إلى الحافظة",
+            "count": "عدد",
+            "newRow": "صف جديد"
+        },
+        "selectOption": {
+            "create": "يخلق",
+            "purpleColor": "أرجواني",
+            "pinkColor": "لون القرنفل",
+            "lightPinkColor": "وردي فاتح",
+            "orangeColor": "البرتقالي",
+            "yellowColor": "أصفر",
+            "limeColor": "جير",
+            "greenColor": "أخضر",
+            "aquaColor": "أكوا",
+            "blueColor": "أزرق",
+            "deleteTag": "حذف العلامة",
+            "colorPanelTitle": "الألوان",
+            "panelTitle": "حدد خيارًا أو أنشئ خيارًا",
+            "searchOption": "ابحث عن خيار"
+        },
+        "checklist": {
+            "panelTitle": "أضف عنصرًا"
+        },
+        "menuName": "شبكة",
+        "referencedGridPrefix": "نظرا ل"
+    },
+    "document": {
+        "menuName": "وثيقة",
+        "date": {
+            "timeHintTextInTwelveHour": "01:00 مساءً",
+            "timeHintTextInTwentyFourHour": "13:00"
+        },
+        "slashMenu": {
+            "board": {
+                "selectABoardToLinkTo": "حدد لوحة للارتباط بها",
+                "createANewBoard": "قم بإنشاء لوحة جديدة"
+            },
+            "grid": {
+                "selectAGridToLinkTo": "حدد الشبكة للارتباط بها",
+                "createANewGrid": "قم بإنشاء شبكة جديدة"
+            }
+        },
+        "plugins": {
+            "referencedBoard": "المجلس المشار إليه",
+            "referencedGrid": "الشبكة المشار إليها",
+            "autoGeneratorMenuItemName": "كاتب OpenAI",
+            "autoGeneratorTitleName": "OpenAI: اطلب من الذكاء الاصطناعي كتابة أي شيء ...",
+            "autoGeneratorLearnMore": "يتعلم أكثر",
+            "autoGeneratorGenerate": "يولد",
+            "autoGeneratorHintText": "اسأل OpenAI ...",
+            "autoGeneratorCantGetOpenAIKey": "لا يمكن الحصول على مفتاح OpenAI",
+            "smartEdit": "مساعدي الذكاء الاصطناعي",
+            "openAI": "OpenAI",
+            "smartEditFixSpelling": "أصلح التهجئة",
+            "warning": "⚠️ يمكن أن تكون استجابات الذكاء الاصطناعي غير دقيقة أو مضللة.",
+            "smartEditSummarize": "لخص",
+            "smartEditCouldNotFetchResult": "تعذر جلب النتيجة من OpenAI",
+            "smartEditCouldNotFetchKey": "تعذر جلب مفتاح OpenAI",
+            "smartEditDisabled": "قم بتوصيل OpenAI في الإعدادات",
+            "discardResponse": "هل تريد تجاهل استجابات الذكاء الاصطناعي؟",
+            "cover": {
+                "changeCover": "تبديل الغطاء",
+                "colors": "الألوان",
+                "images": "الصور",
+                "clearAll": "امسح الكل",
+                "abstract": "خلاصة",
+                "addCover": "أضف الغلاف",
+                "addLocalImage": "أضف الصورة المحلية",
+                "invalidImageUrl": "عنوان URL للصورة غير صالح",
+                "failedToAddImageToGallery": "فشل في إضافة الصورة إلى المعرض",
+                "enterImageUrl": "أدخل عنوان URL للصورة",
+                "add": "يضيف",
+                "back": "خلف",
+                "saveToGallery": "حفظ في المعرض",
+                "removeIcon": "إزالة الرمز",
+                "pasteImageUrl": "لصق عنوان URL للصورة",
+                "or": "أو",
+                "pickFromFiles": "اختر من الملفات",
+                "couldNotFetchImage": "تعذر جلب الصورة",
+                "imageSavingFailed": "فشل حفظ الصورة",
+                "addIcon": "إضافة أيقونة",
+                "coverRemoveAlert": "ستتم إزالته من الغلاف بعد حذفه.",
+                "alertDialogConfirmation": "هل أنت متأكد أنك تريد الاستمرار؟"
+            },
+            "mathEquation": {
+                "addMathEquation": "أضف معادلة رياضية",
+                "editMathEquation": "تحرير المعادلة الرياضية"
+            }
+        }
+    },
+    "board": {
+        "column": {
+            "create_new_card": "جديد"
+        },
+        "menuName": "سبورة",
+        "referencedBoardPrefix": "نظرا ل"
+    },
+    "calendar": {
+        "menuName": "تقويم",
+        "defaultNewCalendarTitle": "بدون عنوان",
+        "navigation": {
+            "today": "اليوم",
+            "jumpToday": "انتقل إلى اليوم",
+            "previousMonth": "الشهر الماضى",
+            "nextMonth": "الشهر القادم"
+        },
+        "settings": {
+            "showWeekNumbers": "إظهار أرقام الأسبوع",
+            "showWeekends": "عرض عطلات نهاية الأسبوع",
+            "firstDayOfWeek": "اليوم الأول من الأسبوع",
+            "layoutDateField": "تقويم التخطيط بواسطة"
+        }
+    }
+}

+ 11 - 3
frontend/appflowy_flutter/assets/translations/en.json

@@ -74,6 +74,7 @@
     "shortcuts": "Shortcuts",
     "whatsNew": "What's new?",
     "help": "Help & Support",
+    "markdown": "Markdown",
     "debug": {
       "name": "Debug Info",
       "success": "Copied debug info to clipboard!",
@@ -128,6 +129,7 @@
   },
   "button": {
     "OK": "OK",
+    "Done": "Done",
     "Cancel": "Cancel",
     "signIn": "Sign In",
     "signOut": "Sign Out",
@@ -278,7 +280,7 @@
       "numberFormat": "Number format",
       "dateFormat": "Date format",
       "includeTime": "Include time",
-      "dateFormatFriendly": "Month Day,Year",
+      "dateFormatFriendly": "Month Day, Year",
       "dateFormatISO": "Year-Month-Day",
       "dateFormatLocal": "Month/Day/Year",
       "dateFormatUS": "Year/Month/Day",
@@ -306,7 +308,8 @@
       "textPlaceholder": "Empty",
       "copyProperty": "Copied property to clipboard",
       "count": "Count",
-      "newRow": "New row"
+      "newRow": "New row",
+      "action": "Action"
     },
     "selectOption": {
       "create": "Create",
@@ -360,6 +363,7 @@
       "smartEditFixSpelling": "Fix spelling",
       "warning": "⚠️ AI responses can be inaccurate or misleading.",
       "smartEditSummarize": "Summarize",
+      "smartEditImproveWriting": "Improve Writing",
       "smartEditCouldNotFetchResult": "Could not fetch result from OpenAI",
       "smartEditCouldNotFetchKey": "Could not fetch OpenAI key",
       "smartEditDisabled": "Connect OpenAI in Settings",
@@ -387,6 +391,10 @@
         "addIcon": "Add Icon",
         "coverRemoveAlert": "It will be removed from cover after it is deleted.",
         "alertDialogConfirmation": "Are you sure, you want to continue?"
+      },
+      "mathEquation": {
+        "addMathEquation": "Add Math Equation",
+        "editMathEquation": "Edit Math Equation"
       }
     }
   },
@@ -409,7 +417,7 @@
     "settings": {
       "showWeekNumbers": "Show week numbers",
       "showWeekends": "Show weekends",
-      "firstDayOfWeek": "First day of week",
+      "firstDayOfWeek": "Start week on",
       "layoutDateField": "Layout calendar by"
     }
   }

+ 1 - 1
frontend/appflowy_flutter/assets/translations/id-ID.json

@@ -175,7 +175,7 @@
       "numberFormat": "Format angka",
       "dateFormat": "Format tanggal",
       "includeTime": "Sertakan waktu",
-      "dateFormatFriendly": "Bulan Hari,Tahun",
+      "dateFormatFriendly": "Bulan Hari, Tahun",
       "dateFormatISO": "Tahun-Bulan-Hari",
       "dateFormatLocal": "Bulan/Hari/Tahun",
       "dateFormatUS": "Tahun/Bulan/Hari",

+ 1 - 1
frontend/appflowy_flutter/assets/translations/ja-JP.json

@@ -167,7 +167,7 @@
       "numberFormat": "数値書式",
       "dateFormat": "日付書式",
       "includeTime": "時刻を含める",
-      "dateFormatFriendly": "月 日,年",
+      "dateFormatFriendly": "月 日, 年",
       "dateFormatISO": "年-月-日",
       "dateFormatLocal": "月/日/年",
       "dateFormatUS": "年/月/日",

+ 1 - 1
frontend/appflowy_flutter/assets/translations/ko-KR.json

@@ -179,7 +179,7 @@
       "numberFormat": "숫자 형식",
       "dateFormat": "날짜 형식",
       "includeTime": "시간 표시",
-      "dateFormatFriendly": "월 일,년",
+      "dateFormatFriendly": "월 일, 년",
       "dateFormatISO": "년-월-일",
       "dateFormatLocal": "월/일/년",
       "dateFormatUS": "년/월/일",

+ 1 - 1
frontend/appflowy_flutter/assets/translations/pt-BR.json

@@ -275,7 +275,7 @@
       "numberFormat": "Formato numérico",
       "dateFormat": "Formato de data",
       "includeTime": "Incluir hora",
-      "dateFormatFriendly": "Mês Dia,Ano",
+      "dateFormatFriendly": "Mês Dia, Ano",
       "dateFormatISO": "Ano-Mês-Dia",
       "dateFormatLocal": "Mês/Dia/Ano",
       "dateFormatUS": "Ano/Mês/Dia",

+ 15 - 5
frontend/appflowy_flutter/assets/translations/ru-RU.json

@@ -44,7 +44,8 @@
     "small": "маленький",
     "medium": "средний",
     "large": "большой",
-    "fontSize": "Размер шрифта"
+    "fontSize": "Размер шрифта",
+    "import": "Импортировать"
   },
   "disclosureAction": {
     "rename": "Переименовать",
@@ -70,8 +71,10 @@
   },
   "dialogCreatePageNameHint": "Имя страницы",
   "questionBubble": {
+    "shortcuts": "Комбинации клавиш",
     "whatsNew": "Что нового?",
     "help": "Помощь",
+    "markdown": "Markdown",
     "debug": {
       "name": "Отладочная информация",
       "success": "Скопировано в буфер обмена!",
@@ -126,6 +129,7 @@
   },
   "button": {
     "OK": "OK",
+    "Done": "Завершить",
     "Cancel": "Отмена",
     "signIn": "Войти",
     "signOut": "Выйти",
@@ -170,7 +174,7 @@
     },
     "appearance": {
       "themeMode": {
-        "label": "Режим темы",
+        "label": "Тема приложения",
         "light": "Светлая",
         "dark": "Тёмная",
         "system": "Системная"
@@ -197,7 +201,7 @@
       "create": "Создать",
       "folderPath": "Путь к вашей папке",
       "locationCannotBeEmpty": "Путь не может быть пустым",
-      "pathCopiedSnackbar": "File storage path copied to clipboard!"
+      "pathCopiedSnackbar": "Путь скопирован в буфер обмена!"
     },
     "user": {
       "name": "Имя",
@@ -216,7 +220,8 @@
       "addFilter": "Добавить фильтр",
       "deleteFilter": "Удалить фильтр",
       "filterBy": "Фильтровать по...",
-      "typeAValue": "Введите значение..."
+      "typeAValue": "Введите значение...",
+      "layout": "Раскладка"
     },
     "textFilter": {
       "contains": "Содержит",
@@ -303,7 +308,8 @@
       "textPlaceholder": "Пусто",
       "copyProperty": "Свойство скопировано",
       "count": "Количество",
-      "newRow": "Новая строка"
+      "newRow": "Новая строка",
+      "action": "Действия"
     },
     "selectOption": {
       "create": "Создать",
@@ -384,6 +390,10 @@
         "addIcon": "Добавить иконку",
         "coverRemoveAlert": "Изображение будет удалено с обложки",
         "alertDialogConfirmation": "Вы хотите продолжить?"
+      },
+      "mathEquation": {
+        "addMathEquation": "Добавить математическое выражение",
+        "editMathEquation": "Редактировать математическое выражение"
       }
     }
   },

+ 1 - 1
frontend/appflowy_flutter/assets/translations/sv.json

@@ -183,7 +183,7 @@
       "numberFormat": "Sifferformat",
       "dateFormat": "Datumformat",
       "includeTime": "Inkludera tid",
-      "dateFormatFriendly": "Månad Dag,År",
+      "dateFormatFriendly": "Månad Dag, År",
       "dateFormatISO": "År-Månad-Dag",
       "dateFormatLocal": "Månad/Dag/År",
       "dateFormatUS": "År/Månad/Dag",

+ 1 - 1
frontend/appflowy_flutter/assets/translations/zh-CN.json

@@ -183,7 +183,7 @@
       "numberFormat": "数字格式",
       "dateFormat": "日期格式",
       "includeTime": "包含时间",
-      "dateFormatFriendly": "月 日,年",
+      "dateFormatFriendly": "月 日, 年",
       "dateFormatISO": "年-月-日",
       "dateFormatLocal": "月/日/年",
       "dateFormatUS": "年/月/日",

+ 1 - 1
frontend/appflowy_flutter/assets/translations/zh-TW.json

@@ -278,7 +278,7 @@
       "numberFormat": "數字格式",
       "dateFormat": "日期格式",
       "includeTime": "包含時間",
-      "dateFormatFriendly": "月 日,年",
+      "dateFormatFriendly": "月 日, 年",
       "dateFormatISO": "年-月-日",
       "dateFormatLocal": "月/日/年",
       "dateFormatUS": "年/月/日",

+ 54 - 0
frontend/appflowy_flutter/integration_test/cover_image_test.dart

@@ -0,0 +1,54 @@
+import 'package:flowy_infra_ui/widget/rounded_button.dart';
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+import 'util/util.dart';
+
+/// Integration tests for an empty board. The [TestWorkspaceService] will load
+/// a workspace from an empty board `assets/test/workspaces/board.zip` for all
+/// tests.
+///
+/// To create another integration test with a preconfigured workspace.
+/// Use the following steps.
+/// 1. Create a new workspace from the AppFlowy launch screen.
+/// 2. Modify the workspace until it is suitable as the starting point for
+///    the integration test you need to land.
+/// 3. Use a zip utility program to zip the workspace folder that you created.
+/// 4. Add the zip file under `assets/test/workspaces/`
+/// 5. Add a new enumeration to [TestWorkspace] in `integration_test/utils/data.dart`.
+///    For example, if you added a workspace called `empty_calendar.zip`,
+///    then [TestWorkspace] should have the following value:
+/// ```dart
+/// enum TestWorkspace {
+///   board('board'),
+///   empty_calendar('empty_calendar');
+///
+///   /* code */
+/// }
+/// ```
+/// 6. Double check that the .zip file that you added is included as an asset in
+///    the pubspec.yaml file under appflowy_flutter.
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+  const service = TestWorkspaceService(TestWorkspace.coverImage);
+
+  group('cover image', () {
+    setUpAll(() async => await service.setUpAll());
+    setUp(() async => await service.setUp());
+
+    testWidgets(
+        'hovering on cover image will display change and delete cover image buttons',
+        (tester) async {
+      await tester.initializeAppFlowy();
+      expect(find.byType(Image), findsOneWidget);
+
+      final TestPointer pointer = TestPointer(1, PointerDeviceKind.mouse);
+      final imageFinder = find.byType(Image);
+      Offset offset = tester.getCenter(imageFinder);
+
+      pointer.hover(offset);
+      expect(find.byType(RoundedTextButton), findsOneWidget);
+    });
+  });
+}

+ 108 - 0
frontend/appflowy_flutter/integration_test/open_ai_smart_menu_test.dart

@@ -0,0 +1,108 @@
+import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+import 'util/mock/mock_openai_repository.dart';
+import 'util/util.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';
+import 'package:appflowy/startup/startup.dart';
+
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+  const service = TestWorkspaceService(TestWorkspace.aiWorkSpace);
+
+  group('integration tests for open-ai smart menu', () {
+    setUpAll(() async => await service.setUpAll());
+    setUp(() async => await service.setUp());
+
+    testWidgets('testing selection on open-ai smart menu replace', (tester) async {
+      final appFlowyEditor = await setUpOpenAITesting(tester);
+      final editorState = appFlowyEditor.editorState;
+
+      editorState.service.selectionService.updateSelection(
+        Selection(
+          start: Position(path: [1], offset: 4),
+          end: Position(path: [1], offset: 10),
+        ),
+      );
+      await tester.pumpAndSettle(const Duration(milliseconds: 500));
+      await tester.pumpAndSettle();
+
+      expect(find.byType(ToolbarWidget), findsAtLeastNWidgets(1));
+
+      await tester.tap(find.byTooltip('AI Assistants'));
+      await tester.pumpAndSettle(const Duration(milliseconds: 500));
+
+      await tester.tap(find.text('Summarize'));
+      await tester.pumpAndSettle();
+
+      await tester.tap(find.byType(FlowyRichTextButton, skipOffstage: false).first);
+      await tester.pumpAndSettle();
+
+      expect(
+        editorState.service.selectionService.currentSelection.value,
+        Selection(
+          start: Position(path: [1], offset: 4),
+          end: Position(path: [1], offset: 84),
+        ),
+      );
+    });
+    testWidgets('testing selection on open-ai smart menu insert', (tester) async {
+      final appFlowyEditor = await setUpOpenAITesting(tester);
+      final editorState = appFlowyEditor.editorState;
+
+      editorState.service.selectionService.updateSelection(
+        Selection(
+          start: Position(path: [1], offset: 0),
+          end: Position(path: [1], offset: 5),
+        ),
+      );
+      await tester.pumpAndSettle(const Duration(milliseconds: 500));
+      await tester.pumpAndSettle();
+      expect(find.byType(ToolbarWidget), findsAtLeastNWidgets(1));
+
+      await tester.tap(find.byTooltip('AI Assistants'));
+      await tester.pumpAndSettle(const Duration(milliseconds: 500));
+
+      await tester.tap(find.text('Summarize'));
+      await tester.pumpAndSettle();
+
+      await tester.tap(find.byType(FlowyRichTextButton, skipOffstage: false).at(1));
+      await tester.pumpAndSettle();
+
+      expect(
+        editorState.service.selectionService.currentSelection.value,
+        Selection(
+          start: Position(path: [2], offset: 0),
+          end: Position(path: [3], offset: 0),
+        ),
+      );
+    });
+  });
+}
+
+Future<AppFlowyEditor> setUpOpenAITesting(WidgetTester tester) async {
+  await tester.initializeAppFlowy();
+  await mockOpenAIRepository();
+
+  await simulateKeyDownEvent(LogicalKeyboardKey.controlLeft);
+  await simulateKeyDownEvent(LogicalKeyboardKey.backslash);
+  await tester.pumpAndSettle();
+
+  final Finder editor = find.byType(AppFlowyEditor);
+  await tester.tap(editor);
+  await tester.pumpAndSettle();
+  return (tester.state(editor).widget as AppFlowyEditor);
+}
+
+Future<void> mockOpenAIRepository() async {
+  await getIt.unregister<OpenAIRepository>();
+  getIt.registerFactoryAsync<OpenAIRepository>(
+    () => Future.value(
+      MockOpenAIRepository(),
+    ),
+  );
+  return;
+}

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

@@ -3,6 +3,7 @@ import 'package:integration_test/integration_test.dart';
 import 'board_test.dart' as board_test;
 import 'switch_folder_test.dart' as switch_folder_test;
 import 'empty_document_test.dart' as empty_document_test;
+import 'open_ai_smart_menu_test.dart' as smart_menu_test;
 
 /// The main task runner for all integration tests in AppFlowy.
 ///
@@ -16,4 +17,5 @@ void main() {
   switch_folder_test.main();
   board_test.main();
   empty_document_test.main();
+  smart_menu_test.main();
 }

+ 3 - 1
frontend/appflowy_flutter/integration_test/util/data.dart

@@ -9,7 +9,9 @@ import 'package:shared_preferences/shared_preferences.dart';
 
 enum TestWorkspace {
   board("board"),
-  emptyDocument("empty_document");
+  emptyDocument("empty_document"),
+  aiWorkSpace("ai_workspace"),
+  coverImage("cover_image");
 
   const TestWorkspace(this._name);
 

+ 76 - 0
frontend/appflowy_flutter/integration_test/util/mock/mock_openai_repository.dart

@@ -0,0 +1,76 @@
+import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
+import 'package:mocktail/mocktail.dart';
+import 'dart:convert';
+import 'package:appflowy/plugins/document/presentation/plugins/openai/service/text_completion.dart';
+import 'package:appflowy/plugins/document/presentation/plugins/openai/service/error.dart';
+import 'package:http/http.dart' as http;
+import 'dart:async';
+
+class MyMockClient extends Mock implements http.Client {
+  @override
+  Future<http.StreamedResponse> send(http.BaseRequest request) async {
+    final requestType = request.method;
+    final requestUri = request.url;
+
+    if (requestType == 'POST' && requestUri == OpenAIRequestType.textCompletion.uri) {
+      final responseHeaders = <String, String>{'content-type': 'text/event-stream'};
+      final responseBody = Stream.fromIterable([
+        utf8.encode(
+          '{ "choices": [{"text": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula ", "index": 0, "logprobs": null, "finish_reason": null}]}',
+        ),
+        utf8.encode('\n'),
+        utf8.encode('[DONE]'),
+      ]);
+
+      // Return a mocked response with the expected data
+      return http.StreamedResponse(responseBody, 200, headers: responseHeaders);
+    }
+
+    // Return an error response for any other request
+    return http.StreamedResponse(const Stream.empty(), 404);
+  }
+}
+
+class MockOpenAIRepository extends HttpOpenAIRepository {
+  MockOpenAIRepository() : super(apiKey: 'dummyKey', client: MyMockClient());
+
+  @override
+  Future<void> getStreamedCompletions({
+    required String prompt,
+    required Future<void> Function() onStart,
+    required Future<void> Function(TextCompletionResponse response) onProcess,
+    required Future<void> Function() onEnd,
+    required void Function(OpenAIError error) onError,
+    String? suffix,
+    int maxTokens = 2048,
+    double temperature = 0.3,
+    bool useAction = false,
+  }) async {
+    final request = http.Request('POST', OpenAIRequestType.textCompletion.uri);
+    final response = await client.send(request);
+
+    var previousSyntax = '';
+    if (response.statusCode == 200) {
+      await for (final chunk in response.stream.transform(const Utf8Decoder()).transform(const LineSplitter())) {
+        await onStart();
+        final data = chunk.trim().split('data: ');
+        if (data[0] != '[DONE]') {
+          final response = TextCompletionResponse.fromJson(
+            json.decode(data[0]),
+          );
+          if (response.choices.isNotEmpty) {
+            final text = response.choices.first.text;
+            if (text == previousSyntax && text == '\n') {
+              continue;
+            }
+            await onProcess(response);
+            previousSyntax = response.choices.first.text;
+          }
+        } else {
+          await onEnd();
+        }
+      }
+    }
+    return;
+  }
+}

+ 1 - 0
frontend/appflowy_flutter/lib/core/helpers/helpers.dart

@@ -1 +1,2 @@
 export 'target_platform.dart';
+export 'url_validator.dart';

+ 21 - 0
frontend/appflowy_flutter/lib/core/helpers/url_validator.dart

@@ -0,0 +1,21 @@
+import 'package:dartz/dartz.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+part 'url_validator.freezed.dart';
+
+Either<UriFailure, Uri> parseValidUrl(String url) {
+  try {
+    final uri = Uri.parse(url);
+    if (uri.scheme.isEmpty || uri.host.isEmpty) {
+      return left(const UriFailure.invalidSchemeHost());
+    }
+    return right(uri);
+  } on FormatException {
+    return left(const UriFailure.invalidUriFormat());
+  }
+}
+
+@freezed
+class UriFailure with _$UriFailure {
+  const factory UriFailure.invalidSchemeHost() = _InvalidSchemeHost;
+  const factory UriFailure.invalidUriFormat() = _InvalidUriFormat;
+}

+ 2 - 6
frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart

@@ -77,10 +77,7 @@ class CellController<T, D> extends Equatable {
     _cellListener?.start(
       onCellChanged: (result) {
         result.fold(
-          (_) {
-            _cellCache.remove(_cacheKey);
-            _loadData();
-          },
+          (_) => _loadData(),
           (err) => Log.error(err),
         );
       },
@@ -174,8 +171,8 @@ class CellController<T, D> extends Equatable {
 
   void _loadData() {
     _saveDataOperation?.cancel();
-
     _loadDataOperation?.cancel();
+
     _loadDataOperation = Timer(const Duration(milliseconds: 10), () {
       _cellDataLoader.loadData().then((data) {
         if (data != null) {
@@ -183,7 +180,6 @@ class CellController<T, D> extends Equatable {
         } else {
           _cellCache.remove(_cacheKey);
         }
-
         _cellDataNotifier?.value = data;
       });
     });

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller_builder.dart

@@ -55,7 +55,7 @@ class CellControllerBuilder {
       case FieldType.Number:
         final cellDataLoader = CellDataLoader(
           cellId: _cellId,
-          parser: StringCellDataParser(),
+          parser: NumberCellDataParser(),
           reloadOnFieldChanged: true,
         );
         return NumberCellController(

+ 13 - 1
frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_loader.dart

@@ -27,7 +27,12 @@ class CellDataLoader<T> {
       (result) => result.fold(
         (CellPB cell) {
           try {
-            return parser.parserData(cell.data);
+            // Return null the data of the cell is empty.
+            if (cell.data.isEmpty) {
+              return null;
+            } else {
+              return parser.parserData(cell.data);
+            }
           } catch (e, s) {
             Log.error('$parser parser cellData failed, $e');
             Log.error('Stack trace \n $s');
@@ -51,6 +56,13 @@ class StringCellDataParser implements CellDataParser<String> {
   }
 }
 
+class NumberCellDataParser implements CellDataParser<String> {
+  @override
+  String? parserData(List<int> data) {
+    return utf8.decode(data);
+  }
+}
+
 class DateCellDataParser implements CellDataParser<DateCellDataPB> {
   @override
   DateCellDataPB? parserData(List<int> data) {

+ 6 - 1
frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_persistence.dart

@@ -45,7 +45,12 @@ class DateCellDataPersistence implements CellDataPersistence<DateCellData> {
   Future<Option<FlowyError>> save(DateCellData data) {
     var payload = DateChangesetPB.create()..cellPath = _makeCellPath(cellId);
 
-    final date = (data.date.millisecondsSinceEpoch ~/ 1000).toString();
+    // This is a bit of a hack. This converts the data.date which is in
+    // UTC to Local but actually changes the timestamp instead of just
+    // changing the isUtc flag
+    final dateTime = DateTime(data.date.year, data.date.month, data.date.day);
+
+    final date = (dateTime.millisecondsSinceEpoch ~/ 1000).toString();
     payload.date = date;
     payload.isUtc = data.date.isUtc;
     payload.includeTime = data.includeTime;

+ 9 - 3
frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart

@@ -116,7 +116,7 @@ class DatabaseController {
     }
   }
 
-  void addListener({
+  void setListener({
     DatabaseCallbacks? onDatabaseChanged,
     LayoutCallbacks? onLayoutChanged,
     GroupCallbacks? onGroupChanged,
@@ -212,6 +212,11 @@ class DatabaseController {
     await _databaseViewBackendSvc.closeView();
     await fieldController.dispose();
     await groupListener.stop();
+    await _viewCache.dispose();
+    _databaseCallbacks = null;
+    _groupCallbacks = null;
+    _layoutCallbacks = null;
+    _calendarLayoutCallbacks = null;
   }
 
   Future<void> _loadGroups() async {
@@ -252,7 +257,7 @@ class DatabaseController {
         _databaseCallbacks?.onRowsCreated?.call(ids);
       },
     );
-    _viewCache.addListener(callbacks);
+    _viewCache.setListener(callbacks);
   }
 
   void _listenOnFieldsChanged() {
@@ -337,9 +342,10 @@ class RowDataBuilder {
     _cellDataByFieldId[fieldInfo.field.id] = num.toString();
   }
 
+  /// The date should use the UTC timezone. Becuase the backend uses UTC timezone to format the time string.
   void insertDate(FieldInfo fieldInfo, DateTime date) {
     assert(fieldInfo.fieldType == FieldType.DateTime);
-    final timestamp = (date.millisecondsSinceEpoch ~/ 1000);
+    final timestamp = (date.toUtc().millisecondsSinceEpoch ~/ 1000);
     _cellDataByFieldId[fieldInfo.field.id] = timestamp.toString();
   }
 

+ 2 - 1
frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart

@@ -112,9 +112,10 @@ class DatabaseViewCache {
   Future<void> dispose() async {
     await _databaseViewListener.stop();
     await _rowCache.dispose();
+    _callbacks = null;
   }
 
-  void addListener(DatabaseViewCallbacks callbacks) {
+  void setListener(DatabaseViewCallbacks callbacks) {
     _callbacks = callbacks;
   }
 }

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart

@@ -237,7 +237,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
       },
     );
 
-    _databaseController.addListener(
+    _databaseController.setListener(
       onDatabaseChanged: onDatabaseChanged,
       onGroupChanged: onGroupChanged,
     );

+ 5 - 5
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart

@@ -78,7 +78,7 @@ class BoardContent extends StatefulWidget {
 
 class _BoardContentState extends State<BoardContent> {
   late AppFlowyBoardScrollController scrollManager;
-  final cardConfiguration = CardConfiguration<String>();
+  final renderHook = RowCardRenderHook<String>();
 
   final config = const AppFlowyBoardConfig(
     groupBackgroundColor: Color(0xffF7F8FC),
@@ -87,7 +87,7 @@ class _BoardContentState extends State<BoardContent> {
   @override
   void initState() {
     scrollManager = AppFlowyBoardScrollController();
-    cardConfiguration.addSelectOptionHook((options, groupId) {
+    renderHook.addSelectOptionHook((options, groupId, _) {
       // The cell should hide if the option id is equal to the groupId.
       final isInGroup =
           options.where((element) => element.id == groupId).isNotEmpty;
@@ -254,15 +254,15 @@ class _BoardContentState extends State<BoardContent> {
       key: ValueKey(groupItemId),
       margin: config.cardPadding,
       decoration: _makeBoxDecoration(context),
-      child: Card<String>(
+      child: RowCard<String>(
         row: rowPB,
         viewId: viewId,
         rowCache: rowCache,
         cardData: groupData.group.groupId,
-        fieldId: groupItem.fieldInfo.id,
+        groupingFieldId: groupItem.fieldInfo.id,
         isEditing: isEditing,
         cellBuilder: cellBuilder,
-        configuration: cardConfiguration,
+        renderHook: renderHook,
         openCard: (context) => _openCard(
           viewId,
           fieldController,

+ 107 - 51
frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart

@@ -47,7 +47,13 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
             emit(state.copyWith(database: Some(database)));
           },
           didLoadAllEvents: (events) {
-            emit(state.copyWith(initialEvents: events, allEvents: events));
+            final calenderEvents = _calendarEventDataFromEventPBs(events);
+            emit(
+              state.copyWith(
+                initialEvents: calenderEvents,
+                allEvents: calenderEvents,
+              ),
+            );
           },
           didReceiveNewLayoutField: (CalendarLayoutSettingPB layoutSettings) {
             _loadAllEvents();
@@ -56,6 +62,11 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
           createEvent: (DateTime date, String title) async {
             await _createEvent(date, title);
           },
+          didCreateEvent: (CalendarEventData<CalendarDayEvent> event) {
+            emit(
+              state.copyWith(editEvent: event),
+            );
+          },
           updateCalendarLayoutSetting:
               (CalendarLayoutSettingPB layoutSetting) async {
             await _updateCalendarLayoutSetting(layoutSetting);
@@ -63,7 +74,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
           didUpdateEvent: (CalendarEventData<CalendarDayEvent> eventData) {
             var allEvents = [...state.allEvents];
             final index = allEvents.indexWhere(
-              (element) => element.event!.cellId == eventData.event!.cellId,
+              (element) => element.event!.eventId == eventData.event!.eventId,
             );
             if (index != -1) {
               allEvents[index] = eventData;
@@ -71,22 +82,13 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
             emit(
               state.copyWith(
                 allEvents: allEvents,
-                updateEvent: eventData,
-              ),
-            );
-          },
-          didReceiveNewEvent: (CalendarEventData<CalendarDayEvent> event) {
-            emit(
-              state.copyWith(
-                allEvents: [...state.allEvents, event],
-                newEvent: event,
               ),
             );
           },
           didDeleteEvents: (List<RowId> deletedRowIds) {
             var events = [...state.allEvents];
             events.retainWhere(
-              (element) => !deletedRowIds.contains(element.event!.cellId.rowId),
+              (element) => !deletedRowIds.contains(element.event!.eventId),
             );
             emit(
               state.copyWith(
@@ -95,11 +97,25 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
               ),
             );
           },
+          didReceiveEvent: (CalendarEventData<CalendarDayEvent> event) {
+            emit(
+              state.copyWith(
+                allEvents: [...state.allEvents, event],
+                newEvent: event,
+              ),
+            );
+          },
         );
       },
     );
   }
 
+  @override
+  Future<void> close() async {
+    await _databaseController.dispose();
+    return super.close();
+  }
+
   FieldInfo? _getCalendarFieldInfo(String fieldId) {
     final fieldInfos = _databaseController.fieldController.fieldInfos;
     final index = fieldInfos.indexWhere(
@@ -143,17 +159,27 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
         final dateField = _getCalendarFieldInfo(settings.fieldId);
         final titleField = _getTitleFieldInfo();
         if (dateField != null && titleField != null) {
-          final result = await _databaseController.createRow(
+          final newRow = await _databaseController.createRow(
             withCells: (builder) {
               builder.insertDate(dateField, date);
               builder.insertText(titleField, title);
             },
+          ).then(
+            (result) => result.fold(
+              (newRow) => newRow,
+              (err) {
+                Log.error(err);
+                return null;
+              },
+            ),
           );
 
-          return result.fold(
-            (newRow) {},
-            (err) => Log.error(err),
-          );
+          if (newRow != null) {
+            final event = await _loadEvent(newRow.id);
+            if (event != null && !isClosed) {
+              add(CalendarEvent.didCreateEvent(event));
+            }
+          }
         }
       },
     );
@@ -187,15 +213,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
       result.fold(
         (events) {
           if (!isClosed) {
-            final calendarEvents = <CalendarEventData<CalendarDayEvent>>[];
-            for (final eventPB in events.items) {
-              final calendarEvent = _calendarEventDataFromEventPB(eventPB);
-              if (calendarEvent != null) {
-                calendarEvents.add(calendarEvent);
-              }
-            }
-
-            add(CalendarEvent.didLoadAllEvents(calendarEvents));
+            add(CalendarEvent.didLoadAllEvents(events.items));
           }
         },
         (r) => Log.error(r),
@@ -203,22 +221,32 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
     });
   }
 
+  List<CalendarEventData<CalendarDayEvent>> _calendarEventDataFromEventPBs(
+    List<CalendarEventPB> eventPBs,
+  ) {
+    final calendarEvents = <CalendarEventData<CalendarDayEvent>>[];
+    for (final eventPB in eventPBs) {
+      final event = _calendarEventDataFromEventPB(eventPB);
+      if (event != null) {
+        calendarEvents.add(event);
+      }
+    }
+    return calendarEvents;
+  }
+
   CalendarEventData<CalendarDayEvent>? _calendarEventDataFromEventPB(
     CalendarEventPB eventPB,
   ) {
-    final fieldInfo = fieldInfoByFieldId[eventPB.titleFieldId];
+    final fieldInfo = fieldInfoByFieldId[eventPB.dateFieldId];
     if (fieldInfo != null) {
-      final cellId = CellIdentifier(
-        viewId: viewId,
-        rowId: eventPB.rowId,
-        fieldInfo: fieldInfo,
-      );
-
       final eventData = CalendarDayEvent(
         event: eventPB,
-        cellId: cellId,
+        eventId: eventPB.rowId,
+        dateFieldId: eventPB.dateFieldId,
       );
 
+      // The timestamp is using UTC in the backend, so we need to convert it
+      // to local time.
       final date = DateTime.fromMillisecondsSinceEpoch(
         eventPB.timestamp.toInt() * 1000,
       );
@@ -243,25 +271,29 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
           for (var fieldInfo in fieldInfos) fieldInfo.field.id: fieldInfo
         };
       },
-      onRowsChanged: ((onRowsChanged, rowByRowId, reason) {}),
-      onRowsCreated: ((ids) async {
-        for (final id in ids) {
+      onRowsCreated: ((rowIds) async {
+        for (final id in rowIds) {
           final event = await _loadEvent(id);
           if (event != null && !isClosed) {
-            add(CalendarEvent.didReceiveNewEvent(event));
+            add(CalendarEvent.didReceiveEvent(event));
           }
         }
       }),
-      onRowsDeleted: (ids) {
+      onRowsDeleted: (rowIds) {
         if (isClosed) return;
-        add(CalendarEvent.didDeleteEvents(ids));
+        add(CalendarEvent.didDeleteEvents(rowIds));
       },
-      onRowsUpdated: (ids) async {
+      onRowsUpdated: (rowIds) async {
         if (isClosed) return;
-        for (final id in ids) {
+        for (final id in rowIds) {
           final event = await _loadEvent(id);
-          if (event != null) {
-            add(CalendarEvent.didUpdateEvent(event));
+          if (event != null && isEventDayChanged(event)) {
+            if (isEventDayChanged(event)) {
+              add(CalendarEvent.didDeleteEvents([id]));
+              add(CalendarEvent.didReceiveEvent(event));
+            } else {
+              add(CalendarEvent.didUpdateEvent(event));
+            }
           }
         }
       },
@@ -276,7 +308,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
       onCalendarLayoutChanged: _didReceiveNewLayoutField,
     );
 
-    _databaseController.addListener(
+    _databaseController.setListener(
       onDatabaseChanged: onDatabaseChanged,
       onLayoutChanged: onLayoutChanged,
       onCalendarLayoutChanged: onCalendarLayoutFieldChanged,
@@ -296,6 +328,19 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
       add(CalendarEvent.didReceiveNewLayoutField(layoutSetting.calendar));
     }
   }
+
+  bool isEventDayChanged(
+    CalendarEventData<CalendarDayEvent> event,
+  ) {
+    final index = state.allEvents.indexWhere(
+      (element) => element.event!.eventId == event.event!.eventId,
+    );
+    if (index != -1) {
+      return state.allEvents[index].date.day != event.date.day;
+    } else {
+      return false;
+    }
+  }
 }
 
 typedef Events = List<CalendarEventData<CalendarDayEvent>>;
@@ -310,7 +355,7 @@ class CalendarEvent with _$CalendarEvent {
   ) = _ReceiveCalendarSettings;
 
   // Called after loading all the current evnets
-  const factory CalendarEvent.didLoadAllEvents(Events events) =
+  const factory CalendarEvent.didLoadAllEvents(List<CalendarEventPB> events) =
       _ReceiveCalendarEvents;
 
   // Called when specific event was updated
@@ -319,10 +364,15 @@ class CalendarEvent with _$CalendarEvent {
   ) = _DidUpdateEvent;
 
   // Called after creating a new event
-  const factory CalendarEvent.didReceiveNewEvent(
+  const factory CalendarEvent.didCreateEvent(
     CalendarEventData<CalendarDayEvent> event,
   ) = _DidReceiveNewEvent;
 
+  // Called when receive a new event
+  const factory CalendarEvent.didReceiveEvent(
+    CalendarEventData<CalendarDayEvent> event,
+  ) = _DidReceiveEvent;
+
   // Called when deleting events
   const factory CalendarEvent.didDeleteEvents(List<RowId> rowIds) =
       _DidDeleteEvents;
@@ -348,11 +398,13 @@ class CalendarEvent with _$CalendarEvent {
 class CalendarState with _$CalendarState {
   const factory CalendarState({
     required Option<DatabasePB> database,
+    // events by row id
     required Events allEvents,
     required Events initialEvents,
+    CalendarEventData<CalendarDayEvent>? editEvent,
     CalendarEventData<CalendarDayEvent>? newEvent,
-    required List<RowId> deleteEventIds,
     CalendarEventData<CalendarDayEvent>? updateEvent,
+    required List<String> deleteEventIds,
     required Option<CalendarLayoutSettingPB> settings,
     required DatabaseLoadingState loadingState,
     required Option<FlowyError> noneOrError,
@@ -389,8 +441,12 @@ class CalendarEditingRow {
 
 class CalendarDayEvent {
   final CalendarEventPB event;
-  final CellIdentifier cellId;
+  final String dateFieldId;
+  final String eventId;
 
-  RowId get eventId => cellId.rowId;
-  CalendarDayEvent({required this.cellId, required this.event});
+  CalendarDayEvent({
+    required this.dateFieldId,
+    required this.eventId,
+    required this.event,
+  });
 }

+ 200 - 108
frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart

@@ -1,7 +1,10 @@
 import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
 import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
+import 'package:appflowy/plugins/database_view/widgets/card/card.dart';
 import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
-import 'package:appflowy/plugins/database_view/widgets/card/cells/text_card_cell.dart';
+import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
+import 'package:appflowy/plugins/database_view/widgets/card/cells/number_card_cell.dart';
+import 'package:appflowy/plugins/database_view/widgets/card/cells/url_card_cell.dart';
 import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
 import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
@@ -10,11 +13,11 @@ import 'package:flowy_infra/image.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';
 import 'package:provider/provider.dart';
 
 import '../../grid/presentation/layout/sizes.dart';
+import '../../widgets/row/cells/select_option_cell/extension.dart';
 import '../application/calendar_bloc.dart';
 
 class CalendarDayCard extends StatelessWidget {
@@ -23,11 +26,10 @@ class CalendarDayCard extends StatelessWidget {
   final bool isInMonth;
   final DateTime date;
   final RowCache _rowCache;
-  final CardCellBuilder _cellBuilder;
   final List<CalendarDayEvent> events;
   final void Function(DateTime) onCreateEvent;
 
-  CalendarDayCard({
+  const CalendarDayCard({
     required this.viewId,
     required this.isToday,
     required this.isInMonth,
@@ -37,7 +39,6 @@ class CalendarDayCard extends StatelessWidget {
     required this.events,
     Key? key,
   })  : _rowCache = rowCache,
-        _cellBuilder = CardCellBuilder(rowCache.cellCache),
         super(key: key);
 
   @override
@@ -49,65 +50,183 @@ class CalendarDayCard extends StatelessWidget {
 
     return ChangeNotifierProvider(
       create: (_) => _CardEnterNotifier(),
-      builder: ((context, child) {
-        final children = events.map((event) {
-          return _DayEventCell(
-            event: event,
-            viewId: viewId,
-            onClick: () => _showRowDetailPage(event, context),
-            child: _cellBuilder.buildCell(
-              cellId: event.cellId,
-              styles: {FieldType.RichText: TextCardCellStyle(10)},
+      builder: (context, child) {
+        Widget? multipleCards;
+        if (events.isNotEmpty) {
+          multipleCards = Flexible(
+            child: ListView.separated(
+              itemBuilder: (BuildContext context, int index) =>
+                  _buildCard(context, events[index]),
+              itemCount: events.length,
+              padding: const EdgeInsets.fromLTRB(8.0, 0, 8.0, 8.0),
+              separatorBuilder: (BuildContext context, int index) =>
+                  VSpace(GridSize.typeOptionSeparatorHeight),
             ),
           );
-        }).toList();
+        }
 
         final child = Column(
           mainAxisSize: MainAxisSize.min,
           children: [
-            Padding(
-              padding: const EdgeInsets.all(8.0),
-              child: _Header(
-                date: date,
-                isInMonth: isInMonth,
-                isToday: isToday,
-                onCreate: () => onCreateEvent(date),
-              ),
+            _Header(
+              date: date,
+              isInMonth: isInMonth,
+              isToday: isToday,
+              onCreate: () => onCreateEvent(date),
             ),
+
+            // Add a separator between the header and the content.
             VSpace(GridSize.typeOptionSeparatorHeight),
-            Flexible(
-              child: ListView.separated(
-                itemBuilder: (BuildContext context, int index) {
-                  return children[index];
-                },
-                itemCount: children.length,
-                padding: const EdgeInsets.symmetric(horizontal: 8.0),
-                separatorBuilder: (BuildContext context, int index) =>
-                    VSpace(GridSize.typeOptionSeparatorHeight),
-              ),
-            ),
+
+            // Use SizedBox instead of ListView if there are no cards.
+            multipleCards ?? const SizedBox(),
           ],
         );
 
         return Container(
           color: backgroundColor,
-          child: MouseRegion(
-            cursor: SystemMouseCursors.click,
-            onEnter: (p) => notifyEnter(context, true),
-            onExit: (p) => notifyEnter(context, false),
-            child: Padding(
-              padding: const EdgeInsets.symmetric(vertical: 8.0),
-              child: child,
+          child: GestureDetector(
+            onDoubleTap: () => onCreateEvent(date),
+            child: MouseRegion(
+              cursor: SystemMouseCursors.basic,
+              onEnter: (p) => notifyEnter(context, true),
+              onExit: (p) => notifyEnter(context, false),
+              child: Padding(
+                padding: const EdgeInsets.only(top: 8.0),
+                child: child,
+              ),
             ),
           ),
         );
-      }),
+      },
+    );
+  }
+
+  GestureDetector _buildCard(BuildContext context, CalendarDayEvent event) {
+    final styles = <FieldType, CardCellStyle>{
+      FieldType.Number: NumberCardCellStyle(10),
+      FieldType.URL: URLCardCellStyle(10),
+    };
+
+    final cellBuilder = CardCellBuilder<String>(
+      _rowCache.cellCache,
+      styles: styles,
+    );
+
+    final rowInfo = _rowCache.getRow(event.eventId);
+    final renderHook = RowCardRenderHook<String>();
+    renderHook.addTextCellHook((cellData, primaryFieldId, _) {
+      if (cellData.isEmpty) {
+        return const SizedBox();
+      }
+      return Align(
+        alignment: Alignment.centerLeft,
+        child: FlowyText.medium(
+          cellData,
+          textAlign: TextAlign.left,
+          fontSize: 11,
+          maxLines: null, // Enable multiple lines
+        ),
+      );
+    });
+
+    renderHook.addDateCellHook((cellData, cardData, _) {
+      return Align(
+        alignment: Alignment.centerLeft,
+        child: Padding(
+          padding: const EdgeInsets.symmetric(vertical: 2),
+          child: Row(
+            mainAxisAlignment: MainAxisAlignment.spaceBetween,
+            children: [
+              Flexible(
+                flex: 3,
+                child: FlowyText.regular(
+                  cellData.date,
+                  fontSize: 10,
+                  color: Theme.of(context).hintColor,
+                  overflow: TextOverflow.ellipsis,
+                ),
+              ),
+              Flexible(
+                child: FlowyText.regular(
+                  cellData.time,
+                  fontSize: 10,
+                  color: Theme.of(context).hintColor,
+                  overflow: TextOverflow.ellipsis,
+                ),
+              )
+            ],
+          ),
+        ),
+      );
+    });
+
+    renderHook.addSelectOptionHook((selectedOptions, cardData, _) {
+      final children = selectedOptions.map(
+        (option) {
+          return SelectOptionTag.fromOption(
+            context: context,
+            option: option,
+          );
+        },
+      ).toList();
+
+      return IntrinsicHeight(
+        child: Padding(
+          padding: const EdgeInsets.symmetric(vertical: 2),
+          child: SizedBox.expand(
+            child: Wrap(spacing: 4, runSpacing: 4, children: children),
+          ),
+        ),
+      );
+    });
+
+    // renderHook.addDateFieldHook((cellData, cardData) {
+
+    final card = RowCard<String>(
+      // Add the key here to make sure the card is rebuilt when the cells
+      // in this row are updated.
+      key: ValueKey(event.eventId),
+      row: rowInfo!.rowPB,
+      viewId: viewId,
+      rowCache: _rowCache,
+      cardData: event.dateFieldId,
+      isEditing: false,
+      cellBuilder: cellBuilder,
+      openCard: (context) => _showRowDetailPage(event, context),
+      styleConfiguration: const RowCardStyleConfiguration(
+        showAccessory: false,
+        cellPadding: EdgeInsets.zero,
+      ),
+      renderHook: renderHook,
+      onStartEditing: () {},
+      onEndEditing: () {},
+    );
+
+    return GestureDetector(
+      onTap: () => _showRowDetailPage(event, context),
+      child: MouseRegion(
+        cursor: SystemMouseCursors.click,
+        child: Container(
+          padding: const EdgeInsets.symmetric(horizontal: 2),
+          decoration: BoxDecoration(
+            border: Border.fromBorderSide(
+              BorderSide(
+                color: Theme.of(context).dividerColor,
+                width: 1.5,
+              ),
+            ),
+            borderRadius: Corners.s6Border,
+          ),
+          child: card,
+        ),
+      ),
     );
   }
 
   void _showRowDetailPage(CalendarDayEvent event, BuildContext context) {
     final dataController = RowController(
-      rowId: event.cellId.rowId,
+      rowId: event.eventId,
       viewId: viewId,
       rowCache: _rowCache,
     );
@@ -133,42 +252,6 @@ class CalendarDayCard extends StatelessWidget {
   }
 }
 
-class _DayEventCell extends StatelessWidget {
-  final String viewId;
-  final CalendarDayEvent event;
-  final VoidCallback onClick;
-  final Widget child;
-  const _DayEventCell({
-    required this.viewId,
-    required this.event,
-    required this.onClick,
-    required this.child,
-    Key? key,
-  }) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return FlowyHover(
-      child: GestureDetector(
-        onTap: onClick,
-        child: Container(
-          padding: const EdgeInsets.symmetric(horizontal: 8),
-          decoration: BoxDecoration(
-            border: Border.fromBorderSide(
-              BorderSide(
-                color: Theme.of(context).dividerColor,
-                width: 1.0,
-              ),
-            ),
-            borderRadius: Corners.s6Border,
-          ),
-          child: child,
-        ),
-      ),
-    );
-  }
-}
-
 class _Header extends StatelessWidget {
   final bool isToday;
   final bool isInMonth;
@@ -191,12 +274,16 @@ class _Header extends StatelessWidget {
           isInMonth: isInMonth,
           date: date,
         );
-        return Row(
-          children: [
-            if (notifier.onEnter) _NewEventButton(onClick: onCreate),
-            const Spacer(),
-            badge,
-          ],
+
+        return Padding(
+          padding: const EdgeInsets.symmetric(horizontal: 8.0),
+          child: Row(
+            children: [
+              if (notifier.onEnter) _NewEventButton(onClick: onCreate),
+              const Spacer(),
+              badge,
+            ],
+          ),
         );
       },
     );
@@ -215,10 +302,8 @@ class _NewEventButton extends StatelessWidget {
     return FlowyIconButton(
       onPressed: onClick,
       iconPadding: EdgeInsets.zero,
-      icon: svgWidget(
-        "home/add",
-        color: Theme.of(context).iconTheme.color,
-      ),
+      icon: const FlowySvg(name: "home/add"),
+      hoverColor: AFThemeExtension.of(context).lightGreyHover,
       width: 22,
     );
   }
@@ -237,31 +322,38 @@ class _DayBadge extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    Color dayTextColor = Theme.of(context).colorScheme.onSurface;
-    String dayString = date.day == 1
-        ? DateFormat('MMM d', context.locale.toLanguageTag()).format(date)
-        : date.day.toString();
+    Color dayTextColor = Theme.of(context).colorScheme.onBackground;
+    String monthString =
+        DateFormat("MMM ", context.locale.toLanguageTag()).format(date);
+    String dayString = date.day.toString();
 
-    if (isToday) {
-      dayTextColor = Theme.of(context).colorScheme.onPrimary;
-    }
     if (!isInMonth) {
       dayTextColor = Theme.of(context).disabledColor;
     }
+    if (isToday) {
+      dayTextColor = Theme.of(context).colorScheme.onPrimary;
+    }
 
-    Widget day = Container(
-      decoration: BoxDecoration(
-        color: isToday ? Theme.of(context).colorScheme.primary : null,
-        borderRadius: Corners.s6Border,
-      ),
-      padding: GridSize.typeOptionContentInsets,
-      child: FlowyText.medium(
-        dayString,
-        color: dayTextColor,
-      ),
+    return Row(
+      children: [
+        if (date.day == 1) FlowyText.medium(monthString),
+        Container(
+          decoration: BoxDecoration(
+            color: isToday ? Theme.of(context).colorScheme.primary : null,
+            borderRadius: Corners.s6Border,
+          ),
+          width: isToday ? 26 : null,
+          height: isToday ? 26 : null,
+          padding: GridSize.typeOptionContentInsets,
+          child: Center(
+            child: FlowyText.medium(
+              dayString,
+              color: dayTextColor,
+            ),
+          ),
+        ),
+      ],
     );
-
-    return day;
   }
 }
 

+ 37 - 12
frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart

@@ -9,6 +9,9 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
+import '../../application/row/row_data_controller.dart';
+import '../../widgets/row/cell_builder.dart';
+import '../../widgets/row/row_detail.dart';
 import 'calendar_day.dart';
 import 'layout/sizes.dart';
 import 'toolbar/calendar_toolbar.dart';
@@ -70,19 +73,16 @@ class _CalendarPageState extends State<CalendarPage> {
               },
             ),
             BlocListener<CalendarBloc, CalendarState>(
-              listenWhen: (p, c) => p.updateEvent != c.updateEvent,
+              listenWhen: (p, c) => p.editEvent != c.editEvent,
               listener: (context, state) {
-                if (state.updateEvent != null) {
-                  _eventController.removeWhere(
-                    (element) =>
-                        state.updateEvent!.event!.eventId ==
-                        element.event!.eventId,
-                  );
-                  _eventController.add(state.updateEvent!);
+                if (state.editEvent != null) {
+                  _showEditEventPage(state.editEvent!.event!, context);
                 }
               },
             ),
             BlocListener<CalendarBloc, CalendarState>(
+              // Event create by click the + button or double click on the
+              // calendar
               listenWhen: (p, c) => p.newEvent != c.newEvent,
               listener: (context, state) {
                 if (state.newEvent != null) {
@@ -116,7 +116,7 @@ class _CalendarPageState extends State<CalendarPage> {
       child: MonthView(
         key: _calendarState,
         controller: _eventController,
-        cellAspectRatio: .9,
+        cellAspectRatio: .6,
         startDay: _weekdayFromInt(firstDayOfWeek),
         borderColor: Theme.of(context).dividerColor,
         headerBuilder: _headerNavigatorBuilder,
@@ -137,7 +137,7 @@ class _CalendarPageState extends State<CalendarPage> {
         FlowyIconButton(
           width: CalendarSize.navigatorButtonWidth,
           height: CalendarSize.navigatorButtonHeight,
-          icon: svgWidget('home/arrow_left'),
+          icon: const FlowySvg(name: 'home/arrow_left'),
           tooltipText: LocaleKeys.calendar_navigation_previousMonth.tr(),
           hoverColor: AFThemeExtension.of(context).lightGreyHover,
           onPressed: () => _calendarState?.currentState?.previousPage(),
@@ -155,7 +155,7 @@ class _CalendarPageState extends State<CalendarPage> {
         FlowyIconButton(
           width: CalendarSize.navigatorButtonWidth,
           height: CalendarSize.navigatorButtonHeight,
-          icon: svgWidget('home/arrow_right'),
+          icon: const FlowySvg(name: 'home/arrow_right'),
           tooltipText: LocaleKeys.calendar_navigation_nextMonth.tr(),
           hoverColor: AFThemeExtension.of(context).lightGreyHover,
           onPressed: () => _calendarState?.currentState?.nextPage(),
@@ -185,7 +185,12 @@ class _CalendarPageState extends State<CalendarPage> {
     isInMonth,
   ) {
     final events = calenderEvents.map((value) => value.event!).toList();
-
+    // Sort the events by timestamp. Because the database view is not
+    // reserving the order of the events. Reserving the order of the rows/events
+    // is implemnted in the develop branch(WIP). Will be replaced with that.
+    events.sort(
+      (a, b) => a.event.timestamp.compareTo(b.event.timestamp),
+    );
     return CalendarDayCard(
       viewId: widget.view.id,
       isToday: isToday,
@@ -208,4 +213,24 @@ class _CalendarPageState extends State<CalendarPage> {
     // MonthView places the first day of week on the second column for some reason.
     return WeekDays.values[(dayOfWeek + 1) % 7];
   }
+
+  void _showEditEventPage(CalendarDayEvent event, BuildContext context) {
+    final dataController = RowController(
+      rowId: event.eventId,
+      viewId: widget.view.id,
+      rowCache: _calendarBloc.rowCache,
+    );
+
+    FlowyOverlay.show(
+      context: context,
+      builder: (BuildContext context) {
+        return RowDetailPage(
+          cellBuilder: GridCellBuilder(
+            cellCache: _calendarBloc.rowCache.cellCache,
+          ),
+          dataController: dataController,
+        );
+      },
+    );
+  }
 }

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart

@@ -87,7 +87,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
         }
       },
     );
-    databaseController.addListener(onDatabaseChanged: onDatabaseChanged);
+    databaseController.setListener(onDatabaseChanged: onDatabaseChanged);
   }
 
   Future<void> _openGrid(Emitter<GridState> emit) async {

+ 18 - 7
frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart

@@ -1,3 +1,4 @@
+import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
@@ -7,31 +8,39 @@ import '../../../application/row/row_data_controller.dart';
 part 'row_detail_bloc.freezed.dart';
 
 class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
+  final RowBackendService rowService;
   final RowController dataController;
 
   RowDetailBloc({
     required this.dataController,
-  }) : super(RowDetailState.initial()) {
+  })  : rowService = RowBackendService(viewId: dataController.viewId),
+        super(RowDetailState.initial()) {
     on<RowDetailEvent>(
       (event, emit) async {
-        await event.map(
-          initial: (_Initial value) async {
+        await event.when(
+          initial: () async {
             await _startListening();
             final cells = dataController.loadData();
             if (!isClosed) {
               add(RowDetailEvent.didReceiveCellDatas(cells.values.toList()));
             }
           },
-          didReceiveCellDatas: (_DidReceiveCellDatas value) {
-            emit(state.copyWith(gridCells: value.gridCells));
+          didReceiveCellDatas: (cells) {
+            emit(state.copyWith(gridCells: cells));
           },
-          deleteField: (_DeleteField value) {
+          deleteField: (fieldId) {
             final fieldService = FieldBackendService(
               viewId: dataController.viewId,
-              fieldId: value.fieldId,
+              fieldId: fieldId,
             );
             fieldService.deleteField();
           },
+          deleteRow: (rowId) async {
+            await rowService.deleteRow(rowId);
+          },
+          duplicateRow: (String rowId) async {
+            await rowService.duplicateRow(rowId);
+          },
         );
       },
     );
@@ -58,6 +67,8 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
 class RowDetailEvent with _$RowDetailEvent {
   const factory RowDetailEvent.initial() = _Initial;
   const factory RowDetailEvent.deleteField(String fieldId) = _DeleteField;
+  const factory RowDetailEvent.deleteRow(String rowId) = _DeleteRow;
+  const factory RowDetailEvent.duplicateRow(String rowId) = _DuplicateRow;
   const factory RowDetailEvent.didReceiveCellDatas(
     List<CellIdentifier> gridCells,
   ) = _DidReceiveCellDatas;

+ 1 - 2
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_editor.dart

@@ -147,8 +147,6 @@ class _FieldNameTextFieldState extends State<_FieldNameTextField> {
     widget.popoverMutex.listenOnPopoverChanged(() {
       if (focusNode.hasFocus) {
         focusNode.unfocus();
-      } else {
-        focusNode.requestFocus();
       }
     });
 
@@ -205,6 +203,7 @@ class _DeleteFieldButton extends StatelessWidget {
       builder: (context, state) {
         final enable = !state.canDelete && !state.isGroupField;
         Widget button = FlowyButton(
+          disable: !enable,
           text: FlowyText.medium(
             LocaleKeys.grid_field_delete.tr(),
             color: enable ? null : Theme.of(context).disabledColor,

+ 2 - 6
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_list.dart

@@ -1,6 +1,5 @@
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:flowy_infra/image.dart';
-import 'package:flowy_infra/theme_extension.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
 import 'package:flutter/material.dart';
@@ -58,15 +57,12 @@ class FieldTypeCell extends StatelessWidget {
     return SizedBox(
       height: GridSize.popoverItemHeight,
       child: FlowyButton(
-        hoverColor: AFThemeExtension.of(context).lightGreyHover,
         text: FlowyText.medium(
           fieldType.title(),
-          color: AFThemeExtension.of(context).textColor,
         ),
         onTap: () => onSelectField(fieldType),
-        leftIcon: svgWidget(
-          fieldType.iconName(),
-          color: Theme.of(context).iconTheme.color,
+        leftIcon: FlowySvg(
+          name: fieldType.iconName(),
         ),
       ),
     );

+ 2 - 11
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart

@@ -4,7 +4,6 @@ import 'package:appflowy/plugins/database_view/application/field/type_option/typ
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:dartz/dartz.dart' show Either;
 import 'package:flowy_infra/image.dart';
-import 'package:flowy_infra/theme_extension.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
@@ -113,20 +112,12 @@ class _SwitchFieldButton extends StatelessWidget {
   Widget _buildMoreButton(BuildContext context) {
     final bloc = context.read<FieldTypeOptionEditBloc>();
     return FlowyButton(
-      hoverColor: AFThemeExtension.of(context).lightGreyHover,
       text: FlowyText.medium(
         bloc.state.field.fieldType.title(),
-        color: AFThemeExtension.of(context).textColor,
       ),
       margin: GridSize.typeOptionContentInsets,
-      leftIcon: svgWidget(
-        bloc.state.field.fieldType.iconName(),
-        color: Theme.of(context).iconTheme.color,
-      ),
-      rightIcon: svgWidget(
-        "grid/more",
-        color: Theme.of(context).iconTheme.color,
-      ),
+      leftIcon: FlowySvg(name: bloc.state.field.fieldType.iconName()),
+      rightIcon: const FlowySvg(name: 'grid/more'),
     );
   }
 }

+ 1 - 0
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart

@@ -186,6 +186,7 @@ class CreateFieldButton extends StatelessWidget {
     return AppFlowyPopover(
       direction: PopoverDirection.bottomWithRightAligned,
       asBarrier: true,
+      margin: EdgeInsets.zero,
       constraints: BoxConstraints.loose(const Size(240, 600)),
       child: FlowyButton(
         radius: BorderRadius.zero,

+ 2 - 43
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart

@@ -1,7 +1,5 @@
 import 'package:appflowy/plugins/database_view/application/field/type_option/date_bloc.dart';
 import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
-import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
-import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart';
 import 'package:easy_localization/easy_localization.dart' hide DateFormat;
 import 'package:appflowy/generated/locale_keys.g.dart';
@@ -54,7 +52,6 @@ class DateTypeOptionWidget extends TypeOptionWidget {
             const TypeOptionSeparator(),
             _renderDateFormatButton(context, state.typeOption.dateFormat),
             _renderTimeFormatButton(context, state.typeOption.timeFormat),
-            const _IncludeTimeButton(),
           ];
 
           return ListView.separated(
@@ -191,44 +188,6 @@ class TimeFormatButton extends StatelessWidget {
   }
 }
 
-class _IncludeTimeButton extends StatelessWidget {
-  const _IncludeTimeButton({Key? key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return BlocSelector<DateTypeOptionBloc, DateTypeOptionState, bool>(
-      selector: (state) => state.typeOption.includeTime,
-      builder: (context, includeTime) {
-        return Padding(
-          padding: const EdgeInsets.symmetric(horizontal: 12.0),
-          child: SizedBox(
-            height: GridSize.popoverItemHeight,
-            child: Padding(
-              padding: GridSize.typeOptionContentInsets,
-              child: Row(
-                children: [
-                  FlowyText.medium(LocaleKeys.grid_field_includeTime.tr()),
-                  const Spacer(),
-                  Toggle(
-                    value: includeTime,
-                    onChanged: (value) {
-                      context
-                          .read<DateTypeOptionBloc>()
-                          .add(DateTypeOptionEvent.includeTime(!value));
-                    },
-                    style: ToggleStyle.big,
-                    padding: EdgeInsets.zero,
-                  ),
-                ],
-              ),
-            ),
-          ),
-        );
-      },
-    );
-  }
-}
-
 class DateFormatList extends StatelessWidget {
   final DateFormatPB selectedFormat;
   final Function(DateFormatPB format) onSelected;
@@ -280,7 +239,7 @@ class DateFormatCell extends StatelessWidget {
   Widget build(BuildContext context) {
     Widget? checkmark;
     if (isSelected) {
-      checkmark = svgWidget("grid/checkmark");
+      checkmark = const FlowySvg(name: 'grid/checkmark');
     }
 
     return SizedBox(
@@ -364,7 +323,7 @@ class TimeFormatCell extends StatelessWidget {
   Widget build(BuildContext context) {
     Widget? checkmark;
     if (isSelected) {
-      checkmark = svgWidget("grid/checkmark");
+      checkmark = const FlowySvg(name: 'grid/checkmark');
     }
 
     return SizedBox(

+ 4 - 9
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/number.dart

@@ -4,7 +4,6 @@ import 'package:appflowy/plugins/database_view/application/field/type_option/typ
 import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pbenum.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:flowy_infra/image.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';
@@ -60,15 +59,10 @@ class NumberTypeOptionWidget extends TypeOptionWidget {
           final selectNumUnitButton = SizedBox(
             height: GridSize.popoverItemHeight,
             child: FlowyButton(
-              hoverColor: AFThemeExtension.of(context).lightGreyHover,
               margin: GridSize.typeOptionContentInsets,
-              rightIcon: svgWidget(
-                "grid/more",
-                color: AFThemeExtension.of(context).textColor,
-              ),
+              rightIcon: const FlowySvg(name: 'grid/more'),
               text: FlowyText.regular(
                 state.typeOption.format.title(),
-                color: AFThemeExtension.of(context).textColor,
               ),
             ),
           );
@@ -79,7 +73,6 @@ class NumberTypeOptionWidget extends TypeOptionWidget {
             alignment: Alignment.centerLeft,
             child: FlowyText.medium(
               LocaleKeys.grid_field_numberFormat.tr(),
-              color: AFThemeExtension.of(context).textColor,
             ),
           );
           return Padding(
@@ -188,7 +181,9 @@ class NumberFormatCell extends StatelessWidget {
   Widget build(BuildContext context) {
     Widget? checkmark;
     if (isSelected) {
-      checkmark = svgWidget("grid/checkmark");
+      checkmark = const FlowySvg(
+        name: 'grid/checkmark',
+      );
     }
 
     return SizedBox(

+ 6 - 7
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/select_option_editor.dart

@@ -105,15 +105,10 @@ class _DeleteTag extends StatelessWidget {
     return SizedBox(
       height: GridSize.popoverItemHeight,
       child: FlowyButton(
-        hoverColor: AFThemeExtension.of(context).lightGreyHover,
         text: FlowyText.medium(
           LocaleKeys.grid_selectOption_deleteTag.tr(),
-          color: AFThemeExtension.of(context).textColor,
-        ),
-        leftIcon: svgWidget(
-          "grid/delete",
-          color: Theme.of(context).iconTheme.color,
         ),
+        leftIcon: const FlowySvg(name: 'grid/delete'),
         onTap: () {
           context
               .read<EditSelectOptionBloc>()
@@ -226,7 +221,11 @@ class _SelectOptionColorCell extends StatelessWidget {
     return SizedBox(
       height: GridSize.popoverItemHeight,
       child: FlowyButton(
-        text: FlowyText.medium(color.optionName()),
+        hoverColor: AFThemeExtension.of(context).lightGreyHover,
+        text: FlowyText.medium(
+          color.optionName(),
+          color: AFThemeExtension.of(context).textColor,
+        ),
         leftIcon: colorIcon,
         rightIcon: checkmark,
         onTap: () {

+ 67 - 28
frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart

@@ -1,3 +1,4 @@
+import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
 import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
 import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/action.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
@@ -13,23 +14,40 @@ import 'card_cell_builder.dart';
 import 'container/accessory.dart';
 import 'container/card_container.dart';
 
-class Card<CustomCardData> extends StatefulWidget {
+/// Edit a database row with card style widget
+class RowCard<CustomCardData> extends StatefulWidget {
   final RowPB row;
   final String viewId;
-  final String fieldId;
+  final String? groupingFieldId;
+
+  /// Allows passing a custom card data object to the card. The card will be
+  /// returned in the [CardCellBuilder] and can be used to build the card.
   final CustomCardData? cardData;
   final bool isEditing;
   final RowCache rowCache;
+
+  /// The [CardCellBuilder] is used to build the card cells.
   final CardCellBuilder<CustomCardData> cellBuilder;
+
+  /// Called when the user taps on the card.
   final void Function(BuildContext) openCard;
+
+  /// Called when the user starts editing the card.
   final VoidCallback onStartEditing;
+
+  /// Called when the user ends editing the card.
   final VoidCallback onEndEditing;
-  final CardConfiguration<CustomCardData>? configuration;
 
-  const Card({
+  /// The [RowCardRenderHook] is used to render the card's cell. Other than
+  /// using the default cell builder. For example the [SelectOptionCardCell]
+  final RowCardRenderHook<CustomCardData>? renderHook;
+
+  final RowCardStyleConfiguration styleConfiguration;
+
+  const RowCard({
     required this.row,
     required this.viewId,
-    required this.fieldId,
+    this.groupingFieldId,
     required this.isEditing,
     required this.rowCache,
     required this.cellBuilder,
@@ -37,15 +55,19 @@ class Card<CustomCardData> extends StatefulWidget {
     required this.onStartEditing,
     required this.onEndEditing,
     this.cardData,
-    this.configuration,
+    this.styleConfiguration = const RowCardStyleConfiguration(
+      showAccessory: true,
+    ),
+    this.renderHook,
     Key? key,
   }) : super(key: key);
 
   @override
-  State<Card<CustomCardData>> createState() => _CardState<CustomCardData>();
+  State<RowCard<CustomCardData>> createState() =>
+      _RowCardState<CustomCardData>();
 }
 
-class _CardState<T> extends State<Card<T>> {
+class _RowCardState<T> extends State<RowCard<T>> {
   late CardBloc _cardBloc;
   late EditableRowNotifier rowNotifier;
   late PopoverController popoverController;
@@ -56,15 +78,15 @@ class _CardState<T> extends State<Card<T>> {
     rowNotifier = EditableRowNotifier(isEditing: widget.isEditing);
     _cardBloc = CardBloc(
       viewId: widget.viewId,
-      groupFieldId: widget.fieldId,
+      groupFieldId: widget.groupingFieldId,
       isEditing: widget.isEditing,
       row: widget.row,
       rowCache: widget.rowCache,
-    )..add(const BoardCardEvent.initial());
+    )..add(const RowCardEvent.initial());
 
     rowNotifier.isEditing.addListener(() {
       if (!mounted) return;
-      _cardBloc.add(BoardCardEvent.setIsEditing(rowNotifier.isEditing.value));
+      _cardBloc.add(RowCardEvent.setIsEditing(rowNotifier.isEditing.value));
 
       if (rowNotifier.isEditing.value) {
         widget.onStartEditing();
@@ -81,7 +103,7 @@ class _CardState<T> extends State<Card<T>> {
   Widget build(BuildContext context) {
     return BlocProvider.value(
       value: _cardBloc,
-      child: BlocBuilder<CardBloc, BoardCardState>(
+      child: BlocBuilder<CardBloc, RowCardState>(
         buildWhen: (previous, current) {
           // Rebuild when:
           // 1.If the length of the cells is not the same
@@ -106,21 +128,26 @@ class _CardState<T> extends State<Card<T>> {
               context,
               popoverContext,
             ),
-            child: BoardCardContainer(
+            child: RowCardContainer(
               buildAccessoryWhen: () => state.isEditing == false,
               accessoryBuilder: (context) {
-                return [
-                  _CardEditOption(rowNotifier: rowNotifier),
-                  _CardMoreOption(),
-                ];
+                if (widget.styleConfiguration.showAccessory == false) {
+                  return [];
+                } else {
+                  return [
+                    _CardEditOption(rowNotifier: rowNotifier),
+                    _CardMoreOption(),
+                  ];
+                }
               },
               openAccessory: _handleOpenAccessory,
               openCard: (context) => widget.openCard(context),
               child: _CardContent<T>(
                 rowNotifier: rowNotifier,
                 cellBuilder: widget.cellBuilder,
+                styleConfiguration: widget.styleConfiguration,
                 cells: state.cells,
-                cardConfiguration: widget.configuration,
+                renderHook: widget.renderHook,
                 cardData: widget.cardData,
               ),
             ),
@@ -166,15 +193,17 @@ class _CardState<T> extends State<Card<T>> {
 class _CardContent<CustomCardData> extends StatelessWidget {
   final CardCellBuilder<CustomCardData> cellBuilder;
   final EditableRowNotifier rowNotifier;
-  final List<BoardCellEquatable> cells;
-  final CardConfiguration<CustomCardData>? cardConfiguration;
+  final List<CellIdentifier> cells;
+  final RowCardRenderHook<CustomCardData>? renderHook;
   final CustomCardData? cardData;
+  final RowCardStyleConfiguration styleConfiguration;
   const _CardContent({
     required this.rowNotifier,
     required this.cellBuilder,
     required this.cells,
     required this.cardData,
-    this.cardConfiguration,
+    required this.styleConfiguration,
+    this.renderHook,
     Key? key,
   }) : super(key: key);
 
@@ -188,30 +217,30 @@ class _CardContent<CustomCardData> extends StatelessWidget {
 
   List<Widget> _makeCells(
     BuildContext context,
-    List<BoardCellEquatable> cells,
+    List<CellIdentifier> cells,
   ) {
     final List<Widget> children = [];
     // Remove all the cell listeners.
     rowNotifier.unbind();
 
     cells.asMap().forEach(
-      (int index, BoardCellEquatable cell) {
+      (int index, CellIdentifier cell) {
         final isEditing = index == 0 ? rowNotifier.isEditing.value : false;
         final cellNotifier = EditableCardNotifier(isEditing: isEditing);
 
         if (index == 0) {
           // Only use the first cell to receive user's input when click the edit
           // button
-          rowNotifier.bindCell(cell.identifier, cellNotifier);
+          rowNotifier.bindCell(cell, cellNotifier);
         }
 
         final child = Padding(
-          key: cell.identifier.key(),
-          padding: const EdgeInsets.only(left: 4, right: 4),
+          key: cell.key(),
+          padding: styleConfiguration.cellPadding,
           child: cellBuilder.buildCell(
-            cellId: cell.identifier,
+            cellId: cell,
             cellNotifier: cellNotifier,
-            cardConfiguration: cardConfiguration,
+            renderHook: renderHook,
             cardData: cardData,
           ),
         );
@@ -265,3 +294,13 @@ class _CardEditOption extends StatelessWidget with CardAccessory {
   @override
   AccessoryType get type => AccessoryType.edit;
 }
+
+class RowCardStyleConfiguration {
+  final bool showAccessory;
+  final EdgeInsets cellPadding;
+
+  const RowCardStyleConfiguration({
+    this.showAccessory = true,
+    this.cellPadding = const EdgeInsets.only(left: 4, right: 4),
+  });
+}

+ 27 - 40
frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart

@@ -1,5 +1,4 @@
 import 'dart:collection';
-import 'package:equatable/equatable.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
@@ -12,9 +11,9 @@ import '../../application/row/row_service.dart';
 
 part 'card_bloc.freezed.dart';
 
-class CardBloc extends Bloc<BoardCardEvent, BoardCardState> {
+class CardBloc extends Bloc<RowCardEvent, RowCardState> {
   final RowPB row;
-  final String groupFieldId;
+  final String? groupFieldId;
   final RowBackendService _rowBackendSvc;
   final RowCache _rowCache;
   VoidCallback? _rowCallback;
@@ -28,13 +27,13 @@ class CardBloc extends Bloc<BoardCardEvent, BoardCardState> {
   })  : _rowBackendSvc = RowBackendService(viewId: viewId),
         _rowCache = rowCache,
         super(
-          BoardCardState.initial(
+          RowCardState.initial(
             row,
             _makeCells(groupFieldId, rowCache.loadGridCells(row.id)),
             isEditing,
           ),
         ) {
-    on<BoardCardEvent>(
+    on<RowCardEvent>(
       (event, emit) async {
         await event.when(
           initial: () async {
@@ -69,7 +68,7 @@ class CardBloc extends Bloc<BoardCardEvent, BoardCardState> {
     return RowInfo(
       viewId: _rowBackendSvc.viewId,
       fields: UnmodifiableListView(
-        state.cells.map((cell) => cell.identifier.fieldInfo).toList(),
+        state.cells.map((cell) => cell.fieldInfo).toList(),
       ),
       rowPB: state.rowPB,
     );
@@ -81,70 +80,58 @@ class CardBloc extends Bloc<BoardCardEvent, BoardCardState> {
       onCellUpdated: (cellMap, reason) {
         if (!isClosed) {
           final cells = _makeCells(groupFieldId, cellMap);
-          add(BoardCardEvent.didReceiveCells(cells, reason));
+          add(RowCardEvent.didReceiveCells(cells, reason));
         }
       },
     );
   }
 }
 
-List<BoardCellEquatable> _makeCells(
-  String groupFieldId,
+List<CellIdentifier> _makeCells(
+  String? groupFieldId,
   CellByFieldId originalCellMap,
 ) {
-  List<BoardCellEquatable> cells = [];
+  List<CellIdentifier> cells = [];
   for (final entry in originalCellMap.entries) {
     // Filter out the cell if it's fieldId equal to the groupFieldId
-    if (entry.value.fieldId != groupFieldId) {
-      cells.add(BoardCellEquatable(entry.value));
+    if (groupFieldId != null) {
+      if (entry.value.fieldId == groupFieldId) {
+        continue;
+      }
     }
+
+    cells.add(entry.value);
   }
   return cells;
 }
 
 @freezed
-class BoardCardEvent with _$BoardCardEvent {
-  const factory BoardCardEvent.initial() = _InitialRow;
-  const factory BoardCardEvent.setIsEditing(bool isEditing) = _IsEditing;
-  const factory BoardCardEvent.didReceiveCells(
-    List<BoardCellEquatable> cells,
+class RowCardEvent with _$RowCardEvent {
+  const factory RowCardEvent.initial() = _InitialRow;
+  const factory RowCardEvent.setIsEditing(bool isEditing) = _IsEditing;
+  const factory RowCardEvent.didReceiveCells(
+    List<CellIdentifier> cells,
     RowsChangedReason reason,
   ) = _DidReceiveCells;
 }
 
 @freezed
-class BoardCardState with _$BoardCardState {
-  const factory BoardCardState({
+class RowCardState with _$RowCardState {
+  const factory RowCardState({
     required RowPB rowPB,
-    required List<BoardCellEquatable> cells,
+    required List<CellIdentifier> cells,
     required bool isEditing,
     RowsChangedReason? changeReason,
-  }) = _BoardCardState;
+  }) = _RowCardState;
 
-  factory BoardCardState.initial(
+  factory RowCardState.initial(
     RowPB rowPB,
-    List<BoardCellEquatable> cells,
+    List<CellIdentifier> cells,
     bool isEditing,
   ) =>
-      BoardCardState(
+      RowCardState(
         rowPB: rowPB,
         cells: cells,
         isEditing: isEditing,
       );
 }
-
-class BoardCellEquatable extends Equatable {
-  final CellIdentifier identifier;
-
-  const BoardCellEquatable(this.identifier);
-
-  @override
-  List<Object?> get props {
-    return [
-      identifier.fieldInfo.id,
-      identifier.fieldInfo.fieldType,
-      identifier.fieldInfo.visibility,
-      identifier.fieldInfo.width,
-    ];
-  }
-}

+ 15 - 9
frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart

@@ -15,15 +15,15 @@ import 'cells/url_card_cell.dart';
 // T represents as the Generic card data
 class CardCellBuilder<CustomCardData> {
   final CellCache cellCache;
+  final Map<FieldType, CardCellStyle>? styles;
 
-  CardCellBuilder(this.cellCache);
+  CardCellBuilder(this.cellCache, {this.styles});
 
   Widget buildCell({
     CustomCardData? cardData,
     required CellIdentifier cellId,
     EditableCardNotifier? cellNotifier,
-    CardConfiguration<CustomCardData>? cardConfiguration,
-    Map<FieldType, CardCellStyle>? styles,
+    RowCardRenderHook<CustomCardData>? renderHook,
   }) {
     final cellControllerBuilder = CellControllerBuilder(
       cellId: cellId,
@@ -39,20 +39,21 @@ class CardCellBuilder<CustomCardData> {
           key: key,
         );
       case FieldType.DateTime:
-        return DateCardCell(
+        return DateCardCell<CustomCardData>(
+          renderHook: renderHook?.renderHook[FieldType.DateTime],
           cellControllerBuilder: cellControllerBuilder,
           key: key,
         );
       case FieldType.SingleSelect:
         return SelectOptionCardCell<CustomCardData>(
-          renderHook: cardConfiguration?.renderHook[FieldType.SingleSelect],
+          renderHook: renderHook?.renderHook[FieldType.SingleSelect],
           cellControllerBuilder: cellControllerBuilder,
           cardData: cardData,
           key: key,
         );
       case FieldType.MultiSelect:
         return SelectOptionCardCell<CustomCardData>(
-          renderHook: cardConfiguration?.renderHook[FieldType.MultiSelect],
+          renderHook: renderHook?.renderHook[FieldType.MultiSelect],
           cellControllerBuilder: cellControllerBuilder,
           cardData: cardData,
           editableNotifier: cellNotifier,
@@ -64,19 +65,24 @@ class CardCellBuilder<CustomCardData> {
           key: key,
         );
       case FieldType.Number:
-        return NumberCardCell(
+        return NumberCardCell<CustomCardData>(
+          renderHook: renderHook?.renderHook[FieldType.Number],
+          style: isStyleOrNull<NumberCardCellStyle>(style),
           cellControllerBuilder: cellControllerBuilder,
           key: key,
         );
       case FieldType.RichText:
-        return TextCardCell(
+        return TextCardCell<CustomCardData>(
+          renderHook: renderHook?.renderHook[FieldType.RichText],
           cellControllerBuilder: cellControllerBuilder,
           editableNotifier: cellNotifier,
+          cardData: cardData,
           style: isStyleOrNull<TextCardCellStyle>(style),
           key: key,
         );
       case FieldType.URL:
-        return URLCardCell(
+        return URLCardCell<CustomCardData>(
+          style: isStyleOrNull<URLCardCellStyle>(style),
           cellControllerBuilder: cellControllerBuilder,
           key: key,
         );

+ 54 - 9
frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart

@@ -1,27 +1,72 @@
 import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
 import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
+import 'package:appflowy_backend/log.dart';
 import 'package:flutter/material.dart';
 
-typedef CellRenderHook<C, T> = Widget? Function(C cellData, T cardData);
+typedef CellRenderHook<C, CustomCardData> = Widget? Function(
+  C cellData,
+  CustomCardData cardData,
+  BuildContext buildContext,
+);
 typedef RenderHookByFieldType<C> = Map<FieldType, CellRenderHook<dynamic, C>>;
 
-class CardConfiguration<CustomCardData> {
+/// The [RowCardRenderHook] is used to customize the rendering of the
+///  card cell. Each cell has itw own field type. So the [renderHook]
+///  is a map of [FieldType] to [CellRenderHook].
+class RowCardRenderHook<CustomCardData> {
   final RenderHookByFieldType<CustomCardData> renderHook = {};
-  CardConfiguration();
+  RowCardRenderHook();
 
+  /// Add render hook for the FieldType.SingleSelect and FieldType.MultiSelect
   void addSelectOptionHook(
-    CellRenderHook<List<SelectOptionPB>, CustomCardData> hook,
+    CellRenderHook<List<SelectOptionPB>, CustomCardData?> hook,
   ) {
-    selectOptionHook(cellData, cardData) {
-      if (cellData is List<SelectOptionPB>) {
-        hook(cellData, cardData);
+    final hookFn = _typeSafeHook<List<SelectOptionPB>>(hook);
+    renderHook[FieldType.SingleSelect] = hookFn;
+    renderHook[FieldType.MultiSelect] = hookFn;
+  }
+
+  /// Add a render hook for the [FieldType.RichText]
+  void addTextCellHook(
+    CellRenderHook<String, CustomCardData?> hook,
+  ) {
+    renderHook[FieldType.RichText] = _typeSafeHook<String>(hook);
+  }
+
+  /// Add a render hook for the [FieldType.Number]
+  void addNumberCellHook(
+    CellRenderHook<String, CustomCardData?> hook,
+  ) {
+    renderHook[FieldType.Number] = _typeSafeHook<String>(hook);
+  }
+
+  /// Add a render hook for the [FieldType.Date]
+  void addDateCellHook(
+    CellRenderHook<DateCellDataPB, CustomCardData?> hook,
+  ) {
+    renderHook[FieldType.DateTime] = _typeSafeHook<DateCellDataPB>(hook);
+  }
+
+  CellRenderHook<dynamic, CustomCardData> _typeSafeHook<C>(
+    CellRenderHook<C, CustomCardData?> hook,
+  ) {
+    hookFn(cellData, cardData, buildContext) {
+      if (cellData == null) {
+        return null;
+      }
+
+      if (cellData is C) {
+        return hook(cellData, cardData, buildContext);
+      } else {
+        Log.debug("Unexpected cellData type: ${cellData.runtimeType}");
+        return null;
       }
     }
 
-    renderHook[FieldType.SingleSelect] = selectOptionHook;
-    renderHook[FieldType.MultiSelect] = selectOptionHook;
+    return hookFn;
   }
 }
 

+ 10 - 7
frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/checkbox_card_cell.dart

@@ -44,13 +44,16 @@ class _CheckboxCardCellState extends State<CheckboxCardCell> {
               : svgWidget('editor/editor_uncheck');
           return Align(
             alignment: Alignment.centerLeft,
-            child: FlowyIconButton(
-              iconPadding: EdgeInsets.zero,
-              icon: icon,
-              width: 20,
-              onPressed: () => context
-                  .read<CheckboxCardCellBloc>()
-                  .add(const CheckboxCardCellEvent.select()),
+            child: Padding(
+              padding: const EdgeInsets.symmetric(vertical: 2),
+              child: FlowyIconButton(
+                iconPadding: EdgeInsets.zero,
+                icon: icon,
+                width: 20,
+                onPressed: () => context
+                    .read<CheckboxCardCellBloc>()
+                    .add(const CheckboxCardCellEvent.select()),
+              ),
             ),
           );
         },

+ 12 - 1
frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/date_card_cell.dart

@@ -7,11 +7,13 @@ import '../bloc/date_card_cell_bloc.dart';
 import '../define.dart';
 import 'card_cell.dart';
 
-class DateCardCell extends CardCell {
+class DateCardCell<CustomCardData> extends CardCell {
   final CellControllerBuilder cellControllerBuilder;
+  final CellRenderHook<dynamic, CustomCardData>? renderHook;
 
   const DateCardCell({
     required this.cellControllerBuilder,
+    this.renderHook,
     Key? key,
   }) : super(key: key);
 
@@ -42,6 +44,15 @@ class _DateCardCellState extends State<DateCardCell> {
           if (state.dateStr.isEmpty) {
             return const SizedBox();
           } else {
+            Widget? custom = widget.renderHook?.call(
+              state.data,
+              widget.cardData,
+              context,
+            );
+            if (custom != null) {
+              return custom;
+            }
+
             return Align(
               alignment: Alignment.centerLeft,
               child: Padding(

+ 23 - 3
frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/number_card_cell.dart

@@ -7,13 +7,24 @@ import '../bloc/number_card_cell_bloc.dart';
 import '../define.dart';
 import 'card_cell.dart';
 
-class NumberCardCell extends CardCell {
+class NumberCardCellStyle extends CardCellStyle {
+  final double fontSize;
+
+  NumberCardCellStyle(this.fontSize);
+}
+
+class NumberCardCell<CustomCardData>
+    extends CardCell<CustomCardData, NumberCardCellStyle> {
+  final CellRenderHook<String, CustomCardData>? renderHook;
   final CellControllerBuilder cellControllerBuilder;
 
   const NumberCardCell({
     required this.cellControllerBuilder,
+    CustomCardData? cardData,
+    NumberCardCellStyle? style,
+    this.renderHook,
     Key? key,
-  }) : super(key: key);
+  }) : super(key: key, style: style, cardData: cardData);
 
   @override
   State<NumberCardCell> createState() => _NumberCardCellState();
@@ -42,6 +53,15 @@ class _NumberCardCellState extends State<NumberCardCell> {
           if (state.content.isEmpty) {
             return const SizedBox();
           } else {
+            Widget? custom = widget.renderHook?.call(
+              state.content,
+              widget.cardData,
+              context,
+            );
+            if (custom != null) {
+              return custom;
+            }
+
             return Align(
               alignment: Alignment.centerLeft,
               child: Padding(
@@ -50,7 +70,7 @@ class _NumberCardCellState extends State<NumberCardCell> {
                 ),
                 child: FlowyText.medium(
                   state.content,
-                  fontSize: 14,
+                  fontSize: widget.style?.fontSize ?? 14,
                 ),
               ),
             );

+ 5 - 3
frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart

@@ -11,17 +11,18 @@ import 'card_cell.dart';
 
 class SelectOptionCardCellStyle extends CardCellStyle {}
 
-class SelectOptionCardCell<T> extends CardCell<T, SelectOptionCardCellStyle>
+class SelectOptionCardCell<CustomCardData>
+    extends CardCell<CustomCardData, SelectOptionCardCellStyle>
     with EditableCell {
   final CellControllerBuilder cellControllerBuilder;
-  final CellRenderHook<List<SelectOptionPB>, T>? renderHook;
+  final CellRenderHook<List<SelectOptionPB>, CustomCardData>? renderHook;
 
   @override
   final EditableCardNotifier? editableNotifier;
 
   SelectOptionCardCell({
     required this.cellControllerBuilder,
-    required T? cardData,
+    required CustomCardData? cardData,
     this.renderHook,
     this.editableNotifier,
     Key? key,
@@ -57,6 +58,7 @@ class _SelectOptionCardCellState extends State<SelectOptionCardCell> {
           Widget? custom = widget.renderHook?.call(
             state.selectedOptions,
             widget.cardData,
+            context,
           );
           if (custom != null) {
             return custom;

+ 16 - 3
frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart

@@ -14,18 +14,21 @@ class TextCardCellStyle extends CardCellStyle {
   TextCardCellStyle(this.fontSize);
 }
 
-class TextCardCell extends CardCell<String, TextCardCellStyle>
-    with EditableCell {
+class TextCardCell<CustomCardData>
+    extends CardCell<CustomCardData, TextCardCellStyle> with EditableCell {
   @override
   final EditableCardNotifier? editableNotifier;
   final CellControllerBuilder cellControllerBuilder;
+  final CellRenderHook<String, CustomCardData>? renderHook;
 
   const TextCardCell({
     required this.cellControllerBuilder,
+    required CustomCardData? cardData,
     this.editableNotifier,
+    this.renderHook,
     TextCardCellStyle? style,
     Key? key,
-  }) : super(key: key, style: style);
+  }) : super(key: key, style: style, cardData: cardData);
 
   @override
   State<TextCardCell> createState() => _TextCardCellState();
@@ -104,6 +107,16 @@ class _TextCardCellState extends State<TextCardCell> {
             return previous != current;
           },
           builder: (context, state) {
+            // Returns a custom render widget
+            Widget? custom = widget.renderHook?.call(
+              state.content,
+              widget.cardData,
+              context,
+            );
+            if (custom != null) {
+              return custom;
+            }
+
             if (state.content.isEmpty &&
                 state.enableEdit == false &&
                 focusWhenInit == false) {

+ 11 - 3
frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/url_card_cell.dart

@@ -8,13 +8,21 @@ import '../bloc/url_card_cell_bloc.dart';
 import '../define.dart';
 import 'card_cell.dart';
 
-class URLCardCell extends CardCell {
+class URLCardCellStyle extends CardCellStyle {
+  final double fontSize;
+
+  URLCardCellStyle(this.fontSize);
+}
+
+class URLCardCell<CustomCardData>
+    extends CardCell<CustomCardData, URLCardCellStyle> {
   final CellControllerBuilder cellControllerBuilder;
 
   const URLCardCell({
     required this.cellControllerBuilder,
+    URLCardCellStyle? style,
     Key? key,
-  }) : super(key: key);
+  }) : super(key: key, style: style);
 
   @override
   State<URLCardCell> createState() => _URLCardCellState();
@@ -55,7 +63,7 @@ class _URLCardCellState extends State<URLCardCell> {
                     style: Theme.of(context)
                         .textTheme
                         .bodyMedium!
-                        .size(FontSizes.s14)
+                        .size(widget.style?.fontSize ?? FontSizes.s14)
                         .textColor(Theme.of(context).colorScheme.primary)
                         .underline,
                   ),

+ 2 - 2
frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/container/card_container.dart

@@ -4,13 +4,13 @@ import 'package:styled_widget/styled_widget.dart';
 
 import 'accessory.dart';
 
-class BoardCardContainer extends StatelessWidget {
+class RowCardContainer extends StatelessWidget {
   final Widget child;
   final CardAccessoryBuilder? accessoryBuilder;
   final bool Function()? buildAccessoryWhen;
   final void Function(BuildContext) openCard;
   final void Function(AccessoryType) openAccessory;
-  const BoardCardContainer({
+  const RowCardContainer({
     required this.child,
     required this.openCard,
     required this.openAccessory,

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart

@@ -20,7 +20,7 @@ class ChecklistProgressBar extends StatelessWidget {
       percent: percent,
       padding: EdgeInsets.zero,
       progressColor: Theme.of(context).colorScheme.primary,
-      backgroundColor: AFThemeExtension.of(context).progressBarBGcolor,
+      backgroundColor: AFThemeExtension.of(context).progressBarBGColor,
       barRadius: const Radius.circular(5),
     );
   }

+ 1 - 4
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart

@@ -289,10 +289,7 @@ Option<DateCellData> calDataFromCellData(DateCellDataPB? cellData) {
   Option<DateCellData> dateData = none();
   if (cellData != null) {
     final timestamp = cellData.timestamp * 1000;
-    final date = DateTime.fromMillisecondsSinceEpoch(
-      timestamp.toInt(),
-      isUtc: true,
-    );
+    final date = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt());
     dateData = Some(
       DateCellData(
         date: date,

+ 11 - 10
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/number_cell/number_cell_bloc.dart

@@ -1,11 +1,11 @@
 import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
-import 'package:appflowy_backend/log.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
 
 part 'number_cell_bloc.freezed.dart';
 
+//
 class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
   final NumberCellController cellController;
   void Function()? _onCellChangedFn;
@@ -22,17 +22,18 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
           didReceiveCellUpdate: (cellContent) {
             emit(state.copyWith(cellContent: cellContent ?? ""));
           },
-          updateCell: (text) {
+          updateCell: (text) async {
             if (state.cellContent != text) {
               emit(state.copyWith(cellContent: text));
-              cellController.saveCellData(
-                text,
-                onFinish: (result) {
-                  result.fold(
-                    () {},
-                    (err) => Log.error(err),
-                  );
-                },
+              await cellController.saveCellData(text);
+
+              // If the input content is "abc" that can't parsered as number then the data stored in the backend will be an empty string.
+              // So for every cell data that will be formatted in the backend.
+              // It needs to get the formatted data after saving.
+              add(
+                NumberCellEvent.didReceiveCellUpdate(
+                  cellController.getCellData(),
+                ),
               );
             }
           },

+ 7 - 1
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/text_cell/text_cell.dart

@@ -8,9 +8,13 @@ import '../../cell_builder.dart';
 
 class GridTextCellStyle extends GridCellStyle {
   String? placeholder;
+  TextStyle? textStyle;
+  bool? autofocus;
 
   GridTextCellStyle({
     this.placeholder,
+    this.textStyle,
+    this.autofocus,
   });
 }
 
@@ -66,7 +70,9 @@ class _GridTextCellState extends GridFocusNodeCellState<GridTextCell> {
             controller: _controller,
             focusNode: focusNode,
             maxLines: null,
-            style: Theme.of(context).textTheme.bodyMedium,
+            style: widget.cellStyle?.textStyle ??
+                Theme.of(context).textTheme.bodyMedium,
+            autofocus: widget.cellStyle?.autofocus ?? false,
             decoration: InputDecoration(
               contentPadding: EdgeInsets.only(
                 top: GridSize.cellContentInsets.top,

+ 163 - 105
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart

@@ -3,6 +3,7 @@ import 'package:appflowy/plugins/database_view/application/field/type_option/typ
 import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
 import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
 import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
+import 'package:collection/collection.dart';
 import 'package:flowy_infra/theme_extension.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -43,83 +44,85 @@ class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate {
 }
 
 class _RowDetailPageState extends State<RowDetailPage> {
-  final padding = const EdgeInsets.symmetric(
-    horizontal: 40,
-    vertical: 20,
-  );
-
   @override
   Widget build(BuildContext context) {
     return FlowyDialog(
       child: BlocProvider(
         create: (context) {
-          final bloc = RowDetailBloc(
-            dataController: widget.dataController,
-          );
-          bloc.add(const RowDetailEvent.initial());
-          return bloc;
+          return RowDetailBloc(dataController: widget.dataController)
+            ..add(const RowDetailEvent.initial());
         },
-        child: Padding(
-          padding: padding,
-          child: Column(
-            children: [
-              const _Header(),
-              Expanded(
-                child: _PropertyColumn(
-                  cellBuilder: widget.cellBuilder,
-                  viewId: widget.dataController.viewId,
-                ),
-              ),
-            ],
-          ),
+        child: ListView(
+          children: [
+            // using ListView here for future expansion:
+            // - header and cover image
+            // - lower rich text area
+            IntrinsicHeight(child: _responsiveRowInfo()),
+            const Divider(height: 1.0),
+            const SizedBox(height: 10),
+          ],
         ),
       ),
     );
   }
-}
-
-class _Header extends StatelessWidget {
-  const _Header({Key? key}) : super(key: key);
 
-  @override
-  Widget build(BuildContext context) {
-    return SizedBox(
-      height: 30,
-      child: Row(
-        children: const [Spacer(), _CloseButton()],
-      ),
+  Widget _responsiveRowInfo() {
+    final rowDataColumn = _PropertyColumn(
+      cellBuilder: widget.cellBuilder,
+      viewId: widget.dataController.viewId,
     );
-  }
-}
-
-class _CloseButton extends StatelessWidget {
-  const _CloseButton({Key? key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return FlowyIconButton(
-      hoverColor: AFThemeExtension.of(context).lightGreyHover,
-      width: 24,
-      onPressed: () => FlowyOverlay.pop(context),
-      iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2),
-      icon: svgWidget(
-        "home/close",
-        color: Theme.of(context).iconTheme.color,
-      ),
+    final rowOptionColumn = _RowOptionColumn(
+      viewId: widget.dataController.viewId,
+      rowId: widget.dataController.rowId,
     );
+    if (MediaQuery.of(context).size.width > 800) {
+      return Row(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Flexible(
+            flex: 4,
+            child: Padding(
+              padding: const EdgeInsets.fromLTRB(50, 50, 20, 20),
+              child: rowDataColumn,
+            ),
+          ),
+          const VerticalDivider(width: 1.0),
+          Flexible(
+            child: Padding(
+              padding: const EdgeInsets.fromLTRB(20, 50, 20, 20),
+              child: rowOptionColumn,
+            ),
+          ),
+        ],
+      );
+    } else {
+      return Column(
+        crossAxisAlignment: CrossAxisAlignment.stretch,
+        mainAxisSize: MainAxisSize.min,
+        children: [
+          Padding(
+            padding: const EdgeInsets.fromLTRB(20, 50, 20, 20),
+            child: rowDataColumn,
+          ),
+          const Divider(height: 1.0),
+          Padding(
+            padding: const EdgeInsets.all(20),
+            child: rowOptionColumn,
+          )
+        ],
+      );
+    }
   }
 }
 
 class _PropertyColumn extends StatelessWidget {
   final String viewId;
   final GridCellBuilder cellBuilder;
-  final ScrollController _scrollController;
-  _PropertyColumn({
+  const _PropertyColumn({
     required this.viewId,
     required this.cellBuilder,
     Key? key,
-  })  : _scrollController = ScrollController(),
-        super(key: key);
+  }) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
@@ -127,63 +130,61 @@ class _PropertyColumn extends StatelessWidget {
       buildWhen: (previous, current) => previous.gridCells != current.gridCells,
       builder: (context, state) {
         return Column(
+          mainAxisSize: MainAxisSize.min,
+          crossAxisAlignment: CrossAxisAlignment.start,
           children: [
-            Expanded(child: _wrapScrollbar(buildPropertyCells(state))),
-            const VSpace(10),
-            _CreatePropertyButton(
-              viewId: viewId,
-              onClosed: _scrollToNewProperty,
+            _RowTitle(
+              cellId: state.gridCells
+                  .firstWhereOrNull((e) => e.fieldInfo.isPrimary),
+              cellBuilder: cellBuilder,
             ),
+            const VSpace(20),
+            ...state.gridCells
+                .where((element) => !element.fieldInfo.isPrimary)
+                .map(
+                  (cell) => Padding(
+                    padding: const EdgeInsets.only(bottom: 4.0),
+                    child: _PropertyCell(
+                      cellId: cell,
+                      cellBuilder: cellBuilder,
+                    ),
+                  ),
+                )
+                .toList(),
+            const VSpace(20),
+            _CreatePropertyButton(viewId: viewId),
           ],
         );
       },
     );
   }
+}
 
-  Widget buildPropertyCells(RowDetailState state) {
-    return ListView.separated(
-      controller: _scrollController,
-      itemCount: state.gridCells.length,
-      itemBuilder: (BuildContext context, int index) {
-        return _PropertyCell(
-          cellId: state.gridCells[index],
-          cellBuilder: cellBuilder,
-        );
-      },
-      separatorBuilder: (BuildContext context, int index) {
-        return const VSpace(2);
-      },
-    );
-  }
+class _RowTitle extends StatelessWidget {
+  final CellIdentifier? cellId;
+  final GridCellBuilder cellBuilder;
+  const _RowTitle({this.cellId, required this.cellBuilder, Key? key})
+      : super(key: key);
 
-  Widget _wrapScrollbar(Widget child) {
-    return ScrollbarListStack(
-      axis: Axis.vertical,
-      controller: _scrollController,
-      barSize: GridSize.scrollBarSize,
-      autoHideScrollbar: false,
-      child: child,
+  @override
+  Widget build(BuildContext context) {
+    if (cellId == null) {
+      return const SizedBox();
+    }
+    final style = GridTextCellStyle(
+      placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
+      textStyle: Theme.of(context).textTheme.titleLarge,
+      autofocus: true,
     );
-  }
-
-  void _scrollToNewProperty() {
-    WidgetsBinding.instance.addPostFrameCallback((_) {
-      _scrollController.animateTo(
-        _scrollController.position.maxScrollExtent,
-        duration: const Duration(milliseconds: 250),
-        curve: Curves.ease,
-      );
-    });
+    return cellBuilder.build(cellId!, style: style);
   }
 }
 
 class _CreatePropertyButton extends StatefulWidget {
   final String viewId;
-  final VoidCallback onClosed;
 
   const _CreatePropertyButton({
     required this.viewId,
-    required this.onClosed,
     Key? key,
   }) : super(key: key);
 
@@ -206,10 +207,9 @@ class _CreatePropertyButtonState extends State<_CreatePropertyButton> {
       constraints: BoxConstraints.loose(const Size(240, 200)),
       controller: popoverController,
       direction: PopoverDirection.topWithLeftAligned,
-      onClose: widget.onClosed,
-      child: Container(
+      margin: EdgeInsets.zero,
+      child: SizedBox(
         height: 40,
-        decoration: _makeBoxDecoration(context),
         child: FlowyButton(
           text: FlowyText.medium(
             LocaleKeys.grid_field_newProperty.tr(),
@@ -243,14 +243,6 @@ class _CreatePropertyButtonState extends State<_CreatePropertyButton> {
       },
     );
   }
-
-  BoxDecoration _makeBoxDecoration(BuildContext context) {
-    final borderSide =
-        BorderSide(color: Theme.of(context).dividerColor, width: 1.0);
-    return BoxDecoration(
-      border: Border(top: borderSide),
-    );
-  }
 }
 
 class _PropertyCell extends StatefulWidget {
@@ -376,3 +368,69 @@ GridCellStyle? _customCellStyle(FieldType fieldType) {
   }
   throw UnimplementedError;
 }
+
+class _RowOptionColumn extends StatelessWidget {
+  final String rowId;
+  const _RowOptionColumn({
+    required String viewId,
+    required this.rowId,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      mainAxisSize: MainAxisSize.min,
+      children: [
+        Padding(
+          padding: const EdgeInsets.only(left: 10),
+          child: FlowyText(LocaleKeys.grid_row_action.tr()),
+        ),
+        const VSpace(15),
+        _DeleteButton(rowId: rowId),
+        _DuplicateButton(rowId: rowId),
+      ],
+    );
+  }
+}
+
+class _DeleteButton extends StatelessWidget {
+  final String rowId;
+  const _DeleteButton({required this.rowId, Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      height: GridSize.popoverItemHeight,
+      child: FlowyButton(
+        text: FlowyText.regular(LocaleKeys.grid_row_delete.tr()),
+        leftIcon: const FlowySvg(name: "home/trash"),
+        onTap: () {
+          context.read<RowDetailBloc>().add(RowDetailEvent.deleteRow(rowId));
+          FlowyOverlay.pop(context);
+        },
+      ),
+    );
+  }
+}
+
+class _DuplicateButton extends StatelessWidget {
+  final String rowId;
+  const _DuplicateButton({required this.rowId, Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      height: GridSize.popoverItemHeight,
+      child: FlowyButton(
+        text: FlowyText.regular(LocaleKeys.grid_row_duplicate.tr()),
+        leftIcon: const FlowySvg(name: "grid/duplicate"),
+        onTap: () {
+          context.read<RowDetailBloc>().add(RowDetailEvent.duplicateRow(rowId));
+          FlowyOverlay.pop(context);
+        },
+      ),
+    );
+  }
+}

+ 1 - 12
frontend/appflowy_flutter/lib/plugins/document/document_page.dart

@@ -1,15 +1,6 @@
-import 'package:appflowy/plugins/document/presentation/plugins/board/board_view_menu_item.dart';
+import 'package:appflowy/plugins/document/presentation/plugins/plugins.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
-import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/board/board_node_widget.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_node_widget.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/grid/grid_menu_item.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/grid/grid_node_widget.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/auto_completion_plugins.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_toolbar_item.dart';
 import 'package:dartz/dartz.dart' as dartz;
 import 'package:flowy_infra_ui/widget/error_page.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
@@ -20,8 +11,6 @@ import '../../startup/startup.dart';
 import 'application/doc_bloc.dart';
 import 'editor_styles.dart';
 import 'presentation/banner.dart';
-import 'presentation/plugins/grid/grid_view_menu_item.dart';
-import 'presentation/plugins/board/board_menu_item.dart';
 
 class DocumentPage extends StatefulWidget {
   final VoidCallback onDeleted;

+ 47 - 16
frontend/appflowy_flutter/lib/plugins/document/editor_styles.dart

@@ -1,32 +1,63 @@
 import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:flutter/material.dart';
+import 'package:google_fonts/google_fonts.dart';
 import 'package:provider/provider.dart';
 
 EditorStyle customEditorTheme(BuildContext context) {
   final documentStyle = context.watch<DocumentAppearanceCubit>().state;
-  var editorStyle = Theme.of(context).brightness == Brightness.dark
-      ? EditorStyle.dark
-      : EditorStyle.light;
-  editorStyle = editorStyle.copyWith(
-    padding: const EdgeInsets.symmetric(horizontal: 100, vertical: 0),
-    textStyle: editorStyle.textStyle?.copyWith(
-      fontFamily: 'poppins',
-      fontSize: documentStyle.fontSize,
-    ),
-    placeholderTextStyle: editorStyle.placeholderTextStyle?.copyWith(
+  final theme = Theme.of(context);
+
+  var editorStyle = EditorStyle(
+    // Editor styles
+    padding: const EdgeInsets.symmetric(horizontal: 100),
+    backgroundColor: theme.colorScheme.surface,
+    cursorColor: theme.colorScheme.primary,
+    // Text styles
+    textPadding: const EdgeInsets.symmetric(vertical: 8.0),
+    textStyle: TextStyle(
       fontFamily: 'poppins',
       fontSize: documentStyle.fontSize,
+      color: theme.colorScheme.onBackground,
     ),
-    bold: editorStyle.bold?.copyWith(
-      fontWeight: FontWeight.w600,
+    selectionColor: theme.colorScheme.tertiary.withOpacity(0.2),
+    // Selection menu
+    selectionMenuBackgroundColor: theme.cardColor,
+    selectionMenuItemTextColor: theme.iconTheme.color,
+    selectionMenuItemIconColor: theme.colorScheme.onBackground,
+    selectionMenuItemSelectedIconColor: theme.colorScheme.onSurface,
+    selectionMenuItemSelectedTextColor: theme.colorScheme.onSurface,
+    selectionMenuItemSelectedColor: theme.hoverColor,
+    // Toolbar and its item's style
+    toolbarColor: theme.colorScheme.onTertiary,
+    toolbarElevation: 0,
+    lineHeight: 1.5,
+    placeholderTextStyle:
+        TextStyle(fontSize: documentStyle.fontSize, color: theme.hintColor),
+    bold: const TextStyle(
       fontFamily: 'poppins-Bold',
+      fontWeight: FontWeight.w600,
+    ),
+    italic: const TextStyle(fontStyle: FontStyle.italic),
+    underline: const TextStyle(decoration: TextDecoration.underline),
+    strikethrough: const TextStyle(decoration: TextDecoration.lineThrough),
+    href: TextStyle(
+      color: theme.colorScheme.primary,
+      decoration: TextDecoration.underline,
     ),
-    backgroundColor: Theme.of(context).colorScheme.surface,
-    selectionMenuBackgroundColor: Theme.of(context).cardColor,
-    selectionMenuItemSelectedIconColor: Theme.of(context).colorScheme.onSurface,
-    selectionMenuItemSelectedTextColor: Theme.of(context).colorScheme.onSurface,
+    highlightColorHex: '0x6000BCF0',
+    code: GoogleFonts.robotoMono(
+      textStyle: TextStyle(
+        fontSize: documentStyle.fontSize,
+        fontWeight: FontWeight.normal,
+        color: Colors.red,
+        backgroundColor: theme.colorScheme.inverseSurface,
+      ),
+    ),
+    popupMenuFGColor: theme.iconTheme.color,
+    popupMenuHoverColor: theme.colorScheme.tertiaryContainer,
   );
+
   return editorStyle;
 }
 

+ 12 - 3
frontend/appflowy_flutter/lib/plugins/document/presentation/more/font_size_switcher.dart

@@ -1,4 +1,5 @@
 import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
+import 'package:flowy_infra/theme_extension.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flutter/material.dart';
 import 'package:appflowy/generated/locale_keys.g.dart';
@@ -24,6 +25,8 @@ class _FontSizeSwitcherState extends State<FontSizeSwitcher> {
 
   @override
   Widget build(BuildContext context) {
+    final selectedBgColor = AFThemeExtension.of(context).toggleButtonBGColor;
+    final foregroundColor = Theme.of(context).colorScheme.onBackground;
     return BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(
       builder: (context, state) {
         return Column(
@@ -43,10 +46,16 @@ class _FontSizeSwitcherState extends State<FontSizeSwitcher> {
               onPressed: (int index) {
                 _updateSelectedFontSize(_fontSizes[index].item2);
               },
+              color: foregroundColor,
               borderRadius: const BorderRadius.all(Radius.circular(5)),
-              selectedColor: Theme.of(context).colorScheme.tertiary,
-              fillColor: Theme.of(context).colorScheme.primary,
-              color: Theme.of(context).hintColor,
+              borderColor: foregroundColor,
+              borderWidth: 0.5,
+              // when selected
+              selectedColor: foregroundColor,
+              selectedBorderColor: foregroundColor,
+              fillColor: selectedBgColor,
+              // when hover
+              hoverColor: selectedBgColor.withOpacity(0.3),
               constraints: const BoxConstraints(
                 minHeight: 40.0,
                 minWidth: 80.0,

+ 1 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/more/more_button.dart

@@ -12,6 +12,7 @@ class DocumentMoreButton extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return PopupMenuButton<int>(
+      color: Theme.of(context).colorScheme.surfaceVariant,
       offset: const Offset(0, 30),
       itemBuilder: (context) {
         return [

+ 7 - 6
frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/callout/callout_node_widget.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/callout/callout_node_widget.dart

@@ -1,6 +1,5 @@
+import 'package:appflowy/plugins/document/presentation/plugins/plugins.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:appflowy_editor_plugins/src/emoji_picker/emoji_menu_item.dart';
-import 'package:appflowy_editor_plugins/src/extensions/theme_extension.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:flowy_infra/theme_extension.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -192,10 +191,12 @@ class _CalloutWidgetState extends State<_CalloutWidget> with SelectableMixin {
   Widget _buildColorPicker() {
     return FlowyColorPicker(
       colors: FlowyTint.values
-          .map((t) => ColorOption(
-                color: t.color(context),
-                name: t.tintName(AppFlowyEditorLocalizations.current),
-              ))
+          .map(
+            (t) => ColorOption(
+              color: t.color(context),
+              name: t.tintName(AppFlowyEditorLocalizations.current),
+            ),
+          )
           .toList(),
       selected: tint.color(context),
       onTap: (color, index) {

+ 3 - 3
frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/code_block/code_block_node_widget.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/code_block/code_block_node_widget.dart

@@ -157,11 +157,11 @@ class __CodeBlockNodeWidgeState extends State<_CodeBlockNodeWidge>
             ? TextSpan(text: node.value)
             : TextSpan(
                 text: node.value,
-                style: _builtInCodeBlockTheme[node.className!]));
+                style: _builtInCodeBlockTheme[node.className!],),);
       } else if (node.children != null) {
         List<TextSpan> tmp = [];
         currentSpans.add(TextSpan(
-            children: tmp, style: _builtInCodeBlockTheme[node.className!]));
+            children: tmp, style: _builtInCodeBlockTheme[node.className!],),);
         stack.add(currentSpans);
         currentSpans = tmp;
 
@@ -213,7 +213,7 @@ const _builtInCodeBlockTheme = {
   'attr': TextStyle(color: Color(0xff836C28)),
   'subst': TextStyle(color: Color(0xff000000)),
   'formula': TextStyle(
-      backgroundColor: Color(0xffeeeeee), fontStyle: FontStyle.italic),
+      backgroundColor: Color(0xffeeeeee), fontStyle: FontStyle.italic,),
   'addition': TextStyle(backgroundColor: Color(0xffbaeeba)),
   'deletion': TextStyle(backgroundColor: Color(0xffffc8bd)),
   'selector-id': TextStyle(color: Color(0xff9b703f)),

+ 1 - 1
frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/code_block/code_block_shortcut_event.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/code_block/code_block_shortcut_event.dart

@@ -1,5 +1,5 @@
+import 'package:appflowy/plugins/document/presentation/plugins/plugins.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:appflowy_editor_plugins/src/code_block/code_block_node_widget.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 

+ 3 - 6
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/change_cover_popover.dart

@@ -2,11 +2,8 @@ import 'dart:io';
 import 'dart:ui';
 
 import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/cover/change_cover_popover_bloc.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_image_picker.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_node_widget.dart';
+import 'package:appflowy/plugins/document/presentation/plugins/plugins.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/size.dart';
@@ -257,8 +254,6 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
               if (index == 0) {
                 return Container(
                   decoration: BoxDecoration(
-                    color:
-                        Theme.of(context).colorScheme.primary.withOpacity(0.15),
                     border: Border.all(
                       color: Theme.of(context).colorScheme.primary,
                     ),
@@ -270,6 +265,8 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
                       Icons.add,
                       color: Theme.of(context).colorScheme.primary,
                     ),
+                    hoverColor:
+                        Theme.of(context).colorScheme.primary.withOpacity(0.15),
                     width: 20,
                     onPressed: () {
                       setState(() {

+ 55 - 42
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_image_picker.dart

@@ -145,7 +145,7 @@ class _NetworkImageUrlInputState extends State<NetworkImageUrlInput> {
             },
             hoverColor: Colors.transparent,
             fillColor: buttonDisabled
-                ? Colors.grey
+                ? Theme.of(context).disabledColor
                 : Theme.of(context).colorScheme.primary,
             height: 36,
             title: LocaleKeys.document_plugins_cover_add.tr(),
@@ -174,7 +174,7 @@ class ImagePickerActionButtons extends StatelessWidget {
       children: [
         FlowyTextButton(
           LocaleKeys.document_plugins_cover_back.tr(),
-          hoverColor: Colors.transparent,
+          hoverColor: Theme.of(context).colorScheme.secondaryContainer,
           fillColor: Colors.transparent,
           mainAxisAlignment: MainAxisAlignment.end,
           onPressed: () => onBackPressed(),
@@ -182,7 +182,7 @@ class ImagePickerActionButtons extends StatelessWidget {
         FlowyTextButton(
           LocaleKeys.document_plugins_cover_saveToGallery.tr(),
           onPressed: () => onSave(),
-          hoverColor: Colors.transparent,
+          hoverColor: Theme.of(context).colorScheme.secondaryContainer,
           fillColor: Colors.transparent,
           mainAxisAlignment: MainAxisAlignment.end,
           fontColor: Theme.of(context).colorScheme.primary,
@@ -204,48 +204,61 @@ class CoverImagePreviewWidget extends StatefulWidget {
 
 class _CoverImagePreviewWidgetState extends State<CoverImagePreviewWidget> {
   _buildFilePickerWidget(BuildContext ctx) {
-    return Column(
-      mainAxisAlignment: MainAxisAlignment.center,
-      children: [
-        Row(
-          mainAxisAlignment: MainAxisAlignment.center,
-          children: [
-            svgWidget(
-              "editor/add",
-              size: const Size(20, 20),
-            ),
-            const SizedBox(
-              width: 3,
-            ),
-            FlowyText(
-              LocaleKeys.document_plugins_cover_pasteImageUrl.tr(),
-            ),
-          ],
-        ),
-        const SizedBox(
-          height: 10,
-        ),
-        FlowyText(
-          LocaleKeys.document_plugins_cover_or.tr(),
-          color: Colors.grey,
-        ),
-        const SizedBox(
-          height: 10,
+    return Container(
+      decoration: BoxDecoration(
+        color: Theme.of(context).cardColor,
+        borderRadius: Corners.s6Border,
+        border: Border.fromBorderSide(
+          BorderSide(
+            color: Theme.of(context).colorScheme.primary,
+            width: 1,
+          ),
         ),
-        FlowyButton(
-          onTap: () {
-            ctx.read<CoverImagePickerBloc>().add(const PickFileImage());
-          },
-          useIntrinsicWidth: true,
-          leftIcon: svgWidget(
-            "file_icon",
-            size: const Size(25, 25),
+      ),
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.center,
+        children: [
+          Row(
+            mainAxisAlignment: MainAxisAlignment.center,
+            children: [
+              const FlowySvg(
+                name: 'editor/add',
+                size: Size(20, 20),
+              ),
+              const SizedBox(
+                width: 3,
+              ),
+              FlowyText(
+                LocaleKeys.document_plugins_cover_pasteImageUrl.tr(),
+              ),
+            ],
           ),
-          text: FlowyText(
-            LocaleKeys.document_plugins_cover_pickFromFiles.tr(),
+          const SizedBox(
+            height: 10,
           ),
-        ),
-      ],
+          FlowyText(
+            LocaleKeys.document_plugins_cover_or.tr(),
+            fontWeight: FontWeight.w300,
+          ),
+          const SizedBox(
+            height: 10,
+          ),
+          FlowyButton(
+            hoverColor: Theme.of(context).hoverColor,
+            onTap: () {
+              ctx.read<CoverImagePickerBloc>().add(const PickFileImage());
+            },
+            useIntrinsicWidth: true,
+            leftIcon: const FlowySvg(
+              name: 'file_icon',
+              size: Size(20, 20),
+            ),
+            text: FlowyText(
+              LocaleKeys.document_plugins_cover_pickFromFiles.tr(),
+            ),
+          ),
+        ],
+      ),
     );
   }
 

+ 63 - 36
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_node_widget.dart

@@ -5,7 +5,7 @@ import 'package:appflowy/plugins/document/presentation/plugins/cover/change_cove
 import 'package:appflowy/plugins/document/presentation/plugins/cover/emoji_popover.dart';
 import 'package:appflowy/plugins/document/presentation/plugins/cover/icon_widget.dart';
 import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
-import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg;
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/image.dart';
@@ -393,21 +393,32 @@ class _CoverImageState extends State<_CoverImage> {
         mainAxisSize: MainAxisSize.min,
         children: [
           AppFlowyPopover(
+            onClose: () {
+              setOverlayButtonsHidden(true);
+            },
             offset: const Offset(-125, 10),
             controller: popoverController,
             direction: PopoverDirection.bottomWithCenterAligned,
             constraints: BoxConstraints.loose(const Size(380, 450)),
             margin: EdgeInsets.zero,
-            child: RoundedTextButton(
-              onPressed: () {
-                popoverController.show();
-              },
-              hoverColor: Theme.of(context).colorScheme.surface,
-              textColor: Theme.of(context).colorScheme.tertiary,
-              fillColor: Theme.of(context).colorScheme.surface.withOpacity(0.8),
-              width: 120,
-              height: 28,
-              title: LocaleKeys.document_plugins_cover_changeCover.tr(),
+            child: Visibility(
+              maintainState: true,
+              maintainAnimation: true,
+              maintainSize: true,
+              visible: !isOverlayButtonsHidden,
+              child: RoundedTextButton(
+                onPressed: () {
+                  popoverController.show();
+                  setOverlayButtonsHidden(true);
+                },
+                hoverColor: Theme.of(context).colorScheme.surface,
+                textColor: Theme.of(context).colorScheme.tertiary,
+                fillColor:
+                    Theme.of(context).colorScheme.surface.withOpacity(0.5),
+                width: 120,
+                height: 28,
+                title: LocaleKeys.document_plugins_cover_changeCover.tr(),
+              ),
             ),
             popupBuilder: (BuildContext popoverContext) {
               return ChangeCoverPopover(
@@ -418,18 +429,24 @@ class _CoverImageState extends State<_CoverImage> {
             },
           ),
           const SizedBox(width: 10),
-          FlowyIconButton(
-            fillColor: Theme.of(context).colorScheme.surface.withOpacity(0.8),
-            hoverColor: Theme.of(context).colorScheme.surface,
-            iconPadding: const EdgeInsets.all(5),
-            width: 28,
-            icon: svgWidget(
-              'editor/delete',
-              color: Theme.of(context).colorScheme.tertiary,
+          Visibility(
+            maintainAnimation: true,
+            maintainSize: true,
+            maintainState: true,
+            visible: !isOverlayButtonsHidden,
+            child: FlowyIconButton(
+              hoverColor: Theme.of(context).colorScheme.surface,
+              fillColor: Theme.of(context).colorScheme.surface.withOpacity(0.5),
+              iconPadding: const EdgeInsets.all(5),
+              width: 28,
+              icon: svgWidget(
+                'editor/delete',
+                color: Theme.of(context).colorScheme.tertiary,
+              ),
+              onPressed: () {
+                widget.onCoverChanged(CoverSelectionType.initial, null);
+              },
             ),
-            onPressed: () {
-              widget.onCoverChanged(CoverSelectionType.initial, null);
-            },
           ),
         ],
       ),
@@ -477,20 +494,30 @@ class _CoverImageState extends State<_CoverImage> {
         break;
     }
 //OverflowBox needs to be wraped by a widget with constraints(or from its parent) first,otherwise it will occur an error
-    return SizedBox(
-      height: height,
-      child: OverflowBox(
-        maxWidth: screenSize.width,
-        child: Stack(
-          children: [
-            Container(
-              padding: const EdgeInsets.only(bottom: 10),
-              height: double.infinity,
-              width: double.infinity,
-              child: coverImage,
-            ),
-            hasCover ? _buildCoverOverlayButtons(context) : const SizedBox()
-          ],
+    return MouseRegion(
+      onEnter: (event) {
+        setOverlayButtonsHidden(false);
+      },
+      onExit: (event) {
+        setOverlayButtonsHidden(true);
+      },
+      child: SizedBox(
+        height: height,
+        child: OverflowBox(
+          maxWidth: screenSize.width,
+          child: Stack(
+            children: [
+              Container(
+                padding: const EdgeInsets.only(bottom: 10),
+                height: double.infinity,
+                width: double.infinity,
+                child: coverImage,
+              ),
+              hasCover
+                  ? _buildCoverOverlayButtons(context)
+                  : const SizedBox.shrink()
+            ],
+          ),
         ),
       ),
     );

+ 0 - 1
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/emoji_popover.dart

@@ -4,7 +4,6 @@ import 'package:appflowy/workspace/presentation/widgets/emoji_picker/src/default
 import 'package:appflowy/workspace/presentation/widgets/emoji_picker/src/emoji_view_state.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:easy_localization/easy_localization.dart';
-import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra_ui/style_widget/button.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flutter/material.dart';

+ 0 - 0
frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/divider/divider_node_widget.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/divider/divider_node_widget.dart


+ 1 - 1
frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/divider/divider_shortcut_event.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/divider/divider_shortcut_event.dart

@@ -1,5 +1,5 @@
+import 'package:appflowy/plugins/document/presentation/plugins/divider/divider_node_widget.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:appflowy_editor_plugins/src/divider/divider_node_widget.dart';
 import 'package:flutter/material.dart';
 
 // insert divider into a document by typing three minuses.

+ 1 - 1
frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/emoji_picker/emoji_menu_item.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/emoji_menu_item.dart

@@ -48,7 +48,7 @@ void _showEmojiSelectionMenu(
         ),
       ),
     );
-  });
+  },);
 
   Overlay.of(context).insert(_emojiSelectionMenu!);
 

+ 0 - 0
frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/emoji_picker/emoji_picker.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/emoji_picker.dart


+ 1 - 1
frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/emoji_picker/src/config.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/src/config.dart

@@ -27,7 +27,7 @@ class Config {
           const TextStyle(fontSize: 20, color: Colors.black26),
       this.tabIndicatorAnimDuration = kTabScrollDuration,
       this.categoryIcons = const CategoryIcons(),
-      this.buttonMode = ButtonMode.MATERIAL});
+      this.buttonMode = ButtonMode.MATERIAL,});
 
   /// Number of emojis per row
   final int columns;

+ 8 - 8
frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/emoji_picker/src/default_emoji_picker_view.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/src/default_emoji_picker_view.dart

@@ -27,14 +27,14 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
   @override
   void initState() {
     var initCategory = widget.state.categoryEmoji.indexWhere(
-        (element) => element.category == widget.config.initCategory);
+        (element) => element.category == widget.config.initCategory,);
     if (initCategory == -1) {
       initCategory = 0;
     }
     _tabController = TabController(
         initialIndex: initCategory,
         length: widget.state.categoryEmoji.length,
-        vsync: this);
+        vsync: this,);
     _pageController = PageController(initialPage: initCategory);
     _emojiFocusNode.requestFocus();
 
@@ -79,7 +79,7 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
             ),
             onPressed: () {
               widget.state.onBackspacePressed!();
-            }),
+            },),
       );
     }
     return Container();
@@ -161,7 +161,7 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
                               .asMap()
                               .entries
                               .map<Widget>((item) => _buildCategory(
-                                  item.value.category, emojiSize))
+                                  item.value.category, emojiSize,),)
                               .toList(),
                     ),
                   ),
@@ -207,7 +207,7 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
   }
 
   Widget _buildButtonWidget(
-      {required VoidCallback onPressed, required Widget child}) {
+      {required VoidCallback onPressed, required Widget child,}) {
     if (widget.config.buttonMode == ButtonMode.MATERIAL) {
       return InkWell(
         onTap: onPressed,
@@ -270,7 +270,7 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
           widget.state.onEmojiSelected(categoryEmoji.category, emoji);
         },
         child: FittedBox(
-          fit: BoxFit.fill,
+          fit: BoxFit.scaleDown,
           child: Text(
             emoji.emoji,
             textScaleFactor: 1.0,
@@ -279,7 +279,7 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
               backgroundColor: Colors.transparent,
             ),
           ),
-        ));
+        ),);
   }
 
   Widget _buildNoRecent() {
@@ -288,6 +288,6 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
       widget.config.noRecentsText,
       style: widget.config.noRecentsStyle,
       textAlign: TextAlign.center,
-    ));
+    ),);
   }
 }

+ 0 - 0
frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/emoji_picker/src/emoji_lists.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/src/emoji_lists.dart


+ 13 - 13
frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/emoji_picker/src/emoji_picker.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/src/emoji_picker.dart

@@ -191,29 +191,29 @@ class EmojiPickerState extends State<EmojiPicker> {
     }
     categoryEmoji.addAll([
       CategoryEmoji(Category.SMILEYS,
-          await _getAvailableEmojis(emoji_list.smileys, title: 'smileys')),
+          await _getAvailableEmojis(emoji_list.smileys, title: 'smileys'),),
       CategoryEmoji(Category.ANIMALS,
-          await _getAvailableEmojis(emoji_list.animals, title: 'animals')),
+          await _getAvailableEmojis(emoji_list.animals, title: 'animals'),),
       CategoryEmoji(Category.FOODS,
-          await _getAvailableEmojis(emoji_list.foods, title: 'foods')),
+          await _getAvailableEmojis(emoji_list.foods, title: 'foods'),),
       CategoryEmoji(
           Category.ACTIVITIES,
           await _getAvailableEmojis(emoji_list.activities,
-              title: 'activities')),
+              title: 'activities',),),
       CategoryEmoji(Category.TRAVEL,
-          await _getAvailableEmojis(emoji_list.travel, title: 'travel')),
+          await _getAvailableEmojis(emoji_list.travel, title: 'travel'),),
       CategoryEmoji(Category.OBJECTS,
-          await _getAvailableEmojis(emoji_list.objects, title: 'objects')),
+          await _getAvailableEmojis(emoji_list.objects, title: 'objects'),),
       CategoryEmoji(Category.SYMBOLS,
-          await _getAvailableEmojis(emoji_list.symbols, title: 'symbols')),
+          await _getAvailableEmojis(emoji_list.symbols, title: 'symbols'),),
       CategoryEmoji(Category.FLAGS,
-          await _getAvailableEmojis(emoji_list.flags, title: 'flags'))
+          await _getAvailableEmojis(emoji_list.flags, title: 'flags'),)
     ]);
   }
 
   // Get available emoji for given category title
   Future<List<Emoji>> _getAvailableEmojis(Map<String, String> map,
-      {required String title}) async {
+      {required String title,}) async {
     Map<String, String>? newMap;
 
     // Get Emojis cached locally if available
@@ -236,7 +236,7 @@ class EmojiPickerState extends State<EmojiPicker> {
 
   // Check if emoji is available on current platform
   Future<Map<String, String>?> _getPlatformAvailableEmoji(
-      Map<String, String> emoji) async {
+      Map<String, String> emoji,) async {
     if (Platform.isAndroid) {
       Map<String, String>? filtered = {};
       var delimiter = '|';
@@ -244,7 +244,7 @@ class EmojiPickerState extends State<EmojiPicker> {
         var entries = emoji.values.join(delimiter);
         var keys = emoji.keys.join(delimiter);
         var result = (await platform.invokeMethod<String>('checkAvailability',
-            {'emojiKeys': keys, 'emojiEntries': entries})) as String;
+            {'emojiKeys': keys, 'emojiEntries': entries},)) as String;
         var resultKeys = result.split(delimiter);
         for (var i = 0; i < resultKeys.length; i++) {
           filtered[resultKeys[i]] = emoji[resultKeys[i]]!;
@@ -272,7 +272,7 @@ class EmojiPickerState extends State<EmojiPicker> {
 
   // Stores filtered emoji locally for faster access next time
   Future<void> _cacheFilteredEmojis(
-      String title, Map<String, String> emojis) async {
+      String title, Map<String, String> emojis,) async {
     final prefs = await SharedPreferences.getInstance();
     var emojiJson = jsonEncode(emojis);
     prefs.setString(title, emojiJson);
@@ -305,7 +305,7 @@ class EmojiPickerState extends State<EmojiPicker> {
     recentEmoji.sort((a, b) => b.counter - a.counter);
     // Limit entries to recentsLimit
     recentEmoji = recentEmoji.sublist(
-        0, min(widget.config.recentsLimit, recentEmoji.length));
+        0, min(widget.config.recentsLimit, recentEmoji.length),);
     // save locally
     prefs.setString('recent', jsonEncode(recentEmoji));
   }

+ 0 - 0
frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/emoji_picker/src/emoji_picker_builder.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/src/emoji_picker_builder.dart


+ 0 - 0
frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/emoji_picker/src/emoji_view_state.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/src/emoji_view_state.dart


+ 0 - 0
frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/emoji_picker/src/models/category_models.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/src/models/category_models.dart


+ 0 - 0
frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/emoji_picker/src/models/emoji_model.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/src/models/emoji_model.dart


+ 0 - 0
frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/emoji_picker/src/models/recent_emoji_model.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/emoji_picker/src/models/recent_emoji_model.dart


+ 1 - 0
frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/extensions/theme_extension.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/extensions/flowy_tint_extension.dart

@@ -1,3 +1,4 @@
+
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:flowy_infra/theme_extension.dart';
 import 'package:flutter/material.dart';

+ 0 - 168
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/horizontal_rule_node_widget.dart

@@ -1,168 +0,0 @@
-import 'dart:collection';
-
-import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:flutter/material.dart';
-
-ShortcutEvent insertHorizontalRule = ShortcutEvent(
-  key: 'Horizontal rule',
-  command: 'Minus',
-  handler: _insertHorzaontalRule,
-);
-
-ShortcutEventHandler _insertHorzaontalRule = (editorState, event) {
-  final selection = editorState.service.selectionService.currentSelection.value;
-  final textNodes = editorState.service.selectionService.currentSelectedNodes
-      .whereType<TextNode>();
-  if (textNodes.length != 1 || selection == null) {
-    return KeyEventResult.ignored;
-  }
-  final textNode = textNodes.first;
-  if (textNode.toPlainText() == '--') {
-    final transaction = editorState.transaction
-      ..deleteText(textNode, 0, 2)
-      ..insertNode(
-        textNode.path,
-        Node(
-          type: 'horizontal_rule',
-          children: LinkedList(),
-          attributes: {},
-        ),
-      )
-      ..afterSelection =
-          Selection.single(path: textNode.path.next, startOffset: 0);
-    editorState.apply(transaction);
-    return KeyEventResult.handled;
-  }
-  return KeyEventResult.ignored;
-};
-
-SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
-  name: 'Horizontal rule',
-  icon: (editorState, onSelected) => Icon(
-    Icons.horizontal_rule,
-    color: onSelected
-        ? editorState.editorStyle.selectionMenuItemSelectedIconColor
-        : editorState.editorStyle.selectionMenuItemIconColor,
-    size: 18.0,
-  ),
-  keywords: ['horizontal rule'],
-  handler: (editorState, _, __) {
-    final selection =
-        editorState.service.selectionService.currentSelection.value;
-    final textNodes = editorState.service.selectionService.currentSelectedNodes
-        .whereType<TextNode>();
-    if (selection == null || textNodes.isEmpty) {
-      return;
-    }
-    final textNode = textNodes.first;
-    if (textNode.toPlainText().isEmpty) {
-      final transaction = editorState.transaction
-        ..insertNode(
-          textNode.path,
-          Node(
-            type: 'horizontal_rule',
-            children: LinkedList(),
-            attributes: {},
-          ),
-        )
-        ..afterSelection =
-            Selection.single(path: textNode.path.next, startOffset: 0);
-      editorState.apply(transaction);
-    } else {
-      final transaction = editorState.transaction
-        ..insertNode(
-          selection.end.path.next,
-          TextNode(
-            children: LinkedList(),
-            attributes: {
-              'subtype': 'horizontal_rule',
-            },
-            delta: Delta()..insert('---'),
-          ),
-        )
-        ..afterSelection = selection;
-      editorState.apply(transaction);
-    }
-  },
-);
-
-class HorizontalRuleWidgetBuilder extends NodeWidgetBuilder<Node> {
-  @override
-  Widget build(NodeWidgetContext<Node> context) {
-    return _HorizontalRuleWidget(
-      key: context.node.key,
-      node: context.node,
-      editorState: context.editorState,
-    );
-  }
-
-  @override
-  NodeValidator<Node> get nodeValidator => (node) {
-        return true;
-      };
-}
-
-class _HorizontalRuleWidget extends StatefulWidget {
-  const _HorizontalRuleWidget({
-    Key? key,
-    required this.node,
-    required this.editorState,
-  }) : super(key: key);
-
-  final Node node;
-  final EditorState editorState;
-
-  @override
-  State<_HorizontalRuleWidget> createState() => __HorizontalRuleWidgetState();
-}
-
-class __HorizontalRuleWidgetState extends State<_HorizontalRuleWidget>
-    with SelectableMixin {
-  RenderBox get _renderBox => context.findRenderObject() as RenderBox;
-
-  @override
-  Widget build(BuildContext context) {
-    return Container(
-      padding: const EdgeInsets.symmetric(vertical: 10),
-      child: Container(
-        height: 1,
-        color: Colors.grey,
-      ),
-    );
-  }
-
-  @override
-  Position start() => Position(path: widget.node.path, offset: 0);
-
-  @override
-  Position end() => Position(path: widget.node.path, offset: 1);
-
-  @override
-  Position getPositionInOffset(Offset start) => end();
-
-  @override
-  bool get shouldCursorBlink => false;
-
-  @override
-  CursorStyle get cursorStyle => CursorStyle.borderLine;
-
-  @override
-  Rect? getCursorRectInPosition(Position position) {
-    final size = _renderBox.size;
-    return Rect.fromLTWH(-size.width / 2.0, 0, size.width, size.height);
-  }
-
-  @override
-  List<Rect> getRectsInSelection(Selection selection) =>
-      [Offset.zero & _renderBox.size];
-
-  @override
-  Selection getSelectionInRange(Offset start, Offset end) => Selection.single(
-        path: widget.node.path,
-        startOffset: 0,
-        endOffset: 1,
-      );
-
-  @override
-  Offset localToGlobal(Offset offset) => _renderBox.localToGlobal(offset);
-}

+ 0 - 0
frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/infra/svg.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/infra/svg.dart


+ 19 - 9
frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/math_equation/math_equation_node_widget.dart → frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/math_equation/math_equation_node_widget.dart

@@ -1,4 +1,9 @@
+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/style_widget/text.dart';
+import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
+import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_math_fork/flutter_math.dart';
@@ -131,14 +136,14 @@ class _MathEquationNodeWidgetState extends State<_MathEquationNodeWidget> {
       decoration: BoxDecoration(
         borderRadius: const BorderRadius.all(Radius.circular(8.0)),
         color: _isHover || _mathEquation.isEmpty
-            ? Colors.grey[200]
+            ? Theme.of(context).colorScheme.tertiaryContainer
             : Colors.transparent,
       ),
       child: Center(
         child: _mathEquation.isEmpty
-            ? Text(
-                'Add a Math Equation',
-                style: widget.editorState.editorStyle.placeholderTextStyle,
+            ? FlowyText.medium(
+                LocaleKeys.document_plugins_mathEquation_addMathEquation.tr(),
+                fontSize: 16,
               )
             : Math.tex(
                 _mathEquation,
@@ -155,7 +160,10 @@ class _MathEquationNodeWidgetState extends State<_MathEquationNodeWidget> {
       builder: (context) {
         final controller = TextEditingController(text: _mathEquation);
         return AlertDialog(
-          title: const Text('Edit Math Equation'),
+          backgroundColor: Theme.of(context).canvasColor,
+          title: Text(
+            LocaleKeys.document_plugins_mathEquation_editMathEquation.tr(),
+          ),
           content: RawKeyboardListener(
             focusNode: FocusNode(),
             onKey: (key) {
@@ -178,15 +186,17 @@ class _MathEquationNodeWidgetState extends State<_MathEquationNodeWidget> {
             ),
           ),
           actions: [
-            TextButton(
+            SecondaryTextButton(
+              LocaleKeys.button_Cancel.tr(),
               onPressed: () => _dismiss(context),
-              child: const Text('Cancel'),
             ),
-            TextButton(
+            PrimaryTextButton(
+              LocaleKeys.button_Done.tr(),
               onPressed: () => _updateMathEquation(controller.text, context),
-              child: const Text('Done'),
             ),
           ],
+          actionsPadding: const EdgeInsets.only(bottom: 20),
+          actionsAlignment: MainAxisAlignment.spaceAround,
         );
       },
     );

+ 1 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart

@@ -50,6 +50,7 @@ abstract class OpenAIRepository {
     String? suffix,
     int maxTokens = 2048,
     double temperature = 0.3,
+    bool useAction = false,
   });
 
   ///  Get edits from GPT-3

+ 10 - 1
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart

@@ -5,7 +5,8 @@ import 'package:easy_localization/easy_localization.dart';
 
 enum SmartEditAction {
   summarize,
-  fixSpelling;
+  fixSpelling,
+  improveWriting;
 
   String get toInstruction {
     switch (this) {
@@ -13,6 +14,8 @@ enum SmartEditAction {
         return 'Tl;dr';
       case SmartEditAction.fixSpelling:
         return 'Correct this to standard English:';
+      case SmartEditAction.improveWriting:
+        return 'Rewrite this in your own words:';
     }
   }
 
@@ -22,6 +25,8 @@ enum SmartEditAction {
         return '$input\n\nTl;dr';
       case SmartEditAction.fixSpelling:
         return 'Correct this to standard English:\n\n$input';
+      case SmartEditAction.improveWriting:
+        return 'Rewrite this:\n\n$input';
     }
   }
 
@@ -31,6 +36,8 @@ enum SmartEditAction {
         return SmartEditAction.summarize;
       case 1:
         return SmartEditAction.fixSpelling;
+      case 2:
+        return SmartEditAction.improveWriting;
     }
     return SmartEditAction.fixSpelling;
   }
@@ -41,6 +48,8 @@ enum SmartEditAction {
         return LocaleKeys.document_plugins_smartEditSummarize.tr();
       case SmartEditAction.fixSpelling:
         return LocaleKeys.document_plugins_smartEditFixSpelling.tr();
+      case SmartEditAction.improveWriting:
+        return LocaleKeys.document_plugins_smartEditImproveWriting.tr();
     }
   }
 }

+ 71 - 51
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart

@@ -4,7 +4,7 @@ import 'package:appflowy/plugins/document/presentation/plugins/openai/service/op
 import 'package:appflowy/plugins/document/presentation/plugins/openai/util/learn_more_action.dart';
 import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart';
 import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart';
-import 'package:appflowy/user/application/user_service.dart';
+import 'package:appflowy/startup/startup.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -242,7 +242,7 @@ class _SmartEditInputState extends State<_SmartEditInput> {
           ),
           onPressed: () async {
             await _onReplace();
-            _onExit();
+            await _onExit();
           },
         ),
         const Space(10, 0),
@@ -257,7 +257,7 @@ class _SmartEditInputState extends State<_SmartEditInput> {
           ),
           onPressed: () async {
             await _onInsertBelow();
-            _onExit();
+            await _onExit();
           },
         ),
         const Space(10, 0),
@@ -272,10 +272,13 @@ class _SmartEditInputState extends State<_SmartEditInput> {
           ),
           onPressed: () async => await _onExit(),
         ),
-        const Spacer(),
-        FlowyText.regular(
-          LocaleKeys.document_plugins_warning.tr(),
-          color: Theme.of(context).hintColor,
+        const Spacer(flex: 2),
+        Expanded(
+          child: FlowyText.regular(
+            overflow: TextOverflow.ellipsis,
+            LocaleKeys.document_plugins_warning.tr(),
+            color: Theme.of(context).hintColor,
+          ),
         ),
       ],
     );
@@ -298,7 +301,22 @@ class _SmartEditInputState extends State<_SmartEditInput> {
       selection,
       texts,
     );
-    return widget.editorState.apply(transaction);
+    await widget.editorState.apply(transaction);
+
+    int endOffset = texts.last.length;
+    if (texts.length == 1) {
+      endOffset += selection.start.offset;
+    }
+
+    await widget.editorState.updateCursorSelection(
+      Selection(
+        start: selection.start,
+        end: Position(
+          path: [selection.start.path.first + texts.length - 1],
+          offset: endOffset,
+        ),
+      ),
+    );
   }
 
   Future<void> _onInsertBelow() async {
@@ -317,7 +335,16 @@ class _SmartEditInputState extends State<_SmartEditInput> {
         ),
       ),
     );
-    return widget.editorState.apply(transaction);
+    await widget.editorState.apply(transaction);
+
+    await widget.editorState.updateCursorSelection(
+      Selection(
+        start: Position(path: selection.end.path.next, offset: 0),
+        end: Position(
+          path: [selection.end.path.next.first + texts.length],
+        ),
+      ),
+    );
   }
 
   Future<void> _onExit() async {
@@ -333,49 +360,42 @@ class _SmartEditInputState extends State<_SmartEditInput> {
   }
 
   Future<void> _requestCompletions() async {
-    final result = await UserBackendService.getCurrentUserProfile();
-    return result.fold((l) async {
-      final openAIRepository = HttpOpenAIRepository(
-        client: client,
-        apiKey: l.openaiKey,
-      );
+    final openAIRepository = await getIt.getAsync<OpenAIRepository>();
 
-      var lines = input.split('\n\n');
-      if (action == SmartEditAction.summarize) {
-        lines = [lines.join('\n')];
-      }
-      for (var i = 0; i < lines.length; i++) {
-        final element = lines[i];
-        await openAIRepository.getStreamedCompletions(
-          useAction: true,
-          prompt: action.prompt(element),
-          onStart: () async {
-            setState(() {
-              loading = false;
-            });
-          },
-          onProcess: (response) async {
-            setState(() {
-              this.result += response.choices.first.text;
-            });
-          },
-          onEnd: () async {
-            setState(() {
-              if (i != lines.length - 1) {
-                this.result += '\n';
-              }
-            });
-          },
-          onError: (error) async {
-            await _showError(error.message);
-            await _onExit();
-          },
-        );
-      }
-    }, (r) async {
-      await _showError(r.msg);
-      await _onExit();
-    });
+    var lines = input.split('\n\n');
+    if (action == SmartEditAction.summarize) {
+      lines = [lines.join('\n')];
+    }
+    for (var i = 0; i < lines.length; i++) {
+      final element = lines[i];
+      await openAIRepository.getStreamedCompletions(
+        useAction: true,
+        prompt: action.prompt(element),
+        onStart: () async {
+          setState(() {
+            loading = false;
+          });
+        },
+        onProcess: (response) async {
+          setState(() {
+            if (response.choices.first.text != '\n') {
+              result += response.choices.first.text;
+            }
+          });
+        },
+        onEnd: () async {
+          setState(() {
+            if (i != lines.length - 1) {
+              result += '\n';
+            }
+          });
+        },
+        onError: (error) async {
+          await _showError(error.message);
+          await _onExit();
+        },
+      );
+    }
   }
 
   Future<void> _showError(String message) async {

+ 21 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/plugins.dart

@@ -0,0 +1,21 @@
+export 'board/board_node_widget.dart';
+export 'board/board_menu_item.dart';
+export 'board/board_view_menu_item.dart';
+export 'callout/callout_node_widget.dart';
+export 'code_block/code_block_node_widget.dart';
+export 'code_block/code_block_shortcut_event.dart';
+export 'cover/change_cover_popover_bloc.dart';
+export 'cover/cover_node_widget.dart';
+export 'cover/cover_image_picker.dart';
+export 'divider/divider_node_widget.dart';
+export 'divider/divider_shortcut_event.dart';
+export 'emoji_picker/emoji_menu_item.dart';
+export 'extensions/flowy_tint_extension.dart';
+export 'grid/grid_menu_item.dart';
+export 'grid/grid_node_widget.dart';
+export 'grid/grid_view_menu_item.dart';
+export 'math_equation/math_equation_node_widget.dart';
+export 'openai/widgets/auto_completion_node_widget.dart';
+export 'openai/widgets/auto_completion_plugins.dart';
+export 'openai/widgets/smart_edit_node_widget.dart';
+export 'openai/widgets/smart_edit_toolbar_item.dart';

+ 6 - 2
frontend/appflowy_flutter/lib/plugins/trash/src/sizes.dart

@@ -4,10 +4,14 @@ class TrashSizes {
   static double get fileNameWidth => 320 * scale;
   static double get lashModifyWidth => 230 * scale;
   static double get createTimeWidth => 230 * scale;
-  static double get padding => 100 * scale;
+  // padding between createTime and action icon
+  static double get padding => 40 * scale;
+  static double get actionIconWidth => 40 * scale;
   static double get totalWidth =>
       TrashSizes.fileNameWidth +
       TrashSizes.lashModifyWidth +
       TrashSizes.createTimeWidth +
-      TrashSizes.padding;
+      TrashSizes.padding +
+      // restore and delete icon
+      2 * TrashSizes.actionIconWidth;
 }

+ 6 - 10
frontend/appflowy_flutter/lib/plugins/trash/src/trash_cell.dart

@@ -38,23 +38,19 @@ class TrashCell extends StatelessWidget {
         ),
         const Spacer(),
         FlowyIconButton(
-          width: 26,
+          iconColorOnHover: Theme.of(context).colorScheme.onSurface,
+          width: TrashSizes.actionIconWidth,
           onPressed: onRestore,
           iconPadding: const EdgeInsets.all(5),
-          icon: svgWidget(
-            "editor/restore",
-            color: Theme.of(context).iconTheme.color,
-          ),
+          icon: const FlowySvg(name: 'editor/restore'),
         ),
         const HSpace(20),
         FlowyIconButton(
-          width: 26,
+          iconColorOnHover: Theme.of(context).colorScheme.onSurface,
+          width: TrashSizes.actionIconWidth,
           onPressed: onDelete,
           iconPadding: const EdgeInsets.all(5),
-          icon: svgWidget(
-            "editor/delete",
-            color: Theme.of(context).iconTheme.color,
-          ),
+          icon: const FlowySvg(name: 'editor/delete'),
         ),
       ],
     );

+ 2 - 8
frontend/appflowy_flutter/lib/plugins/trash/trash_page.dart

@@ -96,10 +96,7 @@ class _TrashPageState extends State<TrashPage> {
           IntrinsicWidth(
             child: FlowyButton(
               text: FlowyText.medium(LocaleKeys.trash_restoreAll.tr()),
-              leftIcon: svgWidget(
-                'editor/restore',
-                color: Theme.of(context).iconTheme.color,
-              ),
+              leftIcon: const FlowySvg(name: 'editor/restore'),
               onTap: () => context.read<TrashBloc>().add(
                     const TrashEvent.restoreAll(),
                   ),
@@ -109,10 +106,7 @@ class _TrashPageState extends State<TrashPage> {
           IntrinsicWidth(
             child: FlowyButton(
               text: FlowyText.medium(LocaleKeys.trash_deleteAll.tr()),
-              leftIcon: svgWidget(
-                'editor/delete',
-                color: Theme.of(context).iconTheme.color,
-              ),
+              leftIcon: const FlowySvg(name: 'editor/delete'),
               onTap: () =>
                   context.read<TrashBloc>().add(const TrashEvent.deleteAll()),
             ),

+ 21 - 2
frontend/appflowy_flutter/lib/startup/deps_resolver.dart

@@ -4,6 +4,7 @@ import 'package:appflowy/plugins/database_view/application/field/field_controlle
 import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
 import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart';
 import 'package:appflowy/plugins/database_view/grid/application/grid_header_bloc.dart';
+import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
 import 'package:appflowy/user/application/user_listener.dart';
 import 'package:appflowy/user/application/user_service.dart';
 import 'package:appflowy/util/file_picker/file_picker_impl.dart';
@@ -25,6 +26,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:get_it/get_it.dart';
+import 'package:http/http.dart' as http;
 
 class DependencyResolver {
   static Future<void> resolve(GetIt getIt) async {
@@ -42,8 +44,25 @@ class DependencyResolver {
   }
 }
 
-void _resolveCommonService(GetIt getIt) {
+void _resolveCommonService(GetIt getIt) async {
   getIt.registerFactory<FilePickerService>(() => FilePicker());
+
+  getIt.registerFactoryAsync<OpenAIRepository>(
+    () async {
+      final result = await UserBackendService.getCurrentUserProfile();
+      return result.fold(
+        (l) {
+          return HttpOpenAIRepository(
+            client: http.Client(),
+            apiKey: l.openaiKey,
+          );
+        },
+        (r) {
+          throw Exception('Failed to get user profile: ${r.msg}');
+        },
+      );
+    },
+  );
 }
 
 void _resolveUserDeps(GetIt getIt) {
@@ -153,4 +172,4 @@ void _resolveGridDeps(GetIt getIt) {
     (viewId, cache) =>
         DatabasePropertyBloc(viewId: viewId, fieldController: cache),
   );
-}
+}

+ 1 - 0
frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart

@@ -28,6 +28,7 @@ class InitAppWidgetTask extends LaunchTask {
       EasyLocalization(
         supportedLocales: const [
           // In alphabetical order
+          Locale('ar', 'AR'),
           Locale('ca', 'ES'),
           Locale('de', 'DE'),
           Locale('en'),

+ 20 - 10
frontend/appflowy_flutter/lib/workspace/application/appearance.dart

@@ -225,6 +225,8 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
       secondaryContainer: theme.selector,
       onSecondaryContainer: theme.topbarBg,
       tertiary: theme.shader7,
+      // Editor: toolbarColor
+      onTertiary: theme.toolbarColor,
       tertiaryContainer: theme.questionBubbleBG,
       background: theme.surface,
       onBackground: theme.text,
@@ -240,8 +242,15 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
       shadow: theme.shadow,
     );
 
+    const Set<MaterialState> scrollbarInteractiveStates = <MaterialState>{
+      MaterialState.pressed,
+      MaterialState.hovered,
+      MaterialState.dragged,
+    };
+
     return ThemeData(
       brightness: brightness,
+      dialogBackgroundColor: theme.surface,
       textTheme: _getTextTheme(fontFamily: fontFamily, fontColor: theme.text),
       textSelectionTheme: TextSelectionThemeData(
         cursorColor: theme.main2,
@@ -262,20 +271,20 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
         contentTextStyle: TextStyle(color: colorScheme.onSurface),
       ),
       scrollbarTheme: ScrollbarThemeData(
-        thumbColor: MaterialStateProperty.all(theme.shader3),
+        thumbColor: MaterialStateProperty.resolveWith((states) {
+          if (states.any(scrollbarInteractiveStates.contains)) {
+            return theme.shader7;
+          }
+          return theme.shader5;
+        }),
         thickness: MaterialStateProperty.resolveWith((states) {
-          const Set<MaterialState> interactiveStates = <MaterialState>{
-            MaterialState.pressed,
-            MaterialState.hovered,
-            MaterialState.dragged,
-          };
-          if (states.any(interactiveStates.contains)) {
-            return 5.0;
+          if (states.any(scrollbarInteractiveStates.contains)) {
+            return 4;
           }
           return 3.0;
         }),
         crossAxisMargin: 0.0,
-        mainAxisMargin: 0.0,
+        mainAxisMargin: 6.0,
         radius: Corners.s10Radius,
       ),
       materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
@@ -308,7 +317,8 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
           greySelect: theme.bg3,
           lightGreyHover: theme.hoverBG3,
           toggleOffFill: theme.shader5,
-          progressBarBGcolor: theme.progressBarBGcolor,
+          progressBarBGColor: theme.progressBarBGColor,
+          toggleButtonBGColor: theme.toggleButtonBGColor,
           code: _getFontStyle(
             fontFamily: monospaceFontFamily,
             fontColor: theme.shader3,

+ 2 - 0
frontend/appflowy_flutter/lib/workspace/application/settings/settings_location_cubit.dart

@@ -25,6 +25,8 @@ class SettingsLocation {
     if (Platform.isMacOS) {
       // remove the prefix `/Volumes/*`
       return _path?.replaceFirst(RegExp(r'^/Volumes/[^/]+'), '');
+    } else if (Platform.isWindows) {
+      return _path?.replaceAll("/", "\\");
     }
     return _path;
   }

+ 4 - 4
frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart

@@ -77,8 +77,8 @@ class ThemeSetting extends StatelessWidget {
       child: FlowyButton(
         text: FlowyText.medium(theme),
         rightIcon: currentTheme == theme
-            ? svgWidget("grid/checkmark")
-            : const SizedBox(),
+            ? const FlowySvg(name: 'grid/checkmark')
+            : null,
         onTap: () {
           if (currentTheme != theme) {
             context.read<AppearanceSettingsCubit>().setTheme(theme);
@@ -134,8 +134,8 @@ class ThemeModeSetting extends StatelessWidget {
       child: FlowyButton(
         text: FlowyText.medium(_themeModeLabelText(themeMode)),
         rightIcon: currentThemeMode == themeMode
-            ? svgWidget("grid/checkmark")
-            : const SizedBox(),
+            ? const FlowySvg(name: 'grid/checkmark')
+            : null,
         onTap: () {
           if (currentThemeMode != themeMode) {
             context.read<AppearanceSettingsCubit>().setThemeMode(themeMode);

+ 9 - 3
frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart

@@ -35,9 +35,8 @@ class SettingsFileLocationCustomzierState
       child: BlocBuilder<SettingsLocationCubit, SettingsLocation>(
         builder: (context, state) {
           return ListTile(
-            title: FlowyText.regular(
+            title: FlowyText.medium(
               LocaleKeys.settings_files_defaultLocation.tr(),
-              fontSize: 15.0,
               overflow: TextOverflow.ellipsis,
             ),
             subtitle: Tooltip(
@@ -63,7 +62,6 @@ class SettingsFileLocationCustomzierState
                 },
                 child: FlowyText.regular(
                   state.path ?? '',
-                  fontSize: 10.0,
                   overflow: TextOverflow.ellipsis,
                 ),
               ),
@@ -74,7 +72,11 @@ class SettingsFileLocationCustomzierState
                 Tooltip(
                   message: LocaleKeys.settings_files_restoreLocation.tr(),
                   child: FlowyIconButton(
+                    height: 40,
+                    width: 40,
                     icon: const Icon(Icons.restore_outlined),
+                    hoverColor:
+                        Theme.of(context).colorScheme.secondaryContainer,
                     onPressed: () async {
                       final result = await appFlowyDocumentDirectory();
                       await _setCustomLocation(result.path);
@@ -96,7 +98,11 @@ class SettingsFileLocationCustomzierState
                 Tooltip(
                   message: LocaleKeys.settings_files_customizeLocation.tr(),
                   child: FlowyIconButton(
+                    height: 40,
+                    width: 40,
                     icon: const Icon(Icons.folder_open_outlined),
+                    hoverColor:
+                        Theme.of(context).colorScheme.secondaryContainer,
                     onPressed: () async {
                       final result =
                           await getIt<FilePickerService>().getDirectoryPath();

Some files were not shown because too many files changed in this diff