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,107 @@
|
|
|
1
|
+
// Adapted from https://github.com/sanity-io/sanity/blob/next/packages/sanity/src/desk/components/paneItem/PaneItem.tsx
|
|
2
|
+
|
|
3
|
+
import {DocumentIcon} from '@sanity/icons'
|
|
4
|
+
import type {PropsWithChildren} from 'react'
|
|
5
|
+
import React, {useMemo} from 'react'
|
|
6
|
+
import type {SanityDocument} from 'sanity'
|
|
7
|
+
import type {CollatedHit, FIXME, SchemaType} from 'sanity'
|
|
8
|
+
import {PreviewCard, useDocumentPresence, useDocumentPreviewStore, useSchema} from 'sanity'
|
|
9
|
+
import {usePaneRouter} from 'sanity/desk'
|
|
10
|
+
import {IntentLink} from 'sanity/router'
|
|
11
|
+
|
|
12
|
+
import {PluginPlacement} from '../../util/types'
|
|
13
|
+
import {MissingSchemaType} from './MissingSchemaType'
|
|
14
|
+
import {PaneItemPreview} from './PaneItemPreview'
|
|
15
|
+
|
|
16
|
+
interface DocumentPreviewProps {
|
|
17
|
+
schemaType?: SchemaType
|
|
18
|
+
documentPair: CollatedHit<SanityDocument>
|
|
19
|
+
placement?: PluginPlacement
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Return `false` if we explicitly disable the icon.
|
|
24
|
+
* Otherwise return the passed icon or the schema type icon as a backup.
|
|
25
|
+
*/
|
|
26
|
+
export function getIconWithFallback(
|
|
27
|
+
icon: React.ComponentType<any> | false | undefined,
|
|
28
|
+
schemaType: SchemaType | undefined,
|
|
29
|
+
defaultIcon: React.ComponentType<any>
|
|
30
|
+
): React.ComponentType<any> | false {
|
|
31
|
+
if (icon === false) {
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return icon || ((schemaType && schemaType.icon) as any) || defaultIcon || false
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** When inside the field input, we can open the reference on a child pane */
|
|
39
|
+
function DocumentPreviewInInput(props: PropsWithChildren<DocumentPreviewProps>) {
|
|
40
|
+
const {ChildLink} = usePaneRouter()
|
|
41
|
+
|
|
42
|
+
return (linkProps: PropsWithChildren) => (
|
|
43
|
+
<ChildLink
|
|
44
|
+
childId={props.documentPair.id}
|
|
45
|
+
// Pass the schemaType of the document so `paneChild` in `buildPagesStructure` can access it
|
|
46
|
+
childParameters={{type: props.documentPair.type}}
|
|
47
|
+
>
|
|
48
|
+
{linkProps.children}
|
|
49
|
+
</ChildLink>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** When inside the tool, we must use a regular intent link to take users to the desk tool */
|
|
54
|
+
function DocumentPreviewInRool(props: DocumentPreviewProps) {
|
|
55
|
+
return (linkProps: PropsWithChildren) => (
|
|
56
|
+
<IntentLink intent="edit" params={{id: props.documentPair.id}}>
|
|
57
|
+
{linkProps.children}
|
|
58
|
+
</IntentLink>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function DocumentPreview(props: DocumentPreviewProps) {
|
|
63
|
+
const {schemaType, documentPair} = props
|
|
64
|
+
const doc = documentPair?.draft || documentPair?.published
|
|
65
|
+
const id = documentPair.id || ''
|
|
66
|
+
const documentPreviewStore = useDocumentPreviewStore()
|
|
67
|
+
const schema = useSchema()
|
|
68
|
+
const documentPresence = useDocumentPresence(id)
|
|
69
|
+
const hasSchemaType = Boolean(schemaType && schemaType.name && schema.get(schemaType.name))
|
|
70
|
+
|
|
71
|
+
const PreviewComponent = useMemo(() => {
|
|
72
|
+
if (!doc) return null
|
|
73
|
+
|
|
74
|
+
if (!schemaType || !hasSchemaType) {
|
|
75
|
+
return <MissingSchemaType value={doc as SanityDocument} />
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<PaneItemPreview
|
|
80
|
+
documentPreviewStore={documentPreviewStore}
|
|
81
|
+
icon={getIconWithFallback(undefined, schemaType, DocumentIcon)}
|
|
82
|
+
schemaType={schemaType}
|
|
83
|
+
layout="default"
|
|
84
|
+
value={doc}
|
|
85
|
+
presence={documentPresence}
|
|
86
|
+
/>
|
|
87
|
+
)
|
|
88
|
+
}, [hasSchemaType, schemaType, documentPresence, doc, documentPreviewStore])
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<PreviewCard
|
|
92
|
+
__unstable_focusRing
|
|
93
|
+
as={
|
|
94
|
+
(props.placement === 'input'
|
|
95
|
+
? DocumentPreviewInInput(props)
|
|
96
|
+
: DocumentPreviewInRool(props)) as FIXME
|
|
97
|
+
}
|
|
98
|
+
data-as="a"
|
|
99
|
+
data-ui="PaneItem"
|
|
100
|
+
padding={2}
|
|
101
|
+
radius={2}
|
|
102
|
+
tone="inherit"
|
|
103
|
+
>
|
|
104
|
+
{PreviewComponent}
|
|
105
|
+
</PreviewCard>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Adapted from https://github.com/sanity-io/sanity/blob/next/packages/sanity/src/desk/components/DraftStatus.tsx
|
|
2
|
+
import {EditIcon} from '@sanity/icons'
|
|
3
|
+
import {Box, Text, Tooltip} from '@sanity/ui'
|
|
4
|
+
import React from 'react'
|
|
5
|
+
import type {PreviewValue, SanityDocument} from 'sanity'
|
|
6
|
+
import {TextWithTone} from 'sanity'
|
|
7
|
+
|
|
8
|
+
import {TimeAgo} from './TimeAgo'
|
|
9
|
+
|
|
10
|
+
export function DraftStatus(props: {document?: PreviewValue | Partial<SanityDocument> | null}) {
|
|
11
|
+
const {document} = props
|
|
12
|
+
const updatedAt = document && '_updatedAt' in document && document._updatedAt
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Tooltip
|
|
16
|
+
portal
|
|
17
|
+
content={
|
|
18
|
+
<Box padding={2}>
|
|
19
|
+
<Text size={1}>
|
|
20
|
+
{document ? (
|
|
21
|
+
<>Edited {updatedAt && <TimeAgo time={updatedAt} />}</>
|
|
22
|
+
) : (
|
|
23
|
+
<>No unpublished edits</>
|
|
24
|
+
)}
|
|
25
|
+
</Text>
|
|
26
|
+
</Box>
|
|
27
|
+
}
|
|
28
|
+
>
|
|
29
|
+
<TextWithTone tone="caution" dimmed={!document} muted={!document} size={1}>
|
|
30
|
+
<EditIcon />
|
|
31
|
+
</TextWithTone>
|
|
32
|
+
</Tooltip>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Adapted from https://github.com/sanity-io/sanity/blob/next/packages/sanity/src/desk/components/MissingSchemaType.tsx
|
|
2
|
+
import {WarningOutlineIcon} from '@sanity/icons'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import type {SanityDocument} from 'sanity'
|
|
5
|
+
import type {GeneralPreviewLayoutKey} from 'sanity'
|
|
6
|
+
import {SanityDefaultPreview} from 'sanity'
|
|
7
|
+
|
|
8
|
+
export interface MissingSchemaTypeProps {
|
|
9
|
+
layout?: GeneralPreviewLayoutKey
|
|
10
|
+
value: SanityDocument
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const getUnknownTypeFallback = (id: string, typeName: string) => ({
|
|
14
|
+
title: (
|
|
15
|
+
<em>
|
|
16
|
+
No schema found for type <code>{typeName}</code>
|
|
17
|
+
</em>
|
|
18
|
+
),
|
|
19
|
+
subtitle: (
|
|
20
|
+
<em>
|
|
21
|
+
Document: <code>{id}</code>
|
|
22
|
+
</em>
|
|
23
|
+
),
|
|
24
|
+
media: () => <WarningOutlineIcon />,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export function MissingSchemaType(props: MissingSchemaTypeProps) {
|
|
28
|
+
const {layout, value} = props
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<SanityDefaultPreview {...getUnknownTypeFallback(value._id, value._type)} layout={layout} />
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Adapted from:
|
|
2
|
+
// https://github.com/sanity-io/sanity/blob/next/packages/sanity/src/desk/components/paneItem/PaneItemPreview.tsx
|
|
3
|
+
import {Inline} from '@sanity/ui'
|
|
4
|
+
import {isNumber, isString} from 'lodash'
|
|
5
|
+
import React, {isValidElement} from 'react'
|
|
6
|
+
import {useMemoObservable} from 'react-rx'
|
|
7
|
+
import type {SanityDocument, SchemaType} from 'sanity'
|
|
8
|
+
import {PreviewValue} from 'sanity'
|
|
9
|
+
import {
|
|
10
|
+
DocumentPresence,
|
|
11
|
+
DocumentPreviewPresence,
|
|
12
|
+
DocumentPreviewStore,
|
|
13
|
+
GeneralPreviewLayoutKey,
|
|
14
|
+
getPreviewStateObservable,
|
|
15
|
+
getPreviewValueWithFallback,
|
|
16
|
+
isRecord,
|
|
17
|
+
SanityDefaultPreview,
|
|
18
|
+
} from 'sanity'
|
|
19
|
+
|
|
20
|
+
import {DraftStatus} from './DraftStatus'
|
|
21
|
+
import {PublishedStatus} from './PublishedStatus'
|
|
22
|
+
|
|
23
|
+
export interface PaneItemPreviewState {
|
|
24
|
+
isLoading?: boolean
|
|
25
|
+
draft?: PreviewValue | Partial<SanityDocument> | null
|
|
26
|
+
published?: PreviewValue | Partial<SanityDocument> | null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PaneItemPreviewProps {
|
|
30
|
+
documentPreviewStore: DocumentPreviewStore
|
|
31
|
+
icon: React.ComponentType | false
|
|
32
|
+
layout: GeneralPreviewLayoutKey
|
|
33
|
+
presence?: DocumentPresence[]
|
|
34
|
+
schemaType: SchemaType
|
|
35
|
+
value: SanityDocument
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function PaneItemPreview(props: PaneItemPreviewProps) {
|
|
39
|
+
const {icon, layout, presence, schemaType, value} = props
|
|
40
|
+
const title =
|
|
41
|
+
(isRecord(value.title) && isValidElement(value.title)) ||
|
|
42
|
+
isString(value.title) ||
|
|
43
|
+
isNumber(value.title)
|
|
44
|
+
? value.title
|
|
45
|
+
: null
|
|
46
|
+
|
|
47
|
+
// NOTE: this emits sync so can never be null
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
49
|
+
const {draft, published, isLoading} = useMemoObservable<PaneItemPreviewState>(
|
|
50
|
+
() => getPreviewStateObservable(props.documentPreviewStore, schemaType, value._id, title),
|
|
51
|
+
[props.documentPreviewStore, schemaType, value._id, title]
|
|
52
|
+
)!
|
|
53
|
+
|
|
54
|
+
const status = isLoading ? null : (
|
|
55
|
+
<Inline space={4}>
|
|
56
|
+
{presence && presence.length > 0 && <DocumentPreviewPresence presence={presence} />}
|
|
57
|
+
<PublishedStatus document={published} />
|
|
58
|
+
<DraftStatus document={draft} />
|
|
59
|
+
</Inline>
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<SanityDefaultPreview
|
|
64
|
+
{...(getPreviewValueWithFallback({value, draft, published}) as any)}
|
|
65
|
+
isPlaceholder={isLoading}
|
|
66
|
+
icon={icon}
|
|
67
|
+
layout={layout}
|
|
68
|
+
status={status}
|
|
69
|
+
/>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Adapted from https://github.com/sanity-io/sanity/blob/next/packages/sanity/src/desk/components/PublishedStatus.tsx
|
|
2
|
+
|
|
3
|
+
import {PublishIcon} from '@sanity/icons'
|
|
4
|
+
import {Box, Text, Tooltip} from '@sanity/ui'
|
|
5
|
+
import React from 'react'
|
|
6
|
+
import type {PreviewValue, SanityDocument} from 'sanity'
|
|
7
|
+
import {TextWithTone} from 'sanity'
|
|
8
|
+
|
|
9
|
+
import {TimeAgo} from './TimeAgo'
|
|
10
|
+
|
|
11
|
+
export function PublishedStatus(props: {document?: PreviewValue | Partial<SanityDocument> | null}) {
|
|
12
|
+
const {document} = props
|
|
13
|
+
const updatedAt = document && '_updatedAt' in document && document._updatedAt
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<Tooltip
|
|
17
|
+
portal
|
|
18
|
+
content={
|
|
19
|
+
<Box padding={2}>
|
|
20
|
+
<Text size={1}>
|
|
21
|
+
{document ? (
|
|
22
|
+
<>Published {updatedAt && <TimeAgo time={updatedAt} />}</>
|
|
23
|
+
) : (
|
|
24
|
+
<>Not published</>
|
|
25
|
+
)}
|
|
26
|
+
</Text>
|
|
27
|
+
</Box>
|
|
28
|
+
}
|
|
29
|
+
>
|
|
30
|
+
<TextWithTone tone="positive" dimmed={!document} muted={!document} size={1}>
|
|
31
|
+
<PublishIcon />
|
|
32
|
+
</TextWithTone>
|
|
33
|
+
</Tooltip>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Adapted from https://github.com/sanity-io/sanity/blob/next/packages/sanity/src/desk/components/TimeAgo.tsx
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import {useTimeAgo} from 'sanity'
|
|
4
|
+
|
|
5
|
+
export interface TimeAgoProps {
|
|
6
|
+
time: string | Date
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function TimeAgo({time}: TimeAgoProps) {
|
|
10
|
+
const timeAgo = useTimeAgo(time)
|
|
11
|
+
|
|
12
|
+
return <span title={timeAgo}>{timeAgo} ago</span>
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React, {SVGProps} from 'react'
|
|
2
|
+
|
|
3
|
+
export function ResolutionIcon(props: SVGProps<SVGSVGElement>) {
|
|
4
|
+
return (
|
|
5
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
|
|
6
|
+
<path
|
|
7
|
+
fill="currentColor"
|
|
8
|
+
d="M20 9V6h-3V4h5v5h-2ZM2 9V4h5v2H4v3H2Zm15 11v-2h3v-3h2v5h-5ZM2 20v-5h2v3h3v2H2Zm4-4V8h12v8H6Zm2-2h8v-4H8v4Zm0 0v-4v4Z"
|
|
9
|
+
/>
|
|
10
|
+
</svg>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React, {SVGProps} from 'react'
|
|
2
|
+
|
|
3
|
+
export function StopWatchIcon(props: SVGProps<SVGSVGElement>) {
|
|
4
|
+
return (
|
|
5
|
+
<svg
|
|
6
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
7
|
+
width="1em"
|
|
8
|
+
height="1em"
|
|
9
|
+
viewBox="0 0 512 512"
|
|
10
|
+
{...props}
|
|
11
|
+
>
|
|
12
|
+
<path d="M232 306.667h48V176h-48v130.667z" fill="currentColor" />
|
|
13
|
+
<path
|
|
14
|
+
d="M407.67 170.271l30.786-30.786-33.942-33.941-30.785 30.786C341.217 111.057 300.369 96 256 96 149.961 96 64 181.961 64 288s85.961 192 192 192 192-85.961 192-192c0-44.369-15.057-85.217-40.33-117.729zm-45.604 223.795C333.734 422.398 296.066 438 256 438s-77.735-15.602-106.066-43.934C121.602 365.735 106 328.066 106 288s15.602-77.735 43.934-106.066C178.265 153.602 215.934 138 256 138s77.734 15.602 106.066 43.934C390.398 210.265 406 247.934 406 288s-15.602 77.735-43.934 106.066z"
|
|
15
|
+
fill="currentColor"
|
|
16
|
+
/>
|
|
17
|
+
<path d="M192 32h128v48H192z" fill="currentColor" />
|
|
18
|
+
</svg>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Icon of a monitor with a play button.
|
|
5
|
+
* Credits: material design icons & react-icons
|
|
6
|
+
*/
|
|
7
|
+
const ToolIcon = () => (
|
|
8
|
+
<svg
|
|
9
|
+
stroke="currentColor"
|
|
10
|
+
fill="currentColor"
|
|
11
|
+
strokeWidth="0"
|
|
12
|
+
viewBox="0 0 24 24"
|
|
13
|
+
height="1em"
|
|
14
|
+
width="1em"
|
|
15
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
16
|
+
>
|
|
17
|
+
<path d="M21 3H3c-1.11 0-2 .89-2 2v12c0 1.1.89 2 2 2h5v2h8v-2h5c1.1 0 1.99-.9 1.99-2L23 5c0-1.11-.9-2-2-2zm0 14H3V5h18v12zm-5-6l-7 4V7z" />
|
|
18
|
+
</svg>
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
export default ToolIcon
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {useMemo, useState} from 'react'
|
|
2
|
+
import {collate, createHookFromObservableFactory, DocumentStore, useDocumentStore} from 'sanity'
|
|
3
|
+
|
|
4
|
+
import {SANITY_API_VERSION} from '../hooks/useClient'
|
|
5
|
+
import {createSearchFilter} from '../util/createSearchFilter'
|
|
6
|
+
import type {VideoAssetDocument} from '../util/types'
|
|
7
|
+
|
|
8
|
+
export const ASSET_SORT_OPTIONS = {
|
|
9
|
+
createdDesc: {groq: '_createdAt desc', label: 'Newest first'},
|
|
10
|
+
createdAsc: {groq: '_createdAt asc', label: 'First created (oldest)'},
|
|
11
|
+
filenameAsc: {groq: 'filename asc', label: 'By filename (A-Z)'},
|
|
12
|
+
filenameDesc: {groq: 'filename desc', label: 'By filename (Z-A)'},
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type SortOption = keyof typeof ASSET_SORT_OPTIONS
|
|
16
|
+
|
|
17
|
+
const useAssetDocuments = createHookFromObservableFactory<
|
|
18
|
+
VideoAssetDocument[],
|
|
19
|
+
{
|
|
20
|
+
documentStore: DocumentStore
|
|
21
|
+
sort: SortOption
|
|
22
|
+
searchQuery: string
|
|
23
|
+
}
|
|
24
|
+
>(({documentStore, sort, searchQuery}) => {
|
|
25
|
+
const search = createSearchFilter(searchQuery)
|
|
26
|
+
const filter = [`_type == "mux.videoAsset"`, ...search.filter].filter(Boolean).join(' && ')
|
|
27
|
+
|
|
28
|
+
const query = ASSET_SORT_OPTIONS[sort].groq
|
|
29
|
+
return documentStore.listenQuery(/* groq */ `*[${filter}] | order(${query})`, search.params, {
|
|
30
|
+
apiVersion: SANITY_API_VERSION,
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
export default function useAssets() {
|
|
35
|
+
const documentStore = useDocumentStore()
|
|
36
|
+
const [sort, setSort] = useState<SortOption>('createdDesc')
|
|
37
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
38
|
+
|
|
39
|
+
const [assetDocuments = [], isLoading] = useAssetDocuments({documentStore, sort, searchQuery})
|
|
40
|
+
const assets = useMemo(
|
|
41
|
+
() =>
|
|
42
|
+
// Avoid displaying both drafts & published assets by collating them together and giving preference to drafts
|
|
43
|
+
collate<VideoAssetDocument>(assetDocuments).map(
|
|
44
|
+
(collated) =>
|
|
45
|
+
({
|
|
46
|
+
...(collated.draft || collated.published || {}),
|
|
47
|
+
_id: collated.id,
|
|
48
|
+
} as VideoAssetDocument)
|
|
49
|
+
),
|
|
50
|
+
[assetDocuments]
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
assets,
|
|
55
|
+
isLoading,
|
|
56
|
+
sort,
|
|
57
|
+
searchQuery,
|
|
58
|
+
setSort,
|
|
59
|
+
setSearchQuery,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {useCallback} from 'react'
|
|
2
2
|
import {PatchEvent, unset} from 'sanity'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {deleteAssetOnMux} from '../actions/assets'
|
|
5
5
|
import {useClient} from '../hooks/useClient'
|
|
6
6
|
import type {MuxInputProps, VideoAssetDocument} from '../util/types'
|
|
7
7
|
|
|
@@ -13,7 +13,7 @@ export const useCancelUpload = (asset: VideoAssetDocument, onChange: MuxInputPro
|
|
|
13
13
|
}
|
|
14
14
|
onChange(PatchEvent.from(unset()))
|
|
15
15
|
if (asset.assetId) {
|
|
16
|
-
|
|
16
|
+
deleteAssetOnMux(client, asset.assetId)
|
|
17
17
|
}
|
|
18
18
|
if (asset._id) {
|
|
19
19
|
client.delete(asset._id)
|
package/src/hooks/useClient.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// As it's required to specify the API Version this custom hook ensures it's all using the same version
|
|
2
2
|
import {useClient as useSanityClient} from 'sanity'
|
|
3
3
|
|
|
4
|
+
export const SANITY_API_VERSION = '2022-09-14'
|
|
5
|
+
|
|
4
6
|
export function useClient() {
|
|
5
|
-
return useSanityClient({apiVersion:
|
|
7
|
+
return useSanityClient({apiVersion: SANITY_API_VERSION})
|
|
6
8
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {createHookFromObservableFactory, DocumentStore, SanityDocument} from 'sanity'
|
|
2
|
+
|
|
3
|
+
import {SANITY_API_VERSION} from './useClient'
|
|
4
|
+
|
|
5
|
+
const useDocReferences = createHookFromObservableFactory<
|
|
6
|
+
SanityDocument[],
|
|
7
|
+
{
|
|
8
|
+
documentStore: DocumentStore
|
|
9
|
+
id: string
|
|
10
|
+
}
|
|
11
|
+
>(({documentStore, id}) => {
|
|
12
|
+
return documentStore.listenQuery(
|
|
13
|
+
/* groq */ '*[references($id)]{_id, _type, _rev, _updatedAt, _createdAt}',
|
|
14
|
+
{id},
|
|
15
|
+
{
|
|
16
|
+
apiVersion: SANITY_API_VERSION,
|
|
17
|
+
}
|
|
18
|
+
)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
export default useDocReferences
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type {RefObject} from 'react'
|
|
2
|
+
import {useEffect, useRef, useState} from 'react'
|
|
3
|
+
|
|
4
|
+
type IntersectionOptions = {
|
|
5
|
+
root?: Element | null
|
|
6
|
+
rootMargin?: string
|
|
7
|
+
threshold?: number
|
|
8
|
+
onChange?: (inView: boolean) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function useInView<RefElement = HTMLElement>(
|
|
12
|
+
options: IntersectionOptions = {}
|
|
13
|
+
): {inView: boolean; ref: RefObject<RefElement>} {
|
|
14
|
+
const [inView, setInView] = useState(false)
|
|
15
|
+
const ref = useRef(null)
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!ref.current) return
|
|
19
|
+
|
|
20
|
+
const observer = new IntersectionObserver(([entry], obs) => {
|
|
21
|
+
// ==== from react-intersection-observer ====
|
|
22
|
+
// While it would be nice if you could just look at isIntersecting to determine if the component is inside the viewport, browsers can't agree on how to use it.
|
|
23
|
+
// -Firefox ignores `threshold` when considering `isIntersecting`, so it will never be false again if `threshold` is > 0
|
|
24
|
+
const nowInView =
|
|
25
|
+
entry.isIntersecting &&
|
|
26
|
+
obs.thresholds.some((threshold) => entry.intersectionRatio >= threshold)
|
|
27
|
+
|
|
28
|
+
// Update our state when observer callback fires
|
|
29
|
+
setInView(nowInView)
|
|
30
|
+
options?.onChange?.(nowInView)
|
|
31
|
+
}, options)
|
|
32
|
+
|
|
33
|
+
const toObserve = ref.current
|
|
34
|
+
observer.observe(toObserve)
|
|
35
|
+
|
|
36
|
+
// eslint-disable-next-line
|
|
37
|
+
return () => {
|
|
38
|
+
if (toObserve) observer.unobserve(toObserve)
|
|
39
|
+
}
|
|
40
|
+
}, [options])
|
|
41
|
+
|
|
42
|
+
return {inView, ref}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default useInView
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {definePlugin} from 'sanity'
|
|
2
2
|
|
|
3
|
+
import createStudioTool from './components/StudioTool'
|
|
3
4
|
import {muxVideoCustomRendering} from './plugin'
|
|
4
5
|
import {muxVideo, muxVideoAsset} from './schema'
|
|
5
6
|
import type {Config} from './util/types'
|
|
@@ -23,5 +24,6 @@ export const muxInput = definePlugin<Partial<Config> | void>((userConfig) => {
|
|
|
23
24
|
},
|
|
24
25
|
],
|
|
25
26
|
},
|
|
27
|
+
tools: config.tool === false ? undefined : [createStudioTool(config)],
|
|
26
28
|
}
|
|
27
29
|
})
|
package/src/plugin.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
|
|
3
3
|
import Input from './components/Input'
|
|
4
|
-
import
|
|
4
|
+
import VideoThumbnail from './components/VideoThumbnail'
|
|
5
5
|
import type {Config, MuxInputProps, VideoAssetDocument} from './util/types'
|
|
6
6
|
|
|
7
7
|
export function muxVideoCustomRendering(config: Config) {
|
package/src/util/constants.ts
CHANGED
|
@@ -4,3 +4,10 @@ export const name = 'mux-input' as const
|
|
|
4
4
|
export const cacheNs = 'sanity-plugin-mux-input' as const
|
|
5
5
|
|
|
6
6
|
export const muxSecretsDocumentId = 'secrets.mux' as const
|
|
7
|
+
|
|
8
|
+
export const DIALOGS_Z_INDEX = 60_000
|
|
9
|
+
|
|
10
|
+
export const THUMBNAIL_ASPECT_RATIO = 16 / 9
|
|
11
|
+
|
|
12
|
+
/** Thumbnails and input shouldn't go beyond this aspect ratio */
|
|
13
|
+
export const MIN_ASPECT_RATIO = 1
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Adaptation of Sanity's createSearchQuery for our limited use case:
|
|
2
|
+
// https://github.com/sanity-io/sanity/blob/next/packages/sanity/src/core/search/weighted/createSearchQuery.ts
|
|
3
|
+
import {compact, toLower, trim, uniq, words} from 'lodash'
|
|
4
|
+
|
|
5
|
+
const SPECIAL_CHARS = /([^!@#$%^&*(),\\/?";:{}|[\]+<>\s-])+/g
|
|
6
|
+
const STRIP_EDGE_CHARS = /(^[.]+)|([.]+$)/
|
|
7
|
+
|
|
8
|
+
function tokenize(string: string): string[] {
|
|
9
|
+
return (string.match(SPECIAL_CHARS) || []).map((token) => token.replace(STRIP_EDGE_CHARS, ''))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function toGroqParams(terms: string[]): Record<string, string> {
|
|
13
|
+
const params: Record<string, string> = {}
|
|
14
|
+
return terms.reduce((acc, term, i) => {
|
|
15
|
+
acc[`t${i}`] = `*${term}*` // "t" is short for term
|
|
16
|
+
return acc
|
|
17
|
+
}, params)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Convert a string into an array of tokenized terms.
|
|
22
|
+
*
|
|
23
|
+
* Any (multi word) text wrapped in double quotes will be treated as "phrases", or separate tokens that
|
|
24
|
+
* will not have its special characters removed.
|
|
25
|
+
* E.g.`"the" "fantastic mr" fox fox book` =\> ["the", `"fantastic mr"`, "fox", "book"]
|
|
26
|
+
*
|
|
27
|
+
* Phrases wrapped in quotes are assigned relevance scoring differently from regular words.
|
|
28
|
+
*
|
|
29
|
+
* @internal
|
|
30
|
+
*/
|
|
31
|
+
function extractTermsFromQuery(query: string): string[] {
|
|
32
|
+
const quotedQueries = [] as string[]
|
|
33
|
+
const unquotedQuery = query.replace(/("[^"]*")/g, (match) => {
|
|
34
|
+
if (words(match).length > 1) {
|
|
35
|
+
quotedQueries.push(match)
|
|
36
|
+
return ''
|
|
37
|
+
}
|
|
38
|
+
return match
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// Lowercase and trim quoted queries
|
|
42
|
+
const quotedTerms = quotedQueries.map((str) => trim(toLower(str)))
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Convert (remaining) search query into an array of deduped, sanitized tokens.
|
|
46
|
+
* All white space and special characters are removed.
|
|
47
|
+
* e.g. "The saint of Saint-Germain-des-Prés" =\> ['the', 'saint', 'of', 'germain', 'des', 'pres']
|
|
48
|
+
*/
|
|
49
|
+
const remainingTerms = uniq(compact(tokenize(toLower(unquotedQuery))))
|
|
50
|
+
|
|
51
|
+
return [...quotedTerms, ...remainingTerms]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Which properties of the video asset document should we match users' queries against */
|
|
55
|
+
const SEARCH_PATHS = ['filename', 'assetId', '_id']
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create GROQ constraints, given search terms and the full spec of available document types and fields.
|
|
59
|
+
* Essentially a large list of all possible fields (joined by logical OR) to match our search terms against.
|
|
60
|
+
*/
|
|
61
|
+
function createConstraints(terms: string[]) {
|
|
62
|
+
const constraints = terms
|
|
63
|
+
.map((_term, i) => SEARCH_PATHS.map((joinedPath) => `${joinedPath} match $t${i}`))
|
|
64
|
+
.filter((constraint) => constraint.length > 0)
|
|
65
|
+
|
|
66
|
+
return constraints.map((constraint) => `(${constraint.join(' || ')})`)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createSearchFilter(query: string) {
|
|
70
|
+
const terms = extractTermsFromQuery(query)
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
filter: createConstraints(terms),
|
|
74
|
+
params: {
|
|
75
|
+
...toGroqParams(terms),
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
// From: https://stackoverflow.com/a/11486026/10433647
|
|
3
|
+
export default function formatSeconds(seconds: number): string {
|
|
4
|
+
if (typeof seconds !== 'number' || Number.isNaN(seconds)) {
|
|
5
|
+
return ''
|
|
6
|
+
}
|
|
7
|
+
// Hours, minutes and seconds
|
|
8
|
+
const hrs = ~~(seconds / 3600)
|
|
9
|
+
const mins = ~~((seconds % 3600) / 60)
|
|
10
|
+
const secs = ~~seconds % 60
|
|
11
|
+
|
|
12
|
+
// Output like "1:01" or "4:03:59" or "123:03:59"
|
|
13
|
+
let ret = ''
|
|
14
|
+
|
|
15
|
+
if (hrs > 0) {
|
|
16
|
+
ret += '' + hrs + ':' + (mins < 10 ? '0' : '')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
ret += '' + mins + ':' + (secs < 10 ? '0' : '')
|
|
20
|
+
ret += '' + secs
|
|
21
|
+
return ret
|
|
22
|
+
}
|
|
@@ -6,7 +6,7 @@ import {getPlaybackPolicy} from './getPlaybackPolicy'
|
|
|
6
6
|
import type {AnimatedThumbnailOptions, MuxAnimatedThumbnailUrl, VideoAssetDocument} from './types'
|
|
7
7
|
|
|
8
8
|
export interface AnimatedPosterSrcOptions extends AnimatedThumbnailOptions {
|
|
9
|
-
asset: VideoAssetDocument
|
|
9
|
+
asset: Partial<VideoAssetDocument>
|
|
10
10
|
client: SanityClient
|
|
11
11
|
}
|
|
12
12
|
|