feat: synced scrolling for translation page
This commit is contained in:
parent
219cea0c53
commit
02604c466d
@ -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",
|
||||
|
||||
@ -940,7 +940,9 @@
|
||||
"output.placeholder": "翻訳",
|
||||
"processing": "翻訳中...",
|
||||
"title": "翻訳",
|
||||
"tooltip.newline": "改行"
|
||||
"tooltip.newline": "改行",
|
||||
"scroll_sync.enable": "開啟滾動同步",
|
||||
"scroll_sync.disable": "關閉滾動同步"
|
||||
},
|
||||
"tray": {
|
||||
"quit": "終了",
|
||||
|
||||
@ -940,7 +940,9 @@
|
||||
"output.placeholder": "Перевод",
|
||||
"processing": "Перевод в процессе...",
|
||||
"title": "Перевод",
|
||||
"tooltip.newline": "Перевести"
|
||||
"tooltip.newline": "Перевести",
|
||||
"scroll_sync.enable": "Включить синхронизацию прокрутки",
|
||||
"scroll_sync.disable": "Отключить синхронизацию прокрутки"
|
||||
},
|
||||
"tray": {
|
||||
"quit": "Выйти",
|
||||
|
||||
@ -940,7 +940,9 @@
|
||||
"output.placeholder": "翻译",
|
||||
"processing": "翻译中...",
|
||||
"title": "翻译",
|
||||
"tooltip.newline": "换行"
|
||||
"tooltip.newline": "换行",
|
||||
"scroll_sync.enable": "开启滚动同步",
|
||||
"scroll_sync.disable": "关闭滚动同步"
|
||||
},
|
||||
"tray": {
|
||||
"quit": "退出",
|
||||
|
||||
@ -940,7 +940,9 @@
|
||||
"output.placeholder": "翻譯",
|
||||
"processing": "翻譯中...",
|
||||
"title": "翻譯",
|
||||
"tooltip.newline": "換行"
|
||||
"tooltip.newline": "換行",
|
||||
"scroll_sync.enable": "開啟滾動同步",
|
||||
"scroll_sync.disable": "關閉滾動同步"
|
||||
},
|
||||
"tray": {
|
||||
"quit": "結束",
|
||||
|
||||
@ -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<HTMLDivElement>(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(), [])
|
||||
|
||||
@ -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 (
|
||||
<Container id="translate-page">
|
||||
<Navbar>
|
||||
@ -258,6 +306,16 @@ const TranslatePage: FC = () => {
|
||||
options={[{ label: t('translate.any.language'), value: 'any' }]}
|
||||
/>
|
||||
<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>
|
||||
|
||||
<Tooltip
|
||||
@ -288,6 +346,7 @@ const TranslatePage: FC = () => {
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
onScroll={handleInputScroll}
|
||||
disabled={loading}
|
||||
spellCheck={false}
|
||||
allowClear
|
||||
@ -322,7 +381,9 @@ const TranslatePage: FC = () => {
|
||||
/>
|
||||
</OperationBar>
|
||||
|
||||
<OutputText>{result || t('translate.output.placeholder')}</OutputText>
|
||||
<OutputText ref={outputTextRef} onScroll={handleOutputScroll}>
|
||||
{result || t('translate.output.placeholder')}
|
||||
</OutputText>
|
||||
</OutputContainer>
|
||||
</ContentContainer>
|
||||
</Container>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user