Skip to content

feat(mobile): 補齊閱讀頁書籤/目錄/資訊面板,修正封面路徑持久化與圖示風格#4

Merged
Retsomm merged 2 commits into
mainfrom
dev
Jul 5, 2026
Merged

feat(mobile): 補齊閱讀頁書籤/目錄/資訊面板,修正封面路徑持久化與圖示風格#4
Retsomm merged 2 commits into
mainfrom
dev

Conversation

@Retsomm

@Retsomm Retsomm commented Jul 5, 2026

Copy link
Copy Markdown
Owner
  • 頂部工具列新增書籤按鈕與清單面板(書籤/目錄/資訊/註記四分頁),皆比照網頁版對應功能
  • 全面改用 react-native-svg 照抄網頁版線條圖示,取代原本的 Unicode 符號/emoji
  • 修正封面圖用絕對路徑持久化、App 重新安裝後失效的問題,改存檔名現算路徑
  • 設定/清單面板改成點擊圖示開關的方式,移除多餘的關閉鈕與麵包屑列

Summary by CodeRabbit

  • New Features

    • Added in-reader navigation with bookmarks, chapter list, book info, and notes panels.
    • Added tab bar icons and refreshed toolbar controls for faster access.
    • Added copy-book-title and text-to-speech controls with icon-based buttons.
  • Bug Fixes

    • Improved bookmark and chapter navigation behavior, including direct jumps to selected locations.
    • Fixed cover artwork handling so saved covers continue to load correctly after reinstall or app updates.
    • Enhanced chapter labeling and reading position updates for more accurate progress display.

- 頂部工具列新增書籤按鈕與清單面板(書籤/目錄/資訊/註記四分頁),皆比照網頁版對應功能
- 全面改用 react-native-svg 照抄網頁版線條圖示,取代原本的 Unicode 符號/emoji
- 修正封面圖用絕對路徑持久化、App 重新安裝後失效的問題,改存檔名現算路徑
- 設定/清單面板改成點擊圖示開關的方式,移除多餘的關閉鈕與麵包屑列

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
travel-in-time Ready Ready Preview, Comment Jul 5, 2026 3:17am

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Retsomm, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 37 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6a97213a-24d1-47f4-8271-2ca94d4aa1c9

📥 Commits

Reviewing files that changed from the base of the PR and between 2a8270d and 443a878.

📒 Files selected for processing (4)
  • mobile/components/BookCard.tsx
  • mobile/components/ListPanel.tsx
  • mobile/components/icons.tsx
  • mobile/lib/library.ts
📝 Walkthrough

Walkthrough

Adds bookmark and table-of-contents navigation to the mobile reader, including a new ListPanel component, WebView message protocol extensions (goto, tocLoaded, chapter-title fields), SVG-based icon components replacing text glyphs, and a cover-image persistence fix switching from absolute URIs to filenames.

Changes

Bookmarks, TOC, and Icons Feature

Layer / File(s) Summary
Message protocol contract
mobile/lib/readerMessages.ts
Adds TocItem interface and extends InboundMessage/OutboundMessage with goto, tocLoaded, and enriched relocated (href, chapterTitle) variants.
reader-web TOC/chapter-title and goto handling
mobile/reader-web/index.ts
Builds TOC/spine caches, computes chapter labels, emits tocLoaded, enriches relocated payload, and handles inbound goto by calling rendition.display.
SVG icon components
mobile/components/icons.tsx
New module exporting reusable icon components (IconBack, IconBook, IconBookmarkOutline, etc.) built with react-native-svg, plus the expo-clipboard/react-native-svg package additions.
Reader screen state and header UI
mobile/app/reader/[id].tsx
Adds bookmark/TOC/current-location state, loads persisted bookmarks, handles relocated/tocLoaded messages, adds toggle/delete/navigate handlers, and replaces header controls with icon-based Pressables; removes SettingsPanel onClose prop.
ListPanel component
mobile/components/ListPanel.tsx
New component rendering bookmarks/chapters/bookinfo/notes tabs with delete confirmation, TOC rendering, cover/progress display, and copy-title action.
Tab bar and settings panel icon wiring
mobile/app/(tabs)/_layout.tsx, mobile/components/SettingsPanel.tsx
Wires new icons into tab bar screens and settings panel TTS controls; removes settings panel close button.
Cover filename persistence fix
mobile/lib/library.ts, mobile/components/BookCard.tsx, mobile/app/(tabs)/index.tsx
Adds coverFilename field to BookRecord, updates cover save/read/delete logic to use filenames with dynamic path resolution, exports generateId, and updates consumers to use getCoverUri.
Progress documentation
RN_SETUP_GUIDE.md
Updates progress tracker marking TOC/bookmark/panel work complete and records fixes for panel toggling, cover persistence, and icon-based panel toggles.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ReaderScreen as Reader Screen
  participant WebView as reader-web (WebView)
  participant ListPanel

  User->>ReaderScreen: Tap list panel icon
  ReaderScreen->>ListPanel: Render with bookmarks, toc, currentHref
  User->>ListPanel: Select chapter/bookmark
  ListPanel->>ReaderScreen: onNavigateChapter/onNavigateBookmark
  ReaderScreen->>WebView: postMessage goto(target)
  WebView->>WebView: rendition.display(target)
  WebView->>ReaderScreen: relocated(href, chapterTitle, cfi)
  ReaderScreen->>ReaderScreen: update currentCfi/currentHref/currentChapterTitle
  ReaderScreen->>ReaderScreen: persist CFI/progress
Loading
sequenceDiagram
  participant WebView as reader-web (WebView)
  participant Book as epub.js Book
  participant ReaderScreen as Reader Screen

  WebView->>Book: load book.navigation.toc, book.spine.items
  WebView->>WebView: build tocCache, spineHrefs
  WebView->>ReaderScreen: postMessage tocLoaded(toc)
  ReaderScreen->>ReaderScreen: setToc(toc)
Loading

Possibly related PRs

  • Retsomm/TravelInTime#1: Builds on the same baseline reader-web WebView message protocol and mobile/lib/library.ts cover/metadata implementation extended here.
  • Retsomm/TravelInTime#2: Extends the existing extractor/message protocol and coverUricoverFilename metadata patching introduced there, layering TOC/goto features on the same messaging path.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 標題準確概括了新增閱讀頁書籤/目錄/資訊面板、封面持久化修正與圖示風格調整。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
mobile/components/icons.tsx (1)

11-105: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting a shared SVG wrapper to reduce duplication.

Every icon repeats the same <Svg width={size} height={size} viewBox="0 0 24 24" fill=... stroke=... strokeWidth=... strokeLinecap=... strokeLinejoin=...> boilerplate. A small shared wrapper component taking children and the common props would remove this repetition across all ~13 icons and centralize future style tweaks.

♻️ Example wrapper extraction
+const IconBase = ({ size = 18, color = '`#000`', filled = false, strokeWidth = 1.8, children }: IconProps & { filled?: boolean; strokeWidth?: number; children: React.ReactNode }) => (
+  <Svg width={size} height={size} viewBox="0 0 24 24" fill={filled ? color : 'none'} stroke={color} strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round">
+    {children}
+  </Svg>
+);
+
-export const IconBack = ({ size = 18, color = '`#000`' }: IconProps) => (
-  <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round">
-    <Polyline points="15 18 9 12 15 6" />
-  </Svg>
-);
+export const IconBack = (props: IconProps) => (
+  <IconBase {...props}>
+    <Polyline points="15 18 9 12 15 6" />
+  </IconBase>
+);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mobile/components/icons.tsx` around lines 11 - 105, The icon components in
IconBack, IconClose, IconSettings, and the other exports all repeat the same Svg
boilerplate, so extract a shared wrapper component for the common width, height,
viewBox, fill, stroke, strokeWidth, strokeLinecap, and strokeLinejoin props.
Update the individual icon functions to render only their shape children through
that wrapper, keeping size and color as inputs so future style changes are
centralized and the repeated JSX is removed.
mobile/lib/library.ts (3)

154-162: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Orphaned cover file if extension changes across saves for the same book id.

saveCoverImage only deletes/overwrites the file at the newly computed ${id}.${coverExtension(mediaType)} path. If a later save for the same id resolves to a different extension than a prior save, the previous cover file under the old extension is never deleted, leaking storage over time.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mobile/lib/library.ts` around lines 154 - 162, The saveCoverImage helper
leaves behind the previous cover file when the computed extension changes for
the same book id. Update saveCoverImage to first clean up any existing cover
files for that id in coversDir(), not just the newly named File, then
recreate/write the current one so only one cover remains per id regardless of
coverExtension(mediaType).

132-134: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate cover-filename derivation logic.

The record.coverFilename ?? record.coverUri?.split('/').pop() fallback is duplicated in removeBook and getCoverUri. If this derivation logic changes in one place but not the other, removeBook could fail to delete the correct cover file (or delete the wrong one) while getCoverUri still resolves it differently, silently leaking files or breaking display consistency.

♻️ Extract a shared helper
+const resolveCoverFilename = (record: Pick<BookRecord, 'coverFilename' | 'coverUri'>): string | undefined =>
+  record.coverFilename ?? record.coverUri?.split('/').pop();
+
 export const removeBook = (id: string) =>
   updateMeta((records) => {
     const record = records.find((r) => r.id === id);
     if (record) {
       const file = new File(booksDir(), record.filename);
       if (file.exists) file.delete();
-      const coverFilename = record.coverFilename ?? record.coverUri?.split('/').pop();
+      const coverFilename = resolveCoverFilename(record);
       if (coverFilename) {
         const cover = new File(coversDir(), coverFilename);
         if (cover.exists) cover.delete();
       }
     }
     ...
   });

 export const getCoverUri = (record: BookRecord): string | null => {
-  const filename = record.coverFilename ?? record.coverUri?.split('/').pop();
+  const filename = resolveCoverFilename(record);
   if (!filename) return null;
   const file = new File(coversDir(), filename);
   return file.exists ? file.uri : null;
 };

Also applies to: 167-172

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mobile/lib/library.ts` around lines 132 - 134, The cover filename fallback is
duplicated between removeBook and getCoverUri in library.ts, so extract the
shared derivation into a single helper and use it from both places. Create a
helper near the existing record handling logic that returns the resolved cover
filename from record.coverFilename or record.coverUri, then update removeBook
and getCoverUri to call that helper so both paths stay consistent if the logic
changes.

167-172: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

getCoverUri performs a synchronous FS existence check on every call, including in render paths.

File.exists triggers a synchronous native call per invocation. getCoverUri(record) is invoked directly in BookCard's and ListPanel's render bodies (see relevant snippets), meaning every re-render of every visible book card/bookinfo tab does a blocking disk stat, instead of the previous zero-cost record.coverUri field read. This is a regression in render-path cost, most noticeable in the library grid FlatList.

Consider memoizing the result in the consuming components (e.g., useMemo(() => getCoverUri(record), [record.coverFilename, record.coverUri])) so the stat only re-runs when the cover reference actually changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mobile/lib/library.ts` around lines 167 - 172, The issue is that getCoverUri
does a synchronous File.exists check on every render-path call, which is too
expensive in BookCard and ListPanel. Update the consuming components to memoize
getCoverUri(record) with useMemo keyed by record.coverFilename and
record.coverUri so the native stat only runs when the cover reference changes.
Keep getCoverUri as the lookup helper, but avoid calling it directly inside
render bodies.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@mobile/components/icons.tsx`:
- Around line 11-105: The icon components in IconBack, IconClose, IconSettings,
and the other exports all repeat the same Svg boilerplate, so extract a shared
wrapper component for the common width, height, viewBox, fill, stroke,
strokeWidth, strokeLinecap, and strokeLinejoin props. Update the individual icon
functions to render only their shape children through that wrapper, keeping size
and color as inputs so future style changes are centralized and the repeated JSX
is removed.

In `@mobile/lib/library.ts`:
- Around line 154-162: The saveCoverImage helper leaves behind the previous
cover file when the computed extension changes for the same book id. Update
saveCoverImage to first clean up any existing cover files for that id in
coversDir(), not just the newly named File, then recreate/write the current one
so only one cover remains per id regardless of coverExtension(mediaType).
- Around line 132-134: The cover filename fallback is duplicated between
removeBook and getCoverUri in library.ts, so extract the shared derivation into
a single helper and use it from both places. Create a helper near the existing
record handling logic that returns the resolved cover filename from
record.coverFilename or record.coverUri, then update removeBook and getCoverUri
to call that helper so both paths stay consistent if the logic changes.
- Around line 167-172: The issue is that getCoverUri does a synchronous
File.exists check on every render-path call, which is too expensive in BookCard
and ListPanel. Update the consuming components to memoize getCoverUri(record)
with useMemo keyed by record.coverFilename and record.coverUri so the native
stat only runs when the cover reference changes. Keep getCoverUri as the lookup
helper, but avoid calling it directly inside render bodies.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 893c7bf0-bc77-416b-8fd7-6518bd58a275

📥 Commits

Reviewing files that changed from the base of the PR and between a867fd3 and 2a8270d.

⛔ Files ignored due to path filters (1)
  • mobile/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (13)
  • RN_SETUP_GUIDE.md
  • mobile/app/(tabs)/_layout.tsx
  • mobile/app/(tabs)/index.tsx
  • mobile/app/reader/[id].tsx
  • mobile/components/BookCard.tsx
  • mobile/components/ListPanel.tsx
  • mobile/components/SettingsPanel.tsx
  • mobile/components/icons.tsx
  • mobile/lib/library.ts
  • mobile/lib/readerHtml.generated.ts
  • mobile/lib/readerMessages.ts
  • mobile/package.json
  • mobile/reader-web/index.ts

- saveCoverImage 換副檔名時清除舊封面檔,避免留下孤兒檔案
- 抽出共用的封面檔名回退邏輯,供 removeBook / getCoverUri 共用
- BookCard / ListPanel 用 useMemo 快取 getCoverUri,避免每次 render 都做檔案系統檢查
- icons.tsx 抽出共用 Svg 外殼元件,減少重複屬性

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Retsomm Retsomm merged commit 9011a20 into main Jul 5, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant