sanity-plugin-mux-input 2.13.0 → 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.
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
@@ -1330,55 +1415,1266 @@ function SelectSortOptions(props) {
1330
1415
  }
1331
1416
  );
1332
1417
  }
1333
- const SpinnerBox = () => /* @__PURE__ */ jsxRuntime.jsx(
1334
- ui.Box,
1335
- {
1336
- style: {
1337
- display: "flex",
1338
- alignItems: "center",
1339
- justifyContent: "center",
1340
- minHeight: "150px"
1341
- },
1342
- children: /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, {})
1343
- }
1344
- ), IconInfo = (props) => {
1345
- const Icon = props.icon;
1346
- return /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 2, align: "center", padding: 1, children: [
1347
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: (props.size || 1) + 1, muted: !0, children: /* @__PURE__ */ jsxRuntime.jsx(Icon, {}) }),
1348
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: props.size || 1, muted: props.muted, children: props.text })
1418
+ const SpinnerBox = () => /* @__PURE__ */ jsxRuntime.jsx(
1419
+ ui.Box,
1420
+ {
1421
+ style: {
1422
+ display: "flex",
1423
+ alignItems: "center",
1424
+ justifyContent: "center",
1425
+ minHeight: "150px"
1426
+ },
1427
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, {})
1428
+ }
1429
+ ), IconInfo = (props) => {
1430
+ const Icon = props.icon;
1431
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 2, align: "center", padding: 1, children: [
1432
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: (props.size || 1) + 1, muted: !0, children: /* @__PURE__ */ jsxRuntime.jsx(Icon, {}) }),
1433
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: props.size || 1, muted: props.muted, children: props.text })
1434
+ ] });
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
+ )
1349
2327
  ] });
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",
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,
@@ -1561,7 +2861,7 @@ function VideoPlayer({
1561
2861
  crossOrigin: "anonymous",
1562
2862
  metadata: {
1563
2863
  player_name: "Sanity Admin Dashboard",
1564
- player_version: "2.13.0",
2864
+ player_version: "2.14.0",
1565
2865
  page_type: "Preview Player"
1566
2866
  },
1567
2867
  audio: isAudio,
@@ -1596,7 +2896,8 @@ function VideoPlayer({
1596
2896
  ]
1597
2897
  }
1598
2898
  ),
1599
- dialogState === "edit-thumbnail" && /* @__PURE__ */ jsxRuntime.jsx(EditThumbnailDialog, { asset, currentTime: muxPlayer?.current?.currentTime })
2899
+ dialogState === "edit-thumbnail" && /* @__PURE__ */ jsxRuntime.jsx(EditThumbnailDialog, { asset, currentTime: muxPlayer?.current?.currentTime }),
2900
+ dialogState === "edit-captions" && /* @__PURE__ */ jsxRuntime.jsx(CaptionsDialog, { asset })
1600
2901
  ] });
1601
2902
  }
1602
2903
  function assetIsAudio(asset) {
@@ -1868,7 +3169,8 @@ function getVideoMetadata(doc) {
1868
3169
  duration: doc.data?.duration ? formatSeconds(doc.data?.duration) : void 0,
1869
3170
  aspect_ratio: doc.data?.aspect_ratio,
1870
3171
  max_stored_resolution: doc.data?.max_stored_resolution,
1871
- 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") || []
1872
3174
  };
1873
3175
  }
1874
3176
  function useVideoDetails(props) {
@@ -2060,7 +3362,20 @@ const AssetInput = (props) => /* @__PURE__ */ jsxRuntime.jsx(FormField$1, { titl
2060
3362
  minHeight: containerHeight
2061
3363
  } : void 0,
2062
3364
  children: [
2063
- /* @__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
+ ] }),
2064
3379
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, flex: 1, sizing: "border", children: [
2065
3380
  /* @__PURE__ */ jsxRuntime.jsxs(ui.TabList, { space: 2, children: [
2066
3381
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -2390,10 +3705,10 @@ function VideoInBrowser({
2390
3705
  );
2391
3706
  }
2392
3707
  function VideosBrowser({ onSelect }) {
2393
- 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(
2394
3709
  () => assets.find((a2) => a2._id === editedAsset?._id) || editedAsset,
2395
3710
  [editedAsset, assets]
2396
- );
3711
+ ), pageStart = page * pageLimit, pageEnd = pageStart + pageLimit;
2397
3712
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2398
3713
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { padding: 4, space: 4, style: { minHeight: "50vh" }, children: [
2399
3714
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "space-between", align: "center", children: [
@@ -2407,7 +3722,8 @@ function VideosBrowser({ onSelect }) {
2407
3722
  placeholder: "Search videos"
2408
3723
  }
2409
3724
  ),
2410
- /* @__PURE__ */ jsxRuntime.jsx(SelectSortOptions, { setSort, sort })
3725
+ /* @__PURE__ */ jsxRuntime.jsx(SelectSortOptions, { setSort, sort }),
3726
+ /* @__PURE__ */ jsxRuntime.jsx(PageSelector, { page, setPage, total: pageTotal, limit: pageLimit })
2411
3727
  ] }),
2412
3728
  (onSelect ? "input" : "tool") == "tool" && /* @__PURE__ */ jsxRuntime.jsxs(ui.Inline, { space: 2, children: [
2413
3729
  /* @__PURE__ */ jsxRuntime.jsx(ImportVideosFromMux, {}),
@@ -2430,7 +3746,7 @@ function VideosBrowser({ onSelect }) {
2430
3746
  style: {
2431
3747
  gridTemplateColumns: "repeat(auto-fill, minmax(250px, 1fr))"
2432
3748
  },
2433
- children: assets.map((asset) => /* @__PURE__ */ jsxRuntime.jsx(
3749
+ children: assets.slice(pageStart, pageEnd).map((asset) => /* @__PURE__ */ jsxRuntime.jsx(
2434
3750
  VideoInBrowser,
2435
3751
  {
2436
3752
  asset,
@@ -3138,14 +4454,24 @@ function PlayerActionsMenu(props) {
3138
4454
  onClick: () => setDialogState("select-video")
3139
4455
  }
3140
4456
  ),
3141
- isVideoAsset(asset) && /* @__PURE__ */ jsxRuntime.jsx(
3142
- ui.MenuItem,
3143
- {
3144
- icon: icons.ImageIcon,
3145
- text: "Thumbnail",
3146
- onClick: () => setDialogState("edit-thumbnail")
3147
- }
3148
- ),
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
+ ] }),
3149
4475
  /* @__PURE__ */ jsxRuntime.jsx(ui.MenuDivider, {}),
3150
4476
  hasConfigAccess && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3151
4477
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -3199,36 +4525,6 @@ function formatBytes(bytes, si = !1, dp = 1) {
3199
4525
  while (Math.round(Math.abs(bytes) * r) / r >= thresh && u2 < units.length - 1);
3200
4526
  return bytes.toFixed(dp) + " " + units[u2];
3201
4527
  }
3202
- const SUPPORTED_MUX_LANGUAGES = [
3203
- { label: "English", code: "en", state: "Stable" },
3204
- { label: "Spanish", code: "es", state: "Stable" },
3205
- { label: "Italian", code: "it", state: "Stable" },
3206
- { label: "Portuguese", code: "pt", state: "Stable" },
3207
- { label: "German", code: "de", state: "Stable" },
3208
- { label: "French", code: "fr", state: "Stable" },
3209
- { label: "Polish", code: "pl", state: "Beta" },
3210
- { label: "Russian", code: "ru", state: "Beta" },
3211
- { label: "Dutch", code: "nl", state: "Beta" },
3212
- { label: "Catalan", code: "ca", state: "Beta" },
3213
- { label: "Turkish", code: "tr", state: "Beta" },
3214
- { label: "Swedish", code: "sv", state: "Beta" },
3215
- { label: "Ukrainian", code: "uk", state: "Beta" },
3216
- { label: "Norwegian", code: "no", state: "Beta" },
3217
- { label: "Finnish", code: "fi", state: "Beta" },
3218
- { label: "Slovak", code: "sk", state: "Beta" },
3219
- { label: "Greek", code: "el", state: "Beta" },
3220
- { label: "Czech", code: "cs", state: "Beta" },
3221
- { label: "Croatian", code: "hr", state: "Beta" },
3222
- { label: "Danish", code: "da", state: "Beta" },
3223
- { label: "Romanian", code: "ro", state: "Beta" },
3224
- { label: "Bulgarian", code: "bg", state: "Beta" }
3225
- ];
3226
- function isCustomTextTrack(track) {
3227
- return track.type !== "autogenerated";
3228
- }
3229
- function isAutogeneratedTrack(track) {
3230
- return track.type === "autogenerated";
3231
- }
3232
4528
  const ALL_LANGUAGE_CODES = LanguagesList__default.default.getAllCodes().map((code) => ({
3233
4529
  value: code,
3234
4530
  label: LanguagesList__default.default.getNativeName(code)
@@ -3850,12 +5146,10 @@ function withFocusRing(component) {
3850
5146
  `;
3851
5147
  });
3852
5148
  }
3853
- const ctrlKey = 17, cmdKey = 91, UploadCardWithFocusRing = withFocusRing(ui.Card), UploadCard = React.forwardRef(
5149
+ const UploadCardWithFocusRing = withFocusRing(ui.Card), UploadCard = React.forwardRef(
3854
5150
  ({ children, tone, onPaste, onDrop, onDragEnter, onDragLeave, onDragOver }, forwardedRef) => {
3855
- const ctrlDown = React.useRef(!1), inputRef = React.useRef(null), handleKeyDown = React.useCallback((event) => {
3856
- (event.keyCode == ctrlKey || event.keyCode == cmdKey) && (ctrlDown.current = !0), ctrlDown.current && event.keyCode == 86 && inputRef.current.focus();
3857
- }, []), handleKeyUp = React.useCallback((event) => {
3858
- (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();
3859
5153
  }, []);
3860
5154
  return /* @__PURE__ */ jsxRuntime.jsxs(
3861
5155
  UploadCardWithFocusRing,
@@ -3867,14 +5161,13 @@ const ctrlKey = 17, cmdKey = 91, UploadCardWithFocusRing = withFocusRing(ui.Card
3867
5161
  shadow: 0,
3868
5162
  tabIndex: 0,
3869
5163
  onKeyDown: handleKeyDown,
3870
- onKeyUp: handleKeyUp,
3871
5164
  onPaste,
3872
5165
  onDrop,
3873
5166
  onDragEnter,
3874
5167
  onDragLeave,
3875
5168
  onDragOver,
3876
5169
  children: [
3877
- /* @__PURE__ */ jsxRuntime.jsx(HiddenInput$1, { ref: inputRef, onPaste }),
5170
+ /* @__PURE__ */ jsxRuntime.jsx(HiddenInput$1, { ref: inputRef }),
3878
5171
  children
3879
5172
  ]
3880
5173
  }
@@ -4132,6 +5425,8 @@ function Uploader(props) {
4132
5425
  input: { type: "file", files }
4133
5426
  });
4134
5427
  }, handlePaste = (event) => {
5428
+ if (event.target.closest("#vtt-url"))
5429
+ return;
4135
5430
  event.preventDefault(), event.stopPropagation();
4136
5431
  const url = (event.clipboardData || window.clipboardData)?.getData("text")?.trim();
4137
5432
  if (!isValidUrl(url)) {