Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
313c2ab2bf | ||
|
|
fe5d37f45e | ||
|
|
92f6221ba0 | ||
|
|
0590a0c56f | ||
|
|
13ffc3a515 | ||
|
|
74b36226f2 | ||
|
|
d501d0304a | ||
|
|
1059933c87 | ||
|
|
5fa58b931e | ||
|
|
ddecc72de7 | ||
|
|
d35a0c5e1e | ||
|
|
340994ce77 | ||
|
|
42b2f21c4d | ||
|
|
e4b9da54dd | ||
|
|
ccc41314ae | ||
|
|
93eb6a19a5 | ||
|
|
e4f2e19d2c | ||
|
|
73a687c9a7 | ||
|
|
32ca3c11fa | ||
|
|
0d648dd188 | ||
|
|
86b7989c89 | ||
|
|
01be6ab596 | ||
|
|
a3d01e8d34 | ||
|
|
808bd47b64 | ||
|
|
f4b506b26b | ||
|
|
1f0d2e2039 |
@@ -19,8 +19,8 @@ android {
|
||||
applicationId 'org.koitharu.kotatsu'
|
||||
minSdk = 21
|
||||
targetSdk = 35
|
||||
versionCode = 1005
|
||||
versionName = '8.1'
|
||||
versionCode = 1008
|
||||
versionName = '8.1.2'
|
||||
generatedDensities = []
|
||||
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
||||
ksp {
|
||||
|
||||
@@ -31,7 +31,7 @@ import org.koitharu.kotatsu.core.util.ext.withPartialWakeLock
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import javax.inject.Inject
|
||||
import com.google.android.material.R as materialR
|
||||
import androidx.appcompat.R as appcompatR
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AutoFixService : CoroutineIntentService() {
|
||||
@@ -95,7 +95,7 @@ class AutoFixService : CoroutineIntentService() {
|
||||
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
|
||||
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
|
||||
.addAction(
|
||||
materialR.drawable.material_ic_clear_black_24dp,
|
||||
appcompatR.drawable.abc_ic_clear_material,
|
||||
applicationContext.getString(android.R.string.cancel),
|
||||
jobContext.getCancelIntent(),
|
||||
)
|
||||
|
||||
@@ -6,10 +6,18 @@ import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.core.network.proxy.ProxyProvider
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.ParserMangaRepository
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.util.ext.configureForParser
|
||||
import org.koitharu.kotatsu.core.util.ext.consumeAll
|
||||
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -18,6 +26,9 @@ abstract class BaseBrowserActivity : BaseActivity<ActivityBrowserBinding>(), Bro
|
||||
@Inject
|
||||
lateinit var proxyProvider: ProxyProvider
|
||||
|
||||
@Inject
|
||||
lateinit var mangaRepositoryFactory: MangaRepository.Factory
|
||||
|
||||
private lateinit var onBackPressedCallback: WebViewBackPressedCallback
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -28,10 +39,21 @@ abstract class BaseBrowserActivity : BaseActivity<ActivityBrowserBinding>(), Bro
|
||||
viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar)
|
||||
onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView)
|
||||
onBackPressedDispatcher.addCallback(onBackPressedCallback)
|
||||
onCreate2(savedInstanceState)
|
||||
|
||||
val mangaSource = MangaSource(intent?.getStringExtra(AppRouter.KEY_SOURCE))
|
||||
val repository = mangaRepositoryFactory.create(mangaSource) as? ParserMangaRepository
|
||||
val userAgent = intent?.getStringExtra(AppRouter.KEY_USER_AGENT)?.nullIfEmpty()
|
||||
?: repository?.getRequestHeaders()?.get(CommonHeaders.USER_AGENT)
|
||||
viewBinding.webView.configureForParser(userAgent)
|
||||
|
||||
onCreate2(savedInstanceState, mangaSource, repository)
|
||||
}
|
||||
|
||||
protected abstract fun onCreate2(savedInstanceState: Bundle?)
|
||||
protected abstract fun onCreate2(
|
||||
savedInstanceState: Bundle?,
|
||||
source: MangaSource,
|
||||
repository: ParserMangaRepository?
|
||||
)
|
||||
|
||||
override fun onApplyWindowInsets(
|
||||
v: View,
|
||||
|
||||
@@ -8,30 +8,19 @@ import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.ParserMangaRepository
|
||||
import org.koitharu.kotatsu.core.util.ext.configureForParser
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import javax.inject.Inject
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
|
||||
@AndroidEntryPoint
|
||||
class BrowserActivity : BaseBrowserActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var mangaRepositoryFactory: MangaRepository.Factory
|
||||
|
||||
override fun onCreate2(savedInstanceState: Bundle?) {
|
||||
setDisplayHomeAsUp(true, true)
|
||||
val mangaSource = MangaSource(intent?.getStringExtra(AppRouter.KEY_SOURCE))
|
||||
val repository = mangaRepositoryFactory.create(mangaSource) as? ParserMangaRepository
|
||||
val userAgent = repository?.getRequestHeaders()?.get(CommonHeaders.USER_AGENT)
|
||||
viewBinding.webView.configureForParser(userAgent)
|
||||
viewBinding.webView.webViewClient = BrowserClient(proxyProvider, this)
|
||||
override fun onCreate2(savedInstanceState: Bundle?, source: MangaSource, repository: ParserMangaRepository?) {
|
||||
setDisplayHomeAsUp(isEnabled = true, showUpAsClose = true)
|
||||
viewBinding.webView.webViewClient = BrowserClient(this)
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
proxyProvider.applyWebViewConfig()
|
||||
|
||||
@@ -3,10 +3,8 @@ package org.koitharu.kotatsu.browser
|
||||
import android.graphics.Bitmap
|
||||
import android.webkit.WebView
|
||||
import androidx.webkit.WebViewClientCompat
|
||||
import org.koitharu.kotatsu.core.network.proxy.ProxyProvider
|
||||
|
||||
open class BrowserClient(
|
||||
private val proxyProvider: ProxyProvider,
|
||||
private val callback: BrowserCallback
|
||||
) : WebViewClientCompat() {
|
||||
|
||||
|
||||
@@ -22,9 +22,11 @@ import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||
import org.koitharu.kotatsu.core.util.ext.configureForParser
|
||||
import org.koitharu.kotatsu.core.parser.ParserMangaRepository
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.network.CloudFlareHelper
|
||||
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -37,15 +39,14 @@ class CloudFlareActivity : BaseBrowserActivity(), CloudFlareCallback {
|
||||
|
||||
private lateinit var cfClient: CloudFlareClient
|
||||
|
||||
override fun onCreate2(savedInstanceState: Bundle?) {
|
||||
setDisplayHomeAsUp(true, true)
|
||||
override fun onCreate2(savedInstanceState: Bundle?, source: MangaSource, repository: ParserMangaRepository?) {
|
||||
setDisplayHomeAsUp(isEnabled = true, showUpAsClose = true)
|
||||
val url = intent?.dataString
|
||||
if (url.isNullOrEmpty()) {
|
||||
finishAfterTransition()
|
||||
return
|
||||
}
|
||||
cfClient = CloudFlareClient(proxyProvider, cookieJar, this, url)
|
||||
viewBinding.webView.configureForParser(intent?.getStringExtra(AppRouter.KEY_USER_AGENT))
|
||||
cfClient = CloudFlareClient(cookieJar, this, url)
|
||||
viewBinding.webView.webViewClient = cfClient
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
@@ -106,8 +107,7 @@ class CloudFlareActivity : BaseBrowserActivity(), CloudFlareCallback {
|
||||
|
||||
override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) {
|
||||
setTitle(title)
|
||||
supportActionBar?.subtitle =
|
||||
subtitle?.toString()?.toHttpUrlOrNull()?.topPrivateDomain() ?: subtitle
|
||||
supportActionBar?.subtitle = subtitle?.toString()?.toHttpUrlOrNull()?.host.ifNullOrEmpty { subtitle }
|
||||
}
|
||||
|
||||
private fun restartCheck() {
|
||||
|
||||
@@ -4,17 +4,15 @@ import android.graphics.Bitmap
|
||||
import android.webkit.WebView
|
||||
import org.koitharu.kotatsu.browser.BrowserClient
|
||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||
import org.koitharu.kotatsu.core.network.proxy.ProxyProvider
|
||||
import org.koitharu.kotatsu.parsers.network.CloudFlareHelper
|
||||
|
||||
private const val LOOP_COUNTER = 3
|
||||
|
||||
class CloudFlareClient(
|
||||
proxyProvider: ProxyProvider,
|
||||
private val cookieJar: MutableCookieJar,
|
||||
private val callback: CloudFlareCallback,
|
||||
private val targetUrl: String,
|
||||
) : BrowserClient(proxyProvider, callback) {
|
||||
) : BrowserClient(callback) {
|
||||
|
||||
private val oldClearance = getClearance()
|
||||
private var counter = 0
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.dialog.buildAlertDialog
|
||||
import org.koitharu.kotatsu.core.util.ext.isHttpUrl
|
||||
import org.koitharu.kotatsu.core.util.ext.restartApplication
|
||||
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
|
||||
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
||||
@@ -163,7 +164,7 @@ class ExceptionResolver @AssistedInject constructor(
|
||||
is ScrobblerAuthRequiredException,
|
||||
is AuthRequiredException -> R.string.sign_in
|
||||
|
||||
is NotFoundException -> if (e.url.isNotEmpty()) R.string.open_in_browser else 0
|
||||
is NotFoundException -> if (e.url.isHttpUrl()) R.string.open_in_browser else 0
|
||||
is UnsupportedSourceException -> if (e.manga != null) R.string.alternatives else 0
|
||||
is SSLException,
|
||||
is CertPathValidatorException -> R.string.fix
|
||||
|
||||
@@ -29,7 +29,7 @@ import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate
|
||||
import org.koitharu.kotatsu.core.util.ext.isWebViewUnavailable
|
||||
import org.koitharu.kotatsu.main.ui.protect.ScreenshotPolicyHelper
|
||||
import com.google.android.material.R as materialR
|
||||
import androidx.appcompat.R as appcompatR
|
||||
|
||||
abstract class BaseActivity<B : ViewBinding> :
|
||||
AppCompatActivity(),
|
||||
@@ -103,7 +103,7 @@ abstract class BaseActivity<B : ViewBinding> :
|
||||
supportActionBar?.run {
|
||||
setDisplayHomeAsUpEnabled(isEnabled)
|
||||
if (showUpAsClose) {
|
||||
setHomeAsUpIndicator(materialR.drawable.ic_clear_black_24)
|
||||
setHomeAsUpIndicator(appcompatR.drawable.abc_ic_clear_material)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
|
||||
import org.koitharu.kotatsu.core.util.ext.copyToClipboard
|
||||
import org.koitharu.kotatsu.core.util.ext.getCauseUrl
|
||||
import org.koitharu.kotatsu.core.util.ext.isHttpUrl
|
||||
import org.koitharu.kotatsu.core.util.ext.isReportable
|
||||
import org.koitharu.kotatsu.core.util.ext.report
|
||||
import org.koitharu.kotatsu.core.util.ext.requireSerializable
|
||||
@@ -43,7 +44,7 @@ class ErrorDetailsDialog : AlertDialogFragment<DialogErrorDetailsBinding>(), Vie
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
binding.buttonBrowser.setOnClickListener(this)
|
||||
binding.textViewSummary.text = exception.message
|
||||
val isUrlAvailable = !exception.getCauseUrl().isNullOrEmpty()
|
||||
val isUrlAvailable = exception.getCauseUrl()?.isHttpUrl() == true
|
||||
binding.buttonBrowser.isVisible = isUrlAvailable
|
||||
binding.textViewBrowser.isVisible = isUrlAvailable
|
||||
binding.textViewDescription.setTextAndVisible(
|
||||
|
||||
@@ -8,7 +8,7 @@ import android.view.animation.DecelerateInterpolator
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.view.ViewCompat
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import com.google.android.material.navigation.NavigationBarView
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
|
||||
import org.koitharu.kotatsu.core.util.ext.measureHeight
|
||||
@@ -16,7 +16,7 @@ import org.koitharu.kotatsu.core.util.ext.measureHeight
|
||||
class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
|
||||
context: Context? = null,
|
||||
attrs: AttributeSet? = null,
|
||||
) : CoordinatorLayout.Behavior<BottomNavigationView>(context, attrs) {
|
||||
) : CoordinatorLayout.Behavior<NavigationBarView>(context, attrs) {
|
||||
|
||||
@ViewCompat.NestedScrollType
|
||||
private var lastStartedType: Int = 0
|
||||
@@ -34,13 +34,13 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun layoutDependsOn(parent: CoordinatorLayout, child: BottomNavigationView, dependency: View): Boolean {
|
||||
override fun layoutDependsOn(parent: CoordinatorLayout, child: NavigationBarView, dependency: View): Boolean {
|
||||
return dependency is AppBarLayout
|
||||
}
|
||||
|
||||
override fun onDependentViewChanged(
|
||||
parent: CoordinatorLayout,
|
||||
child: BottomNavigationView,
|
||||
child: NavigationBarView,
|
||||
dependency: View,
|
||||
): Boolean {
|
||||
val appBarSize = dependency.measureHeight()
|
||||
@@ -54,7 +54,7 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
|
||||
|
||||
override fun onStartNestedScroll(
|
||||
coordinatorLayout: CoordinatorLayout,
|
||||
child: BottomNavigationView,
|
||||
child: NavigationBarView,
|
||||
directTargetChild: View,
|
||||
target: View,
|
||||
axes: Int,
|
||||
@@ -70,7 +70,7 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
|
||||
|
||||
override fun onNestedPreScroll(
|
||||
coordinatorLayout: CoordinatorLayout,
|
||||
child: BottomNavigationView,
|
||||
child: NavigationBarView,
|
||||
target: View,
|
||||
dx: Int,
|
||||
dy: Int,
|
||||
@@ -85,7 +85,7 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
|
||||
|
||||
override fun onStopNestedScroll(
|
||||
coordinatorLayout: CoordinatorLayout,
|
||||
child: BottomNavigationView,
|
||||
child: NavigationBarView,
|
||||
target: View,
|
||||
type: Int,
|
||||
) {
|
||||
@@ -94,7 +94,7 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun animateBottomNavigationVisibility(child: BottomNavigationView, isVisible: Boolean) {
|
||||
private fun animateBottomNavigationVisibility(child: NavigationBarView, isVisible: Boolean) {
|
||||
offsetAnimator?.cancel()
|
||||
offsetAnimator = ValueAnimator().apply {
|
||||
interpolator = DecelerateInterpolator()
|
||||
|
||||
@@ -3,10 +3,12 @@ package org.koitharu.kotatsu.core.ui.widgets
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.animation.TimeInterpolator
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.ViewPropertyAnimator
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.StyleRes
|
||||
@@ -15,9 +17,11 @@ import androidx.core.view.isVisible
|
||||
import androidx.customview.view.AbsSavedState
|
||||
import androidx.interpolator.view.animation.FastOutLinearInInterpolator
|
||||
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationMenuView
|
||||
import com.google.android.material.navigation.NavigationBarView
|
||||
import org.koitharu.kotatsu.core.util.ext.applySystemAnimatorScale
|
||||
import org.koitharu.kotatsu.core.util.ext.measureHeight
|
||||
import kotlin.math.max
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
private const val STATE_DOWN = 1
|
||||
@@ -26,12 +30,14 @@ private const val STATE_UP = 2
|
||||
private const val SLIDE_UP_ANIMATION_DURATION = 225L
|
||||
private const val SLIDE_DOWN_ANIMATION_DURATION = 175L
|
||||
|
||||
private const val MAX_ITEM_COUNT = 6
|
||||
|
||||
class SlidingBottomNavigationView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
@AttrRes defStyleAttr: Int = materialR.attr.bottomNavigationStyle,
|
||||
@StyleRes defStyleRes: Int = materialR.style.Widget_Design_BottomNavigationView,
|
||||
) : BottomNavigationView(context, attrs, defStyleAttr, defStyleRes),
|
||||
) : NavigationBarView(context, attrs, defStyleAttr, defStyleRes),
|
||||
CoordinatorLayout.AttachedBehavior {
|
||||
|
||||
private var currentAnimator: ViewPropertyAnimator? = null
|
||||
@@ -55,6 +61,49 @@ class SlidingBottomNavigationView @JvmOverloads constructor(
|
||||
return behavior
|
||||
}
|
||||
|
||||
/** From BottomNavigationView **/
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
super.onTouchEvent(event)
|
||||
// Consume all events to avoid views under the BottomNavigationView from receiving touch events.
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val minHeightSpec = makeMinHeightSpec(heightMeasureSpec)
|
||||
super.onMeasure(widthMeasureSpec, minHeightSpec)
|
||||
if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY) {
|
||||
setMeasuredDimension(
|
||||
measuredWidth,
|
||||
max(
|
||||
measuredHeight,
|
||||
suggestedMinimumHeight + paddingTop + paddingBottom,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeMinHeightSpec(measureSpec: Int): Int {
|
||||
var minHeight = suggestedMinimumHeight
|
||||
if (MeasureSpec.getMode(measureSpec) != MeasureSpec.EXACTLY && minHeight > 0) {
|
||||
minHeight += paddingTop + paddingBottom
|
||||
|
||||
return MeasureSpec.makeMeasureSpec(
|
||||
max(MeasureSpec.getSize(measureSpec), minHeight), MeasureSpec.AT_MOST,
|
||||
)
|
||||
}
|
||||
|
||||
return measureSpec
|
||||
}
|
||||
|
||||
override fun getMaxItemCount(): Int = MAX_ITEM_COUNT
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
override fun createNavigationBarMenuView(context: Context) = BottomNavigationMenuView(context)
|
||||
|
||||
/** End **/
|
||||
|
||||
override fun onSaveInstanceState(): Parcelable {
|
||||
val superState = super.onSaveInstanceState()
|
||||
return SavedState(superState, currentState, translationY)
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.util
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
|
||||
/**
|
||||
* ProgressIndicator become INVISIBLE instead of GONE by hide() call.
|
||||
* It`s final so we need this workaround
|
||||
*/
|
||||
class GoneOnInvisibleListener(
|
||||
private val view: View,
|
||||
) : ViewTreeObserver.OnGlobalLayoutListener {
|
||||
|
||||
override fun onGlobalLayout() {
|
||||
if (view.visibility == View.INVISIBLE) {
|
||||
view.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
fun attach() {
|
||||
view.viewTreeObserver.addOnGlobalLayoutListener(this)
|
||||
onGlobalLayout()
|
||||
}
|
||||
|
||||
fun detach() {
|
||||
view.viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,8 @@ inline fun <T, R> Flow<List<T>>.mapItems(crossinline transform: (T) -> R): Flow<
|
||||
return map { list -> list.map(transform) }
|
||||
}
|
||||
|
||||
fun <T> Flow<T>.throttle(timeoutMillis: Long): Flow<T> = throttle { timeoutMillis }
|
||||
|
||||
fun <T> Flow<T>.throttle(timeoutMillis: (T) -> Long): Flow<T> {
|
||||
var lastEmittedAt = 0L
|
||||
return transformLatest { value ->
|
||||
|
||||
@@ -10,7 +10,6 @@ import androidx.appcompat.widget.ActionMenuView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.descendants
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -155,9 +154,9 @@ fun TabLayout.setTabsEnabled(enabled: Boolean) {
|
||||
|
||||
fun BaseProgressIndicator<*>.showOrHide(value: Boolean) {
|
||||
if (value) {
|
||||
if (!isVisible) show()
|
||||
show()
|
||||
} else {
|
||||
if (isVisible) hide()
|
||||
hide()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ class DetailsErrorObserver(
|
||||
|
||||
override suspend fun emit(value: Throwable) {
|
||||
val snackbar = Snackbar.make(host, value.getDisplayMessage(host.context.resources), Snackbar.LENGTH_SHORT)
|
||||
snackbar.setAnchorView(activity.viewBinding.containerBottomSheet)
|
||||
if (value is NotFoundException || value is UnsupportedSourceException) {
|
||||
snackbar.duration = Snackbar.LENGTH_INDEFINITE
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||
import org.koitharu.kotatsu.core.ui.dialog.buildAlertDialog
|
||||
import org.koitharu.kotatsu.core.util.ext.isHttpUrl
|
||||
|
||||
class DetailsMenuProvider(
|
||||
private val activity: FragmentActivity,
|
||||
@@ -36,7 +37,7 @@ class DetailsMenuProvider(
|
||||
menu.findItem(R.id.action_share).isVisible = manga != null && AppRouter.isShareSupported(manga)
|
||||
menu.findItem(R.id.action_save).isVisible = manga?.source != null && manga.source != LocalMangaSource
|
||||
menu.findItem(R.id.action_delete).isVisible = manga?.source == LocalMangaSource
|
||||
menu.findItem(R.id.action_browser).isVisible = manga?.source != LocalMangaSource
|
||||
menu.findItem(R.id.action_browser).isVisible = manga?.publicUrl?.isHttpUrl() == true
|
||||
menu.findItem(R.id.action_alternatives).isVisible = manga?.source != LocalMangaSource
|
||||
menu.findItem(R.id.action_shortcut).isVisible = ShortcutManagerCompat.isRequestPinShortcutSupported(activity)
|
||||
menu.findItem(R.id.action_scrobbling).isVisible = viewModel.isScrobblingAvailable
|
||||
|
||||
@@ -125,15 +125,16 @@ class ReadButtonDelegate(
|
||||
}
|
||||
|
||||
private fun onHistoryChanged(isLoading: Boolean, info: HistoryInfo) {
|
||||
val isChaptersLoading = isLoading && (info.totalChapters <= 0 || info.isChapterMissing)
|
||||
buttonRead.setText(
|
||||
when {
|
||||
isLoading -> R.string.loading_
|
||||
isChaptersLoading -> R.string.loading_
|
||||
info.isIncognitoMode -> R.string.incognito
|
||||
info.canContinue -> R.string._continue
|
||||
else -> R.string.read
|
||||
},
|
||||
)
|
||||
splitButton.isEnabled = !isLoading && info.isValid
|
||||
splitButton.isEnabled = !isChaptersLoading && info.isValid
|
||||
}
|
||||
|
||||
private fun Menu.populateBranchList() {
|
||||
|
||||
@@ -36,7 +36,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.format
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import java.util.UUID
|
||||
import com.google.android.material.R as materialR
|
||||
import androidx.appcompat.R as appcompatR
|
||||
|
||||
private const val CHANNEL_ID_DEFAULT = "download"
|
||||
private const val CHANNEL_ID_SILENT = "download_bg"
|
||||
@@ -70,7 +70,7 @@ class DownloadNotificationFactory @AssistedInject constructor(
|
||||
|
||||
private val actionCancel by lazy {
|
||||
NotificationCompat.Action(
|
||||
materialR.drawable.material_ic_clear_black_24dp,
|
||||
appcompatR.drawable.abc_ic_clear_material,
|
||||
context.getString(android.R.string.cancel),
|
||||
workManager.createCancelPendingIntent(uuid),
|
||||
)
|
||||
|
||||
@@ -72,7 +72,7 @@ import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
|
||||
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
|
||||
import org.koitharu.kotatsu.settings.backup.PeriodicalBackupService
|
||||
import javax.inject.Inject
|
||||
import com.google.android.material.R as materialR
|
||||
import androidx.appcompat.R as appcompatR
|
||||
|
||||
private const val TAG_SEARCH = "search"
|
||||
|
||||
@@ -231,6 +231,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
|
||||
topMargin = barsInsets.top
|
||||
bottomMargin = barsInsets.bottom
|
||||
}
|
||||
updateContainerBottomMargin()
|
||||
return insets.consume(v, typeMask, start = viewBinding.navRail != null)
|
||||
}
|
||||
|
||||
@@ -429,9 +430,9 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
|
||||
supportActionBar?.apply {
|
||||
setHomeAsUpIndicator(
|
||||
if (isOpened) {
|
||||
materialR.drawable.ic_arrow_back_black_24
|
||||
appcompatR.drawable.abc_ic_ab_back_material
|
||||
} else {
|
||||
materialR.drawable.ic_search_black_24
|
||||
appcompatR.drawable.abc_ic_search_api_material
|
||||
},
|
||||
)
|
||||
setHomeActionContentDescription(
|
||||
|
||||
@@ -127,8 +127,10 @@ class ReaderActionsView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(slider: Slider) {
|
||||
isSliderChanged = false
|
||||
isSliderTracking = true
|
||||
if (!isSliderTracking) {
|
||||
isSliderChanged = false
|
||||
isSliderTracking = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(slider: Slider) {
|
||||
|
||||
@@ -105,7 +105,7 @@ class ReaderActivity :
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivityReaderBinding.inflate(layoutInflater))
|
||||
readerManager = ReaderManager(supportFragmentManager, viewBinding.container, settings)
|
||||
setDisplayHomeAsUp(true, false)
|
||||
setDisplayHomeAsUp(isEnabled = true, showUpAsClose = false)
|
||||
touchHelper = TapGridDispatcher(this, this)
|
||||
scrollTimer = scrollTimerFactory.create(this, this)
|
||||
pageSaveHelper = pageSaveHelperFactory.create(this)
|
||||
@@ -146,7 +146,7 @@ class ReaderActivity :
|
||||
.setAnchorView(viewBinding.toolbarDocked)
|
||||
.show()
|
||||
}
|
||||
viewModel.readerSettings.observe(this) {
|
||||
viewModel.readerSettingsProducer.observe(this) {
|
||||
viewBinding.infoBar.applyColorScheme(isBlackOnWhite = it.background.isLight(this))
|
||||
}
|
||||
viewModel.isZoomControlsEnabled.observe(this) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.plus
|
||||
@@ -85,6 +86,7 @@ class ReaderViewModel @Inject constructor(
|
||||
interactor: DetailsInteractor,
|
||||
deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
|
||||
downloadScheduler: DownloadWorker.Scheduler,
|
||||
readerSettingsProducerFactory: ReaderSettings.Producer.Factory,
|
||||
) : ChaptersPagesViewModel(
|
||||
settings = settings,
|
||||
interactor = interactor,
|
||||
@@ -170,12 +172,8 @@ class ReaderViewModel @Inject constructor(
|
||||
}
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false)
|
||||
|
||||
val readerSettings = ReaderSettings(
|
||||
parentScope = viewModelScope,
|
||||
settings = settings,
|
||||
colorFilterFlow = manga.flatMapLatest {
|
||||
if (it == null) flowOf(null) else dataRepository.observeColorFilter(it.id)
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null),
|
||||
val readerSettingsProducer = readerSettingsProducerFactory.create(
|
||||
manga.mapNotNull { it?.id },
|
||||
)
|
||||
|
||||
val isMangaNsfw = manga.map { it?.contentRating == ContentRating.ADULT }
|
||||
|
||||
@@ -1,66 +1,69 @@
|
||||
package org.koitharu.kotatsu.reader.ui.config
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Bitmap
|
||||
import android.view.View
|
||||
import androidx.annotation.CheckResult
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.collection.scatterSetOf
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import com.davemorrissey.labs.subscaleview.decoder.SkiaImageDecoder
|
||||
import com.davemorrissey.labs.subscaleview.decoder.SkiaImageRegionDecoder
|
||||
import com.davemorrissey.labs.subscaleview.decoder.SkiaPooledImageRegionDecoder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderBackground
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||
import org.koitharu.kotatsu.core.util.MediatorStateFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
|
||||
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
||||
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
|
||||
|
||||
class ReaderSettings(
|
||||
private val parentScope: CoroutineScope,
|
||||
private val settings: AppSettings,
|
||||
private val colorFilterFlow: StateFlow<ReaderColorFilter?>,
|
||||
) : MediatorLiveData<ReaderSettings>() {
|
||||
data class ReaderSettings(
|
||||
val zoomMode: ZoomMode,
|
||||
val background: ReaderBackground,
|
||||
val colorFilter: ReaderColorFilter?,
|
||||
val isReaderOptimizationEnabled: Boolean,
|
||||
val bitmapConfig: Bitmap.Config,
|
||||
val isPagesNumbersEnabled: Boolean,
|
||||
val isPagesCropEnabledStandard: Boolean,
|
||||
val isPagesCropEnabledWebtoon: Boolean,
|
||||
) {
|
||||
|
||||
private val internalObserver = InternalObserver()
|
||||
private var collectJob: Job? = null
|
||||
|
||||
val zoomMode: ZoomMode
|
||||
get() = settings.zoomMode
|
||||
|
||||
val background: ReaderBackground
|
||||
get() = settings.readerBackground
|
||||
|
||||
val colorFilter: ReaderColorFilter?
|
||||
get() = colorFilterFlow.value?.takeUnless { it.isEmpty } ?: settings.readerColorFilter
|
||||
|
||||
val isReaderOptimizationEnabled: Boolean
|
||||
get() = settings.isReaderOptimizationEnabled
|
||||
|
||||
val bitmapConfig: Bitmap.Config
|
||||
get() = if (settings.is32BitColorsEnabled) {
|
||||
private constructor(settings: AppSettings, colorFilterOverride: ReaderColorFilter?) : this(
|
||||
zoomMode = settings.zoomMode,
|
||||
background = settings.readerBackground,
|
||||
colorFilter = colorFilterOverride?.takeUnless { it.isEmpty } ?: settings.readerColorFilter,
|
||||
isReaderOptimizationEnabled = settings.isReaderOptimizationEnabled,
|
||||
bitmapConfig = if (settings.is32BitColorsEnabled) {
|
||||
Bitmap.Config.ARGB_8888
|
||||
} else {
|
||||
Bitmap.Config.RGB_565
|
||||
}
|
||||
|
||||
val isPagesNumbersEnabled: Boolean
|
||||
get() = settings.isPagesNumbersEnabled
|
||||
},
|
||||
isPagesNumbersEnabled = settings.isPagesNumbersEnabled,
|
||||
isPagesCropEnabledStandard = settings.isPagesCropEnabled(ReaderMode.STANDARD),
|
||||
isPagesCropEnabledWebtoon = settings.isPagesCropEnabled(ReaderMode.WEBTOON),
|
||||
)
|
||||
|
||||
fun applyBackground(view: View) {
|
||||
view.background = background.resolve(view.context)
|
||||
}
|
||||
|
||||
fun isPagesCropEnabled(isWebtoon: Boolean) = settings.isPagesCropEnabled(
|
||||
if (isWebtoon) ReaderMode.WEBTOON else ReaderMode.STANDARD,
|
||||
)
|
||||
fun isPagesCropEnabled(isWebtoon: Boolean) = if (isWebtoon) {
|
||||
isPagesCropEnabledWebtoon
|
||||
} else {
|
||||
isPagesCropEnabledStandard
|
||||
}
|
||||
|
||||
@CheckResult
|
||||
fun applyBitmapConfig(ssiv: SubsamplingScaleImageView): Boolean {
|
||||
@@ -78,33 +81,13 @@ class ReaderSettings(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onInactive() {
|
||||
super.onInactive()
|
||||
settings.unsubscribe(internalObserver)
|
||||
collectJob?.cancel()
|
||||
collectJob = null
|
||||
}
|
||||
class Producer @AssistedInject constructor(
|
||||
@Assisted private val mangaId: Flow<Long>,
|
||||
private val settings: AppSettings,
|
||||
private val mangaDataRepository: MangaDataRepository,
|
||||
) : MediatorStateFlow<ReaderSettings>(ReaderSettings(settings, null)) {
|
||||
|
||||
override fun onActive() {
|
||||
super.onActive()
|
||||
settings.subscribe(internalObserver)
|
||||
collectJob?.cancel()
|
||||
collectJob = parentScope.launch {
|
||||
colorFilterFlow.collect(internalObserver)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getValue() = this
|
||||
|
||||
private fun notifyChanged() {
|
||||
value = value
|
||||
}
|
||||
|
||||
private inner class InternalObserver :
|
||||
FlowCollector<ReaderColorFilter?>,
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
private val settingsKeys = setOf(
|
||||
private val settingsKeys = scatterSetOf(
|
||||
AppSettings.KEY_ZOOM_MODE,
|
||||
AppSettings.KEY_PAGES_NUMBERS,
|
||||
AppSettings.KEY_READER_BACKGROUND,
|
||||
@@ -114,18 +97,38 @@ class ReaderSettings(
|
||||
AppSettings.KEY_CF_BRIGHTNESS,
|
||||
AppSettings.KEY_CF_INVERTED,
|
||||
AppSettings.KEY_CF_GRAYSCALE,
|
||||
AppSettings.KEY_READER_CROP,
|
||||
)
|
||||
private var job: Job? = null
|
||||
|
||||
override suspend fun emit(value: ReaderColorFilter?) {
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
notifyChanged()
|
||||
override fun onActive() {
|
||||
assert(job?.isActive != true)
|
||||
job?.cancel()
|
||||
job = processLifecycleScope.launch(Dispatchers.Default) {
|
||||
observeImpl()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
if (key in settingsKeys) {
|
||||
notifyChanged()
|
||||
override fun onInactive() {
|
||||
job?.cancel()
|
||||
job = null
|
||||
}
|
||||
|
||||
private suspend fun observeImpl() {
|
||||
combine(
|
||||
mangaId.flatMapLatest { mangaDataRepository.observeColorFilter(it) },
|
||||
settings.observe().filter { x -> x == null || x in settingsKeys }.onStart { emit(null) },
|
||||
) { mangaCf, settingsKey ->
|
||||
ReaderSettings(settings, mangaCf)
|
||||
}.collect {
|
||||
publishValue(it)
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
||||
fun create(mangaId: Flow<Long>): Producer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +1,58 @@
|
||||
package org.koitharu.kotatsu.reader.ui.pager
|
||||
|
||||
import android.content.ComponentCallbacks2
|
||||
import android.content.ComponentCallbacks2.TRIM_MEMORY_COMPLETE
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.view.View
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.davemorrissey.labs.subscaleview.DefaultOnImageEventListener
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.core.ui.list.lifecycle.LifecycleAwareViewHolder
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
|
||||
import org.koitharu.kotatsu.core.util.ext.isSerializable
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.databinding.LayoutPageInfoBinding
|
||||
import org.koitharu.kotatsu.parsers.util.ifZero
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||
import org.koitharu.kotatsu.reader.ui.pager.PageHolderDelegate.State
|
||||
import org.koitharu.kotatsu.reader.ui.pager.vm.PageState
|
||||
import org.koitharu.kotatsu.reader.ui.pager.vm.PageViewModel
|
||||
import org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonHolder
|
||||
|
||||
abstract class BasePageHolder<B : ViewBinding>(
|
||||
protected val binding: B,
|
||||
loader: PageLoader,
|
||||
protected val settings: ReaderSettings,
|
||||
readerSettingsProducer: ReaderSettings.Producer,
|
||||
networkState: NetworkState,
|
||||
exceptionResolver: ExceptionResolver,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
) : LifecycleAwareViewHolder(binding.root, lifecycleOwner), PageHolderDelegate.Callback {
|
||||
) : LifecycleAwareViewHolder(binding.root, lifecycleOwner), DefaultOnImageEventListener, ComponentCallbacks2 {
|
||||
|
||||
@Suppress("LeakingThis")
|
||||
protected val delegate = PageHolderDelegate(
|
||||
protected val viewModel = PageViewModel(
|
||||
loader = loader,
|
||||
readerSettings = settings,
|
||||
callback = this,
|
||||
settingsProducer = readerSettingsProducer,
|
||||
networkState = networkState,
|
||||
exceptionResolver = exceptionResolver,
|
||||
isWebtoon = this is WebtoonHolder,
|
||||
)
|
||||
protected val bindingInfo = LayoutPageInfoBinding.bind(binding.root)
|
||||
protected abstract val ssiv: SubsamplingScaleImageView
|
||||
|
||||
protected val settings: ReaderSettings
|
||||
get() = viewModel.settingsProducer.value
|
||||
|
||||
val context: Context
|
||||
get() = itemView.context
|
||||
@@ -42,51 +60,140 @@ abstract class BasePageHolder<B : ViewBinding>(
|
||||
var boundData: ReaderPage? = null
|
||||
private set
|
||||
|
||||
override fun onConfigChanged() {
|
||||
settings.applyBackground(itemView)
|
||||
init {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
ssiv.bindToLifecycle(this@BasePageHolder)
|
||||
ssiv.isEagerLoadingEnabled = !context.isLowRamDevice()
|
||||
ssiv.addOnImageEventListener(viewModel)
|
||||
ssiv.addOnImageEventListener(this@BasePageHolder)
|
||||
}
|
||||
val clickListener = View.OnClickListener { v ->
|
||||
when (v.id) {
|
||||
R.id.button_retry -> viewModel.retry(
|
||||
page = boundData?.toMangaPage() ?: return@OnClickListener,
|
||||
isFromUser = true,
|
||||
)
|
||||
|
||||
R.id.button_error_details -> viewModel.showErrorDetails(boundData?.url)
|
||||
}
|
||||
}
|
||||
bindingInfo.buttonRetry.setOnClickListener(clickListener)
|
||||
bindingInfo.buttonErrorDetails.setOnClickListener(clickListener)
|
||||
}
|
||||
|
||||
fun requireData(): ReaderPage {
|
||||
return checkNotNull(boundData) { "Calling requireData() before bind()" }
|
||||
@CallSuper
|
||||
protected open fun onConfigChanged(settings: ReaderSettings) {
|
||||
settings.applyBackground(itemView)
|
||||
if (settings.applyBitmapConfig(ssiv)) {
|
||||
reloadImage()
|
||||
} else if (viewModel.state.value is PageState.Shown) {
|
||||
onReady()
|
||||
}
|
||||
ssiv.applyDownSampling(isResumed())
|
||||
}
|
||||
|
||||
fun reloadImage() {
|
||||
val source = (viewModel.state.value as? PageState.Shown)?.source ?: return
|
||||
ssiv.setImage(source)
|
||||
}
|
||||
|
||||
fun bind(data: ReaderPage) {
|
||||
boundData = data
|
||||
viewModel.onBind(data.toMangaPage())
|
||||
onBind(data)
|
||||
}
|
||||
|
||||
protected abstract fun onBind(data: ReaderPage)
|
||||
@CallSuper
|
||||
protected open fun onBind(data: ReaderPage) = Unit
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
context.registerComponentCallbacks(delegate)
|
||||
context.registerComponentCallbacks(this)
|
||||
viewModel.state.observe(this, ::onStateChanged)
|
||||
viewModel.settingsProducer.observe(this, ::onConfigChanged)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (delegate.state == State.ERROR && !delegate.isLoading()) {
|
||||
boundData?.let { delegate.retry(it.toMangaPage(), isFromUser = false) }
|
||||
ssiv.applyDownSampling(isForeground = true)
|
||||
if (viewModel.state.value is PageState.Error && !viewModel.isLoading()) {
|
||||
boundData?.let { viewModel.retry(it.toMangaPage(), isFromUser = false) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
ssiv.applyDownSampling(isForeground = false)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
context.unregisterComponentCallbacks(delegate)
|
||||
context.unregisterComponentCallbacks(this)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
open fun onAttachedToWindow() {
|
||||
delegate.onAttachedToWindow()
|
||||
}
|
||||
open fun onAttachedToWindow() = Unit
|
||||
|
||||
@CallSuper
|
||||
open fun onDetachedFromWindow() {
|
||||
delegate.onDetachedFromWindow()
|
||||
}
|
||||
open fun onDetachedFromWindow() = Unit
|
||||
|
||||
@CallSuper
|
||||
open fun onRecycled() {
|
||||
delegate.onRecycle()
|
||||
viewModel.onRecycle()
|
||||
ssiv.recycle()
|
||||
}
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) = Unit
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
final override fun onLowMemory() = onTrimMemory(TRIM_MEMORY_COMPLETE)
|
||||
|
||||
protected open fun onStateChanged(state: PageState) {
|
||||
bindingInfo.layoutError.isVisible = state is PageState.Error
|
||||
bindingInfo.progressBar.isGone = state.isFinalState()
|
||||
bindingInfo.textViewStatus.isGone = state.isFinalState()
|
||||
val progress = (state as? PageState.Loading)?.progress ?: -1
|
||||
if (progress in 0..100) {
|
||||
bindingInfo.progressBar.isIndeterminate = false
|
||||
bindingInfo.progressBar.setProgressCompat(progress, true)
|
||||
bindingInfo.textViewStatus.text = context.getString(R.string.percent_string_pattern, progress.toString())
|
||||
} else {
|
||||
bindingInfo.progressBar.isIndeterminate = true
|
||||
bindingInfo.textViewStatus.setText(R.string.loading_)
|
||||
}
|
||||
when (state) {
|
||||
is PageState.Converting -> {
|
||||
bindingInfo.textViewStatus.setText(R.string.processing_)
|
||||
}
|
||||
|
||||
is PageState.Empty -> Unit
|
||||
|
||||
is PageState.Error -> {
|
||||
val e = state.error
|
||||
bindingInfo.textViewError.text = e.getDisplayMessage(context.resources)
|
||||
bindingInfo.buttonRetry.setText(
|
||||
ExceptionResolver.getResolveStringId(e).ifZero { R.string.try_again },
|
||||
)
|
||||
bindingInfo.buttonErrorDetails.isVisible = e.isSerializable()
|
||||
bindingInfo.layoutError.isVisible = true
|
||||
bindingInfo.progressBar.hide()
|
||||
}
|
||||
|
||||
is PageState.Loaded -> {
|
||||
bindingInfo.textViewStatus.setText(R.string.processing_)
|
||||
ssiv.setImage(state.source)
|
||||
}
|
||||
|
||||
is PageState.Loading -> {
|
||||
if (state.preview != null && ssiv.getState() == null) {
|
||||
ssiv.setImage(state.preview)
|
||||
}
|
||||
}
|
||||
|
||||
is PageState.Shown -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
protected fun SubsamplingScaleImageView.applyDownSampling(isForeground: Boolean) {
|
||||
|
||||
@@ -142,7 +142,7 @@ abstract class BasePagerReaderFragment : BaseReaderFragment<FragmentReaderPagerB
|
||||
override fun onCreateAdapter(): BaseReaderAdapter<*> = PagesAdapter(
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
loader = pageLoader,
|
||||
settings = viewModel.readerSettings,
|
||||
readerSettingsProducer = viewModel.readerSettingsProducer,
|
||||
networkState = networkState,
|
||||
exceptionResolver = exceptionResolver,
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ import kotlin.coroutines.suspendCoroutine
|
||||
@Suppress("LeakingThis")
|
||||
abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
|
||||
private val loader: PageLoader,
|
||||
private val readerSettings: ReaderSettings,
|
||||
private val readerSettingsProducer: ReaderSettings.Producer,
|
||||
private val networkState: NetworkState,
|
||||
private val exceptionResolver: ExceptionResolver,
|
||||
) : RecyclerView.Adapter<H>() {
|
||||
@@ -58,7 +58,7 @@ abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
|
||||
final override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int,
|
||||
): H = onCreateViewHolder(parent, loader, readerSettings, networkState, exceptionResolver)
|
||||
): H = onCreateViewHolder(parent, loader, readerSettingsProducer, networkState, exceptionResolver)
|
||||
|
||||
suspend fun setItems(items: List<ReaderPage>) = suspendCoroutine { cont ->
|
||||
differ.submitList(items) {
|
||||
@@ -69,7 +69,7 @@ abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
|
||||
protected abstract fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
loader: PageLoader,
|
||||
settings: ReaderSettings,
|
||||
readerSettingsProducer: ReaderSettings.Producer,
|
||||
networkState: NetworkState,
|
||||
exceptionResolver: ExceptionResolver,
|
||||
): H
|
||||
|
||||
@@ -1,262 +0,0 @@
|
||||
package org.koitharu.kotatsu.reader.ui.pager
|
||||
|
||||
import android.content.ComponentCallbacks2
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.Observer
|
||||
import com.davemorrissey.labs.subscaleview.DefaultOnImageEventListener
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||
import java.io.IOException
|
||||
|
||||
class PageHolderDelegate(
|
||||
private val loader: PageLoader,
|
||||
private val readerSettings: ReaderSettings,
|
||||
private val callback: Callback,
|
||||
private val networkState: NetworkState,
|
||||
private val exceptionResolver: ExceptionResolver,
|
||||
private val isWebtoon: Boolean,
|
||||
) : DefaultOnImageEventListener, Observer<ReaderSettings>, ComponentCallbacks2 {
|
||||
|
||||
private val scope = loader.loaderScope + Dispatchers.Main.immediate
|
||||
var state = State.EMPTY
|
||||
private set
|
||||
private var job: Job? = null
|
||||
private var uri: Uri? = null
|
||||
private var cachedBounds: Rect? = null
|
||||
private var error: Throwable? = null
|
||||
|
||||
init {
|
||||
scope.launch(Dispatchers.Main) { // the same as post() -- wait until child fields init
|
||||
callback.onConfigChanged()
|
||||
}
|
||||
}
|
||||
|
||||
fun isLoading() = job?.isActive == true
|
||||
|
||||
fun onBind(page: MangaPage) {
|
||||
val prevJob = job
|
||||
job = scope.launch {
|
||||
prevJob?.cancelAndJoin()
|
||||
doLoad(page, force = false)
|
||||
}
|
||||
}
|
||||
|
||||
fun retry(page: MangaPage, isFromUser: Boolean) {
|
||||
val prevJob = job
|
||||
job = scope.launch {
|
||||
prevJob?.cancelAndJoin()
|
||||
val e = error
|
||||
if (e != null && ExceptionResolver.canResolve(e)) {
|
||||
if (!isFromUser) {
|
||||
return@launch
|
||||
}
|
||||
exceptionResolver.resolve(e)
|
||||
}
|
||||
doLoad(page, force = true)
|
||||
}
|
||||
}
|
||||
|
||||
fun showErrorDetails(url: String?) {
|
||||
val e = error ?: return
|
||||
exceptionResolver.showErrorDetails(e, url)
|
||||
}
|
||||
|
||||
fun onAttachedToWindow() {
|
||||
readerSettings.observeForever(this)
|
||||
}
|
||||
|
||||
fun onDetachedFromWindow() {
|
||||
readerSettings.removeObserver(this)
|
||||
}
|
||||
|
||||
fun onRecycle() {
|
||||
state = State.EMPTY
|
||||
uri = null
|
||||
cachedBounds = null
|
||||
error = null
|
||||
job?.cancel()
|
||||
}
|
||||
|
||||
fun reload() {
|
||||
if (state == State.SHOWN) {
|
||||
uri?.let {
|
||||
callback.onImageReady(it.toImageSource(cachedBounds))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReady() {
|
||||
if (state >= State.LOADED) {
|
||||
state = State.SHOWING
|
||||
error = null
|
||||
callback.onImageShowing(readerSettings, isPreview = false)
|
||||
} else if (state == State.LOADING_WITH_PREVIEW) {
|
||||
callback.onImageShowing(readerSettings, isPreview = true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onImageLoaded() {
|
||||
if (state >= State.LOADED) {
|
||||
state = State.SHOWN
|
||||
error = null
|
||||
callback.onImageShown()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onImageLoadError(e: Throwable) {
|
||||
e.printStackTraceDebug()
|
||||
if (state < State.LOADED) {
|
||||
// ignore preview error
|
||||
return
|
||||
}
|
||||
val uri = this.uri
|
||||
error = e
|
||||
if (state == State.LOADED && e is IOException && uri != null && uri.toFileOrNull()?.exists() != false) {
|
||||
tryConvert(uri, e)
|
||||
} else {
|
||||
state = State.ERROR
|
||||
callback.onError(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChanged(value: ReaderSettings) {
|
||||
if (state == State.SHOWN) {
|
||||
callback.onImageShowing(readerSettings, isPreview = false)
|
||||
}
|
||||
callback.onConfigChanged()
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) = Unit
|
||||
|
||||
@Suppress("OVERRIDE_DEPRECATION")
|
||||
override fun onLowMemory() = Unit
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
callback.onTrimMemory()
|
||||
}
|
||||
|
||||
private fun tryConvert(uri: Uri, e: Exception) {
|
||||
val prevJob = job
|
||||
job = scope.launch {
|
||||
prevJob?.join()
|
||||
state = State.CONVERTING
|
||||
try {
|
||||
val newUri = loader.convertBimap(uri)
|
||||
cachedBounds = if (readerSettings.isPagesCropEnabled(isWebtoon)) {
|
||||
loader.getTrimmedBounds(newUri)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
state = State.CONVERTED
|
||||
callback.onImageReady(newUri.toImageSource(cachedBounds))
|
||||
} catch (ce: CancellationException) {
|
||||
throw ce
|
||||
} catch (e2: Throwable) {
|
||||
e2.printStackTrace()
|
||||
e.addSuppressed(e2)
|
||||
state = State.ERROR
|
||||
callback.onError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun doLoad(data: MangaPage, force: Boolean) = coroutineScope {
|
||||
state = State.LOADING
|
||||
error = null
|
||||
callback.onLoadingStarted()
|
||||
launch {
|
||||
val preview = loader.loadPreview(data) ?: return@launch
|
||||
if (state == State.LOADING) {
|
||||
state = State.LOADING_WITH_PREVIEW
|
||||
callback.onPreviewReady(preview)
|
||||
}
|
||||
}
|
||||
try {
|
||||
val task = withContext(Dispatchers.Default) {
|
||||
loader.loadPageAsync(data, force)
|
||||
}
|
||||
val progressObserver = observeProgress(this, task.progressAsFlow())
|
||||
val file = task.await()
|
||||
progressObserver.cancelAndJoin()
|
||||
uri = file
|
||||
state = State.LOADED
|
||||
cachedBounds = if (readerSettings.isPagesCropEnabled(isWebtoon)) {
|
||||
loader.getTrimmedBounds(checkNotNull(uri))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
callback.onImageReady(checkNotNull(uri).toImageSource(cachedBounds))
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTraceDebug()
|
||||
state = State.ERROR
|
||||
error = e
|
||||
callback.onError(e)
|
||||
if (e is IOException && !networkState.value) {
|
||||
networkState.awaitForConnection()
|
||||
retry(data, isFromUser = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeProgress(scope: CoroutineScope, progress: Flow<Float>) = progress
|
||||
.debounce(250)
|
||||
.onEach { callback.onProgressChanged((100 * it).toInt()) }
|
||||
.launchIn(scope)
|
||||
|
||||
private fun Uri.toImageSource(bounds: Rect?): ImageSource {
|
||||
val source = ImageSource.uri(this)
|
||||
return if (bounds != null) {
|
||||
source.region(bounds)
|
||||
} else {
|
||||
source
|
||||
}
|
||||
}
|
||||
|
||||
enum class State {
|
||||
EMPTY, LOADING, LOADING_WITH_PREVIEW, LOADED, CONVERTING, CONVERTED, SHOWING, SHOWN, ERROR
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
|
||||
fun onLoadingStarted()
|
||||
|
||||
fun onError(e: Throwable)
|
||||
|
||||
fun onPreviewReady(source: ImageSource)
|
||||
|
||||
fun onImageReady(source: ImageSource)
|
||||
|
||||
fun onImageShowing(settings: ReaderSettings, isPreview: Boolean)
|
||||
|
||||
fun onImageShown()
|
||||
|
||||
fun onProgressChanged(progress: Int)
|
||||
|
||||
fun onConfigChanged()
|
||||
|
||||
fun onTrimMemory()
|
||||
}
|
||||
}
|
||||
@@ -17,10 +17,17 @@ class DoublePageHolder(
|
||||
owner: LifecycleOwner,
|
||||
binding: ItemPageBinding,
|
||||
loader: PageLoader,
|
||||
settings: ReaderSettings,
|
||||
readerSettingsProducer: ReaderSettings.Producer,
|
||||
networkState: NetworkState,
|
||||
exceptionResolver: ExceptionResolver,
|
||||
) : PageHolder(owner, binding, loader, settings, networkState, exceptionResolver) {
|
||||
) : PageHolder(
|
||||
owner = owner,
|
||||
binding = binding,
|
||||
loader = loader,
|
||||
readerSettingsProducer = readerSettingsProducer,
|
||||
networkState = networkState,
|
||||
exceptionResolver = exceptionResolver,
|
||||
) {
|
||||
|
||||
private val isEven: Boolean
|
||||
get() = bindingAdapterPosition and 1 == 0
|
||||
@@ -35,7 +42,7 @@ class DoublePageHolder(
|
||||
.gravity = (if (isEven) Gravity.START else Gravity.END) or Gravity.BOTTOM
|
||||
}
|
||||
|
||||
override fun onImageShowing(settings: ReaderSettings, isPreview: Boolean) {
|
||||
override fun onReady() {
|
||||
with(binding.ssiv) {
|
||||
maxScale = 2f * maxOf(
|
||||
width / sWidth.toFloat(),
|
||||
|
||||
@@ -13,22 +13,22 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
||||
class DoublePagesAdapter(
|
||||
private val lifecycleOwner: LifecycleOwner,
|
||||
loader: PageLoader,
|
||||
settings: ReaderSettings,
|
||||
readerSettingsProducer: ReaderSettings.Producer,
|
||||
networkState: NetworkState,
|
||||
exceptionResolver: ExceptionResolver,
|
||||
) : BaseReaderAdapter<DoublePageHolder>(loader, settings, networkState, exceptionResolver) {
|
||||
) : BaseReaderAdapter<DoublePageHolder>(loader, readerSettingsProducer, networkState, exceptionResolver) {
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
loader: PageLoader,
|
||||
settings: ReaderSettings,
|
||||
readerSettingsProducer: ReaderSettings.Producer,
|
||||
networkState: NetworkState,
|
||||
exceptionResolver: ExceptionResolver,
|
||||
) = DoublePageHolder(
|
||||
owner = lifecycleOwner,
|
||||
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||
loader = loader,
|
||||
settings = settings,
|
||||
readerSettingsProducer = readerSettingsProducer,
|
||||
networkState = networkState,
|
||||
exceptionResolver = exceptionResolver,
|
||||
)
|
||||
|
||||
@@ -90,7 +90,7 @@ open class DoubleReaderFragment : BaseReaderFragment<FragmentReaderDoubleBinding
|
||||
override fun onCreateAdapter() = DoublePagesAdapter(
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
loader = pageLoader,
|
||||
settings = viewModel.readerSettings,
|
||||
readerSettingsProducer = viewModel.readerSettingsProducer,
|
||||
networkState = networkState,
|
||||
exceptionResolver = exceptionResolver,
|
||||
)
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
package org.koitharu.kotatsu.reader.ui.pager.reversed
|
||||
|
||||
import android.graphics.PointF
|
||||
import android.os.Build
|
||||
import android.view.Gravity
|
||||
import android.view.RoundedCorner
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowInsets
|
||||
import android.widget.FrameLayout
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.core.util.ext.isRtl
|
||||
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||
@@ -17,17 +25,24 @@ class ReversedPageHolder(
|
||||
owner: LifecycleOwner,
|
||||
binding: ItemPageBinding,
|
||||
loader: PageLoader,
|
||||
settings: ReaderSettings,
|
||||
readerSettingsProducer: ReaderSettings.Producer,
|
||||
networkState: NetworkState,
|
||||
exceptionResolver: ExceptionResolver,
|
||||
) : PageHolder(owner, binding, loader, settings, networkState, exceptionResolver) {
|
||||
) : PageHolder(
|
||||
owner = owner,
|
||||
binding = binding,
|
||||
loader = loader,
|
||||
readerSettingsProducer = readerSettingsProducer,
|
||||
networkState = networkState,
|
||||
exceptionResolver = exceptionResolver,
|
||||
) {
|
||||
|
||||
init {
|
||||
(binding.textViewNumber.layoutParams as FrameLayout.LayoutParams)
|
||||
.gravity = Gravity.START or Gravity.BOTTOM
|
||||
}
|
||||
|
||||
override fun onImageShowing(settings: ReaderSettings, isPreview: Boolean) {
|
||||
override fun onReady() {
|
||||
with(binding.ssiv) {
|
||||
maxScale = 2f * maxOf(
|
||||
width / sWidth.toFloat(),
|
||||
|
||||
@@ -13,22 +13,22 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
||||
class ReversedPagesAdapter(
|
||||
private val lifecycleOwner: LifecycleOwner,
|
||||
loader: PageLoader,
|
||||
settings: ReaderSettings,
|
||||
readerSettingsProducer: ReaderSettings.Producer,
|
||||
networkState: NetworkState,
|
||||
exceptionResolver: ExceptionResolver,
|
||||
) : BaseReaderAdapter<ReversedPageHolder>(loader, settings, networkState, exceptionResolver) {
|
||||
) : BaseReaderAdapter<ReversedPageHolder>(loader, readerSettingsProducer, networkState, exceptionResolver) {
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
loader: PageLoader,
|
||||
settings: ReaderSettings,
|
||||
readerSettingsProducer: ReaderSettings.Producer,
|
||||
networkState: NetworkState,
|
||||
exceptionResolver: ExceptionResolver,
|
||||
) = ReversedPageHolder(
|
||||
owner = lifecycleOwner,
|
||||
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||
loader = loader,
|
||||
settings = settings,
|
||||
readerSettingsProducer = readerSettingsProducer,
|
||||
networkState = networkState,
|
||||
exceptionResolver = exceptionResolver,
|
||||
)
|
||||
|
||||
@@ -19,7 +19,7 @@ class ReversedReaderFragment : BasePagerReaderFragment() {
|
||||
override fun onCreateAdapter() = ReversedPagesAdapter(
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
loader = pageLoader,
|
||||
settings = viewModel.readerSettings,
|
||||
readerSettingsProducer = viewModel.readerSettingsProducer,
|
||||
networkState = networkState,
|
||||
exceptionResolver = exceptionResolver,
|
||||
)
|
||||
|
||||
@@ -2,22 +2,28 @@ package org.koitharu.kotatsu.reader.ui.pager.standard
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.PointF
|
||||
import android.os.Build
|
||||
import android.view.Gravity
|
||||
import android.view.RoundedCorner
|
||||
import android.view.View
|
||||
import android.view.WindowInsets
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.widget.FrameLayout
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.view.OnApplyWindowInsetsListener
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.setMargins
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
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.model.ZoomMode
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ZoomControl
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
|
||||
import org.koitharu.kotatsu.core.util.ext.isSerializable
|
||||
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
||||
import org.koitharu.kotatsu.parsers.util.ifZero
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||
import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder
|
||||
@@ -27,77 +33,48 @@ open class PageHolder(
|
||||
owner: LifecycleOwner,
|
||||
binding: ItemPageBinding,
|
||||
loader: PageLoader,
|
||||
settings: ReaderSettings,
|
||||
readerSettingsProducer: ReaderSettings.Producer,
|
||||
networkState: NetworkState,
|
||||
exceptionResolver: ExceptionResolver,
|
||||
) : BasePageHolder<ItemPageBinding>(binding, loader, settings, networkState, exceptionResolver, owner),
|
||||
View.OnClickListener,
|
||||
ZoomControl.ZoomControlListener {
|
||||
) : BasePageHolder<ItemPageBinding>(
|
||||
binding = binding,
|
||||
loader = loader,
|
||||
readerSettingsProducer = readerSettingsProducer,
|
||||
networkState = networkState,
|
||||
exceptionResolver = exceptionResolver,
|
||||
lifecycleOwner = owner,
|
||||
), ZoomControl.ZoomControlListener, OnApplyWindowInsetsListener {
|
||||
|
||||
override val ssiv = binding.ssiv
|
||||
|
||||
init {
|
||||
binding.ssiv.bindToLifecycle(owner)
|
||||
binding.ssiv.isEagerLoadingEnabled = !context.isLowRamDevice()
|
||||
binding.ssiv.addOnImageEventListener(delegate)
|
||||
@Suppress("LeakingThis")
|
||||
bindingInfo.buttonRetry.setOnClickListener(this)
|
||||
@Suppress("LeakingThis")
|
||||
bindingInfo.buttonErrorDetails.setOnClickListener(this)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.root, this)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
binding.ssiv.applyDownSampling(isForeground = true)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
binding.ssiv.applyDownSampling(isForeground = false)
|
||||
}
|
||||
|
||||
override fun onConfigChanged() {
|
||||
super.onConfigChanged()
|
||||
if (settings.applyBitmapConfig(binding.ssiv)) {
|
||||
delegate.reload()
|
||||
override fun onApplyWindowInsets(
|
||||
v: View,
|
||||
insets: WindowInsetsCompat
|
||||
): WindowInsetsCompat {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
insets.toWindowInsets()?.let {
|
||||
applyRoundedCorners(it)
|
||||
}
|
||||
}
|
||||
binding.ssiv.applyDownSampling(isResumed())
|
||||
return insets
|
||||
}
|
||||
|
||||
override fun onConfigChanged(settings: ReaderSettings) {
|
||||
super.onConfigChanged(settings)
|
||||
binding.textViewNumber.isVisible = settings.isPagesNumbersEnabled
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBind(data: ReaderPage) {
|
||||
delegate.onBind(data.toMangaPage())
|
||||
super.onBind(data)
|
||||
binding.textViewNumber.text = (data.index + 1).toString()
|
||||
}
|
||||
|
||||
override fun onRecycled() {
|
||||
super.onRecycled()
|
||||
binding.ssiv.recycle()
|
||||
}
|
||||
|
||||
override fun onLoadingStarted() {
|
||||
bindingInfo.layoutError.isVisible = false
|
||||
bindingInfo.progressBar.show()
|
||||
binding.ssiv.recycle()
|
||||
}
|
||||
|
||||
override fun onProgressChanged(progress: Int) {
|
||||
if (progress in 0..100) {
|
||||
bindingInfo.progressBar.isIndeterminate = false
|
||||
bindingInfo.progressBar.setProgressCompat(progress, true)
|
||||
} else {
|
||||
bindingInfo.progressBar.isIndeterminate = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreviewReady(source: ImageSource) {
|
||||
binding.ssiv.setImage(source)
|
||||
}
|
||||
|
||||
override fun onImageReady(source: ImageSource) {
|
||||
binding.ssiv.setImage(source)
|
||||
}
|
||||
|
||||
override fun onImageShowing(settings: ReaderSettings, isPreview: Boolean) {
|
||||
override fun onReady() {
|
||||
binding.ssiv.maxScale = 2f * maxOf(
|
||||
binding.ssiv.width / binding.ssiv.sWidth.toFloat(),
|
||||
binding.ssiv.height / binding.ssiv.sHeight.toFloat(),
|
||||
@@ -137,31 +114,6 @@ open class PageHolder(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onImageShown() {
|
||||
bindingInfo.progressBar.hide()
|
||||
}
|
||||
|
||||
override fun onTrimMemory() {
|
||||
// TODO https://developer.android.com/topic/performance/memory
|
||||
}
|
||||
|
||||
final override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.button_retry -> delegate.retry(boundData?.toMangaPage() ?: return, isFromUser = true)
|
||||
R.id.button_error_details -> delegate.showErrorDetails(boundData?.url)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
bindingInfo.textViewError.text = e.getDisplayMessage(context.resources)
|
||||
bindingInfo.buttonRetry.setText(
|
||||
ExceptionResolver.getResolveStringId(e).ifZero { R.string.try_again },
|
||||
)
|
||||
bindingInfo.buttonErrorDetails.isVisible = e.isSerializable()
|
||||
bindingInfo.layoutError.isVisible = true
|
||||
bindingInfo.progressBar.hide()
|
||||
}
|
||||
|
||||
override fun onZoomIn() {
|
||||
scaleBy(1.2f)
|
||||
}
|
||||
@@ -170,6 +122,29 @@ open class PageHolder(
|
||||
scaleBy(0.8f)
|
||||
}
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
protected open fun applyRoundedCorners(insets: WindowInsets) {
|
||||
binding.textViewNumber.updateLayoutParams<FrameLayout.LayoutParams> {
|
||||
val baseMargin = context.resources.getDimensionPixelOffset(R.dimen.margin_small)
|
||||
val absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection)
|
||||
val corner = when {
|
||||
absoluteGravity and Gravity.LEFT == Gravity.LEFT -> {
|
||||
insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT)
|
||||
}
|
||||
|
||||
absoluteGravity and Gravity.RIGHT == Gravity.RIGHT -> {
|
||||
insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT)
|
||||
}
|
||||
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
setMargins(baseMargin + (corner?.radius ?: 0))
|
||||
}
|
||||
}
|
||||
|
||||
private fun scaleBy(factor: Float) {
|
||||
val ssiv = binding.ssiv
|
||||
val center = ssiv.getCenter() ?: return
|
||||
|
||||
@@ -13,22 +13,27 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
||||
class PagesAdapter(
|
||||
private val lifecycleOwner: LifecycleOwner,
|
||||
loader: PageLoader,
|
||||
settings: ReaderSettings,
|
||||
readerSettingsProducer: ReaderSettings.Producer,
|
||||
networkState: NetworkState,
|
||||
exceptionResolver: ExceptionResolver,
|
||||
) : BaseReaderAdapter<PageHolder>(loader, settings, networkState, exceptionResolver) {
|
||||
) : BaseReaderAdapter<PageHolder>(
|
||||
loader = loader,
|
||||
readerSettingsProducer = readerSettingsProducer,
|
||||
networkState = networkState,
|
||||
exceptionResolver = exceptionResolver,
|
||||
) {
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
loader: PageLoader,
|
||||
settings: ReaderSettings,
|
||||
readerSettingsProducer: ReaderSettings.Producer,
|
||||
networkState: NetworkState,
|
||||
exceptionResolver: ExceptionResolver,
|
||||
) = PageHolder(
|
||||
owner = lifecycleOwner,
|
||||
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||
loader = loader,
|
||||
settings = settings,
|
||||
readerSettingsProducer = readerSettingsProducer,
|
||||
networkState = networkState,
|
||||
exceptionResolver = exceptionResolver,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.koitharu.kotatsu.reader.ui.pager.vm
|
||||
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||
|
||||
sealed class PageState {
|
||||
|
||||
data object Empty : PageState()
|
||||
|
||||
data class Loading(
|
||||
val preview: ImageSource?,
|
||||
val progress: Int,
|
||||
) : PageState()
|
||||
|
||||
data class Loaded(
|
||||
val source: ImageSource,
|
||||
val isConverted: Boolean,
|
||||
) : PageState()
|
||||
|
||||
class Converting() : PageState()
|
||||
|
||||
data class Shown(
|
||||
val source: ImageSource,
|
||||
val isConverted: Boolean,
|
||||
) : PageState()
|
||||
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : PageState()
|
||||
|
||||
fun isFinalState(): Boolean = this is Error || this is Shown
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package org.koitharu.kotatsu.reader.ui.pager.vm
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.davemorrissey.labs.subscaleview.DefaultOnImageEventListener
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
import kotlinx.coroutines.withContext
|
||||
import okio.IOException
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.throttle
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||
|
||||
class PageViewModel(
|
||||
private val loader: PageLoader,
|
||||
val settingsProducer: ReaderSettings.Producer,
|
||||
private val networkState: NetworkState,
|
||||
private val exceptionResolver: ExceptionResolver,
|
||||
private val isWebtoon: Boolean,
|
||||
) : DefaultOnImageEventListener {
|
||||
|
||||
private val scope = loader.loaderScope + Dispatchers.Main.immediate
|
||||
private var job: Job? = null
|
||||
private var cachedBounds: Rect? = null
|
||||
|
||||
val state = MutableStateFlow<PageState>(PageState.Empty)
|
||||
|
||||
fun isLoading() = job?.isActive == true
|
||||
|
||||
fun onBind(page: MangaPage) {
|
||||
val prevJob = job
|
||||
job = scope.launch(Dispatchers.Default) {
|
||||
prevJob?.cancelAndJoin()
|
||||
doLoad(page, force = false)
|
||||
}
|
||||
}
|
||||
|
||||
fun retry(page: MangaPage, isFromUser: Boolean) {
|
||||
val prevJob = job
|
||||
job = scope.launch {
|
||||
prevJob?.cancelAndJoin()
|
||||
val e = (state.value as? PageState.Error)?.error
|
||||
if (e != null && ExceptionResolver.canResolve(e)) {
|
||||
if (isFromUser) {
|
||||
exceptionResolver.resolve(e)
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Default) {
|
||||
doLoad(page, force = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showErrorDetails(url: String?) {
|
||||
val e = (state.value as? PageState.Error)?.error ?: return
|
||||
exceptionResolver.showErrorDetails(e, url)
|
||||
}
|
||||
|
||||
fun onRecycle() {
|
||||
state.value = PageState.Empty
|
||||
cachedBounds = null
|
||||
job?.cancel()
|
||||
}
|
||||
|
||||
override fun onImageLoaded() {
|
||||
state.update { currentState ->
|
||||
if (currentState is PageState.Loaded) {
|
||||
PageState.Shown(currentState.source, currentState.isConverted)
|
||||
} else {
|
||||
currentState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onImageLoadError(e: Throwable) {
|
||||
e.printStackTraceDebug()
|
||||
|
||||
state.update { currentState ->
|
||||
if (currentState is PageState.Loaded) {
|
||||
val uri = (currentState.source as? ImageSource.Uri)?.uri
|
||||
if (!currentState.isConverted && uri != null && e is IOException) {
|
||||
tryConvert(uri, e)
|
||||
PageState.Converting()
|
||||
} else {
|
||||
PageState.Error(e)
|
||||
}
|
||||
} else {
|
||||
currentState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun tryConvert(uri: Uri, e: Exception) {
|
||||
val prevJob = job
|
||||
job = scope.launch(Dispatchers.Default) {
|
||||
prevJob?.join()
|
||||
state.value = PageState.Converting()
|
||||
try {
|
||||
val newUri = loader.convertBimap(uri)
|
||||
cachedBounds = if (settingsProducer.value.isPagesCropEnabled(isWebtoon)) {
|
||||
loader.getTrimmedBounds(newUri)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
state.value = PageState.Loaded(uri.toImageSource(cachedBounds), isConverted = true)
|
||||
} catch (ce: CancellationException) {
|
||||
throw ce
|
||||
} catch (e2: Throwable) {
|
||||
e2.printStackTrace()
|
||||
e.addSuppressed(e2)
|
||||
state.value = PageState.Error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private suspend fun doLoad(data: MangaPage, force: Boolean) = coroutineScope {
|
||||
state.value = PageState.Loading(null, -1)
|
||||
val previewJob = launch {
|
||||
val preview = loader.loadPreview(data) ?: return@launch
|
||||
state.update {
|
||||
if (it is PageState.Loading) it.copy(preview = preview) else it
|
||||
}
|
||||
}
|
||||
try {
|
||||
val task = loader.loadPageAsync(data, force)
|
||||
val progressObserver = observeProgress(this, task.progressAsFlow())
|
||||
val uri = task.await()
|
||||
progressObserver.cancelAndJoin()
|
||||
previewJob.cancel()
|
||||
cachedBounds = if (settingsProducer.value.isPagesCropEnabled(isWebtoon)) {
|
||||
loader.getTrimmedBounds(uri)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
state.value = PageState.Loaded(uri.toImageSource(cachedBounds), isConverted = false)
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTraceDebug()
|
||||
state.value = PageState.Error(e)
|
||||
if (e is IOException && !networkState.value) {
|
||||
networkState.awaitForConnection()
|
||||
retry(data, isFromUser = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeProgress(scope: CoroutineScope, progress: Flow<Float>) = progress
|
||||
.throttle(250)
|
||||
.onEach {
|
||||
val progressValue = (100 * it).toInt()
|
||||
state.update { currentState ->
|
||||
if (currentState is PageState.Loading) {
|
||||
currentState.copy(progress = progressValue)
|
||||
} else {
|
||||
currentState
|
||||
}
|
||||
}
|
||||
}.launchIn(scope)
|
||||
|
||||
private fun Uri.toImageSource(bounds: Rect?): ImageSource {
|
||||
val source = ImageSource.uri(this)
|
||||
return if (bounds != null) {
|
||||
source.region(bounds)
|
||||
} else {
|
||||
source
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,15 +13,15 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
||||
class WebtoonAdapter(
|
||||
private val lifecycleOwner: LifecycleOwner,
|
||||
loader: PageLoader,
|
||||
settings: ReaderSettings,
|
||||
readerSettingsProducer: ReaderSettings.Producer,
|
||||
networkState: NetworkState,
|
||||
exceptionResolver: ExceptionResolver,
|
||||
) : BaseReaderAdapter<WebtoonHolder>(loader, settings, networkState, exceptionResolver) {
|
||||
) : BaseReaderAdapter<WebtoonHolder>(loader, readerSettingsProducer, networkState, exceptionResolver) {
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
loader: PageLoader,
|
||||
settings: ReaderSettings,
|
||||
readerSettingsProducer: ReaderSettings.Producer,
|
||||
networkState: NetworkState,
|
||||
exceptionResolver: ExceptionResolver,
|
||||
) = WebtoonHolder(
|
||||
@@ -32,7 +32,7 @@ class WebtoonAdapter(
|
||||
false,
|
||||
),
|
||||
loader = loader,
|
||||
settings = settings,
|
||||
readerSettingsProducer = readerSettingsProducer,
|
||||
networkState = networkState,
|
||||
exceptionResolver = exceptionResolver,
|
||||
)
|
||||
|
||||
@@ -1,101 +1,39 @@
|
||||
package org.koitharu.kotatsu.reader.ui.pager.webtoon
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.core.util.GoneOnInvisibleListener
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.isSerializable
|
||||
import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding
|
||||
import org.koitharu.kotatsu.parsers.util.ifZero
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||
import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
|
||||
class WebtoonHolder(
|
||||
owner: LifecycleOwner,
|
||||
binding: ItemPageWebtoonBinding,
|
||||
loader: PageLoader,
|
||||
settings: ReaderSettings,
|
||||
readerSettingsProducer: ReaderSettings.Producer,
|
||||
networkState: NetworkState,
|
||||
exceptionResolver: ExceptionResolver,
|
||||
) : BasePageHolder<ItemPageWebtoonBinding>(binding, loader, settings, networkState, exceptionResolver, owner),
|
||||
View.OnClickListener {
|
||||
) : BasePageHolder<ItemPageWebtoonBinding>(
|
||||
binding = binding,
|
||||
loader = loader,
|
||||
readerSettingsProducer = readerSettingsProducer,
|
||||
networkState = networkState,
|
||||
exceptionResolver = exceptionResolver,
|
||||
lifecycleOwner = owner,
|
||||
) {
|
||||
|
||||
override val ssiv = binding.ssiv
|
||||
|
||||
private var scrollToRestore = 0
|
||||
private val goneOnInvisibleListener = GoneOnInvisibleListener(bindingInfo.progressBar)
|
||||
|
||||
init {
|
||||
binding.ssiv.bindToLifecycle(owner)
|
||||
binding.ssiv.addOnImageEventListener(delegate)
|
||||
bindingInfo.buttonRetry.setOnClickListener(this)
|
||||
bindingInfo.buttonErrorDetails.setOnClickListener(this)
|
||||
bindingInfo.progressBar.setVisibilityAfterHide(View.GONE)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
binding.ssiv.applyDownSampling(isForeground = true)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
binding.ssiv.applyDownSampling(isForeground = false)
|
||||
}
|
||||
|
||||
override fun onConfigChanged() {
|
||||
super.onConfigChanged()
|
||||
if (settings.applyBitmapConfig(binding.ssiv)) {
|
||||
delegate.reload()
|
||||
}
|
||||
binding.ssiv.applyDownSampling(isResumed())
|
||||
}
|
||||
|
||||
override fun onBind(data: ReaderPage) {
|
||||
delegate.onBind(data.toMangaPage())
|
||||
}
|
||||
|
||||
override fun onRecycled() {
|
||||
super.onRecycled()
|
||||
binding.ssiv.recycle()
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
goneOnInvisibleListener.attach()
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
goneOnInvisibleListener.detach()
|
||||
}
|
||||
|
||||
override fun onLoadingStarted() {
|
||||
bindingInfo.layoutError.isVisible = false
|
||||
bindingInfo.progressBar.show()
|
||||
binding.ssiv.recycle()
|
||||
}
|
||||
|
||||
override fun onProgressChanged(progress: Int) {
|
||||
if (progress in 0..100) {
|
||||
bindingInfo.progressBar.isIndeterminate = false
|
||||
bindingInfo.progressBar.setProgressCompat(progress, true)
|
||||
} else {
|
||||
bindingInfo.progressBar.isIndeterminate = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreviewReady(source: ImageSource) = Unit
|
||||
|
||||
override fun onImageReady(source: ImageSource) {
|
||||
binding.ssiv.setImage(source)
|
||||
}
|
||||
|
||||
override fun onImageShowing(settings: ReaderSettings, isPreview: Boolean) {
|
||||
override fun onReady() {
|
||||
binding.ssiv.colorFilter = settings.colorFilter?.toColorFilter()
|
||||
with(binding.ssiv) {
|
||||
scrollTo(
|
||||
@@ -109,31 +47,6 @@ class WebtoonHolder(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onImageShown() {
|
||||
bindingInfo.progressBar.hide()
|
||||
}
|
||||
|
||||
override fun onTrimMemory() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.button_retry -> delegate.retry(boundData?.toMangaPage() ?: return, isFromUser = true)
|
||||
R.id.button_error_details -> delegate.showErrorDetails(boundData?.url)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
bindingInfo.textViewError.text = e.getDisplayMessage(context.resources)
|
||||
bindingInfo.buttonRetry.setText(
|
||||
ExceptionResolver.getResolveStringId(e).ifZero { R.string.try_again },
|
||||
)
|
||||
bindingInfo.buttonErrorDetails.isVisible = e.isSerializable()
|
||||
bindingInfo.layoutError.isVisible = true
|
||||
bindingInfo.progressBar.hide()
|
||||
}
|
||||
|
||||
fun getScrollY() = binding.ssiv.getScroll()
|
||||
|
||||
fun restoreScroll(scroll: Int) {
|
||||
|
||||
@@ -67,7 +67,7 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
||||
rv.addItemDecoration(WebtoonGapsDecoration())
|
||||
}
|
||||
}
|
||||
viewModel.readerSettings.observe(viewLifecycleOwner) {
|
||||
viewModel.readerSettingsProducer.observe(viewLifecycleOwner) {
|
||||
it.applyBackground(binding.root)
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,7 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
||||
override fun onCreateAdapter() = WebtoonAdapter(
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
loader = pageLoader,
|
||||
settings = viewModel.readerSettings,
|
||||
readerSettingsProducer = viewModel.readerSettingsProducer,
|
||||
networkState = networkState,
|
||||
exceptionResolver = exceptionResolver,
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
||||
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
|
||||
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
|
||||
import org.koitharu.kotatsu.core.util.ext.getCauseUrl
|
||||
import org.koitharu.kotatsu.core.util.ext.isHttpUrl
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||
@@ -87,15 +88,15 @@ class RemoteListFragment : MangaListFragment(), FilterCoordinator.Owner {
|
||||
}
|
||||
|
||||
private fun openInBrowser(url: String?) {
|
||||
if (url.isNullOrEmpty()) {
|
||||
Snackbar.make(requireViewBinding().recyclerView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT)
|
||||
.show()
|
||||
} else {
|
||||
if (url?.isHttpUrl() == true) {
|
||||
router.openBrowser(
|
||||
url = url,
|
||||
source = viewModel.source,
|
||||
title = viewModel.source.getTitle(requireContext()),
|
||||
)
|
||||
} else {
|
||||
Snackbar.make(requireViewBinding().recyclerView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.dropWhile
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.joinAll
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -67,7 +68,7 @@ class SearchViewModel @Inject constructor(
|
||||
|
||||
val list: StateFlow<List<ListModel>> = combine(
|
||||
results,
|
||||
isLoading,
|
||||
isLoading.dropWhile { !it },
|
||||
includeDisabledSources,
|
||||
) { list, loading, includeDisabled ->
|
||||
when {
|
||||
|
||||
@@ -34,7 +34,6 @@ import org.koitharu.kotatsu.settings.search.SettingsItem
|
||||
import org.koitharu.kotatsu.settings.search.SettingsSearchFragment
|
||||
import org.koitharu.kotatsu.settings.search.SettingsSearchViewModel
|
||||
import org.koitharu.kotatsu.settings.sources.SourceSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.sources.SourceSettingsFragment.Companion.EXTRA_SOURCE
|
||||
import org.koitharu.kotatsu.settings.sources.SourcesSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.sources.manage.SourcesManageFragment
|
||||
import org.koitharu.kotatsu.settings.tracker.TrackerSettingsFragment
|
||||
@@ -57,7 +56,7 @@ class SettingsActivity :
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivitySettingsBinding.inflate(layoutInflater))
|
||||
setDisplayHomeAsUp(true, false)
|
||||
setDisplayHomeAsUp(isEnabled = true, showUpAsClose = false)
|
||||
val fm = supportFragmentManager
|
||||
val currentFragment = fm.findFragmentById(R.id.container)
|
||||
if (currentFragment == null || (isMasterDetails && currentFragment is RootSettingsFragment)) {
|
||||
@@ -151,7 +150,7 @@ class SettingsActivity :
|
||||
AppRouter.ACTION_PROXY -> ProxySettingsFragment()
|
||||
AppRouter.ACTION_MANAGE_DOWNLOADS -> DownloadsSettingsFragment()
|
||||
AppRouter.ACTION_SOURCE -> SourceSettingsFragment.newInstance(
|
||||
MangaSource(intent.getStringExtra(EXTRA_SOURCE)),
|
||||
MangaSource(intent.getStringExtra(AppRouter.KEY_SOURCE)),
|
||||
)
|
||||
|
||||
AppRouter.ACTION_MANAGE_SOURCES -> SourcesManageFragment()
|
||||
|
||||
@@ -37,7 +37,7 @@ import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.util.EnumSet
|
||||
import javax.inject.Inject
|
||||
import com.google.android.material.R as materialR
|
||||
import androidx.appcompat.R as appcompatR
|
||||
|
||||
@AndroidEntryPoint
|
||||
class RestoreService : CoroutineIntentService() {
|
||||
@@ -219,7 +219,7 @@ class RestoreService : CoroutineIntentService() {
|
||||
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
|
||||
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
|
||||
.addAction(
|
||||
materialR.drawable.material_ic_clear_black_24dp,
|
||||
appcompatR.drawable.abc_ic_clear_material,
|
||||
applicationContext.getString(android.R.string.cancel),
|
||||
getCancelIntent(),
|
||||
).build()
|
||||
|
||||
@@ -9,6 +9,7 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||
import org.koitharu.kotatsu.core.model.getTitle
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.parser.EmptyMangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.ParserMangaRepository
|
||||
@@ -121,10 +122,8 @@ class SourceSettingsFragment : BasePreferenceFragment(0), Preference.OnPreferenc
|
||||
private const val KEY_AUTH = "auth"
|
||||
private const val KEY_ENABLE = "enable"
|
||||
|
||||
const val EXTRA_SOURCE = "source"
|
||||
|
||||
fun newInstance(source: MangaSource) = SourceSettingsFragment().withArgs(1) {
|
||||
putString(EXTRA_SOURCE, source.name)
|
||||
putString(AppRouter.KEY_SOURCE, source.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import okhttp3.HttpUrl
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||
import org.koitharu.kotatsu.core.parser.CachingMangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
@@ -32,7 +33,7 @@ class SourceSettingsViewModel @Inject constructor(
|
||||
private val mangaSourcesRepository: MangaSourcesRepository,
|
||||
) : BaseViewModel(), SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
val source = MangaSource(savedStateHandle.get<String>(SourceSettingsFragment.EXTRA_SOURCE))
|
||||
val source = MangaSource(savedStateHandle.get<String>(AppRouter.KEY_SOURCE))
|
||||
val repository = mangaRepositoryFactory.create(source)
|
||||
|
||||
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||
|
||||
@@ -14,46 +14,34 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.browser.BaseBrowserActivity
|
||||
import org.koitharu.kotatsu.browser.BrowserCallback
|
||||
import org.koitharu.kotatsu.browser.BrowserClient
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.model.getTitle
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.ParserMangaRepository
|
||||
import org.koitharu.kotatsu.core.util.ext.configureForParser
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.settings.sources.SourceSettingsFragment.Companion.EXTRA_SOURCE
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SourceAuthActivity : BaseBrowserActivity(), BrowserCallback {
|
||||
|
||||
@Inject
|
||||
lateinit var mangaRepositoryFactory: MangaRepository.Factory
|
||||
|
||||
private lateinit var authProvider: MangaParserAuthProvider
|
||||
|
||||
override fun onCreate2(savedInstanceState: Bundle?) {
|
||||
val source = MangaSource(intent?.getStringExtra(EXTRA_SOURCE))
|
||||
if (source !is MangaParserSource) {
|
||||
override fun onCreate2(savedInstanceState: Bundle?, source: MangaSource, repository: ParserMangaRepository?) {
|
||||
if (repository == null) {
|
||||
finishAfterTransition()
|
||||
return
|
||||
}
|
||||
val repository = mangaRepositoryFactory.create(source) as? ParserMangaRepository
|
||||
authProvider = (repository)?.getAuthProvider() ?: run {
|
||||
authProvider = repository.getAuthProvider() ?: run {
|
||||
Toast.makeText(
|
||||
this,
|
||||
getString(R.string.auth_not_supported_by, source.title),
|
||||
getString(R.string.auth_not_supported_by, source.getTitle(this)),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
finishAfterTransition()
|
||||
return
|
||||
}
|
||||
setDisplayHomeAsUp(true, true)
|
||||
viewBinding.webView.configureForParser(repository.getRequestHeaders()[CommonHeaders.USER_AGENT])
|
||||
viewBinding.webView.webViewClient = BrowserClient(proxyProvider, this)
|
||||
setDisplayHomeAsUp(isEnabled = true, showUpAsClose = true)
|
||||
viewBinding.webView.webViewClient = BrowserClient(this)
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
proxyProvider.applyWebViewConfig()
|
||||
@@ -63,7 +51,7 @@ class SourceAuthActivity : BaseBrowserActivity(), BrowserCallback {
|
||||
if (savedInstanceState == null) {
|
||||
val url = authProvider.authUrl
|
||||
onTitleChanged(
|
||||
source.title,
|
||||
source.getTitle(this@SourceAuthActivity),
|
||||
getString(R.string.loading_),
|
||||
)
|
||||
viewBinding.webView.loadUrl(url)
|
||||
@@ -92,13 +80,10 @@ class SourceAuthActivity : BaseBrowserActivity(), BrowserCallback {
|
||||
}
|
||||
|
||||
class Contract : ActivityResultContract<MangaSource, Boolean>() {
|
||||
override fun createIntent(context: Context, input: MangaSource): Intent {
|
||||
return AppRouter.sourceAuthIntent(context, input)
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): Boolean {
|
||||
return resultCode == RESULT_OK
|
||||
}
|
||||
override fun createIntent(context: Context, input: MangaSource) = AppRouter.sourceAuthIntent(context, input)
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?) = resultCode == RESULT_OK
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -17,6 +17,7 @@ import coil3.ImageLoader
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
@@ -117,7 +118,7 @@ class SourcesManageFragment :
|
||||
override fun onItemSettingsClick(item: SourceConfigItem.SourceItem) {
|
||||
(activity as? SettingsActivity)?.openFragment(
|
||||
fragmentClass = SourceSettingsFragment::class.java,
|
||||
args = Bundle(1).apply { putString(SourceSettingsFragment.EXTRA_SOURCE, item.source.name) },
|
||||
args = Bundle(1).apply { putString(AppRouter.KEY_SOURCE, item.source.name) },
|
||||
isFromRoot = false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.pow
|
||||
import kotlin.random.Random
|
||||
import com.google.android.material.R as materialR
|
||||
import androidx.appcompat.R as appcompatR
|
||||
|
||||
@HiltWorker
|
||||
class SuggestionsWorker @AssistedInject constructor(
|
||||
@@ -137,7 +137,7 @@ class SuggestionsWorker @AssistedInject constructor(
|
||||
false,
|
||||
),
|
||||
).addAction(
|
||||
materialR.drawable.material_ic_clear_black_24dp,
|
||||
appcompatR.drawable.abc_ic_clear_material,
|
||||
applicationContext.getString(android.R.string.cancel),
|
||||
workManager.createCancelPendingIntent(id),
|
||||
)
|
||||
|
||||
@@ -70,7 +70,7 @@ import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
import kotlin.math.roundToInt
|
||||
import com.google.android.material.R as materialR
|
||||
import androidx.appcompat.R as appcompatR
|
||||
|
||||
@HiltWorker
|
||||
class TrackWorker @AssistedInject constructor(
|
||||
@@ -215,7 +215,7 @@ class TrackWorker @AssistedInject constructor(
|
||||
),
|
||||
)
|
||||
addAction(
|
||||
materialR.drawable.material_ic_clear_black_24dp,
|
||||
appcompatR.drawable.abc_ic_clear_material,
|
||||
applicationContext.getString(android.R.string.cancel),
|
||||
workManager.createCancelPendingIntent(id),
|
||||
)
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|bottom"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_margin="@dimen/margin_small"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textColor="?android:textColorTertiary"
|
||||
|
||||
@@ -5,15 +5,26 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:parentTag="android.widget.FrameLayout">
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
app:hideAnimationBehavior="escape"
|
||||
app:trackCornerRadius="0dp"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true"
|
||||
android:max="100"
|
||||
app:hideAnimationBehavior="escape"
|
||||
app:showAnimationBehavior="none" />
|
||||
android:layout_marginHorizontal="60dp"
|
||||
android:gravity="center"
|
||||
android:textAppearance="?textAppearanceBodyLarge"
|
||||
tools:text="72%" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_error"
|
||||
@@ -22,10 +33,12 @@
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="60dp"
|
||||
android:layout_marginEnd="60dp"
|
||||
android:background="@drawable/bg_card"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/screen_padding"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
tools:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_error"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="favourites">পছন্দের গুলো</string>
|
||||
<string name="history">ইতিহাস</string>
|
||||
<string name="favourites">পছন্দের</string>
|
||||
<string name="history">সম্প্রতি দেখা</string>
|
||||
<string name="local_storage">লোকাল স্টোরেজ</string>
|
||||
<string name="_continue">চালিয়ে যান</string>
|
||||
<string name="clear_thumbs_cache">থাম্বনেইল ক্যাচ সাফ করুন</string>
|
||||
@@ -12,20 +12,20 @@
|
||||
<string name="app_update_available">অ্যাপের নতুন ভার্সন পাওয়া গেছে</string>
|
||||
<string name="open_in_browser">ব্রাউজারে খুলুন</string>
|
||||
<string name="error_occurred">কিছু একটা সমস্যা হয়েছে</string>
|
||||
<string name="details">খুঁটিনাটি</string>
|
||||
<string name="chapters">অধ্যায়</string>
|
||||
<string name="details">বিস্তারিত</string>
|
||||
<string name="chapters">চ্যাপ্টার</string>
|
||||
<string name="list">তালিকা</string>
|
||||
<string name="detailed_list">পুঙ্খানুপুঙ্খ তালিকা</string>
|
||||
<string name="detailed_list">বিস্তারিত তালিকা</string>
|
||||
<string name="grid">গ্রিড</string>
|
||||
<string name="settings">সেটিং সমূহ</string>
|
||||
<string name="settings">সেটিংস</string>
|
||||
<string name="loading_">লোড হচ্ছে…</string>
|
||||
<string name="close">বন্ধ</string>
|
||||
<string name="try_again">আবারো চেষ্টা করুন</string>
|
||||
<string name="try_again">আবার চেষ্টা করুন</string>
|
||||
<string name="clear_history">ইতিহাস মুছুন</string>
|
||||
<string name="computing_">প্রস্তুত হচ্ছে…</string>
|
||||
<string name="chapter_d_of_d">%2$d টির মধ্যে %1$d তম পর্ব</string>
|
||||
<string name="chapter_d_of_d">%2$d এর্ %1$d তম অধ্যায়</string>
|
||||
<string name="nothing_found">কিছু পাওয়া যায়নি</string>
|
||||
<string name="history_is_empty">কোনো ইতিহাস লেখা হয়নি</string>
|
||||
<string name="history_is_empty">কোনো ইতিহাস নেই</string>
|
||||
<string name="read">পড়ুন</string>
|
||||
<string name="add_to_favourites">পছন্দ করুন</string>
|
||||
<string name="text_file_not_supported">একটি ZIP অথবা CBZ ফাইল নিন</string>
|
||||
@@ -38,7 +38,7 @@
|
||||
<string name="operation_not_supported">এই কাজটি করা সম্ভব নয়</string>
|
||||
<string name="switch_pages">পেজ পাল্টান</string>
|
||||
<string name="search_history_cleared">সাফ করা হয়েছে</string>
|
||||
<string name="network_error">নেটওয়ার্কে সমস্যা</string>
|
||||
<string name="network_error">নেটওয়ার্কে ত্রুটি</string>
|
||||
<string name="remote_sources">মানগা সোর্স সমূহ</string>
|
||||
<string name="you_have_not_favourites_yet">এখনো কিছু পছন্দ হয়নি</string>
|
||||
<string name="add_new_category">নতুন বিভাগ</string>
|
||||
@@ -176,4 +176,7 @@
|
||||
<string name="theme_name_dynamic">ডাইনামিক</string>
|
||||
<string name="theme_name_miku">মিকু</string>
|
||||
<string name="data_not_restored">ডেটা পুনরুদ্ধার করা হয়নি</string>
|
||||
<string name="incognito_mode_hint">আপনার পড়ার অগ্রগতি সেভ হবে না</string>
|
||||
<string name="volume_">আওয়াজ%d</string>
|
||||
<string name="volume_unknown">অজানা ভলিউম</string>
|
||||
</resources>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<string name="history_is_empty">Zatím žádná historie</string>
|
||||
<string name="read">Číst</string>
|
||||
<string name="you_have_not_favourites_yet">Zatím žádné oblíbené</string>
|
||||
<string name="add_to_favourites">Oblíbit toto</string>
|
||||
<string name="add_to_favourites">Přidat do oblíbených</string>
|
||||
<string name="add">Přidat</string>
|
||||
<string name="share">Sdílet</string>
|
||||
<string name="create_shortcut">Vytvořit zkratku…</string>
|
||||
@@ -791,4 +791,15 @@
|
||||
<string name="error_disclaimer_app_outdated">Vypadá to, že vaše verze Kotatsu je zastaralá. Prosíme, nainstalujte nejnovější verzi pro získání všech dostupných oprav chyb.</string>
|
||||
<string name="disable_captcha_notifications">Vypnout oznámení o captcha</string>
|
||||
<string name="disable_captcha_notifications_summary">Nebudete dostávat oznámení o řešení CAPTCHA pro tento zdroj, ale to může vést k rozbití operací na pozadí (hledání nových kapitol, získávání doporučení atd)</string>
|
||||
<string name="tags_warnings">Zvýraznit nebezpečné žánry</string>
|
||||
<string name="tags_warnings_summary">Zvýraznit žánry, které mohou být nevhodné pro většinu uživatelů</string>
|
||||
<string name="nsfw_16">16+</string>
|
||||
<string name="link_to_manga_in_app">Odkaz na mangau v Kotatsu</string>
|
||||
<string name="clear_browser_data">Vyčistit data prohlížeče</string>
|
||||
<string name="exclude_nsfw_from_suggestions_summary">Dospělá manga nebude zobrazena v návrzích. Tato funkce může být nepřesná s některými zdroji</string>
|
||||
<string name="include_disabled_sources">Zahrnout vypnuté zdroje</string>
|
||||
<string name="suggestions_disabled_sources_summary">Zobrazit návrhy ze všech zdrojů mangy, včetně vypnutých</string>
|
||||
<string name="clear_browser_data_summary">Vyčistit data prohlížeče, např. cache a cookies. Upozornění: Všude budete odhlášeni a budte muset znovu řešit captcha</string>
|
||||
<string name="global_search">Globální vyhledávání</string>
|
||||
<string name="search_everywhere">Hledat všude</string>
|
||||
</resources>
|
||||
|
||||
@@ -671,7 +671,7 @@
|
||||
<string name="seconds_short">s %d</string>
|
||||
<string name="minutes_seconds_short">%1$d m %2$d s</string>
|
||||
<string name="sfw">SFW</string>
|
||||
<string name="not_in_favorites">Wala sa paborito mo</string>
|
||||
<string name="not_in_favorites">Wala sa mga paborito</string>
|
||||
<string name="unpopular">Hindi sikat</string>
|
||||
<string name="low_rating">Mababa ang rating</string>
|
||||
<string name="sort_order_asc">Pataas</string>
|
||||
@@ -793,4 +793,16 @@
|
||||
<string name="unnamed_chapter">Walang pangalan na kabanata</string>
|
||||
<string name="error_disclaimer_app_outdated">Mukhang luma na ang bersyon mo ng Kotatsu. Mangyaring i-install ang pinakabagong bersyon upang makuha ang lahat ng magagamit na mga pag-aayos.</string>
|
||||
<string name="error_disclaimer_report">Maaari kang magsumite ng ulat ng bug sa mga developer. Makakatulong ito sa amin na magsiyasat at ayusin ang isyu.</string>
|
||||
<string name="tags_warnings">I-highlight ang mga mapanganib na genre</string>
|
||||
<string name="nsfw_16">16+</string>
|
||||
<string name="clear_browser_data">Linisin ang data ng browser</string>
|
||||
<string name="no_write_permission_to_file">Walang pahintulot na magsulat ng file</string>
|
||||
<string name="exclude_nsfw_from_suggestions_summary">Ang manga pang-matanda ay hindi ipapakita sa mga mungkahi. Maaaring gumana nang hindi tumpak ang opsyong ito sa ilang source</string>
|
||||
<string name="include_disabled_sources">Isama ang mga hindi pinagana na source</string>
|
||||
<string name="suggestions_disabled_sources_summary">Magpakita ng mga mungkahi mula sa lahat ng manga source, kabilang ang mga hindi napagana</string>
|
||||
<string name="tags_warnings_summary">I-highlight ang mga genre na maaaring hindi naaangkop para sa karamihan ng mga user</string>
|
||||
<string name="link_to_manga_in_app">Link sa manga sa Kotatsu</string>
|
||||
<string name="simple">Pinasimple</string>
|
||||
<string name="link_to_manga_on_s">Link sa manga sa %s</string>
|
||||
<string name="clear_browser_data_summary">Linisin ang data ng browser tulad ng cache at mga cookie. Babala: Ang awtorisasyon sa mga source ng manga ay maaaring maging di-balido</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="local_storage">Penyimpanan lokal</string>
|
||||
<string name="favourites">Favorit</string>
|
||||
<string name="favourites">Banyak disukai</string>
|
||||
<string name="history">Riwayat</string>
|
||||
<string name="error_occurred">Terjadi kesalahan</string>
|
||||
<string name="network_error">Kesalahan jaringan</string>
|
||||
@@ -18,7 +18,7 @@
|
||||
<string name="nothing_found">Tidak ditemukan</string>
|
||||
<string name="history_is_empty">Riwayat kosong</string>
|
||||
<string name="read">Baca</string>
|
||||
<string name="you_have_not_favourites_yet">Belum ada favorit</string>
|
||||
<string name="you_have_not_favourites_yet">Belum ada yang disukai</string>
|
||||
<string name="add_to_favourites">Buat favorit</string>
|
||||
<string name="add_new_category">Kategori baru</string>
|
||||
<string name="add">Tambah</string>
|
||||
@@ -66,7 +66,7 @@
|
||||
<string name="reader_settings">Pengaturan pembaca</string>
|
||||
<string name="switch_pages">Ganti halaman</string>
|
||||
<string name="chapters">Bab</string>
|
||||
<string name="list">Daftari</string>
|
||||
<string name="list">Daftar</string>
|
||||
<string name="detailed_list">Daftar rinci</string>
|
||||
<string name="webtoon">Webtoon</string>
|
||||
<string name="read_mode">Mode baca</string>
|
||||
@@ -204,7 +204,7 @@
|
||||
<string name="create_category">Kategori baru</string>
|
||||
<string name="light_indicator">Indikator LED</string>
|
||||
<string name="vibration">Getaran</string>
|
||||
<string name="favourites_categories">Kategori favorit</string>
|
||||
<string name="favourites_categories">Kategori disukai</string>
|
||||
<string name="remove_category">Hapus</string>
|
||||
<string name="clear_updates_feed">Bersihkan aliran pembaruan</string>
|
||||
<string name="right_to_left">Kanan-ke-kiri</string>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<string name="search_manga">Cerca manga</string>
|
||||
<string name="search">Cerca</string>
|
||||
<string name="share_s">Condividi %s</string>
|
||||
<string name="create_shortcut">Crea una scorciatoia…</string>
|
||||
<string name="create_shortcut">Crea scorciatoia</string>
|
||||
<string name="share">Condividi</string>
|
||||
<string name="save">Salva</string>
|
||||
<string name="add">Aggiungi</string>
|
||||
@@ -249,7 +249,7 @@
|
||||
<string name="send">Invia</string>
|
||||
<string name="disable_all">Disabilita tutto</string>
|
||||
<string name="use_fingerprint">Usa le impronte digitali se disponibili</string>
|
||||
<string name="appwidget_shelf_description">Manga dai preferiti</string>
|
||||
<string name="appwidget_shelf_description">Manga dai tuoi preferiti</string>
|
||||
<string name="appwidget_recent_description">I manga letti di recente</string>
|
||||
<string name="report">Segnala</string>
|
||||
<string name="tracking">Monitoraggio</string>
|
||||
@@ -669,7 +669,7 @@
|
||||
<string name="too_many_requests_message_retry">Troppe richieste. Riprova dopo %s</string>
|
||||
<string name="skip_all">Salta tutto</string>
|
||||
<string name="stuck">Bloccato</string>
|
||||
<string name="not_in_favorites">Non nei preferiti</string>
|
||||
<string name="not_in_favorites">Non presente nei preferiti</string>
|
||||
<string name="plugin_incompatible">Plugin incompatibile o errore interno. Assicurati di utilizzare l\'ultima versione del plugin e di Kotatsu</string>
|
||||
<string name="updated_long_ago">Aggiornato molto tempo fa</string>
|
||||
<string name="unpopular">Impopolare</string>
|
||||
@@ -804,4 +804,6 @@
|
||||
<string name="include_disabled_sources">Includi fonti disabilitate</string>
|
||||
<string name="suggestions_disabled_sources_summary">Mostra suggerimenti da tutte le fonti manga, incluse quelle disabilite</string>
|
||||
<string name="exclude_nsfw_from_suggestions_summary">I manga per adulti non verranno mostrati nei suggerimenti. Questa opzione potrebbe non funzionare accuratamente con alcune fonti</string>
|
||||
<string name="tags_warnings">Evidenzia i generi pericolosi</string>
|
||||
<string name="tags_warnings_summary">Evidenzia i generi che potrebbero essere inappropriati per la maggior parte degli utenti</string>
|
||||
</resources>
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
<string name="cannot_find_available_storage">Sem espaço de armazenamento disponível</string>
|
||||
<string name="other_storage">Outro armazenamento</string>
|
||||
<string name="done">Feito</string>
|
||||
<string name="all_favourites">Todas as favoritas</string>
|
||||
<string name="all_favourites">Todos os favoritos</string>
|
||||
<string name="favourites_category_empty">Categoria vazia</string>
|
||||
<string name="read_later">Ler depois</string>
|
||||
<string name="updates">Atualizações</string>
|
||||
@@ -158,7 +158,7 @@
|
||||
<string name="show_pages_numbers">Páginas numeradas</string>
|
||||
<string name="screenshots_policy">Política de capturas de tela</string>
|
||||
<string name="screenshots_allow">Permitir</string>
|
||||
<string name="screenshots_block_nsfw">Bloquear no NSFW</string>
|
||||
<string name="screenshots_block_nsfw">Bloquear conteúdo NSFW</string>
|
||||
<string name="screenshots_block_all">Nunca permitir</string>
|
||||
<string name="suggestions">Sugestões</string>
|
||||
<string name="suggestions_enable">Ativar sugestões</string>
|
||||
@@ -269,7 +269,7 @@
|
||||
<string name="clear_cookies_summary">Pode ajudar em caso de problemas. Todas as autorizações serão invalidadas</string>
|
||||
<string name="show_reading_indicators">Mostrar indicadores de progresso de leitura</string>
|
||||
<string name="data_deletion">Exclusão de dados</string>
|
||||
<string name="show_reading_indicators_summary">Mostrar porcentagem lida no histórico e em favoritas</string>
|
||||
<string name="show_reading_indicators_summary">Mostrar porcentagem de leitura no histórico e nos favoritos</string>
|
||||
<string name="exclude_nsfw_from_history_summary">Obras marcadas como NSFW nunca serão adicionadas ao histórico e seu progresso não será salvo</string>
|
||||
<string name="show_all">Mostrar tudo</string>
|
||||
<string name="clear_all_history">Limpar todo o histórico</string>
|
||||
@@ -300,7 +300,7 @@
|
||||
<string name="other_cache">Outro cache</string>
|
||||
<string name="storage_usage">Uso do armazenamento</string>
|
||||
<string name="available">Disponível</string>
|
||||
<string name="removed_from_favourites">Removida das favoritas</string>
|
||||
<string name="removed_from_favourites">Removido dos favoritos</string>
|
||||
<string name="options">Opções</string>
|
||||
<string name="incognito_mode">Modo anônimo</string>
|
||||
<string name="automatic_scroll">Rolagem automática</string>
|
||||
@@ -590,7 +590,7 @@
|
||||
<string name="chapters_grid_view">Exibição em grade</string>
|
||||
<string name="alternatives">Alternativas</string>
|
||||
<string name="manga_migration">Migração de obra</string>
|
||||
<string name="migration_completed">Migração completada</string>
|
||||
<string name="migration_completed">Migração concluída</string>
|
||||
<string name="chapters_deleted_pattern">%1$s removido, %2$s limpo</string>
|
||||
<string name="delete_read_chapters">Apagar capítulos lidos</string>
|
||||
<string name="delete_read_chapters_summary">Apagar capítulos lidos do armazenamento local para liberar espaço</string>
|
||||
@@ -617,8 +617,8 @@
|
||||
<string name="show_updated">Mostrar atualização</string>
|
||||
<string name="disable_connectivity_check">Desativar a verificação de conectividade</string>
|
||||
<string name="disable_connectivity_check_summary">Ignore a verificação de conectividade caso tenha problemas com ela (por exemplo, entrar no modo off-line mesmo que a rede esteja conectada)</string>
|
||||
<string name="webtoon_gaps">Lacunas no modo webtoon</string>
|
||||
<string name="webtoon_gaps_summary">Mostrar lacunas verticais entre as páginas no modo webtoon</string>
|
||||
<string name="webtoon_gaps">Lacunas no modo Webtoon</string>
|
||||
<string name="webtoon_gaps_summary">Mostrar lacunas verticais entre as páginas no modo Webtoon</string>
|
||||
<string name="authors">Autores</string>
|
||||
<string name="ignore_ssl_errors_summary">Você pode desativar a verificação de certificados SSL caso tenha problemas relacionados a SSL ao acessar recursos de rede. Isso pode afetar sua segurança. É necessário reiniciar o aplicativo após alterar essa configuração.</string>
|
||||
<string name="search_suggestions">Sugestões de pesquisa</string>
|
||||
@@ -633,7 +633,7 @@
|
||||
<string name="new_chapters_pattern">%1$s: %2$d</string>
|
||||
<string name="pin_navigation_ui">Fixar interface de navegação</string>
|
||||
<string name="pin_navigation_ui_summary">Não esconder barra de navegação e visualização de pesquisa ao rolar</string>
|
||||
<string name="_new">Novo</string>
|
||||
<string name="_new">Novos</string>
|
||||
<string name="all_languages">Todas os idiomas</string>
|
||||
<string name="screenshots_block_incognito">Bloquear no modo de navegação anônima</string>
|
||||
<string name="image_server">Servidor de imagem preferido</string>
|
||||
@@ -711,7 +711,7 @@
|
||||
<string name="minutes_seconds_short">%1$d min %2$d s</string>
|
||||
<string name="unpopular">Impopular</string>
|
||||
<string name="stuck">Preso</string>
|
||||
<string name="not_in_favorites">Não está nas favoritas</string>
|
||||
<string name="not_in_favorites">Não está nos favoritos</string>
|
||||
<string name="fixing_manga">Corrigindo a obra</string>
|
||||
<string name="fixed">Corrigida</string>
|
||||
<string name="no_fix_required">Nenhuma correção necessária para \"%s\"</string>
|
||||
@@ -768,7 +768,7 @@
|
||||
<string name="rating">Avaliação</string>
|
||||
<string name="source">Fonte</string>
|
||||
<string name="restoring_backup">Restaurando backup</string>
|
||||
<string name="error_disclaimer_manga">Tente abrir o manga em um navegador para ter certeza de que o mesmo está disponível na fonte</string>
|
||||
<string name="error_disclaimer_manga">Tente abrir o manga em um navegador para ter certeza de que o mesmo está disponível na fonte.</string>
|
||||
<string name="error_disclaimer_app_outdated">Parece que a sua versão do Kotatsu está desatualizada. Por favor instale a última versão para obter todos as correções disponíveis.</string>
|
||||
<string name="search_everywhere">Pesquise em todos os lugares</string>
|
||||
<string name="clear_browser_data">Limpar dados do navegador</string>
|
||||
@@ -797,4 +797,10 @@
|
||||
<string name="disable_captcha_notifications_summary">Você não receberá notificações sobre solucionar CAPTCHA para essa fonte, mas isso pode causar falha em operações de segundo plano (checagem de novos capítulos, obtenção de recomendações, etc)</string>
|
||||
<string name="global_search">Pesquisa global</string>
|
||||
<string name="badges_in_lists">Emblemas em listas</string>
|
||||
<string name="tags_warnings">Destacar gêneros perigosos</string>
|
||||
<string name="tags_warnings_summary">Destacar gêneros que podem ser inapropriados para a maioria dos usuários</string>
|
||||
<string name="nsfw_16">+16</string>
|
||||
<string name="exclude_nsfw_from_suggestions_summary">Mangás adultos não serão exibidos nas sugestões. Essa opção pode funcionar de forma imprecisa com algumas fontes</string>
|
||||
<string name="include_disabled_sources">Incluir fontes desabilitadas</string>
|
||||
<string name="suggestions_disabled_sources_summary">Mostrar sugestões de todas as fontes de mangá, incluindo as desabilitadas</string>
|
||||
</resources>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<string name="add">Добавить</string>
|
||||
<string name="save">Сохранить</string>
|
||||
<string name="share">Поделиться</string>
|
||||
<string name="create_shortcut">Создать ярлык…</string>
|
||||
<string name="create_shortcut">Создать ярлык</string>
|
||||
<string name="share_s">Поделиться %s</string>
|
||||
<string name="search">Поиск</string>
|
||||
<string name="search_manga">Поиск манги</string>
|
||||
@@ -795,4 +795,15 @@
|
||||
<string name="error_disclaimer_manga">Попробуйте открыть мангу в браузере, чтобы убедиться, что она доступна в источнике.</string>
|
||||
<string name="disable_captcha_notifications">Отключить уведомления о CAPTCHA</string>
|
||||
<string name="disable_captcha_notifications_summary">Вы не будете получать уведомления о прохождении CAPTCHA для этого источника, но это может привести к тому, что фоновые операции перестанут работать (проверка новых глав, обновление рекомендаций и т. д.)</string>
|
||||
<string name="tags_warnings">Выделять опасные жанры</string>
|
||||
<string name="tags_warnings_summary">Выделять жанры, которые могут быть неприемлемы для большинства пользователей</string>
|
||||
<string name="clear_browser_data">Очистить данные браузера</string>
|
||||
<string name="no_write_permission_to_file">Нет прав на запись в файл</string>
|
||||
<string name="link_to_manga_on_s">Ссылка на мангу на %s</string>
|
||||
<string name="exclude_nsfw_from_suggestions_summary">Взрослая манга не будет отображаться в рекомендациях. Эта опция может не работать с некоторыми источниками</string>
|
||||
<string name="include_disabled_sources">Включить отключенные источники</string>
|
||||
<string name="suggestions_disabled_sources_summary">Отображать рекомендации из всех источников манги, включая отключенные</string>
|
||||
<string name="link_to_manga_in_app">Ссылка на мангу в Kotatsu</string>
|
||||
<string name="nsfw_16">16+</string>
|
||||
<string name="clear_browser_data_summary">Удалить данные встроенного браузера, такие как кэш и куки. Внимание: авторизация в источниках манги может быть потеряна</string>
|
||||
</resources>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<string name="nothing_found">Hiçbir şey bulunamadı</string>
|
||||
<string name="history_is_empty">Geçmiş yok</string>
|
||||
<string name="read">Oku</string>
|
||||
<string name="you_have_not_favourites_yet">Henüz favorileriniz yok</string>
|
||||
<string name="you_have_not_favourites_yet">Henüz favoriniz yok</string>
|
||||
<string name="add_to_favourites">Favorilere ekle</string>
|
||||
<string name="add_new_category">Yeni kategori</string>
|
||||
<string name="add">Ekle</string>
|
||||
@@ -804,4 +804,6 @@
|
||||
<string name="exclude_nsfw_from_suggestions_summary">Yetişkin mangaları önerilerde gösterilmeyecektir. Bu seçenek her kaynak için doğru çalışmayabilir</string>
|
||||
<string name="nsfw_16">16+</string>
|
||||
<string name="include_disabled_sources">Devre dışı bırakılmış kaynakları dahil et</string>
|
||||
<string name="tags_warnings">Riskli türleri işaretle</string>
|
||||
<string name="tags_warnings_summary">Çoğu kullanıcılar için uygunsuz olabilecek türleri işaretle</string>
|
||||
</resources>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<string name="you_have_not_favourites_yet">Chưa có mục yêu thích nào</string>
|
||||
<string name="add_to_favourites">Thêm vào mục yêu thích</string>
|
||||
<string name="add">Thêm</string>
|
||||
<string name="create_shortcut">Tạo lối tắt…</string>
|
||||
<string name="create_shortcut">Tạo lối tắt</string>
|
||||
<string name="share_s">Chia sẻ %s</string>
|
||||
<string name="search_manga">Tìm kiếm manga</string>
|
||||
<string name="manga_downloading_">Đang tải xuống…</string>
|
||||
@@ -450,7 +450,7 @@
|
||||
<string name="suggest_new_sources_summary">Hiện cửa sổ bật các nguồn mới sau khi cập nhật ứng dụng</string>
|
||||
<string name="by_relevance">Liên quan</string>
|
||||
<string name="periodic_backups">Sao lưu định kì</string>
|
||||
<string name="online_variant">Biến thể trên mạng</string>
|
||||
<string name="online_variant">Xem chi tiết với nguồn</string>
|
||||
<string name="frequency_once_per_week">Một lần mỗi tuần</string>
|
||||
<string name="frequency_twice_per_month">Hai lần mỗi tháng</string>
|
||||
<string name="categories">Thể loại</string>
|
||||
@@ -802,4 +802,6 @@
|
||||
<string name="include_disabled_sources">Bao gồm các nguồn đã bị vô hiệu hoá</string>
|
||||
<string name="suggestions_disabled_sources_summary">Hiện gợi ý từ tất cả các nguồn đọc, bao gồm cả những nguồn đã bị vô hiệu hoá</string>
|
||||
<string name="nsfw_16">16+</string>
|
||||
<string name="tags_warnings">Highlight những thể loại không an toàn</string>
|
||||
<string name="tags_warnings_summary">Highlight những thể loại mà nó không thực sự phù hợp với hầu hết người đọc (mọi lứa tuổi)</string>
|
||||
</resources>
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
<string name="add">添加</string>
|
||||
<string name="save">保存</string>
|
||||
<string name="share">分享</string>
|
||||
<string name="create_shortcut">创建快捷方式…</string>
|
||||
<string name="create_shortcut">创建快捷方式</string>
|
||||
<string name="share_s">分享 %s</string>
|
||||
<string name="search">搜索</string>
|
||||
<string name="search_manga">搜索漫画</string>
|
||||
@@ -801,4 +801,6 @@
|
||||
<string name="include_disabled_sources">包括已关闭图源</string>
|
||||
<string name="suggestions_disabled_sources_summary">在搜索建议中显示所有图源,包括已关闭图源</string>
|
||||
<string name="simple">默认</string>
|
||||
<string name="tags_warnings">高亮敏感风格</string>
|
||||
<string name="tags_warnings_summary">将可能会对大部分用户不友善的风格高亮</string>
|
||||
</resources>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<string name="add">Add</string>
|
||||
<string name="save">Save</string>
|
||||
<string name="share">Share</string>
|
||||
<string name="create_shortcut">Create shortcut…</string>
|
||||
<string name="create_shortcut">Create shortcut</string>
|
||||
<string name="share_s">Share %s</string>
|
||||
<string name="search">Search</string>
|
||||
<string name="search_manga">Search manga</string>
|
||||
|
||||
@@ -31,7 +31,7 @@ material = "1.13.0-alpha12"
|
||||
moshi = "1.15.2"
|
||||
okhttp = "4.12.0"
|
||||
okio = "3.10.2"
|
||||
parsers = "8bb0c4f4f1"
|
||||
parsers = "20a24db949"
|
||||
preference = "1.2.1"
|
||||
recyclerview = "1.4.0"
|
||||
room = "2.6.1"
|
||||
|
||||
Reference in New Issue
Block a user