diff --git a/apps/readest-app/public/locales/ar/translation.json b/apps/readest-app/public/locales/ar/translation.json index c8e52778..e6f3a4b2 100644 --- a/apps/readest-app/public/locales/ar/translation.json +++ b/apps/readest-app/public/locales/ar/translation.json @@ -1839,5 +1839,6 @@ "Next word (.)": "الكلمة التالية (.)", "Start Delay": "تأخير البدء", "Off": "إيقاف", - "Look up": "بحث" + "Look up": "بحث", + "Search on Goodreads": "البحث في Goodreads" } diff --git a/apps/readest-app/public/locales/bn/translation.json b/apps/readest-app/public/locales/bn/translation.json index d8d39c72..218ab958 100644 --- a/apps/readest-app/public/locales/bn/translation.json +++ b/apps/readest-app/public/locales/bn/translation.json @@ -1707,5 +1707,6 @@ "Next word (.)": "পরবর্তী শব্দ (.)", "Start Delay": "শুরুর বিলম্ব", "Off": "বন্ধ", - "Look up": "খুঁজুন" + "Look up": "খুঁজুন", + "Search on Goodreads": "Goodreads-এ অনুসন্ধান করুন" } diff --git a/apps/readest-app/public/locales/bo/translation.json b/apps/readest-app/public/locales/bo/translation.json index e5658bac..826586b5 100644 --- a/apps/readest-app/public/locales/bo/translation.json +++ b/apps/readest-app/public/locales/bo/translation.json @@ -1674,5 +1674,6 @@ "Next word (.)": "ཚིག་རྗེས་མ། (.)", "Start Delay": "འགོ་འཛུགས་ཕྱིར་འགྱངས།", "Off": "ཁ་རྒྱག", - "Look up": "འཚོལ་བ།" + "Look up": "འཚོལ་བ།", + "Search on Goodreads": "Goodreads ནང་འཚོལ།" } diff --git a/apps/readest-app/public/locales/de/translation.json b/apps/readest-app/public/locales/de/translation.json index 727e4352..2a074691 100644 --- a/apps/readest-app/public/locales/de/translation.json +++ b/apps/readest-app/public/locales/de/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/el/translation.json b/apps/readest-app/public/locales/el/translation.json index 458d737f..d9816ce5 100644 --- a/apps/readest-app/public/locales/el/translation.json +++ b/apps/readest-app/public/locales/el/translation.json @@ -1707,5 +1707,6 @@ "Next word (.)": "Επόμενη λέξη (.)", "Start Delay": "Καθυστέρηση έναρξης", "Off": "Ανενεργό", - "Look up": "Αναζήτηση" + "Look up": "Αναζήτηση", + "Search on Goodreads": "Αναζήτηση στο Goodreads" } diff --git a/apps/readest-app/public/locales/es/translation.json b/apps/readest-app/public/locales/es/translation.json index 5fac2599..23d279ac 100644 --- a/apps/readest-app/public/locales/es/translation.json +++ b/apps/readest-app/public/locales/es/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/fa/translation.json b/apps/readest-app/public/locales/fa/translation.json index c03da60f..2dc03215 100644 --- a/apps/readest-app/public/locales/fa/translation.json +++ b/apps/readest-app/public/locales/fa/translation.json @@ -1707,5 +1707,6 @@ "Next word (.)": "واژهٔ بعدی (.)", "Start Delay": "تأخیر شروع", "Off": "خاموش", - "Look up": "جستجو" + "Look up": "جستجو", + "Search on Goodreads": "جستجو در Goodreads" } diff --git a/apps/readest-app/public/locales/fr/translation.json b/apps/readest-app/public/locales/fr/translation.json index 31a08c1e..80d57bab 100644 --- a/apps/readest-app/public/locales/fr/translation.json +++ b/apps/readest-app/public/locales/fr/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/he/translation.json b/apps/readest-app/public/locales/he/translation.json index 9fb0abec..8ab25085 100644 --- a/apps/readest-app/public/locales/he/translation.json +++ b/apps/readest-app/public/locales/he/translation.json @@ -1740,5 +1740,6 @@ "Next word (.)": "מילה הבאה (.)", "Start Delay": "השהיית התחלה", "Off": "כבוי", - "Look up": "חיפוש" + "Look up": "חיפוש", + "Search on Goodreads": "חיפוש ב-Goodreads" } diff --git a/apps/readest-app/public/locales/hi/translation.json b/apps/readest-app/public/locales/hi/translation.json index 93abb797..c410724a 100644 --- a/apps/readest-app/public/locales/hi/translation.json +++ b/apps/readest-app/public/locales/hi/translation.json @@ -1707,5 +1707,6 @@ "Next word (.)": "अगला शब्द (.)", "Start Delay": "आरंभ विलंब", "Off": "बंद", - "Look up": "खोजें" + "Look up": "खोजें", + "Search on Goodreads": "Goodreads पर खोजें" } diff --git a/apps/readest-app/public/locales/hu/translation.json b/apps/readest-app/public/locales/hu/translation.json index 710a0351..c3a0480f 100644 --- a/apps/readest-app/public/locales/hu/translation.json +++ b/apps/readest-app/public/locales/hu/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/id/translation.json b/apps/readest-app/public/locales/id/translation.json index 914252a6..0eaa1683 100644 --- a/apps/readest-app/public/locales/id/translation.json +++ b/apps/readest-app/public/locales/id/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/it/translation.json b/apps/readest-app/public/locales/it/translation.json index c820c8cc..ba23e8c2 100644 --- a/apps/readest-app/public/locales/it/translation.json +++ b/apps/readest-app/public/locales/it/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/ja/translation.json b/apps/readest-app/public/locales/ja/translation.json index 54a2e304..0736d487 100644 --- a/apps/readest-app/public/locales/ja/translation.json +++ b/apps/readest-app/public/locales/ja/translation.json @@ -1674,5 +1674,6 @@ "Next word (.)": "次の単語 (.)", "Start Delay": "開始遅延", "Off": "オフ", - "Look up": "調べる" + "Look up": "調べる", + "Search on Goodreads": "Goodreadsで検索" } diff --git a/apps/readest-app/public/locales/ko/translation.json b/apps/readest-app/public/locales/ko/translation.json index 43cb4a29..4b28ef5c 100644 --- a/apps/readest-app/public/locales/ko/translation.json +++ b/apps/readest-app/public/locales/ko/translation.json @@ -1674,5 +1674,6 @@ "Next word (.)": "다음 단어 (.)", "Start Delay": "시작 지연", "Off": "끄기", - "Look up": "찾기" + "Look up": "찾기", + "Search on Goodreads": "Goodreads에서 검색" } diff --git a/apps/readest-app/public/locales/ms/translation.json b/apps/readest-app/public/locales/ms/translation.json index 29431cc5..aa698879 100644 --- a/apps/readest-app/public/locales/ms/translation.json +++ b/apps/readest-app/public/locales/ms/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/nl/translation.json b/apps/readest-app/public/locales/nl/translation.json index 901a8929..74a17fe3 100644 --- a/apps/readest-app/public/locales/nl/translation.json +++ b/apps/readest-app/public/locales/nl/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/pl/translation.json b/apps/readest-app/public/locales/pl/translation.json index aaae873b..633f69d6 100644 --- a/apps/readest-app/public/locales/pl/translation.json +++ b/apps/readest-app/public/locales/pl/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/pt-BR/translation.json b/apps/readest-app/public/locales/pt-BR/translation.json index f6f53d1c..5686f0d6 100644 --- a/apps/readest-app/public/locales/pt-BR/translation.json +++ b/apps/readest-app/public/locales/pt-BR/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/pt/translation.json b/apps/readest-app/public/locales/pt/translation.json index 41b5efdd..fd452a78 100644 --- a/apps/readest-app/public/locales/pt/translation.json +++ b/apps/readest-app/public/locales/pt/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/ro/translation.json b/apps/readest-app/public/locales/ro/translation.json index 03e308d2..ccb0231d 100644 --- a/apps/readest-app/public/locales/ro/translation.json +++ b/apps/readest-app/public/locales/ro/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/ru/translation.json b/apps/readest-app/public/locales/ru/translation.json index 1d1af1b4..56ed7c0f 100644 --- a/apps/readest-app/public/locales/ru/translation.json +++ b/apps/readest-app/public/locales/ru/translation.json @@ -1773,5 +1773,6 @@ "Next word (.)": "Следующее слово (.)", "Start Delay": "Задержка старта", "Off": "Выкл.", - "Look up": "Найти" + "Look up": "Найти", + "Search on Goodreads": "Искать в Goodreads" } diff --git a/apps/readest-app/public/locales/si/translation.json b/apps/readest-app/public/locales/si/translation.json index c229fa79..f01ee7af 100644 --- a/apps/readest-app/public/locales/si/translation.json +++ b/apps/readest-app/public/locales/si/translation.json @@ -1707,5 +1707,6 @@ "Next word (.)": "ඊළඟ වචනය (.)", "Start Delay": "ආරම්භක ප්‍රමාදය", "Off": "අක්‍රිය", - "Look up": "සොයන්න" + "Look up": "සොයන්න", + "Search on Goodreads": "Goodreads හි සොයන්න" } diff --git a/apps/readest-app/public/locales/sl/translation.json b/apps/readest-app/public/locales/sl/translation.json index 4d7ba101..44c86c11 100644 --- a/apps/readest-app/public/locales/sl/translation.json +++ b/apps/readest-app/public/locales/sl/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/sv/translation.json b/apps/readest-app/public/locales/sv/translation.json index 9e37cfbb..9b4921b7 100644 --- a/apps/readest-app/public/locales/sv/translation.json +++ b/apps/readest-app/public/locales/sv/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/ta/translation.json b/apps/readest-app/public/locales/ta/translation.json index e1c64414..a232099e 100644 --- a/apps/readest-app/public/locales/ta/translation.json +++ b/apps/readest-app/public/locales/ta/translation.json @@ -1707,5 +1707,6 @@ "Next word (.)": "அடுத்த சொல் (.)", "Start Delay": "தொடக்க தாமதம்", "Off": "அணை", - "Look up": "தேடு" + "Look up": "தேடு", + "Search on Goodreads": "Goodreads இல் தேடு" } diff --git a/apps/readest-app/public/locales/th/translation.json b/apps/readest-app/public/locales/th/translation.json index 8591878b..d14574c1 100644 --- a/apps/readest-app/public/locales/th/translation.json +++ b/apps/readest-app/public/locales/th/translation.json @@ -1674,5 +1674,6 @@ "Next word (.)": "คำถัดไป (.)", "Start Delay": "หน่วงเวลาเริ่มต้น", "Off": "ปิด", - "Look up": "ค้นหา" + "Look up": "ค้นหา", + "Search on Goodreads": "ค้นหาใน Goodreads" } diff --git a/apps/readest-app/public/locales/tr/translation.json b/apps/readest-app/public/locales/tr/translation.json index 1bd89ff6..8abd8644 100644 --- a/apps/readest-app/public/locales/tr/translation.json +++ b/apps/readest-app/public/locales/tr/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/uk/translation.json b/apps/readest-app/public/locales/uk/translation.json index 83e1214b..ffa9844e 100644 --- a/apps/readest-app/public/locales/uk/translation.json +++ b/apps/readest-app/public/locales/uk/translation.json @@ -1773,5 +1773,6 @@ "Next word (.)": "Наступне слово (.)", "Start Delay": "Затримка старту", "Off": "Вимк.", - "Look up": "Знайти" + "Look up": "Знайти", + "Search on Goodreads": "Шукати в Goodreads" } diff --git a/apps/readest-app/public/locales/uz/translation.json b/apps/readest-app/public/locales/uz/translation.json index 55069f19..b6fd69f3 100644 --- a/apps/readest-app/public/locales/uz/translation.json +++ b/apps/readest-app/public/locales/uz/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/vi/translation.json b/apps/readest-app/public/locales/vi/translation.json index e59c387b..11f44a35 100644 --- a/apps/readest-app/public/locales/vi/translation.json +++ b/apps/readest-app/public/locales/vi/translation.json @@ -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" } diff --git a/apps/readest-app/public/locales/zh-CN/translation.json b/apps/readest-app/public/locales/zh-CN/translation.json index d33bdde6..7986b176 100644 --- a/apps/readest-app/public/locales/zh-CN/translation.json +++ b/apps/readest-app/public/locales/zh-CN/translation.json @@ -1674,5 +1674,6 @@ "Next word (.)": "下一个词 (.)", "Start Delay": "开始延迟", "Off": "关闭", - "Look up": "查询" + "Look up": "查询", + "Search on Goodreads": "在 Goodreads 中搜索" } diff --git a/apps/readest-app/public/locales/zh-TW/translation.json b/apps/readest-app/public/locales/zh-TW/translation.json index ed4a5ae0..19da7abd 100644 --- a/apps/readest-app/public/locales/zh-TW/translation.json +++ b/apps/readest-app/public/locales/zh-TW/translation.json @@ -1674,5 +1674,6 @@ "Next word (.)": "下一個詞 (.)", "Start Delay": "開始延遲", "Off": "關閉", - "Look up": "查詢" + "Look up": "查詢", + "Search on Goodreads": "在 Goodreads 搜尋" } diff --git a/apps/readest-app/src/__tests__/app/library/book-context-menu.test.ts b/apps/readest-app/src/__tests__/app/library/book-context-menu.test.ts index 044bf04d..9bffe526 100644 --- a/apps/readest-app/src/__tests__/app/library/book-context-menu.test.ts +++ b/apps/readest-app/src/__tests__/app/library/book-context-menu.test.ts @@ -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', ]); }); diff --git a/apps/readest-app/src/__tests__/services/dictionaries/webSearchTemplates.test.ts b/apps/readest-app/src/__tests__/services/dictionaries/webSearchTemplates.test.ts new file mode 100644 index 00000000..e6f1806f --- /dev/null +++ b/apps/readest-app/src/__tests__/services/dictionaries/webSearchTemplates.test.ts @@ -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', + ); + }); +}); diff --git a/apps/readest-app/src/__tests__/store/custom-dictionary-store.test.ts b/apps/readest-app/src/__tests__/store/custom-dictionary-store.test.ts index f8f1d751..38975846 100644 --- a/apps/readest-app/src/__tests__/store/custom-dictionary-store.test.ts +++ b/apps/readest-app/src/__tests__/store/custom-dictionary-store.test.ts @@ -652,6 +652,7 @@ describe('customDictionaryStore — loadCustomDictionaries reconciliation', () = 'web:builtin:google', 'web:builtin:urban', 'web:builtin:merriam-webster', + 'web:builtin:goodreads', ]); }); diff --git a/apps/readest-app/src/__tests__/utils/goodreads.test.ts b/apps/readest-app/src/__tests__/utils/goodreads.test.ts new file mode 100644 index 00000000..4ad9c7d3 --- /dev/null +++ b/apps/readest-app/src/__tests__/utils/goodreads.test.ts @@ -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 => ({ + 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', + ); + }); +}); diff --git a/apps/readest-app/src/app/library/components/BookshelfItem.tsx b/apps/readest-app/src/app/library/components/BookshelfItem.tsx index fbf3c86f..54a6fbd8 100644 --- a/apps/readest-app/src/app/library/components/BookshelfItem.tsx +++ b/apps/readest-app/src/app/library/components/BookshelfItem.tsx @@ -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 = ({ revealItemInDir(folder); }, }, + searchGoodreads: { + text: _('Search on Goodreads'), + action: async () => { + openExternalUrl(getGoodreadsSearchUrl(getBookGoodreadsQuery(book))); + }, + }, download: { text: _('Download Book'), action: async () => { diff --git a/apps/readest-app/src/app/library/utils/libraryUtils.ts b/apps/readest-app/src/app/library/utils/libraryUtils.ts index 4ca15d33..499cce83 100644 --- a/apps/readest-app/src/app/library/utils/libraryUtils.ts +++ b/apps/readest-app/src/app/library/utils/libraryUtils.ts @@ -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 diff --git a/apps/readest-app/src/components/metadata/BookDetailView.tsx b/apps/readest-app/src/components/metadata/BookDetailView.tsx index 87062dae..07c166f6 100644 --- a/apps/readest-app/src/components/metadata/BookDetailView.tsx +++ b/apps/readest-app/src/components/metadata/BookDetailView.tsx @@ -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 = ({ )} + {onDelete && ( [t.id, t])); diff --git a/apps/readest-app/src/store/customDictionaryStore.ts b/apps/readest-app/src/store/customDictionaryStore.ts index ad2d4b99..d4b6f2e9 100644 --- a/apps/readest-app/src/store/customDictionaryStore.ts +++ b/apps/readest-app/src/store/customDictionaryStore.ts @@ -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: [], }; diff --git a/apps/readest-app/src/utils/goodreads.ts b/apps/readest-app/src/utils/goodreads.ts new file mode 100644 index 00000000..d83c8817 --- /dev/null +++ b/apps/readest-app/src/utils/goodreads.ts @@ -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): string => + [book.title, book.author] + .map((part) => part?.trim()) + .filter(Boolean) + .join(' '); diff --git a/apps/readest-app/src/utils/open.ts b/apps/readest-app/src/utils/open.ts index c8165ef5..71a6dae1 100644 --- a/apps/readest-app/src/utils/open.ts +++ b/apps/readest-app/src/utils/open.ts @@ -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 (