This commit is contained in:
Koitharu
2020-12-22 07:48:34 +02:00
parent 9a0b7c4700
commit 7fd71c13f3
36 changed files with 274 additions and 113 deletions

View File

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

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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?,

View File

@@ -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 }) {

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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()
}
}
}

View File

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

View File

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

View File

@@ -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 =

View File

@@ -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 {

View File

@@ -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)
}
}
}

View File

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

View File

@@ -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

View File

@@ -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(

View File

@@ -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

View File

@@ -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>() {

View File

@@ -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)
}
}

View File

@@ -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) {

View File

@@ -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
)
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
)
}

View File

@@ -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
)
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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()
}

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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>