diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/DoubleManga.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/DoubleManga.kt new file mode 100644 index 000000000..460286fa1 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/DoubleManga.kt @@ -0,0 +1,76 @@ +package org.koitharu.kotatsu.core.model + +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.reader.data.filterChapters + +data class DoubleManga( + private val remoteManga: Result?, + private val localManga: Result?, +) { + + constructor(manga: Manga) : this( + remoteManga = if (manga.source != MangaSource.LOCAL) Result.success(manga) else null, + localManga = if (manga.source == MangaSource.LOCAL) Result.success(manga) else null, + ) + + val remote: Manga? + get() = remoteManga?.getOrNull() + + val local: Manga? + get() = localManga?.getOrNull() + + val any: Manga? + get() = remote ?: local + + val hasRemote: Boolean + get() = remoteManga?.isSuccess == true + + val hasLocal: Boolean + get() = localManga?.isSuccess == true + + val chapters: List? by lazy(LazyThreadSafetyMode.PUBLICATION) { + mergeChapters() + } + + fun requireAny(): Manga { + val result = remoteManga?.getOrNull() ?: localManga?.getOrNull() + if (result != null) { + return result + } + throw ( + remoteManga?.exceptionOrNull() + ?: localManga?.exceptionOrNull() + ?: IllegalStateException("No online either local manga available") + ) + } + + fun filterChapters(branch: String?) = DoubleManga( + remoteManga?.map { it.filterChapters(branch) }, + localManga?.map { it.filterChapters(branch) }, + ) + + private fun mergeChapters(): List? { + val remoteChapters = remote?.chapters + val localChapters = local?.chapters + if (localChapters == null && remoteChapters == null) { + return null + } + val localMap = if (!localChapters.isNullOrEmpty()) { + localChapters.associateByTo(LinkedHashMap(localChapters.size)) { it.id } + } else { + null + } + val result = ArrayList(maxOf(remoteChapters?.size ?: 0, localChapters?.size ?: 0)) + remoteChapters?.forEach { r -> + localMap?.remove(r.id)?.let { l -> + result.add(l) + } ?: result.add(r) + } + localMap?.values?.let { + result.addAll(it) + } + return result + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt index 4ed4c7885..cbfc5e1f8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt @@ -5,6 +5,7 @@ import org.koitharu.kotatsu.core.util.ext.iterator import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.mapToSet fun Collection.ids() = mapToSet { it.id } @@ -54,3 +55,6 @@ fun Manga.getPreferredBranch(history: MangaHistory?): String? { } return candidates.ifEmpty { groups }.maxByOrNull { it.value.size }?.key } + +val Manga.isLocal: Boolean + get() = source == MangaSource.LOCAL diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaIntent.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaIntent.kt index 5c3d54a17..8ab582147 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaIntent.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaIntent.kt @@ -12,28 +12,31 @@ import org.koitharu.kotatsu.parsers.model.Manga class MangaIntent private constructor( @JvmField val manga: Manga?, - @JvmField val mangaId: Long, + @JvmField val id: Long, @JvmField val uri: Uri?, ) { constructor(intent: Intent?) : this( manga = intent?.getParcelableExtraCompat(KEY_MANGA)?.manga, - mangaId = intent?.getLongExtra(KEY_ID, ID_NONE) ?: ID_NONE, + id = intent?.getLongExtra(KEY_ID, ID_NONE) ?: ID_NONE, uri = intent?.data, ) constructor(savedStateHandle: SavedStateHandle) : this( manga = savedStateHandle.get(KEY_MANGA)?.manga, - mangaId = savedStateHandle[KEY_ID] ?: ID_NONE, + id = savedStateHandle[KEY_ID] ?: ID_NONE, uri = savedStateHandle[BaseActivity.EXTRA_DATA], ) constructor(args: Bundle?) : this( manga = args?.getParcelableCompat(KEY_MANGA)?.manga, - mangaId = args?.getLong(KEY_ID, ID_NONE) ?: ID_NONE, + id = args?.getLong(KEY_ID, ID_NONE) ?: ID_NONE, uri = null, ) + val mangaId: Long + get() = if (id != ID_NONE) id else manga?.id ?: uri?.lastPathSegment?.toLongOrNull() ?: ID_NONE + companion object { const val ID_NONE = 0L diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DetailsInteractor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DetailsInteractor.kt new file mode 100644 index 000000000..3968e4399 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DetailsInteractor.kt @@ -0,0 +1,94 @@ +package org.koitharu.kotatsu.details.domain + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import org.koitharu.kotatsu.core.model.DoubleManga +import org.koitharu.kotatsu.core.model.isLocal +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.prefs.observeAsFlow +import org.koitharu.kotatsu.favourites.domain.FavouritesRepository +import org.koitharu.kotatsu.history.domain.HistoryRepository +import org.koitharu.kotatsu.local.data.LocalManga +import org.koitharu.kotatsu.local.domain.LocalMangaRepository +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler +import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo +import org.koitharu.kotatsu.tracker.domain.TrackingRepository +import org.koitharu.kotatsu.util.ext.printStackTraceDebug +import java.io.IOException +import javax.inject.Inject + +class DetailsInteractor @Inject constructor( + private val historyRepository: HistoryRepository, + private val favouritesRepository: FavouritesRepository, + private val localMangaRepository: LocalMangaRepository, + private val trackingRepository: TrackingRepository, + private val settings: AppSettings, + private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>, +) { + + fun observeIsFavourite(mangaId: Long): Flow { + return favouritesRepository.observeCategoriesIds(mangaId) + .map { it.isNotEmpty() } + } + + fun observeNewChapters(mangaId: Long): Flow { + return settings.observeAsFlow(AppSettings.KEY_TRACKER_ENABLED) { isTrackerEnabled } + .flatMapLatest { isEnabled -> + if (isEnabled) { + trackingRepository.observeNewChaptersCount(mangaId) + } else { + flowOf(0) + } + } + } + + fun observeScrobblingInfo(mangaId: Long): Flow> { + return combine( + scrobblers.map { it.observeScrobblingInfo(mangaId) }, + ) { scrobblingInfo -> + scrobblingInfo.filterNotNull() + } + } + + suspend fun deleteLocalManga(manga: Manga) { + val victim = if (manga.isLocal) manga else localMangaRepository.findSavedManga(manga)?.manga + checkNotNull(victim) { "Cannot find saved manga for ${manga.title}" } + val original = if (manga.isLocal) localMangaRepository.getRemoteManga(manga) else manga + localMangaRepository.delete(victim) || throw IOException("Unable to delete file") + runCatchingCancellable { + historyRepository.deleteOrSwap(victim, original) + }.onFailure { + it.printStackTraceDebug() + } + } + + fun observeIncognitoMode(mangaFlow: Flow): Flow { + return mangaFlow + .distinctUntilChangedBy { it?.isNsfw } + .flatMapLatest { manga -> + if (manga != null) { + historyRepository.observeShouldSkip(manga) + } else { + settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled } + } + } + } + + suspend fun updateLocal(subject: DoubleManga?, localManga: LocalManga): DoubleManga? { + return if (subject?.any?.id == localManga.manga.id) { + subject.copy( + localManga = runCatchingCancellable { + localMangaRepository.getDetails(localManga.manga) + }, + ) + } else { + subject + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt index e82993114..e3b3c1e64 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt @@ -160,12 +160,14 @@ class ChaptersFragment : val selectedIds = selectionController?.peekCheckedIds() ?: return false val allItems = chaptersAdapter?.items.orEmpty() val items = allItems.withIndex().filter { (_, x) -> x.chapter.id in selectedIds } - menu.findItem(R.id.action_save).isVisible = items.none { (_, x) -> - x.chapter.source == MangaSource.LOCAL - } - menu.findItem(R.id.action_delete).isVisible = items.all { (_, x) -> - x.chapter.source == MangaSource.LOCAL + var canSave = true + var canDelete = true + items.forEach { (_, x) -> + val isLocal = x.isDownloaded || x.chapter.source == MangaSource.LOCAL + if (isLocal) canSave = false else canDelete = false } + menu.findItem(R.id.action_save).isVisible = canSave + menu.findItem(R.id.action_delete).isVisible = canDelete menu.findItem(R.id.action_select_all).isVisible = items.size < allItems.size menu.findItem(R.id.action_mark_current).isVisible = items.size == 1 mode.title = items.size.toString() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersMapper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersMapper.kt index 618d623e1..27a937c0c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersMapper.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersMapper.kt @@ -52,7 +52,7 @@ fun mapChapters( isCurrent = chapter.id == currentId, isUnread = isUnread, isNew = false, - isDownloaded = false, + isDownloaded = remoteManga != null, ) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt index 0d3101f28..927e9b80c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt @@ -8,6 +8,7 @@ import androidx.core.net.toUri import androidx.core.text.getSpans import androidx.core.text.parseAsHtml import androidx.lifecycle.LiveData +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.asFlow import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope @@ -25,82 +26,74 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.update import kotlinx.coroutines.plus 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.DoubleManga +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.observeAsFlow import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.util.SingleLiveEvent import org.koitharu.kotatsu.core.util.asFlowLiveData import org.koitharu.kotatsu.core.util.ext.computeSize +import org.koitharu.kotatsu.core.util.ext.requireValue import org.koitharu.kotatsu.core.util.ext.toFileOrNull import org.koitharu.kotatsu.details.domain.BranchComparator +import org.koitharu.kotatsu.details.domain.DetailsInteractor 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.download.ui.worker.DownloadWorker -import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.local.data.LocalManga import org.koitharu.kotatsu.local.data.LocalStorageChanges -import org.koitharu.kotatsu.local.domain.LocalMangaRepository +import org.koitharu.kotatsu.local.domain.DoubleMangaLoader import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.parsers.util.runCatchingCancellable 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 -import org.koitharu.kotatsu.tracker.domain.TrackingRepository -import org.koitharu.kotatsu.util.ext.printStackTraceDebug -import java.io.IOException import javax.inject.Inject @HiltViewModel class DetailsViewModel @Inject constructor( private val historyRepository: HistoryRepository, - favouritesRepository: FavouritesRepository, - private val localMangaRepository: LocalMangaRepository, - trackingRepository: TrackingRepository, private val bookmarksRepository: BookmarksRepository, private val settings: AppSettings, private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>, private val imageGetter: Html.ImageGetter, - private val delegate: MangaDetailsDelegate, @LocalStorageChanges private val localStorageChanges: SharedFlow, private val downloadScheduler: DownloadWorker.Scheduler, + private val interactor: DetailsInteractor, + savedStateHandle: SavedStateHandle, + private val mangaLoader: DoubleMangaLoader, ) : BaseViewModel() { + private val intent = MangaIntent(savedStateHandle) + private val mangaId = intent.mangaId + private val doubleManga: MutableStateFlow = MutableStateFlow(intent.manga?.let { DoubleManga(it) }) private var loadingJob: Job val onShowToast = SingleLiveEvent() val onDownloadStarted = SingleLiveEvent() - private val mangaData = combine( - delegate.onlineManga, - delegate.localManga, - ) { o, l -> - o ?: l - }.stateIn(viewModelScope, SharingStarted.Lazily, null) + private val mangaData = doubleManga.map { it?.any } + .stateIn(viewModelScope, SharingStarted.Eagerly, doubleManga.value?.any) - private val history = historyRepository.observeOne(delegate.mangaId) + private val history = historyRepository.observeOne(mangaId) .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null) - private val favourite = favouritesRepository.observeCategoriesIds(delegate.mangaId).map { it.isNotEmpty() } + private val favourite = interactor.observeIsFavourite(mangaId) .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false) - private val newChapters = settings.observeAsFlow(AppSettings.KEY_TRACKER_ENABLED) { isTrackerEnabled } - .flatMapLatest { isEnabled -> - if (isEnabled) { - trackingRepository.observeNewChaptersCount(delegate.mangaId) - } else { - flowOf(0) - } - }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0) + private val newChapters = interactor.observeNewChapters(mangaId) + .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0) private val chaptersQuery = MutableStateFlow("") + private val selectedBranch = MutableStateFlow(null) private val chaptersReversed = settings.observeAsFlow(AppSettings.KEY_REVERSE_CHAPTERS) { chaptersReverse } .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false) @@ -112,9 +105,9 @@ class DetailsViewModel @Inject constructor( val historyInfo: LiveData = combine( mangaData, - delegate.selectedBranch, + selectedBranch, history, - historyRepository.observeShouldSkip(mangaData), + interactor.observeIncognitoMode(mangaData), ) { m, b, h, im -> HistoryInfo(m, b, h, im) }.asFlowLiveData( @@ -126,10 +119,11 @@ class DetailsViewModel @Inject constructor( if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList()) }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList()) - val localSize = delegate.localManga + val localSize = doubleManga .map { - if (it != null) { - val file = it.url.toUri().toFileOrNull() + val local = it?.local + if (local != null) { + val file = local.url.toUri().toFileOrNull() file?.computeSize() ?: 0L } else { 0L @@ -152,46 +146,38 @@ class DetailsViewModel @Inject constructor( val isScrobblingAvailable: Boolean get() = scrobblers.any { it.isAvailable } - val scrobblingInfo: LiveData> = combine( - scrobblers.map { it.observeScrobblingInfo(delegate.mangaId) }, - ) { scrobblingInfo -> - scrobblingInfo.filterNotNull() - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList()) + val scrobblingInfo: LiveData> = interactor.observeScrobblingInfo(mangaId) + .asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList()) val branches: LiveData> = combine( - delegate.onlineManga, - delegate.localManga, - delegate.selectedBranch, - ) { m, l, b -> - val chapters = concat(m?.chapters, l?.chapters) - if (chapters.isEmpty()) return@combine emptyList() + doubleManga, + selectedBranch, + ) { m, b -> + val chapters = m?.chapters + if (chapters.isNullOrEmpty()) return@combine emptyList() chapters.groupBy { x -> x.branch } .map { x -> MangaBranch(x.key, x.value.size, x.key == b) } .sortedWith(BranchComparator()) }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList()) - val selectedBranchName = delegate.selectedBranch + val selectedBranchName = selectedBranch .asFlowLiveData(viewModelScope.coroutineContext, null) val isChaptersEmpty: LiveData = combine( - delegate.onlineManga, - delegate.localManga, + doubleManga, isLoading.asFlow(), - ) { manga, local, loading -> - (manga != null && manga.chapters.isNullOrEmpty()) && - (local != null && local.chapters.isNullOrEmpty()) && - !loading + ) { manga, loading -> + manga?.any != null && manga.chapters.isNullOrEmpty() && !loading }.asFlowLiveData(viewModelScope.coroutineContext, false) val chapters = combine( combine( - delegate.onlineManga, - delegate.localManga, + doubleManga, history, - delegate.selectedBranch, + selectedBranch, newChapters, - ) { manga, local, history, branch, news -> - mapChapters(manga, local, history, news, branch) + ) { manga, history, branch, news -> + mapChapters(manga?.remote, manga?.local, history, news, branch) }, chaptersReversed, chaptersQuery, @@ -200,7 +186,7 @@ class DetailsViewModel @Inject constructor( }.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default) val selectedBranchValue: String? - get() = delegate.selectedBranch.value + get() = selectedBranch.value init { loadingJob = doLoad() @@ -216,20 +202,14 @@ class DetailsViewModel @Inject constructor( } fun deleteLocal() { - val m = delegate.localManga.value + val m = doubleManga.value?.local if (m == null) { onShowToast.call(R.string.file_not_found) return } launchLoadingJob(Dispatchers.Default) { - val manga = if (m.source == MangaSource.LOCAL) m else localMangaRepository.findSavedManga(m)?.manga - checkNotNull(manga) { "Cannot find saved manga for ${m.title}" } - val original = localMangaRepository.getRemoteManga(manga) - localMangaRepository.delete(manga) || throw IOException("Unable to delete file") - runCatchingCancellable { - historyRepository.deleteOrSwap(manga, original) - } - onMangaRemoved.emitCall(manga) + interactor.deleteLocalManga(m) + onMangaRemoved.emitCall(m) } } @@ -245,11 +225,7 @@ class DetailsViewModel @Inject constructor( } fun setSelectedBranch(branch: String?) { - delegate.selectedBranch.value = branch - } - - fun getRemoteManga(): Manga? { - return delegate.onlineManga.value + selectedBranch.value = branch } fun performChapterSearch(query: String?) { @@ -260,7 +236,7 @@ class DetailsViewModel @Inject constructor( val scrobbler = getScrobbler(index) ?: return launchJob(Dispatchers.Default) { scrobbler.updateScrobblingInfo( - mangaId = delegate.mangaId, + mangaId = mangaId, rating = rating, status = status, comment = null, @@ -272,26 +248,32 @@ class DetailsViewModel @Inject constructor( val scrobbler = getScrobbler(index) ?: return launchJob(Dispatchers.Default) { scrobbler.unregisterScrobbling( - mangaId = delegate.mangaId, + mangaId = mangaId, ) } } fun markChapterAsCurrent(chapterId: Long) { launchJob(Dispatchers.Default) { - val manga = checkNotNull(mangaData.value) - val chapters = checkNotNull(manga.getChapters(selectedBranchValue)) + val manga = checkNotNull(doubleManga.value) + val chapters = checkNotNull(manga.filterChapters(selectedBranchValue).chapters) 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, chapterId = chapterId, page = 0, scroll = 0, percent = percent) + historyRepository.addOrUpdate( + manga = manga.requireAny(), + chapterId = chapterId, + page = 0, + scroll = 0, + percent = percent, + ) } } fun download(chaptersIds: Set?) { launchJob(Dispatchers.Default) { downloadScheduler.schedule( - delegate.onlineManga.value ?: checkNotNull(manga.value), + doubleManga.requireValue().requireAny(), chaptersIds, ) onDownloadStarted.emitCall(Unit) @@ -299,7 +281,12 @@ class DetailsViewModel @Inject constructor( } private fun doLoad() = launchLoadingJob(Dispatchers.Default) { - delegate.doLoad() + val result = mangaLoader.load(intent) + val manga = result.requireAny() + // find default branch + val hist = historyRepository.getOne(manga) + selectedBranch.value = manga.getPreferredBranch(hist) + doubleManga.value = result } private fun List.filterSearch(query: String): List { @@ -313,21 +300,9 @@ class DetailsViewModel @Inject constructor( private suspend fun onDownloadComplete(downloadedManga: LocalManga?) { downloadedManga ?: return - val currentManga = mangaData.value ?: return - if (currentManga.id != downloadedManga.manga.id) { - return - } - if (currentManga.source == MangaSource.LOCAL) { - reload() - } else { - viewModelScope.launch(Dispatchers.Default) { - runCatchingCancellable { - localMangaRepository.getDetails(downloadedManga.manga) - }.onSuccess { - delegate.publishManga(it) - }.onFailure { - it.printStackTraceDebug() - } + launchJob { + doubleManga.update { + interactor.updateLocal(it, downloadedManga) } } } @@ -353,18 +328,4 @@ class DetailsViewModel @Inject constructor( } return scrobbler } - - private fun concat(a: List?, b: List?): List { - return when { - a == null && b == null -> emptyList() - a == null && b != null -> b - a != null && b == null -> a - a != null && b != null -> buildList(a.size + b.size) { - addAll(a) - addAll(b) - } - - else -> error("This shouldn't have happened") - } - } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt deleted file mode 100644 index eee5e04e6..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt +++ /dev/null @@ -1,89 +0,0 @@ -package org.koitharu.kotatsu.details.ui - -import androidx.lifecycle.SavedStateHandle -import dagger.hilt.android.ViewModelLifecycle -import dagger.hilt.android.scopes.ViewModelScoped -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.stateIn -import org.koitharu.kotatsu.core.model.getPreferredBranch -import org.koitharu.kotatsu.core.os.NetworkState -import org.koitharu.kotatsu.core.parser.MangaDataRepository -import org.koitharu.kotatsu.core.parser.MangaIntent -import org.koitharu.kotatsu.core.parser.MangaRepository -import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope -import org.koitharu.kotatsu.history.domain.HistoryRepository -import org.koitharu.kotatsu.local.domain.LocalMangaRepository -import org.koitharu.kotatsu.parsers.exception.NotFoundException -import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.parsers.util.runCatchingCancellable -import org.koitharu.kotatsu.util.ext.printStackTraceDebug -import javax.inject.Inject - -@ViewModelScoped -class MangaDetailsDelegate @Inject constructor( - savedStateHandle: SavedStateHandle, - lifecycle: ViewModelLifecycle, - private val mangaDataRepository: MangaDataRepository, - private val historyRepository: HistoryRepository, - private val localMangaRepository: LocalMangaRepository, - private val mangaRepositoryFactory: MangaRepository.Factory, - networkState: NetworkState, -) { - private val viewModelScope = RetainedLifecycleCoroutineScope(lifecycle) - - private val intent = MangaIntent(savedStateHandle) - private val onlineMangaStateFlow = MutableStateFlow(null) - private val localMangaStateFlow = MutableStateFlow(null) - - val onlineManga = combine( - onlineMangaStateFlow, - networkState, - ) { m, s -> m.takeIf { s } } - .stateIn(viewModelScope, SharingStarted.Lazily, null) - val localManga = localMangaStateFlow.asStateFlow() - - val selectedBranch = MutableStateFlow(null) - val mangaId = intent.manga?.id ?: intent.mangaId - - init { - intent.manga?.let { - publishManga(it) - } - } - - suspend fun doLoad() { - var manga = mangaDataRepository.resolveIntent(intent) ?: throw NotFoundException("Cannot find manga", "") - publishManga(manga) - manga = mangaRepositoryFactory.create(manga.source).getDetails(manga) - // find default branch - val hist = historyRepository.getOne(manga) - selectedBranch.value = manga.getPreferredBranch(hist) - publishManga(manga) - runCatchingCancellable { - if (manga.source == MangaSource.LOCAL) { - val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatchingCancellable null - mangaRepositoryFactory.create(m.source).getDetails(m) - } else { - localMangaRepository.findSavedManga(manga)?.manga - } - }.onFailure { error -> - error.printStackTraceDebug() - }.onSuccess { - if (it != null) { - publishManga(it) - } - } - } - - fun publishManga(manga: Manga) { - if (manga.source == MangaSource.LOCAL) { - localMangaStateFlow - } else { - onlineMangaStateFlow - }.value = manga - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/domain/HistoryRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/domain/HistoryRepository.kt index 784f74280..3cc4a81b0 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/history/domain/HistoryRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/domain/HistoryRepository.kt @@ -4,9 +4,7 @@ import androidx.room.withTransaction import dagger.Reusable import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import org.koitharu.kotatsu.core.db.MangaDatabase @@ -17,7 +15,6 @@ import org.koitharu.kotatsu.core.db.entity.toMangaTag import org.koitharu.kotatsu.core.db.entity.toMangaTags import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.core.ui.util.ReversibleHandle import org.koitharu.kotatsu.core.util.ext.mapItems import org.koitharu.kotatsu.history.data.HistoryEntity @@ -161,18 +158,6 @@ class HistoryRepository @Inject constructor( .distinctUntilChanged() } - fun observeShouldSkip(mangaFlow: Flow): Flow { - return mangaFlow - .distinctUntilChangedBy { it?.isNsfw } - .flatMapLatest { m -> - if (m != null) { - observeShouldSkip(m) - } else { - settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled } - } - } - } - private suspend fun recover(ids: Collection) { db.withTransaction { for (id in ids) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/DoubleMangaLoader.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/DoubleMangaLoader.kt new file mode 100644 index 000000000..5cf8325c1 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/DoubleMangaLoader.kt @@ -0,0 +1,66 @@ +package org.koitharu.kotatsu.local.domain + +import dagger.Reusable +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import org.koitharu.kotatsu.core.model.DoubleManga +import org.koitharu.kotatsu.core.model.isLocal +import org.koitharu.kotatsu.core.parser.MangaDataRepository +import org.koitharu.kotatsu.core.parser.MangaIntent +import org.koitharu.kotatsu.core.parser.MangaRepository +import org.koitharu.kotatsu.parsers.exception.NotFoundException +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import javax.inject.Inject + +@Reusable +class DoubleMangaLoader @Inject constructor( + private val mangaDataRepository: MangaDataRepository, + private val localMangaRepository: LocalMangaRepository, + private val mangaRepositoryFactory: MangaRepository.Factory, +) { + + suspend fun load(manga: Manga): DoubleManga = coroutineScope { + val remoteDeferred = async(Dispatchers.Default) { loadRemote(manga) } + val localDeferred = async(Dispatchers.Default) { loadLocal(manga) } + DoubleManga( + remoteManga = remoteDeferred.await(), + localManga = localDeferred.await(), + ) + } + + suspend fun load(mangaId: Long): DoubleManga { + val manga = mangaDataRepository.findMangaById(mangaId) ?: throwNFE() + return load(manga) + } + + suspend fun load(intent: MangaIntent): DoubleManga { + val manga = mangaDataRepository.resolveIntent(intent) ?: throwNFE() + return load(manga) + } + + private suspend fun loadLocal(manga: Manga): Result? { + return runCatchingCancellable { + if (manga.isLocal) { + localMangaRepository.getDetails(manga) + } else { + localMangaRepository.findSavedManga(manga)?.manga + } ?: return null + } + } + + private suspend fun loadRemote(manga: Manga): Result? { + return runCatchingCancellable { + val seed = if (manga.isLocal) { + localMangaRepository.getRemoteManga(manga) + } else { + manga + } ?: return null + val repository = mangaRepositoryFactory.create(seed.source) + repository.getDetails(seed) + } + } + + private fun throwNFE(): Nothing = throw NotFoundException("Cannot find manga", "") +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt index 9763882a7..6d3964978 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import kotlinx.coroutines.runInterruptible +import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.util.CompositeMutex import org.koitharu.kotatsu.core.util.ext.deleteAwait @@ -99,8 +100,11 @@ class LocalMangaRepository @Inject constructor( suspend fun deleteChapters(manga: Manga, ids: Set) { lockManga(manga.id) try { - LocalMangaUtil(manga).deleteChapters(ids) - localStorageChanges.emit(LocalManga(manga)) + val subject = if (manga.isLocal) manga else checkNotNull(findSavedManga(manga)) { + "Manga is not stored on local storage" + }.manga + LocalMangaUtil(subject).deleteChapters(ids) + localStorageChanges.emit(LocalManga(subject)) } finally { unlockManga(manga.id) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/ChaptersLoader.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/ChaptersLoader.kt index 334efab8a..6cff4c94f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/ChaptersLoader.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/ChaptersLoader.kt @@ -4,8 +4,8 @@ import android.util.LongSparseArray import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import org.koitharu.kotatsu.core.model.DoubleManga import org.koitharu.kotatsu.core.parser.MangaRepository -import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import javax.inject.Inject @@ -17,17 +17,27 @@ class ChaptersLoader @Inject constructor( private val mangaRepositoryFactory: MangaRepository.Factory, ) { - val chapters = LongSparseArray() + private val chapters = LongSparseArray() private val chapterPages = ChapterPages() private val mutex = Mutex() - suspend fun loadPrevNextChapter(manga: Manga, currentId: Long, isNext: Boolean) { + val size: Int + get() = chapters.size() + + suspend fun init(manga: DoubleManga) = mutex.withLock { + chapters.clear() + manga.chapters?.forEach { + chapters.put(it.id, it) + } + } + + suspend fun loadPrevNextChapter(manga: DoubleManga, currentId: Long, isNext: Boolean) { val chapters = manga.chapters ?: return val predicate: (MangaChapter) -> Boolean = { it.id == currentId } val index = if (isNext) chapters.indexOfFirst(predicate) else chapters.indexOfLast(predicate) if (index == -1) return val newChapter = chapters.getOrNull(if (isNext) index + 1 else index - 1) ?: return - val newPages = loadChapter(manga, newChapter.id) + val newPages = loadChapter(newChapter.id) mutex.withLock { if (chapterPages.chaptersSize > 1) { // trim pages @@ -47,14 +57,16 @@ class ChaptersLoader @Inject constructor( } } - suspend fun loadSingleChapter(manga: Manga, chapterId: Long) { - val pages = loadChapter(manga, chapterId) + suspend fun loadSingleChapter(chapterId: Long) { + val pages = loadChapter(chapterId) mutex.withLock { chapterPages.clear() chapterPages.addLast(chapterId, pages) } } + fun peekChapter(chapterId: Long): MangaChapter? = chapters[chapterId] + fun getPages(chapterId: Long): List { return chapterPages.subList(chapterId) } @@ -69,9 +81,9 @@ class ChaptersLoader @Inject constructor( fun snapshot() = chapterPages.toList() - private suspend fun loadChapter(manga: Manga, chapterId: Long): List { + private suspend fun loadChapter(chapterId: Long): List { val chapter = checkNotNull(chapters[chapterId]) { "Requested chapter not found" } - val repo = mangaRepositoryFactory.create(manga.source) + val repo = mangaRepositoryFactory.create(chapter.source) return repo.getPages(chapter).mapIndexed { index, page -> ReaderPage(page, index, chapterId) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt index 3fff9ab9a..6dfb02e93 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt @@ -183,7 +183,7 @@ class ReaderActivity : val state = viewModel.getCurrentState() ?: return false PagesThumbnailsSheet.show( supportFragmentManager, - viewModel.manga ?: return false, + viewModel.manga?.any ?: return false, state.chapterId, state.page, ) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt index f092a5c06..029bf76b3 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt @@ -1,7 +1,6 @@ package org.koitharu.kotatsu.reader.ui import android.net.Uri -import android.util.LongSparseArray import androidx.activity.result.ActivityResultLauncher import androidx.annotation.AnyThread import androidx.annotation.MainThread @@ -16,6 +15,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine @@ -33,6 +33,7 @@ import kotlinx.coroutines.plus 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.DoubleManga import org.koitharu.kotatsu.core.os.ShortcutsUpdater import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaIntent @@ -50,12 +51,11 @@ import org.koitharu.kotatsu.core.util.ext.processLifecycleScope import org.koitharu.kotatsu.core.util.ext.requireValue import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.PROGRESS_NONE +import org.koitharu.kotatsu.local.domain.DoubleMangaLoader import org.koitharu.kotatsu.parsers.exception.NotFoundException import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.util.runCatchingCancellable -import org.koitharu.kotatsu.reader.data.filterChapters import org.koitharu.kotatsu.reader.domain.ChaptersLoader import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.ui.config.ReaderSettings @@ -79,6 +79,7 @@ class ReaderViewModel @Inject constructor( private val pageLoader: PageLoader, private val chaptersLoader: ChaptersLoader, private val shortcutsUpdater: ShortcutsUpdater, + private val mangaLoader: DoubleMangaLoader, ) : BaseViewModel() { private val intent = MangaIntent(savedStateHandle) @@ -90,9 +91,9 @@ class ReaderViewModel @Inject constructor( private var bookmarkJob: Job? = null private var stateChangeJob: Job? = null private val currentState = MutableStateFlow(savedStateHandle[ReaderActivity.EXTRA_STATE]) - private val mangaData = MutableStateFlow(intent.manga) - private val chapters: LongSparseArray - get() = chaptersLoader.chapters + private val mangaData = MutableStateFlow(intent.manga?.let { DoubleManga(it) }) + private val mangaFlow: Flow + get() = mangaData.map { it?.any } val readerMode = MutableLiveData() val onPageSaved = SingleLiveEvent() @@ -100,7 +101,7 @@ class ReaderViewModel @Inject constructor( val uiState = MutableLiveData(null) val content = MutableLiveData(ReaderContent(emptyList(), null)) - val manga: Manga? + val manga: DoubleManga? get() = mangaData.value val readerAnimation = settings.observeAsLiveData( @@ -124,13 +125,13 @@ class ReaderViewModel @Inject constructor( val readerSettings = ReaderSettings( parentScope = viewModelScope, settings = settings, - colorFilterFlow = mangaData.flatMapLatest { + colorFilterFlow = mangaFlow.flatMapLatest { if (it == null) flowOf(null) else dataRepository.observeColorFilter(it.id) }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null), ) val isScreenshotsBlockEnabled = combine( - mangaData, + mangaFlow, settings.observeAsFlow(AppSettings.KEY_SCREENSHOTS_POLICY) { screenshotsPolicy }, ) { manga, policy -> policy == ScreenshotsPolicy.BLOCK_ALL || @@ -138,7 +139,7 @@ class ReaderViewModel @Inject constructor( }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false) val isBookmarkAdded: LiveData = currentState.flatMapLatest { state -> - val manga = mangaData.value + val manga = mangaData.value?.any if (state == null || manga == null) { flowOf(false) } else { @@ -154,7 +155,7 @@ class ReaderViewModel @Inject constructor( if (key == AppSettings.KEY_READER_SLIDER) notifyStateChanged() }.launchIn(viewModelScope + Dispatchers.Default) launchJob(Dispatchers.Default) { - val mangaId = mangaData.filterNotNull().first().id + val mangaId = mangaFlow.filterNotNull().first().id shortcutsUpdater.notifyMangaOpened(mangaId) } } @@ -166,7 +167,7 @@ class ReaderViewModel @Inject constructor( fun switchMode(newMode: ReaderMode) { launchJob { - val manga = checkNotNull(mangaData.value) + val manga = checkNotNull(mangaData.value?.any) dataRepository.saveReaderMode( manga = manga, mode = newMode, @@ -189,7 +190,7 @@ class ReaderViewModel @Inject constructor( } val readerState = state ?: currentState.value ?: return historyRepository.saveStateAsync( - manga = mangaData.value ?: return, + manga = mangaData.value?.any ?: return, state = readerState, percent = computePercent(readerState.chapterId, readerState.page), ) @@ -242,7 +243,7 @@ class ReaderViewModel @Inject constructor( loadingJob = launchLoadingJob(Dispatchers.Default) { prevJob?.cancelAndJoin() content.postValue(ReaderContent(emptyList(), null)) - chaptersLoader.loadSingleChapter(mangaData.requireValue(), id) + chaptersLoader.loadSingleChapter(id) content.postValue(ReaderContent(chaptersLoader.snapshot(), ReaderState(id, page, 0))) } } @@ -285,7 +286,7 @@ class ReaderViewModel @Inject constructor( val state = checkNotNull(currentState.value) val page = checkNotNull(getCurrentPage()) { "Page not found" } val bookmark = Bookmark( - manga = checkNotNull(mangaData.value), + manga = checkNotNull(mangaData.value?.any), pageId = page.id, chapterId = state.chapterId, page = state.page, @@ -305,7 +306,7 @@ class ReaderViewModel @Inject constructor( } bookmarkJob = launchJob { loadingJob?.join() - val manga = checkNotNull(mangaData.value) + val manga = checkNotNull(mangaData.value?.any) val page = checkNotNull(getCurrentPage()) { "Page not found" } bookmarksRepository.removeBookmark(manga.id, page.id) onShowToast.call(R.string.bookmark_removed) @@ -314,32 +315,31 @@ class ReaderViewModel @Inject constructor( private fun loadImpl() { loadingJob = launchLoadingJob(Dispatchers.Default) { - var manga = dataRepository.resolveIntent(intent) ?: throw NotFoundException("Cannot find manga", "") + var manga = + DoubleManga(dataRepository.resolveIntent(intent) ?: throw NotFoundException("Cannot find manga", "")) mangaData.value = manga - val repo = mangaRepositoryFactory.create(manga.source) - manga = repo.getDetails(manga) - manga.chapters?.forEach { - chapters.put(it.id, it) - } + manga = mangaLoader.load(intent) + chaptersLoader.init(manga) // determine mode - val mode = detectReaderMode(manga, repo) + val singleManga = manga.requireAny() + val mode = detectReaderMode(singleManga) // obtain state if (currentState.value == null) { - currentState.value = historyRepository.getOne(manga)?.let { + currentState.value = historyRepository.getOne(singleManga)?.let { ReaderState(it) - } ?: ReaderState(manga, preselectedBranch) + } ?: ReaderState(singleManga, preselectedBranch) } - val branch = chapters[currentState.value?.chapterId ?: 0L]?.branch + val branch = chaptersLoader.peekChapter(currentState.value?.chapterId ?: 0L)?.branch mangaData.value = manga.filterChapters(branch) readerMode.emitValue(mode) - chaptersLoader.loadSingleChapter(manga, requireNotNull(currentState.value).chapterId) + chaptersLoader.loadSingleChapter(requireNotNull(currentState.value).chapterId) // save state if (!isIncognito) { currentState.value?.let { val percent = computePercent(it.chapterId, it.page) - historyRepository.addOrUpdate(manga, it.chapterId, it.page, it.scroll, percent) + historyRepository.addOrUpdate(singleManga, it.chapterId, it.page, it.scroll, percent) } } notifyStateChanged() @@ -367,15 +367,16 @@ class ReaderViewModel @Inject constructor( } } - private suspend fun detectReaderMode(manga: Manga, repo: MangaRepository): ReaderMode { + private suspend fun detectReaderMode(manga: Manga): ReaderMode { dataRepository.getReaderMode(manga.id)?.let { return it } val defaultMode = settings.defaultReaderMode if (!settings.isReaderModeDetectionEnabled || defaultMode == ReaderMode.WEBTOON) { return defaultMode } - val chapter = currentState.value?.chapterId?.let(chapters::get) + val chapter = currentState.value?.chapterId?.let { chaptersLoader.peekChapter(it) } ?: manga.chapters?.randomOrNull() ?: error("There are no chapters in this manga") + val repo = mangaRepositoryFactory.create(manga.source) val pages = repo.getPages(chapter) return runCatchingCancellable { val isWebtoon = dataRepository.determineMangaIsWebtoon(repo, pages) @@ -390,12 +391,12 @@ class ReaderViewModel @Inject constructor( @WorkerThread private fun notifyStateChanged() { val state = getCurrentState() - val chapter = state?.chapterId?.let(chapters::get) + val chapter = state?.chapterId?.let { chaptersLoader.peekChapter(it) } val newState = ReaderUiState( - mangaName = manga?.title, + mangaName = manga?.any?.title, chapterName = chapter?.name, chapterNumber = chapter?.number ?: 0, - chaptersTotal = manga?.getChapters(chapter?.branch)?.size ?: 0, + chaptersTotal = manga?.any?.getChapters(chapter?.branch)?.size ?: 0, totalPages = if (chapter != null) chaptersLoader.getPagesCount(chapter.id) else 0, currentPage = state?.page ?: 0, isSliderEnabled = settings.isReaderSliderEnabled, @@ -405,8 +406,8 @@ class ReaderViewModel @Inject constructor( } private fun computePercent(chapterId: Long, pageIndex: Int): Float { - val branch = chapters[chapterId]?.branch - val chapters = manga?.getChapters(branch) ?: return PROGRESS_NONE + val branch = chaptersLoader.peekChapter(chapterId)?.branch + val chapters = manga?.any?.getChapters(branch) ?: return PROGRESS_NONE val chaptersCount = chapters.size val chapterIndex = chapters.indexOfFirst { x -> x.id == chapterId } val pagesCount = chaptersLoader.getPagesCount(chapterId) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigBottomSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigBottomSheet.kt index d83552079..714819f51 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigBottomSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigBottomSheet.kt @@ -108,7 +108,7 @@ class ReaderConfigBottomSheet : R.id.button_color_filter -> { val page = viewModel.getCurrentPage() ?: return - val manga = viewModel.manga ?: return + val manga = viewModel.manga?.any ?: return startActivity(ColorFilterConfigActivity.newIntent(v.context, manga, page)) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsViewModel.kt index bf9e86070..934245a5c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsViewModel.kt @@ -12,8 +12,8 @@ import org.koitharu.kotatsu.core.util.ext.emitValue import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.LoadingFooter +import org.koitharu.kotatsu.local.domain.DoubleMangaLoader import org.koitharu.kotatsu.parsers.util.SuspendLazy -import org.koitharu.kotatsu.reader.data.filterChapters import org.koitharu.kotatsu.reader.domain.ChaptersLoader import javax.inject.Inject @@ -22,6 +22,7 @@ class PagesThumbnailsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, mangaRepositoryFactory: MangaRepository.Factory, private val chaptersLoader: ChaptersLoader, + private val mangaLoader: DoubleMangaLoader, ) : BaseViewModel() { private val currentPageIndex: Int = savedStateHandle[PagesThumbnailsSheet.ARG_CURRENT_PAGE] ?: -1 @@ -30,13 +31,9 @@ class PagesThumbnailsViewModel @Inject constructor( private val repository = mangaRepositoryFactory.create(manga.source) private val mangaDetails = SuspendLazy { - repository.getDetails(manga).let { - chaptersLoader.chapters.clear() + mangaLoader.load(manga).let { val b = manga.chapters?.find { ch -> ch.id == initialChapterId }?.branch branch.emitValue(b) - it.getChapters(b)?.forEach { ch -> - chaptersLoader.chapters.put(ch.id, ch) - } it.filterChapters(b) } } @@ -50,7 +47,8 @@ class PagesThumbnailsViewModel @Inject constructor( init { loadingJob = launchJob(Dispatchers.Default) { - chaptersLoader.loadSingleChapter(mangaDetails.get(), initialChapterId) + chaptersLoader.init(mangaDetails.get()) + chaptersLoader.loadSingleChapter(initialChapterId) updateList() } } @@ -80,14 +78,14 @@ class PagesThumbnailsViewModel @Inject constructor( val mangaChapters = mangaDetails.tryGet().getOrNull()?.chapters.orEmpty() val hasPrevChapter = snapshot.firstOrNull()?.chapterId != mangaChapters.firstOrNull()?.id val hasNextChapter = snapshot.lastOrNull()?.chapterId != mangaChapters.lastOrNull()?.id - val pages = buildList(snapshot.size + chaptersLoader.chapters.size() + 2) { + val pages = buildList(snapshot.size + chaptersLoader.size + 2) { if (hasPrevChapter) { add(LoadingFooter(-1)) } var previousChapterId = 0L for (page in snapshot) { if (page.chapterId != previousChapterId) { - chaptersLoader.chapters[page.chapterId]?.let { + chaptersLoader.peekChapter(page.chapterId)?.let { add(ListHeader(it.name, 0, null)) } previousChapterId = page.chapterId diff --git a/app/src/main/res/raw/tags_redlist b/app/src/main/res/raw/tags_redlist index ecc5d9f11..537f2ecb8 100644 --- a/app/src/main/res/raw/tags_redlist +++ b/app/src/main/res/raw/tags_redlist @@ -18,3 +18,6 @@ scat тентакли футанари инцест +boys' love +girls' love +bdsm