diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareActivity.kt index 28c2f47d6..b75711ae4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareActivity.kt @@ -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(), CloudFlareCal } } - class Contract : ActivityResultContract() { + class Contract : ActivityResultContract() { 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 } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/resolve/ExceptionResolver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/resolve/ExceptionResolver.kt index 0241c200a..44bd5d976 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/resolve/ExceptionResolver.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/resolve/ExceptionResolver.kt @@ -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 { - +class ExceptionResolver @AssistedInject constructor( + @Assisted private val host: Host, + private val settings: AppSettings, + private val scrobblerAuthHelperProvider: Provider, +) { private val continuations = MutableScatterMap>(1) - private val activity: FragmentActivity? - private val fragment: Fragment? - private val sourceAuthContract: ActivityResultLauncher - private val cloudflareContract: ActivityResultLauncher - - 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) - } - constructor(fragment: Fragment) { - this.fragment = fragment - activity = null - sourceAuthContract = fragment.registerForActivityResult(SourceAuthActivity.Contract(), this) - cloudflareContract = fragment.registerForActivityResult(CloudFlareActivity.Contract(), this) + private val sourceAuthContract = host.registerForActivityResult(SourceAuthActivity.Contract()) { + handleActivityResult(SourceAuthActivity.TAG, it) } - - 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 { } is ProxyConfigException -> { - context?.run { + host.withContext { startActivity(SettingsActivity.newProxySettingsIntent(this)) } false @@ -93,6 +79,20 @@ class ExceptionResolver : ActivityResultCallback { 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 { 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 { .show() } - private fun getAppSettings(context: Context): AppSettings { - return EntryPointAccessors.fromApplication(context).settings + private inline fun Host.withContext(block: Context.() -> Unit) { + 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 { @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, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseActivity.kt index 15413de01..391e846e4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseActivity.kt @@ -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 : AppCompatActivity(), + ExceptionResolver.Host, ScreenshotPolicyHelper.ContentContainer, WindowInsetsDelegate.WindowInsetsListener { @@ -41,8 +38,8 @@ abstract class BaseActivity : 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 : private var defaultStatusBarColor = Color.TRANSPARENT override fun onCreate(savedInstanceState: Bundle?) { - val settings = EntryPointAccessors.fromApplication(this, BaseActivityEntryPoint::class.java).settings + val entryPoint = EntryPointAccessors.fromApplication(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 : 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 : protected fun hasViewBinding() = ::viewBinding.isInitialized - @EntryPoint - @InstallIn(SingletonComponent::class) - interface BaseActivityEntryPoint { - val settings: AppSettings - } - companion object { const val EXTRA_DATA = "data" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseActivityEntryPoint.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseActivityEntryPoint.kt new file mode 100644 index 000000000..d92236b47 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseActivityEntryPoint.kt @@ -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 +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseFragment.kt index 262622209..a611a1b66 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseFragment.kt @@ -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 : 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 : protected val actionModeDelegate: ActionModeDelegate get() = (requireActivity() as BaseActivity<*>).actionModeDelegate + override fun onAttach(context: Context) { + super.onAttach(context) + val entryPoint = EntryPointAccessors.fromApplication(context) + exceptionResolver = entryPoint.exceptionResolverFactory.create(this) + } + final override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BasePreferenceFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BasePreferenceFragment.kt index 0a5a90c07..43b29b380 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BasePreferenceFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BasePreferenceFragment.kt @@ -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(context) + exceptionResolver = entryPoint.exceptionResolverFactory.create(this) + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val themedContext = (view.parentView ?: view).context diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/BaseAdaptiveSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/BaseAdaptiveSheet.kt index b776652e5..5d239f589 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/BaseAdaptiveSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/BaseAdaptiveSheet.kt @@ -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 : AppCompatDialogFragment() { +abstract class BaseAdaptiveSheet : 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 : AppCompatDialogFragment() { private set private var lockCounter = 0 + override fun onAttach(context: Context) { + super.onAttach(context) + val entryPoint = EntryPointAccessors.fromApplication(context) + exceptionResolver = entryPoint.exceptionResolverFactory.create(this) + } + final override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/TaggedActivityResult.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/TaggedActivityResult.kt deleted file mode 100644 index c55aaa121..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/TaggedActivityResult.kt +++ /dev/null @@ -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 -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Throwable.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Throwable.kt index 13b3b7cdb..c193e01c9 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Throwable.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Throwable.kt @@ -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) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt index 1c9cc6401..58960a50f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt @@ -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(), } 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) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt index 51842f604..57b78605d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt @@ -299,7 +299,7 @@ abstract class MangaListFragment : } R.id.action_favourite -> { - FavoriteSheet.show(childFragmentManager, selectedItems) + FavoriteSheet.show(getChildFragmentManager(), selectedItems) mode.finish() true } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListFragment.kt index 069240354..8deaed651 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListFragment.kt @@ -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) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt index 28795aac5..626dc91f1 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt @@ -66,7 +66,7 @@ class RemoteListFragment : MangaListFragment(), FilterOwner { } override fun onFilterClick(view: View?) { - FilterSheetFragment.show(childFragmentManager) + FilterSheetFragment.show(getChildFragmentManager()) } override fun onEmptyActionClick() { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/anilist/data/AniListInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/anilist/data/AniListInterceptor.kt index 25045c83e..04a39ad4d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/anilist/data/AniListInterceptor.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/anilist/data/AniListInterceptor.kt @@ -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 } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/ScrobblerAuthRequiredException.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/ScrobblerAuthRequiredException.kt new file mode 100644 index 000000000..d8a554de9 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/ScrobblerAuthRequiredException.kt @@ -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() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/ScrobblerRepositoryMap.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/ScrobblerRepositoryMap.kt new file mode 100644 index 000000000..5d1874535 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/ScrobblerRepositoryMap.kt @@ -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, + private val aniListRepository: Provider, + private val malRepository: Provider, + private val kitsuRepository: Provider, +) { + + operator fun get(scrobblerService: ScrobblerService): ScrobblerRepository = when (scrobblerService) { + ScrobblerService.SHIKIMORI -> shikimoriRepository + ScrobblerService.ANILIST -> aniListRepository + ScrobblerService.MAL -> malRepository + ScrobblerService.KITSU -> kitsuRepository + }.get() +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/ScrobblerAuthHelper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/ScrobblerAuthHelper.kt new file mode 100644 index 000000000..330a8edbf --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/ScrobblerAuthHelper.kt @@ -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)) + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorSheet.kt index 2c28d382d..604602ae1 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorSheet.kt @@ -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 { + ListStateHolderListener, + AsyncListDiffer.ListListener { @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() { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorViewModel.kt index df8238106..cf21cdf49 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorViewModel.kt @@ -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() + 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 }, + ) + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuInterceptor.kt index c1a087fd8..302fce4f6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuInterceptor.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuInterceptor.kt @@ -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 { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/mal/data/MALInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/mal/data/MALInterceptor.kt index fb0e0dcc1..f11362533 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/mal/data/MALInterceptor.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/mal/data/MALInterceptor.kt @@ -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()) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriInterceptor.kt index dfd6e93aa..ae5de4501 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriInterceptor.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriInterceptor.kt @@ -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}") } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/ServicesSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/ServicesSettingsFragment.kt index c92f69fa1..af1f7a465 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/ServicesSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/ServicesSettingsFragment.kt @@ -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,22 +14,17 @@ 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 @@ -38,19 +32,10 @@ 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 + lateinit var syncController: SyncController @Inject - lateinit var syncController: SyncController + lateinit var scrobblerAuthHelper: ScrobblerAuthHelper override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.pref_services) @@ -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(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() + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsFragment.kt index d9b592942..ba9645bf6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsFragment.kt @@ -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() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt index 6cf680fdf..ea285a8a4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt @@ -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(), BrowserCallba viewBinding.webView.updatePadding(bottom = insets.bottom) } - class Contract : ActivityResultContract() { + class Contract : ActivityResultContract() { override fun createIntent(context: Context, input: MangaSource): Intent { return newIntent(context, input) } - override fun parseResult(resultCode: Int, intent: Intent?): TaggedActivityResult { - return TaggedActivityResult(TAG, resultCode) + override fun parseResult(resultCode: Int, intent: Intent?): Boolean { + return resultCode == RESULT_OK } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ad29b5c7a..42fbc29c1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -694,4 +694,6 @@ Descending Date Popularity + Sign in to %s to continue + Sign in to set up integration with %s. This will allow you to track your manga reading progress and status