fix: 修复CameraScreen编译错误,整合文件名模板功能
This commit is contained in:
Binary file not shown.
@@ -33,6 +33,7 @@ class PreferencesManager(private val context: Context) {
|
|||||||
private val KEY_MERGE_TITLE = stringPreferencesKey("merge_title")
|
private val KEY_MERGE_TITLE = stringPreferencesKey("merge_title")
|
||||||
private val KEY_MERGE_CONTENT = stringPreferencesKey("merge_content")
|
private val KEY_MERGE_CONTENT = stringPreferencesKey("merge_content")
|
||||||
private val KEY_RECORDER_NAME = stringPreferencesKey("recorder_name")
|
private val KEY_RECORDER_NAME = stringPreferencesKey("recorder_name")
|
||||||
|
private val KEY_FILE_NAME_TEMPLATE = stringPreferencesKey("file_name_template")
|
||||||
}
|
}
|
||||||
|
|
||||||
val watermarkStyle: Flow<WatermarkStyle> = context.dataStore.data.map { prefs ->
|
val watermarkStyle: Flow<WatermarkStyle> = context.dataStore.data.map { prefs ->
|
||||||
@@ -104,6 +105,10 @@ class PreferencesManager(private val context: Context) {
|
|||||||
prefs[KEY_RECORDER_NAME] ?: ""
|
prefs[KEY_RECORDER_NAME] ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val fileNameTemplate: Flow<String> = context.dataStore.data.map { prefs ->
|
||||||
|
prefs[KEY_FILE_NAME_TEMPLATE] ?: "{project}_{inspector}_{date}"
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun setWatermarkStyle(style: WatermarkStyle) {
|
suspend fun setWatermarkStyle(style: WatermarkStyle) {
|
||||||
context.dataStore.edit { prefs ->
|
context.dataStore.edit { prefs ->
|
||||||
prefs[KEY_WATERMARK_STYLE] = style.name
|
prefs[KEY_WATERMARK_STYLE] = style.name
|
||||||
@@ -169,4 +174,10 @@ class PreferencesManager(private val context: Context) {
|
|||||||
prefs[KEY_MERGE_CONTENT] = content
|
prefs[KEY_MERGE_CONTENT] = content
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun setFileNameTemplate(template: String) {
|
||||||
|
context.dataStore.edit { prefs ->
|
||||||
|
prefs[KEY_FILE_NAME_TEMPLATE] = template
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,9 +49,10 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
@@ -76,8 +77,8 @@ import com.inspection.camera.util.ImageProcessor
|
|||||||
import com.inspection.camera.util.LocationHelper
|
import com.inspection.camera.util.LocationHelper
|
||||||
import com.inspection.camera.util.PermissionManager
|
import com.inspection.camera.util.PermissionManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -90,7 +91,7 @@ fun CameraScreen(
|
|||||||
onNavigateToSettings: () -> Unit,
|
onNavigateToSettings: () -> Unit,
|
||||||
onNavigateToMerge: (List<Uri>) -> Unit,
|
onNavigateToMerge: (List<Uri>) -> Unit,
|
||||||
preferencesManager: PreferencesManager
|
preferencesManager: PreferencesManager
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
@@ -100,25 +101,46 @@ fun CameraScreen(
|
|||||||
var flashMode by remember { mutableIntStateOf(ImageCapture.FLASH_MODE_AUTO) }
|
var flashMode by remember { mutableIntStateOf(ImageCapture.FLASH_MODE_AUTO) }
|
||||||
var locationText by remember { mutableStateOf("") }
|
var locationText by remember { mutableStateOf("") }
|
||||||
var recorderName by remember { mutableStateOf("") }
|
var recorderName by remember { mutableStateOf("") }
|
||||||
|
var fileNameTemplate by remember { mutableStateOf("{project}_{inspector}_{date}") }
|
||||||
var isLocationLoading by remember { mutableStateOf(true) }
|
var isLocationLoading by remember { mutableStateOf(true) }
|
||||||
var currentWatermarkStyle by remember { mutableStateOf(WatermarkStyle.Default) }
|
var currentWatermarkStyle by remember { mutableStateOf(WatermarkStyle.Default) }
|
||||||
var currentImageQuality by remember { mutableStateOf(ImageQuality.Standard) }
|
var currentImageQuality by remember { mutableStateOf(ImageQuality.Standard) }
|
||||||
var locationMode by remember { mutableStateOf(LocationMode.Network) }
|
var locationMode by remember { mutableStateOf(LocationMode.Network) }
|
||||||
var showPermissionDeniedDialog by remember { mutableStateOf(false) }
|
var showPermissionDeniedDialog by remember { mutableStateOf(false) }
|
||||||
|
var flashVisible by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val capturedImages = remember { mutableStateListOf<Uri>() }
|
val capturedImages = remember { mutableStateListOf<Uri>() }
|
||||||
val locationHelper = remember { LocationHelper(context) }
|
val locationHelper = remember { LocationHelper(context) }
|
||||||
val shutterSound = remember { MediaActionSound() }
|
val shutterSound = remember { MediaActionSound() }
|
||||||
var flashVisible by remember { mutableStateOf(false) }
|
|
||||||
|
val deviceId = Settings.Secure.getString(
|
||||||
|
context.contentResolver,
|
||||||
|
Settings.Secure.ANDROID_ID
|
||||||
|
) ?: ""
|
||||||
|
|
||||||
|
val computedFileName by remember(recorderName, fileNameTemplate) {
|
||||||
|
derivedStateOf {
|
||||||
|
ImageProcessor.generateFileName(
|
||||||
|
template = fileNameTemplate,
|
||||||
|
projectName = "",
|
||||||
|
deviceId = deviceId,
|
||||||
|
inspectorName = recorderName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
shutterSound.load(MediaActionSound.SHUTTER_CLICK)
|
shutterSound.load(MediaActionSound.SHUTTER_CLICK)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取记录人信息(偏好)
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
preferencesManager.recorderName.collect { recorderName = it }
|
preferencesManager.recorderName.collect { recorderName = it }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
preferencesManager.fileNameTemplate.collect { fileNameTemplate = it }
|
||||||
|
}
|
||||||
|
|
||||||
// 权限状态
|
// 权限状态
|
||||||
val permissionsState = rememberMultiplePermissionsState(
|
val permissionsState = rememberMultiplePermissionsState(
|
||||||
permissions = listOf(
|
permissions = listOf(
|
||||||
@@ -205,14 +227,14 @@ fun CameraScreen(
|
|||||||
isLocationLoading = false
|
isLocationLoading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
// 闪屏效果覆盖层(拍照成功时显示)
|
// 闪屏效果覆盖层(拍照成功时显示)
|
||||||
if (flashVisible) {
|
if (flashVisible) {
|
||||||
Box(modifier = Modifier
|
Box(modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(Color.White.copy(alpha = 0.9f))
|
.background(Color.White.copy(alpha = 0.9f))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (permissionsState.allPermissionsGranted) {
|
if (permissionsState.allPermissionsGranted) {
|
||||||
CameraContent(
|
CameraContent(
|
||||||
flashMode = flashMode,
|
flashMode = flashMode,
|
||||||
@@ -221,6 +243,11 @@ fun CameraScreen(
|
|||||||
if (!isCapturing) {
|
if (!isCapturing) {
|
||||||
isCapturing = true
|
isCapturing = true
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
val location = try {
|
||||||
|
locationHelper.getCurrentLocation()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
capturePhoto(
|
capturePhoto(
|
||||||
context = context,
|
context = context,
|
||||||
imageCapture = imageCapture,
|
imageCapture = imageCapture,
|
||||||
@@ -228,10 +255,11 @@ fun CameraScreen(
|
|||||||
watermarkStyle = currentWatermarkStyle,
|
watermarkStyle = currentWatermarkStyle,
|
||||||
imageQuality = currentImageQuality,
|
imageQuality = currentImageQuality,
|
||||||
locationText = getValidLocationTextForPhoto(locationText, locationHelper),
|
locationText = getValidLocationTextForPhoto(locationText, locationHelper),
|
||||||
|
latitude = location?.latitude,
|
||||||
|
longitude = location?.longitude,
|
||||||
onComplete = { uri ->
|
onComplete = { uri ->
|
||||||
capturedImages.add(uri)
|
capturedImages.add(uri)
|
||||||
isCapturing = false
|
isCapturing = false
|
||||||
|
|
||||||
},
|
},
|
||||||
onError = {
|
onError = {
|
||||||
isCapturing = false
|
isCapturing = false
|
||||||
@@ -241,7 +269,8 @@ fun CameraScreen(
|
|||||||
flashVisible = true
|
flashVisible = true
|
||||||
scope.launch { delay(150); flashVisible = false }
|
scope.launch { delay(150); flashVisible = false }
|
||||||
},
|
},
|
||||||
recorderName = recorderName
|
recorderName = recorderName,
|
||||||
|
fileName = computedFileName
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -340,7 +369,7 @@ private fun CameraContent(
|
|||||||
|
|
||||||
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
||||||
|
|
||||||
try {
|
try {
|
||||||
cameraProvider.unbindAll()
|
cameraProvider.unbindAll()
|
||||||
cameraProvider.bindToLifecycle(
|
cameraProvider.bindToLifecycle(
|
||||||
lifecycleOwner,
|
lifecycleOwner,
|
||||||
@@ -372,11 +401,6 @@ private fun CameraContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 强制重组:当 flashMode 变化时触发
|
|
||||||
LaunchedEffect(flashMode) {
|
|
||||||
// 闪光灯变化时确保相机已绑定
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
// 相机预览
|
// 相机预览
|
||||||
AndroidView(
|
AndroidView(
|
||||||
@@ -523,7 +547,6 @@ private suspend fun getValidLocationTextForPhoto(
|
|||||||
): String {
|
): String {
|
||||||
Log.d("CameraScreen", "getValidLocationTextForPhoto called, currentLocationText=$currentLocationText")
|
Log.d("CameraScreen", "getValidLocationTextForPhoto called, currentLocationText=$currentLocationText")
|
||||||
|
|
||||||
// 检查当前定位文本是否有效
|
|
||||||
val invalidTexts = listOf("正在定位...", "定位失败", "请授予定位权限", "GPS定位失败")
|
val invalidTexts = listOf("正在定位...", "定位失败", "请授予定位权限", "GPS定位失败")
|
||||||
val isInvalid = currentLocationText.isBlank() || invalidTexts.contains(currentLocationText)
|
val isInvalid = currentLocationText.isBlank() || invalidTexts.contains(currentLocationText)
|
||||||
|
|
||||||
@@ -532,7 +555,6 @@ private suspend fun getValidLocationTextForPhoto(
|
|||||||
return currentLocationText
|
return currentLocationText
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试快速获取当前位置(使用缓存)
|
|
||||||
Log.d("CameraScreen", "Requesting new location...")
|
Log.d("CameraScreen", "Requesting new location...")
|
||||||
val location = locationHelper.getCurrentLocation()
|
val location = locationHelper.getCurrentLocation()
|
||||||
val result = location?.let {
|
val result = location?.let {
|
||||||
@@ -549,10 +571,13 @@ private fun capturePhoto(
|
|||||||
watermarkStyle: WatermarkStyle,
|
watermarkStyle: WatermarkStyle,
|
||||||
imageQuality: ImageQuality,
|
imageQuality: ImageQuality,
|
||||||
locationText: String,
|
locationText: String,
|
||||||
|
latitude: Double? = null,
|
||||||
|
longitude: Double? = null,
|
||||||
onComplete: (Uri) -> Unit,
|
onComplete: (Uri) -> Unit,
|
||||||
onError: () -> Unit = {},
|
onError: () -> Unit = {},
|
||||||
onFeedback: (() -> Unit)? = null,
|
onFeedback: (() -> Unit)? = null,
|
||||||
recorderName: String = ""
|
recorderName: String = "",
|
||||||
|
fileName: String = ""
|
||||||
) {
|
) {
|
||||||
Log.d("CameraScreen", "capturePhoto called, locationText=$locationText")
|
Log.d("CameraScreen", "capturePhoto called, locationText=$locationText")
|
||||||
val photoFile = File(
|
val photoFile = File(
|
||||||
@@ -563,7 +588,6 @@ private fun capturePhoto(
|
|||||||
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
|
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
|
||||||
val executor = Executors.newSingleThreadExecutor()
|
val executor = Executors.newSingleThreadExecutor()
|
||||||
|
|
||||||
// 拍照时立即播放快门声音和闪屏效果
|
|
||||||
onFeedback?.invoke()
|
onFeedback?.invoke()
|
||||||
|
|
||||||
imageCapture.takePicture(
|
imageCapture.takePicture(
|
||||||
@@ -579,8 +603,8 @@ private fun capturePhoto(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val timeText = ImageProcessor.getCurrentTimeText()
|
val timeText = ImageProcessor.getCurrentTimeText()
|
||||||
Log.d("CameraScreen", "Adding watermark: timeText=$timeText, locationText=$locationText")
|
Log.d("CameraScreen", "Adding watermark: timeText=$timeText, locationText=$locationText")
|
||||||
val watermarkedBitmap = ImageProcessor.addWatermark(
|
val watermarkedBitmap = ImageProcessor.addWatermark(
|
||||||
bitmap,
|
bitmap,
|
||||||
@@ -590,9 +614,10 @@ private fun capturePhoto(
|
|||||||
recorderName
|
recorderName
|
||||||
)
|
)
|
||||||
|
|
||||||
// 保存到相册
|
val effectiveFileName = fileName.ifBlank {
|
||||||
val fileName = ImageProcessor.generateFileName("")
|
ImageProcessor.generateFileName(inspectorName = recorderName)
|
||||||
val uri = ImageProcessor.saveToGallery(context, watermarkedBitmap, fileName, imageQuality.quality)
|
}
|
||||||
|
val uri = ImageProcessor.saveToGallery(context, watermarkedBitmap, effectiveFileName, imageQuality.quality, latitude, longitude)
|
||||||
|
|
||||||
bitmap.recycle()
|
bitmap.recycle()
|
||||||
watermarkedBitmap.recycle()
|
watermarkedBitmap.recycle()
|
||||||
|
|||||||
@@ -76,10 +76,6 @@ fun GalleryScreen(onNavigateBack: () -> Unit) {
|
|||||||
var isSelectionMode by remember { mutableStateOf(false) }
|
var isSelectionMode by remember { mutableStateOf(false) }
|
||||||
var selectedImageUri by remember { mutableStateOf<Uri?>(null) }
|
var selectedImageUri by remember { mutableStateOf<Uri?>(null) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
images = loadImagesFromGallery(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
val sections: List<GallerySection> = remember(images) {
|
val sections: List<GallerySection> = remember(images) {
|
||||||
categorizeImages(images)
|
categorizeImages(images)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -601,7 +601,7 @@ fun MergeScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val fileName = ImageProcessor.generateFileName(title.ifBlank { "合成" })
|
val fileName = ImageProcessor.generateFileName(projectName = title.ifBlank { "合成" })
|
||||||
val uri = ImageProcessor.saveToGallery(context, bitmap, fileName)
|
val uri = ImageProcessor.saveToGallery(context, bitmap, fileName)
|
||||||
uri?.let { onMergeComplete(it) }
|
uri?.let { onMergeComplete(it) }
|
||||||
showSaveDialog = false
|
showSaveDialog = false
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ object ImageProcessor {
|
|||||||
|
|
||||||
private val dateFormat = SimpleDateFormat("yyyy年M月d日 HH:mm:ss", Locale.getDefault())
|
private val dateFormat = SimpleDateFormat("yyyy年M月d日 HH:mm:ss", Locale.getDefault())
|
||||||
private val fileNameFormat = SimpleDateFormat("yyyyMMddHHmm", Locale.getDefault())
|
private val fileNameFormat = SimpleDateFormat("yyyyMMddHHmm", Locale.getDefault())
|
||||||
|
private val dateOnlyFormat = SimpleDateFormat("yyyyMMdd", Locale.getDefault())
|
||||||
|
private val timeOnlyFormat = SimpleDateFormat("HHmmss", Locale.getDefault())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 尝试加载图片 Bitmap
|
* 尝试加载图片 Bitmap
|
||||||
@@ -83,14 +85,39 @@ object ImageProcessor {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成文件名
|
* 生成文件名
|
||||||
|
* 支持模板:{project}_{device}_{inspector}_{date}_{time}.jpg
|
||||||
*/
|
*/
|
||||||
fun generateFileName(theme: String): String {
|
fun generateFileName(
|
||||||
val timeStr = fileNameFormat.format(Date())
|
template: String = "{project}_{inspector}_{date}",
|
||||||
return if (theme.isNotBlank()) {
|
projectName: String = "",
|
||||||
"巡检报告_${theme}_$timeStr.jpg"
|
deviceId: String = "",
|
||||||
} else {
|
inspectorName: String = ""
|
||||||
"巡检报告_$timeStr.jpg"
|
): String {
|
||||||
|
val now = Date()
|
||||||
|
val dateStr = dateOnlyFormat.format(now)
|
||||||
|
val timeStr = timeOnlyFormat.format(now)
|
||||||
|
|
||||||
|
var fileName = template
|
||||||
|
.replace("{project}", projectName.ifBlank { "巡检" })
|
||||||
|
.replace("{device}", deviceId)
|
||||||
|
.replace("{inspector}", inspectorName)
|
||||||
|
.replace("{date}", dateStr)
|
||||||
|
.replace("{time}", timeStr)
|
||||||
|
|
||||||
|
// 清理可能的双下划线
|
||||||
|
fileName = fileName.replace("__", "_")
|
||||||
|
|
||||||
|
// 确保文件名不为空
|
||||||
|
if (fileName.isBlank() || fileName == ".jpg") {
|
||||||
|
fileName = "巡检_${dateStr}_${timeStr}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 确保以 .jpg 结尾
|
||||||
|
if (!fileName.endsWith(".jpg", ignoreCase = true)) {
|
||||||
|
fileName += ".jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileName
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -24,16 +24,14 @@ class ImageProcessorTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `test generateFileName with theme`() {
|
fun `test generateFileName with theme`() {
|
||||||
val theme = "日常巡检"
|
val theme = "日常巡检"
|
||||||
val result = ImageProcessor.generateFileName(theme)
|
val result = ImageProcessor.generateFileName(projectName = theme)
|
||||||
assertTrue(result.contains("巡检报告_"))
|
|
||||||
assertTrue(result.contains(theme))
|
assertTrue(result.contains(theme))
|
||||||
assertTrue(result.endsWith(".jpg"))
|
assertTrue(result.endsWith(".jpg"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test generateFileName without theme`() {
|
fun `test generateFileName without theme`() {
|
||||||
val result = ImageProcessor.generateFileName("")
|
val result = ImageProcessor.generateFileName(projectName = "")
|
||||||
assertTrue(result.contains("巡检报告_"))
|
|
||||||
assertTrue(result.endsWith(".jpg"))
|
assertTrue(result.endsWith(".jpg"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +47,7 @@ class ImageProcessorTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test ImageQuality values`() {
|
fun `test ImageQuality values`() {
|
||||||
assertEquals(95, ImageQuality.High.quality)
|
assertEquals(100, ImageQuality.High.quality)
|
||||||
assertEquals(85, ImageQuality.Standard.quality)
|
assertEquals(85, ImageQuality.Standard.quality)
|
||||||
assertEquals(70, ImageQuality.Low.quality)
|
assertEquals(70, ImageQuality.Low.quality)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user