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:
@@ -1839,5 +1839,6 @@
|
||||
"Next word (.)": "الكلمة التالية (.)",
|
||||
"Start Delay": "تأخير البدء",
|
||||
"Off": "إيقاف",
|
||||
"Look up": "بحث"
|
||||
"Look up": "بحث",
|
||||
"Search on Goodreads": "البحث في Goodreads"
|
||||
}
|
||||
|
||||
@@ -1707,5 +1707,6 @@
|
||||
"Next word (.)": "পরবর্তী শব্দ (.)",
|
||||
"Start Delay": "শুরুর বিলম্ব",
|
||||
"Off": "বন্ধ",
|
||||
"Look up": "খুঁজুন"
|
||||
"Look up": "খুঁজুন",
|
||||
"Search on Goodreads": "Goodreads-এ অনুসন্ধান করুন"
|
||||
}
|
||||
|
||||
@@ -1674,5 +1674,6 @@
|
||||
"Next word (.)": "ཚིག་རྗེས་མ། (.)",
|
||||
"Start Delay": "འགོ་འཛུགས་ཕྱིར་འགྱངས།",
|
||||
"Off": "ཁ་རྒྱག",
|
||||
"Look up": "འཚོལ་བ།"
|
||||
"Look up": "འཚོལ་བ།",
|
||||
"Search on Goodreads": "Goodreads ནང་འཚོལ།"
|
||||
}
|
||||
|
||||
@@ -1707,5 +1707,6 @@
|
||||
"Next word (.)": "Nächstes Wort (.)",
|
||||
"Start Delay": "Startverzögerung",
|
||||
"Off": "Aus",
|
||||
"Look up": "Nachschlagen"
|
||||
"Look up": "Nachschlagen",
|
||||
"Search on Goodreads": "Auf Goodreads suchen"
|
||||
}
|
||||
|
||||
@@ -1707,5 +1707,6 @@
|
||||
"Next word (.)": "Επόμενη λέξη (.)",
|
||||
"Start Delay": "Καθυστέρηση έναρξης",
|
||||
"Off": "Ανενεργό",
|
||||
"Look up": "Αναζήτηση"
|
||||
"Look up": "Αναζήτηση",
|
||||
"Search on Goodreads": "Αναζήτηση στο Goodreads"
|
||||
}
|
||||
|
||||
@@ -1740,5 +1740,6 @@
|
||||
"Next word (.)": "Palabra siguiente (.)",
|
||||
"Start Delay": "Retardo de inicio",
|
||||
"Off": "Desactivado",
|
||||
"Look up": "Buscar"
|
||||
"Look up": "Buscar",
|
||||
"Search on Goodreads": "Buscar en Goodreads"
|
||||
}
|
||||
|
||||
@@ -1707,5 +1707,6 @@
|
||||
"Next word (.)": "واژهٔ بعدی (.)",
|
||||
"Start Delay": "تأخیر شروع",
|
||||
"Off": "خاموش",
|
||||
"Look up": "جستجو"
|
||||
"Look up": "جستجو",
|
||||
"Search on Goodreads": "جستجو در Goodreads"
|
||||
}
|
||||
|
||||
@@ -1740,5 +1740,6 @@
|
||||
"Next word (.)": "Mot suivant (.)",
|
||||
"Start Delay": "Délai de démarrage",
|
||||
"Off": "Désactivé",
|
||||
"Look up": "Rechercher"
|
||||
"Look up": "Rechercher",
|
||||
"Search on Goodreads": "Rechercher sur Goodreads"
|
||||
}
|
||||
|
||||
@@ -1740,5 +1740,6 @@
|
||||
"Next word (.)": "מילה הבאה (.)",
|
||||
"Start Delay": "השהיית התחלה",
|
||||
"Off": "כבוי",
|
||||
"Look up": "חיפוש"
|
||||
"Look up": "חיפוש",
|
||||
"Search on Goodreads": "חיפוש ב-Goodreads"
|
||||
}
|
||||
|
||||
@@ -1707,5 +1707,6 @@
|
||||
"Next word (.)": "अगला शब्द (.)",
|
||||
"Start Delay": "आरंभ विलंब",
|
||||
"Off": "बंद",
|
||||
"Look up": "खोजें"
|
||||
"Look up": "खोजें",
|
||||
"Search on Goodreads": "Goodreads पर खोजें"
|
||||
}
|
||||
|
||||
@@ -1707,5 +1707,6 @@
|
||||
"Next word (.)": "Következő szó (.)",
|
||||
"Start Delay": "Indítási késleltetés",
|
||||
"Off": "Ki",
|
||||
"Look up": "Keresés"
|
||||
"Look up": "Keresés",
|
||||
"Search on Goodreads": "Keresés a Goodreadson"
|
||||
}
|
||||
|
||||
@@ -1674,5 +1674,6 @@
|
||||
"Next word (.)": "Kata berikutnya (.)",
|
||||
"Start Delay": "Tunda mulai",
|
||||
"Off": "Mati",
|
||||
"Look up": "Cari"
|
||||
"Look up": "Cari",
|
||||
"Search on Goodreads": "Cari di Goodreads"
|
||||
}
|
||||
|
||||
@@ -1740,5 +1740,6 @@
|
||||
"Next word (.)": "Parola successiva (.)",
|
||||
"Start Delay": "Ritardo di avvio",
|
||||
"Off": "Disattivato",
|
||||
"Look up": "Cerca"
|
||||
"Look up": "Cerca",
|
||||
"Search on Goodreads": "Cerca su Goodreads"
|
||||
}
|
||||
|
||||
@@ -1674,5 +1674,6 @@
|
||||
"Next word (.)": "次の単語 (.)",
|
||||
"Start Delay": "開始遅延",
|
||||
"Off": "オフ",
|
||||
"Look up": "調べる"
|
||||
"Look up": "調べる",
|
||||
"Search on Goodreads": "Goodreadsで検索"
|
||||
}
|
||||
|
||||
@@ -1674,5 +1674,6 @@
|
||||
"Next word (.)": "다음 단어 (.)",
|
||||
"Start Delay": "시작 지연",
|
||||
"Off": "끄기",
|
||||
"Look up": "찾기"
|
||||
"Look up": "찾기",
|
||||
"Search on Goodreads": "Goodreads에서 검색"
|
||||
}
|
||||
|
||||
@@ -1674,5 +1674,6 @@
|
||||
"Next word (.)": "Perkataan seterusnya (.)",
|
||||
"Start Delay": "Lengah mula",
|
||||
"Off": "Mati",
|
||||
"Look up": "Cari"
|
||||
"Look up": "Cari",
|
||||
"Search on Goodreads": "Cari di Goodreads"
|
||||
}
|
||||
|
||||
@@ -1707,5 +1707,6 @@
|
||||
"Next word (.)": "Volgend woord (.)",
|
||||
"Start Delay": "Startvertraging",
|
||||
"Off": "Uit",
|
||||
"Look up": "Opzoeken"
|
||||
"Look up": "Opzoeken",
|
||||
"Search on Goodreads": "Zoeken op Goodreads"
|
||||
}
|
||||
|
||||
@@ -1773,5 +1773,6 @@
|
||||
"Next word (.)": "Następne słowo (.)",
|
||||
"Start Delay": "Opóźnienie startu",
|
||||
"Off": "Wył.",
|
||||
"Look up": "Wyszukaj"
|
||||
"Look up": "Wyszukaj",
|
||||
"Search on Goodreads": "Szukaj w Goodreads"
|
||||
}
|
||||
|
||||
@@ -1740,5 +1740,6 @@
|
||||
"Next word (.)": "Próxima palavra (.)",
|
||||
"Start Delay": "Atraso de início",
|
||||
"Off": "Desligado",
|
||||
"Look up": "Pesquisar"
|
||||
"Look up": "Pesquisar",
|
||||
"Search on Goodreads": "Pesquisar no Goodreads"
|
||||
}
|
||||
|
||||
@@ -1740,5 +1740,6 @@
|
||||
"Next word (.)": "Próxima palavra (.)",
|
||||
"Start Delay": "Atraso de início",
|
||||
"Off": "Desligado",
|
||||
"Look up": "Pesquisar"
|
||||
"Look up": "Pesquisar",
|
||||
"Search on Goodreads": "Pesquisar no Goodreads"
|
||||
}
|
||||
|
||||
@@ -1740,5 +1740,6 @@
|
||||
"Next word (.)": "Cuvântul următor (.)",
|
||||
"Start Delay": "Întârziere la pornire",
|
||||
"Off": "Dezactivat",
|
||||
"Look up": "Caută"
|
||||
"Look up": "Caută",
|
||||
"Search on Goodreads": "Caută pe Goodreads"
|
||||
}
|
||||
|
||||
@@ -1773,5 +1773,6 @@
|
||||
"Next word (.)": "Следующее слово (.)",
|
||||
"Start Delay": "Задержка старта",
|
||||
"Off": "Выкл.",
|
||||
"Look up": "Найти"
|
||||
"Look up": "Найти",
|
||||
"Search on Goodreads": "Искать в Goodreads"
|
||||
}
|
||||
|
||||
@@ -1707,5 +1707,6 @@
|
||||
"Next word (.)": "ඊළඟ වචනය (.)",
|
||||
"Start Delay": "ආරම්භක ප්රමාදය",
|
||||
"Off": "අක්රිය",
|
||||
"Look up": "සොයන්න"
|
||||
"Look up": "සොයන්න",
|
||||
"Search on Goodreads": "Goodreads හි සොයන්න"
|
||||
}
|
||||
|
||||
@@ -1773,5 +1773,6 @@
|
||||
"Next word (.)": "Naslednja beseda (.)",
|
||||
"Start Delay": "Zakasnitev začetka",
|
||||
"Off": "Izklopljeno",
|
||||
"Look up": "Poišči"
|
||||
"Look up": "Poišči",
|
||||
"Search on Goodreads": "Išči v Goodreads"
|
||||
}
|
||||
|
||||
@@ -1707,5 +1707,6 @@
|
||||
"Next word (.)": "Nästa ord (.)",
|
||||
"Start Delay": "Startfördröjning",
|
||||
"Off": "Av",
|
||||
"Look up": "Slå upp"
|
||||
"Look up": "Slå upp",
|
||||
"Search on Goodreads": "Sök på Goodreads"
|
||||
}
|
||||
|
||||
@@ -1707,5 +1707,6 @@
|
||||
"Next word (.)": "அடுத்த சொல் (.)",
|
||||
"Start Delay": "தொடக்க தாமதம்",
|
||||
"Off": "அணை",
|
||||
"Look up": "தேடு"
|
||||
"Look up": "தேடு",
|
||||
"Search on Goodreads": "Goodreads இல் தேடு"
|
||||
}
|
||||
|
||||
@@ -1674,5 +1674,6 @@
|
||||
"Next word (.)": "คำถัดไป (.)",
|
||||
"Start Delay": "หน่วงเวลาเริ่มต้น",
|
||||
"Off": "ปิด",
|
||||
"Look up": "ค้นหา"
|
||||
"Look up": "ค้นหา",
|
||||
"Search on Goodreads": "ค้นหาใน Goodreads"
|
||||
}
|
||||
|
||||
@@ -1707,5 +1707,6 @@
|
||||
"Next word (.)": "Sonraki kelime (.)",
|
||||
"Start Delay": "Başlangıç gecikmesi",
|
||||
"Off": "Kapalı",
|
||||
"Look up": "Ara"
|
||||
"Look up": "Ara",
|
||||
"Search on Goodreads": "Goodreads'te ara"
|
||||
}
|
||||
|
||||
@@ -1773,5 +1773,6 @@
|
||||
"Next word (.)": "Наступне слово (.)",
|
||||
"Start Delay": "Затримка старту",
|
||||
"Off": "Вимк.",
|
||||
"Look up": "Знайти"
|
||||
"Look up": "Знайти",
|
||||
"Search on Goodreads": "Шукати в Goodreads"
|
||||
}
|
||||
|
||||
@@ -1707,5 +1707,6 @@
|
||||
"Next word (.)": "Keyingi soʻz (.)",
|
||||
"Start Delay": "Boshlash kechikishi",
|
||||
"Off": "Oʻchiq",
|
||||
"Look up": "Qidirish"
|
||||
"Look up": "Qidirish",
|
||||
"Search on Goodreads": "Goodreads'da qidirish"
|
||||
}
|
||||
|
||||
@@ -1674,5 +1674,6 @@
|
||||
"Next word (.)": "Từ tiếp theo (.)",
|
||||
"Start Delay": "Độ trễ bắt đầu",
|
||||
"Off": "Tắt",
|
||||
"Look up": "Tra cứu"
|
||||
"Look up": "Tra cứu",
|
||||
"Search on Goodreads": "Tìm trên Goodreads"
|
||||
}
|
||||
|
||||
@@ -1674,5 +1674,6 @@
|
||||
"Next word (.)": "下一个词 (.)",
|
||||
"Start Delay": "开始延迟",
|
||||
"Off": "关闭",
|
||||
"Look up": "查询"
|
||||
"Look up": "查询",
|
||||
"Search on Goodreads": "在 Goodreads 中搜索"
|
||||
}
|
||||
|
||||
@@ -1674,5 +1674,6 @@
|
||||
"Next word (.)": "下一個詞 (.)",
|
||||
"Start Delay": "開始延遲",
|
||||
"Off": "關閉",
|
||||
"Look up": "查詢"
|
||||
"Look up": "查詢",
|
||||
"Search on Goodreads": "在 Goodreads 搜尋"
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ describe('getBookContextMenuItemIds', () => {
|
||||
'markFinished',
|
||||
'showDetails',
|
||||
'showInFinder',
|
||||
'searchGoodreads',
|
||||
'upload',
|
||||
'share',
|
||||
'delete',
|
||||
@@ -37,6 +38,7 @@ describe('getBookContextMenuItemIds', () => {
|
||||
'clearStatus',
|
||||
'showDetails',
|
||||
'showInFinder',
|
||||
'searchGoodreads',
|
||||
'upload',
|
||||
'share',
|
||||
'delete',
|
||||
@@ -52,6 +54,7 @@ describe('getBookContextMenuItemIds', () => {
|
||||
'clearStatus',
|
||||
'showDetails',
|
||||
'showInFinder',
|
||||
'searchGoodreads',
|
||||
'upload',
|
||||
'share',
|
||||
'delete',
|
||||
@@ -66,6 +69,7 @@ describe('getBookContextMenuItemIds', () => {
|
||||
'markFinished',
|
||||
'showDetails',
|
||||
'showInFinder',
|
||||
'searchGoodreads',
|
||||
'download',
|
||||
'share',
|
||||
'delete',
|
||||
@@ -80,6 +84,7 @@ describe('getBookContextMenuItemIds', () => {
|
||||
'markFinished',
|
||||
'showDetails',
|
||||
'showInFinder',
|
||||
'searchGoodreads',
|
||||
'delete',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { BUILTIN_WEB_SEARCH_IDS } from '@/services/dictionaries/types';
|
||||
import {
|
||||
BUILTIN_WEB_SEARCHES,
|
||||
getBuiltinWebSearch,
|
||||
substituteUrlTemplate,
|
||||
} from '@/services/dictionaries/webSearchTemplates';
|
||||
|
||||
describe('Goodreads built-in web search', () => {
|
||||
it('is registered as a built-in template', () => {
|
||||
const tpl = getBuiltinWebSearch(BUILTIN_WEB_SEARCH_IDS.goodreads);
|
||||
expect(tpl).toBeDefined();
|
||||
expect(tpl?.name).toBe('Goodreads');
|
||||
});
|
||||
|
||||
it('is included in the built-in list', () => {
|
||||
const ids = BUILTIN_WEB_SEARCHES.map((t) => t.id);
|
||||
expect(ids).toContain(BUILTIN_WEB_SEARCH_IDS.goodreads);
|
||||
});
|
||||
|
||||
it('produces a Goodreads search URL when the word is substituted', () => {
|
||||
const tpl = getBuiltinWebSearch(BUILTIN_WEB_SEARCH_IDS.goodreads)!;
|
||||
expect(substituteUrlTemplate(tpl.urlTemplate, 'The Dispossessed')).toBe(
|
||||
'https://www.goodreads.com/search?q=The%20Dispossessed',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -652,6 +652,7 @@ describe('customDictionaryStore — loadCustomDictionaries reconciliation', () =
|
||||
'web:builtin:google',
|
||||
'web:builtin:urban',
|
||||
'web:builtin:merriam-webster',
|
||||
'web:builtin:goodreads',
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { getBookGoodreadsQuery, getGoodreadsSearchUrl } from '@/utils/goodreads';
|
||||
import { Book } from '@/types/book';
|
||||
|
||||
const createBook = (overrides: Partial<Book> = {}): Book => ({
|
||||
hash: 'hash-1',
|
||||
format: 'EPUB',
|
||||
title: 'The Left Hand of Darkness',
|
||||
author: 'Ursula K. Le Guin',
|
||||
createdAt: 0,
|
||||
updatedAt: 0,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('getGoodreadsSearchUrl', () => {
|
||||
it('builds a Goodreads search URL with the query percent-encoded', () => {
|
||||
expect(getGoodreadsSearchUrl('The Left Hand of Darkness')).toBe(
|
||||
'https://www.goodreads.com/search?q=The%20Left%20Hand%20of%20Darkness',
|
||||
);
|
||||
});
|
||||
|
||||
it('trims surrounding whitespace before encoding', () => {
|
||||
expect(getGoodreadsSearchUrl(' Dune ')).toBe('https://www.goodreads.com/search?q=Dune');
|
||||
});
|
||||
|
||||
it('encodes reserved characters so the query survives intact', () => {
|
||||
expect(getGoodreadsSearchUrl('Cat & Mouse')).toBe(
|
||||
'https://www.goodreads.com/search?q=Cat%20%26%20Mouse',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBookGoodreadsQuery', () => {
|
||||
it('combines title and author', () => {
|
||||
expect(getBookGoodreadsQuery(createBook())).toBe('The Left Hand of Darkness Ursula K. Le Guin');
|
||||
});
|
||||
|
||||
it('falls back to the title alone when the author is empty', () => {
|
||||
expect(getBookGoodreadsQuery(createBook({ author: '' }))).toBe('The Left Hand of Darkness');
|
||||
});
|
||||
|
||||
it('trims each part', () => {
|
||||
expect(getBookGoodreadsQuery(createBook({ title: ' Dune ', author: ' Herbert ' }))).toBe(
|
||||
'Dune Herbert',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,8 @@ import { useLongPress } from '@/hooks/useLongPress';
|
||||
import { Menu, type MenuItemOptions } from '@tauri-apps/api/menu';
|
||||
import { revealItemInDir } from '@tauri-apps/plugin-opener';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { openExternalUrl } from '@/utils/open';
|
||||
import { getBookGoodreadsQuery, getGoodreadsSearchUrl } from '@/utils/goodreads';
|
||||
import { getOSPlatform } from '@/utils/misc';
|
||||
import { throttle } from '@/utils/throttle';
|
||||
import { navigateToReader, showReaderWindow } from '@/utils/nav';
|
||||
@@ -271,6 +273,12 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
revealItemInDir(folder);
|
||||
},
|
||||
},
|
||||
searchGoodreads: {
|
||||
text: _('Search on Goodreads'),
|
||||
action: async () => {
|
||||
openExternalUrl(getGoodreadsSearchUrl(getBookGoodreadsQuery(book)));
|
||||
},
|
||||
},
|
||||
download: {
|
||||
text: _('Download Book'),
|
||||
action: async () => {
|
||||
|
||||
@@ -592,6 +592,7 @@ export type BookContextMenuItemId =
|
||||
| 'clearStatus'
|
||||
| 'showDetails'
|
||||
| 'showInFinder'
|
||||
| 'searchGoodreads'
|
||||
| 'download'
|
||||
| 'upload'
|
||||
| 'share'
|
||||
@@ -612,7 +613,7 @@ export const getBookContextMenuItemIds = (book: Book): BookContextMenuItemId[] =
|
||||
if (book.readingStatus === 'finished' || book.readingStatus === 'unread') {
|
||||
ids.push('clearStatus');
|
||||
}
|
||||
ids.push('showDetails', 'showInFinder');
|
||||
ids.push('showDetails', 'showInFinder', 'searchGoodreads');
|
||||
if (book.uploadedAt && !book.downloadedAt) ids.push('download');
|
||||
if (!book.uploadedAt && book.downloadedAt) ids.push('upload');
|
||||
// Share is offered for any local-or-uploaded book; the dialog uploads first
|
||||
|
||||
@@ -9,9 +9,12 @@ import {
|
||||
MdExpandMore,
|
||||
MdExpandLess,
|
||||
} from 'react-icons/md';
|
||||
import { FaGoodreads } from 'react-icons/fa';
|
||||
|
||||
import { Book } from '@/types/book';
|
||||
import { BookMetadata } from '@/libs/document';
|
||||
import { openExternalUrl } from '@/utils/open';
|
||||
import { getBookGoodreadsQuery, getGoodreadsSearchUrl } from '@/utils/goodreads';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
@@ -98,6 +101,12 @@ const BookDetailView: React.FC<BookDetailViewProps> = ({
|
||||
<MdOutlineEdit className='hover:fill-blue-500' />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => openExternalUrl(getGoodreadsSearchUrl(getBookGoodreadsQuery(book)))}
|
||||
title={_('Search on Goodreads')}
|
||||
>
|
||||
<FaGoodreads className='fill-base-content' />
|
||||
</button>
|
||||
{onDelete && (
|
||||
<Dropdown
|
||||
label={_('Delete Book Options')}
|
||||
|
||||
@@ -201,6 +201,7 @@ export const BUILTIN_WEB_SEARCH_IDS = {
|
||||
google: 'web:builtin:google',
|
||||
urban: 'web:builtin:urban',
|
||||
merriamWebster: 'web:builtin:merriam-webster',
|
||||
goodreads: 'web:builtin:goodreads',
|
||||
} as const;
|
||||
|
||||
export type BuiltinWebSearchId =
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* Built-in web-search templates.
|
||||
*
|
||||
* Three universally-useful entries (Google, Urban Dictionary,
|
||||
* Merriam-Webster). They are seeded into `providerOrder` but **disabled by
|
||||
* default** — users opt in from the settings list. Custom URL templates
|
||||
* live in `settings.webSearches`; this file is only the immutable
|
||||
* built-ins.
|
||||
* A few universally-useful entries (Google, Urban Dictionary,
|
||||
* Merriam-Webster, and Goodreads for looking up a selected title). They are
|
||||
* seeded into `providerOrder` but **disabled by default** — users opt in
|
||||
* from the settings list. Custom URL templates live in
|
||||
* `settings.webSearches`; this file is only the immutable built-ins.
|
||||
*
|
||||
* The `%WORD%` placeholder marks where the looked-up word goes; the
|
||||
* provider URL-encodes it at substitution time. (GoldenDict users may know
|
||||
@@ -38,6 +38,12 @@ export const BUILTIN_WEB_SEARCHES: BuiltinWebSearchTemplate[] = [
|
||||
nameKey: 'Merriam-Webster',
|
||||
urlTemplate: 'https://www.merriam-webster.com/dictionary/%WORD%',
|
||||
},
|
||||
{
|
||||
id: BUILTIN_WEB_SEARCH_IDS.goodreads,
|
||||
name: 'Goodreads',
|
||||
nameKey: 'Goodreads',
|
||||
urlTemplate: 'https://www.goodreads.com/search?q=%WORD%',
|
||||
},
|
||||
];
|
||||
|
||||
const BUILTIN_BY_ID = new Map(BUILTIN_WEB_SEARCHES.map((t) => [t.id, t]));
|
||||
|
||||
@@ -30,6 +30,7 @@ const BUILTIN_WEB_ORDER = [
|
||||
BUILTIN_WEB_SEARCH_IDS.google,
|
||||
BUILTIN_WEB_SEARCH_IDS.urban,
|
||||
BUILTIN_WEB_SEARCH_IDS.merriamWebster,
|
||||
BUILTIN_WEB_SEARCH_IDS.goodreads,
|
||||
];
|
||||
|
||||
const DEFAULT_DICTIONARY_SETTINGS: DictionarySettings = {
|
||||
@@ -49,6 +50,7 @@ const DEFAULT_DICTIONARY_SETTINGS: DictionarySettings = {
|
||||
[BUILTIN_WEB_SEARCH_IDS.google]: false,
|
||||
[BUILTIN_WEB_SEARCH_IDS.urban]: false,
|
||||
[BUILTIN_WEB_SEARCH_IDS.merriamWebster]: false,
|
||||
[BUILTIN_WEB_SEARCH_IDS.goodreads]: false,
|
||||
},
|
||||
webSearches: [],
|
||||
};
|
||||
|
||||
@@ -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(' ');
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user