Browse Source

Merge branch 'upstream-main' into feat/tauri-kanban

# Conflicts:
#	frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts
ascarbek 2 years ago
parent
commit
745ee264c8
31 changed files with 275 additions and 156 deletions
  1. 5 2
      frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart
  2. 4 6
      frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart
  3. 2 2
      frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart
  4. 1 0
      frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/toolbar/board_toolbar.dart
  5. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart
  6. 2 2
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart
  7. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/number.dart
  8. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/select_option.dart
  9. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/sort_editor.dart
  10. 18 15
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/filter_button.dart
  11. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_property.dart
  12. 19 16
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/setting_button.dart
  13. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/sort_button.dart
  14. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart
  15. 3 3
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart
  16. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart
  17. 2 2
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/url_cell/url_cell.dart
  18. 52 44
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_image_picker_bloc.dart
  19. 1 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart
  20. 7 4
      frontend/appflowy_flutter/lib/user/application/user_listener.dart
  21. 1 0
      frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/add_button.dart
  22. 1 0
      frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart
  23. 3 0
      frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart
  24. 31 0
      frontend/appflowy_tauri/src/appflowy_app/components/tests/TestGroup.tsx
  25. 13 5
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts
  26. 1 1
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_controller.ts
  27. 2 1
      frontend/rust-lib/flowy-database/src/services/group/controller_impls/select_option_controller/util.rs
  28. 17 15
      frontend/rust-lib/flowy-folder/src/services/workspace/controller.rs
  29. 34 17
      frontend/scripts/install_dev_env/install_linux.sh
  30. 26 8
      frontend/scripts/install_dev_env/install_macos.sh
  31. 22 5
      frontend/scripts/install_dev_env/install_windows.sh

+ 5 - 2
frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart

@@ -159,8 +159,11 @@ class DatabaseController {
     );
     );
   }
   }
 
 
-  Future<Either<Unit, FlowyError>> moveRow(RowPB fromRow,
-      {RowPB? toRow, String? groupId}) {
+  Future<Either<Unit, FlowyError>> moveRow({
+    required RowPB fromRow,
+    required String groupId,
+    RowPB? toRow,
+  }) {
     return _databaseViewBackendSvc.moveRow(
     return _databaseViewBackendSvc.moveRow(
       fromRowId: fromRow.id,
       fromRowId: fromRow.id,
       toGroupId: groupId,
       toGroupId: groupId,

+ 4 - 6
frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart

@@ -46,15 +46,13 @@ class DatabaseViewBackendService {
 
 
   Future<Either<Unit, FlowyError>> moveRow({
   Future<Either<Unit, FlowyError>> moveRow({
     required String fromRowId,
     required String fromRowId,
-    required String? toGroupId,
-    required String? toRowId,
+    required String toGroupId,
+    String? toRowId,
   }) {
   }) {
     var payload = MoveGroupRowPayloadPB.create()
     var payload = MoveGroupRowPayloadPB.create()
       ..viewId = viewId
       ..viewId = viewId
-      ..fromRowId = fromRowId;
-    if (toGroupId != null) {
-      payload.toGroupId = toGroupId;
-    }
+      ..fromRowId = fromRowId
+      ..toGroupId = toGroupId;
 
 
     if (toRowId != null) {
     if (toRowId != null) {
       payload.toRowId = toRowId;
       payload.toRowId = toRowId;

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

@@ -54,7 +54,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
         final toRow = groupControllers[groupId]?.rowAtIndex(toIndex);
         final toRow = groupControllers[groupId]?.rowAtIndex(toIndex);
         if (fromRow != null) {
         if (fromRow != null) {
           _databaseController.moveRow(
           _databaseController.moveRow(
-            fromRow,
+            fromRow: fromRow,
             toRow: toRow,
             toRow: toRow,
             groupId: groupId,
             groupId: groupId,
           );
           );
@@ -70,7 +70,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
         final toRow = groupControllers[toGroupId]?.rowAtIndex(toIndex);
         final toRow = groupControllers[toGroupId]?.rowAtIndex(toIndex);
         if (fromRow != null) {
         if (fromRow != null) {
           _databaseController.moveRow(
           _databaseController.moveRow(
-            fromRow,
+            fromRow: fromRow,
             toRow: toRow,
             toRow: toRow,
             groupId: toGroupId,
             groupId: toGroupId,
           );
           );

+ 1 - 0
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/toolbar/board_toolbar.dart

@@ -65,6 +65,7 @@ class _SettingButtonState extends State<_SettingButton> {
     return AppFlowyPopover(
     return AppFlowyPopover(
       controller: popoverController,
       controller: popoverController,
       direction: PopoverDirection.leftWithTopAligned,
       direction: PopoverDirection.leftWithTopAligned,
+      offset: const Offset(-8, 0),
       triggerActions: PopoverTriggerFlags.none,
       triggerActions: PopoverTriggerFlags.none,
       constraints: BoxConstraints.loose(const Size(260, 400)),
       constraints: BoxConstraints.loose(const Size(260, 400)),
       margin: EdgeInsets.zero,
       margin: EdgeInsets.zero,

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

@@ -87,7 +87,7 @@ class _SwitchFieldButton extends StatelessWidget {
       asBarrier: true,
       asBarrier: true,
       triggerActions: PopoverTriggerFlags.click,
       triggerActions: PopoverTriggerFlags.click,
       mutex: popoverMutex,
       mutex: popoverMutex,
-      offset: const Offset(20, 0),
+      offset: const Offset(8, 0),
       popupBuilder: (popOverContext) {
       popupBuilder: (popOverContext) {
         return FieldTypeList(onSelectField: (newFieldType) {
         return FieldTypeList(onSelectField: (newFieldType) {
           context
           context

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

@@ -80,7 +80,7 @@ class DateTypeOptionWidget extends TypeOptionWidget {
       mutex: popoverMutex,
       mutex: popoverMutex,
       asBarrier: true,
       asBarrier: true,
       triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
       triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
-      offset: const Offset(20, 0),
+      offset: const Offset(8, 0),
       constraints: BoxConstraints.loose(const Size(460, 440)),
       constraints: BoxConstraints.loose(const Size(460, 440)),
       popupBuilder: (popoverContext) {
       popupBuilder: (popoverContext) {
         return DateFormatList(
         return DateFormatList(
@@ -107,7 +107,7 @@ class DateTypeOptionWidget extends TypeOptionWidget {
       mutex: popoverMutex,
       mutex: popoverMutex,
       asBarrier: true,
       asBarrier: true,
       triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
       triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
-      offset: const Offset(20, 0),
+      offset: const Offset(8, 0),
       constraints: BoxConstraints.loose(const Size(460, 440)),
       constraints: BoxConstraints.loose(const Size(460, 440)),
       popupBuilder: (BuildContext popoverContext) {
       popupBuilder: (BuildContext popoverContext) {
         return TimeFormatList(
         return TimeFormatList(

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

@@ -77,7 +77,7 @@ class NumberTypeOptionWidget extends TypeOptionWidget {
               mutex: popoverMutex,
               mutex: popoverMutex,
               triggerActions:
               triggerActions:
                   PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
                   PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
-              offset: const Offset(20, 0),
+              offset: const Offset(8, 0),
               constraints: BoxConstraints.loose(const Size(460, 440)),
               constraints: BoxConstraints.loose(const Size(460, 440)),
               margin: EdgeInsets.zero,
               margin: EdgeInsets.zero,
               child: Padding(
               child: Padding(

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/select_option.dart

@@ -203,7 +203,7 @@ class _OptionCellState extends State<_OptionCell> {
     return AppFlowyPopover(
     return AppFlowyPopover(
       controller: _popoverController,
       controller: _popoverController,
       mutex: widget.popoverMutex,
       mutex: widget.popoverMutex,
-      offset: const Offset(20, 0),
+      offset: const Offset(8, 0),
       margin: EdgeInsets.zero,
       margin: EdgeInsets.zero,
       asBarrier: true,
       asBarrier: true,
       constraints: BoxConstraints.loose(const Size(460, 460)),
       constraints: BoxConstraints.loose(const Size(460, 460)),

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/sort_editor.dart

@@ -176,7 +176,7 @@ class _AddSortButtonState extends State<_AddSortButton> {
       mutex: widget.popoverMutex,
       mutex: widget.popoverMutex,
       direction: PopoverDirection.bottomWithLeftAligned,
       direction: PopoverDirection.bottomWithLeftAligned,
       constraints: BoxConstraints.loose(const Size(200, 300)),
       constraints: BoxConstraints.loose(const Size(200, 300)),
-      offset: const Offset(0, 10),
+      offset: const Offset(0, 8),
       triggerActions: PopoverTriggerFlags.none,
       triggerActions: PopoverTriggerFlags.none,
       asBarrier: true,
       asBarrier: true,
       child: SizedBox(
       child: SizedBox(

+ 18 - 15
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/filter_button.dart

@@ -30,20 +30,23 @@ class _FilterButtonState extends State<FilterButton> {
 
 
         return _wrapPopover(
         return _wrapPopover(
           context,
           context,
-          FlowyTextButton(
-            LocaleKeys.grid_settings_filter.tr(),
-            fontColor: textColor,
-            fillColor: Colors.transparent,
-            hoverColor: AFThemeExtension.of(context).lightGreyHover,
-            padding: GridSize.typeOptionContentInsets,
-            onPressed: () {
-              final bloc = context.read<GridFilterMenuBloc>();
-              if (bloc.state.filters.isEmpty) {
-                _popoverController.show();
-              } else {
-                bloc.add(const GridFilterMenuEvent.toggleMenu());
-              }
-            },
+          SizedBox(
+            height: 26,
+            child: FlowyTextButton(
+              LocaleKeys.grid_settings_filter.tr(),
+              fontColor: textColor,
+              fillColor: Colors.transparent,
+              hoverColor: AFThemeExtension.of(context).lightGreyHover,
+              padding: GridSize.typeOptionContentInsets,
+              onPressed: () {
+                final bloc = context.read<GridFilterMenuBloc>();
+                if (bloc.state.filters.isEmpty) {
+                  _popoverController.show();
+                } else {
+                  bloc.add(const GridFilterMenuEvent.toggleMenu());
+                }
+              },
+            ),
           ),
           ),
         );
         );
       },
       },
@@ -55,7 +58,7 @@ class _FilterButtonState extends State<FilterButton> {
       controller: _popoverController,
       controller: _popoverController,
       direction: PopoverDirection.bottomWithLeftAligned,
       direction: PopoverDirection.bottomWithLeftAligned,
       constraints: BoxConstraints.loose(const Size(200, 300)),
       constraints: BoxConstraints.loose(const Size(200, 300)),
-      offset: const Offset(0, 10),
+      offset: const Offset(0, 8),
       triggerActions: PopoverTriggerFlags.none,
       triggerActions: PopoverTriggerFlags.none,
       child: child,
       child: child,
       popupBuilder: (BuildContext context) {
       popupBuilder: (BuildContext context) {

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_property.dart

@@ -109,7 +109,7 @@ class _GridPropertyCellState extends State<_GridPropertyCell> {
     return AppFlowyPopover(
     return AppFlowyPopover(
       mutex: widget.popoverMutex,
       mutex: widget.popoverMutex,
       controller: _popoverController,
       controller: _popoverController,
-      offset: const Offset(20, 0),
+      offset: const Offset(8, 0),
       direction: PopoverDirection.leftWithTopAligned,
       direction: PopoverDirection.leftWithTopAligned,
       constraints: BoxConstraints.loose(const Size(240, 400)),
       constraints: BoxConstraints.loose(const Size(240, 400)),
       triggerActions: PopoverTriggerFlags.none,
       triggerActions: PopoverTriggerFlags.none,

+ 19 - 16
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/setting_button.dart

@@ -41,23 +41,26 @@ class _SettingButtonState extends State<SettingButton> {
         );
         );
       },
       },
       builder: (context, settingContext) {
       builder: (context, settingContext) {
-        return AppFlowyPopover(
-          controller: _popoverController,
-          constraints: BoxConstraints.loose(const Size(260, 400)),
-          direction: PopoverDirection.bottomWithLeftAligned,
-          offset: const Offset(0, 10),
-          margin: EdgeInsets.zero,
-          triggerActions: PopoverTriggerFlags.none,
-          child: FlowyTextButton(
-            LocaleKeys.settings_title.tr(),
-            fillColor: Colors.transparent,
-            hoverColor: AFThemeExtension.of(context).lightGreyHover,
-            padding: GridSize.typeOptionContentInsets,
-            onPressed: () => _popoverController.show(),
+        return SizedBox(
+          height: 26,
+          child: AppFlowyPopover(
+            controller: _popoverController,
+            constraints: BoxConstraints.loose(const Size(260, 400)),
+            direction: PopoverDirection.bottomWithLeftAligned,
+            offset: const Offset(0, 8),
+            margin: EdgeInsets.zero,
+            triggerActions: PopoverTriggerFlags.none,
+            child: FlowyTextButton(
+              LocaleKeys.settings_title.tr(),
+              fillColor: Colors.transparent,
+              hoverColor: AFThemeExtension.of(context).lightGreyHover,
+              padding: GridSize.typeOptionContentInsets,
+              onPressed: () => _popoverController.show(),
+            ),
+            popupBuilder: (BuildContext context) {
+              return _GridSettingListPopover(settingContext: settingContext);
+            },
           ),
           ),
-          popupBuilder: (BuildContext context) {
-            return _GridSettingListPopover(settingContext: settingContext);
-          },
         );
         );
       },
       },
     );
     );

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/sort_button.dart

@@ -58,7 +58,7 @@ class _SortButtonState extends State<SortButton> {
       controller: _popoverController,
       controller: _popoverController,
       direction: PopoverDirection.bottomWithLeftAligned,
       direction: PopoverDirection.bottomWithLeftAligned,
       constraints: BoxConstraints.loose(const Size(200, 300)),
       constraints: BoxConstraints.loose(const Size(200, 300)),
-      offset: const Offset(0, 10),
+      offset: const Offset(0, 8),
       margin: const EdgeInsets.all(6),
       margin: const EdgeInsets.all(6),
       triggerActions: PopoverTriggerFlags.none,
       triggerActions: PopoverTriggerFlags.none,
       child: child,
       child: child,

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

@@ -145,7 +145,7 @@ class _ChecklistOptionCellState extends State<_ChecklistOptionCell> {
   Widget _wrapPopover(Widget child) {
   Widget _wrapPopover(Widget child) {
     return AppFlowyPopover(
     return AppFlowyPopover(
       controller: _popoverController,
       controller: _popoverController,
-      offset: const Offset(20, 0),
+      offset: const Offset(8, 0),
       asBarrier: true,
       asBarrier: true,
       constraints: BoxConstraints.loose(const Size(200, 300)),
       constraints: BoxConstraints.loose(const Size(200, 300)),
       mutex: widget.popoverMutex,
       mutex: widget.popoverMutex,

+ 3 - 3
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart

@@ -376,7 +376,7 @@ class _DateTypeOptionButton extends StatelessWidget {
         return AppFlowyPopover(
         return AppFlowyPopover(
           mutex: popoverMutex,
           mutex: popoverMutex,
           triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
           triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
-          offset: const Offset(20, 0),
+          offset: const Offset(8, 0),
           margin: EdgeInsets.zero,
           margin: EdgeInsets.zero,
           constraints: BoxConstraints.loose(const Size(140, 100)),
           constraints: BoxConstraints.loose(const Size(140, 100)),
           child: Padding(
           child: Padding(
@@ -431,7 +431,7 @@ class _CalDateTimeSettingState extends State<_CalDateTimeSetting> {
       AppFlowyPopover(
       AppFlowyPopover(
         mutex: timeSettingPopoverMutex,
         mutex: timeSettingPopoverMutex,
         triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
         triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
-        offset: const Offset(20, 0),
+        offset: const Offset(8, 0),
         popupBuilder: (BuildContext context) {
         popupBuilder: (BuildContext context) {
           return DateFormatList(
           return DateFormatList(
             selectedFormat: widget.dateTypeOptionPB.dateFormat,
             selectedFormat: widget.dateTypeOptionPB.dateFormat,
@@ -449,7 +449,7 @@ class _CalDateTimeSettingState extends State<_CalDateTimeSetting> {
       AppFlowyPopover(
       AppFlowyPopover(
         mutex: timeSettingPopoverMutex,
         mutex: timeSettingPopoverMutex,
         triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
         triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
-        offset: const Offset(20, 0),
+        offset: const Offset(8, 0),
         popupBuilder: (BuildContext context) {
         popupBuilder: (BuildContext context) {
           return TimeFormatList(
           return TimeFormatList(
               selectedFormat: widget.dateTypeOptionPB.timeFormat,
               selectedFormat: widget.dateTypeOptionPB.timeFormat,

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart

@@ -285,7 +285,7 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
     );
     );
     return AppFlowyPopover(
     return AppFlowyPopover(
       controller: _popoverController,
       controller: _popoverController,
-      offset: const Offset(20, 0),
+      offset: const Offset(8, 0),
       margin: EdgeInsets.zero,
       margin: EdgeInsets.zero,
       asBarrier: true,
       asBarrier: true,
       constraints: BoxConstraints.loose(const Size(200, 460)),
       constraints: BoxConstraints.loose(const Size(200, 460)),

+ 2 - 2
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/url_cell/url_cell.dart

@@ -127,7 +127,7 @@ class _GridURLCellState extends GridCellState<GridURLCell> {
             constraints: BoxConstraints.loose(const Size(300, 160)),
             constraints: BoxConstraints.loose(const Size(300, 160)),
             direction: PopoverDirection.bottomWithLeftAligned,
             direction: PopoverDirection.bottomWithLeftAligned,
             triggerActions: PopoverTriggerFlags.none,
             triggerActions: PopoverTriggerFlags.none,
-            offset: const Offset(0, 20),
+            offset: const Offset(0, 8),
             child: SizedBox.expand(
             child: SizedBox.expand(
               child: GestureDetector(
               child: GestureDetector(
                 child: Align(alignment: Alignment.centerLeft, child: richText),
                 child: Align(alignment: Alignment.centerLeft, child: richText),
@@ -210,7 +210,7 @@ class _EditURLAccessoryState extends State<_EditURLAccessory>
       constraints: BoxConstraints.loose(const Size(300, 160)),
       constraints: BoxConstraints.loose(const Size(300, 160)),
       controller: _popoverController,
       controller: _popoverController,
       direction: PopoverDirection.bottomWithLeftAligned,
       direction: PopoverDirection.bottomWithLeftAligned,
-      offset: const Offset(0, 20),
+      offset: const Offset(0, 8),
       child: svgWidget(
       child: svgWidget(
         "editor/edit",
         "editor/edit",
         color: Theme.of(context).colorScheme.onSurface,
         color: Theme.of(context).colorScheme.onSurface,

+ 52 - 44
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_image_picker_bloc.dart

@@ -12,13 +12,15 @@ import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:dartz/dartz.dart';
 import 'package:dartz/dartz.dart';
 import 'package:http/http.dart' as http;
 import 'package:http/http.dart' as http;
 import 'package:shared_preferences/shared_preferences.dart';
 import 'package:shared_preferences/shared_preferences.dart';
-import 'package:path/path.dart' as path;
+import 'package:path/path.dart' as p;
 import 'change_cover_popover.dart';
 import 'change_cover_popover.dart';
 
 
 part 'cover_image_picker_bloc.freezed.dart';
 part 'cover_image_picker_bloc.freezed.dart';
 
 
 class CoverImagePickerBloc
 class CoverImagePickerBloc
     extends Bloc<CoverImagePickerEvent, CoverImagePickerState> {
     extends Bloc<CoverImagePickerEvent, CoverImagePickerState> {
+  static const allowedExtensions = ['jpg', 'png', 'jpeg'];
+
   CoverImagePickerBloc() : super(const CoverImagePickerState.initial()) {
   CoverImagePickerBloc() : super(const CoverImagePickerState.initial()) {
     on<CoverImagePickerEvent>(
     on<CoverImagePickerEvent>(
       (event, emit) async {
       (event, emit) async {
@@ -28,7 +30,7 @@ class CoverImagePickerBloc
           },
           },
           urlSubmit: (UrlSubmit urlSubmit) async {
           urlSubmit: (UrlSubmit urlSubmit) async {
             emit(const CoverImagePickerState.loading());
             emit(const CoverImagePickerState.loading());
-            final validateImage = await _validateUrl(urlSubmit.path);
+            final validateImage = await _validateURL(urlSubmit.path);
             if (validateImage) {
             if (validateImage) {
               emit(CoverImagePickerState.networkImage(left(urlSubmit.path)));
               emit(CoverImagePickerState.networkImage(left(urlSubmit.path)));
             } else {
             } else {
@@ -86,28 +88,22 @@ class CoverImagePickerBloc
     if (state is FileImagePicked) {
     if (state is FileImagePicked) {
       try {
       try {
         final path = state.path;
         final path = state.path;
-        final newPath = '$directory/${path.split("\\").last}';
+        final newPath = p.join(directory, p.split(path).last);
         final newFile = await File(path).copy(newPath);
         final newFile = await File(path).copy(newPath);
         imagePaths.add(newFile.path);
         imagePaths.add(newFile.path);
-        await prefs.setStringList(kLocalImagesKey, imagePaths);
-        return imagePaths;
       } catch (e) {
       } catch (e) {
         return null;
         return null;
       }
       }
     } else if (state is NetworkImagePicked) {
     } else if (state is NetworkImagePicked) {
       try {
       try {
-        String? url = state.successOrFail.fold((path) => path, (r) => null);
+        final url = state.successOrFail.fold((path) => path, (r) => null);
         if (url != null) {
         if (url != null) {
           final response = await http.get(Uri.parse(url));
           final response = await http.get(Uri.parse(url));
-          final newPath =
-              "$directory/IMG_$_timeStampString.${_getExtention(url)}";
-
+          final newPath = p.join(directory, _networkImageName(url));
           final imageFile = File(newPath);
           final imageFile = File(newPath);
           await imageFile.create();
           await imageFile.create();
           await imageFile.writeAsBytes(response.bodyBytes);
           await imageFile.writeAsBytes(response.bodyBytes);
           imagePaths.add(imageFile.absolute.path);
           imagePaths.add(imageFile.absolute.path);
-          await prefs.setStringList(kLocalImagesKey, imagePaths);
-          return imagePaths;
         } else {
         } else {
           return null;
           return null;
         }
         }
@@ -115,59 +111,71 @@ class CoverImagePickerBloc
         return null;
         return null;
       }
       }
     }
     }
+    await prefs.setStringList(kLocalImagesKey, imagePaths);
+    return imagePaths;
   }
   }
 
 
-  _pickImages() async {
-    FilePickerResult? result = await getIt<FilePickerService>().pickFiles(
+  Future<String?> _pickImages() async {
+    final result = await getIt<FilePickerService>().pickFiles(
       dialogTitle: LocaleKeys.document_plugins_cover_addLocalImage.tr(),
       dialogTitle: LocaleKeys.document_plugins_cover_addLocalImage.tr(),
       allowMultiple: false,
       allowMultiple: false,
       type: fp.FileType.image,
       type: fp.FileType.image,
-      allowedExtensions: ['jpg', 'png', 'jpeg'],
+      allowedExtensions: allowedExtensions,
     );
     );
     if (result != null && result.files.isNotEmpty) {
     if (result != null && result.files.isNotEmpty) {
-      final path = result.files.first.path;
-      if (path != null) {
-        return path;
-      } else {
-        return null;
-      }
+      return result.files.first.path;
     }
     }
     return null;
     return null;
   }
   }
 
 
   Future<String> _coverPath() async {
   Future<String> _coverPath() async {
     final directory = await getIt<SettingsLocationCubit>().fetchLocation();
     final directory = await getIt<SettingsLocationCubit>().fetchLocation();
-    return Directory(path.join(directory, 'covers'))
+    return Directory(p.join(directory, 'covers'))
         .create(recursive: true)
         .create(recursive: true)
         .then((value) => value.path);
         .then((value) => value.path);
   }
   }
 
 
-  String get _timeStampString =>
-      DateTime.now().millisecondsSinceEpoch.toString();
-
-  String? _getExtention(String path) => path.contains(".jpg")
-      ? "jpg"
-      : path.contains(".png")
-          ? "png"
-          : path.contains(".jpeg")
-              ? "jpeg"
-              : (path.contains("auto=format") && path.contains("unsplash"))
-                  ? "jpeg"
-                  : null;
+  String _networkImageName(String url) {
+    return 'IMG_${DateTime.now().millisecondsSinceEpoch.toString()}.${_getExtention(
+      url,
+      fromNetwork: true,
+    )}';
+  }
 
 
-  _validateUrl(String path) async {
-    if (_getExtention(path) != null) {
-      try {
-        final response = await http.get(Uri.parse(path));
-        if (response.statusCode == 200) {
-          return true;
-        } else {
-          return false;
-        }
-      } catch (e) {
-        return false;
+  String? _getExtention(
+    String path, {
+    bool fromNetwork = false,
+  }) {
+    String? ext;
+    if (!fromNetwork) {
+      final extension = p.extension(path);
+      if (extension.isEmpty) {
+        return null;
       }
       }
+      ext = extension.substring(1);
     } else {
     } else {
+      final uri = Uri.parse(path);
+      final paramters = uri.queryParameters;
+      final dl = paramters['dl'];
+      if (dl != null) {
+        ext = p.extension(dl).substring(1);
+      }
+    }
+    if (allowedExtensions.contains(ext)) {
+      return ext;
+    }
+    return null;
+  }
+
+  Future<bool> _validateURL(String path) async {
+    final extension = _getExtention(path, fromNetwork: true);
+    if (extension == null) {
+      return false;
+    }
+    try {
+      final response = await http.head(Uri.parse(path));
+      return response.statusCode == 200;
+    } catch (e) {
       return false;
       return false;
     }
     }
   }
   }

+ 1 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart

@@ -82,6 +82,7 @@ class ShareActionList extends StatelessWidget {
     final docShareBloc = context.read<DocShareBloc>();
     final docShareBloc = context.read<DocShareBloc>();
     return PopoverActionList<ShareActionWrapper>(
     return PopoverActionList<ShareActionWrapper>(
       direction: PopoverDirection.bottomWithCenterAligned,
       direction: PopoverDirection.bottomWithCenterAligned,
+      offset: const Offset(0, 8),
       actions: ShareAction.values
       actions: ShareAction.values
           .map((action) => ShareActionWrapper(action))
           .map((action) => ShareActionWrapper(action))
           .toList(),
           .toList(),

+ 7 - 4
frontend/appflowy_flutter/lib/user/application/user_listener.dart

@@ -83,11 +83,10 @@ class UserWorkspaceListener {
       PublishNotifier();
       PublishNotifier();
 
 
   FolderNotificationListener? _listener;
   FolderNotificationListener? _listener;
-  final UserProfilePB _userProfile;
 
 
   UserWorkspaceListener({
   UserWorkspaceListener({
     required UserProfilePB userProfile,
     required UserProfilePB userProfile,
-  }) : _userProfile = userProfile;
+  });
 
 
   void start({
   void start({
     void Function(AuthNotifyValue)? onAuthChanged,
     void Function(AuthNotifyValue)? onAuthChanged,
@@ -106,14 +105,18 @@ class UserWorkspaceListener {
       _settingChangedNotifier?.addPublishListener(onSettingUpdated);
       _settingChangedNotifier?.addPublishListener(onSettingUpdated);
     }
     }
 
 
+    // The "current-workspace" is predefined in the backend. Do not try to
+    // modify it
     _listener = FolderNotificationListener(
     _listener = FolderNotificationListener(
-      objectId: _userProfile.token,
+      objectId: "current-workspace",
       handler: _handleObservableType,
       handler: _handleObservableType,
     );
     );
   }
   }
 
 
   void _handleObservableType(
   void _handleObservableType(
-      FolderNotification ty, Either<Uint8List, FlowyError> result) {
+    FolderNotification ty,
+    Either<Uint8List, FlowyError> result,
+  ) {
     switch (ty) {
     switch (ty) {
       case FolderNotification.DidCreateWorkspace:
       case FolderNotification.DidCreateWorkspace:
       case FolderNotification.DidDeleteWorkspace:
       case FolderNotification.DidDeleteWorkspace:

+ 1 - 0
frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/add_button.dart

@@ -48,6 +48,7 @@ class AddButton extends StatelessWidget {
     return PopoverActionList<PopoverAction>(
     return PopoverActionList<PopoverAction>(
       direction: PopoverDirection.bottomWithLeftAligned,
       direction: PopoverDirection.bottomWithLeftAligned,
       actions: actions,
       actions: actions,
+      offset: const Offset(0, 8),
       buildChild: (controller) {
       buildChild: (controller) {
         return FlowyIconButton(
         return FlowyIconButton(
           width: 22,
           width: 22,

+ 1 - 0
frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart

@@ -42,6 +42,7 @@ class BubbleActionList extends StatelessWidget {
     return PopoverActionList<PopoverAction>(
     return PopoverActionList<PopoverAction>(
       direction: PopoverDirection.topWithRightAligned,
       direction: PopoverDirection.topWithRightAligned,
       actions: actions,
       actions: actions,
+      offset: const Offset(0, -8),
       buildChild: (controller) {
       buildChild: (controller) {
         return FlowyTextButton(
         return FlowyTextButton(
           '?',
           '?',

+ 3 - 0
frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart

@@ -13,6 +13,7 @@ class PopoverActionList<T extends PopoverAction> extends StatefulWidget {
   final Widget Function(PopoverController) buildChild;
   final Widget Function(PopoverController) buildChild;
   final VoidCallback? onClosed;
   final VoidCallback? onClosed;
   final bool asBarrier;
   final bool asBarrier;
+  final Offset offset;
 
 
   const PopoverActionList({
   const PopoverActionList({
     required this.actions,
     required this.actions,
@@ -22,6 +23,7 @@ class PopoverActionList<T extends PopoverAction> extends StatefulWidget {
     this.onClosed,
     this.onClosed,
     this.direction = PopoverDirection.rightWithTopAligned,
     this.direction = PopoverDirection.rightWithTopAligned,
     this.asBarrier = false,
     this.asBarrier = false,
+    this.offset = Offset.zero,
     this.constraints = const BoxConstraints(
     this.constraints = const BoxConstraints(
       minWidth: 120,
       minWidth: 120,
       maxWidth: 460,
       maxWidth: 460,
@@ -54,6 +56,7 @@ class _PopoverActionListState<T extends PopoverAction>
       constraints: widget.constraints,
       constraints: widget.constraints,
       direction: widget.direction,
       direction: widget.direction,
       mutex: widget.mutex,
       mutex: widget.mutex,
+      offset: widget.offset,
       triggerActions: PopoverTriggerFlags.none,
       triggerActions: PopoverTriggerFlags.none,
       onClose: widget.onClosed,
       onClose: widget.onClosed,
       popupBuilder: (BuildContext popoverContext) {
       popupBuilder: (BuildContext popoverContext) {

+ 31 - 0
frontend/appflowy_tauri/src/appflowy_app/components/tests/TestGroup.tsx

@@ -69,6 +69,37 @@ async function moveKanbanBoardRow() {
   // Create row in no status group
   // Create row in no status group
   const firstGroup = databaseController.groups.getValue()[1];
   const firstGroup = databaseController.groups.getValue()[1];
   const secondGroup = databaseController.groups.getValue()[2];
   const secondGroup = databaseController.groups.getValue()[2];
+  // subscribe the group changes
+  firstGroup.subscribe({
+    onRemoveRow: (groupId, deleteRowId) => {
+      console.log(groupId + 'did remove:' + deleteRowId);
+    },
+    onInsertRow: (groupId, rowPB) => {
+      console.log(groupId + 'did insert:' + rowPB.id);
+    },
+    onUpdateRow: (groupId, rowPB) => {
+      console.log(groupId + 'did update:' + rowPB.id);
+    },
+    onCreateRow: (groupId, rowPB) => {
+      console.log(groupId + 'did create:' + rowPB.id);
+    },
+  });
+
+  secondGroup.subscribe({
+    onRemoveRow: (groupId, deleteRowId) => {
+      console.log(groupId + 'did remove:' + deleteRowId);
+    },
+    onInsertRow: (groupId, rowPB) => {
+      console.log(groupId + 'did insert:' + rowPB.id);
+    },
+    onUpdateRow: (groupId, rowPB) => {
+      console.log(groupId + 'did update:' + rowPB.id);
+    },
+    onCreateRow: (groupId, rowPB) => {
+      console.log(groupId + 'did create:' + rowPB.id);
+    },
+  });
+
   const row = firstGroup.rowAtIndex(0).unwrap();
   const row = firstGroup.rowAtIndex(0).unwrap();
   await databaseController.moveRow(row.id, secondGroup.groupId);
   await databaseController.moveRow(row.id, secondGroup.groupId);
 
 

+ 13 - 5
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts

@@ -61,12 +61,19 @@ export class DatabaseBackendService {
     return DatabaseEventCreateRow(payload);
     return DatabaseEventCreateRow(payload);
   };
   };
 
 
-  /// Move a row to another group
-  moveRow = (rowId: string, groupId?: string) => {
-    const payload = MoveGroupRowPayloadPB.fromObject({ view_id: this.viewId, from_row_id: rowId });
-    if (groupId !== undefined) {
-      payload.to_group_id = groupId;
+  /// Move the row from one group to another group
+  /// [groupId] can be the moving row's group id or others.
+  /// [toRowId] is used to locate the moving row location.
+  moveGroupRow = (fromRowId: string, groupId: string, toRowId?: string) => {
+    const payload = MoveGroupRowPayloadPB.fromObject({
+      view_id: this.viewId,
+      from_row_id: fromRowId,
+      to_group_id: groupId,
+    });
+    if (toRowId !== undefined) {
+      payload.to_row_id = toRowId;
     }
     }
+
     return DatabaseEventMoveGroupRow(payload);
     return DatabaseEventMoveGroupRow(payload);
   };
   };
 
 
@@ -106,6 +113,7 @@ export class DatabaseBackendService {
   };
   };
 
 
   /// Get all groups in database
   /// Get all groups in database
+  /// It should only call once after the board open
   loadGroups = () => {
   loadGroups = () => {
     const payload = DatabaseViewIdPB.fromObject({ value: this.viewId });
     const payload = DatabaseViewIdPB.fromObject({ value: this.viewId });
     return DatabaseEventGetGroups(payload);
     return DatabaseEventGetGroups(payload);

+ 1 - 1
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_controller.ts

@@ -76,7 +76,7 @@ export class DatabaseController {
   };
   };
 
 
   moveRow = (rowId: string, groupId: string) => {
   moveRow = (rowId: string, groupId: string) => {
-    return this.backendService.moveRow(rowId, groupId);
+    return this.backendService.moveGroupRow(rowId, groupId);
   };
   };
 
 
   exchangeRow = async (fromRowId: string, toRowId: string) => {
   exchangeRow = async (fromRowId: string, toRowId: string) => {

+ 2 - 1
frontend/rust-lib/flowy-database/src/services/group/controller_impls/select_option_controller/util.rs

@@ -115,6 +115,8 @@ pub fn move_group_row(
     }
     }
 
 
     // Update the corresponding row's cell content.
     // Update the corresponding row's cell content.
+    // If the from_index is none which means the row is not belong to this group before and
+    // it is moved from other groups.
     if from_index.is_none() {
     if from_index.is_none() {
       let cell_rev = make_inserted_cell_rev(&group.id, field_rev);
       let cell_rev = make_inserted_cell_rev(&group.id, field_rev);
       if let Some(cell_rev) = cell_rev {
       if let Some(cell_rev) = cell_rev {
@@ -126,7 +128,6 @@ pub fn move_group_row(
         row_changeset
         row_changeset
           .cell_by_field_id
           .cell_by_field_id
           .insert(field_rev.id.clone(), cell_rev);
           .insert(field_rev.id.clone(), cell_rev);
-        changeset.updated_rows.push(RowPB::from(*row_rev));
       }
       }
     }
     }
   }
   }

+ 17 - 15
frontend/rust-lib/flowy-folder/src/services/workspace/controller.rs

@@ -11,6 +11,7 @@ use crate::{
 };
 };
 use flowy_sqlite::kv::KV;
 use flowy_sqlite::kv::KV;
 use folder_model::{AppRevision, WorkspaceRevision};
 use folder_model::{AppRevision, WorkspaceRevision};
+use lib_dispatch::prelude::ToBytes;
 use std::sync::Arc;
 use std::sync::Arc;
 
 
 pub struct WorkspaceController {
 pub struct WorkspaceController {
@@ -41,7 +42,6 @@ impl WorkspaceController {
   ) -> Result<WorkspaceRevision, FlowyError> {
   ) -> Result<WorkspaceRevision, FlowyError> {
     let workspace = self.create_workspace_on_server(params.clone()).await?;
     let workspace = self.create_workspace_on_server(params.clone()).await?;
     let user_id = self.user.user_id()?;
     let user_id = self.user.user_id()?;
-    let token = self.user.token()?;
     let workspaces = self
     let workspaces = self
       .persistence
       .persistence
       .begin_transaction(|transaction| {
       .begin_transaction(|transaction| {
@@ -53,9 +53,7 @@ impl WorkspaceController {
       .map(|workspace_rev| workspace_rev.into())
       .map(|workspace_rev| workspace_rev.into())
       .collect();
       .collect();
     let repeated_workspace = RepeatedWorkspacePB { items: workspaces };
     let repeated_workspace = RepeatedWorkspacePB { items: workspaces };
-    send_notification(&token, FolderNotification::DidCreateWorkspace)
-      .payload(repeated_workspace)
-      .send();
+    send_workspace_notification(FolderNotification::DidCreateWorkspace, repeated_workspace);
     set_current_workspace(&user_id, &workspace.id);
     set_current_workspace(&user_id, &workspace.id);
     Ok(workspace)
     Ok(workspace)
   }
   }
@@ -76,9 +74,7 @@ impl WorkspaceController {
       })
       })
       .await?;
       .await?;
 
 
-    send_notification(&workspace_id, FolderNotification::DidUpdateWorkspace)
-      .payload(workspace)
-      .send();
+    send_workspace_notification(FolderNotification::DidUpdateWorkspace, workspace);
     self.update_workspace_on_server(params)?;
     self.update_workspace_on_server(params)?;
 
 
     Ok(())
     Ok(())
@@ -87,7 +83,6 @@ impl WorkspaceController {
   #[allow(dead_code)]
   #[allow(dead_code)]
   pub(crate) async fn delete_workspace(&self, workspace_id: &str) -> Result<(), FlowyError> {
   pub(crate) async fn delete_workspace(&self, workspace_id: &str) -> Result<(), FlowyError> {
     let user_id = self.user.user_id()?;
     let user_id = self.user.user_id()?;
-    let token = self.user.token()?;
     let repeated_workspace = self
     let repeated_workspace = self
       .persistence
       .persistence
       .begin_transaction(|transaction| {
       .begin_transaction(|transaction| {
@@ -95,9 +90,8 @@ impl WorkspaceController {
         self.read_workspaces(None, &user_id, &transaction)
         self.read_workspaces(None, &user_id, &transaction)
       })
       })
       .await?;
       .await?;
-    send_notification(&token, FolderNotification::DidDeleteWorkspace)
-      .payload(repeated_workspace)
-      .send();
+
+    send_workspace_notification(FolderNotification::DidDeleteWorkspace, repeated_workspace);
     self.delete_workspace_on_server(workspace_id)?;
     self.delete_workspace_on_server(workspace_id)?;
     Ok(())
     Ok(())
   }
   }
@@ -224,7 +218,6 @@ pub async fn notify_workspace_setting_did_change(
   view_id: &str,
   view_id: &str,
 ) -> FlowyResult<()> {
 ) -> FlowyResult<()> {
   let user_id = folder_manager.user.user_id()?;
   let user_id = folder_manager.user.user_id()?;
-  let token = folder_manager.user.token()?;
   let workspace_id = get_current_workspace(&user_id)?;
   let workspace_id = get_current_workspace(&user_id)?;
 
 
   let workspace_setting = folder_manager
   let workspace_setting = folder_manager
@@ -250,11 +243,20 @@ pub async fn notify_workspace_setting_did_change(
       Ok(setting)
       Ok(setting)
     })
     })
     .await?;
     .await?;
+  send_workspace_notification(
+    FolderNotification::DidUpdateWorkspaceSetting,
+    workspace_setting,
+  );
+  Ok(())
+}
 
 
-  send_notification(&token, FolderNotification::DidUpdateWorkspaceSetting)
-    .payload(workspace_setting)
+/// The [CURRENT_WORKSPACE] represents as the current workspace that opened by the
+/// user. Only one workspace can be opened at a time.
+const CURRENT_WORKSPACE: &str = "current-workspace";
+fn send_workspace_notification<T: ToBytes>(ty: FolderNotification, payload: T) {
+  send_notification(CURRENT_WORKSPACE, ty)
+    .payload(payload)
     .send();
     .send();
-  Ok(())
 }
 }
 
 
 const CURRENT_WORKSPACE_ID: &str = "current_workspace_id";
 const CURRENT_WORKSPACE_ID: &str = "current_workspace_id";

+ 34 - 17
frontend/scripts/install_dev_env/install_linux.sh

@@ -6,39 +6,56 @@ RED="\e[31m"
 ENDCOLOR="\e[0m"
 ENDCOLOR="\e[0m"
 
 
 printMessage() {
 printMessage() {
-   printf "${YELLOW}AppFlowy : $1${ENDCOLOR}\n"
+    printf "${YELLOW}AppFlowy : $1${ENDCOLOR}\n"
 }
 }
 
 
 printSuccess() {
 printSuccess() {
-   printf "${GREEN}AppFlowy : $1${ENDCOLOR}\n"
+    printf "${GREEN}AppFlowy : $1${ENDCOLOR}\n"
 }
 }
 
 
 printError() {
 printError() {
-   printf "${RED}AppFlowy : $1${ENDCOLOR}\n"
+    printf "${RED}AppFlowy : $1${ENDCOLOR}\n"
 }
 }
 
 
-
 # Note: This script does not install applications which are installed by the package manager. There are too many package managers out there.
 # Note: This script does not install applications which are installed by the package manager. There are too many package managers out there.
 
 
-# Install Rust 
+# Install Rust
 printMessage "The Rust programming language is required to compile AppFlowy."
 printMessage "The Rust programming language is required to compile AppFlowy."
 printMessage "We can install it now if you don't already have it on your system."
 printMessage "We can install it now if you don't already have it on your system."
 
 
 read -p "$(printSuccess "Do you want to install Rust? [y/N]") " installrust
 read -p "$(printSuccess "Do you want to install Rust? [y/N]") " installrust
 
 
 if [ ${installrust^^} == "Y" ]; then
 if [ ${installrust^^} == "Y" ]; then
-   printMessage "Installing Rust."
-   curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
-   source $HOME/.cargo/env
-   rustup toolchain install stable
-   rustup default stable
+    printMessage "Installing Rust."
+    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
+    source $HOME/.cargo/env
+    rustup toolchain install stable
+    rustup default stable
 else
 else
-   printMessage "Skipping Rust installation."
+    printMessage "Skipping Rust installation."
 fi
 fi
 
 
-# Enable the flutter stable channel
 printMessage "Setting up Flutter"
 printMessage "Setting up Flutter"
-flutter channel stable
+# Get the current Flutter version
+FLUTTER_VERSION=$(flutter --version | grep -oP 'Flutter \K\S+')
+# Check if the current version is 3.3.10
+if [ "$FLUTTER_VERSION" = "3.3.10" ]; then
+    echo "Flutter version is already 3.3.10"
+else
+    # Get the path to the Flutter SDK
+    FLUTTER_PATH=$(which flutter)
+    FLUTTER_PATH=${FLUTTER_PATH%/bin/flutter}
+
+    current_dir=$(pwd)
+
+    cd $FLUTTER_PATH
+    # Use git to checkout version 3.3.10 of Flutter
+    git checkout 3.3.10
+    # Get back to current working directory
+    cd "$current_dir"
+
+    echo "Switched to Flutter version 3.3.10"
+fi
 
 
 # Enable linux desktop
 # Enable linux desktop
 flutter config --enable-linux-desktop
 flutter config --enable-linux-desktop
@@ -47,9 +64,9 @@ flutter config --enable-linux-desktop
 flutter doctor
 flutter doctor
 
 
 printMessage "Installing keybinder-3.0"
 printMessage "Installing keybinder-3.0"
-if command apt-get &> /dev/null; then
+if command apt-get &>/dev/null; then
     sudo apt-get install keybinder-3.0-dev
     sudo apt-get install keybinder-3.0-dev
-elif command dnf &> /dev/null; then
+elif command dnf &>/dev/null; then
     sudo dnf install keybinder3-devel
     sudo dnf install keybinder3-devel
 else
 else
     echo 'Your system is not supported, please install keybinder3 manually.'
     echo 'Your system is not supported, please install keybinder3 manually.'
@@ -59,11 +76,11 @@ fi
 printMessage "Setting up githooks."
 printMessage "Setting up githooks."
 git config core.hooksPath .githooks
 git config core.hooksPath .githooks
 
 
-# Install go-gitlint 
+# Install go-gitlint
 printMessage "Installing go-gitlint."
 printMessage "Installing go-gitlint."
 GOLINT_FILENAME="go-gitlint_1.1.0_linux_x86_64.tar.gz"
 GOLINT_FILENAME="go-gitlint_1.1.0_linux_x86_64.tar.gz"
 wget https://github.com/llorllale/go-gitlint/releases/download/1.1.0/${GOLINT_FILENAME}
 wget https://github.com/llorllale/go-gitlint/releases/download/1.1.0/${GOLINT_FILENAME}
-tar -zxv --directory .githooks/. -f ${GOLINT_FILENAME} gitlint 
+tar -zxv --directory .githooks/. -f ${GOLINT_FILENAME} gitlint
 rm ${GOLINT_FILENAME}
 rm ${GOLINT_FILENAME}
 
 
 # Change to the frontend directory
 # Change to the frontend directory

+ 26 - 8
frontend/scripts/install_dev_env/install_macos.sh

@@ -17,8 +17,7 @@ printError() {
    printf "${RED}AppFlowy : $1${ENDCOLOR}\n"
    printf "${RED}AppFlowy : $1${ENDCOLOR}\n"
 }
 }
 
 
-
-# Install Rust 
+# Install Rust
 printMessage "The Rust programming language is required to compile AppFlowy."
 printMessage "The Rust programming language is required to compile AppFlowy."
 printMessage "We can install it now if you don't already have it on your system."
 printMessage "We can install it now if you don't already have it on your system."
 
 
@@ -28,7 +27,7 @@ if [[ "${installrust:-N}" == [Yy] ]]; then
    printMessage "Installing Rust."
    printMessage "Installing Rust."
    brew install rustup-init
    brew install rustup-init
    rustup-init -y --default-toolchain=stable
    rustup-init -y --default-toolchain=stable
-  
+
    source "$HOME/.cargo/env"
    source "$HOME/.cargo/env"
 else
 else
    printMessage "Skipping Rust installation."
    printMessage "Skipping Rust installation."
@@ -36,11 +35,30 @@ fi
 
 
 # Install sqllite
 # Install sqllite
 printMessage "Installing sqlLite3."
 printMessage "Installing sqlLite3."
-brew install sqlite3 
+brew install sqlite3
 
 
-# Enable the flutter stable channel
 printMessage "Setting up Flutter"
 printMessage "Setting up Flutter"
-flutter channel stable
+
+# Get the current Flutter version
+FLUTTER_VERSION=$(flutter --version | grep -oE 'Flutter [^ ]+' | grep -oE '[^ ]+$')
+# Check if the current version is 3.3.10
+if [ "$FLUTTER_VERSION" = "3.3.10" ]; then
+   echo "Flutter version is already 3.3.10"
+else
+   # Get the path to the Flutter SDK
+   FLUTTER_PATH=$(which flutter)
+   FLUTTER_PATH=${FLUTTER_PATH%/bin/flutter}
+
+   current_dir=$(pwd)
+
+   cd $FLUTTER_PATH
+   # Use git to checkout version 3.3.10 of Flutter
+   git checkout 3.3.10
+   # Get back to current working directory
+   cd "$current_dir"
+
+   echo "Switched to Flutter version 3.3.10"
+fi
 
 
 # Enable linux desktop
 # Enable linux desktop
 flutter config --enable-macos-desktop
 flutter config --enable-macos-desktop
@@ -52,11 +70,11 @@ flutter doctor
 printMessage "Setting up githooks."
 printMessage "Setting up githooks."
 git config core.hooksPath .githooks
 git config core.hooksPath .githooks
 
 
-# Install go-gitlint 
+# Install go-gitlint
 printMessage "Installing go-gitlint."
 printMessage "Installing go-gitlint."
 GOLINT_FILENAME="go-gitlint_1.1.0_osx_x86_64.tar.gz"
 GOLINT_FILENAME="go-gitlint_1.1.0_osx_x86_64.tar.gz"
 curl -L https://github.com/llorllale/go-gitlint/releases/download/1.1.0/${GOLINT_FILENAME} --output ${GOLINT_FILENAME}
 curl -L https://github.com/llorllale/go-gitlint/releases/download/1.1.0/${GOLINT_FILENAME} --output ${GOLINT_FILENAME}
-tar -zxv --directory .githooks/. -f ${GOLINT_FILENAME} gitlint 
+tar -zxv --directory .githooks/. -f ${GOLINT_FILENAME} gitlint
 rm ${GOLINT_FILENAME}
 rm ${GOLINT_FILENAME}
 
 
 # Change to the frontend directory
 # Change to the frontend directory

+ 22 - 5
frontend/scripts/install_dev_env/install_windows.sh

@@ -17,7 +17,6 @@ printError() {
    printf "${RED}AppFlowy : $1${ENDCOLOR}\n"
    printf "${RED}AppFlowy : $1${ENDCOLOR}\n"
 }
 }
 
 
-
 # Note: This script does not install applications which are installed by the package manager. There are too many package managers out there.
 # Note: This script does not install applications which are installed by the package manager. There are too many package managers out there.
 
 
 # Install Rust
 # Install Rust
@@ -46,9 +45,27 @@ else
    printSuccess "Rust has been detected on your system, so Rust installation has been skipped"
    printSuccess "Rust has been detected on your system, so Rust installation has been skipped"
 fi
 fi
 
 
-# Enable the flutter stable channel
 printMessage "Setting up Flutter"
 printMessage "Setting up Flutter"
-flutter channel stable
+# Get the current Flutter version
+FLUTTER_VERSION=$(flutter --version | grep -oP 'Flutter \K\S+')
+# Check if the current version is 3.3.10
+if [ "$FLUTTER_VERSION" = "3.3.10" ]; then
+   echo "Flutter version is already 3.3.10"
+else
+   # Get the path to the Flutter SDK
+   FLUTTER_PATH=$(which flutter)
+   FLUTTER_PATH=${FLUTTER_PATH%/bin/flutter}
+
+   current_dir=$(pwd)
+
+   cd $FLUTTER_PATH
+   # Use git to checkout version 3.3.10 of Flutter
+   git checkout 3.3.10
+   # Get back to current working directory
+   cd "$current_dir"
+
+   echo "Switched to Flutter version 3.3.10"
+fi
 
 
 # Add pub cache and cargo to PATH
 # Add pub cache and cargo to PATH
 powershell '[Environment]::SetEnvironmentVariable("PATH", $Env:PATH + ";" + $Env:LOCALAPPDATA + "\Pub\Cache\Bin", [EnvironmentVariableTarget]::User)'
 powershell '[Environment]::SetEnvironmentVariable("PATH", $Env:PATH + ";" + $Env:LOCALAPPDATA + "\Pub\Cache\Bin", [EnvironmentVariableTarget]::User)'
@@ -64,14 +81,14 @@ flutter doctor
 printMessage "Setting up githooks."
 printMessage "Setting up githooks."
 git config core.hooksPath .githooks
 git config core.hooksPath .githooks
 
 
-# Install go-gitlint 
+# Install go-gitlint
 printMessage "Installing go-gitlint."
 printMessage "Installing go-gitlint."
 GOLINT_FILENAME="go-gitlint_1.1.0_windows_x86_64.tar.gz"
 GOLINT_FILENAME="go-gitlint_1.1.0_windows_x86_64.tar.gz"
 if curl --proto '=https' --tlsv1.2 -sSfL https://github.com/llorllale/go-gitlint/releases/download/1.1.0/${GOLINT_FILENAME} -o ${GOLINT_FILENAME}; then
 if curl --proto '=https' --tlsv1.2 -sSfL https://github.com/llorllale/go-gitlint/releases/download/1.1.0/${GOLINT_FILENAME} -o ${GOLINT_FILENAME}; then
    tar -zxv --directory .githooks/. -f ${GOLINT_FILENAME} gitlint.exe
    tar -zxv --directory .githooks/. -f ${GOLINT_FILENAME} gitlint.exe
    rm ${GOLINT_FILENAME}
    rm ${GOLINT_FILENAME}
 else
 else
- printError "Failed to install go-gitlint"
+   printError "Failed to install go-gitlint"
 fi
 fi
 
 
 # Change to the frontend directory
 # Change to the frontend directory