Experimental: improve manga loading in reader

This commit is contained in:
Koitharu
2025-07-23 17:05:46 +03:00
parent 506a8b6e90
commit d8efe374a8
10 changed files with 270 additions and 172 deletions

View File

@@ -44,13 +44,13 @@ abstract class BaseViewModel : ViewModel() {
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job = viewModelScope.launch(context + createErrorHandler(), start, block)
): Job = viewModelScope.launch(context.withDefaultExceptionHandler(), start, block)
protected fun launchLoadingJob(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job = viewModelScope.launch(context + createErrorHandler(), start) {
): Job = viewModelScope.launch(context.withDefaultExceptionHandler(), start) {
loadingCounter.increment()
try {
block()
@@ -81,15 +81,28 @@ abstract class BaseViewModel : ViewModel() {
protected fun MutableStateFlow<Int>.decrement() = update { it - 1 }
private fun createErrorHandler() = CoroutineExceptionHandler { coroutineContext, throwable ->
throwable.printStackTraceDebug()
if (coroutineContext[SkipErrors.key] == null && throwable !is CancellationException) {
errorEvent.call(throwable)
private fun CoroutineContext.withDefaultExceptionHandler() =
if (this[CoroutineExceptionHandler.Key] is EventExceptionHandler) {
this
} else {
this + EventExceptionHandler(errorEvent)
}
}
protected object SkipErrors : AbstractCoroutineContextElement(Key) {
private object Key : CoroutineContext.Key<SkipErrors>
}
protected class EventExceptionHandler(
private val event: MutableEventFlow<Throwable>,
) : AbstractCoroutineContextElement(CoroutineExceptionHandler),
CoroutineExceptionHandler {
override fun handleException(context: CoroutineContext, exception: Throwable) {
exception.printStackTraceDebug()
if (context[SkipErrors.key] == null && exception !is CancellationException) {
event.call(exception)
}
}
}
}

View File

@@ -1,53 +1,30 @@
package org.koitharu.kotatsu.core.util
import androidx.collection.ArrayMap
import androidx.annotation.VisibleForTesting
import kotlinx.coroutines.sync.Mutex
import java.util.concurrent.ConcurrentHashMap
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
open class MultiMutex<T : Any> : Set<T> {
open class MultiMutex<T : Any> {
private val delegates = ArrayMap<T, Mutex>()
private val delegates = ConcurrentHashMap<T, Mutex>()
override val size: Int
get() = delegates.size
@VisibleForTesting
val size: Int
get() = delegates.count { it.value.isLocked }
override fun contains(element: T): Boolean = synchronized(delegates) {
delegates.containsKey(element)
}
fun isNotEmpty() = delegates.any { it.value.isLocked }
override fun containsAll(elements: Collection<T>): Boolean = synchronized(delegates) {
elements.all { x -> delegates.containsKey(x) }
}
override fun isEmpty(): Boolean = delegates.isEmpty()
override fun iterator(): Iterator<T> = synchronized(delegates) {
delegates.keys.toList()
}.iterator()
fun isLocked(element: T): Boolean = synchronized(delegates) {
delegates[element]?.isLocked == true
}
fun tryLock(element: T): Boolean {
val mutex = synchronized(delegates) {
delegates.getOrPut(element, ::Mutex)
}
return mutex.tryLock()
}
fun isEmpty() = delegates.none { it.value.isLocked }
suspend fun lock(element: T) {
val mutex = synchronized(delegates) {
delegates.getOrPut(element, ::Mutex)
}
val mutex = delegates.computeIfAbsent(element) { Mutex() }
mutex.lock()
}
fun unlock(element: T) {
synchronized(delegates) {
delegates.remove(element)?.unlock()
}
delegates[element]?.unlock()
}
suspend inline fun <R> withLock(element: T, block: () -> R): R {

View File

@@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.flow.transformWhile
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withTimeout
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.parsers.util.suspendlazy.SuspendLazy
import java.util.concurrent.TimeUnit

View File

@@ -17,6 +17,7 @@ data class MangaDetails(
private val localManga: LocalManga?,
private val override: MangaOverride?,
val description: CharSequence?,
@Deprecated("Caller should decide if manga is loaded enough by itself")
val isLoaded: Boolean,
) {
@@ -31,13 +32,12 @@ data class MangaDetails(
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> by lazy { mergeChapters() }
val chapters: Map<String?, List<MangaChapter>> by lazy {
allChapters.groupBy { it.branch }
}
val isLocal
get() = manga.isLocal
@@ -51,7 +51,22 @@ data class MangaDetails(
.ifNullOrEmpty { localManga?.manga?.coverUrl }
?.nullIfEmpty()
fun toManga() = manga.withOverride(override)
private val mergedManga by lazy {
if (localManga == null) {
// fast path
manga.withOverride(override)
} else {
manga.copy(
title = override?.title.ifNullOrEmpty { manga.title },
coverUrl = override?.coverUrl.ifNullOrEmpty { manga.coverUrl },
largeCoverUrl = override?.coverUrl.ifNullOrEmpty { manga.largeCoverUrl },
contentRating = override?.contentRating ?: manga.contentRating,
chapters = allChapters,
)
}
}
fun toManga() = mergedManga
fun getLocale(): Locale? {
findAppropriateLocale(chapters.keys.singleOrNull())?.let {

View File

@@ -1,11 +1,13 @@
package org.koitharu.kotatsu.details.domain
import android.util.Log
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.onEach
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.prefs.AppSettings

View File

@@ -9,30 +9,33 @@ import androidx.core.text.parseAsHtml
import coil3.request.CachePolicy
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.runInterruptible
import okio.IOException
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.nav.MangaIntent
import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.parser.CachingMangaRepository
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.util.ext.peek
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.ui.model.MangaOverride
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.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
import org.koitharu.kotatsu.parsers.util.recoverNotNull
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.tracker.domain.CheckNewChaptersUseCase
import javax.inject.Inject
import javax.inject.Provider
class DetailsLoadUseCase @Inject constructor(
private val mangaDataRepository: MangaDataRepository,
@@ -40,91 +43,115 @@ class DetailsLoadUseCase @Inject constructor(
private val mangaRepositoryFactory: MangaRepository.Factory,
private val recoverUseCase: RecoverMangaUseCase,
private val imageGetter: Html.ImageGetter,
private val newChaptersUseCaseProvider: Provider<CheckNewChaptersUseCase>,
private val networkState: NetworkState,
) {
operator fun invoke(intent: MangaIntent, force: Boolean): Flow<MangaDetails> = channelFlow {
operator fun invoke(intent: MangaIntent, force: Boolean): Flow<MangaDetails> = flow {
val manga = requireNotNull(mangaDataRepository.resolveIntent(intent, withChapters = true)) {
"Cannot resolve intent $intent"
}
val override = mangaDataRepository.getOverride(manga.id)
send(
emit(
MangaDetails(
manga = manga,
localManga = null,
override = override,
description = null,
description = manga.description?.parseAsHtml(withImages = false),
isLoaded = false,
),
)
if (manga.isLocal) {
val details = getDetails(manga, force)
send(
loadLocal(manga, override, force)
} else {
loadRemote(manga, override, force)
}
}.distinctUntilChanged()
.buffer(Channel.UNLIMITED)
.flowOn(Dispatchers.Default)
/**
* Load local manga + try to load the linked remote one if network is not restricted
* Suppress any network errors
*/
private suspend fun FlowCollector<MangaDetails>.loadLocal(manga: Manga, override: MangaOverride?, force: Boolean) {
val skipNetworkLoad = !force && networkState.isOfflineOrRestricted()
val localDetails = localMangaRepository.getDetails(manga)
emit(
MangaDetails(
manga = localDetails,
localManga = null,
override = override,
description = localDetails.description?.parseAsHtml(withImages = false),
isLoaded = skipNetworkLoad,
),
)
if (skipNetworkLoad) {
return
}
val remoteManga = localMangaRepository.getRemoteManga(manga)
if (remoteManga == null) {
emit(
MangaDetails(
manga = details,
manga = localDetails,
localManga = null,
override = override,
description = details.description?.parseAsHtml(withImages = false)?.trim(),
description = localDetails.description?.parseAsHtml(withImages = true),
isLoaded = true,
),
)
return@channelFlow
}
val local = async {
localMangaRepository.findSavedManga(manga)
}
if (!force && networkState.isOfflineOrRestricted()) {
// try to avoid loading if has saved manga
val localManga = local.await()
if (localManga != null) {
send(
MangaDetails(
manga = manga,
localManga = localManga,
override = override,
description = manga.description?.parseAsHtml(withImages = true)?.trim(),
isLoaded = true,
),
)
return@channelFlow
} else {
val remoteDetails = getDetails(remoteManga, force).getOrNull()
emit(
MangaDetails(
manga = remoteDetails ?: remoteManga,
localManga = LocalManga(localDetails),
override = override,
description = (remoteDetails ?: localDetails).description?.parseAsHtml(withImages = true),
isLoaded = true,
),
)
if (remoteDetails != null) {
mangaDataRepository.updateChapters(remoteDetails)
}
}
try {
val details = getDetails(manga, force)
launch { mangaDataRepository.updateChapters(details) }
launch { updateTracker(details) }
send(
}
/**
* Load remote manga + saved one if available
* Throw network errors after loading local manga only
*/
private suspend fun FlowCollector<MangaDetails>.loadRemote(
manga: Manga,
override: MangaOverride?,
force: Boolean
) = coroutineScope {
val remoteDeferred = async {
getDetails(manga, force)
}
val localManga = localMangaRepository.findSavedManga(manga, withDetails = true)
if (localManga != null) {
emit(
MangaDetails(
manga = details,
localManga = local.peek(),
manga = manga,
localManga = localManga,
override = override,
description = details.description?.parseAsHtml(withImages = false)?.trim(),
description = localManga.manga.description?.parseAsHtml(withImages = true),
isLoaded = false,
),
)
send(
MangaDetails(
manga = details,
localManga = local.await(),
override = override,
description = details.description?.parseAsHtml(withImages = true)?.trim(),
isLoaded = true,
),
)
} catch (e: IOException) {
local.await()?.manga?.also { localManga ->
send(
MangaDetails(
manga = localManga,
localManga = null,
override = override,
description = localManga.description?.parseAsHtml(withImages = false)?.trim(),
isLoaded = true,
),
)
} ?: close(e)
}
val remoteDetails = remoteDeferred.await().getOrThrow()
emit(
MangaDetails(
manga = remoteDetails,
localManga = localManga,
override = override,
description = (remoteDetails.description
?: localManga?.manga?.description)?.parseAsHtml(withImages = true),
isLoaded = true,
),
)
mangaDataRepository.updateChapters(remoteDetails)
}
private suspend fun getDetails(seed: Manga, force: Boolean) = runCatchingCancellable {
@@ -140,20 +167,18 @@ class DetailsLoadUseCase @Inject constructor(
} 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 suspend fun String.parseAsHtml(withImages: Boolean): CharSequence? = if (withImages) {
runInterruptible(Dispatchers.IO) {
parseAsHtml(imageGetter = imageGetter)
}.filterSpans()
} else {
runInterruptible(Dispatchers.Default) {
parseAsHtml()
}.filterSpans().sanitize()
}.trim().nullIfEmpty()
private fun Spanned.filterSpans(): Spanned {
val spannable = SpannableString.valueOf(this)
val spans = spannable.getSpans<ForegroundColorSpan>()
@@ -162,10 +187,4 @@ class DetailsLoadUseCase @Inject constructor(
}
return spannable
}
private suspend fun updateTracker(details: Manga) = runCatchingCancellable {
newChaptersUseCaseProvider.get()(details)
}.onFailure { e ->
e.printStackTraceDebug()
}
}

View File

@@ -12,7 +12,6 @@ import android.widget.Button
import android.widget.FrameLayout
import android.widget.LinearLayout
import androidx.annotation.AttrRes
import androidx.appcompat.widget.TooltipCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import com.google.android.material.slider.Slider
@@ -50,7 +49,9 @@ class ReaderActionsView @JvmOverloads constructor(
private val binding = LayoutReaderActionsBinding.inflate(LayoutInflater.from(context), this)
private val rotationObserver = object : ContentObserver(handler) {
override fun onChange(selfChange: Boolean) {
updateRotationButton()
post {
updateRotationButton()
}
}
}
private var isSliderChanged = false

View File

@@ -29,10 +29,15 @@ import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.DialogErrorObserver
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.nav.router
import org.koitharu.kotatsu.core.prefs.AppSettings
@@ -133,7 +138,7 @@ class ReaderActivity :
}
}
viewModel.onError.observeEvent(
viewModel.onLoadingError.observeEvent(
this,
DialogErrorObserver(
host = viewBinding.container,
@@ -148,13 +153,24 @@ class ReaderActivity :
},
),
)
viewModel.onError.observeEvent(
this,
SnackbarErrorObserver(
host = viewBinding.container,
fragment = null,
resolver = exceptionResolver,
onResolved = null,
),
)
viewModel.readerMode.observe(this, Lifecycle.State.STARTED, this::onInitReader)
viewModel.onPageSaved.observeEvent(this, PagesSavedObserver(viewBinding.container))
viewModel.uiState.zipWithPrevious().observe(this, this::onUiStateChanged)
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
viewModel.content.observe(this) {
onLoadingStateChanged(viewModel.isLoading.value)
}
combine(
viewModel.isLoading,
viewModel.content.map { it.pages.isNotEmpty() }.distinctUntilChanged(),
::Pair,
).flowOn(Dispatchers.Default)
.observe(this, this::onLoadingStateChanged)
viewModel.isKeepScreenOnEnabled.observe(this, this::setKeepScreenOn)
viewModel.isInfoBarTransparent.observe(this) { viewBinding.infoBar.drawBackground = !it }
viewModel.isInfoBarEnabled.observe(this, ::onReaderBarChanged)
@@ -243,9 +259,14 @@ class ReaderActivity :
viewBinding.timerControl.onReaderModeChanged(mode)
}
private fun onLoadingStateChanged(isLoading: Boolean) {
val hasPages = viewModel.content.value.pages.isNotEmpty()
viewBinding.layoutLoading.isVisible = isLoading && !hasPages
private fun onLoadingStateChanged(value: Pair<Boolean, Boolean>) {
val (isLoading, hasPages) = value
val showLoadingLayout = isLoading && !hasPages
if (viewBinding.layoutLoading.isVisible != showLoadingLayout) {
val transition = Fade().addTarget(viewBinding.layoutLoading)
TransitionManager.beginDelayedTransition(viewBinding.root, transition)
viewBinding.layoutLoading.isVisible = showLoadingLayout
}
if (isLoading && hasPages) {
viewBinding.toastView.show(R.string.loading_)
} else {

View File

@@ -1,12 +1,14 @@
package org.koitharu.kotatsu.reader.ui
import android.net.Uri
import android.util.Log
import androidx.annotation.AnyThread
import androidx.annotation.MainThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
@@ -57,6 +59,7 @@ import org.koitharu.kotatsu.parsers.model.ContentRating
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.parsers.util.sizeOrZero
import org.koitharu.kotatsu.reader.domain.ChaptersLoader
import org.koitharu.kotatsu.reader.domain.DetectReaderModeUseCase
@@ -108,13 +111,12 @@ class ReaderViewModel @Inject constructor(
private var stateChangeJob: Job? = null
init {
selectedBranch.value = savedStateHandle.get<String>(ReaderIntent.EXTRA_BRANCH)
readingState.value = savedStateHandle[ReaderIntent.EXTRA_STATE]
mangaDetails.value = intent.manga?.let { MangaDetails(it) }
}
val readerMode = MutableStateFlow<ReaderMode?>(null)
val onPageSaved = MutableEventFlow<Collection<Uri>>()
val onLoadingError = MutableEventFlow<Throwable>()
val onShowToast = MutableEventFlow<Int>()
val onAskNsfwIncognito = MutableEventFlow<Unit>()
val uiState = MutableStateFlow<ReaderUiState?>(null)
@@ -393,31 +395,56 @@ class ReaderViewModel @Inject constructor(
}
private fun loadImpl() {
loadingJob = launchLoadingJob(Dispatchers.Default) {
val details = detailsLoadUseCase.invoke(intent, force = false).first { x -> x.isLoaded }
mangaDetails.value = details
chaptersLoader.init(details)
val manga = details.toManga()
// obtain state
if (readingState.value == null) {
readingState.value = getStateFromIntent(manga)
}
val mode = detectReaderModeUseCase.invoke(manga, readingState.value)
val branch = chaptersLoader.peekChapter(readingState.value?.chapterId ?: 0L)?.branch
selectedBranch.value = branch
mangaDetails.value = details.filterChapters(branch)
readerMode.value = mode
loadingJob = launchLoadingJob(Dispatchers.Default + EventExceptionHandler(onLoadingError)) {
val exception = try {
detailsLoadUseCase(intent, force = false)
.collect { details ->
if (mangaDetails.value == null) {
mangaDetails.value = details
}
chaptersLoader.init(details)
val manga = details.toManga()
// obtain state
if (readingState.value == null) {
val newState = getStateFromIntent(manga)
if (newState == null) {
return@collect // manga not loaded yet if cannot get state
}
readingState.value = newState
val mode = runCatchingCancellable {
detectReaderModeUseCase(manga, newState)
}.getOrDefault(settings.defaultReaderMode)
val branch = chaptersLoader.peekChapter(newState.chapterId)?.branch
selectedBranch.value = branch
readerMode.value = mode
chaptersLoader.loadSingleChapter(newState.chapterId)
}
mangaDetails.value = details.filterChapters(selectedBranch.value)
chaptersLoader.loadSingleChapter(requireNotNull(readingState.value).chapterId)
// save state
if (!isIncognitoMode.firstNotNull()) {
readingState.value?.let {
val percent = computePercent(it.chapterId, it.page)
historyUpdateUseCase.invoke(manga, it, percent)
}
// save state
if (!isIncognitoMode.firstNotNull()) {
readingState.value?.let {
val percent = computePercent(it.chapterId, it.page)
historyUpdateUseCase(manga, it, percent)
}
}
notifyStateChanged()
content.value = ReaderContent(chaptersLoader.snapshot(), readingState.value)
}
null // no errors
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
e
}
if (readingState.value == null) {
onLoadingError.call(
exception ?: IllegalStateException("Unable to load manga. This should never happen. Please report"),
)
} else if (exception != null) {
// manga has been loaded but error occurred
errorEvent.call(exception)
}
notifyStateChanged()
content.value = ReaderContent(chaptersLoader.snapshot(), readingState.value)
}
}
@@ -513,18 +540,40 @@ class ReaderViewModel @Inject constructor(
}
}
private suspend fun getStateFromIntent(manga: Manga): ReaderState {
val history = historyRepository.getOne(manga)
val preselectedBranch = selectedBranch.value
val result = if (history != null) {
if (preselectedBranch != null && preselectedBranch != manga.findChapterById(history.chapterId)?.branch) {
private suspend fun getStateFromIntent(manga: Manga): ReaderState? {
// check if we have at least some chapters loaded
if (manga.chapters.isNullOrEmpty()) {
return null
}
// specific state is requested
val requestedState: ReaderState? = savedStateHandle[ReaderIntent.EXTRA_STATE]
if (requestedState != null) {
return if (manga.findChapterById(requestedState.chapterId) != null) {
requestedState
} else {
null
}
}
val requestedBranch: String? = savedStateHandle[ReaderIntent.EXTRA_BRANCH]
// continue reading
val history = historyRepository.getOne(manga)
if (history != null) {
val chapter = manga.findChapterById(history.chapterId) ?: return null
// specified branch is requested
return if (ReaderIntent.EXTRA_BRANCH in savedStateHandle) {
if (chapter.branch == requestedBranch) {
ReaderState(history)
} else {
ReaderState(manga, requestedBranch)
}
} else {
ReaderState(history)
}
} else {
null
}
return result ?: ReaderState(manga, preselectedBranch ?: manga.getPreferredBranch(null))
// start from beginning
val preferredBranch = requestedBranch ?: manga.getPreferredBranch(null)
return ReaderState(manga, preferredBranch)
}
}

View File

@@ -33,7 +33,7 @@ class CheckNewChaptersUseCase @Inject constructor(
suspend operator fun invoke(manga: Manga): MangaUpdates = mutex.withLock(manga.id) {
repository.updateTracks()
val tracking = repository.getTrackOrNull(manga) ?: return MangaUpdates.Failure(
val tracking = repository.getTrackOrNull(manga) ?: return@withLock MangaUpdates.Failure(
manga = manga,
error = null,
)