Refactor details and reader ViewModels

This commit is contained in:
Koitharu
2024-08-31 12:09:22 +03:00
parent eb49b31aeb
commit 0cc019ef19
18 changed files with 378 additions and 275 deletions

View File

@@ -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(),
)

View File

@@ -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,

View File

@@ -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) }

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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)

View File

@@ -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}")
}
}
}

View File

@@ -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()

View File

@@ -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>) {

View File

@@ -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(),
)

View File

@@ -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()

View File

@@ -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(),
)

View File

@@ -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?
)
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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))
}

View File

@@ -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>