sanity-plugin-mux-input 2.1.0 → 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.
Files changed (67) hide show
  1. package/lib/index.cjs +4037 -4
  2. package/lib/index.cjs.map +1 -1
  3. package/lib/index.d.ts +14 -1
  4. package/lib/index.js +4026 -2
  5. package/lib/index.js.map +1 -1
  6. package/package.json +27 -28
  7. package/src/actions/assets.ts +30 -2
  8. package/src/clients/upChunkObservable.ts +1 -1
  9. package/src/components/ConfigureApi.tsx +9 -1
  10. package/src/components/FormField.tsx +8 -10
  11. package/src/components/IconInfo.tsx +23 -0
  12. package/src/components/Input.styled.tsx +0 -8
  13. package/src/components/Input.tsx +4 -3
  14. package/src/components/InputBrowser.tsx +1 -8
  15. package/src/components/Player.styled.tsx +5 -144
  16. package/src/components/Player.tsx +23 -109
  17. package/src/components/PlayerActionsMenu.tsx +0 -4
  18. package/src/components/SelectAsset.tsx +18 -58
  19. package/src/components/SelectSortOptions.tsx +45 -0
  20. package/src/components/SpinnerBox.tsx +17 -0
  21. package/src/components/StudioTool.tsx +20 -0
  22. package/src/components/VideoDetails/DeleteDialog.tsx +156 -0
  23. package/src/components/VideoDetails/VideoDetails.tsx +298 -0
  24. package/src/components/VideoDetails/VideoReferences.tsx +70 -0
  25. package/src/components/VideoDetails/useVideoDetails.ts +85 -0
  26. package/src/components/VideoInBrowser.tsx +183 -0
  27. package/src/components/VideoMetadata.tsx +43 -0
  28. package/src/components/VideoPlayer.tsx +69 -0
  29. package/src/components/VideoThumbnail.tsx +106 -0
  30. package/src/components/VideosBrowser.tsx +83 -0
  31. package/src/components/__legacy__Uploader.tsx +2 -9
  32. package/src/components/documentPreview/DocumentPreview.tsx +107 -0
  33. package/src/components/documentPreview/DraftStatus.tsx +34 -0
  34. package/src/components/documentPreview/MissingSchemaType.tsx +33 -0
  35. package/src/components/documentPreview/PaneItemPreview.tsx +71 -0
  36. package/src/components/documentPreview/PublishedStatus.tsx +35 -0
  37. package/src/components/documentPreview/TimeAgo.tsx +13 -0
  38. package/src/components/documentPreview/paneItemTypes.ts +7 -0
  39. package/src/components/icons/Resolution.tsx +12 -0
  40. package/src/components/icons/StopWatch.tsx +20 -0
  41. package/src/components/icons/ToolIcon.tsx +21 -0
  42. package/src/hooks/useAssets.ts +61 -0
  43. package/src/hooks/useCancelUpload.ts +2 -2
  44. package/src/hooks/useClient.ts +3 -1
  45. package/src/hooks/useDocReferences.ts +21 -0
  46. package/src/hooks/useInView.ts +45 -0
  47. package/src/index.ts +2 -0
  48. package/src/plugin.tsx +1 -1
  49. package/src/util/constants.ts +7 -0
  50. package/src/util/createSearchFilter.ts +78 -0
  51. package/src/util/formatSeconds.ts +22 -0
  52. package/src/util/getAnimatedPosterSrc.ts +1 -1
  53. package/src/util/getPlaybackId.ts +1 -1
  54. package/src/util/getPlaybackPolicy.ts +1 -1
  55. package/src/util/getVideoMetadata.ts +18 -0
  56. package/src/util/types.ts +16 -1
  57. package/lib/_chunks/Player-d8f163ed.cjs +0 -474
  58. package/lib/_chunks/Player-d8f163ed.cjs.map +0 -1
  59. package/lib/_chunks/Player-fb9712c0.js +0 -465
  60. package/lib/_chunks/Player-fb9712c0.js.map +0 -1
  61. package/lib/_chunks/index-0656ede8.js +0 -3229
  62. package/lib/_chunks/index-0656ede8.js.map +0 -1
  63. package/lib/_chunks/index-9cd542f1.cjs +0 -3271
  64. package/lib/_chunks/index-9cd542f1.cjs.map +0 -1
  65. package/src/components/EditThumbnailDialog.tsx +0 -74
  66. package/src/components/VideoSource.styled.tsx +0 -235
  67. 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,7 @@
1
+ import type {PreviewValue, SanityDocument} from 'sanity'
2
+
3
+ export interface PaneItemPreviewState {
4
+ isLoading?: boolean
5
+ draft?: PreviewValue | Partial<SanityDocument> | null
6
+ published?: PreviewValue | Partial<SanityDocument> | null
7
+ }
@@ -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 {deleteAsset} from '../actions/assets'
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
- deleteAsset(client, asset.assetId)
16
+ deleteAssetOnMux(client, asset.assetId)
17
17
  }
18
18
  if (asset._id) {
19
19
  client.delete(asset._id)
@@ -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: '2022-09-14'})
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 {VideoThumbnail} from './components/VideoSource.styled'
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) {
@@ -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