Refactor manga details loading

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

View File

@@ -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,
)
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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", "")
}

View File

@@ -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
}
}

View File

@@ -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,
)
}

View File

@@ -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),
) {

View File

@@ -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) {

View File

@@ -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