Refactor manga details loading
This commit is contained in:
@@ -43,5 +43,7 @@ class MangaIntent private constructor(
|
|||||||
|
|
||||||
const val KEY_MANGA = "manga"
|
const val KEY_MANGA = "manga"
|
||||||
const val KEY_ID = "id"
|
const val KEY_ID = "id"
|
||||||
|
|
||||||
|
fun of(manga: Manga) = MangaIntent(manga, manga.id, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class ViewBadge(
|
|||||||
) : View.OnLayoutChangeListener, DefaultLifecycleObserver {
|
) : View.OnLayoutChangeListener, DefaultLifecycleObserver {
|
||||||
|
|
||||||
private var badgeDrawable: BadgeDrawable? = null
|
private var badgeDrawable: BadgeDrawable? = null
|
||||||
|
private var maxCharacterCount: Int = -1
|
||||||
|
|
||||||
var counter: Int
|
var counter: Int
|
||||||
get() = badgeDrawable?.number ?: 0
|
get() = badgeDrawable?.number ?: 0
|
||||||
@@ -48,8 +49,16 @@ class ViewBadge(
|
|||||||
clearBadge()
|
clearBadge()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setMaxCharacterCount(value: Int) {
|
||||||
|
maxCharacterCount = value
|
||||||
|
badgeDrawable?.maxCharacterCount = value
|
||||||
|
}
|
||||||
|
|
||||||
private fun initBadge(): BadgeDrawable {
|
private fun initBadge(): BadgeDrawable {
|
||||||
val badge = BadgeDrawable.create(anchor.context)
|
val badge = BadgeDrawable.create(anchor.context)
|
||||||
|
if (maxCharacterCount > 0) {
|
||||||
|
badge.maxCharacterCount = maxCharacterCount
|
||||||
|
}
|
||||||
anchor.addOnLayoutChangeListener(this)
|
anchor.addOnLayoutChangeListener(this)
|
||||||
BadgeUtils.attachBadgeDrawable(badge, anchor)
|
BadgeUtils.attachBadgeDrawable(badge, anchor)
|
||||||
badgeDrawable = badge
|
badgeDrawable = badge
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import kotlinx.coroutines.Deferred
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
|
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
|
||||||
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
@@ -79,3 +80,11 @@ fun <T> Deferred<T>.getCompletionResultOrNull(): Result<T>? = if (isCompleted) {
|
|||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <T> Deferred<T>.peek(): T? = if (isCompleted) {
|
||||||
|
runCatchingCancellable {
|
||||||
|
getCompleted()
|
||||||
|
}.getOrNull()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package org.koitharu.kotatsu.details.data
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.core.model.isLocal
|
||||||
|
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.reader.data.filterChapters
|
||||||
|
|
||||||
|
data class MangaDetails(
|
||||||
|
private val manga: Manga,
|
||||||
|
private val localManga: LocalManga?,
|
||||||
|
val description: CharSequence?,
|
||||||
|
val isLoaded: Boolean,
|
||||||
|
) {
|
||||||
|
|
||||||
|
val id: Long
|
||||||
|
get() = manga.id
|
||||||
|
|
||||||
|
val chapters: Map<String?, List<MangaChapter>> = manga.chapters?.groupBy { it.branch }.orEmpty()
|
||||||
|
|
||||||
|
val branches: Set<String?>
|
||||||
|
get() = chapters.keys
|
||||||
|
|
||||||
|
val allChapters: List<MangaChapter>
|
||||||
|
get() = manga.chapters.orEmpty()
|
||||||
|
|
||||||
|
val isLocal
|
||||||
|
get() = manga.isLocal
|
||||||
|
|
||||||
|
val local: LocalManga?
|
||||||
|
get() = localManga ?: if (manga.isLocal) LocalManga(manga) else null
|
||||||
|
|
||||||
|
fun toManga() = manga
|
||||||
|
|
||||||
|
fun filterChapters(branch: String?) = MangaDetails(
|
||||||
|
manga = manga.filterChapters(branch),
|
||||||
|
localManga = localManga?.run {
|
||||||
|
copy(manga = manga.filterChapters(branch))
|
||||||
|
},
|
||||||
|
description = description,
|
||||||
|
isLoaded = isLoaded,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.flowOf
|
|||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
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.favourites.domain.FavouritesRepository
|
||||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||||
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
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 org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@Deprecated("")
|
/* TODO: remove */
|
||||||
class DetailsInteractor @Inject constructor(
|
class DetailsInteractor @Inject constructor(
|
||||||
private val historyRepository: HistoryRepository,
|
private val historyRepository: HistoryRepository,
|
||||||
private val favouritesRepository: FavouritesRepository,
|
private val favouritesRepository: FavouritesRepository,
|
||||||
@@ -66,13 +66,22 @@ class DetailsInteractor @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateLocal(subject: DoubleManga?, localManga: LocalManga): DoubleManga? {
|
suspend fun updateLocal(subject: MangaDetails?, localManga: LocalManga): MangaDetails? {
|
||||||
return if (subject?.any?.id == localManga.manga.id) {
|
subject ?: return null
|
||||||
subject.copy(
|
return if (subject.id == localManga.manga.id) {
|
||||||
localManga = runCatchingCancellable {
|
if (subject.isLocal) {
|
||||||
localMangaRepository.getDetails(localManga.manga)
|
subject.copy(
|
||||||
},
|
manga = localManga.manga,
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
subject.copy(
|
||||||
|
localManga = runCatchingCancellable {
|
||||||
|
localManga.copy(
|
||||||
|
manga = localMangaRepository.getDetails(localManga.manga),
|
||||||
|
)
|
||||||
|
}.getOrNull() ?: subject.local,
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
subject
|
subject
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package org.koitharu.kotatsu.details.domain
|
||||||
|
|
||||||
|
import android.text.Html
|
||||||
|
import android.text.SpannableString
|
||||||
|
import android.text.Spanned
|
||||||
|
import android.text.style.ForegroundColorSpan
|
||||||
|
import androidx.core.text.getSpans
|
||||||
|
import androidx.core.text.parseAsHtml
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
|
import kotlinx.coroutines.runInterruptible
|
||||||
|
import org.koitharu.kotatsu.core.model.isLocal
|
||||||
|
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||||
|
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||||
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.peek
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.sanitize
|
||||||
|
import org.koitharu.kotatsu.details.data.MangaDetails
|
||||||
|
import org.koitharu.kotatsu.explore.domain.RecoverMangaUseCase
|
||||||
|
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||||
|
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.util.recoverNotNull
|
||||||
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DetailsLoadUseCase @Inject constructor(
|
||||||
|
private val mangaDataRepository: MangaDataRepository,
|
||||||
|
private val localMangaRepository: LocalMangaRepository,
|
||||||
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
|
private val recoverUseCase: RecoverMangaUseCase,
|
||||||
|
private val imageGetter: Html.ImageGetter,
|
||||||
|
) {
|
||||||
|
|
||||||
|
operator fun invoke(intent: MangaIntent): Flow<MangaDetails> = channelFlow {
|
||||||
|
val manga = requireNotNull(mangaDataRepository.resolveIntent(intent)) {
|
||||||
|
"Cannot resolve intent $intent"
|
||||||
|
}
|
||||||
|
val local = if (!manga.isLocal) {
|
||||||
|
async {
|
||||||
|
localMangaRepository.findSavedManga(manga)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
send(MangaDetails(manga, null, null, false))
|
||||||
|
val details = getDetails(manga)
|
||||||
|
send(MangaDetails(details, local?.peek(), details.description?.parseAsHtml(withImages = false), false))
|
||||||
|
send(MangaDetails(details, local?.await(), details.description?.parseAsHtml(withImages = true), true))
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getDetails(seed: Manga) = runCatchingCancellable {
|
||||||
|
val repository = mangaRepositoryFactory.create(seed.source)
|
||||||
|
repository.getDetails(seed)
|
||||||
|
}.recoverNotNull { e ->
|
||||||
|
if (e is NotFoundException) {
|
||||||
|
recoverUseCase(seed)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}.getOrThrow()
|
||||||
|
|
||||||
|
private suspend fun String.parseAsHtml(withImages: Boolean): CharSequence? {
|
||||||
|
return if (withImages) {
|
||||||
|
runInterruptible(Dispatchers.IO) {
|
||||||
|
parseAsHtml(imageGetter = imageGetter)
|
||||||
|
}.filterSpans()
|
||||||
|
} else {
|
||||||
|
runInterruptible(Dispatchers.Default) {
|
||||||
|
parseAsHtml()
|
||||||
|
}.filterSpans().sanitize()
|
||||||
|
}.takeUnless { it.isBlank() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Spanned.filterSpans(): Spanned {
|
||||||
|
val spannable = SpannableString.valueOf(this)
|
||||||
|
val spans = spannable.getSpans<ForegroundColorSpan>()
|
||||||
|
for (span in spans) {
|
||||||
|
spannable.removeSpan(span)
|
||||||
|
}
|
||||||
|
return spannable
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.domain
|
|
||||||
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
|
||||||
import kotlinx.coroutines.flow.flow
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
|
||||||
import org.koitharu.kotatsu.core.model.isLocal
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
|
||||||
import org.koitharu.kotatsu.details.domain.model.DoubleManga
|
|
||||||
import org.koitharu.kotatsu.explore.domain.RecoverMangaUseCase
|
|
||||||
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
|
||||||
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.util.recoverNotNull
|
|
||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class DoubleMangaLoadUseCase @Inject constructor(
|
|
||||||
private val mangaDataRepository: MangaDataRepository,
|
|
||||||
private val localMangaRepository: LocalMangaRepository,
|
|
||||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
|
||||||
private val recoverUseCase: RecoverMangaUseCase,
|
|
||||||
) {
|
|
||||||
|
|
||||||
operator fun invoke(manga: Manga): Flow<DoubleManga> = flow {
|
|
||||||
var lastValue: DoubleManga? = null
|
|
||||||
var emitted = false
|
|
||||||
invokeImpl(manga).collect {
|
|
||||||
lastValue = it
|
|
||||||
if (it.any != null) {
|
|
||||||
emitted = true
|
|
||||||
emit(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!emitted) {
|
|
||||||
lastValue?.requireAny()
|
|
||||||
}
|
|
||||||
}.flowOn(Dispatchers.Default)
|
|
||||||
|
|
||||||
operator fun invoke(mangaId: Long): Flow<DoubleManga> = flow {
|
|
||||||
emit(mangaDataRepository.findMangaById(mangaId) ?: throwNFE())
|
|
||||||
}.flatMapLatest { invoke(it) }
|
|
||||||
|
|
||||||
operator fun invoke(intent: MangaIntent): Flow<DoubleManga> = flow {
|
|
||||||
emit(mangaDataRepository.resolveIntent(intent) ?: throwNFE())
|
|
||||||
}.flatMapLatest { invoke(it) }
|
|
||||||
|
|
||||||
private suspend fun loadLocal(manga: Manga): Result<Manga>? {
|
|
||||||
return runCatchingCancellable {
|
|
||||||
if (manga.isLocal) {
|
|
||||||
localMangaRepository.getDetails(manga)
|
|
||||||
} else {
|
|
||||||
localMangaRepository.findSavedManga(manga)?.manga
|
|
||||||
} ?: return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun loadRemote(manga: Manga): Result<Manga>? {
|
|
||||||
return runCatchingCancellable {
|
|
||||||
val seed = if (manga.isLocal) {
|
|
||||||
localMangaRepository.getRemoteManga(manga)
|
|
||||||
} else {
|
|
||||||
manga
|
|
||||||
} ?: return null
|
|
||||||
val repository = mangaRepositoryFactory.create(seed.source)
|
|
||||||
repository.getDetails(seed)
|
|
||||||
}.recoverNotNull { e ->
|
|
||||||
if (e is NotFoundException) {
|
|
||||||
recoverUseCase(manga)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun invokeImpl(manga: Manga): Flow<DoubleManga> = combine(
|
|
||||||
flow { emit(null); emit(loadRemote(manga)) },
|
|
||||||
flow { emit(null); emit(loadLocal(manga)) },
|
|
||||||
) { remote, local ->
|
|
||||||
DoubleManga(
|
|
||||||
remoteManga = remote,
|
|
||||||
localManga = local,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun throwNFE(): Nothing = throw NotFoundException("Cannot find manga", "")
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.domain.model
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.core.model.findById
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.reader.data.filterChapters
|
|
||||||
|
|
||||||
data class DoubleManga(
|
|
||||||
private val remoteManga: Result<Manga>?,
|
|
||||||
private val localManga: Result<Manga>?,
|
|
||||||
) {
|
|
||||||
|
|
||||||
constructor(manga: Manga) : this(
|
|
||||||
remoteManga = if (manga.source != MangaSource.LOCAL) Result.success(manga) else null,
|
|
||||||
localManga = if (manga.source == MangaSource.LOCAL) Result.success(manga) else null,
|
|
||||||
)
|
|
||||||
|
|
||||||
val remote: Manga?
|
|
||||||
get() = remoteManga?.getOrNull()
|
|
||||||
|
|
||||||
val local: Manga?
|
|
||||||
get() = localManga?.getOrNull()
|
|
||||||
|
|
||||||
val any: Manga?
|
|
||||||
get() = remote ?: local
|
|
||||||
|
|
||||||
val hasRemote: Boolean
|
|
||||||
get() = remoteManga?.isSuccess == true
|
|
||||||
|
|
||||||
val hasLocal: Boolean
|
|
||||||
get() = localManga?.isSuccess == true
|
|
||||||
|
|
||||||
val chapters: List<MangaChapter>? by lazy(LazyThreadSafetyMode.PUBLICATION) {
|
|
||||||
mergeChapters()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasChapter(id: Long): Boolean {
|
|
||||||
return local?.chapters?.findById(id) != null || remote?.chapters?.findById(id) != null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun requireAny(): Manga {
|
|
||||||
val result = remoteManga?.getOrNull() ?: localManga?.getOrNull()
|
|
||||||
if (result != null) {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
throw (
|
|
||||||
remoteManga?.exceptionOrNull()
|
|
||||||
?: localManga?.exceptionOrNull()
|
|
||||||
?: IllegalStateException("No online either local manga available")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun filterChapters(branch: String?) = DoubleManga(
|
|
||||||
remoteManga?.map { it.filterChapters(branch) },
|
|
||||||
localManga?.map { it.filterChapters(branch) },
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun mergeChapters(): List<MangaChapter>? {
|
|
||||||
val remoteChapters = remote?.chapters
|
|
||||||
val localChapters = local?.chapters
|
|
||||||
if (localChapters == null && remoteChapters == null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val localMap = if (!localChapters.isNullOrEmpty()) {
|
|
||||||
localChapters.associateByTo(LinkedHashMap(localChapters.size)) { it.id }
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
val result = ArrayList<MangaChapter>(maxOf(remoteChapters?.size ?: 0, localChapters?.size ?: 0))
|
|
||||||
remoteChapters?.forEach { r ->
|
|
||||||
localMap?.remove(r.id)?.let { l ->
|
|
||||||
result.add(l)
|
|
||||||
} ?: result.add(r)
|
|
||||||
}
|
|
||||||
localMap?.values?.let {
|
|
||||||
result.addAll(it)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,21 +2,19 @@ package org.koitharu.kotatsu.details.ui
|
|||||||
|
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
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.ChapterListItem
|
||||||
import org.koitharu.kotatsu.details.ui.model.toListItem
|
import org.koitharu.kotatsu.details.ui.model.toListItem
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||||
|
|
||||||
fun mapChapters(
|
fun MangaDetails.mapChapters(
|
||||||
remoteManga: Manga?,
|
|
||||||
localManga: Manga?,
|
|
||||||
history: MangaHistory?,
|
history: MangaHistory?,
|
||||||
newCount: Int,
|
newCount: Int,
|
||||||
branch: String?,
|
branch: String?,
|
||||||
bookmarks: List<Bookmark>,
|
bookmarks: List<Bookmark>,
|
||||||
): List<ChapterListItem> {
|
): List<ChapterListItem> {
|
||||||
val remoteChapters = remoteManga?.getChapters(branch).orEmpty()
|
val remoteChapters = chapters[branch].orEmpty()
|
||||||
val localChapters = localManga?.getChapters(branch).orEmpty()
|
val localChapters = local?.manga?.getChapters(branch).orEmpty()
|
||||||
if (remoteChapters.isEmpty() && localChapters.isEmpty()) {
|
if (remoteChapters.isEmpty() && localChapters.isEmpty()) {
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
@@ -57,7 +55,7 @@ fun mapChapters(
|
|||||||
isCurrent = chapter.id == currentId,
|
isCurrent = chapter.id == currentId,
|
||||||
isUnread = isUnread,
|
isUnread = isUnread,
|
||||||
isNew = false,
|
isNew = false,
|
||||||
isDownloaded = remoteManga != null,
|
isDownloaded = !isLocal,
|
||||||
isBookmarked = chapter.id in bookmarked,
|
isBookmarked = chapter.id in bookmarked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ class DetailsActivity :
|
|||||||
viewBinding.buttonRead.setOnContextClickListenerCompat(this)
|
viewBinding.buttonRead.setOnContextClickListenerCompat(this)
|
||||||
viewBinding.buttonDropdown.setOnClickListener(this)
|
viewBinding.buttonDropdown.setOnClickListener(this)
|
||||||
viewBadge = ViewBadge(viewBinding.buttonRead, this)
|
viewBadge = ViewBadge(viewBinding.buttonRead, this)
|
||||||
|
viewBadge.setMaxCharacterCount(1)
|
||||||
|
|
||||||
if (viewBinding.layoutBottom != null) {
|
if (viewBinding.layoutBottom != null) {
|
||||||
val behavior = BottomSheetBehavior.from(checkNotNull(viewBinding.layoutBottom))
|
val behavior = BottomSheetBehavior.from(checkNotNull(viewBinding.layoutBottom))
|
||||||
@@ -139,7 +140,7 @@ class DetailsActivity :
|
|||||||
}
|
}
|
||||||
viewModel.isChaptersReversed.observe(
|
viewModel.isChaptersReversed.observe(
|
||||||
this,
|
this,
|
||||||
MenuInvalidator(viewBinding.toolbarChapters ?: this)
|
MenuInvalidator(viewBinding.toolbarChapters ?: this),
|
||||||
)
|
)
|
||||||
viewModel.favouriteCategories.observe(this, MenuInvalidator(this))
|
viewModel.favouriteCategories.observe(this, MenuInvalidator(this))
|
||||||
viewModel.branches.observe(this) {
|
viewModel.branches.observe(this) {
|
||||||
@@ -148,7 +149,7 @@ class DetailsActivity :
|
|||||||
viewModel.chapters.observe(this, PrefetchObserver(this))
|
viewModel.chapters.observe(this, PrefetchObserver(this))
|
||||||
viewModel.onDownloadStarted.observeEvent(
|
viewModel.onDownloadStarted.observeEvent(
|
||||||
this,
|
this,
|
||||||
DownloadStartedObserver(viewBinding.containerDetails)
|
DownloadStartedObserver(viewBinding.containerDetails),
|
||||||
)
|
)
|
||||||
|
|
||||||
addMenuProvider(
|
addMenuProvider(
|
||||||
@@ -255,7 +256,7 @@ class DetailsActivity :
|
|||||||
window.setNavigationBarTransparentCompat(
|
window.setNavigationBarTransparentCompat(
|
||||||
this,
|
this,
|
||||||
viewBinding.layoutBottom?.elevation ?: 0f,
|
viewBinding.layoutBottom?.elevation ?: 0f,
|
||||||
0.9f
|
0.9f,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
viewBinding.cardChapters?.updateLayoutParams<MarginLayoutParams> {
|
viewBinding.cardChapters?.updateLayoutParams<MarginLayoutParams> {
|
||||||
@@ -281,14 +282,14 @@ class DetailsActivity :
|
|||||||
info.currentChapter >= 0 -> getString(
|
info.currentChapter >= 0 -> getString(
|
||||||
R.string.chapter_d_of_d,
|
R.string.chapter_d_of_d,
|
||||||
info.currentChapter + 1,
|
info.currentChapter + 1,
|
||||||
info.totalChapters
|
info.totalChapters,
|
||||||
)
|
)
|
||||||
|
|
||||||
info.totalChapters == 0 -> getString(R.string.no_chapters)
|
info.totalChapters == 0 -> getString(R.string.no_chapters)
|
||||||
else -> resources.getQuantityString(
|
else -> resources.getQuantityString(
|
||||||
R.plurals.chapters,
|
R.plurals.chapters,
|
||||||
info.totalChapters,
|
info.totalChapters,
|
||||||
info.totalChapters
|
info.totalChapters,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
viewBinding.toolbarChapters?.title = text
|
viewBinding.toolbarChapters?.title = text
|
||||||
@@ -311,8 +312,8 @@ class DetailsActivity :
|
|||||||
ForegroundColorSpan(
|
ForegroundColorSpan(
|
||||||
v.context.getThemeColor(
|
v.context.getThemeColor(
|
||||||
android.R.attr.textColorSecondary,
|
android.R.attr.textColorSecondary,
|
||||||
Color.LTGRAY
|
Color.LTGRAY,
|
||||||
)
|
),
|
||||||
),
|
),
|
||||||
RelativeSizeSpan(0.74f),
|
RelativeSizeSpan(0.74f),
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
package org.koitharu.kotatsu.details.ui
|
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.SavedStateHandle
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
@@ -17,7 +10,7 @@ import kotlinx.coroutines.flow.SharedFlow
|
|||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.filterNot
|
import kotlinx.coroutines.flow.filterNot
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
@@ -25,7 +18,6 @@ import kotlinx.coroutines.flow.flowOf
|
|||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.mapLatest
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.transformLatest
|
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.R
|
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.ui.BaseViewModel
|
||||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||||
import org.koitharu.kotatsu.core.util.ext.call
|
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.computeSize
|
||||||
import org.koitharu.kotatsu.core.util.ext.onFirst
|
import org.koitharu.kotatsu.core.util.ext.onFirst
|
||||||
import org.koitharu.kotatsu.core.util.ext.requireValue
|
import org.koitharu.kotatsu.core.util.ext.requireValue
|
||||||
import org.koitharu.kotatsu.core.util.ext.sanitize
|
import org.koitharu.kotatsu.details.data.MangaDetails
|
||||||
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
|
|
||||||
import org.koitharu.kotatsu.details.domain.BranchComparator
|
import org.koitharu.kotatsu.details.domain.BranchComparator
|
||||||
import org.koitharu.kotatsu.details.domain.DetailsInteractor
|
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.RelatedMangaUseCase
|
||||||
import org.koitharu.kotatsu.details.domain.model.DoubleManga
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||||
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
|
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
|
||||||
import org.koitharu.kotatsu.details.ui.model.MangaBranch
|
import org.koitharu.kotatsu.details.ui.model.MangaBranch
|
||||||
@@ -74,22 +63,19 @@ class DetailsViewModel @Inject constructor(
|
|||||||
private val bookmarksRepository: BookmarksRepository,
|
private val bookmarksRepository: BookmarksRepository,
|
||||||
private val settings: AppSettings,
|
private val settings: AppSettings,
|
||||||
private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
|
private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
|
||||||
private val imageGetter: Html.ImageGetter,
|
|
||||||
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
|
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
|
||||||
private val downloadScheduler: DownloadWorker.Scheduler,
|
private val downloadScheduler: DownloadWorker.Scheduler,
|
||||||
private val interactor: DetailsInteractor,
|
private val interactor: DetailsInteractor,
|
||||||
savedStateHandle: SavedStateHandle,
|
savedStateHandle: SavedStateHandle,
|
||||||
private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
|
private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
|
||||||
private val doubleMangaLoadUseCase: DoubleMangaLoadUseCase,
|
|
||||||
private val relatedMangaUseCase: RelatedMangaUseCase,
|
private val relatedMangaUseCase: RelatedMangaUseCase,
|
||||||
private val extraProvider: ListExtraProvider,
|
private val extraProvider: ListExtraProvider,
|
||||||
|
private val detailsLoadUseCase: DetailsLoadUseCase,
|
||||||
networkState: NetworkState,
|
networkState: NetworkState,
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
private val intent = MangaIntent(savedStateHandle)
|
private val intent = MangaIntent(savedStateHandle)
|
||||||
private val mangaId = intent.mangaId
|
private val mangaId = intent.mangaId
|
||||||
private val doubleManga: MutableStateFlow<DoubleManga?> =
|
|
||||||
MutableStateFlow(intent.manga?.let { DoubleManga(it) })
|
|
||||||
private var loadingJob: Job
|
private var loadingJob: Job
|
||||||
|
|
||||||
val onShowToast = MutableEventFlow<Int>()
|
val onShowToast = MutableEventFlow<Int>()
|
||||||
@@ -97,8 +83,9 @@ class DetailsViewModel @Inject constructor(
|
|||||||
val onSelectChapter = MutableEventFlow<Long>()
|
val onSelectChapter = MutableEventFlow<Long>()
|
||||||
val onDownloadStarted = MutableEventFlow<Unit>()
|
val onDownloadStarted = MutableEventFlow<Unit>()
|
||||||
|
|
||||||
val manga = doubleManga.map { it?.any }
|
val details = MutableStateFlow(intent.manga?.let { MangaDetails(it, null, null, false) })
|
||||||
.stateIn(viewModelScope, SharingStarted.Eagerly, doubleManga.value?.any)
|
val manga = details.map { x -> x?.toManga() }
|
||||||
|
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
||||||
|
|
||||||
val history = historyRepository.observeOne(mangaId)
|
val history = historyRepository.observeOne(mangaId)
|
||||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
||||||
@@ -135,28 +122,17 @@ class DetailsViewModel @Inject constructor(
|
|||||||
if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList())
|
if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList())
|
||||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, emptyList())
|
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, emptyList())
|
||||||
|
|
||||||
val localSize = doubleManga
|
val localSize = details
|
||||||
.map {
|
.map { it?.local }
|
||||||
val local = it?.local
|
.distinctUntilChanged()
|
||||||
if (local != null) {
|
.map { local ->
|
||||||
val file = local.url.toUri().toFileOrNull()
|
local?.file?.computeSize() ?: 0L
|
||||||
file?.computeSize() ?: 0L
|
|
||||||
} else {
|
|
||||||
0L
|
|
||||||
}
|
|
||||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(), 0)
|
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(), 0)
|
||||||
|
|
||||||
val description = manga
|
@Deprecated("")
|
||||||
.distinctUntilChangedBy { it?.description.orEmpty() }
|
val description = details
|
||||||
.transformLatest {
|
.map { it?.description }
|
||||||
val description = it?.description
|
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, null)
|
||||||
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)
|
|
||||||
|
|
||||||
val onMangaRemoved = MutableEventFlow<Manga>()
|
val onMangaRemoved = MutableEventFlow<Manga>()
|
||||||
val isScrobblingAvailable: Boolean
|
val isScrobblingAvailable: Boolean
|
||||||
@@ -165,9 +141,7 @@ class DetailsViewModel @Inject constructor(
|
|||||||
val scrobblingInfo: StateFlow<List<ScrobblingInfo>> = interactor.observeScrobblingInfo(mangaId)
|
val scrobblingInfo: StateFlow<List<ScrobblingInfo>> = interactor.observeScrobblingInfo(mangaId)
|
||||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
||||||
|
|
||||||
val relatedManga: StateFlow<List<MangaItemModel>> = doubleManga.map {
|
val relatedManga: StateFlow<List<MangaItemModel>> = manga
|
||||||
it?.remote
|
|
||||||
}.distinctUntilChangedBy { it?.id }
|
|
||||||
.mapLatest {
|
.mapLatest {
|
||||||
if (it != null && settings.isRelatedMangaEnabled) {
|
if (it != null && settings.isRelatedMangaEnabled) {
|
||||||
relatedMangaUseCase.invoke(it)?.toUi(ListMode.GRID, extraProvider).orEmpty()
|
relatedMangaUseCase.invoke(it)?.toUi(ListMode.GRID, extraProvider).orEmpty()
|
||||||
@@ -178,40 +152,32 @@ class DetailsViewModel @Inject constructor(
|
|||||||
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
|
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
|
||||||
|
|
||||||
val branches: StateFlow<List<MangaBranch>> = combine(
|
val branches: StateFlow<List<MangaBranch>> = combine(
|
||||||
doubleManga,
|
details,
|
||||||
selectedBranch,
|
selectedBranch,
|
||||||
) { m, b ->
|
) { m, b ->
|
||||||
val chapters = m?.chapters
|
(m?.chapters ?: return@combine emptyList())
|
||||||
if (chapters.isNullOrEmpty()) return@combine emptyList()
|
|
||||||
chapters.groupBy { x -> x.branch }
|
|
||||||
.map { x -> MangaBranch(x.key, x.value.size, x.key == b) }
|
.map { x -> MangaBranch(x.key, x.value.size, x.key == b) }
|
||||||
.sortedWith(BranchComparator())
|
.sortedWith(BranchComparator())
|
||||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
||||||
|
|
||||||
val isChaptersEmpty: StateFlow<Boolean> = combine(
|
val isChaptersEmpty: StateFlow<Boolean> = details.map {
|
||||||
doubleManga,
|
it != null && it.isLoaded && it.allChapters.isEmpty()
|
||||||
isLoading,
|
|
||||||
) { manga, loading ->
|
|
||||||
manga?.any != null && manga.chapters.isNullOrEmpty() && !loading
|
|
||||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
|
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
|
||||||
|
|
||||||
val chapters = combine(
|
val chapters = combine(
|
||||||
combine(
|
combine(
|
||||||
doubleManga,
|
details,
|
||||||
history,
|
history,
|
||||||
selectedBranch,
|
selectedBranch,
|
||||||
newChaptersCount,
|
newChaptersCount,
|
||||||
bookmarks,
|
bookmarks,
|
||||||
networkState,
|
) { manga, history, branch, news, bookmarks ->
|
||||||
) { manga, history, branch, news, bookmarks, isOnline ->
|
manga?.mapChapters(
|
||||||
mapChapters(
|
|
||||||
manga?.remote?.takeIf { isOnline },
|
|
||||||
manga?.local,
|
|
||||||
history,
|
history,
|
||||||
news,
|
news,
|
||||||
branch,
|
branch,
|
||||||
bookmarks,
|
bookmarks,
|
||||||
)
|
).orEmpty()
|
||||||
},
|
},
|
||||||
isChaptersReversed,
|
isChaptersReversed,
|
||||||
chaptersQuery,
|
chaptersQuery,
|
||||||
@@ -242,7 +208,7 @@ class DetailsViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun deleteLocal() {
|
fun deleteLocal() {
|
||||||
val m = doubleManga.value?.local
|
val m = details.value?.local?.manga
|
||||||
if (m == null) {
|
if (m == null) {
|
||||||
onShowToast.call(R.string.file_not_found)
|
onShowToast.call(R.string.file_not_found)
|
||||||
return
|
return
|
||||||
@@ -295,13 +261,13 @@ class DetailsViewModel @Inject constructor(
|
|||||||
|
|
||||||
fun markChapterAsCurrent(chapterId: Long) {
|
fun markChapterAsCurrent(chapterId: Long) {
|
||||||
launchJob(Dispatchers.Default) {
|
launchJob(Dispatchers.Default) {
|
||||||
val manga = checkNotNull(doubleManga.value)
|
val manga = checkNotNull(details.value)
|
||||||
val chapters = checkNotNull(manga.filterChapters(selectedBranchValue).chapters)
|
val chapters = checkNotNull(manga.chapters[selectedBranchValue])
|
||||||
val chapterIndex = chapters.indexOfFirst { it.id == chapterId }
|
val chapterIndex = chapters.indexOfFirst { it.id == chapterId }
|
||||||
check(chapterIndex in chapters.indices) { "Chapter not found" }
|
check(chapterIndex in chapters.indices) { "Chapter not found" }
|
||||||
val percent = chapterIndex / chapters.size.toFloat()
|
val percent = chapterIndex / chapters.size.toFloat()
|
||||||
historyRepository.addOrUpdate(
|
historyRepository.addOrUpdate(
|
||||||
manga = manga.requireAny(),
|
manga = manga.toManga(),
|
||||||
chapterId = chapterId,
|
chapterId = chapterId,
|
||||||
page = 0,
|
page = 0,
|
||||||
scroll = 0,
|
scroll = 0,
|
||||||
@@ -313,7 +279,7 @@ class DetailsViewModel @Inject constructor(
|
|||||||
fun download(chaptersIds: Set<Long>?) {
|
fun download(chaptersIds: Set<Long>?) {
|
||||||
launchJob(Dispatchers.Default) {
|
launchJob(Dispatchers.Default) {
|
||||||
downloadScheduler.schedule(
|
downloadScheduler.schedule(
|
||||||
doubleManga.requireValue().requireAny(),
|
details.requireValue().toManga(),
|
||||||
chaptersIds,
|
chaptersIds,
|
||||||
)
|
)
|
||||||
onDownloadStarted.call(Unit)
|
onDownloadStarted.call(Unit)
|
||||||
@@ -333,14 +299,14 @@ class DetailsViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
|
private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
|
||||||
doubleMangaLoadUseCase.invoke(intent)
|
detailsLoadUseCase.invoke(intent)
|
||||||
.onFirst {
|
.onFirst {
|
||||||
val manga = it.requireAny()
|
val manga = it.toManga()
|
||||||
// find default branch
|
// find default branch
|
||||||
val hist = historyRepository.getOne(manga)
|
val hist = historyRepository.getOne(manga)
|
||||||
selectedBranch.value = manga.getPreferredBranch(hist)
|
selectedBranch.value = manga.getPreferredBranch(hist)
|
||||||
}.collect {
|
}.collect {
|
||||||
doubleManga.value = it
|
details.value = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,21 +322,12 @@ class DetailsViewModel @Inject constructor(
|
|||||||
private suspend fun onDownloadComplete(downloadedManga: LocalManga?) {
|
private suspend fun onDownloadComplete(downloadedManga: LocalManga?) {
|
||||||
downloadedManga ?: return
|
downloadedManga ?: return
|
||||||
launchJob {
|
launchJob {
|
||||||
doubleManga.update {
|
details.update {
|
||||||
interactor.updateLocal(it, downloadedManga)
|
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? {
|
private fun getScrobbler(index: Int): Scrobbler? {
|
||||||
val info = scrobblingInfo.value.getOrNull(index)
|
val info = scrobblingInfo.value.getOrNull(index)
|
||||||
val scrobbler = if (info != null) {
|
val scrobbler = if (info != null) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
|||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
|
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
|
||||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
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.getThemeColor
|
||||||
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
||||||
import org.koitharu.kotatsu.databinding.ItemChapterBinding
|
import org.koitharu.kotatsu.databinding.ItemChapterBinding
|
||||||
@@ -47,8 +47,7 @@ fun chapterListItemAD(
|
|||||||
}
|
}
|
||||||
binding.imageViewBookmarked.isVisible = item.isBookmarked
|
binding.imageViewBookmarked.isVisible = item.isBookmarked
|
||||||
binding.imageViewDownloaded.isVisible = item.isDownloaded
|
binding.imageViewDownloaded.isVisible = item.isDownloaded
|
||||||
// binding.imageViewNew.isVisible = item.isNew
|
binding.textViewTitle.drawableEnd = if (item.isNew) {
|
||||||
binding.textViewTitle.drawableStart = if (item.isNew) {
|
|
||||||
ContextCompat.getDrawable(context, R.drawable.ic_new)
|
ContextCompat.getDrawable(context, R.drawable.ic_new)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
|||||||
@@ -93,8 +93,11 @@ class HistoryRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
val tags = manga.tags.toEntities()
|
val tags = manga.tags.toEntities()
|
||||||
db.withTransaction {
|
db.withTransaction {
|
||||||
db.tagsDao.upsert(tags)
|
val existing = db.mangaDao.find(manga.id)?.manga
|
||||||
db.mangaDao.upsert(manga.toEntity(), tags)
|
if (existing == null || existing.source == manga.source.name) {
|
||||||
|
db.tagsDao.upsert(tags)
|
||||||
|
db.mangaDao.upsert(manga.toEntity(), tags)
|
||||||
|
}
|
||||||
db.historyDao.upsert(
|
db.historyDao.upsert(
|
||||||
HistoryEntity(
|
HistoryEntity(
|
||||||
mangaId = manga.id,
|
mangaId = manga.id,
|
||||||
|
|||||||
@@ -2,16 +2,10 @@ package org.koitharu.kotatsu.reader.domain
|
|||||||
|
|
||||||
import android.util.LongSparseArray
|
import android.util.LongSparseArray
|
||||||
import dagger.hilt.android.scopes.ViewModelScoped
|
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.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
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.parsers.model.MangaChapter
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -23,32 +17,24 @@ class ChaptersLoader @Inject constructor(
|
|||||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val chapters = MutableStateFlow(LongSparseArray<MangaChapter>(0))
|
private val chapters = LongSparseArray<MangaChapter>()
|
||||||
private val chapterPages = ChapterPages()
|
private val chapterPages = ChapterPages()
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
|
|
||||||
val size: Int // TODO flow
|
val size: Int
|
||||||
get() = chapters.value.size()
|
get() = chapters.size()
|
||||||
|
|
||||||
fun init(scope: CoroutineScope, manga: Flow<DoubleManga>) = scope.launch {
|
suspend fun init(manga: MangaDetails) = mutex.withLock {
|
||||||
manga.collect {
|
chapters.clear()
|
||||||
val ch = it.chapters.orEmpty()
|
manga.allChapters.forEach {
|
||||||
val longSparseArray = LongSparseArray<MangaChapter>(ch.size)
|
chapters.put(it.id, it)
|
||||||
ch.forEach { x -> longSparseArray.put(x.id, x) }
|
|
||||||
mutex.withLock {
|
|
||||||
chapters.value = longSparseArray
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun loadPrevNextChapter(manga: DoubleManga, currentId: Long, isNext: Boolean) {
|
suspend fun loadPrevNextChapter(manga: MangaDetails, currentId: Long, isNext: Boolean) {
|
||||||
val chapters = manga.chapters ?: return
|
val chapters = manga.allChapters
|
||||||
val predicate: (MangaChapter) -> Boolean = { it.id == currentId }
|
val predicate: (MangaChapter) -> Boolean = { it.id == currentId }
|
||||||
val index = if (isNext) {
|
val index = if (isNext) chapters.indexOfFirst(predicate) else chapters.indexOfLast(predicate)
|
||||||
chapters.indexOfFirst(predicate)
|
|
||||||
} else {
|
|
||||||
chapters.indexOfLast(predicate)
|
|
||||||
}
|
|
||||||
if (index == -1) return
|
if (index == -1) return
|
||||||
val newChapter = chapters.getOrNull(if (isNext) index + 1 else index - 1) ?: return
|
val newChapter = chapters.getOrNull(if (isNext) index + 1 else index - 1) ?: return
|
||||||
val newPages = loadChapter(newChapter.id)
|
val newPages = loadChapter(newChapter.id)
|
||||||
@@ -79,11 +65,7 @@ class ChaptersLoader @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun peekChapter(chapterId: Long): MangaChapter? = chapters.value[chapterId]
|
fun peekChapter(chapterId: Long): MangaChapter? = chapters[chapterId]
|
||||||
|
|
||||||
suspend fun awaitChapter(chapterId: Long): MangaChapter? = chapters.mapNotNull { x ->
|
|
||||||
x[chapterId]
|
|
||||||
}.firstOrNull()
|
|
||||||
|
|
||||||
fun getPages(chapterId: Long): List<ReaderPage> {
|
fun getPages(chapterId: Long): List<ReaderPage> {
|
||||||
return chapterPages.subList(chapterId)
|
return chapterPages.subList(chapterId)
|
||||||
@@ -100,7 +82,7 @@ class ChaptersLoader @Inject constructor(
|
|||||||
fun snapshot() = chapterPages.toList()
|
fun snapshot() = chapterPages.toList()
|
||||||
|
|
||||||
private suspend fun loadChapter(chapterId: Long): List<ReaderPage> {
|
private suspend fun loadChapter(chapterId: Long): List<ReaderPage> {
|
||||||
val chapter = checkNotNull(awaitChapter(chapterId)) { "Requested chapter not found" }
|
val chapter = checkNotNull(chapters[chapterId]) { "Requested chapter not found" }
|
||||||
val repo = mangaRepositoryFactory.create(chapter.source)
|
val repo = mangaRepositoryFactory.create(chapter.source)
|
||||||
return repo.getPages(chapter).mapIndexed { index, page ->
|
return repo.getPages(chapter).mapIndexed { index, page ->
|
||||||
ReaderPage(page, index, chapterId)
|
ReaderPage(page, index, chapterId)
|
||||||
|
|||||||
@@ -33,13 +33,11 @@ class ChaptersSheet : BaseAdaptiveSheet<SheetChaptersBinding>(),
|
|||||||
override fun onCreateViewBinding(
|
override fun onCreateViewBinding(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
): SheetChaptersBinding {
|
) = SheetChaptersBinding.inflate(inflater, container, false)
|
||||||
return SheetChaptersBinding.inflate(inflater, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewBindingCreated(binding: SheetChaptersBinding, savedInstanceState: Bundle?) {
|
override fun onViewBindingCreated(binding: SheetChaptersBinding, savedInstanceState: Bundle?) {
|
||||||
super.onViewBindingCreated(binding, savedInstanceState)
|
super.onViewBindingCreated(binding, savedInstanceState)
|
||||||
val chapters = viewModel.manga?.chapters
|
val chapters = viewModel.manga?.allChapters
|
||||||
if (chapters.isNullOrEmpty()) {
|
if (chapters.isNullOrEmpty()) {
|
||||||
dismissAllowingStateLoss()
|
dismissAllowingStateLoss()
|
||||||
return
|
return
|
||||||
@@ -61,7 +59,7 @@ class ChaptersSheet : BaseAdaptiveSheet<SheetChaptersBinding>(),
|
|||||||
val offset =
|
val offset =
|
||||||
(resources.getDimensionPixelSize(R.dimen.chapter_list_item_height) * 0.6).roundToInt()
|
(resources.getDimensionPixelSize(R.dimen.chapter_list_item_height) * 0.6).roundToInt()
|
||||||
adapter.setItems(
|
adapter.setItems(
|
||||||
items, RecyclerViewScrollCallback(binding.recyclerView, targetPosition, offset)
|
items, RecyclerViewScrollCallback(binding.recyclerView, targetPosition, offset),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
adapter.items = items
|
adapter.items = items
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ class ReaderActivity :
|
|||||||
val state = viewModel.getCurrentState() ?: return false
|
val state = viewModel.getCurrentState() ?: return false
|
||||||
PagesThumbnailsSheet.show(
|
PagesThumbnailsSheet.show(
|
||||||
supportFragmentManager,
|
supportFragmentManager,
|
||||||
viewModel.manga?.any ?: return false,
|
viewModel.manga?.toManga() ?: return false,
|
||||||
state.chapterId,
|
state.chapterId,
|
||||||
state.page,
|
state.page,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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.ifNullOrEmpty
|
||||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||||
import org.koitharu.kotatsu.core.util.ext.requireValue
|
import org.koitharu.kotatsu.core.util.ext.requireValue
|
||||||
import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase
|
import org.koitharu.kotatsu.details.data.MangaDetails
|
||||||
import org.koitharu.kotatsu.details.domain.model.DoubleManga
|
import org.koitharu.kotatsu.details.domain.DetailsLoadUseCase
|
||||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||||
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
|
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
|
||||||
import org.koitharu.kotatsu.history.domain.HistoryUpdateUseCase
|
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.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
import org.koitharu.kotatsu.reader.domain.ChaptersLoader
|
import org.koitharu.kotatsu.reader.domain.ChaptersLoader
|
||||||
@@ -74,7 +73,7 @@ class ReaderViewModel @Inject constructor(
|
|||||||
private val pageLoader: PageLoader,
|
private val pageLoader: PageLoader,
|
||||||
private val chaptersLoader: ChaptersLoader,
|
private val chaptersLoader: ChaptersLoader,
|
||||||
private val appShortcutManager: AppShortcutManager,
|
private val appShortcutManager: AppShortcutManager,
|
||||||
private val doubleMangaLoadUseCase: DoubleMangaLoadUseCase,
|
private val detailsLoadUseCase: DetailsLoadUseCase,
|
||||||
private val historyUpdateUseCase: HistoryUpdateUseCase,
|
private val historyUpdateUseCase: HistoryUpdateUseCase,
|
||||||
private val detectReaderModeUseCase: DetectReaderModeUseCase,
|
private val detectReaderModeUseCase: DetectReaderModeUseCase,
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
@@ -88,9 +87,9 @@ class ReaderViewModel @Inject constructor(
|
|||||||
private var bookmarkJob: Job? = null
|
private var bookmarkJob: Job? = null
|
||||||
private var stateChangeJob: Job? = null
|
private var stateChangeJob: Job? = null
|
||||||
private val currentState = MutableStateFlow<ReaderState?>(savedStateHandle[ReaderActivity.EXTRA_STATE])
|
private val currentState = MutableStateFlow<ReaderState?>(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<Manga?>
|
private val mangaFlow: Flow<Manga?>
|
||||||
get() = mangaData.map { it?.any }
|
get() = mangaData.map { it?.toManga() }
|
||||||
|
|
||||||
val readerMode = MutableStateFlow<ReaderMode?>(null)
|
val readerMode = MutableStateFlow<ReaderMode?>(null)
|
||||||
val onPageSaved = MutableEventFlow<Uri?>()
|
val onPageSaved = MutableEventFlow<Uri?>()
|
||||||
@@ -98,7 +97,7 @@ class ReaderViewModel @Inject constructor(
|
|||||||
val uiState = MutableStateFlow<ReaderUiState?>(null)
|
val uiState = MutableStateFlow<ReaderUiState?>(null)
|
||||||
|
|
||||||
val content = MutableStateFlow(ReaderContent(emptyList(), null))
|
val content = MutableStateFlow(ReaderContent(emptyList(), null))
|
||||||
val manga: DoubleManga?
|
val manga: MangaDetails?
|
||||||
get() = mangaData.value
|
get() = mangaData.value
|
||||||
|
|
||||||
val pageAnimation = settings.observeAsStateFlow(
|
val pageAnimation = settings.observeAsStateFlow(
|
||||||
@@ -148,7 +147,7 @@ class ReaderViewModel @Inject constructor(
|
|||||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false)
|
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false)
|
||||||
|
|
||||||
val isBookmarkAdded = currentState.flatMapLatest { state ->
|
val isBookmarkAdded = currentState.flatMapLatest { state ->
|
||||||
val manga = mangaData.value?.any
|
val manga = mangaData.value?.toManga()
|
||||||
if (state == null || manga == null) {
|
if (state == null || manga == null) {
|
||||||
flowOf(false)
|
flowOf(false)
|
||||||
} else {
|
} else {
|
||||||
@@ -178,7 +177,7 @@ class ReaderViewModel @Inject constructor(
|
|||||||
|
|
||||||
fun switchMode(newMode: ReaderMode) {
|
fun switchMode(newMode: ReaderMode) {
|
||||||
launchJob {
|
launchJob {
|
||||||
val manga = checkNotNull(mangaData.value?.any)
|
val manga = checkNotNull(mangaData.value?.toManga())
|
||||||
dataRepository.saveReaderMode(
|
dataRepository.saveReaderMode(
|
||||||
manga = manga,
|
manga = manga,
|
||||||
mode = newMode,
|
mode = newMode,
|
||||||
@@ -199,7 +198,7 @@ class ReaderViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
val readerState = state ?: currentState.value ?: return
|
val readerState = state ?: currentState.value ?: return
|
||||||
historyUpdateUseCase.invokeAsync(
|
historyUpdateUseCase.invokeAsync(
|
||||||
manga = mangaData.value?.any ?: return,
|
manga = mangaData.value?.toManga() ?: return,
|
||||||
readerState = readerState,
|
readerState = readerState,
|
||||||
percent = computePercent(readerState.chapterId, readerState.page),
|
percent = computePercent(readerState.chapterId, readerState.page),
|
||||||
)
|
)
|
||||||
@@ -295,7 +294,7 @@ class ReaderViewModel @Inject constructor(
|
|||||||
val state = checkNotNull(currentState.value)
|
val state = checkNotNull(currentState.value)
|
||||||
val page = checkNotNull(getCurrentPage()) { "Page not found" }
|
val page = checkNotNull(getCurrentPage()) { "Page not found" }
|
||||||
val bookmark = Bookmark(
|
val bookmark = Bookmark(
|
||||||
manga = checkNotNull(mangaData.value?.any),
|
manga = mangaData.requireValue().toManga(),
|
||||||
pageId = page.id,
|
pageId = page.id,
|
||||||
chapterId = state.chapterId,
|
chapterId = state.chapterId,
|
||||||
page = state.page,
|
page = state.page,
|
||||||
@@ -315,7 +314,7 @@ class ReaderViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
bookmarkJob = launchJob {
|
bookmarkJob = launchJob {
|
||||||
loadingJob?.join()
|
loadingJob?.join()
|
||||||
val manga = checkNotNull(mangaData.value?.any)
|
val manga = mangaData.requireValue().toManga()
|
||||||
val state = checkNotNull(getCurrentState())
|
val state = checkNotNull(getCurrentState())
|
||||||
bookmarksRepository.removeBookmark(manga.id, state.chapterId, state.page)
|
bookmarksRepository.removeBookmark(manga.id, state.chapterId, state.page)
|
||||||
onShowToast.call(R.string.bookmark_removed)
|
onShowToast.call(R.string.bookmark_removed)
|
||||||
@@ -324,25 +323,19 @@ class ReaderViewModel @Inject constructor(
|
|||||||
|
|
||||||
private fun loadImpl() {
|
private fun loadImpl() {
|
||||||
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
||||||
var manga = DoubleManga(
|
val details = detailsLoadUseCase.invoke(intent).first { x -> x.isLoaded }
|
||||||
dataRepository.resolveIntent(intent)
|
mangaData.value = details
|
||||||
?: throw NotFoundException("Cannot find manga", ""),
|
chaptersLoader.init(details)
|
||||||
)
|
val manga = details.toManga()
|
||||||
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()
|
|
||||||
// obtain state
|
// obtain state
|
||||||
if (currentState.value == null) {
|
if (currentState.value == null) {
|
||||||
currentState.value = historyRepository.getOne(singleManga)?.let {
|
currentState.value = historyRepository.getOne(manga)?.let {
|
||||||
ReaderState(it)
|
ReaderState(it)
|
||||||
} ?: ReaderState(singleManga, preselectedBranch)
|
} ?: ReaderState(manga, preselectedBranch)
|
||||||
}
|
}
|
||||||
val mode = detectReaderModeUseCase.invoke(singleManga, currentState.value)
|
val mode = detectReaderModeUseCase.invoke(manga, currentState.value)
|
||||||
val branch = chaptersLoader.awaitChapter(currentState.value?.chapterId ?: 0L)?.branch
|
val branch = chaptersLoader.peekChapter(currentState.value?.chapterId ?: 0L)?.branch
|
||||||
mangaData.value = manga.filterChapters(branch)
|
mangaData.value = details.filterChapters(branch)
|
||||||
readerMode.value = mode
|
readerMode.value = mode
|
||||||
|
|
||||||
chaptersLoader.loadSingleChapter(requireNotNull(currentState.value).chapterId)
|
chaptersLoader.loadSingleChapter(requireNotNull(currentState.value).chapterId)
|
||||||
@@ -350,7 +343,7 @@ class ReaderViewModel @Inject constructor(
|
|||||||
if (!isIncognito) {
|
if (!isIncognito) {
|
||||||
currentState.value?.let {
|
currentState.value?.let {
|
||||||
val percent = computePercent(it.chapterId, it.page)
|
val percent = computePercent(it.chapterId, it.page)
|
||||||
historyUpdateUseCase.invoke(singleManga, it, percent)
|
historyUpdateUseCase.invoke(manga, it, percent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
notifyStateChanged()
|
notifyStateChanged()
|
||||||
@@ -383,11 +376,11 @@ class ReaderViewModel @Inject constructor(
|
|||||||
val state = getCurrentState()
|
val state = getCurrentState()
|
||||||
val chapter = state?.chapterId?.let { chaptersLoader.peekChapter(it) }
|
val chapter = state?.chapterId?.let { chaptersLoader.peekChapter(it) }
|
||||||
val newState = ReaderUiState(
|
val newState = ReaderUiState(
|
||||||
mangaName = manga?.any?.title,
|
mangaName = manga?.toManga()?.title,
|
||||||
branch = chapter?.branch,
|
branch = chapter?.branch,
|
||||||
chapterName = chapter?.name,
|
chapterName = chapter?.name,
|
||||||
chapterNumber = chapter?.number ?: 0,
|
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,
|
totalPages = if (chapter != null) chaptersLoader.getPagesCount(chapter.id) else 0,
|
||||||
currentPage = state?.page ?: 0,
|
currentPage = state?.page ?: 0,
|
||||||
isSliderEnabled = settings.isReaderSliderEnabled,
|
isSliderEnabled = settings.isReaderSliderEnabled,
|
||||||
@@ -398,7 +391,7 @@ class ReaderViewModel @Inject constructor(
|
|||||||
|
|
||||||
private fun computePercent(chapterId: Long, pageIndex: Int): Float {
|
private fun computePercent(chapterId: Long, pageIndex: Int): Float {
|
||||||
val branch = chaptersLoader.peekChapter(chapterId)?.branch
|
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 chaptersCount = chapters.size
|
||||||
val chapterIndex = chapters.indexOfFirst { x -> x.id == chapterId }
|
val chapterIndex = chapters.indexOfFirst { x -> x.id == chapterId }
|
||||||
val pagesCount = chaptersLoader.getPagesCount(chapterId)
|
val pagesCount = chaptersLoader.getPagesCount(chapterId)
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ class ReaderConfigSheet :
|
|||||||
|
|
||||||
R.id.button_color_filter -> {
|
R.id.button_color_filter -> {
|
||||||
val page = viewModel.getCurrentPage() ?: return
|
val page = viewModel.getCurrentPage() ?: return
|
||||||
val manga = viewModel.manga?.any ?: return
|
val manga = viewModel.manga?.toManga() ?: return
|
||||||
startActivity(ColorFilterConfigActivity.newIntent(v.context, manga, page))
|
startActivity(ColorFilterConfigActivity.newIntent(v.context, manga, page))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,17 +7,17 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.last
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import org.koitharu.kotatsu.core.model.findById
|
import org.koitharu.kotatsu.core.model.findById
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
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.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||||
import org.koitharu.kotatsu.core.util.ext.firstNotNull
|
import org.koitharu.kotatsu.core.util.ext.firstNotNull
|
||||||
import org.koitharu.kotatsu.core.util.ext.require
|
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.ListHeader
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.reader.domain.ChaptersLoader
|
import org.koitharu.kotatsu.reader.domain.ChaptersLoader
|
||||||
@@ -28,7 +28,7 @@ class PagesThumbnailsViewModel @Inject constructor(
|
|||||||
savedStateHandle: SavedStateHandle,
|
savedStateHandle: SavedStateHandle,
|
||||||
mangaRepositoryFactory: MangaRepository.Factory,
|
mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
private val chaptersLoader: ChaptersLoader,
|
private val chaptersLoader: ChaptersLoader,
|
||||||
doubleMangaLoadUseCase: DoubleMangaLoadUseCase,
|
detailsLoadUseCase: DetailsLoadUseCase,
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
private val currentPageIndex: Int =
|
private val currentPageIndex: Int =
|
||||||
@@ -37,7 +37,7 @@ class PagesThumbnailsViewModel @Inject constructor(
|
|||||||
val manga = savedStateHandle.require<ParcelableManga>(PagesThumbnailsSheet.ARG_MANGA).manga
|
val manga = savedStateHandle.require<ParcelableManga>(PagesThumbnailsSheet.ARG_MANGA).manga
|
||||||
|
|
||||||
private val repository = mangaRepositoryFactory.create(manga.source)
|
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
|
val b = manga.chapters?.findById(initialChapterId)?.branch
|
||||||
branch.value = b
|
branch.value = b
|
||||||
it.filterChapters(b)
|
it.filterChapters(b)
|
||||||
@@ -52,8 +52,7 @@ class PagesThumbnailsViewModel @Inject constructor(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
||||||
chaptersLoader.init(viewModelScope, mangaDetails.filterNotNull())
|
chaptersLoader.init(checkNotNull(mangaDetails.last()))
|
||||||
mangaDetails.first { x -> x?.hasChapter(initialChapterId) == true }
|
|
||||||
chaptersLoader.loadSingleChapter(initialChapterId)
|
chaptersLoader.loadSingleChapter(initialChapterId)
|
||||||
updateList()
|
updateList()
|
||||||
}
|
}
|
||||||
@@ -79,13 +78,13 @@ class PagesThumbnailsViewModel @Inject constructor(
|
|||||||
updateList()
|
updateList()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateList() {
|
private fun updateList() {
|
||||||
val snapshot = chaptersLoader.snapshot()
|
val snapshot = chaptersLoader.snapshot()
|
||||||
val pages = buildList(snapshot.size + chaptersLoader.size + 2) {
|
val pages = buildList(snapshot.size + chaptersLoader.size + 2) {
|
||||||
var previousChapterId = 0L
|
var previousChapterId = 0L
|
||||||
for (page in snapshot) {
|
for (page in snapshot) {
|
||||||
if (page.chapterId != previousChapterId) {
|
if (page.chapterId != previousChapterId) {
|
||||||
chaptersLoader.awaitChapter(page.chapterId)?.let {
|
chaptersLoader.peekChapter(page.chapterId)?.let {
|
||||||
add(ListHeader(it.name))
|
add(ListHeader(it.name))
|
||||||
}
|
}
|
||||||
previousChapterId = page.chapterId
|
previousChapterId = page.chapterId
|
||||||
|
|||||||
Reference in New Issue
Block a user