Make MangaViewModel collect the entry only while subscribed

This commit is contained in:
AntsyLich
2026-10-07 19:59:51 +06:00
parent 9a6dea804d
commit 7ff97761b9
@@ -46,15 +46,25 @@ import eu.kanade.tachiyomi.util.chapter.getNextUnread
import eu.kanade.tachiyomi.util.removeCovers import eu.kanade.tachiyomi.util.removeCovers
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import mihon.domain.chapter.interactor.FilterChaptersForDownload import mihon.domain.chapter.interactor.FilterChaptersForDownload
@@ -84,12 +94,12 @@ import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaWithChapterCount import tachiyomi.domain.manga.model.MangaWithChapterCount
import tachiyomi.domain.manga.model.applyFilter import tachiyomi.domain.manga.model.applyFilter
import tachiyomi.domain.manga.repository.MangaRepository
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import tachiyomi.domain.track.interactor.GetTracks import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.source.local.isLocal import tachiyomi.source.local.isLocal
import kotlin.math.floor import kotlin.math.floor
import kotlin.time.Duration.Companion.seconds
@AssistedInject @AssistedInject
class MangaViewModel( class MangaViewModel(
@@ -117,7 +127,6 @@ class MangaViewModel(
private val getTracks: GetTracks, private val getTracks: GetTracks,
private val addTracks: AddTracks, private val addTracks: AddTracks,
private val setMangaCategories: SetMangaCategories, private val setMangaCategories: SetMangaCategories,
private val mangaRepository: MangaRepository,
private val filterChaptersForDownload: FilterChaptersForDownload, private val filterChaptersForDownload: FilterChaptersForDownload,
private val updateMangaFromRemote: UpdateMangaFromRemote, private val updateMangaFromRemote: UpdateMangaFromRemote,
private val sourceManager: SourceManager, private val sourceManager: SourceManager,
@@ -125,9 +134,6 @@ class MangaViewModel(
private val coverCache: CoverCache, private val coverCache: CoverCache,
) : ViewModel() { ) : ViewModel() {
val state: StateFlow<MangaViewModel.State>
field = MutableStateFlow<MangaViewModel.State>(State.Loading)
@AssistedFactory @AssistedFactory
@ManualViewModelAssistedFactoryKey @ManualViewModelAssistedFactoryKey
@ContributesIntoMap(AppScope::class) @ContributesIntoMap(AppScope::class)
@@ -165,126 +171,163 @@ class MangaViewModel(
LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in libraryPreferences.autoUpdateMangaRestrictions.get() LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in libraryPreferences.autoUpdateMangaRestrictions.get()
private val selectedPositions: Array<Int> = arrayOf(-1, -1) // first and last selected index in list private val selectedPositions: Array<Int> = arrayOf(-1, -1) // first and last selected index in list
private val selectedChapterIds: HashSet<Long> = HashSet() private val selectedChapterIds = MutableStateFlow(emptySet<Long>())
/** private val dialog = MutableStateFlow<Dialog?>(null)
* Helper function to update the UI state only if it's currently in success state
*/ private val isRefreshingData = MutableStateFlow(false)
private inline fun updateSuccessState(func: (State.Success) -> State.Success) {
state.update { private val downloadStates = MutableStateFlow(emptyMap<Long, DownloadProgress>())
when (it) {
State.Loading -> it // A finished chapter leaves the queue, and its last status change can be lost with it, so its state is left to
is State.Success -> func(it) // the queried item once it's no longer queued
} private val queuedDownloadStates = combine(downloadStates, downloadManager.queueState) { states, queue ->
val queuedChapterIds = queue.mapTo(HashSet()) { it.chapter.id }
states.filterKeys { it in queuedChapterIds }
}
private val hideMissingChapters = libraryPreferences.hideMissingChapters.get()
private var hasPromptedToAddBefore = false
private val defaultChapterFlagsJob = viewModelScope.launchIO {
val manga = getMangaAndChapters.awaitManga(mangaId)
if (!manga.favorite) {
setMangaDefaultChapterFlags.await(manga)
} }
} }
private val mangaAndChapters = combine(
flow {
// So an entry outside the library isn't shown with its old chapter settings first
defaultChapterFlagsJob.join()
emitAll(getMangaAndChapters.subscribe(mangaId, applyScanlatorFilter = true))
}
.distinctUntilChanged(),
downloadCache.changes,
downloadManager.queueState,
) { (manga, chapters), _, _ ->
MangaAndChapters(
manga = manga,
source = sourceManager.getOrStub(manga.source),
chapters = chapters.toChapterListItems(manga),
)
}
private val scanlators = combine(
getAvailableScanlators.subscribe(mangaId).distinctUntilChanged(),
getExcludedScanlators.subscribe(mangaId).distinctUntilChanged(),
::Pair,
)
private val trackers = flow { emit(sourceManager.getOrStub(getMangaAndChapters.awaitManga(mangaId).source)) }
.flatMapLatest { source ->
combine(
getTracks.subscribe(mangaId).catch { logcat(LogPriority.ERROR, it) },
trackerManager.loggedInTrackersFlow(),
) { mangaTracks, loggedInTrackers ->
// Show only if the service supports this manga's source
val supportedTrackers = loggedInTrackers.filter { (it as? EnhancedTracker)?.accept(source) ?: true }
val supportedTrackerIds = supportedTrackers.map { it.id }.toHashSet()
val supportedTrackerTracks = mangaTracks.filter { it.trackerId in supportedTrackerIds }
supportedTrackerTracks.size to supportedTrackers.isNotEmpty()
}
}
.distinctUntilChanged()
.onStart { emit(0 to false) }
val state: StateFlow<State> = combine(
mangaAndChapters,
scanlators,
trackers,
combine(selectedChapterIds, queuedDownloadStates, ::Pair),
combine(dialog, isRefreshingData, ::Pair),
) {
mangaAndChapters,
(availableScanlators, excludedScanlators),
(trackingCount, hasLoggedInTrackers),
(selectedIds, downloads),
(dialog, isRefreshingData),
->
State.Success(
manga = mangaAndChapters.manga,
source = mangaAndChapters.source,
isFromSource = isFromSource,
chapters = mangaAndChapters.chapters.map { item ->
val download = downloads[item.id]
val selected = item.id in selectedIds
if (download == null && !selected) return@map item
item.copy(
downloadState = download?.status ?: item.downloadState,
downloadProgress = download?.progress ?: item.downloadProgress,
selected = selected,
)
},
availableScanlators = availableScanlators,
excludedScanlators = excludedScanlators,
trackingCount = trackingCount,
hasLoggedInTrackers = hasLoggedInTrackers,
isRefreshingData = isRefreshingData,
dialog = dialog,
hideMissingChapters = hideMissingChapters,
)
}
.flowOn(Dispatchers.IO)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5.seconds), State.Loading)
init { init {
viewModelScope.launchIO { viewModelScope.launchIO {
combine( merge(downloadManager.statusFlow(), downloadManager.progressFlow())
getMangaAndChapters.subscribe(mangaId, applyScanlatorFilter = true).distinctUntilChanged(), .filter { it.manga.id == mangaId }
downloadCache.changes, .catch { logcat(LogPriority.ERROR, it) }
downloadManager.queueState, .collect(::updateDownloadState)
) { mangaAndChapters, _, _ -> mangaAndChapters }
.collectLatest { (manga, chapters) ->
updateSuccessState {
it.copy(
manga = manga,
chapters = chapters.toChapterListItems(manga),
)
}
}
} }
viewModelScope.launchIO { viewModelScope.launchIO {
getExcludedScanlators.subscribe(mangaId) // The state loads the entry and its chapters anyway, so they aren't queried separately
.distinctUntilChanged() val loadedState = state.filterIsInstance<State.Success>().first()
.collectLatest { excludedScanlators -> val manga = loadedState.manga
updateSuccessState {
it.copy(excludedScanlators = excludedScanlators)
}
}
}
viewModelScope.launchIO {
getAvailableScanlators.subscribe(mangaId)
.distinctUntilChanged()
.collectLatest { availableScanlators ->
updateSuccessState {
it.copy(availableScanlators = availableScanlators)
}
}
}
observeDownloads()
viewModelScope.launchIO {
val manga = getMangaAndChapters.awaitManga(mangaId)
val chapters = getMangaAndChapters.awaitChapters(mangaId, applyScanlatorFilter = true)
.toChapterListItems(manga)
if (!manga.favorite) {
setMangaDefaultChapterFlags.await(manga)
}
val needRefreshInfo = !manga.initialized val needRefreshInfo = !manga.initialized
val needRefreshChapter = chapters.isEmpty() val needRefreshChapter = loadedState.chapters.isEmpty()
// Show what we have earlier
state.update {
State.Success(
manga = manga,
source = sourceManager.getOrStub(manga.source),
isFromSource = isFromSource,
chapters = chapters,
availableScanlators = getAvailableScanlators.await(mangaId),
excludedScanlators = getExcludedScanlators.await(mangaId),
isRefreshingData = needRefreshInfo || needRefreshChapter,
dialog = null,
hideMissingChapters = libraryPreferences.hideMissingChapters.get(),
)
}
// Start observe tracking since it only needs mangaId
observeTrackers()
// Fetch info-chapters when needed // Fetch info-chapters when needed
if ((needRefreshInfo || needRefreshChapter) && viewModelScope.isActive) { if (needRefreshInfo || needRefreshChapter) {
isRefreshingData.value = true
fetchAllFromSource( fetchAllFromSource(
manga = manga,
manualFetch = false, manualFetch = false,
fetchDetails = needRefreshInfo, fetchDetails = needRefreshInfo,
fetchChapters = needRefreshChapter, fetchChapters = needRefreshChapter,
) )
isRefreshingData.value = false
} }
// Initial loading finished
updateSuccessState { it.copy(isRefreshingData = false) }
} }
} }
fun fetchAllFromSource(manualFetch: Boolean = true) { fun fetchAllFromSource(manualFetch: Boolean = true) {
viewModelScope.launch { viewModelScope.launch {
updateSuccessState { it.copy(isRefreshingData = true) } isRefreshingData.value = true
fetchAllFromSource( fetchAllFromSource(
manga = getMangaAndChapters.awaitManga(mangaId),
manualFetch = manualFetch, manualFetch = manualFetch,
fetchDetails = true, fetchDetails = true,
fetchChapters = true, fetchChapters = true,
) )
updateSuccessState { it.copy(isRefreshingData = false) } isRefreshingData.value = false
} }
} }
private suspend fun fetchAllFromSource( private suspend fun fetchAllFromSource(
manga: Manga,
manualFetch: Boolean, manualFetch: Boolean,
fetchDetails: Boolean, fetchDetails: Boolean,
fetchChapters: Boolean, fetchChapters: Boolean,
) { ) {
val state = successState ?: return
try { try {
withUIContext { withUIContext {
val update = updateMangaFromRemote( val update = updateMangaFromRemote(
source = state.source, source = sourceManager.getOrStub(manga.source),
manga = state.manga, manga = manga,
fetchDetails = fetchDetails, fetchDetails = fetchDetails,
fetchChapters = fetchChapters, fetchChapters = fetchChapters,
manualFetch = manualFetch, manualFetch = manualFetch,
@@ -358,7 +401,7 @@ class MangaViewModel(
val duplicates = getDuplicateLibraryManga(manga) val duplicates = getDuplicateLibraryManga(manga)
if (duplicates.isNotEmpty()) { if (duplicates.isNotEmpty()) {
updateSuccessState { it.copy(dialog = Dialog.DuplicateManga(manga, duplicates)) } dialog.value = Dialog.DuplicateManga(manga, duplicates)
return@launchIO return@launchIO
} }
} }
@@ -397,35 +440,24 @@ class MangaViewModel(
viewModelScope.launch { viewModelScope.launch {
val categories = getCategories() val categories = getCategories()
val selection = getMangaCategoryIds(manga) val selection = getMangaCategoryIds(manga)
updateSuccessState { successState -> dialog.value = Dialog.ChangeCategory(
successState.copy( manga = manga,
dialog = Dialog.ChangeCategory( initialSelection = categories.mapAsCheckboxState { it.id in selection },
manga = manga, )
initialSelection = categories.mapAsCheckboxState { it.id in selection },
),
)
}
} }
} }
fun showSetFetchIntervalDialog() { fun showSetFetchIntervalDialog() {
val manga = successState?.manga ?: return val manga = successState?.manga ?: return
updateSuccessState { dialog.value = Dialog.SetFetchInterval(manga)
it.copy(dialog = Dialog.SetFetchInterval(manga))
}
} }
fun setFetchInterval(manga: Manga, interval: Int) { fun setFetchInterval(manga: Manga, interval: Int) {
viewModelScope.launchIO { viewModelScope.launchIO {
if ( updateManga.awaitUpdateFetchInterval(
updateManga.awaitUpdateFetchInterval( // Custom intervals are negative
// Custom intervals are negative manga.copy(fetchInterval = -interval),
manga.copy(fetchInterval = -interval), )
)
) {
val updatedManga = mangaRepository.getMangaById(manga.id)
updateSuccessState { it.copy(manga = updatedManga) }
}
} }
} }
@@ -503,41 +535,16 @@ class MangaViewModel(
// Chapters list - start // Chapters list - start
private fun observeDownloads() {
viewModelScope.launchIO {
downloadManager.statusFlow()
.filter { it.manga.id == successState?.manga?.id }
.catch { error -> logcat(LogPriority.ERROR, error) }
.collect {
withUIContext {
updateDownloadState(it)
}
}
}
viewModelScope.launchIO {
downloadManager.progressFlow()
.filter { it.manga.id == successState?.manga?.id }
.catch { error -> logcat(LogPriority.ERROR, error) }
.collect {
withUIContext {
updateDownloadState(it)
}
}
}
}
private fun updateDownloadState(download: Download) { private fun updateDownloadState(download: Download) {
updateSuccessState { successState -> val chapterId = download.chapter.id
val modifiedIndex = successState.chapters.indexOfFirst { it.id == download.chapter.id } downloadStates.update {
if (modifiedIndex < 0) return@updateSuccessState successState // Terminal states are derived by the queried item itself, so drop the override instead
// of letting it outlive reality, e.g. showing a since deleted chapter as downloaded.
val newChapters = successState.chapters.toMutableList().apply { if (download.status == Download.State.NOT_DOWNLOADED || download.status == Download.State.DOWNLOADED) {
val item = removeAt(modifiedIndex) it - chapterId
.copy(downloadState = download.status, downloadProgress = download.progress) } else {
add(modifiedIndex, item) it + (chapterId to DownloadProgress(download.status, download.progress))
} }
successState.copy(chapters = newChapters)
} }
} }
@@ -558,7 +565,6 @@ class MangaViewModel(
chapter = chapter, chapter = chapter,
downloadState = downloadState, downloadState = downloadState,
downloadProgress = activeDownload?.progress ?: 0, downloadProgress = activeDownload?.progress ?: 0,
selected = chapter.id in selectedChapterIds,
) )
} }
} }
@@ -638,7 +644,7 @@ class MangaViewModel(
chapters: List<Chapter>, chapters: List<Chapter>,
startNow: Boolean, startNow: Boolean,
) { ) {
val successState = successState ?: return if (successState == null) return
viewModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
if (startNow) { if (startNow) {
@@ -648,10 +654,8 @@ class MangaViewModel(
downloadChapters(chapters) downloadChapters(chapters)
} }
if (!isFavorited && !successState.hasPromptedToAddBefore) { if (!isFavorited && !hasPromptedToAddBefore) {
updateSuccessState { state -> hasPromptedToAddBefore = true
state.copy(hasPromptedToAddBefore = true)
}
val result = snackbarHostState.showSnackbar( val result = snackbarHostState.showSnackbar(
message = context.stringResource(MR.strings.snack_add_to_library), message = context.stringResource(MR.strings.snack_add_to_library),
actionLabel = context.stringResource(MR.strings.action_add), actionLabel = context.stringResource(MR.strings.action_add),
@@ -938,120 +942,70 @@ class MangaViewModel(
selected: Boolean, selected: Boolean,
fromLongPress: Boolean = false, fromLongPress: Boolean = false,
) { ) {
updateSuccessState { successState -> val chapters = successState?.processedChapters ?: return
val newChapters = successState.processedChapters.toMutableList().apply { val selectedIndex = chapters.indexOfFirst { it.id == item.chapter.id }
val selectedIndex = successState.processedChapters.indexOfFirst { it.id == item.chapter.id } if (selectedIndex < 0) return
if (selectedIndex < 0) return@apply
val selectedItem = get(selectedIndex) // Read from the ids, as the state catches up with a selection only after it's been made
if ((selectedItem.selected && selected) || (!selectedItem.selected && !selected)) return@apply val selectedIds = HashSet(selectedChapterIds.value)
if ((item.id in selectedIds) == selected) return
val firstSelection = none { it.selected } val firstSelection = chapters.none { it.id in selectedIds }
set(selectedIndex, selectedItem.copy(selected = selected)) selectedIds.addOrRemove(item.id, selected)
selectedChapterIds.addOrRemove(item.id, selected)
if (selected && fromLongPress) { if (selected && fromLongPress) {
if (firstSelection) { if (firstSelection) {
selectedPositions[0] = selectedIndex selectedPositions[0] = selectedIndex
selectedPositions[1] = selectedIndex selectedPositions[1] = selectedIndex
} else { } else {
// Try to select the items in-between when possible // Try to select the items in-between when possible
val range: IntRange val range: IntRange
if (selectedIndex < selectedPositions[0]) { if (selectedIndex < selectedPositions[0]) {
range = selectedIndex + 1..<selectedPositions[0] range = selectedIndex + 1..<selectedPositions[0]
selectedPositions[0] = selectedIndex selectedPositions[0] = selectedIndex
} else if (selectedIndex > selectedPositions[1]) { } else if (selectedIndex > selectedPositions[1]) {
range = (selectedPositions[1] + 1)..<selectedIndex range = (selectedPositions[1] + 1)..<selectedIndex
selectedPositions[1] = selectedIndex selectedPositions[1] = selectedIndex
} else { } else {
// Just select itself // Just select itself
range = IntRange.EMPTY range = IntRange.EMPTY
} }
range.forEach { range.forEach { selectedIds.add(chapters[it].id) }
val inbetweenItem = get(it) }
if (!inbetweenItem.selected) { } else if (!fromLongPress) {
selectedChapterIds.add(inbetweenItem.id) if (!selected) {
set(it, inbetweenItem.copy(selected = true)) if (selectedIndex == selectedPositions[0]) {
} selectedPositions[0] = chapters.indexOfFirst { it.id in selectedIds }
} } else if (selectedIndex == selectedPositions[1]) {
} selectedPositions[1] = chapters.indexOfLast { it.id in selectedIds }
} else if (!fromLongPress) { }
if (!selected) { } else {
if (selectedIndex == selectedPositions[0]) { if (selectedIndex < selectedPositions[0]) {
selectedPositions[0] = indexOfFirst { it.selected } selectedPositions[0] = selectedIndex
} else if (selectedIndex == selectedPositions[1]) { } else if (selectedIndex > selectedPositions[1]) {
selectedPositions[1] = indexOfLast { it.selected } selectedPositions[1] = selectedIndex
}
} else {
if (selectedIndex < selectedPositions[0]) {
selectedPositions[0] = selectedIndex
} else if (selectedIndex > selectedPositions[1]) {
selectedPositions[1] = selectedIndex
}
}
} }
} }
successState.copy(chapters = newChapters)
} }
selectedChapterIds.value = selectedIds
} }
fun toggleAllSelection(selected: Boolean) { fun toggleAllSelection(selected: Boolean) {
updateSuccessState { successState -> selectedChapterIds.value = if (selected) allChapters.orEmpty().mapTo(HashSet()) { it.id } else emptySet()
val newChapters = successState.chapters.map { selectedPositions[0] = -1
selectedChapterIds.addOrRemove(it.id, selected) selectedPositions[1] = -1
it.copy(selected = selected)
}
selectedPositions[0] = -1
selectedPositions[1] = -1
successState.copy(chapters = newChapters)
}
} }
fun invertSelection() { fun invertSelection() {
updateSuccessState { successState -> val selectedIds = selectedChapterIds.value
val newChapters = successState.chapters.map { selectedChapterIds.value = allChapters.orEmpty().filterNot { it.id in selectedIds }.mapTo(HashSet()) { it.id }
selectedChapterIds.addOrRemove(it.id, !it.selected) selectedPositions[0] = -1
it.copy(selected = !it.selected) selectedPositions[1] = -1
}
selectedPositions[0] = -1
selectedPositions[1] = -1
successState.copy(chapters = newChapters)
}
} }
// Chapters list - end // Chapters list - end
// Track sheet - start
private fun observeTrackers() {
val manga = successState?.manga ?: return
viewModelScope.launchIO {
combine(
getTracks.subscribe(manga.id).catch { logcat(LogPriority.ERROR, it) },
trackerManager.loggedInTrackersFlow(),
) { mangaTracks, loggedInTrackers ->
// Show only if the service supports this manga's source
val supportedTrackers = loggedInTrackers.filter { (it as? EnhancedTracker)?.accept(source!!) ?: true }
val supportedTrackerIds = supportedTrackers.map { it.id }.toHashSet()
val supportedTrackerTracks = mangaTracks.filter { it.trackerId in supportedTrackerIds }
supportedTrackerTracks.size to supportedTrackers.isNotEmpty()
}
.distinctUntilChanged()
.collectLatest { (trackingCount, hasLoggedInTrackers) ->
updateSuccessState {
it.copy(
trackingCount = trackingCount,
hasLoggedInTrackers = hasLoggedInTrackers,
)
}
}
}
}
// Track sheet - end
sealed interface Dialog { sealed interface Dialog {
data class ChangeCategory( data class ChangeCategory(
val manga: Manga, val manga: Manga,
@@ -1067,28 +1021,28 @@ class MangaViewModel(
} }
fun dismissDialog() { fun dismissDialog() {
updateSuccessState { it.copy(dialog = null) } dialog.value = null
} }
fun showDeleteChapterDialog(chapters: List<Chapter>) { fun showDeleteChapterDialog(chapters: List<Chapter>) {
updateSuccessState { it.copy(dialog = Dialog.DeleteChapters(chapters)) } dialog.value = Dialog.DeleteChapters(chapters)
} }
fun showSettingsDialog() { fun showSettingsDialog() {
updateSuccessState { it.copy(dialog = Dialog.SettingsSheet) } dialog.value = Dialog.SettingsSheet
} }
fun showTrackDialog() { fun showTrackDialog() {
updateSuccessState { it.copy(dialog = Dialog.TrackSheet) } dialog.value = Dialog.TrackSheet
} }
fun showCoverDialog() { fun showCoverDialog() {
updateSuccessState { it.copy(dialog = Dialog.FullCover) } dialog.value = Dialog.FullCover
} }
fun showMigrateDialog(duplicate: Manga) { fun showMigrateDialog(duplicate: Manga) {
val manga = successState?.manga ?: return val manga = successState?.manga ?: return
updateSuccessState { it.copy(dialog = Dialog.Migrate(target = manga, current = duplicate)) } dialog.value = Dialog.Migrate(target = manga, current = duplicate)
} }
fun setExcludedScanlators(excludedScanlators: Set<String>) { fun setExcludedScanlators(excludedScanlators: Set<String>) {
@@ -1097,6 +1051,10 @@ class MangaViewModel(
} }
} }
private data class MangaAndChapters(val manga: Manga, val source: Source, val chapters: List<ChapterList.Item>)
private data class DownloadProgress(val status: Download.State, val progress: Int)
sealed interface State { sealed interface State {
@Immutable @Immutable
data object Loading : State data object Loading : State
@@ -1113,7 +1071,6 @@ class MangaViewModel(
val hasLoggedInTrackers: Boolean = false, val hasLoggedInTrackers: Boolean = false,
val isRefreshingData: Boolean = false, val isRefreshingData: Boolean = false,
val dialog: Dialog? = null, val dialog: Dialog? = null,
val hasPromptedToAddBefore: Boolean = false,
val hideMissingChapters: Boolean = false, val hideMissingChapters: Boolean = false,
) : State { ) : State {
val processedChapters by lazy { val processedChapters by lazy {