sanity-plugin-mux-input 2.13.0 → 2.15.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 +25 -24
- package/dist/index.d.mts +35 -2
- package/dist/index.d.ts +35 -2
- package/dist/index.js +2176 -461
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2178 -463
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/_exports/index.ts +1 -0
- package/src/actions/assets.ts +75 -0
- package/src/actions/secrets.ts +6 -1
- package/src/actions/upload.ts +1 -1
- package/src/components/AddCaptionDialog.tsx +421 -0
- package/src/components/CaptionsDialog.tsx +23 -0
- package/src/components/ConfigureApi.tsx +51 -5
- package/src/components/EditCaptionDialog.tsx +508 -0
- package/src/components/InputBrowser.tsx +8 -2
- package/src/components/Onboard.tsx +2 -2
- package/src/components/PageSelector.tsx +54 -0
- package/src/components/Player.styled.tsx +7 -2
- package/src/components/PlayerActionsMenu.tsx +14 -6
- package/src/components/SelectAsset.tsx +9 -3
- package/src/components/StudioTool.tsx +2 -2
- package/src/components/TextTracksManager.tsx +781 -0
- package/src/components/UploadConfiguration.tsx +104 -343
- package/src/components/Uploader.styled.tsx +8 -15
- package/src/components/Uploader.tsx +25 -7
- package/src/components/VideoDetails/VideoDetails.tsx +43 -7
- package/src/components/VideoInBrowser.tsx +53 -6
- package/src/components/VideoPlayer.tsx +122 -47
- package/src/components/VideoThumbnail.tsx +84 -72
- package/src/components/VideosBrowser.tsx +15 -5
- package/src/components/uploadConfiguration/PlaybackPolicy.tsx +95 -6
- package/src/components/uploadConfiguration/PlaybackPolicyOption.tsx +26 -10
- package/src/components/uploadConfiguration/ResolutionTierSelector.tsx +71 -0
- package/src/components/uploadConfiguration/StaticRenditionSelector.tsx +179 -0
- package/src/context/DrmPlaybackWarningContext.tsx +93 -0
- package/src/hooks/useAccessControl.ts +1 -0
- package/src/hooks/useDialogState.ts +1 -1
- package/src/hooks/useFetchFileSize.ts +54 -0
- package/src/hooks/useMediaMetadata.ts +100 -0
- package/src/hooks/useSaveSecrets.ts +10 -3
- package/src/hooks/useSecretsDocumentValues.ts +9 -1
- package/src/hooks/useSecretsFormState.ts +6 -3
- package/src/util/asserters.ts +14 -0
- package/src/util/createUrlParamsObject.ts +7 -3
- package/src/util/generateJwt.ts +11 -2
- package/src/util/getPlaybackPolicy.ts +63 -4
- package/src/util/getStoryboardSrc.ts +7 -3
- package/src/util/getVideoMetadata.ts +4 -1
- package/src/util/getVideoSrc.ts +9 -9
- package/src/util/readSecrets.ts +3 -1
- package/src/util/textTracks.ts +222 -0
- package/src/util/tryWithSuspend.ts +22 -0
- package/src/util/types.ts +39 -6
- package/src/util/getPlaybackId.ts +0 -9
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import {DownloadIcon, TranslateIcon, UploadIcon} from '@sanity/icons'
|
|
2
|
+
import {
|
|
3
|
+
Autocomplete,
|
|
4
|
+
Button,
|
|
5
|
+
Card,
|
|
6
|
+
Dialog,
|
|
7
|
+
Flex,
|
|
8
|
+
Label,
|
|
9
|
+
Spinner,
|
|
10
|
+
Stack,
|
|
11
|
+
Text,
|
|
12
|
+
TextInput,
|
|
13
|
+
useToast,
|
|
14
|
+
} from '@sanity/ui'
|
|
15
|
+
import LanguagesList from 'iso-639-1'
|
|
16
|
+
import {useEffect, useId, useRef, useState} from 'react'
|
|
17
|
+
|
|
18
|
+
import {addTextTrackFromUrl, deleteTextTrack, getAsset} from '../actions/assets'
|
|
19
|
+
import {useClient} from '../hooks/useClient'
|
|
20
|
+
import {generateJwt} from '../util/generateJwt'
|
|
21
|
+
import {getPlaybackId} from '../util/getPlaybackPolicy'
|
|
22
|
+
import {getPlaybackPolicy} from '../util/getPlaybackPolicy'
|
|
23
|
+
import {downloadVttFile, extractErrorMessage, pollTrackStatus} from '../util/textTracks'
|
|
24
|
+
import type {MuxTextTrack, VideoAssetDocument} from '../util/types'
|
|
25
|
+
|
|
26
|
+
const LANGUAGE_OPTIONS = LanguagesList.getAllCodes().map((code) => ({
|
|
27
|
+
value: code,
|
|
28
|
+
label: LanguagesList.getNativeName(code),
|
|
29
|
+
}))
|
|
30
|
+
|
|
31
|
+
export interface Props {
|
|
32
|
+
asset: VideoAssetDocument
|
|
33
|
+
track: MuxTextTrack
|
|
34
|
+
onUpdate: (track: MuxTextTrack, oldTrackId?: string) => void
|
|
35
|
+
onClose: () => void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default function EditCaptionDialog({asset, track, onUpdate, onClose}: Props) {
|
|
39
|
+
const client = useClient()
|
|
40
|
+
const toast = useToast()
|
|
41
|
+
const dialogId = `EditCaptionDialog${useId()}`
|
|
42
|
+
|
|
43
|
+
const isAutogenerated =
|
|
44
|
+
track.text_source === 'generated_live' ||
|
|
45
|
+
track.text_source === 'generated_live_final' ||
|
|
46
|
+
track.text_source === 'generated_vod'
|
|
47
|
+
|
|
48
|
+
const [vttUrl, setVttUrl] = useState('')
|
|
49
|
+
const [languageCode, setLanguageCode] = useState(track.language_code || '')
|
|
50
|
+
const [selectedLanguage, setSelectedLanguage] = useState<{value: string; label: string} | null>(
|
|
51
|
+
() => {
|
|
52
|
+
const baseCode = track.language_code?.split('-')[0]
|
|
53
|
+
const found = LANGUAGE_OPTIONS.find(
|
|
54
|
+
(opt) => opt.value === track.language_code || opt.value === baseCode
|
|
55
|
+
)
|
|
56
|
+
if (found) return found
|
|
57
|
+
if (track.name) {
|
|
58
|
+
const foundByName = LANGUAGE_OPTIONS.find((opt) => opt.label === track.name)
|
|
59
|
+
if (foundByName) return foundByName
|
|
60
|
+
}
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
const [name, setName] = useState(track.name || '')
|
|
65
|
+
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
66
|
+
const [downloading, setDownloading] = useState(false)
|
|
67
|
+
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
|
68
|
+
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
setLanguageCode(track.language_code || '')
|
|
72
|
+
setName(track.name || '')
|
|
73
|
+
setVttUrl('')
|
|
74
|
+
const baseCode = track.language_code?.split('-')[0]
|
|
75
|
+
const foundByCode = LANGUAGE_OPTIONS.find(
|
|
76
|
+
(opt) => opt.value === track.language_code || opt.value === baseCode
|
|
77
|
+
)
|
|
78
|
+
const foundByName = track.name ? LANGUAGE_OPTIONS.find((opt) => opt.label === track.name) : null
|
|
79
|
+
setSelectedLanguage(foundByCode || foundByName || null)
|
|
80
|
+
}, [track, asset, client])
|
|
81
|
+
|
|
82
|
+
const handleDownloadCurrentFile = async () => {
|
|
83
|
+
setDownloading(true)
|
|
84
|
+
try {
|
|
85
|
+
await downloadVttFile(client, asset, track)
|
|
86
|
+
} catch (error) {
|
|
87
|
+
let errorMessage = 'Please try again'
|
|
88
|
+
let title = 'Failed to download VTT file'
|
|
89
|
+
|
|
90
|
+
if (error instanceof Error) {
|
|
91
|
+
errorMessage = error.message
|
|
92
|
+
if (error.message.includes('Track')) {
|
|
93
|
+
title = 'Cannot download'
|
|
94
|
+
}
|
|
95
|
+
} else if (error === 'Track ID is missing' || error === 'Track is not ready yet') {
|
|
96
|
+
errorMessage = String(error)
|
|
97
|
+
title = 'Cannot download'
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
toast.push({
|
|
101
|
+
title,
|
|
102
|
+
status: 'error',
|
|
103
|
+
description: errorMessage,
|
|
104
|
+
})
|
|
105
|
+
} finally {
|
|
106
|
+
setDownloading(false)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const getCurrentFileName = () => {
|
|
111
|
+
if (track.id && asset.filename) {
|
|
112
|
+
return `${asset.filename}-${track.language_code || 'en'}.vtt`
|
|
113
|
+
}
|
|
114
|
+
return `captions-${track.language_code || 'en'}.vtt`
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const uploadVttFile = async (file: File): Promise<string> => {
|
|
118
|
+
const assetDocument = await client.assets.upload('file', file, {
|
|
119
|
+
filename: file.name,
|
|
120
|
+
})
|
|
121
|
+
return assetDocument.url
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const refreshAssetData = async () => {
|
|
125
|
+
if (!asset._id || !asset.assetId) return
|
|
126
|
+
try {
|
|
127
|
+
const latestAssetData = await getAsset(client, asset.assetId)
|
|
128
|
+
await client
|
|
129
|
+
.patch(asset._id)
|
|
130
|
+
.set({data: latestAssetData.data, status: latestAssetData.data.status})
|
|
131
|
+
.commit()
|
|
132
|
+
} catch (refreshError) {
|
|
133
|
+
console.error('Failed to refresh asset data:', refreshError)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const handleUpdateTrackWithNewUrl = async () => {
|
|
138
|
+
if (!asset.assetId) {
|
|
139
|
+
throw new Error('Asset ID is required')
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const trimmedName = name.trim()
|
|
143
|
+
const trimmedLanguageCode = languageCode.trim()
|
|
144
|
+
|
|
145
|
+
const oldTrackId = track.id
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
await deleteTextTrack(client, asset.assetId, oldTrackId)
|
|
149
|
+
} catch (deleteError) {
|
|
150
|
+
toast.push({
|
|
151
|
+
title: 'Failed to delete old track',
|
|
152
|
+
status: 'error',
|
|
153
|
+
description: 'Could not delete the old track. Please try again or delete it manually.',
|
|
154
|
+
})
|
|
155
|
+
setIsSubmitting(false)
|
|
156
|
+
throw deleteError
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let vttUrlToUse = vttUrl.trim()
|
|
160
|
+
|
|
161
|
+
if (selectedFile) {
|
|
162
|
+
try {
|
|
163
|
+
vttUrlToUse = await uploadVttFile(selectedFile)
|
|
164
|
+
} catch (uploadError) {
|
|
165
|
+
toast.push({
|
|
166
|
+
title: 'Failed to upload VTT file',
|
|
167
|
+
status: 'error',
|
|
168
|
+
description: 'Could not upload the VTT file to Sanity. Please try again.',
|
|
169
|
+
})
|
|
170
|
+
setIsSubmitting(false)
|
|
171
|
+
throw uploadError
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
await addTextTrackFromUrl(client, asset.assetId, vttUrlToUse, {
|
|
177
|
+
language_code: trimmedLanguageCode,
|
|
178
|
+
name: trimmedName,
|
|
179
|
+
text_type: 'subtitles',
|
|
180
|
+
})
|
|
181
|
+
} catch (error: unknown) {
|
|
182
|
+
toast.push({
|
|
183
|
+
title: 'Failed to update caption track',
|
|
184
|
+
status: 'error',
|
|
185
|
+
description: extractErrorMessage(error, 'Failed to update caption track'),
|
|
186
|
+
})
|
|
187
|
+
setIsSubmitting(false)
|
|
188
|
+
throw error
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const result = await pollTrackStatus({
|
|
192
|
+
client,
|
|
193
|
+
assetId: asset.assetId,
|
|
194
|
+
trackName: trimmedName,
|
|
195
|
+
trackLanguageCode: trimmedLanguageCode,
|
|
196
|
+
onTrackErrored: async (erroredTrack) => {
|
|
197
|
+
const errorMessage =
|
|
198
|
+
erroredTrack.error?.messages?.[0] ||
|
|
199
|
+
erroredTrack.error?.type ||
|
|
200
|
+
'The track failed to download from the provided URL'
|
|
201
|
+
toast.push({
|
|
202
|
+
title: 'Caption track failed',
|
|
203
|
+
status: 'error',
|
|
204
|
+
description: errorMessage,
|
|
205
|
+
})
|
|
206
|
+
await refreshAssetData()
|
|
207
|
+
onUpdate(erroredTrack, oldTrackId)
|
|
208
|
+
setIsSubmitting(false)
|
|
209
|
+
},
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
if (!result.found || !result.track) {
|
|
213
|
+
toast.push({
|
|
214
|
+
title: 'Caption track may have been updated',
|
|
215
|
+
status: 'warning',
|
|
216
|
+
description:
|
|
217
|
+
'The track was updated but its status could not be determined. It may still be processing. Please refresh the page to see if it appears.',
|
|
218
|
+
})
|
|
219
|
+
setIsSubmitting(false)
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (result.status === 'errored') {
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
await refreshAssetData()
|
|
228
|
+
|
|
229
|
+
if (result.status === 'preparing') {
|
|
230
|
+
toast.push({
|
|
231
|
+
title: 'Caption track is processing',
|
|
232
|
+
status: 'info',
|
|
233
|
+
description:
|
|
234
|
+
'The track was updated and is being processed. It will appear in the list shortly.',
|
|
235
|
+
})
|
|
236
|
+
} else {
|
|
237
|
+
toast.push({
|
|
238
|
+
title: 'Caption track updated',
|
|
239
|
+
status: 'success',
|
|
240
|
+
description: 'Caption track updated successfully',
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
onUpdate(result.track, oldTrackId)
|
|
245
|
+
setIsSubmitting(false)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const handleSubmit = async () => {
|
|
249
|
+
if (!name.trim()) {
|
|
250
|
+
toast.push({
|
|
251
|
+
title: 'Audio name required',
|
|
252
|
+
status: 'error',
|
|
253
|
+
description: 'Please enter an audio name for this caption track',
|
|
254
|
+
})
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!languageCode.trim()) {
|
|
259
|
+
toast.push({
|
|
260
|
+
title: 'Language code required',
|
|
261
|
+
status: 'error',
|
|
262
|
+
description: 'Please enter a language code (e.g., en, es, fr)',
|
|
263
|
+
})
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
setIsSubmitting(true)
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
if (!asset.assetId) {
|
|
271
|
+
throw new Error('Asset ID is required')
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const originalVttUrl = (() => {
|
|
275
|
+
if (isAutogenerated || !track.id) return ''
|
|
276
|
+
const playbackId = getPlaybackId(asset)
|
|
277
|
+
if (!playbackId) return ''
|
|
278
|
+
let url = `https://stream.mux.com/${playbackId}/text/${track.id}.vtt`
|
|
279
|
+
if (getPlaybackPolicy(asset)?.policy === 'signed') {
|
|
280
|
+
const token = generateJwt(client, playbackId, 'v')
|
|
281
|
+
url += `?token=${token}`
|
|
282
|
+
}
|
|
283
|
+
return url
|
|
284
|
+
})()
|
|
285
|
+
|
|
286
|
+
const urlChanged =
|
|
287
|
+
selectedFile !== null || (vttUrl.trim() && vttUrl.trim() !== originalVttUrl)
|
|
288
|
+
|
|
289
|
+
if (!urlChanged) {
|
|
290
|
+
toast.push({
|
|
291
|
+
title: 'No changes',
|
|
292
|
+
status: 'info',
|
|
293
|
+
description:
|
|
294
|
+
'Please provide a new VTT file or URL using the "Replace" button or URL field to update the track.',
|
|
295
|
+
})
|
|
296
|
+
setIsSubmitting(false)
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (urlChanged) {
|
|
301
|
+
if (!selectedFile && vttUrl.trim()) {
|
|
302
|
+
try {
|
|
303
|
+
void new URL(vttUrl.trim())
|
|
304
|
+
} catch {
|
|
305
|
+
toast.push({
|
|
306
|
+
title: 'Invalid URL',
|
|
307
|
+
status: 'error',
|
|
308
|
+
description: 'Please enter a valid URL (e.g., https://example.com/subtitles.vtt)',
|
|
309
|
+
})
|
|
310
|
+
setIsSubmitting(false)
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (!selectedFile && !vttUrl.trim()) {
|
|
316
|
+
toast.push({
|
|
317
|
+
title: 'VTT file or URL required',
|
|
318
|
+
status: 'error',
|
|
319
|
+
description: 'Please select a VTT file or enter a VTT file URL',
|
|
320
|
+
})
|
|
321
|
+
setIsSubmitting(false)
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
await handleUpdateTrackWithNewUrl()
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
onClose()
|
|
329
|
+
} catch (error) {
|
|
330
|
+
toast.push({
|
|
331
|
+
title: 'Failed to update caption track',
|
|
332
|
+
status: 'error',
|
|
333
|
+
description: error instanceof Error ? error.message : 'Please try again',
|
|
334
|
+
})
|
|
335
|
+
} finally {
|
|
336
|
+
setIsSubmitting(false)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return (
|
|
341
|
+
<Dialog
|
|
342
|
+
id={dialogId}
|
|
343
|
+
header="Edit Caption Track"
|
|
344
|
+
onClose={onClose}
|
|
345
|
+
width={1}
|
|
346
|
+
onClickOutside={onClose}
|
|
347
|
+
>
|
|
348
|
+
<Stack padding={4} space={4}>
|
|
349
|
+
<Stack space={2}>
|
|
350
|
+
<Card padding={3} marginBottom={2} tone="transparent" border radius={2}>
|
|
351
|
+
<Flex align="center" justify="space-between">
|
|
352
|
+
<Text>{getCurrentFileName()}</Text>
|
|
353
|
+
<Flex gap={2}>
|
|
354
|
+
{track.status !== 'errored' && (
|
|
355
|
+
<Button
|
|
356
|
+
icon={
|
|
357
|
+
downloading ? (
|
|
358
|
+
<Spinner
|
|
359
|
+
style={{
|
|
360
|
+
verticalAlign: 'middle',
|
|
361
|
+
display: 'inline-block',
|
|
362
|
+
marginTop: '-2px',
|
|
363
|
+
width: '0.5em',
|
|
364
|
+
height: '0.5em',
|
|
365
|
+
}}
|
|
366
|
+
/>
|
|
367
|
+
) : (
|
|
368
|
+
<DownloadIcon />
|
|
369
|
+
)
|
|
370
|
+
}
|
|
371
|
+
text="Download"
|
|
372
|
+
mode="ghost"
|
|
373
|
+
tone="primary"
|
|
374
|
+
fontSize={1}
|
|
375
|
+
padding={2}
|
|
376
|
+
onClick={handleDownloadCurrentFile}
|
|
377
|
+
disabled={downloading || isSubmitting}
|
|
378
|
+
/>
|
|
379
|
+
)}
|
|
380
|
+
<Button
|
|
381
|
+
icon={UploadIcon}
|
|
382
|
+
text="Replace"
|
|
383
|
+
mode="ghost"
|
|
384
|
+
tone="primary"
|
|
385
|
+
fontSize={1}
|
|
386
|
+
padding={2}
|
|
387
|
+
onClick={() => fileInputRef.current?.click()}
|
|
388
|
+
disabled={isSubmitting}
|
|
389
|
+
/>
|
|
390
|
+
</Flex>
|
|
391
|
+
</Flex>
|
|
392
|
+
<input
|
|
393
|
+
ref={fileInputRef}
|
|
394
|
+
type="file"
|
|
395
|
+
accept=".vtt,text/vtt"
|
|
396
|
+
style={{display: 'none'}}
|
|
397
|
+
onChange={(e) => {
|
|
398
|
+
if (e.target.files && e.target.files.length > 0 && !isSubmitting) {
|
|
399
|
+
setSelectedFile(e.target.files[0])
|
|
400
|
+
setVttUrl('')
|
|
401
|
+
}
|
|
402
|
+
}}
|
|
403
|
+
/>
|
|
404
|
+
{selectedFile && (
|
|
405
|
+
<Text size={1} muted style={{marginTop: 8}}>
|
|
406
|
+
Selected: {selectedFile.name}
|
|
407
|
+
</Text>
|
|
408
|
+
)}
|
|
409
|
+
</Card>
|
|
410
|
+
<Stack space={2}>
|
|
411
|
+
<Label htmlFor="vtt-url">VTT File URL</Label>
|
|
412
|
+
<TextInput
|
|
413
|
+
id="vtt-url"
|
|
414
|
+
placeholder="https://example.com/subtitles.vtt"
|
|
415
|
+
value={vttUrl}
|
|
416
|
+
onChange={(e) => {
|
|
417
|
+
setVttUrl(e.currentTarget.value)
|
|
418
|
+
setSelectedFile(null)
|
|
419
|
+
}}
|
|
420
|
+
disabled={isSubmitting}
|
|
421
|
+
/>
|
|
422
|
+
<Text size={1} muted>
|
|
423
|
+
Add a URL to replace the existing VTT file with a new one
|
|
424
|
+
</Text>
|
|
425
|
+
</Stack>
|
|
426
|
+
</Stack>
|
|
427
|
+
|
|
428
|
+
<Stack space={2}>
|
|
429
|
+
<Label htmlFor="caption-name">Audio name</Label>
|
|
430
|
+
<Autocomplete
|
|
431
|
+
id="caption-name"
|
|
432
|
+
value={selectedLanguage?.value || ''}
|
|
433
|
+
onChange={(newValue) => {
|
|
434
|
+
const selected = LANGUAGE_OPTIONS.find((opt) => opt.value === newValue)
|
|
435
|
+
if (selected) {
|
|
436
|
+
setSelectedLanguage(selected)
|
|
437
|
+
setLanguageCode(selected.value)
|
|
438
|
+
setName(selected.label)
|
|
439
|
+
}
|
|
440
|
+
}}
|
|
441
|
+
options={LANGUAGE_OPTIONS}
|
|
442
|
+
icon={TranslateIcon}
|
|
443
|
+
placeholder="Select language"
|
|
444
|
+
filterOption={(query, option) =>
|
|
445
|
+
option.label.toLowerCase().indexOf(query.toLowerCase()) > -1 ||
|
|
446
|
+
option.value.toLowerCase().indexOf(query.toLowerCase()) > -1
|
|
447
|
+
}
|
|
448
|
+
openButton
|
|
449
|
+
renderValue={(value) => LANGUAGE_OPTIONS.find((l) => l.value === value)?.label || value}
|
|
450
|
+
renderOption={(option) => (
|
|
451
|
+
<Card data-as="button" padding={3} radius={2} tone="inherit">
|
|
452
|
+
<Text size={2} textOverflow="ellipsis">
|
|
453
|
+
{option.label} ({option.value})
|
|
454
|
+
</Text>
|
|
455
|
+
</Card>
|
|
456
|
+
)}
|
|
457
|
+
disabled={isSubmitting}
|
|
458
|
+
/>
|
|
459
|
+
</Stack>
|
|
460
|
+
|
|
461
|
+
<Stack space={2}>
|
|
462
|
+
<Label htmlFor="caption-language">Language Code</Label>
|
|
463
|
+
<TextInput
|
|
464
|
+
id="caption-language"
|
|
465
|
+
placeholder="en-US"
|
|
466
|
+
value={languageCode}
|
|
467
|
+
onChange={(e) => {
|
|
468
|
+
setLanguageCode(e.currentTarget.value)
|
|
469
|
+
if (selectedLanguage && selectedLanguage.value !== e.currentTarget.value) {
|
|
470
|
+
setSelectedLanguage(null)
|
|
471
|
+
if (!name || name === selectedLanguage.label) {
|
|
472
|
+
setName('')
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}}
|
|
476
|
+
disabled={isSubmitting}
|
|
477
|
+
/>
|
|
478
|
+
</Stack>
|
|
479
|
+
|
|
480
|
+
<Flex gap={2} justify="flex-end" marginTop={2}>
|
|
481
|
+
<Button text="Cancel" mode="ghost" onClick={onClose} disabled={isSubmitting} />
|
|
482
|
+
<Button
|
|
483
|
+
text="Update Caption Track"
|
|
484
|
+
tone="primary"
|
|
485
|
+
icon={
|
|
486
|
+
isSubmitting ? (
|
|
487
|
+
<Spinner
|
|
488
|
+
style={{
|
|
489
|
+
verticalAlign: 'middle',
|
|
490
|
+
display: 'inline-block',
|
|
491
|
+
marginBottom: '-3px',
|
|
492
|
+
width: '1em',
|
|
493
|
+
height: '1em',
|
|
494
|
+
marginRight: '-6px',
|
|
495
|
+
}}
|
|
496
|
+
/>
|
|
497
|
+
) : (
|
|
498
|
+
UploadIcon
|
|
499
|
+
)
|
|
500
|
+
}
|
|
501
|
+
onClick={handleSubmit}
|
|
502
|
+
disabled={isSubmitting}
|
|
503
|
+
/>
|
|
504
|
+
</Flex>
|
|
505
|
+
</Stack>
|
|
506
|
+
</Dialog>
|
|
507
|
+
)
|
|
508
|
+
}
|
|
@@ -16,7 +16,8 @@ export default function InputBrowser({
|
|
|
16
16
|
setDialogState,
|
|
17
17
|
asset,
|
|
18
18
|
onChange,
|
|
19
|
-
|
|
19
|
+
config,
|
|
20
|
+
}: Pick<SelectAssetProps, 'onChange' | 'asset' | 'config'> & {
|
|
20
21
|
setDialogState: SetDialogState
|
|
21
22
|
}) {
|
|
22
23
|
const id = `InputBrowser${useId()}`
|
|
@@ -29,7 +30,12 @@ export default function InputBrowser({
|
|
|
29
30
|
onClose={handleClose}
|
|
30
31
|
width={2}
|
|
31
32
|
>
|
|
32
|
-
<SelectAsset
|
|
33
|
+
<SelectAsset
|
|
34
|
+
config={config}
|
|
35
|
+
asset={asset}
|
|
36
|
+
onChange={onChange}
|
|
37
|
+
setDialogState={setDialogState}
|
|
38
|
+
/>
|
|
33
39
|
</StyledDialog>
|
|
34
40
|
)
|
|
35
41
|
}
|
|
@@ -2,10 +2,10 @@ import {PlugIcon} from '@sanity/icons'
|
|
|
2
2
|
import {Button, Card, Flex, Grid, Heading, Inline, Text} from '@sanity/ui'
|
|
3
3
|
import {useCallback} from 'react'
|
|
4
4
|
|
|
5
|
+
import {useAccessControl} from '../hooks/useAccessControl'
|
|
5
6
|
import type {SetDialogState} from '../hooks/useDialogState'
|
|
6
|
-
import MuxLogo from './MuxLogo'
|
|
7
7
|
import {PluginConfig} from '../util/types'
|
|
8
|
-
import
|
|
8
|
+
import MuxLogo from './MuxLogo'
|
|
9
9
|
|
|
10
10
|
interface OnboardProps {
|
|
11
11
|
setDialogState: SetDialogState
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {ChevronLeftIcon, ChevronRightIcon} from '@sanity/icons'
|
|
2
|
+
import {Button, Label} from '@sanity/ui'
|
|
3
|
+
import {Dispatch, SetStateAction, useEffect} from 'react'
|
|
4
|
+
|
|
5
|
+
const PageSelector = (props: {
|
|
6
|
+
page: number
|
|
7
|
+
setPage: Dispatch<SetStateAction<number>>
|
|
8
|
+
total: number
|
|
9
|
+
}) => {
|
|
10
|
+
const page = props.page
|
|
11
|
+
const setPage = props.setPage
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
// Constraint in bounds.
|
|
15
|
+
const clamped = Math.min(props.total - 1, Math.max(0, page))
|
|
16
|
+
if (page !== clamped) {
|
|
17
|
+
setPage(clamped)
|
|
18
|
+
}
|
|
19
|
+
}, [page, props.total, setPage])
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<>
|
|
23
|
+
<Button
|
|
24
|
+
icon={ChevronLeftIcon}
|
|
25
|
+
mode="bleed"
|
|
26
|
+
padding={3}
|
|
27
|
+
style={{cursor: 'pointer'}}
|
|
28
|
+
disabled={page <= 0}
|
|
29
|
+
onClick={() => {
|
|
30
|
+
setPage((p) => {
|
|
31
|
+
return Math.min(props.total - 1, Math.max(0, p - 1))
|
|
32
|
+
})
|
|
33
|
+
}}
|
|
34
|
+
/>
|
|
35
|
+
<Label muted>
|
|
36
|
+
Page {page + 1}/{props.total}
|
|
37
|
+
</Label>
|
|
38
|
+
<Button
|
|
39
|
+
icon={ChevronRightIcon}
|
|
40
|
+
mode="bleed"
|
|
41
|
+
padding={3}
|
|
42
|
+
style={{cursor: 'pointer'}}
|
|
43
|
+
disabled={page >= props.total - 1}
|
|
44
|
+
onClick={() => {
|
|
45
|
+
setPage((p) => {
|
|
46
|
+
return Math.min(props.total - 1, Math.max(0, p + 1))
|
|
47
|
+
})
|
|
48
|
+
}}
|
|
49
|
+
/>
|
|
50
|
+
</>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default PageSelector
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {useState} from 'react'
|
|
1
|
+
import {Suspense, useState} from 'react'
|
|
2
2
|
import {styled} from 'styled-components'
|
|
3
3
|
|
|
4
4
|
import {useClient} from '../hooks/useClient'
|
|
@@ -46,5 +46,10 @@ export function ThumbnailsMetadataTrack({asset}: ThumbnailsMetadataTrackProps) {
|
|
|
46
46
|
// Why useState instead of useMemo? Because we really really only want to run it exactly once and useMemo doesn't make that guarantee
|
|
47
47
|
const [src] = useState<string>(() => getStoryboardSrc({asset, client}))
|
|
48
48
|
|
|
49
|
-
return
|
|
49
|
+
return (
|
|
50
|
+
/* We use Suspense here because `getStoryboardSrc` uses suspend() under the hood */
|
|
51
|
+
<Suspense fallback={null}>
|
|
52
|
+
<track label="thumbnails" default kind="metadata" src={src} />
|
|
53
|
+
</Suspense>
|
|
54
|
+
)
|
|
50
55
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
PlugIcon,
|
|
6
6
|
ResetIcon,
|
|
7
7
|
SearchIcon,
|
|
8
|
+
TranslateIcon,
|
|
8
9
|
UploadIcon,
|
|
9
10
|
} from '@sanity/icons'
|
|
10
11
|
import {
|
|
@@ -63,7 +64,7 @@ function PlayerActionsMenu(
|
|
|
63
64
|
const {asset, readOnly, dialogState, setDialogState, onChange, onSelect, accept} = props
|
|
64
65
|
const [open, setOpen] = useState(false)
|
|
65
66
|
const [menuElement, setMenuRef] = useState<HTMLDivElement | null>(null)
|
|
66
|
-
const isSigned = useMemo(() => getPlaybackPolicy(asset) === 'signed', [asset])
|
|
67
|
+
const isSigned = useMemo(() => getPlaybackPolicy(asset)?.policy === 'signed', [asset])
|
|
67
68
|
const {hasConfigAccess} = useAccessControl(props.config)
|
|
68
69
|
|
|
69
70
|
const onReset = useCallback(() => onChange(PatchEvent.from(unset([]))), [onChange])
|
|
@@ -122,11 +123,18 @@ function PlayerActionsMenu(
|
|
|
122
123
|
onClick={() => setDialogState('select-video')}
|
|
123
124
|
/>
|
|
124
125
|
{isVideoAsset(asset) && (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
<>
|
|
127
|
+
<MenuItem
|
|
128
|
+
icon={ImageIcon}
|
|
129
|
+
text="Thumbnail"
|
|
130
|
+
onClick={() => setDialogState('edit-thumbnail')}
|
|
131
|
+
/>
|
|
132
|
+
<MenuItem
|
|
133
|
+
icon={TranslateIcon}
|
|
134
|
+
text="Captions"
|
|
135
|
+
onClick={() => setDialogState('edit-captions')}
|
|
136
|
+
/>
|
|
137
|
+
</>
|
|
130
138
|
)}
|
|
131
139
|
<MenuDivider />
|
|
132
140
|
{hasConfigAccess && (
|
|
@@ -2,15 +2,21 @@ import {useCallback} from 'react'
|
|
|
2
2
|
import {PatchEvent, set, setIfMissing, unset} from 'sanity'
|
|
3
3
|
|
|
4
4
|
import type {SetDialogState} from '../hooks/useDialogState'
|
|
5
|
-
import type {MuxInputProps, VideoAssetDocument} from '../util/types'
|
|
5
|
+
import type {MuxInputProps, PluginConfig, VideoAssetDocument} from '../util/types'
|
|
6
6
|
import VideosBrowser, {type VideosBrowserProps} from './VideosBrowser'
|
|
7
7
|
|
|
8
8
|
export interface Props extends Pick<MuxInputProps, 'onChange'> {
|
|
9
9
|
asset?: VideoAssetDocument | null | undefined
|
|
10
10
|
setDialogState: SetDialogState
|
|
11
|
+
config: PluginConfig
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
export default function SelectAssets({
|
|
14
|
+
export default function SelectAssets({
|
|
15
|
+
asset: selectedAsset,
|
|
16
|
+
onChange,
|
|
17
|
+
setDialogState,
|
|
18
|
+
config,
|
|
19
|
+
}: Props) {
|
|
14
20
|
const handleSelect = useCallback<Required<VideosBrowserProps>['onSelect']>(
|
|
15
21
|
(chosenAsset) => {
|
|
16
22
|
if (!chosenAsset?._id) {
|
|
@@ -29,5 +35,5 @@ export default function SelectAssets({asset: selectedAsset, onChange, setDialogS
|
|
|
29
35
|
[onChange, setDialogState, selectedAsset]
|
|
30
36
|
)
|
|
31
37
|
|
|
32
|
-
return <VideosBrowser onSelect={handleSelect} />
|
|
38
|
+
return <VideosBrowser onSelect={handleSelect} config={config} />
|
|
33
39
|
}
|
|
@@ -4,8 +4,8 @@ import type {PluginConfig} from '../util/types'
|
|
|
4
4
|
import ToolIcon from './icons/ToolIcon'
|
|
5
5
|
import VideosBrowser from './VideosBrowser'
|
|
6
6
|
|
|
7
|
-
const StudioTool: React.FC<PluginConfig> = () => {
|
|
8
|
-
return <VideosBrowser />
|
|
7
|
+
const StudioTool: React.FC<PluginConfig> = (config) => {
|
|
8
|
+
return <VideosBrowser config={config} />
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export const DEFAULT_TOOL_CONFIG = {
|