feat: synced scrolling for translation page
This commit is contained in:
parent
219cea0c53
commit
02604c466d
@ -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",
|
||||||
|
|||||||
@ -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": "終了",
|
||||||
|
|||||||
@ -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": "Выйти",
|
||||||
|
|||||||
@ -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": "退出",
|
||||||
|
|||||||
@ -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": "結束",
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user