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