Komiic: Add manga description and refactor some code (#9445)

* add comic description

* fix manga search results missing descriptions

* clean unused variables

* clean unused class

* Add some config options and refactor some code

* refactor some code

* modify config option summary

* apply comments

* modify Queries.kt

* small modification

* Format code

* replace parse method

* optimize check API limit

* modify config summary

* add getChapterUrl()
This commit is contained in:
Hualiang 2025-07-02 16:09:36 +08:00 committed by Draff
parent 3070ed4967
commit 2d0e57517e
Signed by: Draff
GPG Key ID: E8A89F3211677653
7 changed files with 254 additions and 516 deletions

View File

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

View File

@ -1,109 +1,65 @@
package eu.kanade.tachiyomi.extension.zh.komiic package eu.kanade.tachiyomi.extension.zh.komiic
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess 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.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.serialization.decodeFromString import keiyoushi.utils.getPreferences
import kotlinx.serialization.encodeToString import keiyoushi.utils.parseAs
import kotlinx.serialization.json.Json import keiyoushi.utils.toJsonString
import keiyoushi.utils.tryParse
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import java.util.TimeZone import java.util.TimeZone
class Komiic : HttpSource() { class Komiic : HttpSource(), ConfigurableSource {
// Override variables
override var name = "Komiic" override var name = "Komiic"
override val baseUrl = "https://komiic.com" override val baseUrl = "https://komiic.com"
override val lang = "zh" override val lang = "zh"
override val supportsLatest = true override val supportsLatest = true
override val client = network.cloudflareClient
override val client: OkHttpClient = network.cloudflareClient private val apiUrl = "$baseUrl/api/query"
private val preferences = getPreferences()
// Variables override fun setupPreferenceScreen(screen: PreferenceScreen) {
private val queryAPIUrl = "$baseUrl/api/query" preferencesInternal(screen.context).forEach(screen::addPreference)
private val json: Json by injectLazy()
/**
* 解析漫畫列表
* Parse comic list
*/
private inline fun <reified T : ComicListResult> parseComicList(response: Response): MangasPage {
val res = response.parseAs<Data<T>>()
val comics = res.data.comics
val entries = comics.map { comic ->
comic.toSManga()
} }
val hasNextPage = comics.size == PAGE_SIZE // Customize ===================================================================================
return MangasPage(entries, hasNextPage)
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")
}
} }
// Hot Comic private val SManga.id get() = url.substringAfterLast("/")
override fun popularMangaRequest(page: Int): Request { private val SChapter.id get() = url.substringAfterLast("/")
val payload = Payload( private inline fun <reified T> Payload<T>.toRequestBody() = this.toJsonString().toRequestBody(JSON_MEDIA_TYPE)
operationName = "hotComics",
variables = HotComicsVariables(
pagination = MangaListPagination(
PAGE_SIZE,
(page - 1) * PAGE_SIZE,
"MONTH_VIEWS",
"",
true,
),
),
query = QUERY_HOT_COMICS,
).toJsonRequestBody()
return POST(queryAPIUrl, headers, payload)
}
override fun popularMangaParse(response: Response) = parseComicList<HotComicsResponse>(response)
// Recent update
override fun latestUpdatesRequest(page: Int): Request {
val payload = Payload(
operationName = "recentUpdate",
variables = RecentUpdateVariables(
pagination = MangaListPagination(
PAGE_SIZE,
(page - 1) * PAGE_SIZE,
"DATE_UPDATED",
"",
true,
),
),
query = QUERY_RECENT_UPDATE,
).toJsonRequestBody()
return POST(queryAPIUrl, headers, payload)
}
override fun latestUpdatesParse(response: Response) = parseComicList<RecentUpdateResponse>(response)
/** /**
* 根據 ID 搜索漫畫 * 根據 ID 搜索漫畫
* Search the comic based on the ID. * Search the comic based on the ID.
*/ */
private fun comicByIDRequest(id: String): Request { private fun comicByIDRequest(id: String): Request {
val payload = Payload( val variables = Variables().set("comicId", id).build()
operationName = "comicById", val payload = Payload("comicById", variables, QUERY_COMIC_BY_ID)
variables = ComicByIdVariables(id), return POST(apiUrl, headers, payload.toRequestBody())
query = QUERY_COMIC_BY_ID,
).toJsonRequestBody()
return POST(queryAPIUrl, headers, payload)
} }
/** /**
@ -111,32 +67,64 @@ class Komiic : HttpSource() {
* Parse the comic based on the ID. * Parse the comic based on the ID.
*/ */
private fun parseComicByID(response: Response): MangasPage { private fun parseComicByID(response: Response): MangasPage {
val res = response.parseAs<Data<ComicByIDResponse>>() val res = response.parseAs<Data<Comic>>()
val entries = mutableListOf<SManga>() val entries = listOf(res.data.result.toSManga())
val comic = res.data.comic.toSManga() return MangasPage(entries, false)
entries.add(comic)
val hasNextPage = entries.size == PAGE_SIZE
return MangasPage(entries, hasNextPage)
} }
// Search /**
* 檢查 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
// }
// Popular Manga ===============================================================================
override fun popularMangaRequest(page: Int): Request {
val variables = Variables().set(
"pagination",
Pagination((page - 1) * PAGE_SIZE, "MONTH_VIEWS"),
).build()
val payload = Payload("hotComics", variables, QUERY_HOT_COMICS)
return POST(apiUrl, headers, payload.toRequestBody())
}
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)
}
// Latest Updates ==============================================================================
override fun latestUpdatesRequest(page: Int): Request {
val variables = Variables().set(
"pagination",
Pagination((page - 1) * PAGE_SIZE, "DATE_UPDATED"),
).build()
val payload = Payload("recentUpdate", variables, QUERY_RECENT_UPDATE)
return POST(apiUrl, headers, payload.toRequestBody())
}
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
// Search Manga ================================================================================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val payload = Payload( val variables = Variables().set("keyword", query).build()
operationName = "searchComicAndAuthorQuery", val payload = Payload("searchComicAndAuthorQuery", variables, QUERY_SEARCH)
variables = SearchVariables(query), return POST(apiUrl, headers, payload.toRequestBody())
query = QUERY_SEARCH,
).toJsonRequestBody()
return POST(queryAPIUrl, headers, payload)
} }
override fun fetchSearchManga( override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
page: Int,
query: String,
filters: FilterList,
): Observable<MangasPage> {
return if (query.startsWith(PREFIX_ID_SEARCH)) { return if (query.startsWith(PREFIX_ID_SEARCH)) {
val mangaId = query.substringAfter(PREFIX_ID_SEARCH) client.newCall(comicByIDRequest(query.substringAfter(PREFIX_ID_SEARCH)))
client.newCall(comicByIDRequest(mangaId))
.asObservableSuccess() .asObservableSuccess()
.map(::parseComicByID) .map(::parseComicByID)
} else { } else {
@ -145,115 +133,67 @@ class Komiic : HttpSource() {
} }
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {
val res = response.parseAs<Data<SearchResponse>>() val res = response.parseAs<Data<Result<List<Comic>>>>()
val comics = res.data.action.comics val comics = res.data.result.result
return MangasPage(comics.map(Comic::toSManga), comics.size == PAGE_SIZE)
val entries = comics.map { comic ->
comic.toSManga()
} }
val hasNextPage = comics.size == PAGE_SIZE // Manga Details ===============================================================================
return MangasPage(entries, hasNextPage)
}
// Comic details override fun getMangaUrl(manga: SManga) = baseUrl + manga.url
override fun mangaDetailsRequest(manga: SManga) = comicByIDRequest(manga.url.substringAfterLast("/"))
override fun mangaDetailsRequest(manga: SManga) = comicByIDRequest(manga.id)
override fun mangaDetailsParse(response: Response): SManga { override fun mangaDetailsParse(response: Response): SManga {
val res = response.parseAs<Data<ComicByIDResponse>>() val res = response.parseAs<Data<Comic>>()
val comic = res.data.comic.toSManga() return res.data.result.toSManga()
return comic
} }
override fun getMangaUrl(manga: SManga) = "$baseUrl${manga.url}" // Chapter List ================================================================================
/** override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url + "/images/all"
* 解析日期
* Parse date
*/
private fun parseDate(dateStr: String): Long {
return try {
DATE_FORMAT.parse(dateStr)?.time ?: 0L
} catch (e: ParseException) {
e.printStackTrace()
0L
}
}
// Chapter list
override fun chapterListRequest(manga: SManga): Request { override fun chapterListRequest(manga: SManga): Request {
val payload = Payload( val variables = Variables().set("comicId", manga.id).build()
operationName = "chapterByComicId", val payload = Payload("chapterByComicId", variables, QUERY_CHAPTER)
variables = ChapterByComicIdVariables(manga.url.substringAfterLast("/")), return POST("$apiUrl#${manga.url}", headers, payload.toRequestBody())
query = QUERY_CHAPTER,
).toJsonRequestBody()
return POST("$queryAPIUrl#${manga.url}", headers, payload)
} }
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val res = response.parseAs<Data<ChaptersResponse>>() val res = response.parseAs<Data<List<Chapter>>>()
val comics = res.data.chapters val comics = res.data.result.sortedWith(
val comicUrl = response.request.url.fragment compareByDescending<Chapter> { it.type }
.thenByDescending { it.serial.toFloatOrNull() },
val tChapters = comics.filter { it.type == "chapter" } )
val tBooks = comics.filter { it.type == "book" } val display = preferences.getString(CHAPTER_FILTER_PREF, "all")
val items = when (display) {
val entries = (tChapters + tBooks).map { chapter -> "chapter" -> comics.filter { it.type == "chapter" }
SChapter.create().apply { "book" -> comics.filter { it.type == "book" }
url = "$comicUrl/chapter/${chapter.id}/page/1" else -> comics
name = when (chapter.type) {
"chapter" -> "${chapter.serial}"
"book" -> "${chapter.serial}"
else -> chapter.serial
} }
date_upload = parseDate(chapter.dateCreated) val comicUrl = response.request.url.fragment!!
chapter_number = chapter.serial.toFloatOrNull() ?: -1f return items.map { it.toSChapter(comicUrl, DATE_FORMAT::tryParse) }
}
}.reversed()
return entries
} }
/** // Page List ===================================================================================
* 檢查 API 是否達到上限
* Check if the API has reached its limit.
*
* (Idk how to throw an exception in reading page)
*/
// private fun fetchAPILimit(): Boolean {
// val payload = Payload("getImageLimit", "", QUERY_API_LIMIT).toJsonRequestBody()
// val response = client.newCall(POST(queryAPIUrl, headers, payload)).execute()
// val limit = response.parseAs<APILimitData>().getImageLimit
// return limit.limit <= limit.usage
// }
// Page list
override fun pageListRequest(chapter: SChapter): Request { override fun pageListRequest(chapter: SChapter): Request {
val payload = Payload( val variables = Variables().set("chapterId", chapter.id).build()
operationName = "imagesByChapterId", val payload = Payload("imagesByChapterId", variables, QUERY_PAGE_LIST)
variables = ImagesByChapterIdVariables( return POST("$apiUrl#${chapter.url}", headers, payload.toRequestBody())
chapter.url.substringAfter("/chapter/").substringBefore("/page/"),
),
query = QUERY_PAGE_LIST,
).toJsonRequestBody()
return POST("$queryAPIUrl#${chapter.url}", headers, payload)
} }
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
val res = response.parseAs<Data<ImagesResponse>>() val res = response.parseAs<MultiData<Boolean, List<Image>>>()
val pages = res.data.images val check = preferences.getBoolean(CHECK_API_LIMIT_PREF, true)
val chapterUrl = response.request.url.toString().split("#")[1] require(!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}")
}
}
return pages.mapIndexed { index, image -> // Image =======================================================================================
Page(
index,
"${chapterUrl.substringBeforeLast("/")}/$index",
"$baseUrl/api/image/${image.kid}",
)
}
}
override fun imageRequest(page: Page): Request { override fun imageRequest(page: Page): Request {
return super.imageRequest(page).newBuilder() return super.imageRequest(page).newBuilder()
@ -263,23 +203,4 @@ class Komiic : HttpSource() {
} }
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException() override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
private inline fun <reified T> String.parseAs(): T =
json.decodeFromString(this)
private inline fun <reified T> Response.parseAs(): T =
use { body.string() }.parseAs()
private inline fun <reified T : Any> T.toJsonRequestBody(): RequestBody =
json.encodeToString(this)
.toRequestBody(JSON_MEDIA_TYPE)
companion object {
private 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")
}
}
} }

View File

@ -1,49 +1,38 @@
package eu.kanade.tachiyomi.extension.zh.komiic package eu.kanade.tachiyomi.extension.zh.komiic
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.encodeToJsonElement
@Serializable @Serializable
class Payload<T>( data class Payload<T>(
val operationName: String, val operationName: String,
val variables: T, val variables: T,
val query: String, val query: String,
) )
@Serializable @Serializable
data class MangaListPagination( data class Pagination(
val limit: Int,
val offset: Int, val offset: Int,
val orderBy: String, val orderBy: String,
val status: String, @EncodeDefault
val asc: Boolean, val limit: Int = Komiic.PAGE_SIZE,
@EncodeDefault
val status: String = "",
@EncodeDefault
val asc: Boolean = true,
) )
@Serializable class Variables {
data class HotComicsVariables( val variableMap = mutableMapOf<String, JsonElement>()
val pagination: MangaListPagination,
)
@Serializable inline fun <reified T> set(key: String, value: T): Variables {
data class RecentUpdateVariables( variableMap[key] = Json.encodeToJsonElement(value)
val pagination: MangaListPagination, return this
) }
@Serializable fun build() = JsonObject(variableMap)
data class SearchVariables( }
val keyword: String,
)
@Serializable
data class ComicByIdVariables(
val comicId: String,
)
@Serializable
data class ChapterByComicIdVariables(
val comicId: String,
)
@Serializable
data class ImagesByChapterIdVariables(
val chapterId: String,
)

View File

@ -0,0 +1,25 @@
package eu.kanade.tachiyomi.extension.zh.komiic
import android.content.Context
import androidx.preference.ListPreference
import androidx.preference.SwitchPreferenceCompat
const val CHAPTER_FILTER_PREF = "CHAPTER_FILTER"
const val CHECK_API_LIMIT_PREF = "CHECK_API_LIMIT"
fun preferencesInternal(context: Context) = arrayOf(
ListPreference(context).apply {
key = CHAPTER_FILTER_PREF
title = "章節列表顯示"
summary = "注:部分漫畫小概率會有章節缺失,僅顯示章節就沒法通過看卷的內容來補充了。建議不要僅顯示卷,會導致無法獲取及時章節更新"
entries = arrayOf("同時顯示卷和章節", "僅顯示章節", "僅顯示卷")
entryValues = arrayOf("all", "chapter", "book")
setDefaultValue("all")
},
SwitchPreferenceCompat(context).apply {
key = CHECK_API_LIMIT_PREF
title = "自動檢查API受限"
summary = "點擊單個章節請求漫畫圖片時自動檢查一次圖片API是否達到今日請求上限。若已達上限則終止後續操作"
setDefaultValue(true)
},
)

View File

@ -1,159 +1,73 @@
package eu.kanade.tachiyomi.extension.zh.komiic package eu.kanade.tachiyomi.extension.zh.komiic
private fun buildQuery(queryAction: () -> String): String { private fun buildQuery(body: String = "", queryAction: () -> String): String {
return queryAction() return queryAction().trimIndent()
.trimIndent() .replace("#{body}", body.trimIndent())
.replace("%", "$") .replace("%", "$")
} }
val QUERY_HOT_COMICS: String = buildQuery { const val COMIC_BODY =
"""
{
id
title
description
status
imageUrl
authors {
id
name
}
categories {
id
name
}
}
"""
val QUERY_HOT_COMICS = buildQuery(COMIC_BODY) {
""" """
query hotComics(%pagination: Pagination!) { query hotComics(%pagination: Pagination!) {
hotComics(pagination: %pagination) { result: hotComics(pagination: %pagination) #{body}
id
title
status
year
imageUrl
authors {
id
name
__typename
}
categories {
id
name
__typename
}
dateUpdated
monthViews
views
favoriteCount
lastBookUpdate
lastChapterUpdate
__typename
}
} }
""" """
} }
val QUERY_RECENT_UPDATE: String = buildQuery { val QUERY_RECENT_UPDATE = buildQuery(COMIC_BODY) {
""" """
query recentUpdate(%pagination: Pagination!) { query recentUpdate(%pagination: Pagination!) {
recentUpdate(pagination: %pagination) { result: recentUpdate(pagination: %pagination) #{body}
id
title
status
year
imageUrl
authors {
id
name
__typename
}
categories {
id
name
__typename
}
dateUpdated
monthViews
views
favoriteCount
lastBookUpdate
lastChapterUpdate
__typename
}
} }
""" """
} }
val QUERY_SEARCH: String = buildQuery { val QUERY_SEARCH = buildQuery(COMIC_BODY) {
""" """
query searchComicAndAuthorQuery(%keyword: String!) { query searchComicAndAuthorQuery(%keyword: String!) {
searchComicsAndAuthors(keyword: %keyword) { result: searchComicsAndAuthors(keyword: %keyword) {
comics { result: comics #{body}
id
title
status
year
imageUrl
authors {
id
name
__typename
}
categories {
id
name
__typename
}
dateUpdated
monthViews
views
favoriteCount
lastBookUpdate
lastChapterUpdate
__typename
}
authors {
id
name
chName
enName
wikiLink
comicCount
views
__typename
}
__typename
} }
} }
""" """
} }
val QUERY_CHAPTER: String = buildQuery { val QUERY_COMIC_BY_ID = buildQuery(COMIC_BODY) {
"""
query comicById(%comicId: ID!) {
result: comicById(comicId: %comicId) #{body}
}
"""
}
val QUERY_CHAPTER = buildQuery {
""" """
query chapterByComicId(%comicId: ID!) { query chapterByComicId(%comicId: ID!) {
chaptersByComicId(comicId: %comicId) { result: chaptersByComicId(comicId: %comicId) {
id id
serial serial
type type
dateCreated
dateUpdated
size size
__typename
}
}
"""
}
val QUERY_COMIC_BY_ID = buildQuery {
"""
query comicById(%comicId: ID!) {
comicById(comicId: %comicId) {
id
title
status
year
imageUrl
authors {
id
name
__typename
}
categories {
id
name
__typename
}
dateCreated dateCreated
dateUpdated
views
favoriteCount
lastBookUpdate
lastChapterUpdate
__typename
} }
} }
""" """
@ -162,26 +76,15 @@ val QUERY_COMIC_BY_ID = buildQuery {
val QUERY_PAGE_LIST = buildQuery { val QUERY_PAGE_LIST = buildQuery {
""" """
query imagesByChapterId(%chapterId: ID!) { query imagesByChapterId(%chapterId: ID!) {
imagesByChapterId(chapterId: %chapterId) { result1: reachedImageLimit,
result2: imagesByChapterId(chapterId: %chapterId) {
id id
kid kid
height height
width width
__typename
} }
} }
""" """
} }
val QUERY_API_LIMIT = buildQuery { // val QUERY_API_LIMIT = buildQuery { "query reachedImageLimit { result: reachedImageLimit }" }
"""
query getImageLimit {
getImageLimit {
limit
usage
resetInSeconds
__typename
}
}
"""
}

View File

@ -1,141 +1,70 @@
package eu.kanade.tachiyomi.extension.zh.komiic package eu.kanade.tachiyomi.extension.zh.komiic
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Data<T>(val data: T) class Data<T>(val data: Result<T>)
interface ComicListResult {
val comics: List<Comic>
}
@Serializable @Serializable
data class HotComicsResponse( class MultiData<T, V>(val data: MultiResult<T, V>)
@SerialName("hotComics") override val comics: List<Comic>,
) : ComicListResult
@Serializable @Serializable
data class RecentUpdateResponse( class Result<T>(val result: T)
@SerialName("recentUpdate") override val comics: List<Comic>,
) : ComicListResult
interface SearchResult {
val action: ComicsAndAuthors
}
@Serializable @Serializable
data class SearchResponse( class MultiResult<T, V>(val result1: T, val result2: V)
@SerialName("searchComicsAndAuthors") override val action: ComicsAndAuthors,
) : SearchResult
@Serializable @Serializable
data class ComicsAndAuthors( data class ComicItem(val id: String, val name: String)
val comics: List<Comic>,
val authors: List<Author>,
@SerialName("__typename") val typeName: String,
)
interface ComicResult {
val comic: Comic
}
@Serializable
data class ComicByIDResponse(
@SerialName("comicById") override val comic: Comic,
) : ComicResult
@Serializable @Serializable
data class Comic( data class Comic(
val id: String, val id: String,
val title: String, val title: String,
val description: String,
val status: String, val status: String,
val year: Int,
val imageUrl: String, val imageUrl: String,
var authors: List<ComicAuthor>, var authors: List<ComicItem>,
val categories: List<ComicCategory>, val categories: List<ComicItem>,
val dateCreated: String = "",
val dateUpdated: String,
val monthViews: Int = 0,
val views: Int,
val favoriteCount: Int,
val lastBookUpdate: String,
val lastChapterUpdate: String,
@SerialName("__typename") val typeName: String,
) { ) {
private val parseStatus = when (status) {
"ONGOING" -> SManga.ONGOING
"END" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
fun toSManga() = SManga.create().apply { fun toSManga() = SManga.create().apply {
url = "/comic/$id" url = "/comic/$id"
title = this@Comic.title title = this@Comic.title
thumbnail_url = this@Comic.imageUrl thumbnail_url = this@Comic.imageUrl
author = this@Comic.authors.joinToString { it.name } author = this@Comic.authors.joinToString { it.name }
genre = this@Comic.categories.joinToString { it.name } genre = this@Comic.categories.joinToString { it.name }
description = buildString { description = this@Comic.description
append("年份: $year | ") status = when (this@Comic.status) {
append("點閱: ${simplifyNumber(views)} | ") "ONGOING" -> SManga.ONGOING
append("喜愛: ${simplifyNumber(favoriteCount)}\n") "END" -> SManga.COMPLETED
else -> SManga.UNKNOWN
} }
status = parseStatus initialized = this@Comic.description.isNotEmpty()
initialized = true
} }
} }
@Serializable
data class ComicCategory(
val id: String,
val name: String,
@SerialName("__typename") val typeName: String,
)
@Serializable
data class ComicAuthor(
val id: String,
val name: String,
@SerialName("__typename") val typeName: String,
)
@Serializable
data class Author(
val id: String,
val name: String,
val chName: String,
val enName: String,
val wikiLink: String,
val comicCount: Int,
val views: Int,
@SerialName("__typename") val typeName: String,
)
interface ChaptersResult {
val chapters: List<Chapter>
}
@Serializable
data class ChaptersResponse(
@SerialName("chaptersByComicId") override val chapters: List<Chapter>,
) : ChaptersResult
@Serializable @Serializable
data class Chapter( data class Chapter(
val id: String, val id: String,
val serial: String, val serial: String,
val type: String, val type: String,
val dateCreated: String,
val dateUpdated: String,
val size: Int, val size: Int,
@SerialName("__typename") val typeName: String, val dateCreated: String,
) ) {
fun toSChapter(comicUrl: String, parseDate: (String) -> Long) = SChapter.create().apply {
@Serializable url = "$comicUrl/chapter/${this@Chapter.id}"
data class ImagesResponse( name = when (this@Chapter.type) {
@SerialName("imagesByChapterId") val images: List<Image>, "chapter" -> "${this@Chapter.serial}"
) "book" -> "${this@Chapter.serial}"
else -> this@Chapter.serial
}
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
}
}
@Serializable @Serializable
data class Image( data class Image(
@ -143,18 +72,4 @@ data class Image(
val kid: String, val kid: String,
val height: Int, val height: Int,
val width: Int, val width: Int,
@SerialName("__typename") val typeName: String,
)
@Serializable
data class APILimitData(
@SerialName("getImageLimit") val getImageLimit: APILimit,
)
@Serializable
data class APILimit(
val limit: Int,
val usage: Int,
val resetInSeconds: String,
@SerialName("__typename") val typeName: String,
) )

View File

@ -1,15 +0,0 @@
package eu.kanade.tachiyomi.extension.zh.komiic
import kotlin.math.abs
/**
* 簡化數字顯示
*/
fun simplifyNumber(num: Int): String {
return when {
abs(num) < 1000 -> "$num"
abs(num) < 10000 -> "${num / 1000}"
abs(num) < 100000000 -> "${num / 10000}"
else -> "${num / 100000000}"
}
}