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