AS: library badge workaround (#16858)
* AS: workaround for "in library" mark * fix
This commit is contained in:
parent
052c2f09cb
commit
89433cd894
|
@ -1,9 +1,13 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.asurascans
|
package eu.kanade.tachiyomi.extension.all.asurascans
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
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.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
|
||||||
|
@ -11,7 +15,6 @@ import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
|
@ -20,18 +23,16 @@ import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
open class AsuraScans(
|
class AsuraScansEn : MangaThemesia(
|
||||||
override val baseUrl: String,
|
|
||||||
override val lang: String,
|
|
||||||
dateFormat: SimpleDateFormat,
|
|
||||||
) : MangaThemesia(
|
|
||||||
"Asura Scans",
|
"Asura Scans",
|
||||||
baseUrl,
|
"https://www.asurascans.com",
|
||||||
lang,
|
"en",
|
||||||
dateFormat = dateFormat,
|
dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.US),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val preferences = Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
private val preferences = Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
|
|
||||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||||
|
@ -42,35 +43,41 @@ open class AsuraScans(
|
||||||
.rateLimit(1, 3, TimeUnit.SECONDS)
|
.rateLimit(1, 3, TimeUnit.SECONDS)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
override val seriesDescriptionSelector = "div.desc p, div.entry-content p, div[itemprop=description]:not(:has(p))"
|
||||||
val newManga = manga.apply {
|
|
||||||
url = "$url#chapters#${title.toSearchQuery()}"
|
override val pageSelector = "div.rdminimal > img, div.rdminimal > p > img, div.rdminimal > a > img, div.rdminimal > p > a > img, " +
|
||||||
|
"div.rdminimal > noscript > img, div.rdminimal > p > noscript > img, div.rdminimal > a > noscript > img, div.rdminimal > p > a > noscript > img"
|
||||||
|
|
||||||
|
// Permanent Url for Manga/Chapter End
|
||||||
|
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||||
|
return super.fetchPopularManga(page).tempUrlToPermIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
||||||
|
return super.fetchLatestUpdates(page).tempUrlToPermIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
|
return super.fetchSearchManga(page, query, filters).tempUrlToPermIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temp Url for manga/chapter
|
||||||
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||||
|
val newManga = manga.permUrlToTemp()
|
||||||
|
|
||||||
return super.fetchChapterList(newManga)
|
return super.fetchChapterList(newManga)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||||
val newManga = try {
|
val newManga = manga.permUrlToTemp()
|
||||||
manga.apply {
|
|
||||||
url = "$url#details#${title.toSearchQuery()}"
|
|
||||||
}
|
|
||||||
} catch (e: UninitializedPropertyAccessException) {
|
|
||||||
// when called from deep link, title is not present
|
|
||||||
manga
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.fetchMangaDetails(newManga)
|
return super.fetchMangaDetails(newManga)
|
||||||
}
|
}
|
||||||
|
|
||||||
// use updated url for webView
|
|
||||||
override fun getMangaUrl(manga: SManga): String {
|
override fun getMangaUrl(manga: SManga): String {
|
||||||
val dbSlug = manga.url
|
val newManga = manga.permUrlToTemp()
|
||||||
.removeSuffix("/")
|
|
||||||
.substringAfterLast("/")
|
|
||||||
|
|
||||||
val storedSlug = getSlugMap()[dbSlug] ?: dbSlug
|
return baseUrl + newManga.url
|
||||||
|
|
||||||
return "$baseUrl$mangaUrlDirectory/$storedSlug/"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip scriptPages
|
// Skip scriptPages
|
||||||
|
@ -87,15 +94,65 @@ open class AsuraScans(
|
||||||
else -> attr("abs:src")
|
else -> attr("abs:src")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Observable<MangasPage>.tempUrlToPermIfNeeded(): Observable<MangasPage> {
|
||||||
|
return this.map { mangasPage ->
|
||||||
|
MangasPage(
|
||||||
|
mangasPage.mangas.map { it.tempUrlToPermIfNeeded() },
|
||||||
|
mangasPage.hasNextPage,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun SManga.tempUrlToPermIfNeeded(): SManga {
|
||||||
|
if (!preferences.permaUrlPref) return this
|
||||||
|
|
||||||
|
val slugMap = getSlugMap().toMutableMap()
|
||||||
|
|
||||||
|
val sMangaTitleFirstWord = this.title.split(" ")[0]
|
||||||
|
if (!this.url.contains("/$sMangaTitleFirstWord", ignoreCase = true)) {
|
||||||
|
val currentSlug = this.url
|
||||||
|
.removeSuffix("/")
|
||||||
|
.substringAfterLast("/")
|
||||||
|
|
||||||
|
val permaSlug = currentSlug.replaceFirst(TEMP_TO_PERM_REGEX, "")
|
||||||
|
|
||||||
|
slugMap[permaSlug] = currentSlug
|
||||||
|
|
||||||
|
this.url = "$mangaUrlDirectory/$permaSlug/"
|
||||||
|
}
|
||||||
|
putSlugMap(slugMap)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun SManga.permUrlToTemp(): SManga {
|
||||||
|
return try {
|
||||||
|
val dbSlug = this.url
|
||||||
|
.removeSuffix("/")
|
||||||
|
.substringAfterLast("/")
|
||||||
|
|
||||||
|
val storedSlug = getSlugMap()[dbSlug] ?: dbSlug
|
||||||
|
|
||||||
|
this.apply {
|
||||||
|
url = "$mangaUrlDirectory/$storedSlug/#${title.toSearchQuery()}"
|
||||||
|
}
|
||||||
|
} catch (e: UninitializedPropertyAccessException) {
|
||||||
|
// when called from deep link, title is not present
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun urlChangeInterceptor(chain: Interceptor.Chain): Response {
|
private fun urlChangeInterceptor(chain: Interceptor.Chain): Response {
|
||||||
val request = chain.request()
|
val request = chain.request()
|
||||||
|
val response = chain.proceed(request)
|
||||||
|
|
||||||
val frag = request.url.fragment
|
val frag = request.url.fragment
|
||||||
|
|
||||||
if (frag.isNullOrEmpty()) {
|
if (frag.isNullOrEmpty()) {
|
||||||
return chain.proceed(request)
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
val search = frag.substringAfter("#")
|
if (!response.isSuccessful && response.code == 404) {
|
||||||
|
response.close()
|
||||||
|
|
||||||
val dbSlug = request.url.toString()
|
val dbSlug = request.url.toString()
|
||||||
.substringBefore("#")
|
.substringBefore("#")
|
||||||
|
@ -104,21 +161,19 @@ open class AsuraScans(
|
||||||
|
|
||||||
val slugMap = getSlugMap().toMutableMap()
|
val slugMap = getSlugMap().toMutableMap()
|
||||||
|
|
||||||
// make sure db slug key is present in the slugMap
|
|
||||||
val storedSlug = slugMap[dbSlug] ?: dbSlug
|
val storedSlug = slugMap[dbSlug] ?: dbSlug
|
||||||
|
|
||||||
val response = chain.proceed(newRequest(frag, storedSlug))
|
val newSlug = getNewSlug(storedSlug, frag)
|
||||||
|
|
||||||
if (!response.isSuccessful && response.code == 404) {
|
|
||||||
response.close()
|
|
||||||
|
|
||||||
val newSlug = getNewSlug(storedSlug, search)
|
|
||||||
?: throw IOException("Migrate from Asura to Asura")
|
?: throw IOException("Migrate from Asura to Asura")
|
||||||
|
|
||||||
slugMap[dbSlug] = newSlug
|
slugMap[dbSlug] = newSlug
|
||||||
putSlugMap(slugMap)
|
putSlugMap(slugMap)
|
||||||
|
|
||||||
return chain.proceed(newRequest(frag, newSlug))
|
return chain.proceed(
|
||||||
|
request.newBuilder()
|
||||||
|
.url("$baseUrl$mangaUrlDirectory/$newSlug/")
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
@ -142,18 +197,6 @@ open class AsuraScans(
|
||||||
?.substringAfterLast("/")
|
?.substringAfterLast("/")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun newRequest(frag: String, slug: String): Request {
|
|
||||||
val manga = SManga.create().apply {
|
|
||||||
this.url = "$mangaUrlDirectory/$slug/"
|
|
||||||
}
|
|
||||||
|
|
||||||
return when (frag.substringBefore("#")) {
|
|
||||||
"chapters" -> chapterListRequest(manga)
|
|
||||||
"details" -> mangaDetailsRequest(manga)
|
|
||||||
else -> throw IOException("unknown url fragment for urlChangeInterceptor")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun putSlugMap(slugMap: MutableMap<String, String>) {
|
private fun putSlugMap(slugMap: MutableMap<String, String>) {
|
||||||
val serialized = json.encodeToString(slugMap)
|
val serialized = json.encodeToString(slugMap)
|
||||||
|
|
||||||
|
@ -177,7 +220,24 @@ open class AsuraScans(
|
||||||
.replace(trailingPlusRegex, "")
|
.replace(trailingPlusRegex, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
SwitchPreferenceCompat(screen.context).apply {
|
||||||
|
key = PREF_PERM_MANGA_URL_KEY_PREFIX + lang
|
||||||
|
title = PREF_PERM_MANGA_URL_TITLE
|
||||||
|
summary = PREF_PERM_MANGA_URL_SUMMARY
|
||||||
|
setDefaultValue(true)
|
||||||
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
|
addRandomAndCustomUserAgentPreferences(screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val SharedPreferences.permaUrlPref
|
||||||
|
get() = getBoolean(PREF_PERM_MANGA_URL_KEY_PREFIX + lang, true)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val PREF_PERM_MANGA_URL_KEY_PREFIX = "pref_permanent_manga_url_2_"
|
||||||
|
private const val PREF_PERM_MANGA_URL_TITLE = "Permanent Manga URL"
|
||||||
|
private const val PREF_PERM_MANGA_URL_SUMMARY = "Turns all manga urls into permanent ones."
|
||||||
private const val PREF_URL_MAP = "pref_url_map"
|
private const val PREF_URL_MAP = "pref_url_map"
|
||||||
private val TEMP_TO_PERM_REGEX = Regex("""^\d+-""")
|
private val TEMP_TO_PERM_REGEX = Regex("""^\d+-""")
|
||||||
private val titleSpecialCharactersRegex = Regex("""[^a-z0-9]+""")
|
private val titleSpecialCharactersRegex = Regex("""[^a-z0-9]+""")
|
|
@ -1,13 +1,6 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.asurascans
|
package eu.kanade.tachiyomi.extension.all.asurascans
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class AsuraScansFactory : SourceFactory {
|
class AsuraScansFactory : SourceFactory {
|
||||||
override fun createSources() = listOf(
|
override fun createSources() = listOf(
|
||||||
|
@ -15,47 +8,3 @@ class AsuraScansFactory : SourceFactory {
|
||||||
AsuraScansTr(),
|
AsuraScansTr(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class AsuraScansEn : AsuraScans("https://www.asurascans.com", "en", SimpleDateFormat("MMM d, yyyy", Locale.US)) {
|
|
||||||
|
|
||||||
override val seriesDescriptionSelector = "div.desc p, div.entry-content p, div[itemprop=description]:not(:has(p))"
|
|
||||||
|
|
||||||
override val pageSelector = "div.rdminimal > img, div.rdminimal > p > img, div.rdminimal > a > img, div.rdminimal > p > a > img, " +
|
|
||||||
"div.rdminimal > noscript > img, div.rdminimal > p > noscript > img, div.rdminimal > a > noscript > img, div.rdminimal > p > a > noscript > img"
|
|
||||||
}
|
|
||||||
|
|
||||||
class AsuraScansTr : AsuraScans("https://asurascanstr.com", "tr", SimpleDateFormat("MMM d, yyyy", Locale("tr"))) {
|
|
||||||
|
|
||||||
override val seriesArtistSelector = ".fmed b:contains(Çizer)+span"
|
|
||||||
override val seriesAuthorSelector = ".fmed b:contains(Yazar)+span"
|
|
||||||
override val seriesStatusSelector = ".imptdt:contains(Durum) i"
|
|
||||||
override val seriesTypeSelector = ".imptdt:contains(Tür) a"
|
|
||||||
|
|
||||||
override val altNamePrefix: String = "Alternatif isim: "
|
|
||||||
|
|
||||||
override fun String?.parseStatus(): Int = when {
|
|
||||||
this == null -> SManga.UNKNOWN
|
|
||||||
this.contains("Devam Ediyor", ignoreCase = true) -> SManga.ONGOING
|
|
||||||
this.contains("Tamamlandı", ignoreCase = true) -> SManga.COMPLETED
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
|
||||||
val scriptContent = document.selectFirst("script:containsData(ts_reader)")!!.data()
|
|
||||||
val jsonString = scriptContent.substringAfter("ts_reader.run(").substringBefore(");")
|
|
||||||
val tsReader = json.decodeFromString<TSReader>(jsonString)
|
|
||||||
val imageUrls = tsReader.sources.firstOrNull()?.images ?: return emptyList()
|
|
||||||
return imageUrls.mapIndexed { index, imageUrl -> Page(index, imageUrl = imageUrl) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class TSReader(
|
|
||||||
val sources: List<ReaderImageSource>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ReaderImageSource(
|
|
||||||
val source: String,
|
|
||||||
val images: List<String>,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.asurascans
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||||
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class AsuraScansTr : MangaThemesia(
|
||||||
|
"Asura Scans",
|
||||||
|
"https://asurascanstr.com",
|
||||||
|
"tr",
|
||||||
|
dateFormat = SimpleDateFormat("MMM d, yyyy", Locale("tr")),
|
||||||
|
) {
|
||||||
|
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||||
|
.addInterceptor(uaIntercept)
|
||||||
|
.connectTimeout(10, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.rateLimit(1, 3, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override val seriesArtistSelector = ".fmed b:contains(Çizer)+span"
|
||||||
|
override val seriesAuthorSelector = ".fmed b:contains(Yazar)+span"
|
||||||
|
override val seriesStatusSelector = ".imptdt:contains(Durum) i"
|
||||||
|
override val seriesTypeSelector = ".imptdt:contains(Tür) a"
|
||||||
|
|
||||||
|
override val altNamePrefix: String = "Alternatif isim: "
|
||||||
|
|
||||||
|
override fun String?.parseStatus(): Int = when {
|
||||||
|
this == null -> SManga.UNKNOWN
|
||||||
|
this.contains("Devam Ediyor", ignoreCase = true) -> SManga.ONGOING
|
||||||
|
this.contains("Tamamlandı", ignoreCase = true) -> SManga.COMPLETED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun Element.imgAttr(): String = when {
|
||||||
|
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
|
||||||
|
hasAttr("data-src") -> attr("abs:data-src")
|
||||||
|
hasAttr("data-cfsrc") -> attr("abs:data-cfsrc")
|
||||||
|
else -> attr("abs:src")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
val scriptContent = document.selectFirst("script:containsData(ts_reader)")?.data()
|
||||||
|
?: return super.pageListParse(document)
|
||||||
|
val jsonString = scriptContent.substringAfter("ts_reader.run(").substringBefore(");")
|
||||||
|
val tsReader = json.decodeFromString<TSReader>(jsonString)
|
||||||
|
val imageUrls = tsReader.sources.firstOrNull()?.images ?: return emptyList()
|
||||||
|
return imageUrls.mapIndexed { index, imageUrl -> Page(index, imageUrl = imageUrl) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class TSReader(
|
||||||
|
val sources: List<ReaderImageSource>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ReaderImageSource(
|
||||||
|
val source: String,
|
||||||
|
val images: List<String>,
|
||||||
|
)
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ class MangaThemesiaGenerator : ThemeSourceGenerator {
|
||||||
override val baseVersionCode: Int = 25
|
override val baseVersionCode: Int = 25
|
||||||
|
|
||||||
override val sources = listOf(
|
override val sources = listOf(
|
||||||
MultiLang("Asura Scans", "https://www.asurascans.com", listOf("en", "tr"), className = "AsuraScansFactory", pkgName = "asurascans", overrideVersionCode = 21),
|
MultiLang("Asura Scans", "https://www.asurascans.com", listOf("en", "tr"), className = "AsuraScansFactory", pkgName = "asurascans", overrideVersionCode = 22),
|
||||||
MultiLang("Flame Scans", "https://flamescans.org", listOf("en"), className = "FlameScansFactory", pkgName = "flamescans", overrideVersionCode = 4),
|
MultiLang("Flame Scans", "https://flamescans.org", listOf("en"), className = "FlameScansFactory", pkgName = "flamescans", overrideVersionCode = 4),
|
||||||
MultiLang("Komik Lab", "https://komiklab.com", listOf("en", "id"), className = "KomikLabFactory", pkgName = "komiklab", overrideVersionCode = 2),
|
MultiLang("Komik Lab", "https://komiklab.com", listOf("en", "id"), className = "KomikLabFactory", pkgName = "komiklab", overrideVersionCode = 2),
|
||||||
MultiLang("Miau Scan", "https://miauscan.com", listOf("es", "pt-BR")),
|
MultiLang("Miau Scan", "https://miauscan.com", listOf("es", "pt-BR")),
|
||||||
|
|
Loading…
Reference in New Issue