From 2ef1195a90fcbe20a2ed759f34bd4d278dd27842 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Mon, 16 Dec 2024 20:43:37 +0700 Subject: [PATCH] refactor(manga): Slowly using flow --- .../ui/manga/MangaDetailsController.kt | 12 +- .../ui/manga/MangaDetailsPresenter.kt | 244 +++++++++--------- .../tachiyomi/domain/manga/models/Manga.kt | 23 ++ 3 files changed, 145 insertions(+), 134 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt index 01a980fa31..f5dfe23e52 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt @@ -194,7 +194,7 @@ class MangaDetailsController : } } - private val manga: Manga? get() = if (presenter.isMangaLateInitInitialized()) presenter.manga else null + private val manga: Manga? get() = presenter.currentManga.value private var colorAnimator: ValueAnimator? = null override val presenter: MangaDetailsPresenter private var coverColor: Int? = null @@ -674,7 +674,7 @@ class MangaDetailsController : returningFromReader = false runBlocking { val itemAnimator = binding.recycler.itemAnimator - val chapters = withTimeoutOrNull(1000) { presenter.getChaptersNow() } ?: return@runBlocking + val chapters = withTimeoutOrNull(1000) { presenter.setAndGetChapters() } ?: return@runBlocking binding.recycler.itemAnimator = null tabletAdapter?.notifyItemChanged(0) adapter?.setChapters(chapters) @@ -821,15 +821,15 @@ class MangaDetailsController : updateMenuVisibility(activityBinding?.toolbar?.menu) } - fun updateChapters(chapters: List) { + fun updateChapters(fetchFromSource: Boolean = false) { view ?: return binding.swipeRefresh.isRefreshing = presenter.isLoading - if (presenter.chapters.isEmpty() && fromCatalogue && !presenter.hasRequested) { + if (presenter.chapters.isEmpty() && fromCatalogue && !presenter.hasRequested && fetchFromSource) { launchUI { binding.swipeRefresh.isRefreshing = true } - presenter.fetchChaptersFromSource() + presenter.refreshChapters() } tabletAdapter?.notifyItemChanged(0) - adapter?.setChapters(chapters) + adapter?.setChapters(presenter.chapters) addMangaHeader() colorToolbar(binding.recycler.canScrollVertically(-1)) updateMenuVisibility(activityBinding?.toolbar?.menu) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt index 67ed320378..f49dfd10f7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt @@ -34,6 +34,7 @@ import eu.kanade.tachiyomi.data.track.EnhancedTrackService import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.domain.manga.models.Manga +import eu.kanade.tachiyomi.network.HttpException import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.Source @@ -76,11 +77,15 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext @@ -127,11 +132,15 @@ class MangaDetailsPresenter( private val networkPreferences: NetworkPreferences by injectLazy() -// private val currentMangaInternal: MutableStateFlow = MutableStateFlow(null) -// val currentManga get() = currentMangaInternal.asStateFlow() + private val currentMangaInternal = MutableStateFlow(null) + val currentManga = currentMangaInternal.asStateFlow() - lateinit var manga: Manga - fun isMangaLateInitInitialized() = ::manga.isInitialized + /** + * Unsafe, call only after currentManga is no longer null + */ + var manga: Manga + get() = currentManga.value!! + set(value) { currentMangaInternal.value = value } private val customMangaManager: CustomMangaManager by injectLazy() private val mangaShortcutManager: MangaShortcutManager by injectLazy() @@ -151,8 +160,12 @@ class MangaDetailsPresenter( var trackList: List = emptyList() - var chapters: List = emptyList() - private set + private val currentChaptersInternal = MutableStateFlow>(emptyList()) + val currentChapters = currentChaptersInternal.asStateFlow() + + var chapters: List + get() = currentChapters.value + private set(value) { currentChaptersInternal.value = value } var allChapters: List = emptyList() private set @@ -186,7 +199,7 @@ class MangaDetailsPresenter( val controller = view ?: return isLockedFromSearch = controller.shouldLockIfNeeded && SecureActivityDelegate.shouldBeLocked() - if (!::manga.isInitialized) runBlocking { refreshMangaFromDb() } + if (currentManga.value == null) runBlocking { refreshMangaFromDb() } syncData() presenterScope.launchUI { @@ -204,6 +217,22 @@ class MangaDetailsPresenter( presenterScope.launchIO { downloadManager.queueState.collectLatest(::onQueueUpdate) } + presenterScope.launchUI { + currentManga.collectLatest { + if (it == null) return@collectLatest + } + } + presenterScope.launchIO { + currentChapters.collectLatest { chapters -> + allChapters = if (!isScanlatorFiltered()) chapters else getChapter.awaitAll(mangaId, false).map { it.toModel() } + + allChapterScanlators = allChapters.mapNotNull { it.chapter.scanlator }.toSet() + + withUIContext { + controller.updateChapters(allChapters.isEmpty()) + } + } + } runBlocking { tracks = getTrack.awaitAllByMangaId(mangaId) @@ -229,8 +258,7 @@ class MangaDetailsPresenter( controller.updateHeader() refreshAll() } else { - runBlocking { getChapters() } - controller.updateChapters(this.chapters) + runBlocking { chapters = getChapters() } getHistory() } @@ -243,16 +271,14 @@ class MangaDetailsPresenter( fun fetchChapters(andTracking: Boolean = true) { presenterScope.launch { - getChapters() + setCurrentChapters(getChapters()) if (andTracking) fetchTracks() - withContext(Dispatchers.Main) { view?.updateChapters(chapters) } getHistory() } } fun setCurrentManga(manga: Manga?) { -// currentMangaInternal.update { manga } - this.manga = manga!! + currentMangaInternal.update { manga } } // TODO: Use flow to "sync" data instead @@ -264,20 +290,18 @@ class MangaDetailsPresenter( } } - suspend fun getChaptersNow(): List { - getChapters() - return chapters + // TODO: Use getChapter.subscribe() flow instead + suspend fun setAndGetChapters(): List { + return currentChaptersInternal.updateAndGet { getChapters() } } - private suspend fun getChapters(queue: List = downloadManager.queueState.value) { - val chapters = getChapter.awaitAll(mangaId, isScanlatorFiltered()).map { it.toModel() } - allChapters = if (!isScanlatorFiltered()) chapters else getChapter.awaitAll(mangaId, false).map { it.toModel() } + // TODO: Use getChapter.subscribe() flow instead + private fun setCurrentChapters(chapters: List) { + currentChaptersInternal.update { chapters } + } - // Find downloaded chapters - setDownloadedChapters(chapters, queue) - allChapterScanlators = allChapters.mapNotNull { it.chapter.scanlator }.toSet() - - this.chapters = applyChapterFilters(chapters) + private suspend fun getChapters(): List { + return getChapter.awaitAll(mangaId, isScanlatorFiltered()).map { it.toModel() } } private fun getHistory() { @@ -394,7 +418,7 @@ class MangaDetailsPresenter( download = null } - view?.updateChapters(this.chapters) + view?.updateChapters() downloadManager.deleteChapters(listOf(chapter), manga, source, true) } @@ -412,7 +436,7 @@ class MangaDetailsPresenter( } } - if (update) view?.updateChapters(this.chapters) + if (update) view?.updateChapters() if (isEverything) { downloadManager.deleteManga(manga, source) @@ -432,126 +456,93 @@ class MangaDetailsPresenter( if (view?.isNotOnline() == true && !manga.isLocal()) return presenterScope.launch { isLoading = true - var mangaError: java.lang.Exception? = null - var chapterError: java.lang.Exception? = null - val chapters = async(Dispatchers.IO) { - try { - source.getChapterList(manga.copy()) - } catch (e: Exception) { - chapterError = e - emptyList() - } - } - val nManga = async(Dispatchers.IO) { - try { - source.getMangaDetails(manga.copy()) - } catch (e: java.lang.Exception) { - mangaError = e - null - } - } + val tasks = listOf( + async { fetchMangaFromSource() }, + async { fetchChaptersFromSource() }, + ) + tasks.awaitAll() + isLoading = false + } + } - val networkManga = nManga.await() - if (networkManga != null) { - manga.prepareCoverUpdate(coverCache, networkManga, false) - manga.copyFrom(networkManga) - manga.initialized = true + private suspend fun fetchMangaFromSource() { + try { + val manga = manga.copy() + val networkManga = source.getMangaDetails(manga) - updateManga.await(manga.toMangaUpdate()) + manga.prepareCoverUpdate(coverCache, networkManga, false) + manga.copyFrom(networkManga) + manga.initialized = true - launchIO { - val request = - ImageRequest.Builder(preferences.context).data(manga.cover()) - .memoryCachePolicy(CachePolicy.DISABLED) - .diskCachePolicy(CachePolicy.WRITE_ONLY) - .build() + updateManga.await(manga.toMangaUpdate()) - if (preferences.context.imageLoader.execute(request) is SuccessResult) { - withContext(Dispatchers.Main) { - view?.setPaletteColor() - } + setCurrentManga(manga) + + presenterScope.launchNonCancellableIO { + val request = + ImageRequest.Builder(preferences.context).data(manga.cover()) + .memoryCachePolicy(CachePolicy.DISABLED) + .diskCachePolicy(CachePolicy.WRITE_ONLY) + .build() + + if (preferences.context.imageLoader.execute(request) is SuccessResult) { + withContext(Dispatchers.Main) { + view?.setPaletteColor() } } } - val finChapters = chapters.await() - if (finChapters.isNotEmpty()) { - val newChapters = withIOContext { syncChaptersWithSource(finChapters, manga, source) } - if (newChapters.first.isNotEmpty()) { + } catch (e: Exception) { + if (e is HttpException && e.code == 103) return + + withUIContext { + view?.showError(trimException(e)) + } + } + } + + private suspend fun fetchChaptersFromSource(manualFetch: Boolean = true) { + try { + withIOContext { + val chapters = source.getChapterList(manga.copy()) + val (added, removed) = syncChaptersWithSource(chapters, manga, source) + if (added.isNotEmpty() && manualFetch) { if (manga.shouldDownloadNewChapters(preferences)) { - downloadChapters( - newChapters.first.sortedBy { it.chapter_number } - .map { it.toModel() }, - ) + downloadChapters(added.sortedBy { it.chapter_number }.map { it.toModel() }) + } + withUIContext { + view?.view?.context?.let { mangaShortcutManager.updateShortcuts(it) } } - view?.view?.context?.let { mangaShortcutManager.updateShortcuts(it) } } - if (newChapters.second.isNotEmpty()) { - val removedChaptersId = newChapters.second.map { it.id } + if (removed.isNotEmpty() && manualFetch) { + val removedChaptersId = removed.map { it.id } val removedChapters = this@MangaDetailsPresenter.chapters.filter { it.id in removedChaptersId && it.isDownloaded } if (removedChapters.isNotEmpty()) { - withContext(Dispatchers.Main) { - view?.showChaptersRemovedPopup( - removedChapters, - ) + withUIContext { + view?.showChaptersRemovedPopup(removedChapters) } } } - getChapters() + setCurrentChapters(getChapters()) + getHistory() } - isLoading = false - if (chapterError == null) { - withContext(Dispatchers.Main) { - view?.updateChapters(this@MangaDetailsPresenter.chapters) - } + } catch (e: Exception) { + withUIContext { + view?.showError(trimException(e)) } - if (chapterError != null) { - withContext(Dispatchers.Main) { - view?.showError( - trimException(chapterError!!), - ) - } - return@launch - } else if (mangaError != null) { - withContext(Dispatchers.Main) { - view?.showError( - trimException(mangaError!!), - ) - } - } - getHistory() } } /** * Requests an updated list of chapters from the source. */ - fun fetchChaptersFromSource() { - hasRequested = true - isLoading = true - - presenterScope.launch(Dispatchers.IO) { - val chapters = try { - source.getChapterList(manga.copy()) - } catch (e: Exception) { - withContext(Dispatchers.Main) { view?.showError(trimException(e)) } - return@launch - } + fun refreshChapters() { + presenterScope.launchUI { + hasRequested = true + isLoading = true + fetchChaptersFromSource(true) isLoading = false - try { - syncChaptersWithSource(chapters, manga, source) - - getChapters() - withContext(Dispatchers.Main) { - view?.updateChapters(this@MangaDetailsPresenter.chapters) - } - getHistory() - } catch (e: java.lang.Exception) { - withContext(Dispatchers.Main) { - view?.showError(trimException(e)) - } - } } } @@ -579,8 +570,7 @@ class MangaDetailsPresenter( it.toProgressUpdate() } updateChapter.awaitAll(updates) - getChapters() - withContext(Dispatchers.Main) { view?.updateChapters(chapters) } + setCurrentChapters(getChapters()) } } @@ -609,8 +599,7 @@ class MangaDetailsPresenter( if (read && deleteNow && preferences.removeAfterMarkedAsRead().get()) { deleteChapters(selectedChapters, false) } - getChapters() - withContext(Dispatchers.Main) { view?.updateChapters(chapters) } + setCurrentChapters(getChapters()) if (read && deleteNow) { val latestReadChapter = selectedChapters.maxByOrNull { it.chapter_number.toInt() }?.chapter updateTrackChapterMarkedAsRead(preferences, latestReadChapter, manga.id) { @@ -741,8 +730,7 @@ class MangaDetailsPresenter( private suspend fun asyncUpdateMangaAndChapters(justChapters: Boolean = false) { if (!justChapters) updateManga.await(MangaUpdate(manga.id!!, chapterFlags = manga.chapter_flags)) - getChapters() - withUIContext { view?.updateChapters(chapters) } + setCurrentChapters(getChapters()) } private fun isScanlatorFiltered() = manga.filtered_scanlators?.isNotEmpty() == true @@ -1158,9 +1146,9 @@ class MangaDetailsPresenter( } private suspend fun onQueueUpdate(queue: List) = withIOContext { - getChapters(queue) + setDownloadedChapters(chapters, queue) withUIContext { - view?.updateChapters(chapters) + view?.updateChapters() } } diff --git a/domain/src/commonMain/kotlin/eu/kanade/tachiyomi/domain/manga/models/Manga.kt b/domain/src/commonMain/kotlin/eu/kanade/tachiyomi/domain/manga/models/Manga.kt index da8d99f67b..58e8edcc13 100644 --- a/domain/src/commonMain/kotlin/eu/kanade/tachiyomi/domain/manga/models/Manga.kt +++ b/domain/src/commonMain/kotlin/eu/kanade/tachiyomi/domain/manga/models/Manga.kt @@ -35,6 +35,29 @@ interface Manga : SManga { var cover_last_modified: Long + override fun copy(): Manga { + return (super.copy() as Manga).also { + it.id = this.id + it.source = this.source + it.favorite = this.favorite + it.last_update = this.last_update + it.date_added = this.date_added + it.viewer_flags = this.viewer_flags + it.chapter_flags = this.chapter_flags + it.hide_title = this.hide_title + it.filtered_scanlators = this.filtered_scanlators + + it.ogTitle = this.ogTitle + it.ogAuthor = this.ogAuthor + it.ogArtist = this.ogArtist + it.ogDesc = this.ogDesc + it.ogGenre = this.ogGenre + it.ogStatus = this.ogStatus + + it.cover_last_modified = this.cover_last_modified + } + } + @Deprecated("Use ogTitle directly instead", ReplaceWith("ogTitle")) val originalTitle: String get() = ogTitle