fix(en/mangafire): rework mangafire extension (#7625)

* fix(en/mangafire): rework mangafire extension

* oops

* remove non-null assert

* small fixes
This commit is contained in:
Secozzi 2025-02-14 00:10:10 +01:00 committed by Draff
parent 17300b3bd0
commit 1b1ef9274b
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
3 changed files with 339 additions and 326 deletions

View File

@ -1,7 +1,7 @@
ext {
extName = 'MangaFire'
extClass = '.MangaFireFactory'
extVersionCode = 8
extVersionCode = 9
isNsfw = true
}

View File

@ -1,166 +1,190 @@
package eu.kanade.tachiyomi.extension.all.mangafire
import eu.kanade.tachiyomi.source.model.Filter
import okhttp3.HttpUrl
import java.util.Calendar
class Entry(name: String, val id: String) : Filter.CheckBox(name) {
constructor(name: String) : this(name, name)
interface UriFilter {
fun addToUri(builder: HttpUrl.Builder)
}
sealed class Group(
open class UriPartFilter(
name: String,
val param: String,
values: List<Entry>,
) : Filter.Group<Entry>(name, values)
private val param: String,
private val vals: Array<Pair<String, String>>,
defaultValue: String? = null,
) : Filter.Select<String>(
name,
vals.map { it.first }.toTypedArray(),
vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
),
UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
builder.addQueryParameter(param, vals[state].second)
}
}
sealed class Select(
open class UriMultiSelectOption(name: String, val value: String) : Filter.CheckBox(name)
open class UriMultiSelectFilter(
name: String,
val param: String,
private val valuesMap: Map<String, String>,
) : Filter.Select<String>(name, valuesMap.keys.toTypedArray()) {
open val selection: String
get() = valuesMap[values[state]]!!
private val param: String,
private val vals: Array<Pair<String, String>>,
) : Filter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }), UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
val checked = state.filter { it.state }
checked.forEach {
builder.addQueryParameter(param, it.value)
}
}
}
class TypeFilter : Group("Type", "type[]", types)
open class UriTriSelectOption(name: String, val value: String) : Filter.TriState(name)
private val types: List<Entry>
get() = listOf(
Entry("Manga", "manga"),
Entry("One-Shot", "one_shot"),
Entry("Doujinshi", "doujinshi"),
Entry("Light-Novel", "light_novel"),
Entry("Novel", "novel"),
Entry("Manhwa", "manhwa"),
Entry("Manhua", "manhua"),
)
class Genre(name: String, val id: String) : Filter.TriState(name) {
val selection: String
get() = (if (isExcluded()) "-" else "") + id
open class UriTriSelectFilter(
name: String,
private val param: String,
private val vals: Array<Pair<String, String>>,
) : Filter.Group<UriTriSelectOption>(name, vals.map { UriTriSelectOption(it.first, it.second) }), UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
state.forEach { s ->
when (s.state) {
TriState.STATE_INCLUDE -> builder.addQueryParameter(param, s.value)
TriState.STATE_EXCLUDE -> builder.addQueryParameter(param, "-${s.value}")
}
}
}
}
class GenresFilter : Filter.Group<Genre>("Genre", genres) {
val param = "genre[]"
class TypeFilter : UriPartFilter(
"Type",
"type",
arrayOf(
Pair("Manga", "manga"),
Pair("One-Shot", "one_shot"),
Pair("Doujinshi", "doujinshi"),
Pair("Novel", "novel"),
Pair("Manhwa", "manhwa"),
Pair("Manhua", "manhua"),
),
)
val combineMode: Boolean
get() = state.filter { !it.isIgnored() }.size > 1
class GenreFilter : UriTriSelectFilter(
"Genres",
"genre[]",
arrayOf(
Pair("Action", "1"),
Pair("Adventure", "78"),
Pair("Avant Garde", "3"),
Pair("Boys Love", "4"),
Pair("Comedy", "5"),
Pair("Demons", "77"),
Pair("Drama", "6"),
Pair("Ecchi", "7"),
Pair("Fantasy", "79"),
Pair("Girls Love", "9"),
Pair("Gourmet", "10"),
Pair("Harem", "11"),
Pair("Horror", "530"),
Pair("Isekai", "13"),
Pair("Iyashikei", "531"),
Pair("Josei", "15"),
Pair("Kids", "532"),
Pair("Magic", "539"),
Pair("Mahou Shoujo", "533"),
Pair("Martial Arts", "534"),
Pair("Mecha", "19"),
Pair("Military", "535"),
Pair("Music", "21"),
Pair("Mystery", "22"),
Pair("Parody", "23"),
Pair("Psychological", "536"),
Pair("Reverse Harem", "25"),
Pair("Romance", "26"),
Pair("School", "73"),
Pair("Sci-Fi", "28"),
Pair("Seinen", "537"),
Pair("Shoujo", "30"),
Pair("Shounen", "31"),
Pair("Slice of Life", "538"),
Pair("Space", "33"),
Pair("Sports", "34"),
Pair("Super Power", "75"),
Pair("Supernatural", "76"),
Pair("Suspense", "37"),
Pair("Thriller", "38"),
Pair("Vampire", "39"),
),
)
class GenreModeFilter : Filter.CheckBox("Must have all the selected genres"), UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
if (state) {
builder.addQueryParameter("genre_mode", "and")
}
}
}
private val genres: List<Genre>
get() = listOf(
Genre("Action", "1"),
Genre("Adventure", "78"),
Genre("Avant Garde", "3"),
Genre("Boys Love", "4"),
Genre("Comedy", "5"),
Genre("Demons", "77"),
Genre("Drama", "6"),
Genre("Ecchi", "7"),
Genre("Fantasy", "79"),
Genre("Girls Love", "9"),
Genre("Gourmet", "10"),
Genre("Harem", "11"),
Genre("Horror", "530"),
Genre("Isekai", "13"),
Genre("Iyashikei", "531"),
Genre("Josei", "15"),
Genre("Kids", "532"),
Genre("Magic", "539"),
Genre("Mahou Shoujo", "533"),
Genre("Martial Arts", "534"),
Genre("Mecha", "19"),
Genre("Military", "535"),
Genre("Music", "21"),
Genre("Mystery", "22"),
Genre("Parody", "23"),
Genre("Psychological", "536"),
Genre("Reverse Harem", "25"),
Genre("Romance", "26"),
Genre("School", "73"),
Genre("Sci-Fi", "28"),
Genre("Seinen", "537"),
Genre("Shoujo", "30"),
Genre("Shounen", "31"),
Genre("Slice of Life", "538"),
Genre("Space", "33"),
Genre("Sports", "34"),
Genre("Super Power", "75"),
Genre("Supernatural", "76"),
Genre("Suspense", "37"),
Genre("Thriller", "38"),
Genre("Vampire", "39"),
)
class StatusFilter : UriMultiSelectFilter(
"Status",
"status[]",
arrayOf(
Pair("Completed", "completed"),
Pair("Releasing", "releasing"),
Pair("On Hiatus", "on_hiatus"),
Pair("Discontinued", "discontinued"),
Pair("Not Yet Published", "info"),
),
)
class StatusFilter : Group("Status", "status[]", statuses)
class YearFilter : UriMultiSelectFilter(
"Year",
"year[]",
years,
) {
companion object {
private val currentYear by lazy {
Calendar.getInstance()[Calendar.YEAR]
}
private val statuses: List<Entry>
get() = listOf(
Entry("Completed", "completed"),
Entry("Releasing", "releasing"),
Entry("On Hiatus", "on_hiatus"),
Entry("Discontinued", "discontinued"),
Entry("Not Yet Published", "info"),
)
private val years: Array<Pair<String, String>> = buildList(29) {
addAll(
(currentYear downTo (currentYear - 20)).map(Int::toString),
)
class YearFilter : Group("Year", "year[]", years)
addAll(
(2000 downTo 1930 step 10).map { "${it}s" },
)
}.map { Pair(it, it) }.toTypedArray()
}
}
private val years: List<Entry>
get() = listOf(
Entry("2023"),
Entry("2022"),
Entry("2021"),
Entry("2020"),
Entry("2019"),
Entry("2018"),
Entry("2017"),
Entry("2016"),
Entry("2015"),
Entry("2014"),
Entry("2013"),
Entry("2012"),
Entry("2011"),
Entry("2010"),
Entry("2009"),
Entry("2008"),
Entry("2007"),
Entry("2006"),
Entry("2005"),
Entry("2004"),
Entry("2003"),
Entry("2000s"),
Entry("1990s"),
Entry("1980s"),
Entry("1970s"),
Entry("1960s"),
Entry("1950s"),
Entry("1940s"),
)
class MinChapterFilter : Filter.Text("Minimum chapter length"), UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
if (state.isNotEmpty()) {
val value = state.toIntOrNull()?.takeIf { it > 0 }
?: throw IllegalArgumentException("Minimum chapter length must be a positive integer greater than 0")
class ChapterCountFilter : Select("Chapter Count", "minchap", chapterCounts)
builder.addQueryParameter("minchap", value.toString())
}
}
}
private val chapterCounts
get() = mapOf(
"Any" to "",
"At least 1 chapter" to "1",
"At least 3 chapters" to "3",
"At least 5 chapters" to "5",
"At least 10 chapters" to "10",
"At least 20 chapters" to "20",
"At least 30 chapters" to "30",
"At least 50 chapters" to "50",
)
class SortFilter : Select("Sort", "sort", orders)
private val orders
get() = mapOf(
"Trending" to "trending",
"Recently updated" to "recently_updated",
"Recently added" to "recently_added",
"Release date" to "release_date",
"Name A-Z" to "title_az",
"Score" to "scores",
"MAL score" to "mal_scores",
"Most viewed" to "most_viewed",
"Most favourited" to "most_favourited",
)
class SortFilter(defaultValue: String? = null) : UriPartFilter(
"Sort",
"sort",
arrayOf(
Pair("Most relevance", "most_relevance"),
Pair("Recently updated", "recently_updated"),
Pair("Recently added", "recently_added"),
Pair("Release date", "release_date"),
Pair("Trending", "trending"),
Pair("Name A-Z", "title_az"),
Pair("Scores", "scores"),
Pair("MAL scores", "mal_scores"),
Pair("Most viewed", "most_viewed"),
Pair("Most favourited", "most_favourited"),
),
defaultValue,
)

View File

@ -5,7 +5,6 @@ import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.ConfigurableSource
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
@ -24,11 +23,11 @@ import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.select.Evaluator
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
@ -50,72 +49,50 @@ class MangaFire(
override val client = network.cloudflareClient.newBuilder().addInterceptor(ImageInterceptor).build()
override fun popularMangaRequest(page: Int) =
GET("$baseUrl/filter?sort=most_viewed&language[]=$langCode&page=$page", headers)
// ============================== Popular ===============================
override fun popularMangaRequest(page: Int): Request {
return searchMangaRequest(
page,
"",
FilterList(SortFilter(defaultValue = "most_viewed")),
)
}
override fun popularMangaParse(response: Response) = searchMangaParse(response)
override fun latestUpdatesRequest(page: Int) =
GET("$baseUrl/filter?sort=recently_updated&language[]=$langCode&page=$page", headers)
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request {
return searchMangaRequest(
page,
"",
FilterList(SortFilter(defaultValue = "recently_updated")),
)
}
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
// =============================== Search ===============================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val urlBuilder = baseUrl.toHttpUrl().newBuilder()
if (query.isNotBlank()) {
urlBuilder.addPathSegment("filter").apply {
val url = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment("filter")
if (query.isNotBlank()) {
addQueryParameter("keyword", query)
addQueryParameter("page", page.toString())
}
} else {
urlBuilder.addPathSegment("filter").apply {
addQueryParameter("language[]", langCode)
addQueryParameter("page", page.toString())
filters.ifEmpty(::getFilterList).forEach { filter ->
when (filter) {
is Group -> {
filter.state.forEach {
if (it.state) {
addQueryParameter(filter.param, it.id)
}
}
}
is Select -> {
addQueryParameter(filter.param, filter.selection)
}
is GenresFilter -> {
filter.state.forEach {
if (it.state != 0) {
addQueryParameter(filter.param, it.selection)
}
}
if (filter.combineMode) {
addQueryParameter("genre_mode", "and")
}
}
else -> {}
}
}
val filterList = filters.ifEmpty { getFilterList() }
filterList.filterIsInstance<UriFilter>().forEach {
it.addToUri(this)
}
}
return GET(urlBuilder.build(), headers)
}
private fun searchMangaNextPageSelector() = ".page-item.active + .page-item .page-link"
addQueryParameter("language[]", langCode)
addQueryParameter("page", page.toString())
}.build()
private fun searchMangaSelector() = ".original.card-lg .unit .inner"
private fun searchMangaFromElement(element: Element) = SManga.create().apply {
element.selectFirst(".info > a")!!.let {
setUrlWithoutDomain(it.attr("href"))
title = it.ownText()
}
element.selectFirst(Evaluator.Tag("img"))!!.let {
thumbnail_url = it.attr("src")
}
return GET(url, headers)
}
override fun searchMangaParse(response: Response): MangasPage {
@ -135,141 +112,148 @@ class MangaFire(
return MangasPage(entries, hasNextPage)
}
private fun searchMangaNextPageSelector() = ".page-item.active + .page-item .page-link"
private fun searchMangaSelector() = ".original.card-lg .unit .inner"
private fun searchMangaFromElement(element: Element) = SManga.create().apply {
element.selectFirst(".info > a")!!.let {
setUrlWithoutDomain(it.attr("href"))
title = it.ownText()
}
thumbnail_url = element.selectFirst("img")?.attr("abs:src")
}
// =============================== Filters ==============================
override fun getFilterList() = FilterList(
TypeFilter(),
GenreFilter(),
GenreModeFilter(),
StatusFilter(),
YearFilter(),
MinChapterFilter(),
SortFilter(),
)
// =========================== Manga Details ============================
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.removeSuffix(VOLUME_URL_SUFFIX)
override fun mangaDetailsParse(response: Response): SManga {
val document = response.asJsoup()
val manga = mangaDetailsParse(document)
if (response.request.url.fragment == VOLUME_URL_FRAGMENT) {
manga.title = VOLUME_TITLE_PREFIX + manga.title
return mangaDetailsParse(response.asJsoup()).apply {
if (response.request.url.fragment == VOLUME_URL_FRAGMENT) {
title = VOLUME_TITLE_PREFIX + title
}
}
return manga
}
private fun mangaDetailsParse(document: Document) = SManga.create().apply {
val root = document.selectFirst(".info")!!
val mangaTitle = root.child(1).ownText()
title = mangaTitle
description = document.run {
val description = selectFirst(Evaluator.Class("description"))!!.ownText()
when (val altTitle = root.child(2).ownText()) {
"", mangaTitle -> description
else -> "$description\n\nAlternative Title: $altTitle"
with(document.selectFirst(".main-inner:not(.manga-bottom)")!!) {
title = selectFirst("h1")!!.text()
thumbnail_url = selectFirst(".poster img")?.attr("src")
status = selectFirst(".info > p").parseStatus()
description = buildString {
document.selectFirst("#synopsis .modal-content")?.textNodes()?.let {
append(it.joinToString("\n\n"))
}
selectFirst("h6")?.let {
append("\n\nAlternative title: ${it.text()}")
}
}.trim()
selectFirst(".meta")?.let {
author = it.selectFirst("span:contains(Author:) + span")?.text()
val type = it.selectFirst("span:contains(Type:) + span")?.text()
val genres = it.selectFirst("span:contains(Genres:) + span")?.text()
genre = listOfNotNull(type, genres).joinToString()
}
}
thumbnail_url = document.selectFirst(".poster")!!.selectFirst("img")!!.attr("src")
status = when (root.child(0).ownText()) {
"Completed" -> SManga.COMPLETED
"Releasing" -> SManga.ONGOING
"On_hiatus" -> SManga.ON_HIATUS
"Discontinued" -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
with(document.selectFirst(Evaluator.Class("meta"))!!) {
author = selectFirst("span:contains(Author:) + span")?.text()
val type = selectFirst("span:contains(Type:) + span")?.text()
val genres = selectFirst("span:contains(Genres:) + span")?.text()
genre = listOfNotNull(type, genres).joinToString()
}
}
private val chapterType get() = "chapter"
private val volumeType get() = "volume"
private fun Element?.parseStatus(): Int = when (this?.text()?.lowercase()) {
"releasing" -> SManga.ONGOING
"completed" -> SManga.COMPLETED
"on_hiatus" -> SManga.ON_HIATUS
"discontinued" -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
// ============================== Chapters ==============================
override fun getChapterUrl(chapter: SChapter): String {
return baseUrl + chapter.url.substringBeforeLast("#")
}
private fun getAjaxRequest(ajaxType: String, mangaId: String, chapterType: String): Request {
return GET("$baseUrl/ajax/$ajaxType/$mangaId/$chapterType/$langCode", headers)
}
@Serializable
class AjaxReadDto(
val html: String,
)
override fun chapterListParse(response: Response): List<SChapter> {
throw UnsupportedOperationException()
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> =
Observable.fromCallable {
val path = manga.url
val isVolume = path.endsWith(VOLUME_URL_SUFFIX)
val type = if (isVolume) volumeType else chapterType
val request = chapterListRequest(path.removeSuffix(VOLUME_URL_SUFFIX), type)
val response = client.newCall(request).execute()
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val path = manga.url
val mangaId = path.removeSuffix(VOLUME_URL_SUFFIX).substringAfterLast(".")
val isVolume = path.endsWith(VOLUME_URL_SUFFIX)
val abbrPrefix = if (isVolume) "Vol" else "Chap"
val fullPrefix = if (isVolume) "Volume" else "Chapter"
val linkSelector = Evaluator.Tag("a")
parseChapterElements(response, isVolume).map { element ->
SChapter.create().apply {
val number = element.attr("data-number")
chapter_number = number.toFloatOrNull() ?: -1f
val type = if (isVolume) "volume" else "chapter"
val abbrPrefix = if (isVolume) "Vol" else "Chap"
val fullPrefix = if (isVolume) "Volume" else "Chapter"
val link = element.selectFirst(linkSelector)!!
name = run {
val name = link.text()
val prefix = "$abbrPrefix $number: "
if (!name.startsWith(prefix)) return@run name
val realName = name.removePrefix(prefix)
if (realName.contains(number)) realName else "$fullPrefix $number: $realName"
}
setUrlWithoutDomain(link.attr("href") + '#' + type + '/' + element.attr("data-id"))
val ajaxMangaList = client.newCall(getAjaxRequest("manga", mangaId, type))
.execute().parseAs<ResponseDto<String>>().result
.toBodyFragment()
.select(if (isVolume) ".vol-list > .item" else "li")
val ajaxReadList = client.newCall(getAjaxRequest("read", mangaId, type))
.execute().parseAs<ResponseDto<AjaxReadDto>>().result.html
.toBodyFragment()
.select("ul a")
val chapterList = ajaxMangaList.zip(ajaxReadList) { m, r ->
val link = r.selectFirst("a")!!
if (!r.attr("abs:href").toHttpUrl().pathSegments.last().contains(type)) {
return Observable.just(emptyList())
}
assert(m.attr("data-number") == r.attr("data-number")) {
"Chapter count doesn't match. Try updating again."
}
val number = m.attr("data-number")
val dateStr = m.select("span").getOrNull(1)?.text() ?: ""
SChapter.create().apply {
setUrlWithoutDomain("${link.attr("href")}#$type/${r.attr("data-id")}")
chapter_number = number.toFloatOrNull() ?: -1f
name = run {
val name = link.text()
val prefix = "$abbrPrefix $number: "
if (!name.startsWith(prefix)) return@run name
val realName = name.removePrefix(prefix)
if (realName.contains(number)) realName else "$fullPrefix $number: $realName"
}
}.also { if (!isVolume && it.isNotEmpty()) updateChapterList(manga, it) }
}
private fun chapterListRequest(mangaUrl: String, type: String): Request {
val id = mangaUrl.substringAfterLast('.')
return GET("$baseUrl/ajax/manga/$id/$type/$langCode", headers)
}
private fun parseChapterElements(response: Response, isVolume: Boolean): List<Element> {
val result = json.decodeFromString<ResponseDto<String>>(response.body.string()).result
val document = Jsoup.parse(result)
val selector = if (isVolume) "div.unit" else "ul li"
val elements = document.select(selector)
if (elements.size > 0) {
val linkToFirstChapter = elements[0].selectFirst(Evaluator.Tag("a"))!!.attr("href")
val mangaId = linkToFirstChapter.toString().substringAfter('.').substringBefore('/')
val type = if (isVolume) volumeType else chapterType
val request = GET("$baseUrl/ajax/read/$mangaId/$type/$langCode", headers)
val response = client.newCall(request).execute()
val res =
json.decodeFromString<ResponseDto<ChapterIdsDto>>(response.body.string()).result.html
val chapterInfoDocument = Jsoup.parse(res)
val chapters = chapterInfoDocument.select("ul li")
for ((i, it) in elements.withIndex()) {
it.attr("data-id", chapters[i].select("a").attr("data-id"))
date_upload = try {
dateFormat.parse(dateStr)!!.time
} catch (_: ParseException) {
0L
}
}
}
return elements.toList()
return Observable.just(chapterList)
}
@Serializable
class ChapterIdsDto(
val html: String,
val title_format: String,
)
private fun updateChapterList(manga: SManga, chapters: List<SChapter>) {
val request = chapterListRequest(manga.url, chapterType)
val response = client.newCall(request).execute()
val result = json.decodeFromString<ResponseDto<String>>(response.body.string()).result
val document = Jsoup.parse(result)
val elements = document.selectFirst(".scroll-sm")!!.children()
val chapterCount = chapters.size
if (elements.size != chapterCount) throw Exception("Chapter count doesn't match. Try updating again.")
val dateFormat = SimpleDateFormat("MMM dd, yyyy", Locale.US)
for (i in 0 until chapterCount) {
val chapter = chapters[i]
val element = elements[i]
val number = element.attr("data-number").toFloatOrNull() ?: -1f
if (chapter.chapter_number != number) throw Exception("Chapter number doesn't match. Try updating again.")
chapter.name = element.select(Evaluator.Tag("span"))[0].ownText()
val date = element.select(Evaluator.Tag("span"))[1].ownText()
chapter.date_upload = try {
dateFormat.parse(date)!!.time
} catch (_: Throwable) {
0
}
}
}
override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException()
}
// =============================== Pages ================================
override fun pageListRequest(chapter: SChapter): Request {
val typeAndId = chapter.url.substringAfterLast('#')
@ -277,7 +261,7 @@ class MangaFire(
}
override fun pageListParse(response: Response): List<Page> {
val result = json.decodeFromString<ResponseDto<PageListDto>>(response.body.string()).result
val result = response.parseAs<ResponseDto<PageListDto>>().result
return result.pages.mapIndexed { index, image ->
val url = image.url
@ -298,22 +282,11 @@ class MangaFire(
class Image(val url: String, val offset: Int)
@Serializable
class ResponseDto<T>(
val result: T,
val status: Int,
)
override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException()
}
override fun getFilterList() = FilterList(
Filter.Header("NOTE: Ignored if using text search!"),
Filter.Separator(),
TypeFilter(),
GenresFilter(),
StatusFilter(),
YearFilter(),
ChapterCountFilter(),
SortFilter(),
)
// ============================ Preferences =============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply {
@ -323,7 +296,23 @@ class MangaFire(
}.let(screen::addPreference)
}
// ============================= Utilities ==============================
@Serializable
class ResponseDto<T>(
val result: T,
)
private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(body.string())
}
private fun String.toBodyFragment(): Document {
return Jsoup.parseBodyFragment(this, baseUrl)
}
companion object {
private val dateFormat = SimpleDateFormat("MMM dd, yyyy", Locale.US)
private const val SHOW_VOLUME_PREF = "show_volume"
private const val VOLUME_URL_FRAGMENT = "vol"