Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New features and improvements #1419

Merged
merged 16 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions example/macos/Flutter/GeneratedPluginRegistrant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import gal
import pasteboard
import path_provider_foundation
import url_launcher_macos
import video_player_avfoundation

func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
Expand All @@ -19,4 +20,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
}
51 changes: 36 additions & 15 deletions flutter_quill_extensions/lib/embeds/builders.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ import 'widgets/video_app.dart';
import 'widgets/youtube_video_app.dart';

class ImageEmbedBuilder extends EmbedBuilder {
const ImageEmbedBuilder({
ImageEmbedBuilder({
this.onImageRemovedCallback,
this.shouldRemoveImageCallback,
this.forceUseMobileOptionMenu = false,
});

final ImageEmbedBuilderOnRemovedCallback? onImageRemovedCallback;
final ImageEmbedBuilderWillRemoveCallback? shouldRemoveImageCallback;
final ImageEmbedBuilderWillRemoveCallback? onImageRemovedCallback;
EchoEllet marked this conversation as resolved.
Show resolved Hide resolved
final ImageEmbedBuilderOnRemovedCallback? shouldRemoveImageCallback;
final bool forceUseMobileOptionMenu;

@override
String get key => BlockEmbed.imageType;
Expand Down Expand Up @@ -79,7 +80,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(
Expand Down Expand Up @@ -136,12 +137,18 @@ class ImageEmbedBuilder extends EmbedBuilder {

final imageFile = File(imageUrl);

// Call the remove check callback if set
EchoEllet marked this conversation as resolved.
Show resolved Hide resolved
if (await shouldRemoveImageCallback?.call(imageFile) ==
false) {
return;
final shouldRemoveImageEvent = shouldRemoveImageCallback;

var shouldRemoveImage = true;
if (shouldRemoveImageEvent != null) {
shouldRemoveImage = await shouldRemoveImageEvent(
imageFile,
);
}

if (!shouldRemoveImage) {
return;
}
final offset = getEmbedNode(
controller,
controller.selection.start,
Expand All @@ -152,25 +159,39 @@ class ImageEmbedBuilder extends EmbedBuilder {
'',
TextSelection.collapsed(offset: offset),
);

EchoEllet marked this conversation as resolved.
Show resolved Hide resolved
// Call the post remove callback if set
await onImageRemovedCallback?.call(imageFile);
final afterRemoveImageEvent = onImageRemovedCallback;
if (afterRemoveImageEvent != null) {
await afterRemoveImageEvent(imageFile);
}
},
);
return Padding(
padding: const EdgeInsets.fromLTRB(50, 0, 50, 0),
child: SimpleDialog(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10))),
children: [resizeOption, copyOption, removeOption]),
children: [
if (base.isMobile()) resizeOption,
copyOption,
removeOption,
]),
);
});
},
child: image,
);
}

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;
}

Expand Down Expand Up @@ -239,7 +260,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);
}
Expand Down
4 changes: 2 additions & 2 deletions flutter_quill_extensions/lib/embeds/embed_types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ class QuillFile {
final Uint8List bytes;
}

typedef ImageEmbedBuilderWillRemoveCallback = Future<bool> Function(
EchoEllet marked this conversation as resolved.
Show resolved Hide resolved
typedef ImageEmbedBuilderWillRemoveCallback = Future<void> Function(
File imageFile,
);

typedef ImageEmbedBuilderOnRemovedCallback = Future<void> Function(
typedef ImageEmbedBuilderOnRemovedCallback = Future<bool> Function(
File imageFile,
);
22 changes: 21 additions & 1 deletion flutter_quill_extensions/lib/embeds/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
110 changes: 89 additions & 21 deletions flutter_quill_extensions/lib/flutter_quill_extensions.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -22,29 +23,41 @@ 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.
///
/// [onImageRemovedCallback] is called when an image
/// is removed from the editor. This can be used to
/// delete the image from storage, for example:
/// [onImageRemovedCallback] is called when an image is
/// removed from the editor.
/// By default, [onImageRemovedCallback] 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 [onImageRemovedCallback] customization:
/// ```dart
/// (imageFile) async {
/// final fileExists = await imageFile.exists();
/// if (fileExists) {
/// await imageFile.delete();
/// }
/// },
/// afterRemoveImageFromEditor: (imageFile) async {
/// // Your custom logic here
/// // or leave it empty to do nothing
/// }
/// ```
///
/// [shouldRemoveImageCallback] 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.
/// [shouldRemoveImageCallback] 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 [shouldRemoveImageCallback] customization:
/// ```dart
Expand All @@ -54,23 +67,78 @@ 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
EchoEllet marked this conversation as resolved.
Show resolved Hide resolved
/// image from the editor?',
/// ),
/// );
///
/// // Return `true` to allow image removal if the user confirms, otherwise `false`
/// 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,
ImageEmbedBuilderOnRemovedCallback? onImageRemovedCallback,
EchoEllet marked this conversation as resolved.
Show resolved Hide resolved
ImageEmbedBuilderWillRemoveCallback? shouldRemoveImageCallback,
ImageEmbedBuilderWillRemoveCallback? onImageRemovedCallback,
ImageEmbedBuilderOnRemovedCallback? shouldRemoveImageCallback,
bool forceUseMobileOptionMenuForImageClick = false,
}) =>
[
ImageEmbedBuilder(
onImageRemovedCallback: onImageRemovedCallback,
forceUseMobileOptionMenu: forceUseMobileOptionMenuForImageClick,
onImageRemovedCallback: onImageRemovedCallback ??
(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();
}
},
shouldRemoveImageCallback: shouldRemoveImageCallback,
),
VideoEmbedBuilder(onVideoInit: onVideoInit),
Expand Down
Loading