sanity-plugin-r2-video 0.1.8 → 0.1.9

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 +184 -188
  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 { Stack, Flex, Box, Button, TextInput, Card, Text, Grid, Dialog, Spinner, Select, 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';
@@ -107,6 +107,24 @@ var deleteVideoAsset = async (client, config, asset) => {
107
107
  renditions.map((rendition) => rendition.key)
108
108
  );
109
109
  };
110
+ var R2VideoConfigContext = createContext(null);
111
+ var R2VideoConfigProvider = ({ config, children }) => {
112
+ return /* @__PURE__ */ jsx(R2VideoConfigContext.Provider, { value: config, children });
113
+ };
114
+ var useR2VideoConfig = () => {
115
+ const config = useContext(R2VideoConfigContext);
116
+ if (!config) {
117
+ throw new Error(
118
+ "Missing R2 video config. Add `r2Video` to the Studio's plugin list."
119
+ );
120
+ }
121
+ return config;
122
+ };
123
+ var useR2VideoClient = () => {
124
+ const config = useR2VideoConfig();
125
+ const client = useClient({ apiVersion: config.apiVersion });
126
+ return { config, client };
127
+ };
110
128
 
111
129
  // studio/folders.ts
112
130
  var QUERY_FOLDERS = `*[_type == $folderType]{
@@ -150,18 +168,13 @@ var resolvePosterFolder = async (client, folderType, name) => {
150
168
  const created = await client.create({ _type: folderType, name });
151
169
  return created._id;
152
170
  };
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;
171
+ var useFolders = () => {
172
+ const { config, client } = useR2VideoClient();
173
+ const [folders, setFolders] = useState([]);
174
+ useEffect(() => {
175
+ fetchFolders(client, config.folders.type).then(setFolders);
176
+ }, [client, config.folders.type]);
177
+ return { folders, paths: resolveFolderPaths(folders) };
165
178
  };
166
179
 
167
180
  // studio/defaults.ts
@@ -204,6 +217,15 @@ var formatSize = (bytes) => {
204
217
  var formatDuration = (seconds) => {
205
218
  return `${seconds.toFixed(1)}s`;
206
219
  };
220
+ var totalSize = (renditions) => {
221
+ return renditions.reduce((total, rendition) => total + rendition.size, 0);
222
+ };
223
+ var pluralize = (count, noun) => {
224
+ return `${count} ${noun}${count === 1 ? "" : "s"}`;
225
+ };
226
+ var toMessage = (error) => {
227
+ return error instanceof Error ? error.message : String(error);
228
+ };
207
229
  var formatBitrate = (bytes, seconds) => {
208
230
  if (!seconds) {
209
231
  return "-";
@@ -333,6 +355,57 @@ var transcodeVideo = (request, progressed) => {
333
355
  worker.postMessage(request);
334
356
  });
335
357
  };
358
+ var Notice = ({ tone, title, icon, children }) => {
359
+ return /* @__PURE__ */ jsx(Card, { padding: 4, radius: 2, tone, children: /* @__PURE__ */ jsxs(Flex, { align: "flex-start", gap: 3, children: [
360
+ icon && /* @__PURE__ */ jsx(Text, { size: 2, children: icon }),
361
+ /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
362
+ title && /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: title }),
363
+ typeof children === "string" ? /* @__PURE__ */ jsx(Text, { muted: Boolean(title), size: 1, children }) : children
364
+ ] })
365
+ ] }) });
366
+ };
367
+ var Field = ({ label, id, description, children }) => {
368
+ return /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
369
+ /* @__PURE__ */ jsx(Text, { as: id ? "label" : void 0, htmlFor: id, size: 1, weight: "medium", children: label }),
370
+ children,
371
+ description && /* @__PURE__ */ jsx(Text, { muted: true, size: 0, children: description })
372
+ ] });
373
+ };
374
+ var DialogActions = ({
375
+ aside,
376
+ cancel,
377
+ confirm
378
+ }) => {
379
+ return /* @__PURE__ */ jsx(Card, { borderTop: true, padding: 2, children: /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
380
+ aside,
381
+ /* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(
382
+ Button,
383
+ {
384
+ disabled: cancel.disabled,
385
+ mode: "ghost",
386
+ text: cancel.text,
387
+ width: "fill",
388
+ onClick: cancel.onClick
389
+ }
390
+ ) }),
391
+ confirm && /* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(
392
+ Button,
393
+ {
394
+ disabled: confirm.disabled,
395
+ text: confirm.text,
396
+ tone: confirm.tone,
397
+ width: "fill",
398
+ onClick: confirm.onClick
399
+ }
400
+ ) })
401
+ ] }) });
402
+ };
403
+ var Loading = ({ children }) => {
404
+ return /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 3, children: [
405
+ /* @__PURE__ */ jsx(Spinner, { muted: true }),
406
+ /* @__PURE__ */ jsx(Text, { muted: true, size: 1, children })
407
+ ] });
408
+ };
336
409
  var PreviewEncode = ({ file, keepAudio, encoding }) => {
337
410
  const [preview, setPreview] = useState(null);
338
411
  const [isEncoding, setIsEncoding] = useState(false);
@@ -374,7 +447,7 @@ var PreviewEncode = ({ file, keepAudio, encoding }) => {
374
447
  duration: result.duration
375
448
  });
376
449
  } catch (caught) {
377
- setError(caught instanceof Error ? caught.message : String(caught));
450
+ setError(toMessage(caught));
378
451
  }
379
452
  setIsEncoding(false);
380
453
  };
@@ -394,15 +467,12 @@ var PreviewEncode = ({ file, keepAudio, encoding }) => {
394
467
  }
395
468
  )
396
469
  ] }),
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
- ] })
470
+ isEncoding && /* @__PURE__ */ jsxs(Loading, { children: [
471
+ "Encoding top tier - ",
472
+ Math.round(progress * 100),
473
+ "%"
404
474
  ] }),
405
- error && /* @__PURE__ */ jsx(Card, { padding: 3, radius: 2, tone: "critical", children: /* @__PURE__ */ jsx(Text, { size: 1, children: error }) }),
475
+ error && /* @__PURE__ */ jsx(Notice, { tone: "critical", children: error }),
406
476
  preview && !isEncoding && /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
407
477
  /* @__PURE__ */ jsx(
408
478
  "video",
@@ -439,20 +509,15 @@ var PreviewEncode = ({ file, keepAudio, encoding }) => {
439
509
  ] }) });
440
510
  };
441
511
  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
- ] });
512
+ return /* @__PURE__ */ jsx(
513
+ Notice,
514
+ {
515
+ icon: /* @__PURE__ */ jsx(WarningOutlineIcon, {}),
516
+ title: "This browser can't encode video",
517
+ tone: "caution",
518
+ 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."
519
+ }
520
+ );
456
521
  };
457
522
  var ToggleRow = ({ id, label, description, children }) => {
458
523
  return /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 3, children: [
@@ -702,9 +767,6 @@ var describe = (item) => {
702
767
  const { stage, progress, label } = item.progress;
703
768
  return `${STAGE_LABELS[stage]} ${label} \xB7 ${Math.round(progress * 100)}%`;
704
769
  };
705
- var countLabel = (count) => {
706
- return count === 1 ? "1 video" : `${count} video(s)`;
707
- };
708
770
  var toItem = (file) => ({
709
771
  id: crypto.randomUUID(),
710
772
  file,
@@ -718,10 +780,9 @@ var DialogUpload = ({
718
780
  onUploaded,
719
781
  onClose
720
782
  }) => {
721
- const config = useR2VideoConfig();
722
- const client = useClient({ apiVersion: config.apiVersion });
783
+ const { config, client } = useR2VideoClient();
784
+ const { paths } = useFolders();
723
785
  const [canEncode, setCanEncode] = useState(null);
724
- const [folders, setFolders] = useState([]);
725
786
  const [targetFolder, setTargetFolder] = useState(folderId);
726
787
  const [keepAudio, setKeepAudio] = useState(false);
727
788
  const [quality, setQuality] = useState(config.encoding.quality);
@@ -736,13 +797,9 @@ var DialogUpload = ({
736
797
  useEffect(() => {
737
798
  canEncodeLadder(config.encoding.videoCodec).then(setCanEncode);
738
799
  }, [config.encoding.videoCodec]);
739
- useEffect(() => {
740
- fetchFolders(client, config.folders.type).then(setFolders);
741
- }, [client, config.folders.type]);
742
800
  const encoding = useMemo(() => {
743
801
  return { ...config.encoding, quality, preferBitrate };
744
802
  }, [config.encoding, quality, preferBitrate]);
745
- const paths = resolveFolderPaths(folders);
746
803
  const pending = items.filter((item) => item.status === "pending");
747
804
  const isBusy = items.some((item) => item.status === "working");
748
805
  const canStart = pending.length > 0 && !isBusy && canEncode !== false;
@@ -776,7 +833,7 @@ var DialogUpload = ({
776
833
  update(item.id, {
777
834
  status: "failed",
778
835
  progress: null,
779
- error: error instanceof Error ? error.message : String(error)
836
+ error: toMessage(error)
780
837
  });
781
838
  }
782
839
  }
@@ -817,28 +874,22 @@ var DialogUpload = ({
817
874
  id: "r2-video-upload",
818
875
  width: 1,
819
876
  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",
877
+ footer: /* @__PURE__ */ jsx(
878
+ DialogActions,
879
+ {
880
+ cancel: {
826
881
  text: items.length > 0 && !canStart ? "Done" : "Cancel",
827
- width: "fill",
882
+ disabled: isBusy,
828
883
  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)}`,
884
+ },
885
+ confirm: {
886
+ text: isBusy ? "Encoding\u2026" : `Upload ${pluralize(pending.length, "video")}`,
836
887
  tone: "primary",
837
- width: "fill",
888
+ disabled: !canStart,
838
889
  onClick: confirmed
839
890
  }
840
- ) })
841
- ] }) }),
891
+ }
892
+ ),
842
893
  children: /* @__PURE__ */ jsx(Card, { padding: 4, children: /* @__PURE__ */ jsxs(Stack, { gap: 5, children: [
843
894
  canEncode === false && /* @__PURE__ */ jsx(UnsupportedBrowser, {}),
844
895
  /* @__PURE__ */ jsx(
@@ -883,7 +934,7 @@ var DialogUpload = ({
883
934
  }
884
935
  ),
885
936
  items.length > 0 && /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
886
- /* @__PURE__ */ jsx(Text, { muted: true, size: 1, weight: "medium", children: countLabel(items.length) }),
937
+ /* @__PURE__ */ jsx(Text, { muted: true, size: 1, weight: "medium", children: pluralize(items.length, "video") }),
887
938
  /* @__PURE__ */ jsx(Stack, { gap: 2, children: items.map((item) => /* @__PURE__ */ jsx(
888
939
  Card,
889
940
  {
@@ -976,7 +1027,7 @@ var InputVideo = (props) => {
976
1027
  };
977
1028
  return /* @__PURE__ */ jsxs(Stack, { gap: 3, style: { position: "relative" }, ...dropProps, children: [
978
1029
  isDragging && /* @__PURE__ */ jsx(DropToUpload, { isRejected }),
979
- /* @__PURE__ */ jsxs(Flex, { gap: 0, children: [
1030
+ /* @__PURE__ */ jsxs(Flex, { gap: 1, children: [
980
1031
  /* @__PURE__ */ jsx(Box, { flex: 1, children: renderDefault({
981
1032
  ...props,
982
1033
  renderField: (field) => field.children
@@ -1015,7 +1066,7 @@ var VideoPreview = ({ renditions, posterUrl }) => {
1015
1066
  const config = useR2VideoConfig();
1016
1067
  const rendition = resolvePreviewRendition(renditions);
1017
1068
  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." }) });
1069
+ return /* @__PURE__ */ jsx(Notice, { tone: "caution", children: "This video has no renditions to play." });
1019
1070
  }
1020
1071
  return /* @__PURE__ */ jsx(
1021
1072
  "video",
@@ -1054,9 +1105,6 @@ var VideoSummary = ({
1054
1105
  hasAudio,
1055
1106
  uploadedAt
1056
1107
  }) => {
1057
- const totalBytes = renditions.reduce((total, rendition) => {
1058
- return total + rendition.size;
1059
- }, 0);
1060
1108
  const ordered = [...renditions].sort((a, b) => b.height - a.height);
1061
1109
  const largest = ordered[0];
1062
1110
  return /* @__PURE__ */ jsxs(Stack, { gap: 5, children: [
@@ -1070,7 +1118,7 @@ var VideoSummary = ({
1070
1118
  ),
1071
1119
  /* @__PURE__ */ jsx(Fact, { label: "Duration", value: formatDuration(duration) }),
1072
1120
  /* @__PURE__ */ jsx(Fact, { label: "Audio", value: hasAudio ? "Kept" : "Stripped" }),
1073
- /* @__PURE__ */ jsx(Fact, { label: "Total", value: formatSize(totalBytes) })
1121
+ /* @__PURE__ */ jsx(Fact, { label: "Total", value: formatSize(totalSize(renditions)) })
1074
1122
  ] }) }),
1075
1123
  /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
1076
1124
  /* @__PURE__ */ jsx(Text, { muted: true, size: 1, weight: "medium", children: "Renditions" }),
@@ -1277,8 +1325,7 @@ var SCHEMA_R2_VIDEO = defineType({
1277
1325
  }
1278
1326
  });
1279
1327
  var DialogDelete = ({ asset, onDeleted, onClose }) => {
1280
- const config = useR2VideoConfig();
1281
- const client = useClient({ apiVersion: config.apiVersion });
1328
+ const { config, client } = useR2VideoClient();
1282
1329
  const [blockers, setBlockers] = useState(null);
1283
1330
  const [isDeleting, setIsDeleting] = useState(false);
1284
1331
  const [error, setError] = useState(null);
@@ -1294,13 +1341,10 @@ var DialogDelete = ({ asset, onDeleted, onClose }) => {
1294
1341
  await deleteVideoAsset(client, config, asset);
1295
1342
  onDeleted();
1296
1343
  } catch (caught) {
1297
- setError(caught instanceof Error ? caught.message : String(caught));
1344
+ setError(toMessage(caught));
1298
1345
  setIsDeleting(false);
1299
1346
  }
1300
1347
  };
1301
- const totalBytes = asset.renditions.reduce((total, rendition) => {
1302
- return total + rendition.size;
1303
- }, 0);
1304
1348
  const isBlocked = blockers !== null && blockers.length > 0;
1305
1349
  return /* @__PURE__ */ jsx(
1306
1350
  Dialog,
@@ -1309,28 +1353,18 @@ var DialogDelete = ({ asset, onDeleted, onClose }) => {
1309
1353
  id: "r2-video-delete",
1310
1354
  width: 1,
1311
1355
  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,
1356
+ footer: /* @__PURE__ */ jsx(
1357
+ DialogActions,
1358
+ {
1359
+ cancel: { text: "Cancel", disabled: isDeleting, onClick: onClose },
1360
+ confirm: {
1327
1361
  text: isDeleting ? "Deleting\u2026" : "Delete",
1328
1362
  tone: "critical",
1329
- width: "fill",
1363
+ disabled: isDeleting || isBlocked || blockers === null,
1330
1364
  onClick: confirmed
1331
1365
  }
1332
- ) })
1333
- ] }) }),
1366
+ }
1367
+ ),
1334
1368
  children: /* @__PURE__ */ jsx(Card, { padding: 4, children: /* @__PURE__ */ jsxs(Stack, { gap: 4, children: [
1335
1369
  /* @__PURE__ */ jsxs(Text, { size: 1, children: [
1336
1370
  asset.filename,
@@ -1338,15 +1372,11 @@ var DialogDelete = ({ asset, onDeleted, onClose }) => {
1338
1372
  asset.renditions.length,
1339
1373
  " renditions,",
1340
1374
  " ",
1341
- formatSize(totalBytes),
1375
+ formatSize(totalSize(asset.renditions)),
1342
1376
  ", plus its poster."
1343
1377
  ] }),
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" })
1347
- ] }),
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" }),
1378
+ blockers === null && !error && /* @__PURE__ */ jsx(Loading, { children: "Checking where it's used\u2026" }),
1379
+ isBlocked && /* @__PURE__ */ jsx(Notice, { title: "Still in use", tone: "caution", children: /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
1350
1380
  /* @__PURE__ */ jsx(Text, { muted: true, size: 1, children: "Remove it from these documents first:" }),
1351
1381
  blockers.map((blocker) => /* @__PURE__ */ jsxs(Text, { muted: true, size: 1, children: [
1352
1382
  blocker.title || blocker._id,
@@ -1356,36 +1386,25 @@ var DialogDelete = ({ asset, onDeleted, onClose }) => {
1356
1386
  ] }, blocker._id))
1357
1387
  ] }) }),
1358
1388
  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 }) })
1389
+ error && /* @__PURE__ */ jsx(Notice, { tone: "critical", children: error })
1360
1390
  ] }) })
1361
1391
  }
1362
1392
  );
1363
1393
  };
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
1394
  var DialogDetails = ({
1371
1395
  asset,
1372
1396
  onChanged,
1373
1397
  onDelete,
1374
1398
  onClose
1375
1399
  }) => {
1376
- const config = useR2VideoConfig();
1377
- const client = useClient({ apiVersion: config.apiVersion });
1378
- const [folders, setFolders] = useState([]);
1400
+ const { client } = useR2VideoClient();
1401
+ const { paths } = useFolders();
1379
1402
  const [folderId, setFolderId] = useState(
1380
1403
  asset.folder ? asset.folder._ref : ""
1381
1404
  );
1382
1405
  const [filename, setFilename] = useState(asset.filename);
1383
1406
  const [isSaving, setIsSaving] = useState(false);
1384
1407
  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
1408
  const isBroken = asset.renditions.length === 0;
1390
1409
  const trimmed = filename.trim();
1391
1410
  const savedFolderId = asset.folder ? asset.folder._ref : "";
@@ -1426,7 +1445,7 @@ var DialogDetails = ({
1426
1445
  onChanged();
1427
1446
  } catch (caught) {
1428
1447
  reset();
1429
- setError(caught instanceof Error ? caught.message : String(caught));
1448
+ setError(toMessage(caught));
1430
1449
  }
1431
1450
  setIsSaving(false);
1432
1451
  };
@@ -1437,40 +1456,34 @@ var DialogDetails = ({
1437
1456
  id: "r2-video-details",
1438
1457
  width: 1,
1439
1458
  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",
1459
+ footer: /* @__PURE__ */ jsx(
1460
+ DialogActions,
1461
+ {
1462
+ aside: /* @__PURE__ */ jsx(
1463
+ Button,
1464
+ {
1465
+ "aria-label": "Delete video",
1466
+ disabled: isSaving,
1467
+ icon: TrashIcon,
1468
+ mode: "ghost",
1469
+ tone: "critical",
1470
+ onClick: onDelete
1471
+ }
1472
+ ),
1473
+ cancel: {
1457
1474
  text: isDirty ? "Discard" : "Close",
1458
- width: "fill",
1475
+ disabled: isSaving,
1459
1476
  onClick: isDirty ? reset : onClose
1460
- }
1461
- ) }),
1462
- !isBroken && /* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(
1463
- Button,
1464
- {
1465
- disabled: isSaving || !isDirty,
1477
+ },
1478
+ confirm: isBroken ? void 0 : {
1466
1479
  text: isSaving ? "Saving\u2026" : "Save changes",
1467
1480
  tone: "primary",
1468
- width: "fill",
1481
+ disabled: isSaving || !isDirty,
1469
1482
  onClick: save
1470
1483
  }
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: [
1484
+ }
1485
+ ),
1486
+ 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
1487
  /* @__PURE__ */ jsx(
1475
1488
  VideoPreview,
1476
1489
  {
@@ -1479,7 +1492,7 @@ var DialogDetails = ({
1479
1492
  }
1480
1493
  ),
1481
1494
  /* @__PURE__ */ jsxs(Flex, { gap: 3, children: [
1482
- /* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(Field2, { label: "Filename", children: /* @__PURE__ */ jsx(
1495
+ /* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(Field, { label: "Filename", children: /* @__PURE__ */ jsx(
1483
1496
  TextInput,
1484
1497
  {
1485
1498
  "aria-label": "Filename",
@@ -1493,7 +1506,7 @@ var DialogDetails = ({
1493
1506
  }
1494
1507
  }
1495
1508
  ) }) }),
1496
- /* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(Field2, { label: "Folder", children: /* @__PURE__ */ jsxs(
1509
+ /* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(Field, { label: "Folder", children: /* @__PURE__ */ jsxs(
1497
1510
  Select,
1498
1511
  {
1499
1512
  "aria-label": "Folder",
@@ -1507,7 +1520,7 @@ var DialogDetails = ({
1507
1520
  }
1508
1521
  ) }) })
1509
1522
  ] }),
1510
- error && /* @__PURE__ */ jsx(Card, { padding: 3, radius: 2, tone: "critical", children: /* @__PURE__ */ jsx(Text, { size: 1, children: error }) }),
1523
+ error && /* @__PURE__ */ jsx(Notice, { tone: "critical", children: error }),
1511
1524
  /* @__PURE__ */ jsx(
1512
1525
  VideoSummary,
1513
1526
  {
@@ -1556,19 +1569,15 @@ var removeOrphans = async (client, config, orphans) => {
1556
1569
  }
1557
1570
  }
1558
1571
  };
1559
- var countLabel2 = (count, noun) => {
1560
- return `${count} ${noun}${count === 1 ? "" : "s"}`;
1561
- };
1562
1572
  var DialogOrphans = ({ onCleaned, onClose }) => {
1563
- const config = useR2VideoConfig();
1564
- const client = useClient({ apiVersion: config.apiVersion });
1573
+ const { config, client } = useR2VideoClient();
1565
1574
  const [orphans, setOrphans] = useState(null);
1566
1575
  const [isRemoving, setIsRemoving] = useState(false);
1567
1576
  const [isDone, setIsDone] = useState(false);
1568
1577
  const [error, setError] = useState(null);
1569
1578
  useEffect(() => {
1570
1579
  findOrphans(client, config).then(setOrphans).catch((caught) => {
1571
- setError(caught instanceof Error ? caught.message : String(caught));
1580
+ setError(toMessage(caught));
1572
1581
  });
1573
1582
  }, [client, config]);
1574
1583
  const confirmed = async () => {
@@ -1582,7 +1591,7 @@ var DialogOrphans = ({ onCleaned, onClose }) => {
1582
1591
  setIsDone(true);
1583
1592
  onCleaned();
1584
1593
  } catch (caught) {
1585
- setError(caught instanceof Error ? caught.message : String(caught));
1594
+ setError(toMessage(caught));
1586
1595
  }
1587
1596
  setIsRemoving(false);
1588
1597
  };
@@ -1594,46 +1603,37 @@ var DialogOrphans = ({ onCleaned, onClose }) => {
1594
1603
  id: "r2-video-orphans",
1595
1604
  width: 1,
1596
1605
  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",
1606
+ footer: /* @__PURE__ */ jsx(
1607
+ DialogActions,
1608
+ {
1609
+ cancel: {
1603
1610
  text: isDone ? "Done" : "Cancel",
1604
- width: "fill",
1611
+ disabled: isRemoving,
1605
1612
  onClick: onClose
1606
- }
1607
- ) }),
1608
- /* @__PURE__ */ jsx(Box, { flex: 1, children: /* @__PURE__ */ jsx(
1609
- Button,
1610
- {
1611
- disabled: isRemoving || isDone || total === 0,
1613
+ },
1614
+ confirm: {
1612
1615
  text: isRemoving ? "Removing\u2026" : "Remove",
1613
1616
  tone: "critical",
1614
- width: "fill",
1617
+ disabled: isRemoving || isDone || total === 0,
1615
1618
  onClick: confirmed
1616
1619
  }
1617
- ) })
1618
- ] }) }),
1620
+ }
1621
+ ),
1619
1622
  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." }) }),
1623
+ !orphans && !error && /* @__PURE__ */ jsx(Loading, { children: "Comparing storage against the library\u2026" }),
1624
+ isDone && /* @__PURE__ */ jsx(Notice, { tone: "positive", children: "Removed." }),
1625
1625
  orphans && !isDone && total === 0 && /* @__PURE__ */ jsx(Text, { muted: true, size: 1, children: "Nothing to clean up - every stored file belongs to a video." }),
1626
1626
  orphans && !isDone && total > 0 && /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
1627
1627
  /* @__PURE__ */ jsxs(Text, { size: 1, children: [
1628
1628
  "Found ",
1629
- countLabel2(orphans.keys.length, "orphaned file"),
1629
+ pluralize(orphans.keys.length, "orphaned file"),
1630
1630
  " in R2 and ",
1631
- countLabel2(orphans.posterIds.length, "unused poster"),
1631
+ pluralize(orphans.posterIds.length, "unused poster"),
1632
1632
  " in Sanity."
1633
1633
  ] }),
1634
1634
  /* @__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
1635
  ] }),
1636
- error && /* @__PURE__ */ jsx(Card, { padding: 3, radius: 2, tone: "critical", children: /* @__PURE__ */ jsx(Text, { size: 1, children: error }) })
1636
+ error && /* @__PURE__ */ jsx(Notice, { tone: "critical", children: error })
1637
1637
  ] }) })
1638
1638
  }
1639
1639
  );
@@ -1738,8 +1738,7 @@ var matches = (asset, search) => {
1738
1738
  return asset.filename.toLowerCase().includes(term) || folderName.toLowerCase().includes(term);
1739
1739
  };
1740
1740
  var ToolVideoLibrary = () => {
1741
- const config = useR2VideoConfig();
1742
- const client = useClient({ apiVersion: config.apiVersion });
1741
+ const { config, client } = useR2VideoClient();
1743
1742
  const [assets, setAssets] = useState(null);
1744
1743
  const [folders, setFolders] = useState([]);
1745
1744
  const [search, setSearch] = useState("");
@@ -1826,10 +1825,7 @@ var ToolVideoLibrary = () => {
1826
1825
  }
1827
1826
  )
1828
1827
  ] }),
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
- ] }),
1828
+ assets === null && /* @__PURE__ */ jsx(Box, { padding: 4, children: /* @__PURE__ */ jsx(Loading, { children: "Loading library\u2026" }) }),
1833
1829
  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
1830
  /* @__PURE__ */ jsx(Grid, { gridTemplateColumns: [1, 2, 3, 4], gap: 3, children: visible.map((asset) => /* @__PURE__ */ jsx(
1835
1831
  Card,
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.9",
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",