Yet another attempt to make webtoon reader great again
This commit is contained in:
@@ -7,6 +7,8 @@ import android.view.View
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.asExecutor
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||||
@@ -26,6 +28,8 @@ open class PageHolder(
|
|||||||
View.OnClickListener {
|
View.OnClickListener {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
binding.ssiv.setExecutor(Dispatchers.Default.asExecutor())
|
||||||
|
binding.ssiv.setEagerLoadingEnabled(!isLowRamDevice(context))
|
||||||
binding.ssiv.setOnImageEventListener(delegate)
|
binding.ssiv.setOnImageEventListener(delegate)
|
||||||
@Suppress("LeakingThis")
|
@Suppress("LeakingThis")
|
||||||
bindingInfo.buttonRetry.setOnClickListener(this)
|
bindingInfo.buttonRetry.setOnClickListener(this)
|
||||||
|
|||||||
@@ -3,13 +3,16 @@ package org.koitharu.kotatsu.reader.ui.pager.webtoon
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
|
import androidx.annotation.AttrRes
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
|
|
||||||
class WebtoonFrameLayout @JvmOverloads constructor(
|
class WebtoonFrameLayout @JvmOverloads constructor(
|
||||||
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
@AttrRes defStyleAttr: Int = 0,
|
||||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
private val target by lazy {
|
private val target by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
findViewById<WebtoonImageView>(R.id.ssiv)
|
findViewById<WebtoonImageView>(R.id.ssiv)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding
|
|||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder
|
import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||||
|
import org.koitharu.kotatsu.utils.GoneOnInvisibleListener
|
||||||
import org.koitharu.kotatsu.utils.ext.*
|
import org.koitharu.kotatsu.utils.ext.*
|
||||||
|
|
||||||
|
|
||||||
class WebtoonHolder(
|
class WebtoonHolder(
|
||||||
binding: ItemPageWebtoonBinding,
|
binding: ItemPageWebtoonBinding,
|
||||||
loader: PageLoader,
|
loader: PageLoader,
|
||||||
settings: AppSettings,
|
settings: AppSettings,
|
||||||
exceptionResolver: ExceptionResolver
|
exceptionResolver: ExceptionResolver,
|
||||||
) : BasePageHolder<ItemPageWebtoonBinding>(binding, loader, settings, exceptionResolver),
|
) : BasePageHolder<ItemPageWebtoonBinding>(binding, loader, settings, exceptionResolver),
|
||||||
View.OnClickListener {
|
View.OnClickListener {
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ class WebtoonHolder(
|
|||||||
init {
|
init {
|
||||||
binding.ssiv.setOnImageEventListener(delegate)
|
binding.ssiv.setOnImageEventListener(delegate)
|
||||||
bindingInfo.buttonRetry.setOnClickListener(this)
|
bindingInfo.buttonRetry.setOnClickListener(this)
|
||||||
|
GoneOnInvisibleListener(bindingInfo.progressBar).attach()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(data: ReaderPage) {
|
override fun onBind(data: ReaderPage) {
|
||||||
@@ -61,9 +62,9 @@ class WebtoonHolder(
|
|||||||
|
|
||||||
override fun onImageShowing(zoom: ZoomMode) {
|
override fun onImageShowing(zoom: ZoomMode) {
|
||||||
with(binding.ssiv) {
|
with(binding.ssiv) {
|
||||||
maxScale = 2f * width / sWidth.toFloat()
|
|
||||||
setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
|
setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
|
||||||
minScale = width / sWidth.toFloat()
|
minScale = width / sWidth.toFloat()
|
||||||
|
maxScale = minScale
|
||||||
scrollTo(
|
scrollTo(
|
||||||
when {
|
when {
|
||||||
scrollToRestore != 0 -> scrollToRestore
|
scrollToRestore != 0 -> scrollToRestore
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.pager.webtoon
|
package org.koitharu.kotatsu.reader.ui.pager.webtoon
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.PointF
|
import android.graphics.PointF
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.asExecutor
|
||||||
import org.koitharu.kotatsu.parsers.util.toIntUp
|
import org.koitharu.kotatsu.parsers.util.toIntUp
|
||||||
|
import org.koitharu.kotatsu.utils.ext.isLowRamDevice
|
||||||
|
import org.koitharu.kotatsu.utils.ext.parents
|
||||||
|
|
||||||
private const val SCROLL_UNKNOWN = -1
|
private const val SCROLL_UNKNOWN = -1
|
||||||
|
|
||||||
@@ -15,15 +19,15 @@ class WebtoonImageView @JvmOverloads constructor(
|
|||||||
) : SubsamplingScaleImageView(context, attr) {
|
) : SubsamplingScaleImageView(context, attr) {
|
||||||
|
|
||||||
private val ct = PointF()
|
private val ct = PointF()
|
||||||
private val displayHeight = if (context is Activity) {
|
|
||||||
context.window.decorView.height
|
|
||||||
} else {
|
|
||||||
context.resources.displayMetrics.heightPixels
|
|
||||||
}
|
|
||||||
|
|
||||||
private var scrollPos = 0
|
private var scrollPos = 0
|
||||||
private var scrollRange = SCROLL_UNKNOWN
|
private var scrollRange = SCROLL_UNKNOWN
|
||||||
|
|
||||||
|
init {
|
||||||
|
setExecutor(Dispatchers.Default.asExecutor())
|
||||||
|
setEagerLoadingEnabled(!isLowRamDevice(context))
|
||||||
|
}
|
||||||
|
|
||||||
fun scrollBy(delta: Int) {
|
fun scrollBy(delta: Int) {
|
||||||
val maxScroll = getScrollRange()
|
val maxScroll = getScrollRange()
|
||||||
if (maxScroll == 0) {
|
if (maxScroll == 0) {
|
||||||
@@ -36,6 +40,7 @@ class WebtoonImageView @JvmOverloads constructor(
|
|||||||
fun scrollTo(y: Int) {
|
fun scrollTo(y: Int) {
|
||||||
val maxScroll = getScrollRange()
|
val maxScroll = getScrollRange()
|
||||||
if (maxScroll == 0) {
|
if (maxScroll == 0) {
|
||||||
|
resetScaleAndCenter()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
scrollToInternal(y.coerceIn(0, maxScroll))
|
scrollToInternal(y.coerceIn(0, maxScroll))
|
||||||
@@ -58,8 +63,11 @@ class WebtoonImageView @JvmOverloads constructor(
|
|||||||
|
|
||||||
override fun getSuggestedMinimumHeight(): Int {
|
override fun getSuggestedMinimumHeight(): Int {
|
||||||
var desiredHeight = super.getSuggestedMinimumHeight()
|
var desiredHeight = super.getSuggestedMinimumHeight()
|
||||||
if (sHeight == 0 && desiredHeight < displayHeight) {
|
if (sHeight == 0) {
|
||||||
desiredHeight = displayHeight
|
val parentHeight = parentHeight()
|
||||||
|
if (desiredHeight < parentHeight) {
|
||||||
|
desiredHeight = parentHeight
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return desiredHeight
|
return desiredHeight
|
||||||
}
|
}
|
||||||
@@ -84,7 +92,7 @@ class WebtoonImageView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
width = width.coerceAtLeast(suggestedMinimumWidth)
|
width = width.coerceAtLeast(suggestedMinimumWidth)
|
||||||
height = height.coerceIn(suggestedMinimumHeight, displayHeight)
|
height = height.coerceIn(suggestedMinimumHeight, parentHeight())
|
||||||
setMeasuredDimension(width, height)
|
setMeasuredDimension(width, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,4 +109,8 @@ class WebtoonImageView @JvmOverloads constructor(
|
|||||||
val totalHeight = (sHeight * minScale).toIntUp()
|
val totalHeight = (sHeight * minScale).toIntUp()
|
||||||
scrollRange = (totalHeight - height).coerceAtLeast(0)
|
scrollRange = (totalHeight - height).coerceAtLeast(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun parentHeight(): Int {
|
||||||
|
return parents.firstNotNullOfOrNull { it as? RecyclerView }?.height ?: 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kotlin.math.sign
|
import kotlin.math.sign
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
class WebtoonLayoutManager : LinearLayoutManager {
|
class WebtoonLayoutManager : LinearLayoutManager {
|
||||||
|
|
||||||
private var scrollDirection: Int = 0
|
private var scrollDirection: Int = 0
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() {
|
|||||||
|
|
||||||
override fun onInflateView(
|
override fun onInflateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?
|
container: ViewGroup?,
|
||||||
) = FragmentReaderWebtoonBinding.inflate(inflater, container, false)
|
) = FragmentReaderWebtoonBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.reader.ui.thumbnails.adapter
|
|||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
|
import coil.size.Scale
|
||||||
import coil.size.Size
|
import coil.size.Size
|
||||||
import com.google.android.material.R as materialR
|
import com.google.android.material.R as materialR
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
@@ -24,7 +25,6 @@ fun pageThumbnailAD(
|
|||||||
) = adapterDelegateViewBinding<PageThumbnail, PageThumbnail, ItemPageThumbBinding>(
|
) = adapterDelegateViewBinding<PageThumbnail, PageThumbnail, ItemPageThumbBinding>(
|
||||||
{ inflater, parent -> ItemPageThumbBinding.inflate(inflater, parent, false) }
|
{ inflater, parent -> ItemPageThumbBinding.inflate(inflater, parent, false) }
|
||||||
) {
|
) {
|
||||||
|
|
||||||
var job: Job? = null
|
var job: Job? = null
|
||||||
val gridWidth = itemView.context.resources.getDimensionPixelSize(R.dimen.preferred_grid_width)
|
val gridWidth = itemView.context.resources.getDimensionPixelSize(R.dimen.preferred_grid_width)
|
||||||
val thumbSize = Size(
|
val thumbSize = Size(
|
||||||
@@ -39,6 +39,7 @@ fun pageThumbnailAD(
|
|||||||
.data(url)
|
.data(url)
|
||||||
.referer(item.page.referer)
|
.referer(item.page.referer)
|
||||||
.size(thumbSize)
|
.size(thumbSize)
|
||||||
|
.scale(Scale.FILL)
|
||||||
.allowRgb565(true)
|
.allowRgb565(true)
|
||||||
.build()
|
.build()
|
||||||
).drawable
|
).drawable
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package org.koitharu.kotatsu.utils
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package org.koitharu.kotatsu.utils.ext
|
package org.koitharu.kotatsu.utils.ext
|
||||||
|
|
||||||
|
import android.app.ActivityManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Context.ACTIVITY_SERVICE
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.pm.ResolveInfo
|
import android.content.pm.ResolveInfo
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
@@ -27,6 +29,9 @@ import kotlinx.coroutines.suspendCancellableCoroutine
|
|||||||
val Context.connectivityManager: ConnectivityManager
|
val Context.connectivityManager: ConnectivityManager
|
||||||
get() = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
get() = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
|
||||||
|
val Context.activityManager: ActivityManager?
|
||||||
|
get() = getSystemService(ACTIVITY_SERVICE) as? ActivityManager
|
||||||
|
|
||||||
suspend fun ConnectivityManager.waitForNetwork(): Network {
|
suspend fun ConnectivityManager.waitForNetwork(): Network {
|
||||||
val request = NetworkRequest.Builder().build()
|
val request = NetworkRequest.Builder().build()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
@@ -92,4 +97,8 @@ fun Lifecycle.postDelayed(runnable: Runnable, delay: Long) {
|
|||||||
delay(delay)
|
delay(delay)
|
||||||
runnable.run()
|
runnable.run()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isLowRamDevice(context: Context): Boolean {
|
||||||
|
return context.activityManager?.isLowRamDevice ?: false
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import android.app.Activity
|
|||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.ViewParent
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
@@ -138,4 +139,13 @@ fun <T : View> ViewGroup.findViewsByType(clazz: Class<T>): Sequence<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val View.parents: Sequence<ViewParent>
|
||||||
|
get() = sequence {
|
||||||
|
var p: ViewParent? = parent
|
||||||
|
while (p != null) {
|
||||||
|
yield(p)
|
||||||
|
p = p.parent
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user