Handle scrobbler authorization errors
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.browser.cloudflare
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
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.cookies.MutableCookieJar
|
||||
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.databinding.ActivityBrowserBinding
|
||||
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 {
|
||||
return newIntent(context, input)
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): TaggedActivityResult {
|
||||
return TaggedActivityResult(TAG, resultCode)
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): Boolean {
|
||||
return resultCode == Activity.RESULT_OK
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@ package org.koitharu.kotatsu.core.exceptions.resolve
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.ActivityResultCaller
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.collection.MutableScatterMap
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.fragment.app.FragmentManager
|
||||
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.alternatives.ui.AlternativesActivity
|
||||
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.UnsupportedSourceException
|
||||
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.util.TaggedActivityResult
|
||||
import org.koitharu.kotatsu.core.util.ext.findActivity
|
||||
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
|
||||
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
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.sources.auth.SourceAuthActivity
|
||||
import java.security.cert.CertPathValidatorException
|
||||
import javax.inject.Provider
|
||||
import javax.net.ssl.SSLException
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
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 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) {
|
||||
this.activity = activity
|
||||
fragment = null
|
||||
sourceAuthContract = activity.registerForActivityResult(SourceAuthActivity.Contract(), this)
|
||||
cloudflareContract = activity.registerForActivityResult(CloudFlareActivity.Contract(), this)
|
||||
private val sourceAuthContract = host.registerForActivityResult(SourceAuthActivity.Contract()) {
|
||||
handleActivityResult(SourceAuthActivity.TAG, it)
|
||||
}
|
||||
|
||||
constructor(fragment: Fragment) {
|
||||
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)
|
||||
private val cloudflareContract = host.registerForActivityResult(CloudFlareActivity.Contract()) {
|
||||
handleActivityResult(CloudFlareActivity.TAG, it)
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -77,7 +63,7 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
|
||||
}
|
||||
|
||||
is ProxyConfigException -> {
|
||||
context?.run {
|
||||
host.withContext {
|
||||
startActivity(SettingsActivity.newProxySettingsIntent(this))
|
||||
}
|
||||
false
|
||||
@@ -93,6 +79,20 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -106,21 +106,20 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
|
||||
sourceAuthContract.launch(source)
|
||||
}
|
||||
|
||||
private fun openInBrowser(url: String) {
|
||||
context?.run {
|
||||
startActivity(BrowserActivity.newIntent(this, url, null, null))
|
||||
}
|
||||
private fun openInBrowser(url: String) = host.withContext {
|
||||
startActivity(BrowserActivity.newIntent(this, url, null, null))
|
||||
}
|
||||
|
||||
private fun openAlternatives(manga: Manga) {
|
||||
context?.run {
|
||||
startActivity(AlternativesActivity.newIntent(this, manga))
|
||||
}
|
||||
private fun openAlternatives(manga: Manga) = host.withContext {
|
||||
startActivity(AlternativesActivity.newIntent(this, manga))
|
||||
}
|
||||
|
||||
private fun handleActivityResult(tag: String, result: Boolean) {
|
||||
continuations.remove(tag)?.resume(result)
|
||||
}
|
||||
|
||||
private fun showSslErrorDialog() {
|
||||
val ctx = context ?: return
|
||||
val settings = getAppSettings(ctx)
|
||||
val ctx = host.getContext() ?: return
|
||||
if (settings.isSSLBypassEnabled) {
|
||||
Toast.makeText(ctx, R.string.operation_not_supported, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
@@ -136,18 +135,31 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun getAppSettings(context: Context): AppSettings {
|
||||
return EntryPointAccessors.fromApplication<BaseActivityEntryPoint>(context).settings
|
||||
private inline fun Host.withContext(block: Context.() -> Unit) {
|
||||
getContext()?.apply(block)
|
||||
}
|
||||
|
||||
private fun getFragmentManager() = checkNotNull(fragment?.childFragmentManager ?: activity?.supportFragmentManager)
|
||||
interface Host : ActivityResultCaller {
|
||||
|
||||
fun getChildFragmentManager(): FragmentManager
|
||||
|
||||
fun getContext(): Context?
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
||||
fun create(host: Host): ExceptionResolver
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@StringRes
|
||||
fun getResolveStringId(e: Throwable) = when (e) {
|
||||
is CloudFlareProtectedException -> R.string.captcha_solve
|
||||
is ScrobblerAuthRequiredException,
|
||||
is AuthRequiredException -> R.string.sign_in
|
||||
|
||||
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 SSLException,
|
||||
|
||||
@@ -14,25 +14,22 @@ import androidx.appcompat.view.ActionMode
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
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.ui.util.ActionModeDelegate
|
||||
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
|
||||
import org.koitharu.kotatsu.core.util.ext.isWebViewUnavailable
|
||||
import org.koitharu.kotatsu.main.ui.protect.ScreenshotPolicyHelper
|
||||
|
||||
@Suppress("LeakingThis")
|
||||
abstract class BaseActivity<B : ViewBinding> :
|
||||
AppCompatActivity(),
|
||||
ExceptionResolver.Host,
|
||||
ScreenshotPolicyHelper.ContentContainer,
|
||||
WindowInsetsDelegate.WindowInsetsListener {
|
||||
|
||||
@@ -41,8 +38,8 @@ abstract class BaseActivity<B : ViewBinding> :
|
||||
lateinit var viewBinding: B
|
||||
private set
|
||||
|
||||
@JvmField
|
||||
protected val exceptionResolver = ExceptionResolver(this)
|
||||
protected lateinit var exceptionResolver: ExceptionResolver
|
||||
private set
|
||||
|
||||
@JvmField
|
||||
protected val insetsDelegate = WindowInsetsDelegate()
|
||||
@@ -53,13 +50,15 @@ abstract class BaseActivity<B : ViewBinding> :
|
||||
private var defaultStatusBarColor = Color.TRANSPARENT
|
||||
|
||||
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
|
||||
setTheme(settings.colorScheme.styleResId)
|
||||
if (isAmoledTheme) {
|
||||
setTheme(R.style.ThemeOverlay_Kotatsu_Amoled)
|
||||
}
|
||||
putDataToExtras(intent)
|
||||
exceptionResolver = entryPoint.exceptionResolverFactory.create(this)
|
||||
super.onCreate(savedInstanceState)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
insetsDelegate.handleImeInsets = true
|
||||
@@ -88,6 +87,10 @@ abstract class BaseActivity<B : ViewBinding> :
|
||||
setupToolbar()
|
||||
}
|
||||
|
||||
override fun getContext() = this
|
||||
|
||||
override fun getChildFragmentManager(): FragmentManager = supportFragmentManager
|
||||
|
||||
protected fun setContentView(binding: B) {
|
||||
this.viewBinding = binding
|
||||
super.setContentView(binding.root)
|
||||
@@ -178,12 +181,6 @@ abstract class BaseActivity<B : ViewBinding> :
|
||||
|
||||
protected fun hasViewBinding() = ::viewBinding.isInitialized
|
||||
|
||||
@EntryPoint
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface BaseActivityEntryPoint {
|
||||
val settings: AppSettings
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
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
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate
|
||||
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
|
||||
|
||||
@Suppress("LeakingThis")
|
||||
abstract class BaseFragment<B : ViewBinding> :
|
||||
Fragment(),
|
||||
ExceptionResolver.Host,
|
||||
WindowInsetsDelegate.WindowInsetsListener {
|
||||
|
||||
var viewBinding: B? = null
|
||||
private set
|
||||
|
||||
@JvmField
|
||||
protected val exceptionResolver = ExceptionResolver(this)
|
||||
protected lateinit var exceptionResolver: ExceptionResolver
|
||||
private set
|
||||
|
||||
@JvmField
|
||||
protected val insetsDelegate = WindowInsetsDelegate()
|
||||
@@ -27,6 +29,12 @@ abstract class BaseFragment<B : ViewBinding> :
|
||||
protected val actionModeDelegate: 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(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.koitharu.kotatsu.core.ui
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
@@ -12,7 +13,9 @@ import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
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.ui.util.RecyclerViewOwner
|
||||
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
|
||||
@@ -25,7 +28,11 @@ import javax.inject.Inject
|
||||
abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
|
||||
PreferenceFragmentCompat(),
|
||||
WindowInsetsDelegate.WindowInsetsListener,
|
||||
RecyclerViewOwner {
|
||||
RecyclerViewOwner,
|
||||
ExceptionResolver.Host {
|
||||
|
||||
protected lateinit var exceptionResolver: ExceptionResolver
|
||||
private set
|
||||
|
||||
@Inject
|
||||
lateinit var settings: AppSettings
|
||||
@@ -36,6 +43,12 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
|
||||
override val recyclerView: RecyclerView
|
||||
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?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
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.BottomSheetDialog
|
||||
import com.google.android.material.sidesheet.SideSheetDialog
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
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.BaseActivityEntryPoint
|
||||
import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate
|
||||
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 isFitToContentsDisabled = false
|
||||
|
||||
protected lateinit var exceptionResolver: ExceptionResolver
|
||||
private set
|
||||
|
||||
var viewBinding: B? = null
|
||||
private set
|
||||
|
||||
@@ -50,6 +56,12 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
|
||||
private set
|
||||
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(
|
||||
inflater: LayoutInflater,
|
||||
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.ParseException
|
||||
import org.koitharu.kotatsu.parsers.exception.TooManyRequestExceptions
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.ScrobblerAuthRequiredException
|
||||
import java.net.SocketTimeoutException
|
||||
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"
|
||||
|
||||
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 CloudFlareProtectedException -> resources.getString(R.string.captcha_required)
|
||||
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.SortDirection
|
||||
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.widgets.ChipsView
|
||||
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.parentView
|
||||
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.databinding.SheetFilterBinding
|
||||
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.MangaState
|
||||
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 com.google.android.material.R as materialR
|
||||
|
||||
@@ -106,7 +101,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
||||
}
|
||||
|
||||
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 -> {
|
||||
FavoriteSheet.show(childFragmentManager, selectedItems)
|
||||
FavoriteSheet.show(getChildFragmentManager(), selectedItems)
|
||||
mode.finish()
|
||||
true
|
||||
}
|
||||
|
||||
@@ -66,11 +66,11 @@ class LocalListFragment : MangaListFragment(), FilterOwner {
|
||||
}
|
||||
|
||||
override fun onEmptyActionClick() {
|
||||
ImportDialogFragment.show(childFragmentManager)
|
||||
ImportDialogFragment.show(getChildFragmentManager())
|
||||
}
|
||||
|
||||
override fun onFilterClick(view: View?) {
|
||||
FilterSheetFragment.show(childFragmentManager)
|
||||
FilterSheetFragment.show(getChildFragmentManager())
|
||||
}
|
||||
|
||||
override fun onPrimaryButtonClick(tipView: TipView) {
|
||||
|
||||
@@ -66,7 +66,7 @@ class RemoteListFragment : MangaListFragment(), FilterOwner {
|
||||
}
|
||||
|
||||
override fun onFilterClick(view: View?) {
|
||||
FilterSheetFragment.show(childFragmentManager)
|
||||
FilterSheetFragment.show(getChildFragmentManager())
|
||||
}
|
||||
|
||||
override fun onEmptyActionClick() {
|
||||
|
||||
@@ -4,6 +4,9 @@ import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
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"
|
||||
|
||||
@@ -14,11 +17,16 @@ class AniListInterceptor(private val storage: ScrobblerStorage) : Interceptor {
|
||||
val request = sourceRequest.newBuilder()
|
||||
request.header(CommonHeaders.CONTENT_TYPE, JSON)
|
||||
request.header(CommonHeaders.ACCEPT, JSON)
|
||||
if (!sourceRequest.url.pathSegments.contains("oauth")) {
|
||||
val isAuthRequest = sourceRequest.url.pathSegments.contains("oauth")
|
||||
if (!isAuthRequest) {
|
||||
storage.accessToken?.let {
|
||||
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 com.google.android.material.tabs.TabLayout
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
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.parser.MangaIntent
|
||||
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.setProgressIcon
|
||||
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.databinding.SheetScrobblingSelectorBinding
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
||||
@@ -50,7 +53,8 @@ class ScrobblingSelectorSheet :
|
||||
MenuItem.OnActionExpandListener,
|
||||
SearchView.OnQueryTextListener,
|
||||
TabLayout.OnTabSelectedListener,
|
||||
ListStateHolderListener, AsyncListDiffer.ListListener<ListModel> {
|
||||
ListStateHolderListener,
|
||||
AsyncListDiffer.ListListener<ListModel> {
|
||||
|
||||
@Inject
|
||||
lateinit var coil: ImageLoader
|
||||
@@ -134,7 +138,15 @@ class ScrobblingSelectorSheet :
|
||||
}
|
||||
|
||||
override fun onRetryClick(error: Throwable) {
|
||||
viewModel.retry()
|
||||
if (ExceptionResolver.canResolve(error)) {
|
||||
viewLifecycleScope.launch {
|
||||
if (exceptionResolver.resolve(error)) {
|
||||
viewModel.retry()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
viewModel.retry()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEmptyActionClick() {
|
||||
|
||||
@@ -14,11 +14,13 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
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.parser.MangaIntent
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
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.require
|
||||
import org.koitharu.kotatsu.core.util.ext.requireValue
|
||||
@@ -79,8 +81,8 @@ class ScrobblingSelectorViewModel @Inject constructor(
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
|
||||
|
||||
val selectedItemId = MutableStateFlow(NO_ID)
|
||||
val searchQuery = MutableStateFlow(manga.title)
|
||||
val onClose = MutableEventFlow<Unit>()
|
||||
private val searchQuery = MutableStateFlow(manga.title)
|
||||
|
||||
val isEmpty: Boolean
|
||||
get() = scrobblerMangaList.value.isEmpty()
|
||||
@@ -201,11 +203,14 @@ class ScrobblingSelectorViewModel @Inject constructor(
|
||||
actionStringRes = R.string.search,
|
||||
)
|
||||
|
||||
private fun errorHint(e: Throwable) = ScrobblerHint(
|
||||
icon = R.drawable.ic_error_large,
|
||||
textPrimary = R.string.error_occurred,
|
||||
error = e,
|
||||
textSecondary = 0,
|
||||
actionStringRes = R.string.try_again,
|
||||
)
|
||||
private fun errorHint(e: Throwable): ScrobblerHint {
|
||||
val resolveAction = ExceptionResolver.getResolveStringId(e)
|
||||
return ScrobblerHint(
|
||||
icon = R.drawable.ic_error_large,
|
||||
textPrimary = R.string.error_occurred,
|
||||
error = e,
|
||||
textSecondary = if (resolveAction == 0) 0 else R.string.try_again,
|
||||
actionStringRes = resolveAction.ifZero { R.string.try_again },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
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 {
|
||||
|
||||
@@ -12,12 +15,17 @@ class KitsuInterceptor(private val storage: ScrobblerStorage) : Interceptor {
|
||||
val request = sourceRequest.newBuilder()
|
||||
request.header(CommonHeaders.CONTENT_TYPE, 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 {
|
||||
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 {
|
||||
|
||||
@@ -7,6 +7,9 @@ import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.parsers.util.mimeType
|
||||
import org.koitharu.kotatsu.parsers.util.parseHtml
|
||||
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 HTML = "text/html"
|
||||
@@ -18,12 +21,16 @@ class MALInterceptor(private val storage: ScrobblerStorage) : Interceptor {
|
||||
val request = sourceRequest.newBuilder()
|
||||
request.header(CommonHeaders.CONTENT_TYPE, JSON)
|
||||
request.header(CommonHeaders.ACCEPT, JSON)
|
||||
if (!sourceRequest.url.pathSegments.contains("oauth")) {
|
||||
val isAuthRequest = sourceRequest.url.pathSegments.contains("oauth")
|
||||
if (!isAuthRequest) {
|
||||
storage.accessToken?.let {
|
||||
request.header(CommonHeaders.AUTHORIZATION, "Bearer $it")
|
||||
}
|
||||
}
|
||||
val response = chain.proceed(request.build())
|
||||
if (!isAuthRequest && response.code == HttpURLConnection.HTTP_UNAUTHORIZED) {
|
||||
throw ScrobblerAuthRequiredException(ScrobblerService.MAL)
|
||||
}
|
||||
if (response.mimeType == HTML) {
|
||||
throw IOException(response.parseHtml().title())
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ import okhttp3.Response
|
||||
import okio.IOException
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
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"
|
||||
|
||||
@@ -14,12 +17,16 @@ class ShikimoriInterceptor(private val storage: ScrobblerStorage) : Interceptor
|
||||
val sourceRequest = chain.request()
|
||||
val request = sourceRequest.newBuilder()
|
||||
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 {
|
||||
request.header(CommonHeaders.AUTHORIZATION, "Bearer $it")
|
||||
}
|
||||
}
|
||||
val response = chain.proceed(request.build())
|
||||
if (!isAuthRequest && response.code == HttpURLConnection.HTTP_UNAUTHORIZED) {
|
||||
throw ScrobblerAuthRequiredException(ScrobblerService.SHIKIMORI)
|
||||
}
|
||||
if (!response.isSuccessful && !response.isRedirect) {
|
||||
throw IOException("${response.code} ${response.message}")
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.settings
|
||||
import android.accounts.AccountManager
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.preference.Preference
|
||||
@@ -15,43 +14,29 @@ import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
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.printStackTraceDebug
|
||||
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.ui.ScrobblerAuthHelper
|
||||
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.stats.ui.StatsActivity
|
||||
import org.koitharu.kotatsu.sync.domain.SyncController
|
||||
import org.koitharu.kotatsu.sync.ui.SyncSettingsIntent
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ServicesSettingsFragment : BasePreferenceFragment(R.string.services),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
@Inject
|
||||
lateinit var shikimoriRepository: ShikimoriRepository
|
||||
|
||||
@Inject
|
||||
lateinit var aniListRepository: AniListRepository
|
||||
|
||||
@Inject
|
||||
lateinit var malRepository: MALRepository
|
||||
|
||||
@Inject
|
||||
lateinit var kitsuRepository: KitsuRepository
|
||||
|
||||
@Inject
|
||||
lateinit var syncController: SyncController
|
||||
|
||||
@Inject
|
||||
lateinit var scrobblerAuthHelper: ScrobblerAuthHelper
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_services)
|
||||
findPreference<SplitSwitchPreference>(AppSettings.KEY_STATS_ENABLED)?.let {
|
||||
@@ -76,10 +61,10 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services),
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
bindScrobblerSummary(AppSettings.KEY_SHIKIMORI, shikimoriRepository)
|
||||
bindScrobblerSummary(AppSettings.KEY_ANILIST, aniListRepository)
|
||||
bindScrobblerSummary(AppSettings.KEY_MAL, malRepository)
|
||||
bindScrobblerSummary(AppSettings.KEY_KITSU, kitsuRepository)
|
||||
bindScrobblerSummary(AppSettings.KEY_SHIKIMORI, ScrobblerService.SHIKIMORI)
|
||||
bindScrobblerSummary(AppSettings.KEY_ANILIST, ScrobblerService.ANILIST)
|
||||
bindScrobblerSummary(AppSettings.KEY_MAL, ScrobblerService.MAL)
|
||||
bindScrobblerSummary(AppSettings.KEY_KITSU, ScrobblerService.KITSU)
|
||||
bindSyncSummary()
|
||||
}
|
||||
|
||||
@@ -94,38 +79,22 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services),
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
return when (preference.key) {
|
||||
AppSettings.KEY_SHIKIMORI -> {
|
||||
if (!shikimoriRepository.isAuthorized) {
|
||||
launchScrobblerAuth(shikimoriRepository)
|
||||
} else {
|
||||
startActivity(ScrobblerConfigActivity.newIntent(preference.context, ScrobblerService.SHIKIMORI))
|
||||
}
|
||||
handleScrobblerClick(ScrobblerService.SHIKIMORI)
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_MAL -> {
|
||||
if (!malRepository.isAuthorized) {
|
||||
launchScrobblerAuth(malRepository)
|
||||
} else {
|
||||
startActivity(ScrobblerConfigActivity.newIntent(preference.context, ScrobblerService.MAL))
|
||||
}
|
||||
handleScrobblerClick(ScrobblerService.MAL)
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_ANILIST -> {
|
||||
if (!aniListRepository.isAuthorized) {
|
||||
launchScrobblerAuth(aniListRepository)
|
||||
} else {
|
||||
startActivity(ScrobblerConfigActivity.newIntent(preference.context, ScrobblerService.ANILIST))
|
||||
}
|
||||
handleScrobblerClick(ScrobblerService.ANILIST)
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_KITSU -> {
|
||||
if (!kitsuRepository.isAuthorized) {
|
||||
startActivity(Intent(preference.context, KitsuAuthActivity::class.java))
|
||||
} else {
|
||||
startActivity(ScrobblerConfigActivity.newIntent(preference.context, ScrobblerService.KITSU))
|
||||
}
|
||||
handleScrobblerClick(ScrobblerService.KITSU)
|
||||
true
|
||||
}
|
||||
|
||||
@@ -147,14 +116,14 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services),
|
||||
|
||||
private fun bindScrobblerSummary(
|
||||
key: String,
|
||||
repository: ScrobblerRepository
|
||||
scrobblerService: ScrobblerService
|
||||
) {
|
||||
val pref = findPreference<Preference>(key) ?: return
|
||||
if (!repository.isAuthorized) {
|
||||
if (!scrobblerAuthHelper.isAuthorized(scrobblerService)) {
|
||||
pref.setSummary(R.string.disabled)
|
||||
return
|
||||
}
|
||||
val username = repository.cachedUser?.nickname
|
||||
val username = scrobblerAuthHelper.getCachedUser(scrobblerService)?.nickname
|
||||
if (username != null) {
|
||||
pref.summary = getString(R.string.logged_in_as, username)
|
||||
} else {
|
||||
@@ -162,7 +131,7 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services),
|
||||
viewLifecycleScope.launch {
|
||||
pref.summary = withContext(Dispatchers.Default) {
|
||||
runCatching {
|
||||
val user = repository.loadUser()
|
||||
val user = scrobblerAuthHelper.getUser(scrobblerService)
|
||||
getString(R.string.logged_in_as, user.nickname)
|
||||
}.getOrElse {
|
||||
it.printStackTraceDebug()
|
||||
@@ -173,13 +142,11 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services),
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchScrobblerAuth(repository: ScrobblerRepository) {
|
||||
runCatching {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = Uri.parse(repository.oauthUrl)
|
||||
startActivity(intent)
|
||||
}.onFailure {
|
||||
Snackbar.make(listView, it.getDisplayMessage(resources), Snackbar.LENGTH_LONG).show()
|
||||
private fun handleScrobblerClick(scrobblerService: ScrobblerService) {
|
||||
if (!scrobblerAuthHelper.isAuthorized(scrobblerService)) {
|
||||
confirmScrobblerAuth(scrobblerService)
|
||||
} else {
|
||||
startActivity(ScrobblerConfigActivity.newIntent(context ?: return, scrobblerService))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,4 +178,18 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services),
|
||||
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 dagger.hilt.android.AndroidEntryPoint
|
||||
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.model.getTitle
|
||||
import org.koitharu.kotatsu.core.parser.EmptyMangaRepository
|
||||
@@ -27,7 +26,6 @@ import java.io.File
|
||||
class SourceSettingsFragment : BasePreferenceFragment(0), Preference.OnPreferenceChangeListener {
|
||||
|
||||
private val viewModel: SourceSettingsViewModel by viewModels()
|
||||
private val exceptionResolver = ExceptionResolver(this)
|
||||
|
||||
override fun 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.ParserMangaRepository
|
||||
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.databinding.ActivityBrowserBinding
|
||||
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
|
||||
@@ -132,13 +131,13 @@ class SourceAuthActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallba
|
||||
viewBinding.webView.updatePadding(bottom = insets.bottom)
|
||||
}
|
||||
|
||||
class Contract : ActivityResultContract<MangaSource, TaggedActivityResult>() {
|
||||
class Contract : ActivityResultContract<MangaSource, Boolean>() {
|
||||
override fun createIntent(context: Context, input: MangaSource): Intent {
|
||||
return newIntent(context, input)
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): TaggedActivityResult {
|
||||
return TaggedActivityResult(TAG, resultCode)
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): Boolean {
|
||||
return resultCode == RESULT_OK
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -694,4 +694,6 @@
|
||||
<string name="sort_order_desc">Descending</string>
|
||||
<string name="by_date">Date</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>
|
||||
|
||||
Reference in New Issue
Block a user