sanity-plugin-r2-video 0.1.8 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/studio/index.js +397 -243
  2. package/package.json +1 -1
@@ -1,11 +1,11 @@
1
1
  import { resolveRenditionPath } from '../chunk-D5VS4ED7.js';
2
2
  export { resolveRenditionPath } from '../chunk-D5VS4ED7.js';
3
- import { PlayIcon } from '@sanity/icons/Play';
4
- import { defineType, defineField, definePlugin, useClient, set } from 'sanity';
5
- import { createContext, useState, useCallback, useEffect, useRef, useMemo, useContext, useId } from 'react';
3
+ import { createContext, useState, useCallback, useEffect, useRef, useMemo, useId, useContext } from 'react';
4
+ import { defineType, defineField, definePlugin, set, useClient } from 'sanity';
6
5
  import { jsx, jsxs } from 'react/jsx-runtime';
6
+ import { PlayIcon } from '@sanity/icons/Play';
7
7
  import { UploadIcon } from '@sanity/icons/Upload';
8
- import { Stack, Flex, Box, Button, TextInput, Spinner, Text, Card, Grid, Dialog, Select, Switch } from '@sanity/ui';
8
+ import { Box, Stack, Flex, Button, TextInput, Card, Text, Select, Grid, Checkbox, Dialog, Spinner, Switch } from '@sanity/ui';
9
9
  import { AccessDeniedIcon } from '@sanity/icons/AccessDenied';
10
10
  import { canEncodeVideo } from 'mediabunny';
11
11
  import { WarningOutlineIcon } from '@sanity/icons/WarningOutline';
@@ -14,6 +14,7 @@ import { ChevronRightIcon } from '@sanity/icons/ChevronRight';
14
14
  import { SearchIcon } from '@sanity/icons/Search';
15
15
  import { SyncIcon } from '@sanity/icons/Sync';
16
16
  import { TrashIcon } from '@sanity/icons/Trash';
17
+ import styled from 'styled-components';
17
18
  import { FolderIcon } from '@sanity/icons/Folder';
18
19
 
19
20
  // studio/client.ts
@@ -107,6 +108,24 @@ var deleteVideoAsset = async (client, config, asset) => {
107
108
  renditions.map((rendition) => rendition.key)
108
109
  );
109
110
  };
111
+ var R2VideoConfigContext = createContext(null);
112
+ var R2VideoConfigProvider = ({ config, children }) => {
113
+ return /* @__PURE__ */ jsx(R2VideoConfigContext.Provider, { value: config, children });
114
+ };
115
+ var useR2VideoConfig = () => {
116
+ const config = useContext(R2VideoConfigContext);
117
+ if (!config) {
118
+ throw new Error(
119
+ "Missing R2 video config. Add `r2Video` to the Studio's plugin list."
120
+ );
121
+ }
122
+ return config;
123
+ };
124
+ var useR2VideoClient = () => {
125
+ const config = useR2VideoConfig();
126
+ const client = useClient({ apiVersion: config.apiVersion });
127
+ return { config, client };
128
+ };
110
129
 
111
130
  // studio/folders.ts
112
131
  var QUERY_FOLDERS = `*[_type == $folderType]{
@@ -150,18 +169,13 @@ var resolvePosterFolder = async (client, folderType, name) => {
150
169
  const created = await client.create({ _type: folderType, name });
151
170
  return created._id;
152
171
  };
153
- var R2VideoConfigContext = createContext(null);
154
- var R2VideoConfigProvider = ({ config, children }) => {
155
- return /* @__PURE__ */ jsx(R2VideoConfigContext.Provider, { value: config, children });
156
- };
157
- var useR2VideoConfig = () => {
158
- const config = useContext(R2VideoConfigContext);
159
- if (!config) {
160
- throw new Error(
161
- "Missing R2 video config. Add `r2Video` to the Studio's plugin list."
162
- );
163
- }
164
- return config;
172
+ var useFolders = () => {
173
+ const { config, client } = useR2VideoClient();
174
+ const [folders, setFolders] = useState([]);
175
+ useEffect(() => {
176
+ fetchFolders(client, config.folders.type).then(setFolders);
177
+ }, [client, config.folders.type]);
178
+ return { folders, paths: resolveFolderPaths(folders) };
165
179
  };
166
180
 
167
181
  // studio/defaults.ts
@@ -204,6 +218,15 @@ var formatSize = (bytes) => {
204
218
  var formatDuration = (seconds) => {
205
219
  return `${seconds.toFixed(1)}s`;
206
220
  };
221
+ var totalSize = (renditions) => {
222
+ return renditions.reduce((total, rendition) => total + rendition.size, 0);
223
+ };
224
+ var pluralize = (count, noun) => {
225
+ return `${count} ${noun}${count === 1 ? "" : "s"}`;
226
+ };
227
+ var toMessage = (error) => {
228
+ return error instanceof Error ? error.message : String(error);
229
+ };
207
230
  var formatBitrate = (bytes, seconds) => {
208
231
  if (!seconds) {
209
232
  return "-";
@@ -333,6 +356,57 @@ var transcodeVideo = (request, progressed) => {
333
356
  worker.postMessage(request);
334
357
  });
335
358
  };
359
+ var Notice = ({ tone, title, icon, children }) => {
360
+ return /* @__PURE__ */ jsx(Card, { padding: 4, radius: 2, tone, children: /* @__PURE__ */ jsxs(Flex, { align: "flex-start", gap: 3, children: [
361
+ icon && /* @__PURE__ */ jsx(Text, { size: 2, children: icon }),
362
+ /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
363
+ title && /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: title }),
364
+ typeof children === "string" ? /* @__PURE__ */ jsx(Text, { muted: Boolean(title), size: 1, children }) : children
365
+ ] })
366
+ ] }) });
367
+ };
368
+ var Field = ({ label, id, description, children }) => {
369
+ return /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
370
+ /* @__PURE__ */ jsx(Text, { as: id ? "label" : void 0, htmlFor: id, size: 1, weight: "medium", children: label }),
371
+ children,
372
+ description && /* @__PURE__ */ jsx(Text, { muted: true, size: 0, children: description })
373
+ ] });
374
+ };
375
+ var DialogActions = ({
376
+ aside,
377
+ cancel,
378
+ confirm
379
+ }) => {
380
+ return /* @__PURE__ */ jsx(Card, { borderTop: true, padding: 2, children: /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
381
+ aside,
382
+ /* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(
383
+ Button,
384
+ {
385
+ disabled: cancel.disabled,
386
+ mode: "ghost",
387
+ text: cancel.text,
388
+ width: "fill",
389
+ onClick: cancel.onClick
390
+ }
391
+ ) }),
392
+ confirm && /* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(
393
+ Button,
394
+ {
395
+ disabled: confirm.disabled,
396
+ text: confirm.text,
397
+ tone: confirm.tone,
398
+ width: "fill",
399
+ onClick: confirm.onClick
400
+ }
401
+ ) })
402
+ ] }) });
403
+ };
404
+ var Loading = ({ children }) => {
405
+ return /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 3, children: [
406
+ /* @__PURE__ */ jsx(Spinner, { muted: true }),
407
+ /* @__PURE__ */ jsx(Text, { muted: true, size: 1, children })
408
+ ] });
409
+ };
336
410
  var PreviewEncode = ({ file, keepAudio, encoding }) => {
337
411
  const [preview, setPreview] = useState(null);
338
412
  const [isEncoding, setIsEncoding] = useState(false);
@@ -374,7 +448,7 @@ var PreviewEncode = ({ file, keepAudio, encoding }) => {
374
448
  duration: result.duration
375
449
  });
376
450
  } catch (caught) {
377
- setError(caught instanceof Error ? caught.message : String(caught));
451
+ setError(toMessage(caught));
378
452
  }
379
453
  setIsEncoding(false);
380
454
  };
@@ -394,15 +468,12 @@ var PreviewEncode = ({ file, keepAudio, encoding }) => {
394
468
  }
395
469
  )
396
470
  ] }),
397
- isEncoding && /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 3, children: [
398
- /* @__PURE__ */ jsx(Spinner, { muted: true }),
399
- /* @__PURE__ */ jsxs(Text, { muted: true, size: 1, children: [
400
- "Encoding top tier - ",
401
- Math.round(progress * 100),
402
- "%"
403
- ] })
471
+ isEncoding && /* @__PURE__ */ jsxs(Loading, { children: [
472
+ "Encoding top tier - ",
473
+ Math.round(progress * 100),
474
+ "%"
404
475
  ] }),
405
- error && /* @__PURE__ */ jsx(Card, { padding: 3, radius: 2, tone: "critical", children: /* @__PURE__ */ jsx(Text, { size: 1, children: error }) }),
476
+ error && /* @__PURE__ */ jsx(Notice, { tone: "critical", children: error }),
406
477
  preview && !isEncoding && /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
407
478
  /* @__PURE__ */ jsx(
408
479
  "video",
@@ -439,20 +510,15 @@ var PreviewEncode = ({ file, keepAudio, encoding }) => {
439
510
  ] }) });
440
511
  };
441
512
  var UnsupportedBrowser = () => {
442
- return /* @__PURE__ */ jsx(Card, { padding: 4, radius: 2, tone: "caution", children: /* @__PURE__ */ jsxs(Flex, { align: "flex-start", gap: 3, children: [
443
- /* @__PURE__ */ jsx(Text, { size: 2, children: /* @__PURE__ */ jsx(WarningOutlineIcon, {}) }),
444
- /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
445
- /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: "This browser can't encode video" }),
446
- /* @__PURE__ */ jsx(Text, { muted: true, size: 1, children: "Uploading needs WebCodecs h264 encoding, which this browser doesn't provide. Open the Studio in Chrome to add videos. Everything already in the library still works here." })
447
- ] })
448
- ] }) });
449
- };
450
- var Field = ({ id, label, description, children }) => {
451
- return /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
452
- /* @__PURE__ */ jsx(Text, { as: "label", htmlFor: id, size: 1, weight: "medium", children: label }),
453
- children,
454
- /* @__PURE__ */ jsx(Text, { muted: true, size: 0, children: description })
455
- ] });
513
+ return /* @__PURE__ */ jsx(
514
+ Notice,
515
+ {
516
+ icon: /* @__PURE__ */ jsx(WarningOutlineIcon, {}),
517
+ title: "This browser can't encode video",
518
+ tone: "caution",
519
+ children: "Uploading needs WebCodecs h264 encoding, which this browser doesn't provide. Open the Studio in Chrome to add videos. Everything already in the library still works here."
520
+ }
521
+ );
456
522
  };
457
523
  var ToggleRow = ({ id, label, description, children }) => {
458
524
  return /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 3, children: [
@@ -702,9 +768,6 @@ var describe = (item) => {
702
768
  const { stage, progress, label } = item.progress;
703
769
  return `${STAGE_LABELS[stage]} ${label} \xB7 ${Math.round(progress * 100)}%`;
704
770
  };
705
- var countLabel = (count) => {
706
- return count === 1 ? "1 video" : `${count} video(s)`;
707
- };
708
771
  var toItem = (file) => ({
709
772
  id: crypto.randomUUID(),
710
773
  file,
@@ -718,10 +781,9 @@ var DialogUpload = ({
718
781
  onUploaded,
719
782
  onClose
720
783
  }) => {
721
- const config = useR2VideoConfig();
722
- const client = useClient({ apiVersion: config.apiVersion });
784
+ const { config, client } = useR2VideoClient();
785
+ const { paths } = useFolders();
723
786
  const [canEncode, setCanEncode] = useState(null);
724
- const [folders, setFolders] = useState([]);
725
787
  const [targetFolder, setTargetFolder] = useState(folderId);
726
788
  const [keepAudio, setKeepAudio] = useState(false);
727
789
  const [quality, setQuality] = useState(config.encoding.quality);
@@ -736,13 +798,9 @@ var DialogUpload = ({
736
798
  useEffect(() => {
737
799
  canEncodeLadder(config.encoding.videoCodec).then(setCanEncode);
738
800
  }, [config.encoding.videoCodec]);
739
- useEffect(() => {
740
- fetchFolders(client, config.folders.type).then(setFolders);
741
- }, [client, config.folders.type]);
742
801
  const encoding = useMemo(() => {
743
802
  return { ...config.encoding, quality, preferBitrate };
744
803
  }, [config.encoding, quality, preferBitrate]);
745
- const paths = resolveFolderPaths(folders);
746
804
  const pending = items.filter((item) => item.status === "pending");
747
805
  const isBusy = items.some((item) => item.status === "working");
748
806
  const canStart = pending.length > 0 && !isBusy && canEncode !== false;
@@ -776,7 +834,7 @@ var DialogUpload = ({
776
834
  update(item.id, {
777
835
  status: "failed",
778
836
  progress: null,
779
- error: error instanceof Error ? error.message : String(error)
837
+ error: toMessage(error)
780
838
  });
781
839
  }
782
840
  }
@@ -817,28 +875,22 @@ var DialogUpload = ({
817
875
  id: "r2-video-upload",
818
876
  width: 1,
819
877
  onClose: isBusy ? void 0 : onClose,
820
- footer: /* @__PURE__ */ jsx(Card, { borderTop: true, padding: 2, children: /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
821
- /* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(
822
- Button,
823
- {
824
- disabled: isBusy,
825
- mode: "ghost",
878
+ footer: /* @__PURE__ */ jsx(
879
+ DialogActions,
880
+ {
881
+ cancel: {
826
882
  text: items.length > 0 && !canStart ? "Done" : "Cancel",
827
- width: "fill",
883
+ disabled: isBusy,
828
884
  onClick: onClose
829
- }
830
- ) }),
831
- /* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(
832
- Button,
833
- {
834
- disabled: !canStart,
835
- text: isBusy ? "Encoding\u2026" : `Upload ${countLabel(pending.length)}`,
885
+ },
886
+ confirm: {
887
+ text: isBusy ? "Encoding\u2026" : `Upload ${pluralize(pending.length, "video")}`,
836
888
  tone: "primary",
837
- width: "fill",
889
+ disabled: !canStart,
838
890
  onClick: confirmed
839
891
  }
840
- ) })
841
- ] }) }),
892
+ }
893
+ ),
842
894
  children: /* @__PURE__ */ jsx(Card, { padding: 4, children: /* @__PURE__ */ jsxs(Stack, { gap: 5, children: [
843
895
  canEncode === false && /* @__PURE__ */ jsx(UnsupportedBrowser, {}),
844
896
  /* @__PURE__ */ jsx(
@@ -883,7 +935,7 @@ var DialogUpload = ({
883
935
  }
884
936
  ),
885
937
  items.length > 0 && /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
886
- /* @__PURE__ */ jsx(Text, { muted: true, size: 1, weight: "medium", children: countLabel(items.length) }),
938
+ /* @__PURE__ */ jsx(Text, { muted: true, size: 1, weight: "medium", children: pluralize(items.length, "video") }),
887
939
  /* @__PURE__ */ jsx(Stack, { gap: 2, children: items.map((item) => /* @__PURE__ */ jsx(
888
940
  Card,
889
941
  {
@@ -976,7 +1028,7 @@ var InputVideo = (props) => {
976
1028
  };
977
1029
  return /* @__PURE__ */ jsxs(Stack, { gap: 3, style: { position: "relative" }, ...dropProps, children: [
978
1030
  isDragging && /* @__PURE__ */ jsx(DropToUpload, { isRejected }),
979
- /* @__PURE__ */ jsxs(Flex, { gap: 0, children: [
1031
+ /* @__PURE__ */ jsxs(Flex, { gap: 1, children: [
980
1032
  /* @__PURE__ */ jsx(Box, { flex: 1, children: renderDefault({
981
1033
  ...props,
982
1034
  renderField: (field) => field.children
@@ -1015,7 +1067,7 @@ var VideoPreview = ({ renditions, posterUrl }) => {
1015
1067
  const config = useR2VideoConfig();
1016
1068
  const rendition = resolvePreviewRendition(renditions);
1017
1069
  if (!rendition) {
1018
- return /* @__PURE__ */ jsx(Card, { padding: 4, radius: 2, tone: "caution", children: /* @__PURE__ */ jsx(Text, { muted: true, size: 1, children: "This video has no renditions to play." }) });
1070
+ return /* @__PURE__ */ jsx(Notice, { tone: "caution", children: "This video has no renditions to play." });
1019
1071
  }
1020
1072
  return /* @__PURE__ */ jsx(
1021
1073
  "video",
@@ -1054,9 +1106,6 @@ var VideoSummary = ({
1054
1106
  hasAudio,
1055
1107
  uploadedAt
1056
1108
  }) => {
1057
- const totalBytes = renditions.reduce((total, rendition) => {
1058
- return total + rendition.size;
1059
- }, 0);
1060
1109
  const ordered = [...renditions].sort((a, b) => b.height - a.height);
1061
1110
  const largest = ordered[0];
1062
1111
  return /* @__PURE__ */ jsxs(Stack, { gap: 5, children: [
@@ -1070,7 +1119,7 @@ var VideoSummary = ({
1070
1119
  ),
1071
1120
  /* @__PURE__ */ jsx(Fact, { label: "Duration", value: formatDuration(duration) }),
1072
1121
  /* @__PURE__ */ jsx(Fact, { label: "Audio", value: hasAudio ? "Kept" : "Stripped" }),
1073
- /* @__PURE__ */ jsx(Fact, { label: "Total", value: formatSize(totalBytes) })
1122
+ /* @__PURE__ */ jsx(Fact, { label: "Total", value: formatSize(totalSize(renditions)) })
1074
1123
  ] }) }),
1075
1124
  /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
1076
1125
  /* @__PURE__ */ jsx(Text, { muted: true, size: 1, weight: "medium", children: "Renditions" }),
@@ -1276,116 +1325,109 @@ var SCHEMA_R2_VIDEO = defineType({
1276
1325
  }
1277
1326
  }
1278
1327
  });
1279
- var DialogDelete = ({ asset, onDeleted, onClose }) => {
1280
- const config = useR2VideoConfig();
1281
- const client = useClient({ apiVersion: config.apiVersion });
1282
- const [blockers, setBlockers] = useState(null);
1328
+ var DialogDelete = ({ assets, onDeleted, onClose }) => {
1329
+ const { config, client } = useR2VideoClient();
1330
+ const [blocked, setBlocked] = useState(null);
1283
1331
  const [isDeleting, setIsDeleting] = useState(false);
1284
1332
  const [error, setError] = useState(null);
1285
1333
  useEffect(() => {
1286
- findReferencingDocuments(client, asset._id).then(setBlockers).catch(() => {
1287
- setError("Could not check which documents use this video.");
1334
+ Promise.all(
1335
+ assets.map(async (asset) => ({
1336
+ asset,
1337
+ documents: await findReferencingDocuments(client, asset._id)
1338
+ }))
1339
+ ).then((checked) => {
1340
+ setBlocked(checked.filter((entry) => entry.documents.length > 0));
1341
+ }).catch(() => {
1342
+ setError("Could not check which documents use these videos.");
1288
1343
  });
1289
- }, [client, asset._id]);
1344
+ }, [client, assets]);
1290
1345
  const confirmed = async () => {
1291
1346
  setIsDeleting(true);
1292
1347
  setError(null);
1293
1348
  try {
1294
- await deleteVideoAsset(client, config, asset);
1349
+ for (const asset of assets) {
1350
+ await deleteVideoAsset(client, config, asset);
1351
+ }
1295
1352
  onDeleted();
1296
1353
  } catch (caught) {
1297
- setError(caught instanceof Error ? caught.message : String(caught));
1354
+ setError(toMessage(caught));
1298
1355
  setIsDeleting(false);
1299
1356
  }
1300
1357
  };
1301
- const totalBytes = asset.renditions.reduce((total, rendition) => {
1302
- return total + rendition.size;
1303
- }, 0);
1304
- const isBlocked = blockers !== null && blockers.length > 0;
1358
+ const isBlocked = blocked !== null && blocked.length > 0;
1359
+ const renditions = assets.flatMap((asset) => asset.renditions);
1360
+ const isOne = assets.length === 1;
1305
1361
  return /* @__PURE__ */ jsx(
1306
1362
  Dialog,
1307
1363
  {
1308
- header: "Delete video",
1364
+ header: isOne ? "Delete video" : `Delete ${assets.length} videos`,
1309
1365
  id: "r2-video-delete",
1310
1366
  width: 1,
1311
1367
  onClose: isDeleting ? void 0 : onClose,
1312
- footer: /* @__PURE__ */ jsx(Card, { borderTop: true, padding: 2, children: /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
1313
- /* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(
1314
- Button,
1315
- {
1316
- disabled: isDeleting,
1317
- mode: "ghost",
1318
- text: "Cancel",
1319
- width: "fill",
1320
- onClick: onClose
1321
- }
1322
- ) }),
1323
- /* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(
1324
- Button,
1325
- {
1326
- disabled: isDeleting || isBlocked || blockers === null,
1368
+ footer: /* @__PURE__ */ jsx(
1369
+ DialogActions,
1370
+ {
1371
+ cancel: { text: "Cancel", disabled: isDeleting, onClick: onClose },
1372
+ confirm: {
1327
1373
  text: isDeleting ? "Deleting\u2026" : "Delete",
1328
1374
  tone: "critical",
1329
- width: "fill",
1375
+ disabled: isDeleting || isBlocked || blocked === null,
1330
1376
  onClick: confirmed
1331
1377
  }
1332
- ) })
1333
- ] }) }),
1378
+ }
1379
+ ),
1334
1380
  children: /* @__PURE__ */ jsx(Card, { padding: 4, children: /* @__PURE__ */ jsxs(Stack, { gap: 4, children: [
1335
1381
  /* @__PURE__ */ jsxs(Text, { size: 1, children: [
1336
- asset.filename,
1337
- " - ",
1338
- asset.renditions.length,
1339
- " renditions,",
1382
+ isOne ? assets[0].filename : pluralize(assets.length, "video"),
1383
+ " -",
1340
1384
  " ",
1341
- formatSize(totalBytes),
1342
- ", plus its poster."
1343
- ] }),
1344
- blockers === null && !error && /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 3, children: [
1345
- /* @__PURE__ */ jsx(Spinner, { muted: true }),
1346
- /* @__PURE__ */ jsx(Text, { muted: true, size: 1, children: "Checking where it's used\u2026" })
1385
+ pluralize(renditions.length, "rendition"),
1386
+ ",",
1387
+ " ",
1388
+ formatSize(totalSize(renditions)),
1389
+ ", plus",
1390
+ " ",
1391
+ isOne ? "its poster" : "their posters",
1392
+ "."
1347
1393
  ] }),
1348
- isBlocked && /* @__PURE__ */ jsx(Card, { padding: 3, radius: 2, tone: "caution", children: /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
1349
- /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: "Still in use" }),
1350
- /* @__PURE__ */ jsx(Text, { muted: true, size: 1, children: "Remove it from these documents first:" }),
1351
- blockers.map((blocker) => /* @__PURE__ */ jsxs(Text, { muted: true, size: 1, children: [
1352
- blocker.title || blocker._id,
1353
- " (",
1354
- blocker._type,
1355
- ")"
1356
- ] }, blocker._id))
1394
+ blocked === null && !error && /* @__PURE__ */ jsx(Loading, { children: "Checking where they're used\u2026" }),
1395
+ isBlocked && /* @__PURE__ */ jsx(Notice, { title: "Still in use", tone: "caution", children: /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
1396
+ /* @__PURE__ */ jsxs(Text, { muted: true, size: 1, children: [
1397
+ "Remove ",
1398
+ isOne ? "it" : "these",
1399
+ " from these documents first:"
1400
+ ] }),
1401
+ blocked.map((entry) => /* @__PURE__ */ jsxs(Text, { muted: true, size: 1, children: [
1402
+ !isOne && `${entry.asset.filename}: `,
1403
+ entry.documents.map((document) => document.title || document._id).join(", ")
1404
+ ] }, entry.asset._id))
1357
1405
  ] }) }),
1358
- blockers !== null && blockers.length === 0 && /* @__PURE__ */ jsx(Text, { muted: true, size: 1, children: "Nothing references it. This can't be undone." }),
1359
- error && /* @__PURE__ */ jsx(Card, { padding: 3, radius: 2, tone: "critical", children: /* @__PURE__ */ jsx(Text, { size: 1, children: error }) })
1406
+ blocked !== null && blocked.length === 0 && /* @__PURE__ */ jsxs(Text, { muted: true, size: 1, children: [
1407
+ "Nothing references ",
1408
+ isOne ? "it" : "them",
1409
+ ". This can't be undone."
1410
+ ] }),
1411
+ error && /* @__PURE__ */ jsx(Notice, { tone: "critical", children: error })
1360
1412
  ] }) })
1361
1413
  }
1362
1414
  );
1363
1415
  };
1364
- var Field2 = ({ label, children }) => {
1365
- return /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
1366
- /* @__PURE__ */ jsx(Text, { muted: true, size: 1, weight: "medium", children: label }),
1367
- children
1368
- ] });
1369
- };
1370
1416
  var DialogDetails = ({
1371
1417
  asset,
1372
1418
  onChanged,
1373
1419
  onDelete,
1374
1420
  onClose
1375
1421
  }) => {
1376
- const config = useR2VideoConfig();
1377
- const client = useClient({ apiVersion: config.apiVersion });
1378
- const [folders, setFolders] = useState([]);
1422
+ const { client } = useR2VideoClient();
1423
+ const { paths } = useFolders();
1379
1424
  const [folderId, setFolderId] = useState(
1380
1425
  asset.folder ? asset.folder._ref : ""
1381
1426
  );
1382
1427
  const [filename, setFilename] = useState(asset.filename);
1383
1428
  const [isSaving, setIsSaving] = useState(false);
1429
+ const [isFlagged, setIsFlagged] = useState(false);
1384
1430
  const [error, setError] = useState(null);
1385
- useEffect(() => {
1386
- fetchFolders(client, config.folders.type).then(setFolders);
1387
- }, [client, config.folders.type]);
1388
- const paths = resolveFolderPaths(folders);
1389
1431
  const isBroken = asset.renditions.length === 0;
1390
1432
  const trimmed = filename.trim();
1391
1433
  const savedFolderId = asset.folder ? asset.folder._ref : "";
@@ -1395,6 +1437,17 @@ var DialogDetails = ({
1395
1437
  const reset = () => {
1396
1438
  setFilename(asset.filename);
1397
1439
  setFolderId(savedFolderId);
1440
+ setIsFlagged(false);
1441
+ };
1442
+ const attemptClose = () => {
1443
+ if (isSaving) {
1444
+ return;
1445
+ }
1446
+ if (isDirty) {
1447
+ setIsFlagged(true);
1448
+ return;
1449
+ }
1450
+ onClose();
1398
1451
  };
1399
1452
  const save = async () => {
1400
1453
  if (!isDirty) {
@@ -1423,10 +1476,11 @@ var DialogDetails = ({
1423
1476
  mutation = mutation.unset(removals);
1424
1477
  }
1425
1478
  await mutation.commit();
1479
+ setIsFlagged(false);
1426
1480
  onChanged();
1427
1481
  } catch (caught) {
1428
1482
  reset();
1429
- setError(caught instanceof Error ? caught.message : String(caught));
1483
+ setError(toMessage(caught));
1430
1484
  }
1431
1485
  setIsSaving(false);
1432
1486
  };
@@ -1436,41 +1490,36 @@ var DialogDetails = ({
1436
1490
  header: asset.filename || "Untitled video",
1437
1491
  id: "r2-video-details",
1438
1492
  width: 1,
1439
- onClose: isSaving ? void 0 : onClose,
1440
- footer: /* @__PURE__ */ jsx(Card, { borderTop: true, padding: 2, children: /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
1441
- /* @__PURE__ */ jsx(
1442
- Button,
1443
- {
1444
- "aria-label": "Delete video",
1445
- disabled: isSaving,
1446
- icon: TrashIcon,
1447
- mode: "ghost",
1448
- tone: "critical",
1449
- onClick: onDelete
1450
- }
1451
- ),
1452
- /* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(
1453
- Button,
1454
- {
1455
- disabled: isSaving,
1456
- mode: "ghost",
1493
+ onClickOutside: attemptClose,
1494
+ onClose: isSaving ? void 0 : attemptClose,
1495
+ footer: /* @__PURE__ */ jsx(
1496
+ DialogActions,
1497
+ {
1498
+ aside: /* @__PURE__ */ jsx(
1499
+ Button,
1500
+ {
1501
+ "aria-label": "Delete video",
1502
+ disabled: isSaving,
1503
+ icon: TrashIcon,
1504
+ mode: "ghost",
1505
+ tone: "critical",
1506
+ onClick: onDelete
1507
+ }
1508
+ ),
1509
+ cancel: {
1457
1510
  text: isDirty ? "Discard" : "Close",
1458
- width: "fill",
1511
+ disabled: isSaving,
1459
1512
  onClick: isDirty ? reset : onClose
1460
- }
1461
- ) }),
1462
- !isBroken && /* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(
1463
- Button,
1464
- {
1465
- disabled: isSaving || !isDirty,
1513
+ },
1514
+ confirm: isBroken ? void 0 : {
1466
1515
  text: isSaving ? "Saving\u2026" : "Save changes",
1467
1516
  tone: "primary",
1468
- width: "fill",
1517
+ disabled: isSaving || !isDirty,
1469
1518
  onClick: save
1470
1519
  }
1471
- ) })
1472
- ] }) }),
1473
- children: /* @__PURE__ */ jsx(Card, { padding: 4, children: isBroken ? /* @__PURE__ */ jsx(Card, { padding: 4, radius: 2, tone: "caution", children: /* @__PURE__ */ jsx(Text, { size: 1, children: "This video was never finished uploading, so it has no renditions and nothing stored behind it. Delete it." }) }) : /* @__PURE__ */ jsxs(Stack, { gap: 5, children: [
1520
+ }
1521
+ ),
1522
+ children: /* @__PURE__ */ jsx(Card, { padding: 4, children: isBroken ? /* @__PURE__ */ jsx(Notice, { tone: "caution", children: "This video was never finished uploading, so it has no renditions and nothing stored behind it. Delete it." }) : /* @__PURE__ */ jsxs(Stack, { gap: 5, children: [
1474
1523
  /* @__PURE__ */ jsx(
1475
1524
  VideoPreview,
1476
1525
  {
@@ -1479,7 +1528,7 @@ var DialogDetails = ({
1479
1528
  }
1480
1529
  ),
1481
1530
  /* @__PURE__ */ jsxs(Flex, { gap: 3, children: [
1482
- /* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(Field2, { label: "Filename", children: /* @__PURE__ */ jsx(
1531
+ /* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(Field, { label: "Filename", children: /* @__PURE__ */ jsx(
1483
1532
  TextInput,
1484
1533
  {
1485
1534
  "aria-label": "Filename",
@@ -1493,7 +1542,7 @@ var DialogDetails = ({
1493
1542
  }
1494
1543
  }
1495
1544
  ) }) }),
1496
- /* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(Field2, { label: "Folder", children: /* @__PURE__ */ jsxs(
1545
+ /* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(Field, { label: "Folder", children: /* @__PURE__ */ jsxs(
1497
1546
  Select,
1498
1547
  {
1499
1548
  "aria-label": "Folder",
@@ -1507,7 +1556,8 @@ var DialogDetails = ({
1507
1556
  }
1508
1557
  ) }) })
1509
1558
  ] }),
1510
- error && /* @__PURE__ */ jsx(Card, { padding: 3, radius: 2, tone: "critical", children: /* @__PURE__ */ jsx(Text, { size: 1, children: error }) }),
1559
+ isFlagged && /* @__PURE__ */ jsx(Notice, { tone: "caution", children: "Unsaved changes. Save them, or discard to close." }),
1560
+ error && /* @__PURE__ */ jsx(Notice, { tone: "critical", children: error }),
1511
1561
  /* @__PURE__ */ jsx(
1512
1562
  VideoSummary,
1513
1563
  {
@@ -1556,19 +1606,15 @@ var removeOrphans = async (client, config, orphans) => {
1556
1606
  }
1557
1607
  }
1558
1608
  };
1559
- var countLabel2 = (count, noun) => {
1560
- return `${count} ${noun}${count === 1 ? "" : "s"}`;
1561
- };
1562
1609
  var DialogOrphans = ({ onCleaned, onClose }) => {
1563
- const config = useR2VideoConfig();
1564
- const client = useClient({ apiVersion: config.apiVersion });
1610
+ const { config, client } = useR2VideoClient();
1565
1611
  const [orphans, setOrphans] = useState(null);
1566
1612
  const [isRemoving, setIsRemoving] = useState(false);
1567
1613
  const [isDone, setIsDone] = useState(false);
1568
1614
  const [error, setError] = useState(null);
1569
1615
  useEffect(() => {
1570
1616
  findOrphans(client, config).then(setOrphans).catch((caught) => {
1571
- setError(caught instanceof Error ? caught.message : String(caught));
1617
+ setError(toMessage(caught));
1572
1618
  });
1573
1619
  }, [client, config]);
1574
1620
  const confirmed = async () => {
@@ -1582,7 +1628,7 @@ var DialogOrphans = ({ onCleaned, onClose }) => {
1582
1628
  setIsDone(true);
1583
1629
  onCleaned();
1584
1630
  } catch (caught) {
1585
- setError(caught instanceof Error ? caught.message : String(caught));
1631
+ setError(toMessage(caught));
1586
1632
  }
1587
1633
  setIsRemoving(false);
1588
1634
  };
@@ -1594,46 +1640,37 @@ var DialogOrphans = ({ onCleaned, onClose }) => {
1594
1640
  id: "r2-video-orphans",
1595
1641
  width: 1,
1596
1642
  onClose: isRemoving ? void 0 : onClose,
1597
- footer: /* @__PURE__ */ jsx(Card, { borderTop: true, padding: 2, children: /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
1598
- /* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(
1599
- Button,
1600
- {
1601
- disabled: isRemoving,
1602
- mode: "ghost",
1643
+ footer: /* @__PURE__ */ jsx(
1644
+ DialogActions,
1645
+ {
1646
+ cancel: {
1603
1647
  text: isDone ? "Done" : "Cancel",
1604
- width: "fill",
1648
+ disabled: isRemoving,
1605
1649
  onClick: onClose
1606
- }
1607
- ) }),
1608
- /* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(
1609
- Button,
1610
- {
1611
- disabled: isRemoving || isDone || total === 0,
1650
+ },
1651
+ confirm: {
1612
1652
  text: isRemoving ? "Removing\u2026" : "Remove",
1613
1653
  tone: "critical",
1614
- width: "fill",
1654
+ disabled: isRemoving || isDone || total === 0,
1615
1655
  onClick: confirmed
1616
1656
  }
1617
- ) })
1618
- ] }) }),
1657
+ }
1658
+ ),
1619
1659
  children: /* @__PURE__ */ jsx(Card, { padding: 4, children: /* @__PURE__ */ jsxs(Stack, { gap: 4, children: [
1620
- !orphans && !error && /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 3, children: [
1621
- /* @__PURE__ */ jsx(Spinner, { muted: true }),
1622
- /* @__PURE__ */ jsx(Text, { muted: true, size: 1, children: "Comparing storage against the library\u2026" })
1623
- ] }),
1624
- isDone && /* @__PURE__ */ jsx(Card, { padding: 3, radius: 2, tone: "positive", children: /* @__PURE__ */ jsx(Text, { size: 1, children: "Removed." }) }),
1660
+ !orphans && !error && /* @__PURE__ */ jsx(Loading, { children: "Comparing storage against the library\u2026" }),
1661
+ isDone && /* @__PURE__ */ jsx(Notice, { tone: "positive", children: "Removed." }),
1625
1662
  orphans && !isDone && total === 0 && /* @__PURE__ */ jsx(Text, { muted: true, size: 1, children: "Nothing to clean up - every stored file belongs to a video." }),
1626
1663
  orphans && !isDone && total > 0 && /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
1627
1664
  /* @__PURE__ */ jsxs(Text, { size: 1, children: [
1628
1665
  "Found ",
1629
- countLabel2(orphans.keys.length, "orphaned file"),
1666
+ pluralize(orphans.keys.length, "orphaned file"),
1630
1667
  " in R2 and ",
1631
- countLabel2(orphans.posterIds.length, "unused poster"),
1668
+ pluralize(orphans.posterIds.length, "unused poster"),
1632
1669
  " in Sanity."
1633
1670
  ] }),
1634
1671
  /* @__PURE__ */ jsx(Text, { muted: true, size: 1, children: "Nothing references any of them, so removing them can't affect the site. This can't be undone." })
1635
1672
  ] }),
1636
- error && /* @__PURE__ */ jsx(Card, { padding: 3, radius: 2, tone: "critical", children: /* @__PURE__ */ jsx(Text, { size: 1, children: error }) })
1673
+ error && /* @__PURE__ */ jsx(Notice, { tone: "critical", children: error })
1637
1674
  ] }) })
1638
1675
  }
1639
1676
  );
@@ -1716,6 +1753,7 @@ var QUERY_ASSETS = `*[_type == "r2Video.asset"] | order(uploadedAt desc){
1716
1753
  "hasAudio": coalesce(hasAudio, false),
1717
1754
  "renditions": coalesce(renditions, []),
1718
1755
  uploadedAt,
1756
+ "isUsed": count(*[references(^._id)]) > 0,
1719
1757
  "posterUrl": poster.asset->url,
1720
1758
  "folderName": folder->name
1721
1759
  }`;
@@ -1737,9 +1775,22 @@ var matches = (asset, search) => {
1737
1775
  const folderName = asset.folderName ?? "";
1738
1776
  return asset.filename.toLowerCase().includes(term) || folderName.toLowerCase().includes(term);
1739
1777
  };
1778
+ var SelectableCard = styled(Box)`
1779
+ position: relative;
1780
+
1781
+ & [data-checkbox] {
1782
+ opacity: 0;
1783
+ }
1784
+
1785
+ &:hover [data-checkbox],
1786
+ & [data-checkbox]:focus-within,
1787
+ & [data-checkbox][data-selected="true"] {
1788
+ opacity: 1;
1789
+ }
1790
+ `;
1740
1791
  var ToolVideoLibrary = () => {
1741
- const config = useR2VideoConfig();
1742
- const client = useClient({ apiVersion: config.apiVersion });
1792
+ const { config, client } = useR2VideoClient();
1793
+ const { paths: allPaths } = useFolders();
1743
1794
  const [assets, setAssets] = useState(null);
1744
1795
  const [folders, setFolders] = useState([]);
1745
1796
  const [search, setSearch] = useState("");
@@ -1749,6 +1800,9 @@ var ToolVideoLibrary = () => {
1749
1800
  const [droppedFiles, setDroppedFiles] = useState([]);
1750
1801
  const [detailing, setDetailing] = useState(null);
1751
1802
  const [deleting, setDeleting] = useState(null);
1803
+ const [isUnusedOnly, setIsUnusedOnly] = useState(false);
1804
+ const [selectedIds, setSelectedIds] = useState([]);
1805
+ const [isMoving, setIsMoving] = useState(false);
1752
1806
  const load = useCallback(() => {
1753
1807
  client.fetch(QUERY_ASSETS).then(setAssets);
1754
1808
  fetchFolders(client, config.folders.type).then(setFolders);
@@ -1778,8 +1832,29 @@ var ToolVideoLibrary = () => {
1778
1832
  };
1779
1833
  const visible = (assets ?? []).filter((asset) => {
1780
1834
  const inFolder = !folderId || asset.folder && asset.folder._ref === folderId;
1781
- return matches(asset, search) && inFolder;
1835
+ const isShown = !isUnusedOnly || !asset.isUsed;
1836
+ return matches(asset, search) && inFolder && isShown;
1782
1837
  });
1838
+ const selected = visible.filter((asset) => selectedIds.includes(asset._id));
1839
+ const toggleSelected = (id) => {
1840
+ setSelectedIds((current) => {
1841
+ return current.includes(id) ? current.filter((entry) => entry !== id) : [...current, id];
1842
+ });
1843
+ };
1844
+ const moveSelected = async (targetId) => {
1845
+ setIsMoving(true);
1846
+ let transaction = client.transaction();
1847
+ for (const asset of selected) {
1848
+ transaction = targetId ? transaction.patch(
1849
+ asset._id,
1850
+ (patch) => patch.set({ folder: { _type: "reference", _ref: targetId } })
1851
+ ) : transaction.patch(asset._id, (patch) => patch.unset(["folder"]));
1852
+ }
1853
+ await transaction.commit();
1854
+ setIsMoving(false);
1855
+ setSelectedIds([]);
1856
+ load();
1857
+ };
1783
1858
  return /* @__PURE__ */ jsxs(
1784
1859
  Flex,
1785
1860
  {
@@ -1807,6 +1882,15 @@ var ToolVideoLibrary = () => {
1807
1882
  onChange: (event) => setSearch(event.currentTarget.value)
1808
1883
  }
1809
1884
  ) }),
1885
+ /* @__PURE__ */ jsx(
1886
+ Button,
1887
+ {
1888
+ mode: isUnusedOnly ? "default" : "ghost",
1889
+ text: "Unused",
1890
+ tone: isUnusedOnly ? "primary" : "default",
1891
+ onClick: () => setIsUnusedOnly(!isUnusedOnly)
1892
+ }
1893
+ ),
1810
1894
  /* @__PURE__ */ jsx(
1811
1895
  Button,
1812
1896
  {
@@ -1826,45 +1910,115 @@ var ToolVideoLibrary = () => {
1826
1910
  }
1827
1911
  )
1828
1912
  ] }),
1829
- assets === null && /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 3, padding: 4, children: [
1830
- /* @__PURE__ */ jsx(Spinner, { muted: true }),
1831
- /* @__PURE__ */ jsx(Text, { muted: true, size: 1, children: "Loading library\u2026" })
1832
- ] }),
1913
+ selected.length > 0 && /* @__PURE__ */ jsx(Card, { border: true, padding: 2, radius: 2, tone: "primary", children: /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
1914
+ /* @__PURE__ */ jsx(Box, { paddingX: 2, children: /* @__PURE__ */ jsxs(Text, { size: 1, weight: "medium", children: [
1915
+ pluralize(selected.length, "video"),
1916
+ " selected"
1917
+ ] }) }),
1918
+ /* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsxs(
1919
+ Select,
1920
+ {
1921
+ "aria-label": "Move to folder",
1922
+ disabled: isMoving,
1923
+ value: "",
1924
+ onChange: (event) => moveSelected(event.currentTarget.value),
1925
+ children: [
1926
+ /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: "Move to\u2026" }),
1927
+ /* @__PURE__ */ jsx("option", { value: "", children: "No folder" }),
1928
+ allPaths.map((entry) => /* @__PURE__ */ jsx("option", { value: entry.id, children: entry.path }, entry.id))
1929
+ ]
1930
+ }
1931
+ ) }),
1932
+ /* @__PURE__ */ jsx(
1933
+ Button,
1934
+ {
1935
+ disabled: isMoving,
1936
+ mode: "ghost",
1937
+ text: "Clear",
1938
+ onClick: () => setSelectedIds([])
1939
+ }
1940
+ ),
1941
+ /* @__PURE__ */ jsx(
1942
+ Button,
1943
+ {
1944
+ disabled: isMoving,
1945
+ icon: TrashIcon,
1946
+ mode: "ghost",
1947
+ text: "Delete",
1948
+ tone: "critical",
1949
+ onClick: () => setDeleting(selected)
1950
+ }
1951
+ )
1952
+ ] }) }),
1953
+ assets === null && /* @__PURE__ */ jsx(Box, { padding: 4, children: /* @__PURE__ */ jsx(Loading, { children: "Loading library\u2026" }) }),
1833
1954
  assets !== null && visible.length === 0 && /* @__PURE__ */ jsx(Card, { padding: 5, radius: 2, tone: "transparent", children: /* @__PURE__ */ jsx(Text, { align: "center", muted: true, size: 1, children: "No videos here yet." }) }),
1834
- /* @__PURE__ */ jsx(Grid, { gridTemplateColumns: [1, 2, 3, 4], gap: 3, children: visible.map((asset) => /* @__PURE__ */ jsx(
1835
- Card,
1836
- {
1837
- as: "button",
1838
- border: true,
1839
- padding: 2,
1840
- radius: 2,
1841
- style: { cursor: "pointer", textAlign: "left" },
1842
- onClick: () => setDetailing(asset),
1843
- children: /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
1844
- /* @__PURE__ */ jsx(
1845
- Box,
1955
+ /* @__PURE__ */ jsx(Grid, { gridTemplateColumns: [1, 2, 3, 4], gap: 3, children: visible.map((asset) => /* @__PURE__ */ jsxs(SelectableCard, { children: [
1956
+ /* @__PURE__ */ jsx(
1957
+ Box,
1958
+ {
1959
+ "data-checkbox": true,
1960
+ "data-selected": selectedIds.includes(asset._id),
1961
+ style: {
1962
+ position: "absolute",
1963
+ top: 14,
1964
+ left: 14,
1965
+ zIndex: 1
1966
+ },
1967
+ children: /* @__PURE__ */ jsx(
1968
+ Checkbox,
1846
1969
  {
1847
- style: {
1848
- aspectRatio: "16 / 9",
1849
- backgroundImage: asset.posterUrl ? `url(${asset.posterUrl}?w=480&fit=crop&auto=format)` : void 0,
1850
- backgroundPosition: "center",
1851
- backgroundSize: "cover",
1852
- borderRadius: 2
1853
- }
1970
+ "aria-label": `Select ${asset.filename}`,
1971
+ checked: selectedIds.includes(asset._id),
1972
+ onChange: () => toggleSelected(asset._id)
1973
+ }
1974
+ )
1975
+ }
1976
+ ),
1977
+ /* @__PURE__ */ jsx(
1978
+ Card,
1979
+ {
1980
+ as: "button",
1981
+ border: true,
1982
+ padding: 2,
1983
+ radius: 2,
1984
+ style: {
1985
+ cursor: "pointer",
1986
+ textAlign: "left",
1987
+ width: "100%"
1988
+ },
1989
+ onClick: (event) => {
1990
+ if (event.shiftKey) {
1991
+ toggleSelected(asset._id);
1992
+ return;
1854
1993
  }
1855
- ),
1856
- /* @__PURE__ */ jsxs(Stack, { gap: 2, children: [
1857
- /* @__PURE__ */ jsx(Text, { size: 1, textOverflow: "ellipsis", weight: "medium", children: toTitle(asset, Boolean(folderId)) }),
1858
- /* @__PURE__ */ jsxs(Text, { muted: true, size: 1, children: [
1859
- asset.renditions.length,
1860
- " sizes",
1861
- asset.hasAudio ? " \xB7 audio" : ""
1994
+ setDetailing(asset);
1995
+ },
1996
+ children: /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
1997
+ /* @__PURE__ */ jsx(
1998
+ Box,
1999
+ {
2000
+ style: {
2001
+ aspectRatio: "16 / 9",
2002
+ backgroundImage: asset.posterUrl ? `url(${asset.posterUrl}?w=480&fit=crop&auto=format)` : void 0,
2003
+ backgroundPosition: "center",
2004
+ backgroundSize: "cover",
2005
+ borderRadius: 2
2006
+ }
2007
+ }
2008
+ ),
2009
+ /* @__PURE__ */ jsxs(Stack, { gap: 2, children: [
2010
+ /* @__PURE__ */ jsx(Text, { size: 1, textOverflow: "ellipsis", weight: "medium", children: toTitle(asset, Boolean(folderId)) }),
2011
+ /* @__PURE__ */ jsxs(Text, { muted: true, size: 1, children: [
2012
+ asset.renditions.length,
2013
+ " sizes",
2014
+ asset.hasAudio ? " \xB7 audio" : "",
2015
+ asset.isUsed ? "" : " \xB7 unused"
2016
+ ] })
1862
2017
  ] })
1863
2018
  ] })
1864
- ] })
1865
- },
1866
- asset._id
1867
- )) })
2019
+ }
2020
+ )
2021
+ ] }, asset._id)) })
1868
2022
  ] }) }) }),
1869
2023
  isDragging && /* @__PURE__ */ jsx(DropToUpload, { isRejected }),
1870
2024
  isUploadOpen && /* @__PURE__ */ jsx(
@@ -1889,7 +2043,7 @@ var ToolVideoLibrary = () => {
1889
2043
  asset: detailing,
1890
2044
  onChanged: load,
1891
2045
  onDelete: () => {
1892
- setDeleting(detailing);
2046
+ setDeleting([detailing]);
1893
2047
  setDetailing(null);
1894
2048
  },
1895
2049
  onClose: () => setDetailing(null)
@@ -1898,7 +2052,7 @@ var ToolVideoLibrary = () => {
1898
2052
  deleting && /* @__PURE__ */ jsx(
1899
2053
  DialogDelete,
1900
2054
  {
1901
- asset: deleting,
2055
+ assets: deleting,
1902
2056
  onClose: () => setDeleting(null),
1903
2057
  onDeleted: () => {
1904
2058
  setDeleting(null);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sanity-plugin-r2-video",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Video for Sanity Studio, encoded in the browser and stored in Cloudflare R2 as a ladder of plain MP4s.",
5
5
  "keywords": [
6
6
  "sanity",