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