Files
player/src/hooks/useTouchGestures.ts
T
2026-02-13 04:59:21 +03:00

128 lines
4.2 KiB
TypeScript

import { useEffect, MutableRefObject } from 'react'
import { usePlayerContext } from '../contexts/PlayerContext'
import type { TouchConfig } from '../types'
interface TouchData {
startX: number
startY: number
startTime: number
lastTapTime: number
tapCount: number
}
export const useTouchGestures = (containerRef: MutableRefObject<HTMLDivElement | null>, touchConfig?: TouchConfig) => {
const { videoState, togglePlay, seek, setVolume } = usePlayerContext()
useEffect(() => {
const container = containerRef.current
if (!container) return
const maxSeekSeconds = touchConfig?.maxSeekSeconds ?? 30
const maxVolumeChange = touchConfig?.maxVolumeChange ?? 0.5
const doubleTapSeekSeconds = touchConfig?.doubleTapSeekSeconds ?? 10
const touchData: TouchData = {
startX: 0,
startY: 0,
startTime: 0,
lastTapTime: 0,
tapCount: 0,
}
const handleTouchStart = (e: TouchEvent) => {
const touch = e.touches[0]
touchData.startX = touch.clientX
touchData.startY = touch.clientY
touchData.startTime = Date.now()
}
const handleTouchEnd = (e: TouchEvent) => {
const touch = e.changedTouches[0]
const endX = touch.clientX
const endY = touch.clientY
const endTime = Date.now()
const deltaX = endX - touchData.startX
const deltaY = endY - touchData.startY
const deltaTime = endTime - touchData.startTime
// Tap/Double tap detection
if (Math.abs(deltaX) < 10 && Math.abs(deltaY) < 10 && deltaTime < 300) {
const timeSinceLastTap = endTime - touchData.lastTapTime
if (timeSinceLastTap < 400) {
// Double tap
touchData.tapCount++
if (touchData.tapCount === 2) {
handleDoubleTap(endX, container.getBoundingClientRect())
touchData.tapCount = 0
}
} else {
// Single tap
touchData.tapCount = 1
setTimeout(() => {
if (touchData.tapCount === 1) {
togglePlay()
}
touchData.tapCount = 0
}, 400)
}
touchData.lastTapTime = endTime
}
// Swipe detection
if (Math.abs(deltaX) > 50 || Math.abs(deltaY) > 50) {
if (Math.abs(deltaX) > Math.abs(deltaY)) {
// Horizontal swipe - seek
const seekAmount = (deltaX / container.clientWidth) * maxSeekSeconds
seek(Math.max(0, Math.min(videoState.duration, videoState.currentTime + seekAmount)))
} else {
// Vertical swipe - volume
const volumeChange = -(deltaY / container.clientHeight) * maxVolumeChange
setVolume(Math.max(0, Math.min(1, videoState.volume + volumeChange)))
}
}
}
const handleDoubleTap = (x: number, rect: DOMRect) => {
const relativeX = x - rect.left
const isLeftSide = relativeX < rect.width / 2
if (isLeftSide) {
seek(Math.max(0, videoState.currentTime - doubleTapSeekSeconds))
} else {
seek(Math.min(videoState.duration, videoState.currentTime + doubleTapSeekSeconds))
}
showDoubleTapFeedback(isLeftSide)
}
const showDoubleTapFeedback = (isLeft: boolean) => {
const feedback = document.createElement('div')
feedback.className = 'double-tap-feedback'
feedback.style.position = 'absolute'
feedback.style.top = '50%'
feedback.style.left = isLeft ? '25%' : '75%'
feedback.style.transform = 'translate(-50%, -50%)'
feedback.style.color = 'white'
feedback.style.fontSize = '48px'
feedback.style.pointerEvents = 'none'
feedback.style.animation = 'sp-fade-out 0.5s ease-out forwards'
feedback.textContent = isLeft ? `« ${doubleTapSeekSeconds}s` : `${doubleTapSeekSeconds}s »`
container?.appendChild(feedback)
setTimeout(() => feedback.remove(), 500)
}
container.addEventListener('touchstart', handleTouchStart, { passive: true })
container.addEventListener('touchend', handleTouchEnd, { passive: true })
return () => {
container.removeEventListener('touchstart', handleTouchStart)
container.removeEventListener('touchend', handleTouchEnd)
}
}, [containerRef, videoState.currentTime, videoState.duration, videoState.volume, togglePlay, seek, setVolume, touchConfig])
}