mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: render ai text message with appflowy_editor (#5682)
* feat: render ai text message with appflowy_editor * chore: update appflowy_editor * fix: integration test * feat: support paste inAppJson format * chore: update appflowy_editor
This commit is contained in:
parent
0fe383e538
commit
079b9888a8
@ -82,11 +82,11 @@ void main() {
|
|||||||
HeadingBlockKeys.type,
|
HeadingBlockKeys.type,
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
importedPageEditorState.getNodeAtPath([2])!.type,
|
importedPageEditorState.getNodeAtPath([1])!.type,
|
||||||
HeadingBlockKeys.type,
|
HeadingBlockKeys.type,
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
importedPageEditorState.getNodeAtPath([4])!.type,
|
importedPageEditorState.getNodeAtPath([2])!.type,
|
||||||
TableBlockKeys.type,
|
TableBlockKeys.type,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
|
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart';
|
import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_input.dart';
|
import 'package:appflowy/plugins/ai_chat/presentation/chat_input.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_popmenu.dart';
|
import 'package:appflowy/plugins/ai_chat/presentation/chat_popmenu.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||||
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/size.dart';
|
import 'package:flowy_infra/size.dart';
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
@ -176,9 +182,20 @@ class CopyButton extends StatelessWidget {
|
|||||||
size: const Size.square(14),
|
size: const Size.square(14),
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
Clipboard.setData(ClipboardData(text: textMessage.text));
|
final document = markdownToDocument(textMessage.text);
|
||||||
showMessageToast(LocaleKeys.grid_row_copyProperty.tr());
|
await getIt<ClipboardService>().setData(
|
||||||
|
ClipboardServiceData(
|
||||||
|
plainText: textMessage.text,
|
||||||
|
inAppJson: jsonEncode(document.toJson()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (context.mounted) {
|
||||||
|
showToastNotification(
|
||||||
|
context,
|
||||||
|
message: LocaleKeys.grid_url_copiedNotification.tr(),
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'chat_input.dart';
|
import 'chat_input.dart';
|
||||||
|
|
||||||
|
@ -0,0 +1,377 @@
|
|||||||
|
import 'package:appflowy/plugins/document/presentation/editor_configuration.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||||
|
import 'package:appflowy/util/theme_extension.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:markdown_widget/markdown_widget.dart';
|
||||||
|
|
||||||
|
import 'selectable_highlight.dart';
|
||||||
|
|
||||||
|
enum AIMarkdownType {
|
||||||
|
appflowyEditor,
|
||||||
|
markdownWidget,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap the appflowy_editor or markdown_widget as a chat text message widget
|
||||||
|
class AIMarkdownText extends StatelessWidget {
|
||||||
|
const AIMarkdownText({
|
||||||
|
super.key,
|
||||||
|
required this.markdown,
|
||||||
|
this.type = AIMarkdownType.appflowyEditor,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String markdown;
|
||||||
|
final AIMarkdownType type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
switch (type) {
|
||||||
|
case AIMarkdownType.appflowyEditor:
|
||||||
|
return _AppFlowyEditorMarkdown(markdown: markdown);
|
||||||
|
case AIMarkdownType.markdownWidget:
|
||||||
|
return _ThirdPartyMarkdown(markdown: markdown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppFlowyEditorMarkdown extends StatefulWidget {
|
||||||
|
const _AppFlowyEditorMarkdown({
|
||||||
|
required this.markdown,
|
||||||
|
});
|
||||||
|
|
||||||
|
// the text should be the markdown format
|
||||||
|
final String markdown;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_AppFlowyEditorMarkdown> createState() =>
|
||||||
|
_AppFlowyEditorMarkdownState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppFlowyEditorMarkdownState extends State<_AppFlowyEditorMarkdown> {
|
||||||
|
late EditorState editorState;
|
||||||
|
late final styleCustomizer = EditorStyleCustomizer(
|
||||||
|
context: context,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
);
|
||||||
|
late final editorStyle = styleCustomizer.style().copyWith(
|
||||||
|
// hide the cursor
|
||||||
|
cursorColor: Colors.transparent,
|
||||||
|
cursorWidth: 0,
|
||||||
|
);
|
||||||
|
late EditorScrollController scrollController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
editorState = _parseMarkdown(widget.markdown);
|
||||||
|
scrollController = EditorScrollController(
|
||||||
|
editorState: editorState,
|
||||||
|
shrinkWrap: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant _AppFlowyEditorMarkdown oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
|
||||||
|
if (oldWidget.markdown != widget.markdown) {
|
||||||
|
editorState.dispose();
|
||||||
|
editorState = _parseMarkdown(widget.markdown);
|
||||||
|
scrollController.dispose();
|
||||||
|
scrollController = EditorScrollController(
|
||||||
|
editorState: editorState,
|
||||||
|
shrinkWrap: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
scrollController.dispose();
|
||||||
|
editorState.dispose();
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final blockBuilders = getEditorBuilderMap(
|
||||||
|
context: context,
|
||||||
|
editorState: editorState,
|
||||||
|
styleCustomizer: styleCustomizer,
|
||||||
|
// the editor is not editable in the chat
|
||||||
|
editable: false,
|
||||||
|
);
|
||||||
|
return IntrinsicHeight(
|
||||||
|
child: AppFlowyEditor(
|
||||||
|
shrinkWrap: true,
|
||||||
|
// the editor is not editable in the chat
|
||||||
|
editable: false,
|
||||||
|
editorStyle: editorStyle,
|
||||||
|
editorScrollController: scrollController,
|
||||||
|
blockComponentBuilders: blockBuilders,
|
||||||
|
commandShortcutEvents: [customCopyCommand],
|
||||||
|
editorState: editorState,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorState _parseMarkdown(String markdown) {
|
||||||
|
final document = markdownToDocument(
|
||||||
|
markdown,
|
||||||
|
markdownParsers: [
|
||||||
|
const MarkdownCodeBlockParser(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
final editorState = EditorState(document: document);
|
||||||
|
return editorState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ThirdPartyMarkdown extends StatelessWidget {
|
||||||
|
const _ThirdPartyMarkdown({
|
||||||
|
required this.markdown,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String markdown;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MarkdownWidget(
|
||||||
|
data: markdown,
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
config: configFromContext(context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
MarkdownConfig configFromContext(BuildContext context) {
|
||||||
|
return MarkdownConfig(
|
||||||
|
configs: [
|
||||||
|
HrConfig(color: AFThemeExtension.of(context).textColor),
|
||||||
|
_ChatH1Config(
|
||||||
|
style: TextStyle(
|
||||||
|
color: AFThemeExtension.of(context).textColor,
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
dividerColor: AFThemeExtension.of(context).lightGreyHover,
|
||||||
|
),
|
||||||
|
_ChatH2Config(
|
||||||
|
style: TextStyle(
|
||||||
|
color: AFThemeExtension.of(context).textColor,
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
dividerColor: AFThemeExtension.of(context).lightGreyHover,
|
||||||
|
),
|
||||||
|
_ChatH3Config(
|
||||||
|
style: TextStyle(
|
||||||
|
color: AFThemeExtension.of(context).textColor,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
dividerColor: AFThemeExtension.of(context).lightGreyHover,
|
||||||
|
),
|
||||||
|
H4Config(
|
||||||
|
style: TextStyle(
|
||||||
|
color: AFThemeExtension.of(context).textColor,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
H5Config(
|
||||||
|
style: TextStyle(
|
||||||
|
color: AFThemeExtension.of(context).textColor,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
H6Config(
|
||||||
|
style: TextStyle(
|
||||||
|
color: AFThemeExtension.of(context).textColor,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PreConfig(
|
||||||
|
builder: (code, language) {
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: 800,
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(6.0)),
|
||||||
|
child: SelectableHighlightView(
|
||||||
|
code,
|
||||||
|
language: language,
|
||||||
|
theme: getHighlightTheme(context),
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
textStyle: TextStyle(
|
||||||
|
color: AFThemeExtension.of(context).textColor,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
PConfig(
|
||||||
|
textStyle: TextStyle(
|
||||||
|
color: AFThemeExtension.of(context).textColor,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
CodeConfig(
|
||||||
|
style: TextStyle(
|
||||||
|
color: AFThemeExtension.of(context).textColor,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BlockquoteConfig(
|
||||||
|
sideColor: AFThemeExtension.of(context).lightGreyHover,
|
||||||
|
textColor: AFThemeExtension.of(context).textColor,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, TextStyle> getHighlightTheme(BuildContext context) {
|
||||||
|
return {
|
||||||
|
'root': TextStyle(
|
||||||
|
color: const Color(0xffabb2bf),
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).isLightMode ? Colors.white : Colors.black38,
|
||||||
|
),
|
||||||
|
'comment': const TextStyle(
|
||||||
|
color: Color(0xff5c6370),
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
|
'quote': const TextStyle(
|
||||||
|
color: Color(0xff5c6370),
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
|
'doctag': const TextStyle(color: Color(0xffc678dd)),
|
||||||
|
'keyword': const TextStyle(color: Color(0xffc678dd)),
|
||||||
|
'formula': const TextStyle(color: Color(0xffc678dd)),
|
||||||
|
'section': const TextStyle(color: Color(0xffe06c75)),
|
||||||
|
'name': const TextStyle(color: Color(0xffe06c75)),
|
||||||
|
'selector-tag': const TextStyle(color: Color(0xffe06c75)),
|
||||||
|
'deletion': const TextStyle(color: Color(0xffe06c75)),
|
||||||
|
'subst': const TextStyle(color: Color(0xffe06c75)),
|
||||||
|
'literal': const TextStyle(color: Color(0xff56b6c2)),
|
||||||
|
'string': const TextStyle(color: Color(0xff98c379)),
|
||||||
|
'regexp': const TextStyle(color: Color(0xff98c379)),
|
||||||
|
'addition': const TextStyle(color: Color(0xff98c379)),
|
||||||
|
'attribute': const TextStyle(color: Color(0xff98c379)),
|
||||||
|
'meta-string': const TextStyle(color: Color(0xff98c379)),
|
||||||
|
'built_in': const TextStyle(color: Color(0xffe6c07b)),
|
||||||
|
'attr': const TextStyle(color: Color(0xffd19a66)),
|
||||||
|
'variable': const TextStyle(color: Color(0xffd19a66)),
|
||||||
|
'template-variable': const TextStyle(color: Color(0xffd19a66)),
|
||||||
|
'type': const TextStyle(color: Color(0xffd19a66)),
|
||||||
|
'selector-class': const TextStyle(color: Color(0xffd19a66)),
|
||||||
|
'selector-attr': const TextStyle(color: Color(0xffd19a66)),
|
||||||
|
'selector-pseudo': const TextStyle(color: Color(0xffd19a66)),
|
||||||
|
'number': const TextStyle(color: Color(0xffd19a66)),
|
||||||
|
'symbol': const TextStyle(color: Color(0xff61aeee)),
|
||||||
|
'bullet': const TextStyle(color: Color(0xff61aeee)),
|
||||||
|
'link': const TextStyle(color: Color(0xff61aeee)),
|
||||||
|
'meta': const TextStyle(color: Color(0xff61aeee)),
|
||||||
|
'selector-id': const TextStyle(color: Color(0xff61aeee)),
|
||||||
|
'title': const TextStyle(color: Color(0xff61aeee)),
|
||||||
|
'emphasis': const TextStyle(fontStyle: FontStyle.italic),
|
||||||
|
'strong': const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChatH1Config extends HeadingConfig {
|
||||||
|
const _ChatH1Config({
|
||||||
|
this.style = const TextStyle(
|
||||||
|
fontSize: 32,
|
||||||
|
height: 40 / 32,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
required this.dividerColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
final TextStyle style;
|
||||||
|
final Color dividerColor;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tag => MarkdownTag.h1.name;
|
||||||
|
|
||||||
|
@override
|
||||||
|
HeadingDivider? get divider => HeadingDivider(
|
||||||
|
space: 10,
|
||||||
|
color: dividerColor,
|
||||||
|
height: 10,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
///config class for h2
|
||||||
|
class _ChatH2Config extends HeadingConfig {
|
||||||
|
const _ChatH2Config({
|
||||||
|
this.style = const TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
height: 30 / 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
required this.dividerColor,
|
||||||
|
});
|
||||||
|
@override
|
||||||
|
final TextStyle style;
|
||||||
|
final Color dividerColor;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tag => MarkdownTag.h2.name;
|
||||||
|
|
||||||
|
@override
|
||||||
|
HeadingDivider? get divider => HeadingDivider(
|
||||||
|
space: 10,
|
||||||
|
color: dividerColor,
|
||||||
|
height: 10,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChatH3Config extends HeadingConfig {
|
||||||
|
const _ChatH3Config({
|
||||||
|
this.style = const TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
height: 30 / 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
required this.dividerColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
final TextStyle style;
|
||||||
|
final Color dividerColor;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tag => MarkdownTag.h3.name;
|
||||||
|
|
||||||
|
@override
|
||||||
|
HeadingDivider? get divider => HeadingDivider(
|
||||||
|
space: 10,
|
||||||
|
color: dividerColor,
|
||||||
|
height: 10,
|
||||||
|
);
|
||||||
|
}
|
@ -2,19 +2,15 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
|||||||
import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart';
|
import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
|
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_loading.dart';
|
import 'package:appflowy/plugins/ai_chat/presentation/chat_loading.dart';
|
||||||
import 'package:appflowy/util/theme_extension.dart';
|
import 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
||||||
import 'package:markdown_widget/markdown_widget.dart';
|
|
||||||
|
|
||||||
import 'selectable_highlight.dart';
|
|
||||||
|
|
||||||
class ChatAITextMessageWidget extends StatelessWidget {
|
class ChatAITextMessageWidget extends StatelessWidget {
|
||||||
const ChatAITextMessageWidget({
|
const ChatAITextMessageWidget({
|
||||||
@ -59,248 +55,12 @@ class ChatAITextMessageWidget extends StatelessWidget {
|
|||||||
if (state.text.isEmpty) {
|
if (state.text.isEmpty) {
|
||||||
return const ChatAILoading();
|
return const ChatAILoading();
|
||||||
} else {
|
} else {
|
||||||
return _textWidgetBuilder(user, context, state.text);
|
return AIMarkdownText(markdown: state.text);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _textWidgetBuilder(
|
|
||||||
User user,
|
|
||||||
BuildContext context,
|
|
||||||
String text,
|
|
||||||
) {
|
|
||||||
return MarkdownWidget(
|
|
||||||
data: text,
|
|
||||||
shrinkWrap: true,
|
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
config: configFromContext(context),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MarkdownConfig configFromContext(BuildContext context) {
|
|
||||||
return MarkdownConfig(
|
|
||||||
configs: [
|
|
||||||
HrConfig(color: AFThemeExtension.of(context).textColor),
|
|
||||||
ChatH1Config(
|
|
||||||
style: TextStyle(
|
|
||||||
color: AFThemeExtension.of(context).textColor,
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
height: 1.5,
|
|
||||||
),
|
|
||||||
dividerColor: AFThemeExtension.of(context).lightGreyHover,
|
|
||||||
),
|
|
||||||
ChatH2Config(
|
|
||||||
style: TextStyle(
|
|
||||||
color: AFThemeExtension.of(context).textColor,
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
height: 1.5,
|
|
||||||
),
|
|
||||||
dividerColor: AFThemeExtension.of(context).lightGreyHover,
|
|
||||||
),
|
|
||||||
ChatH3Config(
|
|
||||||
style: TextStyle(
|
|
||||||
color: AFThemeExtension.of(context).textColor,
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
height: 1.5,
|
|
||||||
),
|
|
||||||
dividerColor: AFThemeExtension.of(context).lightGreyHover,
|
|
||||||
),
|
|
||||||
H4Config(
|
|
||||||
style: TextStyle(
|
|
||||||
color: AFThemeExtension.of(context).textColor,
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
height: 1.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
H5Config(
|
|
||||||
style: TextStyle(
|
|
||||||
color: AFThemeExtension.of(context).textColor,
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
height: 1.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
H6Config(
|
|
||||||
style: TextStyle(
|
|
||||||
color: AFThemeExtension.of(context).textColor,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
height: 1.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
PreConfig(
|
|
||||||
builder: (code, language) {
|
|
||||||
return ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(
|
|
||||||
minWidth: 800,
|
|
||||||
),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(6.0)),
|
|
||||||
child: SelectableHighlightView(
|
|
||||||
code,
|
|
||||||
language: language,
|
|
||||||
theme: getHightlineTheme(context),
|
|
||||||
padding: const EdgeInsets.all(14),
|
|
||||||
textStyle: TextStyle(
|
|
||||||
color: AFThemeExtension.of(context).textColor,
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
height: 1.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
PConfig(
|
|
||||||
textStyle: TextStyle(
|
|
||||||
color: AFThemeExtension.of(context).textColor,
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
height: 1.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
CodeConfig(
|
|
||||||
style: TextStyle(
|
|
||||||
color: AFThemeExtension.of(context).textColor,
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
height: 1.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
BlockquoteConfig(
|
|
||||||
sideColor: AFThemeExtension.of(context).lightGreyHover,
|
|
||||||
textColor: AFThemeExtension.of(context).textColor,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, TextStyle> getHightlineTheme(BuildContext context) {
|
|
||||||
return {
|
|
||||||
'root': TextStyle(
|
|
||||||
color: const Color(0xffabb2bf),
|
|
||||||
backgroundColor:
|
|
||||||
Theme.of(context).isLightMode ? Colors.white : Colors.black38,
|
|
||||||
),
|
|
||||||
'comment':
|
|
||||||
const TextStyle(color: Color(0xff5c6370), fontStyle: FontStyle.italic),
|
|
||||||
'quote':
|
|
||||||
const TextStyle(color: Color(0xff5c6370), fontStyle: FontStyle.italic),
|
|
||||||
'doctag': const TextStyle(color: Color(0xffc678dd)),
|
|
||||||
'keyword': const TextStyle(color: Color(0xffc678dd)),
|
|
||||||
'formula': const TextStyle(color: Color(0xffc678dd)),
|
|
||||||
'section': const TextStyle(color: Color(0xffe06c75)),
|
|
||||||
'name': const TextStyle(color: Color(0xffe06c75)),
|
|
||||||
'selector-tag': const TextStyle(color: Color(0xffe06c75)),
|
|
||||||
'deletion': const TextStyle(color: Color(0xffe06c75)),
|
|
||||||
'subst': const TextStyle(color: Color(0xffe06c75)),
|
|
||||||
'literal': const TextStyle(color: Color(0xff56b6c2)),
|
|
||||||
'string': const TextStyle(color: Color(0xff98c379)),
|
|
||||||
'regexp': const TextStyle(color: Color(0xff98c379)),
|
|
||||||
'addition': const TextStyle(color: Color(0xff98c379)),
|
|
||||||
'attribute': const TextStyle(color: Color(0xff98c379)),
|
|
||||||
'meta-string': const TextStyle(color: Color(0xff98c379)),
|
|
||||||
'built_in': const TextStyle(color: Color(0xffe6c07b)),
|
|
||||||
'attr': const TextStyle(color: Color(0xffd19a66)),
|
|
||||||
'variable': const TextStyle(color: Color(0xffd19a66)),
|
|
||||||
'template-variable': const TextStyle(color: Color(0xffd19a66)),
|
|
||||||
'type': const TextStyle(color: Color(0xffd19a66)),
|
|
||||||
'selector-class': const TextStyle(color: Color(0xffd19a66)),
|
|
||||||
'selector-attr': const TextStyle(color: Color(0xffd19a66)),
|
|
||||||
'selector-pseudo': const TextStyle(color: Color(0xffd19a66)),
|
|
||||||
'number': const TextStyle(color: Color(0xffd19a66)),
|
|
||||||
'symbol': const TextStyle(color: Color(0xff61aeee)),
|
|
||||||
'bullet': const TextStyle(color: Color(0xff61aeee)),
|
|
||||||
'link': const TextStyle(color: Color(0xff61aeee)),
|
|
||||||
'meta': const TextStyle(color: Color(0xff61aeee)),
|
|
||||||
'selector-id': const TextStyle(color: Color(0xff61aeee)),
|
|
||||||
'title': const TextStyle(color: Color(0xff61aeee)),
|
|
||||||
'emphasis': const TextStyle(fontStyle: FontStyle.italic),
|
|
||||||
'strong': const TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChatH1Config extends HeadingConfig {
|
|
||||||
const ChatH1Config({
|
|
||||||
this.style = const TextStyle(
|
|
||||||
fontSize: 32,
|
|
||||||
height: 40 / 32,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
required this.dividerColor,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
final TextStyle style;
|
|
||||||
final Color dividerColor;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get tag => MarkdownTag.h1.name;
|
|
||||||
|
|
||||||
@override
|
|
||||||
HeadingDivider? get divider => HeadingDivider(
|
|
||||||
space: 10,
|
|
||||||
color: dividerColor,
|
|
||||||
height: 10,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
///config class for h2
|
|
||||||
class ChatH2Config extends HeadingConfig {
|
|
||||||
const ChatH2Config({
|
|
||||||
this.style = const TextStyle(
|
|
||||||
fontSize: 24,
|
|
||||||
height: 30 / 24,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
required this.dividerColor,
|
|
||||||
});
|
|
||||||
@override
|
|
||||||
final TextStyle style;
|
|
||||||
final Color dividerColor;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get tag => MarkdownTag.h2.name;
|
|
||||||
|
|
||||||
@override
|
|
||||||
HeadingDivider? get divider => HeadingDivider(
|
|
||||||
space: 10,
|
|
||||||
color: dividerColor,
|
|
||||||
height: 10,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChatH3Config extends HeadingConfig {
|
|
||||||
const ChatH3Config({
|
|
||||||
this.style = const TextStyle(
|
|
||||||
fontSize: 24,
|
|
||||||
height: 30 / 24,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
required this.dividerColor,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
final TextStyle style;
|
|
||||||
final Color dividerColor;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get tag => MarkdownTag.h3.name;
|
|
||||||
|
|
||||||
@override
|
|
||||||
HeadingDivider? get divider => HeadingDivider(
|
|
||||||
space: 10,
|
|
||||||
color: dividerColor,
|
|
||||||
height: 10,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class StreamingError extends StatelessWidget {
|
class StreamingError extends StatelessWidget {
|
||||||
|
@ -52,7 +52,6 @@ class TextMessageText extends StatelessWidget {
|
|||||||
text,
|
text,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
lineHeight: 1.5,
|
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
color: AFThemeExtension.of(context).textColor,
|
color: AFThemeExtension.of(context).textColor,
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
|
||||||
|
class MarkdownCodeBlockParser extends CustomMarkdownParser {
|
||||||
|
const MarkdownCodeBlockParser();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Node> transform(
|
||||||
|
md.Node element,
|
||||||
|
List<CustomMarkdownParser> parsers, {
|
||||||
|
MarkdownListType listType = MarkdownListType.unknown,
|
||||||
|
int? startNumber,
|
||||||
|
}) {
|
||||||
|
if (element is! md.Element) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.tag != 'pre') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final ec = element.children;
|
||||||
|
if (ec == null || ec.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final code = ec.first;
|
||||||
|
if (code is! md.Element || code.tag != 'code') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
String? language;
|
||||||
|
if (code.attributes.containsKey('class')) {
|
||||||
|
final classes = code.attributes['class']!.split(' ');
|
||||||
|
final languageClass = classes.firstWhere(
|
||||||
|
(c) => c.startsWith('language-'),
|
||||||
|
orElse: () => '',
|
||||||
|
);
|
||||||
|
language = languageClass.substring('language-'.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
final deltaDecoder = DeltaMarkdownDecoder();
|
||||||
|
|
||||||
|
return [
|
||||||
|
codeBlockNode(
|
||||||
|
language: language,
|
||||||
|
delta: deltaDecoder.convertNodes(code.children),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
export 'callout_node_parser.dart';
|
export 'callout_node_parser.dart';
|
||||||
|
export 'markdown_code_parser.dart';
|
||||||
export 'math_equation_node_parser.dart';
|
export 'math_equation_node_parser.dart';
|
||||||
export 'toggle_list_node_parser.dart';
|
export 'toggle_list_node_parser.dart';
|
||||||
|
@ -4,6 +4,7 @@ import 'dart:io';
|
|||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
|
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/migration/editor_migration.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/migration/editor_migration.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/markdown_code_parser.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/share/import_service.dart';
|
import 'package:appflowy/workspace/application/settings/share/import_service.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/import/import_type.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/import/import_type.dart';
|
||||||
@ -220,7 +221,12 @@ class _ImportPanelState extends State<ImportPanel> {
|
|||||||
Uint8List? _documentDataFrom(ImportType importType, String data) {
|
Uint8List? _documentDataFrom(ImportType importType, String data) {
|
||||||
switch (importType) {
|
switch (importType) {
|
||||||
case ImportType.markdownOrText:
|
case ImportType.markdownOrText:
|
||||||
final document = markdownToDocument(data);
|
final document = markdownToDocument(
|
||||||
|
data,
|
||||||
|
markdownParsers: [
|
||||||
|
const MarkdownCodeBlockParser(),
|
||||||
|
],
|
||||||
|
);
|
||||||
return DocumentDataPBFromTo.fromDocument(document)?.writeToBuffer();
|
return DocumentDataPBFromTo.fromDocument(document)?.writeToBuffer();
|
||||||
case ImportType.historyDocument:
|
case ImportType.historyDocument:
|
||||||
final document = EditorMigration.migrateDocument(data);
|
final document = EditorMigration.migrateDocument(data);
|
||||||
|
@ -53,11 +53,11 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: e8ee051
|
ref: d2d9873
|
||||||
resolved-ref: e8ee051719eded6621ccdc2722f696411c020209
|
resolved-ref: d2d987312d3a667336c7e12c36da7dbbb62d66db
|
||||||
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
||||||
source: git
|
source: git
|
||||||
version: "3.0.0"
|
version: "3.1.0"
|
||||||
appflowy_editor_plugins:
|
appflowy_editor_plugins:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -121,6 +121,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.2.0"
|
||||||
|
barcode:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: barcode
|
||||||
|
sha256: ab180ce22c6555d77d45f0178a523669db67f95856e3378259ef2ffeb43e6003
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.8"
|
||||||
|
bidi:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: bidi
|
||||||
|
sha256: "1a7d0c696324b2089f72e7671fd1f1f64fef44c980f3cebc84e803967c597b63"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.10"
|
||||||
bitsdojo_window:
|
bitsdojo_window:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -965,6 +981,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
|
image:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image
|
||||||
|
sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.2.0"
|
||||||
image_gallery_saver:
|
image_gallery_saver:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1219,7 +1243,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.2.0"
|
||||||
markdown:
|
markdown:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: markdown
|
name: markdown
|
||||||
sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051
|
sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051
|
||||||
@ -1434,6 +1458,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.0+3"
|
version: "3.1.0+3"
|
||||||
|
pdf:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pdf
|
||||||
|
sha256: "81d5522bddc1ef5c28e8f0ee40b71708761753c163e0c93a40df56fd515ea0f0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.11.0"
|
||||||
|
pdf_widget_wrapper:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pdf_widget_wrapper
|
||||||
|
sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.4"
|
||||||
percent_indicator:
|
percent_indicator:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1554,6 +1594,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
|
printing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: printing
|
||||||
|
sha256: cc4b256a5a89d5345488e3318897b595867f5181b8c5ed6fc63bfa5f2044aec3
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.13.1"
|
||||||
process:
|
process:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1594,6 +1642,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.3"
|
version: "1.2.3"
|
||||||
|
qr:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: qr
|
||||||
|
sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.1"
|
||||||
realtime_client:
|
realtime_client:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -141,6 +141,7 @@ dependencies:
|
|||||||
shimmer: ^3.0.0
|
shimmer: ^3.0.0
|
||||||
isolates: ^3.0.3+8
|
isolates: ^3.0.3+8
|
||||||
markdown_widget: ^2.3.2+6
|
markdown_widget: ^2.3.2+6
|
||||||
|
markdown:
|
||||||
|
|
||||||
# Window Manager for MacOS and Linux
|
# Window Manager for MacOS and Linux
|
||||||
window_manager: ^0.3.9
|
window_manager: ^0.3.9
|
||||||
@ -187,7 +188,7 @@ dependency_overrides:
|
|||||||
appflowy_editor:
|
appflowy_editor:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||||
ref: "e8ee051"
|
ref: "d2d9873"
|
||||||
|
|
||||||
appflowy_editor_plugins:
|
appflowy_editor_plugins:
|
||||||
git:
|
git:
|
||||||
|
Loading…
Reference in New Issue
Block a user