From 6550a78848e8ec5cf807f2b8a71b65615a58d4a1 Mon Sep 17 00:00:00 2001 From: bizzkoot Date: Tue, 4 Aug 2026 18:39:43 +0800 Subject: [PATCH 01/54] docs(merge): independent review of upstream analysis + applied corrections 4-subagent review verified the 2026-08 upstream merge analysis (170 commits): - All 6 live fork bug claims CONFIRMED, 9/9 ALREADY-HAVE claims SOUND, trap commit 72bfcad03 correctly flagged, PRDs A-D rated SOUND Corrections applied: - 0cb9da9027 reclassified SKIP->PORT-A2 (pre-Drizzle expo-sqlite tx-concurrency fix) - 5 DIRECT labels -> MANUAL (31cb4b99, 93bc5e5e, 2a919ec0, f1fdafd3, a062beee) - 9783c4d5e3 reclassified SKIP->PORT-A1 (safe back-nav deletion) - c3c891cea0 relabeled SKIP-CI -> SKIP-INFRA - PRD-A: 29 fixes (16 Tier-1 + 6 SQL + 7 extended), epub/import.ts hunk added, precondition + fork-side test notes - PRD-C: require BOTH surfaceContainerLow+High theme keys - Counts: PORT 74->76, SKIP 96->94 Report: specs/upstream-merge-analysis-2026-08/REVIEW-2026-08-04.md --- .agents/upstream-merge-memory.md | 32 ++++ .../upstream-merge-analysis-2026-08/README.md | 54 ++++++ .../REVIEW-2026-08-04.md | 103 +++++++++++ .../analysis.md | 102 +++++++++++ .../batches/PRD-A-safety-fixes.md | 129 +++++++++++++ .../batches/PRD-B-high-value-features.md | 93 ++++++++++ .../batches/PRD-C-ux-theme-train.md | 78 ++++++++ .../batches/PRD-D-project-scale-features.md | 99 ++++++++++ .../commit-manifest.csv | 171 ++++++++++++++++++ .../reference/commit_overlap_170.txt | 170 +++++++++++++++++ .../reference/fork_context.md | 45 +++++ .../reference/subagent/group1-report.md | 91 ++++++++++ .../reference/subagent/group2-report.md | 90 +++++++++ .../reference/subagent/group3-report.md | 100 ++++++++++ .../reference/subagent/group4-report.md | 88 +++++++++ .../reference/upstream_commits_170.txt | 170 +++++++++++++++++ 16 files changed, 1615 insertions(+) create mode 100644 specs/upstream-merge-analysis-2026-08/README.md create mode 100644 specs/upstream-merge-analysis-2026-08/REVIEW-2026-08-04.md create mode 100644 specs/upstream-merge-analysis-2026-08/analysis.md create mode 100644 specs/upstream-merge-analysis-2026-08/batches/PRD-A-safety-fixes.md create mode 100644 specs/upstream-merge-analysis-2026-08/batches/PRD-B-high-value-features.md create mode 100644 specs/upstream-merge-analysis-2026-08/batches/PRD-C-ux-theme-train.md create mode 100644 specs/upstream-merge-analysis-2026-08/batches/PRD-D-project-scale-features.md create mode 100644 specs/upstream-merge-analysis-2026-08/commit-manifest.csv create mode 100644 specs/upstream-merge-analysis-2026-08/reference/commit_overlap_170.txt create mode 100644 specs/upstream-merge-analysis-2026-08/reference/fork_context.md create mode 100644 specs/upstream-merge-analysis-2026-08/reference/subagent/group1-report.md create mode 100644 specs/upstream-merge-analysis-2026-08/reference/subagent/group2-report.md create mode 100644 specs/upstream-merge-analysis-2026-08/reference/subagent/group3-report.md create mode 100644 specs/upstream-merge-analysis-2026-08/reference/subagent/group4-report.md create mode 100644 specs/upstream-merge-analysis-2026-08/reference/upstream_commits_170.txt diff --git a/.agents/upstream-merge-memory.md b/.agents/upstream-merge-memory.md index 608a8c71cb..a984a5f24f 100644 --- a/.agents/upstream-merge-memory.md +++ b/.agents/upstream-merge-memory.md @@ -36,6 +36,38 @@ applyTo: '**' # Merge History +## 2026-08-03 - Upstream Merge Analysis (170 commits, PLANNED - no code changes) + +**Analysis Range**: upstream/master @ c3260e8e0 (2026-08-01) from merge base 467a97dcf (2025-12-24) +**Commits Analyzed**: 170 (66 fix, 56 feat, 17 chore, 14 refactor, 5 docs, 4 perf, 2 test, 1 ci) +**Method**: 4 parallel subagents (chronological groups) cross-referencing git diffs vs fork source; key claims spot-verified +**Strategy**: Selective cherry-pick / manual port (full merge still infeasible - 3 architecture walls) + +**Architecture Walls (unchanged since 2025-12-30, now deeper):** +1. DB: upstream Drizzle ORM + op-sqlite (#1735) vs fork expo-sqlite raw SQL + custom MigrationRunner +2. TTS/Native: upstream Nitro modules + MediaSession + WorkManager (#1889/#1896) vs fork custom Kotlin (TTSHighlightModule, TTSForegroundService, DoH) + background-actions +3. Build: upstream Expo 55 managed + rock CLI (#1885/#1812) vs fork Expo 54 bare + Gradle 9.2.0 + +**Outcome**: 76 of 170 commits worth porting (batches A-D). 94 skipped (CI/release/docs/infra/ALREADY-HAVE/Drizzle). (Updated 2026-08-04 after independent review - see below.) + +**Key findings:** +- Fork often carries byte-identical PRE-FIX code - has the same live bugs upstream just fixed +- Verified live fork bugs: deleteReadChaptersFromDb passes novelId as chapter id (wrong folder); deleteDownloads UPDATE without WHERE wipes all flags; ServiceManager.setMeta negative-delay throttle; WHERE sort=1 default-category breaks on reorder; TEXT page comparison in multi-page queries; TTS reads quotes aloud (normalizeText); unawaited runAsync inside withTransactionAsync (NovelQueries/helpers.tsx) +- ALREADY-HAVE (skip): DoH, MediaSession/media notification, WebView-reset fix, metro middleware, EPUB exporter, volume buttons +- TRAP commit: 72bfcad03 (lint cleanup touching WebViewReader/ReaderScreen/ChapterContext) - never cherry-pick +- Raw-SQL layer is a PORTABILITY ADVANTAGE for pre-Drizzle-era SQL fixes + +**Batches planned (details in specs/upstream-merge-analysis-2026-08/):** +- Batch A (29 fixes, half-day): 16 Tier-1 (8 DIRECT clean-apply + 8 MANUAL) + 6 SQL + 7 extended (incl. c3260e8e0 download-removal fix and 0cb9da9027 tx-concurrency fix) +- Batch B (7 features + 15 stretch): Kitsu tracker, ignorable updates, EPUB chapter numbers, parallel library updates, first-unread FAB, jump-to-chapter fix, download cooldown +- Batch C (7 + 2): theme refactor -> dynamic Material You colors -> MD3 slider/tabs/menu +- Batch D (5 core + 10 optional, separate PRs): time tracking, in-chapter search, stats overhaul, reader perf sprint, scheduled updates + +**2026-08-04 - Independent Review + Corrections Applied**: 4 review subagents verified the analysis (all 6 live-bug claims CONFIRMED, 9/9 ALREADY-HAVE SOUND, trap commit correct, PRDs SOUND). Corrections applied to manifest/PRDs/analysis/README: (1) 0cb9da9027 reclassified SKIP->PORT-A2 (pre-Drizzle expo-sqlite tx fix); (2) 5 DIRECT labels -> MANUAL (31cb4b99, 93bc5e5e, 2a919ec0, f1fdafd3, a062beee - fork rewrote those files, hunks fail apply); (3) 9783c4d5e3 reclassified SKIP->PORT-A1/low; (4) c3c891cea0 relabeled SKIP-CI->SKIP-INFRA; PRD-C now requires BOTH surfaceContainerLow+High keys; PRD-A 45c4ea8ca0 now includes epub/import.ts hunk. Full report: specs/upstream-merge-analysis-2026-08/REVIEW-2026-08-04.md + +**Docs**: specs/upstream-merge-analysis-2026-08/ (README.md, analysis.md, commit-manifest.csv, batches/PRD-A..D, REVIEW-2026-08-04.md) +**Status**: PLANNED - implementation deferred; dev branch has unpushed TTS work (commit/push before starting Batch A) + ## 2025-12-30 - Upstream Merge Analysis (FAILED - DIVERGENCE DETECTED) **Analysis Summary:** diff --git a/specs/upstream-merge-analysis-2026-08/README.md b/specs/upstream-merge-analysis-2026-08/README.md new file mode 100644 index 0000000000..5902a772b1 --- /dev/null +++ b/specs/upstream-merge-analysis-2026-08/README.md @@ -0,0 +1,54 @@ +# Upstream Merge Analysis β€” 2026-08 + +**Status**: πŸ“‹ PLANNED (analysis only β€” NO code changes made yet) +**Date**: 2026-08-03 +**Upstream analyzed**: `original` branch (= upstream/master @ `c3260e8e0`, 2026-08-01) +**Fork analyzed**: `dev` branch (HEAD `40c667679` v2.1.3, 2026-08-03) +**Merge base**: `467a97dcf` (2025-12-24) + +--- + +## What this folder contains + +| File | Purpose | +|---|---| +| `README.md` | This index + executive summary | +| `analysis.md` | Full synthesis report: the 3 architecture walls, tiers, per-commit classification rationale, skip list | +| `commit-manifest.csv` | Machine-readable classification of **all 170 upstream commits** (action, batch, value, risk, port, fork-overlap files, notes) | +| `batches/PRD-A-safety-fixes.md` | Batch A: 27 verified bug fixes (15 Tier-1 + 5 SQL + 7 extended) | +| `batches/PRD-B-high-value-features.md` | Batch B: 7 high-value features (+ 15 stretch) | +| `batches/PRD-C-ux-theme-train.md` | Batch C: theme refactor β†’ dynamic colors β†’ M3 components | +| `batches/PRD-D-project-scale-features.md` | Batch D: project-scale rewrites (time tracking, search, stats, perf) | +| `reference/upstream_commits_170.txt` | Raw commit list (hash\|author\|date\|subject) | +| `reference/commit_overlap_170.txt` | Per-commit fork-overlap files | +| `reference/fork_context.md` | Fork architecture context given to analysis agents | +| `reference/subagent/group1-4-report.md` | Full per-commit assessments from the 4 analysis agents | + +## Executive summary + +- **170 upstream commits analyzed**; **76 are worth porting** (in some form), **94 skipped**. +- Full merge remains **infeasible** (confirmed again): three hard architecture walls separate the repos (Drizzle/op-sqlite vs raw SQL; Nitro/WorkManager vs custom Kotlin + background-actions; Expo-managed/rock vs bare Expo 54/Gradle 9.2.0). +- The recommended strategy stays **selective cherry-pick / manual port** (per `.agents/upstream-merge-memory.md` 2025-12-30). +- **Highest-value insight**: the fork often carries the *byte-identical pre-fix code* that upstream just fixed β€” the fork has the same live bugs. Batch A is therefore almost pure risk-reduction with verified low conflict surface. +- **2026-08-04 update**: independent 4-subagent review applied corrections (see `REVIEW-2026-08-04.md`) β€” `0cb9da9027` reclassified to PORT-A2 (pre-Drizzle tx fix), 5 DIRECT labels β†’ MANUAL, `9783c4d5e3` β†’ PORT-A1, `c3c891cea0` relabeled SKIP-INFRA. + +## Suggested execution order + +1. **Batch A β€” Safety fixes** (half-day): 29 verified bug fixes β†’ run `pnpm run test` + TTS wake-cycle tests. +2. **Batch B β€” High-value features** (2–3 days): Kitsu tracker, ignorable update notifications, EPUB chapter numbers, parallel library updates, first-unread FAB, jump-to-chapter fix, download cooldown. +3. **Batch C β€” UX/theme train**: theme refactor β†’ dynamic Material You colors β†’ M3 slider/tabs/menu. +4. **Batch D β€” Project-scale** (separate PRs): time tracking, in-chapter search, stats overhaul, reader perf sprint. +5. **Continuous**: re-scan `original` every 2–4 weeks for new fixes in pre-fix-era code patterns. + +## Preconditions before implementation + +- ⚠️ `dev` branch is currently **unpushed** (TTS text-cleanup work, per AGENTS.md). Commit/push current state before starting Batch A. +- Each PRD lists its own testing gates (`pnpm run type-check`, `lint`, `format`, `test`, TTS wake-cycle/refill suites). + +## Legend (manifest columns) + +- **action**: `PORT` (recommend porting) | `SKIP` (do not port) +- **batch**: A1/A2/A3 (safety), B/B-stretch, C/C-optional, D/D-stretch/D-optional, or SKIP-* category +- **value**: HIGH / MEDIUM / LOW / NONE β€” user-facing benefit to the fork +- **risk**: GREEN (new files / untouched files) / YELLOW (localized overlap) / RED (fork-critical files or upstream-only infra) +- **port**: DIRECT (cherry-pick) / MANUAL (adapt) / MANUAL-rewrite (reimplement on fork architecture) / NOT-FEASIBLE diff --git a/specs/upstream-merge-analysis-2026-08/REVIEW-2026-08-04.md b/specs/upstream-merge-analysis-2026-08/REVIEW-2026-08-04.md new file mode 100644 index 0000000000..6536dd22cf --- /dev/null +++ b/specs/upstream-merge-analysis-2026-08/REVIEW-2026-08-04.md @@ -0,0 +1,103 @@ +# Independent Review of Upstream Merge Analysis β€” 2026-08-04 + +**Reviewer**: 4 independent review subagents (fresh context) + parent spot-verification +**Subject**: `specs/upstream-merge-analysis-2026-08/` (README, analysis.md, commit-manifest.csv, batches/PRD-A..D) + `.agents/upstream-merge-memory.md` entry +**Date**: 2026-08-04 Β· **Fork HEAD**: `9984f90d9` Β· **Upstream**: `original` @ `c3260e8e0` Β· **Merge base**: `467a97dcf` + +--- + +## Verdict + +**The analysis is HIGH quality and substantially accurate.** The PORT-vs-SKIP axis, the 6 "verified live fork bugs," all 9 ALREADY-HAVE claims, the trap-commit warning, and the batch PRDs all hold up under independent verification. **2 real defects, 3 minor inconsistencies, and 4 PRD-level gaps** were found β€” none block implementation, but all should be corrected before Batch A starts. + +--- + +## 1. Verified CORRECT (independent confirmation) + +| Area | Result | Evidence | +|---|---|---| +| Manifest structure | 170 rows, 74 PORT / 96 SKIP β€” counts match analysis.md Β§3/Β§4 exactly | python CSV parse; 74+96=170 | +| Divergence snapshot | behind=170 βœ“; files changed fork=596, upstream=1088, overlap=206 βœ“ | `git rev-list`, `git diff --name-only` | +| Commit-type counts | 66 fix / 56 feat / 17 chore / 14 refactor / 5 docs / 4 perf / 2 test / 1 ci (minor: raw count gives 67 fix incl. "Fix:" variant β€” cosmetic) | `git log --format` | +| 6 live fork bugs (Β§5) | **ALL 6 CONFIRMED** in fork source: ChapterQueries.ts:180 novelId-twice, :173 WHERE-less UPDATE, ServiceManager.ts:152-153 inverted throttle/negative delay, `WHERE sort = 1` Γ—5 sites, `page TEXT` string ordering, core.js:2203 no quote-strip | reviewer 2 (file:line evidence) + parent spot-checks | +| ALREADY-HAVE (9/9) | All SOUND β€” fork has real equivalents: DoH (DoHManagerModule.kt), MediaSession (TTSForegroundService.kt + ttsNotification.ts), EPUB export (@cd-z/react-native-epub-creator), volume buttons, WebView-reset fix (injectJavaScript), metro middleware | reviewer 3 | +| Trap commit `72bfcad034` | Correctly flagged β€” touches WebViewReader/ReaderScreen/ChapterContext, patch does not apply | reviewer 1 + parent (`git apply --check` fails) | +| SKIP categories (13/16 samples) | Valid reasons (CI/release/infra/Drizzle/WorkManager/i18n/docs/dep) | reviewer 3 | +| Batch PRDs A-D | All **SOUND**: ~40 fork file paths verified, commitβ†’file mappings accurate, testing gates present, Aβ†’Bβ†’Cβ†’D ordering sound, consistent with manifest | reviewer 4 | +| Direct ports (8/13) | `git apply --check` applies cleanly: 76106317b6, 65e8610030, 788f6cede7, 57b9d41b5c, 34be7a1234, 9e38ad114d, 45c4ea8ca0, 4088d5966b | reviewer 1 + parent spot-check | +| A1/A2/A3 bug claims (8/9) | Byte-identical pre-fix state verified for cited fork files | reviewer 1 | + +--- + +## 2. Defects found (should fix before implementation) + +### πŸ”΄ D1 β€” `0cb9da9027` misclassified as SKIP-OTHER β€” **should be PORT-A2** +- `refactor: Replace withTransactionAsync with withExclusiveTransactionAsync for Improved Concurrency Handling (#1636)`, dated **2026-01-15** +- **The SKIP rationale was factually wrong**: it was labeled "Drizzle/op-sqlite transaction API," but `git show 0cb9da9027:src/database/db.ts` = `import * as SQLite from 'expo-sqlite'` β€” this is **pre-Drizzle** (Drizzle migration `b8c177bc5` is 2026-02-19). +- It touches exactly the fork-shared files: CategoryQueries.ts, NovelQueries.ts, StatsQueries.ts, helpers.tsx, migrateNovel.ts. +- The fork carries the identical pre-fix pattern: `NovelQueries.ts:180` unawaited `db.runAsync` inside `withTransactionAsync` (verified). Fork already uses `withExclusiveTransactionAsync` in ChapterQueries.ts:26 / LibraryUpdateQueries.ts:62 β€” API available. +- This is a real SQLITE_BUSY/concurrency fix that maps 1:1 onto fork raw-SQL code. + +### 🟠 D2 β€” port=DIRECT over-used (5/13 = 38% false positives) +These 5 rows are labeled `DIRECT`/`GREEN` but **fail `git apply --check`** on the fork tree (files were rewritten with fork infra β€” `useScaledDimensions`/`uiScale`, `eslint-disable` headers, `@env` config, custom .gitignore): + +| hash | cause of mismatch | should be | +|---|---|---| +| `31cb4b994a` | BottomTabBar rewritten (`getStyles` + scaled padding) | MANUAL-trivial, risk YELLOW | +| `93bc5e5e4b` | myAnimeList.ts diverged (`@env` client id, WebBrowser, pkceChallenger) | MANUAL, YELLOW | +| `2a919ec0f9` | .gitignore context differs (fork expo-cli block) | MANUAL-trivial, YELLOW | +| `f1fdafd37f` | BrowseScreen.tsx has eslint-disable header + no Color import | MANUAL, YELLOW | +| `a062beee87` | SetCategoriesModal.tsx rewritten with scaleDimension/AppText | MANUAL, YELLOW | + +All 5 remain valid PORTs β€” only the port-strategy label + risk level are wrong. + +### 🟠 D3 β€” `9783c4d5e3` SKIP-OTHER contradicts its own subagent report +- `fix: Normalize Repository Back Navigation (#1905)` β€” small safe deletion (βˆ’16/+2) in fork-shared SettingsRepositoryScreen.tsx. +- group1 subagent classified it LOW/YELLOW/MANUAL **and explicitly listed it in its batch-B port recommendation**. The manifest overrode to SKIP-OTHER without documenting why. +- Fork carries the identical pre-fix code (`useBackHandler` + unconditional `popTo`). Defensible as a low-value skip, but the override should be documented (or reclassified PORT-A1/low). + +--- + +## 3. Minor inconsistencies (cosmetic, non-blocking) + +| # | Item | Detail | +|---|---|---| +| M1 | Claim 1 attribution (analysis.md Β§5) | `c3260e8e0` "fixes" the novelId-twice bug in effect, but upstream actually corrected the typo earlier in the Drizzle migration `b8c177bc5`. c3260e8e0 remains the correct *porting vehicle* for the fork. | +| M2 | Claim 5 worked example (analysis.md Β§5) | `'10' sorts before '2'` is true for ASC (getNextChapter:453) but inverted for the DESC query cited (line 434; there the failure mode is `'9'` before `'10'`). Substance correct. | +| M3 | `c3c891cea0` category label | Labeled SKIP-CI but is a 40-file managed-workflow migration β†’ should be SKIP-INFRA. Skip decision itself is correct. | +| M4 | `e8c5550256` borderline skip | Perf-only 2-line core.js regex twin of the ported `54fedc2f` (1-line, PORT-A1/HIGH). Defensible but inconsistent; fork carries the pre-fix regex at core.js:4133. | +| M5 | Ahead-count 306 vs 307 | Analysis was run at HEAD `40c667679`; `docs(readme)` landed after β†’ 307 now. Snapshot drift, not an error. | +| M6 | Manifest note | Fork `ttsNotification.ts` defines 3 buttons; the "5-button" claim lives in native MediaSession/notification code. Cosmetic imprecision. | + +--- + +## 4. PRD-level gaps (from PRD review) + +| # | PRD | Gap | +|---|---|---| +| G1 | PRD-A `45c4ea8ca0` | Omits the 7-line `src/services/epub/import.ts` hunk (try/catch around `NativeFile.copyFile` β€” fork import.ts:129 is pre-fix). The commit's named "EPUB import crash loop" fix lives partly there; port must include both files. | +| G2 | PRD-C #6 (risk table) | Mentions only `surfaceContainerLow`; fork themes lack **both** `surfaceContainerHigh` and `surfaceContainerLow` (verified `src/theme/types/index.ts:39-41`, useTheme.ts:22-27, all 10 md3 variants). Both keys must be added to the type + computed-colors + variants. | +| G3 | PRD-A `0c3542836c` | Line refs off by ~8 (35/48/60 not ~43/56/69); INSERT trigger is recompute-style (not byte-identical to upstream pre-fix) β€” julianday change still applies, port valid. | +| G4 | PRD-A `31cb4b994a` | Says "drop paddingVertical: 6"; fork uses scaled `paddingVertical: padding.xs` β€” intent-equivalent, not literal. | +| G5 | PRD-A | README precondition "push `dev` before Batch A" not carried into PRD-A Β§5; fork-side unit tests should be authored for the `c3260e8e0` SQL fixes (upstream tests are Drizzle-era, not cherry-pickable). | + +--- + +## 5. Recommended corrections (priority order) + +1. Reclassify `0cb9da9027` β†’ **PORT-A2** (SQL-layer concurrency fix) + add `NovelQueries.ts:180/321`, `helpers.tsx:133-137` unawaited-`runAsync` items to the audit follow-up list (they pair with `2cd5a97ee8` which the analysis already flagged). +2. Reclassify 5 rows from DIRECT/GREEN β†’ MANUAL(-trivial)/YELLOW: `31cb4b994a`, `93bc5e5e4b`, `2a919ec0f9`, `f1fdafd37f`, `a062beee87`. +3. Document the `9783c4d5e3` skip override (or reclassify PORT-A1/low). +4. PRD-A: add `epub/import.ts` hunk to `45c4ea8ca0`; fix line refs for `0c3542836c`/`31cb4b994a`; carry the "push dev first" precondition + fork-side test note. +5. PRD-C: add `surfaceContainerHigh` alongside `surfaceContainerLow` in the theme-key risk item. +6. Cosmetic: analysis.md Β§5 claim 1 attribution nuance + claim 5 example direction; manifest note for `c3c891cea0` category. + +--- + +## 6. Bottom line + +- **Trustworthiness of analysis.md Β§5**: confirmed β€” all 6 fork-bug claims reproduced in source, all 7 cited upstream fix commits exist with the described corrections. +- **Manifest reliability**: PORT-vs-SKIP axis accurate across all sampled rows; only port-strategy labels (DIRECT) and one hidden-port misclassification (`0cb9da9027`) need fixing. +- **ALREADY-HAVE / SKIP logic**: sound (9/9, 13/16 spot-checks valid; the 3 outliers are documented above). +- **PRDs**: implementable as written; the 5 gaps above are small edits, not redesigns. +- **Net**: the analysis can be trusted as the implementation blueprint, with the ~6 corrections in Β§5 applied first. diff --git a/specs/upstream-merge-analysis-2026-08/analysis.md b/specs/upstream-merge-analysis-2026-08/analysis.md new file mode 100644 index 0000000000..689f9484ea --- /dev/null +++ b/specs/upstream-merge-analysis-2026-08/analysis.md @@ -0,0 +1,102 @@ +# Upstream Merge Analysis β€” Full Report + +**Date**: 2026-08-03 Β· **Upstream**: `original` @ `c3260e8e0` (2026-08-01) Β· **Fork**: `dev` @ `40c667679` (v2.1.3) Β· **Merge base**: `467a97dcf` (2025-12-24) + +**Method**: 170 commits classified by 4 parallel analysis agents (chronological groups of ~43), each cross-referencing `git show` diffs against fork source; key claims independently spot-verified against fork files (see verification log at the end). + +--- + +## 1. Divergence snapshot + +| Metric | Value | +|---|---| +| Commits behind `original` | 170 | +| Commits ahead (`dev` only) | 306 | +| Files changed by fork since merge base | 596 | +| Files changed upstream since merge base | 1088 | +| Files changed by BOTH (overlap surface) | 206 | +| Upstream commits already in fork (patch-id match) | 0 (nothing cherry-picked) | +| Commit types | 66 fix, 56 feat, 17 chore, 14 refactor, 5 docs, 4 perf, 2 test, 1 ci | + +## 2. The three architecture walls + +| Wall | Upstream | Fork | Porting consequence | +|---|---|---|---| +| **Database** | Drizzle ORM + op-sqlite (#1735) + `manager/` + `drizzle/migrations` | expo-sqlite raw SQL + custom `MigrationRunner` + `tables/*.ts` | Drizzle commits NOT-FEASIBLE; only their *intent* translates to raw SQL | +| **TTS & native modules** | Nitro modules (`nitro-tts`, `nitro-epub`), Android MediaSession, WorkManager background tasks (#1889, #1896) | Custom Kotlin (`TTSHighlightModule.kt`, `TTSForegroundService.kt`, `DoHManagerModule.kt`, `NativeFile.kt`), `ttsNotification.ts`, react-native-background-actions | All upstream TTS work is dead to the fork **by design** β€” fork's stack is its differentiator | +| **Build & workflow** | Expo 55 managed (#1885), rock CLI (#1812), `assets/reader/js/` layout, `scripts/generate-env-file.cjs`, config plugins | Expo 54 bare, RN community CLI + Gradle 9.2.0, `android/app/src/main/assets/js/` layout | Path-level diffs never apply; reader-JS commits must be re-diffed at function level | + +**State management also diverged**: upstream moved the novel screen to a zustand store (`useNovel`); fork uses its own `useNovel.ts` hook + `NovelContext`/`ChapterContext`. Perf refactors (`9805da63`, `3d102a52`) cannot transfer as code β€” only their perf *rationales*. + +## 3. Classification summary (all 170) + +| Batch | Count | Meaning | +|---|---|---| +| **A1** | 16 | Verified Tier-1 fixes β€” fork carries byte-identical pre-fix code; 8 verified clean-apply (DIRECT), a few need manual re-application | +| **A2** | 6 | SQL bug fixes β€” copy corrected raw SQL into fork's expo-sqlite layer | +| **A3** | 7 | Extended verified bug fixes β€” localized manual ports | +| **B** | 7 | High-value user-facing features (2–3 days) | +| **B-stretch** | 15 | Optional medium features | +| **C** | 7 | UX/theme train (prerequisite-ordered) | +| **C-optional** | 2 | Optional M3 polish | +| **D** | 5 | Project-scale rewrites (separate PRs) | +| **D-stretch** | 1 | Scheduled library updates (needs native work) | +| **D-optional** | 10 | Lower-priority project-scale candidates | +| **SKIP** | 94 | CI/release/docs/infra/ALREADY-HAVE/architecture-incompatible | + +**PORT total: 76 commits.** Full per-commit detail: `commit-manifest.csv` + `reference/subagent/*`. + +## 4. Skip categories & rationale + +| Category | Count | Reason | +|---|---|---| +| SKIP-CI | 19 | Fork has separate GitHub Actions; release automation (draft releases, semver bump, APK verification) is upstream-only | +| SKIP-INFRA | 13 | rock CLI, Expo 55, managed workflow, repo reorganization, `lneader` org rebranding (actively harmful β€” would point users at wrong repo) | +| SKIP-DRIZZLE | 9 | Migration SQL / op-sqlite APIs; fork's MigrationRunner already covers recovery + counters | +| SKIP-OTHER | 20 | No clear user value or overlaps fork-critical files (incl. **trap commit `72bfcad0`** β€” lint cleanup touching WebViewReader/ReaderScreen/ChapterContext, zero value, maximal conflict surface) | +| ALREADY-HAVE | 9 | Fork shipped its own equivalents, often earlier: DoH (`4d982479`), MediaSession/media notification (`cc04287`, `74808f08`, `67d2c7c0`), WebView-reset fix (`02b6b984`), metro middleware (`58db0352`), EPUB exporter (`4cb1890b`), volume buttons (`52914b97`), collapsed-TTS guard (`cbe7d70e`) | +| SKIP-RELEASE | 6 | Version bumps; fork has own v2.1.3 numbering | +| SKIP-DOCS | 5 | Docs only | +| SKIP-I18N | 4 | Crowdin syncs at `src/i18n/` β€” fork stores `strings/languages/` with custom keys; only per-key diffs, never overwrite | +| SKIP-DEP | 3 | `@legendapp/list` 3.3.3 patches (fork pins ^2.0.16), hermes pinning | +| SKIP-NATIVE | 5 | WorkManager/native-background-tasks/background-actions-removal commits; **`9babe167` (remove background-actions manifest customization) would break the fork β€” do NOT port** | +| SKIP-NITRO | 1 | op-sqlite DevTools adapter | + +## 5. Key findings + +1. **The fork is 4–6 weeks behind on upstream *fix* quality while ahead on TTS.** The 2026-02β†’08 upstream fix cluster (tracker search, genre crashes, download removal, category reorder, date sorting, NativeFile crashes) targets shared legacy code the fork still carries pre-fix β€” the highest risk/benefit ratio available. +2. **Raw-SQL layer is a portability advantage.** Pre-Drizzle-era upstream SQL fixes (numeric page ordering, next-chapter logic, category built-in IDs, EPUB page ordering) map ~1:1 onto fork's `db.getAllAsync` layer. +3. **`NativeFile.kt` is a shared, stable seam** β€” both trees carry the same TurboModule file; the most reliable direct cherry-pick surface (`34be7a12`, `45c4ea8`, `4088d59`). +4. **Translation drift** (`src/i18n/languages/` vs `strings/languages/`) makes every Crowdin commit a manual merge, not a cherry-pick. +5. **Verified live fork bugs** (spot-checked during analysis): + - `deleteReadChaptersFromDb` passes `chapter.novelId` twice β†’ deletes the **wrong folder**; `deleteDownloads` runs `UPDATE Chapter SET isDownloaded = 0` **without WHERE** β†’ wipes all download flags. (Upstream `c3260e8e0` ships the corrected functions and is the fork's porting vehicle; the novelId typo itself was first corrected upstream in the Drizzle migration `b8c177bc`.) + - `ServiceManager.setMeta` throttle computes `delay = 1000 - now - lastNotifUpdate` β†’ negative-delay timers, no throttling. (Upstream `57b9d41b`.) + - Default-category resolution `WHERE sort = 1` breaks after category reorder. (Upstream `ee3a4f20`.) + - Multi-page chapter ordering compares `page` as TEXT β€” `'10'` sorts before `'2'` in the ASC query (`getNextChapter`); in the DESC query (`getPrevChapter`) the failure mode is `'9'` before `'10'`. (Upstream `d15cf98`, `f069320`, `52d5c99c`.) + - Unawaited `db.runAsync` inside `withTransactionAsync` (`NovelQueries.ts:180,321`, `helpers.tsx:133-137`) β†’ SQLITE_BUSY/concurrency risk; the `withExclusiveTransactionAsync` + `await` fix is a **pre-Drizzle** expo-sqlite change that maps 1:1 onto fork code. (Upstream `0cb9da9027` β€” reclassified SKIPβ†’PORT-A2 by review 2026-08-04.) + - `normalizeText` in core.js doesn't strip leading/trailing quotes β†’ TTS reads them aloud. (Upstream `54fedc2f`.) + +## 6. Verification log (spot-checks performed during analysis) + +- `src/database/queries/ChapterQueries.ts:167-184` β€” download-removal bugs confirmed +- `src/services/ServiceManager.ts:152-153` β€” inverted throttle + negative delay confirmed +- `android/.../NativeFile/NativeFile.kt` β€” unlink `throw` pre-fix state confirmed +- `src/screens/Categories/CategoriesScreen.tsx:94` β€” index-based keyExtractor bug confirmed +- `src/screens/reader/hooks/useChapter.ts:125` β€” no DB progress hydration on open +- `src/screens/reader/components/WebViewReader.tsx:473+` β€” settings synced via injectJavaScript (upstream `02b6b984` already covered) +- `android/app/src/main/assets/js/core.js:2203` β€” normalizeText lacks quote-strip +- `src/database/queries/NovelQueries.ts:180,321` + `src/database/utils/helpers.tsx:133-137` β€” unawaited `runAsync` inside `withTransactionAsync` (concrete fix: upstream `0cb9da9027` β†’ `withExclusiveTransactionAsync` + `await`; concept of upstream `2cd5a97` also applies) + +## 8. Review corrections (2026-08-04) + +Independent 4-subagent review (`REVIEW-2026-08-04.md`) confirmed the analysis's core claims and found the following corrections, all applied: + +1. **`0cb9da9027` reclassified SKIP-OTHER β†’ PORT-A2** β€” pre-Drizzle expo-sqlite transaction-concurrency fix (verified `db.ts` at that commit imports expo-sqlite; dated 2026-01-15, before the Drizzle migration). Fork carries the identical pre-fix pattern. +2. **5 rows relabeled DIRECT/GREEN β†’ MANUAL(-trivial)/YELLOW** (`31cb4b99`, `93bc5e5e`, `2a919ec0`, `f1fdafd3`, `a062beee`) β€” their upstream patches fail `git apply --check` on fork files that were rewritten (scaled dimensions, eslint headers, `@env` config). All remain valid PORTs. +3. **`9783c4d5e3` reclassified SKIP-OTHER β†’ PORT-A1/low** β€” small safe deletion in shared SettingsRepositoryScreen.tsx; group1 subagent had recommended porting it. +4. **`c3c891cea0` relabeled SKIP-CI β†’ SKIP-INFRA** β€” it is a 40-file managed-workflow migration, not CI tooling. +5. Counts updated accordingly: PORT 74β†’**76**, SKIP 96β†’**94**, A1 15β†’**16**, A2 5β†’**6**, SKIP-OTHER 22β†’**20**, SKIP-CI 20β†’**19**, SKIP-INFRA 12β†’**13**. + +## 7. Recommended execution order + +See `README.md` Β§Suggested execution order and the per-batch PRDs in `batches/`. diff --git a/specs/upstream-merge-analysis-2026-08/batches/PRD-A-safety-fixes.md b/specs/upstream-merge-analysis-2026-08/batches/PRD-A-safety-fixes.md new file mode 100644 index 0000000000..879063e548 --- /dev/null +++ b/specs/upstream-merge-analysis-2026-08/batches/PRD-A-safety-fixes.md @@ -0,0 +1,129 @@ +# PRD β€” Batch A: Safety Fixes + +**Status**: πŸ“‹ PLANNED (analysis complete; no code changes yet) +**Date**: 2026-08-03 +**Source**: `specs/upstream-merge-analysis-2026-08/analysis.md` Β· Manifest batch: A1, A2, A3 +**Estimated effort**: half-day (Phase 1) + 1 day (Phase 2) + +--- + +## 1. Objective + +Port **29 verified upstream bug fixes** into the fork. These fixes target code the fork often carries **byte-identical to the upstream pre-fix state**, meaning the fork has the same live bugs. Phase 1 is low-risk ports (8 verified clean-apply); Phase 2 is localized manual ports with verified identical pre-fix code. + +**Verified live fork bugs this batch fixes:** +- Deleting read chapters deletes the **wrong folder** (`deleteReadChaptersFromDb` passes `chapter.novelId` as chapter id) +- "Delete downloads" wipes `isDownloaded = 0` for **every chapter** in the DB (UPDATE without WHERE) +- Default category breaks after category reorder (`WHERE sort = 1`) +- TTS reads leading/trailing quotes aloud (`normalizeText`) +- Deleting a downloaded chapter whose files were removed externally **crashes the whole app** (NativeFile unlink `throw`) +- ServiceManager notification throttle computes **negative delays** (never throttles) +- Multi-page novels: chapters ordered by TEXT `page` comparison; wrong next/prev chapter +- Unawaited `db.runAsync` inside `withTransactionAsync` (NovelQueries.ts:180/321, helpers.tsx:133-137) β†’ SQLITE_BUSY/concurrency risk +- Genre strings crash EditInfoModal/NovelInfoComponents on malformed data + +--- + +## 2. Phase 1 β€” Tier-1 verified fixes (16 commits: 8 DIRECT clean-apply + 8 MANUAL) + +| Commit | Subject | Port strategy | +|---|---|---| +| `34be7a1234` | fix(native-file): prevent fatal crash when unlinking missing files (#1832) | NativeFile.kt: `if (!file.exists()) throw` β†’ `return` (1 line). **Top crash fix.** | +| `54fedc2f53` | fix(tts): remove leading/trailing quotes from normalized text (#1869) | `core.js` `normalizeText` (~line 2203): strip `"`/`'` at string ends. Manual apply (core.js diverged elsewhere). Run TTS tests after. | +| `45c4ea8ca0` | fix: handle IOException in NativeFile to prevent EPUB import crash loop (#1315) | Wrap `writeFile`/`readFile`/`copyFileContent` IOExceptions. Direct (NativeFile.kt). **ALSO includes the try/catch around `NativeFile.copyFile` in `src/services/epub/import.ts:129` (fork pre-fix) β€” the commit's named EPUB-import crash-loop fix lives partly there; port BOTH files.** | +| `4088d5966b` | fix: close OkHttp response and streams in NativeFile.downloadFile (#1740) | `response.body!!.byteStream()` β†’ wrap in `use {}`. Direct. | +| `57b9d41b5c` | fix(service): correct notification throttle logic in ServiceManager.setMeta (#1873) | Fix inverted condition + `delay = 1000 - (now - lastNotifUpdate)`. Direct (fork `ServiceManager.ts:152-153`). | +| `9e38ad114d` | fix: sanitize EPUB filename (#1480) | `ExportNovelAsEpubButton.tsx`: sanitize `novel.name` for filesystem-unsafe chars. 1 line. Direct. | +| `93bc5e5e4b` | fix: handle missing MAL list entries and null progress (#1875) | `SetTrackChaptersDialog.tsx` null-safe `progress.toString()` + default-entry creation in `myAnimeList.ts`. **Manual** β€” fork `myAnimeList.ts` diverged (@env client id, WebBrowser, pkceChallenger); only the SetTrackChaptersDialog hunk ports cleanly. | +| `788f6cede7` | fix: prevent table overflow and clean up CSS (#1876) | `index.css`: `div:has(> table):not(#LNReader-chapter){overflow:auto}` + next-button cleanup. Direct. | +| `76106317b6` | fix: restore Midnight Dusk chip contrast (#1939) | `SelectableChip.tsx`: remove `selectedColor={theme.primary}` (1 line). Direct. | +| `31cb4b994a` | fix: align bottom navigation with Material 3 specs | `BottomTabBar/index.tsx`: `paddingBottom: 16 + insets`. **Manual** β€” fork BottomTabBar rewritten (`getStyles` + scaled `padding.xs`); upstream hunk fails `git apply --check`, apply the M3 intent with fork's scaled values. | +| `0ed7d878b4` | fix: prevent white reader drawer seam | `ReaderScreen.tsx`: `drawerStyle={{backgroundColor:'transparent'}}`. Tiny hunk in fork-critical file β€” manual apply. | +| `a062beee87` | fix: add max height to Set Categories modal (#1738) | `SetCategoriesModal.tsx`: FlatList `maxHeight: 40% window`. **Manual** β€” fork modal rewritten (scaleDimension/AppText); hunk fails apply, apply maxHeight intent manually. | +| `f1fdafd37f` | fix: prevent tab bar height jump and shadow flash on Browse screen | `BrowseScreen.tsx`: add `initialLayout` to TabView + border instead of shadow. **Manual** β€” fork has eslint-disable header + no Color import; hunk fails apply, apply intent manually. | +| `65e8610030` | refactor: replace bottom navigation shift with fade | `BottomNavigator.tsx:68`: `animation: 'shift'` β†’ `'fade'`. Direct. | +| `2a919ec0f9` | chore: gitignore tsconfig.tsbuildinfo build cache (#1829) | 1-line `.gitignore`. **Manual-trivial** β€” fork .gitignore context differs (expo-cli block); append `tsconfig.tsbuildinfo` manually. | +| `9783c4d5e3` | fix: normalize repository back navigation (#1905) | `SettingsRepositoryScreen.tsx`: remove `useBackHandler` + unconditional `popTo` on back. Manual-trivial β€” fork carries identical pre-fix code; reclassified from SKIP by review 2026-08-04. | + +## 3. Phase 2a β€” SQL bug fixes (6 commits, MANUAL β€” copy corrected SQL) + +| Commit | Subject | Fork location | +|---|---|---| +| `d15cf98742` | fix: order chapter pages numerically (#1742) | `ChapterQueries.ts`: `CAST(page AS INTEGER)` in `getCustomPages`, `getPrevChapter`, `getNextChapter` | +| `f0693204e1` | fix: improve next chapter selection logic (#1744) | `ChapterQueries.ts:441` `getNextChapter`: correct position-0/page handling + `LIMIT 1`. Port together with `d15cf98` | +| `52d5c99c46` | fix: order EPUB chapters across paginated pages (#1762) | `ChapterQueries.ts:478` `getNovelDownloadedChapters`: `ORDER BY page ASC, position ASC` + rangeβ†’limit/offset rework | +| `a421177f0c` | fix: improve category badge layout and drag reordering | `CategoriesScreen.tsx:94`: fix index-based `keyExtractor` (breaks drag reorder). Optional: IconButton `onPressIn` drag handle | +| `ee3a4f20bc` | fix: preserve default category after reordering (#1922) | `NovelQueries.ts` (4 sites) + `CategoryQueries.ts:47`: replace `WHERE sort = 1` with `BUILT_IN_CATEGORY_IDS` (default=1, local=2); `updateCategoryOrderQuery` writes positional `sort = index+1`. Fork already hardcodes id 2 for Local. | +| `0cb9da9027` | refactor: replace `withTransactionAsync` with `withExclusiveTransactionAsync` (#1636) | `CategoryQueries.ts`/`NovelQueries.ts`/`StatsQueries.ts` + `helpers.tsx` + `migrateNovel.ts`: swap to `withExclusiveTransactionAsync` + `await` all `runAsync` inside transactions. Fork has the identical unawaited pattern (`NovelQueries.ts:180,321`, `helpers.tsx:133-137`). **Pre-Drizzle** expo-sqlite fix β€” reclassified from SKIP by review 2026-08-04. | + +## 4. Phase 2b β€” Extended verified fixes (7 commits, MANUAL, localized) + +| Commit | Subject | Port strategy | +|---|---|---| +| `c3260e8e01` | fix: resolve chapter download queue and removal issues (#1937) | **Highest priority in this batch.** Fix `deleteReadChaptersFromDb` (pass `chapter.id` not `novelId`), `deleteDownloads` (add `WHERE id IN (...)`), await `db.execAsync`. Skip upstream backgroundTasks/useDownloadReconciliation halves (fork uses ServiceManager + MMKV queue). | +| `eb12bdfd62` | fix: prevent novel genre rendering crashes | Add `parseGenres()` util (split on comma, handle null/empty); apply to `EditInfoModal` + `NovelInfoComponents`. | +| `8f53550d2c` | fix: restore tracker search requests | `TrackSearchDialog.tsx`: reschedule search on open + event-based `onSubmitEditing` handler + `returnKeyType="search"`. | +| `0c3542836c` | fix: synchronize library updates and optimize import refreshes | `NovelTable.ts` triggers (lines 35/48/60): `julianday()` comparison (INSERT trigger is recompute-style in fork, not byte-identical to upstream β€” julianday change applies to the `lastUpdatedAt` sub-expression of all three triggers); `LibraryQueries` `lastUpdatedAt` sort; ISO-8601 inserted-chapter timestamps. | +| `25c93afabe` | fix(reader): restore saved chapter progress from db (#1850) | `useChapter.ts:125`: `dbChapter = await getDbChapter(chapter.id)` fallback (3 lines). Complements TTS Math.max reconciliation for non-TTS reader open. | +| `7ddd934c2b` | fix: handle missing tracker client IDs | Guard clauses in `src/services/Trackers/aniList.ts` + `myAnimeList.ts` (skip `scripts/generate-env-file.cjs` hunk β€” fork uses `.env`). | +| `e75f4fba5d` | feat: add more novel statuses and icons (#1931) | `NovelStatus` enum + `getStatusIcon` in `NovelInfoHeader.tsx` (skip `.husky/pre-push` hunk). | + +## 5. Implementation order + +1. Phase 1 (16 commits) β€” can be done in ~4 focused edit sessions, grouped by file: + - Native layer: `34be7a12`, `45c4ea8`, `4088d59` (NativeFile.kt) + - Services: `57b9d41b` (ServiceManager.ts) + - Reader/WebView: `54fedc2f` (core.js), `788f6ced` (index.css), `0ed7d878` (ReaderScreen) + - UI: `76106317`, `31cb4b99`, `a062bee`, `f1fdafd`, `65e86100` + - Export/MAL: `9e38ad1`, `93bc5e5e` + - Housekeeping: `2a919ec0` +2. Phase 2a (SQL): `d15cf98`+`f069320` together, then `52d5c99c`, `a421177f`, `ee3a4f20`, `0cb9da90` (transaction concurrency). +3. Phase 2b: `c3260e8e0` first (correctness), then `eb12bdfd6`, `8f53550d`, `0c354283`, `25c93afa`, `7ddd934c`, `e75f4fba`. + +Work on a branch: `merge/original-sync-batch-a`. + +> ⚠️ **Precondition**: commit/push the current unpushed `dev` work (TTS text-cleanup, per AGENTS.md) BEFORE starting this batch. + +## 6. Testing & validation + +```bash +pnpm run type-check +pnpm run lint:fix +pnpm run format +pnpm run test # 1235+ tests, zero regressions +pnpm run test -- --testPathPattern="ChapterQueries|CategoryQueries|NativeFile|ServiceManager|StatsQueries" +pnpm run test:tts-wake-cycle # core.js + TTS changes (54fedc2f) +pnpm run test:tts-refill +``` + +- After `54fedc2f` (core.js): run TTS wake-cycle + refill suites; verify highlighted text output unchanged (length-preserving). +- After `c3260e8e0`: manual test β€” download 2 novels, delete one novel's read chapters, confirm other novel's download flags + files intact. **Write fork-side unit tests for the two SQL fixes** (upstream's tests are Drizzle-era and not cherry-pickable). +- After `ee3a4f20`: reorder categories, add a novel, confirm it lands in the default category. +- After `0cb9da9027`: run DB query suites (NovelQueries/CategoryQueries/StatsQueries) β€” transaction semantics unchanged, no SQLITE_BUSY regressions. + +## 7. Acceptance criteria + +- [ ] All 29 commits ported; no TypeScript/ESLint regressions +- [ ] Download-flag wipe bug eliminated (UPDATE scoped by `id IN`) +- [ ] Read-chapter deletion removes correct folder +- [ ] TTS no longer reads quote characters +- [ ] Deleting a chapter with missing files doesn't crash the app +- [ ] Multi-page chapters navigate in correct numeric order +- [ ] Default category preserved after reorder +- [ ] Full test suite green (1235+ tests, zero regressions) + TTS suites green +- [ ] `pnpm run format` applied (Husky pre-commit) + +## 8. Risks & mitigations + +| Risk | Mitigation | +|---|---| +| `core.js` divergence (54fedc2f) | Manual 1-line apply at function level; TTS test gates | +| `c3260e8e0` touches DB + download flows | Scoped SQL, manual QA of multi-novel download state | +| `ee3a4f20` changes category resolution semantics | Built-in IDs already partially hardcoded in fork; run CategoryQueries tests | +| Cherry-pick conflicts on fork-modified files | All items in this batch were verified against fork source pre-analysis; apply manually where needed | + +## 9. Out of scope + +- Batch B features, Batch C theme work, Batch D rewrites +- Upstream's backgroundTasks/useDownloadReconciliation architecture +- Drizzle migration commits (SKIP-DRIZZLE, see manifest) diff --git a/specs/upstream-merge-analysis-2026-08/batches/PRD-B-high-value-features.md b/specs/upstream-merge-analysis-2026-08/batches/PRD-B-high-value-features.md new file mode 100644 index 0000000000..10a2ca5796 --- /dev/null +++ b/specs/upstream-merge-analysis-2026-08/batches/PRD-B-high-value-features.md @@ -0,0 +1,93 @@ +# PRD β€” Batch B: High-Value Features + +**Status**: πŸ“‹ PLANNED (analysis complete; no code changes yet) +**Date**: 2026-08-03 +**Source**: `specs/upstream-merge-analysis-2026-08/analysis.md` Β· Manifest batch: B (7) + B-stretch (15) +**Estimated effort**: 2–3 days (core B) Β· stretch items optional, ~1 day each + +--- + +## 1. Objective + +Port **7 high-value user-facing features** that the fork genuinely lacks, adapting each to the fork's architecture (raw-SQL DB, custom TTS, MMKV settings, `strings/languages/` i18n). All are MANUAL ports β€” none cherry-pick cleanly, but each is bounded and self-contained. + +## 2. Scope β€” core batch (7 commits) + +| # | Commit | Feature | Port notes | +|---|---|---|---| +| 1 | `c40edd5b2c` | **Kitsu tracker** (#416) | New service file `kitsu.ts` (~490 lines: auth, list sync, scoring) + score selectors + tracker cards + settings screen + login dialog. Fork's tracker infra exists (`src/services/Trackers/`, TrackerCards, ScoreSelectors, SetTrackScoreDialog, SettingsTrackerScreen). Best value/effort in batch. | +| 2 | `c0877a9b06` | **Ignorable app update notifications** | Long-requested (upstream #600/#1632): "ignore this version" persistence (MMKV) so update prompt never returns. Fork's `useGithubUpdateChecker.ts` is a near-clone of upstream's deleted hook β€” port `AppUpdateChecker` + ignore-version field; keep fork's repo URL (`bizzkoot/lnreader`) + `githubReleaseUtils.pickApkAsset`. | +| 3 | `345d084eba` | **Chapter numbers in EPUB titles** (#1405) | New `epubIncludeChapterNumber` setting (useSettings, additive) + numbered-title composition in export flow. Adapt to fork's `@cd-z/react-native-epub-creator` (JS) β€” NOT upstream's nitro-epub. Files: `ExportEpubModal`, `ExportNovelAsEpubButton`. | +| 4 | `799845426c` | **Parallel library updates across sources** | Fork's `services/updates/index.ts` is a sequential `for` + `sleep(1000)` loop. Port group-by-pluginId + `Promise.all` concurrency-3 pattern. No schema/native changes. Skip upstream `TaskNotificationFactory.kt` (fork shows progress via ServiceManager metadata). | +| 5 | `8f237909d5` | **First unread chapter in ReadButton/FAB** | "Start reading" jumps to first unread chapter instead of `chapters[0]`. New `getFirstUnreadChapter` SQL + exposure via `useNovel` + `ReadButton.tsx`/`NovelScreenList.tsx` wiring. Fork has no first-unread logic today. | +| 6 | `4ad0639796` | **Jump-to-chapter / drawer can't reach unloaded chapters** | New raw-SQL queries `getNovelChaptersByNumber` (`position = ?`) + `getNovelChaptersByName` (`name LIKE ?`) β€” map 1:1 onto fork expo-sqlite; `loadUpToBatch` in useNovel; wire `JumpToChapterModal.tsx` + `ChapterDrawer/index.tsx`. Skip commit's pnpm-lock churn. | +| 7 | `5d996f1388` | **Configurable chapter download cooldown** (#1834) | `chapterDownloadCooldownMs` AppSettings + modal + replace hardcoded `await sleep(1000)` in `downloadChapter.ts:93`. Helps users hitting 429 rate limits. | + +## 3. Scope β€” stretch batch (15 commits, optional) + +| Commit | Feature | Effort | +|---|---|---| +| `c30441b949` | Smart library update filters (skip read/completed/unstarted) | SQL filter trivial (fork schema has `status`/`lastReadAt`); `SmartUpdateDialog` UI in SettingsLibraryScreen | +| `8ac628b3de` | Per-novel history removal + confirm dialog | New `deleteNovelHistory` SQL + `useHistory`/`HistoryScreen`/`HistoryCard` UI | +| `732628c40b` | Date format + relative timestamp settings | New `utils/dateFormat.ts` + Appearance setting + `DateFormatModal`; touches ~8 screens (History, Downloads, ChapterItem, Updates, GoogleDriveModal…) | +| `d02291c22c` | Remember last used library category | Small setting + rehydration in `LibraryScreen` (bottom-tab index state) | +| `97be48dfe1` | Migration review options (cover/metadata source, redownload policy + review dialog) | Adapts to fork's raw-SQL `migrateNovel.ts` + `MigrationNovelList.tsx`; closes upstream #937/#1037 | +| `3e1e211098` | WebView address bar | ~11 lines in `WebviewScreen` Appbar (URL display + address bar) | +| `eac6ad584e` | Novel app bar + EPUB export UX polish | Re-port against fork's `NovelAppbar`, `ExportEpubModal`, `JumpToChapterModal`, `NovelScreenList` (different layouts/uiScale) | +| `b53ec3352f` | Plugin setting types: dropdown + checkboxes | Extend `src/plugins/types/index.ts` + `SourceSettings.tsx` (fork currently pre-fix) | +| `ae99b077d4` | `@noble/ciphers` for plugins (#1709) | Add dep + expose `crypto` surface in `pluginManager.ts` | +| `01596c045a` | Apply active filters to chapter count display (#1793) | Extend raw-SQL `getChapterCount` with filter arg; update `useNovel` call sites (105/270/291) | +| `d2b95ae316` | NativeFile for custom CSS/JS import (#1661) | Replace `StorageAccessFramework.readAsStringAsync` in `AdvancedTab.tsx:206` with NativeFile copy/read | +| `caed965b6b` | EPUB import DocumentPicker cache-cleanup crash (#1725) | `copyToCacheDirectory: false` in `LibraryScreen.tsx:167` | +| `9922d43940` | EPUB export missing-image cleanup (#1696) | Remove `
`/`` refs to deleted files during export | +| `138fdff2e5` | Prevent duplicate WebView bottom insets | Remove `paddingBottom: bottom` in `WebviewScreen.tsx` container (fork likely has same double-inset bug) | +| `4dc933ee5a` | Remove white flash during screen transitions (#1926) | ThemedRootView paints root bg + reader stack background; replace upstream config-plugin hunk with native `res/values` windowBackground | + +## 4. Feature-specific risks + +| Feature | Risk | +|---|---| +| Kitsu tracker | New OAuth/API surface β€” needs network mocking in tests; tracker login dialog rework touches shared components | +| Ignorable update notifications | Persistence key must not clash with existing MMKV `AppSettings` fields; keep fork repo URL | +| Parallel library updates | Concurrency changes update-ordering semantics β€” regression-test global update with >3 sources | +| Jump-to-chapter fix | `loadUpToBatch` touches fork-modified `useNovel` β€” isolate + run novel-screen tests | +| Download cooldown | AppSettings interface change ripples to any screen reading settings | + +## 5. Implementation order (core) + +1. `79984542` (parallel updates) β€” smallest, highest certainty +2. `c40edd5` (Kitsu) β€” new file + wiring, no conflicts +3. `8f23790` + `4ad0639` β€” reader/navigation cluster (share `useNovel`/ChapterQueries touchpoints) +4. `345d084e` + `c0877a9b` β€” settings + persistence cluster +5. `5d996f13` β€” downloader + +Work on a branch: `merge/original-sync-batch-b`. + +## 6. Testing & validation + +```bash +pnpm run type-check && pnpm run lint:fix && pnpm run format +pnpm run test +pnpm run test -- --testPathPattern="Tracker|NovelQueries|ChapterQueries|Settings" +pnpm run test:tts-wake-cycle # safety gate if ReaderScreen/useNovel touched +``` + +Manual QA: +- Kitsu: login β†’ list sync β†’ score sync β†’ search +- Parallel updates: 3+ sources with many novels; observe parallel progress +- Jump-to-chapter: open chapter beyond preloaded batch from both modal and drawer +- Update ignore: tap "ignore version" β†’ relaunch β†’ no prompt + +## 7. Acceptance criteria + +- [ ] All 7 core features live with settings persisted in MMKV +- [ ] No regressions in existing tracker flows (MAL/AniList/MangaUpdates) +- [ ] Library updates complete faster (parallel) without duplicate/overlapping downloads +- [ ] 1235+ tests green; TTS suites green +- [ ] Stretch items each landed as separate small PRs with own tests + +## 8. Out of scope + +- Anything touching the TTS engine internals (WebViewReader TTS sections, TTSAudioManager, .kt TTS modules) +- Drizzle-based upstream implementations (rewrite to raw SQL) +- Batch C theme train and Batch D project-scale work diff --git a/specs/upstream-merge-analysis-2026-08/batches/PRD-C-ux-theme-train.md b/specs/upstream-merge-analysis-2026-08/batches/PRD-C-ux-theme-train.md new file mode 100644 index 0000000000..9f3d66b993 --- /dev/null +++ b/specs/upstream-merge-analysis-2026-08/batches/PRD-C-ux-theme-train.md @@ -0,0 +1,78 @@ +# PRD β€” Batch C: UX / Theme Train + +**Status**: πŸ“‹ PLANNED (analysis complete; no code changes yet) +**Date**: 2026-08-03 +**Source**: `specs/upstream-merge-analysis-2026-08/analysis.md` Β· Manifest batch: C (7) + C-optional (2) +**Estimated effort**: 1–2 days core Β· optional items ~0.5 day each + +--- + +## 1. Objective + +Port the Material-3 UX polish cluster from upstream. **Strict ordering matters**: the dynamic Material You colors feature (`44c8e54e`) depends on the theme-switcher refactor (`8f47e8fd`) landing first. The fork's `useTheme.ts` was verified to match the upstream pre-refactor base, so adaptation is localized. + +## 2. Core batch (7 commits, prerequisite-ordered) + +| Order | Commit | Feature | Port notes | +|---|---|---|---| +| 1 | `8f47e8fd1e` | **Theme switcher refactor** (#1839) | Context-based `useTheme` + `ThemePicker` + appearance-settings rework (~20 files). Fork's `useTheme.ts` matches the pre-refactor base (same imports, same MMKV hooks). **Prerequisite** for dynamic colors. | +| 2 | `44c8e54ed8` | **Dynamic Material You theme colors** (#1863) | Android 12+ wallpaper-derived colors via `@pchmn/expo-material3-theme` + new `src/theme/dynamic.ts` + useTheme/settings/onboarding wiring. High user appeal. Verify UI-scale + tests after. | +| 3 | `0c8546188e` | **MD3 Slider component** | New MD3 Slider (416 lines) replacing `@react-native-community/slider`. Coordinate `package.json` removal; fork's `TextSizeSlider`/`ReaderValueChange` (incl. `ReaderTTSTab` path β€” fork's TTS tab) must migrate. | +| 4 | `6d2a9f8e15` | Slider flicker fix | Only after #3 lands. | +| 5 | `3d34658d0f` | **M3 top tab indicators** | New `TopTabBar` component; replaces `SettingsReaderScreen/components/TabBar.tsx`; applies to 6 screens incl. ReaderBottomSheet. | +| 6 | `17c891e133` | **Standardize bottom sheet UX/styling** | BottomSheet component + `surfaceContainerLow` in `useTheme` + layout util; touches ReaderBottomSheet, NovelBottomSheet, LibraryBottomSheet, FilterBottomSheet, TrackSheet. Fork's useTheme needs the new surface color keys. | +| 7 | `e9f6bdaa20` | **Menu styling + outside-tap dismissal** | Menu component overhaul; fork has its own `src/components/Menu/index.tsx` used by reader/Webview appbars β€” portable pattern. | + +## 3. Optional (2 commits) + +| Commit | Feature | Notes | +|---|---|---| +| `098782d6aa` | Simplify settings layout + theme selection | Removes `react-native-theme-switch-animation` dep β€” verify fork's dependency graph before removing | +| `99c31d56bb` | MD3 outline variant for tab borders | Cosmetic across 6 screens incl. ReaderBottomSheet (fork: ReaderBottomSheetV2/ReaderTTSTab) | + +## 4. Risks + +| Risk | Mitigation | +|---|---| +| Theme refactor touches `useTheme` used everywhere | Land as standalone PR; snapshot screenshots before/after | +| Dynamic colors may conflict with fork's `uiScale`/computed color system | Keep fork's scaling; dynamic palette only feeds existing color roles | +| MD3 Slider removal of community slider ripples to reader value sliders | Migrate all consumers in same PR; check ReaderTTSTab (fork-custom) | +| `surfaceContainerLow` AND `surfaceContainerHigh` new color keys must exist in ALL fork themes (incl. TTS/reader themes). Verified: fork `ThemeColors` (`src/theme/types/index.ts:39-41`) + `addComputedColors` (`useTheme.ts:22-27`) contain NEITHER key | Add BOTH keys to the type + computed-colors function + all 10 `src/theme/md3/` variants in same PR | + +## 5. Implementation order + +1. `8f47e8fd1e` theme refactor β†’ run full test suite (themes touch SettingsAppearanceScreen, useTheme, ThemePicker) +2. `44c8e54ed8` dynamic colors β†’ manual QA on Android 12+ (wallpaper change β†’ app colors follow) +3. `0c8546188e` + `6d2a9f8e15` slider train +4. `3d34658d`, `17c891e1`, `e9f6bdaa` component polish +5. Optional: `098782d6`, `99c31d56` + +Work on a branch: `merge/original-sync-batch-c`. + +## 6. Testing & validation + +```bash +pnpm run type-check && pnpm run lint:fix && pnpm run format +pnpm run test +pnpm run test:tts-wake-cycle # safety gate (ReaderBottomSheet/ReaderTTSTab touched) +``` + +Manual QA: +- Theme switcher: light/dark/amoled + per-app themes still function; TTS sheet readable in all +- Dynamic colors: enable on Android 12+, change wallpaper, confirm palette updates without app restart crash +- Slider: text size slider in reader + TTS tab; no flicker on release +- Bottom sheet standard: open/close each sheet; no layout jump + +## 7. Acceptance criteria + +- [ ] Theme switcher refactor landed with zero visual regressions (screenshot comparison) +- [ ] Dynamic Material You colors work on Android 12+ with toggle in Settings β†’ Appearance +- [ ] MD3 slider replaces community slider app-wide; `@react-native-community/slider` removed from package.json +- [ ] Top tab indicators + bottom sheet standardization applied to all 6 target screens +- [ ] 1235+ tests green; TTS suites green + +## 8. Out of scope + +- Full M3 dialog standardization (`dfd620aa`, 51 files β€” deferred: low value-per-conflict) +- Any reader/TTS engine change +- Batch D rewrites diff --git a/specs/upstream-merge-analysis-2026-08/batches/PRD-D-project-scale-features.md b/specs/upstream-merge-analysis-2026-08/batches/PRD-D-project-scale-features.md new file mode 100644 index 0000000000..ffbd2d4eff --- /dev/null +++ b/specs/upstream-merge-analysis-2026-08/batches/PRD-D-project-scale-features.md @@ -0,0 +1,99 @@ +# PRD β€” Batch D: Project-Scale Features (Separate PRs) + +**Status**: πŸ“‹ PLANNED (analysis complete; no code changes yet) +**Date**: 2026-08-03 +**Source**: `specs/upstream-merge-analysis-2026-08/analysis.md` Β· Manifest batch: D (5) + D-stretch (1) + D-optional (10) +**Estimated effort**: each core item is its own multi-day PR + +--- + +## 1. Objective + +The highest-value upstream features that **cannot be cherry-picked** β€” every one depends on upstream-only infrastructure (Drizzle, zustand store, Nitro, WorkManager) or rewrites fork-critical files. Each must be **reimplemented on the fork's architecture** (raw-SQL + custom TTS + expo-sqlite) as an independent PR with its own test plan. + +## 2. Core projects (5) + +### D1. Time Tracking & Statistics β€” `89c43c1eef` (+ toggle `4a4208e336`) +- **User value**: reading-time stats per chapter/novel, sessions, statistics screen. Fork's StatsScreen covers library counts only β€” no `timeSpent`/session anywhere. +- **Reimplementation**: 1 new table via fork's `MigrationRunner` (raw SQL), extended raw-SQL `StatsQueries`, standalone `useTimeTracking` hook (~59 lines, portable from upstream), wiring into `useChapter`/`WebViewReader` (fork-critical β€” careful). +- **Order**: after `89c43c1e` lands, add `4a4208e3` (toggle + inactivity timeout). +- **Risks**: WebViewReader wiring interacts with TTS paragraph events β€” run full TTS suites. + +### D2. In-Chapter Search β€” `71da09ed44` (#1877) +- **User value**: best reader feature in the whole upstream set (word search inside current chapter). +- **Reimplementation**: upstream `search.js` (442 lines) + `ReaderSearchbar` (246) must be re-adapted against fork's diverged `core.js` (bionic reading, spacing rebuild, custom TTS speak/normalize) and its DOM rebuild lifecycle. ReaderScreen/ReaderAppbar/WebViewReader/useChapter wiring. +- **Risks**: HIGH β€” DOM index contract with TTS paragraph highlighting must stay intact; expect a day+ of work + TTS regression testing. + +### D3. Stats Overhaul: Genre Taxonomy + Charts β€” `96fb52df17` (#1919) +- **User value**: StatsScreen tabs (Overview/Time/Plugins), SVG donut charts, genre taxonomy screen with per-genre carousels (+2939/βˆ’719, 31 files). +- **Reimplementation**: needs new dep `react-native-svg` (fork lacks it); upstream query half is Drizzle β†’ rewrite `StatsQueries.ts` in raw SQL. UI components are new files (low conflict). +- **Prerequisite**: D1 time-tracking provides the Time tab data. + +### D4. Reader Perf Sprint β€” `3d102a52a7` (#1899) +- **User value**: faster chapter open, no re-render stutter while reading, scroll restore ~100ms vs ~1.5s. +- **Strategy**: do NOT port code (touches every fork-critical reader file + upstream store/asset layout). Port **techniques** as a manual perf sprint on fork's `WebViewReader.tsx`/`useChapter.ts`/`core.js`: + - Defer adjacent-chapter resolution + - Cache sanitized HTML + - Lazy-mount drawer + settings sheet + - Memoized drawer rows + - Single coalesced scroll listener + - One mark-as-read per chapter +- **Risks**: HIGH conflict surface by nature; run TTS wake-cycle/refill after every sub-change. + +### D5. Scheduled Library Updates β€” `164f5d2592` (D-stretch) +- **User value**: automatic library updates at 12/24/48/72/168h intervals (fork has only `updateLibraryOnLaunch`). +- **Reimplementation**: upstream uses Expo-module `native-background-tasks`/WorkManager β€” fork would add a native scheduler (AlarmManager/WorkManager) OR adapt fork's ServiceManager/headless task; plus `automaticLibraryUpdateIntervalHours` in `AppSettings` + `SettingsLibraryScreen` dialog. +- **Risks**: native scheduling + battery/Doze behavior; moderate-to-high effort. + +## 3. Optional projects (10, lower priority) + +| Commit | Project | Note | +|---|---|---| +| `198b7443ae` | RTL language support (#1717) | 44 files incl. WebViewReader (84-line diff) β€” dedicated effort for Arabic/Urdu/Hebrew users | +| `a16ec87631` | Novel covers/lists/chapter-group perf | expo-image `NovelCoverImage`, virtualized `NovelChapterGroup` lists β€” large manual port + perf regression testing | +| `9805da63cf` | Novel screen perf/architecture | Re-implement perf goals (skeletons, memoized rows, lazy modals) on fork's useNovel stack | +| `5e62c36a9b` | Library settings + category-based global updates | Completes fork's TODO-stubbed `DefaultCategoryDialog` (SettingsLibraryScreen:18); Drizzleβ†’SQL rewrite | +| `bd527a02ef` | Backup selection / plugin restore overhaul | +2021/βˆ’249, 31 files; port only selection/options/plugin-restore slices | +| `98a2a1d725` | Scanlator filtering (#1864) | Needs schema migration + plugin-pipeline change to populate column β€” only useful for aggregator sources. Do last, if at all | +| `2f45db659b` | Async DB ops (SQLITE_BUSY) | Fork-side incremental effort across queries/useNovel/library/downloadChapter/migrateNovel | +| `29bd6612e9` | Library screen perf + category index bug | Re-implement on fork's LibraryScreen/useLibrary | +| `4c7684a3a7` | Novel screen perf + download-state tracking | Port selectively (updateChapter state bug, Set-based tracking) | +| `11ff69d4f5` | Pre-fetch adjacent page chapters | Port only the LibraryUpdateQueries preload half; reader half touches fork-critical useChapter | + +## 4. Common risks + +- **Every D project touches fork-critical files** (WebViewReader, useChapter, useSettings, database, core.js). Each must be its own branch + PR with dedicated regression gates. +- **Drizzleβ†’SQL translation** is required for all query work β€” fork's `MigrationRunner` + raw-SQL layer is the target. +- **TTS regression** is the #1 hazard (D1, D2, D4) β€” the TTS wake-cycle/refill suites are mandatory gates. + +## 5. Suggested sequencing + +1. D4 (perf sprint) β€” improves baseline for everything else; sub-item sized +2. D1 (time tracking) β†’ D3 (stats overhaul) β€” data β†’ presentation +3. D2 (in-chapter search) β€” biggest user win, heaviest risk +4. D5 (scheduled updates) β€” native scheduling as its own project +5. Optional projects β€” only after core D lands and `dev` is stable + +## 6. Testing & validation (per project) + +```bash +pnpm run type-check && pnpm run lint:fix && pnpm run format +pnpm run test +pnpm run test:tts-wake-cycle && pnpm run test:tts-refill # D1/D2/D4 mandatory +``` + +Manual QA per project (per acceptance criteria defined in each PR's own spec). + +## 7. Acceptance criteria (per project) + +- [ ] Feature fully functional on fork architecture (raw SQL, custom TTS, MMKV settings) +- [ ] Zero regressions in existing fork features (TTS, DoH, backup, per-novel settings) +- [ ] 1235+ tests green + TTS suites green +- [ ] Performance metrics measured before/after (for D3/D4) +- [ ] `pnpm run format` applied before commit + +## 8. Out of scope + +- Porting upstream's zustand store / Nitro / WorkManager architecture itself +- Batch A–C work (tracked in their PRDs) +- Any upstream commit marked SKIP in `commit-manifest.csv` diff --git a/specs/upstream-merge-analysis-2026-08/commit-manifest.csv b/specs/upstream-merge-analysis-2026-08/commit-manifest.csv new file mode 100644 index 0000000000..c98c91493a --- /dev/null +++ b/specs/upstream-merge-analysis-2026-08/commit-manifest.csv @@ -0,0 +1,171 @@ +hash,short,date,author,subject,action,batch,value,risk,port,fork-overlap-files,notes +c3260e8e01e4704b57137d787ffe9cfec7941cbc,c3260e8e01,2026-08-01,Rajarshee Chatterjee,fix: resolve chapter download queue and removal issues (closes #1937),PORT,A3,HIGH,YELLOW,MANUAL,,Chapter download queue/removal. Fork has live bugs: deleteReadChaptersFromDb passes novelId twice (wrong folder); deleteDownloads UPDATE without WHERE wipes all flags. +c0877a9b0656e22c3c788854006fb456c3ed704a,c0877a9b06,2026-08-01,Rajarshee Chatterjee,feat: add ignorable app update notifications,PORT,B,HIGH,YELLOW,MANUAL,,Ignorable app update notifications (ignore-version persistence in useGithubUpdateChecker-like hook). +96fb52df1730912c40ee6918cf6a6fcd74e31cc8,96fb52df17,2026-08-01,Patrick Loser,feat: enhance stats with genre taxonomy and charts (#1919),PORT,D,HIGH,RED,MANUAL-rewrite,,Stats overhaul: genre taxonomy + SVG charts (#1919) - needs react-native-svg + raw-SQL stats rewrite. +76106317b68e8aade12b868afb16dda4671ceb16,76106317b6,2026-07-31,Rajarshee Chatterjee,fix: restore midnight dusk chip contrast (closes #1939),PORT,A1,MEDIUM,GREEN,DIRECT,,Midnight Dusk chip contrast (1-line SelectableChip). +e75f4fba5da08e2317cfed2fe92b8de09457475c,e75f4fba5d,2026-07-29,K1ngfish3r,feat: add more novel statuses and icons (#1931),PORT,A3,MEDIUM,YELLOW,MANUAL,"src/plugins/types/index.ts,src/screens/novel/components/Info/NovelInfoHeader.tsx",More novel statuses + icons (NovelStatus enum + getStatusIcon; skip .husky hunk). +4dc933ee5a361eb0535120fa192c1eb51ba8cc70,4dc933ee5a,2026-07-29,Rajarshee Chatterjee,fix: remove white flash during screen transitions (closes #1926) (#1927),PORT,B-stretch,MEDIUM,YELLOW,MANUAL,App.tsx,Remove white flash during screen transitions; replace config-plugin hunk with native res windowBackground change. +bfb12b04aa31e3013f50503697253b549ee9618c,bfb12b04aa,2026-07-29,Rajarshee Chatterjee,feat: add configurable automatic backup scheduling,SKIP,SKIP-NATIVE,-,-,-,"src/hooks/persisted/useSettings.ts,src/screens/settings/SettingsBackupScreen/index.tsx",Automatic backup scheduling needs upstream BackgroundTaskScheduler/WorkManager + expo native-file changes - fork would need from-scratch reimplementation on background-actions. +b7639ab55a308c55617f0088c193052b9e0bd2b2,b7639ab55a,2026-07-29,Patrick Loser,feat: upload javascript source maps (#1924),SKIP,SKIP-CI,-,-,-,src/components/AppErrorBoundary/AppErrorBoundary.tsx,Dev-tooling/CI. +ecc5b84a64ac1aa841583acb43d7ac8aab5a72c5,ecc5b84a64,2026-07-29,github-actions[bot],chore: release v2.1.2,SKIP,SKIP-RELEASE,-,-,-,package.json,Version bump; fork has own numbering (v2.1.3). +eb12bdfd623bd28c2c3f88f585fefca9352711a4,eb12bdfd62,2026-07-29,Rajarshee Chatterjee,fix: prevent novel genre rendering crashes,PORT,A3,HIGH,YELLOW,MANUAL,"src/screens/novel/components/EditInfoModal.tsx,src/screens/novel/components/Info/NovelInfoComponents.tsx","Novel genre rendering crash (parseGenres helper; fork has same split("","") pattern)." +9612bf6f893d0078c566f9651405097b7f8d1f24,9612bf6f89,2026-07-29,Rajarshee Chatterjee,chore: update translations (#1928),SKIP,SKIP-I18N,-,-,-,,Crowdin sync; fork strings at strings/languages with custom keys - manual per-key diff only. +0ed7d878b4b75e1f6ebc83d0978f05cad5920014,0ed7d878b4,2026-07-29,Rajarshee Chatterjee,fix: prevent white reader drawer seam,PORT,A1,LOW,YELLOW,MANUAL-trivial,src/screens/reader/ReaderScreen.tsx,White reader drawer seam; tiny hunk in fork-critical ReaderScreen. +5f45e4c7f2d4f77da3b73bdfdfcdb5b477fd6849,5f45e4c7f2,2026-07-28,Rajarshee Chatterjee,fix: recover interrupted chapter migrations,SKIP,SKIP-DRIZZLE,-,-,-,src/database/db.ts,Drizzle migration SQL; fork MigrationRunner already covers. +ca24671a2f847c5722009b6b8451e9c54d3a2c12,ca24671a2f,2026-07-28,Rajarshee Chatterjee,fix: support fork-safe preview builds,SKIP,SKIP-CI,-,-,-,,CI/preview flavor infra (fork has no preview variant). +ee3a4f20bc41ccf9e3f33ce37e7191cd5c7e6a17,ee3a4f20bc,2026-07-28,Rajarshee Chatterjee,fix: preserve default category after reordering (closes #1922),PORT,A2,HIGH,YELLOW,MANUAL,"src/database/queries/__tests__/CategoryQueries.test.ts,src/database/queries/__tests__/NovelQueries.test.ts,src/database/queries/CategoryQueries.ts,src/database/queries/NovelQueries.ts,src/screens/Categories/CategoriesScreen.tsx",Preserve default category after reorder (built-in IDs + positional sort); fork uses WHERE sort=1 in 4 places. +0c3542836c847c0e824e473c06394bd5219d6329,0c3542836c,2026-07-28,Rajarshee Chatterjee,fix: synchronize library updates and optimize import refreshes,PORT,A3,MEDIUM,YELLOW,MANUAL,"src/database/queries/ChapterQueries.ts,src/screens/library/hooks/useLibrary.ts",Library sync + import refresh (julianday trigger + lastUpdatedAt sort; fork triggers identical pre-fix). +f71c9c859471968684baef6e7f3da55a9d2f5821,f71c9c8594,2026-07-28,Patrick Loser,fix: ignore discord notifications outside the main repository (#1923),SKIP,SKIP-CI,-,-,-,,CI/notification infra. +f53546cc01d76b8b3f73ed4a3b88b374f775d746,f53546cc01,2026-07-28,github-actions[bot],chore: release v2.1.1,SKIP,SKIP-RELEASE,-,-,-,package.json,Version bump; fork has own numbering (v2.1.3). +594b078fad3308c284b3b9cd8484056b40d3c821,594b078fad,2026-07-28,Rajarshee Chatterjee,fix: harden database migration recovery,SKIP,SKIP-DRIZZLE,-,-,-,src/database/db.ts,Drizzle migration SQL; fork MigrationRunner already covers. +ac1b2f5ce446ae90db315be11d1494bc49539f40,ac1b2f5ce4,2026-07-27,Rajarshee Chatterjee,feat: add durable completion notifications,SKIP,SKIP-NATIVE,-,-,-,,Durable completion notifications built on upstream native-background-tasks/WorkManager - fork has no such infra. +648d21062f8812ae2a8526954a374424c0dede3c,648d21062f,2026-07-27,Rajarshee Chatterjee,feat: add parallel task queues with per-plugin download limits,SKIP,SKIP-NATIVE,-,-,-,src/services/migrate/migrateNovel.ts,Parallel task queues + per-plugin download limits depend on upstream BackgroundTaskDao/scheduler - would need reimplementation on ServiceManager queue. +89d715ce6bc22ea15885ff86b3359033f3253b4c,89d715ce6b,2026-07-27,Rajarshee Chatterjee,docs: replace beta badge with build preview status,SKIP,SKIP-DOCS,-,-,-,README.md,Documentation only. +bd527a02efa9d642b7168e432bfb965a0ae85f51,bd527a02ef,2026-07-27,Rajarshee Chatterjee,"feat: improve backup selection, notifications, and plugin restore",PORT,D-optional,HIGH,RED,MANUAL,"src/components/Checkbox/Checkbox.tsx,src/screens/settings/SettingsBackupScreen/Components/GoogleDriveModal.tsx,src/screens/settings/SettingsBackupScreen/Components/SelfHostModal.tsx,src/screens/settings/SettingsBackupScreen/index.tsx,src/services/backup/local/index.ts,src/services/backup/types.ts,src/services/backup/utils.ts","Backup selection/notifications/plugin-restore overhaul (+2021/-249, 31 files). Port only selection/options/plugin-restore slices as dedicated effort." +11117c1b5404820868683522242296d4f61d7642,11117c1b54,2026-07-27,Rajarshee Chatterjee,feat: add github-style embed for nightly notifications,SKIP,SKIP-CI,-,-,-,,GitHub-style embed for nightly notifications - CI workflow only. +ba25d5b8fe3af0b006ad09293a499b58cbee1479,ba25d5b8fe,2026-07-27,Patrick Loser,feat: add rozenite devtools integration (#1912),SKIP,SKIP-INFRA,-,-,-,"App.tsx,package.json,pnpm-lock.yaml",op-sqlite adapter + metro config; fork uses expo-sqlite. +31cb4b994a332bd6f99864ba792aaa988bf4720b,31cb4b994a,2026-07-27,Rajarshee Chatterjee,fix: align bottom navigation with material 3 specs,PORT,A1,LOW,YELLOW,MANUAL-trivial,src/components/BottomTabBar/index.tsx,Bottom nav M3 padding. Fork BottomTabBar rewritten (getStyles + scaled padding.xs) so upstream hunk fails git apply --check - manual port (review 2026-08-04). +72ef618af1bdb6a40783ab119649e302a89e12d8,72ef618af1,2026-07-27,Rajarshee Chatterjee,fix: preserve existing data during novel counter migration,SKIP,SKIP-DRIZZLE,-,-,-,,Drizzle migration SQL; fork MigrationRunner already covers. +936fd223bbde71c3e0496f22ea4f28c9714d7987,936fd223bb,2026-07-27,Rajarshee Chatterjee,feat: add discord notifications for nightly builds,SKIP,SKIP-CI,-,-,-,,CI/discord notification infra. +6d26438d2dd5b7faf21786bc390ae83b43aca6d5,6d26438d2d,2026-07-27,Rajarshee Chatterjee,test: update expected database migrations,SKIP,SKIP-DRIZZLE,-,-,-,AGENTS.md,Drizzle migration SQL; fork MigrationRunner already covers. +5eb5a55c4c654c046f984f1e324b36587389dfa8,5eb5a55c4c,2026-07-27,Rajarshee Chatterjee,fix: repair skipped novel counter migrations,SKIP,SKIP-DRIZZLE,-,-,-,,Drizzle migration SQL; fork MigrationRunner already covers. +6cd86d1422de47dc8e08c53e645c678b39ed66cc,6cd86d1422,2026-07-27,Rajarshee Chatterjee,chore: update translations (#1906),SKIP,SKIP-I18N,-,-,-,,Crowdin sync; fork strings at strings/languages with custom keys - manual per-key diff only. +9783c4d5e3a0f7e5736d1fa50a8dd360a760f76c,9783c4d5e3,2026-07-26,Patrick Loser,fix: normalize repository back navigation (#1905),PORT,A1,LOW,YELLOW,MANUAL-trivial,src/screens/settings/SettingsRepositoryScreen/SettingsRepositoryScreen.tsx,Reclassified PORT-A1 per review 2026-08-04: small safe deletion (remove useBackHandler + popToTop); group1 subagent had recommended porting; fork carries identical pre-fix code. +87f602a1fdaaf4456b1947a6097b5a7e65caa1a3,87f602a1fd,2026-07-26,github-actions[bot],chore: release v2.1.0,SKIP,SKIP-RELEASE,-,-,-,package.json,Version bump; fork has own numbering (v2.1.3). +3d32ddd6e933d5b5208f7813b99b8755e700d483,3d32ddd6e9,2026-07-26,Rajarshee Chatterjee,fix: improve release selection and apk verification,SKIP,SKIP-RELEASE,-,-,-,,Version bump; fork has own numbering (v2.1.3). +7ddd934c2b834ba8fbed6969dab89728e3316251,7ddd934c2b,2026-07-26,Rajarshee Chatterjee,fix: handle missing tracker client ids,PORT,A3,MEDIUM,YELLOW,MANUAL,,Handle missing tracker client IDs (guard clauses in aniList.ts/myAnimeList.ts only). +b53c91f748581227d63a28d28d33de87c1cbb296,b53c91f748,2026-07-26,Rajarshee Chatterjee,chore: separate jest and type check workflows,SKIP,SKIP-CI,-,-,-,package.json,Fork has separate CI. +9d106a371620a513f1f4cf57f511936866a77fd2,9d106a3716,2026-07-26,Rajarshee Chatterjee,feat: add semantic version bump selection,SKIP,SKIP-CI,-,-,-,,Release automation. +8b0a8dc4c07b6d77532ef9b070bbce288afd92d4,8b0a8dc4c0,2026-07-26,Patrick Loser,chore: update husky pre-commit and pre-push hooks,SKIP,SKIP-INFRA,-,-,-,package.json,Fork has own hooks; bundled volume-module change conflicts. +72bfcad03469440c1e9cf5ff88b5ff97a544c45e,72bfcad034,2026-07-26,Patrick Loser,fix: resolve type and lint issues,SKIP,SKIP-OTHER,-,-,-,"eslint.config.js,src/components/ErrorView/ErrorView.tsx,src/database/queries/__tests__/NovelQueries.test.ts,src/database/queries/__tests__/StatsQueries.test.ts,src/hooks/common/useGithubUpdateChecker.ts,src/navigators/types/index.ts,src/screens/browse/discover/AniListTopNovels.tsx,src/screens/browse/discover/MalTopNovels.tsx,src/screens/browse/migration/MigrationSourceItem.tsx,src/screens/browse/SourceNovels.tsx,src/screens/BrowseSourceScreen/useBrowseSource.ts,src/screens/GlobalSearchScreen/GlobalSearchScreen.tsx,src/screens/library/hooks/useLibrary.ts,src/screens/more/About.tsx,src/screens/more/components/MoreHeader.tsx,src/screens/more/MoreScreen.tsx,src/screens/more/TaskQueueScreen.tsx,src/screens/reader/ChapterContext.tsx,src/screens/reader/components/ChapterDrawer/index.tsx,src/screens/reader/components/WebViewReader.tsx,src/screens/reader/ReaderScreen.tsx,src/screens/settings/SettingsBackupScreen/Components/GoogleDriveModal.tsx,src/screens/settings/SettingsBackupScreen/index.tsx,src/screens/settings/SettingsReaderScreen/SettingsReaderScreen.tsx,src/screens/settings/SettingsRepositoryScreen/components/RepositoryCard.tsx,src/screens/settings/SettingsTrackerScreen.tsx,src/screens/WebviewScreen/components/Appbar.tsx,src/screens/WebviewScreen/components/Menu.tsx,src/screens/WebviewScreen/WebviewScreen.tsx","TRAP: lint/type cleanup touching WebViewReader/ReaderScreen/ChapterContext/useLibrary - zero value, maximal conflict surface. NEVER cherry-pick." +345d084eba5f1862c37d15c3790ce36610116d70,345d084eba,2026-07-26,Rajarshee Chatterjee,feat: add chapter numbers to epub titles (closes #1405),PORT,B,HIGH,YELLOW,MANUAL,"src/hooks/persisted/useSettings.ts,src/screens/novel/components/ExportEpubModal.tsx,src/screens/novel/components/ExportNovelAsEpubButton.tsx",Chapter numbers in EPUB titles (setting + title composition; adapt to @cd-z/react-native-epub-creator). +10b6042a9c671f47ba33f7810c47935a54706f40,10b6042a9c,2026-07-26,Rajarshee Chatterjee,feat: add epub export progress notifications,SKIP,SKIP-INFRA,-,-,-,"src/screens/novel/components/ExportEpubModal.tsx,src/screens/novel/components/ExportNovelAsEpubButton.tsx",Expo infra commit. +bd7161172551568e579e60ece7072f8c5a05fa65,bd71611725,2026-07-26,Rajarshee Chatterjee,feat: improve chapter drawer and webview app bar,SKIP,SKIP-OTHER,-,-,-,"src/screens/reader/components/ChapterDrawer/index.tsx,src/screens/reader/ReaderScreen.tsx,src/screens/WebviewScreen/components/Appbar.tsx",No clear user value / overlaps fork-critical files or infra. See subagent report. +8f53550d2c6ec75b8d9cefe0142218aaec26f726,8f53550d2c,2026-07-26,Rajarshee Chatterjee,fix: restore tracker search requests,PORT,A3,HIGH,YELLOW,MANUAL,src/screens/novel/components/Tracker/TrackSearchDialog.tsx,Restore tracker search requests (scheduleSearch bypass + stale searchText; fork shares broken flow). +52914b978df3267342b9cd3d18270bf19ae5a7cb,52914b978d,2026-07-26,Rajarshee Chatterjee,fix: migrate volume button events to expo listener api,SKIP,ALREADY-HAVE,-,-,-,src/screens/reader/hooks/useChapter.ts,Fork has own volume-button listener wiring. +ff12759a66ad68791d7b8270579b8f7d020e73f5,ff12759a66,2026-07-26,Rajarshee Chatterjee,ci: run preview builds daily when missing,SKIP,SKIP-CI,-,-,-,,CI/preview flavor infra (fork has no preview variant). +e62868b82ccd1784ad83cd00d338c47d2818df94,e62868b82c,2026-07-26,Rajarshee Chatterjee,feat: automate draft android releases,SKIP,SKIP-RELEASE,-,-,-,".gitignore,package.json,src/hooks/common/useGithubUpdateChecker.ts",Version bump; fork has own numbering (v2.1.3). +97be48dfe17b6a5261284cc3052ab5dcbab8d20d,97be48dfe1,2026-07-26,Rajarshee Chatterjee,feat: add migration review options,PORT,B-stretch,MEDIUM,YELLOW,MANUAL,"AGENTS.md,src/screens/browse/migration/MigrationNovelList.tsx,src/services/migrate/migrateNovel.ts",Migration review options (cover/metadata/redownload + review dialog). +cbe7d70e2df349db7ebf36a448e1f84b3903fb78,cbe7d70e2d,2026-07-26,Rajarshee Chatterjee,fix: restore collapsed tts button interaction,SKIP,ALREADY-HAVE,-,-,-,,Fork index.js already guards TTS controller element. +6548e9a6f8efc0bb21e46af005072d7599b326e9,6548e9a6f8,2026-07-26,Rajarshee Chatterjee,fix: improve paged reader layout and performance,SKIP,SKIP-OTHER,-,-,-,src/screens/reader/ReaderScreen.tsx,No clear user value / overlaps fork-critical files or infra. See subagent report. +138fdff2e57e55e02f9775d11dfb47482701f5c5,138fdff2e5,2026-07-26,Rajarshee Chatterjee,fix: prevent duplicate webview bottom insets,PORT,B-stretch,MEDIUM,YELLOW,MANUAL,src/screens/WebviewScreen/WebviewScreen.tsx,Prevent duplicate WebView bottom insets (fork likely same bug). +f1dd4b77a891e501b9345e5d0fe81b6405bce450,f1dd4b77a8,2026-07-26,Rajarshee Chatterjee,feat: improve empty chapter reporting,SKIP,SKIP-OTHER,-,-,-,"src/screens/reader/components/WebViewReader.tsx,src/screens/reader/utils/sanitizeChapterText.ts",No clear user value / overlaps fork-critical files or infra. See subagent report. +9b356ffd17c28d7f37285157f3f34488af67a266,9b356ffd17,2026-07-26,Rajarshee Chatterjee,fix: preserve client ids and improve environment generation,SKIP,SKIP-OTHER,-,-,-,,No clear user value / overlaps fork-critical files or infra. See subagent report. +5cf7e15d7631f3ada56d4cb979694214745bd10d,5cf7e15d76,2026-07-26,Rajarshee Chatterjee,feat: add reader setting descriptions,SKIP,SKIP-OTHER,-,-,-,"src/screens/settings/SettingsReaderScreen/tabs/AccessibilityTab.tsx,src/screens/settings/SettingsReaderScreen/tabs/NavigationTab.tsx",No clear user value / overlaps fork-critical files or infra. See subagent report. +6a7e90c179b735521aa9d778e68f1727607e8ac5,6a7e90c179,2026-07-26,Rajarshee Chatterjee,fix: align advanced reader tabs and localize actions,SKIP,SKIP-OTHER,-,-,-,src/screens/settings/SettingsReaderScreen/tabs/AdvancedTab.tsx,No clear user value / overlaps fork-critical files or infra. See subagent report. +99c31d56bb45c6e04fe96cff3f14c3ca55fccba4,99c31d56bb,2026-07-26,Rajarshee Chatterjee,fix: use md3 outline variant for tab borders,PORT,C-optional,LOW,YELLOW,MANUAL,"src/screens/browse/BrowseScreen.tsx,src/screens/library/components/LibraryBottomSheet/LibraryBottomSheet.tsx,src/screens/library/LibraryScreen.tsx,src/screens/novel/components/NovelBottomSheet.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderBottomSheet.tsx,src/screens/settings/SettingsReaderScreen/SettingsReaderScreen.tsx",MD3 outline variant for tab borders. +4d98247980c21f892df6cf0da19ecad8a6f0d136,4d98247980,2026-07-26,Rajarshee Chatterjee,feat: add android dns over https support,SKIP,ALREADY-HAVE,-,-,-,src/screens/settings/SettingsAdvancedScreen.tsx,Fork shipped own DoH (DoHManagerModule.kt) earlier. +164f5d259243a68f14a7e8a069b2e2cfc42aab84,164f5d2592,2026-07-25,Rajarshee Chatterjee,feat: add automatic library update scheduling,PORT,D-stretch,HIGH,YELLOW,MANUAL,src/hooks/persisted/useSettings.ts,Automatic library update scheduling - needs native scheduler on fork ServiceManager. +89cddf9e3d07d3f7a7c7cb71e8cc8e5e4e96fcc3,89cddf9e3d,2026-07-25,Rajarshee Chatterjee,fix: expand discover row ripple,SKIP,SKIP-OTHER,-,-,-,src/screens/browse/discover/DiscoverCard.tsx,No clear user value / overlaps fork-critical files or infra. See subagent report. +eac6ad584e85d4ed72cf76fafcde1c4d0551687d,eac6ad584e,2026-07-25,Rajarshee Chatterjee,feat: improve novel app bar and epub export ux,PORT,B-stretch,MEDIUM,YELLOW,MANUAL,"src/screens/novel/components/ExportEpubModal.tsx,src/screens/novel/components/JumpToChapterModal.tsx,src/screens/novel/components/NovelAppbar.tsx,src/screens/novel/components/NovelScreenList.tsx,src/screens/novel/NovelScreen.tsx",Novel app bar + EPUB export UX polish. +93fe04c28cc4776a96f86aa157b729ddf3c218c3,93fe04c28c,2026-07-25,Rajarshee Chatterjee,feat: polish reader controls and navigation,SKIP,SKIP-OTHER,-,-,-,"src/components/Common/ToggleButton.tsx,src/components/SegmentedControl/SegmentedControl.tsx,src/database/queries/ChapterQueries.ts,src/navigators/MoreStack.tsx,src/screens/browse/discover/DiscoverCard.tsx,src/screens/more/DownloadsScreen.tsx,src/screens/reader/components/ReaderAppbar.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderBottomSheet.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderFontPicker.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderSheetPreferenceItem.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderTextAlignSelector.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderThemeSelector.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderValueChange.tsx,src/screens/reader/components/ReaderBottomSheet/TextSizeSlider.tsx,src/screens/reader/components/ReaderBottomSheet/TTSTab.tsx,src/screens/reader/components/ReaderFooter.tsx,src/screens/reader/ReaderScreen.tsx,src/screens/settings/SettingsTrackerScreen.tsx",Broad reader-controls polish incl. Drizzle ChapterQueries limit + every fork-critical file. Only selective sub-fixes portable - see group2 report. +c9358b3a9fc49ad6a98ac0293da46bf9eab39bf8,c9358b3a9f,2026-07-25,Rajarshee Chatterjee,feat: update android branding assets,SKIP,SKIP-CI,-,-,-,,Binary assets; cherry-picks but inert. +67d2c7c01b101d83d0033aad2c0a99247f04620e,67d2c7c01b,2026-07-25,Rajarshee Chatterjee,feat: add collapsible tts controls and stable notification progress,SKIP,ALREADY-HAVE,-,-,-,,Collapsible TTS controls; fork has own TTS UI + stable progress. +c30441b9493d5e75fbe82a608b034c6239fa493f,c30441b949,2026-07-25,Rajarshee Chatterjee,feat: add smart library update filters,PORT,B-stretch,MEDIUM,YELLOW,MANUAL,src/hooks/persisted/useSettings.ts,Smart library update filters (skip read/completed/unstarted). +6d2a9f8e1530604860375a9be3abb92b4502d01b,6d2a9f8e15,2026-07-25,Rajarshee Chatterjee,fix: prevent slider flicker after release,PORT,C,LOW,GREEN,MANUAL,,Slider flicker fix - only after MD3 Slider ported. +799845426c8f2e651de508d471d8319add41d2cc,799845426c,2026-07-25,Rajarshee Chatterjee,feat: add parallel library updates across sources,PORT,B,MEDIUM,YELLOW,MANUAL,,Parallel library updates (group by pluginId + Promise.all concurrency 3; fork updates/index.ts is sequential sleep(1000)). +8ac628b3deda367f68af93f9f43d7a1da8c80f9d,8ac628b3de,2026-07-25,Rajarshee Chatterjee,fix: improve history removal flow.,PORT,B-stretch,MEDIUM,YELLOW,MANUAL,"src/database/queries/__tests__/HistoryQueries.test.ts,src/screens/history/components/HistoryCard/HistoryCard.tsx,src/screens/history/HistoryScreen.tsx",Per-novel history removal + confirm dialog. +d02291c22c079e5faee9817b00c7bd87ed7f47da,d02291c22c,2026-07-25,Rajarshee Chatterjee,feat: remember last used library category,PORT,B-stretch,MEDIUM,YELLOW,MANUAL,"src/hooks/persisted/useSettings.ts,src/screens/library/LibraryScreen.tsx",Remember last used library category. +7af0911f5b17e1c606dbf60e0f1d1b8863c78221,7af0911f5b,2026-07-25,Rajarshee Chatterjee,feat: add high-resolution android flavor assets,SKIP,SKIP-CI,-,-,-,,Binary flavor assets. +098782d6aa9a188bf007402155bd66e3dbc14523,098782d6aa,2026-07-25,Rajarshee Chatterjee,refactor: simplify settings layout and theme selection,PORT,C-optional,LOW,YELLOW,MANUAL,"package.json,pnpm-lock.yaml,src/components/List/List.tsx,src/components/ThemePicker/ThemePicker.tsx,src/screens/onboarding/ThemeSelectionStep.tsx,src/screens/settings/SettingsAppearanceScreen/SettingsAppearanceScreen.tsx,src/screens/settings/SettingsGeneralScreen/SettingsGeneralScreen.tsx,src/screens/settings/SettingsTrackerScreen.tsx",Simplify settings layout + theme selection (removes react-native-theme-switch-animation). +732628c40bb3f1afbbb0336577f9a56f97ef9115,732628c40b,2026-07-25,Rajarshee Chatterjee,feat: add date format and relative timestamp settings.,PORT,B-stretch,MEDIUM,YELLOW,MANUAL,"src/hooks/persisted/useSettings.ts,src/screens/history/HistoryScreen.tsx,src/screens/more/DownloadsScreen.tsx,src/screens/novel/components/ChapterItem.tsx,src/screens/novel/components/NovelScreenList.tsx,src/screens/settings/SettingsAppearanceScreen/SettingsAppearanceScreen.tsx,src/screens/settings/SettingsBackupScreen/Components/GoogleDriveModal.tsx,src/screens/updates/UpdatesScreen.tsx",Date format + relative timestamp settings. +5e62c36a9b0c0d7ca562383f2d9938e722216f00,5e62c36a9b,2026-07-25,Rajarshee Chatterjee,feat: add library settings and category-based global updates,PORT,D-optional,MEDIUM,RED,MANUAL-rewrite,"src/database/queries/__tests__/NovelQueries.test.ts,src/database/queries/NovelQueries.ts,src/hooks/persisted/useSettings.ts,src/navigators/MoreStack.tsx,src/navigators/types/index.ts,src/screens/settings/SettingsGeneralScreen/SettingsGeneralScreen.tsx,src/screens/settings/SettingsLibraryScreen/DefaultCategoryDialog.tsx,src/screens/settings/SettingsScreen.tsx",Library settings + category-based global updates; completes fork TODO-stubbed DefaultCategoryDialog but needs Drizzle->SQL rewrite. +3d102a52a7d780b096afd78c6d5214e46b670b47,3d102a52a7,2026-07-25,Rajarshee Chatterjee,perf(reader): cut chapter open latency and reading-time re-renders (#1899),PORT,D,HIGH,RED,MANUAL-rewrite,"src/hooks/persisted/useSettings.ts,src/screens/reader/ChapterContext.tsx,src/screens/reader/components/ChapterDrawer/index.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderBottomSheet.tsx,src/screens/reader/components/ReaderFooter.tsx,src/screens/reader/components/WebViewReader.tsx,src/screens/reader/hooks/useChapter.ts,src/screens/reader/ReaderScreen.tsx,src/screens/reader/utils/sanitizeChapterText.ts,src/screens/WebviewScreen/WebviewScreen.tsx","Reader perf sprint (#1899) - port TECHNIQUES (deferred fetch, HTML cache, lazy mount, coalesced scroll) not code." +4dcf21303715793087ea3bd49a9d510943e30944,4dcf213037,2026-07-25,Rajarshee Chatterjee,fix: restore reader changes reverted during expo migration,SKIP,SKIP-OTHER,-,-,-,,Restores upstream assets/reader/js/search.js (442 lines) - fork has no in-reader search; feature = 71da09ed. Skip. +4de82472ee70fd0c36114f0ee2e213ff6bf81099,4de82472ee,2026-07-25,Rajarshee Chatterjee,fix: resolve require cycles and legendlist render updates,SKIP,SKIP-DEP,-,-,-,"pnpm-lock.yaml,pnpm-workspace.yaml,src/components/SearchbarV2/SearchbarV2.tsx,src/screens/settings/SettingsRepositoryScreen/SettingsRepositoryScreen.tsx,src/services/backup/utils.ts",LegendList v3 patch + Drizzle manager; version mismatch. +58db0352e6359d4b1c69391d260105abbe4ede60,58db0352e6,2026-07-25,Rajarshee Chatterjee,fix: restore metro reader asset middleware,SKIP,ALREADY-HAVE,-,-,-,,Fork metro.config.cjs already serves reader assets. +74808f086e5e3fbd479138681d700814c3370386,74808f086e,2026-07-24,Rajarshee Chatterjee,feat: replace legacy media controls with nitro-powered playback (#1896),SKIP,ALREADY-HAVE,-,-,-,"babel.config.js,package.json,src/hooks/persisted/useSettings.ts,src/screens/reader/components/ReaderBottomSheet/TTSTab.tsx,src/screens/reader/components/WebViewReader.tsx,src/screens/reader/hooks/useChapter.ts,src/screens/reader/ReaderScreen.tsx,src/screens/settings/SettingsReaderScreen/Modals/VoicePickerModal.tsx,src/screens/settings/SettingsReaderScreen/SettingsReaderScreen.tsx,tsconfig.json",Nitro media controls; fork has own 5-button ttsNotification.ts. +b96351ea8117f4a1e464b0a7ab1820a557172c92,b96351ea81,2026-07-24,amir-rezaei,docs: fix contraction and missing article in `testing.md` (#1895),SKIP,SKIP-DOCS,-,-,-,,Documentation only. +eb8dc6df368ba7628178d6481294fca1b30f9019,eb8dc6df36,2026-07-24,amir-rezaei,"docs: fix ""withoud"" typo in `contributing-nix.md` (#1894)",SKIP,SKIP-DOCS,-,-,-,CONTRIBUTING-NIX.md,Documentation only. +0c8546188eb33a0ac3be46596503cd38e4fb4d75,0c8546188e,2026-07-24,Rajarshee Chatterjee,feat: add md3 slider component.,PORT,C,MEDIUM,YELLOW,MANUAL,"package.json,pnpm-lock.yaml,src/screens/reader/components/ReaderBottomSheet/ReaderValueChange.tsx,src/screens/reader/components/ReaderBottomSheet/TextSizeSlider.tsx,src/screens/reader/components/ReaderBottomSheet/TTSTab.tsx,src/screens/settings/SettingsReaderScreen/ReaderTextSize.tsx",MD3 Slider component (replaces @react-native-community/slider; coordinate package.json). +e9f6bdaa203890fb7d69b261f31afc431fba429c,e9f6bdaa20,2026-07-24,Rajarshee Chatterjee,fix: modernize menu styling and outside-tap dismissal,PORT,C,MEDIUM,YELLOW,MANUAL,src/components/Menu/index.tsx,Menu outside-tap dismissal modernization. +3d34658d0faee3f1a1dce6ed77f745804851c4c0,3d34658d0f,2026-07-24,Rajarshee Chatterjee,feat: add material 3 top tab indicators,PORT,C,MEDIUM,YELLOW,MANUAL,"src/screens/browse/BrowseScreen.tsx,src/screens/library/components/LibraryBottomSheet/LibraryBottomSheet.tsx,src/screens/library/LibraryScreen.tsx,src/screens/novel/components/NovelBottomSheet.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderBottomSheet.tsx,src/screens/settings/SettingsReaderScreen/components/TabBar.tsx,src/screens/settings/SettingsReaderScreen/SettingsReaderScreen.tsx",M3 top tab indicators (new TopTabBar). +d44ce7412ac512b392d35e33d6dc9d98587a2350,d44ce7412a,2026-07-24,amir-rezaei,"docs: fix trailing period, organization expansion, and comma in `contributing.md` (#1893)",SKIP,SKIP-DOCS,-,-,-,,Documentation only. +17c891e1334409fa29d4a365b9ba46014b81b591,17c891e133,2026-07-24,Rajarshee Chatterjee,refactor: standardize bottom sheet ux and styling,PORT,C,MEDIUM,YELLOW,MANUAL,"src/components/BottomSheet/BottomSheet.tsx,src/hooks/persisted/useTheme.ts,src/screens/BrowseSourceScreen/components/FilterBottomSheet.tsx,src/screens/library/components/LibraryBottomSheet/LibraryBottomSheet.tsx,src/screens/library/LibraryScreen.tsx,src/screens/novel/components/NovelBottomSheet.tsx,src/screens/novel/components/PageNavigationBottomSheet.tsx,src/screens/novel/components/Tracker/TrackSheet.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderBottomSheet.tsx,src/screens/settings/SettingsReaderScreen/SettingsReaderScreen.tsx",Standardize bottom sheet UX/styling (surfaceContainerLow in useTheme). +9805da63cf23d58f0561b9335e0a07c1789e9a82,9805da63cf,2026-07-24,Rajarshee Chatterjee,refactor: optimize novel screen performance and architecture,PORT,D-optional,HIGH,RED,MANUAL-rewrite,"src/screens/novel/components/EditInfoModal.tsx,src/screens/novel/components/ExportEpubModal.tsx,src/screens/novel/components/ExportNovelAsEpubButton.tsx,src/screens/novel/components/Info/NovelInfoComponents.tsx,src/screens/novel/components/Info/NovelInfoHeader.tsx,src/screens/novel/components/JumpToChapterModal.tsx,src/screens/novel/components/NovelAppbar.tsx,src/screens/novel/components/NovelBottomSheet.tsx,src/screens/novel/components/NovelScreenButtonGroup/NovelScreenButtonGroup.tsx,src/screens/novel/components/NovelScreenList.tsx,src/screens/novel/components/SetCategoriesModal.tsx,src/screens/novel/components/Tracker/ScoreSelectors.tsx,src/screens/novel/components/Tracker/TrackSheet.tsx,src/screens/novel/NovelScreen.tsx,src/services/migrate/migrateNovel.ts","Novel screen perf/architecture (zustand store upstream) - re-implement perf goals (skeletons, memo, lazy modals) on fork useNovel stack." +afc20277e0f8a8d4b49d1ebd1c7689c599ba2e06,afc20277e0,2026-07-24,Rajarshee Chatterjee,fix: restore android build types for ci,SKIP,SKIP-CI,-,-,-,".gitignore,package.json",rock-CLI CI/build infra. +14061d207b77d8930c4d15af135189ccb4f4bd9e,14061d207b,2026-07-24,amir-rezaei,"docs: fix title case headings, github capitalization, and e.g. formatting in `contributing.md` (#1892)",SKIP,SKIP-DOCS,-,-,-,,Documentation only. +916374b5df3cfa3b09d464109340ad81bdbef457,916374b5df,2026-07-24,Rajarshee Chatterjee,refactor: reorganize repository structure,SKIP,SKIP-INFRA,-,-,-,"babel.config.js,package.json,scripts/generate-string-types.cjs,src/components/ColorPickerModal/ColorPickerModal.tsx,src/components/ConfirmationDialog/ConfirmationDialog.tsx,src/components/NewUpdateDialog.tsx,src/components/NovelCover.tsx,src/components/SearchbarV2/SearchbarV2.tsx,src/database/queries/CategoryQueries.ts,src/database/queries/ChapterQueries.ts,src/database/queries/NovelQueries.ts,src/navigators/BottomNavigator.tsx,src/screens/browse/BrowseScreen.tsx,src/screens/browse/discover/DiscoverCard.tsx,src/screens/browse/migration/Migration.tsx,src/screens/browse/migration/MigrationNovelList.tsx,src/screens/browse/settings/modals/ConcurrentSearchesModal.tsx,src/screens/browse/SourceNovels.tsx,src/screens/BrowseSourceScreen/components/FilterBottomSheet.tsx,src/screens/Categories/CategoriesScreen.tsx,src/screens/Categories/components/AddCategoryModal.tsx,src/screens/Categories/components/DeleteCategoryModal.tsx,src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx,src/screens/GlobalSearchScreen/GlobalSearchScreen.tsx,src/screens/history/components/ClearHistoryDialog.tsx,src/screens/history/components/HistoryCard/HistoryCard.tsx,src/screens/history/HistoryScreen.tsx,src/screens/library/components/LibraryBottomSheet/LibraryBottomSheet.tsx,src/screens/library/LibraryScreen.tsx,src/screens/more/About.tsx,src/screens/more/components/RemoveDownloadsDialog.tsx,src/screens/more/DownloadsScreen.tsx,src/screens/more/MoreScreen.tsx,src/screens/more/TaskQueueScreen.tsx,src/screens/novel/components/Chapter/ChapterDownloadButtons.tsx,src/screens/novel/components/ChapterItem.tsx,src/screens/novel/components/DownloadCustomChapterModal.tsx,src/screens/novel/components/EditInfoModal.tsx,src/screens/novel/components/ExportEpubModal.tsx,src/screens/novel/components/ExportNovelAsEpubButton.tsx,src/screens/novel/components/Info/NovelInfoComponents.tsx,src/screens/novel/components/Info/NovelInfoHeader.tsx,src/screens/novel/components/JumpToChapterModal.tsx,src/screens/novel/components/NovelAppbar.tsx,src/screens/novel/components/NovelBottomSheet.tsx,src/screens/novel/components/NovelScreenButtonGroup/NovelScreenButtonGroup.tsx,src/screens/novel/components/NovelScreenList.tsx,src/screens/novel/components/NovelSummary/NovelSummary.tsx,src/screens/novel/components/SetCategoriesModal.tsx,src/screens/novel/components/Tracker/TrackSearchDialog.tsx,src/screens/novel/NovelScreen.tsx,src/screens/onboarding/OnboardingScreen.tsx,src/screens/onboarding/ThemeSelectionStep.tsx,src/screens/reader/components/ChapterDrawer/index.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderBottomSheet.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderFontPicker.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderTextAlignSelector.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderThemeSelector.tsx,src/screens/reader/components/ReaderBottomSheet/TextSizeSlider.tsx,src/screens/reader/components/ReaderBottomSheet/TTSTab.tsx,src/screens/reader/components/WebViewReader.tsx,src/screens/reader/hooks/useChapter.ts,src/screens/reader/ReaderScreen.tsx,src/screens/reader/utils/sanitizeChapterText.ts,src/screens/settings/components/ConnectionModal.tsx,src/screens/settings/SettingsAdvancedScreen.tsx,src/screens/settings/SettingsAppearanceScreen/LanguagePickerModal.tsx,src/screens/settings/SettingsAppearanceScreen/SettingsAppearanceScreen.tsx,src/screens/settings/SettingsBackupScreen/Components/GoogleDriveModal.tsx,src/screens/settings/SettingsBackupScreen/Components/SelfHostModal.tsx,src/screens/settings/SettingsBackupScreen/index.tsx,src/screens/settings/SettingsGeneralScreen/modals/DisplayModeModal.tsx,src/screens/settings/SettingsGeneralScreen/modals/GridSizeModal.tsx,src/screens/settings/SettingsGeneralScreen/modals/NovelBadgesModal.tsx,src/screens/settings/SettingsGeneralScreen/modals/NovelSortModal.tsx,src/screens/settings/SettingsGeneralScreen/SettingsGeneralScreen.tsx,src/screens/settings/SettingsLibraryScreen/DefaultCategoryDialog.tsx,src/screens/settings/SettingsReaderScreen/Modals/FontPickerModal.tsx,src/screens/settings/SettingsReaderScreen/Modals/VoicePickerModal.tsx,src/screens/settings/SettingsReaderScreen/ReaderTextSize.tsx,src/screens/settings/SettingsReaderScreen/SettingsReaderScreen.tsx,src/screens/settings/SettingsReaderScreen/tabs/AccessibilityTab.tsx,src/screens/settings/SettingsReaderScreen/tabs/AdvancedTab.tsx,src/screens/settings/SettingsReaderScreen/tabs/DisplayTab.tsx,src/screens/settings/SettingsReaderScreen/tabs/NavigationTab.tsx,src/screens/settings/SettingsReaderScreen/tabs/ThemeTab.tsx,src/screens/settings/SettingsRepositoryScreen/components/AddRepositoryModal.tsx,src/screens/settings/SettingsRepositoryScreen/components/DeleteRepositoryModal.tsx,src/screens/settings/SettingsRepositoryScreen/components/RepositoryCard.tsx,src/screens/settings/SettingsRepositoryScreen/SettingsRepositoryScreen.tsx,src/screens/settings/SettingsScreen.tsx,src/screens/settings/SettingsTrackerScreen.tsx,src/screens/StatsScreen/StatsScreen.tsx,src/screens/updates/UpdatesScreen.tsx,src/screens/WebviewScreen/components/Menu.tsx,src/services/backup/local/index.ts,src/services/backup/utils.ts,tsconfig.json","Pure repo shuffle (strings/ -> src/i18n/); zero value, huge conflicts." +0a2a6c96ca453fd68ce42d4723c0f42c0a730519,0a2a6c96ca,2026-07-24,Rajarshee Chatterjee,feat: add variant-specific android splash screens,SKIP,SKIP-CI,-,-,-,,Flavor-specific assets; fork has no variants. +65e86100304ae27ba8049937558b4e25b49252f6,65e8610030,2026-07-24,Rajarshee Chatterjee,refactor: replace bottom navigation shift with fade,PORT,A1,LOW,GREEN,DIRECT,src/navigators/BottomNavigator.tsx,Bottom nav shift->fade animation (1 line). +1322065290f63141633c30cd3013b7ba81c12fae,1322065290,2026-07-24,Rajarshee Chatterjee,feat: add variant-specific android app icons,SKIP,SKIP-CI,-,-,-,,Flavor-specific assets. +22a1efd1e5c548a27b3ab3b8c54909a5092c1e37,22a1efd1e5,2026-07-24,Rajarshee Chatterjee,perf: optimize react native skeleton loading components,SKIP,SKIP-DEP,-,-,-,"src/components/Skeleton/Skeleton.tsx,src/screens/browse/loadingAnimation/MalLoading.tsx,src/screens/browse/loadingAnimation/TrackerLoading.tsx,src/screens/history/components/HistorySkeletonLoading.tsx,src/screens/novel/components/Info/NovelInfoHeader.tsx,src/screens/novel/components/LoadingAnimation/NovelScreenLoading.tsx,src/screens/updates/components/UpdatesSkeletonLoading.tsx",Reanimated shimmer rewrite; fork has own optimized Skeleton. +269606ebb8e352f72fbab401bd5d93ed15c1a7a2,269606ebb8,2026-07-24,Rajarshee Chatterjee,fix: resolve nitro epub import alias,SKIP,SKIP-NITRO,-,-,-,tsconfig.json,Nitro module infra; fork uses custom Kotlin/JS. NOT-FEASIBLE. +4cb1890b62eb57cc75ca0628a11be312c930ddc4,4cb1890b62,2026-07-24,Rajarshee Chatterjee,feat: add nitro epub exporter,SKIP,ALREADY-HAVE,-,-,-,".gitignore,package.json,pnpm-lock.yaml,pnpm-workspace.yaml,src/screens/novel/components/ExportEpubModal.tsx,src/screens/novel/components/ExportNovelAsEpubButton.tsx,strings/languages/en/strings.json,strings/types/index.ts",Fork exports EPUB via @cd-z/react-native-epub-creator (JS). +a421177f0cb5b74e30b869c6595ed12c9263586a,a421177f0c,2026-07-24,Rajarshee Chatterjee,fix: improve category badge layout and drag reordering,PORT,A2,MEDIUM,YELLOW,MANUAL,"src/components/IconButtonV2/IconButtonV2.tsx,src/screens/Categories/CategoriesScreen.tsx,src/screens/Categories/components/CategoryCard.tsx",Category drag-reorder keyExtractor fix (fork CategoriesScreen.tsx:94 identical bug). +a16ec876311692a6cb3da9348d09e7cf287c9fb6,a16ec87631,2026-07-24,Rajarshee Chatterjee,"perf: optimize novel covers, lists, and chapter groups",PORT,D-optional,HIGH,RED,MANUAL-rewrite,"package.json,pnpm-lock.yaml,src/components/ErrorScreenV2/ErrorScreenV2.tsx,src/components/ListView.tsx,src/components/NovelCover.tsx,src/components/NovelList.tsx,src/database/queries/ChapterQueries.ts,src/database/types/index.ts,src/plugins/types/index.ts,src/screens/browse/discover/DiscoverNovelCard/index.tsx,src/screens/browse/globalsearch/GlobalSearchNovelCover.tsx,src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx,src/screens/history/components/HistoryCard/HistoryCard.tsx,src/screens/more/DownloadsScreen.tsx,src/screens/novel/components/ChapterItem.tsx,src/screens/novel/components/Info/NovelInfoComponents.tsx,src/screens/novel/components/Tracker/TrackSearchDialog.tsx,src/screens/StatsScreen/StatsScreen.tsx,src/screens/updates/components/UpdateNovelCard.tsx,src/screens/updates/UpdatesScreen.tsx","Novel covers/lists/chapter-group perf (expo-image, virtualized lists)." +dfd620aa292fadd3b8798013701ae1bef954fe57,dfd620aa29,2026-07-23,Rajarshee Chatterjee,feat: standardize dialogs with material 3 patterns,SKIP,SKIP-OTHER,-,-,-,"src/components/ColorPickerModal/ColorPickerModal.tsx,src/components/ConfirmationDialog/ConfirmationDialog.tsx,src/components/DialogTitle/DialogTitle.tsx,src/components/NewUpdateDialog.tsx,src/hooks/persisted/useTheme.ts,src/screens/browse/migration/MigrationNovelList.tsx,src/screens/browse/settings/modals/ConcurrentSearchesModal.tsx,src/screens/Categories/components/AddCategoryModal.tsx,src/screens/Categories/components/DeleteCategoryModal.tsx,src/screens/history/components/ClearHistoryDialog.tsx,src/screens/history/HistoryScreen.tsx,src/screens/more/components/RemoveDownloadsDialog.tsx,src/screens/more/DownloadsScreen.tsx,src/screens/novel/components/DownloadCustomChapterModal.tsx,src/screens/novel/components/EditInfoModal.tsx,src/screens/novel/components/ExportEpubModal.tsx,src/screens/novel/components/JumpToChapterModal.tsx,src/screens/novel/components/SetCategoriesModal.tsx,src/screens/novel/components/Tracker/TrackSearchDialog.tsx,src/screens/reader/components/ReaderBottomSheet/TTSTab.tsx,src/screens/settings/components/ConnectionModal.tsx,src/screens/settings/SettingsAdvancedScreen.tsx,src/screens/settings/SettingsAppearanceScreen/LanguagePickerModal.tsx,src/screens/settings/SettingsBackupScreen/Components/GoogleDriveModal.tsx,src/screens/settings/SettingsBackupScreen/Components/SelfHostModal.tsx,src/screens/settings/SettingsGeneralScreen/modals/DisplayModeModal.tsx,src/screens/settings/SettingsGeneralScreen/modals/GridSizeModal.tsx,src/screens/settings/SettingsGeneralScreen/modals/NovelBadgesModal.tsx,src/screens/settings/SettingsGeneralScreen/modals/NovelSortModal.tsx,src/screens/settings/SettingsLibraryScreen/DefaultCategoryDialog.tsx,src/screens/settings/SettingsReaderScreen/Modals/FontPickerModal.tsx,src/screens/settings/SettingsReaderScreen/Modals/VoicePickerModal.tsx,src/screens/settings/SettingsReaderScreen/tabs/AdvancedTab.tsx,src/screens/settings/SettingsRepositoryScreen/components/AddRepositoryModal.tsx,src/screens/settings/SettingsRepositoryScreen/components/DeleteRepositoryModal.tsx,src/screens/settings/SettingsTrackerScreen.tsx",No clear user value / overlaps fork-critical files or infra. See subagent report. +a5a6e8326f72d00271fd01cf8d6374cc35310147,a5a6e8326f,2026-07-23,Rajarshee Chatterjee,chore: update `@legendapp/list` to 3.3.3,SKIP,SKIP-DEP,-,-,-,"package.json,pnpm-lock.yaml,src/screens/library/components/LibraryBottomSheet/LibraryBottomSheet.tsx,src/screens/novel/components/JumpToChapterModal.tsx,src/screens/novel/components/NovelDrawer.tsx,src/screens/novel/components/NovelScreenList.tsx,src/screens/novel/components/PageNavigationBottomSheet.tsx,src/screens/novel/NovelScreen.tsx,src/screens/onboarding/ThemeSelectionStep.tsx,src/screens/reader/components/ChapterDrawer/index.tsx,src/screens/settings/SettingsReaderScreen/Modals/VoicePickerModal.tsx",Patches @legendapp/list@3.3.3; fork pins ^2.0.16. +7f48a8ba8299180ffd5b07ba6314960213cd13cd,7f48a8ba82,2026-07-23,Rajarshee Chatterjee,refactor: improve plugins screen ux and code,SKIP,SKIP-OTHER,-,-,-,"src/components/IconButtonV2/IconButtonV2.tsx,src/components/SearchbarV2/SearchbarV2.tsx,src/components/Switch/Switch.tsx,src/components/Switch/SwitchItem.tsx,src/hooks/persisted/index.ts,src/navigators/BottomNavigator.tsx,src/navigators/types/index.ts,src/screens/browse/BrowseScreen.tsx,src/screens/browse/components/AvailableTab.tsx,src/screens/browse/components/DeferredPluginListItem.tsx,src/screens/browse/components/InstalledTab.tsx,src/screens/browse/components/Modals/SourceSettings.tsx,src/screens/browse/components/PluginListItem.tsx,src/screens/browse/components/PluginListItemSkeleton.tsx,src/screens/browse/discover/DiscoverCard.tsx,src/screens/browse/migration/Migration.tsx,src/screens/browse/migration/MigrationNovels.tsx,src/screens/settings/SettingsRepositoryScreen/SettingsRepositoryScreen.tsx,strings/languages/en/strings.json,strings/types/index.ts",No clear user value / overlaps fork-critical files or infra. See subagent report. +ae34d7b4ec6be28f7ad5ae25c663c7d2b3db43ef,ae34d7b4ec,2026-07-23,Rajarshee Chatterjee,feat: add dynamic preview apk metadata,SKIP,SKIP-CI,-,-,-,,CI/preview flavor infra (fork has no preview variant). +44c8e54ed80bb47cc49c2461adca66ced94b2d1c,44c8e54ed8,2026-07-23,Rajarshee Chatterjee,feat: add dynamic material theme colors (closes #1863),PORT,C,HIGH,YELLOW,MANUAL,"App.tsx,package.json,pnpm-lock.yaml,src/hooks/persisted/useTheme.ts,src/screens/onboarding/ThemeSelectionStep.tsx,src/screens/settings/SettingsAppearanceScreen/SettingsAppearanceScreen.tsx,strings/languages/en/strings.json,strings/types/index.ts",Dynamic Material You theme colors (#1863) via @pchmn/expo-material3-theme. +4a4208e33602d65ef9942d05e54a692addd87e2f,4a4208e336,2026-07-23,Ecasept,feat: add time-tracking toggle and inactivity timeout (#1890),PORT,D,MEDIUM,RED,MANUAL-rewrite,"src/hooks/persisted/useSettings.ts,src/screens/reader/components/WebViewReader.tsx,src/screens/reader/hooks/useChapter.ts,src/screens/reader/ReaderScreen.tsx,src/screens/settings/SettingsGeneralScreen/SettingsGeneralScreen.tsx,strings/languages/en/strings.json,strings/types/index.ts",Time-tracking toggle + inactivity timeout (#1890) - after 89c43c1e. +d2385b8dc2a9cd911e7567e55d6b7dc2c43f2372,d2385b8dc2,2026-07-23,CD-Z,update build.yml,SKIP,SKIP-CI,-,-,-,,Fork has separate CI. +9babe1676dd053069d127743a61bc7a8bc11256c,9babe1676d,2026-07-22,Rajarshee Chatterjee,refactor: remove react native background actions manifest customization,SKIP,SKIP-NATIVE,-,-,-,package.json,Native background-task infra (WorkManager/Expo modules); fork uses background-actions + ServiceManager. +8c38f499e4b62de02157aa8dcd1c9207978033bf,8c38f499e4,2026-07-22,Rajarshee Chatterjee,chore: update appstate test setup and `pnpm` configuration,SKIP,SKIP-CI,-,-,-,,Test infra for feature fork lacks. +89c43c1eeffd5a9980b3aaebe647f3d94252f8b8,89c43c1eef,2026-07-22,Ecasept,feat: implement time tracking and statistics (#1886),PORT,D,HIGH,RED,MANUAL-rewrite,"src/database/queries/__tests__/StatsQueries.test.ts,src/database/queries/ChapterQueries.ts,src/database/queries/StatsQueries.ts,src/database/types/index.ts,src/screens/reader/components/WebViewReader.tsx,src/screens/reader/hooks/useChapter.ts,src/screens/StatsScreen/StatsScreen.tsx,strings/languages/en/strings.json,strings/types/index.ts",Time tracking & statistics (#1886) - rewrite raw-SQL table + hook + screen (fork has no timeSpent). +c3c891cea084796b3640233826dacfc7aab6f8d6,c3c891cea0,2026-07-22,Patrick Loser,refactor: migrate to expo managed workflow (#1885),SKIP,SKIP-INFRA,-,-,-,".gitignore,android/app/build.gradle,android/app/proguard-rules.pro,android/app/src/main/AndroidManifest.xml,android/app/src/main/java/com/rajarsheechatterjee/LNReader/MainActivity.kt,android/app/src/main/java/com/rajarsheechatterjee/LNReader/MainApplication.kt,android/app/src/main/java/com/rajarsheechatterjee/NativeFile/NativeFile.kt,android/app/src/main/res/raw/loading.json,android/build.gradle,android/gradle.properties,android/gradle/wrapper/gradle-wrapper.properties,android/settings.gradle,App.tsx,babel.config.js,eslint.config.js,ios/LNReader/Images.xcassets/AppIcon.appiconset/Contents.json,ios/loading.json,package.json,pnpm-lock.yaml,pnpm-workspace.yaml,src/components/BottomTabBar/index.tsx,src/components/EmptyView/EmptyView.tsx,src/components/ErrorScreenV2/ErrorScreenV2.tsx,src/components/IconButtonV2/IconButtonV2.tsx,src/components/NovelCover.tsx,src/components/NovelList.tsx,src/components/SearchbarV2/SearchbarV2.tsx,src/database/queries/CategoryQueries.ts,src/database/queries/ChapterQueries.ts,src/database/queries/NovelQueries.ts,src/hooks/persisted/useNovel.ts,src/plugins/helpers/fetch.ts,src/screens/browse/BrowseScreen.tsx,src/screens/browse/components/PluginListItem.tsx,src/screens/browse/loadingAnimation/MalLoading.tsx,src/screens/browse/loadingAnimation/TrackerLoading.tsx,src/screens/library/hooks/useLibrary.ts,src/screens/novel/components/ExportNovelAsEpubButton.tsx,src/screens/novel/components/Info/NovelInfoHeader.tsx,src/screens/novel/components/NovelScreenList.tsx,src/screens/onboarding/ThemeSelectionStep.tsx,src/screens/reader/components/ReaderFooter.tsx,src/screens/reader/components/WebViewReader.tsx,src/screens/reader/hooks/useChapter.ts,src/screens/settings/SettingsAppearanceScreen/SettingsAppearanceScreen.tsx,src/screens/settings/SettingsBackupScreen/index.tsx,src/screens/settings/SettingsReaderScreen/tabs/AdvancedTab.tsx,src/services/backup/local/index.ts,src/services/backup/utils.ts,src/theme/utils/setBarColor.ts,strings/languages/en/strings.json,strings/types/index.ts,tsconfig.json",Fork has separate CI. Managed-workflow migration (40 files) - relabeled SKIP-INFRA per review 2026-08-04. +e4cf64a112dbfd9f9dd853abec99afbdc09f1460,e4cf64a112,2026-07-22,Rajarshee Chatterjee,refactor: migrate background tasks to native android scheduling (#1889),SKIP,SKIP-NATIVE,-,-,-,"android/app/build.gradle,android/app/src/main/AndroidManifest.xml,android/app/src/main/java/com/rajarsheechatterjee/LNReader/MainApplication.kt,android/app/src/main/java/com/rajarsheechatterjee/NativeFile/NativeFile.kt,App.tsx,package.json,pnpm-lock.yaml,src/database/db.ts,src/database/queries/ChapterQueries.ts,src/database/queries/NovelQueries.ts,src/hooks/index.ts,src/hooks/persisted/useNovel.ts,src/screens/browse/migration/MigrationNovelList.tsx,src/screens/library/hooks/useLibrary.ts,src/screens/library/LibraryScreen.tsx,src/screens/more/MoreScreen.tsx,src/screens/more/TaskQueueScreen.tsx,src/screens/novel/components/ExportNovelAsEpubButton.tsx,src/screens/novel/components/NovelScreenList.tsx,src/screens/reader/hooks/useChapter.ts,src/screens/settings/SettingsBackupScreen/Components/GoogleDriveModal.tsx,src/screens/settings/SettingsBackupScreen/Components/SelfHostModal.tsx,src/screens/settings/SettingsBackupScreen/index.tsx,src/screens/settings/SettingsReaderScreen/tabs/AdvancedTab.tsx,src/screens/updates/UpdatesScreen.tsx,src/services/backup/local/index.ts,src/services/backup/utils.ts,src/services/migrate/migrateNovel.ts,src/services/ServiceManager.ts",Native background-task infra (WorkManager/Expo modules); fork uses background-actions + ServiceManager. +3e1e211098779264ba123121cc66bb48385fcf77,3e1e211098,2026-07-19,Luigi,feat: add address bar in webview (#1870),PORT,B-stretch,MEDIUM,YELLOW,MANUAL,"src/screens/WebviewScreen/components/Appbar.tsx,src/screens/WebviewScreen/WebviewScreen.tsx",WebView address bar (~11 lines WebviewScreen). +02b6b9848fb3349373de09a4343b6d273605b842,02b6b9848f,2026-07-19,Ecasept,fix: prevent webview reset when reader settings change (#1884),SKIP,ALREADY-HAVE,-,-,-,src/screens/reader/components/WebViewReader.tsx,Fork already syncs settings via injectJavaScript (no WebView reload). +71da09ed444707586eee6fc63f7f2ee182bb2cda,71da09ed44,2026-07-19,Shisii,feat(reader): add current chapter search (#1877),PORT,D,HIGH,RED,MANUAL-rewrite,"android/app/src/main/assets/css/index.css,android/app/src/main/assets/js/core.js,src/screens/reader/components/ReaderAppbar.tsx,src/screens/reader/components/WebViewReader.tsx,src/screens/reader/hooks/useChapter.ts,src/screens/reader/ReaderScreen.tsx,strings/languages/en/strings.json,strings/types/index.ts",In-chapter search (#1877) - re-adapt 442-line search.js against fork core.js. +4404494ec9a57c3d2a8b1afb4734c9fc569773a2,4404494ec9,2026-07-19,Rajarshee Chatterjee,fix(database): repair migration history for scanlator column,SKIP,SKIP-DRIZZLE,-,-,-,src/database/db.ts,Drizzle migration SQL; fork MigrationRunner already covers. +98a2a1d725d51d333ea3969b74434b2917a92161,98a2a1d725,2026-07-15,Luigi,feat: implement scanlator filtering (#1864),PORT,D-optional,MEDIUM,RED,MANUAL-rewrite,"App.tsx,src/components/Checkbox/Checkbox.tsx,src/components/NovelCover.tsx,src/components/Switch/Switch.tsx,src/components/Switch/SwitchItem.tsx,src/database/queries/ChapterQueries.ts,src/database/types/index.ts,src/plugins/types/index.ts,src/screens/history/components/HistoryCard/HistoryCard.tsx,src/screens/more/MoreScreen.tsx,src/screens/novel/components/ChapterItem.tsx,src/screens/novel/components/NovelAppbar.tsx,src/screens/novel/components/NovelBottomSheet.tsx,src/screens/novel/components/Tracker/TrackSearchDialog.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderBottomSheet.tsx,src/screens/reader/components/ReaderBottomSheet/TTSTab.tsx,src/screens/reader/components/WebViewReader.tsx,src/screens/reader/hooks/useChapter.ts,src/screens/reader/ReaderScreen.tsx,src/screens/settings/SettingsReaderScreen/SettingsReaderScreen.tsx,src/screens/settings/SettingsReaderScreen/tabs/AdvancedTab.tsx,strings/languages/en/strings.json,strings/types/index.ts",Scanlator filtering - needs schema migration + plugin pipeline change; aggregator sources only. +788f6cede7a5676a3d30cf0fd8e565cddd3c486c,788f6cede7,2026-07-13,Palloxin,fix: prevent table overflow and clean up css (#1876) (#1876),PORT,A1,MEDIUM,GREEN,DIRECT,android/app/src/main/assets/css/index.css,Reader table-overflow CSS fix (index.css). +7abee71ac33ab4b16c0c2a629640b1a7c2f5eb3b,7abee71ac3,2026-07-13,Rajarshee Chatterjee,test: generate debug environment variables in testing workflow,SKIP,SKIP-CI,-,-,-,,Fork has separate CI. +52d5c99c46b14148bd262c878701abf91c8b593b,52d5c99c46,2026-07-02,Rajarshee Chatterjee,fix: order epub chapters across paginated pages (closes #1762),PORT,A2,MEDIUM,YELLOW,MANUAL,src/database/queries/ChapterQueries.ts,"EPUB chapters page ordering (getNovelDownloadedChapters ORDER BY page ASC, position ASC); fork has page column." +93bc5e5e4bac14fea10913cb215a7d9c297d6c31,93bc5e5e4b,2026-07-02,Rajarshee Chatterjee,fix: handle missing mal list entries and null progress (closes #1875),PORT,A1,MEDIUM,YELLOW,MANUAL,,MAL null progress fix. myAnimeList.ts diverged in fork (@env client id, WebBrowser, pkceChallenger) - only SetTrackChaptersDialog hunk ports cleanly; MANUAL (review 2026-08-04). +5d996f13883219090ad39ddbe7b52e88e36107c7,5d996f1388,2026-07-01,Jeon JaeHyeong,feat(downloads): make chapter download cooldown configurable (#1834),PORT,B,MEDIUM,YELLOW,MANUAL,"src/hooks/persisted/useSettings.ts,src/screens/settings/SettingsGeneralScreen/SettingsGeneralScreen.tsx,strings/languages/en/strings.json",Configurable chapter download cooldown (replaces hardcoded sleep(1000) in downloadChapter.ts). +bbb2645a6f6c47f7cda8d83ef096c329e03ffa6a,bbb2645a6f,2026-07-01,Palloxin,chore: clean reader preview content (#1867),SKIP,SKIP-CI,-,-,-,,CI/preview flavor infra (fork has no preview variant). +54fedc2f531ecdda29dea74f3cc279a7a4525261,54fedc2f53,2026-07-01,Vikas Movva,fix(tts): remove leading and trailing quotes from normalized text (#1869),PORT,A1,HIGH,YELLOW,MANUAL-trivial,android/app/src/main/assets/js/core.js,"TTS normalizeText quote-strip (1 line, core.js:2203). Improves fork TTS output." +e8c5550256dfe06c5ae12c106801e8688173a830,e8c5550256,2026-07-01,Palloxin,perf: optimize regex for removing extra paragraph spacing (#1872),SKIP,SKIP-OTHER,-,-,-,android/app/src/main/assets/js/core.js,No clear user value / overlaps fork-critical files or infra. See subagent report. +57b9d41b5c3eacc9cbf5e38082c4efb2a1a11f5e,57b9d41b5c,2026-07-01,Ali Hamza Malik,fix(service): correct notification throttle logic in servicemanager.setmeta (#1873),PORT,A1,MEDIUM,GREEN,DIRECT,src/services/ServiceManager.ts,"ServiceManager.setMeta throttle (fork has inverted condition + negative delay, :152-153)." +34be7a123499cc7733161482fdf5d42e4c1bae51,34be7a1234,2026-05-24,Jeon JaeHyeong,fix(native-file): prevent fatal crash when unlinking missing files (#1832),PORT,A1,HIGH,GREEN,DIRECT,android/app/src/main/java/com/rajarsheechatterjee/NativeFile/NativeFile.kt,NativeFile.kt unlink crash fix; fork byte-identical pre-fix (throw->return). Top crash fix. +60f42ed6243dc5d4daa68c727d60c9fb5dd6576a,60f42ed624,2026-05-24,Jeon JaeHyeong,chore(build): patch @rock-js/plugin-metro for windows compatibility (#1830),SKIP,SKIP-INFRA,-,-,-,"android/settings.gradle,pnpm-lock.yaml,pnpm-workspace.yaml",rock CLI build system; fork uses community CLI + Gradle 9.2.0. +25c93afabe24d34f2130ee0fd7e0846a9b4861ff,25c93afabe,2026-05-24,Anthony Truong,fix(reader): restore saved chapter progress from db if available (#1850),PORT,A3,MEDIUM,YELLOW,MANUAL,src/screens/reader/hooks/useChapter.ts,Restore chapter progress from DB on reader open (3 lines useChapter.ts). +2cd5a97ee8e482e92b45e9b702c9ba974e06a092,2cd5a97ee8,2026-05-10,Gaurav Tupe,fix: await database operations inside transactions (#1845),SKIP,SKIP-DRIZZLE,-,-,-,"src/database/queries/CategoryQueries.ts,src/database/queries/ChapterQueries.ts,src/database/queries/NovelQueries.ts",await DB ops in transactions (op-sqlite tx API) - NOT-FEASIBLE direct; CONCEPT applies to fork unawaited runAsync in NovelQueries (audit follow-up). +8f47e8fd1e1aedaf22df8275484f27b5d40b2a39,8f47e8fd1e,2026-05-07,Patrick Loser,theme switcher refactor (#1839),PORT,C,MEDIUM,YELLOW,MANUAL,"App.tsx,package.json,pnpm-lock.yaml,src/components/BottomTabBar/index.tsx,src/components/ColorPickerModal/ColorPickerModal.tsx,src/components/List/List.tsx,src/components/SegmentedControl/SegmentedControl.tsx,src/components/ThemePicker/ThemePicker.tsx,src/hooks/persisted/index.ts,src/hooks/persisted/useTheme.ts,src/screens/onboarding/ThemeSelectionStep.tsx,src/screens/settings/SettingsAppearanceScreen/LanguagePickerModal.tsx,src/screens/settings/SettingsAppearanceScreen/SettingsAppearanceScreen.tsx,src/theme/utils/setBarColor.ts",Theme switcher refactor (#1839) - prerequisite for dynamic colors; fork useTheme matches pre-refactor base. +eafa545b2a34320b5e1eb2d99ba61b6888838da8,eafa545b2a,2026-05-06,Patrick Loser,use novel refactor (#1838),SKIP,SKIP-INFRA,-,-,-,"package.json,pnpm-lock.yaml,src/database/db.ts,src/database/queries/__tests__/NovelQueries.test.ts,src/database/queries/ChapterQueries.ts,src/database/queries/NovelQueries.ts,src/hooks/persisted/index.ts,src/hooks/persisted/useNovel.ts,src/hooks/persisted/useTheme.ts,src/screens/novel/components/ChapterItem.tsx,src/screens/novel/components/Info/NovelInfoHeader.tsx,src/screens/novel/components/JumpToChapterModal.tsx,src/screens/novel/components/NovelBottomSheet.tsx,src/screens/novel/components/NovelScreenList.tsx,src/screens/novel/components/PageNavigationBottomSheet.tsx,src/screens/novel/NovelScreen.tsx,src/screens/reader/components/ChapterDrawer/index.tsx,src/screens/reader/components/ReaderAppbar.tsx,src/screens/reader/components/ReaderFooter.tsx,src/screens/reader/hooks/useChapter.ts,src/screens/settings/SettingsAdvancedScreen.tsx,src/screens/settings/SettingsReaderScreen/tabs/NavigationTab.tsx,src/screens/updates/UpdatesScreen.tsx,src/services/migrate/migrateNovel.ts,strings/languages/en/strings.json,strings/types/index.ts",useNovel zustand store refactor (75 files) - fork has own useNovel hook + raw SQL. Maintainability-only. +b680a07750f0b7650836c0573566e641d4aaa679,b680a07750,2026-05-03,Jeon JaeHyeong,fix(browse): use gesture-handler pressable in pluginlistitem (#1828),SKIP,SKIP-OTHER,-,-,-,src/screens/browse/components/PluginListItem.tsx,No clear user value / overlaps fork-critical files or infra. See subagent report. +2a919ec0f963a4d4613e3e0ebe99a16fa7e2dbe3,2a919ec0f9,2026-05-03,Jeon JaeHyeong,chore: gitignore tsconfig.tsbuildinfo build cache (#1829),PORT,A1,LOW,YELLOW,MANUAL-trivial,.gitignore,gitignore tsconfig.tsbuildinfo. Fork .gitignore context differs (expo-cli block) - manual append (review 2026-08-04). +01596c045a4637acf056f75cf87ffc0198d79899,01596c045a,2026-04-14,Definisi,fix: apply active filters to chapter count display (closes #1793) (#1797),PORT,B-stretch,MEDIUM,YELLOW,MANUAL,"src/database/queries/ChapterQueries.ts,src/hooks/persisted/useNovel.ts",Apply active filters to chapter count display. +3de93c5c093ab2fb7e0d509e367e4fec813082e2,3de93c5c09,2026-04-13,Patrick Loser,feat: migrate from rn community cli to rock (#1812),SKIP,SKIP-INFRA,-,-,-,".gitignore,android/app/build.gradle,android/settings.gradle,babel.config.js,package.json,pnpm-lock.yaml,src/screens/more/About.tsx,src/screens/novel/components/Info/NovelInfoHeader.tsx,tsconfig.json",rock CLI build system; fork uses community CLI + Gradle 9.2.0. +306f5b27e2716f1a963fd992c06f5c961f44c926,306f5b27e2,2026-04-06,Patrick Loser,fix: install the right version of hermes compiler,SKIP,SKIP-INFRA,-,-,-,"package.json,pnpm-lock.yaml",Expo 55 toolchain pinning; N/A to fork Expo 54. +06f0f66e3e4659db14127b1b56202dd3a8fca521,06f0f66e3e,2026-04-05,Patrick Loser,fix: add hermes-compiler dev dep,SKIP,SKIP-INFRA,-,-,-,"package.json,pnpm-lock.yaml",Expo 55 toolchain pinning; N/A to fork Expo 54. +64921a054e9d4a11a0aad294b9703a3ab4c16b9f,64921a054e,2026-04-05,Patrick Loser,feat: expo 55 upgrade,SKIP,SKIP-INFRA,-,-,-,".gitignore,android/app/src/main/AndroidManifest.xml,android/app/src/main/java/com/rajarsheechatterjee/LNReader/MainApplication.kt,android/build.gradle,android/gradle/wrapper/gradle-wrapper.properties,android/settings.gradle,babel.config.js,package.json,pnpm-lock.yaml,src/components/BottomSheet/BottomSheet.tsx,src/components/List/List.tsx,src/database/db.ts,src/hooks/persisted/useNovel.ts,src/plugins/helpers/storage.ts,src/screens/Categories/components/CategoryCard.tsx,src/screens/more/About.tsx,src/screens/novel/components/ChapterItem.tsx,src/screens/novel/components/Info/NovelInfoHeader.tsx,src/screens/novel/components/JumpToChapterModal.tsx,src/screens/novel/components/NovelScreenList.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderSheetPreferenceItem.tsx,src/screens/settings/SettingsAdvancedScreen.tsx",Expo 55 upgrade - belongs to fork own upgrade decision. +44e035ad38b25f79741199b6aea64479831ccc14,44e035ad38,2026-03-12,Rajarshee Chatterjee,chore: update translations (#1683),SKIP,SKIP-I18N,-,-,-,,Crowdin sync; fork strings at strings/languages with custom keys - manual per-key diff only. +1803cd071d24a4d8ada8c55814a6ce62eaf98481,1803cd071d,2026-02-25,Patrick Loser,feat: add testing support for react code (#1763),SKIP,SKIP-CI,-,-,-,"__mocks__/react-native-mmkv.js,babel.config.js,package.json,pnpm-lock.yaml,src/hooks/persisted/useNovel.ts,src/services/migrate/migrateNovel.ts,tsconfig.json",CI/test infra. +e4d7ba306b4441b4743ab9edb7c96751d62e3cde,e4d7ba306b,2026-02-24,Patrick Loser,fix: safety level may not be changed inside a transaction error (#1758),SKIP,SKIP-DRIZZLE,-,-,-,src/database/db.ts,Drizzle db.ts PRAGMA handling. +cc0428752b2e9869677a6bd2dccc449f7a98a552,cc0428752b,2026-02-20,Rajarshee Chatterjee,feat: replace tts notifications with android mediasession controls (closes #1711),SKIP,ALREADY-HAVE,-,-,-,"android/app/build.gradle,android/app/src/main/assets/js/core.js,android/app/src/main/java/com/rajarsheechatterjee/LNReader/MainApplication.kt,App.tsx,src/screens/reader/components/WebViewReader.tsx",MediaSession controls; fork has own TTS media notification. +b8c177bc55016b5a8fb96cc120e8ac050a2f7c73,b8c177bc55,2026-02-19,Patrick Loser,refactor: migrate to drizzle and `op-sqlite` (#1735),SKIP,SKIP-DRIZZLE,-,-,-,".gitignore,App.tsx,babel.config.js,package.json,pnpm-lock.yaml,pnpm-workspace.yaml,src/components/ListView.tsx,src/components/NovelCover.tsx,src/database/db.ts,src/database/migrations/002_add_novel_counters.ts,src/database/migrations/index.ts,src/database/queries/__tests__/CategoryQueries.test.ts,src/database/queries/__tests__/HistoryQueries.test.ts,src/database/queries/__tests__/NovelQueries.test.ts,src/database/queries/__tests__/RepositoryQueries.test.ts,src/database/queries/__tests__/StatsQueries.test.ts,src/database/queries/CategoryQueries.ts,src/database/queries/ChapterQueries.ts,src/database/queries/NovelQueries.ts,src/database/queries/StatsQueries.ts,src/database/tables/ChapterTable.ts,src/database/types/index.ts,src/database/utils/migrationRunner.ts,src/hooks/common/useDatabaseInitialization.ts,src/hooks/common/useGithubUpdateChecker.ts,src/hooks/index.ts,src/hooks/persisted/useNovel.ts,src/hooks/persisted/useSettings.ts,src/navigators/types/index.ts,src/screens/browse/components/AvailableTab.tsx,src/screens/BrowseSourceScreen/components/FilterBottomSheet.tsx,src/screens/Categories/CategoriesScreen.tsx,src/screens/Categories/components/AddCategoryModal.tsx,src/screens/Categories/components/CategoryCard.tsx,src/screens/history/HistoryScreen.tsx,src/screens/library/hooks/useLibrary.ts,src/screens/library/LibraryScreen.tsx,src/screens/novel/components/ChapterItem.tsx,src/screens/novel/components/EditInfoModal.tsx,src/screens/novel/components/Info/NovelInfoHeader.tsx,src/screens/novel/components/NovelBottomSheet.tsx,src/screens/novel/components/NovelScreenList.tsx,src/screens/novel/components/SetCategoriesModal.tsx,src/screens/novel/NovelScreen.tsx,src/screens/reader/components/ChapterDrawer/index.tsx,src/screens/reader/hooks/useChapter.ts,src/screens/reader/ReaderScreen.tsx,src/screens/settings/SettingsAdvancedScreen.tsx,src/screens/settings/SettingsGeneralScreen/SettingsGeneralScreen.tsx,src/screens/settings/SettingsRepositoryScreen/components/DeleteRepositoryModal.tsx,src/screens/settings/SettingsRepositoryScreen/SettingsRepositoryScreen.tsx,src/screens/settings/SettingsTrackerScreen.tsx,src/screens/updates/components/UpdateNovelCard.tsx,src/services/backup/utils.ts,src/services/migrate/migrateNovel.ts,src/services/Trackers/mangaUpdates.ts,tsconfig.json",Drizzle/op-sqlite migration or API; fork uses raw SQL + MigrationRunner. NOT-FEASIBLE. +7083c51dea1be0af3bb0d53c341f19b32a35dca4,7083c51dea,2026-02-19,Rajarshee Chatterjee,fix: include selectedids in chapter list extradata for selection highlight (closes #1750),SKIP,SKIP-OTHER,-,-,-,src/screens/novel/components/NovelScreenList.tsx,No clear user value / overlaps fork-critical files or infra. See subagent report. +f0693204e1f8f961dff85b195f3c7f4f6f9bb0ce,f0693204e1,2026-02-18,tomsLt0,fix: improve next chapter selection logic (#1744),PORT,A2,MEDIUM,YELLOW,MANUAL,src/database/queries/ChapterQueries.ts,Next chapter selection logic + LIMIT 1; fork ChapterQueries.ts:441 identical pre-fix. Port with d15cf98. +3f8071e48ee603f906cc9e3910abe90d6e3874a5,3f8071e48e,2026-02-18,Rajarshee Chatterjee,chore: release v2.0.3,SKIP,SKIP-RELEASE,-,-,-,"android/app/build.gradle,package.json",Version bump; fork has own numbering (v2.1.3). +a1c5350e8e330e783b6537c366351c2082e99150,a1c5350e8e,2026-02-12,Rajarshee Chatterjee,fix: miscellaneous ui tweaks,SKIP,SKIP-OTHER,-,-,-,"src/components/List/List.tsx,src/screens/Categories/components/CategoryCard.tsx,src/screens/library/LibraryScreen.tsx,src/screens/novel/components/Info/NovelInfoComponents.tsx,src/screens/novel/components/Info/NovelInfoHeader.tsx,src/screens/updates/components/UpdateNovelCard.tsx",No clear user value / overlaps fork-critical files or infra. See subagent report. +8f237909d54310220d518000ace81b5fddbc3b5f,8f237909d5,2026-02-12,Rajarshee Chatterjee,feat: show first unread chapter in readbutton and fab when no last read,PORT,B,MEDIUM,YELLOW,MANUAL,"src/database/queries/ChapterQueries.ts,src/hooks/persisted/useNovel.ts,src/screens/novel/components/Info/NovelInfoHeader.tsx,src/screens/novel/components/NovelScreenList.tsx",First unread chapter in ReadButton/FAB (getFirstUnreadChapter SQL + wiring). +11ff69d4f5b4a79286247ac6c40ca1eac72ad283,11ff69d4f5,2026-02-12,Rajarshee Chatterjee,fix: pre-fetch adjacent page chapters in reader and library updates,PORT,D-optional,MEDIUM,RED,MANUAL-rewrite,"src/components/BottomSheet/BottomSheet.tsx,src/screens/reader/hooks/useChapter.ts",Pre-fetch adjacent page chapters - port only LibraryUpdateQueries preload part; reader part touches fork-critical useChapter. +d15cf987425709f97ec168a46af8d936e944c29e,d15cf98742,2026-02-12,Rajarshee Chatterjee,fix: order chapter pages numerically in queries for accurate retrieval (closes #1742),PORT,A2,MEDIUM,YELLOW,MANUAL,src/database/queries/ChapterQueries.ts,Numeric page ordering CAST(page AS INTEGER) in getCustomPages/getPrevChapter/getNextChapter; fork has identical buggy SQL. +29bd6612e9a9950c61b9a569b761ffa1aaa5d4eb,29bd6612e9,2026-02-12,Rajarshee Chatterjee,refactor: optimize library screen performance and fix category bug,PORT,D-optional,MEDIUM,RED,MANUAL-rewrite,"src/components/NovelCover.tsx,src/components/NovelList.tsx,src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx,src/screens/library/components/LibraryBottomSheet/LibraryBottomSheet.tsx,src/screens/library/hooks/useLibrary.ts,src/screens/library/LibraryScreen.tsx",Library screen perf + update-category index bug - re-implement on fork LibraryScreen/useLibrary. +ab1e36d280768be46acdb57557e09b3e2d37f12b,ab1e36d280,2026-02-08,Rajarshee Chatterjee,refactor: update components to follow material design 3 guidelines,SKIP,SKIP-OTHER,-,-,-,"src/components/Actionbar/Actionbar.tsx,src/components/BottomSheet/BottomSheet.tsx,src/components/BottomTabBar/index.tsx,src/components/Chip/Chip.tsx,src/components/ConfirmationDialog/ConfirmationDialog.tsx,src/components/IconButtonV2/IconButtonV2.tsx,src/components/List/List.tsx,src/components/Menu/index.tsx,src/components/SearchbarV2/SearchbarV2.tsx,src/components/SegmentedControl/SegmentedControl.tsx,src/components/Switch/Switch.tsx,src/components/Switch/SwitchItem.tsx,src/screens/more/MoreScreen.tsx,src/screens/novel/components/JumpToChapterModal.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderSheetPreferenceItem.tsx",No clear user value / overlaps fork-critical files or infra. See subagent report. +f1fdafd37f1f7d0816484c2e414e39d9885499d8,f1fdafd37f,2026-02-07,Rajarshee Chatterjee,fix: prevent tab bar height jump and shadow flash on browse screen,PORT,A1,LOW,YELLOW,MANUAL,src/screens/browse/BrowseScreen.tsx,Browse tab-bar height jump/shadow flash (initialLayout). Fork BrowseScreen.tsx has eslint-disable header + no Color import - hunk fails apply; MANUAL (review 2026-08-04). +4c7684a3a71e57fb27f910b3edfbdd902353dd47,4c7684a3a7,2026-02-07,Rajarshee Chatterjee,refactor: optimize novel screen performance and fix download state tracking,PORT,D-optional,MEDIUM,RED,MANUAL-rewrite,".gitignore,src/hooks/persisted/useNovel.ts,src/screens/novel/components/Chapter/ChapterDownloadButtons.tsx,src/screens/novel/components/ChapterItem.tsx,src/screens/novel/components/Info/NovelInfoComponents.tsx,src/screens/novel/components/Info/NovelInfoHeader.tsx,src/screens/novel/components/NovelAppbar.tsx,src/screens/novel/components/NovelScreenList.tsx,src/screens/novel/components/PagePaginationControl.tsx,src/screens/novel/NovelScreen.tsx,src/screens/updates/components/UpdateNovelCard.tsx","Novel screen perf + download state tracking - port selectively (updateChapter state bug, Set-based download tracking)." +2f45db659b2fdfa77f9213023d9d2707cbc452e1,2f45db659b,2026-02-07,Rajarshee Chatterjee,fix: replace sync database operations with async to prevent database lock (closes #1716),PORT,D-optional,MEDIUM,RED,MANUAL-rewrite,"src/database/queries/ChapterQueries.ts,src/database/queries/NovelQueries.ts,src/hooks/persisted/useNovel.ts,src/screens/library/hooks/useLibrary.ts,src/screens/library/LibraryScreen.tsx,src/screens/novel/components/Chapter/ChapterDownloadButtons.tsx,src/services/migrate/migrateNovel.ts",Async DB ops to prevent SQLITE_BUSY - fork-side incremental effort. +9e38ad114d182d39880a962cb7acefa55235310d,9e38ad114d,2026-02-06,Rajarshee Chatterjee,fix: sanitize epub filename to remove filesystem-unsafe characters (closes #1480),PORT,A1,MEDIUM,GREEN,DIRECT,src/screens/novel/components/ExportNovelAsEpubButton.tsx,EPUB filename sanitize (1 line ExportNovelAsEpubButton). +45c4ea8ca02031c803224b0d906a6045364d2817,45c4ea8ca0,2026-02-06,Rajarshee Chatterjee,fix: handle ioexception in nativefile to prevent epub import crash loop (closes #1315),PORT,A1,MEDIUM,GREEN,DIRECT,android/app/src/main/java/com/rajarsheechatterjee/NativeFile/NativeFile.kt,NativeFile.kt IOException wrap; prevents EPUB import crash loop. +d2b95ae3166f337d8d3597e1cc9a0bff65832d62,d2b95ae316,2026-02-06,Rajarshee Chatterjee,fix: use nativefile for custom css/js import to fix content uri read failure (closes #1661),PORT,B-stretch,MEDIUM,YELLOW,MANUAL,src/screens/settings/SettingsReaderScreen/tabs/AdvancedTab.tsx,NativeFile for custom CSS/JS import (Content URI fix). +02121152b3e3cdbbaaff8a34b51cb8e9b2010538,02121152b3,2026-02-06,Rajarshee Chatterjee,fix: stabilize scroll handler references to prevent infinite re-render loop (closes #1679),SKIP,SKIP-OTHER,-,-,-,"src/screens/browse/discover/MalTopNovels.tsx,src/screens/novel/components/NovelScreenList.tsx",No clear user value / overlaps fork-critical files or infra. See subagent report. +caed965b6bad1b788eee3035daba42322454fe26,caed965b6b,2026-02-06,Rajarshee Chatterjee,fix: prevent epub import crash from documentpicker cache cleanup (closes #1725),PORT,B-stretch,MEDIUM,YELLOW,MANUAL,src/screens/library/LibraryScreen.tsx,EPUB import DocumentPicker cache-cleanup crash (copyToCacheDirectory:false). +4088d5966bbc5ec27801b1ddc553f5621b4b6d79,4088d5966b,2026-02-06,Rajarshee Chatterjee,fix: close okhttp response and streams in nativefile.downloadfile (closes #1740),PORT,A1,MEDIUM,GREEN,DIRECT,android/app/src/main/java/com/rajarsheechatterjee/NativeFile/NativeFile.kt,OkHttp response.use in NativeFile.downloadFile; leak fix. +a062beee87d4173383e3033eeacb9376642645e5,a062beee87,2026-02-06,Rajarshee Chatterjee,fix: add max height to set categories modal (closes #1738),PORT,A1,LOW,YELLOW,MANUAL,src/screens/novel/components/SetCategoriesModal.tsx,Set Categories modal maxHeight. Fork SetCategoriesModal rewritten (scaleDimension/AppText) - hunk fails apply; MANUAL (review 2026-08-04). +30958825162fbba2f7b805de6d41ab8e9183f28b,3095882516,2026-02-01,Patrick Loser,replace react-native-paper menus with custom implementation (#1733),SKIP,SKIP-OTHER,-,-,-,"src/components/Menu/index.tsx,src/screens/browse/components/Modals/SourceSettings.tsx,src/screens/BrowseSourceScreen/components/FilterBottomSheet.tsx,src/screens/more/TaskQueueScreen.tsx,src/screens/novel/components/Chapter/ChapterDownloadButtons.tsx",No clear user value / overlaps fork-critical files or infra. See subagent report. +4ad06397960f1fbc633604ef06c2dfca193a220e,4ad0639796,2026-01-31,Patrick Loser,fix: chapter loading for jump to chapter modal and chapter drawer,PORT,B,MEDIUM,YELLOW,MANUAL,"pnpm-lock.yaml,src/database/queries/ChapterQueries.ts,src/hooks/persisted/useNovel.ts,src/screens/novel/components/JumpToChapterModal.tsx,src/screens/novel/NovelScreen.tsx,src/screens/reader/components/ChapterDrawer/index.tsx",Jump-to-chapter/drawer reach unloaded chapters (getNovelChaptersByNumber/Name raw SQL + loadUpToBatch). +b53ec3352f8ce9084e32ddaa352526b39ec6003c,b53ec3352f,2026-01-28,Rajarshee Chatterjee,feat: support additional plugin setting types (dropdown and checkboxes),PORT,B-stretch,MEDIUM,YELLOW,MANUAL,"src/plugins/types/index.ts,src/screens/browse/components/Modals/SourceSettings.tsx",Plugin setting types: dropdown + checkboxes. +c40edd5b2cc68c2b746ba588aa39e142ab99f01e,c40edd5b2c,2026-01-25,Rajarshee Chatterjee,feat: add kitsu tracker (closes #416),PORT,B,HIGH,YELLOW,MANUAL,"src/screens/novel/components/Tracker/ScoreSelectors.tsx,src/screens/novel/components/Tracker/TrackerCards.tsx,src/screens/settings/SettingsTrackerScreen.tsx",Kitsu tracker (new kitsu.ts service + tracker UI wiring). Best value/effort in batch. +bdd42caaaf3c3d5961d444aabc9150057d2de5d9,bdd42caaaf,2026-01-20,Rajarshee Chatterjee,chore: update repository links to use lneader instead of lnreader,SKIP,SKIP-INFRA,-,-,-,"README.md,src/screens/more/About.tsx,strings/languages/en/strings.json",Repo-link rebrand to upstream org - actively harmful for fork. +ab4eee31ceb82c3f89e459279bef8b56de5ec10c,ab4eee31ce,2026-01-20,Rajarshee Chatterjee,feat: allow all safe tags and attributes when sanitizing chapter html (closes #1715),SKIP,SKIP-OTHER,-,-,-,src/screens/reader/utils/sanitizeChapterText.ts,No clear user value / overlaps fork-critical files or infra. See subagent report. +3f9886a2e5300bb9913888872847727ab63597fc,3f9886a2e5,2026-01-17,Rajarshee Chatterjee,chore: update repository links to use lneader instead of lnreader,SKIP,SKIP-INFRA,-,-,-,"README.md,src/screens/more/About.tsx,strings/languages/en/strings.json,strings/types/index.ts",Repo-link rebrand to upstream org - actively harmful for fork. +198b7443ae9c0ee7cc3ff608c0fc89a88880fa84,198b7443ae,2026-01-20,kingstar1111,feat: add rtl (right-to-left) language support (#1717),PORT,D-optional,MEDIUM,RED,MANUAL-rewrite,"src/components/Checkbox/Checkbox.tsx,src/components/Chip/Chip.tsx,src/components/ColorPreferenceItem/ColorPreferenceItem.tsx,src/components/List/List.tsx,src/components/ListView.tsx,src/components/Menu/index.tsx,src/components/RadioButton.tsx,src/components/RadioButton/RadioButton.tsx,src/components/SearchbarV2/SearchbarV2.tsx,src/components/Switch/SwitchItem.tsx,src/components/ThemePicker/ThemePicker.tsx,src/hooks/common/useGithubUpdateChecker.ts,src/navigators/BottomNavigator.tsx,src/screens/browse/components/PluginListItem.tsx,src/screens/browse/components/PluginListItemSkeleton.tsx,src/screens/browse/migration/MigrationSourceItem.tsx,src/screens/BrowseSourceScreen/components/FilterBottomSheet.tsx,src/screens/Categories/components/CategoryCard.tsx,src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx,src/screens/history/components/HistoryCard/HistoryCard.tsx,src/screens/library/hooks/useLibrary.ts,src/screens/library/LibraryScreen.tsx,src/screens/novel/components/ChapterItem.tsx,src/screens/novel/components/NovelAppbar.tsx,src/screens/novel/components/PageNavigationBottomSheet.tsx,src/screens/novel/components/PagePaginationControl.tsx,src/screens/novel/components/Tracker/TrackSearchDialog.tsx,src/screens/novel/components/Tracker/TrackSheet.tsx,src/screens/reader/components/ChapterDrawer/index.tsx,src/screens/reader/components/ReaderAppbar.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderFontPicker.tsx,src/screens/reader/components/ReaderBottomSheet/TTSTab.tsx,src/screens/reader/components/WebViewReader.tsx,src/screens/reader/ReaderScreen.tsx,src/screens/settings/SettingsReaderScreen/components/TabBar.tsx,src/screens/settings/SettingsReaderScreen/SettingsReaderScreen.tsx,src/screens/settings/SettingsReaderScreen/tabs/AdvancedTab.tsx,src/screens/settings/SettingsTrackerScreen.tsx,src/screens/updates/components/UpdateNovelCard.tsx",RTL language support (44 files incl. WebViewReader). +f9c5ca303d71830811a203f98a17c3dd4846f887,f9c5ca303d,2026-01-16,Rajarshee Chatterjee,chore: update indonesian translations (closes #1653),SKIP,SKIP-I18N,-,-,-,,Crowdin sync; fork strings at strings/languages with custom keys - manual per-key diff only. +9922d4394058a6da29ab2789cd8d7b75a37bf1bc,9922d43940,2026-01-15,Rajarshee Chatterjee,fix: remove missing image references during epub export (closes #1696),PORT,B-stretch,MEDIUM,YELLOW,MANUAL,src/screens/novel/components/ExportNovelAsEpubButton.tsx,EPUB export missing-image cleanup. +ae99b077d4e883e678b31ba0235a7abae88e1e94,ae99b077d4,2026-01-15,Rajarshee Chatterjee,feat: add `@noble/ciphers` for plugins (closes #1709),PORT,B-stretch,MEDIUM,YELLOW,MANUAL,"package.json,pnpm-lock.yaml",@noble/ciphers for plugins. +0cb9da90276c6a30d5d917557195f0f0b0ebc70c,0cb9da9027,2026-01-15,Rajarshee Chatterjee,refactor: replace `withtransactionasync` with `withexclusivetransactionasync` for improved concurrency handling (closes #1636),PORT,A2,MEDIUM,YELLOW,MANUAL,"src/database/queries/CategoryQueries.ts,src/database/queries/NovelQueries.ts,src/database/queries/StatsQueries.ts,src/database/utils/helpers.tsx,src/services/migrate/migrateNovel.ts",Reclassified PORT-A2 per review 2026-08-04: PRE-Drizzle expo-sqlite tx fix (withTransactionAsync -> withExclusiveTransactionAsync + await runAsync); fork has identical pre-fix pattern (NovelQueries.ts:180/321, helpers.tsx:133-137 unawaited runAsync); maps 1:1 onto fork raw SQL. diff --git a/specs/upstream-merge-analysis-2026-08/reference/commit_overlap_170.txt b/specs/upstream-merge-analysis-2026-08/reference/commit_overlap_170.txt new file mode 100644 index 0000000000..f89c2fdc4f --- /dev/null +++ b/specs/upstream-merge-analysis-2026-08/reference/commit_overlap_170.txt @@ -0,0 +1,170 @@ +c3260e8e01e4704b57137d787ffe9cfec7941cbc|fix: Resolve Chapter Download Queue and Removal Issues (Closes #1937)| +c0877a9b0656e22c3c788854006fb456c3ed704a|feat: Add Ignorable App Update Notifications| +96fb52df1730912c40ee6918cf6a6fcd74e31cc8|feat: Enhance Stats With Genre Taxonomy and Charts (#1919)| +76106317b68e8aade12b868afb16dda4671ceb16|fix: Restore Midnight Dusk Chip Contrast (Closes #1939)| +e75f4fba5da08e2317cfed2fe92b8de09457475c|feat: Add More Novel Statuses and Icons (#1931)|src/plugins/types/index.ts,src/screens/novel/components/Info/NovelInfoHeader.tsx +4dc933ee5a361eb0535120fa192c1eb51ba8cc70|fix: Remove White Flash During Screen Transitions (Closes #1926) (#1927)|App.tsx +bfb12b04aa31e3013f50503697253b549ee9618c|feat: Add Configurable Automatic Backup Scheduling|src/hooks/persisted/useSettings.ts,src/screens/settings/SettingsBackupScreen/index.tsx +b7639ab55a308c55617f0088c193052b9e0bd2b2|feat: Upload JavaScript Source Maps (#1924)|src/components/AppErrorBoundary/AppErrorBoundary.tsx +ecc5b84a64ac1aa841583acb43d7ac8aab5a72c5|chore: Release v2.1.2|package.json +eb12bdfd623bd28c2c3f88f585fefca9352711a4|fix: Prevent Novel Genre Rendering Crashes|src/screens/novel/components/EditInfoModal.tsx,src/screens/novel/components/Info/NovelInfoComponents.tsx +9612bf6f893d0078c566f9651405097b7f8d1f24|chore: Update Translations (#1928)| +0ed7d878b4b75e1f6ebc83d0978f05cad5920014|fix: Prevent White Reader Drawer Seam|src/screens/reader/ReaderScreen.tsx +5f45e4c7f2d4f77da3b73bdfdfcdb5b477fd6849|fix: Recover Interrupted Chapter Migrations|src/database/db.ts +ca24671a2f847c5722009b6b8451e9c54d3a2c12|fix: Support Fork-Safe Preview Builds| +ee3a4f20bc41ccf9e3f33ce37e7191cd5c7e6a17|fix: Preserve Default Category After Reordering (Closes #1922)|src/database/queries/__tests__/CategoryQueries.test.ts,src/database/queries/__tests__/NovelQueries.test.ts,src/database/queries/CategoryQueries.ts,src/database/queries/NovelQueries.ts,src/screens/Categories/CategoriesScreen.tsx +0c3542836c847c0e824e473c06394bd5219d6329|fix: Synchronize Library Updates And Optimize Import Refreshes|src/database/queries/ChapterQueries.ts,src/screens/library/hooks/useLibrary.ts +f71c9c859471968684baef6e7f3da55a9d2f5821|fix: Ignore Discord Notifications Outside the Main Repository (#1923)| +f53546cc01d76b8b3f73ed4a3b88b374f775d746|chore: Release v2.1.1|package.json +594b078fad3308c284b3b9cd8484056b40d3c821|fix: Harden Database Migration Recovery|src/database/db.ts +ac1b2f5ce446ae90db315be11d1494bc49539f40|feat: Add Durable Completion Notifications| +648d21062f8812ae2a8526954a374424c0dede3c|feat: Add Parallel Task Queues With Per-Plugin Download Limits|src/services/migrate/migrateNovel.ts +89d715ce6bc22ea15885ff86b3359033f3253b4c|docs: Replace Beta Badge With Build Preview Status|README.md +bd527a02efa9d642b7168e432bfb965a0ae85f51|feat: Improve Backup Selection, Notifications, And Plugin Restore|src/components/Checkbox/Checkbox.tsx,src/screens/settings/SettingsBackupScreen/Components/GoogleDriveModal.tsx,src/screens/settings/SettingsBackupScreen/Components/SelfHostModal.tsx,src/screens/settings/SettingsBackupScreen/index.tsx,src/services/backup/local/index.ts,src/services/backup/types.ts,src/services/backup/utils.ts +11117c1b5404820868683522242296d4f61d7642|feat: Add GitHub-Style Embed For Nightly Notifications| +ba25d5b8fe3af0b006ad09293a499b58cbee1479|feat: Add Rozenite DevTools Integration (#1912)|App.tsx,package.json,pnpm-lock.yaml +31cb4b994a332bd6f99864ba792aaa988bf4720b|fix: Align Bottom Navigation With Material 3 Specs|src/components/BottomTabBar/index.tsx +72ef618af1bdb6a40783ab119649e302a89e12d8|fix: Preserve Existing Data During Novel Counter Migration| +936fd223bbde71c3e0496f22ea4f28c9714d7987|feat: Add Discord Notifications For Nightly Builds| +6d26438d2dd5b7faf21786bc390ae83b43aca6d5|test: Update Expected Database Migrations|AGENTS.md +5eb5a55c4c654c046f984f1e324b36587389dfa8|fix: Repair Skipped Novel Counter Migrations| +6cd86d1422de47dc8e08c53e645c678b39ed66cc|chore: Update Translations (#1906)| +9783c4d5e3a0f7e5736d1fa50a8dd360a760f76c|fix: Normalize Repository Back Navigation (#1905)|src/screens/settings/SettingsRepositoryScreen/SettingsRepositoryScreen.tsx +87f602a1fdaaf4456b1947a6097b5a7e65caa1a3|chore: Release v2.1.0|package.json +3d32ddd6e933d5b5208f7813b99b8755e700d483|fix: Improve Release Selection And APK Verification| +7ddd934c2b834ba8fbed6969dab89728e3316251|fix: Handle Missing Tracker Client IDs| +b53c91f748581227d63a28d28d33de87c1cbb296|chore: Separate Jest And Type Check Workflows|package.json +9d106a371620a513f1f4cf57f511936866a77fd2|feat: Add Semantic Version Bump Selection| +8b0a8dc4c07b6d77532ef9b070bbce288afd92d4|chore: Update Husky Pre-Commit and Pre-Push Hooks|package.json +72bfcad03469440c1e9cf5ff88b5ff97a544c45e|fix: Resolve Type and Lint Issues|eslint.config.js,src/components/ErrorView/ErrorView.tsx,src/database/queries/__tests__/NovelQueries.test.ts,src/database/queries/__tests__/StatsQueries.test.ts,src/hooks/common/useGithubUpdateChecker.ts,src/navigators/types/index.ts,src/screens/browse/discover/AniListTopNovels.tsx,src/screens/browse/discover/MalTopNovels.tsx,src/screens/browse/migration/MigrationSourceItem.tsx,src/screens/browse/SourceNovels.tsx,src/screens/BrowseSourceScreen/useBrowseSource.ts,src/screens/GlobalSearchScreen/GlobalSearchScreen.tsx,src/screens/library/hooks/useLibrary.ts,src/screens/more/About.tsx,src/screens/more/components/MoreHeader.tsx,src/screens/more/MoreScreen.tsx,src/screens/more/TaskQueueScreen.tsx,src/screens/reader/ChapterContext.tsx,src/screens/reader/components/ChapterDrawer/index.tsx,src/screens/reader/components/WebViewReader.tsx,src/screens/reader/ReaderScreen.tsx,src/screens/settings/SettingsBackupScreen/Components/GoogleDriveModal.tsx,src/screens/settings/SettingsBackupScreen/index.tsx,src/screens/settings/SettingsReaderScreen/SettingsReaderScreen.tsx,src/screens/settings/SettingsRepositoryScreen/components/RepositoryCard.tsx,src/screens/settings/SettingsTrackerScreen.tsx,src/screens/WebviewScreen/components/Appbar.tsx,src/screens/WebviewScreen/components/Menu.tsx,src/screens/WebviewScreen/WebviewScreen.tsx +345d084eba5f1862c37d15c3790ce36610116d70|feat: Add Chapter Numbers To EPUB Titles (Closes #1405)|src/hooks/persisted/useSettings.ts,src/screens/novel/components/ExportEpubModal.tsx,src/screens/novel/components/ExportNovelAsEpubButton.tsx +10b6042a9c671f47ba33f7810c47935a54706f40|feat: Add EPUB Export Progress Notifications|src/screens/novel/components/ExportEpubModal.tsx,src/screens/novel/components/ExportNovelAsEpubButton.tsx +bd7161172551568e579e60ece7072f8c5a05fa65|feat: Improve Chapter Drawer And WebView App Bar|src/screens/reader/components/ChapterDrawer/index.tsx,src/screens/reader/ReaderScreen.tsx,src/screens/WebviewScreen/components/Appbar.tsx +8f53550d2c6ec75b8d9cefe0142218aaec26f726|fix: Restore Tracker Search Requests|src/screens/novel/components/Tracker/TrackSearchDialog.tsx +52914b978df3267342b9cd3d18270bf19ae5a7cb|fix: Migrate Volume Button Events to Expo Listener API|src/screens/reader/hooks/useChapter.ts +ff12759a66ad68791d7b8270579b8f7d020e73f5|ci: Run Preview Builds Daily When Missing| +e62868b82ccd1784ad83cd00d338c47d2818df94|feat: Automate Draft Android Releases|.gitignore,package.json,src/hooks/common/useGithubUpdateChecker.ts +97be48dfe17b6a5261284cc3052ab5dcbab8d20d|feat: Add Migration Review Options|AGENTS.md,src/screens/browse/migration/MigrationNovelList.tsx,src/services/migrate/migrateNovel.ts +cbe7d70e2df349db7ebf36a448e1f84b3903fb78|fix: Restore Collapsed TTS Button Interaction| +6548e9a6f8efc0bb21e46af005072d7599b326e9|fix: Improve Paged Reader Layout and Performance|src/screens/reader/ReaderScreen.tsx +138fdff2e57e55e02f9775d11dfb47482701f5c5|fix: Prevent Duplicate WebView Bottom Insets|src/screens/WebviewScreen/WebviewScreen.tsx +f1dd4b77a891e501b9345e5d0fe81b6405bce450|feat: Improve Empty Chapter Reporting|src/screens/reader/components/WebViewReader.tsx,src/screens/reader/utils/sanitizeChapterText.ts +9b356ffd17c28d7f37285157f3f34488af67a266|fix: Preserve Client IDs And Improve Environment Generation| +5cf7e15d7631f3ada56d4cb979694214745bd10d|feat: Add Reader Setting Descriptions|src/screens/settings/SettingsReaderScreen/tabs/AccessibilityTab.tsx,src/screens/settings/SettingsReaderScreen/tabs/NavigationTab.tsx +6a7e90c179b735521aa9d778e68f1727607e8ac5|fix: Align Advanced Reader Tabs And Localize Actions|src/screens/settings/SettingsReaderScreen/tabs/AdvancedTab.tsx +99c31d56bb45c6e04fe96cff3f14c3ca55fccba4|fix: Use MD3 Outline Variant For Tab Borders|src/screens/browse/BrowseScreen.tsx,src/screens/library/components/LibraryBottomSheet/LibraryBottomSheet.tsx,src/screens/library/LibraryScreen.tsx,src/screens/novel/components/NovelBottomSheet.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderBottomSheet.tsx,src/screens/settings/SettingsReaderScreen/SettingsReaderScreen.tsx +4d98247980c21f892df6cf0da19ecad8a6f0d136|feat: Add Android DNS Over HTTPS Support|src/screens/settings/SettingsAdvancedScreen.tsx +164f5d259243a68f14a7e8a069b2e2cfc42aab84|feat: Add Automatic Library Update Scheduling|src/hooks/persisted/useSettings.ts +89cddf9e3d07d3f7a7c7cb71e8cc8e5e4e96fcc3|fix: Expand Discover Row Ripple|src/screens/browse/discover/DiscoverCard.tsx +eac6ad584e85d4ed72cf76fafcde1c4d0551687d|feat: Improve Novel App Bar And EPUB Export UX|src/screens/novel/components/ExportEpubModal.tsx,src/screens/novel/components/JumpToChapterModal.tsx,src/screens/novel/components/NovelAppbar.tsx,src/screens/novel/components/NovelScreenList.tsx,src/screens/novel/NovelScreen.tsx +93fe04c28cc4776a96f86aa157b729ddf3c218c3|feat: Polish Reader Controls And Navigation|src/components/Common/ToggleButton.tsx,src/components/SegmentedControl/SegmentedControl.tsx,src/database/queries/ChapterQueries.ts,src/navigators/MoreStack.tsx,src/screens/browse/discover/DiscoverCard.tsx,src/screens/more/DownloadsScreen.tsx,src/screens/reader/components/ReaderAppbar.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderBottomSheet.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderFontPicker.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderSheetPreferenceItem.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderTextAlignSelector.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderThemeSelector.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderValueChange.tsx,src/screens/reader/components/ReaderBottomSheet/TextSizeSlider.tsx,src/screens/reader/components/ReaderBottomSheet/TTSTab.tsx,src/screens/reader/components/ReaderFooter.tsx,src/screens/reader/ReaderScreen.tsx,src/screens/settings/SettingsTrackerScreen.tsx +c9358b3a9fc49ad6a98ac0293da46bf9eab39bf8|feat: Update Android Branding Assets| +67d2c7c01b101d83d0033aad2c0a99247f04620e|feat: Add Collapsible TTS Controls And Stable Notification Progress| +c30441b9493d5e75fbe82a608b034c6239fa493f|feat: Add Smart Library Update Filters|src/hooks/persisted/useSettings.ts +6d2a9f8e1530604860375a9be3abb92b4502d01b|fix: Prevent Slider Flicker After Release| +799845426c8f2e651de508d471d8319add41d2cc|feat: Add Parallel Library Updates Across Sources| +8ac628b3deda367f68af93f9f43d7a1da8c80f9d|fix: Improve History Removal Flow.|src/database/queries/__tests__/HistoryQueries.test.ts,src/screens/history/components/HistoryCard/HistoryCard.tsx,src/screens/history/HistoryScreen.tsx +d02291c22c079e5faee9817b00c7bd87ed7f47da|feat: Remember Last Used Library Category|src/hooks/persisted/useSettings.ts,src/screens/library/LibraryScreen.tsx +7af0911f5b17e1c606dbf60e0f1d1b8863c78221|feat: Add High-Resolution Android Flavor Assets| +098782d6aa9a188bf007402155bd66e3dbc14523|refactor: Simplify Settings Layout And Theme Selection|package.json,pnpm-lock.yaml,src/components/List/List.tsx,src/components/ThemePicker/ThemePicker.tsx,src/screens/onboarding/ThemeSelectionStep.tsx,src/screens/settings/SettingsAppearanceScreen/SettingsAppearanceScreen.tsx,src/screens/settings/SettingsGeneralScreen/SettingsGeneralScreen.tsx,src/screens/settings/SettingsTrackerScreen.tsx +732628c40bb3f1afbbb0336577f9a56f97ef9115|feat: Add Date Format And Relative Timestamp Settings.|src/hooks/persisted/useSettings.ts,src/screens/history/HistoryScreen.tsx,src/screens/more/DownloadsScreen.tsx,src/screens/novel/components/ChapterItem.tsx,src/screens/novel/components/NovelScreenList.tsx,src/screens/settings/SettingsAppearanceScreen/SettingsAppearanceScreen.tsx,src/screens/settings/SettingsBackupScreen/Components/GoogleDriveModal.tsx,src/screens/updates/UpdatesScreen.tsx +5e62c36a9b0c0d7ca562383f2d9938e722216f00|feat: Add Library Settings And Category-Based Global Updates|src/database/queries/__tests__/NovelQueries.test.ts,src/database/queries/NovelQueries.ts,src/hooks/persisted/useSettings.ts,src/navigators/MoreStack.tsx,src/navigators/types/index.ts,src/screens/settings/SettingsGeneralScreen/SettingsGeneralScreen.tsx,src/screens/settings/SettingsLibraryScreen/DefaultCategoryDialog.tsx,src/screens/settings/SettingsScreen.tsx +3d102a52a7d780b096afd78c6d5214e46b670b47|perf(reader): Cut Chapter Open Latency and Reading-Time Re-Renders (#1899)|src/hooks/persisted/useSettings.ts,src/screens/reader/ChapterContext.tsx,src/screens/reader/components/ChapterDrawer/index.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderBottomSheet.tsx,src/screens/reader/components/ReaderFooter.tsx,src/screens/reader/components/WebViewReader.tsx,src/screens/reader/hooks/useChapter.ts,src/screens/reader/ReaderScreen.tsx,src/screens/reader/utils/sanitizeChapterText.ts,src/screens/WebviewScreen/WebviewScreen.tsx +4dcf21303715793087ea3bd49a9d510943e30944|fix: Restore Reader Changes Reverted During Expo Migration| +4de82472ee70fd0c36114f0ee2e213ff6bf81099|fix: Resolve Require Cycles And LegendList Render Updates|pnpm-lock.yaml,pnpm-workspace.yaml,src/components/SearchbarV2/SearchbarV2.tsx,src/screens/settings/SettingsRepositoryScreen/SettingsRepositoryScreen.tsx,src/services/backup/utils.ts +58db0352e6359d4b1c69391d260105abbe4ede60|fix: Restore Metro Reader Asset Middleware| +74808f086e5e3fbd479138681d700814c3370386|feat: Replace Legacy Media Controls with Nitro-Powered Playback (#1896)|babel.config.js,package.json,src/hooks/persisted/useSettings.ts,src/screens/reader/components/ReaderBottomSheet/TTSTab.tsx,src/screens/reader/components/WebViewReader.tsx,src/screens/reader/hooks/useChapter.ts,src/screens/reader/ReaderScreen.tsx,src/screens/settings/SettingsReaderScreen/Modals/VoicePickerModal.tsx,src/screens/settings/SettingsReaderScreen/SettingsReaderScreen.tsx,tsconfig.json +b96351ea8117f4a1e464b0a7ab1820a557172c92|docs: Fix Contraction and Missing Article in `TESTING.md` (#1895)| +eb8dc6df368ba7628178d6481294fca1b30f9019|docs: Fix "Withoud" Typo in `CONTRIBUTING-NIX.md` (#1894)|CONTRIBUTING-NIX.md +0c8546188eb33a0ac3be46596503cd38e4fb4d75|feat: Add MD3 Slider Component.|package.json,pnpm-lock.yaml,src/screens/reader/components/ReaderBottomSheet/ReaderValueChange.tsx,src/screens/reader/components/ReaderBottomSheet/TextSizeSlider.tsx,src/screens/reader/components/ReaderBottomSheet/TTSTab.tsx,src/screens/settings/SettingsReaderScreen/ReaderTextSize.tsx +e9f6bdaa203890fb7d69b261f31afc431fba429c|fix: Modernize Menu Styling And Outside-Tap Dismissal|src/components/Menu/index.tsx +3d34658d0faee3f1a1dce6ed77f745804851c4c0|feat: Add Material 3 Top Tab Indicators|src/screens/browse/BrowseScreen.tsx,src/screens/library/components/LibraryBottomSheet/LibraryBottomSheet.tsx,src/screens/library/LibraryScreen.tsx,src/screens/novel/components/NovelBottomSheet.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderBottomSheet.tsx,src/screens/settings/SettingsReaderScreen/components/TabBar.tsx,src/screens/settings/SettingsReaderScreen/SettingsReaderScreen.tsx +d44ce7412ac512b392d35e33d6dc9d98587a2350|docs: Fix Trailing Period, Organization Expansion, and Comma in `CONTRIBUTING.md` (#1893)| +17c891e1334409fa29d4a365b9ba46014b81b591|refactor: Standardize Bottom Sheet UX and Styling|src/components/BottomSheet/BottomSheet.tsx,src/hooks/persisted/useTheme.ts,src/screens/BrowseSourceScreen/components/FilterBottomSheet.tsx,src/screens/library/components/LibraryBottomSheet/LibraryBottomSheet.tsx,src/screens/library/LibraryScreen.tsx,src/screens/novel/components/NovelBottomSheet.tsx,src/screens/novel/components/PageNavigationBottomSheet.tsx,src/screens/novel/components/Tracker/TrackSheet.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderBottomSheet.tsx,src/screens/settings/SettingsReaderScreen/SettingsReaderScreen.tsx +9805da63cf23d58f0561b9335e0a07c1789e9a82|refactor: Optimize Novel Screen Performance And Architecture|src/screens/novel/components/EditInfoModal.tsx,src/screens/novel/components/ExportEpubModal.tsx,src/screens/novel/components/ExportNovelAsEpubButton.tsx,src/screens/novel/components/Info/NovelInfoComponents.tsx,src/screens/novel/components/Info/NovelInfoHeader.tsx,src/screens/novel/components/JumpToChapterModal.tsx,src/screens/novel/components/NovelAppbar.tsx,src/screens/novel/components/NovelBottomSheet.tsx,src/screens/novel/components/NovelScreenButtonGroup/NovelScreenButtonGroup.tsx,src/screens/novel/components/NovelScreenList.tsx,src/screens/novel/components/SetCategoriesModal.tsx,src/screens/novel/components/Tracker/ScoreSelectors.tsx,src/screens/novel/components/Tracker/TrackSheet.tsx,src/screens/novel/NovelScreen.tsx,src/services/migrate/migrateNovel.ts +afc20277e0f8a8d4b49d1ebd1c7689c599ba2e06|fix: Restore Android Build Types For CI|.gitignore,package.json +14061d207b77d8930c4d15af135189ccb4f4bd9e|docs: Fix Title Case Headings, GitHub Capitalization, and E.g. Formatting in `CONTRIBUTING.md` (#1892)| +916374b5df3cfa3b09d464109340ad81bdbef457|refactor: Reorganize Repository Structure|babel.config.js,package.json,scripts/generate-string-types.cjs,src/components/ColorPickerModal/ColorPickerModal.tsx,src/components/ConfirmationDialog/ConfirmationDialog.tsx,src/components/NewUpdateDialog.tsx,src/components/NovelCover.tsx,src/components/SearchbarV2/SearchbarV2.tsx,src/database/queries/CategoryQueries.ts,src/database/queries/ChapterQueries.ts,src/database/queries/NovelQueries.ts,src/navigators/BottomNavigator.tsx,src/screens/browse/BrowseScreen.tsx,src/screens/browse/discover/DiscoverCard.tsx,src/screens/browse/migration/Migration.tsx,src/screens/browse/migration/MigrationNovelList.tsx,src/screens/browse/settings/modals/ConcurrentSearchesModal.tsx,src/screens/browse/SourceNovels.tsx,src/screens/BrowseSourceScreen/components/FilterBottomSheet.tsx,src/screens/Categories/CategoriesScreen.tsx,src/screens/Categories/components/AddCategoryModal.tsx,src/screens/Categories/components/DeleteCategoryModal.tsx,src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx,src/screens/GlobalSearchScreen/GlobalSearchScreen.tsx,src/screens/history/components/ClearHistoryDialog.tsx,src/screens/history/components/HistoryCard/HistoryCard.tsx,src/screens/history/HistoryScreen.tsx,src/screens/library/components/LibraryBottomSheet/LibraryBottomSheet.tsx,src/screens/library/LibraryScreen.tsx,src/screens/more/About.tsx,src/screens/more/components/RemoveDownloadsDialog.tsx,src/screens/more/DownloadsScreen.tsx,src/screens/more/MoreScreen.tsx,src/screens/more/TaskQueueScreen.tsx,src/screens/novel/components/Chapter/ChapterDownloadButtons.tsx,src/screens/novel/components/ChapterItem.tsx,src/screens/novel/components/DownloadCustomChapterModal.tsx,src/screens/novel/components/EditInfoModal.tsx,src/screens/novel/components/ExportEpubModal.tsx,src/screens/novel/components/ExportNovelAsEpubButton.tsx,src/screens/novel/components/Info/NovelInfoComponents.tsx,src/screens/novel/components/Info/NovelInfoHeader.tsx,src/screens/novel/components/JumpToChapterModal.tsx,src/screens/novel/components/NovelAppbar.tsx,src/screens/novel/components/NovelBottomSheet.tsx,src/screens/novel/components/NovelScreenButtonGroup/NovelScreenButtonGroup.tsx,src/screens/novel/components/NovelScreenList.tsx,src/screens/novel/components/NovelSummary/NovelSummary.tsx,src/screens/novel/components/SetCategoriesModal.tsx,src/screens/novel/components/Tracker/TrackSearchDialog.tsx,src/screens/novel/NovelScreen.tsx,src/screens/onboarding/OnboardingScreen.tsx,src/screens/onboarding/ThemeSelectionStep.tsx,src/screens/reader/components/ChapterDrawer/index.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderBottomSheet.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderFontPicker.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderTextAlignSelector.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderThemeSelector.tsx,src/screens/reader/components/ReaderBottomSheet/TextSizeSlider.tsx,src/screens/reader/components/ReaderBottomSheet/TTSTab.tsx,src/screens/reader/components/WebViewReader.tsx,src/screens/reader/hooks/useChapter.ts,src/screens/reader/ReaderScreen.tsx,src/screens/reader/utils/sanitizeChapterText.ts,src/screens/settings/components/ConnectionModal.tsx,src/screens/settings/SettingsAdvancedScreen.tsx,src/screens/settings/SettingsAppearanceScreen/LanguagePickerModal.tsx,src/screens/settings/SettingsAppearanceScreen/SettingsAppearanceScreen.tsx,src/screens/settings/SettingsBackupScreen/Components/GoogleDriveModal.tsx,src/screens/settings/SettingsBackupScreen/Components/SelfHostModal.tsx,src/screens/settings/SettingsBackupScreen/index.tsx,src/screens/settings/SettingsGeneralScreen/modals/DisplayModeModal.tsx,src/screens/settings/SettingsGeneralScreen/modals/GridSizeModal.tsx,src/screens/settings/SettingsGeneralScreen/modals/NovelBadgesModal.tsx,src/screens/settings/SettingsGeneralScreen/modals/NovelSortModal.tsx,src/screens/settings/SettingsGeneralScreen/SettingsGeneralScreen.tsx,src/screens/settings/SettingsLibraryScreen/DefaultCategoryDialog.tsx,src/screens/settings/SettingsReaderScreen/Modals/FontPickerModal.tsx,src/screens/settings/SettingsReaderScreen/Modals/VoicePickerModal.tsx,src/screens/settings/SettingsReaderScreen/ReaderTextSize.tsx,src/screens/settings/SettingsReaderScreen/SettingsReaderScreen.tsx,src/screens/settings/SettingsReaderScreen/tabs/AccessibilityTab.tsx,src/screens/settings/SettingsReaderScreen/tabs/AdvancedTab.tsx,src/screens/settings/SettingsReaderScreen/tabs/DisplayTab.tsx,src/screens/settings/SettingsReaderScreen/tabs/NavigationTab.tsx,src/screens/settings/SettingsReaderScreen/tabs/ThemeTab.tsx,src/screens/settings/SettingsRepositoryScreen/components/AddRepositoryModal.tsx,src/screens/settings/SettingsRepositoryScreen/components/DeleteRepositoryModal.tsx,src/screens/settings/SettingsRepositoryScreen/components/RepositoryCard.tsx,src/screens/settings/SettingsRepositoryScreen/SettingsRepositoryScreen.tsx,src/screens/settings/SettingsScreen.tsx,src/screens/settings/SettingsTrackerScreen.tsx,src/screens/StatsScreen/StatsScreen.tsx,src/screens/updates/UpdatesScreen.tsx,src/screens/WebviewScreen/components/Menu.tsx,src/services/backup/local/index.ts,src/services/backup/utils.ts,tsconfig.json +0a2a6c96ca453fd68ce42d4723c0f42c0a730519|feat: Add Variant-Specific Android Splash Screens| +65e86100304ae27ba8049937558b4e25b49252f6|refactor: Replace Bottom Navigation Shift With Fade|src/navigators/BottomNavigator.tsx +1322065290f63141633c30cd3013b7ba81c12fae|feat: Add Variant-Specific Android App Icons| +22a1efd1e5c548a27b3ab3b8c54909a5092c1e37|perf: Optimize React Native Skeleton Loading Components|src/components/Skeleton/Skeleton.tsx,src/screens/browse/loadingAnimation/MalLoading.tsx,src/screens/browse/loadingAnimation/TrackerLoading.tsx,src/screens/history/components/HistorySkeletonLoading.tsx,src/screens/novel/components/Info/NovelInfoHeader.tsx,src/screens/novel/components/LoadingAnimation/NovelScreenLoading.tsx,src/screens/updates/components/UpdatesSkeletonLoading.tsx +269606ebb8e352f72fbab401bd5d93ed15c1a7a2|fix: Resolve Nitro EPUB Import Alias|tsconfig.json +4cb1890b62eb57cc75ca0628a11be312c930ddc4|feat: Add Nitro EPUB Exporter|.gitignore,package.json,pnpm-lock.yaml,pnpm-workspace.yaml,src/screens/novel/components/ExportEpubModal.tsx,src/screens/novel/components/ExportNovelAsEpubButton.tsx,strings/languages/en/strings.json,strings/types/index.ts +a421177f0cb5b74e30b869c6595ed12c9263586a|fix: Improve Category Badge Layout And Drag Reordering|src/components/IconButtonV2/IconButtonV2.tsx,src/screens/Categories/CategoriesScreen.tsx,src/screens/Categories/components/CategoryCard.tsx +a16ec876311692a6cb3da9348d09e7cf287c9fb6|perf: Optimize Novel Covers, Lists, and Chapter Groups|package.json,pnpm-lock.yaml,src/components/ErrorScreenV2/ErrorScreenV2.tsx,src/components/ListView.tsx,src/components/NovelCover.tsx,src/components/NovelList.tsx,src/database/queries/ChapterQueries.ts,src/database/types/index.ts,src/plugins/types/index.ts,src/screens/browse/discover/DiscoverNovelCard/index.tsx,src/screens/browse/globalsearch/GlobalSearchNovelCover.tsx,src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx,src/screens/history/components/HistoryCard/HistoryCard.tsx,src/screens/more/DownloadsScreen.tsx,src/screens/novel/components/ChapterItem.tsx,src/screens/novel/components/Info/NovelInfoComponents.tsx,src/screens/novel/components/Tracker/TrackSearchDialog.tsx,src/screens/StatsScreen/StatsScreen.tsx,src/screens/updates/components/UpdateNovelCard.tsx,src/screens/updates/UpdatesScreen.tsx +dfd620aa292fadd3b8798013701ae1bef954fe57|feat: Standardize Dialogs with Material 3 Patterns|src/components/ColorPickerModal/ColorPickerModal.tsx,src/components/ConfirmationDialog/ConfirmationDialog.tsx,src/components/DialogTitle/DialogTitle.tsx,src/components/NewUpdateDialog.tsx,src/hooks/persisted/useTheme.ts,src/screens/browse/migration/MigrationNovelList.tsx,src/screens/browse/settings/modals/ConcurrentSearchesModal.tsx,src/screens/Categories/components/AddCategoryModal.tsx,src/screens/Categories/components/DeleteCategoryModal.tsx,src/screens/history/components/ClearHistoryDialog.tsx,src/screens/history/HistoryScreen.tsx,src/screens/more/components/RemoveDownloadsDialog.tsx,src/screens/more/DownloadsScreen.tsx,src/screens/novel/components/DownloadCustomChapterModal.tsx,src/screens/novel/components/EditInfoModal.tsx,src/screens/novel/components/ExportEpubModal.tsx,src/screens/novel/components/JumpToChapterModal.tsx,src/screens/novel/components/SetCategoriesModal.tsx,src/screens/novel/components/Tracker/TrackSearchDialog.tsx,src/screens/reader/components/ReaderBottomSheet/TTSTab.tsx,src/screens/settings/components/ConnectionModal.tsx,src/screens/settings/SettingsAdvancedScreen.tsx,src/screens/settings/SettingsAppearanceScreen/LanguagePickerModal.tsx,src/screens/settings/SettingsBackupScreen/Components/GoogleDriveModal.tsx,src/screens/settings/SettingsBackupScreen/Components/SelfHostModal.tsx,src/screens/settings/SettingsGeneralScreen/modals/DisplayModeModal.tsx,src/screens/settings/SettingsGeneralScreen/modals/GridSizeModal.tsx,src/screens/settings/SettingsGeneralScreen/modals/NovelBadgesModal.tsx,src/screens/settings/SettingsGeneralScreen/modals/NovelSortModal.tsx,src/screens/settings/SettingsLibraryScreen/DefaultCategoryDialog.tsx,src/screens/settings/SettingsReaderScreen/Modals/FontPickerModal.tsx,src/screens/settings/SettingsReaderScreen/Modals/VoicePickerModal.tsx,src/screens/settings/SettingsReaderScreen/tabs/AdvancedTab.tsx,src/screens/settings/SettingsRepositoryScreen/components/AddRepositoryModal.tsx,src/screens/settings/SettingsRepositoryScreen/components/DeleteRepositoryModal.tsx,src/screens/settings/SettingsTrackerScreen.tsx +a5a6e8326f72d00271fd01cf8d6374cc35310147|chore: Update `@legendapp/list` to 3.3.3|package.json,pnpm-lock.yaml,src/screens/library/components/LibraryBottomSheet/LibraryBottomSheet.tsx,src/screens/novel/components/JumpToChapterModal.tsx,src/screens/novel/components/NovelDrawer.tsx,src/screens/novel/components/NovelScreenList.tsx,src/screens/novel/components/PageNavigationBottomSheet.tsx,src/screens/novel/NovelScreen.tsx,src/screens/onboarding/ThemeSelectionStep.tsx,src/screens/reader/components/ChapterDrawer/index.tsx,src/screens/settings/SettingsReaderScreen/Modals/VoicePickerModal.tsx +7f48a8ba8299180ffd5b07ba6314960213cd13cd|refactor: Improve Plugins Screen UX and Code|src/components/IconButtonV2/IconButtonV2.tsx,src/components/SearchbarV2/SearchbarV2.tsx,src/components/Switch/Switch.tsx,src/components/Switch/SwitchItem.tsx,src/hooks/persisted/index.ts,src/navigators/BottomNavigator.tsx,src/navigators/types/index.ts,src/screens/browse/BrowseScreen.tsx,src/screens/browse/components/AvailableTab.tsx,src/screens/browse/components/DeferredPluginListItem.tsx,src/screens/browse/components/InstalledTab.tsx,src/screens/browse/components/Modals/SourceSettings.tsx,src/screens/browse/components/PluginListItem.tsx,src/screens/browse/components/PluginListItemSkeleton.tsx,src/screens/browse/discover/DiscoverCard.tsx,src/screens/browse/migration/Migration.tsx,src/screens/browse/migration/MigrationNovels.tsx,src/screens/settings/SettingsRepositoryScreen/SettingsRepositoryScreen.tsx,strings/languages/en/strings.json,strings/types/index.ts +ae34d7b4ec6be28f7ad5ae25c663c7d2b3db43ef|feat: Add Dynamic Preview APK Metadata| +44c8e54ed80bb47cc49c2461adca66ced94b2d1c|feat: Add Dynamic Material Theme Colors (Closes #1863)|App.tsx,package.json,pnpm-lock.yaml,src/hooks/persisted/useTheme.ts,src/screens/onboarding/ThemeSelectionStep.tsx,src/screens/settings/SettingsAppearanceScreen/SettingsAppearanceScreen.tsx,strings/languages/en/strings.json,strings/types/index.ts +4a4208e33602d65ef9942d05e54a692addd87e2f|feat: Add Time-Tracking Toggle and Inactivity Timeout (#1890)|src/hooks/persisted/useSettings.ts,src/screens/reader/components/WebViewReader.tsx,src/screens/reader/hooks/useChapter.ts,src/screens/reader/ReaderScreen.tsx,src/screens/settings/SettingsGeneralScreen/SettingsGeneralScreen.tsx,strings/languages/en/strings.json,strings/types/index.ts +d2385b8dc2a9cd911e7567e55d6b7dc2c43f2372|Update build.yml| +9babe1676dd053069d127743a61bc7a8bc11256c|refactor: Remove React Native Background Actions Manifest Customization|package.json +8c38f499e4b62de02157aa8dcd1c9207978033bf|chore: Update AppState Test Setup And `pnpm` Configuration| +89c43c1eeffd5a9980b3aaebe647f3d94252f8b8|feat: Implement Time Tracking and Statistics (#1886)|src/database/queries/__tests__/StatsQueries.test.ts,src/database/queries/ChapterQueries.ts,src/database/queries/StatsQueries.ts,src/database/types/index.ts,src/screens/reader/components/WebViewReader.tsx,src/screens/reader/hooks/useChapter.ts,src/screens/StatsScreen/StatsScreen.tsx,strings/languages/en/strings.json,strings/types/index.ts +c3c891cea084796b3640233826dacfc7aab6f8d6|refactor: Migrate to Expo Managed Workflow (#1885)|.gitignore,android/app/build.gradle,android/app/proguard-rules.pro,android/app/src/main/AndroidManifest.xml,android/app/src/main/java/com/rajarsheechatterjee/LNReader/MainActivity.kt,android/app/src/main/java/com/rajarsheechatterjee/LNReader/MainApplication.kt,android/app/src/main/java/com/rajarsheechatterjee/NativeFile/NativeFile.kt,android/app/src/main/res/raw/loading.json,android/build.gradle,android/gradle.properties,android/gradle/wrapper/gradle-wrapper.properties,android/settings.gradle,App.tsx,babel.config.js,eslint.config.js,ios/LNReader/Images.xcassets/AppIcon.appiconset/Contents.json,ios/loading.json,package.json,pnpm-lock.yaml,pnpm-workspace.yaml,src/components/BottomTabBar/index.tsx,src/components/EmptyView/EmptyView.tsx,src/components/ErrorScreenV2/ErrorScreenV2.tsx,src/components/IconButtonV2/IconButtonV2.tsx,src/components/NovelCover.tsx,src/components/NovelList.tsx,src/components/SearchbarV2/SearchbarV2.tsx,src/database/queries/CategoryQueries.ts,src/database/queries/ChapterQueries.ts,src/database/queries/NovelQueries.ts,src/hooks/persisted/useNovel.ts,src/plugins/helpers/fetch.ts,src/screens/browse/BrowseScreen.tsx,src/screens/browse/components/PluginListItem.tsx,src/screens/browse/loadingAnimation/MalLoading.tsx,src/screens/browse/loadingAnimation/TrackerLoading.tsx,src/screens/library/hooks/useLibrary.ts,src/screens/novel/components/ExportNovelAsEpubButton.tsx,src/screens/novel/components/Info/NovelInfoHeader.tsx,src/screens/novel/components/NovelScreenList.tsx,src/screens/onboarding/ThemeSelectionStep.tsx,src/screens/reader/components/ReaderFooter.tsx,src/screens/reader/components/WebViewReader.tsx,src/screens/reader/hooks/useChapter.ts,src/screens/settings/SettingsAppearanceScreen/SettingsAppearanceScreen.tsx,src/screens/settings/SettingsBackupScreen/index.tsx,src/screens/settings/SettingsReaderScreen/tabs/AdvancedTab.tsx,src/services/backup/local/index.ts,src/services/backup/utils.ts,src/theme/utils/setBarColor.ts,strings/languages/en/strings.json,strings/types/index.ts,tsconfig.json +e4cf64a112dbfd9f9dd853abec99afbdc09f1460|refactor: Migrate Background Tasks to Native Android Scheduling (#1889)|android/app/build.gradle,android/app/src/main/AndroidManifest.xml,android/app/src/main/java/com/rajarsheechatterjee/LNReader/MainApplication.kt,android/app/src/main/java/com/rajarsheechatterjee/NativeFile/NativeFile.kt,App.tsx,package.json,pnpm-lock.yaml,src/database/db.ts,src/database/queries/ChapterQueries.ts,src/database/queries/NovelQueries.ts,src/hooks/index.ts,src/hooks/persisted/useNovel.ts,src/screens/browse/migration/MigrationNovelList.tsx,src/screens/library/hooks/useLibrary.ts,src/screens/library/LibraryScreen.tsx,src/screens/more/MoreScreen.tsx,src/screens/more/TaskQueueScreen.tsx,src/screens/novel/components/ExportNovelAsEpubButton.tsx,src/screens/novel/components/NovelScreenList.tsx,src/screens/reader/hooks/useChapter.ts,src/screens/settings/SettingsBackupScreen/Components/GoogleDriveModal.tsx,src/screens/settings/SettingsBackupScreen/Components/SelfHostModal.tsx,src/screens/settings/SettingsBackupScreen/index.tsx,src/screens/settings/SettingsReaderScreen/tabs/AdvancedTab.tsx,src/screens/updates/UpdatesScreen.tsx,src/services/backup/local/index.ts,src/services/backup/utils.ts,src/services/migrate/migrateNovel.ts,src/services/ServiceManager.ts +3e1e211098779264ba123121cc66bb48385fcf77|feat: Add Address Bar in WebView (#1870)|src/screens/WebviewScreen/components/Appbar.tsx,src/screens/WebviewScreen/WebviewScreen.tsx +02b6b9848fb3349373de09a4343b6d273605b842|fix: Prevent WebView Reset When Reader Settings Change (#1884)|src/screens/reader/components/WebViewReader.tsx +71da09ed444707586eee6fc63f7f2ee182bb2cda|feat(reader): Add Current Chapter Search (#1877)|android/app/src/main/assets/css/index.css,android/app/src/main/assets/js/core.js,src/screens/reader/components/ReaderAppbar.tsx,src/screens/reader/components/WebViewReader.tsx,src/screens/reader/hooks/useChapter.ts,src/screens/reader/ReaderScreen.tsx,strings/languages/en/strings.json,strings/types/index.ts +4404494ec9a57c3d2a8b1afb4734c9fc569773a2|fix(database): Repair Migration History For Scanlator Column|src/database/db.ts +98a2a1d725d51d333ea3969b74434b2917a92161|feat: Implement Scanlator Filtering (#1864)|App.tsx,src/components/Checkbox/Checkbox.tsx,src/components/NovelCover.tsx,src/components/Switch/Switch.tsx,src/components/Switch/SwitchItem.tsx,src/database/queries/ChapterQueries.ts,src/database/types/index.ts,src/plugins/types/index.ts,src/screens/history/components/HistoryCard/HistoryCard.tsx,src/screens/more/MoreScreen.tsx,src/screens/novel/components/ChapterItem.tsx,src/screens/novel/components/NovelAppbar.tsx,src/screens/novel/components/NovelBottomSheet.tsx,src/screens/novel/components/Tracker/TrackSearchDialog.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderBottomSheet.tsx,src/screens/reader/components/ReaderBottomSheet/TTSTab.tsx,src/screens/reader/components/WebViewReader.tsx,src/screens/reader/hooks/useChapter.ts,src/screens/reader/ReaderScreen.tsx,src/screens/settings/SettingsReaderScreen/SettingsReaderScreen.tsx,src/screens/settings/SettingsReaderScreen/tabs/AdvancedTab.tsx,strings/languages/en/strings.json,strings/types/index.ts +788f6cede7a5676a3d30cf0fd8e565cddd3c486c|fix: Prevent Table Overflow and Clean Up CSS (#1876) (#1876)|android/app/src/main/assets/css/index.css +7abee71ac33ab4b16c0c2a629640b1a7c2f5eb3b|test: Generate Debug Environment Variables In Testing Workflow| +52d5c99c46b14148bd262c878701abf91c8b593b|fix: Order EPUB Chapters Across Paginated Pages (Closes #1762)|src/database/queries/ChapterQueries.ts +93bc5e5e4bac14fea10913cb215a7d9c297d6c31|fix: Handle Missing MAL List Entries and Null Progress (Closes #1875)| +5d996f13883219090ad39ddbe7b52e88e36107c7|feat(downloads): Make Chapter Download Cooldown Configurable (#1834)|src/hooks/persisted/useSettings.ts,src/screens/settings/SettingsGeneralScreen/SettingsGeneralScreen.tsx,strings/languages/en/strings.json +bbb2645a6f6c47f7cda8d83ef096c329e03ffa6a|chore: Clean Reader Preview Content (#1867)| +54fedc2f531ecdda29dea74f3cc279a7a4525261|fix(tts): Remove Leading and Trailing Quotes from Normalized Text (#1869)|android/app/src/main/assets/js/core.js +e8c5550256dfe06c5ae12c106801e8688173a830|perf: Optimize Regex for Removing Extra Paragraph Spacing (#1872)|android/app/src/main/assets/js/core.js +57b9d41b5c3eacc9cbf5e38082c4efb2a1a11f5e|fix(service): Correct Notification Throttle Logic in ServiceManager.setMeta (#1873)|src/services/ServiceManager.ts +34be7a123499cc7733161482fdf5d42e4c1bae51|fix(native-file): prevent fatal crash when unlinking missing files (#1832)|android/app/src/main/java/com/rajarsheechatterjee/NativeFile/NativeFile.kt +60f42ed6243dc5d4daa68c727d60c9fb5dd6576a|chore(build): patch @rock-js/plugin-metro for Windows compatibility (#1830)|android/settings.gradle,pnpm-lock.yaml,pnpm-workspace.yaml +25c93afabe24d34f2130ee0fd7e0846a9b4861ff|fix(reader): restore saved chapter progress from db if available (#1850)|src/screens/reader/hooks/useChapter.ts +2cd5a97ee8e482e92b45e9b702c9ba974e06a092|fix: await database operations inside transactions (#1845)|src/database/queries/CategoryQueries.ts,src/database/queries/ChapterQueries.ts,src/database/queries/NovelQueries.ts +8f47e8fd1e1aedaf22df8275484f27b5d40b2a39|Theme switcher refactor (#1839)|App.tsx,package.json,pnpm-lock.yaml,src/components/BottomTabBar/index.tsx,src/components/ColorPickerModal/ColorPickerModal.tsx,src/components/List/List.tsx,src/components/SegmentedControl/SegmentedControl.tsx,src/components/ThemePicker/ThemePicker.tsx,src/hooks/persisted/index.ts,src/hooks/persisted/useTheme.ts,src/screens/onboarding/ThemeSelectionStep.tsx,src/screens/settings/SettingsAppearanceScreen/LanguagePickerModal.tsx,src/screens/settings/SettingsAppearanceScreen/SettingsAppearanceScreen.tsx,src/theme/utils/setBarColor.ts +eafa545b2a34320b5e1eb2d99ba61b6888838da8|Use novel refactor (#1838)|package.json,pnpm-lock.yaml,src/database/db.ts,src/database/queries/__tests__/NovelQueries.test.ts,src/database/queries/ChapterQueries.ts,src/database/queries/NovelQueries.ts,src/hooks/persisted/index.ts,src/hooks/persisted/useNovel.ts,src/hooks/persisted/useTheme.ts,src/screens/novel/components/ChapterItem.tsx,src/screens/novel/components/Info/NovelInfoHeader.tsx,src/screens/novel/components/JumpToChapterModal.tsx,src/screens/novel/components/NovelBottomSheet.tsx,src/screens/novel/components/NovelScreenList.tsx,src/screens/novel/components/PageNavigationBottomSheet.tsx,src/screens/novel/NovelScreen.tsx,src/screens/reader/components/ChapterDrawer/index.tsx,src/screens/reader/components/ReaderAppbar.tsx,src/screens/reader/components/ReaderFooter.tsx,src/screens/reader/hooks/useChapter.ts,src/screens/settings/SettingsAdvancedScreen.tsx,src/screens/settings/SettingsReaderScreen/tabs/NavigationTab.tsx,src/screens/updates/UpdatesScreen.tsx,src/services/migrate/migrateNovel.ts,strings/languages/en/strings.json,strings/types/index.ts +b680a07750f0b7650836c0573566e641d4aaa679|fix(browse): use gesture-handler Pressable in PluginListItem (#1828)|src/screens/browse/components/PluginListItem.tsx +2a919ec0f963a4d4613e3e0ebe99a16fa7e2dbe3|chore: gitignore tsconfig.tsbuildinfo build cache (#1829)|.gitignore +01596c045a4637acf056f75cf87ffc0198d79899|fix: Apply active filters to chapter count display (Closes #1793) (#1797)|src/database/queries/ChapterQueries.ts,src/hooks/persisted/useNovel.ts +3de93c5c093ab2fb7e0d509e367e4fec813082e2|feat: migrate from rn community cli to rock (#1812)|.gitignore,android/app/build.gradle,android/settings.gradle,babel.config.js,package.json,pnpm-lock.yaml,src/screens/more/About.tsx,src/screens/novel/components/Info/NovelInfoHeader.tsx,tsconfig.json +306f5b27e2716f1a963fd992c06f5c961f44c926|fix: install the right version of hermes compiler|package.json,pnpm-lock.yaml +06f0f66e3e4659db14127b1b56202dd3a8fca521|fix: Add hermes-compiler dev dep|package.json,pnpm-lock.yaml +64921a054e9d4a11a0aad294b9703a3ab4c16b9f|feat: Expo 55 upgrade|.gitignore,android/app/src/main/AndroidManifest.xml,android/app/src/main/java/com/rajarsheechatterjee/LNReader/MainApplication.kt,android/build.gradle,android/gradle/wrapper/gradle-wrapper.properties,android/settings.gradle,babel.config.js,package.json,pnpm-lock.yaml,src/components/BottomSheet/BottomSheet.tsx,src/components/List/List.tsx,src/database/db.ts,src/hooks/persisted/useNovel.ts,src/plugins/helpers/storage.ts,src/screens/Categories/components/CategoryCard.tsx,src/screens/more/About.tsx,src/screens/novel/components/ChapterItem.tsx,src/screens/novel/components/Info/NovelInfoHeader.tsx,src/screens/novel/components/JumpToChapterModal.tsx,src/screens/novel/components/NovelScreenList.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderSheetPreferenceItem.tsx,src/screens/settings/SettingsAdvancedScreen.tsx +44e035ad38b25f79741199b6aea64479831ccc14|chore: Update Translations (#1683)| +1803cd071d24a4d8ada8c55814a6ce62eaf98481|feat: Add testing support for react code (#1763)|__mocks__/react-native-mmkv.js,babel.config.js,package.json,pnpm-lock.yaml,src/hooks/persisted/useNovel.ts,src/services/migrate/migrateNovel.ts,tsconfig.json +e4d7ba306b4441b4743ab9edb7c96751d62e3cde|fix: Safety level may not be changed inside a transaction error (#1758)|src/database/db.ts +cc0428752b2e9869677a6bd2dccc449f7a98a552|feat: Replace TTS Notifications with Android MediaSession Controls (Closes #1711)|android/app/build.gradle,android/app/src/main/assets/js/core.js,android/app/src/main/java/com/rajarsheechatterjee/LNReader/MainApplication.kt,App.tsx,src/screens/reader/components/WebViewReader.tsx +b8c177bc55016b5a8fb96cc120e8ac050a2f7c73|refactor: Migrate to Drizzle and `op-sqlite` (#1735)|.gitignore,App.tsx,babel.config.js,package.json,pnpm-lock.yaml,pnpm-workspace.yaml,src/components/ListView.tsx,src/components/NovelCover.tsx,src/database/db.ts,src/database/migrations/002_add_novel_counters.ts,src/database/migrations/index.ts,src/database/queries/__tests__/CategoryQueries.test.ts,src/database/queries/__tests__/HistoryQueries.test.ts,src/database/queries/__tests__/NovelQueries.test.ts,src/database/queries/__tests__/RepositoryQueries.test.ts,src/database/queries/__tests__/StatsQueries.test.ts,src/database/queries/CategoryQueries.ts,src/database/queries/ChapterQueries.ts,src/database/queries/NovelQueries.ts,src/database/queries/StatsQueries.ts,src/database/tables/ChapterTable.ts,src/database/types/index.ts,src/database/utils/migrationRunner.ts,src/hooks/common/useDatabaseInitialization.ts,src/hooks/common/useGithubUpdateChecker.ts,src/hooks/index.ts,src/hooks/persisted/useNovel.ts,src/hooks/persisted/useSettings.ts,src/navigators/types/index.ts,src/screens/browse/components/AvailableTab.tsx,src/screens/BrowseSourceScreen/components/FilterBottomSheet.tsx,src/screens/Categories/CategoriesScreen.tsx,src/screens/Categories/components/AddCategoryModal.tsx,src/screens/Categories/components/CategoryCard.tsx,src/screens/history/HistoryScreen.tsx,src/screens/library/hooks/useLibrary.ts,src/screens/library/LibraryScreen.tsx,src/screens/novel/components/ChapterItem.tsx,src/screens/novel/components/EditInfoModal.tsx,src/screens/novel/components/Info/NovelInfoHeader.tsx,src/screens/novel/components/NovelBottomSheet.tsx,src/screens/novel/components/NovelScreenList.tsx,src/screens/novel/components/SetCategoriesModal.tsx,src/screens/novel/NovelScreen.tsx,src/screens/reader/components/ChapterDrawer/index.tsx,src/screens/reader/hooks/useChapter.ts,src/screens/reader/ReaderScreen.tsx,src/screens/settings/SettingsAdvancedScreen.tsx,src/screens/settings/SettingsGeneralScreen/SettingsGeneralScreen.tsx,src/screens/settings/SettingsRepositoryScreen/components/DeleteRepositoryModal.tsx,src/screens/settings/SettingsRepositoryScreen/SettingsRepositoryScreen.tsx,src/screens/settings/SettingsTrackerScreen.tsx,src/screens/updates/components/UpdateNovelCard.tsx,src/services/backup/utils.ts,src/services/migrate/migrateNovel.ts,src/services/Trackers/mangaUpdates.ts,tsconfig.json +7083c51dea1be0af3bb0d53c341f19b32a35dca4|fix: Include selectedIds in Chapter List extraData for Selection Highlight (Closes #1750)|src/screens/novel/components/NovelScreenList.tsx +f0693204e1f8f961dff85b195f3c7f4f6f9bb0ce|fix: Improve Next Chapter Selection Logic (#1744)|src/database/queries/ChapterQueries.ts +3f8071e48ee603f906cc9e3910abe90d6e3874a5|chore: Release v2.0.3|android/app/build.gradle,package.json +a1c5350e8e330e783b6537c366351c2082e99150|fix: Miscellaneous UI Tweaks|src/components/List/List.tsx,src/screens/Categories/components/CategoryCard.tsx,src/screens/library/LibraryScreen.tsx,src/screens/novel/components/Info/NovelInfoComponents.tsx,src/screens/novel/components/Info/NovelInfoHeader.tsx,src/screens/updates/components/UpdateNovelCard.tsx +8f237909d54310220d518000ace81b5fddbc3b5f|feat: Show First Unread Chapter in ReadButton and FAB When No Last Read|src/database/queries/ChapterQueries.ts,src/hooks/persisted/useNovel.ts,src/screens/novel/components/Info/NovelInfoHeader.tsx,src/screens/novel/components/NovelScreenList.tsx +11ff69d4f5b4a79286247ac6c40ca1eac72ad283|fix: Pre-Fetch Adjacent Page Chapters in Reader and Library Updates|src/components/BottomSheet/BottomSheet.tsx,src/screens/reader/hooks/useChapter.ts +d15cf987425709f97ec168a46af8d936e944c29e|fix: Order Chapter Pages Numerically in Queries for Accurate Retrieval (Closes #1742)|src/database/queries/ChapterQueries.ts +29bd6612e9a9950c61b9a569b761ffa1aaa5d4eb|refactor: Optimize Library Screen Performance and Fix Category Bug|src/components/NovelCover.tsx,src/components/NovelList.tsx,src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx,src/screens/library/components/LibraryBottomSheet/LibraryBottomSheet.tsx,src/screens/library/hooks/useLibrary.ts,src/screens/library/LibraryScreen.tsx +ab1e36d280768be46acdb57557e09b3e2d37f12b|refactor: Update Components to Follow Material Design 3 Guidelines|src/components/Actionbar/Actionbar.tsx,src/components/BottomSheet/BottomSheet.tsx,src/components/BottomTabBar/index.tsx,src/components/Chip/Chip.tsx,src/components/ConfirmationDialog/ConfirmationDialog.tsx,src/components/IconButtonV2/IconButtonV2.tsx,src/components/List/List.tsx,src/components/Menu/index.tsx,src/components/SearchbarV2/SearchbarV2.tsx,src/components/SegmentedControl/SegmentedControl.tsx,src/components/Switch/Switch.tsx,src/components/Switch/SwitchItem.tsx,src/screens/more/MoreScreen.tsx,src/screens/novel/components/JumpToChapterModal.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderSheetPreferenceItem.tsx +f1fdafd37f1f7d0816484c2e414e39d9885499d8|fix: Prevent Tab Bar Height Jump and Shadow Flash on Browse Screen|src/screens/browse/BrowseScreen.tsx +4c7684a3a71e57fb27f910b3edfbdd902353dd47|refactor: Optimize Novel Screen Performance and Fix Download State Tracking|.gitignore,src/hooks/persisted/useNovel.ts,src/screens/novel/components/Chapter/ChapterDownloadButtons.tsx,src/screens/novel/components/ChapterItem.tsx,src/screens/novel/components/Info/NovelInfoComponents.tsx,src/screens/novel/components/Info/NovelInfoHeader.tsx,src/screens/novel/components/NovelAppbar.tsx,src/screens/novel/components/NovelScreenList.tsx,src/screens/novel/components/PagePaginationControl.tsx,src/screens/novel/NovelScreen.tsx,src/screens/updates/components/UpdateNovelCard.tsx +2f45db659b2fdfa77f9213023d9d2707cbc452e1|fix: Replace Sync Database Operations With Async to Prevent Database Lock (Closes #1716)|src/database/queries/ChapterQueries.ts,src/database/queries/NovelQueries.ts,src/hooks/persisted/useNovel.ts,src/screens/library/hooks/useLibrary.ts,src/screens/library/LibraryScreen.tsx,src/screens/novel/components/Chapter/ChapterDownloadButtons.tsx,src/services/migrate/migrateNovel.ts +9e38ad114d182d39880a962cb7acefa55235310d|fix: Sanitize EPUB FileName to Remove Filesystem-Unsafe Characters (Closes #1480)|src/screens/novel/components/ExportNovelAsEpubButton.tsx +45c4ea8ca02031c803224b0d906a6045364d2817|fix: Handle IOException in NativeFile to Prevent EPUB Import Crash Loop (Closes #1315)|android/app/src/main/java/com/rajarsheechatterjee/NativeFile/NativeFile.kt +d2b95ae3166f337d8d3597e1cc9a0bff65832d62|fix: Use NativeFile for Custom CSS/JS Import to Fix Content URI Read Failure (Closes #1661)|src/screens/settings/SettingsReaderScreen/tabs/AdvancedTab.tsx +02121152b3e3cdbbaaff8a34b51cb8e9b2010538|fix: Stabilize Scroll Handler References to Prevent Infinite Re-render Loop (Closes #1679)|src/screens/browse/discover/MalTopNovels.tsx,src/screens/novel/components/NovelScreenList.tsx +caed965b6bad1b788eee3035daba42322454fe26|fix: Prevent EPUB Import Crash from DocumentPicker Cache Cleanup (Closes #1725)|src/screens/library/LibraryScreen.tsx +4088d5966bbc5ec27801b1ddc553f5621b4b6d79|fix: Close OkHttp Response and Streams in NativeFile.downloadFile (Closes #1740)|android/app/src/main/java/com/rajarsheechatterjee/NativeFile/NativeFile.kt +a062beee87d4173383e3033eeacb9376642645e5|fix: Add Max Height to Set Categories Modal (Closes #1738)|src/screens/novel/components/SetCategoriesModal.tsx +30958825162fbba2f7b805de6d41ab8e9183f28b|Replace react-native-paper menus with custom implementation (#1733)|src/components/Menu/index.tsx,src/screens/browse/components/Modals/SourceSettings.tsx,src/screens/BrowseSourceScreen/components/FilterBottomSheet.tsx,src/screens/more/TaskQueueScreen.tsx,src/screens/novel/components/Chapter/ChapterDownloadButtons.tsx +4ad06397960f1fbc633604ef06c2dfca193a220e|Fix: Chapter loading for jump to chapter modal and chapter drawer|pnpm-lock.yaml,src/database/queries/ChapterQueries.ts,src/hooks/persisted/useNovel.ts,src/screens/novel/components/JumpToChapterModal.tsx,src/screens/novel/NovelScreen.tsx,src/screens/reader/components/ChapterDrawer/index.tsx +b53ec3352f8ce9084e32ddaa352526b39ec6003c|feat: Support Additional Plugin Setting Types (Dropdown and Checkboxes)|src/plugins/types/index.ts,src/screens/browse/components/Modals/SourceSettings.tsx +c40edd5b2cc68c2b746ba588aa39e142ab99f01e|feat: Add Kitsu Tracker (Closes #416)|src/screens/novel/components/Tracker/ScoreSelectors.tsx,src/screens/novel/components/Tracker/TrackerCards.tsx,src/screens/settings/SettingsTrackerScreen.tsx +bdd42caaaf3c3d5961d444aabc9150057d2de5d9|chore: Update Repository Links to Use lneader Instead of LNReader|README.md,src/screens/more/About.tsx,strings/languages/en/strings.json +ab4eee31ceb82c3f89e459279bef8b56de5ec10c|feat: Allow All Safe Tags and Attributes When Sanitizing Chapter HTML (Closes #1715)|src/screens/reader/utils/sanitizeChapterText.ts +3f9886a2e5300bb9913888872847727ab63597fc|chore: Update Repository Links to Use lneader Instead of LNReader|README.md,src/screens/more/About.tsx,strings/languages/en/strings.json,strings/types/index.ts +198b7443ae9c0ee7cc3ff608c0fc89a88880fa84|feat: Add RTL (Right-to-Left) Language Support (#1717)|src/components/Checkbox/Checkbox.tsx,src/components/Chip/Chip.tsx,src/components/ColorPreferenceItem/ColorPreferenceItem.tsx,src/components/List/List.tsx,src/components/ListView.tsx,src/components/Menu/index.tsx,src/components/RadioButton.tsx,src/components/RadioButton/RadioButton.tsx,src/components/SearchbarV2/SearchbarV2.tsx,src/components/Switch/SwitchItem.tsx,src/components/ThemePicker/ThemePicker.tsx,src/hooks/common/useGithubUpdateChecker.ts,src/navigators/BottomNavigator.tsx,src/screens/browse/components/PluginListItem.tsx,src/screens/browse/components/PluginListItemSkeleton.tsx,src/screens/browse/migration/MigrationSourceItem.tsx,src/screens/BrowseSourceScreen/components/FilterBottomSheet.tsx,src/screens/Categories/components/CategoryCard.tsx,src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx,src/screens/history/components/HistoryCard/HistoryCard.tsx,src/screens/library/hooks/useLibrary.ts,src/screens/library/LibraryScreen.tsx,src/screens/novel/components/ChapterItem.tsx,src/screens/novel/components/NovelAppbar.tsx,src/screens/novel/components/PageNavigationBottomSheet.tsx,src/screens/novel/components/PagePaginationControl.tsx,src/screens/novel/components/Tracker/TrackSearchDialog.tsx,src/screens/novel/components/Tracker/TrackSheet.tsx,src/screens/reader/components/ChapterDrawer/index.tsx,src/screens/reader/components/ReaderAppbar.tsx,src/screens/reader/components/ReaderBottomSheet/ReaderFontPicker.tsx,src/screens/reader/components/ReaderBottomSheet/TTSTab.tsx,src/screens/reader/components/WebViewReader.tsx,src/screens/reader/ReaderScreen.tsx,src/screens/settings/SettingsReaderScreen/components/TabBar.tsx,src/screens/settings/SettingsReaderScreen/SettingsReaderScreen.tsx,src/screens/settings/SettingsReaderScreen/tabs/AdvancedTab.tsx,src/screens/settings/SettingsTrackerScreen.tsx,src/screens/updates/components/UpdateNovelCard.tsx +f9c5ca303d71830811a203f98a17c3dd4846f887|chore: Update Indonesian Translations (Closes #1653)| +9922d4394058a6da29ab2789cd8d7b75a37bf1bc|fix: Remove Missing Image References During EPUB Export (Closes #1696)|src/screens/novel/components/ExportNovelAsEpubButton.tsx +ae99b077d4e883e678b31ba0235a7abae88e1e94|feat: Add `@noble/ciphers` for Plugins (Closes #1709)|package.json,pnpm-lock.yaml +0cb9da90276c6a30d5d917557195f0f0b0ebc70c|refactor: Replace `withTransactionAsync` With `withExclusiveTransactionAsync` for Improved Concurrency Handling (Closes #1636)|src/database/queries/CategoryQueries.ts,src/database/queries/NovelQueries.ts,src/database/queries/StatsQueries.ts,src/database/utils/helpers.tsx,src/services/migrate/migrateNovel.ts diff --git a/specs/upstream-merge-analysis-2026-08/reference/fork_context.md b/specs/upstream-merge-analysis-2026-08/reference/fork_context.md new file mode 100644 index 0000000000..d3c4a1b93d --- /dev/null +++ b/specs/upstream-merge-analysis-2026-08/reference/fork_context.md @@ -0,0 +1,45 @@ +# Fork Architecture Context (for merge-value analysis) + +## Fork (branch `dev`, HEAD 40c6676797 v2.1.3, 2026-08-03) +- React Native 0.82.1, React 19.1.1, Expo SDK 54 (bare workflow w/ custom Kotlin native modules) +- **TTS (fork's crown jewel, heavily customized)**: + - WebViewReader.tsx (controller), useTTSController.ts (state machine), TTSAudioManager.ts, TTSState.ts, ttsBridge.ts + - Native: TTSHighlightModule.kt, TTSForegroundService.kt (custom Kotlin, NOT Nitro) + - Features: background playback, per-paragraph highlight, sleep timer, smart rewind, text cleanup pipeline (htmlParagraphExtractor.ts), per-novel TTS overrides, 5-button media notification (ttsNotification.ts), chapter list live progress sync +- **Database**: expo-sqlite `SQLite.openDatabaseSync` + raw SQL table files (src/database/tables/*) + custom MigrationRunner (src/database/utils/migrationRunner.ts) + migrations/ β€” NOT Drizzle +- **Settings**: MMKV via src/hooks/persisted/useSettings.ts (AppSettings interface, ChapterGeneralSettings/ChapterReaderSettings) +- **Reader UI**: WebView (core.js at android/app/src/main/assets/js/core.js), ReaderBottomSheetV2 β†’ ReaderTTSTab.tsx (custom) +- **UI lib**: react-native-paper 5.14.5 +- **Plugins**: src/plugins/pluginManager.ts dynamic loading, plugin types in src/plugins/types/ +- **Build**: Gradle 9.2.0, Java 17, pnpm; scripts dev:start / dev:android + +## Upstream (`original` = upstream/master, c3260e8e01, 2026-08-01) +- Migrated to **Expo managed workflow** (#1885), Expo 55, rock CLI (#1812) +- **Database**: **Drizzle ORM + op-sqlite** (#1735) β€” completely different layer (src/database/schema, manager/) +- **TTS**: **Nitro-powered playback** (#1896), Android **MediaSession** controls (#1711) β€” different native stack +- **Background tasks**: native Android scheduling (#1889, replaced react-native-background-actions) +- Repository reorganized (916374b5df) + +## Fork-critical files (heavily rewritten in fork β€” upstream changes here are RED risk) +- src/screens/reader/components/WebViewReader.tsx +- src/screens/reader/hooks/useTTSController.ts, useTTSUtilities.ts +- src/services/TTSAudioManager.ts, TTSState.ts, ttsBridge.ts, ttsNotification.ts +- src/hooks/persisted/useSettings.ts +- src/database/* (db.ts, tables/, queries/, utils/migrationRunner.ts) +- src/screens/reader/components/ReaderBottomSheet/ (fork: ReaderTTSTab.tsx; upstream file structure differs) +- android/app/src/main/assets/js/core.js, index.js +- android/app/src/main/java/com/rajarsheechatterjee/LNReader/*.kt +- android/app/build.gradle, package.json + +## Prior analysis (memory bank 2025-12-30) +Full merge infeasible (100+ conflicts). RECOMMENDED: selective cherry-pick / manual port of beneficial upstream features. + +## Assessment rules +For each commit classify: +- VALUE: HIGH (clear user benefit, missing in fork) / MEDIUM (nice-to-have) / LOW / NONE (chore/docs/infra) / ALREADY-HAVE (fork has own equivalent e.g. DoH, media notification, volume buttons, text cleanup, migration recovery) +- RISK if cherry-picked directly: GREEN (new files or files fork didn't touch) / YELLOW (some overlap, localized, portable) / RED (touches fork-critical files or depends on Drizzle/Nitro/Expo55/rock infrastructure) +- Portability: DIRECT (cherry-pick clean) / MANUAL (needs adaptation to fork architecture) / NOT-FEASIBLE (depends on upstream-only infra) +- One-line rationale each. + +Use `git show --stat` / `git show ` (read-only) when you need diff details. +Output as a markdown report with a table per commit + group summary (top valuable, notable risks). diff --git a/specs/upstream-merge-analysis-2026-08/reference/subagent/group1-report.md b/specs/upstream-merge-analysis-2026-08/reference/subagent/group1-report.md new file mode 100644 index 0000000000..04f0dfde65 --- /dev/null +++ b/specs/upstream-merge-analysis-2026-08/reference/subagent/group1-report.md @@ -0,0 +1,91 @@ +# Group 1 Upstream Commit Analysis β€” Selective Adoption for LNReader Fork (bizzkoot/lnreader) + +**Analysis date:** 2026-08-03 Β· **Branch:** dev @ 40c667679 (v2.1.3) Β· **Upstream analyzed:** original/master @ c3260e8e0 (2026-08-01) +**Method:** read-only `git show --stat` / `git show` of all 43 commits + direct inspection of fork's current source to verify ALREADY-HAVE, overlap, and bug-parity. + +**Headline verification result:** Several upstream "fix" commits were made against code that the fork still carries **byte-for-byte identical to the upstream pre-fix state** (verified: SelectableChip.tsx, NovelInfoHeader getStatusIcon, NovelStatus enum, deleteDownloads/deleteReadChaptersFromDb in ChapterQueries.ts, Category `sort = 1` lookups, NovelTable triggers `MAX(updatedTime)`). These fixes port trivially with high confidence. + +--- + +## Per-commit assessment (43 commits) + +| commit (short) | subject | VALUE | RISK | PORT | rationale | +|---|---|---|---|---|---| +| c3260e8e0 | fix: Resolve Chapter Download Queue and Removal Issues (#1937) | **HIGH** | YELLOW | MANUAL | Fork's `deleteDownloads` (src/database/queries/ChapterQueries.ts:167-173) has the exact pre-fix bugs: un-awaited `deleteDownloadedFiles`, and `UPDATE Chapter SET isDownloaded = 0` **without WHERE** (marks every chapter undownloaded). Worse, fork's `deleteReadChaptersFromDb` (line 177-184) passes `chapter.novelId` as the chapter id β†’ **deletes the wrong folder**. Port the DB/file-deletion fixes (WHERE id IN, await Promise.all, correct chapter.id) + undownloaded-chapter ordering to raw SQL. Skip upstream's backgroundTasks/taskDefinitions/useDownloadReconciliation parts (fork uses ServiceManager + MMKV queue). | +| c0877a9b0 | feat: Add Ignorable App Update Notifications | **HIGH** | YELLOW | MANUAL | Long-requested (upstream #600/#1632): lets users permanently dismiss an update prompt. Fork's NewUpdateDialog has no dismiss option. Fork's `useGithubUpdateChecker.ts` is a near-clone of upstream's deleted hook (fork points at bizzkoot repo). Port AppUpdateChecker/useAppUpdateChecker + "ignore this version" MMKV persistence; keep fork's repo URL/asset picker (`githubReleaseUtils.pickApkAsset`). | +| 96fb52df1 | feat: Enhance Stats With Genre Taxonomy and Charts (#1919) | **HIGH** | RED | MANUAL | Major user-facing upgrade: StatsScreen tabs (Overview/Time/Plugins), SVG donut charts, genre taxonomy screen with per-genre novel carousels (31 files, +2939/-719). Requires new dep `react-native-svg` (fork lacks it) and a rewrite of fork's raw-SQL StatsQueries.ts (upstream part is Drizzle). UI components are new files (low conflict), but stats queries + i18n key types + navigators must be adapted. High effort, high payoff. | +| 76106317b | fix: Restore Midnight Dusk Chip Contrast (#1939) | MEDIUM | GREEN | DIRECT | 1-line deletion (`selectedColor={theme.primary}`) in SelectableChip.tsx. Fork's file is **byte-identical** to upstream pre-fix (verified). Cherry-picks cleanly. Cosmetic bugfix for dark theme chip contrast. | +| e75f4fba5 | feat: Add More Novel Statuses and Icons (#1931) | MEDIUM | YELLOW | MANUAL | Adds `STUB` + `Inactive` statuses and full statusβ†’icon map. Fork's NovelStatus enum + `getStatusIcon` (NovelInfoHeader.tsx:72-80) match upstream pre-fix exactly β€” small localized edits. Skip `.husky/pre-push` hunk (fork has own hook file). | +| 4dc933ee5 | fix: Remove White Flash During Screen Transitions (#1926) (#1927) | MEDIUM | YELLOW | MANUAL | ThemedRootView paints root with theme background; ReaderStack gives Chapter screen reader background. Fork App.tsx has same GestureHandlerRootView structure (line 115). The `plugins/withAndroidCustomizations.js` config-plugin hunk is upstream-only (fork is bare workflow, no `plugins/` dir) β†’ replace with a native `android/app/src/main/res` windowBackground change. | +| bfb12b04a | feat: Add Configurable Automatic Backup Scheduling | **HIGH** | RED | NOT-FEASIBLE | Automatic scheduled backups = real user value (fork backup is manual-only). But native side is upstream-only infra: BackgroundTaskScheduler.kt / AutomaticBackupScheduler.kt / expo `native-file` module changes. `useSettings.ts` addition is additive (4 new fields) but the scheduler has no fork equivalent (fork uses react-native-background-actions + ServiceManager). Would need a from-scratch reimplementation to port. | +| b7639ab55 | feat: Upload JavaScript Source Maps (#1924) | LOW | YELLOW | MANUAL | Dev-facing crash-debugging aid (source map upload + AppErrorBoundary changes). Fork has its own AppErrorBoundary.tsx and different build/CI (Gradle 9.2.0, own workflows). Low value; not recommended. | +| ecc5b84a6 | chore: Release v2.1.2 | NONE | GREEN | NOT-FEASIBLE | Version bump + issue-template text. Fork is on its own v2.1.3 numbering. Skip. | +| eb12bdfd6 | fix: Prevent Novel Genre Rendering Crashes | **HIGH** | YELLOW | MANUAL | Crash fix: `parseGenres()` helper (splits on comma, handles null/empty) applied to EditInfoModal + NovelInfoComponents. Fork's components use the same fragile `genres?.split(',')` pattern (verified) β†’ same crash risk on malformed genre data. Port util + hardened components. | +| 9612bf6f8 | chore: Update Translations (#1928) | LOW | YELLOW | MANUAL | 35 language JSON updates (Crowdin). Fork stores translations at `strings/languages/` (vs upstream `src/i18n/languages/`) β†’ mechanical merge, not cherry-pickable. Only worthwhile if fork cares about non-English parity; conflicts with fork-local string keys possible. | +| 0ed7d878b | fix: Prevent White Reader Drawer Seam | LOW | YELLOW | MANUAL | 2-line fix: `drawerStyle={{backgroundColor:'transparent'}}` on reader Drawer. ReaderScreen.tsx is fork-critical but the addition is a tiny isolated hunk. Cheap cosmetic fix; safe to apply. | +| 5f45e4c7f | fix: Recover Interrupted Chapter Migrations | NONE | RED | NOT-FEASIBLE | Drizzle `migration.sql` + `src/database/db.ts`. Fork uses expo-sqlite + custom MigrationRunner and already has its own migration recovery (ALREADY-HAVE concept). No user value to port. | +| ca24671a2 | fix: Support Fork-Safe Preview Builds | NONE | GREEN | NOT-FEASIBLE | GitHub Actions workflow tweak (upstream's own CI). Fork has separate CI. Skip. | +| ee3a4f20b | fix: Preserve Default Category After Reordering (#1922) | **HIGH** | YELLOW | MANUAL | Real bug present in fork: default-category resolution via `(SELECT id FROM Category WHERE sort = 1)` in NovelQueries.ts:114,138,201,301 and CategoryQueries.ts:47 β€” breaks once categories are reordered; `updateCategoryOrderQuery` also writes `c.sort` values instead of positional. Port = introduce `BUILT_IN_CATEGORY_IDS` (default=1, local=2) and write `sort = index+1`. Verified fork hardcodes id 2 for Local already, so concept maps cleanly. | +| 0c3542836 | fix: Synchronize Library Updates And Optimize Import Refreshes | MEDIUM | YELLOW | MANUAL | Fixes text-date comparisons via `julianday()` in triggers + `lastUpdatedAt ASC/DESC` sort rewrite; ISO-8601 timestamp format for inserted chapters. Fork's NovelTable.ts triggers (lines 43/56/69) and LibraryQueries.ts sortOrder path are identical pre-fix (verified) β†’ same latent bug when chapter updatedTime formats mix. Port trigger + sort-order hunks to raw SQL. | +| f71c9c859 | fix: Ignore Discord Notifications Outside Main Repository | NONE | GREEN | NOT-FEASIBLE | CI workflow 1-liner. Skip. | +| f53546cc0 | chore: Release v2.1.1 | NONE | GREEN | NOT-FEASIBLE | Version bump. Skip. | +| 594b078fa | fix: Harden Database Migration Recovery | NONE | RED | NOT-FEASIBLE | Drizzle `db.ts` + migration SQL. Fork has its own MigrationRunner + recovery (ALREADY-HAVE). No value. | +| ac1b2f5ce | feat: Add Durable Completion Notifications | LOW | RED | NOT-FEASIBLE | Built on upstream-only `native-background-tasks` Expo module (TaskNotificationFactory, WorkManager). Fork has no such infra (MMKV queue + ServiceManager). Low value; skip. | +| 648d21062 | feat: Add Parallel Task Queues With Per-Plugin Download Limits | LOW | RED | NOT-FEASIBLE | Depends on upstream native BackgroundTaskDao/scheduler + `src/services/backgroundTasks` contract changes (includes migrateNovel.ts 1-line hunk). Concept (per-plugin download limits) is nice but requires reimplementation on fork's ServiceManager queue. Not worth direct port. | +| 89d715ce6 | docs: Replace Beta Badge With Build Preview Status | NONE | GREEN | NOT-FEASIBLE | README text. Skip. | +| bd527a02e | feat: Improve Backup Selection, Notifications, And Plugin Restore | **HIGH** | RED | MANUAL | Backup include-options selection, plugin restore from backup, richer results β€” genuine user value. But +2021/-249 across 31 files heavily overlapping fork's backup stack (SettingsBackupScreen/index.tsx, GoogleDriveModal, SelfHostModal, Checkbox.tsx, services/backup/*), and notification parts depend on upstream backgroundTasks contracts. Port only the selection/options/plugin-restore logic; high effort. | +| 11117c1b5 | feat: Add GitHub-Style Embed For Nightly Notifications | NONE | GREEN | NOT-FEASIBLE | CI workflow only. Skip. | +| ba25d5b8f | feat: Add Rozenite DevTools Integration (#1912) | NONE | RED | NOT-FEASIBLE | op-sqlite adapter + metro config + 1828-line lockfile churn. Fork uses expo-sqlite (`SQLite.openDatabaseSync`) β€” no op-sqlite. Zero value. | +| 31cb4b994 | fix: Align Bottom Navigation With Material 3 Specs | LOW | GREEN | DIRECT | `paddingBottom: 16 + (insets?.bottom || 0)` + remove `paddingVertical: 6`. Fork's BottomTabBar/index.tsx:65 has identical pre-fix line (verified). 2-line cosmetic change; cherry-picks cleanly. | +| 72ef618af | fix: Preserve Existing Data During Novel Counter Migration | NONE | RED | NOT-FEASIBLE | Drizzle migration SQL. Fork's own `migrations/002_add_novel_counters.ts` already preserves data (ALREADY-HAVE). Skip. | +| 936fd223b | feat: Add Discord Notifications For Nightly Builds | NONE | GREEN | NOT-FEASIBLE | CI workflow. Skip. | +| 6d26438d2 | test: Update Expected Database Migrations | NONE | RED | NOT-FEASIBLE | Test/docs for Drizzle migration list (AGENTS.md hunk). Skip. | +| 5eb5a55c4 | fix: Repair Skipped Novel Counter Migrations | NONE | RED | NOT-FEASIBLE | Drizzle migration SQL + migrations.js. Fork's runner applies its own counter migration. Skip. | +| 6cd86d142 | chore: Update Translations (#1906) | LOW | YELLOW | MANUAL | Same as 9612bf6f8 β€” 33 language files, different path in fork (`strings/languages/`), mechanical merge only. | +| 9783c4d5e | fix: Normalize Repository Back Navigation (#1905) | LOW | YELLOW | MANUAL | Removes `useBackHandler` popToTop weirdness on repository screen. Fork's SettingsRepositoryScreen.tsx exists (overlap) and uses same navigator types. Small deletion; low value. | +| 87f602a1f | chore: Release v2.1.0 | NONE | GREEN | NOT-FEASIBLE | Version bump. Skip. | +| 3d32ddd6e | fix: Improve Release Selection And APK Verification | NONE | GREEN | NOT-FEASIBLE | Release workflow + scripts/resolve-release-version.cjs (fork doesn't have this script). Skip. | +| 7ddd934c2 | fix: Handle Missing Tracker Client IDs | MEDIUM | YELLOW | MANUAL | Guard clauses in aniList.ts / myAnimeList.ts so missing client ID throws a clear error instead of building a broken auth URL. Fork's tracker files exist (src/services/Trackers/) and share structure; `scripts/generate-env-file.cjs` hunk doesn't apply (fork uses `.env` + env.d.ts) β†’ port only the two tracker-file guards. | +| b53c91f74 | chore: Separate Jest And Type Check Workflows | NONE | GREEN | NOT-FEASIBLE | CI + package.json script renames. Skip. | +| 9d106a371 | feat: Add Semantic Version Bump Selection | NONE | GREEN | NOT-FEASIBLE | Release workflow + missing script. Skip. | +| 8b0a8dc4c | chore: Update Husky Pre-Commit and Pre-Push Hooks | NONE | RED | NOT-FEASIBLE | package.json + .husky + NativeVolumeButtonListenerModule (fork uses its own volume module). Bundled 3-line useTimeTracking change is negligible. Skip. | +| 72bfcad03 | fix: Resolve Type and Lint Issues | LOW | RED | NOT-FEASIBLE | 40+ file lint/type cleanup including **WebViewReader.tsx (39 lines), ReaderScreen.tsx, ChapterContext.tsx, useLibrary.ts** β€” all fork-critical, heavily rewritten in fork. Zero user value, maximal conflict surface. **Do not port.** | +| 345d084eb | feat: Add Chapter Numbers To EPUB Titles (#1405) | **HIGH** | YELLOW | MANUAL | User-requested export feature: `epubIncludeChapterNumber` setting + numbered-title composition. Fork's ExportEpubModal/ExportNovelAsEpubButton exist with same structure but use `@cd-z/react-native-epub-creator` (JS) vs upstream nitro-epub β†’ port the option plumbing + title logic only. `useSettings.ts` change is one additive field. | +| 10b6042a9 | feat: EPUB Export Progress Notifications | LOW | RED | NOT-FEASIBLE | nitro-epub C++ (HybridEpub.cpp/hpp, EpubExporter) + native background tasks. Fork's EPUB export is synchronous JS β€” different mechanism entirely. Progress-notification concept would need a new implementation. Skip. | +| bd7161172 | feat: Improve Chapter Drawer And WebView App Bar | MEDIUM | RED | MANUAL | Drawer UX improvements incl. a genuine bugfix: `chapters.findIndex(el => el.id === chapter.id) || 0` mis-fallbacks to 0 on chapter 0... actually returns 0 when index is 0 anyway; the real fix is the `index >= 0 ? index : 0` fallback + scroll-to-current math. But fork's ChapterDrawer uses `@legendapp/list` LegendList (upstream uses FlashList) and ReaderScreen.tsx is fork-critical β†’ port only the index-calculation logic manually. App-bar hunk (WebviewScreen) is lower risk. | +| 8f53550d2 | fix: Restore Tracker Search Requests | **HIGH** | YELLOW | MANUAL | Fixes broken tracker search-on-open (scheduleSearch bypassed; submit handler used stale `searchText` state). Fork's TrackSearchDialog.tsx is fork-modified (overlap) and likely carries the same broken flow. Port the `useEffect` reschedule + `onSubmitEditing` event-based handler + `returnKeyType="search"`. | + +--- + +## Group Summary + +### (a) Top 5 most valuable commits for the fork + +1. **c3260e8e0 β€” Chapter Download Queue and Removal Issues** (HIGH). The fork literally contains the bug being fixed: `deleteReadChaptersFromDb` passes `chapter.novelId` as the chapter id (deletes the wrong folder), and `deleteDownloads` runs `UPDATE Chapter SET isDownloaded = 0` with no WHERE clause (wipes download flags across the whole DB). Users deleting one novel's downloads lose flags everywhere and orphan files. The DB part ports in ~20 lines of raw SQL. **Highest risk/benefit ratio in the group.** + +2. **ee3a4f20b β€” Preserve Default Category After Reordering** (HIGH). The fork resolves the default category via `WHERE sort = 1` in four places (NovelQueries.ts:114,138,201,301; CategoryQueries.ts:47) and writes `c.sort` on reorder. After any category reorder, new novels silently land in the wrong category. The fix (built-in IDs + positional sort) is small and maps directly onto the fork's hardcoded `id = 2` Local-category convention. + +3. **eb12bdfd6 β€” Prevent Novel Genre Rendering Crashes** (HIGH). A malformed/empty genre string crashes EditInfoModal and NovelInfoComponents in the fork today (same `split(',')` patterns as upstream pre-fix). `parseGenres()` helper + 3 component hunks is a small, safe crash fix. + +4. **345d084eb β€” Chapter Numbers in EPUB Titles** (HIGH). Explicit user feature request (upstream #1405). Fork's EPUB export path is different (`@cd-z/react-native-epub-creator`), so the setting + title composition must be adapted, but it is fully feasible in JS and has no native dependency. + +5. **c0877a9b0 β€” Ignorable App Update Notifications** (HIGH). Closes the long-standing "can't dismiss update prompt" complaint (upstream #600/#1632). Fork's update checker is a near-clone of the deleted upstream hook, so the port is mostly file-copy + repo-URL adaptation. + +Honorable mentions: **8f53550d2** (restores broken tracker search β€” likely broken in fork today) and **0c3542836** (library "last updated" sort/trigger correctness, identical pre-fix code verified). **96fb52df1** (stats overhaul) is the highest-value *feature*, but at RED risk with a full raw-SQL stats rewrite + new dependency β€” schedule as its own project. + +### (b) Notable risks / red flags + +- **72bfcad03 (lint/type cleanup) is a trap**: touches WebViewReader.tsx, ReaderScreen.tsx, ChapterContext.tsx, useLibrary.ts β€” the fork's most rewritten files. Lint-only value, guaranteed conflicts. Never cherry-pick. +- **bd7161172 (chapter drawer)** touches ReaderScreen.tsx and a drawer whose list component diverged (fork: `@legendapp/list` LegendList; upstream: `@shopify/flash-list`). Only the index-calc bugfix is safely portable. +- **bd527a02e (backup overhaul)** is +2000 lines into files the fork modified heavily (SettingsBackupScreen, both provider modals, Checkbox, services/backup/*). Port only the selection/plugin-restore slices, as a dedicated effort. +- **All native-scheduling/notification commits** (bfb12b04a, 648d21062, ac1b2f5ce, 10b6042a9, and the notification half of bd527a02e) depend on upstream-only Expo-module infrastructure (`modules/native-background-tasks`, `modules/native-file`, nitro-epub). NOT-FEASIBLE β€” the fork would have to reimplement on react-native-background-actions + ServiceManager. +- **All Drizzle migration commits** (5f45e4c7f, 594b078fa, 72ef618af, 5eb5a55c4, 6d26438d2) are NOT-FEASIBLE; the fork's own MigrationRunner already covers recovery/counters. +- **Release/chore/CI commits** (12 of 43) carry zero user value; the fork has its own version numbering, workflows, and husky hooks. + +### (c) Architecture-divergence insights + +1. **DB layer divergence is logic-level, not conceptual.** Upstream fixes (WHERE-scoped updates, julianday date math, positional sort, built-in category IDs) express pure SQL logic; the fork's raw-SQL layer accepts them almost 1:1. The porting cost is low because the *pre-fix* fork code is often byte-identical to upstream's pre-fix code (verified for SelectableChip, NovelInfoHeader, NovelStatus enum, triggers, deleteDownloads). +2. **The fork is 4–6 weeks behind on "fix" quality while being ahead on TTS.** The 2026-07-26β†’08-01 upstream fix cluster (tracker search, genre crashes, download removal, category reorder, date sorting) is exactly the batch the fork should take β€” these are regression-style fixes to shared legacy code, not features tied to upstream's new infra. +3. **Three hard upstream-only infrastructure pillars** make certain commits unportable regardless of value: (1) Drizzle/op-sqlite vs expo-sqlite raw SQL; (2) Expo-module background tasks + nitro modules vs react-native-background-actions/ServiceManager and custom Kotlin TTS; (3) Expo-managed config plugins (`plugins/`, `modules/`, `scripts/generate-env-file.cjs`) vs the fork's bare workflow. +4. **Translation path drift** (`src/i18n/languages/*` upstream vs `strings/languages/*` fork) plus fork-local string keys means all Crowdin commits are manual merges, not cherry-picks. +5. **Recommended execution order** for the fork: batch A (safe, high-value fixes): c3260e8e0, ee3a4f20b, eb12bdfd6, 8f53550d2, 0c3542836, 76106317b, 31cb4b994, 0ed7d878b, e75f4fba5, 7ddd934c2 β†’ batch B (feature ports needing UI work): 345d084eb, c0877a9b0, 9783c4d5e, bd7161172 (index fix only) β†’ batch C (project-scale): 96fb52df1, bd527a02e slices β†’ skip everything else. diff --git a/specs/upstream-merge-analysis-2026-08/reference/subagent/group2-report.md b/specs/upstream-merge-analysis-2026-08/reference/subagent/group2-report.md new file mode 100644 index 0000000000..150de1874b --- /dev/null +++ b/specs/upstream-merge-analysis-2026-08/reference/subagent/group2-report.md @@ -0,0 +1,90 @@ +# Group 2 Upstream Commit Analysis β€” Selective Port Assessment + +**Fork:** branch `dev` @ 40c6676797 (v2.1.3) β€” bare-workflow Expo SDK 54, expo-sqlite raw SQL, custom Kotlin TTS stack (TTSHighlightModule.kt / TTSForegroundService.kt), MMKV settings, WebView reader (core.js at `android/app/src/main/assets/js/`), react-native-paper 5.14.5. +**Upstream:** `original` = upstream/master @ c3260e8e01 β€” Expo 55 managed, Drizzle ORM + op-sqlite, Nitro modules, rock CLI, zustand-based novel store. +**Prior decision:** full merge infeasible (100+ conflicts); strategy = selective cherry-pick/manual port (`.agents/upstream-merge-memory.md`, 2025-12-30). + +All 43 commits verified via `git show --stat` / `git show` / fork file inspection. + +--- + +## Per-commit assessment + +| # | commit (short) | subject | VALUE | RISK | PORT | rationale | +|---|---|---|---|---|---|---| +| 1 | 52914b978 | fix: Migrate Volume Button Events to Expo Listener API | ALREADY-HAVE | YELLOW | MANUAL | Fork has its own volume-button scroll in `src/screens/reader/hooks/useChapter.ts` (NativeVolumeButtonListener + NativeEventEmitter, connectVolumeButton). This is an upstream-internal API migration (expo `useEventListener`); fork's own listener wiring already works. No user-facing gain. | +| 2 | ff12759a | ci: Run Preview Builds Daily When Missing | NONE | GREEN | DIRECT | CI-only change to `.github/workflows/build.yml`. No app code; pointless for fork's build setup. | +| 3 | e62868b8 | feat: Automate Draft Android Releases | LOW | YELLOW | NOT-FEASIBLE | CI release automation + scripts + `useGithubUpdateChecker.ts` (fork has its own pointing at bizzkoot/lnreader). Depends on rock CLI `plugins/android/android-build-types.gradle` (upstream-only infra) and fork's release process differs. | +| 4 | 97be48df | feat: Add Migration Review Options | MEDIUM-HIGH | YELLOW | MANUAL | User-facing migration UX (choose cover/metadata source, redownload policy) + new MigrationReviewDialog. Fork's `migrateNovel.ts` is raw-SQL/ServiceManager-based and `MigrationNovelList.tsx` exists but differs; `backgroundTasks/contracts.ts` infra is upstream-only. Adaptable. Closes upstream #937/#1037. | +| 5 | cbe7d70e | fix: Restore Collapsed TTS Button Interaction | ALREADY-HAVE | GREEN | MANUAL | 1-line `??=` guard in upstream `assets/reader/js/index.js`. Fork's own index.js (android/app/src/main/assets/js/index.js, line ~548) already guards `if (!controllerElement) controllerElement = document.getElementById('TTS-Controller')` inside `ontouchstart`. Equivalent protection exists. | +| 6 | 6548e9a6 | fix: Improve Paged Reader Layout and Performance | MEDIUM | RED | NOT-FEASIBLE | ~218-line rewrite of upstream `core.js` + css + search.js. Fork's core.js lives at a different path and is heavily TTS-customized (137KB); ReaderScreen.tsx change references `window.pageReader?.movePage(0)` which fork's core.js does not expose. Paged-reader perf ideas portable only as a manual feature port. | +| 7 | 138fdff2 | fix: Prevent Duplicate WebView Bottom Insets | MEDIUM | YELLOW | MANUAL | Upstream removes `useSafeAreaInsets` padding from `WebviewScreen.tsx`. Fork's WebviewScreen.tsx (cookie-hardened, line 171) still has `containerStyle={{ paddingBottom: bottom }}` β†’ likely has the same double-inset bug. Small localized fix; file is fork-modified (cookie parsing) so needs manual merge. | +| 8 | f1dd4b77 | feat: Improve Empty Chapter Reporting | MEDIUM | RED | MANUAL | Adds GitHub-issue report link + HTML-escaped empty-chapter message in WebViewReader.tsx + sanitizeChapterText.ts. Both files are fork-critical; fork's sanitizeChapterText.ts is only 1KB (no sanitizeOptions caching). i18n lives in fork at `strings/languages/*` not `src/i18n/*`. Feature is portable conceptually, needs full manual wiring. | +| 9 | 9b356ffd | fix: Preserve Client IDs And Improve Environment Generation | NONE | GREEN | NOT-FEASIBLE | Rewrites upstream-only `scripts/generate-env-file.cjs` (Expo managed env generation). Fork has no such script/infra. | +| 10 | 5cf7e15d | feat: Add Reader Setting Descriptions | LOW | YELLOW | MANUAL | Adds helper descriptions to AccessibilityTab/NavigationTab. Fork's AccessibilityTab (32.7KB) is heavily TTS-customized (text cleanup, per-novel overrides); descriptions are cosmetic. Low payoff, some merge friction. | +| 11 | 6a7e90c1 | fix: Align Advanced Reader Tabs And Localize Actions | LOW | YELLOW | MANUAL | AdvancedTab.tsx alignment + hardcoded strings β†’ i18n. Fork has its own AdvancedTab.tsx; cosmetic. Fork strings structure differs. | +| 12 | 99c31d56 | fix: Use MD3 Outline Variant For Tab Borders | LOW | YELLOW | MANUAL | Cosmetic border variant across 6 screens incl. ReaderBottomSheet (fork's is ReaderBottomSheetV2/ReaderTTSTab). Fork uses paper 5.14.5; trivially portable but purely visual. | +| 13 | 4d982479 | feat: Add Android DNS Over HTTPS Support | ALREADY-HAVE | GREEN | NOT-FEASIBLE | Fork shipped its own DoH long ago (`DoHManagerModule.kt`, `DoHManager.ts`, SettingsAdvancedScreen DoH picker, persistence). Upstream implementation is an expo local module (`modules/native-doh`). Redundant. | +| 14 | 164f5d25 | feat: Add Automatic Library Update Scheduling | HIGH | YELLOW | MANUAL | User-facing: schedule library updates at 12/24/48/72/168h intervals. Fork has `updateLibraryOnLaunch` only. Requires a new native scheduler (WorkManager-like; upstream uses expo-module `native-background-tasks`) plus SettingsLibraryScreen UI and `useSettings` field. Fork's ServiceManager (react-native-background-actions) is a different paradigm β€” feasible but real native+JS work. | +| 15 | 89cddf9e | fix: Expand Discover Row Ripple | LOW | YELLOW | MANUAL | Pressable restructure in DiscoverCard.tsx. Fork's DiscoverCard takes `theme` prop and uses AppText/scaleDimension β€” different structure. Minor ripple-visual fix. | +| 16 | eac6ad58 | feat: Improve Novel App Bar And EPUB Export UX | MEDIUM | YELLOW | MANUAL | Large UX polish of NovelAppbar, ExportEpubModal, JumpToChapterModal, NovelScreenList + tests. Fork has its own versions of all four (different layouts, uiScale). Real user-facing value (jump-to-chapter + EPUB export improvements) but requires manual re-port against fork components. | +| 17 | 93fe04c2 | feat: Polish Reader Controls And Navigation | MEDIUM-HIGH | RED | NOT-FEASIBLE | Very broad: touches ChapterQueries.ts (Drizzle `limit` param), ReaderAppbar, all ReaderBottomSheet children, ReaderFooter, ReaderScreen, navigators (MoreStack/ReaderStack/Main), Slider/ToggleButton/SegmentedControl, DownloadsScreen. Fork-modified copies of nearly every file + Drizzle-only DB change. Selective sub-fixes (e.g., ChapterQueries limit β†’ fork SQL) possible manually. | +| 18 | c9358b3a | feat: Update Android Branding Assets | LOW | GREEN | DIRECT | Binary-only icon/splash refresh in `debug`/`preview` flavors. Cherry-picks cleanly; fork has its own branding but assets are inert to TTS/DB. | +| 19 | 67d2c7c0 | feat: Add Collapsible TTS Controls And Stable Notification Progress | MEDIUM | RED | NOT-FEASIBLE | Touches nitro-tts Kotlin/Swift (`TtsMediaNotification.kt`) + upstream index.js/tts.css. Fork has its own TTS notification (`ttsNotification.ts`, TTSForegroundService.kt) and its own TTS controller UI. Nitro dependency = NOT-FEASIBLE; fork's "stable progress" equivalent already exists. | +| 20 | c30441b9 | feat: Add Smart Library Update Filters | MEDIUM-HIGH | YELLOW | MANUAL | Skip-completed/unstarted/unread novels in global updates + `getLibraryNovelsForGlobalUpdate` (Drizzle) + SmartUpdateDialog. Fork's `services/updates/index.ts` + raw-SQL LibraryQueries are structurally different but the filter concept ports cleanly to fork SQL (`status`, `lastReadAt` exist in fork schema). UseSettings overlap is small. | +| 21 | 6d2a9f8e | fix: Prevent Slider Flicker After Release | LOW | GREEN | MANUAL | Fix to the new MD3 Slider component (0c854618). Only relevant if MD3 Slider is ported first; fork currently uses `@react-native-community/slider`. | +| 22 | 79984542 | feat: Add Parallel Library Updates Across Sources | MEDIUM-HIGH | YELLOW | MANUAL | Concurrency-3 parallel updates across sources (grouped by pluginId). Fork's `updates/index.ts` is a sequential `for` + `sleep(1000)` loop; the same grouping/Promise.all pattern adapts directly. Native `TaskNotificationFactory.kt` part is upstream-only (fork shows progress via ServiceManager metadata). Real speed win for large libraries. | +| 23 | 8ac628b3 | fix: Improve History Removal Flow | MEDIUM | YELLOW | MANUAL | Per-novel history removal + confirm dialog + async flows. Fork has raw-SQL HistoryQueries + own useHistory/HistoryScreen/HistoryCard. New `deleteNovelHistory` SQL is trivial; UI adaption needed. | +| 24 | d02291c2 | feat: Remember Last Used Library Category | MEDIUM | YELLOW | MANUAL | Persists last library tab selection. Fork's useSettings LibrarySettings + LibraryScreen exist; small setting + rehydration. Fork uses bottom-tab navigation in LibraryScreen (index state) β€” adaptable. | +| 25 | 7af0911f | feat: Add High-Resolution Android Flavor Assets | LOW | GREEN | DIRECT | Binary-only res/mipmap/drawable additions. Cherry-picks cleanly; inert. | +| 26 | 098782d6 | refactor: Simplify Settings Layout And Theme Selection | LOW | YELLOW | MANUAL | Removes `react-native-theme-switch-animation` dep, simplifies ThemePicker/ThemeSelectionStep/SettingsAppearanceScreen. Fork has its own theme system (md3 theme dirs + uiScale) and its own package.json β€” dep removal must be verified against fork's dependency graph. Net user value low. | +| 27 | 732628c4 | feat: Add Date Format And Relative Timestamp Settings | MEDIUM | YELLOW | MANUAL | New `utils/dateFormat.ts` + Appearance setting + DateFormatModal; touches useSettings, HistoryScreen, DownloadsScreen, ChapterItem, NovelScreenList, UpdatesScreen, GoogleDriveModal. Fork uses dayjs already (translations.ts). User-facing (timestamp formatting choice); manual adaptation of ~8 fork screens needed. | +| 28 | 5e62c36a | feat: Add Library Settings And Category-Based Global Updates | MEDIUM-HIGH | RED | NOT-FEASIBLE | Adds `defaultCategoryId` + include/exclude category sets for global updates. Core logic in Drizzle NovelQueries/LibraryQueries (`getCategoryForNewNovel`, `getLibraryNovelsForGlobalUpdate`); fork's NovelQueries.ts is raw SQL and fork-critical. Fork's SettingsLibraryScreen already has a **TODO-stubbed** DefaultCategoryDialog (line 18) this feature would complete. Concept very valuable; port = rewrite in raw SQL + new SettingsLibrary UI. | +| 29 | 3d102a52 | perf(reader): Cut Chapter Open Latency (#1899) | HIGH | RED | NOT-FEASIBLE | Big perf pass: WebViewReader.tsx (βˆ’314/+), useChapter.ts (465 lines), core.js (107), index.js/search.js, useSettings, ChapterContext, ChapterDrawer, ReaderBottomSheet, ReaderFooter, SkeletonLines, sanitizeChapterText, WebviewScreen + useNovel zustand store. Touches every fork-critical reader file and depends on upstream store/asset layout. Cherry-pick = guaranteed conflict storm. Concepts (deferred adjacent-chapter fetch, sanitized-HTML cache, lazy drawer/sheet, memoized drawer rows, shared scroll listener) are excellent candidates for a manual perf port. | +| 30 | 4dcf2130 | fix: Restore Reader Changes Reverted During Expo Migration | MEDIUM | RED | NOT-FEASIBLE | Restores upstream `assets/reader/js/search.js` (442 new lines) + core.js/css tweaks. Fork's reader asset layout differs and fork has no in-reader search feature. Would require building the feature from scratch. | +| 31 | 4de82472 | fix: Resolve Require Cycles And LegendList Render Updates | LOW | RED | NOT-FEASIBLE | Patches `@legendapp__list@3.3.3` (fork pins `^2.0.16`), Drizzle `database/manager/liveQuery.ts`, backup/utils require-cycle. Version mismatch + Drizzle/op-sqlite manager = NOT-FEASIBLE. | +| 32 | 58db0352 | fix: Restore Metro Reader Asset Middleware | ALREADY-HAVE | GREEN | NOT-FEASIBLE | Fork's `metro.config.cjs` already implements an equivalent `enhanceMiddleware` serving `android/app/src/main` reader assets (paths mirror the fork's asset location). Redundant. | +| 33 | 74808f08 | feat: Replace Legacy Media Controls with Nitro (#1896) | ALREADY-HAVE | RED | NOT-FEASIBLE | Nitro-powered TTS + MediaSession. Fork has its own complete native TTS stack (TTSForegroundService.kt, TTSHighlightModule.kt, ttsNotification.ts 5-button media notification). Replacing it = throwing away fork's crown jewel. NOT-FEASIBLE by design; feature (media notification) already exists in fork. | +| 34 | b96351ea | docs: Fix Contraction in TESTING.md | NONE | GREEN | DIRECT | Docs typo. | +| 35 | eb8dc6df | docs: Fix "Withoud" Typo in CONTRIBUTING-NIX.md | NONE | GREEN | DIRECT | Docs typo. | +| 36 | 0c854618 | feat: Add MD3 Slider Component | MEDIUM | YELLOW | MANUAL | New MD3 Slider (416 lines) + removes `@react-native-community/slider`. Fork's TextSizeSlider/ReaderValueChange use community slider. Porting gives consistent M3 styling + fixes; touches fork's ReaderTTSTab path (upstream TTSTab.tsx). package.json change must be coordinated. | +| 37 | e9f6bdaa | fix: Modernize Menu Styling And Outside-Tap Dismissal | MEDIUM | YELLOW | MANUAL | Menu component overhaul (outside-tap dismiss, tests). Fork has its own Menu component (`src/components/Menu/index.tsx`) used by reader/Webview appbars; pattern is portable. | +| 38 | 3d34658d | feat: Add Material 3 Top Tab Indicators | MEDIUM | YELLOW | MANUAL | New TopTabBar component + replaces SettingsReaderScreen TabBar (fork has its own `SettingsReaderScreen/components/TabBar.tsx`). Visual M3 polish across 6 screens incl. ReaderBottomSheet. Moderate manual effort. | +| 39 | d44ce741 | docs: CONTRIBUTING.md | NONE | GREEN | DIRECT | Docs typo. | +| 40 | 17c891e1 | refactor: Standardize Bottom Sheet UX and Styling | MEDIUM | YELLOW | MANUAL | BottomSheet component + `useTheme` (adds `surfaceContainerLow`) + layout util; touches ReaderBottomSheet, NovelBottomSheet, LibraryBottomSheet, FilterBottomSheet, TrackSheet. Fork has its own BottomSheet + useTheme (fork's useTheme differs: computed colors, no surfaceContainerLow yet). Portable as a styling/UX backport. | +| 41 | 9805da63 | refactor: Optimize Novel Screen Performance And Architecture | HIGH | RED | NOT-FEASIBLE | Massive refactor onto upstream's zustand `useNovel` store + new hooks (useNovelRefresh, useNovelScreenActions, etc.), NovelScreen βˆ’209 lines, NovelInfoHeader split. Fork has its own `useNovel.ts` hook + NovelContext architecture. Perf goals (skeletons, memoized rows, lazy modals) are valuable but must be re-implemented on fork's stack. | +| 42 | afc20277 | fix: Restore Android Build Types For CI | NONE | GREEN | NOT-FEASIBLE | CI + rock-CLI `plugins/android/android-build-types.gradle`. Fork's Gradle 9.2.0/build setup is custom; no rock. | +| 43 | 14061d20 | docs: CONTRIBUTING.md | NONE | GREEN | DIRECT | Docs typo. | + +--- + +## Group Summary + +### (a) Top 5 most valuable commits for the fork + +1. **3d102a52 β€” perf(reader): Cut Chapter Open Latency (#1899)** β€” Highest user-visible payoff (faster chapter open, no re-render stutter while reading, scroll restore ~100ms vs ~1.5s). Cannot be cherry-picked (touches every fork-critical reader file + upstream store/asset layout) but its *techniques* are directly applicable: defer adjacent-chapter resolution, cache sanitized HTML, lazy-mount drawer + settings sheet, memoized drawer rows, single coalesced scroll listener, one mark-as-read per chapter. **Recommended as a manual perf sprint on fork's WebViewReader.tsx/useChapter.ts/core.js.** +2. **164f5d25 β€” Automatic Library Update Scheduling** β€” Genuinely missing user feature (scheduled background updates). Manual port: add a native scheduler (or adapt fork's ServiceManager/headless task + AlarmManager/WorkManager), `automaticLibraryUpdateIntervalHours` in useSettings AppSettings, SettingsLibraryScreen dialog. Moderate effort, high appreciation for a library-heavy userbase. +3. **79984542 β€” Parallel Library Updates Across Sources** β€” Simple, contained speed win. Fork's sequential `for` + `sleep(1000)` in `services/updates/index.ts` can adopt the same group-by-pluginId + `Promise.all` concurrency-3 pattern with no schema or native changes. Low-risk, near-certain benefit. +4. **c30441b9 β€” Smart Library Update Filters** β€” Skip-completed/unstarted/unread novels during global update. Fork's `Novel` table already has `status`/`lastReadAt`, so the SQL filter is trivial; UI (SmartUpdateDialog in SettingsLibraryScreen) adapts to fork's settings style. Good value/effort ratio. +5. **97be48df β€” Migration Review Options** β€” Migration is a core fork flow (own migrateNovel.ts); adding cover/metadata/redownload preferences + review dialog is user-visible and self-contained. Fork's raw-SQL migrate service makes it a moderate manual port. + +Honorable mentions: **5e62c36a** (default category + category-based updates β€” completes fork's TODO-stubbed DefaultCategoryDialog, but Drizzleβ†’SQL rewrite needed), **732628c4** (date format/relative timestamps β€” broad but mechanical), **8ac628b3** (per-novel history removal), **eac6ad58** (novel screen + EPUB export UX). + +### (b) Notable risks / red flags + +- **RED, do not cherry-pick:** `3d102a52`, `93fe04c2`, `f1dd4b77`, `6548e9a6`, `4dcf2130`, `5e62c36a`, `9805da63`, `4de82472`, `098782d6` (package.json). Every one of these touches `WebViewReader.tsx`, `useChapter.ts`, `ChapterQueries.ts`/`NovelQueries.ts`, `core.js`, `ReaderScreen.tsx`, `ReaderFooter.tsx`, or `package.json` β€” all fork-rewritten. +- **Upstream-only infra (NOT-FEASIBLE):** Nitro (`74808f08`, `67d2c7c0`), Drizzle/op-sqlite (`4de82472`, parts of `93fe04c2`, `5e62c36a`, `c30441b9`, `8ac628b3`), Expo 55 managed workflow (env scripts `9b356ffd`, rock CLI `afc20277`, `e62868b8`), expo local modules (`4d982479`, `164f5d25` native half). +- **Version/toolchain mismatches:** `4de82472` patches `@legendapp__list@3.3.3` while fork pins `^2.0.16`; `58db0352` metro middleware already exists in fork (`metro.config.cjs`); `0c854618` removes a dependency the fork still uses. +- **i18n divergence:** upstream edits `src/i18n/languages/*/strings.json`; fork uses `strings/languages/*` + `translations.ts`. Every upstream commit touching strings (`f1dd4b77`, `93fe04c2`, `c30441b9`, `eac6ad58`, `6a7e90c1`) needs string-tree reconciliation, not a clean merge. +- **Asset-layout divergence:** upstream `assets/reader/js/*` vs fork `android/app/src/main/assets/js/*` β€” reader-JS commits (`cbe7d70e`, `6548e9a6`, `4dcf2130`, `67d2c7c0`) never apply to fork paths. + +### (c) Architecture-divergence insights + +1. **DB layer is the hardest seam.** Upstream is Drizzle ORM on op-sqlite; fork is expo-sqlite raw SQL with its own migration runner. Any upstream "query" commit must be re-expressed as fork SQL β€” a pure port (not a cherry-pick). Value-laden queries (global-update filters, limit params, per-novel history delete) are individually small in SQL, so a "port the feature, rewrite the query" pattern is the right workflow. +2. **TTS is a one-way street.** Fork's custom Kotlin TTS (foreground service + highlight module + WebView tts bridge + text cleanup) is its differentiator. All upstream TTS work now targets Nitro/MediaSession and is effectively dead to the fork except as UX inspiration. The fork's own `ttsNotification.ts` already delivers what upstream's notification work does. +3. **Reader UI lives in the fork's WebView, not upstream's.** Both sides independently evolved `core.js`/`index.js`; fork's is TTS-integrated and length-preserving. Upstream reader-perf and paged-reader changes should be treated as *specifications* to re-implement, not code to import. +4. **State management diverged.** Upstream moved novel screen onto a zustand store; fork uses `useNovel.ts` hook + `NovelContext`/`ChapterContext`. Perf refactors like `9805da63` and `3d102a52` cannot transfer; their perf *rationales* (memoization, deferred heavy work, lazy mounting) should be applied directly to fork components. +5. **Selective porting is confirmed viable.** The clean wins are the library-update cluster (`164f5d25`, `79984542`, `c30441b9`, `5e62c36a`), migration UX (`97be48df`), history removal (`8ac628b3`), date formatting (`732628c4`), and the M3 component polish set (`0c854618`, `3d34658d`, `17c891e1`, `e9f6bdaa`). These are the same cluster upstream shipped in late July 2026 and align with fork gaps (TODO-stubbed default category, sequential updates, no date-format control). + +--- +*Evidence base: `git show` for all 43 hashes; fork inspection of useChapter.ts, WebViewReader.tsx, sanitizeChapterText.ts, core.js/index.js, LibraryQueries/NovelQueries/ChapterQueries/HistoryQueries, useSettings.ts, useTheme.ts, ServiceManager.ts, updates/index.ts, metro.config.cjs, strings/ layout, package.json deps, novel/settings/library screen trees.* diff --git a/specs/upstream-merge-analysis-2026-08/reference/subagent/group3-report.md b/specs/upstream-merge-analysis-2026-08/reference/subagent/group3-report.md new file mode 100644 index 0000000000..40b8066d0c --- /dev/null +++ b/specs/upstream-merge-analysis-2026-08/reference/subagent/group3-report.md @@ -0,0 +1,100 @@ +# Group 3 β€” Upstream Commit Adoption Analysis (43 commits, 2026-05-03 β†’ 2026-07-24) + +Fork: branch `dev` (v2.1.3, 2026-08-03) Β· Upstream: `original` = upstream/master (c3260e8e01, 2026-08-01) +Method: read-only inspection of each commit's diff (`git show`) cross-checked against fork source. Every HIGH/MEDIUM port recommendation below was verified against the fork's actual current file contents. + +--- + +## Per-commit assessment table + +| # | commit (short) | subject | VALUE | RISK | PORT | rationale | +|---|---|---|---|---|---|---| +| 1 | 916374b5df | refactor: Reorganize Repository Structure | NONE | RED | NOT-FEASIBLE | Pure churn: `strings/`β†’`src/i18n/` moves, 100+ import updates, removes `gradle/android-build-types.gradle`. Zero user value; would create hundreds of conflicts. | +| 2 | 0a2a6c96ca | feat: Add Variant-Specific Android Splash Screens | NONE | RED | NOT-FEASIBLE | Expo-managed `app.json` + `src/.../preview/res` flavor resources. Fork is bare workflow, single variant (build.gradle uses `customAppId` property only). | +| 3 | 65e8610030 | refactor: Replace Bottom Navigation Shift With Fade | LOW | YELLOW | DIRECT | 1-line `animation: 'shift'`β†’`'fade'`. Fork's BottomNavigator.tsx:68 still has `animation: 'shift'` (verified) β†’ trivially portable, but pure visual preference. | +| 4 | 1322065290 | feat: Add Variant-Specific Android App Icons | NONE | RED | NOT-FEASIBLE | Flavor-scoped mipmap resources for upstream's `preview` variant. Fork has no build variants. No user value. | +| 5 | 22a1efd1e5 | perf: Optimize RN Skeleton Loading Components | LOW | YELLOW | MANUAL | Reanimated shimmer rewrite (new ShimmerPlaceholder.tsx, moves useLoadingColors to @utils). Fork has its own `Skeleton/Skeleton.tsx` (8K, own useLoadingColors.tsx) already optimized. Minor perf/a11y polish only. | +| 6 | 269606ebb8 | fix: Resolve Nitro EPUB Import Alias | NONE | RED | NOT-FEASIBLE | tsconfig path alias for the Nitro module the fork doesn't have. | +| 7 | 4cb1890b62 | feat: Add Nitro EPUB Exporter | ALREADY-HAVE | RED | NOT-FEASIBLE | Fork already exports EPUB via `@cd-z/react-native-epub-creator` (ExportNovelAsEpubButton.tsx:5). Upstream version is a C++ Nitro module (`modules/nitro-epub/`) β€” upstream-only infra. | +| 8 | a421177f0c | fix: Improve Category Badge Layout And Drag Reordering | MEDIUM | YELLOW | MANUAL | Fork's CategoriesScreen.tsx:94 has the identical buggy `keyExtractor={(item,index)=>`${item.id}-${index}`}` that breaks drag reordering β€” verified. Port the keyExtractor fix (1 line). Drag-handle switch to IconButton `onPressIn` is optional (fork's TouchableOpacity+onLongPress works). Badge overflow fix useful. | +| 9 | a16ec87631 | perf: Optimize Novel Covers, Lists, and Chapter Groups | HIGH | RED | MANUAL | New `NovelCoverImage` (expo-image), `NovelCoverLayoutContext`, virtualized `NovelChapterGroup`/`UpdateNovelChapterGroup` lists + `useUpdates` rework. Genuine perf win for library/updates/history. BUT touches ChapterQueries.ts, NovelCover, NovelList, many screen components the fork rewrote β†’ large manual port, needs perf regression testing. | +| 10 | dfd620aa29 | feat: Standardize Dialogs with Material 3 Patterns | MEDIUM | RED | MANUAL | 51 files / 1603 insertions rework of every dialog incl. the fork's custom `ReaderTTSTab.tsx` and SettingsAdvancedScreen. Visual consistency only; low value-per-conflict. Defer. | +| 11 | a5a6e8326f | chore: Update `@legendapp/list` to 3.3.3 | LOW | YELLOW | MANUAL | Fork pins `^2.0.16`. Bump touches ~10 list-using screens; v3 API mostly compatible but needs testing. Marginal benefit. | +| 12 | 7f48a8ba82 | refactor: Improve Plugins Screen UX and Code | MEDIUM | RED | MANUAL | New PluginDetailsScreen, usePlugins selectors rework (434 lines). Fork has its own pluginManager/plugin UI (heavily diverged BrowseScreen, PluginListItem, SourceSettings) β†’ heavy overlap, large manual port. | +| 13 | ae34d7b4ec | feat: Add Dynamic Preview APK Metadata | NONE | RED | NOT-FEASIBLE | build.yml + `gradle/android-build-types.gradle` flavor infra the fork deleted/never had. CI-only. | +| 14 | 44c8e54ed80 | feat: Add Dynamic Material Theme Colors (Closes #1863) | HIGH | YELLOW | MANUAL | Android 12+ Material You colors via `@pchmn/expo-material3-theme` + new `src/theme/dynamic.ts` + useTheme/settings/onboarding wiring. Fork's useTheme.ts matches upstream pre-refactor base (verified β€” same imports/structure) so adaptation is localized. Moderate effort, high user appeal. Needs UI-scale/testing verification. | +| 15 | 4a4208e336 | feat: Add Time-Tracking Toggle and Inactivity Timeout (#1890) | MEDIUM | RED | MANUAL | Builds on #1886 (time tracking) which the fork lacks; touches useSettings.ts (fork-critical), WebViewReader.tsx, useChapter.ts, and upstream's relocated `assets/reader/js/core.js` (fork keeps `android/app/src/main/assets/js/core.js`). Port only after #1886. | +| 16 | d2385b8dc2 | Update build.yml | NONE | β€” | NOT-FEASIBLE | CI-only for upstream's GitHub workflow; fork's CI is separate. | +| 17 | 9babe1676d | refactor: Remove React Native Background Actions Manifest Customization | NONE | RED | NOT-FEASIBLE | Fork still depends on `react-native-background-actions@^4.0.1` (ServiceManager.ts:1, package.json:89). Applying this would break the fork's background download/update tasks. Do NOT port. | +| 18 | 8c38f499e4 | chore: Update AppState Test Setup And `pnpm` Configuration | NONE | β€” | NOT-FEASIBLE | Test-infra for the time-tracking feature fork lacks. | +| 19 | 89c43c1eef | feat: Implement Time Tracking and Statistics (#1886) | HIGH | RED | MANUAL | New user-facing feature (reading time per chapter/novel, sessions, stats screen). Fork has StatsQueries.ts/StatsScreen but only library-count stats β€” no time tracking (verified: no timeSpent/session anywhere). Upstream implementation rides Drizzle migration + op-sqlite + useNovel zustand store β†’ must be rewritten as: raw-SQL table via fork's MigrationRunner, raw-SQL StatsQueries, standalone useTimeTracking hook (59 lines, portable), WebViewReader/useChapter wiring (fork-critical). Big but bounded. | +| 20 | c3c891cea0 | refactor: Migrate to Expo Managed Workflow (#1885) | NONE | RED | NOT-FEASIBLE | Would invalidate fork's custom Kotlin native modules (TTSHighlightModule, TTSForegroundService, DoH, NativeFile, ZipArchive, VolumeButtonListener), Gradle 9.2.0 setup, and dev scripts. Anti-goal for the fork. | +| 21 | e4cf64a112 | refactor: Migrate Background Tasks to Native Android Scheduling (#1889) | MEDIUM | RED | NOT-FEASIBLE | WorkManager + headless-task stack (BackgroundTaskScheduler, LNReaderTaskWorker, NativeBackgroundTasks, 243-line NativeFile.kt rework, ServiceManager rework, batched downloads, checkpoint recovery). Fork's ServiceManager + background-actions + downloader are tightly coupled; this is a multi-week native+TS project. The "download checkpoint recovery" idea is worth noting as a follow-up though. | +| 22 | 3e1e211098 | feat: Add Address Bar in WebView (#1870) | MEDIUM | YELLOW | MANUAL | ~11-line addition to WebviewScreen Appbar (URL display + address bar). Fork's WebviewScreen.tsx (6K) + components/ differ (own Menu.tsx) β†’ small manual adaptation. Nice UX for source webviews. | +| 23 | 02b6b9848f | fix: Prevent WebView Reset When Reader Settings Change (#1884) | ALREADY-HAVE | RED | MANUAL | Fork never reloads WebView on settings change: settings removed from memoizedHTML deps (TTS highlight-offset fix), settings synced via MMKV listener injectJavaScript (WebViewReader.tsx:473+), and useTTSController has its own CHAPTER_READER_SETTINGS listener (line 591). Only missing nicety: upstream's "restart TTS only when TTS-settings actually changed" optimization (`areTTSSettingsEqual`). Optional, low priority. | +| 24 | 71da09ed44 | feat(reader): Add Current Chapter Search (#1877) | HIGH | RED | MANUAL | In-chapter word search: new `search.js` (442 lines) + ReaderSearchbar (246) + core.js hooks + ReaderScreen/ReaderAppbar/WebViewReader/useChapter wiring. Fork's core.js has diverged heavily (bionic reading, spacing rebuild, custom TTS) β€” search must be re-adapted against fork's DOM/rebuild lifecycle. Best user-facing feature in this group after time-tracking; expect a day+ of manual work + TTS regression testing. | +| 25 | 4404494ec9 | fix(database): Repair Migration History For Scanlator Column | NONE | RED | NOT-FEASIBLE | Drizzle `__drizzle_migrations` bookkeeping for a scanlator column the fork doesn't have. Fork has its own MigrationRunner + migration-recovery mechanisms (ALREADY-HAVE in spirit). | +| 26 | 98a2a1d725 | feat: Implement Scanlator Filtering (#1864) | MEDIUM-HIGH | RED | MANUAL | Scanlator/group column + filter UI. Fork has NO scanlator column (verified β€” zero hits in src/database). Requires: new column via fork's MigrationRunner, plugin `Chapter` type + fetch pipeline changes to capture scanlator, 275-line ChapterQueries rework (Drizzle β†’ SQL), filter UI in chapter list/bottom sheet. Large manual port; medium value (only relevant for aggregator sources that expose scanlator). | +| 27 | 788f6cede7 | fix: Prevent Table Overflow and Clean Up CSS (#1876) | MEDIUM | GREEN | DIRECT | `div:has(> table):not(#LNReader-chapter){overflow:auto}` + next-button CSS cleanup. Fork's index.css still has pre-fix `border-width: 1` and no overflow rule (verified lines 86-89). Small, safe, user-visible (wide tables no longer overflow reader). | +| 28 | 7abee71ac3 | test: Generate Debug Environment Variables In Testing Workflow | NONE | β€” | NOT-FEASIBLE | CI-only. | +| 29 | 52d5c99c46 | fix: Order EPUB Chapters Across Paginated Pages (Closes #1762) | MEDIUM | YELLOW | MANUAL | Fork's `getNovelDownloadedChapters` (ChapterQueries.ts:478) orders by `position` only; fork's ChapterTable HAS `page TEXT DEFAULT "1"` (verified). Manual SQL change: `ORDER BY page ASC, position ASC` + rework the position range filter to limit/offset semantics. Fixes paginated-EPUB downloaded-chapter order. | +| 30 | 93bc5e5e4b | fix: Handle Missing MAL List Entries and Null Progress (Closes #1875) | MEDIUM | GREEN | DIRECT | Fork has the identical pre-fix code: SetTrackChaptersDialog.tsx:17,21 `progress.toString()` (null-crash) and myAnimeList.ts `getUserListEntry` without default-entry creation (verified). Applies cleanly; adds a test file. | +| 31 | 5d996f1388 | feat(downloads): Make Chapter Download Cooldown Configurable (#1834) | MEDIUM | RED | MANUAL | New `chapterDownloadCooldownMs` AppSettings + modal + replaces `sleep(1000)` in downloadChapter.ts. Fork's downloadChapter.ts:93 has the same hardcoded `await sleep(1000)` (verified) but fork's useSettings AppSettings interface differs β†’ manual port. Helps users hitting 429 rate limits. | +| 32 | bbb2645a6f | chore: Clean Reader Preview Content (#1867) | LOW | GREEN | DIRECT | Rewrites SettingsReaderScreen/utils.ts `dummyHTML` (soft-wrapped, table in div). Fork's utils.ts is identical to the pre-clean upstream file (verified β€” same hard-wrapped HTML + base64 img). Cosmetic/dev-facing. | +| 33 | 54fedc2f | fix(tts): Remove Leading and Trailing Quotes from Normalized Text (#1869) | HIGH | YELLOW | MANUAL (trivial) | 1-line addition to `normalizeText`. Fork's core.js:2203 normalizeText is byte-identical to upstream pre-fix (verified β€” lacks the quote-strip). Directly improves TTS output quality on the fork's crown-jewel feature. Manual 1-line apply (not a raw cherry-pick due to core.js divergence elsewhere). Test TTS pipeline. | +| 34 | e8c555025 | perf: Optimize Regex for Removing Extra Paragraph Spacing (#1872) | LOW | YELLOW | MANUAL | Fork's spacing block (core.js:4126-4139) has evolved (bionic/spacing rebuild). First `
`-strip pattern already equivalent (`\b[^>]*>`), second pattern at line 4139 is still the old `(?<=<\/?p>\s*
)`. Partially applicable; minor. | +| 35 | 57b9d41b5c | fix(service): Correct Notification Throttle Logic in ServiceManager.setMeta (#1873) | MEDIUM | GREEN | DIRECT | Fork's ServiceManager.ts:152-153 has the identical inverted-throttle bug (`> 1000` + wrong delay math, verified). The current code never throttles correctly and schedules negative-delay timers. 2-line fix; safe (fork still on old background-actions architecture, same as upstream pre-fix). | +| 36 | 34be7a1234 | fix(native-file): prevent fatal crash when unlinking missing files (#1832) | HIGH | GREEN | DIRECT | Fork's NativeFile.kt:133-135 is byte-identical (`if (!file.exists()) throw Exception(...)`, verified). This bug kills the whole process (uncaught exception on TurboModule thread) when deleting a downloaded chapter whose files were removed externally (e.g. backup restore). 1-line fix (`throw`β†’`return`). Top crash fix. | +| 37 | 60f42ed624 | chore(build): patch @rock-js/plugin-metro for Windows compatibility (#1830) | NONE | RED | NOT-FEASIBLE | rock-CLI workspace patch; fork doesn't use rock. | +| 38 | 25c93afabe | fix(reader): restore saved chapter progress from db if available (#1850) | MEDIUM | YELLOW | MANUAL | Fork's useChapter `getChapter` (useChapter.ts:125) uses `navChapter ?? chapter` with no DB progress hydration (verified). Add `dbChapter = await getDbChapter(chapter.id)` fallback (3 lines, same as upstream). Complements the fork's TTS Math.max reconciliation for the non-TTS reader-open path. | +| 39 | 2cd5a97ee8 | fix: await database operations inside transactions (#1845) | MEDIUM | RED | NOT-FEASIBLE | Drizzle `.run()` additions (op-sqlite tx API) β€” cannot port directly. CONCEPT applies: fork's NovelQueries.ts:180,321 use `withTransactionAsync` with non-awaited `runAsync(...).then(...)` (verified), a similar race-prone pattern. Recommend a manual audit of fork query transactions as a follow-up, not a cherry-pick. | +| 40 | 8f47e8fd1e | Theme switcher refactor (#1839) | MEDIUM | RED | MANUAL | Context-based useTheme + ThemePicker + appearance-settings rework (~20 files). Fork's useTheme.ts matches the upstream pre-refactor base (verified β€” imports defaultTheme, same MMKV hooks). Self-contained module; prerequisite for a clean port of #44c8e54ed80 (dynamic colors). Medium effort. | +| 41 | eafa545b2a | Use novel refactor (#1838) | MEDIUM | RED | NOT-FEASIBLE | 75 files / 6232 insertions: zustand novel store + MMKV adapter + Drizzle db.ts + LibraryUpdateQueries rewrite. Fork has its own useNovel.ts (16.6K) + raw-SQL layer. Maintainability-only value; too invasive. | +| 42 | b680a07750 | fix(browse): use gesture-handler Pressable in PluginListItem (#1828) | LOW | YELLOW | MANUAL | WSA mouse-tap fix. Fork's PluginListItem.tsx:2 still imports RN `Pressable` (verified). Trivial import swap; low value (Windows Subsystem for Android edge case). | +| 43 | 2a919ec0f9 | chore: gitignore tsconfig.tsbuildinfo build cache (#1829) | LOW | GREEN | DIRECT | 1-line .gitignore addition. Harmless housekeeping. | + +--- + +## Group summary + +### (a) Top 5 most valuable commits for the fork (value-per-effort) + +1. **34be7a1234 β€” NativeFile.kt unlink crash fix (HIGH, GREEN, DIRECT)** β€” Verified the fork has the exact same throwing code. Uncaught exceptions on the TurboModule thread kill the entire app when deleting a downloaded chapter whose files vanished (common after backup restore/external cleanup). One-line fix, zero risk, and it protects the Downloads "delete all" flow. Port first. +2. **54fedc2f β€” TTS leading/trailing-quote strip (HIGH, YELLOW, 1-line manual)** β€” Verified fork's `core.js normalizeText` lacks the strip. TTS currently reads stray quote characters aloud on every paragraph. Directly improves the fork's headline TTS feature; tiny change to a byte-identical function. +3. **57b9d41b5c β€” ServiceManager.setMeta throttle fix (MEDIUM, GREEN, DIRECT)** β€” Verified the fork has the identical inverted condition + wrong delay math (ServiceManager.ts:152-153). The notification throttle is effectively broken (negative-delay timers, no throttling). 2-line fix on an otherwise untouched file. +4. **89c43c1eef β€” Time Tracking & Statistics (HIGH, RED, MANUAL)** β€” The only genuinely *new* user-facing feature in this group that the fork completely lacks (fork's StatsScreen covers library counts only). Reading-time stats is a commonly requested feature. Must be re-implemented against the fork's raw-SQL MigrationRunner and useChapter/WebViewReader wiring (upstream version rides Drizzle + the useNovel zustand store). Bounded scope: one stats table, one hook (~59 lines), one screen. +5. **44c8e54ed80 β€” Dynamic Material Theme Colors (HIGH, YELLOW, MANUAL)** β€” Android 12+ Material You colors via an Expo module the fork can install. High appeal, and the fork's useTheme matches the upstream base so the adaptation is localized. Recommended order: port 8f47e8fd1e (theme refactor) first, then this. + +Honorable mentions (worth queueing): 71da09ed44 (in-chapter search β€” best reader feature but heavy core.js work), a16ec87631 (cover/list perf), 25c93afabe (DB progress restore), 93bc5e5e4b (MAL null fix, DIRECT), 52d5c99c46 (EPUB page ordering), 788f6cede7 (table overflow, DIRECT), 5d996f1388 (download cooldown), 3e1e211098 (webview address bar). + +### (b) Notable risks / red flags + +- **core.js divergence (54fedc2f, e8c555025, 71da09ed44):** The fork's `android/app/src/main/assets/js/core.js` has evolved independently (bionic reading, spacing rebuild, custom TTS speak/normalize). Only tiny additive edits are safe; the 442-line `search.js` feature must be re-integrated with the fork's DOM rebuild and TTS state machine. Any core.js change requires the fork's TTS wake-cycle/refill test suite. +- **Fork-critical file touches:** 89c43c1eef, 4a4208e336, 71da09ed44, 02b6b9848f, a16ec87631, 98a2a1d725, 5d996f1388 all touch WebViewReader.tsx / useChapter.ts / useSettings.ts / database β€” expect conflict-heavy manual ports. +- **Upstream-only infra (do NOT attempt):** c3c891cea0 (Expo managed), e4cf64a112 (WorkManager scheduling), eafa545b2a (zustand novel store), 2cd5a97ee8 + 4404494ec9 + 52d5c99c46 (Drizzle/op-sqlite APIs), 4cb1890b62 (Nitro C++), 916374b5df (repo shuffle), 9babe1676d (actively harmful β€” fork still needs background-actions). +- **Dep bumps (a5a6e8326f, 22a1efd1e5) need Android build + list virtualization regression checks.** +- **Value inflation trap:** 98a2a1d725 (scanlator) looks attractive but requires a schema migration + plugin-pipeline change to ever populate the column; only useful for aggregator sources. Do last, if at all. + +### (c) Architecture-divergence insights + +- **DB layer is the hard boundary.** Every upstream DB commit assumes Drizzle schema objects, `dbManager.write`, `tx.run()`, `drizzle/migrations/*/snapshot.json`. The fork's raw-SQL + custom MigrationRunner means these commits are never cherry-pickable β€” only their *intent* is portable (await-in-transaction, page ordering, scanlator column, stats tables). +- **WebView asset location differs.** Upstream moved reader JS to `assets/reader/js/core.js` (Expo-managed); fork keeps `android/app/src/main/assets/js/core.js`. Path-based diffs will never apply; diff at the function level instead. +- **Feature coupling upstream is a chain.** Time-tracking (#1886β†’#1890) and scanlator (#1864β†’#4404494ec9) are built on the useNovel zustand store (#1838) β€” a refactor the fork rightly rejects. Porting those features means unbundling them from the store and wiring them into the fork's ChapterContext/useChapter flow. +- **Fork equivalents confirmed (skip):** EPUB export (JS lib vs Nitro), WebView settings sync (fork's MMKV-listener + ref pattern), migration recovery (fork's MigrationRunner), background notification (fork's ServiceManager β€” but with the throttle bug above worth fixing), media notification (ttsNotification.ts), DoH, volume buttons, text cleanup. +- **Recommended batch order:** (1) trivial verified exact-match fixes 34be7a1234, 57b9d41b5c, 93bc5e5e4b, 788f6cede7, 54fedc2f, 25c93afabe, bbb2645a6f, 65e8610030, 2a919ec0f9; (2) small SQL/UI ports 52d5c99c46, a421177f0c, 3e1e211098, b680a07750, 5d996f1388; (3) themed feature train 8f47e8fd1e β†’ 44c8e54ed80; (4) big features 89c43c1eef, 71da09ed44, a16ec87631 as separate PRs with TTS regression testing. + +--- + +## Verification evidence (spot-checks performed) + +- Fork `android/app/src/main/java/com/rajarsheechatterjee/NativeFile/NativeFile.kt:133-135` β€” byte-identical pre-fix unlink (matches #1832 context). +- Fork `src/services/ServiceManager.ts:152-153` β€” identical inverted throttle (`now - lastNotifUpdate > 1000`, `delay = 1000 - now - lastNotifUpdate`). +- Fork `src/screens/novel/components/Tracker/SetTrackChaptersDialog.tsx:17,21` + `src/services/Trackers/myAnimeList.ts` β€” identical pre-fix (no default-entry creation). +- Fork `src/screens/reader/components/WebViewReader.tsx:473-490` β€” settings applied via injectJavaScript without WebView reload; `useTTSController.ts:591` reads CHAPTER_READER_SETTINGS directly β†’ #1884 already covered. +- Fork `android/app/src/main/assets/js/core.js:2203` normalizeText lacks quote-strip; `:4139` second spacing regex still pre-fix form. +- Fork `src/database/queries/ChapterQueries.ts:478` getNovelDownloadedChapters orders by position only; ChapterTable has `page TEXT` column. +- Fork `src/database/queries/NovelQueries.ts:180,321` β€” unawaited `runAsync` inside `withTransactionAsync` (concept of #1845 applies). +- Fork `src/screens/Categories/CategoriesScreen.tsx:94` β€” same index-based keyExtractor bug. +- Fork `src/screens/reader/hooks/useChapter.ts:125` β€” no DB progress hydration on open. +- Fork `src/screens/browse/components/PluginListItem.tsx:2` β€” RN Pressable (pre-fix #1828). +- Fork has NO scanlator column, NO timeSpent/session stats, NO dynamic theme, NO address bar, NO chapter search β€” features genuinely missing. diff --git a/specs/upstream-merge-analysis-2026-08/reference/subagent/group4-report.md b/specs/upstream-merge-analysis-2026-08/reference/subagent/group4-report.md new file mode 100644 index 0000000000..21eda22e2f --- /dev/null +++ b/specs/upstream-merge-analysis-2026-08/reference/subagent/group4-report.md @@ -0,0 +1,88 @@ +# Group 4 β€” Upstream Commit Analysis (41 commits) for Selective Porting + +**Scope**: commits from `original` (upstream/master), newest-first. Fork = branch `dev` (HEAD 40c6676797 v2.1.3), RN 0.82.1 / Expo 54 bare / expo-sqlite raw SQL / custom Kotlin TTS (TTSForegroundService.kt, TTSHighlightModule.kt) / react-native-paper 5.14.5. +All findings verified via read-only `git show`/`grep`/`read` against the fork tree. + +Legend β€” RISK: GREEN = new files or files fork didn't touch; YELLOW = some overlap, localized/portable; RED = fork-critical file or upstream-only infra. PORT: DIRECT / MANUAL / NOT-FEASIBLE. + +--- + +## Commit-by-Commit Table + +| # | commit (short) | subject | VALUE | RISK | PORT | rationale | +|---|---|---|---|---|---|---| +| 1 | 01596c0 | fix: Apply active filters to chapter count display (#1797) | MEDIUM | YELLOW | MANUAL | Real UX bug: `getChapterCount` ignores the unread/downloaded/bookmarked filter, so the header count stays at total. Upstream impl is Drizzle (`$count`+`and()`+`chapterFilterToSQL`); fork's raw-SQL `getChapterCount(novelId, page='1')` (ChapterQueries.ts:401, uses `getFirstSync`) needs a `filter` arg appended as SQL. `useNovel.ts` call sites (lines 105/270/291) are fork-modified β†’ manual. | +| 2 | 3de93c5 | feat: migrate from rn community cli to rock (#1812) | NONE | RED | NOT-FEASIBLE | rock CLI build orchestration (rock.config.mjs, env scripts, package.json/build.gradle/tsconfig). Fork is RN community CLI + Gradle 9.2.0 bare workflow. No user-facing value. | +| 3 | 306f5b2 | fix: install the right version of hermes compiler | NONE | RED | NOT-FEASIBLE | Package/lock pinning for Expo 55 toolchain. N/A to fork's Expo 54 dependency set. | +| 4 | 06f0f66 | fix: Add hermes-compiler dev dep | NONE | RED | NOT-FEASIBLE | Same infra category; package.json/pnpm-lock only. | +| 5 | 64921a0 | feat: Expo 55 upgrade | NONE | RED | NOT-FEASIBLE | Upstream migrated to Expo managed workflow; 42 files incl. MainApplication.kt, build.gradle, db.ts, useNovel, ChapterItem, reader bottom sheet. Fork is Expo 54 bare with custom Kotlin. Upgrade decision belongs to the fork itself, not a cherry-pick. | +| 6 | 44e035a | chore: Update Translations (#1683) | LOW | GREEN | DIRECT | Crowdin sync across 34 string files. Fork's strings/languages are heavily customized (custom TTS strings); a raw overwrite would clobber fork strings. Only take if diffed per-key. | +| 7 | 1803cd0 | feat: Add testing support for react code (#1763) | LOW | YELLOW | MANUAL | Test infra (babel config, jest mocks, test-utils). Fork already runs 1235 tests with its own jest setup; useful only as inspiration for useNovel tests. | +| 8 | e4d7ba3 | fix: Safety level may not be changed inside a transaction error (#1758) | LOW | YELLOW | MANUAL | Splits PRAGMA execution out of the DB bootstrap transaction. Drizzle `db.ts` diff, but the *concept* (don't run `PRAGMA`/`safety_level` inside a transaction) could apply to fork's own db bootstrap. Not verified as a live fork bug β†’ low priority. | +| 9 | cc04287 | feat: Replace TTS Notifications with Android MediaSession Controls (#1711) | ALREADY-HAVE | RED | NOT-FEASIBLE | Fork already ships a 5-button TTS media notification (ttsNotification.ts + DebugMediaButtonReceiver.kt). Upstream's MediaSession adds lock-screen controls/seekbar/album art but is built on its Nitro TTS stack and rewrites WebViewReader.tsx, core.js, MainApplication.kt, build.gradle. Fork's equivalent notification is functional; port not worth the risk. | +| 10 | b8c177b | refactor: Migrate to Drizzle and op-sqlite (#1735) | NONE | RED | NOT-FEASIBLE | Complete DB-layer rewrite (42 files). Fork deliberately uses expo-sqlite + raw SQL tables + custom MigrationRunner. Directly contrary to fork architecture. | +| 11 | 7083c51 | fix: Include selectedIds in Chapter List extraData (#1750) | LOW | YELLOW | MANUAL | LegendList selection-highlight re-render fix. Fork's NovelScreenList.tsx:329 already passes `selected.length` in extraData, so the fork is largely covered; verify selection toggle of equal-length sets before porting. | +| 12 | f069320 | fix: Improve Next Chapter Selection Logic (#1744) | MEDIUM | YELLOW | MANUAL | Bug fix: `getNextChapter` misbehaves when `position=0` across pages and lacks `LIMIT 1`. Fork's getNextChapter (ChapterQueries.ts:441) has the *identical* pre-fix SQL (`(position = 0 AND page > ?)` / `ORDER BY position ASC, page ASC`). Port = copy the corrected SQL. Port together with d15cf98. | +| 13 | 3f8071e | chore: Release v2.0.3 | NONE | YELLOW | NOT-FEASIBLE | Version bump (build.gradle, package.json, issue templates). Fork is v2.1.3 with its own versioning. | +| 14 | a1c5350 | fix: Miscellaneous UI Tweaks | LOW | YELLOW | MANUAL | Cosmetic tweaks; the NovelInfoHeader change is a 230-line rewrite that conflicts with fork's customized header (TTS/continue-reading FAB). Low payoff. | +| 15 | 8f23790 | feat: Show First Unread Chapter in ReadButton and FAB When No Last Read | MEDIUM | YELLOW | MANUAL | Nice UX: "start reading" jumps to first *unread* chapter instead of chapters[0]. Fork's ReadButton.tsx currently falls back to `chapters[0]` (possibly read). Port needs: new `getFirstUnreadChapter` SQL + useNovel exposure + ReadButton/NovelScreenList wiring. Fork has no first-unread logic today (verified). | +| 16 | 11ff69d | fix: Pre-Fetch Adjacent Page Chapters in Reader and Library Updates | MEDIUM | RED | MANUAL | Reader pagination UX + library update preload. Touches `useChapter.ts` β€” fork's reader hook is heavily customized for TTS progress sync (fork-critical). High conflict surface; only worth porting the LibraryUpdateQueries part. | +| 17 | d15cf98 | fix: Order Chapter Pages Numerically in Queries (#1742) | MEDIUM | YELLOW | MANUAL | Bug fix for multi-page novels: `getCustomPages`, `getPrevChapter`, `getNextChapter` now `CAST(page AS INTEGER)` sort/compare. Fork's versions (ChapterQueries.ts:337, 422, 441) have the identical buggy SQL. Direct SQL copy, localized. | +| 18 | 29bd661 | refactor: Optimize Library Screen Performance and Fix Category Bug | MEDIUM | RED | MANUAL | Library perf (SelectionContext, memoized items, Set lookups) + a real "Update Category" index bug. Fork's LibraryScreen/useLibrary are heavily rewritten (selection + updates integration), so this is a re-implementation, not a patch. Value real but effort high. | +| 19 | ab1e36d | refactor: Update Components to Follow Material Design 3 Guidelines | LOW | YELLOW | MANUAL | MD3 polish across 13 shared components (Switch, TabBar, Chip, BottomSheet…). Fork on paper 5.14.5 with custom-scaling components; cosmetic, risky churn for little user-visible gain. | +| 20 | f1fdafd | fix: Prevent Tab Bar Height Jump and Shadow Flash on Browse Screen | LOW | GREEN | DIRECT | Small, self-contained: add `initialLayout` to TabView + border instead of shadow in BrowseScreen.tsx. Fork's BrowseScreen still uses plain TabView (verified) β†’ applies cleanly. Low value but safe. | +| 21 | 4c7684a | refactor: Optimize Novel Screen Performance and Fix Download State Tracking | MEDIUM | RED | MANUAL | Real wins (Set-based download tracking, memoized ChapterItem, state-mutation bug in updateChapter) but rewrites NovelScreen/ChapterItem/NovelInfoHeader/useNovel β€” all fork-modified. Re-implement selectively (e.g., the updateChapter state bug) rather than port wholesale. | +| 22 | 2f45db6 | fix: Replace Sync Database Operations With Async to Prevent Database Lock (#1716) | MEDIUM | RED | MANUAL | Correct stability direction (sync `getAllSync`/`getFirstSync` calls cause SQLITE_BUSY). Fork's DB layer still uses sync calls (e.g., getCustomPages, getPageChaptersBatched, getChapterCount). Broad change across queries/useNovel/library/downloadChapter/migrateNovel; worth doing incrementally as a fork-side effort, not a cherry-pick. | +| 23 | 9e38ad1 | fix: Sanitize EPUB FileName to Remove Filesystem-Unsafe Characters (#1480) | MEDIUM | GREEN | DIRECT | 1-line fix in ExportNovelAsEpubButton.tsx:140. Fork has the identical pre-fix line (`novel.name.replace(/\s/g,'')`). Filesystem-unsafe chars break EPUB export. Zero conflict. | +| 24 | 45c4ea8 | fix: Handle IOException in NativeFile to Prevent EPUB Import Crash Loop (#1315) | MEDIUM | GREEN | DIRECT | Fork's `android/.../NativeFile/NativeFile.kt` is byte-identical to upstream's pre-fix version (verified: `writeFile`/`readFile`/`copyFileContent` unwrapped). Wrapping IOExceptions stops native bridge crash loops on stale MMKV import tasks. Best cherry-pick candidate in the group. | +| 25 | d2b95ae | fix: Use NativeFile for Custom CSS/JS Import to Fix Content URI Read Failure (#1661) | MEDIUM | YELLOW | MANUAL | Fork's AdvancedTab.tsx:206 still uses `StorageAccessFramework.readAsStringAsync` (pre-fix). Fork's NativeFile exposes the same copyFile/readFile API, so the replacement is straightforward; verify fork's imported-file temp path handling. | +| 26 | 0212115 | fix: Stabilize Scroll Handler References to Prevent Infinite Re-render Loop (#1679) | LOW | YELLOW | MANUAL | useCallback/useRef stabilization in MalTopNovels + NovelScreenList. Fork has both files but with local modifications; low value, safe to skip or port the MalTopNovels half. | +| 27 | caed965 | fix: Prevent EPUB Import Crash from DocumentPicker Cache Cleanup (#1725) | MEDIUM | YELLOW | MANUAL | 1-line: `copyToCacheDirectory: false` in LibraryScreen.tsx:167 (fork still `true`, verified). Applies nearly directly despite fork's LibraryScreen differences. | +| 28 | 4088d59 | fix: Close OkHttp Response and Streams in NativeFile.downloadFile (#1740) | MEDIUM | GREEN | DIRECT | Fork's NativeFile.kt `downloadFile` matches upstream pre-fix exactly (verified: leaky `response.body!!.byteStream()` without `use`). Connection/memory leak fix for batch image downloads. Direct port. | +| 29 | a062bee | fix: Add Max Height to Set Categories Modal (#1738) | LOW | GREEN | DIRECT | Adds `maxHeight: 40% window` to the category FlatList. Fork's SetCategoriesModal.tsx lacks it (verified). Trivial, safe. | +| 30 | 3095882 | Replace react-native-paper menus with custom implementation (#1733) | LOW | YELLOW | MANUAL | Drops paper `Menu` for a custom one (4 files). Fork's Menu/index.tsx still imports paper `Portal` (verified). Cosmetic/architectural prep on upstream's path to de-papering; low fork value, medium churn. | +| 31 | 4ad0639 | Fix: Chapter loading for jump to chapter modal and chapter drawer | MEDIUM-HIGH | YELLOW | MANUAL | Real bug: jump-to-chapter modal and chapter drawer can't reach chapters beyond the preloaded batch. Upstream adds `getNovelChaptersByNumber`/`getNovelChaptersByName` (raw SQL β€” directly compatible with fork) + `loadUpToBatch` in useNovel + modal/drawer wiring. Fork has JumpToChapterModal.tsx and ChapterDrawer/index.tsx. Ignore the pnpm-lock churn in the commit. Best candidate among useNovel-touching commits. | +| 32 | b53ec33 | feat: Support Additional Plugin Setting Types (Dropdown and Checkboxes) | MEDIUM | YELLOW | MANUAL | Plugin-ecosystem feature: sources can declare dropdown/multi-checkbox settings. Fork's `src/plugins/types/index.ts` and SourceSettings.tsx are pre-fix (verified: no dropdown/checkbox types). Useful for richer sources; needs adaptation to fork's plugin types. | +| 33 | c40edd5 | feat: Add Kitsu Tracker (Closes #416) | HIGH | YELLOW | MANUAL | Biggest user-facing feature in the group: new Kitsu tracker service (kitsu.ts, ~490 lines) + score selectors, tracker cards, settings screen, login dialog rework. Fork has the full tracker infrastructure (aniList/MAL/MangaUpdates; TrackerCards/ScoreSelectors/SetTrackScoreDialog exist) with a different structure β†’ port as new service + adapt UI wiring. High value for tracker users. | +| 34 | bdd42ca | chore: Update Repository Links to lneader Instead of LNReader | NONE | YELLOW | NOT-FEASIBLE | Rebranches repo links/org rename across 40 string files. Fork is a separate project (bizzkoot/lnreader) β€” adopting would point users at the wrong repo. Do not port. | +| 35 | ab4eee3 | feat: Allow All Safe Tags and Attributes When Sanitizing Chapter HTML (#1715) | MEDIUM | YELLOW | MANUAL | Renders tables/footnotes/del-ins/epub:type etc. in chapter HTML. Fork's sanitizeChapterText.ts is the pre-fix version + fork's own `span[style]` (adopted #1573). The change is additive; must reconcile with fork's `style`-preservation and check it doesn't bloat TTS paragraph extraction (htmlParagraphExtractor.ts consumes sanitized HTML). | +| 36 | 3f9886a | chore: Update Repository Links to lneader (dup) | NONE | YELLOW | NOT-FEASIBLE | Same as bdd42ca. Do not port. | +| 37 | 198b744 | feat: Add RTL (Right-to-Left) Language Support (#1717) | MEDIUM | RED | MANUAL | 44 files, 770 insertions: RTL layout across every screen incl. WebViewReader.tsx (fork-critical, 84-line diff), TTSTab.tsx (fork deleted/renamed β†’ ReaderTTSTab.tsx), SettingsReaderScreen/AdvancedTab (fork-modified). Real feature for Arabic/Urdu/Hebrew users but requires a dedicated fork-side porting effort. Do not attempt as cherry-pick. | +| 38 | f9c5ca3 | chore: Update Indonesian Translations (Closes #1653) | LOW | GREEN | DIRECT | id_ID strings re-sync (444 changed lines). Fork's id_ID differs; diff per-key before adopting. | +| 39 | 9922d43 | fix: Remove Missing Image References During EPUB Export (#1696) | MEDIUM | YELLOW | MANUAL | Removes `
`/`` pointing at deleted files during EPUB export. Fork's ExportNovelAsEpubButton.tsx is at the pre-fix state (verified: `readFile` without image cleanup). Ports cleanly; watch fork's chapter path layout (uses same NOVEL_STORAGE layout). | +| 40 | ae99b07 | feat: Add `@noble/ciphers` for Plugins (Closes #1709) | MEDIUM | YELLOW | MANUAL | Adds cipher support (AES etc.) exposed to source plugins in pluginManager.ts. Fork's pluginManager.ts is custom (per-plugin sandboxing); adding the dep + `crypto` surface is a small manual job with clear ecosystem benefit. | +| 41 | 0cb9da9 | refactor: Replace `withTransactionAsync` With `withExclusiveTransactionAsync` (#1636) | NONE | RED | NOT-FEASIBLE | Drizzle/op-sqlite transaction API. Fork's raw-SQL transaction handling is entirely different. Skip. | + +--- + +## Group Summary + +### (a) Top 5 most valuable commits for the fork + +1. **c40edd5 β€” Kitsu tracker (HIGH)** β€” The only new user-facing feature in the group. Kitsu has a large dedicated readership community. The tracker service is a new file (kitsu.ts); the fork's tracker infra (ScoreSelectors, TrackerCards, SettingsTrackerScreen, login dialog) already exists, so the port is additive wiring rather than a rewrite. Best value-per-effort in this group. + +2. **4ad0639 β€” Jump-to-chapter / chapter-drawer loading (MEDIUM-HIGH)** β€” Fixes a genuinely broken UX: chapters not yet in the loaded batch are unreachable from the jump-to-chapter modal and the chapter drawer. Upstream's new queries are raw SQL (`position = ?`, `name LIKE ?`) that map 1:1 onto the fork's expo-sqlite layer. `loadUpToBatch` in useNovel is the only messy bit (fork-modified hook). Skip the commit's pnpm-lock churn. + +3. **45c4ea8 + 4088d59 β€” NativeFile.kt IOException wrap + OkHttp `response.use` (MEDIUM)** β€” Two real stability fixes (import crash-loop, connection/memory leak during batch image downloads). Verified the fork's `NativeFile.kt` is byte-identical to upstream's pre-fix state, so these cherry-pick **directly** β€” the lowest-risk high-value ports in the whole group. + +4. **d15cf98 + f069320 β€” Numeric page ordering / next-chapter SQL fixes (MEDIUM)** β€” Multi-page novels get wrong prev/next chapter navigation and unordered page lists because `page` is text-compared. The fork's `getCustomPages`/`getPrevChapter`/`getNextChapter` still carry the identical buggy SQL; porting is copying corrected SQL into ChapterQueries.ts (3 functions, no API change). Port both together since they touch the same lines. + +5. **9e38ad1 + 9922d43 β€” EPUB export filename sanitization + missing-image cleanup (MEDIUM)** β€” Export is currently broken for titles containing `/ : * ? " < > |` (9e38ad1 is a 1-line direct port) and produces broken image placeholders (9922d43). Both land in ExportNovelAsEpubButton.tsx which is unmodified in the fork. High value for EPUB users, near-zero risk. + +Also worth scheduling (not top-5 but valuable): **2f45db6** (async DB ops β†’ SQLITE_BUSY stability) and **29bd661** (library category bug + perf) β€” both are MEDIUM value but RED-risk because they rewrite fork-modified files; treat as fork-side re-implementations, not cherry-picks. **01596c0** (filtered chapter count) is a nice small manual port. **b53ec33** (plugin dropdown/checkbox settings) and **ae99b07** (@noble/ciphers) strengthen the fork's plugin ecosystem. **d2b95ae** (CSS/JS import) and **caed965** (EPUB picker) are two small crash-fix manual ports that apply to fork files still in pre-fix state. + +### (b) Notable risks / red flags + +- **NOT-FEASIBLE infrastructure cluster**: `3de93c5` (rock CLI), `64921a0` (Expo 55), `b8c177b` (Drizzle+op-sqlite), `0cb9da9` (withExclusiveTransactionAsync), `e4d7ba3` (Drizzle db.ts), `306f5b2`/`06f0f66` (hermes pinning). These are upstream-only infra; attempting them drags the fork toward the managed/Drizzle architecture it deliberately diverged from. +- **RED on fork-critical files**: `11ff69d` (useChapter.ts β€” TTS progress sync hook), `2f45db6` (entire DB query layer + useNovel), `29bd661` (library screen), `4c7684a` (novel screen + ChapterItem + useNovel), `198b744` (RTL β€” 44 files incl. WebViewReader.tsx), `cc04287` (WebViewReader/core.js/native β€” ALREADY-HAVE feature anyway). +- **ALREADY-HAVE**: only `cc04287` (fork's own 5-button TTS media notification). The upstream MediaSession implementation adds seekbar/album art but rides on their Nitro TTS stack β€” do not port. +- **Do-not-port churn**: `bdd42ca`/`3f9886a` (repo-link rebrand to upstream's new org β€” actively wrong for a fork), `3f8071e` (release bump), translation syncs (`44e035a`, `f9c5ca3`) risk clobbering fork's customized string files. +- **Conflict magnet**: `useNovel.ts` is touched by 6+ commits in this group (01596c0, 8f23790, 4ad0639, 2f45db6, 4c7684a, 1803cd0). Any port touching it should be sequenced and regression-tested against the fork's 1235-test suite (TTS wake/refill tests especially). + +### (c) Architecture-divergence insights + +1. **The fork's raw-SQL layer is a *portability advantage***: upstream commits written against pre-Drizzle SQL (d15cf98, f069320, 4ad0639's new queries, 01596c0's concept) map directly onto fork's expo-sqlite `db.getAllAsync`. The fork is effectively pinned to the *pre-Drizzle* upstream codebase for DB logic β€” good news for these specific fixes. +2. **TTS is a hard wall**: every upstream commit touching playback/native TTS (cc04287) assumes the Nitro/MediaSession stack; the fork's ForegroundService + TTSHighlightModule + WebView bridge is a parallel universe. Only UI-layer fixes that don't cross the TTS boundary are safe. +3. **NativeFile.kt is a shared, stable seam**: both trees carry the same TurboModule file. It's the single most reliable place for direct cherry-picks (45c4ea8, 4088d59) and a good test case for validating the port workflow. +4. **Upstream is de-papering** (3095882 custom Menu β†’ MD3 refactors); the fork remains on react-native-paper 5.14.5 with custom UI scaling. UI refactor commits will keep conflicting for the foreseeable future β€” port only user-visible fixes (f1fdafd, a062bee, 9e38ad1), not architecture churn. +5. **useNovel.ts / novel-screen files are the fork's second conflict hotspot** after TTS files; bundle their ports (8f23790, 4ad0639, 01596c0) into one coordinated change with dedicated tests. diff --git a/specs/upstream-merge-analysis-2026-08/reference/upstream_commits_170.txt b/specs/upstream-merge-analysis-2026-08/reference/upstream_commits_170.txt new file mode 100644 index 0000000000..631b933afe --- /dev/null +++ b/specs/upstream-merge-analysis-2026-08/reference/upstream_commits_170.txt @@ -0,0 +1,170 @@ +c3260e8e01e4704b57137d787ffe9cfec7941cbc|Rajarshee Chatterjee|2026-08-01|fix: Resolve Chapter Download Queue and Removal Issues (Closes #1937) +c0877a9b0656e22c3c788854006fb456c3ed704a|Rajarshee Chatterjee|2026-08-01|feat: Add Ignorable App Update Notifications +96fb52df1730912c40ee6918cf6a6fcd74e31cc8|Patrick Loser|2026-08-01|feat: Enhance Stats With Genre Taxonomy and Charts (#1919) +76106317b68e8aade12b868afb16dda4671ceb16|Rajarshee Chatterjee|2026-07-31|fix: Restore Midnight Dusk Chip Contrast (Closes #1939) +e75f4fba5da08e2317cfed2fe92b8de09457475c|K1ngfish3r|2026-07-29|feat: Add More Novel Statuses and Icons (#1931) +4dc933ee5a361eb0535120fa192c1eb51ba8cc70|Rajarshee Chatterjee|2026-07-29|fix: Remove White Flash During Screen Transitions (Closes #1926) (#1927) +bfb12b04aa31e3013f50503697253b549ee9618c|Rajarshee Chatterjee|2026-07-29|feat: Add Configurable Automatic Backup Scheduling +b7639ab55a308c55617f0088c193052b9e0bd2b2|Patrick Loser|2026-07-29|feat: Upload JavaScript Source Maps (#1924) +ecc5b84a64ac1aa841583acb43d7ac8aab5a72c5|github-actions[bot]|2026-07-29|chore: Release v2.1.2 +eb12bdfd623bd28c2c3f88f585fefca9352711a4|Rajarshee Chatterjee|2026-07-29|fix: Prevent Novel Genre Rendering Crashes +9612bf6f893d0078c566f9651405097b7f8d1f24|Rajarshee Chatterjee|2026-07-29|chore: Update Translations (#1928) +0ed7d878b4b75e1f6ebc83d0978f05cad5920014|Rajarshee Chatterjee|2026-07-29|fix: Prevent White Reader Drawer Seam +5f45e4c7f2d4f77da3b73bdfdfcdb5b477fd6849|Rajarshee Chatterjee|2026-07-28|fix: Recover Interrupted Chapter Migrations +ca24671a2f847c5722009b6b8451e9c54d3a2c12|Rajarshee Chatterjee|2026-07-28|fix: Support Fork-Safe Preview Builds +ee3a4f20bc41ccf9e3f33ce37e7191cd5c7e6a17|Rajarshee Chatterjee|2026-07-28|fix: Preserve Default Category After Reordering (Closes #1922) +0c3542836c847c0e824e473c06394bd5219d6329|Rajarshee Chatterjee|2026-07-28|fix: Synchronize Library Updates And Optimize Import Refreshes +f71c9c859471968684baef6e7f3da55a9d2f5821|Patrick Loser|2026-07-28|fix: Ignore Discord Notifications Outside the Main Repository (#1923) +f53546cc01d76b8b3f73ed4a3b88b374f775d746|github-actions[bot]|2026-07-28|chore: Release v2.1.1 +594b078fad3308c284b3b9cd8484056b40d3c821|Rajarshee Chatterjee|2026-07-28|fix: Harden Database Migration Recovery +ac1b2f5ce446ae90db315be11d1494bc49539f40|Rajarshee Chatterjee|2026-07-27|feat: Add Durable Completion Notifications +648d21062f8812ae2a8526954a374424c0dede3c|Rajarshee Chatterjee|2026-07-27|feat: Add Parallel Task Queues With Per-Plugin Download Limits +89d715ce6bc22ea15885ff86b3359033f3253b4c|Rajarshee Chatterjee|2026-07-27|docs: Replace Beta Badge With Build Preview Status +bd527a02efa9d642b7168e432bfb965a0ae85f51|Rajarshee Chatterjee|2026-07-27|feat: Improve Backup Selection, Notifications, And Plugin Restore +11117c1b5404820868683522242296d4f61d7642|Rajarshee Chatterjee|2026-07-27|feat: Add GitHub-Style Embed For Nightly Notifications +ba25d5b8fe3af0b006ad09293a499b58cbee1479|Patrick Loser|2026-07-27|feat: Add Rozenite DevTools Integration (#1912) +31cb4b994a332bd6f99864ba792aaa988bf4720b|Rajarshee Chatterjee|2026-07-27|fix: Align Bottom Navigation With Material 3 Specs +72ef618af1bdb6a40783ab119649e302a89e12d8|Rajarshee Chatterjee|2026-07-27|fix: Preserve Existing Data During Novel Counter Migration +936fd223bbde71c3e0496f22ea4f28c9714d7987|Rajarshee Chatterjee|2026-07-27|feat: Add Discord Notifications For Nightly Builds +6d26438d2dd5b7faf21786bc390ae83b43aca6d5|Rajarshee Chatterjee|2026-07-27|test: Update Expected Database Migrations +5eb5a55c4c654c046f984f1e324b36587389dfa8|Rajarshee Chatterjee|2026-07-27|fix: Repair Skipped Novel Counter Migrations +6cd86d1422de47dc8e08c53e645c678b39ed66cc|Rajarshee Chatterjee|2026-07-27|chore: Update Translations (#1906) +9783c4d5e3a0f7e5736d1fa50a8dd360a760f76c|Patrick Loser|2026-07-26|fix: Normalize Repository Back Navigation (#1905) +87f602a1fdaaf4456b1947a6097b5a7e65caa1a3|github-actions[bot]|2026-07-26|chore: Release v2.1.0 +3d32ddd6e933d5b5208f7813b99b8755e700d483|Rajarshee Chatterjee|2026-07-26|fix: Improve Release Selection And APK Verification +7ddd934c2b834ba8fbed6969dab89728e3316251|Rajarshee Chatterjee|2026-07-26|fix: Handle Missing Tracker Client IDs +b53c91f748581227d63a28d28d33de87c1cbb296|Rajarshee Chatterjee|2026-07-26|chore: Separate Jest And Type Check Workflows +9d106a371620a513f1f4cf57f511936866a77fd2|Rajarshee Chatterjee|2026-07-26|feat: Add Semantic Version Bump Selection +8b0a8dc4c07b6d77532ef9b070bbce288afd92d4|Patrick Loser|2026-07-26|chore: Update Husky Pre-Commit and Pre-Push Hooks +72bfcad03469440c1e9cf5ff88b5ff97a544c45e|Patrick Loser|2026-07-26|fix: Resolve Type and Lint Issues +345d084eba5f1862c37d15c3790ce36610116d70|Rajarshee Chatterjee|2026-07-26|feat: Add Chapter Numbers To EPUB Titles (Closes #1405) +10b6042a9c671f47ba33f7810c47935a54706f40|Rajarshee Chatterjee|2026-07-26|feat: Add EPUB Export Progress Notifications +bd7161172551568e579e60ece7072f8c5a05fa65|Rajarshee Chatterjee|2026-07-26|feat: Improve Chapter Drawer And WebView App Bar +8f53550d2c6ec75b8d9cefe0142218aaec26f726|Rajarshee Chatterjee|2026-07-26|fix: Restore Tracker Search Requests +52914b978df3267342b9cd3d18270bf19ae5a7cb|Rajarshee Chatterjee|2026-07-26|fix: Migrate Volume Button Events to Expo Listener API +ff12759a66ad68791d7b8270579b8f7d020e73f5|Rajarshee Chatterjee|2026-07-26|ci: Run Preview Builds Daily When Missing +e62868b82ccd1784ad83cd00d338c47d2818df94|Rajarshee Chatterjee|2026-07-26|feat: Automate Draft Android Releases +97be48dfe17b6a5261284cc3052ab5dcbab8d20d|Rajarshee Chatterjee|2026-07-26|feat: Add Migration Review Options +cbe7d70e2df349db7ebf36a448e1f84b3903fb78|Rajarshee Chatterjee|2026-07-26|fix: Restore Collapsed TTS Button Interaction +6548e9a6f8efc0bb21e46af005072d7599b326e9|Rajarshee Chatterjee|2026-07-26|fix: Improve Paged Reader Layout and Performance +138fdff2e57e55e02f9775d11dfb47482701f5c5|Rajarshee Chatterjee|2026-07-26|fix: Prevent Duplicate WebView Bottom Insets +f1dd4b77a891e501b9345e5d0fe81b6405bce450|Rajarshee Chatterjee|2026-07-26|feat: Improve Empty Chapter Reporting +9b356ffd17c28d7f37285157f3f34488af67a266|Rajarshee Chatterjee|2026-07-26|fix: Preserve Client IDs And Improve Environment Generation +5cf7e15d7631f3ada56d4cb979694214745bd10d|Rajarshee Chatterjee|2026-07-26|feat: Add Reader Setting Descriptions +6a7e90c179b735521aa9d778e68f1727607e8ac5|Rajarshee Chatterjee|2026-07-26|fix: Align Advanced Reader Tabs And Localize Actions +99c31d56bb45c6e04fe96cff3f14c3ca55fccba4|Rajarshee Chatterjee|2026-07-26|fix: Use MD3 Outline Variant For Tab Borders +4d98247980c21f892df6cf0da19ecad8a6f0d136|Rajarshee Chatterjee|2026-07-26|feat: Add Android DNS Over HTTPS Support +164f5d259243a68f14a7e8a069b2e2cfc42aab84|Rajarshee Chatterjee|2026-07-25|feat: Add Automatic Library Update Scheduling +89cddf9e3d07d3f7a7c7cb71e8cc8e5e4e96fcc3|Rajarshee Chatterjee|2026-07-25|fix: Expand Discover Row Ripple +eac6ad584e85d4ed72cf76fafcde1c4d0551687d|Rajarshee Chatterjee|2026-07-25|feat: Improve Novel App Bar And EPUB Export UX +93fe04c28cc4776a96f86aa157b729ddf3c218c3|Rajarshee Chatterjee|2026-07-25|feat: Polish Reader Controls And Navigation +c9358b3a9fc49ad6a98ac0293da46bf9eab39bf8|Rajarshee Chatterjee|2026-07-25|feat: Update Android Branding Assets +67d2c7c01b101d83d0033aad2c0a99247f04620e|Rajarshee Chatterjee|2026-07-25|feat: Add Collapsible TTS Controls And Stable Notification Progress +c30441b9493d5e75fbe82a608b034c6239fa493f|Rajarshee Chatterjee|2026-07-25|feat: Add Smart Library Update Filters +6d2a9f8e1530604860375a9be3abb92b4502d01b|Rajarshee Chatterjee|2026-07-25|fix: Prevent Slider Flicker After Release +799845426c8f2e651de508d471d8319add41d2cc|Rajarshee Chatterjee|2026-07-25|feat: Add Parallel Library Updates Across Sources +8ac628b3deda367f68af93f9f43d7a1da8c80f9d|Rajarshee Chatterjee|2026-07-25|fix: Improve History Removal Flow. +d02291c22c079e5faee9817b00c7bd87ed7f47da|Rajarshee Chatterjee|2026-07-25|feat: Remember Last Used Library Category +7af0911f5b17e1c606dbf60e0f1d1b8863c78221|Rajarshee Chatterjee|2026-07-25|feat: Add High-Resolution Android Flavor Assets +098782d6aa9a188bf007402155bd66e3dbc14523|Rajarshee Chatterjee|2026-07-25|refactor: Simplify Settings Layout And Theme Selection +732628c40bb3f1afbbb0336577f9a56f97ef9115|Rajarshee Chatterjee|2026-07-25|feat: Add Date Format And Relative Timestamp Settings. +5e62c36a9b0c0d7ca562383f2d9938e722216f00|Rajarshee Chatterjee|2026-07-25|feat: Add Library Settings And Category-Based Global Updates +3d102a52a7d780b096afd78c6d5214e46b670b47|Rajarshee Chatterjee|2026-07-25|perf(reader): Cut Chapter Open Latency and Reading-Time Re-Renders (#1899) +4dcf21303715793087ea3bd49a9d510943e30944|Rajarshee Chatterjee|2026-07-25|fix: Restore Reader Changes Reverted During Expo Migration +4de82472ee70fd0c36114f0ee2e213ff6bf81099|Rajarshee Chatterjee|2026-07-25|fix: Resolve Require Cycles And LegendList Render Updates +58db0352e6359d4b1c69391d260105abbe4ede60|Rajarshee Chatterjee|2026-07-25|fix: Restore Metro Reader Asset Middleware +74808f086e5e3fbd479138681d700814c3370386|Rajarshee Chatterjee|2026-07-24|feat: Replace Legacy Media Controls with Nitro-Powered Playback (#1896) +b96351ea8117f4a1e464b0a7ab1820a557172c92|amir-rezaei|2026-07-24|docs: Fix Contraction and Missing Article in `TESTING.md` (#1895) +eb8dc6df368ba7628178d6481294fca1b30f9019|amir-rezaei|2026-07-24|docs: Fix "Withoud" Typo in `CONTRIBUTING-NIX.md` (#1894) +0c8546188eb33a0ac3be46596503cd38e4fb4d75|Rajarshee Chatterjee|2026-07-24|feat: Add MD3 Slider Component. +e9f6bdaa203890fb7d69b261f31afc431fba429c|Rajarshee Chatterjee|2026-07-24|fix: Modernize Menu Styling And Outside-Tap Dismissal +3d34658d0faee3f1a1dce6ed77f745804851c4c0|Rajarshee Chatterjee|2026-07-24|feat: Add Material 3 Top Tab Indicators +d44ce7412ac512b392d35e33d6dc9d98587a2350|amir-rezaei|2026-07-24|docs: Fix Trailing Period, Organization Expansion, and Comma in `CONTRIBUTING.md` (#1893) +17c891e1334409fa29d4a365b9ba46014b81b591|Rajarshee Chatterjee|2026-07-24|refactor: Standardize Bottom Sheet UX and Styling +9805da63cf23d58f0561b9335e0a07c1789e9a82|Rajarshee Chatterjee|2026-07-24|refactor: Optimize Novel Screen Performance And Architecture +afc20277e0f8a8d4b49d1ebd1c7689c599ba2e06|Rajarshee Chatterjee|2026-07-24|fix: Restore Android Build Types For CI +14061d207b77d8930c4d15af135189ccb4f4bd9e|amir-rezaei|2026-07-24|docs: Fix Title Case Headings, GitHub Capitalization, and E.g. Formatting in `CONTRIBUTING.md` (#1892) +916374b5df3cfa3b09d464109340ad81bdbef457|Rajarshee Chatterjee|2026-07-24|refactor: Reorganize Repository Structure +0a2a6c96ca453fd68ce42d4723c0f42c0a730519|Rajarshee Chatterjee|2026-07-24|feat: Add Variant-Specific Android Splash Screens +65e86100304ae27ba8049937558b4e25b49252f6|Rajarshee Chatterjee|2026-07-24|refactor: Replace Bottom Navigation Shift With Fade +1322065290f63141633c30cd3013b7ba81c12fae|Rajarshee Chatterjee|2026-07-24|feat: Add Variant-Specific Android App Icons +22a1efd1e5c548a27b3ab3b8c54909a5092c1e37|Rajarshee Chatterjee|2026-07-24|perf: Optimize React Native Skeleton Loading Components +269606ebb8e352f72fbab401bd5d93ed15c1a7a2|Rajarshee Chatterjee|2026-07-24|fix: Resolve Nitro EPUB Import Alias +4cb1890b62eb57cc75ca0628a11be312c930ddc4|Rajarshee Chatterjee|2026-07-24|feat: Add Nitro EPUB Exporter +a421177f0cb5b74e30b869c6595ed12c9263586a|Rajarshee Chatterjee|2026-07-24|fix: Improve Category Badge Layout And Drag Reordering +a16ec876311692a6cb3da9348d09e7cf287c9fb6|Rajarshee Chatterjee|2026-07-24|perf: Optimize Novel Covers, Lists, and Chapter Groups +dfd620aa292fadd3b8798013701ae1bef954fe57|Rajarshee Chatterjee|2026-07-23|feat: Standardize Dialogs with Material 3 Patterns +a5a6e8326f72d00271fd01cf8d6374cc35310147|Rajarshee Chatterjee|2026-07-23|chore: Update `@legendapp/list` to 3.3.3 +7f48a8ba8299180ffd5b07ba6314960213cd13cd|Rajarshee Chatterjee|2026-07-23|refactor: Improve Plugins Screen UX and Code +ae34d7b4ec6be28f7ad5ae25c663c7d2b3db43ef|Rajarshee Chatterjee|2026-07-23|feat: Add Dynamic Preview APK Metadata +44c8e54ed80bb47cc49c2461adca66ced94b2d1c|Rajarshee Chatterjee|2026-07-23|feat: Add Dynamic Material Theme Colors (Closes #1863) +4a4208e33602d65ef9942d05e54a692addd87e2f|Ecasept|2026-07-23|feat: Add Time-Tracking Toggle and Inactivity Timeout (#1890) +d2385b8dc2a9cd911e7567e55d6b7dc2c43f2372|CD-Z|2026-07-23|Update build.yml +9babe1676dd053069d127743a61bc7a8bc11256c|Rajarshee Chatterjee|2026-07-22|refactor: Remove React Native Background Actions Manifest Customization +8c38f499e4b62de02157aa8dcd1c9207978033bf|Rajarshee Chatterjee|2026-07-22|chore: Update AppState Test Setup And `pnpm` Configuration +89c43c1eeffd5a9980b3aaebe647f3d94252f8b8|Ecasept|2026-07-22|feat: Implement Time Tracking and Statistics (#1886) +c3c891cea084796b3640233826dacfc7aab6f8d6|Patrick Loser|2026-07-22|refactor: Migrate to Expo Managed Workflow (#1885) +e4cf64a112dbfd9f9dd853abec99afbdc09f1460|Rajarshee Chatterjee|2026-07-22|refactor: Migrate Background Tasks to Native Android Scheduling (#1889) +3e1e211098779264ba123121cc66bb48385fcf77|Luigi|2026-07-19|feat: Add Address Bar in WebView (#1870) +02b6b9848fb3349373de09a4343b6d273605b842|Ecasept|2026-07-19|fix: Prevent WebView Reset When Reader Settings Change (#1884) +71da09ed444707586eee6fc63f7f2ee182bb2cda|Shisii|2026-07-19|feat(reader): Add Current Chapter Search (#1877) +4404494ec9a57c3d2a8b1afb4734c9fc569773a2|Rajarshee Chatterjee|2026-07-19|fix(database): Repair Migration History For Scanlator Column +98a2a1d725d51d333ea3969b74434b2917a92161|Luigi|2026-07-15|feat: Implement Scanlator Filtering (#1864) +788f6cede7a5676a3d30cf0fd8e565cddd3c486c|Palloxin|2026-07-13|fix: Prevent Table Overflow and Clean Up CSS (#1876) (#1876) +7abee71ac33ab4b16c0c2a629640b1a7c2f5eb3b|Rajarshee Chatterjee|2026-07-13|test: Generate Debug Environment Variables In Testing Workflow +52d5c99c46b14148bd262c878701abf91c8b593b|Rajarshee Chatterjee|2026-07-02|fix: Order EPUB Chapters Across Paginated Pages (Closes #1762) +93bc5e5e4bac14fea10913cb215a7d9c297d6c31|Rajarshee Chatterjee|2026-07-02|fix: Handle Missing MAL List Entries and Null Progress (Closes #1875) +5d996f13883219090ad39ddbe7b52e88e36107c7|Jeon JaeHyeong|2026-07-01|feat(downloads): Make Chapter Download Cooldown Configurable (#1834) +bbb2645a6f6c47f7cda8d83ef096c329e03ffa6a|Palloxin|2026-07-01|chore: Clean Reader Preview Content (#1867) +54fedc2f531ecdda29dea74f3cc279a7a4525261|Vikas Movva|2026-07-01|fix(tts): Remove Leading and Trailing Quotes from Normalized Text (#1869) +e8c5550256dfe06c5ae12c106801e8688173a830|Palloxin|2026-07-01|perf: Optimize Regex for Removing Extra Paragraph Spacing (#1872) +57b9d41b5c3eacc9cbf5e38082c4efb2a1a11f5e|Ali Hamza Malik|2026-07-01|fix(service): Correct Notification Throttle Logic in ServiceManager.setMeta (#1873) +34be7a123499cc7733161482fdf5d42e4c1bae51|Jeon JaeHyeong|2026-05-24|fix(native-file): prevent fatal crash when unlinking missing files (#1832) +60f42ed6243dc5d4daa68c727d60c9fb5dd6576a|Jeon JaeHyeong|2026-05-24|chore(build): patch @rock-js/plugin-metro for Windows compatibility (#1830) +25c93afabe24d34f2130ee0fd7e0846a9b4861ff|Anthony Truong|2026-05-24|fix(reader): restore saved chapter progress from db if available (#1850) +2cd5a97ee8e482e92b45e9b702c9ba974e06a092|Gaurav Tupe|2026-05-10|fix: await database operations inside transactions (#1845) +8f47e8fd1e1aedaf22df8275484f27b5d40b2a39|Patrick Loser|2026-05-07|Theme switcher refactor (#1839) +eafa545b2a34320b5e1eb2d99ba61b6888838da8|Patrick Loser|2026-05-06|Use novel refactor (#1838) +b680a07750f0b7650836c0573566e641d4aaa679|Jeon JaeHyeong|2026-05-03|fix(browse): use gesture-handler Pressable in PluginListItem (#1828) +2a919ec0f963a4d4613e3e0ebe99a16fa7e2dbe3|Jeon JaeHyeong|2026-05-03|chore: gitignore tsconfig.tsbuildinfo build cache (#1829) +01596c045a4637acf056f75cf87ffc0198d79899|Definisi|2026-04-14|fix: Apply active filters to chapter count display (Closes #1793) (#1797) +3de93c5c093ab2fb7e0d509e367e4fec813082e2|Patrick Loser|2026-04-13|feat: migrate from rn community cli to rock (#1812) +306f5b27e2716f1a963fd992c06f5c961f44c926|Patrick Loser|2026-04-06|fix: install the right version of hermes compiler +06f0f66e3e4659db14127b1b56202dd3a8fca521|Patrick Loser|2026-04-05|fix: Add hermes-compiler dev dep +64921a054e9d4a11a0aad294b9703a3ab4c16b9f|Patrick Loser|2026-04-05|feat: Expo 55 upgrade +44e035ad38b25f79741199b6aea64479831ccc14|Rajarshee Chatterjee|2026-03-12|chore: Update Translations (#1683) +1803cd071d24a4d8ada8c55814a6ce62eaf98481|Patrick Loser|2026-02-25|feat: Add testing support for react code (#1763) +e4d7ba306b4441b4743ab9edb7c96751d62e3cde|Patrick Loser|2026-02-24|fix: Safety level may not be changed inside a transaction error (#1758) +cc0428752b2e9869677a6bd2dccc449f7a98a552|Rajarshee Chatterjee|2026-02-20|feat: Replace TTS Notifications with Android MediaSession Controls (Closes #1711) +b8c177bc55016b5a8fb96cc120e8ac050a2f7c73|Patrick Loser|2026-02-19|refactor: Migrate to Drizzle and `op-sqlite` (#1735) +7083c51dea1be0af3bb0d53c341f19b32a35dca4|Rajarshee Chatterjee|2026-02-19|fix: Include selectedIds in Chapter List extraData for Selection Highlight (Closes #1750) +f0693204e1f8f961dff85b195f3c7f4f6f9bb0ce|tomsLt0|2026-02-18|fix: Improve Next Chapter Selection Logic (#1744) +3f8071e48ee603f906cc9e3910abe90d6e3874a5|Rajarshee Chatterjee|2026-02-18|chore: Release v2.0.3 +a1c5350e8e330e783b6537c366351c2082e99150|Rajarshee Chatterjee|2026-02-12|fix: Miscellaneous UI Tweaks +8f237909d54310220d518000ace81b5fddbc3b5f|Rajarshee Chatterjee|2026-02-12|feat: Show First Unread Chapter in ReadButton and FAB When No Last Read +11ff69d4f5b4a79286247ac6c40ca1eac72ad283|Rajarshee Chatterjee|2026-02-12|fix: Pre-Fetch Adjacent Page Chapters in Reader and Library Updates +d15cf987425709f97ec168a46af8d936e944c29e|Rajarshee Chatterjee|2026-02-12|fix: Order Chapter Pages Numerically in Queries for Accurate Retrieval (Closes #1742) +29bd6612e9a9950c61b9a569b761ffa1aaa5d4eb|Rajarshee Chatterjee|2026-02-12|refactor: Optimize Library Screen Performance and Fix Category Bug +ab1e36d280768be46acdb57557e09b3e2d37f12b|Rajarshee Chatterjee|2026-02-08|refactor: Update Components to Follow Material Design 3 Guidelines +f1fdafd37f1f7d0816484c2e414e39d9885499d8|Rajarshee Chatterjee|2026-02-07|fix: Prevent Tab Bar Height Jump and Shadow Flash on Browse Screen +4c7684a3a71e57fb27f910b3edfbdd902353dd47|Rajarshee Chatterjee|2026-02-07|refactor: Optimize Novel Screen Performance and Fix Download State Tracking +2f45db659b2fdfa77f9213023d9d2707cbc452e1|Rajarshee Chatterjee|2026-02-07|fix: Replace Sync Database Operations With Async to Prevent Database Lock (Closes #1716) +9e38ad114d182d39880a962cb7acefa55235310d|Rajarshee Chatterjee|2026-02-06|fix: Sanitize EPUB FileName to Remove Filesystem-Unsafe Characters (Closes #1480) +45c4ea8ca02031c803224b0d906a6045364d2817|Rajarshee Chatterjee|2026-02-06|fix: Handle IOException in NativeFile to Prevent EPUB Import Crash Loop (Closes #1315) +d2b95ae3166f337d8d3597e1cc9a0bff65832d62|Rajarshee Chatterjee|2026-02-06|fix: Use NativeFile for Custom CSS/JS Import to Fix Content URI Read Failure (Closes #1661) +02121152b3e3cdbbaaff8a34b51cb8e9b2010538|Rajarshee Chatterjee|2026-02-06|fix: Stabilize Scroll Handler References to Prevent Infinite Re-render Loop (Closes #1679) +caed965b6bad1b788eee3035daba42322454fe26|Rajarshee Chatterjee|2026-02-06|fix: Prevent EPUB Import Crash from DocumentPicker Cache Cleanup (Closes #1725) +4088d5966bbc5ec27801b1ddc553f5621b4b6d79|Rajarshee Chatterjee|2026-02-06|fix: Close OkHttp Response and Streams in NativeFile.downloadFile (Closes #1740) +a062beee87d4173383e3033eeacb9376642645e5|Rajarshee Chatterjee|2026-02-06|fix: Add Max Height to Set Categories Modal (Closes #1738) +30958825162fbba2f7b805de6d41ab8e9183f28b|Patrick Loser|2026-02-01|Replace react-native-paper menus with custom implementation (#1733) +4ad06397960f1fbc633604ef06c2dfca193a220e|Patrick Loser|2026-01-31|Fix: Chapter loading for jump to chapter modal and chapter drawer +b53ec3352f8ce9084e32ddaa352526b39ec6003c|Rajarshee Chatterjee|2026-01-28|feat: Support Additional Plugin Setting Types (Dropdown and Checkboxes) +c40edd5b2cc68c2b746ba588aa39e142ab99f01e|Rajarshee Chatterjee|2026-01-25|feat: Add Kitsu Tracker (Closes #416) +bdd42caaaf3c3d5961d444aabc9150057d2de5d9|Rajarshee Chatterjee|2026-01-20|chore: Update Repository Links to Use lneader Instead of LNReader +ab4eee31ceb82c3f89e459279bef8b56de5ec10c|Rajarshee Chatterjee|2026-01-20|feat: Allow All Safe Tags and Attributes When Sanitizing Chapter HTML (Closes #1715) +3f9886a2e5300bb9913888872847727ab63597fc|Rajarshee Chatterjee|2026-01-17|chore: Update Repository Links to Use lneader Instead of LNReader +198b7443ae9c0ee7cc3ff608c0fc89a88880fa84|kingstar1111|2026-01-20|feat: Add RTL (Right-to-Left) Language Support (#1717) +f9c5ca303d71830811a203f98a17c3dd4846f887|Rajarshee Chatterjee|2026-01-16|chore: Update Indonesian Translations (Closes #1653) +9922d4394058a6da29ab2789cd8d7b75a37bf1bc|Rajarshee Chatterjee|2026-01-15|fix: Remove Missing Image References During EPUB Export (Closes #1696) +ae99b077d4e883e678b31ba0235a7abae88e1e94|Rajarshee Chatterjee|2026-01-15|feat: Add `@noble/ciphers` for Plugins (Closes #1709) +0cb9da90276c6a30d5d917557195f0f0b0ebc70c|Rajarshee Chatterjee|2026-01-15|refactor: Replace `withTransactionAsync` With `withExclusiveTransactionAsync` for Improved Concurrency Handling (Closes #1636) From f352ec1df9c655464e186a272adb8c564fa51a61 Mon Sep 17 00:00:00 2001 From: bizzkoot Date: Tue, 4 Aug 2026 18:42:18 +0800 Subject: [PATCH 02/54] fix(native-file): harden file operations against crashes - unlink: return instead of throwing when file already missing (34be7a1234) - wrap writeFile/readFile/copyFileContent in try/catch so IOException serializes across the TurboModule bridge instead of crashing the app process (45c4ea8ca0) - guard EPUB import copyFile with a user-friendly error (45c4ea8ca0) - close OkHttp response body and streams in downloadFile to prevent OOM during batch downloads (4088d5966b) --- .../NativeFile/NativeFile.kt | 75 ++++++++++++------- src/services/epub/import.ts | 8 +- 2 files changed, 55 insertions(+), 28 deletions(-) diff --git a/android/app/src/main/java/com/rajarsheechatterjee/NativeFile/NativeFile.kt b/android/app/src/main/java/com/rajarsheechatterjee/NativeFile/NativeFile.kt index e2948ea639..43110e8caa 100644 --- a/android/app/src/main/java/com/rajarsheechatterjee/NativeFile/NativeFile.kt +++ b/android/app/src/main/java/com/rajarsheechatterjee/NativeFile/NativeFile.kt @@ -74,13 +74,21 @@ class NativeFile(context: ReactApplicationContext) : } override fun writeFile(path: String, content: String) { - val fw = FileWriter(path) - fw.write(content) - fw.close() + try { + val fw = FileWriter(path) + fw.write(content) + fw.close() + } catch (e: IOException) { + throw Exception("Failed to write file '$path': ${e.message}") + } } override fun readFile(path: String): String { - return File(path).bufferedReader().readText() + val file = File(path) + if (!file.exists()) { + throw Exception("File not found: '$path'") + } + return file.bufferedReader().readText() } override fun copyFile(filepath: String, destPath: String) { @@ -97,17 +105,27 @@ class NativeFile(context: ReactApplicationContext) : destPath: String, onDone: (() -> Unit)? = null, ) { - val inputStream = getInputStream(filepath) - val outputStream = getOutputStream(destPath) - val buffer = ByteArray(BUFFER_SIZE) - var length: Int - while (inputStream.read(buffer).also { length = it } > 0) { - outputStream.write(buffer, 0, length) - } - inputStream.close() - outputStream.close() - if (onDone != null) { - onDone() + try { + val inputStream = getInputStream(filepath) + try { + val outputStream = getOutputStream(destPath) + try { + val buffer = ByteArray(BUFFER_SIZE) + var length: Int + while (inputStream.read(buffer).also { length = it } > 0) { + outputStream.write(buffer, 0, length) + } + } finally { + outputStream.close() + } + } finally { + inputStream.close() + } + if (onDone != null) { + onDone() + } + } catch (e: IOException) { + throw Exception("Failed to copy file from '$filepath' to '$destPath': ${e.message}") } } @@ -132,7 +150,7 @@ class NativeFile(context: ReactApplicationContext) : override fun unlink(filepath: String) { val file = File(filepath) - if (!file.exists()) throw Exception("File does not exist") + if (!file.exists()) return deleteRecursive(file) } @@ -191,18 +209,21 @@ class NativeFile(context: ReactApplicationContext) : } override fun onResponse(call: Call, response: Response) { - if (!response.isSuccessful || response.body == null) { - promise.reject(Exception("Failed to download load: ${response.code}")) - return - } - try { - val inputStream = decompressStream(response.body!!.byteStream()) - FileOutputStream(destPath).use { fos -> - inputStream.copyTo(fos, BUFFER_SIZE) + response.use { + if (!it.isSuccessful || it.body == null) { + promise.reject(Exception("Failed to download: ${it.code}")) + return + } + try { + decompressStream(it.body!!.byteStream()).use { inputStream -> + FileOutputStream(destPath).use { fos -> + inputStream.copyTo(fos, BUFFER_SIZE) + } + } + promise.resolve(null) + } catch (e: Exception) { + promise.reject(e) } - promise.resolve(null) - } catch (e: Exception) { - promise.reject(e) } } }) diff --git a/src/services/epub/import.ts b/src/services/epub/import.ts index 0ddfee06a4..6dc215155e 100644 --- a/src/services/epub/import.ts +++ b/src/services/epub/import.ts @@ -126,7 +126,13 @@ export const importEpub = async ( const epubFilePath = NativeFile.getConstants().ExternalCachesDirectoryPath + '/novel.epub'; - NativeFile.copyFile(uri, epubFilePath); + try { + NativeFile.copyFile(uri, epubFilePath); + } catch (error) { + throw new Error( + `Failed to read EPUB file "${filename}". The file may have been moved or deleted. Please try importing again.`, + ); + } const epubDirPath = NativeFile.getConstants().ExternalCachesDirectoryPath + '/epub'; if (NativeFile.exists(epubDirPath)) { From ce69ec0e2c301226378dd024b174ddb29c1090db Mon Sep 17 00:00:00 2001 From: bizzkoot Date: Tue, 4 Aug 2026 18:42:36 +0800 Subject: [PATCH 03/54] fix(service): correct notification throttle and sanitize epub filenames - ServiceManager.setMeta: invert throttle condition (< 1000) and compute delay as 1000 - (now - lastNotifUpdate) so the notification update is actually debounced by 1s instead of firing immediately (57b9d41b5c) - ExportNovelAsEpubButton: strip filesystem-unsafe chars from epub filename with a safe fallback (9e38ad114d) --- src/screens/novel/components/ExportNovelAsEpubButton.tsx | 2 +- src/services/ServiceManager.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/screens/novel/components/ExportNovelAsEpubButton.tsx b/src/screens/novel/components/ExportNovelAsEpubButton.tsx index b3078e3e8a..800ee2726f 100644 --- a/src/screens/novel/components/ExportNovelAsEpubButton.tsx +++ b/src/screens/novel/components/ExportNovelAsEpubButton.tsx @@ -137,7 +137,7 @@ const ExportNovelAsEpubButton: React.FC = ({ epub = new EpubBuilder( { title: novel.name, - fileName: novel.name.replace(/\s/g, ''), + fileName: novel.name.replace(/[\\/:*?"<>|\s]/g, '') || 'novel', language: 'en', cover: novel.cover, description: novel.summary, diff --git a/src/services/ServiceManager.ts b/src/services/ServiceManager.ts index 30d245b2a6..46b8a672cf 100644 --- a/src/services/ServiceManager.ts +++ b/src/services/ServiceManager.ts @@ -149,8 +149,8 @@ export default class ServiceManager { taskList[0].task?.name !== 'DOWNLOAD_CHAPTER' ) { const now = Date.now(); - if (now - this.lastNotifUpdate > 1000) { - const delay = 1000 - now - this.lastNotifUpdate; + if (now - this.lastNotifUpdate < 1000) { + const delay = 1000 - (now - this.lastNotifUpdate); const id = ++this.currentPendingUpdate; setTimeout(() => { if (this.currentPendingUpdate !== id) { From a7eb69d09bf8725ea879b1b1b4f86a0b3a0a4e7e Mon Sep 17 00:00:00 2001 From: bizzkoot Date: Tue, 4 Aug 2026 18:42:53 +0800 Subject: [PATCH 04/54] fix(reader): prevent table overflow, restore chip contrast, fade bottom nav - index.css: scroll tables not in the reader chapter element to avoid overflow; cleanup next-button styles (788f6cede7) - SelectableChip: drop selectedColor override restoring Midnight Dusk contrast (76106317b6) - BottomNavigator: shift -> fade animation (65e8610030) --- android/app/src/main/assets/css/index.css | 7 +++++-- src/components/Chip/SelectableChip.tsx | 1 - src/navigators/BottomNavigator.tsx | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/android/app/src/main/assets/css/index.css b/android/app/src/main/assets/css/index.css index c327c0975b..2c8ed41efd 100644 --- a/android/app/src/main/assets/css/index.css +++ b/android/app/src/main/assets/css/index.css @@ -53,6 +53,10 @@ img { max-width: 100%; } +div:has(> table):not(#LNReader-chapter) { + overflow: auto; +} + table { background-color: var(--theme-onPrimary); border-collapse: collapse; @@ -83,8 +87,6 @@ td { margin-left: var(--readerSettings-padding); margin-right: var(--readerSettings-padding); border-radius: 50px; - border-width: 1; - color: var(--theme-onPrimary); background-color: var(--theme-primary); font-family: var(--readerSettings-fontFamily); font-size: 16px; @@ -93,6 +95,7 @@ td { } .next-button { + color: var(--theme-onPrimary); min-height: 40px; text-overflow: ellipsis; overflow: hidden; diff --git a/src/components/Chip/SelectableChip.tsx b/src/components/Chip/SelectableChip.tsx index 518dff71c9..15f7253aaf 100644 --- a/src/components/Chip/SelectableChip.tsx +++ b/src/components/Chip/SelectableChip.tsx @@ -33,7 +33,6 @@ const SelectableChip: React.FC = ({ showSelectedCheck={showCheckIcon} style={styles.chip} textStyle={{ fontFamily: customFontFamily }} - selectedColor={theme.primary} theme={{ colors: theme }} mode={selected ? 'flat' : mode} > diff --git a/src/navigators/BottomNavigator.tsx b/src/navigators/BottomNavigator.tsx index 080d4a3ff6..7cf8cf73cd 100644 --- a/src/navigators/BottomNavigator.tsx +++ b/src/navigators/BottomNavigator.tsx @@ -65,7 +65,7 @@ const BottomNavigator = () => { ({ headerShown: false, - animation: 'shift', + animation: 'fade', lazy: true, tabBarBadgeStyle: { backgroundColor: theme.error, From 261b379a114fb62f9dfaa7af2162af0a60ee8dc1 Mon Sep 17 00:00:00 2001 From: bizzkoot Date: Tue, 4 Aug 2026 18:43:25 +0800 Subject: [PATCH 05/54] fix(tts): strip leading and trailing quotes from normalized text normalizeText now removes surrounding quote characters (straight and curly) so TTS no longer reads them aloud. Length-preserving - only strips quote chars at string edges, paragraph index contract intact (54fedc2f53) --- android/app/src/main/assets/js/core.js | 1 + 1 file changed, 1 insertion(+) diff --git a/android/app/src/main/assets/js/core.js b/android/app/src/main/assets/js/core.js index edcc578d04..ac7c8b3109 100644 --- a/android/app/src/main/assets/js/core.js +++ b/android/app/src/main/assets/js/core.js @@ -2203,6 +2203,7 @@ window.tts = new (function () { this.normalizeText = text => { if (!text) return ''; return text + .replace(/^["'β€œβ€β€˜β€™]+|["'β€œβ€β€˜β€™]+$/g, '') .replace(/\s+/g, ' ') .replace(/\s*([.,!?;:])\s*/g, '$1 ') .trim(); From 45bd7b0af15b941935f7a1e59804105195222be2 Mon Sep 17 00:00:00 2001 From: bizzkoot Date: Tue, 4 Aug 2026 18:43:27 +0800 Subject: [PATCH 06/54] fix(reader): prevent white drawer seam on chapter screen Transparent drawer background so the reader drawer doesn't flash a white seam over the chapter background (0ed7d878b4) --- src/screens/reader/ReaderScreen.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/screens/reader/ReaderScreen.tsx b/src/screens/reader/ReaderScreen.tsx index 9880dc37a6..beaf9a3c62 100644 --- a/src/screens/reader/ReaderScreen.tsx +++ b/src/screens/reader/ReaderScreen.tsx @@ -41,6 +41,7 @@ const Chapter = ({ route, navigation }: ChapterScreenProps) => { initialChapter={route.params.chapter} > setOpen(true)} onClose={() => setOpen(false)} @@ -168,4 +169,5 @@ export default Chapter; const styles = StyleSheet.create({ container: { flex: 1 }, + drawer: { backgroundColor: 'transparent' }, }); From 341cf16c3928c35eceb5bec8865bdf5c65308023 Mon Sep 17 00:00:00 2001 From: bizzkoot Date: Tue, 4 Aug 2026 18:46:02 +0800 Subject: [PATCH 07/54] fix(ui): align bottom nav with M3, stabilize browse tab bar, cap modal height - BottomTabBar: add 16px bottom padding per M3 spec and trim pressable vertical padding (intent of 31cb4b994a; fork uses scaled padding.xs) - BrowseScreen: initialLayout for TabView prevents tab bar height jump; replace shadow with border to stop shadow flash (f1fdafd37f) - SetCategoriesModal: cap category list at 40% window height so long lists don't overflow the modal (a062beee87) --- src/components/BottomTabBar/index.tsx | 4 ++-- src/screens/browse/BrowseScreen.tsx | 9 +++++++++ src/screens/novel/components/SetCategoriesModal.tsx | 6 +++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/components/BottomTabBar/index.tsx b/src/components/BottomTabBar/index.tsx index 918bbbb996..0f8bb95e72 100644 --- a/src/components/BottomTabBar/index.tsx +++ b/src/components/BottomTabBar/index.tsx @@ -62,7 +62,7 @@ function CustomBottomTabBar({ styles.container, { backgroundColor: theme.surface2 || theme.surface, - paddingBottom: insets?.bottom || 0, + paddingBottom: 16 + (insets?.bottom || 0), }, ]} > @@ -163,7 +163,7 @@ const getStyles = ( flex: 1, justifyContent: 'center', alignItems: 'center', - paddingVertical: padding.xs + 2, + paddingVertical: padding.xs, paddingHorizontal: padding.xs, position: 'relative', }, diff --git a/src/screens/browse/BrowseScreen.tsx b/src/screens/browse/BrowseScreen.tsx index b1d95ab829..b4067fa1dd 100644 --- a/src/screens/browse/BrowseScreen.tsx +++ b/src/screens/browse/BrowseScreen.tsx @@ -1,6 +1,8 @@ /* eslint-disable react-native/no-inline-styles -- Dynamic styles require inline approach for theme-dependent values */ import React, { useEffect, useMemo } from 'react'; +import { useWindowDimensions } from 'react-native'; import { TabView, TabBar } from 'react-native-tab-view'; +import Color from 'color'; import { useSearch } from '@hooks'; import { usePlugins, useTheme, useAppSettings } from '@hooks/persisted'; @@ -24,6 +26,7 @@ const BrowseScreen = ({ navigation }: BrowseScreenProps) => { const { searchText, setSearchText, clearSearchbar } = useSearch(); const { languagesFilter } = usePlugins(); const { uiScale = 1.0 } = useAppSettings(); + const layout = useWindowDimensions(); const searchbarActions = useMemo( () => @@ -70,6 +73,7 @@ const BrowseScreen = ({ navigation }: BrowseScreenProps) => { /> { if (languagesFilter.length === 0) { return ( @@ -112,6 +116,11 @@ const BrowseScreen = ({ navigation }: BrowseScreenProps) => { indicatorStyle={{ backgroundColor: theme.primary, height: 3 }} style={{ backgroundColor: theme.surface, + elevation: 0, + borderBottomWidth: 1, + borderBottomColor: Color(theme.isDark ? '#FFFFFF' : '#000000') + .alpha(0.12) + .string(), }} inactiveColor={theme.secondary} activeColor={theme.primary} diff --git a/src/screens/novel/components/SetCategoriesModal.tsx b/src/screens/novel/components/SetCategoriesModal.tsx index 860c14fbeb..dd7da5e2af 100644 --- a/src/screens/novel/components/SetCategoriesModal.tsx +++ b/src/screens/novel/components/SetCategoriesModal.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useState } from 'react'; -import { FlatList, StyleSheet, View } from 'react-native'; +import { Dimensions, FlatList, StyleSheet, View } from 'react-native'; import { Divider, Portal } from 'react-native-paper'; import { NavigationProp, useNavigation } from '@react-navigation/native'; @@ -49,6 +49,9 @@ const SetCategoryModal: React.FC = ({ checkboxView: { marginBottom: scaleDimension(5, uiScale), }, + categoryList: { + maxHeight: Dimensions.get('window').height * 0.4, + }, flex: { flex: 1, }, @@ -90,6 +93,7 @@ const SetCategoryModal: React.FC = ({ ( Date: Tue, 4 Aug 2026 18:46:04 +0800 Subject: [PATCH 08/54] fix(tracker): null-safe chapter progress in set-track dialog String(trackItem.progress ?? 0) instead of progress.toString() so a null/undefined progress value no longer crashes the dialog (93bc5e5e4b; myAnimeList.ts hunk skipped - fork already null-handles) --- .../novel/components/Tracker/SetTrackChaptersDialog.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/screens/novel/components/Tracker/SetTrackChaptersDialog.tsx b/src/screens/novel/components/Tracker/SetTrackChaptersDialog.tsx index 5a8b506403..41908be822 100644 --- a/src/screens/novel/components/Tracker/SetTrackChaptersDialog.tsx +++ b/src/screens/novel/components/Tracker/SetTrackChaptersDialog.tsx @@ -14,11 +14,11 @@ const SetTrackChaptersDialog: React.FC = ({ onUpdateChapters, }) => { const theme = useTheme(); - const [chapters, setChapters] = useState(trackItem.progress.toString()); + const [chapters, setChapters] = useState(String(trackItem.progress ?? 0)); useEffect(() => { if (visible) { - setChapters(trackItem.progress.toString()); + setChapters(String(trackItem.progress ?? 0)); } }, [visible, trackItem.progress]); From 7d0d0a1bf56175befa981cb75de5331aaf68506a Mon Sep 17 00:00:00 2001 From: bizzkoot Date: Tue, 4 Aug 2026 18:46:05 +0800 Subject: [PATCH 09/54] chore: gitignore tsconfig build cache Ignore *.tsbuildinfo (2a919ec0f9) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1d0ed36516..63ddb789bd 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ npm-debug.* *.key *.mobileprovision *.orig.* +*.tsbuildinfo web-build/ .env reader_playground/index.html From c2a10f08f1f351a9da71be0f1a6574c853959eda Mon Sep 17 00:00:00 2001 From: bizzkoot Date: Tue, 4 Aug 2026 18:46:05 +0800 Subject: [PATCH 10/54] fix(settings): normalize repository screen back navigation Remove useBackHandler + unconditional popTo so back always respects the navigation stack (9783c4d5e3) --- .../SettingsRepositoryScreen.tsx | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/screens/settings/SettingsRepositoryScreen/SettingsRepositoryScreen.tsx b/src/screens/settings/SettingsRepositoryScreen/SettingsRepositoryScreen.tsx index 47c9efbe85..4e82ea1a4e 100644 --- a/src/screens/settings/SettingsRepositoryScreen/SettingsRepositoryScreen.tsx +++ b/src/screens/settings/SettingsRepositoryScreen/SettingsRepositoryScreen.tsx @@ -11,17 +11,14 @@ import { updateRepository, } from '@database/queries/RepositoryQueries'; import { Repository } from '@database/types'; -import { useBackHandler, useBoolean } from '@hooks/index'; +import { useBoolean } from '@hooks/index'; import { usePlugins, useTheme } from '@hooks/persisted'; import { getString } from '@strings/translations'; import AddRepositoryModal from './components/AddRepositoryModal'; import RepositoryCard from './components/RepositoryCard'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { - RespositorySettingsScreenProps, - RootStackParamList, -} from '@navigators/types'; +import { RespositorySettingsScreenProps } from '@navigators/types'; import { showToast } from '@utils/showToast'; const SettingsBrowseScreen = ({ @@ -75,14 +72,6 @@ const SettingsBrowseScreen = ({ } }, [params, upsertRepository]); - useBackHandler(() => { - if (!navigation.canGoBack()) { - navigation.popTo('BottomNavigator'); - return true; - } - return false; - }); - return ( ('BottomNavigator'); }} theme={theme} /> From 95f79626d1257018053f48953925aa9a07383468 Mon Sep 17 00:00:00 2001 From: bizzkoot Date: Tue, 4 Aug 2026 19:21:07 +0800 Subject: [PATCH 11/54] fix(db): order chapter pages numerically --- src/database/queries/ChapterQueries.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/database/queries/ChapterQueries.ts b/src/database/queries/ChapterQueries.ts index 39d178d339..9549e22871 100644 --- a/src/database/queries/ChapterQueries.ts +++ b/src/database/queries/ChapterQueries.ts @@ -336,7 +336,7 @@ export const getRecentReadingChapters = (novelId: number, limit: number = 4) => export const getCustomPages = (novelId: number) => db.getAllSync<{ page: string }>( - 'SELECT DISTINCT page from Chapter WHERE novelId = ?', + 'SELECT DISTINCT page from Chapter WHERE novelId = ? ORDER BY CAST(page AS INTEGER) ASC', novelId, ); @@ -425,13 +425,13 @@ export const getPrevChapter = ( page: string, ) => db.getFirstAsync( - `SELECT * FROM Chapter - WHERE novelId = ? + `SELECT * FROM Chapter + WHERE novelId = ? AND ( - (position < ? AND page = ?) - OR page < ? + (position < ? AND page = ?) + OR CAST(page AS INTEGER) < CAST(? AS INTEGER) ) - ORDER BY position DESC, page DESC`, + ORDER BY CAST(page AS INTEGER) DESC, position DESC`, novelId, chapterPosition, page, @@ -444,13 +444,14 @@ export const getNextChapter = ( page: string, ) => db.getFirstAsync( - `SELECT * FROM Chapter - WHERE novelId = ? + `SELECT * FROM Chapter + WHERE novelId = ? AND ( - (page = ? AND position > ?) - OR (position = 0 AND page > ?) + (page = ? AND position > ?) + OR CAST(page AS INTEGER) > CAST(? AS INTEGER) ) - ORDER BY position ASC, page ASC`, + ORDER BY CAST(page AS INTEGER) ASC, position ASC + LIMIT 1`, novelId, page, chapterPosition, From f4f8defdf7235db59deb810e6a0ef72b58dbb949 Mon Sep 17 00:00:00 2001 From: bizzkoot Date: Tue, 4 Aug 2026 19:21:21 +0800 Subject: [PATCH 12/54] fix(db): order epub chapters across paginated pages --- src/database/queries/ChapterQueries.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/database/queries/ChapterQueries.ts b/src/database/queries/ChapterQueries.ts index 9549e22871..e8dd51c541 100644 --- a/src/database/queries/ChapterQueries.ts +++ b/src/database/queries/ChapterQueries.ts @@ -483,7 +483,7 @@ export const getNovelDownloadedChapters = ( ) => { if (startPosition !== undefined && endPosition !== undefined) { return db.getAllAsync( - 'SELECT * FROM Chapter WHERE novelId = ? AND isDownloaded = 1 AND position >= ? AND position <= ? ORDER BY position ASC', + 'SELECT * FROM Chapter WHERE novelId = ? AND isDownloaded = 1 AND position >= ? AND position <= ? ORDER BY CAST(page AS INTEGER) ASC, position ASC', novelId, startPosition - 1, endPosition - 1, @@ -491,7 +491,7 @@ export const getNovelDownloadedChapters = ( } return db.getAllAsync( - 'SELECT * FROM Chapter WHERE novelId = ? AND isDownloaded = 1 ORDER BY position ASC', + 'SELECT * FROM Chapter WHERE novelId = ? AND isDownloaded = 1 ORDER BY CAST(page AS INTEGER) ASC, position ASC', novelId, ); }; From 153038d70076876fa0debb0d4c36cd24ebf77f19 Mon Sep 17 00:00:00 2001 From: bizzkoot Date: Tue, 4 Aug 2026 19:21:33 +0800 Subject: [PATCH 13/54] fix(categories): stable key for drag reordering --- src/screens/Categories/CategoriesScreen.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/screens/Categories/CategoriesScreen.tsx b/src/screens/Categories/CategoriesScreen.tsx index 57fe33b1f6..e18d0922c1 100644 --- a/src/screens/Categories/CategoriesScreen.tsx +++ b/src/screens/Categories/CategoriesScreen.tsx @@ -91,7 +91,7 @@ const CategoriesScreen = () => { data={userCategories} contentContainerStyle={styles.contentCtn} renderItem={renderItem} - keyExtractor={(item, index) => `${item.id}-${index}`} + keyExtractor={item => item.id.toString()} onDragEnd={onDragEnd} activationDistance={10} autoscrollSpeed={100} From da8b1b33795b773f15259eead6001bc33406fdff Mon Sep 17 00:00:00 2001 From: bizzkoot Date: Tue, 4 Aug 2026 19:22:25 +0800 Subject: [PATCH 14/54] fix(db): preserve default category after reordering --- src/database/constants.ts | 4 ++++ src/database/queries/CategoryQueries.ts | 7 ++++--- src/database/queries/NovelQueries.ts | 13 +++++++------ .../queries/__tests__/CategoryQueries.test.ts | 8 +++----- src/screens/Categories/CategoriesScreen.tsx | 2 +- 5 files changed, 19 insertions(+), 15 deletions(-) create mode 100644 src/database/constants.ts diff --git a/src/database/constants.ts b/src/database/constants.ts new file mode 100644 index 0000000000..24443fa74d --- /dev/null +++ b/src/database/constants.ts @@ -0,0 +1,4 @@ +export const BUILT_IN_CATEGORY_IDS = { + default: 1, + local: 2, +} as const; diff --git a/src/database/queries/CategoryQueries.ts b/src/database/queries/CategoryQueries.ts index ccf6c18b93..0636eacbf8 100644 --- a/src/database/queries/CategoryQueries.ts +++ b/src/database/queries/CategoryQueries.ts @@ -4,6 +4,7 @@ import { showToast } from '@utils/showToast'; import { getString } from '@strings/translations'; import { db } from '@database/db'; import { getAllSync, runSync } from '@database/utils/helpers'; +import { BUILT_IN_CATEGORY_IDS } from '@database/constants'; const getCategoriesQuery = ` SELECT @@ -44,7 +45,7 @@ export const createCategory = (categoryName: string): void => runSync([[createCategoryQuery, [categoryName]]]); const beforeDeleteCategoryQuery = ` - UPDATE NovelCategory SET categoryId = (SELECT id FROM Category WHERE sort = 1) + UPDATE NovelCategory SET categoryId = ${BUILT_IN_CATEGORY_IDS.default} WHERE novelId IN ( SELECT novelId FROM NovelCategory GROUP BY novelId @@ -93,8 +94,8 @@ export const updateCategoryOrderInDb = (categories: Category[]): void => { return; } runSync( - categories.map(c => { - return [updateCategoryOrderQuery, [c.sort, c.id]]; + categories.map((c, index) => { + return [updateCategoryOrderQuery, [index + 1, c.id]]; }), ); }; diff --git a/src/database/queries/NovelQueries.ts b/src/database/queries/NovelQueries.ts index 0e3b5a5f06..aa53c34f70 100644 --- a/src/database/queries/NovelQueries.ts +++ b/src/database/queries/NovelQueries.ts @@ -21,6 +21,7 @@ import { getPlugin } from '@plugins/pluginManager'; import { db } from '@database/db'; import NativeFile from '@specs/NativeFile'; import { deleteNovelTtsSettings } from '@services/tts/novelTtsSettings'; +import { BUILT_IN_CATEGORY_IDS } from '@database/constants'; export const insertNovelAndChapters = async ( pluginId: string, @@ -111,14 +112,14 @@ export const switchNovelToLibraryQuery = async ( () => showToast(getString('browseScreen.removeFromLibrary')), ] : [ - 'INSERT INTO NovelCategory (novelId, categoryId) VALUES (?, (SELECT DISTINCT id FROM Category WHERE sort = 1))', + `INSERT INTO NovelCategory (novelId, categoryId) VALUES (?, ${BUILT_IN_CATEGORY_IDS.default})`, [novel.id], () => showToast(getString('browseScreen.addedToLibrary')), ], ]; if (novel.pluginId === 'local') { queries.push([ - 'INSERT INTO NovelCategory (novelId, categoryId) VALUES (?, 2)', + `INSERT INTO NovelCategory (novelId, categoryId) VALUES (?, ${BUILT_IN_CATEGORY_IDS.local})`, [novel.id], ]); } @@ -135,7 +136,7 @@ export const switchNovelToLibraryQuery = async ( () => showToast(getString('browseScreen.addedToLibrary')), ], [ - 'INSERT INTO NovelCategory (novelId, categoryId) VALUES (?, (SELECT DISTINCT id FROM Category WHERE sort = 1))', + `INSERT INTO NovelCategory (novelId, categoryId) VALUES (?, ${BUILT_IN_CATEGORY_IDS.default})`, [novelId], ], ]); @@ -198,7 +199,7 @@ export const restoreLibrary = async (novel: NovelInfo) => { await new Promise((resolve, reject) => { runAsync([ [ - 'INSERT OR REPLACE INTO NovelCategory (novelId, categoryId) VALUES (?, (SELECT DISTINCT id FROM Category WHERE sort = 1))', + `INSERT OR REPLACE INTO NovelCategory (novelId, categoryId) VALUES (?, ${BUILT_IN_CATEGORY_IDS.default})`, [novelId!], () => { db.runAsync('UPDATE Novel SET inLibrary = 1 WHERE id = ?', [ @@ -280,7 +281,7 @@ export const updateNovelCategories = async ( queries.push([ `DELETE FROM NovelCategory WHERE novelId IN (${novelIds.join( ',', - )}) AND categoryId != 2`, + )}) AND categoryId != ${BUILT_IN_CATEGORY_IDS.local}`, ]); // if no category is selected => set to the default category if (categoryIds.length) { @@ -298,7 +299,7 @@ export const updateNovelCategories = async ( `INSERT OR IGNORE INTO NovelCategory (novelId, categoryId) VALUES ( ${novelId}, - IFNULL((SELECT categoryId FROM NovelCategory WHERE novelId = ${novelId}), (SELECT id FROM Category WHERE sort = 1)) + IFNULL((SELECT categoryId FROM NovelCategory WHERE novelId = ${novelId}), ${BUILT_IN_CATEGORY_IDS.default}) )`, ]); }); diff --git a/src/database/queries/__tests__/CategoryQueries.test.ts b/src/database/queries/__tests__/CategoryQueries.test.ts index 1fcba5cd15..7dc503a7e4 100644 --- a/src/database/queries/__tests__/CategoryQueries.test.ts +++ b/src/database/queries/__tests__/CategoryQueries.test.ts @@ -277,9 +277,7 @@ describe('CategoryQueries', () => { expect(runSync).toHaveBeenCalledWith([ [ - expect.stringContaining( - 'UPDATE NovelCategory SET categoryId = (SELECT id FROM Category WHERE sort = 1)', - ), + expect.stringContaining('UPDATE NovelCategory SET categoryId = 1'), [10], ], ['DELETE FROM Category WHERE id = ?', [10]], @@ -452,8 +450,8 @@ describe('CategoryQueries', () => { CategoryQueries.updateCategoryOrderInDb(categories); expect(runSync).toHaveBeenCalledWith([ - ['UPDATE Category SET sort = ? WHERE id = ?', [10, 5]], - ['UPDATE Category SET sort = ? WHERE id = ?', [20, 6]], + ['UPDATE Category SET sort = ? WHERE id = ?', [1, 5]], + ['UPDATE Category SET sort = ? WHERE id = ?', [2, 6]], ]); }); }); diff --git a/src/screens/Categories/CategoriesScreen.tsx b/src/screens/Categories/CategoriesScreen.tsx index e18d0922c1..de4c7cb3b1 100644 --- a/src/screens/Categories/CategoriesScreen.tsx +++ b/src/screens/Categories/CategoriesScreen.tsx @@ -56,7 +56,7 @@ const CategoriesScreen = () => { const updatedOrderCategories = [...systemCategories, ...data].map( (category, index) => ({ ...category, - sort: index, + sort: index + 1, }), ); From 969f088cbe522e06b924e412e73b8ba69f690fa5 Mon Sep 17 00:00:00 2001 From: bizzkoot Date: Tue, 4 Aug 2026 19:22:59 +0800 Subject: [PATCH 15/54] fix(db): use exclusive transactions and await runAsync --- src/database/queries/NovelQueries.ts | 11 +++++------ src/database/queries/__tests__/NovelQueries.test.ts | 4 ++-- src/database/utils/helpers.tsx | 2 +- src/services/migrate/migrateNovel.ts | 2 +- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/database/queries/NovelQueries.ts b/src/database/queries/NovelQueries.ts index aa53c34f70..462277256b 100644 --- a/src/database/queries/NovelQueries.ts +++ b/src/database/queries/NovelQueries.ts @@ -178,8 +178,8 @@ export const restoreLibrary = async (novel: NovelInfo) => { throw e; }); let novelId: number | undefined; - await db.withTransactionAsync(async () => { - db.runAsync(restoreFromBackupQuery, [ + await db.withExclusiveTransactionAsync(async () => { + const data = await db.runAsync(restoreFromBackupQuery, [ sourceNovel.path, novel.name, novel.pluginId, @@ -190,9 +190,8 @@ export const restoreLibrary = async (novel: NovelInfo) => { novel.status || '', novel.genres || '', sourceNovel.totalPages || 0, - ]).then(data => { - novelId = data.lastInsertRowId; - }); + ]); + novelId = data.lastInsertRowId; }); if (novelId && novelId > 0) { @@ -319,7 +318,7 @@ const restoreObjectQuery = (table: string, obj: Record) => { export const _restoreNovelAndChapters = async (backupNovel: BackupNovel) => { const { chapters, ...novel } = backupNovel; - await db.withTransactionAsync(async () => { + await db.withExclusiveTransactionAsync(async () => { await db.runAsync('DELETE FROM Novel WHERE id = ?', [novel.id]); await db.runAsync( restoreObjectQuery('Novel', novel), diff --git a/src/database/queries/__tests__/NovelQueries.test.ts b/src/database/queries/__tests__/NovelQueries.test.ts index 5e9275b1ff..d0f3c67c5d 100644 --- a/src/database/queries/__tests__/NovelQueries.test.ts +++ b/src/database/queries/__tests__/NovelQueries.test.ts @@ -89,7 +89,7 @@ jest.mock('@database/db', () => { getAllAsync: jest.fn(() => Promise.resolve([])), getFirstAsync: jest.fn(() => Promise.resolve(null)), execAsync: jest.fn(() => Promise.resolve()), - withTransactionAsync: jest.fn((fn: () => Promise) => fn()), + withExclusiveTransactionAsync: jest.fn((fn: () => Promise) => fn()), }; return { db: mockDb, @@ -925,7 +925,7 @@ describe('NovelQueries', () => { await NovelQueries._restoreNovelAndChapters(mockBackupNovel); - expect(db.withTransactionAsync).toHaveBeenCalled(); + expect(db.withExclusiveTransactionAsync).toHaveBeenCalled(); expect(db.runAsync).toHaveBeenCalledWith( 'DELETE FROM Novel WHERE id = ?', [1], diff --git a/src/database/utils/helpers.tsx b/src/database/utils/helpers.tsx index ad0263c32b..1b7d2eccd3 100644 --- a/src/database/utils/helpers.tsx +++ b/src/database/utils/helpers.tsx @@ -131,7 +131,7 @@ type params = SQLiteBindValue[]; type TransactionObject = [query, ...params]; export async function transactionAsync(transactionObject: TransactionObject[]) { - await db.withTransactionAsync(async () => { + await db.withExclusiveTransactionAsync(async () => { const promises = transactionObject.map(([query, ...params]) => { return db.runAsync(query, ...params); }); diff --git a/src/services/migrate/migrateNovel.ts b/src/services/migrate/migrateNovel.ts index e789071e8c..e56ade66ce 100644 --- a/src/services/migrate/migrateNovel.ts +++ b/src/services/migrate/migrateNovel.ts @@ -74,7 +74,7 @@ export const migrateNovel = async ( toChapters = await getNovelChapters(toNovel.id); } - await db.withTransactionAsync(async () => { + await db.withExclusiveTransactionAsync(async () => { await db.runAsync( migrateNovelMetaDataQuery, fromNovel.cover || toNovel!.cover || '', From edfdbaea67424da2cd2052c862aa2c350945a5ad Mon Sep 17 00:00:00 2001 From: bizzkoot Date: Tue, 4 Aug 2026 19:23:49 +0800 Subject: [PATCH 16/54] fix(db): scope download deletion and fix read-chapter folder --- src/database/queries/ChapterQueries.ts | 18 ++- .../ChapterQueries.downloads.test.ts | 114 ++++++++++++++++++ 2 files changed, 127 insertions(+), 5 deletions(-) create mode 100644 src/database/queries/__tests__/ChapterQueries.downloads.test.ts diff --git a/src/database/queries/ChapterQueries.ts b/src/database/queries/ChapterQueries.ts index e8dd51c541..6e4007573b 100644 --- a/src/database/queries/ChapterQueries.ts +++ b/src/database/queries/ChapterQueries.ts @@ -165,25 +165,33 @@ export const deleteChapters = async ( }; export const deleteDownloads = async (chapters: DownloadedChapter[]) => { + if (!chapters?.length) { + return; + } await Promise.all( chapters?.map(chapter => { deleteDownloadedFiles(chapter.pluginId, chapter.novelId, chapter.id); }), ); - await db.execAsync('UPDATE Chapter SET isDownloaded = 0'); + const chapterIdsString = chapters?.map(chapter => chapter.id).toString(); + await db.execAsync( + `UPDATE Chapter SET isDownloaded = 0 WHERE id IN (${chapterIdsString})`, + ); }; export const deleteReadChaptersFromDb = async () => { const chapters = await getReadDownloadedChapters(); await Promise.all( chapters?.map(chapter => { - deleteDownloadedFiles(chapter.pluginId, chapter.novelId, chapter.novelId); + deleteDownloadedFiles(chapter.pluginId, chapter.novelId, chapter.id); }), ); const chapterIdsString = chapters?.map(chapter => chapter.id).toString(); - db.execAsync( - `UPDATE Chapter SET isDownloaded = 0 WHERE id IN (${chapterIdsString})`, - ); + if (chapterIdsString) { + await db.execAsync( + `UPDATE Chapter SET isDownloaded = 0 WHERE id IN (${chapterIdsString})`, + ); + } showToast(getString('novelScreen.readChaptersDeleted')); }; diff --git a/src/database/queries/__tests__/ChapterQueries.downloads.test.ts b/src/database/queries/__tests__/ChapterQueries.downloads.test.ts new file mode 100644 index 0000000000..3ad9785e12 --- /dev/null +++ b/src/database/queries/__tests__/ChapterQueries.downloads.test.ts @@ -0,0 +1,114 @@ +import { db } from '@database/db'; +import * as ChapterQueries from '../ChapterQueries'; +import NativeFile from '@specs/NativeFile'; + +jest.mock('@database/db', () => ({ + db: { + runAsync: jest.fn(() => + Promise.resolve({ lastInsertRowId: 1, changes: 1 }), + ), + execAsync: jest.fn(() => Promise.resolve()), + withExclusiveTransactionAsync: jest.fn(callback => + callback({ + runAsync: jest.fn(() => + Promise.resolve({ lastInsertRowId: 1, changes: 1 }), + ), + }), + ), + getAllAsync: jest.fn(() => Promise.resolve([])), + getFirstAsync: jest.fn(() => Promise.resolve(null)), + getFirstSync: jest.fn(() => null), + getAllSync: jest.fn(() => []), + }, +})); + +jest.mock('@utils/showToast', () => ({ + showToast: jest.fn(), +})); + +jest.mock('@strings/translations', () => ({ + getString: jest.fn(key => key), +})); + +jest.mock('@utils/Storages', () => ({ + NOVEL_STORAGE: 'file://novels', +})); + +jest.mock('@specs/NativeFile', () => ({ + unlink: jest.fn(), +})); + +describe('ChapterQueries download deletion', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('deleteDownloads', () => { + it('should delete files and clear isDownloaded only for the given chapter ids', async () => { + const chapters = [ + { id: 1, novelId: 10, pluginId: 'plugin-a' }, + { id: 2, novelId: 10, pluginId: 'plugin-a' }, + ]; + + await ChapterQueries.deleteDownloads(chapters); + + // Each chapter's folder is removed: NOVEL_STORAGE/pluginId/novelId/chapterId + expect(NativeFile.unlink).toHaveBeenCalledWith( + 'file://novels/plugin-a/10/1', + ); + expect(NativeFile.unlink).toHaveBeenCalledWith( + 'file://novels/plugin-a/10/2', + ); + expect(NativeFile.unlink).toHaveBeenCalledTimes(2); + + // The flag reset must be scoped to the given ids, never global + expect(db.execAsync).toHaveBeenCalledWith( + 'UPDATE Chapter SET isDownloaded = 0 WHERE id IN (1,2)', + ); + }); + + it('should not touch the database when no chapters are passed', async () => { + await ChapterQueries.deleteDownloads([]); + + expect(NativeFile.unlink).not.toHaveBeenCalled(); + expect(db.execAsync).not.toHaveBeenCalled(); + }); + }); + + describe('deleteReadChaptersFromDb', () => { + it('should delete files using the chapter id as the folder segment', async () => { + const readDownloadedChapters = [ + { id: 5, novelId: 10, pluginId: 'plugin-a' }, + { id: 6, novelId: 11, pluginId: 'plugin-b' }, + ]; + (db.getAllAsync as jest.Mock).mockResolvedValueOnce( + readDownloadedChapters, + ); + + await ChapterQueries.deleteReadChaptersFromDb(); + + // Folder must use chapter.id as the last segment, NOT novelId twice + expect(NativeFile.unlink).toHaveBeenCalledWith( + 'file://novels/plugin-a/10/5', + ); + expect(NativeFile.unlink).toHaveBeenCalledWith( + 'file://novels/plugin-b/11/6', + ); + expect(NativeFile.unlink).toHaveBeenCalledTimes(2); + + // execAsync is awaited and scoped to the read chapters + expect(db.execAsync).toHaveBeenCalledWith( + 'UPDATE Chapter SET isDownloaded = 0 WHERE id IN (5,6)', + ); + }); + + it('should no-op safely when there are no read downloaded chapters', async () => { + (db.getAllAsync as jest.Mock).mockResolvedValueOnce([]); + + await ChapterQueries.deleteReadChaptersFromDb(); + + expect(NativeFile.unlink).not.toHaveBeenCalled(); + expect(db.execAsync).not.toHaveBeenCalled(); + }); + }); +}); From 91c10cf83500c9ac365fabf575fd01e4d45804c0 Mon Sep 17 00:00:00 2001 From: bizzkoot Date: Tue, 4 Aug 2026 19:24:35 +0800 Subject: [PATCH 17/54] fix(novel): prevent genre rendering crashes --- .../novel/components/EditInfoModal.tsx | 16 +++++------ .../components/Info/NovelInfoComponents.tsx | 5 ++-- .../novel/utils/__tests__/genres.test.ts | 28 +++++++++++++++++++ src/screens/novel/utils/genres.ts | 7 +++++ 4 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 src/screens/novel/utils/__tests__/genres.test.ts create mode 100644 src/screens/novel/utils/genres.ts diff --git a/src/screens/novel/components/EditInfoModal.tsx b/src/screens/novel/components/EditInfoModal.tsx index 16f1fb33cb..8752a22a95 100644 --- a/src/screens/novel/components/EditInfoModal.tsx +++ b/src/screens/novel/components/EditInfoModal.tsx @@ -20,6 +20,7 @@ import { translateNovelStatus } from '@utils/translateEnum'; import { useAppSettings } from '@hooks/persisted'; import { scaleDimension } from '@theme/scaling'; import AppText from '@components/AppText'; +import { parseGenres } from '../utils/genres'; interface EditInfoModalProps { theme: ThemeColors; @@ -107,10 +108,9 @@ const EditInfoModal = ({ const removeTag = (t: string) => { setNovelInfo({ ...novel, - genres: novelInfo.genres - ?.split(',') + genres: parseGenres(novelInfo.genres) .filter(item => item !== t) - ?.join(','), + .join(','), }); }; @@ -235,9 +235,9 @@ const EditInfoModal = ({ setNovelInfo(prevVal => ({ ...prevVal, - genres: novelInfo.genres - ? `${novelInfo.genres},` + newGenreTrimmed - : newGenreTrimmed, + genres: [...parseGenres(prevVal.genres), newGenreTrimmed].join( + ',', + ), })); setNewGenre(''); }} @@ -246,11 +246,11 @@ const EditInfoModal = ({ style={styles.inputWrapper} /> - {novelInfo.genres !== undefined && novelInfo.genres !== '' ? ( + {parseGenres(novelInfo.genres).length > 0 ? ( 'novelTag' + index} renderItem={({ item }) => ( { const { uiScale = 1.0 } = useAppSettings(); @@ -406,7 +407,7 @@ const NovelGenres = ({ [uiScale], ); - const data = genres.split(/,\s*/); + const data = parseGenres(genres); return ( { + it('splits comma separated genres and trims whitespace', () => { + expect(parseGenres('Action, Fantasy, Romance ')).toEqual([ + 'Action', + 'Fantasy', + 'Romance', + ]); + }); + + it('filters empty entries', () => { + expect(parseGenres('Action,,,Drama')).toEqual(['Action', 'Drama']); + }); + + it('returns an empty array for null/undefined', () => { + expect(parseGenres(null)).toEqual([]); + expect(parseGenres(undefined)).toEqual([]); + }); + + it('returns an empty array for non-string values', () => { + expect(parseGenres(42)).toEqual([]); + }); + + it('returns an empty array for empty string', () => { + expect(parseGenres('')).toEqual([]); + }); +}); diff --git a/src/screens/novel/utils/genres.ts b/src/screens/novel/utils/genres.ts new file mode 100644 index 0000000000..7f727a6b4f --- /dev/null +++ b/src/screens/novel/utils/genres.ts @@ -0,0 +1,7 @@ +export const parseGenres = (genres: unknown): string[] => + typeof genres === 'string' + ? genres + .split(',') + .map(genre => genre.trim()) + .filter(Boolean) + : []; From cf5b5923ed25293d6209e338d49b9c695ef7f728 Mon Sep 17 00:00:00 2001 From: bizzkoot Date: Tue, 4 Aug 2026 19:25:16 +0800 Subject: [PATCH 18/54] fix(db): date-correct library updates and sorting --- src/database/queries/LibraryQueries.ts | 6 +++++- src/database/tables/NovelTable.ts | 24 +++++++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/database/queries/LibraryQueries.ts b/src/database/queries/LibraryQueries.ts index aaa5e947cb..71cb705157 100644 --- a/src/database/queries/LibraryQueries.ts +++ b/src/database/queries/LibraryQueries.ts @@ -27,7 +27,11 @@ export const getLibraryNovelsFromDb = ( } if (sortOrder) { - query += ` ORDER BY ${sortOrder}`; + const dateAwareSortOrder = sortOrder.replace( + /^lastUpdatedAt (ASC|DESC)$/, + 'julianday(lastUpdatedAt) $1', + ); + query += ` ORDER BY ${dateAwareSortOrder}`; } return getAllSync([query, [searchText ?? '']]); diff --git a/src/database/tables/NovelTable.ts b/src/database/tables/NovelTable.ts index acba784aa6..62f9e286c5 100644 --- a/src/database/tables/NovelTable.ts +++ b/src/database/tables/NovelTable.ts @@ -40,7 +40,13 @@ BEGIN totalChapters = (SELECT COUNT(*) FROM Chapter WHERE Chapter.novelId = Novel.id), chaptersDownloaded = (SELECT COUNT(*) FROM Chapter WHERE Chapter.novelId = Novel.id AND Chapter.isDownloaded = 1), chaptersUnread = (SELECT COUNT(*) FROM Chapter WHERE Chapter.novelId = Novel.id AND Chapter.unread = 1), - lastUpdatedAt = (SELECT MAX(updatedTime) FROM Chapter WHERE Chapter.novelId = Novel.id) + lastUpdatedAt = ( + SELECT updatedTime + FROM Chapter + WHERE Chapter.novelId = Novel.id AND updatedTime IS NOT NULL + ORDER BY julianday(updatedTime) DESC + LIMIT 1 + ) WHERE id = NEW.novelId; END; @@ -53,7 +59,13 @@ BEGIN chaptersDownloaded = (SELECT COUNT(*) FROM Chapter WHERE Chapter.novelId = Novel.id AND Chapter.isDownloaded = 1), chaptersUnread = (SELECT COUNT(*) FROM Chapter WHERE Chapter.novelId = Novel.id AND Chapter.unread = 1), lastReadAt = (SELECT MAX(readTime) FROM Chapter WHERE Chapter.novelId = Novel.id), - lastUpdatedAt = (SELECT MAX(updatedTime) FROM Chapter WHERE Chapter.novelId = Novel.id) + lastUpdatedAt = ( + SELECT updatedTime + FROM Chapter + WHERE Chapter.novelId = Novel.id AND updatedTime IS NOT NULL + ORDER BY julianday(updatedTime) DESC + LIMIT 1 + ) WHERE id = NEW.novelId; END; `; @@ -66,7 +78,13 @@ BEGIN chaptersUnread = (SELECT COUNT(*) FROM Chapter WHERE Chapter.novelId = Novel.id AND Chapter.unread = 1), totalChapters = (SELECT COUNT(*) FROM Chapter WHERE Chapter.novelId = Novel.id), lastReadAt = (SELECT MAX(readTime) FROM Chapter WHERE Chapter.novelId = Novel.id), - lastUpdatedAt = (SELECT MAX(updatedTime) FROM Chapter WHERE Chapter.novelId = Novel.id) + lastUpdatedAt = ( + SELECT updatedTime + FROM Chapter + WHERE Chapter.novelId = Novel.id AND updatedTime IS NOT NULL + ORDER BY julianday(updatedTime) DESC + LIMIT 1 + ) WHERE id = OLD.novelId; END; `; From b0b32fe28f5e57acd541e8b3d42a026e54bc229f Mon Sep 17 00:00:00 2001 From: bizzkoot Date: Tue, 4 Aug 2026 19:25:35 +0800 Subject: [PATCH 19/54] fix(reader): restore saved chapter progress from db --- src/screens/reader/hooks/useChapter.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/screens/reader/hooks/useChapter.ts b/src/screens/reader/hooks/useChapter.ts index 79f766b6b1..0effd82638 100644 --- a/src/screens/reader/hooks/useChapter.ts +++ b/src/screens/reader/hooks/useChapter.ts @@ -128,7 +128,10 @@ export default function useChapter( const getChapter = useCallback( async (navChapter?: ChapterInfo) => { try { - const chap = navChapter ?? chapter; + const dbChapter = navChapter + ? undefined + : await getDbChapter(chapter.id); + const chap = dbChapter ?? navChapter ?? chapter; const cachedText = chapterTextCache.get(chap.id); const text = cachedText ?? loadChapterText(chap.id, chap.path); const [nextChap, prevChap, awaitedText] = await Promise.all([ From ac1cb9153d353f5246112e198c56587cd480263c Mon Sep 17 00:00:00 2001 From: bizzkoot Date: Tue, 4 Aug 2026 19:25:52 +0800 Subject: [PATCH 20/54] fix(tracker): handle missing client IDs --- src/services/Trackers/aniList.ts | 7 +++++-- src/services/Trackers/myAnimeList.ts | 6 +++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/services/Trackers/aniList.ts b/src/services/Trackers/aniList.ts index d0e5eb9909..618021ee57 100644 --- a/src/services/Trackers/aniList.ts +++ b/src/services/Trackers/aniList.ts @@ -8,8 +8,6 @@ const clientId = ANILIST_CLIENT_ID; const redirectUri = Linking.createURL('tracker/AL'); -const authUrl = `https://anilist.co/api/v2/oauth/authorize?client_id=${clientId}&response_type=token`; - const searchQuery = `query($search: String) { Page { media(search: $search, type: MANGA, format: NOVEL, sort: POPULARITY_DESC) { @@ -46,6 +44,11 @@ const updateListEntryMutation = `mutation($id: Int!, $status: MediaListStatus, $ export const aniListTracker = { authenticate: async () => { + if (!clientId) { + throw new Error('AniList client ID is not configured'); + } + + const authUrl = `https://anilist.co/api/v2/oauth/authorize?client_id=${clientId}&response_type=token`; const result = await WebBrowser.openAuthSessionAsync(authUrl, redirectUri); if (result.type === 'success') { const { url } = result; diff --git a/src/services/Trackers/myAnimeList.ts b/src/services/Trackers/myAnimeList.ts index 0b34b24f85..7b59691377 100644 --- a/src/services/Trackers/myAnimeList.ts +++ b/src/services/Trackers/myAnimeList.ts @@ -8,7 +8,6 @@ const baseOAuthUrl = 'https://myanimelist.net/v1/oauth2/authorize'; const tokenUrl = 'https://myanimelist.net/v1/oauth2/token'; const baseApiUrl = 'https://api.myanimelist.net/v2'; const challenge = pkceChallenger(); -const authUrl = `${baseOAuthUrl}?response_type=code&client_id=${clientId}&code_challenge_method=plain&code_challenge=${challenge}`; const redirectUri = Linking.createURL('tracker/MAL'); export const malToNormalized: Record = { @@ -29,6 +28,11 @@ const normalizedToMal: Record = { export const myAnimeListTracker: Tracker = { authenticate: async () => { + if (!clientId) { + throw new Error('MyAnimeList client ID is not configured'); + } + + const authUrl = `${baseOAuthUrl}?response_type=code&client_id=${clientId}&code_challenge_method=plain&code_challenge=${challenge}`; const result = await WebBrowser.openAuthSessionAsync(authUrl, redirectUri); if (result.type !== 'success') { From ac92f34e7d1e7d8c0c1272151d13e9430dabea25 Mon Sep 17 00:00:00 2001 From: bizzkoot Date: Tue, 4 Aug 2026 19:26:21 +0800 Subject: [PATCH 21/54] feat(novel): add more statuses and icons --- src/plugins/types/index.ts | 2 ++ .../novel/components/Info/NovelInfoHeader.tsx | 23 +++++++++++-------- src/utils/translateEnum.ts | 6 +++++ 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/plugins/types/index.ts b/src/plugins/types/index.ts index 15d1a8e08b..7e5ac45a20 100644 --- a/src/plugins/types/index.ts +++ b/src/plugins/types/index.ts @@ -23,6 +23,8 @@ export enum NovelStatus { PublishingFinished = 'Publishing Finished', Cancelled = 'Cancelled', OnHiatus = 'On Hiatus', + STUB = 'STUB', + Inactive = 'Inactive', } export interface SourceNovel extends NovelItem { diff --git a/src/screens/novel/components/Info/NovelInfoHeader.tsx b/src/screens/novel/components/Info/NovelInfoHeader.tsx index 165db55777..4c2a43635a 100644 --- a/src/screens/novel/components/Info/NovelInfoHeader.tsx +++ b/src/screens/novel/components/Info/NovelInfoHeader.tsx @@ -69,15 +69,20 @@ interface NovelInfoHeaderProps { trackerSheetRef: React.RefObject; } -const getStatusIcon = (status?: string) => { - if (status === NovelStatus.Ongoing) { - return 'clock-outline'; - } - if (status === NovelStatus.Completed) { - return 'check-all'; - } - return 'help'; -}; +const STATUS_ICON_MAP = { + [NovelStatus.Ongoing]: 'clock-outline', + [NovelStatus.Completed]: 'check-all', + [NovelStatus.OnHiatus]: 'pause-circle-outline', + [NovelStatus.Cancelled]: 'cancel', + [NovelStatus.Licensed]: 'copyright', + [NovelStatus.PublishingFinished]: 'book-check-outline', + [NovelStatus.Unknown]: 'help-circle-outline', + [NovelStatus.STUB]: 'book-off-outline', + [NovelStatus.Inactive]: 'sleep', +} as const; + +const getStatusIcon = (status?: string) => + STATUS_ICON_MAP[status as keyof typeof STATUS_ICON_MAP] ?? 'help'; const NovelInfoHeader = ({ chapters, diff --git a/src/utils/translateEnum.ts b/src/utils/translateEnum.ts index e73bab9584..86d371707a 100644 --- a/src/utils/translateEnum.ts +++ b/src/utils/translateEnum.ts @@ -17,6 +17,12 @@ export const translateNovelStatus = (status?: NovelStatus | string) => { return getString('novelScreen.status.licensed'); case NovelStatus.PublishingFinished: return getString('novelScreen.status.publishingFinished'); + // New statuses are displayed verbatim; i18n keys intentionally not added + // to keep translation files untouched (see batch constraints). + case NovelStatus.STUB: + return 'STUB'; + case NovelStatus.Inactive: + return 'Inactive'; default: return status ?? ''; } From ca926130878adfd87e295229b870329197928e58 Mon Sep 17 00:00:00 2001 From: bizzkoot Date: Tue, 4 Aug 2026 19:28:04 +0800 Subject: [PATCH 22/54] test(db): type-safe fixtures for download deletion tests --- .../queries/__tests__/ChapterQueries.downloads.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/database/queries/__tests__/ChapterQueries.downloads.test.ts b/src/database/queries/__tests__/ChapterQueries.downloads.test.ts index 3ad9785e12..5c0c430678 100644 --- a/src/database/queries/__tests__/ChapterQueries.downloads.test.ts +++ b/src/database/queries/__tests__/ChapterQueries.downloads.test.ts @@ -1,6 +1,7 @@ import { db } from '@database/db'; import * as ChapterQueries from '../ChapterQueries'; import NativeFile from '@specs/NativeFile'; +import type { DownloadedChapter } from '../../types'; jest.mock('@database/db', () => ({ db: { @@ -48,7 +49,7 @@ describe('ChapterQueries download deletion', () => { const chapters = [ { id: 1, novelId: 10, pluginId: 'plugin-a' }, { id: 2, novelId: 10, pluginId: 'plugin-a' }, - ]; + ] as DownloadedChapter[]; await ChapterQueries.deleteDownloads(chapters); @@ -80,7 +81,7 @@ describe('ChapterQueries download deletion', () => { const readDownloadedChapters = [ { id: 5, novelId: 10, pluginId: 'plugin-a' }, { id: 6, novelId: 11, pluginId: 'plugin-b' }, - ]; + ] as DownloadedChapter[]; (db.getAllAsync as jest.Mock).mockResolvedValueOnce( readDownloadedChapters, ); From d612280023a1d7f53708837dd9894043d203ff06 Mon Sep 17 00:00:00 2001 From: bizzkoot Date: Tue, 4 Aug 2026 20:37:42 +0800 Subject: [PATCH 23/54] fix(tracker): restore search requests on submit using event text --- .../components/Tracker/TrackSearchDialog.tsx | 56 ++++++++++++------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/src/screens/novel/components/Tracker/TrackSearchDialog.tsx b/src/screens/novel/components/Tracker/TrackSearchDialog.tsx index ad3c0fdb6a..2043bbe2c2 100644 --- a/src/screens/novel/components/Tracker/TrackSearchDialog.tsx +++ b/src/screens/novel/components/Tracker/TrackSearchDialog.tsx @@ -1,5 +1,11 @@ import React, { useCallback, useEffect, useState, useMemo } from 'react'; -import { ActivityIndicator, Image, StyleSheet, View } from 'react-native'; +import { + ActivityIndicator, + Image, + StyleSheet, + TextInputSubmitEditingEvent, + View, +} from 'react-native'; import { ScrollView } from 'react-native-gesture-handler'; import { TextInput, TouchableRipple } from 'react-native-paper'; import MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons'; @@ -80,23 +86,27 @@ const TrackSearchDialog: React.FC = ({ const [searchText, setSearchText] = useState(novelName); const [selectedNovel, setSelectedNovel] = useState(); - const getSearchResults = useCallback(async () => { - setLoading(true); - try { - const trackerObj = getTracker(tracker.name); - const results = await trackerObj.handleSearch(searchText, tracker.auth); - setSearchResults(results); - } catch (error) { - showToast( - `Failed to fetch search results from ${tracker.name}: ${getErrorMessage( - error, - )}`, - ); - setSearchResults([]); - } finally { - setLoading(false); - } - }, [searchText, tracker.auth, tracker.name]); + const getSearchResults = useCallback( + async (text?: string) => { + setLoading(true); + try { + const trackerObj = getTracker(tracker.name); + const query = text ?? searchText; + const results = await trackerObj.handleSearch(query, tracker.auth); + setSearchResults(results); + } catch (error) { + showToast( + `Failed to fetch search results from ${tracker.name}: ${getErrorMessage( + error, + )}`, + ); + setSearchResults([]); + } finally { + setLoading(false); + } + }, + [searchText, tracker.auth, tracker.name], + ); useEffect(() => { if (visible) { @@ -104,6 +114,13 @@ const TrackSearchDialog: React.FC = ({ } }, [getSearchResults, visible]); + const handleSubmitSearch = useCallback( + (event: TextInputSubmitEditingEvent) => { + void getSearchResults(event.nativeEvent.text); + }, + [getSearchResults], + ); + const handleSelectNovel = useCallback((item: SearchResult) => { setSelectedNovel(item); }, []); @@ -180,7 +197,8 @@ const TrackSearchDialog: React.FC = ({ Date: Tue, 4 Aug 2026 20:38:33 +0800 Subject: [PATCH 24/54] docs(merge): record Batch A completion (29/29 fixes ported) --- .agents/upstream-merge-memory.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.agents/upstream-merge-memory.md b/.agents/upstream-merge-memory.md index a984a5f24f..436054f060 100644 --- a/.agents/upstream-merge-memory.md +++ b/.agents/upstream-merge-memory.md @@ -66,7 +66,9 @@ applyTo: '**' **2026-08-04 - Independent Review + Corrections Applied**: 4 review subagents verified the analysis (all 6 live-bug claims CONFIRMED, 9/9 ALREADY-HAVE SOUND, trap commit correct, PRDs SOUND). Corrections applied to manifest/PRDs/analysis/README: (1) 0cb9da9027 reclassified SKIP->PORT-A2 (pre-Drizzle expo-sqlite tx fix); (2) 5 DIRECT labels -> MANUAL (31cb4b99, 93bc5e5e, 2a919ec0, f1fdafd3, a062beee - fork rewrote those files, hunks fail apply); (3) 9783c4d5e3 reclassified SKIP->PORT-A1/low; (4) c3c891cea0 relabeled SKIP-CI->SKIP-INFRA; PRD-C now requires BOTH surfaceContainerLow+High keys; PRD-A 45c4ea8ca0 now includes epub/import.ts hunk. Full report: specs/upstream-merge-analysis-2026-08/REVIEW-2026-08-04.md **Docs**: specs/upstream-merge-analysis-2026-08/ (README.md, analysis.md, commit-manifest.csv, batches/PRD-A..D, REVIEW-2026-08-04.md) -**Status**: PLANNED - implementation deferred; dev branch has unpushed TTS work (commit/push before starting Batch A) +**Status**: Batch A βœ… COMPLETED 2026-08-04 (29/29 commits ported) on `merge/original-sync-batch-b` (head `d61228002`); Branch B-D pending. Dev branch has unpushed TTS work (commit/push before pushing Batch A). + +**2026-08-04 - Batch A (Safety Fixes) βœ… COMPLETED**: 29/29 upstream fixes ported on `merge/original-sync-batch-b` (23 commits ahead of master; includes the 2026-08-04 review corrections). Validated: type-check βœ…, lint βœ… (5 pre-existing warnings in WebViewReader.tsx), format βœ…, **73 suites / 1265 tests passing (zero regressions)**. Last item ported: `8f53550d2c` restore tracker search requests (TrackSearchDialog.tsx, manual port, commit `d61228002`). ## 2025-12-30 - Upstream Merge Analysis (FAILED - DIVERGENCE DETECTED) From a1d23a0a2904117ae72baa62739ea7d05e9fe143 Mon Sep 17 00:00:00 2001 From: bizzkoot Date: Tue, 4 Aug 2026 20:44:56 +0800 Subject: [PATCH 25/54] feat(updates): run library updates in parallel across sources --- src/services/updates/__tests__/index.test.ts | 129 +++++++++++++++++++ src/services/updates/index.ts | 69 +++++++--- 2 files changed, 183 insertions(+), 15 deletions(-) create mode 100644 src/services/updates/__tests__/index.test.ts diff --git a/src/services/updates/__tests__/index.test.ts b/src/services/updates/__tests__/index.test.ts new file mode 100644 index 0000000000..12207bb5bb --- /dev/null +++ b/src/services/updates/__tests__/index.test.ts @@ -0,0 +1,129 @@ +import { + getLibraryWithCategory, + getLibraryNovelsFromDb, +} from '@database/queries/LibraryQueries'; +import type { BackgroundTaskMetadata } from '@services/ServiceManager'; +import type { LibraryNovelInfo } from '@database/types'; +import { updateNovel } from '../LibraryUpdateQueries'; +import { updateLibrary } from '../index'; + +jest.mock('@database/queries/LibraryQueries', () => ({ + getLibraryNovelsFromDb: jest.fn(), + getLibraryWithCategory: jest.fn(), +})); +jest.mock('../LibraryUpdateQueries', () => ({ + updateNovel: jest.fn(), +})); +jest.mock('@utils/sleep', () => ({ + sleep: jest.fn(() => Promise.resolve()), +})); +jest.mock('@utils/showToast', () => ({ + showToast: jest.fn(), +})); +jest.mock('@utils/mmkv/mmkv', () => ({ + MMKVStorage: { set: jest.fn() }, + getMMKVObject: jest.fn(() => ({})), +})); +jest.mock('@hooks/persisted/useUpdates', () => ({ + LAST_UPDATE_TIME: 'LAST_UPDATE_TIME', +})); +jest.mock('@hooks/persisted/useSettings', () => ({ + APP_SETTINGS: 'APP_SETTINGS', +})); + +const mockedGetLibraryNovels = jest.mocked(getLibraryNovelsFromDb); +const mockedGetLibraryWithCategory = jest.mocked(getLibraryWithCategory); +const mockedUpdateNovel = jest.mocked(updateNovel); + +const novel = (id: number, pluginId: string, name: string): LibraryNovelInfo => + ({ + id, + pluginId, + name, + path: `/${name}`, + }) as LibraryNovelInfo; + +const flushPromises = () => new Promise(resolve => setImmediate(resolve)); + +describe('updateLibrary', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('updates at most three different sources while keeping each source sequential', async () => { + const novels = [ + novel(1, 'source-a', 'A One'), + novel(2, 'source-a', 'A Two'), + novel(3, 'source-b', 'B One'), + novel(4, 'source-c', 'C One'), + novel(5, 'source-d', 'D One'), + ]; + mockedGetLibraryNovels.mockReturnValue(novels); + + const completions = new Map void>(); + mockedUpdateNovel.mockImplementation( + (_pluginId, _path, novelId) => + new Promise(resolve => completions.set(novelId, resolve)), + ); + + let metadata: BackgroundTaskMetadata = { + name: 'Update library', + isRunning: false, + progress: undefined, + progressText: undefined, + }; + const progressSnapshots: BackgroundTaskMetadata[] = []; + const setMeta = ( + transform: (meta: BackgroundTaskMetadata) => BackgroundTaskMetadata, + ) => { + metadata = transform(metadata); + progressSnapshots.push(metadata); + }; + + const updatePromise = updateLibrary({}, setMeta); + await flushPromises(); + + expect(mockedUpdateNovel.mock.calls.map(([pluginId]) => pluginId)).toEqual([ + 'source-a', + 'source-b', + 'source-c', + ]); + expect(progressSnapshots[progressSnapshots.length - 1]?.progressText).toBe( + ['A One', 'B One', 'C One'].join('\n'), + ); + + completions.get(3)?.(); + await flushPromises(); + expect(mockedUpdateNovel).toHaveBeenCalledTimes(4); + expect(mockedUpdateNovel.mock.calls[3][0]).toBe('source-d'); + + completions.get(1)?.(); + await flushPromises(); + expect(mockedUpdateNovel).toHaveBeenCalledTimes(5); + expect(mockedUpdateNovel.mock.calls[4][0]).toBe('source-a'); + + completions.get(2)?.(); + completions.get(4)?.(); + completions.get(5)?.(); + await updatePromise; + + expect(metadata).toMatchObject({ + isRunning: false, + progress: 1, + progressText: undefined, + }); + }); + + it('uses the category query when an update is category-scoped', async () => { + mockedGetLibraryWithCategory.mockReturnValue([]); + + await updateLibrary({ categoryId: 4 }, jest.fn()); + + expect(mockedGetLibraryWithCategory).toHaveBeenCalledWith( + 4, + undefined, + true, + ); + expect(mockedGetLibraryNovels).not.toHaveBeenCalled(); + }); +}); diff --git a/src/services/updates/index.ts b/src/services/updates/index.ts index b4a539a932..ff2c005289 100644 --- a/src/services/updates/index.ts +++ b/src/services/updates/index.ts @@ -13,6 +13,20 @@ import dayjs from 'dayjs'; import { APP_SETTINGS, AppSettings } from '@hooks/persisted/useSettings'; import { BackgroundTaskMetadata } from '@services/ServiceManager'; +const UPDATE_SOURCE_CONCURRENCY = 3; + +const groupNovelsByPlugin = ( + novels: LibraryNovelInfo[], +): LibraryNovelInfo[][] => { + const groupedNovels = new Map(); + for (const novel of novels) { + const pluginNovels = groupedNovels.get(novel.pluginId); + if (pluginNovels) pluginNovels.push(novel); + else groupedNovels.set(novel.pluginId, [novel]); + } + return [...groupedNovels.values()]; +}; + const updateLibrary = async ( { categoryId, @@ -55,26 +69,50 @@ const updateLibrary = async ( if (libraryNovels.length > 0) { MMKVStorage.set(LAST_UPDATE_TIME, dayjs().format('YYYY-MM-DD HH:mm:ss')); - for (let i = 0; i < libraryNovels.length; i++) { + + const sourceQueues = groupNovelsByPlugin(libraryNovels); + const activeNovels = new Map(); + let completedNovels = 0; + let nextSourceQueue = 0; + + const publishProgress = () => { setMeta(meta => ({ ...meta, - progressText: libraryNovels[i].name, - progress: i / libraryNovels.length, + progressText: [...activeNovels.values()].join('\n') || undefined, + progress: completedNovels / libraryNovels.length, })); + }; + + const updateSourceQueue = async (sourceQueue: LibraryNovelInfo[]) => { + for (const novel of sourceQueue) { + activeNovels.set(novel.pluginId, novel.name); + publishProgress(); + try { + await updateNovel(novel.pluginId, novel.path, novel.id, options); + await sleep(1000); + } catch (error: any) { + showToast(novel.name + ': ' + error.message); + } finally { + completedNovels += 1; + activeNovels.delete(novel.pluginId); + publishProgress(); + } + } + }; - try { - await updateNovel( - libraryNovels[i].pluginId, - libraryNovels[i].path, - libraryNovels[i].id, - options, - ); - await sleep(1000); - } catch (error: any) { - showToast(libraryNovels[i].name + ': ' + error.message); - continue; + const updateNextSource = async () => { + while (nextSourceQueue < sourceQueues.length) { + const sourceQueue = sourceQueues[nextSourceQueue++]; + await updateSourceQueue(sourceQueue); } - } + }; + + await Promise.all( + Array.from( + { length: Math.min(UPDATE_SOURCE_CONCURRENCY, sourceQueues.length) }, + updateNextSource, + ), + ); } else { showToast("There's no novel to be updated"); } @@ -82,6 +120,7 @@ const updateLibrary = async ( setMeta(meta => ({ ...meta, progress: 1, + progressText: undefined, isRunning: false, })); }; From f7c4b65e8afa78bd85cce1411a1641d636b6c26e Mon Sep 17 00:00:00 2001 From: bizzkoot Date: Tue, 4 Aug 2026 20:53:01 +0800 Subject: [PATCH 26/54] feat(tracker): add Kitsu tracker support --- assets/kitsu.png | Bin 0 -> 142923 bytes src/hooks/persisted/useTrackedNovel.ts | 1 + src/hooks/persisted/useTracker.ts | 2 + .../components/Tracker/ScoreSelectors.tsx | 36 ++ .../Tracker/SetTrackScoreDialog.tsx | 8 + .../novel/components/Tracker/TrackerCards.tsx | 9 +- .../novel/components/Tracker/constants.ts | 23 + .../settings/SettingsTrackerScreen.tsx | 166 ++++-- ...LoginDialog.tsx => TrackerLoginDialog.tsx} | 16 +- src/services/Trackers/__tests__/kitsu.test.ts | 426 +++++++++++++++ src/services/Trackers/index.ts | 2 +- src/services/Trackers/kitsu.ts | 489 ++++++++++++++++++ 12 files changed, 1130 insertions(+), 48 deletions(-) create mode 100644 assets/kitsu.png rename src/screens/settings/components/{MangaUpdatesLoginDialog.tsx => TrackerLoginDialog.tsx} (92%) create mode 100644 src/services/Trackers/__tests__/kitsu.test.ts create mode 100644 src/services/Trackers/kitsu.ts diff --git a/assets/kitsu.png b/assets/kitsu.png new file mode 100644 index 0000000000000000000000000000000000000000..4d487aaad5f15db4879751cd68b8c4bb494316ea GIT binary patch literal 142923 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�^UG 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 T!pu1|)XxJW^Sj^g*vI}q1t|yb literal 0 HcmV?d00001 diff --git a/src/hooks/persisted/useTrackedNovel.ts b/src/hooks/persisted/useTrackedNovel.ts index e60a31e167..e02cad3fcc 100644 --- a/src/hooks/persisted/useTrackedNovel.ts +++ b/src/hooks/persisted/useTrackedNovel.ts @@ -56,6 +56,7 @@ export const useTrackedNovel = (novelId: number | 'NO_ID') => { 'AniList', 'MyAnimeList', 'MangaUpdates', + 'Kitsu', ]; trackerNames.forEach(trackerName => { diff --git a/src/hooks/persisted/useTracker.ts b/src/hooks/persisted/useTracker.ts index f854840bc5..9232928d25 100644 --- a/src/hooks/persisted/useTracker.ts +++ b/src/hooks/persisted/useTracker.ts @@ -2,6 +2,7 @@ import { AuthenticationResult, Tracker, TrackerName } from '@services/Trackers'; import { aniListTracker } from '@services/Trackers/aniList'; import { myAnimeListTracker } from '@services/Trackers/myAnimeList'; import { mangaUpdatesTracker } from '@services/Trackers/mangaUpdates'; +import { kitsuTracker } from '@services/Trackers/kitsu'; import { useMMKVObject, useMMKVString } from 'react-native-mmkv'; import { useEffect } from 'react'; import { @@ -23,6 +24,7 @@ const trackers: Record = { AniList: aniListTracker, MyAnimeList: myAnimeListTracker, MangaUpdates: mangaUpdatesTracker, + Kitsu: kitsuTracker, }; export const getTracker = (name: TrackerName) => { diff --git a/src/screens/novel/components/Tracker/ScoreSelectors.tsx b/src/screens/novel/components/Tracker/ScoreSelectors.tsx index 799a8aee48..e8c907256c 100644 --- a/src/screens/novel/components/Tracker/ScoreSelectors.tsx +++ b/src/screens/novel/components/Tracker/ScoreSelectors.tsx @@ -6,6 +6,7 @@ import { RadioButton, RadioButtonGroup } from '@components/RadioButton'; import { useTheme, useAppSettings } from '@hooks/persisted'; import { getAniListScoreFormatting, + getKitsuScoreFormatting, getMyAnimeListScoreLabel, } from './constants'; import { @@ -164,3 +165,38 @@ export const AniListScoreSelector: React.FC = ({ ); }; + +/** + * Kitsu uses a 0.5-10 scale (displayed as half-increments). + * Score values: 0 (no score), 1 (0.5), 2 (1.0), 3 (1.5), ... 20 (10.0) + */ +export const KitsuScoreSelector: React.FC = ({ + trackItem, + onUpdateScore, +}) => { + const theme = useTheme(); + const formatting = getKitsuScoreFormatting(); + const scores = Array.from({ length: formatting.count }, (_, i) => i); + + const handleValueChange = (value: string) => { + onUpdateScore(Number(value)); + }; + + return ( + + + {scores.map(score => ( + + ))} + + + ); +}; diff --git a/src/screens/novel/components/Tracker/SetTrackScoreDialog.tsx b/src/screens/novel/components/Tracker/SetTrackScoreDialog.tsx index 7080679312..92bb2b1675 100644 --- a/src/screens/novel/components/Tracker/SetTrackScoreDialog.tsx +++ b/src/screens/novel/components/Tracker/SetTrackScoreDialog.tsx @@ -5,6 +5,7 @@ import { Button, DialogTitle, Modal } from '@components'; import { getString } from '@strings/translations'; import { AniListScoreSelector, + KitsuScoreSelector, MangaUpdatesScoreSelector, MyAnimeListScoreSelector, } from './ScoreSelectors'; @@ -46,6 +47,13 @@ const SetTrackScoreDialog: React.FC = ({ onUpdateScore={setSelectedScore} /> ); + case 'Kitsu': + return ( + + ); case 'AniList': default: return ( diff --git a/src/screens/novel/components/Tracker/TrackerCards.tsx b/src/screens/novel/components/Tracker/TrackerCards.tsx index c39bf9128f..0bce6cc30b 100644 --- a/src/screens/novel/components/Tracker/TrackerCards.tsx +++ b/src/screens/novel/components/Tracker/TrackerCards.tsx @@ -4,7 +4,10 @@ import { IconButton } from 'react-native-paper'; import { useAppSettings, useTheme } from '@hooks/persisted'; import { scaleDimension } from '@theme/scaling'; -import { getAniListScoreFormatting } from './constants'; +import { + getAniListScoreFormatting, + getKitsuScoreFormatting, +} from './constants'; import { AddTrackingCardProps, TrackedItemCardProps } from './types'; import { useScaledDimensions } from '@hooks/useScaledDimensions'; import AppText from '@components/AppText'; @@ -304,6 +307,10 @@ export const TrackedItemCard: React.FC = ({ : trackItem.score.toFixed(1); } + if (tracker.name === 'Kitsu') { + return getKitsuScoreFormatting().label(trackItem.score); + } + return trackItem.score; }, [tracker, trackItem.score]); diff --git a/src/screens/novel/components/Tracker/constants.ts b/src/screens/novel/components/Tracker/constants.ts index 9d0c0992f5..61dff640da 100644 --- a/src/screens/novel/components/Tracker/constants.ts +++ b/src/screens/novel/components/Tracker/constants.ts @@ -7,6 +7,7 @@ export const TRACKER_ICONS: Record = { MyAnimeList: require('../../../../../assets/mal.png'), MangaUpdates: require('../../../../../assets/mangaupdates.png'), AniList: require('../../../../../assets/anilist.png'), + Kitsu: require('../../../../../assets/kitsu.png'), }; export const STATUS_LABELS: Record = { @@ -91,3 +92,25 @@ export const getAniListScoreFormatting = ( }; } }; + +/** + * Kitsu uses a 0.5-10 scale (displayed as half-increments). + * Internally stored as ratingTwenty (2-20), displayed as ratingTwenty/2. + * This provides 21 options: 0 (no score), 0.5, 1.0, 1.5, ... 10.0 + */ +export const getKitsuScoreFormatting = (): ScoreFormatting => { + return { + count: 21, + label: score => { + if (score === 0) { + return '-'; + } + /* Convert index to Kitsu's half-point scale (0.5-10) */ + const displayScore = score / 2; + return displayScore.toLocaleString(undefined, { + minimumFractionDigits: displayScore % 1 === 0 ? 0 : 1, + maximumFractionDigits: 1, + }); + }, + }; +}; diff --git a/src/screens/settings/SettingsTrackerScreen.tsx b/src/screens/settings/SettingsTrackerScreen.tsx index b022693b94..29079123c5 100644 --- a/src/screens/settings/SettingsTrackerScreen.tsx +++ b/src/screens/settings/SettingsTrackerScreen.tsx @@ -18,8 +18,9 @@ import { scaleDimension } from '@theme/scaling'; import { Appbar, List, Modal, SafeAreaView } from '@components'; import { TrackerSettingsScreenProps } from '@navigators/types'; import { getString } from '@strings/translations'; -import MangaUpdatesLoginDialog from './components/MangaUpdatesLoginDialog'; -import { authenticateWithCredentials } from '@services/Trackers/mangaUpdates'; +import TrackerLoginDialog from './components/TrackerLoginDialog'; +import { authenticateWithCredentials as mangaUpdatesAuth } from '@services/Trackers/mangaUpdates'; +import { authenticateWithCredentials as kitsuAuth } from '@services/Trackers/kitsu'; import { showToast } from '@utils/showToast'; interface TrackerStyles { @@ -54,6 +55,15 @@ const MangaUpdatesLogo = ({ styles }: { styles: TrackerStyles }) => ( ); +const KitsuLogo = ({ styles }: { styles: TrackerStyles }) => ( + + + +); + const TrackerScreen = ({ navigation }: TrackerSettingsScreenProps) => { const theme = useTheme(); const { isTrackerAuthenticated, setTracker, removeTracker, getTrackerAuth } = @@ -115,26 +125,37 @@ const TrackerScreen = ({ navigation }: TrackerSettingsScreenProps) => { setLogoutTrackerName(''); }; - // MangaUpdates Login Dialog - const [mangaUpdatesLoginVisible, setMangaUpdatesLoginVisible] = - useState(false); - const showMangaUpdatesLogin = () => setMangaUpdatesLoginVisible(true); - const hideMangaUpdatesLogin = () => setMangaUpdatesLoginVisible(false); + // Credential-based Login Dialog (MangaUpdates, Kitsu) + const [credentialLoginTracker, setCredentialLoginTracker] = useState< + 'MangaUpdates' | 'Kitsu' | null + >(null); + const showCredentialLogin = (tracker: 'MangaUpdates' | 'Kitsu') => + setCredentialLoginTracker(tracker); + const hideCredentialLogin = () => setCredentialLoginTracker(null); + + const handleCredentialLogin = async (username: string, password: string) => { + if (!credentialLoginTracker) { + return; + } - const handleMangaUpdatesLogin = async ( - username: string, - password: string, - ) => { try { - const auth = await authenticateWithCredentials(username, password); - setTracker('MangaUpdates', auth); - hideMangaUpdatesLogin(); - showToast('Successfully logged in to MangaUpdates'); + let auth; + if (credentialLoginTracker === 'MangaUpdates') { + auth = await mangaUpdatesAuth(username, password); + } else if (credentialLoginTracker === 'Kitsu') { + auth = await kitsuAuth(username, password); + } else { + throw new Error('Unknown tracker'); + } + + setTracker(credentialLoginTracker, auth); + hideCredentialLogin(); + showToast(`Successfully logged in to ${credentialLoginTracker}`); } catch (error) { if (error instanceof Error) { throw error; // Let the dialog handle the error display } - throw new Error('Failed to authenticate with MangaUpdates'); + throw new Error(`Failed to authenticate with ${credentialLoginTracker}`); } }; @@ -232,35 +253,95 @@ const TrackerScreen = ({ navigation }: TrackerSettingsScreenProps) => { if (isTrackerAuthenticated('MangaUpdates')) { showModal('MangaUpdates'); } else { - showMangaUpdatesLogin(); + showCredentialLogin('MangaUpdates'); } }} rippleColor={theme.rippleColor} style={styles.listItem} /> - {isTrackerAuthenticated('MyAnimeList') && - getTrackerAuth('MyAnimeList')?.auth?.expiresAt && - getTrackerAuth('MyAnimeList')!.auth.expiresAt < - new Date(Date.now()) ? ( + } + right={ + isTrackerAuthenticated('Kitsu') + ? () => ( + + ) + : undefined + } + onPress={() => { + if (isTrackerAuthenticated('Kitsu')) { + showModal('Kitsu'); + } else { + showCredentialLogin('Kitsu'); + } + }} + rippleColor={theme.rippleColor} + style={styles.listItem} + /> + {(isTrackerAuthenticated('MyAnimeList') && + getTrackerAuth('MyAnimeList')?.auth?.expiresAt && + getTrackerAuth('MyAnimeList')!.auth.expiresAt < + new Date(Date.now())) || + (isTrackerAuthenticated('Kitsu') && + getTrackerAuth('Kitsu')?.auth?.expiresAt && + getTrackerAuth('Kitsu')!.auth.expiresAt < + new Date(Date.now())) ? ( <> {getString('common.settings')} - { - const trackerAuth = getTrackerAuth('MyAnimeList'); - const revalidate = getTracker('MyAnimeList')?.revalidate; - if (revalidate && trackerAuth) { - const auth = await revalidate(trackerAuth.auth); - setTracker('MyAnimeList', auth); - } - }} - theme={theme} - /> + {isTrackerAuthenticated('MyAnimeList') && + getTrackerAuth('MyAnimeList')?.auth?.expiresAt && + getTrackerAuth('MyAnimeList')!.auth.expiresAt < + new Date(Date.now()) && ( + { + const trackerAuth = getTrackerAuth('MyAnimeList'); + const revalidate = + getTracker('MyAnimeList')?.revalidate; + if (revalidate && trackerAuth) { + const auth = await revalidate(trackerAuth.auth); + setTracker('MyAnimeList', auth); + } + }} + theme={theme} + /> + )} + {isTrackerAuthenticated('Kitsu') && + getTrackerAuth('Kitsu')?.auth?.expiresAt && + getTrackerAuth('Kitsu')!.auth.expiresAt < + new Date(Date.now()) && ( + { + const trackerAuth = getTrackerAuth('Kitsu'); + const revalidate = getTracker('Kitsu')?.revalidate; + if (revalidate && trackerAuth) { + try { + const auth = await revalidate(trackerAuth.auth); + setTracker('Kitsu', auth); + showToast('Successfully refreshed Kitsu session'); + } catch (error) { + showToast( + 'Failed to refresh Kitsu session. Please log in again.', + ); + removeTracker('Kitsu'); + } + } + }} + theme={theme} + /> + )} ) : null} @@ -294,7 +375,8 @@ const TrackerScreen = ({ navigation }: TrackerSettingsScreenProps) => { logoutTrackerName as | 'AniList' | 'MyAnimeList' - | 'MangaUpdates', + | 'MangaUpdates' + | 'Kitsu', ); hideModal(); }} @@ -303,10 +385,14 @@ const TrackerScreen = ({ navigation }: TrackerSettingsScreenProps) => { - diff --git a/src/screens/settings/components/MangaUpdatesLoginDialog.tsx b/src/screens/settings/components/TrackerLoginDialog.tsx similarity index 92% rename from src/screens/settings/components/MangaUpdatesLoginDialog.tsx rename to src/screens/settings/components/TrackerLoginDialog.tsx index b043a88ae6..8b0f3185e5 100644 --- a/src/screens/settings/components/MangaUpdatesLoginDialog.tsx +++ b/src/screens/settings/components/TrackerLoginDialog.tsx @@ -6,16 +6,20 @@ import { Modal } from '@components'; import { useAppSettings, useTheme } from '@hooks/persisted'; import { scaleDimension } from '@theme/scaling'; -interface MangaUpdatesLoginDialogProps { +interface TrackerLoginDialogProps { visible: boolean; + trackerName: string; onDismiss: () => void; onSubmit: (username: string, password: string) => Promise; + usernameLabel?: string; } -const MangaUpdatesLoginDialog: React.FC = ({ +const TrackerLoginDialog: React.FC = ({ visible, + trackerName, onDismiss, onSubmit, + usernameLabel = 'Username', }) => { const theme = useTheme(); const { uiScale = 1.0 } = useAppSettings(); @@ -67,7 +71,7 @@ const MangaUpdatesLoginDialog: React.FC = ({ const handleSubmit = async () => { if (!username.trim() || !password.trim()) { - setError('Username and password are required'); + setError(`${usernameLabel} and password are required`); return; } @@ -97,7 +101,7 @@ const MangaUpdatesLoginDialog: React.FC = ({ - Login to MangaUpdates + Login to {trackerName} = ({ borderColor: theme.outline, }, ]} - placeholder="Username" + placeholder={usernameLabel} placeholderTextColor={theme.onSurfaceVariant} value={username} onChangeText={setUsername} @@ -167,4 +171,4 @@ const MangaUpdatesLoginDialog: React.FC = ({ ); }; -export default MangaUpdatesLoginDialog; +export default TrackerLoginDialog; diff --git a/src/services/Trackers/__tests__/kitsu.test.ts b/src/services/Trackers/__tests__/kitsu.test.ts new file mode 100644 index 0000000000..14371b366b --- /dev/null +++ b/src/services/Trackers/__tests__/kitsu.test.ts @@ -0,0 +1,426 @@ +/** + * Kitsu tracker unit tests + * + * Tests the Kitsu tracker's search, library entry and score conversion + * logic by mocking the global fetch API. + */ + +import { authenticateWithCredentials, kitsuTracker } from '../kitsu'; + +const BASE_URL = 'https://kitsu.app/api/edge/'; +const LOGIN_URL = 'https://kitsu.app/api/oauth/token'; +const ALGOLIA_KEY_URL = 'https://kitsu.app/api/edge/algolia-keys/media/'; + +const originalFetch = global.fetch; +let mockFetch: jest.Mock; + +const jsonResponse = (status: number, body: any) => ({ + status, + json: jest.fn().mockResolvedValue(body), +}); + +const auth = { + accessToken: 'test-access-token', + refreshToken: 'test-refresh-token', + expiresAt: new Date(), + meta: { + userId: 'user-1', + refreshToken: 'test-refresh-token', + createdAt: 1, + }, +}; + +const getFetchCall = (predicate: (url: string, options: any) => boolean) => + mockFetch.mock.calls.find( + ([url, options]: any[]) => + typeof url === 'string' && predicate(url, options || {}), + ); + +describe('kitsuTracker', () => { + beforeEach(() => { + mockFetch = jest.fn(); + global.fetch = mockFetch as unknown as typeof fetch; + }); + + afterEach(() => { + global.fetch = originalFetch; + jest.clearAllMocks(); + }); + + describe('handleSearch', () => { + beforeEach(() => { + mockFetch.mockImplementation((url: string) => { + if (url === ALGOLIA_KEY_URL) { + return Promise.resolve( + jsonResponse(200, { media: { key: 'algolia-test-key' } }), + ); + } + return Promise.resolve(jsonResponse(200, { hits: [] })); + }); + }); + + it('should return only items with subtype "novel" mapped to SearchResult', async () => { + mockFetch.mockImplementation((url: string) => { + if (url === ALGOLIA_KEY_URL) { + return Promise.resolve( + jsonResponse(200, { media: { key: 'algolia-test-key' } }), + ); + } + return Promise.resolve( + jsonResponse(200, { + hits: [ + { + id: 1, + canonicalTitle: 'Novel One', + chapterCount: 12, + subtype: 'novel', + posterImage: { original: 'https://example.com/one.png' }, + synopsis: 'A novel', + }, + { + id: 2, + canonicalTitle: 'Manga Only', + chapterCount: 3, + subtype: 'manga', + posterImage: { original: 'https://example.com/two.png' }, + synopsis: 'Not a novel', + }, + { + id: 3, + canonicalTitle: 'Novel Two', + chapterCount: null, + subtype: 'novel', + posterImage: null, + synopsis: null, + }, + ], + }), + ); + }); + + const result = await kitsuTracker.handleSearch('test', auth); + + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ + id: 1, + title: 'Novel One', + coverImage: 'https://example.com/one.png', + totalChapters: 12, + }); + expect(result[1]).toEqual({ + id: 3, + title: 'Novel Two', + coverImage: '', + totalChapters: undefined, + }); + expect(result[1].coverImage).toBe(''); + expect(result[1].totalChapters).toBeUndefined(); + }); + + it('should return an empty array when Algolia returns a non-200 status', async () => { + mockFetch.mockImplementation((url: string) => { + if (url === ALGOLIA_KEY_URL) { + return Promise.resolve( + jsonResponse(200, { media: { key: 'algolia-test-key' } }), + ); + } + return Promise.resolve(jsonResponse(500, { error: 'boom' })); + }); + + const result = await kitsuTracker.handleSearch('test', auth); + + expect(result).toEqual([]); + }); + + it('should return an empty array when the Algolia key fetch fails', async () => { + mockFetch.mockResolvedValue(jsonResponse(403, { error: 'forbidden' })); + + const result = await kitsuTracker.handleSearch('test', auth); + + expect(result).toEqual([]); + }); + }); + + describe('getUserListEntry', () => { + it('should map an existing library entry to normalized status, progress and score', async () => { + mockFetch.mockResolvedValue( + jsonResponse(200, { + data: [ + { + id: 'lib-1', + attributes: { + status: 'current', + progress: 10, + ratingTwenty: 16, + startedAt: null, + finishedAt: null, + }, + }, + ], + }), + ); + + const result = await kitsuTracker.getUserListEntry(42, auth); + + expect(result).toEqual({ status: 'CURRENT', progress: 10, score: 8 }); + }); + + it('should map on_hold to PAUSED and planned to PLANNING', async () => { + mockFetch.mockResolvedValue( + jsonResponse(200, { + data: [ + { + id: 'lib-2', + attributes: { + status: 'on_hold', + progress: 3, + ratingTwenty: null, + startedAt: null, + finishedAt: null, + }, + }, + ], + }), + ); + + const result = await kitsuTracker.getUserListEntry(42, auth); + + expect(result.status).toBe('PAUSED'); + expect(result.score).toBe(0); + }); + + it('should return default values when no entry exists', async () => { + mockFetch.mockResolvedValue(jsonResponse(200, { data: [] })); + + const result = await kitsuTracker.getUserListEntry(42, auth); + + expect(result).toEqual({ status: 'CURRENT', progress: 0, score: 0 }); + }); + }); + + describe('updateUserListEntry', () => { + it('should PATCH an existing library entry with score*2 as ratingTwenty and mapped status', async () => { + mockFetch.mockImplementation((url: string) => { + if (url.includes('library-entries?filter[manga_id]=')) { + return Promise.resolve( + jsonResponse(200, { + data: [ + { + id: 'lib-1', + attributes: { + status: 'current', + progress: 10, + ratingTwenty: 10, + startedAt: null, + finishedAt: null, + }, + }, + ], + }), + ); + } + return Promise.resolve(jsonResponse(200, { data: { id: 'lib-1' } })); + }); + + const result = await kitsuTracker.updateUserListEntry( + 42, + { status: 'COMPLETED', progress: 20, score: 8 }, + auth, + ); + + expect(result).toEqual({ status: 'COMPLETED', progress: 20, score: 8 }); + + const patchCall = getFetchCall(url => url.includes('library-entries/')); + expect(patchCall).toBeDefined(); + const [patchUrl, patchOptions] = patchCall!; + expect(patchUrl).toBe(`${BASE_URL}library-entries/lib-1`); + expect(patchOptions.method).toBe('PATCH'); + const patchBody = JSON.parse(patchOptions.body); + expect(patchBody.data.attributes).toEqual({ + status: 'completed', + progress: 20, + ratingTwenty: 16, + }); + }); + + it('should POST to create a new library entry when none exists', async () => { + mockFetch.mockImplementation((url: string) => { + if (url.includes('library-entries?filter[manga_id]=')) { + return Promise.resolve(jsonResponse(200, { data: [] })); + } + if (url === `${BASE_URL}library-entries`) { + return Promise.resolve( + jsonResponse(201, { data: { id: 'new-lib-1' } }), + ); + } + return Promise.resolve( + jsonResponse(200, { data: { id: 'new-lib-1' } }), + ); + }); + + const result = await kitsuTracker.updateUserListEntry( + 42, + { status: 'PLANNING', progress: 0, score: 0 }, + auth, + ); + + expect(result).toEqual({ status: 'PLANNING', progress: 0, score: 0 }); + + const postCall = getFetchCall( + (url, options) => + url === `${BASE_URL}library-entries` && options.method === 'POST', + ); + expect(postCall).toBeDefined(); + const postBody = JSON.parse(postCall![1].body); + expect(postBody.data.attributes).toEqual({ + status: 'planned', + progress: 0, + }); + expect(postBody.data.relationships.media.data).toEqual({ + id: '42', + type: 'manga', + }); + }); + + it('should PATCH the newly created entry with score when score is provided', async () => { + mockFetch.mockImplementation((url: string) => { + if (url.includes('library-entries?filter[manga_id]=')) { + return Promise.resolve(jsonResponse(200, { data: [] })); + } + if (url === `${BASE_URL}library-entries`) { + return Promise.resolve( + jsonResponse(201, { data: { id: 'new-lib-1' } }), + ); + } + return Promise.resolve( + jsonResponse(200, { data: { id: 'new-lib-1' } }), + ); + }); + + await kitsuTracker.updateUserListEntry( + 42, + { status: 'CURRENT', progress: 5, score: 4 }, + auth, + ); + + const patchCall = getFetchCall( + url => url === `${BASE_URL}library-entries/new-lib-1`, + ); + expect(patchCall).toBeDefined(); + const patchBody = JSON.parse(patchCall![1].body); + expect(patchBody.data.attributes.ratingTwenty).toBe(8); + }); + + it('should throw when the library update fails', async () => { + mockFetch.mockImplementation((url: string) => { + if (url.includes('library-entries?filter[manga_id]=')) { + return Promise.resolve( + jsonResponse(200, { + data: [ + { + id: 'lib-1', + attributes: { + status: 'current', + progress: 10, + ratingTwenty: 10, + startedAt: null, + finishedAt: null, + }, + }, + ], + }), + ); + } + return Promise.resolve(jsonResponse(500, { error: 'boom' })); + }); + + await expect( + kitsuTracker.updateUserListEntry( + 42, + { status: 'COMPLETED', progress: 20, score: 8 }, + auth, + ), + ).rejects.toThrow('Failed to update Kitsu entry'); + }); + }); + + describe('authenticateWithCredentials', () => { + it('should exchange credentials for a token and fetch the current user id', async () => { + mockFetch.mockImplementation((url: string) => { + if (url === LOGIN_URL) { + return Promise.resolve( + jsonResponse(200, { + access_token: 'access-1', + token_type: 'Bearer', + created_at: 1000, + expires_in: 3600, + refresh_token: 'refresh-1', + }), + ); + } + if (url.includes('users?filter[self]=true')) { + return Promise.resolve( + jsonResponse(200, { data: [{ id: 'user-1' }] }), + ); + } + return Promise.resolve(jsonResponse(404, {})); + }); + + const result = await authenticateWithCredentials( + 'test@example.com', + 'password', + ); + + expect(result.accessToken).toBe('access-1'); + expect(result.refreshToken).toBe('refresh-1'); + expect(result.meta?.userId).toBe('user-1'); + }); + + it('should throw with the error description on invalid credentials', async () => { + mockFetch.mockResolvedValue( + jsonResponse(400, { + error_description: 'Invalid username or password', + }), + ); + + await expect(authenticateWithCredentials('bad', 'bad')).rejects.toThrow( + 'Invalid username or password', + ); + }); + }); + + describe('revalidate', () => { + it('should refresh the session using the stored refresh token', async () => { + mockFetch.mockResolvedValue( + jsonResponse(200, { + access_token: 'access-2', + token_type: 'Bearer', + created_at: 2000, + expires_in: 3600, + refresh_token: 'refresh-2', + }), + ); + + const result = await kitsuTracker.revalidate!(auth); + + expect(result.accessToken).toBe('access-2'); + expect(result.refreshToken).toBe('refresh-2'); + expect(result.meta?.userId).toBe('user-1'); + expect(result.meta?.refreshToken).toBe('refresh-2'); + + const loginCall = getFetchCall((url, options) => url === LOGIN_URL); + expect(loginCall![1].body).toContain('grant_type=refresh_token'); + }); + + it('should throw when no refresh token is available', async () => { + await expect( + kitsuTracker.revalidate!({ + accessToken: 'access-1', + refreshToken: 'refresh-1', + expiresAt: new Date(), + meta: undefined, + }), + ).rejects.toThrow('No refresh token available'); + }); + }); +}); diff --git a/src/services/Trackers/index.ts b/src/services/Trackers/index.ts index 736a614ed4..db5e00ec72 100644 --- a/src/services/Trackers/index.ts +++ b/src/services/Trackers/index.ts @@ -1,4 +1,4 @@ -export type TrackerName = 'AniList' | 'MyAnimeList' | 'MangaUpdates'; +export type TrackerName = 'AniList' | 'MyAnimeList' | 'MangaUpdates' | 'Kitsu'; /** * Normalized list status values. * Not all trackers will use all of these. diff --git a/src/services/Trackers/kitsu.ts b/src/services/Trackers/kitsu.ts new file mode 100644 index 0000000000..780fc999e4 --- /dev/null +++ b/src/services/Trackers/kitsu.ts @@ -0,0 +1,489 @@ +import { AuthenticationResult, Tracker, UserListStatus } from './index'; + +/** + * Kitsu Tracker Implementation + * + * Kitsu uses username/password authentication with OAuth2 client credentials. + * It supports refresh tokens for session renewal. + */ + +const BASE_URL = 'https://kitsu.app/api/edge/'; +const LOGIN_URL = 'https://kitsu.app/api/oauth/token'; +const ALGOLIA_KEY_URL = 'https://kitsu.app/api/edge/algolia-keys/media/'; + +const ALGOLIA_APP_ID = 'AWQO5J657S'; +const ALGOLIA_URL = `https://${ALGOLIA_APP_ID}-dsn.algolia.net/1/indexes/production_media/query/`; +const ALGOLIA_FILTER = + '&facetFilters=%5B%22kind%3Amanga%22%5D&attributesToRetrieve=' + + '%5B%22synopsis%22%2C%22averageRating%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22' + + 'posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D'; + +/* Kitsu public OAuth credentials */ +const CLIENT_ID = + 'dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd'; +const CLIENT_SECRET = + '54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151'; + +const VND_API_JSON = 'application/vnd.api+json'; + +/* Kitsu status mapping */ +const KITSU_STATUS = { + CURRENT: 'current', + COMPLETED: 'completed', + ON_HOLD: 'on_hold', + DROPPED: 'dropped', + PLAN_TO_READ: 'planned', +} as const; + +export const kitsuToNormalized: Record = { + current: 'CURRENT', + completed: 'COMPLETED', + on_hold: 'PAUSED', + dropped: 'DROPPED', + planned: 'PLANNING', +}; + +const normalizedToKitsu: Record = { + CURRENT: 'current', + COMPLETED: 'completed', + PAUSED: 'on_hold', + DROPPED: 'dropped', + PLANNING: 'planned', + REPEATING: 'current', +}; + +type KitsuAuthMeta = { + userId: string; + refreshToken: string; + createdAt: number; +}; + +type KitsuOAuth = { + access_token: string; + token_type: string; + created_at: number; + expires_in: number; + refresh_token: string; +}; + +type KitsuAlgoliaSearchItem = { + id: number; + canonicalTitle: string; + chapterCount: number | null; + subtype: string | null; + posterImage: { + original: string; + large?: string; + medium?: string; + small?: string; + tiny?: string; + } | null; + synopsis: string | null; +}; + +type KitsuLibraryEntry = { + id: string; + attributes: { + status: string; + progress: number; + ratingTwenty: number | null; + startedAt: string | null; + finishedAt: string | null; + }; +}; + +/** + * Authenticates with Kitsu using username and password. + * This function is called from the login dialog in settings. + */ +export async function authenticateWithCredentials( + username: string, + password: string, +): Promise> { + const formBody = new URLSearchParams({ + username, + password, + grant_type: 'password', + client_id: CLIENT_ID, + client_secret: CLIENT_SECRET, + }).toString(); + + const response = await fetch(LOGIN_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: formBody, + }); + + if (response.status !== 200) { + const error = await response.json().catch(() => ({})); + throw new Error(error.error_description || 'Invalid username or password'); + } + + const oauth: KitsuOAuth = await response.json(); + + /* Get current user ID */ + const userId = await getCurrentUser(oauth.access_token); + + return { + accessToken: oauth.access_token, + refreshToken: oauth.refresh_token, + expiresAt: new Date((oauth.created_at + oauth.expires_in) * 1000), + meta: { + userId, + refreshToken: oauth.refresh_token, + createdAt: oauth.created_at, + }, + }; +} + +async function getCurrentUser(accessToken: string): Promise { + const url = `${BASE_URL}users?filter[self]=true`; + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: VND_API_JSON, + }, + }); + + if (response.status !== 200) { + throw new Error('Failed to get current user'); + } + + const data = await response.json(); + return data.data[0].id; +} + +async function getAlgoliaKey(accessToken: string): Promise { + const response = await fetch(ALGOLIA_KEY_URL, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: VND_API_JSON, + }, + }); + + if (response.status !== 200) { + throw new Error('Failed to get Algolia key'); + } + + const data = await response.json(); + return data.media.key; +} + +async function algoliaSearch( + key: string, + query: string, +): Promise { + const jsonBody = JSON.stringify({ + params: `query=${encodeURIComponent(query)}${ALGOLIA_FILTER}`, + }); + + const response = await fetch(ALGOLIA_URL, { + method: 'POST', + headers: { + 'X-Algolia-Application-Id': ALGOLIA_APP_ID, + 'X-Algolia-API-Key': key, + 'Content-Type': 'application/json', + }, + body: jsonBody, + }); + + if (response.status !== 200) { + return []; + } + + const data = await response.json(); + return data.hits || []; +} + +async function findLibraryEntry( + mediaId: number | string, + userId: string, + accessToken: string, +): Promise<{ libraryId: string; entry: KitsuLibraryEntry } | null> { + const url = `${BASE_URL}library-entries?filter[manga_id]=${mediaId}&filter[user_id]=${userId}&include=manga`; + + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: VND_API_JSON, + }, + }); + + if (response.status !== 200) { + return null; + } + + const data = await response.json(); + + if (data.data && data.data.length > 0) { + return { + libraryId: data.data[0].id, + entry: data.data[0], + }; + } + + return null; +} + +async function addToLibrary( + mediaId: number | string, + userId: string, + status: string, + progress: number, + accessToken: string, +): Promise { + const body = JSON.stringify({ + data: { + type: 'libraryEntries', + attributes: { + status, + progress, + }, + relationships: { + user: { + data: { + id: userId, + type: 'users', + }, + }, + media: { + data: { + id: String(mediaId), + type: 'manga', + }, + }, + }, + }, + }); + + const response = await fetch(`${BASE_URL}library-entries`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': VND_API_JSON, + 'Accept': VND_API_JSON, + }, + body, + }); + + if (response.status !== 201 && response.status !== 200) { + throw new Error('Failed to add to library'); + } + + const data = await response.json(); + return data.data.id; +} + +async function updateLibraryEntry( + libraryId: string, + status: string, + progress: number, + ratingTwenty: number | null, + accessToken: string, +): Promise { + const body = JSON.stringify({ + data: { + type: 'libraryEntries', + id: libraryId, + attributes: { + status, + progress, + ratingTwenty, + }, + }, + }); + + const response = await fetch(`${BASE_URL}library-entries/${libraryId}`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': VND_API_JSON, + 'Accept': VND_API_JSON, + }, + body, + }); + + if (response.status !== 200) { + throw new Error('Failed to update library entry'); + } +} + +/** + * Converts a 0-10 score to Kitsu's ratingTwenty (0-20 scale). + * Kitsu uses a 20-point scale internally (2-20), displayed as 0.5-10.0. + */ +function scoreToRatingTwenty(score: number): number | null { + if (score <= 0) { + return null; + } + /* Convert 0-10 to 0-20, ensuring minimum of 2 if score > 0 */ + return Math.max(2, Math.round(score * 2)); +} + +/** + * Converts Kitsu's ratingTwenty (0-20 scale) to a 0-10 score. + */ +function ratingTwentyToScore(ratingTwenty: number | null): number { + if (ratingTwenty === null || ratingTwenty <= 0) { + return 0; + } + return ratingTwenty / 2; +} + +export const kitsuTracker: Tracker = { + authenticate: async () => { + /** + * Authentication is handled by authenticateWithCredentials + * which is called from the login dialog in settings. + */ + throw new Error('Please use the login dialog to authenticate with Kitsu'); + }, + + revalidate: async auth => { + if (!auth.meta?.refreshToken) { + throw new Error('No refresh token available'); + } + + const formBody = new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: auth.meta.refreshToken, + client_id: CLIENT_ID, + client_secret: CLIENT_SECRET, + }).toString(); + + const response = await fetch(LOGIN_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: formBody, + }); + + if (response.status !== 200) { + throw new Error('Session expired, please re-authenticate'); + } + + const oauth: KitsuOAuth = await response.json(); + + return { + accessToken: oauth.access_token, + refreshToken: oauth.refresh_token, + expiresAt: new Date((oauth.created_at + oauth.expires_in) * 1000), + meta: { + userId: auth.meta.userId, + refreshToken: oauth.refresh_token, + createdAt: oauth.created_at, + }, + }; + }, + + handleSearch: async (search, auth) => { + try { + const algoliaKey = await getAlgoliaKey(auth.accessToken); + const hits = await algoliaSearch(algoliaKey, search); + + /* Filter to only include novels (subtype === 'novel') */ + return hits + .filter(hit => hit.subtype === 'novel') + .map(hit => ({ + id: hit.id, + title: hit.canonicalTitle, + coverImage: hit.posterImage?.original || '', + totalChapters: hit.chapterCount || undefined, + })); + } catch (error) { + return []; + } + }, + + getUserListEntry: async (id, auth) => { + try { + const entry = await findLibraryEntry( + id, + auth.meta!.userId, + auth.accessToken, + ); + + if (entry) { + return { + status: kitsuToNormalized[entry.entry.attributes.status] || 'CURRENT', + progress: entry.entry.attributes.progress || 0, + score: ratingTwentyToScore(entry.entry.attributes.ratingTwenty), + }; + } + + /* Entry not found, return defaults */ + return { + status: 'CURRENT', + progress: 0, + score: 0, + }; + } catch (error) { + return { + status: 'CURRENT', + progress: 0, + score: 0, + }; + } + }, + + updateUserListEntry: async (id, payload, auth) => { + const status = payload.status + ? normalizedToKitsu[payload.status] + : KITSU_STATUS.CURRENT; + const progress = Math.round(payload.progress || 0); + const ratingTwenty = scoreToRatingTwenty(payload.score || 0); + + try { + /* Check if entry exists */ + const existingEntry = await findLibraryEntry( + id, + auth.meta!.userId, + auth.accessToken, + ); + + let libraryId: string; + + if (existingEntry) { + libraryId = existingEntry.libraryId; + await updateLibraryEntry( + libraryId, + status, + progress, + ratingTwenty, + auth.accessToken, + ); + } else { + /* Create new entry */ + libraryId = await addToLibrary( + id, + auth.meta!.userId, + status, + progress, + auth.accessToken, + ); + + /* Update with score if provided */ + if (ratingTwenty !== null) { + await updateLibraryEntry( + libraryId, + status, + progress, + ratingTwenty, + auth.accessToken, + ); + } + } + + return { + status: payload.status || 'CURRENT', + progress, + score: payload.score || 0, + }; + } catch (error) { + throw new Error('Failed to update Kitsu entry'); + } + }, +}; From 0c6a6dc9662494349595026861198cedca73ed06 Mon Sep 17 00:00:00 2001 From: bizzkoot Date: Tue, 4 Aug 2026 20:59:56 +0800 Subject: [PATCH 27/54] feat(novel): jump to first unread chapter in read button and fab --- src/database/queries/ChapterQueries.ts | 11 ++++++++++ src/hooks/persisted/useNovel.ts | 13 ++++++++++++ .../novel/components/Info/NovelInfoHeader.tsx | 4 +++- .../novel/components/Info/ReadButton.tsx | 20 +++++++++---------- .../novel/components/NovelScreenList.tsx | 8 ++++++-- 5 files changed, 43 insertions(+), 13 deletions(-) diff --git a/src/database/queries/ChapterQueries.ts b/src/database/queries/ChapterQueries.ts index 6e4007573b..f6e83b5b15 100644 --- a/src/database/queries/ChapterQueries.ts +++ b/src/database/queries/ChapterQueries.ts @@ -413,6 +413,17 @@ export const getChapterCount = (novelId: number, page: string = '1') => page, )?.['COUNT(*)'] ?? 0; +export const getFirstUnreadChapter = ( + novelId: number, + filter?: string, + page?: string, +) => + db.getFirstAsync( + `SELECT * FROM Chapter WHERE novelId = ? AND page = ? AND unread = 1 ${filter || ''} ORDER BY position ASC LIMIT 1`, + novelId, + page || '1', + ); + export const getPageChaptersBatched = ( novelId: number, sort?: string, diff --git a/src/hooks/persisted/useNovel.ts b/src/hooks/persisted/useNovel.ts index 50676d916a..cc2e0fb2dc 100644 --- a/src/hooks/persisted/useNovel.ts +++ b/src/hooks/persisted/useNovel.ts @@ -21,6 +21,7 @@ import { insertChapters, getCustomPages, getChapterCount, + getFirstUnreadChapter as _getFirstUnreadChapter, getPageChaptersBatched, updateChapterProgress as _updateChapterProgress, } from '@database/queries/ChapterQueries'; @@ -93,6 +94,9 @@ export const useNovel = (novelOrPath: string | NovelInfo, pluginId: string) => { ); const [chapters, _setChapters] = useState([]); + const [firstUnreadChapter, setFirstUnreadChapter] = useState< + ChapterInfo | undefined + >(); const [batchInformation, setBatchInformation] = useState<{ batch: number; total: number; @@ -297,6 +301,13 @@ export const useNovel = (novelOrPath: string | NovelInfo, pluginId: string) => { totalChapters: chapterCount, }); setChapters(newChapters); + + const unread = await _getFirstUnreadChapter( + novel.id, + novelSettings.filter, + page, + ); + setFirstUnreadChapter(unread ?? undefined); } }, [ novel, @@ -574,6 +585,7 @@ export const useNovel = (novelOrPath: string | NovelInfo, pluginId: string) => { novel, lastRead, chapters, + firstUnreadChapter, novelSettings, batchInformation, getNextChapterBatch, @@ -605,6 +617,7 @@ export const useNovel = (novelOrPath: string | NovelInfo, pluginId: string) => { novel, lastRead, chapters, + firstUnreadChapter, novelSettings, batchInformation, getNextChapterBatch, diff --git a/src/screens/novel/components/Info/NovelInfoHeader.tsx b/src/screens/novel/components/Info/NovelInfoHeader.tsx index 4c2a43635a..56f73a2f2a 100644 --- a/src/screens/novel/components/Info/NovelInfoHeader.tsx +++ b/src/screens/novel/components/Info/NovelInfoHeader.tsx @@ -49,6 +49,7 @@ interface NovelInfoHeaderProps { deleteDownloadsSnackbar: UseBooleanReturnType; fetching: boolean; filter: string; + firstUnreadChapter?: ChapterInfo; isLoading: boolean; lastRead?: ChapterInfo; navigateToChapter: (chapter: ChapterInfo) => void; @@ -89,6 +90,7 @@ const NovelInfoHeader = ({ deleteDownloadsSnackbar, fetching, filter, + firstUnreadChapter, isLoading = false, lastRead, navigateToChapter, @@ -262,7 +264,7 @@ const NovelInfoHeader = ({ )} {isLoading && (!novel.genres || !novel.summary) ? ( diff --git a/src/screens/novel/components/Info/ReadButton.tsx b/src/screens/novel/components/Info/ReadButton.tsx index 2fdb8337fc..dfbc394df5 100644 --- a/src/screens/novel/components/Info/ReadButton.tsx +++ b/src/screens/novel/components/Info/ReadButton.tsx @@ -8,39 +8,39 @@ import Animated, { ZoomIn } from 'react-native-reanimated'; import { StyleSheet } from 'react-native'; interface ReadButtonProps { - chapters: ChapterInfo[]; + firstUnreadChapter?: ChapterInfo; lastRead?: ChapterInfo; navigateToChapter: (chapter: ChapterInfo) => void; } const ReadButton = ({ - chapters, + firstUnreadChapter, lastRead, navigateToChapter, }: ReadButtonProps) => { const { useFabForContinueReading = false } = useAppSettings(); - const navigateToLastReadChapter = () => { - if (lastRead) { - navigateToChapter(lastRead); - } else if (chapters.length) { - navigateToChapter(chapters[0]); + const targetChapter = lastRead ?? firstUnreadChapter; + + const navigateToTargetChapter = () => { + if (targetChapter) { + navigateToChapter(targetChapter); } }; if (!useFabForContinueReading) { - return chapters.length > 0 || lastRead ? ( + return targetChapter ? (