forked from NPM/numass-framework
Fix watcher view
This commit is contained in:
parent
307d4cb18a
commit
48c7c27af4
@ -88,7 +88,7 @@ open class DefaultEnvelopeReader : EnvelopeReader {
|
||||
val dataLength = tag.dataSize
|
||||
if (metaLength < 0 || dataLength < 0) {
|
||||
LoggerFactory.getLogger(javaClass).error("Can't lazy read infinite data or meta. Returning non-lazy envelope")
|
||||
return read(file)
|
||||
return read(Files.newInputStream(file))
|
||||
}
|
||||
|
||||
val metaBuffer = ByteBuffer.allocate(metaLength)
|
||||
|
@ -215,7 +215,7 @@ interface Name : Comparable<Name> {
|
||||
return of(segments[0])
|
||||
}
|
||||
|
||||
return of(Stream.of(*segments).filter { it -> !it.isEmpty() }.map<Name>{ of(it) }.toList())
|
||||
return of(Stream.of(*segments).filter { it -> it.isNotEmpty() }.map { of(it) }.toList())
|
||||
}
|
||||
|
||||
fun joinString(vararg segments: String): String {
|
||||
|
@ -82,7 +82,7 @@ class AmplitudeView : View(title = "Numass amplitude spectrum plot", icon = Imag
|
||||
|
||||
init {
|
||||
data.addListener(MapChangeListener { change ->
|
||||
val key = change.key
|
||||
val key = change.key.toString()
|
||||
if (change.wasAdded()) {
|
||||
replotOne(key, change.valueAdded)
|
||||
} else if (change.wasRemoved()) {
|
||||
@ -153,7 +153,7 @@ class AmplitudeView : View(title = "Numass amplitude spectrum plot", icon = Imag
|
||||
plotJobs.clear()
|
||||
|
||||
data.forEach { (key, point) ->
|
||||
replotOne(key, point)
|
||||
replotOne(key.toString(), point)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ package inr.numass.viewer
|
||||
|
||||
import hep.dataforge.context.ContextAware
|
||||
import hep.dataforge.meta.Meta
|
||||
import hep.dataforge.names.Name
|
||||
import hep.dataforge.storage.tables.TableLoader
|
||||
import hep.dataforge.tables.Adapters
|
||||
import hep.dataforge.tables.ListTable
|
||||
@ -20,13 +21,11 @@ import javafx.collections.ObservableList
|
||||
import javafx.collections.ObservableMap
|
||||
import kotlinx.coroutines.*
|
||||
import tornadofx.*
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.StandardWatchEventKinds
|
||||
import java.nio.file.WatchKey
|
||||
import java.nio.file.*
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
import kotlin.math.floor
|
||||
|
||||
|
||||
class DataController : Controller(), ContextAware {
|
||||
override val context get() = app.context
|
||||
|
||||
@ -45,11 +44,11 @@ class DataController : Controller(), ContextAware {
|
||||
point.channels.mapValues { (_, value) -> analyzer.getAmplitudeSpectrum(value) }
|
||||
}
|
||||
|
||||
val spectrum: Deferred<Table> = context.async{
|
||||
val spectrum: Deferred<Table> = context.async {
|
||||
analyzer.getAmplitudeSpectrum(point)
|
||||
}
|
||||
|
||||
val timeSpectrum: Deferred<Table> = context.async{
|
||||
val timeSpectrum: Deferred<Table> = context.async {
|
||||
val cr = spectrum.await().sumOf {
|
||||
it.getValue(NumassAnalyzer.COUNT_KEY).int
|
||||
}.toDouble() / point.length.toMillis() * 1000
|
||||
@ -76,19 +75,19 @@ class DataController : Controller(), ContextAware {
|
||||
}
|
||||
}
|
||||
|
||||
private val cache = Misc.getLRUCache<String, CachedPoint>(400)
|
||||
private val cache = Misc.getLRUCache<Name, CachedPoint>(400)
|
||||
|
||||
fun getCachedPoint(id: String, point: NumassPoint): CachedPoint = cache.getOrPut(id) { CachedPoint(point) }
|
||||
fun getCachedPoint(id: Name, point: NumassPoint): CachedPoint = cache.getOrPut(id) { CachedPoint(point) }
|
||||
|
||||
fun getSpectrumAsync(id: String, point: NumassPoint): Deferred<Table> =
|
||||
fun getSpectrumAsync(id: Name, point: NumassPoint): Deferred<Table> =
|
||||
getCachedPoint(id, point).spectrum
|
||||
|
||||
suspend fun getChannelSpectra(id: String, point: NumassPoint): Map<Int, Table> =
|
||||
suspend fun getChannelSpectra(id: Name, point: NumassPoint): Map<Int, Table> =
|
||||
getCachedPoint(id, point).channelSpectra.await()
|
||||
|
||||
val sets: ObservableMap<String, NumassSet> = FXCollections.observableHashMap()
|
||||
val points: ObservableMap<String, CachedPoint> = FXCollections.observableHashMap()
|
||||
val sc: ObservableMap<String, TableLoader> = FXCollections.observableHashMap()
|
||||
val sets: ObservableMap<Name, NumassSet> = FXCollections.observableHashMap()
|
||||
val points: ObservableMap<Name, CachedPoint> = FXCollections.observableHashMap()
|
||||
val sc: ObservableMap<Name, TableLoader> = FXCollections.observableHashMap()
|
||||
|
||||
val files: ObservableList<Path> = FXCollections.observableArrayList()
|
||||
|
||||
@ -102,7 +101,8 @@ class DataController : Controller(), ContextAware {
|
||||
if (watchPath != null) {
|
||||
Files.list(watchPath).toList()
|
||||
.filter {
|
||||
!Files.isDirectory(it) && it.fileName.toString().startsWith(NumassDataLoader.POINT_FRAGMENT_NAME)
|
||||
!Files.isDirectory(it) && it.fileName.toString()
|
||||
.startsWith(NumassDataLoader.POINT_FRAGMENT_NAME)
|
||||
}
|
||||
.sortedBy { file ->
|
||||
val attr = Files.readAttributes(file, BasicFileAttributes::class.java)
|
||||
@ -117,26 +117,20 @@ class DataController : Controller(), ContextAware {
|
||||
}
|
||||
}
|
||||
val watcher = watchPath.fileSystem.newWatchService()
|
||||
watchJob = app.context.launch {
|
||||
watchJob = app.context.launch(Dispatchers.IO) {
|
||||
watcher.use { watcher ->
|
||||
val key: WatchKey = watchPath.register(watcher,
|
||||
StandardWatchEventKinds.ENTRY_CREATE)
|
||||
coroutineContext[Job]?.invokeOnCompletion {
|
||||
key.cancel()
|
||||
}
|
||||
watchPath.register(watcher, StandardWatchEventKinds.ENTRY_CREATE)
|
||||
while (isActive) {
|
||||
try {
|
||||
key.pollEvents().forEach { event ->
|
||||
if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
|
||||
val path: Path = event.context() as Path
|
||||
runLater {
|
||||
files.add(watchPath.resolve(path))
|
||||
}
|
||||
val key: WatchKey = watcher.take()
|
||||
for (event: WatchEvent<*> in key.pollEvents()) {
|
||||
if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
|
||||
val path: Path = event.context() as Path
|
||||
runLater {
|
||||
files.add(watchPath.resolve(path))
|
||||
}
|
||||
}
|
||||
} catch (x: Throwable) {
|
||||
app.context.logger.error("Error during dynamic point read", x)
|
||||
}
|
||||
key.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -153,21 +147,21 @@ class DataController : Controller(), ContextAware {
|
||||
}
|
||||
|
||||
|
||||
fun addPoint(id: String, point: NumassPoint): CachedPoint {
|
||||
fun addPoint(id: Name, point: NumassPoint): CachedPoint {
|
||||
val newPoint = getCachedPoint(id, point)
|
||||
points[id] = newPoint
|
||||
return newPoint
|
||||
}
|
||||
|
||||
fun addSet(id: String, set: NumassSet) {
|
||||
fun addSet(id: Name, set: NumassSet) {
|
||||
sets[id] = set
|
||||
}
|
||||
|
||||
fun addSc(id: String, set: TableLoader) {
|
||||
fun addSc(id: Name, set: TableLoader) {
|
||||
sc[id] = set
|
||||
}
|
||||
|
||||
fun remove(id: String) {
|
||||
fun remove(id: Name) {
|
||||
points.remove(id)
|
||||
sets.remove(id)
|
||||
sc.remove(id)
|
||||
|
@ -1,12 +1,15 @@
|
||||
package inr.numass.viewer
|
||||
|
||||
import hep.dataforge.asName
|
||||
import hep.dataforge.fx.dfIconView
|
||||
import hep.dataforge.io.envelopes.Envelope
|
||||
import hep.dataforge.names.AlphanumComparator
|
||||
import hep.dataforge.names.Name
|
||||
import inr.numass.data.NumassDataUtils
|
||||
import inr.numass.data.NumassEnvelopeType
|
||||
import inr.numass.data.api.NumassPoint
|
||||
import inr.numass.data.storage.NumassDataLoader
|
||||
import javafx.scene.control.ContextMenu
|
||||
import kotlinx.coroutines.launch
|
||||
import tornadofx.*
|
||||
import java.nio.file.Path
|
||||
|
||||
@ -28,24 +31,46 @@ class DirectoryWatchView : View(title = "Numass storage", icon = dfIconView) {
|
||||
return NumassDataUtils.read(envelope)
|
||||
}
|
||||
|
||||
//private class PointContainer(val path: Path, val checkedProperty: BooleanProperty = SimpleBooleanProperty())
|
||||
|
||||
override val root = splitpane {
|
||||
listview(dataController.files) {
|
||||
//multiSelect(true)
|
||||
listview(dataController.files.sorted { l, r -> AlphanumComparator.compare(l.toString(), r.toString()) }) {
|
||||
cellFormat { path: Path ->
|
||||
text = path.fileName.toString()
|
||||
graphic = null
|
||||
if (path.fileName.toString().startsWith(NumassDataLoader.POINT_FRAGMENT_NAME)) {
|
||||
val point = readPointFile(path)
|
||||
val cachedPoint = dataController.addPoint(path.toString(), point)
|
||||
//val point = dataController.getCachedPoint(value.toString())
|
||||
contextMenu = ContextMenu().apply {
|
||||
item("Info") {
|
||||
action {
|
||||
PointInfoView(cachedPoint).openModal(escapeClosesWindow = true)
|
||||
val name = Name.of(path.map { it.toString().asName() })
|
||||
text = null
|
||||
graphic = checkbox(path.fileName.toString()).apply {
|
||||
isSelected = false
|
||||
selectedProperty().onChange {
|
||||
if (it) {
|
||||
app.context.launch {
|
||||
dataController.addPoint(name, readPointFile(path))
|
||||
}
|
||||
} else {
|
||||
dataController.remove(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// app.context.launch {
|
||||
// val point = readPointFile(path)
|
||||
// val cachedPoint = dataController.addPoint(path.toString().asName(), point)
|
||||
//
|
||||
// //val point = dataController.getCachedPoint(value.toString())
|
||||
// withContext(Dispatchers.JavaFx) {
|
||||
// contextMenu = ContextMenu().apply {
|
||||
// item("Info") {
|
||||
// action {
|
||||
// PointInfoView(cachedPoint).openModal(escapeClosesWindow = true)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
} else {
|
||||
text = path.fileName.toString()
|
||||
graphic = null
|
||||
contextMenu = null
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ import hep.dataforge.tables.Adapters
|
||||
import inr.numass.data.api.NumassSet
|
||||
import javafx.collections.MapChangeListener
|
||||
import javafx.scene.image.ImageView
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import tornadofx.*
|
||||
|
||||
|
||||
@ -48,10 +47,10 @@ class HVView : View(title = "High voltage time plot", icon = ImageView(dfIcon))
|
||||
val isEmpty = booleanBinding(data) { data.isEmpty() }
|
||||
|
||||
init {
|
||||
data.addListener { change: MapChangeListener.Change<out String, out NumassSet> ->
|
||||
data.addListener { change: MapChangeListener.Change<out Name, out NumassSet> ->
|
||||
isEmpty.invalidate()
|
||||
if (change.wasRemoved()) {
|
||||
frame.plots.remove(Name.ofSingle(change.key))
|
||||
frame.plots.remove(change.key)
|
||||
}
|
||||
if (change.wasAdded()) {
|
||||
runLater { container.progress = -1.0 }
|
||||
@ -59,8 +58,8 @@ class HVView : View(title = "High voltage time plot", icon = ImageView(dfIcon))
|
||||
change.valueAdded.getHvData()
|
||||
} ui { table ->
|
||||
if (table != null) {
|
||||
((frame[change.key] as? DataPlot)
|
||||
?: DataPlot(change.key, adapter = Adapters.buildXYAdapter("timestamp", "value")).also { frame.add(it) })
|
||||
((frame[change.key.toString()] as? DataPlot)
|
||||
?: DataPlot(change.key.toString(), adapter = Adapters.buildXYAdapter("timestamp", "value")).also { frame.add(it) })
|
||||
.fillData(table)
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
package inr.numass.viewer
|
||||
|
||||
import hep.dataforge.asName
|
||||
import hep.dataforge.fx.dfIconView
|
||||
import hep.dataforge.fx.except
|
||||
import hep.dataforge.fx.runGoal
|
||||
@ -67,7 +68,7 @@ class MainView : View(title = "Numass viewer", icon = dfIconView) {
|
||||
NumassDataLoader(app.context, null, path.fileName.toString(), path)
|
||||
} ui { loader: NumassDataLoader ->
|
||||
contentView = spectrumView
|
||||
dataController.addSet(loader.name, loader)
|
||||
dataController.addSet(loader.name.asName(), loader)
|
||||
|
||||
} except {
|
||||
alert(
|
||||
@ -116,7 +117,7 @@ class MainView : View(title = "Numass viewer", icon = dfIconView) {
|
||||
val point = NumassDataUtils.read(it)
|
||||
runLater {
|
||||
contentView = amplitudeView
|
||||
dataController.addPoint(path.fileName.toString(), point)
|
||||
dataController.addPoint(path.fileName.toString().asName(), point)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import hep.dataforge.fx.dfIcon
|
||||
import hep.dataforge.fx.plots.PlotContainer
|
||||
import hep.dataforge.fx.runGoal
|
||||
import hep.dataforge.fx.ui
|
||||
import hep.dataforge.names.Name
|
||||
import hep.dataforge.plots.PlotGroup
|
||||
import hep.dataforge.plots.data.DataPlot
|
||||
import hep.dataforge.plots.jfreechart.JFreeChartFrame
|
||||
@ -13,7 +14,6 @@ import hep.dataforge.storage.tables.asTable
|
||||
import hep.dataforge.tables.Adapters
|
||||
import javafx.collections.MapChangeListener
|
||||
import javafx.scene.image.ImageView
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import tornadofx.*
|
||||
|
||||
/**
|
||||
@ -38,16 +38,16 @@ class SlowControlView : View(title = "Numass slow control view", icon = ImageVie
|
||||
}
|
||||
|
||||
init {
|
||||
data.addListener { change: MapChangeListener.Change<out String, out TableLoader> ->
|
||||
data.addListener { change: MapChangeListener.Change<out Name, out TableLoader> ->
|
||||
if (change.wasRemoved()) {
|
||||
plot.remove(change.key)
|
||||
plot.remove(change.key.toString())
|
||||
}
|
||||
if (change.wasAdded()) {
|
||||
runGoal(app.context,"loadTable[${change.key}]") {
|
||||
val plotData = change.valueAdded.asTable().await()
|
||||
val names = plotData.format.namesAsArray().filter { it != "timestamp" }
|
||||
|
||||
val group = PlotGroup(change.key)
|
||||
val group = PlotGroup(change.key.toString())
|
||||
|
||||
names.forEach {
|
||||
val adapter = Adapters.buildXYAdapter("timestamp", it);
|
||||
|
@ -99,9 +99,9 @@ class SpectrumView : View(title = "Numass spectrum plot", icon = ImageView(dfIco
|
||||
}
|
||||
|
||||
init {
|
||||
data.addListener { change: MapChangeListener.Change<out String, out NumassSet> ->
|
||||
data.addListener { change: MapChangeListener.Change<out Name, out NumassSet> ->
|
||||
if (change.wasRemoved()) {
|
||||
frame.plots.remove(Name.ofSingle(change.key))
|
||||
frame.plots.remove(Name.ofSingle(change.key.toString()))
|
||||
}
|
||||
|
||||
if (change.wasAdded()) {
|
||||
@ -118,11 +118,11 @@ class SpectrumView : View(title = "Numass spectrum plot", icon = ImageView(dfIco
|
||||
|
||||
data.forEach { (name, set) ->
|
||||
val plot: DataPlot =
|
||||
frame.plots[Name.ofSingle(name)] as DataPlot? ?: DataPlot(name).apply { frame.add(this) }
|
||||
frame.plots[name] as DataPlot? ?: DataPlot(name.toString()).apply { frame.add(this) }
|
||||
|
||||
app.context.launch {
|
||||
val points = set.points.map {
|
||||
dataController.getCachedPoint("$name/${it.voltage}[${it.index}]", it)
|
||||
dataController.getCachedPoint(Name.join("$name","${it.voltage}[${it.index}]"), it)
|
||||
}.map { cachedPoint ->
|
||||
val count = cachedPoint.spectrum.await().countInWindow(loChannel.toShort(), upChannel.toShort())
|
||||
val seconds = cachedPoint.length.toMillis() / 1000.0
|
||||
|
@ -1,10 +1,12 @@
|
||||
package inr.numass.viewer
|
||||
|
||||
import hep.dataforge.asName
|
||||
import hep.dataforge.fx.dfIconView
|
||||
import hep.dataforge.fx.meta.MetaViewer
|
||||
import hep.dataforge.meta.Meta
|
||||
import hep.dataforge.meta.Metoid
|
||||
import hep.dataforge.names.AlphanumComparator
|
||||
import hep.dataforge.names.Name
|
||||
import hep.dataforge.storage.Storage
|
||||
import hep.dataforge.storage.files.FileTableLoader
|
||||
import hep.dataforge.storage.tables.TableLoader
|
||||
@ -35,15 +37,15 @@ class StorageView : View(title = "Numass storage", icon = dfIconView) {
|
||||
private val hvView: HVView by inject()
|
||||
private val scView: SlowControlView by inject()
|
||||
|
||||
private inner class Container(val id: String, val content: Any) {
|
||||
private inner class Container(val name: Name, val content: Any) {
|
||||
val checkedProperty = SimpleBooleanProperty(false)
|
||||
var checked by checkedProperty
|
||||
|
||||
val infoView: UIComponent by lazy {
|
||||
when (content) {
|
||||
is NumassPoint -> PointInfoView(dataController.getCachedPoint(id, content))
|
||||
is Metoid -> MetaViewer(content.meta, title = "Meta view: $id")
|
||||
else -> MetaViewer(Meta.empty(), title = "Meta view: $id")
|
||||
is NumassPoint -> PointInfoView(dataController.getCachedPoint(name, content))
|
||||
is Metoid -> MetaViewer(content.meta, title = "Meta view: $name")
|
||||
else -> MetaViewer(Meta.empty(), title = "Meta view: $name")
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,23 +56,23 @@ class StorageView : View(title = "Numass storage", icon = dfIconView) {
|
||||
when (content) {
|
||||
is NumassPoint -> {
|
||||
if (selected) {
|
||||
dataController.addPoint(id, content)
|
||||
dataController.addPoint(name, content)
|
||||
} else {
|
||||
dataController.remove(id)
|
||||
dataController.remove(name)
|
||||
}
|
||||
}
|
||||
is NumassSet -> {
|
||||
if (selected) {
|
||||
dataController.addSet(id, content)
|
||||
dataController.addSet(name, content)
|
||||
} else {
|
||||
dataController.remove(id)
|
||||
dataController.remove(name)
|
||||
}
|
||||
}
|
||||
is TableLoader -> {
|
||||
if (selected) {
|
||||
dataController.addSc(id, content)
|
||||
dataController.addSc(name, content)
|
||||
} else {
|
||||
dataController.remove(id)
|
||||
dataController.remove(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -95,7 +97,7 @@ class StorageView : View(title = "Numass storage", icon = dfIconView) {
|
||||
} else {
|
||||
buildContainer(it, this)
|
||||
}
|
||||
}.sortedWith(Comparator.comparing({ it.id }, AlphanumComparator)).asObservable()
|
||||
}.sortedWith(Comparator.comparing({ it.name.toString() }, AlphanumComparator)).asObservable()
|
||||
is NumassSet -> content.points
|
||||
.sortedBy { it.index }
|
||||
.map { buildContainer(it, this) }
|
||||
@ -117,7 +119,7 @@ class StorageView : View(title = "Numass storage", icon = dfIconView) {
|
||||
storageProperty.onChange { storage ->
|
||||
dataController.clear()
|
||||
if (storage == null) return@onChange
|
||||
root = TreeItem(Container(storage.name, storage))
|
||||
root = TreeItem(Container(storage.name.asName(), storage))
|
||||
root.isExpanded = true
|
||||
lazyPopulate(leafCheck = {
|
||||
!it.value.hasChildren
|
||||
@ -152,7 +154,7 @@ class StorageView : View(title = "Numass storage", icon = dfIconView) {
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
text = value.id
|
||||
text = value.name.toString()
|
||||
graphic = null
|
||||
}
|
||||
}
|
||||
@ -210,19 +212,19 @@ class StorageView : View(title = "Numass storage", icon = dfIconView) {
|
||||
|
||||
private fun buildContainer(content: Any, parent: Container): Container =
|
||||
when (content) {
|
||||
is Storage -> Container(content.fullName.toString(), content)
|
||||
is Storage -> Container(content.fullName, content)
|
||||
is NumassSet -> {
|
||||
val id: String = if (content is NumassDataLoader) {
|
||||
content.fullName.unescaped
|
||||
val id: Name = if (content is NumassDataLoader) {
|
||||
content.fullName
|
||||
} else {
|
||||
content.name
|
||||
content.name.asName()
|
||||
}
|
||||
Container(id, content)
|
||||
}
|
||||
is NumassPoint -> {
|
||||
Container("${parent.id}/${content.voltage}[${content.index}]", content)
|
||||
Container("${parent.name}/${content.voltage}[${content.index}]".asName(), content)
|
||||
}
|
||||
is FileTableLoader -> Container(content.path.toString(), content)
|
||||
is FileTableLoader -> Container(Name.of(content.path.map { it.toString().asName() }), content)
|
||||
else -> throw IllegalArgumentException("Unknown content type: ${content::class.java}");
|
||||
}
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ class TimeView : View(title = "Numass time spectrum plot", icon = ImageView(dfIc
|
||||
|
||||
init {
|
||||
data.addListener(MapChangeListener { change ->
|
||||
val key = change.key
|
||||
val key = change.key.toString()
|
||||
if (change.wasAdded()) {
|
||||
replotOne(key, change.valueAdded)
|
||||
} else if(change.wasRemoved()){
|
||||
@ -105,16 +105,5 @@ class TimeView : View(title = "Numass time spectrum plot", icon = ImageView(dfIc
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun replot() {
|
||||
frame.plots.clear()
|
||||
plotJobs.forEach { (_, job) -> job.cancel() }
|
||||
plotJobs.clear()
|
||||
|
||||
data.forEach { (key, point) ->
|
||||
replotOne(key, point)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user