fix: home screen & image load
This commit is contained in:
@@ -31,7 +31,6 @@ kotlin {
|
|||||||
implementation(compose.preview)
|
implementation(compose.preview)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
implementation(libs.ktor.client.okhttp)
|
implementation(libs.ktor.client.okhttp)
|
||||||
implementation("io.ktor:ktor-client-android:3.3.0")
|
|
||||||
}
|
}
|
||||||
commonMain.dependencies {
|
commonMain.dependencies {
|
||||||
implementation(compose.runtime)
|
implementation(compose.runtime)
|
||||||
@@ -52,12 +51,12 @@ kotlin {
|
|||||||
implementation(compose.materialIconsExtended)
|
implementation(compose.materialIconsExtended)
|
||||||
implementation("co.touchlab:kermit:2.0.4") //Add latest version
|
implementation("co.touchlab:kermit:2.0.4") //Add latest version
|
||||||
implementation("io.coil-kt.coil3:coil-compose:3.3.0")
|
implementation("io.coil-kt.coil3:coil-compose:3.3.0")
|
||||||
|
implementation("io.coil-kt.coil3:coil-network-ktor3:3.3.0")
|
||||||
}
|
}
|
||||||
commonTest.dependencies {
|
commonTest.dependencies {
|
||||||
implementation(libs.kotlin.test)
|
implementation(libs.kotlin.test)
|
||||||
}
|
}
|
||||||
iosMain.dependencies {
|
iosMain.dependencies {
|
||||||
implementation("io.ktor:ktor-client-darwin:3.3.0")
|
|
||||||
implementation(libs.ktor.client.darwin)
|
implementation(libs.ktor.client.darwin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package moe.uni.comfy.comfyuikmp.ui
|
package moe.uni.comfy.comfyuikmp.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.History
|
import androidx.compose.material.icons.filled.History
|
||||||
@@ -18,6 +19,7 @@ import moe.uni.comfy.comfyuikmp.ui.importer.ImportScreen
|
|||||||
import moe.uni.comfy.comfyuikmp.ui.navigation.Dest
|
import moe.uni.comfy.comfyuikmp.ui.navigation.Dest
|
||||||
import moe.uni.comfy.comfyuikmp.ui.navigation.rememberNavController
|
import moe.uni.comfy.comfyuikmp.ui.navigation.rememberNavController
|
||||||
import moe.uni.comfy.comfyuikmp.ui.settings.SettingsScreen
|
import moe.uni.comfy.comfyuikmp.ui.settings.SettingsScreen
|
||||||
|
import moe.uni.comfy.comfyuikmp.ui.view.ViewScreen
|
||||||
import moe.uni.comfy.comfyuikmp.util.mainViewModelFactory
|
import moe.uni.comfy.comfyuikmp.util.mainViewModelFactory
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
@@ -33,6 +35,13 @@ fun AppRoot(viewModel: AppViewModel = viewModel(factory = mainViewModelFactory))
|
|||||||
val snackbar = remember { SnackbarHostState() }
|
val snackbar = remember { SnackbarHostState() }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
val viewTarget by viewModel.viewTarget.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(viewTarget) {
|
||||||
|
if (viewTarget != null && navController.current.value != Dest.View) {
|
||||||
|
navController.navigate(Dest.View)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
snackbarHost = { SnackbarHost(hostState = snackbar) },
|
snackbarHost = { SnackbarHost(hostState = snackbar) },
|
||||||
@@ -47,12 +56,16 @@ fun AppRoot(viewModel: AppViewModel = viewModel(factory = mainViewModelFactory))
|
|||||||
Dest.Import -> "导入工作流"
|
Dest.Import -> "导入工作流"
|
||||||
Dest.Generate -> "生成"
|
Dest.Generate -> "生成"
|
||||||
Dest.Gallery -> "历史"
|
Dest.Gallery -> "历史"
|
||||||
|
Dest.View -> "查看"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
if (current == Dest.Import || current == Dest.Generate || current == Dest.Gallery) {
|
if (current == Dest.Import || current == Dest.Generate || current == Dest.Gallery || current == Dest.View) {
|
||||||
IconButton(onClick = { navController.pop() }) {
|
IconButton(onClick = {
|
||||||
|
if (current == Dest.View) viewModel.clearOpenImage()
|
||||||
|
navController.pop()
|
||||||
|
}) {
|
||||||
Icon(Icons.Filled.ArrowBack, contentDescription = "返回")
|
Icon(Icons.Filled.ArrowBack, contentDescription = "返回")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,7 +125,7 @@ fun AppRoot(viewModel: AppViewModel = viewModel(factory = mainViewModelFactory))
|
|||||||
val name = it.values.firstOrNull()?.meta?.title ?: "工作流"
|
val name = it.values.firstOrNull()?.meta?.title ?: "工作流"
|
||||||
val id = it.hashCode().toString() + Clock.System.now().toEpochMilliseconds()
|
val id = it.hashCode().toString() + Clock.System.now().toEpochMilliseconds()
|
||||||
viewModel.setWorkflow(id,name,it)
|
viewModel.setWorkflow(id,name,it)
|
||||||
navController.navigate(Dest.Generate)
|
navController.pop()
|
||||||
},
|
},
|
||||||
onError = { msg -> scope.launch { snackbar.showSnackbar(msg) } }
|
onError = { msg -> scope.launch { snackbar.showSnackbar(msg) } }
|
||||||
)
|
)
|
||||||
@@ -124,17 +137,25 @@ fun AppRoot(viewModel: AppViewModel = viewModel(factory = mainViewModelFactory))
|
|||||||
latestImage = uiState.image,
|
latestImage = uiState.image,
|
||||||
progress = uiState.progress,
|
progress = uiState.progress,
|
||||||
useRandom = uiState.useRandom,
|
useRandom = uiState.useRandom,
|
||||||
|
latestImageRef = viewModel.latestImageRef.collectAsState().value,
|
||||||
onPromptChange = {
|
onPromptChange = {
|
||||||
viewModel.updatePrompt(it)
|
viewModel.updatePrompt(it)
|
||||||
},
|
},
|
||||||
onRandomSwitchChange = {
|
onRandomSwitchChange = {
|
||||||
viewModel.useRandomSeed(it)
|
viewModel.useRandomSeed(it)
|
||||||
|
|
||||||
}
|
},
|
||||||
|
onOpenImage = { ref -> viewModel.openImage(ref) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Dest.Gallery -> GalleryScreen(contentPadding = padding,viewModel)
|
Dest.Gallery -> GalleryScreen(contentPadding = padding,viewModel)
|
||||||
|
|
||||||
|
Dest.View -> {
|
||||||
|
val ref by viewModel.viewTarget.collectAsState()
|
||||||
|
if (ref != null) ViewScreen(ref!!, contentPadding = padding)
|
||||||
|
else Text("无可查看图片", modifier = Modifier.padding(padding))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ class AppViewModel : ViewModel() {
|
|||||||
private val _historyImages = MutableStateFlow<List<ImageRef>>(emptyList())
|
private val _historyImages = MutableStateFlow<List<ImageRef>>(emptyList())
|
||||||
val historyImages: StateFlow<List<ImageRef>> = _historyImages.asStateFlow()
|
val historyImages: StateFlow<List<ImageRef>> = _historyImages.asStateFlow()
|
||||||
|
|
||||||
|
private val _latestImageRef = MutableStateFlow<ImageRef?>(null)
|
||||||
|
val latestImageRef: StateFlow<ImageRef?> = _latestImageRef.asStateFlow()
|
||||||
|
|
||||||
|
private val _viewTarget = MutableStateFlow<ImageRef?>(null)
|
||||||
|
val viewTarget: StateFlow<ImageRef?> = _viewTarget.asStateFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
var oldId = ""
|
var oldId = ""
|
||||||
@@ -54,6 +60,7 @@ class AppViewModel : ViewModel() {
|
|||||||
}, onImages = {
|
}, onImages = {
|
||||||
wsScope.launch {
|
wsScope.launch {
|
||||||
it.map { ref ->
|
it.map { ref ->
|
||||||
|
_latestImageRef.value = ref
|
||||||
_uiState.value = _uiState.value.copy(image = getImage(ref))
|
_uiState.value = _uiState.value.copy(image = getImage(ref))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,6 +128,14 @@ class AppViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun openImage(ref: ImageRef) {
|
||||||
|
_viewTarget.value = ref
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearOpenImage() {
|
||||||
|
_viewTarget.value = null
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
|
|||||||
+7
-2
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -16,10 +17,12 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import moe.uni.comfy.comfyuikmp.di.AppGraph
|
import moe.uni.comfy.comfyuikmp.di.AppGraph
|
||||||
import moe.uni.comfy.comfyuikmp.ui.AppViewModel
|
import moe.uni.comfy.comfyuikmp.ui.AppViewModel
|
||||||
|
import moe.uni.comfy.comfyuikmp.ui.navigation.Dest
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -53,15 +56,17 @@ fun GalleryScreen(contentPadding: PaddingValues, viewModel: AppViewModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
Card(modifier = Modifier.fillMaxWidth().aspectRatio(3f / 4f).clickable {
|
||||||
|
viewModel.openImage(img)
|
||||||
|
}) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = url,
|
model = url,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-5
@@ -25,6 +25,7 @@ import androidx.compose.material3.Scaffold
|
|||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.rememberBottomSheetScaffoldState
|
import androidx.compose.material3.rememberBottomSheetScaffoldState
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import kotlinx.serialization.json.JsonElement
|
import kotlinx.serialization.json.JsonElement
|
||||||
import kotlinx.serialization.json.JsonPrimitive
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
import kotlinx.serialization.json.booleanOrNull
|
import kotlinx.serialization.json.booleanOrNull
|
||||||
@@ -34,6 +35,7 @@ import moe.uni.comfy.comfyuikmp.data.model.ComfyPrompt
|
|||||||
import moe.uni.comfy.comfyuikmp.data.model.updateNodeStringInput
|
import moe.uni.comfy.comfyuikmp.data.model.updateNodeStringInput
|
||||||
import moe.uni.comfy.comfyuikmp.data.model.updateNodePrimitiveInput
|
import moe.uni.comfy.comfyuikmp.data.model.updateNodePrimitiveInput
|
||||||
import moe.uni.comfy.comfyuikmp.data.network.ProgressUpdate
|
import moe.uni.comfy.comfyuikmp.data.network.ProgressUpdate
|
||||||
|
import moe.uni.comfy.comfyuikmp.data.model.ImageRef
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -43,8 +45,10 @@ fun GenerateScreen(
|
|||||||
latestImage: ImageBitmap?,
|
latestImage: ImageBitmap?,
|
||||||
progress: ProgressUpdate? = null,
|
progress: ProgressUpdate? = null,
|
||||||
useRandom: Boolean,
|
useRandom: Boolean,
|
||||||
|
latestImageRef: ImageRef? = null,
|
||||||
onPromptChange: (ComfyPrompt) -> Unit,
|
onPromptChange: (ComfyPrompt) -> Unit,
|
||||||
onRandomSwitchChange: (Boolean) -> Unit,
|
onRandomSwitchChange: (Boolean) -> Unit,
|
||||||
|
onOpenImage: (ImageRef) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val state = rememberBottomSheetScaffoldState()
|
val state = rememberBottomSheetScaffoldState()
|
||||||
Scaffold(modifier = Modifier.padding(contentPadding)) {
|
Scaffold(modifier = Modifier.padding(contentPadding)) {
|
||||||
@@ -186,11 +190,22 @@ fun GenerateScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (latestImage != null) {
|
if (latestImage != null) {
|
||||||
Image(
|
if (latestImageRef != null) {
|
||||||
bitmap = latestImage,
|
Image(
|
||||||
contentDescription = null,
|
bitmap = latestImage,
|
||||||
modifier = Modifier.fillMaxWidth().padding(top = 32.dp)
|
contentDescription = null,
|
||||||
)
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 32.dp)
|
||||||
|
.clickable { onOpenImage(latestImageRef) }
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Image(
|
||||||
|
bitmap = latestImage,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(top = 32.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ fun HomeScreen(
|
|||||||
onCreate: () -> Unit
|
onCreate: () -> Unit
|
||||||
) {
|
) {
|
||||||
val repo = remember { AppGraph.workflows }
|
val repo = remember { AppGraph.workflows }
|
||||||
val items = repo.list()
|
var items by remember { mutableStateOf(repo.list()) }
|
||||||
var renaming by remember { mutableStateOf<WorkflowItem?>(null) }
|
var renaming by remember { mutableStateOf<WorkflowItem?>(null) }
|
||||||
var newName by remember { mutableStateOf("") }
|
var newName by remember { mutableStateOf("") }
|
||||||
LazyColumn(contentPadding = contentPadding) {
|
LazyColumn(contentPadding = contentPadding) {
|
||||||
@@ -58,7 +58,7 @@ fun HomeScreen(
|
|||||||
Row(modifier = Modifier.padding(horizontal = 8.dp)) {
|
Row(modifier = Modifier.padding(horizontal = 8.dp)) {
|
||||||
IconButton(onClick = { renaming = w; newName = w.name }) { Icon(Icons.Filled.Edit, contentDescription = null,
|
IconButton(onClick = { renaming = w; newName = w.name }) { Icon(Icons.Filled.Edit, contentDescription = null,
|
||||||
Modifier.size(24.dp)) }
|
Modifier.size(24.dp)) }
|
||||||
IconButton(onClick = { AppGraph.workflows.delete(w.id) }) { Icon(Icons.Filled.Delete, contentDescription = null,
|
IconButton(onClick = { AppGraph.workflows.delete(w.id); items = repo.list() }) { Icon(Icons.Filled.Delete, contentDescription = null,
|
||||||
Modifier.size(24.dp)) }
|
Modifier.size(24.dp)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,6 +70,7 @@ fun HomeScreen(
|
|||||||
AlertDialog(onDismissRequest = { renaming = null }, confirmButton = {
|
AlertDialog(onDismissRequest = { renaming = null }, confirmButton = {
|
||||||
Button(onClick = {
|
Button(onClick = {
|
||||||
AppGraph.workflows.rename(r.id, newName.ifBlank { r.name })
|
AppGraph.workflows.rename(r.id, newName.ifBlank { r.name })
|
||||||
|
items = repo.list()
|
||||||
renaming = null
|
renaming = null
|
||||||
}) { Text("确定") }
|
}) { Text("确定") }
|
||||||
}, dismissButton = {
|
}, dismissButton = {
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ enum class Dest {
|
|||||||
Settings,
|
Settings,
|
||||||
Import,
|
Import,
|
||||||
Generate,
|
Generate,
|
||||||
Gallery
|
Gallery,
|
||||||
|
View
|
||||||
}
|
}
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package moe.uni.comfy.comfyuikmp.ui.view
|
||||||
|
|
||||||
|
import androidx.compose.foundation.gestures.detectTransformGestures
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
import moe.uni.comfy.comfyuikmp.data.model.ImageRef
|
||||||
|
import moe.uni.comfy.comfyuikmp.di.AppGraph
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ViewScreen(ref: ImageRef, contentPadding: PaddingValues) {
|
||||||
|
val base = AppGraph.settings.getBaseUrl() ?: ""
|
||||||
|
val url = remember(ref, base) {
|
||||||
|
buildString {
|
||||||
|
append(base)
|
||||||
|
append("/view?filename=")
|
||||||
|
append(ref.filename)
|
||||||
|
ref.subfolder?.let {
|
||||||
|
append("&subfolder=")
|
||||||
|
append(it)
|
||||||
|
}
|
||||||
|
ref.type?.let {
|
||||||
|
append("&type=")
|
||||||
|
append(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val scaleState = remember { mutableFloatStateOf(1f) }
|
||||||
|
val rotationState = remember { mutableFloatStateOf(0f) }
|
||||||
|
val translationState = remember { mutableStateOf(Offset.Zero) }
|
||||||
|
|
||||||
|
Box(Modifier.fillMaxSize().padding(contentPadding)) {
|
||||||
|
AsyncImage(
|
||||||
|
model = url,
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.graphicsLayer(
|
||||||
|
scaleX = scaleState.floatValue,
|
||||||
|
scaleY = scaleState.floatValue,
|
||||||
|
rotationZ = rotationState.floatValue,
|
||||||
|
translationX = translationState.value.x,
|
||||||
|
translationY = translationState.value.y
|
||||||
|
)
|
||||||
|
.pointerInput("doubleTapReset") {
|
||||||
|
detectTapGestures(
|
||||||
|
onDoubleTap = {
|
||||||
|
scaleState.floatValue = 1f
|
||||||
|
rotationState.floatValue = 0f
|
||||||
|
translationState.value = Offset.Zero
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.pointerInput("transform") {
|
||||||
|
detectTransformGestures { _, pan, zoom, rotation ->
|
||||||
|
val newScale = (scaleState.floatValue * zoom).coerceIn(0.5f, 8f)
|
||||||
|
scaleState.floatValue = newScale
|
||||||
|
translationState.value += pan
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user