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/<hash>/. 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.
This commit is contained in:
loveheaven
2026-06-10 12:54:53 +08:00
committed by GitHub
parent d12e1ad087
commit 88d8aa285f
2 changed files with 39 additions and 1 deletions
@@ -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();
});
});
@@ -222,6 +222,21 @@ const BookDetailView: React.FC<BookDetailViewProps> = ({
{metadata?.identifier || _('Unknown')}
</p>
</div>
{/*
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/<hash>/ 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 && (
<div className='col-span-2 overflow-hidden sm:col-span-3'>
<span className='font-bold'>{_('File Path')}</span>
<p className='text-neutral-content text-sm break-all' title={book.filePath}>
{book.filePath}
</p>
</div>
)}
</div>
</div>
)}