mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
e9d7d0b7b3
commit
792573f46d
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,12 +356,15 @@ 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 [
|
||||
// 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(),
|
||||
|
@ -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,6 +48,9 @@ class _ImageMenuState extends State<ImageMenu> {
|
||||
child: Row(
|
||||
children: [
|
||||
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(
|
||||
onTap: copyImageLink,
|
||||
),
|
||||
@ -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(),
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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,9 +153,21 @@ class _UploadImageMenuState extends State<UploadImageMenu> {
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
alignment: Alignment.center,
|
||||
constraints: constraints,
|
||||
child: UploadImageFileWidget(
|
||||
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:
|
||||
return Container(
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
11
frontend/appflowy_flutter/lib/util/file_extension.dart
Normal file
11
frontend/appflowy_flutter/lib/util/file_extension.dart
Normal 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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user