You've already forked bilibili-subtitle
上下键移动
This commit is contained in:
@@ -43,6 +43,7 @@ import {getSummarize} from '../util/biz_util'
|
|||||||
import {openUrl} from '@kky002/kky-util'
|
import {openUrl} from '@kky002/kky-util'
|
||||||
import Markdown from '../components/Markdown'
|
import Markdown from '../components/Markdown'
|
||||||
import {random} from 'lodash-es'
|
import {random} from 'lodash-es'
|
||||||
|
import useKeyService from '../hooks/useKeyService'
|
||||||
|
|
||||||
const Body = () => {
|
const Body = () => {
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
@@ -207,6 +208,9 @@ const Body = () => {
|
|||||||
dispatch(setAskFold(!askFold))
|
dispatch(setAskFold(!askFold))
|
||||||
}, [askFold, dispatch])
|
}, [askFold, dispatch])
|
||||||
|
|
||||||
|
// service
|
||||||
|
useKeyService()
|
||||||
|
|
||||||
// 自动滚动
|
// 自动滚动
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (checkAutoScroll && curOffsetTop && autoScroll && !needScroll) {
|
if (checkAutoScroll && curOffsetTop && autoScroll && !needScroll) {
|
||||||
@@ -304,6 +308,13 @@ const Body = () => {
|
|||||||
segmentIdx={segmentIdx} bodyRef={bodyRef}/>)}
|
segmentIdx={segmentIdx} bodyRef={bodyRef}/>)}
|
||||||
|
|
||||||
{/* tip */}
|
{/* tip */}
|
||||||
|
<div className='text-sm font-semibold text-center'>快捷键提示</div>
|
||||||
|
<ul className='list-disc text-sm desc pl-5'>
|
||||||
|
<li>单击字幕跳转,双击字幕跳转+切换暂停。</li>
|
||||||
|
<li>alt+单击字幕复制单条字幕。</li>
|
||||||
|
<li>上下方向键来移动当前字幕(可先点击字幕使焦点在字幕列表内)。</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
{/* <div className='flex flex-col items-center text-center pt-1 pb-2'> */}
|
{/* <div className='flex flex-col items-center text-center pt-1 pb-2'> */}
|
||||||
{/* <div className='font-semibold text-accent'>💡<span className='underline underline-offset-4'>提示</span>💡</div> */}
|
{/* <div className='font-semibold text-accent'>💡<span className='underline underline-offset-4'>提示</span>💡</div> */}
|
||||||
{/* <div className='text-sm desc px-2'>可以尝试将<span className='text-amber-600 font-semibold'>概览</span>生成的内容粘贴到<span */}
|
{/* <div className='text-sm desc px-2'>可以尝试将<span className='text-amber-600 font-semibold'>概览</span>生成的内容粘贴到<span */}
|
||||||
@@ -314,7 +325,7 @@ const Body = () => {
|
|||||||
{/* </button>} */}
|
{/* </button>} */}
|
||||||
{/* </div> */}
|
{/* </div> */}
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
<div className='flex flex-col items-center text-center py-2 mx-4'>
|
<div className='flex flex-col items-center text-center py-2 mx-4 border-t border-t-base-300'>
|
||||||
<div className='font-semibold text-accent flex items-center gap-1'><img src='/bibigpt.png'
|
<div className='font-semibold text-accent flex items-center gap-1'><img src='/bibigpt.png'
|
||||||
alt='BibiGPT logo'
|
alt='BibiGPT logo'
|
||||||
className='w-8 h-8'/>BibiGPT
|
className='w-8 h-8'/>BibiGPT
|
||||||
|
@@ -9,8 +9,9 @@ const CompactSegmentItem = (props: {
|
|||||||
isIn: boolean
|
isIn: boolean
|
||||||
last: boolean
|
last: boolean
|
||||||
moveCallback: (event: any) => void
|
moveCallback: (event: any) => void
|
||||||
|
move2Callback: (event: any) => void
|
||||||
}) => {
|
}) => {
|
||||||
const {item, idx, last, isIn, moveCallback} = props
|
const {item, idx, last, isIn, moveCallback, move2Callback} = props
|
||||||
const transResult = useAppSelector(state => state.env.transResults[idx])
|
const transResult = useAppSelector(state => state.env.transResults[idx])
|
||||||
const envData = useAppSelector(state => state.env.envData)
|
const envData = useAppSelector(state => state.env.envData)
|
||||||
const fontSize = useAppSelector(state => state.env.envData.fontSize)
|
const fontSize = useAppSelector(state => state.env.envData.fontSize)
|
||||||
@@ -19,7 +20,7 @@ const CompactSegmentItem = (props: {
|
|||||||
const display = useMemo(() => getDisplay(envData.transDisplay, item.content, transText), [envData.transDisplay, item.content, transText])
|
const display = useMemo(() => getDisplay(envData.transDisplay, item.content, transText), [envData.transDisplay, item.content, transText])
|
||||||
|
|
||||||
return <div className={classNames('inline', fontSize === 'large'?'text-sm':'text-xs')}>
|
return <div className={classNames('inline', fontSize === 'large'?'text-sm':'text-xs')}>
|
||||||
<span className={'pl-1 pr-0.5 py-0.5 cursor-pointer rounded-sm hover:bg-base-200'} onClick={moveCallback}>
|
<span className={'pl-1 pr-0.5 py-0.5 cursor-pointer rounded-sm hover:bg-base-200'} onClick={moveCallback} onDoubleClick={move2Callback}>
|
||||||
<text className={classNames('font-medium', isIn ? 'text-primary underline' : '')}>{display.main}</text>
|
<text className={classNames('font-medium', isIn ? 'text-primary underline' : '')}>{display.main}</text>
|
||||||
{display.sub && <text className='desc'>({display.sub})</text>}</span>
|
{display.sub && <text className='desc'>({display.sub})</text>}</span>
|
||||||
<span className='text-base-content/75'>{!last && ','}</span>
|
<span className='text-base-content/75'>{!last && ','}</span>
|
||||||
|
@@ -9,8 +9,9 @@ const NormalSegmentItem = (props: {
|
|||||||
idx: number
|
idx: number
|
||||||
isIn: boolean
|
isIn: boolean
|
||||||
moveCallback: (event: any) => void
|
moveCallback: (event: any) => void
|
||||||
|
move2Callback: (event: any) => void
|
||||||
}) => {
|
}) => {
|
||||||
const {item, idx, isIn, moveCallback} = props
|
const {item, idx, isIn, moveCallback, move2Callback} = props
|
||||||
const transResult = useAppSelector(state => state.env.transResults[idx])
|
const transResult = useAppSelector(state => state.env.transResults[idx])
|
||||||
const envData = useAppSelector(state => state.env.envData)
|
const envData = useAppSelector(state => state.env.envData)
|
||||||
const fontSize = useAppSelector(state => state.env.envData.fontSize)
|
const fontSize = useAppSelector(state => state.env.envData.fontSize)
|
||||||
@@ -19,7 +20,7 @@ const NormalSegmentItem = (props: {
|
|||||||
const display = useMemo(() => getDisplay(envData.transDisplay, item.content, transText), [envData.transDisplay, item.content, transText])
|
const display = useMemo(() => getDisplay(envData.transDisplay, item.content, transText), [envData.transDisplay, item.content, transText])
|
||||||
|
|
||||||
return <div className={classNames('flex py-0.5 cursor-pointer rounded-sm hover:bg-base-200', fontSize === 'large'?'text-sm':'text-xs')}
|
return <div className={classNames('flex py-0.5 cursor-pointer rounded-sm hover:bg-base-200', fontSize === 'large'?'text-sm':'text-xs')}
|
||||||
onClick={moveCallback}>
|
onClick={moveCallback} onDoubleClick={move2Callback}>
|
||||||
<div className='desc w-[66px] flex justify-center'>{formatTime(item.from)}</div>
|
<div className='desc w-[66px] flex justify-center'>{formatTime(item.from)}</div>
|
||||||
<div className={'flex-1'}>
|
<div className={'flex-1'}>
|
||||||
<div className={classNames('font-medium', isIn ? 'text-primary underline' : '')}>{display.main}</div>
|
<div className={classNames('font-medium', isIn ? 'text-primary underline' : '')}>{display.main}</div>
|
||||||
|
@@ -46,7 +46,7 @@ const SummarizeItemOverview = (props: {
|
|||||||
if (event.altKey) { // 复制
|
if (event.altKey) { // 复制
|
||||||
navigator.clipboard.writeText(overviewItem.key).catch(console.error)
|
navigator.clipboard.writeText(overviewItem.key).catch(console.error)
|
||||||
} else {
|
} else {
|
||||||
move(time)
|
move(time, false)
|
||||||
}
|
}
|
||||||
}, [overviewItem.key, move, time])
|
}, [overviewItem.key, move, time])
|
||||||
|
|
||||||
|
@@ -33,7 +33,15 @@ const SegmentItem = (props: {
|
|||||||
if (event.altKey) { // 复制
|
if (event.altKey) { // 复制
|
||||||
navigator.clipboard.writeText(item.content).catch(console.error)
|
navigator.clipboard.writeText(item.content).catch(console.error)
|
||||||
} else {
|
} else {
|
||||||
move(item.from)
|
move(item.from, false)
|
||||||
|
}
|
||||||
|
}, [item.content, item.from, move])
|
||||||
|
|
||||||
|
const move2Callback = useCallback((event: any) => {
|
||||||
|
if (event.altKey) { // 复制
|
||||||
|
navigator.clipboard.writeText(item.content).catch(console.error)
|
||||||
|
} else {
|
||||||
|
move(item.from, true)
|
||||||
}
|
}
|
||||||
}, [item.content, item.from, move])
|
}, [item.content, item.from, move])
|
||||||
|
|
||||||
@@ -63,6 +71,7 @@ const SegmentItem = (props: {
|
|||||||
isIn={isIn}
|
isIn={isIn}
|
||||||
last={last}
|
last={last}
|
||||||
moveCallback={moveCallback}
|
moveCallback={moveCallback}
|
||||||
|
move2Callback={move2Callback}
|
||||||
/>
|
/>
|
||||||
:
|
:
|
||||||
<NormalSegmentItem
|
<NormalSegmentItem
|
||||||
@@ -70,6 +79,7 @@ const SegmentItem = (props: {
|
|||||||
idx={idx}
|
idx={idx}
|
||||||
isIn={isIn}
|
isIn={isIn}
|
||||||
moveCallback={moveCallback}
|
moveCallback={moveCallback}
|
||||||
|
move2Callback={move2Callback}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
|
@@ -158,6 +158,9 @@ window.addEventListener("message", (event) => {
|
|||||||
const video = getVideoElement()
|
const video = getVideoElement()
|
||||||
if (video) {
|
if (video) {
|
||||||
video.currentTime = data.time
|
video.currentTime = data.time
|
||||||
|
if (data.togglePause) {
|
||||||
|
video.paused ? video.play() : video.pause()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
59
src/hooks/useKeyService.ts
Normal file
59
src/hooks/useKeyService.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import {useEffect} from 'react'
|
||||||
|
import {useMemoizedFn} from 'ahooks/es'
|
||||||
|
import {useAppSelector} from './redux'
|
||||||
|
import useSubtitle from './useSubtitle'
|
||||||
|
|
||||||
|
const useKeyService = () => {
|
||||||
|
const curIdx = useAppSelector(state => state.env.curIdx)
|
||||||
|
const data = useAppSelector(state => state.env.data)
|
||||||
|
const {move} = useSubtitle()
|
||||||
|
|
||||||
|
const onKeyDown = useMemoizedFn((e: KeyboardEvent) => {
|
||||||
|
// 有按其他控制键时,不触发
|
||||||
|
if (e.ctrlKey || e.metaKey || e.shiftKey) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let cursorInInput = false
|
||||||
|
if (document.activeElement != null) {
|
||||||
|
const tagName = document.activeElement.tagName
|
||||||
|
if (tagName === 'INPUT' || tagName === 'TEXTAREA') {
|
||||||
|
cursorInInput = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let prevent = false
|
||||||
|
|
||||||
|
// up arrow
|
||||||
|
if (e.key === 'ArrowUp') {
|
||||||
|
if (curIdx && (data != null) && !cursorInInput) {
|
||||||
|
prevent = true
|
||||||
|
const newCurIdx = Math.max(curIdx - 1, 0)
|
||||||
|
move(data.body[newCurIdx].from, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// down arrow
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
if (curIdx !== undefined && (data != null) && !cursorInInput) {
|
||||||
|
prevent = true
|
||||||
|
const newCurIdx = Math.min(curIdx + 1, data.body.length - 1)
|
||||||
|
move(data.body[newCurIdx].from, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 阻止默认事件
|
||||||
|
if (prevent) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 检测快捷键
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('keydown', onKeyDown)
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', onKeyDown)
|
||||||
|
}
|
||||||
|
}, [onKeyDown])
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useKeyService
|
@@ -5,8 +5,8 @@ import {setNeedScroll} from '../redux/envReducer'
|
|||||||
const useSubtitle = () => {
|
const useSubtitle = () => {
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
const move = useCallback((time: number) => {
|
const move = useCallback((time: number, togglePause: boolean) => {
|
||||||
window.parent.postMessage({type: 'move', time}, '*')
|
window.parent.postMessage({type: 'move', time, togglePause}, '*')
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const scrollIntoView = useCallback((ref: React.RefObject<HTMLDivElement>) => {
|
const scrollIntoView = useCallback((ref: React.RefObject<HTMLDivElement>) => {
|
||||||
|
Reference in New Issue
Block a user