Rich text editor for Flutter
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

223 lines
6.8 KiB

import 'dart:io' show File;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_quill/extensions.dart';
import 'package:flutter_quill/flutter_quill.dart' hide Text;
import 'package:flutter_quill/translations.dart';
import 'package:image_picker/image_picker.dart';
import '../embed_types.dart';
class LinkDialog extends StatefulWidget {
const LinkDialog({
this.dialogTheme,
this.link,
this.linkRegExp,
Key? key,
}) : super(key: key);
final QuillDialogTheme? dialogTheme;
final String? link;
final RegExp? linkRegExp;
@override
LinkDialogState createState() => LinkDialogState();
}
class LinkDialogState extends State<LinkDialog> {
late String _link;
late TextEditingController _controller;
late RegExp _linkRegExp;
@override
void initState() {
super.initState();
_link = widget.link ?? '';
_controller = TextEditingController(text: _link);
// TODO: Consider replace the default Regex with this one
// Since that is not the reason I sent the changes then I will not edit it
// final defaultLinkNonSecureRegExp = RegExp(r'https?://.*?\.(?:png|jpe?g|gif|bmp|webp|tiff?)'); // Not secure
// final defaultLinkRegExp = RegExp(r'https://.*?\.(?:png|jpe?g|gif|bmp|webp|tiff?)'); // Secure
// _linkRegExp = widget.linkRegExp ?? defaultLinkRegExp;
_linkRegExp = widget.linkRegExp ?? AutoFormatMultipleLinksRule.linkRegExp;
}
@override
Widget build(BuildContext context) {
return AlertDialog(
backgroundColor: widget.dialogTheme?.dialogBackgroundColor,
content: TextField(
keyboardType: TextInputType.multiline,
maxLines: null,
style: widget.dialogTheme?.inputTextStyle,
decoration: InputDecoration(
labelText: 'Paste a link'.i18n,
labelStyle: widget.dialogTheme?.labelTextStyle,
floatingLabelStyle: widget.dialogTheme?.labelTextStyle),
autofocus: true,
onChanged: _linkChanged,
controller: _controller,
),
actions: [
TextButton(
onPressed: _link.isNotEmpty && _linkRegExp.hasMatch(_link)
? _applyLink
: null,
child: Text(
'Ok'.i18n,
style: widget.dialogTheme?.labelTextStyle,
),
),
],
);
}
void _linkChanged(String value) {
setState(() {
_link = value;
});
}
void _applyLink() {
Navigator.pop(context, _link.trim());
}
}
class ImageVideoUtils {
static Future<MediaPickSetting?> selectMediaPickSetting(
BuildContext context,
) =>
showDialog<MediaPickSetting>(
context: context,
builder: (ctx) => AlertDialog(
contentPadding: EdgeInsets.zero,
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextButton.icon(
icon: const Icon(
Icons.collections,
color: Colors.orangeAccent,
),
label: Text('Gallery'.i18n),
onPressed: () => Navigator.pop(ctx, MediaPickSetting.Gallery),
),
TextButton.icon(
icon: const Icon(
Icons.link,
color: Colors.cyanAccent,
),
label: Text('Link'.i18n),
onPressed: () => Navigator.pop(ctx, MediaPickSetting.Link),
)
],
),
),
);
/// For image picking logic
static Future<void> handleImageButtonTap(
BuildContext context,
QuillController controller,
ImageSource imageSource,
OnImagePickCallback onImagePickCallback,
{FilePickImpl? filePickImpl,
WebImagePickImpl? webImagePickImpl}) async {
final index = controller.selection.baseOffset;
final length = controller.selection.extentOffset - index;
String? imageUrl;
if (kIsWeb) {
assert(
webImagePickImpl != null,
'Please provide webImagePickImpl for Web '
'(check out example directory for how to do it)');
imageUrl = await webImagePickImpl!(onImagePickCallback);
} else if (isMobile()) {
imageUrl = await _pickImage(imageSource, onImagePickCallback);
} else {
assert(filePickImpl != null, 'Desktop must provide filePickImpl');
imageUrl =
await _pickImageDesktop(context, filePickImpl!, onImagePickCallback);
}
if (imageUrl != null) {
controller.replaceText(index, length, BlockEmbed.image(imageUrl), null);
}
}
static Future<String?> _pickImage(
ImageSource source, OnImagePickCallback onImagePickCallback) async {
final pickedFile = await ImagePicker().pickImage(source: source);
if (pickedFile == null) {
return null;
}
return onImagePickCallback(File(pickedFile.path));
}
static Future<String?> _pickImageDesktop(
BuildContext context,
FilePickImpl filePickImpl,
OnImagePickCallback onImagePickCallback) async {
final filePath = await filePickImpl(context);
if (filePath == null || filePath.isEmpty) return null;
final file = File(filePath);
return onImagePickCallback(file);
}
/// For video picking logic
static Future<void> handleVideoButtonTap(
BuildContext context,
QuillController controller,
ImageSource videoSource,
OnVideoPickCallback onVideoPickCallback,
{FilePickImpl? filePickImpl,
WebVideoPickImpl? webVideoPickImpl}) async {
final index = controller.selection.baseOffset;
final length = controller.selection.extentOffset - index;
String? videoUrl;
if (kIsWeb) {
assert(
webVideoPickImpl != null,
'Please provide webVideoPickImpl for Web '
'(check out example directory for how to do it)');
videoUrl = await webVideoPickImpl!(onVideoPickCallback);
} else if (isMobile()) {
videoUrl = await _pickVideo(videoSource, onVideoPickCallback);
} else {
assert(filePickImpl != null, 'Desktop must provide filePickImpl');
videoUrl =
await _pickVideoDesktop(context, filePickImpl!, onVideoPickCallback);
}
if (videoUrl != null) {
controller.replaceText(index, length, BlockEmbed.video(videoUrl), null);
}
}
static Future<String?> _pickVideo(
ImageSource source, OnVideoPickCallback onVideoPickCallback) async {
final pickedFile = await ImagePicker().pickVideo(source: source);
if (pickedFile == null) {
return null;
}
return onVideoPickCallback(File(pickedFile.path));
}
static Future<String?> _pickVideoDesktop(
BuildContext context,
FilePickImpl filePickImpl,
OnVideoPickCallback onVideoPickCallback) async {
final filePath = await filePickImpl(context);
if (filePath == null || filePath.isEmpty) return null;
final file = File(filePath);
return onVideoPickCallback(file);
}
}