Update Komiic (#10376)

- Refresh token automatically
- Refactor requests
- Fetch genres from website
- Tweak chapter list
This commit is contained in:
stevenyomi 2025-09-04 08:34:36 +00:00 committed by Draff
parent 3e65d19929
commit df9da07535
Signed by: Draff
GPG Key ID: E8A89F3211677653
8 changed files with 345 additions and 333 deletions

View File

@ -1,7 +1,7 @@
ext {
extName = 'Komiic'
extClass = '.Komiic'
extVersionCode = 3
extVersionCode = 4
isNsfw = true
}

View File

@ -3,73 +3,86 @@ package eu.kanade.tachiyomi.extension.zh.komiic
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.Serializable
import java.text.SimpleDateFormat
@Serializable
class Data<T>(val data: Result<T>)
class ResponseDto(private val data: DataDto?, private val errors: List<ErrorDto>?) {
fun getData() = data ?: throw Exception(errors!!.joinToString("\n") { it.message })
}
@Serializable
class MultiData<T, V>(val data: MultiResult<T, V>)
class ErrorDto(val message: String)
@Serializable
class Result<T>(val result: T)
@Serializable
class MultiResult<T, V>(val result1: T, val result2: V)
@Serializable
data class Item(val id: String, val name: String)
@Serializable
data class Comic(
val id: String,
val title: String,
val description: String,
val status: String,
val imageUrl: String,
var authors: List<Item>,
val categories: List<Item>,
class DataDto(
private val comics: List<MangaDto>?,
val allCategory: List<ItemDto>?,
private val searchComicsAndAuthors: DataDto?,
val comicById: MangaDto?,
val chaptersByComicId: List<ChapterDto>?,
val reachedImageLimit: Boolean?,
val imagesByChapterId: List<PageDto>?,
) {
fun getListing(): List<MangaDto> = comics ?: searchComicsAndAuthors!!.comics!!
}
@Serializable
class JwtPayload(val exp: Long)
@Serializable
class ItemDto(val id: String, val name: String)
@Serializable
class MangaDto(
private val id: String,
private val title: String,
private val description: String,
private val status: String,
private val imageUrl: String,
private val authors: List<ItemDto>,
private val categories: List<ItemDto>,
) {
val url get() = "/comic/$id"
fun toSManga() = SManga.create().apply {
url = "/comic/$id"
title = this@Comic.title
thumbnail_url = this@Comic.imageUrl
author = this@Comic.authors.joinToString { it.name }
genre = this@Comic.categories.joinToString { it.name }
description = this@Comic.description
status = when (this@Comic.status) {
url = this@MangaDto.url
title = this@MangaDto.title
thumbnail_url = this@MangaDto.imageUrl
author = this@MangaDto.authors.joinToString { it.name }
genre = this@MangaDto.categories.joinToString { it.name }
description = this@MangaDto.description
status = when (this@MangaDto.status) {
"ONGOING" -> SManga.ONGOING
"END" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
initialized = this@Comic.description.isNotEmpty()
initialized = this@MangaDto.description.isNotEmpty()
}
}
@Serializable
data class Chapter(
val id: String,
class ChapterDto(
private val id: String,
val serial: String,
val type: String,
val size: Int,
val dateCreated: String,
private val size: Int,
private val dateCreated: String,
) {
fun toSChapter(comicUrl: String, parseDate: (String) -> Long) = SChapter.create().apply {
url = "$comicUrl/chapter/${this@Chapter.id}"
name = when (this@Chapter.type) {
"chapter" -> "${this@Chapter.serial}"
"book" -> "${this@Chapter.serial}"
else -> this@Chapter.serial
fun toSChapter(mangaUrl: String, dateFormat: SimpleDateFormat) = SChapter.create().apply {
val (suffix, typeName) = when (val type = this@ChapterDto.type) {
"chapter" -> Pair("", "連載")
"book" -> Pair("", "單行本")
else -> throw Exception("未知章節類型:$type")
}
scanlator = "${this@Chapter.size}P"
date_upload = parseDate(this@Chapter.dateCreated)
chapter_number = if (this@Chapter.type == "book") 0F else this@Chapter.serial.toFloatOrNull() ?: -1f
url = "$mangaUrl/chapter/${this@ChapterDto.id}"
name = "${this@ChapterDto.serial}$suffix${this@ChapterDto.size}P"
scanlator = typeName
date_upload = dateFormat.parse(this@ChapterDto.dateCreated)!!.time
chapter_number = this@ChapterDto.serial.toFloatOrNull() ?: -1f
}
}
@Serializable
data class Image(
val id: String,
class PageDto(
val kid: String,
val height: Int,
val width: Int,
)

View File

@ -3,43 +3,50 @@ package eu.kanade.tachiyomi.extension.zh.komiic
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
var categories: List<ItemDto> = emptyList()
fun buildFilterList(): FilterList {
val categories = mapOf(
"1" to "愛情", "3" to "神鬼", "4" to "校園", "5" to "搞笑", "6" to "生活",
"7" to "懸疑", "8" to "冒險", "10" to "職場", "11" to "魔幻", "2" to "後宮",
"12" to "魔法", "13" to "格鬥", "14" to "宅男", "15" to "勵志", "16" to "耽美",
"17" to "科幻", "18" to "百合", "19" to "治癒", "20" to "萌系", "21" to "熱血",
"22" to "競技", "23" to "推理", "24" to "雜誌", "25" to "偵探", "26" to "偽娘",
"27" to "美食", "9" to "恐怖", "28" to "四格", "31" to "社會", "32" to "歷史",
"33" to "戰爭", "34" to "舞蹈", "35" to "武俠", "36" to "機戰", "37" to "音樂",
"40" to "體育", "42" to "黑道", "46" to "腐女", "47" to "異世界", "48" to "驚悚",
"51" to "成人", "54" to "戰鬥", "55" to "復仇", "56" to "轉生", "57" to "黑暗奇幻",
"58" to "戲劇", "59" to "生存", "60" to "策略", "61" to "政治", "62" to "黑暗",
"64" to "動作", "70" to "性轉換", "73" to "色情", "181" to "校园", "78" to "日常",
"81" to "青春", "83" to "料理", "85" to "醫療", "86" to "致鬱", "87" to "心理",
"88" to "穿越", "92" to "友情", "93" to "犯罪", "97" to "劇情",
"110" to "運動", "113" to "少女", "114" to "賭博", "119" to "情色", "123" to "女性向",
"128" to "性轉", "129" to "溫馨", "164" to "同人",
)
val categoryFilter = if (categories.isNotEmpty()) {
CategoryFilter()
} else {
Filter.Header("點擊“重設”載入類型")
}
return FilterList(
Filter.Header("過濾條件(搜索關鍵字時無效)"),
CategoryFilter(categories),
StatusFilter(),
Filter.Header("篩選條件(搜索關鍵字時無效)"),
categoryFilter,
SortFilter(),
StatusFilter(),
RatingFilter(),
)
}
interface KomiicFilter {
fun apply(variables: ListingVariables)
}
class Category(val id: String, name: String) : Filter.CheckBox(name)
class CategoryFilter(categories: Map<String, String>) :
Filter.Group<Category>("類型(篩選同時包含全部所選標簽的漫畫)", categories.map { Category(it.key, it.value) }) {
val selected get() = state.filter(Category::state).map(Category::id)
class CategoryFilter :
Filter.Group<Category>("類型(篩選同時包含全部所選標簽的漫畫)", categories.map { Category(it.id, it.name) }), KomiicFilter {
override fun apply(variables: ListingVariables) {
variables.categoryId = state.mapNotNull { if (it.state) it.id else null }
}
}
class StatusFilter : Filter.Select<String>("狀態", arrayOf("全部", "連載", "完結")) {
val value get() = arrayOf("", "ONGOING", "END")[state]
class StatusFilter : Filter.Select<String>("狀態", arrayOf("全部", "連載", "完結")), KomiicFilter {
override fun apply(variables: ListingVariables) {
variables.pagination.status = arrayOf("", "ONGOING", "END")[state]
}
}
class SortFilter : Filter.Select<String>("排序", arrayOf("更新", "觀看數", "喜愛數")) {
val value get() = arrayOf("DATE_UPDATED", "VIEWS", "FAVORITE_COUNT")[state]
class SortFilter : Filter.Select<String>("排序", arrayOf("更新", "本月觀看數(不能篩選類型)", "觀看數", "喜愛數")), KomiicFilter {
override fun apply(variables: ListingVariables) {
variables.pagination.orderBy = arrayOf(OrderBy.DATE_UPDATED, OrderBy.MONTH_VIEWS, OrderBy.VIEWS, OrderBy.FAVORITE_COUNT)[state]
}
}
class RatingFilter : Filter.Select<String>("色氣程度", arrayOf("全部", "", "1", "2", "3", "≥4", "5")), KomiicFilter {
override fun apply(variables: ListingVariables) {
variables.pagination.sexyLevel = arrayOf(null, 0, 1, 2, 3, 4, 5)[state]
}
}

View File

@ -1,35 +1,48 @@
package eu.kanade.tachiyomi.extension.zh.komiic
import android.util.Base64
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
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.firstInstance
import keiyoushi.utils.getPreferencesLazy
import keiyoushi.utils.parseAs
import keiyoushi.utils.toJsonString
import keiyoushi.utils.tryParse
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.RequestBody
import okhttp3.Response
import rx.Observable
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
class Komiic : HttpSource(), ConfigurableSource {
override var name = "Komiic"
override val name = "Komiic"
override val baseUrl = "https://komiic.com"
override val lang = "zh"
override val supportsLatest = true
override val client = network.cloudflareClient
override val client = network.cloudflareClient.newBuilder()
.addInterceptor { chain ->
refreshToken(chain)
chain.proceed(chain.request())
}
.build()
private fun refreshToken(chain: Interceptor.Chain) {
val url = chain.request().url
if (url.pathSegments[0] != "api") return
val cookie = client.cookieJar.loadForRequest(url).find { it.name == "komiic-access-token" } ?: return
val parts = cookie.value.split(".")
if (parts.size != 3) throw IOException("Token 格式無效")
val payload = Base64.decode(parts[1], Base64.DEFAULT).decodeToString()
if (System.currentTimeMillis() + 3600_000 < payload.parseAs<JwtPayload>().exp * 1000) return
val response = chain.proceed(POST("$baseUrl/auth/refresh", headers)).apply { close() }
if (!response.isSuccessful) throw IOException("刷新 Token 失敗HTTP ${response.code}")
}
private val apiUrl = "$baseUrl/api/query"
private val preferences by getPreferencesLazy()
@ -40,168 +53,98 @@ class Komiic : HttpSource(), ConfigurableSource {
// Customize ===================================================================================
companion object {
const val PAGE_SIZE = 20
const val PREFIX_ID_SEARCH = "id:"
private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
private val DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
}
private val SManga.id get() = url.substringAfterLast("/")
private val SChapter.id get() = url.substringAfterLast("/")
private inline fun <reified T> Payload<T>.toRequestBody() = this.toJsonString().toRequestBody(JSON_MEDIA_TYPE)
/**
* 根據 ID 搜索漫畫
* Search the comic based on the ID.
*/
private fun comicByIDRequest(id: String): Request {
val variables = Variables().field("comicId", id).build()
val payload = Payload(Query.COMIC_BY_ID, variables)
return POST(apiUrl, headers, payload.toRequestBody())
}
/**
* 根據 ID 解析搜索來的漫畫
* Parse the comic based on the ID.
*/
private fun parseComicByID(response: Response): MangasPage {
val res = response.parseAs<Data<Comic>>()
val entries = listOf(res.data.result.toSManga())
return MangasPage(entries, false)
}
/**
* 檢查 API 是否達到上限
* Check if the API has reached its limit.
* But how to throw an exception message to notify user in reading page?
*/
// private fun checkAPILimit(): Observable<Boolean> {
// val payload = Payload("reachedImageLimit", Variables().build(), QUERY_API_LIMIT)
// val response = client.newCall(POST(queryAPIUrl, headers, payload.toRequestBody()))
// val limit = response.asObservableSuccess().map { it.parseAs<Data<Boolean>>().data.result }
// return limit
// }
private fun RequestBody.request() = POST(apiUrl, headers, this)
private fun Response.parse() = parseAs<ResponseDto>().getData()
// Popular Manga ===============================================================================
override fun popularMangaRequest(page: Int): Request {
val pagination = Pagination((page - 1) * PAGE_SIZE, "MONTH_VIEWS")
val variables = Variables().field("pagination", pagination).build()
val payload = Payload(Query.HOT_COMICS, variables)
return POST(apiUrl, headers, payload.toRequestBody())
val pagination = Pagination((page - 1) * PAGE_SIZE, OrderBy.MONTH_VIEWS)
return listingQuery(ListingVariables(pagination)).request()
}
override fun popularMangaParse(response: Response): MangasPage {
val res = response.parseAs<Data<List<Comic>>>()
val comics = res.data.result
return MangasPage(comics.map(Comic::toSManga), comics.size == PAGE_SIZE)
}
override fun popularMangaParse(response: Response) = parseListing(response.parse())
// Latest Updates ==============================================================================
override fun latestUpdatesRequest(page: Int): Request {
val pagination = Pagination((page - 1) * PAGE_SIZE, "DATE_UPDATED")
val variables = Variables().field("pagination", pagination).build()
val payload = Payload(Query.RECENT_UPDATE, variables)
return POST(apiUrl, headers, payload.toRequestBody())
val pagination = Pagination((page - 1) * PAGE_SIZE, OrderBy.DATE_UPDATED)
return listingQuery(ListingVariables(pagination)).request()
}
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
override fun latestUpdatesParse(response: Response) = parseListing(response.parse())
// Search Manga ================================================================================
override fun getFilterList() = buildFilterList()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query.isNotBlank()) {
val variables = Variables().field("keyword", query).build()
val payload = Payload(Query.SEARCH, variables)
return POST(apiUrl, headers, payload.toRequestBody())
} else {
val categories = filters.firstInstance<CategoryFilter>()
val status = filters.firstInstance<StatusFilter>()
val sort = filters.firstInstance<SortFilter>()
val variables = Variables().field(
"pagination",
Pagination((page - 1) * PAGE_SIZE, sort.value, status.value, false),
).field("categoryId", categories.selected).build()
val payload = Payload(Query.COMIC_BY_CATEGORIES, variables)
return POST(apiUrl, headers, payload.toRequestBody())
}
}
override fun searchMangaParse(response: Response): MangasPage {
val res = response.parseAs<Data<Result<List<Comic>>>>()
val comics = res.data.result.result
return MangasPage(comics.map(Comic::toSManga), comics.size == PAGE_SIZE)
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(PREFIX_ID_SEARCH)) {
return client.newCall(comicByIDRequest(query.substringAfter(PREFIX_ID_SEARCH)))
.asObservableSuccess().map(::parseComicByID)
return if (query.startsWith(PREFIX_ID_SEARCH)) {
idsQuery(query.removePrefix(PREFIX_ID_SEARCH)).request()
} else if (query.isNotBlank()) {
return super.fetchSearchManga(page, query, filters)
searchQuery(query).request()
} else {
val variables = ListingVariables(Pagination((page - 1) * PAGE_SIZE))
for (filter in filters) if (filter is KomiicFilter) filter.apply(variables)
listingQuery(variables).request()
}
return client.newCall(searchMangaRequest(page, query, filters))
.asObservableSuccess().map(::popularMangaParse)
}
override fun searchMangaParse(response: Response) = parseListing(response.parse())
// Manga Details ===============================================================================
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url
override fun mangaDetailsRequest(manga: SManga) = comicByIDRequest(manga.id)
override fun mangaDetailsRequest(manga: SManga) = mangaQuery(manga.id).request()
override fun mangaDetailsParse(response: Response): SManga {
val res = response.parseAs<Data<Comic>>()
return res.data.result.toSManga()
}
override fun mangaDetailsParse(response: Response) = response.parse().comicById!!.toSManga()
// Chapter List ================================================================================
override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url + "/images/all"
override fun chapterListRequest(manga: SManga): Request {
val variables = Variables().field("comicId", manga.id).build()
val payload = Payload(Query.CHAPTERS_BY_COMIC_ID, variables)
return POST("$apiUrl#${manga.url}", headers, payload.toRequestBody())
}
override fun chapterListRequest(manga: SManga) = mangaQuery(manga.id).request()
override fun chapterListParse(response: Response): List<SChapter> {
val res = response.parseAs<Data<List<Chapter>>>()
val comics = res.data.result.sortedWith(
compareByDescending<Chapter> { it.type }
val data = response.parse()
val chapters = data.chaptersByComicId!!.toMutableList()
when (preferences.getString(CHAPTER_FILTER_PREF, "all")!!) {
"chapter" -> chapters.retainAll { it.type == "chapter" }
"book" -> chapters.retainAll { it.type == "book" }
else -> {}
}
chapters.sortWith(
compareByDescending<ChapterDto> { it.type }
.thenByDescending { it.serial.toFloatOrNull() },
)
val display = preferences.getString(CHAPTER_FILTER_PREF, "all")
val items = when (display) {
"chapter" -> comics.filter { it.type == "chapter" }
"book" -> comics.filter { it.type == "book" }
else -> comics
val mangaUrl = data.comicById!!.url
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
val comicUrl = response.request.url.fragment!!
return items.map { it.toSChapter(comicUrl, DATE_FORMAT::tryParse) }
return chapters.map { it.toSChapter(mangaUrl, dateFormat) }
}
// Page List ===================================================================================
override fun pageListRequest(chapter: SChapter): Request {
val variables = Variables().field("chapterId", chapter.id).build()
val payload = Payload(Query.IMAGES_BY_CHAPTER_ID, variables)
return POST("$apiUrl#${chapter.url}", headers, payload.toRequestBody())
return pageListQuery(chapter.id).request().newBuilder()
.tag(String::class.java, chapter.url)
.build()
}
override fun pageListParse(response: Response): List<Page> {
val res = response.parseAs<MultiData<Boolean, List<Image>>>()
val data = response.parse()
val check = preferences.getBoolean(CHECK_API_LIMIT_PREF, true)
check(!check || !res.data.result1) { "今日圖片讀取次數已達上限,請登录或明天再來!" }
val chapterUrl = response.request.url.fragment!!
return res.data.result2.mapIndexed { index, image ->
Page(index, "$chapterUrl/page/$index", "$baseUrl/api/image/${image.kid}")
if (check && data.reachedImageLimit!!) {
throw Exception("今日圖片讀取次數已達上限,請登录或明天再來!")
}
val chapterUrl = response.request.tag(String::class.java)!!
return data.imagesByChapterId!!.mapIndexed { index, image ->
Page(index, "$chapterUrl/page/${index + 1}", "$baseUrl/api/image/${image.kid}")
}
}
@ -209,7 +152,7 @@ class Komiic : HttpSource(), ConfigurableSource {
override fun imageRequest(page: Page): Request {
return super.imageRequest(page).newBuilder()
.addHeader("accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8'")
.addHeader("accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")
.addHeader("referer", page.url)
.build()
}

View File

@ -3,38 +3,48 @@ package eu.kanade.tachiyomi.extension.zh.komiic
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.encodeToJsonElement
const val PAGE_SIZE = 30 // using 20 causes weird behavior in the filter endpoint
@Serializable
data class Payload<T>(
val operationName: String,
val variables: T,
val query: String,
class ListingVariables(
val pagination: Pagination,
) {
constructor(query: Query, variables: T) : this(query.operation, variables, query.body)
@EncodeDefault
var categoryId: List<String> = emptyList()
fun encode() = Json.encodeToJsonElement(this) as JsonObject
}
@Suppress("unused")
@Serializable
data class Pagination(
val offset: Int,
val orderBy: String,
class Pagination(
private val offset: Int,
@EncodeDefault
val status: String = "",
var orderBy: OrderBy = OrderBy.DATE_UPDATED,
) {
@EncodeDefault
val asc: Boolean = true,
var status: String = ""
@EncodeDefault
val limit: Int = Komiic.PAGE_SIZE,
)
private val asc: Boolean = false // this should be true in popular but doesn't take effect in any case
class Variables {
val variableMap = mutableMapOf<String, JsonElement>()
@EncodeDefault
private val limit: Int = PAGE_SIZE
inline fun <reified T> field(key: String, value: T): Variables {
variableMap[key] = Json.encodeToJsonElement(value)
return this
}
fun build() = JsonObject(variableMap)
@EncodeDefault
var sexyLevel: Int? = null
}
enum class OrderBy {
DATE_UPDATED,
DATE_CREATED,
VIEWS,
MONTH_VIEWS,
ID,
COMIC_DATE_UPDATED,
FAVORITE_ADDED,
FAVORITE_COUNT,
}

View File

@ -0,0 +1,152 @@
package eu.kanade.tachiyomi.extension.zh.komiic
import eu.kanade.tachiyomi.source.model.MangasPage
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.add
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonArray
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
private fun buildQuery(query: String): String {
return query.trimIndent()
.replace("#{body}", COMIC_BODY.trimIndent())
.replace("%", "$")
}
private const val COMIC_BODY =
"""
{
id
title
description
status
imageUrl
authors {
id
name
}
categories {
id
name
}
}
"""
private fun buildRequestBody(query: String, variables: JsonObject): RequestBody {
val body = buildJsonObject {
put("query", query)
put("variables", variables)
}
val contentType = "application/json".toMediaType()
return Json.encodeToString(body).toByteArray().toRequestBody(contentType)
}
fun parseListing(data: DataDto): MangasPage {
data.allCategory?.let { categories = it }
val listing = data.getListing()
val entries = listing.map { it.toSManga() }
val hasNextPage = listing.size == PAGE_SIZE
return MangasPage(entries, hasNextPage)
}
fun listingQuery(variables: ListingVariables): RequestBody {
if (variables.pagination.orderBy == OrderBy.MONTH_VIEWS) return popularQuery(variables)
val query = buildQuery(
"""
query comicByCategories(%categoryId: [ID!]!, %pagination: Pagination!) {
comics: comicByCategories(categoryId: %categoryId, pagination: %pagination) #{body}
allCategory { id name }
}
""",
)
return buildRequestBody(query, variables.encode())
}
private fun popularQuery(variables: ListingVariables): RequestBody {
if (variables.categoryId.isNotEmpty()) throw Exception("“本月最夯”不能篩選類型")
val query = buildQuery(
"""
query hotComics(%pagination: Pagination!) {
comics: hotComics(pagination: %pagination) #{body}
allCategory { id name }
}
""",
)
return buildRequestBody(query, variables.encode())
}
fun searchQuery(keyword: String): RequestBody {
val query = buildQuery(
"""
query searchComicAndAuthorQuery(%keyword: String!) {
searchComicsAndAuthors(keyword: %keyword) {
comics #{body}
}
allCategory { id name }
}
""",
)
val variables = buildJsonObject {
put("keyword", keyword)
}
return buildRequestBody(query, variables)
}
fun idsQuery(id: String): RequestBody {
val query = buildQuery(
"""
query comicByIds(%comicIds: [ID]!) {
comics: comicByIds(comicIds: %comicIds) #{body}
}
""",
)
val variables = buildJsonObject {
putJsonArray("comicIds") {
add(id)
}
}
return buildRequestBody(query, variables)
}
fun mangaQuery(id: String): RequestBody {
val query = buildQuery(
"""
query chapterByComicId(%comicId: ID!) {
comicById(comicId: %comicId) #{body}
chaptersByComicId(comicId: %comicId) {
id
serial
type
size
dateCreated
}
}
""",
)
val variables = buildJsonObject {
put("comicId", id)
}
return buildRequestBody(query, variables)
}
fun pageListQuery(chapterId: String): RequestBody {
val query = buildQuery(
"""
query imagesByChapterId(%chapterId: ID!) {
reachedImageLimit
imagesByChapterId(chapterId: %chapterId) {
kid
}
}
""",
)
val variables = buildJsonObject {
put("chapterId", chapterId)
}
return buildRequestBody(query, variables)
}

View File

@ -1,115 +0,0 @@
package eu.kanade.tachiyomi.extension.zh.komiic
enum class Query {
HOT_COMICS {
override val operation = "hotComics"
override val body = buildQuery(comicBody) {
"""
query hotComics(%pagination: Pagination!) {
result: hotComics(pagination: %pagination) #{body}
}
"""
}
},
RECENT_UPDATE {
override val operation = "recentUpdate"
override val body = buildQuery(comicBody) {
"""
query recentUpdate(%pagination: Pagination!) {
result: recentUpdate(pagination: %pagination) #{body}
}
"""
}
},
SEARCH {
override val operation = "searchComicAndAuthorQuery"
override val body = buildQuery(comicBody) {
"""
query searchComicAndAuthorQuery(%keyword: String!) {
result: searchComicsAndAuthors(keyword: %keyword) {
result: comics #{body}
}
}
"""
}
},
COMIC_BY_CATEGORIES {
override val operation = "comicByCategories"
override val body = buildQuery(comicBody) {
"""
query comicByCategories(%categoryId: [ID!]!, %pagination: Pagination!) {
result: comicByCategories(categoryId: %categoryId, pagination: %pagination) #{body}
}
"""
}
},
COMIC_BY_ID {
override val operation = "comicById"
override val body = buildQuery(comicBody) {
"""
query comicById(%comicId: ID!) {
result: comicById(comicId: %comicId) #{body}
}
"""
}
},
CHAPTERS_BY_COMIC_ID {
override val operation = "chapterByComicId"
override val body = buildQuery {
"""
query chapterByComicId(%comicId: ID!) {
result: chaptersByComicId(comicId: %comicId) {
id
serial
type
size
dateCreated
}
}
"""
}
},
IMAGES_BY_CHAPTER_ID {
override val operation = "imagesByChapterId"
override val body = buildQuery {
"""
query imagesByChapterId(%chapterId: ID!) {
result1: reachedImageLimit,
result2: imagesByChapterId(chapterId: %chapterId) {
id
kid
height
width
}
}
"""
}
}, ;
abstract val body: String
abstract val operation: String
val comicBody =
"""
{
id
title
description
status
imageUrl
authors {
id
name
}
categories {
id
name
}
}
"""
fun buildQuery(body: String = "", queryAction: () -> String): String {
return queryAction().trimIndent()
.replace("#{body}", body.trimIndent())
.replace("%", "$")
}
}

View File

@ -7,6 +7,8 @@ import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
const val PREFIX_ID_SEARCH = "id:"
class UrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -15,7 +17,7 @@ class UrlActivity : Activity() {
val id = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${Komiic.PREFIX_ID_SEARCH}$id")
putExtra("query", "$PREFIX_ID_SEARCH$id")
putExtra("filter", packageName)
}