feat(library): search a book on Goodreads from the library and reader (#4543) (#4548)

Adds a quick "Search on Goodreads" action so readers can jump straight to
Goodreads to track a book instead of retyping the title there.

- Library: a Goodreads button in the Book Details view (works on web,
  desktop and mobile) searching the book's title + author, plus a
  "Search on Goodreads" item in the desktop right-click context menu.
- Reader: Goodreads is added as a built-in web-search provider so
  highlighted text (e.g. a short-story title inside a magazine) can be
  looked up on Goodreads. Disabled by default like the other built-ins;
  enable it in Settings -> Dictionaries.

Both surfaces are used because the native context menu is desktop-only;
the Book Details button covers web and mobile. Adds a shared
openExternalUrl() helper and translates "Search on Goodreads" across all
locales (the Goodreads brand name is kept verbatim).

Closes #4543

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin
2026-06-12 14:30:17 +08:00
committed by GitHub
parent cfe2bb9116
commit ceddee3793
45 changed files with 212 additions and 39 deletions
+17
View File
@@ -0,0 +1,17 @@
import { Book } from '@/types/book';
const GOODREADS_SEARCH_URL = 'https://www.goodreads.com/search';
/** Build a Goodreads search URL for an arbitrary query string. */
export const getGoodreadsSearchUrl = (query: string): string =>
`${GOODREADS_SEARCH_URL}?q=${encodeURIComponent(query.trim())}`;
/**
* Compose the Goodreads search query for a book from its title and author.
* The author improves match precision; it's dropped when empty.
*/
export const getBookGoodreadsQuery = (book: Pick<Book, 'title' | 'author'>): string =>
[book.title, book.author]
.map((part) => part?.trim())
.filter(Boolean)
.join(' ');
+14
View File
@@ -1,6 +1,20 @@
import { openUrl } from '@tauri-apps/plugin-opener';
import { isTauriAppPlatform } from '@/services/environment';
/**
* Open an external URL in the system browser. On Tauri the in-app webview
* ignores `target="_blank"`, so route through the opener plugin; on web fall
* back to `window.open`. Unlike `interceptWindowOpen`, this works without the
* reader-scoped `window.open` override being installed.
*/
export const openExternalUrl = (url: string) => {
if (isTauriAppPlatform()) {
void openUrl(url).catch((err) => console.warn('Failed to open external URL', url, err));
} else {
window.open(url, '_blank', 'noopener,noreferrer');
}
};
export const interceptWindowOpen = () => {
const windowOpen = window.open;
globalThis.open = function (