Fixes
This commit is contained in:
@@ -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'
|
||||||
}
|
}
|
||||||
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
|
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) {
|
||||||
doLoad(page, force = false)
|
job = launchInstead(job) {
|
||||||
|
doLoad(page, force = false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun retry(page: MangaPage) {
|
fun retry(page: MangaPage) {
|
||||||
doLoad(page, force = true)
|
job = launchInstead(job) {
|
||||||
|
(error as? ResolvableException)?.let {
|
||||||
|
exceptionResolver.resolve(it)
|
||||||
|
}
|
||||||
|
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,25 +83,25 @@ 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) {
|
||||||
val pageUrl = data.source.repository.getPageFullUrl(data)
|
val pageUrl = data.source.repository.getPageFullUrl(data)
|
||||||
check(pageUrl.isNotEmpty()) { "Cannot obtain full image url" }
|
check(pageUrl.isNotEmpty()) { "Cannot obtain full image url" }
|
||||||
loader.loadFile(pageUrl, force)
|
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)
|
|
||||||
}
|
}
|
||||||
|
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 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 {
|
||||||
val version = repo.getLatestVersion()
|
withContext(Dispatchers.Default) {
|
||||||
val newVersionId = VersionId.parse(version.name)
|
val version = repo.getLatestVersion()
|
||||||
val currentVersionId = VersionId.parse(BuildConfig.VERSION_NAME)
|
val newVersionId = VersionId.parse(version.name)
|
||||||
val result = newVersionId > currentVersionId
|
val currentVersionId = VersionId.parse(BuildConfig.VERSION_NAME)
|
||||||
if (result) {
|
val result = newVersionId > currentVersionId
|
||||||
showUpdateDialog(version)
|
if (result) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
showUpdateDialog(version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
settings.appUpdate = System.currentTimeMillis()
|
||||||
|
result
|
||||||
}
|
}
|
||||||
settings.appUpdate = System.currentTimeMillis()
|
}.onFailure {
|
||||||
result
|
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) {
|
||||||
cont.resume(response)
|
if (cont.isActive) {
|
||||||
|
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>
|
||||||
Reference in New Issue
Block a user