diff --git a/src/ru/mangabuff/AndroidManifest.xml b/src/ru/mangabuff/AndroidManifest.xml
new file mode 100644
index 000000000..80c894ce2
--- /dev/null
+++ b/src/ru/mangabuff/AndroidManifest.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ru/mangabuff/build.gradle b/src/ru/mangabuff/build.gradle
new file mode 100644
index 000000000..7d7449c6d
--- /dev/null
+++ b/src/ru/mangabuff/build.gradle
@@ -0,0 +1,8 @@
+ext {
+ extName = 'MangaBuff'
+ extClass = '.MangaBuff'
+ extVersionCode = 1
+ isNsfw = false
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/ru/mangabuff/res/mipmap-hdpi/ic_launcher.png b/src/ru/mangabuff/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..aa839268a
Binary files /dev/null and b/src/ru/mangabuff/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/ru/mangabuff/res/mipmap-mdpi/ic_launcher.png b/src/ru/mangabuff/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..2032cc6e7
Binary files /dev/null and b/src/ru/mangabuff/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/ru/mangabuff/res/mipmap-xhdpi/ic_launcher.png b/src/ru/mangabuff/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..bd8d566c3
Binary files /dev/null and b/src/ru/mangabuff/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/ru/mangabuff/res/mipmap-xxhdpi/ic_launcher.png b/src/ru/mangabuff/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..58591e85b
Binary files /dev/null and b/src/ru/mangabuff/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/ru/mangabuff/res/mipmap-xxxhdpi/ic_launcher.png b/src/ru/mangabuff/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..49ac0b367
Binary files /dev/null and b/src/ru/mangabuff/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/ru/mangabuff/src/eu/kanade/tachiyomi/extension/ru/mangabuff/MangaBuff.kt b/src/ru/mangabuff/src/eu/kanade/tachiyomi/extension/ru/mangabuff/MangaBuff.kt
new file mode 100644
index 000000000..4d58f0366
--- /dev/null
+++ b/src/ru/mangabuff/src/eu/kanade/tachiyomi/extension/ru/mangabuff/MangaBuff.kt
@@ -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 {
+ 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 {
+ 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()
+ .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 {
+ 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 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)
+ }
+}
diff --git a/src/ru/mangabuff/src/eu/kanade/tachiyomi/extension/ru/mangabuff/MangaBuffFilters.kt b/src/ru/mangabuff/src/eu/kanade/tachiyomi/extension/ru/mangabuff/MangaBuffFilters.kt
new file mode 100644
index 000000000..2604195cf
--- /dev/null
+++ b/src/ru/mangabuff/src/eu/kanade/tachiyomi/extension/ru/mangabuff/MangaBuffFilters.kt
@@ -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>,
+ defaultValue: String? = null,
+) : Filter.Select(
+ 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>,
+) : Filter.Group(
+ 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>,
+) : Filter.Group(
+ 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"),
+ )
+ }
+}
diff --git a/src/ru/mangabuff/src/eu/kanade/tachiyomi/extension/ru/mangabuff/MangaBuffUrlActivity.kt b/src/ru/mangabuff/src/eu/kanade/tachiyomi/extension/ru/mangabuff/MangaBuffUrlActivity.kt
new file mode 100644
index 000000000..65d0707df
--- /dev/null
+++ b/src/ru/mangabuff/src/eu/kanade/tachiyomi/extension/ru/mangabuff/MangaBuffUrlActivity.kt
@@ -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)
+ }
+}