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:
Mathias Mogensen 2024-04-17 10:52:58 +02:00 committed by GitHub
parent 49e9b8c358
commit 0a5f3e8fa1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 311 additions and 1226 deletions

View File

@ -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);

View File

@ -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(),

View File

@ -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,

View File

@ -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 = '=';

View File

@ -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]),
),
),
],
);
}
}

View File

@ -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,
),
),
),
);
}
}

View File

@ -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]),
),
),
],
);
}
}

View File

@ -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;
};

View File

@ -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),
};

View File

@ -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,
),
),
);

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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';

View File

@ -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);
}

View File

@ -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:

View File

@ -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