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) }
|
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> {
|
fun <T> Flow<T>.throttle(timeoutMillis: (T) -> Long): Flow<T> {
|
||||||
var lastEmittedAt = 0L
|
var lastEmittedAt = 0L
|
||||||
return transformLatest { value ->
|
return transformLatest { value ->
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ class ReaderActivity :
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(ActivityReaderBinding.inflate(layoutInflater))
|
setContentView(ActivityReaderBinding.inflate(layoutInflater))
|
||||||
readerManager = ReaderManager(supportFragmentManager, viewBinding.container, settings)
|
readerManager = ReaderManager(supportFragmentManager, viewBinding.container, settings)
|
||||||
setDisplayHomeAsUp(true, false)
|
setDisplayHomeAsUp(isEnabled = true, showUpAsClose = false)
|
||||||
touchHelper = TapGridDispatcher(this, this)
|
touchHelper = TapGridDispatcher(this, this)
|
||||||
scrollTimer = scrollTimerFactory.create(this, this)
|
scrollTimer = scrollTimerFactory.create(this, this)
|
||||||
pageSaveHelper = pageSaveHelperFactory.create(this)
|
pageSaveHelper = pageSaveHelperFactory.create(this)
|
||||||
@@ -146,7 +146,7 @@ class ReaderActivity :
|
|||||||
.setAnchorView(viewBinding.toolbarDocked)
|
.setAnchorView(viewBinding.toolbarDocked)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
viewModel.readerSettings.observe(this) {
|
viewModel.readerSettingsProducer.observe(this) {
|
||||||
viewBinding.infoBar.applyColorScheme(isBlackOnWhite = it.background.isLight(this))
|
viewBinding.infoBar.applyColorScheme(isBlackOnWhite = it.background.isLight(this))
|
||||||
}
|
}
|
||||||
viewModel.isZoomControlsEnabled.observe(this) {
|
viewModel.isZoomControlsEnabled.observe(this) {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.flatMapLatest
|
|||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
@@ -85,6 +86,7 @@ class ReaderViewModel @Inject constructor(
|
|||||||
interactor: DetailsInteractor,
|
interactor: DetailsInteractor,
|
||||||
deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
|
deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
|
||||||
downloadScheduler: DownloadWorker.Scheduler,
|
downloadScheduler: DownloadWorker.Scheduler,
|
||||||
|
readerSettingsProducerFactory: ReaderSettings.Producer.Factory,
|
||||||
) : ChaptersPagesViewModel(
|
) : ChaptersPagesViewModel(
|
||||||
settings = settings,
|
settings = settings,
|
||||||
interactor = interactor,
|
interactor = interactor,
|
||||||
@@ -170,12 +172,8 @@ class ReaderViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false)
|
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false)
|
||||||
|
|
||||||
val readerSettings = ReaderSettings(
|
val readerSettingsProducer = readerSettingsProducerFactory.create(
|
||||||
parentScope = viewModelScope,
|
manga.mapNotNull { it?.id },
|
||||||
settings = settings,
|
|
||||||
colorFilterFlow = manga.flatMapLatest {
|
|
||||||
if (it == null) flowOf(null) else dataRepository.observeColorFilter(it.id)
|
|
||||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val isMangaNsfw = manga.map { it?.contentRating == ContentRating.ADULT }
|
val isMangaNsfw = manga.map { it?.contentRating == ContentRating.ADULT }
|
||||||
|
|||||||
@@ -1,66 +1,69 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.config
|
package org.koitharu.kotatsu.reader.ui.config
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.annotation.CheckResult
|
import androidx.annotation.CheckResult
|
||||||
import androidx.lifecycle.MediatorLiveData
|
import androidx.collection.scatterSetOf
|
||||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||||
import com.davemorrissey.labs.subscaleview.decoder.SkiaImageDecoder
|
import com.davemorrissey.labs.subscaleview.decoder.SkiaImageDecoder
|
||||||
import com.davemorrissey.labs.subscaleview.decoder.SkiaImageRegionDecoder
|
import com.davemorrissey.labs.subscaleview.decoder.SkiaImageRegionDecoder
|
||||||
import com.davemorrissey.labs.subscaleview.decoder.SkiaPooledImageRegionDecoder
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.FlowCollector
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
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.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
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.AppSettings
|
||||||
import org.koitharu.kotatsu.core.prefs.ReaderBackground
|
import org.koitharu.kotatsu.core.prefs.ReaderBackground
|
||||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
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.isLowRamDevice
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
||||||
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
|
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
|
||||||
|
|
||||||
class ReaderSettings(
|
data class ReaderSettings(
|
||||||
private val parentScope: CoroutineScope,
|
val zoomMode: ZoomMode,
|
||||||
private val settings: AppSettings,
|
val background: ReaderBackground,
|
||||||
private val colorFilterFlow: StateFlow<ReaderColorFilter?>,
|
val colorFilter: ReaderColorFilter?,
|
||||||
) : MediatorLiveData<ReaderSettings>() {
|
val isReaderOptimizationEnabled: Boolean,
|
||||||
|
val bitmapConfig: Bitmap.Config,
|
||||||
|
val isPagesNumbersEnabled: Boolean,
|
||||||
|
val isPagesCropEnabledStandard: Boolean,
|
||||||
|
val isPagesCropEnabledWebtoon: Boolean,
|
||||||
|
) {
|
||||||
|
|
||||||
private val internalObserver = InternalObserver()
|
private constructor(settings: AppSettings, colorFilterOverride: ReaderColorFilter?) : this(
|
||||||
private var collectJob: Job? = null
|
zoomMode = settings.zoomMode,
|
||||||
|
background = settings.readerBackground,
|
||||||
val zoomMode: ZoomMode
|
colorFilter = colorFilterOverride?.takeUnless { it.isEmpty } ?: settings.readerColorFilter,
|
||||||
get() = settings.zoomMode
|
isReaderOptimizationEnabled = settings.isReaderOptimizationEnabled,
|
||||||
|
bitmapConfig = if (settings.is32BitColorsEnabled) {
|
||||||
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) {
|
|
||||||
Bitmap.Config.ARGB_8888
|
Bitmap.Config.ARGB_8888
|
||||||
} else {
|
} else {
|
||||||
Bitmap.Config.RGB_565
|
Bitmap.Config.RGB_565
|
||||||
}
|
},
|
||||||
|
isPagesNumbersEnabled = settings.isPagesNumbersEnabled,
|
||||||
val isPagesNumbersEnabled: Boolean
|
isPagesCropEnabledStandard = settings.isPagesCropEnabled(ReaderMode.STANDARD),
|
||||||
get() = settings.isPagesNumbersEnabled
|
isPagesCropEnabledWebtoon = settings.isPagesCropEnabled(ReaderMode.WEBTOON),
|
||||||
|
)
|
||||||
|
|
||||||
fun applyBackground(view: View) {
|
fun applyBackground(view: View) {
|
||||||
view.background = background.resolve(view.context)
|
view.background = background.resolve(view.context)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isPagesCropEnabled(isWebtoon: Boolean) = settings.isPagesCropEnabled(
|
fun isPagesCropEnabled(isWebtoon: Boolean) = if (isWebtoon) {
|
||||||
if (isWebtoon) ReaderMode.WEBTOON else ReaderMode.STANDARD,
|
isPagesCropEnabledWebtoon
|
||||||
)
|
} else {
|
||||||
|
isPagesCropEnabledStandard
|
||||||
|
}
|
||||||
|
|
||||||
@CheckResult
|
@CheckResult
|
||||||
fun applyBitmapConfig(ssiv: SubsamplingScaleImageView): Boolean {
|
fun applyBitmapConfig(ssiv: SubsamplingScaleImageView): Boolean {
|
||||||
@@ -78,33 +81,13 @@ class ReaderSettings(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onInactive() {
|
class Producer @AssistedInject constructor(
|
||||||
super.onInactive()
|
@Assisted private val mangaId: Flow<Long>,
|
||||||
settings.unsubscribe(internalObserver)
|
private val settings: AppSettings,
|
||||||
collectJob?.cancel()
|
private val mangaDataRepository: MangaDataRepository,
|
||||||
collectJob = null
|
) : MediatorStateFlow<ReaderSettings>(ReaderSettings(settings, null)) {
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActive() {
|
private val settingsKeys = scatterSetOf(
|
||||||
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(
|
|
||||||
AppSettings.KEY_ZOOM_MODE,
|
AppSettings.KEY_ZOOM_MODE,
|
||||||
AppSettings.KEY_PAGES_NUMBERS,
|
AppSettings.KEY_PAGES_NUMBERS,
|
||||||
AppSettings.KEY_READER_BACKGROUND,
|
AppSettings.KEY_READER_BACKGROUND,
|
||||||
@@ -114,18 +97,38 @@ class ReaderSettings(
|
|||||||
AppSettings.KEY_CF_BRIGHTNESS,
|
AppSettings.KEY_CF_BRIGHTNESS,
|
||||||
AppSettings.KEY_CF_INVERTED,
|
AppSettings.KEY_CF_INVERTED,
|
||||||
AppSettings.KEY_CF_GRAYSCALE,
|
AppSettings.KEY_CF_GRAYSCALE,
|
||||||
|
AppSettings.KEY_READER_CROP,
|
||||||
)
|
)
|
||||||
|
private var job: Job? = null
|
||||||
|
|
||||||
override suspend fun emit(value: ReaderColorFilter?) {
|
override fun onActive() {
|
||||||
withContext(Dispatchers.Main.immediate) {
|
assert(job?.isActive != true)
|
||||||
notifyChanged()
|
job?.cancel()
|
||||||
|
job = processLifecycleScope.launch(Dispatchers.Default) {
|
||||||
|
observeImpl()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
override fun onInactive() {
|
||||||
if (key in settingsKeys) {
|
job?.cancel()
|
||||||
notifyChanged()
|
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
|
package org.koitharu.kotatsu.reader.ui.pager
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.view.View
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
|
import androidx.core.view.isGone
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
|
import com.davemorrissey.labs.subscaleview.DefaultOnImageEventListener
|
||||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||||
import org.koitharu.kotatsu.core.os.NetworkState
|
import org.koitharu.kotatsu.core.os.NetworkState
|
||||||
import org.koitharu.kotatsu.core.ui.list.lifecycle.LifecycleAwareViewHolder
|
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.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.databinding.LayoutPageInfoBinding
|
||||||
|
import org.koitharu.kotatsu.parsers.util.ifZero
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
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
|
import org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonHolder
|
||||||
|
|
||||||
abstract class BasePageHolder<B : ViewBinding>(
|
abstract class BasePageHolder<B : ViewBinding>(
|
||||||
protected val binding: B,
|
protected val binding: B,
|
||||||
loader: PageLoader,
|
loader: PageLoader,
|
||||||
protected val settings: ReaderSettings,
|
readerSettingsProducer: ReaderSettings.Producer,
|
||||||
networkState: NetworkState,
|
networkState: NetworkState,
|
||||||
exceptionResolver: ExceptionResolver,
|
exceptionResolver: ExceptionResolver,
|
||||||
lifecycleOwner: LifecycleOwner,
|
lifecycleOwner: LifecycleOwner,
|
||||||
) : LifecycleAwareViewHolder(binding.root, lifecycleOwner), PageHolderDelegate.Callback {
|
) : LifecycleAwareViewHolder(binding.root, lifecycleOwner), DefaultOnImageEventListener {
|
||||||
|
|
||||||
@Suppress("LeakingThis")
|
protected val viewModel = PageViewModel(
|
||||||
protected val delegate = PageHolderDelegate(
|
|
||||||
loader = loader,
|
loader = loader,
|
||||||
readerSettings = settings,
|
settingsProducer = readerSettingsProducer,
|
||||||
callback = this,
|
|
||||||
networkState = networkState,
|
networkState = networkState,
|
||||||
exceptionResolver = exceptionResolver,
|
exceptionResolver = exceptionResolver,
|
||||||
isWebtoon = this is WebtoonHolder,
|
isWebtoon = this is WebtoonHolder,
|
||||||
)
|
)
|
||||||
protected val bindingInfo = LayoutPageInfoBinding.bind(binding.root)
|
protected val bindingInfo = LayoutPageInfoBinding.bind(binding.root)
|
||||||
|
protected abstract val ssiv: SubsamplingScaleImageView
|
||||||
|
|
||||||
|
protected val settings: ReaderSettings
|
||||||
|
get() = viewModel.settingsProducer.value
|
||||||
|
|
||||||
val context: Context
|
val context: Context
|
||||||
get() = itemView.context
|
get() = itemView.context
|
||||||
@@ -42,51 +58,128 @@ abstract class BasePageHolder<B : ViewBinding>(
|
|||||||
var boundData: ReaderPage? = null
|
var boundData: ReaderPage? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
override fun onConfigChanged() {
|
init {
|
||||||
settings.applyBackground(itemView)
|
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 {
|
@CallSuper
|
||||||
return checkNotNull(boundData) { "Calling requireData() before bind()" }
|
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) {
|
fun bind(data: ReaderPage) {
|
||||||
boundData = data
|
boundData = data
|
||||||
|
viewModel.onBind(data.toMangaPage())
|
||||||
onBind(data)
|
onBind(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract fun onBind(data: ReaderPage)
|
@CallSuper
|
||||||
|
protected open fun onBind(data: ReaderPage) = Unit
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
context.registerComponentCallbacks(delegate)
|
context.registerComponentCallbacks(viewModel)
|
||||||
|
viewModel.state.observe(this, ::onStateChanged)
|
||||||
|
viewModel.settingsProducer.observe(this, ::onConfigChanged)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
if (delegate.state == State.ERROR && !delegate.isLoading()) {
|
ssiv.applyDownSampling(isForeground = true)
|
||||||
boundData?.let { delegate.retry(it.toMangaPage(), isFromUser = false) }
|
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() {
|
override fun onDestroy() {
|
||||||
context.unregisterComponentCallbacks(delegate)
|
context.unregisterComponentCallbacks(viewModel)
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
@CallSuper
|
open fun onAttachedToWindow() = Unit
|
||||||
open fun onAttachedToWindow() {
|
|
||||||
delegate.onAttachedToWindow()
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallSuper
|
open fun onDetachedFromWindow() = Unit
|
||||||
open fun onDetachedFromWindow() {
|
|
||||||
delegate.onDetachedFromWindow()
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
open fun onRecycled() {
|
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) {
|
protected fun SubsamplingScaleImageView.applyDownSampling(isForeground: Boolean) {
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ abstract class BasePagerReaderFragment : BaseReaderFragment<FragmentReaderPagerB
|
|||||||
override fun onCreateAdapter(): BaseReaderAdapter<*> = PagesAdapter(
|
override fun onCreateAdapter(): BaseReaderAdapter<*> = PagesAdapter(
|
||||||
lifecycleOwner = viewLifecycleOwner,
|
lifecycleOwner = viewLifecycleOwner,
|
||||||
loader = pageLoader,
|
loader = pageLoader,
|
||||||
settings = viewModel.readerSettings,
|
readerSettingsProducer = viewModel.readerSettingsProducer,
|
||||||
networkState = networkState,
|
networkState = networkState,
|
||||||
exceptionResolver = exceptionResolver,
|
exceptionResolver = exceptionResolver,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import kotlin.coroutines.suspendCoroutine
|
|||||||
@Suppress("LeakingThis")
|
@Suppress("LeakingThis")
|
||||||
abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
|
abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
|
||||||
private val loader: PageLoader,
|
private val loader: PageLoader,
|
||||||
private val readerSettings: ReaderSettings,
|
private val readerSettingsProducer: ReaderSettings.Producer,
|
||||||
private val networkState: NetworkState,
|
private val networkState: NetworkState,
|
||||||
private val exceptionResolver: ExceptionResolver,
|
private val exceptionResolver: ExceptionResolver,
|
||||||
) : RecyclerView.Adapter<H>() {
|
) : RecyclerView.Adapter<H>() {
|
||||||
@@ -58,7 +58,7 @@ abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
|
|||||||
final override fun onCreateViewHolder(
|
final override fun onCreateViewHolder(
|
||||||
parent: ViewGroup,
|
parent: ViewGroup,
|
||||||
viewType: Int,
|
viewType: Int,
|
||||||
): H = onCreateViewHolder(parent, loader, readerSettings, networkState, exceptionResolver)
|
): H = onCreateViewHolder(parent, loader, readerSettingsProducer, networkState, exceptionResolver)
|
||||||
|
|
||||||
suspend fun setItems(items: List<ReaderPage>) = suspendCoroutine { cont ->
|
suspend fun setItems(items: List<ReaderPage>) = suspendCoroutine { cont ->
|
||||||
differ.submitList(items) {
|
differ.submitList(items) {
|
||||||
@@ -69,7 +69,7 @@ abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
|
|||||||
protected abstract fun onCreateViewHolder(
|
protected abstract fun onCreateViewHolder(
|
||||||
parent: ViewGroup,
|
parent: ViewGroup,
|
||||||
loader: PageLoader,
|
loader: PageLoader,
|
||||||
settings: ReaderSettings,
|
readerSettingsProducer: ReaderSettings.Producer,
|
||||||
networkState: NetworkState,
|
networkState: NetworkState,
|
||||||
exceptionResolver: ExceptionResolver,
|
exceptionResolver: ExceptionResolver,
|
||||||
): H
|
): H
|
||||||
|
|||||||
@@ -17,10 +17,17 @@ class DoublePageHolder(
|
|||||||
owner: LifecycleOwner,
|
owner: LifecycleOwner,
|
||||||
binding: ItemPageBinding,
|
binding: ItemPageBinding,
|
||||||
loader: PageLoader,
|
loader: PageLoader,
|
||||||
settings: ReaderSettings,
|
readerSettingsProducer: ReaderSettings.Producer,
|
||||||
networkState: NetworkState,
|
networkState: NetworkState,
|
||||||
exceptionResolver: ExceptionResolver,
|
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
|
private val isEven: Boolean
|
||||||
get() = bindingAdapterPosition and 1 == 0
|
get() = bindingAdapterPosition and 1 == 0
|
||||||
@@ -35,7 +42,7 @@ class DoublePageHolder(
|
|||||||
.gravity = (if (isEven) Gravity.START else Gravity.END) or Gravity.BOTTOM
|
.gravity = (if (isEven) Gravity.START else Gravity.END) or Gravity.BOTTOM
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onImageShowing(settings: ReaderSettings, isPreview: Boolean) {
|
override fun onReady() {
|
||||||
with(binding.ssiv) {
|
with(binding.ssiv) {
|
||||||
maxScale = 2f * maxOf(
|
maxScale = 2f * maxOf(
|
||||||
width / sWidth.toFloat(),
|
width / sWidth.toFloat(),
|
||||||
|
|||||||
@@ -13,22 +13,22 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
|||||||
class DoublePagesAdapter(
|
class DoublePagesAdapter(
|
||||||
private val lifecycleOwner: LifecycleOwner,
|
private val lifecycleOwner: LifecycleOwner,
|
||||||
loader: PageLoader,
|
loader: PageLoader,
|
||||||
settings: ReaderSettings,
|
readerSettingsProducer: ReaderSettings.Producer,
|
||||||
networkState: NetworkState,
|
networkState: NetworkState,
|
||||||
exceptionResolver: ExceptionResolver,
|
exceptionResolver: ExceptionResolver,
|
||||||
) : BaseReaderAdapter<DoublePageHolder>(loader, settings, networkState, exceptionResolver) {
|
) : BaseReaderAdapter<DoublePageHolder>(loader, readerSettingsProducer, networkState, exceptionResolver) {
|
||||||
|
|
||||||
override fun onCreateViewHolder(
|
override fun onCreateViewHolder(
|
||||||
parent: ViewGroup,
|
parent: ViewGroup,
|
||||||
loader: PageLoader,
|
loader: PageLoader,
|
||||||
settings: ReaderSettings,
|
readerSettingsProducer: ReaderSettings.Producer,
|
||||||
networkState: NetworkState,
|
networkState: NetworkState,
|
||||||
exceptionResolver: ExceptionResolver,
|
exceptionResolver: ExceptionResolver,
|
||||||
) = DoublePageHolder(
|
) = DoublePageHolder(
|
||||||
owner = lifecycleOwner,
|
owner = lifecycleOwner,
|
||||||
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||||
loader = loader,
|
loader = loader,
|
||||||
settings = settings,
|
readerSettingsProducer = readerSettingsProducer,
|
||||||
networkState = networkState,
|
networkState = networkState,
|
||||||
exceptionResolver = exceptionResolver,
|
exceptionResolver = exceptionResolver,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ open class DoubleReaderFragment : BaseReaderFragment<FragmentReaderDoubleBinding
|
|||||||
override fun onCreateAdapter() = DoublePagesAdapter(
|
override fun onCreateAdapter() = DoublePagesAdapter(
|
||||||
lifecycleOwner = viewLifecycleOwner,
|
lifecycleOwner = viewLifecycleOwner,
|
||||||
loader = pageLoader,
|
loader = pageLoader,
|
||||||
settings = viewModel.readerSettings,
|
readerSettingsProducer = viewModel.readerSettingsProducer,
|
||||||
networkState = networkState,
|
networkState = networkState,
|
||||||
exceptionResolver = exceptionResolver,
|
exceptionResolver = exceptionResolver,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,17 +17,24 @@ class ReversedPageHolder(
|
|||||||
owner: LifecycleOwner,
|
owner: LifecycleOwner,
|
||||||
binding: ItemPageBinding,
|
binding: ItemPageBinding,
|
||||||
loader: PageLoader,
|
loader: PageLoader,
|
||||||
settings: ReaderSettings,
|
readerSettingsProducer: ReaderSettings.Producer,
|
||||||
networkState: NetworkState,
|
networkState: NetworkState,
|
||||||
exceptionResolver: ExceptionResolver,
|
exceptionResolver: ExceptionResolver,
|
||||||
) : PageHolder(owner, binding, loader, settings, networkState, exceptionResolver) {
|
) : PageHolder(
|
||||||
|
owner = owner,
|
||||||
|
binding = binding,
|
||||||
|
loader = loader,
|
||||||
|
readerSettingsProducer = readerSettingsProducer,
|
||||||
|
networkState = networkState,
|
||||||
|
exceptionResolver = exceptionResolver,
|
||||||
|
) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
(binding.textViewNumber.layoutParams as FrameLayout.LayoutParams)
|
(binding.textViewNumber.layoutParams as FrameLayout.LayoutParams)
|
||||||
.gravity = Gravity.START or Gravity.BOTTOM
|
.gravity = Gravity.START or Gravity.BOTTOM
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onImageShowing(settings: ReaderSettings, isPreview: Boolean) {
|
override fun onReady() {
|
||||||
with(binding.ssiv) {
|
with(binding.ssiv) {
|
||||||
maxScale = 2f * maxOf(
|
maxScale = 2f * maxOf(
|
||||||
width / sWidth.toFloat(),
|
width / sWidth.toFloat(),
|
||||||
|
|||||||
@@ -13,22 +13,22 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
|||||||
class ReversedPagesAdapter(
|
class ReversedPagesAdapter(
|
||||||
private val lifecycleOwner: LifecycleOwner,
|
private val lifecycleOwner: LifecycleOwner,
|
||||||
loader: PageLoader,
|
loader: PageLoader,
|
||||||
settings: ReaderSettings,
|
readerSettingsProducer: ReaderSettings.Producer,
|
||||||
networkState: NetworkState,
|
networkState: NetworkState,
|
||||||
exceptionResolver: ExceptionResolver,
|
exceptionResolver: ExceptionResolver,
|
||||||
) : BaseReaderAdapter<ReversedPageHolder>(loader, settings, networkState, exceptionResolver) {
|
) : BaseReaderAdapter<ReversedPageHolder>(loader, readerSettingsProducer, networkState, exceptionResolver) {
|
||||||
|
|
||||||
override fun onCreateViewHolder(
|
override fun onCreateViewHolder(
|
||||||
parent: ViewGroup,
|
parent: ViewGroup,
|
||||||
loader: PageLoader,
|
loader: PageLoader,
|
||||||
settings: ReaderSettings,
|
readerSettingsProducer: ReaderSettings.Producer,
|
||||||
networkState: NetworkState,
|
networkState: NetworkState,
|
||||||
exceptionResolver: ExceptionResolver,
|
exceptionResolver: ExceptionResolver,
|
||||||
) = ReversedPageHolder(
|
) = ReversedPageHolder(
|
||||||
owner = lifecycleOwner,
|
owner = lifecycleOwner,
|
||||||
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||||
loader = loader,
|
loader = loader,
|
||||||
settings = settings,
|
readerSettingsProducer = readerSettingsProducer,
|
||||||
networkState = networkState,
|
networkState = networkState,
|
||||||
exceptionResolver = exceptionResolver,
|
exceptionResolver = exceptionResolver,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class ReversedReaderFragment : BasePagerReaderFragment() {
|
|||||||
override fun onCreateAdapter() = ReversedPagesAdapter(
|
override fun onCreateAdapter() = ReversedPagesAdapter(
|
||||||
lifecycleOwner = viewLifecycleOwner,
|
lifecycleOwner = viewLifecycleOwner,
|
||||||
loader = pageLoader,
|
loader = pageLoader,
|
||||||
settings = viewModel.readerSettings,
|
readerSettingsProducer = viewModel.readerSettingsProducer,
|
||||||
networkState = networkState,
|
networkState = networkState,
|
||||||
exceptionResolver = exceptionResolver,
|
exceptionResolver = exceptionResolver,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,23 +2,15 @@ package org.koitharu.kotatsu.reader.ui.pager.standard
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.graphics.PointF
|
import android.graphics.PointF
|
||||||
import android.view.View
|
|
||||||
import android.view.animation.DecelerateInterpolator
|
import android.view.animation.DecelerateInterpolator
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
|
||||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||||
import org.koitharu.kotatsu.core.os.NetworkState
|
import org.koitharu.kotatsu.core.os.NetworkState
|
||||||
import org.koitharu.kotatsu.core.ui.widgets.ZoomControl
|
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.databinding.ItemPageBinding
|
||||||
import org.koitharu.kotatsu.parsers.util.ifZero
|
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder
|
import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder
|
||||||
@@ -28,37 +20,25 @@ open class PageHolder(
|
|||||||
owner: LifecycleOwner,
|
owner: LifecycleOwner,
|
||||||
binding: ItemPageBinding,
|
binding: ItemPageBinding,
|
||||||
loader: PageLoader,
|
loader: PageLoader,
|
||||||
settings: ReaderSettings,
|
readerSettingsProducer: ReaderSettings.Producer,
|
||||||
networkState: NetworkState,
|
networkState: NetworkState,
|
||||||
exceptionResolver: ExceptionResolver,
|
exceptionResolver: ExceptionResolver,
|
||||||
) : BasePageHolder<ItemPageBinding>(binding, loader, settings, networkState, exceptionResolver, owner),
|
) : BasePageHolder<ItemPageBinding>(
|
||||||
View.OnClickListener,
|
binding = binding,
|
||||||
|
loader = loader,
|
||||||
|
readerSettingsProducer = readerSettingsProducer,
|
||||||
|
networkState = networkState,
|
||||||
|
exceptionResolver = exceptionResolver,
|
||||||
|
lifecycleOwner = owner,
|
||||||
|
),
|
||||||
ZoomControl.ZoomControlListener {
|
ZoomControl.ZoomControlListener {
|
||||||
|
|
||||||
init {
|
override val ssiv = binding.ssiv
|
||||||
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 fun onResume() {
|
override fun onConfigChanged(settings: ReaderSettings) {
|
||||||
super.onResume()
|
super.onConfigChanged(settings)
|
||||||
binding.ssiv.applyDownSampling(isForeground = true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
super.onPause()
|
|
||||||
binding.ssiv.applyDownSampling(isForeground = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onConfigChanged() {
|
|
||||||
super.onConfigChanged()
|
|
||||||
if (settings.applyBitmapConfig(binding.ssiv)) {
|
if (settings.applyBitmapConfig(binding.ssiv)) {
|
||||||
delegate.reload()
|
reloadImage()
|
||||||
}
|
}
|
||||||
binding.ssiv.applyDownSampling(isResumed())
|
binding.ssiv.applyDownSampling(isResumed())
|
||||||
binding.textViewNumber.isVisible = settings.isPagesNumbersEnabled
|
binding.textViewNumber.isVisible = settings.isPagesNumbersEnabled
|
||||||
@@ -66,7 +46,7 @@ open class PageHolder(
|
|||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onBind(data: ReaderPage) {
|
override fun onBind(data: ReaderPage) {
|
||||||
delegate.onBind(data.toMangaPage())
|
super.onBind(data)
|
||||||
binding.textViewNumber.text = (data.index + 1).toString()
|
binding.textViewNumber.text = (data.index + 1).toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,33 +55,7 @@ open class PageHolder(
|
|||||||
binding.ssiv.recycle()
|
binding.ssiv.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLoadingStarted() {
|
override fun onReady() {
|
||||||
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) {
|
|
||||||
binding.ssiv.maxScale = 2f * maxOf(
|
binding.ssiv.maxScale = 2f * maxOf(
|
||||||
binding.ssiv.width / binding.ssiv.sWidth.toFloat(),
|
binding.ssiv.width / binding.ssiv.sWidth.toFloat(),
|
||||||
binding.ssiv.height / binding.ssiv.sHeight.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() {
|
override fun onZoomIn() {
|
||||||
scaleBy(1.2f)
|
scaleBy(1.2f)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,22 +13,27 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
|||||||
class PagesAdapter(
|
class PagesAdapter(
|
||||||
private val lifecycleOwner: LifecycleOwner,
|
private val lifecycleOwner: LifecycleOwner,
|
||||||
loader: PageLoader,
|
loader: PageLoader,
|
||||||
settings: ReaderSettings,
|
readerSettingsProducer: ReaderSettings.Producer,
|
||||||
networkState: NetworkState,
|
networkState: NetworkState,
|
||||||
exceptionResolver: ExceptionResolver,
|
exceptionResolver: ExceptionResolver,
|
||||||
) : BaseReaderAdapter<PageHolder>(loader, settings, networkState, exceptionResolver) {
|
) : BaseReaderAdapter<PageHolder>(
|
||||||
|
loader = loader,
|
||||||
|
readerSettingsProducer = readerSettingsProducer,
|
||||||
|
networkState = networkState,
|
||||||
|
exceptionResolver = exceptionResolver,
|
||||||
|
) {
|
||||||
|
|
||||||
override fun onCreateViewHolder(
|
override fun onCreateViewHolder(
|
||||||
parent: ViewGroup,
|
parent: ViewGroup,
|
||||||
loader: PageLoader,
|
loader: PageLoader,
|
||||||
settings: ReaderSettings,
|
readerSettingsProducer: ReaderSettings.Producer,
|
||||||
networkState: NetworkState,
|
networkState: NetworkState,
|
||||||
exceptionResolver: ExceptionResolver,
|
exceptionResolver: ExceptionResolver,
|
||||||
) = PageHolder(
|
) = PageHolder(
|
||||||
owner = lifecycleOwner,
|
owner = lifecycleOwner,
|
||||||
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||||
loader = loader,
|
loader = loader,
|
||||||
settings = settings,
|
readerSettingsProducer = readerSettingsProducer,
|
||||||
networkState = networkState,
|
networkState = networkState,
|
||||||
exceptionResolver = exceptionResolver,
|
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.ComponentCallbacks2
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.lifecycle.Observer
|
import androidx.annotation.WorkerThread
|
||||||
import com.davemorrissey.labs.subscaleview.DefaultOnImageEventListener
|
import com.davemorrissey.labs.subscaleview.DefaultOnImageEventListener
|
||||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
@@ -14,41 +14,38 @@ import kotlinx.coroutines.Job
|
|||||||
import kotlinx.coroutines.cancelAndJoin
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import kotlinx.coroutines.withContext
|
import okio.IOException
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||||
import org.koitharu.kotatsu.core.os.NetworkState
|
import org.koitharu.kotatsu.core.os.NetworkState
|
||||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
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.parsers.model.MangaPage
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
class PageHolderDelegate(
|
class PageViewModel(
|
||||||
private val loader: PageLoader,
|
private val loader: PageLoader,
|
||||||
private val readerSettings: ReaderSettings,
|
val settingsProducer: ReaderSettings.Producer,
|
||||||
private val callback: Callback,
|
|
||||||
private val networkState: NetworkState,
|
private val networkState: NetworkState,
|
||||||
private val exceptionResolver: ExceptionResolver,
|
private val exceptionResolver: ExceptionResolver,
|
||||||
private val isWebtoon: Boolean,
|
private val isWebtoon: Boolean,
|
||||||
) : DefaultOnImageEventListener, Observer<ReaderSettings>, ComponentCallbacks2 {
|
) : DefaultOnImageEventListener, ComponentCallbacks2 {
|
||||||
|
|
||||||
private val scope = loader.loaderScope + Dispatchers.Main.immediate
|
private val scope = loader.loaderScope + Dispatchers.Main.immediate
|
||||||
var state = State.EMPTY
|
|
||||||
private set
|
|
||||||
private var job: Job? = null
|
private var job: Job? = null
|
||||||
private var uri: Uri? = null
|
|
||||||
private var cachedBounds: Rect? = null
|
private var cachedBounds: Rect? = null
|
||||||
private var error: Throwable? = null
|
|
||||||
|
val state = MutableStateFlow<PageState>(PageState.Empty)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
scope.launch(Dispatchers.Main) { // the same as post() -- wait until child fields 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) {
|
fun onBind(page: MangaPage) {
|
||||||
val prevJob = job
|
val prevJob = job
|
||||||
job = scope.launch {
|
job = scope.launch(Dispatchers.Default) {
|
||||||
prevJob?.cancelAndJoin()
|
prevJob?.cancelAndJoin()
|
||||||
doLoad(page, force = false)
|
doLoad(page, force = false)
|
||||||
}
|
}
|
||||||
@@ -66,8 +63,8 @@ class PageHolderDelegate(
|
|||||||
val prevJob = job
|
val prevJob = job
|
||||||
job = scope.launch {
|
job = scope.launch {
|
||||||
prevJob?.cancelAndJoin()
|
prevJob?.cancelAndJoin()
|
||||||
val e = error
|
val e = (state.value as? PageState.Error)?.error
|
||||||
if (e != null && ExceptionResolver.canResolve(e)) {
|
if (e != null && ExceptionResolver.Companion.canResolve(e)) {
|
||||||
if (!isFromUser) {
|
if (!isFromUser) {
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
@@ -78,75 +75,38 @@ class PageHolderDelegate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun showErrorDetails(url: String?) {
|
fun showErrorDetails(url: String?) {
|
||||||
val e = error ?: return
|
val e = (state.value as? PageState.Error)?.error ?: return
|
||||||
exceptionResolver.showErrorDetails(e, url)
|
exceptionResolver.showErrorDetails(e, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAttachedToWindow() {
|
|
||||||
readerSettings.observeForever(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onDetachedFromWindow() {
|
|
||||||
readerSettings.removeObserver(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onRecycle() {
|
fun onRecycle() {
|
||||||
state = State.EMPTY
|
state.value = PageState.Empty
|
||||||
uri = null
|
|
||||||
cachedBounds = null
|
cachedBounds = null
|
||||||
error = null
|
|
||||||
job?.cancel()
|
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() {
|
override fun onImageLoaded() {
|
||||||
if (state >= State.LOADED) {
|
state.update {
|
||||||
state = State.SHOWN
|
if (it is PageState.Loaded) PageState.Shown(it.source, it.isConverted) else it
|
||||||
error = null
|
|
||||||
callback.onImageShown(isPreview = false)
|
|
||||||
} else if (state == State.LOADING_WITH_PREVIEW) {
|
|
||||||
callback.onImageShown(isPreview = true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onImageLoadError(e: Throwable) {
|
override fun onImageLoadError(e: Throwable) {
|
||||||
e.printStackTraceDebug()
|
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) {
|
state.update { currentState ->
|
||||||
if (state == State.SHOWN) {
|
if (currentState is PageState.Loaded) {
|
||||||
callback.onImageShowing(readerSettings, isPreview = false)
|
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
|
override fun onConfigurationChanged(newConfig: Configuration) = Unit
|
||||||
@@ -155,68 +115,58 @@ class PageHolderDelegate(
|
|||||||
override fun onLowMemory() = Unit
|
override fun onLowMemory() = Unit
|
||||||
|
|
||||||
override fun onTrimMemory(level: Int) {
|
override fun onTrimMemory(level: Int) {
|
||||||
callback.onTrimMemory()
|
// callback.onTrimMemory()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun tryConvert(uri: Uri, e: Exception) {
|
private fun tryConvert(uri: Uri, e: Exception) {
|
||||||
val prevJob = job
|
val prevJob = job
|
||||||
job = scope.launch {
|
job = scope.launch {
|
||||||
prevJob?.join()
|
prevJob?.join()
|
||||||
state = State.CONVERTING
|
state.value = PageState.Converting()
|
||||||
try {
|
try {
|
||||||
val newUri = loader.convertBimap(uri)
|
val newUri = loader.convertBimap(uri)
|
||||||
cachedBounds = if (readerSettings.isPagesCropEnabled(isWebtoon)) {
|
cachedBounds = if (settingsProducer.value.isPagesCropEnabled(isWebtoon)) {
|
||||||
loader.getTrimmedBounds(newUri)
|
loader.getTrimmedBounds(newUri)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
state = State.CONVERTED
|
state.value = PageState.Loaded(uri.toImageSource(cachedBounds), isConverted = true)
|
||||||
callback.onImageReady(newUri.toImageSource(cachedBounds))
|
|
||||||
} catch (ce: CancellationException) {
|
} catch (ce: CancellationException) {
|
||||||
throw ce
|
throw ce
|
||||||
} catch (e2: Throwable) {
|
} catch (e2: Throwable) {
|
||||||
e2.printStackTrace()
|
e2.printStackTrace()
|
||||||
e.addSuppressed(e2)
|
e.addSuppressed(e2)
|
||||||
state = State.ERROR
|
state.value = PageState.Error(e)
|
||||||
callback.onError(e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
private suspend fun doLoad(data: MangaPage, force: Boolean) = coroutineScope {
|
private suspend fun doLoad(data: MangaPage, force: Boolean) = coroutineScope {
|
||||||
state = State.LOADING
|
state.value = PageState.Loading(null/* TODO */, -1)
|
||||||
error = null
|
|
||||||
callback.onLoadingStarted()
|
|
||||||
val previewJob = launch {
|
val previewJob = launch {
|
||||||
val preview = loader.loadPreview(data)
|
val preview = loader.loadPreview(data) ?: return@launch
|
||||||
if (preview != null && state == State.LOADING) {
|
state.update {
|
||||||
state = State.LOADING_WITH_PREVIEW
|
if (it is PageState.Loading) it.copy(preview = preview) else it
|
||||||
callback.onPreviewReady(preview)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
val task = withContext(Dispatchers.Default) {
|
val task = loader.loadPageAsync(data, force)
|
||||||
loader.loadPageAsync(data, force)
|
|
||||||
}
|
|
||||||
val progressObserver = observeProgress(this, task.progressAsFlow())
|
val progressObserver = observeProgress(this, task.progressAsFlow())
|
||||||
val file = task.await()
|
val uri = task.await()
|
||||||
progressObserver.cancelAndJoin()
|
progressObserver.cancelAndJoin()
|
||||||
previewJob.cancel()
|
previewJob.cancel()
|
||||||
uri = file
|
cachedBounds = if (settingsProducer.value.isPagesCropEnabled(isWebtoon)) {
|
||||||
state = State.LOADED
|
loader.getTrimmedBounds(uri)
|
||||||
cachedBounds = if (readerSettings.isPagesCropEnabled(isWebtoon)) {
|
|
||||||
loader.getTrimmedBounds(checkNotNull(uri))
|
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
callback.onImageReady(checkNotNull(uri).toImageSource(cachedBounds))
|
state.value = PageState.Loaded(uri.toImageSource(cachedBounds), isConverted = false)
|
||||||
} catch (e: CancellationException) {
|
} catch (e: CancellationException) {
|
||||||
throw e
|
throw e
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
e.printStackTraceDebug()
|
e.printStackTraceDebug()
|
||||||
state = State.ERROR
|
state.value = PageState.Error(e)
|
||||||
error = e
|
|
||||||
callback.onError(e)
|
|
||||||
if (e is IOException && !networkState.value) {
|
if (e is IOException && !networkState.value) {
|
||||||
networkState.awaitForConnection()
|
networkState.awaitForConnection()
|
||||||
retry(data, isFromUser = false)
|
retry(data, isFromUser = false)
|
||||||
@@ -225,9 +175,17 @@ class PageHolderDelegate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun observeProgress(scope: CoroutineScope, progress: Flow<Float>) = progress
|
private fun observeProgress(scope: CoroutineScope, progress: Flow<Float>) = progress
|
||||||
.debounce(250)
|
.throttle(250)
|
||||||
.onEach { callback.onProgressChanged((100 * it).toInt()) }
|
.onEach {
|
||||||
.launchIn(scope)
|
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 {
|
private fun Uri.toImageSource(bounds: Rect?): ImageSource {
|
||||||
val source = ImageSource.uri(this)
|
val source = ImageSource.uri(this)
|
||||||
@@ -237,29 +195,4 @@ class PageHolderDelegate(
|
|||||||
source
|
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(
|
class WebtoonAdapter(
|
||||||
private val lifecycleOwner: LifecycleOwner,
|
private val lifecycleOwner: LifecycleOwner,
|
||||||
loader: PageLoader,
|
loader: PageLoader,
|
||||||
settings: ReaderSettings,
|
readerSettingsProducer: ReaderSettings.Producer,
|
||||||
networkState: NetworkState,
|
networkState: NetworkState,
|
||||||
exceptionResolver: ExceptionResolver,
|
exceptionResolver: ExceptionResolver,
|
||||||
) : BaseReaderAdapter<WebtoonHolder>(loader, settings, networkState, exceptionResolver) {
|
) : BaseReaderAdapter<WebtoonHolder>(loader, readerSettingsProducer, networkState, exceptionResolver) {
|
||||||
|
|
||||||
override fun onCreateViewHolder(
|
override fun onCreateViewHolder(
|
||||||
parent: ViewGroup,
|
parent: ViewGroup,
|
||||||
loader: PageLoader,
|
loader: PageLoader,
|
||||||
settings: ReaderSettings,
|
readerSettingsProducer: ReaderSettings.Producer,
|
||||||
networkState: NetworkState,
|
networkState: NetworkState,
|
||||||
exceptionResolver: ExceptionResolver,
|
exceptionResolver: ExceptionResolver,
|
||||||
) = WebtoonHolder(
|
) = WebtoonHolder(
|
||||||
@@ -32,7 +32,7 @@ class WebtoonAdapter(
|
|||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
loader = loader,
|
loader = loader,
|
||||||
settings = settings,
|
readerSettingsProducer = readerSettingsProducer,
|
||||||
networkState = networkState,
|
networkState = networkState,
|
||||||
exceptionResolver = exceptionResolver,
|
exceptionResolver = exceptionResolver,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,65 +1,43 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.pager.webtoon
|
package org.koitharu.kotatsu.reader.ui.pager.webtoon
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
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.exceptions.resolve.ExceptionResolver
|
||||||
import org.koitharu.kotatsu.core.os.NetworkState
|
import org.koitharu.kotatsu.core.os.NetworkState
|
||||||
import org.koitharu.kotatsu.core.util.GoneOnInvisibleListener
|
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.databinding.ItemPageWebtoonBinding
|
||||||
import org.koitharu.kotatsu.parsers.util.ifZero
|
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder
|
import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
|
||||||
|
|
||||||
class WebtoonHolder(
|
class WebtoonHolder(
|
||||||
owner: LifecycleOwner,
|
owner: LifecycleOwner,
|
||||||
binding: ItemPageWebtoonBinding,
|
binding: ItemPageWebtoonBinding,
|
||||||
loader: PageLoader,
|
loader: PageLoader,
|
||||||
settings: ReaderSettings,
|
readerSettingsProducer: ReaderSettings.Producer,
|
||||||
networkState: NetworkState,
|
networkState: NetworkState,
|
||||||
exceptionResolver: ExceptionResolver,
|
exceptionResolver: ExceptionResolver,
|
||||||
) : BasePageHolder<ItemPageWebtoonBinding>(binding, loader, settings, networkState, exceptionResolver, owner),
|
) : BasePageHolder<ItemPageWebtoonBinding>(
|
||||||
View.OnClickListener {
|
binding = binding,
|
||||||
|
loader = loader,
|
||||||
|
readerSettingsProducer = readerSettingsProducer,
|
||||||
|
networkState = networkState,
|
||||||
|
exceptionResolver = exceptionResolver,
|
||||||
|
lifecycleOwner = owner,
|
||||||
|
) {
|
||||||
|
|
||||||
|
override val ssiv = binding.ssiv
|
||||||
|
|
||||||
private var scrollToRestore = 0
|
private var scrollToRestore = 0
|
||||||
private val goneOnInvisibleListener = GoneOnInvisibleListener(bindingInfo.progressBar)
|
private val goneOnInvisibleListener = GoneOnInvisibleListener(bindingInfo.progressBar)
|
||||||
|
|
||||||
init {
|
override fun onConfigChanged(settings: ReaderSettings) {
|
||||||
binding.ssiv.bindToLifecycle(owner)
|
super.onConfigChanged(settings)
|
||||||
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()
|
|
||||||
if (settings.applyBitmapConfig(binding.ssiv)) {
|
if (settings.applyBitmapConfig(binding.ssiv)) {
|
||||||
delegate.reload()
|
reloadImage()
|
||||||
}
|
}
|
||||||
binding.ssiv.applyDownSampling(isResumed())
|
binding.ssiv.applyDownSampling(isResumed())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(data: ReaderPage) {
|
|
||||||
delegate.onBind(data.toMangaPage())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRecycled() {
|
override fun onRecycled() {
|
||||||
super.onRecycled()
|
super.onRecycled()
|
||||||
binding.ssiv.recycle()
|
binding.ssiv.recycle()
|
||||||
@@ -75,31 +53,7 @@ class WebtoonHolder(
|
|||||||
goneOnInvisibleListener.detach()
|
goneOnInvisibleListener.detach()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLoadingStarted() {
|
override fun onReady() {
|
||||||
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) {
|
|
||||||
binding.ssiv.colorFilter = settings.colorFilter?.toColorFilter()
|
binding.ssiv.colorFilter = settings.colorFilter?.toColorFilter()
|
||||||
with(binding.ssiv) {
|
with(binding.ssiv) {
|
||||||
scrollTo(
|
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 getScrollY() = binding.ssiv.getScroll()
|
||||||
|
|
||||||
fun restoreScroll(scroll: Int) {
|
fun restoreScroll(scroll: Int) {
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
|||||||
rv.addItemDecoration(WebtoonGapsDecoration())
|
rv.addItemDecoration(WebtoonGapsDecoration())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
viewModel.readerSettings.observe(viewLifecycleOwner) {
|
viewModel.readerSettingsProducer.observe(viewLifecycleOwner) {
|
||||||
it.applyBackground(binding.root)
|
it.applyBackground(binding.root)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,7 +81,7 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
|||||||
override fun onCreateAdapter() = WebtoonAdapter(
|
override fun onCreateAdapter() = WebtoonAdapter(
|
||||||
lifecycleOwner = viewLifecycleOwner,
|
lifecycleOwner = viewLifecycleOwner,
|
||||||
loader = pageLoader,
|
loader = pageLoader,
|
||||||
settings = viewModel.readerSettings,
|
readerSettingsProducer = viewModel.readerSettingsProducer,
|
||||||
networkState = networkState,
|
networkState = networkState,
|
||||||
exceptionResolver = exceptionResolver,
|
exceptionResolver = exceptionResolver,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
<string name="add">Add</string>
|
<string name="add">Add</string>
|
||||||
<string name="save">Save</string>
|
<string name="save">Save</string>
|
||||||
<string name="share">Share</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="share_s">Share %s</string>
|
||||||
<string name="search">Search</string>
|
<string name="search">Search</string>
|
||||||
<string name="search_manga">Search manga</string>
|
<string name="search_manga">Search manga</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user