@banou/media-player 0.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.
@@ -0,0 +1,651 @@
1
+ /// <reference types="@emotion/react/types/css-prop" />
2
+ import type { Dispatch, DOMAttributes, HTMLAttributes, MouseEvent, MouseEventHandler, SetStateAction } from 'react'
3
+
4
+ import type { ChromeOptions } from '.'
5
+ import type { FKNVideoControl, Subtitle, TransmuxError } from '..'
6
+
7
+ import ReactTooltip from 'react-tooltip'
8
+ import { css } from '@emotion/react'
9
+ import { useEffect, useMemo, useRef, useState } from 'react'
10
+ import { Volume, Volume1, Volume2, VolumeX, AlertTriangle } from 'react-feather'
11
+
12
+ import useScrub from '../use-scrub'
13
+ import { tagToLanguage } from '../languages'
14
+ import useNamespacedLocalStorage from '../use-local-storage'
15
+
16
+ const style = css`
17
+ position: relative;
18
+ grid-column: 1;
19
+ grid-row: 1;
20
+ align-self: end;
21
+ height: calc(4.8rem + var(--background-padding));
22
+ width: 100%;
23
+ padding: 0 2rem;
24
+ margin: 0 auto;
25
+
26
+ /* subtle background black gradient for scenes with high brightness to properly display white progress bar */
27
+ padding-top: var(--background-padding);
28
+ background: linear-gradient(0deg, rgba(0,0,0,0.4) 0%, rgba(0,0,0,0.1) calc(100% - 1rem), rgba(0,0,0,0) 100%);
29
+
30
+ .progress-bar {
31
+ position: relative;
32
+ height: .4rem;
33
+ cursor: pointer;
34
+ user-select: none;
35
+ transition: transform .2s ease-in-out;
36
+
37
+
38
+ &:hover {
39
+ .background-bar {
40
+ transform: scaleY(1.5);
41
+ }
42
+ }
43
+ .background-bar {
44
+ position: absolute;
45
+ inset: 0;
46
+ background-color: hsla(0, 100%, 100%, .2);
47
+ }
48
+
49
+ .cursor-time {
50
+ pointer-events: none;
51
+ position: absolute;
52
+ top: -2rem;
53
+ width: 5rem;
54
+ margin-left: -2.5rem;
55
+ display: flex;
56
+ justify-content: center;
57
+ }
58
+
59
+ .load {
60
+ transform-origin: 0 0;
61
+ background-color: hsla(0, 100%, 100%, .4);
62
+ position: absolute;
63
+ bottom: 0;
64
+ height: .4rem;
65
+ width: 100%;
66
+ }
67
+ .play {
68
+ transform-origin: 0 0;
69
+ background-color: hsla(0, 100%, 50%, .8);
70
+ position: absolute;
71
+ bottom: 0;
72
+ height: .4rem;
73
+ width: 100%;
74
+ }
75
+ .padding {
76
+ position: absolute;
77
+ bottom: -4px;
78
+ height: 1.6rem;
79
+ width: 100%;
80
+ }
81
+ }
82
+
83
+ .controls {
84
+ height: 100%;
85
+ display: grid;
86
+ align-items: center;
87
+ grid-template-columns: 5rem fit-content(4.8rem) 20rem auto fit-content(10rem) 5rem 5rem 5rem;
88
+ grid-gap: 1rem;
89
+ color: #fff;
90
+ user-select: none;
91
+
92
+ .picture-in-picture, .fullscreen, .play-button {
93
+ height: 100%;
94
+ color: #fff;
95
+ background-color: transparent;
96
+ border: none;
97
+ cursor: pointer;
98
+ }
99
+
100
+ .volume-area {
101
+ display: flex;
102
+ /* grid-template-columns: 4.8rem fit-content(0rem); */
103
+ height: 100%;
104
+ cursor: pointer;
105
+
106
+ .mute-button {
107
+ color: #fff;
108
+ border: none;
109
+ background: none;
110
+ height: 100%;
111
+ width: 4.8rem;
112
+ cursor: pointer;
113
+ }
114
+
115
+ .volume-panel {
116
+ display: inline-block;
117
+ width: 0;
118
+ /* width: 100%; */
119
+ /* width: 12rem; */
120
+ height: 100%;
121
+ -webkit-transition: margin .2s cubic-bezier(0.4,0,1,1),width .2s cubic-bezier(0.4,0,1,1);
122
+ transition: margin .2s cubic-bezier(0.4,0,1,1),width .2s cubic-bezier(0.4,0,1,1);
123
+ cursor: pointer;
124
+ outline: 0;
125
+
126
+ &.volume-control-hover {
127
+ width: 6rem;
128
+ /* width: 52px; */
129
+ margin-right: 3px;
130
+ -webkit-transition: margin .2s cubic-bezier(0,0,0.2,1),width .2s cubic-bezier(0,0,0.2,1);
131
+ transition: margin .2s cubic-bezier(0,0,0.2,1),width .2s cubic-bezier(0,0,0.2,1);
132
+ }
133
+
134
+ .slider {
135
+ height: 100%;
136
+ min-height: 36px;
137
+ position: relative;
138
+ overflow: hidden;
139
+
140
+ .slider-handle {
141
+ /* left: 40px; */
142
+ position: absolute;
143
+ top: 50%;
144
+ width: 12px;
145
+ height: 12px;
146
+ border-radius: 6px;
147
+ margin-top: -6px;
148
+ margin-left: -5px;
149
+ /* background: #fff; */
150
+ }
151
+ .slider-handle::before, .slider-handle::after {
152
+ content: "";
153
+ position: absolute;
154
+ display: block;
155
+ top: 50%;
156
+ left: 0;
157
+ height: 3px;
158
+ margin-top: -2px;
159
+ width: 64px;
160
+ }
161
+ .slider-handle::before {
162
+ left: -58px;
163
+ background: #fff;
164
+ }
165
+ .slider-handle::after {
166
+ left: 6px;
167
+ background: rgba(255,255,255,.2);
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ .time {
174
+ font-family: Roboto;
175
+ padding-bottom: .5rem;
176
+ }
177
+
178
+ .custom-controls {
179
+ display: flex;
180
+ justify-content: end;
181
+ }
182
+
183
+ .error-area {
184
+ position: relative;
185
+ height: 3.8rem;
186
+
187
+ .error-menu {
188
+ position: absolute;
189
+ bottom: 4.8rem;
190
+ left: -10rem;
191
+ width: 25rem;
192
+ /* background: rgba(0, 0, 0, .2); */
193
+ /* background: rgb(35, 35, 35); */
194
+ padding: 1rem;
195
+ background-color: rgb(51, 51, 51);
196
+
197
+ &.hide {
198
+ display: none;
199
+ }
200
+
201
+ .error-menu-message {
202
+
203
+ }
204
+
205
+ button {
206
+ width: 100%;
207
+ padding: 0.5rem 0;
208
+ background: none;
209
+ border: none;
210
+ color: #fff;
211
+ cursor: pointer;
212
+ text-shadow: 2px 2px #000;
213
+ }
214
+ }
215
+
216
+ .error-menu-button {
217
+ display: grid;
218
+ grid-auto-flow: column;
219
+ align-items: center;
220
+ height: 100%;
221
+ width: 100%;
222
+ cursor: pointer;
223
+ color: #eb1010;
224
+ font-weight: bold;
225
+ background: none;
226
+ border: none;
227
+ }
228
+ }
229
+
230
+ .subtitle-area {
231
+ position: relative;
232
+ height: 3.8rem;
233
+
234
+ .subtitle-menu {
235
+ position: absolute;
236
+ bottom: 4.8rem;
237
+ left: -1rem;
238
+ /* background: rgba(0, 0, 0, .2); */
239
+ /* background: rgb(35, 35, 35); */
240
+ padding: 1rem 0;
241
+ background-color: rgb(17, 17, 17);
242
+ /* background-color: rgb(51, 51, 51); */
243
+ border-radius: .3rem;
244
+
245
+ &.hide {
246
+ display: none;
247
+ }
248
+
249
+ button {
250
+ width: 100%;
251
+ padding: 1rem;
252
+ /* padding: 0.5rem 0; */
253
+ background: none;
254
+ border: none;
255
+ color: #fff;
256
+ cursor: pointer;
257
+ text-shadow: 2px 2px #000;
258
+
259
+ &:hover {
260
+ background-color: rgb(51, 51, 51);
261
+ }
262
+ }
263
+ }
264
+
265
+ .subtitle-menu-button {
266
+ height: 100%;
267
+ width: 100%;
268
+ cursor: pointer;
269
+ color: #fff;
270
+ background: none;
271
+ border: none;
272
+ }
273
+ }
274
+ }
275
+ `
276
+
277
+ export type BottomOptions = {
278
+ duration?: number
279
+ currentTime?: number
280
+ togglePlay: () => void
281
+ isSubtitleMenuHidden: boolean
282
+ setIsSubtitleMenuHidden: Dispatch<SetStateAction<boolean>>
283
+ isErrorMenuHidden: boolean
284
+ setIsErrorMenuHidden: Dispatch<SetStateAction<boolean>>
285
+ setCurrentSubtitleTrack: Dispatch<SetStateAction<number | undefined>>
286
+ isFullscreen: boolean
287
+ toggleFullscreen: () => void
288
+ subtitleTrack: Subtitle | undefined
289
+ errors: TransmuxError[]
290
+ customControls?: FKNVideoControl[]
291
+ } &
292
+ Pick<ChromeOptions, 'seek' | 'setVolume' | 'loadedTime' | 'isPlaying' | 'tracks' | 'pictureInPicture'> &
293
+ HTMLAttributes<HTMLDivElement>
294
+
295
+ export default ({
296
+ duration,
297
+ currentTime,
298
+ togglePlay,
299
+ seek,
300
+ setVolume,
301
+ isSubtitleMenuHidden,
302
+ setIsSubtitleMenuHidden,
303
+ isErrorMenuHidden,
304
+ setIsErrorMenuHidden,
305
+ setCurrentSubtitleTrack,
306
+ loadedTime,
307
+ isPlaying,
308
+ tracks,
309
+ isFullscreen,
310
+ toggleFullscreen,
311
+ subtitleTrack,
312
+ pictureInPicture,
313
+ errors,
314
+ customControls,
315
+ ...rest
316
+ }: BottomOptions) => {
317
+ const progressBarRef = useRef<HTMLDivElement>(null)
318
+ const volumeBarRef = useRef<HTMLDivElement>(null)
319
+ const [hiddenVolumeArea, setHiddenVolumeArea] = useState(false)
320
+ const { scrub: seekScrub, value: seekScrubValue } = useScrub({ ref: progressBarRef })
321
+ const isPictureInPictureEnabled = useMemo(() => document.pictureInPictureEnabled, [])
322
+ const [hasShownErrorPopup, setHasShownErrorPopup] = useState(false)
323
+
324
+ const useStoredValue = useNamespacedLocalStorage<{ volume: number, muted: boolean }>('fkn-media-player')
325
+ const [volume, setStoredVolume] = useStoredValue('volume', 1)
326
+ const [isMuted, setStoredMuted] = useStoredValue('muted', false)
327
+ const { scrub: volumeScrub, value: volumeScrubValue } = useScrub({ ref: volumeBarRef, defaultValue: volume })
328
+
329
+ useEffect(() => {
330
+ if (hasShownErrorPopup) return
331
+ setHasShownErrorPopup(true)
332
+ errorMenuButtonClick(undefined)
333
+ }, [hasShownErrorPopup, errors.length])
334
+
335
+ useEffect(() => {
336
+ if (seekScrubValue === undefined) return
337
+ seek(seekScrubValue * (duration ?? 0))
338
+ }, [seekScrubValue])
339
+
340
+ useEffect(() => {
341
+ if (isMuted) {
342
+ setVolume(0)
343
+ setStoredMuted(isMuted)
344
+ if (volumeScrubValue) setStoredVolume(volumeScrubValue)
345
+ return
346
+ }
347
+ if (volumeScrubValue === undefined) return
348
+ setVolume(volumeScrubValue ** 2)
349
+ setStoredVolume(volumeScrubValue)
350
+ setStoredMuted(isMuted)
351
+ }, [volumeScrubValue, isMuted])
352
+
353
+ const hoverVolumeArea: React.DOMAttributes<HTMLDivElement>['onMouseOver'] = () => {
354
+ setHiddenVolumeArea(false)
355
+ }
356
+
357
+ const mouseOutBottom: React.DOMAttributes<HTMLDivElement>['onMouseOut'] = (ev) => {
358
+ if (ev.currentTarget !== ev.relatedTarget && ev.relatedTarget !== null) return
359
+ setHiddenVolumeArea(true)
360
+ }
361
+
362
+ const toggleMuteButton = () => {
363
+ setStoredMuted(value => !value)
364
+ }
365
+
366
+ const setSubtitleTrack = (ev: MouseEvent<HTMLButtonElement, globalThis.MouseEvent>, track: Subtitle | undefined, trackIndex: number) => {
367
+ setCurrentSubtitleTrack(track === undefined ? track : trackIndex)
368
+ }
369
+
370
+ const subtitleMenuButtonClick: MouseEventHandler<HTMLButtonElement> = (ev) => {
371
+ if (!isSubtitleMenuHidden && ev.relatedTarget === null) {
372
+ setIsSubtitleMenuHidden(value => !value)
373
+ return
374
+ }
375
+ setIsSubtitleMenuHidden(false)
376
+ const clickListener = (ev: globalThis.MouseEvent) => {
377
+ ev.stopPropagation()
378
+ setIsSubtitleMenuHidden(true)
379
+ document.removeEventListener('click', clickListener)
380
+ }
381
+ setTimeout(() => {
382
+ document.addEventListener('click', clickListener)
383
+ }, 0)
384
+ }
385
+
386
+ const errorMenuButtonClick = (ev: MouseEvent<HTMLDivElement, globalThis.MouseEvent> | undefined) => {
387
+ if (!isErrorMenuHidden && ev?.relatedTarget === null) {
388
+ setIsErrorMenuHidden(value => !value)
389
+ return
390
+ }
391
+ setIsErrorMenuHidden(false)
392
+ const clickListener = (ev: globalThis.MouseEvent) => {
393
+ ev.stopPropagation()
394
+ setIsErrorMenuHidden(true)
395
+ document.removeEventListener('click', clickListener)
396
+ }
397
+ setTimeout(() => {
398
+ document.addEventListener('click', clickListener)
399
+ }, 0)
400
+ }
401
+
402
+ const SubtitleTrackToName = ({ subtitleTrack: subtitle }: { subtitleTrack: Subtitle | undefined }) => {
403
+ if (!subtitle) return <>Disabled</>
404
+
405
+ if (!subtitle.title && !subtitle.language) return <>Default</>
406
+ const language =
407
+ subtitle.language === 'jpn' ? 'ja'
408
+ : tagToLanguage(subtitle.language) ? tagToLanguage(subtitle.language)
409
+ : subtitle.language
410
+
411
+ if (subtitle.title) {
412
+ return (
413
+ <>
414
+ {subtitle.title?.replace('subs', '')}{language ? `(${language})` : ''}
415
+ </>
416
+ )
417
+ }
418
+
419
+ return (
420
+ <>
421
+ {language}
422
+ </>
423
+ )
424
+ }
425
+
426
+ const shownErrors = useMemo(() =>
427
+ [
428
+ ...new Set(
429
+ errors
430
+ .map(error =>
431
+ error.message === 'non monotonicaly increasing DTS values'
432
+ ? (
433
+ error.count > 2
434
+ ? 'The video seems malformatted and might have issues(skipped frames) during playback'
435
+ : null
436
+ )
437
+ : 'An unexpected error occured while trying to play the video'
438
+ )
439
+ )
440
+ ]
441
+ .filter(Boolean)
442
+ .map(str => <div key={str} className="error-menu-message">{str}</div>)
443
+ , [errors])
444
+
445
+ useEffect(() => {
446
+ ReactTooltip.rebuild()
447
+ }, [tracks.length])
448
+
449
+ useEffect(() => {
450
+ const eventListener = (ev: KeyboardEvent) => {
451
+ if (ev.key === 'f') toggleFullscreen()
452
+ if (ev.key === 'k') togglePlay()
453
+ if (ev.key === 'm') toggleMuteButton()
454
+ }
455
+ window.addEventListener('keydown', eventListener)
456
+ return () => window.removeEventListener('keydown', eventListener)
457
+ }, [toggleFullscreen, togglePlay, toggleMuteButton])
458
+
459
+ const [progressBarOverTime, setProgressBarOverTime] = useState<number | undefined>(undefined)
460
+
461
+ const onProgressBarOver: DOMAttributes<HTMLDivElement>['onMouseMove'] = (ev) => {
462
+ const percentage = ev.nativeEvent.offsetX / ev.currentTarget.offsetWidth
463
+ const time = percentage * (duration ?? 0)
464
+ setProgressBarOverTime(time)
465
+ }
466
+
467
+ const hideProgressBarTime = () => {
468
+ if (!progressBarRef.current) return
469
+ setProgressBarOverTime(undefined)
470
+ }
471
+
472
+ return (
473
+ <div css={style} onMouseOut={mouseOutBottom} {...rest}>
474
+ <div className="preview"></div>
475
+ <div
476
+ ref={progressBarRef}
477
+ className="progress-bar"
478
+ onMouseMove={onProgressBarOver}
479
+ onMouseOut={hideProgressBarTime}
480
+ >
481
+ <div className="background-bar"></div>
482
+ {
483
+ progressBarOverTime
484
+ ? (
485
+ <div className="cursor-time" data-tip={progressBarOverTime} style={{ left: `${1 / ((duration ?? 0) / (progressBarOverTime ?? 1)) * 100}%` }}>
486
+ {
487
+ new Date((progressBarOverTime ?? 0) * 1000)
488
+ .toISOString()
489
+ .substr(
490
+ (duration ?? 0) >= 3600
491
+ ? 11
492
+ : 14,
493
+ (duration ?? 0) >= 3600
494
+ ? 8
495
+ : 5
496
+
497
+ )
498
+ }
499
+ </div>
500
+ )
501
+ : undefined
502
+ }
503
+ <div className="progress"></div>
504
+ {/* bar showing the currently loaded progress */}
505
+ <div className="load" style={{ transform: `scaleX(${1 / ((duration ?? 0) / (loadedTime?.[1] ?? 0))})` }}></div>
506
+ {/* bar to show when hovering to potentially seek */}
507
+ <div className="hover"></div>
508
+ {/* bar displaying the current playback progress */}
509
+ <div className="play" style={{
510
+ transform: `scaleX(${
511
+ typeof duration !== 'number' || typeof currentTime !== 'number'
512
+ ? 0
513
+ : 1 / ((duration ?? 0) / (currentTime ?? 0))
514
+ })`
515
+ }}>
516
+ </div>
517
+ <div className="chapters"></div>
518
+ <div className="scrubber"></div>
519
+ <div className="padding" onMouseDown={seekScrub}></div>
520
+ </div>
521
+ <div className="controls">
522
+ <ReactTooltip id="play-button-tooltip" effect="solid" place="top">
523
+ {
524
+ isPlaying
525
+ ? 'Pause (k)'
526
+ : 'Play (k)'
527
+ }
528
+ </ReactTooltip>
529
+ <button className="play-button" type="button" data-tip data-for="play-button-tooltip" onClick={togglePlay}>
530
+ {
531
+ isPlaying
532
+ ? <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-pause"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg>
533
+ : <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-play"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>
534
+ }
535
+ </button>
536
+ <div className="volume-area" onMouseOver={hoverVolumeArea}>
537
+ <ReactTooltip id="mute-button-tooltip" effect="solid" place="top">
538
+ {
539
+ isMuted
540
+ ? 'Unmute (m)'
541
+ : 'Mute (m)'
542
+ }
543
+ </ReactTooltip>
544
+ <button className="mute-button" data-tip data-for="mute-button-tooltip" onClick={toggleMuteButton}>
545
+ {
546
+ isMuted ? <VolumeX/>
547
+ : (volume ?? 0) > 0.66 ? <Volume2/>
548
+ : (volume ?? 0) > 0.33 ? <Volume1/>
549
+ : (volume ?? 0) > 0 ? <Volume/>
550
+ : <VolumeX/>
551
+ }
552
+ </button>
553
+ <div ref={volumeBarRef} className={`volume-panel${hiddenVolumeArea ? '' : ' volume-control-hover'}`} onMouseDown={volumeScrub}>
554
+ <div className="slider">
555
+ <div className="slider-handle" style={{ left: `${(volume ?? 0) * 100}%` }}></div>
556
+ </div>
557
+ </div>
558
+ </div>
559
+ <div className="time">
560
+ <span>{new Date((currentTime ?? 0) * 1000).toISOString().substr(11, 8)}</span>
561
+ <span> / </span>
562
+ <span>{duration ? new Date(duration * 1000).toISOString().substr(11, 8) : ''}</span>
563
+ </div>
564
+ <div className="custom-controls">
565
+ {
566
+ customControls?.length
567
+ ? customControls.map((Control, i) => <Control key={i}/>)
568
+ : null
569
+ }
570
+ </div>
571
+ <div className="error-area">
572
+ {
573
+ shownErrors.length
574
+ ? (
575
+ <>
576
+ <div className={`error-menu ${isErrorMenuHidden ? 'hide' : ''}`}>
577
+ {
578
+ isErrorMenuHidden
579
+ ? null
580
+ : shownErrors
581
+ }
582
+ </div>
583
+ <div className="error-menu-button" onClick={errorMenuButtonClick}>
584
+ <AlertTriangle/>
585
+ Error
586
+ </div>
587
+ </>
588
+ )
589
+ : null
590
+ }
591
+ </div>
592
+ <ReactTooltip id="subtitle-button-tooltip" globalEventOff="click" effect="solid" place="top" disable={!isSubtitleMenuHidden}>
593
+ Subtitles
594
+ </ReactTooltip>
595
+ <div className="subtitle-area">
596
+ {
597
+ tracks.length
598
+ ? (
599
+ <>
600
+ <div className={`subtitle-menu ${isSubtitleMenuHidden ? 'hide' : ''}`}>
601
+ {
602
+ isSubtitleMenuHidden
603
+ ? null
604
+ : (
605
+ [...tracks, undefined].map((track, i) =>
606
+ <button key={!track ? 'disabled' : i} onClick={ev => setSubtitleTrack(ev, track, i)}>
607
+ <SubtitleTrackToName subtitleTrack={track}/>
608
+ </button>
609
+ )
610
+ )
611
+ }
612
+ </div>
613
+ <button className="subtitle-menu-button" data-tip data-for="subtitle-button-tooltip" onClick={subtitleMenuButtonClick}>
614
+ <SubtitleTrackToName subtitleTrack={subtitleTrack}/>
615
+ </button>
616
+ </>
617
+ )
618
+ : null
619
+ }
620
+ </div>
621
+ <ReactTooltip id="pip-button-tooltip" effect="solid" place="top">
622
+ Picture in Picture
623
+ </ReactTooltip>
624
+ {
625
+ isPictureInPictureEnabled
626
+ ? (
627
+ <button className="picture-in-picture" data-tip data-for="pip-button-tooltip" onClick={pictureInPicture}>
628
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="3" width="21" height="14" rx="2" ry="2"></rect><rect x="12.5" y="8.5" width="8" height="6" rx="2" ry="2"></rect></svg>
629
+ </button>
630
+ )
631
+ : null
632
+ }
633
+ <ReactTooltip id="fullscreen-button-tooltip" effect="solid" place="top">
634
+ {
635
+ isFullscreen
636
+ ? 'Exit full screen (f)'
637
+ : 'Full screen (f)'
638
+ }
639
+ </ReactTooltip>
640
+ <button className="fullscreen" data-tip data-for="fullscreen-button-tooltip" onClick={toggleFullscreen}>
641
+ {
642
+ isFullscreen
643
+ ? <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-minimize"><path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"></path></svg>
644
+ : <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-maximize"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path></svg>
645
+ }
646
+ </button>
647
+ </div>
648
+ </div>
649
+ )
650
+ }
651
+