Refactor details and reader ViewModels
This commit is contained in:
@@ -17,7 +17,7 @@ android {
|
||||
minSdk = 21
|
||||
targetSdk = 35
|
||||
versionCode = 665
|
||||
versionName = '7.5-b1'
|
||||
versionName = '7.5-b2'
|
||||
generatedDensities = []
|
||||
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
||||
ksp {
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderViewModel
|
||||
|
||||
class KotatsuApp : BaseApp() {
|
||||
|
||||
@@ -30,6 +31,7 @@ class KotatsuApp : BaseApp() {
|
||||
.setClassInstanceLimit(PagesCache::class.java, 1)
|
||||
.setClassInstanceLimit(MangaLoaderContext::class.java, 1)
|
||||
.setClassInstanceLimit(PageLoader::class.java, 1)
|
||||
.setClassInstanceLimit(ReaderViewModel::class.java, 1)
|
||||
.penaltyLog()
|
||||
.build(),
|
||||
)
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.details.ui
|
||||
import android.content.Context
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
import org.koitharu.kotatsu.details.data.MangaDetails
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.details.ui.model.toListItem
|
||||
@@ -12,7 +11,7 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
|
||||
fun MangaDetails.mapChapters(
|
||||
history: MangaHistory?,
|
||||
currentChapterId: Long,
|
||||
newCount: Int,
|
||||
branch: String?,
|
||||
bookmarks: List<Bookmark>,
|
||||
@@ -24,7 +23,6 @@ fun MangaDetails.mapChapters(
|
||||
return emptyList()
|
||||
}
|
||||
val bookmarked = bookmarks.mapToSet { it.chapterId }
|
||||
val currentId = history?.chapterId ?: 0L
|
||||
val newFrom = if (newCount == 0 || remoteChapters.isEmpty()) Int.MAX_VALUE else remoteChapters.size - newCount
|
||||
val ids = buildSet(maxOf(remoteChapters.size, localChapters.size)) {
|
||||
remoteChapters.mapTo(this) { it.id }
|
||||
@@ -36,14 +34,14 @@ fun MangaDetails.mapChapters(
|
||||
} else {
|
||||
null
|
||||
}
|
||||
var isUnread = currentId !in ids
|
||||
var isUnread = currentChapterId !in ids
|
||||
for (chapter in remoteChapters) {
|
||||
val local = localMap?.remove(chapter.id)
|
||||
if (chapter.id == currentId) {
|
||||
if (chapter.id == currentChapterId) {
|
||||
isUnread = true
|
||||
}
|
||||
result += (local ?: chapter).toListItem(
|
||||
isCurrent = chapter.id == currentId,
|
||||
isCurrent = chapter.id == currentChapterId,
|
||||
isUnread = isUnread,
|
||||
isNew = isUnread && result.size >= newFrom,
|
||||
isDownloaded = local != null,
|
||||
@@ -53,11 +51,11 @@ fun MangaDetails.mapChapters(
|
||||
}
|
||||
if (!localMap.isNullOrEmpty()) {
|
||||
for (chapter in localMap.values) {
|
||||
if (chapter.id == currentId) {
|
||||
if (chapter.id == currentChapterId) {
|
||||
isUnread = true
|
||||
}
|
||||
result += chapter.toListItem(
|
||||
isCurrent = chapter.id == currentId,
|
||||
isCurrent = chapter.id == currentChapterId,
|
||||
isUnread = isUnread,
|
||||
isNew = false,
|
||||
isDownloaded = !isLocal,
|
||||
|
||||
@@ -72,7 +72,6 @@ import org.koitharu.kotatsu.core.util.ext.joinToStringWithLimit
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.parentView
|
||||
import org.koitharu.kotatsu.core.util.ext.resolveDp
|
||||
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
|
||||
import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat
|
||||
@@ -162,7 +161,7 @@ class DetailsActivity :
|
||||
}
|
||||
TitleExpandListener(viewBinding.textViewTitle).attach()
|
||||
|
||||
viewModel.details.filterNotNull().observe(this, ::onMangaUpdated)
|
||||
viewModel.mangaDetails.filterNotNull().observe(this, ::onMangaUpdated)
|
||||
viewModel.onMangaRemoved.observeEvent(this, ::onMangaRemoved)
|
||||
viewModel.onError
|
||||
.filterNot { ChaptersPagesSheet.isShown(supportFragmentManager) }
|
||||
|
||||
@@ -12,32 +12,23 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.plus
|
||||
import okio.FileNotFoundException
|
||||
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.findById
|
||||
import org.koitharu.kotatsu.core.model.getPreferredBranch
|
||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
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.computeSize
|
||||
import org.koitharu.kotatsu.core.util.ext.onEachWhile
|
||||
import org.koitharu.kotatsu.core.util.ext.requireValue
|
||||
import org.koitharu.kotatsu.details.data.MangaDetails
|
||||
import org.koitharu.kotatsu.details.domain.BranchComparator
|
||||
import org.koitharu.kotatsu.details.domain.DetailsInteractor
|
||||
@@ -45,9 +36,9 @@ import org.koitharu.kotatsu.details.domain.DetailsLoadUseCase
|
||||
import org.koitharu.kotatsu.details.domain.ProgressUpdateUseCase
|
||||
import org.koitharu.kotatsu.details.domain.ReadingTimeUseCase
|
||||
import org.koitharu.kotatsu.details.domain.RelatedMangaUseCase
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
|
||||
import org.koitharu.kotatsu.details.ui.model.MangaBranch
|
||||
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesViewModel
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import org.koitharu.kotatsu.list.domain.MangaListMapper
|
||||
@@ -57,6 +48,7 @@ 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.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
|
||||
@@ -66,37 +58,42 @@ import javax.inject.Inject
|
||||
@HiltViewModel
|
||||
class DetailsViewModel @Inject constructor(
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val bookmarksRepository: BookmarksRepository,
|
||||
private val settings: AppSettings,
|
||||
bookmarksRepository: BookmarksRepository,
|
||||
settings: AppSettings,
|
||||
private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
|
||||
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
|
||||
private val downloadScheduler: DownloadWorker.Scheduler,
|
||||
@LocalStorageChanges localStorageChanges: SharedFlow<LocalManga?>,
|
||||
downloadScheduler: DownloadWorker.Scheduler,
|
||||
private val interactor: DetailsInteractor,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
|
||||
deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
|
||||
private val relatedMangaUseCase: RelatedMangaUseCase,
|
||||
private val mangaListMapper: MangaListMapper,
|
||||
private val detailsLoadUseCase: DetailsLoadUseCase,
|
||||
private val progressUpdateUseCase: ProgressUpdateUseCase,
|
||||
private val readingTimeUseCase: ReadingTimeUseCase,
|
||||
private val statsRepository: StatsRepository,
|
||||
) : BaseViewModel() {
|
||||
statsRepository: StatsRepository,
|
||||
) : ChaptersPagesViewModel(
|
||||
settings = settings,
|
||||
interactor = interactor,
|
||||
bookmarksRepository = bookmarksRepository,
|
||||
historyRepository = historyRepository,
|
||||
downloadScheduler = downloadScheduler,
|
||||
deleteLocalMangaUseCase = deleteLocalMangaUseCase,
|
||||
localStorageChanges = localStorageChanges,
|
||||
) {
|
||||
|
||||
private val intent = MangaIntent(savedStateHandle)
|
||||
private var loadingJob: Job
|
||||
val mangaId = intent.mangaId
|
||||
|
||||
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||
val onSelectChapter = MutableEventFlow<Long>()
|
||||
val onDownloadStarted = MutableEventFlow<Unit>()
|
||||
|
||||
val details = MutableStateFlow(intent.manga?.let { MangaDetails(it, null, null, false) })
|
||||
val manga = details.map { x -> x?.toManga() }
|
||||
.withErrorHandling()
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
||||
init {
|
||||
mangaDetails.value = intent.manga?.let { MangaDetails(it, null, null, false) }
|
||||
}
|
||||
|
||||
val history = historyRepository.observeOne(mangaId)
|
||||
.withErrorHandling()
|
||||
.onEach { h ->
|
||||
readingState.value = h?.let(::ReaderState)
|
||||
}.withErrorHandling()
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
||||
|
||||
val favouriteCategories = interactor.observeFavourite(mangaId)
|
||||
@@ -109,31 +106,8 @@ class DetailsViewModel @Inject constructor(
|
||||
|
||||
val remoteManga = MutableStateFlow<Manga?>(null)
|
||||
|
||||
val newChaptersCount = details.flatMapLatest { d ->
|
||||
if (d?.isLocal == false) {
|
||||
interactor.observeNewChapters(mangaId)
|
||||
} else {
|
||||
flowOf(0)
|
||||
}
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
|
||||
|
||||
private val chaptersQuery = MutableStateFlow("")
|
||||
val selectedBranch = MutableStateFlow<String?>(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 historyInfo: StateFlow<HistoryInfo> = combine(
|
||||
details,
|
||||
mangaDetails,
|
||||
selectedBranch,
|
||||
history,
|
||||
interactor.observeIncognitoMode(manga),
|
||||
@@ -145,11 +119,7 @@ class DetailsViewModel @Inject constructor(
|
||||
initialValue = HistoryInfo(null, null, null, false),
|
||||
)
|
||||
|
||||
val bookmarks = manga.flatMapLatest {
|
||||
if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList())
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, emptyList())
|
||||
|
||||
val localSize = details
|
||||
val localSize = mangaDetails
|
||||
.map { it?.local }
|
||||
.distinctUntilChanged()
|
||||
.combine(localStorageChanges.onStart { emit(null) }) { x, _ -> x }
|
||||
@@ -163,7 +133,6 @@ class DetailsViewModel @Inject constructor(
|
||||
}
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(5000), 0L)
|
||||
|
||||
val onMangaRemoved = MutableEventFlow<Manga>()
|
||||
val isScrobblingAvailable: Boolean
|
||||
get() = scrobblers.any { it.isEnabled }
|
||||
|
||||
@@ -182,7 +151,7 @@ class DetailsViewModel @Inject constructor(
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, emptyList())
|
||||
|
||||
val branches: StateFlow<List<MangaBranch>> = combine(
|
||||
details,
|
||||
mangaDetails,
|
||||
selectedBranch,
|
||||
history,
|
||||
) { m, b, h ->
|
||||
@@ -201,35 +170,8 @@ class DetailsViewModel @Inject constructor(
|
||||
}.sortedWith(BranchComparator())
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
val isChaptersEmpty: StateFlow<Boolean> = details.map {
|
||||
it != null && it.isLoaded && it.allChapters.isEmpty()
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
|
||||
|
||||
val chapters = combine(
|
||||
combine(
|
||||
details,
|
||||
history,
|
||||
selectedBranch,
|
||||
newChaptersCount,
|
||||
bookmarks,
|
||||
isChaptersInGridView,
|
||||
) { manga, history, branch, news, bookmarks, grid ->
|
||||
manga?.mapChapters(
|
||||
history,
|
||||
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())
|
||||
|
||||
val readingTime = combine(
|
||||
details,
|
||||
mangaDetails,
|
||||
selectedBranch,
|
||||
history,
|
||||
) { m, b, h ->
|
||||
@@ -242,18 +184,14 @@ class DetailsViewModel @Inject constructor(
|
||||
init {
|
||||
loadingJob = doLoad()
|
||||
launchJob(Dispatchers.Default) {
|
||||
localStorageChanges
|
||||
.collect { onDownloadComplete(it) }
|
||||
}
|
||||
launchJob(Dispatchers.Default) {
|
||||
val manga = details.firstOrNull { !it?.chapters.isNullOrEmpty() } ?: return@launchJob
|
||||
val manga = mangaDetails.firstOrNull { !it?.chapters.isNullOrEmpty() } ?: return@launchJob
|
||||
val h = history.firstOrNull()
|
||||
if (h != null) {
|
||||
progressUpdateUseCase(manga.toManga())
|
||||
}
|
||||
}
|
||||
launchJob(Dispatchers.Default) {
|
||||
val manga = details.firstOrNull { it != null && it.isLocal } ?: return@launchJob
|
||||
val manga = mangaDetails.firstOrNull { it != null && it.isLocal } ?: return@launchJob
|
||||
remoteManga.value = interactor.findRemote(manga.toManga())
|
||||
}
|
||||
}
|
||||
@@ -263,41 +201,6 @@ class DetailsViewModel @Inject constructor(
|
||||
loadingJob = doLoad()
|
||||
}
|
||||
|
||||
fun deleteLocal() {
|
||||
val m = details.value?.local?.manga
|
||||
if (m == null) {
|
||||
errorEvent.call(FileNotFoundException())
|
||||
return
|
||||
}
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
deleteLocalMangaUseCase(m)
|
||||
onMangaRemoved.call(m)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeBookmark(bookmark: Bookmark) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
bookmarksRepository.removeBookmark(bookmark)
|
||||
onActionDone.call(ReversibleAction(R.string.bookmark_removed, null))
|
||||
}
|
||||
}
|
||||
|
||||
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 updateScrobbling(index: Int, rating: Float, status: ScrobblingStatus?) {
|
||||
val scrobbler = getScrobbler(index) ?: return
|
||||
launchJob(Dispatchers.Default) {
|
||||
@@ -319,34 +222,6 @@ class DetailsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun markChapterAsCurrent(chapterId: Long) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
val manga = checkNotNull(details.value)
|
||||
val chapters = checkNotNull(manga.chapters[selectedBranchValue])
|
||||
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(
|
||||
details.requireValue().toManga(),
|
||||
chaptersIds,
|
||||
)
|
||||
onDownloadStarted.call(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
fun startChaptersSelection() {
|
||||
val chapters = chapters.value
|
||||
val chapter = chapters.find {
|
||||
@@ -374,28 +249,10 @@ class DetailsViewModel @Inject constructor(
|
||||
selectedBranch.value = manga.getPreferredBranch(hist)
|
||||
true
|
||||
}.collect {
|
||||
details.value = it
|
||||
mangaDetails.value = it
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
launchJob {
|
||||
details.update {
|
||||
interactor.updateLocal(it, downloadedManga)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getScrobbler(index: Int): Scrobbler? {
|
||||
val info = scrobblingInfo.value.getOrNull(index)
|
||||
val scrobbler = if (info != null) {
|
||||
|
||||
@@ -14,14 +14,13 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
||||
import org.koitharu.kotatsu.core.util.ext.setValueRounded
|
||||
import org.koitharu.kotatsu.core.util.progress.IntPercentLabelFormatter
|
||||
import org.koitharu.kotatsu.details.ui.DetailsViewModel
|
||||
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet.Companion.TAB_BOOKMARKS
|
||||
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet.Companion.TAB_CHAPTERS
|
||||
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet.Companion.TAB_PAGES
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
class ChapterPagesMenuProvider(
|
||||
private val viewModel: DetailsViewModel,
|
||||
private val viewModel: ChaptersPagesViewModel,
|
||||
private val sheet: BaseAdaptiveSheet<*>,
|
||||
private val pager: ViewPager2,
|
||||
private val settings: AppSettings,
|
||||
|
||||
@@ -7,7 +7,6 @@ import android.view.ViewGroup
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||
@@ -30,7 +29,6 @@ import org.koitharu.kotatsu.core.util.ext.setTabsEnabled
|
||||
import org.koitharu.kotatsu.core.util.ext.showDistinct
|
||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||
import org.koitharu.kotatsu.databinding.SheetChaptersPagesBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsViewModel
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -40,7 +38,7 @@ class ChaptersPagesSheet : BaseAdaptiveSheet<SheetChaptersPagesBinding>(), Actio
|
||||
@Inject
|
||||
lateinit var settings: AppSettings
|
||||
|
||||
private val viewModel by activityViewModels<DetailsViewModel>()
|
||||
private val viewModel by ChaptersPagesViewModel.ActivityVMLazy(this)
|
||||
|
||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetChaptersPagesBinding {
|
||||
return SheetChaptersPagesBinding.inflate(inflater, container, false)
|
||||
|
||||
@@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import coil.ImageLoader
|
||||
@@ -30,7 +29,7 @@ import org.koitharu.kotatsu.core.util.ext.findParentCallback
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.databinding.FragmentMangaBookmarksBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsViewModel
|
||||
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesViewModel
|
||||
import org.koitharu.kotatsu.list.ui.GridSpanResolver
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
|
||||
@@ -42,7 +41,7 @@ import javax.inject.Inject
|
||||
class BookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(),
|
||||
OnListItemClickListener<Bookmark>, ListSelectionController.Callback {
|
||||
|
||||
private val activityViewModel by activityViewModels<DetailsViewModel>()
|
||||
private val activityViewModel by ChaptersPagesViewModel.ActivityVMLazy(this)
|
||||
private val viewModel by viewModels<BookmarksViewModel>()
|
||||
|
||||
@Inject
|
||||
@@ -62,7 +61,7 @@ class BookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(),
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
activityViewModel.manga.observe(this, viewModel)
|
||||
activityViewModel.mangaDetails.observe(this, viewModel)
|
||||
}
|
||||
|
||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentMangaBookmarksBinding {
|
||||
@@ -125,7 +124,7 @@ class BookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(),
|
||||
dismissParentDialog()
|
||||
} else {
|
||||
val intent = IntentBuilder(view.context)
|
||||
.manga(activityViewModel.manga.value ?: return)
|
||||
.manga(activityViewModel.getMangaOrNull() ?: return)
|
||||
.bookmark(item)
|
||||
.incognito(true)
|
||||
.build()
|
||||
|
||||
@@ -21,6 +21,7 @@ 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.details.data.MangaDetails
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
@@ -32,7 +33,7 @@ import javax.inject.Inject
|
||||
class BookmarksViewModel @Inject constructor(
|
||||
private val bookmarksRepository: BookmarksRepository,
|
||||
settings: AppSettings,
|
||||
) : BaseViewModel(), FlowCollector<Manga?> {
|
||||
) : BaseViewModel(), FlowCollector<MangaDetails?> {
|
||||
|
||||
private val manga = MutableStateFlow<Manga?>(null)
|
||||
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||
@@ -50,8 +51,8 @@ class BookmarksViewModel @Inject constructor(
|
||||
.filterNotNull()
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, listOf(LoadingState))
|
||||
|
||||
override suspend fun emit(value: Manga?) {
|
||||
manga.value = value
|
||||
override suspend fun emit(value: MangaDetails?) {
|
||||
manga.value = value?.toManga()
|
||||
}
|
||||
|
||||
fun removeBookmarks(ids: Set<Long>) {
|
||||
|
||||
@@ -7,10 +7,10 @@ import android.view.ViewGroup
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.ancestors
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
@@ -28,10 +28,10 @@ import org.koitharu.kotatsu.core.util.ext.findParentCallback
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.databinding.FragmentChaptersBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsViewModel
|
||||
import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter
|
||||
import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesViewModel
|
||||
import org.koitharu.kotatsu.details.ui.withVolumeHeaders
|
||||
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
@@ -40,11 +40,12 @@ import org.koitharu.kotatsu.reader.ui.ReaderNavigationCallback
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ChaptersFragment :
|
||||
BaseFragment<FragmentChaptersBinding>(),
|
||||
OnListItemClickListener<ChapterListItem> {
|
||||
|
||||
private val viewModel by activityViewModels<DetailsViewModel>()
|
||||
private val viewModel by ChaptersPagesViewModel.ActivityVMLazy(this)
|
||||
|
||||
private var chaptersAdapter: ChaptersAdapter? = null
|
||||
private var selectionController: ListSelectionController? = null
|
||||
@@ -107,7 +108,7 @@ class ChaptersFragment :
|
||||
} else {
|
||||
startActivity(
|
||||
IntentBuilder(view.context)
|
||||
.manga(viewModel.manga.value ?: return)
|
||||
.manga(viewModel.getMangaOrNull() ?: return)
|
||||
.state(ReaderState(item.chapter.id, 0, 0))
|
||||
.build(),
|
||||
)
|
||||
|
||||
@@ -11,11 +11,11 @@ import org.koitharu.kotatsu.core.ui.list.BaseListSelectionCallback
|
||||
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
||||
import org.koitharu.kotatsu.core.util.ext.toCollection
|
||||
import org.koitharu.kotatsu.core.util.ext.toSet
|
||||
import org.koitharu.kotatsu.details.ui.DetailsViewModel
|
||||
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesViewModel
|
||||
import org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService
|
||||
|
||||
class ChaptersSelectionCallback(
|
||||
private val viewModel: DetailsViewModel,
|
||||
private val viewModel: ChaptersPagesViewModel,
|
||||
recyclerView: RecyclerView,
|
||||
) : BaseListSelectionCallback(recyclerView) {
|
||||
|
||||
@@ -60,7 +60,7 @@ class ChaptersSelectionCallback(
|
||||
|
||||
R.id.action_delete -> {
|
||||
val ids = controller.peekCheckedIds()
|
||||
val manga = viewModel.manga.value
|
||||
val manga = viewModel.getMangaOrNull()
|
||||
when {
|
||||
ids.isEmpty() || manga == null -> Unit
|
||||
ids.size == manga.chapters?.size -> viewModel.deleteLocal()
|
||||
|
||||
@@ -7,7 +7,6 @@ import android.view.ViewGroup
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -30,7 +29,7 @@ import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.showOrHide
|
||||
import org.koitharu.kotatsu.databinding.FragmentPagesBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsViewModel
|
||||
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesViewModel
|
||||
import org.koitharu.kotatsu.list.ui.GridSpanResolver
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
|
||||
@@ -46,15 +45,15 @@ class PagesFragment :
|
||||
BaseFragment<FragmentPagesBinding>(),
|
||||
OnListItemClickListener<PageThumbnail> {
|
||||
|
||||
private val detailsViewModel by activityViewModels<DetailsViewModel>()
|
||||
private val viewModel by viewModels<PagesViewModel>()
|
||||
|
||||
@Inject
|
||||
lateinit var coil: ImageLoader
|
||||
|
||||
@Inject
|
||||
lateinit var settings: AppSettings
|
||||
|
||||
private val parentViewModel by ChaptersPagesViewModel.ActivityVMLazy(this)
|
||||
private val viewModel by viewModels<PagesViewModel>()
|
||||
|
||||
private var thumbnailsAdapter: PageThumbnailAdapter? = null
|
||||
private var spanResolver: GridSpanResolver? = null
|
||||
private var scrollListener: ScrollListener? = null
|
||||
@@ -64,12 +63,12 @@ class PagesFragment :
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
combine(
|
||||
detailsViewModel.details,
|
||||
detailsViewModel.history,
|
||||
detailsViewModel.selectedBranch,
|
||||
) { details, history, branch ->
|
||||
parentViewModel.mangaDetails,
|
||||
parentViewModel.readingState,
|
||||
parentViewModel.selectedBranch,
|
||||
) { details, readingState, branch ->
|
||||
if (details != null && (details.isLoaded || details.chapters.isNotEmpty())) {
|
||||
PagesViewModel.State(details.filterChapters(branch), history, branch)
|
||||
PagesViewModel.State(details.filterChapters(branch), readingState, branch)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -102,7 +101,7 @@ class PagesFragment :
|
||||
it.spanCount = checkNotNull(spanResolver).spanCount
|
||||
}
|
||||
}
|
||||
detailsViewModel.isChaptersEmpty.observe(viewLifecycleOwner, ::onNoChaptersChanged)
|
||||
parentViewModel.isChaptersEmpty.observe(viewLifecycleOwner, ::onNoChaptersChanged)
|
||||
viewModel.thumbnails.observe(viewLifecycleOwner, ::onThumbnailsChanged)
|
||||
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
|
||||
viewModel.isLoading.observe(viewLifecycleOwner) { binding.progressBar.showOrHide(it) }
|
||||
@@ -127,7 +126,7 @@ class PagesFragment :
|
||||
} else {
|
||||
startActivity(
|
||||
IntentBuilder(view.context)
|
||||
.manga(detailsViewModel.manga.value ?: return)
|
||||
.manga(parentViewModel.getMangaOrNull() ?: return)
|
||||
.state(ReaderState(item.page.chapterId, item.page.index, 0))
|
||||
.build(),
|
||||
)
|
||||
|
||||
@@ -7,7 +7,6 @@ import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
@@ -16,12 +15,13 @@ import org.koitharu.kotatsu.details.data.MangaDetails
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.reader.domain.ChaptersLoader
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class PagesViewModel @Inject constructor(
|
||||
private val chaptersLoader: ChaptersLoader,
|
||||
private val settings: AppSettings,
|
||||
settings: AppSettings,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private var loadingJob: Job? = null
|
||||
@@ -75,13 +75,13 @@ class PagesViewModel @Inject constructor(
|
||||
|
||||
private suspend fun doInit(state: State) {
|
||||
chaptersLoader.init(state.details)
|
||||
val initialChapterId = state.history?.chapterId?.takeIf {
|
||||
val initialChapterId = state.readerState?.chapterId?.takeIf {
|
||||
chaptersLoader.peekChapter(it) != null
|
||||
} ?: state.details.allChapters.firstOrNull()?.id ?: return
|
||||
if (!chaptersLoader.hasPages(initialChapterId)) {
|
||||
chaptersLoader.loadSingleChapter(initialChapterId)
|
||||
}
|
||||
updateList(state.history)
|
||||
updateList(state.readerState)
|
||||
}
|
||||
|
||||
private fun loadPrevNextChapter(isNext: Boolean): Job = launchJob(Dispatchers.Default) {
|
||||
@@ -91,13 +91,13 @@ class PagesViewModel @Inject constructor(
|
||||
val currentState = state.firstNotNull()
|
||||
val currentId = (if (isNext) chaptersLoader.last() else chaptersLoader.first()).chapterId
|
||||
chaptersLoader.loadPrevNextChapter(currentState.details, currentId, isNext)
|
||||
updateList(currentState.history)
|
||||
updateList(currentState.readerState)
|
||||
} finally {
|
||||
indicator.value = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateList(history: MangaHistory?) {
|
||||
private fun updateList(readerState: ReaderState?) {
|
||||
val snapshot = chaptersLoader.snapshot()
|
||||
val pages = buildList(snapshot.size + chaptersLoader.size + 2) {
|
||||
var previousChapterId = 0L
|
||||
@@ -109,7 +109,7 @@ class PagesViewModel @Inject constructor(
|
||||
previousChapterId = page.chapterId
|
||||
}
|
||||
this += PageThumbnail(
|
||||
isCurrent = history?.let {
|
||||
isCurrent = readerState?.let {
|
||||
page.chapterId == it.chapterId && page.index == it.page
|
||||
} ?: false,
|
||||
page = page,
|
||||
@@ -121,7 +121,7 @@ class PagesViewModel @Inject constructor(
|
||||
|
||||
data class State(
|
||||
val details: MangaDetails,
|
||||
val history: MangaHistory?,
|
||||
val readerState: ReaderState?,
|
||||
val branch: String?
|
||||
)
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ class ReaderActivity :
|
||||
}
|
||||
|
||||
override fun getParentActivityIntent(): Intent? {
|
||||
val manga = viewModel.manga?.toManga() ?: return null
|
||||
val manga = viewModel.getMangaOrNull() ?: return null
|
||||
return DetailsActivity.newIntent(this, manga)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filter
|
||||
@@ -41,7 +42,6 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
|
||||
@@ -49,11 +49,17 @@ import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.requireValue
|
||||
import org.koitharu.kotatsu.core.util.ext.sizeOrZero
|
||||
import org.koitharu.kotatsu.details.data.MangaDetails
|
||||
import org.koitharu.kotatsu.details.domain.DetailsInteractor
|
||||
import org.koitharu.kotatsu.details.domain.DetailsLoadUseCase
|
||||
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet.Companion.TAB_PAGES
|
||||
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesViewModel
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import org.koitharu.kotatsu.history.domain.HistoryUpdateUseCase
|
||||
import org.koitharu.kotatsu.list.domain.ReadingProgress.Companion.PROGRESS_NONE
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
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.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.util.assertNotNull
|
||||
@@ -70,14 +76,12 @@ private const val BOUNDS_PAGE_OFFSET = 2
|
||||
private const val PREFETCH_LIMIT = 10
|
||||
|
||||
@HiltViewModel
|
||||
class ReaderViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
class ReaderViewModel @Inject constructor(
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
private val dataRepository: MangaDataRepository,
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val bookmarksRepository: BookmarksRepository,
|
||||
private val settings: AppSettings,
|
||||
settings: AppSettings,
|
||||
private val pageSaveHelper: PageSaveHelper,
|
||||
private val pageLoader: PageLoader,
|
||||
private val chaptersLoader: ChaptersLoader,
|
||||
@@ -86,18 +90,31 @@ constructor(
|
||||
private val historyUpdateUseCase: HistoryUpdateUseCase,
|
||||
private val detectReaderModeUseCase: DetectReaderModeUseCase,
|
||||
private val statsCollector: StatsCollector,
|
||||
) : BaseViewModel() {
|
||||
@LocalStorageChanges localStorageChanges: SharedFlow<LocalManga?>,
|
||||
interactor: DetailsInteractor,
|
||||
deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
|
||||
downloadScheduler: DownloadWorker.Scheduler,
|
||||
) : ChaptersPagesViewModel(
|
||||
settings = settings,
|
||||
interactor = interactor,
|
||||
bookmarksRepository = bookmarksRepository,
|
||||
historyRepository = historyRepository,
|
||||
downloadScheduler = downloadScheduler,
|
||||
deleteLocalMangaUseCase = deleteLocalMangaUseCase,
|
||||
localStorageChanges = localStorageChanges,
|
||||
) {
|
||||
private val intent = MangaIntent(savedStateHandle)
|
||||
private val preselectedBranch = savedStateHandle.get<String>(ReaderActivity.EXTRA_BRANCH)
|
||||
|
||||
private var loadingJob: Job? = null
|
||||
private var pageSaveJob: Job? = null
|
||||
private var bookmarkJob: Job? = null
|
||||
private var stateChangeJob: Job? = null
|
||||
private val currentState = MutableStateFlow<ReaderState?>(savedStateHandle[ReaderActivity.EXTRA_STATE])
|
||||
private val mangaData = MutableStateFlow(intent.manga?.let { MangaDetails(it, null, null, false) })
|
||||
private val mangaFlow: Flow<Manga?>
|
||||
get() = mangaData.map { it?.toManga() }
|
||||
|
||||
init {
|
||||
selectedBranch.value = savedStateHandle.get<String>(ReaderActivity.EXTRA_BRANCH)
|
||||
readingState.value = savedStateHandle[ReaderActivity.EXTRA_STATE]
|
||||
mangaDetails.value = intent.manga?.let { MangaDetails(it, null, null, false) }
|
||||
}
|
||||
|
||||
val readerMode = MutableStateFlow<ReaderMode?>(null)
|
||||
val onPageSaved = MutableEventFlow<Uri?>()
|
||||
@@ -107,16 +124,13 @@ constructor(
|
||||
val incognitoMode = if (savedStateHandle.get<Boolean>(ReaderActivity.EXTRA_INCOGNITO) == true) {
|
||||
MutableStateFlow(true)
|
||||
} else {
|
||||
mangaFlow.map {
|
||||
it != null && historyRepository.shouldSkip(it)
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
||||
interactor.observeIncognitoMode(manga)
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
||||
}
|
||||
|
||||
val isPagesSheetEnabled = observeIsPagesSheetEnabled()
|
||||
|
||||
val content = MutableStateFlow(ReaderContent(emptyList(), null))
|
||||
val manga: MangaDetails?
|
||||
get() = mangaData.value
|
||||
|
||||
val pageAnimation = settings.observeAsStateFlow(
|
||||
scope = viewModelScope + Dispatchers.Default,
|
||||
@@ -164,15 +178,15 @@ constructor(
|
||||
val readerSettings = ReaderSettings(
|
||||
parentScope = viewModelScope,
|
||||
settings = settings,
|
||||
colorFilterFlow = mangaFlow.flatMapLatest {
|
||||
colorFilterFlow = manga.flatMapLatest {
|
||||
if (it == null) flowOf(null) else dataRepository.observeColorFilter(it.id)
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null),
|
||||
)
|
||||
|
||||
val isMangaNsfw = mangaFlow.map { it?.isNsfw == true }
|
||||
val isMangaNsfw = manga.map { it?.isNsfw == true }
|
||||
|
||||
val isBookmarkAdded = currentState.flatMapLatest { state ->
|
||||
val manga = mangaData.value?.toManga()
|
||||
val isBookmarkAdded = readingState.flatMapLatest { state ->
|
||||
val manga = mangaDetails.value?.toManga()
|
||||
if (state == null || manga == null) {
|
||||
flowOf(false)
|
||||
} else {
|
||||
@@ -190,7 +204,7 @@ constructor(
|
||||
if (key == AppSettings.KEY_READER_SLIDER) notifyStateChanged()
|
||||
}.launchIn(viewModelScope + Dispatchers.Default)
|
||||
launchJob(Dispatchers.Default) {
|
||||
val mangaId = mangaFlow.filterNotNull().first().id
|
||||
val mangaId = manga.filterNotNull().first().id
|
||||
appShortcutManager.notifyMangaOpened(mangaId)
|
||||
}
|
||||
}
|
||||
@@ -201,14 +215,14 @@ constructor(
|
||||
}
|
||||
|
||||
fun onPause() {
|
||||
manga?.let {
|
||||
getMangaOrNull()?.let {
|
||||
statsCollector.onPause(it.id)
|
||||
}
|
||||
}
|
||||
|
||||
fun switchMode(newMode: ReaderMode) {
|
||||
launchJob {
|
||||
val manga = checkNotNull(mangaData.value?.toManga())
|
||||
val manga = checkNotNull(getMangaOrNull())
|
||||
dataRepository.saveReaderMode(
|
||||
manga = manga,
|
||||
mode = newMode,
|
||||
@@ -222,24 +236,24 @@ constructor(
|
||||
|
||||
fun saveCurrentState(state: ReaderState? = null) {
|
||||
if (state != null) {
|
||||
currentState.value = state
|
||||
readingState.value = state
|
||||
savedStateHandle[ReaderActivity.EXTRA_STATE] = state
|
||||
}
|
||||
if (incognitoMode.value) {
|
||||
return
|
||||
}
|
||||
val readerState = state ?: currentState.value ?: return
|
||||
val readerState = state ?: readingState.value ?: return
|
||||
historyUpdateUseCase.invokeAsync(
|
||||
manga = mangaData.value?.toManga() ?: return,
|
||||
manga = getMangaOrNull() ?: return,
|
||||
readerState = readerState,
|
||||
percent = computePercent(readerState.chapterId, readerState.page),
|
||||
)
|
||||
}
|
||||
|
||||
fun getCurrentState() = currentState.value
|
||||
fun getCurrentState() = readingState.value
|
||||
|
||||
fun getCurrentChapterPages(): List<MangaPage>? {
|
||||
val chapterId = currentState.value?.chapterId ?: return null
|
||||
val chapterId = readingState.value?.chapterId ?: return null
|
||||
return chaptersLoader.getPages(chapterId).map { it.toMangaPage() }
|
||||
}
|
||||
|
||||
@@ -272,7 +286,7 @@ constructor(
|
||||
}
|
||||
|
||||
fun getCurrentPage(): MangaPage? {
|
||||
val state = currentState.value ?: return null
|
||||
val state = readingState.value ?: return null
|
||||
return content.value.pages.find {
|
||||
it.chapterId == state.chapterId && it.index == state.page
|
||||
}?.toMangaPage()
|
||||
@@ -294,9 +308,9 @@ constructor(
|
||||
val prevJob = loadingJob
|
||||
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
||||
prevJob?.cancelAndJoin()
|
||||
val prevState = currentState.requireValue()
|
||||
val prevState = readingState.requireValue()
|
||||
val newChapterId = if (delta != 0) {
|
||||
val allChapters = checkNotNull(manga).allChapters
|
||||
val allChapters = mangaDetails.requireValue().allChapters
|
||||
var index = allChapters.indexOfFirst { x -> x.id == prevState.chapterId }
|
||||
if (index < 0) {
|
||||
return@launchLoadingJob
|
||||
@@ -330,7 +344,7 @@ constructor(
|
||||
}
|
||||
val centerPos = (lowerPos + upperPos) / 2
|
||||
pages.getOrNull(centerPos)?.let { page ->
|
||||
currentState.update { cs ->
|
||||
readingState.update { cs ->
|
||||
cs?.copy(chapterId = page.chapterId, page = page.index)
|
||||
}
|
||||
}
|
||||
@@ -357,10 +371,10 @@ constructor(
|
||||
}
|
||||
bookmarkJob = launchJob(Dispatchers.Default) {
|
||||
loadingJob?.join()
|
||||
val state = checkNotNull(currentState.value)
|
||||
val state = checkNotNull(readingState.value)
|
||||
val page = checkNotNull(getCurrentPage()) { "Page not found" }
|
||||
val bookmark = Bookmark(
|
||||
manga = mangaData.requireValue().toManga(),
|
||||
manga = requireManga(),
|
||||
pageId = page.id,
|
||||
chapterId = state.chapterId,
|
||||
page = state.page,
|
||||
@@ -380,7 +394,7 @@ constructor(
|
||||
}
|
||||
bookmarkJob = launchJob {
|
||||
loadingJob?.join()
|
||||
val manga = mangaData.requireValue().toManga()
|
||||
val manga = requireManga()
|
||||
val state = checkNotNull(getCurrentState())
|
||||
bookmarksRepository.removeBookmark(manga.id, state.chapterId, state.page)
|
||||
onShowToast.call(R.string.bookmark_removed)
|
||||
@@ -390,28 +404,29 @@ constructor(
|
||||
private fun loadImpl() {
|
||||
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
||||
val details = detailsLoadUseCase.invoke(intent).first { x -> x.isLoaded }
|
||||
mangaData.value = details
|
||||
mangaDetails.value = details
|
||||
chaptersLoader.init(details)
|
||||
val manga = details.toManga()
|
||||
// obtain state
|
||||
if (currentState.value == null) {
|
||||
currentState.value = getStateFromIntent(manga)
|
||||
if (readingState.value == null) {
|
||||
readingState.value = getStateFromIntent(manga)
|
||||
}
|
||||
val mode = detectReaderModeUseCase.invoke(manga, currentState.value)
|
||||
val branch = chaptersLoader.peekChapter(currentState.value?.chapterId ?: 0L)?.branch
|
||||
mangaData.value = details.filterChapters(branch)
|
||||
val mode = detectReaderModeUseCase.invoke(manga, readingState.value)
|
||||
val branch = chaptersLoader.peekChapter(readingState.value?.chapterId ?: 0L)?.branch
|
||||
selectedBranch.value = branch
|
||||
mangaDetails.value = details.filterChapters(branch)
|
||||
readerMode.value = mode
|
||||
|
||||
chaptersLoader.loadSingleChapter(requireNotNull(currentState.value).chapterId)
|
||||
chaptersLoader.loadSingleChapter(requireNotNull(readingState.value).chapterId)
|
||||
// save state
|
||||
if (!incognitoMode.value) {
|
||||
currentState.value?.let {
|
||||
readingState.value?.let {
|
||||
val percent = computePercent(it.chapterId, it.page)
|
||||
historyUpdateUseCase.invoke(manga, it, percent)
|
||||
}
|
||||
}
|
||||
notifyStateChanged()
|
||||
content.value = ReaderContent(chaptersLoader.snapshot(), currentState.value)
|
||||
content.value = ReaderContent(chaptersLoader.snapshot(), readingState.value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,7 +435,7 @@ constructor(
|
||||
val prevJob = loadingJob
|
||||
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
||||
prevJob?.join()
|
||||
chaptersLoader.loadPrevNextChapter(mangaData.requireValue(), currentId, isNext)
|
||||
chaptersLoader.loadPrevNextChapter(mangaDetails.requireValue(), currentId, isNext)
|
||||
content.value = ReaderContent(chaptersLoader.snapshot(), null)
|
||||
}
|
||||
}
|
||||
@@ -439,7 +454,7 @@ constructor(
|
||||
private fun notifyStateChanged() {
|
||||
val state = getCurrentState().assertNotNull("state") ?: return
|
||||
val chapter = chaptersLoader.peekChapter(state.chapterId).assertNotNull("chapter") ?: return
|
||||
val m = manga.assertNotNull("manga") ?: return
|
||||
val m = mangaDetails.value.assertNotNull("manga") ?: return
|
||||
val chapterIndex = m.chapters[chapter.branch]?.indexOfFirst { it.id == chapter.id } ?: -1
|
||||
val newState = ReaderUiState(
|
||||
mangaName = m.toManga().title,
|
||||
@@ -461,7 +476,7 @@ constructor(
|
||||
|
||||
private fun computePercent(chapterId: Long, pageIndex: Int): Float {
|
||||
val branch = chaptersLoader.peekChapter(chapterId)?.branch
|
||||
val chapters = manga?.chapters?.get(branch) ?: return PROGRESS_NONE
|
||||
val chapters = mangaDetails.value?.chapters?.get(branch) ?: return PROGRESS_NONE
|
||||
val chaptersCount = chapters.size
|
||||
val chapterIndex = chapters.indexOfFirst { x -> x.id == chapterId }
|
||||
val pagesCount = chaptersLoader.getPagesCount(chapterId)
|
||||
@@ -495,6 +510,7 @@ constructor(
|
||||
|
||||
private suspend fun getStateFromIntent(manga: Manga): ReaderState {
|
||||
val history = historyRepository.getOne(manga)
|
||||
val preselectedBranch = selectedBranch.value
|
||||
val result = if (history != null) {
|
||||
if (preselectedBranch != null && preselectedBranch != manga.findChapter(history.chapterId)?.branch) {
|
||||
null
|
||||
|
||||
@@ -69,7 +69,7 @@ class ReaderConfigSheet :
|
||||
?: ReaderMode.STANDARD
|
||||
imageServerDelegate = ImageServerDelegate(
|
||||
mangaRepositoryFactory = mangaRepositoryFactory,
|
||||
mangaSource = viewModel.manga?.toManga()?.source,
|
||||
mangaSource = viewModel.getMangaOrNull()?.source,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ class ReaderConfigSheet :
|
||||
|
||||
R.id.button_color_filter -> {
|
||||
val page = viewModel.getCurrentPage() ?: return
|
||||
val manga = viewModel.manga?.toManga() ?: return
|
||||
val manga = viewModel.getMangaOrNull() ?: return
|
||||
startActivity(ColorFilterConfigActivity.newIntent(v.context, manga, page))
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<string name="chapter_d_of_d">Chapter %1$d of %2$d</string>
|
||||
<string name="close">Close</string>
|
||||
<string name="try_again">Try again</string>
|
||||
<!-- Should be short -->
|
||||
<string name="retry">Retry</string>
|
||||
<string name="clear_history">Clear history</string>
|
||||
<string name="nothing_found">Nothing found</string>
|
||||
|
||||
Reference in New Issue
Block a user