Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ef9b16da0b | |||
| 5d1ef983e9 | |||
| eb78a776cf |
@@ -15,8 +15,8 @@ android {
|
|||||||
applicationId 'org.koitharu.kotatsu'
|
applicationId 'org.koitharu.kotatsu'
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 33
|
targetSdkVersion 33
|
||||||
versionCode 547
|
versionCode 548
|
||||||
versionName '5.1.3'
|
versionName '5.1.4'
|
||||||
generatedDensities = []
|
generatedDensities = []
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ afterEvaluate {
|
|||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
//noinspection GradleDependency
|
//noinspection GradleDependency
|
||||||
implementation('com.github.KotatsuApp:kotatsu-parsers:fa7ea5b16a') {
|
implementation('com.github.KotatsuApp:kotatsu-parsers:44e28b40d3') {
|
||||||
exclude group: 'org.json', module: 'json'
|
exclude group: 'org.json', module: 'json'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,8 +115,8 @@ dependencies {
|
|||||||
implementation 'androidx.room:room-ktx:2.5.1'
|
implementation 'androidx.room:room-ktx:2.5.1'
|
||||||
kapt 'androidx.room:room-compiler:2.5.1'
|
kapt 'androidx.room:room-compiler:2.5.1'
|
||||||
|
|
||||||
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
|
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
|
||||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.3'
|
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.11.0'
|
||||||
implementation 'com.squareup.okio:okio:3.3.0'
|
implementation 'com.squareup.okio:okio:3.3.0'
|
||||||
|
|
||||||
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
|
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
|
||||||
@@ -127,8 +127,8 @@ dependencies {
|
|||||||
implementation 'androidx.hilt:hilt-work:1.0.0'
|
implementation 'androidx.hilt:hilt-work:1.0.0'
|
||||||
kapt 'androidx.hilt:hilt-compiler:1.0.0'
|
kapt 'androidx.hilt:hilt-compiler:1.0.0'
|
||||||
|
|
||||||
implementation 'io.coil-kt:coil-base:2.3.0'
|
implementation 'io.coil-kt:coil-base:2.4.0'
|
||||||
implementation 'io.coil-kt:coil-svg:2.3.0'
|
implementation 'io.coil-kt:coil-svg:2.4.0'
|
||||||
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:1b19231b2f'
|
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:1b19231b2f'
|
||||||
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
||||||
implementation 'io.noties.markwon:core:4.6.2'
|
implementation 'io.noties.markwon:core:4.6.2'
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import androidx.sqlite.db.SupportSQLiteQuery
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import org.intellij.lang.annotations.Language
|
import org.intellij.lang.annotations.Language
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||||
|
import org.koitharu.kotatsu.favourites.domain.model.Cover
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
@@ -71,12 +72,12 @@ abstract class FavouritesDao {
|
|||||||
)
|
)
|
||||||
abstract suspend fun findAllManga(categoryId: Int): List<MangaEntity>
|
abstract suspend fun findAllManga(categoryId: Int): List<MangaEntity>
|
||||||
|
|
||||||
suspend fun findCovers(categoryId: Long, order: SortOrder): List<String> {
|
suspend fun findCovers(categoryId: Long, order: SortOrder): List<Cover> {
|
||||||
val orderBy = getOrderBy(order)
|
val orderBy = getOrderBy(order)
|
||||||
|
|
||||||
@Language("RoomSql")
|
@Language("RoomSql")
|
||||||
val query = SimpleSQLiteQuery(
|
val query = SimpleSQLiteQuery(
|
||||||
"SELECT m.cover_url FROM favourites AS f LEFT JOIN manga AS m ON f.manga_id = m.manga_id " +
|
"SELECT m.cover_url AS url, m.source AS source FROM favourites AS f LEFT JOIN manga AS m ON f.manga_id = m.manga_id " +
|
||||||
"WHERE f.category_id = ? AND deleted_at = 0 ORDER BY $orderBy",
|
"WHERE f.category_id = ? AND deleted_at = 0 ORDER BY $orderBy",
|
||||||
arrayOf<Any>(categoryId),
|
arrayOf<Any>(categoryId),
|
||||||
)
|
)
|
||||||
@@ -145,7 +146,7 @@ abstract class FavouritesDao {
|
|||||||
protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow<List<FavouriteManga>>
|
protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow<List<FavouriteManga>>
|
||||||
|
|
||||||
@RawQuery
|
@RawQuery
|
||||||
protected abstract suspend fun findCoversImpl(query: SupportSQLiteQuery): List<String>
|
protected abstract suspend fun findCoversImpl(query: SupportSQLiteQuery): List<Cover>
|
||||||
|
|
||||||
@Query("UPDATE favourites SET deleted_at = :deletedAt WHERE manga_id = :mangaId")
|
@Query("UPDATE favourites SET deleted_at = :deletedAt WHERE manga_id = :mangaId")
|
||||||
protected abstract suspend fun setDeletedAt(mangaId: Long, deletedAt: Long)
|
protected abstract suspend fun setDeletedAt(mangaId: Long, deletedAt: Long)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
|||||||
import org.koitharu.kotatsu.favourites.data.toFavouriteCategory
|
import org.koitharu.kotatsu.favourites.data.toFavouriteCategory
|
||||||
import org.koitharu.kotatsu.favourites.data.toManga
|
import org.koitharu.kotatsu.favourites.data.toManga
|
||||||
import org.koitharu.kotatsu.favourites.data.toMangaList
|
import org.koitharu.kotatsu.favourites.data.toMangaList
|
||||||
|
import org.koitharu.kotatsu.favourites.domain.model.Cover
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels
|
import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels
|
||||||
@@ -66,11 +67,11 @@ class FavouritesRepository @Inject constructor(
|
|||||||
}.distinctUntilChanged()
|
}.distinctUntilChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun observeCategoriesWithCovers(): Flow<Map<FavouriteCategory, List<String>>> {
|
fun observeCategoriesWithCovers(): Flow<Map<FavouriteCategory, List<Cover>>> {
|
||||||
return db.favouriteCategoriesDao.observeAll()
|
return db.favouriteCategoriesDao.observeAll()
|
||||||
.map {
|
.map {
|
||||||
db.withTransaction {
|
db.withTransaction {
|
||||||
val res = LinkedHashMap<FavouriteCategory, List<String>>()
|
val res = LinkedHashMap<FavouriteCategory, List<Cover>>()
|
||||||
for (entity in it) {
|
for (entity in it) {
|
||||||
val cat = entity.toFavouriteCategory()
|
val cat = entity.toFavouriteCategory()
|
||||||
res[cat] = db.favouritesDao.findCovers(
|
res[cat] = db.favouritesDao.findCovers(
|
||||||
|
|||||||
@@ -18,11 +18,12 @@ import org.koitharu.kotatsu.R
|
|||||||
import org.koitharu.kotatsu.databinding.ItemCategoryBinding
|
import org.koitharu.kotatsu.databinding.ItemCategoryBinding
|
||||||
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener
|
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.utils.ext.animatorDurationScale
|
|
||||||
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
|
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
|
||||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||||
|
import org.koitharu.kotatsu.utils.ext.getAnimationDuration
|
||||||
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
||||||
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
||||||
|
import org.koitharu.kotatsu.utils.ext.source
|
||||||
|
|
||||||
fun categoryAD(
|
fun categoryAD(
|
||||||
coil: ImageLoader,
|
coil: ImageLoader,
|
||||||
@@ -53,10 +54,7 @@ fun categoryAD(
|
|||||||
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 153))
|
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 153))
|
||||||
val fallback = ColorDrawable(Color.TRANSPARENT)
|
val fallback = ColorDrawable(Color.TRANSPARENT)
|
||||||
val coverViews = arrayOf(binding.imageViewCover1, binding.imageViewCover2, binding.imageViewCover3)
|
val coverViews = arrayOf(binding.imageViewCover1, binding.imageViewCover2, binding.imageViewCover3)
|
||||||
val crossFadeDuration = (
|
val crossFadeDuration = context.getAnimationDuration(R.integer.config_defaultAnimTime).toInt()
|
||||||
context.resources.getInteger(R.integer.config_defaultAnimTime) *
|
|
||||||
context.animatorDurationScale
|
|
||||||
).toInt()
|
|
||||||
itemView.setOnClickListener(eventListener)
|
itemView.setOnClickListener(eventListener)
|
||||||
itemView.setOnLongClickListener(eventListener)
|
itemView.setOnLongClickListener(eventListener)
|
||||||
itemView.setOnTouchListener(eventListener)
|
itemView.setOnTouchListener(eventListener)
|
||||||
@@ -77,9 +75,11 @@ fun categoryAD(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
repeat(coverViews.size) { i ->
|
repeat(coverViews.size) { i ->
|
||||||
coverViews[i].newImageRequest(lifecycleOwner, item.covers.getOrNull(i))?.run {
|
val cover = item.covers.getOrNull(i)
|
||||||
|
coverViews[i].newImageRequest(lifecycleOwner, cover?.url)?.run {
|
||||||
placeholder(R.drawable.ic_placeholder)
|
placeholder(R.drawable.ic_placeholder)
|
||||||
fallback(fallback)
|
fallback(fallback)
|
||||||
|
source(cover?.mangaSource)
|
||||||
crossfade(crossFadeDuration * (i + 1))
|
crossfade(crossFadeDuration * (i + 1))
|
||||||
error(R.drawable.ic_error_placeholder)
|
error(R.drawable.ic_error_placeholder)
|
||||||
allowRgb565(true)
|
allowRgb565(true)
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package org.koitharu.kotatsu.favourites.ui.categories.adapter
|
package org.koitharu.kotatsu.favourites.ui.categories.adapter
|
||||||
|
|
||||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||||
|
import org.koitharu.kotatsu.favourites.domain.model.Cover
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
|
||||||
class CategoryListModel(
|
class CategoryListModel(
|
||||||
val mangaCount: Int,
|
val mangaCount: Int,
|
||||||
val covers: List<String>,
|
val covers: List<Cover>,
|
||||||
val category: FavouriteCategory,
|
val category: FavouriteCategory,
|
||||||
val isReorderMode: Boolean,
|
val isReorderMode: Boolean,
|
||||||
) : ListModel {
|
) : ListModel {
|
||||||
@@ -21,9 +22,7 @@ class CategoryListModel(
|
|||||||
if (covers != other.covers) return false
|
if (covers != other.covers) return false
|
||||||
if (category.id != other.category.id) return false
|
if (category.id != other.category.id) return false
|
||||||
if (category.title != other.category.title) return false
|
if (category.title != other.category.title) return false
|
||||||
if (category.order != other.category.order) return false
|
return category.order == other.category.order
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
@@ -35,4 +34,4 @@ class CategoryListModel(
|
|||||||
result = 31 * result + category.order.hashCode()
|
result = 31 * result + category.order.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,9 +54,10 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
suspend fun put(url: String, source: Source): File = withContext(Dispatchers.IO) {
|
suspend fun put(url: String, source: Source): File = withContext(Dispatchers.IO) {
|
||||||
val file = File(cacheDir.get().parentFile, url.longHashCode().toString())
|
val file = File(cacheDir.get().parentFile, url.longHashCode().toString())
|
||||||
try {
|
try {
|
||||||
file.sink(append = false).buffer().use {
|
val bytes = file.sink(append = false).buffer().use {
|
||||||
it.writeAllCancellable(source)
|
it.writeAllCancellable(source)
|
||||||
}
|
}
|
||||||
|
check(bytes != 0L) { "No data has been written" }
|
||||||
lruCache.get().put(url, file)
|
lruCache.get().put(url, file)
|
||||||
} finally {
|
} finally {
|
||||||
file.delete()
|
file.delete()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.reader.domain
|
package org.koitharu.kotatsu.reader.domain
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@@ -8,6 +9,7 @@ import androidx.collection.LongSparseArray
|
|||||||
import androidx.collection.set
|
import androidx.collection.set
|
||||||
import dagger.hilt.android.ActivityRetainedLifecycle
|
import dagger.hilt.android.ActivityRetainedLifecycle
|
||||||
import dagger.hilt.android.lifecycle.RetainedLifecycle
|
import dagger.hilt.android.lifecycle.RetainedLifecycle
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.android.scopes.ActivityRetainedScoped
|
import dagger.hilt.android.scopes.ActivityRetainedScoped
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -30,8 +32,10 @@ import org.koitharu.kotatsu.parsers.model.MangaPage
|
|||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.util.await
|
import org.koitharu.kotatsu.parsers.util.await
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||||
|
import org.koitharu.kotatsu.utils.FileSize
|
||||||
import org.koitharu.kotatsu.utils.RetainedLifecycleCoroutineScope
|
import org.koitharu.kotatsu.utils.RetainedLifecycleCoroutineScope
|
||||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
|
import org.koitharu.kotatsu.utils.ext.ramAvailable
|
||||||
import org.koitharu.kotatsu.utils.ext.withProgress
|
import org.koitharu.kotatsu.utils.ext.withProgress
|
||||||
import org.koitharu.kotatsu.utils.progress.ProgressDeferred
|
import org.koitharu.kotatsu.utils.progress.ProgressDeferred
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -44,9 +48,11 @@ import kotlin.coroutines.CoroutineContext
|
|||||||
|
|
||||||
private const val PROGRESS_UNDEFINED = -1f
|
private const val PROGRESS_UNDEFINED = -1f
|
||||||
private const val PREFETCH_LIMIT_DEFAULT = 10
|
private const val PREFETCH_LIMIT_DEFAULT = 10
|
||||||
|
private const val PREFETCH_MIN_RAM_MB = 80L
|
||||||
|
|
||||||
@ActivityRetainedScoped
|
@ActivityRetainedScoped
|
||||||
class PageLoader @Inject constructor(
|
class PageLoader @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
lifecycle: ActivityRetainedLifecycle,
|
lifecycle: ActivityRetainedLifecycle,
|
||||||
private val okHttp: OkHttpClient,
|
private val okHttp: OkHttpClient,
|
||||||
private val cache: PagesCache,
|
private val cache: PagesCache,
|
||||||
@@ -75,7 +81,7 @@ class PageLoader @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun isPrefetchApplicable(): Boolean {
|
fun isPrefetchApplicable(): Boolean {
|
||||||
return repository is RemoteMangaRepository && settings.isPagesPreloadEnabled()
|
return repository is RemoteMangaRepository && settings.isPagesPreloadEnabled() && !isLowRam()
|
||||||
}
|
}
|
||||||
|
|
||||||
@AnyThread
|
@AnyThread
|
||||||
@@ -116,6 +122,9 @@ class PageLoader @Inject constructor(
|
|||||||
|
|
||||||
suspend fun convertInPlace(file: File) {
|
suspend fun convertInPlace(file: File) {
|
||||||
convertLock.withLock {
|
convertLock.withLock {
|
||||||
|
if (context.ramAvailable < file.length() * 2) {
|
||||||
|
return@withLock
|
||||||
|
}
|
||||||
runInterruptible(Dispatchers.Default) {
|
runInterruptible(Dispatchers.Default) {
|
||||||
val image = BitmapFactory.decodeFile(file.absolutePath)
|
val image = BitmapFactory.decodeFile(file.absolutePath)
|
||||||
try {
|
try {
|
||||||
@@ -212,6 +221,10 @@ class PageLoader @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isLowRam(): Boolean {
|
||||||
|
return context.ramAvailable <= FileSize.MEGABYTES.convert(PREFETCH_MIN_RAM_MB, FileSize.BYTES)
|
||||||
|
}
|
||||||
|
|
||||||
private class InternalErrorHandler : AbstractCoroutineContextElement(CoroutineExceptionHandler),
|
private class InternalErrorHandler : AbstractCoroutineContextElement(CoroutineExceptionHandler),
|
||||||
CoroutineExceptionHandler {
|
CoroutineExceptionHandler {
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.utils.ext
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.ActivityManager
|
import android.app.ActivityManager
|
||||||
|
import android.app.ActivityManager.MemoryInfo
|
||||||
import android.app.ActivityOptions
|
import android.app.ActivityOptions
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Context.ACTIVITY_SERVICE
|
import android.content.Context.ACTIVITY_SERVICE
|
||||||
@@ -138,6 +139,13 @@ fun isLowRamDevice(context: Context): Boolean {
|
|||||||
return context.activityManager?.isLowRamDevice ?: false
|
return context.activityManager?.isLowRamDevice ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val Context.ramAvailable: Long
|
||||||
|
get() {
|
||||||
|
val result = MemoryInfo()
|
||||||
|
activityManager?.getMemoryInfo(result)
|
||||||
|
return result.availMem
|
||||||
|
}
|
||||||
|
|
||||||
fun scaleUpActivityOptionsOf(view: View): ActivityOptions = ActivityOptions.makeScaleUpAnimation(
|
fun scaleUpActivityOptionsOf(view: View): ActivityOptions = ActivityOptions.makeScaleUpAnimation(
|
||||||
view,
|
view,
|
||||||
0,
|
0,
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package org.koitharu.kotatsu.favourites.domain.model
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|
||||||
|
class Cover(
|
||||||
|
val url: String,
|
||||||
|
val source: String,
|
||||||
|
) {
|
||||||
|
|
||||||
|
val mangaSource: MangaSource?
|
||||||
|
get() = if (source.isEmpty()) null else MangaSource.values().find { it.name == source }
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as Cover
|
||||||
|
|
||||||
|
if (url != other.url) return false
|
||||||
|
return source == other.source
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = url.hashCode()
|
||||||
|
result = 31 * result + source.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "Cover(url='$url', source=$source)"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user