From 3b64f94ff808328c40516a56884c228461e66b08 Mon Sep 17 00:00:00 2001 From: Hualiang <78242797+hualiong@users.noreply.github.com> Date: Tue, 8 Jul 2025 00:27:34 +0800 Subject: [PATCH] Komiic: Add search filter (#9531) * 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 * Format code * replace parse method * optimize check API limit * modify config summary * add search filter * add getChapterUrl() * refactor Query.kt * use filters.firstInstance() * nothing * Replace require() with check() --- src/zh/komiic/build.gradle | 2 +- .../zh/komiic/{Response.kt => Entity.kt} | 6 +- .../tachiyomi/extension/zh/komiic/Filters.kt | 45 +++++++ .../tachiyomi/extension/zh/komiic/Komiic.kt | 72 ++++++----- .../tachiyomi/extension/zh/komiic/Payload.kt | 10 +- .../extension/zh/komiic/Preferences.kt | 4 +- .../tachiyomi/extension/zh/komiic/Queries.kt | 90 -------------- .../tachiyomi/extension/zh/komiic/Query.kt | 115 ++++++++++++++++++ 8 files changed, 214 insertions(+), 130 deletions(-) rename src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/{Response.kt => Entity.kt} (94%) create mode 100644 src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Filters.kt delete mode 100644 src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Queries.kt create mode 100644 src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Query.kt diff --git a/src/zh/komiic/build.gradle b/src/zh/komiic/build.gradle index 4370221d3..eb0dee562 100644 --- a/src/zh/komiic/build.gradle +++ b/src/zh/komiic/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'Komiic' extClass = '.Komiic' - extVersionCode = 2 + extVersionCode = 3 isNsfw = true } diff --git a/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Response.kt b/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Entity.kt similarity index 94% rename from src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Response.kt rename to src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Entity.kt index 2dae12c65..b881f291e 100644 --- a/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Response.kt +++ b/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Entity.kt @@ -17,7 +17,7 @@ class Result(val result: T) class MultiResult(val result1: T, val result2: V) @Serializable -data class ComicItem(val id: String, val name: String) +data class Item(val id: String, val name: String) @Serializable data class Comic( @@ -26,8 +26,8 @@ data class Comic( val description: String, val status: String, val imageUrl: String, - var authors: List, - val categories: List, + var authors: List, + val categories: List, ) { fun toSManga() = SManga.create().apply { url = "/comic/$id" diff --git a/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Filters.kt b/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Filters.kt new file mode 100644 index 000000000..13970c07b --- /dev/null +++ b/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Filters.kt @@ -0,0 +1,45 @@ +package eu.kanade.tachiyomi.extension.zh.komiic + +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.source.model.FilterList + +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 "同人", + ) + return FilterList( + Filter.Header("過濾條件(搜索關鍵字時無效)"), + CategoryFilter(categories), + StatusFilter(), + SortFilter(), + ) +} + +class Category(val id: String, name: String) : Filter.CheckBox(name) + +class CategoryFilter(categories: Map) : + Filter.Group("類型(篩選同時包含全部所選標簽的漫畫)", categories.map { Category(it.key, it.value) }) { + val selected get() = state.filter(Category::state).map(Category::id) +} + +class StatusFilter : Filter.Select("狀態", arrayOf("全部", "連載", "完結")) { + val value get() = arrayOf("", "ONGOING", "END")[state] +} + +class SortFilter : Filter.Select("排序", arrayOf("更新", "觀看數", "喜愛數")) { + val value get() = arrayOf("DATE_UPDATED", "VIEWS", "FAVORITE_COUNT")[state] +} diff --git a/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Komiic.kt b/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Komiic.kt index 42b9b15f6..ad158cd8c 100644 --- a/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Komiic.kt +++ b/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Komiic.kt @@ -10,7 +10,8 @@ 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.getPreferences +import keiyoushi.utils.firstInstance +import keiyoushi.utils.getPreferencesLazy import keiyoushi.utils.parseAs import keiyoushi.utils.toJsonString import keiyoushi.utils.tryParse @@ -31,7 +32,7 @@ class Komiic : HttpSource(), ConfigurableSource { override val client = network.cloudflareClient private val apiUrl = "$baseUrl/api/query" - private val preferences = getPreferences() + private val preferences by getPreferencesLazy() override fun setupPreferenceScreen(screen: PreferenceScreen) { preferencesInternal(screen.context).forEach(screen::addPreference) @@ -57,8 +58,8 @@ class Komiic : HttpSource(), ConfigurableSource { * Search the comic based on the ID. */ private fun comicByIDRequest(id: String): Request { - val variables = Variables().set("comicId", id).build() - val payload = Payload("comicById", variables, QUERY_COMIC_BY_ID) + val variables = Variables().field("comicId", id).build() + val payload = Payload(Query.COMIC_BY_ID, variables) return POST(apiUrl, headers, payload.toRequestBody()) } @@ -87,11 +88,9 @@ class Komiic : HttpSource(), ConfigurableSource { // 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) + 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()) } @@ -104,11 +103,9 @@ class Komiic : HttpSource(), ConfigurableSource { // 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) + 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()) } @@ -116,19 +113,23 @@ class Komiic : HttpSource(), ConfigurableSource { // Search Manga ================================================================================ - override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - val variables = Variables().set("keyword", query).build() - val payload = Payload("searchComicAndAuthorQuery", variables, QUERY_SEARCH) - return POST(apiUrl, headers, payload.toRequestBody()) - } + override fun getFilterList() = buildFilterList() - override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { - return if (query.startsWith(PREFIX_ID_SEARCH)) { - client.newCall(comicByIDRequest(query.substringAfter(PREFIX_ID_SEARCH))) - .asObservableSuccess() - .map(::parseComicByID) + 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 { - super.fetchSearchManga(page, query, filters) + val categories = filters.firstInstance() + val status = filters.firstInstance() + val sort = filters.firstInstance() + 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()) } } @@ -138,6 +139,17 @@ class Komiic : HttpSource(), ConfigurableSource { return MangasPage(comics.map(Comic::toSManga), comics.size == PAGE_SIZE) } + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + if (query.startsWith(PREFIX_ID_SEARCH)) { + return client.newCall(comicByIDRequest(query.substringAfter(PREFIX_ID_SEARCH))) + .asObservableSuccess().map(::parseComicByID) + } else if (query.isNotBlank()) { + return super.fetchSearchManga(page, query, filters) + } + return client.newCall(searchMangaRequest(page, query, filters)) + .asObservableSuccess().map(::popularMangaParse) + } + // Manga Details =============================================================================== override fun getMangaUrl(manga: SManga) = baseUrl + manga.url @@ -154,8 +166,8 @@ class Komiic : HttpSource(), ConfigurableSource { override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url + "/images/all" override fun chapterListRequest(manga: SManga): Request { - val variables = Variables().set("comicId", manga.id).build() - val payload = Payload("chapterByComicId", variables, QUERY_CHAPTER) + 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()) } @@ -178,15 +190,15 @@ class Komiic : HttpSource(), ConfigurableSource { // Page List =================================================================================== override fun pageListRequest(chapter: SChapter): Request { - val variables = Variables().set("chapterId", chapter.id).build() - val payload = Payload("imagesByChapterId", variables, QUERY_PAGE_LIST) + 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()) } override fun pageListParse(response: Response): List { val res = response.parseAs>>() val check = preferences.getBoolean(CHECK_API_LIMIT_PREF, true) - require(!check || !res.data.result1) { "今日圖片讀取次數已達上限,請登录或明天再來!" } + 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}") diff --git a/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Payload.kt b/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Payload.kt index 2d4e96e95..0628f6b54 100644 --- a/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Payload.kt +++ b/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Payload.kt @@ -12,24 +12,26 @@ data class Payload( val operationName: String, val variables: T, val query: String, -) +) { + constructor(query: Query, variables: T) : this(query.operation, variables, query.body) +} @Serializable data class Pagination( val offset: Int, val orderBy: String, @EncodeDefault - val limit: Int = Komiic.PAGE_SIZE, - @EncodeDefault val status: String = "", @EncodeDefault val asc: Boolean = true, + @EncodeDefault + val limit: Int = Komiic.PAGE_SIZE, ) class Variables { val variableMap = mutableMapOf() - inline fun set(key: String, value: T): Variables { + inline fun field(key: String, value: T): Variables { variableMap[key] = Json.encodeToJsonElement(value) return this } diff --git a/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Preferences.kt b/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Preferences.kt index 848487bdc..c4c4a0a7e 100644 --- a/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Preferences.kt +++ b/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Preferences.kt @@ -18,8 +18,8 @@ fun preferencesInternal(context: Context) = arrayOf( }, SwitchPreferenceCompat(context).apply { key = CHECK_API_LIMIT_PREF - title = "自動檢查API受限" - summary = "點擊單個章節請求漫畫圖片時,自動檢查一次圖片API是否達到今日請求上限。若已達上限,則終止後續操作" + title = "自動檢查 API 狀態" + summary = "點擊單個章節請求漫畫圖片時,自動檢查一次圖片API是否達到今日請求上限。若已達上限,則終止後續操作(注:關閉后仍會檢查API,只是不再終止操作)" setDefaultValue(true) }, ) diff --git a/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Queries.kt b/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Queries.kt deleted file mode 100644 index 9bc699826..000000000 --- a/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Queries.kt +++ /dev/null @@ -1,90 +0,0 @@ -package eu.kanade.tachiyomi.extension.zh.komiic - -private fun buildQuery(body: String = "", queryAction: () -> String): String { - return queryAction().trimIndent() - .replace("#{body}", body.trimIndent()) - .replace("%", "$") -} - -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!) { - result: hotComics(pagination: %pagination) #{body} - } - """ -} - -val QUERY_RECENT_UPDATE = buildQuery(COMIC_BODY) { - """ - query recentUpdate(%pagination: Pagination!) { - result: recentUpdate(pagination: %pagination) #{body} - } - """ -} - -val QUERY_SEARCH = buildQuery(COMIC_BODY) { - """ - query searchComicAndAuthorQuery(%keyword: String!) { - result: searchComicsAndAuthors(keyword: %keyword) { - result: comics #{body} - } - } - """ -} - -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!) { - result: chaptersByComicId(comicId: %comicId) { - id - serial - type - size - dateCreated - } - } - """ -} - -val QUERY_PAGE_LIST = buildQuery { - """ - query imagesByChapterId(%chapterId: ID!) { - result1: reachedImageLimit, - result2: imagesByChapterId(chapterId: %chapterId) { - id - kid - height - width - } - } - """ -} - -// val QUERY_API_LIMIT = buildQuery { "query reachedImageLimit { result: reachedImageLimit }" } diff --git a/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Query.kt b/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Query.kt new file mode 100644 index 000000000..7f2beb88a --- /dev/null +++ b/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Query.kt @@ -0,0 +1,115 @@ +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("%", "$") + } +}