Replace LiveData with StateFlow
This commit is contained in:
@@ -4,8 +4,8 @@ import android.util.LongSparseArray
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.koitharu.kotatsu.core.model.DoubleManga
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.details.domain.model.DoubleManga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package org.koitharu.kotatsu.reader.domain
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.util.Size
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import okhttp3.OkHttpClient
|
||||
import org.koitharu.kotatsu.core.model.findChapter
|
||||
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.util.await
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import java.io.InputStream
|
||||
import java.util.zip.ZipFile
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class DetectReaderModeUseCase @Inject constructor(
|
||||
private val dataRepository: MangaDataRepository,
|
||||
private val settings: AppSettings,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
@MangaHttpClient private val okHttpClient: OkHttpClient,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(manga: Manga, state: ReaderState?): ReaderMode {
|
||||
dataRepository.getReaderMode(manga.id)?.let { return it }
|
||||
val defaultMode = settings.defaultReaderMode
|
||||
if (!settings.isReaderModeDetectionEnabled || defaultMode == ReaderMode.WEBTOON) {
|
||||
return defaultMode
|
||||
}
|
||||
val chapter = state?.let { manga.findChapter(it.chapterId) }
|
||||
?: manga.chapters?.firstOrNull()
|
||||
?: error("There are no chapters in this manga")
|
||||
val repo = mangaRepositoryFactory.create(manga.source)
|
||||
val pages = repo.getPages(chapter)
|
||||
return runCatchingCancellable {
|
||||
val isWebtoon = guessMangaIsWebtoon(repo, pages)
|
||||
if (isWebtoon) ReaderMode.WEBTOON else defaultMode
|
||||
}.onSuccess {
|
||||
dataRepository.saveReaderMode(manga, it)
|
||||
}.onFailure {
|
||||
it.printStackTraceDebug()
|
||||
}.getOrDefault(defaultMode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatic determine type of manga by page size
|
||||
* @return ReaderMode.WEBTOON if page is wide
|
||||
*/
|
||||
private suspend fun guessMangaIsWebtoon(repository: MangaRepository, pages: List<MangaPage>): Boolean {
|
||||
val pageIndex = (pages.size * 0.3).roundToInt()
|
||||
val page = requireNotNull(pages.getOrNull(pageIndex)) { "No pages" }
|
||||
val url = repository.getPageUrl(page)
|
||||
val uri = Uri.parse(url)
|
||||
val size = if (uri.scheme == "cbz") {
|
||||
runInterruptible(Dispatchers.IO) {
|
||||
val zip = ZipFile(uri.schemeSpecificPart)
|
||||
val entry = zip.getEntry(uri.fragment)
|
||||
zip.getInputStream(entry).use {
|
||||
getBitmapSize(it)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val request = PageLoader.createPageRequest(page, url)
|
||||
okHttpClient.newCall(request).await().use {
|
||||
runInterruptible(Dispatchers.IO) {
|
||||
getBitmapSize(it.body?.byteStream())
|
||||
}
|
||||
}
|
||||
}
|
||||
return size.width * MIN_WEBTOON_RATIO < size.height
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val MIN_WEBTOON_RATIO = 1.8
|
||||
|
||||
private fun getBitmapSize(input: InputStream?): Size {
|
||||
val options = BitmapFactory.Options().apply {
|
||||
inJustDecodeBounds = true
|
||||
}
|
||||
BitmapFactory.decodeStream(input, null, options)?.recycle()
|
||||
val imageHeight: Int = options.outHeight
|
||||
val imageWidth: Int = options.outWidth
|
||||
check(imageHeight > 0 && imageWidth > 0)
|
||||
return Size(imageWidth, imageHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.koitharu.kotatsu.reader.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
@@ -15,7 +16,6 @@ import okio.IOException
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import okio.source
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
|
||||
@@ -74,7 +74,7 @@ class PageSaveHelper @Inject constructor(
|
||||
var extension = name.substringAfterLast('.', "")
|
||||
name = name.substringBeforeLast('.')
|
||||
if (extension.length !in 2..4) {
|
||||
val mimeType = MangaDataRepository.getImageMimeType(file)
|
||||
val mimeType = getImageMimeType(file)
|
||||
extension = if (mimeType != null) {
|
||||
MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: EXTENSION_FALLBACK
|
||||
} else {
|
||||
@@ -83,4 +83,12 @@ class PageSaveHelper @Inject constructor(
|
||||
}
|
||||
return name.toFileNameSafe().take(MAX_FILENAME_LENGTH) + "." + extension
|
||||
}
|
||||
|
||||
private suspend fun getImageMimeType(file: File): String? = runInterruptible(Dispatchers.IO) {
|
||||
val options = BitmapFactory.Options().apply {
|
||||
inJustDecodeBounds = true
|
||||
}
|
||||
BitmapFactory.decodeFile(file.path, options)?.recycle()
|
||||
options.outMimeType
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,9 +42,11 @@ import org.koitharu.kotatsu.core.util.IdlingDetector
|
||||
import org.koitharu.kotatsu.core.util.ShareHelper
|
||||
import org.koitharu.kotatsu.core.util.ext.hasGlobalPoint
|
||||
import org.koitharu.kotatsu.core.util.ext.isRtl
|
||||
import org.koitharu.kotatsu.core.util.ext.observeWithPrevious
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.postDelayed
|
||||
import org.koitharu.kotatsu.core.util.ext.setValueRounded
|
||||
import org.koitharu.kotatsu.core.util.ext.zipWithPrevious
|
||||
import org.koitharu.kotatsu.databinding.ActivityReaderBinding
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
@@ -108,7 +110,7 @@ class ReaderActivity :
|
||||
insetsDelegate.interceptingWindowInsetsListener = this
|
||||
idlingDetector.bindToLifecycle(this)
|
||||
|
||||
viewModel.onError.observe(
|
||||
viewModel.onError.observeEvent(
|
||||
this,
|
||||
DialogErrorObserver(
|
||||
host = viewBinding.container,
|
||||
@@ -117,23 +119,23 @@ class ReaderActivity :
|
||||
onResolved = { isResolved ->
|
||||
if (isResolved) {
|
||||
viewModel.reload()
|
||||
} else if (viewModel.content.value?.pages.isNullOrEmpty()) {
|
||||
} else if (viewModel.content.value.pages.isEmpty()) {
|
||||
finishAfterTransition()
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
viewModel.readerMode.observe(this, this::onInitReader)
|
||||
viewModel.onPageSaved.observe(this, this::onPageSaved)
|
||||
viewModel.uiState.observeWithPrevious(this, this::onUiStateChanged)
|
||||
viewModel.onPageSaved.observeEvent(this, this::onPageSaved)
|
||||
viewModel.uiState.zipWithPrevious().observe(this, this::onUiStateChanged)
|
||||
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
|
||||
viewModel.content.observe(this) {
|
||||
onLoadingStateChanged(viewModel.isLoading.value == true)
|
||||
onLoadingStateChanged(viewModel.isLoading.value)
|
||||
}
|
||||
viewModel.isScreenshotsBlockEnabled.observe(this, this::setWindowSecure)
|
||||
viewModel.isInfoBarEnabled.observe(this, ::onReaderBarChanged)
|
||||
viewModel.isBookmarkAdded.observe(this, this::onBookmarkStateChanged)
|
||||
viewModel.onShowToast.observe(this) { msgId ->
|
||||
viewModel.onShowToast.observeEvent(this) { msgId ->
|
||||
Snackbar.make(viewBinding.container, msgId, Snackbar.LENGTH_SHORT)
|
||||
.setAnchorView(viewBinding.appbarBottom)
|
||||
.show()
|
||||
@@ -150,7 +152,10 @@ class ReaderActivity :
|
||||
viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState())
|
||||
}
|
||||
|
||||
private fun onInitReader(mode: ReaderMode) {
|
||||
private fun onInitReader(mode: ReaderMode?) {
|
||||
if (mode == null) {
|
||||
return
|
||||
}
|
||||
if (readerManager.currentMode != mode) {
|
||||
readerManager.replace(mode)
|
||||
}
|
||||
@@ -190,7 +195,7 @@ class ReaderActivity :
|
||||
}
|
||||
|
||||
R.id.action_bookmark -> {
|
||||
if (viewModel.isBookmarkAdded.value == true) {
|
||||
if (viewModel.isBookmarkAdded.value) {
|
||||
viewModel.removeBookmark()
|
||||
} else {
|
||||
viewModel.addBookmark()
|
||||
@@ -209,7 +214,7 @@ class ReaderActivity :
|
||||
}
|
||||
|
||||
private fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
val hasPages = !viewModel.content.value?.pages.isNullOrEmpty()
|
||||
val hasPages = viewModel.content.value.pages.isNotEmpty()
|
||||
viewBinding.layoutLoading.isVisible = isLoading && !hasPages
|
||||
if (isLoading && hasPages) {
|
||||
viewBinding.toastView.show(R.string.loading_)
|
||||
@@ -260,7 +265,7 @@ class ReaderActivity :
|
||||
|
||||
override fun onPageSelected(page: ReaderPage) {
|
||||
lifecycleScope.launch(Dispatchers.Default) {
|
||||
val pages = viewModel.content.value?.pages ?: return@launch
|
||||
val pages = viewModel.content.value.pages
|
||||
val index = pages.indexOfFirst { it.chapterId == page.chapterId && it.id == page.id }
|
||||
if (index != -1) {
|
||||
withContext(Dispatchers.Main) {
|
||||
@@ -311,7 +316,7 @@ class ReaderActivity :
|
||||
TransitionManager.beginDelayedTransition(viewBinding.root, transition)
|
||||
viewBinding.appbarTop.isVisible = isUiVisible
|
||||
viewBinding.appbarBottom?.isVisible = isUiVisible
|
||||
viewBinding.infoBar.isGone = isUiVisible || (viewModel.isInfoBarEnabled.value == false)
|
||||
viewBinding.infoBar.isGone = isUiVisible || (!viewModel.isInfoBarEnabled.value)
|
||||
if (isUiVisible) {
|
||||
showSystemUI()
|
||||
} else {
|
||||
@@ -367,7 +372,8 @@ class ReaderActivity :
|
||||
menuItem.setIcon(if (isAdded) R.drawable.ic_bookmark_added else R.drawable.ic_bookmark)
|
||||
}
|
||||
|
||||
private fun onUiStateChanged(uiState: ReaderUiState?, previous: ReaderUiState?) {
|
||||
private fun onUiStateChanged(pair: Pair<ReaderUiState?, ReaderUiState?>) {
|
||||
val (uiState: ReaderUiState?, previous: ReaderUiState?) = pair
|
||||
title = uiState?.chapterName ?: uiState?.mangaName ?: getString(R.string.loading_)
|
||||
viewBinding.infoBar.update(uiState)
|
||||
if (uiState == null) {
|
||||
|
||||
@@ -5,8 +5,6 @@ import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
@@ -28,35 +26,32 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
||||
import org.koitharu.kotatsu.core.model.DoubleManga
|
||||
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||
import org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsLiveData
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.util.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.core.util.asFlowLiveData
|
||||
import org.koitharu.kotatsu.core.util.ext.emitValue
|
||||
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.core.util.ext.requireValue
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
|
||||
import org.koitharu.kotatsu.local.domain.DoubleMangaLoader
|
||||
import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase
|
||||
import org.koitharu.kotatsu.details.domain.model.DoubleManga
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
|
||||
import org.koitharu.kotatsu.history.domain.HistoryUpdateUseCase
|
||||
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.reader.domain.ChaptersLoader
|
||||
import org.koitharu.kotatsu.reader.domain.DetectReaderModeUseCase
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
|
||||
@@ -70,7 +65,6 @@ private const val PREFETCH_LIMIT = 10
|
||||
@HiltViewModel
|
||||
class ReaderViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
private val dataRepository: MangaDataRepository,
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val bookmarksRepository: BookmarksRepository,
|
||||
@@ -79,7 +73,9 @@ class ReaderViewModel @Inject constructor(
|
||||
private val pageLoader: PageLoader,
|
||||
private val chaptersLoader: ChaptersLoader,
|
||||
private val shortcutsUpdater: ShortcutsUpdater,
|
||||
private val mangaLoader: DoubleMangaLoader,
|
||||
private val doubleMangaLoadUseCase: DoubleMangaLoadUseCase,
|
||||
private val historyUpdateUseCase: HistoryUpdateUseCase,
|
||||
private val detectReaderModeUseCase: DetectReaderModeUseCase,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val intent = MangaIntent(savedStateHandle)
|
||||
@@ -95,29 +91,29 @@ class ReaderViewModel @Inject constructor(
|
||||
private val mangaFlow: Flow<Manga?>
|
||||
get() = mangaData.map { it?.any }
|
||||
|
||||
val readerMode = MutableLiveData<ReaderMode>()
|
||||
val onPageSaved = SingleLiveEvent<Uri?>()
|
||||
val onShowToast = SingleLiveEvent<Int>()
|
||||
val uiState = MutableLiveData<ReaderUiState?>(null)
|
||||
val readerMode = MutableStateFlow<ReaderMode?>(null)
|
||||
val onPageSaved = MutableEventFlow<Uri?>()
|
||||
val onShowToast = MutableEventFlow<Int>()
|
||||
val uiState = MutableStateFlow<ReaderUiState?>(null)
|
||||
|
||||
val content = MutableLiveData(ReaderContent(emptyList(), null))
|
||||
val content = MutableStateFlow(ReaderContent(emptyList(), null))
|
||||
val manga: DoubleManga?
|
||||
get() = mangaData.value
|
||||
|
||||
val readerAnimation = settings.observeAsLiveData(
|
||||
context = viewModelScope.coroutineContext + Dispatchers.Default,
|
||||
val readerAnimation = settings.observeAsStateFlow(
|
||||
scope = viewModelScope + Dispatchers.Default,
|
||||
key = AppSettings.KEY_READER_ANIMATION,
|
||||
valueProducer = { readerAnimation },
|
||||
)
|
||||
|
||||
val isInfoBarEnabled = settings.observeAsLiveData(
|
||||
context = viewModelScope.coroutineContext + Dispatchers.Default,
|
||||
val isInfoBarEnabled = settings.observeAsStateFlow(
|
||||
scope = viewModelScope + Dispatchers.Default,
|
||||
key = AppSettings.KEY_READER_BAR,
|
||||
valueProducer = { isReaderBarEnabled },
|
||||
)
|
||||
|
||||
val isWebtoonZoomEnabled = settings.observeAsLiveData(
|
||||
context = viewModelScope.coroutineContext + Dispatchers.Default,
|
||||
val isWebtoonZoomEnabled = settings.observeAsStateFlow(
|
||||
scope = viewModelScope + Dispatchers.Default,
|
||||
key = AppSettings.KEY_WEBTOON_ZOOM,
|
||||
valueProducer = { isWebtoonZoomEnable },
|
||||
)
|
||||
@@ -136,9 +132,9 @@ class ReaderViewModel @Inject constructor(
|
||||
) { manga, policy ->
|
||||
policy == ScreenshotsPolicy.BLOCK_ALL ||
|
||||
(policy == ScreenshotsPolicy.BLOCK_NSFW && manga != null && manga.isNsfw)
|
||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false)
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false)
|
||||
|
||||
val isBookmarkAdded: LiveData<Boolean> = currentState.flatMapLatest { state ->
|
||||
val isBookmarkAdded = currentState.flatMapLatest { state ->
|
||||
val manga = mangaData.value?.any
|
||||
if (state == null || manga == null) {
|
||||
flowOf(false)
|
||||
@@ -146,7 +142,7 @@ class ReaderViewModel @Inject constructor(
|
||||
bookmarksRepository.observeBookmark(manga, state.chapterId, state.page)
|
||||
.map { it != null }
|
||||
}
|
||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false)
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
||||
|
||||
init {
|
||||
loadImpl()
|
||||
@@ -173,10 +169,8 @@ class ReaderViewModel @Inject constructor(
|
||||
mode = newMode,
|
||||
)
|
||||
readerMode.value = newMode
|
||||
content.value?.run {
|
||||
content.value = copy(
|
||||
state = getCurrentState(),
|
||||
)
|
||||
content.update {
|
||||
it.copy(state = getCurrentState())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,9 +183,9 @@ class ReaderViewModel @Inject constructor(
|
||||
return
|
||||
}
|
||||
val readerState = state ?: currentState.value ?: return
|
||||
historyRepository.saveStateAsync(
|
||||
historyUpdateUseCase.invokeAsync(
|
||||
manga = mangaData.value?.any ?: return,
|
||||
state = readerState,
|
||||
readerState = readerState,
|
||||
percent = computePercent(readerState.chapterId, readerState.page),
|
||||
)
|
||||
}
|
||||
@@ -212,12 +206,12 @@ class ReaderViewModel @Inject constructor(
|
||||
prevJob?.cancelAndJoin()
|
||||
try {
|
||||
val dest = pageSaveHelper.savePage(pageLoader, page, saveLauncher)
|
||||
onPageSaved.emitCall(dest)
|
||||
onPageSaved.call(dest)
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
e.printStackTraceDebug()
|
||||
onPageSaved.emitCall(null)
|
||||
onPageSaved.call(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -233,7 +227,7 @@ class ReaderViewModel @Inject constructor(
|
||||
|
||||
fun getCurrentPage(): MangaPage? {
|
||||
val state = currentState.value ?: return null
|
||||
return content.value?.pages?.find {
|
||||
return content.value.pages.find {
|
||||
it.chapterId == state.chapterId && it.index == state.page
|
||||
}?.toMangaPage()
|
||||
}
|
||||
@@ -242,9 +236,9 @@ class ReaderViewModel @Inject constructor(
|
||||
val prevJob = loadingJob
|
||||
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
||||
prevJob?.cancelAndJoin()
|
||||
content.postValue(ReaderContent(emptyList(), null))
|
||||
content.value = ReaderContent(emptyList(), null)
|
||||
chaptersLoader.loadSingleChapter(id)
|
||||
content.postValue(ReaderContent(chaptersLoader.snapshot(), ReaderState(id, page, 0)))
|
||||
content.value = ReaderContent(chaptersLoader.snapshot(), ReaderState(id, page, 0))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,7 +248,7 @@ class ReaderViewModel @Inject constructor(
|
||||
stateChangeJob = launchJob(Dispatchers.Default) {
|
||||
prevJob?.cancelAndJoin()
|
||||
loadingJob?.join()
|
||||
val pages = content.value?.pages ?: return@launchJob
|
||||
val pages = content.value.pages
|
||||
pages.getOrNull(position)?.let { page ->
|
||||
currentState.update { cs ->
|
||||
cs?.copy(chapterId = page.chapterId, page = page.index)
|
||||
@@ -296,7 +290,7 @@ class ReaderViewModel @Inject constructor(
|
||||
percent = computePercent(state.chapterId, state.page),
|
||||
)
|
||||
bookmarksRepository.addBookmark(bookmark)
|
||||
onShowToast.emitCall(R.string.bookmark_added)
|
||||
onShowToast.call(R.string.bookmark_added)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,32 +312,31 @@ class ReaderViewModel @Inject constructor(
|
||||
var manga =
|
||||
DoubleManga(dataRepository.resolveIntent(intent) ?: throw NotFoundException("Cannot find manga", ""))
|
||||
mangaData.value = manga
|
||||
manga = mangaLoader.load(intent)
|
||||
manga = doubleMangaLoadUseCase(intent)
|
||||
chaptersLoader.init(manga)
|
||||
// determine mode
|
||||
val singleManga = manga.requireAny()
|
||||
val mode = detectReaderMode(singleManga)
|
||||
// obtain state
|
||||
if (currentState.value == null) {
|
||||
currentState.value = historyRepository.getOne(singleManga)?.let {
|
||||
ReaderState(it)
|
||||
} ?: ReaderState(singleManga, preselectedBranch)
|
||||
}
|
||||
|
||||
val mode = detectReaderModeUseCase.invoke(singleManga, currentState.value)
|
||||
val branch = chaptersLoader.peekChapter(currentState.value?.chapterId ?: 0L)?.branch
|
||||
mangaData.value = manga.filterChapters(branch)
|
||||
readerMode.emitValue(mode)
|
||||
readerMode.value = mode
|
||||
|
||||
chaptersLoader.loadSingleChapter(requireNotNull(currentState.value).chapterId)
|
||||
// save state
|
||||
if (!isIncognito) {
|
||||
currentState.value?.let {
|
||||
val percent = computePercent(it.chapterId, it.page)
|
||||
historyRepository.addOrUpdate(singleManga, it.chapterId, it.page, it.scroll, percent)
|
||||
historyUpdateUseCase.invoke(singleManga, it, percent)
|
||||
}
|
||||
}
|
||||
notifyStateChanged()
|
||||
content.emitValue(ReaderContent(chaptersLoader.snapshot(), currentState.value))
|
||||
content.value = ReaderContent(chaptersLoader.snapshot(), currentState.value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,7 +346,7 @@ class ReaderViewModel @Inject constructor(
|
||||
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
||||
prevJob?.join()
|
||||
chaptersLoader.loadPrevNextChapter(mangaData.requireValue(), currentId, isNext)
|
||||
content.emitValue(ReaderContent(chaptersLoader.snapshot(), null))
|
||||
content.value = ReaderContent(chaptersLoader.snapshot(), null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,27 +360,6 @@ class ReaderViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun detectReaderMode(manga: Manga): ReaderMode {
|
||||
dataRepository.getReaderMode(manga.id)?.let { return it }
|
||||
val defaultMode = settings.defaultReaderMode
|
||||
if (!settings.isReaderModeDetectionEnabled || defaultMode == ReaderMode.WEBTOON) {
|
||||
return defaultMode
|
||||
}
|
||||
val chapter = currentState.value?.chapterId?.let { chaptersLoader.peekChapter(it) }
|
||||
?: manga.chapters?.randomOrNull()
|
||||
?: error("There are no chapters in this manga")
|
||||
val repo = mangaRepositoryFactory.create(manga.source)
|
||||
val pages = repo.getPages(chapter)
|
||||
return runCatchingCancellable {
|
||||
val isWebtoon = dataRepository.determineMangaIsWebtoon(repo, pages)
|
||||
if (isWebtoon) ReaderMode.WEBTOON else defaultMode
|
||||
}.onSuccess {
|
||||
dataRepository.saveReaderMode(manga, it)
|
||||
}.onFailure {
|
||||
it.printStackTraceDebug()
|
||||
}.getOrDefault(defaultMode)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun notifyStateChanged() {
|
||||
val state = getCurrentState()
|
||||
@@ -402,7 +374,7 @@ class ReaderViewModel @Inject constructor(
|
||||
isSliderEnabled = settings.isReaderSliderEnabled,
|
||||
percent = if (state != null) computePercent(state.chapterId, state.page) else PROGRESS_NONE,
|
||||
)
|
||||
uiState.postValue(newState)
|
||||
uiState.value = newState
|
||||
}
|
||||
|
||||
private fun computePercent(chapterId: Long, pageIndex: Int): Float {
|
||||
@@ -419,23 +391,3 @@ class ReaderViewModel @Inject constructor(
|
||||
return ppc * chapterIndex + ppc * pagePercent
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is not a member of the ReaderViewModel
|
||||
* because it should work independently of the ViewModel's lifecycle.
|
||||
*/
|
||||
private fun HistoryRepository.saveStateAsync(manga: Manga, state: ReaderState, percent: Float): Job {
|
||||
return processLifecycleScope.launch(Dispatchers.Default) {
|
||||
runCatchingCancellable {
|
||||
addOrUpdate(
|
||||
manga = manga,
|
||||
chapterId = state.chapterId,
|
||||
page = state.page,
|
||||
scroll = state.scroll,
|
||||
percent = percent,
|
||||
)
|
||||
}.onFailure {
|
||||
it.printStackTraceDebug()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.util.ext.decodeRegion
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.core.util.ext.indicator
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.setValueRounded
|
||||
import org.koitharu.kotatsu.databinding.ActivityColorFilterBinding
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
@@ -64,7 +66,7 @@ class ColorFilterConfigActivity :
|
||||
viewModel.colorFilter.observe(this, this::onColorFilterChanged)
|
||||
viewModel.isLoading.observe(this, this::onLoadingChanged)
|
||||
viewModel.preview.observe(this, this::onPreviewChanged)
|
||||
viewModel.onDismiss.observe(this) {
|
||||
viewModel.onDismiss.observeEvent(this) {
|
||||
finishAfterTransition()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.content.DialogInterface
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
|
||||
class ColorFilterConfigBackPressedDispatcher(
|
||||
private val context: Context,
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
package org.koitharu.kotatsu.reader.ui.colorfilter
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaPages
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.util.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.emitValue
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
|
||||
import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity.Companion.EXTRA_MANGA
|
||||
@@ -26,9 +26,9 @@ class ColorFilterConfigViewModel @Inject constructor(
|
||||
private val manga = checkNotNull(savedStateHandle.get<ParcelableManga>(EXTRA_MANGA)?.manga)
|
||||
|
||||
private var initialColorFilter: ReaderColorFilter? = null
|
||||
val colorFilter = MutableLiveData<ReaderColorFilter?>(null)
|
||||
val onDismiss = SingleLiveEvent<Unit>()
|
||||
val preview = MutableLiveData<MangaPage?>(null)
|
||||
val colorFilter = MutableStateFlow<ReaderColorFilter?>(null)
|
||||
val onDismiss = MutableEventFlow<Unit>()
|
||||
val preview = MutableStateFlow<MangaPage?>(null)
|
||||
|
||||
val isChanged: Boolean
|
||||
get() = colorFilter.value != initialColorFilter
|
||||
@@ -44,13 +44,11 @@ class ColorFilterConfigViewModel @Inject constructor(
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
val repository = mangaRepositoryFactory.create(page.source)
|
||||
val url = repository.getPageUrl(page)
|
||||
preview.emitValue(
|
||||
MangaPage(
|
||||
id = page.id,
|
||||
url = url,
|
||||
preview = page.preview,
|
||||
source = page.source,
|
||||
),
|
||||
preview.value = MangaPage(
|
||||
id = page.id,
|
||||
url = url,
|
||||
preview = page.preview,
|
||||
source = page.source,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -72,7 +70,7 @@ class ColorFilterConfigViewModel @Inject constructor(
|
||||
fun save() {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
mangaDataRepository.saveColorFilter(manga, colorFilter.value)
|
||||
onDismiss.emitCall(Unit)
|
||||
onDismiss.call(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,12 +18,14 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsLiveData
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
|
||||
import org.koitharu.kotatsu.core.ui.BaseBottomSheet
|
||||
import org.koitharu.kotatsu.core.util.ScreenOrientationHelper
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
|
||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||
import org.koitharu.kotatsu.databinding.SheetReaderConfigBinding
|
||||
@@ -75,8 +77,8 @@ class ReaderConfigBottomSheet :
|
||||
binding.sliderTimer.addOnChangeListener(this)
|
||||
binding.switchScrollTimer.setOnCheckedChangeListener(this)
|
||||
|
||||
settings.observeAsLiveData(
|
||||
context = lifecycleScope.coroutineContext + Dispatchers.Default,
|
||||
settings.observeAsStateFlow(
|
||||
scope = lifecycleScope + Dispatchers.Default,
|
||||
key = AppSettings.KEY_READER_AUTOSCROLL_SPEED,
|
||||
valueProducer = { readerAutoscrollSpeed },
|
||||
).observe(viewLifecycleOwner) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.fragment.app.activityViewModels
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import org.koitharu.kotatsu.core.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.core.util.ext.getParcelableCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderViewModel
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.core.util.ext.doOnPageChanged
|
||||
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.recyclerView
|
||||
import org.koitharu.kotatsu.core.util.ext.resetTransformations
|
||||
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
|
||||
|
||||
@@ -10,6 +10,7 @@ import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.core.util.ext.doOnPageChanged
|
||||
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.recyclerView
|
||||
import org.koitharu.kotatsu.core.util.ext.resetTransformations
|
||||
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.core.util.ext.findCenterViewPosition
|
||||
import org.koitharu.kotatsu.core.util.ext.firstVisibleItemPosition
|
||||
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
|
||||
import org.koitharu.kotatsu.databinding.FragmentReaderWebtoonBinding
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
|
||||
@@ -20,6 +20,8 @@ import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.list.ScrollListenerInvalidationObserver
|
||||
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
|
||||
import org.koitharu.kotatsu.core.ui.widgets.BottomSheetHeaderBar
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
|
||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||
import org.koitharu.kotatsu.databinding.SheetPagesBinding
|
||||
@@ -93,7 +95,7 @@ class PagesThumbnailsSheet :
|
||||
viewModel.branch.observe(viewLifecycleOwner) {
|
||||
onExpansionStateChanged(binding.headerBar, binding.headerBar.isExpanded)
|
||||
}
|
||||
viewModel.onError.observe(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
|
||||
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
package org.koitharu.kotatsu.reader.ui.thumbnails
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.util.ext.emitValue
|
||||
import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||
import org.koitharu.kotatsu.local.domain.DoubleMangaLoader
|
||||
import org.koitharu.kotatsu.parsers.util.SuspendLazy
|
||||
import org.koitharu.kotatsu.reader.domain.ChaptersLoader
|
||||
import javax.inject.Inject
|
||||
@@ -22,7 +21,7 @@ class PagesThumbnailsViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
mangaRepositoryFactory: MangaRepository.Factory,
|
||||
private val chaptersLoader: ChaptersLoader,
|
||||
private val mangaLoader: DoubleMangaLoader,
|
||||
private val doubleMangaLoadUseCase: DoubleMangaLoadUseCase,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val currentPageIndex: Int = savedStateHandle[PagesThumbnailsSheet.ARG_CURRENT_PAGE] ?: -1
|
||||
@@ -31,9 +30,9 @@ class PagesThumbnailsViewModel @Inject constructor(
|
||||
|
||||
private val repository = mangaRepositoryFactory.create(manga.source)
|
||||
private val mangaDetails = SuspendLazy {
|
||||
mangaLoader.load(manga).let {
|
||||
doubleMangaLoadUseCase(manga).let {
|
||||
val b = manga.chapters?.find { ch -> ch.id == initialChapterId }?.branch
|
||||
branch.emitValue(b)
|
||||
branch.value = b
|
||||
it.filterChapters(b)
|
||||
}
|
||||
}
|
||||
@@ -41,8 +40,8 @@ class PagesThumbnailsViewModel @Inject constructor(
|
||||
private var loadingPrevJob: Job? = null
|
||||
private var loadingNextJob: Job? = null
|
||||
|
||||
val thumbnails = MutableLiveData<List<ListModel>>()
|
||||
val branch = MutableLiveData<String?>()
|
||||
val thumbnails = MutableStateFlow<List<ListModel>>(emptyList())
|
||||
val branch = MutableStateFlow<String?>(null)
|
||||
val title = manga.title
|
||||
|
||||
init {
|
||||
@@ -100,6 +99,6 @@ class PagesThumbnailsViewModel @Inject constructor(
|
||||
add(LoadingFooter(1))
|
||||
}
|
||||
}
|
||||
thumbnails.emitValue(pages)
|
||||
thumbnails.value = pages
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user