Add a event that triggers after removing the image from the editor && delete unused dependencies and upgrade all packages and plugins and remove gallery_saver which has not been updated for more than 23 months, it was a great plugin but it old now, and I also add some simple documentation and other minor improvements (#1413)

pull/1414/head
Ahmed Hnewa 2 years ago committed by GitHub
parent 09113cbc90
commit 3e0ed1212a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 239
      flutter_quill_extensions/lib/embeds/builders.dart
  2. 3
      flutter_quill_extensions/lib/embeds/embed_types.dart
  3. 6
      flutter_quill_extensions/lib/embeds/toolbar/image_video_utils.dart
  4. 81
      flutter_quill_extensions/lib/embeds/utils.dart
  5. 29
      flutter_quill_extensions/lib/flutter_quill_extensions.dart
  6. 21
      flutter_quill_extensions/pubspec.yaml

@ -1,4 +1,4 @@
import 'dart:io'; import 'dart:io' show File;
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -7,13 +7,13 @@ import 'package:flutter/services.dart';
import 'package:flutter_quill/extensions.dart' as base; import 'package:flutter_quill/extensions.dart' as base;
import 'package:flutter_quill/flutter_quill.dart' hide Text; import 'package:flutter_quill/flutter_quill.dart' hide Text;
import 'package:flutter_quill/translations.dart'; import 'package:flutter_quill/translations.dart';
import 'package:gal/gal.dart';
import 'package:http/http.dart' as http;
import 'package:math_keyboard/math_keyboard.dart'; import 'package:math_keyboard/math_keyboard.dart';
import 'package:universal_html/html.dart' as html; import 'package:universal_html/html.dart' as html;
import '../shims/dart_ui_fake.dart' import '../shims/dart_ui_fake.dart'
if (dart.library.html) '../shims/dart_ui_real.dart' as ui; if (dart.library.html) 'package:flutter_quill_extensions/shims/dart_ui_real.dart'
as ui;
import 'embed_types.dart';
import 'utils.dart'; import 'utils.dart';
import 'widgets/image.dart'; import 'widgets/image.dart';
import 'widgets/image_resizer.dart'; import 'widgets/image_resizer.dart';
@ -21,6 +21,9 @@ import 'widgets/video_app.dart';
import 'widgets/youtube_video_app.dart'; import 'widgets/youtube_video_app.dart';
class ImageEmbedBuilder extends EmbedBuilder { class ImageEmbedBuilder extends EmbedBuilder {
ImageEmbedBuilder({required this.afterRemoveImageFromEditor});
final ImageEmbedBuilderAfterRemoveImageFromEditor afterRemoveImageFromEditor;
@override @override
String get key => BlockEmbed.imageType; String get key => BlockEmbed.imageType;
@ -38,112 +41,118 @@ class ImageEmbedBuilder extends EmbedBuilder {
) { ) {
assert(!kIsWeb, 'Please provide image EmbedBuilder for Web'); assert(!kIsWeb, 'Please provide image EmbedBuilder for Web');
var image; Widget image = const SizedBox.shrink();
final imageUrl = standardizeImageUrl(node.value.data); final imageUrl = standardizeImageUrl(node.value.data);
OptionalSize? _imageSize; OptionalSize? imageSize;
final style = node.style.attributes['style']; final style = node.style.attributes['style'];
if (base.isMobile() && style != null) { if (base.isMobile() && style != null) {
final _attrs = base.parseKeyValuePairs(style.value.toString(), { final attrs = base.parseKeyValuePairs(style.value.toString(), {
Attribute.mobileWidth, Attribute.mobileWidth,
Attribute.mobileHeight, Attribute.mobileHeight,
Attribute.mobileMargin, Attribute.mobileMargin,
Attribute.mobileAlignment Attribute.mobileAlignment
}); });
if (_attrs.isNotEmpty) { if (attrs.isNotEmpty) {
assert( assert(
_attrs[Attribute.mobileWidth] != null && attrs[Attribute.mobileWidth] != null &&
_attrs[Attribute.mobileHeight] != null, attrs[Attribute.mobileHeight] != null,
'mobileWidth and mobileHeight must be specified'); 'mobileWidth and mobileHeight must be specified');
final w = double.parse(_attrs[Attribute.mobileWidth]!); final w = double.parse(attrs[Attribute.mobileWidth]!);
final h = double.parse(_attrs[Attribute.mobileHeight]!); final h = double.parse(attrs[Attribute.mobileHeight]!);
_imageSize = OptionalSize(w, h); imageSize = OptionalSize(w, h);
final m = _attrs[Attribute.mobileMargin] == null final m = attrs[Attribute.mobileMargin] == null
? 0.0 ? 0.0
: double.parse(_attrs[Attribute.mobileMargin]!); : double.parse(attrs[Attribute.mobileMargin]!);
final a = base.getAlignment(_attrs[Attribute.mobileAlignment]); final a = base.getAlignment(attrs[Attribute.mobileAlignment]);
image = Padding( image = Padding(
padding: EdgeInsets.all(m), padding: EdgeInsets.all(m),
child: imageByUrl(imageUrl, width: w, height: h, alignment: a)); child: imageByUrl(imageUrl, width: w, height: h, alignment: a));
} }
} }
if (_imageSize == null) { if (imageSize == null) {
image = imageByUrl(imageUrl); image = imageByUrl(imageUrl);
_imageSize = OptionalSize((image as Image).width, image.height); imageSize = OptionalSize((image as Image).width, image.height);
} }
if (!readOnly && base.isMobile()) { if (!readOnly && base.isMobile()) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
showDialog( showDialog(
context: context, context: context,
builder: (context) { builder: (context) {
final resizeOption = _SimpleDialogItem( final resizeOption = _SimpleDialogItem(
icon: Icons.settings_outlined, icon: Icons.settings_outlined,
color: Colors.lightBlueAccent, color: Colors.lightBlueAccent,
text: 'Resize'.i18n, text: 'Resize'.i18n,
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);
showCupertinoModalPopup<void>( showCupertinoModalPopup<void>(
context: context, context: context,
builder: (context) { builder: (context) {
final _screenSize = MediaQuery.of(context).size; final screenSize = MediaQuery.of(context).size;
return ImageResizer( return ImageResizer(
onImageResize: (w, h) { onImageResize: (w, h) {
final res = getEmbedNode( final res = getEmbedNode(
controller, controller.selection.start); controller, controller.selection.start);
final attr = base.replaceStyleString( final attr = base.replaceStyleString(
getImageStyleString(controller), w, h); getImageStyleString(controller), w, h);
controller controller
..skipRequestKeyboard = true ..skipRequestKeyboard = true
..formatText( ..formatText(
res.offset, 1, StyleAttribute(attr)); res.offset, 1, StyleAttribute(attr));
}, },
imageWidth: _imageSize?.width, imageWidth: imageSize?.width,
imageHeight: _imageSize?.height, imageHeight: imageSize?.height,
maxWidth: _screenSize.width, maxWidth: screenSize.width,
maxHeight: _screenSize.height); maxHeight: screenSize.height);
}); });
}, },
); );
final copyOption = _SimpleDialogItem( final copyOption = _SimpleDialogItem(
icon: Icons.copy_all_outlined, icon: Icons.copy_all_outlined,
color: Colors.cyanAccent, color: Colors.cyanAccent,
text: 'Copy'.i18n, text: 'Copy'.i18n,
onPressed: () { onPressed: () {
final imageNode = final imageNode =
getEmbedNode(controller, controller.selection.start) getEmbedNode(controller, controller.selection.start)
.value; .value;
final imageUrl = imageNode.value.data; final imageUrl = imageNode.value.data;
controller.copiedImageUrl = controller.copiedImageUrl =
ImageUrl(imageUrl, getImageStyleString(controller)); ImageUrl(imageUrl, getImageStyleString(controller));
Navigator.pop(context); Navigator.pop(context);
}, },
); );
final removeOption = _SimpleDialogItem( final removeOption = _SimpleDialogItem(
icon: Icons.delete_forever_outlined, icon: Icons.delete_forever_outlined,
color: Colors.red.shade200, color: Colors.red.shade200,
text: 'Remove'.i18n, text: 'Remove'.i18n,
onPressed: () { onPressed: () async {
final offset = final navigator = Navigator.of(context);
getEmbedNode(controller, controller.selection.start) final offset =
.offset; getEmbedNode(controller, controller.selection.start)
controller.replaceText(offset, 1, '', .offset;
TextSelection.collapsed(offset: offset)); controller.replaceText(
Navigator.pop(context); offset,
}, 1,
); '',
return Padding( TextSelection.collapsed(offset: offset),
padding: const EdgeInsets.fromLTRB(50, 0, 50, 0), );
child: SimpleDialog( navigator.pop();
shape: const RoundedRectangleBorder( await afterRemoveImageFromEditor(File(imageUrl));
borderRadius: },
BorderRadius.all(Radius.circular(10))), );
children: [resizeOption, copyOption, removeOption]), return Padding(
); padding: const EdgeInsets.fromLTRB(50, 0, 50, 0),
}); child: SimpleDialog(
}, shape: const RoundedRectangleBorder(
child: image); borderRadius: BorderRadius.all(Radius.circular(10))),
children: [resizeOption, copyOption, removeOption]),
);
});
},
child: image,
);
} }
if (!readOnly || !base.isMobile() || isImageBase64(imageUrl)) { if (!readOnly || !base.isMobile() || isImageBase64(imageUrl)) {
@ -151,7 +160,11 @@ class ImageEmbedBuilder extends EmbedBuilder {
} }
// We provide option menu for mobile platform excluding base64 image // We provide option menu for mobile platform excluding base64 image
return _menuOptionsForReadonlyImage(context, imageUrl, image); return _menuOptionsForReadonlyImage(
context,
imageUrl,
image,
);
} }
} }
@ -271,29 +284,39 @@ Widget _menuOptionsForReadonlyImage(
text: 'Save'.i18n, text: 'Save'.i18n,
onPressed: () async { onPressed: () async {
imageUrl = appendFileExtensionToImageUrl(imageUrl); imageUrl = appendFileExtensionToImageUrl(imageUrl);
final messenger = ScaffoldMessenger.of(context);
Navigator.of(context).pop();
// Download image final saveImageResult = await saveImage(imageUrl);
final uri = Uri.parse(imageUrl); final imageSavedSuccessfully = saveImageResult.isSuccess;
final response = await http.get(uri);
if (response.statusCode != 200) { messenger.clearSnackBars();
throw Exception(
'failed to download image: ${response.statusCode}',
);
}
// Save image to a temporary path if (!imageSavedSuccessfully) {
final fileName = uri.pathSegments.isEmpty ? 'image.jpg' // TODO: Please translate this
: uri.pathSegments.last; messenger.showSnackBar(const SnackBar(
final imagePath = '${Directory.systemTemp.path}/menu-opt-$fileName'; content: Text(
final imageFile = File(imagePath); 'Error while saveing the image',
await imageFile.writeAsBytes(response.bodyBytes); )));
return;
}
// Save image to gallery var message = 'Saved'.i18n;
await Gal.putImage(imagePath); switch (saveImageResult.method) {
// TODO: Please translate this too
case SaveImageResultMethod.network:
message += ' using the network.';
break;
case SaveImageResultMethod.localStorage:
message += ' using the local storage.';
break;
}
ScaffoldMessenger.of(context) messenger.showSnackBar(
.showSnackBar(SnackBar(content: Text('Saved'.i18n))); SnackBar(
Navigator.pop(context); content: Text(message),
),
);
}, },
); );
final zoomOption = _SimpleDialogItem( final zoomOption = _SimpleDialogItem(

@ -44,3 +44,6 @@ class QuillFile {
final String path; final String path;
final Uint8List bytes; final Uint8List bytes;
} }
typedef ImageEmbedBuilderAfterRemoveImageFromEditor = Future<void> Function(
File imageFile);

@ -35,6 +35,12 @@ class LinkDialogState extends State<LinkDialog> {
super.initState(); super.initState();
_link = widget.link ?? ''; _link = widget.link ?? '';
_controller = TextEditingController(text: _link); _controller = TextEditingController(text: _link);
// TODO: Consider replace the default Regex with this one
// Since that is not the reason I sent the changes then I will not edit it
// final defaultLinkNonSecureRegExp = RegExp(r'https?://.*?\.(?:png|jpe?g|gif|bmp|webp|tiff?)'); // Not secure
// final defaultLinkRegExp = RegExp(r'https://.*?\.(?:png|jpe?g|gif|bmp|webp|tiff?)'); // Secure
// _linkRegExp = widget.linkRegExp ?? defaultLinkRegExp;
_linkRegExp = widget.linkRegExp ?? AutoFormatMultipleLinksRule.linkRegExp; _linkRegExp = widget.linkRegExp ?? AutoFormatMultipleLinksRule.linkRegExp;
} }

@ -1,5 +1,84 @@
import 'package:string_validator/string_validator.dart'; import 'dart:io';
import 'package:flutter/foundation.dart' show Uint8List;
import 'package:http/http.dart' as http;
import 'package:image_gallery_saver/image_gallery_saver.dart';
// I would like to orgnize the project structure and the code more
// but here I don't want to change too much since that is a community project
RegExp _base64 = RegExp(
r'^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{4})$',
);
bool isBase64(String str) {
return _base64.hasMatch(str);
}
bool isImageBase64(String imageUrl) { bool isImageBase64(String imageUrl) {
return !imageUrl.startsWith('http') && isBase64(imageUrl); return !imageUrl.startsWith('http') && isBase64(imageUrl);
} }
enum SaveImageResultMethod { network, localStorage }
class _SaveImageResult {
const _SaveImageResult({required this.isSuccess, required this.method});
final bool isSuccess;
final SaveImageResultMethod method;
}
Future<_SaveImageResult> saveImage(String imageUrl) async {
final imageFile = File(imageUrl);
final imageExistsLocally = await imageFile.exists();
if (!imageExistsLocally) {
final success = await _saveNetworkImageToLocal(imageUrl);
return _SaveImageResult(
isSuccess: success,
method: SaveImageResultMethod.network,
);
}
final success = await _saveImageLocally(imageFile);
return _SaveImageResult(
isSuccess: success,
method: SaveImageResultMethod.localStorage,
);
}
Future<bool> _saveNetworkImageToLocal(String imageUrl) async {
try {
final response = await http.get(
Uri.parse(imageUrl),
);
if (response.statusCode != 200) {
return false;
}
final imageBytes = response.bodyBytes;
final result = await ImageGallerySaver.saveImage(
Uint8List.fromList(imageBytes),
);
return result['isSuccess'];
} catch (e) {
return false;
}
}
Future<Uint8List> _convertFileToUint8List(File file) async {
try {
final uint8list = await file.readAsBytes();
return uint8list;
} catch (e) {
return Uint8List(0);
}
}
Future<bool> _saveImageLocally(File imageFile) async {
try {
final imageBytes = await _convertFileToUint8List(imageFile);
final result = await ImageGallerySaver.saveImage(imageBytes);
return result['isSuccess'];
} catch (e) {
return false;
}
}

@ -20,11 +20,36 @@ export 'embeds/toolbar/video_button.dart';
export 'embeds/utils.dart'; export 'embeds/utils.dart';
class FlutterQuillEmbeds { class FlutterQuillEmbeds {
/// Returns a list of embed builders for Quill editors.
///
/// [onVideoInit] is called when a video is initialized.
/// [onRemoveImage] is called when an image is removed from the editor.
/// By default, [onRemoveImage] deletes the cached image if it still exists.
/// If you want to customize
/// the behavior, pass your own function that handles the removal.
///
/// Example of [onRemoveImage] customization:
/// ```dart
/// onRemoveImage: (imageFile) async {
/// // Your custom logic here
/// // or leave it empty to do nothing
/// }
/// ```
static List<EmbedBuilder> builders({ static List<EmbedBuilder> builders({
void Function(GlobalKey videoContainerKey)? onVideoInit, void Function(GlobalKey videoContainerKey)? onVideoInit,
ImageEmbedBuilderAfterRemoveImageFromEditor? afterRemoveImageFromEditor,
}) => }) =>
[ [
ImageEmbedBuilder(), ImageEmbedBuilder(
afterRemoveImageFromEditor: afterRemoveImageFromEditor ??
(imageFile) async {
// TODO: Please change this default code
final fileExists = await imageFile.exists();
if (fileExists) {
await imageFile.delete();
}
},
),
VideoEmbedBuilder(onVideoInit: onVideoInit), VideoEmbedBuilder(onVideoInit: onVideoInit),
FormulaEmbedBuilder(), FormulaEmbedBuilder(),
]; ];
@ -80,7 +105,7 @@ class FlutterQuillEmbeds {
iconTheme: iconTheme, iconTheme: iconTheme,
dialogTheme: dialogTheme, dialogTheme: dialogTheme,
linkRegExp: videoLinkRegExp, linkRegExp: videoLinkRegExp,
), ),
if ((onImagePickCallback != null || onVideoPickCallback != null) && if ((onImagePickCallback != null || onVideoPickCallback != null) &&
showCameraButton) showCameraButton)
(controller, toolbarIconSize, iconTheme, dialogTheme) => CameraButton( (controller, toolbarIconSize, iconTheme, dialogTheme) => CameraButton(

@ -12,18 +12,21 @@ dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
flutter_quill: ^7.2.19 flutter_quill: ^7.4.7
http: ^1.1.0 http: ^1.1.0
image_picker: ">=0.8.5 <2.0.0" image_picker: ">=1.0.4"
photo_view: ^0.14.0 photo_view: ^0.14.0
video_player: ^2.7.0 video_player: ^2.7.2
youtube_player_flutter: ^8.1.1 youtube_player_flutter: ^8.1.2
gal: ^2.1.1 # gallery_saver: ^2.1.1
math_keyboard: ">=0.1.8 <0.3.0" math_keyboard: ">=0.2.1"
string_validator: ^1.0.0 # string_validator: ^1.0.0
universal_html: ^2.2.1 universal_html: ^2.2.4
url_launcher: ^6.1.9 # url_launcher: ^6.1.14
# dio: ^5.3.3
image_gallery_saver: ^2.0.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

Loading…
Cancel
Save