Fix editor glitches
This commit is contained in:
parent
484423a17a
commit
ca2b267fc4
@ -1,3 +1,6 @@
|
|||||||
|
import scientifik.useFx
|
||||||
|
import scientifik.useSerialization
|
||||||
|
|
||||||
val dataforgeVersion by extra("0.1.8-dev-2")
|
val dataforgeVersion by extra("0.1.8-dev-2")
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
@ -33,5 +36,5 @@ subprojects {
|
|||||||
apply(plugin = "scientifik.publish")
|
apply(plugin = "scientifik.publish")
|
||||||
}
|
}
|
||||||
useSerialization()
|
useSerialization()
|
||||||
useFx(FXModule.CONTROLS, version = fxVersion)
|
useFx(scientifik.FXModule.CONTROLS, version = fxVersion)
|
||||||
}
|
}
|
@ -7,6 +7,7 @@ import hep.dataforge.vis.VisualGroup
|
|||||||
import hep.dataforge.vis.VisualObject
|
import hep.dataforge.vis.VisualObject
|
||||||
import hep.dataforge.vis.bootstrap.*
|
import hep.dataforge.vis.bootstrap.*
|
||||||
import hep.dataforge.vis.react.component
|
import hep.dataforge.vis.react.component
|
||||||
|
import hep.dataforge.vis.react.flexColumn
|
||||||
import hep.dataforge.vis.react.state
|
import hep.dataforge.vis.react.state
|
||||||
import hep.dataforge.vis.spatial.VisualGroup3D
|
import hep.dataforge.vis.spatial.VisualGroup3D
|
||||||
import hep.dataforge.vis.spatial.VisualObject3D
|
import hep.dataforge.vis.spatial.VisualObject3D
|
||||||
@ -16,12 +17,18 @@ import hep.dataforge.vis.spatial.specifications.Canvas
|
|||||||
import hep.dataforge.vis.spatial.three.ThreeCanvas
|
import hep.dataforge.vis.spatial.three.ThreeCanvas
|
||||||
import hep.dataforge.vis.spatial.three.ThreeCanvasComponent
|
import hep.dataforge.vis.spatial.three.ThreeCanvasComponent
|
||||||
import hep.dataforge.vis.spatial.three.canvasControls
|
import hep.dataforge.vis.spatial.three.canvasControls
|
||||||
|
import kotlinx.css.FlexBasis
|
||||||
|
import kotlinx.css.Overflow
|
||||||
|
import kotlinx.css.flex
|
||||||
|
import kotlinx.css.overflow
|
||||||
import org.w3c.files.FileReader
|
import org.w3c.files.FileReader
|
||||||
import org.w3c.files.get
|
import org.w3c.files.get
|
||||||
import react.RProps
|
import react.RProps
|
||||||
import react.dom.h1
|
import react.dom.h1
|
||||||
import scientifik.gdml.GDML
|
import scientifik.gdml.GDML
|
||||||
import scientifik.gdml.parse
|
import scientifik.gdml.parse
|
||||||
|
import styled.css
|
||||||
|
import styled.styledDiv
|
||||||
import kotlin.browser.window
|
import kotlin.browser.window
|
||||||
import kotlin.math.PI
|
import kotlin.math.PI
|
||||||
|
|
||||||
@ -63,8 +70,16 @@ val GDMLApp = component<GDMLAppProps> { props ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
flexColumn {
|
flexColumn {
|
||||||
|
css {
|
||||||
|
flex(1.0, 1.0, FlexBasis.auto)
|
||||||
|
}
|
||||||
h1 { +"GDML/JSON loader demo" }
|
h1 { +"GDML/JSON loader demo" }
|
||||||
gridRow {
|
styledDiv {
|
||||||
|
css {
|
||||||
|
classes.add("row")
|
||||||
|
classes.add("p-1")
|
||||||
|
overflow = Overflow.auto
|
||||||
|
}
|
||||||
gridColumn(3) {
|
gridColumn(3) {
|
||||||
card("Load data") {
|
card("Load data") {
|
||||||
fileDrop("(drag file here)") { files ->
|
fileDrop("(drag file here)") { files ->
|
||||||
@ -82,7 +97,7 @@ val GDMLApp = component<GDMLAppProps> { props ->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
//tree
|
//tree
|
||||||
card("Object tree") {
|
card("Object tree", "overflow-auto") {
|
||||||
visual?.let {
|
visual?.let {
|
||||||
objectTree(it, selected, select)
|
objectTree(it, selected, select)
|
||||||
}
|
}
|
||||||
@ -106,7 +121,7 @@ val GDMLApp = component<GDMLAppProps> { props ->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
gridColumn(3) {
|
gridColumn(3) {
|
||||||
gridRow {
|
container {
|
||||||
//settings
|
//settings
|
||||||
canvas?.let {
|
canvas?.let {
|
||||||
card("Canvas configuration") {
|
card("Canvas configuration") {
|
||||||
@ -114,10 +129,10 @@ val GDMLApp = component<GDMLAppProps> { props ->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
gridRow {
|
container {
|
||||||
namecrumbs(selected, "World") { selected = it }
|
namecrumbs(selected, "World") { selected = it }
|
||||||
}
|
}
|
||||||
gridRow {
|
container {
|
||||||
//properties
|
//properties
|
||||||
card("Properties") {
|
card("Properties") {
|
||||||
selected.let { selected ->
|
selected.let { selected ->
|
||||||
|
@ -8,7 +8,6 @@ import hep.dataforge.vis.spatial.gdml.GDMLTransformer
|
|||||||
import hep.dataforge.vis.spatial.gdml.LUnit
|
import hep.dataforge.vis.spatial.gdml.LUnit
|
||||||
import hep.dataforge.vis.spatial.gdml.toVisual
|
import hep.dataforge.vis.spatial.gdml.toVisual
|
||||||
import react.child
|
import react.child
|
||||||
import react.dom.div
|
|
||||||
import react.dom.render
|
import react.dom.render
|
||||||
import kotlin.browser.document
|
import kotlin.browser.document
|
||||||
|
|
||||||
@ -47,12 +46,10 @@ private class GDMLDemoApp : Application {
|
|||||||
val element = document.getElementById("app") ?: error("Element with id 'app' not found on page")
|
val element = document.getElementById("app") ?: error("Element with id 'app' not found on page")
|
||||||
|
|
||||||
render(element) {
|
render(element) {
|
||||||
div("h-100") {
|
child(GDMLApp) {
|
||||||
child(GDMLApp) {
|
attrs {
|
||||||
attrs {
|
this.context = context
|
||||||
this.context = context
|
this.rootObject = cubes().toVisual(gdmlConfiguration)
|
||||||
this.rootObject = cubes().toVisual(gdmlConfiguration)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,8 +19,8 @@ inline fun TagConsumer<HTMLElement>.card(title: String, crossinline block: TagCo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun RBuilder.card(title: String, crossinline block: RBuilder.() -> Unit) {
|
inline fun RBuilder.card(title: String, classes: String? = null, crossinline block: RBuilder.() -> Unit) {
|
||||||
div("card w-100") {
|
div("card w-100 $classes") {
|
||||||
div("card-body") {
|
div("card-body") {
|
||||||
h3(classes = "card-title") {
|
h3(classes = "card-title") {
|
||||||
+title
|
+title
|
||||||
@ -166,12 +166,6 @@ fun RBuilder.accordion(id: String, builder: RAccordionBuilder.() -> Unit): React
|
|||||||
|
|
||||||
fun joinStyles(vararg styles: String?) = styles.joinToString(separator = " ") { it ?: "" }
|
fun joinStyles(vararg styles: String?) = styles.joinToString(separator = " ") { it ?: "" }
|
||||||
|
|
||||||
inline fun RBuilder.flexColumn(classes: String? = null, block: RDOMBuilder<DIV>.() -> Unit) =
|
|
||||||
div(joinStyles(classes, "d-flex flex-column"), block)
|
|
||||||
|
|
||||||
inline fun RBuilder.flexRow(classes: String? = null, block: RDOMBuilder<DIV>.() -> Unit) =
|
|
||||||
div(joinStyles(classes, "d-flex flex-row"), block)
|
|
||||||
|
|
||||||
enum class ContainerSize(val suffix: String) {
|
enum class ContainerSize(val suffix: String) {
|
||||||
DEFAULT(""),
|
DEFAULT(""),
|
||||||
SM("-sm"),
|
SM("-sm"),
|
||||||
|
@ -8,13 +8,17 @@ import hep.dataforge.names.plus
|
|||||||
import hep.dataforge.values.Value
|
import hep.dataforge.values.Value
|
||||||
import hep.dataforge.vis.react.RFBuilder
|
import hep.dataforge.vis.react.RFBuilder
|
||||||
import hep.dataforge.vis.react.component
|
import hep.dataforge.vis.react.component
|
||||||
|
import hep.dataforge.vis.react.flexRow
|
||||||
import hep.dataforge.vis.react.state
|
import hep.dataforge.vis.react.state
|
||||||
|
import kotlinx.css.*
|
||||||
import kotlinx.html.classes
|
import kotlinx.html.classes
|
||||||
import kotlinx.html.js.onClickFunction
|
import kotlinx.html.js.onClickFunction
|
||||||
import org.w3c.dom.Element
|
import org.w3c.dom.Element
|
||||||
import org.w3c.dom.events.Event
|
import org.w3c.dom.events.Event
|
||||||
import react.*
|
import react.*
|
||||||
import react.dom.*
|
import react.dom.*
|
||||||
|
import styled.css
|
||||||
|
import styled.styledDiv
|
||||||
|
|
||||||
interface ConfigEditorItemProps : RProps {
|
interface ConfigEditorItemProps : RProps {
|
||||||
|
|
||||||
@ -132,35 +136,43 @@ private fun RFBuilder.configEditorItem(props: ConfigEditorItemProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
is MetaItem.ValueItem -> {
|
is MetaItem.ValueItem -> {
|
||||||
div {
|
flexRow {
|
||||||
div("row form-group") {
|
css {
|
||||||
div("col d-inline-flex") {
|
alignItems = Align.center
|
||||||
span("tree-label align-self-center") {
|
justifyContent= JustifyContent.flexEnd
|
||||||
+token
|
}
|
||||||
attrs {
|
styledDiv {
|
||||||
if (item == null) {
|
css{
|
||||||
classes += "tree-label-inactive"
|
flexGrow = 1.0
|
||||||
}
|
}
|
||||||
|
span("tree-label align-self-center") {
|
||||||
|
+token
|
||||||
|
attrs {
|
||||||
|
if (item == null) {
|
||||||
|
classes += "tree-label-inactive"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div("col-6 d-inline-flex") {
|
}
|
||||||
valueChooser(
|
styledDiv {
|
||||||
props.name,
|
valueChooser(
|
||||||
actualItem,
|
props.name,
|
||||||
descriptorItem as? ValueDescriptor,
|
actualItem,
|
||||||
valueChanged
|
descriptorItem as? ValueDescriptor,
|
||||||
)
|
valueChanged
|
||||||
|
)
|
||||||
|
}
|
||||||
|
styledDiv {
|
||||||
|
css {
|
||||||
|
flexShrink = 1.0
|
||||||
}
|
}
|
||||||
div("col-auto d-inline-flex p-1") {
|
button(classes = "btn btn-link align-self-center") {
|
||||||
button(classes = "btn btn-link align-self-center") {
|
+"\u00D7"
|
||||||
+"\u00D7"
|
attrs {
|
||||||
attrs {
|
if (item == null) {
|
||||||
if (item == null) {
|
disabled = true
|
||||||
disabled = true
|
} else {
|
||||||
} else {
|
onClickFunction = removeClick
|
||||||
onClickFunction = removeClick
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -176,6 +188,7 @@ interface ConfigEditorProps : RProps {
|
|||||||
var default: Meta?
|
var default: Meta?
|
||||||
var descriptor: NodeDescriptor?
|
var descriptor: NodeDescriptor?
|
||||||
}
|
}
|
||||||
|
|
||||||
val ConfigEditor = component<ConfigEditorProps> { props ->
|
val ConfigEditor = component<ConfigEditorProps> { props ->
|
||||||
child(ConfigEditorItem) {
|
child(ConfigEditorItem) {
|
||||||
attrs {
|
attrs {
|
||||||
@ -212,7 +225,12 @@ fun RBuilder.configEditor(config: Config, descriptor: NodeDescriptor? = null, de
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun RBuilder.configEditor(obj: Configurable, descriptor: NodeDescriptor? = obj.descriptor, default: Meta? = null, key: Any? = null) {
|
fun RBuilder.configEditor(
|
||||||
|
obj: Configurable,
|
||||||
|
descriptor: NodeDescriptor? = obj.descriptor,
|
||||||
|
default: Meta? = null,
|
||||||
|
key: Any? = null
|
||||||
|
) {
|
||||||
child(ConfigEditor) {
|
child(ConfigEditor) {
|
||||||
attrs {
|
attrs {
|
||||||
this.key = key?.toString() ?: ""
|
this.key = key?.toString() ?: ""
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
package hep.dataforge.vis.bootstrap
|
package hep.dataforge.vis.bootstrap
|
||||||
|
|
||||||
import hep.dataforge.meta.MetaItem
|
import hep.dataforge.meta.*
|
||||||
import hep.dataforge.meta.descriptors.ValueDescriptor
|
import hep.dataforge.meta.descriptors.ValueDescriptor
|
||||||
import hep.dataforge.meta.get
|
|
||||||
import hep.dataforge.meta.string
|
|
||||||
import hep.dataforge.meta.value
|
|
||||||
import hep.dataforge.names.Name
|
import hep.dataforge.names.Name
|
||||||
import hep.dataforge.values.*
|
import hep.dataforge.values.*
|
||||||
import hep.dataforge.vis.Colors
|
import hep.dataforge.vis.Colors
|
||||||
@ -16,12 +13,8 @@ import org.w3c.dom.HTMLElement
|
|||||||
import org.w3c.dom.HTMLInputElement
|
import org.w3c.dom.HTMLInputElement
|
||||||
import org.w3c.dom.HTMLSelectElement
|
import org.w3c.dom.HTMLSelectElement
|
||||||
import org.w3c.dom.events.Event
|
import org.w3c.dom.events.Event
|
||||||
import org.w3c.dom.events.KeyboardEvent
|
|
||||||
import react.*
|
import react.*
|
||||||
import react.dom.div
|
import react.dom.*
|
||||||
import react.dom.input
|
|
||||||
import react.dom.option
|
|
||||||
import react.dom.select
|
|
||||||
|
|
||||||
interface ValueChooserProps : RProps {
|
interface ValueChooserProps : RProps {
|
||||||
var item: MetaItem<*>?
|
var item: MetaItem<*>?
|
||||||
@ -30,17 +23,12 @@ interface ValueChooserProps : RProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ValueChooserState : RState {
|
interface ValueChooserState : RState {
|
||||||
var value: Value?
|
|
||||||
var rawInput: Boolean?
|
var rawInput: Boolean?
|
||||||
}
|
}
|
||||||
|
|
||||||
class ValueChooserComponent(props: ValueChooserProps) : RComponent<ValueChooserProps, ValueChooserState>(props) {
|
class ValueChooserComponent(props: ValueChooserProps) : RComponent<ValueChooserProps, ValueChooserState>(props) {
|
||||||
private val element = createRef<HTMLElement>()
|
private val element = createRef<HTMLElement>()
|
||||||
|
|
||||||
override fun ValueChooserState.init(props: ValueChooserProps) {
|
|
||||||
value = props.item.value
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getValue(): Value? {
|
private fun getValue(): Value? {
|
||||||
val element = element.current ?: return null//state.element ?: return null
|
val element = element.current ?: return null//state.element ?: return null
|
||||||
return when (element) {
|
return when (element) {
|
||||||
@ -54,24 +42,13 @@ class ValueChooserComponent(props: ValueChooserProps) : RComponent<ValueChooserP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val valueChanged: (Event) -> Unit = { _ ->
|
private val commit: (Event) -> Unit = { event ->
|
||||||
setState {
|
props.valueChanged?.invoke(getValue())
|
||||||
value = getValue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val valueChangeAndCommit: (Event) -> Unit = { event ->
|
|
||||||
val res = getValue()
|
|
||||||
setState {
|
|
||||||
value = res
|
|
||||||
}
|
|
||||||
props.valueChanged?.invoke(res)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val keyDown: (Event) -> Unit = { event ->
|
private val keyDown: (Event) -> Unit = { event ->
|
||||||
if (event is KeyboardEvent && event.key == "Enter") {
|
if (event.type == "keydown" && event.asDynamic().key == "Enter") {
|
||||||
props.valueChanged?.invoke(getValue())
|
commit(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,11 +60,11 @@ class ValueChooserComponent(props: ValueChooserProps) : RComponent<ValueChooserP
|
|||||||
override fun componentDidUpdate(prevProps: ValueChooserProps, prevState: ValueChooserState, snapshot: Any) {
|
override fun componentDidUpdate(prevProps: ValueChooserProps, prevState: ValueChooserState, snapshot: Any) {
|
||||||
(element.current as? HTMLInputElement)?.let { element ->
|
(element.current as? HTMLInputElement)?.let { element ->
|
||||||
if (element.type == "checkbox") {
|
if (element.type == "checkbox") {
|
||||||
element.checked = state.value?.boolean ?: false
|
element.defaultChecked = props.item?.boolean ?: false
|
||||||
} else {
|
} else {
|
||||||
element.value = state.value?.string ?: ""
|
element.defaultValue = props.item?.string ?: ""
|
||||||
}
|
}
|
||||||
element.indeterminate = state.value == null
|
element.indeterminate = props.item == null
|
||||||
}
|
}
|
||||||
// (state.element as? HTMLSelectElement)?.let { element ->
|
// (state.element as? HTMLSelectElement)?.let { element ->
|
||||||
// state.value?.let { element.value = it.string }
|
// state.value?.let { element.value = it.string }
|
||||||
@ -96,8 +73,7 @@ class ValueChooserComponent(props: ValueChooserProps) : RComponent<ValueChooserP
|
|||||||
|
|
||||||
private fun RBuilder.stringInput() = input(type = InputType.text) {
|
private fun RBuilder.stringInput() = input(type = InputType.text) {
|
||||||
attrs {
|
attrs {
|
||||||
this.value = state.value?.string ?: ""
|
this.defaultValue = props.item?.string ?: ""
|
||||||
onChangeFunction = valueChanged
|
|
||||||
onKeyDownFunction = keyDown
|
onKeyDownFunction = keyDown
|
||||||
}
|
}
|
||||||
ref = element
|
ref = element
|
||||||
@ -112,19 +88,19 @@ class ValueChooserComponent(props: ValueChooserProps) : RComponent<ValueChooserP
|
|||||||
descriptor?.widgetType == "color" -> input(type = InputType.color) {
|
descriptor?.widgetType == "color" -> input(type = InputType.color) {
|
||||||
ref = element
|
ref = element
|
||||||
attrs {
|
attrs {
|
||||||
this.value = state.value?.let { value ->
|
this.defaultValue = props.item?.value?.let { value ->
|
||||||
if (value.type == ValueType.NUMBER) Colors.rgbToString(value.int)
|
if (value.type == ValueType.NUMBER) Colors.rgbToString(value.int)
|
||||||
else value.string
|
else value.string
|
||||||
} ?: "#000000"
|
} ?: "#000000"
|
||||||
onChangeFunction = valueChangeAndCommit
|
onChangeFunction = commit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
type == ValueType.BOOLEAN -> {
|
type == ValueType.BOOLEAN -> {
|
||||||
input(type = InputType.checkBox) {
|
input(type = InputType.checkBox) {
|
||||||
ref = element
|
ref = element
|
||||||
attrs {
|
attrs {
|
||||||
checked = state.value?.boolean ?: false
|
defaultChecked = props.item?.boolean ?: false
|
||||||
onChangeFunction = valueChangeAndCommit
|
onChangeFunction = commit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -140,8 +116,7 @@ class ValueChooserComponent(props: ValueChooserProps) : RComponent<ValueChooserP
|
|||||||
descriptor.attributes["max"].string?.let {
|
descriptor.attributes["max"].string?.let {
|
||||||
max = it
|
max = it
|
||||||
}
|
}
|
||||||
this.value = state.value?.string ?: ""
|
defaultValue = props.item?.string ?: ""
|
||||||
onChangeFunction = valueChanged
|
|
||||||
onKeyDownFunction = keyDown
|
onKeyDownFunction = keyDown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -153,9 +128,9 @@ class ValueChooserComponent(props: ValueChooserProps) : RComponent<ValueChooserP
|
|||||||
}
|
}
|
||||||
ref = element
|
ref = element
|
||||||
attrs {
|
attrs {
|
||||||
this.value = state.value?.string ?: ""
|
this.value = props.item?.string ?: ""
|
||||||
multiple = false
|
multiple = false
|
||||||
onChangeFunction = valueChangeAndCommit
|
onChangeFunction = commit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> stringInput()
|
else -> stringInput()
|
||||||
|
@ -1,3 +1,11 @@
|
|||||||
|
/*full height*/
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-fluid { height: inherit; }
|
||||||
|
|
||||||
/* Remove default bullets */
|
/* Remove default bullets */
|
||||||
ul, .tree {
|
ul, .tree {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
|
29
ui/react/src/main/kotlin/hep/dataforge/vis/react/layout.kt
Normal file
29
ui/react/src/main/kotlin/hep/dataforge/vis/react/layout.kt
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package hep.dataforge.vis.react
|
||||||
|
|
||||||
|
import kotlinx.css.Display
|
||||||
|
import kotlinx.css.FlexDirection
|
||||||
|
import kotlinx.css.display
|
||||||
|
import kotlinx.css.flexDirection
|
||||||
|
import kotlinx.html.DIV
|
||||||
|
import react.RBuilder
|
||||||
|
import styled.StyledDOMBuilder
|
||||||
|
import styled.css
|
||||||
|
import styled.styledDiv
|
||||||
|
|
||||||
|
inline fun RBuilder.flexColumn(block: StyledDOMBuilder<DIV>.() -> Unit) =
|
||||||
|
styledDiv {
|
||||||
|
css {
|
||||||
|
display = Display.flex
|
||||||
|
flexDirection = FlexDirection.column
|
||||||
|
}
|
||||||
|
block()
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun RBuilder.flexRow(block: StyledDOMBuilder<DIV>.() -> Unit) =
|
||||||
|
styledDiv {
|
||||||
|
css {
|
||||||
|
display = Display.flex
|
||||||
|
flexDirection = FlexDirection.row
|
||||||
|
}
|
||||||
|
block()
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user