sanity-plugin-mux-input 3.0.4 → 4.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.
Files changed (130) hide show
  1. package/README.md +0 -2
  2. package/dist/index.d.ts +134 -193
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +2893 -4417
  5. package/dist/index.js.map +1 -1
  6. package/package.json +33 -43
  7. package/dist/index.cjs +0 -7270
  8. package/dist/index.cjs.map +0 -1
  9. package/dist/index.d.cts +0 -347
  10. package/sanity.json +0 -8
  11. package/src/_exports/index.ts +0 -73
  12. package/src/actions/assets.ts +0 -152
  13. package/src/actions/secrets.ts +0 -111
  14. package/src/actions/upload.ts +0 -310
  15. package/src/clients/upChunkObservable.ts +0 -54
  16. package/src/components/AddCaptionDialog.tsx +0 -440
  17. package/src/components/CaptionsDialog.tsx +0 -23
  18. package/src/components/ConfigureApi.styled.tsx +0 -19
  19. package/src/components/ConfigureApi.tsx +0 -296
  20. package/src/components/DraggableWatermark.tsx +0 -877
  21. package/src/components/EditCaptionDialog.tsx +0 -510
  22. package/src/components/EditThumbnailDialog.tsx +0 -122
  23. package/src/components/ErrorBoundaryCard.tsx +0 -96
  24. package/src/components/FileInputButton.tsx +0 -54
  25. package/src/components/FileInputMenuItem.styled.tsx +0 -36
  26. package/src/components/FileInputMenuItem.tsx +0 -85
  27. package/src/components/FormField.tsx +0 -38
  28. package/src/components/IconInfo.tsx +0 -22
  29. package/src/components/ImportVideosFromMux.tsx +0 -342
  30. package/src/components/Input.styled.tsx +0 -22
  31. package/src/components/Input.tsx +0 -78
  32. package/src/components/InputBrowser.tsx +0 -41
  33. package/src/components/InputError.tsx +0 -17
  34. package/src/components/MuxLogo.tsx +0 -42
  35. package/src/components/Onboard.tsx +0 -65
  36. package/src/components/PageSelector.tsx +0 -54
  37. package/src/components/Player.styled.tsx +0 -55
  38. package/src/components/Player.tsx +0 -117
  39. package/src/components/PlayerActionsMenu.tsx +0 -190
  40. package/src/components/ResyncMetadata.tsx +0 -280
  41. package/src/components/SelectAsset.tsx +0 -39
  42. package/src/components/SelectSortOptions.tsx +0 -45
  43. package/src/components/SpinnerBox.tsx +0 -16
  44. package/src/components/StudioTool.tsx +0 -24
  45. package/src/components/TextTracksEditor.tsx +0 -117
  46. package/src/components/TextTracksManager.tsx +0 -737
  47. package/src/components/UploadConfiguration.tsx +0 -694
  48. package/src/components/UploadPlaceholder.tsx +0 -88
  49. package/src/components/UploadProgress.tsx +0 -80
  50. package/src/components/Uploader.styled.tsx +0 -66
  51. package/src/components/Uploader.tsx +0 -498
  52. package/src/components/VideoDetails/DeleteDialog.tsx +0 -147
  53. package/src/components/VideoDetails/VideoDetails.tsx +0 -358
  54. package/src/components/VideoDetails/VideoReferences.tsx +0 -63
  55. package/src/components/VideoDetails/useVideoDetails.ts +0 -103
  56. package/src/components/VideoInBrowser.tsx +0 -245
  57. package/src/components/VideoMetadata.tsx +0 -45
  58. package/src/components/VideoPlayer.tsx +0 -235
  59. package/src/components/VideoThumbnail.tsx +0 -138
  60. package/src/components/VideosBrowser.tsx +0 -100
  61. package/src/components/documentPreview/DocumentPreview.tsx +0 -83
  62. package/src/components/documentPreview/DraftStatus.tsx +0 -34
  63. package/src/components/documentPreview/MissingSchemaType.tsx +0 -32
  64. package/src/components/documentPreview/PaneItemPreview.tsx +0 -74
  65. package/src/components/documentPreview/PublishedStatus.tsx +0 -35
  66. package/src/components/documentPreview/TimeAgo.tsx +0 -12
  67. package/src/components/documentPreview/paneItemTypes.ts +0 -7
  68. package/src/components/icons/Audio.tsx +0 -13
  69. package/src/components/icons/Resolution.tsx +0 -12
  70. package/src/components/icons/StopWatch.tsx +0 -20
  71. package/src/components/icons/ToolIcon.tsx +0 -19
  72. package/src/components/uploadConfiguration/PlaybackPolicy.tsx +0 -133
  73. package/src/components/uploadConfiguration/PlaybackPolicyOption.tsx +0 -76
  74. package/src/components/uploadConfiguration/PlaybackPolicyWarning.tsx +0 -29
  75. package/src/components/uploadConfiguration/ResolutionTierSelector.tsx +0 -72
  76. package/src/components/uploadConfiguration/StaticRenditionSelector.tsx +0 -180
  77. package/src/components/withFocusRing/helpers.ts +0 -24
  78. package/src/components/withFocusRing/index.ts +0 -1
  79. package/src/components/withFocusRing/withFocusRing.ts +0 -30
  80. package/src/context/DialogStateContext.tsx +0 -36
  81. package/src/context/DrmPlaybackWarningContext.tsx +0 -93
  82. package/src/hooks/useAccessControl.ts +0 -13
  83. package/src/hooks/useAssetDocumentValues.ts +0 -11
  84. package/src/hooks/useAssets.ts +0 -68
  85. package/src/hooks/useCancelUpload.ts +0 -22
  86. package/src/hooks/useClient.ts +0 -8
  87. package/src/hooks/useDialogState.ts +0 -11
  88. package/src/hooks/useDocReferences.ts +0 -21
  89. package/src/hooks/useFetchFileSize.ts +0 -54
  90. package/src/hooks/useImportMuxAssets.ts +0 -132
  91. package/src/hooks/useInView.ts +0 -42
  92. package/src/hooks/useMediaMetadata.ts +0 -103
  93. package/src/hooks/useMuxAssets.ts +0 -179
  94. package/src/hooks/useMuxPolling.ts +0 -49
  95. package/src/hooks/useResyncAsset.ts +0 -110
  96. package/src/hooks/useResyncMuxMetadata.ts +0 -176
  97. package/src/hooks/useSaveSecrets.ts +0 -78
  98. package/src/hooks/useSecretsDocumentValues.ts +0 -38
  99. package/src/hooks/useSecretsFormState.ts +0 -47
  100. package/src/plugin.tsx +0 -31
  101. package/src/sanity-ui.d.ts +0 -5
  102. package/src/schema.ts +0 -196
  103. package/src/util/addKeysToMuxData.ts +0 -30
  104. package/src/util/areSecretsSignable.ts +0 -5
  105. package/src/util/asserters.ts +0 -36
  106. package/src/util/assetTitlePlaceholder.ts +0 -31
  107. package/src/util/constants.ts +0 -15
  108. package/src/util/convertWatermarkToMux.ts +0 -160
  109. package/src/util/createSearchFilter.ts +0 -76
  110. package/src/util/createUrlParamsObject.ts +0 -29
  111. package/src/util/extractFiles.ts +0 -67
  112. package/src/util/formatBytes.ts +0 -32
  113. package/src/util/formatDriveShareLink.ts +0 -64
  114. package/src/util/formatSeconds.ts +0 -49
  115. package/src/util/generateJwt.ts +0 -57
  116. package/src/util/getAnimatedPosterSrc.ts +0 -26
  117. package/src/util/getPlaybackPolicy.ts +0 -69
  118. package/src/util/getPosterSrc.ts +0 -28
  119. package/src/util/getStoryboardSrc.ts +0 -27
  120. package/src/util/getVideoMetadata.ts +0 -23
  121. package/src/util/getVideoSrc.ts +0 -23
  122. package/src/util/isSigned.ts +0 -20
  123. package/src/util/parsers.ts +0 -5
  124. package/src/util/pluginVersion.ts +0 -1
  125. package/src/util/readSecrets.ts +0 -38
  126. package/src/util/roundPxString.ts +0 -16
  127. package/src/util/textTracks.ts +0 -222
  128. package/src/util/tryWithSuspend.ts +0 -22
  129. package/src/util/types.ts +0 -596
  130. package/v2-incompatible.js +0 -11
@@ -1,737 +0,0 @@
1
- import {
2
- AddIcon,
3
- ChevronDownIcon,
4
- ChevronUpIcon,
5
- DownloadIcon,
6
- EditIcon,
7
- ErrorOutlineIcon,
8
- TrashIcon,
9
- } from '@sanity/icons'
10
- import {Box, Button, Card, Dialog, Flex, Heading, Spinner, Stack, Text, useToast} from '@sanity/ui'
11
- import {useEffect, useId, useMemo, useState} from 'react'
12
-
13
- import {deleteTextTrack} from '../actions/assets'
14
- import {useClient} from '../hooks/useClient'
15
- import {useResyncAsset} from '../hooks/useResyncAsset'
16
- import {downloadVttFile} from '../util/textTracks'
17
- import type {MuxTextTrack, VideoAssetDocument} from '../util/types'
18
- import AddCaptionDialog from './AddCaptionDialog'
19
- import EditCaptionDialog from './EditCaptionDialog'
20
-
21
- interface TrackCardProps {
22
- track: MuxTextTrack
23
- iconOnly: boolean
24
- downloadingTrackId: string | null
25
- deletingTrackId: string | null
26
- trackToEdit: MuxTextTrack | null
27
- getTrackSourceLabel: (track: MuxTextTrack) => string
28
- handleDownload: (track: MuxTextTrack) => void
29
- setTrackToEdit: (track: MuxTextTrack) => void
30
- setTrackToDelete: (track: MuxTextTrack) => void
31
- }
32
-
33
- function TrackCard({
34
- track,
35
- iconOnly,
36
- downloadingTrackId,
37
- deletingTrackId,
38
- trackToEdit,
39
- getTrackSourceLabel,
40
- handleDownload,
41
- setTrackToEdit,
42
- setTrackToDelete,
43
- }: TrackCardProps) {
44
- const isDisabled = (action: 'download' | 'edit' | 'delete') => {
45
- if (action === 'download') {
46
- return (
47
- downloadingTrackId !== null || deletingTrackId === track.id || trackToEdit?.id === track.id
48
- )
49
- }
50
- if (action === 'edit') {
51
- return (
52
- downloadingTrackId === track.id ||
53
- deletingTrackId === track.id ||
54
- trackToEdit?.id === track.id
55
- )
56
- }
57
- return (
58
- downloadingTrackId === track.id || deletingTrackId !== null || trackToEdit?.id === track.id
59
- )
60
- }
61
-
62
- const renderActionButtons = () => {
63
- if (track.status === 'preparing') {
64
- return (
65
- <Flex align="center" gap={2}>
66
- <Spinner
67
- muted
68
- style={{
69
- width: '0.75em',
70
- height: '0.75em',
71
- verticalAlign: 'middle',
72
- display: 'inline-block',
73
- marginBottom: '-2px',
74
- }}
75
- />
76
- <Text size={1} muted>
77
- Processing...
78
- </Text>
79
- </Flex>
80
- )
81
- }
82
-
83
- return (
84
- <Flex gap={2}>
85
- {track.status !== 'errored' && (
86
- <Button
87
- icon={
88
- downloadingTrackId === track.id ? (
89
- <Spinner
90
- style={{
91
- verticalAlign: 'middle',
92
- display: 'inline-block',
93
- marginTop: '-2px',
94
- width: '0.5em',
95
- height: '0.5em',
96
- }}
97
- />
98
- ) : (
99
- <DownloadIcon />
100
- )
101
- }
102
- text={iconOnly ? undefined : 'Download'}
103
- mode="ghost"
104
- tone="primary"
105
- fontSize={1}
106
- padding={2}
107
- onClick={() => handleDownload(track)}
108
- disabled={isDisabled('download')}
109
- title="Download"
110
- />
111
- )}
112
- <Button
113
- icon={<EditIcon />}
114
- text={iconOnly ? undefined : 'Edit'}
115
- mode="ghost"
116
- tone="primary"
117
- fontSize={1}
118
- padding={2}
119
- disabled={isDisabled('edit')}
120
- onClick={() => setTrackToEdit(track)}
121
- title="Edit"
122
- />
123
- <Button
124
- icon={
125
- deletingTrackId === track.id ? (
126
- <Spinner
127
- style={{
128
- verticalAlign: 'middle',
129
- display: 'inline-block',
130
- marginTop: '-2px',
131
- width: '0.5em',
132
- height: '0.5em',
133
- }}
134
- />
135
- ) : (
136
- <TrashIcon />
137
- )
138
- }
139
- text={iconOnly ? undefined : 'Delete'}
140
- mode="ghost"
141
- tone="critical"
142
- fontSize={1}
143
- padding={2}
144
- disabled={isDisabled('delete')}
145
- onClick={() => setTrackToDelete(track)}
146
- title="Delete"
147
- />
148
- </Flex>
149
- )
150
- }
151
-
152
- return (
153
- <Card
154
- padding={3}
155
- radius={2}
156
- tone={track.status === 'errored' ? 'caution' : 'transparent'}
157
- border
158
- >
159
- <Flex align="center" justify="space-between" gap={3}>
160
- <Stack space={2} flex={1}>
161
- <Flex align="center" gap={2}>
162
- <Text weight="semibold">{track.name || 'Untitled'}</Text>
163
- <Text size={1} muted>
164
- ({getTrackSourceLabel(track)})
165
- </Text>
166
- {track.status === 'errored' && (
167
- <ErrorOutlineIcon
168
- style={{color: 'var(--card-critical-color)'}}
169
- aria-label="Error"
170
- fontSize={20}
171
- />
172
- )}
173
- </Flex>
174
- {track.language_code && (
175
- <Text size={1} muted>
176
- Language: {track.language_code}
177
- </Text>
178
- )}
179
- {track.status === 'errored' && track.error && (
180
- <Text size={1} style={{color: 'var(--card-critical-color)'}}>
181
- {track.error.messages?.[0] || track.error.type || 'Failed to process track'}
182
- </Text>
183
- )}
184
- </Stack>
185
- {renderActionButtons()}
186
- </Flex>
187
- </Card>
188
- )
189
- }
190
-
191
- interface TextTracksManagerProps {
192
- asset: VideoAssetDocument
193
- iconOnly?: boolean
194
- tracks?: MuxTextTrack[]
195
- collapseTracks?: boolean
196
- }
197
-
198
- export default function TextTracksManager({
199
- asset,
200
- iconOnly = false,
201
- tracks: propTracks,
202
- collapseTracks = false,
203
- }: TextTracksManagerProps) {
204
- const client = useClient()
205
- const toast = useToast()
206
- const dialogId = `DeleteCaptionDialog${useId()}`
207
- const {resyncAsset} = useResyncAsset()
208
- const [downloadingTrackId, setDownloadingTrackId] = useState<string | null>(null)
209
- const [deletingTrackId, setDeletingTrackId] = useState<string | null>(null)
210
- const [addedTracks, setAddedTracks] = useState<MuxTextTrack[]>([])
211
- const [updatedTracks, setUpdatedTracks] = useState<Map<string, MuxTextTrack>>(new Map())
212
- const [trackActivityOrder, setTrackActivityOrder] = useState<Map<string, number>>(new Map())
213
- const [autogeneratedTrackIds, setAutogeneratedTrackIds] = useState<Set<string>>(new Set())
214
- const [trackToDelete, setTrackToDelete] = useState<MuxTextTrack | null>(null)
215
- const [trackToEdit, setTrackToEdit] = useState<MuxTextTrack | null>(null)
216
- const [showAddDialog, setShowAddDialog] = useState(false)
217
- const [isExpanded, setIsExpanded] = useState(false)
218
-
219
- const MAX_VISIBLE_TRACKS = 4
220
-
221
- const realTracks: MuxTextTrack[] = propTracks
222
- ? propTracks
223
- : asset.data?.tracks?.filter((track): track is MuxTextTrack => track.type === 'text') || []
224
-
225
- const activeTracks = realTracks.filter(
226
- (track) =>
227
- track.id &&
228
- (track.status === 'ready' || track.status === 'preparing' || track.status === 'errored')
229
- )
230
-
231
- const allTracks = useMemo(() => {
232
- const tracksWithUpdates = activeTracks.map((track) => {
233
- const updated = updatedTracks.get(track.id)
234
- return updated || track
235
- })
236
-
237
- const isMockTrackReplaced = (mockTrack: MuxTextTrack, realTracksList: MuxTextTrack[]) => {
238
- if (!mockTrack.id || !mockTrack.id.startsWith('generating-')) {
239
- return false
240
- }
241
- return realTracksList.some((realTrack) => {
242
- const nameMatches = realTrack.name === mockTrack.name
243
- const languageMatches = realTrack.language_code === mockTrack.language_code
244
- if (!nameMatches || !languageMatches) {
245
- return false
246
- }
247
- if (realTrack.status === 'ready') {
248
- const isGenerated =
249
- realTrack.text_source === 'generated_live' ||
250
- realTrack.text_source === 'generated_live_final' ||
251
- realTrack.text_source === 'generated_vod'
252
- return isGenerated
253
- }
254
- if (realTrack.status === 'preparing') {
255
- return true
256
- }
257
- return false
258
- })
259
- }
260
-
261
- const isTrackAlreadyInRealTracks = (
262
- addedTrack: MuxTextTrack,
263
- realTracksList: MuxTextTrack[]
264
- ) => {
265
- if (!addedTrack.id) return false
266
- if (addedTrack.id.startsWith('generating-')) {
267
- return isMockTrackReplaced(addedTrack, realTracksList)
268
- }
269
- return realTracksList.some((realTrack) => realTrack.id === addedTrack.id)
270
- }
271
-
272
- const tracksToKeep = addedTracks.filter((addedTrack) => {
273
- if (addedTrack.id && addedTrack.id.startsWith('generating-')) {
274
- return !isMockTrackReplaced(addedTrack, tracksWithUpdates)
275
- }
276
- return !isTrackAlreadyInRealTracks(addedTrack, tracksWithUpdates)
277
- })
278
-
279
- return [...tracksWithUpdates, ...tracksToKeep]
280
- }, [activeTracks, addedTracks, updatedTracks])
281
-
282
- useEffect(() => {
283
- const newAutogeneratedIds = new Set<string>()
284
-
285
- activeTracks.forEach((track) => {
286
- if (
287
- track.id &&
288
- (track.text_source === 'generated_live' ||
289
- track.text_source === 'generated_live_final' ||
290
- track.text_source === 'generated_vod')
291
- ) {
292
- newAutogeneratedIds.add(track.id)
293
- }
294
- })
295
-
296
- addedTracks.forEach((mockTrack) => {
297
- if (mockTrack.id && mockTrack.id.startsWith('generating-')) {
298
- const realTrack = activeTracks.find((rt) => {
299
- const nameMatches = rt.name === mockTrack.name
300
- const languageMatches = rt.language_code === mockTrack.language_code
301
- return nameMatches && languageMatches
302
- })
303
- if (realTrack?.id) {
304
- newAutogeneratedIds.add(realTrack.id)
305
- }
306
- }
307
- })
308
-
309
- setAutogeneratedTrackIds((prev) => {
310
- let hasNew = false
311
- const updated = new Set(prev)
312
- newAutogeneratedIds.forEach((id) => {
313
- if (!prev.has(id)) {
314
- updated.add(id)
315
- hasNew = true
316
- }
317
- })
318
- return hasNew ? updated : prev
319
- })
320
- }, [activeTracks, addedTracks])
321
-
322
- useEffect(() => {
323
- const preparingTracks = allTracks.filter((track) => track.status === 'preparing')
324
- if (preparingTracks.length === 0 || !asset.assetId || !asset._id) {
325
- return undefined
326
- }
327
-
328
- const interval = setInterval(async () => {
329
- try {
330
- const muxData = await resyncAsset(asset)
331
- if (!muxData) return
332
-
333
- const fetchedTracks =
334
- muxData.tracks?.filter((track): track is MuxTextTrack => track.type === 'text') || []
335
-
336
- const isMockTrackReplaced = (
337
- mockTrack: MuxTextTrack,
338
- fetchedTracksList: MuxTextTrack[]
339
- ) => {
340
- if (!mockTrack.id || !mockTrack.id.startsWith('generating-')) {
341
- return false
342
- }
343
- return fetchedTracksList.some((realTrack) => {
344
- const nameMatches = realTrack.name === mockTrack.name
345
- const languageMatches = realTrack.language_code === mockTrack.language_code
346
- if (!nameMatches || !languageMatches) {
347
- return false
348
- }
349
- if (realTrack.status === 'ready') {
350
- const isGenerated =
351
- realTrack.text_source === 'generated_live' ||
352
- realTrack.text_source === 'generated_live_final' ||
353
- realTrack.text_source === 'generated_vod'
354
- return isGenerated
355
- }
356
- if (realTrack.status === 'preparing') {
357
- return true
358
- }
359
- return false
360
- })
361
- }
362
-
363
- const newAutogeneratedIds = new Set<string>()
364
- fetchedTracks.forEach((track) => {
365
- if (
366
- track.id &&
367
- (track.text_source === 'generated_live' ||
368
- track.text_source === 'generated_live_final' ||
369
- track.text_source === 'generated_vod')
370
- ) {
371
- newAutogeneratedIds.add(track.id)
372
- }
373
- })
374
-
375
- const findMatchingRealTrack = (mockTrack: MuxTextTrack, tracksList: MuxTextTrack[]) => {
376
- return tracksList.find((rt) => {
377
- const nameMatches = rt.name === mockTrack.name
378
- const languageMatches = rt.language_code === mockTrack.language_code
379
- return nameMatches && languageMatches
380
- })
381
- }
382
-
383
- setAddedTracks((prev) => {
384
- return prev.filter((mockTrack) => {
385
- if (mockTrack.id && mockTrack.id.startsWith('generating-')) {
386
- const replaced = isMockTrackReplaced(mockTrack, fetchedTracks)
387
- if (replaced) {
388
- const realTrack = findMatchingRealTrack(mockTrack, fetchedTracks)
389
- if (realTrack?.id) {
390
- newAutogeneratedIds.add(realTrack.id)
391
- setTrackActivityOrder((prevOrder) => {
392
- const mockOrder = prevOrder.get(mockTrack.id)
393
- if (mockOrder) {
394
- const newMap = new Map(prevOrder)
395
- newMap.set(realTrack.id, mockOrder)
396
- return newMap
397
- }
398
- return prevOrder
399
- })
400
- }
401
- }
402
- return !replaced
403
- }
404
- return true
405
- })
406
- })
407
-
408
- if (newAutogeneratedIds.size > 0) {
409
- setAutogeneratedTrackIds((prevIds) => {
410
- const updated = new Set(prevIds)
411
- newAutogeneratedIds.forEach((id) => updated.add(id))
412
- return updated
413
- })
414
- }
415
- } catch (error) {
416
- console.error('Failed to refresh asset data:', error)
417
- }
418
- }, 3000) // Poll every 3 seconds
419
-
420
- return () => clearInterval(interval)
421
- }, [allTracks, asset, resyncAsset])
422
-
423
- const visibleTracks = allTracks
424
- .filter(
425
- (track) =>
426
- track.status === 'ready' || track.status === 'preparing' || track.status === 'errored'
427
- )
428
- .sort((a, b) => {
429
- const orderA = trackActivityOrder.get(a.id) || 0
430
- const orderB = trackActivityOrder.get(b.id) || 0
431
-
432
- if (orderA > 0 && orderB > 0) {
433
- return orderB - orderA
434
- }
435
-
436
- if (orderA > 0) return -1
437
- if (orderB > 0) return 1
438
-
439
- const aIsPreparing = a.status === 'preparing'
440
- const bIsPreparing = b.status === 'preparing'
441
- if (aIsPreparing && !bIsPreparing) return -1
442
- if (!aIsPreparing && bIsPreparing) return 1
443
-
444
- const aIsAutogenerated =
445
- (a.id && a.id.startsWith('generating-')) || (a.id && autogeneratedTrackIds.has(a.id))
446
- const bIsAutogenerated =
447
- (b.id && b.id.startsWith('generating-')) || (b.id && autogeneratedTrackIds.has(b.id))
448
- if (aIsAutogenerated && !bIsAutogenerated) return -1
449
- if (!aIsAutogenerated && bIsAutogenerated) return 1
450
-
451
- return 0
452
- })
453
-
454
- const handleDownload = async (track: MuxTextTrack) => {
455
- if (!track.id) return
456
-
457
- setDownloadingTrackId(track.id)
458
- try {
459
- await downloadVttFile(client, asset, track)
460
- } catch (error) {
461
- toast.push({
462
- title: 'Failed to download VTT file',
463
- status: 'error',
464
- description: error instanceof Error ? error.message : 'Please try again',
465
- })
466
- } finally {
467
- setDownloadingTrackId(null)
468
- }
469
- }
470
-
471
- const confirmDelete = async () => {
472
- if (!trackToDelete || !trackToDelete.id) return
473
-
474
- const track = trackToDelete
475
- setTrackToDelete(null)
476
- setDeletingTrackId(track.id)
477
- try {
478
- if (!asset.assetId) {
479
- throw new Error('Asset ID is required')
480
- }
481
- await deleteTextTrack(client, asset.assetId, track.id)
482
-
483
- // Refresh asset data after deletion
484
- await resyncAsset(asset)
485
-
486
- toast.push({
487
- title: 'Successfully deleted caption track',
488
- status: 'success',
489
- })
490
-
491
- setAddedTracks((prev) => prev.filter((t) => t.id !== track.id))
492
- setUpdatedTracks((prev) => {
493
- const newMap = new Map(prev)
494
- newMap.delete(track.id)
495
- return newMap
496
- })
497
- setTrackActivityOrder((prev) => {
498
- const newMap = new Map(prev)
499
- newMap.delete(track.id)
500
- return newMap
501
- })
502
- setAutogeneratedTrackIds((prev) => {
503
- const updated = new Set(prev)
504
- updated.delete(track.id)
505
- return updated
506
- })
507
- } catch (error) {
508
- toast.push({
509
- title: 'Failed to delete caption track',
510
- status: 'error',
511
- description: error instanceof Error ? error.message : 'Please try again',
512
- })
513
- } finally {
514
- setDeletingTrackId(null)
515
- }
516
- }
517
-
518
- const handleAddTrack = (track: MuxTextTrack) => {
519
- setAddedTracks((prev) => [...prev, track])
520
- setTrackActivityOrder((prev) => {
521
- const newMap = new Map(prev)
522
- newMap.set(track.id, prev.size + 1)
523
- return newMap
524
- })
525
- setShowAddDialog(false)
526
- }
527
-
528
- const handleUpdateTrack = async (updatedTrack: MuxTextTrack, oldTrackId?: string) => {
529
- if (oldTrackId) {
530
- setAddedTracks((prev) => prev.filter((t) => t.id !== oldTrackId))
531
- setUpdatedTracks((prev) => {
532
- const newMap = new Map(prev)
533
- newMap.delete(oldTrackId)
534
- return newMap
535
- })
536
- setTrackActivityOrder((prev) => {
537
- const newMap = new Map(prev)
538
- newMap.delete(oldTrackId)
539
- return newMap
540
- })
541
- setAutogeneratedTrackIds((prev) => {
542
- const updated = new Set(prev)
543
- updated.delete(oldTrackId)
544
- return updated
545
- })
546
- }
547
-
548
- const isAddedTrack = addedTracks.some((t) => t.id === updatedTrack.id)
549
-
550
- if (isAddedTrack) {
551
- setAddedTracks((prev) => prev.map((t) => (t.id === updatedTrack.id ? updatedTrack : t)))
552
- } else {
553
- setUpdatedTracks((prev) => {
554
- const newMap = new Map(prev)
555
- newMap.set(updatedTrack.id, updatedTrack)
556
- return newMap
557
- })
558
- }
559
-
560
- setTrackActivityOrder((prev) => {
561
- const newMap = new Map(prev)
562
- newMap.set(updatedTrack.id, prev.size + 1)
563
- return newMap
564
- })
565
-
566
- setTrackToEdit(null)
567
-
568
- // Refresh asset data after update
569
- await resyncAsset(asset)
570
- }
571
-
572
- const getTrackSourceLabel = (track: MuxTextTrack) => {
573
- if (track.id && track.id.startsWith('generating-')) {
574
- return 'Auto-generated'
575
- }
576
- if (track.id && autogeneratedTrackIds.has(track.id)) {
577
- return 'Auto-generated'
578
- }
579
- if (
580
- track.text_source === 'generated_live_final' ||
581
- track.text_source === 'generated_live' ||
582
- track.text_source === 'generated_vod'
583
- ) {
584
- return 'Auto-generated'
585
- }
586
- if (track.text_source === 'uploaded') {
587
- return 'Uploaded'
588
- }
589
- return 'Custom'
590
- }
591
-
592
- if (visibleTracks.length === 0 && !showAddDialog) {
593
- return (
594
- <Stack space={3}>
595
- <Flex justify="flex-end">
596
- <Button
597
- icon={AddIcon}
598
- text="Add Caption"
599
- tone="primary"
600
- onClick={() => setShowAddDialog(true)}
601
- />
602
- </Flex>
603
- <Card padding={4} radius={2} tone="transparent" border>
604
- <Text size={1} muted>
605
- No captions available. Add captions when uploading a video or add them manually.
606
- </Text>
607
- </Card>
608
- {showAddDialog && (
609
- <AddCaptionDialog
610
- asset={asset}
611
- onAdd={handleAddTrack}
612
- onClose={() => setShowAddDialog(false)}
613
- />
614
- )}
615
- </Stack>
616
- )
617
- }
618
-
619
- const displayedTracks =
620
- collapseTracks && !isExpanded ? visibleTracks.slice(0, MAX_VISIBLE_TRACKS) : visibleTracks
621
- const hasMoreTracks = collapseTracks && visibleTracks.length > MAX_VISIBLE_TRACKS
622
-
623
- return (
624
- <Stack space={3}>
625
- <Flex justify="flex-end">
626
- <Button
627
- icon={AddIcon}
628
- text="Add Caption"
629
- tone="primary"
630
- onClick={() => setShowAddDialog(true)}
631
- />
632
- </Flex>
633
-
634
- {displayedTracks.map((track) => (
635
- <TrackCard
636
- key={track.id}
637
- track={track}
638
- iconOnly={iconOnly}
639
- downloadingTrackId={downloadingTrackId}
640
- deletingTrackId={deletingTrackId}
641
- trackToEdit={trackToEdit}
642
- getTrackSourceLabel={getTrackSourceLabel}
643
- handleDownload={handleDownload}
644
- setTrackToEdit={setTrackToEdit}
645
- setTrackToDelete={setTrackToDelete}
646
- />
647
- ))}
648
-
649
- {hasMoreTracks && (
650
- <Flex justify="center">
651
- <Button
652
- icon={isExpanded ? ChevronUpIcon : ChevronDownIcon}
653
- text={
654
- isExpanded ? 'Show less' : `Show ${visibleTracks.length - MAX_VISIBLE_TRACKS} more`
655
- }
656
- mode="ghost"
657
- tone="primary"
658
- onClick={() => setIsExpanded(!isExpanded)}
659
- />
660
- </Flex>
661
- )}
662
-
663
- {trackToDelete && (
664
- <Dialog
665
- animate
666
- id={dialogId}
667
- header="Delete track"
668
- onClose={() => setTrackToDelete(null)}
669
- onClickOutside={() => setTrackToDelete(null)}
670
- width={1}
671
- >
672
- <Card
673
- padding={3}
674
- style={{
675
- minHeight: '150px',
676
- display: 'flex',
677
- alignItems: 'center',
678
- justifyContent: 'center',
679
- }}
680
- >
681
- <Stack space={3}>
682
- <Heading size={2}>
683
- Are you sure you want to delete &quot;
684
- {trackToDelete.name || trackToDelete.language_code || 'Untitled'}&quot;?
685
- </Heading>
686
- <Text size={2}>This action is irreversible</Text>
687
- <Stack space={4} marginY={4}>
688
- <Box>
689
- <Button
690
- icon={
691
- deletingTrackId === trackToDelete.id ? (
692
- <Spinner
693
- style={{
694
- verticalAlign: 'middle',
695
- display: 'inline-block',
696
- marginTop: '-2px',
697
- width: '0.5em',
698
- height: '0.5em',
699
- }}
700
- />
701
- ) : (
702
- <TrashIcon />
703
- )
704
- }
705
- fontSize={2}
706
- padding={3}
707
- text="Delete track"
708
- tone="critical"
709
- onClick={confirmDelete}
710
- disabled={deletingTrackId !== null}
711
- />
712
- </Box>
713
- </Stack>
714
- </Stack>
715
- </Card>
716
- </Dialog>
717
- )}
718
-
719
- {showAddDialog && (
720
- <AddCaptionDialog
721
- asset={asset}
722
- onAdd={handleAddTrack}
723
- onClose={() => setShowAddDialog(false)}
724
- />
725
- )}
726
-
727
- {trackToEdit && (
728
- <EditCaptionDialog
729
- asset={asset}
730
- track={trackToEdit}
731
- onUpdate={handleUpdateTrack}
732
- onClose={() => setTrackToEdit(null)}
733
- />
734
- )}
735
- </Stack>
736
- )
737
- }