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:
parent
3070ed4967
commit
2d0e57517e
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Komiic'
|
extName = 'Komiic'
|
||||||
extClass = '.Komiic'
|
extClass = '.Komiic'
|
||||||
extVersionCode = 1
|
extVersionCode = 2
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
}
|
||||||
|
|
||||||
/**
|
// Customize ===================================================================================
|
||||||
* 解析漫畫列表
|
|
||||||
* 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 ->
|
companion object {
|
||||||
comic.toSManga()
|
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
val hasNextPage = comics.size == PAGE_SIZE
|
|
||||||
return MangasPage(entries, hasNextPage)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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,116 +133,68 @@ 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
|
|
||||||
return MangasPage(entries, hasNextPage)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Comic details
|
// Manga Details ===============================================================================
|
||||||
override fun mangaDetailsRequest(manga: SManga) = comicByIDRequest(manga.url.substringAfterLast("/"))
|
|
||||||
|
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url
|
||||||
|
|
||||||
|
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} 話"
|
val comicUrl = response.request.url.fragment!!
|
||||||
"book" -> "第 ${chapter.serial} 卷"
|
return items.map { it.toSChapter(comicUrl, DATE_FORMAT::tryParse) }
|
||||||
else -> chapter.serial
|
|
||||||
}
|
|
||||||
date_upload = parseDate(chapter.dateCreated)
|
|
||||||
chapter_number = chapter.serial.toFloatOrNull() ?: -1f
|
|
||||||
}
|
|
||||||
}.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 pages.mapIndexed { index, image ->
|
return res.data.result2.mapIndexed { index, image ->
|
||||||
Page(
|
Page(index, "$chapterUrl/page/$index", "$baseUrl/api/image/${image.kid}")
|
||||||
index,
|
|
||||||
"${chapterUrl.substringBeforeLast("/")}/$index",
|
|
||||||
"$baseUrl/api/image/${image.kid}",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Image =======================================================================================
|
||||||
|
|
||||||
override fun imageRequest(page: Page): Request {
|
override fun imageRequest(page: Page): Request {
|
||||||
return super.imageRequest(page).newBuilder()
|
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'")
|
||||||
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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,
|
|
||||||
)
|
|
||||||
|
@ -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)
|
||||||
|
},
|
||||||
|
)
|
@ -1,187 +1,90 @@
|
|||||||
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 =
|
||||||
"""
|
"""
|
||||||
query hotComics(%pagination: Pagination!) {
|
{
|
||||||
hotComics(pagination: %pagination) {
|
id
|
||||||
id
|
title
|
||||||
title
|
description
|
||||||
status
|
status
|
||||||
year
|
imageUrl
|
||||||
imageUrl
|
authors {
|
||||||
authors {
|
id
|
||||||
id
|
name
|
||||||
name
|
}
|
||||||
__typename
|
categories {
|
||||||
}
|
id
|
||||||
categories {
|
name
|
||||||
id
|
}
|
||||||
name
|
}
|
||||||
__typename
|
"""
|
||||||
}
|
|
||||||
dateUpdated
|
val QUERY_HOT_COMICS = buildQuery(COMIC_BODY) {
|
||||||
monthViews
|
"""
|
||||||
views
|
query hotComics(%pagination: Pagination!) {
|
||||||
favoriteCount
|
result: hotComics(pagination: %pagination) #{body}
|
||||||
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 chapterByComicId(%comicId: ID!) {
|
query comicById(%comicId: ID!) {
|
||||||
chaptersByComicId(comicId: %comicId) {
|
result: comicById(comicId: %comicId) #{body}
|
||||||
id
|
}
|
||||||
serial
|
|
||||||
type
|
|
||||||
dateCreated
|
|
||||||
dateUpdated
|
|
||||||
size
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
val QUERY_COMIC_BY_ID = buildQuery {
|
val QUERY_CHAPTER = buildQuery {
|
||||||
"""
|
"""
|
||||||
query comicById(%comicId: ID!) {
|
query chapterByComicId(%comicId: ID!) {
|
||||||
comicById(comicId: %comicId) {
|
result: chaptersByComicId(comicId: %comicId) {
|
||||||
id
|
id
|
||||||
title
|
serial
|
||||||
status
|
type
|
||||||
year
|
size
|
||||||
imageUrl
|
dateCreated
|
||||||
authors {
|
}
|
||||||
id
|
}
|
||||||
name
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
categories {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
dateCreated
|
|
||||||
dateUpdated
|
|
||||||
views
|
|
||||||
favoriteCount
|
|
||||||
lastBookUpdate
|
|
||||||
lastChapterUpdate
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
val QUERY_PAGE_LIST = buildQuery {
|
val QUERY_PAGE_LIST = buildQuery {
|
||||||
"""
|
"""
|
||||||
query imagesByChapterId(%chapterId: ID!) {
|
query imagesByChapterId(%chapterId: ID!) {
|
||||||
imagesByChapterId(chapterId: %chapterId) {
|
result1: reachedImageLimit,
|
||||||
id
|
result2: imagesByChapterId(chapterId: %chapterId) {
|
||||||
kid
|
id
|
||||||
height
|
kid
|
||||||
width
|
height
|
||||||
__typename
|
width
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
val QUERY_API_LIMIT = buildQuery {
|
// val QUERY_API_LIMIT = buildQuery { "query reachedImageLimit { result: reachedImageLimit }" }
|
||||||
"""
|
|
||||||
query getImageLimit {
|
|
||||||
getImageLimit {
|
|
||||||
limit
|
|
||||||
usage
|
|
||||||
resetInSeconds
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
|
@ -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,
|
|
||||||
)
|
)
|
||||||
|
@ -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}億"
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user