@banou/media-player 0.6.0 → 0.6.2

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 (37) hide show
  1. package/README.md +6 -6
  2. package/package.json +51 -51
  3. package/src/assets/picture-in-picture.svg +5 -5
  4. package/src/components/chrome.tsx +95 -95
  5. package/src/components/control-bar.tsx +333 -333
  6. package/src/components/overlay.tsx +91 -91
  7. package/src/components/playback-slider.tsx +174 -174
  8. package/src/components/progress-bar.tsx +251 -251
  9. package/src/components/settings.tsx +321 -321
  10. package/src/components/sound.tsx +100 -100
  11. package/src/components/tooltip-display.tsx +83 -83
  12. package/src/components/volume-slider.tsx +156 -156
  13. package/src/index.tsx +195 -195
  14. package/src/main.tsx +169 -169
  15. package/src/state-machines/data-source.ts +84 -84
  16. package/src/state-machines/index.ts +5 -5
  17. package/src/state-machines/media-properties.ts +82 -82
  18. package/src/state-machines/media-source.ts +129 -129
  19. package/src/state-machines/media.ts +255 -255
  20. package/src/state-machines/subtitles.ts +285 -285
  21. package/src/state-machines/thumbnails.ts +123 -123
  22. package/src/state-machines/utils.ts +94 -94
  23. package/src/utils/actor-utils.ts +19 -19
  24. package/src/utils/colors.ts +8 -8
  25. package/src/utils/context.ts +23 -23
  26. package/src/utils/fonts.ts +159 -159
  27. package/src/utils/index.ts +229 -229
  28. package/src/utils/languages.ts +262 -262
  29. package/src/utils/mp4box.ts +74 -74
  30. package/src/utils/time.ts +15 -15
  31. package/src/utils/use-local-storage.ts +30 -30
  32. package/src/utils/use-scrub.ts +40 -40
  33. package/src/utils/volume-utils.ts +39 -39
  34. package/src/utils/window-height.ts +19 -19
  35. package/src/vite-env.d.ts +1 -1
  36. package/tsconfig.json +28 -28
  37. package/tsconfig.node.json +9 -9
@@ -1,333 +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
- 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
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