mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support inserting local image (#2913)
This commit is contained in:
parent
c870dbeac4
commit
11d05b303d
@ -452,6 +452,12 @@
|
|||||||
"center": "Center",
|
"center": "Center",
|
||||||
"right": "Right",
|
"right": "Right",
|
||||||
"defaultColor": "Default"
|
"defaultColor": "Default"
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"copiedToPasteBoard": "The image link has been copied to the clipboard"
|
||||||
|
},
|
||||||
|
"outline": {
|
||||||
|
"addHeadingToCreateOutline": "Add headings to create a table of contents."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -118,7 +118,7 @@ const _sample = r'''
|
|||||||
---
|
---
|
||||||
[] Highlight any text, and use the editing menu to _style_ **your** writing `however` you ~~like.~~
|
[] Highlight any text, and use the editing menu to _style_ **your** writing `however` you ~~like.~~
|
||||||
|
|
||||||
[] Type / followed by /bullet or /num to create a list.
|
[] Type followed by bullet or num to create a list.
|
||||||
|
|
||||||
[x] Click `+ New Page` button at the bottom of your sidebar to add a new page.
|
[x] Click `+ New Page` button at the bottom of your sidebar to add a new page.
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import 'package:appflowy/plugins/document/application/doc_service.dart';
|
|||||||
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart'
|
import 'package:appflowy_editor/appflowy_editor.dart'
|
||||||
show EditorState, LogLevel, TransactionTime;
|
show EditorState, LogLevel, TransactionTime, Selection, paragraphNode;
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
@ -155,6 +155,9 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await _transactionAdapter.apply(event.$2, editorState);
|
await _transactionAdapter.apply(event.$2, editorState);
|
||||||
|
|
||||||
|
// check if the document is empty.
|
||||||
|
applyRules();
|
||||||
});
|
});
|
||||||
|
|
||||||
// output the log from the editor when debug mode
|
// output the log from the editor when debug mode
|
||||||
@ -166,6 +169,39 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> applyRules() async {
|
||||||
|
ensureAtLeastOneParagraphExists();
|
||||||
|
ensureLastNodeIsEditable();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> ensureLastNodeIsEditable() async {
|
||||||
|
final editorState = this.editorState;
|
||||||
|
if (editorState == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final document = editorState.document;
|
||||||
|
final lastNode = document.root.children.lastOrNull;
|
||||||
|
if (lastNode == null || lastNode.delta == null) {
|
||||||
|
final transaction = editorState.transaction;
|
||||||
|
transaction.insertNode([document.root.children.length], paragraphNode());
|
||||||
|
await editorState.apply(transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> ensureAtLeastOneParagraphExists() async {
|
||||||
|
final editorState = this.editorState;
|
||||||
|
if (editorState == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final document = editorState.document;
|
||||||
|
if (document.root.children.isEmpty) {
|
||||||
|
final transaction = editorState.transaction;
|
||||||
|
transaction.insertNode([0], paragraphNode());
|
||||||
|
transaction.afterSelection = Selection.collapse([0], 0);
|
||||||
|
await editorState.apply(transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
|
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option_action.dart';
|
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_list.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';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/inline_page/inline_page_reference.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/inline_page/inline_page_reference.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:collection/collection.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:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
@ -55,20 +54,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
|||||||
highlightColorItem,
|
highlightColorItem,
|
||||||
];
|
];
|
||||||
|
|
||||||
late final slashMenuItems = [
|
late final List<SelectionMenuItem> slashMenuItems;
|
||||||
inlineGridMenuItem(documentBloc),
|
|
||||||
referencedGridMenuItem,
|
|
||||||
inlineBoardMenuItem(documentBloc),
|
|
||||||
referencedBoardMenuItem,
|
|
||||||
inlineCalendarMenuItem(documentBloc),
|
|
||||||
referencedCalendarMenuItem,
|
|
||||||
calloutItem,
|
|
||||||
mathEquationItem,
|
|
||||||
codeBlockItem,
|
|
||||||
emojiMenuItem,
|
|
||||||
autoGeneratorMenuItem,
|
|
||||||
outlineItem,
|
|
||||||
];
|
|
||||||
|
|
||||||
late final Map<String, BlockComponentBuilder> blockComponentBuilders =
|
late final Map<String, BlockComponentBuilder> blockComponentBuilders =
|
||||||
_customAppFlowyBlockComponentBuilders();
|
_customAppFlowyBlockComponentBuilders();
|
||||||
@ -119,6 +105,9 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
slashMenuItems = _customSlashMenuItems();
|
||||||
|
|
||||||
effectiveScrollController = widget.scrollController ?? ScrollController();
|
effectiveScrollController = widget.scrollController ?? ScrollController();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,6 +208,15 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
|||||||
),
|
),
|
||||||
ImageBlockKeys.type: ImageBlockComponentBuilder(
|
ImageBlockKeys.type: ImageBlockComponentBuilder(
|
||||||
configuration: configuration,
|
configuration: configuration,
|
||||||
|
showMenu: true,
|
||||||
|
menuBuilder: (node, state) => Positioned(
|
||||||
|
top: 0,
|
||||||
|
right: 10,
|
||||||
|
child: ImageMenu(
|
||||||
|
node: node,
|
||||||
|
state: state,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
DatabaseBlockKeys.gridType: DatabaseViewBlockComponentBuilder(
|
DatabaseBlockKeys.gridType: DatabaseViewBlockComponentBuilder(
|
||||||
configuration: configuration,
|
configuration: configuration,
|
||||||
@ -254,8 +252,15 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
|||||||
),
|
),
|
||||||
AutoCompletionBlockKeys.type: AutoCompletionBlockComponentBuilder(),
|
AutoCompletionBlockKeys.type: AutoCompletionBlockComponentBuilder(),
|
||||||
SmartEditBlockKeys.type: SmartEditBlockComponentBuilder(),
|
SmartEditBlockKeys.type: SmartEditBlockComponentBuilder(),
|
||||||
ToggleListBlockKeys.type: ToggleListBlockComponentBuilder(),
|
ToggleListBlockKeys.type: ToggleListBlockComponentBuilder(
|
||||||
OutlineBlockKeys.type: OutlineBlockComponentBuilder(),
|
configuration: configuration,
|
||||||
|
),
|
||||||
|
OutlineBlockKeys.type: OutlineBlockComponentBuilder(
|
||||||
|
configuration: configuration.copyWith(
|
||||||
|
placeholderTextStyle: (_) =>
|
||||||
|
styleCustomizer.outlineBlockPlaceholderStyleBuilder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
final builders = {
|
final builders = {
|
||||||
@ -325,6 +330,34 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
|||||||
return builders;
|
return builders;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<SelectionMenuItem> _customSlashMenuItems() {
|
||||||
|
final items = [...standardSelectionMenuItems];
|
||||||
|
final imageItem = items.firstWhereOrNull(
|
||||||
|
(element) => element.name == AppFlowyEditorLocalizations.current.image,
|
||||||
|
);
|
||||||
|
if (imageItem != null) {
|
||||||
|
final imageItemIndex = items.indexOf(imageItem);
|
||||||
|
if (imageItemIndex != -1) {
|
||||||
|
items[imageItemIndex] = customImageMenuItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
...items,
|
||||||
|
inlineGridMenuItem(documentBloc),
|
||||||
|
referencedGridMenuItem,
|
||||||
|
inlineBoardMenuItem(documentBloc),
|
||||||
|
referencedBoardMenuItem,
|
||||||
|
inlineCalendarMenuItem(documentBloc),
|
||||||
|
referencedCalendarMenuItem,
|
||||||
|
calloutItem,
|
||||||
|
outlineItem,
|
||||||
|
mathEquationItem,
|
||||||
|
codeBlockItem,
|
||||||
|
emojiMenuItem,
|
||||||
|
autoGeneratorMenuItem,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
(bool, Selection?) _computeAutoFocusParameters() {
|
(bool, Selection?) _computeAutoFocusParameters() {
|
||||||
if (widget.editorState.document.isEmpty) {
|
if (widget.editorState.document.isEmpty) {
|
||||||
return (true, Selection.collapse([0], 0));
|
return (true, Selection.collapse([0], 0));
|
||||||
|
@ -0,0 +1,290 @@
|
|||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg;
|
||||||
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra/image.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class ImageMenu extends StatefulWidget {
|
||||||
|
const ImageMenu({
|
||||||
|
super.key,
|
||||||
|
required this.node,
|
||||||
|
required this.state,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Node node;
|
||||||
|
final ImageBlockComponentWidgetState state;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ImageMenu> createState() => _ImageMenuState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ImageMenuState extends State<ImageMenu> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return Container(
|
||||||
|
height: 32,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.cardColor,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
blurRadius: 5,
|
||||||
|
spreadRadius: 1,
|
||||||
|
color: Colors.black.withOpacity(0.1),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
borderRadius: BorderRadius.circular(4.0),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const HSpace(4),
|
||||||
|
_ImageCopyLinkButton(
|
||||||
|
onTap: copyImageLink,
|
||||||
|
),
|
||||||
|
const HSpace(4),
|
||||||
|
_ImageAlignButton(
|
||||||
|
node: widget.node,
|
||||||
|
state: widget.state,
|
||||||
|
),
|
||||||
|
const _Divider(),
|
||||||
|
_ImageDeleteButton(
|
||||||
|
onTap: () => deleteImage(),
|
||||||
|
),
|
||||||
|
const HSpace(4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void copyImageLink() {
|
||||||
|
final url = widget.node.attributes[ImageBlockKeys.url];
|
||||||
|
if (url != null) {
|
||||||
|
Clipboard.setData(ClipboardData(text: url));
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: FlowyText(
|
||||||
|
LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteImage() async {
|
||||||
|
final node = widget.node;
|
||||||
|
final editorState = context.read<EditorState>();
|
||||||
|
final transaction = editorState.transaction;
|
||||||
|
transaction.deleteNode(node);
|
||||||
|
transaction.afterSelection = null;
|
||||||
|
await editorState.apply(transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ImageCopyLinkButton extends StatelessWidget {
|
||||||
|
const _ImageCopyLinkButton({
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: const FlowySvg(
|
||||||
|
name: 'editor/copy',
|
||||||
|
size: Size.square(16),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ImageAlignButton extends StatefulWidget {
|
||||||
|
const _ImageAlignButton({
|
||||||
|
required this.node,
|
||||||
|
required this.state,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Node node;
|
||||||
|
final ImageBlockComponentWidgetState state;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ImageAlignButton> createState() => _ImageAlignButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
const interceptorKey = 'image-align';
|
||||||
|
|
||||||
|
class _ImageAlignButtonState extends State<_ImageAlignButton> {
|
||||||
|
final gestureInterceptor = SelectionGestureInterceptor(
|
||||||
|
key: interceptorKey,
|
||||||
|
canTap: (details) => false,
|
||||||
|
);
|
||||||
|
|
||||||
|
String get align => widget.node.attributes['align'] ?? 'center';
|
||||||
|
final popoverController = PopoverController();
|
||||||
|
late final EditorState editorState;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
editorState = context.read<EditorState>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
allowMenuClose();
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return IgnoreParentGestureWidget(
|
||||||
|
child: AppFlowyPopover(
|
||||||
|
onClose: allowMenuClose,
|
||||||
|
controller: popoverController,
|
||||||
|
windowPadding: const EdgeInsets.all(0),
|
||||||
|
margin: const EdgeInsets.all(0),
|
||||||
|
direction: PopoverDirection.bottomWithCenterAligned,
|
||||||
|
offset: const Offset(0, 10),
|
||||||
|
child: buildAlignIcon(),
|
||||||
|
popupBuilder: (_) {
|
||||||
|
preventMenuClose();
|
||||||
|
return _AlignButtons(
|
||||||
|
onAlignChanged: onAlignChanged,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onAlignChanged(String align) {
|
||||||
|
popoverController.close();
|
||||||
|
|
||||||
|
final transaction = editorState.transaction;
|
||||||
|
transaction.updateNode(widget.node, {
|
||||||
|
ImageBlockKeys.align: align,
|
||||||
|
});
|
||||||
|
editorState.apply(transaction);
|
||||||
|
|
||||||
|
allowMenuClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void preventMenuClose() {
|
||||||
|
widget.state.alwaysShowMenu = true;
|
||||||
|
editorState.service.selectionService.registerGestureInterceptor(
|
||||||
|
gestureInterceptor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void allowMenuClose() {
|
||||||
|
widget.state.alwaysShowMenu = false;
|
||||||
|
editorState.service.selectionService.unregisterGestureInterceptor(
|
||||||
|
interceptorKey,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildAlignIcon() {
|
||||||
|
return FlowySvg(
|
||||||
|
name: 'editor/align/$align',
|
||||||
|
size: const Size.square(16),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AlignButtons extends StatelessWidget {
|
||||||
|
const _AlignButtons({
|
||||||
|
required this.onAlignChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Function(String align) onAlignChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 32,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const HSpace(4),
|
||||||
|
_AlignButton(
|
||||||
|
align: 'left',
|
||||||
|
onTap: () => onAlignChanged('left'),
|
||||||
|
),
|
||||||
|
const _Divider(),
|
||||||
|
_AlignButton(
|
||||||
|
align: 'left',
|
||||||
|
onTap: () => onAlignChanged('center'),
|
||||||
|
),
|
||||||
|
const _Divider(),
|
||||||
|
_AlignButton(
|
||||||
|
align: 'left',
|
||||||
|
onTap: () => onAlignChanged('right'),
|
||||||
|
),
|
||||||
|
const HSpace(4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AlignButton extends StatelessWidget {
|
||||||
|
const _AlignButton({
|
||||||
|
required this.align,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String align;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: FlowySvg(
|
||||||
|
name: 'editor/align/$align',
|
||||||
|
size: const Size.square(16),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ImageDeleteButton extends StatelessWidget {
|
||||||
|
const _ImageDeleteButton({
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: const FlowySvg(
|
||||||
|
name: 'editor/delete',
|
||||||
|
size: Size.square(16),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Divider extends StatelessWidget {
|
||||||
|
const _Divider();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Container(
|
||||||
|
width: 1,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:appflowy/startup/startup.dart';
|
||||||
|
import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart';
|
||||||
|
import 'package:appflowy_backend/log.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||||
|
import 'package:flowy_infra/uuid.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
final customImageMenuItem = SelectionMenuItem(
|
||||||
|
name: AppFlowyEditorLocalizations.current.image,
|
||||||
|
icon: (editorState, isSelected, style) => SelectionMenuIconWidget(
|
||||||
|
name: 'image',
|
||||||
|
isSelected: isSelected,
|
||||||
|
style: style,
|
||||||
|
),
|
||||||
|
keywords: ['image', 'picture', 'img', 'photo'],
|
||||||
|
handler: (editorState, menuService, context) {
|
||||||
|
final container = Overlay.of(context);
|
||||||
|
showImageMenu(
|
||||||
|
container,
|
||||||
|
editorState,
|
||||||
|
menuService,
|
||||||
|
onInsertImage: (url) async {
|
||||||
|
// if the url is http, we can insert it directly
|
||||||
|
// otherwise, if it's a file url, we need to copy the file to the app's document directory
|
||||||
|
|
||||||
|
final regex = RegExp('^(http|https)://');
|
||||||
|
if (regex.hasMatch(url)) {
|
||||||
|
await editorState.insertImageNode(url);
|
||||||
|
} else {
|
||||||
|
final path = await getIt<ApplicationDataStorage>().getPath();
|
||||||
|
final imagePath = p.join(
|
||||||
|
path,
|
||||||
|
'images',
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
// create the directory if not exists
|
||||||
|
final directory = Directory(imagePath);
|
||||||
|
if (!directory.existsSync()) {
|
||||||
|
await directory.create(recursive: true);
|
||||||
|
}
|
||||||
|
final copyToPath = p.join(
|
||||||
|
imagePath,
|
||||||
|
'${uuid()}${p.extension(url)}',
|
||||||
|
);
|
||||||
|
await File(url).copy(
|
||||||
|
copyToPath,
|
||||||
|
);
|
||||||
|
await editorState.insertImageNode(copyToPath);
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('cannot copy image file', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
@ -120,6 +120,15 @@ class _OutlineBlockWidgetState extends State<OutlineBlockWidget>
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
if (children.isEmpty) {
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
LocaleKeys.document_plugins_outline_addHeadingToCreateOutline.tr(),
|
||||||
|
style: configuration.placeholderTextStyle(node),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
|
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
|
||||||
@ -184,7 +193,7 @@ class OutlineItemWidget extends StatelessWidget {
|
|||||||
|
|
||||||
extension on Node {
|
extension on Node {
|
||||||
double get leftIndent {
|
double get leftIndent {
|
||||||
assert(type != HeadingBlockKeys.type);
|
assert(type == HeadingBlockKeys.type);
|
||||||
if (type != HeadingBlockKeys.type) {
|
if (type != HeadingBlockKeys.type) {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
@ -16,3 +16,7 @@ export 'openai/widgets/smart_edit_toolbar_item.dart';
|
|||||||
export 'toggle/toggle_block_component.dart';
|
export 'toggle/toggle_block_component.dart';
|
||||||
export 'toggle/toggle_block_shortcut_event.dart';
|
export 'toggle/toggle_block_shortcut_event.dart';
|
||||||
export 'outline/outline_block_component.dart';
|
export 'outline/outline_block_component.dart';
|
||||||
|
export 'image/image_menu.dart';
|
||||||
|
export 'image/image_selection_menu.dart';
|
||||||
|
export 'actions/option_action.dart';
|
||||||
|
export 'actions/block_action_list.dart';
|
||||||
|
@ -127,6 +127,17 @@ class EditorStyleCustomizer {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TextStyle outlineBlockPlaceholderStyleBuilder() {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
|
||||||
|
return TextStyle(
|
||||||
|
fontFamily: 'poppins',
|
||||||
|
fontSize: fontSize,
|
||||||
|
height: 1.5,
|
||||||
|
color: theme.colorScheme.onBackground.withOpacity(0.6),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
SelectionMenuStyle selectionMenuStyleBuilder() {
|
SelectionMenuStyle selectionMenuStyleBuilder() {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
return SelectionMenuStyle(
|
return SelectionMenuStyle(
|
||||||
|
@ -25,6 +25,11 @@ class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
|
|||||||
void fetch() async {
|
void fetch() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final fontSize = prefs.getDouble(_kDocumentAppearanceFontSize) ?? 16.0;
|
final fontSize = prefs.getDouble(_kDocumentAppearanceFontSize) ?? 16.0;
|
||||||
|
|
||||||
|
if (isClosed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
@ -35,6 +40,11 @@ class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
|
|||||||
void syncFontSize(double fontSize) async {
|
void syncFontSize(double fontSize) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
prefs.setDouble(_kDocumentAppearanceFontSize, fontSize);
|
prefs.setDouble(_kDocumentAppearanceFontSize, fontSize);
|
||||||
|
|
||||||
|
if (isClosed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
|
@ -53,9 +53,9 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "250b1a5"
|
ref: "572a174"
|
||||||
resolved-ref: "250b1a59856b337fc2d4b26a1dabdec265e80acf"
|
resolved-ref: "572a174892267e2f78f9c3d7f1fe4ca71c9be0db"
|
||||||
url: "https://github.com/AppFlowy-IO/appflowy-editor"
|
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
||||||
source: git
|
source: git
|
||||||
version: "1.0.4"
|
version: "1.0.4"
|
||||||
appflowy_popover:
|
appflowy_popover:
|
||||||
|
@ -45,9 +45,8 @@ dependencies:
|
|||||||
# appflowy_editor: ^1.0.4
|
# appflowy_editor: ^1.0.4
|
||||||
appflowy_editor:
|
appflowy_editor:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/AppFlowy-IO/appflowy-editor
|
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||||
ref: 250b1a5
|
ref: 572a174
|
||||||
|
|
||||||
appflowy_popover:
|
appflowy_popover:
|
||||||
path: packages/appflowy_popover
|
path: packages/appflowy_popover
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user