mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Plumbing to set filters on Android views (#44453)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/44453 This works similar to how `transform` is parsed in that it sets tags on the View to actually update the prop when all the prop setters are done being called since the parsing of the array is not very trivial. Besides that it is pretty simple and just calls into `FilterHelper` and uses `setRenderEffect`: https://developer.android.com/reference/android/view/View#setRenderEffect(android.graphics.RenderEffect). That API is only exposed in version 31 of the SDK so it is gated accordingly. Changelog: [Internal] Reviewed By: NickGerleman Differential Revision: D54640600 fbshipit-source-id: ad4cde2bed9611f476f4ecb2550c2269965d7917
This commit is contained in:
committed by
Facebook GitHub Bot
parent
6213b2a62a
commit
b0e746e4bb
@@ -3970,6 +3970,7 @@ public abstract class com/facebook/react/uimanager/BaseViewManager : com/faceboo
|
||||
public fun setClick (Landroid/view/View;Z)V
|
||||
public fun setClickCapture (Landroid/view/View;Z)V
|
||||
public fun setElevation (Landroid/view/View;F)V
|
||||
public fun setFilter (Landroid/view/View;Lcom/facebook/react/bridge/ReadableArray;)V
|
||||
public fun setImportantForAccessibility (Landroid/view/View;Ljava/lang/String;)V
|
||||
public fun setMoveShouldSetResponder (Landroid/view/View;Z)V
|
||||
public fun setMoveShouldSetResponderCapture (Landroid/view/View;Z)V
|
||||
@@ -4039,6 +4040,7 @@ public abstract interface class com/facebook/react/uimanager/BaseViewManagerInte
|
||||
public abstract fun setBorderTopLeftRadius (Landroid/view/View;F)V
|
||||
public abstract fun setBorderTopRightRadius (Landroid/view/View;F)V
|
||||
public abstract fun setElevation (Landroid/view/View;F)V
|
||||
public abstract fun setFilter (Landroid/view/View;Lcom/facebook/react/bridge/ReadableArray;)V
|
||||
public abstract fun setImportantForAccessibility (Landroid/view/View;Ljava/lang/String;)V
|
||||
public abstract fun setNativeId (Landroid/view/View;Ljava/lang/String;)V
|
||||
public abstract fun setOpacity (Landroid/view/View;F)V
|
||||
@@ -5333,6 +5335,7 @@ public final class com/facebook/react/uimanager/ViewProps {
|
||||
public static final field ELLIPSIZE_MODE Ljava/lang/String;
|
||||
public static final field ENABLED Ljava/lang/String;
|
||||
public static final field END Ljava/lang/String;
|
||||
public static final field FILTER Ljava/lang/String;
|
||||
public static final field FLEX Ljava/lang/String;
|
||||
public static final field FLEX_BASIS Ljava/lang/String;
|
||||
public static final field FLEX_DIRECTION Ljava/lang/String;
|
||||
|
||||
+42
-1
@@ -8,6 +8,7 @@
|
||||
package com.facebook.react.uimanager;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
@@ -104,6 +105,10 @@ public abstract class BaseViewManager<T extends View, C extends LayoutShadowNode
|
||||
view.setTag(R.id.invalidate_transform, null);
|
||||
view.removeOnLayoutChangeListener(this);
|
||||
|
||||
view.setTag(R.id.use_hardware_layer, null);
|
||||
view.setTag(R.id.filter, null);
|
||||
applyFilter(view, null);
|
||||
|
||||
// setShadowColor
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
view.setOutlineAmbientShadowColor(Color.BLACK);
|
||||
@@ -181,6 +186,12 @@ public abstract class BaseViewManager<T extends View, C extends LayoutShadowNode
|
||||
view.setBackgroundColor(backgroundColor);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ReactProp(name = ViewProps.FILTER)
|
||||
public void setFilter(@NonNull T view, @Nullable ReadableArray filter) {
|
||||
view.setTag(R.id.filter, filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ReactProp(name = ViewProps.TRANSFORM)
|
||||
public void setTransform(@NonNull T view, @Nullable ReadableArray matrix) {
|
||||
@@ -235,7 +246,7 @@ public abstract class BaseViewManager<T extends View, C extends LayoutShadowNode
|
||||
@Override
|
||||
@ReactProp(name = ViewProps.RENDER_TO_HARDWARE_TEXTURE)
|
||||
public void setRenderToHardwareTexture(@NonNull T view, boolean useHWTexture) {
|
||||
view.setLayerType(useHWTexture ? View.LAYER_TYPE_HARDWARE : View.LAYER_TYPE_NONE, null);
|
||||
view.setTag(R.id.use_hardware_layer, useHWTexture);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -491,6 +502,28 @@ public abstract class BaseViewManager<T extends View, C extends LayoutShadowNode
|
||||
}
|
||||
}
|
||||
|
||||
private void applyFilter(@NonNull T view, @Nullable ReadableArray filter) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
view.setRenderEffect(null);
|
||||
}
|
||||
Boolean useHWLayer = (Boolean) view.getTag(R.id.use_hardware_layer);
|
||||
int layerType =
|
||||
useHWLayer != null && useHWLayer ? View.LAYER_TYPE_HARDWARE : View.LAYER_TYPE_NONE;
|
||||
view.setLayerType(layerType, null);
|
||||
|
||||
if (filter == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (FilterHelper.isOnlyColorMatrixFilters(filter)) {
|
||||
Paint p = new Paint();
|
||||
p.setColorFilter(FilterHelper.parseColorMatrixFilters(filter));
|
||||
view.setLayerType(View.LAYER_TYPE_HARDWARE, p);
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
view.setRenderEffect(FilterHelper.parseFilters(filter));
|
||||
}
|
||||
}
|
||||
|
||||
protected void setTransformProperty(
|
||||
@NonNull T view,
|
||||
@Nullable ReadableArray transforms,
|
||||
@@ -595,6 +628,14 @@ public abstract class BaseViewManager<T extends View, C extends LayoutShadowNode
|
||||
setTransformProperty(view, transformMatrix, transformOrigin);
|
||||
view.setTag(R.id.invalidate_transform, false);
|
||||
}
|
||||
|
||||
Boolean useHWLayer = (Boolean) view.getTag(R.id.use_hardware_layer);
|
||||
if (useHWLayer != null) {
|
||||
view.setLayerType(useHWLayer ? View.LAYER_TYPE_HARDWARE : View.LAYER_TYPE_NONE, null);
|
||||
}
|
||||
|
||||
ReadableArray filter = (ReadableArray) view.getTag(R.id.filter);
|
||||
applyFilter(view, filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+2
@@ -48,6 +48,8 @@ public interface BaseViewManagerInterface<T extends View> {
|
||||
|
||||
void setElevation(T view, float elevation);
|
||||
|
||||
void setFilter(T view, ReadableArray filter);
|
||||
|
||||
void setShadowColor(T view, int shadowColor);
|
||||
|
||||
void setImportantForAccessibility(T view, @Nullable String importantForAccessibility);
|
||||
|
||||
+188
-128
@@ -12,8 +12,9 @@ import android.graphics.ColorMatrix
|
||||
import android.graphics.ColorMatrixColorFilter
|
||||
import android.graphics.RenderEffect
|
||||
import android.graphics.Shader
|
||||
import android.graphics.Shader.TileMode
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
@TargetApi(31)
|
||||
internal object FilterHelper {
|
||||
@@ -23,9 +24,9 @@ internal object FilterHelper {
|
||||
filters ?: return null
|
||||
var chainedEffects: RenderEffect? = null
|
||||
for (i in 0 until filters.size()) {
|
||||
val filter = filters.getMap(i)
|
||||
val filterName = filter.getString("name") ?: continue
|
||||
val amount = filter.getDouble("amount").toFloat()
|
||||
val filter = filters.getMap(i).getEntryIterator().next()
|
||||
val filterName = filter.key
|
||||
val amount = (filter.value as Double).toFloat()
|
||||
|
||||
chainedEffects =
|
||||
when (filterName) {
|
||||
@@ -43,6 +44,47 @@ internal object FilterHelper {
|
||||
return chainedEffects
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
public fun parseColorMatrixFilters(filters: ReadableArray?): ColorMatrixColorFilter? {
|
||||
filters ?: return null
|
||||
// New ColorMatrix objects represent the identity matrix
|
||||
val resultColorMatrix = ColorMatrix()
|
||||
for (i in 0 until filters.size()) {
|
||||
val filter = filters.getMap(i).getEntryIterator().next()
|
||||
val filterName = filter.key
|
||||
val amount = (filter.value as Double).toFloat()
|
||||
|
||||
val tempColorMatrix =
|
||||
when (filterName) {
|
||||
"brightness" -> createBrightnessColorMatrix(amount)
|
||||
"contrast" -> createContrastColorMatrix(amount)
|
||||
"grayscale" -> createGrayscaleColorMatrix(amount)
|
||||
"sepia" -> createSepiaColorMatrix(amount)
|
||||
"saturate" -> createSaturateColorMatrix(amount)
|
||||
"hueRotate" -> createHueRotateColorMatrix(amount)
|
||||
"invert" -> createInvertColorMatrix(amount)
|
||||
else -> throw IllegalArgumentException("Invalid color matrix filter: $filterName")
|
||||
}
|
||||
|
||||
resultColorMatrix.preConcat(tempColorMatrix)
|
||||
}
|
||||
|
||||
return ColorMatrixColorFilter(resultColorMatrix)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
public fun isOnlyColorMatrixFilters(filters: ReadableArray?): Boolean {
|
||||
filters ?: return false
|
||||
for (i in 0 until filters.size()) {
|
||||
val filter = filters.getMap(i).getEntryIterator().next()
|
||||
val filterName = filter.key
|
||||
if (filterName == "blur") {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// https://www.w3.org/TR/filter-effects-1/#blurEquivalent
|
||||
public fun createBlurEffect(sigma: Float, chainedEffects: RenderEffect? = null): RenderEffect? {
|
||||
if (sigma <= 0.5) {
|
||||
@@ -66,9 +108,13 @@ internal object FilterHelper {
|
||||
amount: Float,
|
||||
chainedEffects: RenderEffect? = null
|
||||
): RenderEffect {
|
||||
return createColorMatrixEffect(createBrightnessColorMatrix(amount), chainedEffects)
|
||||
}
|
||||
|
||||
private fun createBrightnessColorMatrix(amount: Float): ColorMatrix {
|
||||
val matrix = ColorMatrix()
|
||||
matrix.setScale(amount, amount, amount, 1f)
|
||||
return createColorMatrixEffect(matrix, chainedEffects)
|
||||
return matrix
|
||||
}
|
||||
|
||||
// https://www.w3.org/TR/filter-effects-1/#contrastEquivalent
|
||||
@@ -76,33 +122,35 @@ internal object FilterHelper {
|
||||
amount: Float,
|
||||
chainedEffects: RenderEffect? = null
|
||||
): RenderEffect {
|
||||
return createColorMatrixEffect(createContrastColorMatrix(amount), chainedEffects)
|
||||
}
|
||||
|
||||
private fun createContrastColorMatrix(amount: Float): ColorMatrix {
|
||||
// Multiply by 255 as Android operates in [0, 255] while the spec operates in [0, 1].
|
||||
// This really only matters if there is an intercept that needs to be added
|
||||
val intercept = 255 * (-(amount / 2.0f) + 0.5f)
|
||||
val matrix =
|
||||
ColorMatrix(
|
||||
floatArrayOf(
|
||||
amount,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
intercept,
|
||||
0f,
|
||||
amount,
|
||||
0f,
|
||||
0f,
|
||||
intercept,
|
||||
0f,
|
||||
0f,
|
||||
amount,
|
||||
0f,
|
||||
intercept,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
1f,
|
||||
0f))
|
||||
return createColorMatrixEffect(matrix, chainedEffects)
|
||||
return ColorMatrix(
|
||||
floatArrayOf(
|
||||
amount,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
intercept,
|
||||
0f,
|
||||
amount,
|
||||
0f,
|
||||
0f,
|
||||
intercept,
|
||||
0f,
|
||||
0f,
|
||||
amount,
|
||||
0f,
|
||||
intercept,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
1f,
|
||||
0f))
|
||||
}
|
||||
|
||||
// https://www.w3.org/TR/filter-effects-1/#grayscaleEquivalent
|
||||
@@ -110,60 +158,64 @@ internal object FilterHelper {
|
||||
amount: Float,
|
||||
chainedEffects: RenderEffect? = null
|
||||
): RenderEffect {
|
||||
return createColorMatrixEffect(createGrayscaleColorMatrix(amount), chainedEffects)
|
||||
}
|
||||
|
||||
private fun createGrayscaleColorMatrix(amount: Float): ColorMatrix {
|
||||
val inverseAmount = 1 - amount
|
||||
val matrix =
|
||||
ColorMatrix(
|
||||
floatArrayOf(
|
||||
0.2_126f + 0.7_874f * inverseAmount,
|
||||
0.7_152f - 0.7_152f * inverseAmount,
|
||||
0.0_722f - 0.0_722f * inverseAmount,
|
||||
0f,
|
||||
0f,
|
||||
0.2_126f - 0.2_126f * inverseAmount,
|
||||
0.7_152f + 0.2_848f * inverseAmount,
|
||||
0.0_722f - 0.0_722f * inverseAmount,
|
||||
0f,
|
||||
0f,
|
||||
0.2_126f - 0.2_126f * inverseAmount,
|
||||
0.7_152f - 0.7_152f * inverseAmount,
|
||||
0.0_722f + 0.9_278f * inverseAmount,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
1f,
|
||||
0f))
|
||||
return createColorMatrixEffect(matrix, chainedEffects)
|
||||
return ColorMatrix(
|
||||
floatArrayOf(
|
||||
0.2_126f + 0.7_874f * inverseAmount,
|
||||
0.7_152f - 0.7_152f * inverseAmount,
|
||||
0.0_722f - 0.0_722f * inverseAmount,
|
||||
0f,
|
||||
0f,
|
||||
0.2_126f - 0.2_126f * inverseAmount,
|
||||
0.7_152f + 0.2_848f * inverseAmount,
|
||||
0.0_722f - 0.0_722f * inverseAmount,
|
||||
0f,
|
||||
0f,
|
||||
0.2_126f - 0.2_126f * inverseAmount,
|
||||
0.7_152f - 0.7_152f * inverseAmount,
|
||||
0.0_722f + 0.9_278f * inverseAmount,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
1f,
|
||||
0f))
|
||||
}
|
||||
|
||||
// https://www.w3.org/TR/filter-effects-1/#sepiaEquivalent
|
||||
public fun createSepiaEffect(amount: Float, chainedEffects: RenderEffect? = null): RenderEffect {
|
||||
return createColorMatrixEffect(createSepiaColorMatrix(amount), chainedEffects)
|
||||
}
|
||||
|
||||
private fun createSepiaColorMatrix(amount: Float): ColorMatrix {
|
||||
val inverseAmount = 1 - amount
|
||||
val matrix =
|
||||
ColorMatrix(
|
||||
floatArrayOf(
|
||||
0.393f + 0.607f * inverseAmount,
|
||||
0.769f - 0.769f * inverseAmount,
|
||||
0.189f - 0.189f * inverseAmount,
|
||||
0f,
|
||||
0f,
|
||||
0.349f - 0.349f * inverseAmount,
|
||||
0.686f + 0.314f * inverseAmount,
|
||||
0.168f - 0.168f * inverseAmount,
|
||||
0f,
|
||||
0f,
|
||||
0.272f - 0.272f * inverseAmount,
|
||||
0.534f - 0.534f * inverseAmount,
|
||||
0.131f + 0.869f * inverseAmount,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
1f,
|
||||
0f))
|
||||
return createColorMatrixEffect(matrix, chainedEffects)
|
||||
return ColorMatrix(
|
||||
floatArrayOf(
|
||||
0.393f + 0.607f * inverseAmount,
|
||||
0.769f - 0.769f * inverseAmount,
|
||||
0.189f - 0.189f * inverseAmount,
|
||||
0f,
|
||||
0f,
|
||||
0.349f - 0.349f * inverseAmount,
|
||||
0.686f + 0.314f * inverseAmount,
|
||||
0.168f - 0.168f * inverseAmount,
|
||||
0f,
|
||||
0f,
|
||||
0.272f - 0.272f * inverseAmount,
|
||||
0.534f - 0.534f * inverseAmount,
|
||||
0.131f + 0.869f * inverseAmount,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
1f,
|
||||
0f))
|
||||
}
|
||||
|
||||
// https://www.w3.org/TR/filter-effects-1/#saturateEquivalent
|
||||
@@ -171,9 +223,13 @@ internal object FilterHelper {
|
||||
amount: Float,
|
||||
chainedEffects: RenderEffect? = null
|
||||
): RenderEffect {
|
||||
return createColorMatrixEffect(createSaturateColorMatrix(amount), chainedEffects)
|
||||
}
|
||||
|
||||
private fun createSaturateColorMatrix(amount: Float): ColorMatrix {
|
||||
val matrix = ColorMatrix()
|
||||
matrix.setSaturation(amount)
|
||||
return createColorMatrixEffect(matrix, chainedEffects)
|
||||
return matrix
|
||||
}
|
||||
|
||||
// https://www.w3.org/TR/filter-effects-1/#huerotateEquivalent
|
||||
@@ -181,63 +237,67 @@ internal object FilterHelper {
|
||||
amount: Float,
|
||||
chainedEffects: RenderEffect? = null
|
||||
): RenderEffect {
|
||||
return createColorMatrixEffect(createHueRotateColorMatrix(amount), chainedEffects)
|
||||
}
|
||||
|
||||
private fun createHueRotateColorMatrix(amount: Float): ColorMatrix {
|
||||
val amountRads = Math.toRadians(amount.toDouble())
|
||||
val cos = Math.cos(amountRads).toFloat()
|
||||
val sin = Math.sin(amountRads).toFloat()
|
||||
val matrix =
|
||||
ColorMatrix(
|
||||
floatArrayOf(
|
||||
0.213f + 0.787f * cos - 0.213f * sin,
|
||||
0.715f - 0.715f * cos - 0.715f * sin,
|
||||
0.072f - 0.072f * cos + 0.928f * sin,
|
||||
0f,
|
||||
0f,
|
||||
0.213f - 0.213f * cos + 0.143f * sin,
|
||||
0.715f + 0.285f * cos + 0.140f * sin,
|
||||
0.072f - 0.072f * cos - 0.283f * sin,
|
||||
0f,
|
||||
0f,
|
||||
0.213f - 0.213f * cos - 0.787f * sin,
|
||||
0.715f - 0.715f * cos + 0.715f * sin,
|
||||
0.072f + 0.928f * cos + 0.072f * sin,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
1f,
|
||||
0f))
|
||||
return createColorMatrixEffect(matrix, chainedEffects)
|
||||
val cos = cos(amountRads).toFloat()
|
||||
val sin = sin(amountRads).toFloat()
|
||||
return ColorMatrix(
|
||||
floatArrayOf(
|
||||
0.213f + 0.787f * cos - 0.213f * sin,
|
||||
0.715f - 0.715f * cos - 0.715f * sin,
|
||||
0.072f - 0.072f * cos + 0.928f * sin,
|
||||
0f,
|
||||
0f,
|
||||
0.213f - 0.213f * cos + 0.143f * sin,
|
||||
0.715f + 0.285f * cos + 0.140f * sin,
|
||||
0.072f - 0.072f * cos - 0.283f * sin,
|
||||
0f,
|
||||
0f,
|
||||
0.213f - 0.213f * cos - 0.787f * sin,
|
||||
0.715f - 0.715f * cos + 0.715f * sin,
|
||||
0.072f + 0.928f * cos + 0.072f * sin,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
1f,
|
||||
0f))
|
||||
}
|
||||
|
||||
// https://www.w3.org/TR/filter-effects-1/#invertEquivalent
|
||||
public fun createInvertEffect(amount: Float, chainedEffects: RenderEffect? = null): RenderEffect {
|
||||
return createColorMatrixEffect(createInvertColorMatrix(amount), chainedEffects)
|
||||
}
|
||||
|
||||
private fun createInvertColorMatrix(amount: Float): ColorMatrix {
|
||||
val slope = 1 - 2 * amount
|
||||
val intercept = amount * 255
|
||||
val matrix =
|
||||
ColorMatrix(
|
||||
floatArrayOf(
|
||||
slope,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
intercept,
|
||||
0f,
|
||||
slope,
|
||||
0f,
|
||||
0f,
|
||||
intercept,
|
||||
0f,
|
||||
0f,
|
||||
slope,
|
||||
0f,
|
||||
intercept,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
1f,
|
||||
0f))
|
||||
return createColorMatrixEffect(matrix, chainedEffects)
|
||||
return ColorMatrix(
|
||||
floatArrayOf(
|
||||
slope,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
intercept,
|
||||
0f,
|
||||
slope,
|
||||
0f,
|
||||
0f,
|
||||
intercept,
|
||||
0f,
|
||||
0f,
|
||||
slope,
|
||||
0f,
|
||||
intercept,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
1f,
|
||||
0f))
|
||||
}
|
||||
|
||||
private fun createColorMatrixEffect(
|
||||
|
||||
+1
@@ -135,6 +135,7 @@ public object ViewProps {
|
||||
public const val BORDER_START_COLOR: String = "borderStartColor"
|
||||
public const val BORDER_END_COLOR: String = "borderEndColor"
|
||||
public const val ON_LAYOUT: String = "onLayout"
|
||||
public const val FILTER: String = "experimental_filter"
|
||||
public const val TRANSFORM: String = "transform"
|
||||
public const val TRANSFORM_ORIGIN: String = "transformOrigin"
|
||||
public const val ELEVATION: String = "elevation"
|
||||
|
||||
@@ -56,4 +56,10 @@
|
||||
|
||||
<!-- tag is used to invalidate transform style in view manager -->
|
||||
<item type="id" name="invalidate_transform"/>
|
||||
|
||||
<!-- tag is used to store if we should render the view to a hardware texture -->
|
||||
<item type="id" name="use_hardware_layer"/>
|
||||
|
||||
<!-- tag is used to store graphical filter effects to apply to the view -->
|
||||
<item type="id" name="filter"/>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user