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.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { useClient as useClient$1, createHookFromObservableFactory, useDocumentStore, collate, useDocumentValues, truncateString, useFormattedDuration, SanityDefaultPreview, useTimeAgo, TextWithTone, isRecord, getPreviewStateObservable, getPreviewValueWithFallback, DocumentPreviewPresence, useDocumentPreviewStore, useSchema, useDocumentPresence, PreviewCard, useCurrentUser, isReference, useProjectId, useDataset, PatchEvent, unset, setIfMissing, set, LinearProgress, FormField as FormField$2, definePlugin } from "sanity";
2
2
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
3
- import { ErrorOutlineIcon, InfoOutlineIcon, RetryIcon, CheckmarkCircleIcon, RetrieveIcon, SyncIcon, SortIcon, WarningOutlineIcon, EditIcon, PublishIcon, DocumentIcon, TrashIcon, RevertIcon, SearchIcon, ClockIcon, CropIcon, CalendarIcon, TagIcon, CheckmarkIcon, LockIcon, PlayIcon, PlugIcon, EllipsisHorizontalIcon, UploadIcon, ImageIcon, ResetIcon, TranslateIcon, WarningFilledIcon, DocumentVideoIcon } from "@sanity/icons";
4
- import { useTheme_v2, Stack, Flex, Box, Text, Button, Dialog, Card, TextInput, Checkbox, Code, Inline, Spinner, Heading, MenuButton, Menu, MenuItem, Tooltip, useToast, TabList, Tab, TabPanel, Label as Label$1, Grid, useClickOutsideEvent, Popover, MenuDivider, Autocomplete, Radio, rem } from "@sanity/ui";
3
+ import { ErrorOutlineIcon, InfoOutlineIcon, RetryIcon, CheckmarkCircleIcon, RetrieveIcon, ChevronLeftIcon, ChevronRightIcon, SyncIcon, SortIcon, UploadIcon, TranslateIcon, DownloadIcon, AddIcon, ChevronUpIcon, ChevronDownIcon, TrashIcon, EditIcon, WarningOutlineIcon, PublishIcon, DocumentIcon, RevertIcon, SearchIcon, ClockIcon, CropIcon, CalendarIcon, TagIcon, CheckmarkIcon, LockIcon, PlayIcon, PlugIcon, EllipsisHorizontalIcon, ImageIcon, ResetIcon, WarningFilledIcon, DocumentVideoIcon } from "@sanity/icons";
4
+ import { useTheme_v2, Stack, Flex, Box, Text, Button, Dialog, Card, TextInput, Checkbox, Code, Inline, Spinner, Heading, Label as Label$1, MenuButton, Menu, MenuItem, useToast, Autocomplete, Tooltip, TabList, Tab, TabPanel, Grid, useClickOutsideEvent, Popover, MenuDivider, Radio, rem } from "@sanity/ui";
5
5
  import React, { useState, useMemo, useCallback, useReducer, useId, memo, useRef, useEffect, createContext, useContext, isValidElement, PureComponent, createElement, forwardRef, Suspense } from "react";
6
6
  import compact from "lodash/compact.js";
7
7
  import toLower from "lodash/toLower.js";
@@ -13,6 +13,7 @@ import { defer, timer, of, Observable, concat, throwError, from, Subject } from
13
13
  import { styled, css } from "styled-components";
14
14
  import { uuid } from "@sanity/uuid";
15
15
  import { expand, concatMap, tap, switchMap, mergeMap, catchError, mergeMapTo, takeUntil } from "rxjs/operators";
16
+ import LanguagesList from "iso-639-1";
16
17
  import MuxPlayer from "@mux/mux-player-react/lazy";
17
18
  import { IntentLink } from "sanity/router";
18
19
  import isNumber from "lodash/isNumber.js";
@@ -22,7 +23,6 @@ import useSWR from "swr";
22
23
  import scrollIntoView from "scroll-into-view-if-needed";
23
24
  import { UpChunk } from "@mux/upchunk";
24
25
  import { isValidElementType } from "react-is";
25
- import LanguagesList from "iso-639-1";
26
26
  const ToolIcon = () => /* @__PURE__ */ jsx(
27
27
  "svg",
28
28
  {
@@ -567,6 +567,51 @@ function listAssets(client, options) {
567
567
  query
568
568
  });
569
569
  }
570
+ function addTextTrackFromUrl(client, assetId, vttUrl, options) {
571
+ const { dataset } = client.config();
572
+ return client.request({
573
+ url: `/addons/mux/assets/${dataset}/${assetId}/tracks`,
574
+ withCredentials: !0,
575
+ method: "POST",
576
+ body: {
577
+ url: vttUrl,
578
+ type: "text",
579
+ language_code: options.language_code,
580
+ name: options.name,
581
+ text_type: options.text_type || "subtitles"
582
+ },
583
+ headers: {
584
+ "Content-Type": "application/json"
585
+ }
586
+ });
587
+ }
588
+ function generateSubtitles(client, assetId, audioTrackId, options) {
589
+ const { dataset } = client.config();
590
+ return client.request({
591
+ url: `/addons/mux/assets/${dataset}/${assetId}/tracks/${audioTrackId}/generate-subtitles`,
592
+ withCredentials: !0,
593
+ method: "POST",
594
+ body: {
595
+ generated_subtitles: [
596
+ {
597
+ language_code: options.language_code,
598
+ name: options.name
599
+ }
600
+ ]
601
+ },
602
+ headers: {
603
+ "Content-Type": "application/json"
604
+ }
605
+ });
606
+ }
607
+ function deleteTextTrack(client, assetId, trackId) {
608
+ const { dataset } = client.config();
609
+ return client.request({
610
+ url: `/addons/mux/assets/${dataset}/${assetId}/tracks/${trackId}`,
611
+ withCredentials: !0,
612
+ method: "DELETE"
613
+ });
614
+ }
570
615
  const ASSETS_PER_PAGE = 100;
571
616
  async function fetchMuxAssetsPage(client, cursor) {
572
617
  try {
@@ -1104,6 +1149,46 @@ function ImportVideosFromMux() {
1104
1149
  if (importAssets.hasSecrets)
1105
1150
  return importAssets.dialogOpen ? /* @__PURE__ */ jsx(ImportVideosDialog, { ...importAssets }) : /* @__PURE__ */ jsx(Button, { mode: "bleed", text: "Import from Mux", onClick: importAssets.openDialog });
1106
1151
  }
1152
+ const PageSelector = (props) => {
1153
+ const page = props.page, setPage = props.setPage;
1154
+ return useEffect(() => {
1155
+ const clamped = Math.min(props.total - 1, Math.max(0, page));
1156
+ page !== clamped && setPage(clamped);
1157
+ }, [page, props.total, setPage]), /* @__PURE__ */ jsxs(Fragment, { children: [
1158
+ /* @__PURE__ */ jsx(
1159
+ Button,
1160
+ {
1161
+ icon: ChevronLeftIcon,
1162
+ mode: "bleed",
1163
+ padding: 3,
1164
+ style: { cursor: "pointer" },
1165
+ disabled: page <= 0,
1166
+ onClick: () => {
1167
+ setPage((page2) => Math.min(props.total - 1, Math.max(0, page2 - 1)));
1168
+ }
1169
+ }
1170
+ ),
1171
+ /* @__PURE__ */ jsxs(Label$1, { muted: !0, children: [
1172
+ "Page ",
1173
+ page + 1,
1174
+ "/",
1175
+ props.total
1176
+ ] }),
1177
+ /* @__PURE__ */ jsx(
1178
+ Button,
1179
+ {
1180
+ icon: ChevronRightIcon,
1181
+ mode: "bleed",
1182
+ padding: 3,
1183
+ style: { cursor: "pointer" },
1184
+ disabled: page >= props.total - 1,
1185
+ onClick: () => {
1186
+ setPage((page2) => Math.min(props.total - 1, Math.max(0, page2 + 1)));
1187
+ }
1188
+ }
1189
+ )
1190
+ ] });
1191
+ };
1107
1192
  function useResyncMuxMetadata() {
1108
1193
  const documentStore = useDocumentStore(), client = useClient$1({
1109
1194
  apiVersion: SANITY_API_VERSION
@@ -1329,55 +1414,1266 @@ function SelectSortOptions(props) {
1329
1414
  }
1330
1415
  );
1331
1416
  }
1332
- const SpinnerBox = () => /* @__PURE__ */ jsx(
1333
- Box,
1334
- {
1335
- style: {
1336
- display: "flex",
1337
- alignItems: "center",
1338
- justifyContent: "center",
1339
- minHeight: "150px"
1340
- },
1341
- children: /* @__PURE__ */ jsx(Spinner, {})
1342
- }
1343
- ), IconInfo = (props) => {
1344
- const Icon = props.icon;
1345
- return /* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", padding: 1, children: [
1346
- /* @__PURE__ */ jsx(Text, { size: (props.size || 1) + 1, muted: !0, children: /* @__PURE__ */ jsx(Icon, {}) }),
1347
- /* @__PURE__ */ jsx(Text, { size: props.size || 1, muted: props.muted, children: props.text })
1417
+ const SpinnerBox = () => /* @__PURE__ */ jsx(
1418
+ Box,
1419
+ {
1420
+ style: {
1421
+ display: "flex",
1422
+ alignItems: "center",
1423
+ justifyContent: "center",
1424
+ minHeight: "150px"
1425
+ },
1426
+ children: /* @__PURE__ */ jsx(Spinner, {})
1427
+ }
1428
+ ), IconInfo = (props) => {
1429
+ const Icon = props.icon;
1430
+ return /* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", padding: 1, children: [
1431
+ /* @__PURE__ */ jsx(Text, { size: (props.size || 1) + 1, muted: !0, children: /* @__PURE__ */ jsx(Icon, {}) }),
1432
+ /* @__PURE__ */ jsx(Text, { size: props.size || 1, muted: props.muted, children: props.text })
1433
+ ] });
1434
+ };
1435
+ function ResolutionIcon(props) {
1436
+ return /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "1em", height: "1em", viewBox: "0 0 24 24", ...props, children: /* @__PURE__ */ jsx(
1437
+ "path",
1438
+ {
1439
+ fill: "currentColor",
1440
+ d: "M20 9V6h-3V4h5v5h-2ZM2 9V4h5v2H4v3H2Zm15 11v-2h3v-3h2v5h-5ZM2 20v-5h2v3h3v2H2Zm4-4V8h12v8H6Zm2-2h8v-4H8v4Zm0 0v-4v4Z"
1441
+ }
1442
+ ) });
1443
+ }
1444
+ function StopWatchIcon(props) {
1445
+ return /* @__PURE__ */ jsxs(
1446
+ "svg",
1447
+ {
1448
+ xmlns: "http://www.w3.org/2000/svg",
1449
+ width: "1em",
1450
+ height: "1em",
1451
+ viewBox: "0 0 512 512",
1452
+ ...props,
1453
+ children: [
1454
+ /* @__PURE__ */ jsx("path", { d: "M232 306.667h48V176h-48v130.667z", fill: "currentColor" }),
1455
+ /* @__PURE__ */ jsx(
1456
+ "path",
1457
+ {
1458
+ 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",
1459
+ fill: "currentColor"
1460
+ }
1461
+ ),
1462
+ /* @__PURE__ */ jsx("path", { d: "M192 32h128v48H192z", fill: "currentColor" })
1463
+ ]
1464
+ }
1465
+ );
1466
+ }
1467
+ function extractErrorMessage(error, defaultMessage = "Failed to process request") {
1468
+ let message = "";
1469
+ if (error && typeof error == "object") {
1470
+ const err = error;
1471
+ message = err.response?.body?.message || err.message || "";
1472
+ } else typeof error == "string" && (message = error);
1473
+ if (!message)
1474
+ return defaultMessage;
1475
+ const match = message.match(/\(([^)]+)\)/);
1476
+ if (match && match[1])
1477
+ return match[1];
1478
+ if (message.includes("responded with")) {
1479
+ const parts = message.split("(");
1480
+ if (parts.length > 1)
1481
+ return parts[parts.length - 1].replace(")", "").trim();
1482
+ }
1483
+ return message;
1484
+ }
1485
+ async function pollTrackStatus(options) {
1486
+ const {
1487
+ client,
1488
+ assetId,
1489
+ trackName,
1490
+ trackLanguageCode,
1491
+ maxAttempts = 10,
1492
+ onTrackFound,
1493
+ onTrackErrored,
1494
+ onTrackReady
1495
+ } = options, trimmedName = trackName.trim(), trimmedLanguageCode = trackLanguageCode.trim();
1496
+ let newTrack, attempts = 0, trackFound = !1;
1497
+ const findTrack = (textTracks) => {
1498
+ let foundTrack = textTracks.find(
1499
+ (track) => track.name === trimmedName && track.language_code === trimmedLanguageCode
1500
+ );
1501
+ return foundTrack || (foundTrack = textTracks.find((track) => track.language_code === trimmedLanguageCode)), !foundTrack && textTracks.length > 0 && (foundTrack = textTracks[textTracks.length - 1]), foundTrack;
1502
+ };
1503
+ for (; attempts < maxAttempts; ) {
1504
+ try {
1505
+ attempts > 0 && await new Promise((resolve) => setTimeout(resolve, 1e3));
1506
+ const textTracks = (await getAsset(client, assetId)).data.tracks?.filter((track) => track.type === "text") || [], foundTrack = findTrack(textTracks);
1507
+ if (!foundTrack) {
1508
+ attempts++;
1509
+ continue;
1510
+ }
1511
+ if (trackFound = !0, newTrack = foundTrack, onTrackFound && onTrackFound(foundTrack), foundTrack.status === "ready") {
1512
+ onTrackReady && onTrackReady(foundTrack);
1513
+ break;
1514
+ }
1515
+ if (foundTrack.status === "errored")
1516
+ return onTrackErrored && onTrackErrored(foundTrack), {
1517
+ track: foundTrack,
1518
+ found: !0,
1519
+ status: "errored"
1520
+ };
1521
+ } catch (error) {
1522
+ console.error("Failed to fetch updated asset:", error);
1523
+ }
1524
+ attempts++;
1525
+ }
1526
+ return !newTrack || !trackFound ? {
1527
+ track: void 0,
1528
+ found: !1,
1529
+ status: "not-found"
1530
+ } : newTrack.status === "preparing" ? {
1531
+ track: newTrack,
1532
+ found: !0,
1533
+ status: "preparing"
1534
+ } : {
1535
+ track: newTrack,
1536
+ found: !0,
1537
+ status: "ready"
1538
+ };
1539
+ }
1540
+ async function downloadVttFile(client, asset, track) {
1541
+ if (!track.id)
1542
+ throw new Error("Track ID is missing");
1543
+ if (track.status !== "ready")
1544
+ throw new Error(`Track is not ready yet. Status: ${track.status}`);
1545
+ if (!asset.assetId)
1546
+ throw new Error("Asset ID is required");
1547
+ const playbackId = getPlaybackId(asset);
1548
+ if (!playbackId)
1549
+ throw new Error("Playback ID is required");
1550
+ const playbackPolicy = getPlaybackPolicy(asset);
1551
+ let downloadUrl = `https://stream.mux.com/${playbackId}/text/${track.id}.vtt`;
1552
+ if (playbackPolicy === "signed") {
1553
+ const token = generateJwt(client, playbackId, "v");
1554
+ downloadUrl += `?token=${token}`;
1555
+ }
1556
+ const response = await fetch(downloadUrl);
1557
+ if (!response.ok)
1558
+ throw new Error(`Failed to download file: ${response.statusText}`);
1559
+ const blob = await response.blob(), blobUrl = URL.createObjectURL(blob), link = document.createElement("a");
1560
+ 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);
1561
+ }
1562
+ const SUPPORTED_MUX_LANGUAGES = [
1563
+ { label: "English", code: "en", state: "Stable" },
1564
+ { label: "Spanish", code: "es", state: "Stable" },
1565
+ { label: "Italian", code: "it", state: "Stable" },
1566
+ { label: "Portuguese", code: "pt", state: "Stable" },
1567
+ { label: "German", code: "de", state: "Stable" },
1568
+ { label: "French", code: "fr", state: "Stable" },
1569
+ { label: "Polish", code: "pl", state: "Beta" },
1570
+ { label: "Russian", code: "ru", state: "Beta" },
1571
+ { label: "Dutch", code: "nl", state: "Beta" },
1572
+ { label: "Catalan", code: "ca", state: "Beta" },
1573
+ { label: "Turkish", code: "tr", state: "Beta" },
1574
+ { label: "Swedish", code: "sv", state: "Beta" },
1575
+ { label: "Ukrainian", code: "uk", state: "Beta" },
1576
+ { label: "Norwegian", code: "no", state: "Beta" },
1577
+ { label: "Finnish", code: "fi", state: "Beta" },
1578
+ { label: "Slovak", code: "sk", state: "Beta" },
1579
+ { label: "Greek", code: "el", state: "Beta" },
1580
+ { label: "Czech", code: "cs", state: "Beta" },
1581
+ { label: "Croatian", code: "hr", state: "Beta" },
1582
+ { label: "Danish", code: "da", state: "Beta" },
1583
+ { label: "Romanian", code: "ro", state: "Beta" },
1584
+ { label: "Bulgarian", code: "bg", state: "Beta" }
1585
+ ];
1586
+ function isCustomTextTrack(track) {
1587
+ return track.type !== "autogenerated";
1588
+ }
1589
+ function isAutogeneratedTrack(track) {
1590
+ return track.type === "autogenerated";
1591
+ }
1592
+ const LANGUAGE_OPTIONS$1 = LanguagesList.getAllCodes().map((code) => ({
1593
+ value: code,
1594
+ label: LanguagesList.getNativeName(code)
1595
+ })), MUX_LANGUAGE_OPTIONS = SUPPORTED_MUX_LANGUAGES.map((lang) => ({
1596
+ value: lang.code,
1597
+ label: lang.label
1598
+ }));
1599
+ function AddCaptionDialog({ asset, onAdd, onClose }) {
1600
+ const client = useClient(), toast = useToast(), dialogId = `AddCaptionDialog${useId()}`, [isAutogenerated, setIsAutogenerated] = useState(!1), [vttUrl, setVttUrl] = useState(""), [languageCode, setLanguageCode] = useState(""), [selectedLanguage, setSelectedLanguage] = useState(
1601
+ null
1602
+ ), [name2, setName] = useState(""), [isSubmitting, setIsSubmitting] = useState(!1), [selectedFile, setSelectedFile] = useState(null), fileInputRef = useRef(null), uploadVttFile = async (file) => (await client.assets.upload("file", file, {
1603
+ filename: file.name
1604
+ })).url, handleAddTrackFromUrl = async () => {
1605
+ if (!asset.assetId)
1606
+ throw new Error("Asset ID is required");
1607
+ const trimmedName = name2.trim(), trimmedLanguageCode = languageCode.trim();
1608
+ let vttUrlToUse = vttUrl.trim();
1609
+ if (selectedFile)
1610
+ try {
1611
+ vttUrlToUse = await uploadVttFile(selectedFile);
1612
+ } catch (uploadError) {
1613
+ throw toast.push({
1614
+ title: "Failed to upload VTT file",
1615
+ status: "error",
1616
+ description: "Could not upload the VTT file to Sanity. Please try again."
1617
+ }), setIsSubmitting(!1), uploadError;
1618
+ }
1619
+ await addTextTrackFromUrl(client, asset.assetId, vttUrlToUse, {
1620
+ language_code: trimmedLanguageCode,
1621
+ name: trimmedName,
1622
+ text_type: "subtitles"
1623
+ });
1624
+ const result = await pollTrackStatus({
1625
+ client,
1626
+ assetId: asset.assetId,
1627
+ trackName: trimmedName,
1628
+ trackLanguageCode: trimmedLanguageCode,
1629
+ onTrackErrored: (track) => {
1630
+ const errorMessage = track.error?.messages?.[0] || track.error?.type || "The track failed to download from the provided URL";
1631
+ toast.push({
1632
+ title: "Caption track failed",
1633
+ status: "error",
1634
+ description: errorMessage
1635
+ }), onAdd(track), onClose();
1636
+ }
1637
+ });
1638
+ if (!result.found || !result.track) {
1639
+ toast.push({
1640
+ title: "Caption track may have been added",
1641
+ status: "warning",
1642
+ 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."
1643
+ }), onClose();
1644
+ return;
1645
+ }
1646
+ if (result.status !== "errored") {
1647
+ if (result.status === "preparing") {
1648
+ toast.push({
1649
+ title: "Caption track is processing",
1650
+ status: "info",
1651
+ description: "The track was created and is being processed. It will appear in the list shortly."
1652
+ }), onAdd(result.track), onClose();
1653
+ return;
1654
+ }
1655
+ toast.push({
1656
+ title: "Caption track added",
1657
+ status: "success",
1658
+ description: "Caption track added successfully"
1659
+ }), onAdd(result.track), onClose();
1660
+ }
1661
+ }, handleGenerateSubtitles = async () => {
1662
+ if (!asset.assetId)
1663
+ throw new Error("Asset ID is required");
1664
+ const audioTrack = (await getAsset(client, asset.assetId)).data.tracks?.find((track) => track.type === "audio");
1665
+ if (!audioTrack || !audioTrack.id)
1666
+ throw toast.push({
1667
+ title: "No audio track found",
1668
+ status: "error",
1669
+ description: "The asset does not have an audio track. Auto-generated subtitles require an audio track."
1670
+ }), new Error("No audio track found");
1671
+ await generateSubtitles(client, asset.assetId, audioTrack.id, {
1672
+ language_code: languageCode.trim(),
1673
+ name: name2.trim()
1674
+ });
1675
+ const mockTrack = {
1676
+ type: "text",
1677
+ id: `generating-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
1678
+ text_type: "subtitles",
1679
+ text_source: "generated_live",
1680
+ language_code: languageCode.trim(),
1681
+ name: name2.trim(),
1682
+ status: "preparing"
1683
+ };
1684
+ toast.push({
1685
+ title: "Generating subtitles",
1686
+ status: "success",
1687
+ description: "This may take a few minutes"
1688
+ }), onAdd(mockTrack), onClose();
1689
+ }, handleSubmit = async () => {
1690
+ if (!isAutogenerated) {
1691
+ if (!selectedFile && !vttUrl.trim()) {
1692
+ toast.push({
1693
+ title: "VTT file or URL required",
1694
+ status: "error",
1695
+ description: "Please select a VTT file or enter a VTT file URL"
1696
+ });
1697
+ return;
1698
+ }
1699
+ if (vttUrl.trim() && !selectedFile)
1700
+ try {
1701
+ new URL(vttUrl.trim());
1702
+ } catch {
1703
+ toast.push({
1704
+ title: "Invalid URL",
1705
+ status: "error",
1706
+ description: "Please enter a valid URL (e.g., https://example.com/subtitles.vtt)"
1707
+ });
1708
+ return;
1709
+ }
1710
+ }
1711
+ if (!name2.trim()) {
1712
+ toast.push({
1713
+ title: "Audio name required",
1714
+ status: "error",
1715
+ description: "Please enter an audio name for this caption track"
1716
+ });
1717
+ return;
1718
+ }
1719
+ if (!languageCode.trim()) {
1720
+ toast.push({
1721
+ title: "Language code required",
1722
+ status: "error",
1723
+ description: "Please enter a language code (e.g., en, es, fr)"
1724
+ });
1725
+ return;
1726
+ }
1727
+ setIsSubmitting(!0);
1728
+ try {
1729
+ isAutogenerated ? await handleGenerateSubtitles() : await handleAddTrackFromUrl();
1730
+ } catch (error) {
1731
+ toast.push({
1732
+ title: "Failed to add caption track",
1733
+ status: "error",
1734
+ description: extractErrorMessage(error, "Failed to add caption track")
1735
+ });
1736
+ } finally {
1737
+ setIsSubmitting(!1);
1738
+ }
1739
+ };
1740
+ return /* @__PURE__ */ jsx(
1741
+ Dialog,
1742
+ {
1743
+ id: dialogId,
1744
+ header: "Add Caption Track",
1745
+ onClose,
1746
+ width: 1,
1747
+ onClickOutside: onClose,
1748
+ children: /* @__PURE__ */ jsxs(Stack, { padding: 4, space: 4, children: [
1749
+ /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
1750
+ /* @__PURE__ */ jsxs(Flex, { align: "center", marginBottom: 3, children: [
1751
+ /* @__PURE__ */ jsx(
1752
+ Checkbox,
1753
+ {
1754
+ id: "autogenerated-checkbox",
1755
+ style: { display: "block" },
1756
+ checked: isAutogenerated,
1757
+ onChange: (e) => {
1758
+ setIsAutogenerated(e.currentTarget.checked), e.currentTarget.checked && setVttUrl("");
1759
+ },
1760
+ disabled: isSubmitting
1761
+ }
1762
+ ),
1763
+ /* @__PURE__ */ jsx(Flex, { flex: 1, paddingLeft: 2, children: /* @__PURE__ */ jsx(Text, { children: /* @__PURE__ */ jsx("label", { htmlFor: "autogenerated-checkbox", children: "Generate captions" }) }) })
1764
+ ] }),
1765
+ !isAutogenerated && /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
1766
+ /* @__PURE__ */ jsxs(Card, { padding: 3, marginBottom: 2, tone: "transparent", border: !0, radius: 2, children: [
1767
+ /* @__PURE__ */ jsxs(Flex, { align: "center", justify: "space-between", children: [
1768
+ /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: selectedFile ? `Selected: ${selectedFile.name}` : "No file selected" }),
1769
+ /* @__PURE__ */ jsx(
1770
+ Button,
1771
+ {
1772
+ icon: UploadIcon,
1773
+ text: "Select File",
1774
+ mode: "ghost",
1775
+ tone: "primary",
1776
+ fontSize: 1,
1777
+ padding: 2,
1778
+ onClick: () => fileInputRef.current?.click(),
1779
+ disabled: isSubmitting
1780
+ }
1781
+ )
1782
+ ] }),
1783
+ /* @__PURE__ */ jsx(
1784
+ "input",
1785
+ {
1786
+ ref: fileInputRef,
1787
+ type: "file",
1788
+ accept: ".vtt,text/vtt",
1789
+ style: { display: "none" },
1790
+ onChange: (e) => {
1791
+ e.target.files && e.target.files.length > 0 && !isSubmitting && (setSelectedFile(e.target.files[0]), setVttUrl(""));
1792
+ }
1793
+ }
1794
+ )
1795
+ ] }),
1796
+ /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, style: { textAlign: "center" }, children: "Or enter the VTT file URL" }),
1797
+ /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
1798
+ /* @__PURE__ */ jsx(Label$1, { htmlFor: "vtt-url", children: "VTT File URL" }),
1799
+ /* @__PURE__ */ jsx(
1800
+ TextInput,
1801
+ {
1802
+ id: "vtt-url",
1803
+ placeholder: "https://example.com/subtitles.vtt",
1804
+ value: vttUrl,
1805
+ onChange: (e) => {
1806
+ setVttUrl(e.currentTarget.value), setSelectedFile(null);
1807
+ },
1808
+ disabled: isSubmitting
1809
+ }
1810
+ )
1811
+ ] })
1812
+ ] })
1813
+ ] }),
1814
+ /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
1815
+ /* @__PURE__ */ jsx(Label$1, { htmlFor: "caption-name", children: "Audio name" }),
1816
+ /* @__PURE__ */ jsx(
1817
+ Autocomplete,
1818
+ {
1819
+ id: "caption-name",
1820
+ value: selectedLanguage?.value || "",
1821
+ onChange: (newValue) => {
1822
+ const selected = (isAutogenerated ? MUX_LANGUAGE_OPTIONS : LANGUAGE_OPTIONS$1).find((opt) => opt.value === newValue);
1823
+ selected && (setSelectedLanguage(selected), setLanguageCode(selected.value), setName(selected.label));
1824
+ },
1825
+ options: isAutogenerated ? MUX_LANGUAGE_OPTIONS : LANGUAGE_OPTIONS$1,
1826
+ icon: TranslateIcon,
1827
+ placeholder: "Select language",
1828
+ filterOption: (query, option) => option.label.toLowerCase().indexOf(query.toLowerCase()) > -1 || option.value.toLowerCase().indexOf(query.toLowerCase()) > -1,
1829
+ openButton: !0,
1830
+ renderValue: (value) => (isAutogenerated ? MUX_LANGUAGE_OPTIONS : LANGUAGE_OPTIONS$1).find(
1831
+ (l) => l.value === value
1832
+ )?.label || value,
1833
+ renderOption: (option) => /* @__PURE__ */ jsx(Card, { "data-as": "button", padding: 3, radius: 2, tone: "inherit", children: /* @__PURE__ */ jsxs(Text, { size: 2, textOverflow: "ellipsis", children: [
1834
+ option.label,
1835
+ " (",
1836
+ option.value,
1837
+ ")"
1838
+ ] }) }),
1839
+ disabled: isSubmitting
1840
+ }
1841
+ )
1842
+ ] }),
1843
+ /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
1844
+ /* @__PURE__ */ jsx(Label$1, { htmlFor: "caption-language", children: "Language Code" }),
1845
+ /* @__PURE__ */ jsx(
1846
+ TextInput,
1847
+ {
1848
+ id: "caption-language",
1849
+ placeholder: "en-US",
1850
+ value: languageCode,
1851
+ onChange: (e) => {
1852
+ setLanguageCode(e.currentTarget.value), selectedLanguage && selectedLanguage.value !== e.currentTarget.value && (setSelectedLanguage(null), (!name2 || name2 === selectedLanguage.label) && setName(""));
1853
+ },
1854
+ disabled: isSubmitting
1855
+ }
1856
+ )
1857
+ ] }),
1858
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, justify: "flex-end", marginTop: 2, children: [
1859
+ /* @__PURE__ */ jsx(Button, { text: "Cancel", mode: "ghost", onClick: onClose, disabled: isSubmitting }),
1860
+ /* @__PURE__ */ jsx(
1861
+ Button,
1862
+ {
1863
+ text: "Add Caption Track",
1864
+ tone: "primary",
1865
+ icon: isSubmitting ? /* @__PURE__ */ jsx(
1866
+ Spinner,
1867
+ {
1868
+ style: {
1869
+ verticalAlign: "middle",
1870
+ display: "inline-block",
1871
+ marginBottom: "-3px",
1872
+ width: "1em",
1873
+ height: "1em",
1874
+ marginRight: "-6px"
1875
+ }
1876
+ }
1877
+ ) : /* @__PURE__ */ jsx(UploadIcon, {}),
1878
+ onClick: handleSubmit,
1879
+ disabled: isSubmitting
1880
+ }
1881
+ )
1882
+ ] })
1883
+ ] })
1884
+ }
1885
+ );
1886
+ }
1887
+ const LANGUAGE_OPTIONS = LanguagesList.getAllCodes().map((code) => ({
1888
+ value: code,
1889
+ label: LanguagesList.getNativeName(code)
1890
+ }));
1891
+ function EditCaptionDialog({ asset, track, onUpdate, onClose }) {
1892
+ const client = useClient(), toast = useToast(), dialogId = `EditCaptionDialog${useId()}`, isAutogenerated = track.text_source === "generated_live" || track.text_source === "generated_live_final" || track.text_source === "generated_vod", [vttUrl, setVttUrl] = useState(""), [languageCode, setLanguageCode] = useState(track.language_code || ""), [selectedLanguage, setSelectedLanguage] = useState(
1893
+ () => {
1894
+ const baseCode = track.language_code?.split("-")[0], found = LANGUAGE_OPTIONS.find(
1895
+ (opt) => opt.value === track.language_code || opt.value === baseCode
1896
+ );
1897
+ if (found) return found;
1898
+ if (track.name) {
1899
+ const foundByName = LANGUAGE_OPTIONS.find((opt) => opt.label === track.name);
1900
+ if (foundByName) return foundByName;
1901
+ }
1902
+ return null;
1903
+ }
1904
+ ), [name2, setName] = useState(track.name || ""), [isSubmitting, setIsSubmitting] = useState(!1), [downloading, setDownloading] = useState(!1), [selectedFile, setSelectedFile] = useState(null), fileInputRef = useRef(null);
1905
+ useEffect(() => {
1906
+ setLanguageCode(track.language_code || ""), setName(track.name || ""), setVttUrl("");
1907
+ const baseCode = track.language_code?.split("-")[0], foundByCode = LANGUAGE_OPTIONS.find(
1908
+ (opt) => opt.value === track.language_code || opt.value === baseCode
1909
+ ), foundByName = track.name ? LANGUAGE_OPTIONS.find((opt) => opt.label === track.name) : null;
1910
+ setSelectedLanguage(foundByCode || foundByName || null);
1911
+ }, [track, asset, client]);
1912
+ const handleDownloadCurrentFile = async () => {
1913
+ setDownloading(!0);
1914
+ try {
1915
+ await downloadVttFile(client, asset, track);
1916
+ } catch (error) {
1917
+ let errorMessage = "Please try again", title = "Failed to download VTT file";
1918
+ 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({
1919
+ title,
1920
+ status: "error",
1921
+ description: errorMessage
1922
+ });
1923
+ } finally {
1924
+ setDownloading(!1);
1925
+ }
1926
+ }, 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, {
1927
+ filename: file.name
1928
+ })).url, refreshAssetData = async () => {
1929
+ if (!(!asset._id || !asset.assetId))
1930
+ try {
1931
+ const latestAssetData = await getAsset(client, asset.assetId);
1932
+ await client.patch(asset._id).set({ data: latestAssetData.data, status: latestAssetData.data.status }).commit();
1933
+ } catch (refreshError) {
1934
+ console.error("Failed to refresh asset data:", refreshError);
1935
+ }
1936
+ }, handleUpdateTrackWithNewUrl = async () => {
1937
+ if (!asset.assetId)
1938
+ throw new Error("Asset ID is required");
1939
+ const trimmedName = name2.trim(), trimmedLanguageCode = languageCode.trim(), oldTrackId = track.id;
1940
+ try {
1941
+ await deleteTextTrack(client, asset.assetId, oldTrackId);
1942
+ } catch (deleteError) {
1943
+ throw toast.push({
1944
+ title: "Failed to delete old track",
1945
+ status: "error",
1946
+ description: "Could not delete the old track. Please try again or delete it manually."
1947
+ }), setIsSubmitting(!1), deleteError;
1948
+ }
1949
+ let vttUrlToUse = vttUrl.trim();
1950
+ if (selectedFile)
1951
+ try {
1952
+ vttUrlToUse = await uploadVttFile(selectedFile);
1953
+ } catch (uploadError) {
1954
+ throw toast.push({
1955
+ title: "Failed to upload VTT file",
1956
+ status: "error",
1957
+ description: "Could not upload the VTT file to Sanity. Please try again."
1958
+ }), setIsSubmitting(!1), uploadError;
1959
+ }
1960
+ try {
1961
+ await addTextTrackFromUrl(client, asset.assetId, vttUrlToUse, {
1962
+ language_code: trimmedLanguageCode,
1963
+ name: trimmedName,
1964
+ text_type: "subtitles"
1965
+ });
1966
+ } catch (error) {
1967
+ throw toast.push({
1968
+ title: "Failed to update caption track",
1969
+ status: "error",
1970
+ description: extractErrorMessage(error, "Failed to update caption track")
1971
+ }), setIsSubmitting(!1), error;
1972
+ }
1973
+ const result = await pollTrackStatus({
1974
+ client,
1975
+ assetId: asset.assetId,
1976
+ trackName: trimmedName,
1977
+ trackLanguageCode: trimmedLanguageCode,
1978
+ onTrackErrored: async (erroredTrack) => {
1979
+ const errorMessage = erroredTrack.error?.messages?.[0] || erroredTrack.error?.type || "The track failed to download from the provided URL";
1980
+ toast.push({
1981
+ title: "Caption track failed",
1982
+ status: "error",
1983
+ description: errorMessage
1984
+ }), await refreshAssetData(), onUpdate(erroredTrack, oldTrackId), setIsSubmitting(!1);
1985
+ }
1986
+ });
1987
+ if (!result.found || !result.track) {
1988
+ toast.push({
1989
+ title: "Caption track may have been updated",
1990
+ status: "warning",
1991
+ 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."
1992
+ }), setIsSubmitting(!1);
1993
+ return;
1994
+ }
1995
+ result.status !== "errored" && (await refreshAssetData(), result.status === "preparing" ? toast.push({
1996
+ title: "Caption track is processing",
1997
+ status: "info",
1998
+ description: "The track was updated and is being processed. It will appear in the list shortly."
1999
+ }) : toast.push({
2000
+ title: "Caption track updated",
2001
+ status: "success",
2002
+ description: "Caption track updated successfully"
2003
+ }), onUpdate(result.track, oldTrackId), setIsSubmitting(!1));
2004
+ }, handleSubmit = async () => {
2005
+ if (!name2.trim()) {
2006
+ toast.push({
2007
+ title: "Audio name required",
2008
+ status: "error",
2009
+ description: "Please enter an audio name for this caption track"
2010
+ });
2011
+ return;
2012
+ }
2013
+ if (!languageCode.trim()) {
2014
+ toast.push({
2015
+ title: "Language code required",
2016
+ status: "error",
2017
+ description: "Please enter a language code (e.g., en, es, fr)"
2018
+ });
2019
+ return;
2020
+ }
2021
+ setIsSubmitting(!0);
2022
+ try {
2023
+ if (!asset.assetId)
2024
+ throw new Error("Asset ID is required");
2025
+ const originalVttUrl = (() => {
2026
+ if (isAutogenerated || !track.id) return "";
2027
+ const playbackId = getPlaybackId(asset);
2028
+ if (!playbackId) return "";
2029
+ let url = `https://stream.mux.com/${playbackId}/text/${track.id}.vtt`;
2030
+ if (getPlaybackPolicy(asset) === "signed") {
2031
+ const token = generateJwt(client, playbackId, "v");
2032
+ url += `?token=${token}`;
2033
+ }
2034
+ return url;
2035
+ })(), urlChanged = selectedFile !== null || vttUrl.trim() && vttUrl.trim() !== originalVttUrl;
2036
+ if (!urlChanged) {
2037
+ toast.push({
2038
+ title: "No changes",
2039
+ status: "info",
2040
+ description: 'Please provide a new VTT file or URL using the "Replace" button or URL field to update the track.'
2041
+ }), setIsSubmitting(!1);
2042
+ return;
2043
+ }
2044
+ if (urlChanged) {
2045
+ if (!selectedFile && vttUrl.trim())
2046
+ try {
2047
+ new URL(vttUrl.trim());
2048
+ } catch {
2049
+ toast.push({
2050
+ title: "Invalid URL",
2051
+ status: "error",
2052
+ description: "Please enter a valid URL (e.g., https://example.com/subtitles.vtt)"
2053
+ }), setIsSubmitting(!1);
2054
+ return;
2055
+ }
2056
+ if (!selectedFile && !vttUrl.trim()) {
2057
+ toast.push({
2058
+ title: "VTT file or URL required",
2059
+ status: "error",
2060
+ description: "Please select a VTT file or enter a VTT file URL"
2061
+ }), setIsSubmitting(!1);
2062
+ return;
2063
+ }
2064
+ await handleUpdateTrackWithNewUrl();
2065
+ }
2066
+ onClose();
2067
+ } catch (error) {
2068
+ toast.push({
2069
+ title: "Failed to update caption track",
2070
+ status: "error",
2071
+ description: error instanceof Error ? error.message : "Please try again"
2072
+ });
2073
+ } finally {
2074
+ setIsSubmitting(!1);
2075
+ }
2076
+ };
2077
+ return /* @__PURE__ */ jsx(
2078
+ Dialog,
2079
+ {
2080
+ id: dialogId,
2081
+ header: "Edit Caption Track",
2082
+ onClose,
2083
+ width: 1,
2084
+ onClickOutside: onClose,
2085
+ children: /* @__PURE__ */ jsxs(Stack, { padding: 4, space: 4, children: [
2086
+ /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
2087
+ /* @__PURE__ */ jsxs(Card, { padding: 3, marginBottom: 2, tone: "transparent", border: !0, radius: 2, children: [
2088
+ /* @__PURE__ */ jsxs(Flex, { align: "center", justify: "space-between", children: [
2089
+ /* @__PURE__ */ jsx(Text, { children: getCurrentFileName() }),
2090
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
2091
+ track.status !== "errored" && /* @__PURE__ */ jsx(
2092
+ Button,
2093
+ {
2094
+ icon: downloading ? /* @__PURE__ */ jsx(
2095
+ Spinner,
2096
+ {
2097
+ style: {
2098
+ verticalAlign: "middle",
2099
+ display: "inline-block",
2100
+ marginTop: "-2px",
2101
+ width: "0.5em",
2102
+ height: "0.5em"
2103
+ }
2104
+ }
2105
+ ) : /* @__PURE__ */ jsx(DownloadIcon, {}),
2106
+ text: "Download",
2107
+ mode: "ghost",
2108
+ tone: "primary",
2109
+ fontSize: 1,
2110
+ padding: 2,
2111
+ onClick: handleDownloadCurrentFile,
2112
+ disabled: downloading || isSubmitting
2113
+ }
2114
+ ),
2115
+ /* @__PURE__ */ jsx(
2116
+ Button,
2117
+ {
2118
+ icon: UploadIcon,
2119
+ text: "Replace",
2120
+ mode: "ghost",
2121
+ tone: "primary",
2122
+ fontSize: 1,
2123
+ padding: 2,
2124
+ onClick: () => fileInputRef.current?.click(),
2125
+ disabled: isSubmitting
2126
+ }
2127
+ )
2128
+ ] })
2129
+ ] }),
2130
+ /* @__PURE__ */ jsx(
2131
+ "input",
2132
+ {
2133
+ ref: fileInputRef,
2134
+ type: "file",
2135
+ accept: ".vtt,text/vtt",
2136
+ style: { display: "none" },
2137
+ onChange: (e) => {
2138
+ e.target.files && e.target.files.length > 0 && !isSubmitting && (setSelectedFile(e.target.files[0]), setVttUrl(""));
2139
+ }
2140
+ }
2141
+ ),
2142
+ selectedFile && /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, style: { marginTop: 8 }, children: [
2143
+ "Selected: ",
2144
+ selectedFile.name
2145
+ ] })
2146
+ ] }),
2147
+ /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
2148
+ /* @__PURE__ */ jsx(Label$1, { htmlFor: "vtt-url", children: "VTT File URL" }),
2149
+ /* @__PURE__ */ jsx(
2150
+ TextInput,
2151
+ {
2152
+ id: "vtt-url",
2153
+ placeholder: "https://example.com/subtitles.vtt",
2154
+ value: vttUrl,
2155
+ onChange: (e) => {
2156
+ setVttUrl(e.currentTarget.value), setSelectedFile(null);
2157
+ },
2158
+ disabled: isSubmitting
2159
+ }
2160
+ ),
2161
+ /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Add a URL to replace the existing VTT file with a new one" })
2162
+ ] })
2163
+ ] }),
2164
+ /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
2165
+ /* @__PURE__ */ jsx(Label$1, { htmlFor: "caption-name", children: "Audio name" }),
2166
+ /* @__PURE__ */ jsx(
2167
+ Autocomplete,
2168
+ {
2169
+ id: "caption-name",
2170
+ value: selectedLanguage?.value || "",
2171
+ onChange: (newValue) => {
2172
+ const selected = LANGUAGE_OPTIONS.find((opt) => opt.value === newValue);
2173
+ selected && (setSelectedLanguage(selected), setLanguageCode(selected.value), setName(selected.label));
2174
+ },
2175
+ options: LANGUAGE_OPTIONS,
2176
+ icon: TranslateIcon,
2177
+ placeholder: "Select language",
2178
+ filterOption: (query, option) => option.label.toLowerCase().indexOf(query.toLowerCase()) > -1 || option.value.toLowerCase().indexOf(query.toLowerCase()) > -1,
2179
+ openButton: !0,
2180
+ renderValue: (value) => LANGUAGE_OPTIONS.find((l) => l.value === value)?.label || value,
2181
+ renderOption: (option) => /* @__PURE__ */ jsx(Card, { "data-as": "button", padding: 3, radius: 2, tone: "inherit", children: /* @__PURE__ */ jsxs(Text, { size: 2, textOverflow: "ellipsis", children: [
2182
+ option.label,
2183
+ " (",
2184
+ option.value,
2185
+ ")"
2186
+ ] }) }),
2187
+ disabled: isSubmitting
2188
+ }
2189
+ )
2190
+ ] }),
2191
+ /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
2192
+ /* @__PURE__ */ jsx(Label$1, { htmlFor: "caption-language", children: "Language Code" }),
2193
+ /* @__PURE__ */ jsx(
2194
+ TextInput,
2195
+ {
2196
+ id: "caption-language",
2197
+ placeholder: "en-US",
2198
+ value: languageCode,
2199
+ onChange: (e) => {
2200
+ setLanguageCode(e.currentTarget.value), selectedLanguage && selectedLanguage.value !== e.currentTarget.value && (setSelectedLanguage(null), (!name2 || name2 === selectedLanguage.label) && setName(""));
2201
+ },
2202
+ disabled: isSubmitting
2203
+ }
2204
+ )
2205
+ ] }),
2206
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, justify: "flex-end", marginTop: 2, children: [
2207
+ /* @__PURE__ */ jsx(Button, { text: "Cancel", mode: "ghost", onClick: onClose, disabled: isSubmitting }),
2208
+ /* @__PURE__ */ jsx(
2209
+ Button,
2210
+ {
2211
+ text: "Update Caption Track",
2212
+ tone: "primary",
2213
+ icon: isSubmitting ? /* @__PURE__ */ jsx(
2214
+ Spinner,
2215
+ {
2216
+ style: {
2217
+ verticalAlign: "middle",
2218
+ display: "inline-block",
2219
+ marginBottom: "-3px",
2220
+ width: "1em",
2221
+ height: "1em",
2222
+ marginRight: "-6px"
2223
+ }
2224
+ }
2225
+ ) : UploadIcon,
2226
+ onClick: handleSubmit,
2227
+ disabled: isSubmitting
2228
+ }
2229
+ )
2230
+ ] })
2231
+ ] })
2232
+ }
2233
+ );
2234
+ }
2235
+ function TrackCard({
2236
+ track,
2237
+ iconOnly,
2238
+ downloadingTrackId,
2239
+ deletingTrackId,
2240
+ trackToEdit,
2241
+ getTrackSourceLabel,
2242
+ handleDownload,
2243
+ setTrackToEdit,
2244
+ setTrackToDelete
2245
+ }) {
2246
+ 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__ */ jsxs(Flex, { align: "center", gap: 2, children: [
2247
+ /* @__PURE__ */ jsx(
2248
+ Spinner,
2249
+ {
2250
+ muted: !0,
2251
+ style: {
2252
+ width: "0.75em",
2253
+ height: "0.75em",
2254
+ verticalAlign: "middle",
2255
+ display: "inline-block",
2256
+ marginBottom: "-2px"
2257
+ }
2258
+ }
2259
+ ),
2260
+ /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Processing..." })
2261
+ ] }) : /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
2262
+ track.status !== "errored" && /* @__PURE__ */ jsx(
2263
+ Button,
2264
+ {
2265
+ icon: downloadingTrackId === track.id ? /* @__PURE__ */ jsx(
2266
+ Spinner,
2267
+ {
2268
+ style: {
2269
+ verticalAlign: "middle",
2270
+ display: "inline-block",
2271
+ marginTop: "-2px",
2272
+ width: "0.5em",
2273
+ height: "0.5em"
2274
+ }
2275
+ }
2276
+ ) : /* @__PURE__ */ jsx(DownloadIcon, {}),
2277
+ text: iconOnly ? void 0 : "Download",
2278
+ mode: "ghost",
2279
+ tone: "primary",
2280
+ fontSize: 1,
2281
+ padding: 2,
2282
+ onClick: () => handleDownload(track),
2283
+ disabled: isDisabled("download"),
2284
+ title: "Download"
2285
+ }
2286
+ ),
2287
+ /* @__PURE__ */ jsx(
2288
+ Button,
2289
+ {
2290
+ icon: /* @__PURE__ */ jsx(EditIcon, {}),
2291
+ text: iconOnly ? void 0 : "Edit",
2292
+ mode: "ghost",
2293
+ tone: "primary",
2294
+ fontSize: 1,
2295
+ padding: 2,
2296
+ disabled: isDisabled("edit"),
2297
+ onClick: () => setTrackToEdit(track),
2298
+ title: "Edit"
2299
+ }
2300
+ ),
2301
+ /* @__PURE__ */ jsx(
2302
+ Button,
2303
+ {
2304
+ icon: deletingTrackId === track.id ? /* @__PURE__ */ jsx(
2305
+ Spinner,
2306
+ {
2307
+ style: {
2308
+ verticalAlign: "middle",
2309
+ display: "inline-block",
2310
+ marginTop: "-2px",
2311
+ width: "0.5em",
2312
+ height: "0.5em"
2313
+ }
2314
+ }
2315
+ ) : /* @__PURE__ */ jsx(TrashIcon, {}),
2316
+ text: iconOnly ? void 0 : "Delete",
2317
+ mode: "ghost",
2318
+ tone: "critical",
2319
+ fontSize: 1,
2320
+ padding: 2,
2321
+ disabled: isDisabled("delete"),
2322
+ onClick: () => setTrackToDelete(track),
2323
+ title: "Delete"
2324
+ }
2325
+ )
1348
2326
  ] });
1349
- };
1350
- function ResolutionIcon(props) {
1351
- return /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "1em", height: "1em", viewBox: "0 0 24 24", ...props, children: /* @__PURE__ */ jsx(
1352
- "path",
2327
+ return /* @__PURE__ */ jsx(
2328
+ Card,
1353
2329
  {
1354
- fill: "currentColor",
1355
- d: "M20 9V6h-3V4h5v5h-2ZM2 9V4h5v2H4v3H2Zm15 11v-2h3v-3h2v5h-5ZM2 20v-5h2v3h3v2H2Zm4-4V8h12v8H6Zm2-2h8v-4H8v4Zm0 0v-4v4Z"
2330
+ padding: 3,
2331
+ radius: 2,
2332
+ tone: track.status === "errored" ? "caution" : "transparent",
2333
+ border: !0,
2334
+ children: /* @__PURE__ */ jsxs(Flex, { align: "center", justify: "space-between", gap: 3, children: [
2335
+ /* @__PURE__ */ jsxs(Stack, { space: 2, flex: 1, children: [
2336
+ /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
2337
+ /* @__PURE__ */ jsx(Text, { weight: "semibold", children: track.name || "Untitled" }),
2338
+ /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
2339
+ "(",
2340
+ getTrackSourceLabel(track),
2341
+ ")"
2342
+ ] }),
2343
+ track.status === "errored" && /* @__PURE__ */ jsx(
2344
+ ErrorOutlineIcon,
2345
+ {
2346
+ style: { color: "var(--card-critical-color)" },
2347
+ "aria-label": "Error",
2348
+ fontSize: 20
2349
+ }
2350
+ )
2351
+ ] }),
2352
+ track.language_code && /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
2353
+ "Language: ",
2354
+ track.language_code
2355
+ ] }),
2356
+ track.status === "errored" && track.error && /* @__PURE__ */ jsx(Text, { size: 1, style: { color: "var(--card-critical-color)" }, children: track.error.messages?.[0] || track.error.type || "Failed to process track" })
2357
+ ] }),
2358
+ renderActionButtons()
2359
+ ] })
1356
2360
  }
1357
- ) });
2361
+ );
1358
2362
  }
1359
- function StopWatchIcon(props) {
1360
- return /* @__PURE__ */ jsxs(
1361
- "svg",
1362
- {
1363
- xmlns: "http://www.w3.org/2000/svg",
1364
- width: "1em",
1365
- height: "1em",
1366
- viewBox: "0 0 512 512",
1367
- ...props,
1368
- children: [
1369
- /* @__PURE__ */ jsx("path", { d: "M232 306.667h48V176h-48v130.667z", fill: "currentColor" }),
1370
- /* @__PURE__ */ jsx(
1371
- "path",
1372
- {
1373
- d: "M407.67 170.271l30.786-30.786-33.942-33.941-30.785 30.786C341.217 111.057 300.369 96 256 96 149.961 96 64 181.961 64 288s85.961 192 192 192 192-85.961 192-192c0-44.369-15.057-85.217-40.33-117.729zm-45.604 223.795C333.734 422.398 296.066 438 256 438s-77.735-15.602-106.066-43.934C121.602 365.735 106 328.066 106 288s15.602-77.735 43.934-106.066C178.265 153.602 215.934 138 256 138s77.734 15.602 106.066 43.934C390.398 210.265 406 247.934 406 288s-15.602 77.735-43.934 106.066z",
1374
- fill: "currentColor"
2363
+ function TextTracksManager({
2364
+ asset,
2365
+ iconOnly = !1,
2366
+ tracks: propTracks,
2367
+ collapseTracks = !1
2368
+ }) {
2369
+ const client = useClient(), toast = useToast(), dialogId = `DeleteCaptionDialog${useId()}`, [downloadingTrackId, setDownloadingTrackId] = useState(null), [deletingTrackId, setDeletingTrackId] = useState(null), [addedTracks, setAddedTracks] = useState([]), [updatedTracks, setUpdatedTracks] = useState(/* @__PURE__ */ new Map()), [trackActivityOrder, setTrackActivityOrder] = useState(/* @__PURE__ */ new Map()), [autogeneratedTrackIds, setAutogeneratedTrackIds] = useState(/* @__PURE__ */ new Set()), [trackToDelete, setTrackToDelete] = useState(null), [trackToEdit, setTrackToEdit] = useState(null), [showAddDialog, setShowAddDialog] = useState(!1), [isExpanded, setIsExpanded] = useState(!1), MAX_VISIBLE_TRACKS = 4;
2370
+ useEffect(() => {
2371
+ if (!asset.assetId || !asset._id) return;
2372
+ const assetId = asset.assetId, documentId = asset._id;
2373
+ (async () => {
2374
+ try {
2375
+ const response = await getAsset(client, assetId);
2376
+ await client.patch(documentId).set({ data: response.data, status: response.data.status }).commit();
2377
+ } catch (error) {
2378
+ console.error("Failed to refresh asset data:", error);
2379
+ }
2380
+ })();
2381
+ }, [asset.assetId, asset._id, client]);
2382
+ const activeTracks = (propTracks || asset.data?.tracks?.filter((track) => track.type === "text") || []).filter(
2383
+ (track) => track.id && (track.status === "ready" || track.status === "preparing" || track.status === "errored")
2384
+ ), allTracks = useMemo(() => {
2385
+ const tracksWithUpdates = activeTracks.map((track) => updatedTracks.get(track.id) || track), isMockTrackReplaced = (mockTrack, realTracksList) => !mockTrack.id || !mockTrack.id.startsWith("generating-") ? !1 : realTracksList.some((realTrack) => {
2386
+ const nameMatches = realTrack.name === mockTrack.name, languageMatches = realTrack.language_code === mockTrack.language_code;
2387
+ 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";
2388
+ }), 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));
2389
+ return [...tracksWithUpdates, ...tracksToKeep];
2390
+ }, [activeTracks, addedTracks, updatedTracks]);
2391
+ useEffect(() => {
2392
+ const newAutogeneratedIds = /* @__PURE__ */ new Set();
2393
+ activeTracks.forEach((track) => {
2394
+ track.id && (track.text_source === "generated_live" || track.text_source === "generated_live_final" || track.text_source === "generated_vod") && newAutogeneratedIds.add(track.id);
2395
+ }), addedTracks.forEach((mockTrack) => {
2396
+ if (mockTrack.id && mockTrack.id.startsWith("generating-")) {
2397
+ const realTrack = activeTracks.find((rt) => {
2398
+ const nameMatches = rt.name === mockTrack.name, languageMatches = rt.language_code === mockTrack.language_code;
2399
+ return nameMatches && languageMatches;
2400
+ });
2401
+ realTrack?.id && newAutogeneratedIds.add(realTrack.id);
2402
+ }
2403
+ }), setAutogeneratedTrackIds((prev) => {
2404
+ let hasNew = !1;
2405
+ const updated = new Set(prev);
2406
+ return newAutogeneratedIds.forEach((id) => {
2407
+ prev.has(id) || (updated.add(id), hasNew = !0);
2408
+ }), hasNew ? updated : prev;
2409
+ });
2410
+ }, [activeTracks, addedTracks]), useEffect(() => {
2411
+ if (allTracks.filter((track) => track.status === "preparing").length === 0 || !asset.assetId || !asset._id)
2412
+ return;
2413
+ const assetId = asset.assetId, documentId = asset._id, interval = setInterval(async () => {
2414
+ try {
2415
+ const response = await getAsset(client, assetId);
2416
+ await client.patch(documentId).set({ data: response.data, status: response.data.status }).commit();
2417
+ const fetchedTracks = response.data.tracks?.filter((track) => track.type === "text") || [], isMockTrackReplaced = (mockTrack, fetchedTracksList) => !mockTrack.id || !mockTrack.id.startsWith("generating-") ? !1 : fetchedTracksList.some((realTrack) => {
2418
+ const nameMatches = realTrack.name === mockTrack.name, languageMatches = realTrack.language_code === mockTrack.language_code;
2419
+ 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";
2420
+ }), newAutogeneratedIds = /* @__PURE__ */ new Set();
2421
+ fetchedTracks.forEach((track) => {
2422
+ track.id && (track.text_source === "generated_live" || track.text_source === "generated_live_final" || track.text_source === "generated_vod") && newAutogeneratedIds.add(track.id);
2423
+ });
2424
+ const findMatchingRealTrack = (mockTrack, tracksList) => tracksList.find((rt) => {
2425
+ const nameMatches = rt.name === mockTrack.name, languageMatches = rt.language_code === mockTrack.language_code;
2426
+ return nameMatches && languageMatches;
2427
+ });
2428
+ setAddedTracks((prev) => prev.filter((mockTrack) => {
2429
+ if (mockTrack.id && mockTrack.id.startsWith("generating-")) {
2430
+ const replaced = isMockTrackReplaced(mockTrack, fetchedTracks);
2431
+ if (replaced) {
2432
+ const realTrack = findMatchingRealTrack(mockTrack, fetchedTracks);
2433
+ realTrack?.id && (newAutogeneratedIds.add(realTrack.id), setTrackActivityOrder((prevOrder) => {
2434
+ const mockOrder = prevOrder.get(mockTrack.id);
2435
+ if (mockOrder) {
2436
+ const newMap = new Map(prevOrder);
2437
+ return newMap.set(realTrack.id, mockOrder), newMap;
2438
+ }
2439
+ return prevOrder;
2440
+ }));
2441
+ }
2442
+ return !replaced;
1375
2443
  }
1376
- ),
1377
- /* @__PURE__ */ jsx("path", { d: "M192 32h128v48H192z", fill: "currentColor" })
1378
- ]
2444
+ return !0;
2445
+ })), newAutogeneratedIds.size > 0 && setAutogeneratedTrackIds((prevIds) => {
2446
+ const updated = new Set(prevIds);
2447
+ return newAutogeneratedIds.forEach((id) => updated.add(id)), updated;
2448
+ });
2449
+ } catch (error) {
2450
+ console.error("Failed to refresh asset data:", error);
2451
+ }
2452
+ }, 3e3);
2453
+ return () => clearInterval(interval);
2454
+ }, [allTracks, asset.assetId, asset._id, client]);
2455
+ const visibleTracks = allTracks.filter(
2456
+ (track) => track.status === "ready" || track.status === "preparing" || track.status === "errored"
2457
+ ).sort((a2, b) => {
2458
+ const orderA = trackActivityOrder.get(a2.id) || 0, orderB = trackActivityOrder.get(b.id) || 0;
2459
+ if (orderA > 0 && orderB > 0)
2460
+ return orderB - orderA;
2461
+ if (orderA > 0) return -1;
2462
+ if (orderB > 0) return 1;
2463
+ const aIsPreparing = a2.status === "preparing", bIsPreparing = b.status === "preparing";
2464
+ if (aIsPreparing && !bIsPreparing) return -1;
2465
+ if (!aIsPreparing && bIsPreparing) return 1;
2466
+ 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);
2467
+ return aIsAutogenerated && !bIsAutogenerated ? -1 : !aIsAutogenerated && bIsAutogenerated ? 1 : 0;
2468
+ }), handleDownload = async (track) => {
2469
+ if (track.id) {
2470
+ setDownloadingTrackId(track.id);
2471
+ try {
2472
+ await downloadVttFile(client, asset, track);
2473
+ } catch (error) {
2474
+ toast.push({
2475
+ title: "Failed to download VTT file",
2476
+ status: "error",
2477
+ description: error instanceof Error ? error.message : "Please try again"
2478
+ });
2479
+ } finally {
2480
+ setDownloadingTrackId(null);
2481
+ }
1379
2482
  }
1380
- );
2483
+ }, confirmDelete = async () => {
2484
+ if (!trackToDelete || !trackToDelete.id) return;
2485
+ const track = trackToDelete;
2486
+ setTrackToDelete(null), setDeletingTrackId(track.id);
2487
+ try {
2488
+ if (!asset.assetId)
2489
+ throw new Error("Asset ID is required");
2490
+ if (await deleteTextTrack(client, asset.assetId, track.id), asset._id)
2491
+ try {
2492
+ const response = await getAsset(client, asset.assetId);
2493
+ await client.patch(asset._id).set({ data: response.data, status: response.data.status }).commit();
2494
+ } catch (refreshError) {
2495
+ console.error("Failed to refresh asset data:", refreshError);
2496
+ }
2497
+ toast.push({
2498
+ title: "Successfully deleted caption track",
2499
+ status: "success"
2500
+ }), setAddedTracks((prev) => prev.filter((t) => t.id !== track.id)), setUpdatedTracks((prev) => {
2501
+ const newMap = new Map(prev);
2502
+ return newMap.delete(track.id), newMap;
2503
+ }), setTrackActivityOrder((prev) => {
2504
+ const newMap = new Map(prev);
2505
+ return newMap.delete(track.id), newMap;
2506
+ }), setAutogeneratedTrackIds((prev) => {
2507
+ const updated = new Set(prev);
2508
+ return updated.delete(track.id), updated;
2509
+ });
2510
+ } catch (error) {
2511
+ toast.push({
2512
+ title: "Failed to delete caption track",
2513
+ status: "error",
2514
+ description: error instanceof Error ? error.message : "Please try again"
2515
+ });
2516
+ } finally {
2517
+ setDeletingTrackId(null);
2518
+ }
2519
+ }, handleAddTrack = (track) => {
2520
+ setAddedTracks((prev) => [...prev, track]), setTrackActivityOrder((prev) => {
2521
+ const newMap = new Map(prev);
2522
+ return newMap.set(track.id, prev.size + 1), newMap;
2523
+ }), setShowAddDialog(!1);
2524
+ }, handleUpdateTrack = async (updatedTrack, oldTrackId) => {
2525
+ if (oldTrackId && (setAddedTracks((prev) => prev.filter((t) => t.id !== oldTrackId)), setUpdatedTracks((prev) => {
2526
+ const newMap = new Map(prev);
2527
+ return newMap.delete(oldTrackId), newMap;
2528
+ }), setTrackActivityOrder((prev) => {
2529
+ const newMap = new Map(prev);
2530
+ return newMap.delete(oldTrackId), newMap;
2531
+ }), setAutogeneratedTrackIds((prev) => {
2532
+ const updated = new Set(prev);
2533
+ return updated.delete(oldTrackId), updated;
2534
+ })), addedTracks.some((t) => t.id === updatedTrack.id) ? setAddedTracks((prev) => prev.map((t) => t.id === updatedTrack.id ? updatedTrack : t)) : setUpdatedTracks((prev) => {
2535
+ const newMap = new Map(prev);
2536
+ return newMap.set(updatedTrack.id, updatedTrack), newMap;
2537
+ }), setTrackActivityOrder((prev) => {
2538
+ const newMap = new Map(prev);
2539
+ return newMap.set(updatedTrack.id, prev.size + 1), newMap;
2540
+ }), setTrackToEdit(null), asset._id && asset.assetId)
2541
+ try {
2542
+ const response = await getAsset(client, asset.assetId);
2543
+ await client.patch(asset._id).set({ data: response.data, status: response.data.status }).commit();
2544
+ } catch (refreshError) {
2545
+ console.error("Failed to refresh asset data:", refreshError);
2546
+ }
2547
+ }, 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";
2548
+ if (visibleTracks.length === 0 && !showAddDialog)
2549
+ return /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
2550
+ /* @__PURE__ */ jsx(Flex, { justify: "flex-end", children: /* @__PURE__ */ jsx(
2551
+ Button,
2552
+ {
2553
+ icon: AddIcon,
2554
+ text: "Add Caption",
2555
+ tone: "primary",
2556
+ onClick: () => setShowAddDialog(!0)
2557
+ }
2558
+ ) }),
2559
+ /* @__PURE__ */ jsx(Card, { padding: 4, radius: 2, tone: "transparent", border: !0, children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "No captions available. Add captions when uploading a video or add them manually." }) }),
2560
+ showAddDialog && /* @__PURE__ */ jsx(
2561
+ AddCaptionDialog,
2562
+ {
2563
+ asset,
2564
+ onAdd: handleAddTrack,
2565
+ onClose: () => setShowAddDialog(!1)
2566
+ }
2567
+ )
2568
+ ] });
2569
+ const displayedTracks = collapseTracks && !isExpanded ? visibleTracks.slice(0, MAX_VISIBLE_TRACKS) : visibleTracks, hasMoreTracks = collapseTracks && visibleTracks.length > MAX_VISIBLE_TRACKS;
2570
+ return /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
2571
+ /* @__PURE__ */ jsx(Flex, { justify: "flex-end", children: /* @__PURE__ */ jsx(
2572
+ Button,
2573
+ {
2574
+ icon: AddIcon,
2575
+ text: "Add Caption",
2576
+ tone: "primary",
2577
+ onClick: () => setShowAddDialog(!0)
2578
+ }
2579
+ ) }),
2580
+ displayedTracks.map((track) => /* @__PURE__ */ jsx(
2581
+ TrackCard,
2582
+ {
2583
+ track,
2584
+ iconOnly,
2585
+ downloadingTrackId,
2586
+ deletingTrackId,
2587
+ trackToEdit,
2588
+ getTrackSourceLabel,
2589
+ handleDownload,
2590
+ setTrackToEdit,
2591
+ setTrackToDelete
2592
+ },
2593
+ track.id
2594
+ )),
2595
+ hasMoreTracks && /* @__PURE__ */ jsx(Flex, { justify: "center", children: /* @__PURE__ */ jsx(
2596
+ Button,
2597
+ {
2598
+ icon: isExpanded ? ChevronUpIcon : ChevronDownIcon,
2599
+ text: isExpanded ? "Show less" : `Show ${visibleTracks.length - MAX_VISIBLE_TRACKS} more`,
2600
+ mode: "ghost",
2601
+ tone: "primary",
2602
+ onClick: () => setIsExpanded(!isExpanded)
2603
+ }
2604
+ ) }),
2605
+ trackToDelete && /* @__PURE__ */ jsx(
2606
+ Dialog,
2607
+ {
2608
+ animate: !0,
2609
+ id: dialogId,
2610
+ header: "Delete track",
2611
+ onClose: () => setTrackToDelete(null),
2612
+ onClickOutside: () => setTrackToDelete(null),
2613
+ width: 1,
2614
+ children: /* @__PURE__ */ jsx(
2615
+ Card,
2616
+ {
2617
+ padding: 3,
2618
+ style: {
2619
+ minHeight: "150px",
2620
+ display: "flex",
2621
+ alignItems: "center",
2622
+ justifyContent: "center"
2623
+ },
2624
+ children: /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
2625
+ /* @__PURE__ */ jsxs(Heading, { size: 2, children: [
2626
+ 'Are you sure you want to delete "',
2627
+ trackToDelete.name || trackToDelete.language_code || "Untitled",
2628
+ '"?'
2629
+ ] }),
2630
+ /* @__PURE__ */ jsx(Text, { size: 2, children: "This action is irreversible" }),
2631
+ /* @__PURE__ */ jsx(Stack, { space: 4, marginY: 4, children: /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(
2632
+ Button,
2633
+ {
2634
+ icon: deletingTrackId === trackToDelete.id ? /* @__PURE__ */ jsx(
2635
+ Spinner,
2636
+ {
2637
+ style: {
2638
+ verticalAlign: "middle",
2639
+ display: "inline-block",
2640
+ marginTop: "-2px",
2641
+ width: "0.5em",
2642
+ height: "0.5em"
2643
+ }
2644
+ }
2645
+ ) : /* @__PURE__ */ jsx(TrashIcon, {}),
2646
+ fontSize: 2,
2647
+ padding: 3,
2648
+ text: "Delete track",
2649
+ tone: "critical",
2650
+ onClick: confirmDelete,
2651
+ disabled: deletingTrackId !== null
2652
+ }
2653
+ ) }) })
2654
+ ] })
2655
+ }
2656
+ )
2657
+ }
2658
+ ),
2659
+ showAddDialog && /* @__PURE__ */ jsx(
2660
+ AddCaptionDialog,
2661
+ {
2662
+ asset,
2663
+ onAdd: handleAddTrack,
2664
+ onClose: () => setShowAddDialog(!1)
2665
+ }
2666
+ ),
2667
+ trackToEdit && /* @__PURE__ */ jsx(
2668
+ EditCaptionDialog,
2669
+ {
2670
+ asset,
2671
+ track: trackToEdit,
2672
+ onUpdate: handleUpdateTrack,
2673
+ onClose: () => setTrackToEdit(null)
2674
+ }
2675
+ )
2676
+ ] });
1381
2677
  }
1382
2678
  const DialogStateContext = createContext({
1383
2679
  dialogState: !1,
@@ -1395,6 +2691,10 @@ function getVideoSrc({ asset, client }) {
1395
2691
  }
1396
2692
  return `https://stream.mux.com/${playbackId}.m3u8?${searchParams}`;
1397
2693
  }
2694
+ function CaptionsDialog({ asset }) {
2695
+ const { setDialogState } = useDialogStateContext(), dialogId = `CaptionsDialog${useId()}`;
2696
+ return /* @__PURE__ */ jsx(Dialog, { id: dialogId, header: "Edit Captions", onClose: () => setDialogState(!1), width: 1, children: /* @__PURE__ */ jsx(Stack, { padding: 4, children: /* @__PURE__ */ jsx(TextTracksManager, { asset }) }) });
2697
+ }
1398
2698
  function getDevicePixelRatio(options) {
1399
2699
  const {
1400
2700
  defaultDpr = 1,
@@ -1560,7 +2860,7 @@ function VideoPlayer({
1560
2860
  crossOrigin: "anonymous",
1561
2861
  metadata: {
1562
2862
  player_name: "Sanity Admin Dashboard",
1563
- player_version: "2.13.0",
2863
+ player_version: "2.14.0",
1564
2864
  page_type: "Preview Player"
1565
2865
  },
1566
2866
  audio: isAudio,
@@ -1595,7 +2895,8 @@ function VideoPlayer({
1595
2895
  ]
1596
2896
  }
1597
2897
  ),
1598
- dialogState === "edit-thumbnail" && /* @__PURE__ */ jsx(EditThumbnailDialog, { asset, currentTime: muxPlayer?.current?.currentTime })
2898
+ dialogState === "edit-thumbnail" && /* @__PURE__ */ jsx(EditThumbnailDialog, { asset, currentTime: muxPlayer?.current?.currentTime }),
2899
+ dialogState === "edit-captions" && /* @__PURE__ */ jsx(CaptionsDialog, { asset })
1599
2900
  ] });
1600
2901
  }
1601
2902
  function assetIsAudio(asset) {
@@ -1867,7 +3168,8 @@ function getVideoMetadata(doc) {
1867
3168
  duration: doc.data?.duration ? formatSeconds(doc.data?.duration) : void 0,
1868
3169
  aspect_ratio: doc.data?.aspect_ratio,
1869
3170
  max_stored_resolution: doc.data?.max_stored_resolution,
1870
- max_stored_frame_rate: doc.data?.max_stored_frame_rate
3171
+ max_stored_frame_rate: doc.data?.max_stored_frame_rate,
3172
+ text_tracks: doc.data?.tracks?.filter((track) => track.type === "text") || []
1871
3173
  };
1872
3174
  }
1873
3175
  function useVideoDetails(props) {
@@ -2059,7 +3361,20 @@ const AssetInput = (props) => /* @__PURE__ */ jsx(FormField$1, { title: props.la
2059
3361
  minHeight: containerHeight
2060
3362
  } : void 0,
2061
3363
  children: [
2062
- /* @__PURE__ */ jsx(Stack, { space: 4, flex: 1, sizing: "border", children: /* @__PURE__ */ jsx(VideoPlayer, { asset: props.asset, autoPlay: props.asset.autoPlay || !1 }) }),
3364
+ /* @__PURE__ */ jsxs(Stack, { space: 4, flex: 1, sizing: "border", children: [
3365
+ /* @__PURE__ */ jsx(VideoPlayer, { asset: props.asset, autoPlay: props.asset.autoPlay || !1 }),
3366
+ tab === "details" && /* @__PURE__ */ jsx(
3367
+ TextTracksManager,
3368
+ {
3369
+ asset: props.asset,
3370
+ iconOnly: !0,
3371
+ collapseTracks: !0,
3372
+ tracks: displayInfo?.text_tracks || props.asset.data?.tracks?.filter(
3373
+ (track) => track.type === "text"
3374
+ ) || []
3375
+ }
3376
+ )
3377
+ ] }),
2063
3378
  /* @__PURE__ */ jsxs(Stack, { space: 4, flex: 1, sizing: "border", children: [
2064
3379
  /* @__PURE__ */ jsxs(TabList, { space: 2, children: [
2065
3380
  /* @__PURE__ */ jsx(
@@ -2389,10 +3704,10 @@ function VideoInBrowser({
2389
3704
  );
2390
3705
  }
2391
3706
  function VideosBrowser({ onSelect }) {
2392
- const { assets, isLoading, searchQuery, setSearchQuery, setSort, sort } = useAssets(), [editedAsset, setEditedAsset] = useState(null), freshEditedAsset = useMemo(
3707
+ const { assets, isLoading, searchQuery, setSearchQuery, setSort, sort } = useAssets(), [page, setPage] = useState(0), pageLimit = 20, pageTotal = Math.floor(assets.length / pageLimit) + 1, [editedAsset, setEditedAsset] = useState(null), freshEditedAsset = useMemo(
2393
3708
  () => assets.find((a2) => a2._id === editedAsset?._id) || editedAsset,
2394
3709
  [editedAsset, assets]
2395
- );
3710
+ ), pageStart = page * pageLimit, pageEnd = pageStart + pageLimit;
2396
3711
  return /* @__PURE__ */ jsxs(Fragment, { children: [
2397
3712
  /* @__PURE__ */ jsxs(Stack, { padding: 4, space: 4, style: { minHeight: "50vh" }, children: [
2398
3713
  /* @__PURE__ */ jsxs(Flex, { justify: "space-between", align: "center", children: [
@@ -2406,7 +3721,8 @@ function VideosBrowser({ onSelect }) {
2406
3721
  placeholder: "Search videos"
2407
3722
  }
2408
3723
  ),
2409
- /* @__PURE__ */ jsx(SelectSortOptions, { setSort, sort })
3724
+ /* @__PURE__ */ jsx(SelectSortOptions, { setSort, sort }),
3725
+ /* @__PURE__ */ jsx(PageSelector, { page, setPage, total: pageTotal, limit: pageLimit })
2410
3726
  ] }),
2411
3727
  (onSelect ? "input" : "tool") == "tool" && /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
2412
3728
  /* @__PURE__ */ jsx(ImportVideosFromMux, {}),
@@ -2429,7 +3745,7 @@ function VideosBrowser({ onSelect }) {
2429
3745
  style: {
2430
3746
  gridTemplateColumns: "repeat(auto-fill, minmax(250px, 1fr))"
2431
3747
  },
2432
- children: assets.map((asset) => /* @__PURE__ */ jsx(
3748
+ children: assets.slice(pageStart, pageEnd).map((asset) => /* @__PURE__ */ jsx(
2433
3749
  VideoInBrowser,
2434
3750
  {
2435
3751
  asset,
@@ -3137,14 +4453,24 @@ function PlayerActionsMenu(props) {
3137
4453
  onClick: () => setDialogState("select-video")
3138
4454
  }
3139
4455
  ),
3140
- isVideoAsset(asset) && /* @__PURE__ */ jsx(
3141
- MenuItem,
3142
- {
3143
- icon: ImageIcon,
3144
- text: "Thumbnail",
3145
- onClick: () => setDialogState("edit-thumbnail")
3146
- }
3147
- ),
4456
+ isVideoAsset(asset) && /* @__PURE__ */ jsxs(Fragment, { children: [
4457
+ /* @__PURE__ */ jsx(
4458
+ MenuItem,
4459
+ {
4460
+ icon: ImageIcon,
4461
+ text: "Thumbnail",
4462
+ onClick: () => setDialogState("edit-thumbnail")
4463
+ }
4464
+ ),
4465
+ /* @__PURE__ */ jsx(
4466
+ MenuItem,
4467
+ {
4468
+ icon: TranslateIcon,
4469
+ text: "Captions",
4470
+ onClick: () => setDialogState("edit-captions")
4471
+ }
4472
+ )
4473
+ ] }),
3148
4474
  /* @__PURE__ */ jsx(MenuDivider, {}),
3149
4475
  hasConfigAccess && /* @__PURE__ */ jsxs(Fragment, { children: [
3150
4476
  /* @__PURE__ */ jsx(
@@ -3198,36 +4524,6 @@ function formatBytes(bytes, si = !1, dp = 1) {
3198
4524
  while (Math.round(Math.abs(bytes) * r) / r >= thresh && u2 < units.length - 1);
3199
4525
  return bytes.toFixed(dp) + " " + units[u2];
3200
4526
  }
3201
- const SUPPORTED_MUX_LANGUAGES = [
3202
- { label: "English", code: "en", state: "Stable" },
3203
- { label: "Spanish", code: "es", state: "Stable" },
3204
- { label: "Italian", code: "it", state: "Stable" },
3205
- { label: "Portuguese", code: "pt", state: "Stable" },
3206
- { label: "German", code: "de", state: "Stable" },
3207
- { label: "French", code: "fr", state: "Stable" },
3208
- { label: "Polish", code: "pl", state: "Beta" },
3209
- { label: "Russian", code: "ru", state: "Beta" },
3210
- { label: "Dutch", code: "nl", state: "Beta" },
3211
- { label: "Catalan", code: "ca", state: "Beta" },
3212
- { label: "Turkish", code: "tr", state: "Beta" },
3213
- { label: "Swedish", code: "sv", state: "Beta" },
3214
- { label: "Ukrainian", code: "uk", state: "Beta" },
3215
- { label: "Norwegian", code: "no", state: "Beta" },
3216
- { label: "Finnish", code: "fi", state: "Beta" },
3217
- { label: "Slovak", code: "sk", state: "Beta" },
3218
- { label: "Greek", code: "el", state: "Beta" },
3219
- { label: "Czech", code: "cs", state: "Beta" },
3220
- { label: "Croatian", code: "hr", state: "Beta" },
3221
- { label: "Danish", code: "da", state: "Beta" },
3222
- { label: "Romanian", code: "ro", state: "Beta" },
3223
- { label: "Bulgarian", code: "bg", state: "Beta" }
3224
- ];
3225
- function isCustomTextTrack(track) {
3226
- return track.type !== "autogenerated";
3227
- }
3228
- function isAutogeneratedTrack(track) {
3229
- return track.type === "autogenerated";
3230
- }
3231
4527
  const ALL_LANGUAGE_CODES = LanguagesList.getAllCodes().map((code) => ({
3232
4528
  value: code,
3233
4529
  label: LanguagesList.getNativeName(code)
@@ -3849,12 +5145,10 @@ function withFocusRing(component) {
3849
5145
  `;
3850
5146
  });
3851
5147
  }
3852
- const ctrlKey = 17, cmdKey = 91, UploadCardWithFocusRing = withFocusRing(Card), UploadCard = forwardRef(
5148
+ const UploadCardWithFocusRing = withFocusRing(Card), UploadCard = forwardRef(
3853
5149
  ({ children, tone, onPaste, onDrop, onDragEnter, onDragLeave, onDragOver }, forwardedRef) => {
3854
- const ctrlDown = useRef(!1), inputRef = useRef(null), handleKeyDown = useCallback((event) => {
3855
- (event.keyCode == ctrlKey || event.keyCode == cmdKey) && (ctrlDown.current = !0), ctrlDown.current && event.keyCode == 86 && inputRef.current.focus();
3856
- }, []), handleKeyUp = useCallback((event) => {
3857
- (event.keyCode == ctrlKey || event.keyCode == cmdKey) && (ctrlDown.current = !1);
5150
+ const inputRef = useRef(null), handleKeyDown = useCallback((event) => {
5151
+ event.target.closest("#vtt-url") || (event.ctrlKey || event.metaKey) && event.key === "v" && inputRef.current.focus();
3858
5152
  }, []);
3859
5153
  return /* @__PURE__ */ jsxs(
3860
5154
  UploadCardWithFocusRing,
@@ -3866,14 +5160,13 @@ const ctrlKey = 17, cmdKey = 91, UploadCardWithFocusRing = withFocusRing(Card),
3866
5160
  shadow: 0,
3867
5161
  tabIndex: 0,
3868
5162
  onKeyDown: handleKeyDown,
3869
- onKeyUp: handleKeyUp,
3870
5163
  onPaste,
3871
5164
  onDrop,
3872
5165
  onDragEnter,
3873
5166
  onDragLeave,
3874
5167
  onDragOver,
3875
5168
  children: [
3876
- /* @__PURE__ */ jsx(HiddenInput$1, { ref: inputRef, onPaste }),
5169
+ /* @__PURE__ */ jsx(HiddenInput$1, { ref: inputRef }),
3877
5170
  children
3878
5171
  ]
3879
5172
  }
@@ -4131,6 +5424,8 @@ function Uploader(props) {
4131
5424
  input: { type: "file", files }
4132
5425
  });
4133
5426
  }, handlePaste = (event) => {
5427
+ if (event.target.closest("#vtt-url"))
5428
+ return;
4134
5429
  event.preventDefault(), event.stopPropagation();
4135
5430
  const url = (event.clipboardData || window.clipboardData)?.getData("text")?.trim();
4136
5431
  if (!isValidUrl(url)) {