Refactor details and reader ViewModels

This commit is contained in:
Koitharu
2024-08-31 12:09:22 +03:00
parent eb49b31aeb
commit 0cc019ef19
18 changed files with 378 additions and 275 deletions

View File

@@ -17,7 +17,7 @@ android {
minSdk = 21 minSdk = 21
targetSdk = 35 targetSdk = 35
versionCode = 665 versionCode = 665
versionName = '7.5-b1' versionName = '7.5-b2'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner' testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
ksp { ksp {

View File

@@ -8,6 +8,7 @@ import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.local.data.PagesCache import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.ReaderViewModel
class KotatsuApp : BaseApp() { class KotatsuApp : BaseApp() {
@@ -30,6 +31,7 @@ class KotatsuApp : BaseApp() {
.setClassInstanceLimit(PagesCache::class.java, 1) .setClassInstanceLimit(PagesCache::class.java, 1)
.setClassInstanceLimit(MangaLoaderContext::class.java, 1) .setClassInstanceLimit(MangaLoaderContext::class.java, 1)
.setClassInstanceLimit(PageLoader::class.java, 1) .setClassInstanceLimit(PageLoader::class.java, 1)
.setClassInstanceLimit(ReaderViewModel::class.java, 1)
.penaltyLog() .penaltyLog()
.build(), .build(),
) )

View File

@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.details.ui
import android.content.Context import android.content.Context
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.details.data.MangaDetails import org.koitharu.kotatsu.details.data.MangaDetails
import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.toListItem import org.koitharu.kotatsu.details.ui.model.toListItem
@@ -12,7 +11,7 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
fun MangaDetails.mapChapters( fun MangaDetails.mapChapters(
history: MangaHistory?, currentChapterId: Long,
newCount: Int, newCount: Int,
branch: String?, branch: String?,
bookmarks: List<Bookmark>, bookmarks: List<Bookmark>,
@@ -24,7 +23,6 @@ fun MangaDetails.mapChapters(
return emptyList() return emptyList()
} }
val bookmarked = bookmarks.mapToSet { it.chapterId } val bookmarked = bookmarks.mapToSet { it.chapterId }
val currentId = history?.chapterId ?: 0L
val newFrom = if (newCount == 0 || remoteChapters.isEmpty()) Int.MAX_VALUE else remoteChapters.size - newCount val newFrom = if (newCount == 0 || remoteChapters.isEmpty()) Int.MAX_VALUE else remoteChapters.size - newCount
val ids = buildSet(maxOf(remoteChapters.size, localChapters.size)) { val ids = buildSet(maxOf(remoteChapters.size, localChapters.size)) {
remoteChapters.mapTo(this) { it.id } remoteChapters.mapTo(this) { it.id }
@@ -36,14 +34,14 @@ fun MangaDetails.mapChapters(
} else { } else {
null null
} }
var isUnread = currentId !in ids var isUnread = currentChapterId !in ids
for (chapter in remoteChapters) { for (chapter in remoteChapters) {
val local = localMap?.remove(chapter.id) val local = localMap?.remove(chapter.id)
if (chapter.id == currentId) { if (chapter.id == currentChapterId) {
isUnread = true isUnread = true
} }
result += (local ?: chapter).toListItem( result += (local ?: chapter).toListItem(
isCurrent = chapter.id == currentId, isCurrent = chapter.id == currentChapterId,
isUnread = isUnread, isUnread = isUnread,
isNew = isUnread && result.size >= newFrom, isNew = isUnread && result.size >= newFrom,
isDownloaded = local != null, isDownloaded = local != null,
@@ -53,11 +51,11 @@ fun MangaDetails.mapChapters(
} }
if (!localMap.isNullOrEmpty()) { if (!localMap.isNullOrEmpty()) {
for (chapter in localMap.values) { for (chapter in localMap.values) {
if (chapter.id == currentId) { if (chapter.id == currentChapterId) {
isUnread = true isUnread = true
} }
result += chapter.toListItem( result += chapter.toListItem(
isCurrent = chapter.id == currentId, isCurrent = chapter.id == currentChapterId,
isUnread = isUnread, isUnread = isUnread,
isNew = false, isNew = false,
isDownloaded = !isLocal, isDownloaded = !isLocal,

View File

@@ -72,7 +72,6 @@ import org.koitharu.kotatsu.core.util.ext.joinToStringWithLimit
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.parentView 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.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat
import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat
@@ -162,7 +161,7 @@ class DetailsActivity :
} }
TitleExpandListener(viewBinding.textViewTitle).attach() TitleExpandListener(viewBinding.textViewTitle).attach()
viewModel.details.filterNotNull().observe(this, ::onMangaUpdated) viewModel.mangaDetails.filterNotNull().observe(this, ::onMangaUpdated)
viewModel.onMangaRemoved.observeEvent(this, ::onMangaRemoved) viewModel.onMangaRemoved.observeEvent(this, ::onMangaRemoved)
viewModel.onError viewModel.onError
.filterNot { ChaptersPagesSheet.isShown(supportFragmentManager) } .filterNot { ChaptersPagesSheet.isShown(supportFragmentManager) }

View File

@@ -12,32 +12,23 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import okio.FileNotFoundException
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
import org.koitharu.kotatsu.core.model.findById import org.koitharu.kotatsu.core.model.findById
import org.koitharu.kotatsu.core.model.getPreferredBranch import org.koitharu.kotatsu.core.model.getPreferredBranch
import org.koitharu.kotatsu.core.parser.MangaIntent import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.util.ReversibleAction import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call 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.computeSize
import org.koitharu.kotatsu.core.util.ext.onEachWhile import org.koitharu.kotatsu.core.util.ext.onEachWhile
import org.koitharu.kotatsu.core.util.ext.requireValue
import org.koitharu.kotatsu.details.data.MangaDetails import org.koitharu.kotatsu.details.data.MangaDetails
import org.koitharu.kotatsu.details.domain.BranchComparator import org.koitharu.kotatsu.details.domain.BranchComparator
import org.koitharu.kotatsu.details.domain.DetailsInteractor import org.koitharu.kotatsu.details.domain.DetailsInteractor
@@ -45,9 +36,9 @@ import org.koitharu.kotatsu.details.domain.DetailsLoadUseCase
import org.koitharu.kotatsu.details.domain.ProgressUpdateUseCase import org.koitharu.kotatsu.details.domain.ProgressUpdateUseCase
import org.koitharu.kotatsu.details.domain.ReadingTimeUseCase import org.koitharu.kotatsu.details.domain.ReadingTimeUseCase
import org.koitharu.kotatsu.details.domain.RelatedMangaUseCase import org.koitharu.kotatsu.details.domain.RelatedMangaUseCase
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.HistoryInfo import org.koitharu.kotatsu.details.ui.model.HistoryInfo
import org.koitharu.kotatsu.details.ui.model.MangaBranch import org.koitharu.kotatsu.details.ui.model.MangaBranch
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesViewModel
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.list.domain.MangaListMapper import org.koitharu.kotatsu.list.domain.MangaListMapper
@@ -57,6 +48,7 @@ import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase
import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
@@ -66,37 +58,42 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class DetailsViewModel @Inject constructor( class DetailsViewModel @Inject constructor(
private val historyRepository: HistoryRepository, private val historyRepository: HistoryRepository,
private val bookmarksRepository: BookmarksRepository, bookmarksRepository: BookmarksRepository,
private val settings: AppSettings, settings: AppSettings,
private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>, private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>, @LocalStorageChanges localStorageChanges: SharedFlow<LocalManga?>,
private val downloadScheduler: DownloadWorker.Scheduler, downloadScheduler: DownloadWorker.Scheduler,
private val interactor: DetailsInteractor, private val interactor: DetailsInteractor,
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase, deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
private val relatedMangaUseCase: RelatedMangaUseCase, private val relatedMangaUseCase: RelatedMangaUseCase,
private val mangaListMapper: MangaListMapper, private val mangaListMapper: MangaListMapper,
private val detailsLoadUseCase: DetailsLoadUseCase, private val detailsLoadUseCase: DetailsLoadUseCase,
private val progressUpdateUseCase: ProgressUpdateUseCase, private val progressUpdateUseCase: ProgressUpdateUseCase,
private val readingTimeUseCase: ReadingTimeUseCase, private val readingTimeUseCase: ReadingTimeUseCase,
private val statsRepository: StatsRepository, statsRepository: StatsRepository,
) : BaseViewModel() { ) : ChaptersPagesViewModel(
settings = settings,
interactor = interactor,
bookmarksRepository = bookmarksRepository,
historyRepository = historyRepository,
downloadScheduler = downloadScheduler,
deleteLocalMangaUseCase = deleteLocalMangaUseCase,
localStorageChanges = localStorageChanges,
) {
private val intent = MangaIntent(savedStateHandle) private val intent = MangaIntent(savedStateHandle)
private var loadingJob: Job private var loadingJob: Job
val mangaId = intent.mangaId val mangaId = intent.mangaId
val onActionDone = MutableEventFlow<ReversibleAction>() init {
val onSelectChapter = MutableEventFlow<Long>() mangaDetails.value = intent.manga?.let { MangaDetails(it, null, null, false) }
val onDownloadStarted = MutableEventFlow<Unit>() }
val details = MutableStateFlow(intent.manga?.let { MangaDetails(it, null, null, false) })
val manga = details.map { x -> x?.toManga() }
.withErrorHandling()
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
val history = historyRepository.observeOne(mangaId) val history = historyRepository.observeOne(mangaId)
.withErrorHandling() .onEach { h ->
readingState.value = h?.let(::ReaderState)
}.withErrorHandling()
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null) .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
val favouriteCategories = interactor.observeFavourite(mangaId) val favouriteCategories = interactor.observeFavourite(mangaId)
@@ -109,31 +106,8 @@ class DetailsViewModel @Inject constructor(
val remoteManga = MutableStateFlow<Manga?>(null) val remoteManga = MutableStateFlow<Manga?>(null)
val newChaptersCount = details.flatMapLatest { d ->
if (d?.isLocal == false) {
interactor.observeNewChapters(mangaId)
} else {
flowOf(0)
}
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
private val chaptersQuery = MutableStateFlow("")
val selectedBranch = MutableStateFlow<String?>(null)
val isChaptersReversed = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.Default,
key = AppSettings.KEY_REVERSE_CHAPTERS,
valueProducer = { isChaptersReverse },
)
val isChaptersInGridView = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.Default,
key = AppSettings.KEY_GRID_VIEW_CHAPTERS,
valueProducer = { isChaptersGridView },
)
val historyInfo: StateFlow<HistoryInfo> = combine( val historyInfo: StateFlow<HistoryInfo> = combine(
details, mangaDetails,
selectedBranch, selectedBranch,
history, history,
interactor.observeIncognitoMode(manga), interactor.observeIncognitoMode(manga),
@@ -145,11 +119,7 @@ class DetailsViewModel @Inject constructor(
initialValue = HistoryInfo(null, null, null, false), initialValue = HistoryInfo(null, null, null, false),
) )
val bookmarks = manga.flatMapLatest { val localSize = mangaDetails
if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList())
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, emptyList())
val localSize = details
.map { it?.local } .map { it?.local }
.distinctUntilChanged() .distinctUntilChanged()
.combine(localStorageChanges.onStart { emit(null) }) { x, _ -> x } .combine(localStorageChanges.onStart { emit(null) }) { x, _ -> x }
@@ -163,7 +133,6 @@ class DetailsViewModel @Inject constructor(
} }
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(5000), 0L) }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(5000), 0L)
val onMangaRemoved = MutableEventFlow<Manga>()
val isScrobblingAvailable: Boolean val isScrobblingAvailable: Boolean
get() = scrobblers.any { it.isEnabled } get() = scrobblers.any { it.isEnabled }
@@ -182,7 +151,7 @@ class DetailsViewModel @Inject constructor(
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, emptyList()) }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, emptyList())
val branches: StateFlow<List<MangaBranch>> = combine( val branches: StateFlow<List<MangaBranch>> = combine(
details, mangaDetails,
selectedBranch, selectedBranch,
history, history,
) { m, b, h -> ) { m, b, h ->
@@ -201,35 +170,8 @@ class DetailsViewModel @Inject constructor(
}.sortedWith(BranchComparator()) }.sortedWith(BranchComparator())
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList()) }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
val isChaptersEmpty: StateFlow<Boolean> = details.map {
it != null && it.isLoaded && it.allChapters.isEmpty()
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
val chapters = combine(
combine(
details,
history,
selectedBranch,
newChaptersCount,
bookmarks,
isChaptersInGridView,
) { manga, history, branch, news, bookmarks, grid ->
manga?.mapChapters(
history,
news,
branch,
bookmarks,
grid,
).orEmpty()
},
isChaptersReversed,
chaptersQuery,
) { list, reversed, query ->
(if (reversed) list.asReversed() else list).filterSearch(query)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
val readingTime = combine( val readingTime = combine(
details, mangaDetails,
selectedBranch, selectedBranch,
history, history,
) { m, b, h -> ) { m, b, h ->
@@ -242,18 +184,14 @@ class DetailsViewModel @Inject constructor(
init { init {
loadingJob = doLoad() loadingJob = doLoad()
launchJob(Dispatchers.Default) { launchJob(Dispatchers.Default) {
localStorageChanges val manga = mangaDetails.firstOrNull { !it?.chapters.isNullOrEmpty() } ?: return@launchJob
.collect { onDownloadComplete(it) }
}
launchJob(Dispatchers.Default) {
val manga = details.firstOrNull { !it?.chapters.isNullOrEmpty() } ?: return@launchJob
val h = history.firstOrNull() val h = history.firstOrNull()
if (h != null) { if (h != null) {
progressUpdateUseCase(manga.toManga()) progressUpdateUseCase(manga.toManga())
} }
} }
launchJob(Dispatchers.Default) { launchJob(Dispatchers.Default) {
val manga = details.firstOrNull { it != null && it.isLocal } ?: return@launchJob val manga = mangaDetails.firstOrNull { it != null && it.isLocal } ?: return@launchJob
remoteManga.value = interactor.findRemote(manga.toManga()) remoteManga.value = interactor.findRemote(manga.toManga())
} }
} }
@@ -263,41 +201,6 @@ class DetailsViewModel @Inject constructor(
loadingJob = doLoad() loadingJob = doLoad()
} }
fun deleteLocal() {
val m = details.value?.local?.manga
if (m == null) {
errorEvent.call(FileNotFoundException())
return
}
launchLoadingJob(Dispatchers.Default) {
deleteLocalMangaUseCase(m)
onMangaRemoved.call(m)
}
}
fun removeBookmark(bookmark: Bookmark) {
launchJob(Dispatchers.Default) {
bookmarksRepository.removeBookmark(bookmark)
onActionDone.call(ReversibleAction(R.string.bookmark_removed, null))
}
}
fun setChaptersReversed(newValue: Boolean) {
settings.isChaptersReverse = newValue
}
fun setChaptersInGridView(newValue: Boolean) {
settings.isChaptersGridView = newValue
}
fun setSelectedBranch(branch: String?) {
selectedBranch.value = branch
}
fun performChapterSearch(query: String?) {
chaptersQuery.value = query?.trim().orEmpty()
}
fun updateScrobbling(index: Int, rating: Float, status: ScrobblingStatus?) { fun updateScrobbling(index: Int, rating: Float, status: ScrobblingStatus?) {
val scrobbler = getScrobbler(index) ?: return val scrobbler = getScrobbler(index) ?: return
launchJob(Dispatchers.Default) { launchJob(Dispatchers.Default) {
@@ -319,34 +222,6 @@ class DetailsViewModel @Inject constructor(
} }
} }
fun markChapterAsCurrent(chapterId: Long) {
launchJob(Dispatchers.Default) {
val manga = checkNotNull(details.value)
val chapters = checkNotNull(manga.chapters[selectedBranchValue])
val chapterIndex = chapters.indexOfFirst { it.id == chapterId }
check(chapterIndex in chapters.indices) { "Chapter not found" }
val percent = chapterIndex / chapters.size.toFloat()
historyRepository.addOrUpdate(
manga = manga.toManga(),
chapterId = chapterId,
page = 0,
scroll = 0,
percent = percent,
force = true,
)
}
}
fun download(chaptersIds: Set<Long>?) {
launchJob(Dispatchers.Default) {
downloadScheduler.schedule(
details.requireValue().toManga(),
chaptersIds,
)
onDownloadStarted.call(Unit)
}
}
fun startChaptersSelection() { fun startChaptersSelection() {
val chapters = chapters.value val chapters = chapters.value
val chapter = chapters.find { val chapter = chapters.find {
@@ -374,28 +249,10 @@ class DetailsViewModel @Inject constructor(
selectedBranch.value = manga.getPreferredBranch(hist) selectedBranch.value = manga.getPreferredBranch(hist)
true true
}.collect { }.collect {
details.value = it mangaDetails.value = it
} }
} }
private fun List<ChapterListItem>.filterSearch(query: String): List<ChapterListItem> {
if (query.isEmpty() || this.isEmpty()) {
return this
}
return filter {
it.chapter.name.contains(query, ignoreCase = true)
}
}
private suspend fun onDownloadComplete(downloadedManga: LocalManga?) {
downloadedManga ?: return
launchJob {
details.update {
interactor.updateLocal(it, downloadedManga)
}
}
}
private fun getScrobbler(index: Int): Scrobbler? { private fun getScrobbler(index: Int): Scrobbler? {
val info = scrobblingInfo.value.getOrNull(index) val info = scrobblingInfo.value.getOrNull(index)
val scrobbler = if (info != null) { val scrobbler = if (info != null) {

View File

@@ -14,14 +14,13 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.util.ext.setValueRounded import org.koitharu.kotatsu.core.util.ext.setValueRounded
import org.koitharu.kotatsu.core.util.progress.IntPercentLabelFormatter import org.koitharu.kotatsu.core.util.progress.IntPercentLabelFormatter
import org.koitharu.kotatsu.details.ui.DetailsViewModel
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet.Companion.TAB_BOOKMARKS import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet.Companion.TAB_BOOKMARKS
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet.Companion.TAB_CHAPTERS import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet.Companion.TAB_CHAPTERS
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet.Companion.TAB_PAGES import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet.Companion.TAB_PAGES
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
class ChapterPagesMenuProvider( class ChapterPagesMenuProvider(
private val viewModel: DetailsViewModel, private val viewModel: ChaptersPagesViewModel,
private val sheet: BaseAdaptiveSheet<*>, private val sheet: BaseAdaptiveSheet<*>,
private val pager: ViewPager2, private val pager: ViewPager2,
private val settings: AppSettings, private val settings: AppSettings,

View File

@@ -7,7 +7,6 @@ import android.view.ViewGroup
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.activityViewModels
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
@@ -30,7 +29,6 @@ import org.koitharu.kotatsu.core.util.ext.setTabsEnabled
import org.koitharu.kotatsu.core.util.ext.showDistinct import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.SheetChaptersPagesBinding import org.koitharu.kotatsu.databinding.SheetChaptersPagesBinding
import org.koitharu.kotatsu.details.ui.DetailsViewModel
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
import javax.inject.Inject import javax.inject.Inject
@@ -40,7 +38,7 @@ class ChaptersPagesSheet : BaseAdaptiveSheet<SheetChaptersPagesBinding>(), Actio
@Inject @Inject
lateinit var settings: AppSettings lateinit var settings: AppSettings
private val viewModel by activityViewModels<DetailsViewModel>() private val viewModel by ChaptersPagesViewModel.ActivityVMLazy(this)
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetChaptersPagesBinding { override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetChaptersPagesBinding {
return SheetChaptersPagesBinding.inflate(inflater, container, false) return SheetChaptersPagesBinding.inflate(inflater, container, false)

View File

@@ -0,0 +1,233 @@
package org.koitharu.kotatsu.details.ui.pager
import android.app.Activity
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.plus
import okio.FileNotFoundException
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
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.requireValue
import org.koitharu.kotatsu.details.data.MangaDetails
import org.koitharu.kotatsu.details.domain.DetailsInteractor
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.details.ui.DetailsViewModel
import org.koitharu.kotatsu.details.ui.mapChapters
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.ReaderViewModel
abstract class ChaptersPagesViewModel(
@JvmField protected val settings: AppSettings,
private val interactor: DetailsInteractor,
private val bookmarksRepository: BookmarksRepository,
private val historyRepository: HistoryRepository,
private val downloadScheduler: DownloadWorker.Scheduler,
private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
private val localStorageChanges: SharedFlow<LocalManga?>,
) : BaseViewModel() {
val mangaDetails = MutableStateFlow<MangaDetails?>(null)
val readingState = MutableStateFlow<ReaderState?>(null)
val onActionDone = MutableEventFlow<ReversibleAction>()
val onSelectChapter = MutableEventFlow<Long>()
val onDownloadStarted = MutableEventFlow<Unit>()
val onMangaRemoved = MutableEventFlow<Manga>()
private val chaptersQuery = MutableStateFlow("")
val selectedBranch = MutableStateFlow<String?>(null)
val manga = mangaDetails.map { x -> x?.toManga() }
.withErrorHandling()
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
val isChaptersReversed = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.Default,
key = AppSettings.KEY_REVERSE_CHAPTERS,
valueProducer = { isChaptersReverse },
)
val isChaptersInGridView = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.Default,
key = AppSettings.KEY_GRID_VIEW_CHAPTERS,
valueProducer = { isChaptersGridView },
)
val newChaptersCount = mangaDetails.flatMapLatest { d ->
if (d?.isLocal == false) {
interactor.observeNewChapters(d.id)
} else {
flowOf(0)
}
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
val isChaptersEmpty: StateFlow<Boolean> = mangaDetails.map {
it != null && it.isLoaded && it.allChapters.isEmpty()
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
val bookmarks = mangaDetails.flatMapLatest {
if (it != null) bookmarksRepository.observeBookmarks(it.toManga()) else flowOf(emptyList())
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, emptyList())
val chapters = combine(
combine(
mangaDetails,
readingState.map { it?.chapterId ?: 0L }.distinctUntilChanged(),
selectedBranch,
newChaptersCount,
bookmarks,
isChaptersInGridView,
) { manga, currentChapterId, branch, news, bookmarks, grid ->
manga?.mapChapters(
currentChapterId,
news,
branch,
bookmarks,
grid,
).orEmpty()
},
isChaptersReversed,
chaptersQuery,
) { list, reversed, query ->
(if (reversed) list.asReversed() else list).filterSearch(query)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
init {
launchJob(Dispatchers.Default) {
localStorageChanges
.collect { onDownloadComplete(it) }
}
}
fun setChaptersReversed(newValue: Boolean) {
settings.isChaptersReverse = newValue
}
fun setChaptersInGridView(newValue: Boolean) {
settings.isChaptersGridView = newValue
}
fun setSelectedBranch(branch: String?) {
selectedBranch.value = branch
}
fun performChapterSearch(query: String?) {
chaptersQuery.value = query?.trim().orEmpty()
}
fun getMangaOrNull(): Manga? = mangaDetails.value?.toManga()
fun requireManga() = mangaDetails.requireValue().toManga()
fun markChapterAsCurrent(chapterId: Long) {
launchJob(Dispatchers.Default) {
val manga = mangaDetails.requireValue()
val chapters = checkNotNull(manga.chapters[selectedBranch.value])
val chapterIndex = chapters.indexOfFirst { it.id == chapterId }
check(chapterIndex in chapters.indices) { "Chapter not found" }
val percent = chapterIndex / chapters.size.toFloat()
historyRepository.addOrUpdate(
manga = manga.toManga(),
chapterId = chapterId,
page = 0,
scroll = 0,
percent = percent,
force = true,
)
}
}
fun download(chaptersIds: Set<Long>?) {
launchJob(Dispatchers.Default) {
downloadScheduler.schedule(
requireManga(),
chaptersIds,
)
onDownloadStarted.call(Unit)
}
}
fun deleteLocal() {
val m = mangaDetails.value?.local?.manga
if (m == null) {
errorEvent.call(FileNotFoundException())
return
}
launchLoadingJob(Dispatchers.Default) {
deleteLocalMangaUseCase(m)
onMangaRemoved.call(m)
}
}
private fun List<ChapterListItem>.filterSearch(query: String): List<ChapterListItem> {
if (query.isEmpty() || this.isEmpty()) {
return this
}
return filter {
it.chapter.name.contains(query, ignoreCase = true)
}
}
private suspend fun onDownloadComplete(downloadedManga: LocalManga?) {
downloadedManga ?: return
mangaDetails.update {
interactor.updateLocal(it, downloadedManga)
}
}
class ActivityVMLazy(
private val fragment: Fragment,
) : Lazy<ChaptersPagesViewModel> {
private var cached: ChaptersPagesViewModel? = null
override val value: ChaptersPagesViewModel
get() {
val viewModel = cached
return if (viewModel == null) {
val activity = fragment.requireActivity()
val vmClass = getViewModelClass(activity)
ViewModelProvider.create(
store = activity.viewModelStore,
factory = activity.defaultViewModelProviderFactory,
extras = activity.defaultViewModelCreationExtras,
)[vmClass].also { cached = it }
} else {
viewModel
}
}
override fun isInitialized(): Boolean = cached != null
private fun getViewModelClass(activity: Activity) = when (activity) {
is ReaderActivity -> ReaderViewModel::class.java
is DetailsActivity -> DetailsViewModel::class.java
else -> error("Wrong activity ${activity.javaClass.simpleName} for ${ChaptersPagesViewModel::class.java.simpleName}")
}
}
}

View File

@@ -8,7 +8,6 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import coil.ImageLoader import coil.ImageLoader
@@ -30,7 +29,7 @@ import org.koitharu.kotatsu.core.util.ext.findParentCallback
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.databinding.FragmentMangaBookmarksBinding import org.koitharu.kotatsu.databinding.FragmentMangaBookmarksBinding
import org.koitharu.kotatsu.details.ui.DetailsViewModel import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesViewModel
import org.koitharu.kotatsu.list.ui.GridSpanResolver import org.koitharu.kotatsu.list.ui.GridSpanResolver
import org.koitharu.kotatsu.list.ui.adapter.ListItemType import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
@@ -42,7 +41,7 @@ import javax.inject.Inject
class BookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(), class BookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(),
OnListItemClickListener<Bookmark>, ListSelectionController.Callback { OnListItemClickListener<Bookmark>, ListSelectionController.Callback {
private val activityViewModel by activityViewModels<DetailsViewModel>() private val activityViewModel by ChaptersPagesViewModel.ActivityVMLazy(this)
private val viewModel by viewModels<BookmarksViewModel>() private val viewModel by viewModels<BookmarksViewModel>()
@Inject @Inject
@@ -62,7 +61,7 @@ class BookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(),
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
activityViewModel.manga.observe(this, viewModel) activityViewModel.mangaDetails.observe(this, viewModel)
} }
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentMangaBookmarksBinding { override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentMangaBookmarksBinding {
@@ -125,7 +124,7 @@ class BookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(),
dismissParentDialog() dismissParentDialog()
} else { } else {
val intent = IntentBuilder(view.context) val intent = IntentBuilder(view.context)
.manga(activityViewModel.manga.value ?: return) .manga(activityViewModel.getMangaOrNull() ?: return)
.bookmark(item) .bookmark(item)
.incognito(true) .incognito(true)
.build() .build()

View File

@@ -21,6 +21,7 @@ import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.util.ReversibleAction import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.details.data.MangaDetails
import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
@@ -32,7 +33,7 @@ import javax.inject.Inject
class BookmarksViewModel @Inject constructor( class BookmarksViewModel @Inject constructor(
private val bookmarksRepository: BookmarksRepository, private val bookmarksRepository: BookmarksRepository,
settings: AppSettings, settings: AppSettings,
) : BaseViewModel(), FlowCollector<Manga?> { ) : BaseViewModel(), FlowCollector<MangaDetails?> {
private val manga = MutableStateFlow<Manga?>(null) private val manga = MutableStateFlow<Manga?>(null)
val onActionDone = MutableEventFlow<ReversibleAction>() val onActionDone = MutableEventFlow<ReversibleAction>()
@@ -50,8 +51,8 @@ class BookmarksViewModel @Inject constructor(
.filterNotNull() .filterNotNull()
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, listOf(LoadingState)) .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, listOf(LoadingState))
override suspend fun emit(value: Manga?) { override suspend fun emit(value: MangaDetails?) {
manga.value = value manga.value = value?.toManga()
} }
fun removeBookmarks(ids: Set<Long>) { fun removeBookmarks(ids: Set<Long>) {

View File

@@ -7,10 +7,10 @@ import android.view.ViewGroup
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.ancestors import androidx.core.view.ancestors
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
@@ -28,10 +28,10 @@ import org.koitharu.kotatsu.core.util.ext.findParentCallback
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.databinding.FragmentChaptersBinding import org.koitharu.kotatsu.databinding.FragmentChaptersBinding
import org.koitharu.kotatsu.details.ui.DetailsViewModel
import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter
import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration
import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesViewModel
import org.koitharu.kotatsu.details.ui.withVolumeHeaders import org.koitharu.kotatsu.details.ui.withVolumeHeaders
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
@@ -40,11 +40,12 @@ import org.koitharu.kotatsu.reader.ui.ReaderNavigationCallback
import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderState
import kotlin.math.roundToInt import kotlin.math.roundToInt
@AndroidEntryPoint
class ChaptersFragment : class ChaptersFragment :
BaseFragment<FragmentChaptersBinding>(), BaseFragment<FragmentChaptersBinding>(),
OnListItemClickListener<ChapterListItem> { OnListItemClickListener<ChapterListItem> {
private val viewModel by activityViewModels<DetailsViewModel>() private val viewModel by ChaptersPagesViewModel.ActivityVMLazy(this)
private var chaptersAdapter: ChaptersAdapter? = null private var chaptersAdapter: ChaptersAdapter? = null
private var selectionController: ListSelectionController? = null private var selectionController: ListSelectionController? = null
@@ -107,7 +108,7 @@ class ChaptersFragment :
} else { } else {
startActivity( startActivity(
IntentBuilder(view.context) IntentBuilder(view.context)
.manga(viewModel.manga.value ?: return) .manga(viewModel.getMangaOrNull() ?: return)
.state(ReaderState(item.chapter.id, 0, 0)) .state(ReaderState(item.chapter.id, 0, 0))
.build(), .build(),
) )

View File

@@ -11,11 +11,11 @@ import org.koitharu.kotatsu.core.ui.list.BaseListSelectionCallback
import org.koitharu.kotatsu.core.ui.list.ListSelectionController import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.util.ext.toCollection import org.koitharu.kotatsu.core.util.ext.toCollection
import org.koitharu.kotatsu.core.util.ext.toSet import org.koitharu.kotatsu.core.util.ext.toSet
import org.koitharu.kotatsu.details.ui.DetailsViewModel import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesViewModel
import org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService import org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService
class ChaptersSelectionCallback( class ChaptersSelectionCallback(
private val viewModel: DetailsViewModel, private val viewModel: ChaptersPagesViewModel,
recyclerView: RecyclerView, recyclerView: RecyclerView,
) : BaseListSelectionCallback(recyclerView) { ) : BaseListSelectionCallback(recyclerView) {
@@ -60,7 +60,7 @@ class ChaptersSelectionCallback(
R.id.action_delete -> { R.id.action_delete -> {
val ids = controller.peekCheckedIds() val ids = controller.peekCheckedIds()
val manga = viewModel.manga.value val manga = viewModel.getMangaOrNull()
when { when {
ids.isEmpty() || manga == null -> Unit ids.isEmpty() || manga == null -> Unit
ids.size == manga.chapters?.size -> viewModel.deleteLocal() ids.size == manga.chapters?.size -> viewModel.deleteLocal()

View File

@@ -7,7 +7,6 @@ import android.view.ViewGroup
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@@ -30,7 +29,7 @@ import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.showOrHide import org.koitharu.kotatsu.core.util.ext.showOrHide
import org.koitharu.kotatsu.databinding.FragmentPagesBinding import org.koitharu.kotatsu.databinding.FragmentPagesBinding
import org.koitharu.kotatsu.details.ui.DetailsViewModel import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesViewModel
import org.koitharu.kotatsu.list.ui.GridSpanResolver import org.koitharu.kotatsu.list.ui.GridSpanResolver
import org.koitharu.kotatsu.list.ui.adapter.ListItemType import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
@@ -46,15 +45,15 @@ class PagesFragment :
BaseFragment<FragmentPagesBinding>(), BaseFragment<FragmentPagesBinding>(),
OnListItemClickListener<PageThumbnail> { OnListItemClickListener<PageThumbnail> {
private val detailsViewModel by activityViewModels<DetailsViewModel>()
private val viewModel by viewModels<PagesViewModel>()
@Inject @Inject
lateinit var coil: ImageLoader lateinit var coil: ImageLoader
@Inject @Inject
lateinit var settings: AppSettings lateinit var settings: AppSettings
private val parentViewModel by ChaptersPagesViewModel.ActivityVMLazy(this)
private val viewModel by viewModels<PagesViewModel>()
private var thumbnailsAdapter: PageThumbnailAdapter? = null private var thumbnailsAdapter: PageThumbnailAdapter? = null
private var spanResolver: GridSpanResolver? = null private var spanResolver: GridSpanResolver? = null
private var scrollListener: ScrollListener? = null private var scrollListener: ScrollListener? = null
@@ -64,12 +63,12 @@ class PagesFragment :
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
combine( combine(
detailsViewModel.details, parentViewModel.mangaDetails,
detailsViewModel.history, parentViewModel.readingState,
detailsViewModel.selectedBranch, parentViewModel.selectedBranch,
) { details, history, branch -> ) { details, readingState, branch ->
if (details != null && (details.isLoaded || details.chapters.isNotEmpty())) { if (details != null && (details.isLoaded || details.chapters.isNotEmpty())) {
PagesViewModel.State(details.filterChapters(branch), history, branch) PagesViewModel.State(details.filterChapters(branch), readingState, branch)
} else { } else {
null null
} }
@@ -102,7 +101,7 @@ class PagesFragment :
it.spanCount = checkNotNull(spanResolver).spanCount it.spanCount = checkNotNull(spanResolver).spanCount
} }
} }
detailsViewModel.isChaptersEmpty.observe(viewLifecycleOwner, ::onNoChaptersChanged) parentViewModel.isChaptersEmpty.observe(viewLifecycleOwner, ::onNoChaptersChanged)
viewModel.thumbnails.observe(viewLifecycleOwner, ::onThumbnailsChanged) viewModel.thumbnails.observe(viewLifecycleOwner, ::onThumbnailsChanged)
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this)) viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
viewModel.isLoading.observe(viewLifecycleOwner) { binding.progressBar.showOrHide(it) } viewModel.isLoading.observe(viewLifecycleOwner) { binding.progressBar.showOrHide(it) }
@@ -127,7 +126,7 @@ class PagesFragment :
} else { } else {
startActivity( startActivity(
IntentBuilder(view.context) IntentBuilder(view.context)
.manga(detailsViewModel.manga.value ?: return) .manga(parentViewModel.getMangaOrNull() ?: return)
.state(ReaderState(item.page.chapterId, item.page.index, 0)) .state(ReaderState(item.page.chapterId, item.page.index, 0))
.build(), .build(),
) )

View File

@@ -7,7 +7,6 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
@@ -16,12 +15,13 @@ import org.koitharu.kotatsu.details.data.MangaDetails
import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.reader.domain.ChaptersLoader import org.koitharu.kotatsu.reader.domain.ChaptersLoader
import org.koitharu.kotatsu.reader.ui.ReaderState
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class PagesViewModel @Inject constructor( class PagesViewModel @Inject constructor(
private val chaptersLoader: ChaptersLoader, private val chaptersLoader: ChaptersLoader,
private val settings: AppSettings, settings: AppSettings,
) : BaseViewModel() { ) : BaseViewModel() {
private var loadingJob: Job? = null private var loadingJob: Job? = null
@@ -75,13 +75,13 @@ class PagesViewModel @Inject constructor(
private suspend fun doInit(state: State) { private suspend fun doInit(state: State) {
chaptersLoader.init(state.details) chaptersLoader.init(state.details)
val initialChapterId = state.history?.chapterId?.takeIf { val initialChapterId = state.readerState?.chapterId?.takeIf {
chaptersLoader.peekChapter(it) != null chaptersLoader.peekChapter(it) != null
} ?: state.details.allChapters.firstOrNull()?.id ?: return } ?: state.details.allChapters.firstOrNull()?.id ?: return
if (!chaptersLoader.hasPages(initialChapterId)) { if (!chaptersLoader.hasPages(initialChapterId)) {
chaptersLoader.loadSingleChapter(initialChapterId) chaptersLoader.loadSingleChapter(initialChapterId)
} }
updateList(state.history) updateList(state.readerState)
} }
private fun loadPrevNextChapter(isNext: Boolean): Job = launchJob(Dispatchers.Default) { private fun loadPrevNextChapter(isNext: Boolean): Job = launchJob(Dispatchers.Default) {
@@ -91,13 +91,13 @@ class PagesViewModel @Inject constructor(
val currentState = state.firstNotNull() val currentState = state.firstNotNull()
val currentId = (if (isNext) chaptersLoader.last() else chaptersLoader.first()).chapterId val currentId = (if (isNext) chaptersLoader.last() else chaptersLoader.first()).chapterId
chaptersLoader.loadPrevNextChapter(currentState.details, currentId, isNext) chaptersLoader.loadPrevNextChapter(currentState.details, currentId, isNext)
updateList(currentState.history) updateList(currentState.readerState)
} finally { } finally {
indicator.value = false indicator.value = false
} }
} }
private fun updateList(history: MangaHistory?) { private fun updateList(readerState: ReaderState?) {
val snapshot = chaptersLoader.snapshot() val snapshot = chaptersLoader.snapshot()
val pages = buildList(snapshot.size + chaptersLoader.size + 2) { val pages = buildList(snapshot.size + chaptersLoader.size + 2) {
var previousChapterId = 0L var previousChapterId = 0L
@@ -109,7 +109,7 @@ class PagesViewModel @Inject constructor(
previousChapterId = page.chapterId previousChapterId = page.chapterId
} }
this += PageThumbnail( this += PageThumbnail(
isCurrent = history?.let { isCurrent = readerState?.let {
page.chapterId == it.chapterId && page.index == it.page page.chapterId == it.chapterId && page.index == it.page
} ?: false, } ?: false,
page = page, page = page,
@@ -121,7 +121,7 @@ class PagesViewModel @Inject constructor(
data class State( data class State(
val details: MangaDetails, val details: MangaDetails,
val history: MangaHistory?, val readerState: ReaderState?,
val branch: String? val branch: String?
) )
} }

View File

@@ -164,7 +164,7 @@ class ReaderActivity :
} }
override fun getParentActivityIntent(): Intent? { override fun getParentActivityIntent(): Intent? {
val manga = viewModel.manga?.toManga() ?: return null val manga = viewModel.getMangaOrNull() ?: return null
return DetailsActivity.newIntent(this, manga) return DetailsActivity.newIntent(this, manga)
} }

View File

@@ -15,6 +15,7 @@ import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
@@ -41,7 +42,6 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
@@ -49,11 +49,17 @@ import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.requireValue import org.koitharu.kotatsu.core.util.ext.requireValue
import org.koitharu.kotatsu.core.util.ext.sizeOrZero import org.koitharu.kotatsu.core.util.ext.sizeOrZero
import org.koitharu.kotatsu.details.data.MangaDetails import org.koitharu.kotatsu.details.data.MangaDetails
import org.koitharu.kotatsu.details.domain.DetailsInteractor
import org.koitharu.kotatsu.details.domain.DetailsLoadUseCase import org.koitharu.kotatsu.details.domain.DetailsLoadUseCase
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet.Companion.TAB_PAGES import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet.Companion.TAB_PAGES
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesViewModel
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.history.domain.HistoryUpdateUseCase import org.koitharu.kotatsu.history.domain.HistoryUpdateUseCase
import org.koitharu.kotatsu.list.domain.ReadingProgress.Companion.PROGRESS_NONE import org.koitharu.kotatsu.list.domain.ReadingProgress.Companion.PROGRESS_NONE
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.assertNotNull import org.koitharu.kotatsu.parsers.util.assertNotNull
@@ -70,14 +76,12 @@ private const val BOUNDS_PAGE_OFFSET = 2
private const val PREFETCH_LIMIT = 10 private const val PREFETCH_LIMIT = 10
@HiltViewModel @HiltViewModel
class ReaderViewModel class ReaderViewModel @Inject constructor(
@Inject
constructor(
private val savedStateHandle: SavedStateHandle, private val savedStateHandle: SavedStateHandle,
private val dataRepository: MangaDataRepository, private val dataRepository: MangaDataRepository,
private val historyRepository: HistoryRepository, private val historyRepository: HistoryRepository,
private val bookmarksRepository: BookmarksRepository, private val bookmarksRepository: BookmarksRepository,
private val settings: AppSettings, settings: AppSettings,
private val pageSaveHelper: PageSaveHelper, private val pageSaveHelper: PageSaveHelper,
private val pageLoader: PageLoader, private val pageLoader: PageLoader,
private val chaptersLoader: ChaptersLoader, private val chaptersLoader: ChaptersLoader,
@@ -86,18 +90,31 @@ constructor(
private val historyUpdateUseCase: HistoryUpdateUseCase, private val historyUpdateUseCase: HistoryUpdateUseCase,
private val detectReaderModeUseCase: DetectReaderModeUseCase, private val detectReaderModeUseCase: DetectReaderModeUseCase,
private val statsCollector: StatsCollector, private val statsCollector: StatsCollector,
) : BaseViewModel() { @LocalStorageChanges localStorageChanges: SharedFlow<LocalManga?>,
interactor: DetailsInteractor,
deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
downloadScheduler: DownloadWorker.Scheduler,
) : ChaptersPagesViewModel(
settings = settings,
interactor = interactor,
bookmarksRepository = bookmarksRepository,
historyRepository = historyRepository,
downloadScheduler = downloadScheduler,
deleteLocalMangaUseCase = deleteLocalMangaUseCase,
localStorageChanges = localStorageChanges,
) {
private val intent = MangaIntent(savedStateHandle) private val intent = MangaIntent(savedStateHandle)
private val preselectedBranch = savedStateHandle.get<String>(ReaderActivity.EXTRA_BRANCH)
private var loadingJob: Job? = null private var loadingJob: Job? = null
private var pageSaveJob: Job? = null private var pageSaveJob: Job? = null
private var bookmarkJob: Job? = null private var bookmarkJob: Job? = null
private var stateChangeJob: Job? = null private var stateChangeJob: Job? = null
private val currentState = MutableStateFlow<ReaderState?>(savedStateHandle[ReaderActivity.EXTRA_STATE])
private val mangaData = MutableStateFlow(intent.manga?.let { MangaDetails(it, null, null, false) }) init {
private val mangaFlow: Flow<Manga?> selectedBranch.value = savedStateHandle.get<String>(ReaderActivity.EXTRA_BRANCH)
get() = mangaData.map { it?.toManga() } readingState.value = savedStateHandle[ReaderActivity.EXTRA_STATE]
mangaDetails.value = intent.manga?.let { MangaDetails(it, null, null, false) }
}
val readerMode = MutableStateFlow<ReaderMode?>(null) val readerMode = MutableStateFlow<ReaderMode?>(null)
val onPageSaved = MutableEventFlow<Uri?>() val onPageSaved = MutableEventFlow<Uri?>()
@@ -107,16 +124,13 @@ constructor(
val incognitoMode = if (savedStateHandle.get<Boolean>(ReaderActivity.EXTRA_INCOGNITO) == true) { val incognitoMode = if (savedStateHandle.get<Boolean>(ReaderActivity.EXTRA_INCOGNITO) == true) {
MutableStateFlow(true) MutableStateFlow(true)
} else { } else {
mangaFlow.map { interactor.observeIncognitoMode(manga)
it != null && historyRepository.shouldSkip(it) .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
} }
val isPagesSheetEnabled = observeIsPagesSheetEnabled() val isPagesSheetEnabled = observeIsPagesSheetEnabled()
val content = MutableStateFlow(ReaderContent(emptyList(), null)) val content = MutableStateFlow(ReaderContent(emptyList(), null))
val manga: MangaDetails?
get() = mangaData.value
val pageAnimation = settings.observeAsStateFlow( val pageAnimation = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.Default, scope = viewModelScope + Dispatchers.Default,
@@ -164,15 +178,15 @@ constructor(
val readerSettings = ReaderSettings( val readerSettings = ReaderSettings(
parentScope = viewModelScope, parentScope = viewModelScope,
settings = settings, settings = settings,
colorFilterFlow = mangaFlow.flatMapLatest { colorFilterFlow = manga.flatMapLatest {
if (it == null) flowOf(null) else dataRepository.observeColorFilter(it.id) if (it == null) flowOf(null) else dataRepository.observeColorFilter(it.id)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null), }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null),
) )
val isMangaNsfw = mangaFlow.map { it?.isNsfw == true } val isMangaNsfw = manga.map { it?.isNsfw == true }
val isBookmarkAdded = currentState.flatMapLatest { state -> val isBookmarkAdded = readingState.flatMapLatest { state ->
val manga = mangaData.value?.toManga() val manga = mangaDetails.value?.toManga()
if (state == null || manga == null) { if (state == null || manga == null) {
flowOf(false) flowOf(false)
} else { } else {
@@ -190,7 +204,7 @@ constructor(
if (key == AppSettings.KEY_READER_SLIDER) notifyStateChanged() if (key == AppSettings.KEY_READER_SLIDER) notifyStateChanged()
}.launchIn(viewModelScope + Dispatchers.Default) }.launchIn(viewModelScope + Dispatchers.Default)
launchJob(Dispatchers.Default) { launchJob(Dispatchers.Default) {
val mangaId = mangaFlow.filterNotNull().first().id val mangaId = manga.filterNotNull().first().id
appShortcutManager.notifyMangaOpened(mangaId) appShortcutManager.notifyMangaOpened(mangaId)
} }
} }
@@ -201,14 +215,14 @@ constructor(
} }
fun onPause() { fun onPause() {
manga?.let { getMangaOrNull()?.let {
statsCollector.onPause(it.id) statsCollector.onPause(it.id)
} }
} }
fun switchMode(newMode: ReaderMode) { fun switchMode(newMode: ReaderMode) {
launchJob { launchJob {
val manga = checkNotNull(mangaData.value?.toManga()) val manga = checkNotNull(getMangaOrNull())
dataRepository.saveReaderMode( dataRepository.saveReaderMode(
manga = manga, manga = manga,
mode = newMode, mode = newMode,
@@ -222,24 +236,24 @@ constructor(
fun saveCurrentState(state: ReaderState? = null) { fun saveCurrentState(state: ReaderState? = null) {
if (state != null) { if (state != null) {
currentState.value = state readingState.value = state
savedStateHandle[ReaderActivity.EXTRA_STATE] = state savedStateHandle[ReaderActivity.EXTRA_STATE] = state
} }
if (incognitoMode.value) { if (incognitoMode.value) {
return return
} }
val readerState = state ?: currentState.value ?: return val readerState = state ?: readingState.value ?: return
historyUpdateUseCase.invokeAsync( historyUpdateUseCase.invokeAsync(
manga = mangaData.value?.toManga() ?: return, manga = getMangaOrNull() ?: return,
readerState = readerState, readerState = readerState,
percent = computePercent(readerState.chapterId, readerState.page), percent = computePercent(readerState.chapterId, readerState.page),
) )
} }
fun getCurrentState() = currentState.value fun getCurrentState() = readingState.value
fun getCurrentChapterPages(): List<MangaPage>? { fun getCurrentChapterPages(): List<MangaPage>? {
val chapterId = currentState.value?.chapterId ?: return null val chapterId = readingState.value?.chapterId ?: return null
return chaptersLoader.getPages(chapterId).map { it.toMangaPage() } return chaptersLoader.getPages(chapterId).map { it.toMangaPage() }
} }
@@ -272,7 +286,7 @@ constructor(
} }
fun getCurrentPage(): MangaPage? { fun getCurrentPage(): MangaPage? {
val state = currentState.value ?: return null val state = readingState.value ?: return null
return content.value.pages.find { return content.value.pages.find {
it.chapterId == state.chapterId && it.index == state.page it.chapterId == state.chapterId && it.index == state.page
}?.toMangaPage() }?.toMangaPage()
@@ -294,9 +308,9 @@ constructor(
val prevJob = loadingJob val prevJob = loadingJob
loadingJob = launchLoadingJob(Dispatchers.Default) { loadingJob = launchLoadingJob(Dispatchers.Default) {
prevJob?.cancelAndJoin() prevJob?.cancelAndJoin()
val prevState = currentState.requireValue() val prevState = readingState.requireValue()
val newChapterId = if (delta != 0) { val newChapterId = if (delta != 0) {
val allChapters = checkNotNull(manga).allChapters val allChapters = mangaDetails.requireValue().allChapters
var index = allChapters.indexOfFirst { x -> x.id == prevState.chapterId } var index = allChapters.indexOfFirst { x -> x.id == prevState.chapterId }
if (index < 0) { if (index < 0) {
return@launchLoadingJob return@launchLoadingJob
@@ -330,7 +344,7 @@ constructor(
} }
val centerPos = (lowerPos + upperPos) / 2 val centerPos = (lowerPos + upperPos) / 2
pages.getOrNull(centerPos)?.let { page -> pages.getOrNull(centerPos)?.let { page ->
currentState.update { cs -> readingState.update { cs ->
cs?.copy(chapterId = page.chapterId, page = page.index) cs?.copy(chapterId = page.chapterId, page = page.index)
} }
} }
@@ -357,10 +371,10 @@ constructor(
} }
bookmarkJob = launchJob(Dispatchers.Default) { bookmarkJob = launchJob(Dispatchers.Default) {
loadingJob?.join() loadingJob?.join()
val state = checkNotNull(currentState.value) val state = checkNotNull(readingState.value)
val page = checkNotNull(getCurrentPage()) { "Page not found" } val page = checkNotNull(getCurrentPage()) { "Page not found" }
val bookmark = Bookmark( val bookmark = Bookmark(
manga = mangaData.requireValue().toManga(), manga = requireManga(),
pageId = page.id, pageId = page.id,
chapterId = state.chapterId, chapterId = state.chapterId,
page = state.page, page = state.page,
@@ -380,7 +394,7 @@ constructor(
} }
bookmarkJob = launchJob { bookmarkJob = launchJob {
loadingJob?.join() loadingJob?.join()
val manga = mangaData.requireValue().toManga() val manga = requireManga()
val state = checkNotNull(getCurrentState()) val state = checkNotNull(getCurrentState())
bookmarksRepository.removeBookmark(manga.id, state.chapterId, state.page) bookmarksRepository.removeBookmark(manga.id, state.chapterId, state.page)
onShowToast.call(R.string.bookmark_removed) onShowToast.call(R.string.bookmark_removed)
@@ -390,28 +404,29 @@ constructor(
private fun loadImpl() { private fun loadImpl() {
loadingJob = launchLoadingJob(Dispatchers.Default) { loadingJob = launchLoadingJob(Dispatchers.Default) {
val details = detailsLoadUseCase.invoke(intent).first { x -> x.isLoaded } val details = detailsLoadUseCase.invoke(intent).first { x -> x.isLoaded }
mangaData.value = details mangaDetails.value = details
chaptersLoader.init(details) chaptersLoader.init(details)
val manga = details.toManga() val manga = details.toManga()
// obtain state // obtain state
if (currentState.value == null) { if (readingState.value == null) {
currentState.value = getStateFromIntent(manga) readingState.value = getStateFromIntent(manga)
} }
val mode = detectReaderModeUseCase.invoke(manga, currentState.value) val mode = detectReaderModeUseCase.invoke(manga, readingState.value)
val branch = chaptersLoader.peekChapter(currentState.value?.chapterId ?: 0L)?.branch val branch = chaptersLoader.peekChapter(readingState.value?.chapterId ?: 0L)?.branch
mangaData.value = details.filterChapters(branch) selectedBranch.value = branch
mangaDetails.value = details.filterChapters(branch)
readerMode.value = mode readerMode.value = mode
chaptersLoader.loadSingleChapter(requireNotNull(currentState.value).chapterId) chaptersLoader.loadSingleChapter(requireNotNull(readingState.value).chapterId)
// save state // save state
if (!incognitoMode.value) { if (!incognitoMode.value) {
currentState.value?.let { readingState.value?.let {
val percent = computePercent(it.chapterId, it.page) val percent = computePercent(it.chapterId, it.page)
historyUpdateUseCase.invoke(manga, it, percent) historyUpdateUseCase.invoke(manga, it, percent)
} }
} }
notifyStateChanged() notifyStateChanged()
content.value = ReaderContent(chaptersLoader.snapshot(), currentState.value) content.value = ReaderContent(chaptersLoader.snapshot(), readingState.value)
} }
} }
@@ -420,7 +435,7 @@ constructor(
val prevJob = loadingJob val prevJob = loadingJob
loadingJob = launchLoadingJob(Dispatchers.Default) { loadingJob = launchLoadingJob(Dispatchers.Default) {
prevJob?.join() prevJob?.join()
chaptersLoader.loadPrevNextChapter(mangaData.requireValue(), currentId, isNext) chaptersLoader.loadPrevNextChapter(mangaDetails.requireValue(), currentId, isNext)
content.value = ReaderContent(chaptersLoader.snapshot(), null) content.value = ReaderContent(chaptersLoader.snapshot(), null)
} }
} }
@@ -439,7 +454,7 @@ constructor(
private fun notifyStateChanged() { private fun notifyStateChanged() {
val state = getCurrentState().assertNotNull("state") ?: return val state = getCurrentState().assertNotNull("state") ?: return
val chapter = chaptersLoader.peekChapter(state.chapterId).assertNotNull("chapter") ?: return val chapter = chaptersLoader.peekChapter(state.chapterId).assertNotNull("chapter") ?: return
val m = manga.assertNotNull("manga") ?: return val m = mangaDetails.value.assertNotNull("manga") ?: return
val chapterIndex = m.chapters[chapter.branch]?.indexOfFirst { it.id == chapter.id } ?: -1 val chapterIndex = m.chapters[chapter.branch]?.indexOfFirst { it.id == chapter.id } ?: -1
val newState = ReaderUiState( val newState = ReaderUiState(
mangaName = m.toManga().title, mangaName = m.toManga().title,
@@ -461,7 +476,7 @@ constructor(
private fun computePercent(chapterId: Long, pageIndex: Int): Float { private fun computePercent(chapterId: Long, pageIndex: Int): Float {
val branch = chaptersLoader.peekChapter(chapterId)?.branch val branch = chaptersLoader.peekChapter(chapterId)?.branch
val chapters = manga?.chapters?.get(branch) ?: return PROGRESS_NONE val chapters = mangaDetails.value?.chapters?.get(branch) ?: return PROGRESS_NONE
val chaptersCount = chapters.size val chaptersCount = chapters.size
val chapterIndex = chapters.indexOfFirst { x -> x.id == chapterId } val chapterIndex = chapters.indexOfFirst { x -> x.id == chapterId }
val pagesCount = chaptersLoader.getPagesCount(chapterId) val pagesCount = chaptersLoader.getPagesCount(chapterId)
@@ -495,6 +510,7 @@ constructor(
private suspend fun getStateFromIntent(manga: Manga): ReaderState { private suspend fun getStateFromIntent(manga: Manga): ReaderState {
val history = historyRepository.getOne(manga) val history = historyRepository.getOne(manga)
val preselectedBranch = selectedBranch.value
val result = if (history != null) { val result = if (history != null) {
if (preselectedBranch != null && preselectedBranch != manga.findChapter(history.chapterId)?.branch) { if (preselectedBranch != null && preselectedBranch != manga.findChapter(history.chapterId)?.branch) {
null null

View File

@@ -69,7 +69,7 @@ class ReaderConfigSheet :
?: ReaderMode.STANDARD ?: ReaderMode.STANDARD
imageServerDelegate = ImageServerDelegate( imageServerDelegate = ImageServerDelegate(
mangaRepositoryFactory = mangaRepositoryFactory, mangaRepositoryFactory = mangaRepositoryFactory,
mangaSource = viewModel.manga?.toManga()?.source, mangaSource = viewModel.getMangaOrNull()?.source,
) )
} }
@@ -144,7 +144,7 @@ class ReaderConfigSheet :
R.id.button_color_filter -> { R.id.button_color_filter -> {
val page = viewModel.getCurrentPage() ?: return val page = viewModel.getCurrentPage() ?: return
val manga = viewModel.manga?.toManga() ?: return val manga = viewModel.getMangaOrNull() ?: return
startActivity(ColorFilterConfigActivity.newIntent(v.context, manga, page)) startActivity(ColorFilterConfigActivity.newIntent(v.context, manga, page))
} }

View File

@@ -19,6 +19,7 @@
<string name="chapter_d_of_d">Chapter %1$d of %2$d</string> <string name="chapter_d_of_d">Chapter %1$d of %2$d</string>
<string name="close">Close</string> <string name="close">Close</string>
<string name="try_again">Try again</string> <string name="try_again">Try again</string>
<!-- Should be short -->
<string name="retry">Retry</string> <string name="retry">Retry</string>
<string name="clear_history">Clear history</string> <string name="clear_history">Clear history</string>
<string name="nothing_found">Nothing found</string> <string name="nothing_found">Nothing found</string>