Add Comix (#11658)
* Comix init * Add genre, author and artist parsing * add search and filters * hardcode genres and themes * Add the ability to retrieve chapter pages (#1) Co-authored-by: EgoMaw <dev@egomaw.net> * Add a pref for deduplicating chapters, cleanup code * fix api path * apparently sometimes the synopsis can be null, or an empty string * changes according to feedback * fix pagination, and dont call func inside map when not needed * Fix chapter list parsing ignoring first page and remove unused fields from DTOs * dont use custom Json instance --------- Co-authored-by: Hiirbaf <42479509+Hiirbaf@users.noreply.github.com>
This commit is contained in:
parent
1e768ace94
commit
7bf99f4bd0
8
src/en/comix/build.gradle
Normal file
8
src/en/comix/build.gradle
Normal file
@ -0,0 +1,8 @@
|
||||
ext {
|
||||
extName = 'Comix'
|
||||
extClass = '.Comix'
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
BIN
src/en/comix/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/en/comix/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
BIN
src/en/comix/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/en/comix/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src/en/comix/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/en/comix/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.9 KiB |
BIN
src/en/comix/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/en/comix/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
src/en/comix/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/en/comix/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
288
src/en/comix/src/eu/kanade/tachiyomi/extension/en/comix/Comix.kt
Normal file
288
src/en/comix/src/eu/kanade/tachiyomi/extension/en/comix/Comix.kt
Normal file
@ -0,0 +1,288 @@
|
||||
package eu.kanade.tachiyomi.extension.en.comix
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
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.HttpSource
|
||||
import keiyoushi.utils.getPreferences
|
||||
import keiyoushi.utils.parseAs
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
|
||||
class Comix : HttpSource(), ConfigurableSource {
|
||||
|
||||
override val name = "Comix"
|
||||
override val baseUrl = "https://comix.to"
|
||||
private val apiUrl = "https://comix.to/api/v2/"
|
||||
override val lang = "en"
|
||||
override val supportsLatest = true
|
||||
|
||||
private val preferences: SharedPreferences = getPreferences()
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.rateLimit(5)
|
||||
.build()
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder().add("Referer", "$baseUrl/")
|
||||
|
||||
override fun imageUrlParse(response: Response) =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
/******************************* POPULAR MANGA ************************************/
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val url = apiUrl.toHttpUrl().newBuilder()
|
||||
.addPathSegment("manga")
|
||||
.addQueryParameter("order[views_30d]", "desc")
|
||||
.addQueryParameter("limit", "50")
|
||||
.addQueryParameter("page", page.toString())
|
||||
.build()
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response) =
|
||||
searchMangaParse(response)
|
||||
|
||||
/******************************* LATEST MANGA ************************************/
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val url = apiUrl.toHttpUrl().newBuilder()
|
||||
.addPathSegment("manga")
|
||||
.addQueryParameter("order[chapter_updated_at]", "desc")
|
||||
.addQueryParameter("limit", "50")
|
||||
.addQueryParameter("page", page.toString())
|
||||
.build()
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response) =
|
||||
searchMangaParse(response)
|
||||
|
||||
/******************************* SEARCHING ***************************************/
|
||||
override fun getFilterList() = ComixFilters().getFilterList()
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = apiUrl.toHttpUrl().newBuilder()
|
||||
.addPathSegment("manga")
|
||||
|
||||
filters.filterIsInstance<ComixFilters.UriFilter>()
|
||||
.forEach { it.addToUri(url) }
|
||||
|
||||
// Make searches accurate
|
||||
if (query.isNotBlank()) {
|
||||
url.addQueryParameter("keyword", query)
|
||||
url.removeAllQueryParameters("order[views_30d]")
|
||||
url.setQueryParameter("order[relevance]", "desc")
|
||||
}
|
||||
|
||||
url.addQueryParameter("limit", "50")
|
||||
.addQueryParameter("page", page.toString())
|
||||
|
||||
return GET(url.build(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val res: SearchResponse = response.parseAs()
|
||||
val posterQuality = preferences.posterQuality()
|
||||
|
||||
val manga =
|
||||
res.result.items.map { manga -> manga.toBasicSManga(posterQuality) }
|
||||
return MangasPage(manga, res.result.pagination.page < res.result.pagination.lastPage)
|
||||
}
|
||||
|
||||
/******************************* MANGA DETAILS ***************************************/
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
val url = apiUrl.toHttpUrl().newBuilder()
|
||||
.addPathSegment("manga")
|
||||
.addPathSegment(manga.url)
|
||||
.addQueryParameter("includes[]", "demographic")
|
||||
.addQueryParameter("includes[]", "genre")
|
||||
.addQueryParameter("includes[]", "theme")
|
||||
.addQueryParameter("includes[]", "author")
|
||||
.addQueryParameter("includes[]", "artist")
|
||||
.addQueryParameter("includes[]", "publisher")
|
||||
.build()
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val mangaResponse: SingleMangaResponse = response.parseAs()
|
||||
|
||||
return mangaResponse.result.toSManga(
|
||||
preferences.posterQuality(),
|
||||
preferences.alternativeNamesInDescription(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun getMangaUrl(manga: SManga): String =
|
||||
"$baseUrl/title${manga.url}"
|
||||
|
||||
/******************************* Chapters List *******************************/
|
||||
override fun getChapterUrl(chapter: SChapter) =
|
||||
"$baseUrl/${chapter.url}"
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
return chapterListRequest(manga.url.removePrefix("/"), 1)
|
||||
}
|
||||
|
||||
private fun chapterListRequest(mangaHash: String, page: Int): Request {
|
||||
val url = apiUrl.toHttpUrl().newBuilder()
|
||||
.addPathSegment("manga")
|
||||
.addPathSegment(mangaHash)
|
||||
.addPathSegment("chapters")
|
||||
.addQueryParameter("order[number]", "desc")
|
||||
.addQueryParameter("limit", "100")
|
||||
.addQueryParameter("page", page.toString())
|
||||
.build()
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val deduplicate = preferences.deduplicateChapters()
|
||||
val mangaHash = response.request.url.pathSegments[3]
|
||||
var resp: ChapterDetailsResponse = response.parseAs()
|
||||
|
||||
// When deduplication is enabled store only the best chapter per number.
|
||||
var chapterMap: LinkedHashMap<Number, Chapter>? = null
|
||||
// When disabled just accumulate all.
|
||||
var chapterList: ArrayList<Chapter>? = null
|
||||
|
||||
if (deduplicate) {
|
||||
chapterMap = LinkedHashMap()
|
||||
deduplicateChapters(chapterMap, resp.result.items)
|
||||
} else {
|
||||
chapterList = ArrayList(resp.result.items)
|
||||
}
|
||||
|
||||
var page = 2
|
||||
var hasNext: Boolean
|
||||
|
||||
do {
|
||||
resp = client
|
||||
.newCall(chapterListRequest(mangaHash, page++))
|
||||
.execute()
|
||||
.parseAs()
|
||||
|
||||
val items = resp.result.items
|
||||
hasNext = resp.result.pagination.lastPage > resp.result.pagination.page
|
||||
|
||||
if (deduplicate) {
|
||||
deduplicateChapters(chapterMap!!, items)
|
||||
} else {
|
||||
chapterList!!.addAll(items)
|
||||
}
|
||||
} while (hasNext)
|
||||
|
||||
val finalChapters: List<Chapter> =
|
||||
if (deduplicate) {
|
||||
chapterMap!!.values.toList()
|
||||
} else {
|
||||
chapterList!!
|
||||
}
|
||||
|
||||
return finalChapters.map { it.toSChapter(mangaHash) }
|
||||
}
|
||||
|
||||
private fun deduplicateChapters(
|
||||
chapterMap: LinkedHashMap<Number, Chapter>,
|
||||
items: List<Chapter>,
|
||||
) {
|
||||
for (ch in items) {
|
||||
val key = ch.number
|
||||
val current = chapterMap[key]
|
||||
if (current == null) {
|
||||
chapterMap[key] = ch
|
||||
} else {
|
||||
// Prefer official scan group
|
||||
val officialNew = ch.scanlationGroupId == 9275
|
||||
val officialCurrent = current.scanlationGroupId == 9275
|
||||
val better = when {
|
||||
officialNew && !officialCurrent -> true
|
||||
!officialNew && officialCurrent -> false
|
||||
// compare votes then updatedAt
|
||||
else -> when {
|
||||
ch.votes > current.votes -> true
|
||||
ch.votes < current.votes -> false
|
||||
else -> ch.updatedAt > current.updatedAt
|
||||
}
|
||||
}
|
||||
if (better) chapterMap[key] = ch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/******************************* Page List (Reader) ************************************/
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val chapterId = chapter.url.substringAfterLast("/")
|
||||
val url = "${apiUrl}chapters/$chapterId"
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val res: ChapterResponse = response.parseAs()
|
||||
val result = res.result ?: throw Exception("Chapter not found")
|
||||
|
||||
if (result.images.isEmpty()) {
|
||||
throw Exception("No images found for chapter ${result.chapterId}")
|
||||
}
|
||||
|
||||
return result.images.mapIndexed { index, url ->
|
||||
Page(index, imageUrl = url)
|
||||
}
|
||||
}
|
||||
|
||||
/******************************* PREFERENCES ************************************/
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_POSTER_QUALITY
|
||||
title = "Thumbnail Quality"
|
||||
summary = "Change the quality of the thumbnail. Current: %s."
|
||||
entryValues = arrayOf("small", "medium", "large")
|
||||
entries = arrayOf("Small", "Medium", "Large")
|
||||
setDefaultValue("large")
|
||||
}.let(screen::addPreference)
|
||||
|
||||
SwitchPreferenceCompat(screen.context).apply {
|
||||
key = DEDUPLICATE_CHAPTERS
|
||||
title = "Deduplicate Chapters"
|
||||
summary = "Remove duplicate chapters from the chapter list.\n" +
|
||||
"Official chapters (Comix-marked) are preferred, followed by the highest-voted or most recent.\n" +
|
||||
"Warning: It can be slow on large lists."
|
||||
|
||||
setDefaultValue(false)
|
||||
}.let(screen::addPreference)
|
||||
|
||||
SwitchPreferenceCompat(screen.context).apply {
|
||||
key = ALTERNATIVE_NAMES_IN_DESCRIPTION
|
||||
title = "Show Alternative Names in Description"
|
||||
|
||||
setDefaultValue(false)
|
||||
}.let(screen::addPreference)
|
||||
}
|
||||
|
||||
private fun SharedPreferences.posterQuality() =
|
||||
getString(PREF_POSTER_QUALITY, "large")
|
||||
|
||||
private fun SharedPreferences.deduplicateChapters() =
|
||||
getBoolean(DEDUPLICATE_CHAPTERS, false)
|
||||
|
||||
private fun SharedPreferences.alternativeNamesInDescription() =
|
||||
getBoolean(ALTERNATIVE_NAMES_IN_DESCRIPTION, false)
|
||||
|
||||
companion object {
|
||||
private const val PREF_POSTER_QUALITY = "pref_poster_quality"
|
||||
private const val DEDUPLICATE_CHAPTERS = "pref_deduplicate_chapters"
|
||||
private const val ALTERNATIVE_NAMES_IN_DESCRIPTION = "pref_alt_names_in_description"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,177 @@
|
||||
package eu.kanade.tachiyomi.extension.en.comix
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Term(
|
||||
@SerialName("term_id")
|
||||
private val termId: Int,
|
||||
private val type: String,
|
||||
val title: String,
|
||||
private val slug: String,
|
||||
private val count: Int?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class Manga(
|
||||
@SerialName("hash_id")
|
||||
private val hashId: String,
|
||||
private val title: String,
|
||||
@SerialName("alt_titles")
|
||||
private val altTitles: List<String>,
|
||||
private val synopsis: String?,
|
||||
private val type: String,
|
||||
private val poster: Poster,
|
||||
private val status: String,
|
||||
@SerialName("is_nsfw")
|
||||
private val isNsfw: Boolean,
|
||||
private val author: List<Term>?,
|
||||
private val artist: List<Term>?,
|
||||
private val genre: List<Term>?,
|
||||
private val theme: List<Term>?,
|
||||
private val demographic: List<Term>?,
|
||||
) {
|
||||
@Serializable
|
||||
class Poster(
|
||||
private val small: String,
|
||||
private val medium: String,
|
||||
private val large: String,
|
||||
) {
|
||||
fun from(quality: String?) = when (quality) {
|
||||
"large" -> large
|
||||
"small" -> small
|
||||
else -> medium
|
||||
}
|
||||
}
|
||||
|
||||
fun toSManga(
|
||||
posterQuality: String?,
|
||||
altTitlesInDesc: Boolean = false,
|
||||
) = SManga.create().apply {
|
||||
url = "/$hashId"
|
||||
title = this@Manga.title
|
||||
author = this@Manga.author.takeUnless { it.isNullOrEmpty() }?.joinToString { it.title }
|
||||
artist = this@Manga.artist.takeUnless { it.isNullOrEmpty() }?.joinToString { it.title }
|
||||
description = buildString {
|
||||
synopsis.takeUnless { it.isNullOrEmpty() }
|
||||
?.let { append(it) }
|
||||
altTitles.takeIf { altTitlesInDesc && it.isNotEmpty() }
|
||||
?.let { altName ->
|
||||
append("\n\n")
|
||||
append("Alternative Names:\n")
|
||||
append(altName.joinToString("\n"))
|
||||
}
|
||||
}
|
||||
initialized = true
|
||||
status = when (this@Manga.status) {
|
||||
"releasing" -> SManga.ONGOING
|
||||
"on_hiatus" -> SManga.ON_HIATUS
|
||||
"finished" -> SManga.COMPLETED
|
||||
"discontinued" -> SManga.CANCELLED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
thumbnail_url = this@Manga.poster.from(posterQuality)
|
||||
genre = getGenres()
|
||||
}
|
||||
|
||||
fun toBasicSManga(posterQuality: String?) = SManga.create().apply {
|
||||
url = "/$hashId"
|
||||
title = this@Manga.title
|
||||
thumbnail_url = this@Manga.poster.from(posterQuality)
|
||||
}
|
||||
|
||||
fun getGenres() = buildList {
|
||||
when (type) {
|
||||
"manhwa" -> add("Manhwa")
|
||||
"manhua" -> add("Manhua")
|
||||
"manga" -> add("Manga")
|
||||
else -> add("Other")
|
||||
}
|
||||
genre.takeUnless { it.isNullOrEmpty() }?.map { it.title }
|
||||
.let { addAll(it ?: emptyList()) }
|
||||
theme.takeUnless { it.isNullOrEmpty() }?.map { it.title }
|
||||
.let { addAll(it ?: emptyList()) }
|
||||
demographic.takeUnless { it.isNullOrEmpty() }?.map { it.title }
|
||||
.let { addAll(it ?: emptyList()) }
|
||||
if (isNsfw) add("NSFW")
|
||||
}.distinct().joinToString()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class SingleMangaResponse(
|
||||
val result: Manga,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class Pagination(
|
||||
@SerialName("current_page") val page: Int,
|
||||
@SerialName("last_page") val lastPage: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class SearchResponse(
|
||||
val result: Items,
|
||||
) {
|
||||
@Serializable
|
||||
class Items(
|
||||
val items: List<Manga>,
|
||||
val pagination: Pagination,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ChapterDetailsResponse(
|
||||
val result: Items,
|
||||
) {
|
||||
@Serializable
|
||||
class Items(
|
||||
val items: List<Chapter>,
|
||||
val pagination: Pagination,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class Chapter(
|
||||
@SerialName("chapter_id")
|
||||
private val chapterId: Int,
|
||||
@SerialName("scanlation_group_id") val scanlationGroupId: Int,
|
||||
val number: Double,
|
||||
private val name: String,
|
||||
val votes: Int,
|
||||
@SerialName("updated_at")
|
||||
val updatedAt: Long,
|
||||
@SerialName("scanlation_group")
|
||||
private val scanlationGroup: ScanlationGroup?,
|
||||
) {
|
||||
@Serializable
|
||||
class ScanlationGroup(
|
||||
val name: String,
|
||||
)
|
||||
|
||||
fun toSChapter(mangaId: String) = SChapter.create().apply {
|
||||
url = "title/$mangaId/$chapterId"
|
||||
name = buildString {
|
||||
append("Chapter ")
|
||||
append(this@Chapter.number.toString().removeSuffix(".0"))
|
||||
this@Chapter.name.takeUnless { it.isEmpty() }?.let { append(": $it") }
|
||||
}
|
||||
date_upload = this@Chapter.updatedAt * 1000
|
||||
chapter_number = this@Chapter.number.toFloat()
|
||||
scanlator = this@Chapter.scanlationGroup?.name ?: "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ChapterResponse(
|
||||
val result: Items?,
|
||||
) {
|
||||
@Serializable
|
||||
class Items(
|
||||
@SerialName("chapter_id")
|
||||
val chapterId: Int,
|
||||
val images: List<String>,
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,265 @@
|
||||
package eu.kanade.tachiyomi.extension.en.comix
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import okhttp3.HttpUrl
|
||||
import java.util.Calendar
|
||||
|
||||
class ComixFilters {
|
||||
interface UriFilter {
|
||||
fun addToUri(builder: HttpUrl.Builder)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val currentYear by lazy {
|
||||
Calendar.getInstance()[Calendar.YEAR]
|
||||
}
|
||||
|
||||
private fun getYearsArray(includeOlder: Boolean): Array<Pair<String, String>> {
|
||||
val years = (currentYear downTo 1990).map { it.toString() to it.toString() }
|
||||
return if (includeOlder) {
|
||||
(years + ("Older" to "older")).toTypedArray()
|
||||
} else {
|
||||
years.toTypedArray()
|
||||
}
|
||||
}
|
||||
|
||||
fun getGenres() = arrayOf(
|
||||
Pair("Action", "6"),
|
||||
Pair("Adult", "87264"),
|
||||
Pair("Adventure", "7"),
|
||||
Pair("Boys Love", "8"),
|
||||
Pair("Comedy", "9"),
|
||||
Pair("Crime", "10"),
|
||||
Pair("Drama", "11"),
|
||||
Pair("Ecchi", "87265"),
|
||||
Pair("Fantasy", "12"),
|
||||
Pair("Girls Love", "13"),
|
||||
Pair("Hentai", "87266"),
|
||||
Pair("Historical", "14"),
|
||||
Pair("Horror", "15"),
|
||||
Pair("Isekai", "16"),
|
||||
Pair("Magical Girls", "17"),
|
||||
Pair("Mature", "87267"),
|
||||
Pair("Mecha", "18"),
|
||||
Pair("Medical", "19"),
|
||||
Pair("Mystery", "20"),
|
||||
Pair("Philosophical", "21"),
|
||||
Pair("Psychological", "22"),
|
||||
Pair("Romance", "23"),
|
||||
Pair("Sci-Fi", "24"),
|
||||
Pair("Slice of Life", "25"),
|
||||
Pair("Smut", "87268"),
|
||||
Pair("Sports", "26"),
|
||||
Pair("Superhero", "27"),
|
||||
Pair("Thriller", "28"),
|
||||
Pair("Tragedy", "29"),
|
||||
Pair("Wuxia", "30"),
|
||||
Pair("Aliens", "31"),
|
||||
Pair("Animals", "32"),
|
||||
Pair("Cooking", "33"),
|
||||
Pair("Cross Dressing", "34"),
|
||||
Pair("Delinquents", "35"),
|
||||
Pair("Demons", "36"),
|
||||
Pair("Genderswap", "37"),
|
||||
Pair("Ghosts", "38"),
|
||||
Pair("Gyaru", "39"),
|
||||
Pair("Harem", "40"),
|
||||
Pair("Incest", "41"),
|
||||
Pair("Loli", "42"),
|
||||
Pair("Mafia", "43"),
|
||||
Pair("Magic", "44"),
|
||||
Pair("Martial Arts", "45"),
|
||||
Pair("Military", "46"),
|
||||
Pair("Monster Girls", "47"),
|
||||
Pair("Monsters", "48"),
|
||||
Pair("Music", "49"),
|
||||
Pair("Ninja", "50"),
|
||||
Pair("Office Workers", "51"),
|
||||
Pair("Police", "52"),
|
||||
Pair("Post-Apocalyptic", "53"),
|
||||
Pair("Reincarnation", "54"),
|
||||
Pair("Reverse Harem", "55"),
|
||||
Pair("Samurai", "56"),
|
||||
Pair("School Life", "57"),
|
||||
Pair("Shota", "58"),
|
||||
Pair("Supernatural", "59"),
|
||||
Pair("Survival", "60"),
|
||||
Pair("Time Travel", "61"),
|
||||
Pair("Traditional Games", "62"),
|
||||
Pair("Vampires", "63"),
|
||||
Pair("Video Games", "64"),
|
||||
Pair("Villainess", "65"),
|
||||
Pair("Virtual Reality", "66"),
|
||||
Pair("Zombies", "67"),
|
||||
)
|
||||
|
||||
fun getDemographics() = arrayOf(
|
||||
Pair("Shoujo", "1"),
|
||||
Pair("Shounen", "2"),
|
||||
Pair("Josei", "3"),
|
||||
Pair("Seinen", "4"),
|
||||
)
|
||||
}
|
||||
|
||||
fun getFilterList() = FilterList(
|
||||
SortFilter(getSortables()),
|
||||
StatusFilter(),
|
||||
MinChapterFilter(),
|
||||
GenreFilter(getGenres()),
|
||||
TypeFilter(),
|
||||
DemographicFilter(getDemographics()),
|
||||
Filter.Separator(),
|
||||
Filter.Header("Release Year"),
|
||||
YearFromFilter(),
|
||||
YearToFilter(),
|
||||
)
|
||||
|
||||
private open class UriPartFilter(
|
||||
name: String,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
private open class UriMultiSelectOption(name: String, val value: String) : Filter.CheckBox(name)
|
||||
|
||||
private open class UriMultiSelectFilter(
|
||||
name: String,
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private open class UriTriSelectOption(name: String, val value: String) : Filter.TriState(name)
|
||||
|
||||
private 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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DemographicFilter(val demographics: Array<Pair<String, String>>) :
|
||||
UriTriSelectFilter(
|
||||
"Demographic",
|
||||
"demographics[]",
|
||||
demographics,
|
||||
)
|
||||
|
||||
private class TypeFilter : UriMultiSelectFilter(
|
||||
"Type",
|
||||
"type",
|
||||
arrayOf(
|
||||
Pair("Manga", "manga"),
|
||||
Pair("Manhwa", "manhwa"),
|
||||
Pair("Manhua", "manhua"),
|
||||
Pair("Other", "other"),
|
||||
),
|
||||
)
|
||||
|
||||
private class GenreFilter(genres: Array<Pair<String, String>>) : UriTriSelectFilter(
|
||||
"Genres",
|
||||
"genres[]",
|
||||
genres,
|
||||
)
|
||||
|
||||
private class StatusFilter : UriMultiSelectFilter(
|
||||
"Status",
|
||||
"statuses[]",
|
||||
arrayOf(
|
||||
Pair("Finished", "finished"),
|
||||
Pair("Releasing", "releasing"),
|
||||
Pair("On Hiatus", "on_hiatus"),
|
||||
Pair("Discontinued", "discontinued"),
|
||||
Pair("Not Yet Released", "not_yet_released"),
|
||||
),
|
||||
)
|
||||
|
||||
private class YearFromFilter : UriPartFilter(
|
||||
"From",
|
||||
"release_year[from]",
|
||||
getYearsArray(includeOlder = true),
|
||||
"older",
|
||||
)
|
||||
|
||||
private class YearToFilter : UriPartFilter(
|
||||
"To",
|
||||
"release_year[to]",
|
||||
getYearsArray(includeOlder = false),
|
||||
)
|
||||
|
||||
private 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",
|
||||
)
|
||||
builder.addQueryParameter("min_chap", value.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class Sortable(val title: String, val value: String) {
|
||||
override fun toString(): String = title
|
||||
}
|
||||
|
||||
private fun getSortables() = arrayOf(
|
||||
Sortable("Best Match", "relevance"),
|
||||
Sortable("Popular", "views_30d"),
|
||||
Sortable("Updated Date", "chapter_updated_at"),
|
||||
Sortable("Created Date", "created_at"),
|
||||
Sortable("Title", "title"),
|
||||
Sortable("Year", "year"),
|
||||
Sortable("Total Views", "total_views"),
|
||||
Sortable("Most Follows", "followed_count"),
|
||||
)
|
||||
|
||||
private class SortFilter(private val sortables: Array<Sortable>) :
|
||||
Filter.Sort(
|
||||
"Sort By",
|
||||
sortables.map(Sortable::title).toTypedArray(),
|
||||
Selection(1, false),
|
||||
),
|
||||
UriFilter {
|
||||
override fun addToUri(builder: HttpUrl.Builder) {
|
||||
if (state != null) {
|
||||
val query = sortables[state!!.index].value
|
||||
val value = if (state!!.ascending) "asc" else "desc"
|
||||
builder.addQueryParameter("order[$query]", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user