sanity-plugin-mux-input 2.4.0 → 2.5.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Sanity.io
3
+ Copyright (c) 2025 Sanity.io
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -77,19 +77,19 @@ The Mux plugin will find its access tokens by fetching this document.
77
77
 
78
78
  When a Mux video is uploaded/chosen in a document via this plugin, it gets stored as a reference to the video document:
79
79
 
80
- ```json
80
+ ```json5
81
81
  // example document
82
82
  {
83
- "_type": "exampleSchemaWithVideo",
83
+ _type: 'exampleSchemaWithVideo',
84
84
  // Example video field
85
- "myVideoField": {
86
- "_type": "mux.video",
87
- "asset": {
88
- "_type": "reference",
89
- "_weak": true,
90
- "_ref": "4e37284e-cec2-406d-973c-fdf9ab1e5598" // 👈 ID of the document holding the video's Mux data
91
- }
92
- }
85
+ myVideoField: {
86
+ _type: 'mux.video',
87
+ asset: {
88
+ _type: 'reference',
89
+ _weak: true,
90
+ _ref: '4e37284e-cec2-406d-973c-fdf9ab1e5598', // 👈 ID of the document holding the video's Mux data
91
+ },
92
+ },
93
93
  }
94
94
  ```
95
95
 
@@ -112,54 +112,55 @@ Before you can display videos in your frontend, you need to follow these referen
112
112
 
113
113
  For reference, here's an example `mux.videoAsset` document:
114
114
 
115
- ```json
115
+ ```json5
116
116
  {
117
- "_id": "4e37284e-cec2-406d-973c-fdf9ab1e5598",
118
- "_type": "mux.videoAsset",
119
- "assetId": "7ovyI76F92n02H00mWP7lOCZMIU00N4iysDiQDNppX026HY",
120
- "filename": "mux-example-video.mp4",
121
- "status": "ready",
122
- "playbackId": "YA02HBpY02fKWHDRMNilo301pdH02LY3k9HTcK43ItGJLWA",
117
+ _id: '4e37284e-cec2-406d-973c-fdf9ab1e5598',
118
+ _type: 'mux.videoAsset',
119
+ assetId: '7ovyI76F92n02H00mWP7lOCZMIU00N4iysDiQDNppX026HY',
120
+ filename: 'mux-example-video.mp4',
121
+ status: 'ready',
122
+ playbackId: 'YA02HBpY02fKWHDRMNilo301pdH02LY3k9HTcK43ItGJLWA',
123
+ thumbTime: 65.82,
123
124
  // Full Mux asset data:
124
- "data": {
125
- "encoding_tier": "smart",
126
- "max_resolution_tier": "1080p",
127
- "aspect_ratio": "16:9",
128
- "created_at": "1706645034",
129
- "duration": 25.492133,
130
- "status": "ready",
131
- "master_access": "none",
132
- "max_stored_frame_rate": 29.97,
133
- "playback_ids": [
125
+ data: {
126
+ encoding_tier: 'smart',
127
+ max_resolution_tier: '1080p',
128
+ aspect_ratio: '16:9',
129
+ created_at: '1706645034',
130
+ duration: 25.492133,
131
+ status: 'ready',
132
+ master_access: 'none',
133
+ max_stored_frame_rate: 29.97,
134
+ playback_ids: [
134
135
  {
135
- "id": "YA02HBpY02fKWHDRMNilo301pdH02LY3k9HTcK43ItGJLWA",
136
- "policy": "signed"
137
- }
136
+ id: 'YA02HBpY02fKWHDRMNilo301pdH02LY3k9HTcK43ItGJLWA',
137
+ policy: 'signed',
138
+ },
138
139
  ],
139
- "resolution_tier": "1080p",
140
- "ingest_type": "on_demand_url",
141
- "max_stored_resolution": "HD",
142
- "tracks": [
140
+ resolution_tier: '1080p',
141
+ ingest_type: 'on_demand_url',
142
+ max_stored_resolution: 'HD',
143
+ tracks: [
143
144
  {
144
- "max_channel_layout": "stereo",
145
- "max_channels": 2,
146
- "id": "00MKMC73SYimw1YTh0102lPJJp9w2R5rHddpNX1N9opAMk",
147
- "type": "audio",
148
- "primary": true,
149
- "duration": 25.45
145
+ max_channel_layout: 'stereo',
146
+ max_channels: 2,
147
+ id: '00MKMC73SYimw1YTh0102lPJJp9w2R5rHddpNX1N9opAMk',
148
+ type: 'audio',
149
+ primary: true,
150
+ duration: 25.45,
150
151
  },
151
152
  {
152
- "max_frame_rate": 29.97,
153
- "max_height": 1080,
154
- "id": "g1wEph3CVvbJL01YNKzAWMyH8N1SxW00WeECGjqwEHW9g",
155
- "type": "video",
156
- "duration": 25.4254,
157
- "max_width": 1920
158
- }
153
+ max_frame_rate: 29.97,
154
+ max_height: 1080,
155
+ id: 'g1wEph3CVvbJL01YNKzAWMyH8N1SxW00WeECGjqwEHW9g',
156
+ type: 'video',
157
+ duration: 25.4254,
158
+ max_width: 1920,
159
+ },
159
160
  ],
160
- "id": "7ovyI76F92n02H00mWP7lOCZMIU00N4iysDiQDNppX026HY",
161
- "mp4_support": "none"
162
- }
161
+ id: '7ovyI76F92n02H00mWP7lOCZMIU00N4iysDiQDNppX026HY',
162
+ mp4_support: 'none',
163
+ },
163
164
  }
164
165
  ```
165
166
 
@@ -279,7 +280,7 @@ Issues are actively monitored and PRs are welcome. When developing this plugin t
279
280
 
280
281
  ### Publishing
281
282
 
282
- Run the ["CI" workflow](https://github.com/sanity-io/sanity-plugin-mux-input/actions/workflows/ci.yml).
283
+ You can run the ["CI and Release" workflow](<[https://github.com/sanity-io/sanity-plugin-mux-input/actions/workflows/ci.yml](https://github.com/sanity-io/sanity-plugin-mux-input/actions/workflows/main.yml)>).
283
284
  Make sure to select the main branch and check "Release new version".
284
285
 
285
286
  Semantic release will only release on configured branches, so it is safe to run release on any branch.
@@ -301,11 +302,12 @@ After Studio v3 turns stable this behavior will change. The v2 version will then
301
302
 
302
303
  ### Develop & test
303
304
 
304
- This plugin uses [@sanity/plugin-kit](https://github.com/sanity-io/plugin-kit)
305
- with default configuration for build & watch scripts.
305
+ You can run the example locally by doing the following:
306
306
 
307
- See [Testing a plugin in Sanity Studio](https://github.com/sanity-io/plugin-kit#testing-a-plugin-in-sanity-studio)
308
- on how to run this plugin with hotreload in the studio.
307
+ 1. run `npm install` and `npm dev` on the root of the repo
308
+ 2. In the terminal, a command with `yalc` will be shown, that command will allow you to run the version that you have locally directly on the example or on your own app.
309
+ 3. run `npm install` and `npm dev` on the `/example` directory where the app with the example exists or in your own app
310
+ 4. the studio and app should auto reload with your changes in the plugin package you have locally
309
311
 
310
312
  ### Release new version
311
313
 
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
19
19
  mod
20
20
  ));
21
21
  Object.defineProperty(exports, "__esModule", { value: !0 });
22
- var sanity = require("sanity"), jsxRuntime = require("react/jsx-runtime"), icons = require("@sanity/icons"), ui = require("@sanity/ui"), React = require("react"), compact = require("lodash/compact.js"), toLower = require("lodash/toLower.js"), trim = require("lodash/trim.js"), uniq = require("lodash/uniq.js"), words = require("lodash/words.js"), styledComponents = require("styled-components"), uuid = require("@sanity/uuid"), rxjs = require("rxjs"), operators = require("rxjs/operators"), suspendReact = require("suspend-react"), MuxPlayer = require("@mux/mux-player-react"), desk = require("sanity/desk"), router = require("sanity/router"), isNumber = require("lodash/isNumber.js"), isString = require("lodash/isString.js"), reactRx = require("react-rx"), useSWR = require("swr"), scrollIntoView = require("scroll-into-view-if-needed"), upchunk = require("@mux/upchunk"), reactIs = require("react-is"), LanguagesList = require("iso-639-1");
22
+ var sanity = require("sanity"), jsxRuntime = require("react/jsx-runtime"), icons = require("@sanity/icons"), ui = require("@sanity/ui"), React = require("react"), reactRx = require("react-rx"), compact = require("lodash/compact.js"), toLower = require("lodash/toLower.js"), trim = require("lodash/trim.js"), uniq = require("lodash/uniq.js"), words = require("lodash/words.js"), styledComponents = require("styled-components"), uuid = require("@sanity/uuid"), rxjs = require("rxjs"), operators = require("rxjs/operators"), suspendReact = require("suspend-react"), MuxPlayer = require("@mux/mux-player-react"), desk = require("sanity/desk"), router = require("sanity/router"), isNumber = require("lodash/isNumber.js"), isString = require("lodash/isString.js"), useSWR = require("swr"), scrollIntoView = require("scroll-into-view-if-needed"), upchunk = require("@mux/upchunk"), reactIs = require("react-is"), LanguagesList = require("iso-639-1");
23
23
  function _interopDefaultCompat(e) {
24
24
  return e && typeof e == "object" && "default" in e ? e : { default: e };
25
25
  }
@@ -71,31 +71,38 @@ const ASSET_SORT_OPTIONS = {
71
71
  createdAsc: { groq: "_createdAt asc", label: "First created (oldest)" },
72
72
  filenameAsc: { groq: "filename asc", label: "By filename (A-Z)" },
73
73
  filenameDesc: { groq: "filename desc", label: "By filename (Z-A)" }
74
- }, useAssetDocuments = sanity.createHookFromObservableFactory(({ documentStore, sort, searchQuery }) => {
75
- const search = createSearchFilter(searchQuery), filter = ['_type == "mux.videoAsset"', ...search.filter].filter(Boolean).join(" && "), sortFragment = ASSET_SORT_OPTIONS[sort].groq;
76
- return documentStore.listenQuery(
77
- /* groq */
78
- `*[${filter}] | order(${sortFragment})`,
79
- search.params,
80
- {
81
- apiVersion: SANITY_API_VERSION
82
- }
83
- );
84
- });
74
+ }, useAssetDocuments = ({
75
+ documentStore,
76
+ sort,
77
+ searchQuery
78
+ }) => {
79
+ const memoizedObservable = React.useMemo(() => {
80
+ const search = createSearchFilter(searchQuery), filter = ['_type == "mux.videoAsset"', ...search.filter].filter(Boolean).join(" && "), sortFragment = ASSET_SORT_OPTIONS[sort].groq;
81
+ return documentStore.listenQuery(
82
+ /* groq */
83
+ `*[${filter}] | order(${sortFragment})`,
84
+ search.params,
85
+ {
86
+ apiVersion: SANITY_API_VERSION
87
+ }
88
+ );
89
+ }, [documentStore, sort, searchQuery]);
90
+ return reactRx.useObservable(memoizedObservable, void 0);
91
+ };
85
92
  function useAssets() {
86
- const documentStore = sanity.useDocumentStore(), [sort, setSort] = React.useState("createdDesc"), [searchQuery, setSearchQuery] = React.useState(""), [assetDocuments = [], isLoading] = useAssetDocuments({ documentStore, sort, searchQuery });
93
+ const documentStore = sanity.useDocumentStore(), [sort, setSort] = React.useState("createdDesc"), [searchQuery, setSearchQuery] = React.useState(""), assetDocumentsObservable = useAssetDocuments({ documentStore, sort, searchQuery }), isLoading = assetDocumentsObservable === void 0;
87
94
  return {
88
95
  assets: React.useMemo(
89
96
  () => (
90
97
  // Avoid displaying both drafts & published assets by collating them together and giving preference to drafts
91
- sanity.collate(assetDocuments).map(
98
+ sanity.collate(assetDocumentsObservable ?? []).map(
92
99
  (collated) => ({
93
100
  ...collated.draft || collated.published || {},
94
101
  _id: collated.id
95
102
  })
96
103
  )
97
104
  ),
98
- [assetDocuments]
105
+ [assetDocumentsObservable]
99
106
  ),
100
107
  isLoading,
101
108
  sort,
@@ -343,6 +350,17 @@ function getPlaybackId(asset) {
343
350
  function getPlaybackPolicy(asset) {
344
351
  return asset.data?.playback_ids?.find((playbackId) => asset.playbackId === playbackId.id)?.policy ?? "public";
345
352
  }
353
+ function createUrlParamsObject(client, asset, params, audience) {
354
+ const playbackId = getPlaybackId(asset);
355
+ let searchParams = new URLSearchParams(
356
+ JSON.parse(JSON.stringify(params, (_, v) => v ?? void 0))
357
+ );
358
+ if (getPlaybackPolicy(asset) === "signed") {
359
+ const token = generateJwt(client, playbackId, audience, params);
360
+ searchParams = new URLSearchParams({ token });
361
+ }
362
+ return { playbackId, searchParams };
363
+ }
346
364
  function getAnimatedPosterSrc({
347
365
  asset,
348
366
  client,
@@ -352,16 +370,22 @@ function getAnimatedPosterSrc({
352
370
  end = start + 5,
353
371
  fps = 15
354
372
  }) {
355
- const params = { height, width, start, end, fps }, playbackId = getPlaybackId(asset);
356
- let searchParams = new URLSearchParams(
357
- JSON.parse(JSON.stringify(params, (_, v) => v ?? void 0))
358
- );
359
- if (getPlaybackPolicy(asset) === "signed") {
360
- const token = generateJwt(client, playbackId, "g", params);
361
- searchParams = new URLSearchParams({ token });
362
- }
373
+ const params = { height, width, start, end, fps }, { playbackId, searchParams } = createUrlParamsObject(client, asset, params, "g");
363
374
  return `https://image.mux.com/${playbackId}/animated.gif?${searchParams}`;
364
375
  }
376
+ function getPosterSrc({
377
+ asset,
378
+ client,
379
+ fit_mode,
380
+ height,
381
+ time = asset.thumbTime ?? void 0,
382
+ width
383
+ }) {
384
+ const params = { fit_mode, height, width };
385
+ time && (params.time = time);
386
+ const { playbackId, searchParams } = createUrlParamsObject(client, asset, params, "t");
387
+ return `https://image.mux.com/${playbackId}/thumbnail.png?${searchParams}`;
388
+ }
365
389
  const Image = styledComponents.styled.img`
366
390
  transition: opacity 0.175s ease-out 0s;
367
391
  display: block;
@@ -376,16 +400,18 @@ const Image = styledComponents.styled.img`
376
400
  };
377
401
  function VideoThumbnail({
378
402
  asset,
379
- width
403
+ width,
404
+ staticImage = !1
380
405
  }) {
381
- const { inView, ref } = useInView(), posterWidth = width || 250, [status, setStatus] = React.useState("loading"), client = useClient(), animatedSrc = React.useMemo(() => {
406
+ const { inView, ref } = useInView(), posterWidth = width || 250, [status, setStatus] = React.useState("loading"), client = useClient(), src = React.useMemo(() => {
382
407
  try {
383
- return getAnimatedPosterSrc({ asset, client, width: posterWidth });
408
+ let thumbnail;
409
+ return staticImage ? thumbnail = getPosterSrc({ asset, client, width: posterWidth }) : thumbnail = getAnimatedPosterSrc({ asset, client, width: posterWidth }), thumbnail;
384
410
  } catch {
385
411
  status !== "error" && setStatus("error");
386
412
  return;
387
413
  }
388
- }, [asset, client, posterWidth, status]);
414
+ }, [asset, client, posterWidth, status, staticImage]);
389
415
  function handleLoad() {
390
416
  setStatus("loaded");
391
417
  }
@@ -440,8 +466,8 @@ function VideoThumbnail({
440
466
  /* @__PURE__ */ jsxRuntime.jsx(
441
467
  Image,
442
468
  {
443
- src: animatedSrc,
444
- alt: `Preview for video ${asset.filename || asset.assetId}`,
469
+ src,
470
+ alt: `Preview for ${staticImage ? "image" : "video"} ${asset.filename || asset.assetId}`,
445
471
  onLoad: handleLoad,
446
472
  onError: handleError,
447
473
  style: {
@@ -754,6 +780,14 @@ function StopWatchIcon(props) {
754
780
  }
755
781
  );
756
782
  }
783
+ const DialogStateContext = React.createContext({
784
+ dialogState: !1,
785
+ setDialogState: () => null
786
+ }), DialogStateProvider = ({
787
+ dialogState,
788
+ setDialogState,
789
+ children
790
+ }) => /* @__PURE__ */ jsxRuntime.jsx(DialogStateContext.Provider, { value: { dialogState, setDialogState }, children }), useDialogStateContext = () => React.useContext(DialogStateContext);
757
791
  function getVideoSrc({ asset, client }) {
758
792
  const playbackId = getPlaybackId(asset), searchParams = new URLSearchParams();
759
793
  if (getPlaybackPolicy(asset) === "signed") {
@@ -762,12 +796,100 @@ function getVideoSrc({ asset, client }) {
762
796
  }
763
797
  return `https://stream.mux.com/${playbackId}.m3u8?${searchParams}`;
764
798
  }
799
+ function getDevicePixelRatio(options) {
800
+ const {
801
+ defaultDpr = 1,
802
+ maxDpr = 3,
803
+ round = !0
804
+ } = options || {}, dpr = typeof window < "u" && typeof window.devicePixelRatio == "number" ? window.devicePixelRatio : defaultDpr;
805
+ return Math.min(Math.max(1, round ? Math.floor(dpr) : dpr), maxDpr);
806
+ }
807
+ function formatSeconds(seconds) {
808
+ if (typeof seconds != "number" || Number.isNaN(seconds))
809
+ return "";
810
+ const hrs = ~~(seconds / 3600), mins = ~~(seconds % 3600 / 60), secs = ~~seconds % 60;
811
+ let ret = "";
812
+ return hrs > 0 && (ret += "" + hrs + ":" + (mins < 10 ? "0" : "")), ret += "" + mins + ":" + (secs < 10 ? "0" : ""), ret += "" + secs, ret;
813
+ }
814
+ function formatSecondsToHHMMSS(seconds) {
815
+ const hrs = Math.floor(seconds / 3600).toString().padStart(2, "0"), mins = Math.floor(seconds % 3600 / 60).toString().padStart(2, "0"), secs = Math.floor(seconds % 60).toString().padStart(2, "0");
816
+ return `${hrs}:${mins}:${secs}`;
817
+ }
818
+ function isValidTimeFormat(time) {
819
+ return /^([0-1]?[0-9]|2[0-3]):([0-5]?[0-9]):([0-5]?[0-9])$/.test(time) || time === "";
820
+ }
821
+ function getSecondsFromTimeFormat(time) {
822
+ const [hh = 0, mm = 0, ss = 0] = time.split(":").map(Number);
823
+ return hh * 3600 + mm * 60 + ss;
824
+ }
825
+ function EditThumbnailDialog({ asset, currentTime = 0 }) {
826
+ const client = useClient(), { setDialogState } = useDialogStateContext(), dialogId = `EditThumbnailDialog${React.useId()}`, [timeFormatted, setTimeFormatted] = React.useState(
827
+ () => formatSecondsToHHMMSS(currentTime)
828
+ ), [nextTime, setNextTime] = React.useState(currentTime), [inputError, setInputError] = React.useState(""), assetWithNewThumbnail = React.useMemo(() => ({ ...asset, thumbTime: nextTime }), [asset, nextTime]), [saving, setSaving] = React.useState(!1), [saveThumbnailError, setSaveThumbnailError] = React.useState(null), handleSave = () => {
829
+ setSaving(!0), client.patch(asset._id).set({ thumbTime: nextTime }).commit({ returnDocuments: !1 }).then(() => void setDialogState(!1)).catch(setSaveThumbnailError).finally(() => void setSaving(!1));
830
+ }, width = 300 * getDevicePixelRatio({ maxDpr: 2 });
831
+ if (saveThumbnailError)
832
+ throw saveThumbnailError;
833
+ return /* @__PURE__ */ jsxRuntime.jsx(
834
+ ui.Dialog,
835
+ {
836
+ id: dialogId,
837
+ header: "Edit thumbnail",
838
+ onClose: () => setDialogState(!1),
839
+ footer: /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { padding: 3, children: /* @__PURE__ */ jsxRuntime.jsx(
840
+ ui.Button,
841
+ {
842
+ disabled: inputError !== "",
843
+ mode: "ghost",
844
+ tone: "primary",
845
+ loading: saving,
846
+ onClick: handleSave,
847
+ text: "Set new thumbnail"
848
+ },
849
+ "thumbnail"
850
+ ) }),
851
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, padding: 3, children: [
852
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
853
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, weight: "semibold", children: "Current:" }),
854
+ /* @__PURE__ */ jsxRuntime.jsx(VideoThumbnail, { asset, width, staticImage: !0 })
855
+ ] }),
856
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
857
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, weight: "semibold", children: "New:" }),
858
+ /* @__PURE__ */ jsxRuntime.jsx(VideoThumbnail, { asset: assetWithNewThumbnail, width, staticImage: !0 })
859
+ ] }),
860
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 2, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { align: "center", justify: "center", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 5, weight: "semibold", children: "Or" }) }) }),
861
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
862
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, weight: "semibold", children: "Selected time for thumbnail (hh:mm:ss):" }),
863
+ /* @__PURE__ */ jsxRuntime.jsx(
864
+ ui.TextInput,
865
+ {
866
+ size: 1,
867
+ value: timeFormatted,
868
+ placeholder: "hh:mm:ss",
869
+ onChange: (event) => {
870
+ const value = event.currentTarget.value;
871
+ if (setTimeFormatted(value), isValidTimeFormat(value)) {
872
+ setInputError("");
873
+ const totalSeconds = getSecondsFromTimeFormat(value);
874
+ setNextTime(totalSeconds);
875
+ } else
876
+ setInputError("Invalid time format");
877
+ },
878
+ customValidity: inputError
879
+ }
880
+ )
881
+ ] })
882
+ ] })
883
+ }
884
+ );
885
+ }
765
886
  function VideoPlayer({
766
887
  asset,
888
+ thumbnailWidth = 250,
767
889
  children,
768
890
  ...props
769
891
  }) {
770
- const client = useClient(), isAudio = assetIsAudio(asset), { src: videoSrc, error } = React.useMemo(() => {
892
+ const client = useClient(), { dialogState } = useDialogStateContext(), isAudio = assetIsAudio(asset), muxPlayer = React.useRef(null), thumbnail = getPosterSrc({ asset, client, width: thumbnailWidth }), { src: videoSrc, error } = React.useMemo(() => {
771
893
  try {
772
894
  const src = asset?.playbackId && getVideoSrc({ client, asset });
773
895
  return src ? { src } : { error: new TypeError("Asset has no playback ID") };
@@ -785,49 +907,54 @@ function VideoPlayer({
785
907
  return isAudio && (aspectRatio = props.forceAspectRatio ? (
786
908
  // Make it wider when forcing aspect ratio to balance with videos' rendering height (audio players overflow a bit)
787
909
  props.forceAspectRatio * 1.2
788
- ) : AUDIO_ASPECT_RATIO), /* @__PURE__ */ jsxRuntime.jsxs(ui.Card, { tone: "transparent", style: { aspectRatio, position: "relative" }, children: [
789
- videoSrc && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
790
- /* @__PURE__ */ jsxRuntime.jsx(
791
- MuxPlayer__default.default,
910
+ ) : AUDIO_ASPECT_RATIO), /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
911
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Card, { tone: "transparent", style: { aspectRatio, position: "relative" }, children: [
912
+ videoSrc && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
913
+ /* @__PURE__ */ jsxRuntime.jsx(
914
+ MuxPlayer__default.default,
915
+ {
916
+ poster: thumbnail,
917
+ ref: muxPlayer,
918
+ ...props,
919
+ playsInline: !0,
920
+ playbackId: asset.playbackId,
921
+ tokens: signedToken ? { playback: signedToken, thumbnail: signedToken, storyboard: signedToken } : void 0,
922
+ preload: "metadata",
923
+ crossOrigin: "anonymous",
924
+ metadata: {
925
+ player_name: "Sanity Admin Dashboard",
926
+ player_version: "2.5.0",
927
+ page_type: "Preview Player"
928
+ },
929
+ audio: isAudio,
930
+ style: {
931
+ height: "100%",
932
+ width: "100%",
933
+ display: "block",
934
+ objectFit: "contain"
935
+ }
936
+ }
937
+ ),
938
+ children
939
+ ] }),
940
+ error ? /* @__PURE__ */ jsxRuntime.jsx(
941
+ "div",
792
942
  {
793
- ...props,
794
- playsInline: !0,
795
- playbackId: asset.playbackId,
796
- tokens: signedToken ? { playback: signedToken, thumbnail: signedToken, storyboard: signedToken } : void 0,
797
- preload: "metadata",
798
- crossOrigin: "anonymous",
799
- metadata: {
800
- player_name: "Sanity Admin Dashboard",
801
- player_version: "2.4.0",
802
- page_type: "Preview Player"
803
- },
804
- audio: isAudio,
805
943
  style: {
806
- height: "100%",
807
- width: "100%",
808
- display: "block",
809
- objectFit: "contain"
810
- }
944
+ position: "absolute",
945
+ top: "50%",
946
+ left: "50%",
947
+ transform: "translate(-50%, -50%)"
948
+ },
949
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { muted: !0, children: [
950
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ErrorOutlineIcon, { style: { marginRight: "0.15em" } }),
951
+ typeof error == "object" && "message" in error && typeof error.message == "string" ? error.message : "Error loading video"
952
+ ] })
811
953
  }
812
- ),
954
+ ) : null,
813
955
  children
814
956
  ] }),
815
- error ? /* @__PURE__ */ jsxRuntime.jsx(
816
- "div",
817
- {
818
- style: {
819
- position: "absolute",
820
- top: "50%",
821
- left: "50%",
822
- transform: "translate(-50%, -50%)"
823
- },
824
- children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { muted: !0, children: [
825
- /* @__PURE__ */ jsxRuntime.jsx(icons.ErrorOutlineIcon, { style: { marginRight: "0.15em" } }),
826
- typeof error == "object" && "message" in error && typeof error.message == "string" ? error.message : "Error loading video"
827
- ] })
828
- }
829
- ) : null,
830
- children
957
+ dialogState === "edit-thumbnail" && /* @__PURE__ */ jsxRuntime.jsx(EditThumbnailDialog, { asset, currentTime: muxPlayer?.current?.currentTime })
831
958
  ] });
832
959
  }
833
960
  function assetIsAudio(asset) {
@@ -1150,13 +1277,6 @@ const useDocReferences = sanity.createHookFromObservableFactory(({ documentStore
1150
1277
  apiVersion: SANITY_API_VERSION
1151
1278
  }
1152
1279
  ));
1153
- function formatSeconds(seconds) {
1154
- if (typeof seconds != "number" || Number.isNaN(seconds))
1155
- return "";
1156
- const hrs = ~~(seconds / 3600), mins = ~~(seconds % 3600 / 60), secs = ~~seconds % 60;
1157
- let ret = "";
1158
- return hrs > 0 && (ret += "" + hrs + ":" + (mins < 10 ? "0" : "")), ret += "" + mins + ":" + (secs < 10 ? "0" : ""), ret += "" + secs, ret;
1159
- }
1160
1280
  function getVideoMetadata(doc) {
1161
1281
  const id = doc.assetId || doc._id || "", date = doc.data?.created_at ? new Date(Number(doc.data.created_at) * 1e3) : new Date(doc._createdAt || doc._updatedAt || Date.now());
1162
1282
  return {
@@ -3279,7 +3399,7 @@ const FileButton = styledComponents.styled(ui.MenuItem)(({ theme }) => {
3279
3399
  `, LockButton = styledComponents.styled(ui.Button)`
3280
3400
  background: transparent;
3281
3401
  color: white;
3282
- `;
3402
+ `, isVideoAsset = (asset) => asset._type === "mux.videoAsset";
3283
3403
  function PlayerActionsMenu(props) {
3284
3404
  const { asset, readOnly, dialogState, setDialogState, onChange, onSelect } = props, [open, setOpen] = React.useState(!1), [menuElement, setMenuRef] = React.useState(null), isSigned = React.useMemo(() => getPlaybackPolicy(asset) === "signed", [asset]), onReset = React.useCallback(() => onChange(sanity.PatchEvent.from(sanity.unset([]))), [onChange]);
3285
3405
  return React.useEffect(() => {
@@ -3323,6 +3443,14 @@ function PlayerActionsMenu(props) {
3323
3443
  onClick: () => setDialogState("select-video")
3324
3444
  }
3325
3445
  ),
3446
+ isVideoAsset(asset) && /* @__PURE__ */ jsxRuntime.jsx(
3447
+ ui.MenuItem,
3448
+ {
3449
+ icon: icons.ImageIcon,
3450
+ text: "Thumbnail",
3451
+ onClick: () => setDialogState("edit-thumbnail")
3452
+ }
3453
+ ),
3326
3454
  /* @__PURE__ */ jsxRuntime.jsx(ui.MenuDivider, {}),
3327
3455
  /* @__PURE__ */ jsxRuntime.jsx(
3328
3456
  ui.MenuItem,
@@ -3941,7 +4069,7 @@ function Uploader(props) {
3941
4069
  case "commitUpload":
3942
4070
  return Object.assign({}, prev, { uploadStatus: { progress: 0 } });
3943
4071
  case "progressInfo": {
3944
- const { type, action: _, ...payload } = action;
4072
+ const { ...payload } = action;
3945
4073
  return Object.assign({}, prev, {
3946
4074
  uploadStatus: {
3947
4075
  ...prev.uploadStatus,
@@ -4060,7 +4188,7 @@ function Uploader(props) {
4060
4188
  idx > -1 && dragEnteredEls.current.splice(idx, 1), dragEnteredEls.current.length === 0 && setDragState(null);
4061
4189
  };
4062
4190
  if (state.error !== null) {
4063
- const error = { state };
4191
+ const error = {};
4064
4192
  return /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 3, direction: "column", justify: "center", align: "center", children: [
4065
4193
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 5, muted: !0, children: /* @__PURE__ */ jsxRuntime.jsx(icons.ErrorOutlineIcon, {}) }),
4066
4194
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "Something went wrong" }),
@@ -4103,20 +4231,27 @@ function Uploader(props) {
4103
4231
  onPaste: handlePaste,
4104
4232
  ref: containerRef,
4105
4233
  children: props.asset ? /* @__PURE__ */ jsxRuntime.jsx(
4106
- Player,
4234
+ DialogStateProvider,
4107
4235
  {
4108
- readOnly: props.readOnly,
4109
- asset: props.asset,
4110
- onChange: props.onChange,
4111
- buttons: /* @__PURE__ */ jsxRuntime.jsx(
4112
- PlayerActionsMenu$1,
4236
+ dialogState: props.dialogState,
4237
+ setDialogState: props.setDialogState,
4238
+ children: /* @__PURE__ */ jsxRuntime.jsx(
4239
+ Player,
4113
4240
  {
4241
+ readOnly: props.readOnly,
4114
4242
  asset: props.asset,
4115
- dialogState: props.dialogState,
4116
- setDialogState: props.setDialogState,
4117
4243
  onChange: props.onChange,
4118
- onSelect: handleUpload,
4119
- readOnly: props.readOnly
4244
+ buttons: /* @__PURE__ */ jsxRuntime.jsx(
4245
+ PlayerActionsMenu$1,
4246
+ {
4247
+ asset: props.asset,
4248
+ dialogState: props.dialogState,
4249
+ setDialogState: props.setDialogState,
4250
+ onChange: props.onChange,
4251
+ onSelect: handleUpload,
4252
+ readOnly: props.readOnly
4253
+ }
4254
+ )
4120
4255
  }
4121
4256
  )
4122
4257
  }