[RU]Lib control hiding chapters (#12001)
* [RU]Lib control hiding chapters * no mixing * sort right * scanlator optional * replacing the largest mode * duble * duble 2 * more true scanlator * fix mobile parse * fix * fix getScanlatorTeamName * more scanlator group * too rare * solo scanlate * sort sync * black list
This commit is contained in:
parent
c388889e02
commit
b62fe0d0e0
@ -6,7 +6,7 @@ ext {
|
|||||||
extName = 'HentaiLib'
|
extName = 'HentaiLib'
|
||||||
pkgNameSuffix = 'ru.libhentai'
|
pkgNameSuffix = 'ru.libhentai'
|
||||||
extClass = '.LibHentai'
|
extClass = '.LibHentai'
|
||||||
extVersionCode = 16
|
extVersionCode = 17
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -217,13 +217,13 @@ class LibHentai : ConfigurableSource, HttpSource() {
|
|||||||
val manga = SManga.create()
|
val manga = SManga.create()
|
||||||
|
|
||||||
val body = document.select("div.media-info-list").first()
|
val body = document.select("div.media-info-list").first()
|
||||||
val rawCategory = body.select("div.media-info-list__title:contains(Тип) + div").text()
|
val rawCategory = document.select(".media-short-info a.media-short-info__item").text()
|
||||||
val category = when {
|
val category = when {
|
||||||
rawCategory == "Комикс западный" -> "Комикс"
|
rawCategory == "Комикс западный" -> "Комикс"
|
||||||
rawCategory.isNotBlank() -> rawCategory
|
rawCategory.isNotBlank() -> rawCategory
|
||||||
else -> "Манга"
|
else -> "Манга"
|
||||||
}
|
}
|
||||||
var rawAgeStop = body.select("div.media-info-list__title:contains(Возрастной рейтинг) + div").text()
|
var rawAgeStop = document.select(".media-short-info .media-short-info__item[data-caution]").text()
|
||||||
if (rawAgeStop.isEmpty()) {
|
if (rawAgeStop.isEmpty()) {
|
||||||
rawAgeStop = "0+"
|
rawAgeStop = "0+"
|
||||||
}
|
}
|
||||||
@ -250,8 +250,8 @@ class LibHentai : ConfigurableSource, HttpSource() {
|
|||||||
else -> dataManga!!.jsonObject["name"]!!.jsonPrimitive.content
|
else -> dataManga!!.jsonObject["name"]!!.jsonPrimitive.content
|
||||||
}
|
}
|
||||||
manga.thumbnail_url = document.select(".media-header__cover").attr("src")
|
manga.thumbnail_url = document.select(".media-header__cover").attr("src")
|
||||||
manga.author = body.select("div.media-info-list__title:contains(Автор) + div").text()
|
manga.author = body.select("div.media-info-list__title:contains(Автор) + div a").joinToString { it.text() }
|
||||||
manga.artist = body.select("div.media-info-list__title:contains(Художник) + div").text()
|
manga.artist = body.select("div.media-info-list__title:contains(Художник) + div a").joinToString { it.text() }
|
||||||
manga.status = if (document.html().contains("paper empty section")
|
manga.status = if (document.html().contains("paper empty section")
|
||||||
) {
|
) {
|
||||||
SManga.LICENSED
|
SManga.LICENSED
|
||||||
@ -297,14 +297,15 @@ class LibHentai : ConfigurableSource, HttpSource() {
|
|||||||
val chaptersList = data["chapters"]!!.jsonObject["list"]?.jsonArray
|
val chaptersList = data["chapters"]!!.jsonObject["list"]?.jsonArray
|
||||||
val slug = data["manga"]!!.jsonObject["slug"]!!.jsonPrimitive.content
|
val slug = data["manga"]!!.jsonObject["slug"]!!.jsonPrimitive.content
|
||||||
val branches = data["chapters"]!!.jsonObject["branches"]!!.jsonArray.reversed()
|
val branches = data["chapters"]!!.jsonObject["branches"]!!.jsonArray.reversed()
|
||||||
|
val teams = data["chapters"]!!.jsonObject["teams"]!!.jsonArray
|
||||||
val sortingList = preferences.getString(SORTING_PREF, "ms_mixing")
|
val sortingList = preferences.getString(SORTING_PREF, "ms_mixing")
|
||||||
|
|
||||||
val chapters: List<SChapter>? = if (branches.isNotEmpty() && !sortingList.equals("ms_mixing")) {
|
val chapters: List<SChapter>? = if (branches.isNotEmpty()) {
|
||||||
sortChaptersByTranslator(sortingList, chaptersList, slug, branches)
|
sortChaptersByTranslator(sortingList, chaptersList, slug, branches)
|
||||||
} else {
|
} else {
|
||||||
chaptersList
|
chaptersList
|
||||||
?.filter { it.jsonObject["status"]?.jsonPrimitive?.intOrNull != 2 }
|
?.filter { it.jsonObject["status"]?.jsonPrimitive?.intOrNull != 2 }
|
||||||
?.map { chapterFromElement(it, sortingList, slug) }
|
?.map { chapterFromElement(it, sortingList, slug, null, null, teams, chaptersList) }
|
||||||
}
|
}
|
||||||
|
|
||||||
return chapters ?: emptyList()
|
return chapters ?: emptyList()
|
||||||
@ -313,47 +314,49 @@ class LibHentai : ConfigurableSource, HttpSource() {
|
|||||||
private fun sortChaptersByTranslator
|
private fun sortChaptersByTranslator
|
||||||
(sortingList: String?, chaptersList: JsonArray?, slug: String, branches: List<JsonElement>): List<SChapter>? {
|
(sortingList: String?, chaptersList: JsonArray?, slug: String, branches: List<JsonElement>): List<SChapter>? {
|
||||||
var chapters: List<SChapter>? = null
|
var chapters: List<SChapter>? = null
|
||||||
|
val volume = "(?<=/v)[0-9]+(?=/c[0-9]+)".toRegex()
|
||||||
when (sortingList) {
|
when (sortingList) {
|
||||||
"ms_combining" -> {
|
"ms_mixing" -> {
|
||||||
val tempChaptersList = mutableListOf<SChapter>()
|
val tempChaptersList = mutableListOf<SChapter>()
|
||||||
for (currentBranch in branches.withIndex()) {
|
for (currentBranch in branches.withIndex()) {
|
||||||
val teamId = branches[currentBranch.index].jsonObject["id"]!!.jsonPrimitive.int
|
val branch = branches[currentBranch.index]
|
||||||
|
val teamId = branch.jsonObject["id"]!!.jsonPrimitive.int
|
||||||
|
val teams = branch.jsonObject["teams"]!!.jsonArray.filter { it.jsonObject["is_active"]?.jsonPrimitive?.intOrNull == 1 }
|
||||||
|
val teamsBranch = if (teams.size == 1)
|
||||||
|
teams[0].jsonObject["name"]?.jsonPrimitive?.contentOrNull
|
||||||
|
else if (teams.isEmpty())
|
||||||
|
branch.jsonObject["teams"]!!.jsonArray[0].jsonObject["name"]!!.jsonPrimitive.content
|
||||||
|
else null
|
||||||
chapters = chaptersList
|
chapters = chaptersList
|
||||||
?.filter { it.jsonObject["branch_id"]?.jsonPrimitive?.intOrNull == teamId && it.jsonObject["status"]?.jsonPrimitive?.intOrNull != 2 }
|
?.filter { it.jsonObject["branch_id"]?.jsonPrimitive?.intOrNull == teamId && it.jsonObject["status"]?.jsonPrimitive?.intOrNull != 2 }
|
||||||
?.map { chapterFromElement(it, sortingList, slug, teamId, branches) }
|
?.map { chapterFromElement(it, sortingList, slug, teamId, branches) }
|
||||||
chapters?.let { tempChaptersList.addAll(it) }
|
chapters?.let {
|
||||||
}
|
if ((tempChaptersList.size < it.size) && !groupTranslates.contains(teamsBranch.toString()))
|
||||||
chapters = tempChaptersList
|
tempChaptersList.addAll(0, it)
|
||||||
}
|
else
|
||||||
"ms_largest" -> {
|
tempChaptersList.addAll(it)
|
||||||
val sizesChaptersLists = mutableListOf<Int>()
|
|
||||||
for (currentBranch in branches.withIndex()) {
|
|
||||||
val teamId = branches[currentBranch.index].jsonObject["id"]!!.jsonPrimitive.int
|
|
||||||
val chapterSize = chaptersList
|
|
||||||
?.filter { it.jsonObject["branch_id"]?.jsonPrimitive?.intOrNull == teamId }!!.size
|
|
||||||
sizesChaptersLists.add(chapterSize)
|
|
||||||
}
|
|
||||||
val max = sizesChaptersLists.indexOfFirst { it == sizesChaptersLists.maxOrNull() ?: 0 }
|
|
||||||
val teamId = branches[max].jsonObject["id"]!!.jsonPrimitive.int
|
|
||||||
|
|
||||||
chapters = chaptersList
|
|
||||||
?.filter { it.jsonObject["branch_id"]?.jsonPrimitive?.intOrNull == teamId && it.jsonObject["status"]?.jsonPrimitive?.intOrNull != 2 }
|
|
||||||
?.map { chapterFromElement(it, sortingList, slug, teamId, branches) }
|
|
||||||
}
|
|
||||||
"ms_active" -> {
|
|
||||||
for (currentBranch in branches.withIndex()) {
|
|
||||||
val teams = branches[currentBranch.index].jsonObject["teams"]!!.jsonArray
|
|
||||||
for (currentTeam in teams.withIndex()) {
|
|
||||||
if (teams[currentTeam.index].jsonObject["is_active"]!!.jsonPrimitive.int == 1) {
|
|
||||||
val teamId = branches[currentBranch.index].jsonObject["id"]!!.jsonPrimitive.int
|
|
||||||
chapters = chaptersList
|
|
||||||
?.filter { it.jsonObject["branch_id"]?.jsonPrimitive?.intOrNull == teamId && it.jsonObject["status"]?.jsonPrimitive?.intOrNull != 2 }
|
|
||||||
?.map { chapterFromElement(it, sortingList, slug, teamId, branches) }
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
chapters ?: throw Exception("Активный перевод не назначен на сайте")
|
chapters = tempChaptersList.distinctBy { volume.find(it.url)?.value + "--" + it.chapter_number }.sortedWith(compareBy({ -it.chapter_number }, { volume.find(it.url)?.value }))
|
||||||
|
}
|
||||||
|
"ms_combining" -> {
|
||||||
|
val tempChaptersList = mutableListOf<SChapter>()
|
||||||
|
for (currentBranch in branches.withIndex()) {
|
||||||
|
val branch = branches[currentBranch.index]
|
||||||
|
val teamId = branch.jsonObject["id"]!!.jsonPrimitive.int
|
||||||
|
val teams = branch.jsonObject["teams"]!!.jsonArray.filter { it.jsonObject["is_active"]?.jsonPrimitive?.intOrNull == 1 }
|
||||||
|
val teamsBranch = if (teams.size == 1)
|
||||||
|
teams[0].jsonObject["name"]?.jsonPrimitive?.contentOrNull
|
||||||
|
else if (teams.isEmpty())
|
||||||
|
branch.jsonObject["teams"]!!.jsonArray[0].jsonObject["name"]!!.jsonPrimitive.content
|
||||||
|
else null
|
||||||
|
chapters = chaptersList
|
||||||
|
?.filter { it.jsonObject["branch_id"]?.jsonPrimitive?.intOrNull == teamId && it.jsonObject["status"]?.jsonPrimitive?.intOrNull != 2 }
|
||||||
|
?.map { chapterFromElement(it, sortingList, slug, teamId, branches) }
|
||||||
|
if (!groupTranslates.contains(teamsBranch.toString()))
|
||||||
|
chapters?.let { tempChaptersList.addAll(it) }
|
||||||
|
}
|
||||||
|
chapters = tempChaptersList
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -361,11 +364,13 @@ class LibHentai : ConfigurableSource, HttpSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun chapterFromElement
|
private fun chapterFromElement
|
||||||
(chapterItem: JsonElement, sortingList: String?, slug: String, teamIdParam: Int? = null, branches: List<JsonElement>? = null): SChapter {
|
(chapterItem: JsonElement, sortingList: String?, slug: String, teamIdParam: Int? = null, branches: List<JsonElement>? = null, teams: List<JsonElement>? = null, chaptersList: JsonArray? = null): SChapter {
|
||||||
val chapter = SChapter.create()
|
val chapter = SChapter.create()
|
||||||
|
|
||||||
val volume = chapterItem.jsonObject["chapter_volume"]!!.jsonPrimitive.int
|
val volume = chapterItem.jsonObject["chapter_volume"]!!.jsonPrimitive.int
|
||||||
val number = chapterItem.jsonObject["chapter_number"]!!.jsonPrimitive.content
|
val number = chapterItem.jsonObject["chapter_number"]!!.jsonPrimitive.content
|
||||||
|
val chapterScanlatorId = chapterItem.jsonObject["chapter_scanlator_id"]!!.jsonPrimitive.int
|
||||||
|
val isScanlatorId = teams?.filter { it.jsonObject["id"]?.jsonPrimitive?.intOrNull == chapterScanlatorId }
|
||||||
val teamId = if (teamIdParam != null) "?bid=$teamIdParam" else ""
|
val teamId = if (teamIdParam != null) "?bid=$teamIdParam" else ""
|
||||||
|
|
||||||
val url = "$baseUrl/$slug/v$volume/c$number$teamId"
|
val url = "$baseUrl/$slug/v$volume/c$number$teamId"
|
||||||
@ -374,12 +379,11 @@ class LibHentai : ConfigurableSource, HttpSource() {
|
|||||||
|
|
||||||
val nameChapter = chapterItem.jsonObject["chapter_name"]?.jsonPrimitive?.contentOrNull
|
val nameChapter = chapterItem.jsonObject["chapter_name"]?.jsonPrimitive?.contentOrNull
|
||||||
val fullNameChapter = "Том $volume. Глава $number"
|
val fullNameChapter = "Том $volume. Глава $number"
|
||||||
if (!sortingList.equals("ms_mixing")) {
|
chapter.scanlator = if (teams?.size == 1) teams[0].jsonObject["name"]?.jsonPrimitive?.content else if (isScanlatorId.orEmpty().isNotEmpty()) isScanlatorId!![0].jsonObject["name"]?.jsonPrimitive?.content else branches?.let { getScanlatorTeamName(it, chapterItem) } ?: if ((preferences.getBoolean(isScan_USER, false)) || (chaptersList?.distinctBy { it.jsonObject["username"]!!.jsonPrimitive.content }?.size == 1)) chapterItem.jsonObject["username"]!!.jsonPrimitive.content else null
|
||||||
chapter.scanlator = branches?.let { getScanlatorTeamName(it, chapterItem) }
|
|
||||||
}
|
|
||||||
chapter.name = if (nameChapter.isNullOrBlank()) fullNameChapter else "$fullNameChapter - $nameChapter"
|
chapter.name = if (nameChapter.isNullOrBlank()) fullNameChapter else "$fullNameChapter - $nameChapter"
|
||||||
chapter.date_upload = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
chapter.date_upload = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||||
.parse(chapterItem.jsonObject["chapter_created_at"]!!.jsonPrimitive.content.substringBefore(" "))?.time ?: 0L
|
.parse(chapterItem.jsonObject["chapter_created_at"]!!.jsonPrimitive.content.substringBefore(" "))?.time ?: 0L
|
||||||
|
chapter.chapter_number = number.toFloat()
|
||||||
|
|
||||||
return chapter
|
return chapter
|
||||||
}
|
}
|
||||||
@ -393,22 +397,15 @@ class LibHentai : ConfigurableSource, HttpSource() {
|
|||||||
for (currentTeam in teams.withIndex()) {
|
for (currentTeam in teams.withIndex()) {
|
||||||
val team = teams[currentTeam.index].jsonObject
|
val team = teams[currentTeam.index].jsonObject
|
||||||
val scanlatorId = chapterItem.jsonObject["chapter_scanlator_id"]!!.jsonPrimitive.int
|
val scanlatorId = chapterItem.jsonObject["chapter_scanlator_id"]!!.jsonPrimitive.int
|
||||||
scanlatorData = if ((scanlatorId == team.jsonObject["id"]!!.jsonPrimitive.int) ||
|
if ((scanlatorId == team.jsonObject["id"]!!.jsonPrimitive.int) ||
|
||||||
(scanlatorId == 0 && team["is_active"]!!.jsonPrimitive.int == 1)
|
(scanlatorId == 0 && team["is_active"]!!.jsonPrimitive.int == 1)
|
||||||
) team["name"]!!.jsonPrimitive.content else branch["teams"]!!.jsonArray[0].jsonObject["name"]!!.jsonPrimitive.content
|
) return team["name"]!!.jsonPrimitive.content else scanlatorData = branch["teams"]!!.jsonArray[0].jsonObject["name"]!!.jsonPrimitive.content
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return scanlatorData
|
return scanlatorData
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
|
|
||||||
"""Глава\s(\d+)""".toRegex().find(chapter.name)?.let {
|
|
||||||
val number = it.groups[1]?.value!!
|
|
||||||
chapter.chapter_number = number.toFloat()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pages
|
// Pages
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
@ -627,7 +624,7 @@ class LibHentai : ConfigurableSource, HttpSource() {
|
|||||||
private class OrderBy : Filter.Sort(
|
private class OrderBy : Filter.Sort(
|
||||||
"Сортировка",
|
"Сортировка",
|
||||||
arrayOf("Рейтинг", "Имя", "Просмотры", "Дате добавления", "Дате обновления", "Кол-во глав"),
|
arrayOf("Рейтинг", "Имя", "Просмотры", "Дате добавления", "Дате обновления", "Кол-во глав"),
|
||||||
Selection(0, false)
|
Selection(2, false)
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -909,6 +906,12 @@ class LibHentai : ConfigurableSource, HttpSource() {
|
|||||||
private const val SORTING_PREF = "MangaLibSorting"
|
private const val SORTING_PREF = "MangaLibSorting"
|
||||||
private const val SORTING_PREF_Title = "Способ выбора переводчиков"
|
private const val SORTING_PREF_Title = "Способ выбора переводчиков"
|
||||||
|
|
||||||
|
private const val isScan_USER = "ScanlatorUsername"
|
||||||
|
private const val isScan_USER_Title = "Альтернативный переводчик"
|
||||||
|
|
||||||
|
private const val TRANSLATORS_TITLE = "Чёрный список переводчиков\n(для красоты через «/» или с новой строки)"
|
||||||
|
private const val TRANSLATORS_DEFAULT = ""
|
||||||
|
|
||||||
private const val LANGUAGE_PREF = "MangaLibTitleLanguage"
|
private const val LANGUAGE_PREF = "MangaLibTitleLanguage"
|
||||||
private const val LANGUAGE_PREF_Title = "Выбор языка на обложке"
|
private const val LANGUAGE_PREF_Title = "Выбор языка на обложке"
|
||||||
|
|
||||||
@ -917,6 +920,7 @@ class LibHentai : ConfigurableSource, HttpSource() {
|
|||||||
|
|
||||||
private var server: String? = preferences.getString(SERVER_PREF, null)
|
private var server: String? = preferences.getString(SERVER_PREF, null)
|
||||||
private var isEng: String? = preferences.getString(LANGUAGE_PREF, "eng")
|
private var isEng: String? = preferences.getString(LANGUAGE_PREF, "eng")
|
||||||
|
private var groupTranslates: String = preferences.getString(TRANSLATORS_TITLE, TRANSLATORS_DEFAULT)!!
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
val serverPref = ListPreference(screen.context).apply {
|
val serverPref = ListPreference(screen.context).apply {
|
||||||
key = SERVER_PREF
|
key = SERVER_PREF
|
||||||
@ -935,10 +939,9 @@ class LibHentai : ConfigurableSource, HttpSource() {
|
|||||||
key = SORTING_PREF
|
key = SORTING_PREF
|
||||||
title = SORTING_PREF_Title
|
title = SORTING_PREF_Title
|
||||||
entries = arrayOf(
|
entries = arrayOf(
|
||||||
"Полный список (без повторных переводов)", "Все переводы (друг за другом)",
|
"Полный список (без повторных переводов)", "Все переводы (друг за другом)"
|
||||||
"Наибольшее число глав", "Активный перевод"
|
|
||||||
)
|
)
|
||||||
entryValues = arrayOf("ms_mixing", "ms_combining", "ms_largest", "ms_active")
|
entryValues = arrayOf("ms_mixing", "ms_combining")
|
||||||
summary = "%s"
|
summary = "%s"
|
||||||
setDefaultValue("ms_mixing")
|
setDefaultValue("ms_mixing")
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
@ -946,6 +949,17 @@ class LibHentai : ConfigurableSource, HttpSource() {
|
|||||||
preferences.edit().putString(SORTING_PREF, selected).commit()
|
preferences.edit().putString(SORTING_PREF, selected).commit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val scanlatorUsername = androidx.preference.CheckBoxPreference(screen.context).apply {
|
||||||
|
key = isScan_USER
|
||||||
|
title = isScan_USER_Title
|
||||||
|
summary = "Отображает Ник переводчика если Группа не указана явно."
|
||||||
|
setDefaultValue(false)
|
||||||
|
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
val checkValue = newValue as Boolean
|
||||||
|
preferences.edit().putBoolean(key, checkValue).commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
val titleLanguagePref = ListPreference(screen.context).apply {
|
val titleLanguagePref = ListPreference(screen.context).apply {
|
||||||
key = LANGUAGE_PREF
|
key = LANGUAGE_PREF
|
||||||
title = LANGUAGE_PREF_Title
|
title = LANGUAGE_PREF_Title
|
||||||
@ -962,6 +976,27 @@ class LibHentai : ConfigurableSource, HttpSource() {
|
|||||||
}
|
}
|
||||||
screen.addPreference(serverPref)
|
screen.addPreference(serverPref)
|
||||||
screen.addPreference(sortingPref)
|
screen.addPreference(sortingPref)
|
||||||
|
screen.addPreference(screen.editTextPreference(TRANSLATORS_TITLE, TRANSLATORS_DEFAULT, groupTranslates))
|
||||||
|
screen.addPreference(scanlatorUsername)
|
||||||
screen.addPreference(titleLanguagePref)
|
screen.addPreference(titleLanguagePref)
|
||||||
}
|
}
|
||||||
|
private fun PreferenceScreen.editTextPreference(title: String, default: String, value: String): androidx.preference.EditTextPreference {
|
||||||
|
return androidx.preference.EditTextPreference(context).apply {
|
||||||
|
key = title
|
||||||
|
this.title = title
|
||||||
|
summary = value.replace("/", "\n")
|
||||||
|
this.setDefaultValue(default)
|
||||||
|
dialogTitle = title
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
try {
|
||||||
|
val res = preferences.edit().putString(title, newValue as String).commit()
|
||||||
|
Toast.makeText(context, "Для обновления списка необходимо перезапустить приложение с полной остановкой.", Toast.LENGTH_LONG).show()
|
||||||
|
res
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ ext {
|
|||||||
extName = 'MangaLib'
|
extName = 'MangaLib'
|
||||||
pkgNameSuffix = 'ru.libmanga'
|
pkgNameSuffix = 'ru.libmanga'
|
||||||
extClass = '.LibManga'
|
extClass = '.LibManga'
|
||||||
extVersionCode = 71
|
extVersionCode = 72
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
@ -208,13 +208,13 @@ class LibManga : ConfigurableSource, HttpSource() {
|
|||||||
val manga = SManga.create()
|
val manga = SManga.create()
|
||||||
|
|
||||||
val body = document.select("div.media-info-list").first()
|
val body = document.select("div.media-info-list").first()
|
||||||
val rawCategory = body.select("div.media-info-list__title:contains(Тип) + div").text()
|
val rawCategory = document.select(".media-short-info a.media-short-info__item").text()
|
||||||
val category = when {
|
val category = when {
|
||||||
rawCategory == "Комикс западный" -> "Комикс"
|
rawCategory == "Комикс западный" -> "Комикс"
|
||||||
rawCategory.isNotBlank() -> rawCategory
|
rawCategory.isNotBlank() -> rawCategory
|
||||||
else -> "Манга"
|
else -> "Манга"
|
||||||
}
|
}
|
||||||
var rawAgeStop = body.select("div.media-info-list__title:contains(Возрастной рейтинг) + div").text()
|
var rawAgeStop = document.select(".media-short-info .media-short-info__item[data-caution]").text()
|
||||||
if (rawAgeStop.isEmpty()) {
|
if (rawAgeStop.isEmpty()) {
|
||||||
rawAgeStop = "0+"
|
rawAgeStop = "0+"
|
||||||
}
|
}
|
||||||
@ -241,8 +241,8 @@ class LibManga : ConfigurableSource, HttpSource() {
|
|||||||
else -> dataManga!!.jsonObject["name"]!!.jsonPrimitive.content
|
else -> dataManga!!.jsonObject["name"]!!.jsonPrimitive.content
|
||||||
}
|
}
|
||||||
manga.thumbnail_url = document.select(".media-header__cover").attr("src")
|
manga.thumbnail_url = document.select(".media-header__cover").attr("src")
|
||||||
manga.author = body.select("div.media-info-list__title:contains(Автор) + div").text()
|
manga.author = body.select("div.media-info-list__title:contains(Автор) + div a").joinToString { it.text() }
|
||||||
manga.artist = body.select("div.media-info-list__title:contains(Художник) + div").text()
|
manga.artist = body.select("div.media-info-list__title:contains(Художник) + div a").joinToString { it.text() }
|
||||||
manga.status = if (document.html().contains("paper empty section")
|
manga.status = if (document.html().contains("paper empty section")
|
||||||
) {
|
) {
|
||||||
SManga.LICENSED
|
SManga.LICENSED
|
||||||
@ -288,14 +288,15 @@ class LibManga : ConfigurableSource, HttpSource() {
|
|||||||
val chaptersList = data["chapters"]!!.jsonObject["list"]?.jsonArray
|
val chaptersList = data["chapters"]!!.jsonObject["list"]?.jsonArray
|
||||||
val slug = data["manga"]!!.jsonObject["slug"]!!.jsonPrimitive.content
|
val slug = data["manga"]!!.jsonObject["slug"]!!.jsonPrimitive.content
|
||||||
val branches = data["chapters"]!!.jsonObject["branches"]!!.jsonArray.reversed()
|
val branches = data["chapters"]!!.jsonObject["branches"]!!.jsonArray.reversed()
|
||||||
|
val teams = data["chapters"]!!.jsonObject["teams"]!!.jsonArray
|
||||||
val sortingList = preferences.getString(SORTING_PREF, "ms_mixing")
|
val sortingList = preferences.getString(SORTING_PREF, "ms_mixing")
|
||||||
|
|
||||||
val chapters: List<SChapter>? = if (branches.isNotEmpty() && !sortingList.equals("ms_mixing")) {
|
val chapters: List<SChapter>? = if (branches.isNotEmpty()) {
|
||||||
sortChaptersByTranslator(sortingList, chaptersList, slug, branches)
|
sortChaptersByTranslator(sortingList, chaptersList, slug, branches)
|
||||||
} else {
|
} else {
|
||||||
chaptersList
|
chaptersList
|
||||||
?.filter { it.jsonObject["status"]?.jsonPrimitive?.intOrNull != 2 }
|
?.filter { it.jsonObject["status"]?.jsonPrimitive?.intOrNull != 2 }
|
||||||
?.map { chapterFromElement(it, sortingList, slug) }
|
?.map { chapterFromElement(it, sortingList, slug, null, null, teams, chaptersList) }
|
||||||
}
|
}
|
||||||
|
|
||||||
return chapters ?: emptyList()
|
return chapters ?: emptyList()
|
||||||
@ -304,47 +305,49 @@ class LibManga : ConfigurableSource, HttpSource() {
|
|||||||
private fun sortChaptersByTranslator
|
private fun sortChaptersByTranslator
|
||||||
(sortingList: String?, chaptersList: JsonArray?, slug: String, branches: List<JsonElement>): List<SChapter>? {
|
(sortingList: String?, chaptersList: JsonArray?, slug: String, branches: List<JsonElement>): List<SChapter>? {
|
||||||
var chapters: List<SChapter>? = null
|
var chapters: List<SChapter>? = null
|
||||||
|
val volume = "(?<=/v)[0-9]+(?=/c[0-9]+)".toRegex()
|
||||||
when (sortingList) {
|
when (sortingList) {
|
||||||
"ms_combining" -> {
|
"ms_mixing" -> {
|
||||||
val tempChaptersList = mutableListOf<SChapter>()
|
val tempChaptersList = mutableListOf<SChapter>()
|
||||||
for (currentBranch in branches.withIndex()) {
|
for (currentBranch in branches.withIndex()) {
|
||||||
val teamId = branches[currentBranch.index].jsonObject["id"]!!.jsonPrimitive.int
|
val branch = branches[currentBranch.index]
|
||||||
|
val teamId = branch.jsonObject["id"]!!.jsonPrimitive.int
|
||||||
|
val teams = branch.jsonObject["teams"]!!.jsonArray.filter { it.jsonObject["is_active"]?.jsonPrimitive?.intOrNull == 1 }
|
||||||
|
val teamsBranch = if (teams.size == 1)
|
||||||
|
teams[0].jsonObject["name"]?.jsonPrimitive?.contentOrNull
|
||||||
|
else if (teams.isEmpty())
|
||||||
|
branch.jsonObject["teams"]!!.jsonArray[0].jsonObject["name"]!!.jsonPrimitive.content
|
||||||
|
else null
|
||||||
chapters = chaptersList
|
chapters = chaptersList
|
||||||
?.filter { it.jsonObject["branch_id"]?.jsonPrimitive?.intOrNull == teamId && it.jsonObject["status"]?.jsonPrimitive?.intOrNull != 2 }
|
?.filter { it.jsonObject["branch_id"]?.jsonPrimitive?.intOrNull == teamId && it.jsonObject["status"]?.jsonPrimitive?.intOrNull != 2 }
|
||||||
?.map { chapterFromElement(it, sortingList, slug, teamId, branches) }
|
?.map { chapterFromElement(it, sortingList, slug, teamId, branches) }
|
||||||
chapters?.let { tempChaptersList.addAll(it) }
|
chapters?.let {
|
||||||
}
|
if ((tempChaptersList.size < it.size) && !groupTranslates.contains(teamsBranch.toString()))
|
||||||
chapters = tempChaptersList
|
tempChaptersList.addAll(0, it)
|
||||||
}
|
else
|
||||||
"ms_largest" -> {
|
tempChaptersList.addAll(it)
|
||||||
val sizesChaptersLists = mutableListOf<Int>()
|
|
||||||
for (currentBranch in branches.withIndex()) {
|
|
||||||
val teamId = branches[currentBranch.index].jsonObject["id"]!!.jsonPrimitive.int
|
|
||||||
val chapterSize = chaptersList
|
|
||||||
?.filter { it.jsonObject["branch_id"]?.jsonPrimitive?.intOrNull == teamId }!!.size
|
|
||||||
sizesChaptersLists.add(chapterSize)
|
|
||||||
}
|
|
||||||
val max = sizesChaptersLists.indexOfFirst { it == sizesChaptersLists.maxOrNull() ?: 0 }
|
|
||||||
val teamId = branches[max].jsonObject["id"]!!.jsonPrimitive.int
|
|
||||||
|
|
||||||
chapters = chaptersList
|
|
||||||
?.filter { it.jsonObject["branch_id"]?.jsonPrimitive?.intOrNull == teamId && it.jsonObject["status"]?.jsonPrimitive?.intOrNull != 2 }
|
|
||||||
?.map { chapterFromElement(it, sortingList, slug, teamId, branches) }
|
|
||||||
}
|
|
||||||
"ms_active" -> {
|
|
||||||
for (currentBranch in branches.withIndex()) {
|
|
||||||
val teams = branches[currentBranch.index].jsonObject["teams"]!!.jsonArray
|
|
||||||
for (currentTeam in teams.withIndex()) {
|
|
||||||
if (teams[currentTeam.index].jsonObject["is_active"]!!.jsonPrimitive.int == 1) {
|
|
||||||
val teamId = branches[currentBranch.index].jsonObject["id"]!!.jsonPrimitive.int
|
|
||||||
chapters = chaptersList
|
|
||||||
?.filter { it.jsonObject["branch_id"]?.jsonPrimitive?.intOrNull == teamId && it.jsonObject["status"]?.jsonPrimitive?.intOrNull != 2 }
|
|
||||||
?.map { chapterFromElement(it, sortingList, slug, teamId, branches) }
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
chapters ?: throw Exception("Активный перевод не назначен на сайте")
|
chapters = tempChaptersList.distinctBy { volume.find(it.url)?.value + "--" + it.chapter_number }.sortedWith(compareBy({ -it.chapter_number }, { volume.find(it.url)?.value }))
|
||||||
|
}
|
||||||
|
"ms_combining" -> {
|
||||||
|
val tempChaptersList = mutableListOf<SChapter>()
|
||||||
|
for (currentBranch in branches.withIndex()) {
|
||||||
|
val branch = branches[currentBranch.index]
|
||||||
|
val teamId = branch.jsonObject["id"]!!.jsonPrimitive.int
|
||||||
|
val teams = branch.jsonObject["teams"]!!.jsonArray.filter { it.jsonObject["is_active"]?.jsonPrimitive?.intOrNull == 1 }
|
||||||
|
val teamsBranch = if (teams.size == 1)
|
||||||
|
teams[0].jsonObject["name"]?.jsonPrimitive?.contentOrNull
|
||||||
|
else if (teams.isEmpty())
|
||||||
|
branch.jsonObject["teams"]!!.jsonArray[0].jsonObject["name"]!!.jsonPrimitive.content
|
||||||
|
else null
|
||||||
|
chapters = chaptersList
|
||||||
|
?.filter { it.jsonObject["branch_id"]?.jsonPrimitive?.intOrNull == teamId && it.jsonObject["status"]?.jsonPrimitive?.intOrNull != 2 }
|
||||||
|
?.map { chapterFromElement(it, sortingList, slug, teamId, branches) }
|
||||||
|
if (!groupTranslates.contains(teamsBranch.toString()))
|
||||||
|
chapters?.let { tempChaptersList.addAll(it) }
|
||||||
|
}
|
||||||
|
chapters = tempChaptersList
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,11 +355,13 @@ class LibManga : ConfigurableSource, HttpSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun chapterFromElement
|
private fun chapterFromElement
|
||||||
(chapterItem: JsonElement, sortingList: String?, slug: String, teamIdParam: Int? = null, branches: List<JsonElement>? = null): SChapter {
|
(chapterItem: JsonElement, sortingList: String?, slug: String, teamIdParam: Int? = null, branches: List<JsonElement>? = null, teams: List<JsonElement>? = null, chaptersList: JsonArray? = null): SChapter {
|
||||||
val chapter = SChapter.create()
|
val chapter = SChapter.create()
|
||||||
|
|
||||||
val volume = chapterItem.jsonObject["chapter_volume"]!!.jsonPrimitive.int
|
val volume = chapterItem.jsonObject["chapter_volume"]!!.jsonPrimitive.int
|
||||||
val number = chapterItem.jsonObject["chapter_number"]!!.jsonPrimitive.content
|
val number = chapterItem.jsonObject["chapter_number"]!!.jsonPrimitive.content
|
||||||
|
val chapterScanlatorId = chapterItem.jsonObject["chapter_scanlator_id"]!!.jsonPrimitive.int
|
||||||
|
val isScanlatorId = teams?.filter { it.jsonObject["id"]?.jsonPrimitive?.intOrNull == chapterScanlatorId }
|
||||||
val teamId = if (teamIdParam != null) "?bid=$teamIdParam" else ""
|
val teamId = if (teamIdParam != null) "?bid=$teamIdParam" else ""
|
||||||
|
|
||||||
val url = "$baseUrl/$slug/v$volume/c$number$teamId"
|
val url = "$baseUrl/$slug/v$volume/c$number$teamId"
|
||||||
@ -365,12 +370,11 @@ class LibManga : ConfigurableSource, HttpSource() {
|
|||||||
|
|
||||||
val nameChapter = chapterItem.jsonObject["chapter_name"]?.jsonPrimitive?.contentOrNull
|
val nameChapter = chapterItem.jsonObject["chapter_name"]?.jsonPrimitive?.contentOrNull
|
||||||
val fullNameChapter = "Том $volume. Глава $number"
|
val fullNameChapter = "Том $volume. Глава $number"
|
||||||
if (!sortingList.equals("ms_mixing")) {
|
chapter.scanlator = if (teams?.size == 1) teams[0].jsonObject["name"]?.jsonPrimitive?.content else if (isScanlatorId.orEmpty().isNotEmpty()) isScanlatorId!![0].jsonObject["name"]?.jsonPrimitive?.content else branches?.let { getScanlatorTeamName(it, chapterItem) } ?: if ((preferences.getBoolean(isScan_USER, false)) || (chaptersList?.distinctBy { it.jsonObject["username"]!!.jsonPrimitive.content }?.size == 1)) chapterItem.jsonObject["username"]!!.jsonPrimitive.content else null
|
||||||
chapter.scanlator = branches?.let { getScanlatorTeamName(it, chapterItem) }
|
|
||||||
}
|
|
||||||
chapter.name = if (nameChapter.isNullOrBlank()) fullNameChapter else "$fullNameChapter - $nameChapter"
|
chapter.name = if (nameChapter.isNullOrBlank()) fullNameChapter else "$fullNameChapter - $nameChapter"
|
||||||
chapter.date_upload = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
chapter.date_upload = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||||
.parse(chapterItem.jsonObject["chapter_created_at"]!!.jsonPrimitive.content.substringBefore(" "))?.time ?: 0L
|
.parse(chapterItem.jsonObject["chapter_created_at"]!!.jsonPrimitive.content.substringBefore(" "))?.time ?: 0L
|
||||||
|
chapter.chapter_number = number.toFloat()
|
||||||
|
|
||||||
return chapter
|
return chapter
|
||||||
}
|
}
|
||||||
@ -384,22 +388,15 @@ class LibManga : ConfigurableSource, HttpSource() {
|
|||||||
for (currentTeam in teams.withIndex()) {
|
for (currentTeam in teams.withIndex()) {
|
||||||
val team = teams[currentTeam.index].jsonObject
|
val team = teams[currentTeam.index].jsonObject
|
||||||
val scanlatorId = chapterItem.jsonObject["chapter_scanlator_id"]!!.jsonPrimitive.int
|
val scanlatorId = chapterItem.jsonObject["chapter_scanlator_id"]!!.jsonPrimitive.int
|
||||||
scanlatorData = if ((scanlatorId == team.jsonObject["id"]!!.jsonPrimitive.int) ||
|
if ((scanlatorId == team.jsonObject["id"]!!.jsonPrimitive.int) ||
|
||||||
(scanlatorId == 0 && team["is_active"]!!.jsonPrimitive.int == 1)
|
(scanlatorId == 0 && team["is_active"]!!.jsonPrimitive.int == 1)
|
||||||
) team["name"]!!.jsonPrimitive.content else branch["teams"]!!.jsonArray[0].jsonObject["name"]!!.jsonPrimitive.content
|
) return team["name"]!!.jsonPrimitive.content else scanlatorData = branch["teams"]!!.jsonArray[0].jsonObject["name"]!!.jsonPrimitive.content
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return scanlatorData
|
return scanlatorData
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
|
|
||||||
"""Глава\s(\d+)""".toRegex().find(chapter.name)?.let {
|
|
||||||
val number = it.groups[1]?.value!!
|
|
||||||
chapter.chapter_number = number.toFloat()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pages
|
// Pages
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
@ -625,7 +622,7 @@ class LibManga : ConfigurableSource, HttpSource() {
|
|||||||
private class OrderBy : Filter.Sort(
|
private class OrderBy : Filter.Sort(
|
||||||
"Сортировка",
|
"Сортировка",
|
||||||
arrayOf("Рейтинг", "Имя", "Просмотры", "Дате добавления", "Дате обновления", "Кол-во глав"),
|
arrayOf("Рейтинг", "Имя", "Просмотры", "Дате добавления", "Дате обновления", "Кол-во глав"),
|
||||||
Selection(0, false)
|
Selection(2, false)
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -845,6 +842,12 @@ class LibManga : ConfigurableSource, HttpSource() {
|
|||||||
private const val SORTING_PREF = "MangaLibSorting"
|
private const val SORTING_PREF = "MangaLibSorting"
|
||||||
private const val SORTING_PREF_Title = "Способ выбора переводчиков"
|
private const val SORTING_PREF_Title = "Способ выбора переводчиков"
|
||||||
|
|
||||||
|
private const val isScan_USER = "ScanlatorUsername"
|
||||||
|
private const val isScan_USER_Title = "Альтернативный переводчик"
|
||||||
|
|
||||||
|
private const val TRANSLATORS_TITLE = "Чёрный список переводчиков\n(для красоты через «/» или с новой строки)"
|
||||||
|
private const val TRANSLATORS_DEFAULT = ""
|
||||||
|
|
||||||
private const val DOMAIN_PREF = "MangaLibDomain"
|
private const val DOMAIN_PREF = "MangaLibDomain"
|
||||||
private const val DOMAIN_PREF_Title = "Выбор домена"
|
private const val DOMAIN_PREF_Title = "Выбор домена"
|
||||||
|
|
||||||
@ -856,6 +859,7 @@ class LibManga : ConfigurableSource, HttpSource() {
|
|||||||
|
|
||||||
private var server: String? = preferences.getString(SERVER_PREF, null)
|
private var server: String? = preferences.getString(SERVER_PREF, null)
|
||||||
private var isEng: String? = preferences.getString(LANGUAGE_PREF, "eng")
|
private var isEng: String? = preferences.getString(LANGUAGE_PREF, "eng")
|
||||||
|
private var groupTranslates: String = preferences.getString(TRANSLATORS_TITLE, TRANSLATORS_DEFAULT)!!
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
val serverPref = ListPreference(screen.context).apply {
|
val serverPref = ListPreference(screen.context).apply {
|
||||||
key = SERVER_PREF
|
key = SERVER_PREF
|
||||||
@ -874,10 +878,9 @@ class LibManga : ConfigurableSource, HttpSource() {
|
|||||||
key = SORTING_PREF
|
key = SORTING_PREF
|
||||||
title = SORTING_PREF_Title
|
title = SORTING_PREF_Title
|
||||||
entries = arrayOf(
|
entries = arrayOf(
|
||||||
"Полный список (без повторных переводов)", "Все переводы (друг за другом)",
|
"Полный список (без повторных переводов)", "Все переводы (друг за другом)"
|
||||||
"Наибольшее число глав", "Активный перевод"
|
|
||||||
)
|
)
|
||||||
entryValues = arrayOf("ms_mixing", "ms_combining", "ms_largest", "ms_active")
|
entryValues = arrayOf("ms_mixing", "ms_combining")
|
||||||
summary = "%s"
|
summary = "%s"
|
||||||
setDefaultValue("ms_mixing")
|
setDefaultValue("ms_mixing")
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
@ -885,6 +888,17 @@ class LibManga : ConfigurableSource, HttpSource() {
|
|||||||
preferences.edit().putString(SORTING_PREF, selected).commit()
|
preferences.edit().putString(SORTING_PREF, selected).commit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val scanlatorUsername = androidx.preference.CheckBoxPreference(screen.context).apply {
|
||||||
|
key = isScan_USER
|
||||||
|
title = isScan_USER_Title
|
||||||
|
summary = "Отображает Ник переводчика если Группа не указана явно."
|
||||||
|
setDefaultValue(false)
|
||||||
|
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
val checkValue = newValue as Boolean
|
||||||
|
preferences.edit().putBoolean(key, checkValue).commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
val domainPref = ListPreference(screen.context).apply {
|
val domainPref = ListPreference(screen.context).apply {
|
||||||
key = DOMAIN_PREF
|
key = DOMAIN_PREF
|
||||||
title = DOMAIN_PREF_Title
|
title = DOMAIN_PREF_Title
|
||||||
@ -922,6 +936,27 @@ class LibManga : ConfigurableSource, HttpSource() {
|
|||||||
screen.addPreference(domainPref)
|
screen.addPreference(domainPref)
|
||||||
screen.addPreference(serverPref)
|
screen.addPreference(serverPref)
|
||||||
screen.addPreference(sortingPref)
|
screen.addPreference(sortingPref)
|
||||||
|
screen.addPreference(screen.editTextPreference(TRANSLATORS_TITLE, TRANSLATORS_DEFAULT, groupTranslates))
|
||||||
|
screen.addPreference(scanlatorUsername)
|
||||||
screen.addPreference(titleLanguagePref)
|
screen.addPreference(titleLanguagePref)
|
||||||
}
|
}
|
||||||
|
private fun PreferenceScreen.editTextPreference(title: String, default: String, value: String): androidx.preference.EditTextPreference {
|
||||||
|
return androidx.preference.EditTextPreference(context).apply {
|
||||||
|
key = title
|
||||||
|
this.title = title
|
||||||
|
summary = value.replace("/", "\n")
|
||||||
|
this.setDefaultValue(default)
|
||||||
|
dialogTitle = title
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
try {
|
||||||
|
val res = preferences.edit().putString(title, newValue as String).commit()
|
||||||
|
Toast.makeText(context, "Для обновления списка необходимо перезапустить приложение с полной остановкой.", Toast.LENGTH_LONG).show()
|
||||||
|
res
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user