sanity-plugin-mux-input 2.1.1 → 2.2.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/lib/index.cjs +4037 -4
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.ts +14 -1
- package/lib/index.js +4026 -2
- package/lib/index.js.map +1 -1
- package/package.json +27 -28
- package/src/actions/assets.ts +30 -2
- package/src/components/ConfigureApi.tsx +9 -1
- package/src/components/FormField.tsx +8 -10
- package/src/components/IconInfo.tsx +23 -0
- package/src/components/Input.styled.tsx +0 -8
- package/src/components/Input.tsx +4 -3
- package/src/components/InputBrowser.tsx +1 -8
- package/src/components/Player.styled.tsx +5 -144
- package/src/components/Player.tsx +23 -109
- package/src/components/PlayerActionsMenu.tsx +0 -4
- package/src/components/SelectAsset.tsx +18 -58
- package/src/components/SelectSortOptions.tsx +45 -0
- package/src/components/SpinnerBox.tsx +17 -0
- package/src/components/StudioTool.tsx +20 -0
- package/src/components/VideoDetails/DeleteDialog.tsx +156 -0
- package/src/components/VideoDetails/VideoDetails.tsx +298 -0
- package/src/components/VideoDetails/VideoReferences.tsx +70 -0
- package/src/components/VideoDetails/useVideoDetails.ts +85 -0
- package/src/components/VideoInBrowser.tsx +183 -0
- package/src/components/VideoMetadata.tsx +43 -0
- package/src/components/VideoPlayer.tsx +69 -0
- package/src/components/VideoThumbnail.tsx +106 -0
- package/src/components/VideosBrowser.tsx +83 -0
- package/src/components/__legacy__Uploader.tsx +2 -9
- package/src/components/documentPreview/DocumentPreview.tsx +107 -0
- package/src/components/documentPreview/DraftStatus.tsx +34 -0
- package/src/components/documentPreview/MissingSchemaType.tsx +33 -0
- package/src/components/documentPreview/PaneItemPreview.tsx +71 -0
- package/src/components/documentPreview/PublishedStatus.tsx +35 -0
- package/src/components/documentPreview/TimeAgo.tsx +13 -0
- package/src/components/documentPreview/paneItemTypes.ts +7 -0
- package/src/components/icons/Resolution.tsx +12 -0
- package/src/components/icons/StopWatch.tsx +20 -0
- package/src/components/icons/ToolIcon.tsx +21 -0
- package/src/hooks/useAssets.ts +61 -0
- package/src/hooks/useCancelUpload.ts +2 -2
- package/src/hooks/useClient.ts +3 -1
- package/src/hooks/useDocReferences.ts +21 -0
- package/src/hooks/useInView.ts +45 -0
- package/src/index.ts +2 -0
- package/src/plugin.tsx +1 -1
- package/src/util/constants.ts +7 -0
- package/src/util/createSearchFilter.ts +78 -0
- package/src/util/formatSeconds.ts +22 -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/getVideoMetadata.ts +18 -0
- package/src/util/types.ts +16 -1
- package/lib/_chunks/Player-547f8e2a.cjs +0 -474
- package/lib/_chunks/Player-547f8e2a.cjs.map +0 -1
- package/lib/_chunks/Player-bfdb96f6.js +0 -465
- package/lib/_chunks/Player-bfdb96f6.js.map +0 -1
- package/lib/_chunks/index-39e38243.cjs +0 -3251
- package/lib/_chunks/index-39e38243.cjs.map +0 -1
- package/lib/_chunks/index-71899191.js +0 -3229
- package/lib/_chunks/index-71899191.js.map +0 -1
- package/src/components/EditThumbnailDialog.tsx +0 -74
- package/src/components/VideoSource.styled.tsx +0 -235
- package/src/components/VideoSource.tsx +0 -318
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type {SanityDocument} from '@sanity/client'
|
|
2
|
+
import {Box, Card, Text} from '@sanity/ui'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import {collate, useSchema} from 'sanity'
|
|
5
|
+
import styled from 'styled-components'
|
|
6
|
+
|
|
7
|
+
import {PluginPlacement} from '../../util/types'
|
|
8
|
+
import {DocumentPreview} from '../documentPreview/DocumentPreview'
|
|
9
|
+
import SpinnerBox from '../SpinnerBox'
|
|
10
|
+
|
|
11
|
+
const Container = styled(Box)`
|
|
12
|
+
* {
|
|
13
|
+
color: ${(props: any) => props.theme.sanity.color.base.fg};
|
|
14
|
+
}
|
|
15
|
+
a {
|
|
16
|
+
text-decoration: none;
|
|
17
|
+
}
|
|
18
|
+
h2 {
|
|
19
|
+
font-size: ${(props: any) => props.theme.sanity.fonts.text.sizes[1]};
|
|
20
|
+
}
|
|
21
|
+
`
|
|
22
|
+
|
|
23
|
+
const FileReferences: React.FC<{
|
|
24
|
+
references?: SanityDocument[]
|
|
25
|
+
isLoaded: boolean
|
|
26
|
+
placement: PluginPlacement
|
|
27
|
+
}> = (props) => {
|
|
28
|
+
const schema = useSchema()
|
|
29
|
+
if (!props.isLoaded) {
|
|
30
|
+
return <SpinnerBox />
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!props.references?.length) {
|
|
34
|
+
return (
|
|
35
|
+
<Text size={2} weight="bold" muted style={{marginTop: '1.5rem', textAlign: 'center'}}>
|
|
36
|
+
No documents are using this file
|
|
37
|
+
</Text>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const documentPairs = collate(props.references || [])
|
|
42
|
+
return (
|
|
43
|
+
<Container>
|
|
44
|
+
{documentPairs?.map((documentPair) => {
|
|
45
|
+
const schemaType = schema.get(documentPair.type)
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Card
|
|
49
|
+
key={documentPair.id}
|
|
50
|
+
marginBottom={2}
|
|
51
|
+
padding={2}
|
|
52
|
+
radius={2}
|
|
53
|
+
shadow={1}
|
|
54
|
+
style={{overflow: 'hidden'}}
|
|
55
|
+
>
|
|
56
|
+
<Box>
|
|
57
|
+
<DocumentPreview
|
|
58
|
+
documentPair={documentPair}
|
|
59
|
+
schemaType={schemaType}
|
|
60
|
+
placement={props.placement}
|
|
61
|
+
/>
|
|
62
|
+
</Box>
|
|
63
|
+
</Card>
|
|
64
|
+
)
|
|
65
|
+
})}
|
|
66
|
+
</Container>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default FileReferences
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {useToast} from '@sanity/ui'
|
|
2
|
+
import {useState} from 'react'
|
|
3
|
+
import {useDocumentStore} from 'sanity'
|
|
4
|
+
|
|
5
|
+
import {useClient} from '../../hooks/useClient'
|
|
6
|
+
import useDocReferences from '../../hooks/useDocReferences'
|
|
7
|
+
import getVideoMetadata from '../../util/getVideoMetadata'
|
|
8
|
+
import {PluginPlacement, VideoAssetDocument} from '../../util/types'
|
|
9
|
+
|
|
10
|
+
export interface FileDetailsProps {
|
|
11
|
+
placement: PluginPlacement
|
|
12
|
+
closeDialog: () => void
|
|
13
|
+
asset: VideoAssetDocument & {autoPlay?: boolean}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function useFileDetails(props: FileDetailsProps) {
|
|
17
|
+
const documentStore = useDocumentStore()
|
|
18
|
+
const toast = useToast()
|
|
19
|
+
const client = useClient()
|
|
20
|
+
const [references, referencesLoading] = useDocReferences({
|
|
21
|
+
documentStore,
|
|
22
|
+
id: props.asset._id as string,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const [originalAsset, setOriginalAsset] = useState(() => props.asset)
|
|
26
|
+
const [filename, setFilename] = useState(props.asset.filename)
|
|
27
|
+
const modified = filename !== originalAsset.filename
|
|
28
|
+
|
|
29
|
+
const displayInfo = getVideoMetadata({...props.asset, filename})
|
|
30
|
+
|
|
31
|
+
const [state, setState] = useState<'deleting' | 'closing' | 'idle' | 'saving'>('idle')
|
|
32
|
+
|
|
33
|
+
function handleClose() {
|
|
34
|
+
if (state !== 'idle') return
|
|
35
|
+
|
|
36
|
+
if (modified) {
|
|
37
|
+
setState('closing')
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
props.closeDialog()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function confirmClose(shouldClose: boolean) {
|
|
45
|
+
if (state !== 'closing') return
|
|
46
|
+
|
|
47
|
+
if (shouldClose) props.closeDialog()
|
|
48
|
+
|
|
49
|
+
setState('idle')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function saveChanges() {
|
|
53
|
+
if (state !== 'idle') return
|
|
54
|
+
setState('saving')
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
await client.patch(props.asset._id).set({filename}).commit()
|
|
58
|
+
setOriginalAsset((prev) => ({...prev, filename}))
|
|
59
|
+
toast.push({title: 'File name updated', status: 'success'})
|
|
60
|
+
} catch (error) {
|
|
61
|
+
toast.push({
|
|
62
|
+
title: 'Failed updating file name',
|
|
63
|
+
status: 'error',
|
|
64
|
+
description: typeof error === 'string' ? error : 'Please try again',
|
|
65
|
+
})
|
|
66
|
+
setFilename(originalAsset.filename)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
setState('idle')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
references,
|
|
74
|
+
referencesLoading,
|
|
75
|
+
modified,
|
|
76
|
+
filename,
|
|
77
|
+
setFilename,
|
|
78
|
+
displayInfo,
|
|
79
|
+
state,
|
|
80
|
+
setState,
|
|
81
|
+
handleClose,
|
|
82
|
+
confirmClose,
|
|
83
|
+
saveChanges,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import {CheckmarkIcon, EditIcon, LockIcon, PlayIcon} from '@sanity/icons'
|
|
2
|
+
import {Button, Card, Stack, Text, Tooltip} from '@sanity/ui'
|
|
3
|
+
import React, {useState} from 'react'
|
|
4
|
+
import styled from 'styled-components'
|
|
5
|
+
|
|
6
|
+
import {THUMBNAIL_ASPECT_RATIO} from '../util/constants'
|
|
7
|
+
import {getPlaybackPolicy} from '../util/getPlaybackPolicy'
|
|
8
|
+
import {VideoAssetDocument} from '../util/types'
|
|
9
|
+
import IconInfo from './IconInfo'
|
|
10
|
+
import VideoMetadata from './VideoMetadata'
|
|
11
|
+
import VideoPlayer from './VideoPlayer'
|
|
12
|
+
import VideoThumbnail from './VideoThumbnail'
|
|
13
|
+
|
|
14
|
+
const PlayButton = styled.button`
|
|
15
|
+
display: block;
|
|
16
|
+
padding: 0;
|
|
17
|
+
margin: 0;
|
|
18
|
+
border: none;
|
|
19
|
+
border-radius: 0.1875rem;
|
|
20
|
+
position: relative;
|
|
21
|
+
cursor: pointer;
|
|
22
|
+
|
|
23
|
+
&::after {
|
|
24
|
+
content: '';
|
|
25
|
+
background: var(--card-fg-color);
|
|
26
|
+
opacity: 0;
|
|
27
|
+
display: block;
|
|
28
|
+
position: absolute;
|
|
29
|
+
inset: 0;
|
|
30
|
+
z-index: 10;
|
|
31
|
+
transition: 0.15s ease-out;
|
|
32
|
+
border-radius: inherit;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
> div[data-play] {
|
|
36
|
+
z-index: 11;
|
|
37
|
+
opacity: 0;
|
|
38
|
+
transition: 0.15s 0.05s ease-out;
|
|
39
|
+
position: absolute;
|
|
40
|
+
left: 50%;
|
|
41
|
+
top: 50%;
|
|
42
|
+
transform: translate(-50%, -50%);
|
|
43
|
+
color: var(--card-fg-color);
|
|
44
|
+
background: var(--card-bg-color);
|
|
45
|
+
width: auto;
|
|
46
|
+
height: 30%;
|
|
47
|
+
aspect-ratio: 1;
|
|
48
|
+
border-radius: 100%;
|
|
49
|
+
display: flex;
|
|
50
|
+
justify-content: center;
|
|
51
|
+
align-items: center;
|
|
52
|
+
box-sizing: border-box;
|
|
53
|
+
> svg {
|
|
54
|
+
display: block;
|
|
55
|
+
width: 70%;
|
|
56
|
+
height: auto;
|
|
57
|
+
// Visual balance to center-align the icon
|
|
58
|
+
transform: translateX(5%);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
&:hover,
|
|
63
|
+
&:focus {
|
|
64
|
+
&::after {
|
|
65
|
+
opacity: 0.3;
|
|
66
|
+
}
|
|
67
|
+
> div[data-play] {
|
|
68
|
+
opacity: 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
`
|
|
72
|
+
|
|
73
|
+
export default function VideoInBrowser({
|
|
74
|
+
onSelect,
|
|
75
|
+
onEdit,
|
|
76
|
+
asset,
|
|
77
|
+
}: {
|
|
78
|
+
onSelect?: (asset: VideoAssetDocument) => void
|
|
79
|
+
onEdit?: (asset: VideoAssetDocument) => void
|
|
80
|
+
asset: VideoAssetDocument
|
|
81
|
+
}) {
|
|
82
|
+
const [renderVideo, setRenderVideo] = useState(false)
|
|
83
|
+
const select = React.useCallback(() => onSelect?.(asset), [onSelect, asset])
|
|
84
|
+
const edit = React.useCallback(() => onEdit?.(asset), [onEdit, asset])
|
|
85
|
+
|
|
86
|
+
if (!asset) {
|
|
87
|
+
return null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const playbackPolicy = getPlaybackPolicy(asset)
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<Card
|
|
94
|
+
border
|
|
95
|
+
padding={2}
|
|
96
|
+
sizing="border"
|
|
97
|
+
radius={2}
|
|
98
|
+
style={{
|
|
99
|
+
position: 'relative',
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
{playbackPolicy === 'signed' && (
|
|
103
|
+
<Tooltip
|
|
104
|
+
content={
|
|
105
|
+
<Card padding={2} radius={2}>
|
|
106
|
+
<IconInfo icon={LockIcon} text="Signed playback policy" size={2} />
|
|
107
|
+
</Card>
|
|
108
|
+
}
|
|
109
|
+
placement="right"
|
|
110
|
+
fallbackPlacements={['top', 'bottom']}
|
|
111
|
+
portal
|
|
112
|
+
>
|
|
113
|
+
<Card
|
|
114
|
+
tone="caution"
|
|
115
|
+
style={{
|
|
116
|
+
borderRadius: '100%',
|
|
117
|
+
position: 'absolute',
|
|
118
|
+
left: '1em',
|
|
119
|
+
top: '1em',
|
|
120
|
+
zIndex: 10,
|
|
121
|
+
}}
|
|
122
|
+
padding={2}
|
|
123
|
+
border
|
|
124
|
+
>
|
|
125
|
+
<Text muted size={1}>
|
|
126
|
+
<LockIcon />
|
|
127
|
+
</Text>
|
|
128
|
+
</Card>
|
|
129
|
+
</Tooltip>
|
|
130
|
+
)}
|
|
131
|
+
<Stack
|
|
132
|
+
space={3}
|
|
133
|
+
height="fill"
|
|
134
|
+
style={{
|
|
135
|
+
gridTemplateRows: 'min-content min-content 1fr',
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
{renderVideo ? (
|
|
139
|
+
<VideoPlayer asset={asset} autoPlay forceAspectRatio={THUMBNAIL_ASPECT_RATIO} />
|
|
140
|
+
) : (
|
|
141
|
+
<PlayButton onClick={() => setRenderVideo(true)}>
|
|
142
|
+
<div data-play>
|
|
143
|
+
<PlayIcon />
|
|
144
|
+
</div>
|
|
145
|
+
<VideoThumbnail asset={asset} />
|
|
146
|
+
</PlayButton>
|
|
147
|
+
)}
|
|
148
|
+
<VideoMetadata asset={asset} />
|
|
149
|
+
<div
|
|
150
|
+
style={{
|
|
151
|
+
display: 'flex',
|
|
152
|
+
width: '100%',
|
|
153
|
+
alignItems: 'flex-end',
|
|
154
|
+
justifyContent: 'flex-start',
|
|
155
|
+
gap: '.35rem',
|
|
156
|
+
}}
|
|
157
|
+
>
|
|
158
|
+
{onSelect && (
|
|
159
|
+
<Button
|
|
160
|
+
icon={CheckmarkIcon}
|
|
161
|
+
fontSize={2}
|
|
162
|
+
padding={2}
|
|
163
|
+
mode="ghost"
|
|
164
|
+
text="Select"
|
|
165
|
+
style={{flex: 1}}
|
|
166
|
+
tone="positive"
|
|
167
|
+
onClick={select}
|
|
168
|
+
/>
|
|
169
|
+
)}
|
|
170
|
+
<Button
|
|
171
|
+
icon={EditIcon}
|
|
172
|
+
fontSize={2}
|
|
173
|
+
padding={2}
|
|
174
|
+
mode="ghost"
|
|
175
|
+
text="Details"
|
|
176
|
+
style={{flex: 1}}
|
|
177
|
+
onClick={edit}
|
|
178
|
+
/>
|
|
179
|
+
</div>
|
|
180
|
+
</Stack>
|
|
181
|
+
</Card>
|
|
182
|
+
)
|
|
183
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {CalendarIcon, ClockIcon} from '@sanity/icons'
|
|
2
|
+
import {Inline, Stack, Text} from '@sanity/ui'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
|
|
5
|
+
import getVideoMetadata from '../util/getVideoMetadata'
|
|
6
|
+
import {VideoAssetDocument} from '../util/types'
|
|
7
|
+
import IconInfo from './IconInfo'
|
|
8
|
+
|
|
9
|
+
const VideoMetadata = (props: {asset: VideoAssetDocument}) => {
|
|
10
|
+
if (!props.asset) {
|
|
11
|
+
return null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const displayInfo = getVideoMetadata(props.asset)
|
|
15
|
+
return (
|
|
16
|
+
<Stack space={2}>
|
|
17
|
+
{displayInfo.title && (
|
|
18
|
+
<Text
|
|
19
|
+
size={1}
|
|
20
|
+
weight="semibold"
|
|
21
|
+
style={{
|
|
22
|
+
wordWrap: 'break-word',
|
|
23
|
+
}}
|
|
24
|
+
>
|
|
25
|
+
{displayInfo.title}
|
|
26
|
+
</Text>
|
|
27
|
+
)}
|
|
28
|
+
<Inline space={3}>
|
|
29
|
+
{displayInfo?.duration && (
|
|
30
|
+
<IconInfo text={displayInfo.duration} icon={ClockIcon} size={1} muted />
|
|
31
|
+
)}
|
|
32
|
+
<IconInfo
|
|
33
|
+
text={displayInfo.createdAt.toISOString().split('T')[0]}
|
|
34
|
+
icon={CalendarIcon}
|
|
35
|
+
size={1}
|
|
36
|
+
muted
|
|
37
|
+
/>
|
|
38
|
+
</Inline>
|
|
39
|
+
</Stack>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default VideoMetadata
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import MuxPlayer, {MuxPlayerProps} from '@mux/mux-player-react'
|
|
2
|
+
import {Card} from '@sanity/ui'
|
|
3
|
+
import React, {PropsWithChildren, useMemo} from 'react'
|
|
4
|
+
|
|
5
|
+
import {useClient} from '../hooks/useClient'
|
|
6
|
+
import {MIN_ASPECT_RATIO} from '../util/constants'
|
|
7
|
+
import {getVideoSrc} from '../util/getVideoSrc'
|
|
8
|
+
import type {VideoAssetDocument} from '../util/types'
|
|
9
|
+
import pluginPkg from './../../package.json'
|
|
10
|
+
|
|
11
|
+
export default function VideoPlayer({
|
|
12
|
+
asset,
|
|
13
|
+
children,
|
|
14
|
+
...props
|
|
15
|
+
}: PropsWithChildren<
|
|
16
|
+
{asset: VideoAssetDocument; forceAspectRatio?: number} & Partial<Pick<MuxPlayerProps, 'autoPlay'>>
|
|
17
|
+
>) {
|
|
18
|
+
const client = useClient()
|
|
19
|
+
|
|
20
|
+
const videoSrc = useMemo(() => asset?.playbackId && getVideoSrc({client, asset}), [asset, client])
|
|
21
|
+
|
|
22
|
+
const signedToken = useMemo(() => {
|
|
23
|
+
try {
|
|
24
|
+
const url = new URL(videoSrc!)
|
|
25
|
+
return url.searchParams.get('token')
|
|
26
|
+
} catch {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
}, [videoSrc])
|
|
30
|
+
|
|
31
|
+
const [width, height] = (asset?.data?.aspect_ratio ?? '16:9').split(':').map(Number)
|
|
32
|
+
const targetAspectRatio =
|
|
33
|
+
props.forceAspectRatio || (Number.isNaN(width) ? 16 / 9 : width / height)
|
|
34
|
+
const aspectRatio = Math.max(MIN_ASPECT_RATIO, targetAspectRatio)
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<Card tone="transparent" style={{aspectRatio: aspectRatio, position: 'relative'}}>
|
|
38
|
+
{videoSrc && (
|
|
39
|
+
<>
|
|
40
|
+
<MuxPlayer
|
|
41
|
+
{...props}
|
|
42
|
+
playsInline
|
|
43
|
+
playbackId={asset.playbackId}
|
|
44
|
+
tokens={
|
|
45
|
+
signedToken
|
|
46
|
+
? {playback: signedToken, thumbnail: signedToken, storyboard: signedToken}
|
|
47
|
+
: undefined
|
|
48
|
+
}
|
|
49
|
+
streamType="on-demand"
|
|
50
|
+
preload="metadata"
|
|
51
|
+
crossOrigin="anonymous"
|
|
52
|
+
metadata={{
|
|
53
|
+
player_name: 'Sanity Admin Dashboard',
|
|
54
|
+
player_version: pluginPkg.version,
|
|
55
|
+
page_type: 'Preview Player',
|
|
56
|
+
}}
|
|
57
|
+
style={{
|
|
58
|
+
height: '100%',
|
|
59
|
+
width: '100%',
|
|
60
|
+
display: 'block',
|
|
61
|
+
objectFit: 'contain',
|
|
62
|
+
}}
|
|
63
|
+
/>
|
|
64
|
+
{children}
|
|
65
|
+
</>
|
|
66
|
+
)}
|
|
67
|
+
</Card>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import {ErrorOutlineIcon} from '@sanity/icons'
|
|
2
|
+
import {Card, CardTone, Stack, Text} from '@sanity/ui'
|
|
3
|
+
import React, {useMemo, useState} from 'react'
|
|
4
|
+
import styled from 'styled-components'
|
|
5
|
+
|
|
6
|
+
import {useClient} from '../hooks/useClient'
|
|
7
|
+
import useInView from '../hooks/useInView'
|
|
8
|
+
import {THUMBNAIL_ASPECT_RATIO} from '../util/constants'
|
|
9
|
+
import {getAnimatedPosterSrc} from '../util/getAnimatedPosterSrc'
|
|
10
|
+
import {VideoAssetDocument} from '../util/types'
|
|
11
|
+
import SpinnerBox from './SpinnerBox'
|
|
12
|
+
|
|
13
|
+
const Image = styled.img`
|
|
14
|
+
transition: opacity 0.175s ease-out 0s;
|
|
15
|
+
display: block;
|
|
16
|
+
width: 100%;
|
|
17
|
+
height: 100%;
|
|
18
|
+
object-fit: contain;
|
|
19
|
+
object-position: center center;
|
|
20
|
+
`
|
|
21
|
+
|
|
22
|
+
type ImageStatus = 'loading' | 'error' | 'loaded'
|
|
23
|
+
|
|
24
|
+
const STATUS_TO_TONE: Record<ImageStatus, CardTone> = {
|
|
25
|
+
loading: 'transparent',
|
|
26
|
+
error: 'critical',
|
|
27
|
+
loaded: 'default',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default function VideoThumbnail({
|
|
31
|
+
asset,
|
|
32
|
+
width = 250,
|
|
33
|
+
}: {
|
|
34
|
+
asset: Partial<VideoAssetDocument>
|
|
35
|
+
width?: number
|
|
36
|
+
}) {
|
|
37
|
+
const {inView, ref} = useInView()
|
|
38
|
+
|
|
39
|
+
const [status, setStatus] = useState<ImageStatus>('loading')
|
|
40
|
+
const client = useClient()
|
|
41
|
+
|
|
42
|
+
const animatedSrc = useMemo(() => {
|
|
43
|
+
try {
|
|
44
|
+
return getAnimatedPosterSrc({asset, client, width})
|
|
45
|
+
} catch {
|
|
46
|
+
if (status !== 'error') setStatus('error')
|
|
47
|
+
return undefined
|
|
48
|
+
}
|
|
49
|
+
}, [asset, client, width, status, setStatus])
|
|
50
|
+
|
|
51
|
+
function handleLoad() {
|
|
52
|
+
setStatus('loaded')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function handleError() {
|
|
56
|
+
setStatus('error')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Card
|
|
61
|
+
style={{
|
|
62
|
+
aspectRatio: THUMBNAIL_ASPECT_RATIO,
|
|
63
|
+
position: 'relative',
|
|
64
|
+
}}
|
|
65
|
+
border
|
|
66
|
+
radius={2}
|
|
67
|
+
ref={ref as any}
|
|
68
|
+
tone={STATUS_TO_TONE[status]}
|
|
69
|
+
>
|
|
70
|
+
{inView ? (
|
|
71
|
+
<>
|
|
72
|
+
{status === 'loading' && <SpinnerBox />}
|
|
73
|
+
{status === 'error' && (
|
|
74
|
+
<Stack
|
|
75
|
+
space={4}
|
|
76
|
+
style={{
|
|
77
|
+
position: 'absolute',
|
|
78
|
+
width: '100%',
|
|
79
|
+
left: 0,
|
|
80
|
+
top: '50%',
|
|
81
|
+
transform: 'translateY(-50%)',
|
|
82
|
+
justifyItems: 'center',
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
<Text size={4} muted>
|
|
86
|
+
<ErrorOutlineIcon style={{fontSize: '1.75em'}} />
|
|
87
|
+
</Text>
|
|
88
|
+
<Text muted align="center">
|
|
89
|
+
Failed loading thumbnail
|
|
90
|
+
</Text>
|
|
91
|
+
</Stack>
|
|
92
|
+
)}
|
|
93
|
+
<Image
|
|
94
|
+
src={animatedSrc}
|
|
95
|
+
alt={`Preview for video ${asset.filename || asset.assetId}`}
|
|
96
|
+
onLoad={handleLoad}
|
|
97
|
+
onError={handleError}
|
|
98
|
+
style={{
|
|
99
|
+
opacity: status === 'loaded' ? 1 : 0,
|
|
100
|
+
}}
|
|
101
|
+
/>
|
|
102
|
+
</>
|
|
103
|
+
) : null}
|
|
104
|
+
</Card>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {SearchIcon} from '@sanity/icons'
|
|
2
|
+
import {Card, Flex, Grid, Label, Stack, Text, TextInput} from '@sanity/ui'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
|
|
5
|
+
import useAssets from '../hooks/useAssets'
|
|
6
|
+
import type {VideoAssetDocument} from '../util/types'
|
|
7
|
+
import {SelectSortOptions} from './SelectSortOptions'
|
|
8
|
+
import SpinnerBox from './SpinnerBox'
|
|
9
|
+
import {FileDetailsProps} from './VideoDetails/useVideoDetails'
|
|
10
|
+
import VideoDetails from './VideoDetails/VideoDetails'
|
|
11
|
+
import VideoInBrowser from './VideoInBrowser'
|
|
12
|
+
|
|
13
|
+
export interface VideosBrowserProps {
|
|
14
|
+
onSelect?: (asset: VideoAssetDocument) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function VideosBrowser({onSelect}: VideosBrowserProps) {
|
|
18
|
+
const {assets, isLoading, searchQuery, setSearchQuery, setSort, sort} = useAssets()
|
|
19
|
+
const [editedAsset, setEditedAsset] = React.useState<FileDetailsProps['asset'] | null>(null)
|
|
20
|
+
const freshEditedAsset = React.useMemo(
|
|
21
|
+
() => assets.find((a) => a._id === editedAsset?._id) || editedAsset,
|
|
22
|
+
[editedAsset, assets]
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<>
|
|
27
|
+
<Stack padding={4} space={4}>
|
|
28
|
+
<Flex justify="space-between" align="center">
|
|
29
|
+
<Flex align="center" gap={3}>
|
|
30
|
+
<TextInput
|
|
31
|
+
value={searchQuery}
|
|
32
|
+
icon={SearchIcon}
|
|
33
|
+
onInput={(e: React.FormEvent<HTMLInputElement>) =>
|
|
34
|
+
setSearchQuery(e.currentTarget.value)
|
|
35
|
+
}
|
|
36
|
+
placeholder="Search files"
|
|
37
|
+
/>
|
|
38
|
+
<SelectSortOptions setSort={setSort} sort={sort} />
|
|
39
|
+
</Flex>
|
|
40
|
+
</Flex>
|
|
41
|
+
<Stack space={3}>
|
|
42
|
+
{assets?.length > 0 && (
|
|
43
|
+
<Label muted>
|
|
44
|
+
{assets.length} video{assets.length > 1 ? 's' : null}{' '}
|
|
45
|
+
{searchQuery ? `matching "${searchQuery}"` : 'found'}
|
|
46
|
+
</Label>
|
|
47
|
+
)}
|
|
48
|
+
<Grid
|
|
49
|
+
gap={2}
|
|
50
|
+
style={{
|
|
51
|
+
gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))',
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
{assets.map((asset) => (
|
|
55
|
+
<VideoInBrowser
|
|
56
|
+
key={asset._id}
|
|
57
|
+
asset={asset}
|
|
58
|
+
onEdit={setEditedAsset}
|
|
59
|
+
onSelect={onSelect}
|
|
60
|
+
/>
|
|
61
|
+
))}
|
|
62
|
+
</Grid>
|
|
63
|
+
</Stack>
|
|
64
|
+
{isLoading && <SpinnerBox />}
|
|
65
|
+
|
|
66
|
+
{!isLoading && assets.length === 0 && (
|
|
67
|
+
<Card padding={2} marginY={4} border radius={2}>
|
|
68
|
+
<Text align="center" muted>
|
|
69
|
+
{searchQuery ? `No videos found for "${searchQuery}"` : 'No videos in this dataset'}
|
|
70
|
+
</Text>
|
|
71
|
+
</Card>
|
|
72
|
+
)}
|
|
73
|
+
</Stack>
|
|
74
|
+
{freshEditedAsset && (
|
|
75
|
+
<VideoDetails
|
|
76
|
+
closeDialog={() => setEditedAsset(null)}
|
|
77
|
+
asset={freshEditedAsset}
|
|
78
|
+
placement={onSelect ? 'input' : 'tool'}
|
|
79
|
+
/>
|
|
80
|
+
)}
|
|
81
|
+
</>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* eslint-disable no-nested-ternary */
|
|
2
2
|
// This component needs to be refactored into a functional component
|
|
3
3
|
|
|
4
|
-
import React, {
|
|
4
|
+
import React, {Component} from 'react'
|
|
5
5
|
import {type Observable, Subject} from 'rxjs'
|
|
6
6
|
import {takeUntil, tap} from 'rxjs/operators'
|
|
7
7
|
import type {SanityClient} from 'sanity'
|
|
@@ -12,17 +12,12 @@ import {type DialogState, type SetDialogState} from '../hooks/useDialogState'
|
|
|
12
12
|
import {extractDroppedFiles} from '../util/extractFiles'
|
|
13
13
|
import type {Config, MuxInputProps, Secrets, VideoAssetDocument} from '../util/types'
|
|
14
14
|
import InputBrowser from './InputBrowser'
|
|
15
|
+
import Player from './Player'
|
|
15
16
|
import PlayerActionsMenu from './PlayerActionsMenu'
|
|
16
17
|
import {UploadCard} from './Uploader.styled'
|
|
17
18
|
import UploadPlaceholder from './UploadPlaceholder'
|
|
18
19
|
import {UploadProgress} from './UploadProgress'
|
|
19
20
|
|
|
20
|
-
// TODO: Without this lazy load call a build error occurs in the Player component from the import
|
|
21
|
-
// of media-chrome components. media-chrome ships separate ESM and CJS compatible modules in versions >=1.0.0.
|
|
22
|
-
// Once the plugin is updated to media-chrome >= 1.0.0, remove this lazy load.
|
|
23
|
-
// import Player from './Player'
|
|
24
|
-
const Player = lazy(() => import('./Player'))
|
|
25
|
-
|
|
26
21
|
interface Props extends Pick<MuxInputProps, 'onChange' | 'readOnly'> {
|
|
27
22
|
config: Config
|
|
28
23
|
client: SanityClient
|
|
@@ -249,8 +244,6 @@ class MuxVideoInputUploader extends Component<Props, State> {
|
|
|
249
244
|
readOnly={this.props.readOnly}
|
|
250
245
|
asset={this.props.asset}
|
|
251
246
|
onChange={this.props.onChange}
|
|
252
|
-
dialogState={this.props.dialogState}
|
|
253
|
-
setDialogState={this.props.setDialogState}
|
|
254
247
|
buttons={
|
|
255
248
|
<PlayerActionsMenu
|
|
256
249
|
asset={this.props.asset}
|