sanity-plugin-mux-input 3.0.5 → 4.0.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 (123) hide show
  1. package/dist/index.js +20 -92
  2. package/dist/index.js.map +1 -1
  3. package/package.json +5 -15
  4. package/dist/index.cjs +0 -5746
  5. package/dist/index.cjs.map +0 -1
  6. package/dist/index.d.cts +0 -288
  7. package/dist/index.d.cts.map +0 -1
  8. package/sanity.json +0 -8
  9. package/src/_exports/index.ts +0 -73
  10. package/src/actions/assets.ts +0 -152
  11. package/src/actions/secrets.ts +0 -110
  12. package/src/actions/upload.ts +0 -308
  13. package/src/clients/upChunkObservable.ts +0 -54
  14. package/src/components/AddCaptionDialog.tsx +0 -440
  15. package/src/components/CaptionsDialog.tsx +0 -23
  16. package/src/components/ConfigureApi.styled.tsx +0 -19
  17. package/src/components/ConfigureApi.tsx +0 -296
  18. package/src/components/DraggableWatermark.tsx +0 -885
  19. package/src/components/EditCaptionDialog.tsx +0 -511
  20. package/src/components/EditThumbnailDialog.tsx +0 -121
  21. package/src/components/ErrorBoundaryCard.tsx +0 -97
  22. package/src/components/FileInputButton.tsx +0 -54
  23. package/src/components/FileInputMenuItem.styled.tsx +0 -36
  24. package/src/components/FileInputMenuItem.tsx +0 -85
  25. package/src/components/FormField.tsx +0 -38
  26. package/src/components/IconInfo.tsx +0 -22
  27. package/src/components/ImportVideosFromMux.tsx +0 -339
  28. package/src/components/Input.styled.tsx +0 -22
  29. package/src/components/Input.tsx +0 -78
  30. package/src/components/InputBrowser.tsx +0 -41
  31. package/src/components/MuxLogo.tsx +0 -42
  32. package/src/components/Onboard.tsx +0 -65
  33. package/src/components/PageSelector.tsx +0 -54
  34. package/src/components/Player.styled.tsx +0 -11
  35. package/src/components/Player.tsx +0 -117
  36. package/src/components/PlayerActionsMenu.tsx +0 -191
  37. package/src/components/ResyncMetadata.tsx +0 -278
  38. package/src/components/SelectAsset.tsx +0 -39
  39. package/src/components/SelectSortOptions.tsx +0 -45
  40. package/src/components/SpinnerBox.tsx +0 -16
  41. package/src/components/StudioTool.tsx +0 -24
  42. package/src/components/TextTracksEditor.tsx +0 -117
  43. package/src/components/TextTracksManager.tsx +0 -738
  44. package/src/components/UploadConfiguration.tsx +0 -696
  45. package/src/components/UploadPlaceholder.tsx +0 -88
  46. package/src/components/UploadProgress.tsx +0 -80
  47. package/src/components/Uploader.styled.tsx +0 -65
  48. package/src/components/Uploader.tsx +0 -499
  49. package/src/components/VideoDetails/DeleteDialog.tsx +0 -148
  50. package/src/components/VideoDetails/VideoDetails.tsx +0 -358
  51. package/src/components/VideoDetails/VideoReferences.tsx +0 -63
  52. package/src/components/VideoDetails/useVideoDetails.ts +0 -103
  53. package/src/components/VideoInBrowser.tsx +0 -245
  54. package/src/components/VideoMetadata.tsx +0 -45
  55. package/src/components/VideoPlayer.tsx +0 -241
  56. package/src/components/VideoThumbnail.tsx +0 -139
  57. package/src/components/VideosBrowser.tsx +0 -100
  58. package/src/components/documentPreview/DocumentPreview.tsx +0 -84
  59. package/src/components/documentPreview/DraftStatus.tsx +0 -34
  60. package/src/components/documentPreview/MissingSchemaType.tsx +0 -32
  61. package/src/components/documentPreview/PaneItemPreview.tsx +0 -67
  62. package/src/components/documentPreview/PublishedStatus.tsx +0 -35
  63. package/src/components/documentPreview/TimeAgo.tsx +0 -12
  64. package/src/components/icons/Audio.tsx +0 -13
  65. package/src/components/icons/Resolution.tsx +0 -12
  66. package/src/components/icons/StopWatch.tsx +0 -20
  67. package/src/components/icons/ToolIcon.tsx +0 -19
  68. package/src/components/uploadConfiguration/PlaybackPolicy.tsx +0 -133
  69. package/src/components/uploadConfiguration/PlaybackPolicyOption.tsx +0 -76
  70. package/src/components/uploadConfiguration/PlaybackPolicyWarning.tsx +0 -29
  71. package/src/components/uploadConfiguration/ResolutionTierSelector.tsx +0 -72
  72. package/src/components/uploadConfiguration/StaticRenditionSelector.tsx +0 -180
  73. package/src/components/withFocusRing/helpers.ts +0 -24
  74. package/src/components/withFocusRing/index.ts +0 -1
  75. package/src/components/withFocusRing/withFocusRing.ts +0 -30
  76. package/src/context/DialogStateContext.tsx +0 -33
  77. package/src/context/DrmPlaybackWarningContext.tsx +0 -97
  78. package/src/hooks/useAccessControl.ts +0 -13
  79. package/src/hooks/useAssetDocumentValues.ts +0 -11
  80. package/src/hooks/useAssets.ts +0 -73
  81. package/src/hooks/useCancelUpload.ts +0 -22
  82. package/src/hooks/useClient.ts +0 -8
  83. package/src/hooks/useDialogState.ts +0 -11
  84. package/src/hooks/useDocReferences.ts +0 -21
  85. package/src/hooks/useFetchFileSize.ts +0 -55
  86. package/src/hooks/useImportMuxAssets.ts +0 -132
  87. package/src/hooks/useInView.ts +0 -41
  88. package/src/hooks/useMediaMetadata.ts +0 -104
  89. package/src/hooks/useMuxAssets.ts +0 -179
  90. package/src/hooks/useMuxPolling.ts +0 -52
  91. package/src/hooks/useResyncAsset.ts +0 -110
  92. package/src/hooks/useResyncMuxMetadata.ts +0 -169
  93. package/src/hooks/useSaveSecrets.ts +0 -78
  94. package/src/hooks/useSecretsDocumentValues.ts +0 -38
  95. package/src/hooks/useSecretsFormState.ts +0 -47
  96. package/src/plugin.tsx +0 -31
  97. package/src/sanity-ui.d.ts +0 -5
  98. package/src/schema.ts +0 -196
  99. package/src/util/addKeysToMuxData.ts +0 -30
  100. package/src/util/asserters.ts +0 -23
  101. package/src/util/assetTitlePlaceholder.ts +0 -31
  102. package/src/util/constants.ts +0 -15
  103. package/src/util/convertWatermarkToMux.ts +0 -160
  104. package/src/util/createSearchFilter.ts +0 -76
  105. package/src/util/createUrlParamsObject.ts +0 -29
  106. package/src/util/extractFiles.ts +0 -67
  107. package/src/util/formatBytes.ts +0 -31
  108. package/src/util/formatDriveShareLink.ts +0 -64
  109. package/src/util/formatSeconds.ts +0 -48
  110. package/src/util/generateJwt.ts +0 -57
  111. package/src/util/getAnimatedPosterSrc.ts +0 -26
  112. package/src/util/getPlaybackPolicy.ts +0 -69
  113. package/src/util/getPosterSrc.ts +0 -28
  114. package/src/util/getVideoMetadata.ts +0 -23
  115. package/src/util/getVideoSrc.ts +0 -23
  116. package/src/util/parsers.ts +0 -5
  117. package/src/util/pluginVersion.ts +0 -5
  118. package/src/util/readSecrets.ts +0 -38
  119. package/src/util/roundPxString.ts +0 -16
  120. package/src/util/textTracks.ts +0 -222
  121. package/src/util/tryWithSuspend.ts +0 -22
  122. package/src/util/types.ts +0 -566
  123. package/v2-incompatible.js +0 -11
@@ -1,511 +0,0 @@
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 {addKeysToMuxData} from '../util/addKeysToMuxData'
21
- import {generateJwt} from '../util/generateJwt'
22
- import {getPlaybackId} from '../util/getPlaybackPolicy'
23
- import {getPlaybackPolicy} from '../util/getPlaybackPolicy'
24
- import {downloadVttFile, extractErrorMessage, pollTrackStatus} from '../util/textTracks'
25
- import type {MuxTextTrack, VideoAssetDocument} from '../util/types'
26
-
27
- const LANGUAGE_OPTIONS = LanguagesList.getAllCodes().map((code) => ({
28
- value: code,
29
- label: LanguagesList.getNativeName(code),
30
- }))
31
-
32
- export interface Props {
33
- asset: VideoAssetDocument
34
- track: MuxTextTrack
35
- onUpdate: (track: MuxTextTrack, oldTrackId?: string) => void
36
- onClose: () => void
37
- }
38
-
39
- export default function EditCaptionDialog({asset, track, onUpdate, onClose}: Props) {
40
- const client = useClient()
41
- const toast = useToast()
42
- const dialogId = `EditCaptionDialog${useId()}`
43
-
44
- const isAutogenerated =
45
- track.text_source === 'generated_live' ||
46
- track.text_source === 'generated_live_final' ||
47
- track.text_source === 'generated_vod'
48
-
49
- const [vttUrl, setVttUrl] = useState('')
50
- const [languageCode, setLanguageCode] = useState(track.language_code || '')
51
- const [selectedLanguage, setSelectedLanguage] = useState<{value: string; label: string} | null>(
52
- () => {
53
- const baseCode = track.language_code?.split('-')[0]
54
- const found = LANGUAGE_OPTIONS.find(
55
- (opt) => opt.value === track.language_code || opt.value === baseCode,
56
- )
57
- if (found) return found
58
- if (track.name) {
59
- const foundByName = LANGUAGE_OPTIONS.find((opt) => opt.label === track.name)
60
- if (foundByName) return foundByName
61
- }
62
- return null
63
- },
64
- )
65
- const [name, setName] = useState(track.name || '')
66
- const [isSubmitting, setIsSubmitting] = useState(false)
67
- const [downloading, setDownloading] = useState(false)
68
- const [selectedFile, setSelectedFile] = useState<File | null>(null)
69
- const fileInputRef = useRef<HTMLInputElement>(null)
70
-
71
- useEffect(() => {
72
- // oxlint-disable-next-line react/react-compiler
73
- setLanguageCode(track.language_code || '')
74
- setName(track.name || '')
75
- setVttUrl('')
76
- const baseCode = track.language_code?.split('-')[0]
77
- const foundByCode = LANGUAGE_OPTIONS.find(
78
- (opt) => opt.value === track.language_code || opt.value === baseCode,
79
- )
80
- const foundByName = track.name ? LANGUAGE_OPTIONS.find((opt) => opt.label === track.name) : null
81
- setSelectedLanguage(foundByCode || foundByName || null)
82
- }, [track, asset, client])
83
-
84
- const handleDownloadCurrentFile = async () => {
85
- setDownloading(true)
86
- try {
87
- await downloadVttFile(client, asset, track)
88
- } catch (error) {
89
- let errorMessage = 'Please try again'
90
- let title = 'Failed to download VTT file'
91
-
92
- if (error instanceof Error) {
93
- errorMessage = error.message
94
- if (error.message.includes('Track')) {
95
- title = 'Cannot download'
96
- }
97
- } else if (error === 'Track ID is missing' || error === 'Track is not ready yet') {
98
- errorMessage = error
99
- title = 'Cannot download'
100
- }
101
-
102
- toast.push({
103
- title,
104
- status: 'error',
105
- description: errorMessage,
106
- })
107
- } finally {
108
- setDownloading(false)
109
- }
110
- }
111
-
112
- const getCurrentFileName = () => {
113
- if (track.id && asset.filename) {
114
- return `${asset.filename}-${track.language_code || 'en'}.vtt`
115
- }
116
- return `captions-${track.language_code || 'en'}.vtt`
117
- }
118
-
119
- const uploadVttFile = async (file: File): Promise<string> => {
120
- const assetDocument = await client.assets.upload('file', file, {
121
- filename: file.name,
122
- })
123
- return assetDocument.url
124
- }
125
-
126
- const refreshAssetData = async () => {
127
- if (!asset._id || !asset.assetId) return
128
- try {
129
- const latestAssetData = await getAsset(client, asset.assetId)
130
- const dataWithKeys = addKeysToMuxData(latestAssetData.data)
131
- await client
132
- .patch(asset._id)
133
- .set({data: dataWithKeys, status: latestAssetData.data.status})
134
- .commit({returnDocuments: false})
135
- } catch (refreshError) {
136
- console.error('Failed to refresh asset data:', refreshError)
137
- }
138
- }
139
-
140
- const handleUpdateTrackWithNewUrl = async () => {
141
- if (!asset.assetId) {
142
- throw new Error('Asset ID is required')
143
- }
144
-
145
- const trimmedName = name.trim()
146
- const trimmedLanguageCode = languageCode.trim()
147
-
148
- const oldTrackId = track.id
149
-
150
- try {
151
- await deleteTextTrack(client, asset.assetId, oldTrackId)
152
- } catch (deleteError) {
153
- toast.push({
154
- title: 'Failed to delete old track',
155
- status: 'error',
156
- description: 'Could not delete the old track. Please try again or delete it manually.',
157
- })
158
- setIsSubmitting(false)
159
- throw deleteError
160
- }
161
-
162
- let vttUrlToUse = vttUrl.trim()
163
-
164
- if (selectedFile) {
165
- try {
166
- vttUrlToUse = await uploadVttFile(selectedFile)
167
- } catch (uploadError) {
168
- toast.push({
169
- title: 'Failed to upload VTT file',
170
- status: 'error',
171
- description: 'Could not upload the VTT file to Sanity. Please try again.',
172
- })
173
- setIsSubmitting(false)
174
- throw uploadError
175
- }
176
- }
177
-
178
- try {
179
- await addTextTrackFromUrl(client, asset.assetId, vttUrlToUse, {
180
- language_code: trimmedLanguageCode,
181
- name: trimmedName,
182
- text_type: 'subtitles',
183
- })
184
- } catch (error: unknown) {
185
- toast.push({
186
- title: 'Failed to update caption track',
187
- status: 'error',
188
- description: extractErrorMessage(error, 'Failed to update caption track'),
189
- })
190
- setIsSubmitting(false)
191
- throw error
192
- }
193
-
194
- const result = await pollTrackStatus({
195
- client,
196
- assetId: asset.assetId,
197
- trackName: trimmedName,
198
- trackLanguageCode: trimmedLanguageCode,
199
- onTrackErrored: async (erroredTrack) => {
200
- const errorMessage =
201
- erroredTrack.error?.messages?.[0] ||
202
- erroredTrack.error?.type ||
203
- 'The track failed to download from the provided URL'
204
- toast.push({
205
- title: 'Caption track failed',
206
- status: 'error',
207
- description: errorMessage,
208
- })
209
- await refreshAssetData()
210
- onUpdate(erroredTrack, oldTrackId)
211
- setIsSubmitting(false)
212
- },
213
- })
214
-
215
- if (!result.found || !result.track) {
216
- toast.push({
217
- title: 'Caption track may have been updated',
218
- status: 'warning',
219
- description:
220
- '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.',
221
- })
222
- setIsSubmitting(false)
223
- return
224
- }
225
-
226
- if (result.status === 'errored') {
227
- return
228
- }
229
-
230
- await refreshAssetData()
231
-
232
- if (result.status === 'preparing') {
233
- toast.push({
234
- title: 'Caption track is processing',
235
- status: 'info',
236
- description:
237
- 'The track was updated and is being processed. It will appear in the list shortly.',
238
- })
239
- } else {
240
- toast.push({
241
- title: 'Caption track updated',
242
- status: 'success',
243
- description: 'Caption track updated successfully',
244
- })
245
- }
246
-
247
- onUpdate(result.track, oldTrackId)
248
- setIsSubmitting(false)
249
- }
250
-
251
- const handleSubmit = async () => {
252
- if (!name.trim()) {
253
- toast.push({
254
- title: 'Audio name required',
255
- status: 'error',
256
- description: 'Please enter an audio name for this caption track',
257
- })
258
- return
259
- }
260
-
261
- if (!languageCode.trim()) {
262
- toast.push({
263
- title: 'Language code required',
264
- status: 'error',
265
- description: 'Please enter a language code (e.g., en, es, fr)',
266
- })
267
- return
268
- }
269
-
270
- setIsSubmitting(true)
271
-
272
- try {
273
- if (!asset.assetId) {
274
- throw new Error('Asset ID is required')
275
- }
276
-
277
- const originalVttUrl = (() => {
278
- if (isAutogenerated || !track.id) return ''
279
- const playbackId = getPlaybackId(asset)
280
- if (!playbackId) return ''
281
- let url = `https://stream.mux.com/${playbackId}/text/${track.id}.vtt`
282
- if (getPlaybackPolicy(asset)?.policy === 'signed') {
283
- const token = generateJwt(client, playbackId, 'v')
284
- url += `?token=${token}`
285
- }
286
- return url
287
- })()
288
-
289
- const urlChanged =
290
- selectedFile !== null || (vttUrl.trim() && vttUrl.trim() !== originalVttUrl)
291
-
292
- if (!urlChanged) {
293
- toast.push({
294
- title: 'No changes',
295
- status: 'info',
296
- description:
297
- 'Please provide a new VTT file or URL using the "Replace" button or URL field to update the track.',
298
- })
299
- setIsSubmitting(false)
300
- return
301
- }
302
-
303
- if (urlChanged) {
304
- if (!selectedFile && vttUrl.trim()) {
305
- try {
306
- void new URL(vttUrl.trim())
307
- } catch {
308
- toast.push({
309
- title: 'Invalid URL',
310
- status: 'error',
311
- description: 'Please enter a valid URL (e.g., https://example.com/subtitles.vtt)',
312
- })
313
- setIsSubmitting(false)
314
- return
315
- }
316
- }
317
-
318
- if (!selectedFile && !vttUrl.trim()) {
319
- toast.push({
320
- title: 'VTT file or URL required',
321
- status: 'error',
322
- description: 'Please select a VTT file or enter a VTT file URL',
323
- })
324
- setIsSubmitting(false)
325
- return
326
- }
327
-
328
- await handleUpdateTrackWithNewUrl()
329
- }
330
-
331
- onClose()
332
- } catch (error) {
333
- toast.push({
334
- title: 'Failed to update caption track',
335
- status: 'error',
336
- description: error instanceof Error ? error.message : 'Please try again',
337
- })
338
- } finally {
339
- setIsSubmitting(false)
340
- }
341
- }
342
-
343
- return (
344
- <Dialog
345
- id={dialogId}
346
- header="Edit Caption Track"
347
- onClose={onClose}
348
- width={1}
349
- onClickOutside={onClose}
350
- >
351
- <Stack padding={4} space={4}>
352
- <Stack space={2}>
353
- <Card padding={3} marginBottom={2} tone="transparent" border radius={2}>
354
- <Flex align="center" justify="space-between">
355
- <Text>{getCurrentFileName()}</Text>
356
- <Flex gap={2}>
357
- {track.status !== 'errored' && (
358
- <Button
359
- icon={
360
- downloading ? (
361
- <Spinner
362
- style={{
363
- verticalAlign: 'middle',
364
- display: 'inline-block',
365
- marginTop: '-2px',
366
- width: '0.5em',
367
- height: '0.5em',
368
- }}
369
- />
370
- ) : (
371
- <DownloadIcon />
372
- )
373
- }
374
- text="Download"
375
- mode="ghost"
376
- tone="primary"
377
- fontSize={1}
378
- padding={2}
379
- onClick={handleDownloadCurrentFile}
380
- disabled={downloading || isSubmitting}
381
- />
382
- )}
383
- <Button
384
- icon={UploadIcon}
385
- text="Replace"
386
- mode="ghost"
387
- tone="primary"
388
- fontSize={1}
389
- padding={2}
390
- onClick={() => fileInputRef.current?.click()}
391
- disabled={isSubmitting}
392
- />
393
- </Flex>
394
- </Flex>
395
- <input
396
- ref={fileInputRef}
397
- type="file"
398
- accept=".vtt,text/vtt"
399
- style={{display: 'none'}}
400
- onChange={(e) => {
401
- if (e.target.files && e.target.files.length > 0 && !isSubmitting) {
402
- setSelectedFile(e.target.files[0]!)
403
- setVttUrl('')
404
- }
405
- }}
406
- />
407
- {selectedFile && (
408
- <Text size={1} muted style={{marginTop: 8}}>
409
- Selected: {selectedFile.name}
410
- </Text>
411
- )}
412
- </Card>
413
- <Stack space={2}>
414
- <Label htmlFor="vtt-url">VTT File URL</Label>
415
- <TextInput
416
- id="vtt-url"
417
- placeholder="https://example.com/subtitles.vtt"
418
- value={vttUrl}
419
- onChange={(e) => {
420
- setVttUrl(e.currentTarget.value)
421
- setSelectedFile(null)
422
- }}
423
- disabled={isSubmitting}
424
- />
425
- <Text size={1} muted>
426
- Add a URL to replace the existing VTT file with a new one
427
- </Text>
428
- </Stack>
429
- </Stack>
430
-
431
- <Stack space={2}>
432
- <Label htmlFor="caption-name">Audio name</Label>
433
- <Autocomplete
434
- id="caption-name"
435
- value={selectedLanguage?.value || ''}
436
- onChange={(newValue) => {
437
- const selected = LANGUAGE_OPTIONS.find((opt) => opt.value === newValue)
438
- if (selected) {
439
- setSelectedLanguage(selected)
440
- setLanguageCode(selected.value)
441
- setName(selected.label)
442
- }
443
- }}
444
- options={LANGUAGE_OPTIONS}
445
- icon={TranslateIcon}
446
- placeholder="Select language"
447
- filterOption={(query, option) =>
448
- option.label.toLowerCase().indexOf(query.toLowerCase()) > -1 ||
449
- option.value.toLowerCase().indexOf(query.toLowerCase()) > -1
450
- }
451
- openButton
452
- renderValue={(value) => LANGUAGE_OPTIONS.find((l) => l.value === value)?.label || value}
453
- renderOption={(option) => (
454
- <Card data-as="button" padding={3} radius={2} tone="inherit">
455
- <Text size={2} textOverflow="ellipsis">
456
- {option.label} ({option.value})
457
- </Text>
458
- </Card>
459
- )}
460
- disabled={isSubmitting}
461
- />
462
- </Stack>
463
-
464
- <Stack space={2}>
465
- <Label htmlFor="caption-language">Language Code</Label>
466
- <TextInput
467
- id="caption-language"
468
- placeholder="en-US"
469
- value={languageCode}
470
- onChange={(e) => {
471
- setLanguageCode(e.currentTarget.value)
472
- if (selectedLanguage && selectedLanguage.value !== e.currentTarget.value) {
473
- setSelectedLanguage(null)
474
- if (!name || name === selectedLanguage.label) {
475
- setName('')
476
- }
477
- }
478
- }}
479
- disabled={isSubmitting}
480
- />
481
- </Stack>
482
-
483
- <Flex gap={2} justify="flex-end" marginTop={2}>
484
- <Button text="Cancel" mode="ghost" onClick={onClose} disabled={isSubmitting} />
485
- <Button
486
- text="Update Caption Track"
487
- tone="primary"
488
- icon={
489
- isSubmitting ? (
490
- <Spinner
491
- style={{
492
- verticalAlign: 'middle',
493
- display: 'inline-block',
494
- marginBottom: '-3px',
495
- width: '1em',
496
- height: '1em',
497
- marginRight: '-6px',
498
- }}
499
- />
500
- ) : (
501
- UploadIcon
502
- )
503
- }
504
- onClick={handleSubmit}
505
- disabled={isSubmitting}
506
- />
507
- </Flex>
508
- </Stack>
509
- </Dialog>
510
- )
511
- }
@@ -1,121 +0,0 @@
1
- import {Button, Dialog, Flex, Stack, Text, TextInput} from '@sanity/ui'
2
- import React, {useId, useMemo, useState} from 'react'
3
- import {getDevicePixelRatio} from 'use-device-pixel-ratio'
4
-
5
- import {useDialogStateContext} from '../context/DialogStateContext'
6
- import {useClient} from '../hooks/useClient'
7
- import {
8
- formatSecondsToHHMMSS,
9
- getSecondsFromTimeFormat,
10
- isValidTimeFormat,
11
- } from '../util/formatSeconds'
12
- import type {VideoAssetDocument} from '../util/types'
13
- import VideoThumbnail from './VideoThumbnail'
14
-
15
- export interface Props {
16
- asset: VideoAssetDocument
17
- currentTime?: number
18
- }
19
-
20
- export default function EditThumbnailDialog({asset, currentTime = 0}: Props) {
21
- const client = useClient()
22
-
23
- const {setDialogState} = useDialogStateContext()
24
- const dialogId = `EditThumbnailDialog${useId()}`
25
-
26
- const [timeFormatted, setTimeFormatted] = useState<string>(() =>
27
- formatSecondsToHHMMSS(currentTime),
28
- )
29
- const [nextTime, setNextTime] = useState<number>(currentTime)
30
- const [inputError, setInputError] = useState<string>('')
31
-
32
- const assetWithNewThumbnail = useMemo(() => ({...asset, thumbTime: nextTime}), [asset, nextTime])
33
- const [saving, setSaving] = useState(false)
34
- const [saveThumbnailError, setSaveThumbnailError] = useState<Error | null>(null)
35
- const handleSave = () => {
36
- setSaving(true)
37
- client
38
- .patch(asset._id)
39
- .set({thumbTime: nextTime})
40
- .commit({returnDocuments: false})
41
- .then(() => setDialogState(false))
42
- .catch(setSaveThumbnailError)
43
- .finally(() => setSaving(false))
44
- }
45
- const width = 300 * getDevicePixelRatio({maxDpr: 2})
46
-
47
- if (saveThumbnailError) {
48
- // @TODO handle errors more gracefully
49
- throw saveThumbnailError
50
- }
51
-
52
- const handleInputChange = (event: React.FormEvent<HTMLInputElement>) => {
53
- const value = event.currentTarget.value
54
- setTimeFormatted(value)
55
-
56
- if (isValidTimeFormat(value)) {
57
- setInputError('')
58
- const totalSeconds = getSecondsFromTimeFormat(value)
59
- setNextTime(totalSeconds)
60
- } else {
61
- setInputError('Invalid time format')
62
- }
63
- }
64
-
65
- return (
66
- <Dialog
67
- id={dialogId}
68
- header="Edit thumbnail"
69
- onClose={() => setDialogState(false)}
70
- footer={
71
- <Stack padding={3}>
72
- <Button
73
- key="thumbnail"
74
- disabled={inputError !== ''}
75
- mode="ghost"
76
- tone="primary"
77
- loading={saving}
78
- onClick={handleSave}
79
- text="Set new thumbnail"
80
- />
81
- </Stack>
82
- }
83
- >
84
- <Stack space={3} padding={3}>
85
- <Stack space={2}>
86
- <Text size={1} weight="semibold">
87
- Current:
88
- </Text>
89
- <VideoThumbnail asset={asset} width={width} staticImage />
90
- </Stack>
91
- <Stack space={2}>
92
- <Text size={1} weight="semibold">
93
- New:
94
- </Text>
95
- <VideoThumbnail asset={assetWithNewThumbnail} width={width} staticImage />
96
- </Stack>
97
-
98
- <Stack space={2}>
99
- <Flex align={'center'} justify={'center'}>
100
- <Text size={5} weight="semibold">
101
- Or
102
- </Text>
103
- </Flex>
104
- </Stack>
105
-
106
- <Stack space={2}>
107
- <Text size={1} weight="semibold">
108
- Selected time for thumbnail (hh:mm:ss):
109
- </Text>
110
- <TextInput
111
- size={1}
112
- value={timeFormatted}
113
- placeholder="hh:mm:ss"
114
- onChange={handleInputChange}
115
- customValidity={inputError}
116
- />
117
- </Stack>
118
- </Stack>
119
- </Dialog>
120
- )
121
- }