parent
f2aac2df29
commit
fbc633f01e
21 changed files with 767 additions and 321 deletions
@ -0,0 +1,21 @@ |
|||||||
|
import 'package:flutter/foundation.dart' show immutable; |
||||||
|
|
||||||
|
/// The configurations for the quill editor widget of flutter quill |
||||||
|
@immutable |
||||||
|
class QuillEditorConfigurations { |
||||||
|
const QuillEditorConfigurations({ |
||||||
|
this.placeholder, |
||||||
|
this.readOnly = false, |
||||||
|
}); |
||||||
|
|
||||||
|
/// The text placeholder in the quill editor |
||||||
|
final String? placeholder; |
||||||
|
|
||||||
|
/// Whether the text can be changed. |
||||||
|
/// |
||||||
|
/// When this is set to `true`, the text cannot be modified |
||||||
|
/// by any shortcut or keyboard operation. The text is still selectable. |
||||||
|
/// |
||||||
|
/// Defaults to `false`. Must not be `null`. |
||||||
|
final bool readOnly; |
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
import 'package:flutter/material.dart' show Color, Colors, Locale; |
||||||
|
import './editor/configurations.dart' show QuillEditorConfigurations; |
||||||
|
import './toolbar/configurations.dart' show QuillToolbarConfigurations; |
||||||
|
|
||||||
|
/// The shared configurations between [QuillEditorConfigurations] and |
||||||
|
/// [QuillToolbarConfigurations] so we don't duplicate things |
||||||
|
class QuillSharedConfigurations { |
||||||
|
const QuillSharedConfigurations({ |
||||||
|
this.dialogBarrierColor = Colors.black54, |
||||||
|
this.locale, |
||||||
|
}); |
||||||
|
|
||||||
|
// This is just example or showcase of this major update to make the library |
||||||
|
// more maintanable, flexible, and customizable |
||||||
|
/// The barrier color of the shown dialogs |
||||||
|
final Color dialogBarrierColor; |
||||||
|
|
||||||
|
/// The locale to use for the editor and toolbar, defaults to system locale |
||||||
|
/// More https://github.com/singerdmx/flutter-quill#translation |
||||||
|
final Locale? locale; |
||||||
|
} |
@ -0,0 +1,48 @@ |
|||||||
|
import 'package:flutter/foundation.dart' show VoidCallback, immutable; |
||||||
|
import 'package:flutter/widgets.dart' show IconData, Widget; |
||||||
|
|
||||||
|
import '../../../../../flutter_quill.dart' show QuillController, QuillProvider; |
||||||
|
import '../../../themes/quill_icon_theme.dart' show QuillIconTheme; |
||||||
|
import '../../quill_configurations.dart' show kDefaultIconSize; |
||||||
|
|
||||||
|
/// The [T] is the options for the button, usually should refresnce itself |
||||||
|
/// it's used in [childBuilder] so the developer can custmize this when using it |
||||||
|
/// The [I] is extra options for the button, usually for it's state |
||||||
|
@immutable |
||||||
|
class QuillToolbarBaseButtonOptions<T, I> { |
||||||
|
const QuillToolbarBaseButtonOptions({ |
||||||
|
this.iconData, |
||||||
|
this.globalIconSize = kDefaultIconSize, |
||||||
|
this.afterButtonPressed, |
||||||
|
this.tooltip, |
||||||
|
this.iconTheme, |
||||||
|
this.childBuilder, |
||||||
|
this.controller, |
||||||
|
}); |
||||||
|
|
||||||
|
/// By default it will use a Icon data from Icons which comes from material |
||||||
|
/// library, to change this, please pass a different value |
||||||
|
/// If there is no Icon in this button then pass null in the child class |
||||||
|
final IconData? iconData; |
||||||
|
|
||||||
|
/// To change the the icon size pass a different value, by default will be |
||||||
|
/// [kDefaultIconSize] |
||||||
|
/// this will be used for all the buttons but you can override this |
||||||
|
final double globalIconSize; |
||||||
|
|
||||||
|
/// To do extra logic after pressing the button |
||||||
|
final VoidCallback? afterButtonPressed; |
||||||
|
|
||||||
|
/// By default it will use the default tooltip which already localized |
||||||
|
final String? tooltip; |
||||||
|
|
||||||
|
/// Use custom theme |
||||||
|
final QuillIconTheme? iconTheme; |
||||||
|
|
||||||
|
/// If you want to dispaly a differnet widget based using a builder |
||||||
|
final Widget Function(T options, I extraOptions)? childBuilder; |
||||||
|
|
||||||
|
/// By default it will be from the one in [QuillProvider] |
||||||
|
/// To override it you must pass not null controller |
||||||
|
final QuillController? controller; |
||||||
|
} |
@ -0,0 +1,79 @@ |
|||||||
|
import 'package:flutter/foundation.dart' show immutable; |
||||||
|
import 'package:flutter/material.dart' show Colors, PopupMenuEntry; |
||||||
|
import 'package:flutter/widgets.dart' |
||||||
|
show |
||||||
|
Color, |
||||||
|
ValueChanged, |
||||||
|
EdgeInsetsGeometry, |
||||||
|
TextStyle, |
||||||
|
EdgeInsets, |
||||||
|
TextOverflow; |
||||||
|
|
||||||
|
import '../../../../../flutter_quill.dart'; |
||||||
|
|
||||||
|
@immutable |
||||||
|
class QuillToolbarFontFamilyButtonExtraOptions { |
||||||
|
const QuillToolbarFontFamilyButtonExtraOptions({ |
||||||
|
required this.defaultDisplayText, |
||||||
|
required this.currentValue, |
||||||
|
}); |
||||||
|
final String defaultDisplayText; |
||||||
|
final String currentValue; |
||||||
|
} |
||||||
|
|
||||||
|
class QuillToolbarFontFamilyButtonOptions extends QuillToolbarBaseButtonOptions< |
||||||
|
QuillToolbarFontFamilyButtonOptions, |
||||||
|
QuillToolbarFontFamilyButtonExtraOptions> { |
||||||
|
const QuillToolbarFontFamilyButtonOptions({ |
||||||
|
required this.attribute, |
||||||
|
this.rawItemsMap, |
||||||
|
super.controller, |
||||||
|
super.iconData, |
||||||
|
super.afterButtonPressed, |
||||||
|
super.tooltip, |
||||||
|
super.iconTheme, |
||||||
|
super.childBuilder, |
||||||
|
this.onSelected, |
||||||
|
this.padding, |
||||||
|
this.style, |
||||||
|
this.width, |
||||||
|
this.initialValue, |
||||||
|
this.labelOverflow = TextOverflow.visible, |
||||||
|
this.overrideTooltipByFontFamily = false, |
||||||
|
this.itemHeight, |
||||||
|
this.itemPadding, |
||||||
|
this.defaultItemColor = Colors.red, |
||||||
|
this.renderFontFamilies = true, |
||||||
|
@Deprecated('It is not required because of `rawItemsMap`') this.items, |
||||||
|
this.highlightElevation = 1, |
||||||
|
this.hoverElevation = 1, |
||||||
|
this.fillColor, |
||||||
|
this.iconSize, |
||||||
|
}); |
||||||
|
|
||||||
|
final Color? fillColor; |
||||||
|
final double hoverElevation; |
||||||
|
final double highlightElevation; |
||||||
|
@Deprecated('It is not required because of `rawItemsMap`') |
||||||
|
final List<PopupMenuEntry<String>>? items; |
||||||
|
|
||||||
|
/// By default it will be [fontFamilyValues] from [QuillToolbarConfigurations] |
||||||
|
/// You can override this if you want |
||||||
|
final Map<String, String>? rawItemsMap; |
||||||
|
final ValueChanged<String>? onSelected; |
||||||
|
final Attribute attribute; |
||||||
|
|
||||||
|
final EdgeInsetsGeometry? padding; |
||||||
|
final TextStyle? style; |
||||||
|
final double? width; |
||||||
|
final bool renderFontFamilies; |
||||||
|
final String? initialValue; |
||||||
|
final TextOverflow labelOverflow; |
||||||
|
final bool overrideTooltipByFontFamily; |
||||||
|
final double? itemHeight; |
||||||
|
final EdgeInsets? itemPadding; |
||||||
|
final Color? defaultItemColor; |
||||||
|
|
||||||
|
/// By default will use [globalIconSize] |
||||||
|
final double? iconSize; |
||||||
|
} |
@ -0,0 +1,39 @@ |
|||||||
|
import 'package:flutter/foundation.dart' show VoidCallback, immutable; |
||||||
|
|
||||||
|
import '../../../../../flutter_quill.dart'; |
||||||
|
|
||||||
|
@immutable |
||||||
|
class HistoryButtonExtraOptions { |
||||||
|
const HistoryButtonExtraOptions({ |
||||||
|
required this.onPressed, |
||||||
|
required this.canPressed, |
||||||
|
}); |
||||||
|
|
||||||
|
/// When the button pressed |
||||||
|
final VoidCallback onPressed; |
||||||
|
|
||||||
|
/// If it can redo or undo |
||||||
|
final bool canPressed; |
||||||
|
} |
||||||
|
|
||||||
|
@immutable |
||||||
|
class QuillToolbarHistoryButtonOptions extends QuillToolbarBaseButtonOptions< |
||||||
|
QuillToolbarHistoryButtonOptions, HistoryButtonExtraOptions> { |
||||||
|
const QuillToolbarHistoryButtonOptions({ |
||||||
|
required this.isUndo, |
||||||
|
super.iconData, |
||||||
|
super.controller, |
||||||
|
super.iconTheme, |
||||||
|
super.afterButtonPressed, |
||||||
|
super.tooltip, |
||||||
|
super.childBuilder, |
||||||
|
this.iconSize, |
||||||
|
}); |
||||||
|
|
||||||
|
/// If this true then it will be the undo button |
||||||
|
/// otherwise it will be redo |
||||||
|
final bool isUndo; |
||||||
|
|
||||||
|
/// By default will use [globalIconSize] |
||||||
|
final double? iconSize; |
||||||
|
} |
@ -0,0 +1,15 @@ |
|||||||
|
import 'package:flutter/foundation.dart' show immutable; |
||||||
|
import 'package:flutter/widgets.dart' show IconData; |
||||||
|
|
||||||
|
import 'base.dart'; |
||||||
|
|
||||||
|
@immutable |
||||||
|
class QuillToolbarToggleStyleButtonOptions |
||||||
|
extends QuillToolbarBaseButtonOptions { |
||||||
|
const QuillToolbarToggleStyleButtonOptions({ |
||||||
|
required this.iconData, |
||||||
|
}); |
||||||
|
|
||||||
|
@override |
||||||
|
final IconData iconData; |
||||||
|
} |
@ -0,0 +1,94 @@ |
|||||||
|
import 'package:flutter/foundation.dart' show immutable; |
||||||
|
import 'package:flutter/material.dart' show Icons; |
||||||
|
|
||||||
|
import '../../documents/attribute.dart'; |
||||||
|
import 'buttons/base.dart'; |
||||||
|
import 'buttons/font_family.dart'; |
||||||
|
import 'buttons/history.dart'; |
||||||
|
|
||||||
|
export './buttons/base.dart'; |
||||||
|
export './buttons/history.dart'; |
||||||
|
export './buttons/toggle_style.dart'; |
||||||
|
|
||||||
|
/// The default size of the icon of a button. |
||||||
|
const double kDefaultIconSize = 18; |
||||||
|
|
||||||
|
/// The default size for the toolbar (width, height) |
||||||
|
const double defaultToolbarSize = kDefaultIconSize * 2; |
||||||
|
|
||||||
|
/// The factor of how much larger the button is in relation to the icon. |
||||||
|
const double kIconButtonFactor = 1.77; |
||||||
|
|
||||||
|
/// The horizontal margin between the contents of each toolbar section. |
||||||
|
const double kToolbarSectionSpacing = 4; |
||||||
|
|
||||||
|
/// The configurations for the toolbar widget of flutter quill |
||||||
|
@immutable |
||||||
|
class QuillToolbarConfigurations { |
||||||
|
const QuillToolbarConfigurations({ |
||||||
|
this.buttonOptions = const QuillToolbarButtonOptions(), |
||||||
|
this.multiRowsDisplay = true, |
||||||
|
this.fontFamilyValues, |
||||||
|
|
||||||
|
/// By default it will calculated based on the [baseOptions] iconSize |
||||||
|
/// You can change it but the the change only apply if |
||||||
|
/// the [multiRowsDisplay] is false, if [multiRowsDisplay] then the value |
||||||
|
/// will be [kDefaultIconSize] * 2 |
||||||
|
double? toolbarSize, |
||||||
|
}) : _toolbarSize = toolbarSize; |
||||||
|
|
||||||
|
final double? _toolbarSize; |
||||||
|
|
||||||
|
/// The toolbar size, by default it will be `baseButtonOptions.iconSize * 2` |
||||||
|
double get toolbarSize { |
||||||
|
final alternativeToolbarSize = _toolbarSize; |
||||||
|
if (alternativeToolbarSize != null) { |
||||||
|
return alternativeToolbarSize; |
||||||
|
} |
||||||
|
return buttonOptions.baseButtonOptions.globalIconSize * 2; |
||||||
|
} |
||||||
|
|
||||||
|
/// If you want change spesefic buttons or all of them |
||||||
|
/// then you came to the right place |
||||||
|
final QuillToolbarButtonOptions buttonOptions; |
||||||
|
final bool multiRowsDisplay; |
||||||
|
|
||||||
|
/// By default will be final |
||||||
|
/// ``` |
||||||
|
/// { |
||||||
|
/// 'Sans Serif': 'sans-serif', |
||||||
|
/// 'Serif': 'serif', |
||||||
|
/// 'Monospace': 'monospace', |
||||||
|
/// 'Ibarra Real Nova': 'ibarra-real-nova', |
||||||
|
/// 'SquarePeg': 'square-peg', |
||||||
|
/// 'Nunito': 'nunito', |
||||||
|
/// 'Pacifico': 'pacifico', |
||||||
|
/// 'Roboto Mono': 'roboto-mono', |
||||||
|
/// 'Clear'.i18n: 'Clear' |
||||||
|
/// }; |
||||||
|
/// ``` |
||||||
|
final Map<String, String>? fontFamilyValues; |
||||||
|
} |
||||||
|
|
||||||
|
/// The configurations for the buttons of the toolbar widget of flutter quill |
||||||
|
@immutable |
||||||
|
class QuillToolbarButtonOptions { |
||||||
|
const QuillToolbarButtonOptions({ |
||||||
|
this.baseButtonOptions = const QuillToolbarBaseButtonOptions(), |
||||||
|
this.undoHistoryButtonOptions = const QuillToolbarHistoryButtonOptions( |
||||||
|
isUndo: true, |
||||||
|
), |
||||||
|
this.redoHistoryButtonOptions = const QuillToolbarHistoryButtonOptions( |
||||||
|
isUndo: false, |
||||||
|
), |
||||||
|
this.fontFamilyButtonOptions = const QuillToolbarFontFamilyButtonOptions( |
||||||
|
attribute: Attribute.font, |
||||||
|
), |
||||||
|
}); |
||||||
|
|
||||||
|
/// The base configurations for all the buttons |
||||||
|
final QuillToolbarBaseButtonOptions baseButtonOptions; |
||||||
|
final QuillToolbarHistoryButtonOptions undoHistoryButtonOptions; |
||||||
|
final QuillToolbarHistoryButtonOptions redoHistoryButtonOptions; |
||||||
|
final QuillToolbarFontFamilyButtonOptions fontFamilyButtonOptions; |
||||||
|
} |
@ -0,0 +1,17 @@ |
|||||||
|
import 'package:flutter/widgets.dart' show BuildContext; |
||||||
|
|
||||||
|
import '../../../flutter_quill.dart' show QuillController, QuillProvider; |
||||||
|
import 'build_context.dart'; |
||||||
|
|
||||||
|
extension QuillControllerExt on QuillController? { |
||||||
|
/// Simple logic to use the current passed controller if not null |
||||||
|
/// if null then we will have to use the default one from [QuillProvider] |
||||||
|
/// using the [context] |
||||||
|
QuillController notNull(BuildContext context) { |
||||||
|
final controller = this; |
||||||
|
if (controller != null) { |
||||||
|
return controller; |
||||||
|
} |
||||||
|
return context.requireQuillController; |
||||||
|
} |
||||||
|
} |
@ -1,89 +1,138 @@ |
|||||||
import 'package:flutter/material.dart'; |
import 'package:flutter/material.dart'; |
||||||
|
|
||||||
import '../../models/themes/quill_icon_theme.dart'; |
import '../../../translations.dart'; |
||||||
|
import '../../utils/extensions/build_context.dart'; |
||||||
|
import '../../utils/extensions/quill_controller.dart'; |
||||||
import '../controller.dart'; |
import '../controller.dart'; |
||||||
import '../toolbar.dart'; |
import '../toolbar.dart'; |
||||||
|
|
||||||
class HistoryButton extends StatefulWidget { |
class QuillToolbarHistoryButton extends StatefulWidget { |
||||||
const HistoryButton({ |
const QuillToolbarHistoryButton({ |
||||||
required this.icon, |
required this.options, |
||||||
required this.controller, |
super.key, |
||||||
required this.undo, |
}); |
||||||
this.iconSize = kDefaultIconSize, |
|
||||||
this.iconTheme, |
final QuillToolbarHistoryButtonOptions options; |
||||||
this.afterButtonPressed, |
|
||||||
this.tooltip, |
|
||||||
Key? key, |
|
||||||
}) : super(key: key); |
|
||||||
|
|
||||||
final IconData icon; |
|
||||||
final double iconSize; |
|
||||||
final bool undo; |
|
||||||
final QuillController controller; |
|
||||||
final QuillIconTheme? iconTheme; |
|
||||||
final VoidCallback? afterButtonPressed; |
|
||||||
final String? tooltip; |
|
||||||
|
|
||||||
@override |
@override |
||||||
_HistoryButtonState createState() => _HistoryButtonState(); |
_QuillToolbarHistoryButtonState createState() => |
||||||
|
_QuillToolbarHistoryButtonState(); |
||||||
} |
} |
||||||
|
|
||||||
class _HistoryButtonState extends State<HistoryButton> { |
class _QuillToolbarHistoryButtonState extends State<QuillToolbarHistoryButton> { |
||||||
Color? _iconColor; |
|
||||||
late ThemeData theme; |
late ThemeData theme; |
||||||
|
var _canPressed = false; |
||||||
|
|
||||||
|
QuillToolbarHistoryButtonOptions get options { |
||||||
|
return widget.options; |
||||||
|
} |
||||||
|
|
||||||
|
QuillController get controller { |
||||||
|
return options.controller.notNull(context); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
void initState() { |
||||||
|
super.initState(); |
||||||
|
_listenForChanges(); // Listen for changes and change it |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> _listenForChanges() async { |
||||||
|
await Future.delayed(Duration.zero); // Wait for the widget to built |
||||||
|
_updateCanPressed(); // Set the init state |
||||||
|
|
||||||
|
// Listen for changes and change it |
||||||
|
controller.changes.listen((event) async { |
||||||
|
_updateCanPressed(); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
@override |
@override |
||||||
Widget build(BuildContext context) { |
Widget build(BuildContext context) { |
||||||
theme = Theme.of(context); |
theme = Theme.of(context); |
||||||
_setIconColor(); |
|
||||||
|
|
||||||
final fillColor = |
final baseButtonConfigurations = |
||||||
widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor; |
context.requireQuillToolbarBaseButtonOptions; |
||||||
widget.controller.changes.listen((event) async { |
final tooltip = options.tooltip ?? |
||||||
_setIconColor(); |
baseButtonConfigurations.tooltip ?? |
||||||
}); |
(options.isUndo ? 'Undo'.i18n : 'Redo'.i18n); |
||||||
|
final iconData = options.iconData ?? |
||||||
|
baseButtonConfigurations.iconData ?? |
||||||
|
(options.isUndo ? Icons.undo_outlined : Icons.redo_outlined); |
||||||
|
final childBuilder = |
||||||
|
options.childBuilder ?? baseButtonConfigurations.childBuilder; |
||||||
|
final iconSize = options.iconSize ?? |
||||||
|
context.requireQuillToolbarBaseButtonOptions.globalIconSize; |
||||||
|
final iconTheme = options.iconTheme ?? baseButtonConfigurations.iconTheme; |
||||||
|
|
||||||
|
final fillColor = iconTheme?.iconUnselectedFillColor ?? theme.canvasColor; |
||||||
|
|
||||||
|
final afterButtonPressed = options.afterButtonPressed ?? |
||||||
|
baseButtonConfigurations.afterButtonPressed; |
||||||
|
|
||||||
|
if (childBuilder != null) { |
||||||
|
return childBuilder( |
||||||
|
QuillToolbarHistoryButtonOptions( |
||||||
|
isUndo: options.isUndo, |
||||||
|
afterButtonPressed: afterButtonPressed, |
||||||
|
controller: controller, |
||||||
|
iconData: iconData, |
||||||
|
iconSize: iconSize, |
||||||
|
iconTheme: iconTheme, |
||||||
|
tooltip: tooltip, |
||||||
|
), |
||||||
|
HistoryButtonExtraOptions( |
||||||
|
onPressed: () { |
||||||
|
_updateHistory(); |
||||||
|
afterButtonPressed?.call(); |
||||||
|
}, |
||||||
|
canPressed: _canPressed, |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
return QuillIconButton( |
return QuillIconButton( |
||||||
tooltip: widget.tooltip, |
tooltip: tooltip, |
||||||
highlightElevation: 0, |
highlightElevation: 0, |
||||||
hoverElevation: 0, |
hoverElevation: 0, |
||||||
size: widget.iconSize * kIconButtonFactor, |
size: iconSize * kIconButtonFactor, |
||||||
icon: Icon(widget.icon, size: widget.iconSize, color: _iconColor), |
icon: Icon( |
||||||
|
iconData, |
||||||
|
size: iconSize, |
||||||
|
color: _canPressed |
||||||
|
? iconTheme?.iconUnselectedColor ?? theme.iconTheme.color |
||||||
|
: iconTheme?.disabledIconColor ?? theme.disabledColor, |
||||||
|
), |
||||||
fillColor: fillColor, |
fillColor: fillColor, |
||||||
borderRadius: widget.iconTheme?.borderRadius ?? 2, |
borderRadius: iconTheme?.borderRadius ?? 2, |
||||||
onPressed: _changeHistory, |
onPressed: _updateHistory, |
||||||
afterPressed: widget.afterButtonPressed, |
afterPressed: afterButtonPressed, |
||||||
); |
); |
||||||
} |
} |
||||||
|
|
||||||
void _setIconColor() { |
void _updateCanPressed() { |
||||||
if (!mounted) return; |
if (!mounted) return; |
||||||
|
|
||||||
if (widget.undo) { |
setState(() { |
||||||
setState(() { |
if (options.isUndo) { |
||||||
_iconColor = widget.controller.hasUndo |
_canPressed = controller.hasUndo; |
||||||
? widget.iconTheme?.iconUnselectedColor ?? theme.iconTheme.color |
return; |
||||||
: widget.iconTheme?.disabledIconColor ?? theme.disabledColor; |
} |
||||||
}); |
_canPressed = controller.hasRedo; |
||||||
} else { |
}); |
||||||
setState(() { |
|
||||||
_iconColor = widget.controller.hasRedo |
|
||||||
? widget.iconTheme?.iconUnselectedColor ?? theme.iconTheme.color |
|
||||||
: widget.iconTheme?.disabledIconColor ?? theme.disabledColor; |
|
||||||
}); |
|
||||||
} |
|
||||||
} |
} |
||||||
|
|
||||||
void _changeHistory() { |
void _updateHistory() { |
||||||
if (widget.undo) { |
if (options.isUndo) { |
||||||
if (widget.controller.hasUndo) { |
if (controller.hasUndo) { |
||||||
widget.controller.undo(); |
controller.undo(); |
||||||
} |
|
||||||
} else { |
|
||||||
if (widget.controller.hasRedo) { |
|
||||||
widget.controller.redo(); |
|
||||||
} |
} |
||||||
|
// _updateCanPressed(); // We are already listeneting for the changes |
||||||
|
return; |
||||||
} |
} |
||||||
|
|
||||||
_setIconColor(); |
if (controller.hasRedo) { |
||||||
|
controller.redo(); |
||||||
|
// _updateCanPressed(); // We are already listeneting for the changes |
||||||
|
} |
||||||
} |
} |
||||||
} |
} |
||||||
|
Loading…
Reference in new issue