Compare commits
2 Commits
44fe4d963c
...
3ee14eabe6
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ee14eabe6 | |||
| 247f5f31a5 |
@@ -8,7 +8,7 @@ CheckShot 是一个面向 Android 的图片处理与检查工具,包含水印
|
||||
- 地点水印:优先通过 Geocoder 联网解析地址,失败时回落显示经纬度。可在设置中配置校准方式。
|
||||
- 样式:提供三种预设样式(默认/简约/醒目),并可在设置中预览和应用。
|
||||
- 多图拼图(合成)模块
|
||||
- 布局规则:核心布局为 2x2 网格,且支持扩展布局如 1+3、3+1、1+2、2+1、单图等,图片自动缩放裁剪以适配网格。
|
||||
- 布局规则:支持 2x2 和 3x3 两种网格布局,图片自动缩放裁剪以适配网格。
|
||||
- 核心能力:图片拼接、模板化布局编辑(替换/删除图片)、合成质量控制(分辨率/清晰度)。
|
||||
- 交互:支持替换网格中的图片、删除图片、添加新图片、设置合成质量和文本水印文本。
|
||||
- 设置与通用配置
|
||||
|
||||
@@ -60,8 +60,7 @@ enum class ImageQuality(val quality: Int, val displayName: String) {
|
||||
/**
|
||||
* 合成布局类型
|
||||
*/
|
||||
enum class MergeLayoutType(val rows: Int, val cols: Int, val displayName: String) {
|
||||
Grid2x2(2, 2, "2x2网格"),
|
||||
Grid1x3(1, 3, "1+3布局"),
|
||||
Grid3x1(3, 1, "3+1布局")
|
||||
enum class MergeLayoutType(val rows: Int, val cols: Int, val displayName: String, val maxImages: Int) {
|
||||
Grid2x2(2, 2, "2x2网格", 4),
|
||||
Grid3x3(3, 3, "3x3网格", 9)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.inspection.camera.ui.camera
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.provider.Settings
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
@@ -119,6 +121,23 @@ fun CameraScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否有权限被永久拒绝
|
||||
val hasPermanentlyDeniedPermission = remember(permissionsState) {
|
||||
permissionsState.permissions.any { permissionState ->
|
||||
val status = permissionState.status
|
||||
status is com.google.accompanist.permissions.PermissionStatus.Denied &&
|
||||
!status.shouldShowRationale
|
||||
}
|
||||
}
|
||||
|
||||
// 打开应用设置页面
|
||||
val openAppSettings = {
|
||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
data = Uri.parse("package:${context.packageName}")
|
||||
}
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
// 加载配置
|
||||
LaunchedEffect(Unit) {
|
||||
preferencesManager.watermarkStyle.collect { style ->
|
||||
@@ -177,7 +196,7 @@ fun CameraScreen(
|
||||
flashMode = flashMode,
|
||||
watermarkStyle = currentWatermarkStyle,
|
||||
imageQuality = currentImageQuality,
|
||||
locationText = if (locationText.isNotBlank()) locationText else "未知地点",
|
||||
locationText = getValidLocationTextForPhoto(locationText, manualAddress, locationHelper),
|
||||
onComplete = { uri ->
|
||||
capturedImages.add(uri)
|
||||
isCapturing = false
|
||||
@@ -198,7 +217,9 @@ fun CameraScreen(
|
||||
PermissionRequest(
|
||||
onRequestPermission = { permissionsState.launchMultiplePermissionRequest() },
|
||||
showDialog = showPermissionDeniedDialog,
|
||||
onDismissDialog = { showPermissionDeniedDialog = false }
|
||||
onDismissDialog = { showPermissionDeniedDialog = false },
|
||||
hasPermanentlyDeniedPermission = hasPermanentlyDeniedPermission,
|
||||
openAppSettings = openAppSettings
|
||||
)
|
||||
}
|
||||
|
||||
@@ -400,7 +421,9 @@ private fun BottomControls(
|
||||
private fun PermissionRequest(
|
||||
onRequestPermission: () -> Unit,
|
||||
showDialog: Boolean,
|
||||
onDismissDialog: () -> Unit
|
||||
onDismissDialog: () -> Unit,
|
||||
hasPermanentlyDeniedPermission: Boolean,
|
||||
openAppSettings: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -415,14 +438,48 @@ private fun PermissionRequest(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "请授予权限以使用拍照和地点水印功能",
|
||||
text = if (hasPermanentlyDeniedPermission)
|
||||
"权限被永久拒绝,请在设置中手动开启权限"
|
||||
else
|
||||
"请授予权限以使用拍照和地点水印功能",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
if (hasPermanentlyDeniedPermission) {
|
||||
Button(onClick = openAppSettings) {
|
||||
Text("打开设置")
|
||||
}
|
||||
} else {
|
||||
Button(onClick = onRequestPermission) {
|
||||
Text("授予权限")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getValidLocationTextForPhoto(
|
||||
currentLocationText: String,
|
||||
manualAddress: String,
|
||||
locationHelper: LocationHelper
|
||||
): String {
|
||||
// 检查当前定位文本是否有效
|
||||
val invalidTexts = listOf("正在定位...", "定位失败", "请授予定位权限")
|
||||
val isInvalid = currentLocationText.isBlank() || invalidTexts.contains(currentLocationText)
|
||||
|
||||
if (!isInvalid) {
|
||||
return currentLocationText
|
||||
}
|
||||
|
||||
// 使用手动地址
|
||||
if (manualAddress.isNotBlank()) {
|
||||
return manualAddress
|
||||
}
|
||||
|
||||
// 尝试快速获取当前位置(使用缓存)
|
||||
val location = locationHelper.getCurrentLocation()
|
||||
return location?.let {
|
||||
"${"%.4f".format(it.latitude)}, ${"%.4f".format(it.longitude)}"
|
||||
} ?: "未知地点"
|
||||
}
|
||||
|
||||
private fun capturePhoto(
|
||||
|
||||
@@ -85,7 +85,9 @@ fun MergeScreen(
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val images = remember { mutableStateListOf<Uri>().apply { addAll(imageUris) } }
|
||||
data class ImageWithCache(val uri: Uri, val cachePath: String?)
|
||||
|
||||
val images = remember { mutableStateListOf<ImageWithCache>() }
|
||||
var layoutType by remember { mutableStateOf(MergeLayoutType.Grid2x2) }
|
||||
var imageQuality by remember { mutableStateOf(ImageQuality.Standard) }
|
||||
var titleStyle by remember { mutableStateOf(WatermarkStyle.Default) }
|
||||
@@ -97,16 +99,48 @@ fun MergeScreen(
|
||||
var showSaveDialog by remember { mutableStateOf(false) }
|
||||
var selectedImageIndex by remember { mutableStateOf(-1) }
|
||||
|
||||
// 将 URI 图片复制到缓存目录
|
||||
suspend fun copyImageToCache(uri: android.net.Uri): String? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val input = context.contentResolver.openInputStream(uri) ?: return@withContext null
|
||||
val cacheDir = context.cacheDir
|
||||
val imgDir = java.io.File(cacheDir, "merge_images")
|
||||
if (!imgDir.exists()) {
|
||||
imgDir.mkdirs()
|
||||
}
|
||||
val fileName = "img_${System.nanoTime()}.jpg"
|
||||
val outFile = java.io.File(imgDir, fileName)
|
||||
java.io.FileOutputStream(outFile).use { output ->
|
||||
input.use { inStream -> inStream.copyTo(output) }
|
||||
}
|
||||
outFile.absolutePath
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 图片选择器
|
||||
val imagePickerLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.GetMultipleContents()
|
||||
) { uris ->
|
||||
if (uris.isNotEmpty()) {
|
||||
scope.launch {
|
||||
if (selectedImageIndex >= 0 && selectedImageIndex < images.size) {
|
||||
images[selectedImageIndex] = uris.first()
|
||||
// 替换指定位置的图片
|
||||
val path = copyImageToCache(uris.first())
|
||||
if (path != null) {
|
||||
images[selectedImageIndex] = ImageWithCache(uris.first(), path)
|
||||
}
|
||||
} else {
|
||||
if (images.size < layoutType.maxImages) {
|
||||
images.addAll(uris.take(layoutType.maxImages - images.size))
|
||||
// 添加新图片
|
||||
uris.take(layoutType.maxImages - images.size).forEach { uri ->
|
||||
val path = copyImageToCache(uri)
|
||||
if (path != null) {
|
||||
images.add(ImageWithCache(uri, path))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -208,7 +242,7 @@ fun MergeScreen(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
itemsIndexed(images) { index, uri ->
|
||||
itemsIndexed(images) { index, imageWithCache ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.aspectRatio(1f)
|
||||
@@ -221,7 +255,7 @@ fun MergeScreen(
|
||||
) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(context)
|
||||
.data(uri)
|
||||
.data(imageWithCache.uri)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
contentDescription = null,
|
||||
@@ -321,11 +355,14 @@ fun MergeScreen(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
previewBitmap = withContext(Dispatchers.Default) {
|
||||
val imageItems = images.map { uri ->
|
||||
val path = convertUriToPath(context, uri)
|
||||
ImageItem(uri = uri, path = path ?: uri.toString())
|
||||
val imageItems = images.map { img ->
|
||||
ImageItem(
|
||||
uri = img.uri,
|
||||
path = img.cachePath ?: img.uri.toString()
|
||||
)
|
||||
}
|
||||
ImageProcessor.mergeImages(
|
||||
context,
|
||||
imageItems,
|
||||
layoutType,
|
||||
imageQuality
|
||||
@@ -392,8 +429,14 @@ fun MergeScreen(
|
||||
TextButton(onClick = {
|
||||
scope.launch {
|
||||
val bitmap = withContext(Dispatchers.Default) {
|
||||
val imageItems = images.map { ImageItem(uri = it, path = it.toString()) }
|
||||
val imageItems = images.map { img ->
|
||||
ImageItem(
|
||||
uri = img.uri,
|
||||
path = img.cachePath ?: img.uri.toString()
|
||||
)
|
||||
}
|
||||
ImageProcessor.mergeImages(
|
||||
context,
|
||||
imageItems,
|
||||
layoutType,
|
||||
imageQuality
|
||||
@@ -438,11 +481,7 @@ private fun LayoutOption(
|
||||
) {
|
||||
val displayText = when (layout) {
|
||||
MergeLayoutType.Grid2x2 -> "2x2"
|
||||
MergeLayoutType.Grid1x3 -> "1+3"
|
||||
MergeLayoutType.Grid3x1 -> "3+1"
|
||||
MergeLayoutType.Grid1x2 -> "1+2"
|
||||
MergeLayoutType.Grid2x1 -> "2+1"
|
||||
MergeLayoutType.Grid1x1 -> "单图"
|
||||
MergeLayoutType.Grid3x3 -> "3x3"
|
||||
}
|
||||
|
||||
Box(
|
||||
@@ -468,19 +507,3 @@ private fun LayoutOption(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 将 Content Uri 拷贝到缓存目录,返回本地文件路径(可被 BitmapFactory.decodeFile 使用)
|
||||
private suspend fun convertUriToPath(context: android.content.Context, uri: android.net.Uri): String? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val input = context.contentResolver.openInputStream(uri) ?: return@withContext null
|
||||
val tmpFile = java.io.File(context.cacheDir, "img_${System.nanoTime()}.jpg")
|
||||
java.io.FileOutputStream(tmpFile).use { output ->
|
||||
input.use { inStream -> inStream.copyTo(output) }
|
||||
}
|
||||
tmpFile.absolutePath
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,11 @@ import com.google.android.gms.location.LocationRequest
|
||||
import com.google.android.gms.location.LocationResult
|
||||
import com.google.android.gms.location.LocationServices
|
||||
import com.google.android.gms.location.Priority
|
||||
import android.location.LocationManager
|
||||
import android.location.LocationListener
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.tasks.await
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import java.util.Locale
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
@@ -30,14 +33,157 @@ class LocationHelper(private val context: Context) {
|
||||
Geocoder(context, Locale.getDefault())
|
||||
}
|
||||
|
||||
// 位置缓存
|
||||
private var lastLocation: Location? = null
|
||||
private var lastLocationTime: Long = 0
|
||||
private val LOCATION_CACHE_VALID_MS = 30000 // 30秒缓存有效
|
||||
|
||||
/**
|
||||
* 获取当前位置
|
||||
* 检查定位服务是否启用(GPS或网络)
|
||||
*/
|
||||
fun isLocationEnabled(): Boolean {
|
||||
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) ||
|
||||
locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前位置(带缓存)
|
||||
*/
|
||||
@SuppressLint("MissingPermission")
|
||||
suspend fun getCurrentLocation(): Location? {
|
||||
// 检查缓存是否有效
|
||||
if (lastLocation != null && System.currentTimeMillis() - lastLocationTime < LOCATION_CACHE_VALID_MS) {
|
||||
Log.d("LocationHelper", "Using cached location: $lastLocation")
|
||||
return lastLocation
|
||||
}
|
||||
|
||||
// 检查定位服务是否启用
|
||||
if (!isLocationEnabled()) {
|
||||
Log.w("LocationHelper", "Location services disabled")
|
||||
return null
|
||||
}
|
||||
|
||||
var result: Location? = null
|
||||
|
||||
// 并行尝试多种定位方式,使用最快的
|
||||
result = tryHmsLocation()
|
||||
if (result == null) {
|
||||
result = tryGmsLocation()
|
||||
}
|
||||
if (result == null) {
|
||||
result = getNetworkLocationFallback()
|
||||
}
|
||||
|
||||
// 更新缓存
|
||||
if (result != null) {
|
||||
lastLocation = result
|
||||
lastLocationTime = System.currentTimeMillis()
|
||||
Log.d("LocationHelper", "Location obtained and cached: $result")
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private suspend fun tryGmsLocation(): Location? {
|
||||
return try {
|
||||
fusedLocationClient.lastLocation.await()
|
||||
Log.d("LocationHelper", "Requesting GMS last location...")
|
||||
// 先尝试获取最后已知位置(最快)
|
||||
val lastLocation = fusedLocationClient.lastLocation.await()
|
||||
if (lastLocation != null) {
|
||||
Log.d("LocationHelper", "Got GMS last location: $lastLocation")
|
||||
return lastLocation
|
||||
}
|
||||
|
||||
// 最后位置为空,请求新位置(快速低精度)
|
||||
Log.d("LocationHelper", "Last location null, requesting fresh location...")
|
||||
requestFastLocation()
|
||||
} catch (e: Exception) {
|
||||
Log.e("LocationHelper", "GMS location failed", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// HMS 位置服务反射实现(无编译时依赖)
|
||||
@Suppress("MissingPermission")
|
||||
private suspend fun tryHmsLocation(): Location? {
|
||||
Log.d("LocationHelper", "Attempt HMS location via reflection...")
|
||||
return try {
|
||||
val servicesClass = Class.forName("com.huawei.hms.location.LocationServices")
|
||||
val getClient = servicesClass.getMethod("getFusedLocationProviderClient", Context::class.java)
|
||||
val client = getClient.invoke(null, context)
|
||||
val clientClass = client!!.javaClass
|
||||
val getLastLocation = clientClass.getMethod("getLastLocation")
|
||||
val task = getLastLocation.invoke(client)
|
||||
val taskClass = task!!.javaClass
|
||||
val latch = java.util.concurrent.CountDownLatch(1)
|
||||
var result: Location? = null
|
||||
val onSuccess = java.lang.reflect.Proxy.newProxyInstance(
|
||||
LocationHelper::class.java.classLoader,
|
||||
arrayOf(Class.forName("com.huawei.hmf.tasks.OnSuccessListener")),
|
||||
java.lang.reflect.InvocationHandler { _, method, args ->
|
||||
if (method.name == "onSuccess" && args != null) {
|
||||
result = args[0] as Location
|
||||
latch.countDown()
|
||||
}
|
||||
null
|
||||
}
|
||||
)
|
||||
val onFailure = java.lang.reflect.Proxy.newProxyInstance(
|
||||
LocationHelper::class.java.classLoader,
|
||||
arrayOf(Class.forName("com.huawei.hmf.tasks.OnFailureListener")),
|
||||
java.lang.reflect.InvocationHandler { _, method, args ->
|
||||
if (method.name == "onFailure" && args != null) {
|
||||
latch.countDown()
|
||||
}
|
||||
null
|
||||
}
|
||||
)
|
||||
val addSuccess = taskClass.getMethod("addOnSuccessListener", Class.forName("com.huawei.hmf.tasks.OnSuccessListener"))
|
||||
val addFailure = taskClass.getMethod("addOnFailureListener", Class.forName("com.huawei.hmf.tasks.OnFailureListener"))
|
||||
addSuccess.invoke(task, onSuccess)
|
||||
addFailure.invoke(task, onFailure)
|
||||
latch.await(5000, java.util.concurrent.TimeUnit.MILLISECONDS)
|
||||
result
|
||||
} catch (t: Throwable) {
|
||||
Log.e("LocationHelper", "HMS location reflection failed", t)
|
||||
Log.e("LocationHelper", "HMS location fallback failed", t)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private suspend fun requestFastLocation(): Location? {
|
||||
return try {
|
||||
Log.d("LocationHelper", "Creating fast location request...")
|
||||
// 使用平衡功耗精度,更快获取位置
|
||||
val locationRequest = LocationRequest.Builder(Priority.PRIORITY_BALANCED_POWER_ACCURACY, 5000)
|
||||
.setWaitForAccurateLocation(false)
|
||||
.setMinUpdateIntervalMillis(1000)
|
||||
.setMaxUpdateDelayMillis(8000)
|
||||
.build()
|
||||
|
||||
withTimeoutOrNull(8000) {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
Log.d("LocationHelper", "Requesting fast location updates...")
|
||||
val callback = object : LocationCallback() {
|
||||
override fun onLocationResult(result: LocationResult) {
|
||||
Log.d("LocationHelper", "Got fast location result: ${result.lastLocation}")
|
||||
fusedLocationClient.removeLocationUpdates(this)
|
||||
continuation.resume(result.lastLocation)
|
||||
}
|
||||
}
|
||||
fusedLocationClient.requestLocationUpdates(locationRequest, callback, Looper.getMainLooper())
|
||||
|
||||
continuation.invokeOnCancellation {
|
||||
Log.d("LocationHelper", "Fast location request cancelled")
|
||||
fusedLocationClient.removeLocationUpdates(callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("LocationHelper", "Request fast location failed", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -48,7 +194,9 @@ class LocationHelper(private val context: Context) {
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getAddressFromLocation(latitude: Double, longitude: Double): String {
|
||||
return try {
|
||||
Log.d("LocationHelper", "Getting address for: $latitude, $longitude")
|
||||
val addresses = geocoder.getFromLocation(latitude, longitude, 1)
|
||||
Log.d("LocationHelper", "Geocoder results: $addresses")
|
||||
if (!addresses.isNullOrEmpty()) {
|
||||
val address = addresses[0]
|
||||
buildString {
|
||||
@@ -60,6 +208,7 @@ class LocationHelper(private val context: Context) {
|
||||
"${"%.4f".format(latitude)}, ${"%.4f".format(longitude)}"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("LocationHelper", "Geocoder error", e)
|
||||
"${"%.4f".format(latitude)}, ${"%.4f".format(longitude)}"
|
||||
}
|
||||
}
|
||||
@@ -79,6 +228,33 @@ class LocationHelper(private val context: Context) {
|
||||
Log.d("LocationHelper", "Network location: $location")
|
||||
return location?.let { getAddressFromLocation(it.latitude, it.longitude) } ?: ""
|
||||
}
|
||||
|
||||
// 备选方案:当设备没有 Google Play Services 时,尝试使用 Android 原生 LocationManager 获取网络定位
|
||||
internal suspend fun getNetworkLocationFallback(): Location? {
|
||||
return try {
|
||||
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
val provider = LocationManager.NETWORK_PROVIDER
|
||||
val enabled = locationManager.isProviderEnabled(provider)
|
||||
if (!enabled) return null
|
||||
|
||||
suspendCancellableCoroutine { cont ->
|
||||
val listener = object : LocationListener {
|
||||
override fun onLocationChanged(location: Location) {
|
||||
locationManager.removeUpdates(this)
|
||||
cont.resume(location)
|
||||
}
|
||||
override fun onStatusChanged(provider: String?, status: Int, extras: android.os.Bundle?) { /* no-op */ }
|
||||
override fun onProviderEnabled(provider: String) { /* no-op */ }
|
||||
override fun onProviderDisabled(provider: String) { /* no-op */ }
|
||||
}
|
||||
locationManager.requestLocationUpdates(provider, 0L, 0f, listener, Looper.getMainLooper())
|
||||
cont.invokeOnCancellation { locationManager.removeUpdates(listener) }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("LocationHelper", "Network fallback location failed", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T> com.google.android.gms.tasks.Task<T>.await(): T {
|
||||
|
||||
Reference in New Issue
Block a user