sanity-plugin-mux-input 3.0.5 → 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 (123) hide show
  1. package/dist/index.js +28 -28
  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,738 +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
- // oxlint-disable-next-line react/react-compiler
310
- setAutogeneratedTrackIds((prev) => {
311
- let hasNew = false
312
- const updated = new Set(prev)
313
- newAutogeneratedIds.forEach((id) => {
314
- if (!prev.has(id)) {
315
- updated.add(id)
316
- hasNew = true
317
- }
318
- })
319
- return hasNew ? updated : prev
320
- })
321
- }, [activeTracks, addedTracks])
322
-
323
- useEffect(() => {
324
- const preparingTracks = allTracks.filter((track) => track.status === 'preparing')
325
- if (preparingTracks.length === 0 || !asset.assetId || !asset._id) {
326
- return undefined
327
- }
328
-
329
- const interval = setInterval(async () => {
330
- try {
331
- const muxData = await resyncAsset(asset)
332
- if (!muxData) return
333
-
334
- const fetchedTracks =
335
- muxData.tracks?.filter((track): track is MuxTextTrack => track.type === 'text') || []
336
-
337
- const isMockTrackReplaced = (
338
- mockTrack: MuxTextTrack,
339
- fetchedTracksList: MuxTextTrack[],
340
- ) => {
341
- if (!mockTrack.id || !mockTrack.id.startsWith('generating-')) {
342
- return false
343
- }
344
- return fetchedTracksList.some((realTrack) => {
345
- const nameMatches = realTrack.name === mockTrack.name
346
- const languageMatches = realTrack.language_code === mockTrack.language_code
347
- if (!nameMatches || !languageMatches) {
348
- return false
349
- }
350
- if (realTrack.status === 'ready') {
351
- const isGenerated =
352
- realTrack.text_source === 'generated_live' ||
353
- realTrack.text_source === 'generated_live_final' ||
354
- realTrack.text_source === 'generated_vod'
355
- return isGenerated
356
- }
357
- if (realTrack.status === 'preparing') {
358
- return true
359
- }
360
- return false
361
- })
362
- }
363
-
364
- const newAutogeneratedIds = new Set<string>()
365
- fetchedTracks.forEach((track) => {
366
- if (
367
- track.id &&
368
- (track.text_source === 'generated_live' ||
369
- track.text_source === 'generated_live_final' ||
370
- track.text_source === 'generated_vod')
371
- ) {
372
- newAutogeneratedIds.add(track.id)
373
- }
374
- })
375
-
376
- const findMatchingRealTrack = (mockTrack: MuxTextTrack, tracksList: MuxTextTrack[]) => {
377
- return tracksList.find((rt) => {
378
- const nameMatches = rt.name === mockTrack.name
379
- const languageMatches = rt.language_code === mockTrack.language_code
380
- return nameMatches && languageMatches
381
- })
382
- }
383
-
384
- setAddedTracks((prev) => {
385
- return prev.filter((mockTrack) => {
386
- if (mockTrack.id && mockTrack.id.startsWith('generating-')) {
387
- const replaced = isMockTrackReplaced(mockTrack, fetchedTracks)
388
- if (replaced) {
389
- const realTrack = findMatchingRealTrack(mockTrack, fetchedTracks)
390
- if (realTrack?.id) {
391
- newAutogeneratedIds.add(realTrack.id)
392
- setTrackActivityOrder((prevOrder) => {
393
- const mockOrder = prevOrder.get(mockTrack.id)
394
- if (mockOrder) {
395
- const newMap = new Map(prevOrder)
396
- newMap.set(realTrack.id, mockOrder)
397
- return newMap
398
- }
399
- return prevOrder
400
- })
401
- }
402
- }
403
- return !replaced
404
- }
405
- return true
406
- })
407
- })
408
-
409
- if (newAutogeneratedIds.size > 0) {
410
- setAutogeneratedTrackIds((prevIds) => {
411
- const updated = new Set(prevIds)
412
- newAutogeneratedIds.forEach((id) => updated.add(id))
413
- return updated
414
- })
415
- }
416
- } catch (error) {
417
- console.error('Failed to refresh asset data:', error)
418
- }
419
- }, 3000) // Poll every 3 seconds
420
-
421
- return () => clearInterval(interval)
422
- }, [allTracks, asset, resyncAsset])
423
-
424
- const visibleTracks = allTracks
425
- .filter(
426
- (track) =>
427
- track.status === 'ready' || track.status === 'preparing' || track.status === 'errored',
428
- )
429
- .sort((a, b) => {
430
- const orderA = trackActivityOrder.get(a.id) || 0
431
- const orderB = trackActivityOrder.get(b.id) || 0
432
-
433
- if (orderA > 0 && orderB > 0) {
434
- return orderB - orderA
435
- }
436
-
437
- if (orderA > 0) return -1
438
- if (orderB > 0) return 1
439
-
440
- const aIsPreparing = a.status === 'preparing'
441
- const bIsPreparing = b.status === 'preparing'
442
- if (aIsPreparing && !bIsPreparing) return -1
443
- if (!aIsPreparing && bIsPreparing) return 1
444
-
445
- const aIsAutogenerated =
446
- (a.id && a.id.startsWith('generating-')) || (a.id && autogeneratedTrackIds.has(a.id))
447
- const bIsAutogenerated =
448
- (b.id && b.id.startsWith('generating-')) || (b.id && autogeneratedTrackIds.has(b.id))
449
- if (aIsAutogenerated && !bIsAutogenerated) return -1
450
- if (!aIsAutogenerated && bIsAutogenerated) return 1
451
-
452
- return 0
453
- })
454
-
455
- const handleDownload = async (track: MuxTextTrack) => {
456
- if (!track.id) return
457
-
458
- setDownloadingTrackId(track.id)
459
- try {
460
- await downloadVttFile(client, asset, track)
461
- } catch (error) {
462
- toast.push({
463
- title: 'Failed to download VTT file',
464
- status: 'error',
465
- description: error instanceof Error ? error.message : 'Please try again',
466
- })
467
- } finally {
468
- setDownloadingTrackId(null)
469
- }
470
- }
471
-
472
- const confirmDelete = async () => {
473
- if (!trackToDelete || !trackToDelete.id) return
474
-
475
- const track = trackToDelete
476
- setTrackToDelete(null)
477
- setDeletingTrackId(track.id)
478
- try {
479
- if (!asset.assetId) {
480
- throw new Error('Asset ID is required')
481
- }
482
- await deleteTextTrack(client, asset.assetId, track.id)
483
-
484
- // Refresh asset data after deletion
485
- await resyncAsset(asset)
486
-
487
- toast.push({
488
- title: 'Successfully deleted caption track',
489
- status: 'success',
490
- })
491
-
492
- setAddedTracks((prev) => prev.filter((t) => t.id !== track.id))
493
- setUpdatedTracks((prev) => {
494
- const newMap = new Map(prev)
495
- newMap.delete(track.id)
496
- return newMap
497
- })
498
- setTrackActivityOrder((prev) => {
499
- const newMap = new Map(prev)
500
- newMap.delete(track.id)
501
- return newMap
502
- })
503
- setAutogeneratedTrackIds((prev) => {
504
- const updated = new Set(prev)
505
- updated.delete(track.id)
506
- return updated
507
- })
508
- } catch (error) {
509
- toast.push({
510
- title: 'Failed to delete caption track',
511
- status: 'error',
512
- description: error instanceof Error ? error.message : 'Please try again',
513
- })
514
- } finally {
515
- setDeletingTrackId(null)
516
- }
517
- }
518
-
519
- const handleAddTrack = (track: MuxTextTrack) => {
520
- setAddedTracks((prev) => [...prev, track])
521
- setTrackActivityOrder((prev) => {
522
- const newMap = new Map(prev)
523
- newMap.set(track.id, prev.size + 1)
524
- return newMap
525
- })
526
- setShowAddDialog(false)
527
- }
528
-
529
- const handleUpdateTrack = async (updatedTrack: MuxTextTrack, oldTrackId?: string) => {
530
- if (oldTrackId) {
531
- setAddedTracks((prev) => prev.filter((t) => t.id !== oldTrackId))
532
- setUpdatedTracks((prev) => {
533
- const newMap = new Map(prev)
534
- newMap.delete(oldTrackId)
535
- return newMap
536
- })
537
- setTrackActivityOrder((prev) => {
538
- const newMap = new Map(prev)
539
- newMap.delete(oldTrackId)
540
- return newMap
541
- })
542
- setAutogeneratedTrackIds((prev) => {
543
- const updated = new Set(prev)
544
- updated.delete(oldTrackId)
545
- return updated
546
- })
547
- }
548
-
549
- const isAddedTrack = addedTracks.some((t) => t.id === updatedTrack.id)
550
-
551
- if (isAddedTrack) {
552
- setAddedTracks((prev) => prev.map((t) => (t.id === updatedTrack.id ? updatedTrack : t)))
553
- } else {
554
- setUpdatedTracks((prev) => {
555
- const newMap = new Map(prev)
556
- newMap.set(updatedTrack.id, updatedTrack)
557
- return newMap
558
- })
559
- }
560
-
561
- setTrackActivityOrder((prev) => {
562
- const newMap = new Map(prev)
563
- newMap.set(updatedTrack.id, prev.size + 1)
564
- return newMap
565
- })
566
-
567
- setTrackToEdit(null)
568
-
569
- // Refresh asset data after update
570
- await resyncAsset(asset)
571
- }
572
-
573
- const getTrackSourceLabel = (track: MuxTextTrack) => {
574
- if (track.id && track.id.startsWith('generating-')) {
575
- return 'Auto-generated'
576
- }
577
- if (track.id && autogeneratedTrackIds.has(track.id)) {
578
- return 'Auto-generated'
579
- }
580
- if (
581
- track.text_source === 'generated_live_final' ||
582
- track.text_source === 'generated_live' ||
583
- track.text_source === 'generated_vod'
584
- ) {
585
- return 'Auto-generated'
586
- }
587
- if (track.text_source === 'uploaded') {
588
- return 'Uploaded'
589
- }
590
- return 'Custom'
591
- }
592
-
593
- if (visibleTracks.length === 0 && !showAddDialog) {
594
- return (
595
- <Stack space={3}>
596
- <Flex justify="flex-end">
597
- <Button
598
- icon={AddIcon}
599
- text="Add Caption"
600
- tone="primary"
601
- onClick={() => setShowAddDialog(true)}
602
- />
603
- </Flex>
604
- <Card padding={4} radius={2} tone="transparent" border>
605
- <Text size={1} muted>
606
- No captions available. Add captions when uploading a video or add them manually.
607
- </Text>
608
- </Card>
609
- {showAddDialog && (
610
- <AddCaptionDialog
611
- asset={asset}
612
- onAdd={handleAddTrack}
613
- onClose={() => setShowAddDialog(false)}
614
- />
615
- )}
616
- </Stack>
617
- )
618
- }
619
-
620
- const displayedTracks =
621
- collapseTracks && !isExpanded ? visibleTracks.slice(0, MAX_VISIBLE_TRACKS) : visibleTracks
622
- const hasMoreTracks = collapseTracks && visibleTracks.length > MAX_VISIBLE_TRACKS
623
-
624
- return (
625
- <Stack space={3}>
626
- <Flex justify="flex-end">
627
- <Button
628
- icon={AddIcon}
629
- text="Add Caption"
630
- tone="primary"
631
- onClick={() => setShowAddDialog(true)}
632
- />
633
- </Flex>
634
-
635
- {displayedTracks.map((track) => (
636
- <TrackCard
637
- key={track.id}
638
- track={track}
639
- iconOnly={iconOnly}
640
- downloadingTrackId={downloadingTrackId}
641
- deletingTrackId={deletingTrackId}
642
- trackToEdit={trackToEdit}
643
- getTrackSourceLabel={getTrackSourceLabel}
644
- handleDownload={handleDownload}
645
- setTrackToEdit={setTrackToEdit}
646
- setTrackToDelete={setTrackToDelete}
647
- />
648
- ))}
649
-
650
- {hasMoreTracks && (
651
- <Flex justify="center">
652
- <Button
653
- icon={isExpanded ? ChevronUpIcon : ChevronDownIcon}
654
- text={
655
- isExpanded ? 'Show less' : `Show ${visibleTracks.length - MAX_VISIBLE_TRACKS} more`
656
- }
657
- mode="ghost"
658
- tone="primary"
659
- onClick={() => setIsExpanded(!isExpanded)}
660
- />
661
- </Flex>
662
- )}
663
-
664
- {trackToDelete && (
665
- <Dialog
666
- animate
667
- id={dialogId}
668
- header="Delete track"
669
- onClose={() => setTrackToDelete(null)}
670
- onClickOutside={() => setTrackToDelete(null)}
671
- width={1}
672
- >
673
- <Card
674
- padding={3}
675
- style={{
676
- minHeight: '150px',
677
- display: 'flex',
678
- alignItems: 'center',
679
- justifyContent: 'center',
680
- }}
681
- >
682
- <Stack space={3}>
683
- <Heading size={2}>
684
- Are you sure you want to delete &quot;
685
- {trackToDelete.name || trackToDelete.language_code || 'Untitled'}&quot;?
686
- </Heading>
687
- <Text size={2}>This action is irreversible</Text>
688
- <Stack space={4} marginY={4}>
689
- <Box>
690
- <Button
691
- icon={
692
- deletingTrackId === trackToDelete.id ? (
693
- <Spinner
694
- style={{
695
- verticalAlign: 'middle',
696
- display: 'inline-block',
697
- marginTop: '-2px',
698
- width: '0.5em',
699
- height: '0.5em',
700
- }}
701
- />
702
- ) : (
703
- <TrashIcon />
704
- )
705
- }
706
- fontSize={2}
707
- padding={3}
708
- text="Delete track"
709
- tone="critical"
710
- onClick={confirmDelete}
711
- disabled={deletingTrackId !== null}
712
- />
713
- </Box>
714
- </Stack>
715
- </Stack>
716
- </Card>
717
- </Dialog>
718
- )}
719
-
720
- {showAddDialog && (
721
- <AddCaptionDialog
722
- asset={asset}
723
- onAdd={handleAddTrack}
724
- onClose={() => setShowAddDialog(false)}
725
- />
726
- )}
727
-
728
- {trackToEdit && (
729
- <EditCaptionDialog
730
- asset={asset}
731
- track={trackToEdit}
732
- onUpdate={handleUpdateTrack}
733
- onClose={() => setTrackToEdit(null)}
734
- />
735
- )}
736
- </Stack>
737
- )
738
- }