Skip to content

Fix migration UI crash, various performance & QoL changes#635

Open
RsReez wants to merge 5 commits into
null2264:masterfrom
RsReez:fix/migration-and-chapter-improvements
Open

Fix migration UI crash, various performance & QoL changes#635
RsReez wants to merge 5 commits into
null2264:masterfrom
RsReez:fix/migration-and-chapter-improvements

Conversation

@RsReez

@RsReez RsReez commented Jun 27, 2026

Copy link
Copy Markdown

Bug Fixes

This PR adds performance improvements, QoL additions, and fixes several bugs found through crash log analysis and testing across the migration UI, library, and manga detail screens.

Migration UI Crash (DeferredField)

Problem: DeferredField.set() was calling mutex.tryLock() but ignoring its return value, then unconditionally calling mutex.unlock(). If initialize() had already unlocked the mutex before set() was called, the second unlock threw IllegalStateException and crashed the app during manga source migration.

Fix: Store the result of tryLock() in a locked variable and only call unlock() if it returned true. Also guard initialize() with an early return if already initialized to prevent being called twice.

Migration UI Progressive Slowdown

Problem: MigrationProcessHolder.bind() launched a coroutine via launchUI that suspended on searchResult.get() waiting for a search result. Every notifyItemChanged() call after a migration completed would call bind() again on every visible holder, launching another suspended coroutine without cancelling the previous one. These accumulated indefinitely on Dispatchers.Main, causing the UI to get progressively slower with each migration completed.

Fix: Give each holder its own MainScope with a tracked bindJob that gets cancelled at the start of every bind() call, so only one coroutine per holder ever waits at a time. Add onRecycled() for clean teardown and wire it up via onViewRecycled() in MigrationProcessAdapter.

Migration UI Coroutine Scope Cleanup (MigratingManga)

Problem: MigratingManga held a bare CoroutineContext for migrationJob rather than a proper CoroutineScope. This meant cancellation wasn't properly propagated to child coroutines, and there was no clean way to cancel all in-flight work for a manga entry.

Fix: Replace the bare CoroutineContext with a proper CoroutineScope (migrationScope) backed by a SupervisorJob, so one failing source search doesn't cancel sibling searches. Expose a cancel() method for clean teardown. Keep migrationJob as a property that returns the scope's coroutineContext for call sites that need it directly.

Library CursorWindow Overflow (slight mitigation)

Partial Fix: Add a separate findAllAsFlow query that explicitly lists all columns except description, used only for the reactive flow subscription. The original findAll query is preserved for one-shot fetches. A corresponding flowMapper was added to LibraryManga and MangaRepositoryImpl updated to use findAllAsFlow with flowMapper for the flow subscription.

Manga Description Blank When Opening from Library

Problem: As a consequence of excluding description from the library flow, manga objects passed from the library to the detail screen had a null description. MangaDetailsPresenter.onCreate() only re-fetches from the database if manga.initialized is false, so it would display no description until a manual refresh.

Fix: Add a manga.description == null check alongside the existing !::manga.isInitialized check in MangaDetailsPresenter.onCreate(). If the manga arrives with a null description it immediately fetches fresh from the mangas table by ID before rendering — a fast single-row lookup.

Library Selection Wiped on Every Chapter Download

Problem: onNextLibraryUpdate called destroyActionModeIfNeeded() unconditionally on every library flow emission. Since downloads write to the chapters table and invalidate the library flow, every chapter download completion wiped the user's entire manga selection in the library.

Fix: Make the call conditional — only call destroyActionModeIfNeeded() if selectedMangas is empty, or if none of the selected manga exist in the updated mangaMap. This preserves legitimate cases like selection being cleared after a manga is deleted or filtered out, while leaving selection intact during routine library refreshes.

Chapter List Stutter & Stuck Download Completion Animation

Problem: downloadManager.queueState emits the entire global download queue on every
state change for any manga. onQueueUpdate originally rebuilt the chapter list (a full DB
read + RecyclerView dataset replacement) on every single emission, causing visible stutter
when other manga were downloading in the background.

A naive fix — early-returning when queue.none { it.manga.id == mangaId } — caused a second,
worse bug: the final progress update for a chapter often arrives in the same the download
completes and is removed from the queue, so that check is true right when the rebuild is
needed. Left the download button stuck mid-animation, not transitioning to the
completed state, until something else forced an unrelated rebind

Fix: Track whether this manga had any queued chapters as of the last emission
(hadQueuedChapters). Skip the rebuild only when the manga has no queued chapters now and
had none last time, but always rebuild on the transition from "had queued chapters" to "has
none" — guaranteeing the final completion update is never dropped, while still skipping
emissions for unrelated manga downloading in the background.

Chapter Mark as Read Freeze, Missed Marks, and Start Reading Button

Problem: Three related issues caused by the same root:

  • Freeze (~1 second): Marking a single chapter as read or bookmarked called adapter.setChapters()updateDataSet(), replacing every ChapterItem object in the adapter and rebinding the entire list including the manga header with cover art, metadata, genres and description.

  • Missed marks: The same updateDataSet() call replaced all ChapterItem objects mid-gesture when swiping rapidly. FlexibleAdapter's swipe recovery would then fire notifyItemChanged on the in-progress item, resetting the swipe without completing it — producing a consistent miss rate of roughly 6 in every 20 rapid swipes.

  • Start reading button not updating: After fixing the above, the start reading button in MangaHeaderHolder stopped updating its chapter number since it only refreshes during a full bind() call.

Fix:

  • Add updateChaptersQuick() to MangaDetailsController — for single chapter marks call adapter.notifyItemChanged(position) on just the one affected item, leaving all other ChapterItem objects untouched. This eliminates both the freeze and the missed marks. Bulk operations still use the full updateChapters().
  • Add a dedicated updateReadingButton() method to MangaHeaderHolder that only updates the button's text and enabled state based on getNextUnreadChapter(), called directly on the live holder instance via getHeader() from the controller. Avoids triggering a full header rebind which would cause a visible blink and reintroduce the lag.
Migrate All Progress Dialog

Problem: Clicking Migrate All or Copy All showed no indication that migration was in progress — the screen appeared with no feedback while still being interactable.

Fix: Show a non-dismissible progress dialog during performMigrations that updates with current / total as each manga is migrated. Also refactored performMigrations to take a snapshot of items at start for stability, preventing the list changing mid-migration from causing skips or crashes.

Library Update Spinner Not Animating / Getting Stuck Off

Problem: The per-category update (refresh) icon on the library screen frequently never
started spinning when tapped, or appeared to only start once a manga with an actual update
was found partway through the scan - even though the update was genuinely running the whole
time. With no updates found at all, the icon never spun.

Root cause: LibraryUpdateJob.startNow() only enqueues a WorkManager job; the job
instance's categoryIds set isn't populated until a few lines into doWork(). WorkManager
reports the work as RUNNING right as doWork() starts, before categoryIds is filled in.
LibraryController's isRunningFlow collector only fires once per state transition, so if
it processed that "now running" event during this gap, every header read
categoryInQueue() == false and the spinner never started - and nothing re-checked it again
until an unrelated rebind happened to occur later (most commonly triggered by a manga
actually having new chapters, which is why it looked correlated with "finding an update").

Fix: Add isCategoryUpdating() to LibraryCategoryAdapter.LibraryListener, implemented
in LibraryController via a pendingManualCategoryUpdates set that's added to the instant a
category update is triggered (single-category tap or global update) and only cleared once
isRunningFlow confirms the job has actually stopped. LibraryHeaderHolder now checks this
instead of calling LibraryUpdateJob.categoryInQueue() directly, so the spinner is immune to
premature/racy rebinds and stays on continuously from tap to genuine completion.

UI Freeze Adding a Category to an Already-Running Library Update

Problem: Tapping a category's update button while a different category's update was
already in progress froze the entire app for several seconds.

Root cause: LibraryUpdateJob.addCategory() fetched the manga to update via
runBlocking { getMangaToUpdate(categoryId) }, which calls getLibraryManga.await() - a
full library database fetch. Since addCategory() is called from startNow(), itself
called directly from the button tap handler on the main thread, runBlocking froze the UI
for as long as that query took.

Note. Freeze severity likely scales with library size, since addCategory()'s
runBlocking fetch pulls the entire library (not just the target
category) and competes with the running job's own DB activity - small
libraries may not exhibit this noticeably, larger ones more so.

Fix: Move addCategory()'s work into extraScope.launch (already Dispatchers.IO
elsewhere in the same class) instead of blocking the calling thread. Also optimized
isRunning() to check the in-memory instance reference first (set for the exact duration
a job is actually running) before falling back to a blocking WorkManager query, avoiding a
smaller redundant cost on every call from the same process.


Files Changed

File Change
DeferredField.kt Fix mutex double-unlock in set(), guard initialize() against duplicate calls
MigrationProcessHolder.kt Per-holder MainScope with bindJob cancellation, add onRecycled()
MigrationProcessAdapter.kt Add onViewRecycled(), batch snapshot in performMigrations, progress callback
MigratingManga.kt Replace bare CoroutineContext with proper CoroutineScope, expose cancel()
LibraryController.kt Conditional destroyActionModeIfNeeded() to preserve selected library manga during background downloads, isCategoryUpdating()/pendingManualCategoryUpdates for reliable update spinner state
LibraryManga.kt Add flowMapper excluding description for reactive flow subscription
MangaDetailsController.kt Add updateChaptersQuick() for single chapter updates
MangaDetailsPresenter.kt description == null check, hadQueuedChapters queue-transition tracking in onQueueUpdate, updateChaptersQuick usage
LibraryHeaderHolder.kt Use isCategoryUpdating() via listener instead of LibraryUpdateJob.categoryInQueue() directly
LibraryCategoryAdapter.kt Add isCategoryUpdating() to LibraryListener interface
LibraryUpdateJob.kt Fix UI freeze in addCategory() by moving DB fetch off the calling thread, optimize isRunning() with in-memory instance check
MangaHeaderHolder.kt Add updateReadingButton() for targeted button text update without full rebind
library_view.sq Add findAllAsFlow query excluding description
MangaRepositoryImpl.kt Use findAllAsFlow with flowMapper for flow subscription, fix Logger.e(e)
MigrationListController.kt Add migrate all / copy all progress dialog for migrations screen
strings.xml Add migrating_progress string
LibraryGridHolder.kt Skip cover reload when cover unchanged to prevent thumbnail blink on resume
ids.xml Add manga_cover_modified resource ID

Closes #634

Fixes #595


Add a 👍 reaction to pull requests you find important.

RsReez added 3 commits June 27, 2026 14:37
…ess bar to migration screen so progress is visible, improved performMigrations stability
@RsReez RsReez changed the title Fix migration UI crash, chapter marking performance, library crashes, cursor overflow and manga selection issue while downloading Fix migration UI crash, chapter marking performance, library crash, mitigates cursor overflow, and fixes manga selection issue while downloading Jun 29, 2026
…anga view when downloading chapters individually.
@RsReez RsReez changed the title Fix migration UI crash, chapter marking performance, library crash, mitigates cursor overflow, and fixes manga selection issue while downloading Fix migration UI crash, various performance & QoL changes Jun 30, 2026
…that scales with library size on concurrent category updates
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fairy regular crashes when migrating manga Chapter list touch interactions feel buggy when Manga Details header (cover/description) is fully visible

1 participant