diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt index a617cad7e..c14d81151 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt @@ -28,9 +28,8 @@ abstract class BaseActivity : 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 diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFragment.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFragment.kt index 76f774bc8..25ca6a503 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFragment.kt @@ -20,9 +20,8 @@ abstract class BaseFragment : 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 diff --git a/app/src/main/java/org/koitharu/kotatsu/core/exceptions/AuthRequiredException.kt b/app/src/main/java/org/koitharu/kotatsu/core/exceptions/AuthRequiredException.kt index 9067878fb..90a40c050 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/exceptions/AuthRequiredException.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/exceptions/AuthRequiredException.kt @@ -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 diff --git a/app/src/main/java/org/koitharu/kotatsu/core/exceptions/resolve/ExceptionResolver.kt b/app/src/main/java/org/koitharu/kotatsu/core/exceptions/resolve/ExceptionResolver.kt index e4d4fdfb0..efad58757 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/exceptions/resolve/ExceptionResolver.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/exceptions/resolve/ExceptionResolver.kt @@ -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 { private val continuations = ArrayMap>(1) + private lateinit var sourceAuthContract: ActivityResultLauncher + + 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 { 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) } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepositoryAuthProvider.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepositoryAuthProvider.kt index 16cc5e39f..86fbf4926 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepositoryAuthProvider.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepositoryAuthProvider.kt @@ -5,4 +5,6 @@ interface MangaRepositoryAuthProvider { val authUrl: String fun isAuthorized(): Boolean + + suspend fun getUsername(): String } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ChanRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ChanRepository.kt index ceb4ee024..6e7005757 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ChanRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ChanRepository.kt @@ -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() } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt index 8fb6ee6a7..d59e171ba 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt @@ -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 = emptySet() + override val sortOrders: Set = 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 } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaLibRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaLibRepository.kt index 8ebbf2c44..e279356ce 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaLibRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaLibRepository.kt @@ -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 = 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" diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NudeMoonRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NudeMoonRepository.kt index e1da76ba5..91dc0bf59 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NudeMoonRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NudeMoonRepository.kt @@ -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") } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt index 2ac28c8ac..e98442148 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt @@ -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 { 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 { 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) { diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt index 0d2bb2651..cd15c2bfa 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt @@ -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(), } 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(), } } + 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" diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt index 974655089..bae04af94 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt @@ -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(null) + private val currentState = MutableStateFlow(initialState) private val mangaData = MutableStateFlow(intent.manga) private val chapters = LongSparseArray() @@ -87,45 +87,7 @@ class ReaderViewModel( val onZoomChanged = SingleLiveEvent() 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 diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt index 750d66013..d15a1e2a4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt @@ -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(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" diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt index b5f24a9c0..5065b7466 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt @@ -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(), 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(), 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(), BrowserCallba binding.webView.updatePadding(bottom = insets.bottom) } + class Contract : ActivityResultContract() { + 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) diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/TaggedActivityResult.kt b/app/src/main/java/org/koitharu/kotatsu/utils/TaggedActivityResult.kt new file mode 100644 index 000000000..ee84cffb2 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/TaggedActivityResult.kt @@ -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 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1e3a5c93f..3cfc6ebc6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -265,4 +265,5 @@ Only using WiFi Always Preload pages + Logged in as %s \ No newline at end of file