@therealtinhtute/baroiplayer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +384 -0
- package/dist/context/media.js +2 -0
- package/dist/context/media.js.map +1 -0
- package/dist/context/media.mjs +15 -0
- package/dist/context/media.mjs.map +1 -0
- package/dist/context/store.js +2 -0
- package/dist/context/store.js.map +1 -0
- package/dist/context/store.mjs +5 -0
- package/dist/context/store.mjs.map +1 -0
- package/dist/context-BMteAJDN.js +1140 -0
- package/dist/context-BMteAJDN.js.map +1 -0
- package/dist/context-DCJ_scmO.cjs +23 -0
- package/dist/context-DCJ_scmO.cjs.map +1 -0
- package/dist/core-events-B5W_j8yU.js +480 -0
- package/dist/core-events-B5W_j8yU.js.map +1 -0
- package/dist/core-events-CN0-Vhhq.cjs +2 -0
- package/dist/core-events-CN0-Vhhq.cjs.map +1 -0
- package/dist/core-plugins-Bq9KLany.js +696 -0
- package/dist/core-plugins-Bq9KLany.js.map +1 -0
- package/dist/core-plugins-CPDSHMiQ.cjs +2 -0
- package/dist/core-plugins-CPDSHMiQ.cjs.map +1 -0
- package/dist/core.js +37 -0
- package/dist/core.js.map +1 -0
- package/dist/core.mjs +1217 -0
- package/dist/core.mjs.map +1 -0
- package/dist/enhanced-components-DHv0MFvG.js +1073 -0
- package/dist/enhanced-components-DHv0MFvG.js.map +1 -0
- package/dist/enhanced-components-DTIObgB7.cjs +2 -0
- package/dist/enhanced-components-DTIObgB7.cjs.map +1 -0
- package/dist/hooks-C_r8wexn.cjs +4 -0
- package/dist/hooks-C_r8wexn.cjs.map +1 -0
- package/dist/hooks-Da2wVXlF.js +1630 -0
- package/dist/hooks-Da2wVXlF.js.map +1 -0
- package/dist/index.css +1 -0
- package/dist/index.js +112 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +6126 -0
- package/dist/index.mjs.map +1 -0
- package/dist/minimal-components-DSq5sQpy.js +2002 -0
- package/dist/minimal-components-DSq5sQpy.js.map +1 -0
- package/dist/minimal-components-DflWiihN.cjs +2 -0
- package/dist/minimal-components-DflWiihN.cjs.map +1 -0
- package/dist/minimal-components.css +1 -0
- package/dist/mona-loading.gif +0 -0
- package/dist/performance-monitor-CUW6f5ll.cjs +2 -0
- package/dist/performance-monitor-CUW6f5ll.cjs.map +1 -0
- package/dist/performance-monitor-rMW1RgV3.js +302 -0
- package/dist/performance-monitor-rMW1RgV3.js.map +1 -0
- package/dist/utils-BnscpYYd.js +2798 -0
- package/dist/utils-BnscpYYd.js.map +1 -0
- package/dist/utils-D_czS_0K.cjs +9 -0
- package/dist/utils-D_czS_0K.cjs.map +1 -0
- package/dist/utils.js +2 -0
- package/dist/utils.js.map +1 -0
- package/dist/utils.mjs +7 -0
- package/dist/utils.mjs.map +1 -0
- package/package.json +114 -0
- package/src/adapters/videojs-adapter.tsx +470 -0
- package/src/assets/mona-loading.gif +0 -0
- package/src/components/AnimateLoading.tsx +115 -0
- package/src/components/LoadingState.tsx +248 -0
- package/src/components/accessibility/keyboard-help-overlay.tsx +213 -0
- package/src/components/audio-player.tsx +85 -0
- package/src/components/basic/index.ts +37 -0
- package/src/components/enhanced/advanced-features.tsx +316 -0
- package/src/components/enhanced/debug-panel.tsx +422 -0
- package/src/components/enhanced/enhanced-controls.tsx +600 -0
- package/src/components/enhanced/enhanced-media-player.tsx +509 -0
- package/src/components/enhanced/error-handling.tsx +345 -0
- package/src/components/enhanced/index.ts +39 -0
- package/src/components/enhanced/media-player-enhanced.tsx +349 -0
- package/src/components/examples/basic-video-player.tsx +55 -0
- package/src/components/examples/custom-controls-example.tsx +63 -0
- package/src/components/examples/enhanced-controls-example.tsx +114 -0
- package/src/components/examples/enhanced-media-player-example.tsx +293 -0
- package/src/components/examples/loading-states-demo.tsx +142 -0
- package/src/components/examples/react-19-demo.tsx +381 -0
- package/src/components/examples/volume-orientation-demo.tsx +114 -0
- package/src/components/icon-demo.tsx +209 -0
- package/src/components/icons/index.tsx +166 -0
- package/src/components/icons/svg-icons/ArrowLeftIcon.tsx +18 -0
- package/src/components/icons/svg-icons/ArrowRightIcon.tsx +18 -0
- package/src/components/icons/svg-icons/AudioIcon.tsx +22 -0
- package/src/components/icons/svg-icons/BackwardIcon.tsx +12 -0
- package/src/components/icons/svg-icons/CameraIcon.tsx +19 -0
- package/src/components/icons/svg-icons/CheckIcon.tsx +19 -0
- package/src/components/icons/svg-icons/EnablePiPIcon.tsx +17 -0
- package/src/components/icons/svg-icons/ForwardIcon.tsx +12 -0
- package/src/components/icons/svg-icons/FullscreenEnterIcon.tsx +23 -0
- package/src/components/icons/svg-icons/FullscreenExitIcon.tsx +23 -0
- package/src/components/icons/svg-icons/LoadingIcon.tsx +32 -0
- package/src/components/icons/svg-icons/PauseIcon.tsx +23 -0
- package/src/components/icons/svg-icons/PlayIcon.tsx +21 -0
- package/src/components/icons/svg-icons/PlaybackSpeedIcon.tsx +23 -0
- package/src/components/icons/svg-icons/QualityIcon.tsx +21 -0
- package/src/components/icons/svg-icons/SettingsIcon.tsx +12 -0
- package/src/components/icons/svg-icons/SliderIcon.tsx +21 -0
- package/src/components/icons/svg-icons/SubtitleIcon.tsx +21 -0
- package/src/components/icons/svg-icons/SubtitleOffIcon.tsx +25 -0
- package/src/components/icons/svg-icons/UnPiPIcon.tsx +17 -0
- package/src/components/icons/svg-icons/VolumeMutedIcon.tsx +16 -0
- package/src/components/icons/svg-icons/VolumeOneIcon.tsx +22 -0
- package/src/components/icons/svg-icons/VolumeThreeIcon.tsx +16 -0
- package/src/components/icons/svg-icons/VolumeTwoIcon.tsx +16 -0
- package/src/components/legacy.tsx +93 -0
- package/src/components/media-metadata-display.tsx +100 -0
- package/src/components/media-player-audio.tsx +39 -0
- package/src/components/media-player-controls.tsx +226 -0
- package/src/components/media-player-error.tsx +163 -0
- package/src/components/media-player-fullscreen.tsx +109 -0
- package/src/components/media-player-hls.tsx +111 -0
- package/src/components/media-player-loading.tsx +76 -0
- package/src/components/media-player-play.tsx +75 -0
- package/src/components/media-player-root.tsx +340 -0
- package/src/components/media-player-seek.tsx +168 -0
- package/src/components/media-player-time.tsx +99 -0
- package/src/components/media-player-tooltip.tsx +58 -0
- package/src/components/media-player-video.tsx +39 -0
- package/src/components/media-player-volume.tsx +155 -0
- package/src/components/media-player.tsx +257 -0
- package/src/components/media-suspense.tsx +362 -0
- package/src/components/player-settings-form.tsx +163 -0
- package/src/components/react-player-wrapper.tsx +651 -0
- package/src/components/simple-video-player.tsx +60 -0
- package/src/components/ui/enhanced-seek-bar.tsx +235 -0
- package/src/components/ui/fullscreen-button.tsx +60 -0
- package/src/components/ui/media-resource-preloader.tsx +395 -0
- package/src/components/ui/play-button.tsx +107 -0
- package/src/components/ui/seek-bar.tsx +161 -0
- package/src/components/ui/time-display.tsx +40 -0
- package/src/components/ui/volume-control.tsx +143 -0
- package/src/components/unified/base-media-player.tsx +246 -0
- package/src/components/unified/media-player.tsx +75 -0
- package/src/components/variants/minimal/AccessibleMinimalPlayer.tsx +561 -0
- package/src/components/variants/minimal/EnhancedMinimalMediaPlayer.tsx +552 -0
- package/src/components/variants/minimal/MinimalControls.tsx +142 -0
- package/src/components/variants/minimal/MinimalLoading.tsx +32 -0
- package/src/components/variants/minimal/MinimalMediaPlayer.tsx +315 -0
- package/src/components/variants/minimal/MinimalOverlay.tsx +64 -0
- package/src/components/variants/minimal/MinimalPlayButton.tsx +65 -0
- package/src/components/variants/minimal/MinimalSeekBar.tsx +150 -0
- package/src/components/variants/minimal/MinimalVolumeControl.tsx +80 -0
- package/src/components/variants/minimal/SimpleMinimalPlayer.tsx +210 -0
- package/src/components/variants/minimal/components/CenterPlayButton.tsx +22 -0
- package/src/components/variants/minimal/components/ControlBar.tsx +146 -0
- package/src/components/variants/minimal/components/EnhancedSettingsMenu.tsx +361 -0
- package/src/components/variants/minimal/components/PlaybackControls.tsx +51 -0
- package/src/components/variants/minimal/components/RightControls.tsx +173 -0
- package/src/components/variants/minimal/components/SeekBar.tsx +131 -0
- package/src/components/variants/minimal/components/SettingsDropdown.tsx +70 -0
- package/src/components/variants/minimal/components/SubtitleDisplay.tsx +21 -0
- package/src/components/variants/minimal/components/TimeDisplay.tsx +18 -0
- package/src/components/variants/minimal/components/VideoElement.tsx +40 -0
- package/src/components/variants/minimal/components/VolumeControl.tsx +217 -0
- package/src/components/variants/minimal/index.ts +23 -0
- package/src/components/video-player.tsx +100 -0
- package/src/context/error-handling.ts +394 -0
- package/src/context/media-context.tsx +208 -0
- package/src/context/media-provider.tsx +265 -0
- package/src/context/media-store.ts +379 -0
- package/src/context/persistence.ts +453 -0
- package/src/core/events/event-system.ts +442 -0
- package/src/core/events/hooks.ts +302 -0
- package/src/core/events/index.ts +22 -0
- package/src/core/events/middleware.ts +269 -0
- package/src/core/index.ts +22 -0
- package/src/core/optimization/bundle-optimizer.tsx +308 -0
- package/src/core/performance/cicd.ts +96 -0
- package/src/core/performance/index.ts +38 -0
- package/src/core/performance/media-loading-optimizer.ts +538 -0
- package/src/core/performance/performance-budgets.ts +432 -0
- package/src/core/performance/performance-monitor.tsx +476 -0
- package/src/core/performance/performance-testing.ts +459 -0
- package/src/core/plugins/base-plugin.ts +224 -0
- package/src/core/plugins/enhanced-plugin-system.ts +411 -0
- package/src/core/plugins/examples/analytics-plugin.ts +242 -0
- package/src/core/plugins/index.ts +39 -0
- package/src/core/plugins/registry.ts +327 -0
- package/src/core/plugins/types.ts +159 -0
- package/src/core/plugins/utils.ts +316 -0
- package/src/core/progressive-loading.tsx +322 -0
- package/src/core/providers/enhanced-hls-provider.ts +339 -0
- package/src/core/providers/service-provider.tsx +168 -0
- package/src/core/utils.tsx +38 -0
- package/src/features/media-session/media-session-manager.ts +429 -0
- package/src/features/picture-in-picture/pip-manager.ts +429 -0
- package/src/features/playlist/playlist-manager.ts +606 -0
- package/src/features/subtitles/subtitle-manager.ts +441 -0
- package/src/features/subtitles/subtitle-parser.ts +280 -0
- package/src/features/thumbnails/thumbnail-generator.ts +519 -0
- package/src/hooks/accessibility/index.ts +27 -0
- package/src/hooks/accessibility/use-accessibility-preferences.ts +188 -0
- package/src/hooks/accessibility/use-focus-management.ts +176 -0
- package/src/hooks/accessibility/use-keyboard-shortcuts.ts +144 -0
- package/src/hooks/accessibility/use-screen-reader.ts +182 -0
- package/src/hooks/core/index.ts +47 -0
- package/src/hooks/legacy.ts +27 -0
- package/src/hooks/use-enhanced-media-player.ts +468 -0
- package/src/hooks/use-intelligent-preloading.ts +358 -0
- package/src/hooks/use-keyboard-controls.ts +115 -0
- package/src/hooks/use-media-actions.ts +175 -0
- package/src/hooks/use-media-metadata.ts +89 -0
- package/src/hooks/use-media-optimistic.ts +270 -0
- package/src/hooks/use-media-player.ts +312 -0
- package/src/hooks/use-media-state.ts +190 -0
- package/src/hooks/use-memory-optimization.ts +358 -0
- package/src/hooks/use-optimistic-controls.ts +92 -0
- package/src/hooks/use-optimized-media.ts +263 -0
- package/src/hooks/use-touch-gestures.ts +417 -0
- package/src/hooks/useEnhancedSubtitles.ts +258 -0
- package/src/hooks/useKeyboardShortcuts.ts +74 -0
- package/src/hooks/useMediaControls.ts +52 -0
- package/src/hooks/useScreenshot.ts +57 -0
- package/src/hooks/useSubtitles.ts +91 -0
- package/src/hooks/useVideoPlayer.ts +232 -0
- package/src/icons/components/FullscreenIcon.tsx +37 -0
- package/src/icons/components/SettingsIcon.tsx +38 -0
- package/src/icons/components/animated/LoadingIcon.tsx +43 -0
- package/src/icons/components/animated/PlayPauseIcon.tsx +50 -0
- package/src/icons/components/media/PauseIcon.tsx +39 -0
- package/src/icons/components/media/PlayIcon.tsx +38 -0
- package/src/icons/components/volume/VolumeIcon.tsx +39 -0
- package/src/icons/components/volume/VolumeOffIcon.tsx +39 -0
- package/src/icons/index.ts +56 -0
- package/src/icons/provider.tsx +64 -0
- package/src/icons/types.ts +40 -0
- package/src/index-enhanced.ts +35 -0
- package/src/index-legacy.ts +326 -0
- package/src/index.css +102 -0
- package/src/index.ts +216 -0
- package/src/lib/deprecation.ts +51 -0
- package/src/lib/format-time.ts +37 -0
- package/src/lib/index.ts +3 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/globals.css +462 -0
- package/src/styles/minimal-player.css +589 -0
- package/src/styles/pixel-art.css +46 -0
- package/src/styles/videojs.css +100 -0
- package/src/test/setup.ts +79 -0
- package/src/types/events.ts +47 -0
- package/src/types/formats.ts +95 -0
- package/src/types/index.ts +190 -0
- package/src/types/videojs-plugins.d.ts +14 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import React, { memo, useState, useCallback, useEffect } from "react"
|
|
2
|
+
import { cn } from "../../../../lib/utils"
|
|
3
|
+
import { defaultMinimalIcons } from "../../../icons"
|
|
4
|
+
|
|
5
|
+
export interface SubtitleTrack {
|
|
6
|
+
id: string
|
|
7
|
+
label: string
|
|
8
|
+
language?: string
|
|
9
|
+
src?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface EnhancedSettingsMenuProps {
|
|
13
|
+
showSettings: boolean
|
|
14
|
+
playbackRate: number
|
|
15
|
+
subtitlesEnabled: boolean
|
|
16
|
+
subtitleTracks: SubtitleTrack[]
|
|
17
|
+
currentSubtitleTrack?: string
|
|
18
|
+
subtitleFontSize: "small" | "medium" | "large"
|
|
19
|
+
hasSubtitles: boolean
|
|
20
|
+
subtitleLoadError?: boolean
|
|
21
|
+
onToggleSettings: () => void
|
|
22
|
+
onChangePlaybackRate: (rate: number) => void
|
|
23
|
+
onToggleSubtitles: () => void
|
|
24
|
+
onSelectSubtitleTrack?: (trackId: string | null) => void
|
|
25
|
+
onChangeSubtitleFontSize: (size: "small" | "medium" | "large") => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type MenuView = "main" | "subtitles" | "fontSize" | "speed"
|
|
29
|
+
|
|
30
|
+
export const EnhancedSettingsMenu = function EnhancedSettingsMenu({
|
|
31
|
+
showSettings,
|
|
32
|
+
playbackRate,
|
|
33
|
+
subtitlesEnabled,
|
|
34
|
+
subtitleTracks,
|
|
35
|
+
currentSubtitleTrack,
|
|
36
|
+
subtitleFontSize = "medium",
|
|
37
|
+
hasSubtitles,
|
|
38
|
+
subtitleLoadError,
|
|
39
|
+
onToggleSettings,
|
|
40
|
+
onChangePlaybackRate,
|
|
41
|
+
onToggleSubtitles,
|
|
42
|
+
onSelectSubtitleTrack,
|
|
43
|
+
onChangeSubtitleFontSize
|
|
44
|
+
}: EnhancedSettingsMenuProps) {
|
|
45
|
+
const [currentView, setCurrentView] = useState<MenuView>("main")
|
|
46
|
+
const playbackRates = [0.5, 0.75, 1, 1.25, 1.5, 2]
|
|
47
|
+
const fontSizes = [
|
|
48
|
+
{ value: "small", label: "Small", size: "12px" },
|
|
49
|
+
{ value: "medium", label: "Medium", size: "16px" },
|
|
50
|
+
{ value: "large", label: "Large", size: "20px" }
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
// Reset to main menu when settings close
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!showSettings) {
|
|
56
|
+
setCurrentView("main")
|
|
57
|
+
}
|
|
58
|
+
}, [showSettings])
|
|
59
|
+
|
|
60
|
+
const handleSettingsClick = useCallback((e: React.MouseEvent) => {
|
|
61
|
+
e.stopPropagation()
|
|
62
|
+
onToggleSettings()
|
|
63
|
+
}, [onToggleSettings])
|
|
64
|
+
|
|
65
|
+
const handleDropdownClick = useCallback((e: React.MouseEvent) => {
|
|
66
|
+
e.stopPropagation()
|
|
67
|
+
}, [])
|
|
68
|
+
|
|
69
|
+
const handleBackClick = useCallback(() => {
|
|
70
|
+
setCurrentView("main")
|
|
71
|
+
}, [])
|
|
72
|
+
|
|
73
|
+
const handleSelectSpeed = useCallback((rate: number) => {
|
|
74
|
+
onChangePlaybackRate(rate)
|
|
75
|
+
setCurrentView("main")
|
|
76
|
+
}, [onChangePlaybackRate])
|
|
77
|
+
|
|
78
|
+
const handleSelectFontSize = useCallback((size: "small" | "medium" | "large") => {
|
|
79
|
+
onChangeSubtitleFontSize(size)
|
|
80
|
+
setCurrentView("main")
|
|
81
|
+
}, [onChangeSubtitleFontSize])
|
|
82
|
+
|
|
83
|
+
const handleSelectSubtitle = useCallback((trackId: string | null) => {
|
|
84
|
+
if (trackId === null) {
|
|
85
|
+
// Turn off subtitles
|
|
86
|
+
if (subtitlesEnabled) {
|
|
87
|
+
onToggleSubtitles()
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
// Select track and enable if needed
|
|
91
|
+
if (!subtitlesEnabled) {
|
|
92
|
+
onToggleSubtitles()
|
|
93
|
+
}
|
|
94
|
+
onSelectSubtitleTrack?.(trackId)
|
|
95
|
+
}
|
|
96
|
+
setCurrentView("main")
|
|
97
|
+
}, [subtitlesEnabled, onToggleSubtitles, onSelectSubtitleTrack])
|
|
98
|
+
|
|
99
|
+
const renderMainMenu = () => (
|
|
100
|
+
<>
|
|
101
|
+
<div className="border-gray-700 border-b px-3 py-2 font-semibold text-gray-400 text-xs uppercase tracking-wider">
|
|
102
|
+
Settings
|
|
103
|
+
</div>
|
|
104
|
+
<div className="py-1">
|
|
105
|
+
{/* Subtitles Option */}
|
|
106
|
+
<button
|
|
107
|
+
className={cn(
|
|
108
|
+
"flex w-full items-center justify-between px-3 py-2 text-left text-sm transition-colors hover:bg-gray-800",
|
|
109
|
+
!hasSubtitles && "cursor-not-allowed opacity-50"
|
|
110
|
+
)}
|
|
111
|
+
onClick={() => hasSubtitles && setCurrentView("subtitles")}
|
|
112
|
+
disabled={!hasSubtitles}
|
|
113
|
+
>
|
|
114
|
+
<div className="flex items-center gap-2">
|
|
115
|
+
{defaultMinimalIcons.subtitle && <defaultMinimalIcons.subtitle size={16} />}
|
|
116
|
+
<span>Subtitles</span>
|
|
117
|
+
</div>
|
|
118
|
+
<div className="flex items-center gap-1 text-gray-400 text-xs">
|
|
119
|
+
{!hasSubtitles ? (
|
|
120
|
+
<span className="text-gray-500">Unavailable</span>
|
|
121
|
+
) : subtitleLoadError ? (
|
|
122
|
+
<span className="text-red-400">Error</span>
|
|
123
|
+
) : subtitlesEnabled ? (
|
|
124
|
+
<span className="text-cyan-400">On</span>
|
|
125
|
+
) : (
|
|
126
|
+
<span>Off</span>
|
|
127
|
+
)}
|
|
128
|
+
{hasSubtitles && (
|
|
129
|
+
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
130
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
131
|
+
</svg>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
</button>
|
|
135
|
+
|
|
136
|
+
{/* Font Size Option - only show if subtitles are available */}
|
|
137
|
+
{hasSubtitles && (
|
|
138
|
+
<button
|
|
139
|
+
className="flex w-full items-center justify-between px-3 py-2 text-left text-sm transition-colors hover:bg-gray-800"
|
|
140
|
+
onClick={() => setCurrentView("fontSize")}
|
|
141
|
+
>
|
|
142
|
+
<div className="flex items-center gap-2">
|
|
143
|
+
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
144
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
|
145
|
+
d="M4 7h16M4 7v10m0-10l4 10m8-10l4 10m-4-10v10" />
|
|
146
|
+
</svg>
|
|
147
|
+
<span>Font Size</span>
|
|
148
|
+
</div>
|
|
149
|
+
<div className="flex items-center gap-1 text-gray-400 text-xs">
|
|
150
|
+
<span className="capitalize">{subtitleFontSize}</span>
|
|
151
|
+
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
152
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
153
|
+
</svg>
|
|
154
|
+
</div>
|
|
155
|
+
</button>
|
|
156
|
+
)}
|
|
157
|
+
|
|
158
|
+
{/* Playback Speed Option */}
|
|
159
|
+
<button
|
|
160
|
+
className="flex w-full items-center justify-between px-3 py-2 text-left text-sm transition-colors hover:bg-gray-800"
|
|
161
|
+
onClick={() => setCurrentView("speed")}
|
|
162
|
+
>
|
|
163
|
+
<div className="flex items-center gap-2">
|
|
164
|
+
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
165
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
|
166
|
+
d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
167
|
+
</svg>
|
|
168
|
+
<span>Playback Speed</span>
|
|
169
|
+
</div>
|
|
170
|
+
<div className="flex items-center gap-1 text-gray-400 text-xs">
|
|
171
|
+
<span>{playbackRate === 1 ? "Normal" : `${playbackRate}x`}</span>
|
|
172
|
+
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
173
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
174
|
+
</svg>
|
|
175
|
+
</div>
|
|
176
|
+
</button>
|
|
177
|
+
</div>
|
|
178
|
+
</>
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
const renderSubtitlesMenu = () => (
|
|
182
|
+
<>
|
|
183
|
+
<div className="flex items-center justify-between border-gray-700 border-b px-3 py-2">
|
|
184
|
+
<button
|
|
185
|
+
onClick={handleBackClick}
|
|
186
|
+
className="text-gray-400 transition-colors hover:text-white"
|
|
187
|
+
>
|
|
188
|
+
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
189
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
|
190
|
+
</svg>
|
|
191
|
+
</button>
|
|
192
|
+
<span className="font-semibold text-gray-300 text-sm">Subtitles</span>
|
|
193
|
+
<div className="w-4" />
|
|
194
|
+
</div>
|
|
195
|
+
<div className="py-1">
|
|
196
|
+
{/* Off option */}
|
|
197
|
+
<button
|
|
198
|
+
className={cn(
|
|
199
|
+
"flex w-full items-center justify-between px-3 py-2 text-left text-sm transition-colors hover:bg-gray-800",
|
|
200
|
+
!subtitlesEnabled && "bg-cyan-700/20 text-cyan-400"
|
|
201
|
+
)}
|
|
202
|
+
onClick={() => handleSelectSubtitle(null)}
|
|
203
|
+
>
|
|
204
|
+
<span>Off</span>
|
|
205
|
+
{!subtitlesEnabled && (
|
|
206
|
+
<svg className="h-4 w-4 text-cyan-400" fill="currentColor" viewBox="0 0 20 20">
|
|
207
|
+
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" />
|
|
208
|
+
</svg>
|
|
209
|
+
)}
|
|
210
|
+
</button>
|
|
211
|
+
|
|
212
|
+
{/* Subtitle tracks */}
|
|
213
|
+
{subtitleTracks.length > 0 ? (
|
|
214
|
+
subtitleTracks.map((track) => (
|
|
215
|
+
<button
|
|
216
|
+
key={track.id}
|
|
217
|
+
className={cn(
|
|
218
|
+
"flex w-full items-center justify-between px-3 py-2 text-left text-sm transition-colors hover:bg-gray-800",
|
|
219
|
+
subtitlesEnabled && currentSubtitleTrack === track.id && "bg-cyan-700/20 text-cyan-400"
|
|
220
|
+
)}
|
|
221
|
+
onClick={() => handleSelectSubtitle(track.id)}
|
|
222
|
+
>
|
|
223
|
+
<span>{track.label}</span>
|
|
224
|
+
{subtitlesEnabled && currentSubtitleTrack === track.id && (
|
|
225
|
+
<svg className="h-4 w-4 text-cyan-400" fill="currentColor" viewBox="0 0 20 20">
|
|
226
|
+
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" />
|
|
227
|
+
</svg>
|
|
228
|
+
)}
|
|
229
|
+
</button>
|
|
230
|
+
))
|
|
231
|
+
) : (
|
|
232
|
+
<button
|
|
233
|
+
className={cn(
|
|
234
|
+
"flex w-full items-center justify-between px-3 py-2 text-left text-sm transition-colors hover:bg-gray-800",
|
|
235
|
+
subtitlesEnabled && "bg-cyan-700/20 text-cyan-400"
|
|
236
|
+
)}
|
|
237
|
+
onClick={() => {
|
|
238
|
+
if (!subtitlesEnabled) {
|
|
239
|
+
onToggleSubtitles()
|
|
240
|
+
}
|
|
241
|
+
setCurrentView("main")
|
|
242
|
+
}}
|
|
243
|
+
>
|
|
244
|
+
<span>Default</span>
|
|
245
|
+
{subtitlesEnabled && (
|
|
246
|
+
<svg className="h-4 w-4 text-cyan-400" fill="currentColor" viewBox="0 0 20 20">
|
|
247
|
+
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" />
|
|
248
|
+
</svg>
|
|
249
|
+
)}
|
|
250
|
+
</button>
|
|
251
|
+
)}
|
|
252
|
+
</div>
|
|
253
|
+
</>
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
const renderFontSizeMenu = () => (
|
|
257
|
+
<>
|
|
258
|
+
<div className="flex items-center justify-between border-gray-700 border-b px-3 py-2">
|
|
259
|
+
<button
|
|
260
|
+
onClick={handleBackClick}
|
|
261
|
+
className="text-gray-400 transition-colors hover:text-white"
|
|
262
|
+
>
|
|
263
|
+
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
264
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
|
265
|
+
</svg>
|
|
266
|
+
</button>
|
|
267
|
+
<span className="font-semibold text-gray-300 text-sm">Font Size</span>
|
|
268
|
+
<div className="w-4" />
|
|
269
|
+
</div>
|
|
270
|
+
<div className="py-1">
|
|
271
|
+
{fontSizes.map((size) => (
|
|
272
|
+
<button
|
|
273
|
+
key={size.value}
|
|
274
|
+
className={cn(
|
|
275
|
+
"flex w-full items-center justify-between px-3 py-2 text-left text-sm transition-colors hover:bg-gray-800",
|
|
276
|
+
subtitleFontSize === size.value && "bg-cyan-700/20 text-cyan-400"
|
|
277
|
+
)}
|
|
278
|
+
onClick={() => handleSelectFontSize(size.value as "small" | "medium" | "large")}
|
|
279
|
+
>
|
|
280
|
+
<div className="flex items-center gap-3">
|
|
281
|
+
<span className="text-gray-400" style={{ fontSize: size.size }}>Aa</span>
|
|
282
|
+
<span>{size.label}</span>
|
|
283
|
+
</div>
|
|
284
|
+
{subtitleFontSize === size.value && (
|
|
285
|
+
<svg className="h-4 w-4 text-cyan-400" fill="currentColor" viewBox="0 0 20 20">
|
|
286
|
+
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" />
|
|
287
|
+
</svg>
|
|
288
|
+
)}
|
|
289
|
+
</button>
|
|
290
|
+
))}
|
|
291
|
+
</div>
|
|
292
|
+
</>
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
const renderSpeedMenu = () => (
|
|
296
|
+
<>
|
|
297
|
+
<div className="flex items-center justify-between border-gray-700 border-b px-3 py-2">
|
|
298
|
+
<button
|
|
299
|
+
onClick={handleBackClick}
|
|
300
|
+
className="text-gray-400 transition-colors hover:text-white"
|
|
301
|
+
>
|
|
302
|
+
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
303
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
|
304
|
+
</svg>
|
|
305
|
+
</button>
|
|
306
|
+
<span className="font-semibold text-gray-300 text-sm">Playback Speed</span>
|
|
307
|
+
<div className="w-4" />
|
|
308
|
+
</div>
|
|
309
|
+
<div className="py-1">
|
|
310
|
+
{playbackRates.map((rate) => (
|
|
311
|
+
<button
|
|
312
|
+
key={rate}
|
|
313
|
+
className={cn(
|
|
314
|
+
"flex w-full items-center justify-between px-3 py-2 text-left text-sm transition-colors hover:bg-gray-800",
|
|
315
|
+
playbackRate === rate && "bg-cyan-700/20 text-cyan-400"
|
|
316
|
+
)}
|
|
317
|
+
onClick={() => handleSelectSpeed(rate)}
|
|
318
|
+
>
|
|
319
|
+
<span>{rate === 1 ? "Normal" : `${rate}x`}</span>
|
|
320
|
+
{playbackRate === rate && (
|
|
321
|
+
<svg className="h-4 w-4 text-cyan-400" fill="currentColor" viewBox="0 0 20 20">
|
|
322
|
+
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" />
|
|
323
|
+
</svg>
|
|
324
|
+
)}
|
|
325
|
+
</button>
|
|
326
|
+
))}
|
|
327
|
+
</div>
|
|
328
|
+
</>
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
return (
|
|
332
|
+
<div className="relative" style={{ zIndex: showSettings ? 60 : "auto" }}>
|
|
333
|
+
<button
|
|
334
|
+
className={cn("minimal-btn h-9 w-9", showSettings && "text-cyan-500")}
|
|
335
|
+
onClick={handleSettingsClick}
|
|
336
|
+
aria-label="Settings"
|
|
337
|
+
title="Settings"
|
|
338
|
+
>
|
|
339
|
+
{defaultMinimalIcons.settings && <defaultMinimalIcons.settings size={18} />}
|
|
340
|
+
</button>
|
|
341
|
+
|
|
342
|
+
{showSettings && (
|
|
343
|
+
<div
|
|
344
|
+
className="absolute right-0 mb-2 min-w-[240px] overflow-hidden rounded-lg border border-gray-700 bg-gray-900 shadow-xl"
|
|
345
|
+
style={{
|
|
346
|
+
bottom: "100%",
|
|
347
|
+
zIndex: 1000,
|
|
348
|
+
maxHeight: "400px",
|
|
349
|
+
overflowY: "auto"
|
|
350
|
+
}}
|
|
351
|
+
onClick={handleDropdownClick}
|
|
352
|
+
>
|
|
353
|
+
{currentView === "main" && renderMainMenu()}
|
|
354
|
+
{currentView === "subtitles" && renderSubtitlesMenu()}
|
|
355
|
+
{currentView === "fontSize" && renderFontSizeMenu()}
|
|
356
|
+
{currentView === "speed" && renderSpeedMenu()}
|
|
357
|
+
</div>
|
|
358
|
+
)}
|
|
359
|
+
</div>
|
|
360
|
+
)
|
|
361
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React, { memo } from "react"
|
|
2
|
+
import { defaultMinimalIcons, MinimalBackward10Icon, MinimalForward10Icon } from "../../../icons"
|
|
3
|
+
|
|
4
|
+
interface PlaybackControlsProps {
|
|
5
|
+
isPlaying: boolean
|
|
6
|
+
onTogglePlay: () => void
|
|
7
|
+
onSeekBackward: () => void
|
|
8
|
+
onSeekForward: () => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const PlaybackControls = function PlaybackControls({
|
|
12
|
+
isPlaying,
|
|
13
|
+
onTogglePlay,
|
|
14
|
+
onSeekBackward,
|
|
15
|
+
onSeekForward
|
|
16
|
+
}: PlaybackControlsProps) {
|
|
17
|
+
const PlayIcon = defaultMinimalIcons.play
|
|
18
|
+
const PauseIcon = defaultMinimalIcons.pause
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="flex items-center gap-2">
|
|
22
|
+
<button
|
|
23
|
+
className="minimal-btn size-15"
|
|
24
|
+
onClick={onTogglePlay}
|
|
25
|
+
aria-label={isPlaying ? "Pause" : "Play"}
|
|
26
|
+
>
|
|
27
|
+
{isPlaying
|
|
28
|
+
? PauseIcon && <PauseIcon size={32} />
|
|
29
|
+
: PlayIcon && <PlayIcon size={32} />}
|
|
30
|
+
</button>
|
|
31
|
+
|
|
32
|
+
<button
|
|
33
|
+
className="minimal-btn h-9 w-9"
|
|
34
|
+
onClick={onSeekBackward}
|
|
35
|
+
aria-label="Backward 10 seconds"
|
|
36
|
+
title="Backward 10s (J)"
|
|
37
|
+
>
|
|
38
|
+
<MinimalBackward10Icon size={18} />
|
|
39
|
+
</button>
|
|
40
|
+
|
|
41
|
+
<button
|
|
42
|
+
className="minimal-btn h-9 w-9"
|
|
43
|
+
onClick={onSeekForward}
|
|
44
|
+
aria-label="Forward 10 seconds"
|
|
45
|
+
title="Forward 10s (L)"
|
|
46
|
+
>
|
|
47
|
+
<MinimalForward10Icon size={18} />
|
|
48
|
+
</button>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import React, { memo, useState, useRef, useEffect } from "react"
|
|
2
|
+
import { cn } from "../../../../lib/utils"
|
|
3
|
+
import {
|
|
4
|
+
MinimalSubtitleIcon,
|
|
5
|
+
MinimalScreenshotIcon,
|
|
6
|
+
defaultMinimalIcons
|
|
7
|
+
} from "../../../icons"
|
|
8
|
+
import { SettingsDropdown } from "./SettingsDropdown"
|
|
9
|
+
|
|
10
|
+
interface RightControlsProps {
|
|
11
|
+
subtitleSrc?: string
|
|
12
|
+
subtitlesEnabled: boolean
|
|
13
|
+
subtitleTracks?: Array<{
|
|
14
|
+
id: string
|
|
15
|
+
label: string
|
|
16
|
+
language?: string
|
|
17
|
+
src?: string
|
|
18
|
+
}>
|
|
19
|
+
currentTrackId?: string | null
|
|
20
|
+
hasSubtitles?: boolean
|
|
21
|
+
subtitleLoadError?: Error | null
|
|
22
|
+
isFullscreen: boolean
|
|
23
|
+
showSettings: boolean
|
|
24
|
+
playbackRate: number
|
|
25
|
+
onToggleSubtitles: () => void
|
|
26
|
+
onSelectSubtitleTrack?: (trackId: string | null) => void
|
|
27
|
+
onTakeScreenshot: (ctrlKey: boolean) => void
|
|
28
|
+
onToggleFullscreen: () => void
|
|
29
|
+
onToggleSettings: () => void
|
|
30
|
+
onChangePlaybackRate: (rate: number) => void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const RightControls = function RightControls({
|
|
34
|
+
subtitleSrc,
|
|
35
|
+
subtitlesEnabled,
|
|
36
|
+
subtitleTracks,
|
|
37
|
+
currentTrackId,
|
|
38
|
+
hasSubtitles,
|
|
39
|
+
subtitleLoadError,
|
|
40
|
+
isFullscreen,
|
|
41
|
+
showSettings,
|
|
42
|
+
playbackRate,
|
|
43
|
+
onToggleSubtitles,
|
|
44
|
+
onSelectSubtitleTrack,
|
|
45
|
+
onTakeScreenshot,
|
|
46
|
+
onToggleFullscreen,
|
|
47
|
+
onToggleSettings,
|
|
48
|
+
onChangePlaybackRate
|
|
49
|
+
}: RightControlsProps) {
|
|
50
|
+
const [showSubtitleMenu, setShowSubtitleMenu] = useState(false)
|
|
51
|
+
const subtitleMenuRef = useRef<HTMLDivElement>(null)
|
|
52
|
+
const FullscreenIcon = defaultMinimalIcons.fullscreen
|
|
53
|
+
const ExitFullscreenIcon = defaultMinimalIcons.exitFullscreen
|
|
54
|
+
|
|
55
|
+
// Close subtitle menu when clicking outside
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
58
|
+
if (subtitleMenuRef.current && !subtitleMenuRef.current.contains(event.target as Node)) {
|
|
59
|
+
setShowSubtitleMenu(false)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (showSubtitleMenu) {
|
|
64
|
+
document.addEventListener("mousedown", handleClickOutside)
|
|
65
|
+
return () => document.removeEventListener("mousedown", handleClickOutside)
|
|
66
|
+
}
|
|
67
|
+
}, [showSubtitleMenu])
|
|
68
|
+
|
|
69
|
+
const handleSubtitleClick = () => {
|
|
70
|
+
if (subtitleTracks && subtitleTracks.length > 0) {
|
|
71
|
+
setShowSubtitleMenu(!showSubtitleMenu)
|
|
72
|
+
} else {
|
|
73
|
+
onToggleSubtitles()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className="flex items-center gap-2">
|
|
79
|
+
{/* Subtitle Button with Dropdown */}
|
|
80
|
+
{hasSubtitles && (
|
|
81
|
+
<div className="relative" ref={subtitleMenuRef}>
|
|
82
|
+
<button
|
|
83
|
+
className={cn(
|
|
84
|
+
"minimal-btn h-9 w-9",
|
|
85
|
+
subtitlesEnabled && "text-cyan-500",
|
|
86
|
+
subtitleLoadError && "text-red-500"
|
|
87
|
+
)}
|
|
88
|
+
onClick={handleSubtitleClick}
|
|
89
|
+
aria-label="Subtitles"
|
|
90
|
+
title={subtitleLoadError ? "Subtitle Error" : "Subtitles (C)"}
|
|
91
|
+
>
|
|
92
|
+
<MinimalSubtitleIcon size={18} />
|
|
93
|
+
</button>
|
|
94
|
+
|
|
95
|
+
{/* Subtitle Track Selection Menu */}
|
|
96
|
+
{showSubtitleMenu && subtitleTracks && subtitleTracks.length > 0 && (
|
|
97
|
+
<div className="absolute right-0 bottom-full mb-2 w-48 rounded-md bg-gray-900/95 py-2 shadow-lg backdrop-blur-sm">
|
|
98
|
+
<div className="px-3 py-1 font-semibold text-gray-400 text-xs">Subtitles</div>
|
|
99
|
+
|
|
100
|
+
{/* Off Option */}
|
|
101
|
+
<button
|
|
102
|
+
className={cn(
|
|
103
|
+
"flex w-full items-center px-3 py-1.5 text-gray-300 text-sm hover:bg-gray-800",
|
|
104
|
+
!subtitlesEnabled && "bg-gray-800 text-cyan-500"
|
|
105
|
+
)}
|
|
106
|
+
onClick={() => {
|
|
107
|
+
onSelectSubtitleTrack?.(null)
|
|
108
|
+
setShowSubtitleMenu(false)
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
Off
|
|
112
|
+
</button>
|
|
113
|
+
|
|
114
|
+
{/* Track Options */}
|
|
115
|
+
{subtitleTracks.map((track) => (
|
|
116
|
+
<button
|
|
117
|
+
key={track.id}
|
|
118
|
+
className={cn(
|
|
119
|
+
"flex w-full items-center px-3 py-1.5 text-gray-300 text-sm hover:bg-gray-800",
|
|
120
|
+
currentTrackId === track.id && "bg-gray-800 text-cyan-500"
|
|
121
|
+
)}
|
|
122
|
+
onClick={() => {
|
|
123
|
+
onSelectSubtitleTrack?.(track.id)
|
|
124
|
+
setShowSubtitleMenu(false)
|
|
125
|
+
}}
|
|
126
|
+
>
|
|
127
|
+
<span>{track.label}</span>
|
|
128
|
+
{track.language && (
|
|
129
|
+
<span className="ml-2 text-gray-500 text-xs">({track.language})</span>
|
|
130
|
+
)}
|
|
131
|
+
</button>
|
|
132
|
+
))}
|
|
133
|
+
|
|
134
|
+
{/* Error Message */}
|
|
135
|
+
{subtitleLoadError && (
|
|
136
|
+
<div className="mt-2 border-gray-800 border-t px-3 py-2">
|
|
137
|
+
<p className="text-red-400 text-xs">Failed to load subtitles</p>
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
144
|
+
|
|
145
|
+
<button
|
|
146
|
+
className="minimal-btn h-9 w-9"
|
|
147
|
+
onClick={(e) => onTakeScreenshot(e.ctrlKey || e.metaKey)}
|
|
148
|
+
aria-label="Take Screenshot"
|
|
149
|
+
title="Screenshot (S) - Ctrl+Click to copy"
|
|
150
|
+
>
|
|
151
|
+
<MinimalScreenshotIcon size={18} />
|
|
152
|
+
</button>
|
|
153
|
+
|
|
154
|
+
<SettingsDropdown
|
|
155
|
+
showSettings={showSettings}
|
|
156
|
+
playbackRate={playbackRate}
|
|
157
|
+
onToggleSettings={onToggleSettings}
|
|
158
|
+
onChangePlaybackRate={onChangePlaybackRate}
|
|
159
|
+
/>
|
|
160
|
+
|
|
161
|
+
<button
|
|
162
|
+
className="minimal-btn h-9 w-9"
|
|
163
|
+
onClick={onToggleFullscreen}
|
|
164
|
+
aria-label={isFullscreen ? "Exit Fullscreen" : "Enter Fullscreen"}
|
|
165
|
+
title="Fullscreen (F)"
|
|
166
|
+
>
|
|
167
|
+
{isFullscreen
|
|
168
|
+
? ExitFullscreenIcon && <ExitFullscreenIcon size={18} />
|
|
169
|
+
: FullscreenIcon && <FullscreenIcon size={18} />}
|
|
170
|
+
</button>
|
|
171
|
+
</div>
|
|
172
|
+
)
|
|
173
|
+
}
|