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 5c5ef2883..2d5d6d2aa 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 @@ -17,6 +17,8 @@ fun Collection.distinctById() = distinctBy { it.id } @JvmName("chaptersIds") fun Collection.ids() = mapToSet { it.id } +fun Collection.findById(id: Long) = find { x -> x.id == id } + fun Collection.countChaptersByBranch(): Int { if (size <= 1) { return size @@ -30,7 +32,7 @@ fun Collection.countChaptersByBranch(): Int { } fun Manga.findChapter(id: Long): MangaChapter? { - return chapters?.find { it.id == id } + return chapters?.findById(id) } fun Manga.getPreferredBranch(history: MangaHistory?): String? { @@ -39,7 +41,7 @@ fun Manga.getPreferredBranch(history: MangaHistory?): String? { return null } if (history != null) { - val currentChapter = ch.find { it.id == history.chapterId } + val currentChapter = ch.findById(history.chapterId) if (currentChapter != null) { return currentChapter.branch } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Flow.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Flow.kt index 5312142c8..28e1189b7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Flow.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Flow.kt @@ -5,6 +5,8 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion @@ -72,7 +74,7 @@ fun combine( flow4: Flow, flow5: Flow, flow6: Flow, - transform: suspend (T1, T2, T3, T4, T5, T6) -> R + transform: suspend (T1, T2, T3, T4, T5, T6) -> R, ): Flow = combine(flow, flow2, flow3, flow4, flow5, flow6) { args: Array<*> -> transform( args[0] as T1, @@ -83,3 +85,7 @@ fun combine( args[5] as T6, ) } + +suspend fun Flow.firstNotNull(): T = checkNotNull(first { x -> x != null }) + +suspend fun Flow.firstNotNullOrNull(): T? = firstOrNull { x -> x != null } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/View.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/View.kt index ddb3a0c0f..5f3bc159e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/View.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/View.kt @@ -8,10 +8,11 @@ import android.view.ViewGroup import android.view.ViewParent import android.view.inputmethod.InputMethodManager import android.widget.Checkable -import android.widget.CompoundButton import androidx.core.view.children +import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 +import com.google.android.material.progressindicator.BaseProgressIndicator import com.google.android.material.slider.Slider import com.google.android.material.tabs.TabLayout import kotlin.math.roundToInt @@ -155,3 +156,11 @@ fun TabLayout.setTabsEnabled(enabled: Boolean) { getTabAt(i)?.view?.isEnabled = enabled } } + +fun BaseProgressIndicator<*>.showOrHide(value: Boolean) { + if (value) { + if (!isVisible) show() + } else { + if (isVisible) hide() + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DoubleMangaLoadUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DoubleMangaLoadUseCase.kt index e228acb57..89f8ee2a7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DoubleMangaLoadUseCase.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DoubleMangaLoadUseCase.kt @@ -1,8 +1,11 @@ package org.koitharu.kotatsu.details.domain import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaIntent @@ -23,24 +26,28 @@ class DoubleMangaLoadUseCase @Inject constructor( private val recoverUseCase: RecoverMangaUseCase, ) { - suspend operator fun invoke(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(), - ) - } + operator fun invoke(manga: Manga): Flow = flow { + var lastValue: DoubleManga? = null + var emitted = false + invokeImpl(manga).collect { + lastValue = it + if (it.any != null) { + emitted = true + emit(it) + } + } + if (!emitted) { + lastValue?.requireAny() + } + }.flowOn(Dispatchers.Default) - suspend operator fun invoke(mangaId: Long): DoubleManga { - val manga = mangaDataRepository.findMangaById(mangaId) ?: throwNFE() - return invoke(manga) - } + operator fun invoke(mangaId: Long): Flow = flow { + emit(mangaDataRepository.findMangaById(mangaId) ?: throwNFE()) + }.flatMapLatest { invoke(it) } - suspend operator fun invoke(intent: MangaIntent): DoubleManga { - val manga = mangaDataRepository.resolveIntent(intent) ?: throwNFE() - return invoke(manga) - } + operator fun invoke(intent: MangaIntent): Flow = flow { + emit(mangaDataRepository.resolveIntent(intent) ?: throwNFE()) + }.flatMapLatest { invoke(it) } private suspend fun loadLocal(manga: Manga): Result? { return runCatchingCancellable { @@ -70,5 +77,15 @@ class DoubleMangaLoadUseCase @Inject constructor( } } + private fun invokeImpl(manga: Manga): Flow = combine( + flow { emit(null); emit(loadRemote(manga)) }, + flow { emit(null); emit(loadLocal(manga)) }, + ) { remote, local -> + DoubleManga( + remoteManga = remote, + localManga = local, + ) + } + private fun throwNFE(): Nothing = throw NotFoundException("Cannot find manga", "") } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/model/DoubleManga.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/model/DoubleManga.kt index 732f59902..749616006 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/model/DoubleManga.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/model/DoubleManga.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.details.domain.model +import org.koitharu.kotatsu.core.model.findById import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaSource @@ -34,6 +35,10 @@ data class DoubleManga( mergeChapters() } + fun hasChapter(id: Long): Boolean { + return local?.chapters?.findById(id) != null || remote?.chapters?.findById(id) != null + } + fun requireAny(): Manga { val result = remoteManga?.getOrNull() ?: localManga?.getOrNull() if (result != null) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt index 0debc76e2..e2944b6fd 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt @@ -5,6 +5,7 @@ import android.content.Intent import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.EntryPointAccessors import org.koitharu.kotatsu.core.cache.ContentCache +import org.koitharu.kotatsu.core.model.findById import org.koitharu.kotatsu.core.model.parcelable.ParcelableChapter import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.parser.MangaRepository @@ -72,7 +73,7 @@ class MangaPrefetchService : CoroutineIntentService() { val chapter = if (history == null) { chapters.firstOrNull() } else { - chapters.find { x -> x.id == history.chapterId } ?: chapters.firstOrNull() + chapters.findById(history.chapterId) ?: chapters.firstOrNull() } ?: return runCatchingCancellable { repo.getPages(chapter) } } @@ -122,7 +123,7 @@ class MangaPrefetchService : CoroutineIntentService() { } val entryPoint = EntryPointAccessors.fromApplication( context, - PrefetchCompanionEntryPoint::class.java + PrefetchCompanionEntryPoint::class.java, ) return entryPoint.contentCache.isCachingEnabled && entryPoint.settings.isContentPrefetchEnabled } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsFragment.kt index 32ffa04ef..31935011d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsFragment.kt @@ -43,6 +43,7 @@ import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.parentView import org.koitharu.kotatsu.core.util.ext.resolveDp import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf +import org.koitharu.kotatsu.core.util.ext.showOrHide import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.databinding.FragmentDetailsBinding import org.koitharu.kotatsu.details.ui.model.ChapterListItem @@ -247,11 +248,7 @@ class DetailsFragment : } private fun onLoadingStateChanged(isLoading: Boolean) { - if (isLoading) { - requireViewBinding().progressBar.show() - } else { - requireViewBinding().progressBar.hide() - } + requireViewBinding().progressBar.showOrHide(isLoading) } private fun onBookmarksChanged(bookmarks: List) { 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 405ce9f54..8ca533d5a 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 @@ -42,6 +42,7 @@ import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.combine import org.koitharu.kotatsu.core.util.ext.computeSize +import org.koitharu.kotatsu.core.util.ext.onFirst import org.koitharu.kotatsu.core.util.ext.requireValue import org.koitharu.kotatsu.core.util.ext.sanitize import org.koitharu.kotatsu.core.util.ext.toFileOrNull @@ -87,7 +88,8 @@ class DetailsViewModel @Inject constructor( private val intent = MangaIntent(savedStateHandle) private val mangaId = intent.mangaId - private val doubleManga: MutableStateFlow = MutableStateFlow(intent.manga?.let { DoubleManga(it) }) + private val doubleManga: MutableStateFlow = + MutableStateFlow(intent.manga?.let { DoubleManga(it) }) private var loadingJob: Job val onShowToast = MutableEventFlow() @@ -202,7 +204,14 @@ class DetailsViewModel @Inject constructor( bookmarks, networkState, ) { manga, history, branch, news, bookmarks, isOnline -> - mapChapters(manga?.remote?.takeIf { isOnline }, manga?.local, history, news, branch, bookmarks) + mapChapters( + manga?.remote?.takeIf { isOnline }, + manga?.local, + history, + news, + branch, + bookmarks, + ) }, isChaptersReversed, chaptersQuery, @@ -324,12 +333,15 @@ class DetailsViewModel @Inject constructor( } private fun doLoad() = launchLoadingJob(Dispatchers.Default) { - val result = doubleMangaLoadUseCase(intent) - val manga = result.requireAny() - // find default branch - val hist = historyRepository.getOne(manga) - selectedBranch.value = manga.getPreferredBranch(hist) - doubleManga.value = result + doubleMangaLoadUseCase.invoke(intent) + .onFirst { + val manga = it.requireAny() + // find default branch + val hist = historyRepository.getOne(manga) + selectedBranch.value = manga.getPreferredBranch(hist) + }.collect { + doubleManga.value = it + } } private fun List.filterSearch(query: String): List { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt index 4b9db3ecf..80a01682f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt @@ -14,6 +14,7 @@ import org.koitharu.kotatsu.core.db.entity.toManga 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.model.findById import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.ui.util.ReversibleHandle import org.koitharu.kotatsu.core.util.ext.mapItems @@ -107,7 +108,7 @@ class HistoryRepository @Inject constructor( ), ) trackingRepository.syncWithHistory(manga, chapterId) - val chapter = manga.chapters?.find { x -> x.id == chapterId } + val chapter = manga.chapters?.findById(chapterId) if (chapter != null) { scrobblers.forEach { it.tryScrobble(manga.id, chapter) } } @@ -181,7 +182,7 @@ class HistoryRepository @Inject constructor( private suspend fun HistoryEntity.recoverIfNeeded(manga: Manga): HistoryEntity { val chapters = manga.chapters - if (chapters.isNullOrEmpty() || chapters.any { it.id == chapterId }) { + if (chapters.isNullOrEmpty() || chapters.findById(chapterId) != null) { return this } val newChapterId = chapters.getOrNull( diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaDirOutput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaDirOutput.kt index ef050f1cc..ce04320ea 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaDirOutput.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaDirOutput.kt @@ -2,6 +2,7 @@ package org.koitharu.kotatsu.local.data.output import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible +import org.koitharu.kotatsu.core.model.findById import org.koitharu.kotatsu.core.util.ext.deleteAwait import org.koitharu.kotatsu.core.util.ext.takeIfReadable import org.koitharu.kotatsu.core.zip.ZipOutput @@ -87,7 +88,7 @@ class LocalMangaDirOutput( suspend fun deleteChapter(chapterId: Long) { val chapter = checkNotNull(index.getMangaInfo()?.chapters) { "No chapters found" - }.first { it.id == chapterId } + }.findById(chapterId) ?: error("Chapter not found") val chapterDir = File(rootFile, chapterFileName(chapter)) chapterDir.deleteAwait() index.removeChapter(chapterId) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/domain/CoverRestoreInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/domain/CoverRestoreInterceptor.kt index 9aef8c91d..11f34a6c2 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/main/domain/CoverRestoreInterceptor.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/domain/CoverRestoreInterceptor.kt @@ -8,6 +8,7 @@ import coil.request.ImageResult import org.jsoup.HttpStatusException import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository +import org.koitharu.kotatsu.core.model.findById import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository @@ -92,7 +93,7 @@ class CoverRestoreInterceptor @Inject constructor( private suspend fun restoreBookmarkImpl(bookmark: Bookmark): Boolean { val repo = repositoryFactory.create(bookmark.manga.source) as? RemoteMangaRepository ?: return false - val chapter = repo.getDetails(bookmark.manga).chapters?.find { it.id == bookmark.chapterId } ?: return false + val chapter = repo.getDetails(bookmark.manga).chapters?.findById(bookmark.chapterId) ?: return false val page = repo.getPages(chapter)[bookmark.page] val imageUrl = page.preview.ifNullOrEmpty { page.url } return if (imageUrl != bookmark.imageUrl) { 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 1c75d5702..b60c842b9 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 @@ -2,6 +2,12 @@ package org.koitharu.kotatsu.reader.domain import android.util.LongSparseArray import dagger.hilt.android.scopes.ViewModelScoped +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.koitharu.kotatsu.core.parser.MangaRepository @@ -17,24 +23,32 @@ class ChaptersLoader @Inject constructor( private val mangaRepositoryFactory: MangaRepository.Factory, ) { - private val chapters = LongSparseArray() + private val chapters = MutableStateFlow(LongSparseArray(0)) private val chapterPages = ChapterPages() private val mutex = Mutex() - val size: Int - get() = chapters.size() + val size: Int // TODO flow + get() = chapters.value.size() - suspend fun init(manga: DoubleManga) = mutex.withLock { - chapters.clear() - manga.chapters?.forEach { - chapters.put(it.id, it) + fun init(scope: CoroutineScope, manga: Flow) = scope.launch { + manga.collect { + val ch = it.chapters.orEmpty() + val longSparseArray = LongSparseArray(ch.size) + ch.forEach { x -> longSparseArray.put(x.id, x) } + mutex.withLock { + chapters.value = longSparseArray + } } } 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) + 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(newChapter.id) @@ -65,7 +79,11 @@ class ChaptersLoader @Inject constructor( } } - fun peekChapter(chapterId: Long): MangaChapter? = chapters[chapterId] + fun peekChapter(chapterId: Long): MangaChapter? = chapters.value[chapterId] + + suspend fun awaitChapter(chapterId: Long): MangaChapter? = chapters.mapNotNull { x -> + x[chapterId] + }.firstOrNull() fun getPages(chapterId: Long): List { return chapterPages.subList(chapterId) @@ -82,7 +100,7 @@ class ChaptersLoader @Inject constructor( fun snapshot() = chapterPages.toList() private suspend fun loadChapter(chapterId: Long): List { - val chapter = checkNotNull(chapters[chapterId]) { "Requested chapter not found" } + val chapter = checkNotNull(awaitChapter(chapterId)) { "Requested chapter not found" } 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/ReaderViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt index fc6f6c6ab..fb6b72261 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 @@ -87,7 +87,8 @@ class ReaderViewModel @Inject constructor( private var pageSaveJob: Job? = null private var bookmarkJob: Job? = null private var stateChangeJob: Job? = null - private val currentState = MutableStateFlow(savedStateHandle[ReaderActivity.EXTRA_STATE]) + private val currentState = + MutableStateFlow(savedStateHandle[ReaderActivity.EXTRA_STATE]) private val mangaData = MutableStateFlow(intent.manga?.let { DoubleManga(it) }) private val mangaFlow: Flow get() = mangaData.map { it?.any } @@ -317,8 +318,9 @@ class ReaderViewModel @Inject constructor( ?: throw NotFoundException("Cannot find manga", ""), ) mangaData.value = manga - manga = doubleMangaLoadUseCase(intent) - chaptersLoader.init(manga) + val mangaFlow = doubleMangaLoadUseCase(intent) + manga = mangaFlow.first { x -> x.any != null } + chaptersLoader.init(viewModelScope, mangaFlow) // determine mode val singleManga = manga.requireAny() // obtain state @@ -328,7 +330,7 @@ class ReaderViewModel @Inject constructor( } ?: ReaderState(singleManga, preselectedBranch) } val mode = detectReaderModeUseCase.invoke(singleManga, currentState.value) - val branch = chaptersLoader.peekChapter(currentState.value?.chapterId ?: 0L)?.branch + val branch = chaptersLoader.awaitChapter(currentState.value?.chapterId ?: 0L)?.branch mangaData.value = manga.filterChapters(branch) readerMode.value = mode diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt index d86f15d8b..0e68ea9ae 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt @@ -24,6 +24,7 @@ import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.plus import org.koitharu.kotatsu.core.util.ext.showDistinct +import org.koitharu.kotatsu.core.util.ext.showOrHide import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.databinding.SheetPagesBinding import org.koitharu.kotatsu.list.ui.MangaListSpanResolver @@ -84,6 +85,7 @@ class PagesThumbnailsSheet : viewModel.thumbnails.observe(viewLifecycleOwner, ::onThumbnailsChanged) viewModel.branch.observe(viewLifecycleOwner, ::updateTitle) viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this)) + viewModel.isLoading.observe(viewLifecycleOwner) { binding.progressBar.showOrHide(it) } } override fun onDestroyView() { 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 18d41b1ef..7b7e438a9 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 @@ -1,19 +1,26 @@ package org.koitharu.kotatsu.reader.ui.thumbnails import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import org.koitharu.kotatsu.core.model.findById import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.util.ext.firstNotNull +import org.koitharu.kotatsu.core.util.ext.firstNotNullOrNull import org.koitharu.kotatsu.core.util.ext.require import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase 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.parsers.util.SuspendLazy import org.koitharu.kotatsu.reader.domain.ChaptersLoader import javax.inject.Inject @@ -30,13 +37,12 @@ class PagesThumbnailsViewModel @Inject constructor( val manga = savedStateHandle.require(PagesThumbnailsSheet.ARG_MANGA).manga private val repository = mangaRepositoryFactory.create(manga.source) - private val mangaDetails = SuspendLazy { - doubleMangaLoadUseCase(manga).let { - val b = manga.chapters?.find { ch -> ch.id == initialChapterId }?.branch - branch.value = b - it.filterChapters(b) - } - } + private val mangaDetails = doubleMangaLoadUseCase(manga).map { + val b = manga.chapters?.findById(initialChapterId)?.branch + branch.value = b + it.filterChapters(b) + }.withErrorHandling() + .stateIn(viewModelScope, SharingStarted.Lazily, null) private var loadingJob: Job? = null private var loadingPrevJob: Job? = null private var loadingNextJob: Job? = null @@ -46,8 +52,9 @@ class PagesThumbnailsViewModel @Inject constructor( val branch = MutableStateFlow(null) init { - loadingJob = launchJob(Dispatchers.Default) { - chaptersLoader.init(mangaDetails.get()) + loadingJob = launchLoadingJob(Dispatchers.Default) { + chaptersLoader.init(viewModelScope, mangaDetails.filterNotNull()) + mangaDetails.first { x -> x?.hasChapter(initialChapterId) == true } chaptersLoader.loadSingleChapter(initialChapterId) updateList() } @@ -55,7 +62,7 @@ class PagesThumbnailsViewModel @Inject constructor( fun allowLoadAbove() { if (!isLoadAboveAllowed) { - loadingJob = launchJob(Dispatchers.Default) { + loadingJob = launchLoadingJob(Dispatchers.Default) { isLoadAboveAllowed = true updateList() } @@ -78,23 +85,18 @@ class PagesThumbnailsViewModel @Inject constructor( private fun loadPrevNextChapter(isNext: Boolean): Job = launchLoadingJob(Dispatchers.Default) { val currentId = (if (isNext) chaptersLoader.last() else chaptersLoader.first()).chapterId - chaptersLoader.loadPrevNextChapter(mangaDetails.get(), currentId, isNext) + chaptersLoader.loadPrevNextChapter(mangaDetails.firstNotNull(), currentId, isNext) updateList() } private suspend fun updateList() { val snapshot = chaptersLoader.snapshot() - val mangaChapters = mangaDetails.tryGet().getOrNull()?.chapters.orEmpty() - val hasPrevChapter = isLoadAboveAllowed && snapshot.firstOrNull()?.chapterId != mangaChapters.firstOrNull()?.id - val hasNextChapter = snapshot.lastOrNull()?.chapterId != mangaChapters.lastOrNull()?.id + val mangaChapters = mangaDetails.firstNotNullOrNull()?.chapters.orEmpty() 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.peekChapter(page.chapterId)?.let { + chaptersLoader.awaitChapter(page.chapterId)?.let { add(ListHeader(it.name)) } previousChapterId = page.chapterId @@ -105,9 +107,6 @@ class PagesThumbnailsViewModel @Inject constructor( page = page, ) } - if (hasNextChapter) { - add(LoadingFooter(1)) - } } thumbnails.value = pages } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAdapter.kt index c0f1cf253..a519b4a4c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAdapter.kt @@ -8,7 +8,6 @@ import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller import org.koitharu.kotatsu.list.ui.adapter.ListItemType import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD -import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail @@ -22,7 +21,6 @@ class PageThumbnailAdapter( init { addDelegate(ListItemType.PAGE_THUMB, pageThumbnailAD(coil, lifecycleOwner, clickListener)) addDelegate(ListItemType.HEADER, listHeaderAD(null)) - addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD()) } override fun getSectionText(context: Context, position: Int): CharSequence? { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigActivity.kt index e6d252e12..52d42edf0 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigActivity.kt @@ -19,6 +19,7 @@ import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observeEvent +import org.koitharu.kotatsu.core.util.ext.showOrHide import org.koitharu.kotatsu.databinding.ActivityScrobblerConfigBinding import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration @@ -115,13 +116,7 @@ class ScrobblerConfigActivity : BaseActivity(), } private fun onLoadingStateChanged(isLoading: Boolean) { - viewBinding.progressBar.run { - if (isLoading) { - show() - } else { - hide() - } - } + viewBinding.progressBar.showOrHide(isLoading) } private fun showUserDialog() { diff --git a/app/src/main/res/layout/sheet_pages.xml b/app/src/main/res/layout/sheet_pages.xml index 140e944af..7c5fd9fad 100644 --- a/app/src/main/res/layout/sheet_pages.xml +++ b/app/src/main/res/layout/sheet_pages.xml @@ -14,7 +14,8 @@ + android:layout_height="match_parent" + android:minHeight="240dp"> + +