Add MangaBuff (#3891)
* Add MangaBuff * style * Update src/ru/mangabuff/src/eu/kanade/tachiyomi/extension/ru/mangabuff/MangaBuff.kt Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> * PR comments Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> --------- Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
This commit is contained in:
parent
5b920b207a
commit
121f0591db
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<activity
|
||||||
|
android:name="eu.kanade.tachiyomi.extension.ru.mangabuff.MangaBuffUrlActivity"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:theme="@android:style/Theme.NoDisplay">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data
|
||||||
|
android:host="mangabuff.ru"
|
||||||
|
android:pathPattern="/manga/..*"
|
||||||
|
android:scheme="https" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
|
@ -0,0 +1,8 @@
|
||||||
|
ext {
|
||||||
|
extName = 'MangaBuff'
|
||||||
|
extClass = '.MangaBuff'
|
||||||
|
extVersionCode = 1
|
||||||
|
isNsfw = false
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
After Width: | Height: | Size: 6.5 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
Binary file not shown.
After Width: | Height: | Size: 37 KiB |
|
@ -0,0 +1,343 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.ru.mangabuff
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class MangaBuff : ParsedHttpSource() {
|
||||||
|
override val baseUrl = "https://mangabuff.ru"
|
||||||
|
override val lang = "ru"
|
||||||
|
override val name = "MangaBuff"
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override val client = network.cloudflareClient.newBuilder()
|
||||||
|
.addInterceptor(::tokenInterceptor)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
// From Akuma - CSRF token
|
||||||
|
private var storedToken: String? = null
|
||||||
|
|
||||||
|
private fun tokenInterceptor(chain: Interceptor.Chain): Response {
|
||||||
|
val request = chain.request()
|
||||||
|
|
||||||
|
if (request.method == "POST" && request.header("X-CSRF-TOKEN") == null) {
|
||||||
|
val newRequest = request.newBuilder()
|
||||||
|
val token = getToken()
|
||||||
|
val response = chain.proceed(
|
||||||
|
newRequest
|
||||||
|
.addHeader("X-CSRF-TOKEN", token)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (response.code == 419) {
|
||||||
|
response.close()
|
||||||
|
storedToken = null // reset the token
|
||||||
|
val newToken = getToken()
|
||||||
|
return chain.proceed(
|
||||||
|
newRequest
|
||||||
|
.addHeader("X-CSRF-TOKEN", newToken)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = chain.proceed(request)
|
||||||
|
|
||||||
|
if (response.header("Content-Type")?.contains("text/html") != true) {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
storedToken = Jsoup.parse(response.peekBody(Long.MAX_VALUE).string())
|
||||||
|
.selectFirst("head meta[name*=csrf-token]")
|
||||||
|
?.attr("content")
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getToken(): String {
|
||||||
|
if (storedToken.isNullOrEmpty()) {
|
||||||
|
val request = GET(baseUrl, headers)
|
||||||
|
client.newCall(request).execute().close() // updates token in interceptor
|
||||||
|
}
|
||||||
|
return storedToken!!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Popular
|
||||||
|
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", SortFilter.POPULAR)
|
||||||
|
override fun popularMangaSelector() = searchMangaSelector()
|
||||||
|
override fun popularMangaFromElement(element: Element) = searchMangaFromElement(element)
|
||||||
|
override fun popularMangaNextPageSelector() = searchMangaNextPageSelector()
|
||||||
|
|
||||||
|
// Latest
|
||||||
|
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", SortFilter.LATEST)
|
||||||
|
override fun latestUpdatesSelector() = searchMangaSelector()
|
||||||
|
override fun latestUpdatesFromElement(element: Element) = searchMangaFromElement(element)
|
||||||
|
override fun latestUpdatesNextPageSelector() = searchMangaNextPageSelector()
|
||||||
|
|
||||||
|
// Search
|
||||||
|
override fun fetchSearchManga(
|
||||||
|
page: Int,
|
||||||
|
query: String,
|
||||||
|
filters: FilterList,
|
||||||
|
): Observable<MangasPage> {
|
||||||
|
if (!query.startsWith(SEARCH_PREFIX)) {
|
||||||
|
return super.fetchSearchManga(page, query, filters)
|
||||||
|
}
|
||||||
|
|
||||||
|
val request = GET("$baseUrl/manga/${query.substringAfter(SEARCH_PREFIX)}")
|
||||||
|
return client.newCall(request).asObservableSuccess().map { response ->
|
||||||
|
val details = mangaDetailsParse(response)
|
||||||
|
details.setUrlWithoutDomain(request.url.toString())
|
||||||
|
MangasPage(listOf(details), false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
if (query.isNotEmpty()) {
|
||||||
|
val url = "$baseUrl/search".toHttpUrl().newBuilder().apply {
|
||||||
|
addQueryParameter("q", query)
|
||||||
|
if (page != 1) addQueryParameter("page", page.toString())
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
val url = "$baseUrl/manga".toHttpUrl().newBuilder().apply {
|
||||||
|
(filters.find { it is GenreFilter } as? GenreFilter)?.let { filter ->
|
||||||
|
filter.included?.forEach { addQueryParameter("genres[]", it) }
|
||||||
|
}
|
||||||
|
(filters.find { it is TypeFilter } as? TypeFilter)?.let { filter ->
|
||||||
|
filter.included?.forEach { addQueryParameter("type_id[]", it) }
|
||||||
|
}
|
||||||
|
(filters.find { it is TagFilter } as? TagFilter)?.let { filter ->
|
||||||
|
filter.included?.forEach { addQueryParameter("tags[]", it) }
|
||||||
|
}
|
||||||
|
(filters.find { it is StatusFilter } as? StatusFilter)?.let { filter ->
|
||||||
|
filter.checked?.forEach { addQueryParameter("status_id[]", it) }
|
||||||
|
}
|
||||||
|
(filters.find { it is AgeFilter } as? AgeFilter)?.let { filter ->
|
||||||
|
filter.checked?.forEach { addQueryParameter("age_rating[]", it) }
|
||||||
|
}
|
||||||
|
(filters.find { it is RatingFilter } as? RatingFilter)?.let { filter ->
|
||||||
|
filter.checked?.forEach { addQueryParameter("rating[]", it) }
|
||||||
|
}
|
||||||
|
(filters.find { it is YearFilter } as? YearFilter)?.let { filter ->
|
||||||
|
filter.checked?.forEach { addQueryParameter("year[]", it) }
|
||||||
|
}
|
||||||
|
(filters.find { it is ChapterCountFilter } as? ChapterCountFilter)?.let { filter ->
|
||||||
|
filter.checked?.forEach { addQueryParameter("chapters[]", it) }
|
||||||
|
}
|
||||||
|
(filters.find { it is GenreFilter } as? GenreFilter)?.let { filter ->
|
||||||
|
filter.excluded?.forEach { addQueryParameter("without_genres[]", it) }
|
||||||
|
}
|
||||||
|
(filters.find { it is TypeFilter } as? TypeFilter)?.let { filter ->
|
||||||
|
filter.excluded?.forEach { addQueryParameter("without_type_id[]", it) }
|
||||||
|
}
|
||||||
|
(filters.find { it is TagFilter } as? TagFilter)?.let { filter ->
|
||||||
|
filter.excluded?.forEach { addQueryParameter("without_tags[]", it) }
|
||||||
|
}
|
||||||
|
(filters.find { it is SortFilter } as? SortFilter)?.let { filter ->
|
||||||
|
addQueryParameter("sort", filter.selected)
|
||||||
|
}
|
||||||
|
if (page != 1) addQueryParameter("page", page.toString())
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = ".cards .cards__item"
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
||||||
|
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
|
||||||
|
title = element.selectFirst(".cards__name")!!.text()
|
||||||
|
|
||||||
|
val slug = "$baseUrl$url".toHttpUrl().pathSegments.last()
|
||||||
|
thumbnail_url = "$baseUrl/img/manga/posters/$slug.jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector() =
|
||||||
|
".pagination .pagination__button--active + li:not(:last-child)"
|
||||||
|
|
||||||
|
// Details
|
||||||
|
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||||
|
title = document.selectFirst("h1, .manga__name, .manga-mobile__name")!!.text()
|
||||||
|
|
||||||
|
description = buildString {
|
||||||
|
document
|
||||||
|
.selectFirst(".manga__description")
|
||||||
|
?.text()
|
||||||
|
?.also { append(it) }
|
||||||
|
|
||||||
|
document // rating%
|
||||||
|
.selectFirst(".manga__rating")
|
||||||
|
?.text()
|
||||||
|
?.toDoubleOrNull()
|
||||||
|
?.let { it / 10.0 }
|
||||||
|
?.also {
|
||||||
|
if (isNotEmpty()) append("\n\n")
|
||||||
|
append(String.format(Locale("ru"), "Рейтинг: %.0f%%", it * 100))
|
||||||
|
}
|
||||||
|
|
||||||
|
document // views
|
||||||
|
.selectFirst(".manga__views")
|
||||||
|
?.text()
|
||||||
|
?.replace(" ", "")
|
||||||
|
?.toIntOrNull()
|
||||||
|
?.also {
|
||||||
|
if (isNotEmpty()) append("\n\n")
|
||||||
|
append(String.format(Locale("ru"), "Просмотров: %,d", it))
|
||||||
|
}
|
||||||
|
|
||||||
|
document // favorites
|
||||||
|
.selectFirst(".manga")
|
||||||
|
?.attr("data-fav-count")
|
||||||
|
?.takeIf { it.isNotEmpty() }
|
||||||
|
?.toIntOrNull()
|
||||||
|
?.also {
|
||||||
|
if (isNotEmpty()) append("\n\n")
|
||||||
|
append(String.format(Locale("ru"), "Избранное: %,d", it))
|
||||||
|
}
|
||||||
|
|
||||||
|
document // alternative names
|
||||||
|
.select(".manga__name-alt > span, .manga-mobile__name-alt > span")
|
||||||
|
.eachText()
|
||||||
|
.takeIf { it.isNotEmpty() }
|
||||||
|
?.also {
|
||||||
|
if (isNotEmpty()) append("\n\n")
|
||||||
|
append("Альтернативные названия:\n")
|
||||||
|
append(it.joinToString("\n") { "• $it" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
genre = buildList {
|
||||||
|
addAll(document.select(".manga__middle-links > a:not(:last-child)").eachText())
|
||||||
|
addAll(document.select(".manga-mobile__info > a:not(:last-child)").eachText())
|
||||||
|
addAll(document.select(".tags > .tags__item").eachText())
|
||||||
|
}.takeIf { it.isNotEmpty() }?.joinToString()
|
||||||
|
|
||||||
|
status = document
|
||||||
|
.select(".manga__middle-links > a:last-child, .manga-mobile__info > a:last-child")
|
||||||
|
.text()
|
||||||
|
.parseStatus()
|
||||||
|
|
||||||
|
thumbnail_url = document
|
||||||
|
.selectFirst(".manga__img img, img.manga-mobile__image")
|
||||||
|
?.absUrl("src")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chapters
|
||||||
|
override fun chapterListSelector() = "a.chapters__item"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||||
|
setUrlWithoutDomain(element.absUrl("href"))
|
||||||
|
name = element.select(".chapters__volume, .chapters__value, .chapters__name").text()
|
||||||
|
date_upload = runCatching {
|
||||||
|
dateFormat.parse(element.selectFirst(".chapters__add-date")!!.text())!!.time
|
||||||
|
}.getOrDefault(0L)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
val document = Jsoup.parse(response.peekBody(Long.MAX_VALUE).string())
|
||||||
|
|
||||||
|
val chapters = super.chapterListParse(response)
|
||||||
|
|
||||||
|
// HTML only shows 100 entries. If this class is present it will load more via API
|
||||||
|
if (document.selectFirst(".load-chapters-trigger") == null) {
|
||||||
|
return chapters
|
||||||
|
}
|
||||||
|
|
||||||
|
val mangaId = document.selectFirst(".manga")?.attr("data-id")
|
||||||
|
?: throw Exception("Не удалось найти ID манги")
|
||||||
|
|
||||||
|
val form = FormBody.Builder()
|
||||||
|
.add("manga_id", mangaId)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val moreChapters = client
|
||||||
|
.newCall(POST("$baseUrl/chapters/load", headers, form))
|
||||||
|
.execute()
|
||||||
|
.parseAs<WrappedHtmlDto>()
|
||||||
|
.content
|
||||||
|
.let(Jsoup::parseBodyFragment)
|
||||||
|
.select(chapterListSelector())
|
||||||
|
.map(::chapterFromElement)
|
||||||
|
|
||||||
|
return chapters + moreChapters
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pages
|
||||||
|
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
return document.select(".reader__pages img").mapIndexed { i, img ->
|
||||||
|
Page(i, document.location(), img.imgAttr())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other
|
||||||
|
override fun getFilterList() = FilterList(
|
||||||
|
Filter.Header("ПРИМЕЧАНИЕ: Игнорируется, если используется поиск по тексту!"),
|
||||||
|
Filter.Separator(),
|
||||||
|
SortFilter(),
|
||||||
|
GenreFilter(),
|
||||||
|
TypeFilter(),
|
||||||
|
TagFilter(),
|
||||||
|
StatusFilter(),
|
||||||
|
AgeFilter(),
|
||||||
|
RatingFilter(),
|
||||||
|
YearFilter(),
|
||||||
|
ChapterCountFilter(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun String.parseStatus(): Int = when (this.lowercase()) {
|
||||||
|
"завершен" -> SManga.COMPLETED
|
||||||
|
"продолжается" -> SManga.ONGOING
|
||||||
|
"заморожен" -> SManga.ON_HIATUS
|
||||||
|
"заброшен" -> SManga.CANCELLED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Element.imgAttr(): String = when {
|
||||||
|
hasAttr("data-src") -> absUrl("data-src")
|
||||||
|
else -> absUrl("src")
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> Response.parseAs(): T =
|
||||||
|
json.decodeFromString(body.string())
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class WrappedHtmlDto(
|
||||||
|
val content: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val SEARCH_PREFIX = "slug:"
|
||||||
|
private val dateFormat = SimpleDateFormat("dd.MM.yyyy", Locale.ROOT)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,289 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.ru.mangabuff
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
|
||||||
|
abstract class SelectFilter(
|
||||||
|
name: String,
|
||||||
|
private val options: List<Pair<String, String>>,
|
||||||
|
defaultValue: String? = null,
|
||||||
|
) : Filter.Select<String>(
|
||||||
|
name,
|
||||||
|
options.map { it.first }.toTypedArray(),
|
||||||
|
options.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
|
||||||
|
) {
|
||||||
|
val selected get() = options[state].second.takeUnless { it.isEmpty() }
|
||||||
|
}
|
||||||
|
|
||||||
|
class CheckBoxFilter(name: String, val value: String) : Filter.CheckBox(name)
|
||||||
|
|
||||||
|
abstract class CheckBoxGroup(
|
||||||
|
name: String,
|
||||||
|
options: List<Pair<String, String>>,
|
||||||
|
) : Filter.Group<CheckBoxFilter>(
|
||||||
|
name,
|
||||||
|
options.map { CheckBoxFilter(it.first, it.second) },
|
||||||
|
) {
|
||||||
|
val checked get() = state.filter { it.state }.map { it.value }.takeUnless { it.isEmpty() }
|
||||||
|
}
|
||||||
|
|
||||||
|
class TriStateFilter(name: String, val value: String) : Filter.TriState(name)
|
||||||
|
|
||||||
|
abstract class TriStateGroup(
|
||||||
|
name: String,
|
||||||
|
private val options: List<Pair<String, String>>,
|
||||||
|
) : Filter.Group<TriStateFilter>(
|
||||||
|
name,
|
||||||
|
options.map { TriStateFilter(it.first, it.second) },
|
||||||
|
) {
|
||||||
|
val included get() = state.filter { it.isIncluded() }.map { it.value }.takeUnless { it.isEmpty() }
|
||||||
|
val excluded get() = state.filter { it.isExcluded() }.map { it.value }.takeUnless { it.isEmpty() }
|
||||||
|
}
|
||||||
|
|
||||||
|
class SortFilter(defaultOrder: String? = null) : SelectFilter("Сортировать по", sort, defaultOrder) {
|
||||||
|
companion object {
|
||||||
|
private val sort = listOf(
|
||||||
|
Pair("Популярные", "views"),
|
||||||
|
Pair("Обновленные", "updated_at"),
|
||||||
|
Pair("По рейтингу", "rating"),
|
||||||
|
Pair("По новинкам", "created_at"),
|
||||||
|
)
|
||||||
|
|
||||||
|
val POPULAR = FilterList(SortFilter("views"))
|
||||||
|
val LATEST = FilterList(SortFilter("updated_at"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GenreFilter : TriStateGroup("Жанр", genres) {
|
||||||
|
companion object {
|
||||||
|
private val genres = listOf(
|
||||||
|
Pair("Арт", "1"),
|
||||||
|
Pair("Боевик", "2"),
|
||||||
|
Pair("Боевые искусства", "4"),
|
||||||
|
Pair("Вампиры", "5"),
|
||||||
|
Pair("Гарем", "6"),
|
||||||
|
Pair("Гендерная интрига", "7"),
|
||||||
|
Pair("Героическое фэнтези", "8"),
|
||||||
|
Pair("Детектив", "9"),
|
||||||
|
Pair("Дзёсэй", "10"),
|
||||||
|
Pair("Додзинси", "11"),
|
||||||
|
Pair("Драма", "12"),
|
||||||
|
Pair("Ёнкома", "39"),
|
||||||
|
Pair("Игра", "18"),
|
||||||
|
Pair("История", "13"),
|
||||||
|
Pair("Киберпанк", "21"),
|
||||||
|
Pair("Кодомо", "40"),
|
||||||
|
Pair("Комедия", "14"),
|
||||||
|
Pair("Махо-сёдзе", "20"),
|
||||||
|
Pair("Меха", "15"),
|
||||||
|
Pair("Мистика", "16"),
|
||||||
|
Pair("Научная фантастика", "17"),
|
||||||
|
Pair("Повседневность", "19"),
|
||||||
|
Pair("Постапокалиптика", "22"),
|
||||||
|
Pair("Приключения", "24"),
|
||||||
|
Pair("Психология", "25"),
|
||||||
|
Pair("Романтика", "26"),
|
||||||
|
Pair("Самурайский боевик", "28"),
|
||||||
|
Pair("Сверхъестественное", "30"),
|
||||||
|
Pair("Сёдзё", "31"),
|
||||||
|
Pair("Сёнэн", "29"),
|
||||||
|
Pair("Спорт", "32"),
|
||||||
|
Pair("Сэйнэн", "33"),
|
||||||
|
Pair("Трагедия", "23"),
|
||||||
|
Pair("Триллер", "34"),
|
||||||
|
Pair("Ужасы", "35"),
|
||||||
|
Pair("Фантастика", "27"),
|
||||||
|
Pair("Фэнтези", "36"),
|
||||||
|
Pair("Школа", "3"),
|
||||||
|
Pair("Эротика", "37"),
|
||||||
|
Pair("Этти", "38"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TypeFilter : TriStateGroup("Тип", types) {
|
||||||
|
companion object {
|
||||||
|
private val types = listOf(
|
||||||
|
Pair("Манга", "1"),
|
||||||
|
Pair("OEL-манга", "2"),
|
||||||
|
Pair("Манхва", "3"),
|
||||||
|
Pair("Маньхуа", "4"),
|
||||||
|
Pair("Сингл", "5"),
|
||||||
|
Pair("Руманга", "6"),
|
||||||
|
Pair("Комикс западный", "7"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TagFilter : TriStateGroup("теги", tags) {
|
||||||
|
companion object {
|
||||||
|
private val tags = listOf(
|
||||||
|
Pair("Азартные игры", "7759"),
|
||||||
|
Pair("Алхимия", "7750"),
|
||||||
|
Pair("Амнезия / Потеря памяти", "7776"),
|
||||||
|
Pair("амнезия/потеря памяти", "7780"),
|
||||||
|
Pair("Ангелы", "7744"),
|
||||||
|
Pair("Антигерой", "7691"),
|
||||||
|
Pair("Антиутопия", "7755"),
|
||||||
|
Pair("Апокалипсис", "7774"),
|
||||||
|
Pair("Армия", "7767"),
|
||||||
|
Pair("Артефакты", "7727"),
|
||||||
|
Pair("Боги", "7679"),
|
||||||
|
Pair("Бои на мечах", "7700"),
|
||||||
|
Pair("Борьба за власть", "7734"),
|
||||||
|
Pair("Брат и сестра", "7725"),
|
||||||
|
Pair("Будущее", "7756"),
|
||||||
|
Pair("в первый раз", "7695"),
|
||||||
|
Pair("Ведьма", "7772"),
|
||||||
|
Pair("Вестерн", "7771"),
|
||||||
|
Pair("Видеоигры", "7704"),
|
||||||
|
Pair("Виртуальная реальность", "7760"),
|
||||||
|
Pair("Владыка демонов", "7743"),
|
||||||
|
Pair("Военные", "7676"),
|
||||||
|
Pair("Война", "7770"),
|
||||||
|
Pair("Волшебники / маги", "7680"),
|
||||||
|
Pair("Волшебные существа", "7721"),
|
||||||
|
Pair("Воспоминания из другого мира", "7713"),
|
||||||
|
Pair("Выживание", "7739"),
|
||||||
|
Pair("ГГ женщина", "7702"),
|
||||||
|
Pair("ГГ имба", "7709"),
|
||||||
|
Pair("ГГ мужчина", "7681"),
|
||||||
|
Pair("Геймеры", "7758"),
|
||||||
|
Pair("Гильдии", "7762"),
|
||||||
|
Pair("Глупый ГГ", "7718"),
|
||||||
|
Pair("Гоблины", "7766"),
|
||||||
|
Pair("Горничные", "7753"),
|
||||||
|
Pair("Гяру", "7773"),
|
||||||
|
Pair("Демоны", "7682"),
|
||||||
|
Pair("Драконы", "7751"),
|
||||||
|
Pair("Дружба", "7703"),
|
||||||
|
Pair("Жестокий мир", "7728"),
|
||||||
|
Pair("Жестокость", "7784"),
|
||||||
|
Pair("Животные компаньоны", "7752"),
|
||||||
|
Pair("Завоевание мира", "7748"),
|
||||||
|
Pair("Зверолюди", "7707"),
|
||||||
|
Pair("Злые духи", "7683"),
|
||||||
|
Pair("Зомби", "7726"),
|
||||||
|
Pair("Игровые элементы", "7723"),
|
||||||
|
Pair("Империи", "7711"),
|
||||||
|
Pair("Квесты", "7735"),
|
||||||
|
Pair("Космос", "7749"),
|
||||||
|
Pair("Кулинария", "7740"),
|
||||||
|
Pair("Культивация", "7731"),
|
||||||
|
Pair("Легендарное оружие", "7714"),
|
||||||
|
Pair("Лоли", "7791"),
|
||||||
|
Pair("Магическая академия", "7684"),
|
||||||
|
Pair("Магия", "7677"),
|
||||||
|
Pair("Мафия", "7690"),
|
||||||
|
Pair("Медицина", "7761"),
|
||||||
|
Pair("Месть", "7741"),
|
||||||
|
Pair("Монстр Девушки", "7719"),
|
||||||
|
Pair("Монстродевушки", "7720"),
|
||||||
|
Pair("Монстры", "7685"),
|
||||||
|
Pair("Музыка", "7675"),
|
||||||
|
Pair("Навыки / способности", "7715"),
|
||||||
|
Pair("Наёмники", "7764"),
|
||||||
|
Pair("Насилие / жестокость", "7692"),
|
||||||
|
Pair("Нежить", "7686"),
|
||||||
|
Pair("Ниндзя", "7732"),
|
||||||
|
Pair("Обмен телами", "7757"),
|
||||||
|
Pair("Обратный Гарем", "7705"),
|
||||||
|
Pair("Огнестрельное оружие", "7777"),
|
||||||
|
Pair("Офисные Работники", "7754"),
|
||||||
|
Pair("Пародия", "7745"),
|
||||||
|
Pair("Пираты", "7724"),
|
||||||
|
Pair("Подземелья", "7722"),
|
||||||
|
Pair("Политика", "7736"),
|
||||||
|
Pair("Полиция", "7693"),
|
||||||
|
Pair("Преступники / Криминал", "7733"),
|
||||||
|
Pair("Призраки / Духи", "7687"),
|
||||||
|
Pair("Путешествие во времени", "7710"),
|
||||||
|
Pair("Путешествия во времени", "7730"),
|
||||||
|
Pair("Рабы", "7765"),
|
||||||
|
Pair("Разумные расы", "7688"),
|
||||||
|
Pair("Ранги силы", "7746"),
|
||||||
|
Pair("Реинкарнация", "7706"),
|
||||||
|
Pair("Роботы", "7769"),
|
||||||
|
Pair("Рыцари", "7701"),
|
||||||
|
Pair("Самураи", "7698"),
|
||||||
|
Pair("Система", "7737"),
|
||||||
|
Pair("Скрытие личности", "7708"),
|
||||||
|
Pair("Спасение мира", "7747"),
|
||||||
|
Pair("Спортивное тело", "7742"),
|
||||||
|
Pair("Средневековье", "7699"),
|
||||||
|
Pair("Стимпанк", "7781"),
|
||||||
|
Pair("Супергерои", "7775"),
|
||||||
|
Pair("Традиционные игры", "7768"),
|
||||||
|
Pair("Умный ГГ", "7716"),
|
||||||
|
Pair("Учитель / ученик", "7717"),
|
||||||
|
Pair("Философия", "7729"),
|
||||||
|
Pair("Хикикомори", "7763"),
|
||||||
|
Pair("Холодное оружие", "7738"),
|
||||||
|
Pair("Шантаж", "7778"),
|
||||||
|
Pair("Эльфы", "7678"),
|
||||||
|
Pair("юные", "7696"),
|
||||||
|
Pair("Якудза", "7689"),
|
||||||
|
Pair("Яндере", "7779"),
|
||||||
|
Pair("Япония", "7674"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StatusFilter : CheckBoxGroup("Статус", statuses) {
|
||||||
|
companion object {
|
||||||
|
private val statuses = listOf(
|
||||||
|
Pair("Завершен", "1"),
|
||||||
|
Pair("Продолжается", "2"),
|
||||||
|
Pair("Заморожен", "3"),
|
||||||
|
Pair("Заброшен", "4"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AgeFilter : CheckBoxGroup("Возрастной рейтинг", ages) {
|
||||||
|
companion object {
|
||||||
|
private val ages = listOf(
|
||||||
|
Pair("18+", "18+"),
|
||||||
|
Pair("16+", "16+"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RatingFilter : CheckBoxGroup("Рейтинг", ratings) {
|
||||||
|
companion object {
|
||||||
|
private val ratings = listOf(
|
||||||
|
Pair("Рейтинг 50%+", "5"),
|
||||||
|
Pair("Рейтинг 60%+", "6"),
|
||||||
|
Pair("Рейтинг 70%+", "7"),
|
||||||
|
Pair("Рейтинг 80%+", "8"),
|
||||||
|
Pair("Рейтинг 90%+", "9"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class YearFilter : CheckBoxGroup("Год выпуска", years) {
|
||||||
|
companion object {
|
||||||
|
private val years = listOf(
|
||||||
|
Pair("2024", "2024"),
|
||||||
|
Pair("2023", "2023"),
|
||||||
|
Pair("2022", "2022"),
|
||||||
|
Pair("2021", "2021"),
|
||||||
|
Pair("2020", "2020"),
|
||||||
|
Pair("2019", "2019"),
|
||||||
|
Pair("2018", "2018"),
|
||||||
|
Pair("2017", "2017"),
|
||||||
|
Pair("2016", "2016"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChapterCountFilter : CheckBoxGroup("Колличество глав", chapters) {
|
||||||
|
companion object {
|
||||||
|
private val chapters = listOf(
|
||||||
|
Pair("<50", "0"),
|
||||||
|
Pair("50-100", "50"),
|
||||||
|
Pair("100-200", "100"),
|
||||||
|
Pair(">200", "200"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.ru.mangabuff
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
class MangaBuffUrlActivity : Activity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
val pathSegments = intent?.data?.pathSegments
|
||||||
|
|
||||||
|
if (pathSegments != null && pathSegments.size > 1) {
|
||||||
|
val slug = pathSegments[1]
|
||||||
|
|
||||||
|
val mainIntent = Intent().apply {
|
||||||
|
action = "eu.kanade.tachiyomi.SEARCH"
|
||||||
|
putExtra("query", "${MangaBuff.SEARCH_PREFIX}$slug")
|
||||||
|
putExtra("filter", packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
startActivity(mainIntent)
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
Log.e("MangaBuffUrlActivity", e.toString())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e("MangaBuffUrlActivity", "could not parse uri from intent $intent")
|
||||||
|
}
|
||||||
|
|
||||||
|
finish()
|
||||||
|
exitProcess(0)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue