Improving flutter quill extensions (#1564)
* organizations for flutter quill extensionspull/1577/head
parent
7ccd61e63e
commit
039e3193d9
99 changed files with 1441 additions and 625 deletions
File diff suppressed because one or more lines are too long
@ -1,7 +1,7 @@ |
|||||||
import 'package:flutter/material.dart'; |
import 'package:flutter/material.dart'; |
||||||
import 'package:flutter_quill/flutter_quill.dart'; |
import 'package:flutter_quill/flutter_quill.dart'; |
||||||
|
|
||||||
import '../../models/config/toolbar/buttons/formula.dart'; |
import '../../../models/config/toolbar/buttons/formula.dart'; |
||||||
|
|
||||||
class QuillToolbarFormulaButton extends StatelessWidget { |
class QuillToolbarFormulaButton extends StatelessWidget { |
||||||
const QuillToolbarFormulaButton({ |
const QuillToolbarFormulaButton({ |
@ -0,0 +1,86 @@ |
|||||||
|
import 'package:flutter/foundation.dart' show kIsWeb; |
||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:flutter_quill/flutter_quill.dart' hide OptionalSize; |
||||||
|
import 'package:flutter_quill/translations.dart'; |
||||||
|
|
||||||
|
import '../../../models/config/editor/image/image.dart'; |
||||||
|
import '../../../models/config/shared_configurations.dart'; |
||||||
|
import '../../../utils/element_utils/element_utils.dart'; |
||||||
|
import '../../widgets/image.dart'; |
||||||
|
import 'image_menu.dart'; |
||||||
|
|
||||||
|
class QuillEditorImageEmbedBuilder extends EmbedBuilder { |
||||||
|
QuillEditorImageEmbedBuilder({ |
||||||
|
required this.configurations, |
||||||
|
}); |
||||||
|
final QuillEditorImageEmbedConfigurations configurations; |
||||||
|
|
||||||
|
@override |
||||||
|
String get key => BlockEmbed.imageType; |
||||||
|
|
||||||
|
@override |
||||||
|
bool get expanded => false; |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build( |
||||||
|
BuildContext context, |
||||||
|
QuillController controller, |
||||||
|
Embed node, |
||||||
|
bool readOnly, |
||||||
|
bool inline, |
||||||
|
TextStyle textStyle, |
||||||
|
) { |
||||||
|
assert(!kIsWeb, 'Please provide image EmbedBuilder for Web'); |
||||||
|
|
||||||
|
final imageSource = standardizeImageUrl(node.value.data); |
||||||
|
final ((imageSize), margin, alignment) = getElementAttributes(node); |
||||||
|
|
||||||
|
final width = imageSize.width; |
||||||
|
final height = imageSize.height; |
||||||
|
|
||||||
|
final image = getImageWidgetByImageSource( |
||||||
|
imageSource, |
||||||
|
imageProviderBuilder: configurations.imageProviderBuilder, |
||||||
|
imageErrorWidgetBuilder: configurations.imageErrorWidgetBuilder, |
||||||
|
alignment: alignment, |
||||||
|
height: height, |
||||||
|
width: width, |
||||||
|
assetsPrefix: QuillSharedExtensionsConfigurations.get(context: context) |
||||||
|
.assetsPrefix, |
||||||
|
); |
||||||
|
|
||||||
|
final imageSaverService = |
||||||
|
QuillSharedExtensionsConfigurations.get(context: context) |
||||||
|
.imageSaverService; |
||||||
|
return GestureDetector( |
||||||
|
onTap: configurations.onImageClicked ?? |
||||||
|
() => showDialog( |
||||||
|
context: context, |
||||||
|
builder: (_) => QuillProvider.value( |
||||||
|
value: context.requireQuillProvider, |
||||||
|
child: FlutterQuillLocalizationsWidget( |
||||||
|
child: ImageOptionsMenu( |
||||||
|
controller: controller, |
||||||
|
configurations: configurations, |
||||||
|
imageSource: imageSource, |
||||||
|
imageSize: imageSize, |
||||||
|
isReadOnly: readOnly, |
||||||
|
imageSaverService: imageSaverService, |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
child: Builder( |
||||||
|
builder: (context) { |
||||||
|
if (margin != null) { |
||||||
|
return Padding( |
||||||
|
padding: EdgeInsets.all(margin), |
||||||
|
child: image, |
||||||
|
); |
||||||
|
} |
||||||
|
return image; |
||||||
|
}, |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -1,8 +1,8 @@ |
|||||||
import 'package:flutter/widgets.dart' show BuildContext; |
import 'package:flutter/widgets.dart' show BuildContext; |
||||||
import 'package:meta/meta.dart' show immutable; |
import 'package:meta/meta.dart' show immutable; |
||||||
|
|
||||||
import 'image.dart'; |
import '../../image/editor/image_embed_types.dart'; |
||||||
import 'video.dart'; |
import '../../video/video.dart'; |
||||||
|
|
||||||
enum CameraAction { |
enum CameraAction { |
||||||
video, |
video, |
@ -1,7 +1,7 @@ |
|||||||
import 'package:flutter/material.dart'; |
import 'package:flutter/material.dart'; |
||||||
import 'package:flutter_quill/translations.dart'; |
import 'package:flutter_quill/translations.dart'; |
||||||
|
|
||||||
import '../../embed_types/camera.dart'; |
import 'camera_types.dart'; |
||||||
|
|
||||||
class SelectCameraActionDialog extends StatelessWidget { |
class SelectCameraActionDialog extends StatelessWidget { |
||||||
const SelectCameraActionDialog({super.key}); |
const SelectCameraActionDialog({super.key}); |
@ -0,0 +1,32 @@ |
|||||||
|
import 'package:flutter_quill/flutter_quill.dart' |
||||||
|
show Attribute, AttributeScope; |
||||||
|
|
||||||
|
// class FlutterWidthAttribute extends Attribute<String?> { |
||||||
|
// const FlutterWidthAttribute(String? val) |
||||||
|
// : super('flutterWidth', AttributeScope.ignore, val); |
||||||
|
// } |
||||||
|
|
||||||
|
// class FlutterHeightAttribute extends Attribute<String?> { |
||||||
|
// const FlutterHeightAttribute(String? val) |
||||||
|
// : super('flutterHeight', AttributeScope.ignore, val); |
||||||
|
// } |
||||||
|
|
||||||
|
// class FlutterMarginAttribute extends Attribute<String?> { |
||||||
|
// const FlutterMarginAttribute(String? val) |
||||||
|
// : super('flutterMargin', AttributeScope.ignore, val); |
||||||
|
// } |
||||||
|
|
||||||
|
class FlutterAlignmentAttribute extends Attribute<String?> { |
||||||
|
const FlutterAlignmentAttribute(String? val) |
||||||
|
: super('flutterAlignment', AttributeScope.ignore, val); |
||||||
|
} |
||||||
|
|
||||||
|
extension AttributeExt on Attribute { |
||||||
|
// static const FlutterWidthAttribute flutterWidth = FlutterWidthAttribute(null); |
||||||
|
// static const FlutterHeightAttribute flutterHeight = |
||||||
|
// FlutterHeightAttribute(null); |
||||||
|
// static const FlutterMarginAttribute flutterMargin = |
||||||
|
// FlutterMarginAttribute(null); |
||||||
|
static const FlutterAlignmentAttribute flutterAlignment = |
||||||
|
FlutterAlignmentAttribute(null); |
||||||
|
} |
@ -1,30 +0,0 @@ |
|||||||
import 'package:flutter_quill/flutter_quill.dart' |
|
||||||
show Attribute, AttributeScope; |
|
||||||
|
|
||||||
class MobileWidthAttribute extends Attribute<String?> { |
|
||||||
const MobileWidthAttribute(String? val) |
|
||||||
: super('mobileWidth', AttributeScope.ignore, val); |
|
||||||
} |
|
||||||
|
|
||||||
class MobileHeightAttribute extends Attribute<String?> { |
|
||||||
const MobileHeightAttribute(String? val) |
|
||||||
: super('mobileHeight', AttributeScope.ignore, val); |
|
||||||
} |
|
||||||
|
|
||||||
class MobileMarginAttribute extends Attribute<String?> { |
|
||||||
const MobileMarginAttribute(String? val) |
|
||||||
: super('mobileMargin', AttributeScope.ignore, val); |
|
||||||
} |
|
||||||
|
|
||||||
class MobileAlignmentAttribute extends Attribute<String?> { |
|
||||||
const MobileAlignmentAttribute(String? val) |
|
||||||
: super('mobileAlignment', AttributeScope.ignore, val); |
|
||||||
} |
|
||||||
|
|
||||||
extension AttributeExt on Attribute { |
|
||||||
static const MobileWidthAttribute mobileWidth = MobileWidthAttribute(null); |
|
||||||
static const MobileHeightAttribute mobileHeight = MobileHeightAttribute(null); |
|
||||||
static const MobileMarginAttribute mobileMargin = MobileMarginAttribute(null); |
|
||||||
static const MobileAlignmentAttribute mobileAlignment = |
|
||||||
MobileAlignmentAttribute(null); |
|
||||||
} |
|
@ -1,7 +1,7 @@ |
|||||||
import 'package:flutter/widgets.dart' show Color; |
import 'package:flutter/widgets.dart' show Color; |
||||||
import 'package:flutter_quill/flutter_quill.dart'; |
import 'package:flutter_quill/flutter_quill.dart'; |
||||||
|
|
||||||
import '../../../../embeds/embed_types/camera.dart'; |
import '../../../../embeds/others/camera_button/camera_types.dart'; |
||||||
|
|
||||||
class QuillToolbarCameraButtonExtraOptions |
class QuillToolbarCameraButtonExtraOptions |
||||||
extends QuillToolbarBaseButtonExtraOptions { |
extends QuillToolbarBaseButtonExtraOptions { |
@ -1,7 +1,7 @@ |
|||||||
import 'package:flutter/widgets.dart' show Color; |
import 'package:flutter/widgets.dart' show Color; |
||||||
import 'package:flutter_quill/flutter_quill.dart'; |
import 'package:flutter_quill/flutter_quill.dart'; |
||||||
|
|
||||||
import '../../../../embeds/embed_types/video.dart'; |
import '../../../../embeds/video/video.dart'; |
||||||
|
|
||||||
class QuillToolbarVideoButtonExtraOptions |
class QuillToolbarVideoButtonExtraOptions |
||||||
extends QuillToolbarBaseButtonExtraOptions { |
extends QuillToolbarBaseButtonExtraOptions { |
@ -1,150 +0,0 @@ |
|||||||
import 'package:flutter/foundation.dart'; |
|
||||||
import 'package:flutter/material.dart'; |
|
||||||
import 'package:flutter_quill/extensions.dart' as base; |
|
||||||
import 'package:flutter_quill/flutter_quill.dart' hide OptionalSize; |
|
||||||
|
|
||||||
import '../../../../logic/models/config/shared_configurations.dart'; |
|
||||||
import '../../../models/config/editor/image/image.dart'; |
|
||||||
import '../../../utils/utils.dart'; |
|
||||||
import '../../widgets/image.dart'; |
|
||||||
import 'image_menu.dart'; |
|
||||||
|
|
||||||
class QuillEditorImageEmbedBuilder extends EmbedBuilder { |
|
||||||
QuillEditorImageEmbedBuilder({ |
|
||||||
required this.configurations, |
|
||||||
}); |
|
||||||
final QuillEditorImageEmbedConfigurations configurations; |
|
||||||
|
|
||||||
@override |
|
||||||
String get key => BlockEmbed.imageType; |
|
||||||
|
|
||||||
@override |
|
||||||
bool get expanded => false; |
|
||||||
|
|
||||||
@override |
|
||||||
Widget build( |
|
||||||
BuildContext context, |
|
||||||
QuillController controller, |
|
||||||
base.Embed node, |
|
||||||
bool readOnly, |
|
||||||
bool inline, |
|
||||||
TextStyle textStyle, |
|
||||||
) { |
|
||||||
assert(!kIsWeb, 'Please provide image EmbedBuilder for Web'); |
|
||||||
|
|
||||||
final imageSource = standardizeImageUrl(node.value.data); |
|
||||||
final ((imageSize), margin, alignment) = getElementAttributes(node); |
|
||||||
|
|
||||||
final width = imageSize.width; |
|
||||||
final height = imageSize.height; |
|
||||||
|
|
||||||
final image = getImageWidgetByImageSource( |
|
||||||
imageSource, |
|
||||||
imageProviderBuilder: configurations.imageProviderBuilder, |
|
||||||
imageErrorWidgetBuilder: configurations.imageErrorWidgetBuilder, |
|
||||||
alignment: alignment, |
|
||||||
height: height, |
|
||||||
width: width, |
|
||||||
assetsPrefix: QuillSharedExtensionsConfigurations.get(context: context) |
|
||||||
.assetsPrefix, |
|
||||||
); |
|
||||||
|
|
||||||
// OptionalSize? imageSize; |
|
||||||
// final style = node.style.attributes['style']; |
|
||||||
|
|
||||||
// if (style != null) { |
|
||||||
// final attrs = base.isMobile(supportWeb: false) |
|
||||||
// ? base.parseKeyValuePairs(style.value.toString(), { |
|
||||||
// Attribute.mobileWidth, |
|
||||||
// Attribute.mobileHeight, |
|
||||||
// Attribute.mobileMargin, |
|
||||||
// Attribute.mobileAlignment, |
|
||||||
// }) |
|
||||||
// : base.parseKeyValuePairs(style.value.toString(), { |
|
||||||
// Attribute.width.key, |
|
||||||
// Attribute.height.key, |
|
||||||
// Attribute.margin, |
|
||||||
// Attribute.alignment, |
|
||||||
// }); |
|
||||||
// if (attrs.isNotEmpty) { |
|
||||||
// final width = double.tryParse( |
|
||||||
// (base.isMobile(supportWeb: false) |
|
||||||
// ? attrs[Attribute.mobileWidth] |
|
||||||
// : attrs[Attribute.width.key]) ?? |
|
||||||
// '', |
|
||||||
// ); |
|
||||||
// final height = double.tryParse( |
|
||||||
// (base.isMobile(supportWeb: false) |
|
||||||
// ? attrs[Attribute.mobileHeight] |
|
||||||
// : attrs[Attribute.height.key]) ?? |
|
||||||
// '', |
|
||||||
// ); |
|
||||||
// final alignment = base.getAlignment(base.isMobile(supportWeb: false) |
|
||||||
// ? attrs[Attribute.mobileAlignment] |
|
||||||
// : attrs[Attribute.alignment]); |
|
||||||
// final margin = (base.isMobile(supportWeb: false) |
|
||||||
// ? double.tryParse(Attribute.mobileMargin) |
|
||||||
// : double.tryParse(Attribute.margin)) ?? |
|
||||||
// 0.0; |
|
||||||
|
|
||||||
// imageSize = OptionalSize(width, height); |
|
||||||
// image = Padding( |
|
||||||
// padding: EdgeInsets.all(margin), |
|
||||||
// child: getImageWidgetByImageSource( |
|
||||||
// imageSource, |
|
||||||
// width: width, |
|
||||||
// height: height, |
|
||||||
// alignment: alignment, |
|
||||||
// imageProviderBuilder: configurations.imageProviderBuilder, |
|
||||||
// imageErrorWidgetBuilder: configurations.imageErrorWidgetBuilder, |
|
||||||
// ), |
|
||||||
// ); |
|
||||||
// } |
|
||||||
// } |
|
||||||
|
|
||||||
// if (imageSize == null) { |
|
||||||
// image = getImageWidgetByImageSource( |
|
||||||
// imageSource, |
|
||||||
// imageProviderBuilder: configurations.imageProviderBuilder, |
|
||||||
// imageErrorWidgetBuilder: configurations.imageErrorWidgetBuilder, |
|
||||||
// ); |
|
||||||
// imageSize = OptionalSize((image as Image).width, image.height); |
|
||||||
// } |
|
||||||
|
|
||||||
final imageSaverService = |
|
||||||
QuillSharedExtensionsConfigurations.get(context: context) |
|
||||||
.imageSaverService; |
|
||||||
return GestureDetector( |
|
||||||
onTap: configurations.onImageClicked ?? |
|
||||||
() => showDialog( |
|
||||||
context: context, |
|
||||||
builder: (_) { |
|
||||||
return QuillProvider.value( |
|
||||||
value: context.requireQuillProvider, |
|
||||||
child: FlutterQuillLocalizationsWidget( |
|
||||||
child: ImageOptionsMenu( |
|
||||||
controller: controller, |
|
||||||
configurations: configurations, |
|
||||||
imageSource: imageSource, |
|
||||||
imageSize: imageSize, |
|
||||||
isReadOnly: readOnly, |
|
||||||
imageSaverService: imageSaverService, |
|
||||||
), |
|
||||||
), |
|
||||||
); |
|
||||||
}, |
|
||||||
), |
|
||||||
child: Builder( |
|
||||||
builder: (context) { |
|
||||||
if (margin != null) { |
|
||||||
return Padding( |
|
||||||
padding: EdgeInsets.all(margin), |
|
||||||
child: image, |
|
||||||
); |
|
||||||
} |
|
||||||
return image; |
|
||||||
}, |
|
||||||
), |
|
||||||
); |
|
||||||
} |
|
||||||
} |
|
@ -1,58 +0,0 @@ |
|||||||
// import 'dart:convert' show jsonDecode, jsonEncode; |
|
||||||
|
|
||||||
// import 'package:flutter/widgets.dart'; |
|
||||||
// import 'package:flutter_inappwebview/flutter_inappwebview.dart'; |
|
||||||
// import 'package:flutter_quill/flutter_quill.dart'; |
|
||||||
// import 'package:meta/meta.dart' show experimental; |
|
||||||
|
|
||||||
// import '../../models/config/editor/webview.dart'; |
|
||||||
|
|
||||||
// @experimental |
|
||||||
// class QuillEditorWebViewBlockEmbed extends CustomBlockEmbed { |
|
||||||
// const QuillEditorWebViewBlockEmbed( |
|
||||||
// String value, |
|
||||||
// ) : super(webViewType, value); |
|
||||||
|
|
||||||
// factory QuillEditorWebViewBlockEmbed.fromDocument(Document document) => |
|
||||||
// QuillEditorWebViewBlockEmbed(jsonEncode(document.toDelta().toJson())); |
|
||||||
|
|
||||||
// static const String webViewType = 'webview'; |
|
||||||
|
|
||||||
// Document get document => Document.fromJson(jsonDecode(data)); |
|
||||||
// } |
|
||||||
|
|
||||||
// @experimental |
|
||||||
// class QuillEditorWebViewEmbedBuilder extends EmbedBuilder { |
|
||||||
// const QuillEditorWebViewEmbedBuilder({ |
|
||||||
// required this.configurations, |
|
||||||
// }); |
|
||||||
|
|
||||||
// @override |
|
||||||
// bool get expanded => false; |
|
||||||
|
|
||||||
// final QuillEditorWebViewEmbedConfigurations configurations; |
|
||||||
// @override |
|
||||||
// Widget build( |
|
||||||
// BuildContext context, |
|
||||||
// QuillController controller, |
|
||||||
// Embed node, |
|
||||||
// bool readOnly, |
|
||||||
// bool inline, |
|
||||||
// TextStyle textStyle, |
|
||||||
// ) { |
|
||||||
// final url = node.value.data as String; |
|
||||||
|
|
||||||
// return SizedBox( |
|
||||||
// width: double.infinity, |
|
||||||
// height: 200, |
|
||||||
// child: InAppWebView( |
|
||||||
// initialUrlRequest: URLRequest( |
|
||||||
// url: Uri.parse(url), |
|
||||||
// ), |
|
||||||
// ), |
|
||||||
// ); |
|
||||||
// } |
|
||||||
|
|
||||||
// @override |
|
||||||
// String get key => 'webview'; |
|
||||||
// } |
|
@ -1,201 +0,0 @@ |
|||||||
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.error, required this.method}); |
|
||||||
|
|
||||||
final String? error; |
|
||||||
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( |
|
||||||
error: null, |
|
||||||
method: SaveImageResultMethod.network, |
|
||||||
); |
|
||||||
} catch (e) { |
|
||||||
return SaveImageResult( |
|
||||||
error: e.toString(), |
|
||||||
method: SaveImageResultMethod.network, |
|
||||||
); |
|
||||||
} |
|
||||||
} |
|
||||||
try { |
|
||||||
await imageSaverService.saveLocalImage(imageUrl); |
|
||||||
return const SaveImageResult( |
|
||||||
error: null, |
|
||||||
method: SaveImageResultMethod.localStorage, |
|
||||||
); |
|
||||||
} catch (e) { |
|
||||||
return SaveImageResult( |
|
||||||
error: e.toString(), |
|
||||||
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, |
|
||||||
); |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,29 @@ |
|||||||
|
Map<String, String> parseCssString(String cssString) { |
||||||
|
final result = <String, String>{}; |
||||||
|
final declarations = cssString.split(';'); |
||||||
|
|
||||||
|
for (final declaration in declarations) { |
||||||
|
final parts = declaration.split(':'); |
||||||
|
if (parts.length == 2) { |
||||||
|
final property = parts[0].trim(); |
||||||
|
final value = parts[1].trim(); |
||||||
|
result[property] = value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
double? parseCssPropertyAsDouble(String value) { |
||||||
|
if (value.trim().isEmpty) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
final list = [ |
||||||
|
'px', |
||||||
|
// '%', 'vw', 'vh', 'em', 'rem' |
||||||
|
]; |
||||||
|
for (final element in list) { |
||||||
|
value = value.replaceFirst(element, ''); |
||||||
|
} |
||||||
|
return double.tryParse(value); |
||||||
|
} |
@ -0,0 +1,98 @@ |
|||||||
|
import 'package:flutter/foundation.dart' show immutable; |
||||||
|
import 'package:flutter/widgets.dart' show Alignment; |
||||||
|
import 'package:flutter_quill/extensions.dart'; |
||||||
|
import 'package:flutter_quill/flutter_quill.dart' show Attribute, Node; |
||||||
|
|
||||||
|
import 'element_shared_utils.dart'; |
||||||
|
|
||||||
|
/// Theses properties are not officialy supported by quill js |
||||||
|
/// but they are only used in all platforms other than web |
||||||
|
/// and they will be stored in css style property so quill js ignore them |
||||||
|
enum ExtraElementProperties { |
||||||
|
deletable, |
||||||
|
} |
||||||
|
|
||||||
|
( |
||||||
|
ElementSize elementSize, |
||||||
|
double? margin, |
||||||
|
Alignment alignment, |
||||||
|
) getElementAttributes( |
||||||
|
Node node, |
||||||
|
) { |
||||||
|
var elementSize = const ElementSize(null, null); |
||||||
|
var elementAlignment = Alignment.center; |
||||||
|
double? elementMargin; |
||||||
|
|
||||||
|
final heightValue = parseCssPropertyAsDouble( |
||||||
|
node.style.attributes[Attribute.height.key]?.value.toString() ?? ''); |
||||||
|
final widthValue = parseCssPropertyAsDouble( |
||||||
|
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) { |
||||||
|
// It css value as string but we will try to support it anyway |
||||||
|
|
||||||
|
final cssAttrs = parseCssString(cssStyle.value.toString()); |
||||||
|
|
||||||
|
// todo: This could be improved much better |
||||||
|
final cssHeightValue = |
||||||
|
parseCssPropertyAsDouble((cssAttrs[Attribute.height.key]) ?? ''); |
||||||
|
final cssWidthValue = |
||||||
|
parseCssPropertyAsDouble((cssAttrs[Attribute.width.key]) ?? ''); |
||||||
|
|
||||||
|
// cssHeightValue != null && elementSize.height == null |
||||||
|
if (cssHeightValue != null) { |
||||||
|
elementSize = elementSize.copyWith(height: cssHeightValue); |
||||||
|
} |
||||||
|
if (cssWidthValue != null) { |
||||||
|
elementSize = elementSize.copyWith(width: cssWidthValue); |
||||||
|
} |
||||||
|
|
||||||
|
elementAlignment = getAlignment(cssAttrs['alignment']); |
||||||
|
|
||||||
|
final margin = double.tryParse('margin'); |
||||||
|
if (margin != null) { |
||||||
|
elementMargin = margin; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return (elementSize, elementMargin, elementAlignment); |
||||||
|
} |
||||||
|
|
||||||
|
@immutable |
||||||
|
class ElementSize { |
||||||
|
const ElementSize( |
||||||
|
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; |
||||||
|
|
||||||
|
ElementSize copyWith({ |
||||||
|
double? width, |
||||||
|
double? height, |
||||||
|
}) { |
||||||
|
return ElementSize( |
||||||
|
width ?? this.width, |
||||||
|
height ?? this.height, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,89 @@ |
|||||||
|
import 'dart:io' show File; |
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart' show immutable; |
||||||
|
|
||||||
|
import '../embeds/widgets/image.dart'; |
||||||
|
import '../services/image_saver/s_image_saver.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.error, required this.method}); |
||||||
|
|
||||||
|
final String? error; |
||||||
|
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( |
||||||
|
error: null, |
||||||
|
method: SaveImageResultMethod.network, |
||||||
|
); |
||||||
|
} catch (e) { |
||||||
|
return SaveImageResult( |
||||||
|
error: e.toString(), |
||||||
|
method: SaveImageResultMethod.network, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
try { |
||||||
|
await imageSaverService.saveLocalImage(imageUrl); |
||||||
|
return const SaveImageResult( |
||||||
|
error: null, |
||||||
|
method: SaveImageResultMethod.localStorage, |
||||||
|
); |
||||||
|
} catch (e) { |
||||||
|
return SaveImageResult( |
||||||
|
error: e.toString(), |
||||||
|
method: SaveImageResultMethod.localStorage, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -1 +1,298 @@ |
|||||||
{} |
{ |
||||||
|
"ar": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"bg": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"bn": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"cs": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"da": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"de": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"en_US": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"es": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"fa": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"fr": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"he": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"hi": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"id": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"it": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"ja": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"ko": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"ms": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"nl": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"no": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"pl": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"pt": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"pt_BR": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"ru": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"sr": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"sw": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"tk": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"tr": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"uk": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"ur": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"vi": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"zh": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"zh_CN": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
], |
||||||
|
|
||||||
|
"zh_HK": [ |
||||||
|
"pickAPhotoFromYourGallery", |
||||||
|
"takeAPhotoUsingYourCamera", |
||||||
|
"pasteAPhotoUsingALink", |
||||||
|
"pickAVideoFromYourGallery", |
||||||
|
"recordAVideoUsingYourCamera", |
||||||
|
"pasteAVideoUsingALink" |
||||||
|
] |
||||||
|
} |
||||||
|
@ -1,3 +1,4 @@ |
|||||||
library flutter_quill.translations; |
library flutter_quill.translations; |
||||||
|
|
||||||
export 'src/l10n/extensions/localizations.dart'; |
export 'src/l10n/extensions/localizations.dart'; |
||||||
|
export 'src/l10n/widgets/localizations.dart'; |
||||||
|
Loading…
Reference in new issue