Fix some authorization issues
This commit is contained in:
@@ -28,9 +28,8 @@ abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(), OnApplyWindo
|
||||
protected lateinit var binding: B
|
||||
private set
|
||||
|
||||
protected val exceptionResolver by lazy(LazyThreadSafetyMode.NONE) {
|
||||
ExceptionResolver(this, supportFragmentManager)
|
||||
}
|
||||
@Suppress("LeakingThis")
|
||||
protected val exceptionResolver = ExceptionResolver(this)
|
||||
|
||||
private var lastInsets: Insets = Insets.NONE
|
||||
|
||||
|
||||
@@ -20,9 +20,8 @@ abstract class BaseFragment<B : ViewBinding> : Fragment(), OnApplyWindowInsetsLi
|
||||
protected val binding: B
|
||||
get() = checkNotNull(viewBinding)
|
||||
|
||||
protected val exceptionResolver by lazy(LazyThreadSafetyMode.NONE) {
|
||||
ExceptionResolver(viewLifecycleOwner, childFragmentManager)
|
||||
}
|
||||
@Suppress("LeakingThis")
|
||||
protected val exceptionResolver = ExceptionResolver(this)
|
||||
|
||||
private var lastInsets: Insets = Insets.NONE
|
||||
|
||||
|
||||
@@ -3,9 +3,10 @@ package org.koitharu.kotatsu.core.exceptions
|
||||
import androidx.annotation.StringRes
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ResolvableException
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
|
||||
class AuthRequiredException(
|
||||
val url: String
|
||||
val source: MangaSource,
|
||||
) : RuntimeException("Authorization required"), ResolvableException {
|
||||
|
||||
@StringRes
|
||||
|
||||
@@ -1,40 +1,71 @@
|
||||
package org.koitharu.kotatsu.core.exceptions.resolve
|
||||
|
||||
import android.util.ArrayMap
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog
|
||||
import org.koitharu.kotatsu.core.exceptions.AuthRequiredException
|
||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
|
||||
import org.koitharu.kotatsu.utils.TaggedActivityResult
|
||||
import org.koitharu.kotatsu.utils.isSuccess
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
class ExceptionResolver(
|
||||
private val lifecycleOwner: LifecycleOwner,
|
||||
private val fm: FragmentManager
|
||||
) {
|
||||
class ExceptionResolver private constructor(
|
||||
private val activity: FragmentActivity?,
|
||||
private val fragment: Fragment?,
|
||||
): ActivityResultCallback<TaggedActivityResult> {
|
||||
|
||||
private val continuations = ArrayMap<String, Continuation<Boolean>>(1)
|
||||
private lateinit var sourceAuthContract: ActivityResultLauncher<MangaSource>
|
||||
|
||||
constructor(activity: FragmentActivity) : this(activity = activity, fragment = null) {
|
||||
sourceAuthContract = activity.registerForActivityResult(SourceAuthActivity.Contract(), this)
|
||||
}
|
||||
|
||||
constructor(fragment: Fragment) : this(activity = null, fragment = fragment) {
|
||||
sourceAuthContract = fragment.registerForActivityResult(SourceAuthActivity.Contract(), this)
|
||||
}
|
||||
|
||||
override fun onActivityResult(result: TaggedActivityResult?) {
|
||||
result ?: return
|
||||
continuations.remove(result.tag)?.resume(result.isSuccess)
|
||||
}
|
||||
|
||||
suspend fun resolve(e: ResolvableException): Boolean = when (e) {
|
||||
is CloudFlareProtectedException -> resolveCF(e.url)
|
||||
is AuthRequiredException -> false //TODO
|
||||
is AuthRequiredException -> resolveAuthException(e.source)
|
||||
else -> false
|
||||
}
|
||||
|
||||
private suspend fun resolveCF(url: String) = suspendCancellableCoroutine<Boolean> { cont ->
|
||||
private suspend fun resolveCF(url: String): Boolean {
|
||||
val dialog = CloudFlareDialog.newInstance(url)
|
||||
fm.clearFragmentResult(CloudFlareDialog.TAG)
|
||||
continuations[CloudFlareDialog.TAG] = cont
|
||||
fm.setFragmentResultListener(CloudFlareDialog.TAG, lifecycleOwner) { key, result ->
|
||||
continuations.remove(key)?.resume(result.getBoolean(CloudFlareDialog.EXTRA_RESULT))
|
||||
}
|
||||
dialog.show(fm, CloudFlareDialog.TAG)
|
||||
cont.invokeOnCancellation {
|
||||
continuations.remove(CloudFlareDialog.TAG, cont)
|
||||
fm.clearFragmentResultListener(CloudFlareDialog.TAG)
|
||||
dialog.dismiss()
|
||||
val fm = getFragmentManager()
|
||||
return suspendCancellableCoroutine { cont ->
|
||||
fm.clearFragmentResult(CloudFlareDialog.TAG)
|
||||
continuations[CloudFlareDialog.TAG] = cont
|
||||
fm.setFragmentResultListener(CloudFlareDialog.TAG, checkNotNull(fragment ?: activity)) { key, result ->
|
||||
continuations.remove(key)?.resume(result.getBoolean(CloudFlareDialog.EXTRA_RESULT))
|
||||
}
|
||||
dialog.show(fm, CloudFlareDialog.TAG)
|
||||
cont.invokeOnCancellation {
|
||||
continuations.remove(CloudFlareDialog.TAG, cont)
|
||||
fm.clearFragmentResultListener(CloudFlareDialog.TAG)
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun resolveAuthException(source: MangaSource): Boolean = suspendCoroutine { cont ->
|
||||
continuations[SourceAuthActivity.TAG] = cont
|
||||
sourceAuthContract.launch(source)
|
||||
}
|
||||
|
||||
private fun getFragmentManager() = checkNotNull(fragment?.childFragmentManager ?: activity?.supportFragmentManager)
|
||||
}
|
||||
@@ -5,4 +5,6 @@ interface MangaRepositoryAuthProvider {
|
||||
val authUrl: String
|
||||
|
||||
fun isAuthorized(): Boolean
|
||||
|
||||
suspend fun getUsername(): String
|
||||
}
|
||||
@@ -61,7 +61,7 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe
|
||||
tags = runCatching {
|
||||
row.selectFirst("div.genre")?.select("a")?.mapToSet {
|
||||
MangaTag(
|
||||
title = it.text().toTitleCase(),
|
||||
title = it.text().toTagName(),
|
||||
key = it.attr("href").substringAfterLast('/').urlEncoded(),
|
||||
source = source
|
||||
)
|
||||
@@ -136,7 +136,7 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe
|
||||
return root.select("li.sidetag").mapToSet { li ->
|
||||
val a = li.children().last() ?: throw ParseException("a is null")
|
||||
MangaTag(
|
||||
title = a.text().toTitleCase(),
|
||||
title = a.text().toTagName(),
|
||||
key = a.attr("href").substringAfterLast('/'),
|
||||
source = source
|
||||
)
|
||||
@@ -159,4 +159,5 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe
|
||||
else -> "favdesc"
|
||||
}
|
||||
|
||||
private fun String.toTagName() = replace('_', ' ').toTitleCase()
|
||||
}
|
||||
@@ -2,10 +2,13 @@ package org.koitharu.kotatsu.core.parser.site
|
||||
|
||||
import org.jsoup.nodes.Element
|
||||
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.core.exceptions.AuthRequiredException
|
||||
import org.koitharu.kotatsu.core.exceptions.ParseException
|
||||
import org.koitharu.kotatsu.core.model.*
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepositoryAuthProvider
|
||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
import java.util.*
|
||||
import kotlin.math.pow
|
||||
|
||||
private const val DOMAIN_UNAUTHORIZED = "e-hentai.org"
|
||||
@@ -17,7 +20,9 @@ class ExHentaiRepository(
|
||||
|
||||
override val source = MangaSource.EXHENTAI
|
||||
|
||||
override val sortOrders: Set<SortOrder> = emptySet()
|
||||
override val sortOrders: Set<SortOrder> = EnumSet.of(
|
||||
SortOrder.NEWEST,
|
||||
)
|
||||
|
||||
override val defaultDomain: String
|
||||
get() = if (isAuthorized()) DOMAIN_AUTHORIZED else DOMAIN_UNAUTHORIZED
|
||||
@@ -206,6 +211,20 @@ class ExHentaiRepository(
|
||||
return false
|
||||
}
|
||||
|
||||
override suspend fun getUsername(): String {
|
||||
val doc = loaderContext.httpGet("https://forums.${DOMAIN_UNAUTHORIZED}/").parseHtml().body()
|
||||
val username = doc.getElementById("userlinks")
|
||||
?.getElementsByAttributeValueContaining("href", "?showuser=")
|
||||
?.firstOrNull()
|
||||
?.ownText()
|
||||
?: if (doc.getElementById("userlinksguest") != null) {
|
||||
throw AuthRequiredException(source)
|
||||
} else {
|
||||
throw ParseException()
|
||||
}
|
||||
return username
|
||||
}
|
||||
|
||||
private fun isAuthorized(domain: String): Boolean {
|
||||
val cookies = loaderContext.cookieJar.getCookies(domain).mapToSet { x -> x.name }
|
||||
return authCookies.all { it in cookies }
|
||||
|
||||
@@ -7,18 +7,22 @@ import org.koitharu.kotatsu.base.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.core.exceptions.AuthRequiredException
|
||||
import org.koitharu.kotatsu.core.exceptions.ParseException
|
||||
import org.koitharu.kotatsu.core.model.*
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepositoryAuthProvider
|
||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
open class MangaLibRepository(loaderContext: MangaLoaderContext) :
|
||||
RemoteMangaRepository(loaderContext) {
|
||||
RemoteMangaRepository(loaderContext), MangaRepositoryAuthProvider {
|
||||
|
||||
override val defaultDomain = "mangalib.me"
|
||||
|
||||
override val source = MangaSource.MANGALIB
|
||||
|
||||
override val authUrl: String
|
||||
get() = "https://${getDomain()}/login"
|
||||
|
||||
override val sortOrders: Set<SortOrder> = EnumSet.of(
|
||||
SortOrder.RATING,
|
||||
SortOrder.ALPHABETICAL,
|
||||
@@ -153,7 +157,7 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
|
||||
val fullUrl = chapter.url.withDomain()
|
||||
val doc = loaderContext.httpGet(fullUrl).parseHtml()
|
||||
if (doc.location().endsWith("/register")) {
|
||||
throw AuthRequiredException("/login".inContextOf(doc))
|
||||
throw AuthRequiredException(source)
|
||||
}
|
||||
val scripts = doc.head().select("script")
|
||||
val pg = (doc.body().getElementById("pg")?.html() ?: parseFailed("Element #pg not found"))
|
||||
@@ -212,6 +216,14 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
|
||||
throw ParseException("Script with genres not found")
|
||||
}
|
||||
|
||||
override fun isAuthorized(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override suspend fun getUsername(): String {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
private fun getSortKey(sortOrder: SortOrder?) = when (sortOrder) {
|
||||
SortOrder.RATING -> "desc&sort=rate"
|
||||
SortOrder.ALPHABETICAL -> "asc&sort=name"
|
||||
|
||||
@@ -170,7 +170,7 @@ class NudeMoonRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposit
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getUsername(): String {
|
||||
override suspend fun getUsername(): String {
|
||||
val body = loaderContext.httpGet("https://${getDomain()}/").parseHtml()
|
||||
.body()
|
||||
return body
|
||||
@@ -180,7 +180,7 @@ class NudeMoonRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposit
|
||||
?.substringAfterLast('/')
|
||||
?: run {
|
||||
throw if (body.selectFirst("form[name=\"loginform\"]") != null) {
|
||||
AuthRequiredException(authUrl)
|
||||
AuthRequiredException(source)
|
||||
} else {
|
||||
ParseException("Cannot find username")
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.core.parser.site
|
||||
|
||||
import okhttp3.Headers
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
@@ -9,6 +10,7 @@ import org.koitharu.kotatsu.core.model.*
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepositoryAuthProvider
|
||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
import java.net.URLDecoder
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
@@ -62,7 +64,7 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
|
||||
.append((offset / PAGE_SIZE) + 1)
|
||||
.append("&count=")
|
||||
.append(PAGE_SIZE)
|
||||
val content = loaderContext.httpGet(urlBuilder.toString()).parseJson()
|
||||
val content = loaderContext.httpGet(urlBuilder.toString(), getApiHeaders()).parseJson()
|
||||
.getJSONArray("content")
|
||||
return content.map { jo ->
|
||||
val url = "/manga/${jo.getString("dir")}"
|
||||
@@ -95,7 +97,8 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
|
||||
val slug = manga.url.find(regexLastUrlPath)
|
||||
?: throw ParseException("Cannot obtain slug from ${manga.url}")
|
||||
val data = loaderContext.httpGet(
|
||||
url = "https://api.$domain/api/titles/$slug/"
|
||||
url = "https://api.$domain/api/titles/$slug/",
|
||||
headers = getApiHeaders(),
|
||||
).parseJson()
|
||||
val content = try {
|
||||
data.getJSONObject("content")
|
||||
@@ -150,7 +153,7 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val referer = "https://${getDomain()}/"
|
||||
val content = loaderContext.httpGet(chapter.url.withDomain(subdomain = "api")).parseJson()
|
||||
val content = loaderContext.httpGet(chapter.url.withDomain(subdomain = "api"), getApiHeaders()).parseJson()
|
||||
.getJSONObject("content")
|
||||
val pages = content.optJSONArray("pages")
|
||||
if (pages == null) {
|
||||
@@ -177,7 +180,7 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
|
||||
|
||||
override suspend fun getTags(): Set<MangaTag> {
|
||||
val domain = getDomain()
|
||||
val content = loaderContext.httpGet("https://api.$domain/api/forms/titles/?get=genres")
|
||||
val content = loaderContext.httpGet("https://api.$domain/api/forms/titles/?get=genres", getApiHeaders())
|
||||
.parseJson().getJSONObject("content").getJSONArray("genres")
|
||||
return content.mapToSet { jo ->
|
||||
MangaTag(
|
||||
@@ -194,6 +197,23 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getUsername(): String {
|
||||
val jo = loaderContext.httpGet(
|
||||
url = "https://api.${getDomain()}/api/users/current/",
|
||||
headers = getApiHeaders(),
|
||||
).parseJson()
|
||||
return jo.getJSONObject("content").getString("username")
|
||||
}
|
||||
|
||||
private fun getApiHeaders(): Headers? {
|
||||
val userCookie = loaderContext.cookieJar.getCookies(getDomain()).find {
|
||||
it.name == "user"
|
||||
} ?: return null
|
||||
val jo = JSONObject(URLDecoder.decode(userCookie.value, Charsets.UTF_8.name()))
|
||||
val accessToken = jo.getStringOrNull("access_token") ?: return null
|
||||
return Headers.headersOf("authorization", "bearer $accessToken")
|
||||
}
|
||||
|
||||
private fun copyCookies() {
|
||||
val domain = getDomain()
|
||||
loaderContext.cookieJar.copyCookies(domain, "api.$domain")
|
||||
@@ -220,7 +240,8 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
|
||||
var page = 1
|
||||
while (true) {
|
||||
val content = loaderContext.httpGet(
|
||||
"https://api.$domain/api/titles/chapters/?branch_id=$branchId&page=$page&count=100"
|
||||
url = "https://api.$domain/api/titles/chapters/?branch_id=$branchId&page=$page&count=100",
|
||||
headers = getApiHeaders(),
|
||||
).parseJson().getJSONArray("content")
|
||||
val len = content.length()
|
||||
if (len == 0) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.reader.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
@@ -35,6 +36,7 @@ import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.domain.MangaIntent
|
||||
import org.koitharu.kotatsu.base.ui.BaseFullscreenActivity
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ResolvableException
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
@@ -216,14 +218,14 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
|
||||
}
|
||||
|
||||
private fun onError(e: Throwable) {
|
||||
val listener = ErrorDialogListener(e)
|
||||
val dialog = MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.error_occurred)
|
||||
.setMessage(e.getDisplayMessage(resources))
|
||||
.setPositiveButton(R.string.close, null)
|
||||
if (viewModel.content.value?.pages.isNullOrEmpty()) {
|
||||
dialog.setOnDismissListener {
|
||||
finish()
|
||||
}
|
||||
.setNegativeButton(R.string.close, listener)
|
||||
.setOnCancelListener(listener)
|
||||
if (e is ResolvableException) {
|
||||
dialog.setPositiveButton(e.resolveTextId, listener)
|
||||
}
|
||||
dialog.show()
|
||||
}
|
||||
@@ -369,6 +371,36 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
|
||||
}
|
||||
}
|
||||
|
||||
private inner class ErrorDialogListener(
|
||||
private val exception: Throwable,
|
||||
) : DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
|
||||
|
||||
override fun onClick(dialog: DialogInterface?, which: Int) {
|
||||
if (which == DialogInterface.BUTTON_POSITIVE && exception is ResolvableException) {
|
||||
dialog?.dismiss()
|
||||
tryResolve(exception)
|
||||
} else {
|
||||
onCancel(dialog)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCancel(dialog: DialogInterface?) {
|
||||
if (viewModel.content.value?.pages.isNullOrEmpty()) {
|
||||
finishAfterTransition()
|
||||
}
|
||||
}
|
||||
|
||||
private fun tryResolve(e: ResolvableException) {
|
||||
lifecycleScope.launch {
|
||||
if (exceptionResolver.resolve(e)) {
|
||||
viewModel.reload()
|
||||
} else {
|
||||
onCancel(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val ACTION_MANGA_READ = "${BuildConfig.APPLICATION_ID}.action.READ_MANGA"
|
||||
|
||||
@@ -32,8 +32,8 @@ import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
||||
|
||||
class ReaderViewModel(
|
||||
intent: MangaIntent,
|
||||
state: ReaderState?,
|
||||
private val intent: MangaIntent,
|
||||
initialState: ReaderState?,
|
||||
private val dataRepository: MangaDataRepository,
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val shortcutsRepository: ShortcutsRepository,
|
||||
@@ -42,7 +42,7 @@ class ReaderViewModel(
|
||||
) : BaseViewModel() {
|
||||
|
||||
private var loadingJob: Job? = null
|
||||
private val currentState = MutableStateFlow<ReaderState?>(null)
|
||||
private val currentState = MutableStateFlow(initialState)
|
||||
private val mangaData = MutableStateFlow(intent.manga)
|
||||
private val chapters = LongSparseArray<MangaChapter>()
|
||||
|
||||
@@ -87,45 +87,7 @@ class ReaderViewModel(
|
||||
val onZoomChanged = SingleLiveEvent<Unit>()
|
||||
|
||||
init {
|
||||
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
||||
var manga = dataRepository.resolveIntent(intent)
|
||||
?: throw MangaNotFoundException("Cannot find manga")
|
||||
mangaData.value = manga
|
||||
val repo = MangaRepository(manga.source)
|
||||
manga = repo.getDetails(manga)
|
||||
manga.chapters?.forEach {
|
||||
chapters.put(it.id, it)
|
||||
}
|
||||
// determine mode
|
||||
val mode =
|
||||
dataRepository.getReaderMode(manga.id) ?: manga.chapters?.randomOrNull()?.let {
|
||||
val pages = repo.getPages(it)
|
||||
val isWebtoon = MangaUtils.determineMangaIsWebtoon(pages)
|
||||
val newMode = getReaderMode(isWebtoon)
|
||||
if (isWebtoon != null) {
|
||||
dataRepository.savePreferences(manga, newMode)
|
||||
}
|
||||
newMode
|
||||
} ?: error("There are no chapters in this manga")
|
||||
// obtain state
|
||||
currentState.value = state ?: historyRepository.getOne(manga)?.let {
|
||||
ReaderState.from(it)
|
||||
} ?: ReaderState.initial(manga)
|
||||
|
||||
val branch = chapters[currentState.value?.chapterId ?: 0L].branch
|
||||
mangaData.value = manga.copy(chapters = manga.chapters?.filter { it.branch == branch })
|
||||
readerMode.postValue(mode)
|
||||
|
||||
val pages = loadChapter(requireNotNull(currentState.value).chapterId)
|
||||
// save state
|
||||
currentState.value?.let {
|
||||
historyRepository.addOrUpdate(manga, it.chapterId, it.page, it.scroll)
|
||||
shortcutsRepository.updateShortcuts()
|
||||
}
|
||||
|
||||
content.postValue(ReaderContent(pages, currentState.value))
|
||||
}
|
||||
|
||||
loadImpl()
|
||||
subscribeToSettings()
|
||||
}
|
||||
|
||||
@@ -134,6 +96,11 @@ class ReaderViewModel(
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun reload() {
|
||||
loadingJob?.cancel()
|
||||
loadImpl()
|
||||
}
|
||||
|
||||
fun switchMode(newMode: ReaderMode) {
|
||||
launchJob {
|
||||
val manga = checkNotNull(mangaData.value)
|
||||
@@ -219,6 +186,49 @@ class ReaderViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadImpl() {
|
||||
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
||||
var manga = dataRepository.resolveIntent(intent)
|
||||
?: throw MangaNotFoundException("Cannot find manga")
|
||||
mangaData.value = manga
|
||||
val repo = MangaRepository(manga.source)
|
||||
manga = repo.getDetails(manga)
|
||||
manga.chapters?.forEach {
|
||||
chapters.put(it.id, it)
|
||||
}
|
||||
// determine mode
|
||||
val mode =
|
||||
dataRepository.getReaderMode(manga.id) ?: manga.chapters?.randomOrNull()?.let {
|
||||
val pages = repo.getPages(it)
|
||||
val isWebtoon = MangaUtils.determineMangaIsWebtoon(pages)
|
||||
val newMode = getReaderMode(isWebtoon)
|
||||
if (isWebtoon != null) {
|
||||
dataRepository.savePreferences(manga, newMode)
|
||||
}
|
||||
newMode
|
||||
} ?: error("There are no chapters in this manga")
|
||||
// obtain state
|
||||
if (currentState.value == null) {
|
||||
currentState.value = historyRepository.getOne(manga)?.let {
|
||||
ReaderState.from(it)
|
||||
} ?: ReaderState.initial(manga)
|
||||
}
|
||||
|
||||
val branch = chapters[currentState.value?.chapterId ?: 0L].branch
|
||||
mangaData.value = manga.copy(chapters = manga.chapters?.filter { it.branch == branch })
|
||||
readerMode.postValue(mode)
|
||||
|
||||
val pages = loadChapter(requireNotNull(currentState.value).chapterId)
|
||||
// save state
|
||||
currentState.value?.let {
|
||||
historyRepository.addOrUpdate(manga, it.chapterId, it.page, it.scroll)
|
||||
shortcutsRepository.updateShortcuts()
|
||||
}
|
||||
|
||||
content.postValue(ReaderContent(pages, currentState.value))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getReaderMode(isWebtoon: Boolean?) = when {
|
||||
isWebtoon == true -> ReaderMode.WEBTOON
|
||||
settings.isPreferRtlReader -> ReaderMode.REVERSED
|
||||
|
||||
@@ -2,12 +2,18 @@ package org.koitharu.kotatsu.settings
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.ArrayMap
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.TwoStatePreference
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.AuthRequiredException
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepositoryAuthProvider
|
||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||
@@ -17,6 +23,7 @@ import org.koitharu.kotatsu.settings.utils.EditTextBindListener
|
||||
import org.koitharu.kotatsu.settings.utils.EditTextDefaultSummaryProvider
|
||||
import org.koitharu.kotatsu.utils.ext.mangaRepositoryOf
|
||||
import org.koitharu.kotatsu.utils.ext.parcelableArgument
|
||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
|
||||
class SourceSettingsFragment : PreferenceFragmentCompat() {
|
||||
@@ -51,6 +58,15 @@ class SourceSettingsFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
findPreference<Preference>(SourceSettings.KEY_AUTH)?.run {
|
||||
if (isVisible) {
|
||||
loadUsername(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
return when (preference.key) {
|
||||
SourceSettings.KEY_AUTH -> {
|
||||
@@ -87,6 +103,21 @@ class SourceSettingsFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadUsername(preference: Preference) = viewLifecycleScope.launch {
|
||||
runCatching {
|
||||
withContext(Dispatchers.Default) {
|
||||
(repository as MangaRepositoryAuthProvider).getUsername()
|
||||
}
|
||||
}.onSuccess { username ->
|
||||
preference.title = getString(R.string.logged_in_as, username)
|
||||
}.onFailure { error ->
|
||||
preference.isEnabled = error is AuthRequiredException
|
||||
if (BuildConfig.DEBUG) {
|
||||
error.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val EXTRA_SOURCE = "source"
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package org.koitharu.kotatsu.settings.sources.auth
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
@@ -17,6 +19,7 @@ import org.koitharu.kotatsu.browser.BrowserClient
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepositoryAuthProvider
|
||||
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
|
||||
import org.koitharu.kotatsu.utils.TaggedActivityResult
|
||||
import org.koitharu.kotatsu.utils.ext.mangaRepositoryOf
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@@ -61,6 +64,7 @@ class SourceAuthActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallba
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
binding.webView.stopLoading()
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
finishAfterTransition()
|
||||
true
|
||||
}
|
||||
@@ -89,6 +93,7 @@ class SourceAuthActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallba
|
||||
binding.progressBar.isVisible = isLoading
|
||||
if (!isLoading && repository.isAuthorized()) {
|
||||
Toast.makeText(this, R.string.auth_complete, Toast.LENGTH_SHORT).show()
|
||||
setResult(Activity.RESULT_OK)
|
||||
finishAfterTransition()
|
||||
}
|
||||
}
|
||||
@@ -103,9 +108,20 @@ class SourceAuthActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallba
|
||||
binding.webView.updatePadding(bottom = insets.bottom)
|
||||
}
|
||||
|
||||
class Contract : ActivityResultContract<MangaSource, TaggedActivityResult>() {
|
||||
override fun createIntent(context: Context, input: MangaSource): Intent {
|
||||
return newIntent(context, input)
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): TaggedActivityResult {
|
||||
return TaggedActivityResult(TAG, resultCode)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val EXTRA_SOURCE = "source"
|
||||
const val TAG = "SourceAuthActivity"
|
||||
|
||||
fun newIntent(context: Context, source: MangaSource): Intent {
|
||||
return Intent(context, SourceAuthActivity::class.java)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.koitharu.kotatsu.utils
|
||||
|
||||
import android.app.Activity
|
||||
|
||||
class TaggedActivityResult(
|
||||
val tag: String,
|
||||
val result: Int,
|
||||
)
|
||||
|
||||
val TaggedActivityResult.isSuccess: Boolean
|
||||
get() = this.result == Activity.RESULT_OK
|
||||
@@ -265,4 +265,5 @@
|
||||
<string name="only_using_wifi">Only using WiFi</string>
|
||||
<string name="always">Always</string>
|
||||
<string name="preload_pages">Preload pages</string>
|
||||
<string name="logged_in_as">Logged in as %s</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user