Fix JS platform issues: test isolation, dead code, canvas snapshot
This commit is contained in:
parent
bb3440279e
commit
15ec7eba0c
@ -194,12 +194,7 @@ abstract class HabitList : Iterable<Habit> {
|
|||||||
habit.color.toCsvColor(),
|
habit.color.toCsvColor(),
|
||||||
if (habit.isNumerical) habit.unit else "",
|
if (habit.isNumerical) habit.unit else "",
|
||||||
if (habit.isNumerical) habit.targetType.name else "",
|
if (habit.isNumerical) habit.targetType.name else "",
|
||||||
if (habit.isNumerical) {
|
if (habit.isNumerical) format("%.1f", habit.targetValue) else "",
|
||||||
val s = habit.targetValue.toString()
|
|
||||||
if ('.' !in s) "$s.0" else s
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
},
|
|
||||||
habit.isArchived.toString()
|
habit.isArchived.toString()
|
||||||
)
|
)
|
||||||
sb.append(csvLine(cols))
|
sb.append(csvLine(cols))
|
||||||
|
|||||||
@ -0,0 +1,51 @@
|
|||||||
|
package org.isoron.platform.io
|
||||||
|
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class FilesTest {
|
||||||
|
private val fileOpener = createTestFileOpener()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testWriteStringAndLines() = runTest {
|
||||||
|
val file = fileOpener.openUserFile("test-write-string")
|
||||||
|
file.writeString("hello\nworld\n")
|
||||||
|
val lines = file.lines()
|
||||||
|
assertEquals(listOf("hello", "world"), lines)
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testWriteBytesAndReadBytes() = runTest {
|
||||||
|
val file = fileOpener.openUserFile("test-write-bytes")
|
||||||
|
val data = byteArrayOf(0x53, 0x51, 0x4C, 0x69, 0x74, 0x65)
|
||||||
|
file.writeBytes(data)
|
||||||
|
val read = file.readBytes(6)
|
||||||
|
assertTrue(data.contentEquals(read))
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testReadBytesWithLimit() = runTest {
|
||||||
|
val file = fileOpener.openUserFile("test-read-limit")
|
||||||
|
file.writeBytes(byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8))
|
||||||
|
val read = file.readBytes(3)
|
||||||
|
assertEquals(3, read.size)
|
||||||
|
assertEquals(1, read[0])
|
||||||
|
assertEquals(2, read[1])
|
||||||
|
assertEquals(3, read[2])
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testExists() = runTest {
|
||||||
|
val file = fileOpener.openUserFile("test-exists")
|
||||||
|
file.writeString("data")
|
||||||
|
assertTrue(file.exists())
|
||||||
|
file.delete()
|
||||||
|
assertFalse(file.exists())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
package org.isoron.platform.time
|
||||||
|
|
||||||
|
import org.isoron.platform.io.createTestDateFormatter
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class LocalDateFormatterTest {
|
||||||
|
private val formatter = createTestDateFormatter()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testShortWeekdayName() {
|
||||||
|
assertEquals("Sat", formatter.shortWeekdayName(DayOfWeek.SATURDAY))
|
||||||
|
assertEquals("Sun", formatter.shortWeekdayName(DayOfWeek.SUNDAY))
|
||||||
|
assertEquals("Mon", formatter.shortWeekdayName(DayOfWeek.MONDAY))
|
||||||
|
assertEquals("Tue", formatter.shortWeekdayName(DayOfWeek.TUESDAY))
|
||||||
|
assertEquals("Wed", formatter.shortWeekdayName(DayOfWeek.WEDNESDAY))
|
||||||
|
assertEquals("Thu", formatter.shortWeekdayName(DayOfWeek.THURSDAY))
|
||||||
|
assertEquals("Fri", formatter.shortWeekdayName(DayOfWeek.FRIDAY))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testLongWeekdayName() {
|
||||||
|
assertEquals("Saturday", formatter.longWeekdayName(DayOfWeek.SATURDAY))
|
||||||
|
assertEquals("Sunday", formatter.longWeekdayName(DayOfWeek.SUNDAY))
|
||||||
|
assertEquals("Monday", formatter.longWeekdayName(DayOfWeek.MONDAY))
|
||||||
|
assertEquals("Tuesday", formatter.longWeekdayName(DayOfWeek.TUESDAY))
|
||||||
|
assertEquals("Wednesday", formatter.longWeekdayName(DayOfWeek.WEDNESDAY))
|
||||||
|
assertEquals("Thursday", formatter.longWeekdayName(DayOfWeek.THURSDAY))
|
||||||
|
assertEquals("Friday", formatter.longWeekdayName(DayOfWeek.FRIDAY))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testShortWeekdayNameFromDate() {
|
||||||
|
// 2015-01-25 is a Sunday
|
||||||
|
assertEquals("Sun", formatter.shortWeekdayName(LocalDate(2015, 1, 25)))
|
||||||
|
// 2015-01-26 is a Monday
|
||||||
|
assertEquals("Mon", formatter.shortWeekdayName(LocalDate(2015, 1, 26)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testShortMonthName() {
|
||||||
|
assertEquals("Jan", formatter.shortMonthName(LocalDate(2015, 1, 1)))
|
||||||
|
assertEquals("Feb", formatter.shortMonthName(LocalDate(2015, 2, 1)))
|
||||||
|
assertEquals("Dec", formatter.shortMonthName(LocalDate(2015, 12, 1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testLongMonthName() {
|
||||||
|
assertEquals("January", formatter.longMonthName(LocalDate(2015, 1, 1)))
|
||||||
|
assertEquals("February", formatter.longMonthName(LocalDate(2015, 2, 1)))
|
||||||
|
assertEquals("December", formatter.longMonthName(LocalDate(2015, 12, 1)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -135,7 +135,14 @@ class JsCanvas(
|
|||||||
this.textAlign = align
|
this.textAlign = align
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toImage(): Image = JsImage(canvas)
|
override fun toImage(): Image {
|
||||||
|
val copy = document.createElement("canvas") as HTMLCanvasElement
|
||||||
|
copy.width = canvas.width
|
||||||
|
copy.height = canvas.height
|
||||||
|
val copyCtx = copy.getContext("2d") as CanvasRenderingContext2D
|
||||||
|
copyCtx.drawImage(canvas, 0.0, 0.0)
|
||||||
|
return JsImage(copy)
|
||||||
|
}
|
||||||
|
|
||||||
override fun measureText(text: String): Double {
|
override fun measureText(text: String): Double {
|
||||||
updateFont()
|
updateFont()
|
||||||
|
|||||||
@ -43,7 +43,7 @@ class JsImage(private val canvas: HTMLCanvasElement) : Image {
|
|||||||
val init = RequestInit(
|
val init = RequestInit(
|
||||||
method = "POST",
|
method = "POST",
|
||||||
body = blob,
|
body = blob,
|
||||||
headers = json("X-File-Path" to path),
|
headers = json("X-File-Path" to path)
|
||||||
)
|
)
|
||||||
suspendCoroutine<Unit> { cont ->
|
suspendCoroutine<Unit> { cont ->
|
||||||
window.fetch("/save-file", init).then(
|
window.fetch("/save-file", init).then(
|
||||||
|
|||||||
@ -9,8 +9,11 @@ import kotlin.js.Promise
|
|||||||
external fun initSqlJs(config: dynamic = definedExternally): Promise<dynamic>
|
external fun initSqlJs(config: dynamic = definedExternally): Promise<dynamic>
|
||||||
|
|
||||||
private fun sqlJsNewDatabase(sqlJs: dynamic): dynamic {
|
private fun sqlJsNewDatabase(sqlJs: dynamic): dynamic {
|
||||||
val ctor = sqlJs.Database
|
return js("new sqlJs.Database()")
|
||||||
return js("new ctor()")
|
}
|
||||||
|
|
||||||
|
private fun sqlJsOpenDatabase(sqlJs: dynamic, data: dynamic): dynamic {
|
||||||
|
return js("new sqlJs.Database(data)")
|
||||||
}
|
}
|
||||||
|
|
||||||
class JsPreparedStatement(
|
class JsPreparedStatement(
|
||||||
@ -63,6 +66,9 @@ class JsPreparedStatement(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun bindLong(index: Int, value: Long) {
|
override fun bindLong(index: Int, value: Long) {
|
||||||
|
require(value in -(9e15.toLong())..9e15.toLong()) {
|
||||||
|
"Long value $value exceeds JS safe integer range"
|
||||||
|
}
|
||||||
bindings[index - 1] = value.toDouble()
|
bindings[index - 1] = value.toDouble()
|
||||||
needsBind = true
|
needsBind = true
|
||||||
}
|
}
|
||||||
@ -119,8 +125,7 @@ class JsDatabaseOpener(
|
|||||||
for (i in bytes.indices) {
|
for (i in bytes.indices) {
|
||||||
data[i] = bytes[i]
|
data[i] = bytes[i]
|
||||||
}
|
}
|
||||||
val ctor = sqlJs.Database
|
val db = sqlJsOpenDatabase(sqlJs, data)
|
||||||
val db = js("new ctor(data)")
|
|
||||||
return JsDatabase(db)
|
return JsDatabase(db)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,13 +8,6 @@ import org.w3c.dom.HTMLCanvasElement
|
|||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
import kotlin.js.Promise
|
|
||||||
|
|
||||||
private fun <T> Promise<T>.await(): suspend () -> T = {
|
|
||||||
suspendCoroutine { cont ->
|
|
||||||
then({ cont.resume(it) }, { cont.resumeWithException(it) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class JsFileStorage {
|
class JsFileStorage {
|
||||||
private val store = mutableMapOf<String, ByteArray>()
|
private val store = mutableMapOf<String, ByteArray>()
|
||||||
@ -48,7 +41,8 @@ class JsUserFile(
|
|||||||
override suspend fun lines(): List<String> {
|
override suspend fun lines(): List<String> {
|
||||||
val bytes = storage.read(pathString)
|
val bytes = storage.read(pathString)
|
||||||
?: error("File not found: $pathString")
|
?: error("File not found: $pathString")
|
||||||
return bytes.decodeToString().lines()
|
val result = bytes.decodeToString().lines()
|
||||||
|
return if (result.lastOrNull() == "") result.dropLast(1) else result
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun writeString(content: String) {
|
override suspend fun writeString(content: String) {
|
||||||
|
|||||||
@ -13,9 +13,15 @@ actual fun computeToday(hourOffset: Int, minuteOffset: Int): LocalDate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
actual fun getFirstWeekdayNumberAccordingToLocale(): Int {
|
actual fun getFirstWeekdayNumberAccordingToLocale(): Int {
|
||||||
// JS Intl does not expose first day of week. Default to Sunday (1)
|
return try {
|
||||||
// matching java.util.Calendar convention where Sunday=1.
|
val locale = js("new Intl.Locale(navigator.language)")
|
||||||
return 1
|
val weekInfo = locale.getWeekInfo()
|
||||||
|
val firstDay = (weekInfo.firstDay as Number).toInt()
|
||||||
|
// CLDR uses 1=Monday..7=Sunday; convert to Calendar convention 1=Sunday..7=Saturday
|
||||||
|
firstDay % 7 + 1
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
1 // Default to Sunday if Intl API unavailable
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toLocaleDateString(date: Date, locale: String, options: dynamic): String {
|
private fun toLocaleDateString(date: Date, locale: String, options: dynamic): String {
|
||||||
|
|||||||
@ -9,9 +9,17 @@ import kotlin.coroutines.resume
|
|||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
val sharedTestStorage = JsFileStorage()
|
private var currentTestStorage = JsFileStorage()
|
||||||
|
|
||||||
actual fun createTestFileOpener(): FileOpener = JsFileOpener(sharedTestStorage)
|
actual fun createTestFileOpener(): FileOpener {
|
||||||
|
currentTestStorage = JsFileStorage()
|
||||||
|
return JsFileOpener(currentTestStorage)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
"Use createTestDatabaseOpenerSuspend() instead",
|
||||||
|
level = DeprecationLevel.ERROR
|
||||||
|
)
|
||||||
actual fun createTestDatabaseOpener(): DatabaseOpener {
|
actual fun createTestDatabaseOpener(): DatabaseOpener {
|
||||||
throw UnsupportedOperationException(
|
throw UnsupportedOperationException(
|
||||||
"createTestDatabaseOpener() requires async sql.js init. " +
|
"createTestDatabaseOpener() requires async sql.js init. " +
|
||||||
@ -21,7 +29,7 @@ actual fun createTestDatabaseOpener(): DatabaseOpener {
|
|||||||
|
|
||||||
actual suspend fun createTestDatabaseOpenerSuspend(): DatabaseOpener {
|
actual suspend fun createTestDatabaseOpenerSuspend(): DatabaseOpener {
|
||||||
val sqlJs = TestDatabaseHelper.getInitializedSqlJs()
|
val sqlJs = TestDatabaseHelper.getInitializedSqlJs()
|
||||||
return JsDatabaseOpener(sqlJs, sharedTestStorage)
|
return JsDatabaseOpener(sqlJs, currentTestStorage)
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun createTestCanvas(width: Int, height: Int): Canvas {
|
actual fun createTestCanvas(width: Int, height: Int): Canvas {
|
||||||
|
|||||||
@ -1,88 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2025 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package org.isoron.platform.gui
|
|
||||||
|
|
||||||
import org.isoron.platform.time.DayOfWeek
|
|
||||||
import org.isoron.platform.time.JavaLocalDateFormatter
|
|
||||||
import org.isoron.platform.time.getFirstWeekdayNumberAccordingToLocale
|
|
||||||
import org.junit.Test
|
|
||||||
import java.util.Locale
|
|
||||||
import kotlin.test.assertContentEquals
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
|
|
||||||
class JavaLocalDateFormatterTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testShortWeekdayNames_us() {
|
|
||||||
val formatter = JavaLocalDateFormatter(Locale.US)
|
|
||||||
assertContentEquals(
|
|
||||||
arrayOf("Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri"),
|
|
||||||
formatter.shortWeekdayNames(DayOfWeek.SATURDAY)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testShortWeekdayNames_germany() {
|
|
||||||
val formatter = JavaLocalDateFormatter(Locale.GERMANY)
|
|
||||||
assertContentEquals(
|
|
||||||
arrayOf("Sa.", "So.", "Mo.", "Di.", "Mi.", "Do.", "Fr."),
|
|
||||||
formatter.shortWeekdayNames(DayOfWeek.SATURDAY)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testLongWeekdayNames_us() {
|
|
||||||
val formatter = JavaLocalDateFormatter(Locale.US)
|
|
||||||
assertContentEquals(
|
|
||||||
arrayOf("Saturday", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday"),
|
|
||||||
formatter.longWeekdayNames(DayOfWeek.SATURDAY)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testLongWeekdayNames_germany() {
|
|
||||||
val formatter = JavaLocalDateFormatter(Locale.GERMANY)
|
|
||||||
assertContentEquals(
|
|
||||||
arrayOf("Samstag", "Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag"),
|
|
||||||
formatter.longWeekdayNames(DayOfWeek.SATURDAY)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testGetFirstWeekdayNumberAccordingToLocale_us() {
|
|
||||||
val saved = Locale.getDefault()
|
|
||||||
try {
|
|
||||||
Locale.setDefault(Locale.US)
|
|
||||||
assertEquals(1, getFirstWeekdayNumberAccordingToLocale())
|
|
||||||
} finally {
|
|
||||||
Locale.setDefault(saved)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testGetFirstWeekdayNumberAccordingToLocale_germany() {
|
|
||||||
val saved = Locale.getDefault()
|
|
||||||
try {
|
|
||||||
Locale.setDefault(Locale.GERMANY)
|
|
||||||
assertEquals(2, getFirstWeekdayNumberAccordingToLocale())
|
|
||||||
} finally {
|
|
||||||
Locale.setDefault(saved)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,76 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2025 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.platform.io
|
|
||||||
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlin.test.Test
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.assertFalse
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
class JavaFilesTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testWriteStringAndLines() = runBlocking {
|
|
||||||
val file = JavaUserFile(kotlin.io.path.createTempFile("test", ".txt"))
|
|
||||||
file.writeString("hello\nworld\n")
|
|
||||||
val lines = file.lines()
|
|
||||||
assertEquals(listOf("hello", "world"), lines)
|
|
||||||
file.delete()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testWriteBytesAndReadBytes() = runBlocking {
|
|
||||||
val file = JavaUserFile(kotlin.io.path.createTempFile("test", ".bin"))
|
|
||||||
val data = byteArrayOf(0x53, 0x51, 0x4C, 0x69, 0x74, 0x65)
|
|
||||||
file.writeBytes(data)
|
|
||||||
val read = file.readBytes(6)
|
|
||||||
assertTrue(data.contentEquals(read))
|
|
||||||
file.delete()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testReadBytesWithLimit() = runBlocking {
|
|
||||||
val file = JavaUserFile(kotlin.io.path.createTempFile("test", ".bin"))
|
|
||||||
file.writeBytes(byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8))
|
|
||||||
val read = file.readBytes(3)
|
|
||||||
assertEquals(3, read.size)
|
|
||||||
assertEquals(1, read[0])
|
|
||||||
assertEquals(2, read[1])
|
|
||||||
assertEquals(3, read[2])
|
|
||||||
file.delete()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testPath() = runBlocking {
|
|
||||||
val tmpPath = kotlin.io.path.createTempFile("test", ".txt")
|
|
||||||
val file = JavaUserFile(tmpPath)
|
|
||||||
assertEquals(tmpPath.toString(), file.pathString)
|
|
||||||
file.delete()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testExists() = runBlocking {
|
|
||||||
val file = JavaUserFile(kotlin.io.path.createTempFile("test", ".txt"))
|
|
||||||
assertTrue(file.exists())
|
|
||||||
file.delete()
|
|
||||||
assertFalse(file.exists())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user