Fix some authorization issues

pull/126/head
Koitharu 4 years ago
parent 2947cd3038
commit 3aed24fb49
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -28,9 +28,8 @@ abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(), OnApplyWindo
protected lateinit var binding: B protected lateinit var binding: B
private set private set
protected val exceptionResolver by lazy(LazyThreadSafetyMode.NONE) { @Suppress("LeakingThis")
ExceptionResolver(this, supportFragmentManager) protected val exceptionResolver = ExceptionResolver(this)
}
private var lastInsets: Insets = Insets.NONE private var lastInsets: Insets = Insets.NONE

@ -20,9 +20,8 @@ abstract class BaseFragment<B : ViewBinding> : Fragment(), OnApplyWindowInsetsLi
protected val binding: B protected val binding: B
get() = checkNotNull(viewBinding) get() = checkNotNull(viewBinding)
protected val exceptionResolver by lazy(LazyThreadSafetyMode.NONE) { @Suppress("LeakingThis")
ExceptionResolver(viewLifecycleOwner, childFragmentManager) protected val exceptionResolver = ExceptionResolver(this)
}
private var lastInsets: Insets = Insets.NONE private var lastInsets: Insets = Insets.NONE

@ -3,9 +3,10 @@ package org.koitharu.kotatsu.core.exceptions
import androidx.annotation.StringRes import androidx.annotation.StringRes
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ResolvableException import org.koitharu.kotatsu.core.exceptions.resolve.ResolvableException
import org.koitharu.kotatsu.core.model.MangaSource
class AuthRequiredException( class AuthRequiredException(
val url: String val source: MangaSource,
) : RuntimeException("Authorization required"), ResolvableException { ) : RuntimeException("Authorization required"), ResolvableException {
@StringRes @StringRes

@ -1,33 +1,56 @@
package org.koitharu.kotatsu.core.exceptions.resolve package org.koitharu.kotatsu.core.exceptions.resolve
import android.util.ArrayMap import android.util.ArrayMap
import androidx.fragment.app.FragmentManager import androidx.activity.result.ActivityResultCallback
import androidx.lifecycle.LifecycleOwner import androidx.activity.result.ActivityResultLauncher
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog
import org.koitharu.kotatsu.core.exceptions.AuthRequiredException import org.koitharu.kotatsu.core.exceptions.AuthRequiredException
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException 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.Continuation
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class ExceptionResolver( class ExceptionResolver private constructor(
private val lifecycleOwner: LifecycleOwner, private val activity: FragmentActivity?,
private val fm: FragmentManager private val fragment: Fragment?,
) { ): ActivityResultCallback<TaggedActivityResult> {
private val continuations = ArrayMap<String, Continuation<Boolean>>(1) 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) { suspend fun resolve(e: ResolvableException): Boolean = when (e) {
is CloudFlareProtectedException -> resolveCF(e.url) is CloudFlareProtectedException -> resolveCF(e.url)
is AuthRequiredException -> false //TODO is AuthRequiredException -> resolveAuthException(e.source)
else -> false else -> false
} }
private suspend fun resolveCF(url: String) = suspendCancellableCoroutine<Boolean> { cont -> private suspend fun resolveCF(url: String): Boolean {
val dialog = CloudFlareDialog.newInstance(url) val dialog = CloudFlareDialog.newInstance(url)
val fm = getFragmentManager()
return suspendCancellableCoroutine { cont ->
fm.clearFragmentResult(CloudFlareDialog.TAG) fm.clearFragmentResult(CloudFlareDialog.TAG)
continuations[CloudFlareDialog.TAG] = cont continuations[CloudFlareDialog.TAG] = cont
fm.setFragmentResultListener(CloudFlareDialog.TAG, lifecycleOwner) { key, result -> fm.setFragmentResultListener(CloudFlareDialog.TAG, checkNotNull(fragment ?: activity)) { key, result ->
continuations.remove(key)?.resume(result.getBoolean(CloudFlareDialog.EXTRA_RESULT)) continuations.remove(key)?.resume(result.getBoolean(CloudFlareDialog.EXTRA_RESULT))
} }
dialog.show(fm, CloudFlareDialog.TAG) dialog.show(fm, CloudFlareDialog.TAG)
@ -37,4 +60,12 @@ class ExceptionResolver(
dialog.dismiss() 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 val authUrl: String
fun isAuthorized(): Boolean fun isAuthorized(): Boolean
suspend fun getUsername(): String
} }

@ -61,7 +61,7 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe
tags = runCatching { tags = runCatching {
row.selectFirst("div.genre")?.select("a")?.mapToSet { row.selectFirst("div.genre")?.select("a")?.mapToSet {
MangaTag( MangaTag(
title = it.text().toTitleCase(), title = it.text().toTagName(),
key = it.attr("href").substringAfterLast('/').urlEncoded(), key = it.attr("href").substringAfterLast('/').urlEncoded(),
source = source source = source
) )
@ -136,7 +136,7 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe
return root.select("li.sidetag").mapToSet { li -> return root.select("li.sidetag").mapToSet { li ->
val a = li.children().last() ?: throw ParseException("a is null") val a = li.children().last() ?: throw ParseException("a is null")
MangaTag( MangaTag(
title = a.text().toTitleCase(), title = a.text().toTagName(),
key = a.attr("href").substringAfterLast('/'), key = a.attr("href").substringAfterLast('/'),
source = source source = source
) )
@ -159,4 +159,5 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe
else -> "favdesc" 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.jsoup.nodes.Element
import org.koitharu.kotatsu.base.domain.MangaLoaderContext 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.model.*
import org.koitharu.kotatsu.core.parser.MangaRepositoryAuthProvider import org.koitharu.kotatsu.core.parser.MangaRepositoryAuthProvider
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
import java.util.*
import kotlin.math.pow import kotlin.math.pow
private const val DOMAIN_UNAUTHORIZED = "e-hentai.org" private const val DOMAIN_UNAUTHORIZED = "e-hentai.org"
@ -17,7 +20,9 @@ class ExHentaiRepository(
override val source = MangaSource.EXHENTAI override val source = MangaSource.EXHENTAI
override val sortOrders: Set<SortOrder> = emptySet() override val sortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.NEWEST,
)
override val defaultDomain: String override val defaultDomain: String
get() = if (isAuthorized()) DOMAIN_AUTHORIZED else DOMAIN_UNAUTHORIZED get() = if (isAuthorized()) DOMAIN_AUTHORIZED else DOMAIN_UNAUTHORIZED
@ -206,6 +211,20 @@ class ExHentaiRepository(
return false 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 { private fun isAuthorized(domain: String): Boolean {
val cookies = loaderContext.cookieJar.getCookies(domain).mapToSet { x -> x.name } val cookies = loaderContext.cookieJar.getCookies(domain).mapToSet { x -> x.name }
return authCookies.all { it in cookies } 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.AuthRequiredException
import org.koitharu.kotatsu.core.exceptions.ParseException import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.* import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.MangaRepositoryAuthProvider
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
open class MangaLibRepository(loaderContext: MangaLoaderContext) : open class MangaLibRepository(loaderContext: MangaLoaderContext) :
RemoteMangaRepository(loaderContext) { RemoteMangaRepository(loaderContext), MangaRepositoryAuthProvider {
override val defaultDomain = "mangalib.me" override val defaultDomain = "mangalib.me"
override val source = MangaSource.MANGALIB override val source = MangaSource.MANGALIB
override val authUrl: String
get() = "https://${getDomain()}/login"
override val sortOrders: Set<SortOrder> = EnumSet.of( override val sortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.RATING, SortOrder.RATING,
SortOrder.ALPHABETICAL, SortOrder.ALPHABETICAL,
@ -153,7 +157,7 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
val fullUrl = chapter.url.withDomain() val fullUrl = chapter.url.withDomain()
val doc = loaderContext.httpGet(fullUrl).parseHtml() val doc = loaderContext.httpGet(fullUrl).parseHtml()
if (doc.location().endsWith("/register")) { if (doc.location().endsWith("/register")) {
throw AuthRequiredException("/login".inContextOf(doc)) throw AuthRequiredException(source)
} }
val scripts = doc.head().select("script") val scripts = doc.head().select("script")
val pg = (doc.body().getElementById("pg")?.html() ?: parseFailed("Element #pg not found")) 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") 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) { private fun getSortKey(sortOrder: SortOrder?) = when (sortOrder) {
SortOrder.RATING -> "desc&sort=rate" SortOrder.RATING -> "desc&sort=rate"
SortOrder.ALPHABETICAL -> "asc&sort=name" 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() val body = loaderContext.httpGet("https://${getDomain()}/").parseHtml()
.body() .body()
return body return body
@ -180,7 +180,7 @@ class NudeMoonRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposit
?.substringAfterLast('/') ?.substringAfterLast('/')
?: run { ?: run {
throw if (body.selectFirst("form[name=\"loginform\"]") != null) { throw if (body.selectFirst("form[name=\"loginform\"]") != null) {
AuthRequiredException(authUrl) AuthRequiredException(source)
} else { } else {
ParseException("Cannot find username") ParseException("Cannot find username")
} }

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.core.parser.site package org.koitharu.kotatsu.core.parser.site
import okhttp3.Headers
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject 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.MangaRepositoryAuthProvider
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
import java.net.URLDecoder
import java.text.DateFormat import java.text.DateFormat
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -62,7 +64,7 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
.append((offset / PAGE_SIZE) + 1) .append((offset / PAGE_SIZE) + 1)
.append("&count=") .append("&count=")
.append(PAGE_SIZE) .append(PAGE_SIZE)
val content = loaderContext.httpGet(urlBuilder.toString()).parseJson() val content = loaderContext.httpGet(urlBuilder.toString(), getApiHeaders()).parseJson()
.getJSONArray("content") .getJSONArray("content")
return content.map { jo -> return content.map { jo ->
val url = "/manga/${jo.getString("dir")}" val url = "/manga/${jo.getString("dir")}"
@ -95,7 +97,8 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
val slug = manga.url.find(regexLastUrlPath) val slug = manga.url.find(regexLastUrlPath)
?: throw ParseException("Cannot obtain slug from ${manga.url}") ?: throw ParseException("Cannot obtain slug from ${manga.url}")
val data = loaderContext.httpGet( val data = loaderContext.httpGet(
url = "https://api.$domain/api/titles/$slug/" url = "https://api.$domain/api/titles/$slug/",
headers = getApiHeaders(),
).parseJson() ).parseJson()
val content = try { val content = try {
data.getJSONObject("content") data.getJSONObject("content")
@ -150,7 +153,7 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> { override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val referer = "https://${getDomain()}/" 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") .getJSONObject("content")
val pages = content.optJSONArray("pages") val pages = content.optJSONArray("pages")
if (pages == null) { if (pages == null) {
@ -177,7 +180,7 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
override suspend fun getTags(): Set<MangaTag> { override suspend fun getTags(): Set<MangaTag> {
val domain = getDomain() 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") .parseJson().getJSONObject("content").getJSONArray("genres")
return content.mapToSet { jo -> return content.mapToSet { jo ->
MangaTag( 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() { private fun copyCookies() {
val domain = getDomain() val domain = getDomain()
loaderContext.cookieJar.copyCookies(domain, "api.$domain") loaderContext.cookieJar.copyCookies(domain, "api.$domain")
@ -220,7 +240,8 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
var page = 1 var page = 1
while (true) { while (true) {
val content = loaderContext.httpGet( 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") ).parseJson().getJSONArray("content")
val len = content.length() val len = content.length()
if (len == 0) { if (len == 0) {

@ -2,6 +2,7 @@ package org.koitharu.kotatsu.reader.ui
import android.Manifest import android.Manifest
import android.content.Context import android.content.Context
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
@ -35,6 +36,7 @@ import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaIntent import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.base.ui.BaseFullscreenActivity 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.Manga
import org.koitharu.kotatsu.core.model.MangaChapter import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.core.model.MangaPage import org.koitharu.kotatsu.core.model.MangaPage
@ -216,14 +218,14 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
} }
private fun onError(e: Throwable) { private fun onError(e: Throwable) {
val listener = ErrorDialogListener(e)
val dialog = MaterialAlertDialogBuilder(this) val dialog = MaterialAlertDialogBuilder(this)
.setTitle(R.string.error_occurred) .setTitle(R.string.error_occurred)
.setMessage(e.getDisplayMessage(resources)) .setMessage(e.getDisplayMessage(resources))
.setPositiveButton(R.string.close, null) .setNegativeButton(R.string.close, listener)
if (viewModel.content.value?.pages.isNullOrEmpty()) { .setOnCancelListener(listener)
dialog.setOnDismissListener { if (e is ResolvableException) {
finish() dialog.setPositiveButton(e.resolveTextId, listener)
}
} }
dialog.show() 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 { companion object {
const val ACTION_MANGA_READ = "${BuildConfig.APPLICATION_ID}.action.READ_MANGA" 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 import org.koitharu.kotatsu.utils.ext.processLifecycleScope
class ReaderViewModel( class ReaderViewModel(
intent: MangaIntent, private val intent: MangaIntent,
state: ReaderState?, initialState: ReaderState?,
private val dataRepository: MangaDataRepository, private val dataRepository: MangaDataRepository,
private val historyRepository: HistoryRepository, private val historyRepository: HistoryRepository,
private val shortcutsRepository: ShortcutsRepository, private val shortcutsRepository: ShortcutsRepository,
@ -42,7 +42,7 @@ class ReaderViewModel(
) : BaseViewModel() { ) : BaseViewModel() {
private var loadingJob: Job? = null private var loadingJob: Job? = null
private val currentState = MutableStateFlow<ReaderState?>(null) private val currentState = MutableStateFlow(initialState)
private val mangaData = MutableStateFlow(intent.manga) private val mangaData = MutableStateFlow(intent.manga)
private val chapters = LongSparseArray<MangaChapter>() private val chapters = LongSparseArray<MangaChapter>()
@ -87,45 +87,7 @@ class ReaderViewModel(
val onZoomChanged = SingleLiveEvent<Unit>() val onZoomChanged = SingleLiveEvent<Unit>()
init { init {
loadingJob = launchLoadingJob(Dispatchers.Default) { loadImpl()
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))
}
subscribeToSettings() subscribeToSettings()
} }
@ -134,6 +96,11 @@ class ReaderViewModel(
super.onCleared() super.onCleared()
} }
fun reload() {
loadingJob?.cancel()
loadImpl()
}
fun switchMode(newMode: ReaderMode) { fun switchMode(newMode: ReaderMode) {
launchJob { launchJob {
val manga = checkNotNull(mangaData.value) 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 { private fun getReaderMode(isWebtoon: Boolean?) = when {
isWebtoon == true -> ReaderMode.WEBTOON isWebtoon == true -> ReaderMode.WEBTOON
settings.isPreferRtlReader -> ReaderMode.REVERSED settings.isPreferRtlReader -> ReaderMode.REVERSED

@ -2,12 +2,18 @@ package org.koitharu.kotatsu.settings
import android.os.Bundle import android.os.Bundle
import android.util.ArrayMap import android.util.ArrayMap
import android.view.View
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import androidx.preference.EditTextPreference import androidx.preference.EditTextPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.TwoStatePreference 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.R
import org.koitharu.kotatsu.core.exceptions.AuthRequiredException
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.parser.MangaRepositoryAuthProvider import org.koitharu.kotatsu.core.parser.MangaRepositoryAuthProvider
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository 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.settings.utils.EditTextDefaultSummaryProvider
import org.koitharu.kotatsu.utils.ext.mangaRepositoryOf import org.koitharu.kotatsu.utils.ext.mangaRepositoryOf
import org.koitharu.kotatsu.utils.ext.parcelableArgument import org.koitharu.kotatsu.utils.ext.parcelableArgument
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import org.koitharu.kotatsu.utils.ext.withArgs import org.koitharu.kotatsu.utils.ext.withArgs
class SourceSettingsFragment : PreferenceFragmentCompat() { 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 { override fun onPreferenceTreeClick(preference: Preference): Boolean {
return when (preference.key) { return when (preference.key) {
SourceSettings.KEY_AUTH -> { 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 { companion object {
private const val EXTRA_SOURCE = "source" private const val EXTRA_SOURCE = "source"

@ -1,12 +1,14 @@
package org.koitharu.kotatsu.settings.sources.auth package org.koitharu.kotatsu.settings.sources.auth
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.MenuItem import android.view.MenuItem
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContract
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updatePadding 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.model.MangaSource
import org.koitharu.kotatsu.core.parser.MangaRepositoryAuthProvider import org.koitharu.kotatsu.core.parser.MangaRepositoryAuthProvider
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
import org.koitharu.kotatsu.utils.TaggedActivityResult
import org.koitharu.kotatsu.utils.ext.mangaRepositoryOf import org.koitharu.kotatsu.utils.ext.mangaRepositoryOf
import com.google.android.material.R as materialR 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) { override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
android.R.id.home -> { android.R.id.home -> {
binding.webView.stopLoading() binding.webView.stopLoading()
setResult(Activity.RESULT_CANCELED)
finishAfterTransition() finishAfterTransition()
true true
} }
@ -89,6 +93,7 @@ class SourceAuthActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallba
binding.progressBar.isVisible = isLoading binding.progressBar.isVisible = isLoading
if (!isLoading && repository.isAuthorized()) { if (!isLoading && repository.isAuthorized()) {
Toast.makeText(this, R.string.auth_complete, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.auth_complete, Toast.LENGTH_SHORT).show()
setResult(Activity.RESULT_OK)
finishAfterTransition() finishAfterTransition()
} }
} }
@ -103,9 +108,20 @@ class SourceAuthActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallba
binding.webView.updatePadding(bottom = insets.bottom) 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 { companion object {
private const val EXTRA_SOURCE = "source" private const val EXTRA_SOURCE = "source"
const val TAG = "SourceAuthActivity"
fun newIntent(context: Context, source: MangaSource): Intent { fun newIntent(context: Context, source: MangaSource): Intent {
return Intent(context, SourceAuthActivity::class.java) 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="only_using_wifi">Only using WiFi</string>
<string name="always">Always</string> <string name="always">Always</string>
<string name="preload_pages">Preload pages</string> <string name="preload_pages">Preload pages</string>
<string name="logged_in_as">Logged in as %s</string>
</resources> </resources>
Loading…
Cancel
Save