mirror of
https://github.com/mihonapp/mihon.git
synced 2026-10-16 13:48:09 -07:00
Make MangaViewModel collect the entry only while subscribed
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user