From 4682cc87522e13550f754c630aafd3151c2d9be1 Mon Sep 17 00:00:00 2001 From: beerpsi <92439990+beerpiss@users.noreply.github.com> Date: Fri, 9 Feb 2024 00:11:18 +0700 Subject: [PATCH] Add Comikey (#1110) * Add Comikey * Remove logging * i18n * Comikey Brasil, paid chapters toggle, use other chapter endpoint * Don't parse author/artist in searchMangaFromElement * makeEpisodeSlug private * Move gundamUrl outside of class constructor * paginate latest * paginate search * Properly distinguish i18n keys from normal messages in WebView script * Parse statuses better * Add genre for entry format * remove unnecessary getChapterUrl * Fix status on BR * ACTUALLY fix status on BR * Fix more Comikey Brasil stupidity * Validate that manifestUrl is valid * Revert "Validate that manifestUrl is valid" This reverts commit d744fd42b45ae46baf48308ec3f354546d1452af. * Proper i18n in WebView script * Add explanation for weird binding * Move helper functions to bottom * Support signing in through WebView * Fix chapter list when signed in * Properly filter locked chapters * Remove WebView logging --- src/all/comikey/AndroidManifest.xml | 22 + .../assets/i18n/messages_en.properties | 23 + .../assets/i18n/messages_pt_br.properties | 16 + src/all/comikey/assets/webview-script.js | 54 +++ src/all/comikey/build.gradle | 11 + .../comikey/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2683 bytes .../comikey/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1393 bytes .../comikey/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3301 bytes .../comikey/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 5958 bytes .../res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 7413 bytes .../extension/all/comikey/Comikey.kt | 405 ++++++++++++++++++ .../extension/all/comikey/ComikeyFactory.kt | 13 + .../extension/all/comikey/ComikeyFilters.kt | 68 +++ .../extension/all/comikey/ComikeyModels.kt | 85 ++++ .../all/comikey/ComikeyUrlActivity.kt | 35 ++ 15 files changed, 732 insertions(+) create mode 100644 src/all/comikey/AndroidManifest.xml create mode 100644 src/all/comikey/assets/i18n/messages_en.properties create mode 100644 src/all/comikey/assets/i18n/messages_pt_br.properties create mode 100644 src/all/comikey/assets/webview-script.js create mode 100644 src/all/comikey/build.gradle create mode 100644 src/all/comikey/res/mipmap-hdpi/ic_launcher.png create mode 100644 src/all/comikey/res/mipmap-mdpi/ic_launcher.png create mode 100644 src/all/comikey/res/mipmap-xhdpi/ic_launcher.png create mode 100644 src/all/comikey/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 src/all/comikey/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 src/all/comikey/src/eu/kanade/tachiyomi/extension/all/comikey/Comikey.kt create mode 100644 src/all/comikey/src/eu/kanade/tachiyomi/extension/all/comikey/ComikeyFactory.kt create mode 100644 src/all/comikey/src/eu/kanade/tachiyomi/extension/all/comikey/ComikeyFilters.kt create mode 100644 src/all/comikey/src/eu/kanade/tachiyomi/extension/all/comikey/ComikeyModels.kt create mode 100644 src/all/comikey/src/eu/kanade/tachiyomi/extension/all/comikey/ComikeyUrlActivity.kt diff --git a/src/all/comikey/AndroidManifest.xml b/src/all/comikey/AndroidManifest.xml new file mode 100644 index 000000000..e5689c3e4 --- /dev/null +++ b/src/all/comikey/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + diff --git a/src/all/comikey/assets/i18n/messages_en.properties b/src/all/comikey/assets/i18n/messages_en.properties new file mode 100644 index 000000000..a5afeaf8c --- /dev/null +++ b/src/all/comikey/assets/i18n/messages_en.properties @@ -0,0 +1,23 @@ +sort_by=Sort by +sort_last_updated=Last updated +sort_name=Name +sort_popularity=Popularity +sort_chapter_count=Chapter count +filter_by=Filter by +all=All +manga=Manga +webtoon=Webtoon +new=New +complete=Complete +exclusive=Exclusive +simulpub=Simulpub +search_use_two_characters=Please use at least 2 characters when searching by title. +pref_hide_locked_chapters=Hide locked chapters +pref_hide_locked_chapters_summary=App restart required +error_timed_out_decrypting_image_links=Timed out decrypting image links +error_locked_chapter_unlock_in_webview=Locked chapter, unlock in WebView. +error_open_in_webview_then_try_again=Open chapter in WebView, then try again +error_token_expired=token expired +error_token_not_found=token not found +error_webview_script_not_found=WebView script not found. +error_unknown_error=Unknown error diff --git a/src/all/comikey/assets/i18n/messages_pt_br.properties b/src/all/comikey/assets/i18n/messages_pt_br.properties new file mode 100644 index 000000000..e1f09cd2a --- /dev/null +++ b/src/all/comikey/assets/i18n/messages_pt_br.properties @@ -0,0 +1,16 @@ +sort_by=Ordenar por +sort_last_updated=Última atualização +sort_name=Nome +sort_popularity=Popularidade +sort_chapter_count=Capítulos +filter_by=Filtrar por +all=Todos +manga=Manga +webtoon=Webtoon +new=Novo +complete=Completo +exclusive=Exclusivo +simulpub=Simulpub +search_use_two_characters=Use pelo menos 2 caracteres ao pesquisar por título. +pref_hide_locked_chapters=Ocultar capítulos bloqueados +pref_hide_locked_chapters_summary=Se requiere reiniciar la app diff --git a/src/all/comikey/assets/webview-script.js b/src/all/comikey/assets/webview-script.js new file mode 100644 index 000000000..4f3e39756 --- /dev/null +++ b/src/all/comikey/assets/webview-script.js @@ -0,0 +1,54 @@ +document.addEventListener("DOMContentLoaded", (e) => { + // This is intentional. Simply binding `_` to `window.__interface__.gettext` will + // throw an error: "Java bridge method can't be invoked on a non-injected object". + const _ = (key) => window.__interface__.gettext(key); + + if (document.querySelector("#unlock-full")) { + window.__interface__.passError(_("error_locked_chapter_unlock_in_webview")); + } +}); + +document.addEventListener( + "you-right-now:reeeeeee", + async (e) => { + const _ = (key) => window.__interface__.gettext(key); + + try { + const db = await new Promise((resolve, reject) => { + const request = indexedDB.open("firebase-app-check-database"); + + request.onsuccess = (event) => resolve(event.target.result); + request.onerror = (event) => reject(event.target); + }); + + const act = await new Promise((resolve, reject) => { + db.onerror = (event) => reject(event.target); + + const request = db.transaction("firebase-app-check-store").objectStore("firebase-app-check-store").getAll(); + + request.onsuccess = (event) => { + const entries = event.target.result; + db.close(); + + if (entries.length < 1) { + window.__interface__.passError(`${_("error_open_in_webview_then_try_again")} (${_("error_token_not_found")}).`); + } + + const value = entries[0].value; + + if (value.expireTimeMillis < Date.now()) { + window.__interface__.passError(`${_("error_open_in_webview_then_try_again")} (${_("error_token_expired")}).`); + } + + resolve(value.token) + } + }); + + const manifest = JSON.parse(document.querySelector("#lmao-init").textContent).manifest; + window.__interface__.passPayload(manifest, act, await e.detail); + } catch (e) { + window.__interface__.passError(`${_("error_unknown_error")}: ${e}`); + } + }, + { once: true }, +); diff --git a/src/all/comikey/build.gradle b/src/all/comikey/build.gradle new file mode 100644 index 000000000..d417b8dd2 --- /dev/null +++ b/src/all/comikey/build.gradle @@ -0,0 +1,11 @@ +ext { + extName = "Comikey" + extClass = ".ComikeyFactory" + extVersionCode = 1 +} + +apply from: "$rootDir/common.gradle" + +dependencies { + implementation(project(":lib:i18n")) +} diff --git a/src/all/comikey/res/mipmap-hdpi/ic_launcher.png b/src/all/comikey/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..f1ed4f5195715311ed5d1c8c86e8333761ebf6b5 GIT binary patch literal 2683 zcmV->3WW8EP)*{|3tw>aHT@XEr)_?u7R3+hgFs0l0VXo;>HT?kPC>B#;*&Bp6lZDvQN}apT4yGSciq z1ZveXW7Mb-(oudG7phJm#^+kPD!W@;Tr66*Zi>2f%|wI?X7udY1(})e{8I24z6(@= zII7O7P}CDAj-W}Cx~N~@Ohm|Sy?SO09oio`IobXiNPQQ`3vpF(l`h<9 zsg@K|sXs+0fc_?l#Kb4Kb?YPs3^1X8 zKOF-Hgk$gCWw6_=-qP1dO{x+H84;#Zt={Q$GNo2x@ZfL^9%N!cu7g*v;xJ`OOS(#z z7&|r!&z{|_j6)0vxZ&H=drV$N-Nk$_*qND0m_98E zW5<{Xo%Lt|91a_WOQ$Gp__jkMbc`EoLVW!Bs`^$Xped$OEZs;=eS(P-!ZBgIj`75q zGwWn$vs#N06Eh9t$LWNIam4Q36MX)7*A;r$XUpX2-Po;`=)*@*3r=;F=K{KXjr(gA>O>XTM=Oc;`q@)3NBJ{lB_EhO93)6 zp0VyEup_0RwA9J2)d{m^X%fqqH^nCzFL>NR_t@oDVfIn2Lw<0OabaY*grO^LA?D+u z*s#7GR;_G;70Vl8&FU7|y=wqnQ;Th0lUTGc65F=+z{ZUoxVAmEZ0^RcHUY`W_j#`} zUqAo;;8ILFBsTT{mMyJ=g$u+i^I2#To{#6x^F)}$lEt-npMz3tqhDS!KD-pm%6fy% zo7!OUVjYVY=~%t0A@=PXCP0_8B5{*A?t(^lHDD|6i;tuGWBazQJReS1g$dAs{W(D9 zsD4VRB2eH{?B3m-Shh^Z(xp09tr8t71|=nV5_x%PIDKk{I7et$vP8q>%PWKkJfF)N zm4d^ENAY+V5lZnQc`UiIgJShK$cU4e4s=2{tX!c>Y}?vG67*bu?%XWaBh8IR4-c11 z&ns7!@pvefFOxua)@$XlLm~Vz2dO)$q&OSv)|y$2HHm`<`pZJ=x&G+U;Y>Ttjm*sF z9s=3vsD*S+s08mtfs>TlyFvs$#WFGyNJ^7fyGFy+D+}N2ZhK-n%5wzQ=}fI0_dOgA z3oe|WPH{secJ64#DLTCZag+rLe2P7Lc9gAJBXo)T_qTYiC)rd)?{3a>VFDDOp}R{7 z*la~Odv-k5uL~tYaA1E|)~76$v$u%3YI~ zc%Fk|9cqCf8T)#F;rkz4s1xJeAzP%7Tc4*k?LgJe<<*s@8 z@(2Zrz>e*q*iOX9Zz^|>*RN0Vc!)f|Yo`fMofiekpTy&7DgKqf-$hY}%TOEJa1Cs?=-*1^pi6H)54DhLx?DdwJa z3i4m$^{WGT{CFcCKU$9`+&+D>38zjrqgzD!gvkB)xW#yIe=f&?*@%6xfa}*IJ?#>l z4vT^~L6lI%V4a5N&2{6pRO^XGdxoz4QI9n8q@8>=$}w~H4q_B8N^IvL+; z)Tq(lsPHYg!rv)Y-I0`(bW3m)T!~7@*NLCjuU|j(8Go2#u~-Tn4o8{YZU?szTi5;< zf@>6xS1ZZ{7r{wz6C5R11MwB&+r%&b^Pm6xWBvN|!_S;Kv+~@za~pygf`i~9I0O+O}=mwzFdknOWO5$F^-H!|1$wyHdT*-0mJc{Z(IZZ)f+M zIrneA^MCzc|JPgVUBdfTKIlN+7qmzztycTF!C>&RSS&gMXtlz-h{WIMs1`Rjw=Y=r ze^6gv?<36q~2om<-q!&z^>tmpgj(>PF~}o;|xEA|hN#O6&#z*bvCx z$kV2cN56jE1p4;vhNn*-iE}cVUU34F4J}(b2ZIN7$Dl#oFmd8A)Ya*g^p3OyL~+s^ zTBM*L55d9SIC8`vMMVWln_gZ5wB+_gLqk2TUOkPcPw&1o-blvp+_{3wmrtO&x{}pI zt0us*goyAv7&opfCQa)3(wv*yZj2ejW9rntsH`kwGoU;H={<4r;t@=q)D?>tj}Ye) z6C+7KbjHL9Jg!|k$=cM46Oh%=A;FHAHmwUbY?xv_E;M7^x=ENag~ygHGhs9uB{s40 z1enEB4jx>JS+ly}z=6fW_e+`g*;bb0b9w zNH$Vlo{weAIujF}apOi1dFQc|uz&wlxhI8flfc`|CMG_{s+FAul9Hal)pY?@uH>+C z1&89|OgU$$?VG@p$EUGwZD#_Hii!d}dUOJ7*KhH#-N$3+P7d0jg>tVelo@z|7LDk1WqA5Ti*x5T?rwviImo{NXNAYX1KbPP!PTWd>gp<( zt}6hveF8Ub?1rNw4^Phl$j%N$US1TgT-hd!IXG~5^vDajxeX-M=Q|K?t~~DD)rj*+NtfX2!U>d=Bnjh9O?tREs|7+s{aQLzX`cl2 zx)OMK^6>QFkdk~^+$JL2Ul=0@!fEMr#qe-f3nV6-Wf9-@2^1AY!qk2d(AZc(=8STMD?6EEG$Vf2>+=vFuR%=oP8bY&hGuQU z1Q?px=GdF(+up!;2J)wqng9SB-<22DM76OzFZ(JrHT4Jp#R;@#QB71Ep~d>>mtTJQ zAtEB;WMyS#eQj+mtU#yJy~vx!#qX^}RaI3z)j~Bw;Uzoge+>CQz}FBrG+00000NkvXXu0mjf-I1c& literal 0 HcmV?d00001 diff --git a/src/all/comikey/res/mipmap-xhdpi/ic_launcher.png b/src/all/comikey/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..3af6cb42aaefd724be0448772d4c4a433eaf82d9 GIT binary patch literal 3301 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 literal 0 HcmV?d00001 diff --git a/src/all/comikey/res/mipmap-xxhdpi/ic_launcher.png b/src/all/comikey/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..ed4cd8b8e5fe9a4aa2ae3acb590e4ffa78104646 GIT binary patch literal 5958 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 literal 0 HcmV?d00001 diff --git a/src/all/comikey/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/comikey/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..68413a8caef3bba3ab2566afde1266bc7f28aa42 GIT binary patch literal 7413 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.getSharedPreferences("source_$id", 0x0000) + } + + override fun popularMangaRequest(page: Int) = GET("$baseUrl/comics/?order=-views&page=$page", headers) + + override fun popularMangaSelector() = searchMangaSelector() + + override fun popularMangaFromElement(element: Element) = searchMangaFromElement(element) + + override fun popularMangaNextPageSelector() = searchMangaNextPageSelector() + + override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/comics/?page=$page", headers) + + override fun latestUpdatesSelector() = searchMangaSelector() + + override fun latestUpdatesFromElement(element: Element) = searchMangaFromElement(element) + + override fun latestUpdatesNextPageSelector() = searchMangaNextPageSelector() + + override fun fetchSearchManga( + page: Int, + query: String, + filters: FilterList, + ): Observable { + return if (query.startsWith(PREFIX_SLUG_SEARCH)) { + val slug = query.removePrefix(PREFIX_SLUG_SEARCH) + val url = "/comics/$slug/" + + fetchMangaDetails(SManga.create().apply { this.url = url }) + .map { MangasPage(listOf(it), false) } + } else { + super.fetchSearchManga(page, query, filters) + } + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = "$baseUrl/comics/".toHttpUrl().newBuilder().apply { + if (page > 1) { + addQueryParameter("page", page.toString()) + } + + if (query.length >= 2) { + addQueryParameter("q", query) + } + + filters.ifEmpty { getFilterList() } + .filterIsInstance() + .forEach { it.addToUri(this) } + }.build() + + return GET(url, headers) + } + + override fun searchMangaSelector() = "div.series-listing[data-view=list] > ul > li" + + override fun searchMangaFromElement(element: Element) = SManga.create().apply { + element.selectFirst("div.series-data span.title a")!!.let { + setUrlWithoutDomain(it.attr("href")) + title = it.text() + } + + description = element.select("div.excerpt p").text() + + "\n\n" + + element.select("div.desc p").text() + genre = element.select("ul.category-listing li a").joinToString { it.text() } + thumbnail_url = element.selectFirst("div.image picture img")?.absUrl("src") + } + + override fun searchMangaNextPageSelector() = "ul.pagination li.next-page:not(.disabled)" + + override fun mangaDetailsParse(document: Document): SManga { + val data = json.decodeFromString( + document.selectFirst("script#comic")!!.data(), + ) + + return SManga.create().apply { + url = data.link + title = data.name + author = data.author.joinToString { it.name } + artist = data.artist.joinToString { it.name } + description = "\"${data.excerpt}\"\n\n${data.description}" + thumbnail_url = "$baseUrl${data.fullCover}" + status = when (data.updateStatus) { + // HACK: Comikey Brasil + 0 -> when { + data.updateText.startsWith("toda", true) -> SManga.ONGOING + listOf("em pausa", "hiato").any { data.updateText.startsWith(it, true) } -> SManga.ON_HIATUS + else -> SManga.UNKNOWN + } + 1 -> SManga.COMPLETED + 3 -> SManga.ON_HIATUS + in (4..14) -> SManga.ONGOING // daily, weekly, bi-weekly, monthly, every day of the week + else -> SManga.UNKNOWN + } + genre = buildList(data.tags.size + 1) { + addAll(data.tags.map { it.name }) + + when (data.format) { + 0 -> add("Comic") + 1 -> add("Manga") + 2 -> add("Webtoon") + else -> {} + } + }.joinToString() + } + } + + override fun chapterListParse(response: Response): List { + val document = response.asJsoup() + val mangaSlug = response.request.url.pathSegments[1] + val mangaData = json.decodeFromString( + document.selectFirst("script#comic")!!.data(), + ) + val defaultChapterPrefix = if (mangaData.format == 2) "episode" else "chapter" + + val chapterUrl = gundamUrl.toHttpUrl().newBuilder().apply { + val mangaId = response.request.url.pathSegments[2] + val gundamToken = document.selectFirst("script:containsData(GUNDAM.token)") + ?.data() + ?.substringAfter("= \"") + ?.substringBefore("\";") + + if (gundamToken != null) { + addPathSegment("comic") + } else { + addPathSegment("comic.public") + } + + addPathSegment(mangaId) + addPathSegment("episodes") + addQueryParameter("language", lang.lowercase()) + gundamToken?.let { addQueryParameter("token", gundamToken) } + }.build() + val data = json.decodeFromString( + client.newCall(GET(chapterUrl, headers)) + .execute() + .body + .string(), + ) + val currentTime = System.currentTimeMillis() + + return data.episodes + .filter { it.readable || !hideLockedChapters } + .map { + SChapter.create().apply { + url = "/read/$mangaSlug/${makeEpisodeSlug(it, defaultChapterPrefix)}/" + name = buildString { + append(it.title) + + if (it.subtitle != null) { + append(": ") + append(it.subtitle) + } + } + chapter_number = it.number + date_upload = try { + dateFormat.parse(it.releasedAt)!!.time + } catch (e: Exception) { + 0L + } + } + } + .filter { it.date_upload <= currentTime } + .reversed() + } + + override fun chapterListSelector() = throw UnsupportedOperationException() + + override fun chapterFromElement(element: Element) = throw UnsupportedOperationException() + + override fun fetchPageList(chapter: SChapter): Observable> { + return Observable.fromCallable { + pageListParse(chapter) + } + } + + override fun pageListParse(document: Document) = throw UnsupportedOperationException() + + @SuppressLint("SetJavaScriptEnabled") + private fun pageListParse(chapter: SChapter): List { + val interfaceName = randomString() + + val handler = Handler(Looper.getMainLooper()) + val latch = CountDownLatch(1) + val jsInterface = JsInterface(latch, json, intl) + var webView: WebView? = null + + handler.post { + val innerWv = WebView(Injekt.get()) + + webView = innerWv + innerWv.settings.domStorageEnabled = true + innerWv.settings.javaScriptEnabled = true + innerWv.settings.blockNetworkImage = true + innerWv.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + innerWv.addJavascriptInterface(jsInterface, interfaceName) + + innerWv.webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + view?.evaluateJavascript(webviewScript.replace("__interface__", interfaceName)) {} + } + } + + innerWv.loadUrl("$baseUrl${chapter.url}") + } + + latch.await(30, TimeUnit.SECONDS) + handler.post { webView?.destroy() } + + if (latch.count == 1L) { + throw Exception(intl["error_timed_out_decrypting_image_links"]) + } + + if (jsInterface.error.isNotEmpty()) { + throw Exception(jsInterface.error) + } + + val manifestUrl = jsInterface.manifestUrl.toHttpUrl() + + return jsInterface.images.mapIndexed { i, it -> + val href = it.alternate.firstOrNull { it.type == "image/webp" }?.href + ?: it.href + val url = manifestUrl.newBuilder().apply { + removePathSegment(manifestUrl.pathSegments.size - 1) + addPathSegments(href) + addQueryParameter("act", jsInterface.act) + }.build() + + Page(i, imageUrl = url.toString()) + } + } + + override fun imageUrlParse(document: Document) = throw UnsupportedOperationException() + + override fun getFilterList() = getComikeyFilters(intl) + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + SwitchPreferenceCompat(screen.context).apply { + key = PREF_HIDE_LOCKED_CHAPTERS + title = intl["pref_hide_locked_chapters"] + summary = intl["pref_hide_locked_chapters_summary"] + setDefaultValue(false) + + setOnPreferenceChangeListener { _, newValue -> + preferences.edit().putBoolean(PREF_HIDE_LOCKED_CHAPTERS, newValue as Boolean).commit() + } + }.also(screen::addPreference) + } + + private val hideLockedChapters by lazy { + preferences.getBoolean(PREF_HIDE_LOCKED_CHAPTERS, false) + } + + private val webviewScript by lazy { + javaClass.getResource("/assets/webview-script.js")?.readText() + ?: throw Exception(intl["error_webview_script_not_found"]) + } + + private fun randomString() = buildString(15) { + val charPool = ('a'..'z') + ('A'..'Z') + + for (i in 0 until 15) { + append(charPool.random()) + } + } + + private fun makeEpisodeSlug(episode: ComikeyEpisode, defaultChapterPrefix: String): String { + val e4pid = episode.id.split("-", limit = 2).last() + val chapterPrefix = if (defaultChapterPrefix == "chapter" && lang != defaultLanguage) { + when (lang) { + "es" -> "capitulo-espanol" + "pt-br" -> "capitulo-portugues" + "fr" -> "chapitre-francais" + "id" -> "bab-bahasa" + else -> "chapter" + } + } else { + defaultChapterPrefix + } + + return "$e4pid/$chapterPrefix-${episode.number.toString().replace(".", "-")}" + } + + private class JsInterface( + private val latch: CountDownLatch, + private val json: Json, + private val intl: Intl, + ) { + var images: List = emptyList() + private set + + var manifestUrl: String = "" + private set + + var act: String = "" + private set + + var error: String = "" + private set + + @JavascriptInterface + @Suppress("UNUSED") + fun gettext(key: String): String { + return intl[key] + } + + @JavascriptInterface + @Suppress("UNUSED") + fun passError(msg: String) { + error = msg + latch.countDown() + } + + @JavascriptInterface + @Suppress("UNUSED") + fun passPayload(manifestUrl: String, act: String, rawData: String) { + this.manifestUrl = manifestUrl + this.act = act + images = json.decodeFromString(rawData).readingOrder + + latch.countDown() + } + } + + companion object { + internal const val PREFIX_SLUG_SEARCH = "slug:" + internal const val PREF_HIDE_LOCKED_CHAPTERS = "hide_locked_chapters" + } +} diff --git a/src/all/comikey/src/eu/kanade/tachiyomi/extension/all/comikey/ComikeyFactory.kt b/src/all/comikey/src/eu/kanade/tachiyomi/extension/all/comikey/ComikeyFactory.kt new file mode 100644 index 000000000..ff3de86b2 --- /dev/null +++ b/src/all/comikey/src/eu/kanade/tachiyomi/extension/all/comikey/ComikeyFactory.kt @@ -0,0 +1,13 @@ +package eu.kanade.tachiyomi.extension.all.comikey + +import eu.kanade.tachiyomi.source.SourceFactory + +class ComikeyFactory : SourceFactory { + override fun createSources() = listOf( + Comikey("en"), + Comikey("es"), + Comikey("id"), + Comikey("pt-BR"), + Comikey("pt-BR", "Comikey Brasil", "https://br.comikey.com", defaultLanguage = "pt-BR"), + ) +} diff --git a/src/all/comikey/src/eu/kanade/tachiyomi/extension/all/comikey/ComikeyFilters.kt b/src/all/comikey/src/eu/kanade/tachiyomi/extension/all/comikey/ComikeyFilters.kt new file mode 100644 index 000000000..108104533 --- /dev/null +++ b/src/all/comikey/src/eu/kanade/tachiyomi/extension/all/comikey/ComikeyFilters.kt @@ -0,0 +1,68 @@ +package eu.kanade.tachiyomi.extension.all.comikey + +import eu.kanade.tachiyomi.lib.i18n.Intl +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.source.model.FilterList +import okhttp3.HttpUrl + +fun getComikeyFilters(intl: Intl) = FilterList( + Filter.Header(intl["search_use_two_characters"]), + Filter.Separator(), + SortFilter(intl["sort_by"], getSortOptions(intl)), + TypeFilter(intl["filter_by"], getTypeOptions(intl)), +) + +fun getSortOptions(intl: Intl) = arrayOf( + intl["sort_last_updated"], + intl["sort_name"], + intl["sort_popularity"], + intl["sort_chapter_count"], +) + +fun getTypeOptions(intl: Intl) = arrayOf( + intl["all"], + intl["manga"], + intl["webtoon"], + intl["new"], + intl["complete"], + intl["exclusive"], + intl["simulpub"], +) + +interface UriFilter { + fun addToUri(builder: HttpUrl.Builder) +} + +class SortFilter(name: String, values: Array) : + Filter.Sort(name, values, Selection(2, false)), + UriFilter { + override fun addToUri(builder: HttpUrl.Builder) { + val state = this.state ?: return + val value = buildString { + if (!state.ascending) { + append("-") + } + + when (state.index) { + 0 -> append("updated") + 1 -> append("name") + 2 -> append("views") + 3 -> append("chapters") + } + } + + builder.addQueryParameter("order", value) + } +} + +class TypeFilter(name: String, values: Array) : + Filter.Select(name, values), + UriFilter { + override fun addToUri(builder: HttpUrl.Builder) { + if (state == 0) { + return + } + + builder.addQueryParameter("filter", values[state].lowercase()) + } +} diff --git a/src/all/comikey/src/eu/kanade/tachiyomi/extension/all/comikey/ComikeyModels.kt b/src/all/comikey/src/eu/kanade/tachiyomi/extension/all/comikey/ComikeyModels.kt new file mode 100644 index 000000000..02c4073ad --- /dev/null +++ b/src/all/comikey/src/eu/kanade/tachiyomi/extension/all/comikey/ComikeyModels.kt @@ -0,0 +1,85 @@ +package eu.kanade.tachiyomi.extension.all.comikey + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ComikeyComic( + val id: Int, + val link: String, + val name: String, + val author: List, + val artist: List, + val tags: List, + val description: String, + val excerpt: String, + val e4pid: String, + val format: Int, + val uslug: String, + @SerialName("full_cover") val fullCover: String, + @SerialName("update_status") val updateStatus: Int, + @SerialName("update_text") val updateText: String, +) + +@Serializable +data class ComikeyEpisodeListResponse( + val episodes: List = emptyList(), +) + +@Serializable +data class ComikeyEpisode( + val id: String, + val number: Float = 0F, + val title: String, + val subtitle: String? = null, + val releasedAt: String, + val availability: ComikeyEpisodeAvailability, + val finalPrice: Int = 0, + val owned: Boolean = false, +) { + val readable + get() = finalPrice == 0 || owned +} + +@Serializable +data class ComikeyEpisodeManifest( + val readingOrder: List, +) + +@Serializable +data class ComikeyPage( + val href: String, + val type: String, + val height: Int, + val width: Int, + val alternate: List, +) + +@Serializable +data class ComikeyAlternatePage( + val href: String, + val type: String, + val height: Int, + val width: Int, +) + +@Serializable +data class ComikeyEpisodeAvailability( + val purchaseEnabled: Boolean = false, +) + +@Serializable +data class ComikeyLmaoInitData( + val manifest: String, +) + +@Serializable +data class ComikeyNameWrapper( + val name: String, +) + +@Serializable +data class ComikeyAuthor( + val id: Int, + val name: String, +) diff --git a/src/all/comikey/src/eu/kanade/tachiyomi/extension/all/comikey/ComikeyUrlActivity.kt b/src/all/comikey/src/eu/kanade/tachiyomi/extension/all/comikey/ComikeyUrlActivity.kt new file mode 100644 index 000000000..68b2bfac3 --- /dev/null +++ b/src/all/comikey/src/eu/kanade/tachiyomi/extension/all/comikey/ComikeyUrlActivity.kt @@ -0,0 +1,35 @@ +package eu.kanade.tachiyomi.extension.all.comikey + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle +import android.util.Log +import kotlin.system.exitProcess + +class ComikeyUrlActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val pathSegments = intent?.data?.pathSegments + + if (pathSegments != null && pathSegments.size > 2) { + val intent = Intent().apply { + action = "eu.kanade.tachiyomi.SEARCH" + putExtra("query", "${Comikey.PREFIX_SLUG_SEARCH}${pathSegments[1]}/${pathSegments[2]}") + putExtra("filter", packageName) + } + + try { + startActivity(intent) + } catch (e: ActivityNotFoundException) { + Log.e("ComikeyUrlActivity", "Could not start activity", e) + } + } else { + Log.e("ComikeyUrlActivity", "Could not parse URI from intent $intent") + } + + finish() + exitProcess(0) + } +}