Refactor manga loading
This commit is contained in:
@@ -4,8 +4,8 @@ import android.util.LongSparseArray
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.koitharu.kotatsu.core.model.DoubleManga
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
import javax.inject.Inject
|
||||
@@ -17,17 +17,27 @@ class ChaptersLoader @Inject constructor(
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
) {
|
||||
|
||||
val chapters = LongSparseArray<MangaChapter>()
|
||||
private val chapters = LongSparseArray<MangaChapter>()
|
||||
private val chapterPages = ChapterPages()
|
||||
private val mutex = Mutex()
|
||||
|
||||
suspend fun loadPrevNextChapter(manga: Manga, currentId: Long, isNext: Boolean) {
|
||||
val size: Int
|
||||
get() = chapters.size()
|
||||
|
||||
suspend fun init(manga: DoubleManga) = mutex.withLock {
|
||||
chapters.clear()
|
||||
manga.chapters?.forEach {
|
||||
chapters.put(it.id, it)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if (index == -1) return
|
||||
val newChapter = chapters.getOrNull(if (isNext) index + 1 else index - 1) ?: return
|
||||
val newPages = loadChapter(manga, newChapter.id)
|
||||
val newPages = loadChapter(newChapter.id)
|
||||
mutex.withLock {
|
||||
if (chapterPages.chaptersSize > 1) {
|
||||
// trim pages
|
||||
@@ -47,14 +57,16 @@ class ChaptersLoader @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadSingleChapter(manga: Manga, chapterId: Long) {
|
||||
val pages = loadChapter(manga, chapterId)
|
||||
suspend fun loadSingleChapter(chapterId: Long) {
|
||||
val pages = loadChapter(chapterId)
|
||||
mutex.withLock {
|
||||
chapterPages.clear()
|
||||
chapterPages.addLast(chapterId, pages)
|
||||
}
|
||||
}
|
||||
|
||||
fun peekChapter(chapterId: Long): MangaChapter? = chapters[chapterId]
|
||||
|
||||
fun getPages(chapterId: Long): List<ReaderPage> {
|
||||
return chapterPages.subList(chapterId)
|
||||
}
|
||||
@@ -69,9 +81,9 @@ class ChaptersLoader @Inject constructor(
|
||||
|
||||
fun snapshot() = chapterPages.toList()
|
||||
|
||||
private suspend fun loadChapter(manga: Manga, chapterId: Long): List<ReaderPage> {
|
||||
private suspend fun loadChapter(chapterId: Long): List<ReaderPage> {
|
||||
val chapter = checkNotNull(chapters[chapterId]) { "Requested chapter not found" }
|
||||
val repo = mangaRepositoryFactory.create(manga.source)
|
||||
val repo = mangaRepositoryFactory.create(chapter.source)
|
||||
return repo.getPages(chapter).mapIndexed { index, page ->
|
||||
ReaderPage(page, index, chapterId)
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ class ReaderActivity :
|
||||
val state = viewModel.getCurrentState() ?: return false
|
||||
PagesThumbnailsSheet.show(
|
||||
supportFragmentManager,
|
||||
viewModel.manga ?: return false,
|
||||
viewModel.manga?.any ?: return false,
|
||||
state.chapterId,
|
||||
state.page,
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.koitharu.kotatsu.reader.ui
|
||||
|
||||
import android.net.Uri
|
||||
import android.util.LongSparseArray
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.annotation.MainThread
|
||||
@@ -16,6 +15,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
@@ -33,6 +33,7 @@ import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
||||
import org.koitharu.kotatsu.core.model.DoubleManga
|
||||
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||
@@ -50,12 +51,11 @@ import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
||||
import org.koitharu.kotatsu.core.util.ext.requireValue
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
|
||||
import org.koitharu.kotatsu.local.domain.DoubleMangaLoader
|
||||
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.reader.data.filterChapters
|
||||
import org.koitharu.kotatsu.reader.domain.ChaptersLoader
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||
@@ -79,6 +79,7 @@ class ReaderViewModel @Inject constructor(
|
||||
private val pageLoader: PageLoader,
|
||||
private val chaptersLoader: ChaptersLoader,
|
||||
private val shortcutsUpdater: ShortcutsUpdater,
|
||||
private val mangaLoader: DoubleMangaLoader,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val intent = MangaIntent(savedStateHandle)
|
||||
@@ -90,9 +91,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)
|
||||
private val chapters: LongSparseArray<MangaChapter>
|
||||
get() = chaptersLoader.chapters
|
||||
private val mangaData = MutableStateFlow(intent.manga?.let { DoubleManga(it) })
|
||||
private val mangaFlow: Flow<Manga?>
|
||||
get() = mangaData.map { it?.any }
|
||||
|
||||
val readerMode = MutableLiveData<ReaderMode>()
|
||||
val onPageSaved = SingleLiveEvent<Uri?>()
|
||||
@@ -100,7 +101,7 @@ class ReaderViewModel @Inject constructor(
|
||||
val uiState = MutableLiveData<ReaderUiState?>(null)
|
||||
|
||||
val content = MutableLiveData(ReaderContent(emptyList(), null))
|
||||
val manga: Manga?
|
||||
val manga: DoubleManga?
|
||||
get() = mangaData.value
|
||||
|
||||
val readerAnimation = settings.observeAsLiveData(
|
||||
@@ -124,13 +125,13 @@ class ReaderViewModel @Inject constructor(
|
||||
val readerSettings = ReaderSettings(
|
||||
parentScope = viewModelScope,
|
||||
settings = settings,
|
||||
colorFilterFlow = mangaData.flatMapLatest {
|
||||
colorFilterFlow = mangaFlow.flatMapLatest {
|
||||
if (it == null) flowOf(null) else dataRepository.observeColorFilter(it.id)
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null),
|
||||
)
|
||||
|
||||
val isScreenshotsBlockEnabled = combine(
|
||||
mangaData,
|
||||
mangaFlow,
|
||||
settings.observeAsFlow(AppSettings.KEY_SCREENSHOTS_POLICY) { screenshotsPolicy },
|
||||
) { manga, policy ->
|
||||
policy == ScreenshotsPolicy.BLOCK_ALL ||
|
||||
@@ -138,7 +139,7 @@ class ReaderViewModel @Inject constructor(
|
||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false)
|
||||
|
||||
val isBookmarkAdded: LiveData<Boolean> = currentState.flatMapLatest { state ->
|
||||
val manga = mangaData.value
|
||||
val manga = mangaData.value?.any
|
||||
if (state == null || manga == null) {
|
||||
flowOf(false)
|
||||
} else {
|
||||
@@ -154,7 +155,7 @@ class ReaderViewModel @Inject constructor(
|
||||
if (key == AppSettings.KEY_READER_SLIDER) notifyStateChanged()
|
||||
}.launchIn(viewModelScope + Dispatchers.Default)
|
||||
launchJob(Dispatchers.Default) {
|
||||
val mangaId = mangaData.filterNotNull().first().id
|
||||
val mangaId = mangaFlow.filterNotNull().first().id
|
||||
shortcutsUpdater.notifyMangaOpened(mangaId)
|
||||
}
|
||||
}
|
||||
@@ -166,7 +167,7 @@ class ReaderViewModel @Inject constructor(
|
||||
|
||||
fun switchMode(newMode: ReaderMode) {
|
||||
launchJob {
|
||||
val manga = checkNotNull(mangaData.value)
|
||||
val manga = checkNotNull(mangaData.value?.any)
|
||||
dataRepository.saveReaderMode(
|
||||
manga = manga,
|
||||
mode = newMode,
|
||||
@@ -189,7 +190,7 @@ class ReaderViewModel @Inject constructor(
|
||||
}
|
||||
val readerState = state ?: currentState.value ?: return
|
||||
historyRepository.saveStateAsync(
|
||||
manga = mangaData.value ?: return,
|
||||
manga = mangaData.value?.any ?: return,
|
||||
state = readerState,
|
||||
percent = computePercent(readerState.chapterId, readerState.page),
|
||||
)
|
||||
@@ -242,7 +243,7 @@ class ReaderViewModel @Inject constructor(
|
||||
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
||||
prevJob?.cancelAndJoin()
|
||||
content.postValue(ReaderContent(emptyList(), null))
|
||||
chaptersLoader.loadSingleChapter(mangaData.requireValue(), id)
|
||||
chaptersLoader.loadSingleChapter(id)
|
||||
content.postValue(ReaderContent(chaptersLoader.snapshot(), ReaderState(id, page, 0)))
|
||||
}
|
||||
}
|
||||
@@ -285,7 +286,7 @@ class ReaderViewModel @Inject constructor(
|
||||
val state = checkNotNull(currentState.value)
|
||||
val page = checkNotNull(getCurrentPage()) { "Page not found" }
|
||||
val bookmark = Bookmark(
|
||||
manga = checkNotNull(mangaData.value),
|
||||
manga = checkNotNull(mangaData.value?.any),
|
||||
pageId = page.id,
|
||||
chapterId = state.chapterId,
|
||||
page = state.page,
|
||||
@@ -305,7 +306,7 @@ class ReaderViewModel @Inject constructor(
|
||||
}
|
||||
bookmarkJob = launchJob {
|
||||
loadingJob?.join()
|
||||
val manga = checkNotNull(mangaData.value)
|
||||
val manga = checkNotNull(mangaData.value?.any)
|
||||
val page = checkNotNull(getCurrentPage()) { "Page not found" }
|
||||
bookmarksRepository.removeBookmark(manga.id, page.id)
|
||||
onShowToast.call(R.string.bookmark_removed)
|
||||
@@ -314,32 +315,31 @@ class ReaderViewModel @Inject constructor(
|
||||
|
||||
private fun loadImpl() {
|
||||
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
||||
var manga = dataRepository.resolveIntent(intent) ?: throw NotFoundException("Cannot find manga", "")
|
||||
var manga =
|
||||
DoubleManga(dataRepository.resolveIntent(intent) ?: throw NotFoundException("Cannot find manga", ""))
|
||||
mangaData.value = manga
|
||||
val repo = mangaRepositoryFactory.create(manga.source)
|
||||
manga = repo.getDetails(manga)
|
||||
manga.chapters?.forEach {
|
||||
chapters.put(it.id, it)
|
||||
}
|
||||
manga = mangaLoader.load(intent)
|
||||
chaptersLoader.init(manga)
|
||||
// determine mode
|
||||
val mode = detectReaderMode(manga, repo)
|
||||
val singleManga = manga.requireAny()
|
||||
val mode = detectReaderMode(singleManga)
|
||||
// obtain state
|
||||
if (currentState.value == null) {
|
||||
currentState.value = historyRepository.getOne(manga)?.let {
|
||||
currentState.value = historyRepository.getOne(singleManga)?.let {
|
||||
ReaderState(it)
|
||||
} ?: ReaderState(manga, preselectedBranch)
|
||||
} ?: ReaderState(singleManga, preselectedBranch)
|
||||
}
|
||||
|
||||
val branch = chapters[currentState.value?.chapterId ?: 0L]?.branch
|
||||
val branch = chaptersLoader.peekChapter(currentState.value?.chapterId ?: 0L)?.branch
|
||||
mangaData.value = manga.filterChapters(branch)
|
||||
readerMode.emitValue(mode)
|
||||
|
||||
chaptersLoader.loadSingleChapter(manga, requireNotNull(currentState.value).chapterId)
|
||||
chaptersLoader.loadSingleChapter(requireNotNull(currentState.value).chapterId)
|
||||
// save state
|
||||
if (!isIncognito) {
|
||||
currentState.value?.let {
|
||||
val percent = computePercent(it.chapterId, it.page)
|
||||
historyRepository.addOrUpdate(manga, it.chapterId, it.page, it.scroll, percent)
|
||||
historyRepository.addOrUpdate(singleManga, it.chapterId, it.page, it.scroll, percent)
|
||||
}
|
||||
}
|
||||
notifyStateChanged()
|
||||
@@ -367,15 +367,16 @@ class ReaderViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun detectReaderMode(manga: Manga, repo: MangaRepository): ReaderMode {
|
||||
private suspend fun detectReaderMode(manga: Manga): ReaderMode {
|
||||
dataRepository.getReaderMode(manga.id)?.let { return it }
|
||||
val defaultMode = settings.defaultReaderMode
|
||||
if (!settings.isReaderModeDetectionEnabled || defaultMode == ReaderMode.WEBTOON) {
|
||||
return defaultMode
|
||||
}
|
||||
val chapter = currentState.value?.chapterId?.let(chapters::get)
|
||||
val chapter = currentState.value?.chapterId?.let { chaptersLoader.peekChapter(it) }
|
||||
?: manga.chapters?.randomOrNull()
|
||||
?: error("There are no chapters in this manga")
|
||||
val repo = mangaRepositoryFactory.create(manga.source)
|
||||
val pages = repo.getPages(chapter)
|
||||
return runCatchingCancellable {
|
||||
val isWebtoon = dataRepository.determineMangaIsWebtoon(repo, pages)
|
||||
@@ -390,12 +391,12 @@ class ReaderViewModel @Inject constructor(
|
||||
@WorkerThread
|
||||
private fun notifyStateChanged() {
|
||||
val state = getCurrentState()
|
||||
val chapter = state?.chapterId?.let(chapters::get)
|
||||
val chapter = state?.chapterId?.let { chaptersLoader.peekChapter(it) }
|
||||
val newState = ReaderUiState(
|
||||
mangaName = manga?.title,
|
||||
mangaName = manga?.any?.title,
|
||||
chapterName = chapter?.name,
|
||||
chapterNumber = chapter?.number ?: 0,
|
||||
chaptersTotal = manga?.getChapters(chapter?.branch)?.size ?: 0,
|
||||
chaptersTotal = manga?.any?.getChapters(chapter?.branch)?.size ?: 0,
|
||||
totalPages = if (chapter != null) chaptersLoader.getPagesCount(chapter.id) else 0,
|
||||
currentPage = state?.page ?: 0,
|
||||
isSliderEnabled = settings.isReaderSliderEnabled,
|
||||
@@ -405,8 +406,8 @@ class ReaderViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun computePercent(chapterId: Long, pageIndex: Int): Float {
|
||||
val branch = chapters[chapterId]?.branch
|
||||
val chapters = manga?.getChapters(branch) ?: return PROGRESS_NONE
|
||||
val branch = chaptersLoader.peekChapter(chapterId)?.branch
|
||||
val chapters = manga?.any?.getChapters(branch) ?: return PROGRESS_NONE
|
||||
val chaptersCount = chapters.size
|
||||
val chapterIndex = chapters.indexOfFirst { x -> x.id == chapterId }
|
||||
val pagesCount = chaptersLoader.getPagesCount(chapterId)
|
||||
|
||||
@@ -108,7 +108,7 @@ class ReaderConfigBottomSheet :
|
||||
|
||||
R.id.button_color_filter -> {
|
||||
val page = viewModel.getCurrentPage() ?: return
|
||||
val manga = viewModel.manga ?: return
|
||||
val manga = viewModel.manga?.any ?: return
|
||||
startActivity(ColorFilterConfigActivity.newIntent(v.context, manga, page))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ import org.koitharu.kotatsu.core.util.ext.emitValue
|
||||
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.local.domain.DoubleMangaLoader
|
||||
import org.koitharu.kotatsu.parsers.util.SuspendLazy
|
||||
import org.koitharu.kotatsu.reader.data.filterChapters
|
||||
import org.koitharu.kotatsu.reader.domain.ChaptersLoader
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -22,6 +22,7 @@ class PagesThumbnailsViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
mangaRepositoryFactory: MangaRepository.Factory,
|
||||
private val chaptersLoader: ChaptersLoader,
|
||||
private val mangaLoader: DoubleMangaLoader,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val currentPageIndex: Int = savedStateHandle[PagesThumbnailsSheet.ARG_CURRENT_PAGE] ?: -1
|
||||
@@ -30,13 +31,9 @@ class PagesThumbnailsViewModel @Inject constructor(
|
||||
|
||||
private val repository = mangaRepositoryFactory.create(manga.source)
|
||||
private val mangaDetails = SuspendLazy {
|
||||
repository.getDetails(manga).let {
|
||||
chaptersLoader.chapters.clear()
|
||||
mangaLoader.load(manga).let {
|
||||
val b = manga.chapters?.find { ch -> ch.id == initialChapterId }?.branch
|
||||
branch.emitValue(b)
|
||||
it.getChapters(b)?.forEach { ch ->
|
||||
chaptersLoader.chapters.put(ch.id, ch)
|
||||
}
|
||||
it.filterChapters(b)
|
||||
}
|
||||
}
|
||||
@@ -50,7 +47,8 @@ class PagesThumbnailsViewModel @Inject constructor(
|
||||
|
||||
init {
|
||||
loadingJob = launchJob(Dispatchers.Default) {
|
||||
chaptersLoader.loadSingleChapter(mangaDetails.get(), initialChapterId)
|
||||
chaptersLoader.init(mangaDetails.get())
|
||||
chaptersLoader.loadSingleChapter(initialChapterId)
|
||||
updateList()
|
||||
}
|
||||
}
|
||||
@@ -80,14 +78,14 @@ class PagesThumbnailsViewModel @Inject constructor(
|
||||
val mangaChapters = mangaDetails.tryGet().getOrNull()?.chapters.orEmpty()
|
||||
val hasPrevChapter = snapshot.firstOrNull()?.chapterId != mangaChapters.firstOrNull()?.id
|
||||
val hasNextChapter = snapshot.lastOrNull()?.chapterId != mangaChapters.lastOrNull()?.id
|
||||
val pages = buildList(snapshot.size + chaptersLoader.chapters.size() + 2) {
|
||||
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.chapters[page.chapterId]?.let {
|
||||
chaptersLoader.peekChapter(page.chapterId)?.let {
|
||||
add(ListHeader(it.name, 0, null))
|
||||
}
|
||||
previousChapterId = page.chapterId
|
||||
|
||||
Reference in New Issue
Block a user