mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Lottie: optimization
This commit is contained in:
parent
9fcef12d55
commit
58a1caf9df
@ -256,9 +256,10 @@ func processAnimationFolderAsync(basePath: String, path: String, stopOnFailure:
|
|||||||
return await processAnimationFolderItems(items: items, countPerBucket: 1, stopOnFailure: stopOnFailure, process: process)
|
return await processAnimationFolderItems(items: items, countPerBucket: 1, stopOnFailure: stopOnFailure, process: process)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(iOS 13.0, *)
|
||||||
func processAnimationFolderParallel(basePath: String, path: String, stopOnFailure: Bool, process: @escaping (String, String, Bool) async -> Bool) async -> Bool {
|
func processAnimationFolderParallel(basePath: String, path: String, stopOnFailure: Bool, process: @escaping (String, String, Bool) async -> Bool) async -> Bool {
|
||||||
let items = buildAnimationFolderItems(basePath: basePath, path: path)
|
let items = buildAnimationFolderItems(basePath: basePath, path: path)
|
||||||
return await processAnimationFolderItems(items: items, countPerBucket: 16, stopOnFailure: stopOnFailure, process: process)
|
return await processAnimationFolderItemsParallel(items: items, stopOnFailure: stopOnFailure, process: process)
|
||||||
}
|
}
|
||||||
|
|
||||||
func cacheReferenceFolderPath(baseCachePath: String, width: Int, name: String) -> String {
|
func cacheReferenceFolderPath(baseCachePath: String, width: Int, name: String) -> String {
|
||||||
|
@ -79,7 +79,7 @@ private final class ReferenceCompareTest {
|
|||||||
|
|
||||||
var continueFromName: String? = "5138957708585599529.json"
|
var continueFromName: String? = "5138957708585599529.json"
|
||||||
|
|
||||||
let _ = await processAnimationFolderParallel(basePath: bundlePath, path: "", stopOnFailure: true, process: { path, name, alwaysDraw in
|
let _ = await processAnimationFolderAsync(basePath: bundlePath, path: "", stopOnFailure: true, process: { path, name, alwaysDraw in
|
||||||
if let continueFromNameValue = continueFromName {
|
if let continueFromNameValue = continueFromName {
|
||||||
if continueFromNameValue == name {
|
if continueFromNameValue == name {
|
||||||
continueFromName = nil
|
continueFromName = nil
|
||||||
|
@ -89,7 +89,13 @@ public:
|
|||||||
|
|
||||||
void displayWithFrame(double frame, bool forceUpdates) {
|
void displayWithFrame(double frame, bool forceUpdates) {
|
||||||
_transformNode->updateTree(frame, forceUpdates);
|
_transformNode->updateTree(frame, forceUpdates);
|
||||||
|
|
||||||
bool layerVisible = isInRangeOrEqual(frame, _inFrame, _outFrame);
|
bool layerVisible = isInRangeOrEqual(frame, _inFrame, _outFrame);
|
||||||
|
|
||||||
|
_contentsLayer->setTransform(_transformNode->globalTransform());
|
||||||
|
_contentsLayer->setOpacity(_transformNode->opacity());
|
||||||
|
_contentsLayer->setIsHidden(!layerVisible);
|
||||||
|
|
||||||
/// Only update contents if current time is within the layers time bounds.
|
/// Only update contents if current time is within the layers time bounds.
|
||||||
if (layerVisible) {
|
if (layerVisible) {
|
||||||
displayContentsWithFrame(frame, forceUpdates);
|
displayContentsWithFrame(frame, forceUpdates);
|
||||||
@ -97,9 +103,6 @@ public:
|
|||||||
_maskLayer->updateWithFrame(frame, forceUpdates);
|
_maskLayer->updateWithFrame(frame, forceUpdates);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_contentsLayer->setTransform(_transformNode->globalTransform());
|
|
||||||
_contentsLayer->setOpacity(_transformNode->opacity());
|
|
||||||
_contentsLayer->setIsHidden(!layerVisible);
|
|
||||||
|
|
||||||
if (const auto delegate = _layerDelegate.lock()) {
|
if (const auto delegate = _layerDelegate.lock()) {
|
||||||
delegate->frameUpdated(frame);
|
delegate->frameUpdated(frame);
|
||||||
@ -168,6 +171,9 @@ public:
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
virtual void updateRenderTree() {
|
||||||
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
std::shared_ptr<LayerTransformNode> const transformNode() const {
|
std::shared_ptr<LayerTransformNode> const transformNode() const {
|
||||||
return _transformNode;
|
return _transformNode;
|
||||||
@ -193,8 +199,6 @@ private:
|
|||||||
|
|
||||||
std::string _keypathName;
|
std::string _keypathName;
|
||||||
|
|
||||||
//std::shared_ptr<RenderTreeNode> _renderTree;
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
virtual bool isImageCompositionLayer() const {
|
virtual bool isImageCompositionLayer() const {
|
||||||
return false;
|
return false;
|
||||||
|
@ -101,6 +101,34 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
virtual std::shared_ptr<RenderTreeNode> renderTreeNode() override {
|
virtual std::shared_ptr<RenderTreeNode> renderTreeNode() override {
|
||||||
|
if (!_renderTreeNode) {
|
||||||
|
_renderTreeNode = std::make_shared<RenderTreeNode>(
|
||||||
|
CGRect(0.0, 0.0, 0.0, 0.0),
|
||||||
|
Vector2D(0.0, 0.0),
|
||||||
|
CATransform3D::identity(),
|
||||||
|
1.0,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
nullptr,
|
||||||
|
std::vector<std::shared_ptr<RenderTreeNode>>(),
|
||||||
|
nullptr,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
_contentsTreeNode = std::make_shared<RenderTreeNode>(
|
||||||
|
CGRect(0.0, 0.0, 0.0, 0.0),
|
||||||
|
Vector2D(0.0, 0.0),
|
||||||
|
CATransform3D::identity(),
|
||||||
|
1.0,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
nullptr,
|
||||||
|
std::vector<std::shared_ptr<RenderTreeNode>>(),
|
||||||
|
nullptr,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (_contentsLayer->isHidden()) {
|
if (_contentsLayer->isHidden()) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@ -121,7 +149,17 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::shared_ptr<RenderTreeNode>> subnodes;
|
std::vector<std::shared_ptr<RenderTreeNode>> subnodes;
|
||||||
subnodes.push_back(std::make_shared<RenderTreeNode>(
|
|
||||||
|
_contentsTreeNode->_bounds = _contentsLayer->bounds();
|
||||||
|
_contentsTreeNode->_position = _contentsLayer->position();
|
||||||
|
_contentsTreeNode->_transform = _contentsLayer->transform();
|
||||||
|
_contentsTreeNode->_alpha = _contentsLayer->opacity();
|
||||||
|
_contentsTreeNode->_masksToBounds = _contentsLayer->masksToBounds();
|
||||||
|
_contentsTreeNode->_isHidden = _contentsLayer->isHidden();
|
||||||
|
_contentsTreeNode->_subnodes = renderTreeValue;
|
||||||
|
|
||||||
|
subnodes.push_back(_contentsTreeNode);
|
||||||
|
/*subnodes.push_back(std::make_shared<RenderTreeNode>(
|
||||||
_contentsLayer->bounds(),
|
_contentsLayer->bounds(),
|
||||||
_contentsLayer->position(),
|
_contentsLayer->position(),
|
||||||
_contentsLayer->transform(),
|
_contentsLayer->transform(),
|
||||||
@ -132,7 +170,7 @@ public:
|
|||||||
renderTreeValue,
|
renderTreeValue,
|
||||||
nullptr,
|
nullptr,
|
||||||
false
|
false
|
||||||
));
|
));*/
|
||||||
|
|
||||||
assert(opacity() == 1.0);
|
assert(opacity() == 1.0);
|
||||||
assert(!isHidden());
|
assert(!isHidden());
|
||||||
@ -140,18 +178,17 @@ public:
|
|||||||
assert(transform().isIdentity());
|
assert(transform().isIdentity());
|
||||||
assert(position() == Vector2D::Zero());
|
assert(position() == Vector2D::Zero());
|
||||||
|
|
||||||
return std::make_shared<RenderTreeNode>(
|
_renderTreeNode->_bounds = bounds();
|
||||||
bounds(),
|
_renderTreeNode->_position = position();
|
||||||
position(),
|
_renderTreeNode->_transform = transform();
|
||||||
transform(),
|
_renderTreeNode->_alpha = opacity();
|
||||||
opacity(),
|
_renderTreeNode->_masksToBounds = masksToBounds();
|
||||||
masksToBounds(),
|
_renderTreeNode->_isHidden = isHidden();
|
||||||
isHidden(),
|
_renderTreeNode->_subnodes = subnodes;
|
||||||
nullptr,
|
_renderTreeNode->_mask = maskNode;
|
||||||
subnodes,
|
_renderTreeNode->_invertMask = invertMask;
|
||||||
maskNode,
|
|
||||||
invertMask
|
return _renderTreeNode;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<RenderTreeNode> renderTree() {
|
std::shared_ptr<RenderTreeNode> renderTree() {
|
||||||
@ -188,11 +225,33 @@ public:
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
virtual void updateRenderTree() override {
|
||||||
|
if (_matteLayer) {
|
||||||
|
_matteLayer->updateRenderTree();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto &animationLayer : _animationLayers) {
|
||||||
|
bool found = false;
|
||||||
|
for (const auto &sublayer : contentsLayer()->sublayers()) {
|
||||||
|
if (animationLayer == sublayer) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found) {
|
||||||
|
animationLayer->updateRenderTree();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
double _frameRate = 0.0;
|
double _frameRate = 0.0;
|
||||||
std::shared_ptr<NodeProperty<Vector1D>> _remappingNode;
|
std::shared_ptr<NodeProperty<Vector1D>> _remappingNode;
|
||||||
|
|
||||||
std::vector<std::shared_ptr<CompositionLayer>> _animationLayers;
|
std::vector<std::shared_ptr<CompositionLayer>> _animationLayers;
|
||||||
|
|
||||||
|
std::shared_ptr<RenderTreeNode> _renderTreeNode;
|
||||||
|
std::shared_ptr<RenderTreeNode> _contentsTreeNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1284,16 +1284,46 @@ CompositionLayer(solidLayer, Vector2D::Zero()) {
|
|||||||
void ShapeCompositionLayer::displayContentsWithFrame(double frame, bool forceUpdates) {
|
void ShapeCompositionLayer::displayContentsWithFrame(double frame, bool forceUpdates) {
|
||||||
_frameTime = frame;
|
_frameTime = frame;
|
||||||
_frameTimeInitialized = true;
|
_frameTimeInitialized = true;
|
||||||
|
|
||||||
_contentTree->itemTree->renderChildren(_frameTime, std::nullopt);
|
_contentTree->itemTree->renderChildren(_frameTime, std::nullopt);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<RenderTreeNode> ShapeCompositionLayer::renderTreeNode() {
|
std::shared_ptr<RenderTreeNode> ShapeCompositionLayer::renderTreeNode() {
|
||||||
if (_contentsLayer->isHidden()) {
|
if (!_frameTimeInitialized) {
|
||||||
return nullptr;
|
_frameTime = 0.0;
|
||||||
|
_frameTimeInitialized = true;
|
||||||
|
_contentTree->itemTree->renderChildren(_frameTime, std::nullopt);
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(_frameTimeInitialized);
|
if (!_renderTreeNode) {
|
||||||
|
_renderTreeNode = std::make_shared<RenderTreeNode>(
|
||||||
|
CGRect(0.0, 0.0, 0.0, 0.0),
|
||||||
|
Vector2D(0.0, 0.0),
|
||||||
|
CATransform3D::identity(),
|
||||||
|
1.0,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
nullptr,
|
||||||
|
std::vector<std::shared_ptr<RenderTreeNode>>(),
|
||||||
|
nullptr,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
std::vector<std::shared_ptr<RenderTreeNode>> renderTreeValue;
|
||||||
|
renderTreeValue.push_back(_contentTree->itemTree->renderTree());
|
||||||
|
|
||||||
|
_contentsTreeNode = std::make_shared<RenderTreeNode>(
|
||||||
|
CGRect(0.0, 0.0, 0.0, 0.0),
|
||||||
|
Vector2D(0.0, 0.0),
|
||||||
|
CATransform3D::identity(),
|
||||||
|
1.0,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
nullptr,
|
||||||
|
renderTreeValue,
|
||||||
|
nullptr,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
std::shared_ptr<RenderTreeNode> maskNode;
|
std::shared_ptr<RenderTreeNode> maskNode;
|
||||||
bool invertMask = false;
|
bool invertMask = false;
|
||||||
@ -1304,45 +1334,39 @@ std::shared_ptr<RenderTreeNode> ShapeCompositionLayer::renderTreeNode() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::shared_ptr<RenderTreeNode>> renderTreeValue;
|
_contentsTreeNode->_bounds = _contentsLayer->bounds();
|
||||||
renderTreeValue.push_back(_contentTree->itemTree->renderTree());
|
_contentsTreeNode->_position = _contentsLayer->position();
|
||||||
|
_contentsTreeNode->_transform = _contentsLayer->transform();
|
||||||
|
_contentsTreeNode->_alpha = _contentsLayer->opacity();
|
||||||
|
_contentsTreeNode->_masksToBounds = _contentsLayer->masksToBounds();
|
||||||
|
_contentsTreeNode->_isHidden = _contentsLayer->isHidden();
|
||||||
|
|
||||||
std::vector<std::shared_ptr<RenderTreeNode>> subnodes;
|
std::vector<std::shared_ptr<RenderTreeNode>> subnodes;
|
||||||
subnodes.push_back(std::make_shared<RenderTreeNode>(
|
subnodes.push_back(_contentsTreeNode);
|
||||||
_contentsLayer->bounds(),
|
|
||||||
_contentsLayer->position(),
|
|
||||||
_contentsLayer->transform(),
|
|
||||||
_contentsLayer->opacity(),
|
|
||||||
_contentsLayer->masksToBounds(),
|
|
||||||
_contentsLayer->isHidden(),
|
|
||||||
nullptr,
|
|
||||||
renderTreeValue,
|
|
||||||
nullptr,
|
|
||||||
false
|
|
||||||
));
|
|
||||||
|
|
||||||
assert(position() == Vector2D::Zero());
|
assert(position() == Vector2D::Zero());
|
||||||
assert(transform().isIdentity());
|
assert(transform().isIdentity());
|
||||||
assert(opacity() == 1.0);
|
assert(opacity() == 1.0);
|
||||||
assert(!masksToBounds());
|
assert(!masksToBounds());
|
||||||
assert(!isHidden());
|
assert(!isHidden());
|
||||||
|
|
||||||
assert(_contentsLayer->bounds() == CGRect(0.0, 0.0, 0.0, 0.0));
|
assert(_contentsLayer->bounds() == CGRect(0.0, 0.0, 0.0, 0.0));
|
||||||
assert(_contentsLayer->position() == Vector2D::Zero());
|
assert(_contentsLayer->position() == Vector2D::Zero());
|
||||||
assert(!_contentsLayer->masksToBounds());
|
assert(!_contentsLayer->masksToBounds());
|
||||||
|
|
||||||
return std::make_shared<RenderTreeNode>(
|
_renderTreeNode->_bounds = bounds();
|
||||||
bounds(),
|
_renderTreeNode->_position = position();
|
||||||
position(),
|
_renderTreeNode->_transform = transform();
|
||||||
transform(),
|
_renderTreeNode->_alpha = opacity();
|
||||||
opacity(),
|
_renderTreeNode->_masksToBounds = masksToBounds();
|
||||||
masksToBounds(),
|
_renderTreeNode->_isHidden = isHidden();
|
||||||
isHidden(),
|
_renderTreeNode->_subnodes = subnodes;
|
||||||
nullptr,
|
_renderTreeNode->_mask = maskNode;
|
||||||
subnodes,
|
_renderTreeNode->_invertMask = invertMask;
|
||||||
maskNode,
|
|
||||||
invertMask
|
return _renderTreeNode;
|
||||||
);
|
}
|
||||||
|
|
||||||
|
void ShapeCompositionLayer::updateRenderTree() {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -18,12 +18,16 @@ public:
|
|||||||
|
|
||||||
virtual void displayContentsWithFrame(double frame, bool forceUpdates) override;
|
virtual void displayContentsWithFrame(double frame, bool forceUpdates) override;
|
||||||
virtual std::shared_ptr<RenderTreeNode> renderTreeNode() override;
|
virtual std::shared_ptr<RenderTreeNode> renderTreeNode() override;
|
||||||
|
virtual void updateRenderTree() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::shared_ptr<ShapeLayerPresentationTree> _contentTree;
|
std::shared_ptr<ShapeLayerPresentationTree> _contentTree;
|
||||||
|
|
||||||
AnimationFrameTime _frameTime = 0.0;
|
AnimationFrameTime _frameTime = 0.0;
|
||||||
bool _frameTimeInitialized = false;
|
bool _frameTimeInitialized = false;
|
||||||
|
|
||||||
|
std::shared_ptr<RenderTreeNode> _renderTreeNode;
|
||||||
|
std::shared_ptr<RenderTreeNode> _contentsTreeNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -23,12 +23,6 @@ public:
|
|||||||
_textProvider = textProvider;
|
_textProvider = textProvider;
|
||||||
_fontProvider = fontProvider;
|
_fontProvider = fontProvider;
|
||||||
|
|
||||||
//_contentsLayer->addSublayer(_textLayer);
|
|
||||||
|
|
||||||
assert(false);
|
|
||||||
//self.textLayer.masksToBounds = false
|
|
||||||
//self.textLayer.isGeometryFlipped = true
|
|
||||||
|
|
||||||
if (_rootNode) {
|
if (_rootNode) {
|
||||||
_childKeypaths.push_back(rootNode);
|
_childKeypaths.push_back(rootNode);
|
||||||
}
|
}
|
||||||
@ -67,42 +61,6 @@ public:
|
|||||||
if (_rootNode) {
|
if (_rootNode) {
|
||||||
_rootNode->rebuildOutputs(frame);
|
_rootNode->rebuildOutputs(frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(false);
|
|
||||||
/*// Get Text Attributes
|
|
||||||
let text = textDocument.value(frame: frame) as! TextDocument
|
|
||||||
let strokeColor = rootNode?.textOutputNode.strokeColor ?? text.strokeColorData?.cgColorValue
|
|
||||||
let strokeWidth = rootNode?.textOutputNode.strokeWidth ?? CGFloat(text.strokeWidth ?? 0)
|
|
||||||
let tracking = (CGFloat(text.fontSize) * (rootNode?.textOutputNode.tracking ?? CGFloat(text.tracking))) / 1000.0
|
|
||||||
let matrix = rootNode?.textOutputNode.xform ?? CATransform3DIdentity
|
|
||||||
let textString = textProvider.textFor(keypathName: keypathName, sourceText: text.text)
|
|
||||||
let ctFont = fontProvider.fontFor(family: text.fontFamily, size: CGFloat(text.fontSize))
|
|
||||||
|
|
||||||
// Set all of the text layer options
|
|
||||||
textLayer.text = textString
|
|
||||||
textLayer.font = ctFont
|
|
||||||
textLayer.alignment = text.justification.textAlignment
|
|
||||||
textLayer.lineHeight = CGFloat(text.lineHeight)
|
|
||||||
textLayer.tracking = tracking
|
|
||||||
|
|
||||||
if let fillColor = rootNode?.textOutputNode.fillColor {
|
|
||||||
textLayer.fillColor = fillColor
|
|
||||||
} else if let fillColor = text.fillColorData?.cgColorValue {
|
|
||||||
textLayer.fillColor = fillColor
|
|
||||||
} else {
|
|
||||||
textLayer.fillColor = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
textLayer.preferredSize = text.textFrameSize?.sizeValue
|
|
||||||
textLayer.strokeOnTop = text.strokeOverFill ?? false
|
|
||||||
textLayer.strokeWidth = strokeWidth
|
|
||||||
textLayer.strokeColor = strokeColor
|
|
||||||
textLayer.sizeToFit()
|
|
||||||
|
|
||||||
textLayer.opacity = Float(rootNode?.textOutputNode.opacity ?? 1)
|
|
||||||
textLayer.transform = CATransform3DIdentity
|
|
||||||
textLayer.position = text.textFramePosition?.pointValue ?? CGPoint.zero
|
|
||||||
textLayer.transform = matrix*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@ -114,7 +72,6 @@ private:
|
|||||||
std::shared_ptr<TextAnimatorNode> _rootNode;
|
std::shared_ptr<TextAnimatorNode> _rootNode;
|
||||||
std::shared_ptr<KeyframeInterpolator<TextDocument>> _textDocument;
|
std::shared_ptr<KeyframeInterpolator<TextDocument>> _textDocument;
|
||||||
|
|
||||||
//std::shared_ptr<CoreTextRenderLayer> _textLayer;
|
|
||||||
std::shared_ptr<AnimationTextProvider> _textProvider;
|
std::shared_ptr<AnimationTextProvider> _textProvider;
|
||||||
std::shared_ptr<AnimationFontProvider> _fontProvider;
|
std::shared_ptr<AnimationFontProvider> _fontProvider;
|
||||||
};
|
};
|
||||||
|
@ -250,6 +250,21 @@ public:
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
virtual void updateRenderTree() {
|
||||||
|
for (const auto &animationLayer : _animationLayers) {
|
||||||
|
bool found = false;
|
||||||
|
for (const auto &sublayer : sublayers()) {
|
||||||
|
if (animationLayer == sublayer) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found) {
|
||||||
|
animationLayer->updateRenderTree();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// MARK: Internal
|
// MARK: Internal
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user