chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ ImageToolbox is an image editor for android
|
||||
~ Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
~
|
||||
~ You should have received a copy of the Apache License
|
||||
~ along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.dynamic.theme
|
||||
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
fun Color.toColorBlind(type: ColorBlindType): Color {
|
||||
val (r, g, b) = listOf(this.red, this.green, this.blue)
|
||||
val newColor = when (type) {
|
||||
ColorBlindType.Protanomaly -> {
|
||||
val matrix = listOf(
|
||||
0.817, 0.183, 0.0,
|
||||
0.333, 0.667, 0.0,
|
||||
0.0, 0.125, 0.875
|
||||
)
|
||||
transformColor(r, g, b, matrix)
|
||||
}
|
||||
|
||||
ColorBlindType.Deuteranomaly -> {
|
||||
val matrix = listOf(
|
||||
0.8, 0.2, 0.0,
|
||||
0.258, 0.742, 0.0,
|
||||
0.0, 0.142, 0.858
|
||||
)
|
||||
transformColor(r, g, b, matrix)
|
||||
}
|
||||
|
||||
ColorBlindType.Tritanomaly -> {
|
||||
val matrix = listOf(
|
||||
0.967, 0.033, 0.0,
|
||||
0.0, 0.733, 0.267,
|
||||
0.0, 0.183, 0.817
|
||||
)
|
||||
transformColor(r, g, b, matrix)
|
||||
}
|
||||
|
||||
ColorBlindType.Protanopia -> {
|
||||
val matrix = listOf(
|
||||
0.567, 0.433, 0.0,
|
||||
0.558, 0.442, 0.0,
|
||||
0.0, 0.242, 0.758
|
||||
)
|
||||
transformColor(r, g, b, matrix)
|
||||
}
|
||||
|
||||
ColorBlindType.Deuteranopia -> {
|
||||
val matrix = listOf(
|
||||
0.625, 0.375, 0.0,
|
||||
0.7, 0.3, 0.0,
|
||||
0.0, 0.3, 0.7
|
||||
)
|
||||
transformColor(r, g, b, matrix)
|
||||
}
|
||||
|
||||
ColorBlindType.Tritanopia -> {
|
||||
val matrix = listOf(
|
||||
0.95, 0.05, 0.0,
|
||||
0.0, 0.433, 0.567,
|
||||
0.0, 0.475, 0.525
|
||||
)
|
||||
transformColor(r, g, b, matrix)
|
||||
}
|
||||
|
||||
ColorBlindType.Achromatomaly -> {
|
||||
val matrix = listOf(
|
||||
0.618, 0.320, 0.062,
|
||||
0.163, 0.775, 0.062,
|
||||
0.163, 0.320, 0.516
|
||||
)
|
||||
transformColor(r, g, b, matrix)
|
||||
}
|
||||
|
||||
ColorBlindType.Achromatopsia -> {
|
||||
val matrix = listOf(
|
||||
0.299, 0.587, 0.114,
|
||||
0.299, 0.587, 0.114,
|
||||
0.299, 0.587, 0.114
|
||||
)
|
||||
transformColor(r, g, b, matrix)
|
||||
}
|
||||
}
|
||||
return Color(newColor[0], newColor[1], newColor[2], this.alpha)
|
||||
}
|
||||
|
||||
private fun transformColor(r: Float, g: Float, b: Float, matrix: List<Double>): List<Float> {
|
||||
val newR = (matrix[0] * r + matrix[1] * g + matrix[2] * b).toFloat()
|
||||
val newG = (matrix[3] * r + matrix[4] * g + matrix[5] * b).toFloat()
|
||||
val newB = (matrix[6] * r + matrix[7] * g + matrix[8] * b).toFloat()
|
||||
return listOf(newR.coerceIn(0f, 1f), newG.coerceIn(0f, 1f), newB.coerceIn(0f, 1f))
|
||||
}
|
||||
|
||||
enum class ColorBlindType {
|
||||
Protanomaly,
|
||||
Deuteranomaly,
|
||||
Tritanomaly,
|
||||
Protanopia,
|
||||
Deuteranopia,
|
||||
Tritanopia,
|
||||
Achromatomaly,
|
||||
Achromatopsia
|
||||
}
|
||||
|
||||
fun ColorScheme.toColorBlind(type: ColorBlindType?): ColorScheme {
|
||||
if (type == null) return this
|
||||
|
||||
return this.copy(
|
||||
primary = primary.toColorBlind(type),
|
||||
onPrimary = onPrimary.toColorBlind(type),
|
||||
primaryContainer = primaryContainer.toColorBlind(type),
|
||||
onPrimaryContainer = onPrimaryContainer.toColorBlind(type),
|
||||
inversePrimary = inversePrimary.toColorBlind(type),
|
||||
secondary = secondary.toColorBlind(type),
|
||||
onSecondary = onSecondary.toColorBlind(type),
|
||||
secondaryContainer = secondaryContainer.toColorBlind(type),
|
||||
onSecondaryContainer = onSecondaryContainer.toColorBlind(type),
|
||||
tertiary = tertiary.toColorBlind(type),
|
||||
onTertiary = onTertiary.toColorBlind(type),
|
||||
tertiaryContainer = tertiaryContainer.toColorBlind(type),
|
||||
onTertiaryContainer = onTertiaryContainer.toColorBlind(type),
|
||||
background = background.toColorBlind(type),
|
||||
onBackground = onBackground.toColorBlind(type),
|
||||
surface = surface.toColorBlind(type),
|
||||
onSurface = onSurface.toColorBlind(type),
|
||||
surfaceVariant = surfaceVariant.toColorBlind(type),
|
||||
onSurfaceVariant = onSurfaceVariant.toColorBlind(type),
|
||||
surfaceTint = surfaceTint.toColorBlind(type),
|
||||
inverseSurface = inverseSurface.toColorBlind(type),
|
||||
inverseOnSurface = inverseOnSurface.toColorBlind(type),
|
||||
error = error.toColorBlind(type),
|
||||
onError = onError.toColorBlind(type),
|
||||
errorContainer = errorContainer.toColorBlind(type),
|
||||
onErrorContainer = onErrorContainer.toColorBlind(type),
|
||||
outline = outline.toColorBlind(type),
|
||||
outlineVariant = outlineVariant.toColorBlind(type),
|
||||
surfaceBright = surfaceBright.toColorBlind(type),
|
||||
surfaceDim = surfaceDim.toColorBlind(type),
|
||||
surfaceContainer = surfaceContainer.toColorBlind(type),
|
||||
surfaceContainerHigh = surfaceContainerHigh.toColorBlind(type),
|
||||
surfaceContainerHighest = surfaceContainerHighest.toColorBlind(type),
|
||||
surfaceContainerLow = surfaceContainerLow.toColorBlind(type),
|
||||
surfaceContainerLowest = surfaceContainerLowest.toColorBlind(type),
|
||||
primaryFixed = primaryFixed.toColorBlind(type),
|
||||
primaryFixedDim = primaryFixedDim.toColorBlind(type),
|
||||
onPrimaryFixed = onPrimaryFixed.toColorBlind(type),
|
||||
onPrimaryFixedVariant = onPrimaryFixedVariant.toColorBlind(type),
|
||||
secondaryFixed = secondaryFixed.toColorBlind(type),
|
||||
secondaryFixedDim = secondaryFixedDim.toColorBlind(type),
|
||||
onSecondaryFixed = onSecondaryFixed.toColorBlind(type),
|
||||
onSecondaryFixedVariant = onSecondaryFixedVariant.toColorBlind(type),
|
||||
tertiaryFixed = tertiaryFixed.toColorBlind(type),
|
||||
tertiaryFixedDim = tertiaryFixedDim.toColorBlind(type),
|
||||
onTertiaryFixed = onTertiaryFixed.toColorBlind(type),
|
||||
onTertiaryFixedVariant = onTertiaryFixedVariant.toColorBlind(type),
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.dynamic.theme
|
||||
|
||||
enum class PaletteStyle {
|
||||
TonalSpot,
|
||||
Neutral,
|
||||
Vibrant,
|
||||
Expressive,
|
||||
Rainbow,
|
||||
FruitSalad,
|
||||
Monochrome,
|
||||
Fidelity,
|
||||
Content
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
@file:Suppress("PrivatePropertyName", "DEPRECATION", "unused")
|
||||
|
||||
package com.t8rin.dynamic.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.window.DialogWindowProvider
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import com.t8rin.imagetoolbox.core.resources.utils.compositeOverSafe
|
||||
|
||||
|
||||
@Stable
|
||||
interface SystemUiController {
|
||||
|
||||
/**
|
||||
* Control for the behavior of the system bars. This value should be one of the
|
||||
* [WindowInsetsControllerCompat] behavior constants:
|
||||
* [WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_TOUCH] (Deprecated),
|
||||
* [WindowInsetsControllerCompat.BEHAVIOR_DEFAULT] and
|
||||
* [WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE].
|
||||
*/
|
||||
var systemBarsBehavior: Int
|
||||
|
||||
/**
|
||||
* Property which holds the status bar visibility. If set to true, show the status bar,
|
||||
* otherwise hide the status bar.
|
||||
*/
|
||||
var isStatusBarVisible: Boolean
|
||||
|
||||
/**
|
||||
* Property which holds the navigation bar visibility. If set to true, show the navigation bar,
|
||||
* otherwise hide the navigation bar.
|
||||
*/
|
||||
var isNavigationBarVisible: Boolean
|
||||
|
||||
/**
|
||||
* Property which holds the status & navigation bar visibility. If set to true, show both bars,
|
||||
* otherwise hide both bars.
|
||||
*/
|
||||
var isSystemBarsVisible: Boolean
|
||||
get() = isNavigationBarVisible && isStatusBarVisible
|
||||
set(value) {
|
||||
isStatusBarVisible = value
|
||||
isNavigationBarVisible = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the status bar color.
|
||||
*
|
||||
* @param color The **desired** [Color] to set. This may require modification if running on an
|
||||
* API level that only supports white status bar icons.
|
||||
* @param darkIcons Whether dark status bar icons would be preferable.
|
||||
* @param transformColorForLightContent A lambda which will be invoked to transform [color] if
|
||||
* dark icons were requested but are not available. Defaults to applying a black scrim.
|
||||
*
|
||||
* @see statusBarDarkContentEnabled
|
||||
*/
|
||||
fun setStatusBarColor(
|
||||
color: Color,
|
||||
darkIcons: Boolean = color.luminance() > 0.5f,
|
||||
transformColorForLightContent: (Color) -> Color = BlackScrimmed
|
||||
)
|
||||
|
||||
/**
|
||||
* Set the navigation bar color.
|
||||
*
|
||||
* @param color The **desired** [Color] to set. This may require modification if running on an
|
||||
* API level that only supports white navigation bar icons. Additionally this will be ignored
|
||||
* and [Color.Transparent] will be used on API 29+ where gesture navigation is preferred or the
|
||||
* system UI automatically applies background protection in other navigation modes.
|
||||
* @param darkIcons Whether dark navigation bar icons would be preferable.
|
||||
* @param navigationBarContrastEnforced Whether the system should ensure that the navigation
|
||||
* bar has enough contrast when a fully transparent background is requested. Only supported on
|
||||
* API 29+.
|
||||
* @param transformColorForLightContent A lambda which will be invoked to transform [color] if
|
||||
* dark icons were requested but are not available. Defaults to applying a black scrim.
|
||||
*
|
||||
* @see navigationBarDarkContentEnabled
|
||||
* @see navigationBarContrastEnforced
|
||||
*/
|
||||
fun setNavigationBarColor(
|
||||
color: Color,
|
||||
darkIcons: Boolean = color.luminance() > 0.5f,
|
||||
navigationBarContrastEnforced: Boolean = true,
|
||||
transformColorForLightContent: (Color) -> Color = BlackScrimmed
|
||||
)
|
||||
|
||||
/**
|
||||
* Set the status and navigation bars to [color].
|
||||
*
|
||||
* @see setStatusBarColor
|
||||
* @see setNavigationBarColor
|
||||
*/
|
||||
fun setSystemBarsColor(
|
||||
color: Color,
|
||||
darkIcons: Boolean = color.luminance() > 0.5f,
|
||||
isNavigationBarContrastEnforced: Boolean = true,
|
||||
transformColorForLightContent: (Color) -> Color = BlackScrimmed
|
||||
) {
|
||||
setStatusBarColor(color, darkIcons, transformColorForLightContent)
|
||||
setNavigationBarColor(
|
||||
color,
|
||||
darkIcons,
|
||||
isNavigationBarContrastEnforced,
|
||||
transformColorForLightContent
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Property which holds whether the status bar icons + content are 'dark' or not.
|
||||
*/
|
||||
var statusBarDarkContentEnabled: Boolean
|
||||
|
||||
/**
|
||||
* Property which holds whether the navigation bar icons + content are 'dark' or not.
|
||||
*/
|
||||
var navigationBarDarkContentEnabled: Boolean
|
||||
|
||||
/**
|
||||
* Property which holds whether the status & navigation bar icons + content are 'dark' or not.
|
||||
*/
|
||||
var systemBarsDarkContentEnabled: Boolean
|
||||
get() = statusBarDarkContentEnabled && navigationBarDarkContentEnabled
|
||||
set(value) {
|
||||
statusBarDarkContentEnabled = value
|
||||
navigationBarDarkContentEnabled = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Property which holds whether the system is ensuring that the navigation bar has enough
|
||||
* contrast when a fully transparent background is requested. Only has an affect when running
|
||||
* on Android API 29+ devices.
|
||||
*/
|
||||
var isNavigationBarContrastEnforced: Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Remembers a [SystemUiController] for the given [window].
|
||||
*
|
||||
* If no [window] is provided, an attempt to find the correct [Window] is made.
|
||||
*
|
||||
* First, if the [LocalView]'s parent is a [DialogWindowProvider], then that dialog's [Window] will
|
||||
* be used.
|
||||
*
|
||||
* Second, we attempt to find [Window] for the [Activity] containing the [LocalView].
|
||||
*
|
||||
* If none of these are found (such as may happen in a preview), then the functionality of the
|
||||
* returned [SystemUiController] will be degraded, but won't throw an exception.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberSystemUiController(
|
||||
window: Window? = findWindow(),
|
||||
): SystemUiController {
|
||||
val view = LocalView.current
|
||||
return remember(view, window) { AndroidSystemUiController(view, window) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun findWindow(): Window? =
|
||||
(LocalView.current.parent as? DialogWindowProvider)?.window
|
||||
?: LocalView.current.context.findWindow()
|
||||
|
||||
private tailrec fun Context.findWindow(): Window? =
|
||||
when (this) {
|
||||
is Activity -> window
|
||||
is ContextWrapper -> baseContext.findWindow()
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper class for setting the navigation and status bar colors for a [View], gracefully
|
||||
* degrading behavior based upon API level.
|
||||
*
|
||||
* Typically you would use [rememberSystemUiController] to remember an instance of this.
|
||||
*/
|
||||
internal class AndroidSystemUiController(
|
||||
private val view: View,
|
||||
private val window: Window?
|
||||
) : SystemUiController {
|
||||
private val windowInsetsController = window?.let {
|
||||
WindowCompat.getInsetsController(it, view)
|
||||
}
|
||||
|
||||
override fun setStatusBarColor(
|
||||
color: Color,
|
||||
darkIcons: Boolean,
|
||||
transformColorForLightContent: (Color) -> Color
|
||||
) {
|
||||
statusBarDarkContentEnabled = darkIcons
|
||||
|
||||
window?.statusBarColor = when {
|
||||
darkIcons && windowInsetsController?.isAppearanceLightStatusBars != true -> {
|
||||
// If we're set to use dark icons, but our windowInsetsController call didn't
|
||||
// succeed (usually due to API level), we instead transform the color to maintain
|
||||
// contrast
|
||||
transformColorForLightContent(color)
|
||||
}
|
||||
|
||||
else -> color
|
||||
}.toArgb()
|
||||
}
|
||||
|
||||
override fun setNavigationBarColor(
|
||||
color: Color,
|
||||
darkIcons: Boolean,
|
||||
navigationBarContrastEnforced: Boolean,
|
||||
transformColorForLightContent: (Color) -> Color
|
||||
) {
|
||||
navigationBarDarkContentEnabled = darkIcons
|
||||
isNavigationBarContrastEnforced = navigationBarContrastEnforced
|
||||
|
||||
window?.navigationBarColor = when {
|
||||
darkIcons && windowInsetsController?.isAppearanceLightNavigationBars != true -> {
|
||||
// If we're set to use dark icons, but our windowInsetsController call didn't
|
||||
// succeed (usually due to API level), we instead transform the color to maintain
|
||||
// contrast
|
||||
transformColorForLightContent(color)
|
||||
}
|
||||
|
||||
else -> color
|
||||
}.toArgb()
|
||||
}
|
||||
|
||||
override var systemBarsBehavior: Int
|
||||
get() = windowInsetsController?.systemBarsBehavior ?: 0
|
||||
set(value) {
|
||||
windowInsetsController?.systemBarsBehavior = value
|
||||
}
|
||||
|
||||
override var isStatusBarVisible: Boolean
|
||||
get() {
|
||||
return ViewCompat.getRootWindowInsets(view)
|
||||
?.isVisible(WindowInsetsCompat.Type.statusBars()) == true
|
||||
}
|
||||
set(value) {
|
||||
if (value) {
|
||||
windowInsetsController?.show(WindowInsetsCompat.Type.statusBars())
|
||||
} else {
|
||||
windowInsetsController?.hide(WindowInsetsCompat.Type.statusBars())
|
||||
}
|
||||
}
|
||||
|
||||
override var isNavigationBarVisible: Boolean
|
||||
get() {
|
||||
return ViewCompat.getRootWindowInsets(view)
|
||||
?.isVisible(WindowInsetsCompat.Type.navigationBars()) == true
|
||||
}
|
||||
set(value) {
|
||||
if (value) {
|
||||
windowInsetsController?.show(WindowInsetsCompat.Type.navigationBars())
|
||||
} else {
|
||||
windowInsetsController?.hide(WindowInsetsCompat.Type.navigationBars())
|
||||
}
|
||||
}
|
||||
|
||||
override var statusBarDarkContentEnabled: Boolean
|
||||
get() = windowInsetsController?.isAppearanceLightStatusBars == true
|
||||
set(value) {
|
||||
windowInsetsController?.isAppearanceLightStatusBars = value
|
||||
}
|
||||
|
||||
override var navigationBarDarkContentEnabled: Boolean
|
||||
get() = windowInsetsController?.isAppearanceLightNavigationBars == true
|
||||
set(value) {
|
||||
windowInsetsController?.isAppearanceLightNavigationBars = value
|
||||
}
|
||||
|
||||
override var isNavigationBarContrastEnforced: Boolean
|
||||
get() = Build.VERSION.SDK_INT >= 29 && window?.isNavigationBarContrastEnforced == true
|
||||
set(value) {
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
window?.isNavigationBarContrastEnforced = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val BlackScrim = Color(0f, 0f, 0f, 0.3f) // 30% opaque black
|
||||
private val BlackScrimmed: (Color) -> Color = { original ->
|
||||
BlackScrim.compositeOverSafe(original)
|
||||
}
|
||||
Reference in New Issue
Block a user