feat: image block revamp (#3615)

This commit is contained in:
Lucas.Xu 2023-10-05 10:40:41 +08:00 committed by GitHub
parent 40dcd13394
commit 36f47f3636
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 794 additions and 63 deletions

View File

@ -1,4 +1,5 @@
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/plugins/inline_actions/handlers/date_reference.dart';
@ -267,10 +268,11 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
),
textStyleBuilder: (level) => styleCustomizer.headingStyleBuilder(level),
),
ImageBlockKeys.type: ImageBlockComponentBuilder(
ImageBlockKeys.type: CustomImageBlockComponentBuilder(
configuration: configuration,
showMenu: true,
menuBuilder: (node, state) => Positioned(
menuBuilder: (Node node, CustomImageBlockComponentState state) =>
Positioned(
top: 0,
right: 10,
child: ImageMenu(

View File

@ -22,7 +22,7 @@ final customizeFontToolbarItem = ToolbarItem(
onClose: () => keepEditorFocusNotifier.value -= 1,
showResetButton: true,
onFontFamilyChanged: (fontFamily) async {
await popoverController.close();
popoverController.close();
try {
await editorState.formatDelta(selection, {
AppFlowyRichTextKeys.fontFamily: fontFamily,

View File

@ -0,0 +1,248 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
typedef CustomImageBlockComponentMenuBuilder = Widget Function(
Node node,
CustomImageBlockComponentState state,
);
class CustomImageBlockComponentBuilder extends BlockComponentBuilder {
CustomImageBlockComponentBuilder({
super.configuration,
this.showMenu = false,
this.menuBuilder,
});
/// Whether to show the menu of this block component.
final bool showMenu;
///
final CustomImageBlockComponentMenuBuilder? menuBuilder;
@override
BlockComponentWidget build(BlockComponentContext blockComponentContext) {
final node = blockComponentContext.node;
return CustomImageBlockComponent(
key: node.key,
node: node,
showActions: showActions(node),
configuration: configuration,
actionBuilder: (context, state) => actionBuilder(
blockComponentContext,
state,
),
showMenu: showMenu,
menuBuilder: menuBuilder,
);
}
@override
bool validate(Node node) => node.delta == null && node.children.isEmpty;
}
class CustomImageBlockComponent extends BlockComponentStatefulWidget {
const CustomImageBlockComponent({
super.key,
required super.node,
super.showActions,
super.actionBuilder,
super.configuration = const BlockComponentConfiguration(),
this.showMenu = false,
this.menuBuilder,
});
/// Whether to show the menu of this block component.
final bool showMenu;
final CustomImageBlockComponentMenuBuilder? menuBuilder;
@override
State<CustomImageBlockComponent> createState() =>
CustomImageBlockComponentState();
}
class CustomImageBlockComponentState extends State<CustomImageBlockComponent>
with SelectableMixin, BlockComponentConfigurable {
@override
BlockComponentConfiguration get configuration => widget.configuration;
@override
Node get node => widget.node;
final imageKey = GlobalKey();
RenderBox? get _renderBox => context.findRenderObject() as RenderBox?;
late final editorState = Provider.of<EditorState>(context, listen: false);
final showActionsNotifier = ValueNotifier<bool>(false);
bool alwaysShowMenu = false;
@override
Widget build(BuildContext context) {
final node = widget.node;
final attributes = node.attributes;
final src = attributes[ImageBlockKeys.url];
final alignment = AlignmentExtension.fromString(
attributes[ImageBlockKeys.align] ?? 'center',
);
final width = attributes[ImageBlockKeys.width]?.toDouble() ??
MediaQuery.of(context).size.width;
final height = attributes[ImageBlockKeys.height]?.toDouble();
Widget child = src.isEmpty
? ImagePlaceholder(
node: node,
)
: ResizableImage(
src: src,
width: width,
height: height,
editable: editorState.editable,
alignment: alignment,
onResize: (width) {
final transaction = editorState.transaction
..updateNode(node, {
ImageBlockKeys.width: width,
});
editorState.apply(transaction);
},
);
child = BlockSelectionContainer(
node: node,
delegate: this,
listenable: editorState.selectionNotifier,
blockColor: editorState.editorStyle.selectionColor,
supportTypes: const [
BlockSelectionType.block,
],
child: Padding(
key: imageKey,
padding: padding,
child: child,
),
);
if (widget.showActions && widget.actionBuilder != null) {
child = BlockComponentActionWrapper(
node: node,
actionBuilder: widget.actionBuilder!,
child: child,
);
}
if (widget.showMenu && widget.menuBuilder != null) {
child = MouseRegion(
onEnter: (_) => showActionsNotifier.value = true,
onExit: (_) {
if (!alwaysShowMenu) {
showActionsNotifier.value = false;
}
},
hitTestBehavior: HitTestBehavior.opaque,
opaque: false,
child: ValueListenableBuilder<bool>(
valueListenable: showActionsNotifier,
builder: (context, value, child) {
final url = node.attributes[ImageBlockKeys.url];
return Stack(
children: [
BlockSelectionContainer(
node: node,
delegate: this,
listenable: editorState.selectionNotifier,
cursorColor: editorState.editorStyle.cursorColor,
selectionColor: editorState.editorStyle.selectionColor,
child: child!,
),
if (value && url.isNotEmpty == true)
widget.menuBuilder!(
widget.node,
this,
),
],
);
},
child: child,
),
);
}
return child;
}
@override
Position start() => Position(path: widget.node.path, offset: 0);
@override
Position end() => Position(path: widget.node.path, offset: 1);
@override
Position getPositionInOffset(Offset start) => end();
@override
bool get shouldCursorBlink => false;
@override
CursorStyle get cursorStyle => CursorStyle.cover;
@override
Rect getBlockRect({
bool shiftWithBaseOffset = false,
}) {
final imageBox = imageKey.currentContext?.findRenderObject();
if (imageBox is RenderBox) {
return Offset.zero & imageBox.size;
}
return Rect.zero;
}
@override
Rect? getCursorRectInPosition(
Position position, {
bool shiftWithBaseOffset = false,
}) {
if (_renderBox == null) {
return null;
}
final size = _renderBox!.size;
return Rect.fromLTWH(-size.width / 2.0, 0, size.width, size.height);
}
@override
List<Rect> getRectsInSelection(
Selection selection, {
bool shiftWithBaseOffset = false,
}) {
if (_renderBox == null) {
return [];
}
final parentBox = context.findRenderObject();
final imageBox = imageKey.currentContext?.findRenderObject();
if (parentBox is RenderBox && imageBox is RenderBox) {
return [
imageBox.localToGlobal(Offset.zero, ancestor: parentBox) &
imageBox.size,
];
}
return [Offset.zero & _renderBox!.size];
}
@override
Selection getSelectionInRange(Offset start, Offset end) => Selection.single(
path: widget.node.path,
startOffset: 0,
endOffset: 1,
);
@override
Offset localToGlobal(
Offset offset, {
bool shiftWithBaseOffset = false,
}) =>
_renderBox!.localToGlobal(offset);
}

View File

@ -0,0 +1,46 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class EmbedImageUrlWidget extends StatefulWidget {
const EmbedImageUrlWidget({
super.key,
required this.onSubmit,
});
final void Function(String url) onSubmit;
@override
State<EmbedImageUrlWidget> createState() => _EmbedImageUrlWidgetState();
}
class _EmbedImageUrlWidgetState extends State<EmbedImageUrlWidget> {
String inputText = '';
@override
Widget build(BuildContext context) {
return Column(
children: [
FlowyTextField(
autoFocus: true,
hintText: LocaleKeys.document_imageBlock_embedLink_placeholder.tr(),
onChanged: (value) => inputText = value,
onEditingComplete: () => widget.onSubmit(inputText),
),
const VSpace(5),
SizedBox(
width: 160,
child: FlowyButton(
margin: const EdgeInsets.all(8.0),
text: FlowyText(
LocaleKeys.document_imageBlock_embedLink_label.tr(),
textAlign: TextAlign.center,
),
onTap: () => widget.onSubmit(inputText),
),
),
],
);
}
}

View File

@ -1,5 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
@ -18,7 +19,7 @@ class ImageMenu extends StatefulWidget {
});
final Node node;
final ImageBlockComponentWidgetState state;
final CustomImageBlockComponentState state;
@override
State<ImageMenu> createState() => _ImageMenuState();
@ -109,7 +110,7 @@ class _ImageAlignButton extends StatefulWidget {
});
final Node node;
final ImageBlockComponentWidgetState state;
final CustomImageBlockComponentState state;
@override
State<_ImageAlignButton> createState() => _ImageAlignButtonState();

View File

@ -0,0 +1,134 @@
import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/settings/application_data_storage.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log, UploadImageMenu;
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:path/path.dart' as p;
import 'package:string_validator/string_validator.dart';
class ImagePlaceholder extends StatefulWidget {
const ImagePlaceholder({
super.key,
required this.node,
});
final Node node;
@override
State<ImagePlaceholder> createState() => _ImagePlaceholderState();
}
class _ImagePlaceholderState extends State<ImagePlaceholder> {
final controller = PopoverController();
late final editorState = context.read<EditorState>();
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
controller: controller,
direction: PopoverDirection.bottomWithCenterAligned,
constraints: const BoxConstraints(
maxWidth: 540,
maxHeight: 260,
minHeight: 80,
),
popupBuilder: (context) {
return UploadImageMenu(
onPickFile: insertLocalImage,
onSubmit: insertNetworkImage,
);
},
child: DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(4),
),
child: FlowyHover(
style: HoverStyle(
borderRadius: BorderRadius.circular(4),
),
child: SizedBox(
height: 48,
child: Row(
children: [
const HSpace(10),
const FlowySvg(
FlowySvgs.image_placeholder_s,
size: Size.square(24),
),
const HSpace(10),
FlowyText(
LocaleKeys.document_plugins_image_addAnImage.tr(),
),
],
),
),
),
),
);
}
Future<void> insertLocalImage(String? url) async {
if (url == null || url.isEmpty) {
controller.close();
return;
}
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,
);
final transaction = editorState.transaction;
transaction.updateNode(widget.node, {
ImageBlockKeys.url: copyToPath,
});
await editorState.apply(transaction);
} catch (e) {
Log.error('cannot copy image file', e);
}
controller.close();
}
Future<void> insertNetworkImage(String url) async {
if (url.isEmpty || !isURL(url)) {
// show error
showSnackBarMessage(
context,
LocaleKeys.document_imageBlock_error_invalidImage.tr(),
);
return;
}
final transaction = editorState.transaction;
transaction.updateNode(widget.node, {
ImageBlockKeys.url: url,
});
await editorState.apply(transaction);
}
}

View File

@ -1,12 +1,4 @@
import 'dart:io';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/settings/application_data_storage.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,
@ -16,44 +8,7 @@ final customImageMenuItem = SelectionMenuItem(
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);
}
}
},
);
handler: (editorState, menuService, context) async {
return await editorState.insertImageNode('');
},
);

View File

@ -0,0 +1,150 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:unsplash_client/unsplash_client.dart';
class UnsplashImageWidget extends StatefulWidget {
const UnsplashImageWidget({
super.key,
required this.onSelectUnsplashImage,
});
final void Function(String url) onSelectUnsplashImage;
@override
State<UnsplashImageWidget> createState() => _UnsplashImageWidgetState();
}
class _UnsplashImageWidgetState extends State<UnsplashImageWidget> {
final client = UnsplashClient(
settings: const ClientSettings(
credentials: AppCredentials(
// TODO: there're the demo keys, we should replace them with the production keys when releasing and inject them with env file.
accessKey: 'YyD-LbW5bVolHWZBq5fWRM_3ezkG2XchRFjhNTnK9TE',
secretKey: '5z4EnxaXjWjWMnuBhc0Ku0uYW2bsYCZlO-REZaqmV6A',
),
),
);
late Future<List<Photo>> randomPhotos;
String query = '';
@override
void initState() {
super.initState();
randomPhotos = client.photos
.random(count: 18, orientation: PhotoOrientation.landscape)
.goAndGet();
}
@override
void dispose() {
client.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
FlowyTextField(
autoFocus: true,
hintText: LocaleKeys.document_imageBlock_searchForAnImage.tr(),
// textAlign: TextAlign.left,
onChanged: (value) => query = value,
onEditingComplete: () => setState(() {
randomPhotos = client.photos
.random(
count: 18,
orientation: PhotoOrientation.landscape,
query: query,
)
.goAndGet();
}),
),
const HSpace(12.0),
Expanded(
child: FutureBuilder(
future: randomPhotos,
builder: (context, value) {
final data = value.data;
if (!value.hasData || data == null || data.isEmpty) {
return const CircularProgressIndicator.adaptive();
}
return GridView.count(
crossAxisCount: 3,
mainAxisSpacing: 16.0,
crossAxisSpacing: 10.0,
childAspectRatio: 4 / 3,
children: data
.map(
(photo) => _UnsplashImage(
photo: photo,
onTap: () => widget.onSelectUnsplashImage(
photo.urls.regular.toString(),
),
),
)
.toList(),
);
},
),
),
],
);
}
}
class _UnsplashImage extends StatelessWidget {
const _UnsplashImage({
required this.photo,
required this.onTap,
});
final Photo photo;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Image.network(
photo.urls.thumb.toString(),
fit: BoxFit.cover,
),
),
FlowyText(
'by ${photo.name}',
fontSize: 10.0,
),
],
),
);
}
}
extension on Photo {
String get name {
if (user.username.isNotEmpty) {
return user.username;
}
if (user.name.isNotEmpty) {
return user.name;
}
if (user.email?.isNotEmpty == true) {
return user.email!;
}
return user.id;
}
}

View File

@ -0,0 +1,49 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/file_picker/file_picker_service.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
class UploadImageFileWidget extends StatelessWidget {
const UploadImageFileWidget({
super.key,
required this.onPickFile,
this.allowedExtensions = const ['jpg', 'png', 'jpeg'],
});
final void Function(String? path) onPickFile;
final List<String> allowedExtensions;
@override
Widget build(BuildContext context) {
return FlowyHover(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTapDown: (_) async {
final result = await getIt<FilePickerService>().pickFiles(
dialogTitle: '',
allowMultiple: false,
type: FileType.image,
allowedExtensions: allowedExtensions,
);
onPickFile(result?.files.firstOrNull?.path);
},
child: Container(
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(vertical: 8.0),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.surfaceVariant,
width: 1.0,
),
),
child: FlowyText(
LocaleKeys.document_imageBlock_upload_placeholder.tr(),
),
),
),
);
}
}

View File

@ -0,0 +1,122 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_file_widget.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
enum UploadImageType {
local,
url,
unsplash,
ai;
String get description {
switch (this) {
case UploadImageType.local:
return LocaleKeys.document_imageBlock_upload_label.tr();
case UploadImageType.url:
return LocaleKeys.document_imageBlock_embedLink_label.tr();
case UploadImageType.unsplash:
return 'Unsplash';
case UploadImageType.ai:
return 'Generate from AI';
}
}
}
class UploadImageMenu extends StatefulWidget {
const UploadImageMenu({
super.key,
required this.onPickFile,
required this.onSubmit,
});
final void Function(String? path) onPickFile;
final void Function(String url) onSubmit;
@override
State<UploadImageMenu> createState() => _UploadImageMenuState();
}
class _UploadImageMenuState extends State<UploadImageMenu> {
int currentTabIndex = 0;
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3, // UploadImageType.values.length, // ai is not implemented yet
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TabBar(
onTap: (value) => setState(() {
currentTabIndex = value;
}),
indicatorSize: TabBarIndicatorSize.label,
isScrollable: true,
overlayColor: MaterialStatePropertyAll(
Theme.of(context).colorScheme.secondary,
),
padding: EdgeInsets.zero,
// splashBorderRadius: BorderRadius.circular(4),
tabs: UploadImageType.values
.where(
(element) => element != UploadImageType.ai,
) // ai is not implemented yet
.map(
(e) => FlowyHover(
style: const HoverStyle(borderRadius: BorderRadius.zero),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 8.0,
),
child: FlowyText(e.description),
),
),
)
.toList(),
),
const Divider(
height: 2,
),
_buildTab(),
],
),
);
}
Widget _buildTab() {
final type = UploadImageType.values[currentTabIndex];
switch (type) {
case UploadImageType.local:
return Padding(
padding: const EdgeInsets.all(8.0),
child: UploadImageFileWidget(
onPickFile: widget.onPickFile,
),
);
case UploadImageType.url:
return Padding(
padding: const EdgeInsets.all(8.0),
child: EmbedImageUrlWidget(
onSubmit: widget.onSubmit,
),
);
case UploadImageType.unsplash:
return Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: UnsplashImageWidget(
onSelectUnsplashImage: widget.onSubmit,
),
),
);
case UploadImageType.ai:
return const FlowyText.medium('ai');
}
}
}

View File

@ -55,7 +55,9 @@ class _SmartEditActionListState extends State<SmartEditActionList> {
actions: SmartEditAction.values
.map((action) => SmartEditActionWrapper(action))
.toList(),
onClosed: () => keepEditorFocusNotifier.value -= 1,
buildChild: (controller) {
keepEditorFocusNotifier.value += 1;
return FlowyIconButton(
hoverColor: Colors.transparent,
tooltipText: isOpenAIEnabled

View File

@ -32,7 +32,7 @@ class SettingsLanguageView extends StatelessWidget {
class LanguageSelector extends StatelessWidget {
final Locale currentLocale;
const LanguageSelector({
super.key,
required this.currentLocale,

View File

@ -1,17 +1,18 @@
import 'package:appflowy_popover/src/layout.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'mask.dart';
import 'mutex.dart';
class PopoverController {
PopoverState? _state;
close() {
void close() {
_state?.close();
}
show() {
void show() {
_state?.showOverlay();
}
}

View File

@ -153,8 +153,8 @@ class AFThemeExtension extends ThemeExtension<AFThemeExtension> {
Color.lerp(toggleButtonBGColor, other.toggleButtonBGColor, t)!,
calendarWeekendBGColor:
Color.lerp(calendarWeekendBGColor, other.calendarWeekendBGColor, t)!,
gridRowCountColor: Color.lerp(
gridRowCountColor, other.gridRowCountColor, t)!,
gridRowCountColor:
Color.lerp(gridRowCountColor, other.gridRowCountColor, t)!,
code: other.code,
callout: other.callout,
caption: other.caption,

View File

@ -54,8 +54,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: "0fdca2f"
resolved-ref: "0fdca2f702485eeec1bfbe50127c06f2a8fd8b1e"
ref: e996c92
resolved-ref: e996c9279d873f55a1b6aa919144763a60f83d32
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
source: git
version: "1.4.3"
@ -1447,7 +1447,7 @@ packages:
source: hosted
version: "1.2.0"
string_validator:
dependency: transitive
dependency: "direct main"
description:
name: string_validator
sha256: b419cf5d21d608522e6e7cafed4deb34b6f268c43df866e63c320bab98a08cf6
@ -1615,6 +1615,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0+1"
unsplash_client:
dependency: "direct main"
description:
name: unsplash_client
sha256: "832011981ef358ef4f816f356375620791d24dc6e1afb33a37f066df9aaea537"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
url_launcher:
dependency: "direct main"
description:

View File

@ -47,7 +47,7 @@ dependencies:
appflowy_editor:
git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git
ref: '0fdca2f'
ref: 'e996c92'
appflowy_popover:
path: packages/appflowy_popover
@ -108,6 +108,8 @@ dependencies:
hive_flutter: ^1.1.0
super_clipboard: ^0.6.3
go_router: ^10.1.2
string_validator: ^1.0.0
unsplash_client: ^2.1.1
# Notifications
# TODO: Consider implementing custom package

View File

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1.5" y="3" width="13" height="10" rx="1.5" stroke="#333333"/>
<circle cx="5.5" cy="6.5" r="1" stroke="#333333"/>
<path d="M5 13L10.112 8.45603C10.4211 8.18126 10.8674 8.12513 11.235 8.31482L14.5 10" stroke="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 330 B

View File

@ -616,7 +616,8 @@
"defaultColor": "Default"
},
"image": {
"copiedToPasteBoard": "The image link has been copied to the clipboard"
"copiedToPasteBoard": "The image link has been copied to the clipboard",
"addAnImage": "Add an image"
},
"outline": {
"addHeadingToCreateOutline": "Add headings to create a table of contents."
@ -657,7 +658,12 @@
"invalidImageSize": "Image size must be less than 5MB",
"invalidImageFormat": "Image format is not supported. Supported formats: JPEG, PNG, GIF, SVG",
"invalidImageUrl": "Invalid image URL"
}
},
"embedLink": {
"label": "Embed link",
"placeholder": "Paste or type an image link"
},
"searchForAnImage": "Search for an image"
},
"codeBlock": {
"language": {