@banou/media-player 0.5.1 → 0.6.1

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 (43) hide show
  1. package/README.md +6 -6
  2. package/build/components/playback-slider.d.ts +2 -0
  3. package/build/index.js +1159 -1012
  4. package/build/state-machines/index.d.ts +2503 -1838
  5. package/build/state-machines/media.d.ts +218 -51
  6. package/build/state-machines/subtitles.d.ts +3 -0
  7. package/build/utils/use-local-storage.d.ts +1 -1
  8. package/package.json +51 -51
  9. package/src/assets/picture-in-picture.svg +5 -5
  10. package/src/components/chrome.tsx +95 -95
  11. package/src/components/control-bar.tsx +333 -327
  12. package/src/components/overlay.tsx +91 -91
  13. package/src/components/playback-slider.tsx +174 -0
  14. package/src/components/progress-bar.tsx +251 -251
  15. package/src/components/settings.tsx +321 -268
  16. package/src/components/sound.tsx +100 -100
  17. package/src/components/tooltip-display.tsx +83 -83
  18. package/src/components/volume-slider.tsx +156 -156
  19. package/src/index.tsx +195 -195
  20. package/src/main.tsx +169 -169
  21. package/src/state-machines/data-source.ts +84 -84
  22. package/src/state-machines/index.ts +5 -5
  23. package/src/state-machines/media-properties.ts +82 -82
  24. package/src/state-machines/media-source.ts +129 -129
  25. package/src/state-machines/media.ts +255 -252
  26. package/src/state-machines/subtitles.ts +285 -269
  27. package/src/state-machines/thumbnails.ts +123 -123
  28. package/src/state-machines/utils.ts +94 -94
  29. package/src/utils/actor-utils.ts +19 -19
  30. package/src/utils/colors.ts +8 -8
  31. package/src/utils/context.ts +23 -23
  32. package/src/utils/fonts.ts +159 -159
  33. package/src/utils/index.ts +229 -229
  34. package/src/utils/languages.ts +262 -262
  35. package/src/utils/mp4box.ts +74 -74
  36. package/src/utils/time.ts +15 -15
  37. package/src/utils/use-local-storage.ts +30 -30
  38. package/src/utils/use-scrub.ts +40 -40
  39. package/src/utils/volume-utils.ts +39 -39
  40. package/src/utils/window-height.ts +19 -19
  41. package/src/vite-env.d.ts +1 -1
  42. package/tsconfig.json +28 -28
  43. package/tsconfig.node.json +9 -9
@@ -1,327 +1,333 @@
1
- import { ReactNode, RefObject, useCallback, useContext, useEffect, useState } from 'react'
2
- import { css } from '@emotion/react'
3
- import { Maximize, Minimize, Pause, Play, RotateCcw } from 'react-feather'
4
-
5
- import { linearToLogVolume, logToLinearVolume } from '../utils/volume-utils'
6
- import { MediaMachineContext } from '../state-machines'
7
- import { TooltipDisplay } from './tooltip-display'
8
- import { togglePlay } from '../utils/actor-utils'
9
- import { MediaPlayerContext } from '../utils/context'
10
- import { ProgressBar } from './progress-bar'
11
- import { formatMediaTime } from '../utils/time'
12
- import { fonts } from '../utils/fonts'
13
- import pictureInPicture from '../assets/picture-in-picture.svg'
14
- import SettingsAction from './settings'
15
- import colors from '../utils/colors'
16
- import Sound from './sound'
17
-
18
- const style = css`
19
- position: absolute;
20
- bottom: 0;
21
- width: 100%;
22
-
23
- background: linear-gradient(0deg, rgba(0,0,0,0.6) 0%, rgba(0,0,0,0.45) 20%, rgba(0,0,0,0.3) 40%, rgba(0,0,0,0.15) 70%, transparent 100%);
24
- transition: opacity 0.1s cubic-bezier(.4,0,1,1);
25
-
26
- .actions {
27
- display: flex;
28
- justify-content: space-between;
29
-
30
- padding: 6px 12px;
31
- @media (min-width: 768px) {
32
- padding: 8px 16px;
33
- }
34
- @media (min-width: 2560px) {
35
- padding: 8px 24px;
36
- }
37
-
38
- .left, .right {
39
- display: flex;
40
- align-items: center;
41
-
42
- button {
43
- display: flex;
44
- align-items: center;
45
-
46
- outline: none;
47
- border: none;
48
- background: none;
49
-
50
- svg {
51
- width: 18px;
52
- height: 18px;
53
- @media (min-width: 768px) {
54
- width: 24px;
55
- height: 24px;
56
- }
57
- @media (min-width: 2560px) {
58
- width: 28px;
59
- height: 28px;
60
- }
61
- }
62
-
63
- svg {
64
- stroke: #fff;
65
- }
66
- }
67
-
68
- // Add basic styling for all the actions
69
- .play, .sound, .time, .settings, .picture-in-picture, .full-screen {
70
- display: flex;
71
- align-items: center;
72
-
73
- height: 100%;
74
-
75
- border-radius: 4px;
76
- user-select: none;
77
-
78
- padding: 8px;
79
- @media (min-width: 768px) {
80
- padding: 8px 12px;
81
- }
82
- @media (min-width: 2560px) {
83
- padding: 8px 12px;
84
- }
85
- }
86
-
87
- // Add styling to the actions that have some interactivity
88
- .play, .sound, .settings, .picture-in-picture, .full-screen {
89
- border-radius: 4px;
90
-
91
- padding: 8px;
92
- @media (min-width: 768px) {
93
- padding: 8px 12px;
94
- }
95
- @media (min-width: 2560px) {
96
- padding: 8px 12px;
97
- }
98
-
99
- cursor: pointer;
100
-
101
- :hover {
102
- background-color: ${colors.hover};
103
- }
104
- }
105
- }
106
-
107
- .left {
108
- .time {
109
- ${fonts.bMedium.regular}
110
- }
111
- }
112
- .right {
113
-
114
- .picture-in-picture {
115
- img {
116
- width: 22px;
117
- height: 22px;
118
- @media (min-width: 768px) {
119
- width: 28px;
120
- height: 28px;
121
- }
122
- @media (min-width: 2560px) {
123
- width: 32px;
124
- height: 32px;
125
- }
126
- }
127
- }
128
- }
129
- }
130
- `
131
-
132
- export const ControlBar = ({ mediaInformation, containerRef }: { mediaInformation?: ReactNode, containerRef: RefObject<HTMLDivElement> }) => {
133
- const mediaActor = MediaMachineContext.useActorRef()
134
- const chromeContext = useContext(MediaPlayerContext)
135
- const isPaused = MediaMachineContext.useSelector((state) => state.context.media.paused)
136
- const volume = MediaMachineContext.useSelector((state) => state.context.media.volume)
137
- const muted = MediaMachineContext.useSelector((state) => state.context.media.muted)
138
- const currentTime = MediaMachineContext.useSelector((state) => state.context.media.currentTime)
139
- const duration = MediaMachineContext.useSelector((state) => state.context.media.duration)
140
- const [volumeElement, setVolumeElement] = useState<HTMLDivElement | undefined>()
141
-
142
- const [isFullscreen, setIsFullscreen] = useState(false)
143
-
144
- useEffect(() => {
145
- const handleFullscreenChange = () => {
146
- setIsFullscreen(!!document.fullscreenElement)
147
- }
148
- document.addEventListener('fullscreenchange', handleFullscreenChange)
149
- return () => {
150
- document.removeEventListener('fullscreenchange', handleFullscreenChange)
151
- }
152
- }, [])
153
-
154
- const toggleFullScreen = () => {
155
- if (!document.fullscreenElement) {
156
- if (containerRef.current) {
157
- containerRef.current.requestFullscreen()
158
- }
159
- } else {
160
- document.exitFullscreen()
161
- }
162
- }
163
-
164
- const modifyVolume = useCallback(({ direction, stepSize }: { direction: 'up' | 'down', stepSize: number }) => {
165
- if (!mediaActor) return
166
- const linearVolume = logToLinearVolume(volume)
167
- const step = (direction === 'up' ? stepSize : -stepSize)
168
- const newLinearVolume = Math.max(0, Math.min(1, linearVolume + step))
169
- const newLogVolume = linearToLogVolume(newLinearVolume)
170
-
171
- mediaActor.send({
172
- type: 'SET_VOLUME',
173
- muted,
174
- volume: newLogVolume
175
- })
176
- }, [mediaActor.id, muted, volume])
177
-
178
- useEffect(() => {
179
- const eventListener = (ev: KeyboardEvent) => {
180
- let shouldPreventDefault = true
181
- // avoid triggering the browser's default behavior (e.g space for pause it opens the full screen)
182
- if (ev.key === 'f') toggleFullScreen()
183
- else if (ev.key === 'k') togglePlay(mediaActor, isPaused, duration, currentTime)
184
- else if (ev.key === ' ') togglePlay(mediaActor, isPaused, duration, currentTime)
185
- else if (ev.key === 'm') {
186
- mediaActor.send({ type: 'SET_VOLUME', muted: !muted, volume })
187
- }
188
- else if (ev.key === 'ArrowUp') {
189
- modifyVolume({ direction: 'up', stepSize: 0.05 })
190
- }
191
- else if (ev.key === 'ArrowDown') {
192
- modifyVolume({ direction: 'down', stepSize: 0.05 })
193
- }
194
- else if (ev.key === 'ArrowRight') {
195
- if (!duration) return
196
- if (currentTime + 5 >= duration) {
197
- mediaActor.send({ type: 'SET_TIME', value: duration })
198
- } else {
199
- mediaActor.send({ type: 'SET_TIME', value: currentTime + 5 })
200
- }
201
- }
202
- else if (ev.key === 'ArrowLeft') {
203
- if (currentTime - 5 < 0) {
204
- mediaActor.send({ type: 'SET_TIME', value: 0 })
205
- } else {
206
- mediaActor.send({ type: 'SET_TIME', value: currentTime - 5 })
207
- }
208
- }
209
- else {
210
- shouldPreventDefault = false
211
- }
212
-
213
- if (shouldPreventDefault) {
214
- ev.preventDefault()
215
- }
216
- }
217
- window.addEventListener('keydown', eventListener)
218
- return () => window.removeEventListener('keydown', eventListener)
219
- }, [mediaActor, volume, muted, isPaused, currentTime, duration])
220
-
221
- const handleWheel = useCallback((e: WheelEvent) => {
222
- e.preventDefault()
223
- if (e.deltaY < 0) { // scroll up
224
- modifyVolume({ direction: 'up', stepSize: 0.05 })
225
- } else { // scroll down
226
- modifyVolume({ direction: 'down', stepSize: 0.05 })
227
- }
228
- }, [modifyVolume])
229
-
230
- useEffect(() => {
231
- if (!volumeElement) return
232
- volumeElement.addEventListener('wheel', handleWheel, { passive: false })
233
- return () => volumeElement.removeEventListener('wheel', handleWheel)
234
- }, [volumeElement, modifyVolume])
235
-
236
- return (
237
- <div css={style} style={{ ...chromeContext.hideUI ? { opacity: '0', pointerEvents: 'none' } : {} }}>
238
- <ProgressBar />
239
- <div className='actions'>
240
- <div className='left'>
241
- <TooltipDisplay
242
- id='play'
243
- tooltipPlace='top-start'
244
- text={
245
- <button
246
- className='play'
247
- type='button'
248
- onClick={() => togglePlay(mediaActor, isPaused, duration, currentTime)}
249
- >
250
- {
251
- duration === currentTime
252
- ? <RotateCcw />
253
- : isPaused
254
- ? <Play />
255
- : <Pause />
256
- }
257
- </button>
258
- }
259
- toolTipText={
260
- <span>
261
- {
262
- duration === currentTime
263
- ? 'Replay (k)'
264
- : isPaused
265
- ? 'Play (k)'
266
- : 'Pause (k)'
267
- }
268
- </span>
269
- }
270
- />
271
- <Sound ref={setVolumeElement}/>
272
- <div className='time'>
273
- {formatMediaTime(currentTime, duration)}
274
- </div>
275
- </div>
276
- <div className='right'>
277
- {mediaInformation}
278
- <SettingsAction />
279
- <TooltipDisplay
280
- id='picture-in-picture'
281
- text={
282
- <button
283
- className='picture-in-picture'
284
- type='button'
285
- >
286
- <img src={pictureInPicture} />
287
- </button>
288
- }
289
- toolTipText={
290
- <span>
291
- Picture in picture
292
- </span>
293
- }
294
- />
295
- <TooltipDisplay
296
- id='full-screen'
297
- tooltipPlace='top-end'
298
- text={
299
- <button
300
- className='full-screen'
301
- type='button'
302
- onClick={toggleFullScreen}
303
- >
304
- {
305
- isFullscreen
306
- ? <Minimize />
307
- : <Maximize />
308
- }
309
- </button>
310
- }
311
- toolTipText={
312
- <span>
313
- {
314
- isFullscreen
315
- ? 'Exit full screen (f)'
316
- : 'Full screen (f)'
317
- }
318
- </span>
319
- }
320
- />
321
- </div>
322
- </div>
323
- </div>
324
- )
325
- }
326
-
327
- export default ControlBar
1
+ import { ReactNode, RefObject, useCallback, useContext, useEffect, useState } from 'react'
2
+ import { css } from '@emotion/react'
3
+ import { Maximize, Minimize, Pause, Play, RotateCcw } from 'react-feather'
4
+
5
+ import { linearToLogVolume, logToLinearVolume } from '../utils/volume-utils'
6
+ import { MediaMachineContext } from '../state-machines'
7
+ import { TooltipDisplay } from './tooltip-display'
8
+ import { togglePlay } from '../utils/actor-utils'
9
+ import { MediaPlayerContext } from '../utils/context'
10
+ import { ProgressBar } from './progress-bar'
11
+ import { formatMediaTime } from '../utils/time'
12
+ import { fonts } from '../utils/fonts'
13
+ import pictureInPicture from '../assets/picture-in-picture.svg'
14
+ import SettingsAction from './settings'
15
+ import colors from '../utils/colors'
16
+ import Sound from './sound'
17
+ import useLocalStorage, { booleanType } from '../utils/use-local-storage'
18
+
19
+ const style = css`
20
+ position: absolute;
21
+ bottom: 0;
22
+ width: 100%;
23
+
24
+ background: linear-gradient(0deg, rgba(0,0,0,0.6) 0%, rgba(0,0,0,0.45) 20%, rgba(0,0,0,0.3) 40%, rgba(0,0,0,0.15) 70%, transparent 100%);
25
+ transition: opacity 0.1s cubic-bezier(.4,0,1,1);
26
+
27
+ .actions {
28
+ display: flex;
29
+ justify-content: space-between;
30
+
31
+ padding: 6px 12px;
32
+ @media (min-width: 768px) {
33
+ padding: 8px 16px;
34
+ }
35
+ @media (min-width: 2560px) {
36
+ padding: 8px 24px;
37
+ }
38
+
39
+ .left, .right {
40
+ display: flex;
41
+ align-items: center;
42
+
43
+ button {
44
+ display: flex;
45
+ align-items: center;
46
+
47
+ outline: none;
48
+ border: none;
49
+ background: none;
50
+
51
+ svg {
52
+ width: 18px;
53
+ height: 18px;
54
+ @media (min-width: 768px) {
55
+ width: 24px;
56
+ height: 24px;
57
+ }
58
+ @media (min-width: 2560px) {
59
+ width: 28px;
60
+ height: 28px;
61
+ }
62
+ }
63
+
64
+ svg {
65
+ stroke: #fff;
66
+ }
67
+ }
68
+
69
+ // Add basic styling for all the actions
70
+ .play, .sound, .time, .settings, .picture-in-picture, .full-screen {
71
+ display: flex;
72
+ align-items: center;
73
+
74
+ height: 100%;
75
+
76
+ border-radius: 4px;
77
+ user-select: none;
78
+
79
+ padding: 8px;
80
+ @media (min-width: 768px) {
81
+ padding: 8px 12px;
82
+ }
83
+ @media (min-width: 2560px) {
84
+ padding: 8px 12px;
85
+ }
86
+ }
87
+
88
+ // Add styling to the actions that have some interactivity
89
+ .play, .sound, .settings, .picture-in-picture, .full-screen {
90
+ border-radius: 4px;
91
+
92
+ padding: 8px;
93
+ @media (min-width: 768px) {
94
+ padding: 8px 12px;
95
+ }
96
+ @media (min-width: 2560px) {
97
+ padding: 8px 12px;
98
+ }
99
+
100
+ cursor: pointer;
101
+
102
+ :hover {
103
+ background-color: ${colors.hover};
104
+ }
105
+ }
106
+ }
107
+
108
+ .left {
109
+ .time {
110
+ ${fonts.bMedium.regular}
111
+ text-shadow: 0 0 4px rgba(0, 0, 0, 1);
112
+ }
113
+ }
114
+ .right {
115
+ .picture-in-picture {
116
+ img {
117
+ width: 22px;
118
+ height: 22px;
119
+ @media (min-width: 768px) {
120
+ width: 28px;
121
+ height: 28px;
122
+ }
123
+ @media (min-width: 2560px) {
124
+ width: 32px;
125
+ height: 32px;
126
+ }
127
+ }
128
+ }
129
+ }
130
+ }
131
+ `
132
+
133
+ export const ControlBar = ({ mediaInformation, containerRef }: { mediaInformation?: ReactNode, containerRef: RefObject<HTMLDivElement> }) => {
134
+ const mediaActor = MediaMachineContext.useActorRef()
135
+ const chromeContext = useContext(MediaPlayerContext)
136
+ const isPaused = MediaMachineContext.useSelector((state) => state.context.media.paused)
137
+ const volume = MediaMachineContext.useSelector((state) => state.context.media.volume)
138
+ const muted = MediaMachineContext.useSelector((state) => state.context.media.muted)
139
+ const currentTime = MediaMachineContext.useSelector((state) => state.context.media.currentTime)
140
+ const duration = MediaMachineContext.useSelector((state) => state.context.media.duration)
141
+ const [volumeElement, setVolumeElement] = useState<HTMLDivElement | undefined>()
142
+ const [hideMediaStats, _] = useLocalStorage('hideMediaStats', 'false') as [booleanType, (newValue: booleanType) => void]
143
+
144
+ const [isFullscreen, setIsFullscreen] = useState(false)
145
+
146
+ useEffect(() => {
147
+ const handleFullscreenChange = () => {
148
+ setIsFullscreen(!!document.fullscreenElement)
149
+ }
150
+ document.addEventListener('fullscreenchange', handleFullscreenChange)
151
+ return () => {
152
+ document.removeEventListener('fullscreenchange', handleFullscreenChange)
153
+ }
154
+ }, [])
155
+
156
+ const toggleFullScreen = () => {
157
+ if (!document.fullscreenElement) {
158
+ if (containerRef.current) {
159
+ containerRef.current.requestFullscreen()
160
+ }
161
+ } else {
162
+ document.exitFullscreen()
163
+ }
164
+ }
165
+
166
+ const modifyVolume = useCallback(({ direction, stepSize }: { direction: 'up' | 'down', stepSize: number }) => {
167
+ if (!mediaActor) return
168
+ const linearVolume = logToLinearVolume(volume)
169
+ const step = (direction === 'up' ? stepSize : -stepSize)
170
+ const newLinearVolume = Math.max(0, Math.min(1, linearVolume + step))
171
+ const newLogVolume = linearToLogVolume(newLinearVolume)
172
+
173
+ mediaActor.send({
174
+ type: 'SET_VOLUME',
175
+ muted,
176
+ volume: newLogVolume
177
+ })
178
+ }, [mediaActor.id, muted, volume])
179
+
180
+ useEffect(() => {
181
+ const eventListener = (ev: KeyboardEvent) => {
182
+ let shouldPreventDefault = true
183
+ // avoid triggering the browser's default behavior (e.g space for pause it opens the full screen)
184
+ if (ev.key === 'f') toggleFullScreen()
185
+ else if (ev.key === 'k') togglePlay(mediaActor, isPaused, duration, currentTime)
186
+ else if (ev.key === ' ') togglePlay(mediaActor, isPaused, duration, currentTime)
187
+ else if (ev.key === 'm') {
188
+ mediaActor.send({ type: 'SET_VOLUME', muted: !muted, volume })
189
+ }
190
+ else if (ev.key === 'ArrowUp') {
191
+ modifyVolume({ direction: 'up', stepSize: 0.05 })
192
+ }
193
+ else if (ev.key === 'ArrowDown') {
194
+ modifyVolume({ direction: 'down', stepSize: 0.05 })
195
+ }
196
+ else if (ev.key === 'ArrowRight') {
197
+ if (!duration) return
198
+ if (currentTime + 5 >= duration) {
199
+ mediaActor.send({ type: 'SET_TIME', value: duration })
200
+ } else {
201
+ mediaActor.send({ type: 'SET_TIME', value: currentTime + 5 })
202
+ }
203
+ }
204
+ else if (ev.key === 'ArrowLeft') {
205
+ if (currentTime - 5 < 0) {
206
+ mediaActor.send({ type: 'SET_TIME', value: 0 })
207
+ } else {
208
+ mediaActor.send({ type: 'SET_TIME', value: currentTime - 5 })
209
+ }
210
+ }
211
+ else {
212
+ shouldPreventDefault = false
213
+ }
214
+
215
+ if (shouldPreventDefault) {
216
+ ev.preventDefault()
217
+ }
218
+ }
219
+ window.addEventListener('keydown', eventListener)
220
+ return () => window.removeEventListener('keydown', eventListener)
221
+ }, [mediaActor, volume, muted, isPaused, currentTime, duration])
222
+
223
+ const handleWheel = useCallback((e: WheelEvent) => {
224
+ e.preventDefault()
225
+ if (e.deltaY < 0) { // scroll up
226
+ modifyVolume({ direction: 'up', stepSize: 0.05 })
227
+ } else { // scroll down
228
+ modifyVolume({ direction: 'down', stepSize: 0.05 })
229
+ }
230
+ }, [modifyVolume])
231
+
232
+ useEffect(() => {
233
+ if (!volumeElement) return
234
+ volumeElement.addEventListener('wheel', handleWheel, { passive: false })
235
+ return () => volumeElement.removeEventListener('wheel', handleWheel)
236
+ }, [volumeElement, modifyVolume])
237
+
238
+ return (
239
+ <div css={style} style={{ ...chromeContext.hideUI ? { opacity: '0', pointerEvents: 'none' } : {} }}>
240
+ <ProgressBar />
241
+ <div className='actions'>
242
+ <div className='left'>
243
+ <TooltipDisplay
244
+ id='play'
245
+ tooltipPlace='top-start'
246
+ text={
247
+ <button
248
+ className='play'
249
+ type='button'
250
+ onClick={() => togglePlay(mediaActor, isPaused, duration, currentTime)}
251
+ >
252
+ {
253
+ duration === currentTime
254
+ ? <RotateCcw />
255
+ : isPaused
256
+ ? <Play />
257
+ : <Pause />
258
+ }
259
+ </button>
260
+ }
261
+ toolTipText={
262
+ <span>
263
+ {
264
+ duration === currentTime
265
+ ? 'Replay (k)'
266
+ : isPaused
267
+ ? 'Play (k)'
268
+ : 'Pause (k)'
269
+ }
270
+ </span>
271
+ }
272
+ />
273
+ <Sound ref={setVolumeElement}/>
274
+ <div className='time'>
275
+ {formatMediaTime(currentTime, duration)}
276
+ </div>
277
+ </div>
278
+ <div className='right'>
279
+ {
280
+ hideMediaStats !== 'true'
281
+ ? mediaInformation
282
+ : null
283
+ }
284
+ <SettingsAction />
285
+ <TooltipDisplay
286
+ id='picture-in-picture'
287
+ text={
288
+ <button
289
+ className='picture-in-picture'
290
+ type='button'
291
+ >
292
+ <img src={pictureInPicture} />
293
+ </button>
294
+ }
295
+ toolTipText={
296
+ <span>
297
+ Picture in picture
298
+ </span>
299
+ }
300
+ />
301
+ <TooltipDisplay
302
+ id='full-screen'
303
+ tooltipPlace='top-end'
304
+ text={
305
+ <button
306
+ className='full-screen'
307
+ type='button'
308
+ onClick={toggleFullScreen}
309
+ >
310
+ {
311
+ isFullscreen
312
+ ? <Minimize />
313
+ : <Maximize />
314
+ }
315
+ </button>
316
+ }
317
+ toolTipText={
318
+ <span>
319
+ {
320
+ isFullscreen
321
+ ? 'Exit full screen (f)'
322
+ : 'Full screen (f)'
323
+ }
324
+ </span>
325
+ }
326
+ />
327
+ </div>
328
+ </div>
329
+ </div>
330
+ )
331
+ }
332
+
333
+ export default ControlBar