Refactor manga details loading
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
package org.koitharu.kotatsu.details.data
|
||||
|
||||
import org.koitharu.kotatsu.core.model.isLocal
|
||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.reader.data.filterChapters
|
||||
|
||||
data class MangaDetails(
|
||||
private val manga: Manga,
|
||||
private val localManga: LocalManga?,
|
||||
val description: CharSequence?,
|
||||
val isLoaded: Boolean,
|
||||
) {
|
||||
|
||||
val id: Long
|
||||
get() = manga.id
|
||||
|
||||
val chapters: Map<String?, List<MangaChapter>> = manga.chapters?.groupBy { it.branch }.orEmpty()
|
||||
|
||||
val branches: Set<String?>
|
||||
get() = chapters.keys
|
||||
|
||||
val allChapters: List<MangaChapter>
|
||||
get() = manga.chapters.orEmpty()
|
||||
|
||||
val isLocal
|
||||
get() = manga.isLocal
|
||||
|
||||
val local: LocalManga?
|
||||
get() = localManga ?: if (manga.isLocal) LocalManga(manga) else null
|
||||
|
||||
fun toManga() = manga
|
||||
|
||||
fun filterChapters(branch: String?) = MangaDetails(
|
||||
manga = manga.filterChapters(branch),
|
||||
localManga = localManga?.run {
|
||||
copy(manga = manga.filterChapters(branch))
|
||||
},
|
||||
description = description,
|
||||
isLoaded = isLoaded,
|
||||
)
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
import org.koitharu.kotatsu.details.domain.model.DoubleManga
|
||||
import org.koitharu.kotatsu.details.data.MangaDetails
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||
@@ -20,7 +20,7 @@ import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
@Deprecated("")
|
||||
/* TODO: remove */
|
||||
class DetailsInteractor @Inject constructor(
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val favouritesRepository: FavouritesRepository,
|
||||
@@ -66,13 +66,22 @@ class DetailsInteractor @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateLocal(subject: DoubleManga?, localManga: LocalManga): DoubleManga? {
|
||||
return if (subject?.any?.id == localManga.manga.id) {
|
||||
subject.copy(
|
||||
localManga = runCatchingCancellable {
|
||||
localMangaRepository.getDetails(localManga.manga)
|
||||
},
|
||||
)
|
||||
suspend fun updateLocal(subject: MangaDetails?, localManga: LocalManga): MangaDetails? {
|
||||
subject ?: return null
|
||||
return if (subject.id == localManga.manga.id) {
|
||||
if (subject.isLocal) {
|
||||
subject.copy(
|
||||
manga = localManga.manga,
|
||||
)
|
||||
} else {
|
||||
subject.copy(
|
||||
localManga = runCatchingCancellable {
|
||||
localManga.copy(
|
||||
manga = localMangaRepository.getDetails(localManga.manga),
|
||||
)
|
||||
}.getOrNull() ?: subject.local,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
subject
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package org.koitharu.kotatsu.details.domain
|
||||
|
||||
import android.text.Html
|
||||
import android.text.SpannableString
|
||||
import android.text.Spanned
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import androidx.core.text.getSpans
|
||||
import androidx.core.text.parseAsHtml
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
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.core.util.ext.peek
|
||||
import org.koitharu.kotatsu.core.util.ext.sanitize
|
||||
import org.koitharu.kotatsu.details.data.MangaDetails
|
||||
import org.koitharu.kotatsu.explore.domain.RecoverMangaUseCase
|
||||
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.recoverNotNull
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import javax.inject.Inject
|
||||
|
||||
class DetailsLoadUseCase @Inject constructor(
|
||||
private val mangaDataRepository: MangaDataRepository,
|
||||
private val localMangaRepository: LocalMangaRepository,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
private val recoverUseCase: RecoverMangaUseCase,
|
||||
private val imageGetter: Html.ImageGetter,
|
||||
) {
|
||||
|
||||
operator fun invoke(intent: MangaIntent): Flow<MangaDetails> = channelFlow {
|
||||
val manga = requireNotNull(mangaDataRepository.resolveIntent(intent)) {
|
||||
"Cannot resolve intent $intent"
|
||||
}
|
||||
val local = if (!manga.isLocal) {
|
||||
async {
|
||||
localMangaRepository.findSavedManga(manga)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
send(MangaDetails(manga, null, null, false))
|
||||
val details = getDetails(manga)
|
||||
send(MangaDetails(details, local?.peek(), details.description?.parseAsHtml(withImages = false), false))
|
||||
send(MangaDetails(details, local?.await(), details.description?.parseAsHtml(withImages = true), true))
|
||||
}
|
||||
|
||||
private suspend fun getDetails(seed: Manga) = runCatchingCancellable {
|
||||
val repository = mangaRepositoryFactory.create(seed.source)
|
||||
repository.getDetails(seed)
|
||||
}.recoverNotNull { e ->
|
||||
if (e is NotFoundException) {
|
||||
recoverUseCase(seed)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.getOrThrow()
|
||||
|
||||
private suspend fun String.parseAsHtml(withImages: Boolean): CharSequence? {
|
||||
return if (withImages) {
|
||||
runInterruptible(Dispatchers.IO) {
|
||||
parseAsHtml(imageGetter = imageGetter)
|
||||
}.filterSpans()
|
||||
} else {
|
||||
runInterruptible(Dispatchers.Default) {
|
||||
parseAsHtml()
|
||||
}.filterSpans().sanitize()
|
||||
}.takeUnless { it.isBlank() }
|
||||
}
|
||||
|
||||
private fun Spanned.filterSpans(): Spanned {
|
||||
val spannable = SpannableString.valueOf(this)
|
||||
val spans = spannable.getSpans<ForegroundColorSpan>()
|
||||
for (span in spans) {
|
||||
spannable.removeSpan(span)
|
||||
}
|
||||
return spannable
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package org.koitharu.kotatsu.details.domain
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import org.koitharu.kotatsu.core.model.isLocal
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.details.domain.model.DoubleManga
|
||||
import org.koitharu.kotatsu.explore.domain.RecoverMangaUseCase
|
||||
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.recoverNotNull
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import javax.inject.Inject
|
||||
|
||||
class DoubleMangaLoadUseCase @Inject constructor(
|
||||
private val mangaDataRepository: MangaDataRepository,
|
||||
private val localMangaRepository: LocalMangaRepository,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
private val recoverUseCase: RecoverMangaUseCase,
|
||||
) {
|
||||
|
||||
operator fun invoke(manga: Manga): Flow<DoubleManga> = flow {
|
||||
var lastValue: DoubleManga? = null
|
||||
var emitted = false
|
||||
invokeImpl(manga).collect {
|
||||
lastValue = it
|
||||
if (it.any != null) {
|
||||
emitted = true
|
||||
emit(it)
|
||||
}
|
||||
}
|
||||
if (!emitted) {
|
||||
lastValue?.requireAny()
|
||||
}
|
||||
}.flowOn(Dispatchers.Default)
|
||||
|
||||
operator fun invoke(mangaId: Long): Flow<DoubleManga> = flow {
|
||||
emit(mangaDataRepository.findMangaById(mangaId) ?: throwNFE())
|
||||
}.flatMapLatest { invoke(it) }
|
||||
|
||||
operator fun invoke(intent: MangaIntent): Flow<DoubleManga> = flow {
|
||||
emit(mangaDataRepository.resolveIntent(intent) ?: throwNFE())
|
||||
}.flatMapLatest { invoke(it) }
|
||||
|
||||
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)
|
||||
}.recoverNotNull { e ->
|
||||
if (e is NotFoundException) {
|
||||
recoverUseCase(manga)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun invokeImpl(manga: Manga): Flow<DoubleManga> = combine(
|
||||
flow { emit(null); emit(loadRemote(manga)) },
|
||||
flow { emit(null); emit(loadLocal(manga)) },
|
||||
) { remote, local ->
|
||||
DoubleManga(
|
||||
remoteManga = remote,
|
||||
localManga = local,
|
||||
)
|
||||
}
|
||||
|
||||
private fun throwNFE(): Nothing = throw NotFoundException("Cannot find manga", "")
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package org.koitharu.kotatsu.details.domain.model
|
||||
|
||||
import org.koitharu.kotatsu.core.model.findById
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
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 hasChapter(id: Long): Boolean {
|
||||
return local?.chapters?.findById(id) != null || remote?.chapters?.findById(id) != null
|
||||
}
|
||||
|
||||
fun requireAny(): Manga {
|
||||
val result = remoteManga?.getOrNull() ?: localManga?.getOrNull()
|
||||
if (result != null) {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -2,21 +2,19 @@ package org.koitharu.kotatsu.details.ui
|
||||
|
||||
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.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.details.ui.model.toListItem
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
|
||||
fun mapChapters(
|
||||
remoteManga: Manga?,
|
||||
localManga: Manga?,
|
||||
fun MangaDetails.mapChapters(
|
||||
history: MangaHistory?,
|
||||
newCount: Int,
|
||||
branch: String?,
|
||||
bookmarks: List<Bookmark>,
|
||||
): List<ChapterListItem> {
|
||||
val remoteChapters = remoteManga?.getChapters(branch).orEmpty()
|
||||
val localChapters = localManga?.getChapters(branch).orEmpty()
|
||||
val remoteChapters = chapters[branch].orEmpty()
|
||||
val localChapters = local?.manga?.getChapters(branch).orEmpty()
|
||||
if (remoteChapters.isEmpty() && localChapters.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
@@ -57,7 +55,7 @@ fun mapChapters(
|
||||
isCurrent = chapter.id == currentId,
|
||||
isUnread = isUnread,
|
||||
isNew = false,
|
||||
isDownloaded = remoteManga != null,
|
||||
isDownloaded = !isLocal,
|
||||
isBookmarked = chapter.id in bookmarked,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ class DetailsActivity :
|
||||
viewBinding.buttonRead.setOnContextClickListenerCompat(this)
|
||||
viewBinding.buttonDropdown.setOnClickListener(this)
|
||||
viewBadge = ViewBadge(viewBinding.buttonRead, this)
|
||||
viewBadge.setMaxCharacterCount(1)
|
||||
|
||||
if (viewBinding.layoutBottom != null) {
|
||||
val behavior = BottomSheetBehavior.from(checkNotNull(viewBinding.layoutBottom))
|
||||
@@ -139,7 +140,7 @@ class DetailsActivity :
|
||||
}
|
||||
viewModel.isChaptersReversed.observe(
|
||||
this,
|
||||
MenuInvalidator(viewBinding.toolbarChapters ?: this)
|
||||
MenuInvalidator(viewBinding.toolbarChapters ?: this),
|
||||
)
|
||||
viewModel.favouriteCategories.observe(this, MenuInvalidator(this))
|
||||
viewModel.branches.observe(this) {
|
||||
@@ -148,7 +149,7 @@ class DetailsActivity :
|
||||
viewModel.chapters.observe(this, PrefetchObserver(this))
|
||||
viewModel.onDownloadStarted.observeEvent(
|
||||
this,
|
||||
DownloadStartedObserver(viewBinding.containerDetails)
|
||||
DownloadStartedObserver(viewBinding.containerDetails),
|
||||
)
|
||||
|
||||
addMenuProvider(
|
||||
@@ -255,7 +256,7 @@ class DetailsActivity :
|
||||
window.setNavigationBarTransparentCompat(
|
||||
this,
|
||||
viewBinding.layoutBottom?.elevation ?: 0f,
|
||||
0.9f
|
||||
0.9f,
|
||||
)
|
||||
}
|
||||
viewBinding.cardChapters?.updateLayoutParams<MarginLayoutParams> {
|
||||
@@ -281,14 +282,14 @@ class DetailsActivity :
|
||||
info.currentChapter >= 0 -> getString(
|
||||
R.string.chapter_d_of_d,
|
||||
info.currentChapter + 1,
|
||||
info.totalChapters
|
||||
info.totalChapters,
|
||||
)
|
||||
|
||||
info.totalChapters == 0 -> getString(R.string.no_chapters)
|
||||
else -> resources.getQuantityString(
|
||||
R.plurals.chapters,
|
||||
info.totalChapters,
|
||||
info.totalChapters
|
||||
info.totalChapters,
|
||||
)
|
||||
}
|
||||
viewBinding.toolbarChapters?.title = text
|
||||
@@ -311,8 +312,8 @@ class DetailsActivity :
|
||||
ForegroundColorSpan(
|
||||
v.context.getThemeColor(
|
||||
android.R.attr.textColorSecondary,
|
||||
Color.LTGRAY
|
||||
)
|
||||
Color.LTGRAY,
|
||||
),
|
||||
),
|
||||
RelativeSizeSpan(0.74f),
|
||||
) {
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
package org.koitharu.kotatsu.details.ui
|
||||
|
||||
import android.text.Html
|
||||
import android.text.SpannableString
|
||||
import android.text.Spanned
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.text.getSpans
|
||||
import androidx.core.text.parseAsHtml
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
@@ -17,7 +10,7 @@ import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNot
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
@@ -25,7 +18,6 @@ import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
@@ -40,17 +32,14 @@ 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.call
|
||||
import org.koitharu.kotatsu.core.util.ext.combine
|
||||
import org.koitharu.kotatsu.core.util.ext.computeSize
|
||||
import org.koitharu.kotatsu.core.util.ext.onFirst
|
||||
import org.koitharu.kotatsu.core.util.ext.requireValue
|
||||
import org.koitharu.kotatsu.core.util.ext.sanitize
|
||||
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
|
||||
import org.koitharu.kotatsu.details.data.MangaDetails
|
||||
import org.koitharu.kotatsu.details.domain.BranchComparator
|
||||
import org.koitharu.kotatsu.details.domain.DetailsInteractor
|
||||
import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase
|
||||
import org.koitharu.kotatsu.details.domain.DetailsLoadUseCase
|
||||
import org.koitharu.kotatsu.details.domain.RelatedMangaUseCase
|
||||
import org.koitharu.kotatsu.details.domain.model.DoubleManga
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
|
||||
import org.koitharu.kotatsu.details.ui.model.MangaBranch
|
||||
@@ -74,22 +63,19 @@ class DetailsViewModel @Inject constructor(
|
||||
private val bookmarksRepository: BookmarksRepository,
|
||||
private val settings: AppSettings,
|
||||
private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
|
||||
private val imageGetter: Html.ImageGetter,
|
||||
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
|
||||
private val downloadScheduler: DownloadWorker.Scheduler,
|
||||
private val interactor: DetailsInteractor,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
|
||||
private val doubleMangaLoadUseCase: DoubleMangaLoadUseCase,
|
||||
private val relatedMangaUseCase: RelatedMangaUseCase,
|
||||
private val extraProvider: ListExtraProvider,
|
||||
private val detailsLoadUseCase: DetailsLoadUseCase,
|
||||
networkState: NetworkState,
|
||||
) : 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 = MutableEventFlow<Int>()
|
||||
@@ -97,8 +83,9 @@ class DetailsViewModel @Inject constructor(
|
||||
val onSelectChapter = MutableEventFlow<Long>()
|
||||
val onDownloadStarted = MutableEventFlow<Unit>()
|
||||
|
||||
val manga = doubleManga.map { it?.any }
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, doubleManga.value?.any)
|
||||
val details = MutableStateFlow(intent.manga?.let { MangaDetails(it, null, null, false) })
|
||||
val manga = details.map { x -> x?.toManga() }
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
||||
|
||||
val history = historyRepository.observeOne(mangaId)
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
||||
@@ -135,28 +122,17 @@ class DetailsViewModel @Inject constructor(
|
||||
if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList())
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, emptyList())
|
||||
|
||||
val localSize = doubleManga
|
||||
.map {
|
||||
val local = it?.local
|
||||
if (local != null) {
|
||||
val file = local.url.toUri().toFileOrNull()
|
||||
file?.computeSize() ?: 0L
|
||||
} else {
|
||||
0L
|
||||
}
|
||||
val localSize = details
|
||||
.map { it?.local }
|
||||
.distinctUntilChanged()
|
||||
.map { local ->
|
||||
local?.file?.computeSize() ?: 0L
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(), 0)
|
||||
|
||||
val description = manga
|
||||
.distinctUntilChangedBy { it?.description.orEmpty() }
|
||||
.transformLatest {
|
||||
val description = it?.description
|
||||
if (description.isNullOrEmpty()) {
|
||||
emit(null)
|
||||
} else {
|
||||
emit(description.parseAsHtml().filterSpans().sanitize())
|
||||
emit(description.parseAsHtml(imageGetter = imageGetter).filterSpans())
|
||||
}
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(5000), null)
|
||||
@Deprecated("")
|
||||
val description = details
|
||||
.map { it?.description }
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, null)
|
||||
|
||||
val onMangaRemoved = MutableEventFlow<Manga>()
|
||||
val isScrobblingAvailable: Boolean
|
||||
@@ -165,9 +141,7 @@ class DetailsViewModel @Inject constructor(
|
||||
val scrobblingInfo: StateFlow<List<ScrobblingInfo>> = interactor.observeScrobblingInfo(mangaId)
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
val relatedManga: StateFlow<List<MangaItemModel>> = doubleManga.map {
|
||||
it?.remote
|
||||
}.distinctUntilChangedBy { it?.id }
|
||||
val relatedManga: StateFlow<List<MangaItemModel>> = manga
|
||||
.mapLatest {
|
||||
if (it != null && settings.isRelatedMangaEnabled) {
|
||||
relatedMangaUseCase.invoke(it)?.toUi(ListMode.GRID, extraProvider).orEmpty()
|
||||
@@ -178,40 +152,32 @@ class DetailsViewModel @Inject constructor(
|
||||
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
|
||||
|
||||
val branches: StateFlow<List<MangaBranch>> = combine(
|
||||
doubleManga,
|
||||
details,
|
||||
selectedBranch,
|
||||
) { m, b ->
|
||||
val chapters = m?.chapters
|
||||
if (chapters.isNullOrEmpty()) return@combine emptyList()
|
||||
chapters.groupBy { x -> x.branch }
|
||||
(m?.chapters ?: return@combine emptyList())
|
||||
.map { x -> MangaBranch(x.key, x.value.size, x.key == b) }
|
||||
.sortedWith(BranchComparator())
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
val isChaptersEmpty: StateFlow<Boolean> = combine(
|
||||
doubleManga,
|
||||
isLoading,
|
||||
) { manga, loading ->
|
||||
manga?.any != null && manga.chapters.isNullOrEmpty() && !loading
|
||||
val isChaptersEmpty: StateFlow<Boolean> = details.map {
|
||||
it != null && it.isLoaded && it.allChapters.isEmpty()
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
|
||||
|
||||
val chapters = combine(
|
||||
combine(
|
||||
doubleManga,
|
||||
details,
|
||||
history,
|
||||
selectedBranch,
|
||||
newChaptersCount,
|
||||
bookmarks,
|
||||
networkState,
|
||||
) { manga, history, branch, news, bookmarks, isOnline ->
|
||||
mapChapters(
|
||||
manga?.remote?.takeIf { isOnline },
|
||||
manga?.local,
|
||||
) { manga, history, branch, news, bookmarks ->
|
||||
manga?.mapChapters(
|
||||
history,
|
||||
news,
|
||||
branch,
|
||||
bookmarks,
|
||||
)
|
||||
).orEmpty()
|
||||
},
|
||||
isChaptersReversed,
|
||||
chaptersQuery,
|
||||
@@ -242,7 +208,7 @@ class DetailsViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun deleteLocal() {
|
||||
val m = doubleManga.value?.local
|
||||
val m = details.value?.local?.manga
|
||||
if (m == null) {
|
||||
onShowToast.call(R.string.file_not_found)
|
||||
return
|
||||
@@ -295,13 +261,13 @@ class DetailsViewModel @Inject constructor(
|
||||
|
||||
fun markChapterAsCurrent(chapterId: Long) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
val manga = checkNotNull(doubleManga.value)
|
||||
val chapters = checkNotNull(manga.filterChapters(selectedBranchValue).chapters)
|
||||
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.requireAny(),
|
||||
manga = manga.toManga(),
|
||||
chapterId = chapterId,
|
||||
page = 0,
|
||||
scroll = 0,
|
||||
@@ -313,7 +279,7 @@ class DetailsViewModel @Inject constructor(
|
||||
fun download(chaptersIds: Set<Long>?) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
downloadScheduler.schedule(
|
||||
doubleManga.requireValue().requireAny(),
|
||||
details.requireValue().toManga(),
|
||||
chaptersIds,
|
||||
)
|
||||
onDownloadStarted.call(Unit)
|
||||
@@ -333,14 +299,14 @@ class DetailsViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
|
||||
doubleMangaLoadUseCase.invoke(intent)
|
||||
detailsLoadUseCase.invoke(intent)
|
||||
.onFirst {
|
||||
val manga = it.requireAny()
|
||||
val manga = it.toManga()
|
||||
// find default branch
|
||||
val hist = historyRepository.getOne(manga)
|
||||
selectedBranch.value = manga.getPreferredBranch(hist)
|
||||
}.collect {
|
||||
doubleManga.value = it
|
||||
details.value = it
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,21 +322,12 @@ class DetailsViewModel @Inject constructor(
|
||||
private suspend fun onDownloadComplete(downloadedManga: LocalManga?) {
|
||||
downloadedManga ?: return
|
||||
launchJob {
|
||||
doubleManga.update {
|
||||
details.update {
|
||||
interactor.updateLocal(it, downloadedManga)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Spanned.filterSpans(): CharSequence {
|
||||
val spannable = SpannableString.valueOf(this)
|
||||
val spans = spannable.getSpans<ForegroundColorSpan>()
|
||||
for (span in spans) {
|
||||
spannable.removeSpan(span)
|
||||
}
|
||||
return spannable.trim()
|
||||
}
|
||||
|
||||
private fun getScrobbler(index: Int): Scrobbler? {
|
||||
val info = scrobblingInfo.value.getOrNull(index)
|
||||
val scrobbler = if (info != null) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.util.ext.drawableStart
|
||||
import org.koitharu.kotatsu.core.util.ext.drawableEnd
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
||||
import org.koitharu.kotatsu.databinding.ItemChapterBinding
|
||||
@@ -47,8 +47,7 @@ fun chapterListItemAD(
|
||||
}
|
||||
binding.imageViewBookmarked.isVisible = item.isBookmarked
|
||||
binding.imageViewDownloaded.isVisible = item.isDownloaded
|
||||
// binding.imageViewNew.isVisible = item.isNew
|
||||
binding.textViewTitle.drawableStart = if (item.isNew) {
|
||||
binding.textViewTitle.drawableEnd = if (item.isNew) {
|
||||
ContextCompat.getDrawable(context, R.drawable.ic_new)
|
||||
} else {
|
||||
null
|
||||
|
||||
Reference in New Issue
Block a user