Skip to content

Commit ffdf5d2

Browse files
authored
fix: openAI image expiration (#3660)
* feat: save the openAI image to local storage * feat: support rendering error block * fix: enter on Toggle list moves heading down without contents
1 parent b247a49 commit ffdf5d2

File tree

9 files changed

+186
-19
lines changed

9 files changed

+186
-19
lines changed

frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,9 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
151151
effectiveScrollController = widget.scrollController ?? ScrollController();
152152

153153
// keep the previous font style when typing new text.
154+
supportSlashMenuNodeWhiteList.addAll([
155+
ToggleListBlockKeys.type,
156+
]);
154157
AppFlowyRichTextKeys.supportSliced.add(AppFlowyRichTextKeys.fontFamily);
155158
}
156159

@@ -353,6 +356,11 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
353356
styleCustomizer.outlineBlockPlaceholderStyleBuilder(),
354357
),
355358
),
359+
errorBlockComponentBuilderKey: ErrorBlockComponentBuilder(
360+
configuration: configuration.copyWith(
361+
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
362+
),
363+
),
356364
};
357365

358366
final builders = {
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import 'dart:convert';
2+
3+
import 'package:appflowy/generated/locale_keys.g.dart';
4+
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
5+
import 'package:appflowy/startup/startup.dart';
6+
import 'package:appflowy/workspace/presentation/home/toast.dart';
7+
import 'package:appflowy_editor/appflowy_editor.dart';
8+
import 'package:easy_localization/easy_localization.dart';
9+
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
10+
import 'package:flutter/material.dart';
11+
12+
class ErrorBlockComponentBuilder extends BlockComponentBuilder {
13+
ErrorBlockComponentBuilder({
14+
super.configuration,
15+
});
16+
17+
@override
18+
BlockComponentWidget build(BlockComponentContext blockComponentContext) {
19+
final node = blockComponentContext.node;
20+
return ErrorBlockComponentWidget(
21+
key: node.key,
22+
node: node,
23+
configuration: configuration,
24+
showActions: showActions(node),
25+
actionBuilder: (context, state) => actionBuilder(
26+
blockComponentContext,
27+
state,
28+
),
29+
);
30+
}
31+
32+
@override
33+
bool validate(Node node) => true;
34+
}
35+
36+
class ErrorBlockComponentWidget extends BlockComponentStatefulWidget {
37+
const ErrorBlockComponentWidget({
38+
super.key,
39+
required super.node,
40+
super.showActions,
41+
super.actionBuilder,
42+
super.configuration = const BlockComponentConfiguration(),
43+
});
44+
45+
@override
46+
State<ErrorBlockComponentWidget> createState() =>
47+
_DividerBlockComponentWidgetState();
48+
}
49+
50+
class _DividerBlockComponentWidgetState extends State<ErrorBlockComponentWidget>
51+
with BlockComponentConfigurable {
52+
@override
53+
BlockComponentConfiguration get configuration => widget.configuration;
54+
55+
@override
56+
Node get node => widget.node;
57+
58+
@override
59+
Widget build(BuildContext context) {
60+
Widget child = DecoratedBox(
61+
decoration: BoxDecoration(
62+
color: Theme.of(context).colorScheme.surfaceVariant,
63+
borderRadius: BorderRadius.circular(4),
64+
),
65+
child: FlowyButton(
66+
onTap: () async {
67+
showSnackBarMessage(
68+
context,
69+
LocaleKeys.document_errorBlock_blockContentHasBeenCopied.tr(),
70+
);
71+
await getIt<ClipboardService>().setData(
72+
ClipboardServiceData(plainText: jsonEncode(node.toJson())),
73+
);
74+
},
75+
text: Container(
76+
height: 48,
77+
alignment: Alignment.center,
78+
child: FlowyText(
79+
LocaleKeys.document_errorBlock_theBlockIsNotSupported.tr(),
80+
),
81+
),
82+
),
83+
);
84+
85+
child = Padding(
86+
padding: padding,
87+
child: child,
88+
);
89+
90+
if (widget.showActions && widget.actionBuilder != null) {
91+
child = BlockComponentActionWrapper(
92+
node: node,
93+
actionBuilder: widget.actionBuilder!,
94+
child: child,
95+
);
96+
}
97+
98+
return child;
99+
}
100+
}

frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
1515
import 'package:flowy_infra_ui/style_widget/hover.dart';
1616
import 'package:flutter/material.dart';
1717
import 'package:flutter_bloc/flutter_bloc.dart';
18+
import 'package:http/http.dart';
1819
import 'package:path/path.dart' as p;
1920
import 'package:string_validator/string_validator.dart';
2021

@@ -47,16 +48,22 @@ class _ImagePlaceholderState extends State<ImagePlaceholder> {
4748
clickHandler: PopoverClickHandler.gestureDetector,
4849
popupBuilder: (context) {
4950
return UploadImageMenu(
50-
onPickFile: (path) {
51+
onSelectedLocalImage: (path) {
5152
controller.close();
52-
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
53-
insertLocalImage(path);
53+
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
54+
await insertLocalImage(path);
5455
});
5556
},
56-
onSubmit: (url) {
57+
onSelectedAIImage: (url) {
5758
controller.close();
58-
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
59-
insertNetworkImage(url);
59+
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
60+
await insertAIImage(url);
61+
});
62+
},
63+
onSelectedNetworkImage: (url) {
64+
controller.close();
65+
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
66+
await insertNetworkImage(url);
6067
});
6168
},
6269
);
@@ -123,7 +130,46 @@ class _ImagePlaceholderState extends State<ImagePlaceholder> {
123130
} catch (e) {
124131
Log.error('cannot copy image file', e);
125132
}
126-
controller.close();
133+
}
134+
135+
Future<void> insertAIImage(String url) async {
136+
if (url.isEmpty || !isURL(url)) {
137+
// show error
138+
showSnackBarMessage(
139+
context,
140+
LocaleKeys.document_imageBlock_error_invalidImage.tr(),
141+
);
142+
return;
143+
}
144+
145+
final path = await getIt<ApplicationDataStorage>().getPath();
146+
final imagePath = p.join(
147+
path,
148+
'images',
149+
);
150+
try {
151+
// create the directory if not exists
152+
final directory = Directory(imagePath);
153+
if (!directory.existsSync()) {
154+
await directory.create(recursive: true);
155+
}
156+
final uri = Uri.parse(url);
157+
final copyToPath = p.join(
158+
imagePath,
159+
'${uuid()}${p.extension(uri.path)}',
160+
);
161+
162+
final response = await get(uri);
163+
await File(copyToPath).writeAsBytes(response.bodyBytes);
164+
165+
final transaction = editorState.transaction;
166+
transaction.updateNode(widget.node, {
167+
ImageBlockKeys.url: copyToPath,
168+
});
169+
await editorState.apply(transaction);
170+
} catch (e) {
171+
Log.error('cannot save image file', e);
172+
}
127173
}
128174

129175
Future<void> insertNetworkImage(String url) async {

frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,14 @@ enum UploadImageType {
3636
class UploadImageMenu extends StatefulWidget {
3737
const UploadImageMenu({
3838
super.key,
39-
required this.onPickFile,
40-
required this.onSubmit,
39+
required this.onSelectedLocalImage,
40+
required this.onSelectedAIImage,
41+
required this.onSelectedNetworkImage,
4142
});
4243

43-
final void Function(String? path) onPickFile;
44-
final void Function(String url) onSubmit;
44+
final void Function(String? path) onSelectedLocalImage;
45+
final void Function(String url) onSelectedAIImage;
46+
final void Function(String url) onSelectedNetworkImage;
4547

4648
@override
4749
State<UploadImageMenu> createState() => _UploadImageMenuState();
@@ -127,22 +129,22 @@ class _UploadImageMenuState extends State<UploadImageMenu> {
127129
return Padding(
128130
padding: const EdgeInsets.all(8.0),
129131
child: UploadImageFileWidget(
130-
onPickFile: widget.onPickFile,
132+
onPickFile: widget.onSelectedLocalImage,
131133
),
132134
);
133135
case UploadImageType.url:
134136
return Padding(
135137
padding: const EdgeInsets.all(8.0),
136138
child: EmbedImageUrlWidget(
137-
onSubmit: widget.onSubmit,
139+
onSubmit: widget.onSelectedNetworkImage,
138140
),
139141
);
140142
case UploadImageType.unsplash:
141143
return Expanded(
142144
child: Padding(
143145
padding: const EdgeInsets.all(8.0),
144146
child: UnsplashImageWidget(
145-
onSelectUnsplashImage: widget.onSubmit,
147+
onSelectUnsplashImage: widget.onSelectedNetworkImage,
146148
),
147149
),
148150
);
@@ -152,7 +154,7 @@ class _UploadImageMenuState extends State<UploadImageMenu> {
152154
child: Padding(
153155
padding: const EdgeInsets.all(8.0),
154156
child: OpenAIImageWidget(
155-
onSelectNetworkImage: widget.onSubmit,
157+
onSelectNetworkImage: widget.onSelectedAIImage,
156158
),
157159
),
158160
)
@@ -168,7 +170,7 @@ class _UploadImageMenuState extends State<UploadImageMenu> {
168170
child: Padding(
169171
padding: const EdgeInsets.all(8.0),
170172
child: StabilityAIImageWidget(
171-
onSelectImage: widget.onPickFile,
173+
onSelectImage: widget.onSelectedLocalImage,
172174
),
173175
),
174176
)

frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export 'copy_and_paste/custom_paste_command.dart';
1212
export 'database/database_view_block_component.dart';
1313
export 'database/inline_database_menu_item.dart';
1414
export 'database/referenced_database_menu_item.dart';
15+
export 'error/error_block_component_builder.dart';
1516
export 'extensions/flowy_tint_extension.dart';
1617
export 'find_and_replace/find_and_replace_menu.dart';
1718
export 'font/customize_font_toolbar_item.dart';

frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcut_event.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ CharacterShortcutEvent insertChildNodeInsideToggleList = CharacterShortcutEvent(
6060
..afterSelection = Selection.collapsed(
6161
Position(path: selection.start.path, offset: 0),
6262
);
63+
} else if (selection.startIndex == 0) {
64+
// insert a paragraph block above the current toggle list block
65+
transaction.insertNode(selection.start.path, paragraphNode());
66+
transaction.afterSelection = Selection.collapsed(
67+
Position(path: selection.start.path.next, offset: 0),
68+
);
6369
} else {
6470
// insert a toggle list block below the current toggle list block
6571
transaction

frontend/appflowy_flutter/pubspec.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ packages:
5454
dependency: "direct main"
5555
description:
5656
path: "."
57-
ref: "0abcf7f"
58-
resolved-ref: "0abcf7f6d273b838c895abdc17f6833540613729"
57+
ref: adb05d4
58+
resolved-ref: adb05d4c49fe2f518e5554cc7d6c2fbe3b01670d
5959
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
6060
source: git
6161
version: "1.4.3"

frontend/appflowy_flutter/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ dependencies:
4747
appflowy_editor:
4848
git:
4949
url: https://github.com/AppFlowy-IO/appflowy-editor.git
50-
ref: "0abcf7f"
50+
ref: "adb05d4"
5151
appflowy_popover:
5252
path: packages/appflowy_popover
5353

frontend/resources/translations/en.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,10 @@
705705
},
706706
"toolbar": {
707707
"resetToDefaultFont": "Reset to default"
708+
},
709+
"errorBlock": {
710+
"theBlockIsNotSupported": "The current version does not support this block.",
711+
"blockContentHasBeenCopied": "The block content has been copied."
708712
}
709713
},
710714
"board": {

0 commit comments

Comments
 (0)