Add new features

pull/1419/head
Ahmed Hnewa 2 years ago
parent 0b49baf489
commit 8fc5333543
No known key found for this signature in database
GPG Key ID: C488CC70BBCEF0D1
  1. 23
      flutter_quill_extensions/lib/embeds/builders.dart
  2. 22
      flutter_quill_extensions/lib/embeds/utils.dart
  3. 98
      flutter_quill_extensions/lib/flutter_quill_extensions.dart
  4. 251
      flutter_quill_extensions/lib/utils/quill_utils.dart

@ -23,10 +23,12 @@ class ImageEmbedBuilder extends EmbedBuilder {
ImageEmbedBuilder({ ImageEmbedBuilder({
this.afterRemoveImageFromEditor, this.afterRemoveImageFromEditor,
this.shouldRemoveImageFromEditor, this.shouldRemoveImageFromEditor,
this.forceUseMobileOptionMenu = false,
}); });
final ImageEmbedBuilderAfterRemoveImageFromEditor? afterRemoveImageFromEditor; final ImageEmbedBuilderAfterRemoveImageFromEditor? afterRemoveImageFromEditor;
final ImageEmbedBuilderShouldRemoveImageFromEditor? final ImageEmbedBuilderShouldRemoveImageFromEditor?
shouldRemoveImageFromEditor; shouldRemoveImageFromEditor;
final bool forceUseMobileOptionMenu;
@override @override
String get key => BlockEmbed.imageType; String get key => BlockEmbed.imageType;
@ -79,7 +81,7 @@ class ImageEmbedBuilder extends EmbedBuilder {
imageSize = OptionalSize((image as Image).width, image.height); imageSize = OptionalSize((image as Image).width, image.height);
} }
if (!readOnly && base.isMobile()) { if (!readOnly && (base.isMobile() || forceUseMobileOptionMenu)) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
showDialog( showDialog(
@ -169,7 +171,11 @@ class ImageEmbedBuilder extends EmbedBuilder {
child: SimpleDialog( child: SimpleDialog(
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10))), borderRadius: BorderRadius.all(Radius.circular(10))),
children: [resizeOption, copyOption, removeOption]), children: [
if (base.isMobile()) resizeOption,
copyOption,
removeOption,
]),
); );
}); });
}, },
@ -177,7 +183,16 @@ class ImageEmbedBuilder extends EmbedBuilder {
); );
} }
if (!readOnly || !base.isMobile() || isImageBase64(imageUrl)) { if (!readOnly || isImageBase64(imageUrl)) {
// To enforce using it on the web, desktop and other platforms
// and that is up to the developer
if (!base.isMobile() && forceUseMobileOptionMenu) {
return _menuOptionsForReadonlyImage(
context,
imageUrl,
image,
);
}
return image; return image;
} }
@ -246,7 +261,7 @@ class VideoEmbedBuilder extends EmbedBuilder {
assert(!kIsWeb, 'Please provide video EmbedBuilder for Web'); assert(!kIsWeb, 'Please provide video EmbedBuilder for Web');
final videoUrl = node.value.data; final videoUrl = node.value.data;
if (videoUrl.contains('youtube.com') || videoUrl.contains('youtu.be')) { if (isYouTubeUrl(videoUrl)) {
return YoutubeVideoApp( return YoutubeVideoApp(
videoUrl: videoUrl, context: context, readOnly: readOnly); videoUrl: videoUrl, context: context, readOnly: readOnly);
} }

@ -15,8 +15,28 @@ bool isBase64(String str) {
return _base64.hasMatch(str); return _base64.hasMatch(str);
} }
bool isHttpBasedUrl(String url) {
try {
final uri = Uri.parse(url.trim());
return uri.isScheme('HTTP') || uri.isScheme('HTTPS');
} catch (_) {
return false;
}
}
bool isYouTubeUrl(String videoUrl) {
try {
final uri = Uri.parse(videoUrl);
return uri.host == 'www.youtube.com' ||
uri.host == 'youtube.com' ||
uri.host == 'youtu.be';
} catch (_) {
return false;
}
}
bool isImageBase64(String imageUrl) { bool isImageBase64(String imageUrl) {
return !imageUrl.startsWith('http') && isBase64(imageUrl); return !isHttpBasedUrl(imageUrl) && isBase64(imageUrl);
} }
enum SaveImageResultMethod { network, localStorage } enum SaveImageResultMethod { network, localStorage }

@ -1,6 +1,7 @@
library flutter_quill_extensions; library flutter_quill_extensions;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_quill/extensions.dart';
import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill/flutter_quill.dart';
import 'embeds/builders.dart'; import 'embeds/builders.dart';
@ -22,17 +23,28 @@ export 'embeds/utils.dart';
class FlutterQuillEmbeds { class FlutterQuillEmbeds {
/// Returns a list of embed builders for QuillEditor. /// Returns a list of embed builders for QuillEditor.
/// ///
/// This method provides a collection of embed builders to enhance the
/// functionality
/// of a QuillEditor. It offers customization options for
/// handling various types of
/// embedded content, such as images, videos, and formulas.
///
/// **Note:** This method is not intended for web usage. /// **Note:** This method is not intended for web usage.
/// For web-specific embeds, use [webBuilders]. /// For web-specific embeds,
/// use [webBuilders].
/// ///
/// [onVideoInit] is called when a video is initialized. /// [onVideoInit] is a callback function that gets triggered when
/// a video is initialized.
/// You can use this to perform actions or setup configurations related
/// to video embedding.
/// ///
/// [afterRemoveImageFromEditor] is called when an image /// [afterRemoveImageFromEditor] is called when an image is
/// is removed from the editor. /// removed from the editor.
/// By default, [afterRemoveImageFromEditor] deletes the cached /// By default, [afterRemoveImageFromEditor] deletes the
/// image if it still exists. /// temporary image file if
/// If you want to customize the behavior, pass your own function /// the platform is mobile and if it still exists. You
/// that handles the removal. /// can customize this behavior
/// by passing your own function that handles the removal process.
/// ///
/// Example of [afterRemoveImageFromEditor] customization: /// Example of [afterRemoveImageFromEditor] customization:
/// ```dart /// ```dart
@ -42,11 +54,10 @@ class FlutterQuillEmbeds {
/// } /// }
/// ``` /// ```
/// ///
/// [shouldRemoveImageFromEditor] is called when the user /// [shouldRemoveImageFromEditor] is a callback
/// attempts to remove an image /// function that is invoked when the
/// from the editor. It allows you to control whether the image /// user attempts to remove an image from the editor. It allows you to control
/// should be removed /// whether the image should be removed based on your custom logic.
/// based on your custom logic.
/// ///
/// Example of [shouldRemoveImageFromEditor] customization: /// Example of [shouldRemoveImageFromEditor] customization:
/// ```dart /// ```dart
@ -56,8 +67,8 @@ class FlutterQuillEmbeds {
/// context: context, /// context: context,
/// options: const YesOrCancelDialogOptions( /// options: const YesOrCancelDialogOptions(
/// title: 'Deleting an image', /// title: 'Deleting an image',
/// message: 'Are you sure you want to delete this image /// message: 'Are you sure you want' ' to delete this
/// from the editor?', /// image from the editor?',
/// ), /// ),
/// ); /// );
/// ///
@ -65,14 +76,69 @@ class FlutterQuillEmbeds {
/// return isShouldRemove; /// return isShouldRemove;
/// } /// }
/// ``` /// ```
///
/// [forceUseMobileOptionMenuForImageClick] is a boolean
/// flag that, when set to `true`,
/// enforces the use of the mobile-specific option menu for image clicks in
/// other platforms like web and desktop, this option doesn't affect mobile.
/// This option
/// can be used to override the default behavior based on the platform.
///
/// The method returns a list of [EmbedBuilder] objects that can be used with
/// QuillEditor
/// to enable embedded content features like images, videos, and formulas.
///
/// Example usage:
/// ```dart
/// final embedBuilders = QuillEmbedBuilders.builders(
/// onVideoInit: (videoContainerKey) {
/// // Custom video initialization logic
/// },
/// // Customize other callback functions as needed
/// );
///
/// final quillEditor = QuillEditor(
/// // Other editor configurations
/// embedBuilders: embedBuilders,
/// );
/// ```
static List<EmbedBuilder> builders({ static List<EmbedBuilder> builders({
void Function(GlobalKey videoContainerKey)? onVideoInit, void Function(GlobalKey videoContainerKey)? onVideoInit,
ImageEmbedBuilderAfterRemoveImageFromEditor? afterRemoveImageFromEditor, ImageEmbedBuilderAfterRemoveImageFromEditor? afterRemoveImageFromEditor,
ImageEmbedBuilderShouldRemoveImageFromEditor? shouldRemoveImageFromEditor, ImageEmbedBuilderShouldRemoveImageFromEditor? shouldRemoveImageFromEditor,
bool forceUseMobileOptionMenuForImageClick = false,
}) => }) =>
[ [
ImageEmbedBuilder( ImageEmbedBuilder(
afterRemoveImageFromEditor: afterRemoveImageFromEditor, forceUseMobileOptionMenu: forceUseMobileOptionMenuForImageClick,
afterRemoveImageFromEditor: afterRemoveImageFromEditor ??
(imageFile) async {
final mobile = isMobile();
// If the platform is not mobile, return void;
// Since the mobile OS gives us a copy of the image
// Note: We should remove the image on Flutter web
// since the behavior is similar to how it is on mobile,
// but since this builder is not for web, we will ignore it
if (!mobile) {
return;
}
// On mobile OS (Android, iOS), the system will not give us
// direct access to the image; instead,
// it will give us the image
// in the temp directory of the application. So, we want to
// remove it when we no longer need it.
// but on desktop we don't want to touch user files
// especially on macOS, where we can't even delete it without
// permission
final isFileExists = await imageFile.exists();
if (isFileExists) {
await imageFile.delete();
}
},
shouldRemoveImageFromEditor: shouldRemoveImageFromEditor, shouldRemoveImageFromEditor: shouldRemoveImageFromEditor,
), ),
VideoEmbedBuilder(onVideoInit: onVideoInit), VideoEmbedBuilder(onVideoInit: onVideoInit),

@ -0,0 +1,251 @@
import 'dart:io' show Directory, File, Platform;
import 'package:flutter/foundation.dart';
import 'package:flutter_quill/flutter_quill.dart' as quill;
import 'package:path/path.dart' as path;
import '../flutter_quill_extensions.dart';
class QuillUtilities {
const QuillUtilities._();
/// Saves a list of images to a specified directory.
///
/// This function is designed to work efficiently on
/// mobile platforms, but it can also be used on other platforms.
///
/// When you have a list of cached image paths
/// from a Quill document and you want to save them,
/// you can use this function.
/// It takes a list of image paths and copies each image to the specified
/// directory. If the image
/// path does not exist, it returns an
/// empty string for that item.
///
/// Make sure that the image paths provided in the [images]
/// list exist, and handle the cases where images are not found accordingly.
///
/// [images]: List of image paths to be saved.
/// [deleteThePreviousImages]: Indicates whether to delete the
/// original cached images after copying.
/// [saveDirectory]: The directory where the images will be saved.
///
/// Returns a list of paths to the newly saved images.
/// For images that do not exist, their paths are returned as empty strings.
///
/// Example usage:
/// ```dart
/// final documentsDir = await getApplicationDocumentsDirectory();
/// final savedImagePaths = await saveImagesToDirectory(
/// images: cachedImagePaths,
/// deleteThePreviousImages: true,
/// saveDirectory: documentsDir,
/// );
/// ```
static Future<List<String>> saveImagesToDirectory({
required Iterable<String> images,
required deleteThePreviousImages,
required Directory saveDirectory,
}) async {
final newImagesFutures = images.map((cachedImagePath) async {
final previousImageFile = File(cachedImagePath);
final isPreviousImageFileExists = await previousImageFile.exists();
if (!isPreviousImageFileExists) {
return '';
}
final newImageFileExtensionWithDot = path.extension(cachedImagePath);
final dateTimeAsString = DateTime.now().toIso8601String();
// TODO: You might want to make it easier for the developer to change
// the newImageFileName, but he can rename it anyway
final newImageFileName =
'quill-image-$dateTimeAsString$newImageFileExtensionWithDot';
final newImagePath = path.join(saveDirectory.path, newImageFileName);
final newImageFile = await previousImageFile.copy(newImagePath);
if (deleteThePreviousImages) {
await previousImageFile.delete();
}
return newImageFile.path;
});
// Await for the saving process for each image
final newImages = await Future.wait(newImagesFutures);
return newImages;
}
/// Deletes all local images referenced in a Quill document.
///
/// This function removes local images from the
/// file system that are referenced in the provided [document].
///
/// [document]: The Quill document from which images will be deleted.
///
/// Throws an [Exception] if any errors occur during the deletion process.
///
/// Example usage:
/// ```dart
/// try {
/// await deleteAllLocalImagesOfDocument(myQuillDocument);
/// } catch (e) {
/// print('Error deleting local images: $e');
/// }
/// ```
static Future<void> deleteAllLocalImagesOfDocument(
quill.Document document,
) async {
final imagesPaths = getImagesPathsFromDocument(
document,
onlyLocalImages: true,
);
for (final image in imagesPaths) {
final imageFile = File(image);
final fileExists = await imageFile.exists();
if (!fileExists) {
return;
}
final deletedFile = await imageFile.delete();
final deletedFileStillExists = await deletedFile.exists();
if (deletedFileStillExists) {
throw Exception(
'We have successfully deleted the file and it is still exists!!',
);
}
}
}
/// Retrieves paths to images embedded in a Quill document.
///
/// This function parses the [document] and returns a list of image paths.
///
/// [document]: The Quill document from which image paths will be retrieved.
/// [onlyLocalImages]: If `true`,
/// only local (non-web) image paths will be included.
///
/// Returns an iterable of image paths.
///
/// Example usage:
/// ```dart
/// final quillDocument = _controller.document;
/// final imagePaths
/// = getImagesPathsFromDocument(quillDocument, onlyLocalImages: true);
/// print('Image paths: $imagePaths');
/// ```
///
/// Note: This function assumes that images are
/// embedded as block embeds in the Quill document.
static Iterable<String> getImagesPathsFromDocument(
quill.Document document, {
required bool onlyLocalImages,
}) {
final images = document.root.children
.whereType<quill.Line>()
.where((node) {
if (node.isEmpty) {
return false;
}
final firstNode = node.children.first;
if (firstNode is! quill.Embed) {
return false;
}
if (firstNode.value.type != quill.BlockEmbed.imageType) {
return false;
}
final imageSource = firstNode.value.data;
if (imageSource is! String) {
return false;
}
if (onlyLocalImages && isHttpBasedUrl(imageSource)) {
return false;
}
return imageSource.trim().isNotEmpty;
})
.toList()
.map((e) => (e.children.first as quill.Embed).value.data as String);
return images;
}
/// Determines if an image file is cached based on the platform.
///
/// On mobile platforms (Android and iOS), images are typically
/// cached in temporary directories.
/// This function helps identify whether the given image file path
/// is a cached path on supported platforms.
///
/// [imagePath] is the path of the image file to check for caching.
///
/// Returns `true` if the image is cached, `false` otherwise.
/// On other platforms it will always return false
static bool isImageCached(String imagePath) {
// Determine if the image path is a cached path based on platform
if (kIsWeb) {
// For now this will not work for web
return false;
}
if (Platform.isAndroid) {
return imagePath.contains('cache');
}
if (Platform.isIOS) {
// Don't use isAppleOS() since macOS has different behavior
return imagePath.contains('tmp');
}
// On other platforms like desktop
// The image is not cached and we will get a direct
// access to the image
return false;
}
/// Retrieves cached image paths from a Quill document,
/// primarily for mobile platforms.
///
/// This function scans a Quill document to identify
/// and return paths to locally cached images.
/// It is specifically designed for mobile
/// operating systems (Android and iOS).
///
/// [document] is the Quill document from which to extract image paths.
///
/// [replaceUnexistentImagesWith] is an optional parameter.
/// If provided, it replaces non-existent image paths
/// with the specified value. If not provided, non-existent
/// image paths are removed from the result.
///
/// Returns a list of cached image paths found in the document.
/// On non-mobile platforms, this function returns an empty list.
static Future<Iterable<String>> getCachedImagePathsFromDocument(
quill.Document document, {
String? replaceUnexistentImagesWith,
}) async {
final imagePaths = getImagesPathsFromDocument(
document,
onlyLocalImages: true,
);
// We don't want the not cached images to be saved again for example.
final cachesImagePaths = imagePaths.where((imagePath) {
final isCurrentImageCached = isImageCached(imagePath);
return isCurrentImageCached;
}).toList();
// Remove all the images that doesn't exists
for (final imagePath in cachesImagePaths) {
final file = File(imagePath);
final exists = await file.exists();
if (!exists) {
final index = cachesImagePaths.indexOf(imagePath);
if (index == -1) {
continue;
}
cachesImagePaths.removeAt(index);
if (replaceUnexistentImagesWith != null) {
cachesImagePaths.insert(
index,
replaceUnexistentImagesWith,
);
}
}
}
return cachesImagePaths;
}
}
Loading…
Cancel
Save