Comick (unoriginal) (#11074)
* Comick (Unoriginal) * icon * factory languages * no need for custom disk cache * unused
This commit is contained in:
parent
cbaf26bf4d
commit
89c380c808
12
src/all/comicklive/build.gradle
Normal file
12
src/all/comicklive/build.gradle
Normal file
@ -0,0 +1,12 @@
|
||||
ext {
|
||||
extName = 'Comick (Unoriginal)'
|
||||
extClass = '.ComickFactory'
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
compileOnly("com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.11")
|
||||
}
|
||||
BIN
src/all/comicklive/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/all/comicklive/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
BIN
src/all/comicklive/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/all/comicklive/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src/all/comicklive/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/all/comicklive/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
BIN
src/all/comicklive/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/all/comicklive/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
src/all/comicklive/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/all/comicklive/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@ -0,0 +1,382 @@
|
||||
package eu.kanade.tachiyomi.extension.all.comicklive
|
||||
|
||||
import android.util.Log
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
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
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import keiyoushi.utils.firstInstance
|
||||
import keiyoushi.utils.firstInstanceOrNull
|
||||
import keiyoushi.utils.getPreferences
|
||||
import keiyoushi.utils.parseAs
|
||||
import keiyoushi.utils.tryParse
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.brotli.BrotliInterceptor
|
||||
import okhttp3.internal.closeQuietly
|
||||
import org.jsoup.Jsoup
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class Comick(
|
||||
override val lang: String,
|
||||
private val siteLang: String = lang,
|
||||
) : HttpSource(), ConfigurableSource {
|
||||
|
||||
override val name = "Comick (Unoriginal)"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val preferences = getPreferences()
|
||||
|
||||
override val baseUrl: String
|
||||
get() {
|
||||
val index = preferences.getString(DOMAIN_PREF, "0")!!.toInt()
|
||||
.coerceAtMost(domains.size - 1)
|
||||
|
||||
return domains[index]
|
||||
}
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
// Referer in interceptor due to domain change preference
|
||||
.addNetworkInterceptor { chain ->
|
||||
val request = chain.request().newBuilder()
|
||||
.header("Referer", "$baseUrl/")
|
||||
.build()
|
||||
|
||||
chain.proceed(request)
|
||||
}
|
||||
// fix disk cache
|
||||
.apply {
|
||||
val index = networkInterceptors().indexOfFirst { it is BrotliInterceptor }
|
||||
if (index >= 0) interceptors().add(networkInterceptors().removeAt(index))
|
||||
}
|
||||
.build()
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val url = "$baseUrl/api/comics/top".toHttpUrl().newBuilder().apply {
|
||||
val days = when (page) {
|
||||
1, 4 -> 7
|
||||
2, 5 -> 30
|
||||
3, 6 -> 90
|
||||
else -> throw UnsupportedOperationException()
|
||||
}
|
||||
val type = when (page) {
|
||||
1, 2, 3 -> "follow"
|
||||
4, 5, 6 -> "most_follow_new"
|
||||
else -> throw UnsupportedOperationException()
|
||||
}
|
||||
addQueryParameter("days", days.toString())
|
||||
addQueryParameter("type", type)
|
||||
fragment(page.toString())
|
||||
}.build()
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val data = response.parseAs<Data<List<BrowseComic>>>()
|
||||
val page = response.request.url.fragment!!.toInt()
|
||||
|
||||
return MangasPage(
|
||||
mangas = data.data.map(BrowseComic::toSManga),
|
||||
hasNextPage = page < 6,
|
||||
)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) =
|
||||
GET("$baseUrl/api/chapters/latest?order=new&page=$page", headers)
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val data = response.parseAs<Data<List<BrowseComic>>>()
|
||||
|
||||
return MangasPage(
|
||||
mangas = data.data.map(BrowseComic::toSManga),
|
||||
hasNextPage = data.data.size == 100,
|
||||
)
|
||||
}
|
||||
|
||||
private var nextCursor: String? = null
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
if (page == 1) {
|
||||
nextCursor = null
|
||||
}
|
||||
|
||||
val url = "$baseUrl/api/search".toHttpUrl().newBuilder().apply {
|
||||
addQueryParameter("order_by", filters.firstInstance<SortFilter>().selected)
|
||||
addQueryParameter("order_direction", "desc")
|
||||
filters.firstInstanceOrNull<GenreFilter>()?.let { genre ->
|
||||
genre.included.forEach {
|
||||
addQueryParameter("genres[]", it)
|
||||
}
|
||||
genre.excluded.forEach {
|
||||
addQueryParameter("excludes[]", it)
|
||||
}
|
||||
}
|
||||
filters.firstInstanceOrNull<TagFilter>()?.let { tag ->
|
||||
tag.included.forEach {
|
||||
addQueryParameter("tags[]", it)
|
||||
}
|
||||
tag.excluded.forEach {
|
||||
addQueryParameter("excluded_tags[]", it)
|
||||
}
|
||||
}
|
||||
filters.firstInstance<DemographicFilter>().checked.forEach {
|
||||
addQueryParameter("demographic[]", it)
|
||||
}
|
||||
filters.firstInstance<CreatedAtFilter>().selected?.let {
|
||||
addQueryParameter("time", it)
|
||||
}
|
||||
filters.firstInstance<TypeFilter>().checked.forEach {
|
||||
addQueryParameter("country[]", it)
|
||||
}
|
||||
filters.firstInstance<MinimumChaptersFilter>().state.let {
|
||||
if (it.isNotBlank()) {
|
||||
if (it.toIntOrNull() == null) {
|
||||
throw Exception("Invalid minimum chapters value: $it")
|
||||
}
|
||||
addQueryParameter("minimum", it)
|
||||
}
|
||||
}
|
||||
filters.firstInstance<StatusFilter>().selected?.let {
|
||||
addQueryParameter("status", it)
|
||||
}
|
||||
filters.firstInstance<ReleaseFrom>().selected?.let {
|
||||
addQueryParameter("from", it)
|
||||
}
|
||||
filters.firstInstance<ReleaseTo>().selected?.let {
|
||||
addQueryParameter("to", it)
|
||||
}
|
||||
filters.firstInstance<ContentRatingFilter>().selected?.let {
|
||||
addQueryParameter("content_rating", it)
|
||||
}
|
||||
addQueryParameter("showAll", "false")
|
||||
addQueryParameter("exclude_mylist", "false")
|
||||
if (query.isNotBlank()) {
|
||||
if (query.trim().length < 3) {
|
||||
throw Exception("Query must be at least 3 characters")
|
||||
}
|
||||
addQueryParameter("q", query.trim())
|
||||
}
|
||||
addQueryParameter("type", "comic")
|
||||
if (page > 1) {
|
||||
addQueryParameter("cursor", nextCursor)
|
||||
}
|
||||
}.build()
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val data = response.parseAs<SearchResponse>()
|
||||
|
||||
nextCursor = data.cursor
|
||||
|
||||
return MangasPage(
|
||||
mangas = data.data.map(BrowseComic::toSManga),
|
||||
hasNextPage = data.cursor != null,
|
||||
)
|
||||
}
|
||||
|
||||
private val metadataClient = client.newBuilder()
|
||||
.addNetworkInterceptor { chain ->
|
||||
chain.proceed(chain.request()).newBuilder()
|
||||
.header("Cache-Control", "max-age=${24 * 60 * 60}")
|
||||
.removeHeader("Pragma")
|
||||
.removeHeader("Expires")
|
||||
.build()
|
||||
}.build()
|
||||
|
||||
override fun getFilterList(): FilterList = runBlocking(Dispatchers.IO) {
|
||||
val filters: MutableList<Filter<*>> = mutableListOf(
|
||||
SortFilter(),
|
||||
DemographicFilter(),
|
||||
CreatedAtFilter(),
|
||||
TypeFilter(),
|
||||
MinimumChaptersFilter(),
|
||||
StatusFilter(),
|
||||
ContentRatingFilter(),
|
||||
ReleaseFrom(),
|
||||
ReleaseTo(),
|
||||
)
|
||||
|
||||
val response = metadataClient.newCall(
|
||||
GET("$baseUrl/api/metadata", headers, CacheControl.FORCE_CACHE),
|
||||
).await()
|
||||
|
||||
// the cache only request fails if it was not cached already
|
||||
if (!response.isSuccessful) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
metadataClient.newCall(
|
||||
GET("$baseUrl/api/metadata", headers, CacheControl.FORCE_NETWORK),
|
||||
).await().closeQuietly()
|
||||
}
|
||||
|
||||
filters.addAll(
|
||||
index = 0,
|
||||
listOf(
|
||||
Filter.Header("Press 'reset' to load genres and tags"),
|
||||
Filter.Separator(),
|
||||
),
|
||||
)
|
||||
return@runBlocking FilterList(filters)
|
||||
}
|
||||
|
||||
val data = try {
|
||||
response.parseAs<Metadata>()
|
||||
} catch (e: Throwable) {
|
||||
Log.e(name, "Unable to parse filters", e)
|
||||
|
||||
filters.addAll(
|
||||
index = 0,
|
||||
listOf(
|
||||
Filter.Header("Failed to parse genres and tags"),
|
||||
Filter.Separator(),
|
||||
),
|
||||
)
|
||||
return@runBlocking FilterList(filters)
|
||||
}
|
||||
|
||||
filters.addAll(
|
||||
index = 1,
|
||||
listOf(
|
||||
GenreFilter(data.genres),
|
||||
TagFilter(data.tags),
|
||||
),
|
||||
)
|
||||
return@runBlocking FilterList(filters)
|
||||
}
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga) =
|
||||
GET("$baseUrl/comic/${manga.url}", headers)
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val data = response.asJsoup()
|
||||
.selectFirst("#comic-data")!!.data()
|
||||
.parseAs<ComicData>()
|
||||
|
||||
return SManga.create().apply {
|
||||
title = data.title
|
||||
url = data.slug
|
||||
thumbnail_url = data.thumbnail
|
||||
status = when (data.status) {
|
||||
1 -> SManga.ONGOING
|
||||
2 -> if (data.translationCompleted) SManga.COMPLETED else SManga.PUBLISHING_FINISHED
|
||||
3 -> SManga.CANCELLED
|
||||
4 -> SManga.ON_HIATUS
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
author = data.authors.joinToString { it.name }
|
||||
artist = data.artists.joinToString { it.name }
|
||||
description = buildString {
|
||||
append(
|
||||
Jsoup.parseBodyFragment(data.desc).wholeText(),
|
||||
)
|
||||
|
||||
if (data.titles.isNotEmpty()) {
|
||||
append("\n\n Alternative Titles: \n")
|
||||
data.titles.forEach {
|
||||
append(it.title, "\n")
|
||||
}
|
||||
}
|
||||
}.trim()
|
||||
genre = buildList {
|
||||
when (data.country) {
|
||||
"jp" -> add("Manga")
|
||||
"cn" -> add("Manhua")
|
||||
"ko" -> add("Manhwa")
|
||||
}
|
||||
when (data.contentRating) {
|
||||
"suggestive" -> add("Content Rating: Suggestive")
|
||||
"erotica" -> add("Content Rating: Erotica")
|
||||
}
|
||||
addAll(data.genres.map { it.genres.name })
|
||||
}.joinToString()
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga) =
|
||||
GET("$baseUrl/api/comics/${manga.url}/chapter-list?lang=$siteLang", headers)
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
var data = response.parseAs<ChapterList>()
|
||||
var page = 2
|
||||
val chapters = data.data.toMutableList()
|
||||
|
||||
while (data.hasNextPage()) {
|
||||
val url = response.request.url.newBuilder()
|
||||
.addQueryParameter("page", page.toString())
|
||||
.build()
|
||||
|
||||
data = client.newCall(GET(url, headers)).execute()
|
||||
.parseAs()
|
||||
chapters += data.data
|
||||
page++
|
||||
}
|
||||
|
||||
val mangaSlug = response.request.url.pathSegments[2]
|
||||
|
||||
return chapters.map {
|
||||
SChapter.create().apply {
|
||||
url = "/comic/$mangaSlug/${it.hid}-chapter-${it.chap}-${it.lang}"
|
||||
name = buildString {
|
||||
if (!it.vol.isNullOrBlank()) {
|
||||
append("Vol. ", it.vol, " ")
|
||||
}
|
||||
append("Ch. ", it.chap)
|
||||
if (!it.title.isNullOrBlank()) {
|
||||
append(": ", it.title)
|
||||
}
|
||||
}
|
||||
date_upload = dateFormat.tryParse(it.createdAt)
|
||||
scanlator = it.groups.joinToString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", Locale.ENGLISH)
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val data = response.asJsoup()
|
||||
.selectFirst("#sv-data")!!.data()
|
||||
.parseAs<PageListData>()
|
||||
|
||||
return data.chapter.images.mapIndexed { index, image ->
|
||||
Page(index, imageUrl = image.url)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = DOMAIN_PREF
|
||||
title = "Preferred Domain"
|
||||
entries = domains
|
||||
entryValues = Array(domains.size) { it.toString() }
|
||||
summary = "%s"
|
||||
setDefaultValue("0")
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
}
|
||||
|
||||
private val domains = arrayOf("https://comick.live", "https://comick.art")
|
||||
private const val DOMAIN_PREF = "domain_pref"
|
||||
@ -0,0 +1,74 @@
|
||||
package eu.kanade.tachiyomi.extension.all.comicklive
|
||||
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
|
||||
class ComickFactory : SourceFactory {
|
||||
// as of 2025-10-15, the commented languages have 0 chapters uploaded
|
||||
// from: /api/languages
|
||||
override fun createSources() = listOf(
|
||||
Comick("en"),
|
||||
// Comick("pt-br", "pt-BR"),
|
||||
// Comick("es-419", "es-la"),
|
||||
Comick("ru"),
|
||||
Comick("vi"),
|
||||
Comick("fr"),
|
||||
Comick("pl"),
|
||||
Comick("id"),
|
||||
Comick("tr"),
|
||||
Comick("it"),
|
||||
Comick("es"),
|
||||
Comick("uk"),
|
||||
// Comick("ar"),
|
||||
// Comick("zh-hk", "zh-Hant"),
|
||||
// Comick("hu"),
|
||||
// Comick("zh", "zh-Hans"),
|
||||
Comick("de"),
|
||||
Comick("ko"),
|
||||
Comick("th"),
|
||||
// Comick("ca"),
|
||||
// Comick("bg"),
|
||||
// Comick("fa"),
|
||||
Comick("ro"),
|
||||
// Comick("cs"),
|
||||
// Comick("mn"),
|
||||
// Comick("pt"),
|
||||
// Comick("he"),
|
||||
// Comick("hi"),
|
||||
// Comick("tl"),
|
||||
Comick("ms"),
|
||||
// Comick("fi"),
|
||||
// Comick("eu"),
|
||||
// Comick("kk"),
|
||||
// Comick("sr"),
|
||||
// Comick("my"),
|
||||
Comick("ja"),
|
||||
// Comick("el"),
|
||||
// Comick("nl"),
|
||||
// Comick("bn"),
|
||||
// Comick("uz"),
|
||||
// Comick("eo"),
|
||||
// Comick("ka"),
|
||||
// Comick("lt"),
|
||||
// Comick("da"),
|
||||
// Comick("ta"),
|
||||
Comick("sv"),
|
||||
// Comick("be"),
|
||||
// Comick("gl"),
|
||||
// Comick("cv"),
|
||||
// Comick("hr"),
|
||||
// Comick("la"),
|
||||
// Comick("ur"),
|
||||
// Comick("ne"),
|
||||
Comick("no"),
|
||||
// Comick("sq"),
|
||||
// Comick("ga"),
|
||||
// Comick("jv"),
|
||||
// Comick("te"),
|
||||
// Comick("sl"),
|
||||
// Comick("et"),
|
||||
// Comick("az"),
|
||||
// Comick("sk"),
|
||||
// Comick("af"),
|
||||
// Comick("lv"),
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,124 @@
|
||||
package eu.kanade.tachiyomi.extension.all.comicklive
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class Data<T>(
|
||||
val data: T,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class SearchResponse(
|
||||
val data: List<BrowseComic>,
|
||||
@SerialName("next_cursor")
|
||||
val cursor: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class BrowseComic(
|
||||
@SerialName("default_thumbnail")
|
||||
private val thumbnail: String,
|
||||
private val slug: String,
|
||||
private val title: String,
|
||||
) {
|
||||
fun toSManga() = SManga.create().apply {
|
||||
url = slug
|
||||
title = this@BrowseComic.title
|
||||
thumbnail_url = thumbnail
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class Metadata(
|
||||
val genres: List<Name>,
|
||||
val tags: List<Name>,
|
||||
) {
|
||||
@Serializable
|
||||
class Name(
|
||||
val name: String,
|
||||
val slug: String,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ComicData(
|
||||
val title: String,
|
||||
val slug: String,
|
||||
@SerialName("default_thumbnail")
|
||||
val thumbnail: String,
|
||||
val status: Int,
|
||||
@SerialName("translation_completed")
|
||||
val translationCompleted: Boolean,
|
||||
val artists: List<Name>,
|
||||
val authors: List<Name>,
|
||||
val desc: String,
|
||||
@SerialName("content_rating")
|
||||
val contentRating: String,
|
||||
val country: String,
|
||||
@SerialName("md_comic_md_genres")
|
||||
val genres: List<Genres>,
|
||||
@SerialName("md_titles")
|
||||
val titles: List<Title>,
|
||||
) {
|
||||
@Serializable
|
||||
class Name(
|
||||
val name: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class Title(
|
||||
val title: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class Genres(
|
||||
@SerialName("md_genres")
|
||||
val genres: Name,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ChapterList(
|
||||
val data: List<Chapter>,
|
||||
private val pagination: Pagination,
|
||||
) {
|
||||
fun hasNextPage() = pagination.page < pagination.lastPage
|
||||
|
||||
@Serializable
|
||||
class Chapter(
|
||||
val hid: String,
|
||||
val chap: String,
|
||||
val vol: String?,
|
||||
val lang: String,
|
||||
val title: String?,
|
||||
@SerialName("created_at")
|
||||
val createdAt: String,
|
||||
@SerialName("group_name")
|
||||
val groups: List<String>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class Pagination(
|
||||
@SerialName("current_page")
|
||||
val page: Int,
|
||||
@SerialName("last_page")
|
||||
val lastPage: Int,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class PageListData(
|
||||
val chapter: ChapterData,
|
||||
) {
|
||||
@Serializable
|
||||
class ChapterData(
|
||||
val images: List<Image>,
|
||||
) {
|
||||
@Serializable
|
||||
class Image(
|
||||
val url: String,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,141 @@
|
||||
package eu.kanade.tachiyomi.extension.all.comicklive
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import java.util.Calendar
|
||||
|
||||
abstract class SelectFilter(
|
||||
name: String,
|
||||
private val options: List<Pair<String, String>>,
|
||||
) : Filter.Select<String>(
|
||||
name,
|
||||
options.map { it.first }.toTypedArray(),
|
||||
) {
|
||||
val selected get() = options[state].second.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
class CheckBoxFilter(name: String, val value: String) : Filter.CheckBox(name)
|
||||
|
||||
abstract class CheckBoxGroup(
|
||||
name: String,
|
||||
options: List<Pair<String, String>>,
|
||||
) : Filter.Group<CheckBoxFilter>(
|
||||
name,
|
||||
options.map { CheckBoxFilter(it.first, it.second) },
|
||||
) {
|
||||
val checked get() = state.filter { it.state }.map { it.value }
|
||||
}
|
||||
|
||||
class TriStateFilter(name: String, val slug: String) : Filter.TriState(name)
|
||||
|
||||
abstract class TriStateGroupFilter(
|
||||
name: String,
|
||||
options: List<Pair<String, String>>,
|
||||
) : Filter.Group<TriStateFilter>(
|
||||
name,
|
||||
options.map { TriStateFilter(it.first, it.second) },
|
||||
) {
|
||||
val included get() = state.filter { it.isIncluded() }.map { it.slug }
|
||||
val excluded get() = state.filter { it.isExcluded() }.map { it.slug }
|
||||
}
|
||||
|
||||
class SortFilter : SelectFilter(
|
||||
name = "Sort",
|
||||
options = listOf(
|
||||
"Latest" to "created_at",
|
||||
"Popular" to "user_follow_count",
|
||||
"Highest Rating" to "rating",
|
||||
"Last Uploaded" to "uploaded",
|
||||
),
|
||||
)
|
||||
|
||||
class GenreFilter(genres: List<Metadata.Name>) : TriStateGroupFilter(
|
||||
name = "Genre",
|
||||
options = genres.map { it.name to it.slug },
|
||||
)
|
||||
|
||||
class TagFilter(tags: List<Metadata.Name>) : TriStateGroupFilter(
|
||||
name = "Tags",
|
||||
options = tags.map { it.name to it.slug },
|
||||
)
|
||||
|
||||
class DemographicFilter : CheckBoxGroup(
|
||||
name = "Demographic",
|
||||
options = listOf(
|
||||
"Shounen" to "1",
|
||||
"Josei" to "2",
|
||||
"Seinen" to "3",
|
||||
"Shoujo" to "4",
|
||||
"None" to "0",
|
||||
),
|
||||
)
|
||||
|
||||
class CreatedAtFilter : SelectFilter(
|
||||
name = "Created At",
|
||||
options = listOf(
|
||||
"" to "",
|
||||
"3 days ago" to "3",
|
||||
"7 days ago" to "7",
|
||||
"30 days ago" to "30",
|
||||
"3 months ago" to "90",
|
||||
"6 months ago" to "180",
|
||||
"1 year ago" to "365",
|
||||
"2 years ago" to "730",
|
||||
),
|
||||
)
|
||||
|
||||
class TypeFilter : CheckBoxGroup(
|
||||
name = "Type",
|
||||
options = listOf(
|
||||
"Manga" to "jp",
|
||||
"Manhwa" to "kr",
|
||||
"Manhua" to "cn",
|
||||
"Others" to "others",
|
||||
),
|
||||
)
|
||||
|
||||
class MinimumChaptersFilter : Filter.Text(
|
||||
name = "Minimum Chapters",
|
||||
)
|
||||
|
||||
class StatusFilter : SelectFilter(
|
||||
name = "Status",
|
||||
options = listOf(
|
||||
"" to "",
|
||||
"Ongoing" to "1",
|
||||
"Completed" to "2",
|
||||
"Cancelled" to "3",
|
||||
"Hiatus" to "4",
|
||||
),
|
||||
)
|
||||
|
||||
class ContentRatingFilter : SelectFilter(
|
||||
name = "Content Rating",
|
||||
options = listOf(
|
||||
"" to "",
|
||||
"Safe" to "safe",
|
||||
"Suggestive" to "suggestive",
|
||||
"Erotica" to "erotica",
|
||||
),
|
||||
)
|
||||
|
||||
class ReleaseFrom : SelectFilter(
|
||||
name = "Release From",
|
||||
options = buildList {
|
||||
add(("" to ""))
|
||||
Calendar.getInstance().get(Calendar.YEAR).downTo(1990).mapTo(this) {
|
||||
("$it" to it.toString())
|
||||
}
|
||||
add(("Before 1990" to "0"))
|
||||
},
|
||||
)
|
||||
|
||||
class ReleaseTo : SelectFilter(
|
||||
name = "Release To",
|
||||
options = buildList {
|
||||
add(("" to ""))
|
||||
Calendar.getInstance().get(Calendar.YEAR).downTo(1990).mapTo(this) {
|
||||
("$it" to it.toString())
|
||||
}
|
||||
add(("Before 1990" to "0"))
|
||||
},
|
||||
)
|
||||
Loading…
x
Reference in New Issue
Block a user