pull/26/head
Koitharu 5 years ago
parent 9a0b7c4700
commit 7fd71c13f3

@ -67,26 +67,26 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2'
implementation 'androidx.core:core-ktx:1.5.0-alpha05' implementation 'androidx.core:core-ktx:1.5.0-alpha05'
implementation 'androidx.activity:activity-ktx:1.2.0-beta02' implementation 'androidx.activity:activity-ktx:1.2.0-rc01'
implementation 'androidx.fragment:fragment-ktx:1.3.0-beta02' implementation 'androidx.fragment:fragment-ktx:1.3.0-rc01'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-beta01' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-rc01'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-beta01' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-rc01'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0-beta01' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0-rc01'
implementation 'androidx.lifecycle:lifecycle-service:2.3.0-beta01' implementation 'androidx.lifecycle:lifecycle-service:2.3.0-rc01'
implementation 'androidx.lifecycle:lifecycle-process:2.3.0-beta01' implementation 'androidx.lifecycle:lifecycle-process:2.3.0-rc01'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.2.0-beta01' implementation 'androidx.recyclerview:recyclerview:1.2.0-beta01'
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01' implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
implementation 'androidx.preference:preference-ktx:1.1.1' implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'androidx.work:work-runtime-ktx:2.4.0' implementation 'androidx.work:work-runtime-ktx:2.4.0'
implementation 'com.google.android.material:material:1.3.0-alpha04' implementation 'com.google.android.material:material:1.3.0-beta01'
//noinspection LifecycleAnnotationProcessorWithJava8 //noinspection LifecycleAnnotationProcessorWithJava8
kapt 'androidx.lifecycle:lifecycle-compiler:2.3.0-beta01' kapt 'androidx.lifecycle:lifecycle-compiler:2.3.0-rc01'
implementation 'androidx.room:room-runtime:2.2.5' implementation 'androidx.room:room-runtime:2.2.6'
implementation 'androidx.room:room-ktx:2.2.5' implementation 'androidx.room:room-ktx:2.2.6'
kapt 'androidx.room:room-compiler:2.2.5' kapt 'androidx.room:room-compiler:2.2.6'
implementation 'com.squareup.okhttp3:okhttp:4.9.0' implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'com.squareup.okio:okio:2.9.0' implementation 'com.squareup.okio:okio:2.9.0'
@ -95,8 +95,8 @@ dependencies {
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.0' implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.0'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.0' implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.0'
implementation 'org.koin:koin-android:2.2.0' implementation 'org.koin:koin-android:2.2.2'
implementation 'org.koin:koin-androidx-viewmodel:2.2.0' implementation 'org.koin:koin-androidx-viewmodel:2.2.2'
implementation 'io.coil-kt:coil-base:1.1.0' implementation 'io.coil-kt:coil-base:1.1.0'
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
implementation 'com.tomclaw.cache:cache:1.0' implementation 'com.tomclaw.cache:cache:1.0'
@ -105,5 +105,5 @@ dependencies {
testImplementation 'junit:junit:4.13.1' testImplementation 'junit:junit:4.13.1'
testImplementation 'org.json:json:20201115' testImplementation 'org.json:json:20201115'
testImplementation 'org.koin:koin-test:2.2.0-rc-2' testImplementation 'org.koin:koin-test:2.2.2'
} }

@ -0,0 +1,3 @@
<resources>
<string name="app_name" translatable="false">Kotatsu Dev</string>
</resources>

@ -40,5 +40,7 @@ abstract class AlertDialogFragment<B : ViewBinding> : DialogFragment() {
open fun onBuildDialog(builder: AlertDialog.Builder) = Unit open fun onBuildDialog(builder: AlertDialog.Builder) = Unit
protected fun bindingOrNull(): B? = viewBinding
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
} }

@ -16,6 +16,7 @@ import androidx.viewbinding.ViewBinding
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(), OnApplyWindowInsetsListener { abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(), OnApplyWindowInsetsListener {
@ -23,6 +24,11 @@ abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(), OnApplyWindo
protected lateinit var binding: B protected lateinit var binding: B
private set private set
protected val exceptionResolver by lazy(LazyThreadSafetyMode.NONE) {
ExceptionResolver(this, supportFragmentManager)
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
if (get<AppSettings>().isAmoledTheme) { if (get<AppSettings>().isAmoledTheme) {
setTheme(R.style.AppTheme_Amoled) setTheme(R.style.AppTheme_Amoled)

@ -11,6 +11,7 @@ import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
abstract class BaseFragment<B : ViewBinding> : Fragment(), OnApplyWindowInsetsListener { abstract class BaseFragment<B : ViewBinding> : Fragment(), OnApplyWindowInsetsListener {
@ -19,6 +20,10 @@ abstract class BaseFragment<B : ViewBinding> : Fragment(), OnApplyWindowInsetsLi
protected val binding: B protected val binding: B
get() = checkNotNull(viewBinding) get() = checkNotNull(viewBinding)
protected val exceptionResolver by lazy(LazyThreadSafetyMode.NONE) {
ExceptionResolver(viewLifecycleOwner, childFragmentManager)
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,

@ -7,15 +7,13 @@ import android.webkit.WebViewClient
import okhttp3.Cookie import okhttp3.Cookie
import okhttp3.CookieJar import okhttp3.CookieJar
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
class CloudFlareClient( class CloudFlareClient(
private val cookieJar: CookieJar,
private val callback: CloudFlareCallback, private val callback: CloudFlareCallback,
private val targetUrl: String private val targetUrl: String
) : WebViewClient(), KoinComponent { ) : WebViewClient() {
private val cookieJar = get<CookieJar>()
private val cookieManager = CookieManager.getInstance() private val cookieManager = CookieManager.getInstance()
init { init {
@ -39,7 +37,8 @@ class CloudFlareClient(
private fun checkClearance() { private fun checkClearance() {
val httpUrl = targetUrl.toHttpUrl() val httpUrl = targetUrl.toHttpUrl()
val cookies = cookieManager.getCookie(targetUrl).split(';').mapNotNull { val rawCookie = cookieManager.getCookie(targetUrl) ?: return
val cookies = rawCookie.split(';').mapNotNull {
Cookie.parse(httpUrl, it) Cookie.parse(httpUrl, it)
} }
if (cookies.none { it.name == CF_CLEARANCE }) { if (cookies.none { it.name == CF_CLEARANCE }) {

@ -1,6 +1,7 @@
package org.koitharu.kotatsu.browser.cloudflare package org.koitharu.kotatsu.browser.cloudflare
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -9,7 +10,8 @@ import android.webkit.CookieManager
import android.webkit.WebSettings import android.webkit.WebSettings
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.fragment.app.setFragmentResult
import org.koin.android.ext.android.get
import org.koitharu.kotatsu.base.ui.AlertDialogFragment import org.koitharu.kotatsu.base.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.network.UserAgentInterceptor import org.koitharu.kotatsu.core.network.UserAgentInterceptor
import org.koitharu.kotatsu.databinding.FragmentCloudflareBinding import org.koitharu.kotatsu.databinding.FragmentCloudflareBinding
@ -19,6 +21,7 @@ import org.koitharu.kotatsu.utils.ext.withArgs
class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), CloudFlareCallback { class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), CloudFlareCallback {
private val url by stringArgument(ARG_URL) private val url by stringArgument(ARG_URL)
private val pendingResult = Bundle(1)
override fun onInflateView( override fun onInflateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -35,7 +38,7 @@ class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), Cloud
databaseEnabled = true databaseEnabled = true
userAgentString = UserAgentInterceptor.userAgent userAgentString = UserAgentInterceptor.userAgent
} }
binding.webView.webViewClient = CloudFlareClient(this, url.orEmpty()) binding.webView.webViewClient = CloudFlareClient(get(), this, url.orEmpty())
CookieManager.getInstance().setAcceptThirdPartyCookies(binding.webView, true) CookieManager.getInstance().setAcceptThirdPartyCookies(binding.webView, true)
if (url.isNullOrEmpty()) { if (url.isNullOrEmpty()) {
dismissAllowingStateLoss() dismissAllowingStateLoss()
@ -63,18 +66,24 @@ class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), Cloud
super.onPause() super.onPause()
} }
override fun onDismiss(dialog: DialogInterface) {
setFragmentResult(TAG, pendingResult)
super.onDismiss(dialog)
}
override fun onPageLoaded() { override fun onPageLoaded() {
binding.progressBar.isInvisible = true bindingOrNull()?.progressBar?.isInvisible = true
} }
override fun onCheckPassed() { override fun onCheckPassed() {
((parentFragment ?: activity) as? SwipeRefreshLayout.OnRefreshListener)?.onRefresh() pendingResult.putBoolean(EXTRA_RESULT, true)
dismiss() dismiss()
} }
companion object { companion object {
const val TAG = "CloudFlareDialog" const val TAG = "CloudFlareDialog"
const val EXTRA_RESULT = "result"
private const val ARG_URL = "url" private const val ARG_URL = "url"
fun newInstance(url: String) = CloudFlareDialog().withArgs(1) { fun newInstance(url: String) = CloudFlareDialog().withArgs(1) {

@ -1,5 +1,12 @@
package org.koitharu.kotatsu.core.exceptions package org.koitharu.kotatsu.core.exceptions
import okio.IOException import okio.IOException
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ResolvableException
class CloudFlareProtectedException(val url: String) : IOException("Protected by CloudFlare") class CloudFlareProtectedException(
val url: String
) : IOException("Protected by CloudFlare"), ResolvableException {
override val resolveTextId: Int = R.string.resolve
}

@ -0,0 +1,38 @@
package org.koitharu.kotatsu.core.exceptions.resolve
import android.util.ArrayMap
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.suspendCancellableCoroutine
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
class ExceptionResolver(
private val lifecycleOwner: LifecycleOwner,
private val fm: FragmentManager
) {
private val continuations = ArrayMap<String, Continuation<Boolean>>(1)
suspend fun resolve(e: ResolvableException): Boolean = when (e) {
is CloudFlareProtectedException -> resolveCF(e.url)
else -> false
}
private suspend fun resolveCF(url: String) = suspendCancellableCoroutine<Boolean> { cont ->
val dialog = CloudFlareDialog.newInstance(url)
fm.clearFragmentResult(CloudFlareDialog.TAG)
continuations[CloudFlareDialog.TAG] = cont
fm.setFragmentResultListener(CloudFlareDialog.TAG, lifecycleOwner) { key, result ->
continuations.remove(key)?.resume(result.getBoolean(CloudFlareDialog.EXTRA_RESULT))
}
dialog.show(fm, CloudFlareDialog.TAG)
cont.invokeOnCancellation {
continuations.remove(CloudFlareDialog.TAG, cont)
fm.clearFragmentResultListener(CloudFlareDialog.TAG)
dialog.dismiss()
}
}
}

@ -0,0 +1,6 @@
package org.koitharu.kotatsu.core.exceptions.resolve
interface ResolvableException {
val resolveTextId: Int
}

@ -27,6 +27,7 @@ class ChipsFactory(val context: Context) {
chip.setChipIconResource(iconRes) chip.setChipIconResource(iconRes)
} }
chip.tag = tag chip.tag = tag
chip.setEnsureMinTouchTargetSize(false)
chip.setOnClickListener(onClickListener) chip.setOnClickListener(onClickListener)
return chip return chip
} }

@ -16,6 +16,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.base.ui.BaseFragment
@ -26,6 +27,7 @@ import org.koitharu.kotatsu.base.ui.list.decor.SectionItemDecoration
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.exceptions.resolve.ResolvableException
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaFilter import org.koitharu.kotatsu.core.model.MangaFilter
import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.ListMode
@ -38,6 +40,7 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.utils.ext.clearItemDecorations import org.koitharu.kotatsu.utils.ext.clearItemDecorations
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.toggleDrawer import org.koitharu.kotatsu.utils.ext.toggleDrawer
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
abstract class MangaListFragment : BaseFragment<FragmentListBinding>(), abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
PaginationScrollListener.Callback, OnListItemClickListener<Manga>, OnFilterChangedListener, PaginationScrollListener.Callback, OnListItemClickListener<Manga>, OnFilterChangedListener,
@ -64,9 +67,7 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding.drawer?.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) binding.drawer?.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
listAdapter = MangaListAdapter(get(), viewLifecycleOwner, this) { listAdapter = MangaListAdapter(get(), viewLifecycleOwner, this, ::resolveException)
viewModel.onRetry()
}
paginationListener = PaginationScrollListener(4, this) paginationListener = PaginationScrollListener(4, this)
with(binding.recyclerView) { with(binding.recyclerView) {
setHasFixedSize(true) setHasFixedSize(true)
@ -163,6 +164,18 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
} }
} }
private fun resolveException(e: Throwable) {
if (e is ResolvableException) {
viewLifecycleScope.launch {
if (exceptionResolver.resolve(e)) {
viewModel.onRetry()
}
}
} else {
viewModel.onRetry()
}
}
@CallSuper @CallSuper
protected open fun onLoadingStateChanged(isLoading: Boolean) { protected open fun onLoadingStateChanged(isLoading: Boolean) {
binding.swipeRefreshLayout.isEnabled = binding.swipeRefreshLayout.isEnabled =

@ -7,13 +7,13 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
fun errorFooterAD( fun errorFooterAD(
onRetryClick: () -> Unit onRetryClick: (Throwable) -> Unit
) = adapterDelegateViewBinding<ErrorFooter, ListModel, ItemErrorFooterBinding>( ) = adapterDelegateViewBinding<ErrorFooter, ListModel, ItemErrorFooterBinding>(
{ inflater, parent -> ItemErrorFooterBinding.inflate(inflater, parent, false) } { inflater, parent -> ItemErrorFooterBinding.inflate(inflater, parent, false) }
) { ) {
binding.root.setOnClickListener { binding.root.setOnClickListener {
onRetryClick() onRetryClick(item.exception)
} }
bind { bind {

@ -8,13 +8,13 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
fun errorStateListAD( fun errorStateListAD(
onRetryClick: () -> Unit onRetryClick: (Throwable) -> Unit
) = adapterDelegateViewBinding<ErrorState, ListModel, ItemErrorStateBinding>( ) = adapterDelegateViewBinding<ErrorState, ListModel, ItemErrorStateBinding>(
{ inflater, parent -> ItemErrorStateBinding.inflate(inflater, parent, false) } { inflater, parent -> ItemErrorStateBinding.inflate(inflater, parent, false) }
) { ) {
binding.buttonRetry.setOnClickListener { binding.buttonRetry.setOnClickListener {
onRetryClick() onRetryClick(item.exception)
} }
bind { bind {
@ -22,6 +22,9 @@ fun errorStateListAD(
text = item.exception.getDisplayMessage(context.resources) text = item.exception.getDisplayMessage(context.resources)
setCompoundDrawablesWithIntrinsicBounds(0, item.icon, 0, 0) setCompoundDrawablesWithIntrinsicBounds(0, item.icon, 0, 0)
} }
binding.buttonRetry.isVisible = item.canRetry with(binding.buttonRetry) {
isVisible = item.canRetry
setText(item.buttonText)
}
} }
} }

@ -16,7 +16,7 @@ class MangaListAdapter(
coil: ImageLoader, coil: ImageLoader,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener<Manga>, clickListener: OnListItemClickListener<Manga>,
onRetryClick: () -> Unit onRetryClick: (Throwable) -> Unit
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) { ) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
init { init {

@ -1,9 +1,11 @@
package org.koitharu.kotatsu.list.ui.model package org.koitharu.kotatsu.list.ui.model
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
data class ErrorState( data class ErrorState(
val exception: Throwable, val exception: Throwable,
@DrawableRes val icon: Int, @DrawableRes val icon: Int,
val canRetry: Boolean val canRetry: Boolean,
@StringRes val buttonText: Int
) : ListModel ) : ListModel

@ -1,6 +1,7 @@
package org.koitharu.kotatsu.list.ui.model package org.koitharu.kotatsu.list.ui.model
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ResolvableException
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.ListMode
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -46,7 +47,8 @@ fun <C : MutableCollection<ListModel>> List<Manga>.toUi(destination: C, mode: Li
fun Throwable.toErrorState(canRetry: Boolean = true) = ErrorState( fun Throwable.toErrorState(canRetry: Boolean = true) = ErrorState(
exception = this, exception = this,
icon = R.drawable.ic_error_large, icon = R.drawable.ic_error_large,
canRetry = canRetry canRetry = canRetry,
buttonText = (this as? ResolvableException)?.resolveTextId ?: R.string.try_again
) )
fun Throwable.toErrorFooter() = ErrorFooter( fun Throwable.toErrorFooter() = ErrorFooter(

@ -3,16 +3,18 @@ package org.koitharu.kotatsu.reader.ui.pager
import android.content.Context import android.content.Context
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.reader.ui.PageLoader import org.koitharu.kotatsu.reader.ui.PageLoader
abstract class BasePageHolder<B : ViewBinding>( abstract class BasePageHolder<B : ViewBinding>(
protected val binding: B, protected val binding: B,
loader: PageLoader, loader: PageLoader,
settings: AppSettings settings: AppSettings,
exceptionResolver: ExceptionResolver
) : RecyclerView.ViewHolder(binding.root), PageHolderDelegate.Callback { ) : RecyclerView.ViewHolder(binding.root), PageHolderDelegate.Callback {
protected val delegate = PageHolderDelegate(loader, settings, this) protected val delegate = PageHolderDelegate(loader, settings, this, exceptionResolver)
val context: Context val context: Context
get() = itemView.context get() = itemView.context

@ -4,6 +4,7 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.reader.ui.PageLoader import org.koitharu.kotatsu.reader.ui.PageLoader
import org.koitharu.kotatsu.utils.ext.resetTransformations import org.koitharu.kotatsu.utils.ext.resetTransformations
@ -12,7 +13,8 @@ import kotlin.coroutines.suspendCoroutine
abstract class BaseReaderAdapter<H : BasePageHolder<*>>( abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
private val loader: PageLoader, private val loader: PageLoader,
private val settings: AppSettings private val settings: AppSettings,
private val exceptionResolver: ExceptionResolver
) : RecyclerView.Adapter<H>() { ) : RecyclerView.Adapter<H>() {
private val differ = AsyncListDiffer(this, DiffCallback()) private val differ = AsyncListDiffer(this, DiffCallback())
@ -42,7 +44,7 @@ abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
final override fun onCreateViewHolder( final override fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
viewType: Int viewType: Int
): H = onCreateViewHolder(parent, loader, settings).also(this::onViewHolderCreated) ): H = onCreateViewHolder(parent, loader, settings, exceptionResolver)
fun setItems(items: List<ReaderPage>, callback: Runnable) { fun setItems(items: List<ReaderPage>, callback: Runnable) {
differ.submitList(items, callback) differ.submitList(items, callback)
@ -54,12 +56,11 @@ abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
} }
} }
protected open fun onViewHolderCreated(holder: H) = Unit
protected abstract fun onCreateViewHolder( protected abstract fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
loader: PageLoader, loader: PageLoader,
settings: AppSettings settings: AppSettings,
exceptionResolver: ExceptionResolver
): H ): H
private class DiffCallback : DiffUtil.ItemCallback<ReaderPage>() { private class DiffCallback : DiffUtil.ItemCallback<ReaderPage>() {

@ -4,6 +4,8 @@ import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.exceptions.resolve.ResolvableException
import org.koitharu.kotatsu.core.model.MangaPage import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
@ -16,39 +18,52 @@ import java.io.IOException
class PageHolderDelegate( class PageHolderDelegate(
private val loader: PageLoader, private val loader: PageLoader,
private val settings: AppSettings, private val settings: AppSettings,
private val callback: Callback private val callback: Callback,
private val exceptionResolver: ExceptionResolver
) : SubsamplingScaleImageView.DefaultOnImageEventListener(), CoroutineScope by loader { ) : SubsamplingScaleImageView.DefaultOnImageEventListener(), CoroutineScope by loader {
private var state = State.EMPTY private var state = State.EMPTY
private var job: Job? = null private var job: Job? = null
private var file: File? = null private var file: File? = null
private var error: Throwable? = null
fun onBind(page: MangaPage) { fun onBind(page: MangaPage) {
job = launchInstead(job) {
doLoad(page, force = false) doLoad(page, force = false)
} }
}
fun retry(page: MangaPage) { fun retry(page: MangaPage) {
job = launchInstead(job) {
(error as? ResolvableException)?.let {
exceptionResolver.resolve(it)
}
doLoad(page, force = true) doLoad(page, force = true)
} }
}
fun onRecycle() { fun onRecycle() {
state = State.EMPTY state = State.EMPTY
file = null file = null
error = null
job?.cancel() job?.cancel()
} }
override fun onReady() { override fun onReady() {
state = State.SHOWING state = State.SHOWING
error = null
callback.onImageShowing(settings.zoomMode) callback.onImageShowing(settings.zoomMode)
} }
override fun onImageLoaded() { override fun onImageLoaded() {
state = State.SHOWN state = State.SHOWN
error = null
callback.onImageShown() callback.onImageShown()
} }
override fun onImageLoadError(e: Exception) { override fun onImageLoadError(e: Exception) {
val file = this.file val file = this.file
error = e
if (state == State.LOADED && e is IOException && file != null && file.exists()) { if (state == State.LOADED && e is IOException && file != null && file.exists()) {
job = launchAfter(job) { job = launchAfter(job) {
state = State.CONVERTING state = State.CONVERTING
@ -68,9 +83,9 @@ class PageHolderDelegate(
} }
} }
private fun doLoad(data: MangaPage, force: Boolean) { private suspend fun doLoad(data: MangaPage, force: Boolean) {
job = launchInstead(job) {
state = State.LOADING state = State.LOADING
error = null
callback.onLoadingStarted() callback.onLoadingStarted()
try { try {
val file = withContext(Dispatchers.IO) { val file = withContext(Dispatchers.IO) {
@ -85,10 +100,10 @@ class PageHolderDelegate(
// do nothing // do nothing
} catch (e: Exception) { } catch (e: Exception) {
state = State.ERROR state = State.ERROR
error = e
callback.onError(e) callback.onError(e)
} }
} }
}
private enum class State { private enum class State {
EMPTY, LOADING, LOADED, CONVERTING, CONVERTED, SHOWING, SHOWN, ERROR EMPTY, LOADING, LOADED, CONVERTING, CONVERTED, SHOWING, SHOWN, ERROR

@ -2,6 +2,7 @@ package org.koitharu.kotatsu.reader.ui.pager.reversed
import android.graphics.PointF import android.graphics.PointF
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.ItemPageBinding import org.koitharu.kotatsu.databinding.ItemPageBinding
@ -11,8 +12,9 @@ import org.koitharu.kotatsu.reader.ui.pager.standard.PageHolder
class ReversedPageHolder( class ReversedPageHolder(
binding: ItemPageBinding, binding: ItemPageBinding,
loader: PageLoader, loader: PageLoader,
settings: AppSettings settings: AppSettings,
) : PageHolder(binding, loader, settings) { exceptionResolver: ExceptionResolver
) : PageHolder(binding, loader, settings, exceptionResolver) {
override fun onImageShowing(zoom: ZoomMode) { override fun onImageShowing(zoom: ZoomMode) {
with(binding.ssiv) { with(binding.ssiv) {

@ -2,6 +2,7 @@ package org.koitharu.kotatsu.reader.ui.pager.reversed
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.ItemPageBinding import org.koitharu.kotatsu.databinding.ItemPageBinding
import org.koitharu.kotatsu.reader.ui.PageLoader import org.koitharu.kotatsu.reader.ui.PageLoader
@ -9,16 +10,19 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
class ReversedPagesAdapter( class ReversedPagesAdapter(
loader: PageLoader, loader: PageLoader,
settings: AppSettings settings: AppSettings,
) : BaseReaderAdapter<ReversedPageHolder>(loader, settings) { exceptionResolver: ExceptionResolver
) : BaseReaderAdapter<ReversedPageHolder>(loader, settings, exceptionResolver) {
override fun onCreateViewHolder( override fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
loader: PageLoader, loader: PageLoader,
settings: AppSettings settings: AppSettings,
exceptionResolver: ExceptionResolver
) = ReversedPageHolder( ) = ReversedPageHolder(
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false), binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
loader = loader, loader = loader,
settings = settings settings = settings,
exceptionResolver = exceptionResolver
) )
} }

@ -25,10 +25,10 @@ class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
pagerAdapter = ReversedPagesAdapter(loader, get()) pagerAdapter = ReversedPagesAdapter(loader, get(), exceptionResolver)
with(binding.pager) { with(binding.pager) {
adapter = pagerAdapter adapter = pagerAdapter
offscreenPageLimit = 1 offscreenPageLimit = 2
doOnPageChanged(::notifyPageChanged) doOnPageChanged(::notifyPageChanged)
} }

@ -7,6 +7,8 @@ import androidx.core.view.isVisible
import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.exceptions.resolve.ResolvableException
import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.ItemPageBinding import org.koitharu.kotatsu.databinding.ItemPageBinding
@ -18,8 +20,9 @@ import org.koitharu.kotatsu.utils.ext.getDisplayMessage
open class PageHolder( open class PageHolder(
binding: ItemPageBinding, binding: ItemPageBinding,
loader: PageLoader, loader: PageLoader,
settings: AppSettings settings: AppSettings, exceptionResolver: ExceptionResolver
) : BasePageHolder<ItemPageBinding>(binding, loader, settings), View.OnClickListener { ) : BasePageHolder<ItemPageBinding>(binding, loader, settings, exceptionResolver),
View.OnClickListener {
init { init {
binding.ssiv.setOnImageEventListener(delegate) binding.ssiv.setOnImageEventListener(delegate)
@ -93,6 +96,9 @@ open class PageHolder(
override fun onError(e: Throwable) { override fun onError(e: Throwable) {
binding.textViewError.text = e.getDisplayMessage(context.resources) binding.textViewError.text = e.getDisplayMessage(context.resources)
binding.buttonRetry.setText(
(e as? ResolvableException)?.resolveTextId ?: R.string.try_again
)
binding.layoutError.isVisible = true binding.layoutError.isVisible = true
binding.progressBar.isVisible = false binding.progressBar.isVisible = false
} }

@ -25,10 +25,10 @@ class PagerReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
pagesAdapter = PagesAdapter(loader, get()) pagesAdapter = PagesAdapter(loader, get(), exceptionResolver)
with(binding.pager) { with(binding.pager) {
adapter = pagesAdapter adapter = pagesAdapter
offscreenPageLimit = 1 offscreenPageLimit = 2
doOnPageChanged(::notifyPageChanged) doOnPageChanged(::notifyPageChanged)
} }

@ -2,6 +2,7 @@ package org.koitharu.kotatsu.reader.ui.pager.standard
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.ItemPageBinding import org.koitharu.kotatsu.databinding.ItemPageBinding
import org.koitharu.kotatsu.reader.ui.PageLoader import org.koitharu.kotatsu.reader.ui.PageLoader
@ -9,16 +10,19 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
class PagesAdapter( class PagesAdapter(
loader: PageLoader, loader: PageLoader,
settings: AppSettings settings: AppSettings,
) : BaseReaderAdapter<PageHolder>(loader, settings) { exceptionResolver: ExceptionResolver
) : BaseReaderAdapter<PageHolder>(loader, settings, exceptionResolver) {
override fun onCreateViewHolder( override fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
loader: PageLoader, loader: PageLoader,
settings: AppSettings settings: AppSettings,
exceptionResolver: ExceptionResolver
) = PageHolder( ) = PageHolder(
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false), binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
loader = loader, loader = loader,
settings = settings settings = settings,
exceptionResolver = exceptionResolver
) )
} }

@ -2,6 +2,7 @@ package org.koitharu.kotatsu.reader.ui.pager.wetoon
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding
import org.koitharu.kotatsu.reader.ui.PageLoader import org.koitharu.kotatsu.reader.ui.PageLoader
@ -9,13 +10,15 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
class WebtoonAdapter( class WebtoonAdapter(
loader: PageLoader, loader: PageLoader,
settings: AppSettings settings: AppSettings,
) : BaseReaderAdapter<WebtoonHolder>(loader, settings) { exceptionResolver: ExceptionResolver
) : BaseReaderAdapter<WebtoonHolder>(loader, settings, exceptionResolver) {
override fun onCreateViewHolder( override fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
loader: PageLoader, loader: PageLoader,
settings: AppSettings settings: AppSettings,
exceptionResolver: ExceptionResolver
) = WebtoonHolder( ) = WebtoonHolder(
binding = ItemPageWebtoonBinding.inflate( binding = ItemPageWebtoonBinding.inflate(
LayoutInflater.from(parent.context), LayoutInflater.from(parent.context),
@ -23,6 +26,7 @@ class WebtoonAdapter(
false false
), ),
loader = loader, loader = loader,
settings = settings settings = settings,
exceptionResolver = exceptionResolver
) )
} }

@ -6,6 +6,8 @@ import androidx.core.view.isVisible
import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.exceptions.resolve.ResolvableException
import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding
@ -18,8 +20,10 @@ import org.koitharu.kotatsu.utils.ext.getDisplayMessage
class WebtoonHolder( class WebtoonHolder(
binding: ItemPageWebtoonBinding, binding: ItemPageWebtoonBinding,
loader: PageLoader, loader: PageLoader,
settings: AppSettings settings: AppSettings,
) : BasePageHolder<ItemPageWebtoonBinding>(binding, loader, settings), View.OnClickListener { exceptionResolver: ExceptionResolver
) : BasePageHolder<ItemPageWebtoonBinding>(binding, loader, settings, exceptionResolver),
View.OnClickListener {
private var scrollToRestore = 0 private var scrollToRestore = 0
@ -74,6 +78,9 @@ class WebtoonHolder(
override fun onError(e: Throwable) { override fun onError(e: Throwable) {
binding.textViewError.text = e.getDisplayMessage(context.resources) binding.textViewError.text = e.getDisplayMessage(context.resources)
binding.buttonRetry.setText(
(e as? ResolvableException)?.resolveTextId ?: R.string.try_again
)
binding.layoutError.isVisible = true binding.layoutError.isVisible = true
binding.progressBar.isVisible = false binding.progressBar.isVisible = false
} }

@ -26,7 +26,7 @@ class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
webtoonAdapter = WebtoonAdapter(loader, get()) webtoonAdapter = WebtoonAdapter(loader, get(), exceptionResolver)
with(binding.recyclerView) { with(binding.recyclerView) {
setHasFixedSize(true) setHasFixedSize(true)
adapter = webtoonAdapter adapter = webtoonAdapter

@ -6,12 +6,14 @@ import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.annotation.MainThread
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject import kotlinx.coroutines.withContext
import org.koin.android.ext.android.get
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.github.AppVersion import org.koitharu.kotatsu.core.github.AppVersion
@ -32,8 +34,8 @@ import java.util.concurrent.TimeUnit
class AppUpdateChecker(private val activity: ComponentActivity) { class AppUpdateChecker(private val activity: ComponentActivity) {
private val settings by activity.inject<AppSettings>() private val settings = activity.get<AppSettings>()
private val repo by activity.inject<GithubRepository>() private val repo = activity.get<GithubRepository>()
fun launchIfNeeded(): Job? { fun launchIfNeeded(): Job? {
return if (settings.appUpdateAuto && settings.appUpdate + PERIOD < System.currentTimeMillis()) { return if (settings.appUpdateAuto && settings.appUpdate + PERIOD < System.currentTimeMillis()) {
@ -52,21 +54,28 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
} }
suspend fun checkNow() = runCatching { suspend fun checkNow() = runCatching {
withContext(Dispatchers.Default) {
val version = repo.getLatestVersion() val version = repo.getLatestVersion()
val newVersionId = VersionId.parse(version.name) val newVersionId = VersionId.parse(version.name)
val currentVersionId = VersionId.parse(BuildConfig.VERSION_NAME) val currentVersionId = VersionId.parse(BuildConfig.VERSION_NAME)
val result = newVersionId > currentVersionId val result = newVersionId > currentVersionId
if (result) { if (result) {
withContext(Dispatchers.Main) {
showUpdateDialog(version) showUpdateDialog(version)
} }
}
settings.appUpdate = System.currentTimeMillis() settings.appUpdate = System.currentTimeMillis()
result result
}
}.onFailure {
it.printStackTrace()
}.getOrNull() }.getOrNull()
private fun launchInternal() = activity.lifecycleScope.launch(Dispatchers.Main) { private fun launchInternal() = activity.lifecycleScope.launch {
checkNow() checkNow()
} }
@MainThread
private fun showUpdateDialog(version: AppVersion) { private fun showUpdateDialog(version: AppVersion) {
AlertDialog.Builder(activity) AlertDialog.Builder(activity)
.setTitle(R.string.app_update_available) .setTitle(R.string.app_update_available)

@ -5,6 +5,7 @@ import android.util.Log
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
import org.koitharu.kotatsu.core.exceptions.WrongPasswordException import org.koitharu.kotatsu.core.exceptions.WrongPasswordException
@ -37,6 +38,7 @@ suspend inline fun <T, R> T.retryUntilSuccess(maxAttempts: Int, action: T.() ->
} }
fun Throwable.getDisplayMessage(resources: Resources) = when (this) { fun Throwable.getDisplayMessage(resources: Resources) = when (this) {
is CloudFlareProtectedException -> resources.getString(R.string.captcha_required)
is UnsupportedOperationException -> resources.getString(R.string.operation_not_supported) is UnsupportedOperationException -> resources.getString(R.string.operation_not_supported)
is UnsupportedFileException -> resources.getString(R.string.text_file_not_supported) is UnsupportedFileException -> resources.getString(R.string.text_file_not_supported)
is FileNotFoundException -> resources.getString(R.string.file_not_found) is FileNotFoundException -> resources.getString(R.string.file_not_found)

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.utils.ext package org.koitharu.kotatsu.utils.ext
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.* import kotlinx.coroutines.*
@ -16,27 +17,27 @@ import kotlin.coroutines.resumeWithException
suspend fun Call.await() = suspendCancellableCoroutine<Response> { cont -> suspend fun Call.await() = suspendCancellableCoroutine<Response> { cont ->
this.enqueue(object : Callback { this.enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) { override fun onFailure(call: Call, e: IOException) {
if (!cont.isCancelled) { if (cont.isActive) {
cont.resumeWithException(e) cont.resumeWithException(e)
} }
} }
override fun onResponse(call: Call, response: Response) { override fun onResponse(call: Call, response: Response) {
if (cont.isActive) {
cont.resume(response) cont.resume(response)
} }
}
}) })
cont.invokeOnCancellation { cont.invokeOnCancellation {
safe {
this.cancel() this.cancel()
} }
}
} }
fun CoroutineScope.launchAfter( inline fun CoroutineScope.launchAfter(
job: Job?, job: Job?,
context: CoroutineContext = EmptyCoroutineContext, context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT, start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit crossinline block: suspend CoroutineScope.() -> Unit
): Job = launch(context, start) { ): Job = launch(context, start) {
try { try {
job?.join() job?.join()
@ -48,11 +49,11 @@ fun CoroutineScope.launchAfter(
block() block()
} }
fun CoroutineScope.launchInstead( inline fun CoroutineScope.launchInstead(
job: Job?, job: Job?,
context: CoroutineContext = EmptyCoroutineContext, context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT, start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit crossinline block: suspend CoroutineScope.() -> Unit
): Job = launch(context, start) { ): Job = launch(context, start) {
try { try {
job?.cancelAndJoin() job?.cancelAndJoin()
@ -71,5 +72,5 @@ val IgnoreErrors
} }
} }
val processLifecycleScope: CoroutineScope val processLifecycleScope: LifecycleCoroutineScope
inline get() = ProcessLifecycleOwner.get().lifecycleScope inline get() = ProcessLifecycleOwner.get().lifecycleScope

@ -10,13 +10,15 @@ data class Progress(
) : Parcelable, Comparable<Progress> { ) : Parcelable, Comparable<Progress> {
override fun compareTo(other: Progress): Int { override fun compareTo(other: Progress): Int {
if (this.total == other.total) { return if (this.total == other.total) {
return this.value.compareTo(other.value) this.value.compareTo(other.value)
} else { } else {
TODO() this.part().compareTo(other.part())
} }
} }
val isIndeterminate: Boolean val isIndeterminate: Boolean
get() = total <= 0 get() = total <= 0
private fun part() = if (isIndeterminate) -1.0 else value / total.toDouble()
} }

@ -119,6 +119,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:padding="6dp" android:padding="6dp"
app:chipSpacingHorizontal="4dp"
app:chipSpacingVertical="6dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider_top" /> app:layout_constraintTop_toBottomOf="@id/divider_top" />

@ -5,7 +5,7 @@
<string name="favourites">Избранное</string> <string name="favourites">Избранное</string>
<string name="history">История</string> <string name="history">История</string>
<string name="error_occurred">Произошла ошибка</string> <string name="error_occurred">Произошла ошибка</string>
<string name="network_error">Ошибка подключения</string> <string name="network_error">Ошибка сети</string>
<string name="details">Подробности</string> <string name="details">Подробности</string>
<string name="chapters">Главы</string> <string name="chapters">Главы</string>
<string name="list">Список</string> <string name="list">Список</string>
@ -188,4 +188,6 @@
<string name="tap_to_try_again">Попробовать ещё раз</string> <string name="tap_to_try_again">Попробовать ещё раз</string>
<string name="reader_mode_hint">Выбранный режим будет сохранён для текущей манги</string> <string name="reader_mode_hint">Выбранный режим будет сохранён для текущей манги</string>
<string name="silent">Без звука</string> <string name="silent">Без звука</string>
<string name="captcha_required">Необходимо пройти CAPTCHA</string>
<string name="resolve">Resolve</string>
</resources> </resources>

@ -190,4 +190,6 @@
<string name="tap_to_try_again">Tap to try again</string> <string name="tap_to_try_again">Tap to try again</string>
<string name="reader_mode_hint">Chosen configuration will be remembered for this manga</string> <string name="reader_mode_hint">Chosen configuration will be remembered for this manga</string>
<string name="silent">Silent</string> <string name="silent">Silent</string>
<string name="captcha_required">CAPTCHA is required</string>
<string name="resolve">Resolve</string>
</resources> </resources>
Loading…
Cancel
Save