Fixes
This commit is contained in:
3
app/src/debug/res/values/strings.xml
Normal file
3
app/src/debug/res/values/strings.xml
Normal file
@@ -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
|
||||
|
||||
protected fun bindingOrNull(): B? = viewBinding
|
||||
|
||||
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.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
|
||||
abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(), OnApplyWindowInsetsListener {
|
||||
@@ -23,6 +24,11 @@ abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(), OnApplyWindo
|
||||
protected lateinit var binding: B
|
||||
private set
|
||||
|
||||
|
||||
protected val exceptionResolver by lazy(LazyThreadSafetyMode.NONE) {
|
||||
ExceptionResolver(this, supportFragmentManager)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
if (get<AppSettings>().isAmoledTheme) {
|
||||
setTheme(R.style.AppTheme_Amoled)
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
|
||||
abstract class BaseFragment<B : ViewBinding> : Fragment(), OnApplyWindowInsetsListener {
|
||||
|
||||
@@ -19,6 +20,10 @@ abstract class BaseFragment<B : ViewBinding> : Fragment(), OnApplyWindowInsetsLi
|
||||
protected val binding: B
|
||||
get() = checkNotNull(viewBinding)
|
||||
|
||||
protected val exceptionResolver by lazy(LazyThreadSafetyMode.NONE) {
|
||||
ExceptionResolver(viewLifecycleOwner, childFragmentManager)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
|
||||
@@ -7,15 +7,13 @@ import android.webkit.WebViewClient
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
|
||||
class CloudFlareClient(
|
||||
private val cookieJar: CookieJar,
|
||||
private val callback: CloudFlareCallback,
|
||||
private val targetUrl: String
|
||||
) : WebViewClient(), KoinComponent {
|
||||
) : WebViewClient() {
|
||||
|
||||
private val cookieJar = get<CookieJar>()
|
||||
private val cookieManager = CookieManager.getInstance()
|
||||
|
||||
init {
|
||||
@@ -39,7 +37,8 @@ class CloudFlareClient(
|
||||
|
||||
private fun checkClearance() {
|
||||
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)
|
||||
}
|
||||
if (cookies.none { it.name == CF_CLEARANCE }) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.koitharu.kotatsu.browser.cloudflare
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -9,7 +10,8 @@ import android.webkit.CookieManager
|
||||
import android.webkit.WebSettings
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
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.core.network.UserAgentInterceptor
|
||||
import org.koitharu.kotatsu.databinding.FragmentCloudflareBinding
|
||||
@@ -19,6 +21,7 @@ import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), CloudFlareCallback {
|
||||
|
||||
private val url by stringArgument(ARG_URL)
|
||||
private val pendingResult = Bundle(1)
|
||||
|
||||
override fun onInflateView(
|
||||
inflater: LayoutInflater,
|
||||
@@ -35,7 +38,7 @@ class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), Cloud
|
||||
databaseEnabled = true
|
||||
userAgentString = UserAgentInterceptor.userAgent
|
||||
}
|
||||
binding.webView.webViewClient = CloudFlareClient(this, url.orEmpty())
|
||||
binding.webView.webViewClient = CloudFlareClient(get(), this, url.orEmpty())
|
||||
CookieManager.getInstance().setAcceptThirdPartyCookies(binding.webView, true)
|
||||
if (url.isNullOrEmpty()) {
|
||||
dismissAllowingStateLoss()
|
||||
@@ -63,18 +66,24 @@ class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), Cloud
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
setFragmentResult(TAG, pendingResult)
|
||||
super.onDismiss(dialog)
|
||||
}
|
||||
|
||||
override fun onPageLoaded() {
|
||||
binding.progressBar.isInvisible = true
|
||||
bindingOrNull()?.progressBar?.isInvisible = true
|
||||
}
|
||||
|
||||
override fun onCheckPassed() {
|
||||
((parentFragment ?: activity) as? SwipeRefreshLayout.OnRefreshListener)?.onRefresh()
|
||||
pendingResult.putBoolean(EXTRA_RESULT, true)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val TAG = "CloudFlareDialog"
|
||||
const val EXTRA_RESULT = "result"
|
||||
private const val ARG_URL = "url"
|
||||
|
||||
fun newInstance(url: String) = CloudFlareDialog().withArgs(1) {
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
package org.koitharu.kotatsu.core.exceptions
|
||||
|
||||
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.tag = tag
|
||||
chip.setEnsureMinTouchTargetSize(false)
|
||||
chip.setOnClickListener(onClickListener)
|
||||
return chip
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koitharu.kotatsu.R
|
||||
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.browser.cloudflare.CloudFlareDialog
|
||||
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.MangaFilter
|
||||
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.getDisplayMessage
|
||||
import org.koitharu.kotatsu.utils.ext.toggleDrawer
|
||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||
|
||||
abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
|
||||
PaginationScrollListener.Callback, OnListItemClickListener<Manga>, OnFilterChangedListener,
|
||||
@@ -64,9 +67,7 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.drawer?.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
|
||||
listAdapter = MangaListAdapter(get(), viewLifecycleOwner, this) {
|
||||
viewModel.onRetry()
|
||||
}
|
||||
listAdapter = MangaListAdapter(get(), viewLifecycleOwner, this, ::resolveException)
|
||||
paginationListener = PaginationScrollListener(4, this)
|
||||
with(binding.recyclerView) {
|
||||
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
|
||||
protected open fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
binding.swipeRefreshLayout.isEnabled =
|
||||
|
||||
@@ -7,13 +7,13 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
|
||||
fun errorFooterAD(
|
||||
onRetryClick: () -> Unit
|
||||
onRetryClick: (Throwable) -> Unit
|
||||
) = adapterDelegateViewBinding<ErrorFooter, ListModel, ItemErrorFooterBinding>(
|
||||
{ inflater, parent -> ItemErrorFooterBinding.inflate(inflater, parent, false) }
|
||||
) {
|
||||
|
||||
binding.root.setOnClickListener {
|
||||
onRetryClick()
|
||||
onRetryClick(item.exception)
|
||||
}
|
||||
|
||||
bind {
|
||||
|
||||
@@ -8,13 +8,13 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
|
||||
fun errorStateListAD(
|
||||
onRetryClick: () -> Unit
|
||||
onRetryClick: (Throwable) -> Unit
|
||||
) = adapterDelegateViewBinding<ErrorState, ListModel, ItemErrorStateBinding>(
|
||||
{ inflater, parent -> ItemErrorStateBinding.inflate(inflater, parent, false) }
|
||||
) {
|
||||
|
||||
binding.buttonRetry.setOnClickListener {
|
||||
onRetryClick()
|
||||
onRetryClick(item.exception)
|
||||
}
|
||||
|
||||
bind {
|
||||
@@ -22,6 +22,9 @@ fun errorStateListAD(
|
||||
text = item.exception.getDisplayMessage(context.resources)
|
||||
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,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
clickListener: OnListItemClickListener<Manga>,
|
||||
onRetryClick: () -> Unit
|
||||
onRetryClick: (Throwable) -> Unit
|
||||
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
|
||||
|
||||
init {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package org.koitharu.kotatsu.list.ui.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
data class ErrorState(
|
||||
val exception: Throwable,
|
||||
@DrawableRes val icon: Int,
|
||||
val canRetry: Boolean
|
||||
val canRetry: Boolean,
|
||||
@StringRes val buttonText: Int
|
||||
) : ListModel
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.koitharu.kotatsu.list.ui.model
|
||||
|
||||
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.prefs.ListMode
|
||||
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(
|
||||
exception = this,
|
||||
icon = R.drawable.ic_error_large,
|
||||
canRetry = canRetry
|
||||
canRetry = canRetry,
|
||||
buttonText = (this as? ResolvableException)?.resolveTextId ?: R.string.try_again
|
||||
)
|
||||
|
||||
fun Throwable.toErrorFooter() = ErrorFooter(
|
||||
|
||||
@@ -3,16 +3,18 @@ package org.koitharu.kotatsu.reader.ui.pager
|
||||
import android.content.Context
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.reader.ui.PageLoader
|
||||
|
||||
abstract class BasePageHolder<B : ViewBinding>(
|
||||
protected val binding: B,
|
||||
loader: PageLoader,
|
||||
settings: AppSettings
|
||||
settings: AppSettings,
|
||||
exceptionResolver: ExceptionResolver
|
||||
) : RecyclerView.ViewHolder(binding.root), PageHolderDelegate.Callback {
|
||||
|
||||
protected val delegate = PageHolderDelegate(loader, settings, this)
|
||||
protected val delegate = PageHolderDelegate(loader, settings, this, exceptionResolver)
|
||||
|
||||
val context: Context
|
||||
get() = itemView.context
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.AsyncListDiffer
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.reader.ui.PageLoader
|
||||
import org.koitharu.kotatsu.utils.ext.resetTransformations
|
||||
@@ -12,7 +13,8 @@ import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
|
||||
private val loader: PageLoader,
|
||||
private val settings: AppSettings
|
||||
private val settings: AppSettings,
|
||||
private val exceptionResolver: ExceptionResolver
|
||||
) : RecyclerView.Adapter<H>() {
|
||||
|
||||
private val differ = AsyncListDiffer(this, DiffCallback())
|
||||
@@ -42,7 +44,7 @@ abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
|
||||
final override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): H = onCreateViewHolder(parent, loader, settings).also(this::onViewHolderCreated)
|
||||
): H = onCreateViewHolder(parent, loader, settings, exceptionResolver)
|
||||
|
||||
fun setItems(items: List<ReaderPage>, callback: Runnable) {
|
||||
differ.submitList(items, callback)
|
||||
@@ -54,12 +56,11 @@ abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun onViewHolderCreated(holder: H) = Unit
|
||||
|
||||
protected abstract fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
loader: PageLoader,
|
||||
settings: AppSettings
|
||||
settings: AppSettings,
|
||||
exceptionResolver: ExceptionResolver
|
||||
): H
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<ReaderPage>() {
|
||||
|
||||
@@ -4,6 +4,8 @@ import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
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.ZoomMode
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
@@ -16,39 +18,52 @@ import java.io.IOException
|
||||
class PageHolderDelegate(
|
||||
private val loader: PageLoader,
|
||||
private val settings: AppSettings,
|
||||
private val callback: Callback
|
||||
private val callback: Callback,
|
||||
private val exceptionResolver: ExceptionResolver
|
||||
) : SubsamplingScaleImageView.DefaultOnImageEventListener(), CoroutineScope by loader {
|
||||
|
||||
private var state = State.EMPTY
|
||||
private var job: Job? = null
|
||||
private var file: File? = null
|
||||
private var error: Throwable? = null
|
||||
|
||||
fun onBind(page: MangaPage) {
|
||||
doLoad(page, force = false)
|
||||
job = launchInstead(job) {
|
||||
doLoad(page, force = false)
|
||||
}
|
||||
}
|
||||
|
||||
fun retry(page: MangaPage) {
|
||||
doLoad(page, force = true)
|
||||
job = launchInstead(job) {
|
||||
(error as? ResolvableException)?.let {
|
||||
exceptionResolver.resolve(it)
|
||||
}
|
||||
doLoad(page, force = true)
|
||||
}
|
||||
}
|
||||
|
||||
fun onRecycle() {
|
||||
state = State.EMPTY
|
||||
file = null
|
||||
error = null
|
||||
job?.cancel()
|
||||
}
|
||||
|
||||
override fun onReady() {
|
||||
state = State.SHOWING
|
||||
error = null
|
||||
callback.onImageShowing(settings.zoomMode)
|
||||
}
|
||||
|
||||
override fun onImageLoaded() {
|
||||
state = State.SHOWN
|
||||
error = null
|
||||
callback.onImageShown()
|
||||
}
|
||||
|
||||
override fun onImageLoadError(e: Exception) {
|
||||
val file = this.file
|
||||
error = e
|
||||
if (state == State.LOADED && e is IOException && file != null && file.exists()) {
|
||||
job = launchAfter(job) {
|
||||
state = State.CONVERTING
|
||||
@@ -68,25 +83,25 @@ class PageHolderDelegate(
|
||||
}
|
||||
}
|
||||
|
||||
private fun doLoad(data: MangaPage, force: Boolean) {
|
||||
job = launchInstead(job) {
|
||||
state = State.LOADING
|
||||
callback.onLoadingStarted()
|
||||
try {
|
||||
val file = withContext(Dispatchers.IO) {
|
||||
val pageUrl = data.source.repository.getPageFullUrl(data)
|
||||
check(pageUrl.isNotEmpty()) { "Cannot obtain full image url" }
|
||||
loader.loadFile(pageUrl, force)
|
||||
}
|
||||
this@PageHolderDelegate.file = file
|
||||
state = State.LOADED
|
||||
callback.onImageReady(file.toUri())
|
||||
} catch (e: CancellationException) {
|
||||
// do nothing
|
||||
} catch (e: Exception) {
|
||||
state = State.ERROR
|
||||
callback.onError(e)
|
||||
private suspend fun doLoad(data: MangaPage, force: Boolean) {
|
||||
state = State.LOADING
|
||||
error = null
|
||||
callback.onLoadingStarted()
|
||||
try {
|
||||
val file = withContext(Dispatchers.IO) {
|
||||
val pageUrl = data.source.repository.getPageFullUrl(data)
|
||||
check(pageUrl.isNotEmpty()) { "Cannot obtain full image url" }
|
||||
loader.loadFile(pageUrl, force)
|
||||
}
|
||||
this@PageHolderDelegate.file = file
|
||||
state = State.LOADED
|
||||
callback.onImageReady(file.toUri())
|
||||
} catch (e: CancellationException) {
|
||||
// do nothing
|
||||
} catch (e: Exception) {
|
||||
state = State.ERROR
|
||||
error = e
|
||||
callback.onError(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.reader.ui.pager.reversed
|
||||
|
||||
import android.graphics.PointF
|
||||
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.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
||||
@@ -11,8 +12,9 @@ import org.koitharu.kotatsu.reader.ui.pager.standard.PageHolder
|
||||
class ReversedPageHolder(
|
||||
binding: ItemPageBinding,
|
||||
loader: PageLoader,
|
||||
settings: AppSettings
|
||||
) : PageHolder(binding, loader, settings) {
|
||||
settings: AppSettings,
|
||||
exceptionResolver: ExceptionResolver
|
||||
) : PageHolder(binding, loader, settings, exceptionResolver) {
|
||||
|
||||
override fun onImageShowing(zoom: ZoomMode) {
|
||||
with(binding.ssiv) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.reader.ui.pager.reversed
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
||||
import org.koitharu.kotatsu.reader.ui.PageLoader
|
||||
@@ -9,16 +10,19 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
||||
|
||||
class ReversedPagesAdapter(
|
||||
loader: PageLoader,
|
||||
settings: AppSettings
|
||||
) : BaseReaderAdapter<ReversedPageHolder>(loader, settings) {
|
||||
settings: AppSettings,
|
||||
exceptionResolver: ExceptionResolver
|
||||
) : BaseReaderAdapter<ReversedPageHolder>(loader, settings, exceptionResolver) {
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
loader: PageLoader,
|
||||
settings: AppSettings
|
||||
settings: AppSettings,
|
||||
exceptionResolver: ExceptionResolver
|
||||
) = ReversedPageHolder(
|
||||
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||
loader = loader,
|
||||
settings = settings
|
||||
settings = settings,
|
||||
exceptionResolver = exceptionResolver
|
||||
)
|
||||
}
|
||||
@@ -25,10 +25,10 @@ class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
pagerAdapter = ReversedPagesAdapter(loader, get())
|
||||
pagerAdapter = ReversedPagesAdapter(loader, get(), exceptionResolver)
|
||||
with(binding.pager) {
|
||||
adapter = pagerAdapter
|
||||
offscreenPageLimit = 1
|
||||
offscreenPageLimit = 2
|
||||
doOnPageChanged(::notifyPageChanged)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import androidx.core.view.isVisible
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
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.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
||||
@@ -18,8 +20,9 @@ import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
open class PageHolder(
|
||||
binding: ItemPageBinding,
|
||||
loader: PageLoader,
|
||||
settings: AppSettings
|
||||
) : BasePageHolder<ItemPageBinding>(binding, loader, settings), View.OnClickListener {
|
||||
settings: AppSettings, exceptionResolver: ExceptionResolver
|
||||
) : BasePageHolder<ItemPageBinding>(binding, loader, settings, exceptionResolver),
|
||||
View.OnClickListener {
|
||||
|
||||
init {
|
||||
binding.ssiv.setOnImageEventListener(delegate)
|
||||
@@ -93,6 +96,9 @@ open class PageHolder(
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
binding.textViewError.text = e.getDisplayMessage(context.resources)
|
||||
binding.buttonRetry.setText(
|
||||
(e as? ResolvableException)?.resolveTextId ?: R.string.try_again
|
||||
)
|
||||
binding.layoutError.isVisible = true
|
||||
binding.progressBar.isVisible = false
|
||||
}
|
||||
|
||||
@@ -25,10 +25,10 @@ class PagerReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
pagesAdapter = PagesAdapter(loader, get())
|
||||
pagesAdapter = PagesAdapter(loader, get(), exceptionResolver)
|
||||
with(binding.pager) {
|
||||
adapter = pagesAdapter
|
||||
offscreenPageLimit = 1
|
||||
offscreenPageLimit = 2
|
||||
doOnPageChanged(::notifyPageChanged)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.reader.ui.pager.standard
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
||||
import org.koitharu.kotatsu.reader.ui.PageLoader
|
||||
@@ -9,16 +10,19 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
||||
|
||||
class PagesAdapter(
|
||||
loader: PageLoader,
|
||||
settings: AppSettings
|
||||
) : BaseReaderAdapter<PageHolder>(loader, settings) {
|
||||
settings: AppSettings,
|
||||
exceptionResolver: ExceptionResolver
|
||||
) : BaseReaderAdapter<PageHolder>(loader, settings, exceptionResolver) {
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
loader: PageLoader,
|
||||
settings: AppSettings
|
||||
settings: AppSettings,
|
||||
exceptionResolver: ExceptionResolver
|
||||
) = PageHolder(
|
||||
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||
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.ViewGroup
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding
|
||||
import org.koitharu.kotatsu.reader.ui.PageLoader
|
||||
@@ -9,13 +10,15 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
||||
|
||||
class WebtoonAdapter(
|
||||
loader: PageLoader,
|
||||
settings: AppSettings
|
||||
) : BaseReaderAdapter<WebtoonHolder>(loader, settings) {
|
||||
settings: AppSettings,
|
||||
exceptionResolver: ExceptionResolver
|
||||
) : BaseReaderAdapter<WebtoonHolder>(loader, settings, exceptionResolver) {
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
loader: PageLoader,
|
||||
settings: AppSettings
|
||||
settings: AppSettings,
|
||||
exceptionResolver: ExceptionResolver
|
||||
) = WebtoonHolder(
|
||||
binding = ItemPageWebtoonBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
@@ -23,6 +26,7 @@ class WebtoonAdapter(
|
||||
false
|
||||
),
|
||||
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.SubsamplingScaleImageView
|
||||
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.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding
|
||||
@@ -18,8 +20,10 @@ import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
class WebtoonHolder(
|
||||
binding: ItemPageWebtoonBinding,
|
||||
loader: PageLoader,
|
||||
settings: AppSettings
|
||||
) : BasePageHolder<ItemPageWebtoonBinding>(binding, loader, settings), View.OnClickListener {
|
||||
settings: AppSettings,
|
||||
exceptionResolver: ExceptionResolver
|
||||
) : BasePageHolder<ItemPageWebtoonBinding>(binding, loader, settings, exceptionResolver),
|
||||
View.OnClickListener {
|
||||
|
||||
private var scrollToRestore = 0
|
||||
|
||||
@@ -74,6 +78,9 @@ class WebtoonHolder(
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
binding.textViewError.text = e.getDisplayMessage(context.resources)
|
||||
binding.buttonRetry.setText(
|
||||
(e as? ResolvableException)?.resolveTextId ?: R.string.try_again
|
||||
)
|
||||
binding.layoutError.isVisible = true
|
||||
binding.progressBar.isVisible = false
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
webtoonAdapter = WebtoonAdapter(loader, get())
|
||||
webtoonAdapter = WebtoonAdapter(loader, get(), exceptionResolver)
|
||||
with(binding.recyclerView) {
|
||||
setHasFixedSize(true)
|
||||
adapter = webtoonAdapter
|
||||
|
||||
@@ -6,12 +6,14 @@ import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
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.R
|
||||
import org.koitharu.kotatsu.core.github.AppVersion
|
||||
@@ -32,8 +34,8 @@ import java.util.concurrent.TimeUnit
|
||||
|
||||
class AppUpdateChecker(private val activity: ComponentActivity) {
|
||||
|
||||
private val settings by activity.inject<AppSettings>()
|
||||
private val repo by activity.inject<GithubRepository>()
|
||||
private val settings = activity.get<AppSettings>()
|
||||
private val repo = activity.get<GithubRepository>()
|
||||
|
||||
fun launchIfNeeded(): Job? {
|
||||
return if (settings.appUpdateAuto && settings.appUpdate + PERIOD < System.currentTimeMillis()) {
|
||||
@@ -52,21 +54,28 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
|
||||
}
|
||||
|
||||
suspend fun checkNow() = runCatching {
|
||||
val version = repo.getLatestVersion()
|
||||
val newVersionId = VersionId.parse(version.name)
|
||||
val currentVersionId = VersionId.parse(BuildConfig.VERSION_NAME)
|
||||
val result = newVersionId > currentVersionId
|
||||
if (result) {
|
||||
showUpdateDialog(version)
|
||||
withContext(Dispatchers.Default) {
|
||||
val version = repo.getLatestVersion()
|
||||
val newVersionId = VersionId.parse(version.name)
|
||||
val currentVersionId = VersionId.parse(BuildConfig.VERSION_NAME)
|
||||
val result = newVersionId > currentVersionId
|
||||
if (result) {
|
||||
withContext(Dispatchers.Main) {
|
||||
showUpdateDialog(version)
|
||||
}
|
||||
}
|
||||
settings.appUpdate = System.currentTimeMillis()
|
||||
result
|
||||
}
|
||||
settings.appUpdate = System.currentTimeMillis()
|
||||
result
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}.getOrNull()
|
||||
|
||||
private fun launchInternal() = activity.lifecycleScope.launch(Dispatchers.Main) {
|
||||
private fun launchInternal() = activity.lifecycleScope.launch {
|
||||
checkNow()
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun showUpdateDialog(version: AppVersion) {
|
||||
AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.app_update_available)
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.util.Log
|
||||
import kotlinx.coroutines.delay
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
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.UnsupportedFileException
|
||||
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) {
|
||||
is CloudFlareProtectedException -> resources.getString(R.string.captcha_required)
|
||||
is UnsupportedOperationException -> resources.getString(R.string.operation_not_supported)
|
||||
is UnsupportedFileException -> resources.getString(R.string.text_file_not_supported)
|
||||
is FileNotFoundException -> resources.getString(R.string.file_not_found)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.utils.ext
|
||||
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.*
|
||||
@@ -16,27 +17,27 @@ import kotlin.coroutines.resumeWithException
|
||||
suspend fun Call.await() = suspendCancellableCoroutine<Response> { cont ->
|
||||
this.enqueue(object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
if (!cont.isCancelled) {
|
||||
if (cont.isActive) {
|
||||
cont.resumeWithException(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
cont.resume(response)
|
||||
if (cont.isActive) {
|
||||
cont.resume(response)
|
||||
}
|
||||
}
|
||||
})
|
||||
cont.invokeOnCancellation {
|
||||
safe {
|
||||
this.cancel()
|
||||
}
|
||||
this.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
fun CoroutineScope.launchAfter(
|
||||
inline fun CoroutineScope.launchAfter(
|
||||
job: Job?,
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
block: suspend CoroutineScope.() -> Unit
|
||||
crossinline block: suspend CoroutineScope.() -> Unit
|
||||
): Job = launch(context, start) {
|
||||
try {
|
||||
job?.join()
|
||||
@@ -48,11 +49,11 @@ fun CoroutineScope.launchAfter(
|
||||
block()
|
||||
}
|
||||
|
||||
fun CoroutineScope.launchInstead(
|
||||
inline fun CoroutineScope.launchInstead(
|
||||
job: Job?,
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
block: suspend CoroutineScope.() -> Unit
|
||||
crossinline block: suspend CoroutineScope.() -> Unit
|
||||
): Job = launch(context, start) {
|
||||
try {
|
||||
job?.cancelAndJoin()
|
||||
@@ -71,5 +72,5 @@ val IgnoreErrors
|
||||
}
|
||||
}
|
||||
|
||||
val processLifecycleScope: CoroutineScope
|
||||
val processLifecycleScope: LifecycleCoroutineScope
|
||||
inline get() = ProcessLifecycleOwner.get().lifecycleScope
|
||||
@@ -10,13 +10,15 @@ data class Progress(
|
||||
) : Parcelable, Comparable<Progress> {
|
||||
|
||||
override fun compareTo(other: Progress): Int {
|
||||
if (this.total == other.total) {
|
||||
return this.value.compareTo(other.value)
|
||||
return if (this.total == other.total) {
|
||||
this.value.compareTo(other.value)
|
||||
} else {
|
||||
TODO()
|
||||
this.part().compareTo(other.part())
|
||||
}
|
||||
}
|
||||
|
||||
val isIndeterminate: Boolean
|
||||
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_marginTop="4dp"
|
||||
android:padding="6dp"
|
||||
app:chipSpacingHorizontal="4dp"
|
||||
app:chipSpacingVertical="6dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/divider_top" />
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<string name="favourites">Избранное</string>
|
||||
<string name="history">История</string>
|
||||
<string name="error_occurred">Произошла ошибка</string>
|
||||
<string name="network_error">Ошибка подключения</string>
|
||||
<string name="network_error">Ошибка сети</string>
|
||||
<string name="details">Подробности</string>
|
||||
<string name="chapters">Главы</string>
|
||||
<string name="list">Список</string>
|
||||
@@ -188,4 +188,6 @@
|
||||
<string name="tap_to_try_again">Попробовать ещё раз</string>
|
||||
<string name="reader_mode_hint">Выбранный режим будет сохранён для текущей манги</string>
|
||||
<string name="silent">Без звука</string>
|
||||
<string name="captcha_required">Необходимо пройти CAPTCHA</string>
|
||||
<string name="resolve">Resolve</string>
|
||||
</resources>
|
||||
@@ -190,4 +190,6 @@
|
||||
<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="silent">Silent</string>
|
||||
<string name="captcha_required">CAPTCHA is required</string>
|
||||
<string name="resolve">Resolve</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user