@banou/media-player 0.6.2 → 0.8.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/README.md +149 -3
- package/dist/engine/index.d.ts +10 -0
- package/dist/engine/index.js +2 -0
- package/dist/engine/picture-in-picture.d.ts +27 -0
- package/dist/engine/playback.d.ts +45 -0
- package/dist/engine/source-buffer.d.ts +12 -0
- package/dist/engine/subtitles.d.ts +36 -0
- package/dist/engine/thumbnails.d.ts +22 -0
- package/dist/engine-Dmc7JSd9.js +470 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +1610 -0
- package/dist/react/components/chrome.d.ts +12 -0
- package/dist/react/components/control-bar.d.ts +2 -0
- package/dist/react/components/overlay.d.ts +4 -0
- package/dist/react/components/playback-slider.d.ts +2 -0
- package/dist/react/components/progress-bar.d.ts +2 -0
- package/dist/react/components/settings.d.ts +2 -0
- package/dist/react/components/sound.d.ts +7 -0
- package/{build → dist/react}/components/tooltip-display.d.ts +8 -8
- package/dist/react/components/volume-slider.d.ts +6 -0
- package/dist/react/hooks/use-drag-value.d.ts +23 -0
- package/dist/react/hooks/use-picture-in-picture.d.ts +2 -0
- package/dist/react/hooks/use-playback.d.ts +8 -0
- package/dist/react/hooks/use-thumbnails.d.ts +12 -0
- package/dist/react/media.d.ts +98 -0
- package/dist/react/player.d.ts +26 -0
- package/dist/react/source-feature.d.ts +106 -0
- package/dist/react/video-player.d.ts +83 -0
- package/dist/utils/source.d.ts +30 -0
- package/dist/utils/track-label.d.ts +31 -0
- package/dist/utils/volume-utils.d.ts +6 -0
- package/package.json +58 -37
- package/src/lib/engine/index.ts +14 -0
- package/src/lib/engine/picture-in-picture.ts +250 -0
- package/src/lib/engine/playback.ts +352 -0
- package/src/lib/engine/source-buffer.ts +61 -0
- package/src/lib/engine/subtitles.ts +210 -0
- package/src/lib/engine/thumbnails.ts +137 -0
- package/src/lib/globals.d.ts +8 -0
- package/src/lib/index.tsx +51 -0
- package/src/lib/react/components/chrome.tsx +130 -0
- package/src/{components → lib/react/components}/control-bar.tsx +74 -96
- package/src/lib/react/components/overlay.tsx +125 -0
- package/src/{components → lib/react/components}/playback-slider.tsx +30 -52
- package/src/lib/react/components/progress-bar.tsx +341 -0
- package/src/lib/react/components/settings.tsx +384 -0
- package/src/lib/react/components/sound.tsx +133 -0
- package/src/{components → lib/react/components}/tooltip-display.tsx +15 -12
- package/src/{components → lib/react/components}/volume-slider.tsx +32 -46
- package/src/lib/react/hooks/use-drag-value.ts +73 -0
- package/src/lib/react/hooks/use-picture-in-picture.ts +36 -0
- package/src/lib/react/hooks/use-playback.ts +146 -0
- package/src/lib/react/hooks/use-thumbnails.ts +78 -0
- package/src/lib/react/media.ts +124 -0
- package/src/lib/react/player.ts +20 -0
- package/src/lib/react/source-feature.ts +108 -0
- package/src/lib/react/video-player.tsx +223 -0
- package/src/lib/utils/fonts.ts +160 -0
- package/src/lib/utils/source.ts +60 -0
- package/src/lib/utils/track-label.ts +51 -0
- package/src/lib/utils/volume-utils.ts +13 -0
- package/build/components/chrome.d.ts +0 -7
- package/build/components/control-bar.d.ts +0 -6
- package/build/components/overlay.d.ts +0 -4
- package/build/components/playback-slider.d.ts +0 -2
- package/build/components/progress-bar.d.ts +0 -1
- package/build/components/settings.d.ts +0 -2
- package/build/components/sound.d.ts +0 -4
- package/build/components/volume-slider.d.ts +0 -6
- package/build/index.d.ts +0 -26
- package/build/index.js +0 -3416
- package/build/main.d.ts +0 -1
- package/build/state-machines/data-source.d.ts +0 -27
- package/build/state-machines/index.d.ts +0 -34038
- package/build/state-machines/media-properties.d.ts +0 -45
- package/build/state-machines/media-source.d.ts +0 -34
- package/build/state-machines/media.d.ts +0 -8558
- package/build/state-machines/subtitles.d.ts +0 -56
- package/build/state-machines/thumbnails.d.ts +0 -24
- package/build/state-machines/utils.d.ts +0 -26
- package/build/utils/actor-utils.d.ts +0 -2
- package/build/utils/context.d.ts +0 -14
- package/build/utils/index.d.ts +0 -4
- package/build/utils/languages.d.ts +0 -260
- package/build/utils/mp4box.d.ts +0 -61
- package/build/utils/use-local-storage.d.ts +0 -3
- package/build/utils/use-scrub.d.ts +0 -11
- package/build/utils/volume-utils.d.ts +0 -17
- package/build/utils/window-height.d.ts +0 -2
- package/src/components/chrome.tsx +0 -95
- package/src/components/overlay.tsx +0 -91
- package/src/components/progress-bar.tsx +0 -251
- package/src/components/settings.tsx +0 -322
- package/src/components/sound.tsx +0 -101
- package/src/index.tsx +0 -195
- package/src/main.tsx +0 -169
- package/src/state-machines/data-source.ts +0 -84
- package/src/state-machines/index.ts +0 -5
- package/src/state-machines/media-properties.ts +0 -82
- package/src/state-machines/media-source.ts +0 -129
- package/src/state-machines/media.ts +0 -255
- package/src/state-machines/subtitles.ts +0 -285
- package/src/state-machines/thumbnails.ts +0 -123
- package/src/state-machines/utils.ts +0 -94
- package/src/utils/actor-utils.ts +0 -19
- package/src/utils/context.ts +0 -23
- package/src/utils/fonts.ts +0 -160
- package/src/utils/index.ts +0 -229
- package/src/utils/languages.ts +0 -262
- package/src/utils/mp4box.ts +0 -74
- package/src/utils/use-local-storage.ts +0 -30
- package/src/utils/use-scrub.ts +0 -40
- package/src/utils/volume-utils.ts +0 -39
- package/src/utils/window-height.ts +0 -19
- package/src/vite-env.d.ts +0 -1
- package/tsconfig.json +0 -28
- package/tsconfig.node.json +0 -9
- /package/{build → dist}/utils/colors.d.ts +0 -0
- /package/{build → dist}/utils/fonts.d.ts +0 -0
- /package/{build → dist}/utils/time.d.ts +0 -0
- /package/src/{assets → lib/assets}/picture-in-picture.svg +0 -0
- /package/src/{utils → lib/utils}/colors.ts +0 -0
- /package/src/{utils → lib/utils}/time.ts +0 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/// <reference types="@emotion/react/types/css-prop" />
|
|
2
|
+
import type { DOMAttributes } from 'react'
|
|
3
|
+
|
|
4
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
5
|
+
import { css } from '@emotion/react'
|
|
6
|
+
|
|
7
|
+
import { usePlayer } from '../player'
|
|
8
|
+
import { useDragValue } from '../hooks/use-drag-value'
|
|
9
|
+
import { fonts } from '../../utils/fonts'
|
|
10
|
+
|
|
11
|
+
const style = css`
|
|
12
|
+
position: relative;
|
|
13
|
+
height: calc(.4 * var(--mp-unit));
|
|
14
|
+
|
|
15
|
+
--thumbnail-width: calc(25 * var(--mp-unit));
|
|
16
|
+
--thumbnail-half: calc(12.5 * var(--mp-unit));
|
|
17
|
+
|
|
18
|
+
transition: transform .2s ease-in-out;
|
|
19
|
+
|
|
20
|
+
margin: 0 12px;
|
|
21
|
+
@media (min-width: 768px) {
|
|
22
|
+
margin: 0 16px;
|
|
23
|
+
}
|
|
24
|
+
@media (min-width: 2560px) {
|
|
25
|
+
margin: 0 24px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
user-select: none;
|
|
29
|
+
cursor: pointer;
|
|
30
|
+
|
|
31
|
+
:hover {
|
|
32
|
+
.background-bar {
|
|
33
|
+
transform: scaleY(1.5);
|
|
34
|
+
}
|
|
35
|
+
.loaded {
|
|
36
|
+
transform: scaleY(1.5);
|
|
37
|
+
top: calc(-.1 * var(--mp-unit));
|
|
38
|
+
}
|
|
39
|
+
.play-container {
|
|
40
|
+
transform: scaleY(1.5);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
.background-bar {
|
|
46
|
+
position: absolute;
|
|
47
|
+
inset: 0;
|
|
48
|
+
background-color: hsla(0, 100%, 100%, .2);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.cursor-time {
|
|
52
|
+
display: flex;
|
|
53
|
+
justify-content: center;
|
|
54
|
+
|
|
55
|
+
text-shadow: 0 0 4px rgba(0, 0, 0, 1);
|
|
56
|
+
${fonts.bMedium.bold}
|
|
57
|
+
|
|
58
|
+
position: absolute;
|
|
59
|
+
top: calc(-2.5 * var(--mp-unit));
|
|
60
|
+
width: calc(5 * var(--mp-unit));
|
|
61
|
+
|
|
62
|
+
margin-left: calc(-2.5 * var(--mp-unit));
|
|
63
|
+
pointer-events: none;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.loaded {
|
|
67
|
+
transform-origin: 0 0;
|
|
68
|
+
position: absolute;
|
|
69
|
+
bottom: 0;
|
|
70
|
+
height: calc(.4 * var(--mp-unit));
|
|
71
|
+
width: 100%;
|
|
72
|
+
.loaded-part {
|
|
73
|
+
transform-origin: 0 0;
|
|
74
|
+
bottom: 0;
|
|
75
|
+
height: calc(.4 * var(--mp-unit));
|
|
76
|
+
width: 100%;
|
|
77
|
+
position: absolute;
|
|
78
|
+
background-color: hsla(0, 100%, 100%, .4);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.play-container {
|
|
83
|
+
position: absolute;
|
|
84
|
+
bottom: 0;
|
|
85
|
+
width: 100%;
|
|
86
|
+
height: calc(.4 * var(--mp-unit));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.play {
|
|
90
|
+
transform-origin: 0 0;
|
|
91
|
+
background-color: #f03;
|
|
92
|
+
position: absolute;
|
|
93
|
+
bottom: 0;
|
|
94
|
+
height: calc(.4 * var(--mp-unit));
|
|
95
|
+
width: 100%;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.padding {
|
|
99
|
+
position: absolute;
|
|
100
|
+
bottom: -7.5px;
|
|
101
|
+
height: calc(2 * var(--mp-unit));
|
|
102
|
+
width: 100%;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.thumbnail {
|
|
106
|
+
display: flex;
|
|
107
|
+
justify-content: center;
|
|
108
|
+
|
|
109
|
+
position: absolute;
|
|
110
|
+
top: calc(calc(-3.4375 * var(--mp-unit)) - var(--thumbnail-width) * 9/16);
|
|
111
|
+
height: calc(var(--thumbnail-width) * 9/16);
|
|
112
|
+
width: var(--thumbnail-width);
|
|
113
|
+
|
|
114
|
+
margin-left: calc(var(--thumbnail-half) * -1);
|
|
115
|
+
pointer-events: none;
|
|
116
|
+
|
|
117
|
+
img {
|
|
118
|
+
border-radius: calc(.4 * var(--mp-unit));
|
|
119
|
+
box-shadow: 0 0 calc(1 * var(--mp-unit)) rgba(0, 0, 0, .5);
|
|
120
|
+
|
|
121
|
+
width: 100%;
|
|
122
|
+
height: 100%;
|
|
123
|
+
object-fit: cover;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* a thumb is far wider than a cursor, so the hit strip grows to 44px and the track thickens */
|
|
128
|
+
@media (pointer: coarse) {
|
|
129
|
+
height: calc(.6 * var(--mp-unit));
|
|
130
|
+
|
|
131
|
+
/* press feedback, coarse only: a finger has no hover, so without this the bar gives no sign
|
|
132
|
+
it is being dragged */
|
|
133
|
+
&.dragging {
|
|
134
|
+
.background-bar, .loaded, .play-container {
|
|
135
|
+
transform: scaleY(1.5);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.loaded {
|
|
140
|
+
height: calc(.6 * var(--mp-unit));
|
|
141
|
+
.loaded-part {
|
|
142
|
+
height: calc(.6 * var(--mp-unit));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.play-container {
|
|
147
|
+
height: calc(.6 * var(--mp-unit));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.play {
|
|
151
|
+
height: calc(.6 * var(--mp-unit));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* grows upward into the video: centred, it would hang over the control row and win hit
|
|
155
|
+
testing against taps meant for play, mute and fullscreen */
|
|
156
|
+
.padding {
|
|
157
|
+
height: 44px;
|
|
158
|
+
bottom: 0;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* the clamp only holds the preview inside the bar while the bar is the wider of the two, so
|
|
163
|
+
below that the preview is narrowed instead */
|
|
164
|
+
@media (max-width: 480px) {
|
|
165
|
+
--thumbnail-width: calc(18 * var(--mp-unit));
|
|
166
|
+
--thumbnail-half: calc(9 * var(--mp-unit));
|
|
167
|
+
}
|
|
168
|
+
`
|
|
169
|
+
|
|
170
|
+
export const ProgressBar = () => {
|
|
171
|
+
const player = usePlayer()
|
|
172
|
+
const currentTime = usePlayer((state) => state.currentTime)
|
|
173
|
+
const duration = usePlayer((state) => state.duration)
|
|
174
|
+
const size = usePlayer((state) => state.size)
|
|
175
|
+
const downloadedRanges = usePlayer((state) => state.downloadedRanges)
|
|
176
|
+
const indexes = usePlayer((state) => state.indexes)
|
|
177
|
+
const thumbnails = usePlayer((state) => state.thumbnails)
|
|
178
|
+
const thumbnailAt = usePlayer((state) => state.thumbnailAt)
|
|
179
|
+
|
|
180
|
+
const progressBarRef = useRef<HTMLDivElement>(null)
|
|
181
|
+
|
|
182
|
+
const [seekFraction, setSeekFraction] = useState<number | undefined>(undefined)
|
|
183
|
+
const [progressBarHoverTime, setProgressBarOverTime] = useState<number | undefined>(undefined)
|
|
184
|
+
|
|
185
|
+
// onChange reports a bare fraction, so the device that opened the gesture is recorded on press
|
|
186
|
+
const dragPointerType = useRef<string | undefined>(undefined)
|
|
187
|
+
|
|
188
|
+
const onSeekDrag = (fraction: number) => {
|
|
189
|
+
setSeekFraction(fraction)
|
|
190
|
+
if (dragPointerType.current === 'mouse') return
|
|
191
|
+
setProgressBarOverTime(fraction * duration)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const { dragging, handlers } = useDragValue({ ref: progressBarRef, onChange: onSeekDrag })
|
|
195
|
+
|
|
196
|
+
const onDragStart: DOMAttributes<HTMLDivElement>['onPointerDown'] = (ev) => {
|
|
197
|
+
dragPointerType.current = ev.pointerType
|
|
198
|
+
handlers.onPointerDown(ev)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const onDragEnd: DOMAttributes<HTMLDivElement>['onPointerUp'] = (ev) => {
|
|
202
|
+
handlers.onPointerUp(ev)
|
|
203
|
+
// a lifted finger leaves nothing over the bar, so the preview it opened closes with it
|
|
204
|
+
if (ev.pointerType === 'mouse') return
|
|
205
|
+
setProgressBarOverTime(undefined)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// offsetX is relative to whichever child is under the pointer, and a captured pointer has none
|
|
209
|
+
const timeAtClientX = (clientX: number) => {
|
|
210
|
+
if (!progressBarRef.current) return undefined
|
|
211
|
+
const { left, right } = progressBarRef.current.getBoundingClientRect()
|
|
212
|
+
const fraction = Math.min(Math.max((clientX - left) / (right - left), 0), 1)
|
|
213
|
+
return fraction * duration
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const onProgressBarOver: DOMAttributes<HTMLDivElement>['onMouseMove'] = (ev) => {
|
|
217
|
+
setProgressBarOverTime(timeAtClientX(ev.clientX))
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const hideProgressBarTime = () => {
|
|
221
|
+
if (!progressBarRef.current) return
|
|
222
|
+
setProgressBarOverTime(undefined)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// duration is 0 until metadata lands, which is never a divisor
|
|
226
|
+
const timePercentage = (time: number) => duration ? (time / duration) * 100 : 0
|
|
227
|
+
|
|
228
|
+
// download percentage is not time percentage: headers and fonts can run to tens of megabytes
|
|
229
|
+
const loadedParts = useMemo(() =>
|
|
230
|
+
downloadedRanges
|
|
231
|
+
?.map((range, i) => {
|
|
232
|
+
if (!size || !duration) return null
|
|
233
|
+
const matchingIndexes = indexes.filter(index => range.startByteOffset <= index.pos && index.pos <= range.endByteOffset)
|
|
234
|
+
const firstIndex = matchingIndexes.at(0)
|
|
235
|
+
const lastIndex = matchingIndexes.at(-1)
|
|
236
|
+
if (!firstIndex || !lastIndex) return null
|
|
237
|
+
const start = firstIndex.timestamp / duration
|
|
238
|
+
const end = lastIndex.timestamp / duration
|
|
239
|
+
const rangeDuration = Number((end - start).toFixed(2)) // to prevent cases like `0.9995140832939585`
|
|
240
|
+
const left = start * 100
|
|
241
|
+
return (
|
|
242
|
+
<div key={i} className="loaded-part" style={{ transform: `scaleX(${rangeDuration})`, marginLeft: `${left}%` }}></div>
|
|
243
|
+
)
|
|
244
|
+
})
|
|
245
|
+
?? [],
|
|
246
|
+
[duration, indexes.length, downloadedRanges?.map(({ startByteOffset, endByteOffset }) => `${startByteOffset}/${endByteOffset}`).join(',')]
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
const cusorTimeString = useMemo(() => {
|
|
250
|
+
if (!progressBarHoverTime || progressBarHoverTime < 0) return undefined
|
|
251
|
+
const hours = Math.floor(progressBarHoverTime! / 3600)
|
|
252
|
+
const minutes = Math.floor((progressBarHoverTime! - hours * 3600) / 60)
|
|
253
|
+
const seconds = Math.floor(progressBarHoverTime! - hours * 3600 - minutes * 60)
|
|
254
|
+
const hoursString =
|
|
255
|
+
hours > 0
|
|
256
|
+
? `${hours}:`
|
|
257
|
+
: ''
|
|
258
|
+
return `${hoursString}${minutes < 10 ? '0' : ''}${minutes}:${seconds < 10 ? '0' : ''}${seconds}`
|
|
259
|
+
}, [progressBarHoverTime])
|
|
260
|
+
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
if (seekFraction === undefined || !duration) return
|
|
263
|
+
const timestamp = seekFraction * duration
|
|
264
|
+
player.seek(timestamp)
|
|
265
|
+
}, [player, seekFraction, duration])
|
|
266
|
+
|
|
267
|
+
const scaleX = useMemo(() => {
|
|
268
|
+
return !duration || typeof currentTime !== 'number'
|
|
269
|
+
? 0
|
|
270
|
+
: currentTime / duration
|
|
271
|
+
}, [duration, currentTime])
|
|
272
|
+
|
|
273
|
+
// an empty url is a gap sentinel, so it renders nothing
|
|
274
|
+
const thumbnail = useMemo(() => {
|
|
275
|
+
if (!progressBarHoverTime) return undefined
|
|
276
|
+
if (thumbnailAt) return thumbnailAt(progressBarHoverTime)
|
|
277
|
+
if (!thumbnails.length) return undefined
|
|
278
|
+
return (
|
|
279
|
+
thumbnails
|
|
280
|
+
.find(({ startTime, endTime }) =>
|
|
281
|
+
startTime <= progressBarHoverTime && progressBarHoverTime < endTime
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
}, [thumbnails, thumbnailAt, progressBarHoverTime])
|
|
285
|
+
|
|
286
|
+
return (
|
|
287
|
+
<div
|
|
288
|
+
css={style}
|
|
289
|
+
ref={progressBarRef}
|
|
290
|
+
className={dragging ? 'progress-bar dragging' : 'progress-bar'}
|
|
291
|
+
onMouseMove={onProgressBarOver}
|
|
292
|
+
onMouseOut={hideProgressBarTime}
|
|
293
|
+
>
|
|
294
|
+
<div className="background-bar" />
|
|
295
|
+
{
|
|
296
|
+
progressBarHoverTime
|
|
297
|
+
? (
|
|
298
|
+
<div
|
|
299
|
+
className="cursor-time"
|
|
300
|
+
style={{ left: `clamp(calc(1.8 * var(--mp-unit)), ${timePercentage(progressBarHoverTime)}%, calc(100% - calc(1.8 * var(--mp-unit))))` }}
|
|
301
|
+
>
|
|
302
|
+
{cusorTimeString}
|
|
303
|
+
</div>
|
|
304
|
+
)
|
|
305
|
+
: undefined
|
|
306
|
+
}
|
|
307
|
+
<div className="progress"></div>
|
|
308
|
+
{/* bar showing the currently loaded progress */}
|
|
309
|
+
<div className="loaded">
|
|
310
|
+
{loadedParts}
|
|
311
|
+
</div>
|
|
312
|
+
{/* bar to show when hovering to potentially seek */}
|
|
313
|
+
<div className="hover"></div>
|
|
314
|
+
{/* bar displaying the current playback progress */}
|
|
315
|
+
<div className='play-container'>
|
|
316
|
+
<div className="play" style={{ transform: `scaleX(${scaleX})` }}></div>
|
|
317
|
+
</div>
|
|
318
|
+
<div className="chapters"></div>
|
|
319
|
+
<div className="scrubber"></div>
|
|
320
|
+
<div
|
|
321
|
+
className="padding"
|
|
322
|
+
{...handlers}
|
|
323
|
+
onPointerDown={onDragStart}
|
|
324
|
+
onPointerUp={onDragEnd}
|
|
325
|
+
onPointerCancel={onDragEnd}
|
|
326
|
+
></div>
|
|
327
|
+
<div
|
|
328
|
+
className="thumbnail"
|
|
329
|
+
style={{ left: `clamp(var(--thumbnail-half), ${timePercentage(progressBarHoverTime ?? 1)}%, calc(100% - var(--thumbnail-half)))` }}
|
|
330
|
+
>
|
|
331
|
+
{
|
|
332
|
+
thumbnail?.url
|
|
333
|
+
? <img src={thumbnail.url}/>
|
|
334
|
+
: undefined
|
|
335
|
+
}
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export default ProgressBar
|