@@ -73,7 +73,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const dictPopupWidth = Math.min(480, maxWidth);
|
||||
const dictPopupHeight = Math.min(300, maxHeight);
|
||||
const transPopupWidth = Math.min(480, maxWidth);
|
||||
const transPopupHeight = Math.min(360, maxHeight);
|
||||
const transPopupHeight = Math.min(265, maxHeight);
|
||||
const annotPopupWidth = Math.min(useResponsiveSize(300), maxWidth);
|
||||
const annotPopupHeight = useResponsiveSize(44);
|
||||
const androidSelectionHandlerHeight = 0;
|
||||
@@ -91,13 +91,8 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
view?.deselect();
|
||||
};
|
||||
|
||||
const {
|
||||
handleScroll,
|
||||
handlePointerup,
|
||||
handleSelectionchange,
|
||||
handleAnnotPopup,
|
||||
handleShowAnnotation,
|
||||
} = useTextSelector(bookKey, setSelection, handleDismissPopup);
|
||||
const { handleScroll, handlePointerup, handleSelectionchange, handleShowPopup, handleUpToPopup } =
|
||||
useTextSelector(bookKey, setSelection, handleDismissPopup);
|
||||
|
||||
const onLoad = (event: Event) => {
|
||||
const detail = (event as CustomEvent).detail;
|
||||
@@ -163,15 +158,15 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
setSelectedStyle(annotation.style!);
|
||||
setSelectedColor(annotation.color!);
|
||||
setSelection(selection);
|
||||
handleShowAnnotation();
|
||||
handleUpToPopup();
|
||||
};
|
||||
|
||||
useFoliateEvents(view, { onLoad, onDrawAnnotation, onShowAnnotation });
|
||||
|
||||
useEffect(() => {
|
||||
handleAnnotPopup(showAnnotPopup);
|
||||
handleShowPopup(showAnnotPopup || showWiktionaryPopup || showWikipediaPopup || showDeepLPopup);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [showAnnotPopup]);
|
||||
}, [showAnnotPopup, showWiktionaryPopup, showWikipediaPopup, showDeepLPopup]);
|
||||
|
||||
useEffect(() => {
|
||||
eventDispatcher.on('export-annotations', handleExportMarkdown);
|
||||
@@ -214,11 +209,11 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
popupPadding,
|
||||
);
|
||||
if (triangPos.point.x == 0 || triangPos.point.y == 0) return;
|
||||
setShowAnnotPopup(true);
|
||||
setAnnotPopupPosition(annotPopupPos);
|
||||
setDictPopupPosition(dictPopupPos);
|
||||
setTranslatorPopupPosition(transPopupPos);
|
||||
setTrianglePosition(triangPos);
|
||||
handleShowAnnotPopup();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selection, bookKey]);
|
||||
@@ -245,6 +240,13 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [progress]);
|
||||
|
||||
const handleShowAnnotPopup = () => {
|
||||
setShowAnnotPopup(true);
|
||||
setShowDeepLPopup(false);
|
||||
setShowWiktionaryPopup(false);
|
||||
setShowWikipediaPopup(false);
|
||||
};
|
||||
|
||||
const handleCopy = () => {
|
||||
if (!selection || !selection.text) return;
|
||||
eventDispatcher.dispatch('toast', {
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Popup from '@/components/Popup';
|
||||
import { Position } from '@/utils/sel';
|
||||
import { getAPIBaseUrl } from '@/services/environment';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { getAPIBaseUrl } from '@/services/environment';
|
||||
import { TRANSLATED_LANGS } from '@/services/constants';
|
||||
|
||||
const LANGUAGES = {
|
||||
AUTO: 'Auto Detect',
|
||||
EN: 'English',
|
||||
DE: 'German',
|
||||
FR: 'French',
|
||||
ES: 'Spanish',
|
||||
IT: 'Italian',
|
||||
EL: 'Greek',
|
||||
PT: 'Portuguese',
|
||||
NL: 'Dutch',
|
||||
PL: 'Polish',
|
||||
UK: 'Ukrainian',
|
||||
RU: 'Russian',
|
||||
AR: 'Arabic',
|
||||
TR: 'Turkish',
|
||||
ID: 'Indonesian',
|
||||
KO: 'Korean',
|
||||
JA: 'Japanese',
|
||||
'ZH-HANS': 'Chinese (Simplified)',
|
||||
'ZH-HANT': 'Chinese (Traditional)',
|
||||
const notSupportedLangs = ['hi', 'vi'];
|
||||
|
||||
const generateTranslatorLangs = () => {
|
||||
const langs = { ...TRANSLATED_LANGS };
|
||||
const result: Record<string, string> = {};
|
||||
for (const [code, name] of Object.entries(langs)) {
|
||||
if (notSupportedLangs.includes(code)) continue;
|
||||
let newCode = code.toUpperCase();
|
||||
if (newCode === 'ZH-CN') {
|
||||
newCode = 'ZH-HANS';
|
||||
} else if (newCode === 'ZH-TW') {
|
||||
newCode = 'ZH-HANT';
|
||||
}
|
||||
result[newCode] = name;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const TRANSLATOR_LANGS = generateTranslatorLangs();
|
||||
|
||||
const DEEPL_API_ENDPOINT = getAPIBaseUrl() + '/deepl/translate';
|
||||
|
||||
interface DeepLPopupProps {
|
||||
@@ -51,7 +51,9 @@ const DeepLPopup: React.FC<DeepLPopupProps> = ({
|
||||
const [sourceLang, setSourceLang] = useState('AUTO');
|
||||
const [targetLang, setTargetLang] = useState(settings.globalReadSettings.translateTargetLang);
|
||||
const [translation, setTranslation] = useState<string | null>(null);
|
||||
const [detectedSourceLang, setDetectedSourceLang] = useState<keyof typeof LANGUAGES | null>(null);
|
||||
const [detectedSourceLang, setDetectedSourceLang] = useState<
|
||||
keyof typeof TRANSLATOR_LANGS | null
|
||||
>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -117,47 +119,59 @@ const DeepLPopup: React.FC<DeepLPopupProps> = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [text, token, sourceLang, targetLang]);
|
||||
|
||||
const popupPadding = useResponsiveSize(10);
|
||||
const maxHeight = window.innerHeight - 2 * popupPadding;
|
||||
const popupMaxHeight = Math.min(360, maxHeight);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Popup
|
||||
trianglePosition={trianglePosition}
|
||||
width={popupWidth}
|
||||
height={popupHeight}
|
||||
minHeight={popupHeight}
|
||||
maxHeight={popupMaxHeight}
|
||||
position={position}
|
||||
className='select-text'
|
||||
className='grid h-full select-text grid-rows-[1fr,auto,1fr] bg-gray-600 text-white'
|
||||
triangleClassName='text-gray-600'
|
||||
>
|
||||
<div className='text-neutral-content relative h-[50%] overflow-y-auto border-b border-neutral-400/75 p-4 font-sans'>
|
||||
<div className='overflow-y-auto p-4 font-sans'>
|
||||
<div className='mb-2 flex items-center justify-between'>
|
||||
<h1 className='text-base font-semibold'>{_('Original Text')}</h1>
|
||||
<h1 className='text-sm font-normal'>{_('Original Text')}</h1>
|
||||
<select
|
||||
value={sourceLang}
|
||||
onChange={handleSourceLangChange}
|
||||
className='select text-neutral-content h-8 min-h-8 rounded-md border-none bg-neutral-200/50 text-sm focus:outline-none focus:ring-0'
|
||||
className='select h-8 min-h-8 rounded-md border-none bg-gray-600 text-sm text-white/75 focus:outline-none focus:ring-0'
|
||||
>
|
||||
{Object.entries(LANGUAGES).map(([code, name]) => {
|
||||
{[
|
||||
['AUTO', _('Auto Detect')],
|
||||
...Object.entries(TRANSLATOR_LANGS).sort((a, b) => a[1].localeCompare(b[1])),
|
||||
].map(([code, name]) => {
|
||||
return (
|
||||
<option key={code} value={code}>
|
||||
{detectedSourceLang && sourceLang === 'AUTO' && code === 'AUTO'
|
||||
? `${LANGUAGES[detectedSourceLang] || detectedSourceLang} ` + _('(detected)')
|
||||
? `${TRANSLATOR_LANGS[detectedSourceLang] || detectedSourceLang} ` +
|
||||
_('(detected)')
|
||||
: name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<p className='text-base'>{text}</p>
|
||||
<p className='text-base text-white/90'>{text}</p>
|
||||
</div>
|
||||
|
||||
<div className='text-neutral-content relative h-[50%] overflow-y-auto p-4 font-sans'>
|
||||
<div className='mx-4 flex-shrink-0 border-t border-gray-500/30'></div>
|
||||
|
||||
<div className='overflow-y-auto p-4 font-sans'>
|
||||
<div className='mb-2 flex items-center justify-between'>
|
||||
<h2 className='text-base font-semibold'>{_('Translated Text')}</h2>
|
||||
<h2 className='text-sm font-normal'>{_('Translated Text')}</h2>
|
||||
<select
|
||||
value={targetLang}
|
||||
onChange={handleTargetLangChange}
|
||||
className='select text-neutral-content h-8 min-h-8 rounded-md border-none bg-neutral-200/50 text-sm focus:outline-none focus:ring-0'
|
||||
className='select h-8 min-h-8 rounded-md border-none bg-gray-600 text-sm text-white/75 focus:outline-none focus:ring-0'
|
||||
>
|
||||
{Object.entries(LANGUAGES)
|
||||
.filter(([code]) => code !== 'AUTO')
|
||||
{Object.entries(TRANSLATOR_LANGS)
|
||||
.sort((a, b) => a[1].localeCompare(b[1]))
|
||||
.map(([code, name]) => (
|
||||
<option key={code} value={code}>
|
||||
{name}
|
||||
@@ -172,7 +186,9 @@ const DeepLPopup: React.FC<DeepLPopupProps> = ({
|
||||
<p className='text-base text-red-600'>{error}</p>
|
||||
) : (
|
||||
<div>
|
||||
<p className='text-base'>{translation || 'No translation available.'}</p>
|
||||
<p className='text-base text-white/90'>
|
||||
{translation || 'No translation available.'}
|
||||
</p>
|
||||
<div className='pt-4 text-sm opacity-60'>Translated by DeepL.</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -19,9 +19,9 @@ export const useTextSelector = (
|
||||
const primaryLang = bookData.book?.primaryLanguage || 'en';
|
||||
const osPlatform = getOSPlatform();
|
||||
|
||||
const isPopuped = useRef(false);
|
||||
const isUpToPopup = useRef(false);
|
||||
const isTextSelected = useRef(false);
|
||||
const isAnnotPopuped = useRef(false);
|
||||
const isAnnotUpToPopup = useRef(false);
|
||||
const selectionPosition = useRef<number | null>(null);
|
||||
|
||||
const isValidSelection = (sel: Selection) => {
|
||||
@@ -67,10 +67,13 @@ export const useTextSelector = (
|
||||
const sel = doc.getSelection() as Selection;
|
||||
if (osPlatform === 'ios') return;
|
||||
if (!isValidSelection(sel)) {
|
||||
if (!isAnnotUpToPopup.current) {
|
||||
if (!isUpToPopup.current) {
|
||||
handleDismissPopup();
|
||||
isTextSelected.current = false;
|
||||
}
|
||||
if (isPopuped.current) {
|
||||
isUpToPopup.current = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -79,6 +82,7 @@ export const useTextSelector = (
|
||||
if (osPlatform === 'android') {
|
||||
makeSelection(sel, index, false);
|
||||
}
|
||||
isUpToPopup.current = true;
|
||||
};
|
||||
const handlePointerup = (doc: Document, index: number) => {
|
||||
// Available on iOS and Desktop, fired at touchend or mouseup
|
||||
@@ -101,17 +105,20 @@ export const useTextSelector = (
|
||||
}
|
||||
};
|
||||
|
||||
const handleAnnotPopup = (showAnnotPopup: boolean) => {
|
||||
const handleShowPopup = (showPopup: boolean) => {
|
||||
setTimeout(
|
||||
() => {
|
||||
isAnnotPopuped.current = showAnnotPopup;
|
||||
if (showPopup && !isPopuped.current) {
|
||||
isUpToPopup.current = false;
|
||||
}
|
||||
isPopuped.current = showPopup;
|
||||
},
|
||||
['android', 'ios'].includes(osPlatform) ? 0 : 500,
|
||||
);
|
||||
};
|
||||
|
||||
const handleShowAnnotation = () => {
|
||||
isAnnotUpToPopup.current = true;
|
||||
const handleUpToPopup = () => {
|
||||
isUpToPopup.current = true;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -125,16 +132,17 @@ export const useTextSelector = (
|
||||
|
||||
useEffect(() => {
|
||||
const handleSingleClick = (): boolean => {
|
||||
if (isAnnotUpToPopup.current) {
|
||||
isAnnotUpToPopup.current = false;
|
||||
if (isUpToPopup.current) {
|
||||
isUpToPopup.current = false;
|
||||
return true;
|
||||
}
|
||||
if (isTextSelected.current) {
|
||||
handleDismissPopup();
|
||||
isTextSelected.current = false;
|
||||
view?.deselect();
|
||||
return true;
|
||||
}
|
||||
if (isAnnotPopuped.current) {
|
||||
if (isPopuped.current) {
|
||||
handleDismissPopup();
|
||||
return true;
|
||||
}
|
||||
@@ -152,7 +160,7 @@ export const useTextSelector = (
|
||||
handleScroll,
|
||||
handlePointerup,
|
||||
handleSelectionchange,
|
||||
handleAnnotPopup,
|
||||
handleShowAnnotation,
|
||||
handleShowPopup,
|
||||
handleUpToPopup,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Position } from '@/utils/sel';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
const Popup = ({
|
||||
width,
|
||||
height,
|
||||
minHeight,
|
||||
maxHeight,
|
||||
position,
|
||||
trianglePosition,
|
||||
children,
|
||||
@@ -11,74 +14,121 @@ const Popup = ({
|
||||
additionalStyle = {},
|
||||
}: {
|
||||
width: number;
|
||||
height: number;
|
||||
height?: number;
|
||||
minHeight?: number;
|
||||
maxHeight?: number;
|
||||
position?: Position;
|
||||
trianglePosition?: Position;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
triangleClassName?: string;
|
||||
additionalStyle?: React.CSSProperties;
|
||||
}) => (
|
||||
<div>
|
||||
<div
|
||||
id='popup-container'
|
||||
className={`bg-base-300 absolute rounded-lg font-sans shadow-xl ${className}`}
|
||||
style={{
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
left: `${position ? position.point.x : -999}px`,
|
||||
top: `${position ? position.point.y : -999}px`,
|
||||
...additionalStyle,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<div
|
||||
className={`triangle text-base-300 absolute ${triangleClassName}`}
|
||||
style={{
|
||||
left:
|
||||
trianglePosition?.dir === 'left'
|
||||
? `${trianglePosition.point.x}px`
|
||||
: trianglePosition?.dir === 'right'
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [adjustedPosition, setAdjustedPosition] = useState(position);
|
||||
const [childrenHeight, setChildrenHeight] = useState(height || minHeight || 0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
const newHeight = entry.contentRect.height;
|
||||
if (newHeight !== childrenHeight) {
|
||||
setChildrenHeight(newHeight);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
resizeObserver.observe(containerRef.current);
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || !position || !trianglePosition) return;
|
||||
if (position.dir !== 'up') {
|
||||
setAdjustedPosition(position);
|
||||
return;
|
||||
}
|
||||
const containerHeight = childrenHeight || containerRef.current.offsetHeight;
|
||||
const newPosition = {
|
||||
...position,
|
||||
point: {
|
||||
...position.point,
|
||||
y: trianglePosition.point.y - containerHeight,
|
||||
},
|
||||
};
|
||||
setAdjustedPosition(newPosition);
|
||||
}, [position, trianglePosition, childrenHeight]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
id='popup-container'
|
||||
ref={containerRef}
|
||||
className={`bg-base-300 absolute rounded-lg font-sans shadow-xl ${className}`}
|
||||
style={{
|
||||
width: `${width}px`,
|
||||
height: height ? `${height}px` : 'auto',
|
||||
minHeight: minHeight ? `${minHeight}px` : 'none',
|
||||
maxHeight: maxHeight ? `${maxHeight}px` : 'none',
|
||||
left: `${adjustedPosition ? adjustedPosition.point.x : -999}px`,
|
||||
top: `${adjustedPosition ? adjustedPosition.point.y : -999}px`,
|
||||
...additionalStyle,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<div
|
||||
className={`triangle text-base-300 absolute ${triangleClassName}`}
|
||||
style={{
|
||||
left:
|
||||
trianglePosition?.dir === 'left'
|
||||
? `${trianglePosition.point.x}px`
|
||||
: `${trianglePosition ? trianglePosition.point.x : -999}px`,
|
||||
top:
|
||||
trianglePosition?.dir === 'up'
|
||||
? `${trianglePosition.point.y}px`
|
||||
: trianglePosition?.dir === 'down'
|
||||
: trianglePosition?.dir === 'right'
|
||||
? `${trianglePosition.point.x}px`
|
||||
: `${trianglePosition ? trianglePosition.point.x : -999}px`,
|
||||
top:
|
||||
trianglePosition?.dir === 'up'
|
||||
? `${trianglePosition.point.y}px`
|
||||
: `${trianglePosition ? trianglePosition.point.y : -999}px`,
|
||||
borderLeft:
|
||||
trianglePosition?.dir === 'right'
|
||||
? 'none'
|
||||
: trianglePosition?.dir === 'left'
|
||||
? `6px solid`
|
||||
: '6px solid transparent',
|
||||
borderRight:
|
||||
trianglePosition?.dir === 'left'
|
||||
? 'none'
|
||||
: trianglePosition?.dir === 'right'
|
||||
? `6px solid`
|
||||
: '6px solid transparent',
|
||||
borderTop:
|
||||
trianglePosition?.dir === 'down'
|
||||
? 'none'
|
||||
: trianglePosition?.dir === 'up'
|
||||
? `6px solid`
|
||||
: '6px solid transparent',
|
||||
borderBottom:
|
||||
trianglePosition?.dir === 'up'
|
||||
? 'none'
|
||||
: trianglePosition?.dir === 'down'
|
||||
? `6px solid`
|
||||
: '6px solid transparent',
|
||||
transform:
|
||||
trianglePosition?.dir === 'left' || trianglePosition?.dir === 'right'
|
||||
? 'translateY(-50%)'
|
||||
: 'translateX(-50%)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
: trianglePosition?.dir === 'down'
|
||||
? `${trianglePosition.point.y}px`
|
||||
: `${trianglePosition ? trianglePosition.point.y : -999}px`,
|
||||
borderLeft:
|
||||
trianglePosition?.dir === 'right'
|
||||
? 'none'
|
||||
: trianglePosition?.dir === 'left'
|
||||
? `6px solid`
|
||||
: '6px solid transparent',
|
||||
borderRight:
|
||||
trianglePosition?.dir === 'left'
|
||||
? 'none'
|
||||
: trianglePosition?.dir === 'right'
|
||||
? `6px solid`
|
||||
: '6px solid transparent',
|
||||
borderTop:
|
||||
trianglePosition?.dir === 'down'
|
||||
? 'none'
|
||||
: trianglePosition?.dir === 'up'
|
||||
? `6px solid`
|
||||
: '6px solid transparent',
|
||||
borderBottom:
|
||||
trianglePosition?.dir === 'up'
|
||||
? 'none'
|
||||
: trianglePosition?.dir === 'down'
|
||||
? `6px solid`
|
||||
: '6px solid transparent',
|
||||
transform:
|
||||
trianglePosition?.dir === 'left' || trianglePosition?.dir === 'right'
|
||||
? 'translateY(-50%)'
|
||||
: 'translateX(-50%)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Popup;
|
||||
|
||||
Reference in New Issue
Block a user