sanity-plugin-mux-input 2.14.0 → 2.16.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 +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +1057 -470
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1059 -472
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/_exports/index.ts +1 -0
- package/src/actions/secrets.ts +6 -1
- package/src/actions/upload.ts +1 -1
- package/src/components/ConfigureApi.tsx +51 -5
- package/src/components/EditCaptionDialog.tsx +2 -2
- package/src/components/InputBrowser.tsx +8 -2
- package/src/components/PageSelector.tsx +4 -7
- package/src/components/Player.styled.tsx +7 -2
- package/src/components/PlayerActionsMenu.tsx +15 -1
- package/src/components/ResyncMetadata.tsx +152 -73
- package/src/components/SelectAsset.tsx +9 -3
- package/src/components/StudioTool.tsx +2 -2
- package/src/components/TextTracksManager.tsx +11 -55
- package/src/components/UploadConfiguration.tsx +104 -343
- package/src/components/Uploader.tsx +18 -7
- package/src/components/VideoDetails/VideoDetails.tsx +55 -19
- package/src/components/VideoDetails/useVideoDetails.ts +15 -1
- package/src/components/VideoInBrowser.tsx +53 -6
- package/src/components/VideoPlayer.tsx +120 -47
- package/src/components/VideoThumbnail.tsx +84 -72
- package/src/components/VideosBrowser.tsx +7 -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/useFetchFileSize.ts +54 -0
- package/src/hooks/useMediaMetadata.ts +100 -0
- package/src/hooks/useResyncAsset.ts +110 -0
- package/src/hooks/useResyncMuxMetadata.ts +33 -0
- package/src/hooks/useSaveSecrets.ts +10 -3
- package/src/hooks/useSecretsDocumentValues.ts +9 -1
- package/src/hooks/useSecretsFormState.ts +6 -3
- package/src/schema.ts +5 -0
- package/src/util/addKeysToMuxData.ts +30 -0
- 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 +1 -0
- package/src/util/getVideoSrc.ts +9 -9
- package/src/util/readSecrets.ts +3 -1
- package/src/util/textTracks.ts +6 -3
- package/src/util/tryWithSuspend.ts +22 -0
- package/src/util/types.ts +27 -2
- package/src/util/getPlaybackId.ts +0 -9
package/package.json
CHANGED
package/src/_exports/index.ts
CHANGED
package/src/actions/secrets.ts
CHANGED
|
@@ -9,6 +9,7 @@ interface SecretsDocument {
|
|
|
9
9
|
enableSignedUrls: boolean
|
|
10
10
|
signingKeyId: string
|
|
11
11
|
signingKeyPrivate: string
|
|
12
|
+
drmConfigId: string
|
|
12
13
|
}
|
|
13
14
|
// eslint-disable-next-line max-params
|
|
14
15
|
export function saveSecrets(
|
|
@@ -17,7 +18,8 @@ export function saveSecrets(
|
|
|
17
18
|
secretKey: string,
|
|
18
19
|
enableSignedUrls: boolean,
|
|
19
20
|
signingKeyId: string,
|
|
20
|
-
signingKeyPrivate: string
|
|
21
|
+
signingKeyPrivate: string,
|
|
22
|
+
drmConfigId: string
|
|
21
23
|
): Promise<SecretsDocument> {
|
|
22
24
|
const doc: SecretsDocument = {
|
|
23
25
|
_id: 'secrets.mux',
|
|
@@ -27,7 +29,10 @@ export function saveSecrets(
|
|
|
27
29
|
enableSignedUrls,
|
|
28
30
|
signingKeyId,
|
|
29
31
|
signingKeyPrivate,
|
|
32
|
+
drmConfigId,
|
|
30
33
|
}
|
|
34
|
+
doc.signingKeyId = enableSignedUrls ? signingKeyId : ''
|
|
35
|
+
doc.signingKeyPrivate = enableSignedUrls ? signingKeyPrivate : ''
|
|
31
36
|
|
|
32
37
|
return client.createOrReplace(doc)
|
|
33
38
|
}
|
package/src/actions/upload.ts
CHANGED
|
@@ -158,7 +158,7 @@ type UploadResponse = {
|
|
|
158
158
|
new_asset_settings: {
|
|
159
159
|
static_renditions?: {resolution: string}[]
|
|
160
160
|
passthrough: string
|
|
161
|
-
playback_policies: ['public' | 'signed']
|
|
161
|
+
playback_policies: ['public' | 'signed' | 'drm']
|
|
162
162
|
}
|
|
163
163
|
status: string
|
|
164
164
|
timeout: number
|
|
@@ -32,7 +32,7 @@ export interface ConfigureApiDialogProps {
|
|
|
32
32
|
secrets: Secrets
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
const fieldNames = ['token', 'secretKey', 'enableSignedUrls'] as const
|
|
35
|
+
const fieldNames = ['token', 'secretKey', 'enableSignedUrls', 'drmConfigId'] as const
|
|
36
36
|
|
|
37
37
|
// Internal dialog component that can be used with external state
|
|
38
38
|
export function ConfigureApiDialog({secrets, setDialogState}: ConfigureApiDialogProps) {
|
|
@@ -44,11 +44,12 @@ export function ConfigureApiDialog({secrets, setDialogState}: ConfigureApiDialog
|
|
|
44
44
|
() =>
|
|
45
45
|
secrets.token !== state.token ||
|
|
46
46
|
secrets.secretKey !== state.secretKey ||
|
|
47
|
-
secrets.enableSignedUrls !== state.enableSignedUrls
|
|
47
|
+
secrets.enableSignedUrls !== state.enableSignedUrls ||
|
|
48
|
+
secrets.drmConfigId !== state.drmConfigId,
|
|
48
49
|
[secrets, state]
|
|
49
50
|
)
|
|
50
51
|
const id = `ConfigureApi${useId()}`
|
|
51
|
-
const [tokenId, secretKeyId, enableSignedUrlsId] = useMemo<typeof fieldNames>(
|
|
52
|
+
const [tokenId, secretKeyId, enableSignedUrlsId, drmConfigIdId] = useMemo<typeof fieldNames>(
|
|
52
53
|
() => fieldNames.map((field) => `${id}-${field}`) as unknown as typeof fieldNames,
|
|
53
54
|
[id]
|
|
54
55
|
)
|
|
@@ -63,8 +64,8 @@ export function ConfigureApiDialog({secrets, setDialogState}: ConfigureApiDialog
|
|
|
63
64
|
if (!saving.current && event.currentTarget.reportValidity()) {
|
|
64
65
|
saving.current = true
|
|
65
66
|
dispatch({type: 'submit'})
|
|
66
|
-
const {token, secretKey, enableSignedUrls} = state
|
|
67
|
-
handleSaveSecrets({token, secretKey, enableSignedUrls})
|
|
67
|
+
const {token, secretKey, enableSignedUrls, drmConfigId} = state
|
|
68
|
+
handleSaveSecrets({token, secretKey, enableSignedUrls, drmConfigId})
|
|
68
69
|
.then((savedSecrets) => {
|
|
69
70
|
const {projectId, dataset} = client.config()
|
|
70
71
|
clear([cacheNs, secretsId, projectId, dataset])
|
|
@@ -106,6 +107,15 @@ export function ConfigureApiDialog({secrets, setDialogState}: ConfigureApiDialog
|
|
|
106
107
|
},
|
|
107
108
|
[dispatch]
|
|
108
109
|
)
|
|
110
|
+
const handleChangeDrmConfigId = useCallback(
|
|
111
|
+
(event: React.FormEvent<HTMLInputElement>) => {
|
|
112
|
+
dispatch({
|
|
113
|
+
type: 'change',
|
|
114
|
+
payload: {name: 'drmConfigId', value: event.currentTarget.value},
|
|
115
|
+
})
|
|
116
|
+
},
|
|
117
|
+
[dispatch]
|
|
118
|
+
)
|
|
109
119
|
|
|
110
120
|
useEffect(() => {
|
|
111
121
|
if (firstField.current) {
|
|
@@ -202,6 +212,42 @@ export function ConfigureApiDialog({secrets, setDialogState}: ConfigureApiDialog
|
|
|
202
212
|
) : null}
|
|
203
213
|
</Stack>
|
|
204
214
|
|
|
215
|
+
<FormField title="DRM Configuration ID" inputId={drmConfigIdId}>
|
|
216
|
+
<TextInput
|
|
217
|
+
id={drmConfigIdId}
|
|
218
|
+
onChange={handleChangeDrmConfigId}
|
|
219
|
+
type="text"
|
|
220
|
+
value={state.drmConfigId ?? ''}
|
|
221
|
+
required={false}
|
|
222
|
+
/>
|
|
223
|
+
</FormField>
|
|
224
|
+
<Card padding={[3, 3, 3]} radius={2} shadow={1} tone="neutral">
|
|
225
|
+
<Stack space={3}>
|
|
226
|
+
<Text size={1}>
|
|
227
|
+
DRM (Digital Rights Management) provides an extra layer of content security for
|
|
228
|
+
video content streamed from Mux. For additional information check out our{' '}
|
|
229
|
+
<a
|
|
230
|
+
href="https://www.mux.com/docs/guides/protect-videos-with-drm#play-drm-protected-videos"
|
|
231
|
+
target="_blank"
|
|
232
|
+
rel="noopener noreferrer"
|
|
233
|
+
>
|
|
234
|
+
DRM Guide
|
|
235
|
+
</a>
|
|
236
|
+
.
|
|
237
|
+
</Text>
|
|
238
|
+
<Text size={1}>
|
|
239
|
+
<a
|
|
240
|
+
href="https://www.mux.com/support/human"
|
|
241
|
+
target="_blank"
|
|
242
|
+
rel="noopener noreferrer"
|
|
243
|
+
>
|
|
244
|
+
Contact us
|
|
245
|
+
</a>{' '}
|
|
246
|
+
to get started using DRM.
|
|
247
|
+
</Text>
|
|
248
|
+
</Stack>
|
|
249
|
+
</Card>
|
|
250
|
+
|
|
205
251
|
<Inline space={2}>
|
|
206
252
|
<Button
|
|
207
253
|
text="Save"
|
|
@@ -18,7 +18,7 @@ import {useEffect, useId, useRef, useState} from 'react'
|
|
|
18
18
|
import {addTextTrackFromUrl, deleteTextTrack, getAsset} from '../actions/assets'
|
|
19
19
|
import {useClient} from '../hooks/useClient'
|
|
20
20
|
import {generateJwt} from '../util/generateJwt'
|
|
21
|
-
import {getPlaybackId} from '../util/
|
|
21
|
+
import {getPlaybackId} from '../util/getPlaybackPolicy'
|
|
22
22
|
import {getPlaybackPolicy} from '../util/getPlaybackPolicy'
|
|
23
23
|
import {downloadVttFile, extractErrorMessage, pollTrackStatus} from '../util/textTracks'
|
|
24
24
|
import type {MuxTextTrack, VideoAssetDocument} from '../util/types'
|
|
@@ -276,7 +276,7 @@ export default function EditCaptionDialog({asset, track, onUpdate, onClose}: Pro
|
|
|
276
276
|
const playbackId = getPlaybackId(asset)
|
|
277
277
|
if (!playbackId) return ''
|
|
278
278
|
let url = `https://stream.mux.com/${playbackId}/text/${track.id}.vtt`
|
|
279
|
-
if (getPlaybackPolicy(asset) === 'signed') {
|
|
279
|
+
if (getPlaybackPolicy(asset)?.policy === 'signed') {
|
|
280
280
|
const token = generateJwt(client, playbackId, 'v')
|
|
281
281
|
url += `?token=${token}`
|
|
282
282
|
}
|
|
@@ -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
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-shadow */
|
|
2
1
|
import {ChevronLeftIcon, ChevronRightIcon} from '@sanity/icons'
|
|
3
2
|
import {Button, Label} from '@sanity/ui'
|
|
4
3
|
import {Dispatch, SetStateAction, useEffect} from 'react'
|
|
@@ -7,8 +6,6 @@ const PageSelector = (props: {
|
|
|
7
6
|
page: number
|
|
8
7
|
setPage: Dispatch<SetStateAction<number>>
|
|
9
8
|
total: number
|
|
10
|
-
// eslint-disable-next-line react/no-unused-prop-types
|
|
11
|
-
limit: number
|
|
12
9
|
}) => {
|
|
13
10
|
const page = props.page
|
|
14
11
|
const setPage = props.setPage
|
|
@@ -30,8 +27,8 @@ const PageSelector = (props: {
|
|
|
30
27
|
style={{cursor: 'pointer'}}
|
|
31
28
|
disabled={page <= 0}
|
|
32
29
|
onClick={() => {
|
|
33
|
-
setPage((
|
|
34
|
-
return Math.min(props.total - 1, Math.max(0,
|
|
30
|
+
setPage((p) => {
|
|
31
|
+
return Math.min(props.total - 1, Math.max(0, p - 1))
|
|
35
32
|
})
|
|
36
33
|
}}
|
|
37
34
|
/>
|
|
@@ -45,8 +42,8 @@ const PageSelector = (props: {
|
|
|
45
42
|
style={{cursor: 'pointer'}}
|
|
46
43
|
disabled={page >= props.total - 1}
|
|
47
44
|
onClick={() => {
|
|
48
|
-
setPage((
|
|
49
|
-
return Math.min(props.total - 1, Math.max(0,
|
|
45
|
+
setPage((p) => {
|
|
46
|
+
return Math.min(props.total - 1, Math.max(0, p + 1))
|
|
50
47
|
})
|
|
51
48
|
}}
|
|
52
49
|
/>
|
|
@@ -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
|
+
SyncIcon,
|
|
8
9
|
TranslateIcon,
|
|
9
10
|
UploadIcon,
|
|
10
11
|
} from '@sanity/icons'
|
|
@@ -28,6 +29,7 @@ import {styled} from 'styled-components'
|
|
|
28
29
|
|
|
29
30
|
import {useAccessControl} from '../hooks/useAccessControl'
|
|
30
31
|
import {type DialogState, type SetDialogState} from '../hooks/useDialogState'
|
|
32
|
+
import {useResyncAsset} from '../hooks/useResyncAsset'
|
|
31
33
|
import {getPlaybackPolicy} from '../util/getPlaybackPolicy'
|
|
32
34
|
import type {MuxInputProps, PluginConfig, VideoAssetDocument} from '../util/types'
|
|
33
35
|
import {FileInputMenuItem} from './FileInputMenuItem'
|
|
@@ -64,11 +66,17 @@ function PlayerActionsMenu(
|
|
|
64
66
|
const {asset, readOnly, dialogState, setDialogState, onChange, onSelect, accept} = props
|
|
65
67
|
const [open, setOpen] = useState(false)
|
|
66
68
|
const [menuElement, setMenuRef] = useState<HTMLDivElement | null>(null)
|
|
67
|
-
const isSigned = useMemo(() => getPlaybackPolicy(asset) === 'signed', [asset])
|
|
69
|
+
const isSigned = useMemo(() => getPlaybackPolicy(asset)?.policy === 'signed', [asset])
|
|
68
70
|
const {hasConfigAccess} = useAccessControl(props.config)
|
|
71
|
+
const {resyncAsset, isResyncing} = useResyncAsset({showToast: true})
|
|
69
72
|
|
|
70
73
|
const onReset = useCallback(() => onChange(PatchEvent.from(unset([]))), [onChange])
|
|
71
74
|
|
|
75
|
+
const handleResync = useCallback(async () => {
|
|
76
|
+
setOpen(false)
|
|
77
|
+
await resyncAsset(asset)
|
|
78
|
+
}, [resyncAsset, asset])
|
|
79
|
+
|
|
72
80
|
useEffect(() => {
|
|
73
81
|
if (open && dialogState) {
|
|
74
82
|
setOpen(false)
|
|
@@ -134,6 +142,12 @@ function PlayerActionsMenu(
|
|
|
134
142
|
text="Captions"
|
|
135
143
|
onClick={() => setDialogState('edit-captions')}
|
|
136
144
|
/>
|
|
145
|
+
<MenuItem
|
|
146
|
+
icon={SyncIcon}
|
|
147
|
+
text="Resync from Mux"
|
|
148
|
+
onClick={handleResync}
|
|
149
|
+
disabled={readOnly || isResyncing}
|
|
150
|
+
/>
|
|
137
151
|
</>
|
|
138
152
|
)}
|
|
139
153
|
<MenuDivider />
|
|
@@ -1,28 +1,106 @@
|
|
|
1
1
|
import {CheckmarkCircleIcon, ErrorOutlineIcon, SyncIcon} from '@sanity/icons'
|
|
2
|
-
import {Box, Button, Card, Dialog, Flex, Heading, Spinner, Stack, Text} from '@sanity/ui'
|
|
2
|
+
import {Box, Button, Card, Dialog, Flex, Heading, Radio, Spinner, Stack, Text} from '@sanity/ui'
|
|
3
|
+
import {useState} from 'react'
|
|
3
4
|
|
|
4
5
|
import useResyncMuxMetadata from '../hooks/useResyncMuxMetadata'
|
|
5
6
|
import {isEmptyOrPlaceholderTitle} from '../util/assetTitlePlaceholder'
|
|
6
7
|
import {DIALOGS_Z_INDEX} from '../util/constants'
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
type SyncOption = 'fillEmpty' | 'syncTitles' | 'fullResync'
|
|
10
|
+
|
|
11
|
+
interface OptionCardProps {
|
|
12
|
+
id: SyncOption
|
|
13
|
+
selected: boolean
|
|
14
|
+
onSelect: (id: SyncOption) => void
|
|
15
|
+
title: string
|
|
16
|
+
count: number
|
|
17
|
+
description: string
|
|
18
|
+
disabled?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function OptionCard({
|
|
22
|
+
id,
|
|
23
|
+
selected,
|
|
24
|
+
onSelect,
|
|
25
|
+
title,
|
|
26
|
+
count,
|
|
27
|
+
description,
|
|
28
|
+
disabled,
|
|
29
|
+
}: OptionCardProps) {
|
|
30
|
+
return (
|
|
31
|
+
<Card
|
|
32
|
+
as="label"
|
|
33
|
+
padding={3}
|
|
34
|
+
radius={2}
|
|
35
|
+
border
|
|
36
|
+
tone={selected ? 'primary' : 'default'}
|
|
37
|
+
style={{
|
|
38
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
39
|
+
opacity: disabled ? 0.5 : 1,
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
<Flex gap={3} align="flex-start">
|
|
43
|
+
<Box paddingTop={1}>
|
|
44
|
+
<Radio
|
|
45
|
+
checked={selected}
|
|
46
|
+
onChange={() => onSelect(id)}
|
|
47
|
+
disabled={disabled}
|
|
48
|
+
name="sync-option"
|
|
49
|
+
/>
|
|
50
|
+
</Box>
|
|
51
|
+
<Stack space={2} flex={1}>
|
|
52
|
+
<Flex align="center" gap={2}>
|
|
53
|
+
<Text size={2} weight="semibold">
|
|
54
|
+
{title} ({count})
|
|
55
|
+
</Text>
|
|
56
|
+
</Flex>
|
|
57
|
+
<Text size={1} muted>
|
|
58
|
+
{description}
|
|
59
|
+
</Text>
|
|
60
|
+
</Stack>
|
|
61
|
+
</Flex>
|
|
62
|
+
</Card>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
9
66
|
function ResyncMetadataDialog(props: ReturnType<typeof useResyncMuxMetadata>) {
|
|
10
67
|
const {resyncState} = props
|
|
11
68
|
|
|
12
|
-
const canTriggerResync = resyncState === 'idle' || resyncState === 'error'
|
|
13
|
-
const isResyncing = resyncState === 'syncing'
|
|
14
|
-
const isDone = resyncState === 'done'
|
|
15
|
-
|
|
16
69
|
const videosToUpdate = props.matchedAssets?.filter((m) => m.muxAsset).length || 0
|
|
17
70
|
const videosWithEmptyOrPlaceholder =
|
|
18
71
|
props.matchedAssets?.filter(
|
|
19
72
|
(m) => m.muxAsset && m.muxTitle && isEmptyOrPlaceholderTitle(m.currentTitle, m.muxAsset.id)
|
|
20
73
|
).length || 0
|
|
21
74
|
|
|
75
|
+
const hasEmptyTitles = videosWithEmptyOrPlaceholder > 0
|
|
76
|
+
const defaultOption: SyncOption = hasEmptyTitles ? 'fillEmpty' : 'syncTitles'
|
|
77
|
+
const [selectedOption, setSelectedOption] = useState<SyncOption>(defaultOption)
|
|
78
|
+
|
|
79
|
+
const canTriggerResync = resyncState === 'idle' || resyncState === 'error'
|
|
80
|
+
const isResyncing = resyncState === 'syncing'
|
|
81
|
+
const isDone = resyncState === 'done'
|
|
82
|
+
const isLoading = props.muxAssets.loading || props.sanityAssetsLoading
|
|
83
|
+
|
|
84
|
+
const handleSync = () => {
|
|
85
|
+
switch (selectedOption) {
|
|
86
|
+
case 'fillEmpty':
|
|
87
|
+
props.syncOnlyEmpty()
|
|
88
|
+
break
|
|
89
|
+
case 'syncTitles':
|
|
90
|
+
props.syncAllVideos()
|
|
91
|
+
break
|
|
92
|
+
case 'fullResync':
|
|
93
|
+
props.syncFullData()
|
|
94
|
+
break
|
|
95
|
+
default:
|
|
96
|
+
break
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
22
100
|
return (
|
|
23
101
|
<Dialog
|
|
24
102
|
animate
|
|
25
|
-
header=
|
|
103
|
+
header="Sync with Mux"
|
|
26
104
|
zOffset={DIALOGS_Z_INDEX}
|
|
27
105
|
id="resync-metadata-dialog"
|
|
28
106
|
onClose={props.closeDialog}
|
|
@@ -32,40 +110,25 @@ function ResyncMetadataDialog(props: ReturnType<typeof useResyncMuxMetadata>) {
|
|
|
32
110
|
footer={
|
|
33
111
|
!isDone && (
|
|
34
112
|
<Card padding={3}>
|
|
35
|
-
<Flex justify="
|
|
113
|
+
<Flex justify="flex-end" gap={2}>
|
|
36
114
|
<Button
|
|
37
115
|
fontSize={2}
|
|
38
116
|
padding={3}
|
|
39
117
|
mode="ghost"
|
|
40
118
|
text="Cancel"
|
|
41
|
-
tone="critical"
|
|
42
119
|
onClick={props.closeDialog}
|
|
43
120
|
disabled={isResyncing}
|
|
44
121
|
/>
|
|
45
|
-
<
|
|
46
|
-
{
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
/>
|
|
56
|
-
)}
|
|
57
|
-
<Button
|
|
58
|
-
icon={SyncIcon}
|
|
59
|
-
fontSize={2}
|
|
60
|
-
padding={3}
|
|
61
|
-
mode="ghost"
|
|
62
|
-
text={`Update all (${videosToUpdate})`}
|
|
63
|
-
tone="positive"
|
|
64
|
-
onClick={props.syncAllVideos}
|
|
65
|
-
iconRight={isResyncing && Spinner}
|
|
66
|
-
disabled={!canTriggerResync}
|
|
67
|
-
/>
|
|
68
|
-
</Flex>
|
|
122
|
+
<Button
|
|
123
|
+
icon={SyncIcon}
|
|
124
|
+
fontSize={2}
|
|
125
|
+
padding={3}
|
|
126
|
+
text="Run sync"
|
|
127
|
+
tone="primary"
|
|
128
|
+
onClick={handleSync}
|
|
129
|
+
iconRight={isResyncing && Spinner}
|
|
130
|
+
disabled={!canTriggerResync || isLoading}
|
|
131
|
+
/>
|
|
69
132
|
</Flex>
|
|
70
133
|
</Card>
|
|
71
134
|
)
|
|
@@ -73,15 +136,17 @@ function ResyncMetadataDialog(props: ReturnType<typeof useResyncMuxMetadata>) {
|
|
|
73
136
|
>
|
|
74
137
|
<Box padding={4}>
|
|
75
138
|
{/* LOADING ASSETS STATE */}
|
|
76
|
-
{
|
|
77
|
-
<Card tone="primary" marginBottom={
|
|
139
|
+
{isLoading && (
|
|
140
|
+
<Card tone="primary" marginBottom={4} padding={3} border radius={2}>
|
|
78
141
|
<Flex align="center" gap={4}>
|
|
79
142
|
<Spinner muted size={4} />
|
|
80
143
|
<Stack space={2}>
|
|
81
144
|
<Text size={2} weight="semibold">
|
|
82
145
|
Loading assets from Mux
|
|
83
146
|
</Text>
|
|
84
|
-
<Text size={1}
|
|
147
|
+
<Text size={1} muted>
|
|
148
|
+
This may take a while.
|
|
149
|
+
</Text>
|
|
85
150
|
</Stack>
|
|
86
151
|
</Flex>
|
|
87
152
|
</Card>
|
|
@@ -89,7 +154,7 @@ function ResyncMetadataDialog(props: ReturnType<typeof useResyncMuxMetadata>) {
|
|
|
89
154
|
|
|
90
155
|
{/* ERROR LOADING MUX */}
|
|
91
156
|
{props.muxAssets.error && (
|
|
92
|
-
<Card tone="critical" marginBottom={
|
|
157
|
+
<Card tone="critical" marginBottom={4} padding={3} border radius={2}>
|
|
93
158
|
<Flex align="center" gap={2}>
|
|
94
159
|
<ErrorOutlineIcon fontSize={36} />
|
|
95
160
|
<Stack space={2}>
|
|
@@ -104,14 +169,16 @@ function ResyncMetadataDialog(props: ReturnType<typeof useResyncMuxMetadata>) {
|
|
|
104
169
|
|
|
105
170
|
{/* SYNCING STATE */}
|
|
106
171
|
{resyncState === 'syncing' && (
|
|
107
|
-
<Card tone="primary" marginBottom={
|
|
172
|
+
<Card tone="primary" marginBottom={4} padding={3} border radius={2}>
|
|
108
173
|
<Flex align="center" gap={4}>
|
|
109
174
|
<Spinner muted size={4} />
|
|
110
175
|
<Stack space={2}>
|
|
111
176
|
<Text size={2} weight="semibold">
|
|
112
|
-
|
|
177
|
+
Syncing metadata
|
|
178
|
+
</Text>
|
|
179
|
+
<Text size={1} muted>
|
|
180
|
+
Updating videos from Mux...
|
|
113
181
|
</Text>
|
|
114
|
-
<Text size={1}>Syncing titles from Mux...</Text>
|
|
115
182
|
</Stack>
|
|
116
183
|
</Flex>
|
|
117
184
|
</Card>
|
|
@@ -119,7 +186,7 @@ function ResyncMetadataDialog(props: ReturnType<typeof useResyncMuxMetadata>) {
|
|
|
119
186
|
|
|
120
187
|
{/* ERROR SYNCING */}
|
|
121
188
|
{resyncState === 'error' && (
|
|
122
|
-
<Card tone="critical" marginBottom={
|
|
189
|
+
<Card tone="critical" marginBottom={4} padding={3} border radius={2}>
|
|
123
190
|
<Flex align="center" gap={2}>
|
|
124
191
|
<ErrorOutlineIcon fontSize={36} />
|
|
125
192
|
<Stack space={2}>
|
|
@@ -138,45 +205,57 @@ function ResyncMetadataDialog(props: ReturnType<typeof useResyncMuxMetadata>) {
|
|
|
138
205
|
|
|
139
206
|
{/* SUCCESS STATE */}
|
|
140
207
|
{resyncState === 'done' && (
|
|
141
|
-
<Stack paddingY={5}
|
|
208
|
+
<Stack paddingY={5} space={3} style={{textAlign: 'center'}}>
|
|
142
209
|
<Box>
|
|
143
210
|
<CheckmarkCircleIcon fontSize={48} />
|
|
144
211
|
</Box>
|
|
145
|
-
<Heading size={2}>
|
|
146
|
-
<Text size={2}
|
|
212
|
+
<Heading size={2}>Sync completed</Heading>
|
|
213
|
+
<Text size={2} muted>
|
|
214
|
+
Videos have been updated from Mux.
|
|
215
|
+
</Text>
|
|
147
216
|
</Stack>
|
|
148
217
|
)}
|
|
149
218
|
|
|
150
|
-
{/*
|
|
151
|
-
{
|
|
219
|
+
{/* OPTIONS */}
|
|
220
|
+
{!isDone && !isLoading && !props.muxAssets.error && (
|
|
152
221
|
<Stack space={4}>
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
{videosToUpdate === 1 ? '' : 's'} with Mux metadata
|
|
156
|
-
</Heading>
|
|
157
|
-
<Text size={2}>
|
|
158
|
-
This will update video titles in Sanity to match those in Mux. No new videos will be
|
|
159
|
-
created.
|
|
222
|
+
<Text size={1} muted>
|
|
223
|
+
Found {videosToUpdate} video{videosToUpdate === 1 ? '' : 's'} linked to Mux.
|
|
160
224
|
</Text>
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
225
|
+
|
|
226
|
+
<Stack space={3}>
|
|
227
|
+
{hasEmptyTitles && (
|
|
228
|
+
<OptionCard
|
|
229
|
+
id="fillEmpty"
|
|
230
|
+
selected={selectedOption === 'fillEmpty'}
|
|
231
|
+
onSelect={setSelectedOption}
|
|
232
|
+
title="Fill missing titles only"
|
|
233
|
+
count={videosWithEmptyOrPlaceholder}
|
|
234
|
+
description="Updates only videos without a title or with placeholder titles (e.g., 'Asset #123') using the title from Mux."
|
|
235
|
+
disabled={isResyncing}
|
|
236
|
+
/>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
<OptionCard
|
|
240
|
+
id="syncTitles"
|
|
241
|
+
selected={selectedOption === 'syncTitles'}
|
|
242
|
+
onSelect={setSelectedOption}
|
|
243
|
+
title="Sync all titles"
|
|
244
|
+
count={videosToUpdate}
|
|
245
|
+
description="Replaces the title in Sanity with the title from Mux for all videos."
|
|
246
|
+
disabled={isResyncing}
|
|
247
|
+
/>
|
|
248
|
+
|
|
249
|
+
<OptionCard
|
|
250
|
+
id="fullResync"
|
|
251
|
+
selected={selectedOption === 'fullResync'}
|
|
252
|
+
onSelect={setSelectedOption}
|
|
253
|
+
title="Full resync"
|
|
254
|
+
count={videosToUpdate}
|
|
255
|
+
description="Updates all fields from Mux including status, duration, tracks, captions, and renditions."
|
|
256
|
+
disabled={isResyncing}
|
|
257
|
+
/>
|
|
258
|
+
</Stack>
|
|
180
259
|
</Stack>
|
|
181
260
|
)}
|
|
182
261
|
</Box>
|
|
@@ -197,5 +276,5 @@ export default function ResyncMetadata() {
|
|
|
197
276
|
}
|
|
198
277
|
|
|
199
278
|
// eslint-disable-next-line consistent-return
|
|
200
|
-
return <Button mode="bleed" text="
|
|
279
|
+
return <Button mode="bleed" text="Sync with Mux" onClick={resyncMetadata.openDialog} />
|
|
201
280
|
}
|
|
@@ -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 = {
|