Initial commit: modern React video player library

Add all source files for a feature-rich, reusable video player built with React, TypeScript, and Vite. Includes core components, context, hooks, utilities, styles, demo app, and configuration files.
This commit is contained in:
hibna
2025-10-29 07:49:06 +03:00
parent d68df70124
commit b57b24d051
47 changed files with 4414 additions and 0 deletions
+125
View File
@@ -0,0 +1,125 @@
import { useEffect, RefObject } from 'react'
import { usePlayerContext } from '../contexts/PlayerContext'
interface TouchData {
startX: number
startY: number
startTime: number
lastTapTime: number
tapCount: number
}
export const useTouchGestures = (containerRef: RefObject<HTMLDivElement>) => {
const { videoState, togglePlay, seek, setVolume } = usePlayerContext()
useEffect(() => {
const container = containerRef.current
if (!container) return
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) * 30 // Max 30 seconds
seek(Math.max(0, Math.min(videoState.duration, videoState.currentTime + seekAmount)))
} else {
// Vertical swipe - volume
const volumeChange = -(deltaY / container.clientHeight) * 0.5 // Max 0.5 volume change
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) {
// Double tap left - rewind 10 seconds
seek(Math.max(0, videoState.currentTime - 10))
} else {
// Double tap right - forward 10 seconds
seek(Math.min(videoState.duration, videoState.currentTime + 10))
}
// Show feedback animation (optional - can be implemented later)
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 = 'fadeOut 0.5s ease-out forwards'
feedback.textContent = isLeft ? '« 10s' : '10s »'
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])
}