feat: optimize image block (#4553)

* feat: add tooltip for maximum image size

* feat: add maximum upload image size tooltip

* feat: limit image size to 10MB

* fix: disable copy link option for cloud image

* fix: disable copy link option for cloud image

* feat: use regex to match the appflowy.cloud image
This commit is contained in:
Lucas.Xu 2024-01-30 22:39:42 +08:00 committed by GitHub
parent e9d7d0b7b3
commit 792573f46d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 120 additions and 57 deletions

View File

@ -48,9 +48,6 @@ PODS:
- fluttertoast (0.0.2): - fluttertoast (0.0.2):
- Flutter - Flutter
- Toast - Toast
- FMDB (2.7.5):
- FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5)
- image_gallery_saver (2.0.2): - image_gallery_saver (2.0.2):
- Flutter - Flutter
- image_picker_ios (0.0.1): - image_picker_ios (0.0.1):
@ -75,19 +72,15 @@ PODS:
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sign_in_with_apple (0.0.1):
- Flutter
- sqflite (0.0.3): - sqflite (0.0.3):
- Flutter - Flutter
- FMDB (>= 2.7.5) - FlutterMacOS
- super_native_extensions (0.0.1): - super_native_extensions (0.0.1):
- Flutter - Flutter
- SwiftyGif (5.4.3) - SwiftyGif (5.4.3)
- Toast (4.0.0) - Toast (4.0.0)
- url_launcher_ios (0.0.1): - url_launcher_ios (0.0.1):
- Flutter - Flutter
- webview_flutter_wkwebview (0.0.1):
- Flutter
DEPENDENCIES: DEPENDENCIES:
- app_links (from `.symlinks/plugins/app_links/ios`) - app_links (from `.symlinks/plugins/app_links/ios`)
@ -107,17 +100,14 @@ DEPENDENCIES:
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- rich_clipboard_ios (from `.symlinks/plugins/rich_clipboard_ios/ios`) - rich_clipboard_ios (from `.symlinks/plugins/rich_clipboard_ios/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`) - sqflite (from `.symlinks/plugins/sqflite/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`) - super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
SPEC REPOS: SPEC REPOS:
trunk: trunk:
- DKImagePickerController - DKImagePickerController
- DKPhotoGallery - DKPhotoGallery
- FMDB
- ReachabilitySwift - ReachabilitySwift
- SDWebImage - SDWebImage
- SwiftyGif - SwiftyGif
@ -158,16 +148,12 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/rich_clipboard_ios/ios" :path: ".symlinks/plugins/rich_clipboard_ios/ios"
shared_preferences_foundation: shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin" :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sign_in_with_apple:
:path: ".symlinks/plugins/sign_in_with_apple/ios"
sqflite: sqflite:
:path: ".symlinks/plugins/sqflite/ios" :path: ".symlinks/plugins/sqflite/darwin"
super_native_extensions: super_native_extensions:
:path: ".symlinks/plugins/super_native_extensions/ios" :path: ".symlinks/plugins/super_native_extensions/ios"
url_launcher_ios: url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios" :path: ".symlinks/plugins/url_launcher_ios/ios"
webview_flutter_wkwebview:
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
app_links: 5ef33d0d295a89d9d16bb81b0e3b0d5f70d6c875 app_links: 5ef33d0d295a89d9d16bb81b0e3b0d5f70d6c875
@ -180,25 +166,22 @@ SPEC CHECKSUMS:
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
integration_test: 13825b8a9334a850581300559b8839134b124670 integration_test: 13825b8a9334a850581300559b8839134b124670
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9 irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86 keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
rich_clipboard_ios: 7588abe18f881a6d0e9ec0b12e51cae2761e8942 rich_clipboard_ios: 7588abe18f881a6d0e9ec0b12e51cae2761e8942
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84 SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
sign_in_with_apple: f3bf75217ea4c2c8b91823f225d70230119b8440 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7 super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a
PODFILE CHECKSUM: 8c681999c7764593c94846b2a64b44d86f7a27ac PODFILE CHECKSUM: 8c681999c7764593c94846b2a64b44d86f7a27ac

View File

@ -1,6 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'package:appflowy/env/cloud_env.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/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
@ -14,6 +13,7 @@ import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/shared/appflowy_network_image.dart'; import 'package:appflowy/shared/appflowy_network_image.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide UploadImageMenu; import 'package:appflowy_editor/appflowy_editor.dart' hide UploadImageMenu;
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
@ -449,6 +449,7 @@ class DocumentCoverState extends State<DocumentCover> {
minHeight: 80, minHeight: 80,
), ),
child: UploadImageMenu( child: UploadImageMenu(
limitMaximumImageSize: !_isLocalMode(),
supportTypes: const [ supportTypes: const [
UploadImageType.color, UploadImageType.color,
UploadImageType.local, UploadImageType.local,
@ -574,6 +575,7 @@ class DocumentCoverState extends State<DocumentCover> {
isPopoverOpen = true; isPopoverOpen = true;
return UploadImageMenu( return UploadImageMenu(
limitMaximumImageSize: !_isLocalMode(),
supportTypes: const [ supportTypes: const [
UploadImageType.color, UploadImageType.color,
UploadImageType.local, UploadImageType.local,
@ -609,9 +611,7 @@ class DocumentCoverState extends State<DocumentCover> {
Future<void> onCoverChanged(CoverType type, String? details) async { Future<void> onCoverChanged(CoverType type, String? details) async {
if (type == CoverType.file && details != null && !isURL(details)) { if (type == CoverType.file && details != null && !isURL(details)) {
final type = await getAuthenticatorType(); if (_isLocalMode()) {
// if the user is using local authenticator, we need to save the image to local storage
if (type == AuthenticatorType.local) {
details = await saveImageToLocalStorage(details); details = await saveImageToLocalStorage(details);
} else { } else {
// else we should save the image to cloud storage // else we should save the image to cloud storage
@ -627,6 +627,12 @@ class DocumentCoverState extends State<DocumentCover> {
isOverlayButtonsHidden = value; isOverlayButtonsHidden = value;
}); });
} }
bool _isLocalMode() {
final userProfilePB = context.read<DocumentBloc>().state.userProfilePB;
final type = userProfilePB?.authenticator ?? AuthenticatorPB.Local;
return type == AuthenticatorPB.Local;
}
} }
@visibleForTesting @visibleForTesting

View File

@ -9,6 +9,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/image/imag
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/resizeable_image.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/resizeable_image.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsupport_image_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsupport_image_widget.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/string_extension.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide ResizableImage; import 'package:appflowy_editor/appflowy_editor.dart' hide ResizableImage;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
@ -355,12 +356,15 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent>
// only used on mobile platform // only used on mobile platform
List<Widget> _buildExtendActionWidgets(BuildContext context) { List<Widget> _buildExtendActionWidgets(BuildContext context) {
final url = widget.node.attributes[CustomImageBlockKeys.url]; final String url = widget.node.attributes[CustomImageBlockKeys.url];
if (!_checkIfURLIsValid(url)) { if (!_checkIfURLIsValid(url)) {
return []; return [];
} }
return [ return [
// disable the copy link button if the image is hosted on appflowy cloud
// because the url needs the verification token to be accessible
if (!url.isAppFlowyCloudUrl)
FlowyOptionTile.text( FlowyOptionTile.text(
showTopBorder: false, showTopBorder: false,
text: LocaleKeys.editor_copyLink.tr(), text: LocaleKeys.editor_copyLink.tr(),

View File

@ -1,6 +1,7 @@
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/image/custom_image_block_component.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart';
import 'package:appflowy/util/string_extension.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
@ -26,6 +27,8 @@ class ImageMenu extends StatefulWidget {
} }
class _ImageMenuState extends State<ImageMenu> { class _ImageMenuState extends State<ImageMenu> {
late final String? url = widget.node.attributes[ImageBlockKeys.url];
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
@ -45,6 +48,9 @@ class _ImageMenuState extends State<ImageMenu> {
child: Row( child: Row(
children: [ children: [
const HSpace(4), const HSpace(4),
// disable the copy link button if the image is hosted on appflowy cloud
// because the url needs the verification token to be accessible
if (!(url?.isAppFlowyCloudUrl ?? false))
_ImageCopyLinkButton( _ImageCopyLinkButton(
onTap: copyImageLink, onTap: copyImageLink,
), ),
@ -64,9 +70,8 @@ class _ImageMenuState extends State<ImageMenu> {
} }
void copyImageLink() { void copyImageLink() {
final url = widget.node.attributes[ImageBlockKeys.url];
if (url != null) { if (url != null) {
Clipboard.setData(ClipboardData(text: url)); Clipboard.setData(ClipboardData(text: url!));
showSnackBarMessage( showSnackBarMessage(
context, context,
LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(), LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(),

View File

@ -9,6 +9,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/image/cust
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/file_extension.dart';
import 'package:appflowy/workspace/application/settings/application_data_storage.dart'; import 'package:appflowy/workspace/application/settings/application_data_storage.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
@ -85,6 +86,7 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
clickHandler: PopoverClickHandler.gestureDetector, clickHandler: PopoverClickHandler.gestureDetector,
popupBuilder: (context) { popupBuilder: (context) {
return UploadImageMenu( return UploadImageMenu(
limitMaximumImageSize: !_isLocalMode(),
onSelectedLocalImage: (path) { onSelectedLocalImage: (path) {
controller.close(); controller.close();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
@ -126,6 +128,7 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
if (PlatformExtension.isDesktopOrWeb) { if (PlatformExtension.isDesktopOrWeb) {
controller.show(); controller.show();
} else { } else {
final isLocalMode = _isLocalMode();
showMobileBottomSheet( showMobileBottomSheet(
context, context,
title: LocaleKeys.editor_image.tr(), title: LocaleKeys.editor_image.tr(),
@ -140,6 +143,7 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
minHeight: 80, minHeight: 80,
), ),
child: UploadImageMenu( child: UploadImageMenu(
limitMaximumImageSize: !isLocalMode,
supportTypes: const [ supportTypes: const [
UploadImageType.local, UploadImageType.local,
UploadImageType.url, UploadImageType.url,
@ -170,15 +174,24 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
return; return;
} }
final userProfilePB = context.read<DocumentBloc>().state.userProfilePB; final size = url.fileSize;
if (size == null || size > 10 * 1024 * 1024) {
// show error
controller.close();
showSnackBarMessage(
context,
LocaleKeys.document_imageBlock_uploadImageErrorImageSizeTooBig.tr(),
);
return;
}
final transaction = editorState.transaction; final transaction = editorState.transaction;
final type = userProfilePB?.authenticator ?? AuthenticatorPB.Local;
String? path; String? path;
CustomImageType imageType = CustomImageType.local; CustomImageType imageType = CustomImageType.local;
// if the user is using local authenticator, we need to save the image to local storage // if the user is using local authenticator, we need to save the image to local storage
if (type == AuthenticatorPB.Local) { if (_isLocalMode()) {
path = await saveImageToLocalStorage(url); path = await saveImageToLocalStorage(url);
} else { } else {
// else we should save the image to cloud storage // else we should save the image to cloud storage
@ -258,4 +271,10 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
}); });
await editorState.apply(transaction); await editorState.apply(transaction);
} }
bool _isLocalMode() {
final userProfilePB = context.read<DocumentBloc>().state.userProfilePB;
final type = userProfilePB?.authenticator ?? AuthenticatorPB.Local;
return type == AuthenticatorPB.Local;
}
} }

View File

@ -48,6 +48,7 @@ class UploadImageMenu extends StatefulWidget {
required this.onSelectedNetworkImage, required this.onSelectedNetworkImage,
this.onSelectedColor, this.onSelectedColor,
this.supportTypes = UploadImageType.values, this.supportTypes = UploadImageType.values,
this.limitMaximumImageSize = false,
}); });
final void Function(String? path) onSelectedLocalImage; final void Function(String? path) onSelectedLocalImage;
@ -55,6 +56,7 @@ class UploadImageMenu extends StatefulWidget {
final void Function(String url) onSelectedNetworkImage; final void Function(String url) onSelectedNetworkImage;
final void Function(String color)? onSelectedColor; final void Function(String color)? onSelectedColor;
final List<UploadImageType> supportTypes; final List<UploadImageType> supportTypes;
final bool limitMaximumImageSize;
@override @override
State<UploadImageMenu> createState() => _UploadImageMenuState(); State<UploadImageMenu> createState() => _UploadImageMenuState();
@ -151,9 +153,21 @@ class _UploadImageMenuState extends State<UploadImageMenu> {
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
alignment: Alignment.center, alignment: Alignment.center,
constraints: constraints, constraints: constraints,
child: UploadImageFileWidget( child: Column(
children: [
UploadImageFileWidget(
onPickFile: widget.onSelectedLocalImage, onPickFile: widget.onSelectedLocalImage,
), ),
if (widget.limitMaximumImageSize) ...[
const VSpace(6.0),
FlowyText(
LocaleKeys.document_imageBlock_maximumImageSize.tr(),
fontSize: 12.0,
color: Theme.of(context).hintColor,
),
],
],
),
); );
case UploadImageType.url: case UploadImageType.url:
return Container( return Container(

View File

@ -1,6 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:appflowy/shared/custom_image_cache_manager.dart'; import 'package:appflowy/shared/custom_image_cache_manager.dart';
import 'package:appflowy/util/string_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -33,7 +34,7 @@ class FlowyNetworkImage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(isURL(url)); assert(isURL(url));
if (url.contains('beta.appflowy')) { if (url.isAppFlowyCloudUrl) {
assert(userProfilePB != null && userProfilePB!.token.isNotEmpty); assert(userProfilePB != null && userProfilePB!.token.isNotEmpty);
} }

View File

@ -0,0 +1,11 @@
import 'dart:io';
extension FileSizeExtension on String {
int? get fileSize {
final file = File(this);
if (file.existsSync()) {
return file.lengthSync();
}
return null;
}
}

View File

@ -1,6 +1,8 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
extension EncodeString on String { extension StringExtension on String {
static const _specialCharacters = r'\/:*?"<>| '; static const _specialCharacters = r'\/:*?"<>| ';
/// Encode a string to a file name. /// Encode a string to a file name.
@ -17,4 +19,20 @@ extension EncodeString on String {
} }
return buffer.toString(); return buffer.toString();
} }
/// Returns the file size of the file at the given path.
///
/// Returns null if the file does not exist.
int? get fileSize {
final file = File(this);
if (file.existsSync()) {
return file.lengthSync();
}
return null;
}
/// Returns if the string is a appflowy cloud url.
bool get isAppFlowyCloudUrl {
return RegExp(r'^(https:\/\/)(.*)(\.appflowy\.cloud\/)(.*)').hasMatch(this);
}
} }

View File

@ -835,7 +835,9 @@
"saveImageToGallery": "Save image", "saveImageToGallery": "Save image",
"failedToAddImageToGallery": "Failed to add image to gallery", "failedToAddImageToGallery": "Failed to add image to gallery",
"successToAddImageToGallery": "Image added to gallery successfully", "successToAddImageToGallery": "Image added to gallery successfully",
"unableToLoadImage": "Unable to load image" "unableToLoadImage": "Unable to load image",
"maximumImageSize": "Maximum supported upload image size is 10MB",
"uploadImageErrorImageSizeTooBig": "Image size must be less than 10MB"
}, },
"codeBlock": { "codeBlock": {
"language": { "language": {