mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-02-25 11:11:24 +08:00
Finish DisableRendering sample and add isRendering flag to SpineView
This commit is contained in:
parent
2e5c1c9ff0
commit
ebc7bc9cfc
@ -0,0 +1,196 @@
|
|||||||
|
package com.esotericsoftware.spine
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.gestures.detectDragGestures
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.ArrowBack
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.layout.positionInParent
|
||||||
|
import androidx.compose.ui.layout.positionInRoot
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.toSize
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import com.esotericsoftware.spine.android.AndroidSkeletonDrawable
|
||||||
|
import com.esotericsoftware.spine.android.AndroidTextureAtlas
|
||||||
|
import com.esotericsoftware.spine.android.SpineController
|
||||||
|
import com.esotericsoftware.spine.android.SpineView
|
||||||
|
import com.esotericsoftware.spine.android.utils.SkeletonDataUtils
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun DisableRendering(nav: NavHostController) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text(text = Destination.DisableRendering.title) },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton({ nav.navigateUp() }) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.ArrowBack,
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(paddingValues)
|
||||||
|
.padding()
|
||||||
|
.onGloballyPositioned { coordinates ->
|
||||||
|
print(coordinates.size.toSize())
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Text("Scroll spine boys out of the viewport")
|
||||||
|
Text("Rendering is disabled when the spine view moves out of the viewport, preserving CPU/GPU resources.")
|
||||||
|
}
|
||||||
|
SpineBoys()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SpineBoys() {
|
||||||
|
var boxSize by remember { mutableStateOf(Size.Zero) }
|
||||||
|
val offsetX = remember { mutableFloatStateOf(0f) }
|
||||||
|
val offsetY = remember { mutableFloatStateOf(0f) }
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clipToBounds()
|
||||||
|
.onGloballyPositioned { coordinates ->
|
||||||
|
boxSize = coordinates.size.toSize()
|
||||||
|
}
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectDragGestures { change, dragAmount ->
|
||||||
|
change.consume()
|
||||||
|
offsetX.floatValue += dragAmount.x
|
||||||
|
offsetY.floatValue += dragAmount.y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
if (boxSize != Size.Zero) {
|
||||||
|
val contentSize = boxSize * 4f
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
val cachedAtlas =
|
||||||
|
remember { AndroidTextureAtlas.fromAsset("spineboy.atlas", context) }
|
||||||
|
val cachedSkeletonData = remember {
|
||||||
|
SkeletonDataUtils.fromAsset(
|
||||||
|
cachedAtlas,
|
||||||
|
"spineboy-pro.json",
|
||||||
|
context
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val spineboys = remember {
|
||||||
|
val rng = Random(System.currentTimeMillis())
|
||||||
|
List(100) { index ->
|
||||||
|
val scale = 0.1f + rng.nextFloat() * 0.2f
|
||||||
|
val position = Offset(
|
||||||
|
rng.nextFloat() * contentSize.width,
|
||||||
|
rng.nextFloat() * contentSize.height
|
||||||
|
)
|
||||||
|
SpineBoyData(
|
||||||
|
index,
|
||||||
|
scale,
|
||||||
|
position,
|
||||||
|
if (index == 99) "hoverboard" else "walk"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spineboys.forEach { spineBoyData ->
|
||||||
|
|
||||||
|
val isSpineBoyVisible = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Box(modifier = Modifier
|
||||||
|
.offset {
|
||||||
|
IntOffset(
|
||||||
|
(-(contentSize.width / 2) + spineBoyData.position.x + offsetX.floatValue.toInt()).toInt(),
|
||||||
|
(-(contentSize.height / 2) + spineBoyData.position.y + offsetY.floatValue.toInt()).toInt(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.size(
|
||||||
|
(boxSize.width * spineBoyData.scale).dp,
|
||||||
|
(boxSize.height * spineBoyData.scale).dp
|
||||||
|
)
|
||||||
|
.onGloballyPositioned { coordinates ->
|
||||||
|
val positionInRoot = coordinates.positionInParent()
|
||||||
|
val size = coordinates.size.toSize()
|
||||||
|
|
||||||
|
val isInViewport = positionInRoot.x < boxSize.width &&
|
||||||
|
positionInRoot.x + size.width > 0 &&
|
||||||
|
positionInRoot.y < boxSize.height &&
|
||||||
|
positionInRoot.y + size.height > 0
|
||||||
|
|
||||||
|
isSpineBoyVisible.value = isInViewport
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
AndroidView(
|
||||||
|
factory = { ctx ->
|
||||||
|
SpineView.loadFromDrawable(
|
||||||
|
AndroidSkeletonDrawable(cachedAtlas, cachedSkeletonData),
|
||||||
|
ctx,
|
||||||
|
SpineController {
|
||||||
|
it.animationState.setAnimation(
|
||||||
|
0,
|
||||||
|
spineBoyData.animation,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
).apply {
|
||||||
|
isRendering = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update = { view ->
|
||||||
|
view.isRendering = isSpineBoyVisible.value
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SpineBoyData(
|
||||||
|
val id: Int,
|
||||||
|
val scale: Float,
|
||||||
|
val position: Offset,
|
||||||
|
val animation: String
|
||||||
|
)
|
||||||
|
|
||||||
@ -20,7 +20,6 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
@ -67,7 +66,7 @@ fun AppContent() {
|
|||||||
Destination.DressUp,
|
Destination.DressUp,
|
||||||
Destination.IKFollowing,
|
Destination.IKFollowing,
|
||||||
Destination.Physics,
|
Destination.Physics,
|
||||||
Destination.TheBoys
|
Destination.DisableRendering
|
||||||
),
|
),
|
||||||
paddingValues
|
paddingValues
|
||||||
)
|
)
|
||||||
@ -117,9 +116,9 @@ fun AppContent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
composable(
|
composable(
|
||||||
Destination.TheBoys.route
|
Destination.DisableRendering.route
|
||||||
) {
|
) {
|
||||||
TheBoys(navController)
|
DisableRendering(navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -188,5 +187,5 @@ sealed class Destination(val route: String, val title: String) {
|
|||||||
data object DressUp : Destination("dressUp", "Dress Up")
|
data object DressUp : Destination("dressUp", "Dress Up")
|
||||||
data object IKFollowing : Destination("ikFollowing", "IK Following")
|
data object IKFollowing : Destination("ikFollowing", "IK Following")
|
||||||
data object Physics: Destination("physics", "Physics (drag anywhere)")
|
data object Physics: Destination("physics", "Physics (drag anywhere)")
|
||||||
data object TheBoys: Destination("theBoys", "100 Spine Boys")
|
data object DisableRendering: Destination("disableRendering", "Disable Rendering")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,133 +0,0 @@
|
|||||||
package com.esotericsoftware.spine
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.gestures.detectDragGestures
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.offset
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.rounded.ArrowBack
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TopAppBar
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clipToBounds
|
|
||||||
import androidx.compose.ui.draw.scale
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.geometry.Size
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.unit.IntOffset
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.toSize
|
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
|
||||||
import androidx.navigation.NavHostController
|
|
||||||
import com.esotericsoftware.spine.android.AndroidSkeletonDrawable
|
|
||||||
import com.esotericsoftware.spine.android.AndroidTextureAtlas
|
|
||||||
import com.esotericsoftware.spine.android.SpineController
|
|
||||||
import com.esotericsoftware.spine.android.SpineView
|
|
||||||
import com.esotericsoftware.spine.android.utils.SkeletonDataUtils
|
|
||||||
import kotlin.random.Random
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun TheBoys(nav: NavHostController) {
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
TopAppBar(
|
|
||||||
title = { Text(text = Destination.TheBoys.title) },
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton({ nav.navigateUp() }) {
|
|
||||||
Icon(
|
|
||||||
Icons.Rounded.ArrowBack,
|
|
||||||
null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) { paddingValues ->
|
|
||||||
var viewportSize by remember { mutableStateOf(Size.Zero) }
|
|
||||||
val offsetX = remember { mutableFloatStateOf(0f) }
|
|
||||||
val offsetY = remember { mutableFloatStateOf(0f) }
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(paddingValues)
|
|
||||||
.fillMaxSize()
|
|
||||||
.clipToBounds()
|
|
||||||
.onGloballyPositioned { coordinates ->
|
|
||||||
viewportSize = coordinates.size.toSize()
|
|
||||||
}
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
detectDragGestures { change, dragAmount ->
|
|
||||||
change.consume()
|
|
||||||
offsetX.floatValue += dragAmount.x
|
|
||||||
offsetY.floatValue += dragAmount.y
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
if (viewportSize != Size.Zero) {
|
|
||||||
val contentSize = viewportSize * 4f
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
|
||||||
val cachedAtlas = remember { AndroidTextureAtlas.fromAsset("spineboy.atlas", context) }
|
|
||||||
val cachedSkeletonData = remember { SkeletonDataUtils.fromAsset(cachedAtlas, "spineboy-pro.json", context) }
|
|
||||||
|
|
||||||
val spineboys = remember {
|
|
||||||
val rng = Random(System.currentTimeMillis())
|
|
||||||
List(100) { index ->
|
|
||||||
val scale = 0.1f + rng.nextFloat() * 0.2f
|
|
||||||
val position = Offset(rng.nextFloat() * contentSize.width, rng.nextFloat() * contentSize.height)
|
|
||||||
SpineBoyData(scale, position, if (index == 99) "hoverboard" else "walk")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
spineboys.forEach { spineBoyData ->
|
|
||||||
Box(modifier = Modifier
|
|
||||||
.offset {
|
|
||||||
IntOffset(
|
|
||||||
(-(contentSize.width / 2) + spineBoyData.position.x + offsetX.floatValue.toInt()).toInt(),
|
|
||||||
(-(contentSize.height / 2) + spineBoyData.position.y + offsetY.floatValue.toInt()).toInt(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.scale(spineBoyData.scale)
|
|
||||||
) {
|
|
||||||
AndroidView(
|
|
||||||
factory = { ctx ->
|
|
||||||
SpineView.loadFromDrawable(
|
|
||||||
AndroidSkeletonDrawable(cachedAtlas, cachedSkeletonData),
|
|
||||||
ctx,
|
|
||||||
SpineController {
|
|
||||||
it.animationState.setAnimation(0, spineBoyData.animation, true)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class SpineBoyData(
|
|
||||||
val scale: Float,
|
|
||||||
val position: Offset,
|
|
||||||
val animation: String
|
|
||||||
)
|
|
||||||
|
|
||||||
@ -119,11 +119,15 @@ public class SpineController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void pause() {
|
public void pause() {
|
||||||
playing = false;
|
if (playing) {
|
||||||
|
playing = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void resume() {
|
public void resume() {
|
||||||
playing = true;
|
if (!playing) {
|
||||||
|
playing = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Point toSkeletonCoordinates(Point position) {
|
public Point toSkeletonCoordinates(Point position) {
|
||||||
@ -132,6 +136,22 @@ public class SpineController {
|
|||||||
return new Point((int) (x / scaleX - offsetX), (int) (y / scaleY - offsetY));
|
return new Point((int) (x / scaleX - offsetX), (int) (y / scaleY - offsetY));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setOnBeforeUpdateWorldTransforms(@Nullable SpineControllerCallback onBeforeUpdateWorldTransforms) {
|
||||||
|
this.onBeforeUpdateWorldTransforms = onBeforeUpdateWorldTransforms;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnAfterUpdateWorldTransforms(@Nullable SpineControllerCallback onAfterUpdateWorldTransforms) {
|
||||||
|
this.onAfterUpdateWorldTransforms = onAfterUpdateWorldTransforms;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnBeforePaint(@Nullable SpineControllerBeforePaintCallback onBeforePaint) {
|
||||||
|
this.onBeforePaint = onBeforePaint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnAfterPaint(@Nullable SpineControllerAfterPaintCallback onAfterPaint) {
|
||||||
|
this.onAfterPaint = onAfterPaint;
|
||||||
|
}
|
||||||
|
|
||||||
protected void setCoordinateTransform(double offsetX, double offsetY, double scaleX, double scaleY) {
|
protected void setCoordinateTransform(double offsetX, double offsetY, double scaleX, double scaleY) {
|
||||||
this.offsetX = offsetX;
|
this.offsetX = offsetX;
|
||||||
this.offsetY = offsetY;
|
this.offsetY = offsetY;
|
||||||
|
|||||||
@ -42,13 +42,20 @@ import android.graphics.Canvas;
|
|||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.Choreographer;
|
import android.view.Choreographer;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.io.Console;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.Timer;
|
||||||
|
import java.util.TimerTask;
|
||||||
|
|
||||||
public class SpineView extends View implements Choreographer.FrameCallback {
|
public class SpineView extends View implements Choreographer.FrameCallback {
|
||||||
|
|
||||||
@ -138,6 +145,7 @@ public class SpineView extends View implements Choreographer.FrameCallback {
|
|||||||
private float x = 0;
|
private float x = 0;
|
||||||
private float y = 0;
|
private float y = 0;
|
||||||
private final SkeletonRenderer renderer = new SkeletonRenderer();
|
private final SkeletonRenderer renderer = new SkeletonRenderer();
|
||||||
|
private Boolean rendering = true;
|
||||||
private Bounds computedBounds = new Bounds();
|
private Bounds computedBounds = new Bounds();
|
||||||
|
|
||||||
private SpineController controller;
|
private SpineController controller;
|
||||||
@ -235,6 +243,14 @@ public class SpineView extends View implements Choreographer.FrameCallback {
|
|||||||
updateCanvasTransform();
|
updateCanvasTransform();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Boolean isRendering() {
|
||||||
|
return rendering;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRendering(Boolean rendering) {
|
||||||
|
this.rendering = rendering;
|
||||||
|
}
|
||||||
|
|
||||||
private void loadFrom(AndroidSkeletonDrawableLoader loader) {
|
private void loadFrom(AndroidSkeletonDrawableLoader loader) {
|
||||||
Handler mainHandler = new Handler(Looper.getMainLooper());
|
Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||||
Thread backgroundThread = new Thread(() -> {
|
Thread backgroundThread = new Thread(() -> {
|
||||||
@ -253,7 +269,7 @@ public class SpineView extends View implements Choreographer.FrameCallback {
|
|||||||
@Override
|
@Override
|
||||||
public void onDraw (@NonNull Canvas canvas) {
|
public void onDraw (@NonNull Canvas canvas) {
|
||||||
super.onDraw(canvas);
|
super.onDraw(canvas);
|
||||||
if (controller == null || !controller.isInitialized()) {
|
if (controller == null || !controller.isInitialized() || !rendering) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user