forked from akai/readest
Add wiktionary lookup
This commit is contained in:
@@ -108,7 +108,7 @@ const FoliateViewer: React.FC<{
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (viewRef.current) {
|
||||
if (viewRef.current && viewRef.current.renderer) {
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
viewRef.current.renderer.setStyles?.(getStyles(viewSettings, themeCode));
|
||||
}
|
||||
|
||||
@@ -9,30 +9,35 @@ import { RiDeleteBinLine } from 'react-icons/ri';
|
||||
import { Overlayer } from 'foliate-js/overlayer.js';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { BookNote, HighlightColor, HighlightStyle } from '@/types/book';
|
||||
import { uniqueId } from '@/utils/misc';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useFoliateEvents } from '../../hooks/useFoliateEvents';
|
||||
import { getPopupPosition, getPosition, Position, TextSelection } from '@/utils/sel';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import Toast from '@/components/Toast';
|
||||
import AnnotationPopup from './AnnotationPopup';
|
||||
import { uniqueId } from '@/utils/misc';
|
||||
import WiktionaryPopup from './WiktionaryPopup';
|
||||
|
||||
const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const { envConfig } = useEnv();
|
||||
const { settings, saveConfig, getProgress, updateBooknotes } = useReaderStore();
|
||||
const { getConfig, getView, getViewsById } = useReaderStore();
|
||||
const { getConfig, getBookData, getView, getViewsById } = useReaderStore();
|
||||
const { notebookNewAnnotation, setNotebookVisible, setNotebookNewAnnotation } = useReaderStore();
|
||||
const globalReadSettings = settings.globalReadSettings;
|
||||
const config = getConfig(bookKey)!;
|
||||
const progress = getProgress(bookKey)!;
|
||||
const bookData = getBookData(bookKey)!;
|
||||
const view = getView(bookKey);
|
||||
|
||||
const isShowingPopup = useRef(false);
|
||||
const isTextSelected = useRef(false);
|
||||
const [selection, setSelection] = useState<TextSelection | null>();
|
||||
const [showPopup, setShowPopup] = useState(false);
|
||||
const [showAnnotPopup, setShowAnnotPopup] = useState(false);
|
||||
const [showWiktionaryPopup, setShowWiktionaryPopup] = useState(false);
|
||||
const [wiktionaryWord, setWiktionaryWord] = useState('');
|
||||
const [trianglePosition, setTrianglePosition] = useState<Position>();
|
||||
const [popupPosition, setPopupPosition] = useState<Position>();
|
||||
const [annotPopupPosition, setAnnotPopupPosition] = useState<Position>();
|
||||
const [dictPopupPosition, setDictPopupPosition] = useState<Position>();
|
||||
const [toastMessage, setToastMessage] = useState('');
|
||||
const [highlightOptionsVisible, setHighlightOptionsVisible] = useState(false);
|
||||
|
||||
@@ -43,8 +48,10 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
globalReadSettings.highlightStyles[selectedStyle],
|
||||
);
|
||||
|
||||
const popupWidth = 240;
|
||||
const popupHeight = 44;
|
||||
const dictPopupWidth = 400;
|
||||
const dictPopupHeight = 300;
|
||||
const annotPopupWidth = 240;
|
||||
const annotPopupHeight = 44;
|
||||
const popupPadding = 10;
|
||||
|
||||
const onLoad = (event: Event) => {
|
||||
@@ -92,8 +99,9 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
useFoliateEvents(view, { onLoad, onDrawAnnotation, onShowAnnotation }, [config]);
|
||||
|
||||
const handleDismissPopup = () => {
|
||||
setShowPopup(false);
|
||||
setSelection(null);
|
||||
setShowAnnotPopup(false);
|
||||
setShowWiktionaryPopup(false);
|
||||
isShowingPopup.current = false;
|
||||
};
|
||||
|
||||
@@ -110,7 +118,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
isTextSelected.current = false;
|
||||
return true;
|
||||
}
|
||||
if (showPopup || isShowingPopup.current) {
|
||||
if (showAnnotPopup || isShowingPopup.current) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -129,10 +137,24 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
if (!gridFrame) return;
|
||||
const rect = gridFrame.getBoundingClientRect();
|
||||
const triangPos = getPosition(selection.range, rect);
|
||||
const popupPos = getPopupPosition(triangPos, rect, popupWidth, popupHeight, popupPadding);
|
||||
const annotPopupPos = getPopupPosition(
|
||||
triangPos,
|
||||
rect,
|
||||
annotPopupWidth,
|
||||
annotPopupHeight,
|
||||
popupPadding,
|
||||
);
|
||||
const dictPopupPos = getPopupPosition(
|
||||
triangPos,
|
||||
rect,
|
||||
dictPopupWidth,
|
||||
dictPopupHeight,
|
||||
popupPadding,
|
||||
);
|
||||
if (triangPos.point.x == 0 || triangPos.point.y == 0) return;
|
||||
setShowPopup(true);
|
||||
setPopupPosition(popupPos);
|
||||
setShowAnnotPopup(true);
|
||||
setAnnotPopupPosition(annotPopupPos);
|
||||
setDictPopupPosition(dictPopupPos);
|
||||
setTrianglePosition(triangPos);
|
||||
isShowingPopup.current = true;
|
||||
}
|
||||
@@ -145,7 +167,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
|
||||
const handleCopy = () => {
|
||||
if (!selection || !selection.text) return;
|
||||
setShowPopup(false);
|
||||
setShowAnnotPopup(false);
|
||||
setToastMessage('Copied to notebook');
|
||||
|
||||
const { booknotes: annotations = [] } = config;
|
||||
@@ -210,7 +232,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
views.forEach((view) => view?.addAnnotation(annotation));
|
||||
} else {
|
||||
annotations.splice(existingIndex, 1);
|
||||
setShowPopup(false);
|
||||
setShowAnnotPopup(false);
|
||||
}
|
||||
} else {
|
||||
annotations.push(annotation);
|
||||
@@ -236,11 +258,16 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
|
||||
const handleSearch = () => {
|
||||
if (!selection || !selection.text) return;
|
||||
setShowPopup(false);
|
||||
setShowAnnotPopup(false);
|
||||
eventDispatcher.dispatch('search', { term: selection.text });
|
||||
};
|
||||
|
||||
const handleDictionary = () => {};
|
||||
const handleDictionary = () => {
|
||||
if (!selection || !selection.text) return;
|
||||
setShowAnnotPopup(false);
|
||||
setShowWiktionaryPopup(true);
|
||||
setWiktionaryWord(selection.text);
|
||||
};
|
||||
|
||||
const selectionAnnotated = selection?.annotated;
|
||||
const buttons = [
|
||||
@@ -257,23 +284,33 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{showPopup && !notebookNewAnnotation && (
|
||||
{(showAnnotPopup || showWiktionaryPopup) && !notebookNewAnnotation && (
|
||||
<div
|
||||
className='fixed inset-0'
|
||||
onClick={handleDismissPopupAndSelection}
|
||||
onContextMenu={handleDismissPopup}
|
||||
/>
|
||||
)}
|
||||
{showPopup && trianglePosition && popupPosition && (
|
||||
{showWiktionaryPopup && trianglePosition && dictPopupPosition && (
|
||||
<WiktionaryPopup
|
||||
word={wiktionaryWord}
|
||||
lang={bookData.bookDoc?.metadata.language as string}
|
||||
position={dictPopupPosition}
|
||||
trianglePosition={trianglePosition}
|
||||
popupWidth={dictPopupWidth}
|
||||
popupHeight={dictPopupHeight}
|
||||
/>
|
||||
)}
|
||||
{showAnnotPopup && trianglePosition && annotPopupPosition && (
|
||||
<AnnotationPopup
|
||||
buttons={buttons}
|
||||
position={popupPosition}
|
||||
position={annotPopupPosition}
|
||||
trianglePosition={trianglePosition}
|
||||
highlightOptionsVisible={highlightOptionsVisible}
|
||||
selectedStyle={selectedStyle}
|
||||
selectedColor={selectedColor}
|
||||
popupWidth={popupWidth}
|
||||
popupHeight={popupHeight}
|
||||
popupWidth={annotPopupWidth}
|
||||
popupHeight={annotPopupHeight}
|
||||
onHighlight={handleHighlight}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Position } from '@/utils/sel';
|
||||
|
||||
type Definition = {
|
||||
definition: string;
|
||||
examples?: string[];
|
||||
};
|
||||
|
||||
type Result = {
|
||||
partOfSpeech: string;
|
||||
definitions: Definition[];
|
||||
language: string;
|
||||
};
|
||||
|
||||
interface WiktionaryProps {
|
||||
word: string;
|
||||
lang?: string;
|
||||
position: Position;
|
||||
trianglePosition: Position;
|
||||
popupWidth: number;
|
||||
popupHeight: number;
|
||||
}
|
||||
|
||||
const WiktionaryPopup: React.FC<WiktionaryProps> = ({
|
||||
word,
|
||||
lang,
|
||||
position,
|
||||
trianglePosition,
|
||||
popupWidth,
|
||||
popupHeight,
|
||||
}) => {
|
||||
const [lookupWord, setLookupWord] = useState(word);
|
||||
const isLookingUp = useRef(false);
|
||||
|
||||
const interceptDictLinks = (definition: string): HTMLElement[] => {
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = definition;
|
||||
|
||||
const links = container.querySelectorAll<HTMLAnchorElement>('a[rel="mw:WikiLink"]');
|
||||
|
||||
links.forEach((link) => {
|
||||
const title = link.getAttribute('title'); // Get the title attribute
|
||||
if (title) {
|
||||
link.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
setLookupWord(title);
|
||||
isLookingUp.current = false;
|
||||
});
|
||||
|
||||
link.className = 'text-primary underline cursor-pointer';
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(container.childNodes) as HTMLElement[];
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isLookingUp.current) {
|
||||
return;
|
||||
}
|
||||
isLookingUp.current = true;
|
||||
const main = document.querySelector('main') as HTMLElement;
|
||||
const footer = document.querySelector('footer') as HTMLElement;
|
||||
|
||||
const fetchDefinitions = async (word: string, language?: string) => {
|
||||
main.innerHTML = '';
|
||||
footer.dataset['state'] = 'loading';
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://en.wiktionary.org/api/rest_v1/page/definition/${word}`,
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch definitions');
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
const results: Result[] | undefined = language
|
||||
? json[language] || json['en']
|
||||
: json[Object.keys(json)[0]!];
|
||||
|
||||
if (!results || results.length === 0) {
|
||||
throw new Error('No results found');
|
||||
}
|
||||
|
||||
const hgroup = document.createElement('hgroup');
|
||||
const h1 = document.createElement('h1');
|
||||
h1.innerText = word;
|
||||
h1.className = 'text-lg font-bold';
|
||||
|
||||
const p = document.createElement('p');
|
||||
p.innerText = results[0]!.language;
|
||||
p.className = 'text-sm italic opacity-75';
|
||||
hgroup.append(h1, p);
|
||||
main.append(hgroup);
|
||||
|
||||
results.forEach(({ partOfSpeech, definitions }: Result) => {
|
||||
const h2 = document.createElement('h2');
|
||||
h2.innerText = partOfSpeech;
|
||||
h2.className = 'text-base font-semibold mt-4';
|
||||
|
||||
const ol = document.createElement('ol');
|
||||
ol.className = 'pl-8 list-decimal';
|
||||
|
||||
definitions.forEach(({ definition, examples }: Definition) => {
|
||||
if (!definition) return;
|
||||
const li = document.createElement('li');
|
||||
const processedContent = interceptDictLinks(definition);
|
||||
li.append(...processedContent);
|
||||
|
||||
if (examples) {
|
||||
const ul = document.createElement('ul');
|
||||
ul.className = 'pl-8 list-disc text-sm italic opacity-75';
|
||||
|
||||
examples.forEach((example) => {
|
||||
const exampleLi = document.createElement('li');
|
||||
exampleLi.innerHTML = example;
|
||||
ul.appendChild(exampleLi);
|
||||
});
|
||||
|
||||
li.appendChild(ul);
|
||||
}
|
||||
|
||||
ol.appendChild(li);
|
||||
});
|
||||
|
||||
main.appendChild(h2);
|
||||
main.appendChild(ol);
|
||||
});
|
||||
|
||||
footer.dataset['state'] = 'loaded';
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
footer.dataset['state'] = 'error';
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className =
|
||||
'flex flex-col items-center justify-center w-full h-full text-center absolute inset-0';
|
||||
|
||||
const h1 = document.createElement('h1');
|
||||
h1.innerText = 'Error';
|
||||
h1.className = 'text-lg font-bold';
|
||||
|
||||
const p = document.createElement('p');
|
||||
p.innerHTML = `Unable to load the word. Try searching directly on <a href="https://en.wiktionary.org/w/index.php?search=${encodeURIComponent(
|
||||
word,
|
||||
)}" target="_blank" rel="noopener noreferrer" class="text-primary underline">Wiktionary</a>.`;
|
||||
|
||||
div.append(h1, p);
|
||||
main.append(div);
|
||||
}
|
||||
};
|
||||
|
||||
const langCode = typeof lang === 'string' ? lang : lang?.[0];
|
||||
fetchDefinitions(lookupWord, langCode);
|
||||
}, [lookupWord, lang]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className='triangle text-neutral absolute z-10'
|
||||
style={{
|
||||
left: `${trianglePosition.point.x}px`,
|
||||
top: `${trianglePosition.point.y}px`,
|
||||
borderLeft: '6px solid transparent',
|
||||
borderRight: '6px solid transparent',
|
||||
borderBottom: trianglePosition.dir === 'up' ? 'none' : `6px solid`,
|
||||
borderTop: trianglePosition.dir === 'up' ? `6px solid` : 'none',
|
||||
transform: 'translateX(-50%)',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className='bg-neutral absolute overflow-y-auto rounded-xl shadow-lg'
|
||||
style={{
|
||||
width: `${popupWidth}px`,
|
||||
height: `${popupHeight}px`,
|
||||
left: `${position.point.x}px`,
|
||||
top: `${position.point.y}px`,
|
||||
}}
|
||||
>
|
||||
<main className='p-4 font-sans' />
|
||||
<footer className='hidden data-[state=loaded]:block data-[state=error]:hidden data-[state=loading]:hidden'>
|
||||
<div className='p-4 text-sm opacity-60'>
|
||||
From <a id='link'>Wiktionary</a>, released under the{' '}
|
||||
<a href='https://creativecommons.org/licenses/by-sa/4.0/'>CC BY-SA License</a>.
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WiktionaryPopup;
|
||||
@@ -1,4 +1,4 @@
|
||||
const doubleClickThreshold = 200;
|
||||
const doubleClickThreshold = 250;
|
||||
const longHoldThreshold = 500;
|
||||
let lastClickTime = 0;
|
||||
let longHoldTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
@@ -43,6 +43,7 @@ export interface BookDoc {
|
||||
metadata: {
|
||||
title: string;
|
||||
author: string;
|
||||
language: string | string[];
|
||||
editor?: string;
|
||||
publisher?: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user