sanity-plugin-mux-input 2.9.1 → 2.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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"), 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"), 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"), suspendReact = require("suspend-react"), rxjs = require("rxjs"), styledComponents = require("styled-components"), uuid = require("@sanity/uuid"), operators = require("rxjs/operators"), MuxPlayer = require("@mux/mux-player-react"), 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");
23
23
  function _interopDefaultCompat(e) {
24
24
  return e && typeof e == "object" && "default" in e ? e : { default: e };
25
25
  }
@@ -106,92 +106,116 @@ function useAssets() {
106
106
  setSearchQuery
107
107
  };
108
108
  }
109
- function parseMuxDate(date) {
110
- return new Date(Number(date) * 1e3);
109
+ function useDialogState() {
110
+ return React.useState(!1);
111
+ }
112
+ function saveSecrets(client, token, secretKey, enableSignedUrls, signingKeyId, signingKeyPrivate) {
113
+ const doc = {
114
+ _id: "secrets.mux",
115
+ _type: "mux.apiKey",
116
+ token,
117
+ secretKey,
118
+ enableSignedUrls,
119
+ signingKeyId,
120
+ signingKeyPrivate
121
+ };
122
+ return client.createOrReplace(doc);
111
123
  }
112
- const FIRST_PAGE = 1, ASSETS_PER_PAGE = 100;
113
- async function fetchMuxAssetsPage({ secretKey, token }, pageNum) {
124
+ async function createSigningKeys(client) {
114
125
  try {
115
- const json = await (await fetch(
116
- `https://api.mux.com/video/v1/assets?limit=${ASSETS_PER_PAGE}&page=${pageNum}`,
117
- {
118
- headers: {
119
- Authorization: `Basic ${btoa(`${token}:${secretKey}`)}`
120
- }
121
- }
122
- )).json();
123
- return json.error ? {
124
- pageNum,
125
- error: {
126
- _tag: "MuxError",
127
- error: json.error
128
- }
129
- } : {
130
- pageNum,
131
- data: json.data
132
- };
133
- } catch {
134
- return {
135
- pageNum,
136
- error: { _tag: "FetchError" }
137
- };
126
+ const { dataset } = client.config();
127
+ return await client.request({
128
+ url: `/addons/mux/signing-keys/${dataset}`,
129
+ withCredentials: !0,
130
+ method: "POST"
131
+ });
132
+ } catch (error) {
133
+ console.error("Error creating signing keys", error);
134
+ const message = error.response?.statusCode === 401 ? 'Unauthorized - Failed to create the Signing Key. Please ensure that the token has "System" permissions' : error.message;
135
+ throw new Error(message);
138
136
  }
139
137
  }
140
- function accumulateIntermediateState(currentState, pageResult) {
141
- const currentData = "data" in currentState && currentState.data || [];
142
- return {
143
- ...currentState,
144
- data: [
145
- ...currentData,
146
- ...("data" in pageResult && pageResult.data || []).filter(
147
- // De-duplicate assets for safety
148
- (asset) => !currentData.some((a2) => a2.id === asset.id)
149
- )
150
- ],
151
- error: "error" in pageResult ? pageResult.error : (
152
- // Reset error if current page is successful
153
- void 0
154
- ),
155
- pageNum: pageResult.pageNum,
156
- loading: !0
157
- };
158
- }
159
- function hasMorePages(pageResult) {
160
- return typeof pageResult == "object" && "data" in pageResult && Array.isArray(pageResult.data) && pageResult.data.length > 0;
138
+ function testSecrets(client) {
139
+ const { dataset } = client.config();
140
+ return client.request({
141
+ url: `/addons/mux/secrets/${dataset}/test`,
142
+ withCredentials: !0,
143
+ method: "GET"
144
+ });
161
145
  }
162
- function useMuxAssets({ secrets, enabled }) {
163
- const [state, setState] = React.useState({ loading: !0, pageNum: FIRST_PAGE });
164
- return React.useEffect(() => {
165
- if (!enabled) return;
166
- const subscription = rxjs.defer(
167
- () => fetchMuxAssetsPage(
168
- secrets,
169
- // When we've already successfully loaded before (fully or partially), we start from the following page to avoid re-fetching
170
- "data" in state && state.data && state.data.length > 0 && !state.error ? state.pageNum + 1 : state.pageNum
171
- )
172
- ).pipe(
173
- // Here we replace "concatMap" with "expand" to recursively fetch next pages
174
- operators.expand((pageResult) => hasMorePages(pageResult) ? rxjs.timer(2e3).pipe(
175
- // eslint-disable-next-line max-nested-callbacks
176
- operators.concatMap(() => rxjs.defer(() => fetchMuxAssetsPage(secrets, pageResult.pageNum + 1)))
177
- ) : rxjs.of()),
178
- // On each iteration, persist intermediate states to give feedback to users
179
- operators.tap(
180
- (pageResult) => setState((prevState) => accumulateIntermediateState(prevState, pageResult))
181
- )
182
- ).subscribe({
183
- // Once done, let the user know we've stopped loading
184
- complete: () => {
185
- setState((prev) => ({
186
- ...prev,
187
- loading: !1
188
- }));
189
- }
146
+ async function haveValidSigningKeys(client, signingKeyId, signingKeyPrivate) {
147
+ if (!(signingKeyId && signingKeyPrivate))
148
+ return !1;
149
+ const { dataset } = client.config();
150
+ try {
151
+ const res = await client.request({
152
+ url: `/addons/mux/signing-keys/${dataset}/${signingKeyId}`,
153
+ withCredentials: !0,
154
+ method: "GET"
190
155
  });
191
- return () => subscription.unsubscribe();
192
- }, [enabled]), state;
156
+ return !!(res.data && res.data.id);
157
+ } catch {
158
+ return console.error("Error fetching signingKeyId", signingKeyId, "assuming it is not valid"), !1;
159
+ }
160
+ }
161
+ function testSecretsObservable(client) {
162
+ const { dataset } = client.config();
163
+ return rxjs.defer(
164
+ () => client.observable.request({
165
+ url: `/addons/mux/secrets/${dataset}/test`,
166
+ withCredentials: !0,
167
+ method: "GET"
168
+ })
169
+ );
193
170
  }
194
- const name = "mux-input", cacheNs = "sanity-plugin-mux-input", muxSecretsDocumentId = "secrets.mux", DIALOGS_Z_INDEX = 6e4, THUMBNAIL_ASPECT_RATIO = 1.7777777777777777, MIN_ASPECT_RATIO = 5 / 4, AUDIO_ASPECT_RATIO = 5 / 1, path$1 = ["token", "secretKey", "enableSignedUrls", "signingKeyId", "signingKeyPrivate"], useSecretsDocumentValues = () => {
171
+ const useSaveSecrets = (client, secrets) => React.useCallback(
172
+ async ({
173
+ token,
174
+ secretKey,
175
+ enableSignedUrls
176
+ }) => {
177
+ let { signingKeyId, signingKeyPrivate } = secrets;
178
+ try {
179
+ if (await saveSecrets(
180
+ client,
181
+ token,
182
+ secretKey,
183
+ enableSignedUrls,
184
+ signingKeyId,
185
+ signingKeyPrivate
186
+ ), !(await testSecrets(client))?.status && token && secretKey)
187
+ throw new Error("Invalid secrets");
188
+ } catch (err) {
189
+ throw console.error("Error while trying to save secrets:", err), err;
190
+ }
191
+ if (enableSignedUrls && !await haveValidSigningKeys(
192
+ client,
193
+ signingKeyId,
194
+ signingKeyPrivate
195
+ ))
196
+ try {
197
+ const { data } = await createSigningKeys(client);
198
+ signingKeyId = data.id, signingKeyPrivate = data.private_key, await saveSecrets(
199
+ client,
200
+ token,
201
+ secretKey,
202
+ enableSignedUrls,
203
+ signingKeyId,
204
+ signingKeyPrivate
205
+ );
206
+ } catch (err) {
207
+ throw console.log("Error while creating and saving signing key:", err?.message), err;
208
+ }
209
+ return {
210
+ token,
211
+ secretKey,
212
+ enableSignedUrls,
213
+ signingKeyId,
214
+ signingKeyPrivate
215
+ };
216
+ },
217
+ [client, secrets]
218
+ ), name = "mux-input", cacheNs = "sanity-plugin-mux-input", muxSecretsDocumentId = "secrets.mux", DIALOGS_Z_INDEX = 6e4, THUMBNAIL_ASPECT_RATIO = 1.7777777777777777, MIN_ASPECT_RATIO = 5 / 4, AUDIO_ASPECT_RATIO = 5 / 1, path$1 = ["token", "secretKey", "enableSignedUrls", "signingKeyId", "signingKeyPrivate"], useSecretsDocumentValues = () => {
195
219
  const { error, isLoading, value } = sanity.useDocumentValues(
196
220
  muxSecretsDocumentId,
197
221
  path$1
@@ -211,89 +235,32 @@ const name = "mux-input", cacheNs = "sanity-plugin-mux-input", muxSecretsDocumen
211
235
  }, [value]);
212
236
  return { error, isLoading, value: cache };
213
237
  };
214
- function useImportMuxAssets() {
215
- const documentStore = sanity.useDocumentStore(), client = sanity.useClient({
216
- apiVersion: SANITY_API_VERSION
217
- }), [assetsInSanity, assetsInSanityLoading] = useAssetsInSanity(documentStore), secretDocumentValues = useSecretsDocumentValues(), hasSecrets = !!secretDocumentValues.value.secrets?.secretKey, [importError, setImportError] = React.useState(), [importState, setImportState] = React.useState("closed"), dialogOpen = importState !== "closed", muxAssets = useMuxAssets({
218
- secrets: secretDocumentValues.value.secrets,
219
- enabled: hasSecrets && dialogOpen
220
- }), missingAssets = React.useMemo(() => assetsInSanity && muxAssets.data ? muxAssets.data.filter((a2) => !assetExistsInSanity(a2, assetsInSanity)) : void 0, [assetsInSanity, muxAssets.data]), [selectedAssets, setSelectedAssets] = React.useState([]), closeDialog = () => {
221
- importState !== "importing" && setImportState("closed");
222
- }, openDialog = () => {
223
- importState === "closed" && setImportState("idle");
224
- };
225
- async function importAssets() {
226
- setImportState("importing");
227
- const documents = selectedAssets.flatMap((asset) => muxAssetToSanityDocument(asset) || []), tx = client.transaction();
228
- documents.forEach((doc) => tx.create(doc));
229
- try {
230
- await tx.commit({ returnDocuments: !1 }), setSelectedAssets([]), setImportState("done");
231
- } catch (error) {
232
- setImportState("error"), setImportError(error);
233
- }
234
- }
238
+ function init({ token, secretKey, enableSignedUrls }) {
235
239
  return {
236
- assetsInSanityLoading,
237
- closeDialog,
238
- dialogOpen,
239
- importState,
240
- importError,
241
- hasSecrets,
242
- importAssets,
243
- missingAssets,
244
- muxAssets,
245
- openDialog,
246
- selectedAssets,
247
- setSelectedAssets
240
+ submitting: !1,
241
+ error: null,
242
+ // Form inputs don't set the state back to null when clearing a field, but uses empty strings
243
+ // This ensures the `dirty` check works correctly
244
+ token: token ?? "",
245
+ secretKey: secretKey ?? "",
246
+ enableSignedUrls: enableSignedUrls ?? !1
248
247
  };
249
248
  }
250
- function muxAssetToSanityDocument(asset) {
251
- const playbackId = (asset.playback_ids || []).find((p) => p.id)?.id;
252
- if (playbackId)
253
- return {
254
- _id: uuid.uuid(),
255
- _type: "mux.videoAsset",
256
- _updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
257
- _createdAt: parseMuxDate(asset.created_at).toISOString(),
258
- assetId: asset.id,
259
- playbackId,
260
- filename: asset.meta?.title ?? `Asset #${sanity.truncateString(asset.id, 15)}`,
261
- status: asset.status,
262
- data: asset
263
- };
264
- }
265
- const useAssetsInSanity = sanity.createHookFromObservableFactory(
266
- (documentStore) => documentStore.listenQuery(
267
- /* groq */
268
- `*[_type == "mux.videoAsset"] {
269
- "uploadId": coalesce(uploadId, data.upload_id),
270
- "assetId": coalesce(assetId, data.id),
271
- }`,
272
- {},
273
- {
274
- apiVersion: SANITY_API_VERSION
275
- }
276
- )
277
- );
278
- function assetExistsInSanity(asset, existingAssets) {
279
- return asset.status !== "ready" ? !1 : existingAssets.some(
280
- (existing) => existing.assetId === asset.id || existing.uploadId === asset.upload_id
281
- );
282
- }
283
- function useInView(ref, options = {}) {
284
- const [inView, setInView] = React.useState(!1);
285
- return React.useEffect(() => {
286
- if (!ref.current) return;
287
- const observer = new IntersectionObserver(([entry], obs) => {
288
- const nowInView = entry.isIntersecting && obs.thresholds.some((threshold) => entry.intersectionRatio >= threshold);
289
- setInView(nowInView), options?.onChange?.(nowInView);
290
- }, options), toObserve = ref.current;
291
- return observer.observe(toObserve), () => {
292
- toObserve && observer.unobserve(toObserve);
293
- };
294
- }, [options, ref]), inView;
249
+ function reducer(state, action) {
250
+ switch (action?.type) {
251
+ case "submit":
252
+ return { ...state, submitting: !0, error: null };
253
+ case "error":
254
+ return { ...state, submitting: !1, error: action.payload };
255
+ case "reset":
256
+ return init(action.payload);
257
+ case "change":
258
+ return { ...state, [action.payload.name]: action.payload.value };
259
+ default:
260
+ throw new Error(`Unknown action type: ${action?.type}`);
261
+ }
295
262
  }
296
- const _id = "secrets.mux";
263
+ const useSecretsFormState = (secrets) => React.useReducer(reducer, secrets, init), _id = "secrets.mux";
297
264
  function readSecrets(client) {
298
265
  const { projectId, dataset } = client.config();
299
266
  return suspendReact.suspend(async () => {
@@ -317,1904 +284,2173 @@ function readSecrets(client) {
317
284
  };
318
285
  }, [cacheNs, _id, projectId, dataset]);
319
286
  }
320
- function generateJwt(client, playbackId, aud, payload) {
321
- const { signingKeyId, signingKeyPrivate } = readSecrets(client);
322
- if (!signingKeyId)
323
- throw new TypeError("Missing `signingKeyId`.\n Check your plugin's configuration");
324
- if (!signingKeyPrivate)
325
- throw new TypeError("Missing `signingKeyPrivate`.\n Check your plugin's configuration");
326
- const { default: sign } = suspendReact.suspend(() => import("jsonwebtoken-esm/sign"), ["jsonwebtoken-esm/sign"]);
327
- return sign(
328
- payload ? JSON.parse(JSON.stringify(payload, (_, v) => v ?? void 0)) : {},
329
- atob(signingKeyPrivate),
330
- {
331
- algorithm: "RS256",
332
- keyid: signingKeyId,
333
- audience: aud,
334
- subject: playbackId,
335
- noTimestamp: !0,
336
- expiresIn: "12h"
337
- }
338
- );
339
- }
340
- function getPlaybackId(asset) {
341
- if (!asset?.playbackId)
342
- throw console.error("Asset is missing a playbackId", { asset }), new TypeError("Missing playbackId");
343
- return asset.playbackId;
344
- }
345
- function getPlaybackPolicy(asset) {
346
- return asset.data?.playback_ids?.find((playbackId) => asset.playbackId === playbackId.id)?.policy ?? "public";
347
- }
348
- function createUrlParamsObject(client, asset, params, audience) {
349
- const playbackId = getPlaybackId(asset);
350
- let searchParams = new URLSearchParams(
351
- JSON.parse(JSON.stringify(params, (_, v) => v ?? void 0))
352
- );
353
- if (getPlaybackPolicy(asset) === "signed") {
354
- const token = generateJwt(client, playbackId, audience, params);
355
- searchParams = new URLSearchParams({ token });
356
- }
357
- return { playbackId, searchParams };
358
- }
359
- function getAnimatedPosterSrc({
360
- asset,
361
- client,
362
- height,
363
- width,
364
- start = asset.thumbTime ? Math.max(0, asset.thumbTime - 2.5) : 0,
365
- end = start + 5,
366
- fps = 15
367
- }) {
368
- const params = { height, width, start, end, fps }, { playbackId, searchParams } = createUrlParamsObject(client, asset, params, "g");
369
- return `https://image.mux.com/${playbackId}/animated.gif?${searchParams}`;
370
- }
371
- function getPosterSrc({
372
- asset,
373
- client,
374
- fit_mode,
375
- height,
376
- time = asset.thumbTime ?? void 0,
377
- width
378
- }) {
379
- const params = { fit_mode, height, width };
380
- time !== void 0 && (params.time = time);
381
- const { playbackId, searchParams } = createUrlParamsObject(client, asset, params, "t");
382
- return `https://image.mux.com/${playbackId}/thumbnail.png?${searchParams}`;
383
- }
384
- const Image = styledComponents.styled.img`
385
- transition: opacity 0.175s ease-out 0s;
386
- display: block;
387
- width: 100%;
388
- height: 100%;
389
- object-fit: contain;
390
- object-position: center center;
391
- `, STATUS_TO_TONE = {
392
- loading: "transparent",
393
- error: "critical",
394
- loaded: "default"
395
- };
396
- function VideoThumbnail({
397
- asset,
398
- width,
399
- staticImage = !1
400
- }) {
401
- const ref = React.useRef(null), inView = useInView(ref), posterWidth = width || 250, [status, setStatus] = React.useState("loading"), client = useClient(), src = React.useMemo(() => {
402
- try {
403
- let thumbnail;
404
- return staticImage ? thumbnail = getPosterSrc({ asset, client, width: posterWidth }) : thumbnail = getAnimatedPosterSrc({ asset, client, width: posterWidth }), thumbnail;
405
- } catch {
406
- status !== "error" && setStatus("error");
407
- return;
408
- }
409
- }, [asset, client, posterWidth, status, staticImage]);
410
- function handleLoad() {
411
- setStatus("loaded");
412
- }
413
- function handleError() {
414
- setStatus("error");
415
- }
287
+ function MuxLogo({ height = 26 }) {
288
+ const id = React.useId(), fillColor = ui.useTheme_v2().color._dark ? "white" : "black", titleId = React.useMemo(() => `${id}-title`, [id]), pathStyle = {
289
+ fillRule: "nonzero"
290
+ };
416
291
  return /* @__PURE__ */ jsxRuntime.jsx(
417
- ui.Card,
292
+ "svg",
418
293
  {
419
- style: {
420
- aspectRatio: THUMBNAIL_ASPECT_RATIO,
421
- position: "relative",
422
- maxWidth: width ? `${width}px` : void 0,
423
- width: "100%",
424
- flex: 1
425
- },
426
- border: !0,
427
- radius: 2,
428
- ref,
429
- tone: STATUS_TO_TONE[status],
430
- children: inView ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
431
- status === "loading" && /* @__PURE__ */ jsxRuntime.jsx(
432
- ui.Box,
294
+ "aria-labelledby": titleId,
295
+ style: { height: `${height}px` },
296
+ viewBox: "0 0 1600 500",
297
+ version: "1.1",
298
+ xmlns: "http://www.w3.org/2000/svg",
299
+ xmlSpace: "preserve",
300
+ children: /* @__PURE__ */ jsxRuntime.jsxs("g", { id: "Layer-1", fill: fillColor, children: [
301
+ /* @__PURE__ */ jsxRuntime.jsx(
302
+ "path",
433
303
  {
434
- style: {
435
- position: "absolute",
436
- left: "50%",
437
- top: "50%",
438
- transform: "translate(-50%, -50%)"
439
- },
440
- children: /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, {})
304
+ d: "M994.287,93.486c-17.121,-0 -31,-13.879 -31,-31c0,-17.121 13.879,-31 31,-31c17.121,-0 31,13.879 31,31c0,17.121 -13.879,31 -31,31m0,-93.486c-34.509,-0 -62.484,27.976 -62.484,62.486l0,187.511c0,68.943 -56.09,125.033 -125.032,125.033c-68.942,-0 -125.03,-56.09 -125.03,-125.033l0,-187.511c0,-34.51 -27.976,-62.486 -62.485,-62.486c-34.509,-0 -62.484,27.976 -62.484,62.486l0,187.511c0,137.853 112.149,250.003 249.999,250.003c137.851,-0 250.001,-112.15 250.001,-250.003l0,-187.511c0,-34.51 -27.976,-62.486 -62.485,-62.486",
305
+ style: pathStyle
441
306
  }
442
307
  ),
443
- status === "error" && /* @__PURE__ */ jsxRuntime.jsxs(
444
- ui.Stack,
308
+ /* @__PURE__ */ jsxRuntime.jsx(
309
+ "path",
445
310
  {
446
- space: 4,
447
- style: {
448
- position: "absolute",
449
- width: "100%",
450
- left: 0,
451
- top: "50%",
452
- transform: "translateY(-50%)",
453
- justifyItems: "center"
454
- },
455
- children: [
456
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 4, muted: !0, children: /* @__PURE__ */ jsxRuntime.jsx(icons.ErrorOutlineIcon, { style: { fontSize: "1.75em" } }) }),
457
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { muted: !0, align: "center", children: "Failed loading thumbnail" })
458
- ]
311
+ d: "M1537.51,468.511c-17.121,-0 -31,-13.879 -31,-31c0,-17.121 13.879,-31 31,-31c17.121,-0 31,13.879 31,31c0,17.121 -13.879,31 -31,31m-275.883,-218.509l-143.33,143.329c-24.402,24.402 -24.402,63.966 0,88.368c24.402,24.402 63.967,24.402 88.369,-0l143.33,-143.329l143.328,143.329c24.402,24.4 63.967,24.402 88.369,-0c24.403,-24.402 24.403,-63.966 0.001,-88.368l-143.33,-143.329l0.001,-0.004l143.329,-143.329c24.402,-24.402 24.402,-63.965 0,-88.367c-24.402,-24.402 -63.967,-24.402 -88.369,-0l-143.329,143.328l-143.329,-143.328c-24.402,-24.401 -63.967,-24.402 -88.369,-0c-24.402,24.402 -24.402,63.965 0,88.367l143.329,143.329l0,0.004Z",
312
+ style: pathStyle
459
313
  }
460
314
  ),
461
315
  /* @__PURE__ */ jsxRuntime.jsx(
462
- Image,
316
+ "path",
463
317
  {
464
- src,
465
- alt: `Preview for ${staticImage ? "image" : "video"} ${asset.filename || asset.assetId}`,
466
- onLoad: handleLoad,
467
- onError: handleError,
468
- style: { opacity: status === "loaded" ? 1 : 0 }
318
+ d: "M437.511,468.521c-17.121,-0 -31,-13.879 -31,-31c0,-17.121 13.879,-31 31,-31c17.121,-0 31,13.879 31,31c0,17.121 -13.879,31 -31,31m23.915,-463.762c-23.348,-9.672 -50.226,-4.327 -68.096,13.544l-143.331,143.329l-143.33,-143.329c-17.871,-17.871 -44.747,-23.216 -68.096,-13.544c-23.349,9.671 -38.574,32.455 -38.574,57.729l0,375.026c0,34.51 27.977,62.486 62.487,62.486c34.51,-0 62.486,-27.976 62.486,-62.486l0,-224.173l80.843,80.844c24.404,24.402 63.965,24.402 88.369,-0l80.843,-80.844l0,224.173c0,34.51 27.976,62.486 62.486,62.486c34.51,-0 62.486,-27.976 62.486,-62.486l0,-375.026c0,-25.274 -15.224,-48.058 -38.573,-57.729",
319
+ style: pathStyle
469
320
  }
470
321
  )
471
- ] }) : null
322
+ ] })
472
323
  }
473
324
  );
474
325
  }
475
- const MissingAssetCheckbox = styledComponents.styled(ui.Checkbox)`
476
- position: static !important;
477
-
478
- input::after {
479
- content: '';
480
- position: absolute;
481
- inset: 0;
482
- display: block;
483
- cursor: pointer;
484
- z-index: 1000;
485
- }
486
- `;
487
- function MissingAsset({
488
- asset,
489
- selectAsset,
490
- selected
491
- }) {
492
- const duration = sanity.useFormattedDuration(asset.duration * 1e3);
493
- return /* @__PURE__ */ jsxRuntime.jsx(
494
- ui.Card,
495
- {
496
- tone: selected ? "positive" : void 0,
497
- border: !0,
498
- paddingX: 2,
499
- paddingY: 3,
500
- style: { position: "relative" },
501
- radius: 1,
502
- children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
503
- /* @__PURE__ */ jsxRuntime.jsx(
504
- MissingAssetCheckbox,
505
- {
506
- checked: selected,
507
- onChange: (e) => {
508
- selectAsset(e.currentTarget.checked);
509
- },
510
- "aria-label": selected ? `Import video ${asset.id}` : `Skip import of video ${asset.id}`
511
- }
512
- ),
513
- /* @__PURE__ */ jsxRuntime.jsx(
514
- VideoThumbnail,
515
- {
516
- asset: {
517
- assetId: asset.id,
518
- data: asset,
519
- filename: asset.id,
520
- playbackId: asset.playback_ids.find((p) => p.id)?.id
521
- },
522
- width: 150
523
- }
524
- ),
525
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
526
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 1, children: [
527
- /* @__PURE__ */ jsxRuntime.jsx(ui.Code, { size: 2, children: sanity.truncateString(asset.id, 15) }),
528
- " ",
529
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { muted: !0, size: 2, children: [
530
- "(",
531
- duration.formatted,
532
- ")"
533
- ] })
534
- ] }),
535
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, children: [
536
- "Uploaded at",
537
- " ",
538
- new Date(Number(asset.created_at) * 1e3).toLocaleDateString("en", {
539
- year: "numeric",
540
- day: "2-digit",
541
- month: "2-digit"
542
- })
543
- ] })
544
- ] })
545
- ] })
546
- },
547
- asset.id
548
- );
326
+ const Logo = styledComponents.styled.span`
327
+ display: inline-block;
328
+ height: 0.8em;
329
+ margin-right: 1em;
330
+ transform: translate(0.3em, -0.2em);
331
+ `, Header = () => /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
332
+ /* @__PURE__ */ jsxRuntime.jsx(Logo, { children: /* @__PURE__ */ jsxRuntime.jsx(MuxLogo, { height: 13 }) }),
333
+ "API Credentials"
334
+ ] });
335
+ function FormField(props) {
336
+ const { children, title, description, inputId } = props;
337
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 1, children: [
338
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { align: "flex-end", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { flex: 1, paddingY: 2, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
339
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "label", htmlFor: inputId, weight: "semibold", size: 1, children: title || /* @__PURE__ */ jsxRuntime.jsx("em", { children: "Untitled" }) }),
340
+ description && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { muted: !0, size: 1, children: description })
341
+ ] }) }) }),
342
+ /* @__PURE__ */ jsxRuntime.jsx("div", { children })
343
+ ] });
549
344
  }
550
- function ImportVideosDialog(props) {
551
- const { importState } = props, canTriggerImport = (importState === "idle" || importState === "error") && props.selectedAssets.length > 0, isImporting = importState === "importing", noAssetsToImport = props.missingAssets?.length === 0 && !props.muxAssets.loading && !props.assetsInSanityLoading;
552
- return /* @__PURE__ */ jsxRuntime.jsx(
553
- ui.Dialog,
554
- {
555
- animate: !0,
556
- header: "Import videos from Mux",
557
- zOffset: DIALOGS_Z_INDEX,
558
- id: "video-details-dialog",
559
- onClose: props.closeDialog,
560
- onClickOutside: props.closeDialog,
561
- width: 1,
562
- position: "fixed",
563
- footer: importState !== "done" && !noAssetsToImport && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 3, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "space-between", align: "center", children: [
564
- /* @__PURE__ */ jsxRuntime.jsx(
565
- ui.Button,
566
- {
567
- fontSize: 2,
568
- padding: 3,
569
- mode: "bleed",
570
- text: "Cancel",
571
- tone: "critical",
572
- onClick: props.closeDialog,
573
- disabled: isImporting
574
- }
575
- ),
576
- props.missingAssets && /* @__PURE__ */ jsxRuntime.jsx(
577
- ui.Button,
578
- {
579
- icon: icons.RetrieveIcon,
580
- fontSize: 2,
581
- padding: 3,
582
- mode: "ghost",
583
- text: props.selectedAssets?.length > 0 ? `Import ${props.selectedAssets.length} video(s)` : "No video(s) selected",
584
- tone: "positive",
585
- onClick: props.importAssets,
586
- iconRight: isImporting && ui.Spinner,
587
- disabled: !canTriggerImport
588
- }
589
- )
590
- ] }) }),
591
- children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Box, { padding: 3, children: [
592
- (props.muxAssets.loading || props.assetsInSanityLoading) && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { tone: "primary", marginBottom: 5, padding: 3, border: !0, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 4, children: [
593
- /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, { muted: !0, size: 4 }),
594
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
595
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, weight: "semibold", children: "Loading assets from Mux" }),
596
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, children: [
597
- "This may take a while.",
598
- props.missingAssets && props.missingAssets.length > 0 && ` There are at least ${props.missingAssets.length} video${props.missingAssets.length > 1 ? "s" : ""} currently not in Sanity...`
599
- ] })
600
- ] })
601
- ] }) }),
602
- props.muxAssets.error && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { tone: "critical", marginBottom: 5, padding: 3, border: !0, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
603
- /* @__PURE__ */ jsxRuntime.jsx(icons.ErrorOutlineIcon, { fontSize: 36 }),
604
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
605
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, weight: "semibold", children: "There was an error getting all data from Mux" }),
606
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, children: props.missingAssets ? `But we've found ${props.missingAssets.length} video${props.missingAssets.length > 1 ? "s" : ""} not in Sanity, which you can start importing now.` : "Please try again or contact a developer for help." })
607
- ] })
608
- ] }) }),
609
- importState === "importing" && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { tone: "primary", marginBottom: 5, padding: 3, border: !0, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 4, children: [
610
- /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, { muted: !0, size: 4 }),
611
- /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 2, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 2, weight: "semibold", children: [
612
- "Importing ",
613
- props.selectedAssets.length,
614
- " video",
615
- props.selectedAssets.length > 1 && "s",
616
- " from Mux"
617
- ] }) })
618
- ] }) }),
619
- importState === "error" && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { tone: "critical", marginBottom: 5, padding: 3, border: !0, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
620
- /* @__PURE__ */ jsxRuntime.jsx(icons.ErrorOutlineIcon, { fontSize: 36 }),
621
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
622
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, weight: "semibold", children: "There was an error importing videos" }),
623
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, children: props.importError ? `Error: ${props.importError}` : "Please try again or contact a developer for help." }),
624
- /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { marginTop: 1, children: /* @__PURE__ */ jsxRuntime.jsx(
625
- ui.Button,
345
+ var FormField$1 = React.memo(FormField);
346
+ const fieldNames = ["token", "secretKey", "enableSignedUrls"];
347
+ function ConfigureApiDialog({ secrets, setDialogState }) {
348
+ const client = useClient(), [state, dispatch] = useSecretsFormState(secrets), hasSecretsInitially = React.useMemo(() => secrets.token && secrets.secretKey, [secrets]), handleClose = React.useCallback(() => setDialogState(!1), [setDialogState]), dirty = React.useMemo(
349
+ () => secrets.token !== state.token || secrets.secretKey !== state.secretKey || secrets.enableSignedUrls !== state.enableSignedUrls,
350
+ [secrets, state]
351
+ ), id = `ConfigureApi${React.useId()}`, [tokenId, secretKeyId, enableSignedUrlsId] = React.useMemo(
352
+ () => fieldNames.map((field) => `${id}-${field}`),
353
+ [id]
354
+ ), firstField = React.useRef(null), handleSaveSecrets = useSaveSecrets(client, secrets), saving = React.useRef(!1), handleSubmit = React.useCallback(
355
+ (event) => {
356
+ if (event.preventDefault(), !saving.current && event.currentTarget.reportValidity()) {
357
+ saving.current = !0, dispatch({ type: "submit" });
358
+ const { token, secretKey, enableSignedUrls } = state;
359
+ handleSaveSecrets({ token, secretKey, enableSignedUrls }).then((savedSecrets) => {
360
+ const { projectId, dataset } = client.config();
361
+ suspendReact.clear([cacheNs, _id, projectId, dataset]), suspendReact.preload(() => Promise.resolve(savedSecrets), [cacheNs, _id, projectId, dataset]), setDialogState(!1);
362
+ }).catch((err) => dispatch({ type: "error", payload: err.message })).finally(() => {
363
+ saving.current = !1;
364
+ });
365
+ }
366
+ },
367
+ [client, dispatch, handleSaveSecrets, setDialogState, state]
368
+ ), handleChangeToken = React.useCallback(
369
+ (event) => {
370
+ dispatch({
371
+ type: "change",
372
+ payload: { name: "token", value: event.currentTarget.value }
373
+ });
374
+ },
375
+ [dispatch]
376
+ ), handleChangeSecretKey = React.useCallback(
377
+ (event) => {
378
+ dispatch({
379
+ type: "change",
380
+ payload: { name: "secretKey", value: event.currentTarget.value }
381
+ });
382
+ },
383
+ [dispatch]
384
+ ), handleChangeEnableSignedUrls = React.useCallback(
385
+ (event) => {
386
+ dispatch({
387
+ type: "change",
388
+ payload: { name: "enableSignedUrls", value: event.currentTarget.checked }
389
+ });
390
+ },
391
+ [dispatch]
392
+ );
393
+ return React.useEffect(() => {
394
+ firstField.current && firstField.current.focus();
395
+ }, [firstField]), /* @__PURE__ */ jsxRuntime.jsx(
396
+ ui.Dialog,
397
+ {
398
+ animate: !0,
399
+ id,
400
+ onClose: handleClose,
401
+ onClickOutside: handleClose,
402
+ header: /* @__PURE__ */ jsxRuntime.jsx(Header, {}),
403
+ zOffset: DIALOGS_Z_INDEX,
404
+ position: "fixed",
405
+ width: 1,
406
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { padding: 3, children: /* @__PURE__ */ jsxRuntime.jsx("form", { onSubmit: handleSubmit, noValidate: !0, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, children: [
407
+ !hasSecretsInitially && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: [3, 3, 3], radius: 2, shadow: 1, tone: "primary", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
408
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, children: [
409
+ "To set up a new access token, go to your",
410
+ " ",
411
+ /* @__PURE__ */ jsxRuntime.jsx(
412
+ "a",
626
413
  {
627
- icon: icons.RetryIcon,
628
- text: "Retry",
629
- tone: "primary",
630
- onClick: props.importAssets
414
+ href: "https://dashboard.mux.com/settings/access-tokens",
415
+ target: "_blank",
416
+ rel: "noreferrer noopener",
417
+ children: "account on mux.com"
631
418
  }
632
- ) })
419
+ ),
420
+ "."
421
+ ] }),
422
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, children: [
423
+ "The access token needs permissions: ",
424
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: "Mux Video " }),
425
+ "(Full Access) and ",
426
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: "Mux Data" }),
427
+ " (Read)",
428
+ /* @__PURE__ */ jsxRuntime.jsx("br", {}),
429
+ "To use Signed URLs, the token must also have System permissions.",
430
+ /* @__PURE__ */ jsxRuntime.jsx("br", {}),
431
+ "The credentials will be stored safely in a hidden document only available to editors."
633
432
  ] })
634
433
  ] }) }),
635
- (noAssetsToImport || importState === "done") && /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { paddingY: 5, marginBottom: 4, space: 3, style: { textAlign: "center" }, children: [
636
- /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(icons.CheckmarkCircleIcon, { fontSize: 48 }) }),
637
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { size: 2, children: importState === "done" ? "Videos imported successfully" : "There are no Mux videos to import" }),
638
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, children: importState === "done" ? "You can now use them in your Sanity content." : "They're all in Sanity and ready to be used in your content." })
639
- ] }),
640
- props.missingAssets && props.missingAssets.length > 0 && (importState === "idle" || importState === "error") && /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, children: [
641
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Heading, { size: 1, children: [
642
- "There are ",
643
- props.missingAssets.length,
644
- props.muxAssets.loading && "+",
645
- " Mux video",
646
- props.missingAssets.length > 1 && "s",
647
- " ",
648
- "not in Sanity"
649
- ] }),
650
- !props.muxAssets.loading && /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", paddingX: 2, children: [
434
+ /* @__PURE__ */ jsxRuntime.jsx(FormField$1, { title: "Access Token", inputId: tokenId, children: /* @__PURE__ */ jsxRuntime.jsx(
435
+ ui.TextInput,
436
+ {
437
+ id: tokenId,
438
+ ref: firstField,
439
+ onChange: handleChangeToken,
440
+ type: "text",
441
+ value: state.token ?? "",
442
+ required: !!state.secretKey || state.enableSignedUrls
443
+ }
444
+ ) }),
445
+ /* @__PURE__ */ jsxRuntime.jsx(FormField$1, { title: "Secret Key", inputId: secretKeyId, children: /* @__PURE__ */ jsxRuntime.jsx(
446
+ ui.TextInput,
447
+ {
448
+ id: secretKeyId,
449
+ onChange: handleChangeSecretKey,
450
+ type: "text",
451
+ value: state.secretKey ?? "",
452
+ required: !!state.token || state.enableSignedUrls
453
+ }
454
+ ) }),
455
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, children: [
456
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", children: [
651
457
  /* @__PURE__ */ jsxRuntime.jsx(
652
458
  ui.Checkbox,
653
459
  {
654
- id: "import-all",
655
- style: { display: "block" },
656
- onClick: (e) => {
657
- e.currentTarget.checked ? props.missingAssets && props.setSelectedAssets(props.missingAssets) : props.setSelectedAssets([]);
658
- },
659
- checked: props.selectedAssets.length === props.missingAssets.length
460
+ id: enableSignedUrlsId,
461
+ onChange: handleChangeEnableSignedUrls,
462
+ checked: state.enableSignedUrls,
463
+ style: { display: "block" }
660
464
  }
661
465
  ),
662
- /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { flex: 1, paddingLeft: 3, as: "label", htmlFor: "import-all", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "Import all" }) })
466
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { flex: 1, paddingLeft: 3, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: enableSignedUrlsId, children: "Enable Signed Urls" }) }) })
663
467
  ] }),
664
- props.missingAssets.map((asset) => /* @__PURE__ */ jsxRuntime.jsx(
665
- MissingAsset,
468
+ secrets.signingKeyId && state.enableSignedUrls ? /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: [3, 3, 3], radius: 2, shadow: 1, tone: "caution", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
469
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, children: "The signing key ID that Sanity will use is:" }),
470
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Code, { size: 1, children: secrets.signingKeyId }),
471
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, children: [
472
+ "This key is only used for previewing content in the Sanity UI.",
473
+ /* @__PURE__ */ jsxRuntime.jsx("br", {}),
474
+ "You should generate a different key to use in your application server."
475
+ ] })
476
+ ] }) }) : null
477
+ ] }),
478
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Inline, { space: 2, children: [
479
+ /* @__PURE__ */ jsxRuntime.jsx(
480
+ ui.Button,
666
481
  {
667
- asset,
668
- selectAsset: (selected) => {
669
- selected ? props.setSelectedAssets([...props.selectedAssets, asset]) : props.setSelectedAssets(props.selectedAssets.filter((a2) => a2.id !== asset.id));
670
- },
671
- selected: props.selectedAssets.some((a2) => a2.id === asset.id)
672
- },
673
- asset.id
674
- ))
675
- ] })
676
- ] })
482
+ text: "Save",
483
+ disabled: !dirty,
484
+ loading: state.submitting,
485
+ tone: "primary",
486
+ mode: "default",
487
+ type: "submit"
488
+ }
489
+ ),
490
+ /* @__PURE__ */ jsxRuntime.jsx(
491
+ ui.Button,
492
+ {
493
+ disabled: state.submitting,
494
+ text: "Cancel",
495
+ mode: "bleed",
496
+ onClick: handleClose
497
+ }
498
+ )
499
+ ] }),
500
+ state.error && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: [3, 3, 3], radius: 2, shadow: 1, tone: "critical", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: state.error }) })
501
+ ] }) }) })
677
502
  }
678
503
  );
679
504
  }
680
- function ImportVideosFromMux() {
681
- const importAssets = useImportMuxAssets();
682
- if (importAssets.hasSecrets)
683
- return importAssets.dialogOpen ? /* @__PURE__ */ jsxRuntime.jsx(ImportVideosDialog, { ...importAssets }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { mode: "bleed", text: "Import from Mux", onClick: importAssets.openDialog });
505
+ function ConfigureApi() {
506
+ const [dialogOpen, setDialogOpen] = useDialogState(), secretDocumentValues = useSecretsDocumentValues(), openDialog = React.useCallback(() => setDialogOpen("secrets"), [setDialogOpen]);
507
+ return dialogOpen === "secrets" ? /* @__PURE__ */ jsxRuntime.jsx(
508
+ ConfigureApiDialog,
509
+ {
510
+ secrets: secretDocumentValues.value.secrets,
511
+ setDialogState: setDialogOpen
512
+ }
513
+ ) : /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { mode: "bleed", text: "Configure plugin", onClick: openDialog });
684
514
  }
685
- const CONTEXT_MENU_POPOVER_PROPS = {
686
- constrainSize: !0,
687
- placement: "bottom",
688
- portal: !0,
689
- width: 0
690
- };
691
- function SelectSortOptions(props) {
692
- const id = React.useId();
693
- return /* @__PURE__ */ jsxRuntime.jsx(
694
- ui.MenuButton,
695
- {
696
- button: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { text: "Sort", icon: icons.SortIcon, mode: "bleed", padding: 3, style: { cursor: "pointer" } }),
697
- id,
698
- menu: /* @__PURE__ */ jsxRuntime.jsx(ui.Menu, { children: Object.entries(ASSET_SORT_OPTIONS).map(([type, { label }]) => /* @__PURE__ */ jsxRuntime.jsx(
699
- ui.MenuItem,
700
- {
701
- "data-as": "button",
702
- onClick: () => props.setSort(type),
703
- padding: 3,
704
- tone: "default",
705
- text: label,
706
- pressed: type === props.sort
707
- },
708
- type
709
- )) }),
710
- popover: CONTEXT_MENU_POPOVER_PROPS
711
- }
712
- );
515
+ function generateAssetPlaceholder(assetId) {
516
+ return `Asset #${sanity.truncateString(assetId, 15)}`;
713
517
  }
714
- const SpinnerBox = () => /* @__PURE__ */ jsxRuntime.jsx(
715
- ui.Box,
716
- {
717
- style: {
718
- display: "flex",
719
- alignItems: "center",
720
- justifyContent: "center",
721
- minHeight: "150px"
722
- },
723
- children: /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, {})
724
- }
725
- );
726
- function FormField(props) {
727
- const { children, title, description, inputId } = props;
728
- return /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 1, children: [
729
- /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { align: "flex-end", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { flex: 1, paddingY: 2, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
730
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "label", htmlFor: inputId, weight: "semibold", size: 1, children: title || /* @__PURE__ */ jsxRuntime.jsx("em", { children: "Untitled" }) }),
731
- description && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { muted: !0, size: 1, children: description })
732
- ] }) }) }),
733
- /* @__PURE__ */ jsxRuntime.jsx("div", { children })
734
- ] });
518
+ function isEmptyOrPlaceholderTitle(filename, assetId) {
519
+ if (!filename || filename.trim() === "")
520
+ return !0;
521
+ const placeholder = generateAssetPlaceholder(assetId);
522
+ return filename === placeholder;
735
523
  }
736
- var FormField$1 = React.memo(FormField);
737
- const IconInfo = (props) => {
738
- const Icon = props.icon;
739
- return /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 2, align: "center", padding: 1, children: [
740
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: (props.size || 1) + 1, muted: !0, children: /* @__PURE__ */ jsxRuntime.jsx(Icon, {}) }),
741
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: props.size || 1, muted: props.muted, children: props.text })
742
- ] });
743
- };
744
- function ResolutionIcon(props) {
745
- return /* @__PURE__ */ jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "1em", height: "1em", viewBox: "0 0 24 24", ...props, children: /* @__PURE__ */ jsxRuntime.jsx(
746
- "path",
747
- {
748
- fill: "currentColor",
749
- d: "M20 9V6h-3V4h5v5h-2ZM2 9V4h5v2H4v3H2Zm15 11v-2h3v-3h2v5h-5ZM2 20v-5h2v3h3v2H2Zm4-4V8h12v8H6Zm2-2h8v-4H8v4Zm0 0v-4v4Z"
750
- }
751
- ) });
524
+ function parseMuxDate(date) {
525
+ return new Date(Number(date) * 1e3);
752
526
  }
753
- function StopWatchIcon(props) {
754
- return /* @__PURE__ */ jsxRuntime.jsxs(
755
- "svg",
756
- {
757
- xmlns: "http://www.w3.org/2000/svg",
758
- width: "1em",
759
- height: "1em",
760
- viewBox: "0 0 512 512",
761
- ...props,
762
- children: [
763
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M232 306.667h48V176h-48v130.667z", fill: "currentColor" }),
764
- /* @__PURE__ */ jsxRuntime.jsx(
765
- "path",
766
- {
767
- 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",
768
- fill: "currentColor"
769
- }
770
- ),
771
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M192 32h128v48H192z", fill: "currentColor" })
772
- ]
773
- }
774
- );
527
+ function deleteAssetOnMux(client, assetId) {
528
+ const { dataset } = client.config();
529
+ return client.request({
530
+ url: `/addons/mux/assets/${dataset}/${assetId}`,
531
+ withCredentials: !0,
532
+ method: "DELETE"
533
+ });
775
534
  }
776
- const DialogStateContext = React.createContext({
777
- dialogState: !1,
778
- setDialogState: () => null
779
- }), DialogStateProvider = ({
780
- dialogState,
781
- setDialogState,
782
- children
783
- }) => /* @__PURE__ */ jsxRuntime.jsx(DialogStateContext.Provider, { value: { dialogState, setDialogState }, children }), useDialogStateContext = () => React.useContext(DialogStateContext);
784
- function getVideoSrc({ asset, client }) {
785
- const playbackId = getPlaybackId(asset), searchParams = new URLSearchParams();
786
- if (getPlaybackPolicy(asset) === "signed") {
787
- const token = generateJwt(client, playbackId, "v");
788
- searchParams.set("token", token);
535
+ async function deleteAsset({
536
+ client,
537
+ asset,
538
+ deleteOnMux
539
+ }) {
540
+ if (!asset?._id) return !0;
541
+ try {
542
+ await client.delete(asset._id);
543
+ } catch {
544
+ return "failed-sanity";
789
545
  }
790
- return `https://stream.mux.com/${playbackId}.m3u8?${searchParams}`;
546
+ if (deleteOnMux && asset?.assetId)
547
+ try {
548
+ await deleteAssetOnMux(client, asset.assetId);
549
+ } catch {
550
+ return "failed-mux";
551
+ }
552
+ return !0;
791
553
  }
792
- function getDevicePixelRatio(options) {
793
- const {
794
- defaultDpr = 1,
795
- maxDpr = 3,
796
- round = !0
797
- } = options || {}, dpr = typeof window < "u" && typeof window.devicePixelRatio == "number" ? window.devicePixelRatio : defaultDpr;
798
- return Math.min(Math.max(1, round ? Math.floor(dpr) : dpr), maxDpr);
554
+ function getAsset(client, assetId) {
555
+ const { dataset } = client.config();
556
+ return client.request({
557
+ url: `/addons/mux/assets/${dataset}/data/${assetId}`,
558
+ withCredentials: !0,
559
+ method: "GET"
560
+ });
799
561
  }
800
- function formatSeconds(seconds) {
801
- if (typeof seconds != "number" || Number.isNaN(seconds))
802
- return "";
803
- const hrs = ~~(seconds / 3600), mins = ~~(seconds % 3600 / 60), secs = ~~seconds % 60;
804
- let ret = "";
805
- return hrs > 0 && (ret += "" + hrs + ":" + (mins < 10 ? "0" : "")), ret += "" + mins + ":" + (secs < 10 ? "0" : ""), ret += "" + secs, ret;
562
+ function listAssets(client, options) {
563
+ const { dataset } = client.config(), query = {};
564
+ return options.limit && (query.limit = options.limit.toString()), options.cursor && (query.cursor = options.cursor), client.request({
565
+ url: `/addons/mux/assets/${dataset}/data/list`,
566
+ withCredentials: !0,
567
+ method: "GET",
568
+ query
569
+ });
806
570
  }
807
- function formatSecondsToHHMMSS(seconds) {
808
- 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");
809
- return `${hrs}:${mins}:${secs}`;
571
+ const ASSETS_PER_PAGE = 100;
572
+ async function fetchMuxAssetsPage(client, cursor) {
573
+ try {
574
+ const response = await listAssets(client, {
575
+ limit: ASSETS_PER_PAGE,
576
+ cursor
577
+ });
578
+ return {
579
+ cursor,
580
+ data: response.data,
581
+ next_cursor: response.next_cursor || null
582
+ };
583
+ } catch {
584
+ return {
585
+ cursor,
586
+ error: { _tag: "FetchError" }
587
+ };
588
+ }
810
589
  }
811
- function isValidTimeFormat(time) {
812
- return /^([0-1]?[0-9]|2[0-3]):([0-5]?[0-9]):([0-5]?[0-9])$/.test(time) || time === "";
590
+ function accumulateIntermediateState(currentState, pageResult) {
591
+ const currentData = "data" in currentState && currentState.data || [], newAssets = "data" in pageResult && pageResult.data || [], { validAssets, skippedInThisPage } = newAssets.reduce(
592
+ (acc, asset) => {
593
+ const hasPlaybackIds = asset.playback_ids && asset.playback_ids.length > 0, isDuplicate = currentData.some((a2) => a2.id === asset.id);
594
+ return hasPlaybackIds || (acc.skippedInThisPage = !0), hasPlaybackIds && !isDuplicate && acc.validAssets.push(asset), acc;
595
+ },
596
+ { validAssets: [], skippedInThisPage: !1 }
597
+ );
598
+ return {
599
+ ...currentState,
600
+ data: [...currentData, ...validAssets],
601
+ error: "error" in pageResult ? pageResult.error : (
602
+ // Reset error if current page is successful
603
+ void 0
604
+ ),
605
+ cursor: "next_cursor" in pageResult ? pageResult.next_cursor : pageResult.cursor,
606
+ loading: !0,
607
+ hasSkippedAssetsWithoutPlayback: currentState.hasSkippedAssetsWithoutPlayback || skippedInThisPage
608
+ };
813
609
  }
814
- function getSecondsFromTimeFormat(time) {
815
- const [hh = 0, mm = 0, ss = 0] = time.split(":").map(Number);
816
- return hh * 3600 + mm * 60 + ss;
610
+ function hasMorePages(pageResult) {
611
+ return typeof pageResult == "object" && "next_cursor" in pageResult && pageResult.next_cursor !== null;
817
612
  }
818
- function EditThumbnailDialog({ asset, currentTime = 0 }) {
819
- const client = useClient(), { setDialogState } = useDialogStateContext(), dialogId = `EditThumbnailDialog${React.useId()}`, [timeFormatted, setTimeFormatted] = React.useState(
820
- () => formatSecondsToHHMMSS(currentTime)
821
- ), [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 = () => {
822
- setSaving(!0), client.patch(asset._id).set({ thumbTime: nextTime }).commit({ returnDocuments: !1 }).then(() => void setDialogState(!1)).catch(setSaveThumbnailError).finally(() => void setSaving(!1));
823
- }, width = 300 * getDevicePixelRatio({ maxDpr: 2 });
824
- if (saveThumbnailError)
825
- throw saveThumbnailError;
826
- return /* @__PURE__ */ jsxRuntime.jsx(
827
- ui.Dialog,
828
- {
829
- id: dialogId,
830
- header: "Edit thumbnail",
831
- onClose: () => setDialogState(!1),
832
- footer: /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { padding: 3, children: /* @__PURE__ */ jsxRuntime.jsx(
833
- ui.Button,
834
- {
835
- disabled: inputError !== "",
836
- mode: "ghost",
837
- tone: "primary",
838
- loading: saving,
839
- onClick: handleSave,
840
- text: "Set new thumbnail"
841
- },
842
- "thumbnail"
843
- ) }),
844
- children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, padding: 3, children: [
845
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
846
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, weight: "semibold", children: "Current:" }),
847
- /* @__PURE__ */ jsxRuntime.jsx(VideoThumbnail, { asset, width, staticImage: !0 })
848
- ] }),
849
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
850
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, weight: "semibold", children: "New:" }),
851
- /* @__PURE__ */ jsxRuntime.jsx(VideoThumbnail, { asset: assetWithNewThumbnail, width, staticImage: !0 })
852
- ] }),
853
- /* @__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" }) }) }),
854
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
855
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, weight: "semibold", children: "Selected time for thumbnail (hh:mm:ss):" }),
856
- /* @__PURE__ */ jsxRuntime.jsx(
857
- ui.TextInput,
858
- {
859
- size: 1,
860
- value: timeFormatted,
861
- placeholder: "hh:mm:ss",
862
- onChange: (event) => {
863
- const value = event.currentTarget.value;
864
- if (setTimeFormatted(value), isValidTimeFormat(value)) {
865
- setInputError("");
866
- const totalSeconds = getSecondsFromTimeFormat(value);
867
- setNextTime(totalSeconds);
868
- } else
869
- setInputError("Invalid time format");
870
- },
871
- customValidity: inputError
872
- }
613
+ function useMuxAssets({ client, enabled }) {
614
+ const [state, setState] = React.useState({ loading: !0, cursor: null });
615
+ return React.useEffect(() => {
616
+ if (!enabled) return;
617
+ const subscription = rxjs.defer(
618
+ () => fetchMuxAssetsPage(
619
+ client,
620
+ // When we've already successfully loaded before (fully or partially), we start from the next cursor to avoid re-fetching
621
+ "data" in state && state.data && state.data.length > 0 && !state.error ? state.cursor : null
622
+ )
623
+ ).pipe(
624
+ // Here we use "expand" to recursively fetch next pages
625
+ operators.expand((pageResult) => hasMorePages(pageResult) ? rxjs.timer(2e3).pipe(
626
+ operators.concatMap(
627
+ () => (
628
+ // eslint-disable-next-line max-nested-callbacks
629
+ rxjs.defer(
630
+ () => fetchMuxAssetsPage(
631
+ client,
632
+ "next_cursor" in pageResult ? pageResult.next_cursor : null
633
+ )
634
+ )
873
635
  )
874
- ] })
875
- ] })
876
- }
877
- );
878
- }
879
- function VideoPlayer({
880
- asset,
881
- thumbnailWidth = 250,
882
- children,
883
- ...props
884
- }) {
885
- const client = useClient(), { dialogState } = useDialogStateContext(), isAudio = assetIsAudio(asset), muxPlayer = React.useRef(null), thumbnail = getPosterSrc({ asset, client, width: thumbnailWidth }), { src: videoSrc, error } = React.useMemo(() => {
886
- try {
887
- const src = asset?.playbackId && getVideoSrc({ client, asset });
888
- return src ? { src } : { error: new TypeError("Asset has no playback ID") };
889
- } catch (error2) {
890
- return { error: error2 };
891
- }
892
- }, [asset, client]), signedToken = React.useMemo(() => {
893
- try {
894
- return new URL(videoSrc).searchParams.get("token");
895
- } catch {
896
- return !1;
897
- }
898
- }, [videoSrc]), [width, height] = (asset?.data?.aspect_ratio ?? "16:9").split(":").map(Number), targetAspectRatio = props.forceAspectRatio || (Number.isNaN(width) ? 16 / 9 : width / height);
899
- let aspectRatio = Math.max(MIN_ASPECT_RATIO, targetAspectRatio);
900
- return isAudio && (aspectRatio = props.forceAspectRatio ? (
901
- // Make it wider when forcing aspect ratio to balance with videos' rendering height (audio players overflow a bit)
902
- props.forceAspectRatio * 1.2
903
- ) : AUDIO_ASPECT_RATIO), /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
904
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Card, { tone: "transparent", style: { aspectRatio, position: "relative" }, children: [
905
- videoSrc && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
906
- /* @__PURE__ */ jsxRuntime.jsx(
907
- MuxPlayer__default.default,
908
- {
909
- poster: thumbnail,
910
- ref: muxPlayer,
911
- ...props,
912
- playsInline: !0,
913
- playbackId: asset.playbackId,
914
- tokens: signedToken ? { playback: signedToken, thumbnail: signedToken, storyboard: signedToken } : void 0,
915
- preload: "metadata",
916
- crossOrigin: "anonymous",
917
- metadata: {
918
- player_name: "Sanity Admin Dashboard",
919
- player_version: "2.9.1",
920
- page_type: "Preview Player"
921
- },
922
- audio: isAudio,
923
- style: {
924
- height: "100%",
925
- width: "100%",
926
- display: "block",
927
- objectFit: "contain"
928
- }
929
- }
930
- ),
931
- children
932
- ] }),
933
- error ? /* @__PURE__ */ jsxRuntime.jsx(
934
- "div",
935
- {
936
- style: {
937
- position: "absolute",
938
- top: "50%",
939
- left: "50%",
940
- transform: "translate(-50%, -50%)"
941
- },
942
- children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { muted: !0, children: [
943
- /* @__PURE__ */ jsxRuntime.jsx(icons.ErrorOutlineIcon, { style: { marginRight: "0.15em" } }),
944
- typeof error == "object" && "message" in error && typeof error.message == "string" ? error.message : "Error loading video"
945
- ] })
946
- }
947
- ) : null,
948
- children
949
- ] }),
950
- dialogState === "edit-thumbnail" && /* @__PURE__ */ jsxRuntime.jsx(EditThumbnailDialog, { asset, currentTime: muxPlayer?.current?.currentTime })
951
- ] });
952
- }
953
- function assetIsAudio(asset) {
954
- return asset.data?.max_stored_resolution === "Audio only";
955
- }
956
- function deleteAssetOnMux(client, assetId) {
957
- const { dataset } = client.config();
958
- return client.request({
959
- url: `/addons/mux/assets/${dataset}/${assetId}`,
960
- withCredentials: !0,
961
- method: "DELETE"
962
- });
636
+ )
637
+ ) : rxjs.of()),
638
+ // On each iteration, persist intermediate states to give feedback to users
639
+ operators.tap(
640
+ (pageResult) => setState((prevState) => accumulateIntermediateState(prevState, pageResult))
641
+ )
642
+ ).subscribe({
643
+ // Once done, let the user know we've stopped loading
644
+ complete: () => {
645
+ setState((prev) => ({
646
+ ...prev,
647
+ loading: !1
648
+ }));
649
+ }
650
+ });
651
+ return () => subscription.unsubscribe();
652
+ }, [enabled]), state;
963
653
  }
964
- async function deleteAsset({
965
- client,
966
- asset,
967
- deleteOnMux
968
- }) {
969
- if (!asset?._id) return !0;
970
- try {
971
- await client.delete(asset._id);
972
- } catch {
973
- return "failed-sanity";
974
- }
975
- if (deleteOnMux && asset?.assetId)
654
+ function useImportMuxAssets() {
655
+ const documentStore = sanity.useDocumentStore(), client = sanity.useClient({
656
+ apiVersion: SANITY_API_VERSION
657
+ }), [assetsInSanity, assetsInSanityLoading] = useAssetsInSanity(documentStore), hasSecrets = !!useSecretsDocumentValues().value.secrets?.secretKey, [importError, setImportError] = React.useState(), [importState, setImportState] = React.useState("closed"), dialogOpen = importState !== "closed", muxAssets = useMuxAssets({
658
+ client,
659
+ enabled: hasSecrets && dialogOpen
660
+ }), missingAssets = React.useMemo(() => assetsInSanity && muxAssets.data ? muxAssets.data.filter((a2) => !assetExistsInSanity(a2, assetsInSanity)) : void 0, [assetsInSanity, muxAssets.data]), [selectedAssets, setSelectedAssets] = React.useState([]), closeDialog = () => {
661
+ importState !== "importing" && setImportState("closed");
662
+ }, openDialog = () => {
663
+ importState === "closed" && setImportState("idle");
664
+ };
665
+ async function importAssets() {
666
+ setImportState("importing");
667
+ const documents = selectedAssets.flatMap((asset) => muxAssetToSanityDocument(asset) || []), tx = client.transaction();
668
+ documents.forEach((doc) => tx.create(doc));
976
669
  try {
977
- await deleteAssetOnMux(client, asset.assetId);
978
- } catch {
979
- return "failed-mux";
670
+ await tx.commit({ returnDocuments: !1 }), setSelectedAssets([]), setImportState("done");
671
+ } catch (error) {
672
+ setImportState("error"), setImportError(error);
980
673
  }
981
- return !0;
982
- }
983
- function getAsset(client, assetId) {
984
- const { dataset } = client.config();
985
- return client.request({
986
- url: `/addons/mux/assets/${dataset}/data/${assetId}`,
987
- withCredentials: !0,
988
- method: "GET"
989
- });
990
- }
991
- const getUnknownTypeFallback = (id, typeName) => ({
992
- title: /* @__PURE__ */ jsxRuntime.jsxs("em", { children: [
993
- "No schema found for type ",
994
- /* @__PURE__ */ jsxRuntime.jsx("code", { children: typeName })
995
- ] }),
996
- subtitle: /* @__PURE__ */ jsxRuntime.jsxs("em", { children: [
997
- "Document: ",
998
- /* @__PURE__ */ jsxRuntime.jsx("code", { children: id })
999
- ] }),
1000
- media: () => /* @__PURE__ */ jsxRuntime.jsx(icons.WarningOutlineIcon, {})
1001
- });
1002
- function MissingSchemaType(props) {
1003
- const { layout, value } = props;
1004
- return /* @__PURE__ */ jsxRuntime.jsx(sanity.SanityDefaultPreview, { ...getUnknownTypeFallback(value._id, value._type), layout });
674
+ }
675
+ return {
676
+ assetsInSanityLoading,
677
+ closeDialog,
678
+ dialogOpen,
679
+ importState,
680
+ importError,
681
+ hasSecrets,
682
+ importAssets,
683
+ missingAssets,
684
+ muxAssets,
685
+ openDialog,
686
+ selectedAssets,
687
+ setSelectedAssets
688
+ };
1005
689
  }
1006
- function TimeAgo({ time }) {
1007
- const timeAgo = sanity.useTimeAgo(time);
1008
- return /* @__PURE__ */ jsxRuntime.jsxs("span", { title: timeAgo, children: [
1009
- timeAgo,
1010
- " ago"
1011
- ] });
690
+ function muxAssetToSanityDocument(asset) {
691
+ const playbackId = (asset.playback_ids || []).find((p) => p.id)?.id;
692
+ if (playbackId)
693
+ return {
694
+ _id: uuid.uuid(),
695
+ _type: "mux.videoAsset",
696
+ _updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
697
+ _createdAt: parseMuxDate(asset.created_at).toISOString(),
698
+ assetId: asset.id,
699
+ playbackId,
700
+ filename: asset.meta?.title ?? generateAssetPlaceholder(asset.id),
701
+ status: asset.status,
702
+ data: asset
703
+ };
1012
704
  }
1013
- function DraftStatus(props) {
1014
- const { document: document2 } = props, updatedAt = document2 && "_updatedAt" in document2 && document2._updatedAt;
1015
- return /* @__PURE__ */ jsxRuntime.jsx(
1016
- ui.Tooltip,
705
+ const useAssetsInSanity = sanity.createHookFromObservableFactory(
706
+ (documentStore) => documentStore.listenQuery(
707
+ /* groq */
708
+ `*[_type == "mux.videoAsset"] {
709
+ "uploadId": coalesce(uploadId, data.upload_id),
710
+ "assetId": coalesce(assetId, data.id),
711
+ }`,
712
+ {},
1017
713
  {
1018
- animate: !0,
1019
- portal: !0,
1020
- content: /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { padding: 2, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, children: document2 ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1021
- "Edited ",
1022
- updatedAt && /* @__PURE__ */ jsxRuntime.jsx(TimeAgo, { time: updatedAt })
1023
- ] }) : /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: "No unpublished edits" }) }) }),
1024
- children: /* @__PURE__ */ jsxRuntime.jsx(sanity.TextWithTone, { tone: "caution", dimmed: !document2, muted: !document2, size: 1, children: /* @__PURE__ */ jsxRuntime.jsx(icons.EditIcon, {}) })
714
+ apiVersion: SANITY_API_VERSION
1025
715
  }
716
+ )
717
+ );
718
+ function assetExistsInSanity(asset, existingAssets) {
719
+ return asset.status !== "ready" ? !1 : existingAssets.some(
720
+ (existing) => existing.assetId === asset.id || existing.uploadId === asset.upload_id
1026
721
  );
1027
722
  }
1028
- function PublishedStatus(props) {
1029
- const { document: document2 } = props, updatedAt = document2 && "_updatedAt" in document2 && document2._updatedAt;
1030
- return /* @__PURE__ */ jsxRuntime.jsx(
1031
- ui.Tooltip,
723
+ function useInView(ref, options = {}) {
724
+ const [inView, setInView] = React.useState(!1);
725
+ return React.useEffect(() => {
726
+ if (!ref.current) return;
727
+ const observer = new IntersectionObserver(([entry], obs) => {
728
+ const nowInView = entry.isIntersecting && obs.thresholds.some((threshold) => entry.intersectionRatio >= threshold);
729
+ setInView(nowInView), options?.onChange?.(nowInView);
730
+ }, options), toObserve = ref.current;
731
+ return observer.observe(toObserve), () => {
732
+ toObserve && observer.unobserve(toObserve);
733
+ };
734
+ }, [options, ref]), inView;
735
+ }
736
+ function generateJwt(client, playbackId, aud, payload) {
737
+ const { signingKeyId, signingKeyPrivate } = readSecrets(client);
738
+ if (!signingKeyId)
739
+ throw new TypeError("Missing `signingKeyId`.\n Check your plugin's configuration");
740
+ if (!signingKeyPrivate)
741
+ throw new TypeError("Missing `signingKeyPrivate`.\n Check your plugin's configuration");
742
+ const { default: sign } = suspendReact.suspend(() => import("jsonwebtoken-esm/sign"), ["jsonwebtoken-esm/sign"]);
743
+ return sign(
744
+ payload ? JSON.parse(JSON.stringify(payload, (_, v) => v ?? void 0)) : {},
745
+ atob(signingKeyPrivate),
1032
746
  {
1033
- animate: !0,
1034
- portal: !0,
1035
- content: /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { padding: 2, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, children: document2 ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1036
- "Published ",
1037
- updatedAt && /* @__PURE__ */ jsxRuntime.jsx(TimeAgo, { time: updatedAt })
1038
- ] }) : /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: "Not published" }) }) }),
1039
- children: /* @__PURE__ */ jsxRuntime.jsx(sanity.TextWithTone, { tone: "positive", dimmed: !document2, muted: !document2, size: 1, children: /* @__PURE__ */ jsxRuntime.jsx(icons.PublishIcon, {}) })
747
+ algorithm: "RS256",
748
+ keyid: signingKeyId,
749
+ audience: aud,
750
+ subject: playbackId,
751
+ noTimestamp: !0,
752
+ expiresIn: "12h"
1040
753
  }
1041
754
  );
1042
755
  }
1043
- function PaneItemPreview(props) {
1044
- const { icon, layout, presence, schemaType, value } = props, title = sanity.isRecord(value.title) && React.isValidElement(value.title) || isString__default.default(value.title) || isNumber__default.default(value.title) ? value.title : null, observable = React.useMemo(
1045
- () => sanity.getPreviewStateObservable(props.documentPreviewStore, schemaType, value._id, title),
1046
- [props.documentPreviewStore, schemaType, title, value._id]
1047
- ), { draft, published, isLoading } = reactRx.useObservable(observable, {
1048
- draft: null,
1049
- published: null,
1050
- isLoading: !0
1051
- }), status = isLoading ? null : /* @__PURE__ */ jsxRuntime.jsxs(ui.Inline, { space: 4, children: [
1052
- presence && presence.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(sanity.DocumentPreviewPresence, { presence }),
1053
- /* @__PURE__ */ jsxRuntime.jsx(PublishedStatus, { document: published }),
1054
- /* @__PURE__ */ jsxRuntime.jsx(DraftStatus, { document: draft })
1055
- ] });
1056
- return /* @__PURE__ */ jsxRuntime.jsx(
1057
- sanity.SanityDefaultPreview,
1058
- {
1059
- ...sanity.getPreviewValueWithFallback({ value, draft, published }),
1060
- isPlaceholder: isLoading,
1061
- icon,
1062
- layout,
1063
- status
1064
- }
756
+ function getPlaybackId(asset) {
757
+ if (!asset?.playbackId)
758
+ throw console.error("Asset is missing a playbackId", { asset }), new TypeError("Missing playbackId");
759
+ return asset.playbackId;
760
+ }
761
+ function getPlaybackPolicy(asset) {
762
+ return asset.data?.playback_ids?.find((playbackId) => asset.playbackId === playbackId.id)?.policy ?? "public";
763
+ }
764
+ function createUrlParamsObject(client, asset, params, audience) {
765
+ const playbackId = getPlaybackId(asset);
766
+ let searchParams = new URLSearchParams(
767
+ JSON.parse(JSON.stringify(params, (_, v) => v ?? void 0))
1065
768
  );
769
+ if (getPlaybackPolicy(asset) === "signed") {
770
+ const token = generateJwt(client, playbackId, audience, params);
771
+ searchParams = new URLSearchParams({ token });
772
+ }
773
+ return { playbackId, searchParams };
1066
774
  }
1067
- function getIconWithFallback(icon, schemaType, defaultIcon) {
1068
- return icon === !1 ? !1 : icon || schemaType && schemaType.icon || defaultIcon || !1;
775
+ function getAnimatedPosterSrc({
776
+ asset,
777
+ client,
778
+ height,
779
+ width,
780
+ start = asset.thumbTime ? Math.max(0, asset.thumbTime - 2.5) : 0,
781
+ end = start + 5,
782
+ fps = 15
783
+ }) {
784
+ const params = { height, width, start, end, fps }, { playbackId, searchParams } = createUrlParamsObject(client, asset, params, "g");
785
+ return `https://image.mux.com/${playbackId}/animated.gif?${searchParams}`;
1069
786
  }
1070
- function DocumentPreviewLink(props) {
1071
- return (linkProps) => /* @__PURE__ */ jsxRuntime.jsx(router.IntentLink, { intent: "edit", params: { id: props.documentPair.id }, children: linkProps.children });
787
+ function getPosterSrc({
788
+ asset,
789
+ client,
790
+ fit_mode,
791
+ height,
792
+ time = asset.thumbTime ?? void 0,
793
+ width
794
+ }) {
795
+ const params = { fit_mode, height, width };
796
+ time !== void 0 && (params.time = time);
797
+ const { playbackId, searchParams } = createUrlParamsObject(client, asset, params, "t");
798
+ return `https://image.mux.com/${playbackId}/thumbnail.png?${searchParams}`;
1072
799
  }
1073
- function DocumentPreview(props) {
1074
- const { schemaType, documentPair } = props, doc = documentPair?.draft || documentPair?.published, id = documentPair.id || "", documentPreviewStore = sanity.useDocumentPreviewStore(), schema = sanity.useSchema(), documentPresence = sanity.useDocumentPresence(id), hasSchemaType = !!(schemaType && schemaType.name && schema.get(schemaType.name)), PreviewComponent = React.useMemo(() => doc ? !schemaType || !hasSchemaType ? /* @__PURE__ */ jsxRuntime.jsx(MissingSchemaType, { value: doc }) : /* @__PURE__ */ jsxRuntime.jsx(
1075
- PaneItemPreview,
1076
- {
1077
- documentPreviewStore,
1078
- icon: getIconWithFallback(void 0, schemaType, icons.DocumentIcon),
1079
- schemaType,
1080
- layout: "default",
1081
- value: doc,
1082
- presence: documentPresence
800
+ const Image = styledComponents.styled.img`
801
+ transition: opacity 0.175s ease-out 0s;
802
+ display: block;
803
+ width: 100%;
804
+ height: 100%;
805
+ object-fit: contain;
806
+ object-position: center center;
807
+ `, STATUS_TO_TONE = {
808
+ loading: "transparent",
809
+ error: "critical",
810
+ loaded: "default"
811
+ };
812
+ function VideoThumbnail({
813
+ asset,
814
+ width,
815
+ staticImage = !1
816
+ }) {
817
+ const ref = React.useRef(null), inView = useInView(ref), posterWidth = width || 250, [status, setStatus] = React.useState("loading"), client = useClient(), src = React.useMemo(() => {
818
+ try {
819
+ let thumbnail;
820
+ return staticImage ? thumbnail = getPosterSrc({ asset, client, width: posterWidth }) : thumbnail = getAnimatedPosterSrc({ asset, client, width: posterWidth }), thumbnail;
821
+ } catch {
822
+ status !== "error" && setStatus("error");
823
+ return;
1083
824
  }
1084
- ) : null, [hasSchemaType, schemaType, documentPresence, doc, documentPreviewStore]);
825
+ }, [asset, client, posterWidth, status, staticImage]);
826
+ function handleLoad() {
827
+ setStatus("loaded");
828
+ }
829
+ function handleError() {
830
+ setStatus("error");
831
+ }
1085
832
  return /* @__PURE__ */ jsxRuntime.jsx(
1086
- sanity.PreviewCard,
833
+ ui.Card,
1087
834
  {
1088
- __unstable_focusRing: !0,
1089
- as: DocumentPreviewLink(props),
1090
- "data-as": "a",
1091
- "data-ui": "PaneItem",
1092
- padding: 2,
835
+ style: {
836
+ aspectRatio: THUMBNAIL_ASPECT_RATIO,
837
+ position: "relative",
838
+ maxWidth: width ? `${width}px` : void 0,
839
+ width: "100%",
840
+ flex: 1
841
+ },
842
+ border: !0,
1093
843
  radius: 2,
1094
- tone: "inherit",
1095
- children: PreviewComponent
844
+ ref,
845
+ tone: STATUS_TO_TONE[status],
846
+ children: inView ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
847
+ status === "loading" && /* @__PURE__ */ jsxRuntime.jsx(
848
+ ui.Box,
849
+ {
850
+ style: {
851
+ position: "absolute",
852
+ left: "50%",
853
+ top: "50%",
854
+ transform: "translate(-50%, -50%)"
855
+ },
856
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, {})
857
+ }
858
+ ),
859
+ status === "error" && /* @__PURE__ */ jsxRuntime.jsxs(
860
+ ui.Stack,
861
+ {
862
+ space: 4,
863
+ style: {
864
+ position: "absolute",
865
+ width: "100%",
866
+ left: 0,
867
+ top: "50%",
868
+ transform: "translateY(-50%)",
869
+ justifyItems: "center"
870
+ },
871
+ children: [
872
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 4, muted: !0, children: /* @__PURE__ */ jsxRuntime.jsx(icons.ErrorOutlineIcon, { style: { fontSize: "1.75em" } }) }),
873
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { muted: !0, align: "center", children: "Failed loading thumbnail" })
874
+ ]
875
+ }
876
+ ),
877
+ /* @__PURE__ */ jsxRuntime.jsx(
878
+ Image,
879
+ {
880
+ src,
881
+ alt: `Preview for ${staticImage ? "image" : "video"} ${asset.filename || asset.assetId}`,
882
+ onLoad: handleLoad,
883
+ onError: handleError,
884
+ style: { opacity: status === "loaded" ? 1 : 0 }
885
+ }
886
+ )
887
+ ] }) : null
1096
888
  }
1097
889
  );
1098
890
  }
1099
- const Container = styledComponents.styled(ui.Box)`
1100
- * {
1101
- color: ${(props) => props.theme.sanity.color.base.fg};
1102
- }
1103
- a {
1104
- text-decoration: none;
1105
- }
1106
- h2 {
1107
- font-size: ${(props) => props.theme.sanity.fonts.text.sizes[1]};
891
+ const MissingAssetCheckbox = styledComponents.styled(ui.Checkbox)`
892
+ position: static !important;
893
+
894
+ input::after {
895
+ content: '';
896
+ position: absolute;
897
+ inset: 0;
898
+ display: block;
899
+ cursor: pointer;
900
+ z-index: 1000;
1108
901
  }
1109
- `, VideoReferences = (props) => {
1110
- const schema = sanity.useSchema();
1111
- if (!props.isLoaded)
1112
- return /* @__PURE__ */ jsxRuntime.jsx(SpinnerBox, {});
1113
- if (!props.references?.length)
1114
- return /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { border: !0, radius: 3, padding: 3, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, children: "No documents are using this video" }) });
1115
- const documentPairs = sanity.collate(props.references || []);
1116
- return /* @__PURE__ */ jsxRuntime.jsx(Container, { children: documentPairs?.map((documentPair) => {
1117
- const schemaType = schema.get(documentPair.type);
1118
- return /* @__PURE__ */ jsxRuntime.jsx(
1119
- ui.Card,
1120
- {
1121
- marginBottom: 2,
1122
- padding: 2,
1123
- radius: 2,
1124
- shadow: 1,
1125
- style: { overflow: "hidden" },
1126
- children: /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(DocumentPreview, { documentPair, schemaType }) })
1127
- },
1128
- documentPair.id
1129
- );
1130
- }) });
1131
- };
1132
- function DeleteDialog({
902
+ `;
903
+ function MissingAsset({
1133
904
  asset,
1134
- references,
1135
- referencesLoading,
1136
- cancelDelete,
1137
- succeededDeleting
905
+ selectAsset,
906
+ selected
1138
907
  }) {
1139
- const client = useClient(), [state, setState] = React.useState("checkingReferences"), [deleteOnMux, setDeleteOnMux] = React.useState(!0), toast = ui.useToast();
1140
- React.useEffect(() => {
1141
- state !== "checkingReferences" || referencesLoading || setState(references?.length ? "cantDelete" : "confirm");
1142
- }, [state, references, referencesLoading]);
1143
- async function confirmDelete() {
1144
- if (state !== "confirm") return;
1145
- setState("processing_deletion");
1146
- const worked = await deleteAsset({ client, asset, deleteOnMux });
1147
- worked === !0 ? (toast.push({ title: "Successfully deleted video", status: "success" }), succeededDeleting()) : worked === "failed-mux" ? (toast.push({
1148
- title: "Deleted video in Sanity",
1149
- description: "But it wasn't deleted in Mux",
1150
- status: "warning"
1151
- }), succeededDeleting()) : (toast.push({ title: "Failed deleting video", status: "error" }), setState("error_deleting"));
1152
- }
908
+ const duration = sanity.useFormattedDuration(asset.duration * 1e3);
1153
909
  return /* @__PURE__ */ jsxRuntime.jsx(
1154
- ui.Dialog,
910
+ ui.Card,
1155
911
  {
1156
- animate: !0,
1157
- header: "Delete video",
1158
- zOffset: DIALOGS_Z_INDEX,
1159
- id: "deleting-video-details-dialog",
1160
- onClose: cancelDelete,
1161
- onClickOutside: cancelDelete,
1162
- width: 1,
1163
- position: "fixed",
1164
- children: /* @__PURE__ */ jsxRuntime.jsx(
1165
- ui.Card,
1166
- {
1167
- padding: 3,
1168
- style: {
1169
- minHeight: "150px",
1170
- display: "flex",
1171
- alignItems: "center",
1172
- justifyContent: "center"
1173
- },
1174
- children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
1175
- state === "checkingReferences" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1176
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { size: 2, children: "Checking if video can be deleted" }),
1177
- /* @__PURE__ */ jsxRuntime.jsx(SpinnerBox, {})
1178
- ] }),
1179
- state === "cantDelete" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1180
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { size: 2, children: "Video can't be deleted" }),
1181
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 2, style: { marginBottom: "2rem" }, children: [
1182
- "There are ",
1183
- references?.length,
1184
- " document",
1185
- references && references.length > 0 && "s",
1186
- " ",
1187
- "pointing to this video. Remove their references to this file or delete them before proceeding."
1188
- ] }),
1189
- /* @__PURE__ */ jsxRuntime.jsx(VideoReferences, { references, isLoaded: !referencesLoading })
1190
- ] }),
1191
- state === "confirm" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1192
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { size: 2, children: "Are you sure you want to delete this video?" }),
1193
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, children: "This action is irreversible" }),
1194
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, marginY: 4, children: [
1195
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", as: "label", children: [
1196
- /* @__PURE__ */ jsxRuntime.jsx(
1197
- ui.Checkbox,
1198
- {
1199
- checked: deleteOnMux,
1200
- onChange: () => setDeleteOnMux((prev) => !prev)
1201
- }
1202
- ),
1203
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { style: { margin: "0 10px" }, children: "Delete asset on Mux" })
1204
- ] }),
1205
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", as: "label", children: [
1206
- /* @__PURE__ */ jsxRuntime.jsx(ui.Checkbox, { disabled: !0, checked: !0 }),
1207
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { style: { margin: "0 10px" }, children: "Delete video from dataset" })
1208
- ] }),
1209
- /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(
1210
- ui.Button,
1211
- {
1212
- icon: icons.TrashIcon,
1213
- fontSize: 2,
1214
- padding: 3,
1215
- text: "Delete video",
1216
- tone: "critical",
1217
- onClick: confirmDelete,
1218
- disabled: ["processing_deletion", "checkingReferences", "cantDelete"].some(
1219
- (s) => s === state
1220
- )
1221
- }
1222
- ) })
1223
- ] })
1224
- ] }),
1225
- state === "processing_deletion" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1226
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { size: 2, children: "Deleting video..." }),
1227
- /* @__PURE__ */ jsxRuntime.jsx(SpinnerBox, {})
1228
- ] }),
1229
- state === "error_deleting" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1230
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { size: 2, children: "Something went wrong!" }),
1231
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, children: "Try deleting the video again by clicking the button below" })
912
+ tone: selected ? "positive" : void 0,
913
+ border: !0,
914
+ paddingX: 2,
915
+ paddingY: 3,
916
+ style: { position: "relative" },
917
+ radius: 1,
918
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
919
+ /* @__PURE__ */ jsxRuntime.jsx(
920
+ MissingAssetCheckbox,
921
+ {
922
+ checked: selected,
923
+ onChange: (e) => {
924
+ selectAsset(e.currentTarget.checked);
925
+ },
926
+ "aria-label": selected ? `Import video ${asset.id}` : `Skip import of video ${asset.id}`
927
+ }
928
+ ),
929
+ /* @__PURE__ */ jsxRuntime.jsx(
930
+ VideoThumbnail,
931
+ {
932
+ asset: {
933
+ assetId: asset.id,
934
+ data: asset,
935
+ filename: asset.id,
936
+ playbackId: asset.playback_ids.find((p) => p.id)?.id
937
+ },
938
+ width: 150
939
+ }
940
+ ),
941
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
942
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 1, children: [
943
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Code, { size: 2, children: sanity.truncateString(asset.id, 15) }),
944
+ " ",
945
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { muted: !0, size: 2, children: [
946
+ "(",
947
+ duration.formatted,
948
+ ")"
1232
949
  ] })
950
+ ] }),
951
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, children: [
952
+ "Uploaded at",
953
+ " ",
954
+ new Date(Number(asset.created_at) * 1e3).toLocaleDateString("en", {
955
+ year: "numeric",
956
+ day: "2-digit",
957
+ month: "2-digit"
958
+ })
1233
959
  ] })
1234
- }
1235
- )
1236
- }
960
+ ] })
961
+ ] })
962
+ },
963
+ asset.id
1237
964
  );
1238
965
  }
1239
- const useDocReferences = sanity.createHookFromObservableFactory(({ documentStore, id }) => documentStore.listenQuery(
1240
- /* groq */
1241
- "*[references($id)]{_id, _type, _rev, _updatedAt, _createdAt}",
1242
- { id },
1243
- {
1244
- apiVersion: SANITY_API_VERSION
1245
- }
1246
- ));
1247
- function getVideoMetadata(doc) {
1248
- 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());
1249
- return {
1250
- title: doc.filename || id.slice(0, 12),
1251
- id,
1252
- playbackId: doc.playbackId,
1253
- createdAt: date,
1254
- duration: doc.data?.duration ? formatSeconds(doc.data?.duration) : void 0,
1255
- aspect_ratio: doc.data?.aspect_ratio,
1256
- max_stored_resolution: doc.data?.max_stored_resolution,
1257
- max_stored_frame_rate: doc.data?.max_stored_frame_rate
1258
- };
1259
- }
1260
- function useVideoDetails(props) {
1261
- const documentStore = sanity.useDocumentStore(), toast = ui.useToast(), client = useClient(), [references, referencesLoading] = useDocReferences(
1262
- React.useMemo(() => ({ documentStore, id: props.asset._id }), [documentStore, props.asset._id])
1263
- ), [originalAsset, setOriginalAsset] = React.useState(() => props.asset), [filename, setFilename] = React.useState(props.asset.filename), modified = filename !== originalAsset.filename, displayInfo = getVideoMetadata({ ...props.asset, filename }), [state, setState] = React.useState("idle");
1264
- function handleClose() {
1265
- if (state === "idle") {
1266
- if (modified) {
1267
- setState("closing");
1268
- return;
1269
- }
1270
- props.closeDialog();
1271
- }
1272
- }
1273
- function confirmClose(shouldClose) {
1274
- state === "closing" && (shouldClose && props.closeDialog(), setState("idle"));
1275
- }
1276
- async function saveChanges() {
1277
- if (state === "idle") {
1278
- setState("saving");
1279
- try {
1280
- await client.patch(props.asset._id).set({ filename }).commit(), setOriginalAsset((prev) => ({ ...prev, filename })), toast.push({
1281
- title: "Video title updated",
1282
- description: `New title: ${filename}`,
1283
- status: "success"
1284
- }), props.closeDialog();
1285
- } catch (error) {
1286
- toast.push({
1287
- title: "Failed updating file name",
1288
- status: "error",
1289
- description: typeof error == "string" ? error : "Please try again"
1290
- }), setFilename(originalAsset.filename);
1291
- }
1292
- setState("idle");
1293
- }
1294
- }
1295
- return {
1296
- references,
1297
- referencesLoading,
1298
- modified,
1299
- filename,
1300
- setFilename,
1301
- displayInfo,
1302
- state,
1303
- setState,
1304
- handleClose,
1305
- confirmClose,
1306
- saveChanges
1307
- };
1308
- }
1309
- const AssetInput = (props) => /* @__PURE__ */ jsxRuntime.jsx(FormField$1, { title: props.label, description: props.description, inputId: props.label, children: /* @__PURE__ */ jsxRuntime.jsx(
1310
- ui.TextInput,
1311
- {
1312
- id: props.label,
1313
- value: props.value,
1314
- placeholder: props.placeholder,
1315
- onInput: props.onInput,
1316
- disabled: props.disabled
1317
- }
1318
- ) }), VideoDetails = (props) => {
1319
- const [tab, setTab] = React.useState("details"), {
1320
- displayInfo,
1321
- filename,
1322
- modified,
1323
- references,
1324
- referencesLoading,
1325
- setFilename,
1326
- state,
1327
- setState,
1328
- handleClose,
1329
- confirmClose,
1330
- saveChanges
1331
- } = useVideoDetails(props), isSaving = state === "saving", [containerHeight, setContainerHeight] = React.useState(null), contentsRef = React__default.default.useRef(null);
1332
- return React.useEffect(() => {
1333
- !contentsRef.current || !("getBoundingClientRect" in contentsRef.current) || setContainerHeight(contentsRef.current.getBoundingClientRect().height);
1334
- }, []), /* @__PURE__ */ jsxRuntime.jsxs(
966
+ function ImportVideosDialog(props) {
967
+ const { importState } = props, canTriggerImport = (importState === "idle" || importState === "error") && props.selectedAssets.length > 0, isImporting = importState === "importing", noAssetsToImport = props.missingAssets?.length === 0 && !props.muxAssets.loading && !props.assetsInSanityLoading;
968
+ return /* @__PURE__ */ jsxRuntime.jsx(
1335
969
  ui.Dialog,
1336
970
  {
1337
971
  animate: !0,
1338
- header: displayInfo.title,
972
+ header: "Import videos from Mux",
1339
973
  zOffset: DIALOGS_Z_INDEX,
1340
974
  id: "video-details-dialog",
1341
- onClose: handleClose,
1342
- onClickOutside: handleClose,
1343
- width: 2,
975
+ onClose: props.closeDialog,
976
+ onClickOutside: props.closeDialog,
977
+ width: 1,
1344
978
  position: "fixed",
1345
- footer: /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 3, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "space-between", align: "center", children: [
979
+ footer: importState !== "done" && !noAssetsToImport && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 3, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "space-between", align: "center", children: [
1346
980
  /* @__PURE__ */ jsxRuntime.jsx(
1347
981
  ui.Button,
1348
982
  {
1349
- icon: icons.TrashIcon,
1350
983
  fontSize: 2,
1351
984
  padding: 3,
1352
- mode: "bleed",
1353
- text: "Delete",
985
+ mode: "ghost",
986
+ text: "Cancel",
1354
987
  tone: "critical",
1355
- onClick: () => setState("deleting"),
1356
- disabled: isSaving
988
+ onClick: props.closeDialog,
989
+ disabled: isImporting
1357
990
  }
1358
991
  ),
1359
- modified && /* @__PURE__ */ jsxRuntime.jsx(
992
+ props.missingAssets && /* @__PURE__ */ jsxRuntime.jsx(
1360
993
  ui.Button,
1361
994
  {
1362
- icon: icons.CheckmarkIcon,
995
+ icon: icons.RetrieveIcon,
1363
996
  fontSize: 2,
1364
997
  padding: 3,
1365
998
  mode: "ghost",
1366
- text: "Save and close",
999
+ text: props.selectedAssets?.length > 0 ? `Import ${props.selectedAssets.length} video(s)` : "No video(s) selected",
1367
1000
  tone: "positive",
1368
- onClick: saveChanges,
1369
- iconRight: isSaving && ui.Spinner,
1370
- disabled: isSaving
1001
+ onClick: props.importAssets,
1002
+ iconRight: isImporting && ui.Spinner,
1003
+ disabled: !canTriggerImport
1371
1004
  }
1372
1005
  )
1373
1006
  ] }) }),
1374
- children: [
1375
- state === "deleting" && /* @__PURE__ */ jsxRuntime.jsx(
1376
- DeleteDialog,
1377
- {
1378
- asset: props.asset,
1379
- cancelDelete: () => setState("idle"),
1380
- referencesLoading,
1381
- references,
1382
- succeededDeleting: () => {
1383
- props.closeDialog();
1384
- }
1385
- }
1386
- ),
1387
- state === "closing" && /* @__PURE__ */ jsxRuntime.jsx(
1388
- ui.Dialog,
1389
- {
1390
- animate: !0,
1391
- header: "You have unsaved changes",
1392
- zOffset: DIALOGS_Z_INDEX,
1393
- id: "closing-video-details-dialog",
1394
- onClose: () => confirmClose(!1),
1395
- onClickOutside: () => confirmClose(!1),
1396
- width: 1,
1397
- position: "fixed",
1398
- footer: /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 3, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "space-between", align: "center", children: [
1399
- /* @__PURE__ */ jsxRuntime.jsx(
1400
- ui.Button,
1401
- {
1402
- icon: icons.ErrorOutlineIcon,
1403
- fontSize: 2,
1404
- padding: 3,
1405
- text: "Discard changes",
1406
- tone: "critical",
1407
- onClick: () => confirmClose(!0)
1408
- }
1409
- ),
1410
- modified && /* @__PURE__ */ jsxRuntime.jsx(
1411
- ui.Button,
1412
- {
1413
- icon: icons.RevertIcon,
1414
- fontSize: 2,
1415
- padding: 3,
1416
- mode: "ghost",
1417
- text: "Keep editing",
1418
- tone: "primary",
1419
- onClick: () => confirmClose(!1)
1420
- }
1421
- )
1422
- ] }) }),
1423
- children: /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 5, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { style: { textAlign: "center" }, space: 3, children: [
1424
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { size: 2, children: "Unsaved changes will be lost" }),
1425
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, children: "Are you sure you want to discard them?" })
1426
- ] }) })
1427
- }
1428
- ),
1007
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Box, { padding: 3, children: [
1008
+ props.muxAssets.hasSkippedAssetsWithoutPlayback && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { tone: "caution", marginBottom: 5, padding: 3, border: !0, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
1009
+ /* @__PURE__ */ jsxRuntime.jsx(icons.InfoOutlineIcon, { fontSize: 36 }),
1010
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1011
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, weight: "semibold", children: "Some videos were skipped" }),
1012
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, children: "Videos without playback IDs cannot be imported and have been excluded from the list." })
1013
+ ] })
1014
+ ] }) }),
1015
+ (props.muxAssets.loading || props.assetsInSanityLoading) && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { tone: "primary", marginBottom: 5, padding: 3, border: !0, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 4, children: [
1016
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, { muted: !0, size: 4 }),
1017
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1018
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, weight: "semibold", children: "Loading assets from Mux" }),
1019
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, children: [
1020
+ "This may take a while.",
1021
+ props.missingAssets && props.missingAssets.length > 0 && ` There are at least ${props.missingAssets.length} video${props.missingAssets.length > 1 ? "s" : ""} currently not in Sanity...`
1022
+ ] })
1023
+ ] })
1024
+ ] }) }),
1025
+ props.muxAssets.error && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { tone: "critical", marginBottom: 5, padding: 3, border: !0, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
1026
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ErrorOutlineIcon, { fontSize: 36 }),
1027
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1028
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, weight: "semibold", children: "There was an error getting all data from Mux" }),
1029
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, children: props.missingAssets ? `But we've found ${props.missingAssets.length} video${props.missingAssets.length > 1 ? "s" : ""} not in Sanity, which you can start importing now.` : "Please try again or contact a developer for help." })
1030
+ ] })
1031
+ ] }) }),
1032
+ importState === "importing" && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { tone: "primary", marginBottom: 5, padding: 3, border: !0, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 4, children: [
1033
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, { muted: !0, size: 4 }),
1034
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 2, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 2, weight: "semibold", children: [
1035
+ "Importing ",
1036
+ props.selectedAssets.length,
1037
+ " video",
1038
+ props.selectedAssets.length > 1 && "s",
1039
+ " from Mux"
1040
+ ] }) })
1041
+ ] }) }),
1042
+ importState === "error" && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { tone: "critical", marginBottom: 5, padding: 3, border: !0, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
1043
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ErrorOutlineIcon, { fontSize: 36 }),
1044
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1045
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, weight: "semibold", children: "There was an error importing videos" }),
1046
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, children: props.importError ? `Error: ${props.importError}` : "Please try again or contact a developer for help." }),
1047
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { marginTop: 1, children: /* @__PURE__ */ jsxRuntime.jsx(
1048
+ ui.Button,
1049
+ {
1050
+ icon: icons.RetryIcon,
1051
+ text: "Retry",
1052
+ tone: "primary",
1053
+ onClick: props.importAssets
1054
+ }
1055
+ ) })
1056
+ ] })
1057
+ ] }) }),
1058
+ (noAssetsToImport || importState === "done") && /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { paddingY: 5, marginBottom: 4, space: 3, style: { textAlign: "center" }, children: [
1059
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(icons.CheckmarkCircleIcon, { fontSize: 48 }) }),
1060
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { size: 2, children: importState === "done" ? "Videos imported successfully" : "There are no Mux videos to import" }),
1061
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, children: importState === "done" ? "You can now use them in your Sanity content." : "They're all in Sanity and ready to be used in your content." })
1062
+ ] }),
1063
+ props.missingAssets && props.missingAssets.length > 0 && (importState === "idle" || importState === "error") && /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, children: [
1064
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Heading, { size: 1, children: [
1065
+ "There are ",
1066
+ props.missingAssets.length,
1067
+ props.muxAssets.loading && "+",
1068
+ " Mux video",
1069
+ props.missingAssets.length > 1 && "s",
1070
+ " ",
1071
+ "not in Sanity"
1072
+ ] }),
1073
+ !props.muxAssets.loading && /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", paddingX: 2, children: [
1074
+ /* @__PURE__ */ jsxRuntime.jsx(
1075
+ ui.Checkbox,
1076
+ {
1077
+ id: "import-all",
1078
+ style: { display: "block" },
1079
+ onClick: (e) => {
1080
+ e.currentTarget.checked ? props.missingAssets && props.setSelectedAssets(props.missingAssets) : props.setSelectedAssets([]);
1081
+ },
1082
+ checked: props.selectedAssets.length === props.missingAssets.length
1083
+ }
1084
+ ),
1085
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { flex: 1, paddingLeft: 3, as: "label", htmlFor: "import-all", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "Import all" }) })
1086
+ ] }),
1087
+ props.missingAssets.map((asset) => /* @__PURE__ */ jsxRuntime.jsx(
1088
+ MissingAsset,
1089
+ {
1090
+ asset,
1091
+ selectAsset: (selected) => {
1092
+ selected ? props.setSelectedAssets([...props.selectedAssets, asset]) : props.setSelectedAssets(props.selectedAssets.filter((a2) => a2.id !== asset.id));
1093
+ },
1094
+ selected: props.selectedAssets.some((a2) => a2.id === asset.id)
1095
+ },
1096
+ asset.id
1097
+ ))
1098
+ ] })
1099
+ ] })
1100
+ }
1101
+ );
1102
+ }
1103
+ function ImportVideosFromMux() {
1104
+ const importAssets = useImportMuxAssets();
1105
+ if (importAssets.hasSecrets)
1106
+ return importAssets.dialogOpen ? /* @__PURE__ */ jsxRuntime.jsx(ImportVideosDialog, { ...importAssets }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { mode: "bleed", text: "Import from Mux", onClick: importAssets.openDialog });
1107
+ }
1108
+ function useResyncMuxMetadata() {
1109
+ const documentStore = sanity.useDocumentStore(), client = sanity.useClient({
1110
+ apiVersion: SANITY_API_VERSION
1111
+ }), [sanityAssets, sanityAssetsLoading] = useSanityAssets(documentStore), hasSecrets = !!useSecretsDocumentValues().value.secrets?.secretKey, [resyncError, setResyncError] = React.useState(), [resyncState, setResyncState] = React.useState("closed"), dialogOpen = resyncState !== "closed", muxAssets = useMuxAssets({
1112
+ client,
1113
+ enabled: hasSecrets && dialogOpen
1114
+ }), matchedAssets = React.useMemo(() => sanityAssets && muxAssets.data ? sanityAssets.map((sanityDoc) => {
1115
+ const muxAsset = muxAssets.data?.find(
1116
+ (m) => m.id === sanityDoc.assetId || m.id === sanityDoc.data?.id
1117
+ );
1118
+ return {
1119
+ sanityDoc,
1120
+ muxAsset,
1121
+ muxTitle: muxAsset?.meta?.title,
1122
+ currentTitle: sanityDoc.filename
1123
+ };
1124
+ }) : void 0, [sanityAssets, muxAssets.data]), closeDialog = () => {
1125
+ resyncState !== "syncing" && setResyncState("closed");
1126
+ }, openDialog = () => {
1127
+ resyncState === "closed" && setResyncState("idle");
1128
+ };
1129
+ async function syncAllVideos() {
1130
+ if (matchedAssets) {
1131
+ setResyncState("syncing");
1132
+ try {
1133
+ const tx = client.transaction();
1134
+ matchedAssets.forEach((matched) => {
1135
+ const newTitle = matched.muxTitle || "";
1136
+ tx.patch(matched.sanityDoc._id, { set: { filename: newTitle } });
1137
+ }), await tx.commit({ returnDocuments: !1 }), setResyncState("done");
1138
+ } catch (error) {
1139
+ setResyncState("error"), setResyncError(error);
1140
+ }
1141
+ }
1142
+ }
1143
+ async function syncOnlyEmpty() {
1144
+ if (matchedAssets) {
1145
+ setResyncState("syncing");
1146
+ try {
1147
+ const tx = client.transaction();
1148
+ matchedAssets.forEach((matched) => {
1149
+ matched.muxAsset && matched.muxTitle && isEmptyOrPlaceholderTitle(matched.currentTitle, matched.muxAsset.id) && tx.patch(matched.sanityDoc._id, { set: { filename: matched.muxTitle } });
1150
+ }), await tx.commit({ returnDocuments: !1 }), setResyncState("done");
1151
+ } catch (error) {
1152
+ setResyncState("error"), setResyncError(error);
1153
+ }
1154
+ }
1155
+ }
1156
+ return {
1157
+ sanityAssetsLoading,
1158
+ closeDialog,
1159
+ dialogOpen,
1160
+ resyncState,
1161
+ resyncError,
1162
+ hasSecrets,
1163
+ syncAllVideos,
1164
+ syncOnlyEmpty,
1165
+ matchedAssets,
1166
+ muxAssets,
1167
+ openDialog
1168
+ };
1169
+ }
1170
+ const useSanityAssets = sanity.createHookFromObservableFactory(
1171
+ (documentStore) => documentStore.listenQuery(
1172
+ /* groq */
1173
+ '*[_type == "mux.videoAsset"]',
1174
+ {},
1175
+ {
1176
+ apiVersion: SANITY_API_VERSION
1177
+ }
1178
+ )
1179
+ );
1180
+ function ResyncMetadataDialog(props) {
1181
+ const { resyncState } = props, canTriggerResync = resyncState === "idle" || resyncState === "error", isResyncing = resyncState === "syncing", isDone = resyncState === "done", videosToUpdate = props.matchedAssets?.filter((m) => m.muxAsset).length || 0, videosWithEmptyOrPlaceholder = props.matchedAssets?.filter(
1182
+ (m) => m.muxAsset && m.muxTitle && isEmptyOrPlaceholderTitle(m.currentTitle, m.muxAsset.id)
1183
+ ).length || 0;
1184
+ return /* @__PURE__ */ jsxRuntime.jsx(
1185
+ ui.Dialog,
1186
+ {
1187
+ animate: !0,
1188
+ header: "Resync Metadata from Mux",
1189
+ zOffset: DIALOGS_Z_INDEX,
1190
+ id: "resync-metadata-dialog",
1191
+ onClose: props.closeDialog,
1192
+ onClickOutside: props.closeDialog,
1193
+ width: 1,
1194
+ position: "fixed",
1195
+ footer: !isDone && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 3, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "space-between", align: "center", children: [
1429
1196
  /* @__PURE__ */ jsxRuntime.jsx(
1430
- ui.Card,
1197
+ ui.Button,
1431
1198
  {
1432
- padding: 4,
1433
- sizing: "border",
1434
- style: {
1435
- containerType: "inline-size"
1436
- },
1437
- children: /* @__PURE__ */ jsxRuntime.jsxs(
1438
- ui.Flex,
1439
- {
1440
- sizing: "border",
1441
- gap: 4,
1442
- direction: ["column", "column", "row"],
1443
- align: "flex-start",
1444
- ref: contentsRef,
1445
- style: typeof containerHeight == "number" ? {
1446
- minHeight: containerHeight
1447
- } : void 0,
1448
- children: [
1449
- /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 4, flex: 1, sizing: "border", children: /* @__PURE__ */ jsxRuntime.jsx(VideoPlayer, { asset: props.asset, autoPlay: props.asset.autoPlay || !1 }) }),
1450
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, flex: 1, sizing: "border", children: [
1451
- /* @__PURE__ */ jsxRuntime.jsxs(ui.TabList, { space: 2, children: [
1452
- /* @__PURE__ */ jsxRuntime.jsx(
1453
- ui.Tab,
1454
- {
1455
- "aria-controls": "details-panel",
1456
- icon: icons.EditIcon,
1457
- id: "details-tab",
1458
- label: "Details",
1459
- onClick: () => setTab("details"),
1460
- selected: tab === "details"
1461
- }
1462
- ),
1463
- /* @__PURE__ */ jsxRuntime.jsx(
1464
- ui.Tab,
1465
- {
1466
- "aria-controls": "references-panel",
1467
- icon: icons.SearchIcon,
1468
- id: "references-tab",
1469
- label: `Used by ${references ? `(${references.length})` : ""}`,
1470
- onClick: () => setTab("references"),
1471
- selected: tab === "references"
1472
- }
1473
- )
1474
- ] }),
1475
- /* @__PURE__ */ jsxRuntime.jsx(
1476
- ui.TabPanel,
1477
- {
1478
- "aria-labelledby": "details-tab",
1479
- id: "details-panel",
1480
- hidden: tab !== "details",
1481
- style: { wordBreak: "break-word" },
1482
- children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, children: [
1483
- /* @__PURE__ */ jsxRuntime.jsx(
1484
- AssetInput,
1485
- {
1486
- label: "Video title or file name",
1487
- description: "Not visible to users. Useful for finding videos later.",
1488
- value: filename || "",
1489
- onInput: (e) => setFilename(e.currentTarget.value),
1490
- disabled: state !== "idle"
1491
- }
1492
- ),
1493
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
1494
- displayInfo?.duration && /* @__PURE__ */ jsxRuntime.jsx(
1495
- IconInfo,
1496
- {
1497
- text: `Duration: ${displayInfo.duration}`,
1498
- icon: icons.ClockIcon,
1499
- size: 2
1500
- }
1501
- ),
1502
- displayInfo?.max_stored_resolution && /* @__PURE__ */ jsxRuntime.jsx(
1503
- IconInfo,
1504
- {
1505
- text: `Max Resolution: ${displayInfo.max_stored_resolution}`,
1506
- icon: ResolutionIcon,
1507
- size: 2
1508
- }
1509
- ),
1510
- displayInfo?.max_stored_frame_rate && /* @__PURE__ */ jsxRuntime.jsx(
1511
- IconInfo,
1512
- {
1513
- text: `Frame rate: ${displayInfo.max_stored_frame_rate}`,
1514
- icon: StopWatchIcon,
1515
- size: 2
1516
- }
1517
- ),
1518
- displayInfo?.aspect_ratio && /* @__PURE__ */ jsxRuntime.jsx(
1519
- IconInfo,
1520
- {
1521
- text: `Aspect Ratio: ${displayInfo.aspect_ratio}`,
1522
- icon: icons.CropIcon,
1523
- size: 2
1524
- }
1525
- ),
1526
- /* @__PURE__ */ jsxRuntime.jsx(
1527
- IconInfo,
1528
- {
1529
- text: `Uploaded on: ${displayInfo.createdAt.toLocaleDateString("en", {
1530
- year: "numeric",
1531
- month: "2-digit",
1532
- day: "2-digit",
1533
- hour: "2-digit",
1534
- minute: "2-digit",
1535
- hour12: !0
1536
- })}`,
1537
- icon: icons.CalendarIcon,
1538
- size: 2
1539
- }
1540
- ),
1541
- /* @__PURE__ */ jsxRuntime.jsx(IconInfo, { text: `Mux ID:
1542
- ${displayInfo.id}`, icon: icons.TagIcon, size: 2 }),
1543
- displayInfo?.playbackId && /* @__PURE__ */ jsxRuntime.jsx(
1544
- IconInfo,
1545
- {
1546
- text: `Playback ID: ${displayInfo.playbackId}`,
1547
- icon: icons.TagIcon,
1548
- size: 2
1549
- }
1550
- )
1551
- ] })
1552
- ] })
1553
- }
1554
- ),
1555
- /* @__PURE__ */ jsxRuntime.jsx(
1556
- ui.TabPanel,
1557
- {
1558
- "aria-labelledby": "references-tab",
1559
- id: "references-panel",
1560
- hidden: tab !== "references",
1561
- children: /* @__PURE__ */ jsxRuntime.jsx(VideoReferences, { references, isLoaded: !referencesLoading })
1562
- }
1199
+ fontSize: 2,
1200
+ padding: 3,
1201
+ mode: "ghost",
1202
+ text: "Cancel",
1203
+ tone: "critical",
1204
+ onClick: props.closeDialog,
1205
+ disabled: isResyncing
1206
+ }
1207
+ ),
1208
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 2, children: [
1209
+ videosWithEmptyOrPlaceholder > 0 && /* @__PURE__ */ jsxRuntime.jsx(
1210
+ ui.Button,
1211
+ {
1212
+ fontSize: 2,
1213
+ padding: 3,
1214
+ mode: "ghost",
1215
+ text: `Update empty (${videosWithEmptyOrPlaceholder})`,
1216
+ tone: "caution",
1217
+ onClick: props.syncOnlyEmpty,
1218
+ disabled: isResyncing || !canTriggerResync
1219
+ }
1220
+ ),
1221
+ /* @__PURE__ */ jsxRuntime.jsx(
1222
+ ui.Button,
1223
+ {
1224
+ icon: icons.SyncIcon,
1225
+ fontSize: 2,
1226
+ padding: 3,
1227
+ mode: "ghost",
1228
+ text: `Update all (${videosToUpdate})`,
1229
+ tone: "positive",
1230
+ onClick: props.syncAllVideos,
1231
+ iconRight: isResyncing && ui.Spinner,
1232
+ disabled: !canTriggerResync
1233
+ }
1234
+ )
1235
+ ] })
1236
+ ] }) }),
1237
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Box, { padding: 4, children: [
1238
+ (props.muxAssets.loading || props.sanityAssetsLoading) && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { tone: "primary", marginBottom: 5, padding: 3, border: !0, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 4, children: [
1239
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, { muted: !0, size: 4 }),
1240
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1241
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, weight: "semibold", children: "Loading assets from Mux" }),
1242
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, children: "This may take a while." })
1243
+ ] })
1244
+ ] }) }),
1245
+ props.muxAssets.error && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { tone: "critical", marginBottom: 5, padding: 3, border: !0, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
1246
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ErrorOutlineIcon, { fontSize: 36 }),
1247
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1248
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, weight: "semibold", children: "There was an error getting data from Mux" }),
1249
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, children: "Please try again or contact a developer for help." })
1250
+ ] })
1251
+ ] }) }),
1252
+ resyncState === "syncing" && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { tone: "primary", marginBottom: 5, padding: 3, border: !0, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 4, children: [
1253
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, { muted: !0, size: 4 }),
1254
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1255
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, weight: "semibold", children: "Updating video metadata" }),
1256
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, children: "Syncing titles from Mux..." })
1257
+ ] })
1258
+ ] }) }),
1259
+ resyncState === "error" && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { tone: "critical", marginBottom: 5, padding: 3, border: !0, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
1260
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ErrorOutlineIcon, { fontSize: 36 }),
1261
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1262
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, weight: "semibold", children: "There was an error syncing metadata" }),
1263
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, children: props.resyncError ? `Error: ${props.resyncError}` : "Please try again or contact a developer for help." })
1264
+ ] })
1265
+ ] }) }),
1266
+ resyncState === "done" && /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { paddingY: 5, marginBottom: 4, space: 3, style: { textAlign: "center" }, children: [
1267
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(icons.CheckmarkCircleIcon, { fontSize: 48 }) }),
1268
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { size: 2, children: "Metadata synced successfully" }),
1269
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, children: "All video titles have been updated from Mux." })
1270
+ ] }),
1271
+ resyncState === "idle" && !props.muxAssets.loading && !props.sanityAssetsLoading && /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, children: [
1272
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Heading, { size: 1, children: [
1273
+ "There ",
1274
+ videosToUpdate === 1 ? "is" : "are",
1275
+ " ",
1276
+ videosToUpdate,
1277
+ " video",
1278
+ videosToUpdate === 1 ? "" : "s",
1279
+ " with Mux metadata"
1280
+ ] }),
1281
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, children: "This will update video titles in Sanity to match those in Mux. No new videos will be created." }),
1282
+ videosWithEmptyOrPlaceholder > 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 3, tone: "caution", border: !0, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "flex-start", gap: 2, children: [
1283
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(icons.ErrorOutlineIcon, {}) }),
1284
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1285
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, weight: "semibold", children: "Videos with empty or placeholder titles" }),
1286
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, muted: !0, children: [
1287
+ videosWithEmptyOrPlaceholder,
1288
+ " video",
1289
+ videosWithEmptyOrPlaceholder === 1 ? "" : "s",
1290
+ ' without titles or with placeholder titles (e.g., "Asset #123") can be updated selectively.'
1291
+ ] })
1292
+ ] })
1293
+ ] }) })
1294
+ ] })
1295
+ ] })
1296
+ }
1297
+ );
1298
+ }
1299
+ function ResyncMetadata() {
1300
+ const resyncMetadata = useResyncMuxMetadata();
1301
+ if (resyncMetadata.hasSecrets)
1302
+ return resyncMetadata.dialogOpen ? /* @__PURE__ */ jsxRuntime.jsx(ResyncMetadataDialog, { ...resyncMetadata }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { mode: "bleed", text: "Resync Metadata", onClick: resyncMetadata.openDialog });
1303
+ }
1304
+ const CONTEXT_MENU_POPOVER_PROPS = {
1305
+ constrainSize: !0,
1306
+ placement: "bottom",
1307
+ portal: !0,
1308
+ width: 0
1309
+ };
1310
+ function SelectSortOptions(props) {
1311
+ const id = React.useId();
1312
+ return /* @__PURE__ */ jsxRuntime.jsx(
1313
+ ui.MenuButton,
1314
+ {
1315
+ button: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { text: "Sort", icon: icons.SortIcon, mode: "bleed", padding: 3, style: { cursor: "pointer" } }),
1316
+ id,
1317
+ menu: /* @__PURE__ */ jsxRuntime.jsx(ui.Menu, { children: Object.entries(ASSET_SORT_OPTIONS).map(([type, { label }]) => /* @__PURE__ */ jsxRuntime.jsx(
1318
+ ui.MenuItem,
1319
+ {
1320
+ "data-as": "button",
1321
+ onClick: () => props.setSort(type),
1322
+ padding: 3,
1323
+ tone: "default",
1324
+ text: label,
1325
+ pressed: type === props.sort
1326
+ },
1327
+ type
1328
+ )) }),
1329
+ popover: CONTEXT_MENU_POPOVER_PROPS
1330
+ }
1331
+ );
1332
+ }
1333
+ const SpinnerBox = () => /* @__PURE__ */ jsxRuntime.jsx(
1334
+ ui.Box,
1335
+ {
1336
+ style: {
1337
+ display: "flex",
1338
+ alignItems: "center",
1339
+ justifyContent: "center",
1340
+ minHeight: "150px"
1341
+ },
1342
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, {})
1343
+ }
1344
+ ), IconInfo = (props) => {
1345
+ const Icon = props.icon;
1346
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 2, align: "center", padding: 1, children: [
1347
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: (props.size || 1) + 1, muted: !0, children: /* @__PURE__ */ jsxRuntime.jsx(Icon, {}) }),
1348
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: props.size || 1, muted: props.muted, children: props.text })
1349
+ ] });
1350
+ };
1351
+ function ResolutionIcon(props) {
1352
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "1em", height: "1em", viewBox: "0 0 24 24", ...props, children: /* @__PURE__ */ jsxRuntime.jsx(
1353
+ "path",
1354
+ {
1355
+ fill: "currentColor",
1356
+ d: "M20 9V6h-3V4h5v5h-2ZM2 9V4h5v2H4v3H2Zm15 11v-2h3v-3h2v5h-5ZM2 20v-5h2v3h3v2H2Zm4-4V8h12v8H6Zm2-2h8v-4H8v4Zm0 0v-4v4Z"
1357
+ }
1358
+ ) });
1359
+ }
1360
+ function StopWatchIcon(props) {
1361
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1362
+ "svg",
1363
+ {
1364
+ xmlns: "http://www.w3.org/2000/svg",
1365
+ width: "1em",
1366
+ height: "1em",
1367
+ viewBox: "0 0 512 512",
1368
+ ...props,
1369
+ children: [
1370
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M232 306.667h48V176h-48v130.667z", fill: "currentColor" }),
1371
+ /* @__PURE__ */ jsxRuntime.jsx(
1372
+ "path",
1373
+ {
1374
+ 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",
1375
+ fill: "currentColor"
1376
+ }
1377
+ ),
1378
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M192 32h128v48H192z", fill: "currentColor" })
1379
+ ]
1380
+ }
1381
+ );
1382
+ }
1383
+ const DialogStateContext = React.createContext({
1384
+ dialogState: !1,
1385
+ setDialogState: () => null
1386
+ }), DialogStateProvider = ({
1387
+ dialogState,
1388
+ setDialogState,
1389
+ children
1390
+ }) => /* @__PURE__ */ jsxRuntime.jsx(DialogStateContext.Provider, { value: { dialogState, setDialogState }, children }), useDialogStateContext = () => React.useContext(DialogStateContext);
1391
+ function getVideoSrc({ asset, client }) {
1392
+ const playbackId = getPlaybackId(asset), searchParams = new URLSearchParams();
1393
+ if (getPlaybackPolicy(asset) === "signed") {
1394
+ const token = generateJwt(client, playbackId, "v");
1395
+ searchParams.set("token", token);
1396
+ }
1397
+ return `https://stream.mux.com/${playbackId}.m3u8?${searchParams}`;
1398
+ }
1399
+ function getDevicePixelRatio(options) {
1400
+ const {
1401
+ defaultDpr = 1,
1402
+ maxDpr = 3,
1403
+ round = !0
1404
+ } = options || {}, dpr = typeof window < "u" && typeof window.devicePixelRatio == "number" ? window.devicePixelRatio : defaultDpr;
1405
+ return Math.min(Math.max(1, round ? Math.floor(dpr) : dpr), maxDpr);
1406
+ }
1407
+ function formatSeconds(seconds) {
1408
+ if (typeof seconds != "number" || Number.isNaN(seconds))
1409
+ return "";
1410
+ const hrs = ~~(seconds / 3600), mins = ~~(seconds % 3600 / 60), secs = ~~seconds % 60;
1411
+ let ret = "";
1412
+ return hrs > 0 && (ret += "" + hrs + ":" + (mins < 10 ? "0" : "")), ret += "" + mins + ":" + (secs < 10 ? "0" : ""), ret += "" + secs, ret;
1413
+ }
1414
+ function formatSecondsToHHMMSS(seconds) {
1415
+ 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");
1416
+ return `${hrs}:${mins}:${secs}`;
1417
+ }
1418
+ function isValidTimeFormat(time) {
1419
+ return /^([0-1]?[0-9]|2[0-3]):([0-5]?[0-9]):([0-5]?[0-9])$/.test(time) || time === "";
1420
+ }
1421
+ function getSecondsFromTimeFormat(time) {
1422
+ const [hh = 0, mm = 0, ss = 0] = time.split(":").map(Number);
1423
+ return hh * 3600 + mm * 60 + ss;
1424
+ }
1425
+ function EditThumbnailDialog({ asset, currentTime = 0 }) {
1426
+ const client = useClient(), { setDialogState } = useDialogStateContext(), dialogId = `EditThumbnailDialog${React.useId()}`, [timeFormatted, setTimeFormatted] = React.useState(
1427
+ () => formatSecondsToHHMMSS(currentTime)
1428
+ ), [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 = () => {
1429
+ setSaving(!0), client.patch(asset._id).set({ thumbTime: nextTime }).commit({ returnDocuments: !1 }).then(() => void setDialogState(!1)).catch(setSaveThumbnailError).finally(() => void setSaving(!1));
1430
+ }, width = 300 * getDevicePixelRatio({ maxDpr: 2 });
1431
+ if (saveThumbnailError)
1432
+ throw saveThumbnailError;
1433
+ return /* @__PURE__ */ jsxRuntime.jsx(
1434
+ ui.Dialog,
1435
+ {
1436
+ id: dialogId,
1437
+ header: "Edit thumbnail",
1438
+ onClose: () => setDialogState(!1),
1439
+ footer: /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { padding: 3, children: /* @__PURE__ */ jsxRuntime.jsx(
1440
+ ui.Button,
1441
+ {
1442
+ disabled: inputError !== "",
1443
+ mode: "ghost",
1444
+ tone: "primary",
1445
+ loading: saving,
1446
+ onClick: handleSave,
1447
+ text: "Set new thumbnail"
1448
+ },
1449
+ "thumbnail"
1450
+ ) }),
1451
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, padding: 3, children: [
1452
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1453
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, weight: "semibold", children: "Current:" }),
1454
+ /* @__PURE__ */ jsxRuntime.jsx(VideoThumbnail, { asset, width, staticImage: !0 })
1455
+ ] }),
1456
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1457
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, weight: "semibold", children: "New:" }),
1458
+ /* @__PURE__ */ jsxRuntime.jsx(VideoThumbnail, { asset: assetWithNewThumbnail, width, staticImage: !0 })
1459
+ ] }),
1460
+ /* @__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" }) }) }),
1461
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1462
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, weight: "semibold", children: "Selected time for thumbnail (hh:mm:ss):" }),
1463
+ /* @__PURE__ */ jsxRuntime.jsx(
1464
+ ui.TextInput,
1465
+ {
1466
+ size: 1,
1467
+ value: timeFormatted,
1468
+ placeholder: "hh:mm:ss",
1469
+ onChange: (event) => {
1470
+ const value = event.currentTarget.value;
1471
+ if (setTimeFormatted(value), isValidTimeFormat(value)) {
1472
+ setInputError("");
1473
+ const totalSeconds = getSecondsFromTimeFormat(value);
1474
+ setNextTime(totalSeconds);
1475
+ } else
1476
+ setInputError("Invalid time format");
1477
+ },
1478
+ customValidity: inputError
1479
+ }
1480
+ )
1481
+ ] })
1482
+ ] })
1483
+ }
1484
+ );
1485
+ }
1486
+ function VideoPlayer({
1487
+ asset,
1488
+ thumbnailWidth = 250,
1489
+ children,
1490
+ ...props
1491
+ }) {
1492
+ const client = useClient(), { dialogState } = useDialogStateContext(), isAudio = assetIsAudio(asset), muxPlayer = React.useRef(null), thumbnail = getPosterSrc({ asset, client, width: thumbnailWidth }), { src: videoSrc, error } = React.useMemo(() => {
1493
+ try {
1494
+ const src = asset?.playbackId && getVideoSrc({ client, asset });
1495
+ return src ? { src } : { error: new TypeError("Asset has no playback ID") };
1496
+ } catch (error2) {
1497
+ return { error: error2 };
1498
+ }
1499
+ }, [asset, client]), signedToken = React.useMemo(() => {
1500
+ try {
1501
+ return new URL(videoSrc).searchParams.get("token");
1502
+ } catch {
1503
+ return !1;
1504
+ }
1505
+ }, [videoSrc]), [width, height] = (asset?.data?.aspect_ratio ?? "16:9").split(":").map(Number), targetAspectRatio = props.forceAspectRatio || (Number.isNaN(width) ? 16 / 9 : width / height);
1506
+ let aspectRatio = Math.max(MIN_ASPECT_RATIO, targetAspectRatio);
1507
+ return isAudio && (aspectRatio = props.forceAspectRatio ? (
1508
+ // Make it wider when forcing aspect ratio to balance with videos' rendering height (audio players overflow a bit)
1509
+ props.forceAspectRatio * 1.2
1510
+ ) : AUDIO_ASPECT_RATIO), /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1511
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Card, { tone: "transparent", style: { aspectRatio, position: "relative" }, children: [
1512
+ videoSrc && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1513
+ /* @__PURE__ */ jsxRuntime.jsx(
1514
+ MuxPlayer__default.default,
1515
+ {
1516
+ poster: thumbnail,
1517
+ ref: muxPlayer,
1518
+ ...props,
1519
+ playsInline: !0,
1520
+ playbackId: asset.playbackId,
1521
+ tokens: signedToken ? { playback: signedToken, thumbnail: signedToken, storyboard: signedToken } : void 0,
1522
+ preload: "metadata",
1523
+ crossOrigin: "anonymous",
1524
+ metadata: {
1525
+ player_name: "Sanity Admin Dashboard",
1526
+ player_version: "2.10.1",
1527
+ page_type: "Preview Player"
1528
+ },
1529
+ audio: isAudio,
1530
+ style: {
1531
+ height: "100%",
1532
+ width: "100%",
1533
+ display: "block",
1534
+ objectFit: "contain"
1535
+ }
1536
+ }
1537
+ ),
1538
+ children
1539
+ ] }),
1540
+ error ? /* @__PURE__ */ jsxRuntime.jsx(
1541
+ "div",
1542
+ {
1543
+ style: {
1544
+ position: "absolute",
1545
+ top: "50%",
1546
+ left: "50%",
1547
+ transform: "translate(-50%, -50%)"
1548
+ },
1549
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { muted: !0, children: [
1550
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ErrorOutlineIcon, { style: { marginRight: "0.15em" } }),
1551
+ typeof error == "object" && "message" in error && typeof error.message == "string" ? error.message : "Error loading video"
1552
+ ] })
1553
+ }
1554
+ ) : null,
1555
+ children
1556
+ ] }),
1557
+ dialogState === "edit-thumbnail" && /* @__PURE__ */ jsxRuntime.jsx(EditThumbnailDialog, { asset, currentTime: muxPlayer?.current?.currentTime })
1558
+ ] });
1559
+ }
1560
+ function assetIsAudio(asset) {
1561
+ return asset.data?.max_stored_resolution === "Audio only";
1562
+ }
1563
+ const getUnknownTypeFallback = (id, typeName) => ({
1564
+ title: /* @__PURE__ */ jsxRuntime.jsxs("em", { children: [
1565
+ "No schema found for type ",
1566
+ /* @__PURE__ */ jsxRuntime.jsx("code", { children: typeName })
1567
+ ] }),
1568
+ subtitle: /* @__PURE__ */ jsxRuntime.jsxs("em", { children: [
1569
+ "Document: ",
1570
+ /* @__PURE__ */ jsxRuntime.jsx("code", { children: id })
1571
+ ] }),
1572
+ media: () => /* @__PURE__ */ jsxRuntime.jsx(icons.WarningOutlineIcon, {})
1573
+ });
1574
+ function MissingSchemaType(props) {
1575
+ const { layout, value } = props;
1576
+ return /* @__PURE__ */ jsxRuntime.jsx(sanity.SanityDefaultPreview, { ...getUnknownTypeFallback(value._id, value._type), layout });
1577
+ }
1578
+ function TimeAgo({ time }) {
1579
+ const timeAgo = sanity.useTimeAgo(time);
1580
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { title: timeAgo, children: [
1581
+ timeAgo,
1582
+ " ago"
1583
+ ] });
1584
+ }
1585
+ function DraftStatus(props) {
1586
+ const { document: document2 } = props, updatedAt = document2 && "_updatedAt" in document2 && document2._updatedAt;
1587
+ return /* @__PURE__ */ jsxRuntime.jsx(
1588
+ ui.Tooltip,
1589
+ {
1590
+ animate: !0,
1591
+ portal: !0,
1592
+ content: /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { padding: 2, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, children: document2 ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1593
+ "Edited ",
1594
+ updatedAt && /* @__PURE__ */ jsxRuntime.jsx(TimeAgo, { time: updatedAt })
1595
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: "No unpublished edits" }) }) }),
1596
+ children: /* @__PURE__ */ jsxRuntime.jsx(sanity.TextWithTone, { tone: "caution", dimmed: !document2, muted: !document2, size: 1, children: /* @__PURE__ */ jsxRuntime.jsx(icons.EditIcon, {}) })
1597
+ }
1598
+ );
1599
+ }
1600
+ function PublishedStatus(props) {
1601
+ const { document: document2 } = props, updatedAt = document2 && "_updatedAt" in document2 && document2._updatedAt;
1602
+ return /* @__PURE__ */ jsxRuntime.jsx(
1603
+ ui.Tooltip,
1604
+ {
1605
+ animate: !0,
1606
+ portal: !0,
1607
+ content: /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { padding: 2, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, children: document2 ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1608
+ "Published ",
1609
+ updatedAt && /* @__PURE__ */ jsxRuntime.jsx(TimeAgo, { time: updatedAt })
1610
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: "Not published" }) }) }),
1611
+ children: /* @__PURE__ */ jsxRuntime.jsx(sanity.TextWithTone, { tone: "positive", dimmed: !document2, muted: !document2, size: 1, children: /* @__PURE__ */ jsxRuntime.jsx(icons.PublishIcon, {}) })
1612
+ }
1613
+ );
1614
+ }
1615
+ function PaneItemPreview(props) {
1616
+ const { icon, layout, presence, schemaType, value } = props, title = sanity.isRecord(value.title) && React.isValidElement(value.title) || isString__default.default(value.title) || isNumber__default.default(value.title) ? value.title : null, observable = React.useMemo(
1617
+ () => sanity.getPreviewStateObservable(props.documentPreviewStore, schemaType, value._id, title),
1618
+ [props.documentPreviewStore, schemaType, title, value._id]
1619
+ ), { draft, published, isLoading } = reactRx.useObservable(observable, {
1620
+ draft: null,
1621
+ published: null,
1622
+ isLoading: !0
1623
+ }), status = isLoading ? null : /* @__PURE__ */ jsxRuntime.jsxs(ui.Inline, { space: 4, children: [
1624
+ presence && presence.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(sanity.DocumentPreviewPresence, { presence }),
1625
+ /* @__PURE__ */ jsxRuntime.jsx(PublishedStatus, { document: published }),
1626
+ /* @__PURE__ */ jsxRuntime.jsx(DraftStatus, { document: draft })
1627
+ ] });
1628
+ return /* @__PURE__ */ jsxRuntime.jsx(
1629
+ sanity.SanityDefaultPreview,
1630
+ {
1631
+ ...sanity.getPreviewValueWithFallback({ value, draft, published }),
1632
+ isPlaceholder: isLoading,
1633
+ icon,
1634
+ layout,
1635
+ status
1636
+ }
1637
+ );
1638
+ }
1639
+ function getIconWithFallback(icon, schemaType, defaultIcon) {
1640
+ return icon === !1 ? !1 : icon || schemaType && schemaType.icon || defaultIcon || !1;
1641
+ }
1642
+ function DocumentPreviewLink(props) {
1643
+ return (linkProps) => /* @__PURE__ */ jsxRuntime.jsx(router.IntentLink, { intent: "edit", params: { id: props.documentPair.id }, children: linkProps.children });
1644
+ }
1645
+ function DocumentPreview(props) {
1646
+ const { schemaType, documentPair } = props, doc = documentPair?.draft || documentPair?.published, id = documentPair.id || "", documentPreviewStore = sanity.useDocumentPreviewStore(), schema = sanity.useSchema(), documentPresence = sanity.useDocumentPresence(id), hasSchemaType = !!(schemaType && schemaType.name && schema.get(schemaType.name)), PreviewComponent = React.useMemo(() => doc ? !schemaType || !hasSchemaType ? /* @__PURE__ */ jsxRuntime.jsx(MissingSchemaType, { value: doc }) : /* @__PURE__ */ jsxRuntime.jsx(
1647
+ PaneItemPreview,
1648
+ {
1649
+ documentPreviewStore,
1650
+ icon: getIconWithFallback(void 0, schemaType, icons.DocumentIcon),
1651
+ schemaType,
1652
+ layout: "default",
1653
+ value: doc,
1654
+ presence: documentPresence
1655
+ }
1656
+ ) : null, [hasSchemaType, schemaType, documentPresence, doc, documentPreviewStore]);
1657
+ return /* @__PURE__ */ jsxRuntime.jsx(
1658
+ sanity.PreviewCard,
1659
+ {
1660
+ __unstable_focusRing: !0,
1661
+ as: DocumentPreviewLink(props),
1662
+ "data-as": "a",
1663
+ "data-ui": "PaneItem",
1664
+ padding: 2,
1665
+ radius: 2,
1666
+ tone: "inherit",
1667
+ children: PreviewComponent
1668
+ }
1669
+ );
1670
+ }
1671
+ const Container = styledComponents.styled(ui.Box)`
1672
+ * {
1673
+ color: ${(props) => props.theme.sanity.color.base.fg};
1674
+ }
1675
+ a {
1676
+ text-decoration: none;
1677
+ }
1678
+ h2 {
1679
+ font-size: ${(props) => props.theme.sanity.fonts.text.sizes[1]};
1680
+ }
1681
+ `, VideoReferences = (props) => {
1682
+ const schema = sanity.useSchema();
1683
+ if (!props.isLoaded)
1684
+ return /* @__PURE__ */ jsxRuntime.jsx(SpinnerBox, {});
1685
+ if (!props.references?.length)
1686
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { border: !0, radius: 3, padding: 3, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, children: "No documents are using this video" }) });
1687
+ const documentPairs = sanity.collate(props.references || []);
1688
+ return /* @__PURE__ */ jsxRuntime.jsx(Container, { children: documentPairs?.map((documentPair) => {
1689
+ const schemaType = schema.get(documentPair.type);
1690
+ return /* @__PURE__ */ jsxRuntime.jsx(
1691
+ ui.Card,
1692
+ {
1693
+ marginBottom: 2,
1694
+ padding: 2,
1695
+ radius: 2,
1696
+ shadow: 1,
1697
+ style: { overflow: "hidden" },
1698
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(DocumentPreview, { documentPair, schemaType }) })
1699
+ },
1700
+ documentPair.id
1701
+ );
1702
+ }) });
1703
+ };
1704
+ function DeleteDialog({
1705
+ asset,
1706
+ references,
1707
+ referencesLoading,
1708
+ cancelDelete,
1709
+ succeededDeleting
1710
+ }) {
1711
+ const client = useClient(), [state, setState] = React.useState("checkingReferences"), [deleteOnMux, setDeleteOnMux] = React.useState(!0), toast = ui.useToast();
1712
+ React.useEffect(() => {
1713
+ state !== "checkingReferences" || referencesLoading || setState(references?.length ? "cantDelete" : "confirm");
1714
+ }, [state, references, referencesLoading]);
1715
+ async function confirmDelete() {
1716
+ if (state !== "confirm") return;
1717
+ setState("processing_deletion");
1718
+ const worked = await deleteAsset({ client, asset, deleteOnMux });
1719
+ worked === !0 ? (toast.push({ title: "Successfully deleted video", status: "success" }), succeededDeleting()) : worked === "failed-mux" ? (toast.push({
1720
+ title: "Deleted video in Sanity",
1721
+ description: "But it wasn't deleted in Mux",
1722
+ status: "warning"
1723
+ }), succeededDeleting()) : (toast.push({ title: "Failed deleting video", status: "error" }), setState("error_deleting"));
1724
+ }
1725
+ return /* @__PURE__ */ jsxRuntime.jsx(
1726
+ ui.Dialog,
1727
+ {
1728
+ animate: !0,
1729
+ header: "Delete video",
1730
+ zOffset: DIALOGS_Z_INDEX,
1731
+ id: "deleting-video-details-dialog",
1732
+ onClose: cancelDelete,
1733
+ onClickOutside: cancelDelete,
1734
+ width: 1,
1735
+ position: "fixed",
1736
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1737
+ ui.Card,
1738
+ {
1739
+ padding: 3,
1740
+ style: {
1741
+ minHeight: "150px",
1742
+ display: "flex",
1743
+ alignItems: "center",
1744
+ justifyContent: "center"
1745
+ },
1746
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
1747
+ state === "checkingReferences" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1748
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { size: 2, children: "Checking if video can be deleted" }),
1749
+ /* @__PURE__ */ jsxRuntime.jsx(SpinnerBox, {})
1750
+ ] }),
1751
+ state === "cantDelete" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1752
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { size: 2, children: "Video can't be deleted" }),
1753
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 2, style: { marginBottom: "2rem" }, children: [
1754
+ "There are ",
1755
+ references?.length,
1756
+ " document",
1757
+ references && references.length > 0 && "s",
1758
+ " ",
1759
+ "pointing to this video. Remove their references to this file or delete them before proceeding."
1760
+ ] }),
1761
+ /* @__PURE__ */ jsxRuntime.jsx(VideoReferences, { references, isLoaded: !referencesLoading })
1762
+ ] }),
1763
+ state === "confirm" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1764
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { size: 2, children: "Are you sure you want to delete this video?" }),
1765
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, children: "This action is irreversible" }),
1766
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, marginY: 4, children: [
1767
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", as: "label", children: [
1768
+ /* @__PURE__ */ jsxRuntime.jsx(
1769
+ ui.Checkbox,
1770
+ {
1771
+ checked: deleteOnMux,
1772
+ onChange: () => setDeleteOnMux((prev) => !prev)
1773
+ }
1774
+ ),
1775
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { style: { margin: "0 10px" }, children: "Delete asset on Mux" })
1776
+ ] }),
1777
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", as: "label", children: [
1778
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Checkbox, { disabled: !0, checked: !0 }),
1779
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { style: { margin: "0 10px" }, children: "Delete video from dataset" })
1780
+ ] }),
1781
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(
1782
+ ui.Button,
1783
+ {
1784
+ icon: icons.TrashIcon,
1785
+ fontSize: 2,
1786
+ padding: 3,
1787
+ text: "Delete video",
1788
+ tone: "critical",
1789
+ onClick: confirmDelete,
1790
+ disabled: ["processing_deletion", "checkingReferences", "cantDelete"].some(
1791
+ (s) => s === state
1563
1792
  )
1564
- ] })
1565
- ]
1566
- }
1567
- )
1568
- }
1569
- )
1570
- ]
1793
+ }
1794
+ ) })
1795
+ ] })
1796
+ ] }),
1797
+ state === "processing_deletion" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1798
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { size: 2, children: "Deleting video..." }),
1799
+ /* @__PURE__ */ jsxRuntime.jsx(SpinnerBox, {})
1800
+ ] }),
1801
+ state === "error_deleting" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1802
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { size: 2, children: "Something went wrong!" }),
1803
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, children: "Try deleting the video again by clicking the button below" })
1804
+ ] })
1805
+ ] })
1806
+ }
1807
+ )
1571
1808
  }
1572
1809
  );
1573
- }, VideoMetadata = (props) => {
1574
- if (!props.asset)
1575
- return null;
1576
- const displayInfo = getVideoMetadata(props.asset);
1577
- return /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1578
- displayInfo.title && /* @__PURE__ */ jsxRuntime.jsx(
1579
- ui.Text,
1580
- {
1581
- size: 1,
1582
- weight: "semibold",
1583
- style: {
1584
- wordWrap: "break-word"
1585
- },
1586
- children: displayInfo.title
1587
- }
1588
- ),
1589
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Inline, { space: 3, children: [
1590
- displayInfo?.duration && /* @__PURE__ */ jsxRuntime.jsx(IconInfo, { text: displayInfo.duration, icon: icons.ClockIcon, size: 1, muted: !0 }),
1591
- /* @__PURE__ */ jsxRuntime.jsx(
1592
- IconInfo,
1593
- {
1594
- text: displayInfo.createdAt.toISOString().split("T")[0],
1595
- icon: icons.CalendarIcon,
1596
- size: 1,
1597
- muted: !0
1598
- }
1599
- ),
1600
- displayInfo.title != displayInfo.id.slice(0, 12) && /* @__PURE__ */ jsxRuntime.jsx(IconInfo, { text: displayInfo.id.slice(0, 12), icon: icons.TagIcon, size: 1, muted: !0 })
1601
- ] })
1602
- ] });
1603
- }, PlayButton = styledComponents.styled.button`
1604
- display: block;
1605
- padding: 0;
1606
- margin: 0;
1607
- border: none;
1608
- border-radius: 0.1875rem;
1609
- position: relative;
1610
- cursor: pointer;
1611
-
1612
- &::after {
1613
- content: '';
1614
- background: var(--card-fg-color);
1615
- opacity: 0;
1616
- display: block;
1617
- position: absolute;
1618
- inset: 0;
1619
- z-index: 10;
1620
- transition: 0.15s ease-out;
1621
- border-radius: inherit;
1810
+ }
1811
+ const useDocReferences = sanity.createHookFromObservableFactory(({ documentStore, id }) => documentStore.listenQuery(
1812
+ /* groq */
1813
+ "*[references($id)]{_id, _type, _rev, _updatedAt, _createdAt}",
1814
+ { id },
1815
+ {
1816
+ apiVersion: SANITY_API_VERSION
1622
1817
  }
1623
-
1624
- > div[data-play] {
1625
- z-index: 11;
1626
- opacity: 0;
1627
- transition: 0.15s 0.05s ease-out;
1628
- position: absolute;
1629
- left: 50%;
1630
- top: 50%;
1631
- transform: translate(-50%, -50%);
1632
- color: var(--card-fg-color);
1633
- background: var(--card-bg-color);
1634
- width: auto;
1635
- height: 30%;
1636
- aspect-ratio: 1;
1637
- border-radius: 100%;
1638
- display: flex;
1639
- justify-content: center;
1640
- align-items: center;
1641
- box-sizing: border-box;
1642
- > svg {
1643
- display: block;
1644
- width: 70%;
1645
- height: auto;
1646
- // Visual balance to center-align the icon
1647
- transform: translateX(5%);
1818
+ ));
1819
+ function getVideoMetadata(doc) {
1820
+ 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());
1821
+ return {
1822
+ title: doc.filename || id.slice(0, 12),
1823
+ id,
1824
+ playbackId: doc.playbackId,
1825
+ createdAt: date,
1826
+ duration: doc.data?.duration ? formatSeconds(doc.data?.duration) : void 0,
1827
+ aspect_ratio: doc.data?.aspect_ratio,
1828
+ max_stored_resolution: doc.data?.max_stored_resolution,
1829
+ max_stored_frame_rate: doc.data?.max_stored_frame_rate
1830
+ };
1831
+ }
1832
+ function useVideoDetails(props) {
1833
+ const documentStore = sanity.useDocumentStore(), toast = ui.useToast(), client = useClient(), [references, referencesLoading] = useDocReferences(
1834
+ React.useMemo(() => ({ documentStore, id: props.asset._id }), [documentStore, props.asset._id])
1835
+ ), [originalAsset, setOriginalAsset] = React.useState(() => props.asset), [filename, setFilename] = React.useState(props.asset.filename), modified = filename !== originalAsset.filename, displayInfo = getVideoMetadata({ ...props.asset, filename }), [state, setState] = React.useState("idle");
1836
+ function handleClose() {
1837
+ if (state === "idle") {
1838
+ if (modified) {
1839
+ setState("closing");
1840
+ return;
1841
+ }
1842
+ props.closeDialog();
1648
1843
  }
1649
1844
  }
1650
-
1651
- &:hover,
1652
- &:focus {
1653
- &::after {
1654
- opacity: 0.3;
1655
- }
1656
- > div[data-play] {
1657
- opacity: 1;
1845
+ function confirmClose(shouldClose) {
1846
+ state === "closing" && (shouldClose && props.closeDialog(), setState("idle"));
1847
+ }
1848
+ async function saveChanges() {
1849
+ if (state === "idle") {
1850
+ setState("saving");
1851
+ try {
1852
+ await client.patch(props.asset._id).set({ filename }).commit(), setOriginalAsset((prev) => ({ ...prev, filename })), toast.push({
1853
+ title: "Video title updated",
1854
+ description: `New title: ${filename}`,
1855
+ status: "success"
1856
+ }), props.closeDialog();
1857
+ } catch (error) {
1858
+ toast.push({
1859
+ title: "Failed updating file name",
1860
+ status: "error",
1861
+ description: typeof error == "string" ? error : "Please try again"
1862
+ }), setFilename(originalAsset.filename);
1863
+ }
1864
+ setState("idle");
1658
1865
  }
1659
1866
  }
1660
- `;
1661
- function VideoInBrowser({
1662
- onSelect,
1663
- onEdit,
1664
- asset
1665
- }) {
1666
- const [renderVideo, setRenderVideo] = React.useState(!1), select = React__default.default.useCallback(() => onSelect?.(asset), [onSelect, asset]), edit = React__default.default.useCallback(() => onEdit?.(asset), [onEdit, asset]);
1667
- if (!asset)
1668
- return null;
1669
- const playbackPolicy = getPlaybackPolicy(asset);
1670
- return /* @__PURE__ */ jsxRuntime.jsxs(
1671
- ui.Card,
1867
+ return {
1868
+ references,
1869
+ referencesLoading,
1870
+ modified,
1871
+ filename,
1872
+ setFilename,
1873
+ displayInfo,
1874
+ state,
1875
+ setState,
1876
+ handleClose,
1877
+ confirmClose,
1878
+ saveChanges
1879
+ };
1880
+ }
1881
+ const AssetInput = (props) => /* @__PURE__ */ jsxRuntime.jsx(FormField$1, { title: props.label, description: props.description, inputId: props.label, children: /* @__PURE__ */ jsxRuntime.jsx(
1882
+ ui.TextInput,
1883
+ {
1884
+ id: props.label,
1885
+ value: props.value,
1886
+ placeholder: props.placeholder,
1887
+ onInput: props.onInput,
1888
+ disabled: props.disabled
1889
+ }
1890
+ ) }), VideoDetails = (props) => {
1891
+ const [tab, setTab] = React.useState("details"), {
1892
+ displayInfo,
1893
+ filename,
1894
+ modified,
1895
+ references,
1896
+ referencesLoading,
1897
+ setFilename,
1898
+ state,
1899
+ setState,
1900
+ handleClose,
1901
+ confirmClose,
1902
+ saveChanges
1903
+ } = useVideoDetails(props), isSaving = state === "saving", [containerHeight, setContainerHeight] = React.useState(null), contentsRef = React__default.default.useRef(null);
1904
+ return React.useEffect(() => {
1905
+ !contentsRef.current || !("getBoundingClientRect" in contentsRef.current) || setContainerHeight(contentsRef.current.getBoundingClientRect().height);
1906
+ }, []), /* @__PURE__ */ jsxRuntime.jsxs(
1907
+ ui.Dialog,
1672
1908
  {
1673
- border: !0,
1674
- padding: 2,
1675
- sizing: "border",
1676
- radius: 2,
1677
- style: {
1678
- position: "relative"
1679
- },
1909
+ animate: !0,
1910
+ header: displayInfo.title,
1911
+ zOffset: DIALOGS_Z_INDEX,
1912
+ id: "video-details-dialog",
1913
+ onClose: handleClose,
1914
+ onClickOutside: handleClose,
1915
+ width: 2,
1916
+ position: "fixed",
1917
+ footer: /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 3, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "space-between", align: "center", children: [
1918
+ /* @__PURE__ */ jsxRuntime.jsx(
1919
+ ui.Button,
1920
+ {
1921
+ icon: icons.TrashIcon,
1922
+ fontSize: 2,
1923
+ padding: 3,
1924
+ mode: "bleed",
1925
+ text: "Delete",
1926
+ tone: "critical",
1927
+ onClick: () => setState("deleting"),
1928
+ disabled: isSaving
1929
+ }
1930
+ ),
1931
+ modified && /* @__PURE__ */ jsxRuntime.jsx(
1932
+ ui.Button,
1933
+ {
1934
+ icon: icons.CheckmarkIcon,
1935
+ fontSize: 2,
1936
+ padding: 3,
1937
+ mode: "ghost",
1938
+ text: "Save and close",
1939
+ tone: "positive",
1940
+ onClick: saveChanges,
1941
+ iconRight: isSaving && ui.Spinner,
1942
+ disabled: isSaving
1943
+ }
1944
+ )
1945
+ ] }) }),
1680
1946
  children: [
1681
- playbackPolicy === "signed" && /* @__PURE__ */ jsxRuntime.jsx(
1682
- ui.Tooltip,
1947
+ state === "deleting" && /* @__PURE__ */ jsxRuntime.jsx(
1948
+ DeleteDialog,
1949
+ {
1950
+ asset: props.asset,
1951
+ cancelDelete: () => setState("idle"),
1952
+ referencesLoading,
1953
+ references,
1954
+ succeededDeleting: () => {
1955
+ props.closeDialog();
1956
+ }
1957
+ }
1958
+ ),
1959
+ state === "closing" && /* @__PURE__ */ jsxRuntime.jsx(
1960
+ ui.Dialog,
1683
1961
  {
1684
1962
  animate: !0,
1685
- content: /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 2, radius: 2, children: /* @__PURE__ */ jsxRuntime.jsx(IconInfo, { icon: icons.LockIcon, text: "Signed playback policy", size: 2 }) }),
1686
- placement: "right",
1687
- fallbackPlacements: ["top", "bottom"],
1688
- portal: !0,
1689
- children: /* @__PURE__ */ jsxRuntime.jsx(
1690
- ui.Card,
1691
- {
1692
- tone: "caution",
1693
- style: {
1694
- borderRadius: "100%",
1695
- position: "absolute",
1696
- left: "1em",
1697
- top: "1em",
1698
- zIndex: 10
1699
- },
1700
- padding: 2,
1701
- border: !0,
1702
- children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { muted: !0, size: 1, children: /* @__PURE__ */ jsxRuntime.jsx(icons.LockIcon, {}) })
1703
- }
1704
- )
1963
+ header: "You have unsaved changes",
1964
+ zOffset: DIALOGS_Z_INDEX,
1965
+ id: "closing-video-details-dialog",
1966
+ onClose: () => confirmClose(!1),
1967
+ onClickOutside: () => confirmClose(!1),
1968
+ width: 1,
1969
+ position: "fixed",
1970
+ footer: /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 3, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "space-between", align: "center", children: [
1971
+ /* @__PURE__ */ jsxRuntime.jsx(
1972
+ ui.Button,
1973
+ {
1974
+ icon: icons.ErrorOutlineIcon,
1975
+ fontSize: 2,
1976
+ padding: 3,
1977
+ text: "Discard changes",
1978
+ tone: "critical",
1979
+ onClick: () => confirmClose(!0)
1980
+ }
1981
+ ),
1982
+ modified && /* @__PURE__ */ jsxRuntime.jsx(
1983
+ ui.Button,
1984
+ {
1985
+ icon: icons.RevertIcon,
1986
+ fontSize: 2,
1987
+ padding: 3,
1988
+ mode: "ghost",
1989
+ text: "Keep editing",
1990
+ tone: "primary",
1991
+ onClick: () => confirmClose(!1)
1992
+ }
1993
+ )
1994
+ ] }) }),
1995
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 5, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { style: { textAlign: "center" }, space: 3, children: [
1996
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { size: 2, children: "Unsaved changes will be lost" }),
1997
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, children: "Are you sure you want to discard them?" })
1998
+ ] }) })
1705
1999
  }
1706
2000
  ),
1707
- /* @__PURE__ */ jsxRuntime.jsxs(
1708
- ui.Stack,
2001
+ /* @__PURE__ */ jsxRuntime.jsx(
2002
+ ui.Card,
1709
2003
  {
1710
- space: 3,
1711
- height: "fill",
2004
+ padding: 4,
2005
+ sizing: "border",
1712
2006
  style: {
1713
- gridTemplateRows: "min-content min-content 1fr"
2007
+ containerType: "inline-size"
1714
2008
  },
1715
- children: [
1716
- renderVideo ? /* @__PURE__ */ jsxRuntime.jsx(VideoPlayer, { asset, autoPlay: !0, forceAspectRatio: THUMBNAIL_ASPECT_RATIO }) : /* @__PURE__ */ jsxRuntime.jsxs(PlayButton, { onClick: () => setRenderVideo(!0), children: [
1717
- /* @__PURE__ */ jsxRuntime.jsx("div", { "data-play": !0, children: /* @__PURE__ */ jsxRuntime.jsx(icons.PlayIcon, {}) }),
1718
- assetIsAudio(asset) ? /* @__PURE__ */ jsxRuntime.jsx(
1719
- "div",
1720
- {
1721
- style: {
1722
- aspectRatio: THUMBNAIL_ASPECT_RATIO,
1723
- display: "flex",
1724
- alignItems: "center",
1725
- justifyContent: "center"
1726
- },
1727
- children: /* @__PURE__ */ jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "3em", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntime.jsx(
1728
- "path",
1729
- {
1730
- fill: "currentColor",
1731
- style: { opacity: "0.65" },
1732
- d: "M10.75 19q.95 0 1.6-.65t.65-1.6V13h3v-2h-4v3.875q-.275-.2-.587-.288t-.663-.087q-.95 0-1.6.65t-.65 1.6t.65 1.6t1.6.65M6 22q-.825 0-1.412-.587T4 20V4q0-.825.588-1.412T6 2h8l6 6v12q0 .825-.587 1.413T18 22zm7-13V4H6v16h12V9zM6 4v5zv16z"
1733
- }
1734
- ) })
1735
- }
1736
- ) : /* @__PURE__ */ jsxRuntime.jsx(VideoThumbnail, { asset })
1737
- ] }),
1738
- /* @__PURE__ */ jsxRuntime.jsx(VideoMetadata, { asset }),
1739
- /* @__PURE__ */ jsxRuntime.jsxs(
1740
- "div",
1741
- {
1742
- style: {
1743
- display: "flex",
1744
- width: "100%",
1745
- alignItems: "flex-end",
1746
- justifyContent: "flex-start",
1747
- gap: ".35rem"
1748
- },
1749
- children: [
1750
- onSelect && /* @__PURE__ */ jsxRuntime.jsx(
1751
- ui.Button,
2009
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
2010
+ ui.Flex,
2011
+ {
2012
+ sizing: "border",
2013
+ gap: 4,
2014
+ direction: ["column", "column", "row"],
2015
+ align: "flex-start",
2016
+ ref: contentsRef,
2017
+ style: typeof containerHeight == "number" ? {
2018
+ minHeight: containerHeight
2019
+ } : void 0,
2020
+ children: [
2021
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 4, flex: 1, sizing: "border", children: /* @__PURE__ */ jsxRuntime.jsx(VideoPlayer, { asset: props.asset, autoPlay: props.asset.autoPlay || !1 }) }),
2022
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, flex: 1, sizing: "border", children: [
2023
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.TabList, { space: 2, children: [
2024
+ /* @__PURE__ */ jsxRuntime.jsx(
2025
+ ui.Tab,
2026
+ {
2027
+ "aria-controls": "details-panel",
2028
+ icon: icons.EditIcon,
2029
+ id: "details-tab",
2030
+ label: "Details",
2031
+ onClick: () => setTab("details"),
2032
+ selected: tab === "details"
2033
+ }
2034
+ ),
2035
+ /* @__PURE__ */ jsxRuntime.jsx(
2036
+ ui.Tab,
2037
+ {
2038
+ "aria-controls": "references-panel",
2039
+ icon: icons.SearchIcon,
2040
+ id: "references-tab",
2041
+ label: `Used by ${references ? `(${references.length})` : ""}`,
2042
+ onClick: () => setTab("references"),
2043
+ selected: tab === "references"
2044
+ }
2045
+ )
2046
+ ] }),
2047
+ /* @__PURE__ */ jsxRuntime.jsx(
2048
+ ui.TabPanel,
1752
2049
  {
1753
- icon: icons.CheckmarkIcon,
1754
- fontSize: 2,
1755
- padding: 2,
1756
- mode: "ghost",
1757
- text: "Select",
1758
- style: { flex: 1 },
1759
- tone: "positive",
1760
- onClick: select
2050
+ "aria-labelledby": "details-tab",
2051
+ id: "details-panel",
2052
+ hidden: tab !== "details",
2053
+ style: { wordBreak: "break-word" },
2054
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, children: [
2055
+ /* @__PURE__ */ jsxRuntime.jsx(
2056
+ AssetInput,
2057
+ {
2058
+ label: "Video title or file name",
2059
+ description: "Not visible to users. Useful for finding videos later.",
2060
+ value: filename || "",
2061
+ onInput: (e) => setFilename(e.currentTarget.value),
2062
+ disabled: state !== "idle"
2063
+ }
2064
+ ),
2065
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
2066
+ displayInfo?.duration && /* @__PURE__ */ jsxRuntime.jsx(
2067
+ IconInfo,
2068
+ {
2069
+ text: `Duration: ${displayInfo.duration}`,
2070
+ icon: icons.ClockIcon,
2071
+ size: 2
2072
+ }
2073
+ ),
2074
+ displayInfo?.max_stored_resolution && /* @__PURE__ */ jsxRuntime.jsx(
2075
+ IconInfo,
2076
+ {
2077
+ text: `Max Resolution: ${displayInfo.max_stored_resolution}`,
2078
+ icon: ResolutionIcon,
2079
+ size: 2
2080
+ }
2081
+ ),
2082
+ displayInfo?.max_stored_frame_rate && /* @__PURE__ */ jsxRuntime.jsx(
2083
+ IconInfo,
2084
+ {
2085
+ text: `Frame rate: ${displayInfo.max_stored_frame_rate}`,
2086
+ icon: StopWatchIcon,
2087
+ size: 2
2088
+ }
2089
+ ),
2090
+ displayInfo?.aspect_ratio && /* @__PURE__ */ jsxRuntime.jsx(
2091
+ IconInfo,
2092
+ {
2093
+ text: `Aspect Ratio: ${displayInfo.aspect_ratio}`,
2094
+ icon: icons.CropIcon,
2095
+ size: 2
2096
+ }
2097
+ ),
2098
+ /* @__PURE__ */ jsxRuntime.jsx(
2099
+ IconInfo,
2100
+ {
2101
+ text: `Uploaded on: ${displayInfo.createdAt.toLocaleDateString("en", {
2102
+ year: "numeric",
2103
+ month: "2-digit",
2104
+ day: "2-digit",
2105
+ hour: "2-digit",
2106
+ minute: "2-digit",
2107
+ hour12: !0
2108
+ })}`,
2109
+ icon: icons.CalendarIcon,
2110
+ size: 2
2111
+ }
2112
+ ),
2113
+ /* @__PURE__ */ jsxRuntime.jsx(IconInfo, { text: `Mux ID:
2114
+ ${displayInfo.id}`, icon: icons.TagIcon, size: 2 }),
2115
+ displayInfo?.playbackId && /* @__PURE__ */ jsxRuntime.jsx(
2116
+ IconInfo,
2117
+ {
2118
+ text: `Playback ID: ${displayInfo.playbackId}`,
2119
+ icon: icons.TagIcon,
2120
+ size: 2
2121
+ }
2122
+ )
2123
+ ] })
2124
+ ] })
1761
2125
  }
1762
2126
  ),
1763
2127
  /* @__PURE__ */ jsxRuntime.jsx(
1764
- ui.Button,
2128
+ ui.TabPanel,
1765
2129
  {
1766
- icon: icons.EditIcon,
1767
- fontSize: 2,
1768
- padding: 2,
1769
- mode: "ghost",
1770
- text: "Details",
1771
- style: { flex: 1 },
1772
- onClick: edit
2130
+ "aria-labelledby": "references-tab",
2131
+ id: "references-panel",
2132
+ hidden: tab !== "references",
2133
+ children: /* @__PURE__ */ jsxRuntime.jsx(VideoReferences, { references, isLoaded: !referencesLoading })
1773
2134
  }
1774
2135
  )
1775
- ]
1776
- }
1777
- )
1778
- ]
2136
+ ] })
2137
+ ]
2138
+ }
2139
+ )
1779
2140
  }
1780
2141
  )
1781
2142
  ]
1782
2143
  }
1783
2144
  );
1784
- }
1785
- function VideosBrowser({ onSelect }) {
1786
- const { assets, isLoading, searchQuery, setSearchQuery, setSort, sort } = useAssets(), [editedAsset, setEditedAsset] = React.useState(null), freshEditedAsset = React.useMemo(
1787
- () => assets.find((a2) => a2._id === editedAsset?._id) || editedAsset,
1788
- [editedAsset, assets]
1789
- );
1790
- return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1791
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { padding: 4, space: 4, style: { minHeight: "50vh" }, children: [
1792
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "space-between", align: "center", children: [
1793
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 3, children: [
1794
- /* @__PURE__ */ jsxRuntime.jsx(
1795
- ui.TextInput,
1796
- {
1797
- value: searchQuery,
1798
- icon: icons.SearchIcon,
1799
- onInput: (e) => setSearchQuery(e.currentTarget.value),
1800
- placeholder: "Search videos"
1801
- }
1802
- ),
1803
- /* @__PURE__ */ jsxRuntime.jsx(SelectSortOptions, { setSort, sort })
1804
- ] }),
1805
- (onSelect ? "input" : "tool") == "tool" && /* @__PURE__ */ jsxRuntime.jsx(ImportVideosFromMux, {})
1806
- ] }),
1807
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
1808
- assets?.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ui.Label, { muted: !0, children: [
1809
- assets.length,
1810
- " video",
1811
- assets.length > 1 ? "s" : null,
1812
- " ",
1813
- searchQuery ? `matching "${searchQuery}"` : "found"
1814
- ] }),
1815
- /* @__PURE__ */ jsxRuntime.jsx(
1816
- ui.Grid,
1817
- {
1818
- gap: 2,
1819
- style: {
1820
- gridTemplateColumns: "repeat(auto-fill, minmax(250px, 1fr))"
1821
- },
1822
- children: assets.map((asset) => /* @__PURE__ */ jsxRuntime.jsx(
1823
- VideoInBrowser,
1824
- {
1825
- asset,
1826
- onEdit: setEditedAsset,
1827
- onSelect
1828
- },
1829
- asset._id
1830
- ))
1831
- }
1832
- )
1833
- ] }),
1834
- isLoading && /* @__PURE__ */ jsxRuntime.jsx(SpinnerBox, {}),
1835
- !isLoading && assets.length === 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { marginY: 4, paddingX: 4, paddingY: 6, border: !0, radius: 2, tone: "transparent", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { align: "center", muted: !0, size: 3, children: searchQuery ? `No videos found for "${searchQuery}"` : "No videos in this dataset" }) })
1836
- ] }),
1837
- freshEditedAsset && /* @__PURE__ */ jsxRuntime.jsx(VideoDetails, { closeDialog: () => setEditedAsset(null), asset: freshEditedAsset })
2145
+ }, VideoMetadata = (props) => {
2146
+ if (!props.asset)
2147
+ return null;
2148
+ const displayInfo = getVideoMetadata(props.asset);
2149
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
2150
+ displayInfo.title && /* @__PURE__ */ jsxRuntime.jsx(
2151
+ ui.Text,
2152
+ {
2153
+ size: 1,
2154
+ weight: "semibold",
2155
+ style: {
2156
+ wordWrap: "break-word"
2157
+ },
2158
+ children: displayInfo.title
2159
+ }
2160
+ ),
2161
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Inline, { space: 3, children: [
2162
+ displayInfo?.duration && /* @__PURE__ */ jsxRuntime.jsx(IconInfo, { text: displayInfo.duration, icon: icons.ClockIcon, size: 1, muted: !0 }),
2163
+ /* @__PURE__ */ jsxRuntime.jsx(
2164
+ IconInfo,
2165
+ {
2166
+ text: displayInfo.createdAt.toISOString().split("T")[0],
2167
+ icon: icons.CalendarIcon,
2168
+ size: 1,
2169
+ muted: !0
2170
+ }
2171
+ ),
2172
+ displayInfo.title != displayInfo.id.slice(0, 12) && /* @__PURE__ */ jsxRuntime.jsx(IconInfo, { text: displayInfo.id.slice(0, 12), icon: icons.TagIcon, size: 1, muted: !0 })
2173
+ ] })
1838
2174
  ] });
1839
- }
1840
- const StudioTool = () => /* @__PURE__ */ jsxRuntime.jsx(VideosBrowser, {}), DEFAULT_TOOL_CONFIG = {
1841
- icon: ToolIcon,
1842
- title: "Videos"
1843
- };
1844
- function createStudioTool(config) {
1845
- const toolConfig = typeof config.tool == "object" ? config.tool : DEFAULT_TOOL_CONFIG;
1846
- return {
1847
- name: "mux",
1848
- icon: toolConfig.icon || DEFAULT_TOOL_CONFIG.icon,
1849
- title: toolConfig.title || DEFAULT_TOOL_CONFIG.title,
1850
- component: (props) => /* @__PURE__ */ jsxRuntime.jsx(StudioTool, { ...config, ...props })
1851
- };
1852
- }
1853
- const path = ["assetId", "data", "playbackId", "status", "thumbTime", "filename"], useAssetDocumentValues = (asset) => sanity.useDocumentValues(
1854
- sanity.isReference(asset) ? asset._ref : "",
1855
- path
1856
- );
1857
- function useDialogState() {
1858
- return React.useState(!1);
1859
- }
1860
- const useMuxPolling = (asset) => {
1861
- const client = useClient(), projectId = sanity.useProjectId(), dataset = sanity.useDataset(), shouldFetch = React.useMemo(
1862
- () => !!asset?.assetId && (asset?.status === "preparing" || asset?.data?.static_renditions?.status === "preparing"),
1863
- [asset?.assetId, asset?.data?.static_renditions?.status, asset?.status]
1864
- );
1865
- return useSWR__default.default(
1866
- shouldFetch ? `/${projectId}/addons/mux/assets/${dataset}/data/${asset?.assetId}` : null,
1867
- async () => {
1868
- const { data } = await client.request({
1869
- url: `/addons/mux/assets/${dataset}/data/${asset.assetId}`,
1870
- withCredentials: !0,
1871
- method: "GET"
1872
- });
1873
- client.patch(asset._id).set({ status: data.status, data }).commit({ returnDocuments: !1 });
1874
- },
1875
- { refreshInterval: 2e3, refreshWhenHidden: !0, dedupingInterval: 1e3 }
1876
- );
1877
- };
1878
- function saveSecrets(client, token, secretKey, enableSignedUrls, signingKeyId, signingKeyPrivate) {
1879
- const doc = {
1880
- _id: "secrets.mux",
1881
- _type: "mux.apiKey",
1882
- token,
1883
- secretKey,
1884
- enableSignedUrls,
1885
- signingKeyId,
1886
- signingKeyPrivate
1887
- };
1888
- return client.createOrReplace(doc);
1889
- }
1890
- async function createSigningKeys(client) {
1891
- try {
1892
- const { dataset } = client.config();
1893
- return await client.request({
1894
- url: `/addons/mux/signing-keys/${dataset}`,
1895
- withCredentials: !0,
1896
- method: "POST"
1897
- });
1898
- } catch (error) {
1899
- console.error("Error creating signing keys", error);
1900
- const message = error.response?.statusCode === 401 ? 'Unauthorized - Failed to create the Signing Key. Please ensure that the token has "System" permissions' : error.message;
1901
- throw new Error(message);
1902
- }
1903
- }
1904
- function testSecrets(client) {
1905
- const { dataset } = client.config();
1906
- return client.request({
1907
- url: `/addons/mux/secrets/${dataset}/test`,
1908
- withCredentials: !0,
1909
- method: "GET"
1910
- });
1911
- }
1912
- async function haveValidSigningKeys(client, signingKeyId, signingKeyPrivate) {
1913
- if (!(signingKeyId && signingKeyPrivate))
1914
- return !1;
1915
- const { dataset } = client.config();
1916
- try {
1917
- const res = await client.request({
1918
- url: `/addons/mux/signing-keys/${dataset}/${signingKeyId}`,
1919
- withCredentials: !0,
1920
- method: "GET"
1921
- });
1922
- return !!(res.data && res.data.id);
1923
- } catch {
1924
- return console.error("Error fetching signingKeyId", signingKeyId, "assuming it is not valid"), !1;
2175
+ }, PlayButton = styledComponents.styled.button`
2176
+ display: block;
2177
+ padding: 0;
2178
+ margin: 0;
2179
+ border: none;
2180
+ border-radius: 0.1875rem;
2181
+ position: relative;
2182
+ cursor: pointer;
2183
+
2184
+ &::after {
2185
+ content: '';
2186
+ background: var(--card-fg-color);
2187
+ opacity: 0;
2188
+ display: block;
2189
+ position: absolute;
2190
+ inset: 0;
2191
+ z-index: 10;
2192
+ transition: 0.15s ease-out;
2193
+ border-radius: inherit;
1925
2194
  }
1926
- }
1927
- function testSecretsObservable(client) {
1928
- const { dataset } = client.config();
1929
- return rxjs.defer(
1930
- () => client.observable.request({
1931
- url: `/addons/mux/secrets/${dataset}/test`,
1932
- withCredentials: !0,
1933
- method: "GET"
1934
- })
1935
- );
1936
- }
1937
- const useSaveSecrets = (client, secrets) => React.useCallback(
1938
- async ({
1939
- token,
1940
- secretKey,
1941
- enableSignedUrls
1942
- }) => {
1943
- let { signingKeyId, signingKeyPrivate } = secrets;
1944
- try {
1945
- if (await saveSecrets(
1946
- client,
1947
- token,
1948
- secretKey,
1949
- enableSignedUrls,
1950
- signingKeyId,
1951
- signingKeyPrivate
1952
- ), !(await testSecrets(client))?.status && token && secretKey)
1953
- throw new Error("Invalid secrets");
1954
- } catch (err) {
1955
- throw console.error("Error while trying to save secrets:", err), err;
2195
+
2196
+ > div[data-play] {
2197
+ z-index: 11;
2198
+ opacity: 0;
2199
+ transition: 0.15s 0.05s ease-out;
2200
+ position: absolute;
2201
+ left: 50%;
2202
+ top: 50%;
2203
+ transform: translate(-50%, -50%);
2204
+ color: var(--card-fg-color);
2205
+ background: var(--card-bg-color);
2206
+ width: auto;
2207
+ height: 30%;
2208
+ aspect-ratio: 1;
2209
+ border-radius: 100%;
2210
+ display: flex;
2211
+ justify-content: center;
2212
+ align-items: center;
2213
+ box-sizing: border-box;
2214
+ > svg {
2215
+ display: block;
2216
+ width: 70%;
2217
+ height: auto;
2218
+ // Visual balance to center-align the icon
2219
+ transform: translateX(5%);
1956
2220
  }
1957
- if (enableSignedUrls && !await haveValidSigningKeys(
1958
- client,
1959
- signingKeyId,
1960
- signingKeyPrivate
1961
- ))
1962
- try {
1963
- const { data } = await createSigningKeys(client);
1964
- signingKeyId = data.id, signingKeyPrivate = data.private_key, await saveSecrets(
1965
- client,
1966
- token,
1967
- secretKey,
1968
- enableSignedUrls,
1969
- signingKeyId,
1970
- signingKeyPrivate
1971
- );
1972
- } catch (err) {
1973
- throw console.log("Error while creating and saving signing key:", err?.message), err;
1974
- }
1975
- return {
1976
- token,
1977
- secretKey,
1978
- enableSignedUrls,
1979
- signingKeyId,
1980
- signingKeyPrivate
1981
- };
1982
- },
1983
- [client, secrets]
1984
- );
1985
- function init({ token, secretKey, enableSignedUrls }) {
1986
- return {
1987
- submitting: !1,
1988
- error: null,
1989
- // Form inputs don't set the state back to null when clearing a field, but uses empty strings
1990
- // This ensures the `dirty` check works correctly
1991
- token: token ?? "",
1992
- secretKey: secretKey ?? "",
1993
- enableSignedUrls: enableSignedUrls ?? !1
1994
- };
1995
- }
1996
- function reducer(state, action) {
1997
- switch (action?.type) {
1998
- case "submit":
1999
- return { ...state, submitting: !0, error: null };
2000
- case "error":
2001
- return { ...state, submitting: !1, error: action.payload };
2002
- case "reset":
2003
- return init(action.payload);
2004
- case "change":
2005
- return { ...state, [action.payload.name]: action.payload.value };
2006
- default:
2007
- throw new Error(`Unknown action type: ${action?.type}`);
2008
2221
  }
2009
- }
2010
- const useSecretsFormState = (secrets) => React.useReducer(reducer, secrets, init);
2011
- function MuxLogo({ height = 26 }) {
2012
- const id = React.useId(), fillColor = ui.useTheme_v2().color._dark ? "white" : "black", titleId = React.useMemo(() => `${id}-title`, [id]), pathStyle = {
2013
- fillRule: "nonzero"
2014
- };
2015
- return /* @__PURE__ */ jsxRuntime.jsx(
2016
- "svg",
2222
+
2223
+ &:hover,
2224
+ &:focus {
2225
+ &::after {
2226
+ opacity: 0.3;
2227
+ }
2228
+ > div[data-play] {
2229
+ opacity: 1;
2230
+ }
2231
+ }
2232
+ `;
2233
+ function VideoInBrowser({
2234
+ onSelect,
2235
+ onEdit,
2236
+ asset
2237
+ }) {
2238
+ const [renderVideo, setRenderVideo] = React.useState(!1), select = React__default.default.useCallback(() => onSelect?.(asset), [onSelect, asset]), edit = React__default.default.useCallback(() => onEdit?.(asset), [onEdit, asset]);
2239
+ if (!asset)
2240
+ return null;
2241
+ const playbackPolicy = getPlaybackPolicy(asset);
2242
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2243
+ ui.Card,
2017
2244
  {
2018
- "aria-labelledby": titleId,
2019
- style: { height: `${height}px` },
2020
- viewBox: "0 0 1600 500",
2021
- version: "1.1",
2022
- xmlns: "http://www.w3.org/2000/svg",
2023
- xmlSpace: "preserve",
2024
- children: /* @__PURE__ */ jsxRuntime.jsxs("g", { id: "Layer-1", fill: fillColor, children: [
2025
- /* @__PURE__ */ jsxRuntime.jsx(
2026
- "path",
2027
- {
2028
- d: "M994.287,93.486c-17.121,-0 -31,-13.879 -31,-31c0,-17.121 13.879,-31 31,-31c17.121,-0 31,13.879 31,31c0,17.121 -13.879,31 -31,31m0,-93.486c-34.509,-0 -62.484,27.976 -62.484,62.486l0,187.511c0,68.943 -56.09,125.033 -125.032,125.033c-68.942,-0 -125.03,-56.09 -125.03,-125.033l0,-187.511c0,-34.51 -27.976,-62.486 -62.485,-62.486c-34.509,-0 -62.484,27.976 -62.484,62.486l0,187.511c0,137.853 112.149,250.003 249.999,250.003c137.851,-0 250.001,-112.15 250.001,-250.003l0,-187.511c0,-34.51 -27.976,-62.486 -62.485,-62.486",
2029
- style: pathStyle
2030
- }
2031
- ),
2032
- /* @__PURE__ */ jsxRuntime.jsx(
2033
- "path",
2245
+ border: !0,
2246
+ padding: 2,
2247
+ sizing: "border",
2248
+ radius: 2,
2249
+ style: {
2250
+ position: "relative"
2251
+ },
2252
+ children: [
2253
+ playbackPolicy === "signed" && /* @__PURE__ */ jsxRuntime.jsx(
2254
+ ui.Tooltip,
2034
2255
  {
2035
- d: "M1537.51,468.511c-17.121,-0 -31,-13.879 -31,-31c0,-17.121 13.879,-31 31,-31c17.121,-0 31,13.879 31,31c0,17.121 -13.879,31 -31,31m-275.883,-218.509l-143.33,143.329c-24.402,24.402 -24.402,63.966 0,88.368c24.402,24.402 63.967,24.402 88.369,-0l143.33,-143.329l143.328,143.329c24.402,24.4 63.967,24.402 88.369,-0c24.403,-24.402 24.403,-63.966 0.001,-88.368l-143.33,-143.329l0.001,-0.004l143.329,-143.329c24.402,-24.402 24.402,-63.965 0,-88.367c-24.402,-24.402 -63.967,-24.402 -88.369,-0l-143.329,143.328l-143.329,-143.328c-24.402,-24.401 -63.967,-24.402 -88.369,-0c-24.402,24.402 -24.402,63.965 0,88.367l143.329,143.329l0,0.004Z",
2036
- style: pathStyle
2256
+ animate: !0,
2257
+ content: /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 2, radius: 2, children: /* @__PURE__ */ jsxRuntime.jsx(IconInfo, { icon: icons.LockIcon, text: "Signed playback policy", size: 2 }) }),
2258
+ placement: "right",
2259
+ fallbackPlacements: ["top", "bottom"],
2260
+ portal: !0,
2261
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2262
+ ui.Card,
2263
+ {
2264
+ tone: "caution",
2265
+ style: {
2266
+ borderRadius: "100%",
2267
+ position: "absolute",
2268
+ left: "1em",
2269
+ top: "1em",
2270
+ zIndex: 10
2271
+ },
2272
+ padding: 2,
2273
+ border: !0,
2274
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { muted: !0, size: 1, children: /* @__PURE__ */ jsxRuntime.jsx(icons.LockIcon, {}) })
2275
+ }
2276
+ )
2037
2277
  }
2038
2278
  ),
2039
- /* @__PURE__ */ jsxRuntime.jsx(
2040
- "path",
2279
+ /* @__PURE__ */ jsxRuntime.jsxs(
2280
+ ui.Stack,
2041
2281
  {
2042
- d: "M437.511,468.521c-17.121,-0 -31,-13.879 -31,-31c0,-17.121 13.879,-31 31,-31c17.121,-0 31,13.879 31,31c0,17.121 -13.879,31 -31,31m23.915,-463.762c-23.348,-9.672 -50.226,-4.327 -68.096,13.544l-143.331,143.329l-143.33,-143.329c-17.871,-17.871 -44.747,-23.216 -68.096,-13.544c-23.349,9.671 -38.574,32.455 -38.574,57.729l0,375.026c0,34.51 27.977,62.486 62.487,62.486c34.51,-0 62.486,-27.976 62.486,-62.486l0,-224.173l80.843,80.844c24.404,24.402 63.965,24.402 88.369,-0l80.843,-80.844l0,224.173c0,34.51 27.976,62.486 62.486,62.486c34.51,-0 62.486,-27.976 62.486,-62.486l0,-375.026c0,-25.274 -15.224,-48.058 -38.573,-57.729",
2043
- style: pathStyle
2282
+ space: 3,
2283
+ height: "fill",
2284
+ style: {
2285
+ gridTemplateRows: "min-content min-content 1fr"
2286
+ },
2287
+ children: [
2288
+ renderVideo ? /* @__PURE__ */ jsxRuntime.jsx(VideoPlayer, { asset, autoPlay: !0, forceAspectRatio: THUMBNAIL_ASPECT_RATIO }) : /* @__PURE__ */ jsxRuntime.jsxs(PlayButton, { onClick: () => setRenderVideo(!0), children: [
2289
+ /* @__PURE__ */ jsxRuntime.jsx("div", { "data-play": !0, children: /* @__PURE__ */ jsxRuntime.jsx(icons.PlayIcon, {}) }),
2290
+ assetIsAudio(asset) ? /* @__PURE__ */ jsxRuntime.jsx(
2291
+ "div",
2292
+ {
2293
+ style: {
2294
+ aspectRatio: THUMBNAIL_ASPECT_RATIO,
2295
+ display: "flex",
2296
+ alignItems: "center",
2297
+ justifyContent: "center"
2298
+ },
2299
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "3em", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntime.jsx(
2300
+ "path",
2301
+ {
2302
+ fill: "currentColor",
2303
+ style: { opacity: "0.65" },
2304
+ d: "M10.75 19q.95 0 1.6-.65t.65-1.6V13h3v-2h-4v3.875q-.275-.2-.587-.288t-.663-.087q-.95 0-1.6.65t-.65 1.6t.65 1.6t1.6.65M6 22q-.825 0-1.412-.587T4 20V4q0-.825.588-1.412T6 2h8l6 6v12q0 .825-.587 1.413T18 22zm7-13V4H6v16h12V9zM6 4v5zv16z"
2305
+ }
2306
+ ) })
2307
+ }
2308
+ ) : /* @__PURE__ */ jsxRuntime.jsx(VideoThumbnail, { asset })
2309
+ ] }),
2310
+ /* @__PURE__ */ jsxRuntime.jsx(VideoMetadata, { asset }),
2311
+ /* @__PURE__ */ jsxRuntime.jsxs(
2312
+ "div",
2313
+ {
2314
+ style: {
2315
+ display: "flex",
2316
+ width: "100%",
2317
+ alignItems: "flex-end",
2318
+ justifyContent: "flex-start",
2319
+ gap: ".35rem"
2320
+ },
2321
+ children: [
2322
+ onSelect && /* @__PURE__ */ jsxRuntime.jsx(
2323
+ ui.Button,
2324
+ {
2325
+ icon: icons.CheckmarkIcon,
2326
+ fontSize: 2,
2327
+ padding: 2,
2328
+ mode: "ghost",
2329
+ text: "Select",
2330
+ style: { flex: 1 },
2331
+ tone: "positive",
2332
+ onClick: select
2333
+ }
2334
+ ),
2335
+ /* @__PURE__ */ jsxRuntime.jsx(
2336
+ ui.Button,
2337
+ {
2338
+ icon: icons.EditIcon,
2339
+ fontSize: 2,
2340
+ padding: 2,
2341
+ mode: "ghost",
2342
+ text: "Details",
2343
+ style: { flex: 1 },
2344
+ onClick: edit
2345
+ }
2346
+ )
2347
+ ]
2348
+ }
2349
+ )
2350
+ ]
2044
2351
  }
2045
2352
  )
2046
- ] })
2353
+ ]
2047
2354
  }
2048
2355
  );
2049
2356
  }
2050
- const Logo = styledComponents.styled.span`
2051
- display: inline-block;
2052
- height: 0.8em;
2053
- margin-right: 1em;
2054
- transform: translate(0.3em, -0.2em);
2055
- `, Header = () => /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2056
- /* @__PURE__ */ jsxRuntime.jsx(Logo, { children: /* @__PURE__ */ jsxRuntime.jsx(MuxLogo, { height: 13 }) }),
2057
- "API Credentials"
2058
- ] }), fieldNames = ["token", "secretKey", "enableSignedUrls"];
2059
- function ConfigureApi({ secrets, setDialogState }) {
2060
- const client = useClient(), [state, dispatch] = useSecretsFormState(secrets), hasSecretsInitially = React.useMemo(() => secrets.token && secrets.secretKey, [secrets]), handleClose = React.useCallback(() => setDialogState(!1), [setDialogState]), dirty = React.useMemo(
2061
- () => secrets.token !== state.token || secrets.secretKey !== state.secretKey || secrets.enableSignedUrls !== state.enableSignedUrls,
2062
- [secrets, state]
2063
- ), id = `ConfigureApi${React.useId()}`, [tokenId, secretKeyId, enableSignedUrlsId] = React.useMemo(
2064
- () => fieldNames.map((field) => `${id}-${field}`),
2065
- [id]
2066
- ), firstField = React.useRef(null), handleSaveSecrets = useSaveSecrets(client, secrets), saving = React.useRef(!1), handleSubmit = React.useCallback(
2067
- (event) => {
2068
- if (event.preventDefault(), !saving.current && event.currentTarget.reportValidity()) {
2069
- saving.current = !0, dispatch({ type: "submit" });
2070
- const { token, secretKey, enableSignedUrls } = state;
2071
- handleSaveSecrets({ token, secretKey, enableSignedUrls }).then((savedSecrets) => {
2072
- const { projectId, dataset } = client.config();
2073
- suspendReact.clear([cacheNs, _id, projectId, dataset]), suspendReact.preload(() => Promise.resolve(savedSecrets), [cacheNs, _id, projectId, dataset]), setDialogState(!1);
2074
- }).catch((err) => dispatch({ type: "error", payload: err.message })).finally(() => {
2075
- saving.current = !1;
2076
- });
2077
- }
2078
- },
2079
- [client, dispatch, handleSaveSecrets, setDialogState, state]
2080
- ), handleChangeToken = React.useCallback(
2081
- (event) => {
2082
- dispatch({
2083
- type: "change",
2084
- payload: { name: "token", value: event.currentTarget.value }
2085
- });
2086
- },
2087
- [dispatch]
2088
- ), handleChangeSecretKey = React.useCallback(
2089
- (event) => {
2090
- dispatch({
2091
- type: "change",
2092
- payload: { name: "secretKey", value: event.currentTarget.value }
2093
- });
2094
- },
2095
- [dispatch]
2096
- ), handleChangeEnableSignedUrls = React.useCallback(
2097
- (event) => {
2098
- dispatch({
2099
- type: "change",
2100
- payload: { name: "enableSignedUrls", value: event.currentTarget.checked }
2101
- });
2102
- },
2103
- [dispatch]
2357
+ function VideosBrowser({ onSelect }) {
2358
+ const { assets, isLoading, searchQuery, setSearchQuery, setSort, sort } = useAssets(), [editedAsset, setEditedAsset] = React.useState(null), freshEditedAsset = React.useMemo(
2359
+ () => assets.find((a2) => a2._id === editedAsset?._id) || editedAsset,
2360
+ [editedAsset, assets]
2104
2361
  );
2105
- return React.useEffect(() => {
2106
- firstField.current && firstField.current.focus();
2107
- }, [firstField]), /* @__PURE__ */ jsxRuntime.jsx(
2108
- ui.Dialog,
2109
- {
2110
- animate: !0,
2111
- id,
2112
- onClose: handleClose,
2113
- header: /* @__PURE__ */ jsxRuntime.jsx(Header, {}),
2114
- width: 1,
2115
- style: {
2116
- maxWidth: "550px"
2117
- },
2118
- children: /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { padding: 4, style: { position: "relative" }, children: /* @__PURE__ */ jsxRuntime.jsx("form", { onSubmit: handleSubmit, noValidate: !0, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, children: [
2119
- !hasSecretsInitially && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: [3, 3, 3], radius: 2, shadow: 1, tone: "primary", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
2120
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, children: [
2121
- "To set up a new access token, go to your",
2122
- " ",
2123
- /* @__PURE__ */ jsxRuntime.jsx(
2124
- "a",
2125
- {
2126
- href: "https://dashboard.mux.com/settings/access-tokens",
2127
- target: "_blank",
2128
- rel: "noreferrer noopener",
2129
- children: "account on mux.com"
2130
- }
2131
- ),
2132
- "."
2133
- ] }),
2134
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, children: [
2135
- "The access token needs permissions: ",
2136
- /* @__PURE__ */ jsxRuntime.jsx("strong", { children: "Mux Video " }),
2137
- "(Full Access) and ",
2138
- /* @__PURE__ */ jsxRuntime.jsx("strong", { children: "Mux Data" }),
2139
- " (Read)",
2140
- /* @__PURE__ */ jsxRuntime.jsx("br", {}),
2141
- "To use Signed URLs, the token must also have System permissions.",
2142
- /* @__PURE__ */ jsxRuntime.jsx("br", {}),
2143
- "The credentials will be stored safely in a hidden document only available to editors."
2144
- ] })
2145
- ] }) }),
2146
- /* @__PURE__ */ jsxRuntime.jsx(FormField$1, { title: "Access Token", inputId: tokenId, children: /* @__PURE__ */ jsxRuntime.jsx(
2147
- ui.TextInput,
2148
- {
2149
- id: tokenId,
2150
- ref: firstField,
2151
- onChange: handleChangeToken,
2152
- type: "text",
2153
- value: state.token ?? "",
2154
- required: !!state.secretKey || state.enableSignedUrls
2155
- }
2156
- ) }),
2157
- /* @__PURE__ */ jsxRuntime.jsx(FormField$1, { title: "Secret Key", inputId: secretKeyId, children: /* @__PURE__ */ jsxRuntime.jsx(
2158
- ui.TextInput,
2159
- {
2160
- id: secretKeyId,
2161
- onChange: handleChangeSecretKey,
2162
- type: "text",
2163
- value: state.secretKey ?? "",
2164
- required: !!state.token || state.enableSignedUrls
2165
- }
2166
- ) }),
2167
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, children: [
2168
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", children: [
2169
- /* @__PURE__ */ jsxRuntime.jsx(
2170
- ui.Checkbox,
2171
- {
2172
- id: enableSignedUrlsId,
2173
- onChange: handleChangeEnableSignedUrls,
2174
- checked: state.enableSignedUrls,
2175
- style: { display: "block" }
2176
- }
2177
- ),
2178
- /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { flex: 1, paddingLeft: 3, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: enableSignedUrlsId, children: "Enable Signed Urls" }) }) })
2179
- ] }),
2180
- secrets.signingKeyId && state.enableSignedUrls ? /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: [3, 3, 3], radius: 2, shadow: 1, tone: "caution", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
2181
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, children: "The signing key ID that Sanity will use is:" }),
2182
- /* @__PURE__ */ jsxRuntime.jsx(ui.Code, { size: 1, children: secrets.signingKeyId }),
2183
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, children: [
2184
- "This key is only used for previewing content in the Sanity UI.",
2185
- /* @__PURE__ */ jsxRuntime.jsx("br", {}),
2186
- "You should generate a different key to use in your application server."
2187
- ] })
2188
- ] }) }) : null
2189
- ] }),
2190
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Inline, { space: 2, children: [
2362
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2363
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { padding: 4, space: 4, style: { minHeight: "50vh" }, children: [
2364
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "space-between", align: "center", children: [
2365
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 3, children: [
2191
2366
  /* @__PURE__ */ jsxRuntime.jsx(
2192
- ui.Button,
2367
+ ui.TextInput,
2193
2368
  {
2194
- text: "Save",
2195
- disabled: !dirty,
2196
- loading: state.submitting,
2197
- tone: "primary",
2198
- mode: "default",
2199
- type: "submit"
2369
+ value: searchQuery,
2370
+ icon: icons.SearchIcon,
2371
+ onInput: (e) => setSearchQuery(e.currentTarget.value),
2372
+ placeholder: "Search videos"
2200
2373
  }
2201
2374
  ),
2202
- /* @__PURE__ */ jsxRuntime.jsx(
2203
- ui.Button,
2204
- {
2205
- disabled: state.submitting,
2206
- text: "Cancel",
2207
- mode: "bleed",
2208
- onClick: handleClose
2209
- }
2210
- )
2375
+ /* @__PURE__ */ jsxRuntime.jsx(SelectSortOptions, { setSort, sort })
2211
2376
  ] }),
2212
- state.error && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: [3, 3, 3], radius: 2, shadow: 1, tone: "critical", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: state.error }) })
2213
- ] }) }) })
2214
- }
2215
- );
2377
+ (onSelect ? "input" : "tool") == "tool" && /* @__PURE__ */ jsxRuntime.jsxs(ui.Inline, { space: 2, children: [
2378
+ /* @__PURE__ */ jsxRuntime.jsx(ImportVideosFromMux, {}),
2379
+ /* @__PURE__ */ jsxRuntime.jsx(ResyncMetadata, {}),
2380
+ /* @__PURE__ */ jsxRuntime.jsx(ConfigureApi, {})
2381
+ ] })
2382
+ ] }),
2383
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
2384
+ assets?.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ui.Label, { muted: !0, children: [
2385
+ assets.length,
2386
+ " video",
2387
+ assets.length > 1 ? "s" : null,
2388
+ " ",
2389
+ searchQuery ? `matching "${searchQuery}"` : "found"
2390
+ ] }),
2391
+ /* @__PURE__ */ jsxRuntime.jsx(
2392
+ ui.Grid,
2393
+ {
2394
+ gap: 2,
2395
+ style: {
2396
+ gridTemplateColumns: "repeat(auto-fill, minmax(250px, 1fr))"
2397
+ },
2398
+ children: assets.map((asset) => /* @__PURE__ */ jsxRuntime.jsx(
2399
+ VideoInBrowser,
2400
+ {
2401
+ asset,
2402
+ onEdit: setEditedAsset,
2403
+ onSelect
2404
+ },
2405
+ asset._id
2406
+ ))
2407
+ }
2408
+ )
2409
+ ] }),
2410
+ isLoading && /* @__PURE__ */ jsxRuntime.jsx(SpinnerBox, {}),
2411
+ !isLoading && assets.length === 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { marginY: 4, paddingX: 4, paddingY: 6, border: !0, radius: 2, tone: "transparent", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { align: "center", muted: !0, size: 3, children: searchQuery ? `No videos found for "${searchQuery}"` : "No videos in this dataset" }) })
2412
+ ] }),
2413
+ freshEditedAsset && /* @__PURE__ */ jsxRuntime.jsx(VideoDetails, { closeDialog: () => setEditedAsset(null), asset: freshEditedAsset })
2414
+ ] });
2415
+ }
2416
+ const StudioTool = () => /* @__PURE__ */ jsxRuntime.jsx(VideosBrowser, {}), DEFAULT_TOOL_CONFIG = {
2417
+ icon: ToolIcon,
2418
+ title: "Videos"
2419
+ };
2420
+ function createStudioTool(config) {
2421
+ const toolConfig = typeof config.tool == "object" ? config.tool : DEFAULT_TOOL_CONFIG;
2422
+ return {
2423
+ name: "mux",
2424
+ icon: toolConfig.icon || DEFAULT_TOOL_CONFIG.icon,
2425
+ title: toolConfig.title || DEFAULT_TOOL_CONFIG.title,
2426
+ component: (props) => /* @__PURE__ */ jsxRuntime.jsx(StudioTool, { ...config, ...props })
2427
+ };
2216
2428
  }
2217
- var ConfigureApi$1 = React.memo(ConfigureApi), c = function(r) {
2429
+ const useAccessControl = (config) => {
2430
+ const user = sanity.useCurrentUser();
2431
+ return { hasConfigAccess: !config?.allowedRolesForConfiguration?.length || user?.roles?.some((role) => config.allowedRolesForConfiguration.includes(role.name)) };
2432
+ }, path = ["assetId", "data", "playbackId", "status", "thumbTime", "filename"], useAssetDocumentValues = (asset) => sanity.useDocumentValues(
2433
+ sanity.isReference(asset) ? asset._ref : "",
2434
+ path
2435
+ ), useMuxPolling = (asset) => {
2436
+ const client = useClient(), projectId = sanity.useProjectId(), dataset = sanity.useDataset(), shouldFetch = React.useMemo(
2437
+ () => !!asset?.assetId && (asset?.status === "preparing" || asset?.data?.static_renditions?.status === "preparing"),
2438
+ [asset?.assetId, asset?.data?.static_renditions?.status, asset?.status]
2439
+ );
2440
+ return useSWR__default.default(
2441
+ shouldFetch ? `/${projectId}/addons/mux/assets/${dataset}/data/${asset?.assetId}` : null,
2442
+ async () => {
2443
+ const { data } = await client.request({
2444
+ url: `/addons/mux/assets/${dataset}/data/${asset.assetId}`,
2445
+ withCredentials: !0,
2446
+ method: "GET"
2447
+ });
2448
+ client.patch(asset._id).set({ status: data.status, data }).commit({ returnDocuments: !1 });
2449
+ },
2450
+ { refreshInterval: 2e3, refreshWhenHidden: !0, dedupingInterval: 1e3 }
2451
+ );
2452
+ };
2453
+ var c = function(r) {
2218
2454
  var t, e;
2219
2455
  function n(t2) {
2220
2456
  var e2;
@@ -2308,10 +2544,7 @@ const InputFallback = () => /* @__PURE__ */ jsxRuntime.jsx("div", { style: { pad
2308
2544
  /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { marginTop: 3, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { align: "center", muted: !0, size: 1, children: "Loading\u2026" }) })
2309
2545
  ] })
2310
2546
  }
2311
- ) }), useAccessControl = (config) => {
2312
- const user = sanity.useCurrentUser();
2313
- return { hasConfigAccess: !config?.allowedRolesForConfiguration?.length || user?.roles?.some((role) => config.allowedRolesForConfiguration.includes(role.name)) };
2314
- };
2547
+ ) });
2315
2548
  function Onboard(props) {
2316
2549
  const { setDialogState } = props, handleOpen = React.useCallback(() => setDialogState("secrets"), [setDialogState]), { hasConfigAccess } = useAccessControl(props.config);
2317
2550
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: 2 }, children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -3099,9 +3332,10 @@ function PlaybackPolicy({
3099
3332
  noPolicySelected && /* @__PURE__ */ jsxRuntime.jsx(PlaybackPolicyWarning, {})
3100
3333
  ] });
3101
3334
  }
3102
- const ENCODING_OPTIONS = [
3103
- { value: "smart", label: "Smart" },
3104
- { value: "baseline", label: "Baseline" }
3335
+ const VIDEO_QUALITY_LEVELS = [
3336
+ { value: "basic", label: "Basic" },
3337
+ { value: "plus", label: "Plus" },
3338
+ { value: "premium", label: "Premium" }
3105
3339
  ], RESOLUTION_TIERS = [
3106
3340
  { value: "1080p", label: "1080p" },
3107
3341
  { value: "1440p", label: "1440p (2k)" },
@@ -3115,7 +3349,7 @@ function UploadConfiguration({
3115
3349
  onClose
3116
3350
  }) {
3117
3351
  const id = React.useId(), autoTextTracks = React.useRef(
3118
- pluginConfig.encoding_tier === "smart" && pluginConfig.defaultAutogeneratedSubtitleLang ? [
3352
+ pluginConfig.video_quality === "plus" && pluginConfig.defaultAutogeneratedSubtitleLang ? [
3119
3353
  {
3120
3354
  _id: uuid.uuid(),
3121
3355
  type: "autogenerated",
@@ -3126,16 +3360,16 @@ function UploadConfiguration({
3126
3360
  ).current, [config, dispatch] = React.useReducer(
3127
3361
  (prev, action) => {
3128
3362
  switch (action.action) {
3129
- case "encoding_tier":
3130
- return action.value === "baseline" ? Object.assign({}, prev, {
3131
- encoding_tier: action.value,
3363
+ case "video_quality":
3364
+ return action.value === "basic" ? Object.assign({}, prev, {
3365
+ video_quality: action.value,
3132
3366
  mp4_support: "none",
3133
3367
  max_resolution_tier: "1080p",
3134
3368
  text_tracks: prev.text_tracks?.filter(({ type }) => type !== "autogenerated"),
3135
3369
  public_policy: !0,
3136
3370
  signed_policy: !1
3137
3371
  }) : Object.assign({}, prev, {
3138
- encoding_tier: action.value,
3372
+ video_quality: action.value,
3139
3373
  mp4_support: pluginConfig.mp4_support,
3140
3374
  max_resolution_tier: pluginConfig.max_resolution_tier,
3141
3375
  text_tracks: [...autoTextTracks, ...prev.text_tracks || []]
@@ -3177,7 +3411,7 @@ function UploadConfiguration({
3177
3411
  }
3178
3412
  },
3179
3413
  {
3180
- encoding_tier: pluginConfig.encoding_tier,
3414
+ video_quality: pluginConfig.video_quality,
3181
3415
  max_resolution_tier: pluginConfig.max_resolution_tier,
3182
3416
  mp4_support: pluginConfig.mp4_support,
3183
3417
  signed_policy: secrets.enableSignedUrls && pluginConfig.defaultSigned,
@@ -3189,7 +3423,7 @@ function UploadConfiguration({
3189
3423
  if (React.useEffect(() => {
3190
3424
  skipConfig && startUpload(formatUploadConfig(config));
3191
3425
  }, []), skipConfig) return null;
3192
- const maxSupportedResolution = RESOLUTION_TIERS.findIndex(
3426
+ const basicConfig = config.video_quality !== "plus" && config.video_quality !== "premium", maxSupportedResolution = RESOLUTION_TIERS.findIndex(
3193
3427
  (rt) => rt.value === pluginConfig.max_resolution_tier
3194
3428
  );
3195
3429
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -3225,9 +3459,9 @@ function UploadConfiguration({
3225
3459
  /* @__PURE__ */ jsxRuntime.jsx(
3226
3460
  sanity.FormField,
3227
3461
  {
3228
- title: "Encoding Tier",
3462
+ title: "Video Quality Level",
3229
3463
  description: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3230
- "The encoding tier informs the cost, quality, and available platform features for the asset.",
3464
+ "The video quality level informs the cost, quality, and available platform features for the asset.",
3231
3465
  " ",
3232
3466
  /* @__PURE__ */ jsxRuntime.jsx(
3233
3467
  "a",
@@ -3239,16 +3473,16 @@ function UploadConfiguration({
3239
3473
  }
3240
3474
  )
3241
3475
  ] }),
3242
- children: /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { gap: 3, children: ENCODING_OPTIONS.map(({ value, label }) => {
3476
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { gap: 3, children: VIDEO_QUALITY_LEVELS.map(({ value, label }) => {
3243
3477
  const inputId = `${id}--encodingtier-${value}`;
3244
3478
  return /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
3245
3479
  /* @__PURE__ */ jsxRuntime.jsx(
3246
3480
  ui.Radio,
3247
3481
  {
3248
- checked: config.encoding_tier === value,
3482
+ checked: config.video_quality === value,
3249
3483
  name: "asset-encodingtier",
3250
3484
  onChange: (e) => dispatch({
3251
- action: "encoding_tier",
3485
+ action: "video_quality",
3252
3486
  value: e.currentTarget.value
3253
3487
  }),
3254
3488
  value,
@@ -3260,7 +3494,7 @@ function UploadConfiguration({
3260
3494
  }) })
3261
3495
  }
3262
3496
  ),
3263
- config.encoding_tier === "smart" && maxSupportedResolution > 0 && /* @__PURE__ */ jsxRuntime.jsx(
3497
+ !basicConfig && maxSupportedResolution > 0 && /* @__PURE__ */ jsxRuntime.jsx(
3264
3498
  sanity.FormField,
3265
3499
  {
3266
3500
  title: "Resolution Tier",
@@ -3300,9 +3534,9 @@ function UploadConfiguration({
3300
3534
  }) })
3301
3535
  }
3302
3536
  ),
3303
- config.encoding_tier === "smart" && /* @__PURE__ */ jsxRuntime.jsx(sanity.FormField, { title: "Additional Configuration", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
3537
+ !basicConfig && /* @__PURE__ */ jsxRuntime.jsx(sanity.FormField, { title: "Additional Configuration", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
3304
3538
  /* @__PURE__ */ jsxRuntime.jsx(PlaybackPolicy, { id, config, secrets, dispatch }),
3305
- config.encoding_tier === "smart" && /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, padding: [0, 2], children: [
3539
+ !basicConfig && /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, padding: [0, 2], children: [
3306
3540
  /* @__PURE__ */ jsxRuntime.jsx(
3307
3541
  ui.Checkbox,
3308
3542
  {
@@ -3321,7 +3555,7 @@ function UploadConfiguration({
3321
3555
  ] })
3322
3556
  ] }) })
3323
3557
  ] }),
3324
- !disableTextTrackConfig && config.encoding_tier === "smart" && /* @__PURE__ */ jsxRuntime.jsx(
3558
+ !disableTextTrackConfig && !basicConfig && /* @__PURE__ */ jsxRuntime.jsx(
3325
3559
  TextTracksEditor,
3326
3560
  {
3327
3561
  tracks: config.text_tracks,
@@ -3332,7 +3566,7 @@ function UploadConfiguration({
3332
3566
  /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { marginTop: 4, children: /* @__PURE__ */ jsxRuntime.jsx(
3333
3567
  ui.Button,
3334
3568
  {
3335
- disabled: config.encoding_tier === "smart" && !config.public_policy && !config.signed_policy,
3569
+ disabled: !basicConfig && !config.public_policy && !config.signed_policy,
3336
3570
  icon: icons.UploadIcon,
3337
3571
  text: "Upload",
3338
3572
  tone: "positive",
@@ -3373,7 +3607,7 @@ function formatUploadConfig(config) {
3373
3607
  mp4_support: config.mp4_support,
3374
3608
  playback_policy: setPlaybackPolicy(config),
3375
3609
  max_resolution_tier: config.max_resolution_tier,
3376
- encoding_tier: config.encoding_tier,
3610
+ video_quality: config.video_quality,
3377
3611
  normalize_audio: config.normalize_audio
3378
3612
  };
3379
3613
  }
@@ -3790,7 +4024,7 @@ const Input = (props) => {
3790
4024
  }
3791
4025
  ),
3792
4026
  dialogState === "secrets" && hasConfigAccess && /* @__PURE__ */ jsxRuntime.jsx(
3793
- ConfigureApi$1,
4027
+ ConfigureApiDialog,
3794
4028
  {
3795
4029
  setDialogState,
3796
4030
  secrets: secretDocumentValues.value.secrets
@@ -3914,6 +4148,10 @@ const muxVideoSchema = {
3914
4148
  type: "string",
3915
4149
  name: "encoding_tier"
3916
4150
  },
4151
+ {
4152
+ type: "string",
4153
+ name: "video_quality"
4154
+ },
3917
4155
  {
3918
4156
  type: "string",
3919
4157
  name: "master_access"
@@ -3992,13 +4230,17 @@ const muxVideoSchema = {
3992
4230
  muxVideoAsset
3993
4231
  ], defaultConfig = {
3994
4232
  mp4_support: "none",
3995
- encoding_tier: "smart",
4233
+ video_quality: "plus",
3996
4234
  max_resolution_tier: "1080p",
3997
4235
  normalize_audio: !1,
3998
4236
  defaultSigned: !1,
3999
4237
  tool: DEFAULT_TOOL_CONFIG,
4000
4238
  allowedRolesForConfiguration: []
4001
4239
  }, muxInput = sanity.definePlugin((userConfig) => {
4240
+ if (typeof userConfig == "object" && "encoding_tier" in userConfig) {
4241
+ const deprecated_encoding_tier = userConfig.encoding_tier;
4242
+ userConfig.video_quality || (deprecated_encoding_tier === "baseline" && (userConfig.video_quality = "basic"), deprecated_encoding_tier === "smart" && (userConfig.video_quality = "plus"));
4243
+ }
4002
4244
  const config = { ...defaultConfig, ...userConfig || {} };
4003
4245
  return {
4004
4246
  name: "mux-input",