From 88d8aa285f4e6518238ad60200cff899447f1b63 Mon Sep 17 00:00:00 2001 From: loveheaven Date: Wed, 10 Jun 2026 12:54:53 +0800 Subject: [PATCH] feat(metadata): show file path for in-place imported books (#4508) In-place imports point at a file the user keeps under one of their external library folders (book.filePath set), as opposed to hash-copy imports that live anonymously under Books//. The book details view didn't surface where an entry actually lives on disk, so users had no way to tell the two storage modes apart or locate the source file. Add a 'File Path' row to the metadata grid that renders only when book.filePath is set, breaks long paths across lines, and exposes the full string via a hover title for paths that overflow the row. --- .../components/BookDetailView.test.tsx | 25 ++++++++++++++++++- .../components/metadata/BookDetailView.tsx | 15 +++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/apps/readest-app/src/__tests__/components/BookDetailView.test.tsx b/apps/readest-app/src/__tests__/components/BookDetailView.test.tsx index da19e583..cedce91e 100644 --- a/apps/readest-app/src/__tests__/components/BookDetailView.test.tsx +++ b/apps/readest-app/src/__tests__/components/BookDetailView.test.tsx @@ -13,7 +13,9 @@ vi.mock('@/store/settingsStore', () => ({ useSettingsStore: () => ({ settings: { metadataSeriesCollapsed: true, - metadataOthersCollapsed: true, + // The "File Path" entry lives under the Metadata section; tests below + // depend on it being expanded by default so the row is in the DOM. + metadataOthersCollapsed: false, metadataDescriptionCollapsed: true, }, }), @@ -100,3 +102,24 @@ describe('BookDetailView delete dropdown layout', () => { expect(menu!.className).toContain('!relative'); }); }); + +describe('BookDetailView file path row', () => { + // book.filePath is only set for in-place imports (and OS-handed paths like + // Android "Open with Readest"). Hash-copy imports leave it undefined, so + // surfacing it lets users tell the two storage modes apart at a glance. + it('shows the actual file path when book.filePath is set', () => { + const filePath = '/Users/me/Library/Books/sample.epub'; + const { getByText } = renderView({ book: makeBook({ filePath }) }); + + expect(getByText('File Path')).toBeTruthy(); + const value = getByText(filePath); + expect(value).toBeTruthy(); + // Long paths must remain hoverable for the full string. + expect(value.getAttribute('title')).toBe(filePath); + }); + + it('omits the file path row for hash-copy books (no filePath)', () => { + const { queryByText } = renderView({ book: makeBook() }); + expect(queryByText('File Path')).toBeNull(); + }); +}); diff --git a/apps/readest-app/src/components/metadata/BookDetailView.tsx b/apps/readest-app/src/components/metadata/BookDetailView.tsx index e26c9cab..87062dae 100644 --- a/apps/readest-app/src/components/metadata/BookDetailView.tsx +++ b/apps/readest-app/src/components/metadata/BookDetailView.tsx @@ -222,6 +222,21 @@ const BookDetailView: React.FC = ({ {metadata?.identifier || _('Unknown')}

+ {/* + Only books imported in-place (or files opened directly via the + OS, e.g. Android "Open with Readest") keep a `filePath`; books + copied into Books// have it left undefined. Surfacing the + path lets the user verify which on-disk file the entry points at + and tell apart in-place vs hash-copy imports at a glance. + */} + {book.filePath && ( +
+ {_('File Path')} +

+ {book.filePath} +

+
+ )} )}