Refactor manga details loading
parent
fbb267e11c
commit
e4efd0f696
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue