mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: use new appflowy-editor-plugins package (#5147)
* feat: use new appflowy-editor-plugins package * fix: code block slash menu item * chore: update build runner dependency * chore: change dependency to pub.dev version * chore: revert generate: false in pubspec
This commit is contained in:
parent
49e9b8c358
commit
0a5f3e8fa1
@ -1,9 +1,10 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@ -23,11 +24,7 @@ void main() {
|
||||
// mock the clipboard
|
||||
const lines = 3;
|
||||
final text = List.generate(lines, (index) => 'line $index').join('\n');
|
||||
AppFlowyClipboard.mockSetData(
|
||||
AppFlowyClipboardData(
|
||||
text: text,
|
||||
),
|
||||
);
|
||||
AppFlowyClipboard.mockSetData(AppFlowyClipboardData(text: text));
|
||||
|
||||
await insertCodeBlockInDocument(tester);
|
||||
|
||||
|
@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_block_copy_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||
@ -148,11 +149,18 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
|
||||
configuration: configuration,
|
||||
),
|
||||
CodeBlockKeys.type: CodeBlockComponentBuilder(
|
||||
editorState: editorState,
|
||||
configuration: configuration.copyWith(
|
||||
textStyle: (_) => styleCustomizer.codeBlockStyleBuilder(),
|
||||
placeholderTextStyle: (_) => styleCustomizer.codeBlockStyleBuilder(),
|
||||
),
|
||||
styleBuilder: () => CodeBlockStyle(
|
||||
backgroundColor: AFThemeExtension.of(context).calloutBGColor,
|
||||
foregroundColor: AFThemeExtension.of(context).textColor.withAlpha(155),
|
||||
),
|
||||
padding: const EdgeInsets.only(left: 20, right: 30, bottom: 34),
|
||||
languagePickerBuilder: codeBlockLanguagePickerBuilder,
|
||||
copyButtonBuilder: codeBlockCopyBuilder,
|
||||
),
|
||||
AutoCompletionBlockKeys.type: AutoCompletionBlockComponentBuilder(),
|
||||
SmartEditBlockKeys.type: SmartEditBlockComponentBuilder(),
|
||||
|
@ -1,6 +1,9 @@
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_configuration.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/align_toolbar_item/custom_text_align_command.dart';
|
||||
@ -21,14 +24,32 @@ import 'package:appflowy/workspace/application/settings/shortcuts/settings_short
|
||||
import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
final codeBlockLocalization = CodeBlockLocalizations(
|
||||
codeBlockNewParagraph:
|
||||
LocaleKeys.settings_shortcuts_commands_codeBlockNewParagraph.tr(),
|
||||
codeBlockAddTwoSpaces:
|
||||
LocaleKeys.settings_shortcuts_commands_codeBlockAddTwoSpaces.tr(),
|
||||
codeBlockDeleteTwoSpaces:
|
||||
LocaleKeys.settings_shortcuts_commands_codeBlockDeleteTwoSpaces.tr(),
|
||||
codeBlockSelectAll:
|
||||
LocaleKeys.settings_shortcuts_commands_codeBlockSelectAll.tr(),
|
||||
codeBlockPasteText:
|
||||
LocaleKeys.settings_shortcuts_commands_codeBlockPasteText.tr(),
|
||||
);
|
||||
|
||||
final localizedCodeBlockCommands =
|
||||
codeBlockCommands(localizations: codeBlockLocalization);
|
||||
|
||||
final List<CommandShortcutEvent> commandShortcutEvents = [
|
||||
toggleToggleListCommand,
|
||||
...codeBlockCommands,
|
||||
...localizedCodeBlockCommands,
|
||||
customCopyCommand,
|
||||
customPasteCommand,
|
||||
customCutCommand,
|
||||
@ -90,7 +111,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
|
||||
late final List<CommandShortcutEvent> commandShortcutEvents = [
|
||||
toggleToggleListCommand,
|
||||
...codeBlockCommands,
|
||||
...localizedCodeBlockCommands,
|
||||
customCopyCommand,
|
||||
customPasteCommand,
|
||||
customCutCommand,
|
||||
@ -264,7 +285,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
final isRTL =
|
||||
context.read<AppearanceSettingsCubit>().state.layoutDirection ==
|
||||
LayoutDirection.rtlLayout;
|
||||
final textDirection = isRTL ? TextDirection.rtl : TextDirection.ltr;
|
||||
final textDirection = isRTL ? ui.TextDirection.rtl : ui.TextDirection.ltr;
|
||||
|
||||
_setRTLToolbarItems(
|
||||
context.read<AppearanceSettingsCubit>().state.enableRtlToolbarItems,
|
||||
@ -371,7 +392,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
calloutItem,
|
||||
outlineItem,
|
||||
mathEquationItem,
|
||||
codeBlockItem,
|
||||
codeBlockItem(LocaleKeys.document_selectionMenu_codeBlock.tr()),
|
||||
toggleListBlockItem,
|
||||
emojiMenuItem,
|
||||
autoGeneratorMenuItem,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_block_component.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
|
||||
const _greater = '>';
|
||||
const _equals = '=';
|
||||
|
@ -1,700 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_item_list_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_language_screen.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_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart' hide TextDirection;
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:highlight/highlight.dart' as highlight;
|
||||
import 'package:highlight/languages/all.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'code_block_themes.dart';
|
||||
|
||||
final supportedLanguages = [
|
||||
'Assembly',
|
||||
'Bash',
|
||||
'BASIC',
|
||||
'C',
|
||||
'C#',
|
||||
'CPP',
|
||||
'Clojure',
|
||||
'CS',
|
||||
'CSS',
|
||||
'Dart',
|
||||
'Docker',
|
||||
'Elixir',
|
||||
'Elm',
|
||||
'Erlang',
|
||||
'Fortran',
|
||||
'Go',
|
||||
'GraphQL',
|
||||
'Haskell',
|
||||
'HTML',
|
||||
'Java',
|
||||
'JavaScript',
|
||||
'JSON',
|
||||
'Kotlin',
|
||||
'LaTeX',
|
||||
'Lisp',
|
||||
'Lua',
|
||||
'Markdown',
|
||||
'MATLAB',
|
||||
'Objective-C',
|
||||
'OCaml',
|
||||
'Perl',
|
||||
'PHP',
|
||||
'PowerShell',
|
||||
'Python',
|
||||
'R',
|
||||
'Ruby',
|
||||
'Rust',
|
||||
'Scala',
|
||||
'Shell',
|
||||
'SQL',
|
||||
'Swift',
|
||||
'TypeScript',
|
||||
'Visual Basic',
|
||||
'XML',
|
||||
'YAML',
|
||||
];
|
||||
|
||||
final codeBlockSupportedLanguages = supportedLanguages
|
||||
.map((e) => e.toLowerCase())
|
||||
.toSet()
|
||||
.intersection(allLanguages.keys.toSet())
|
||||
.toList()
|
||||
..add('auto')
|
||||
..add('c')
|
||||
..sort();
|
||||
|
||||
class CodeBlockKeys {
|
||||
const CodeBlockKeys._();
|
||||
|
||||
static const String type = 'code';
|
||||
|
||||
/// The content of a code block.
|
||||
///
|
||||
/// The value is a String.
|
||||
static const String delta = 'delta';
|
||||
|
||||
/// The language of a code block.
|
||||
///
|
||||
/// The value is a String.
|
||||
static const String language = 'language';
|
||||
}
|
||||
|
||||
Node codeBlockNode({
|
||||
Delta? delta,
|
||||
String? language,
|
||||
}) {
|
||||
final attributes = {
|
||||
CodeBlockKeys.delta: (delta ?? Delta()).toJson(),
|
||||
CodeBlockKeys.language: language,
|
||||
};
|
||||
return Node(
|
||||
type: CodeBlockKeys.type,
|
||||
attributes: attributes,
|
||||
);
|
||||
}
|
||||
|
||||
// defining the callout block menu item for selection
|
||||
SelectionMenuItem codeBlockItem = SelectionMenuItem.node(
|
||||
getName: LocaleKeys.document_selectionMenu_codeBlock.tr,
|
||||
iconData: Icons.abc,
|
||||
keywords: ['code', 'codeblock'],
|
||||
nodeBuilder: (editorState, _) => codeBlockNode(),
|
||||
replace: (_, node) => node.delta?.isEmpty ?? false,
|
||||
);
|
||||
|
||||
const _interceptorKey = 'code-block-interceptor';
|
||||
|
||||
class CodeBlockComponentBuilder extends BlockComponentBuilder {
|
||||
CodeBlockComponentBuilder({
|
||||
super.configuration,
|
||||
this.padding = const EdgeInsets.all(0),
|
||||
});
|
||||
|
||||
final EdgeInsets padding;
|
||||
|
||||
@override
|
||||
BlockComponentWidget build(BlockComponentContext blockComponentContext) {
|
||||
final node = blockComponentContext.node;
|
||||
return CodeBlockComponentWidget(
|
||||
key: node.key,
|
||||
node: node,
|
||||
configuration: configuration,
|
||||
padding: padding,
|
||||
showActions: showActions(node),
|
||||
actionBuilder: (context, state) => actionBuilder(
|
||||
blockComponentContext,
|
||||
state,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool validate(Node node) => node.delta != null;
|
||||
}
|
||||
|
||||
class CodeBlockComponentWidget extends BlockComponentStatefulWidget {
|
||||
const CodeBlockComponentWidget({
|
||||
super.key,
|
||||
required super.node,
|
||||
super.showActions,
|
||||
super.actionBuilder,
|
||||
super.configuration = const BlockComponentConfiguration(),
|
||||
this.padding = const EdgeInsets.all(0),
|
||||
});
|
||||
|
||||
final EdgeInsets padding;
|
||||
|
||||
@override
|
||||
State<CodeBlockComponentWidget> createState() =>
|
||||
_CodeBlockComponentWidgetState();
|
||||
}
|
||||
|
||||
class _CodeBlockComponentWidgetState extends State<CodeBlockComponentWidget>
|
||||
with
|
||||
SelectableMixin,
|
||||
DefaultSelectableMixin,
|
||||
BlockComponentConfigurable,
|
||||
BlockComponentTextDirectionMixin {
|
||||
// the key used to forward focus to the richtext child
|
||||
@override
|
||||
final forwardKey = GlobalKey(debugLabel: 'code_flowy_rich_text');
|
||||
|
||||
@override
|
||||
GlobalKey<State<StatefulWidget>> blockComponentKey =
|
||||
GlobalKey(debugLabel: CodeBlockKeys.type);
|
||||
|
||||
@override
|
||||
BlockComponentConfiguration get configuration => widget.configuration;
|
||||
|
||||
@override
|
||||
GlobalKey<State<StatefulWidget>> get containerKey => node.key;
|
||||
|
||||
@override
|
||||
Node get node => widget.node;
|
||||
|
||||
@override
|
||||
late final editorState = context.read<EditorState>();
|
||||
|
||||
final popoverController = PopoverController();
|
||||
final scrollController = ScrollController();
|
||||
|
||||
// We use this to calculate the position of the cursor in the code block
|
||||
// for automatic scrolling.
|
||||
final codeBlockKey = GlobalKey();
|
||||
|
||||
String? get language => node.attributes[CodeBlockKeys.language] as String?;
|
||||
String? autoDetectLanguage;
|
||||
|
||||
bool isSelected = false;
|
||||
bool isHovering = false;
|
||||
bool canPanStart = true;
|
||||
|
||||
late final interceptor = SelectionGestureInterceptor(
|
||||
key: _interceptorKey,
|
||||
canTap: (_) => canPanStart && !isSelected,
|
||||
canPanStart: (_) => canPanStart && !isSelected,
|
||||
);
|
||||
|
||||
late final StreamSubscription<(TransactionTime, Transaction)>
|
||||
transactionSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
editorState.selectionService.registerGestureInterceptor(interceptor);
|
||||
editorState.selectionNotifier.addListener(calculateScrollPosition);
|
||||
transactionSubscription = editorState.transactionStream.listen((event) {
|
||||
if (event.$2.operations.any((op) => op.path.equals(node.path))) {
|
||||
calculateScrollPosition();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.dispose();
|
||||
editorState.selectionService.currentSelection
|
||||
.removeListener(calculateScrollPosition);
|
||||
editorState.selectionService.unregisterGestureInterceptor(_interceptorKey);
|
||||
transactionSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textDirection = calculateTextDirection(
|
||||
layoutDirection: Directionality.maybeOf(context),
|
||||
);
|
||||
|
||||
Widget child = MouseRegion(
|
||||
onEnter: (_) => setState(() => isHovering = true),
|
||||
onExit: (_) => setState(() => isHovering = false),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
|
||||
color: AFThemeExtension.of(context).calloutBGColor,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
textDirection: textDirection,
|
||||
children: [
|
||||
MouseRegion(
|
||||
onEnter: (_) => setState(() => canPanStart = false),
|
||||
onExit: (_) => setState(() => canPanStart = true),
|
||||
child: Opacity(
|
||||
opacity: isHovering || isSelected ? 1.0 : 0.0,
|
||||
child: Row(
|
||||
children: [
|
||||
_LanguageSelector(
|
||||
controller: popoverController,
|
||||
language: language,
|
||||
isSelected: isSelected,
|
||||
onLanguageSelected: updateLanguage,
|
||||
onMenuOpen: () => isSelected = true,
|
||||
onMenuClose: () => setState(() => isSelected = false),
|
||||
),
|
||||
const Spacer(),
|
||||
_CopyButton(node: node),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildCodeBlock(context, textDirection),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
child = Padding(key: blockComponentKey, padding: padding, child: child);
|
||||
|
||||
child = BlockSelectionContainer(
|
||||
node: node,
|
||||
delegate: this,
|
||||
listenable: editorState.selectionNotifier,
|
||||
blockColor: editorState.editorStyle.selectionColor,
|
||||
supportTypes: const [BlockSelectionType.block],
|
||||
child: child,
|
||||
);
|
||||
|
||||
if (PlatformExtension.isDesktopOrWeb) {
|
||||
if (widget.showActions && widget.actionBuilder != null) {
|
||||
child = BlockComponentActionWrapper(
|
||||
node: widget.node,
|
||||
actionBuilder: widget.actionBuilder!,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// show a fixed menu on mobile
|
||||
child = MobileBlockActionButtons(
|
||||
node: node,
|
||||
editorState: editorState,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
Widget _buildCodeBlock(BuildContext context, TextDirection textDirection) {
|
||||
final isLightMode = Theme.of(context).brightness == Brightness.light;
|
||||
final delta = node.delta ?? Delta();
|
||||
final content = delta.toPlainText();
|
||||
|
||||
final result = highlight.highlight.parse(
|
||||
content,
|
||||
language: language,
|
||||
autoDetection: language == null,
|
||||
);
|
||||
|
||||
autoDetectLanguage = language ?? result.language;
|
||||
|
||||
final codeNodes = result.nodes;
|
||||
if (codeNodes == null) {
|
||||
throw Exception('Code block parse error.');
|
||||
}
|
||||
|
||||
final codeTextSpans = _convert(codeNodes, isLightMode: isLightMode);
|
||||
final linesOfCode = delta.toPlainText().split('\n').length;
|
||||
|
||||
return Padding(
|
||||
padding: widget.padding,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_LinesOfCodeNumbers(
|
||||
linesOfCode: linesOfCode,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
key: codeBlockKey,
|
||||
controller: scrollController,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: AppFlowyRichText(
|
||||
key: forwardKey,
|
||||
delegate: this,
|
||||
node: widget.node,
|
||||
editorState: editorState,
|
||||
placeholderText: placeholderText,
|
||||
lineHeight: 1.5,
|
||||
textSpanDecorator: (_) => TextSpan(
|
||||
style: textStyle,
|
||||
children: codeTextSpans,
|
||||
),
|
||||
placeholderTextSpanDecorator: (textSpan) => textSpan,
|
||||
textDirection: textDirection,
|
||||
cursorColor: editorState.editorStyle.cursorColor,
|
||||
selectionColor: editorState.editorStyle.selectionColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateLanguage(String language) async {
|
||||
final transaction = editorState.transaction
|
||||
..updateNode(
|
||||
node,
|
||||
{CodeBlockKeys.language: language == 'auto' ? null : language},
|
||||
);
|
||||
await editorState.apply(transaction);
|
||||
}
|
||||
|
||||
void calculateScrollPosition() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final selection = editorState.selection;
|
||||
if (!mounted || selection == null || !selection.isCollapsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
final nodes = editorState.getNodesInSelection(selection);
|
||||
if (nodes.isEmpty || nodes.length > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
final selectedNode = nodes.first;
|
||||
if (selectedNode.path.equals(widget.node.path)) {
|
||||
final renderBox =
|
||||
codeBlockKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
final rects = editorState.selectionRects();
|
||||
if (renderBox == null || rects.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final codeBlockOffset = renderBox.localToGlobal(Offset.zero);
|
||||
final codeBlockSize = renderBox.size;
|
||||
|
||||
final cursorRect = rects.first;
|
||||
final cursorRelativeOffset = cursorRect.center - codeBlockOffset;
|
||||
|
||||
// If the relative position of the cursor is less than 1, and the scrollController
|
||||
// is not at offset 0, then we need to scroll to the left to make cursor visible.
|
||||
if (cursorRelativeOffset.dx < 1 && scrollController.offset > 0) {
|
||||
scrollController
|
||||
.jumpTo(scrollController.offset + cursorRelativeOffset.dx - 1);
|
||||
|
||||
// If the relative position of the cursor is greater than the width of the code block,
|
||||
// then we need to scroll to the right to make cursor visible.
|
||||
} else if (cursorRelativeOffset.dx > codeBlockSize.width - 1) {
|
||||
scrollController.jumpTo(
|
||||
scrollController.offset +
|
||||
cursorRelativeOffset.dx -
|
||||
codeBlockSize.width +
|
||||
1,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Copy from flutter.highlight package.
|
||||
// https://github.com/git-touch/highlight.dart/blob/master/flutter_highlight/lib/flutter_highlight.dart
|
||||
List<TextSpan> _convert(
|
||||
List<highlight.Node> nodes, {
|
||||
bool isLightMode = true,
|
||||
}) {
|
||||
final List<TextSpan> spans = [];
|
||||
List<TextSpan> currentSpans = spans;
|
||||
final List<List<TextSpan>> stack = [];
|
||||
|
||||
final codeblockTheme =
|
||||
isLightMode ? lightThemeInCodeblock : darkThemeInCodeBlock;
|
||||
|
||||
void traverse(highlight.Node node) {
|
||||
if (node.value != null) {
|
||||
currentSpans.add(
|
||||
node.className == null
|
||||
? TextSpan(text: node.value)
|
||||
: TextSpan(
|
||||
text: node.value,
|
||||
style: codeblockTheme[node.className!],
|
||||
),
|
||||
);
|
||||
} else if (node.children != null) {
|
||||
final List<TextSpan> tmp = [];
|
||||
currentSpans.add(
|
||||
TextSpan(
|
||||
children: tmp,
|
||||
style: codeblockTheme[node.className!],
|
||||
),
|
||||
);
|
||||
stack.add(currentSpans);
|
||||
currentSpans = tmp;
|
||||
|
||||
for (final n in node.children!) {
|
||||
traverse(n);
|
||||
if (n == node.children!.last) {
|
||||
currentSpans = stack.isEmpty ? spans : stack.removeLast();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final node in nodes) {
|
||||
traverse(node);
|
||||
}
|
||||
|
||||
return spans;
|
||||
}
|
||||
}
|
||||
|
||||
class _LinesOfCodeNumbers extends StatelessWidget {
|
||||
const _LinesOfCodeNumbers({
|
||||
required this.linesOfCode,
|
||||
required this.textStyle,
|
||||
});
|
||||
|
||||
final int linesOfCode;
|
||||
final TextStyle textStyle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
for (int i = 1; i <= linesOfCode; i++)
|
||||
Text(
|
||||
i.toString(),
|
||||
style: textStyle.copyWith(
|
||||
color: AFThemeExtension.of(context).textColor.withAlpha(155),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CopyButton extends StatelessWidget {
|
||||
const _CopyButton({required this.node});
|
||||
|
||||
final Node node;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: FlowyTooltip(
|
||||
message: LocaleKeys.document_codeBlock_copyTooltip.tr(),
|
||||
child: FlowyIconButton(
|
||||
onPressed: () async {
|
||||
await getIt<ClipboardService>().setData(
|
||||
ClipboardServiceData(
|
||||
plainText: node.delta?.toPlainText(),
|
||||
),
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
LocaleKeys.document_codeBlock_codeCopiedSnackbar.tr(),
|
||||
);
|
||||
}
|
||||
},
|
||||
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.copy_s,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LanguageSelector extends StatefulWidget {
|
||||
const _LanguageSelector({
|
||||
required this.controller,
|
||||
this.language,
|
||||
required this.isSelected,
|
||||
required this.onLanguageSelected,
|
||||
this.onMenuOpen,
|
||||
this.onMenuClose,
|
||||
});
|
||||
|
||||
final PopoverController controller;
|
||||
final String? language;
|
||||
final bool isSelected;
|
||||
final void Function(String) onLanguageSelected;
|
||||
final VoidCallback? onMenuOpen;
|
||||
final VoidCallback? onMenuClose;
|
||||
|
||||
@override
|
||||
State<_LanguageSelector> createState() => _LanguageSelectorState();
|
||||
}
|
||||
|
||||
class _LanguageSelectorState extends State<_LanguageSelector> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget child = Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 4.0),
|
||||
child: FlowyTextButton(
|
||||
widget.language?.capitalize() ??
|
||||
LocaleKeys.document_codeBlock_language_auto.tr(),
|
||||
constraints: const BoxConstraints(minWidth: 50),
|
||||
fontColor: Theme.of(context).colorScheme.onBackground,
|
||||
fillColor: widget.isSelected
|
||||
? Theme.of(context).colorScheme.secondaryContainer
|
||||
: Colors.transparent,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 4),
|
||||
onPressed: () async {
|
||||
if (PlatformExtension.isMobile) {
|
||||
final language = await context
|
||||
.push<String>(MobileCodeLanguagePickerScreen.routeName);
|
||||
if (language != null) {
|
||||
widget.onLanguageSelected(language);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (PlatformExtension.isDesktopOrWeb) {
|
||||
child = AppFlowyPopover(
|
||||
controller: widget.controller,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
onOpen: widget.onMenuOpen,
|
||||
constraints: const BoxConstraints(maxHeight: 300, maxWidth: 200),
|
||||
onClose: widget.onMenuClose,
|
||||
popupBuilder: (_) => _LanguageSelectionPopover(
|
||||
editorState: context.read<EditorState>(),
|
||||
language: widget.language,
|
||||
onLanguageSelected: (language) {
|
||||
widget.onLanguageSelected(language);
|
||||
widget.controller.close();
|
||||
},
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
class _LanguageSelectionPopover extends StatefulWidget {
|
||||
const _LanguageSelectionPopover({
|
||||
required this.editorState,
|
||||
required this.language,
|
||||
required this.onLanguageSelected,
|
||||
});
|
||||
|
||||
final EditorState editorState;
|
||||
final String? language;
|
||||
final void Function(String) onLanguageSelected;
|
||||
|
||||
@override
|
||||
State<_LanguageSelectionPopover> createState() =>
|
||||
_LanguageSelectionPopoverState();
|
||||
}
|
||||
|
||||
class _LanguageSelectionPopoverState extends State<_LanguageSelectionPopover> {
|
||||
final searchController = TextEditingController();
|
||||
final focusNode = FocusNode();
|
||||
|
||||
List<String> supportedLanguages =
|
||||
codeBlockSupportedLanguages.map((e) => e.capitalize()).toList();
|
||||
late int selectedIndex = supportedLanguages.indexOf(widget.language ?? '');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
// This is a workaround because longer taps might break the
|
||||
// focus, this might be an issue with the Flutter framework.
|
||||
(_) => Future.delayed(
|
||||
const Duration(milliseconds: 100),
|
||||
() => focusNode.requestFocus(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
focusNode.dispose();
|
||||
searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowyTextField(
|
||||
focusNode: focusNode,
|
||||
autoFocus: false,
|
||||
controller: searchController,
|
||||
hintText: LocaleKeys.document_codeBlock_searchLanguageHint.tr(),
|
||||
onChanged: (_) => setState(() {
|
||||
supportedLanguages = codeBlockSupportedLanguages
|
||||
.where((e) => e.contains(searchController.text.toLowerCase()))
|
||||
.map((e) => e.capitalize())
|
||||
.toList();
|
||||
selectedIndex =
|
||||
codeBlockSupportedLanguages.indexOf(widget.language ?? '');
|
||||
}),
|
||||
),
|
||||
const VSpace(8),
|
||||
Flexible(
|
||||
child: SelectableItemListMenu(
|
||||
shrinkWrap: true,
|
||||
items: supportedLanguages,
|
||||
selectedIndex: selectedIndex,
|
||||
onSelected: (index) =>
|
||||
widget.onLanguageSelected(supportedLanguages[index]),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.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_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
|
||||
CodeBlockCopyBuilder codeBlockCopyBuilder =
|
||||
(_, node) => _CopyButton(node: node);
|
||||
|
||||
class _CopyButton extends StatelessWidget {
|
||||
const _CopyButton({required this.node});
|
||||
|
||||
final Node node;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: FlowyTooltip(
|
||||
message: LocaleKeys.document_codeBlock_copyTooltip.tr(),
|
||||
child: FlowyIconButton(
|
||||
onPressed: () async {
|
||||
await getIt<ClipboardService>().setData(
|
||||
ClipboardServiceData(
|
||||
plainText: node.delta?.toPlainText(),
|
||||
),
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
LocaleKeys.document_codeBlock_codeCopiedSnackbar.tr(),
|
||||
);
|
||||
}
|
||||
},
|
||||
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.copy_s,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,193 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_item_list_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_language_screen.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
CodeBlockLanguagePickerBuilder codeBlockLanguagePickerBuilder = (
|
||||
editorState,
|
||||
supportedLanguages,
|
||||
onLanguageSelected, {
|
||||
selectedLanguage,
|
||||
onMenuClose,
|
||||
onMenuOpen,
|
||||
}) =>
|
||||
_CodeBlockLanguageSelector(
|
||||
editorState: editorState,
|
||||
language: selectedLanguage,
|
||||
supportedLanguages: supportedLanguages,
|
||||
onLanguageSelected: onLanguageSelected,
|
||||
onMenuClose: onMenuClose,
|
||||
onMenuOpen: onMenuOpen,
|
||||
);
|
||||
|
||||
class _CodeBlockLanguageSelector extends StatefulWidget {
|
||||
const _CodeBlockLanguageSelector({
|
||||
required this.editorState,
|
||||
required this.supportedLanguages,
|
||||
this.language,
|
||||
required this.onLanguageSelected,
|
||||
this.onMenuOpen,
|
||||
this.onMenuClose,
|
||||
});
|
||||
|
||||
final EditorState editorState;
|
||||
final List<String> supportedLanguages;
|
||||
final String? language;
|
||||
final void Function(String) onLanguageSelected;
|
||||
final VoidCallback? onMenuOpen;
|
||||
final VoidCallback? onMenuClose;
|
||||
|
||||
@override
|
||||
State<_CodeBlockLanguageSelector> createState() =>
|
||||
_CodeBlockLanguageSelectorState();
|
||||
}
|
||||
|
||||
class _CodeBlockLanguageSelectorState
|
||||
extends State<_CodeBlockLanguageSelector> {
|
||||
final controller = PopoverController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget child = Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 4.0),
|
||||
child: FlowyTextButton(
|
||||
widget.language?.capitalize() ??
|
||||
LocaleKeys.document_codeBlock_language_auto.tr(),
|
||||
constraints: const BoxConstraints(minWidth: 50),
|
||||
fontColor: Theme.of(context).colorScheme.onBackground,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 4),
|
||||
fillColor: Colors.transparent,
|
||||
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
onPressed: () async {
|
||||
if (PlatformExtension.isMobile) {
|
||||
final language = await context
|
||||
.push<String>(MobileCodeLanguagePickerScreen.routeName);
|
||||
if (language != null) {
|
||||
widget.onLanguageSelected(language);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (PlatformExtension.isDesktopOrWeb) {
|
||||
child = AppFlowyPopover(
|
||||
controller: controller,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
onOpen: widget.onMenuOpen,
|
||||
constraints: const BoxConstraints(maxHeight: 300, maxWidth: 200),
|
||||
onClose: widget.onMenuClose,
|
||||
popupBuilder: (_) => _LanguageSelectionPopover(
|
||||
editorState: widget.editorState,
|
||||
language: widget.language,
|
||||
supportedLanguages: widget.supportedLanguages,
|
||||
onLanguageSelected: (language) {
|
||||
widget.onLanguageSelected(language);
|
||||
controller.close();
|
||||
},
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
class _LanguageSelectionPopover extends StatefulWidget {
|
||||
const _LanguageSelectionPopover({
|
||||
required this.editorState,
|
||||
required this.language,
|
||||
required this.supportedLanguages,
|
||||
required this.onLanguageSelected,
|
||||
});
|
||||
|
||||
final EditorState editorState;
|
||||
final String? language;
|
||||
final List<String> supportedLanguages;
|
||||
final void Function(String) onLanguageSelected;
|
||||
|
||||
@override
|
||||
State<_LanguageSelectionPopover> createState() =>
|
||||
_LanguageSelectionPopoverState();
|
||||
}
|
||||
|
||||
class _LanguageSelectionPopoverState extends State<_LanguageSelectionPopover> {
|
||||
final searchController = TextEditingController();
|
||||
final focusNode = FocusNode();
|
||||
late List<String> filteredLanguages =
|
||||
widget.supportedLanguages.map((e) => e.capitalize()).toList();
|
||||
late int selectedIndex =
|
||||
widget.supportedLanguages.indexOf(widget.language ?? '');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
// This is a workaround because longer taps might break the
|
||||
// focus, this might be an issue with the Flutter framework.
|
||||
(_) => Future.delayed(
|
||||
const Duration(milliseconds: 100),
|
||||
() => focusNode.requestFocus(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
focusNode.dispose();
|
||||
searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowyTextField(
|
||||
focusNode: focusNode,
|
||||
autoFocus: false,
|
||||
controller: searchController,
|
||||
hintText: LocaleKeys.document_codeBlock_searchLanguageHint.tr(),
|
||||
onChanged: (_) => setState(() {
|
||||
filteredLanguages = widget.supportedLanguages
|
||||
.where((e) => e.contains(searchController.text.toLowerCase()))
|
||||
.map((e) => e.capitalize())
|
||||
.toList();
|
||||
selectedIndex =
|
||||
widget.supportedLanguages.indexOf(widget.language ?? '');
|
||||
}),
|
||||
),
|
||||
const VSpace(8),
|
||||
Flexible(
|
||||
child: SelectableItemListMenu(
|
||||
shrinkWrap: true,
|
||||
items: filteredLanguages,
|
||||
selectedIndex: selectedIndex,
|
||||
onSelected: (index) =>
|
||||
widget.onLanguageSelected(filteredLanguages[index]),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,373 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
final List<CharacterShortcutEvent> codeBlockCharacterEvents = [
|
||||
enterInCodeBlock,
|
||||
...ignoreKeysInCodeBlock,
|
||||
];
|
||||
|
||||
final List<CommandShortcutEvent> codeBlockCommands = [
|
||||
insertNewParagraphNextToCodeBlockCommand,
|
||||
pasteInCodeblock,
|
||||
selectAllInCodeBlockCommand,
|
||||
tabToInsertSpacesInCodeBlockCommand,
|
||||
tabToDeleteSpacesInCodeBlockCommand,
|
||||
];
|
||||
|
||||
/// press the enter key in code block to insert a new line in it.
|
||||
///
|
||||
/// - support
|
||||
/// - desktop
|
||||
/// - web
|
||||
/// - mobile
|
||||
///
|
||||
final CharacterShortcutEvent enterInCodeBlock = CharacterShortcutEvent(
|
||||
key: 'press enter in code block',
|
||||
character: '\n',
|
||||
handler: _enterInCodeBlockCommandHandler,
|
||||
);
|
||||
|
||||
/// ignore ' ', '/', '_', '*' in code block.
|
||||
///
|
||||
/// - support
|
||||
/// - desktop
|
||||
/// - web
|
||||
/// - mobile
|
||||
///
|
||||
final List<CharacterShortcutEvent> ignoreKeysInCodeBlock =
|
||||
[' ', '/', '_', '*', '~', '-']
|
||||
.map(
|
||||
(e) => CharacterShortcutEvent(
|
||||
key: 'press enter in code block',
|
||||
character: e,
|
||||
handler: (editorState) => _ignoreKeysInCodeBlockCommandHandler(
|
||||
editorState,
|
||||
e,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
/// shift + enter to insert a new node next to the code block.
|
||||
///
|
||||
/// - support
|
||||
/// - desktop
|
||||
/// - web
|
||||
///
|
||||
final CommandShortcutEvent insertNewParagraphNextToCodeBlockCommand =
|
||||
CommandShortcutEvent(
|
||||
key: 'insert a new paragraph next to the code block',
|
||||
command: 'shift+enter',
|
||||
getDescription:
|
||||
LocaleKeys.settings_shortcuts_commands_codeBlockNewParagraph.tr,
|
||||
handler: _insertNewParagraphNextToCodeBlockCommandHandler,
|
||||
);
|
||||
|
||||
/// tab to insert two spaces at the line start in code block.
|
||||
///
|
||||
/// - support
|
||||
/// - desktop
|
||||
/// - web
|
||||
final CommandShortcutEvent tabToInsertSpacesInCodeBlockCommand =
|
||||
CommandShortcutEvent(
|
||||
key: 'tab to insert two spaces at the line start in code block',
|
||||
command: 'tab',
|
||||
getDescription:
|
||||
LocaleKeys.settings_shortcuts_commands_codeBlockAddTwoSpaces.tr,
|
||||
handler: (editorState) => _indentationInCodeBlockCommandHandler(
|
||||
editorState,
|
||||
true,
|
||||
),
|
||||
);
|
||||
|
||||
/// shift+tab to delete two spaces at the line start in code block if needed.
|
||||
///
|
||||
/// - support
|
||||
/// - desktop
|
||||
/// - web
|
||||
final CommandShortcutEvent tabToDeleteSpacesInCodeBlockCommand =
|
||||
CommandShortcutEvent(
|
||||
key: 'shift + tab to delete two spaces at the line start in code block',
|
||||
command: 'shift+tab',
|
||||
getDescription:
|
||||
LocaleKeys.settings_shortcuts_commands_codeBlockDeleteTwoSpaces.tr,
|
||||
handler: (editorState) => _indentationInCodeBlockCommandHandler(
|
||||
editorState,
|
||||
false,
|
||||
),
|
||||
);
|
||||
|
||||
/// CTRL+A to select all content inside a Code Block, if cursor is inside one.
|
||||
///
|
||||
/// - support
|
||||
/// - desktop
|
||||
/// - web
|
||||
final CommandShortcutEvent selectAllInCodeBlockCommand = CommandShortcutEvent(
|
||||
key: 'ctrl + a to select all content inside a code block',
|
||||
command: 'ctrl+a',
|
||||
macOSCommand: 'meta+a',
|
||||
getDescription: LocaleKeys.settings_shortcuts_commands_codeBlockSelectAll.tr,
|
||||
handler: _selectAllInCodeBlockCommandHandler,
|
||||
);
|
||||
|
||||
/// ctrl + v to paste text in code block.
|
||||
///
|
||||
/// - support
|
||||
/// - desktop
|
||||
/// - web
|
||||
final CommandShortcutEvent pasteInCodeblock = CommandShortcutEvent(
|
||||
key: 'paste in codeblock',
|
||||
command: 'ctrl+v',
|
||||
macOSCommand: 'cmd+v',
|
||||
getDescription: LocaleKeys.settings_shortcuts_commands_codeBlockPasteText.tr,
|
||||
handler: _pasteInCodeBlock,
|
||||
);
|
||||
|
||||
CharacterShortcutEventHandler _enterInCodeBlockCommandHandler =
|
||||
(editorState) async {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null || !selection.isCollapsed) {
|
||||
return false;
|
||||
}
|
||||
final node = editorState.getNodeAtPath(selection.end.path);
|
||||
if (node == null || node.type != CodeBlockKeys.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final lines = node.delta?.toPlainText().split('\n');
|
||||
int spaces = 0;
|
||||
if (lines?.isNotEmpty == true) {
|
||||
int index = 0;
|
||||
for (final line in lines!) {
|
||||
if (index <= selection.endIndex &&
|
||||
selection.endIndex <= index + line.length) {
|
||||
final lineSpaces = line.length - line.trimLeft().length;
|
||||
spaces = lineSpaces;
|
||||
break;
|
||||
}
|
||||
index += line.length + 1;
|
||||
}
|
||||
}
|
||||
|
||||
final transaction = editorState.transaction
|
||||
..insertText(
|
||||
node,
|
||||
selection.end.offset,
|
||||
'\n${' ' * spaces}',
|
||||
);
|
||||
await editorState.apply(transaction);
|
||||
return true;
|
||||
};
|
||||
|
||||
Future<bool> _ignoreKeysInCodeBlockCommandHandler(
|
||||
EditorState editorState,
|
||||
String key,
|
||||
) async {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null || !selection.isCollapsed) {
|
||||
return false;
|
||||
}
|
||||
final node = editorState.getNodeAtPath(selection.end.path);
|
||||
if (node == null || node.type != CodeBlockKeys.type) {
|
||||
return false;
|
||||
}
|
||||
await editorState.insertTextAtCurrentSelection(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
CommandShortcutEventHandler _insertNewParagraphNextToCodeBlockCommandHandler =
|
||||
(editorState) {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null || !selection.isCollapsed) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
final node = editorState.getNodeAtPath(selection.end.path);
|
||||
final delta = node?.delta;
|
||||
if (node == null || delta == null || node.type != CodeBlockKeys.type) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
final sliced = delta.slice(selection.startIndex);
|
||||
final transaction = editorState.transaction
|
||||
..deleteText(
|
||||
// delete the text after the cursor in the code block
|
||||
node,
|
||||
selection.startIndex,
|
||||
delta.length - selection.startIndex,
|
||||
)
|
||||
..insertNode(
|
||||
// insert a new paragraph node with the sliced delta after the code block
|
||||
selection.end.path.next,
|
||||
paragraphNode(
|
||||
attributes: {
|
||||
'delta': sliced.toJson(),
|
||||
},
|
||||
),
|
||||
)
|
||||
..afterSelection = Selection.collapsed(
|
||||
Position(path: selection.end.path.next),
|
||||
);
|
||||
editorState.apply(transaction);
|
||||
return KeyEventResult.handled;
|
||||
};
|
||||
|
||||
KeyEventResult _indentationInCodeBlockCommandHandler(
|
||||
EditorState editorState,
|
||||
bool shouldIndent,
|
||||
) {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
final node = editorState.getNodeAtPath(selection.end.path);
|
||||
final delta = node?.delta;
|
||||
if (node == null || delta == null || node.type != CodeBlockKeys.type) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
const spaces = ' ';
|
||||
final lines = delta.toPlainText().split('\n');
|
||||
int index = 0;
|
||||
|
||||
// We store indexes to be indented in a list, because we should
|
||||
// indent it in a reverse order to not mess up the offsets.
|
||||
final List<int> transactions = [];
|
||||
|
||||
for (final line in lines) {
|
||||
if (!shouldIndent && line.startsWith(spaces) || shouldIndent) {
|
||||
bool shouldTransform = false;
|
||||
if (selection.isCollapsed) {
|
||||
shouldTransform = index <= selection.endIndex &&
|
||||
selection.endIndex <= index + line.length;
|
||||
} else {
|
||||
shouldTransform = index + line.length >= selection.startIndex &&
|
||||
selection.endIndex >= index;
|
||||
|
||||
if (shouldIndent && line.trim().isEmpty) {
|
||||
shouldTransform = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldTransform) {
|
||||
transactions.add(index);
|
||||
}
|
||||
}
|
||||
|
||||
index += line.length + 1;
|
||||
}
|
||||
|
||||
if (transactions.isEmpty) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final transaction = editorState.transaction;
|
||||
|
||||
for (final index in transactions.reversed) {
|
||||
if (shouldIndent) {
|
||||
transaction.insertText(node, index, spaces);
|
||||
} else {
|
||||
transaction.deleteText(node, index, spaces.length);
|
||||
}
|
||||
}
|
||||
|
||||
// In case the selection is made backwards, we store the start
|
||||
// and end here, we will adjust the order later
|
||||
final start = !selection.isBackward ? selection.end : selection.start;
|
||||
final end = !selection.isBackward ? selection.start : selection.end;
|
||||
|
||||
final endOffset = shouldIndent
|
||||
? end.offset + (spaces.length * transactions.length)
|
||||
: end.offset - (spaces.length * transactions.length);
|
||||
|
||||
final endSelection = end.copyWith(offset: endOffset);
|
||||
|
||||
final startOffset = shouldIndent
|
||||
? start.offset + spaces.length
|
||||
: start.offset - spaces.length;
|
||||
|
||||
final startSelection = selection.isCollapsed
|
||||
? endSelection
|
||||
: start.copyWith(offset: startOffset);
|
||||
|
||||
transaction.afterSelection = selection.copyWith(
|
||||
start: selection.isBackward ? startSelection : endSelection,
|
||||
end: selection.isBackward ? endSelection : startSelection,
|
||||
);
|
||||
|
||||
editorState.apply(transaction);
|
||||
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
CommandShortcutEventHandler _selectAllInCodeBlockCommandHandler =
|
||||
(editorState) {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null || !selection.isSingle) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final node = editorState.getNodeAtPath(selection.end.path);
|
||||
final delta = node?.delta;
|
||||
if (node == null || delta == null || node.type != CodeBlockKeys.type) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
editorState.service.selectionService.updateSelection(
|
||||
Selection.single(
|
||||
path: node.path,
|
||||
startOffset: 0,
|
||||
endOffset: delta.length,
|
||||
),
|
||||
);
|
||||
|
||||
return KeyEventResult.handled;
|
||||
};
|
||||
|
||||
CommandShortcutEventHandler _pasteInCodeBlock = (editorState) {
|
||||
var selection = editorState.selection;
|
||||
|
||||
if (selection == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
if (editorState.getNodesInSelection(selection).length != 1) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final node = editorState.getNodeAtPath(selection.end.path);
|
||||
if (node == null || node.type != CodeBlockKeys.type) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
// delete the selection first.
|
||||
if (!selection.isCollapsed) {
|
||||
editorState.deleteSelection(selection);
|
||||
}
|
||||
|
||||
// fetch selection again.
|
||||
selection = editorState.selection;
|
||||
if (selection == null) {
|
||||
return KeyEventResult.skipRemainingHandlers;
|
||||
}
|
||||
assert(selection.isCollapsed);
|
||||
|
||||
() async {
|
||||
final data = await AppFlowyClipboard.getData();
|
||||
final text = data.text;
|
||||
if (text != null && text.isNotEmpty) {
|
||||
final transaction = editorState.transaction
|
||||
..insertText(
|
||||
node,
|
||||
selection!.end.offset,
|
||||
text,
|
||||
);
|
||||
|
||||
await editorState.apply(transaction);
|
||||
}
|
||||
}();
|
||||
|
||||
return KeyEventResult.handled;
|
||||
};
|
@ -1,116 +0,0 @@
|
||||
import 'package:flutter/painting.dart';
|
||||
|
||||
const lightThemeInCodeblock = {
|
||||
'root': TextStyle(
|
||||
backgroundColor: Color(0xfffbf1c7),
|
||||
color: Color(0xff3c3836),
|
||||
),
|
||||
'subst': TextStyle(color: Color(0xff3c3836)),
|
||||
'deletion': TextStyle(color: Color(0xff9d0006)),
|
||||
'formula': TextStyle(color: Color(0xff9d0006)),
|
||||
'keyword': TextStyle(color: Color(0xff9d0006)),
|
||||
'link': TextStyle(color: Color(0xff9d0006)),
|
||||
'selector-tag': TextStyle(color: Color(0xff9d0006)),
|
||||
'built_in': TextStyle(color: Color(0xff076678)),
|
||||
'emphasis': TextStyle(
|
||||
color: Color(0xff076678),
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
'name': TextStyle(color: Color(0xff076678)),
|
||||
'quote': TextStyle(color: Color(0xff076678)),
|
||||
'strong': TextStyle(
|
||||
color: Color(0xff076678),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
'title': TextStyle(color: Color(0xff076678)),
|
||||
'variable': TextStyle(color: Color(0xff076678)),
|
||||
'attr': TextStyle(color: Color(0xffb57614)),
|
||||
'params': TextStyle(color: Color(0xffb57614)),
|
||||
'template-tag': TextStyle(color: Color(0xffb57614)),
|
||||
'type': TextStyle(color: Color(0xffb57614)),
|
||||
'builtin-name': TextStyle(color: Color(0xff8f3f71)),
|
||||
'doctag': TextStyle(color: Color(0xff8f3f71)),
|
||||
'literal': TextStyle(color: Color(0xff8f3f71)),
|
||||
'number': TextStyle(color: Color(0xff8f3f71)),
|
||||
'code': TextStyle(color: Color(0xffaf3a03)),
|
||||
'meta': TextStyle(color: Color(0xffaf3a03)),
|
||||
'regexp': TextStyle(color: Color(0xffaf3a03)),
|
||||
'selector-id': TextStyle(color: Color(0xffaf3a03)),
|
||||
'template-variable': TextStyle(color: Color(0xffaf3a03)),
|
||||
'addition': TextStyle(color: Color(0xff79740e)),
|
||||
'meta-string': TextStyle(color: Color(0xff79740e)),
|
||||
'section': TextStyle(
|
||||
color: Color(0xff79740e),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
'selector-attr': TextStyle(color: Color(0xff79740e)),
|
||||
'selector-class': TextStyle(color: Color(0xff79740e)),
|
||||
'string': TextStyle(color: Color(0xff79740e)),
|
||||
'symbol': TextStyle(color: Color(0xff79740e)),
|
||||
'attribute': TextStyle(color: Color(0xff427b58)),
|
||||
'bullet': TextStyle(color: Color(0xff427b58)),
|
||||
'class': TextStyle(color: Color(0xff427b58)),
|
||||
'function': TextStyle(color: Color(0xff427b58)),
|
||||
'meta-keyword': TextStyle(color: Color(0xff427b58)),
|
||||
'selector-pseudo': TextStyle(color: Color(0xff427b58)),
|
||||
'tag': TextStyle(
|
||||
color: Color(0xff427b58),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
'comment': TextStyle(
|
||||
color: Color(0xff928374),
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
'link_label': TextStyle(color: Color(0xff8f3f71)),
|
||||
};
|
||||
|
||||
const darkThemeInCodeBlock = {
|
||||
'root': TextStyle(
|
||||
backgroundColor: Color(0xff000000),
|
||||
color: Color(0xfff8f8f8),
|
||||
),
|
||||
'comment': TextStyle(
|
||||
color: Color(0xffaeaeae),
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
'quote': TextStyle(
|
||||
color: Color(0xffaeaeae),
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
'keyword': TextStyle(color: Color(0xffe28964)),
|
||||
'selector-tag': TextStyle(color: Color(0xffe28964)),
|
||||
'type': TextStyle(color: Color(0xffe28964)),
|
||||
'string': TextStyle(color: Color(0xff65b042)),
|
||||
'subst': TextStyle(color: Color(0xffdaefa3)),
|
||||
'regexp': TextStyle(color: Color(0xffe9c062)),
|
||||
'link': TextStyle(color: Color(0xffe9c062)),
|
||||
'title': TextStyle(color: Color(0xff89bdff)),
|
||||
'section': TextStyle(color: Color(0xff89bdff)),
|
||||
'tag': TextStyle(color: Color(0xff89bdff)),
|
||||
'name': TextStyle(color: Color(0xff89bdff)),
|
||||
'symbol': TextStyle(color: Color(0xff3387cc)),
|
||||
'bullet': TextStyle(color: Color(0xff3387cc)),
|
||||
'number': TextStyle(color: Color(0xff3387cc)),
|
||||
'params': TextStyle(color: Color(0xff3e87e3)),
|
||||
'variable': TextStyle(color: Color(0xff3e87e3)),
|
||||
'template-variable': TextStyle(color: Color(0xff3e87e3)),
|
||||
'attribute': TextStyle(color: Color(0xffcda869)),
|
||||
'meta': TextStyle(color: Color(0xff8996a8)),
|
||||
'formula': TextStyle(
|
||||
backgroundColor: Color(0xff0e2231),
|
||||
color: Color(0xfff8f8f8),
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
'addition': TextStyle(
|
||||
backgroundColor: Color(0xff253b22),
|
||||
color: Color(0xfff8f8f8),
|
||||
),
|
||||
'deletion': TextStyle(
|
||||
backgroundColor: Color(0xff420e09),
|
||||
color: Color(0xfff8f8f8),
|
||||
),
|
||||
'selector-class': TextStyle(color: Color(0xff9b703f)),
|
||||
'selector-id': TextStyle(color: Color(0xff8b98ab)),
|
||||
'emphasis': TextStyle(fontStyle: FontStyle.italic),
|
||||
'strong': TextStyle(fontWeight: FontWeight.bold),
|
||||
};
|
@ -1,10 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/app_bar.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class MobileCodeLanguagePickerScreen extends StatelessWidget {
|
||||
@ -21,7 +22,7 @@ class MobileCodeLanguagePickerScreen extends StatelessWidget {
|
||||
body: SafeArea(
|
||||
child: ListView.separated(
|
||||
itemBuilder: (context, index) {
|
||||
final language = codeBlockSupportedLanguages[index];
|
||||
final language = defaultCodeBlockSupportedLanguages[index];
|
||||
return SizedBox(
|
||||
height: 48,
|
||||
child: FlowyTextButton(
|
||||
@ -35,7 +36,7 @@ class MobileCodeLanguagePickerScreen extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => const Divider(),
|
||||
itemCount: codeBlockSupportedLanguages.length,
|
||||
itemCount: defaultCodeBlockSupportedLanguages.length,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
class EditorMigration {
|
||||
|
@ -1,10 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_blocks_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// convert the current block to other block types
|
||||
// only show in single selection and text type
|
||||
|
@ -1,10 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_blocks_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// convert the current block to other block types
|
||||
// only show in single selection and text type
|
||||
|
@ -1,5 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/type_option_menu_item.dart';
|
||||
@ -11,8 +13,8 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_too
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/startup/tasks/app_widget.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
final addBlockToolbarItem = AppFlowyMobileToolbarItem(
|
||||
|
@ -3,8 +3,7 @@ export 'actions/option_action.dart';
|
||||
export 'align_toolbar_item/align_toolbar_item.dart';
|
||||
export 'base/toolbar_extension.dart';
|
||||
export 'callout/callout_block_component.dart';
|
||||
export 'code_block/code_block_component.dart';
|
||||
export 'code_block/code_block_shortcut_event.dart';
|
||||
export 'code_block/code_block_language_selector.dart';
|
||||
export 'context_menu/custom_context_menu.dart';
|
||||
export 'copy_and_paste/custom_copy_command.dart';
|
||||
export 'copy_and_paste/custom_cut_command.dart';
|
||||
|
@ -1,6 +1,5 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_block_shortcut_event.dart';
|
||||
import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@ -122,5 +121,5 @@ class ShortcutsCubit extends Cubit<ShortcutsState> {
|
||||
}
|
||||
|
||||
extension on CommandShortcutEvent {
|
||||
bool get isCodeBlockCommand => codeBlockCommands.contains(this);
|
||||
bool get isCodeBlockCommand => localizedCodeBlockCommands.contains(this);
|
||||
}
|
||||
|
@ -61,11 +61,10 @@ packages:
|
||||
appflowy_editor_plugins:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "8f238f2"
|
||||
resolved-ref: "8f238f214de72e629fe2d90317518c5a0510cdc5"
|
||||
url: "https://github.com/LucasXu0/appflowy_editor_plugins"
|
||||
source: git
|
||||
name: appflowy_editor_plugins
|
||||
sha256: adfafde707a65ec9674bc3edaa8b8d865e065a08a4f653176d5db0c950521cbb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.1"
|
||||
appflowy_popover:
|
||||
dependency: "direct main"
|
||||
@ -173,10 +172,10 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21"
|
||||
sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.8"
|
||||
version: "2.4.9"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1965,10 +1964,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c
|
||||
sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.4"
|
||||
version: "6.2.6"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -46,11 +46,8 @@ dependencies:
|
||||
ref: 15a3a50
|
||||
appflowy_result:
|
||||
path: packages/appflowy_result
|
||||
appflowy_editor_plugins: ^0.0.1
|
||||
appflowy_editor:
|
||||
appflowy_editor_plugins:
|
||||
git:
|
||||
url: https://github.com/LucasXu0/appflowy_editor_plugins
|
||||
ref: "8f238f2"
|
||||
|
||||
appflowy_popover:
|
||||
path: packages/appflowy_popover
|
||||
@ -142,10 +139,10 @@ dev_dependencies:
|
||||
sdk: flutter
|
||||
integration_test:
|
||||
sdk: flutter
|
||||
build_runner: ^2.4.4
|
||||
build_runner: ^2.4.9
|
||||
freezed: ^2.4.7
|
||||
bloc_test: ^9.1.2
|
||||
json_serializable: ^6.7.0
|
||||
json_serializable: ^6.7.1
|
||||
envied_generator: ^0.5.2
|
||||
|
||||
plugin_platform_interface: any
|
||||
|
Loading…
Reference in New Issue
Block a user