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.
 
 
 
 
 

201 lines
5.5 KiB

import 'dart:io' show File;
import 'package:flutter/foundation.dart' show immutable;
import 'package:flutter/widgets.dart' show Alignment;
import 'package:flutter_quill/extensions.dart' as base;
import 'package:flutter_quill/flutter_quill.dart' show Attribute, Node;
import '../../logic/extensions/attribute.dart';
import '../../logic/services/image_saver/s_image_saver.dart';
import '../embeds/widgets/image.dart';
RegExp _base64 = RegExp(
r'^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{4})$',
);
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 isImageBase64(String imageUrl) {
return !isHttpBasedUrl(imageUrl) && isBase64(imageUrl);
}
bool isYouTubeUrl(String videoUrl) {
try {
final uri = Uri.parse(videoUrl);
return uri.host == 'www.youtube.com' ||
uri.host == 'youtube.com' ||
uri.host == 'youtu.be' ||
uri.host == 'www.youtu.be';
} catch (_) {
return false;
}
}
enum SaveImageResultMethod { network, localStorage }
@immutable
class SaveImageResult {
const SaveImageResult({required this.isSuccess, required this.method});
final bool isSuccess;
final SaveImageResultMethod method;
}
Future<SaveImageResult> saveImage({
required String imageUrl,
required ImageSaverService imageSaverService,
}) async {
final imageFile = File(imageUrl);
final hasPermission = await imageSaverService.hasAccess();
if (!hasPermission) {
await imageSaverService.requestAccess();
}
final imageExistsLocally = await imageFile.exists();
if (!imageExistsLocally) {
try {
await imageSaverService.saveImageFromNetwork(
Uri.parse(appendFileExtensionToImageUrl(imageUrl)),
);
return const SaveImageResult(
isSuccess: true,
method: SaveImageResultMethod.network,
);
} catch (e) {
return const SaveImageResult(
isSuccess: false,
method: SaveImageResultMethod.network,
);
}
}
try {
await imageSaverService.saveLocalImage(imageUrl);
return const SaveImageResult(
isSuccess: true,
method: SaveImageResultMethod.localStorage,
);
} catch (e) {
return const SaveImageResult(
isSuccess: false,
method: SaveImageResultMethod.localStorage,
);
}
}
(
OptionalSize elementSize,
double? margin,
Alignment alignment,
) getElementAttributes(
Node node,
) {
var elementSize = const OptionalSize(null, null);
var elementAlignment = Alignment.center;
double? elementMargin;
// Usually double value
final heightValue = double.tryParse(
node.style.attributes[Attribute.height.key]?.value.toString() ?? '');
final widthValue = double.tryParse(
node.style.attributes[Attribute.width.key]?.value.toString() ?? '');
if (heightValue != null) {
elementSize = elementSize.copyWith(
height: heightValue,
);
}
if (widthValue != null) {
elementSize = elementSize.copyWith(
width: widthValue,
);
}
final cssStyle = node.style.attributes['style'];
if (cssStyle != null) {
final attrs = base.isMobile(supportWeb: false)
? base.parseKeyValuePairs(cssStyle.value.toString(), {
AttributeExt.mobileWidth.key,
AttributeExt.mobileHeight.key,
AttributeExt.mobileMargin.key,
AttributeExt.mobileAlignment.key,
})
: base.parseKeyValuePairs(cssStyle.value.toString(), {
Attribute.width.key,
Attribute.height.key,
'margin',
'alignment',
});
if (attrs.isEmpty) {
return (elementSize, elementMargin, elementAlignment);
}
// It css value as string but we will try to support it anyway
// TODO: This could be improved much better
final cssHeightValue = double.tryParse(((base.isMobile(supportWeb: false)
? attrs[AttributeExt.mobileHeight.key]
: attrs[Attribute.height.key]) ??
'')
.replaceFirst('px', ''));
final cssWidthValue = double.tryParse(((!base.isMobile(supportWeb: false)
? attrs[Attribute.width.key]
: attrs[AttributeExt.mobileWidth.key]) ??
'')
.replaceFirst('px', ''));
if (cssHeightValue != null) {
elementSize = elementSize.copyWith(height: cssHeightValue);
}
if (cssWidthValue != null) {
elementSize = elementSize.copyWith(width: cssWidthValue);
}
elementAlignment = base.getAlignment(base.isMobile(supportWeb: false)
? attrs[AttributeExt.mobileAlignment.key]
: attrs['alignment']);
final margin = (base.isMobile(supportWeb: false)
? double.tryParse(AttributeExt.mobileMargin.key)
: double.tryParse('margin'));
if (margin != null) {
elementMargin = margin;
}
}
return (elementSize, elementMargin, elementAlignment);
}
@immutable
class OptionalSize {
const OptionalSize(
this.width,
this.height,
);
/// If non-null, requires the child to have exactly this width.
/// If null, the child is free to choose its own width.
final double? width;
/// If non-null, requires the child to have exactly this height.
/// If null, the child is free to choose its own height.
final double? height;
OptionalSize copyWith({
double? width,
double? height,
}) {
return OptionalSize(
width ?? this.width,
height ?? this.height,
);
}
}