Browse Source

chore: sync release 0.1.1 (#2075)

Lucas.Xu 2 years ago
parent
commit
98f1ac52b4
43 changed files with 717 additions and 281 deletions
  1. 12 0
      CHANGELOG.md
  2. 1 1
      frontend/Makefile.toml
  3. 23 3
      frontend/appflowy_flutter/.metadata
  4. 1 1
      frontend/appflowy_flutter/android/app/build.gradle
  5. 1 1
      frontend/appflowy_flutter/android/app/src/debug/AndroidManifest.xml
  6. 1 1
      frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml
  7. 1 1
      frontend/appflowy_flutter/android/app/src/main/kotlin/com/example/app_flowy/MainActivity.kt
  8. 1 1
      frontend/appflowy_flutter/android/app/src/profile/AndroidManifest.xml
  9. 9 6
      frontend/appflowy_flutter/assets/translations/en.json
  10. 0 1
      frontend/appflowy_flutter/assets/translations/pt-BR.json
  11. 3 3
      frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj
  12. 17 8
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart
  13. 9 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/util/learn_more_action.dart
  14. 60 38
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart
  15. 5 2
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_plugins.dart
  16. 28 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart
  17. 30 7
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart
  18. 221 107
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart
  19. 5 2
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_toolbar_item.dart
  20. 24 0
      frontend/appflowy_flutter/lib/util/debounce.dart
  21. 23 5
      frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart
  22. 2 2
      frontend/appflowy_flutter/linux/CMakeLists.txt
  23. 1 1
      frontend/appflowy_flutter/linux/appflowy.desktop.temp
  24. 38 26
      frontend/appflowy_flutter/linux/my_application.cc
  25. 3 3
      frontend/appflowy_flutter/macos/Runner/Configs/AppInfo.xcconfig
  26. 2 2
      frontend/appflowy_flutter/packages/appflowy_editor/lib/src/commands/command_extension.dart
  27. 15 17
      frontend/appflowy_flutter/packages/appflowy_editor/lib/src/core/transform/transaction.dart
  28. 3 1
      frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/select_all_handler.dart
  29. 23 0
      frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/selection_service.dart
  30. 2 2
      frontend/appflowy_flutter/packages/appflowy_editor/test/command/command_extension_test.dart
  31. 37 0
      frontend/appflowy_flutter/packages/appflowy_editor/test/core/transform/transaction_test.dart
  32. 35 1
      frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart
  33. 3 0
      frontend/appflowy_flutter/packages/appflowy_editor/test/service/toolbar_service_test.dart
  34. 8 1
      frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart
  35. 14 4
      frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart
  36. 4 2
      frontend/appflowy_flutter/windows/CMakeLists.txt
  37. 22 5
      frontend/appflowy_flutter/windows/runner/CMakeLists.txt
  38. 10 10
      frontend/appflowy_flutter/windows/runner/Runner.rc
  39. 8 4
      frontend/appflowy_flutter/windows/runner/main.cpp
  40. 1 1
      frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.desktop
  41. 3 3
      frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.yml
  42. 5 5
      frontend/scripts/linux_installer/postinst
  43. 3 3
      frontend/scripts/windows_installer/inno_setup_config.iss

+ 12 - 0
CHANGELOG.md

@@ -1,5 +1,17 @@
 # Release Notes
 
+## Version 0.1.1 - 03/21/2023
+
+### New features
+
+- AppFlowy brings the power of OpenAI into your AppFlowy pages. Ask AI to write anything for you in AppFlowy.
+- Support adding a cover image to your page, making your pages beautiful.
+- More shortcuts become available. Click on '?' at the bottom right to access our shortcut guide.
+
+### Bug Fixes
+
+- Fix some bugs
+
 ## Version 0.1.0 - 02/09/2023
 
 ### New features

+ 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.0"
+CURRENT_APP_VERSION = "0.1.1"
 FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
 PRODUCT_NAME = "AppFlowy"
 # CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html

+ 23 - 3
frontend/appflowy_flutter/.metadata

@@ -1,10 +1,30 @@
 # This file tracks properties of this Flutter project.
 # Used by Flutter tool to assess capabilities and perform upgrades etc.
 #
-# This file should be version controlled and should not be manually edited.
+# This file should be version controlled.
 
 version:
-  revision: fa5883b78e566877613ad1ccb48dd92075cb5c23
-  channel: dev
+  revision: 135454af32477f815a7525073027a3ff9eff1bfd
+  channel: stable
 
 project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+  platforms:
+    - platform: root
+      create_revision: 135454af32477f815a7525073027a3ff9eff1bfd
+      base_revision: 135454af32477f815a7525073027a3ff9eff1bfd
+    - platform: windows
+      create_revision: 135454af32477f815a7525073027a3ff9eff1bfd
+      base_revision: 135454af32477f815a7525073027a3ff9eff1bfd
+
+  # User provided section
+
+  # List of Local paths (relative to this file) that should be
+  # ignored by the migrate tool.
+  #
+  # Files that are not part of the templates will be ignored by default.
+  unmanaged_files:
+    - 'lib/main.dart'
+    - 'ios/Runner.xcodeproj/project.pbxproj'

+ 1 - 1
frontend/appflowy_flutter/android/app/build.gradle

@@ -45,7 +45,7 @@ android {
 
     defaultConfig {
         // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
-        applicationId "com.example.appflowy_flutter"
+        applicationId "io.appflowy.appflowy"
         minSdkVersion 19
         targetSdkVersion 31
         versionCode flutterVersionCode.toInteger()

+ 1 - 1
frontend/appflowy_flutter/android/app/src/debug/AndroidManifest.xml

@@ -1,5 +1,5 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.example.appflowy_flutter">
+    package="io.appflowy.appflowy">
     <!-- Flutter needs it to communicate with the running application
          to allow setting breakpoints, to provide hot reload, etc.
     -->

+ 1 - 1
frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml

@@ -1,5 +1,5 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.example.appflowy_flutter">
+    package="io.appflowy.appflowy">
    <application
         android:label="appflowy_flutter"
         android:icon="@mipmap/ic_launcher"

+ 1 - 1
frontend/appflowy_flutter/android/app/src/main/kotlin/com/example/app_flowy/MainActivity.kt

@@ -1,4 +1,4 @@
-package com.example.appflowy_flutter
+package io.appflowy.appflowy
 
 import io.flutter.embedding.android.FlutterActivity
 

+ 1 - 1
frontend/appflowy_flutter/android/app/src/profile/AndroidManifest.xml

@@ -1,5 +1,5 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.example.appflowy_flutter">
+    package="io.appflowy.appflowy">
     <!-- Flutter needs it to communicate with the running application
          to allow setting breakpoints, to provide hot reload, etc.
     -->

+ 9 - 6
frontend/appflowy_flutter/assets/translations/en.json

@@ -138,7 +138,8 @@
     "keep": "Keep",
     "tryAgain": "Try again",
     "discard": "Discard",
-    "replace": "Replace"
+    "replace": "Replace",
+    "insertBelow": "Insert Below"
   },
   "label": {
     "welcome": "Welcome!",
@@ -345,20 +346,21 @@
     "plugins": {
       "referencedBoard": "Referenced Board",
       "referencedGrid": "Referenced Grid",
-      "autoCompletionMenuItemName": "Auto Completion",
-      "autoGeneratorMenuItemName": "Auto Generator",
+      "autoGeneratorMenuItemName": "OpenAI Writer",
       "autoGeneratorTitleName": "OpenAI: Ask AI to write anything...",
       "autoGeneratorLearnMore": "Learn more",
       "autoGeneratorGenerate": "Generate",
-      "autoGeneratorHintText": "Tell us what you want to generate by OpenAI ...",
+      "autoGeneratorHintText": "Ask OpenAI ...",
       "autoGeneratorCantGetOpenAIKey": "Can't get OpenAI key",
-      "smartEdit": "Smart Edit",
-      "smartEditTitleName": "OpenAI: Smart Edit",
+      "smartEdit": "AI Assistants",
+      "openAI": "OpenAI",
       "smartEditFixSpelling": "Fix spelling",
+      "warning": "⚠️ AI responses can be inaccurate or misleading.",
       "smartEditSummarize": "Summarize",
       "smartEditCouldNotFetchResult": "Could not fetch result from OpenAI",
       "smartEditCouldNotFetchKey": "Could not fetch OpenAI key",
       "smartEditDisabled": "Connect OpenAI in Settings",
+      "discardResponse": "Do you want to discard the AI responses?",
       "cover": {
         "changeCover": "Change Cover",
         "colors": "Colors",
@@ -380,6 +382,7 @@
         "imageSavingFailed": "Image Saving Failed",
         "addIcon": "Add Icon"
       }
+
     }
   },
   "board": {

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

@@ -349,7 +349,6 @@
       "autoGeneratorGenerate": "Gerar",
       "autoGeneratorHintText": "Diga-nos o que você deseja gerar por IA ...",
       "autoGeneratorCantGetOpenAIKey": "Não foi possível obter a chave da OpenAI",
-      "smartEditTitleName": "IA: edição inteligente",
       "smartEditFixSpelling": "Corrigir ortografia",
       "smartEditSummarize": "Resumir",
       "smartEditCouldNotFetchResult": "Não foi possível obter o resultado do OpenAI",

+ 3 - 3
frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj

@@ -359,7 +359,7 @@
 				ENABLE_BITCODE = NO;
 				INFOPLIST_FILE = Runner/Info.plist;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				PRODUCT_BUNDLE_IDENTIFIER = com.example.appFlowy;
+				PRODUCT_BUNDLE_IDENTIFIER = io.appflowy.appflowy;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
 				SWIFT_VERSION = 5.0;
@@ -483,7 +483,7 @@
 				ENABLE_BITCODE = NO;
 				INFOPLIST_FILE = Runner/Info.plist;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				PRODUCT_BUNDLE_IDENTIFIER = com.example.appFlowy;
+				PRODUCT_BUNDLE_IDENTIFIER = io.appflowy.appflowy;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
 				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -502,7 +502,7 @@
 				ENABLE_BITCODE = NO;
 				INFOPLIST_FILE = Runner/Info.plist;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				PRODUCT_BUNDLE_IDENTIFIER = com.example.appFlowy;
+				PRODUCT_BUNDLE_IDENTIFIER = io.appflowy.appflowy;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
 				SWIFT_VERSION = 5.0;

+ 17 - 8
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart

@@ -1,7 +1,6 @@
 import 'dart:convert';
 
 import 'package:appflowy/plugins/document/presentation/plugins/openai/service/text_edit.dart';
-import 'package:appflowy_editor/appflowy_editor.dart';
 
 import 'text_completion.dart';
 import 'package:dartz/dartz.dart';
@@ -125,6 +124,7 @@ class HttpOpenAIRepository implements OpenAIRepository {
     String? suffix,
     int maxTokens = 2048,
     double temperature = 0.3,
+    bool useAction = false,
   }) async {
     final parameters = {
       'model': 'text-davinci-003',
@@ -151,14 +151,22 @@ class HttpOpenAIRepository implements OpenAIRepository {
           .transform(const Utf8Decoder())
           .transform(const LineSplitter())) {
         syntax += 1;
-        if (syntax == 3) {
-          await onStart();
-          continue;
-        } else if (syntax < 3) {
-          continue;
+        if (!useAction) {
+          if (syntax == 3) {
+            await onStart();
+            continue;
+          } else if (syntax < 3) {
+            continue;
+          }
+        } else {
+          if (syntax == 2) {
+            await onStart();
+            continue;
+          } else if (syntax < 2) {
+            continue;
+          }
         }
         final data = chunk.trim().split('data: ');
-        Log.editor.info(data.toString());
         if (data.length > 1) {
           if (data[1] != '[DONE]') {
             final response = TextCompletionResponse.fromJson(
@@ -173,7 +181,7 @@ class HttpOpenAIRepository implements OpenAIRepository {
               previousSyntax = response.choices.first.text;
             }
           } else {
-            onEnd();
+            await onEnd();
           }
         }
       }
@@ -183,6 +191,7 @@ class HttpOpenAIRepository implements OpenAIRepository {
         OpenAIError.fromJson(json.decode(body)['error']),
       );
     }
+    return;
   }
 
   @override

+ 9 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/util/learn_more_action.dart

@@ -0,0 +1,9 @@
+import 'package:url_launcher/url_launcher.dart';
+
+Future<void> openLearnMorePage() async {
+  final uri = Uri.parse(
+      'https://appflowy.gitbook.io/docs/essential-documentation/appflowy-x-openai');
+  if (await canLaunchUrl(uri)) {
+    await launchUrl(uri);
+  }
+}

+ 60 - 38
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart

@@ -1,6 +1,8 @@
 import 'dart:convert';
 
 import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
+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/loading.dart';
 import 'package:appflowy/user/application/user_service.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
@@ -9,7 +11,7 @@ import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flowy_infra_ui/style_widget/text_field.dart';
 import 'package:flowy_infra_ui/widget/spacing.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
+import 'package:flutter/rendering.dart';
 import 'package:http/http.dart' as http;
 import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:easy_localization/easy_localization.dart';
@@ -56,6 +58,7 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
   final controller = TextEditingController();
   final focusNode = FocusNode();
   final textFieldFocusNode = FocusNode();
+  final interceptor = SelectionInterceptor();
 
   @override
   void initState() {
@@ -63,6 +66,34 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
 
     textFieldFocusNode.addListener(_onFocusChanged);
     textFieldFocusNode.requestFocus();
+    widget.editorState.service.selectionService.register(interceptor
+      ..canTap = (details) {
+        final renderBox = context.findRenderObject() as RenderBox?;
+        if (renderBox != null) {
+          if (!isTapDownDetailsInRenderBox(details, renderBox)) {
+            if (text.isNotEmpty || controller.text.isNotEmpty) {
+              showDialog(
+                context: context,
+                builder: (context) {
+                  return DiscardDialog(
+                    onConfirm: () => _onDiscard(),
+                    onCancel: () {},
+                  );
+                },
+              );
+            } else if (controller.text.isEmpty) {
+              _onExit();
+            }
+          }
+        }
+        return false;
+      });
+  }
+
+  bool isTapDownDetailsInRenderBox(TapDownDetails details, RenderBox box) {
+    var result = BoxHitTestResult();
+    box.hitTest(result, position: box.globalToLocal(details.globalPosition));
+    return result.path.any((entry) => entry.target == box);
   }
 
   @override
@@ -71,6 +102,7 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
     textFieldFocusNode.removeListener(_onFocusChanged);
     widget.editorState.service.selectionService.currentSelection
         .removeListener(_onCancelWhenSelectionChanged);
+    widget.editorState.service.selectionService.unRegister(interceptor);
 
     super.dispose();
   }
@@ -119,34 +151,26 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
           fontSize: 14,
         ),
         const Spacer(),
-        FlowyText.regular(
-          LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),
-        ),
+        FlowyButton(
+          useIntrinsicWidth: true,
+          text: FlowyText.regular(
+            LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),
+          ),
+          onTap: () async {
+            await openLearnMorePage();
+          },
+        )
       ],
     );
   }
 
   Widget _buildInputWidget(BuildContext context) {
-    return RawKeyboardListener(
-      focusNode: focusNode,
-      onKey: (RawKeyEvent event) async {
-        if (event is! RawKeyDownEvent) return;
-        if (event.logicalKey == LogicalKeyboardKey.enter) {
-          if (controller.text.isNotEmpty) {
-            textFieldFocusNode.unfocus();
-            await _onGenerate();
-          }
-        } else if (event.logicalKey == LogicalKeyboardKey.escape) {
-          await _onExit();
-        }
-      },
-      child: FlowyTextField(
-        hintText: LocaleKeys.document_plugins_autoGeneratorHintText.tr(),
-        controller: controller,
-        maxLines: 3,
-        focusNode: textFieldFocusNode,
-        autoFocus: false,
-      ),
+    return FlowyTextField(
+      hintText: LocaleKeys.document_plugins_autoGeneratorHintText.tr(),
+      controller: controller,
+      maxLines: 3,
+      focusNode: textFieldFocusNode,
+      autoFocus: false,
     );
   }
 
@@ -157,15 +181,9 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
           TextSpan(
             children: [
               TextSpan(
-                text: '${LocaleKeys.button_generate.tr()}  ',
+                text: LocaleKeys.button_generate.tr(),
                 style: Theme.of(context).textTheme.bodyMedium,
               ),
-              TextSpan(
-                text: '↵',
-                style: Theme.of(context).textTheme.bodyMedium?.copyWith(
-                      color: Colors.grey,
-                    ),
-              ),
             ],
           ),
           onPressed: () async => await _onGenerate(),
@@ -175,19 +193,23 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
           TextSpan(
             children: [
               TextSpan(
-                text: '${LocaleKeys.button_Cancel.tr()}  ',
+                text: LocaleKeys.button_Cancel.tr(),
                 style: Theme.of(context).textTheme.bodyMedium,
               ),
-              TextSpan(
-                text: LocaleKeys.button_esc.tr(),
-                style: Theme.of(context).textTheme.bodyMedium?.copyWith(
-                      color: Colors.grey,
-                    ),
-              ),
             ],
           ),
           onPressed: () async => await _onExit(),
         ),
+        Expanded(
+          child: Container(
+            alignment: Alignment.centerRight,
+            child: FlowyText.regular(
+              LocaleKeys.document_plugins_warning.tr(),
+              color: Theme.of(context).hintColor,
+              overflow: TextOverflow.ellipsis,
+            ),
+          ),
+        ),
       ],
     );
   }

+ 5 - 2
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_plugins.dart

@@ -2,10 +2,13 @@ import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/au
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:flutter/material.dart';
 
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:easy_localization/easy_localization.dart';
+
 SelectionMenuItem autoGeneratorMenuItem = SelectionMenuItem.node(
-  name: 'Auto Generator',
+  name: LocaleKeys.document_plugins_autoGeneratorMenuItemName.tr(),
   iconData: Icons.generating_tokens,
-  keywords: ['autogenerator', 'auto generator'],
+  keywords: ['ai', 'openai' 'writer', 'autogenerator'],
   nodeBuilder: (editorState) {
     final node = Node(
       type: kAutoCompletionInputType,

+ 28 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart

@@ -0,0 +1,28 @@
+import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
+
+import 'package:flutter/material.dart';
+import 'package:appflowy/generated/locale_keys.g.dart';
+
+import 'package:easy_localization/easy_localization.dart';
+
+class DiscardDialog extends StatelessWidget {
+  const DiscardDialog({
+    super.key,
+    required this.onConfirm,
+    required this.onCancel,
+  });
+
+  final VoidCallback onConfirm;
+  final VoidCallback onCancel;
+
+  @override
+  Widget build(BuildContext context) {
+    return NavigatorOkCancelDialog(
+      message: LocaleKeys.document_plugins_discardResponse.tr(),
+      okTitle: LocaleKeys.button_discard.tr(),
+      cancelTitle: LocaleKeys.button_Cancel.tr(),
+      onOkPressed: onConfirm,
+      onCancelPressed: onCancel,
+    );
+  }
+}

+ 30 - 7
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart

@@ -10,11 +10,39 @@ enum SmartEditAction {
   String get toInstruction {
     switch (this) {
       case SmartEditAction.summarize:
-        return 'Make this shorter and more concise:';
+        return 'Tl;dr';
       case SmartEditAction.fixSpelling:
         return 'Correct this to standard English:';
     }
   }
+
+  String prompt(String input) {
+    switch (this) {
+      case SmartEditAction.summarize:
+        return '$input\n\nTl;dr';
+      case SmartEditAction.fixSpelling:
+        return 'Correct this to standard English:\n\n$input';
+    }
+  }
+
+  static SmartEditAction from(int index) {
+    switch (index) {
+      case 0:
+        return SmartEditAction.summarize;
+      case 1:
+        return SmartEditAction.fixSpelling;
+    }
+    return SmartEditAction.fixSpelling;
+  }
+
+  String get name {
+    switch (this) {
+      case SmartEditAction.summarize:
+        return LocaleKeys.document_plugins_smartEditSummarize.tr();
+      case SmartEditAction.fixSpelling:
+        return LocaleKeys.document_plugins_smartEditFixSpelling.tr();
+    }
+  }
 }
 
 class SmartEditActionWrapper extends ActionCell {
@@ -26,11 +54,6 @@ class SmartEditActionWrapper extends ActionCell {
 
   @override
   String get name {
-    switch (inner) {
-      case SmartEditAction.summarize:
-        return LocaleKeys.document_plugins_smartEditSummarize.tr();
-      case SmartEditAction.fixSpelling:
-        return LocaleKeys.document_plugins_smartEditFixSpelling.tr();
-    }
+    return inner.name;
   }
 }

+ 221 - 107
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart

@@ -1,19 +1,18 @@
-import 'package:appflowy/plugins/document/presentation/plugins/openai/service/error.dart';
+import 'dart:async';
+
 import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/openai/service/text_edit.dart';
+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_editor/appflowy_editor.dart';
-import 'package:flowy_infra_ui/style_widget/button.dart';
-import 'package:flowy_infra_ui/style_widget/text.dart';
-import 'package:flowy_infra_ui/widget/spacing.dart';
+import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flowy_infra_ui/style_widget/decoration.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
 import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:http/http.dart' as http;
-import 'package:dartz/dartz.dart' as dartz;
-import 'package:appflowy/util/either_extension.dart';
 
 const String kSmartEditType = 'smart_edit_input';
 const String kSmartEditInstructionType = 'smart_edit_instruction';
@@ -22,15 +21,15 @@ const String kSmartEditInputType = 'smart_edit_input';
 class SmartEditInputBuilder extends NodeWidgetBuilder<Node> {
   @override
   NodeValidator<Node> get nodeValidator => (node) {
-        return SmartEditAction.values.map((e) => e.toInstruction).contains(
-                  node.attributes[kSmartEditInstructionType],
-                ) &&
+        return SmartEditAction.values
+                .map((e) => e.index)
+                .contains(node.attributes[kSmartEditInstructionType]) &&
             node.attributes[kSmartEditInputType] is String;
       };
 
   @override
   Widget build(NodeWidgetContext<Node> context) {
-    return _SmartEditInput(
+    return _HoverSmartInput(
       key: context.node.key,
       node: context.node,
       editorState: context.editorState,
@@ -38,28 +37,111 @@ class SmartEditInputBuilder extends NodeWidgetBuilder<Node> {
   }
 }
 
-class _SmartEditInput extends StatefulWidget {
-  final Node node;
+class _HoverSmartInput extends StatefulWidget {
+  const _HoverSmartInput({
+    required super.key,
+    required this.node,
+    required this.editorState,
+  });
 
+  final Node node;
   final EditorState editorState;
+
+  @override
+  State<_HoverSmartInput> createState() => _HoverSmartInputState();
+}
+
+class _HoverSmartInputState extends State<_HoverSmartInput> {
+  final popoverController = PopoverController();
+  final key = GlobalKey(debugLabel: 'smart_edit_input');
+
+  @override
+  void initState() {
+    super.initState();
+    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
+      popoverController.show();
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final width = _maxWidth();
+
+    return AppFlowyPopover(
+      controller: popoverController,
+      direction: PopoverDirection.bottomWithLeftAligned,
+      triggerActions: PopoverTriggerFlags.none,
+      margin: EdgeInsets.zero,
+      constraints: BoxConstraints(maxWidth: width),
+      decoration: FlowyDecoration.decoration(
+        Colors.transparent,
+        Colors.transparent,
+      ),
+      child: const SizedBox(
+        width: double.infinity,
+      ),
+      canClose: () async {
+        final completer = Completer<bool>();
+        final state = key.currentState as _SmartEditInputState;
+        if (state.result.isEmpty) {
+          completer.complete(true);
+        } else {
+          showDialog(
+            context: context,
+            builder: (context) {
+              return DiscardDialog(
+                onConfirm: () => completer.complete(true),
+                onCancel: () => completer.complete(false),
+              );
+            },
+          );
+        }
+        return completer.future;
+      },
+      popupBuilder: (BuildContext popoverContext) {
+        return _SmartEditInput(
+          key: key,
+          node: widget.node,
+          editorState: widget.editorState,
+        );
+      },
+    );
+  }
+
+  double _maxWidth() {
+    var width = double.infinity;
+    final editorSize = widget.editorState.renderBox?.size;
+    final padding = widget.editorState.editorStyle.padding;
+    if (editorSize != null && padding != null) {
+      width = editorSize.width - padding.left - padding.right;
+    }
+    return width;
+  }
+}
+
+class _SmartEditInput extends StatefulWidget {
   const _SmartEditInput({
-    Key? key,
+    required super.key,
     required this.node,
     required this.editorState,
   });
 
+  final Node node;
+  final EditorState editorState;
+
   @override
   State<_SmartEditInput> createState() => _SmartEditInputState();
 }
 
 class _SmartEditInputState extends State<_SmartEditInput> {
-  String get instruction => widget.node.attributes[kSmartEditInstructionType];
+  SmartEditAction get action =>
+      SmartEditAction.from(widget.node.attributes[kSmartEditInstructionType]);
   String get input => widget.node.attributes[kSmartEditInputType];
 
   final focusNode = FocusNode();
   final client = http.Client();
-  dartz.Either<OpenAIError, TextEditResponse>? result;
   bool loading = true;
+  String result = '';
 
   @override
   void initState() {
@@ -72,12 +154,7 @@ class _SmartEditInputState extends State<_SmartEditInput> {
         widget.editorState.service.keyboardService?.enable();
       }
     });
-    _requestEdits().then(
-      (value) => setState(() {
-        result = value;
-        loading = false;
-      }),
-    );
+    _requestCompletions();
   }
 
   @override
@@ -99,28 +176,16 @@ class _SmartEditInputState extends State<_SmartEditInput> {
   }
 
   Widget _buildSmartEditPanel(BuildContext context) {
-    return RawKeyboardListener(
-      focusNode: focusNode,
-      onKey: (RawKeyEvent event) async {
-        if (event is! RawKeyDownEvent) return;
-        if (event.logicalKey == LogicalKeyboardKey.enter) {
-          await _onReplace();
-          await _onExit();
-        } else if (event.logicalKey == LogicalKeyboardKey.escape) {
-          await _onExit();
-        }
-      },
-      child: Column(
-        mainAxisSize: MainAxisSize.min,
-        crossAxisAlignment: CrossAxisAlignment.start,
-        children: [
-          _buildHeaderWidget(context),
-          const Space(0, 10),
-          _buildResultWidget(context),
-          const Space(0, 10),
-          _buildInputFooterWidget(context),
-        ],
-      ),
+    return Column(
+      mainAxisSize: MainAxisSize.min,
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        _buildHeaderWidget(context),
+        const Space(0, 10),
+        _buildResultWidget(context),
+        const Space(0, 10),
+        _buildInputFooterWidget(context),
+      ],
     );
   }
 
@@ -128,13 +193,19 @@ class _SmartEditInputState extends State<_SmartEditInput> {
     return Row(
       children: [
         FlowyText.medium(
-          LocaleKeys.document_plugins_smartEditTitleName.tr(),
+          '${LocaleKeys.document_plugins_openAI.tr()}: ${action.name}',
           fontSize: 14,
         ),
         const Spacer(),
-        FlowyText.regular(
-          LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),
-        ),
+        FlowyButton(
+          useIntrinsicWidth: true,
+          text: FlowyText.regular(
+            LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),
+          ),
+          onTap: () async {
+            await openLearnMorePage();
+          },
+        )
       ],
     );
   }
@@ -147,25 +218,14 @@ class _SmartEditInputState extends State<_SmartEditInput> {
         child: const CircularProgressIndicator(),
       ),
     );
-    if (result == null) {
+    if (result.isEmpty) {
       return loading;
     }
-    return result!.fold((error) {
-      return Flexible(
-        child: Text(
-          error.message,
-          style: Theme.of(context).textTheme.bodyMedium?.copyWith(
-                color: Colors.red,
-              ),
-        ),
-      );
-    }, (response) {
-      return Flexible(
-        child: Text(
-          response.choices.map((e) => e.text).join('\n'),
-        ),
-      );
-    });
+    return Flexible(
+      child: Text(
+        result,
+      ),
+    );
   }
 
   Widget _buildInputFooterWidget(BuildContext context) {
@@ -175,19 +235,13 @@ class _SmartEditInputState extends State<_SmartEditInput> {
           TextSpan(
             children: [
               TextSpan(
-                text: '${LocaleKeys.button_replace.tr()}  ',
+                text: LocaleKeys.button_replace.tr(),
                 style: Theme.of(context).textTheme.bodyMedium,
               ),
-              TextSpan(
-                text: '↵',
-                style: Theme.of(context).textTheme.bodyMedium?.copyWith(
-                      color: Colors.grey,
-                    ),
-              ),
             ],
           ),
-          onPressed: () {
-            _onReplace();
+          onPressed: () async {
+            await _onReplace();
             _onExit();
           },
         ),
@@ -196,19 +250,33 @@ class _SmartEditInputState extends State<_SmartEditInput> {
           TextSpan(
             children: [
               TextSpan(
-                text: '${LocaleKeys.button_Cancel.tr()}  ',
+                text: LocaleKeys.button_insertBelow.tr(),
                 style: Theme.of(context).textTheme.bodyMedium,
               ),
+            ],
+          ),
+          onPressed: () async {
+            await _onInsertBelow();
+            _onExit();
+          },
+        ),
+        const Space(10, 0),
+        FlowyRichTextButton(
+          TextSpan(
+            children: [
               TextSpan(
-                text: LocaleKeys.button_esc.tr(),
-                style: Theme.of(context).textTheme.bodyMedium?.copyWith(
-                      color: Colors.grey,
-                    ),
+                text: LocaleKeys.button_Cancel.tr(),
+                style: Theme.of(context).textTheme.bodyMedium,
               ),
             ],
           ),
           onPressed: () async => await _onExit(),
         ),
+        const Spacer(),
+        FlowyText.regular(
+          LocaleKeys.document_plugins_warning.tr(),
+          color: Theme.of(context).hintColor,
+        ),
       ],
     );
   }
@@ -219,12 +287,11 @@ class _SmartEditInputState extends State<_SmartEditInput> {
     final selectedNodes = widget
         .editorState.service.selectionService.currentSelectedNodes.normalized
         .whereType<TextNode>();
-    if (selection == null || result == null || result!.isLeft()) {
+    if (selection == null || result.isEmpty) {
       return;
     }
 
-    final texts = result!.asRight().choices.first.text.split('\n')
-      ..removeWhere((element) => element.isEmpty);
+    final texts = result.split('\n')..removeWhere((element) => element.isEmpty);
     final transaction = widget.editorState.transaction;
     transaction.replaceTexts(
       selectedNodes.toList(growable: false),
@@ -234,6 +301,25 @@ class _SmartEditInputState extends State<_SmartEditInput> {
     return widget.editorState.apply(transaction);
   }
 
+  Future<void> _onInsertBelow() async {
+    final selection = widget.editorState.service.selectionService
+        .currentSelection.value?.normalized;
+    if (selection == null || result.isEmpty) {
+      return;
+    }
+    final texts = result.split('\n')..removeWhere((element) => element.isEmpty);
+    final transaction = widget.editorState.transaction;
+    transaction.insertNodes(
+      selection.normalized.end.path.next,
+      texts.map(
+        (e) => TextNode(
+          delta: Delta()..insert(e),
+        ),
+      ),
+    );
+    return widget.editorState.apply(transaction);
+  }
+
   Future<void> _onExit() async {
     final transaction = widget.editorState.transaction;
     transaction.deleteNode(widget.node);
@@ -246,35 +332,63 @@ class _SmartEditInputState extends State<_SmartEditInput> {
     );
   }
 
-  Future<dartz.Either<OpenAIError, TextEditResponse>> _requestEdits() async {
+  Future<void> _requestCompletions() async {
     final result = await UserBackendService.getCurrentUserProfile();
-    return result.fold((userProfile) async {
+    return result.fold((l) async {
       final openAIRepository = HttpOpenAIRepository(
         client: client,
-        apiKey: userProfile.openaiKey,
+        apiKey: l.openaiKey,
       );
-      final edits = await openAIRepository.getEdits(
-        input: input,
-        instruction: instruction,
-        n: 1,
-      );
-      return edits.fold((error) async {
-        return dartz.Left(
-          OpenAIError(
-            message:
-                LocaleKeys.document_plugins_smartEditCouldNotFetchResult.tr(),
-          ),
+
+      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();
+          },
         );
-      }, (textEdit) async {
-        return dartz.Right(textEdit);
-      });
-    }, (error) async {
-      // error
-      return dartz.Left(
-        OpenAIError(
-          message: LocaleKeys.document_plugins_smartEditCouldNotFetchKey.tr(),
-        ),
-      );
+      }
+    }, (r) async {
+      await _showError(r.msg);
+      await _onExit();
     });
   }
+
+  Future<void> _showError(String message) async {
+    ScaffoldMessenger.of(context).showSnackBar(
+      SnackBar(
+        action: SnackBarAction(
+          label: LocaleKeys.button_Cancel.tr(),
+          onPressed: () {
+            ScaffoldMessenger.of(context).hideCurrentSnackBar();
+          },
+        ),
+        content: FlowyText(message),
+      ),
+    );
+  }
 }

+ 5 - 2
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_toolbar_item.dart

@@ -101,14 +101,17 @@ class _SmartEditWidgetState extends State<_SmartEditWidget> {
       textNodes.normalized,
       selection.normalized,
     );
+    while (input.last.isEmpty) {
+      input.removeLast();
+    }
     final transaction = widget.editorState.transaction;
     transaction.insertNode(
       selection.normalized.end.path.next,
       Node(
         type: kSmartEditType,
         attributes: {
-          kSmartEditInstructionType: actionWrapper.inner.toInstruction,
-          kSmartEditInputType: input,
+          kSmartEditInstructionType: actionWrapper.inner.index,
+          kSmartEditInputType: input.join('\n\n'),
         },
       ),
     );

+ 24 - 0
frontend/appflowy_flutter/lib/util/debounce.dart

@@ -0,0 +1,24 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+
+class Debounce {
+  final Duration duration;
+  Timer? _timer;
+
+  Debounce({
+    this.duration = const Duration(milliseconds: 1000),
+  });
+
+  void call(VoidCallback action) {
+    dispose();
+    _timer = Timer(duration, () {
+      action();
+    });
+  }
+
+  void dispose() {
+    _timer?.cancel();
+    _timer = null;
+  }
+}

+ 23 - 5
frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart

@@ -1,4 +1,5 @@
 import 'package:appflowy/startup/startup.dart';
+import 'package:appflowy/util/debounce.dart';
 import 'package:flowy_infra/size.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flutter/material.dart';
@@ -98,11 +99,20 @@ class _OpenaiKeyInput extends StatefulWidget {
 
 class _OpenaiKeyInputState extends State<_OpenaiKeyInput> {
   bool visible = false;
+  final textEditingController = TextEditingController();
+  final debounce = Debounce();
+
+  @override
+  void initState() {
+    super.initState();
+
+    textEditingController.text = widget.openAIKey;
+  }
 
   @override
   Widget build(BuildContext context) {
     return TextField(
-      controller: TextEditingController()..text = widget.openAIKey,
+      controller: textEditingController,
       obscureText: !visible,
       decoration: InputDecoration(
         labelText: 'OpenAI Key',
@@ -120,13 +130,21 @@ class _OpenaiKeyInputState extends State<_OpenaiKeyInput> {
           },
         ),
       ),
-      onSubmitted: (val) {
-        context
-            .read<SettingsUserViewBloc>()
-            .add(SettingsUserEvent.updateUserOpenAIKey(val));
+      onChanged: (value) {
+        debounce.call(() {
+          context
+              .read<SettingsUserViewBloc>()
+              .add(SettingsUserEvent.updateUserOpenAIKey(value));
+        });
       },
     );
   }
+
+  @override
+  void dispose() {
+    debounce.dispose();
+    super.dispose();
+  }
 }
 
 class _CurrentIcon extends StatelessWidget {

+ 2 - 2
frontend/appflowy_flutter/linux/CMakeLists.txt

@@ -1,8 +1,8 @@
 cmake_minimum_required(VERSION 3.10)
 project(runner LANGUAGES CXX)
 
-set(BINARY_NAME "appflowy_flutter")
-set(APPLICATION_ID "com.example.appflowy_flutter")
+set(BINARY_NAME "AppFlowy")
+set(APPLICATION_ID "io.appflowy.appflowy")
 
 cmake_policy(SET CMP0063 NEW)
 

+ 1 - 1
frontend/appflowy_flutter/linux/appflowy.desktop.temp

@@ -2,7 +2,7 @@
 Name=AppFlowy
 Comment=An Open Source Alternative to Notion
 Icon=[CHANGE_THIS]/AppFlowy/flowy_logo.svg
-Exec=[CHANGE_THIS]/AppFlowy/appflowy_flutter
+Exec=[CHANGE_THIS]/AppFlowy/AppFlowy
 Categories=Office
 Type=Application
 Terminal=false

+ 38 - 26
frontend/appflowy_flutter/linux/my_application.cc

@@ -7,17 +7,19 @@
 
 #include "flutter/generated_plugin_registrant.h"
 
-struct _MyApplication {
+struct _MyApplication
+{
   GtkApplication parent_instance;
-  char** dart_entrypoint_arguments;
+  char **dart_entrypoint_arguments;
 };
 
 G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
 
 // Implements GApplication::activate.
-static void my_application_activate(GApplication* application) {
-  MyApplication* self = MY_APPLICATION(application);
-  GtkWindow* window =
+static void my_application_activate(GApplication *application)
+{
+  MyApplication *self = MY_APPLICATION(application);
+  GtkWindow *window =
       GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
 
   // Use a header bar when running in GNOME as this is the common style used
@@ -29,22 +31,27 @@ static void my_application_activate(GApplication* application) {
   // if future cases occur).
   gboolean use_header_bar = TRUE;
 #ifdef GDK_WINDOWING_X11
-  GdkScreen* screen = gtk_window_get_screen(window);
-  if (GDK_IS_X11_SCREEN(screen)) {
-    const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
-    if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
+  GdkScreen *screen = gtk_window_get_screen(window);
+  if (GDK_IS_X11_SCREEN(screen))
+  {
+    const gchar *wm_name = gdk_x11_screen_get_window_manager_name(screen);
+    if (g_strcmp0(wm_name, "GNOME Shell") != 0)
+    {
       use_header_bar = FALSE;
     }
   }
 #endif
-  if (use_header_bar) {
-    GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
+  if (use_header_bar)
+  {
+    GtkHeaderBar *header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
     gtk_widget_show(GTK_WIDGET(header_bar));
-    gtk_header_bar_set_title(header_bar, "appflowy_flutter");
+    gtk_header_bar_set_title(header_bar, "AppFlowy");
     gtk_header_bar_set_show_close_button(header_bar, TRUE);
     gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
-  } else {
-    gtk_window_set_title(window, "appflowy_flutter");
+  }
+  else
+  {
+    gtk_window_set_title(window, "AppFlowy");
   }
 
   gtk_window_set_default_size(window, 1280, 720);
@@ -53,7 +60,7 @@ static void my_application_activate(GApplication* application) {
   g_autoptr(FlDartProject) project = fl_dart_project_new();
   fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
 
-  FlView* view = fl_view_new(project);
+  FlView *view = fl_view_new(project);
   gtk_widget_show(GTK_WIDGET(view));
   gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
 
@@ -63,16 +70,18 @@ static void my_application_activate(GApplication* application) {
 }
 
 // Implements GApplication::local_command_line.
-static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
-  MyApplication* self = MY_APPLICATION(application);
+static gboolean my_application_local_command_line(GApplication *application, gchar ***arguments, int *exit_status)
+{
+  MyApplication *self = MY_APPLICATION(application);
   // Strip out the first argument as it is the binary name.
   self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
 
   g_autoptr(GError) error = nullptr;
-  if (!g_application_register(application, nullptr, &error)) {
-     g_warning("Failed to register: %s", error->message);
-     *exit_status = 1;
-     return TRUE;
+  if (!g_application_register(application, nullptr, &error))
+  {
+    g_warning("Failed to register: %s", error->message);
+    *exit_status = 1;
+    return TRUE;
   }
 
   g_application_activate(application);
@@ -82,21 +91,24 @@ static gboolean my_application_local_command_line(GApplication* application, gch
 }
 
 // Implements GObject::dispose.
-static void my_application_dispose(GObject* object) {
-  MyApplication* self = MY_APPLICATION(object);
+static void my_application_dispose(GObject *object)
+{
+  MyApplication *self = MY_APPLICATION(object);
   g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
   G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
 }
 
-static void my_application_class_init(MyApplicationClass* klass) {
+static void my_application_class_init(MyApplicationClass *klass)
+{
   G_APPLICATION_CLASS(klass)->activate = my_application_activate;
   G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
   G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
 }
 
-static void my_application_init(MyApplication* self) {}
+static void my_application_init(MyApplication *self) {}
 
-MyApplication* my_application_new() {
+MyApplication *my_application_new()
+{
   return MY_APPLICATION(g_object_new(my_application_get_type(),
                                      "application-id", APPLICATION_ID,
                                      "flags", G_APPLICATION_NON_UNIQUE,

+ 3 - 3
frontend/appflowy_flutter/macos/Runner/Configs/AppInfo.xcconfig

@@ -5,10 +5,10 @@
 // 'flutter create' template.
 
 // The application's name. By default this is also the title of the Flutter window.
-PRODUCT_NAME = appflowy_flutter
+PRODUCT_NAME = AppFlowy
 
 // The application's bundle identifier
-PRODUCT_BUNDLE_IDENTIFIER = com.example.appFlowy
+PRODUCT_BUNDLE_IDENTIFIER = io.appflowy.appflowy
 
 // The copyright displayed in application information
-PRODUCT_COPYRIGHT = Copyright © 2021 com.example. All rights reserved.
+PRODUCT_COPYRIGHT = Copyright © 2023 AppFlowy.IO. All rights reserved.

+ 2 - 2
frontend/appflowy_flutter/packages/appflowy_editor/lib/src/commands/command_extension.dart

@@ -52,7 +52,7 @@ extension CommandExtension on EditorState {
     throw Exception('path and textNode cannot be null at the same time');
   }
 
-  String getTextInSelection(
+  List<String> getTextInSelection(
     List<TextNode> textNodes,
     Selection selection,
   ) {
@@ -77,6 +77,6 @@ extension CommandExtension on EditorState {
         }
       }
     }
-    return res.join('\n');
+    return res;
   }
 }

+ 15 - 17
frontend/appflowy_flutter/packages/appflowy_editor/lib/src/core/transform/transaction.dart

@@ -264,11 +264,11 @@ extension TextTransaction on Transaction {
     if (index != 0 && attributes == null) {
       newAttributes =
           textNode.delta.slice(max(index - 1, 0), index).first.attributes;
-      if (newAttributes != null) {
-        newAttributes = {...newAttributes}; // make a copy
-      } else {
-        newAttributes =
-            textNode.delta.slice(index, index + length).first.attributes;
+      if (newAttributes == null) {
+        final slicedDelta = textNode.delta.slice(index, index + length);
+        if (slicedDelta.isNotEmpty) {
+          newAttributes = slicedDelta.first.attributes;
+        }
       }
     }
     updateText(
@@ -276,7 +276,7 @@ extension TextTransaction on Transaction {
       Delta()
         ..retain(index)
         ..delete(length)
-        ..insert(text, attributes: newAttributes),
+        ..insert(text, attributes: {...newAttributes ?? {}}),
     );
     afterSelection = Selection.collapsed(
       Position(
@@ -347,24 +347,22 @@ extension TextTransaction on Transaction {
             textNode.toPlainText().length,
             texts.first,
           );
-        } else if (i == length - 1) {
+        } else if (i == length - 1 && texts.length >= 2) {
           replaceText(
             textNode,
             0,
             selection.endIndex,
             texts.last,
           );
+        } else if (i < texts.length - 1) {
+          replaceText(
+            textNode,
+            0,
+            textNode.toPlainText().length,
+            texts[i],
+          );
         } else {
-          if (i < texts.length - 1) {
-            replaceText(
-              textNode,
-              0,
-              textNode.toPlainText().length,
-              texts[i],
-            );
-          } else {
-            deleteNode(textNode);
-          }
+          deleteNode(textNode);
         }
       }
       afterSelection = null;

+ 3 - 1
frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/select_all_handler.dart

@@ -8,7 +8,9 @@ ShortcutEventHandler selectAllHandler = (editorState, event) {
   if (editorState.document.root.children.isEmpty) {
     return KeyEventResult.handled;
   }
-  final firstNode = editorState.document.root.children.first;
+  final firstNode = editorState.document.root.children.firstWhere(
+    (element) => element is TextNode,
+  );
   final lastNode = editorState.document.root.children.last;
   var offset = 0;
   if (lastNode is TextNode) {

+ 23 - 0
frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/selection_service.dart

@@ -82,6 +82,13 @@ abstract class AppFlowySelectionService {
 
   /// The current selection areas's rect in editor.
   List<Rect> get selectionRects;
+
+  void register(SelectionInterceptor interceptor);
+  void unRegister(SelectionInterceptor interceptor);
+}
+
+class SelectionInterceptor {
+  bool Function(TapDownDetails details)? canTap;
 }
 
 class AppFlowySelection extends StatefulWidget {
@@ -212,6 +219,7 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
 
     selectionRects.clear();
     clearSelection();
+    _clearToolbar();
 
     if (selection != null) {
       if (selection.isCollapsed) {
@@ -286,6 +294,10 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
   }
 
   void _onTapDown(TapDownDetails details) {
+    final canTap =
+        _interceptors.every((element) => element.canTap?.call(details) ?? true);
+    if (!canTap) return;
+
     // clear old state.
     _panStartOffset = null;
 
@@ -701,4 +713,15 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
     //   }
     // }
   }
+
+  final List<SelectionInterceptor> _interceptors = [];
+  @override
+  void register(SelectionInterceptor interceptor) {
+    _interceptors.add(interceptor);
+  }
+
+  @override
+  void unRegister(SelectionInterceptor interceptor) {
+    _interceptors.removeWhere((element) => element == interceptor);
+  }
 }

+ 2 - 2
frontend/appflowy_flutter/packages/appflowy_editor/test/command/command_extension_test.dart

@@ -26,11 +26,11 @@ void main() {
           .editorState.service.selectionService.currentSelectedNodes
           .whereType<TextNode>()
           .toList(growable: false);
-      final text = editor.editorState.getTextInSelection(
+      final texts = editor.editorState.getTextInSelection(
         textNodes.normalized,
         selection.normalized,
       );
-      expect(text, 'me\nto\nAppfl');
+      expect(texts, ['me', 'to', 'Appfl']);
     });
   });
 }

+ 37 - 0
frontend/appflowy_flutter/packages/appflowy_editor/test/core/transform/transaction_test.dart

@@ -91,6 +91,43 @@ void main() async {
       expect(textNodes[3].toPlainText(), 'ABC456789');
     });
 
+    testWidgets('test replaceTexts, textNodes.length >> texts.length',
+        (tester) async {
+      TestWidgetsFlutterBinding.ensureInitialized();
+
+      final editor = tester.editor
+        ..insertTextNode('0123456789')
+        ..insertTextNode('0123456789')
+        ..insertTextNode('0123456789')
+        ..insertTextNode('0123456789')
+        ..insertTextNode('0123456789');
+      await editor.startTesting();
+      await tester.pumpAndSettle();
+
+      expect(editor.documentLength, 5);
+
+      final selection = Selection(
+        start: Position(path: [0], offset: 4),
+        end: Position(path: [4], offset: 4),
+      );
+      final transaction = editor.editorState.transaction;
+      var textNodes = [0, 1, 2, 3, 4]
+          .map((e) => editor.nodeAtPath([e])!)
+          .whereType<TextNode>()
+          .toList(growable: false);
+      final texts = ['ABC'];
+      transaction.replaceTexts(textNodes, selection, texts);
+      editor.editorState.apply(transaction);
+      await tester.pumpAndSettle();
+
+      expect(editor.documentLength, 1);
+      textNodes = [0]
+          .map((e) => editor.nodeAtPath([e])!)
+          .whereType<TextNode>()
+          .toList(growable: false);
+      expect(textNodes[0].toPlainText(), '0123ABC');
+    });
+
     testWidgets('test replaceTexts, textNodes.length < texts.length',
         (tester) async {
       TestWidgetsFlutterBinding.ensureInitialized();

+ 35 - 1
frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart

@@ -10,7 +10,8 @@ void main() async {
   });
 
   group('slash_handler.dart', () {
-    testWidgets('Presses / to trigger selection menu', (tester) async {
+    testWidgets('Presses / to trigger selection menu in 0 index',
+        (tester) async {
       const text = 'Welcome to Appflowy 😁';
       const lines = 3;
       final editor = tester.editor;
@@ -41,5 +42,38 @@ void main() async {
         findsNothing,
       );
     });
+
+    testWidgets('Presses / to trigger selection menu in not 0 index',
+        (tester) async {
+      const text = 'Welcome to Appflowy 😁';
+      const lines = 3;
+      final editor = tester.editor;
+      for (var i = 0; i < lines; i++) {
+        editor.insertTextNode(text);
+      }
+      await editor.startTesting();
+      await editor.updateSelection(Selection.single(path: [1], startOffset: 5));
+      await editor.pressLogicKey(LogicalKeyboardKey.slash);
+
+      await tester.pumpAndSettle(const Duration(milliseconds: 1000));
+
+      expect(
+        find.byType(SelectionMenuWidget, skipOffstage: false),
+        findsOneWidget,
+      );
+
+      for (final item in defaultSelectionMenuItems) {
+        expect(find.text(item.name), findsOneWidget);
+      }
+
+      await editor.updateSelection(Selection.single(path: [1], startOffset: 0));
+
+      await tester.pumpAndSettle(const Duration(milliseconds: 200));
+
+      expect(
+        find.byType(SelectionMenuItemWidget, skipOffstage: false),
+        findsNothing,
+      );
+    });
   });
 }

+ 3 - 0
frontend/appflowy_flutter/packages/appflowy_editor/test/service/toolbar_service_test.dart

@@ -94,6 +94,7 @@ void main() async {
       await editor.updateSelection(
         Selection.single(path: [1], startOffset: 0, endOffset: text.length * 2),
       );
+      await tester.pumpAndSettle(const Duration(milliseconds: 500));
       testHighlight(false);
 
       await editor.updateSelection(
@@ -103,6 +104,7 @@ void main() async {
           endOffset: text.length * 2,
         ),
       );
+      await tester.pumpAndSettle(const Duration(milliseconds: 500));
       testHighlight(true);
 
       await editor.updateSelection(
@@ -112,6 +114,7 @@ void main() async {
           endOffset: text.length * 2 - 2,
         ),
       );
+      await tester.pumpAndSettle(const Duration(milliseconds: 500));
       testHighlight(true);
     });
 

+ 8 - 1
frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart

@@ -72,6 +72,7 @@ class Popover extends StatefulWidget {
   final PopoverDirection direction;
 
   final void Function()? onClose;
+  final Future<bool> Function()? canClose;
 
   final bool asBarrier;
 
@@ -92,6 +93,7 @@ class Popover extends StatefulWidget {
     this.mutex,
     this.windowPadding,
     this.onClose,
+    this.canClose,
     this.asBarrier = false,
   }) : super(key: key);
 
@@ -122,7 +124,12 @@ class PopoverState extends State<Popover> {
         children.add(
           PopoverMask(
             decoration: widget.maskDecoration,
-            onTap: () => _removeRootOverlay(),
+            onTap: () async {
+              if (!(await widget.canClose?.call() ?? true)) {
+                return;
+              }
+              _removeRootOverlay();
+            },
             onExit: () => _removeRootOverlay(),
           ),
         );

+ 14 - 4
frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart

@@ -10,11 +10,13 @@ class AppFlowyPopover extends StatelessWidget {
   final int triggerActions;
   final BoxConstraints constraints;
   final void Function()? onClose;
+  final Future<bool> Function()? canClose;
   final PopoverMutex? mutex;
   final Offset? offset;
   final bool asBarrier;
   final EdgeInsets margin;
   final EdgeInsets windowPadding;
+  final Decoration? decoration;
 
   const AppFlowyPopover({
     Key? key,
@@ -22,6 +24,7 @@ class AppFlowyPopover extends StatelessWidget {
     required this.popupBuilder,
     this.direction = PopoverDirection.rightWithTopAligned,
     this.onClose,
+    this.canClose,
     this.constraints = const BoxConstraints(maxWidth: 240, maxHeight: 600),
     this.mutex,
     this.triggerActions = PopoverTriggerFlags.click,
@@ -30,6 +33,7 @@ class AppFlowyPopover extends StatelessWidget {
     this.asBarrier = false,
     this.margin = const EdgeInsets.all(6),
     this.windowPadding = const EdgeInsets.all(8.0),
+    this.decoration,
   }) : super(key: key);
 
   @override
@@ -37,6 +41,7 @@ class AppFlowyPopover extends StatelessWidget {
     return Popover(
       controller: controller,
       onClose: onClose,
+      canClose: canClose,
       direction: direction,
       mutex: mutex,
       asBarrier: asBarrier,
@@ -49,6 +54,7 @@ class AppFlowyPopover extends StatelessWidget {
         return _PopoverContainer(
           constraints: constraints,
           margin: margin,
+          decoration: decoration,
           child: child,
         );
       },
@@ -61,19 +67,23 @@ class _PopoverContainer extends StatelessWidget {
   final Widget child;
   final BoxConstraints constraints;
   final EdgeInsets margin;
+  final Decoration? decoration;
+
   const _PopoverContainer({
     required this.child,
     required this.margin,
     required this.constraints,
+    required this.decoration,
     Key? key,
   }) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
-    final decoration = FlowyDecoration.decoration(
-      Theme.of(context).colorScheme.surface,
-      Theme.of(context).colorScheme.shadow.withOpacity(0.15),
-    );
+    final decoration = this.decoration ??
+        FlowyDecoration.decoration(
+          Theme.of(context).colorScheme.surface,
+          Theme.of(context).colorScheme.shadow.withOpacity(0.15),
+        );
 
     return Material(
       type: MaterialType.transparency,

+ 4 - 2
frontend/appflowy_flutter/windows/CMakeLists.txt

@@ -1,7 +1,7 @@
 cmake_minimum_required(VERSION 3.14)
 project(appflowy_flutter LANGUAGES CXX)
 
-set(BINARY_NAME "appflowy_flutter")
+set(BINARY_NAME "AppFlowy")
 
 cmake_policy(SET CMP0063 NEW)
 
@@ -9,6 +9,7 @@ set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
 
 # Configure build options.
 get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
+
 if(IS_MULTICONFIG)
   set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
     CACHE STRING "" FORCE)
@@ -50,14 +51,15 @@ add_subdirectory("runner")
 # them to the application.
 include(flutter/generated_plugins.cmake)
 
-
 # === Installation ===
 # Support files are copied into place next to the executable, so that it can
 # run in place. This is done instead of making a separate bundle (as on Linux)
 # so that building and running from within Visual Studio will work.
 set(BUILD_BUNDLE_DIR "$<TARGET_FILE_DIR:${BINARY_NAME}>")
+
 # Make the "install" step default, as it's required to run.
 set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
+
 if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
   set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
 endif()

+ 22 - 5
frontend/appflowy_flutter/windows/runner/CMakeLists.txt

@@ -1,6 +1,11 @@
 cmake_minimum_required(VERSION 3.14)
 project(runner LANGUAGES CXX)
 
+# Define the application target. To change its name, change BINARY_NAME in the
+# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
+# work.
+#
+# Any new source files that you add to the application should be added here.
 add_executable(${BINARY_NAME} WIN32
   "flutter_window.cpp"
   "main.cpp"
@@ -10,13 +15,25 @@ add_executable(${BINARY_NAME} WIN32
   "Runner.rc"
   "runner.exe.manifest"
 )
+
+# Apply the standard set of build settings. This can be removed for applications
+# that need different build settings.
 apply_standard_settings(${BINARY_NAME})
+
+# Add preprocessor definitions for the build version.
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
+
+# Disable Windows macros that collide with C++ standard library functions.
 target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
+
+# Add dependency libraries and include directories. Add any application-specific
+# dependencies here.
 target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
 target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
-add_dependencies(${BINARY_NAME} flutter_assemble)
-
 
-# === Flutter Library ===
-#set(DART_FFI "${CMAKE_CURRENT_SOURCE_DIR}/dart_ffi/dart_ffi.dll")
-#set(DART_FFI ${DART_FFI} PARENT_SCOPE)
+# Run the Flutter tool portions of the build. This must not be removed.
+add_dependencies(${BINARY_NAME} flutter_assemble)

+ 10 - 10
frontend/appflowy_flutter/windows/runner/Runner.rc

@@ -60,14 +60,14 @@ IDI_APP_ICON            ICON                    "resources\\app_icon.ico"
 // Version
 //
 
-#ifdef FLUTTER_BUILD_NUMBER
-#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER
+#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
+#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
 #else
-#define VERSION_AS_NUMBER 1,0,0
+#define VERSION_AS_NUMBER 1,0,0,0
 #endif
 
-#ifdef FLUTTER_BUILD_NAME
-#define VERSION_AS_STRING #FLUTTER_BUILD_NAME
+#if defined(FLUTTER_VERSION)
+#define VERSION_AS_STRING FLUTTER_VERSION
 #else
 #define VERSION_AS_STRING "1.0.0"
 #endif
@@ -89,13 +89,13 @@ BEGIN
     BEGIN
         BLOCK "040904e4"
         BEGIN
-            VALUE "CompanyName", "com.example" "\0"
+            VALUE "CompanyName", "io.appflowy" "\0"
             VALUE "FileDescription", "AppFlowy" "\0"
             VALUE "FileVersion", VERSION_AS_STRING "\0"
-            VALUE "InternalName", "appflowy_flutter" "\0"
-            VALUE "LegalCopyright", "Copyright (C) 2021 com.example. All rights reserved." "\0"
-            VALUE "OriginalFilename", "appflowy_flutter.exe" "\0"
-            VALUE "ProductName", "appflowy_flutter" "\0"
+            VALUE "InternalName", "AppFlowy" "\0"
+            VALUE "LegalCopyright", "Copyright (C) 2023 io.appflowy. All rights reserved." "\0"
+            VALUE "OriginalFilename", "AppFlowy.exe" "\0"
+            VALUE "ProductName", "AppFlowy" "\0"
             VALUE "ProductVersion", VERSION_AS_STRING "\0"
         END
     END

+ 8 - 4
frontend/appflowy_flutter/windows/runner/main.cpp

@@ -6,10 +6,12 @@
 #include "utils.h"
 
 int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
-                      _In_ wchar_t *command_line, _In_ int show_command) {
+                      _In_ wchar_t *command_line, _In_ int show_command)
+{
   // Attach to console when present (e.g., 'flutter run') or create a
   // new console when running with a debugger.
-  if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
+  if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent())
+  {
     CreateAndAttachConsole();
   }
 
@@ -27,13 +29,15 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
   FlutterWindow window(project);
   Win32Window::Point origin(10, 10);
   Win32Window::Size size(1280, 720);
-  if (!window.CreateAndShow(L"appflowy_flutter", origin, size)) {
+  if (!window.CreateAndShow(L"AppFlowy", origin, size))
+  {
     return EXIT_FAILURE;
   }
   window.SetQuitOnClose(true);
 
   ::MSG msg;
-  while (::GetMessage(&msg, nullptr, 0, 0)) {
+  while (::GetMessage(&msg, nullptr, 0, 0))
+  {
     ::TranslateMessage(&msg);
     ::DispatchMessage(&msg);
   }

+ 1 - 1
frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.desktop

@@ -2,6 +2,6 @@
 Type=Application
 Name=AppFlowy
 Icon=io.appflowy.AppFlowy
-Exec=env GDK_GL=gles appflowy_flutter %U
+Exec=env GDK_GL=gles AppFlowy %U
 Categories=Network;Productivity;
 Keywords=Notes

+ 3 - 3
frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.yml

@@ -2,7 +2,7 @@ app-id: io.appflowy.AppFlowy
 runtime: org.freedesktop.Platform
 runtime-version: '21.08'
 sdk: org.freedesktop.Sdk
-command: appflowy_flutter
+command: AppFlowy
 separate-locales: false
 finish-args:
   - --share=ipc
@@ -18,10 +18,10 @@ modules:
     build-commands:
       # - ls .
       - cp -r appflowy /app/appflowy
-      - chmod +x /app/appflowy/appflowy_flutter
+      - chmod +x /app/appflowy/AppFlowy
       - install -Dm644 logo.svg /app/share/icons/hicolor/scalable/apps/io.appflowy.AppFlowy.svg
       - mkdir /app/bin
-      - ln -s /app/appflowy/appflowy_flutter /app/bin/appflowy_flutter
+      - ln -s /app/appflowy/AppFlowy /app/bin/AppFlowy
       - install -Dm644 io.appflowy.AppFlowy.desktop /app/share/applications/io.appflowy.AppFlowy.desktop
     sources:
       - type: archive

+ 5 - 5
frontend/scripts/linux_installer/postinst

@@ -1,7 +1,7 @@
 #!/bin/bash
-if [ -e /usr/local/bin/appflowy ]; then
-echo "Symlink already exists, skipping."
+if [ -e /usr/local/bin/AppFlowy ]; then
+    echo "Symlink already exists, skipping."
 else
-echo "Creating Symlink in /usr/local/bin/appflowy"
-ln -s /opt/AppFlowy/appflowy_flutter /usr/local/bin/appflowy
-fi
+    echo "Creating Symlink in /usr/local/bin/appflowy"
+    ln -s /opt/AppFlowy/AppFlowy /usr/local/bin/AppFlowy
+fi

+ 3 - 3
frontend/scripts/windows_installer/inno_setup_config.iss

@@ -7,15 +7,15 @@ SolidCompression=yes
 DefaultDirName={autopf}\AppFlowy\
 DefaultGroupName=AppFlowy
 SetupIconFile=flowy_logo.ico
-UninstallDisplayIcon={app}\appflowy_flutter.exe
+UninstallDisplayIcon={app}\AppFlowy.exe
 UninstallDisplayName=AppFlowy
 AppPublisher=AppFlowy-IO
 VersionInfoVersion={#AppVersion}
 
 [Files]
-Source: "AppFlowy\AppFlowy.exe";DestDir: "{app}";DestName: "appflowy_flutter.exe"
+Source: "AppFlowy\AppFlowy.exe";DestDir: "{app}";DestName: "AppFlowy.exe"
 Source: "AppFlowy\*";DestDir: "{app}"
 Source: "AppFlowy\data\*";DestDir: "{app}\data\"; Flags: recursesubdirs
 
 [Icons]
-Name: "{group}\AppFlowy";Filename: "{app}\appflowy_flutter.exe"
+Name: "{group}\AppFlowy";Filename: "{app}\AppFlowy.exe"