Refactor manga details loading

This commit is contained in:
Koitharu
2023-10-10 11:50:32 +03:00
parent fbb267e11c
commit e4efd0f696
19 changed files with 270 additions and 355 deletions

View File

@@ -2,16 +2,10 @@ 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
import org.koitharu.kotatsu.details.domain.model.DoubleManga
import org.koitharu.kotatsu.details.data.MangaDetails
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import javax.inject.Inject
@@ -23,32 +17,24 @@ class ChaptersLoader @Inject constructor(
private val mangaRepositoryFactory: MangaRepository.Factory,
) {
private val chapters = MutableStateFlow(LongSparseArray<MangaChapter>(0))
private val chapters = LongSparseArray<MangaChapter>()
private val chapterPages = ChapterPages()
private val mutex = Mutex()
val size: Int // TODO flow
get() = chapters.value.size()
val size: Int
get() = chapters.size()
fun init(scope: CoroutineScope, manga: Flow<DoubleManga>) = scope.launch {
manga.collect {
val ch = it.chapters.orEmpty()
val longSparseArray = LongSparseArray<MangaChapter>(ch.size)
ch.forEach { x -> longSparseArray.put(x.id, x) }
mutex.withLock {
chapters.value = longSparseArray
}
suspend fun init(manga: MangaDetails) = mutex.withLock {
chapters.clear()
manga.allChapters.forEach {
chapters.put(it.id, it)
}
}
suspend fun loadPrevNextChapter(manga: DoubleManga, currentId: Long, isNext: Boolean) {
val chapters = manga.chapters ?: return
suspend fun loadPrevNextChapter(manga: MangaDetails, currentId: Long, isNext: Boolean) {
val chapters = manga.allChapters
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)
@@ -79,11 +65,7 @@ class ChaptersLoader @Inject constructor(
}
}
fun peekChapter(chapterId: Long): MangaChapter? = chapters.value[chapterId]
suspend fun awaitChapter(chapterId: Long): MangaChapter? = chapters.mapNotNull { x ->
x[chapterId]
}.firstOrNull()
fun peekChapter(chapterId: Long): MangaChapter? = chapters[chapterId]
fun getPages(chapterId: Long): List<ReaderPage> {
return chapterPages.subList(chapterId)
@@ -100,7 +82,7 @@ class ChaptersLoader @Inject constructor(
fun snapshot() = chapterPages.toList()
private suspend fun loadChapter(chapterId: Long): List<ReaderPage> {
val chapter = checkNotNull(awaitChapter(chapterId)) { "Requested chapter not found" }
val chapter = checkNotNull(chapters[chapterId]) { "Requested chapter not found" }
val repo = mangaRepositoryFactory.create(chapter.source)
return repo.getPages(chapter).mapIndexed { index, page ->
ReaderPage(page, index, chapterId)

View File

@@ -33,13 +33,11 @@ class ChaptersSheet : BaseAdaptiveSheet<SheetChaptersBinding>(),
override fun onCreateViewBinding(
inflater: LayoutInflater,
container: ViewGroup?,
): SheetChaptersBinding {
return SheetChaptersBinding.inflate(inflater, container, false)
}
) = SheetChaptersBinding.inflate(inflater, container, false)
override fun onViewBindingCreated(binding: SheetChaptersBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
val chapters = viewModel.manga?.chapters
val chapters = viewModel.manga?.allChapters
if (chapters.isNullOrEmpty()) {
dismissAllowingStateLoss()
return
@@ -61,7 +59,7 @@ class ChaptersSheet : BaseAdaptiveSheet<SheetChaptersBinding>(),
val offset =
(resources.getDimensionPixelSize(R.dimen.chapter_list_item_height) * 0.6).roundToInt()
adapter.setItems(
items, RecyclerViewScrollCallback(binding.recyclerView, targetPosition, offset)
items, RecyclerViewScrollCallback(binding.recyclerView, targetPosition, offset),
)
} else {
adapter.items = items

View File

@@ -189,7 +189,7 @@ class ReaderActivity :
val state = viewModel.getCurrentState() ?: return false
PagesThumbnailsSheet.show(
supportFragmentManager,
viewModel.manga?.any ?: return false,
viewModel.manga?.toManga() ?: return false,
state.chapterId,
state.page,
)

View File

@@ -44,12 +44,11 @@ import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.requireValue
import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase
import org.koitharu.kotatsu.details.domain.model.DoubleManga
import org.koitharu.kotatsu.details.data.MangaDetails
import org.koitharu.kotatsu.details.domain.DetailsLoadUseCase
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.history.domain.HistoryUpdateUseCase
import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.ChaptersLoader
@@ -74,7 +73,7 @@ class ReaderViewModel @Inject constructor(
private val pageLoader: PageLoader,
private val chaptersLoader: ChaptersLoader,
private val appShortcutManager: AppShortcutManager,
private val doubleMangaLoadUseCase: DoubleMangaLoadUseCase,
private val detailsLoadUseCase: DetailsLoadUseCase,
private val historyUpdateUseCase: HistoryUpdateUseCase,
private val detectReaderModeUseCase: DetectReaderModeUseCase,
) : BaseViewModel() {
@@ -88,9 +87,9 @@ class ReaderViewModel @Inject constructor(
private var bookmarkJob: Job? = null
private var stateChangeJob: Job? = null
private val currentState = MutableStateFlow<ReaderState?>(savedStateHandle[ReaderActivity.EXTRA_STATE])
private val mangaData = MutableStateFlow(intent.manga?.let { DoubleManga(it) })
private val mangaData = MutableStateFlow(intent.manga?.let { MangaDetails(it, null, null, false) })
private val mangaFlow: Flow<Manga?>
get() = mangaData.map { it?.any }
get() = mangaData.map { it?.toManga() }
val readerMode = MutableStateFlow<ReaderMode?>(null)
val onPageSaved = MutableEventFlow<Uri?>()
@@ -98,7 +97,7 @@ class ReaderViewModel @Inject constructor(
val uiState = MutableStateFlow<ReaderUiState?>(null)
val content = MutableStateFlow(ReaderContent(emptyList(), null))
val manga: DoubleManga?
val manga: MangaDetails?
get() = mangaData.value
val pageAnimation = settings.observeAsStateFlow(
@@ -148,7 +147,7 @@ class ReaderViewModel @Inject constructor(
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false)
val isBookmarkAdded = currentState.flatMapLatest { state ->
val manga = mangaData.value?.any
val manga = mangaData.value?.toManga()
if (state == null || manga == null) {
flowOf(false)
} else {
@@ -178,7 +177,7 @@ class ReaderViewModel @Inject constructor(
fun switchMode(newMode: ReaderMode) {
launchJob {
val manga = checkNotNull(mangaData.value?.any)
val manga = checkNotNull(mangaData.value?.toManga())
dataRepository.saveReaderMode(
manga = manga,
mode = newMode,
@@ -199,7 +198,7 @@ class ReaderViewModel @Inject constructor(
}
val readerState = state ?: currentState.value ?: return
historyUpdateUseCase.invokeAsync(
manga = mangaData.value?.any ?: return,
manga = mangaData.value?.toManga() ?: return,
readerState = readerState,
percent = computePercent(readerState.chapterId, readerState.page),
)
@@ -295,7 +294,7 @@ class ReaderViewModel @Inject constructor(
val state = checkNotNull(currentState.value)
val page = checkNotNull(getCurrentPage()) { "Page not found" }
val bookmark = Bookmark(
manga = checkNotNull(mangaData.value?.any),
manga = mangaData.requireValue().toManga(),
pageId = page.id,
chapterId = state.chapterId,
page = state.page,
@@ -315,7 +314,7 @@ class ReaderViewModel @Inject constructor(
}
bookmarkJob = launchJob {
loadingJob?.join()
val manga = checkNotNull(mangaData.value?.any)
val manga = mangaData.requireValue().toManga()
val state = checkNotNull(getCurrentState())
bookmarksRepository.removeBookmark(manga.id, state.chapterId, state.page)
onShowToast.call(R.string.bookmark_removed)
@@ -324,25 +323,19 @@ class ReaderViewModel @Inject constructor(
private fun loadImpl() {
loadingJob = launchLoadingJob(Dispatchers.Default) {
var manga = DoubleManga(
dataRepository.resolveIntent(intent)
?: throw NotFoundException("Cannot find manga", ""),
)
mangaData.value = manga
val mangaFlow = doubleMangaLoadUseCase(intent)
manga = mangaFlow.first { x -> x.any != null }
chaptersLoader.init(viewModelScope, mangaFlow.withErrorHandling())
// determine mode
val singleManga = manga.requireAny()
val details = detailsLoadUseCase.invoke(intent).first { x -> x.isLoaded }
mangaData.value = details
chaptersLoader.init(details)
val manga = details.toManga()
// obtain state
if (currentState.value == null) {
currentState.value = historyRepository.getOne(singleManga)?.let {
currentState.value = historyRepository.getOne(manga)?.let {
ReaderState(it)
} ?: ReaderState(singleManga, preselectedBranch)
} ?: ReaderState(manga, preselectedBranch)
}
val mode = detectReaderModeUseCase.invoke(singleManga, currentState.value)
val branch = chaptersLoader.awaitChapter(currentState.value?.chapterId ?: 0L)?.branch
mangaData.value = manga.filterChapters(branch)
val mode = detectReaderModeUseCase.invoke(manga, currentState.value)
val branch = chaptersLoader.peekChapter(currentState.value?.chapterId ?: 0L)?.branch
mangaData.value = details.filterChapters(branch)
readerMode.value = mode
chaptersLoader.loadSingleChapter(requireNotNull(currentState.value).chapterId)
@@ -350,7 +343,7 @@ class ReaderViewModel @Inject constructor(
if (!isIncognito) {
currentState.value?.let {
val percent = computePercent(it.chapterId, it.page)
historyUpdateUseCase.invoke(singleManga, it, percent)
historyUpdateUseCase.invoke(manga, it, percent)
}
}
notifyStateChanged()
@@ -383,11 +376,11 @@ class ReaderViewModel @Inject constructor(
val state = getCurrentState()
val chapter = state?.chapterId?.let { chaptersLoader.peekChapter(it) }
val newState = ReaderUiState(
mangaName = manga?.any?.title,
mangaName = manga?.toManga()?.title,
branch = chapter?.branch,
chapterName = chapter?.name,
chapterNumber = chapter?.number ?: 0,
chaptersTotal = manga?.any?.getChapters(chapter?.branch)?.size ?: 0,
chaptersTotal = manga?.chapters?.get(chapter?.branch)?.size ?: 0,
totalPages = if (chapter != null) chaptersLoader.getPagesCount(chapter.id) else 0,
currentPage = state?.page ?: 0,
isSliderEnabled = settings.isReaderSliderEnabled,
@@ -398,7 +391,7 @@ class ReaderViewModel @Inject constructor(
private fun computePercent(chapterId: Long, pageIndex: Int): Float {
val branch = chaptersLoader.peekChapter(chapterId)?.branch
val chapters = manga?.any?.getChapters(branch) ?: return PROGRESS_NONE
val chapters = manga?.chapters?.get(branch) ?: return PROGRESS_NONE
val chaptersCount = chapters.size
val chapterIndex = chapters.indexOfFirst { x -> x.id == chapterId }
val pagesCount = chaptersLoader.getPagesCount(chapterId)

View File

@@ -118,7 +118,7 @@ class ReaderConfigSheet :
R.id.button_color_filter -> {
val page = viewModel.getCurrentPage() ?: return
val manga = viewModel.manga?.any ?: return
val manga = viewModel.manga?.toManga() ?: return
startActivity(ColorFilterConfigActivity.newIntent(v.context, manga, page))
}
}

View File

@@ -7,17 +7,17 @@ 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.last
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.MangaIntent
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.require
import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase
import org.koitharu.kotatsu.details.domain.DetailsLoadUseCase
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.reader.domain.ChaptersLoader
@@ -28,7 +28,7 @@ class PagesThumbnailsViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
mangaRepositoryFactory: MangaRepository.Factory,
private val chaptersLoader: ChaptersLoader,
doubleMangaLoadUseCase: DoubleMangaLoadUseCase,
detailsLoadUseCase: DetailsLoadUseCase,
) : BaseViewModel() {
private val currentPageIndex: Int =
@@ -37,7 +37,7 @@ class PagesThumbnailsViewModel @Inject constructor(
val manga = savedStateHandle.require<ParcelableManga>(PagesThumbnailsSheet.ARG_MANGA).manga
private val repository = mangaRepositoryFactory.create(manga.source)
private val mangaDetails = doubleMangaLoadUseCase(manga).map {
private val mangaDetails = detailsLoadUseCase(MangaIntent.of(manga)).map {
val b = manga.chapters?.findById(initialChapterId)?.branch
branch.value = b
it.filterChapters(b)
@@ -52,8 +52,7 @@ class PagesThumbnailsViewModel @Inject constructor(
init {
loadingJob = launchLoadingJob(Dispatchers.Default) {
chaptersLoader.init(viewModelScope, mangaDetails.filterNotNull())
mangaDetails.first { x -> x?.hasChapter(initialChapterId) == true }
chaptersLoader.init(checkNotNull(mangaDetails.last()))
chaptersLoader.loadSingleChapter(initialChapterId)
updateList()
}
@@ -79,13 +78,13 @@ class PagesThumbnailsViewModel @Inject constructor(
updateList()
}
private suspend fun updateList() {
private fun updateList() {
val snapshot = chaptersLoader.snapshot()
val pages = buildList(snapshot.size + chaptersLoader.size + 2) {
var previousChapterId = 0L
for (page in snapshot) {
if (page.chapterId != previousChapterId) {
chaptersLoader.awaitChapter(page.chapterId)?.let {
chaptersLoader.peekChapter(page.chapterId)?.let {
add(ListHeader(it.name))
}
previousChapterId = page.chapterId