Fix reader loading indefinitely in some scenarios (#3686)

Init the reader from the view model instead of the activity so it isn't
cancelled by a configuration change mid init, which left the retained
view model half initialized and needsInit() returning false.

Assisted-by: Claude Code:claude-opus-5
This commit is contained in:
AntsyLich
2026-08-03 23:51:17 +06:00
committed by GitHub
parent f80c91081f
commit f7a1ecd251
3 changed files with 45 additions and 46 deletions
+1
View File
@@ -23,6 +23,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
### Fixed
- Fixed extension installation with shizuku installer ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#3676](https://github.com/mihonapp/mihon/pull/3676))
- Fixed `X-Requested-With` spoofing leaking to unrelated callers ([@AntsyLich](https://github.com/AntsyLich)) ([#3678](https://github.com/mihonapp/mihon/pull/3678))
- Fixed reader loading indefinitely in some scenarios ([@AntsyLich](https://github.com/AntsyLich)) ([#3686](https://github.com/mihonapp/mihon/pull/3686))
## [v0.20.2] - 2026-08-01
### Added
@@ -98,7 +98,6 @@ import tachiyomi.core.common.Constants
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.core.common.util.system.logcat
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.util.collectAsState
@@ -171,26 +170,17 @@ class ReaderActivity : BaseActivity() {
setContentView(binding.root)
binding.setComposeOverlay()
if (viewModel.needsInit()) {
val manga = intent.extras?.getLong("manga", -1) ?: -1L
val chapter = intent.extras?.getLong("chapter", -1) ?: -1L
if (manga == -1L || chapter == -1L) {
finish()
return
}
NotificationReceiver.dismissNotification(this, manga.hashCode(), Notifications.ID_NEW_CHAPTERS)
lifecycleScope.launch {
val initResult = viewModel.init(manga, chapter)
if (!initResult.getOrDefault(false)) {
val exception = initResult.exceptionOrNull() ?: IllegalStateException("Unknown err")
withUIContext {
setInitialChapterError(exception)
}
}
}
if (!viewModel.hasValidArgs) {
finish()
return
}
NotificationReceiver.dismissNotification(
this,
viewModel.mangaId.hashCode(),
Notifications.ID_NEW_CHAPTERS,
)
config = ReaderConfig()
setMenuVisibility(viewModel.state.value.menuVisible)
@@ -200,6 +190,13 @@ class ReaderActivity : BaseActivity() {
.onEach { if (!it) finish() }
.launchIn(lifecycleScope)
viewModel.state
.map { it.initError }
.distinctUntilChanged()
.filterNotNull()
.onEach(::setInitialChapterError)
.launchIn(lifecycleScope)
viewModel.state
.map { it.isLoadingAdjacentChapter }
.distinctUntilChanged()
@@ -53,6 +53,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import logcat.LogPriority
import tachiyomi.core.common.preference.toggle
@@ -107,6 +108,14 @@ class ReaderViewModel @JvmOverloads constructor(
private val mutableState = MutableStateFlow(State())
val state = mutableState.asStateFlow()
/**
* Ids of the manga and chapter the reader was launched with, taken from the activity intent.
*/
val mangaId = savedState.get<Long>("manga") ?: -1L
private val initialChapterId = savedState.get<Long>("chapter") ?: -1L
val hasValidArgs = mangaId != -1L && initialChapterId != -1L
private val eventChannel = Channel<Event>()
val eventFlow = eventChannel.receiveAsFlow()
@@ -244,6 +253,10 @@ class ReaderViewModel @JvmOverloads constructor(
chapterId = currentChapter.chapter.id!!
}
.launchIn(viewModelScope)
if (hasValidArgs) {
viewModelScope.launch { init() }
}
}
override fun onCleared() {
@@ -265,41 +278,28 @@ class ReaderViewModel @JvmOverloads constructor(
}
/**
* Whether this presenter is initialized yet.
* Initializes this presenter with the [mangaId] and [initialChapterId] the reader was launched
* with. This method will fetch the manga from the database and initialize the initial chapter.
* Failures are reported through [State.initError].
*/
fun needsInit(): Boolean {
return manga == null
}
/**
* Initializes this presenter with the given [mangaId] and [initialChapterId]. This method will
* fetch the manga from the database and initialize the initial chapter.
*/
suspend fun init(mangaId: Long, initialChapterId: Long): Result<Boolean> {
if (!needsInit()) return Result.success(true)
return withIOContext {
private suspend fun init() {
withIOContext {
try {
val manga = getManga.await(mangaId)
if (manga != null) {
sourceManager.isInitialized.first { it }
mutableState.update { it.copy(manga = manga) }
if (chapterId == -1L) chapterId = initialChapterId
val manga = getManga.await(mangaId) ?: error("Requested manga of id $mangaId not found")
sourceManager.isInitialized.first { it }
mutableState.update { it.copy(manga = manga) }
if (chapterId == -1L) chapterId = initialChapterId
val context = Injekt.get<Application>()
val source = sourceManager.getOrStub(manga.source)
loader = ChapterLoader(context, downloadManager, downloadProvider, manga, source)
val context = Injekt.get<Application>()
val source = sourceManager.getOrStub(manga.source)
loader = ChapterLoader(context, downloadManager, downloadProvider, manga, source)
loadChapter(loader!!, chapterList.first { chapterId == it.chapter.id })
Result.success(true)
} else {
// Unlikely but okay
Result.success(false)
}
loadChapter(loader!!, chapterList.first { chapterId == it.chapter.id })
} catch (e: Throwable) {
if (e is CancellationException) {
throw e
}
Result.failure(e)
mutableState.update { it.copy(initError = e) }
}
}
}
@@ -948,6 +948,7 @@ class ReaderViewModel @JvmOverloads constructor(
@Immutable
data class State(
val manga: Manga? = null,
val initError: Throwable? = null,
val viewerChapters: ViewerChapters? = null,
val bookmarked: Boolean = false,
val isLoadingAdjacentChapter: Boolean = false,