From f7a1ecd25163b05b847cd81fd324cb87e8bc796e Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Mon, 3 Aug 2026 23:51:17 +0600 Subject: [PATCH] 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 --- CHANGELOG.md | 1 + .../tachiyomi/ui/reader/ReaderActivity.kt | 35 ++++++------ .../tachiyomi/ui/reader/ReaderViewModel.kt | 55 ++++++++++--------- 3 files changed, 45 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ef23ed17..485f41688 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index 1f3ce8b55..351e5ca30 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -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() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt index e45794f8b..47ce42651 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt @@ -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("manga") ?: -1L + private val initialChapterId = savedState.get("chapter") ?: -1L + + val hasValidArgs = mangaId != -1L && initialChapterId != -1L + private val eventChannel = Channel() 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 { - 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() - val source = sourceManager.getOrStub(manga.source) - loader = ChapterLoader(context, downloadManager, downloadProvider, manga, source) + val context = Injekt.get() + 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,