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:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user