Koharu: fix DTO & bypass Cloudflare (#8175)
* Koharu - \r\n → \n * SchaleNetwork: fix loading & support related-manga * (SchaleNetwok/Koharu): Fix DTO & bypass Cloudflare (#128) * Fix DTO * Bypass CloudFlare Turnstile * Add tags filter select box which supports click on 'tag' to search * Allow permanent excluded tags * Revert fork specific Koharu changes * Bump version + lint --------- Co-authored-by: Cuong-Tran <cuongtran.tm@gmail.com>
This commit is contained in:
parent
414b6b8670
commit
3fb70c10de
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'SchaleNetwork'
|
||||
extClass = '.KoharuFactory'
|
||||
extVersionCode = 12
|
||||
extVersionCode = 13
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,24 @@
|
||||
package eu.kanade.tachiyomi.extension.all.koharu
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.extension.all.koharu.KoharuFilters.artistList
|
||||
import eu.kanade.tachiyomi.extension.all.koharu.KoharuFilters.circleList
|
||||
import eu.kanade.tachiyomi.extension.all.koharu.KoharuFilters.femaleList
|
||||
import eu.kanade.tachiyomi.extension.all.koharu.KoharuFilters.genreList
|
||||
import eu.kanade.tachiyomi.extension.all.koharu.KoharuFilters.getFilters
|
||||
import eu.kanade.tachiyomi.extension.all.koharu.KoharuFilters.maleList
|
||||
import eu.kanade.tachiyomi.extension.all.koharu.KoharuFilters.mixedList
|
||||
import eu.kanade.tachiyomi.extension.all.koharu.KoharuFilters.otherList
|
||||
import eu.kanade.tachiyomi.extension.all.koharu.KoharuFilters.parodyList
|
||||
import eu.kanade.tachiyomi.extension.all.koharu.KoharuFilters.tagsFetchAttempts
|
||||
import eu.kanade.tachiyomi.extension.all.koharu.KoharuFilters.tagsFetched
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
@ -12,9 +26,11 @@ 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.model.UpdateStrategy
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
@ -42,11 +58,9 @@ class Koharu(
|
||||
|
||||
private val apiBooksUrl = "$apiUrl/books"
|
||||
|
||||
override val supportsLatest = true
|
||||
private val authUrl = "${baseUrl.replace("://", "://auth.")}/clearance"
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.rateLimit(1)
|
||||
.build()
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
@ -59,6 +73,18 @@ class Koharu(
|
||||
|
||||
private fun remadd() = preferences.getBoolean(PREF_REM_ADD, false)
|
||||
|
||||
private fun alwaysExcludeTags() = preferences.getString(PREF_EXCLUDE_TAGS, "")
|
||||
|
||||
private var _domainUrl: String? = null
|
||||
internal val domainUrl: String
|
||||
get() {
|
||||
return _domainUrl ?: run {
|
||||
val domain = getDomain()
|
||||
_domainUrl = domain
|
||||
domain
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDomain(): String {
|
||||
try {
|
||||
val noRedirectClient = client.newBuilder().followRedirects(false).build()
|
||||
@ -72,20 +98,29 @@ class Koharu(
|
||||
}
|
||||
|
||||
private val lazyHeaders by lazy {
|
||||
val domain = getDomain()
|
||||
headersBuilder()
|
||||
.set("Referer", "$domain/")
|
||||
.set("Origin", domain)
|
||||
.set("Referer", "$domainUrl/")
|
||||
.set("Origin", domainUrl)
|
||||
.build()
|
||||
}
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.rateLimit(3)
|
||||
.build()
|
||||
|
||||
private val interceptedClient: OkHttpClient
|
||||
get() = network.cloudflareClient.newBuilder()
|
||||
.addInterceptor(TurnstileInterceptor(client, domainUrl, authUrl, lazyHeaders["User-Agent"]))
|
||||
.rateLimit(3)
|
||||
.build()
|
||||
|
||||
private fun getManga(book: Entry) = SManga.create().apply {
|
||||
setUrlWithoutDomain("${book.id}/${book.public_key}")
|
||||
setUrlWithoutDomain("${book.id}/${book.key}")
|
||||
title = if (remadd()) book.title.shortenTitle() else book.title
|
||||
thumbnail_url = book.thumbnail.path
|
||||
}
|
||||
|
||||
private fun getImagesByMangaEntry(entry: MangaEntry): Pair<ImagesInfo, String> {
|
||||
private fun getImagesByMangaData(entry: MangaData, entryId: String, entryKey: String): Pair<ImagesInfo, String> {
|
||||
val data = entry.data
|
||||
fun getIPK(
|
||||
ori: DataKey?,
|
||||
@ -96,7 +131,7 @@ class Koharu(
|
||||
): Pair<Int?, String?> {
|
||||
return Pair(
|
||||
ori?.id ?: alt1?.id ?: alt2?.id ?: alt3?.id ?: alt4?.id,
|
||||
ori?.public_key ?: alt1?.public_key ?: alt2?.public_key ?: alt3?.public_key ?: alt4?.public_key,
|
||||
ori?.key ?: alt1?.key ?: alt2?.key ?: alt3?.key ?: alt4?.key,
|
||||
)
|
||||
}
|
||||
val (id, public_key) = when (quality()) {
|
||||
@ -119,35 +154,65 @@ class Koharu(
|
||||
else -> "0"
|
||||
}
|
||||
|
||||
val imagesResponse = client.newCall(GET("$apiBooksUrl/data/${entry.id}/${entry.public_key}/$id/$public_key?v=${entry.updated_at ?: entry.created_at}&w=$realQuality", lazyHeaders)).execute()
|
||||
val imagesResponse = interceptedClient.newCall(GET("$apiBooksUrl/data/$entryId/$entryKey/$id/$public_key/$realQuality?crt=$token", lazyHeaders)).execute()
|
||||
val images = imagesResponse.parseAs<ImagesInfo>() to realQuality
|
||||
return images
|
||||
}
|
||||
|
||||
// Latest
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$apiBooksUrl?page=$page" + if (searchLang.isNotBlank()) "&s=language!:\"$searchLang\"" else "", lazyHeaders)
|
||||
override fun latestUpdatesRequest(page: Int) = GET(
|
||||
apiBooksUrl.toHttpUrl().newBuilder().apply {
|
||||
addQueryParameter("page", page.toString())
|
||||
|
||||
val terms: MutableList<String> = mutableListOf()
|
||||
if (lang != "all") terms += "language:\"^$searchLang$\""
|
||||
val alwaysExcludeTags = alwaysExcludeTags()?.split(",")
|
||||
?.map { it.trim() }?.filter(String::isNotBlank) ?: emptyList()
|
||||
if (alwaysExcludeTags.isNotEmpty()) {
|
||||
terms += "tag:\"${alwaysExcludeTags.joinToString(",") { "-$it" }}\""
|
||||
}
|
||||
if (terms.isNotEmpty()) addQueryParameter("s", terms.joinToString(" "))
|
||||
}.build(),
|
||||
lazyHeaders,
|
||||
)
|
||||
|
||||
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
|
||||
|
||||
// Popular
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET("$apiBooksUrl?sort=8&page=$page" + if (searchLang.isNotBlank()) "&s=language!:\"$searchLang\"" else "", lazyHeaders)
|
||||
override fun popularMangaRequest(page: Int) = GET(
|
||||
apiBooksUrl.toHttpUrl().newBuilder().apply {
|
||||
addQueryParameter("sort", "8")
|
||||
addQueryParameter("page", page.toString())
|
||||
|
||||
val terms: MutableList<String> = mutableListOf()
|
||||
if (lang != "all") terms += "language:\"^$searchLang$\""
|
||||
val alwaysExcludeTags = alwaysExcludeTags()?.split(",")
|
||||
?.map { it.trim() }?.filter(String::isNotBlank) ?: emptyList()
|
||||
if (alwaysExcludeTags.isNotEmpty()) {
|
||||
terms += "tag:\"${alwaysExcludeTags.joinToString(",") { "-$it" }}\""
|
||||
}
|
||||
if (terms.isNotEmpty()) addQueryParameter("s", terms.joinToString(" "))
|
||||
}.build(),
|
||||
lazyHeaders,
|
||||
)
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val data = response.parseAs<Books>()
|
||||
|
||||
return MangasPage(data.entries.map(::getManga), data.page * data.limit < data.total)
|
||||
}
|
||||
|
||||
// Search
|
||||
|
||||
override fun getFilterList(): FilterList = getFilters()
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return when {
|
||||
query.startsWith(PREFIX_ID_KEY_SEARCH) -> {
|
||||
val ipk = query.removePrefix(PREFIX_ID_KEY_SEARCH)
|
||||
val response = client.newCall(GET("$apiBooksUrl/detail/$ipk", lazyHeaders)).execute()
|
||||
Observable.just(searchMangaParse2(response))
|
||||
Observable.just(
|
||||
MangasPage(listOf(mangaDetailsParse(response)), false),
|
||||
)
|
||||
}
|
||||
else -> super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
@ -156,30 +221,61 @@ class Koharu(
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = apiBooksUrl.toHttpUrl().newBuilder().apply {
|
||||
val terms: MutableList<String> = mutableListOf()
|
||||
val includedTags: MutableList<Int> = mutableListOf()
|
||||
val excludedTags: MutableList<Int> = mutableListOf()
|
||||
|
||||
if (lang != "all") terms += "language:\"^$searchLang$\""
|
||||
val alwaysExcludeTags = alwaysExcludeTags()?.split(",")
|
||||
?.map { it.trim() }?.filter(String::isNotBlank) ?: emptyList()
|
||||
if (alwaysExcludeTags.isNotEmpty()) {
|
||||
terms += "tag:\"${alwaysExcludeTags.joinToString(",") { "-$it" }}\""
|
||||
}
|
||||
|
||||
if (lang != "all") terms += "language!:\"$searchLang\""
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is SortFilter -> addQueryParameter("sort", filter.getValue())
|
||||
is KoharuFilters.SortFilter -> addQueryParameter("sort", filter.getValue())
|
||||
|
||||
is CategoryFilter -> {
|
||||
is KoharuFilters.CategoryFilter -> {
|
||||
val activeFilter = filter.state.filter { it.state }
|
||||
if (activeFilter.isNotEmpty()) {
|
||||
addQueryParameter("cat", activeFilter.sumOf { it.value }.toString())
|
||||
}
|
||||
}
|
||||
|
||||
is TextFilter -> {
|
||||
is KoharuFilters.TagFilter -> {
|
||||
includedTags += filter.state
|
||||
.filter { it.isIncluded() }
|
||||
.map { it.id }
|
||||
excludedTags += filter.state
|
||||
.filter { it.isExcluded() }
|
||||
.map { it.id }
|
||||
}
|
||||
|
||||
is KoharuFilters.GenreConditionFilter -> {
|
||||
if (filter.state > 0) {
|
||||
addQueryParameter(filter.param, filter.toUriPart())
|
||||
}
|
||||
}
|
||||
|
||||
is KoharuFilters.TextFilter -> {
|
||||
if (filter.state.isNotEmpty()) {
|
||||
val tags = filter.state.split(",").filter(String::isNotBlank).joinToString(",")
|
||||
if (tags.isNotBlank()) {
|
||||
terms += "${filter.type}!:" + if (filter.type == "pages") tags else '"' + tags + '"'
|
||||
terms += "${filter.type}:" + if (filter.type == "pages") tags else "\"$tags\""
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
if (includedTags.isNotEmpty()) {
|
||||
addQueryParameter("include", includedTags.joinToString(","))
|
||||
}
|
||||
if (excludedTags.isNotEmpty()) {
|
||||
addQueryParameter("exclude", excludedTags.joinToString(","))
|
||||
}
|
||||
|
||||
if (query.isNotEmpty()) terms.add("title:\"$query\"")
|
||||
if (terms.isNotEmpty()) addQueryParameter("s", terms.joinToString(" "))
|
||||
addQueryParameter("page", page.toString())
|
||||
@ -190,20 +286,48 @@ class Koharu(
|
||||
|
||||
override fun searchMangaParse(response: Response) = popularMangaParse(response)
|
||||
|
||||
private fun searchMangaParse2(response: Response): MangasPage {
|
||||
val entry = response.parseAs<MangaEntry>()
|
||||
override fun getFilterList(): FilterList {
|
||||
launchIO { fetchTags() }
|
||||
|
||||
return MangasPage(
|
||||
listOf(
|
||||
SManga.create().apply {
|
||||
setUrlWithoutDomain("${entry.id}/${entry.public_key}")
|
||||
title = if (remadd()) entry.title.shortenTitle() else entry.title
|
||||
thumbnail_url = entry.thumbnails.base + entry.thumbnails.main.path
|
||||
},
|
||||
),
|
||||
false,
|
||||
)
|
||||
return getFilters()
|
||||
}
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
private fun launchIO(block: () -> Unit) = scope.launch { block() }
|
||||
|
||||
/**
|
||||
* Fetch the genres from the source to be used in the filters.
|
||||
*/
|
||||
private fun fetchTags() {
|
||||
if (tagsFetchAttempts < 3 && !tagsFetched) {
|
||||
try {
|
||||
client.newCall(
|
||||
GET("$apiBooksUrl/tags/filters", lazyHeaders),
|
||||
).execute()
|
||||
.use { it.parseAs<List<Filter>>() }
|
||||
.also {
|
||||
tagsFetched = true
|
||||
}
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.map { it.toTag() }
|
||||
?.also { tags ->
|
||||
genreList = tags.filterIsInstance<KoharuFilters.Genre>()
|
||||
femaleList = tags.filterIsInstance<KoharuFilters.Female>()
|
||||
maleList = tags.filterIsInstance<KoharuFilters.Male>()
|
||||
artistList = tags.filterIsInstance<KoharuFilters.Artist>()
|
||||
circleList = tags.filterIsInstance<KoharuFilters.Circle>()
|
||||
parodyList = tags.filterIsInstance<KoharuFilters.Parody>()
|
||||
mixedList = tags.filterIsInstance<KoharuFilters.Mixed>()
|
||||
otherList = tags.filterIsInstance<KoharuFilters.Other>()
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
} finally {
|
||||
tagsFetchAttempts++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Details
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
@ -211,96 +335,11 @@ class Koharu(
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
return response.parseAs<MangaEntry>().toSManga()
|
||||
val mangaDetail = response.parseAs<MangaDetail>()
|
||||
return mangaDetail.toSManga().apply {
|
||||
setUrlWithoutDomain("${mangaDetail.id}/${mangaDetail.key}")
|
||||
title = if (remadd()) mangaDetail.title.shortenTitle() else mangaDetail.title
|
||||
}
|
||||
|
||||
private val dateReformat = SimpleDateFormat("EEEE, d MMM yyyy HH:mm (z)", Locale.ENGLISH)
|
||||
private fun MangaEntry.toSManga() = SManga.create().apply {
|
||||
val artists = mutableListOf<String>()
|
||||
val circles = mutableListOf<String>()
|
||||
val parodies = mutableListOf<String>()
|
||||
val magazines = mutableListOf<String>()
|
||||
val characters = mutableListOf<String>()
|
||||
val cosplayers = mutableListOf<String>()
|
||||
val females = mutableListOf<String>()
|
||||
val males = mutableListOf<String>()
|
||||
val mixed = mutableListOf<String>()
|
||||
val other = mutableListOf<String>()
|
||||
val uploaders = mutableListOf<String>()
|
||||
val tags = mutableListOf<String>()
|
||||
for (tag in this@toSManga.tags) {
|
||||
when (tag.namespace) {
|
||||
1 -> artists.add(tag.name)
|
||||
2 -> circles.add(tag.name)
|
||||
3 -> parodies.add(tag.name)
|
||||
4 -> magazines.add(tag.name)
|
||||
5 -> characters.add(tag.name)
|
||||
6 -> cosplayers.add(tag.name)
|
||||
7 -> tag.name.takeIf { it != "anonymous" }?.let { uploaders.add(it) }
|
||||
8 -> males.add(tag.name + " ♂")
|
||||
9 -> females.add(tag.name + " ♀")
|
||||
10 -> mixed.add(tag.name)
|
||||
12 -> other.add(tag.name)
|
||||
else -> tags.add(tag.name)
|
||||
}
|
||||
}
|
||||
|
||||
var appended = false
|
||||
fun List<String>.joinAndCapitalizeEach(): String? = this.emptyToNull()?.joinToString { it.capitalizeEach() }?.apply { appended = true }
|
||||
title = if (remadd()) this@toSManga.title.shortenTitle() else this@toSManga.title
|
||||
|
||||
author = (circles.emptyToNull() ?: artists).joinToString { it.capitalizeEach() }
|
||||
artist = artists.joinToString { it.capitalizeEach() }
|
||||
genre = (tags + males + females + mixed + other).joinToString { it.capitalizeEach() }
|
||||
description = buildString {
|
||||
circles.joinAndCapitalizeEach()?.let {
|
||||
append("Circles: ", it, "\n")
|
||||
}
|
||||
uploaders.joinAndCapitalizeEach()?.let {
|
||||
append("Uploaders: ", it, "\n")
|
||||
}
|
||||
magazines.joinAndCapitalizeEach()?.let {
|
||||
append("Magazines: ", it, "\n")
|
||||
}
|
||||
cosplayers.joinAndCapitalizeEach()?.let {
|
||||
append("Cosplayers: ", it, "\n")
|
||||
}
|
||||
parodies.joinAndCapitalizeEach()?.let {
|
||||
append("Parodies: ", it, "\n")
|
||||
}
|
||||
characters.joinAndCapitalizeEach()?.let {
|
||||
append("Characters: ", it, "\n")
|
||||
}
|
||||
|
||||
if (appended) append("\n")
|
||||
|
||||
try {
|
||||
append("Posted: ", dateReformat.format(created_at), "\n")
|
||||
} catch (_: Exception) {}
|
||||
|
||||
val dataKey = when (quality()) {
|
||||
"1600" -> data.`1600` ?: data.`1280` ?: data.`0`
|
||||
"1280" -> data.`1280` ?: data.`1600` ?: data.`0`
|
||||
"980" -> data.`980` ?: data.`1280` ?: data.`0`
|
||||
"780" -> data.`780` ?: data.`980` ?: data.`0`
|
||||
else -> data.`0`
|
||||
}
|
||||
append("Size: ", dataKey.readableSize(), "\n\n")
|
||||
append("Pages: ", thumbnails.entries.size, "\n\n")
|
||||
}
|
||||
status = SManga.COMPLETED
|
||||
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
||||
initialized = true
|
||||
}
|
||||
|
||||
private fun String.capitalizeEach() = this.split(" ").joinToString(" ") { s ->
|
||||
s.replaceFirstChar { sr ->
|
||||
if (sr.isLowerCase()) sr.titlecase(Locale.getDefault()) else sr.toString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> Collection<T>.emptyToNull(): Collection<T>? {
|
||||
return this.ifEmpty { null }
|
||||
}
|
||||
|
||||
override fun getMangaUrl(manga: SManga) = "$baseUrl/g/${manga.url}"
|
||||
@ -312,11 +351,11 @@ class Koharu(
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val manga = response.parseAs<MangaEntry>()
|
||||
val manga = response.parseAs<MangaDetail>()
|
||||
return listOf(
|
||||
SChapter.create().apply {
|
||||
name = "Chapter"
|
||||
url = "${manga.id}/${manga.public_key}"
|
||||
url = "${manga.id}/${manga.key}"
|
||||
date_upload = (manga.updated_at ?: manga.created_at)
|
||||
},
|
||||
)
|
||||
@ -326,13 +365,24 @@ class Koharu(
|
||||
|
||||
// Page List
|
||||
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
return interceptedClient.newCall(pageListRequest(chapter))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
pageListParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
return GET("$apiBooksUrl/detail/${chapter.url}", lazyHeaders)
|
||||
return POST("$apiBooksUrl/detail/${chapter.url}?crt=$token", lazyHeaders)
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val mangaEntry = response.parseAs<MangaEntry>()
|
||||
val imagesInfo = getImagesByMangaEntry(mangaEntry)
|
||||
val mangaData = response.parseAs<MangaData>()
|
||||
val url = response.request.url.toString()
|
||||
val matches = Regex("""/detail/(\d+)/([a-z\d]+)""").find(url)
|
||||
if (matches == null || matches.groupValues.size < 3) return emptyList()
|
||||
val imagesInfo = getImagesByMangaData(mangaData, matches.groupValues[1], matches.groupValues[2])
|
||||
|
||||
return imagesInfo.first.entries.mapIndexed { index, image ->
|
||||
Page(index, imageUrl = "${imagesInfo.first.base}/${image.path}?w=${imagesInfo.second}")
|
||||
@ -364,6 +414,13 @@ class Koharu(
|
||||
"Reload manga to apply changes to loaded manga."
|
||||
setDefaultValue(false)
|
||||
}.also(screen::addPreference)
|
||||
|
||||
EditTextPreference(screen.context).apply {
|
||||
key = PREF_EXCLUDE_TAGS
|
||||
title = "Tags to exclude from browse/search"
|
||||
summary = "Separate tags with commas (,).\n" +
|
||||
"Excluding: ${alwaysExcludeTags()}"
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T {
|
||||
@ -374,5 +431,11 @@ class Koharu(
|
||||
const val PREFIX_ID_KEY_SEARCH = "id:"
|
||||
private const val PREF_IMAGERES = "pref_image_quality"
|
||||
private const val PREF_REM_ADD = "pref_remove_additional"
|
||||
private const val PREF_EXCLUDE_TAGS = "pref_exclude_tags"
|
||||
|
||||
internal var token: String? = null
|
||||
internal var authorization: String? = null
|
||||
|
||||
internal val dateReformat = SimpleDateFormat("EEEE, d MMM yyyy HH:mm (z)", Locale.ENGLISH)
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,36 @@
|
||||
package eu.kanade.tachiyomi.extension.all.koharu
|
||||
|
||||
import eu.kanade.tachiyomi.extension.all.koharu.Koharu.Companion.dateReformat
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.Locale
|
||||
|
||||
@Serializable
|
||||
class Tag(
|
||||
var name: String,
|
||||
var namespace: Int = 0,
|
||||
val name: String,
|
||||
val namespace: Int = 0,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class Filter(
|
||||
private val id: Int,
|
||||
private val name: String,
|
||||
private val namespace: Int = 0,
|
||||
) {
|
||||
fun toTag() = when (namespace) {
|
||||
0 -> KoharuFilters.Genre(id, name)
|
||||
1 -> KoharuFilters.Artist(id, name)
|
||||
2 -> KoharuFilters.Circle(id, name)
|
||||
3 -> KoharuFilters.Parody(id, name)
|
||||
8 -> KoharuFilters.Male(id, name)
|
||||
9 -> KoharuFilters.Female(id, name)
|
||||
10 -> KoharuFilters.Mixed(id, name)
|
||||
12 -> KoharuFilters.Other(id, name)
|
||||
else -> KoharuFilters.Tag(id, name, namespace)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class Books(
|
||||
val entries: List<Entry> = emptyList(),
|
||||
@ -19,22 +42,125 @@ class Books(
|
||||
@Serializable
|
||||
class Entry(
|
||||
val id: Int,
|
||||
val public_key: String,
|
||||
val key: String,
|
||||
val title: String,
|
||||
val thumbnail: Thumbnail,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class MangaEntry(
|
||||
class MangaDetail(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
val public_key: String,
|
||||
val key: String,
|
||||
val created_at: Long = 0L,
|
||||
val updated_at: Long?,
|
||||
val thumbnails: Thumbnails,
|
||||
val tags: List<Tag> = emptyList(),
|
||||
) {
|
||||
fun toSManga() = SManga.create().apply {
|
||||
val artists = mutableListOf<String>()
|
||||
val circles = mutableListOf<String>()
|
||||
val parodies = mutableListOf<String>()
|
||||
val magazines = mutableListOf<String>()
|
||||
val characters = mutableListOf<String>()
|
||||
val cosplayers = mutableListOf<String>()
|
||||
val females = mutableListOf<String>()
|
||||
val males = mutableListOf<String>()
|
||||
val mixed = mutableListOf<String>()
|
||||
val language = mutableListOf<String>()
|
||||
val other = mutableListOf<String>()
|
||||
val uploaders = mutableListOf<String>()
|
||||
val tags = mutableListOf<String>()
|
||||
this@MangaDetail.tags.forEach { tag ->
|
||||
when (tag.namespace) {
|
||||
1 -> artists.add(tag.name)
|
||||
2 -> circles.add(tag.name)
|
||||
3 -> parodies.add(tag.name)
|
||||
4 -> magazines.add(tag.name)
|
||||
5 -> characters.add(tag.name)
|
||||
6 -> cosplayers.add(tag.name)
|
||||
7 -> tag.name.takeIf { it != "anonymous" }?.let { uploaders.add(it) }
|
||||
8 -> males.add(tag.name + " ♂")
|
||||
9 -> females.add(tag.name + " ♀")
|
||||
10 -> mixed.add(tag.name)
|
||||
11 -> language.add(tag.name)
|
||||
12 -> other.add(tag.name)
|
||||
else -> tags.add(tag.name)
|
||||
}
|
||||
}
|
||||
|
||||
var appended = false
|
||||
fun List<String>.joinAndCapitalizeEach(): String? = this.emptyToNull()?.joinToString { it.capitalizeEach() }?.apply { appended = true }
|
||||
|
||||
thumbnail_url = thumbnails.base + thumbnails.main.path
|
||||
|
||||
author = (circles.emptyToNull() ?: artists).joinToString { it.capitalizeEach() }
|
||||
artist = artists.joinToString { it.capitalizeEach() }
|
||||
genre = (artists + circles + parodies + magazines + characters + cosplayers + tags + females + males + mixed + other).joinToString { it.capitalizeEach() }
|
||||
description = buildString {
|
||||
circles.joinAndCapitalizeEach()?.let {
|
||||
append("Circles: ", it, "\n")
|
||||
}
|
||||
uploaders.joinAndCapitalizeEach()?.let {
|
||||
append("Uploaders: ", it, "\n")
|
||||
}
|
||||
magazines.joinAndCapitalizeEach()?.let {
|
||||
append("Magazines: ", it, "\n")
|
||||
}
|
||||
cosplayers.joinAndCapitalizeEach()?.let {
|
||||
append("Cosplayers: ", it, "\n")
|
||||
}
|
||||
parodies.joinAndCapitalizeEach()?.let {
|
||||
append("Parodies: ", it, "\n")
|
||||
}
|
||||
characters.joinAndCapitalizeEach()?.let {
|
||||
append("Characters: ", it, "\n")
|
||||
}
|
||||
|
||||
if (appended) append("\n")
|
||||
|
||||
try {
|
||||
append("Posted: ", dateReformat.format(created_at), "\n")
|
||||
} catch (_: Exception) {}
|
||||
|
||||
append("Pages: ", thumbnails.entries.size, "\n\n")
|
||||
}
|
||||
status = SManga.COMPLETED
|
||||
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
||||
initialized = true
|
||||
}
|
||||
|
||||
private fun String.capitalizeEach() = this.split(" ").joinToString(" ") { s ->
|
||||
s.replaceFirstChar { sr ->
|
||||
if (sr.isLowerCase()) sr.titlecase(Locale.getDefault()) else sr.toString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> Collection<T>.emptyToNull(): Collection<T>? {
|
||||
return this.ifEmpty { null }
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class MangaData(
|
||||
val data: Data,
|
||||
)
|
||||
val similar: List<Entry> = emptyList(),
|
||||
) {
|
||||
/**
|
||||
* Return human-readable size of chapter.
|
||||
* @param quality The quality set in PREF_IMAGERES
|
||||
*/
|
||||
fun size(quality: String): String {
|
||||
val dataKey = when (quality) {
|
||||
"1600" -> data.`1600` ?: data.`1280` ?: data.`0`
|
||||
"1280" -> data.`1280` ?: data.`1600` ?: data.`0`
|
||||
"980" -> data.`980` ?: data.`1280` ?: data.`0`
|
||||
"780" -> data.`780` ?: data.`980` ?: data.`0`
|
||||
else -> data.`0`
|
||||
}
|
||||
return dataKey.readableSize()
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class Thumbnails(
|
||||
@ -61,7 +187,7 @@ class Data(
|
||||
class DataKey(
|
||||
val id: Int? = null,
|
||||
val size: Double = 0.0,
|
||||
val public_key: String? = null,
|
||||
val key: String? = null,
|
||||
) {
|
||||
fun readableSize() = when {
|
||||
size >= 300 * 1000 * 1000 -> "${"%.2f".format(size / (1000.0 * 1000.0 * 1000.0))} GB"
|
||||
|
@ -3,35 +3,64 @@ package eu.kanade.tachiyomi.extension.all.koharu
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
|
||||
fun getFilters(): FilterList {
|
||||
object KoharuFilters {
|
||||
var genreList: List<Genre> = KoharuTags.genreList
|
||||
var femaleList: List<Female> = KoharuTags.femaleList
|
||||
var maleList: List<Male> = KoharuTags.maleList
|
||||
var artistList: List<Artist> = KoharuTags.artistList
|
||||
var circleList: List<Circle> = KoharuTags.circleList
|
||||
var parodyList: List<Parody> = KoharuTags.parodyList
|
||||
var mixedList: List<Mixed> = KoharuTags.mixedList
|
||||
var otherList: List<Other> = KoharuTags.otherList
|
||||
|
||||
/**
|
||||
* Whether tags have been fetched
|
||||
*/
|
||||
internal var tagsFetched: Boolean = false
|
||||
|
||||
/**
|
||||
* Inner variable to control how much tries the tags request was called.
|
||||
*/
|
||||
internal var tagsFetchAttempts: Int = 0
|
||||
|
||||
fun getFilters(): FilterList {
|
||||
return FilterList(
|
||||
SortFilter("Sort by", getSortsList),
|
||||
CategoryFilter("Category"),
|
||||
Filter.Separator(),
|
||||
TagFilter("Tags", genreList),
|
||||
TagFilter("Female Tags", femaleList),
|
||||
TagFilter("Male Tags", maleList),
|
||||
TagFilter("Artists", artistList),
|
||||
TagFilter("Circles", circleList),
|
||||
TagFilter("Parodies", parodyList),
|
||||
TagFilter("Mixed", mixedList),
|
||||
TagFilter("Other", otherList),
|
||||
GenreConditionFilter("Include condition", tagsConditionIncludeFilterOptions, "i"),
|
||||
GenreConditionFilter("Exclude condition", tagsConditionExcludeFilterOptions, "e"),
|
||||
Filter.Separator(),
|
||||
Filter.Header("Separate tags with commas (,)"),
|
||||
Filter.Header("Prepend with dash (-) to exclude"),
|
||||
TextFilter("Artists", "artist"),
|
||||
TextFilter("Magazines", "magazine"),
|
||||
TextFilter("Publishers", "publisher"),
|
||||
TextFilter("Characters", "character"),
|
||||
TextFilter("Cosplayers", "cosplayer"),
|
||||
TextFilter("Parodies", "parody"),
|
||||
TextFilter("Circles", "circle"),
|
||||
TextFilter("Male Tags", "male"),
|
||||
TextFilter("Female Tags", "female"),
|
||||
TextFilter("Tags ( Universal )", "tag"),
|
||||
Filter.Header("Filter by pages, for example: (>20)"),
|
||||
TextFilter("Pages", "pages"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal open class TextFilter(name: String, val type: String) : Filter.Text(name)
|
||||
internal open class SortFilter(name: String, private val vals: List<Pair<String, String>>, state: Int = 0) :
|
||||
internal open class TextFilter(name: String, val type: String) : Filter.Text(name)
|
||||
internal open class SortFilter(
|
||||
name: String,
|
||||
private val vals: List<Pair<String, String>>,
|
||||
state: Int = 0,
|
||||
) :
|
||||
Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), state) {
|
||||
fun getValue() = vals[state].second
|
||||
}
|
||||
}
|
||||
|
||||
internal class CategoryFilter(name: String) :
|
||||
internal class CategoryFilter(name: String) :
|
||||
Filter.Group<CheckBoxFilter>(
|
||||
name,
|
||||
listOf(
|
||||
@ -40,12 +69,61 @@ internal class CategoryFilter(name: String) :
|
||||
Pair("Illustration", 8),
|
||||
).map { CheckBoxFilter(it.first, it.second, true) },
|
||||
)
|
||||
internal open class CheckBoxFilter(name: String, val value: Int, state: Boolean) : Filter.CheckBox(name, state)
|
||||
|
||||
private val getSortsList: List<Pair<String, String>> = listOf(
|
||||
internal open class CheckBoxFilter(name: String, val value: Int, state: Boolean) :
|
||||
Filter.CheckBox(name, state)
|
||||
|
||||
private val getSortsList: List<Pair<String, String>> = listOf(
|
||||
Pair("Recently Posted", "4"),
|
||||
Pair("Title", "2"),
|
||||
Pair("Pages", "3"),
|
||||
Pair("Most Viewed", "8"),
|
||||
Pair("Most Favorited", "9"),
|
||||
)
|
||||
)
|
||||
|
||||
internal class GenreConditionFilter(
|
||||
title: String,
|
||||
options: List<Pair<String, String>>,
|
||||
val param: String,
|
||||
) : UriPartFilter(
|
||||
title,
|
||||
options.toTypedArray(),
|
||||
)
|
||||
|
||||
open class Tag(val id: Int, val name: String, val namespace: Int)
|
||||
class Genre(id: Int, name: String) : Tag(id, name, namespace = 0)
|
||||
class Artist(id: Int, name: String) : Tag(id, name, namespace = 1)
|
||||
class Circle(id: Int, name: String) : Tag(id, name, namespace = 2)
|
||||
class Parody(id: Int, name: String) : Tag(id, name, namespace = 3)
|
||||
class Male(id: Int, name: String) : Tag(id, name, namespace = 8)
|
||||
class Female(id: Int, name: String) : Tag(id, name, namespace = 9)
|
||||
class Mixed(id: Int, name: String) : Tag(id, name, namespace = 10)
|
||||
class Other(id: Int, name: String) : Tag(id, name, namespace = 12)
|
||||
|
||||
internal class TagFilter(title: String, tags: List<Tag>) :
|
||||
Filter.Group<TagTriState>(title, tags.map { TagTriState(it.name, it.id) })
|
||||
|
||||
internal class TagTriState(name: String, val id: Int) : Filter.TriState(name)
|
||||
|
||||
open class UriPartFilter(
|
||||
displayName: String,
|
||||
private val vals: Array<Pair<String, String>>,
|
||||
state: Int = 0,
|
||||
) :
|
||||
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray(), state) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
// https://api.schale.network/books?include=<id>,<id>&i=1&exclude=<id>,<id>&e=1
|
||||
private val tagsConditionIncludeFilterOptions: List<Pair<String, String>> =
|
||||
listOf(
|
||||
"AND" to "",
|
||||
"OR" to "1",
|
||||
)
|
||||
|
||||
private val tagsConditionExcludeFilterOptions: List<Pair<String, String>> =
|
||||
listOf(
|
||||
"OR" to "",
|
||||
"AND" to "1",
|
||||
)
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,242 @@
|
||||
package eu.kanade.tachiyomi.extension.all.koharu
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import eu.kanade.tachiyomi.extension.all.koharu.Koharu.Companion.authorization
|
||||
import eu.kanade.tachiyomi.extension.all.koharu.Koharu.Companion.token
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/** Cloudflare Turnstile interceptor */
|
||||
class TurnstileInterceptor(
|
||||
private val client: OkHttpClient,
|
||||
private val domainUrl: String,
|
||||
private val authUrl: String,
|
||||
private val userAgent: String?,
|
||||
) : Interceptor {
|
||||
|
||||
private val context: Application by injectLazy()
|
||||
private val handler by lazy { Handler(Looper.getMainLooper()) }
|
||||
|
||||
private val lazyHeaders by lazy {
|
||||
Headers.Builder().apply {
|
||||
set("User-Agent", userAgent ?: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36")
|
||||
set("Referer", "$domainUrl/")
|
||||
set("Origin", domainUrl)
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun authHeaders(authorization: String) =
|
||||
Headers.Builder().apply {
|
||||
set("User-Agent", userAgent ?: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36")
|
||||
set("Referer", "$domainUrl/")
|
||||
set("Origin", domainUrl)
|
||||
set("Authorization", authorization)
|
||||
}.build()
|
||||
|
||||
private val authorizedRequestRegex by lazy { Regex("""(.+\?crt=)(.*)""") }
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
|
||||
val url = request.url.toString()
|
||||
val matchResult = authorizedRequestRegex.find(url) ?: return chain.proceed(request)
|
||||
if (matchResult.groupValues.size == 3) {
|
||||
val requestingUrl = matchResult.groupValues[1]
|
||||
val crt = matchResult.groupValues[2]
|
||||
var newResponse: Response
|
||||
|
||||
if (crt.isNotBlank() && crt != "null") {
|
||||
// Token already set in URL, just make the request
|
||||
newResponse = chain.proceed(request)
|
||||
if (newResponse.code !in listOf(400, 403)) return newResponse
|
||||
} else {
|
||||
// Token doesn't include, add token then make request
|
||||
if (token.isNullOrBlank()) resolveInWebview()
|
||||
val newRequest = if (request.method == "POST") {
|
||||
POST("${requestingUrl}$token", lazyHeaders)
|
||||
} else {
|
||||
GET("${requestingUrl}$token", lazyHeaders)
|
||||
}
|
||||
newResponse = chain.proceed(newRequest)
|
||||
if (newResponse.code !in listOf(400, 403)) return newResponse
|
||||
}
|
||||
newResponse.close()
|
||||
|
||||
// Request failed, refresh token then try again
|
||||
clearToken()
|
||||
token = null
|
||||
resolveInWebview()
|
||||
val newRequest = if (request.method == "POST") {
|
||||
POST("${requestingUrl}$token", lazyHeaders)
|
||||
} else {
|
||||
GET("${requestingUrl}$token", lazyHeaders)
|
||||
}
|
||||
newResponse = chain.proceed(newRequest)
|
||||
if (newResponse.code !in listOf(400, 403)) return newResponse
|
||||
throw IOException("Open webview once to refresh token (${newResponse.code})")
|
||||
}
|
||||
return chain.proceed(request)
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
fun resolveInWebview(): Pair<String?, String?> {
|
||||
val latch = CountDownLatch(1)
|
||||
var webView: WebView? = null
|
||||
|
||||
handler.post {
|
||||
val webview = WebView(context)
|
||||
webView = webview
|
||||
with(webview.settings) {
|
||||
javaScriptEnabled = true
|
||||
domStorageEnabled = true
|
||||
databaseEnabled = true
|
||||
useWideViewPort = false
|
||||
loadWithOverviewMode = false
|
||||
}
|
||||
|
||||
webview.webViewClient = object : WebViewClient() {
|
||||
override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
|
||||
val authHeader = request?.requestHeaders?.get("Authorization")
|
||||
if (request?.url.toString().contains(authUrl) && authHeader != null) {
|
||||
authorization = authHeader
|
||||
if (request.method == "POST") {
|
||||
// Authorize & requesting a new token.
|
||||
// `authorization` here should be in format: Bearer <authorization>
|
||||
try {
|
||||
val noRedirectClient = client.newBuilder().followRedirects(false).build()
|
||||
val authHeaders = authHeaders(authHeader)
|
||||
val response = noRedirectClient.newCall(POST(authUrl, authHeaders)).execute()
|
||||
response.use {
|
||||
if (response.isSuccessful) {
|
||||
with(response) {
|
||||
token = body.string()
|
||||
.removeSurrounding("\"")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_: IOException) {
|
||||
} finally {
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
if (request.method == "GET") {
|
||||
// Site is trying to recheck old token validation here.
|
||||
// If it fails then site will request a new one using POST method.
|
||||
// But we will check it ourselves.
|
||||
// Normally this might not occur because old token should already be acquired & rechecked via onPageFinished.
|
||||
// `authorization` here should be in format: Bearer <token>
|
||||
val oldToken = authorization
|
||||
?.substringAfterLast(" ")
|
||||
if (oldToken != null && recheckTokenValid(oldToken)) {
|
||||
token = oldToken
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.shouldInterceptRequest(view, request)
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the saved token in localStorage and use it.
|
||||
* This token might already expired. Normally site will check token for expiration with a GET request.
|
||||
* Here will will recheck it ourselves.
|
||||
*/
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
if (view == null) return
|
||||
val script = "javascript:localStorage['clearance']"
|
||||
view.evaluateJavascript(script) {
|
||||
// Avoid overwrite newly requested token
|
||||
if (!it.isNullOrBlank() && it != "null" && token.isNullOrBlank()) {
|
||||
val oldToken = it
|
||||
.removeSurrounding("\"")
|
||||
if (recheckTokenValid(oldToken)) {
|
||||
token = oldToken
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun recheckTokenValid(token: String): Boolean {
|
||||
try {
|
||||
val noRedirectClient = client.newBuilder().followRedirects(false).build()
|
||||
val authHeaders = authHeaders("Bearer $token")
|
||||
val response = noRedirectClient.newCall(GET(authUrl, authHeaders)).execute()
|
||||
response.use {
|
||||
if (response.isSuccessful) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} catch (_: IOException) {
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
webview.loadUrl("$domainUrl/")
|
||||
}
|
||||
|
||||
latch.await(20, TimeUnit.SECONDS)
|
||||
|
||||
handler.post {
|
||||
// One last try to read the token from localStorage, in case it got updated last minute.
|
||||
if (token.isNullOrBlank()) {
|
||||
val script = "javascript:localStorage['clearance']"
|
||||
webView?.evaluateJavascript(script) {
|
||||
if (!it.isNullOrBlank() && it != "null") {
|
||||
token = it
|
||||
.removeSurrounding("\"")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
webView?.stopLoading()
|
||||
webView?.destroy()
|
||||
webView = null
|
||||
}
|
||||
|
||||
return token to authorization
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
private fun clearToken() {
|
||||
val latch = CountDownLatch(1)
|
||||
handler.post {
|
||||
val webView = WebView(context)
|
||||
with(webView.settings) {
|
||||
javaScriptEnabled = true
|
||||
domStorageEnabled = true
|
||||
databaseEnabled = true
|
||||
}
|
||||
webView.webViewClient = object : WebViewClient() {
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
if (view == null) return
|
||||
val script = "javascript:localStorage.clear()"
|
||||
view.evaluateJavascript(script) {
|
||||
token = null
|
||||
view.stopLoading()
|
||||
view.destroy()
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
webView.loadUrl(domainUrl)
|
||||
}
|
||||
latch.await(20, TimeUnit.SECONDS)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user