parent
11dd8e7d39
commit
eeb720e097
23 changed files with 1049 additions and 783 deletions
@ -0,0 +1,36 @@ |
|||||||
|
import 'package:flutter/widgets.dart' show BuildContext; |
||||||
|
import 'package:meta/meta.dart' show immutable; |
||||||
|
|
||||||
|
import 'image.dart'; |
||||||
|
|
||||||
|
enum CameraAction { |
||||||
|
video, |
||||||
|
image, |
||||||
|
} |
||||||
|
|
||||||
|
/// When the user click the camera button, should we take a photo or record |
||||||
|
/// a video using the camera |
||||||
|
/// |
||||||
|
/// by default will show a dialog that ask the user which option he/she wants |
||||||
|
typedef OnRequestCameraActionCallback = Future<CameraAction?> Function( |
||||||
|
BuildContext context, |
||||||
|
); |
||||||
|
|
||||||
|
@immutable |
||||||
|
class QuillToolbarCameraConfigurations { |
||||||
|
const QuillToolbarCameraConfigurations({ |
||||||
|
this.onRequestCameraActionCallback, |
||||||
|
OnImageInsertCallback? onImageInsertCallback, |
||||||
|
this.onImageInsertedCallback, |
||||||
|
}) : _onImageInsertCallback = onImageInsertCallback; |
||||||
|
|
||||||
|
final OnRequestCameraActionCallback? onRequestCameraActionCallback; |
||||||
|
|
||||||
|
final OnImageInsertedCallback? onImageInsertedCallback; |
||||||
|
|
||||||
|
final OnImageInsertCallback? _onImageInsertCallback; |
||||||
|
|
||||||
|
OnImageInsertCallback get onImageInsertCallback { |
||||||
|
return _onImageInsertCallback ?? defaultOnImageInsertCallback(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,66 @@ |
|||||||
|
import 'package:flutter/widgets.dart' show BuildContext; |
||||||
|
import 'package:flutter_quill/flutter_quill.dart'; |
||||||
|
import 'package:meta/meta.dart' show immutable; |
||||||
|
|
||||||
|
import '../../../logic/extensions/controller.dart'; |
||||||
|
import '../../../logic/services/image_picker/s_image_picker.dart'; |
||||||
|
|
||||||
|
/// When request picking an video, for example when the video button toolbar |
||||||
|
/// clicked, it should be null in case the user didn't choose any video or |
||||||
|
/// any other reasons, and it should be the video file path as string that is |
||||||
|
/// exists in case the user picked the video successfully |
||||||
|
/// |
||||||
|
/// by default we already have a default implementation that show a dialog |
||||||
|
/// request the source for picking the video, from gallery, link or camera |
||||||
|
typedef OnRequestPickVideo = Future<String?> Function( |
||||||
|
BuildContext context, |
||||||
|
ImagePickerService imagePickerService, |
||||||
|
); |
||||||
|
|
||||||
|
/// A callback will called when inserting a video in the editor |
||||||
|
/// it have the logic that will insert the video block using the controller |
||||||
|
typedef OnVideoInsertCallback = Future<void> Function( |
||||||
|
String video, |
||||||
|
QuillController controller, |
||||||
|
); |
||||||
|
|
||||||
|
OnVideoInsertCallback defaultOnVideoInsertCallback() { |
||||||
|
return (videoUrl, controller) async { |
||||||
|
controller |
||||||
|
..skipRequestKeyboard = true |
||||||
|
..insertVideoBlock(videoUrl: videoUrl); |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
/// When a new video picked this callback will called and you might want to |
||||||
|
/// do some logic depending on your use case |
||||||
|
typedef OnVideoInsertedCallback = Future<void> Function( |
||||||
|
String video, |
||||||
|
); |
||||||
|
|
||||||
|
enum InsertVideoSource { |
||||||
|
gallery, |
||||||
|
camera, |
||||||
|
link, |
||||||
|
} |
||||||
|
|
||||||
|
/// Configurations for dealing with videos, on insert a video |
||||||
|
/// on request picking a video |
||||||
|
@immutable |
||||||
|
class QuillToolbarVideoConfigurations { |
||||||
|
const QuillToolbarVideoConfigurations({ |
||||||
|
this.onRequestPickVideo, |
||||||
|
this.onVideoInsertedCallback, |
||||||
|
OnVideoInsertCallback? onVideoInsertCallback, |
||||||
|
}) : _onVideoInsertCallback = onVideoInsertCallback; |
||||||
|
|
||||||
|
final OnRequestPickVideo? onRequestPickVideo; |
||||||
|
|
||||||
|
final OnVideoInsertedCallback? onVideoInsertedCallback; |
||||||
|
|
||||||
|
final OnVideoInsertCallback? _onVideoInsertCallback; |
||||||
|
|
||||||
|
OnVideoInsertCallback get onVideoInsertCallback { |
||||||
|
return _onVideoInsertCallback ?? defaultOnVideoInsertCallback(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,37 @@ |
|||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:flutter_quill/translations.dart'; |
||||||
|
|
||||||
|
import '../../embed_types/camera.dart'; |
||||||
|
|
||||||
|
class SelectCameraActionDialog extends StatelessWidget { |
||||||
|
const SelectCameraActionDialog({super.key}); |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
return AlertDialog( |
||||||
|
contentPadding: EdgeInsets.zero, |
||||||
|
backgroundColor: Colors.transparent, |
||||||
|
content: Column( |
||||||
|
mainAxisSize: MainAxisSize.min, |
||||||
|
children: [ |
||||||
|
TextButton.icon( |
||||||
|
icon: const Icon( |
||||||
|
Icons.camera, |
||||||
|
color: Colors.orangeAccent, |
||||||
|
), |
||||||
|
label: Text('Photo'.i18n), |
||||||
|
onPressed: () => Navigator.pop(context, CameraAction.image), |
||||||
|
), |
||||||
|
TextButton.icon( |
||||||
|
icon: const Icon( |
||||||
|
Icons.video_call, |
||||||
|
color: Colors.cyanAccent, |
||||||
|
), |
||||||
|
label: Text('Video'.i18n), |
||||||
|
onPressed: () => Navigator.pop(context, CameraAction.video), |
||||||
|
) |
||||||
|
], |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -1,533 +0,0 @@ |
|||||||
// ignore_for_file: use_build_context_synchronously |
|
||||||
|
|
||||||
import 'dart:math' as math; |
|
||||||
import 'dart:ui'; |
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart'; |
|
||||||
import 'package:flutter/material.dart'; |
|
||||||
import 'package:flutter_quill/extensions.dart'; |
|
||||||
import 'package:flutter_quill/flutter_quill.dart'; |
|
||||||
import 'package:flutter_quill/translations.dart'; |
|
||||||
import 'package:image_picker/image_picker.dart'; |
|
||||||
|
|
||||||
import '../../models/config/toolbar/buttons/media_button.dart'; |
|
||||||
import '../embed_types.dart'; |
|
||||||
import 'utils/image_video_utils.dart'; |
|
||||||
|
|
||||||
/// Widget which combines [ImageButton] and [VideButton] widgets. This widget |
|
||||||
/// has more customization and uses dialog similar to one which is used |
|
||||||
/// on [http://quilljs.com]. |
|
||||||
class QuillToolbarMediaButton extends StatelessWidget { |
|
||||||
QuillToolbarMediaButton({ |
|
||||||
required this.controller, |
|
||||||
required this.options, |
|
||||||
super.key, |
|
||||||
}) : assert(options.type == QuillMediaType.image, |
|
||||||
'Video selection is not supported yet'); |
|
||||||
|
|
||||||
final QuillController controller; |
|
||||||
final QuillToolbarMediaButtonOptions options; |
|
||||||
|
|
||||||
double _iconSize(BuildContext context) { |
|
||||||
final baseFontSize = baseButtonExtraOptions(context).globalIconSize; |
|
||||||
final iconSize = options.iconSize; |
|
||||||
return iconSize ?? baseFontSize; |
|
||||||
} |
|
||||||
|
|
||||||
VoidCallback? _afterButtonPressed(BuildContext context) { |
|
||||||
return options.afterButtonPressed ?? |
|
||||||
baseButtonExtraOptions(context).afterButtonPressed; |
|
||||||
} |
|
||||||
|
|
||||||
QuillIconTheme? _iconTheme(BuildContext context) { |
|
||||||
return options.iconTheme ?? baseButtonExtraOptions(context).iconTheme; |
|
||||||
} |
|
||||||
|
|
||||||
QuillToolbarBaseButtonOptions baseButtonExtraOptions(BuildContext context) { |
|
||||||
return context.requireQuillToolbarBaseButtonOptions; |
|
||||||
} |
|
||||||
|
|
||||||
(IconData, String) get _defaultData { |
|
||||||
switch (options.type) { |
|
||||||
case QuillMediaType.image: |
|
||||||
return (Icons.perm_media, 'Photo media button'); |
|
||||||
case QuillMediaType.video: |
|
||||||
throw UnsupportedError('The video is not supported yet.'); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
IconData _iconData(BuildContext context) { |
|
||||||
return options.iconData ?? |
|
||||||
baseButtonExtraOptions(context).iconData ?? |
|
||||||
_defaultData.$1; |
|
||||||
} |
|
||||||
|
|
||||||
String _tooltip(BuildContext context) { |
|
||||||
return options.tooltip ?? |
|
||||||
baseButtonExtraOptions(context).tooltip ?? |
|
||||||
_defaultData.$2; |
|
||||||
// ('Camera'.i18n); |
|
||||||
} |
|
||||||
|
|
||||||
void _sharedOnPressed(BuildContext context) { |
|
||||||
_onPressedHandler(context); |
|
||||||
_afterButtonPressed(context); |
|
||||||
} |
|
||||||
|
|
||||||
@override |
|
||||||
Widget build(BuildContext context) { |
|
||||||
final tooltip = _tooltip(context); |
|
||||||
final iconSize = _iconSize(context); |
|
||||||
final iconData = _iconData(context); |
|
||||||
final childBuilder = |
|
||||||
options.childBuilder ?? baseButtonExtraOptions(context).childBuilder; |
|
||||||
final iconTheme = _iconTheme(context); |
|
||||||
|
|
||||||
if (childBuilder != null) { |
|
||||||
return childBuilder( |
|
||||||
QuillToolbarMediaButtonOptions( |
|
||||||
type: options.type, |
|
||||||
onMediaPickedCallback: options.onMediaPickedCallback, |
|
||||||
onImagePickCallback: options.onImagePickCallback, |
|
||||||
onVideoPickCallback: options.onVideoPickCallback, |
|
||||||
iconData: iconData, |
|
||||||
afterButtonPressed: _afterButtonPressed(context), |
|
||||||
autovalidateMode: options.autovalidateMode, |
|
||||||
childrenSpacing: options.childrenSpacing, |
|
||||||
dialogBarrierColor: options.dialogBarrierColor, |
|
||||||
dialogTheme: options.dialogTheme, |
|
||||||
filePickImpl: options.filePickImpl, |
|
||||||
fillColor: options.fillColor, |
|
||||||
galleryButtonText: options.galleryButtonText, |
|
||||||
iconTheme: iconTheme, |
|
||||||
iconSize: iconSize, |
|
||||||
hintText: options.hintText, |
|
||||||
labelText: options.labelText, |
|
||||||
submitButtonSize: options.submitButtonSize, |
|
||||||
linkButtonText: options.linkButtonText, |
|
||||||
mediaFilePicker: options.mediaFilePicker, |
|
||||||
submitButtonText: options.submitButtonText, |
|
||||||
validationMessage: options.validationMessage, |
|
||||||
webImagePickImpl: options.webImagePickImpl, |
|
||||||
webVideoPickImpl: options.webVideoPickImpl, |
|
||||||
tooltip: options.tooltip, |
|
||||||
), |
|
||||||
QuillToolbarMediaButtonExtraOptions( |
|
||||||
context: context, |
|
||||||
controller: controller, |
|
||||||
onPressed: () => _sharedOnPressed(context), |
|
||||||
), |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
final theme = Theme.of(context); |
|
||||||
|
|
||||||
final iconColor = |
|
||||||
options.iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; |
|
||||||
final iconFillColor = options.iconTheme?.iconUnselectedFillColor ?? |
|
||||||
options.fillColor ?? |
|
||||||
theme.canvasColor; |
|
||||||
|
|
||||||
return QuillToolbarIconButton( |
|
||||||
icon: Icon(iconData, size: iconSize, color: iconColor), |
|
||||||
tooltip: tooltip, |
|
||||||
highlightElevation: 0, |
|
||||||
hoverElevation: 0, |
|
||||||
size: iconSize * 1.77, |
|
||||||
fillColor: iconFillColor, |
|
||||||
borderRadius: iconTheme?.borderRadius ?? 2, |
|
||||||
onPressed: () => _sharedOnPressed(context), |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
Future<void> _onPressedHandler(BuildContext context) async { |
|
||||||
if (options.onMediaPickedCallback == null) { |
|
||||||
_inputLink(context); |
|
||||||
return; |
|
||||||
} |
|
||||||
final mediaSource = await showDialog<MediaPickSetting>( |
|
||||||
context: context, |
|
||||||
builder: (_) => MediaSourceSelectorDialog( |
|
||||||
dialogTheme: options.dialogTheme, |
|
||||||
galleryButtonText: options.galleryButtonText, |
|
||||||
linkButtonText: options.linkButtonText, |
|
||||||
), |
|
||||||
); |
|
||||||
if (mediaSource == null) { |
|
||||||
return; |
|
||||||
} |
|
||||||
switch (mediaSource) { |
|
||||||
case MediaPickSetting.gallery: |
|
||||||
await _pickImage(); |
|
||||||
break; |
|
||||||
case MediaPickSetting.link: |
|
||||||
_inputLink(context); |
|
||||||
break; |
|
||||||
case MediaPickSetting.camera: |
|
||||||
await ImageVideoUtils.handleImageButtonTap( |
|
||||||
context, |
|
||||||
controller, |
|
||||||
ImageSource.camera, |
|
||||||
options.onImagePickCallback, |
|
||||||
filePickImpl: options.filePickImpl, |
|
||||||
webImagePickImpl: options.webImagePickImpl, |
|
||||||
); |
|
||||||
break; |
|
||||||
case MediaPickSetting.video: |
|
||||||
await ImageVideoUtils.handleVideoButtonTap( |
|
||||||
context, |
|
||||||
controller, |
|
||||||
ImageSource.camera, |
|
||||||
options.onVideoPickCallback, |
|
||||||
filePickImpl: options.filePickImpl, |
|
||||||
webVideoPickImpl: options.webVideoPickImpl, |
|
||||||
); |
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
Future<void> _pickImage() async { |
|
||||||
if (!(kIsWeb || isMobile() || isDesktop())) { |
|
||||||
throw UnsupportedError( |
|
||||||
'Unsupported target platform: ${defaultTargetPlatform.name}', |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
final mediaFileUrl = await _pickMediaFileUrl(); |
|
||||||
|
|
||||||
if (mediaFileUrl != null) { |
|
||||||
final index = controller.selection.baseOffset; |
|
||||||
final length = controller.selection.extentOffset - index; |
|
||||||
controller.replaceText( |
|
||||||
index, |
|
||||||
length, |
|
||||||
BlockEmbed.image(mediaFileUrl), |
|
||||||
null, |
|
||||||
); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
Future<MediaFileUrl?> _pickMediaFileUrl() async { |
|
||||||
final mediaFile = await options.mediaFilePicker?.call(options.type); |
|
||||||
return mediaFile != null |
|
||||||
? options.onMediaPickedCallback?.call(mediaFile) |
|
||||||
: null; |
|
||||||
} |
|
||||||
|
|
||||||
void _inputLink(BuildContext context) { |
|
||||||
showDialog<String>( |
|
||||||
context: context, |
|
||||||
builder: (_) => MediaLinkDialog( |
|
||||||
dialogTheme: options.dialogTheme, |
|
||||||
labelText: options.labelText, |
|
||||||
hintText: options.hintText, |
|
||||||
buttonText: options.submitButtonText, |
|
||||||
buttonSize: options.submitButtonSize, |
|
||||||
childrenSpacing: options.childrenSpacing, |
|
||||||
autovalidateMode: options.autovalidateMode, |
|
||||||
validationMessage: options.validationMessage, |
|
||||||
), |
|
||||||
).then(_linkSubmitted); |
|
||||||
} |
|
||||||
|
|
||||||
void _linkSubmitted(String? value) { |
|
||||||
if (value != null && value.isNotEmpty) { |
|
||||||
final index = controller.selection.baseOffset; |
|
||||||
final length = controller.selection.extentOffset - index; |
|
||||||
final data = options.type.isImage |
|
||||||
? BlockEmbed.image(value) |
|
||||||
: BlockEmbed.video(value); |
|
||||||
controller.replaceText(index, length, data, null); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Provides a dialog for input link to media resource. |
|
||||||
class MediaLinkDialog extends StatefulWidget { |
|
||||||
const MediaLinkDialog({ |
|
||||||
super.key, |
|
||||||
this.link, |
|
||||||
this.dialogTheme, |
|
||||||
this.childrenSpacing = 16.0, |
|
||||||
this.labelText, |
|
||||||
this.hintText, |
|
||||||
this.buttonText, |
|
||||||
this.buttonSize, |
|
||||||
this.autovalidateMode = AutovalidateMode.disabled, |
|
||||||
this.validationMessage, |
|
||||||
}) : assert(childrenSpacing > 0); |
|
||||||
|
|
||||||
final String? link; |
|
||||||
final QuillDialogTheme? dialogTheme; |
|
||||||
|
|
||||||
/// The margin between child widgets in the dialog. |
|
||||||
final double childrenSpacing; |
|
||||||
|
|
||||||
/// The text of label in link add mode. |
|
||||||
final String? labelText; |
|
||||||
|
|
||||||
/// The hint text for link [TextField]. |
|
||||||
final String? hintText; |
|
||||||
|
|
||||||
/// The text of the submit button. |
|
||||||
final String? buttonText; |
|
||||||
|
|
||||||
/// The size of dialog buttons. |
|
||||||
final Size? buttonSize; |
|
||||||
|
|
||||||
final AutovalidateMode autovalidateMode; |
|
||||||
final String? validationMessage; |
|
||||||
|
|
||||||
@override |
|
||||||
State<MediaLinkDialog> createState() => _MediaLinkDialogState(); |
|
||||||
} |
|
||||||
|
|
||||||
class _MediaLinkDialogState extends State<MediaLinkDialog> { |
|
||||||
final _linkFocus = FocusNode(); |
|
||||||
final _linkController = TextEditingController(); |
|
||||||
|
|
||||||
@override |
|
||||||
void dispose() { |
|
||||||
_linkFocus.dispose(); |
|
||||||
_linkController.dispose(); |
|
||||||
super.dispose(); |
|
||||||
} |
|
||||||
|
|
||||||
@override |
|
||||||
Widget build(BuildContext context) { |
|
||||||
final constraints = widget.dialogTheme?.linkDialogConstraints ?? |
|
||||||
() { |
|
||||||
final size = MediaQuery.sizeOf(context); |
|
||||||
final maxWidth = kIsWeb ? size.width / 4 : size.width - 80; |
|
||||||
return BoxConstraints(maxWidth: maxWidth, maxHeight: 80); |
|
||||||
}(); |
|
||||||
|
|
||||||
final buttonStyle = widget.buttonSize != null |
|
||||||
? Theme.of(context) |
|
||||||
.elevatedButtonTheme |
|
||||||
.style |
|
||||||
?.copyWith(fixedSize: MaterialStatePropertyAll(widget.buttonSize)) |
|
||||||
: widget.dialogTheme?.buttonStyle; |
|
||||||
|
|
||||||
final isWrappable = widget.dialogTheme?.isWrappable ?? false; |
|
||||||
|
|
||||||
final children = [ |
|
||||||
Text(widget.labelText ?? 'Enter media'.i18n), |
|
||||||
UtilityWidgets.maybeWidget( |
|
||||||
enabled: !isWrappable, |
|
||||||
wrapper: (child) => Expanded( |
|
||||||
child: child, |
|
||||||
), |
|
||||||
child: Padding( |
|
||||||
padding: EdgeInsets.symmetric(horizontal: widget.childrenSpacing), |
|
||||||
child: TextFormField( |
|
||||||
controller: _linkController, |
|
||||||
focusNode: _linkFocus, |
|
||||||
style: widget.dialogTheme?.inputTextStyle, |
|
||||||
keyboardType: TextInputType.url, |
|
||||||
textInputAction: TextInputAction.done, |
|
||||||
decoration: InputDecoration( |
|
||||||
labelStyle: widget.dialogTheme?.labelTextStyle, |
|
||||||
hintText: widget.hintText, |
|
||||||
), |
|
||||||
autofocus: true, |
|
||||||
autovalidateMode: widget.autovalidateMode, |
|
||||||
validator: _validateLink, |
|
||||||
onChanged: _linkChanged, |
|
||||||
), |
|
||||||
), |
|
||||||
), |
|
||||||
ElevatedButton( |
|
||||||
onPressed: _canPress() ? _submitLink : null, |
|
||||||
style: buttonStyle, |
|
||||||
child: Text(widget.buttonText ?? 'Ok'.i18n), |
|
||||||
), |
|
||||||
]; |
|
||||||
|
|
||||||
return Dialog( |
|
||||||
backgroundColor: widget.dialogTheme?.dialogBackgroundColor, |
|
||||||
shape: widget.dialogTheme?.shape ?? |
|
||||||
DialogTheme.of(context).shape ?? |
|
||||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), |
|
||||||
child: ConstrainedBox( |
|
||||||
constraints: constraints, |
|
||||||
child: Padding( |
|
||||||
padding: |
|
||||||
widget.dialogTheme?.linkDialogPadding ?? const EdgeInsets.all(16), |
|
||||||
child: Form( |
|
||||||
child: isWrappable |
|
||||||
? Wrap( |
|
||||||
alignment: WrapAlignment.center, |
|
||||||
crossAxisAlignment: WrapCrossAlignment.center, |
|
||||||
runSpacing: widget.dialogTheme?.runSpacing ?? 0.0, |
|
||||||
children: children, |
|
||||||
) |
|
||||||
: Row( |
|
||||||
children: children, |
|
||||||
), |
|
||||||
), |
|
||||||
), |
|
||||||
), |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
bool _canPress() => _validateLink(_linkController.text) == null; |
|
||||||
|
|
||||||
void _linkChanged(String value) { |
|
||||||
setState(() { |
|
||||||
_linkController.text = value; |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
void _submitLink() => Navigator.pop(context, _linkController.text); |
|
||||||
|
|
||||||
String? _validateLink(String? value) { |
|
||||||
if ((value?.isEmpty ?? false) || |
|
||||||
!AutoFormatMultipleLinksRule.oneLineLinkRegExp.hasMatch(value!)) { |
|
||||||
return widget.validationMessage ?? 'That is not a valid URL'; |
|
||||||
} |
|
||||||
|
|
||||||
return null; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Media souce selector. |
|
||||||
class MediaSourceSelectorDialog extends StatelessWidget { |
|
||||||
const MediaSourceSelectorDialog({ |
|
||||||
super.key, |
|
||||||
this.dialogTheme, |
|
||||||
this.galleryButtonText, |
|
||||||
this.linkButtonText, |
|
||||||
}); |
|
||||||
|
|
||||||
final QuillDialogTheme? dialogTheme; |
|
||||||
|
|
||||||
/// The text of the gallery button [MediaSourceSelectorDialog]. |
|
||||||
final String? galleryButtonText; |
|
||||||
|
|
||||||
/// The text of the link button [MediaSourceSelectorDialog]. |
|
||||||
final String? linkButtonText; |
|
||||||
|
|
||||||
@override |
|
||||||
Widget build(BuildContext context) { |
|
||||||
final constraints = dialogTheme?.mediaSelectorDialogConstraints ?? |
|
||||||
() { |
|
||||||
final size = MediaQuery.sizeOf(context); |
|
||||||
double maxWidth, maxHeight; |
|
||||||
if (kIsWeb) { |
|
||||||
maxWidth = size.width / 7; |
|
||||||
maxHeight = size.height / 7; |
|
||||||
} else { |
|
||||||
maxWidth = size.width - 80; |
|
||||||
maxHeight = maxWidth / 2; |
|
||||||
} |
|
||||||
return BoxConstraints(maxWidth: maxWidth, maxHeight: maxHeight); |
|
||||||
}(); |
|
||||||
|
|
||||||
final shape = dialogTheme?.shape ?? |
|
||||||
DialogTheme.of(context).shape ?? |
|
||||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)); |
|
||||||
|
|
||||||
return Dialog( |
|
||||||
backgroundColor: dialogTheme?.dialogBackgroundColor, |
|
||||||
shape: shape, |
|
||||||
child: ConstrainedBox( |
|
||||||
constraints: constraints, |
|
||||||
child: Padding( |
|
||||||
padding: dialogTheme?.mediaSelectorDialogPadding ?? |
|
||||||
const EdgeInsets.all(16), |
|
||||||
child: Row( |
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|
||||||
children: [ |
|
||||||
Expanded( |
|
||||||
child: TextButtonWithIcon( |
|
||||||
icon: Icons.collections, |
|
||||||
label: galleryButtonText ?? 'Gallery'.i18n, |
|
||||||
onPressed: () => |
|
||||||
Navigator.pop(context, MediaPickSetting.gallery), |
|
||||||
), |
|
||||||
), |
|
||||||
const SizedBox(width: 10), |
|
||||||
Expanded( |
|
||||||
child: TextButtonWithIcon( |
|
||||||
icon: Icons.link, |
|
||||||
label: linkButtonText ?? 'Link'.i18n, |
|
||||||
onPressed: () => |
|
||||||
Navigator.pop(context, MediaPickSetting.link), |
|
||||||
), |
|
||||||
) |
|
||||||
], |
|
||||||
), |
|
||||||
), |
|
||||||
), |
|
||||||
); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
class TextButtonWithIcon extends StatelessWidget { |
|
||||||
const TextButtonWithIcon({ |
|
||||||
required this.label, |
|
||||||
required this.icon, |
|
||||||
required this.onPressed, |
|
||||||
this.textStyle, |
|
||||||
super.key, |
|
||||||
}); |
|
||||||
|
|
||||||
final String label; |
|
||||||
final IconData icon; |
|
||||||
final VoidCallback onPressed; |
|
||||||
final TextStyle? textStyle; |
|
||||||
|
|
||||||
@override |
|
||||||
Widget build(BuildContext context) { |
|
||||||
final theme = Theme.of(context); |
|
||||||
final scale = MediaQuery.maybeOf(context)?.textScaleFactor ?? 1; |
|
||||||
final gap = scale <= 1 ? 8.0 : lerpDouble(8, 4, math.min(scale - 1, 1))!; |
|
||||||
final buttonStyle = TextButtonTheme.of(context).style; |
|
||||||
final shape = buttonStyle?.shape?.resolve({}) ?? |
|
||||||
const RoundedRectangleBorder( |
|
||||||
borderRadius: BorderRadius.all( |
|
||||||
Radius.circular(4), |
|
||||||
), |
|
||||||
); |
|
||||||
return Material( |
|
||||||
shape: shape, |
|
||||||
textStyle: textStyle ?? |
|
||||||
theme.textButtonTheme.style?.textStyle?.resolve({}) ?? |
|
||||||
theme.textTheme.labelLarge, |
|
||||||
elevation: buttonStyle?.elevation?.resolve({}) ?? 0, |
|
||||||
child: InkWell( |
|
||||||
customBorder: shape, |
|
||||||
onTap: onPressed, |
|
||||||
child: Padding( |
|
||||||
padding: const EdgeInsets.all(16), |
|
||||||
child: Column( |
|
||||||
mainAxisSize: MainAxisSize.min, |
|
||||||
children: <Widget>[ |
|
||||||
Icon(icon), |
|
||||||
SizedBox(height: gap), |
|
||||||
Flexible(child: Text(label)), |
|
||||||
], |
|
||||||
), |
|
||||||
), |
|
||||||
), |
|
||||||
); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Default file picker. |
|
||||||
// Future<QuillFile?> _defaultMediaPicker(QuillMediaType mediaType) async { |
|
||||||
// final pickedFile = mediaType.isImage |
|
||||||
// ? await ImagePicker().pickImage(source: ImageSource.gallery) |
|
||||||
// : await ImagePicker().pickVideo(source: ImageSource.gallery); |
|
||||||
|
|
||||||
// if (pickedFile != null) { |
|
||||||
// return QuillFile( |
|
||||||
// name: pickedFile.name, |
|
||||||
// path: pickedFile.path, |
|
||||||
// bytes: await pickedFile.readAsBytes(), |
|
||||||
// ); |
|
||||||
// } |
|
||||||
|
|
||||||
// return null; |
|
||||||
// } |
|
@ -0,0 +1,533 @@ |
|||||||
|
// // ignore_for_file: use_build_context_synchronously |
||||||
|
|
||||||
|
// import 'dart:math' as math; |
||||||
|
// import 'dart:ui'; |
||||||
|
|
||||||
|
// import 'package:flutter/foundation.dart'; |
||||||
|
// import 'package:flutter/material.dart'; |
||||||
|
// import 'package:flutter_quill/extensions.dart'; |
||||||
|
// import 'package:flutter_quill/flutter_quill.dart'; |
||||||
|
// import 'package:flutter_quill/translations.dart'; |
||||||
|
// import 'package:image_picker/image_picker.dart'; |
||||||
|
|
||||||
|
// import '../../../models/config/toolbar/buttons/media_button.dart'; |
||||||
|
// import '../../embed_types.dart'; |
||||||
|
// import '../utils/image_video_utils.dart'; |
||||||
|
|
||||||
|
// /// Widget which combines [ImageButton] and [VideButton] widgets. This widget |
||||||
|
// /// has more customization and uses dialog similar to one which is used |
||||||
|
// /// on [http://quilljs.com]. |
||||||
|
// class QuillToolbarMediaButton extends StatelessWidget { |
||||||
|
// QuillToolbarMediaButton({ |
||||||
|
// required this.controller, |
||||||
|
// required this.options, |
||||||
|
// super.key, |
||||||
|
// }) : assert(options.type == QuillMediaType.image, |
||||||
|
// 'Video selection is not supported yet'); |
||||||
|
|
||||||
|
// final QuillController controller; |
||||||
|
// final QuillToolbarMediaButtonOptions options; |
||||||
|
|
||||||
|
// double _iconSize(BuildContext context) { |
||||||
|
// final baseFontSize = baseButtonExtraOptions(context).globalIconSize; |
||||||
|
// final iconSize = options.iconSize; |
||||||
|
// return iconSize ?? baseFontSize; |
||||||
|
// } |
||||||
|
|
||||||
|
// VoidCallback? _afterButtonPressed(BuildContext context) { |
||||||
|
// return options.afterButtonPressed ?? |
||||||
|
// baseButtonExtraOptions(context).afterButtonPressed; |
||||||
|
// } |
||||||
|
|
||||||
|
// QuillIconTheme? _iconTheme(BuildContext context) { |
||||||
|
// return options.iconTheme ?? baseButtonExtraOptions(context).iconTheme; |
||||||
|
// } |
||||||
|
|
||||||
|
// QuillToolbarBaseButtonOptions baseButtonExtraOptions(BuildContext context) { |
||||||
|
// return context.requireQuillToolbarBaseButtonOptions; |
||||||
|
// } |
||||||
|
|
||||||
|
// (IconData, String) get _defaultData { |
||||||
|
// switch (options.type) { |
||||||
|
// case QuillMediaType.image: |
||||||
|
// return (Icons.perm_media, 'Photo media button'); |
||||||
|
// case QuillMediaType.video: |
||||||
|
// throw UnsupportedError('The video is not supported yet.'); |
||||||
|
// } |
||||||
|
// } |
||||||
|
|
||||||
|
// IconData _iconData(BuildContext context) { |
||||||
|
// return options.iconData ?? |
||||||
|
// baseButtonExtraOptions(context).iconData ?? |
||||||
|
// _defaultData.$1; |
||||||
|
// } |
||||||
|
|
||||||
|
// String _tooltip(BuildContext context) { |
||||||
|
// return options.tooltip ?? |
||||||
|
// baseButtonExtraOptions(context).tooltip ?? |
||||||
|
// _defaultData.$2; |
||||||
|
// // ('Camera'.i18n); |
||||||
|
// } |
||||||
|
|
||||||
|
// void _sharedOnPressed(BuildContext context) { |
||||||
|
// _onPressedHandler(context); |
||||||
|
// _afterButtonPressed(context); |
||||||
|
// } |
||||||
|
|
||||||
|
// @override |
||||||
|
// Widget build(BuildContext context) { |
||||||
|
// final tooltip = _tooltip(context); |
||||||
|
// final iconSize = _iconSize(context); |
||||||
|
// final iconData = _iconData(context); |
||||||
|
// final childBuilder = |
||||||
|
// options.childBuilder ?? baseButtonExtraOptions(context).childBuilder; |
||||||
|
// final iconTheme = _iconTheme(context); |
||||||
|
|
||||||
|
// if (childBuilder != null) { |
||||||
|
// return childBuilder( |
||||||
|
// QuillToolbarMediaButtonOptions( |
||||||
|
// type: options.type, |
||||||
|
// onMediaPickedCallback: options.onMediaPickedCallback, |
||||||
|
// onImagePickCallback: options.onImagePickCallback, |
||||||
|
// onVideoPickCallback: options.onVideoPickCallback, |
||||||
|
// iconData: iconData, |
||||||
|
// afterButtonPressed: _afterButtonPressed(context), |
||||||
|
// autovalidateMode: options.autovalidateMode, |
||||||
|
// childrenSpacing: options.childrenSpacing, |
||||||
|
// dialogBarrierColor: options.dialogBarrierColor, |
||||||
|
// dialogTheme: options.dialogTheme, |
||||||
|
// filePickImpl: options.filePickImpl, |
||||||
|
// fillColor: options.fillColor, |
||||||
|
// galleryButtonText: options.galleryButtonText, |
||||||
|
// iconTheme: iconTheme, |
||||||
|
// iconSize: iconSize, |
||||||
|
// hintText: options.hintText, |
||||||
|
// labelText: options.labelText, |
||||||
|
// submitButtonSize: options.submitButtonSize, |
||||||
|
// linkButtonText: options.linkButtonText, |
||||||
|
// mediaFilePicker: options.mediaFilePicker, |
||||||
|
// submitButtonText: options.submitButtonText, |
||||||
|
// validationMessage: options.validationMessage, |
||||||
|
// webImagePickImpl: options.webImagePickImpl, |
||||||
|
// webVideoPickImpl: options.webVideoPickImpl, |
||||||
|
// tooltip: options.tooltip, |
||||||
|
// ), |
||||||
|
// QuillToolbarMediaButtonExtraOptions( |
||||||
|
// context: context, |
||||||
|
// controller: controller, |
||||||
|
// onPressed: () => _sharedOnPressed(context), |
||||||
|
// ), |
||||||
|
// ); |
||||||
|
// } |
||||||
|
|
||||||
|
// final theme = Theme.of(context); |
||||||
|
|
||||||
|
// final iconColor = |
||||||
|
// options.iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; |
||||||
|
// final iconFillColor = options.iconTheme?.iconUnselectedFillColor ?? |
||||||
|
// options.fillColor ?? |
||||||
|
// theme.canvasColor; |
||||||
|
|
||||||
|
// return QuillToolbarIconButton( |
||||||
|
// icon: Icon(iconData, size: iconSize, color: iconColor), |
||||||
|
// tooltip: tooltip, |
||||||
|
// highlightElevation: 0, |
||||||
|
// hoverElevation: 0, |
||||||
|
// size: iconSize * 1.77, |
||||||
|
// fillColor: iconFillColor, |
||||||
|
// borderRadius: iconTheme?.borderRadius ?? 2, |
||||||
|
// onPressed: () => _sharedOnPressed(context), |
||||||
|
// ); |
||||||
|
// } |
||||||
|
|
||||||
|
// Future<void> _onPressedHandler(BuildContext context) async { |
||||||
|
// if (options.onMediaPickedCallback == null) { |
||||||
|
// _inputLink(context); |
||||||
|
// return; |
||||||
|
// } |
||||||
|
// final mediaSource = await showDialog<MediaPickSetting>( |
||||||
|
// context: context, |
||||||
|
// builder: (_) => MediaSourceSelectorDialog( |
||||||
|
// dialogTheme: options.dialogTheme, |
||||||
|
// galleryButtonText: options.galleryButtonText, |
||||||
|
// linkButtonText: options.linkButtonText, |
||||||
|
// ), |
||||||
|
// ); |
||||||
|
// if (mediaSource == null) { |
||||||
|
// return; |
||||||
|
// } |
||||||
|
// switch (mediaSource) { |
||||||
|
// case MediaPickSetting.gallery: |
||||||
|
// await _pickImage(); |
||||||
|
// break; |
||||||
|
// case MediaPickSetting.link: |
||||||
|
// _inputLink(context); |
||||||
|
// break; |
||||||
|
// case MediaPickSetting.camera: |
||||||
|
// await ImageVideoUtils.handleImageButtonTap( |
||||||
|
// context, |
||||||
|
// controller, |
||||||
|
// ImageSource.camera, |
||||||
|
// options.onImagePickCallback, |
||||||
|
// filePickImpl: options.filePickImpl, |
||||||
|
// webImagePickImpl: options.webImagePickImpl, |
||||||
|
// ); |
||||||
|
// break; |
||||||
|
// case MediaPickSetting.video: |
||||||
|
// await ImageVideoUtils.handleVideoButtonTap( |
||||||
|
// context, |
||||||
|
// controller, |
||||||
|
// ImageSource.camera, |
||||||
|
// options.onVideoPickCallback, |
||||||
|
// filePickImpl: options.filePickImpl, |
||||||
|
// webVideoPickImpl: options.webVideoPickImpl, |
||||||
|
// ); |
||||||
|
// break; |
||||||
|
// } |
||||||
|
// } |
||||||
|
|
||||||
|
// Future<void> _pickImage() async { |
||||||
|
// if (!(kIsWeb || isMobile() || isDesktop())) { |
||||||
|
// throw UnsupportedError( |
||||||
|
// 'Unsupported target platform: ${defaultTargetPlatform.name}', |
||||||
|
// ); |
||||||
|
// } |
||||||
|
|
||||||
|
// final mediaFileUrl = await _pickMediaFileUrl(); |
||||||
|
|
||||||
|
// if (mediaFileUrl != null) { |
||||||
|
// final index = controller.selection.baseOffset; |
||||||
|
// final length = controller.selection.extentOffset - index; |
||||||
|
// controller.replaceText( |
||||||
|
// index, |
||||||
|
// length, |
||||||
|
// BlockEmbed.image(mediaFileUrl), |
||||||
|
// null, |
||||||
|
// ); |
||||||
|
// } |
||||||
|
// } |
||||||
|
|
||||||
|
// Future<MediaFileUrl?> _pickMediaFileUrl() async { |
||||||
|
// final mediaFile = await options.mediaFilePicker?.call(options.type); |
||||||
|
// return mediaFile != null |
||||||
|
// ? options.onMediaPickedCallback?.call(mediaFile) |
||||||
|
// : null; |
||||||
|
// } |
||||||
|
|
||||||
|
// void _inputLink(BuildContext context) { |
||||||
|
// showDialog<String>( |
||||||
|
// context: context, |
||||||
|
// builder: (_) => MediaLinkDialog( |
||||||
|
// dialogTheme: options.dialogTheme, |
||||||
|
// labelText: options.labelText, |
||||||
|
// hintText: options.hintText, |
||||||
|
// buttonText: options.submitButtonText, |
||||||
|
// buttonSize: options.submitButtonSize, |
||||||
|
// childrenSpacing: options.childrenSpacing, |
||||||
|
// autovalidateMode: options.autovalidateMode, |
||||||
|
// validationMessage: options.validationMessage, |
||||||
|
// ), |
||||||
|
// ).then(_linkSubmitted); |
||||||
|
// } |
||||||
|
|
||||||
|
// void _linkSubmitted(String? value) { |
||||||
|
// if (value != null && value.isNotEmpty) { |
||||||
|
// final index = controller.selection.baseOffset; |
||||||
|
// final length = controller.selection.extentOffset - index; |
||||||
|
// final data = options.type.isImage |
||||||
|
// ? BlockEmbed.image(value) |
||||||
|
// : BlockEmbed.video(value); |
||||||
|
// controller.replaceText(index, length, data, null); |
||||||
|
// } |
||||||
|
// } |
||||||
|
// } |
||||||
|
|
||||||
|
// /// Provides a dialog for input link to media resource. |
||||||
|
// class MediaLinkDialog extends StatefulWidget { |
||||||
|
// const MediaLinkDialog({ |
||||||
|
// super.key, |
||||||
|
// this.link, |
||||||
|
// this.dialogTheme, |
||||||
|
// this.childrenSpacing = 16.0, |
||||||
|
// this.labelText, |
||||||
|
// this.hintText, |
||||||
|
// this.buttonText, |
||||||
|
// this.buttonSize, |
||||||
|
// this.autovalidateMode = AutovalidateMode.disabled, |
||||||
|
// this.validationMessage, |
||||||
|
// }) : assert(childrenSpacing > 0); |
||||||
|
|
||||||
|
// final String? link; |
||||||
|
// final QuillDialogTheme? dialogTheme; |
||||||
|
|
||||||
|
// /// The margin between child widgets in the dialog. |
||||||
|
// final double childrenSpacing; |
||||||
|
|
||||||
|
// /// The text of label in link add mode. |
||||||
|
// final String? labelText; |
||||||
|
|
||||||
|
// /// The hint text for link [TextField]. |
||||||
|
// final String? hintText; |
||||||
|
|
||||||
|
// /// The text of the submit button. |
||||||
|
// final String? buttonText; |
||||||
|
|
||||||
|
// /// The size of dialog buttons. |
||||||
|
// final Size? buttonSize; |
||||||
|
|
||||||
|
// final AutovalidateMode autovalidateMode; |
||||||
|
// final String? validationMessage; |
||||||
|
|
||||||
|
// @override |
||||||
|
// State<MediaLinkDialog> createState() => _MediaLinkDialogState(); |
||||||
|
// } |
||||||
|
|
||||||
|
// class _MediaLinkDialogState extends State<MediaLinkDialog> { |
||||||
|
// final _linkFocus = FocusNode(); |
||||||
|
// final _linkController = TextEditingController(); |
||||||
|
|
||||||
|
// @override |
||||||
|
// void dispose() { |
||||||
|
// _linkFocus.dispose(); |
||||||
|
// _linkController.dispose(); |
||||||
|
// super.dispose(); |
||||||
|
// } |
||||||
|
|
||||||
|
// @override |
||||||
|
// Widget build(BuildContext context) { |
||||||
|
// final constraints = widget.dialogTheme?.linkDialogConstraints ?? |
||||||
|
// () { |
||||||
|
// final size = MediaQuery.sizeOf(context); |
||||||
|
// final maxWidth = kIsWeb ? size.width / 4 : size.width - 80; |
||||||
|
// return BoxConstraints(maxWidth: maxWidth, maxHeight: 80); |
||||||
|
// }(); |
||||||
|
|
||||||
|
// final buttonStyle = widget.buttonSize != null |
||||||
|
// ? Theme.of(context) |
||||||
|
// .elevatedButtonTheme |
||||||
|
// .style |
||||||
|
// ?.copyWith(fixedSize: MaterialStatePropertyAll(widget.buttonSize)) |
||||||
|
// : widget.dialogTheme?.buttonStyle; |
||||||
|
|
||||||
|
// final isWrappable = widget.dialogTheme?.isWrappable ?? false; |
||||||
|
|
||||||
|
// final children = [ |
||||||
|
// Text(widget.labelText ?? 'Enter media'.i18n), |
||||||
|
// UtilityWidgets.maybeWidget( |
||||||
|
// enabled: !isWrappable, |
||||||
|
// wrapper: (child) => Expanded( |
||||||
|
// child: child, |
||||||
|
// ), |
||||||
|
// child: Padding( |
||||||
|
// padding: EdgeInsets.symmetric(horizontal: widget.childrenSpacing), |
||||||
|
// child: TextFormField( |
||||||
|
// controller: _linkController, |
||||||
|
// focusNode: _linkFocus, |
||||||
|
// style: widget.dialogTheme?.inputTextStyle, |
||||||
|
// keyboardType: TextInputType.url, |
||||||
|
// textInputAction: TextInputAction.done, |
||||||
|
// decoration: InputDecoration( |
||||||
|
// labelStyle: widget.dialogTheme?.labelTextStyle, |
||||||
|
// hintText: widget.hintText, |
||||||
|
// ), |
||||||
|
// autofocus: true, |
||||||
|
// autovalidateMode: widget.autovalidateMode, |
||||||
|
// validator: _validateLink, |
||||||
|
// onChanged: _linkChanged, |
||||||
|
// ), |
||||||
|
// ), |
||||||
|
// ), |
||||||
|
// ElevatedButton( |
||||||
|
// onPressed: _canPress() ? _submitLink : null, |
||||||
|
// style: buttonStyle, |
||||||
|
// child: Text(widget.buttonText ?? 'Ok'.i18n), |
||||||
|
// ), |
||||||
|
// ]; |
||||||
|
|
||||||
|
// return Dialog( |
||||||
|
// backgroundColor: widget.dialogTheme?.dialogBackgroundColor, |
||||||
|
// shape: widget.dialogTheme?.shape ?? |
||||||
|
// DialogTheme.of(context).shape ?? |
||||||
|
// RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), |
||||||
|
// child: ConstrainedBox( |
||||||
|
// constraints: constraints, |
||||||
|
// child: Padding( |
||||||
|
// padding: |
||||||
|
// widget.dialogTheme?.linkDialogPadding ?? const EdgeInsets.all(16), |
||||||
|
// child: Form( |
||||||
|
// child: isWrappable |
||||||
|
// ? Wrap( |
||||||
|
// alignment: WrapAlignment.center, |
||||||
|
// crossAxisAlignment: WrapCrossAlignment.center, |
||||||
|
// runSpacing: widget.dialogTheme?.runSpacing ?? 0.0, |
||||||
|
// children: children, |
||||||
|
// ) |
||||||
|
// : Row( |
||||||
|
// children: children, |
||||||
|
// ), |
||||||
|
// ), |
||||||
|
// ), |
||||||
|
// ), |
||||||
|
// ); |
||||||
|
// } |
||||||
|
|
||||||
|
// bool _canPress() => _validateLink(_linkController.text) == null; |
||||||
|
|
||||||
|
// void _linkChanged(String value) { |
||||||
|
// setState(() { |
||||||
|
// _linkController.text = value; |
||||||
|
// }); |
||||||
|
// } |
||||||
|
|
||||||
|
// void _submitLink() => Navigator.pop(context, _linkController.text); |
||||||
|
|
||||||
|
// String? _validateLink(String? value) { |
||||||
|
// if ((value?.isEmpty ?? false) || |
||||||
|
// !AutoFormatMultipleLinksRule.oneLineLinkRegExp.hasMatch(value!)) { |
||||||
|
// return widget.validationMessage ?? 'That is not a valid URL'; |
||||||
|
// } |
||||||
|
|
||||||
|
// return null; |
||||||
|
// } |
||||||
|
// } |
||||||
|
|
||||||
|
// /// Media souce selector. |
||||||
|
// class MediaSourceSelectorDialog extends StatelessWidget { |
||||||
|
// const MediaSourceSelectorDialog({ |
||||||
|
// super.key, |
||||||
|
// this.dialogTheme, |
||||||
|
// this.galleryButtonText, |
||||||
|
// this.linkButtonText, |
||||||
|
// }); |
||||||
|
|
||||||
|
// final QuillDialogTheme? dialogTheme; |
||||||
|
|
||||||
|
// /// The text of the gallery button [MediaSourceSelectorDialog]. |
||||||
|
// final String? galleryButtonText; |
||||||
|
|
||||||
|
// /// The text of the link button [MediaSourceSelectorDialog]. |
||||||
|
// final String? linkButtonText; |
||||||
|
|
||||||
|
// @override |
||||||
|
// Widget build(BuildContext context) { |
||||||
|
// final constraints = dialogTheme?.mediaSelectorDialogConstraints ?? |
||||||
|
// () { |
||||||
|
// final size = MediaQuery.sizeOf(context); |
||||||
|
// double maxWidth, maxHeight; |
||||||
|
// if (kIsWeb) { |
||||||
|
// maxWidth = size.width / 7; |
||||||
|
// maxHeight = size.height / 7; |
||||||
|
// } else { |
||||||
|
// maxWidth = size.width - 80; |
||||||
|
// maxHeight = maxWidth / 2; |
||||||
|
// } |
||||||
|
// return BoxConstraints(maxWidth: maxWidth, maxHeight: maxHeight); |
||||||
|
// }(); |
||||||
|
|
||||||
|
// final shape = dialogTheme?.shape ?? |
||||||
|
// DialogTheme.of(context).shape ?? |
||||||
|
// RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)); |
||||||
|
|
||||||
|
// return Dialog( |
||||||
|
// backgroundColor: dialogTheme?.dialogBackgroundColor, |
||||||
|
// shape: shape, |
||||||
|
// child: ConstrainedBox( |
||||||
|
// constraints: constraints, |
||||||
|
// child: Padding( |
||||||
|
// padding: dialogTheme?.mediaSelectorDialogPadding ?? |
||||||
|
// const EdgeInsets.all(16), |
||||||
|
// child: Row( |
||||||
|
// mainAxisAlignment: MainAxisAlignment.spaceBetween, |
||||||
|
// children: [ |
||||||
|
// Expanded( |
||||||
|
// child: TextButtonWithIcon( |
||||||
|
// icon: Icons.collections, |
||||||
|
// label: galleryButtonText ?? 'Gallery'.i18n, |
||||||
|
// onPressed: () => |
||||||
|
// Navigator.pop(context, MediaPickSetting.gallery), |
||||||
|
// ), |
||||||
|
// ), |
||||||
|
// const SizedBox(width: 10), |
||||||
|
// Expanded( |
||||||
|
// child: TextButtonWithIcon( |
||||||
|
// icon: Icons.link, |
||||||
|
// label: linkButtonText ?? 'Link'.i18n, |
||||||
|
// onPressed: () => |
||||||
|
// Navigator.pop(context, MediaPickSetting.link), |
||||||
|
// ), |
||||||
|
// ) |
||||||
|
// ], |
||||||
|
// ), |
||||||
|
// ), |
||||||
|
// ), |
||||||
|
// ); |
||||||
|
// } |
||||||
|
// } |
||||||
|
|
||||||
|
// class TextButtonWithIcon extends StatelessWidget { |
||||||
|
// const TextButtonWithIcon({ |
||||||
|
// required this.label, |
||||||
|
// required this.icon, |
||||||
|
// required this.onPressed, |
||||||
|
// this.textStyle, |
||||||
|
// super.key, |
||||||
|
// }); |
||||||
|
|
||||||
|
// final String label; |
||||||
|
// final IconData icon; |
||||||
|
// final VoidCallback onPressed; |
||||||
|
// final TextStyle? textStyle; |
||||||
|
|
||||||
|
// @override |
||||||
|
// Widget build(BuildContext context) { |
||||||
|
// final theme = Theme.of(context); |
||||||
|
// final scale = MediaQuery.maybeOf(context)?.textScaleFactor ?? 1; |
||||||
|
// final gap = scale <= 1 ? 8.0 : lerpDouble(8, 4, math.min(scale - 1, 1))!; |
||||||
|
// final buttonStyle = TextButtonTheme.of(context).style; |
||||||
|
// final shape = buttonStyle?.shape?.resolve({}) ?? |
||||||
|
// const RoundedRectangleBorder( |
||||||
|
// borderRadius: BorderRadius.all( |
||||||
|
// Radius.circular(4), |
||||||
|
// ), |
||||||
|
// ); |
||||||
|
// return Material( |
||||||
|
// shape: shape, |
||||||
|
// textStyle: textStyle ?? |
||||||
|
// theme.textButtonTheme.style?.textStyle?.resolve({}) ?? |
||||||
|
// theme.textTheme.labelLarge, |
||||||
|
// elevation: buttonStyle?.elevation?.resolve({}) ?? 0, |
||||||
|
// child: InkWell( |
||||||
|
// customBorder: shape, |
||||||
|
// onTap: onPressed, |
||||||
|
// child: Padding( |
||||||
|
// padding: const EdgeInsets.all(16), |
||||||
|
// child: Column( |
||||||
|
// mainAxisSize: MainAxisSize.min, |
||||||
|
// children: <Widget>[ |
||||||
|
// Icon(icon), |
||||||
|
// SizedBox(height: gap), |
||||||
|
// Flexible(child: Text(label)), |
||||||
|
// ], |
||||||
|
// ), |
||||||
|
// ), |
||||||
|
// ), |
||||||
|
// ); |
||||||
|
// } |
||||||
|
// } |
||||||
|
|
||||||
|
// /// Default file picker. |
||||||
|
// // Future<QuillFile?> _defaultMediaPicker(QuillMediaType mediaType) async { |
||||||
|
// // final pickedFile = mediaType.isImage |
||||||
|
// // ? await ImagePicker().pickImage(source: ImageSource.gallery) |
||||||
|
// // : await ImagePicker().pickVideo(source: ImageSource.gallery); |
||||||
|
|
||||||
|
// // if (pickedFile != null) { |
||||||
|
// // return QuillFile( |
||||||
|
// // name: pickedFile.name, |
||||||
|
// // path: pickedFile.path, |
||||||
|
// // bytes: await pickedFile.readAsBytes(), |
||||||
|
// // ); |
||||||
|
// // } |
||||||
|
|
||||||
|
// // return null; |
||||||
|
// // } |
@ -0,0 +1,57 @@ |
|||||||
|
import 'package:flutter/material.dart'; |
||||||
|
|
||||||
|
import '../../embed_types/video.dart'; |
||||||
|
|
||||||
|
class SelectVideoSourceDialog extends StatelessWidget { |
||||||
|
const SelectVideoSourceDialog({super.key}); |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
return SizedBox( |
||||||
|
height: 230, |
||||||
|
width: double.infinity, |
||||||
|
child: SingleChildScrollView( |
||||||
|
child: Column( |
||||||
|
children: [ |
||||||
|
ListTile( |
||||||
|
title: const Text('Gallery'), |
||||||
|
subtitle: const Text( |
||||||
|
'Pick a video from your gallery', |
||||||
|
), |
||||||
|
leading: const Icon(Icons.photo_sharp), |
||||||
|
onTap: () => Navigator.of(context).pop(InsertVideoSource.gallery), |
||||||
|
), |
||||||
|
ListTile( |
||||||
|
title: const Text('Camera'), |
||||||
|
subtitle: const Text( |
||||||
|
'Record a video using your phone camera', |
||||||
|
), |
||||||
|
leading: const Icon(Icons.camera), |
||||||
|
onTap: () => Navigator.of(context).pop(InsertVideoSource.camera), |
||||||
|
), |
||||||
|
ListTile( |
||||||
|
title: const Text('Link'), |
||||||
|
subtitle: const Text( |
||||||
|
'Paste a video using a link', |
||||||
|
), |
||||||
|
leading: const Icon(Icons.link), |
||||||
|
onTap: () => Navigator.of(context).pop(InsertVideoSource.link), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Future<InsertVideoSource?> showSelectVideoSourceDialog({ |
||||||
|
required BuildContext context, |
||||||
|
}) async { |
||||||
|
final imageSource = await showModalBottomSheet<InsertVideoSource>( |
||||||
|
showDragHandle: true, |
||||||
|
context: context, |
||||||
|
constraints: const BoxConstraints(maxWidth: 640), |
||||||
|
builder: (context) => const SelectVideoSourceDialog(), |
||||||
|
); |
||||||
|
return imageSource; |
||||||
|
} |
Loading…
Reference in new issue