diff --git a/CHANGELOG.md b/CHANGELOG.md index 392f307d7e..25eef12f1c 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml index 8ca63bfa72..1fc240a42a 100644 --- a/frontend/Makefile.toml +++ b/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 diff --git a/frontend/appflowy_flutter/.metadata b/frontend/appflowy_flutter/.metadata index a8ebbd603e..9068867840 100644 --- a/frontend/appflowy_flutter/.metadata +++ b/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' diff --git a/frontend/appflowy_flutter/android/app/build.gradle b/frontend/appflowy_flutter/android/app/build.gradle index 216ec0c1fb..948e9b7f42 100644 --- a/frontend/appflowy_flutter/android/app/build.gradle +++ b/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() diff --git a/frontend/appflowy_flutter/android/app/src/debug/AndroidManifest.xml b/frontend/appflowy_flutter/android/app/src/debug/AndroidManifest.xml index 49c97148ad..7d5632662e 100644 --- a/frontend/appflowy_flutter/android/app/src/debug/AndroidManifest.xml +++ b/frontend/appflowy_flutter/android/app/src/debug/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="io.appflowy.appflowy"> diff --git a/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml b/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml index 96e75259ad..264e1d3232 100644 --- a/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml +++ b/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="io.appflowy.appflowy"> + package="io.appflowy.appflowy"> diff --git a/frontend/appflowy_flutter/assets/translations/en.json b/frontend/appflowy_flutter/assets/translations/en.json index 927e968b4e..9496a242b7 100644 --- a/frontend/appflowy_flutter/assets/translations/en.json +++ b/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": { diff --git a/frontend/appflowy_flutter/assets/translations/pt-BR.json b/frontend/appflowy_flutter/assets/translations/pt-BR.json index aebe816ff0..512de346d4 100644 --- a/frontend/appflowy_flutter/assets/translations/pt-BR.json +++ b/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", diff --git a/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj b/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj index c5e6758eb6..385c983d7f 100644 --- a/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj +++ b/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; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart index 24c664abe4..ccb4b08866 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart +++ b/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 diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/util/learn_more_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/util/learn_more_action.dart new file mode 100644 index 0000000000..abdeeb162e --- /dev/null +++ b/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 openLearnMorePage() async { + final uri = Uri.parse( + 'https://appflowy.gitbook.io/docs/essential-documentation/appflowy-x-openai'); + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart index c5d7a39946..046f99f65a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart +++ b/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, + ), + ), + ), ], ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_plugins.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_plugins.dart index 88ba17b47d..ce9eb5dbef 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_plugins.dart +++ b/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, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart new file mode 100644 index 0000000000..b2f314c425 --- /dev/null +++ b/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, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart index 21678c16e5..053aaaa739 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart +++ b/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; } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart index 0d9c9c29fa..151ccc60d2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart +++ b/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 { @override NodeValidator 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 context) { - return _SmartEditInput( + return _HoverSmartInput( key: context.node.key, node: context.node, editorState: context.editorState, @@ -38,28 +37,111 @@ class SmartEditInputBuilder extends NodeWidgetBuilder { } } -class _SmartEditInput extends StatefulWidget { - final Node node; - - final EditorState editorState; - const _SmartEditInput({ - Key? key, +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(); + 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({ + 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? 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(); - 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 _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 _onExit() async { final transaction = widget.editorState.transaction; transaction.deleteNode(widget.node); @@ -246,35 +332,63 @@ class _SmartEditInputState extends State<_SmartEditInput> { ); } - Future> _requestEdits() async { + Future _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 _showError(String message) async { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + action: SnackBarAction( + label: LocaleKeys.button_Cancel.tr(), + onPressed: () { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + }, + ), + content: FlowyText(message), + ), + ); + } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_toolbar_item.dart index 844ea8df91..8db094e1dc 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_toolbar_item.dart +++ b/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'), }, ), ); diff --git a/frontend/appflowy_flutter/lib/util/debounce.dart b/frontend/appflowy_flutter/lib/util/debounce.dart new file mode 100644 index 0000000000..324818a650 --- /dev/null +++ b/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; + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart index 35e786ff38..1b6f7a9a6f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart +++ b/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() - .add(SettingsUserEvent.updateUserOpenAIKey(val)); + onChanged: (value) { + debounce.call(() { + context + .read() + .add(SettingsUserEvent.updateUserOpenAIKey(value)); + }); }, ); } + + @override + void dispose() { + debounce.dispose(); + super.dispose(); + } } class _CurrentIcon extends StatelessWidget { diff --git a/frontend/appflowy_flutter/linux/CMakeLists.txt b/frontend/appflowy_flutter/linux/CMakeLists.txt index 26146072f2..3c6927375c 100644 --- a/frontend/appflowy_flutter/linux/CMakeLists.txt +++ b/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) diff --git a/frontend/appflowy_flutter/linux/appflowy.desktop.temp b/frontend/appflowy_flutter/linux/appflowy.desktop.temp index d23fdec42b..2b189ef243 100644 --- a/frontend/appflowy_flutter/linux/appflowy.desktop.temp +++ b/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 \ No newline at end of file diff --git a/frontend/appflowy_flutter/linux/my_application.cc b/frontend/appflowy_flutter/linux/my_application.cc index d262d685a7..490ead4cd0 100644 --- a/frontend/appflowy_flutter/linux/my_application.cc +++ b/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, diff --git a/frontend/appflowy_flutter/macos/Runner/Configs/AppInfo.xcconfig b/frontend/appflowy_flutter/macos/Runner/Configs/AppInfo.xcconfig index d0b1c2acf9..656857119f 100644 --- a/frontend/appflowy_flutter/macos/Runner/Configs/AppInfo.xcconfig +++ b/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. diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/commands/command_extension.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/commands/command_extension.dart index 8a9648f6a5..71c3ef4a9d 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/commands/command_extension.dart +++ b/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 getTextInSelection( List textNodes, Selection selection, ) { @@ -77,6 +77,6 @@ extension CommandExtension on EditorState { } } } - return res.join('\n'); + return res; } } diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/core/transform/transaction.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/core/transform/transaction.dart index c1a311d648..15bce2a6b7 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/core/transform/transaction.dart +++ b/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; diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/select_all_handler.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/select_all_handler.dart index 800877b435..bc130fd6bf 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/select_all_handler.dart +++ b/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) { diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/selection_service.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/selection_service.dart index a2e716c9e2..7578407a09 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/selection_service.dart +++ b/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 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 selectionRects.clear(); clearSelection(); + _clearToolbar(); if (selection != null) { if (selection.isCollapsed) { @@ -286,6 +294,10 @@ class _AppFlowySelectionState extends State } 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 // } // } } + + final List _interceptors = []; + @override + void register(SelectionInterceptor interceptor) { + _interceptors.add(interceptor); + } + + @override + void unRegister(SelectionInterceptor interceptor) { + _interceptors.removeWhere((element) => element == interceptor); + } } diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/command/command_extension_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/command/command_extension_test.dart index 3fde48495f..45e5ed24f0 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/command/command_extension_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/command/command_extension_test.dart @@ -26,11 +26,11 @@ void main() { .editorState.service.selectionService.currentSelectedNodes .whereType() .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']); }); }); } diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/core/transform/transaction_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/core/transform/transaction_test.dart index 27a3701d84..8aa53dcabc 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/core/transform/transaction_test.dart +++ b/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() + .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() + .toList(growable: false); + expect(textNodes[0].toPlainText(), '0123ABC'); + }); + testWidgets('test replaceTexts, textNodes.length < texts.length', (tester) async { TestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart index a6e08d5fa8..fd45401acf 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart +++ b/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, + ); + }); }); } diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/toolbar_service_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/toolbar_service_test.dart index 86cd29705d..ce6430903d 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/toolbar_service_test.dart +++ b/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); }); diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart index 3cb0972112..fdb3628011 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart +++ b/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 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 { children.add( PopoverMask( decoration: widget.maskDecoration, - onTap: () => _removeRootOverlay(), + onTap: () async { + if (!(await widget.canClose?.call() ?? true)) { + return; + } + _removeRootOverlay(); + }, onExit: () => _removeRootOverlay(), ), ); diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart index bceadd6dd0..3673720220 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart +++ b/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 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, diff --git a/frontend/appflowy_flutter/windows/CMakeLists.txt b/frontend/appflowy_flutter/windows/CMakeLists.txt index 066bfe709e..5be6e64915 100644 --- a/frontend/appflowy_flutter/windows/CMakeLists.txt +++ b/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 "$") + # 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() diff --git a/frontend/appflowy_flutter/windows/runner/CMakeLists.txt b/frontend/appflowy_flutter/windows/runner/CMakeLists.txt index df6e8b99b5..17411a8ab8 100644 --- a/frontend/appflowy_flutter/windows/runner/CMakeLists.txt +++ b/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}") + +# Run the Flutter tool portions of the build. This must not be removed. 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) \ No newline at end of file diff --git a/frontend/appflowy_flutter/windows/runner/Runner.rc b/frontend/appflowy_flutter/windows/runner/Runner.rc index 1f8c964ed2..0639f41ec5 100644 --- a/frontend/appflowy_flutter/windows/runner/Runner.rc +++ b/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 diff --git a/frontend/appflowy_flutter/windows/runner/main.cpp b/frontend/appflowy_flutter/windows/runner/main.cpp index 8f38159b86..2f7c10b343 100644 --- a/frontend/appflowy_flutter/windows/runner/main.cpp +++ b/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); } diff --git a/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.desktop b/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.desktop index 1e01eb82bb..8b51c0c2ea 100644 --- a/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.desktop +++ b/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 diff --git a/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.yml b/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.yml index 96bcd02e2a..4a0af64e47 100644 --- a/frontend/scripts/flatpack-buildfiles/io.appflowy.AppFlowy.yml +++ b/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 diff --git a/frontend/scripts/linux_installer/postinst b/frontend/scripts/linux_installer/postinst index 9255abb598..4f495f86a2 100644 --- a/frontend/scripts/linux_installer/postinst +++ b/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 \ No newline at end of file + echo "Creating Symlink in /usr/local/bin/appflowy" + ln -s /opt/AppFlowy/AppFlowy /usr/local/bin/AppFlowy +fi diff --git a/frontend/scripts/windows_installer/inno_setup_config.iss b/frontend/scripts/windows_installer/inno_setup_config.iss index adb1e30fc1..dc477d384a 100644 --- a/frontend/scripts/windows_installer/inno_setup_config.iss +++ b/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" \ No newline at end of file +Name: "{group}\AppFlowy";Filename: "{app}\AppFlowy.exe" \ No newline at end of file