sanity-plugin-mux-input 2.2.4 → 2.3.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.
- package/README.md +148 -16
- package/lib/index.cjs +3996 -3677
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +210 -0
- package/lib/index.d.ts +109 -25
- package/lib/index.esm.js +4390 -0
- package/lib/index.esm.js.map +1 -0
- package/lib/index.js +3964 -3626
- package/lib/index.js.map +1 -1
- package/package.json +48 -52
- package/src/_exports/index.ts +32 -0
- package/src/actions/upload.ts +35 -40
- package/src/clients/upChunkObservable.ts +5 -1
- package/src/components/ConfigureApi.tsx +0 -1
- package/src/components/FileInputArea.tsx +92 -0
- package/src/components/FileInputButton.tsx +3 -2
- package/src/components/FileInputMenuItem.styled.tsx +2 -2
- package/src/components/FileInputMenuItem.tsx +2 -10
- package/src/components/ImportVideosFromMux.tsx +317 -0
- package/src/components/Input.tsx +3 -3
- package/src/components/PlayerActionsMenu.tsx +14 -12
- package/src/components/SelectAsset.tsx +1 -1
- package/src/components/StudioTool.tsx +11 -6
- package/src/components/TextTracksEditor.tsx +214 -0
- package/src/components/UploadConfiguration.tsx +390 -0
- package/src/components/UploadPlaceholder.tsx +41 -55
- package/src/components/Uploader.styled.tsx +0 -1
- package/src/components/Uploader.tsx +384 -0
- package/src/components/VideoDetails/DeleteDialog.tsx +20 -24
- package/src/components/VideoPlayer.tsx +33 -5
- package/src/components/VideoThumbnail.tsx +21 -7
- package/src/components/VideosBrowser.tsx +6 -3
- package/src/components/withFocusRing/withFocusRing.ts +20 -22
- package/src/hooks/useClient.ts +1 -1
- package/src/hooks/useImportMuxAssets.ts +127 -0
- package/src/hooks/useMuxAssets.ts +168 -0
- package/src/plugin.tsx +5 -5
- package/src/util/asserters.ts +9 -0
- package/src/util/createSearchFilter.ts +1 -1
- package/src/util/formatBytes.ts +32 -0
- package/src/util/generateJwt.ts +1 -0
- package/src/util/getAnimatedPosterSrc.ts +1 -1
- package/src/util/getPlaybackId.ts +1 -1
- package/src/util/getPlaybackPolicy.ts +1 -1
- package/src/util/parsers.ts +5 -0
- package/src/util/types.ts +195 -12
- package/lib/index.cjs.js +0 -5
- package/src/components/__legacy__Uploader.tsx +0 -280
- package/src/index.ts +0 -29
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import {ErrorOutlineIcon} from '@sanity/icons'
|
|
2
|
+
import {Button, CardTone, Flex, Text, useToast} from '@sanity/ui'
|
|
3
|
+
import React, {useEffect, useReducer, useRef, useState} from 'react'
|
|
4
|
+
import {Subject, Subscription, type Observable} from 'rxjs'
|
|
5
|
+
import {takeUntil, tap} from 'rxjs/operators'
|
|
6
|
+
import type {SanityClient} from 'sanity'
|
|
7
|
+
import {PatchEvent, set, setIfMissing} from 'sanity'
|
|
8
|
+
|
|
9
|
+
import {uploadFile, uploadUrl} from '../actions/upload'
|
|
10
|
+
import {type DialogState, type SetDialogState} from '../hooks/useDialogState'
|
|
11
|
+
import {isValidUrl} from '../util/asserters'
|
|
12
|
+
import {extractDroppedFiles} from '../util/extractFiles'
|
|
13
|
+
import type {
|
|
14
|
+
MuxInputProps,
|
|
15
|
+
MuxNewAssetSettings,
|
|
16
|
+
PluginConfig,
|
|
17
|
+
Secrets,
|
|
18
|
+
VideoAssetDocument,
|
|
19
|
+
} from '../util/types'
|
|
20
|
+
import InputBrowser from './InputBrowser'
|
|
21
|
+
import Player from './Player'
|
|
22
|
+
import PlayerActionsMenu from './PlayerActionsMenu'
|
|
23
|
+
import UploadConfiguration from './UploadConfiguration'
|
|
24
|
+
import {UploadCard} from './Uploader.styled'
|
|
25
|
+
import UploadPlaceholder from './UploadPlaceholder'
|
|
26
|
+
import {UploadProgress} from './UploadProgress'
|
|
27
|
+
|
|
28
|
+
interface Props extends Pick<MuxInputProps, 'onChange' | 'readOnly'> {
|
|
29
|
+
config: PluginConfig
|
|
30
|
+
client: SanityClient
|
|
31
|
+
secrets: Secrets
|
|
32
|
+
asset: VideoAssetDocument | null | undefined
|
|
33
|
+
dialogState: DialogState
|
|
34
|
+
setDialogState: SetDialogState
|
|
35
|
+
needsSetup: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type StagedUpload = {type: 'file'; files: FileList | File[]} | {type: 'url'; url: string}
|
|
39
|
+
type UploadStatus = {
|
|
40
|
+
progress: number
|
|
41
|
+
file?: {name: string | undefined; type: string}
|
|
42
|
+
uuid?: string
|
|
43
|
+
url?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface State {
|
|
47
|
+
stagedUpload: StagedUpload | null
|
|
48
|
+
uploadStatus: UploadStatus | null
|
|
49
|
+
error: Error | null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const INITIAL_STATE: State = {
|
|
53
|
+
stagedUpload: null,
|
|
54
|
+
uploadStatus: null,
|
|
55
|
+
error: null,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type UploadFileEvent = ReturnType<typeof uploadFile> extends Observable<infer T> ? T : never
|
|
59
|
+
type UploadUrlEvent = ReturnType<typeof uploadUrl> extends Observable<infer T> ? T : never
|
|
60
|
+
type UploaderStateAction =
|
|
61
|
+
| {action: 'stageUpload'; input: NonNullable<State['stagedUpload']>}
|
|
62
|
+
| {action: 'commitUpload'}
|
|
63
|
+
| ({action: 'progressInfo'} & (
|
|
64
|
+
| Extract<UploadFileEvent, {type: 'uuid' | 'file'}>
|
|
65
|
+
| Extract<UploadUrlEvent, {type: 'url'}>
|
|
66
|
+
))
|
|
67
|
+
| {action: 'progress'; percent: number}
|
|
68
|
+
| {action: 'error'; error: any}
|
|
69
|
+
| {action: 'complete' | 'reset'}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The main interface for inputting a Mux Video. It handles staging an upload
|
|
73
|
+
* file, setting its configuration, displaying upload progress, and showing
|
|
74
|
+
* the preview player.
|
|
75
|
+
*/
|
|
76
|
+
export default function Uploader(props: Props) {
|
|
77
|
+
const toast = useToast()
|
|
78
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
79
|
+
|
|
80
|
+
const dragEnteredEls = useRef<EventTarget[]>([])
|
|
81
|
+
const [dragState, setDragState] = useState<'valid' | 'invalid' | null>(null)
|
|
82
|
+
|
|
83
|
+
const cancelUploadButton = useRef(
|
|
84
|
+
(() => {
|
|
85
|
+
const events$ = new Subject()
|
|
86
|
+
return {
|
|
87
|
+
observable: events$.asObservable(),
|
|
88
|
+
handleClick: ((event) => events$.next(event)) as React.MouseEventHandler<HTMLButtonElement>,
|
|
89
|
+
}
|
|
90
|
+
})()
|
|
91
|
+
).current
|
|
92
|
+
|
|
93
|
+
const uploadRef = useRef<Subscription | null>(null)
|
|
94
|
+
const [state, dispatch] = useReducer(
|
|
95
|
+
(prev: State, action: UploaderStateAction) => {
|
|
96
|
+
switch (action.action) {
|
|
97
|
+
case 'stageUpload':
|
|
98
|
+
return Object.assign({}, INITIAL_STATE, {stagedUpload: action.input})
|
|
99
|
+
case 'commitUpload':
|
|
100
|
+
return Object.assign({}, prev, {uploadStatus: {progress: 0}})
|
|
101
|
+
case 'progressInfo': {
|
|
102
|
+
const {type, action: _, ...payload} = action
|
|
103
|
+
return Object.assign({}, prev, {
|
|
104
|
+
uploadStatus: {
|
|
105
|
+
...prev.uploadStatus,
|
|
106
|
+
progress: prev.uploadStatus!.progress,
|
|
107
|
+
...payload,
|
|
108
|
+
},
|
|
109
|
+
} satisfies Pick<typeof prev, 'uploadStatus'>)
|
|
110
|
+
}
|
|
111
|
+
case 'progress':
|
|
112
|
+
return Object.assign({}, prev, {
|
|
113
|
+
uploadStatus: {
|
|
114
|
+
...prev.uploadStatus,
|
|
115
|
+
progress: action.percent,
|
|
116
|
+
},
|
|
117
|
+
} satisfies Pick<typeof prev, 'uploadStatus'>)
|
|
118
|
+
case 'reset':
|
|
119
|
+
case 'complete':
|
|
120
|
+
// Clear upload observable on completion
|
|
121
|
+
uploadRef.current?.unsubscribe()
|
|
122
|
+
uploadRef.current = null
|
|
123
|
+
return INITIAL_STATE
|
|
124
|
+
case 'error':
|
|
125
|
+
// Clear upload observable on error
|
|
126
|
+
uploadRef.current?.unsubscribe()
|
|
127
|
+
uploadRef.current = null
|
|
128
|
+
return Object.assign({}, INITIAL_STATE, {error: action.error})
|
|
129
|
+
default:
|
|
130
|
+
return prev
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
stagedUpload: null,
|
|
135
|
+
uploadStatus: null,
|
|
136
|
+
error: null,
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
// Make sure we close out the upload observer on dismount
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
return () => {
|
|
143
|
+
if (uploadRef.current && !uploadRef.current.closed) {
|
|
144
|
+
uploadRef.current.unsubscribe()
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}, [])
|
|
148
|
+
|
|
149
|
+
/* -------------------------------------------------------------------------- */
|
|
150
|
+
/* Uploading */
|
|
151
|
+
/* -------------------------------------------------------------------------- */
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Begins a file or URL upload with the staged files or URL.
|
|
155
|
+
*
|
|
156
|
+
* Should only be called from the UploadConfiguration component, which provides
|
|
157
|
+
* the Mux configuration for the direct asset upload.
|
|
158
|
+
*
|
|
159
|
+
* @param settings The Mux new_asset_settings object to send to Sanity
|
|
160
|
+
* @returns
|
|
161
|
+
*/
|
|
162
|
+
const startUpload = (settings: MuxNewAssetSettings) => {
|
|
163
|
+
const {stagedUpload} = state
|
|
164
|
+
if (!stagedUpload || uploadRef.current) return
|
|
165
|
+
dispatch({action: 'commitUpload'})
|
|
166
|
+
let uploadObservable: Observable<UploadFileEvent | UploadUrlEvent>
|
|
167
|
+
// eslint-disable-next-line default-case
|
|
168
|
+
switch (stagedUpload.type) {
|
|
169
|
+
case 'url':
|
|
170
|
+
uploadObservable = uploadUrl({
|
|
171
|
+
client: props.client,
|
|
172
|
+
url: stagedUpload.url,
|
|
173
|
+
settings,
|
|
174
|
+
})
|
|
175
|
+
break
|
|
176
|
+
case 'file':
|
|
177
|
+
uploadObservable = uploadFile({
|
|
178
|
+
client: props.client,
|
|
179
|
+
file: stagedUpload.files[0],
|
|
180
|
+
settings,
|
|
181
|
+
}).pipe(
|
|
182
|
+
takeUntil(
|
|
183
|
+
cancelUploadButton.observable.pipe(
|
|
184
|
+
tap(() => {
|
|
185
|
+
if (state.uploadStatus?.uuid) {
|
|
186
|
+
props.client.delete(state.uploadStatus.uuid)
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
break
|
|
193
|
+
}
|
|
194
|
+
uploadRef.current = uploadObservable.subscribe({
|
|
195
|
+
next: (event) => {
|
|
196
|
+
switch (event.type) {
|
|
197
|
+
case 'uuid':
|
|
198
|
+
case 'file':
|
|
199
|
+
case 'url':
|
|
200
|
+
dispatch({action: 'progressInfo', ...event})
|
|
201
|
+
break
|
|
202
|
+
case 'progress':
|
|
203
|
+
dispatch({action: 'progress', percent: event.percent})
|
|
204
|
+
break
|
|
205
|
+
case 'success':
|
|
206
|
+
dispatch({action: 'progress', percent: 100})
|
|
207
|
+
props.onChange(
|
|
208
|
+
PatchEvent.from([
|
|
209
|
+
setIfMissing({asset: {}}),
|
|
210
|
+
set({_type: 'reference', _weak: true, _ref: event.asset._id}, ['asset']),
|
|
211
|
+
])
|
|
212
|
+
)
|
|
213
|
+
break
|
|
214
|
+
case 'pause':
|
|
215
|
+
case 'resume':
|
|
216
|
+
default:
|
|
217
|
+
break
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
complete: () => dispatch({action: 'complete'}),
|
|
221
|
+
error: (error) => dispatch({action: 'error', error}),
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* -------------------------- Upload Initialization ------------------------- */
|
|
226
|
+
// The below populate the uploadInput state field, which then triggers the
|
|
227
|
+
// upload configuration, or the startUpload function if no config is required.
|
|
228
|
+
|
|
229
|
+
// Stages an upload from the file selector
|
|
230
|
+
const handleUpload = (files: FileList | File[]) => {
|
|
231
|
+
dispatch({
|
|
232
|
+
action: 'stageUpload',
|
|
233
|
+
input: {type: 'file', files},
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Stages and validates an upload from pasting an asset URL
|
|
238
|
+
const handlePaste: React.ClipboardEventHandler<HTMLInputElement> = (event) => {
|
|
239
|
+
event.preventDefault()
|
|
240
|
+
event.stopPropagation()
|
|
241
|
+
const clipboardData = event.clipboardData || (window as any).clipboardData
|
|
242
|
+
const url = clipboardData.getData('text')
|
|
243
|
+
if (!isValidUrl(url)) {
|
|
244
|
+
toast.push({status: 'error', title: 'Invalid URL for Mux video input.'})
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
dispatch({action: 'stageUpload', input: {type: 'url', url: url}})
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Stages and validates an upload from dragging+dropping files or folders
|
|
251
|
+
const handleDrop: React.DragEventHandler<HTMLDivElement> = (event) => {
|
|
252
|
+
setDragState(null)
|
|
253
|
+
event.preventDefault()
|
|
254
|
+
event.stopPropagation()
|
|
255
|
+
extractDroppedFiles(event.nativeEvent.dataTransfer!).then((files) => {
|
|
256
|
+
dispatch({
|
|
257
|
+
action: 'stageUpload',
|
|
258
|
+
input: {type: 'file', files},
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* ------------------------------- Drag State ------------------------------- */
|
|
264
|
+
|
|
265
|
+
const handleDragOver: React.DragEventHandler<HTMLDivElement> = (event) => {
|
|
266
|
+
event.preventDefault()
|
|
267
|
+
event.stopPropagation()
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const handleDragEnter: React.DragEventHandler<HTMLDivElement> = (event) => {
|
|
271
|
+
event.stopPropagation()
|
|
272
|
+
dragEnteredEls.current.push(event.target)
|
|
273
|
+
const type = event.dataTransfer.items?.[0]?.type
|
|
274
|
+
setDragState(type?.startsWith('video/') ? 'valid' : 'invalid')
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const handleDragLeave: React.DragEventHandler<HTMLDivElement> = (event) => {
|
|
278
|
+
event.stopPropagation()
|
|
279
|
+
const idx = dragEnteredEls.current.indexOf(event.target)
|
|
280
|
+
if (idx > -1) {
|
|
281
|
+
dragEnteredEls.current.splice(idx, 1)
|
|
282
|
+
}
|
|
283
|
+
if (dragEnteredEls.current.length === 0) {
|
|
284
|
+
setDragState(null)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/* -------------------------------- Rendering ------------------------------- */
|
|
289
|
+
|
|
290
|
+
// Upload has errored
|
|
291
|
+
if (state.error !== null) {
|
|
292
|
+
const error = {state}
|
|
293
|
+
return (
|
|
294
|
+
<Flex gap={3} direction="column" justify="center" align="center">
|
|
295
|
+
<Text size={5} muted>
|
|
296
|
+
<ErrorOutlineIcon />
|
|
297
|
+
</Text>
|
|
298
|
+
<Text>Something went wrong</Text>
|
|
299
|
+
{error instanceof Error && error.message && (
|
|
300
|
+
<Text size={1} muted>
|
|
301
|
+
{error.message}
|
|
302
|
+
</Text>
|
|
303
|
+
)}
|
|
304
|
+
<Button text="Upload another file" onClick={() => dispatch({action: 'reset'})} />
|
|
305
|
+
</Flex>
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Upload is in progress
|
|
310
|
+
if (state.uploadStatus !== null) {
|
|
311
|
+
const {uploadStatus} = state
|
|
312
|
+
return (
|
|
313
|
+
<UploadProgress
|
|
314
|
+
onCancel={cancelUploadButton.handleClick}
|
|
315
|
+
progress={uploadStatus.progress}
|
|
316
|
+
filename={uploadStatus.file?.name || uploadStatus.url}
|
|
317
|
+
/>
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Upload needs configuration
|
|
322
|
+
if (state.stagedUpload !== null) {
|
|
323
|
+
return (
|
|
324
|
+
<UploadConfiguration
|
|
325
|
+
stagedUpload={state.stagedUpload}
|
|
326
|
+
pluginConfig={props.config}
|
|
327
|
+
secrets={props.secrets}
|
|
328
|
+
startUpload={startUpload}
|
|
329
|
+
onClose={() => dispatch({action: 'reset'})}
|
|
330
|
+
/>
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Default: No staged upload
|
|
335
|
+
let tone: CardTone | undefined
|
|
336
|
+
if (dragState) tone = dragState === 'valid' ? 'positive' : 'critical'
|
|
337
|
+
|
|
338
|
+
return (
|
|
339
|
+
<>
|
|
340
|
+
<UploadCard
|
|
341
|
+
tone={tone}
|
|
342
|
+
onDrop={handleDrop}
|
|
343
|
+
onDragOver={handleDragOver}
|
|
344
|
+
onDragLeave={handleDragLeave}
|
|
345
|
+
onDragEnter={handleDragEnter}
|
|
346
|
+
onPaste={handlePaste}
|
|
347
|
+
ref={containerRef}
|
|
348
|
+
>
|
|
349
|
+
{props.asset ? (
|
|
350
|
+
<Player
|
|
351
|
+
readOnly={props.readOnly}
|
|
352
|
+
asset={props.asset}
|
|
353
|
+
onChange={props.onChange}
|
|
354
|
+
buttons={
|
|
355
|
+
<PlayerActionsMenu
|
|
356
|
+
asset={props.asset}
|
|
357
|
+
dialogState={props.dialogState}
|
|
358
|
+
setDialogState={props.setDialogState}
|
|
359
|
+
onChange={props.onChange}
|
|
360
|
+
onSelect={handleUpload}
|
|
361
|
+
readOnly={props.readOnly}
|
|
362
|
+
/>
|
|
363
|
+
}
|
|
364
|
+
/>
|
|
365
|
+
) : (
|
|
366
|
+
<UploadPlaceholder
|
|
367
|
+
hovering={dragState !== null}
|
|
368
|
+
onSelect={handleUpload}
|
|
369
|
+
readOnly={!!props.readOnly}
|
|
370
|
+
setDialogState={props.setDialogState}
|
|
371
|
+
needsSetup={props.needsSetup}
|
|
372
|
+
/>
|
|
373
|
+
)}
|
|
374
|
+
</UploadCard>
|
|
375
|
+
{props.dialogState === 'select-video' && (
|
|
376
|
+
<InputBrowser
|
|
377
|
+
asset={props.asset}
|
|
378
|
+
onChange={props.onChange}
|
|
379
|
+
setDialogState={props.setDialogState}
|
|
380
|
+
/>
|
|
381
|
+
)}
|
|
382
|
+
</>
|
|
383
|
+
)
|
|
384
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {TrashIcon} from '@sanity/icons'
|
|
2
|
-
import {Button, Card, Checkbox, Dialog, Flex, Heading, Stack, Text, useToast} from '@sanity/ui'
|
|
3
|
-
import
|
|
4
|
-
import {SanityDocument} from 'sanity'
|
|
2
|
+
import {Box, Button, Card, Checkbox, Dialog, Flex, Heading, Stack, Text, useToast} from '@sanity/ui'
|
|
3
|
+
import {useEffect, useState} from 'react'
|
|
4
|
+
import type {SanityDocument} from 'sanity'
|
|
5
5
|
|
|
6
6
|
import {deleteAsset} from '../../actions/assets'
|
|
7
7
|
import {useClient} from '../../hooks/useClient'
|
|
8
8
|
import {DIALOGS_Z_INDEX} from '../../util/constants'
|
|
9
|
-
import {PluginPlacement, VideoAssetDocument} from '../../util/types'
|
|
9
|
+
import type {PluginPlacement, VideoAssetDocument} from '../../util/types'
|
|
10
10
|
import SpinnerBox from '../SpinnerBox'
|
|
11
11
|
import VideoReferences from './VideoReferences'
|
|
12
12
|
|
|
@@ -69,26 +69,9 @@ export default function DeleteDialog({
|
|
|
69
69
|
onClickOutside={cancelDelete}
|
|
70
70
|
width={1}
|
|
71
71
|
position="fixed"
|
|
72
|
-
footer={
|
|
73
|
-
<Card padding={3}>
|
|
74
|
-
<Flex justify="space-between" align="center">
|
|
75
|
-
<Button
|
|
76
|
-
icon={TrashIcon}
|
|
77
|
-
fontSize={2}
|
|
78
|
-
padding={3}
|
|
79
|
-
text="Delete video"
|
|
80
|
-
tone="critical"
|
|
81
|
-
onClick={confirmDelete}
|
|
82
|
-
disabled={['processing_deletion', 'checkingReferences', 'cantDelete'].some(
|
|
83
|
-
(s) => s === state
|
|
84
|
-
)}
|
|
85
|
-
/>
|
|
86
|
-
</Flex>
|
|
87
|
-
</Card>
|
|
88
|
-
}
|
|
89
72
|
>
|
|
90
73
|
<Card
|
|
91
|
-
padding={
|
|
74
|
+
padding={3}
|
|
92
75
|
style={{
|
|
93
76
|
minHeight: '150px',
|
|
94
77
|
display: 'flex',
|
|
@@ -105,7 +88,7 @@ export default function DeleteDialog({
|
|
|
105
88
|
)}
|
|
106
89
|
{state === 'cantDelete' && (
|
|
107
90
|
<>
|
|
108
|
-
<Heading size={2}>Video can
|
|
91
|
+
<Heading size={2}>Video can't be deleted</Heading>
|
|
109
92
|
<Text size={2} style={{marginBottom: '2rem'}}>
|
|
110
93
|
There are {references?.length} document{references && references.length > 0 && 's'}{' '}
|
|
111
94
|
pointing to this video. Remove their references to this file or delete them before
|
|
@@ -122,7 +105,7 @@ export default function DeleteDialog({
|
|
|
122
105
|
<>
|
|
123
106
|
<Heading size={2}>Are you sure you want to delete this video?</Heading>
|
|
124
107
|
<Text size={2}>This action is irreversible</Text>
|
|
125
|
-
<Stack space={4}
|
|
108
|
+
<Stack space={4} marginY={4}>
|
|
126
109
|
<Flex align="center" as="label">
|
|
127
110
|
<Checkbox
|
|
128
111
|
checked={deleteOnMux}
|
|
@@ -134,6 +117,19 @@ export default function DeleteDialog({
|
|
|
134
117
|
<Checkbox disabled checked />
|
|
135
118
|
<Text style={{margin: '0 10px'}}>Delete video from dataset</Text>
|
|
136
119
|
</Flex>
|
|
120
|
+
<Box>
|
|
121
|
+
<Button
|
|
122
|
+
icon={TrashIcon}
|
|
123
|
+
fontSize={2}
|
|
124
|
+
padding={3}
|
|
125
|
+
text="Delete video"
|
|
126
|
+
tone="critical"
|
|
127
|
+
onClick={confirmDelete}
|
|
128
|
+
disabled={['processing_deletion', 'checkingReferences', 'cantDelete'].some(
|
|
129
|
+
(s) => s === state
|
|
130
|
+
)}
|
|
131
|
+
/>
|
|
132
|
+
</Box>
|
|
137
133
|
</Stack>
|
|
138
134
|
</>
|
|
139
135
|
)}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import MuxPlayer, {MuxPlayerProps} from '@mux/mux-player-react'
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
import MuxPlayer, {type MuxPlayerProps} from '@mux/mux-player-react'
|
|
2
|
+
import {ErrorOutlineIcon} from '@sanity/icons'
|
|
3
|
+
import {Card, Text} from '@sanity/ui'
|
|
4
|
+
import {type PropsWithChildren, useMemo} from 'react'
|
|
4
5
|
|
|
5
6
|
import {useClient} from '../hooks/useClient'
|
|
6
7
|
import {MIN_ASPECT_RATIO} from '../util/constants'
|
|
@@ -17,7 +18,17 @@ export default function VideoPlayer({
|
|
|
17
18
|
>) {
|
|
18
19
|
const client = useClient()
|
|
19
20
|
|
|
20
|
-
const videoSrc = useMemo(() =>
|
|
21
|
+
const {src: videoSrc, error} = useMemo(() => {
|
|
22
|
+
try {
|
|
23
|
+
const src = asset?.playbackId && getVideoSrc({client, asset})
|
|
24
|
+
if (src) return {src: src}
|
|
25
|
+
|
|
26
|
+
return {error: new TypeError('Asset has no playback ID')}
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
28
|
+
} catch (error) {
|
|
29
|
+
return {error}
|
|
30
|
+
}
|
|
31
|
+
}, [asset, client])
|
|
21
32
|
|
|
22
33
|
const signedToken = useMemo(() => {
|
|
23
34
|
try {
|
|
@@ -46,7 +57,6 @@ export default function VideoPlayer({
|
|
|
46
57
|
? {playback: signedToken, thumbnail: signedToken, storyboard: signedToken}
|
|
47
58
|
: undefined
|
|
48
59
|
}
|
|
49
|
-
streamType="on-demand"
|
|
50
60
|
preload="metadata"
|
|
51
61
|
crossOrigin="anonymous"
|
|
52
62
|
metadata={{
|
|
@@ -64,6 +74,24 @@ export default function VideoPlayer({
|
|
|
64
74
|
{children}
|
|
65
75
|
</>
|
|
66
76
|
)}
|
|
77
|
+
{error ? (
|
|
78
|
+
<div
|
|
79
|
+
style={{
|
|
80
|
+
position: 'absolute',
|
|
81
|
+
top: '50%',
|
|
82
|
+
left: '50%',
|
|
83
|
+
transform: 'translate(-50%, -50%)',
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
<Text muted>
|
|
87
|
+
<ErrorOutlineIcon style={{marginRight: '0.15em'}} />
|
|
88
|
+
{typeof error === 'object' && 'message' in error && typeof error.message === 'string'
|
|
89
|
+
? error.message
|
|
90
|
+
: 'Error loading video'}
|
|
91
|
+
</Text>
|
|
92
|
+
</div>
|
|
93
|
+
) : null}
|
|
94
|
+
{children}
|
|
67
95
|
</Card>
|
|
68
96
|
)
|
|
69
97
|
}
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import {ErrorOutlineIcon} from '@sanity/icons'
|
|
2
|
-
import {Card, CardTone, Stack, Text} from '@sanity/ui'
|
|
2
|
+
import {Box, Card, CardTone, Spinner, Stack, Text} from '@sanity/ui'
|
|
3
3
|
import React, {useMemo, useState} from 'react'
|
|
4
4
|
import styled from 'styled-components'
|
|
5
5
|
|
|
6
6
|
import {useClient} from '../hooks/useClient'
|
|
7
7
|
import useInView from '../hooks/useInView'
|
|
8
8
|
import {THUMBNAIL_ASPECT_RATIO} from '../util/constants'
|
|
9
|
-
import {getAnimatedPosterSrc} from '../util/getAnimatedPosterSrc'
|
|
9
|
+
import {getAnimatedPosterSrc, type AnimatedPosterSrcOptions} from '../util/getAnimatedPosterSrc'
|
|
10
10
|
import {VideoAssetDocument} from '../util/types'
|
|
11
|
-
import SpinnerBox from './SpinnerBox'
|
|
12
11
|
|
|
13
12
|
const Image = styled.img`
|
|
14
13
|
transition: opacity 0.175s ease-out 0s;
|
|
@@ -29,19 +28,20 @@ const STATUS_TO_TONE: Record<ImageStatus, CardTone> = {
|
|
|
29
28
|
|
|
30
29
|
export default function VideoThumbnail({
|
|
31
30
|
asset,
|
|
32
|
-
width
|
|
31
|
+
width,
|
|
33
32
|
}: {
|
|
34
|
-
asset:
|
|
33
|
+
asset: AnimatedPosterSrcOptions['asset'] & Pick<VideoAssetDocument, 'filename' | 'assetId'>
|
|
35
34
|
width?: number
|
|
36
35
|
}) {
|
|
37
36
|
const {inView, ref} = useInView()
|
|
37
|
+
const posterWidth = width || 250
|
|
38
38
|
|
|
39
39
|
const [status, setStatus] = useState<ImageStatus>('loading')
|
|
40
40
|
const client = useClient()
|
|
41
41
|
|
|
42
42
|
const animatedSrc = useMemo(() => {
|
|
43
43
|
try {
|
|
44
|
-
return getAnimatedPosterSrc({asset, client, width})
|
|
44
|
+
return getAnimatedPosterSrc({asset, client, width: posterWidth})
|
|
45
45
|
} catch {
|
|
46
46
|
if (status !== 'error') setStatus('error')
|
|
47
47
|
return undefined
|
|
@@ -61,6 +61,9 @@ export default function VideoThumbnail({
|
|
|
61
61
|
style={{
|
|
62
62
|
aspectRatio: THUMBNAIL_ASPECT_RATIO,
|
|
63
63
|
position: 'relative',
|
|
64
|
+
maxWidth: width ? `${width}px` : undefined,
|
|
65
|
+
width: '100%',
|
|
66
|
+
flex: 1,
|
|
64
67
|
}}
|
|
65
68
|
border
|
|
66
69
|
radius={2}
|
|
@@ -69,7 +72,18 @@ export default function VideoThumbnail({
|
|
|
69
72
|
>
|
|
70
73
|
{inView ? (
|
|
71
74
|
<>
|
|
72
|
-
{status === 'loading' &&
|
|
75
|
+
{status === 'loading' && (
|
|
76
|
+
<Box
|
|
77
|
+
style={{
|
|
78
|
+
position: 'absolute',
|
|
79
|
+
left: '50%',
|
|
80
|
+
top: '50%',
|
|
81
|
+
transform: 'translate(-50%, -50%)',
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
<Spinner />
|
|
85
|
+
</Box>
|
|
86
|
+
)}
|
|
73
87
|
{status === 'error' && (
|
|
74
88
|
<Stack
|
|
75
89
|
space={4}
|
|
@@ -4,10 +4,11 @@ import React from 'react'
|
|
|
4
4
|
|
|
5
5
|
import useAssets from '../hooks/useAssets'
|
|
6
6
|
import type {VideoAssetDocument} from '../util/types'
|
|
7
|
+
import ImportVideosFromMux from './ImportVideosFromMux'
|
|
7
8
|
import {SelectSortOptions} from './SelectSortOptions'
|
|
8
9
|
import SpinnerBox from './SpinnerBox'
|
|
9
|
-
import {VideoDetailsProps} from './VideoDetails/useVideoDetails'
|
|
10
10
|
import VideoDetails from './VideoDetails/VideoDetails'
|
|
11
|
+
import {VideoDetailsProps} from './VideoDetails/useVideoDetails'
|
|
11
12
|
import VideoInBrowser from './VideoInBrowser'
|
|
12
13
|
|
|
13
14
|
export interface VideosBrowserProps {
|
|
@@ -22,6 +23,7 @@ export default function VideosBrowser({onSelect}: VideosBrowserProps) {
|
|
|
22
23
|
[editedAsset, assets]
|
|
23
24
|
)
|
|
24
25
|
|
|
26
|
+
const placement = onSelect ? 'input' : 'tool'
|
|
25
27
|
return (
|
|
26
28
|
<>
|
|
27
29
|
<Stack padding={4} space={4} style={{minHeight: '50vh'}}>
|
|
@@ -37,6 +39,7 @@ export default function VideosBrowser({onSelect}: VideosBrowserProps) {
|
|
|
37
39
|
/>
|
|
38
40
|
<SelectSortOptions setSort={setSort} sort={sort} />
|
|
39
41
|
</Flex>
|
|
42
|
+
{placement === 'tool' && <ImportVideosFromMux />}
|
|
40
43
|
</Flex>
|
|
41
44
|
<Stack space={3}>
|
|
42
45
|
{assets?.length > 0 && (
|
|
@@ -64,7 +67,7 @@ export default function VideosBrowser({onSelect}: VideosBrowserProps) {
|
|
|
64
67
|
{isLoading && <SpinnerBox />}
|
|
65
68
|
|
|
66
69
|
{!isLoading && assets.length === 0 && (
|
|
67
|
-
<Card
|
|
70
|
+
<Card marginY={4} paddingX={4} paddingY={6} border radius={2} tone="transparent">
|
|
68
71
|
<Text align="center" muted size={3}>
|
|
69
72
|
{searchQuery ? `No videos found for "${searchQuery}"` : 'No videos in this dataset'}
|
|
70
73
|
</Text>
|
|
@@ -75,7 +78,7 @@ export default function VideosBrowser({onSelect}: VideosBrowserProps) {
|
|
|
75
78
|
<VideoDetails
|
|
76
79
|
closeDialog={() => setEditedAsset(null)}
|
|
77
80
|
asset={freshEditedAsset}
|
|
78
|
-
placement={
|
|
81
|
+
placement={placement}
|
|
79
82
|
/>
|
|
80
83
|
)}
|
|
81
84
|
</>
|