diff --git a/example/lib/screens/quill/my_quill_editor.dart b/example/lib/screens/quill/my_quill_editor.dart index be3c48ef..254057d7 100644 --- a/example/lib/screens/quill/my_quill_editor.dart +++ b/example/lib/screens/quill/my_quill_editor.dart @@ -4,11 +4,13 @@ import 'package:cached_network_image/cached_network_image.dart' show CachedNetworkImageProvider; import 'package:desktop_drop/desktop_drop.dart' show DropTarget; import 'package:flutter/material.dart'; -import 'package:flutter_quill/extensions.dart' show isAndroid, isIOS, isWeb; +import 'package:flutter_quill/extensions.dart' + show isAndroid, isDesktop, isIOS, isWeb; import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill_extensions/embeds/widgets/image.dart' show getImageProviderByImageSource, imageFileExtensions; import 'package:flutter_quill_extensions/flutter_quill_extensions.dart'; +import 'package:flutter_quill_extensions/models/config/video/editor/youtube_video_support_mode.dart'; import 'package:path/path.dart' as path; import '../../extensions/scaffold_messenger.dart'; @@ -128,6 +130,13 @@ class MyQuillEditor extends StatelessWidget { ); }, ), + videoEmbedConfigurations: QuillEditorVideoEmbedConfigurations( + // Loading YouTube videos on Desktop is not supported yet + // when using iframe platform view + youtubeVideoSupportMode: isDesktop(supportWeb: false) + ? YoutubeVideoSupportMode.customPlayerWithDownloadUrl + : YoutubeVideoSupportMode.iframeView, + ), )), TimeStampEmbedBuilderWidget(), ], diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 06bfbf14..c89c5374 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -19,7 +19,7 @@ dependencies: flutter_quill_test: ^9.3.4 quill_html_converter: ^9.3.4 quill_pdf_converter: ^9.3.4 - # Normal packages + # Dart Packages path: ^1.8.3 equatable: ^2.0.5 cross_file: ^0.3.4 diff --git a/flutter_quill_extensions/lib/embeds/video/editor/video_embed.dart b/flutter_quill_extensions/lib/embeds/video/editor/video_embed.dart index 45c37f2a..b201e162 100644 --- a/flutter_quill_extensions/lib/embeds/video/editor/video_embed.dart +++ b/flutter_quill_extensions/lib/embeds/video/editor/video_embed.dart @@ -37,6 +37,7 @@ class QuillEditorVideoEmbedBuilder extends EmbedBuilder { return YoutubeVideoApp( videoUrl: videoUrl, readOnly: readOnly, + youtubeVideoSupportMode: configurations.youtubeVideoSupportMode, ); } final ((elementSize), margin, alignment) = getElementAttributes( @@ -53,7 +54,6 @@ class QuillEditorVideoEmbedBuilder extends EmbedBuilder { alignment: alignment, child: VideoApp( videoUrl: videoUrl, - context: context, readOnly: readOnly, onVideoInit: configurations.onVideoInit, ), diff --git a/flutter_quill_extensions/lib/embeds/video/toolbar/video_button.dart b/flutter_quill_extensions/lib/embeds/video/toolbar/video_button.dart index 51bf2b95..409cb718 100644 --- a/flutter_quill_extensions/lib/embeds/video/toolbar/video_button.dart +++ b/flutter_quill_extensions/lib/embeds/video/toolbar/video_button.dart @@ -9,6 +9,8 @@ import '../../others/image_video_utils.dart'; import '../video.dart'; import 'select_video_source.dart'; +// TODO: Add custom callback to validate the video link input + class QuillToolbarVideoButton extends StatelessWidget { const QuillToolbarVideoButton({ required this.controller, @@ -72,11 +74,6 @@ class QuillToolbarVideoButton extends StatelessWidget { final childBuilder = options.childBuilder ?? baseButtonExtraOptions(context)?.childBuilder; - // final iconColor = - // iconTheme?.iconUnselectedFillColor ?? theme.iconTheme.color; - // final iconFillColor = iconTheme?.iconUnselectedFillColor ?? - // (options.fillColor ?? theme.canvasColor); - if (childBuilder != null) { return childBuilder( QuillToolbarVideoButtonOptions( @@ -150,18 +147,6 @@ class QuillToolbarVideoButton extends StatelessWidget { .onVideoInsertCallback(videoUrl, controller); await options.videoConfigurations.onVideoInsertedCallback?.call(videoUrl); } - - // if (options.onVideoPickCallback != null) { - // final selector = options.mediaPickSettingSelector ?? - // ImageVideoUtils.selectMediaPickSetting; - // final source = await selector(context); - // if (source != null) { - // if (source == MediaPickSetting.gallery) { - // } else { - // await _typeLink(context); - // } - // } - // } else {} } Future _typeLink(BuildContext context) async { @@ -176,13 +161,4 @@ class QuillToolbarVideoButton extends StatelessWidget { ); return value; } - - // void _linkSubmitted(String? value) { - // if (value != null && value.isNotEmpty) { - // final index = controller.selection.baseOffset; - // final length = controller.selection.extentOffset - index; - - // controller.replaceText(index, length, BlockEmbed.video(value), null); - // } - // } } diff --git a/flutter_quill_extensions/lib/embeds/widgets/video_app.dart b/flutter_quill_extensions/lib/embeds/widgets/video_app.dart index 334f64ca..f6dc64fa 100644 --- a/flutter_quill_extensions/lib/embeds/widgets/video_app.dart +++ b/flutter_quill_extensions/lib/embeds/widgets/video_app.dart @@ -13,14 +13,16 @@ import '../../flutter_quill_extensions.dart'; class VideoApp extends StatefulWidget { const VideoApp({ required this.videoUrl, - required this.context, required this.readOnly, + @Deprecated( + 'The context is no longer required and will be removed on future releases', + ) + BuildContext? context, super.key, this.onVideoInit, }); final String videoUrl; - final BuildContext context; final bool readOnly; final void Function(GlobalKey videoContainerKey)? onVideoInit; @@ -92,29 +94,33 @@ class VideoAppState extends State { : _controller.play(); }); }, - child: Stack(alignment: Alignment.center, children: [ - Center( - child: AspectRatio( - aspectRatio: _controller.value.aspectRatio, - child: VideoPlayer(_controller), - )), - _controller.value.isPlaying - ? const SizedBox.shrink() - : Container( - color: const Color(0xfff5f5f5), - child: const Icon( - Icons.play_arrow, - size: 60, - color: Colors.blueGrey, - )) - ]), + child: Stack( + alignment: Alignment.center, + children: [ + Center( + child: AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: VideoPlayer(_controller), + )), + _controller.value.isPlaying + ? const SizedBox.shrink() + : Container( + color: const Color(0xfff5f5f5), + child: const Icon( + Icons.play_arrow, + size: 60, + color: Colors.blueGrey, + ), + ) + ], + ), ), ); } @override void dispose() { - super.dispose(); _controller.dispose(); + super.dispose(); } } diff --git a/flutter_quill_extensions/lib/embeds/widgets/youtube_video_app.dart b/flutter_quill_extensions/lib/embeds/widgets/youtube_video_app.dart index 57b0c3d6..a6ce72cc 100644 --- a/flutter_quill_extensions/lib/embeds/widgets/youtube_video_app.dart +++ b/flutter_quill_extensions/lib/embeds/widgets/youtube_video_app.dart @@ -1,78 +1,143 @@ import 'package:flutter/gestures.dart' show TapGestureRecognizer; -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_quill/flutter_quill.dart' show DefaultStyles; -import 'package:url_launcher/url_launcher.dart' show launchUrl; +import 'package:url_launcher/url_launcher_string.dart'; +import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:youtube_player_flutter/youtube_player_flutter.dart'; +import '../../models/config/video/editor/youtube_video_support_mode.dart'; +import 'video_app.dart'; + class YoutubeVideoApp extends StatefulWidget { const YoutubeVideoApp({ required this.videoUrl, required this.readOnly, + required this.youtubeVideoSupportMode, super.key, }); final String videoUrl; final bool readOnly; + final YoutubeVideoSupportMode youtubeVideoSupportMode; @override YoutubeVideoAppState createState() => YoutubeVideoAppState(); } class YoutubeVideoAppState extends State { - YoutubePlayerController? _youtubeController; + YoutubePlayerController? _youtubeIframeController; + + /// On some platforms such as desktop, Webview is not supported yet + /// as a result the youtube video player package is not supported too + /// this future will be not null and fetch the video url to load it using + /// [VideoApp] + Future? _loadYoutubeVideoByDownloadUrlFuture; + + /// Null if the video URL is not a YouTube video + String? get _videoId { + return YoutubePlayer.convertUrlToId(widget.videoUrl); + } @override void initState() { super.initState(); - final videoId = YoutubePlayer.convertUrlToId(widget.videoUrl); - if (videoId != null) { - _youtubeController = YoutubePlayerController( - initialVideoId: videoId, - flags: const YoutubePlayerFlags( - autoPlay: false, - ), - ); + final videoId = _videoId; + if (videoId == null) { + return; } + switch (widget.youtubeVideoSupportMode) { + case YoutubeVideoSupportMode.disabled: + break; + case YoutubeVideoSupportMode.iframeView: + _youtubeIframeController = YoutubePlayerController( + initialVideoId: videoId, + flags: const YoutubePlayerFlags( + autoPlay: false, + ), + ); + break; + case YoutubeVideoSupportMode.customPlayerWithDownloadUrl: + _loadYoutubeVideoByDownloadUrlFuture = + _loadYoutubeVideoWithVideoPlayerByVideoUrl(); + break; + } + } + + Future _loadYoutubeVideoWithVideoPlayerByVideoUrl() async { + final youtubeExplode = YoutubeExplode(); + final manifest = + await youtubeExplode.videos.streamsClient.getManifest(_videoId); + final streamInfo = manifest.muxed.withHighestBitrate(); + final videoDownloadUri = streamInfo.url; + return videoDownloadUri.toString(); + } + + Widget _clickableVideoLinkText({required DefaultStyles defaultStyles}) { + return RichText( + text: TextSpan( + text: widget.videoUrl, + style: defaultStyles.link, + recognizer: TapGestureRecognizer() + ..onTap = () => launchUrlString(widget.videoUrl), + ), + ); } @override Widget build(BuildContext context) { final defaultStyles = DefaultStyles.getInstance(context); - final youtubeController = _youtubeController; - - if (youtubeController == null) { - if (widget.readOnly) { - return RichText( - text: TextSpan( - text: widget.videoUrl, - style: defaultStyles.link, - recognizer: TapGestureRecognizer() - ..onTap = () => launchUrl( - Uri.parse(widget.videoUrl), - ), + + switch (widget.youtubeVideoSupportMode) { + case YoutubeVideoSupportMode.disabled: + throw UnsupportedError('YouTube video links are not supported'); + case YoutubeVideoSupportMode.iframeView: + final youtubeController = _youtubeIframeController; + + if (youtubeController == null) { + if (widget.readOnly) { + return _clickableVideoLinkText(defaultStyles: defaultStyles); + } + + return RichText( + text: TextSpan(text: widget.videoUrl, style: defaultStyles.link), + ); + } + return YoutubePlayerBuilder( + player: YoutubePlayer( + controller: youtubeController, + showVideoProgressIndicator: true, ), + builder: (context, player) { + return player; + }, + ); + case YoutubeVideoSupportMode.customPlayerWithDownloadUrl: + assert( + _loadYoutubeVideoByDownloadUrlFuture != null, + 'The load youtube video future should not null for "${widget.youtubeVideoSupportMode}" mode', ); - } - return RichText( - text: TextSpan(text: widget.videoUrl, style: defaultStyles.link), - ); + return FutureBuilder( + future: _loadYoutubeVideoByDownloadUrlFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator.adaptive()); + } + if (snapshot.hasError) { + return _clickableVideoLinkText(defaultStyles: defaultStyles); + } + return VideoApp( + videoUrl: snapshot.requireData, + readOnly: widget.readOnly, + ); + }, + ); } - - return YoutubePlayerBuilder( - player: YoutubePlayer( - controller: youtubeController, - showVideoProgressIndicator: true, - ), - builder: (context, player) { - return player; - }, - ); } @override void dispose() { - _youtubeController?.dispose(); + _youtubeIframeController?.dispose(); super.dispose(); } } diff --git a/flutter_quill_extensions/lib/models/config/video/editor/video_configurations.dart b/flutter_quill_extensions/lib/models/config/video/editor/video_configurations.dart index 53039dee..cecc1c2e 100644 --- a/flutter_quill_extensions/lib/models/config/video/editor/video_configurations.dart +++ b/flutter_quill_extensions/lib/models/config/video/editor/video_configurations.dart @@ -1,10 +1,13 @@ import 'package:flutter/widgets.dart' show GlobalKey; import 'package:meta/meta.dart' show immutable; +import 'youtube_video_support_mode.dart'; + @immutable class QuillEditorVideoEmbedConfigurations { const QuillEditorVideoEmbedConfigurations({ this.onVideoInit, + this.youtubeVideoSupportMode = YoutubeVideoSupportMode.iframeView, }); /// [onVideoInit] is a callback function that gets triggered when @@ -21,4 +24,8 @@ class QuillEditorVideoEmbedConfigurations { /// // Customize other callback functions as needed /// ``` final void Function(GlobalKey videoContainerKey)? onVideoInit; + + /// Specifies how YouTube videos should be loaded if the video URL + /// is YouTube video. + final YoutubeVideoSupportMode youtubeVideoSupportMode; } diff --git a/flutter_quill_extensions/lib/models/config/video/editor/youtube_video_support_mode.dart b/flutter_quill_extensions/lib/models/config/video/editor/youtube_video_support_mode.dart new file mode 100644 index 00000000..d66285ce --- /dev/null +++ b/flutter_quill_extensions/lib/models/config/video/editor/youtube_video_support_mode.dart @@ -0,0 +1,19 @@ +/// Enum represents the different modes for handling YouTube video support. +enum YoutubeVideoSupportMode { + /// Disable loading of YouTube videos. + disabled, + + /// Load the video using the official YouTube IFrame API. + /// See [YouTube IFrame API](https://developers.google.com/youtube/iframe_api_reference) for more details. + /// + /// This will use Platform View on native platforms to use WebView + /// The WebView might not be supported on Desktop and will throw an exception + /// + /// See [Flutter InAppWebview Support for Flutter Desktop](https://github.com/pichillilorenzo/flutter_inappwebview/issues/460) + iframeView, + + /// Load the video using a custom video player by fetching the YouTube video URL. + /// Note: This might violate YouTube's terms of service. + /// See [YouTube Terms of Service](https://www.youtube.com/static?template=terms) for more details. + customPlayerWithDownloadUrl, +} diff --git a/flutter_quill_extensions/pubspec.yaml b/flutter_quill_extensions/pubspec.yaml index a97e6567..cca88811 100644 --- a/flutter_quill_extensions/pubspec.yaml +++ b/flutter_quill_extensions/pubspec.yaml @@ -28,7 +28,7 @@ dependencies: flutter: sdk: flutter - # Normal packages + # Dart Packages http: ^1.2.0 path: ^1.8.3 meta: ^1.10.0 @@ -37,10 +37,11 @@ dependencies: flutter_quill: ^9.3.12 photo_view: ^0.15.0 + youtube_explode_dart: ^2.2.1 # Plugins video_player: ^2.8.1 - youtube_player_flutter: ^9.0.0 + youtube_player_flutter: ^9.0.1 url_launcher: ^6.2.1 super_clipboard: ^0.8.15 gal: ^2.3.0 diff --git a/pubspec.yaml b/pubspec.yaml index 2af7b5bb..dce9ba9f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,7 +42,7 @@ dependencies: flutter_localizations: sdk: flutter - # Normal packages + # Dart Packages intl: ^0.19.0 dart_quill_delta: ^9.3.3 collection: ^1.17.0