Handle scrobbler authorization errors

master
Koitharu 2 years ago
parent 861ca63ea9
commit 22643bf9cc
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.browser.cloudflare package org.koitharu.kotatsu.browser.cloudflare
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
@ -28,7 +29,6 @@ import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.TaggedActivityResult
import org.koitharu.kotatsu.core.util.ext.configureForParser import org.koitharu.kotatsu.core.util.ext.configureForParser
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
@ -180,13 +180,13 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
} }
} }
class Contract : ActivityResultContract<CloudFlareProtectedException, TaggedActivityResult>() { class Contract : ActivityResultContract<CloudFlareProtectedException, Boolean>() {
override fun createIntent(context: Context, input: CloudFlareProtectedException): Intent { override fun createIntent(context: Context, input: CloudFlareProtectedException): Intent {
return newIntent(context, input) return newIntent(context, input)
} }
override fun parseResult(resultCode: Int, intent: Intent?): TaggedActivityResult { override fun parseResult(resultCode: Int, intent: Intent?): Boolean {
return TaggedActivityResult(TAG, resultCode) return resultCode == Activity.RESULT_OK
} }
} }

@ -2,14 +2,14 @@ package org.koitharu.kotatsu.core.exceptions.resolve
import android.content.Context import android.content.Context
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultCallback import androidx.activity.result.ActivityResultCaller
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.collection.MutableScatterMap import androidx.collection.MutableScatterMap
import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.EntryPointAccessors import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.alternatives.ui.AlternativesActivity import org.koitharu.kotatsu.alternatives.ui.AlternativesActivity
import org.koitharu.kotatsu.browser.BrowserActivity import org.koitharu.kotatsu.browser.BrowserActivity
@ -18,53 +18,39 @@ import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.exceptions.ProxyConfigException import org.koitharu.kotatsu.core.exceptions.ProxyConfigException
import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseActivity.BaseActivityEntryPoint
import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
import org.koitharu.kotatsu.core.util.TaggedActivityResult
import org.koitharu.kotatsu.core.util.ext.findActivity import org.koitharu.kotatsu.core.util.ext.findActivity
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.exception.NotFoundException import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.scrobbling.common.domain.ScrobblerAuthRequiredException
import org.koitharu.kotatsu.scrobbling.common.ui.ScrobblerAuthHelper
import org.koitharu.kotatsu.settings.SettingsActivity import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
import java.security.cert.CertPathValidatorException import java.security.cert.CertPathValidatorException
import javax.inject.Provider
import javax.net.ssl.SSLException import javax.net.ssl.SSLException
import kotlin.coroutines.Continuation import kotlin.coroutines.Continuation
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> { class ExceptionResolver @AssistedInject constructor(
@Assisted private val host: Host,
private val settings: AppSettings,
private val scrobblerAuthHelperProvider: Provider<ScrobblerAuthHelper>,
) {
private val continuations = MutableScatterMap<String, Continuation<Boolean>>(1) private val continuations = MutableScatterMap<String, Continuation<Boolean>>(1)
private val activity: FragmentActivity?
private val fragment: Fragment?
private val sourceAuthContract: ActivityResultLauncher<MangaSource>
private val cloudflareContract: ActivityResultLauncher<CloudFlareProtectedException>
val context: Context?
get() = activity ?: fragment?.context
constructor(activity: FragmentActivity) { private val sourceAuthContract = host.registerForActivityResult(SourceAuthActivity.Contract()) {
this.activity = activity handleActivityResult(SourceAuthActivity.TAG, it)
fragment = null
sourceAuthContract = activity.registerForActivityResult(SourceAuthActivity.Contract(), this)
cloudflareContract = activity.registerForActivityResult(CloudFlareActivity.Contract(), this)
} }
private val cloudflareContract = host.registerForActivityResult(CloudFlareActivity.Contract()) {
constructor(fragment: Fragment) { handleActivityResult(CloudFlareActivity.TAG, it)
this.fragment = fragment
activity = null
sourceAuthContract = fragment.registerForActivityResult(SourceAuthActivity.Contract(), this)
cloudflareContract = fragment.registerForActivityResult(CloudFlareActivity.Contract(), this)
}
override fun onActivityResult(result: TaggedActivityResult) {
continuations.remove(result.tag)?.resume(result.isSuccess)
} }
fun showDetails(e: Throwable, url: String?) { fun showDetails(e: Throwable, url: String?) {
ErrorDetailsDialog.show(getFragmentManager(), e, url) ErrorDetailsDialog.show(host.getChildFragmentManager(), e, url)
} }
suspend fun resolve(e: Throwable): Boolean = when (e) { suspend fun resolve(e: Throwable): Boolean = when (e) {
@ -77,7 +63,7 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
} }
is ProxyConfigException -> { is ProxyConfigException -> {
context?.run { host.withContext {
startActivity(SettingsActivity.newProxySettingsIntent(this)) startActivity(SettingsActivity.newProxySettingsIntent(this))
} }
false false
@ -93,6 +79,20 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
false false
} }
is ScrobblerAuthRequiredException -> {
val authHelper = scrobblerAuthHelperProvider.get()
if (authHelper.isAuthorized(e.scrobbler)) {
true
} else {
host.withContext {
authHelper.startAuth(this, e.scrobbler).onFailure {
showDetails(it, null)
}
}
false
}
}
else -> false else -> false
} }
@ -106,21 +106,20 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
sourceAuthContract.launch(source) sourceAuthContract.launch(source)
} }
private fun openInBrowser(url: String) { private fun openInBrowser(url: String) = host.withContext {
context?.run {
startActivity(BrowserActivity.newIntent(this, url, null, null)) startActivity(BrowserActivity.newIntent(this, url, null, null))
} }
}
private fun openAlternatives(manga: Manga) { private fun openAlternatives(manga: Manga) = host.withContext {
context?.run {
startActivity(AlternativesActivity.newIntent(this, manga)) startActivity(AlternativesActivity.newIntent(this, manga))
} }
private fun handleActivityResult(tag: String, result: Boolean) {
continuations.remove(tag)?.resume(result)
} }
private fun showSslErrorDialog() { private fun showSslErrorDialog() {
val ctx = context ?: return val ctx = host.getContext() ?: return
val settings = getAppSettings(ctx)
if (settings.isSSLBypassEnabled) { if (settings.isSSLBypassEnabled) {
Toast.makeText(ctx, R.string.operation_not_supported, Toast.LENGTH_SHORT).show() Toast.makeText(ctx, R.string.operation_not_supported, Toast.LENGTH_SHORT).show()
return return
@ -136,18 +135,31 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
.show() .show()
} }
private fun getAppSettings(context: Context): AppSettings { private inline fun Host.withContext(block: Context.() -> Unit) {
return EntryPointAccessors.fromApplication<BaseActivityEntryPoint>(context).settings getContext()?.apply(block)
}
interface Host : ActivityResultCaller {
fun getChildFragmentManager(): FragmentManager
fun getContext(): Context?
} }
private fun getFragmentManager() = checkNotNull(fragment?.childFragmentManager ?: activity?.supportFragmentManager) @AssistedFactory
interface Factory {
fun create(host: Host): ExceptionResolver
}
companion object { companion object {
@StringRes @StringRes
fun getResolveStringId(e: Throwable) = when (e) { fun getResolveStringId(e: Throwable) = when (e) {
is CloudFlareProtectedException -> R.string.captcha_solve is CloudFlareProtectedException -> R.string.captcha_solve
is ScrobblerAuthRequiredException,
is AuthRequiredException -> R.string.sign_in is AuthRequiredException -> R.string.sign_in
is NotFoundException -> if (e.url.isNotEmpty()) R.string.open_in_browser else 0 is NotFoundException -> if (e.url.isNotEmpty()) R.string.open_in_browser else 0
is UnsupportedSourceException -> if (e.manga != null) R.string.alternatives else 0 is UnsupportedSourceException -> if (e.manga != null) R.string.alternatives else 0
is SSLException, is SSLException,

@ -14,25 +14,22 @@ import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.fragment.app.FragmentManager
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
import org.koitharu.kotatsu.core.util.ext.isWebViewUnavailable import org.koitharu.kotatsu.core.util.ext.isWebViewUnavailable
import org.koitharu.kotatsu.main.ui.protect.ScreenshotPolicyHelper import org.koitharu.kotatsu.main.ui.protect.ScreenshotPolicyHelper
@Suppress("LeakingThis")
abstract class BaseActivity<B : ViewBinding> : abstract class BaseActivity<B : ViewBinding> :
AppCompatActivity(), AppCompatActivity(),
ExceptionResolver.Host,
ScreenshotPolicyHelper.ContentContainer, ScreenshotPolicyHelper.ContentContainer,
WindowInsetsDelegate.WindowInsetsListener { WindowInsetsDelegate.WindowInsetsListener {
@ -41,8 +38,8 @@ abstract class BaseActivity<B : ViewBinding> :
lateinit var viewBinding: B lateinit var viewBinding: B
private set private set
@JvmField protected lateinit var exceptionResolver: ExceptionResolver
protected val exceptionResolver = ExceptionResolver(this) private set
@JvmField @JvmField
protected val insetsDelegate = WindowInsetsDelegate() protected val insetsDelegate = WindowInsetsDelegate()
@ -53,13 +50,15 @@ abstract class BaseActivity<B : ViewBinding> :
private var defaultStatusBarColor = Color.TRANSPARENT private var defaultStatusBarColor = Color.TRANSPARENT
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
val settings = EntryPointAccessors.fromApplication(this, BaseActivityEntryPoint::class.java).settings val entryPoint = EntryPointAccessors.fromApplication<BaseActivityEntryPoint>(this)
val settings = entryPoint.settings
isAmoledTheme = settings.isAmoledTheme isAmoledTheme = settings.isAmoledTheme
setTheme(settings.colorScheme.styleResId) setTheme(settings.colorScheme.styleResId)
if (isAmoledTheme) { if (isAmoledTheme) {
setTheme(R.style.ThemeOverlay_Kotatsu_Amoled) setTheme(R.style.ThemeOverlay_Kotatsu_Amoled)
} }
putDataToExtras(intent) putDataToExtras(intent)
exceptionResolver = entryPoint.exceptionResolverFactory.create(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
insetsDelegate.handleImeInsets = true insetsDelegate.handleImeInsets = true
@ -88,6 +87,10 @@ abstract class BaseActivity<B : ViewBinding> :
setupToolbar() setupToolbar()
} }
override fun getContext() = this
override fun getChildFragmentManager(): FragmentManager = supportFragmentManager
protected fun setContentView(binding: B) { protected fun setContentView(binding: B) {
this.viewBinding = binding this.viewBinding = binding
super.setContentView(binding.root) super.setContentView(binding.root)
@ -178,12 +181,6 @@ abstract class BaseActivity<B : ViewBinding> :
protected fun hasViewBinding() = ::viewBinding.isInitialized protected fun hasViewBinding() = ::viewBinding.isInitialized
@EntryPoint
@InstallIn(SingletonComponent::class)
interface BaseActivityEntryPoint {
val settings: AppSettings
}
companion object { companion object {
const val EXTRA_DATA = "data" const val EXTRA_DATA = "data"

@ -0,0 +1,16 @@
package org.koitharu.kotatsu.core.ui
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings
@EntryPoint
@InstallIn(SingletonComponent::class)
interface BaseActivityEntryPoint {
val settings: AppSettings
val exceptionResolverFactory: ExceptionResolver.Factory
}

@ -1,25 +1,27 @@
package org.koitharu.kotatsu.core.ui package org.koitharu.kotatsu.core.ui
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import dagger.hilt.android.EntryPointAccessors
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
@Suppress("LeakingThis")
abstract class BaseFragment<B : ViewBinding> : abstract class BaseFragment<B : ViewBinding> :
Fragment(), Fragment(),
ExceptionResolver.Host,
WindowInsetsDelegate.WindowInsetsListener { WindowInsetsDelegate.WindowInsetsListener {
var viewBinding: B? = null var viewBinding: B? = null
private set private set
@JvmField protected lateinit var exceptionResolver: ExceptionResolver
protected val exceptionResolver = ExceptionResolver(this) private set
@JvmField @JvmField
protected val insetsDelegate = WindowInsetsDelegate() protected val insetsDelegate = WindowInsetsDelegate()
@ -27,6 +29,12 @@ abstract class BaseFragment<B : ViewBinding> :
protected val actionModeDelegate: ActionModeDelegate protected val actionModeDelegate: ActionModeDelegate
get() = (requireActivity() as BaseActivity<*>).actionModeDelegate get() = (requireActivity() as BaseActivity<*>).actionModeDelegate
override fun onAttach(context: Context) {
super.onAttach(context)
val entryPoint = EntryPointAccessors.fromApplication<BaseActivityEntryPoint>(context)
exceptionResolver = entryPoint.exceptionResolverFactory.create(this)
}
final override fun onCreateView( final override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,

@ -1,6 +1,7 @@
package org.koitharu.kotatsu.core.ui package org.koitharu.kotatsu.core.ui
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
@ -12,7 +13,9 @@ import androidx.preference.PreferenceFragmentCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.EntryPointAccessors
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
@ -25,7 +28,11 @@ import javax.inject.Inject
abstract class BasePreferenceFragment(@StringRes private val titleId: Int) : abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
PreferenceFragmentCompat(), PreferenceFragmentCompat(),
WindowInsetsDelegate.WindowInsetsListener, WindowInsetsDelegate.WindowInsetsListener,
RecyclerViewOwner { RecyclerViewOwner,
ExceptionResolver.Host {
protected lateinit var exceptionResolver: ExceptionResolver
private set
@Inject @Inject
lateinit var settings: AppSettings lateinit var settings: AppSettings
@ -36,6 +43,12 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
override val recyclerView: RecyclerView override val recyclerView: RecyclerView
get() = listView get() = listView
override fun onAttach(context: Context) {
super.onAttach(context)
val entryPoint = EntryPointAccessors.fromApplication<BaseActivityEntryPoint>(context)
exceptionResolver = entryPoint.exceptionResolverFactory.create(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val themedContext = (view.parentView ?: view).context val themedContext = (view.parentView ?: view).context

@ -21,16 +21,22 @@ import androidx.viewbinding.ViewBinding
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.sidesheet.SideSheetDialog import com.google.android.material.sidesheet.SideSheetDialog
import dagger.hilt.android.EntryPointAccessors
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.BaseActivityEntryPoint
import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() { abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment(), ExceptionResolver.Host {
private var waitingForDismissAllowingStateLoss = false private var waitingForDismissAllowingStateLoss = false
private var isFitToContentsDisabled = false private var isFitToContentsDisabled = false
protected lateinit var exceptionResolver: ExceptionResolver
private set
var viewBinding: B? = null var viewBinding: B? = null
private set private set
@ -50,6 +56,12 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
private set private set
private var lockCounter = 0 private var lockCounter = 0
override fun onAttach(context: Context) {
super.onAttach(context)
val entryPoint = EntryPointAccessors.fromApplication<BaseActivityEntryPoint>(context)
exceptionResolver = entryPoint.exceptionResolverFactory.create(this)
}
final override fun onCreateView( final override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,

@ -1,12 +0,0 @@
package org.koitharu.kotatsu.core.util
import android.app.Activity
class TaggedActivityResult(
val tag: String,
val result: Int,
) {
val isSuccess: Boolean
get() = result == Activity.RESULT_OK
}

@ -33,6 +33,7 @@ import org.koitharu.kotatsu.parsers.exception.ContentUnavailableException
import org.koitharu.kotatsu.parsers.exception.NotFoundException import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.exception.ParseException import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.exception.TooManyRequestExceptions import org.koitharu.kotatsu.parsers.exception.TooManyRequestExceptions
import org.koitharu.kotatsu.scrobbling.common.domain.ScrobblerAuthRequiredException
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
import java.net.UnknownHostException import java.net.UnknownHostException
@ -40,6 +41,11 @@ private const val MSG_NO_SPACE_LEFT = "No space left on device"
private const val IMAGE_FORMAT_NOT_SUPPORTED = "Image format not supported" private const val IMAGE_FORMAT_NOT_SUPPORTED = "Image format not supported"
fun Throwable.getDisplayMessage(resources: Resources): String = when (this) { fun Throwable.getDisplayMessage(resources: Resources): String = when (this) {
is ScrobblerAuthRequiredException -> resources.getString(
R.string.scrobbler_auth_required,
resources.getString(scrobbler.titleResId),
)
is AuthRequiredException -> resources.getString(R.string.auth_required) is AuthRequiredException -> resources.getString(R.string.auth_required)
is CloudFlareProtectedException -> resources.getString(R.string.captcha_required) is CloudFlareProtectedException -> resources.getString(R.string.captcha_required)
is CloudFlareBlockedException -> resources.getString(R.string.blocked_by_server_message) is CloudFlareBlockedException -> resources.getString(R.string.blocked_by_server_message)

@ -17,7 +17,6 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.GenericSortOrder import org.koitharu.kotatsu.core.model.GenericSortOrder
import org.koitharu.kotatsu.core.model.SortDirection import org.koitharu.kotatsu.core.model.SortDirection
import org.koitharu.kotatsu.core.model.titleResId import org.koitharu.kotatsu.core.model.titleResId
import org.koitharu.kotatsu.core.ui.model.direction
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
@ -25,7 +24,6 @@ import org.koitharu.kotatsu.core.util.ext.getDisplayName
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.parentView import org.koitharu.kotatsu.core.util.ext.parentView
import org.koitharu.kotatsu.core.util.ext.showDistinct import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.core.util.ext.sortedByOrdinal
import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.SheetFilterBinding import org.koitharu.kotatsu.databinding.SheetFilterBinding
import org.koitharu.kotatsu.filter.ui.FilterOwner import org.koitharu.kotatsu.filter.ui.FilterOwner
@ -34,9 +32,6 @@ import org.koitharu.kotatsu.filter.ui.tags.TagsCatalogSheet
import org.koitharu.kotatsu.parsers.model.ContentRating import org.koitharu.kotatsu.parsers.model.ContentRating
import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.mapToSet
import java.util.EnumSet
import java.util.Locale import java.util.Locale
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
@ -106,7 +101,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
} }
is ContentRating -> filter.setContentRating(data, !chip.isChecked) is ContentRating -> filter.setContentRating(data, !chip.isChecked)
null -> TagsCatalogSheet.show(childFragmentManager, chip.parentView?.id == R.id.chips_genresExclude) null -> TagsCatalogSheet.show(getChildFragmentManager(), chip.parentView?.id == R.id.chips_genresExclude)
} }
} }

@ -299,7 +299,7 @@ abstract class MangaListFragment :
} }
R.id.action_favourite -> { R.id.action_favourite -> {
FavoriteSheet.show(childFragmentManager, selectedItems) FavoriteSheet.show(getChildFragmentManager(), selectedItems)
mode.finish() mode.finish()
true true
} }

@ -66,11 +66,11 @@ class LocalListFragment : MangaListFragment(), FilterOwner {
} }
override fun onEmptyActionClick() { override fun onEmptyActionClick() {
ImportDialogFragment.show(childFragmentManager) ImportDialogFragment.show(getChildFragmentManager())
} }
override fun onFilterClick(view: View?) { override fun onFilterClick(view: View?) {
FilterSheetFragment.show(childFragmentManager) FilterSheetFragment.show(getChildFragmentManager())
} }
override fun onPrimaryButtonClick(tipView: TipView) { override fun onPrimaryButtonClick(tipView: TipView) {

@ -66,7 +66,7 @@ class RemoteListFragment : MangaListFragment(), FilterOwner {
} }
override fun onFilterClick(view: View?) { override fun onFilterClick(view: View?) {
FilterSheetFragment.show(childFragmentManager) FilterSheetFragment.show(getChildFragmentManager())
} }
override fun onEmptyActionClick() { override fun onEmptyActionClick() {

@ -4,6 +4,9 @@ import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
import org.koitharu.kotatsu.scrobbling.common.domain.ScrobblerAuthRequiredException
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import java.net.HttpURLConnection
private const val JSON = "application/json" private const val JSON = "application/json"
@ -14,11 +17,16 @@ class AniListInterceptor(private val storage: ScrobblerStorage) : Interceptor {
val request = sourceRequest.newBuilder() val request = sourceRequest.newBuilder()
request.header(CommonHeaders.CONTENT_TYPE, JSON) request.header(CommonHeaders.CONTENT_TYPE, JSON)
request.header(CommonHeaders.ACCEPT, JSON) request.header(CommonHeaders.ACCEPT, JSON)
if (!sourceRequest.url.pathSegments.contains("oauth")) { val isAuthRequest = sourceRequest.url.pathSegments.contains("oauth")
if (!isAuthRequest) {
storage.accessToken?.let { storage.accessToken?.let {
request.header(CommonHeaders.AUTHORIZATION, "Bearer $it") request.header(CommonHeaders.AUTHORIZATION, "Bearer $it")
} }
} }
return chain.proceed(request.build()) val response = chain.proceed(request.build())
if (!isAuthRequest && response.code == HttpURLConnection.HTTP_UNAUTHORIZED) {
throw ScrobblerAuthRequiredException(ScrobblerService.ANILIST)
}
return response
} }
} }

@ -0,0 +1,8 @@
package org.koitharu.kotatsu.scrobbling.common.domain
import okio.IOException
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
class ScrobblerAuthRequiredException(
val scrobbler: ScrobblerService,
) : IOException()

@ -0,0 +1,25 @@
package org.koitharu.kotatsu.scrobbling.common.domain
import org.koitharu.kotatsu.scrobbling.anilist.data.AniListRepository
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerRepository
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.kitsu.data.KitsuRepository
import org.koitharu.kotatsu.scrobbling.mal.data.MALRepository
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository
import javax.inject.Inject
import javax.inject.Provider
class ScrobblerRepositoryMap @Inject constructor(
private val shikimoriRepository: Provider<ShikimoriRepository>,
private val aniListRepository: Provider<AniListRepository>,
private val malRepository: Provider<MALRepository>,
private val kitsuRepository: Provider<KitsuRepository>,
) {
operator fun get(scrobblerService: ScrobblerService): ScrobblerRepository = when (scrobblerService) {
ScrobblerService.SHIKIMORI -> shikimoriRepository
ScrobblerService.ANILIST -> aniListRepository
ScrobblerService.MAL -> malRepository
ScrobblerService.KITSU -> kitsuRepository
}.get()
}

@ -0,0 +1,42 @@
package org.koitharu.kotatsu.scrobbling.common.ui
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.net.Uri
import org.koitharu.kotatsu.scrobbling.common.domain.ScrobblerRepositoryMap
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser
import org.koitharu.kotatsu.scrobbling.kitsu.ui.KitsuAuthActivity
import javax.inject.Inject
class ScrobblerAuthHelper @Inject constructor(
private val repositoriesMap: ScrobblerRepositoryMap,
) {
fun isAuthorized(scrobbler: ScrobblerService) = repositoriesMap[scrobbler].isAuthorized
fun getCachedUser(scrobbler: ScrobblerService): ScrobblerUser? {
return repositoriesMap[scrobbler].cachedUser
}
suspend fun getUser(scrobbler: ScrobblerService): ScrobblerUser {
return repositoriesMap[scrobbler].loadUser()
}
@SuppressLint("UnsafeImplicitIntentLaunch")
fun startAuth(context: Context, scrobbler: ScrobblerService) = runCatching {
if (scrobbler == ScrobblerService.KITSU) {
launchKitsuAuth(context)
} else {
val repository = repositoriesMap[scrobbler]
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(repository.oauthUrl)
context.startActivity(intent)
}
}
private fun launchKitsuAuth(context: Context) {
context.startActivity(Intent(context, KitsuAuthActivity::class.java))
}
}

@ -14,7 +14,9 @@ import androidx.recyclerview.widget.RecyclerView.NO_ID
import coil.ImageLoader import coil.ImageLoader
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.parser.MangaIntent import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
@ -28,6 +30,7 @@ import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.setProgressIcon import org.koitharu.kotatsu.core.util.ext.setProgressIcon
import org.koitharu.kotatsu.core.util.ext.setTabsEnabled import org.koitharu.kotatsu.core.util.ext.setTabsEnabled
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.SheetScrobblingSelectorBinding import org.koitharu.kotatsu.databinding.SheetScrobblingSelectorBinding
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
@ -50,7 +53,8 @@ class ScrobblingSelectorSheet :
MenuItem.OnActionExpandListener, MenuItem.OnActionExpandListener,
SearchView.OnQueryTextListener, SearchView.OnQueryTextListener,
TabLayout.OnTabSelectedListener, TabLayout.OnTabSelectedListener,
ListStateHolderListener, AsyncListDiffer.ListListener<ListModel> { ListStateHolderListener,
AsyncListDiffer.ListListener<ListModel> {
@Inject @Inject
lateinit var coil: ImageLoader lateinit var coil: ImageLoader
@ -134,8 +138,16 @@ class ScrobblingSelectorSheet :
} }
override fun onRetryClick(error: Throwable) { override fun onRetryClick(error: Throwable) {
if (ExceptionResolver.canResolve(error)) {
viewLifecycleScope.launch {
if (exceptionResolver.resolve(error)) {
viewModel.retry() viewModel.retry()
} }
}
} else {
viewModel.retry()
}
}
override fun onEmptyActionClick() { override fun onEmptyActionClick() {
openSearch() openSearch()

@ -14,11 +14,13 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.parser.MangaIntent import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.ifZero
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.require import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.core.util.ext.requireValue import org.koitharu.kotatsu.core.util.ext.requireValue
@ -79,8 +81,8 @@ class ScrobblingSelectorViewModel @Inject constructor(
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState)) }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
val selectedItemId = MutableStateFlow(NO_ID) val selectedItemId = MutableStateFlow(NO_ID)
val searchQuery = MutableStateFlow(manga.title)
val onClose = MutableEventFlow<Unit>() val onClose = MutableEventFlow<Unit>()
private val searchQuery = MutableStateFlow(manga.title)
val isEmpty: Boolean val isEmpty: Boolean
get() = scrobblerMangaList.value.isEmpty() get() = scrobblerMangaList.value.isEmpty()
@ -201,11 +203,14 @@ class ScrobblingSelectorViewModel @Inject constructor(
actionStringRes = R.string.search, actionStringRes = R.string.search,
) )
private fun errorHint(e: Throwable) = ScrobblerHint( private fun errorHint(e: Throwable): ScrobblerHint {
val resolveAction = ExceptionResolver.getResolveStringId(e)
return ScrobblerHint(
icon = R.drawable.ic_error_large, icon = R.drawable.ic_error_large,
textPrimary = R.string.error_occurred, textPrimary = R.string.error_occurred,
error = e, error = e,
textSecondary = 0, textSecondary = if (resolveAction == 0) 0 else R.string.try_again,
actionStringRes = R.string.try_again, actionStringRes = resolveAction.ifZero { R.string.try_again },
) )
} }
}

@ -4,6 +4,9 @@ import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
import org.koitharu.kotatsu.scrobbling.common.domain.ScrobblerAuthRequiredException
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import java.net.HttpURLConnection
class KitsuInterceptor(private val storage: ScrobblerStorage) : Interceptor { class KitsuInterceptor(private val storage: ScrobblerStorage) : Interceptor {
@ -12,12 +15,17 @@ class KitsuInterceptor(private val storage: ScrobblerStorage) : Interceptor {
val request = sourceRequest.newBuilder() val request = sourceRequest.newBuilder()
request.header(CommonHeaders.CONTENT_TYPE, VND_JSON) request.header(CommonHeaders.CONTENT_TYPE, VND_JSON)
request.header(CommonHeaders.ACCEPT, VND_JSON) request.header(CommonHeaders.ACCEPT, VND_JSON)
if (!sourceRequest.url.pathSegments.contains("oauth")) { val isAuthRequest = sourceRequest.url.pathSegments.contains("oauth")
if (!isAuthRequest) {
storage.accessToken?.let { storage.accessToken?.let {
request.header(CommonHeaders.AUTHORIZATION, "Bearer $it") request.header(CommonHeaders.AUTHORIZATION, "Bearer $it")
} }
} }
return chain.proceed(request.build()) val response = chain.proceed(request.build())
if (!isAuthRequest && response.code == HttpURLConnection.HTTP_UNAUTHORIZED) {
throw ScrobblerAuthRequiredException(ScrobblerService.KITSU)
}
return response
} }
companion object { companion object {

@ -7,6 +7,9 @@ import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.parsers.util.mimeType import org.koitharu.kotatsu.parsers.util.mimeType
import org.koitharu.kotatsu.parsers.util.parseHtml import org.koitharu.kotatsu.parsers.util.parseHtml
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
import org.koitharu.kotatsu.scrobbling.common.domain.ScrobblerAuthRequiredException
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import java.net.HttpURLConnection
private const val JSON = "application/json" private const val JSON = "application/json"
private const val HTML = "text/html" private const val HTML = "text/html"
@ -18,12 +21,16 @@ class MALInterceptor(private val storage: ScrobblerStorage) : Interceptor {
val request = sourceRequest.newBuilder() val request = sourceRequest.newBuilder()
request.header(CommonHeaders.CONTENT_TYPE, JSON) request.header(CommonHeaders.CONTENT_TYPE, JSON)
request.header(CommonHeaders.ACCEPT, JSON) request.header(CommonHeaders.ACCEPT, JSON)
if (!sourceRequest.url.pathSegments.contains("oauth")) { val isAuthRequest = sourceRequest.url.pathSegments.contains("oauth")
if (!isAuthRequest) {
storage.accessToken?.let { storage.accessToken?.let {
request.header(CommonHeaders.AUTHORIZATION, "Bearer $it") request.header(CommonHeaders.AUTHORIZATION, "Bearer $it")
} }
} }
val response = chain.proceed(request.build()) val response = chain.proceed(request.build())
if (!isAuthRequest && response.code == HttpURLConnection.HTTP_UNAUTHORIZED) {
throw ScrobblerAuthRequiredException(ScrobblerService.MAL)
}
if (response.mimeType == HTML) { if (response.mimeType == HTML) {
throw IOException(response.parseHtml().title()) throw IOException(response.parseHtml().title())
} }

@ -5,6 +5,9 @@ import okhttp3.Response
import okio.IOException import okio.IOException
import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
import org.koitharu.kotatsu.scrobbling.common.domain.ScrobblerAuthRequiredException
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import java.net.HttpURLConnection
private const val USER_AGENT_SHIKIMORI = "Kotatsu" private const val USER_AGENT_SHIKIMORI = "Kotatsu"
@ -14,12 +17,16 @@ class ShikimoriInterceptor(private val storage: ScrobblerStorage) : Interceptor
val sourceRequest = chain.request() val sourceRequest = chain.request()
val request = sourceRequest.newBuilder() val request = sourceRequest.newBuilder()
request.header(CommonHeaders.USER_AGENT, USER_AGENT_SHIKIMORI) request.header(CommonHeaders.USER_AGENT, USER_AGENT_SHIKIMORI)
if (!sourceRequest.url.pathSegments.contains("oauth")) { val isAuthRequest = sourceRequest.url.pathSegments.contains("oauth")
if (!isAuthRequest) {
storage.accessToken?.let { storage.accessToken?.let {
request.header(CommonHeaders.AUTHORIZATION, "Bearer $it") request.header(CommonHeaders.AUTHORIZATION, "Bearer $it")
} }
} }
val response = chain.proceed(request.build()) val response = chain.proceed(request.build())
if (!isAuthRequest && response.code == HttpURLConnection.HTTP_UNAUTHORIZED) {
throw ScrobblerAuthRequiredException(ScrobblerService.SHIKIMORI)
}
if (!response.isSuccessful && !response.isRedirect) { if (!response.isSuccessful && !response.isRedirect) {
throw IOException("${response.code} ${response.message}") throw IOException("${response.code} ${response.message}")
} }

@ -3,7 +3,6 @@ package org.koitharu.kotatsu.settings
import android.accounts.AccountManager import android.accounts.AccountManager
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.preference.Preference import androidx.preference.Preference
@ -15,22 +14,17 @@ import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.ui.dialog.buildAlertDialog
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
import org.koitharu.kotatsu.scrobbling.anilist.data.AniListRepository
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerRepository
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.common.ui.ScrobblerAuthHelper
import org.koitharu.kotatsu.scrobbling.common.ui.config.ScrobblerConfigActivity import org.koitharu.kotatsu.scrobbling.common.ui.config.ScrobblerConfigActivity
import org.koitharu.kotatsu.scrobbling.kitsu.data.KitsuRepository
import org.koitharu.kotatsu.scrobbling.mal.data.MALRepository
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository
import org.koitharu.kotatsu.sync.domain.SyncController
import org.koitharu.kotatsu.sync.ui.SyncSettingsIntent
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.scrobbling.kitsu.ui.KitsuAuthActivity
import org.koitharu.kotatsu.settings.utils.SplitSwitchPreference import org.koitharu.kotatsu.settings.utils.SplitSwitchPreference
import org.koitharu.kotatsu.stats.ui.StatsActivity import org.koitharu.kotatsu.stats.ui.StatsActivity
import org.koitharu.kotatsu.sync.domain.SyncController
import org.koitharu.kotatsu.sync.ui.SyncSettingsIntent
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -38,19 +32,10 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services),
SharedPreferences.OnSharedPreferenceChangeListener { SharedPreferences.OnSharedPreferenceChangeListener {
@Inject @Inject
lateinit var shikimoriRepository: ShikimoriRepository lateinit var syncController: SyncController
@Inject
lateinit var aniListRepository: AniListRepository
@Inject
lateinit var malRepository: MALRepository
@Inject @Inject
lateinit var kitsuRepository: KitsuRepository lateinit var scrobblerAuthHelper: ScrobblerAuthHelper
@Inject
lateinit var syncController: SyncController
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_services) addPreferencesFromResource(R.xml.pref_services)
@ -76,10 +61,10 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services),
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
bindScrobblerSummary(AppSettings.KEY_SHIKIMORI, shikimoriRepository) bindScrobblerSummary(AppSettings.KEY_SHIKIMORI, ScrobblerService.SHIKIMORI)
bindScrobblerSummary(AppSettings.KEY_ANILIST, aniListRepository) bindScrobblerSummary(AppSettings.KEY_ANILIST, ScrobblerService.ANILIST)
bindScrobblerSummary(AppSettings.KEY_MAL, malRepository) bindScrobblerSummary(AppSettings.KEY_MAL, ScrobblerService.MAL)
bindScrobblerSummary(AppSettings.KEY_KITSU, kitsuRepository) bindScrobblerSummary(AppSettings.KEY_KITSU, ScrobblerService.KITSU)
bindSyncSummary() bindSyncSummary()
} }
@ -94,38 +79,22 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services),
override fun onPreferenceTreeClick(preference: Preference): Boolean { override fun onPreferenceTreeClick(preference: Preference): Boolean {
return when (preference.key) { return when (preference.key) {
AppSettings.KEY_SHIKIMORI -> { AppSettings.KEY_SHIKIMORI -> {
if (!shikimoriRepository.isAuthorized) { handleScrobblerClick(ScrobblerService.SHIKIMORI)
launchScrobblerAuth(shikimoriRepository)
} else {
startActivity(ScrobblerConfigActivity.newIntent(preference.context, ScrobblerService.SHIKIMORI))
}
true true
} }
AppSettings.KEY_MAL -> { AppSettings.KEY_MAL -> {
if (!malRepository.isAuthorized) { handleScrobblerClick(ScrobblerService.MAL)
launchScrobblerAuth(malRepository)
} else {
startActivity(ScrobblerConfigActivity.newIntent(preference.context, ScrobblerService.MAL))
}
true true
} }
AppSettings.KEY_ANILIST -> { AppSettings.KEY_ANILIST -> {
if (!aniListRepository.isAuthorized) { handleScrobblerClick(ScrobblerService.ANILIST)
launchScrobblerAuth(aniListRepository)
} else {
startActivity(ScrobblerConfigActivity.newIntent(preference.context, ScrobblerService.ANILIST))
}
true true
} }
AppSettings.KEY_KITSU -> { AppSettings.KEY_KITSU -> {
if (!kitsuRepository.isAuthorized) { handleScrobblerClick(ScrobblerService.KITSU)
startActivity(Intent(preference.context, KitsuAuthActivity::class.java))
} else {
startActivity(ScrobblerConfigActivity.newIntent(preference.context, ScrobblerService.KITSU))
}
true true
} }
@ -147,14 +116,14 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services),
private fun bindScrobblerSummary( private fun bindScrobblerSummary(
key: String, key: String,
repository: ScrobblerRepository scrobblerService: ScrobblerService
) { ) {
val pref = findPreference<Preference>(key) ?: return val pref = findPreference<Preference>(key) ?: return
if (!repository.isAuthorized) { if (!scrobblerAuthHelper.isAuthorized(scrobblerService)) {
pref.setSummary(R.string.disabled) pref.setSummary(R.string.disabled)
return return
} }
val username = repository.cachedUser?.nickname val username = scrobblerAuthHelper.getCachedUser(scrobblerService)?.nickname
if (username != null) { if (username != null) {
pref.summary = getString(R.string.logged_in_as, username) pref.summary = getString(R.string.logged_in_as, username)
} else { } else {
@ -162,7 +131,7 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services),
viewLifecycleScope.launch { viewLifecycleScope.launch {
pref.summary = withContext(Dispatchers.Default) { pref.summary = withContext(Dispatchers.Default) {
runCatching { runCatching {
val user = repository.loadUser() val user = scrobblerAuthHelper.getUser(scrobblerService)
getString(R.string.logged_in_as, user.nickname) getString(R.string.logged_in_as, user.nickname)
}.getOrElse { }.getOrElse {
it.printStackTraceDebug() it.printStackTraceDebug()
@ -173,13 +142,11 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services),
} }
} }
private fun launchScrobblerAuth(repository: ScrobblerRepository) { private fun handleScrobblerClick(scrobblerService: ScrobblerService) {
runCatching { if (!scrobblerAuthHelper.isAuthorized(scrobblerService)) {
val intent = Intent(Intent.ACTION_VIEW) confirmScrobblerAuth(scrobblerService)
intent.data = Uri.parse(repository.oauthUrl) } else {
startActivity(intent) startActivity(ScrobblerConfigActivity.newIntent(context ?: return, scrobblerService))
}.onFailure {
Snackbar.make(listView, it.getDisplayMessage(resources), Snackbar.LENGTH_LONG).show()
} }
} }
@ -211,4 +178,18 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services),
if (settings.isStatsEnabled) R.string.enabled else R.string.disabled, if (settings.isStatsEnabled) R.string.enabled else R.string.disabled,
) )
} }
private fun confirmScrobblerAuth(scrobblerService: ScrobblerService) {
buildAlertDialog(context ?: return, isCentered = true) {
setIcon(scrobblerService.iconResId)
setTitle(scrobblerService.titleResId)
setMessage(context.getString(R.string.scrobbler_auth_intro, context.getString(scrobblerService.titleResId)))
setPositiveButton(R.string.sign_in) { _, _ ->
scrobblerAuthHelper.startAuth(context, scrobblerService).onFailure {
Snackbar.make(listView, it.getDisplayMessage(resources), Snackbar.LENGTH_LONG).show()
}
}
setNegativeButton(android.R.string.cancel, null)
}.show()
}
} }

@ -7,7 +7,6 @@ import androidx.preference.Preference
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.model.getTitle import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.parser.EmptyMangaRepository import org.koitharu.kotatsu.core.parser.EmptyMangaRepository
@ -27,7 +26,6 @@ import java.io.File
class SourceSettingsFragment : BasePreferenceFragment(0), Preference.OnPreferenceChangeListener { class SourceSettingsFragment : BasePreferenceFragment(0), Preference.OnPreferenceChangeListener {
private val viewModel: SourceSettingsViewModel by viewModels() private val viewModel: SourceSettingsViewModel by viewModels()
private val exceptionResolver = ExceptionResolver(this)
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()

@ -23,7 +23,6 @@ import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.ParserMangaRepository import org.koitharu.kotatsu.core.parser.ParserMangaRepository
import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.TaggedActivityResult
import org.koitharu.kotatsu.core.util.ext.configureForParser import org.koitharu.kotatsu.core.util.ext.configureForParser
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
@ -132,13 +131,13 @@ class SourceAuthActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallba
viewBinding.webView.updatePadding(bottom = insets.bottom) viewBinding.webView.updatePadding(bottom = insets.bottom)
} }
class Contract : ActivityResultContract<MangaSource, TaggedActivityResult>() { class Contract : ActivityResultContract<MangaSource, Boolean>() {
override fun createIntent(context: Context, input: MangaSource): Intent { override fun createIntent(context: Context, input: MangaSource): Intent {
return newIntent(context, input) return newIntent(context, input)
} }
override fun parseResult(resultCode: Int, intent: Intent?): TaggedActivityResult { override fun parseResult(resultCode: Int, intent: Intent?): Boolean {
return TaggedActivityResult(TAG, resultCode) return resultCode == RESULT_OK
} }
} }

@ -694,4 +694,6 @@
<string name="sort_order_desc">Descending</string> <string name="sort_order_desc">Descending</string>
<string name="by_date">Date</string> <string name="by_date">Date</string>
<string name="popularity">Popularity</string> <string name="popularity">Popularity</string>
<string name="scrobbler_auth_required">Sign in to %s to continue</string>
<string name="scrobbler_auth_intro">Sign in to set up integration with %s. This will allow you to track your manga reading progress and status</string>
</resources> </resources>

Loading…
Cancel
Save