Skip to content

Refactor: architecture cleanup (ViewModel decomposition + dead code removal)#69

Merged
Aatricks merged 11 commits into
mainfrom
refactor/architecture-cleanup
Jul 2, 2026
Merged

Refactor: architecture cleanup (ViewModel decomposition + dead code removal)#69
Aatricks merged 11 commits into
mainfrom
refactor/architecture-cleanup

Conversation

@Aatricks

@Aatricks Aatricks commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Decomposes the two largest ViewModels into focused collaborators and removes dead code that was grandfathered in the detekt baseline. No behavior change — every extraction is a verified line-faithful move.

Reader

  • Extract scroll-restore path from ReaderViewModel; document the restore gating
  • Drop redundant progress-controller proxy properties
  • Extract the image-dimension pipeline into ImageDimensionManager

Library

  • Extract multi-select state into LibrarySelectionManager
  • Extract deletion/undo (5s window) into LibraryDeletionCoordinator
  • Extract filter/sort criteria into LibraryFilters
  • Extract download/cache badge state + reconciliation into LibraryDownloadStates
  • LibraryViewModel drops its LargeClass suppression

Web

  • Extract pure element-shaping logic into WebChapterElementShaper

Dead code

  • Remove dead legacy SharedPreferences state (pre-Room persistence nothing reads)
  • Delete baselined-but-unused members (TextUtils regexes, HtmlParser/ReaderViewModel/MangaBatSource leftovers); detekt baseline regenerated, entries only removed

Verified per commit: ./gradlew testStandardDebugUnitTest detekt (349 tests, 0 failures, detekt clean).

Aatricks added 11 commits July 1, 2026 10:38
Behavior-preserving cleanup of the reader restore logic.

- Extract the unified scroll-restore logic out of the ContentArea composable into
  two named functions: runScrollRestore (orchestrator) and awaitStableRestore (the
  watch-until-stable loop). The restore effect is now a one-line call.
- Restructure the orchestrator's early returns into a single when/handled flow so
  each function stays within detekt's complexity limits; the loop's essential
  interrelated conditions carry a documented @Suppress("CyclomaticComplexMethod").
- Document every restore magic constant (poll cadence, stability window, percent
  tolerance, stability threshold, ghost-row percent, sentinel item size).
- Add a doc block above ReaderProgressController's gating flags describing the
  transitions and the semi-independent axes.

No runtime logic, thresholds, control flow, or ordering changed. Verified:
./gradlew testStandardDebugUnitTest detekt (349 tests, 0 failures, detekt clean).
ReaderViewModel aliased four ReaderProgressController fields as its own
pass-through properties. Three (suppressAutoNavUntilUserInteraction,
restoredScrollPercent, restoredProgressSnapshot) were never referenced;
hasUserInteractedSinceLoad was read once internally and not by any UI code.
Remove all four and route the single remaining read directly through the
controller. The controller's own fields are unchanged (its tests use them),
and the members still used by the reader UI (userHasDragged, restoreInProgress,
currentLibraryItemId) stay.

Verified: ./gradlew testStandardDebugUnitTest detekt (349 tests, 0 failures,
detekt clean).
…nManager

Move the resolved-image-dimension machinery out of ReaderViewModel into a new
ImageDimensionManager: the fine-grained Compose state map, the prompt off-main
cache flush, and the trailing-debounced content rebuild (with its jobs and
pending/queued maps). ReaderViewModel keeps the ui-state-coupled content rewrite
(updateCurrentContentImageDimensions / withResolvedImageDimensions) and passes it
to the manager as a callback; it exposes the same public surface
(resolvedImageDimensions, persistImageDimensions) by delegation, so callers and
the reader UI are unchanged. resetState now calls manager.reset(), preserving the
original behavior of leaving the db-flush job and pending map running.

Verified: ./gradlew testStandardDebugUnitTest detekt (349 tests, 0 failures,
detekt clean).
…entShaper

WebContentLoader mixed ~130 lines of pure, stateless element shaping (long-strip
detection, adjacent-image grouping, wide/double-page splitting) into its I/O and
caching orchestration. Move those six functions and their two constants verbatim
into a new stateless WebChapterElementShaper object; WebContentLoader now calls
through it at the three existing sites. No behavior change.

The moved code's pre-existing, already-baselined detekt findings (magic numbers,
long lines, nesting/return-count on the grouping functions) are relocated in
detekt-baseline.xml from WebContentLoader to WebChapterElementShaper — same
findings, new home.

Verified: ./gradlew testStandardDebugUnitTest detekt (349 tests, 0 failures,
detekt clean).
…nager

Move LibraryViewModel's selection state (_selectedItems, _selectionModeEnabled)
and the toggle/select/deselect/group/all/enter/clear operations into a new
LibrarySelectionManager. The ViewModel delegates and reads the manager's flows in
its state combine; group- and select-all operations pass their ids in, keeping the
manager decoupled from the library list and ui state. No behavior change.

Verified: ./gradlew testStandardDebugUnitTest detekt (349 tests, 0 failures,
detekt clean).
Move LibraryViewModel's pending-deletion state and the 5s undo-window logic
(_pendingDeletion, the delete job, schedule/undo/commit/flush/removeImmediate) into
a new LibraryDeletionCoordinator. The ViewModel delegates and reads the
coordinator's pendingDeletion flow in its state combine; deletion errors surface
back through a callback. No behavior change.

Verified: ./gradlew testStandardDebugUnitTest detekt (349 tests, 0 failures,
detekt clean).
Move LibraryViewModel's search/content-type/sort/status filter flows, their setters,
and the pure filterAndSortItems function into a new LibraryFilters holder. The
ViewModel exposes the flows by delegation and applies them via filters.apply() in its
state combine. No behavior change.

Verified: ./gradlew testStandardDebugUnitTest detekt (349 tests, 0 failures, detekt
clean).
…tates

Move LibraryViewModel's per-chapter download/cache badge state and its
reconciliation — the queue observer, on-demand reconcile + auto-resume of
incomplete downloads, and the semaphore-limited cache-state refresh — into a new
LibraryDownloadStates. The ViewModel delegates the public reconcile/refresh methods,
reads the manager's chapterCacheStates in its combine, and routes its three
remaining setCacheState writes through it. No behavior change.

Verified: ./gradlew testStandardDebugUnitTest detekt (349 tests, 0 failures, detekt
clean).
With selection, deletion, filters, and download/cache-state extracted into their own
collaborators, LibraryViewModel's class body is now under detekt's LargeClass
threshold, so the suppression it carried is no longer needed.

Verified: ./gradlew detekt (clean, no LargeClass).
Drop pre-Room content/session persistence that nothing reads anymore:
saveParagraphs/loadParagraphs, saveLibraryItems, the currentUrl / currentTitle /
scrollPosition properties, clearContent, and their now-orphaned keys.
loadLibraryItems stays (LibraryRepository still uses it for one-time migration to
Room). With the dead functions gone, PreferencesManager is under detekt's
TooManyFunctions threshold, so its @Suppress is removed too.

Verified: ./gradlew testStandardDebugUnitTest detekt (349 tests, 0 failures, detekt
clean).
Delete confirmed-unused private members that were baselined rather than removed:
nine dead regexes in TextUtils (one a duplicate of MULTIPLE_SPACES_REGEX),
HtmlParser's unused WHITESPACE_REGEX, ReaderViewModel's unused
MIN_SCROLL_OFFSET_DELTA_PX constant and isPlaceholderAtCurrentPosition (a duplicate
of the live one in ReaderProgressController), and MangaBatSource's unused
fetchChaptersFromApi fallback. Regenerated detekt-baseline.xml to drop the 13
now-stale entries.

Verified: ./gradlew testStandardDebugUnitTest detekt (349 tests, 0 failures, detekt
clean).
@Aatricks Aatricks merged commit ce7a9a4 into main Jul 2, 2026
4 checks passed
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.

1 participant