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
+201
View File
@@ -0,0 +1,201 @@
import React, { useEffect, useRef, useState } from 'react'
import { usePlayerContext } from '../../contexts/PlayerContext'
import { SpeedIcon, SubtitlesIcon, CheckIcon, AudioIcon } from '../../icons'
import type { AudioTrack } from '../../types'
import './SettingsMenu.css'
interface SettingsMenuProps {
subtitles?: Array<{ src: string; lang: string; label: string }>
audioTracks?: AudioTrack[]
}
type MenuView = 'main' | 'speed' | 'subtitles' | 'audio'
export const SettingsMenu: React.FC<SettingsMenuProps> = ({ subtitles = [], audioTracks = [] }) => {
const { uiState, videoState, settings, setPlaybackRate, setSubtitle, setAudioTrack, toggleSettings } = usePlayerContext()
const menuRef = useRef<HTMLDivElement>(null)
const [currentView, setCurrentView] = useState<MenuView>('main')
const playbackRates = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]
// Close menu when clicking outside
useEffect(() => {
if (!uiState.settingsOpen) return
const handleClickOutside = (e: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
toggleSettings()
setCurrentView('main')
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [uiState.settingsOpen, toggleSettings])
// Reset to main view when menu closes
useEffect(() => {
if (!uiState.settingsOpen) {
setCurrentView('main')
}
}, [uiState.settingsOpen])
const goBack = () => {
setCurrentView('main')
}
if (!uiState.settingsOpen) return null
return (
<div ref={menuRef} className="settings-menu">
{/* Main Menu */}
{currentView === 'main' && (
<>
<div className="settings-menu-header">
<h3>Ayarlar</h3>
</div>
<div className="settings-main-options">
<button className="settings-main-option" onClick={() => setCurrentView('speed')}>
<div className="settings-main-option-icon">
<SpeedIcon size={20} color="var(--player-text)" />
</div>
<div className="settings-main-option-content">
<span className="settings-main-option-label">Hız</span>
<span className="settings-main-option-value">
{videoState.playbackRate === 1 ? 'Normal' : `${videoState.playbackRate}x`}
</span>
</div>
<div className="settings-main-option-arrow"></div>
</button>
<button className="settings-main-option" onClick={() => setCurrentView('subtitles')}>
<div className="settings-main-option-icon">
<SubtitlesIcon size={20} color="var(--player-text)" />
</div>
<div className="settings-main-option-content">
<span className="settings-main-option-label">Altyazı</span>
<span className="settings-main-option-value">
{settings.subtitle ? settings.subtitle.label : 'Kapalı'}
</span>
</div>
<div className="settings-main-option-arrow"></div>
</button>
{audioTracks.length > 0 && (
<button className="settings-main-option" onClick={() => setCurrentView('audio')}>
<div className="settings-main-option-icon">
<AudioIcon size={20} color="var(--player-text)" />
</div>
<div className="settings-main-option-content">
<span className="settings-main-option-label">Ses</span>
<span className="settings-main-option-value">
{settings.audioTrack ? settings.audioTrack.name : 'Varsayılan'}
</span>
</div>
<div className="settings-main-option-arrow"></div>
</button>
)}
</div>
</>
)}
{/* Speed Submenu */}
{currentView === 'speed' && (
<>
<div className="settings-menu-header">
<button className="settings-back-button" onClick={goBack}>
</button>
<h3>Oynatma Hızı</h3>
</div>
<div className="settings-options">
{playbackRates.map((rate) => (
<button
key={rate}
className={`settings-option ${videoState.playbackRate === rate ? 'active' : ''}`}
onClick={() => {
setPlaybackRate(rate)
setTimeout(() => goBack(), 150)
}}
>
<span>{rate === 1 ? 'Normal' : `${rate}x`}</span>
{videoState.playbackRate === rate && <CheckIcon size={16} color="var(--player-primary)" />}
</button>
))}
</div>
</>
)}
{/* Subtitles Submenu */}
{currentView === 'subtitles' && (
<>
<div className="settings-menu-header">
<button className="settings-back-button" onClick={goBack}>
</button>
<h3>Altyazı</h3>
</div>
<div className="settings-options">
<button
className={`settings-option ${!settings.subtitle ? 'active' : ''}`}
onClick={() => {
setSubtitle(null)
setTimeout(() => goBack(), 150)
}}
>
<span>Kapalı</span>
{!settings.subtitle && <CheckIcon size={16} color="var(--player-primary)" />}
</button>
{subtitles.length > 0 ? (
subtitles.map((subtitle) => (
<button
key={subtitle.lang}
className={`settings-option ${settings.subtitle?.lang === subtitle.lang ? 'active' : ''}`}
onClick={() => {
setSubtitle(subtitle)
setTimeout(() => goBack(), 150)
}}
>
<span>{subtitle.label}</span>
{settings.subtitle?.lang === subtitle.lang && <CheckIcon size={16} color="var(--player-primary)" />}
</button>
))
) : (
<div className="settings-empty-state">
<span>Altyazı mevcut değil</span>
</div>
)}
</div>
</>
)}
{/* Audio Submenu */}
{currentView === 'audio' && (
<>
<div className="settings-menu-header">
<button className="settings-back-button" onClick={goBack}>
</button>
<h3>Ses</h3>
</div>
<div className="settings-options">
{audioTracks.map((track) => (
<button
key={track.language}
className={`settings-option ${settings.audioTrack?.language === track.language ? 'active' : ''}`}
onClick={() => {
setAudioTrack(track)
setTimeout(() => goBack(), 150)
}}
>
<span>{track.name}</span>
{settings.audioTrack?.language === track.language && <CheckIcon size={16} color="var(--player-primary)" />}
</button>
))}
</div>
</>
)}
</div>
)
}