@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.
Files changed (244) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +384 -0
  3. package/dist/context/media.js +2 -0
  4. package/dist/context/media.js.map +1 -0
  5. package/dist/context/media.mjs +15 -0
  6. package/dist/context/media.mjs.map +1 -0
  7. package/dist/context/store.js +2 -0
  8. package/dist/context/store.js.map +1 -0
  9. package/dist/context/store.mjs +5 -0
  10. package/dist/context/store.mjs.map +1 -0
  11. package/dist/context-BMteAJDN.js +1140 -0
  12. package/dist/context-BMteAJDN.js.map +1 -0
  13. package/dist/context-DCJ_scmO.cjs +23 -0
  14. package/dist/context-DCJ_scmO.cjs.map +1 -0
  15. package/dist/core-events-B5W_j8yU.js +480 -0
  16. package/dist/core-events-B5W_j8yU.js.map +1 -0
  17. package/dist/core-events-CN0-Vhhq.cjs +2 -0
  18. package/dist/core-events-CN0-Vhhq.cjs.map +1 -0
  19. package/dist/core-plugins-Bq9KLany.js +696 -0
  20. package/dist/core-plugins-Bq9KLany.js.map +1 -0
  21. package/dist/core-plugins-CPDSHMiQ.cjs +2 -0
  22. package/dist/core-plugins-CPDSHMiQ.cjs.map +1 -0
  23. package/dist/core.js +37 -0
  24. package/dist/core.js.map +1 -0
  25. package/dist/core.mjs +1217 -0
  26. package/dist/core.mjs.map +1 -0
  27. package/dist/enhanced-components-DHv0MFvG.js +1073 -0
  28. package/dist/enhanced-components-DHv0MFvG.js.map +1 -0
  29. package/dist/enhanced-components-DTIObgB7.cjs +2 -0
  30. package/dist/enhanced-components-DTIObgB7.cjs.map +1 -0
  31. package/dist/hooks-C_r8wexn.cjs +4 -0
  32. package/dist/hooks-C_r8wexn.cjs.map +1 -0
  33. package/dist/hooks-Da2wVXlF.js +1630 -0
  34. package/dist/hooks-Da2wVXlF.js.map +1 -0
  35. package/dist/index.css +1 -0
  36. package/dist/index.js +112 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/index.mjs +6126 -0
  39. package/dist/index.mjs.map +1 -0
  40. package/dist/minimal-components-DSq5sQpy.js +2002 -0
  41. package/dist/minimal-components-DSq5sQpy.js.map +1 -0
  42. package/dist/minimal-components-DflWiihN.cjs +2 -0
  43. package/dist/minimal-components-DflWiihN.cjs.map +1 -0
  44. package/dist/minimal-components.css +1 -0
  45. package/dist/mona-loading.gif +0 -0
  46. package/dist/performance-monitor-CUW6f5ll.cjs +2 -0
  47. package/dist/performance-monitor-CUW6f5ll.cjs.map +1 -0
  48. package/dist/performance-monitor-rMW1RgV3.js +302 -0
  49. package/dist/performance-monitor-rMW1RgV3.js.map +1 -0
  50. package/dist/utils-BnscpYYd.js +2798 -0
  51. package/dist/utils-BnscpYYd.js.map +1 -0
  52. package/dist/utils-D_czS_0K.cjs +9 -0
  53. package/dist/utils-D_czS_0K.cjs.map +1 -0
  54. package/dist/utils.js +2 -0
  55. package/dist/utils.js.map +1 -0
  56. package/dist/utils.mjs +7 -0
  57. package/dist/utils.mjs.map +1 -0
  58. package/package.json +114 -0
  59. package/src/adapters/videojs-adapter.tsx +470 -0
  60. package/src/assets/mona-loading.gif +0 -0
  61. package/src/components/AnimateLoading.tsx +115 -0
  62. package/src/components/LoadingState.tsx +248 -0
  63. package/src/components/accessibility/keyboard-help-overlay.tsx +213 -0
  64. package/src/components/audio-player.tsx +85 -0
  65. package/src/components/basic/index.ts +37 -0
  66. package/src/components/enhanced/advanced-features.tsx +316 -0
  67. package/src/components/enhanced/debug-panel.tsx +422 -0
  68. package/src/components/enhanced/enhanced-controls.tsx +600 -0
  69. package/src/components/enhanced/enhanced-media-player.tsx +509 -0
  70. package/src/components/enhanced/error-handling.tsx +345 -0
  71. package/src/components/enhanced/index.ts +39 -0
  72. package/src/components/enhanced/media-player-enhanced.tsx +349 -0
  73. package/src/components/examples/basic-video-player.tsx +55 -0
  74. package/src/components/examples/custom-controls-example.tsx +63 -0
  75. package/src/components/examples/enhanced-controls-example.tsx +114 -0
  76. package/src/components/examples/enhanced-media-player-example.tsx +293 -0
  77. package/src/components/examples/loading-states-demo.tsx +142 -0
  78. package/src/components/examples/react-19-demo.tsx +381 -0
  79. package/src/components/examples/volume-orientation-demo.tsx +114 -0
  80. package/src/components/icon-demo.tsx +209 -0
  81. package/src/components/icons/index.tsx +166 -0
  82. package/src/components/icons/svg-icons/ArrowLeftIcon.tsx +18 -0
  83. package/src/components/icons/svg-icons/ArrowRightIcon.tsx +18 -0
  84. package/src/components/icons/svg-icons/AudioIcon.tsx +22 -0
  85. package/src/components/icons/svg-icons/BackwardIcon.tsx +12 -0
  86. package/src/components/icons/svg-icons/CameraIcon.tsx +19 -0
  87. package/src/components/icons/svg-icons/CheckIcon.tsx +19 -0
  88. package/src/components/icons/svg-icons/EnablePiPIcon.tsx +17 -0
  89. package/src/components/icons/svg-icons/ForwardIcon.tsx +12 -0
  90. package/src/components/icons/svg-icons/FullscreenEnterIcon.tsx +23 -0
  91. package/src/components/icons/svg-icons/FullscreenExitIcon.tsx +23 -0
  92. package/src/components/icons/svg-icons/LoadingIcon.tsx +32 -0
  93. package/src/components/icons/svg-icons/PauseIcon.tsx +23 -0
  94. package/src/components/icons/svg-icons/PlayIcon.tsx +21 -0
  95. package/src/components/icons/svg-icons/PlaybackSpeedIcon.tsx +23 -0
  96. package/src/components/icons/svg-icons/QualityIcon.tsx +21 -0
  97. package/src/components/icons/svg-icons/SettingsIcon.tsx +12 -0
  98. package/src/components/icons/svg-icons/SliderIcon.tsx +21 -0
  99. package/src/components/icons/svg-icons/SubtitleIcon.tsx +21 -0
  100. package/src/components/icons/svg-icons/SubtitleOffIcon.tsx +25 -0
  101. package/src/components/icons/svg-icons/UnPiPIcon.tsx +17 -0
  102. package/src/components/icons/svg-icons/VolumeMutedIcon.tsx +16 -0
  103. package/src/components/icons/svg-icons/VolumeOneIcon.tsx +22 -0
  104. package/src/components/icons/svg-icons/VolumeThreeIcon.tsx +16 -0
  105. package/src/components/icons/svg-icons/VolumeTwoIcon.tsx +16 -0
  106. package/src/components/legacy.tsx +93 -0
  107. package/src/components/media-metadata-display.tsx +100 -0
  108. package/src/components/media-player-audio.tsx +39 -0
  109. package/src/components/media-player-controls.tsx +226 -0
  110. package/src/components/media-player-error.tsx +163 -0
  111. package/src/components/media-player-fullscreen.tsx +109 -0
  112. package/src/components/media-player-hls.tsx +111 -0
  113. package/src/components/media-player-loading.tsx +76 -0
  114. package/src/components/media-player-play.tsx +75 -0
  115. package/src/components/media-player-root.tsx +340 -0
  116. package/src/components/media-player-seek.tsx +168 -0
  117. package/src/components/media-player-time.tsx +99 -0
  118. package/src/components/media-player-tooltip.tsx +58 -0
  119. package/src/components/media-player-video.tsx +39 -0
  120. package/src/components/media-player-volume.tsx +155 -0
  121. package/src/components/media-player.tsx +257 -0
  122. package/src/components/media-suspense.tsx +362 -0
  123. package/src/components/player-settings-form.tsx +163 -0
  124. package/src/components/react-player-wrapper.tsx +651 -0
  125. package/src/components/simple-video-player.tsx +60 -0
  126. package/src/components/ui/enhanced-seek-bar.tsx +235 -0
  127. package/src/components/ui/fullscreen-button.tsx +60 -0
  128. package/src/components/ui/media-resource-preloader.tsx +395 -0
  129. package/src/components/ui/play-button.tsx +107 -0
  130. package/src/components/ui/seek-bar.tsx +161 -0
  131. package/src/components/ui/time-display.tsx +40 -0
  132. package/src/components/ui/volume-control.tsx +143 -0
  133. package/src/components/unified/base-media-player.tsx +246 -0
  134. package/src/components/unified/media-player.tsx +75 -0
  135. package/src/components/variants/minimal/AccessibleMinimalPlayer.tsx +561 -0
  136. package/src/components/variants/minimal/EnhancedMinimalMediaPlayer.tsx +552 -0
  137. package/src/components/variants/minimal/MinimalControls.tsx +142 -0
  138. package/src/components/variants/minimal/MinimalLoading.tsx +32 -0
  139. package/src/components/variants/minimal/MinimalMediaPlayer.tsx +315 -0
  140. package/src/components/variants/minimal/MinimalOverlay.tsx +64 -0
  141. package/src/components/variants/minimal/MinimalPlayButton.tsx +65 -0
  142. package/src/components/variants/minimal/MinimalSeekBar.tsx +150 -0
  143. package/src/components/variants/minimal/MinimalVolumeControl.tsx +80 -0
  144. package/src/components/variants/minimal/SimpleMinimalPlayer.tsx +210 -0
  145. package/src/components/variants/minimal/components/CenterPlayButton.tsx +22 -0
  146. package/src/components/variants/minimal/components/ControlBar.tsx +146 -0
  147. package/src/components/variants/minimal/components/EnhancedSettingsMenu.tsx +361 -0
  148. package/src/components/variants/minimal/components/PlaybackControls.tsx +51 -0
  149. package/src/components/variants/minimal/components/RightControls.tsx +173 -0
  150. package/src/components/variants/minimal/components/SeekBar.tsx +131 -0
  151. package/src/components/variants/minimal/components/SettingsDropdown.tsx +70 -0
  152. package/src/components/variants/minimal/components/SubtitleDisplay.tsx +21 -0
  153. package/src/components/variants/minimal/components/TimeDisplay.tsx +18 -0
  154. package/src/components/variants/minimal/components/VideoElement.tsx +40 -0
  155. package/src/components/variants/minimal/components/VolumeControl.tsx +217 -0
  156. package/src/components/variants/minimal/index.ts +23 -0
  157. package/src/components/video-player.tsx +100 -0
  158. package/src/context/error-handling.ts +394 -0
  159. package/src/context/media-context.tsx +208 -0
  160. package/src/context/media-provider.tsx +265 -0
  161. package/src/context/media-store.ts +379 -0
  162. package/src/context/persistence.ts +453 -0
  163. package/src/core/events/event-system.ts +442 -0
  164. package/src/core/events/hooks.ts +302 -0
  165. package/src/core/events/index.ts +22 -0
  166. package/src/core/events/middleware.ts +269 -0
  167. package/src/core/index.ts +22 -0
  168. package/src/core/optimization/bundle-optimizer.tsx +308 -0
  169. package/src/core/performance/cicd.ts +96 -0
  170. package/src/core/performance/index.ts +38 -0
  171. package/src/core/performance/media-loading-optimizer.ts +538 -0
  172. package/src/core/performance/performance-budgets.ts +432 -0
  173. package/src/core/performance/performance-monitor.tsx +476 -0
  174. package/src/core/performance/performance-testing.ts +459 -0
  175. package/src/core/plugins/base-plugin.ts +224 -0
  176. package/src/core/plugins/enhanced-plugin-system.ts +411 -0
  177. package/src/core/plugins/examples/analytics-plugin.ts +242 -0
  178. package/src/core/plugins/index.ts +39 -0
  179. package/src/core/plugins/registry.ts +327 -0
  180. package/src/core/plugins/types.ts +159 -0
  181. package/src/core/plugins/utils.ts +316 -0
  182. package/src/core/progressive-loading.tsx +322 -0
  183. package/src/core/providers/enhanced-hls-provider.ts +339 -0
  184. package/src/core/providers/service-provider.tsx +168 -0
  185. package/src/core/utils.tsx +38 -0
  186. package/src/features/media-session/media-session-manager.ts +429 -0
  187. package/src/features/picture-in-picture/pip-manager.ts +429 -0
  188. package/src/features/playlist/playlist-manager.ts +606 -0
  189. package/src/features/subtitles/subtitle-manager.ts +441 -0
  190. package/src/features/subtitles/subtitle-parser.ts +280 -0
  191. package/src/features/thumbnails/thumbnail-generator.ts +519 -0
  192. package/src/hooks/accessibility/index.ts +27 -0
  193. package/src/hooks/accessibility/use-accessibility-preferences.ts +188 -0
  194. package/src/hooks/accessibility/use-focus-management.ts +176 -0
  195. package/src/hooks/accessibility/use-keyboard-shortcuts.ts +144 -0
  196. package/src/hooks/accessibility/use-screen-reader.ts +182 -0
  197. package/src/hooks/core/index.ts +47 -0
  198. package/src/hooks/legacy.ts +27 -0
  199. package/src/hooks/use-enhanced-media-player.ts +468 -0
  200. package/src/hooks/use-intelligent-preloading.ts +358 -0
  201. package/src/hooks/use-keyboard-controls.ts +115 -0
  202. package/src/hooks/use-media-actions.ts +175 -0
  203. package/src/hooks/use-media-metadata.ts +89 -0
  204. package/src/hooks/use-media-optimistic.ts +270 -0
  205. package/src/hooks/use-media-player.ts +312 -0
  206. package/src/hooks/use-media-state.ts +190 -0
  207. package/src/hooks/use-memory-optimization.ts +358 -0
  208. package/src/hooks/use-optimistic-controls.ts +92 -0
  209. package/src/hooks/use-optimized-media.ts +263 -0
  210. package/src/hooks/use-touch-gestures.ts +417 -0
  211. package/src/hooks/useEnhancedSubtitles.ts +258 -0
  212. package/src/hooks/useKeyboardShortcuts.ts +74 -0
  213. package/src/hooks/useMediaControls.ts +52 -0
  214. package/src/hooks/useScreenshot.ts +57 -0
  215. package/src/hooks/useSubtitles.ts +91 -0
  216. package/src/hooks/useVideoPlayer.ts +232 -0
  217. package/src/icons/components/FullscreenIcon.tsx +37 -0
  218. package/src/icons/components/SettingsIcon.tsx +38 -0
  219. package/src/icons/components/animated/LoadingIcon.tsx +43 -0
  220. package/src/icons/components/animated/PlayPauseIcon.tsx +50 -0
  221. package/src/icons/components/media/PauseIcon.tsx +39 -0
  222. package/src/icons/components/media/PlayIcon.tsx +38 -0
  223. package/src/icons/components/volume/VolumeIcon.tsx +39 -0
  224. package/src/icons/components/volume/VolumeOffIcon.tsx +39 -0
  225. package/src/icons/index.ts +56 -0
  226. package/src/icons/provider.tsx +64 -0
  227. package/src/icons/types.ts +40 -0
  228. package/src/index-enhanced.ts +35 -0
  229. package/src/index-legacy.ts +326 -0
  230. package/src/index.css +102 -0
  231. package/src/index.ts +216 -0
  232. package/src/lib/deprecation.ts +51 -0
  233. package/src/lib/format-time.ts +37 -0
  234. package/src/lib/index.ts +3 -0
  235. package/src/lib/utils.ts +6 -0
  236. package/src/styles/globals.css +462 -0
  237. package/src/styles/minimal-player.css +589 -0
  238. package/src/styles/pixel-art.css +46 -0
  239. package/src/styles/videojs.css +100 -0
  240. package/src/test/setup.ts +79 -0
  241. package/src/types/events.ts +47 -0
  242. package/src/types/formats.ts +95 -0
  243. package/src/types/index.ts +190 -0
  244. package/src/types/videojs-plugins.d.ts +14 -0
@@ -0,0 +1,316 @@
1
+ "use client"
2
+
3
+ import React from "react"
4
+ import { useMediaSelector, useMediaDispatch, MediaActionTypes } from "../../context/media-context"
5
+ import { cn } from "../../lib/utils"
6
+ import { formatTime } from "../../lib/format-time"
7
+
8
+ // Types for advanced features
9
+ export interface Chapter {
10
+ id: string
11
+ title: string
12
+ startTime: number
13
+ endTime: number
14
+ thumbnail?: string
15
+ }
16
+
17
+ export interface QualityLevel {
18
+ id: string
19
+ label: string
20
+ width: number
21
+ height: number
22
+ bitrate?: number
23
+ }
24
+
25
+ export interface Subtitle {
26
+ id: string
27
+ label: string
28
+ language: string
29
+ src: string
30
+ default?: boolean
31
+ }
32
+
33
+ // Quality selector component
34
+ export const QualitySelector = function QualitySelector({
35
+ qualities,
36
+ currentQuality,
37
+ onQualityChange,
38
+ className
39
+ }: {
40
+ qualities: QualityLevel[]
41
+ currentQuality: string
42
+ onQualityChange: (qualityId: string) => void
43
+ className?: string
44
+ }) {
45
+ const [isOpen, setIsOpen] = React.useState(false)
46
+
47
+ const currentQualityLabel = React.useMemo(() => {
48
+ return qualities.find(q => q.id === currentQuality)?.label || "Auto"
49
+ }, [qualities, currentQuality])
50
+
51
+ return (
52
+ <div className={cn("relative", className)}>
53
+ <button
54
+ type="button"
55
+ onClick={() => setIsOpen(!isOpen)}
56
+ className="flex items-center gap-2 rounded-lg bg-black/50 px-3 py-2 text-white transition-colors hover:bg-black/70"
57
+ aria-label="Select quality"
58
+ >
59
+ <svg viewBox="0 0 24 24" fill="currentColor" className="h-4 w-4">
60
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
61
+ </svg>
62
+ <span className="text-sm">{currentQualityLabel}</span>
63
+ <svg
64
+ viewBox="0 0 24 24"
65
+ fill="currentColor"
66
+ className={cn("h-4 w-4 transition-transform", isOpen && "rotate-180")}
67
+ >
68
+ <path d="M7 10l5 5 5-5z"/>
69
+ </svg>
70
+ </button>
71
+
72
+ {isOpen && (
73
+ <div className="absolute right-0 bottom-full mb-2 min-w-32 rounded-lg border border-white/20 bg-black/90 py-2 backdrop-blur-sm">
74
+ {qualities.map((quality) => (
75
+ <button
76
+ key={quality.id}
77
+ onClick={() => {
78
+ onQualityChange(quality.id)
79
+ setIsOpen(false)
80
+ }}
81
+ className={cn(
82
+ "w-full px-4 py-2 text-left text-sm text-white transition-colors hover:bg-white/10",
83
+ quality.id === currentQuality && "bg-primary/20 text-primary"
84
+ )}
85
+ >
86
+ {quality.label}
87
+ {quality.bitrate && (
88
+ <span className="ml-2 text-white/60 text-xs">
89
+ ({Math.round(quality.bitrate / 1000)}k)
90
+ </span>
91
+ )}
92
+ </button>
93
+ ))}
94
+ </div>
95
+ )}
96
+ </div>
97
+ )
98
+ }
99
+
100
+ // Subtitle selector component
101
+ export const SubtitleSelector = function SubtitleSelector({
102
+ subtitles,
103
+ currentSubtitle,
104
+ onSubtitleChange,
105
+ className
106
+ }: {
107
+ subtitles: Subtitle[]
108
+ currentSubtitle: string | null
109
+ onSubtitleChange: (subtitleId: string | null) => void
110
+ className?: string
111
+ }) {
112
+ const [isOpen, setIsOpen] = React.useState(false)
113
+
114
+ const currentSubtitleLabel = React.useMemo(() => {
115
+ if (!currentSubtitle) return "Off"
116
+ return subtitles.find(s => s.id === currentSubtitle)?.label || "Off"
117
+ }, [subtitles, currentSubtitle])
118
+
119
+ return (
120
+ <div className={cn("relative", className)}>
121
+ <button
122
+ type="button"
123
+ onClick={() => setIsOpen(!isOpen)}
124
+ className="flex items-center gap-2 rounded-lg bg-black/50 px-3 py-2 text-white transition-colors hover:bg-black/70"
125
+ aria-label="Select subtitles"
126
+ >
127
+ <svg viewBox="0 0 24 24" fill="currentColor" className="h-4 w-4">
128
+ <path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zM4 12h4v2H4v-2zm10 6H4v-2h10v2zm6 0h-4v-2h4v2zm0-4H10v-2h10v2z"/>
129
+ </svg>
130
+ <span className="text-sm">{currentSubtitleLabel}</span>
131
+ <svg
132
+ viewBox="0 0 24 24"
133
+ fill="currentColor"
134
+ className={cn("h-4 w-4 transition-transform", isOpen && "rotate-180")}
135
+ >
136
+ <path d="M7 10l5 5 5-5z"/>
137
+ </svg>
138
+ </button>
139
+
140
+ {isOpen && (
141
+ <div className="absolute right-0 bottom-full mb-2 min-w-32 rounded-lg border border-white/20 bg-black/90 py-2 backdrop-blur-sm">
142
+ <button
143
+ onClick={() => {
144
+ onSubtitleChange(null)
145
+ setIsOpen(false)
146
+ }}
147
+ className={cn(
148
+ "w-full px-4 py-2 text-left text-sm text-white transition-colors hover:bg-white/10",
149
+ !currentSubtitle && "bg-primary/20 text-primary"
150
+ )}
151
+ >
152
+ Off
153
+ </button>
154
+ {subtitles.map((subtitle) => (
155
+ <button
156
+ key={subtitle.id}
157
+ onClick={() => {
158
+ onSubtitleChange(subtitle.id)
159
+ setIsOpen(false)
160
+ }}
161
+ className={cn(
162
+ "w-full px-4 py-2 text-left text-sm text-white transition-colors hover:bg-white/10",
163
+ subtitle.id === currentSubtitle && "bg-primary/20 text-primary"
164
+ )}
165
+ >
166
+ {subtitle.label}
167
+ </button>
168
+ ))}
169
+ </div>
170
+ )}
171
+ </div>
172
+ )
173
+ }
174
+
175
+ // Playback speed selector
176
+ export const PlaybackSpeedSelector = function PlaybackSpeedSelector({
177
+ className
178
+ }: {
179
+ className?: string
180
+ }) {
181
+ const dispatch = useMediaDispatch()
182
+ const playbackRate = useMediaSelector(state => state.playbackRate)
183
+ const [isOpen, setIsOpen] = React.useState(false)
184
+
185
+ const speeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]
186
+
187
+ const handleSpeedChange = React.useCallback((speed: number) => {
188
+ dispatch({ type: MediaActionTypes.SET_PLAYBACK_RATE, detail: speed })
189
+ setIsOpen(false)
190
+ }, [dispatch])
191
+
192
+ return (
193
+ <div className={cn("relative", className)}>
194
+ <button
195
+ type="button"
196
+ onClick={() => setIsOpen(!isOpen)}
197
+ className="flex items-center gap-2 rounded-lg bg-black/50 px-3 py-2 text-white transition-colors hover:bg-black/70"
198
+ aria-label="Select playback speed"
199
+ >
200
+ <svg viewBox="0 0 24 24" fill="currentColor" className="h-4 w-4">
201
+ <path d="M13,8A4,4 0 0,0 9,12A4,4 0 0,0 13,16A4,4 0 0,0 17,12A4,4 0 0,0 13,8M13,14A2,2 0 0,1 11,12A2,2 0 0,1 13,10A2,2 0 0,1 15,12A2,2 0 0,1 13,14M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4Z"/>
202
+ </svg>
203
+ <span className="text-sm">{playbackRate}x</span>
204
+ <svg
205
+ viewBox="0 0 24 24"
206
+ fill="currentColor"
207
+ className={cn("h-4 w-4 transition-transform", isOpen && "rotate-180")}
208
+ >
209
+ <path d="M7 10l5 5 5-5z"/>
210
+ </svg>
211
+ </button>
212
+
213
+ {isOpen && (
214
+ <div className="absolute right-0 bottom-full mb-2 min-w-20 rounded-lg border border-white/20 bg-black/90 py-2 backdrop-blur-sm">
215
+ {speeds.map((speed) => (
216
+ <button
217
+ key={speed}
218
+ onClick={() => handleSpeedChange(speed)}
219
+ className={cn(
220
+ "w-full px-4 py-2 text-left text-sm text-white transition-colors hover:bg-white/10",
221
+ speed === playbackRate && "bg-primary/20 text-primary"
222
+ )}
223
+ >
224
+ {speed}x
225
+ </button>
226
+ ))}
227
+ </div>
228
+ )}
229
+ </div>
230
+ )
231
+ }
232
+
233
+ // Chapters component
234
+ export const ChaptersPanel = function ChaptersPanel({
235
+ chapters,
236
+ onChapterSelect,
237
+ className
238
+ }: {
239
+ chapters: Chapter[]
240
+ onChapterSelect: (chapter: Chapter) => void
241
+ className?: string
242
+ }) {
243
+ const currentTime = useMediaSelector(state => state.currentTime)
244
+
245
+ const currentChapter = React.useMemo(() => {
246
+ return chapters.find(chapter =>
247
+ currentTime >= chapter.startTime && currentTime < chapter.endTime
248
+ )
249
+ }, [chapters, currentTime])
250
+
251
+ return (
252
+ <div className={cn("rounded-lg border border-white/20 bg-black/90 p-4 backdrop-blur-sm", className)}>
253
+ <h3 className="mb-3 font-semibold text-white">Chapters</h3>
254
+ <div className="max-h-64 space-y-2 overflow-y-auto">
255
+ {chapters.map((chapter) => (
256
+ <button
257
+ key={chapter.id}
258
+ onClick={() => onChapterSelect(chapter)}
259
+ className={cn(
260
+ "flex w-full items-center gap-3 rounded-lg p-3 text-left transition-colors",
261
+ "hover:bg-white/10",
262
+ chapter.id === currentChapter?.id && "border border-primary/30 bg-primary/20"
263
+ )}
264
+ >
265
+ {chapter.thumbnail && (
266
+ <img
267
+ src={chapter.thumbnail}
268
+ alt={chapter.title}
269
+ className="h-9 w-16 rounded object-cover"
270
+ />
271
+ )}
272
+ <div className="min-w-0 flex-1">
273
+ <div className="truncate font-medium text-white">{chapter.title}</div>
274
+ <div className="text-sm text-white/60">
275
+ {formatTime(chapter.startTime)} - {formatTime(chapter.endTime)}
276
+ </div>
277
+ </div>
278
+ </button>
279
+ ))}
280
+ </div>
281
+ </div>
282
+ )
283
+ }
284
+
285
+ // Picture-in-Picture button
286
+ export const PictureInPictureButton = function PictureInPictureButton({
287
+ className
288
+ }: {
289
+ className?: string
290
+ }) {
291
+ const dispatch = useMediaDispatch()
292
+ const isPictureInPicture = useMediaSelector(state => state.isPictureInPicture)
293
+
294
+ const handleTogglePiP = React.useCallback(() => {
295
+ dispatch({
296
+ type: isPictureInPicture ? MediaActionTypes.EXIT_PIP : MediaActionTypes.ENTER_PIP
297
+ })
298
+ }, [dispatch, isPictureInPicture])
299
+
300
+ return (
301
+ <button
302
+ type="button"
303
+ onClick={handleTogglePiP}
304
+ className={cn(
305
+ "flex h-10 w-10 items-center justify-center rounded-full text-white",
306
+ "transition-colors hover:bg-white/10",
307
+ className
308
+ )}
309
+ aria-label={isPictureInPicture ? "Exit Picture-in-Picture" : "Enter Picture-in-Picture"}
310
+ >
311
+ <svg viewBox="0 0 24 24" fill="currentColor" className="h-5 w-5">
312
+ <path d="M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14z"/>
313
+ </svg>
314
+ </button>
315
+ )
316
+ }