diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt index fb560da49c..29aedbd6e0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt @@ -182,6 +182,37 @@ var Manga.vibrantCoverColor: Int? id?.let { MangaCoverMetadata.setVibrantColor(it, value) } } +fun Manga.copyDomain(): Manga = MangaImpl().also { other -> + other.url = this.url + other.title = this.title + other.artist = this.artist + other.author = this.author + other.description = this.description + other.genre = this.genre + other.status = this.status + other.thumbnail_url = this.thumbnail_url + other.initialized = this.initialized + + other.id = this.id + other.source = this.source + other.favorite = this.favorite + other.last_update = this.last_update + other.date_added = this.date_added + other.viewer_flags = this.viewer_flags + other.chapter_flags = this.chapter_flags + other.hide_title = this.hide_title + other.filtered_scanlators = this.filtered_scanlators + + other.ogTitle = this.ogTitle + other.ogAuthor = this.ogAuthor + other.ogArtist = this.ogArtist + other.ogDesc = this.ogDesc + other.ogGenre = this.ogGenre + other.ogStatus = this.ogStatus + + other.cover_last_modified = this.cover_last_modified +} + fun Manga.Companion.create(source: Long) = MangaImpl().apply { this.source = source } 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..b382f47097 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,11 @@ class MangaDetailsController : updateMenuVisibility(activityBinding?.toolbar?.menu) } - fun updateChapters(chapters: List) { + fun updateChapters() { view ?: return binding.swipeRefresh.isRefreshing = presenter.isLoading - if (presenter.chapters.isEmpty() && fromCatalogue && !presenter.hasRequested) { - launchUI { binding.swipeRefresh.isRefreshing = true } - presenter.fetchChaptersFromSource() - } + adapter?.setChapters(presenter.chapters) tabletAdapter?.notifyItemChanged(0) - adapter?.setChapters(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..a2630bcb9f 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 @@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.data.database.models.History import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.bookmarkedFilter import eu.kanade.tachiyomi.data.database.models.chapterOrder +import eu.kanade.tachiyomi.data.database.models.copyDomain import eu.kanade.tachiyomi.data.database.models.downloadedFilter import eu.kanade.tachiyomi.data.database.models.prepareCoverUpdate import eu.kanade.tachiyomi.data.database.models.readFilter @@ -34,6 +35,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 +78,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 +133,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 +161,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 +200,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 +218,26 @@ class MangaDetailsPresenter( presenterScope.launchIO { downloadManager.queueState.collectLatest(::onQueueUpdate) } + presenterScope.launchUI { + currentManga.collectLatest { + if (it == null) return@collectLatest + + controller.updateHeader() + } + } + presenterScope.launchIO { + currentChapters.collectLatest { chapters -> + allChapters = if (!isScanlatorFiltered()) chapters else getChapter.awaitAll(mangaId, false).map { it.toModel() } + + allChapterScanlators = allChapters.mapNotNull { it.chapter.scanlator }.toSet() + + getHistory() + + withUIContext { + controller.updateChapters() + } + } + } runBlocking { tracks = getTrack.awaitAllByMangaId(mangaId) @@ -216,25 +250,24 @@ class MangaDetailsPresenter( fun onCreateLate() { val controller = view ?: return + isLoading = true + controller.setRefresh(true) // FIXME: Use progress indicator instead + LibraryUpdateJob.updateFlow .filter { it == mangaId } .onEach { onUpdateManga() } .launchIn(presenterScope) - if (manga.isLocal()) { - refreshAll() - } else if (!manga.initialized) { - isLoading = true - controller.setRefresh(true) - controller.updateHeader() - refreshAll() - } else { - runBlocking { getChapters() } - controller.updateChapters(this.chapters) - getHistory() - } + val updateMangaNeeded = !manga.initialized + val updateChaptersNeeded = runBlocking { setAndGetChapters() }.isEmpty() presenterScope.launch { + val tasks = listOf( + async { if (updateMangaNeeded) fetchMangaFromSource() }, + async { if (updateChaptersNeeded) fetchChaptersFromSource(false) }, + ) + tasks.awaitAll() + setTrackItems() } @@ -243,16 +276,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 +295,18 @@ class MangaDetailsPresenter( } } - suspend fun getChaptersNow(): List { - getChapters() - return chapters + // TODO: Use getChapter.subscribe() flow instead + suspend fun setAndGetChapters(): List { + return currentChaptersInternal.updateAndGet { getChapters().applyChapterFilters() } } - 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.applyChapterFilters() } + } - // 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() { @@ -291,14 +320,17 @@ class MangaDetailsPresenter( * * @param chapters the list of chapter from the database. */ - private fun setDownloadedChapters(chapters: List, queue: List) { - for (chapter in chapters) { - if (downloadManager.isChapterDownloaded(chapter, manga)) { - chapter.status = Download.State.DOWNLOADED - } else if (queue.isNotEmpty()) { - chapter.status = queue.find { it.chapter.id == chapter.id } - ?.status ?: Download.State.default + private fun setDownloadedChapters(queue: List) { + currentChaptersInternal.update { chapters -> + for (chapter in chapters) { + if (downloadManager.isChapterDownloaded(chapter, manga)) { + chapter.status = Download.State.DOWNLOADED + } else if (queue.isNotEmpty()) { + chapter.status = queue.find { it.chapter.id == chapter.id } + ?.status ?: Download.State.default + } } + chapters } } @@ -332,12 +364,12 @@ class MangaDetailsPresenter( * @param chapterList the list of chapters from the database * @return an observable of the list of chapters filtered and sorted. */ - private fun applyChapterFilters(chapterList: List): List { + private fun List.applyChapterFilters(): List { if (isLockedFromSearch) { - return chapterList + return this } - getScrollType(chapterList) - return chapterSort.getChaptersSorted(chapterList) + getScrollType(this) + return chapterSort.getChaptersSorted(this) } fun getChapterUrl(chapter: Chapter): String? { @@ -394,7 +426,7 @@ class MangaDetailsPresenter( download = null } - view?.updateChapters(this.chapters) + view?.updateChapters() downloadManager.deleteChapters(listOf(chapter), manga, source, true) } @@ -412,7 +444,7 @@ class MangaDetailsPresenter( } } - if (update) view?.updateChapters(this.chapters) + if (update) view?.updateChapters() if (isEverything) { downloadManager.deleteManga(manga, source) @@ -432,125 +464,80 @@ 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.copyDomain() + val networkManga = source.getMangaDetails(manga.copy()) - 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() - } - isLoading = false - if (chapterError == null) { - withContext(Dispatchers.Main) { - view?.updateChapters(this@MangaDetailsPresenter.chapters) - } - } - 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 - } - isLoading = false - try { - syncChaptersWithSource(chapters, manga, source) - - getChapters() - withContext(Dispatchers.Main) { - view?.updateChapters(this@MangaDetailsPresenter.chapters) - } + setCurrentChapters(getChapters()) getHistory() - } catch (e: java.lang.Exception) { - withContext(Dispatchers.Main) { - view?.showError(trimException(e)) - } + } + } catch (e: Exception) { + withUIContext { + view?.showError(trimException(e)) } } } @@ -579,8 +566,7 @@ class MangaDetailsPresenter( it.toProgressUpdate() } updateChapter.awaitAll(updates) - getChapters() - withContext(Dispatchers.Main) { view?.updateChapters(chapters) } + setCurrentChapters(getChapters()) } } @@ -609,8 +595,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 +726,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 @@ -811,7 +795,7 @@ class MangaDetailsPresenter( withUIContext { view?.shareManga(uri.uri.toFile()) } - } catch (_: java.lang.Exception) { + } catch (_: Exception) { } } } @@ -1153,15 +1137,15 @@ class MangaDetailsPresenter( override fun onStatusChange(download: Download) { super.onStatusChange(download) - chapters.find { it.id == download.chapter.id }?.status = download.status + currentChaptersInternal.update { chapters -> + chapters.find { it.id == download.chapter.id }?.status = download.status + chapters + } onPageProgressUpdate(download) } private suspend fun onQueueUpdate(queue: List) = withIOContext { - getChapters(queue) - withUIContext { - view?.updateChapters(chapters) - } + setDownloadedChapters(queue) } override fun onQueueUpdate(download: Download) { @@ -1173,7 +1157,10 @@ class MangaDetailsPresenter( } override fun onPageProgressUpdate(download: Download) { - chapters.find { it.id == download.chapter.id }?.download = download + currentChaptersInternal.update { chapters -> + chapters.find { it.id == download.chapter.id }?.download = download + chapters + } view?.updateChapterDownload(download) }