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