forked from akai/readest
This commit is contained in:
@@ -418,5 +418,6 @@
|
||||
"Parallel Read": "القراءة المتزامنة",
|
||||
"Disable": "تعطيل",
|
||||
"Enable": "تمكين",
|
||||
"Upgrade to Readest Premium": "ترقية إلى Readest Premium"
|
||||
"Upgrade to Readest Premium": "ترقية إلى Readest Premium",
|
||||
"Show Source Text": "عرض النص المصدر"
|
||||
}
|
||||
|
||||
@@ -402,5 +402,6 @@
|
||||
"Parallel Read": "Paralleles Lesen",
|
||||
"Disable": "Deaktivieren",
|
||||
"Enable": "Aktivieren",
|
||||
"Upgrade to Readest Premium": "Upgrade auf Readest Premium"
|
||||
"Upgrade to Readest Premium": "Upgrade auf Readest Premium",
|
||||
"Show Source Text": "Quelltext anzeigen"
|
||||
}
|
||||
|
||||
@@ -402,5 +402,6 @@
|
||||
"Cloud Sync Storage": "Χώρος συγχρονισμού cloud",
|
||||
"Disable": "Απενεργοποίηση",
|
||||
"Enable": "Ενεργοποίηση",
|
||||
"Upgrade to Readest Premium": "Αναβάθμιση στο Readest Premium"
|
||||
"Upgrade to Readest Premium": "Αναβάθμιση στο Readest Premium",
|
||||
"Show Source Text": "Εμφάνιση αρχικού κειμένου"
|
||||
}
|
||||
|
||||
@@ -406,5 +406,6 @@
|
||||
"Cloud Sync Storage": "Almacenamiento en la nube",
|
||||
"Disable": "Desactivar",
|
||||
"Enable": "Activar",
|
||||
"Upgrade to Readest Premium": "Actualizar a Readest Premium"
|
||||
"Upgrade to Readest Premium": "Actualizar a Readest Premium",
|
||||
"Show Source Text": "Mostrar texto fuente"
|
||||
}
|
||||
|
||||
@@ -406,5 +406,6 @@
|
||||
"Cloud Sync Storage": "Stockage de synchronisation cloud",
|
||||
"Disable": "Désactiver",
|
||||
"Enable": "Activer",
|
||||
"Upgrade to Readest Premium": "Passer à Readest Premium"
|
||||
"Upgrade to Readest Premium": "Passer à Readest Premium",
|
||||
"Show Source Text": "Afficher le texte source"
|
||||
}
|
||||
|
||||
@@ -402,5 +402,6 @@
|
||||
"Cloud Sync Storage": "क्लाउड सिंक स्टोरेज",
|
||||
"Disable": "अक्षम करें",
|
||||
"Enable": "सक्षम करें",
|
||||
"Upgrade to Readest Premium": "Readest प्रीमियम में अपग्रेड करें"
|
||||
"Upgrade to Readest Premium": "Readest प्रीमियम में अपग्रेड करें",
|
||||
"Show Source Text": "मूल पाठ दिखाएं"
|
||||
}
|
||||
|
||||
@@ -398,5 +398,6 @@
|
||||
"Cloud Sync Storage": "Ruang cloud",
|
||||
"Disable": "Nonaktifkan",
|
||||
"Enable": "Aktifkan",
|
||||
"Upgrade to Readest Premium": "Tingkatkan ke Readest Premium"
|
||||
"Upgrade to Readest Premium": "Tingkatkan ke Readest Premium",
|
||||
"Show Source Text": "Perlihatkan Teks Sumber"
|
||||
}
|
||||
|
||||
@@ -406,5 +406,6 @@
|
||||
"Cloud Sync Storage": "Spazio cloud",
|
||||
"Disable": "Disabilita",
|
||||
"Enable": "Abilita",
|
||||
"Upgrade to Readest Premium": "Aggiorna a Readest Premium"
|
||||
"Upgrade to Readest Premium": "Aggiorna a Readest Premium",
|
||||
"Show Source Text": "Mostra Testo Originale"
|
||||
}
|
||||
|
||||
@@ -398,5 +398,6 @@
|
||||
"Cloud Sync Storage": "クラウド同期容量",
|
||||
"Disable": "無効にする",
|
||||
"Enable": "有効にする",
|
||||
"Upgrade to Readest Premium": "Readest Premiumにアップグレード"
|
||||
"Upgrade to Readest Premium": "Readest Premiumにアップグレード",
|
||||
"Show Source Text": "原文を表示"
|
||||
}
|
||||
|
||||
@@ -398,5 +398,6 @@
|
||||
"Cloud Sync Storage": "클라우드 저장 공간",
|
||||
"Disable": "비활성화",
|
||||
"Enable": "활성화",
|
||||
"Upgrade to Readest Premium": "Readest Premium으로 업그레이드"
|
||||
"Upgrade to Readest Premium": "Readest Premium으로 업그레이드",
|
||||
"Show Source Text": "원본 텍스트 표시"
|
||||
}
|
||||
|
||||
@@ -402,5 +402,6 @@
|
||||
"Cloud Sync Storage": "Cloud Sync Opslag",
|
||||
"Disable": "Uitschakelen",
|
||||
"Enable": "Inschakelen",
|
||||
"Upgrade to Readest Premium": "Upgrade naar Readest Premium"
|
||||
"Upgrade to Readest Premium": "Upgrade naar Readest Premium",
|
||||
"Show Source Text": "Toon brontekst"
|
||||
}
|
||||
|
||||
@@ -410,5 +410,6 @@
|
||||
"Cloud Sync Storage": "Miejsce w chmurze",
|
||||
"Disable": "Wyłącz",
|
||||
"Enable": "Włącz",
|
||||
"Upgrade to Readest Premium": "Uaktualnij do Readest Premium"
|
||||
"Upgrade to Readest Premium": "Uaktualnij do Readest Premium",
|
||||
"Show Source Text": "Pokaż tekst źródłowy"
|
||||
}
|
||||
|
||||
@@ -406,5 +406,6 @@
|
||||
"Cloud Sync Storage": "Armazenamento na Nuvem",
|
||||
"Disable": "Desativar",
|
||||
"Enable": "Ativar",
|
||||
"Upgrade to Readest Premium": "Atualizar para Readest Premium"
|
||||
"Upgrade to Readest Premium": "Atualizar para Readest Premium",
|
||||
"Show Source Text": "Mostrar Texto Fonte"
|
||||
}
|
||||
|
||||
@@ -410,5 +410,6 @@
|
||||
"Cloud Sync Storage": "Облачное хранилище",
|
||||
"Disable": "Отключить",
|
||||
"Enable": "Включить",
|
||||
"Upgrade to Readest Premium": "Обновить до Readest Premium"
|
||||
"Upgrade to Readest Premium": "Обновить до Readest Premium",
|
||||
"Show Source Text": "Показать исходный текст"
|
||||
}
|
||||
|
||||
@@ -402,5 +402,6 @@
|
||||
"Cloud Sync Storage": "Bulut Senkronizasyon Depolama",
|
||||
"Disable": "Devre Dışı Bırak",
|
||||
"Enable": "Etkinleştir",
|
||||
"Upgrade to Readest Premium": "Readest Premium’a Yükselt"
|
||||
"Upgrade to Readest Premium": "Readest Premium’a Yükselt",
|
||||
"Show Source Text": "Kaynak Metni Göster"
|
||||
}
|
||||
|
||||
@@ -410,5 +410,6 @@
|
||||
"Cloud Sync Storage": "Облачне хранилище",
|
||||
"Disable": "Вимкнути",
|
||||
"Enable": "Увімкнути",
|
||||
"Upgrade to Readest Premium": "Оновити до Readest Premium"
|
||||
"Upgrade to Readest Premium": "Оновити до Readest Premium",
|
||||
"Show Source Text": "Показати вихідний текст"
|
||||
}
|
||||
|
||||
@@ -398,5 +398,6 @@
|
||||
"Cloud Sync Storage": "Lưu trữ đồng bộ đám mây",
|
||||
"Disable": "Tắt",
|
||||
"Enable": "Bật",
|
||||
"Upgrade to Readest Premium": "Đăng ký Readest Premium"
|
||||
"Upgrade to Readest Premium": "Đăng ký Readest Premium",
|
||||
"Show Source Text": "Hiển thị văn bản gốc"
|
||||
}
|
||||
|
||||
@@ -381,7 +381,7 @@
|
||||
"Need help? Contact our support team at support@readest.com": "需要帮助?请联系支持团队:support@readest.com",
|
||||
"Free Plan": "免费套餐",
|
||||
"month": "月",
|
||||
"Unlimited Text-to-Speech Hours": "无限文本转语音时长",
|
||||
"Unlimited Text-to-Speech Hours": "AI 朗读不限时长",
|
||||
"DeepL Free Access (10K characters/day)": "DeepL 免费额度(每日1万字符)",
|
||||
"AI Translations (per day)": "AI 翻译(每日)",
|
||||
"Plus Plan": "Plus 套餐",
|
||||
@@ -398,5 +398,6 @@
|
||||
"Cloud Sync Storage": "云同步空间",
|
||||
"Disable": "禁用",
|
||||
"Enable": "启用",
|
||||
"Upgrade to Readest Premium": "升级到 Readest Premium"
|
||||
"Upgrade to Readest Premium": "升级到 Readest Premium",
|
||||
"Show Source Text": "显示原文"
|
||||
}
|
||||
|
||||
@@ -381,7 +381,7 @@
|
||||
"Need help? Contact our support team at support@readest.com": "需要協助?請聯絡支援團隊:support@readest.com",
|
||||
"Free Plan": "免費方案",
|
||||
"month": "月",
|
||||
"Unlimited Text-to-Speech Hours": "無限文字轉語音時數",
|
||||
"Unlimited Text-to-Speech Hours": "AI 朗讀不限時長",
|
||||
"DeepL Free Access (10K characters/day)": "DeepL 免費使用(每日1萬字)",
|
||||
"AI Translations (per day)": "AI 翻譯(每日)",
|
||||
"Plus Plan": "Plus 方案",
|
||||
@@ -398,5 +398,6 @@
|
||||
"Cloud Sync Storage": "雲同步空間",
|
||||
"Disable": "禁用",
|
||||
"Enable": "啟用",
|
||||
"Upgrade to Readest Premium": "升級到 Readest Premium"
|
||||
"Upgrade to Readest Premium": "升級到 Readest Premium",
|
||||
"Show Source Text": "顯示原文"
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { usePagination } from '../hooks/usePagination';
|
||||
import { useFoliateEvents } from '../hooks/useFoliateEvents';
|
||||
import { useProgressSync } from '../hooks/useProgressSync';
|
||||
import { useProgressAutoSave } from '../hooks/useProgressAutoSave';
|
||||
import { getStyles, transformStylesheet } from '@/utils/style';
|
||||
import { applyTranslationStyles, getStyles, transformStylesheet } from '@/utils/style';
|
||||
import { mountAdditionalFonts } from '@/utils/font';
|
||||
import { getBookDirFromLanguage, getBookDirFromWritingMode } from '@/utils/book';
|
||||
import { useUICSS } from '@/hooks/useUICSS';
|
||||
@@ -242,6 +242,7 @@ const FoliateViewer: React.FC<{
|
||||
const height = viewHeight - insets.top - insets.bottom;
|
||||
book.transformTarget?.addEventListener('data', getDocTransformHandler({ width, height }));
|
||||
view.renderer.setStyles?.(getStyles(viewSettings));
|
||||
applyTranslationStyles(viewSettings);
|
||||
|
||||
const animated = viewSettings.animated!;
|
||||
const maxColumnCount = viewSettings.maxColumnCount!;
|
||||
|
||||
@@ -16,6 +16,7 @@ import { parseOpenWithFiles } from '@/helpers/openWith';
|
||||
import { tauriHandleClose, tauriHandleOnCloseWindow } from '@/utils/window';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { uniqueId } from '@/utils/misc';
|
||||
import { throttle } from '@/utils/throttle';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { navigateToLibrary } from '@/utils/nav';
|
||||
import { BOOK_IDS_SEPARATOR } from '@/services/constants';
|
||||
@@ -84,9 +85,11 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
|
||||
|
||||
if (isTauriAppPlatform()) tauriHandleOnCloseWindow(handleCloseBooks);
|
||||
window.addEventListener('beforeunload', handleCloseBooks);
|
||||
eventDispatcher.on('beforereload', handleCloseBooks);
|
||||
eventDispatcher.on('quit-app', handleCloseBooks);
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', handleCloseBooks);
|
||||
eventDispatcher.off('beforereload', handleCloseBooks);
|
||||
eventDispatcher.off('quit-app', handleCloseBooks);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -121,11 +124,11 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
|
||||
navigateToLibrary(router);
|
||||
};
|
||||
|
||||
const handleCloseBooks = async () => {
|
||||
const handleCloseBooks = throttle(async () => {
|
||||
const settings = useSettingsStore.getState().settings;
|
||||
await Promise.all(bookKeys.map((key) => saveConfigAndCloseBook(key)));
|
||||
await saveSettings(envConfig, settings);
|
||||
};
|
||||
}, 500);
|
||||
|
||||
const handleCloseBooksToLibrary = () => {
|
||||
handleCloseBooks();
|
||||
|
||||
@@ -249,7 +249,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
try {
|
||||
Promise.all(annotations.map((annotation) => view?.addAnnotation(annotation)));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.warn(e);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [progress]);
|
||||
|
||||
@@ -4,12 +4,12 @@ import { useReaderStore } from '@/store/readerStore';
|
||||
import { useDeviceControlStore } from '@/store/deviceStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useResetViewSettings } from '../../hooks/useResetSettings';
|
||||
import { getStyles } from '@/utils/style';
|
||||
import { saveAndReload } from '@/utils/reload';
|
||||
import { getMaxInlineSize } from '@/utils/config';
|
||||
import { saveViewSettings } from '../../utils/viewSettingsHelper';
|
||||
import { RELOAD_BEFREE_SAVED_TIMEOUT_MS } from '@/services/constants';
|
||||
import { SettingsPanelPanelProp } from './SettingsDialog';
|
||||
import { useResetViewSettings } from '../../hooks/useResetSettings';
|
||||
import NumberInput from './NumberInput';
|
||||
|
||||
const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset }) => {
|
||||
@@ -108,7 +108,7 @@ const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRes
|
||||
useEffect(() => {
|
||||
if (viewSettings.allowScript === allowScript) return;
|
||||
saveViewSettings(envConfig, bookKey, 'allowScript', allowScript, true, false);
|
||||
setTimeout(() => window.location.reload(), RELOAD_BEFREE_SAVED_TIMEOUT_MS);
|
||||
saveAndReload();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [allowScript]);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getTranslators } from '@/services/translators';
|
||||
import { TRANSLATED_LANGS } from '@/services/constants';
|
||||
import { SettingsPanelPanelProp } from './SettingsDialog';
|
||||
import { useResetViewSettings } from '../../hooks/useResetSettings';
|
||||
import { saveAndReload } from '@/utils/reload';
|
||||
import { initDayjs } from '@/utils/time';
|
||||
import Select from '@/components/Select';
|
||||
|
||||
@@ -24,6 +25,7 @@ const LangPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
const [translationEnabled, setTranslationEnabled] = useState(viewSettings.translationEnabled!);
|
||||
const [translationProvider, setTranslationProvider] = useState(viewSettings.translationProvider!);
|
||||
const [translateTargetLang, setTranslateTargetLang] = useState(viewSettings.translateTargetLang!);
|
||||
const [showTranslateSource, setShowTranslateSource] = useState(viewSettings.showTranslateSource!);
|
||||
|
||||
const resetToDefaults = useResetViewSettings();
|
||||
|
||||
@@ -127,9 +129,19 @@ const LangPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
saveViewSettings(envConfig, bookKey, 'translationEnabled', translationEnabled, true, false);
|
||||
viewSettings.translationEnabled = translationEnabled;
|
||||
setViewSettings(bookKey, { ...viewSettings });
|
||||
if (!showTranslateSource && !translationEnabled) {
|
||||
saveAndReload();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [translationEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showTranslateSource === viewSettings.showTranslateSource) return;
|
||||
saveViewSettings(envConfig, bookKey, 'showTranslateSource', showTranslateSource, false, false);
|
||||
saveAndReload();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [showTranslateSource]);
|
||||
|
||||
return (
|
||||
<div className={clsx('my-4 w-full space-y-6')}>
|
||||
<div className='w-full'>
|
||||
@@ -162,6 +174,17 @@ const LangPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='config-item'>
|
||||
<span className=''>{_('Show Source Text')}</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='toggle'
|
||||
checked={showTranslateSource}
|
||||
disabled={!translationEnabled}
|
||||
onChange={() => setShowTranslateSource(!showTranslateSource)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='config-item'>
|
||||
<span className=''>{_('Translation Service')}</span>
|
||||
<Select
|
||||
|
||||
@@ -11,11 +11,12 @@ import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useResetViewSettings } from '../../hooks/useResetSettings';
|
||||
import { isCJKEnv } from '@/utils/misc';
|
||||
import { getStyles } from '@/utils/style';
|
||||
import { saveAndReload } from '@/utils/reload';
|
||||
import { getMaxInlineSize } from '@/utils/config';
|
||||
import { lockScreenOrientation } from '@/utils/bridge';
|
||||
import { getBookDirFromWritingMode, getBookLangCode } from '@/utils/book';
|
||||
import { MIGHT_BE_RTL_LANGS, RELOAD_BEFREE_SAVED_TIMEOUT_MS } from '@/services/constants';
|
||||
import { saveViewSettings } from '../../utils/viewSettingsHelper';
|
||||
import { getBookDirFromWritingMode, getBookLangCode } from '@/utils/book';
|
||||
import { MIGHT_BE_RTL_LANGS } from '@/services/constants';
|
||||
import { SettingsPanelPanelProp } from './SettingsDialog';
|
||||
import NumberInput from './NumberInput';
|
||||
|
||||
@@ -280,7 +281,7 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
|
||||
(['horizontal-rl', 'vertical-rl'].includes(writingMode) ||
|
||||
['horizontal-rl', 'vertical-rl'].includes(prevWritingMode))
|
||||
) {
|
||||
setTimeout(() => window.location.reload(), RELOAD_BEFREE_SAVED_TIMEOUT_MS);
|
||||
saveAndReload();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [writingMode]);
|
||||
|
||||
@@ -57,7 +57,7 @@ const TOCItemView = React.memo<{
|
||||
);
|
||||
|
||||
return (
|
||||
<span
|
||||
<div
|
||||
role='treeitem'
|
||||
tabIndex={-1}
|
||||
onClick={item.href ? handleClickItem : undefined}
|
||||
@@ -87,7 +87,7 @@ const TOCItemView = React.memo<{
|
||||
{createExpanderIcon(flatItem.isExpanded || false)}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
<div
|
||||
className='ms-2 truncate text-ellipsis'
|
||||
style={{
|
||||
maxWidth: 'calc(100% - 24px)',
|
||||
@@ -96,13 +96,13 @@ const TOCItemView = React.memo<{
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
</div>
|
||||
{item.location && (
|
||||
<span className='text-base-content/50 ms-auto ps-1 text-xs sm:pe-1'>
|
||||
<div className='text-base-content/50 ms-auto ps-1 text-xs sm:pe-1'>
|
||||
{item.location.current + 1}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ const TOCView: React.FC<{
|
||||
const vitualListRef = useRef<VirtualList | null>(null);
|
||||
const staticListRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useTextTranslation(bookKey, containerRef.current);
|
||||
useTextTranslation(bookKey, containerRef.current, false, 'translation-target-toc');
|
||||
|
||||
useEffect(() => {
|
||||
const updateHeight = () => {
|
||||
|
||||
@@ -7,7 +7,12 @@ import { walkTextNodes } from '@/utils/walk';
|
||||
import { debounce } from '@/utils/debounce';
|
||||
import { getLocale } from '@/utils/misc';
|
||||
|
||||
export function useTextTranslation(bookKey: string, view: FoliateView | HTMLElement | null) {
|
||||
export function useTextTranslation(
|
||||
bookKey: string,
|
||||
view: FoliateView | HTMLElement | null,
|
||||
widthLineBreak = false,
|
||||
targetBlockClassName = 'translation-target-block',
|
||||
) {
|
||||
const { getViewSettings, getViewState, getProgress } = useReaderStore();
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
const viewState = getViewState(bookKey);
|
||||
@@ -16,6 +21,7 @@ export function useTextTranslation(bookKey: string, view: FoliateView | HTMLElem
|
||||
const enabled = useRef(viewSettings?.translationEnabled);
|
||||
const [provider, setProvider] = useState(viewSettings?.translationProvider);
|
||||
const [targetLang, setTargetLang] = useState(viewSettings?.translateTargetLang);
|
||||
const showTranslateSourceRef = useRef(viewSettings?.showTranslateSource);
|
||||
|
||||
const { translate } = useTranslator({
|
||||
provider,
|
||||
@@ -123,10 +129,59 @@ export function useTextTranslation(bookKey: string, view: FoliateView | HTMLElem
|
||||
const text = el.textContent?.replaceAll('\n', '').trim();
|
||||
if (!text) return;
|
||||
|
||||
if (el.querySelector('.translation-target')) {
|
||||
if (el.classList.contains('translation-target')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updateSourceNodes = (element: HTMLElement) => {
|
||||
const hasDirectText = Array.from(element.childNodes).some(
|
||||
(node) => node.nodeType === Node.TEXT_NODE && node.textContent?.trim() !== '',
|
||||
);
|
||||
if (hasDirectText) {
|
||||
element.classList.add('translation-source');
|
||||
|
||||
const textNodes = Array.from(element.childNodes).filter(
|
||||
(node) => node.nodeType === Node.TEXT_NODE && node.textContent?.trim() !== '',
|
||||
);
|
||||
|
||||
if (!element.hasAttribute('original-text-stored')) {
|
||||
element.setAttribute(
|
||||
'original-text-nodes',
|
||||
JSON.stringify(textNodes.map((node) => node.textContent)),
|
||||
);
|
||||
element.setAttribute('original-text-stored', 'true');
|
||||
}
|
||||
}
|
||||
const isSource = element.classList.contains('translation-source');
|
||||
if (isSource) {
|
||||
const textNodes = Array.from(element.childNodes).filter(
|
||||
(node) => node.nodeType === Node.TEXT_NODE,
|
||||
) as Text[];
|
||||
|
||||
if (showTranslateSourceRef.current) {
|
||||
const originalTexts = JSON.parse(element.getAttribute('original-text-nodes') || '[]');
|
||||
textNodes.forEach((textNode, index) => {
|
||||
if (originalTexts[index] !== undefined) {
|
||||
textNode.textContent = originalTexts[index];
|
||||
}
|
||||
});
|
||||
} else {
|
||||
textNodes.forEach((textNode) => {
|
||||
textNode.textContent = '';
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const child of Array.from(element.childNodes)) {
|
||||
if (child.nodeType !== Node.ELEMENT_NODE) continue;
|
||||
const node = child as HTMLElement;
|
||||
if (!node.classList.contains('translation-target')) {
|
||||
updateSourceNodes(node);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
updateSourceNodes(el);
|
||||
|
||||
try {
|
||||
const translated = await translateRef.current([text]);
|
||||
const translatedText = translated[0];
|
||||
@@ -136,9 +191,12 @@ export function useTextTranslation(bookKey: string, view: FoliateView | HTMLElem
|
||||
wrapper.className = `translation-target ${!enabled.current ? 'hidden' : ''}`;
|
||||
wrapper.setAttribute('translation-element-mark', '1');
|
||||
wrapper.setAttribute('lang', targetLang || getLocale());
|
||||
if (widthLineBreak) {
|
||||
wrapper.appendChild(document.createElement('br'));
|
||||
}
|
||||
|
||||
const blockWrapper = document.createElement('font');
|
||||
blockWrapper.className = 'translation-target translation-block-wrapper';
|
||||
blockWrapper.className = `translation-target ${targetBlockClassName}`;
|
||||
|
||||
const inner = document.createElement('font');
|
||||
inner.className = 'translation-target target-inner target-inner-theme-none';
|
||||
@@ -201,7 +259,7 @@ export function useTextTranslation(bookKey: string, view: FoliateView | HTMLElem
|
||||
}
|
||||
}
|
||||
}, 500),
|
||||
[],
|
||||
[translateElement],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -218,6 +276,8 @@ export function useTextTranslation(bookKey: string, view: FoliateView | HTMLElem
|
||||
const enabledChanged = enabled.current !== viewSettings.translationEnabled;
|
||||
const providerChanged = provider !== viewSettings.translationProvider;
|
||||
const targetLangChanged = targetLang !== viewSettings.translateTargetLang;
|
||||
const showTranslateSourceChanged =
|
||||
showTranslateSourceRef.current !== viewSettings.showTranslateSource;
|
||||
|
||||
if (enabledChanged) {
|
||||
enabled.current = viewSettings.translationEnabled;
|
||||
@@ -231,12 +291,16 @@ export function useTextTranslation(bookKey: string, view: FoliateView | HTMLElem
|
||||
setTargetLang(viewSettings.translateTargetLang);
|
||||
}
|
||||
|
||||
if (showTranslateSourceChanged) {
|
||||
showTranslateSourceRef.current = viewSettings.showTranslateSource;
|
||||
}
|
||||
|
||||
if (enabledChanged) {
|
||||
toggleTranslationVisibility(viewSettings.translationEnabled);
|
||||
if (enabled.current) {
|
||||
observeTextNodes();
|
||||
}
|
||||
} else if (providerChanged || targetLangChanged) {
|
||||
} else if (providerChanged || targetLangChanged || showTranslateSourceChanged) {
|
||||
updateTranslation();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
@@ -172,6 +172,7 @@ export const DEFAULT_TRANSLATOR_CONFIG: TranslatorConfig = {
|
||||
translationEnabled: false,
|
||||
translationProvider: 'deepl',
|
||||
translateTargetLang: '',
|
||||
showTranslateSource: true,
|
||||
};
|
||||
|
||||
export const DEFAULT_SCREEN_CONFIG: ScreenConfig = {
|
||||
@@ -542,7 +543,7 @@ export const SYNC_NOTES_INTERVAL_SEC = 5;
|
||||
export const SYNC_BOOKS_INTERVAL_SEC = 5;
|
||||
export const CHECK_UPDATE_INTERVAL_SEC = 24 * 60 * 60;
|
||||
|
||||
export const RELOAD_BEFREE_SAVED_TIMEOUT_MS = 300;
|
||||
export const RELOAD_BEFORE_SAVED_TIMEOUT_MS = 300;
|
||||
|
||||
export const MAX_ZOOM_LEVEL = 500;
|
||||
export const MIN_ZOOM_LEVEL = 50;
|
||||
|
||||
@@ -156,6 +156,7 @@ export interface TranslatorConfig {
|
||||
translationEnabled: boolean;
|
||||
translationProvider: string;
|
||||
translateTargetLang: string;
|
||||
showTranslateSource: boolean;
|
||||
}
|
||||
|
||||
export interface ScreenConfig {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { RELOAD_BEFORE_SAVED_TIMEOUT_MS } from '@/services/constants';
|
||||
import { eventDispatcher } from './event';
|
||||
|
||||
export const saveAndReload = async () => {
|
||||
await eventDispatcher.dispatch('beforereload');
|
||||
setTimeout(() => window.location.reload(), RELOAD_BEFORE_SAVED_TIMEOUT_MS);
|
||||
};
|
||||
@@ -333,14 +333,23 @@ export const getFootnoteStyles = () => `
|
||||
}
|
||||
`;
|
||||
|
||||
const getTranslationStyles = () => `
|
||||
.translation-block-wrapper {
|
||||
display: block !important;
|
||||
margin: 0.5em 0 !important;
|
||||
const getTranslationStyles = (showSource: boolean) => `
|
||||
.translation-source {
|
||||
}
|
||||
.translation-target {
|
||||
}
|
||||
.translation-target.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
.translation-target-block {
|
||||
display: block !important;
|
||||
${showSource ? 'margin: 0.5em 0 !important;' : ''}
|
||||
}
|
||||
.translation-target-toc {
|
||||
display: block !important;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
`;
|
||||
|
||||
export interface ThemeCode {
|
||||
@@ -424,11 +433,26 @@ export const getStyles = (viewSettings: ViewSettings, themeCode?: ThemeCode) =>
|
||||
viewSettings.invertImgColorInDark!,
|
||||
themeCode,
|
||||
);
|
||||
const translationStyles = getTranslationStyles();
|
||||
const translationStyles = getTranslationStyles(viewSettings.showTranslateSource!);
|
||||
const userStylesheet = viewSettings.userStylesheet!;
|
||||
return `${layoutStyles}\n${fontStyles}\n${colorStyles}\n${translationStyles}\n${userStylesheet}`;
|
||||
};
|
||||
|
||||
export const applyTranslationStyles = (viewSettings: ViewSettings) => {
|
||||
const styleId = 'translation-styles';
|
||||
|
||||
const existingStyle = document.getElementById(styleId);
|
||||
if (existingStyle) {
|
||||
existingStyle.remove();
|
||||
}
|
||||
|
||||
const styleElement = document.createElement('style');
|
||||
styleElement.id = styleId;
|
||||
styleElement.textContent = getTranslationStyles(viewSettings.showTranslateSource);
|
||||
|
||||
document.head.appendChild(styleElement);
|
||||
};
|
||||
|
||||
export const transformStylesheet = (vw: number, vh: number, css: string) => {
|
||||
const isMobile = ['ios', 'android'].includes(getOSPlatform());
|
||||
const fontScale = isMobile ? 1.25 : 1;
|
||||
|
||||
@@ -22,9 +22,15 @@ export const walkTextNodes = (root: HTMLElement): HTMLElement[] => {
|
||||
}
|
||||
const hasDirectText =
|
||||
child.childNodes &&
|
||||
Array.from(child.childNodes).some(
|
||||
(node) => node.nodeType === Node.TEXT_NODE && node.textContent?.trim(),
|
||||
);
|
||||
Array.from(child.childNodes).some((node) => {
|
||||
if (node.nodeType === Node.TEXT_NODE && node.textContent?.trim()) {
|
||||
return true;
|
||||
}
|
||||
if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === 'SPAN') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (child.children.length === 0 && child.textContent?.trim()) {
|
||||
elements.push(child);
|
||||
} else if (hasDirectText) {
|
||||
|
||||
+1
-1
Submodule packages/foliate-js updated: e6db07ada9...73a6223b2e
Reference in New Issue
Block a user