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

View File

@ -1,6 +1,5 @@
import 'dart:io';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.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/workspace/application/view/view_listener.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_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
@ -449,6 +449,7 @@ class DocumentCoverState extends State<DocumentCover> {
minHeight: 80,
),
child: UploadImageMenu(
limitMaximumImageSize: !_isLocalMode(),
supportTypes: const [
UploadImageType.color,
UploadImageType.local,
@ -574,6 +575,7 @@ class DocumentCoverState extends State<DocumentCover> {
isPopoverOpen = true;
return UploadImageMenu(
limitMaximumImageSize: !_isLocalMode(),
supportTypes: const [
UploadImageType.color,
UploadImageType.local,
@ -609,9 +611,7 @@ class DocumentCoverState extends State<DocumentCover> {
Future<void> onCoverChanged(CoverType type, String? details) async {
if (type == CoverType.file && details != null && !isURL(details)) {
final type = await getAuthenticatorType();
// if the user is using local authenticator, we need to save the image to local storage
if (type == AuthenticatorType.local) {
if (_isLocalMode()) {
details = await saveImageToLocalStorage(details);
} else {
// else we should save the image to cloud storage
@ -627,6 +627,12 @@ class DocumentCoverState extends State<DocumentCover> {
isOverlayButtonsHidden = value;
});
}
bool _isLocalMode() {
final userProfilePB = context.read<DocumentBloc>().state.userProfilePB;
final type = userProfilePB?.authenticator ?? AuthenticatorPB.Local;
return type == AuthenticatorPB.Local;
}
}
@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/unsupport_image_widget.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_editor/appflowy_editor.dart' hide ResizableImage;
import 'package:easy_localization/easy_localization.dart';
@ -355,27 +356,30 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent>
// only used on mobile platform
List<Widget> _buildExtendActionWidgets(BuildContext context) {
final url = widget.node.attributes[CustomImageBlockKeys.url];
final String url = widget.node.attributes[CustomImageBlockKeys.url];
if (!_checkIfURLIsValid(url)) {
return [];
}
return [
FlowyOptionTile.text(
showTopBorder: false,
text: LocaleKeys.editor_copyLink.tr(),
leftIcon: const FlowySvg(
FlowySvgs.m_field_copy_s,
// 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(
showTopBorder: false,
text: LocaleKeys.editor_copyLink.tr(),
leftIcon: const FlowySvg(
FlowySvgs.m_field_copy_s,
),
onTap: () async {
context.pop();
showSnackBarMessage(
context,
LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(),
);
await getIt<ClipboardService>().setPlainText(url);
},
),
onTap: () async {
context.pop();
showSnackBarMessage(
context,
LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(),
);
await getIt<ClipboardService>().setPlainText(url);
},
),
FlowyOptionTile.text(
showTopBorder: false,
text: LocaleKeys.document_imageBlock_saveImageToGallery.tr(),

View File

@ -1,6 +1,7 @@
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/util/string_extension.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
@ -26,6 +27,8 @@ class ImageMenu extends StatefulWidget {
}
class _ImageMenuState extends State<ImageMenu> {
late final String? url = widget.node.attributes[ImageBlockKeys.url];
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
@ -45,9 +48,12 @@ class _ImageMenuState extends State<ImageMenu> {
child: Row(
children: [
const HSpace(4),
_ImageCopyLinkButton(
onTap: copyImageLink,
),
// 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(
onTap: copyImageLink,
),
const HSpace(4),
_ImageAlignButton(
node: widget.node,
@ -64,9 +70,8 @@ class _ImageMenuState extends State<ImageMenu> {
}
void copyImageLink() {
final url = widget.node.attributes[ImageBlockKeys.url];
if (url != null) {
Clipboard.setData(ClipboardData(text: url));
Clipboard.setData(ClipboardData(text: url!));
showSnackBarMessage(
context,
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/upload_image_menu.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/presentation/home/toast.dart';
import 'package:appflowy_backend/log.dart';
@ -85,6 +86,7 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
clickHandler: PopoverClickHandler.gestureDetector,
popupBuilder: (context) {
return UploadImageMenu(
limitMaximumImageSize: !_isLocalMode(),
onSelectedLocalImage: (path) {
controller.close();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
@ -126,6 +128,7 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
if (PlatformExtension.isDesktopOrWeb) {
controller.show();
} else {
final isLocalMode = _isLocalMode();
showMobileBottomSheet(
context,
title: LocaleKeys.editor_image.tr(),
@ -140,6 +143,7 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
minHeight: 80,
),
child: UploadImageMenu(
limitMaximumImageSize: !isLocalMode,
supportTypes: const [
UploadImageType.local,
UploadImageType.url,
@ -170,15 +174,24 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
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 type = userProfilePB?.authenticator ?? AuthenticatorPB.Local;
String? path;
CustomImageType imageType = CustomImageType.local;
// 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);
} else {
// else we should save the image to cloud storage
@ -258,4 +271,10 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
});
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,
this.onSelectedColor,
this.supportTypes = UploadImageType.values,
this.limitMaximumImageSize = false,
});
final void Function(String? path) onSelectedLocalImage;
@ -55,6 +56,7 @@ class UploadImageMenu extends StatefulWidget {
final void Function(String url) onSelectedNetworkImage;
final void Function(String color)? onSelectedColor;
final List<UploadImageType> supportTypes;
final bool limitMaximumImageSize;
@override
State<UploadImageMenu> createState() => _UploadImageMenuState();
@ -151,8 +153,20 @@ class _UploadImageMenuState extends State<UploadImageMenu> {
padding: const EdgeInsets.all(8.0),
alignment: Alignment.center,
constraints: constraints,
child: UploadImageFileWidget(
onPickFile: widget.onSelectedLocalImage,
child: Column(
children: [
UploadImageFileWidget(
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:

View File

@ -1,6 +1,7 @@
import 'dart:convert';
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:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
@ -33,7 +34,7 @@ class FlowyNetworkImage extends StatelessWidget {
Widget build(BuildContext context) {
assert(isURL(url));
if (url.contains('beta.appflowy')) {
if (url.isAppFlowyCloudUrl) {
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';
extension EncodeString on String {
extension StringExtension on String {
static const _specialCharacters = r'\/:*?"<>| ';
/// Encode a string to a file name.
@ -17,4 +19,20 @@ extension EncodeString on String {
}
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",
"failedToAddImageToGallery": "Failed to add image to gallery",
"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": {
"language": {