diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaIntent.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaIntent.kt index 8ab582147..25dd7749e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaIntent.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaIntent.kt @@ -43,5 +43,7 @@ class MangaIntent private constructor( const val KEY_MANGA = "manga" const val KEY_ID = "id" + + fun of(manga: Manga) = MangaIntent(manga, manga.id, null) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ViewBadge.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ViewBadge.kt index e8aa4263d..3b8ccf955 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ViewBadge.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ViewBadge.kt @@ -15,6 +15,7 @@ class ViewBadge( ) : View.OnLayoutChangeListener, DefaultLifecycleObserver { private var badgeDrawable: BadgeDrawable? = null + private var maxCharacterCount: Int = -1 var counter: Int get() = badgeDrawable?.number ?: 0 @@ -48,8 +49,16 @@ class ViewBadge( clearBadge() } + fun setMaxCharacterCount(value: Int) { + maxCharacterCount = value + badgeDrawable?.maxCharacterCount = value + } + private fun initBadge(): BadgeDrawable { val badge = BadgeDrawable.create(anchor.context) + if (maxCharacterCount > 0) { + badge.maxCharacterCount = maxCharacterCount + } anchor.addOnLayoutChangeListener(this) BadgeUtils.attachBadgeDrawable(badge, anchor) badgeDrawable = badge diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Coroutines.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Coroutines.kt index a7c16d706..3eed0b6a8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Coroutines.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Coroutines.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.suspendCancellableCoroutine import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @@ -79,3 +80,11 @@ fun Deferred.getCompletionResultOrNull(): Result? = if (isCompleted) { } else { null } + +fun Deferred.peek(): T? = if (isCompleted) { + runCatchingCancellable { + getCompleted() + }.getOrNull() +} else { + null +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/data/MangaDetails.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/data/MangaDetails.kt new file mode 100644 index 000000000..cc68e0a6c --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/data/MangaDetails.kt @@ -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> = manga.chapters?.groupBy { it.branch }.orEmpty() + + val branches: Set + get() = chapters.keys + + val allChapters: List + 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, + ) +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DetailsInteractor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DetailsInteractor.kt index 350c66772..947198803 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DetailsInteractor.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DetailsInteractor.kt @@ -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 } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DetailsLoadUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DetailsLoadUseCase.kt new file mode 100644 index 000000000..5c48d6a34 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DetailsLoadUseCase.kt @@ -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 = 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() + for (span in spans) { + spannable.removeSpan(span) + } + return spannable + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DoubleMangaLoadUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DoubleMangaLoadUseCase.kt deleted file mode 100644 index 7726cca92..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DoubleMangaLoadUseCase.kt +++ /dev/null @@ -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 = 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 = flow { - emit(mangaDataRepository.findMangaById(mangaId) ?: throwNFE()) - }.flatMapLatest { invoke(it) } - - operator fun invoke(intent: MangaIntent): Flow = flow { - emit(mangaDataRepository.resolveIntent(intent) ?: throwNFE()) - }.flatMapLatest { invoke(it) } - - private suspend fun loadLocal(manga: Manga): Result? { - return runCatchingCancellable { - if (manga.isLocal) { - localMangaRepository.getDetails(manga) - } else { - localMangaRepository.findSavedManga(manga)?.manga - } ?: return null - } - } - - private suspend fun loadRemote(manga: Manga): Result? { - 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 = 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", "") -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/model/DoubleManga.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/model/DoubleManga.kt deleted file mode 100644 index 749616006..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/model/DoubleManga.kt +++ /dev/null @@ -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?, - private val localManga: Result?, -) { - - 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? 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? { - 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(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 - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersMapper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersMapper.kt index 257d4bef5..5a4209f9b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersMapper.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersMapper.kt @@ -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, ): List { - 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, ) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index 95e5af30b..ffa143919 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -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 { @@ -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), ) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt index 8ca533d5a..1348d62b9 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt @@ -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, 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 = - MutableStateFlow(intent.manga?.let { DoubleManga(it) }) private var loadingJob: Job val onShowToast = MutableEventFlow() @@ -97,8 +83,9 @@ class DetailsViewModel @Inject constructor( val onSelectChapter = MutableEventFlow() val onDownloadStarted = MutableEventFlow() - 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() val isScrobblingAvailable: Boolean @@ -165,9 +141,7 @@ class DetailsViewModel @Inject constructor( val scrobblingInfo: StateFlow> = interactor.observeScrobblingInfo(mangaId) .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList()) - val relatedManga: StateFlow> = doubleManga.map { - it?.remote - }.distinctUntilChangedBy { it?.id } + val relatedManga: StateFlow> = 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> = 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 = combine( - doubleManga, - isLoading, - ) { manga, loading -> - manga?.any != null && manga.chapters.isNullOrEmpty() && !loading + val isChaptersEmpty: StateFlow = 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?) { 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() - 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) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChapterListItemAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChapterListItemAD.kt index 9865b59cb..00efda3c3 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChapterListItemAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChapterListItemAD.kt @@ -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 diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt index 80a01682f..fd8a4ab58 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt @@ -93,8 +93,11 @@ class HistoryRepository @Inject constructor( } val tags = manga.tags.toEntities() db.withTransaction { - db.tagsDao.upsert(tags) - db.mangaDao.upsert(manga.toEntity(), tags) + val existing = db.mangaDao.find(manga.id)?.manga + if (existing == null || existing.source == manga.source.name) { + db.tagsDao.upsert(tags) + db.mangaDao.upsert(manga.toEntity(), tags) + } db.historyDao.upsert( HistoryEntity( mangaId = manga.id, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/ChaptersLoader.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/ChaptersLoader.kt index b60c842b9..dec74bbbf 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/ChaptersLoader.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/ChaptersLoader.kt @@ -2,16 +2,10 @@ package org.koitharu.kotatsu.reader.domain import android.util.LongSparseArray import dagger.hilt.android.scopes.ViewModelScoped -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.koitharu.kotatsu.core.parser.MangaRepository -import org.koitharu.kotatsu.details.domain.model.DoubleManga +import org.koitharu.kotatsu.details.data.MangaDetails import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import javax.inject.Inject @@ -23,32 +17,24 @@ class ChaptersLoader @Inject constructor( private val mangaRepositoryFactory: MangaRepository.Factory, ) { - private val chapters = MutableStateFlow(LongSparseArray(0)) + private val chapters = LongSparseArray() private val chapterPages = ChapterPages() private val mutex = Mutex() - val size: Int // TODO flow - get() = chapters.value.size() + val size: Int + get() = chapters.size() - fun init(scope: CoroutineScope, manga: Flow) = scope.launch { - manga.collect { - val ch = it.chapters.orEmpty() - val longSparseArray = LongSparseArray(ch.size) - ch.forEach { x -> longSparseArray.put(x.id, x) } - mutex.withLock { - chapters.value = longSparseArray - } + suspend fun init(manga: MangaDetails) = mutex.withLock { + chapters.clear() + manga.allChapters.forEach { + chapters.put(it.id, it) } } - suspend fun loadPrevNextChapter(manga: DoubleManga, currentId: Long, isNext: Boolean) { - val chapters = manga.chapters ?: return + suspend fun loadPrevNextChapter(manga: MangaDetails, currentId: Long, isNext: Boolean) { + val chapters = manga.allChapters val predicate: (MangaChapter) -> Boolean = { it.id == currentId } - val index = if (isNext) { - chapters.indexOfFirst(predicate) - } else { - chapters.indexOfLast(predicate) - } + 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(newChapter.id) @@ -79,11 +65,7 @@ class ChaptersLoader @Inject constructor( } } - fun peekChapter(chapterId: Long): MangaChapter? = chapters.value[chapterId] - - suspend fun awaitChapter(chapterId: Long): MangaChapter? = chapters.mapNotNull { x -> - x[chapterId] - }.firstOrNull() + fun peekChapter(chapterId: Long): MangaChapter? = chapters[chapterId] fun getPages(chapterId: Long): List { return chapterPages.subList(chapterId) @@ -100,7 +82,7 @@ class ChaptersLoader @Inject constructor( fun snapshot() = chapterPages.toList() private suspend fun loadChapter(chapterId: Long): List { - val chapter = checkNotNull(awaitChapter(chapterId)) { "Requested chapter not found" } + val chapter = checkNotNull(chapters[chapterId]) { "Requested chapter not found" } val repo = mangaRepositoryFactory.create(chapter.source) return repo.getPages(chapter).mapIndexed { index, page -> ReaderPage(page, index, chapterId) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ChaptersSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ChaptersSheet.kt index 6ccf620fd..30b609152 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ChaptersSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ChaptersSheet.kt @@ -33,13 +33,11 @@ class ChaptersSheet : BaseAdaptiveSheet(), override fun onCreateViewBinding( inflater: LayoutInflater, container: ViewGroup?, - ): SheetChaptersBinding { - return SheetChaptersBinding.inflate(inflater, container, false) - } + ) = SheetChaptersBinding.inflate(inflater, container, false) override fun onViewBindingCreated(binding: SheetChaptersBinding, savedInstanceState: Bundle?) { super.onViewBindingCreated(binding, savedInstanceState) - val chapters = viewModel.manga?.chapters + val chapters = viewModel.manga?.allChapters if (chapters.isNullOrEmpty()) { dismissAllowingStateLoss() return @@ -61,7 +59,7 @@ class ChaptersSheet : BaseAdaptiveSheet(), val offset = (resources.getDimensionPixelSize(R.dimen.chapter_list_item_height) * 0.6).roundToInt() adapter.setItems( - items, RecyclerViewScrollCallback(binding.recyclerView, targetPosition, offset) + items, RecyclerViewScrollCallback(binding.recyclerView, targetPosition, offset), ) } else { adapter.items = items diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt index 2c04d3b03..e6dadab56 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt @@ -189,7 +189,7 @@ class ReaderActivity : val state = viewModel.getCurrentState() ?: return false PagesThumbnailsSheet.show( supportFragmentManager, - viewModel.manga?.any ?: return false, + viewModel.manga?.toManga() ?: return false, state.chapterId, state.page, ) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt index c922ab53f..bbb0aab24 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt @@ -44,12 +44,11 @@ import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.requireValue -import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase -import org.koitharu.kotatsu.details.domain.model.DoubleManga +import org.koitharu.kotatsu.details.data.MangaDetails +import org.koitharu.kotatsu.details.domain.DetailsLoadUseCase import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.history.data.PROGRESS_NONE import org.koitharu.kotatsu.history.domain.HistoryUpdateUseCase -import org.koitharu.kotatsu.parsers.exception.NotFoundException import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.reader.domain.ChaptersLoader @@ -74,7 +73,7 @@ class ReaderViewModel @Inject constructor( private val pageLoader: PageLoader, private val chaptersLoader: ChaptersLoader, private val appShortcutManager: AppShortcutManager, - private val doubleMangaLoadUseCase: DoubleMangaLoadUseCase, + private val detailsLoadUseCase: DetailsLoadUseCase, private val historyUpdateUseCase: HistoryUpdateUseCase, private val detectReaderModeUseCase: DetectReaderModeUseCase, ) : BaseViewModel() { @@ -88,9 +87,9 @@ class ReaderViewModel @Inject constructor( private var bookmarkJob: Job? = null private var stateChangeJob: Job? = null private val currentState = MutableStateFlow(savedStateHandle[ReaderActivity.EXTRA_STATE]) - private val mangaData = MutableStateFlow(intent.manga?.let { DoubleManga(it) }) + private val mangaData = MutableStateFlow(intent.manga?.let { MangaDetails(it, null, null, false) }) private val mangaFlow: Flow - get() = mangaData.map { it?.any } + get() = mangaData.map { it?.toManga() } val readerMode = MutableStateFlow(null) val onPageSaved = MutableEventFlow() @@ -98,7 +97,7 @@ class ReaderViewModel @Inject constructor( val uiState = MutableStateFlow(null) val content = MutableStateFlow(ReaderContent(emptyList(), null)) - val manga: DoubleManga? + val manga: MangaDetails? get() = mangaData.value val pageAnimation = settings.observeAsStateFlow( @@ -148,7 +147,7 @@ class ReaderViewModel @Inject constructor( }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false) val isBookmarkAdded = currentState.flatMapLatest { state -> - val manga = mangaData.value?.any + val manga = mangaData.value?.toManga() if (state == null || manga == null) { flowOf(false) } else { @@ -178,7 +177,7 @@ class ReaderViewModel @Inject constructor( fun switchMode(newMode: ReaderMode) { launchJob { - val manga = checkNotNull(mangaData.value?.any) + val manga = checkNotNull(mangaData.value?.toManga()) dataRepository.saveReaderMode( manga = manga, mode = newMode, @@ -199,7 +198,7 @@ class ReaderViewModel @Inject constructor( } val readerState = state ?: currentState.value ?: return historyUpdateUseCase.invokeAsync( - manga = mangaData.value?.any ?: return, + manga = mangaData.value?.toManga() ?: return, readerState = readerState, percent = computePercent(readerState.chapterId, readerState.page), ) @@ -295,7 +294,7 @@ class ReaderViewModel @Inject constructor( val state = checkNotNull(currentState.value) val page = checkNotNull(getCurrentPage()) { "Page not found" } val bookmark = Bookmark( - manga = checkNotNull(mangaData.value?.any), + manga = mangaData.requireValue().toManga(), pageId = page.id, chapterId = state.chapterId, page = state.page, @@ -315,7 +314,7 @@ class ReaderViewModel @Inject constructor( } bookmarkJob = launchJob { loadingJob?.join() - val manga = checkNotNull(mangaData.value?.any) + val manga = mangaData.requireValue().toManga() val state = checkNotNull(getCurrentState()) bookmarksRepository.removeBookmark(manga.id, state.chapterId, state.page) onShowToast.call(R.string.bookmark_removed) @@ -324,25 +323,19 @@ class ReaderViewModel @Inject constructor( private fun loadImpl() { loadingJob = launchLoadingJob(Dispatchers.Default) { - var manga = DoubleManga( - dataRepository.resolveIntent(intent) - ?: throw NotFoundException("Cannot find manga", ""), - ) - mangaData.value = manga - val mangaFlow = doubleMangaLoadUseCase(intent) - manga = mangaFlow.first { x -> x.any != null } - chaptersLoader.init(viewModelScope, mangaFlow.withErrorHandling()) - // determine mode - val singleManga = manga.requireAny() + val details = detailsLoadUseCase.invoke(intent).first { x -> x.isLoaded } + mangaData.value = details + chaptersLoader.init(details) + val manga = details.toManga() // obtain state if (currentState.value == null) { - currentState.value = historyRepository.getOne(singleManga)?.let { + currentState.value = historyRepository.getOne(manga)?.let { ReaderState(it) - } ?: ReaderState(singleManga, preselectedBranch) + } ?: ReaderState(manga, preselectedBranch) } - val mode = detectReaderModeUseCase.invoke(singleManga, currentState.value) - val branch = chaptersLoader.awaitChapter(currentState.value?.chapterId ?: 0L)?.branch - mangaData.value = manga.filterChapters(branch) + val mode = detectReaderModeUseCase.invoke(manga, currentState.value) + val branch = chaptersLoader.peekChapter(currentState.value?.chapterId ?: 0L)?.branch + mangaData.value = details.filterChapters(branch) readerMode.value = mode chaptersLoader.loadSingleChapter(requireNotNull(currentState.value).chapterId) @@ -350,7 +343,7 @@ class ReaderViewModel @Inject constructor( if (!isIncognito) { currentState.value?.let { val percent = computePercent(it.chapterId, it.page) - historyUpdateUseCase.invoke(singleManga, it, percent) + historyUpdateUseCase.invoke(manga, it, percent) } } notifyStateChanged() @@ -383,11 +376,11 @@ class ReaderViewModel @Inject constructor( val state = getCurrentState() val chapter = state?.chapterId?.let { chaptersLoader.peekChapter(it) } val newState = ReaderUiState( - mangaName = manga?.any?.title, + mangaName = manga?.toManga()?.title, branch = chapter?.branch, chapterName = chapter?.name, chapterNumber = chapter?.number ?: 0, - chaptersTotal = manga?.any?.getChapters(chapter?.branch)?.size ?: 0, + chaptersTotal = manga?.chapters?.get(chapter?.branch)?.size ?: 0, totalPages = if (chapter != null) chaptersLoader.getPagesCount(chapter.id) else 0, currentPage = state?.page ?: 0, isSliderEnabled = settings.isReaderSliderEnabled, @@ -398,7 +391,7 @@ class ReaderViewModel @Inject constructor( private fun computePercent(chapterId: Long, pageIndex: Int): Float { val branch = chaptersLoader.peekChapter(chapterId)?.branch - val chapters = manga?.any?.getChapters(branch) ?: return PROGRESS_NONE + val chapters = manga?.chapters?.get(branch) ?: return PROGRESS_NONE val chaptersCount = chapters.size val chapterIndex = chapters.indexOfFirst { x -> x.id == chapterId } val pagesCount = chaptersLoader.getPagesCount(chapterId) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigSheet.kt index 109fc58f3..34dea5634 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigSheet.kt @@ -118,7 +118,7 @@ class ReaderConfigSheet : R.id.button_color_filter -> { val page = viewModel.getCurrentPage() ?: return - val manga = viewModel.manga?.any ?: return + val manga = viewModel.manga?.toManga() ?: return startActivity(ColorFilterConfigActivity.newIntent(v.context, manga, page)) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsViewModel.kt index 4b2809956..cc2cdf394 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsViewModel.kt @@ -7,17 +7,17 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.last import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import org.koitharu.kotatsu.core.model.findById import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga +import org.koitharu.kotatsu.core.parser.MangaIntent import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.util.ext.firstNotNull import org.koitharu.kotatsu.core.util.ext.require -import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase +import org.koitharu.kotatsu.details.domain.DetailsLoadUseCase import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.reader.domain.ChaptersLoader @@ -28,7 +28,7 @@ class PagesThumbnailsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, mangaRepositoryFactory: MangaRepository.Factory, private val chaptersLoader: ChaptersLoader, - doubleMangaLoadUseCase: DoubleMangaLoadUseCase, + detailsLoadUseCase: DetailsLoadUseCase, ) : BaseViewModel() { private val currentPageIndex: Int = @@ -37,7 +37,7 @@ class PagesThumbnailsViewModel @Inject constructor( val manga = savedStateHandle.require(PagesThumbnailsSheet.ARG_MANGA).manga private val repository = mangaRepositoryFactory.create(manga.source) - private val mangaDetails = doubleMangaLoadUseCase(manga).map { + private val mangaDetails = detailsLoadUseCase(MangaIntent.of(manga)).map { val b = manga.chapters?.findById(initialChapterId)?.branch branch.value = b it.filterChapters(b) @@ -52,8 +52,7 @@ class PagesThumbnailsViewModel @Inject constructor( init { loadingJob = launchLoadingJob(Dispatchers.Default) { - chaptersLoader.init(viewModelScope, mangaDetails.filterNotNull()) - mangaDetails.first { x -> x?.hasChapter(initialChapterId) == true } + chaptersLoader.init(checkNotNull(mangaDetails.last())) chaptersLoader.loadSingleChapter(initialChapterId) updateList() } @@ -79,13 +78,13 @@ class PagesThumbnailsViewModel @Inject constructor( updateList() } - private suspend fun updateList() { + private fun updateList() { val snapshot = chaptersLoader.snapshot() val pages = buildList(snapshot.size + chaptersLoader.size + 2) { var previousChapterId = 0L for (page in snapshot) { if (page.chapterId != previousChapterId) { - chaptersLoader.awaitChapter(page.chapterId)?.let { + chaptersLoader.peekChapter(page.chapterId)?.let { add(ListHeader(it.name)) } previousChapterId = page.chapterId