refactor(manga): Slowly using flow attempt 2

This commit is contained in:
Ahmad Ansori Palembani 2024-12-17 07:33:24 +07:00
parent f8d74a6b2f
commit f81be429df
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
3 changed files with 183 additions and 169 deletions

View file

@ -182,6 +182,37 @@ var Manga.vibrantCoverColor: Int?
id?.let { MangaCoverMetadata.setVibrantColor(it, value) } 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 { fun Manga.Companion.create(source: Long) = MangaImpl().apply {
this.source = source this.source = source
} }

View file

@ -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 private var colorAnimator: ValueAnimator? = null
override val presenter: MangaDetailsPresenter override val presenter: MangaDetailsPresenter
private var coverColor: Int? = null private var coverColor: Int? = null
@ -674,7 +674,7 @@ class MangaDetailsController :
returningFromReader = false returningFromReader = false
runBlocking { runBlocking {
val itemAnimator = binding.recycler.itemAnimator 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 binding.recycler.itemAnimator = null
tabletAdapter?.notifyItemChanged(0) tabletAdapter?.notifyItemChanged(0)
adapter?.setChapters(chapters) adapter?.setChapters(chapters)
@ -821,15 +821,11 @@ class MangaDetailsController :
updateMenuVisibility(activityBinding?.toolbar?.menu) updateMenuVisibility(activityBinding?.toolbar?.menu)
} }
fun updateChapters(chapters: List<ChapterItem>) { fun updateChapters() {
view ?: return view ?: return
binding.swipeRefresh.isRefreshing = presenter.isLoading binding.swipeRefresh.isRefreshing = presenter.isLoading
if (presenter.chapters.isEmpty() && fromCatalogue && !presenter.hasRequested) { adapter?.setChapters(presenter.chapters)
launchUI { binding.swipeRefresh.isRefreshing = true }
presenter.fetchChaptersFromSource()
}
tabletAdapter?.notifyItemChanged(0) tabletAdapter?.notifyItemChanged(0)
adapter?.setChapters(chapters)
addMangaHeader() addMangaHeader()
colorToolbar(binding.recycler.canScrollVertically(-1)) colorToolbar(binding.recycler.canScrollVertically(-1))
updateMenuVisibility(activityBinding?.toolbar?.menu) updateMenuVisibility(activityBinding?.toolbar?.menu)

View file

@ -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.Track
import eu.kanade.tachiyomi.data.database.models.bookmarkedFilter import eu.kanade.tachiyomi.data.database.models.bookmarkedFilter
import eu.kanade.tachiyomi.data.database.models.chapterOrder 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.downloadedFilter
import eu.kanade.tachiyomi.data.database.models.prepareCoverUpdate import eu.kanade.tachiyomi.data.database.models.prepareCoverUpdate
import eu.kanade.tachiyomi.data.database.models.readFilter 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.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.domain.manga.models.Manga import eu.kanade.tachiyomi.domain.manga.models.Manga
import eu.kanade.tachiyomi.network.HttpException
import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
@ -76,11 +78,15 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.updateAndGet
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -127,11 +133,15 @@ class MangaDetailsPresenter(
private val networkPreferences: NetworkPreferences by injectLazy() private val networkPreferences: NetworkPreferences by injectLazy()
// private val currentMangaInternal: MutableStateFlow<Manga?> = MutableStateFlow(null) private val currentMangaInternal = MutableStateFlow<Manga?>(null)
// val currentManga get() = currentMangaInternal.asStateFlow() 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 customMangaManager: CustomMangaManager by injectLazy()
private val mangaShortcutManager: MangaShortcutManager by injectLazy() private val mangaShortcutManager: MangaShortcutManager by injectLazy()
@ -151,8 +161,12 @@ class MangaDetailsPresenter(
var trackList: List<TrackItem> = emptyList() var trackList: List<TrackItem> = emptyList()
var chapters: List<ChapterItem> = emptyList() private val currentChaptersInternal = MutableStateFlow<List<ChapterItem>>(emptyList())
private set val currentChapters = currentChaptersInternal.asStateFlow()
var chapters: List<ChapterItem>
get() = currentChapters.value
private set(value) { currentChaptersInternal.value = value }
var allChapters: List<ChapterItem> = emptyList() var allChapters: List<ChapterItem> = emptyList()
private set private set
@ -186,7 +200,7 @@ class MangaDetailsPresenter(
val controller = view ?: return val controller = view ?: return
isLockedFromSearch = controller.shouldLockIfNeeded && SecureActivityDelegate.shouldBeLocked() isLockedFromSearch = controller.shouldLockIfNeeded && SecureActivityDelegate.shouldBeLocked()
if (!::manga.isInitialized) runBlocking { refreshMangaFromDb() } if (currentManga.value == null) runBlocking { refreshMangaFromDb() }
syncData() syncData()
presenterScope.launchUI { presenterScope.launchUI {
@ -204,6 +218,26 @@ class MangaDetailsPresenter(
presenterScope.launchIO { presenterScope.launchIO {
downloadManager.queueState.collectLatest(::onQueueUpdate) 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 { runBlocking {
tracks = getTrack.awaitAllByMangaId(mangaId) tracks = getTrack.awaitAllByMangaId(mangaId)
@ -216,25 +250,24 @@ class MangaDetailsPresenter(
fun onCreateLate() { fun onCreateLate() {
val controller = view ?: return val controller = view ?: return
isLoading = true
controller.setRefresh(true) // FIXME: Use progress indicator instead
LibraryUpdateJob.updateFlow LibraryUpdateJob.updateFlow
.filter { it == mangaId } .filter { it == mangaId }
.onEach { onUpdateManga() } .onEach { onUpdateManga() }
.launchIn(presenterScope) .launchIn(presenterScope)
if (manga.isLocal()) { val updateMangaNeeded = !manga.initialized
refreshAll() val updateChaptersNeeded = runBlocking { setAndGetChapters() }.isEmpty()
} else if (!manga.initialized) {
isLoading = true
controller.setRefresh(true)
controller.updateHeader()
refreshAll()
} else {
runBlocking { getChapters() }
controller.updateChapters(this.chapters)
getHistory()
}
presenterScope.launch { presenterScope.launch {
val tasks = listOf(
async { if (updateMangaNeeded) fetchMangaFromSource() },
async { if (updateChaptersNeeded) fetchChaptersFromSource(false) },
)
tasks.awaitAll()
setTrackItems() setTrackItems()
} }
@ -243,16 +276,14 @@ class MangaDetailsPresenter(
fun fetchChapters(andTracking: Boolean = true) { fun fetchChapters(andTracking: Boolean = true) {
presenterScope.launch { presenterScope.launch {
getChapters() setCurrentChapters(getChapters())
if (andTracking) fetchTracks() if (andTracking) fetchTracks()
withContext(Dispatchers.Main) { view?.updateChapters(chapters) }
getHistory() getHistory()
} }
} }
fun setCurrentManga(manga: Manga?) { fun setCurrentManga(manga: Manga?) {
// currentMangaInternal.update { manga } currentMangaInternal.update { manga }
this.manga = manga!!
} }
// TODO: Use flow to "sync" data instead // TODO: Use flow to "sync" data instead
@ -264,20 +295,18 @@ class MangaDetailsPresenter(
} }
} }
suspend fun getChaptersNow(): List<ChapterItem> { // TODO: Use getChapter.subscribe() flow instead
getChapters() suspend fun setAndGetChapters(): List<ChapterItem> {
return chapters return currentChaptersInternal.updateAndGet { getChapters().applyChapterFilters() }
} }
private suspend fun getChapters(queue: List<Download> = downloadManager.queueState.value) { // TODO: Use getChapter.subscribe() flow instead
val chapters = getChapter.awaitAll(mangaId, isScanlatorFiltered()).map { it.toModel() } private fun setCurrentChapters(chapters: List<ChapterItem>) {
allChapters = if (!isScanlatorFiltered()) chapters else getChapter.awaitAll(mangaId, false).map { it.toModel() } currentChaptersInternal.update { chapters.applyChapterFilters() }
}
// Find downloaded chapters private suspend fun getChapters(): List<ChapterItem> {
setDownloadedChapters(chapters, queue) return getChapter.awaitAll(mangaId, isScanlatorFiltered()).map { it.toModel() }
allChapterScanlators = allChapters.mapNotNull { it.chapter.scanlator }.toSet()
this.chapters = applyChapterFilters(chapters)
} }
private fun getHistory() { private fun getHistory() {
@ -291,14 +320,17 @@ class MangaDetailsPresenter(
* *
* @param chapters the list of chapter from the database. * @param chapters the list of chapter from the database.
*/ */
private fun setDownloadedChapters(chapters: List<ChapterItem>, queue: List<Download>) { private fun setDownloadedChapters(queue: List<Download>) {
for (chapter in chapters) { currentChaptersInternal.update { chapters ->
if (downloadManager.isChapterDownloaded(chapter, manga)) { for (chapter in chapters) {
chapter.status = Download.State.DOWNLOADED if (downloadManager.isChapterDownloaded(chapter, manga)) {
} else if (queue.isNotEmpty()) { chapter.status = Download.State.DOWNLOADED
chapter.status = queue.find { it.chapter.id == chapter.id } } else if (queue.isNotEmpty()) {
?.status ?: Download.State.default 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 * @param chapterList the list of chapters from the database
* @return an observable of the list of chapters filtered and sorted. * @return an observable of the list of chapters filtered and sorted.
*/ */
private fun applyChapterFilters(chapterList: List<ChapterItem>): List<ChapterItem> { private fun List<ChapterItem>.applyChapterFilters(): List<ChapterItem> {
if (isLockedFromSearch) { if (isLockedFromSearch) {
return chapterList return this
} }
getScrollType(chapterList) getScrollType(this)
return chapterSort.getChaptersSorted(chapterList) return chapterSort.getChaptersSorted(this)
} }
fun getChapterUrl(chapter: Chapter): String? { fun getChapterUrl(chapter: Chapter): String? {
@ -394,7 +426,7 @@ class MangaDetailsPresenter(
download = null download = null
} }
view?.updateChapters(this.chapters) view?.updateChapters()
downloadManager.deleteChapters(listOf(chapter), manga, source, true) 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) { if (isEverything) {
downloadManager.deleteManga(manga, source) downloadManager.deleteManga(manga, source)
@ -432,125 +464,80 @@ class MangaDetailsPresenter(
if (view?.isNotOnline() == true && !manga.isLocal()) return if (view?.isNotOnline() == true && !manga.isLocal()) return
presenterScope.launch { presenterScope.launch {
isLoading = true isLoading = true
var mangaError: java.lang.Exception? = null val tasks = listOf(
var chapterError: java.lang.Exception? = null async { fetchMangaFromSource() },
val chapters = async(Dispatchers.IO) { async { fetchChaptersFromSource() },
try { )
source.getChapterList(manga.copy()) tasks.awaitAll()
} catch (e: Exception) { isLoading = false
chapterError = e }
emptyList() }
}
}
val nManga = async(Dispatchers.IO) {
try {
source.getMangaDetails(manga.copy())
} catch (e: java.lang.Exception) {
mangaError = e
null
}
}
val networkManga = nManga.await() private suspend fun fetchMangaFromSource() {
if (networkManga != null) { try {
manga.prepareCoverUpdate(coverCache, networkManga, false) val manga = manga.copyDomain()
manga.copyFrom(networkManga) val networkManga = source.getMangaDetails(manga.copy())
manga.initialized = true
updateManga.await(manga.toMangaUpdate()) manga.prepareCoverUpdate(coverCache, networkManga, false)
manga.copyFrom(networkManga)
manga.initialized = true
launchIO { updateManga.await(manga.toMangaUpdate())
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) { setCurrentManga(manga)
withContext(Dispatchers.Main) {
view?.setPaletteColor() 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() } catch (e: Exception) {
if (finChapters.isNotEmpty()) { if (e is HttpException && e.code == 103) return
val newChapters = withIOContext { syncChaptersWithSource(finChapters, manga, source) }
if (newChapters.first.isNotEmpty()) { 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)) { if (manga.shouldDownloadNewChapters(preferences)) {
downloadChapters( downloadChapters(added.sortedBy { it.chapter_number }.map { it.toModel() })
newChapters.first.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()) { if (removed.isNotEmpty() && manualFetch) {
val removedChaptersId = newChapters.second.map { it.id } val removedChaptersId = removed.map { it.id }
val removedChapters = this@MangaDetailsPresenter.chapters.filter { val removedChapters = this@MangaDetailsPresenter.chapters.filter {
it.id in removedChaptersId && it.isDownloaded it.id in removedChaptersId && it.isDownloaded
} }
if (removedChapters.isNotEmpty()) { if (removedChapters.isNotEmpty()) {
withContext(Dispatchers.Main) { withUIContext {
view?.showChaptersRemovedPopup( view?.showChaptersRemovedPopup(removedChapters)
removedChapters,
)
} }
} }
} }
getChapters() setCurrentChapters(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)
}
getHistory() getHistory()
} catch (e: java.lang.Exception) { }
withContext(Dispatchers.Main) { } catch (e: Exception) {
view?.showError(trimException(e)) withUIContext {
} view?.showError(trimException(e))
} }
} }
} }
@ -579,8 +566,7 @@ class MangaDetailsPresenter(
it.toProgressUpdate() it.toProgressUpdate()
} }
updateChapter.awaitAll(updates) updateChapter.awaitAll(updates)
getChapters() setCurrentChapters(getChapters())
withContext(Dispatchers.Main) { view?.updateChapters(chapters) }
} }
} }
@ -609,8 +595,7 @@ class MangaDetailsPresenter(
if (read && deleteNow && preferences.removeAfterMarkedAsRead().get()) { if (read && deleteNow && preferences.removeAfterMarkedAsRead().get()) {
deleteChapters(selectedChapters, false) deleteChapters(selectedChapters, false)
} }
getChapters() setCurrentChapters(getChapters())
withContext(Dispatchers.Main) { view?.updateChapters(chapters) }
if (read && deleteNow) { if (read && deleteNow) {
val latestReadChapter = selectedChapters.maxByOrNull { it.chapter_number.toInt() }?.chapter val latestReadChapter = selectedChapters.maxByOrNull { it.chapter_number.toInt() }?.chapter
updateTrackChapterMarkedAsRead(preferences, latestReadChapter, manga.id) { updateTrackChapterMarkedAsRead(preferences, latestReadChapter, manga.id) {
@ -741,8 +726,7 @@ class MangaDetailsPresenter(
private suspend fun asyncUpdateMangaAndChapters(justChapters: Boolean = false) { private suspend fun asyncUpdateMangaAndChapters(justChapters: Boolean = false) {
if (!justChapters) updateManga.await(MangaUpdate(manga.id!!, chapterFlags = manga.chapter_flags)) if (!justChapters) updateManga.await(MangaUpdate(manga.id!!, chapterFlags = manga.chapter_flags))
getChapters() setCurrentChapters(getChapters())
withUIContext { view?.updateChapters(chapters) }
} }
private fun isScanlatorFiltered() = manga.filtered_scanlators?.isNotEmpty() == true private fun isScanlatorFiltered() = manga.filtered_scanlators?.isNotEmpty() == true
@ -811,7 +795,7 @@ class MangaDetailsPresenter(
withUIContext { withUIContext {
view?.shareManga(uri.uri.toFile()) view?.shareManga(uri.uri.toFile())
} }
} catch (_: java.lang.Exception) { } catch (_: Exception) {
} }
} }
} }
@ -1153,15 +1137,15 @@ class MangaDetailsPresenter(
override fun onStatusChange(download: Download) { override fun onStatusChange(download: Download) {
super.onStatusChange(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) onPageProgressUpdate(download)
} }
private suspend fun onQueueUpdate(queue: List<Download>) = withIOContext { private suspend fun onQueueUpdate(queue: List<Download>) = withIOContext {
getChapters(queue) setDownloadedChapters(queue)
withUIContext {
view?.updateChapters(chapters)
}
} }
override fun onQueueUpdate(download: Download) { override fun onQueueUpdate(download: Download) {
@ -1173,7 +1157,10 @@ class MangaDetailsPresenter(
} }
override fun onPageProgressUpdate(download: Download) { 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) view?.updateChapterDownload(download)
} }