diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Flow.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Flow.kt index 214f3b349..bce16e71c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Flow.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Flow.kt @@ -61,6 +61,8 @@ inline fun Flow>.mapItems(crossinline transform: (T) -> R): Flow< return map { list -> list.map(transform) } } +fun Flow.throttle(timeoutMillis: Long): Flow = throttle { timeoutMillis } + fun Flow.throttle(timeoutMillis: (T) -> Long): Flow { var lastEmittedAt = 0L return transformLatest { value -> diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt index 798b5ca02..1e6232c33 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt @@ -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) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt index b7d2267ce..08da88145 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt @@ -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 } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderSettings.kt index 03ea3677d..fdb6be6c9 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderSettings.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderSettings.kt @@ -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, -) : MediatorLiveData() { +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, + private val settings: AppSettings, + private val mangaDataRepository: MangaDataRepository, + ) : MediatorStateFlow(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, - 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): Producer + } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt index 71c0f78cf..4ec5e3de9 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt @@ -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( 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( 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) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BasePagerReaderFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BasePagerReaderFragment.kt index 1eccf7bc1..9675cef8e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BasePagerReaderFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BasePagerReaderFragment.kt @@ -142,7 +142,7 @@ abstract class BasePagerReaderFragment : BaseReaderFragment = PagesAdapter( lifecycleOwner = viewLifecycleOwner, loader = pageLoader, - settings = viewModel.readerSettings, + readerSettingsProducer = viewModel.readerSettingsProducer, networkState = networkState, exceptionResolver = exceptionResolver, ) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt index fd50902ea..788fc8e56 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt @@ -15,7 +15,7 @@ import kotlin.coroutines.suspendCoroutine @Suppress("LeakingThis") abstract class BaseReaderAdapter>( private val loader: PageLoader, - private val readerSettings: ReaderSettings, + private val readerSettingsProducer: ReaderSettings.Producer, private val networkState: NetworkState, private val exceptionResolver: ExceptionResolver, ) : RecyclerView.Adapter() { @@ -58,7 +58,7 @@ abstract class BaseReaderAdapter>( 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) = suspendCoroutine { cont -> differ.submitList(items) { @@ -69,7 +69,7 @@ abstract class BaseReaderAdapter>( protected abstract fun onCreateViewHolder( parent: ViewGroup, loader: PageLoader, - settings: ReaderSettings, + readerSettingsProducer: ReaderSettings.Producer, networkState: NetworkState, exceptionResolver: ExceptionResolver, ): H diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoublePageHolder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoublePageHolder.kt index 280b9b50f..9d74c2b81 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoublePageHolder.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoublePageHolder.kt @@ -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(), diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoublePagesAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoublePagesAdapter.kt index f6384ad09..ddb16f2d7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoublePagesAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoublePagesAdapter.kt @@ -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(loader, settings, networkState, exceptionResolver) { +) : BaseReaderAdapter(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, ) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoubleReaderFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoubleReaderFragment.kt index 65f007615..035225161 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoubleReaderFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoubleReaderFragment.kt @@ -90,7 +90,7 @@ open class DoubleReaderFragment : BaseReaderFragment(loader, settings, networkState, exceptionResolver) { +) : BaseReaderAdapter(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, ) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedReaderFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedReaderFragment.kt index e66bbe9b3..993a5884d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedReaderFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedReaderFragment.kt @@ -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, ) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt index 3e5a6c483..4c96cf3fa 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt @@ -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(binding, loader, settings, networkState, exceptionResolver, owner), - View.OnClickListener, +) : BasePageHolder( + 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) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PagesAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PagesAdapter.kt index 0c562ae4d..941bd1131 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PagesAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PagesAdapter.kt @@ -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(loader, settings, networkState, exceptionResolver) { +) : BaseReaderAdapter( + 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, ) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/vm/PageState.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/vm/PageState.kt new file mode 100644 index 000000000..136ffb2e5 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/vm/PageState.kt @@ -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 +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/vm/PageViewModel.kt similarity index 50% rename from app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/vm/PageViewModel.kt index 79ea844cb..50388a9e2 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/vm/PageViewModel.kt @@ -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, 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.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) = 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() - } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonAdapter.kt index 607f33ad6..b655e23cc 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonAdapter.kt @@ -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(loader, settings, networkState, exceptionResolver) { +) : BaseReaderAdapter(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, ) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt index 95d3953f4..600196617 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt @@ -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(binding, loader, settings, networkState, exceptionResolver, owner), - View.OnClickListener { +) : BasePageHolder( + 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) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt index 460cc4856..000b539f5 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt @@ -67,7 +67,7 @@ class WebtoonReaderFragment : BaseReaderFragment() rv.addItemDecoration(WebtoonGapsDecoration()) } } - viewModel.readerSettings.observe(viewLifecycleOwner) { + viewModel.readerSettingsProducer.observe(viewLifecycleOwner) { it.applyBackground(binding.root) } } @@ -81,7 +81,7 @@ class WebtoonReaderFragment : BaseReaderFragment() override fun onCreateAdapter() = WebtoonAdapter( lifecycleOwner = viewLifecycleOwner, loader = pageLoader, - settings = viewModel.readerSettings, + readerSettingsProducer = viewModel.readerSettingsProducer, networkState = networkState, exceptionResolver = exceptionResolver, ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7526098f3..f0c349b43 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -31,7 +31,7 @@ Add Save Share - Create shortcut… + Create shortcut Share %s Search Search manga