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) }
}
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
}

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
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<ChapterItem>) {
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)

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.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<Manga?> = MutableStateFlow(null)
// val currentManga get() = currentMangaInternal.asStateFlow()
private val currentMangaInternal = MutableStateFlow<Manga?>(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<TrackItem> = emptyList()
var chapters: List<ChapterItem> = emptyList()
private set
private val currentChaptersInternal = MutableStateFlow<List<ChapterItem>>(emptyList())
val currentChapters = currentChaptersInternal.asStateFlow()
var chapters: List<ChapterItem>
get() = currentChapters.value
private set(value) { currentChaptersInternal.value = value }
var allChapters: List<ChapterItem> = 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<ChapterItem> {
getChapters()
return chapters
// TODO: Use getChapter.subscribe() flow instead
suspend fun setAndGetChapters(): List<ChapterItem> {
return currentChaptersInternal.updateAndGet { getChapters().applyChapterFilters() }
}
private suspend fun getChapters(queue: List<Download> = 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<ChapterItem>) {
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<ChapterItem> {
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<ChapterItem>, queue: List<Download>) {
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<Download>) {
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<ChapterItem>): List<ChapterItem> {
private fun List<ChapterItem>.applyChapterFilters(): List<ChapterItem> {
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<Download>) = 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)
}