Refactor manga loading
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
package org.koitharu.kotatsu.core.model
|
||||
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.reader.data.filterChapters
|
||||
|
||||
data class DoubleManga(
|
||||
private val remoteManga: Result<Manga>?,
|
||||
private val localManga: Result<Manga>?,
|
||||
) {
|
||||
|
||||
constructor(manga: Manga) : this(
|
||||
remoteManga = if (manga.source != MangaSource.LOCAL) Result.success(manga) else null,
|
||||
localManga = if (manga.source == MangaSource.LOCAL) Result.success(manga) else null,
|
||||
)
|
||||
|
||||
val remote: Manga?
|
||||
get() = remoteManga?.getOrNull()
|
||||
|
||||
val local: Manga?
|
||||
get() = localManga?.getOrNull()
|
||||
|
||||
val any: Manga?
|
||||
get() = remote ?: local
|
||||
|
||||
val hasRemote: Boolean
|
||||
get() = remoteManga?.isSuccess == true
|
||||
|
||||
val hasLocal: Boolean
|
||||
get() = localManga?.isSuccess == true
|
||||
|
||||
val chapters: List<MangaChapter>? by lazy(LazyThreadSafetyMode.PUBLICATION) {
|
||||
mergeChapters()
|
||||
}
|
||||
|
||||
fun requireAny(): Manga {
|
||||
val result = remoteManga?.getOrNull() ?: localManga?.getOrNull()
|
||||
if (result != null) {
|
||||
return result
|
||||
}
|
||||
throw (
|
||||
remoteManga?.exceptionOrNull()
|
||||
?: localManga?.exceptionOrNull()
|
||||
?: IllegalStateException("No online either local manga available")
|
||||
)
|
||||
}
|
||||
|
||||
fun filterChapters(branch: String?) = DoubleManga(
|
||||
remoteManga?.map { it.filterChapters(branch) },
|
||||
localManga?.map { it.filterChapters(branch) },
|
||||
)
|
||||
|
||||
private fun mergeChapters(): List<MangaChapter>? {
|
||||
val remoteChapters = remote?.chapters
|
||||
val localChapters = local?.chapters
|
||||
if (localChapters == null && remoteChapters == null) {
|
||||
return null
|
||||
}
|
||||
val localMap = if (!localChapters.isNullOrEmpty()) {
|
||||
localChapters.associateByTo(LinkedHashMap(localChapters.size)) { it.id }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val result = ArrayList<MangaChapter>(maxOf(remoteChapters?.size ?: 0, localChapters?.size ?: 0))
|
||||
remoteChapters?.forEach { r ->
|
||||
localMap?.remove(r.id)?.let { l ->
|
||||
result.add(l)
|
||||
} ?: result.add(r)
|
||||
}
|
||||
localMap?.values?.let {
|
||||
result.addAll(it)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import org.koitharu.kotatsu.core.util.ext.iterator
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
|
||||
fun Collection<Manga>.ids() = mapToSet { it.id }
|
||||
@@ -54,3 +55,6 @@ fun Manga.getPreferredBranch(history: MangaHistory?): String? {
|
||||
}
|
||||
return candidates.ifEmpty { groups }.maxByOrNull { it.value.size }?.key
|
||||
}
|
||||
|
||||
val Manga.isLocal: Boolean
|
||||
get() = source == MangaSource.LOCAL
|
||||
|
||||
@@ -12,28 +12,31 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
||||
|
||||
class MangaIntent private constructor(
|
||||
@JvmField val manga: Manga?,
|
||||
@JvmField val mangaId: Long,
|
||||
@JvmField val id: Long,
|
||||
@JvmField val uri: Uri?,
|
||||
) {
|
||||
|
||||
constructor(intent: Intent?) : this(
|
||||
manga = intent?.getParcelableExtraCompat<ParcelableManga>(KEY_MANGA)?.manga,
|
||||
mangaId = intent?.getLongExtra(KEY_ID, ID_NONE) ?: ID_NONE,
|
||||
id = intent?.getLongExtra(KEY_ID, ID_NONE) ?: ID_NONE,
|
||||
uri = intent?.data,
|
||||
)
|
||||
|
||||
constructor(savedStateHandle: SavedStateHandle) : this(
|
||||
manga = savedStateHandle.get<ParcelableManga>(KEY_MANGA)?.manga,
|
||||
mangaId = savedStateHandle[KEY_ID] ?: ID_NONE,
|
||||
id = savedStateHandle[KEY_ID] ?: ID_NONE,
|
||||
uri = savedStateHandle[BaseActivity.EXTRA_DATA],
|
||||
)
|
||||
|
||||
constructor(args: Bundle?) : this(
|
||||
manga = args?.getParcelableCompat<ParcelableManga>(KEY_MANGA)?.manga,
|
||||
mangaId = args?.getLong(KEY_ID, ID_NONE) ?: ID_NONE,
|
||||
id = args?.getLong(KEY_ID, ID_NONE) ?: ID_NONE,
|
||||
uri = null,
|
||||
)
|
||||
|
||||
val mangaId: Long
|
||||
get() = if (id != ID_NONE) id else manga?.id ?: uri?.lastPathSegment?.toLongOrNull() ?: ID_NONE
|
||||
|
||||
companion object {
|
||||
|
||||
const val ID_NONE = 0L
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package org.koitharu.kotatsu.details.domain
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.koitharu.kotatsu.core.model.DoubleManga
|
||||
import org.koitharu.kotatsu.core.model.isLocal
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.local.data.LocalManga
|
||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
class DetailsInteractor @Inject constructor(
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val favouritesRepository: FavouritesRepository,
|
||||
private val localMangaRepository: LocalMangaRepository,
|
||||
private val trackingRepository: TrackingRepository,
|
||||
private val settings: AppSettings,
|
||||
private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
|
||||
) {
|
||||
|
||||
fun observeIsFavourite(mangaId: Long): Flow<Boolean> {
|
||||
return favouritesRepository.observeCategoriesIds(mangaId)
|
||||
.map { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
fun observeNewChapters(mangaId: Long): Flow<Int> {
|
||||
return settings.observeAsFlow(AppSettings.KEY_TRACKER_ENABLED) { isTrackerEnabled }
|
||||
.flatMapLatest { isEnabled ->
|
||||
if (isEnabled) {
|
||||
trackingRepository.observeNewChaptersCount(mangaId)
|
||||
} else {
|
||||
flowOf(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun observeScrobblingInfo(mangaId: Long): Flow<List<ScrobblingInfo>> {
|
||||
return combine(
|
||||
scrobblers.map { it.observeScrobblingInfo(mangaId) },
|
||||
) { scrobblingInfo ->
|
||||
scrobblingInfo.filterNotNull()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteLocalManga(manga: Manga) {
|
||||
val victim = if (manga.isLocal) manga else localMangaRepository.findSavedManga(manga)?.manga
|
||||
checkNotNull(victim) { "Cannot find saved manga for ${manga.title}" }
|
||||
val original = if (manga.isLocal) localMangaRepository.getRemoteManga(manga) else manga
|
||||
localMangaRepository.delete(victim) || throw IOException("Unable to delete file")
|
||||
runCatchingCancellable {
|
||||
historyRepository.deleteOrSwap(victim, original)
|
||||
}.onFailure {
|
||||
it.printStackTraceDebug()
|
||||
}
|
||||
}
|
||||
|
||||
fun observeIncognitoMode(mangaFlow: Flow<Manga?>): Flow<Boolean> {
|
||||
return mangaFlow
|
||||
.distinctUntilChangedBy { it?.isNsfw }
|
||||
.flatMapLatest { manga ->
|
||||
if (manga != null) {
|
||||
historyRepository.observeShouldSkip(manga)
|
||||
} else {
|
||||
settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateLocal(subject: DoubleManga?, localManga: LocalManga): DoubleManga? {
|
||||
return if (subject?.any?.id == localManga.manga.id) {
|
||||
subject.copy(
|
||||
localManga = runCatchingCancellable {
|
||||
localMangaRepository.getDetails(localManga.manga)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
subject
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,12 +160,14 @@ class ChaptersFragment :
|
||||
val selectedIds = selectionController?.peekCheckedIds() ?: return false
|
||||
val allItems = chaptersAdapter?.items.orEmpty()
|
||||
val items = allItems.withIndex().filter { (_, x) -> x.chapter.id in selectedIds }
|
||||
menu.findItem(R.id.action_save).isVisible = items.none { (_, x) ->
|
||||
x.chapter.source == MangaSource.LOCAL
|
||||
}
|
||||
menu.findItem(R.id.action_delete).isVisible = items.all { (_, x) ->
|
||||
x.chapter.source == MangaSource.LOCAL
|
||||
var canSave = true
|
||||
var canDelete = true
|
||||
items.forEach { (_, x) ->
|
||||
val isLocal = x.isDownloaded || x.chapter.source == MangaSource.LOCAL
|
||||
if (isLocal) canSave = false else canDelete = false
|
||||
}
|
||||
menu.findItem(R.id.action_save).isVisible = canSave
|
||||
menu.findItem(R.id.action_delete).isVisible = canDelete
|
||||
menu.findItem(R.id.action_select_all).isVisible = items.size < allItems.size
|
||||
menu.findItem(R.id.action_mark_current).isVisible = items.size == 1
|
||||
mode.title = items.size.toString()
|
||||
|
||||
@@ -52,7 +52,7 @@ fun mapChapters(
|
||||
isCurrent = chapter.id == currentId,
|
||||
isUnread = isUnread,
|
||||
isNew = false,
|
||||
isDownloaded = false,
|
||||
isDownloaded = remoteManga != null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.core.net.toUri
|
||||
import androidx.core.text.getSpans
|
||||
import androidx.core.text.parseAsHtml
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.asFlow
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
@@ -25,82 +26,74 @@ import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.flow.update
|
||||
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.model.getPreferredBranch
|
||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.util.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.core.util.asFlowLiveData
|
||||
import org.koitharu.kotatsu.core.util.ext.computeSize
|
||||
import org.koitharu.kotatsu.core.util.ext.requireValue
|
||||
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
|
||||
import org.koitharu.kotatsu.details.domain.BranchComparator
|
||||
import org.koitharu.kotatsu.details.domain.DetailsInteractor
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
|
||||
import org.koitharu.kotatsu.details.ui.model.MangaBranch
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.local.data.LocalManga
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.local.domain.DoubleMangaLoader
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
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.ScrobblingStatus
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class DetailsViewModel @Inject constructor(
|
||||
private val historyRepository: HistoryRepository,
|
||||
favouritesRepository: FavouritesRepository,
|
||||
private val localMangaRepository: LocalMangaRepository,
|
||||
trackingRepository: TrackingRepository,
|
||||
private val bookmarksRepository: BookmarksRepository,
|
||||
private val settings: AppSettings,
|
||||
private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
|
||||
private val imageGetter: Html.ImageGetter,
|
||||
private val delegate: MangaDetailsDelegate,
|
||||
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
|
||||
private val downloadScheduler: DownloadWorker.Scheduler,
|
||||
private val interactor: DetailsInteractor,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val mangaLoader: DoubleMangaLoader,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val intent = MangaIntent(savedStateHandle)
|
||||
private val mangaId = intent.mangaId
|
||||
private val doubleManga: MutableStateFlow<DoubleManga?> = MutableStateFlow(intent.manga?.let { DoubleManga(it) })
|
||||
private var loadingJob: Job
|
||||
|
||||
val onShowToast = SingleLiveEvent<Int>()
|
||||
val onDownloadStarted = SingleLiveEvent<Unit>()
|
||||
|
||||
private val mangaData = combine(
|
||||
delegate.onlineManga,
|
||||
delegate.localManga,
|
||||
) { o, l ->
|
||||
o ?: l
|
||||
}.stateIn(viewModelScope, SharingStarted.Lazily, null)
|
||||
private val mangaData = doubleManga.map { it?.any }
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, doubleManga.value?.any)
|
||||
|
||||
private val history = historyRepository.observeOne(delegate.mangaId)
|
||||
private val history = historyRepository.observeOne(mangaId)
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
||||
|
||||
private val favourite = favouritesRepository.observeCategoriesIds(delegate.mangaId).map { it.isNotEmpty() }
|
||||
private val favourite = interactor.observeIsFavourite(mangaId)
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
||||
|
||||
private val newChapters = settings.observeAsFlow(AppSettings.KEY_TRACKER_ENABLED) { isTrackerEnabled }
|
||||
.flatMapLatest { isEnabled ->
|
||||
if (isEnabled) {
|
||||
trackingRepository.observeNewChaptersCount(delegate.mangaId)
|
||||
} else {
|
||||
flowOf(0)
|
||||
}
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
|
||||
private val newChapters = interactor.observeNewChapters(mangaId)
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
|
||||
|
||||
private val chaptersQuery = MutableStateFlow("")
|
||||
private val selectedBranch = MutableStateFlow<String?>(null)
|
||||
|
||||
private val chaptersReversed = settings.observeAsFlow(AppSettings.KEY_REVERSE_CHAPTERS) { chaptersReverse }
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
||||
@@ -112,9 +105,9 @@ class DetailsViewModel @Inject constructor(
|
||||
|
||||
val historyInfo: LiveData<HistoryInfo> = combine(
|
||||
mangaData,
|
||||
delegate.selectedBranch,
|
||||
selectedBranch,
|
||||
history,
|
||||
historyRepository.observeShouldSkip(mangaData),
|
||||
interactor.observeIncognitoMode(mangaData),
|
||||
) { m, b, h, im ->
|
||||
HistoryInfo(m, b, h, im)
|
||||
}.asFlowLiveData(
|
||||
@@ -126,10 +119,11 @@ class DetailsViewModel @Inject constructor(
|
||||
if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList())
|
||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
|
||||
|
||||
val localSize = delegate.localManga
|
||||
val localSize = doubleManga
|
||||
.map {
|
||||
if (it != null) {
|
||||
val file = it.url.toUri().toFileOrNull()
|
||||
val local = it?.local
|
||||
if (local != null) {
|
||||
val file = local.url.toUri().toFileOrNull()
|
||||
file?.computeSize() ?: 0L
|
||||
} else {
|
||||
0L
|
||||
@@ -152,46 +146,38 @@ class DetailsViewModel @Inject constructor(
|
||||
val isScrobblingAvailable: Boolean
|
||||
get() = scrobblers.any { it.isAvailable }
|
||||
|
||||
val scrobblingInfo: LiveData<List<ScrobblingInfo>> = combine(
|
||||
scrobblers.map { it.observeScrobblingInfo(delegate.mangaId) },
|
||||
) { scrobblingInfo ->
|
||||
scrobblingInfo.filterNotNull()
|
||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
|
||||
val scrobblingInfo: LiveData<List<ScrobblingInfo>> = interactor.observeScrobblingInfo(mangaId)
|
||||
.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
|
||||
|
||||
val branches: LiveData<List<MangaBranch>> = combine(
|
||||
delegate.onlineManga,
|
||||
delegate.localManga,
|
||||
delegate.selectedBranch,
|
||||
) { m, l, b ->
|
||||
val chapters = concat(m?.chapters, l?.chapters)
|
||||
if (chapters.isEmpty()) return@combine emptyList()
|
||||
doubleManga,
|
||||
selectedBranch,
|
||||
) { m, b ->
|
||||
val chapters = m?.chapters
|
||||
if (chapters.isNullOrEmpty()) return@combine emptyList()
|
||||
chapters.groupBy { x -> x.branch }
|
||||
.map { x -> MangaBranch(x.key, x.value.size, x.key == b) }
|
||||
.sortedWith(BranchComparator())
|
||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
|
||||
|
||||
val selectedBranchName = delegate.selectedBranch
|
||||
val selectedBranchName = selectedBranch
|
||||
.asFlowLiveData(viewModelScope.coroutineContext, null)
|
||||
|
||||
val isChaptersEmpty: LiveData<Boolean> = combine(
|
||||
delegate.onlineManga,
|
||||
delegate.localManga,
|
||||
doubleManga,
|
||||
isLoading.asFlow(),
|
||||
) { manga, local, loading ->
|
||||
(manga != null && manga.chapters.isNullOrEmpty()) &&
|
||||
(local != null && local.chapters.isNullOrEmpty()) &&
|
||||
!loading
|
||||
) { manga, loading ->
|
||||
manga?.any != null && manga.chapters.isNullOrEmpty() && !loading
|
||||
}.asFlowLiveData(viewModelScope.coroutineContext, false)
|
||||
|
||||
val chapters = combine(
|
||||
combine(
|
||||
delegate.onlineManga,
|
||||
delegate.localManga,
|
||||
doubleManga,
|
||||
history,
|
||||
delegate.selectedBranch,
|
||||
selectedBranch,
|
||||
newChapters,
|
||||
) { manga, local, history, branch, news ->
|
||||
mapChapters(manga, local, history, news, branch)
|
||||
) { manga, history, branch, news ->
|
||||
mapChapters(manga?.remote, manga?.local, history, news, branch)
|
||||
},
|
||||
chaptersReversed,
|
||||
chaptersQuery,
|
||||
@@ -200,7 +186,7 @@ class DetailsViewModel @Inject constructor(
|
||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
|
||||
val selectedBranchValue: String?
|
||||
get() = delegate.selectedBranch.value
|
||||
get() = selectedBranch.value
|
||||
|
||||
init {
|
||||
loadingJob = doLoad()
|
||||
@@ -216,20 +202,14 @@ class DetailsViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun deleteLocal() {
|
||||
val m = delegate.localManga.value
|
||||
val m = doubleManga.value?.local
|
||||
if (m == null) {
|
||||
onShowToast.call(R.string.file_not_found)
|
||||
return
|
||||
}
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
val manga = if (m.source == MangaSource.LOCAL) m else localMangaRepository.findSavedManga(m)?.manga
|
||||
checkNotNull(manga) { "Cannot find saved manga for ${m.title}" }
|
||||
val original = localMangaRepository.getRemoteManga(manga)
|
||||
localMangaRepository.delete(manga) || throw IOException("Unable to delete file")
|
||||
runCatchingCancellable {
|
||||
historyRepository.deleteOrSwap(manga, original)
|
||||
}
|
||||
onMangaRemoved.emitCall(manga)
|
||||
interactor.deleteLocalManga(m)
|
||||
onMangaRemoved.emitCall(m)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,11 +225,7 @@ class DetailsViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun setSelectedBranch(branch: String?) {
|
||||
delegate.selectedBranch.value = branch
|
||||
}
|
||||
|
||||
fun getRemoteManga(): Manga? {
|
||||
return delegate.onlineManga.value
|
||||
selectedBranch.value = branch
|
||||
}
|
||||
|
||||
fun performChapterSearch(query: String?) {
|
||||
@@ -260,7 +236,7 @@ class DetailsViewModel @Inject constructor(
|
||||
val scrobbler = getScrobbler(index) ?: return
|
||||
launchJob(Dispatchers.Default) {
|
||||
scrobbler.updateScrobblingInfo(
|
||||
mangaId = delegate.mangaId,
|
||||
mangaId = mangaId,
|
||||
rating = rating,
|
||||
status = status,
|
||||
comment = null,
|
||||
@@ -272,26 +248,32 @@ class DetailsViewModel @Inject constructor(
|
||||
val scrobbler = getScrobbler(index) ?: return
|
||||
launchJob(Dispatchers.Default) {
|
||||
scrobbler.unregisterScrobbling(
|
||||
mangaId = delegate.mangaId,
|
||||
mangaId = mangaId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun markChapterAsCurrent(chapterId: Long) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
val manga = checkNotNull(mangaData.value)
|
||||
val chapters = checkNotNull(manga.getChapters(selectedBranchValue))
|
||||
val manga = checkNotNull(doubleManga.value)
|
||||
val chapters = checkNotNull(manga.filterChapters(selectedBranchValue).chapters)
|
||||
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, chapterId = chapterId, page = 0, scroll = 0, percent = percent)
|
||||
historyRepository.addOrUpdate(
|
||||
manga = manga.requireAny(),
|
||||
chapterId = chapterId,
|
||||
page = 0,
|
||||
scroll = 0,
|
||||
percent = percent,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun download(chaptersIds: Set<Long>?) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
downloadScheduler.schedule(
|
||||
delegate.onlineManga.value ?: checkNotNull(manga.value),
|
||||
doubleManga.requireValue().requireAny(),
|
||||
chaptersIds,
|
||||
)
|
||||
onDownloadStarted.emitCall(Unit)
|
||||
@@ -299,7 +281,12 @@ class DetailsViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
|
||||
delegate.doLoad()
|
||||
val result = mangaLoader.load(intent)
|
||||
val manga = result.requireAny()
|
||||
// find default branch
|
||||
val hist = historyRepository.getOne(manga)
|
||||
selectedBranch.value = manga.getPreferredBranch(hist)
|
||||
doubleManga.value = result
|
||||
}
|
||||
|
||||
private fun List<ChapterListItem>.filterSearch(query: String): List<ChapterListItem> {
|
||||
@@ -313,21 +300,9 @@ class DetailsViewModel @Inject constructor(
|
||||
|
||||
private suspend fun onDownloadComplete(downloadedManga: LocalManga?) {
|
||||
downloadedManga ?: return
|
||||
val currentManga = mangaData.value ?: return
|
||||
if (currentManga.id != downloadedManga.manga.id) {
|
||||
return
|
||||
}
|
||||
if (currentManga.source == MangaSource.LOCAL) {
|
||||
reload()
|
||||
} else {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
runCatchingCancellable {
|
||||
localMangaRepository.getDetails(downloadedManga.manga)
|
||||
}.onSuccess {
|
||||
delegate.publishManga(it)
|
||||
}.onFailure {
|
||||
it.printStackTraceDebug()
|
||||
}
|
||||
launchJob {
|
||||
doubleManga.update {
|
||||
interactor.updateLocal(it, downloadedManga)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -353,18 +328,4 @@ class DetailsViewModel @Inject constructor(
|
||||
}
|
||||
return scrobbler
|
||||
}
|
||||
|
||||
private fun <T> concat(a: List<T>?, b: List<T>?): List<T> {
|
||||
return when {
|
||||
a == null && b == null -> emptyList<T>()
|
||||
a == null && b != null -> b
|
||||
a != null && b == null -> a
|
||||
a != null && b != null -> buildList<T>(a.size + b.size) {
|
||||
addAll(a)
|
||||
addAll(b)
|
||||
}
|
||||
|
||||
else -> error("This shouldn't have happened")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
package org.koitharu.kotatsu.details.ui
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import dagger.hilt.android.ViewModelLifecycle
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import org.koitharu.kotatsu.core.model.getPreferredBranch
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import javax.inject.Inject
|
||||
|
||||
@ViewModelScoped
|
||||
class MangaDetailsDelegate @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
lifecycle: ViewModelLifecycle,
|
||||
private val mangaDataRepository: MangaDataRepository,
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val localMangaRepository: LocalMangaRepository,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
networkState: NetworkState,
|
||||
) {
|
||||
private val viewModelScope = RetainedLifecycleCoroutineScope(lifecycle)
|
||||
|
||||
private val intent = MangaIntent(savedStateHandle)
|
||||
private val onlineMangaStateFlow = MutableStateFlow<Manga?>(null)
|
||||
private val localMangaStateFlow = MutableStateFlow<Manga?>(null)
|
||||
|
||||
val onlineManga = combine(
|
||||
onlineMangaStateFlow,
|
||||
networkState,
|
||||
) { m, s -> m.takeIf { s } }
|
||||
.stateIn(viewModelScope, SharingStarted.Lazily, null)
|
||||
val localManga = localMangaStateFlow.asStateFlow()
|
||||
|
||||
val selectedBranch = MutableStateFlow<String?>(null)
|
||||
val mangaId = intent.manga?.id ?: intent.mangaId
|
||||
|
||||
init {
|
||||
intent.manga?.let {
|
||||
publishManga(it)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun doLoad() {
|
||||
var manga = mangaDataRepository.resolveIntent(intent) ?: throw NotFoundException("Cannot find manga", "")
|
||||
publishManga(manga)
|
||||
manga = mangaRepositoryFactory.create(manga.source).getDetails(manga)
|
||||
// find default branch
|
||||
val hist = historyRepository.getOne(manga)
|
||||
selectedBranch.value = manga.getPreferredBranch(hist)
|
||||
publishManga(manga)
|
||||
runCatchingCancellable {
|
||||
if (manga.source == MangaSource.LOCAL) {
|
||||
val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatchingCancellable null
|
||||
mangaRepositoryFactory.create(m.source).getDetails(m)
|
||||
} else {
|
||||
localMangaRepository.findSavedManga(manga)?.manga
|
||||
}
|
||||
}.onFailure { error ->
|
||||
error.printStackTraceDebug()
|
||||
}.onSuccess {
|
||||
if (it != null) {
|
||||
publishManga(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun publishManga(manga: Manga) {
|
||||
if (manga.source == MangaSource.LOCAL) {
|
||||
localMangaStateFlow
|
||||
} else {
|
||||
onlineMangaStateFlow
|
||||
}.value = manga
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,7 @@ import androidx.room.withTransaction
|
||||
import dagger.Reusable
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
@@ -17,7 +15,6 @@ import org.koitharu.kotatsu.core.db.entity.toMangaTag
|
||||
import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
|
||||
import org.koitharu.kotatsu.core.util.ext.mapItems
|
||||
import org.koitharu.kotatsu.history.data.HistoryEntity
|
||||
@@ -161,18 +158,6 @@ class HistoryRepository @Inject constructor(
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
fun observeShouldSkip(mangaFlow: Flow<Manga?>): Flow<Boolean> {
|
||||
return mangaFlow
|
||||
.distinctUntilChangedBy { it?.isNsfw }
|
||||
.flatMapLatest { m ->
|
||||
if (m != null) {
|
||||
observeShouldSkip(m)
|
||||
} else {
|
||||
settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun recover(ids: Collection<Long>) {
|
||||
db.withTransaction {
|
||||
for (id in ids) {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package org.koitharu.kotatsu.local.domain
|
||||
|
||||
import dagger.Reusable
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import org.koitharu.kotatsu.core.model.DoubleManga
|
||||
import org.koitharu.kotatsu.core.model.isLocal
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import javax.inject.Inject
|
||||
|
||||
@Reusable
|
||||
class DoubleMangaLoader @Inject constructor(
|
||||
private val mangaDataRepository: MangaDataRepository,
|
||||
private val localMangaRepository: LocalMangaRepository,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
) {
|
||||
|
||||
suspend fun load(manga: Manga): DoubleManga = coroutineScope {
|
||||
val remoteDeferred = async(Dispatchers.Default) { loadRemote(manga) }
|
||||
val localDeferred = async(Dispatchers.Default) { loadLocal(manga) }
|
||||
DoubleManga(
|
||||
remoteManga = remoteDeferred.await(),
|
||||
localManga = localDeferred.await(),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun load(mangaId: Long): DoubleManga {
|
||||
val manga = mangaDataRepository.findMangaById(mangaId) ?: throwNFE()
|
||||
return load(manga)
|
||||
}
|
||||
|
||||
suspend fun load(intent: MangaIntent): DoubleManga {
|
||||
val manga = mangaDataRepository.resolveIntent(intent) ?: throwNFE()
|
||||
return load(manga)
|
||||
}
|
||||
|
||||
private suspend fun loadLocal(manga: Manga): Result<Manga>? {
|
||||
return runCatchingCancellable {
|
||||
if (manga.isLocal) {
|
||||
localMangaRepository.getDetails(manga)
|
||||
} else {
|
||||
localMangaRepository.findSavedManga(manga)?.manga
|
||||
} ?: return null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadRemote(manga: Manga): Result<Manga>? {
|
||||
return runCatchingCancellable {
|
||||
val seed = if (manga.isLocal) {
|
||||
localMangaRepository.getRemoteManga(manga)
|
||||
} else {
|
||||
manga
|
||||
} ?: return null
|
||||
val repository = mangaRepositoryFactory.create(seed.source)
|
||||
repository.getDetails(seed)
|
||||
}
|
||||
}
|
||||
|
||||
private fun throwNFE(): Nothing = throw NotFoundException("Cannot find manga", "")
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import org.koitharu.kotatsu.core.model.isLocal
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.util.CompositeMutex
|
||||
import org.koitharu.kotatsu.core.util.ext.deleteAwait
|
||||
@@ -99,8 +100,11 @@ class LocalMangaRepository @Inject constructor(
|
||||
suspend fun deleteChapters(manga: Manga, ids: Set<Long>) {
|
||||
lockManga(manga.id)
|
||||
try {
|
||||
LocalMangaUtil(manga).deleteChapters(ids)
|
||||
localStorageChanges.emit(LocalManga(manga))
|
||||
val subject = if (manga.isLocal) manga else checkNotNull(findSavedManga(manga)) {
|
||||
"Manga is not stored on local storage"
|
||||
}.manga
|
||||
LocalMangaUtil(subject).deleteChapters(ids)
|
||||
localStorageChanges.emit(LocalManga(subject))
|
||||
} finally {
|
||||
unlockManga(manga.id)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,3 +18,6 @@ scat
|
||||
тентакли
|
||||
футанари
|
||||
инцест
|
||||
boys' love
|
||||
girls' love
|
||||
bdsm
|
||||
|
||||
Reference in New Issue
Block a user