forked from akai/readest
This commit is contained in:
@@ -8,7 +8,7 @@ import { CreateProofreadRuleOptions, useProofreadStore } from '@/store/proofread
|
||||
import { ProofreadScope } from '@/types/book';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { Position, TextSelection } from '@/utils/sel';
|
||||
import { isWholeWord } from '@/utils/word';
|
||||
import { isPunctuationOnly, isWholeWord } from '@/utils/word';
|
||||
import Select from '@/components/Select';
|
||||
import Popup from '@/components/Popup';
|
||||
|
||||
@@ -41,11 +41,17 @@ const ProofreadPopup: React.FC<ProofreadPopupProps> = ({
|
||||
|
||||
const [replacementText, setReplacementText] = useState('');
|
||||
const [caseSensitive, setCaseSensitive] = useState(true);
|
||||
const [wholeWord, setWholeWord] = useState(!isPunctuationOnly(selection?.text || ''));
|
||||
const [scope, setScope] = useState<ProofreadScope>('selection');
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
useAutoFocus<HTMLInputElement>({ ref: inputRef });
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const text = e.target.value;
|
||||
setReplacementText(text);
|
||||
};
|
||||
|
||||
const handleScopeChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setScope(event.target.value as ProofreadScope);
|
||||
};
|
||||
@@ -58,10 +64,10 @@ const ProofreadPopup: React.FC<ProofreadPopupProps> = ({
|
||||
if (range) {
|
||||
const isValidWholeWord = isWholeWord(range, selection?.text || '');
|
||||
|
||||
if (!isValidWholeWord) {
|
||||
if (wholeWord && !isValidWholeWord) {
|
||||
eventDispatcher.dispatch('toast', {
|
||||
type: 'warning',
|
||||
message: `Cannot replace "${selection.text}" - please select a complete word. Partial word selections (like "and" in "England" or "errand") are not supported.`,
|
||||
message: 'Please select a whole word or uncheck the "Whole word" option.',
|
||||
timeout: 5000,
|
||||
});
|
||||
return;
|
||||
@@ -73,7 +79,7 @@ const ProofreadPopup: React.FC<ProofreadPopupProps> = ({
|
||||
range.insertNode(textNode);
|
||||
}
|
||||
|
||||
const options = {
|
||||
const options: CreateProofreadRuleOptions = {
|
||||
scope,
|
||||
pattern: selection.text,
|
||||
replacement: replacementText.trim(),
|
||||
@@ -82,7 +88,7 @@ const ProofreadPopup: React.FC<ProofreadPopupProps> = ({
|
||||
isRegex: false,
|
||||
enabled: true,
|
||||
caseSensitive,
|
||||
wholeWord: true,
|
||||
wholeWord: wholeWord,
|
||||
};
|
||||
onConfirm?.(options);
|
||||
|
||||
@@ -111,14 +117,14 @@ const ProofreadPopup: React.FC<ProofreadPopupProps> = ({
|
||||
width={popupWidth}
|
||||
minHeight={popupHeight}
|
||||
position={position}
|
||||
className='flex flex-col justify-between rounded-lg bg-gray-700 text-gray-400'
|
||||
className='not-eink:text-gray-400 flex flex-col justify-between rounded-lg bg-gray-700'
|
||||
triangleClassName='text-gray-700'
|
||||
onDismiss={onDismiss}
|
||||
>
|
||||
<div className='flex flex-col gap-6 p-4'>
|
||||
<div className='flex gap-1 text-xs text-gray-400'>
|
||||
<div className='not-eink:text-gray-400 flex gap-1 text-xs'>
|
||||
<span>{_('Selected text:')}</span>
|
||||
<span className='line-clamp-1 select-text break-words text-yellow-300'>
|
||||
<span className='not-eink:text-yellow-300 line-clamp-1 select-text break-words font-medium'>
|
||||
"{selection?.text || ''}"
|
||||
</span>
|
||||
</div>
|
||||
@@ -131,20 +137,23 @@ const ProofreadPopup: React.FC<ProofreadPopupProps> = ({
|
||||
ref={inputRef}
|
||||
type='text'
|
||||
value={replacementText}
|
||||
onChange={(e) => setReplacementText(e.target.value)}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && replacementText.trim()) {
|
||||
handleApply();
|
||||
}
|
||||
}}
|
||||
placeholder={_('Enter text...')}
|
||||
className='w-full flex-1 rounded-md bg-gray-600 p-2 text-sm text-white placeholder-gray-400 focus:outline-none focus:ring-0'
|
||||
className={clsx(
|
||||
'w-full flex-1 rounded-md p-2 text-sm placeholder-gray-400 focus:outline-none focus:ring-0',
|
||||
'not-eink:bg-gray-600 not-eink:text-white eink:border eink:border-base-content',
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
onClick={handleApply}
|
||||
disabled={!replacementText.trim()}
|
||||
className={clsx(
|
||||
'btn btn-sm btn-ghost text-blue-600 disabled:text-gray-600',
|
||||
'btn btn-sm btn-ghost btn-primary disabled:text-base-content/75 text-blue-600 disabled:opacity-75',
|
||||
'bg-transparent hover:bg-transparent disabled:bg-transparent',
|
||||
)}
|
||||
>
|
||||
@@ -153,14 +162,14 @@ const ProofreadPopup: React.FC<ProofreadPopupProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-between gap-4 p-4'>
|
||||
<label className='flex max-w-[30%] cursor-pointer items-center gap-2'>
|
||||
<div className='flex items-center gap-8 p-4'>
|
||||
<label className='flex cursor-pointer items-center gap-2'>
|
||||
<span className='line-clamp-1 text-xs' title={_('Case sensitive:')}>
|
||||
{_('Case sensitive:')}
|
||||
</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='toggle toggle-sm bg-gray-500 checked:bg-black hover:bg-gray-500 hover:checked:bg-black'
|
||||
className='toggle toggle-sm not-eink:bg-gray-500 checked:bg-black hover:bg-gray-500 hover:checked:bg-black'
|
||||
style={
|
||||
{
|
||||
'--tglbg': '#4B5563',
|
||||
@@ -171,17 +180,33 @@ const ProofreadPopup: React.FC<ProofreadPopupProps> = ({
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className='flex max-w-[65%] flex-1 items-center justify-between gap-2'>
|
||||
<label htmlFor='scope-select' className='line-clamp-1 text-xs' title={_('Scope:')}>
|
||||
{_('Scope:')}
|
||||
</label>
|
||||
<Select
|
||||
className='max-w-[50%] bg-gray-600 text-white'
|
||||
value={scope}
|
||||
onChange={handleScopeChange}
|
||||
options={scopeOptions}
|
||||
<label className='flex cursor-pointer items-center gap-2'>
|
||||
<span className='line-clamp-1 text-xs' title={_('Whole word:')}>
|
||||
{_('Whole word:')}
|
||||
</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='toggle toggle-sm bg-gray-500 checked:bg-black hover:bg-gray-500 hover:checked:bg-black'
|
||||
style={
|
||||
{
|
||||
'--tglbg': '#4B5563',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
checked={wholeWord}
|
||||
onChange={(e) => setWholeWord(e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div className='flex flex-1 items-center justify-between gap-2 p-4'>
|
||||
<label htmlFor='scope-select' className='line-clamp-1 text-xs' title={_('Scope:')}>
|
||||
{_('Scope:')}
|
||||
</label>
|
||||
<Select
|
||||
className='not-eink:bg-gray-600 eink:bg-base-100 not-eink:text-white max-w-[85%]'
|
||||
value={scope}
|
||||
onChange={handleScopeChange}
|
||||
options={scopeOptions}
|
||||
/>
|
||||
</div>
|
||||
</Popup>
|
||||
</div>
|
||||
|
||||
@@ -141,7 +141,7 @@ const TranslatorPopup: React.FC<TranslatorPopupProps> = ({
|
||||
minHeight={popupHeight}
|
||||
maxHeight={720}
|
||||
position={position}
|
||||
className='grid h-full select-text grid-rows-[1fr,auto,1fr] bg-gray-600 text-white'
|
||||
className='not-eink:text-white grid h-full select-text grid-rows-[1fr,auto,1fr] bg-gray-600'
|
||||
triangleClassName='text-gray-600'
|
||||
onDismiss={onDismiss}
|
||||
>
|
||||
@@ -149,7 +149,7 @@ const TranslatorPopup: React.FC<TranslatorPopupProps> = ({
|
||||
<div className='mb-2 flex items-center justify-between'>
|
||||
<h1 className='text-sm font-normal'>{_('Original Text')}</h1>
|
||||
<Select
|
||||
className='bg-gray-600 text-white/75'
|
||||
className='not-eink:bg-gray-600 not-eink:text-white eink:bg-base-100'
|
||||
value={sourceLang}
|
||||
onChange={handleSourceLangChange}
|
||||
options={[
|
||||
@@ -167,7 +167,7 @@ const TranslatorPopup: React.FC<TranslatorPopupProps> = ({
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<p className='text-base text-white/90'>{text}</p>
|
||||
<p className='not-eink:text-white/90 text-base'>{text}</p>
|
||||
</div>
|
||||
|
||||
<div className='mx-4 flex-shrink-0 border-t border-gray-500/30'></div>
|
||||
@@ -176,7 +176,7 @@ const TranslatorPopup: React.FC<TranslatorPopupProps> = ({
|
||||
<div className='mb-2 flex items-center justify-between'>
|
||||
<h2 className='text-sm font-normal'>{_('Translated Text')}</h2>
|
||||
<Select
|
||||
className='bg-gray-600 text-white/75'
|
||||
className='not-eink:bg-gray-600 not-eink:text-white eink:bg-base-100'
|
||||
value={targetLang}
|
||||
onChange={handleTargetLangChange}
|
||||
options={[
|
||||
@@ -194,7 +194,7 @@ const TranslatorPopup: React.FC<TranslatorPopupProps> = ({
|
||||
{error ? (
|
||||
<p className='text-base text-red-600'>{error}</p>
|
||||
) : (
|
||||
<p className='text-base text-white/90'>
|
||||
<p className='not-eink:text-white/90 text-base'>
|
||||
{translation || _('No translation available.')}
|
||||
</p>
|
||||
)}
|
||||
@@ -211,7 +211,7 @@ const TranslatorPopup: React.FC<TranslatorPopupProps> = ({
|
||||
})}
|
||||
</div>
|
||||
<Select
|
||||
className='bg-gray-600 text-white/75'
|
||||
className='not-eink:bg-gray-600 not-eink:text-white eink:bg-base-100'
|
||||
value={provider}
|
||||
onChange={handleProviderChange}
|
||||
options={providers.map(({ name: value, label }) => ({ value, label }))}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Popup from '@/components/Popup';
|
||||
import { Position } from '@/utils/sel';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
interface WikipediaPopupProps {
|
||||
text: string;
|
||||
@@ -21,6 +22,7 @@ const WikipediaPopup: React.FC<WikipediaPopupProps> = ({
|
||||
popupHeight,
|
||||
onDismiss,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const isLoading = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -85,12 +87,14 @@ const WikipediaPopup: React.FC<WikipediaPopupProps> = ({
|
||||
|
||||
const errorDiv = document.createElement('div');
|
||||
const h1 = document.createElement('h1');
|
||||
h1.innerText = 'Error';
|
||||
h1.innerText = _('Error');
|
||||
|
||||
const errorMsg = document.createElement('p');
|
||||
errorMsg.innerHTML = `Unable to load the article. Try searching directly on <a href="https://${language}.wikipedia.org/w/index.php?search=${encodeURIComponent(
|
||||
query,
|
||||
)}" target="_blank" rel="noopener noreferrer" class="text-primary underline">Wikipedia</a>.`;
|
||||
errorMsg.innerHTML = _('Unable to load the article. Try searching directly on {{link}}.', {
|
||||
link: `<a href="https://${language}.wikipedia.org/w/index.php?search=${encodeURIComponent(
|
||||
query,
|
||||
)}" target="_blank" rel="noopener noreferrer" class="text-primary underline">Wikipedia</a>`,
|
||||
});
|
||||
|
||||
errorDiv.append(h1, errorMsg);
|
||||
main.appendChild(errorDiv);
|
||||
@@ -101,7 +105,7 @@ const WikipediaPopup: React.FC<WikipediaPopupProps> = ({
|
||||
const bookLang = typeof lang === 'string' ? lang : lang?.[0];
|
||||
const langCode = bookLang ? bookLang.split('-')[0]! : 'en';
|
||||
fetchSummary(text, langCode);
|
||||
}, [text, lang]);
|
||||
}, [_, text, lang]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Position } from '@/utils/sel';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import Popup from '@/components/Popup';
|
||||
|
||||
type Definition = {
|
||||
@@ -32,6 +33,7 @@ const WiktionaryPopup: React.FC<WiktionaryPopupProps> = ({
|
||||
popupHeight,
|
||||
onDismiss,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const [lookupWord, setLookupWord] = useState(word);
|
||||
const isLookingUp = useRef(false);
|
||||
|
||||
@@ -141,13 +143,15 @@ const WiktionaryPopup: React.FC<WiktionaryPopupProps> = ({
|
||||
'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.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>.`;
|
||||
p.innerHTML = _('Unable to load the word. Try searching directly on {{link}}.', {
|
||||
link: `<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);
|
||||
@@ -156,7 +160,7 @@ const WiktionaryPopup: React.FC<WiktionaryPopupProps> = ({
|
||||
|
||||
const langCode = typeof lang === 'string' ? lang : lang?.[0];
|
||||
fetchDefinitions(lookupWord, langCode);
|
||||
}, [lookupWord, lang]);
|
||||
}, [_, lookupWord, lang]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user