Add original quality as option in Bilibili (#13719)
* Add original quality as option in Bilibili. Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com> Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Always show the locked chapters count. * Show extra information in the series details. Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Add missing Chinese translations. Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com> Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com> Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
This commit is contained in:
parent
09c587a0b4
commit
db71211623
@ -36,8 +36,12 @@ class BilibiliManga : Bilibili(
|
||||
|
||||
override val defaultLatestSort: Int = 1
|
||||
|
||||
override fun getAllSortOptions(): Array<String> =
|
||||
arrayOf(intl.sortPopular, intl.sortUpdated, intl.sortFollowers, intl.sortAdded)
|
||||
override fun getAllSortOptions(): Array<BilibiliTag> = arrayOf(
|
||||
BilibiliTag(intl.sortPopular, 0),
|
||||
BilibiliTag(intl.sortUpdated, 1),
|
||||
BilibiliTag(intl.sortFollowers, 2),
|
||||
BilibiliTag(intl.sortAdded, 3)
|
||||
)
|
||||
|
||||
override fun getAllPrices(): Array<String> =
|
||||
arrayOf(intl.priceAll, intl.priceFree, intl.pricePaid, intl.priceWaitForFree)
|
||||
|
@ -63,9 +63,9 @@ abstract class Bilibili(
|
||||
else -> lang
|
||||
}
|
||||
|
||||
protected open val defaultPopularSort: Int = 1
|
||||
protected open val defaultPopularSort: Int = 0
|
||||
|
||||
protected open val defaultLatestSort: Int = 2
|
||||
protected open val defaultLatestSort: Int = 1
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
@ -75,19 +75,23 @@ abstract class Bilibili(
|
||||
|
||||
protected open val signedIn: Boolean = false
|
||||
|
||||
private val chapterImageQuality: String
|
||||
get() = preferences.getString("${IMAGE_QUALITY_PREF_KEY}_$lang", IMAGE_QUALITY_PREF_DEFAULT_VALUE)!!
|
||||
|
||||
private val chapterImageFormat: String
|
||||
get() = preferences.getString("${IMAGE_FORMAT_PREF_KEY}_$lang", IMAGE_FORMAT_PREF_DEFAULT_VALUE)!!
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
searchMangaRequest(page, "", FilterList(SortFilter("", emptyArray(), defaultPopularSort)))
|
||||
override fun popularMangaRequest(page: Int): Request = searchMangaRequest(
|
||||
page = page,
|
||||
query = "",
|
||||
filters = FilterList(
|
||||
SortFilter("", getAllSortOptions(), defaultPopularSort)
|
||||
)
|
||||
)
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage = searchMangaParse(response)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
searchMangaRequest(page, "", FilterList(SortFilter("", emptyArray(), defaultLatestSort)))
|
||||
override fun latestUpdatesRequest(page: Int): Request = searchMangaRequest(
|
||||
page = page,
|
||||
query = "",
|
||||
filters = FilterList(
|
||||
SortFilter("", getAllSortOptions(), defaultLatestSort)
|
||||
)
|
||||
)
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage = searchMangaParse(response)
|
||||
|
||||
@ -99,31 +103,16 @@ abstract class Bilibili(
|
||||
return mangaDetailsApiRequest("/detail/mc$comicId")
|
||||
}
|
||||
|
||||
val order = filters.filterIsInstance<SortFilter>()
|
||||
.firstOrNull()?.state ?: 0
|
||||
|
||||
val status = filters.filterIsInstance<StatusFilter>()
|
||||
.firstOrNull()?.state?.minus(1) ?: -1
|
||||
|
||||
val price = filters.filterIsInstance<PriceFilter>()
|
||||
.firstOrNull()?.state ?: 0
|
||||
|
||||
val styleId = filters.filterIsInstance<GenreFilter>()
|
||||
.firstOrNull()?.selected?.id ?: -1
|
||||
|
||||
val areaId = filters.filterIsInstance<AreaFilter>()
|
||||
.firstOrNull()?.selected?.id ?: -1
|
||||
|
||||
val pageSize = if (query.isBlank()) POPULAR_PER_PAGE else SEARCH_PER_PAGE
|
||||
val price = filters.firstInstanceOrNull<PriceFilter>()?.state ?: 0
|
||||
|
||||
val jsonPayload = buildJsonObject {
|
||||
put("area_id", areaId)
|
||||
put("is_finish", status)
|
||||
put("area_id", filters.firstInstanceOrNull<AreaFilter>()?.selected?.id ?: -1)
|
||||
put("is_finish", filters.firstInstanceOrNull<StatusFilter>()?.state?.minus(1) ?: -1)
|
||||
put("is_free", if (price == 0) -1 else price)
|
||||
put("order", order)
|
||||
put("order", filters.firstInstanceOrNull<SortFilter>()?.selected?.id ?: 0)
|
||||
put("page_num", page)
|
||||
put("page_size", pageSize)
|
||||
put("style_id", styleId)
|
||||
put("page_size", if (query.isBlank()) POPULAR_PER_PAGE else SEARCH_PER_PAGE)
|
||||
put("style_id", filters.firstInstanceOrNull<GenreFilter>()?.selected?.id ?: -1)
|
||||
put("style_prefer", "[]")
|
||||
|
||||
if (query.isNotBlank()) {
|
||||
@ -222,15 +211,27 @@ abstract class Bilibili(
|
||||
|
||||
title = comic.title
|
||||
author = comic.authorName.joinToString()
|
||||
status = if (comic.isFinish == 1) SManga.COMPLETED else SManga.ONGOING
|
||||
genre = comic.genres(intl.pricePaid, EMOJI_LOCKED).joinToString()
|
||||
description = comic.classicLines
|
||||
thumbnail_url = comic.verticalCover + THUMBNAIL_RESOLUTION
|
||||
url = "/detail/mc" + comic.id
|
||||
|
||||
if (comic.hasPaidChapters && !signedIn) {
|
||||
description = "${intl.hasPaidChaptersWarning}\n\n$description"
|
||||
status = when {
|
||||
comic.isFinish == 1 -> SManga.COMPLETED
|
||||
comic.isOnHiatus -> SManga.ON_HIATUS
|
||||
else -> SManga.ONGOING
|
||||
}
|
||||
description = buildString {
|
||||
if (comic.hasPaidChapters && !signedIn) {
|
||||
append("${intl.hasPaidChaptersWarning(comic.paidChaptersCount)}\n\n")
|
||||
}
|
||||
|
||||
append("${comic.classicLines}\n\n")
|
||||
append("${intl.informationTitle}:")
|
||||
append("\n• ${intl.totalChapterCount}: ${intl.localize(comic.episodeList.size)}")
|
||||
|
||||
if (comic.updateWeekdays.isNotEmpty() && status == SManga.ONGOING) {
|
||||
append("\n• ${intl.updatedEvery}: ${intl.getWeekdays(comic.updateWeekdays)}")
|
||||
}
|
||||
}
|
||||
thumbnail_url = comic.verticalCover
|
||||
url = "/detail/mc" + comic.id
|
||||
}
|
||||
|
||||
// Chapters are available in the same url of the manga details.
|
||||
@ -288,10 +289,10 @@ abstract class Bilibili(
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val imageQuality = chapterImageQuality
|
||||
val imageFormat = chapterImageFormat
|
||||
val imageQuality = preferences.chapterImageQuality
|
||||
val imageFormat = preferences.chapterImageFormat
|
||||
|
||||
val imageUrls = result.data!!.images.map { "${it.path}@$imageQuality.$imageFormat" }
|
||||
val imageUrls = result.data!!.images.map { it.url(imageQuality, imageFormat) }
|
||||
val imageTokenRequest = imageTokenRequest(imageUrls)
|
||||
val imageTokenResponse = client.newCall(imageTokenRequest).execute()
|
||||
val imageTokenResult = imageTokenResponse.parseAs<List<BilibiliPageDto>>()
|
||||
@ -324,16 +325,6 @@ abstract class Bilibili(
|
||||
entryValues = IMAGE_QUALITY_PREF_ENTRY_VALUES
|
||||
setDefaultValue(IMAGE_QUALITY_PREF_DEFAULT_VALUE)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
|
||||
preferences.edit()
|
||||
.putString("${IMAGE_QUALITY_PREF_KEY}_$lang", entry)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
val imageFormatPref = ListPreference(screen.context).apply {
|
||||
@ -343,16 +334,6 @@ abstract class Bilibili(
|
||||
entryValues = IMAGE_FORMAT_PREF_ENTRY_VALUES
|
||||
setDefaultValue(IMAGE_FORMAT_PREF_DEFAULT_VALUE)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
|
||||
preferences.edit()
|
||||
.putString("${IMAGE_FORMAT_PREF_KEY}_$lang", entry)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
screen.addPreference(imageQualityPref)
|
||||
@ -363,29 +344,28 @@ abstract class Bilibili(
|
||||
|
||||
protected open fun getAllAreas(): Array<BilibiliTag> = emptyArray()
|
||||
|
||||
protected open fun getAllSortOptions(): Array<String> =
|
||||
arrayOf(intl.sortInterest, intl.sortPopular, intl.sortUpdated)
|
||||
protected open fun getAllSortOptions(): Array<BilibiliTag> = arrayOf(
|
||||
BilibiliTag(intl.sortInterest, 0),
|
||||
BilibiliTag(intl.sortUpdated, 4),
|
||||
)
|
||||
|
||||
protected open fun getAllStatus(): Array<String> =
|
||||
arrayOf(intl.statusAll, intl.statusOngoing, intl.statusComplete)
|
||||
|
||||
protected open fun getAllPrices(): Array<String> =
|
||||
arrayOf(intl.priceAll, intl.priceFree, intl.pricePaid)
|
||||
protected open fun getAllPrices(): Array<String> = emptyArray()
|
||||
|
||||
override fun getFilterList(): FilterList {
|
||||
val filters = mutableListOf(
|
||||
val allAreas = getAllAreas()
|
||||
val allPrices = getAllPrices()
|
||||
|
||||
val filters = listOfNotNull(
|
||||
StatusFilter(intl.statusLabel, getAllStatus()),
|
||||
SortFilter(intl.sortLabel, getAllSortOptions(), defaultPopularSort),
|
||||
PriceFilter(intl.priceLabel, getAllPrices()),
|
||||
GenreFilter(intl.genreLabel, getAllGenres())
|
||||
PriceFilter(intl.priceLabel, getAllPrices()).takeIf { allPrices.isNotEmpty() },
|
||||
GenreFilter(intl.genreLabel, getAllGenres()),
|
||||
AreaFilter(intl.areaLabel, allAreas).takeIf { allAreas.isNotEmpty() }
|
||||
)
|
||||
|
||||
val allAreas = getAllAreas()
|
||||
|
||||
if (allAreas.isNotEmpty()) {
|
||||
filters += AreaFilter(intl.areaLabel, allAreas)
|
||||
}
|
||||
|
||||
return FilterList(filters)
|
||||
}
|
||||
|
||||
@ -414,6 +394,20 @@ abstract class Bilibili(
|
||||
return response
|
||||
}
|
||||
|
||||
protected val SharedPreferences.chapterImageQuality
|
||||
get() = when (getString("${IMAGE_QUALITY_PREF_KEY}_$lang", IMAGE_QUALITY_PREF_DEFAULT_VALUE)!!) {
|
||||
"raw" -> "1600w"
|
||||
"hd" -> "1000w"
|
||||
"sd" -> "800w_50q"
|
||||
else -> "raw+"
|
||||
}
|
||||
|
||||
protected val SharedPreferences.chapterImageFormat
|
||||
get() = getString("${IMAGE_FORMAT_PREF_KEY}_$lang", IMAGE_FORMAT_PREF_DEFAULT_VALUE)!!
|
||||
|
||||
private inline fun <reified R> List<*>.firstInstanceOrNull(): R? =
|
||||
filterIsInstance<R>().firstOrNull()
|
||||
|
||||
protected open fun HttpUrl.Builder.addCommonParameters(): HttpUrl.Builder = let {
|
||||
if (name == "BILIBILI COMICS") {
|
||||
addQueryParameter("lang", apiLang)
|
||||
@ -451,16 +445,16 @@ abstract class Bilibili(
|
||||
const val PREFIX_ID_SEARCH = "id:"
|
||||
private val ID_SEARCH_PATTERN = "^id:(mc)?(\\d+)$".toRegex()
|
||||
|
||||
private const val IMAGE_QUALITY_PREF_KEY = "chapterImageResolution"
|
||||
private val IMAGE_QUALITY_PREF_ENTRY_VALUES = arrayOf("1200w", "800w", "600w_50q")
|
||||
private val IMAGE_QUALITY_PREF_DEFAULT_VALUE = IMAGE_QUALITY_PREF_ENTRY_VALUES[0]
|
||||
private const val IMAGE_QUALITY_PREF_KEY = "chapterImageQuality"
|
||||
private val IMAGE_QUALITY_PREF_ENTRY_VALUES = arrayOf("raw+", "raw", "hd", "sd")
|
||||
private val IMAGE_QUALITY_PREF_DEFAULT_VALUE = IMAGE_QUALITY_PREF_ENTRY_VALUES[1]
|
||||
|
||||
private const val IMAGE_FORMAT_PREF_KEY = "chapterImageFormat"
|
||||
private val IMAGE_FORMAT_PREF_ENTRIES = arrayOf("JPG", "WEBP", "PNG")
|
||||
private val IMAGE_FORMAT_PREF_ENTRY_VALUES = arrayOf("jpg", "webp", "png")
|
||||
private val IMAGE_FORMAT_PREF_DEFAULT_VALUE = IMAGE_FORMAT_PREF_ENTRY_VALUES[0]
|
||||
|
||||
private const val THUMBNAIL_RESOLUTION = "@512w.jpg"
|
||||
const val THUMBNAIL_RESOLUTION = "@512w.jpg"
|
||||
|
||||
private val DATE_FORMATTER by lazy {
|
||||
SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
|
||||
|
@ -23,13 +23,18 @@ data class BilibiliComicDto(
|
||||
@SerialName("ep_list") val episodeList: List<BilibiliEpisodeDto> = emptyList(),
|
||||
val id: Int = 0,
|
||||
@SerialName("is_finish") val isFinish: Int = 0,
|
||||
@SerialName("temp_stop_update") val isOnHiatus: Boolean = false,
|
||||
@SerialName("season_id") val seasonId: Int = 0,
|
||||
val styles: List<String> = emptyList(),
|
||||
val title: String,
|
||||
@SerialName("update_weekday") val updateWeekdays: List<Int> = emptyList(),
|
||||
@SerialName("vertical_cover") val verticalCover: String = ""
|
||||
) {
|
||||
val hasPaidChapters: Boolean
|
||||
get() = episodeList.any { episode -> episode.payMode == 1 && episode.payGold > 0 }
|
||||
get() = paidChaptersCount > 0
|
||||
|
||||
val paidChaptersCount: Int
|
||||
get() = episodeList.filter { episode -> episode.payMode == 1 && episode.payGold > 0 }.size
|
||||
|
||||
fun genres(paidLabel: String, emoji: String): List<String> =
|
||||
(if (hasPaidChapters) listOf("$emoji $paidLabel") else emptyList()) + styles
|
||||
@ -54,8 +59,17 @@ data class BilibiliReader(
|
||||
|
||||
@Serializable
|
||||
data class BilibiliImageDto(
|
||||
val path: String
|
||||
)
|
||||
val path: String,
|
||||
@SerialName("x") val width: Int,
|
||||
@SerialName("y") val height: Int
|
||||
) {
|
||||
|
||||
fun url(quality: String, format: String): String {
|
||||
val imageWidth = if (quality == "raw+") "${width}w" else quality
|
||||
|
||||
return "$path@$imageWidth.$format"
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class BilibiliPageDto(
|
||||
|
@ -6,20 +6,21 @@ data class BilibiliTag(val name: String, val id: Int) {
|
||||
override fun toString(): String = name
|
||||
}
|
||||
|
||||
class GenreFilter(label: String, genres: Array<BilibiliTag>) :
|
||||
Filter.Select<BilibiliTag>(label, genres) {
|
||||
val selected: BilibiliTag
|
||||
get() = values[state]
|
||||
open class EnhancedSelect<T>(name: String, values: Array<T>, state: Int = 0) :
|
||||
Filter.Select<T>(name, values, state) {
|
||||
|
||||
val selected: T?
|
||||
get() = values.getOrNull(state)
|
||||
}
|
||||
|
||||
class GenreFilter(label: String, genres: Array<BilibiliTag>) :
|
||||
EnhancedSelect<BilibiliTag>(label, genres)
|
||||
|
||||
class AreaFilter(label: String, genres: Array<BilibiliTag>) :
|
||||
Filter.Select<BilibiliTag>(label, genres) {
|
||||
val selected: BilibiliTag
|
||||
get() = values[state]
|
||||
}
|
||||
EnhancedSelect<BilibiliTag>(label, genres)
|
||||
|
||||
class SortFilter(label: String, options: Array<String>, state: Int = 0) :
|
||||
Filter.Select<String>(label, options, state)
|
||||
class SortFilter(label: String, options: Array<BilibiliTag>, state: Int = 0) :
|
||||
EnhancedSelect<BilibiliTag>(label, options, state)
|
||||
|
||||
class StatusFilter(label: String, statuses: Array<String>) :
|
||||
Filter.Select<String>(label, statuses)
|
||||
|
@ -10,7 +10,7 @@ class BilibiliGenerator : ThemeSourceGenerator {
|
||||
|
||||
override val themeClass = "Bilibili"
|
||||
|
||||
override val baseVersionCode: Int = 4
|
||||
override val baseVersionCode: Int = 5
|
||||
|
||||
override val sources = listOf(
|
||||
MultiLang(
|
||||
|
@ -1,6 +1,16 @@
|
||||
package eu.kanade.tachiyomi.multisrc.bilibili
|
||||
|
||||
class BilibiliIntl(lang: String) {
|
||||
import java.text.DateFormatSymbols
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
|
||||
class BilibiliIntl(private val lang: String) {
|
||||
|
||||
private val locale by lazy { Locale.forLanguageTag(lang) }
|
||||
|
||||
private val dateFormatSymbols by lazy { DateFormatSymbols(locale) }
|
||||
|
||||
private val numberFormat by lazy { NumberFormat.getInstance(locale) }
|
||||
|
||||
val statusLabel: String = when (lang) {
|
||||
CHINESE, SIMPLIFIED_CHINESE -> "进度"
|
||||
@ -39,18 +49,20 @@ class BilibiliIntl(lang: String) {
|
||||
else -> "Ep. "
|
||||
}
|
||||
|
||||
val hasPaidChaptersWarning: String = when (lang) {
|
||||
fun hasPaidChaptersWarning(chapterCount: Int): String = when (lang) {
|
||||
CHINESE, SIMPLIFIED_CHINESE ->
|
||||
"${Bilibili.EMOJI_WARNING} 此漫画的付费章节已从章节列表中过滤。如果您已购买章节,请在 WebView " +
|
||||
"登录并刷新章节列表以阅读已购章节。"
|
||||
"${Bilibili.EMOJI_WARNING} 此漫画有 ${chapterCount.localized} 个付费章节,已在目录中隐藏。" +
|
||||
"如果你已购买,请在 WebView 登录并刷新目录,即可阅读已购章节。"
|
||||
SPANISH ->
|
||||
"${Bilibili.EMOJI_WARNING} ADVERTENCIA: Esta serie tiene capítulos pagos que fueron " +
|
||||
"filtrados de la lista de capítulos. Si ya compró y tiene alguno en su cuenta, " +
|
||||
"inicie sesión en WebView y actualice la lista de capítulos para leerlos."
|
||||
"${Bilibili.EMOJI_WARNING} ADVERTENCIA: Esta serie tiene ${chapterCount.localized} " +
|
||||
"capítulos pagos que fueron filtrados de la lista de capítulos. Si ya has " +
|
||||
"desbloqueado y tiene alguno en su cuenta, inicie sesión en WebView y " +
|
||||
"actualice la lista de capítulos para leerlos."
|
||||
else ->
|
||||
"${Bilibili.EMOJI_WARNING} WARNING: This series has paid chapters that were filtered " +
|
||||
"out from the chapter list. If you have already bought and have any in your " +
|
||||
"account, sign in through WebView and refresh the chapter list to read them."
|
||||
"${Bilibili.EMOJI_WARNING} WARNING: This series has ${chapterCount.localized} paid " +
|
||||
"chapters that were filtered out from the chapter list. If you have already " +
|
||||
"unlocked and have any in your account, sign in through WebView and refresh " +
|
||||
"the chapter list to read them."
|
||||
}
|
||||
|
||||
val imageQualityPrefTitle: String = when (lang) {
|
||||
@ -61,8 +73,8 @@ class BilibiliIntl(lang: String) {
|
||||
}
|
||||
|
||||
val imageQualityPrefEntries: Array<String> = when (lang) {
|
||||
CHINESE, SIMPLIFIED_CHINESE -> arrayOf("原图", "高", "低")
|
||||
else -> arrayOf("Raw", "HD", "SD")
|
||||
CHINESE, SIMPLIFIED_CHINESE -> arrayOf("原图+", "原图 (1600w)", "高 (1000w)", "低 (800w)")
|
||||
else -> arrayOf("Raw+", "Raw (1600w)", "HD (1000w)", "SD (800w)")
|
||||
}
|
||||
|
||||
val imageFormatPrefTitle: String = when (lang) {
|
||||
@ -167,6 +179,37 @@ class BilibiliIntl(lang: String) {
|
||||
else -> "Failed to get the credential to read the chapter."
|
||||
}
|
||||
|
||||
val informationTitle: String = when (lang) {
|
||||
CHINESE, SIMPLIFIED_CHINESE -> "信息"
|
||||
SPANISH -> "Información"
|
||||
else -> "Information"
|
||||
}
|
||||
|
||||
val totalChapterCount: String = when (lang) {
|
||||
CHINESE, SIMPLIFIED_CHINESE -> "章节总数"
|
||||
SPANISH -> "Número total de capítulos"
|
||||
else -> "Total chapter count"
|
||||
}
|
||||
|
||||
val updatedEvery: String = when (lang) {
|
||||
CHINESE, SIMPLIFIED_CHINESE -> "每周更新时间"
|
||||
SPANISH -> "Actualizado en"
|
||||
else -> "Updated every"
|
||||
}
|
||||
|
||||
fun getWeekdays(dayIndexes: List<Int>): String {
|
||||
val weekdays = dateFormatSymbols.weekdays
|
||||
.filter(String::isNotBlank)
|
||||
.map { dayName -> dayName.replaceFirstChar { it.uppercase(locale) } }
|
||||
|
||||
return dayIndexes.joinToString { weekdays[it] }
|
||||
}
|
||||
|
||||
fun localize(value: Int) = value.localized
|
||||
|
||||
private val Int.localized: String
|
||||
get() = numberFormat.format(this)
|
||||
|
||||
companion object {
|
||||
const val CHINESE = "zh"
|
||||
const val ENGLISH = "en"
|
||||
|
Loading…
x
Reference in New Issue
Block a user