Update page state management in reader
This commit is contained in:
@@ -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 ->
|
||||
|
||||
@@ -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,56 @@
|
||||
package org.koitharu.kotatsu.reader.ui.pager
|
||||
|
||||
import android.content.Context
|
||||
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.core.util.ext.showOrHide
|
||||
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 {
|
||||
|
||||
@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 +58,128 @@ 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 (viewModel.state.value is PageState.Shown) {
|
||||
onReady()
|
||||
}
|
||||
}
|
||||
|
||||
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(viewModel)
|
||||
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(viewModel)
|
||||
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()
|
||||
}
|
||||
|
||||
protected open fun onStateChanged(state: PageState) {
|
||||
bindingInfo.layoutError.isVisible = state is PageState.Error
|
||||
bindingInfo.progressBar.showOrHide(!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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -17,17 +17,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,23 +2,15 @@ package org.koitharu.kotatsu.reader.ui.pager.standard
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.PointF
|
||||
import android.view.View
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import androidx.core.view.isVisible
|
||||
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.core.util.ext.setTextAndVisible
|
||||
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
|
||||
@@ -28,37 +20,25 @@ 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,
|
||||
) : BasePageHolder<ItemPageBinding>(
|
||||
binding = binding,
|
||||
loader = loader,
|
||||
readerSettingsProducer = readerSettingsProducer,
|
||||
networkState = networkState,
|
||||
exceptionResolver = exceptionResolver,
|
||||
lifecycleOwner = owner,
|
||||
),
|
||||
ZoomControl.ZoomControlListener {
|
||||
|
||||
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)
|
||||
}
|
||||
override val ssiv = binding.ssiv
|
||||
|
||||
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()
|
||||
override fun onConfigChanged(settings: ReaderSettings) {
|
||||
super.onConfigChanged(settings)
|
||||
if (settings.applyBitmapConfig(binding.ssiv)) {
|
||||
delegate.reload()
|
||||
reloadImage()
|
||||
}
|
||||
binding.ssiv.applyDownSampling(isResumed())
|
||||
binding.textViewNumber.isVisible = settings.isPagesNumbersEnabled
|
||||
@@ -66,7 +46,7 @@ open class PageHolder(
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBind(data: ReaderPage) {
|
||||
delegate.onBind(data.toMangaPage())
|
||||
super.onBind(data)
|
||||
binding.textViewNumber.text = (data.index + 1).toString()
|
||||
}
|
||||
|
||||
@@ -75,33 +55,7 @@ open class PageHolder(
|
||||
binding.ssiv.recycle()
|
||||
}
|
||||
|
||||
override fun onLoadingStarted() {
|
||||
bindingInfo.layoutError.isVisible = false
|
||||
bindingInfo.progressBar.show()
|
||||
binding.ssiv.recycle()
|
||||
bindingInfo.textViewStatus.setTextAndVisible(R.string.loading_)
|
||||
}
|
||||
|
||||
override fun onProgressChanged(progress: Int) {
|
||||
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_)
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
@@ -141,34 +95,6 @@ open class PageHolder(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onImageShown(isPreview: Boolean) {
|
||||
if (!isPreview) {
|
||||
bindingInfo.progressBar.hide()
|
||||
}
|
||||
bindingInfo.textViewStatus.isVisible = false
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package org.koitharu.kotatsu.reader.ui.pager
|
||||
package org.koitharu.kotatsu.reader.ui.pager.vm
|
||||
|
||||
import android.content.ComponentCallbacks2
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.davemorrissey.labs.subscaleview.DefaultOnImageEventListener
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@@ -14,41 +14,38 @@ 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.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.toFileOrNull
|
||||
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
|
||||
import java.io.IOException
|
||||
|
||||
class PageHolderDelegate(
|
||||
class PageViewModel(
|
||||
private val loader: PageLoader,
|
||||
private val readerSettings: ReaderSettings,
|
||||
private val callback: Callback,
|
||||
val settingsProducer: ReaderSettings.Producer,
|
||||
private val networkState: NetworkState,
|
||||
private val exceptionResolver: ExceptionResolver,
|
||||
private val isWebtoon: Boolean,
|
||||
) : DefaultOnImageEventListener, Observer<ReaderSettings>, ComponentCallbacks2 {
|
||||
) : DefaultOnImageEventListener, 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
|
||||
|
||||
val state = MutableStateFlow<PageState>(PageState.Empty)
|
||||
|
||||
init {
|
||||
scope.launch(Dispatchers.Main) { // the same as post() -- wait until child fields init
|
||||
callback.onConfigChanged()
|
||||
// callback.onConfigChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +53,7 @@ class PageHolderDelegate(
|
||||
|
||||
fun onBind(page: MangaPage) {
|
||||
val prevJob = job
|
||||
job = scope.launch {
|
||||
job = scope.launch(Dispatchers.Default) {
|
||||
prevJob?.cancelAndJoin()
|
||||
doLoad(page, force = false)
|
||||
}
|
||||
@@ -66,8 +63,8 @@ class PageHolderDelegate(
|
||||
val prevJob = job
|
||||
job = scope.launch {
|
||||
prevJob?.cancelAndJoin()
|
||||
val e = error
|
||||
if (e != null && ExceptionResolver.canResolve(e)) {
|
||||
val e = (state.value as? PageState.Error)?.error
|
||||
if (e != null && ExceptionResolver.Companion.canResolve(e)) {
|
||||
if (!isFromUser) {
|
||||
return@launch
|
||||
}
|
||||
@@ -78,75 +75,38 @@ class PageHolderDelegate(
|
||||
}
|
||||
|
||||
fun showErrorDetails(url: String?) {
|
||||
val e = error ?: return
|
||||
val e = (state.value as? PageState.Error)?.error ?: return
|
||||
exceptionResolver.showErrorDetails(e, url)
|
||||
}
|
||||
|
||||
fun onAttachedToWindow() {
|
||||
readerSettings.observeForever(this)
|
||||
}
|
||||
|
||||
fun onDetachedFromWindow() {
|
||||
readerSettings.removeObserver(this)
|
||||
}
|
||||
|
||||
fun onRecycle() {
|
||||
state = State.EMPTY
|
||||
uri = null
|
||||
state.value = PageState.Empty
|
||||
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(isPreview = false)
|
||||
} else if (state == State.LOADING_WITH_PREVIEW) {
|
||||
callback.onImageShown(isPreview = true)
|
||||
state.update {
|
||||
if (it is PageState.Loaded) PageState.Shown(it.source, it.isConverted) else it
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
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
|
||||
}
|
||||
}
|
||||
callback.onConfigChanged()
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) = Unit
|
||||
@@ -155,68 +115,58 @@ class PageHolderDelegate(
|
||||
override fun onLowMemory() = Unit
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
callback.onTrimMemory()
|
||||
// callback.onTrimMemory()
|
||||
}
|
||||
|
||||
private fun tryConvert(uri: Uri, e: Exception) {
|
||||
val prevJob = job
|
||||
job = scope.launch {
|
||||
prevJob?.join()
|
||||
state = State.CONVERTING
|
||||
state.value = PageState.Converting()
|
||||
try {
|
||||
val newUri = loader.convertBimap(uri)
|
||||
cachedBounds = if (readerSettings.isPagesCropEnabled(isWebtoon)) {
|
||||
cachedBounds = if (settingsProducer.value.isPagesCropEnabled(isWebtoon)) {
|
||||
loader.getTrimmedBounds(newUri)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
state = State.CONVERTED
|
||||
callback.onImageReady(newUri.toImageSource(cachedBounds))
|
||||
state.value = PageState.Loaded(uri.toImageSource(cachedBounds), isConverted = true)
|
||||
} catch (ce: CancellationException) {
|
||||
throw ce
|
||||
} catch (e2: Throwable) {
|
||||
e2.printStackTrace()
|
||||
e.addSuppressed(e2)
|
||||
state = State.ERROR
|
||||
callback.onError(e)
|
||||
state.value = PageState.Error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private suspend fun doLoad(data: MangaPage, force: Boolean) = coroutineScope {
|
||||
state = State.LOADING
|
||||
error = null
|
||||
callback.onLoadingStarted()
|
||||
state.value = PageState.Loading(null/* TODO */, -1)
|
||||
val previewJob = launch {
|
||||
val preview = loader.loadPreview(data)
|
||||
if (preview != null && state == State.LOADING) {
|
||||
state = State.LOADING_WITH_PREVIEW
|
||||
callback.onPreviewReady(preview)
|
||||
val preview = loader.loadPreview(data) ?: return@launch
|
||||
state.update {
|
||||
if (it is PageState.Loading) it.copy(preview = preview) else it
|
||||
}
|
||||
}
|
||||
try {
|
||||
val task = withContext(Dispatchers.Default) {
|
||||
loader.loadPageAsync(data, force)
|
||||
}
|
||||
val task = loader.loadPageAsync(data, force)
|
||||
val progressObserver = observeProgress(this, task.progressAsFlow())
|
||||
val file = task.await()
|
||||
val uri = task.await()
|
||||
progressObserver.cancelAndJoin()
|
||||
previewJob.cancel()
|
||||
uri = file
|
||||
state = State.LOADED
|
||||
cachedBounds = if (readerSettings.isPagesCropEnabled(isWebtoon)) {
|
||||
loader.getTrimmedBounds(checkNotNull(uri))
|
||||
cachedBounds = if (settingsProducer.value.isPagesCropEnabled(isWebtoon)) {
|
||||
loader.getTrimmedBounds(uri)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
callback.onImageReady(checkNotNull(uri).toImageSource(cachedBounds))
|
||||
state.value = PageState.Loaded(uri.toImageSource(cachedBounds), isConverted = false)
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTraceDebug()
|
||||
state = State.ERROR
|
||||
error = e
|
||||
callback.onError(e)
|
||||
state.value = PageState.Error(e)
|
||||
if (e is IOException && !networkState.value) {
|
||||
networkState.awaitForConnection()
|
||||
retry(data, isFromUser = false)
|
||||
@@ -225,9 +175,17 @@ class PageHolderDelegate(
|
||||
}
|
||||
|
||||
private fun observeProgress(scope: CoroutineScope, progress: Flow<Float>) = progress
|
||||
.debounce(250)
|
||||
.onEach { callback.onProgressChanged((100 * it).toInt()) }
|
||||
.launchIn(scope)
|
||||
.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)
|
||||
@@ -237,29 +195,4 @@ class PageHolderDelegate(
|
||||
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(isPreview: Boolean)
|
||||
|
||||
fun onProgressChanged(progress: Int)
|
||||
|
||||
fun onConfigChanged()
|
||||
|
||||
fun onTrimMemory()
|
||||
}
|
||||
}
|
||||
@@ -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,65 +1,43 @@
|
||||
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.core.util.ext.setTextAndVisible
|
||||
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)
|
||||
}
|
||||
|
||||
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()
|
||||
override fun onConfigChanged(settings: ReaderSettings) {
|
||||
super.onConfigChanged(settings)
|
||||
if (settings.applyBitmapConfig(binding.ssiv)) {
|
||||
delegate.reload()
|
||||
reloadImage()
|
||||
}
|
||||
binding.ssiv.applyDownSampling(isResumed())
|
||||
}
|
||||
|
||||
override fun onBind(data: ReaderPage) {
|
||||
delegate.onBind(data.toMangaPage())
|
||||
}
|
||||
|
||||
override fun onRecycled() {
|
||||
super.onRecycled()
|
||||
binding.ssiv.recycle()
|
||||
@@ -75,31 +53,7 @@ class WebtoonHolder(
|
||||
goneOnInvisibleListener.detach()
|
||||
}
|
||||
|
||||
override fun onLoadingStarted() {
|
||||
bindingInfo.layoutError.isVisible = false
|
||||
bindingInfo.progressBar.show()
|
||||
binding.ssiv.recycle()
|
||||
bindingInfo.textViewStatus.setTextAndVisible(R.string.loading_)
|
||||
}
|
||||
|
||||
override fun onProgressChanged(progress: Int) {
|
||||
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_)
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
@@ -113,32 +67,6 @@ class WebtoonHolder(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onImageShown(isPreview: Boolean) {
|
||||
bindingInfo.progressBar.hide()
|
||||
bindingInfo.textViewStatus.isVisible = false
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user