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({
this.afterRemoveImageFromEditor,
this.shouldRemoveImageFromEditor,
this.forceUseMobileOptionMenu = false,
});
final ImageEmbedBuilderAfterRemoveImageFromEditor? afterRemoveImageFromEditor;
final ImageEmbedBuilderShouldRemoveImageFromEditor?
shouldRemoveImageFromEditor;
final bool forceUseMobileOptionMenu;
@override
String get key => BlockEmbed.imageType;
@ -79,7 +81,7 @@ class ImageEmbedBuilder extends EmbedBuilder {
imageSize = OptionalSize((image as Image).width, image.height);
}
if (!readOnly && base.isMobile()) {
if (!readOnly && (base.isMobile() || forceUseMobileOptionMenu)) {
return GestureDetector(
onTap: () {
showDialog(
@ -169,7 +171,11 @@ class ImageEmbedBuilder extends EmbedBuilder {
child: SimpleDialog(
shape: const RoundedRectangleBorder(
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;
}
@ -246,7 +261,7 @@ class VideoEmbedBuilder extends EmbedBuilder {
assert(!kIsWeb, 'Please provide video EmbedBuilder for Web');
final videoUrl = node.value.data;
if (videoUrl.contains('youtube.com') || videoUrl.contains('youtu.be')) {
if (isYouTubeUrl(videoUrl)) {
return YoutubeVideoApp(
videoUrl: videoUrl, context: context, readOnly: readOnly);
}

@ -15,8 +15,28 @@ bool isBase64(String 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) {
return !imageUrl.startsWith('http') && isBase64(imageUrl);
return !isHttpBasedUrl(imageUrl) && isBase64(imageUrl);
}
enum SaveImageResultMethod { network, localStorage }

@ -1,6 +1,7 @@
library flutter_quill_extensions;
import 'package:flutter/material.dart';
import 'package:flutter_quill/extensions.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'embeds/builders.dart';
@ -22,17 +23,28 @@ export 'embeds/utils.dart';
class FlutterQuillEmbeds {
/// 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.
/// 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
/// is removed from the editor.
/// By default, [afterRemoveImageFromEditor] deletes the cached
/// image if it still exists.
/// If you want to customize the behavior, pass your own function
/// that handles the removal.
/// [afterRemoveImageFromEditor] is called when an image is
/// removed from the editor.
/// By default, [afterRemoveImageFromEditor] deletes the
/// temporary image file if
/// the platform is mobile and if it still exists. You
/// can customize this behavior
/// by passing your own function that handles the removal process.
///
/// Example of [afterRemoveImageFromEditor] customization:
/// ```dart
@ -42,11 +54,10 @@ class FlutterQuillEmbeds {
/// }
/// ```
///
/// [shouldRemoveImageFromEditor] is called when the user
/// attempts to remove an image
/// from the editor. It allows you to control whether the image
/// should be removed
/// based on your custom logic.
/// [shouldRemoveImageFromEditor] is a callback
/// function that is invoked when the
/// user attempts to remove an image from the editor. It allows you to control
/// whether the image should be removed based on your custom logic.
///
/// Example of [shouldRemoveImageFromEditor] customization:
/// ```dart
@ -56,8 +67,8 @@ class FlutterQuillEmbeds {
/// context: context,
/// options: const YesOrCancelDialogOptions(
/// title: 'Deleting an image',
/// message: 'Are you sure you want to delete this image
/// from the editor?',
/// message: 'Are you sure you want' ' to delete this
/// image from the editor?',
/// ),
/// );
///
@ -65,14 +76,69 @@ class FlutterQuillEmbeds {
/// 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({
void Function(GlobalKey videoContainerKey)? onVideoInit,
ImageEmbedBuilderAfterRemoveImageFromEditor? afterRemoveImageFromEditor,
ImageEmbedBuilderShouldRemoveImageFromEditor? shouldRemoveImageFromEditor,
bool forceUseMobileOptionMenuForImageClick = false,
}) =>
[
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,
),
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