diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index dfcc0417..37625728 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -940,7 +940,9 @@ "output.placeholder": "Translation", "processing": "Translation in progress...", "title": "Translation", - "tooltip.newline": "Newline" + "tooltip.newline": "Newline", + "scroll_sync.enable": "Enable synced scroll", + "scroll_sync.disable": "Disable synced scroll" }, "tray": { "quit": "Quit", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index c7dd1569..e85031d6 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -940,7 +940,9 @@ "output.placeholder": "翻訳", "processing": "翻訳中...", "title": "翻訳", - "tooltip.newline": "改行" + "tooltip.newline": "改行", + "scroll_sync.enable": "開啟滾動同步", + "scroll_sync.disable": "關閉滾動同步" }, "tray": { "quit": "終了", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index d5554ca7..b5f90b92 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -940,7 +940,9 @@ "output.placeholder": "Перевод", "processing": "Перевод в процессе...", "title": "Перевод", - "tooltip.newline": "Перевести" + "tooltip.newline": "Перевести", + "scroll_sync.enable": "Включить синхронизацию прокрутки", + "scroll_sync.disable": "Отключить синхронизацию прокрутки" }, "tray": { "quit": "Выйти", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 0d9e5edb..c3f194be 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -940,7 +940,9 @@ "output.placeholder": "翻译", "processing": "翻译中...", "title": "翻译", - "tooltip.newline": "换行" + "tooltip.newline": "换行", + "scroll_sync.enable": "开启滚动同步", + "scroll_sync.disable": "关闭滚动同步" }, "tray": { "quit": "退出", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 8a564afa..746a7fd4 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -940,7 +940,9 @@ "output.placeholder": "翻譯", "processing": "翻譯中...", "title": "翻譯", - "tooltip.newline": "換行" + "tooltip.newline": "換行", + "scroll_sync.enable": "開啟滾動同步", + "scroll_sync.disable": "關閉滾動同步" }, "tray": { "quit": "結束", diff --git a/src/renderer/src/pages/translate/TranslatePage.tsx b/src/renderer/src/pages/translate/TranslatePage.tsx index 81fb54c8..e8d0a1dd 100644 --- a/src/renderer/src/pages/translate/TranslatePage.tsx +++ b/src/renderer/src/pages/translate/TranslatePage.tsx @@ -4,6 +4,7 @@ import { HistoryOutlined, SendOutlined, SettingOutlined, + SyncOutlined, WarningOutlined } from '@ant-design/icons' import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' @@ -39,8 +40,11 @@ const TranslatePage: FC = () => { const [loading, setLoading] = useState(false) const [copied, setCopied] = useState(false) const [historyDrawerVisible, setHistoryDrawerVisible] = useState(false) + const [isScrollSyncEnabled, setIsScrollSyncEnabled] = useState(true) const contentContainerRef = useRef(null) const textAreaRef = useRef(null) + const outputTextRef = useRef(null) + const isProgrammaticScroll = useRef(false) const translateHistory = useLiveQuery(() => db.translate_history.orderBy('createdAt').reverse().toArray(), []) @@ -182,6 +186,50 @@ const TranslatePage: FC = () => { ) } + // Handle input area scroll event + const handleInputScroll = (e: React.UIEvent) => { + if (!isScrollSyncEnabled || !outputTextRef.current || isProgrammaticScroll.current) return + + isProgrammaticScroll.current = true + + const inputEl = e.currentTarget + const outputEl = outputTextRef.current + + // Calculate scroll position by ratio + const inputScrollRatio = inputEl.scrollTop / (inputEl.scrollHeight - inputEl.clientHeight || 1) + const outputScrollPosition = inputScrollRatio * (outputEl.scrollHeight - outputEl.clientHeight || 1) + + outputEl.scrollTop = outputScrollPosition + + requestAnimationFrame(() => { + isProgrammaticScroll.current = false + }) + } + + // Handle output area scroll event + const handleOutputScroll = (e: React.UIEvent) => { + const inputEl = textAreaRef.current?.resizableTextArea?.textArea + if (!isScrollSyncEnabled || !inputEl || isProgrammaticScroll.current) return + + isProgrammaticScroll.current = true + + const outputEl = e.currentTarget + + // Calculate scroll position by ratio + const outputScrollRatio = outputEl.scrollTop / (outputEl.scrollHeight - outputEl.clientHeight || 1) + const inputScrollPosition = outputScrollRatio * (inputEl.scrollHeight - inputEl.clientHeight || 1) + + inputEl.scrollTop = inputScrollPosition + + requestAnimationFrame(() => { + isProgrammaticScroll.current = false + }) + } + + const toggleScrollSync = () => { + setIsScrollSyncEnabled(!isScrollSyncEnabled) + } + return ( @@ -258,6 +306,16 @@ const TranslatePage: FC = () => { options={[{ label: t('translate.any.language'), value: 'any' }]} /> + + + { value={text} onChange={(e) => setText(e.target.value)} onKeyDown={onKeyDown} + onScroll={handleInputScroll} disabled={loading} spellCheck={false} allowClear @@ -322,7 +381,9 @@ const TranslatePage: FC = () => { /> - {result || t('translate.output.placeholder')} + + {result || t('translate.output.placeholder')} +