Refactor manga loading
parent
bfa9feaef0
commit
dc358ae6a2
@ -0,0 +1,76 @@
|
|||||||
|
package org.koitharu.kotatsu.core.model
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.reader.data.filterChapters
|
||||||
|
|
||||||
|
data class DoubleManga(
|
||||||
|
private val remoteManga: Result<Manga>?,
|
||||||
|
private val localManga: Result<Manga>?,
|
||||||
|
) {
|
||||||
|
|
||||||
|
constructor(manga: Manga) : this(
|
||||||
|
remoteManga = if (manga.source != MangaSource.LOCAL) Result.success(manga) else null,
|
||||||
|
localManga = if (manga.source == MangaSource.LOCAL) Result.success(manga) else null,
|
||||||
|
)
|
||||||
|
|
||||||
|
val remote: Manga?
|
||||||
|
get() = remoteManga?.getOrNull()
|
||||||
|
|
||||||
|
val local: Manga?
|
||||||
|
get() = localManga?.getOrNull()
|
||||||
|
|
||||||
|
val any: Manga?
|
||||||
|
get() = remote ?: local
|
||||||
|
|
||||||
|
val hasRemote: Boolean
|
||||||
|
get() = remoteManga?.isSuccess == true
|
||||||
|
|
||||||
|
val hasLocal: Boolean
|
||||||
|
get() = localManga?.isSuccess == true
|
||||||
|
|
||||||
|
val chapters: List<MangaChapter>? by lazy(LazyThreadSafetyMode.PUBLICATION) {
|
||||||
|
mergeChapters()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requireAny(): Manga {
|
||||||
|
val result = remoteManga?.getOrNull() ?: localManga?.getOrNull()
|
||||||
|
if (result != null) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
throw (
|
||||||
|
remoteManga?.exceptionOrNull()
|
||||||
|
?: localManga?.exceptionOrNull()
|
||||||
|
?: IllegalStateException("No online either local manga available")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun filterChapters(branch: String?) = DoubleManga(
|
||||||
|
remoteManga?.map { it.filterChapters(branch) },
|
||||||
|
localManga?.map { it.filterChapters(branch) },
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun mergeChapters(): List<MangaChapter>? {
|
||||||
|
val remoteChapters = remote?.chapters
|
||||||
|
val localChapters = local?.chapters
|
||||||
|
if (localChapters == null && remoteChapters == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val localMap = if (!localChapters.isNullOrEmpty()) {
|
||||||
|
localChapters.associateByTo(LinkedHashMap(localChapters.size)) { it.id }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
val result = ArrayList<MangaChapter>(maxOf(remoteChapters?.size ?: 0, localChapters?.size ?: 0))
|
||||||
|
remoteChapters?.forEach { r ->
|
||||||
|
localMap?.remove(r.id)?.let { l ->
|
||||||
|
result.add(l)
|
||||||
|
} ?: result.add(r)
|
||||||
|
}
|
||||||
|
localMap?.values?.let {
|
||||||
|
result.addAll(it)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
package org.koitharu.kotatsu.details.domain
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import org.koitharu.kotatsu.core.model.DoubleManga
|
||||||
|
import org.koitharu.kotatsu.core.model.isLocal
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||||
|
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||||
|
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||||
|
import org.koitharu.kotatsu.local.data.LocalManga
|
||||||
|
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
|
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
|
||||||
|
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||||
|
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||||
|
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||||
|
import java.io.IOException
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DetailsInteractor @Inject constructor(
|
||||||
|
private val historyRepository: HistoryRepository,
|
||||||
|
private val favouritesRepository: FavouritesRepository,
|
||||||
|
private val localMangaRepository: LocalMangaRepository,
|
||||||
|
private val trackingRepository: TrackingRepository,
|
||||||
|
private val settings: AppSettings,
|
||||||
|
private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun observeIsFavourite(mangaId: Long): Flow<Boolean> {
|
||||||
|
return favouritesRepository.observeCategoriesIds(mangaId)
|
||||||
|
.map { it.isNotEmpty() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun observeNewChapters(mangaId: Long): Flow<Int> {
|
||||||
|
return settings.observeAsFlow(AppSettings.KEY_TRACKER_ENABLED) { isTrackerEnabled }
|
||||||
|
.flatMapLatest { isEnabled ->
|
||||||
|
if (isEnabled) {
|
||||||
|
trackingRepository.observeNewChaptersCount(mangaId)
|
||||||
|
} else {
|
||||||
|
flowOf(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun observeScrobblingInfo(mangaId: Long): Flow<List<ScrobblingInfo>> {
|
||||||
|
return combine(
|
||||||
|
scrobblers.map { it.observeScrobblingInfo(mangaId) },
|
||||||
|
) { scrobblingInfo ->
|
||||||
|
scrobblingInfo.filterNotNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteLocalManga(manga: Manga) {
|
||||||
|
val victim = if (manga.isLocal) manga else localMangaRepository.findSavedManga(manga)?.manga
|
||||||
|
checkNotNull(victim) { "Cannot find saved manga for ${manga.title}" }
|
||||||
|
val original = if (manga.isLocal) localMangaRepository.getRemoteManga(manga) else manga
|
||||||
|
localMangaRepository.delete(victim) || throw IOException("Unable to delete file")
|
||||||
|
runCatchingCancellable {
|
||||||
|
historyRepository.deleteOrSwap(victim, original)
|
||||||
|
}.onFailure {
|
||||||
|
it.printStackTraceDebug()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun observeIncognitoMode(mangaFlow: Flow<Manga?>): Flow<Boolean> {
|
||||||
|
return mangaFlow
|
||||||
|
.distinctUntilChangedBy { it?.isNsfw }
|
||||||
|
.flatMapLatest { manga ->
|
||||||
|
if (manga != null) {
|
||||||
|
historyRepository.observeShouldSkip(manga)
|
||||||
|
} else {
|
||||||
|
settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateLocal(subject: DoubleManga?, localManga: LocalManga): DoubleManga? {
|
||||||
|
return if (subject?.any?.id == localManga.manga.id) {
|
||||||
|
subject.copy(
|
||||||
|
localManga = runCatchingCancellable {
|
||||||
|
localMangaRepository.getDetails(localManga.manga)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
subject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,89 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.ui
|
|
||||||
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
|
||||||
import dagger.hilt.android.ViewModelLifecycle
|
|
||||||
import dagger.hilt.android.scopes.ViewModelScoped
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
|
||||||
import org.koitharu.kotatsu.core.model.getPreferredBranch
|
|
||||||
import org.koitharu.kotatsu.core.os.NetworkState
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
|
||||||
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
|
|
||||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
|
||||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
|
||||||
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
|
||||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@ViewModelScoped
|
|
||||||
class MangaDetailsDelegate @Inject constructor(
|
|
||||||
savedStateHandle: SavedStateHandle,
|
|
||||||
lifecycle: ViewModelLifecycle,
|
|
||||||
private val mangaDataRepository: MangaDataRepository,
|
|
||||||
private val historyRepository: HistoryRepository,
|
|
||||||
private val localMangaRepository: LocalMangaRepository,
|
|
||||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
|
||||||
networkState: NetworkState,
|
|
||||||
) {
|
|
||||||
private val viewModelScope = RetainedLifecycleCoroutineScope(lifecycle)
|
|
||||||
|
|
||||||
private val intent = MangaIntent(savedStateHandle)
|
|
||||||
private val onlineMangaStateFlow = MutableStateFlow<Manga?>(null)
|
|
||||||
private val localMangaStateFlow = MutableStateFlow<Manga?>(null)
|
|
||||||
|
|
||||||
val onlineManga = combine(
|
|
||||||
onlineMangaStateFlow,
|
|
||||||
networkState,
|
|
||||||
) { m, s -> m.takeIf { s } }
|
|
||||||
.stateIn(viewModelScope, SharingStarted.Lazily, null)
|
|
||||||
val localManga = localMangaStateFlow.asStateFlow()
|
|
||||||
|
|
||||||
val selectedBranch = MutableStateFlow<String?>(null)
|
|
||||||
val mangaId = intent.manga?.id ?: intent.mangaId
|
|
||||||
|
|
||||||
init {
|
|
||||||
intent.manga?.let {
|
|
||||||
publishManga(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun doLoad() {
|
|
||||||
var manga = mangaDataRepository.resolveIntent(intent) ?: throw NotFoundException("Cannot find manga", "")
|
|
||||||
publishManga(manga)
|
|
||||||
manga = mangaRepositoryFactory.create(manga.source).getDetails(manga)
|
|
||||||
// find default branch
|
|
||||||
val hist = historyRepository.getOne(manga)
|
|
||||||
selectedBranch.value = manga.getPreferredBranch(hist)
|
|
||||||
publishManga(manga)
|
|
||||||
runCatchingCancellable {
|
|
||||||
if (manga.source == MangaSource.LOCAL) {
|
|
||||||
val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatchingCancellable null
|
|
||||||
mangaRepositoryFactory.create(m.source).getDetails(m)
|
|
||||||
} else {
|
|
||||||
localMangaRepository.findSavedManga(manga)?.manga
|
|
||||||
}
|
|
||||||
}.onFailure { error ->
|
|
||||||
error.printStackTraceDebug()
|
|
||||||
}.onSuccess {
|
|
||||||
if (it != null) {
|
|
||||||
publishManga(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun publishManga(manga: Manga) {
|
|
||||||
if (manga.source == MangaSource.LOCAL) {
|
|
||||||
localMangaStateFlow
|
|
||||||
} else {
|
|
||||||
onlineMangaStateFlow
|
|
||||||
}.value = manga
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
package org.koitharu.kotatsu.local.domain
|
||||||
|
|
||||||
|
import dagger.Reusable
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import org.koitharu.kotatsu.core.model.DoubleManga
|
||||||
|
import org.koitharu.kotatsu.core.model.isLocal
|
||||||
|
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||||
|
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||||
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
|
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@Reusable
|
||||||
|
class DoubleMangaLoader @Inject constructor(
|
||||||
|
private val mangaDataRepository: MangaDataRepository,
|
||||||
|
private val localMangaRepository: LocalMangaRepository,
|
||||||
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun load(manga: Manga): DoubleManga = coroutineScope {
|
||||||
|
val remoteDeferred = async(Dispatchers.Default) { loadRemote(manga) }
|
||||||
|
val localDeferred = async(Dispatchers.Default) { loadLocal(manga) }
|
||||||
|
DoubleManga(
|
||||||
|
remoteManga = remoteDeferred.await(),
|
||||||
|
localManga = localDeferred.await(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun load(mangaId: Long): DoubleManga {
|
||||||
|
val manga = mangaDataRepository.findMangaById(mangaId) ?: throwNFE()
|
||||||
|
return load(manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun load(intent: MangaIntent): DoubleManga {
|
||||||
|
val manga = mangaDataRepository.resolveIntent(intent) ?: throwNFE()
|
||||||
|
return load(manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadLocal(manga: Manga): Result<Manga>? {
|
||||||
|
return runCatchingCancellable {
|
||||||
|
if (manga.isLocal) {
|
||||||
|
localMangaRepository.getDetails(manga)
|
||||||
|
} else {
|
||||||
|
localMangaRepository.findSavedManga(manga)?.manga
|
||||||
|
} ?: return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadRemote(manga: Manga): Result<Manga>? {
|
||||||
|
return runCatchingCancellable {
|
||||||
|
val seed = if (manga.isLocal) {
|
||||||
|
localMangaRepository.getRemoteManga(manga)
|
||||||
|
} else {
|
||||||
|
manga
|
||||||
|
} ?: return null
|
||||||
|
val repository = mangaRepositoryFactory.create(seed.source)
|
||||||
|
repository.getDetails(seed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun throwNFE(): Nothing = throw NotFoundException("Cannot find manga", "")
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue