feat: synced scrolling for translation page

This commit is contained in:
one 2025-03-09 10:46:15 +08:00 committed by 亢奋猫
parent 219cea0c53
commit 02604c466d
6 changed files with 77 additions and 6 deletions

View File

@ -940,7 +940,9 @@
"output.placeholder": "Translation", "output.placeholder": "Translation",
"processing": "Translation in progress...", "processing": "Translation in progress...",
"title": "Translation", "title": "Translation",
"tooltip.newline": "Newline" "tooltip.newline": "Newline",
"scroll_sync.enable": "Enable synced scroll",
"scroll_sync.disable": "Disable synced scroll"
}, },
"tray": { "tray": {
"quit": "Quit", "quit": "Quit",

View File

@ -940,7 +940,9 @@
"output.placeholder": "翻訳", "output.placeholder": "翻訳",
"processing": "翻訳中...", "processing": "翻訳中...",
"title": "翻訳", "title": "翻訳",
"tooltip.newline": "改行" "tooltip.newline": "改行",
"scroll_sync.enable": "開啟滾動同步",
"scroll_sync.disable": "關閉滾動同步"
}, },
"tray": { "tray": {
"quit": "終了", "quit": "終了",

View File

@ -940,7 +940,9 @@
"output.placeholder": "Перевод", "output.placeholder": "Перевод",
"processing": "Перевод в процессе...", "processing": "Перевод в процессе...",
"title": "Перевод", "title": "Перевод",
"tooltip.newline": "Перевести" "tooltip.newline": "Перевести",
"scroll_sync.enable": "Включить синхронизацию прокрутки",
"scroll_sync.disable": "Отключить синхронизацию прокрутки"
}, },
"tray": { "tray": {
"quit": "Выйти", "quit": "Выйти",

View File

@ -940,7 +940,9 @@
"output.placeholder": "翻译", "output.placeholder": "翻译",
"processing": "翻译中...", "processing": "翻译中...",
"title": "翻译", "title": "翻译",
"tooltip.newline": "换行" "tooltip.newline": "换行",
"scroll_sync.enable": "开启滚动同步",
"scroll_sync.disable": "关闭滚动同步"
}, },
"tray": { "tray": {
"quit": "退出", "quit": "退出",

View File

@ -940,7 +940,9 @@
"output.placeholder": "翻譯", "output.placeholder": "翻譯",
"processing": "翻譯中...", "processing": "翻譯中...",
"title": "翻譯", "title": "翻譯",
"tooltip.newline": "換行" "tooltip.newline": "換行",
"scroll_sync.enable": "開啟滾動同步",
"scroll_sync.disable": "關閉滾動同步"
}, },
"tray": { "tray": {
"quit": "結束", "quit": "結束",

View File

@ -4,6 +4,7 @@ import {
HistoryOutlined, HistoryOutlined,
SendOutlined, SendOutlined,
SettingOutlined, SettingOutlined,
SyncOutlined,
WarningOutlined WarningOutlined
} from '@ant-design/icons' } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
@ -39,8 +40,11 @@ const TranslatePage: FC = () => {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState(false)
const [historyDrawerVisible, setHistoryDrawerVisible] = useState(false) const [historyDrawerVisible, setHistoryDrawerVisible] = useState(false)
const [isScrollSyncEnabled, setIsScrollSyncEnabled] = useState(true)
const contentContainerRef = useRef<HTMLDivElement>(null) const contentContainerRef = useRef<HTMLDivElement>(null)
const textAreaRef = useRef<TextAreaRef>(null) const textAreaRef = useRef<TextAreaRef>(null)
const outputTextRef = useRef<HTMLDivElement>(null)
const isProgrammaticScroll = useRef(false)
const translateHistory = useLiveQuery(() => db.translate_history.orderBy('createdAt').reverse().toArray(), []) 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<HTMLTextAreaElement>) => {
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<HTMLDivElement>) => {
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 ( return (
<Container id="translate-page"> <Container id="translate-page">
<Navbar> <Navbar>
@ -258,6 +306,16 @@ const TranslatePage: FC = () => {
options={[{ label: t('translate.any.language'), value: 'any' }]} options={[{ label: t('translate.any.language'), value: 'any' }]}
/> />
<SettingButton /> <SettingButton />
<Tooltip
mouseEnterDelay={0.5}
title={isScrollSyncEnabled ? t('translate.scroll_sync.disable') : t('translate.scroll_sync.enable')}>
<SyncOutlined
style={{
color: isScrollSyncEnabled ? 'var(--color-primary)' : 'var(--color-text-2)'
}}
onClick={toggleScrollSync}
/>
</Tooltip>
</Flex> </Flex>
<Tooltip <Tooltip
@ -288,6 +346,7 @@ const TranslatePage: FC = () => {
value={text} value={text}
onChange={(e) => setText(e.target.value)} onChange={(e) => setText(e.target.value)}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
onScroll={handleInputScroll}
disabled={loading} disabled={loading}
spellCheck={false} spellCheck={false}
allowClear allowClear
@ -322,7 +381,9 @@ const TranslatePage: FC = () => {
/> />
</OperationBar> </OperationBar>
<OutputText>{result || t('translate.output.placeholder')}</OutputText> <OutputText ref={outputTextRef} onScroll={handleOutputScroll}>
{result || t('translate.output.placeholder')}
</OutputText>
</OutputContainer> </OutputContainer>
</ContentContainer> </ContentContainer>
</Container> </Container>