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) + } +}