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
This commit is contained in:
parent
879eb629b1
commit
4682cc8752
22
src/all/comikey/AndroidManifest.xml
Normal file
22
src/all/comikey/AndroidManifest.xml
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<activity
|
||||
android:name=".all.comikey.ComikeyUrlActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
|
||||
<data android:host="comikey.com" />
|
||||
<data android:host="br.comikey.com" />
|
||||
<data
|
||||
android:pathPattern="/comics/..*/..*/"
|
||||
android:scheme="https"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
23
src/all/comikey/assets/i18n/messages_en.properties
Normal file
23
src/all/comikey/assets/i18n/messages_en.properties
Normal file
@ -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
|
16
src/all/comikey/assets/i18n/messages_pt_br.properties
Normal file
16
src/all/comikey/assets/i18n/messages_pt_br.properties
Normal file
@ -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
|
54
src/all/comikey/assets/webview-script.js
Normal file
54
src/all/comikey/assets/webview-script.js
Normal file
@ -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 },
|
||||
);
|
11
src/all/comikey/build.gradle
Normal file
11
src/all/comikey/build.gradle
Normal file
@ -0,0 +1,11 @@
|
||||
ext {
|
||||
extName = "Comikey"
|
||||
extClass = ".ComikeyFactory"
|
||||
extVersionCode = 1
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lib:i18n"))
|
||||
}
|
BIN
src/all/comikey/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/all/comikey/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.6 KiB |
BIN
src/all/comikey/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/all/comikey/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
BIN
src/all/comikey/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/all/comikey/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
BIN
src/all/comikey/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/all/comikey/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.8 KiB |
BIN
src/all/comikey/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/all/comikey/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.2 KiB |
@ -0,0 +1,405 @@
|
||||
package eu.kanade.tachiyomi.extension.all.comikey
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.View
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.lib.i18n.Intl
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
open class Comikey(
|
||||
final override val lang: String,
|
||||
override val name: String = "Comikey",
|
||||
override val baseUrl: String = "https://comikey.com",
|
||||
private val defaultLanguage: String = "en",
|
||||
) : ParsedHttpSource(), ConfigurableSource {
|
||||
|
||||
private val gundamUrl: String = "https://gundam.comikey.net"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.rateLimit(3)
|
||||
.build()
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
private val dateFormat by lazy {
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT)
|
||||
}
|
||||
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
explicitNulls = false
|
||||
isLenient = true
|
||||
}
|
||||
|
||||
private val intl = Intl(
|
||||
language = lang,
|
||||
baseLanguage = "en",
|
||||
availableLanguages = setOf("en", "pt-BR"),
|
||||
classLoader = this::class.java.classLoader!!,
|
||||
)
|
||||
|
||||
private val preferences by lazy {
|
||||
Injekt.get<Application>().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<MangasPage> {
|
||||
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<UriFilter>()
|
||||
.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<ComikeyComic>(
|
||||
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<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
val mangaSlug = response.request.url.pathSegments[1]
|
||||
val mangaData = json.decodeFromString<ComikeyComic>(
|
||||
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<ComikeyEpisodeListResponse>(
|
||||
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<List<Page>> {
|
||||
return Observable.fromCallable {
|
||||
pageListParse(chapter)
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document) = throw UnsupportedOperationException()
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
private fun pageListParse(chapter: SChapter): List<Page> {
|
||||
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<Application>())
|
||||
|
||||
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<ComikeyPage> = 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<ComikeyEpisodeManifest>(rawData).readingOrder
|
||||
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal const val PREFIX_SLUG_SEARCH = "slug:"
|
||||
internal const val PREF_HIDE_LOCKED_CHAPTERS = "hide_locked_chapters"
|
||||
}
|
||||
}
|
@ -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"),
|
||||
)
|
||||
}
|
@ -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<String>) :
|
||||
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<String>) :
|
||||
Filter.Select<String>(name, values),
|
||||
UriFilter {
|
||||
override fun addToUri(builder: HttpUrl.Builder) {
|
||||
if (state == 0) {
|
||||
return
|
||||
}
|
||||
|
||||
builder.addQueryParameter("filter", values[state].lowercase())
|
||||
}
|
||||
}
|
@ -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<ComikeyAuthor>,
|
||||
val artist: List<ComikeyAuthor>,
|
||||
val tags: List<ComikeyNameWrapper>,
|
||||
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<ComikeyEpisode> = 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<ComikeyPage>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ComikeyPage(
|
||||
val href: String,
|
||||
val type: String,
|
||||
val height: Int,
|
||||
val width: Int,
|
||||
val alternate: List<ComikeyAlternatePage>,
|
||||
)
|
||||
|
||||
@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,
|
||||
)
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user