Refactor details and reader ViewModels
parent
eb49b31aeb
commit
0cc019ef19
@ -0,0 +1,233 @@
|
||||
package org.koitharu.kotatsu.details.ui.pager
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.plus
|
||||
import okio.FileNotFoundException
|
||||
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.core.util.ext.combine
|
||||
import org.koitharu.kotatsu.core.util.ext.requireValue
|
||||
import org.koitharu.kotatsu.details.data.MangaDetails
|
||||
import org.koitharu.kotatsu.details.domain.DetailsInteractor
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.details.ui.DetailsViewModel
|
||||
import org.koitharu.kotatsu.details.ui.mapChapters
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase
|
||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderViewModel
|
||||
|
||||
abstract class ChaptersPagesViewModel(
|
||||
@JvmField protected val settings: AppSettings,
|
||||
private val interactor: DetailsInteractor,
|
||||
private val bookmarksRepository: BookmarksRepository,
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val downloadScheduler: DownloadWorker.Scheduler,
|
||||
private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
|
||||
private val localStorageChanges: SharedFlow<LocalManga?>,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val mangaDetails = MutableStateFlow<MangaDetails?>(null)
|
||||
val readingState = MutableStateFlow<ReaderState?>(null)
|
||||
|
||||
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||
val onSelectChapter = MutableEventFlow<Long>()
|
||||
val onDownloadStarted = MutableEventFlow<Unit>()
|
||||
val onMangaRemoved = MutableEventFlow<Manga>()
|
||||
|
||||
private val chaptersQuery = MutableStateFlow("")
|
||||
val selectedBranch = MutableStateFlow<String?>(null)
|
||||
|
||||
val manga = mangaDetails.map { x -> x?.toManga() }
|
||||
.withErrorHandling()
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
||||
|
||||
val isChaptersReversed = settings.observeAsStateFlow(
|
||||
scope = viewModelScope + Dispatchers.Default,
|
||||
key = AppSettings.KEY_REVERSE_CHAPTERS,
|
||||
valueProducer = { isChaptersReverse },
|
||||
)
|
||||
|
||||
val isChaptersInGridView = settings.observeAsStateFlow(
|
||||
scope = viewModelScope + Dispatchers.Default,
|
||||
key = AppSettings.KEY_GRID_VIEW_CHAPTERS,
|
||||
valueProducer = { isChaptersGridView },
|
||||
)
|
||||
|
||||
val newChaptersCount = mangaDetails.flatMapLatest { d ->
|
||||
if (d?.isLocal == false) {
|
||||
interactor.observeNewChapters(d.id)
|
||||
} else {
|
||||
flowOf(0)
|
||||
}
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
|
||||
|
||||
val isChaptersEmpty: StateFlow<Boolean> = mangaDetails.map {
|
||||
it != null && it.isLoaded && it.allChapters.isEmpty()
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
|
||||
|
||||
val bookmarks = mangaDetails.flatMapLatest {
|
||||
if (it != null) bookmarksRepository.observeBookmarks(it.toManga()) else flowOf(emptyList())
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, emptyList())
|
||||
|
||||
val chapters = combine(
|
||||
combine(
|
||||
mangaDetails,
|
||||
readingState.map { it?.chapterId ?: 0L }.distinctUntilChanged(),
|
||||
selectedBranch,
|
||||
newChaptersCount,
|
||||
bookmarks,
|
||||
isChaptersInGridView,
|
||||
) { manga, currentChapterId, branch, news, bookmarks, grid ->
|
||||
manga?.mapChapters(
|
||||
currentChapterId,
|
||||
news,
|
||||
branch,
|
||||
bookmarks,
|
||||
grid,
|
||||
).orEmpty()
|
||||
},
|
||||
isChaptersReversed,
|
||||
chaptersQuery,
|
||||
) { list, reversed, query ->
|
||||
(if (reversed) list.asReversed() else list).filterSearch(query)
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
init {
|
||||
launchJob(Dispatchers.Default) {
|
||||
localStorageChanges
|
||||
.collect { onDownloadComplete(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun setChaptersReversed(newValue: Boolean) {
|
||||
settings.isChaptersReverse = newValue
|
||||
}
|
||||
|
||||
fun setChaptersInGridView(newValue: Boolean) {
|
||||
settings.isChaptersGridView = newValue
|
||||
}
|
||||
|
||||
fun setSelectedBranch(branch: String?) {
|
||||
selectedBranch.value = branch
|
||||
}
|
||||
|
||||
fun performChapterSearch(query: String?) {
|
||||
chaptersQuery.value = query?.trim().orEmpty()
|
||||
}
|
||||
|
||||
fun getMangaOrNull(): Manga? = mangaDetails.value?.toManga()
|
||||
|
||||
fun requireManga() = mangaDetails.requireValue().toManga()
|
||||
|
||||
fun markChapterAsCurrent(chapterId: Long) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
val manga = mangaDetails.requireValue()
|
||||
val chapters = checkNotNull(manga.chapters[selectedBranch.value])
|
||||
val chapterIndex = chapters.indexOfFirst { it.id == chapterId }
|
||||
check(chapterIndex in chapters.indices) { "Chapter not found" }
|
||||
val percent = chapterIndex / chapters.size.toFloat()
|
||||
historyRepository.addOrUpdate(
|
||||
manga = manga.toManga(),
|
||||
chapterId = chapterId,
|
||||
page = 0,
|
||||
scroll = 0,
|
||||
percent = percent,
|
||||
force = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun download(chaptersIds: Set<Long>?) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
downloadScheduler.schedule(
|
||||
requireManga(),
|
||||
chaptersIds,
|
||||
)
|
||||
onDownloadStarted.call(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteLocal() {
|
||||
val m = mangaDetails.value?.local?.manga
|
||||
if (m == null) {
|
||||
errorEvent.call(FileNotFoundException())
|
||||
return
|
||||
}
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
deleteLocalMangaUseCase(m)
|
||||
onMangaRemoved.call(m)
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<ChapterListItem>.filterSearch(query: String): List<ChapterListItem> {
|
||||
if (query.isEmpty() || this.isEmpty()) {
|
||||
return this
|
||||
}
|
||||
return filter {
|
||||
it.chapter.name.contains(query, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onDownloadComplete(downloadedManga: LocalManga?) {
|
||||
downloadedManga ?: return
|
||||
mangaDetails.update {
|
||||
interactor.updateLocal(it, downloadedManga)
|
||||
}
|
||||
}
|
||||
|
||||
class ActivityVMLazy(
|
||||
private val fragment: Fragment,
|
||||
) : Lazy<ChaptersPagesViewModel> {
|
||||
private var cached: ChaptersPagesViewModel? = null
|
||||
|
||||
override val value: ChaptersPagesViewModel
|
||||
get() {
|
||||
val viewModel = cached
|
||||
return if (viewModel == null) {
|
||||
val activity = fragment.requireActivity()
|
||||
val vmClass = getViewModelClass(activity)
|
||||
ViewModelProvider.create(
|
||||
store = activity.viewModelStore,
|
||||
factory = activity.defaultViewModelProviderFactory,
|
||||
extras = activity.defaultViewModelCreationExtras,
|
||||
)[vmClass].also { cached = it }
|
||||
} else {
|
||||
viewModel
|
||||
}
|
||||
}
|
||||
|
||||
override fun isInitialized(): Boolean = cached != null
|
||||
|
||||
private fun getViewModelClass(activity: Activity) = when (activity) {
|
||||
is ReaderActivity -> ReaderViewModel::class.java
|
||||
is DetailsActivity -> DetailsViewModel::class.java
|
||||
else -> error("Wrong activity ${activity.javaClass.simpleName} for ${ChaptersPagesViewModel::class.java.simpleName}")
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue