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 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
@ -23,11 +24,7 @@ void main() {
|
|||||||
// mock the clipboard
|
// mock the clipboard
|
||||||
const lines = 3;
|
const lines = 3;
|
||||||
final text = List.generate(lines, (index) => 'line $index').join('\n');
|
final text = List.generate(lines, (index) => 'line $index').join('\n');
|
||||||
AppFlowyClipboard.mockSetData(
|
AppFlowyClipboard.mockSetData(AppFlowyClipboardData(text: text));
|
||||||
AppFlowyClipboardData(
|
|
||||||
text: text,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await insertCodeBlockInDocument(tester);
|
await insertCodeBlockInDocument(tester);
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
|
|||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.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/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/image/custom_image_block_component.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||||
@ -148,11 +149,18 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
|
|||||||
configuration: configuration,
|
configuration: configuration,
|
||||||
),
|
),
|
||||||
CodeBlockKeys.type: CodeBlockComponentBuilder(
|
CodeBlockKeys.type: CodeBlockComponentBuilder(
|
||||||
|
editorState: editorState,
|
||||||
configuration: configuration.copyWith(
|
configuration: configuration.copyWith(
|
||||||
textStyle: (_) => styleCustomizer.codeBlockStyleBuilder(),
|
textStyle: (_) => styleCustomizer.codeBlockStyleBuilder(),
|
||||||
placeholderTextStyle: (_) => 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),
|
padding: const EdgeInsets.only(left: 20, right: 30, bottom: 34),
|
||||||
|
languagePickerBuilder: codeBlockLanguagePickerBuilder,
|
||||||
|
copyButtonBuilder: codeBlockCopyBuilder,
|
||||||
),
|
),
|
||||||
AutoCompletionBlockKeys.type: AutoCompletionBlockComponentBuilder(),
|
AutoCompletionBlockKeys.type: AutoCompletionBlockComponentBuilder(),
|
||||||
SmartEditBlockKeys.type: SmartEditBlockComponentBuilder(),
|
SmartEditBlockKeys.type: SmartEditBlockComponentBuilder(),
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.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/application/document_bloc.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_configuration.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';
|
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/application/view_info/view_info_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
|
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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 = [
|
final List<CommandShortcutEvent> commandShortcutEvents = [
|
||||||
toggleToggleListCommand,
|
toggleToggleListCommand,
|
||||||
...codeBlockCommands,
|
...localizedCodeBlockCommands,
|
||||||
customCopyCommand,
|
customCopyCommand,
|
||||||
customPasteCommand,
|
customPasteCommand,
|
||||||
customCutCommand,
|
customCutCommand,
|
||||||
@ -90,7 +111,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
|||||||
|
|
||||||
late final List<CommandShortcutEvent> commandShortcutEvents = [
|
late final List<CommandShortcutEvent> commandShortcutEvents = [
|
||||||
toggleToggleListCommand,
|
toggleToggleListCommand,
|
||||||
...codeBlockCommands,
|
...localizedCodeBlockCommands,
|
||||||
customCopyCommand,
|
customCopyCommand,
|
||||||
customPasteCommand,
|
customPasteCommand,
|
||||||
customCutCommand,
|
customCutCommand,
|
||||||
@ -264,7 +285,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
|||||||
final isRTL =
|
final isRTL =
|
||||||
context.read<AppearanceSettingsCubit>().state.layoutDirection ==
|
context.read<AppearanceSettingsCubit>().state.layoutDirection ==
|
||||||
LayoutDirection.rtlLayout;
|
LayoutDirection.rtlLayout;
|
||||||
final textDirection = isRTL ? TextDirection.rtl : TextDirection.ltr;
|
final textDirection = isRTL ? ui.TextDirection.rtl : ui.TextDirection.ltr;
|
||||||
|
|
||||||
_setRTLToolbarItems(
|
_setRTLToolbarItems(
|
||||||
context.read<AppearanceSettingsCubit>().state.enableRtlToolbarItems,
|
context.read<AppearanceSettingsCubit>().state.enableRtlToolbarItems,
|
||||||
@ -371,7 +392,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
|||||||
calloutItem,
|
calloutItem,
|
||||||
outlineItem,
|
outlineItem,
|
||||||
mathEquationItem,
|
mathEquationItem,
|
||||||
codeBlockItem,
|
codeBlockItem(LocaleKeys.document_selectionMenu_codeBlock.tr()),
|
||||||
toggleListBlockItem,
|
toggleListBlockItem,
|
||||||
emojiMenuItem,
|
emojiMenuItem,
|
||||||
autoGeneratorMenuItem,
|
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/appflowy_editor.dart';
|
||||||
|
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||||
|
|
||||||
const _greater = '>';
|
const _greater = '>';
|
||||||
const _equals = '=';
|
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/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/mobile/presentation/base/app_bar.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/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:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
class MobileCodeLanguagePickerScreen extends StatelessWidget {
|
class MobileCodeLanguagePickerScreen extends StatelessWidget {
|
||||||
@ -21,7 +22,7 @@ class MobileCodeLanguagePickerScreen extends StatelessWidget {
|
|||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final language = codeBlockSupportedLanguages[index];
|
final language = defaultCodeBlockSupportedLanguages[index];
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 48,
|
height: 48,
|
||||||
child: FlowyTextButton(
|
child: FlowyTextButton(
|
||||||
@ -35,7 +36,7 @@ class MobileCodeLanguagePickerScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
separatorBuilder: (_, __) => const Divider(),
|
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/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
class EditorMigration {
|
class EditorMigration {
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_blocks_menu.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/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.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:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
// convert the current block to other block types
|
// convert the current block to other block types
|
||||||
// only show in single selection and text type
|
// 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/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.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/mobile_toolbar_item/mobile_blocks_menu.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.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:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
// convert the current block to other block types
|
// convert the current block to other block types
|
||||||
// only show in single selection and text type
|
// only show in single selection and text type
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/mobile/presentation/base/type_option_menu_item.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/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||||
import 'package:appflowy/startup/tasks/app_widget.dart';
|
import 'package:appflowy/startup/tasks/app_widget.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.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:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
final addBlockToolbarItem = AppFlowyMobileToolbarItem(
|
final addBlockToolbarItem = AppFlowyMobileToolbarItem(
|
||||||
|
@ -3,8 +3,7 @@ export 'actions/option_action.dart';
|
|||||||
export 'align_toolbar_item/align_toolbar_item.dart';
|
export 'align_toolbar_item/align_toolbar_item.dart';
|
||||||
export 'base/toolbar_extension.dart';
|
export 'base/toolbar_extension.dart';
|
||||||
export 'callout/callout_block_component.dart';
|
export 'callout/callout_block_component.dart';
|
||||||
export 'code_block/code_block_component.dart';
|
export 'code_block/code_block_language_selector.dart';
|
||||||
export 'code_block/code_block_shortcut_event.dart';
|
|
||||||
export 'context_menu/custom_context_menu.dart';
|
export 'context_menu/custom_context_menu.dart';
|
||||||
export 'copy_and_paste/custom_copy_command.dart';
|
export 'copy_and_paste/custom_copy_command.dart';
|
||||||
export 'copy_and_paste/custom_cut_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/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_page.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/workspace/application/settings/shortcuts/settings_shortcuts_service.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
@ -122,5 +121,5 @@ class ShortcutsCubit extends Cubit<ShortcutsState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension on CommandShortcutEvent {
|
extension on CommandShortcutEvent {
|
||||||
bool get isCodeBlockCommand => codeBlockCommands.contains(this);
|
bool get isCodeBlockCommand => localizedCodeBlockCommands.contains(this);
|
||||||
}
|
}
|
||||||
|
@ -61,11 +61,10 @@ packages:
|
|||||||
appflowy_editor_plugins:
|
appflowy_editor_plugins:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
name: appflowy_editor_plugins
|
||||||
ref: "8f238f2"
|
sha256: adfafde707a65ec9674bc3edaa8b8d865e065a08a4f653176d5db0c950521cbb
|
||||||
resolved-ref: "8f238f214de72e629fe2d90317518c5a0510cdc5"
|
url: "https://pub.dev"
|
||||||
url: "https://github.com/LucasXu0/appflowy_editor_plugins"
|
source: hosted
|
||||||
source: git
|
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
appflowy_popover:
|
appflowy_popover:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
@ -173,10 +172,10 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: build_runner
|
name: build_runner
|
||||||
sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21"
|
sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.8"
|
version: "2.4.9"
|
||||||
build_runner_core:
|
build_runner_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1965,10 +1964,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: url_launcher
|
name: url_launcher
|
||||||
sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c
|
sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.2.4"
|
version: "6.2.6"
|
||||||
url_launcher_android:
|
url_launcher_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -46,11 +46,8 @@ dependencies:
|
|||||||
ref: 15a3a50
|
ref: 15a3a50
|
||||||
appflowy_result:
|
appflowy_result:
|
||||||
path: packages/appflowy_result
|
path: packages/appflowy_result
|
||||||
|
appflowy_editor_plugins: ^0.0.1
|
||||||
appflowy_editor:
|
appflowy_editor:
|
||||||
appflowy_editor_plugins:
|
|
||||||
git:
|
|
||||||
url: https://github.com/LucasXu0/appflowy_editor_plugins
|
|
||||||
ref: "8f238f2"
|
|
||||||
|
|
||||||
appflowy_popover:
|
appflowy_popover:
|
||||||
path: packages/appflowy_popover
|
path: packages/appflowy_popover
|
||||||
@ -142,10 +139,10 @@ dev_dependencies:
|
|||||||
sdk: flutter
|
sdk: flutter
|
||||||
integration_test:
|
integration_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
build_runner: ^2.4.4
|
build_runner: ^2.4.9
|
||||||
freezed: ^2.4.7
|
freezed: ^2.4.7
|
||||||
bloc_test: ^9.1.2
|
bloc_test: ^9.1.2
|
||||||
json_serializable: ^6.7.0
|
json_serializable: ^6.7.1
|
||||||
envied_generator: ^0.5.2
|
envied_generator: ^0.5.2
|
||||||
|
|
||||||
plugin_platform_interface: any
|
plugin_platform_interface: any
|
||||||
|
Loading…
Reference in New Issue
Block a user