sanity-plugin-mux-input 2.9.0 → 2.10.0

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