sanity-plugin-mux-input 2.12.1 → 2.14.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.
Files changed (35) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +133 -0
  3. package/dist/index.d.mts +63 -1
  4. package/dist/index.d.ts +63 -1
  5. package/dist/index.js +1564 -153
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +1565 -154
  8. package/dist/index.mjs.map +1 -1
  9. package/package.json +1 -1
  10. package/src/_exports/index.ts +1 -0
  11. package/src/actions/assets.ts +75 -0
  12. package/src/components/AddCaptionDialog.tsx +421 -0
  13. package/src/components/CaptionsDialog.tsx +23 -0
  14. package/src/components/EditCaptionDialog.tsx +508 -0
  15. package/src/components/FileInputButton.tsx +2 -2
  16. package/src/components/Onboard.tsx +2 -2
  17. package/src/components/PageSelector.tsx +57 -0
  18. package/src/components/Player.tsx +4 -3
  19. package/src/components/PlayerActionsMenu.tsx +17 -8
  20. package/src/components/TextTracksManager.tsx +781 -0
  21. package/src/components/UploadConfiguration.tsx +181 -4
  22. package/src/components/UploadPlaceholder.tsx +14 -6
  23. package/src/components/Uploader.styled.tsx +8 -15
  24. package/src/components/Uploader.tsx +61 -6
  25. package/src/components/VideoDetails/VideoDetails.tsx +16 -0
  26. package/src/components/VideoInBrowser.tsx +2 -7
  27. package/src/components/VideoPlayer.tsx +35 -6
  28. package/src/components/VideosBrowser.tsx +9 -1
  29. package/src/components/icons/Audio.tsx +13 -0
  30. package/src/hooks/useAccessControl.ts +1 -0
  31. package/src/hooks/useDialogState.ts +1 -1
  32. package/src/util/getVideoMetadata.ts +3 -1
  33. package/src/util/textTracks.ts +219 -0
  34. package/src/util/types.ts +56 -3
  35. package/src/components/FileInputArea.tsx +0 -93
package/dist/index.js CHANGED
@@ -19,11 +19,11 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
19
19
  mod
20
20
  ));
21
21
  Object.defineProperty(exports, "__esModule", { value: !0 });
22
- var sanity = require("sanity"), jsxRuntime = require("react/jsx-runtime"), icons = require("@sanity/icons"), ui = require("@sanity/ui"), React = require("react"), compact = require("lodash/compact.js"), toLower = require("lodash/toLower.js"), trim = require("lodash/trim.js"), uniq = require("lodash/uniq.js"), words = require("lodash/words.js"), suspendReact = require("suspend-react"), rxjs = require("rxjs"), styledComponents = require("styled-components"), uuid = require("@sanity/uuid"), operators = require("rxjs/operators"), MuxPlayer = require("@mux/mux-player-react/lazy"), router = require("sanity/router"), isNumber = require("lodash/isNumber.js"), isString = require("lodash/isString.js"), reactRx = require("react-rx"), useSWR = require("swr"), scrollIntoView = require("scroll-into-view-if-needed"), upchunk = require("@mux/upchunk"), reactIs = require("react-is"), LanguagesList = require("iso-639-1");
22
+ var sanity = require("sanity"), jsxRuntime = require("react/jsx-runtime"), icons = require("@sanity/icons"), ui = require("@sanity/ui"), React = require("react"), compact = require("lodash/compact.js"), toLower = require("lodash/toLower.js"), trim = require("lodash/trim.js"), uniq = require("lodash/uniq.js"), words = require("lodash/words.js"), suspendReact = require("suspend-react"), rxjs = require("rxjs"), styledComponents = require("styled-components"), uuid = require("@sanity/uuid"), operators = require("rxjs/operators"), LanguagesList = require("iso-639-1"), MuxPlayer = require("@mux/mux-player-react/lazy"), router = require("sanity/router"), isNumber = require("lodash/isNumber.js"), isString = require("lodash/isString.js"), reactRx = require("react-rx"), useSWR = require("swr"), scrollIntoView = require("scroll-into-view-if-needed"), upchunk = require("@mux/upchunk"), reactIs = require("react-is");
23
23
  function _interopDefaultCompat(e) {
24
24
  return e && typeof e == "object" && "default" in e ? e : { default: e };
25
25
  }
26
- var React__default = /* @__PURE__ */ _interopDefaultCompat(React), compact__default = /* @__PURE__ */ _interopDefaultCompat(compact), toLower__default = /* @__PURE__ */ _interopDefaultCompat(toLower), trim__default = /* @__PURE__ */ _interopDefaultCompat(trim), uniq__default = /* @__PURE__ */ _interopDefaultCompat(uniq), words__default = /* @__PURE__ */ _interopDefaultCompat(words), MuxPlayer__default = /* @__PURE__ */ _interopDefaultCompat(MuxPlayer), isNumber__default = /* @__PURE__ */ _interopDefaultCompat(isNumber), isString__default = /* @__PURE__ */ _interopDefaultCompat(isString), useSWR__default = /* @__PURE__ */ _interopDefaultCompat(useSWR), scrollIntoView__default = /* @__PURE__ */ _interopDefaultCompat(scrollIntoView), LanguagesList__default = /* @__PURE__ */ _interopDefaultCompat(LanguagesList);
26
+ var React__default = /* @__PURE__ */ _interopDefaultCompat(React), compact__default = /* @__PURE__ */ _interopDefaultCompat(compact), toLower__default = /* @__PURE__ */ _interopDefaultCompat(toLower), trim__default = /* @__PURE__ */ _interopDefaultCompat(trim), uniq__default = /* @__PURE__ */ _interopDefaultCompat(uniq), words__default = /* @__PURE__ */ _interopDefaultCompat(words), LanguagesList__default = /* @__PURE__ */ _interopDefaultCompat(LanguagesList), MuxPlayer__default = /* @__PURE__ */ _interopDefaultCompat(MuxPlayer), isNumber__default = /* @__PURE__ */ _interopDefaultCompat(isNumber), isString__default = /* @__PURE__ */ _interopDefaultCompat(isString), useSWR__default = /* @__PURE__ */ _interopDefaultCompat(useSWR), scrollIntoView__default = /* @__PURE__ */ _interopDefaultCompat(scrollIntoView);
27
27
  const ToolIcon = () => /* @__PURE__ */ jsxRuntime.jsx(
28
28
  "svg",
29
29
  {
@@ -568,6 +568,51 @@ function listAssets(client, options) {
568
568
  query
569
569
  });
570
570
  }
571
+ function addTextTrackFromUrl(client, assetId, vttUrl, options) {
572
+ const { dataset } = client.config();
573
+ return client.request({
574
+ url: `/addons/mux/assets/${dataset}/${assetId}/tracks`,
575
+ withCredentials: !0,
576
+ method: "POST",
577
+ body: {
578
+ url: vttUrl,
579
+ type: "text",
580
+ language_code: options.language_code,
581
+ name: options.name,
582
+ text_type: options.text_type || "subtitles"
583
+ },
584
+ headers: {
585
+ "Content-Type": "application/json"
586
+ }
587
+ });
588
+ }
589
+ function generateSubtitles(client, assetId, audioTrackId, options) {
590
+ const { dataset } = client.config();
591
+ return client.request({
592
+ url: `/addons/mux/assets/${dataset}/${assetId}/tracks/${audioTrackId}/generate-subtitles`,
593
+ withCredentials: !0,
594
+ method: "POST",
595
+ body: {
596
+ generated_subtitles: [
597
+ {
598
+ language_code: options.language_code,
599
+ name: options.name
600
+ }
601
+ ]
602
+ },
603
+ headers: {
604
+ "Content-Type": "application/json"
605
+ }
606
+ });
607
+ }
608
+ function deleteTextTrack(client, assetId, trackId) {
609
+ const { dataset } = client.config();
610
+ return client.request({
611
+ url: `/addons/mux/assets/${dataset}/${assetId}/tracks/${trackId}`,
612
+ withCredentials: !0,
613
+ method: "DELETE"
614
+ });
615
+ }
571
616
  const ASSETS_PER_PAGE = 100;
572
617
  async function fetchMuxAssetsPage(client, cursor) {
573
618
  try {
@@ -1105,6 +1150,46 @@ function ImportVideosFromMux() {
1105
1150
  if (importAssets.hasSecrets)
1106
1151
  return importAssets.dialogOpen ? /* @__PURE__ */ jsxRuntime.jsx(ImportVideosDialog, { ...importAssets }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { mode: "bleed", text: "Import from Mux", onClick: importAssets.openDialog });
1107
1152
  }
1153
+ const PageSelector = (props) => {
1154
+ const page = props.page, setPage = props.setPage;
1155
+ return React.useEffect(() => {
1156
+ const clamped = Math.min(props.total - 1, Math.max(0, page));
1157
+ page !== clamped && setPage(clamped);
1158
+ }, [page, props.total, setPage]), /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1159
+ /* @__PURE__ */ jsxRuntime.jsx(
1160
+ ui.Button,
1161
+ {
1162
+ icon: icons.ChevronLeftIcon,
1163
+ mode: "bleed",
1164
+ padding: 3,
1165
+ style: { cursor: "pointer" },
1166
+ disabled: page <= 0,
1167
+ onClick: () => {
1168
+ setPage((page2) => Math.min(props.total - 1, Math.max(0, page2 - 1)));
1169
+ }
1170
+ }
1171
+ ),
1172
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Label, { muted: !0, children: [
1173
+ "Page ",
1174
+ page + 1,
1175
+ "/",
1176
+ props.total
1177
+ ] }),
1178
+ /* @__PURE__ */ jsxRuntime.jsx(
1179
+ ui.Button,
1180
+ {
1181
+ icon: icons.ChevronRightIcon,
1182
+ mode: "bleed",
1183
+ padding: 3,
1184
+ style: { cursor: "pointer" },
1185
+ disabled: page >= props.total - 1,
1186
+ onClick: () => {
1187
+ setPage((page2) => Math.min(props.total - 1, Math.max(0, page2 + 1)));
1188
+ }
1189
+ }
1190
+ )
1191
+ ] });
1192
+ };
1108
1193
  function useResyncMuxMetadata() {
1109
1194
  const documentStore = sanity.useDocumentStore(), client = sanity.useClient({
1110
1195
  apiVersion: SANITY_API_VERSION
@@ -1347,38 +1432,1249 @@ const SpinnerBox = () => /* @__PURE__ */ jsxRuntime.jsx(
1347
1432
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: (props.size || 1) + 1, muted: !0, children: /* @__PURE__ */ jsxRuntime.jsx(Icon, {}) }),
1348
1433
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: props.size || 1, muted: props.muted, children: props.text })
1349
1434
  ] });
1350
- };
1351
- function ResolutionIcon(props) {
1352
- return /* @__PURE__ */ jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "1em", height: "1em", viewBox: "0 0 24 24", ...props, children: /* @__PURE__ */ jsxRuntime.jsx(
1353
- "path",
1435
+ };
1436
+ function ResolutionIcon(props) {
1437
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "1em", height: "1em", viewBox: "0 0 24 24", ...props, children: /* @__PURE__ */ jsxRuntime.jsx(
1438
+ "path",
1439
+ {
1440
+ fill: "currentColor",
1441
+ d: "M20 9V6h-3V4h5v5h-2ZM2 9V4h5v2H4v3H2Zm15 11v-2h3v-3h2v5h-5ZM2 20v-5h2v3h3v2H2Zm4-4V8h12v8H6Zm2-2h8v-4H8v4Zm0 0v-4v4Z"
1442
+ }
1443
+ ) });
1444
+ }
1445
+ function StopWatchIcon(props) {
1446
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1447
+ "svg",
1448
+ {
1449
+ xmlns: "http://www.w3.org/2000/svg",
1450
+ width: "1em",
1451
+ height: "1em",
1452
+ viewBox: "0 0 512 512",
1453
+ ...props,
1454
+ children: [
1455
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M232 306.667h48V176h-48v130.667z", fill: "currentColor" }),
1456
+ /* @__PURE__ */ jsxRuntime.jsx(
1457
+ "path",
1458
+ {
1459
+ 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",
1460
+ fill: "currentColor"
1461
+ }
1462
+ ),
1463
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M192 32h128v48H192z", fill: "currentColor" })
1464
+ ]
1465
+ }
1466
+ );
1467
+ }
1468
+ function extractErrorMessage(error, defaultMessage = "Failed to process request") {
1469
+ let message = "";
1470
+ if (error && typeof error == "object") {
1471
+ const err = error;
1472
+ message = err.response?.body?.message || err.message || "";
1473
+ } else typeof error == "string" && (message = error);
1474
+ if (!message)
1475
+ return defaultMessage;
1476
+ const match = message.match(/\(([^)]+)\)/);
1477
+ if (match && match[1])
1478
+ return match[1];
1479
+ if (message.includes("responded with")) {
1480
+ const parts = message.split("(");
1481
+ if (parts.length > 1)
1482
+ return parts[parts.length - 1].replace(")", "").trim();
1483
+ }
1484
+ return message;
1485
+ }
1486
+ async function pollTrackStatus(options) {
1487
+ const {
1488
+ client,
1489
+ assetId,
1490
+ trackName,
1491
+ trackLanguageCode,
1492
+ maxAttempts = 10,
1493
+ onTrackFound,
1494
+ onTrackErrored,
1495
+ onTrackReady
1496
+ } = options, trimmedName = trackName.trim(), trimmedLanguageCode = trackLanguageCode.trim();
1497
+ let newTrack, attempts = 0, trackFound = !1;
1498
+ const findTrack = (textTracks) => {
1499
+ let foundTrack = textTracks.find(
1500
+ (track) => track.name === trimmedName && track.language_code === trimmedLanguageCode
1501
+ );
1502
+ return foundTrack || (foundTrack = textTracks.find((track) => track.language_code === trimmedLanguageCode)), !foundTrack && textTracks.length > 0 && (foundTrack = textTracks[textTracks.length - 1]), foundTrack;
1503
+ };
1504
+ for (; attempts < maxAttempts; ) {
1505
+ try {
1506
+ attempts > 0 && await new Promise((resolve) => setTimeout(resolve, 1e3));
1507
+ const textTracks = (await getAsset(client, assetId)).data.tracks?.filter((track) => track.type === "text") || [], foundTrack = findTrack(textTracks);
1508
+ if (!foundTrack) {
1509
+ attempts++;
1510
+ continue;
1511
+ }
1512
+ if (trackFound = !0, newTrack = foundTrack, onTrackFound && onTrackFound(foundTrack), foundTrack.status === "ready") {
1513
+ onTrackReady && onTrackReady(foundTrack);
1514
+ break;
1515
+ }
1516
+ if (foundTrack.status === "errored")
1517
+ return onTrackErrored && onTrackErrored(foundTrack), {
1518
+ track: foundTrack,
1519
+ found: !0,
1520
+ status: "errored"
1521
+ };
1522
+ } catch (error) {
1523
+ console.error("Failed to fetch updated asset:", error);
1524
+ }
1525
+ attempts++;
1526
+ }
1527
+ return !newTrack || !trackFound ? {
1528
+ track: void 0,
1529
+ found: !1,
1530
+ status: "not-found"
1531
+ } : newTrack.status === "preparing" ? {
1532
+ track: newTrack,
1533
+ found: !0,
1534
+ status: "preparing"
1535
+ } : {
1536
+ track: newTrack,
1537
+ found: !0,
1538
+ status: "ready"
1539
+ };
1540
+ }
1541
+ async function downloadVttFile(client, asset, track) {
1542
+ if (!track.id)
1543
+ throw new Error("Track ID is missing");
1544
+ if (track.status !== "ready")
1545
+ throw new Error(`Track is not ready yet. Status: ${track.status}`);
1546
+ if (!asset.assetId)
1547
+ throw new Error("Asset ID is required");
1548
+ const playbackId = getPlaybackId(asset);
1549
+ if (!playbackId)
1550
+ throw new Error("Playback ID is required");
1551
+ const playbackPolicy = getPlaybackPolicy(asset);
1552
+ let downloadUrl = `https://stream.mux.com/${playbackId}/text/${track.id}.vtt`;
1553
+ if (playbackPolicy === "signed") {
1554
+ const token = generateJwt(client, playbackId, "v");
1555
+ downloadUrl += `?token=${token}`;
1556
+ }
1557
+ const response = await fetch(downloadUrl);
1558
+ if (!response.ok)
1559
+ throw new Error(`Failed to download file: ${response.statusText}`);
1560
+ const blob = await response.blob(), blobUrl = URL.createObjectURL(blob), link = document.createElement("a");
1561
+ link.href = blobUrl, link.download = `${asset.filename || "captions"}-${track.language_code || "en"}.vtt`, document.body.appendChild(link), link.click(), document.body.removeChild(link), URL.revokeObjectURL(blobUrl);
1562
+ }
1563
+ const SUPPORTED_MUX_LANGUAGES = [
1564
+ { label: "English", code: "en", state: "Stable" },
1565
+ { label: "Spanish", code: "es", state: "Stable" },
1566
+ { label: "Italian", code: "it", state: "Stable" },
1567
+ { label: "Portuguese", code: "pt", state: "Stable" },
1568
+ { label: "German", code: "de", state: "Stable" },
1569
+ { label: "French", code: "fr", state: "Stable" },
1570
+ { label: "Polish", code: "pl", state: "Beta" },
1571
+ { label: "Russian", code: "ru", state: "Beta" },
1572
+ { label: "Dutch", code: "nl", state: "Beta" },
1573
+ { label: "Catalan", code: "ca", state: "Beta" },
1574
+ { label: "Turkish", code: "tr", state: "Beta" },
1575
+ { label: "Swedish", code: "sv", state: "Beta" },
1576
+ { label: "Ukrainian", code: "uk", state: "Beta" },
1577
+ { label: "Norwegian", code: "no", state: "Beta" },
1578
+ { label: "Finnish", code: "fi", state: "Beta" },
1579
+ { label: "Slovak", code: "sk", state: "Beta" },
1580
+ { label: "Greek", code: "el", state: "Beta" },
1581
+ { label: "Czech", code: "cs", state: "Beta" },
1582
+ { label: "Croatian", code: "hr", state: "Beta" },
1583
+ { label: "Danish", code: "da", state: "Beta" },
1584
+ { label: "Romanian", code: "ro", state: "Beta" },
1585
+ { label: "Bulgarian", code: "bg", state: "Beta" }
1586
+ ];
1587
+ function isCustomTextTrack(track) {
1588
+ return track.type !== "autogenerated";
1589
+ }
1590
+ function isAutogeneratedTrack(track) {
1591
+ return track.type === "autogenerated";
1592
+ }
1593
+ const LANGUAGE_OPTIONS$1 = LanguagesList__default.default.getAllCodes().map((code) => ({
1594
+ value: code,
1595
+ label: LanguagesList__default.default.getNativeName(code)
1596
+ })), MUX_LANGUAGE_OPTIONS = SUPPORTED_MUX_LANGUAGES.map((lang) => ({
1597
+ value: lang.code,
1598
+ label: lang.label
1599
+ }));
1600
+ function AddCaptionDialog({ asset, onAdd, onClose }) {
1601
+ const client = useClient(), toast = ui.useToast(), dialogId = `AddCaptionDialog${React.useId()}`, [isAutogenerated, setIsAutogenerated] = React.useState(!1), [vttUrl, setVttUrl] = React.useState(""), [languageCode, setLanguageCode] = React.useState(""), [selectedLanguage, setSelectedLanguage] = React.useState(
1602
+ null
1603
+ ), [name2, setName] = React.useState(""), [isSubmitting, setIsSubmitting] = React.useState(!1), [selectedFile, setSelectedFile] = React.useState(null), fileInputRef = React.useRef(null), uploadVttFile = async (file) => (await client.assets.upload("file", file, {
1604
+ filename: file.name
1605
+ })).url, handleAddTrackFromUrl = async () => {
1606
+ if (!asset.assetId)
1607
+ throw new Error("Asset ID is required");
1608
+ const trimmedName = name2.trim(), trimmedLanguageCode = languageCode.trim();
1609
+ let vttUrlToUse = vttUrl.trim();
1610
+ if (selectedFile)
1611
+ try {
1612
+ vttUrlToUse = await uploadVttFile(selectedFile);
1613
+ } catch (uploadError) {
1614
+ throw toast.push({
1615
+ title: "Failed to upload VTT file",
1616
+ status: "error",
1617
+ description: "Could not upload the VTT file to Sanity. Please try again."
1618
+ }), setIsSubmitting(!1), uploadError;
1619
+ }
1620
+ await addTextTrackFromUrl(client, asset.assetId, vttUrlToUse, {
1621
+ language_code: trimmedLanguageCode,
1622
+ name: trimmedName,
1623
+ text_type: "subtitles"
1624
+ });
1625
+ const result = await pollTrackStatus({
1626
+ client,
1627
+ assetId: asset.assetId,
1628
+ trackName: trimmedName,
1629
+ trackLanguageCode: trimmedLanguageCode,
1630
+ onTrackErrored: (track) => {
1631
+ const errorMessage = track.error?.messages?.[0] || track.error?.type || "The track failed to download from the provided URL";
1632
+ toast.push({
1633
+ title: "Caption track failed",
1634
+ status: "error",
1635
+ description: errorMessage
1636
+ }), onAdd(track), onClose();
1637
+ }
1638
+ });
1639
+ if (!result.found || !result.track) {
1640
+ toast.push({
1641
+ title: "Caption track may have been added",
1642
+ status: "warning",
1643
+ description: "The track was created but its status could not be determined. It may still be processing. Please refresh the page to see if it appears."
1644
+ }), onClose();
1645
+ return;
1646
+ }
1647
+ if (result.status !== "errored") {
1648
+ if (result.status === "preparing") {
1649
+ toast.push({
1650
+ title: "Caption track is processing",
1651
+ status: "info",
1652
+ description: "The track was created and is being processed. It will appear in the list shortly."
1653
+ }), onAdd(result.track), onClose();
1654
+ return;
1655
+ }
1656
+ toast.push({
1657
+ title: "Caption track added",
1658
+ status: "success",
1659
+ description: "Caption track added successfully"
1660
+ }), onAdd(result.track), onClose();
1661
+ }
1662
+ }, handleGenerateSubtitles = async () => {
1663
+ if (!asset.assetId)
1664
+ throw new Error("Asset ID is required");
1665
+ const audioTrack = (await getAsset(client, asset.assetId)).data.tracks?.find((track) => track.type === "audio");
1666
+ if (!audioTrack || !audioTrack.id)
1667
+ throw toast.push({
1668
+ title: "No audio track found",
1669
+ status: "error",
1670
+ description: "The asset does not have an audio track. Auto-generated subtitles require an audio track."
1671
+ }), new Error("No audio track found");
1672
+ await generateSubtitles(client, asset.assetId, audioTrack.id, {
1673
+ language_code: languageCode.trim(),
1674
+ name: name2.trim()
1675
+ });
1676
+ const mockTrack = {
1677
+ type: "text",
1678
+ id: `generating-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
1679
+ text_type: "subtitles",
1680
+ text_source: "generated_live",
1681
+ language_code: languageCode.trim(),
1682
+ name: name2.trim(),
1683
+ status: "preparing"
1684
+ };
1685
+ toast.push({
1686
+ title: "Generating subtitles",
1687
+ status: "success",
1688
+ description: "This may take a few minutes"
1689
+ }), onAdd(mockTrack), onClose();
1690
+ }, handleSubmit = async () => {
1691
+ if (!isAutogenerated) {
1692
+ if (!selectedFile && !vttUrl.trim()) {
1693
+ toast.push({
1694
+ title: "VTT file or URL required",
1695
+ status: "error",
1696
+ description: "Please select a VTT file or enter a VTT file URL"
1697
+ });
1698
+ return;
1699
+ }
1700
+ if (vttUrl.trim() && !selectedFile)
1701
+ try {
1702
+ new URL(vttUrl.trim());
1703
+ } catch {
1704
+ toast.push({
1705
+ title: "Invalid URL",
1706
+ status: "error",
1707
+ description: "Please enter a valid URL (e.g., https://example.com/subtitles.vtt)"
1708
+ });
1709
+ return;
1710
+ }
1711
+ }
1712
+ if (!name2.trim()) {
1713
+ toast.push({
1714
+ title: "Audio name required",
1715
+ status: "error",
1716
+ description: "Please enter an audio name for this caption track"
1717
+ });
1718
+ return;
1719
+ }
1720
+ if (!languageCode.trim()) {
1721
+ toast.push({
1722
+ title: "Language code required",
1723
+ status: "error",
1724
+ description: "Please enter a language code (e.g., en, es, fr)"
1725
+ });
1726
+ return;
1727
+ }
1728
+ setIsSubmitting(!0);
1729
+ try {
1730
+ isAutogenerated ? await handleGenerateSubtitles() : await handleAddTrackFromUrl();
1731
+ } catch (error) {
1732
+ toast.push({
1733
+ title: "Failed to add caption track",
1734
+ status: "error",
1735
+ description: extractErrorMessage(error, "Failed to add caption track")
1736
+ });
1737
+ } finally {
1738
+ setIsSubmitting(!1);
1739
+ }
1740
+ };
1741
+ return /* @__PURE__ */ jsxRuntime.jsx(
1742
+ ui.Dialog,
1743
+ {
1744
+ id: dialogId,
1745
+ header: "Add Caption Track",
1746
+ onClose,
1747
+ width: 1,
1748
+ onClickOutside: onClose,
1749
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { padding: 4, space: 4, children: [
1750
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1751
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", marginBottom: 3, children: [
1752
+ /* @__PURE__ */ jsxRuntime.jsx(
1753
+ ui.Checkbox,
1754
+ {
1755
+ id: "autogenerated-checkbox",
1756
+ style: { display: "block" },
1757
+ checked: isAutogenerated,
1758
+ onChange: (e) => {
1759
+ setIsAutogenerated(e.currentTarget.checked), e.currentTarget.checked && setVttUrl("");
1760
+ },
1761
+ disabled: isSubmitting
1762
+ }
1763
+ ),
1764
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { flex: 1, paddingLeft: 2, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "autogenerated-checkbox", children: "Generate captions" }) }) })
1765
+ ] }),
1766
+ !isAutogenerated && /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1767
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Card, { padding: 3, marginBottom: 2, tone: "transparent", border: !0, radius: 2, children: [
1768
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", justify: "space-between", children: [
1769
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, children: selectedFile ? `Selected: ${selectedFile.name}` : "No file selected" }),
1770
+ /* @__PURE__ */ jsxRuntime.jsx(
1771
+ ui.Button,
1772
+ {
1773
+ icon: icons.UploadIcon,
1774
+ text: "Select File",
1775
+ mode: "ghost",
1776
+ tone: "primary",
1777
+ fontSize: 1,
1778
+ padding: 2,
1779
+ onClick: () => fileInputRef.current?.click(),
1780
+ disabled: isSubmitting
1781
+ }
1782
+ )
1783
+ ] }),
1784
+ /* @__PURE__ */ jsxRuntime.jsx(
1785
+ "input",
1786
+ {
1787
+ ref: fileInputRef,
1788
+ type: "file",
1789
+ accept: ".vtt,text/vtt",
1790
+ style: { display: "none" },
1791
+ onChange: (e) => {
1792
+ e.target.files && e.target.files.length > 0 && !isSubmitting && (setSelectedFile(e.target.files[0]), setVttUrl(""));
1793
+ }
1794
+ }
1795
+ )
1796
+ ] }),
1797
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, style: { textAlign: "center" }, children: "Or enter the VTT file URL" }),
1798
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1799
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vtt-url", children: "VTT File URL" }),
1800
+ /* @__PURE__ */ jsxRuntime.jsx(
1801
+ ui.TextInput,
1802
+ {
1803
+ id: "vtt-url",
1804
+ placeholder: "https://example.com/subtitles.vtt",
1805
+ value: vttUrl,
1806
+ onChange: (e) => {
1807
+ setVttUrl(e.currentTarget.value), setSelectedFile(null);
1808
+ },
1809
+ disabled: isSubmitting
1810
+ }
1811
+ )
1812
+ ] })
1813
+ ] })
1814
+ ] }),
1815
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1816
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "caption-name", children: "Audio name" }),
1817
+ /* @__PURE__ */ jsxRuntime.jsx(
1818
+ ui.Autocomplete,
1819
+ {
1820
+ id: "caption-name",
1821
+ value: selectedLanguage?.value || "",
1822
+ onChange: (newValue) => {
1823
+ const selected = (isAutogenerated ? MUX_LANGUAGE_OPTIONS : LANGUAGE_OPTIONS$1).find((opt) => opt.value === newValue);
1824
+ selected && (setSelectedLanguage(selected), setLanguageCode(selected.value), setName(selected.label));
1825
+ },
1826
+ options: isAutogenerated ? MUX_LANGUAGE_OPTIONS : LANGUAGE_OPTIONS$1,
1827
+ icon: icons.TranslateIcon,
1828
+ placeholder: "Select language",
1829
+ filterOption: (query, option) => option.label.toLowerCase().indexOf(query.toLowerCase()) > -1 || option.value.toLowerCase().indexOf(query.toLowerCase()) > -1,
1830
+ openButton: !0,
1831
+ renderValue: (value) => (isAutogenerated ? MUX_LANGUAGE_OPTIONS : LANGUAGE_OPTIONS$1).find(
1832
+ (l) => l.value === value
1833
+ )?.label || value,
1834
+ renderOption: (option) => /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { "data-as": "button", padding: 3, radius: 2, tone: "inherit", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 2, textOverflow: "ellipsis", children: [
1835
+ option.label,
1836
+ " (",
1837
+ option.value,
1838
+ ")"
1839
+ ] }) }),
1840
+ disabled: isSubmitting
1841
+ }
1842
+ )
1843
+ ] }),
1844
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1845
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "caption-language", children: "Language Code" }),
1846
+ /* @__PURE__ */ jsxRuntime.jsx(
1847
+ ui.TextInput,
1848
+ {
1849
+ id: "caption-language",
1850
+ placeholder: "en-US",
1851
+ value: languageCode,
1852
+ onChange: (e) => {
1853
+ setLanguageCode(e.currentTarget.value), selectedLanguage && selectedLanguage.value !== e.currentTarget.value && (setSelectedLanguage(null), (!name2 || name2 === selectedLanguage.label) && setName(""));
1854
+ },
1855
+ disabled: isSubmitting
1856
+ }
1857
+ )
1858
+ ] }),
1859
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 2, justify: "flex-end", marginTop: 2, children: [
1860
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { text: "Cancel", mode: "ghost", onClick: onClose, disabled: isSubmitting }),
1861
+ /* @__PURE__ */ jsxRuntime.jsx(
1862
+ ui.Button,
1863
+ {
1864
+ text: "Add Caption Track",
1865
+ tone: "primary",
1866
+ icon: isSubmitting ? /* @__PURE__ */ jsxRuntime.jsx(
1867
+ ui.Spinner,
1868
+ {
1869
+ style: {
1870
+ verticalAlign: "middle",
1871
+ display: "inline-block",
1872
+ marginBottom: "-3px",
1873
+ width: "1em",
1874
+ height: "1em",
1875
+ marginRight: "-6px"
1876
+ }
1877
+ }
1878
+ ) : /* @__PURE__ */ jsxRuntime.jsx(icons.UploadIcon, {}),
1879
+ onClick: handleSubmit,
1880
+ disabled: isSubmitting
1881
+ }
1882
+ )
1883
+ ] })
1884
+ ] })
1885
+ }
1886
+ );
1887
+ }
1888
+ const LANGUAGE_OPTIONS = LanguagesList__default.default.getAllCodes().map((code) => ({
1889
+ value: code,
1890
+ label: LanguagesList__default.default.getNativeName(code)
1891
+ }));
1892
+ function EditCaptionDialog({ asset, track, onUpdate, onClose }) {
1893
+ const client = useClient(), toast = ui.useToast(), dialogId = `EditCaptionDialog${React.useId()}`, isAutogenerated = track.text_source === "generated_live" || track.text_source === "generated_live_final" || track.text_source === "generated_vod", [vttUrl, setVttUrl] = React.useState(""), [languageCode, setLanguageCode] = React.useState(track.language_code || ""), [selectedLanguage, setSelectedLanguage] = React.useState(
1894
+ () => {
1895
+ const baseCode = track.language_code?.split("-")[0], found = LANGUAGE_OPTIONS.find(
1896
+ (opt) => opt.value === track.language_code || opt.value === baseCode
1897
+ );
1898
+ if (found) return found;
1899
+ if (track.name) {
1900
+ const foundByName = LANGUAGE_OPTIONS.find((opt) => opt.label === track.name);
1901
+ if (foundByName) return foundByName;
1902
+ }
1903
+ return null;
1904
+ }
1905
+ ), [name2, setName] = React.useState(track.name || ""), [isSubmitting, setIsSubmitting] = React.useState(!1), [downloading, setDownloading] = React.useState(!1), [selectedFile, setSelectedFile] = React.useState(null), fileInputRef = React.useRef(null);
1906
+ React.useEffect(() => {
1907
+ setLanguageCode(track.language_code || ""), setName(track.name || ""), setVttUrl("");
1908
+ const baseCode = track.language_code?.split("-")[0], foundByCode = LANGUAGE_OPTIONS.find(
1909
+ (opt) => opt.value === track.language_code || opt.value === baseCode
1910
+ ), foundByName = track.name ? LANGUAGE_OPTIONS.find((opt) => opt.label === track.name) : null;
1911
+ setSelectedLanguage(foundByCode || foundByName || null);
1912
+ }, [track, asset, client]);
1913
+ const handleDownloadCurrentFile = async () => {
1914
+ setDownloading(!0);
1915
+ try {
1916
+ await downloadVttFile(client, asset, track);
1917
+ } catch (error) {
1918
+ let errorMessage = "Please try again", title = "Failed to download VTT file";
1919
+ error instanceof Error ? (errorMessage = error.message, error.message.includes("Track") && (title = "Cannot download")) : (error === "Track ID is missing" || error === "Track is not ready yet") && (errorMessage = String(error), title = "Cannot download"), toast.push({
1920
+ title,
1921
+ status: "error",
1922
+ description: errorMessage
1923
+ });
1924
+ } finally {
1925
+ setDownloading(!1);
1926
+ }
1927
+ }, getCurrentFileName = () => track.id && asset.filename ? `${asset.filename}-${track.language_code || "en"}.vtt` : `captions-${track.language_code || "en"}.vtt`, uploadVttFile = async (file) => (await client.assets.upload("file", file, {
1928
+ filename: file.name
1929
+ })).url, refreshAssetData = async () => {
1930
+ if (!(!asset._id || !asset.assetId))
1931
+ try {
1932
+ const latestAssetData = await getAsset(client, asset.assetId);
1933
+ await client.patch(asset._id).set({ data: latestAssetData.data, status: latestAssetData.data.status }).commit();
1934
+ } catch (refreshError) {
1935
+ console.error("Failed to refresh asset data:", refreshError);
1936
+ }
1937
+ }, handleUpdateTrackWithNewUrl = async () => {
1938
+ if (!asset.assetId)
1939
+ throw new Error("Asset ID is required");
1940
+ const trimmedName = name2.trim(), trimmedLanguageCode = languageCode.trim(), oldTrackId = track.id;
1941
+ try {
1942
+ await deleteTextTrack(client, asset.assetId, oldTrackId);
1943
+ } catch (deleteError) {
1944
+ throw toast.push({
1945
+ title: "Failed to delete old track",
1946
+ status: "error",
1947
+ description: "Could not delete the old track. Please try again or delete it manually."
1948
+ }), setIsSubmitting(!1), deleteError;
1949
+ }
1950
+ let vttUrlToUse = vttUrl.trim();
1951
+ if (selectedFile)
1952
+ try {
1953
+ vttUrlToUse = await uploadVttFile(selectedFile);
1954
+ } catch (uploadError) {
1955
+ throw toast.push({
1956
+ title: "Failed to upload VTT file",
1957
+ status: "error",
1958
+ description: "Could not upload the VTT file to Sanity. Please try again."
1959
+ }), setIsSubmitting(!1), uploadError;
1960
+ }
1961
+ try {
1962
+ await addTextTrackFromUrl(client, asset.assetId, vttUrlToUse, {
1963
+ language_code: trimmedLanguageCode,
1964
+ name: trimmedName,
1965
+ text_type: "subtitles"
1966
+ });
1967
+ } catch (error) {
1968
+ throw toast.push({
1969
+ title: "Failed to update caption track",
1970
+ status: "error",
1971
+ description: extractErrorMessage(error, "Failed to update caption track")
1972
+ }), setIsSubmitting(!1), error;
1973
+ }
1974
+ const result = await pollTrackStatus({
1975
+ client,
1976
+ assetId: asset.assetId,
1977
+ trackName: trimmedName,
1978
+ trackLanguageCode: trimmedLanguageCode,
1979
+ onTrackErrored: async (erroredTrack) => {
1980
+ const errorMessage = erroredTrack.error?.messages?.[0] || erroredTrack.error?.type || "The track failed to download from the provided URL";
1981
+ toast.push({
1982
+ title: "Caption track failed",
1983
+ status: "error",
1984
+ description: errorMessage
1985
+ }), await refreshAssetData(), onUpdate(erroredTrack, oldTrackId), setIsSubmitting(!1);
1986
+ }
1987
+ });
1988
+ if (!result.found || !result.track) {
1989
+ toast.push({
1990
+ title: "Caption track may have been updated",
1991
+ status: "warning",
1992
+ description: "The track was updated but its status could not be determined. It may still be processing. Please refresh the page to see if it appears."
1993
+ }), setIsSubmitting(!1);
1994
+ return;
1995
+ }
1996
+ result.status !== "errored" && (await refreshAssetData(), result.status === "preparing" ? toast.push({
1997
+ title: "Caption track is processing",
1998
+ status: "info",
1999
+ description: "The track was updated and is being processed. It will appear in the list shortly."
2000
+ }) : toast.push({
2001
+ title: "Caption track updated",
2002
+ status: "success",
2003
+ description: "Caption track updated successfully"
2004
+ }), onUpdate(result.track, oldTrackId), setIsSubmitting(!1));
2005
+ }, handleSubmit = async () => {
2006
+ if (!name2.trim()) {
2007
+ toast.push({
2008
+ title: "Audio name required",
2009
+ status: "error",
2010
+ description: "Please enter an audio name for this caption track"
2011
+ });
2012
+ return;
2013
+ }
2014
+ if (!languageCode.trim()) {
2015
+ toast.push({
2016
+ title: "Language code required",
2017
+ status: "error",
2018
+ description: "Please enter a language code (e.g., en, es, fr)"
2019
+ });
2020
+ return;
2021
+ }
2022
+ setIsSubmitting(!0);
2023
+ try {
2024
+ if (!asset.assetId)
2025
+ throw new Error("Asset ID is required");
2026
+ const originalVttUrl = (() => {
2027
+ if (isAutogenerated || !track.id) return "";
2028
+ const playbackId = getPlaybackId(asset);
2029
+ if (!playbackId) return "";
2030
+ let url = `https://stream.mux.com/${playbackId}/text/${track.id}.vtt`;
2031
+ if (getPlaybackPolicy(asset) === "signed") {
2032
+ const token = generateJwt(client, playbackId, "v");
2033
+ url += `?token=${token}`;
2034
+ }
2035
+ return url;
2036
+ })(), urlChanged = selectedFile !== null || vttUrl.trim() && vttUrl.trim() !== originalVttUrl;
2037
+ if (!urlChanged) {
2038
+ toast.push({
2039
+ title: "No changes",
2040
+ status: "info",
2041
+ description: 'Please provide a new VTT file or URL using the "Replace" button or URL field to update the track.'
2042
+ }), setIsSubmitting(!1);
2043
+ return;
2044
+ }
2045
+ if (urlChanged) {
2046
+ if (!selectedFile && vttUrl.trim())
2047
+ try {
2048
+ new URL(vttUrl.trim());
2049
+ } catch {
2050
+ toast.push({
2051
+ title: "Invalid URL",
2052
+ status: "error",
2053
+ description: "Please enter a valid URL (e.g., https://example.com/subtitles.vtt)"
2054
+ }), setIsSubmitting(!1);
2055
+ return;
2056
+ }
2057
+ if (!selectedFile && !vttUrl.trim()) {
2058
+ toast.push({
2059
+ title: "VTT file or URL required",
2060
+ status: "error",
2061
+ description: "Please select a VTT file or enter a VTT file URL"
2062
+ }), setIsSubmitting(!1);
2063
+ return;
2064
+ }
2065
+ await handleUpdateTrackWithNewUrl();
2066
+ }
2067
+ onClose();
2068
+ } catch (error) {
2069
+ toast.push({
2070
+ title: "Failed to update caption track",
2071
+ status: "error",
2072
+ description: error instanceof Error ? error.message : "Please try again"
2073
+ });
2074
+ } finally {
2075
+ setIsSubmitting(!1);
2076
+ }
2077
+ };
2078
+ return /* @__PURE__ */ jsxRuntime.jsx(
2079
+ ui.Dialog,
2080
+ {
2081
+ id: dialogId,
2082
+ header: "Edit Caption Track",
2083
+ onClose,
2084
+ width: 1,
2085
+ onClickOutside: onClose,
2086
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { padding: 4, space: 4, children: [
2087
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
2088
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Card, { padding: 3, marginBottom: 2, tone: "transparent", border: !0, radius: 2, children: [
2089
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", justify: "space-between", children: [
2090
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: getCurrentFileName() }),
2091
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 2, children: [
2092
+ track.status !== "errored" && /* @__PURE__ */ jsxRuntime.jsx(
2093
+ ui.Button,
2094
+ {
2095
+ icon: downloading ? /* @__PURE__ */ jsxRuntime.jsx(
2096
+ ui.Spinner,
2097
+ {
2098
+ style: {
2099
+ verticalAlign: "middle",
2100
+ display: "inline-block",
2101
+ marginTop: "-2px",
2102
+ width: "0.5em",
2103
+ height: "0.5em"
2104
+ }
2105
+ }
2106
+ ) : /* @__PURE__ */ jsxRuntime.jsx(icons.DownloadIcon, {}),
2107
+ text: "Download",
2108
+ mode: "ghost",
2109
+ tone: "primary",
2110
+ fontSize: 1,
2111
+ padding: 2,
2112
+ onClick: handleDownloadCurrentFile,
2113
+ disabled: downloading || isSubmitting
2114
+ }
2115
+ ),
2116
+ /* @__PURE__ */ jsxRuntime.jsx(
2117
+ ui.Button,
2118
+ {
2119
+ icon: icons.UploadIcon,
2120
+ text: "Replace",
2121
+ mode: "ghost",
2122
+ tone: "primary",
2123
+ fontSize: 1,
2124
+ padding: 2,
2125
+ onClick: () => fileInputRef.current?.click(),
2126
+ disabled: isSubmitting
2127
+ }
2128
+ )
2129
+ ] })
2130
+ ] }),
2131
+ /* @__PURE__ */ jsxRuntime.jsx(
2132
+ "input",
2133
+ {
2134
+ ref: fileInputRef,
2135
+ type: "file",
2136
+ accept: ".vtt,text/vtt",
2137
+ style: { display: "none" },
2138
+ onChange: (e) => {
2139
+ e.target.files && e.target.files.length > 0 && !isSubmitting && (setSelectedFile(e.target.files[0]), setVttUrl(""));
2140
+ }
2141
+ }
2142
+ ),
2143
+ selectedFile && /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, muted: !0, style: { marginTop: 8 }, children: [
2144
+ "Selected: ",
2145
+ selectedFile.name
2146
+ ] })
2147
+ ] }),
2148
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
2149
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vtt-url", children: "VTT File URL" }),
2150
+ /* @__PURE__ */ jsxRuntime.jsx(
2151
+ ui.TextInput,
2152
+ {
2153
+ id: "vtt-url",
2154
+ placeholder: "https://example.com/subtitles.vtt",
2155
+ value: vttUrl,
2156
+ onChange: (e) => {
2157
+ setVttUrl(e.currentTarget.value), setSelectedFile(null);
2158
+ },
2159
+ disabled: isSubmitting
2160
+ }
2161
+ ),
2162
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, children: "Add a URL to replace the existing VTT file with a new one" })
2163
+ ] })
2164
+ ] }),
2165
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
2166
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "caption-name", children: "Audio name" }),
2167
+ /* @__PURE__ */ jsxRuntime.jsx(
2168
+ ui.Autocomplete,
2169
+ {
2170
+ id: "caption-name",
2171
+ value: selectedLanguage?.value || "",
2172
+ onChange: (newValue) => {
2173
+ const selected = LANGUAGE_OPTIONS.find((opt) => opt.value === newValue);
2174
+ selected && (setSelectedLanguage(selected), setLanguageCode(selected.value), setName(selected.label));
2175
+ },
2176
+ options: LANGUAGE_OPTIONS,
2177
+ icon: icons.TranslateIcon,
2178
+ placeholder: "Select language",
2179
+ filterOption: (query, option) => option.label.toLowerCase().indexOf(query.toLowerCase()) > -1 || option.value.toLowerCase().indexOf(query.toLowerCase()) > -1,
2180
+ openButton: !0,
2181
+ renderValue: (value) => LANGUAGE_OPTIONS.find((l) => l.value === value)?.label || value,
2182
+ renderOption: (option) => /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { "data-as": "button", padding: 3, radius: 2, tone: "inherit", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 2, textOverflow: "ellipsis", children: [
2183
+ option.label,
2184
+ " (",
2185
+ option.value,
2186
+ ")"
2187
+ ] }) }),
2188
+ disabled: isSubmitting
2189
+ }
2190
+ )
2191
+ ] }),
2192
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
2193
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "caption-language", children: "Language Code" }),
2194
+ /* @__PURE__ */ jsxRuntime.jsx(
2195
+ ui.TextInput,
2196
+ {
2197
+ id: "caption-language",
2198
+ placeholder: "en-US",
2199
+ value: languageCode,
2200
+ onChange: (e) => {
2201
+ setLanguageCode(e.currentTarget.value), selectedLanguage && selectedLanguage.value !== e.currentTarget.value && (setSelectedLanguage(null), (!name2 || name2 === selectedLanguage.label) && setName(""));
2202
+ },
2203
+ disabled: isSubmitting
2204
+ }
2205
+ )
2206
+ ] }),
2207
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 2, justify: "flex-end", marginTop: 2, children: [
2208
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { text: "Cancel", mode: "ghost", onClick: onClose, disabled: isSubmitting }),
2209
+ /* @__PURE__ */ jsxRuntime.jsx(
2210
+ ui.Button,
2211
+ {
2212
+ text: "Update Caption Track",
2213
+ tone: "primary",
2214
+ icon: isSubmitting ? /* @__PURE__ */ jsxRuntime.jsx(
2215
+ ui.Spinner,
2216
+ {
2217
+ style: {
2218
+ verticalAlign: "middle",
2219
+ display: "inline-block",
2220
+ marginBottom: "-3px",
2221
+ width: "1em",
2222
+ height: "1em",
2223
+ marginRight: "-6px"
2224
+ }
2225
+ }
2226
+ ) : icons.UploadIcon,
2227
+ onClick: handleSubmit,
2228
+ disabled: isSubmitting
2229
+ }
2230
+ )
2231
+ ] })
2232
+ ] })
2233
+ }
2234
+ );
2235
+ }
2236
+ function TrackCard({
2237
+ track,
2238
+ iconOnly,
2239
+ downloadingTrackId,
2240
+ deletingTrackId,
2241
+ trackToEdit,
2242
+ getTrackSourceLabel,
2243
+ handleDownload,
2244
+ setTrackToEdit,
2245
+ setTrackToDelete
2246
+ }) {
2247
+ const isDisabled = (action) => action === "download" ? downloadingTrackId !== null || deletingTrackId === track.id || trackToEdit?.id === track.id : action === "edit" ? downloadingTrackId === track.id || deletingTrackId === track.id || trackToEdit?.id === track.id : downloadingTrackId === track.id || deletingTrackId !== null || trackToEdit?.id === track.id, renderActionButtons = () => track.status === "preparing" ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
2248
+ /* @__PURE__ */ jsxRuntime.jsx(
2249
+ ui.Spinner,
2250
+ {
2251
+ muted: !0,
2252
+ style: {
2253
+ width: "0.75em",
2254
+ height: "0.75em",
2255
+ verticalAlign: "middle",
2256
+ display: "inline-block",
2257
+ marginBottom: "-2px"
2258
+ }
2259
+ }
2260
+ ),
2261
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, children: "Processing..." })
2262
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 2, children: [
2263
+ track.status !== "errored" && /* @__PURE__ */ jsxRuntime.jsx(
2264
+ ui.Button,
2265
+ {
2266
+ icon: downloadingTrackId === track.id ? /* @__PURE__ */ jsxRuntime.jsx(
2267
+ ui.Spinner,
2268
+ {
2269
+ style: {
2270
+ verticalAlign: "middle",
2271
+ display: "inline-block",
2272
+ marginTop: "-2px",
2273
+ width: "0.5em",
2274
+ height: "0.5em"
2275
+ }
2276
+ }
2277
+ ) : /* @__PURE__ */ jsxRuntime.jsx(icons.DownloadIcon, {}),
2278
+ text: iconOnly ? void 0 : "Download",
2279
+ mode: "ghost",
2280
+ tone: "primary",
2281
+ fontSize: 1,
2282
+ padding: 2,
2283
+ onClick: () => handleDownload(track),
2284
+ disabled: isDisabled("download"),
2285
+ title: "Download"
2286
+ }
2287
+ ),
2288
+ /* @__PURE__ */ jsxRuntime.jsx(
2289
+ ui.Button,
2290
+ {
2291
+ icon: /* @__PURE__ */ jsxRuntime.jsx(icons.EditIcon, {}),
2292
+ text: iconOnly ? void 0 : "Edit",
2293
+ mode: "ghost",
2294
+ tone: "primary",
2295
+ fontSize: 1,
2296
+ padding: 2,
2297
+ disabled: isDisabled("edit"),
2298
+ onClick: () => setTrackToEdit(track),
2299
+ title: "Edit"
2300
+ }
2301
+ ),
2302
+ /* @__PURE__ */ jsxRuntime.jsx(
2303
+ ui.Button,
2304
+ {
2305
+ icon: deletingTrackId === track.id ? /* @__PURE__ */ jsxRuntime.jsx(
2306
+ ui.Spinner,
2307
+ {
2308
+ style: {
2309
+ verticalAlign: "middle",
2310
+ display: "inline-block",
2311
+ marginTop: "-2px",
2312
+ width: "0.5em",
2313
+ height: "0.5em"
2314
+ }
2315
+ }
2316
+ ) : /* @__PURE__ */ jsxRuntime.jsx(icons.TrashIcon, {}),
2317
+ text: iconOnly ? void 0 : "Delete",
2318
+ mode: "ghost",
2319
+ tone: "critical",
2320
+ fontSize: 1,
2321
+ padding: 2,
2322
+ disabled: isDisabled("delete"),
2323
+ onClick: () => setTrackToDelete(track),
2324
+ title: "Delete"
2325
+ }
2326
+ )
2327
+ ] });
2328
+ return /* @__PURE__ */ jsxRuntime.jsx(
2329
+ ui.Card,
1354
2330
  {
1355
- fill: "currentColor",
1356
- d: "M20 9V6h-3V4h5v5h-2ZM2 9V4h5v2H4v3H2Zm15 11v-2h3v-3h2v5h-5ZM2 20v-5h2v3h3v2H2Zm4-4V8h12v8H6Zm2-2h8v-4H8v4Zm0 0v-4v4Z"
2331
+ padding: 3,
2332
+ radius: 2,
2333
+ tone: track.status === "errored" ? "caution" : "transparent",
2334
+ border: !0,
2335
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", justify: "space-between", gap: 3, children: [
2336
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, flex: 1, children: [
2337
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
2338
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { weight: "semibold", children: track.name || "Untitled" }),
2339
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, muted: !0, children: [
2340
+ "(",
2341
+ getTrackSourceLabel(track),
2342
+ ")"
2343
+ ] }),
2344
+ track.status === "errored" && /* @__PURE__ */ jsxRuntime.jsx(
2345
+ icons.ErrorOutlineIcon,
2346
+ {
2347
+ style: { color: "var(--card-critical-color)" },
2348
+ "aria-label": "Error",
2349
+ fontSize: 20
2350
+ }
2351
+ )
2352
+ ] }),
2353
+ track.language_code && /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, muted: !0, children: [
2354
+ "Language: ",
2355
+ track.language_code
2356
+ ] }),
2357
+ track.status === "errored" && track.error && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, style: { color: "var(--card-critical-color)" }, children: track.error.messages?.[0] || track.error.type || "Failed to process track" })
2358
+ ] }),
2359
+ renderActionButtons()
2360
+ ] })
1357
2361
  }
1358
- ) });
2362
+ );
1359
2363
  }
1360
- function StopWatchIcon(props) {
1361
- return /* @__PURE__ */ jsxRuntime.jsxs(
1362
- "svg",
1363
- {
1364
- xmlns: "http://www.w3.org/2000/svg",
1365
- width: "1em",
1366
- height: "1em",
1367
- viewBox: "0 0 512 512",
1368
- ...props,
1369
- children: [
1370
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M232 306.667h48V176h-48v130.667z", fill: "currentColor" }),
1371
- /* @__PURE__ */ jsxRuntime.jsx(
1372
- "path",
1373
- {
1374
- d: "M407.67 170.271l30.786-30.786-33.942-33.941-30.785 30.786C341.217 111.057 300.369 96 256 96 149.961 96 64 181.961 64 288s85.961 192 192 192 192-85.961 192-192c0-44.369-15.057-85.217-40.33-117.729zm-45.604 223.795C333.734 422.398 296.066 438 256 438s-77.735-15.602-106.066-43.934C121.602 365.735 106 328.066 106 288s15.602-77.735 43.934-106.066C178.265 153.602 215.934 138 256 138s77.734 15.602 106.066 43.934C390.398 210.265 406 247.934 406 288s-15.602 77.735-43.934 106.066z",
1375
- fill: "currentColor"
2364
+ function TextTracksManager({
2365
+ asset,
2366
+ iconOnly = !1,
2367
+ tracks: propTracks,
2368
+ collapseTracks = !1
2369
+ }) {
2370
+ const client = useClient(), toast = ui.useToast(), dialogId = `DeleteCaptionDialog${React.useId()}`, [downloadingTrackId, setDownloadingTrackId] = React.useState(null), [deletingTrackId, setDeletingTrackId] = React.useState(null), [addedTracks, setAddedTracks] = React.useState([]), [updatedTracks, setUpdatedTracks] = React.useState(/* @__PURE__ */ new Map()), [trackActivityOrder, setTrackActivityOrder] = React.useState(/* @__PURE__ */ new Map()), [autogeneratedTrackIds, setAutogeneratedTrackIds] = React.useState(/* @__PURE__ */ new Set()), [trackToDelete, setTrackToDelete] = React.useState(null), [trackToEdit, setTrackToEdit] = React.useState(null), [showAddDialog, setShowAddDialog] = React.useState(!1), [isExpanded, setIsExpanded] = React.useState(!1), MAX_VISIBLE_TRACKS = 4;
2371
+ React.useEffect(() => {
2372
+ if (!asset.assetId || !asset._id) return;
2373
+ const assetId = asset.assetId, documentId = asset._id;
2374
+ (async () => {
2375
+ try {
2376
+ const response = await getAsset(client, assetId);
2377
+ await client.patch(documentId).set({ data: response.data, status: response.data.status }).commit();
2378
+ } catch (error) {
2379
+ console.error("Failed to refresh asset data:", error);
2380
+ }
2381
+ })();
2382
+ }, [asset.assetId, asset._id, client]);
2383
+ const activeTracks = (propTracks || asset.data?.tracks?.filter((track) => track.type === "text") || []).filter(
2384
+ (track) => track.id && (track.status === "ready" || track.status === "preparing" || track.status === "errored")
2385
+ ), allTracks = React.useMemo(() => {
2386
+ const tracksWithUpdates = activeTracks.map((track) => updatedTracks.get(track.id) || track), isMockTrackReplaced = (mockTrack, realTracksList) => !mockTrack.id || !mockTrack.id.startsWith("generating-") ? !1 : realTracksList.some((realTrack) => {
2387
+ const nameMatches = realTrack.name === mockTrack.name, languageMatches = realTrack.language_code === mockTrack.language_code;
2388
+ return !nameMatches || !languageMatches ? !1 : realTrack.status === "ready" ? realTrack.text_source === "generated_live" || realTrack.text_source === "generated_live_final" || realTrack.text_source === "generated_vod" : realTrack.status === "preparing";
2389
+ }), isTrackAlreadyInRealTracks = (addedTrack, realTracksList) => addedTrack.id ? addedTrack.id.startsWith("generating-") ? isMockTrackReplaced(addedTrack, realTracksList) : realTracksList.some((realTrack) => realTrack.id === addedTrack.id) : !1, tracksToKeep = addedTracks.filter((addedTrack) => addedTrack.id && addedTrack.id.startsWith("generating-") ? !isMockTrackReplaced(addedTrack, tracksWithUpdates) : !isTrackAlreadyInRealTracks(addedTrack, tracksWithUpdates));
2390
+ return [...tracksWithUpdates, ...tracksToKeep];
2391
+ }, [activeTracks, addedTracks, updatedTracks]);
2392
+ React.useEffect(() => {
2393
+ const newAutogeneratedIds = /* @__PURE__ */ new Set();
2394
+ activeTracks.forEach((track) => {
2395
+ track.id && (track.text_source === "generated_live" || track.text_source === "generated_live_final" || track.text_source === "generated_vod") && newAutogeneratedIds.add(track.id);
2396
+ }), addedTracks.forEach((mockTrack) => {
2397
+ if (mockTrack.id && mockTrack.id.startsWith("generating-")) {
2398
+ const realTrack = activeTracks.find((rt) => {
2399
+ const nameMatches = rt.name === mockTrack.name, languageMatches = rt.language_code === mockTrack.language_code;
2400
+ return nameMatches && languageMatches;
2401
+ });
2402
+ realTrack?.id && newAutogeneratedIds.add(realTrack.id);
2403
+ }
2404
+ }), setAutogeneratedTrackIds((prev) => {
2405
+ let hasNew = !1;
2406
+ const updated = new Set(prev);
2407
+ return newAutogeneratedIds.forEach((id) => {
2408
+ prev.has(id) || (updated.add(id), hasNew = !0);
2409
+ }), hasNew ? updated : prev;
2410
+ });
2411
+ }, [activeTracks, addedTracks]), React.useEffect(() => {
2412
+ if (allTracks.filter((track) => track.status === "preparing").length === 0 || !asset.assetId || !asset._id)
2413
+ return;
2414
+ const assetId = asset.assetId, documentId = asset._id, interval = setInterval(async () => {
2415
+ try {
2416
+ const response = await getAsset(client, assetId);
2417
+ await client.patch(documentId).set({ data: response.data, status: response.data.status }).commit();
2418
+ const fetchedTracks = response.data.tracks?.filter((track) => track.type === "text") || [], isMockTrackReplaced = (mockTrack, fetchedTracksList) => !mockTrack.id || !mockTrack.id.startsWith("generating-") ? !1 : fetchedTracksList.some((realTrack) => {
2419
+ const nameMatches = realTrack.name === mockTrack.name, languageMatches = realTrack.language_code === mockTrack.language_code;
2420
+ return !nameMatches || !languageMatches ? !1 : realTrack.status === "ready" ? realTrack.text_source === "generated_live" || realTrack.text_source === "generated_live_final" || realTrack.text_source === "generated_vod" : realTrack.status === "preparing";
2421
+ }), newAutogeneratedIds = /* @__PURE__ */ new Set();
2422
+ fetchedTracks.forEach((track) => {
2423
+ track.id && (track.text_source === "generated_live" || track.text_source === "generated_live_final" || track.text_source === "generated_vod") && newAutogeneratedIds.add(track.id);
2424
+ });
2425
+ const findMatchingRealTrack = (mockTrack, tracksList) => tracksList.find((rt) => {
2426
+ const nameMatches = rt.name === mockTrack.name, languageMatches = rt.language_code === mockTrack.language_code;
2427
+ return nameMatches && languageMatches;
2428
+ });
2429
+ setAddedTracks((prev) => prev.filter((mockTrack) => {
2430
+ if (mockTrack.id && mockTrack.id.startsWith("generating-")) {
2431
+ const replaced = isMockTrackReplaced(mockTrack, fetchedTracks);
2432
+ if (replaced) {
2433
+ const realTrack = findMatchingRealTrack(mockTrack, fetchedTracks);
2434
+ realTrack?.id && (newAutogeneratedIds.add(realTrack.id), setTrackActivityOrder((prevOrder) => {
2435
+ const mockOrder = prevOrder.get(mockTrack.id);
2436
+ if (mockOrder) {
2437
+ const newMap = new Map(prevOrder);
2438
+ return newMap.set(realTrack.id, mockOrder), newMap;
2439
+ }
2440
+ return prevOrder;
2441
+ }));
2442
+ }
2443
+ return !replaced;
1376
2444
  }
1377
- ),
1378
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M192 32h128v48H192z", fill: "currentColor" })
1379
- ]
2445
+ return !0;
2446
+ })), newAutogeneratedIds.size > 0 && setAutogeneratedTrackIds((prevIds) => {
2447
+ const updated = new Set(prevIds);
2448
+ return newAutogeneratedIds.forEach((id) => updated.add(id)), updated;
2449
+ });
2450
+ } catch (error) {
2451
+ console.error("Failed to refresh asset data:", error);
2452
+ }
2453
+ }, 3e3);
2454
+ return () => clearInterval(interval);
2455
+ }, [allTracks, asset.assetId, asset._id, client]);
2456
+ const visibleTracks = allTracks.filter(
2457
+ (track) => track.status === "ready" || track.status === "preparing" || track.status === "errored"
2458
+ ).sort((a2, b) => {
2459
+ const orderA = trackActivityOrder.get(a2.id) || 0, orderB = trackActivityOrder.get(b.id) || 0;
2460
+ if (orderA > 0 && orderB > 0)
2461
+ return orderB - orderA;
2462
+ if (orderA > 0) return -1;
2463
+ if (orderB > 0) return 1;
2464
+ const aIsPreparing = a2.status === "preparing", bIsPreparing = b.status === "preparing";
2465
+ if (aIsPreparing && !bIsPreparing) return -1;
2466
+ if (!aIsPreparing && bIsPreparing) return 1;
2467
+ const aIsAutogenerated = a2.id && a2.id.startsWith("generating-") || a2.id && autogeneratedTrackIds.has(a2.id), bIsAutogenerated = b.id && b.id.startsWith("generating-") || b.id && autogeneratedTrackIds.has(b.id);
2468
+ return aIsAutogenerated && !bIsAutogenerated ? -1 : !aIsAutogenerated && bIsAutogenerated ? 1 : 0;
2469
+ }), handleDownload = async (track) => {
2470
+ if (track.id) {
2471
+ setDownloadingTrackId(track.id);
2472
+ try {
2473
+ await downloadVttFile(client, asset, track);
2474
+ } catch (error) {
2475
+ toast.push({
2476
+ title: "Failed to download VTT file",
2477
+ status: "error",
2478
+ description: error instanceof Error ? error.message : "Please try again"
2479
+ });
2480
+ } finally {
2481
+ setDownloadingTrackId(null);
2482
+ }
1380
2483
  }
1381
- );
2484
+ }, confirmDelete = async () => {
2485
+ if (!trackToDelete || !trackToDelete.id) return;
2486
+ const track = trackToDelete;
2487
+ setTrackToDelete(null), setDeletingTrackId(track.id);
2488
+ try {
2489
+ if (!asset.assetId)
2490
+ throw new Error("Asset ID is required");
2491
+ if (await deleteTextTrack(client, asset.assetId, track.id), asset._id)
2492
+ try {
2493
+ const response = await getAsset(client, asset.assetId);
2494
+ await client.patch(asset._id).set({ data: response.data, status: response.data.status }).commit();
2495
+ } catch (refreshError) {
2496
+ console.error("Failed to refresh asset data:", refreshError);
2497
+ }
2498
+ toast.push({
2499
+ title: "Successfully deleted caption track",
2500
+ status: "success"
2501
+ }), setAddedTracks((prev) => prev.filter((t) => t.id !== track.id)), setUpdatedTracks((prev) => {
2502
+ const newMap = new Map(prev);
2503
+ return newMap.delete(track.id), newMap;
2504
+ }), setTrackActivityOrder((prev) => {
2505
+ const newMap = new Map(prev);
2506
+ return newMap.delete(track.id), newMap;
2507
+ }), setAutogeneratedTrackIds((prev) => {
2508
+ const updated = new Set(prev);
2509
+ return updated.delete(track.id), updated;
2510
+ });
2511
+ } catch (error) {
2512
+ toast.push({
2513
+ title: "Failed to delete caption track",
2514
+ status: "error",
2515
+ description: error instanceof Error ? error.message : "Please try again"
2516
+ });
2517
+ } finally {
2518
+ setDeletingTrackId(null);
2519
+ }
2520
+ }, handleAddTrack = (track) => {
2521
+ setAddedTracks((prev) => [...prev, track]), setTrackActivityOrder((prev) => {
2522
+ const newMap = new Map(prev);
2523
+ return newMap.set(track.id, prev.size + 1), newMap;
2524
+ }), setShowAddDialog(!1);
2525
+ }, handleUpdateTrack = async (updatedTrack, oldTrackId) => {
2526
+ if (oldTrackId && (setAddedTracks((prev) => prev.filter((t) => t.id !== oldTrackId)), setUpdatedTracks((prev) => {
2527
+ const newMap = new Map(prev);
2528
+ return newMap.delete(oldTrackId), newMap;
2529
+ }), setTrackActivityOrder((prev) => {
2530
+ const newMap = new Map(prev);
2531
+ return newMap.delete(oldTrackId), newMap;
2532
+ }), setAutogeneratedTrackIds((prev) => {
2533
+ const updated = new Set(prev);
2534
+ return updated.delete(oldTrackId), updated;
2535
+ })), addedTracks.some((t) => t.id === updatedTrack.id) ? setAddedTracks((prev) => prev.map((t) => t.id === updatedTrack.id ? updatedTrack : t)) : setUpdatedTracks((prev) => {
2536
+ const newMap = new Map(prev);
2537
+ return newMap.set(updatedTrack.id, updatedTrack), newMap;
2538
+ }), setTrackActivityOrder((prev) => {
2539
+ const newMap = new Map(prev);
2540
+ return newMap.set(updatedTrack.id, prev.size + 1), newMap;
2541
+ }), setTrackToEdit(null), asset._id && asset.assetId)
2542
+ try {
2543
+ const response = await getAsset(client, asset.assetId);
2544
+ await client.patch(asset._id).set({ data: response.data, status: response.data.status }).commit();
2545
+ } catch (refreshError) {
2546
+ console.error("Failed to refresh asset data:", refreshError);
2547
+ }
2548
+ }, getTrackSourceLabel = (track) => track.id && track.id.startsWith("generating-") || track.id && autogeneratedTrackIds.has(track.id) || track.text_source === "generated_live_final" || track.text_source === "generated_live" || track.text_source === "generated_vod" ? "Auto-generated" : track.text_source === "uploaded" ? "Uploaded" : "Custom";
2549
+ if (visibleTracks.length === 0 && !showAddDialog)
2550
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
2551
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { justify: "flex-end", children: /* @__PURE__ */ jsxRuntime.jsx(
2552
+ ui.Button,
2553
+ {
2554
+ icon: icons.AddIcon,
2555
+ text: "Add Caption",
2556
+ tone: "primary",
2557
+ onClick: () => setShowAddDialog(!0)
2558
+ }
2559
+ ) }),
2560
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 4, radius: 2, tone: "transparent", border: !0, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, children: "No captions available. Add captions when uploading a video or add them manually." }) }),
2561
+ showAddDialog && /* @__PURE__ */ jsxRuntime.jsx(
2562
+ AddCaptionDialog,
2563
+ {
2564
+ asset,
2565
+ onAdd: handleAddTrack,
2566
+ onClose: () => setShowAddDialog(!1)
2567
+ }
2568
+ )
2569
+ ] });
2570
+ const displayedTracks = collapseTracks && !isExpanded ? visibleTracks.slice(0, MAX_VISIBLE_TRACKS) : visibleTracks, hasMoreTracks = collapseTracks && visibleTracks.length > MAX_VISIBLE_TRACKS;
2571
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
2572
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { justify: "flex-end", children: /* @__PURE__ */ jsxRuntime.jsx(
2573
+ ui.Button,
2574
+ {
2575
+ icon: icons.AddIcon,
2576
+ text: "Add Caption",
2577
+ tone: "primary",
2578
+ onClick: () => setShowAddDialog(!0)
2579
+ }
2580
+ ) }),
2581
+ displayedTracks.map((track) => /* @__PURE__ */ jsxRuntime.jsx(
2582
+ TrackCard,
2583
+ {
2584
+ track,
2585
+ iconOnly,
2586
+ downloadingTrackId,
2587
+ deletingTrackId,
2588
+ trackToEdit,
2589
+ getTrackSourceLabel,
2590
+ handleDownload,
2591
+ setTrackToEdit,
2592
+ setTrackToDelete
2593
+ },
2594
+ track.id
2595
+ )),
2596
+ hasMoreTracks && /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { justify: "center", children: /* @__PURE__ */ jsxRuntime.jsx(
2597
+ ui.Button,
2598
+ {
2599
+ icon: isExpanded ? icons.ChevronUpIcon : icons.ChevronDownIcon,
2600
+ text: isExpanded ? "Show less" : `Show ${visibleTracks.length - MAX_VISIBLE_TRACKS} more`,
2601
+ mode: "ghost",
2602
+ tone: "primary",
2603
+ onClick: () => setIsExpanded(!isExpanded)
2604
+ }
2605
+ ) }),
2606
+ trackToDelete && /* @__PURE__ */ jsxRuntime.jsx(
2607
+ ui.Dialog,
2608
+ {
2609
+ animate: !0,
2610
+ id: dialogId,
2611
+ header: "Delete track",
2612
+ onClose: () => setTrackToDelete(null),
2613
+ onClickOutside: () => setTrackToDelete(null),
2614
+ width: 1,
2615
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2616
+ ui.Card,
2617
+ {
2618
+ padding: 3,
2619
+ style: {
2620
+ minHeight: "150px",
2621
+ display: "flex",
2622
+ alignItems: "center",
2623
+ justifyContent: "center"
2624
+ },
2625
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
2626
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Heading, { size: 2, children: [
2627
+ 'Are you sure you want to delete "',
2628
+ trackToDelete.name || trackToDelete.language_code || "Untitled",
2629
+ '"?'
2630
+ ] }),
2631
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, children: "This action is irreversible" }),
2632
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 4, marginY: 4, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(
2633
+ ui.Button,
2634
+ {
2635
+ icon: deletingTrackId === trackToDelete.id ? /* @__PURE__ */ jsxRuntime.jsx(
2636
+ ui.Spinner,
2637
+ {
2638
+ style: {
2639
+ verticalAlign: "middle",
2640
+ display: "inline-block",
2641
+ marginTop: "-2px",
2642
+ width: "0.5em",
2643
+ height: "0.5em"
2644
+ }
2645
+ }
2646
+ ) : /* @__PURE__ */ jsxRuntime.jsx(icons.TrashIcon, {}),
2647
+ fontSize: 2,
2648
+ padding: 3,
2649
+ text: "Delete track",
2650
+ tone: "critical",
2651
+ onClick: confirmDelete,
2652
+ disabled: deletingTrackId !== null
2653
+ }
2654
+ ) }) })
2655
+ ] })
2656
+ }
2657
+ )
2658
+ }
2659
+ ),
2660
+ showAddDialog && /* @__PURE__ */ jsxRuntime.jsx(
2661
+ AddCaptionDialog,
2662
+ {
2663
+ asset,
2664
+ onAdd: handleAddTrack,
2665
+ onClose: () => setShowAddDialog(!1)
2666
+ }
2667
+ ),
2668
+ trackToEdit && /* @__PURE__ */ jsxRuntime.jsx(
2669
+ EditCaptionDialog,
2670
+ {
2671
+ asset,
2672
+ track: trackToEdit,
2673
+ onUpdate: handleUpdateTrack,
2674
+ onClose: () => setTrackToEdit(null)
2675
+ }
2676
+ )
2677
+ ] });
1382
2678
  }
1383
2679
  const DialogStateContext = React.createContext({
1384
2680
  dialogState: !1,
@@ -1396,6 +2692,10 @@ function getVideoSrc({ asset, client }) {
1396
2692
  }
1397
2693
  return `https://stream.mux.com/${playbackId}.m3u8?${searchParams}`;
1398
2694
  }
2695
+ function CaptionsDialog({ asset }) {
2696
+ const { setDialogState } = useDialogStateContext(), dialogId = `CaptionsDialog${React.useId()}`;
2697
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Dialog, { id: dialogId, header: "Edit Captions", onClose: () => setDialogState(!1), width: 1, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { padding: 4, children: /* @__PURE__ */ jsxRuntime.jsx(TextTracksManager, { asset }) }) });
2698
+ }
1399
2699
  function getDevicePixelRatio(options) {
1400
2700
  const {
1401
2701
  defaultDpr = 1,
@@ -1483,10 +2783,21 @@ function EditThumbnailDialog({ asset, currentTime = 0 }) {
1483
2783
  }
1484
2784
  );
1485
2785
  }
2786
+ function AudioIcon(props) {
2787
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "1em", height: "1em", viewBox: "0 0 24 24", ...props, children: /* @__PURE__ */ jsxRuntime.jsx(
2788
+ "path",
2789
+ {
2790
+ fill: "currentColor",
2791
+ style: { opacity: "0.65" },
2792
+ 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"
2793
+ }
2794
+ ) });
2795
+ }
1486
2796
  function VideoPlayer({
1487
2797
  asset,
1488
2798
  thumbnailWidth = 250,
1489
2799
  children,
2800
+ hlsConfig,
1490
2801
  ...props
1491
2802
  }) {
1492
2803
  const client = useClient(), { dialogState } = useDialogStateContext(), isAudio = assetIsAudio(asset), muxPlayer = React.useRef(null), {
@@ -1512,53 +2823,81 @@ function VideoPlayer({
1512
2823
  // Make it wider when forcing aspect ratio to balance with videos' rendering height (audio players overflow a bit)
1513
2824
  props.forceAspectRatio * 1.2
1514
2825
  ) : AUDIO_ASPECT_RATIO), /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1515
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Card, { tone: "transparent", style: { aspectRatio, position: "relative" }, children: [
1516
- videoSrc && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1517
- /* @__PURE__ */ jsxRuntime.jsx(
1518
- MuxPlayer__default.default,
1519
- {
1520
- poster: thumbnailSrc,
1521
- ref: muxPlayer,
1522
- ...props,
1523
- playsInline: !0,
1524
- playbackId: asset.playbackId,
1525
- tokens: signedToken ? { playback: signedToken, thumbnail: signedToken, storyboard: signedToken } : void 0,
1526
- preload: "metadata",
1527
- crossOrigin: "anonymous",
1528
- metadata: {
1529
- player_name: "Sanity Admin Dashboard",
1530
- player_version: "2.12.1",
1531
- page_type: "Preview Player"
1532
- },
1533
- audio: isAudio,
1534
- style: {
1535
- height: "100%",
1536
- width: "100%",
1537
- display: "block",
1538
- objectFit: "contain"
2826
+ /* @__PURE__ */ jsxRuntime.jsxs(
2827
+ ui.Card,
2828
+ {
2829
+ tone: "transparent",
2830
+ style: {
2831
+ aspectRatio,
2832
+ position: "relative",
2833
+ ...isAudio && { display: "flex", alignItems: "flex-end" }
2834
+ },
2835
+ children: [
2836
+ videoSrc && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2837
+ isAudio && /* @__PURE__ */ jsxRuntime.jsx(
2838
+ AudioIcon,
2839
+ {
2840
+ style: {
2841
+ padding: "0.5em",
2842
+ width: "2.2em",
2843
+ height: "2.2em",
2844
+ position: "absolute",
2845
+ top: 0,
2846
+ left: 0,
2847
+ zIndex: 1
2848
+ }
2849
+ }
2850
+ ),
2851
+ /* @__PURE__ */ jsxRuntime.jsx(
2852
+ MuxPlayer__default.default,
2853
+ {
2854
+ poster: isAudio ? void 0 : thumbnailSrc,
2855
+ ref: muxPlayer,
2856
+ ...props,
2857
+ playsInline: !0,
2858
+ playbackId: asset.playbackId,
2859
+ tokens: signedToken ? { playback: signedToken, thumbnail: signedToken, storyboard: signedToken } : void 0,
2860
+ preload: "metadata",
2861
+ crossOrigin: "anonymous",
2862
+ metadata: {
2863
+ player_name: "Sanity Admin Dashboard",
2864
+ player_version: "2.14.0",
2865
+ page_type: "Preview Player"
2866
+ },
2867
+ audio: isAudio,
2868
+ _hlsConfig: hlsConfig,
2869
+ style: {
2870
+ ...!isAudio && { height: "100%" },
2871
+ width: "100%",
2872
+ display: "block",
2873
+ objectFit: "contain",
2874
+ ...isAudio && { alignSelf: "end" }
2875
+ }
2876
+ }
2877
+ ),
2878
+ children
2879
+ ] }),
2880
+ error ? /* @__PURE__ */ jsxRuntime.jsx(
2881
+ "div",
2882
+ {
2883
+ style: {
2884
+ position: "absolute",
2885
+ top: "50%",
2886
+ left: "50%",
2887
+ transform: "translate(-50%, -50%)"
2888
+ },
2889
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { muted: !0, children: [
2890
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ErrorOutlineIcon, { style: { marginRight: "0.15em" } }),
2891
+ typeof error == "object" && "message" in error && typeof error.message == "string" ? error.message : "Error loading video"
2892
+ ] })
1539
2893
  }
1540
- }
1541
- ),
1542
- children
1543
- ] }),
1544
- error ? /* @__PURE__ */ jsxRuntime.jsx(
1545
- "div",
1546
- {
1547
- style: {
1548
- position: "absolute",
1549
- top: "50%",
1550
- left: "50%",
1551
- transform: "translate(-50%, -50%)"
1552
- },
1553
- children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { muted: !0, children: [
1554
- /* @__PURE__ */ jsxRuntime.jsx(icons.ErrorOutlineIcon, { style: { marginRight: "0.15em" } }),
1555
- typeof error == "object" && "message" in error && typeof error.message == "string" ? error.message : "Error loading video"
1556
- ] })
1557
- }
1558
- ) : null,
1559
- children
1560
- ] }),
1561
- dialogState === "edit-thumbnail" && /* @__PURE__ */ jsxRuntime.jsx(EditThumbnailDialog, { asset, currentTime: muxPlayer?.current?.currentTime })
2894
+ ) : null,
2895
+ children
2896
+ ]
2897
+ }
2898
+ ),
2899
+ dialogState === "edit-thumbnail" && /* @__PURE__ */ jsxRuntime.jsx(EditThumbnailDialog, { asset, currentTime: muxPlayer?.current?.currentTime }),
2900
+ dialogState === "edit-captions" && /* @__PURE__ */ jsxRuntime.jsx(CaptionsDialog, { asset })
1562
2901
  ] });
1563
2902
  }
1564
2903
  function assetIsAudio(asset) {
@@ -1830,7 +3169,8 @@ function getVideoMetadata(doc) {
1830
3169
  duration: doc.data?.duration ? formatSeconds(doc.data?.duration) : void 0,
1831
3170
  aspect_ratio: doc.data?.aspect_ratio,
1832
3171
  max_stored_resolution: doc.data?.max_stored_resolution,
1833
- max_stored_frame_rate: doc.data?.max_stored_frame_rate
3172
+ max_stored_frame_rate: doc.data?.max_stored_frame_rate,
3173
+ text_tracks: doc.data?.tracks?.filter((track) => track.type === "text") || []
1834
3174
  };
1835
3175
  }
1836
3176
  function useVideoDetails(props) {
@@ -2022,7 +3362,20 @@ const AssetInput = (props) => /* @__PURE__ */ jsxRuntime.jsx(FormField$1, { titl
2022
3362
  minHeight: containerHeight
2023
3363
  } : void 0,
2024
3364
  children: [
2025
- /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 4, flex: 1, sizing: "border", children: /* @__PURE__ */ jsxRuntime.jsx(VideoPlayer, { asset: props.asset, autoPlay: props.asset.autoPlay || !1 }) }),
3365
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, flex: 1, sizing: "border", children: [
3366
+ /* @__PURE__ */ jsxRuntime.jsx(VideoPlayer, { asset: props.asset, autoPlay: props.asset.autoPlay || !1 }),
3367
+ tab === "details" && /* @__PURE__ */ jsxRuntime.jsx(
3368
+ TextTracksManager,
3369
+ {
3370
+ asset: props.asset,
3371
+ iconOnly: !0,
3372
+ collapseTracks: !0,
3373
+ tracks: displayInfo?.text_tracks || props.asset.data?.tracks?.filter(
3374
+ (track) => track.type === "text"
3375
+ ) || []
3376
+ }
3377
+ )
3378
+ ] }),
2026
3379
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, flex: 1, sizing: "border", children: [
2027
3380
  /* @__PURE__ */ jsxRuntime.jsxs(ui.TabList, { space: 2, children: [
2028
3381
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -2300,14 +3653,7 @@ function VideoInBrowser({
2300
3653
  alignItems: "center",
2301
3654
  justifyContent: "center"
2302
3655
  },
2303
- children: /* @__PURE__ */ jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "3em", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntime.jsx(
2304
- "path",
2305
- {
2306
- fill: "currentColor",
2307
- style: { opacity: "0.65" },
2308
- 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"
2309
- }
2310
- ) })
3656
+ children: /* @__PURE__ */ jsxRuntime.jsx(AudioIcon, { width: "3em", height: "3em" })
2311
3657
  }
2312
3658
  ) : /* @__PURE__ */ jsxRuntime.jsx(VideoThumbnail, { asset })
2313
3659
  ] }),
@@ -2359,10 +3705,10 @@ function VideoInBrowser({
2359
3705
  );
2360
3706
  }
2361
3707
  function VideosBrowser({ onSelect }) {
2362
- const { assets, isLoading, searchQuery, setSearchQuery, setSort, sort } = useAssets(), [editedAsset, setEditedAsset] = React.useState(null), freshEditedAsset = React.useMemo(
3708
+ const { assets, isLoading, searchQuery, setSearchQuery, setSort, sort } = useAssets(), [page, setPage] = React.useState(0), pageLimit = 20, pageTotal = Math.floor(assets.length / pageLimit) + 1, [editedAsset, setEditedAsset] = React.useState(null), freshEditedAsset = React.useMemo(
2363
3709
  () => assets.find((a2) => a2._id === editedAsset?._id) || editedAsset,
2364
3710
  [editedAsset, assets]
2365
- );
3711
+ ), pageStart = page * pageLimit, pageEnd = pageStart + pageLimit;
2366
3712
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2367
3713
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { padding: 4, space: 4, style: { minHeight: "50vh" }, children: [
2368
3714
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "space-between", align: "center", children: [
@@ -2376,7 +3722,8 @@ function VideosBrowser({ onSelect }) {
2376
3722
  placeholder: "Search videos"
2377
3723
  }
2378
3724
  ),
2379
- /* @__PURE__ */ jsxRuntime.jsx(SelectSortOptions, { setSort, sort })
3725
+ /* @__PURE__ */ jsxRuntime.jsx(SelectSortOptions, { setSort, sort }),
3726
+ /* @__PURE__ */ jsxRuntime.jsx(PageSelector, { page, setPage, total: pageTotal, limit: pageLimit })
2380
3727
  ] }),
2381
3728
  (onSelect ? "input" : "tool") == "tool" && /* @__PURE__ */ jsxRuntime.jsxs(ui.Inline, { space: 2, children: [
2382
3729
  /* @__PURE__ */ jsxRuntime.jsx(ImportVideosFromMux, {}),
@@ -2399,7 +3746,7 @@ function VideosBrowser({ onSelect }) {
2399
3746
  style: {
2400
3747
  gridTemplateColumns: "repeat(auto-fill, minmax(250px, 1fr))"
2401
3748
  },
2402
- children: assets.map((asset) => /* @__PURE__ */ jsxRuntime.jsx(
3749
+ children: assets.slice(pageStart, pageEnd).map((asset) => /* @__PURE__ */ jsxRuntime.jsx(
2403
3750
  VideoInBrowser,
2404
3751
  {
2405
3752
  asset,
@@ -2932,7 +4279,7 @@ const TopControls = styledComponents.styled.div`
2932
4279
  }
2933
4280
  ) : null
2934
4281
  ] }) });
2935
- }, Player = ({ asset, buttons, readOnly, onChange }) => {
4282
+ }, Player = ({ asset, buttons, readOnly, onChange, config }) => {
2936
4283
  const isLoading = React.useMemo(() => asset?.status === "preparing" ? "Preparing the video" : asset?.status === "waiting_for_upload" ? "Waiting for upload to start" : asset?.status === "waiting" ? "Processing upload" : !(asset?.status === "ready" || typeof asset?.status > "u"), [asset]), isPreparingStaticRenditions = React.useMemo(() => {
2937
4284
  if (asset?.data?.static_renditions?.status && asset?.data?.static_renditions?.status !== "disabled")
2938
4285
  return !1;
@@ -2953,7 +4300,7 @@ const TopControls = styledComponents.styled.div`
2953
4300
  text: isLoading !== !0 && isLoading || "Waiting for Mux to complete the upload",
2954
4301
  onCancel: readOnly ? void 0 : () => handleCancelUpload()
2955
4302
  }
2956
- ) : /* @__PURE__ */ jsxRuntime.jsxs(VideoPlayer, { asset, children: [
4303
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(VideoPlayer, { asset, hlsConfig: config?.hlsConfig, children: [
2957
4304
  buttons && /* @__PURE__ */ jsxRuntime.jsx(TopControls, { slot: "top-chrome", children: buttons }),
2958
4305
  isPreparingStaticRenditions && /* @__PURE__ */ jsxRuntime.jsx(
2959
4306
  ui.Card,
@@ -3065,7 +4412,7 @@ const FileButton = styledComponents.styled(ui.MenuItem)(({ theme }) => {
3065
4412
  color: white;
3066
4413
  `, isVideoAsset = (asset) => asset._type === "mux.videoAsset";
3067
4414
  function PlayerActionsMenu(props) {
3068
- const { asset, readOnly, dialogState, setDialogState, onChange, onSelect } = props, [open, setOpen] = React.useState(!1), [menuElement, setMenuRef] = React.useState(null), isSigned = React.useMemo(() => getPlaybackPolicy(asset) === "signed", [asset]), { hasConfigAccess } = useAccessControl(props.config), onReset = React.useCallback(() => onChange(sanity.PatchEvent.from(sanity.unset([]))), [onChange]);
4415
+ const { asset, readOnly, dialogState, setDialogState, onChange, onSelect, accept } = props, [open, setOpen] = React.useState(!1), [menuElement, setMenuRef] = React.useState(null), isSigned = React.useMemo(() => getPlaybackPolicy(asset) === "signed", [asset]), { hasConfigAccess } = useAccessControl(props.config), onReset = React.useCallback(() => onChange(sanity.PatchEvent.from(sanity.unset([]))), [onChange]);
3069
4416
  return React.useEffect(() => {
3070
4417
  open && dialogState && setOpen(!1);
3071
4418
  }, [dialogState, open]), ui.useClickOutsideEvent(
@@ -3091,7 +4438,7 @@ function PlayerActionsMenu(props) {
3091
4438
  /* @__PURE__ */ jsxRuntime.jsx(
3092
4439
  FileInputMenuItem,
3093
4440
  {
3094
- accept: "video/*",
4441
+ accept,
3095
4442
  icon: icons.UploadIcon,
3096
4443
  onSelect,
3097
4444
  text: "Upload",
@@ -3107,14 +4454,24 @@ function PlayerActionsMenu(props) {
3107
4454
  onClick: () => setDialogState("select-video")
3108
4455
  }
3109
4456
  ),
3110
- isVideoAsset(asset) && /* @__PURE__ */ jsxRuntime.jsx(
3111
- ui.MenuItem,
3112
- {
3113
- icon: icons.ImageIcon,
3114
- text: "Thumbnail",
3115
- onClick: () => setDialogState("edit-thumbnail")
3116
- }
3117
- ),
4457
+ isVideoAsset(asset) && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4458
+ /* @__PURE__ */ jsxRuntime.jsx(
4459
+ ui.MenuItem,
4460
+ {
4461
+ icon: icons.ImageIcon,
4462
+ text: "Thumbnail",
4463
+ onClick: () => setDialogState("edit-thumbnail")
4464
+ }
4465
+ ),
4466
+ /* @__PURE__ */ jsxRuntime.jsx(
4467
+ ui.MenuItem,
4468
+ {
4469
+ icon: icons.TranslateIcon,
4470
+ text: "Captions",
4471
+ onClick: () => setDialogState("edit-captions")
4472
+ }
4473
+ )
4474
+ ] }),
3118
4475
  /* @__PURE__ */ jsxRuntime.jsx(ui.MenuDivider, {}),
3119
4476
  hasConfigAccess && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3120
4477
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -3168,36 +4525,6 @@ function formatBytes(bytes, si = !1, dp = 1) {
3168
4525
  while (Math.round(Math.abs(bytes) * r) / r >= thresh && u2 < units.length - 1);
3169
4526
  return bytes.toFixed(dp) + " " + units[u2];
3170
4527
  }
3171
- const SUPPORTED_MUX_LANGUAGES = [
3172
- { label: "English", code: "en", state: "Stable" },
3173
- { label: "Spanish", code: "es", state: "Stable" },
3174
- { label: "Italian", code: "it", state: "Stable" },
3175
- { label: "Portuguese", code: "pt", state: "Stable" },
3176
- { label: "German", code: "de", state: "Stable" },
3177
- { label: "French", code: "fr", state: "Stable" },
3178
- { label: "Polish", code: "pl", state: "Beta" },
3179
- { label: "Russian", code: "ru", state: "Beta" },
3180
- { label: "Dutch", code: "nl", state: "Beta" },
3181
- { label: "Catalan", code: "ca", state: "Beta" },
3182
- { label: "Turkish", code: "tr", state: "Beta" },
3183
- { label: "Swedish", code: "sv", state: "Beta" },
3184
- { label: "Ukrainian", code: "uk", state: "Beta" },
3185
- { label: "Norwegian", code: "no", state: "Beta" },
3186
- { label: "Finnish", code: "fi", state: "Beta" },
3187
- { label: "Slovak", code: "sk", state: "Beta" },
3188
- { label: "Greek", code: "el", state: "Beta" },
3189
- { label: "Czech", code: "cs", state: "Beta" },
3190
- { label: "Croatian", code: "hr", state: "Beta" },
3191
- { label: "Danish", code: "da", state: "Beta" },
3192
- { label: "Romanian", code: "ro", state: "Beta" },
3193
- { label: "Bulgarian", code: "bg", state: "Beta" }
3194
- ];
3195
- function isCustomTextTrack(track) {
3196
- return track.type !== "autogenerated";
3197
- }
3198
- function isAutogeneratedTrack(track) {
3199
- return track.type === "autogenerated";
3200
- }
3201
4528
  const ALL_LANGUAGE_CODES = LanguagesList__default.default.getAllCodes().map((code) => ({
3202
4529
  value: code,
3203
4530
  label: LanguagesList__default.default.getNativeName(code)
@@ -3455,7 +4782,47 @@ function UploadConfiguration({
3455
4782
  (r) => r !== "highest" && r !== "audio-only"
3456
4783
  ).length > 0, [config.static_renditions]), [renditionMode, setRenditionMode] = React.useState(
3457
4784
  isAdvancedMode ? "advanced" : "standard"
3458
- ), toggleRendition = (rendition) => {
4785
+ ), [videoDuration, setVideoDuration] = React.useState(null), [urlFileSize, setUrlFileSize] = React.useState(null), [isLoadingDuration, setIsLoadingDuration] = React.useState(!1), [isLoadingFileSize, setIsLoadingFileSize] = React.useState(!1), [validationError, setValidationError] = React.useState(null), [canSkipFileSizeValidation, setCanSkipFileSizeValidation] = React.useState(!1), MAX_FILE_SIZE = pluginConfig.maxAssetFileSize, MAX_DURATION_SECONDS = pluginConfig.maxAssetDuration;
4786
+ React.useEffect(() => {
4787
+ setVideoDuration(null), setUrlFileSize(null), setIsLoadingDuration(!1), setIsLoadingFileSize(!1), setValidationError(null), setCanSkipFileSizeValidation(!1);
4788
+ let videoElement = null, currentVideoSrc = null;
4789
+ const cleanupVideo = (shouldRevokeUrl) => {
4790
+ videoElement && (videoElement.onloadedmetadata = null, videoElement.onerror = null, videoElement.src = "", videoElement.load(), videoElement = null), shouldRevokeUrl && currentVideoSrc?.startsWith("blob:") && URL.revokeObjectURL(currentVideoSrc), currentVideoSrc = null;
4791
+ }, validateDuration = (videoSrc, shouldRevokeUrl = !1) => {
4792
+ !MAX_DURATION_SECONDS || MAX_DURATION_SECONDS <= 0 || (setIsLoadingDuration(!0), videoElement = document.createElement("video"), videoElement.preload = "metadata", currentVideoSrc = videoSrc, videoElement.onloadedmetadata = () => {
4793
+ const duration = videoElement.duration;
4794
+ setVideoDuration(duration), setIsLoadingDuration(!1), duration > MAX_DURATION_SECONDS && setValidationError(
4795
+ `Video duration (${formatSeconds(duration)}) exceeds maximum allowed duration of ${formatSeconds(MAX_DURATION_SECONDS)}`
4796
+ ), cleanupVideo(shouldRevokeUrl);
4797
+ }, videoElement.onerror = () => {
4798
+ setIsLoadingDuration(!1), console.warn("Could not read video metadata for validation"), cleanupVideo(shouldRevokeUrl);
4799
+ }, videoElement.src = videoSrc);
4800
+ }, validateFileSize = (size) => MAX_FILE_SIZE === void 0 || size <= MAX_FILE_SIZE ? !0 : (setValidationError(
4801
+ `File size (${formatBytes(size)}) exceeds maximum allowed size of ${formatBytes(MAX_FILE_SIZE)}`
4802
+ ), !1);
4803
+ if (stagedUpload.type === "file") {
4804
+ const file = stagedUpload.files[0];
4805
+ validateFileSize(file.size) && validateDuration(URL.createObjectURL(file), !0);
4806
+ }
4807
+ if (stagedUpload.type === "url") {
4808
+ const url = stagedUpload.url;
4809
+ (async () => {
4810
+ setIsLoadingFileSize(!0);
4811
+ try {
4812
+ const contentLength = (await fetch(url, { method: "HEAD" })).headers.get("content-length"), fileSize = contentLength ? parseInt(contentLength, 10) : null;
4813
+ setIsLoadingFileSize(!1), fileSize && setUrlFileSize(fileSize);
4814
+ const shouldValidateDuration = MAX_FILE_SIZE === void 0 || fileSize === null || validateFileSize(fileSize);
4815
+ fileSize === null && MAX_FILE_SIZE !== void 0 && setCanSkipFileSizeValidation(!0), shouldValidateDuration && validateDuration(url);
4816
+ } catch {
4817
+ setIsLoadingFileSize(!1), console.warn("Could not validate file size from URL"), setCanSkipFileSizeValidation(!0), validateDuration(url);
4818
+ }
4819
+ })();
4820
+ }
4821
+ return () => {
4822
+ cleanupVideo(!0);
4823
+ };
4824
+ }, [stagedUpload, MAX_FILE_SIZE, MAX_DURATION_SECONDS]);
4825
+ const toggleRendition = (rendition) => {
3459
4826
  const current = config.static_renditions, hasRendition = current.includes(rendition);
3460
4827
  dispatch(hasRendition ? {
3461
4828
  action: "static_renditions",
@@ -3490,6 +4857,13 @@ function UploadConfiguration({
3490
4857
  header: "Configure Mux Upload",
3491
4858
  onClose,
3492
4859
  children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { padding: 4, space: 2, children: [
4860
+ validationError && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 3, tone: "critical", radius: 2, marginBottom: 2, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 2, align: "flex-start", children: [
4861
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ErrorOutlineIcon, { width: 20, height: 20 }),
4862
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
4863
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, weight: "semibold", children: "Validation Error" }),
4864
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, children: validationError })
4865
+ ] })
4866
+ ] }) }),
3493
4867
  /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { size: 3, children: "FILE TO UPLOAD" }),
3494
4868
  /* @__PURE__ */ jsxRuntime.jsx(
3495
4869
  ui.Card,
@@ -3503,7 +4877,14 @@ function UploadConfiguration({
3503
4877
  /* @__PURE__ */ jsxRuntime.jsx(icons.DocumentVideoIcon, { fontSize: "2em" }),
3504
4878
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
3505
4879
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { textOverflow: "ellipsis", as: "h2", size: 3, children: stagedUpload.type === "file" ? stagedUpload.files[0].name : stagedUpload.url }),
3506
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "p", size: 1, muted: !0, children: stagedUpload.type === "file" ? `Direct File Upload (${formatBytes(stagedUpload.files[0].size)})` : "File From URL (Unknown size)" })
4880
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "p", size: 1, muted: !0, children: stagedUpload.type === "file" ? `Direct File Upload (${formatBytes(stagedUpload.files[0].size)})` : urlFileSize ? `File From URL (${formatBytes(urlFileSize)})` : isLoadingFileSize ? "File From URL (Loading size...)" : "File From URL (Unknown size)" }),
4881
+ stagedUpload.type === "file" && /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 1, children: [
4882
+ isLoadingDuration && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "p", size: 1, muted: !0, children: "Reading video metadata..." }),
4883
+ videoDuration !== null && !validationError && /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { as: "p", size: 1, muted: !0, children: [
4884
+ "Duration: ",
4885
+ formatSeconds(videoDuration)
4886
+ ] })
4887
+ ] })
3507
4888
  ] })
3508
4889
  ] })
3509
4890
  }
@@ -3695,11 +5076,13 @@ function UploadConfiguration({
3695
5076
  /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { marginTop: 4, children: /* @__PURE__ */ jsxRuntime.jsx(
3696
5077
  ui.Button,
3697
5078
  {
3698
- disabled: !basicConfig && !config.public_policy && !config.signed_policy,
5079
+ disabled: !basicConfig && !config.public_policy && !config.signed_policy || validationError !== null || isLoadingDuration || isLoadingFileSize && !canSkipFileSizeValidation,
3699
5080
  icon: icons.UploadIcon,
3700
5081
  text: "Upload",
3701
5082
  tone: "positive",
3702
- onClick: () => startUpload(formatUploadConfig(config))
5083
+ onClick: () => {
5084
+ validationError || startUpload(formatUploadConfig(config));
5085
+ }
3703
5086
  }
3704
5087
  ) })
3705
5088
  ] })
@@ -3763,12 +5146,10 @@ function withFocusRing(component) {
3763
5146
  `;
3764
5147
  });
3765
5148
  }
3766
- const ctrlKey = 17, cmdKey = 91, UploadCardWithFocusRing = withFocusRing(ui.Card), UploadCard = React.forwardRef(
5149
+ const UploadCardWithFocusRing = withFocusRing(ui.Card), UploadCard = React.forwardRef(
3767
5150
  ({ children, tone, onPaste, onDrop, onDragEnter, onDragLeave, onDragOver }, forwardedRef) => {
3768
- const ctrlDown = React.useRef(!1), inputRef = React.useRef(null), handleKeyDown = React.useCallback((event) => {
3769
- (event.keyCode == ctrlKey || event.keyCode == cmdKey) && (ctrlDown.current = !0), ctrlDown.current && event.keyCode == 86 && inputRef.current.focus();
3770
- }, []), handleKeyUp = React.useCallback((event) => {
3771
- (event.keyCode == ctrlKey || event.keyCode == cmdKey) && (ctrlDown.current = !1);
5151
+ const inputRef = React.useRef(null), handleKeyDown = React.useCallback((event) => {
5152
+ event.target.closest("#vtt-url") || (event.ctrlKey || event.metaKey) && event.key === "v" && inputRef.current.focus();
3772
5153
  }, []);
3773
5154
  return /* @__PURE__ */ jsxRuntime.jsxs(
3774
5155
  UploadCardWithFocusRing,
@@ -3780,14 +5161,13 @@ const ctrlKey = 17, cmdKey = 91, UploadCardWithFocusRing = withFocusRing(ui.Card
3780
5161
  shadow: 0,
3781
5162
  tabIndex: 0,
3782
5163
  onKeyDown: handleKeyDown,
3783
- onKeyUp: handleKeyUp,
3784
5164
  onPaste,
3785
5165
  onDrop,
3786
5166
  onDragEnter,
3787
5167
  onDragLeave,
3788
5168
  onDragOver,
3789
5169
  children: [
3790
- /* @__PURE__ */ jsxRuntime.jsx(HiddenInput$1, { ref: inputRef, onPaste }),
5170
+ /* @__PURE__ */ jsxRuntime.jsx(HiddenInput$1, { ref: inputRef }),
3791
5171
  children
3792
5172
  ]
3793
5173
  }
@@ -3822,7 +5202,7 @@ const ctrlKey = 17, cmdKey = 91, UploadCardWithFocusRing = withFocusRing(ui.Card
3822
5202
  /* @__PURE__ */ jsxRuntime.jsx(
3823
5203
  HiddenInput,
3824
5204
  {
3825
- accept: accept || "video/*",
5205
+ accept,
3826
5206
  ref: inputRef,
3827
5207
  tabIndex: 0,
3828
5208
  type: "file",
@@ -3843,8 +5223,11 @@ const ctrlKey = 17, cmdKey = 91, UploadCardWithFocusRing = withFocusRing(ui.Card
3843
5223
  )
3844
5224
  ] });
3845
5225
  };
5226
+ function formatAcceptString(accept) {
5227
+ return accept.split(",").map((type) => type.trim().replace("/*", "")).join(" or ");
5228
+ }
3846
5229
  function UploadPlaceholder(props) {
3847
- const { setDialogState, readOnly, onSelect, hovering, needsSetup } = props, handleBrowse = React.useCallback(() => setDialogState("select-video"), [setDialogState]), handleConfigureApi = React.useCallback(() => setDialogState("secrets"), [setDialogState]), { hasConfigAccess } = useAccessControl(props.config);
5230
+ const { setDialogState, readOnly, onSelect, hovering, needsSetup, accept } = props, handleBrowse = React.useCallback(() => setDialogState("select-video"), [setDialogState]), handleConfigureApi = React.useCallback(() => setDialogState("secrets"), [setDialogState]), { hasConfigAccess } = useAccessControl(props.config);
3848
5231
  return /* @__PURE__ */ jsxRuntime.jsx(
3849
5232
  ui.Card,
3850
5233
  {
@@ -3867,12 +5250,17 @@ function UploadPlaceholder(props) {
3867
5250
  children: [
3868
5251
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", justify: "flex-start", gap: 2, flex: 1, children: [
3869
5252
  /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { justify: "center", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { muted: !0, children: /* @__PURE__ */ jsxRuntime.jsx(icons.DocumentVideoIcon, {}) }) }),
3870
- /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { justify: "center", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, children: "Drag video or paste URL here" }) })
5253
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { justify: "center", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, muted: !0, children: [
5254
+ "Drag ",
5255
+ formatAcceptString(accept),
5256
+ " file or paste URL here"
5257
+ ] }) })
3871
5258
  ] }),
3872
5259
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Inline, { space: 2, children: [
3873
5260
  /* @__PURE__ */ jsxRuntime.jsx(
3874
5261
  FileInputButton,
3875
5262
  {
5263
+ accept,
3876
5264
  mode: "bleed",
3877
5265
  tone: "default",
3878
5266
  icon: icons.UploadIcon,
@@ -4023,21 +5411,35 @@ function Uploader(props) {
4023
5411
  complete: () => dispatch({ action: "complete" }),
4024
5412
  error: (error) => dispatch({ action: "error", error })
4025
5413
  });
4026
- }, handleUpload = (files) => {
4027
- dispatch({
5414
+ }, invalidFileToast = React.useCallback(() => {
5415
+ toast.push({
5416
+ status: "error",
5417
+ title: `Invalid file type. Accepted types: ${props.config.acceptedMimeTypes?.join(", ")}`
5418
+ });
5419
+ }, [props.config.acceptedMimeTypes, toast]), isInvalidFile = (files) => Array.from(files).some((file) => !props.config.acceptedMimeTypes?.some((acceptedType) => {
5420
+ const pattern = `^${acceptedType.replace("*", ".*")}$`;
5421
+ return new RegExp(pattern).test(file.type);
5422
+ })), handleUpload = (files) => {
5423
+ isInvalidFile(files) || dispatch({
4028
5424
  action: "stageUpload",
4029
5425
  input: { type: "file", files }
4030
5426
  });
4031
5427
  }, handlePaste = (event) => {
5428
+ if (event.target.closest("#vtt-url"))
5429
+ return;
4032
5430
  event.preventDefault(), event.stopPropagation();
4033
- const url = (event.clipboardData || window.clipboardData).getData("text")?.trim();
5431
+ const url = (event.clipboardData || window.clipboardData)?.getData("text")?.trim();
4034
5432
  if (!isValidUrl(url)) {
4035
5433
  toast.push({ status: "error", title: "Invalid URL for Mux video input." });
4036
5434
  return;
4037
5435
  }
4038
5436
  dispatch({ action: "stageUpload", input: { type: "url", url } });
4039
5437
  }, handleDrop = (event) => {
4040
- setDragState(null), event.preventDefault(), event.stopPropagation(), extractDroppedFiles(event.nativeEvent.dataTransfer).then((files) => {
5438
+ if (event.preventDefault(), event.stopPropagation(), dragState === "invalid") {
5439
+ invalidFileToast(), setDragState(null);
5440
+ return;
5441
+ }
5442
+ setDragState(null), extractDroppedFiles(event.nativeEvent.dataTransfer).then((files) => {
4041
5443
  dispatch({
4042
5444
  action: "stageUpload",
4043
5445
  input: { type: "file", files }
@@ -4047,8 +5449,11 @@ function Uploader(props) {
4047
5449
  event.preventDefault(), event.stopPropagation();
4048
5450
  }, handleDragEnter = (event) => {
4049
5451
  event.stopPropagation(), dragEnteredEls.current.push(event.target);
4050
- const type = event.dataTransfer.items?.[0]?.type;
4051
- setDragState(type?.startsWith("video/") ? "valid" : "invalid");
5452
+ const type = event.dataTransfer.items?.[0]?.type, isValidType = props.config.acceptedMimeTypes?.some((acceptedType) => {
5453
+ const pattern = `^${acceptedType.replace("*", ".*")}$`;
5454
+ return new RegExp(pattern).test(type);
5455
+ });
5456
+ setDragState(isValidType ? "valid" : "invalid");
4052
5457
  }, handleDragLeave = (event) => {
4053
5458
  event.stopPropagation();
4054
5459
  const idx = dragEnteredEls.current.indexOf(event.target);
@@ -4086,7 +5491,9 @@ function Uploader(props) {
4086
5491
  }
4087
5492
  );
4088
5493
  let tone;
4089
- return dragState && (tone = dragState === "valid" ? "positive" : "critical"), /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5494
+ dragState && (tone = dragState === "valid" ? "positive" : "critical");
5495
+ const acceptMimeString = props.config?.acceptedMimeTypes?.length ? props.config.acceptedMimeTypes.join(",") : "video/*, audio/*";
5496
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4090
5497
  /* @__PURE__ */ jsxRuntime.jsx(
4091
5498
  UploadCard,
4092
5499
  {
@@ -4108,9 +5515,11 @@ function Uploader(props) {
4108
5515
  readOnly: props.readOnly,
4109
5516
  asset: props.asset,
4110
5517
  onChange: props.onChange,
5518
+ config: props.config,
4111
5519
  buttons: /* @__PURE__ */ jsxRuntime.jsx(
4112
5520
  PlayerActionsMenu$1,
4113
5521
  {
5522
+ accept: acceptMimeString,
4114
5523
  asset: props.asset,
4115
5524
  dialogState: props.dialogState,
4116
5525
  setDialogState: props.setDialogState,
@@ -4126,6 +5535,7 @@ function Uploader(props) {
4126
5535
  ) : /* @__PURE__ */ jsxRuntime.jsx(
4127
5536
  UploadPlaceholder,
4128
5537
  {
5538
+ accept: acceptMimeString,
4129
5539
  hovering: dragState !== null,
4130
5540
  onSelect: handleUpload,
4131
5541
  readOnly: !!props.readOnly,
@@ -4386,7 +5796,8 @@ const muxVideoSchema = {
4386
5796
  defaultPublic: !0,
4387
5797
  defaultSigned: !1,
4388
5798
  tool: DEFAULT_TOOL_CONFIG,
4389
- allowedRolesForConfiguration: []
5799
+ allowedRolesForConfiguration: [],
5800
+ acceptedMimeTypes: ["video/*", "audio/*"]
4390
5801
  };
4391
5802
  function convertLegacyConfig(config) {
4392
5803
  return config.static_renditions && config.static_renditions.length > 0 ? { static_renditions: config.static_renditions } : config.mp4_support === "standard" ? { static_renditions: ["highest"] } : { static_renditions: [] };