@layers-app/editor-video 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/index.css +25 -3
  2. package/dist/index.js +913 -548
  3. package/dist/index.js.map +1 -1
  4. package/dist/plugin/behaviour.d.ts.map +1 -1
  5. package/dist/types/index.d.ts +1 -0
  6. package/dist/types/index.d.ts.map +1 -1
  7. package/dist/ui/VideoBlock.d.ts.map +1 -1
  8. package/dist/ui/VideoUploadComponent.d.ts.map +1 -1
  9. package/dist/ui/components/VideoCustomControls.d.ts.map +1 -1
  10. package/dist/ui/components/VideoSelect.d.ts +8 -0
  11. package/dist/ui/components/VideoSelect.d.ts.map +1 -0
  12. package/dist/ui/components/VideoSettingsModal/ChaptersSection.d.ts +3 -1
  13. package/dist/ui/components/VideoSettingsModal/ChaptersSection.d.ts.map +1 -1
  14. package/dist/ui/components/VideoSettingsModal/CoverSection.d.ts.map +1 -1
  15. package/dist/ui/components/VideoSettingsModal/FooterActions.d.ts +2 -1
  16. package/dist/ui/components/VideoSettingsModal/FooterActions.d.ts.map +1 -1
  17. package/dist/ui/components/VideoSettingsModal/ManualSubtitlesPanel.d.ts +2 -1
  18. package/dist/ui/components/VideoSettingsModal/ManualSubtitlesPanel.d.ts.map +1 -1
  19. package/dist/ui/components/VideoSettingsModal/SubtitlesSection.d.ts.map +1 -1
  20. package/dist/ui/components/VideoSettingsModal/index.d.ts +2 -1
  21. package/dist/ui/components/VideoSettingsModal/index.d.ts.map +1 -1
  22. package/dist/ui/components/VideoSubtitlesMenu/index.d.ts.map +1 -1
  23. package/dist/ui/hooks/useVideoTranscoding.d.ts.map +1 -1
  24. package/dist/ui/hooks/useVideoUpload.d.ts.map +1 -1
  25. package/dist/ui/states/UploadState.d.ts.map +1 -1
  26. package/dist/ui/states/VideoPlayerState.d.ts +3 -1
  27. package/dist/ui/states/VideoPlayerState.d.ts.map +1 -1
  28. package/dist/ui/utils/api.d.ts.map +1 -1
  29. package/dist/ui/utils/videoSubtitlesApi.d.ts +5 -0
  30. package/dist/ui/utils/videoSubtitlesApi.d.ts.map +1 -1
  31. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5,11 +5,12 @@ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
5
5
  import { createContext, useState, useRef, useCallback, useMemo, useContext, forwardRef, createElement, useEffect } from "react";
6
6
  import { createCommand, $getNodeByKey, $setSelection, COMMAND_PRIORITY_LOW, DecoratorNode } from "lexical";
7
7
  import { useTranslation } from "react-i18next";
8
- import { Tooltip, ActionIcon, Text, Progress, Paper, Button, TextInput, Menu, Box, Slider, Flex, Select, Stack, Divider, Group, Textarea, Loader, Modal } from "@mantine/core";
8
+ import { Tooltip, ActionIcon, Text, Progress, Paper, Button, TextInput, Menu, Box, Slider, Flex, useCombobox, Combobox, InputBase, Group, Stack, Divider, Textarea, Loader, Switch, Modal } from "@mantine/core";
9
9
  import { BlockWithAlignableContents } from "@lexical/react/LexicalBlockWithAlignableContents";
10
10
  import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
11
11
  import { Dropzone as Dropzone$1, IMAGE_MIME_TYPE } from "@mantine/dropzone";
12
12
  import ShakaPlayer from "shaka-player-react";
13
+ import { IconChevronDown, IconCheck } from "@tabler/icons-react";
13
14
  import { Carousel } from "@mantine/carousel";
14
15
  const VideoContext = createContext(null);
15
16
  function VideoPluginProvider({
@@ -328,25 +329,34 @@ function setVideoSize(editor, nodeKey, width, height) {
328
329
  });
329
330
  }
330
331
  function setVideoPoster(editor, nodeKey, posterUrl) {
331
- editor.update(() => {
332
- const node = $getNodeByKey(nodeKey);
333
- if (!node || !$isVideoNode(node)) return;
334
- node.setPosterUrl(posterUrl || "");
335
- });
332
+ editor.update(
333
+ () => {
334
+ const node = $getNodeByKey(nodeKey);
335
+ if (!node || !$isVideoNode(node)) return;
336
+ node.setPosterUrl(posterUrl || "");
337
+ },
338
+ { tag: "skip-scroll-into-view" }
339
+ );
336
340
  }
337
341
  function setVideoPrimaryUrl(editor, nodeKey, primaryUrl) {
338
- editor.update(() => {
339
- const node = $getNodeByKey(nodeKey);
340
- if (!node || !$isVideoNode(node)) return;
341
- node.setUrl(primaryUrl);
342
- });
342
+ editor.update(
343
+ () => {
344
+ const node = $getNodeByKey(nodeKey);
345
+ if (!node || !$isVideoNode(node)) return;
346
+ node.setUrl(primaryUrl);
347
+ },
348
+ { tag: "skip-scroll-into-view" }
349
+ );
343
350
  }
344
351
  function setVideoChaptersDisabled(editor, nodeKey, disabled2) {
345
- editor.update(() => {
346
- const node = $getNodeByKey(nodeKey);
347
- if (!node || !$isVideoNode(node)) return;
348
- node.setChaptersDisabled(disabled2);
349
- });
352
+ editor.update(
353
+ () => {
354
+ const node = $getNodeByKey(nodeKey);
355
+ if (!node || !$isVideoNode(node)) return;
356
+ node.setChaptersDisabled(disabled2);
357
+ },
358
+ { tag: "skip-scroll-into-view" }
359
+ );
350
360
  }
351
361
  function removeVideoNode(editor, nodeKey, deleteNode) {
352
362
  deleteNode(editor, nodeKey);
@@ -854,11 +864,6 @@ function useVideoTranscoding({
854
864
  const now = Date.now();
855
865
  if (!procStallReportedRef.current && lastProcChangeAtRef.current > 0 && now - lastProcChangeAtRef.current > STALL_TIMEOUT_MS$1) {
856
866
  procStallReportedRef.current = true;
857
- console.warn("Processing stall detected", {
858
- stage: proc.stage,
859
- percent,
860
- stalledMs: now - lastProcChangeAtRef.current
861
- });
862
867
  }
863
868
  const monotonicPercent = Math.max(maxPercentRef.current, percent);
864
869
  maxPercentRef.current = monotonicPercent;
@@ -955,7 +960,26 @@ function useCancelUpload() {
955
960
  const DEFAULT_CHUNK_SIZE = 8 * 1024 * 1024;
956
961
  const STALL_TIMEOUT_MS = 3e4;
957
962
  const MAX_CONSECUTIVE_CONFLICTS = 3;
963
+ const MAX_RETRIES_PER_CHUNK = 3;
964
+ const MAX_TOTAL_RETRIES = 15;
965
+ const RETRY_BASE_DELAY_MS = 3e3;
966
+ function abortableSleep(ms, signal) {
967
+ return new Promise((resolve) => {
968
+ if (signal == null ? void 0 : signal.aborted) {
969
+ resolve();
970
+ return;
971
+ }
972
+ const id = setTimeout(resolve, ms);
973
+ signal == null ? void 0 : signal.addEventListener("abort", () => {
974
+ clearTimeout(id);
975
+ resolve();
976
+ }, { once: true });
977
+ });
978
+ }
958
979
  function getBaseUrl() {
980
+ if (typeof window !== "undefined" && window.location.hostname === "dev-app.layers.md") {
981
+ return "https://api.layers.md";
982
+ }
959
983
  return "";
960
984
  }
961
985
  function getAuthHeaders$2() {
@@ -971,10 +995,13 @@ async function uploadChunkWithProgress(videoId, file, offset, end, total, baseUr
971
995
  onXhrReady(xhr);
972
996
  }
973
997
  if (signal) {
998
+ if (signal.aborted) {
999
+ reject(new Error("Upload cancelled"));
1000
+ return;
1001
+ }
974
1002
  signal.addEventListener("abort", () => {
975
1003
  xhr.abort();
976
- reject(new Error("Upload cancelled"));
977
- });
1004
+ }, { once: true });
978
1005
  }
979
1006
  xhr.upload.addEventListener("progress", (e) => {
980
1007
  if (e.lengthComputable && onProgress) {
@@ -987,18 +1014,30 @@ async function uploadChunkWithProgress(videoId, file, offset, end, total, baseUr
987
1014
  const contentType = xhr.getResponseHeader("content-type") || "";
988
1015
  const isJson = contentType.includes("application/json");
989
1016
  const data = isJson ? JSON.parse(xhr.responseText) : xhr.responseText;
990
- if (xhr.status === 409 && data && typeof data === "object" && "expectedOffset" in data) {
991
- resolve({
992
- expectedOffset: data.expectedOffset
993
- });
994
- return;
995
- }
996
1017
  resolve(
997
1018
  typeof data === "object" && data !== null ? data : {}
998
1019
  );
999
1020
  } catch (error) {
1000
1021
  reject(error);
1001
1022
  }
1023
+ } else if (xhr.status === 409) {
1024
+ try {
1025
+ const contentType = xhr.getResponseHeader("content-type") || "";
1026
+ const isJson = contentType.includes("application/json");
1027
+ const data = isJson ? JSON.parse(xhr.responseText) : {};
1028
+ if (data && typeof data === "object" && "expectedOffset" in data) {
1029
+ resolve({
1030
+ expectedOffset: data.expectedOffset
1031
+ });
1032
+ return;
1033
+ }
1034
+ } catch {
1035
+ }
1036
+ reject({
1037
+ status: xhr.status,
1038
+ statusText: xhr.statusText,
1039
+ data: { message: "Upload conflict" }
1040
+ });
1002
1041
  } else {
1003
1042
  let errorData = {};
1004
1043
  try {
@@ -1035,9 +1074,7 @@ async function uploadChunkWithProgress(videoId, file, offset, end, total, baseUr
1035
1074
  Object.keys(headers).forEach((key) => {
1036
1075
  xhr.setRequestHeader(key, headers[key]);
1037
1076
  });
1038
- if (!baseUrl) {
1039
- xhr.withCredentials = true;
1040
- }
1077
+ xhr.withCredentials = true;
1041
1078
  xhr.send(blob);
1042
1079
  });
1043
1080
  }
@@ -1073,13 +1110,16 @@ function useVideoUpload({
1073
1110
  isPaused: false,
1074
1111
  isPausing: false,
1075
1112
  currentXhr: null,
1076
- uploadSession: 0
1113
+ chunkAbortController: null,
1114
+ uploadSession: 0,
1115
+ cancelled: false
1077
1116
  });
1078
1117
  const progressIntervalRef = useRef(
1079
1118
  null
1080
1119
  );
1081
1120
  const speedHistoryRef = useRef([]);
1082
1121
  const lastDisplayedRemainingRef = useRef(0);
1122
+ const maxDisplayedPercentRef = useRef(0);
1083
1123
  const MAX_SPEED_HISTORY = 20;
1084
1124
  const EMA_ALPHA = 0.15;
1085
1125
  const emaSpeedRef = useRef(null);
@@ -1096,6 +1136,11 @@ function useVideoUpload({
1096
1136
  }
1097
1137
  const timeDiff = (now - state.lastTime) / 1e3;
1098
1138
  const uploadedDiff = uploaded - state.lastBytes;
1139
+ if (uploadedDiff < 0) {
1140
+ state.lastBytes = uploaded;
1141
+ state.lastTime = now;
1142
+ return { speed: emaSpeedRef.current ?? 0, remaining: lastDisplayedRemainingRef.current };
1143
+ }
1099
1144
  if (timeDiff > 0 && !state.isPaused) {
1100
1145
  const instantSpeed = Math.max(0, uploadedDiff / timeDiff);
1101
1146
  speedHistoryRef.current.push(instantSpeed);
@@ -1134,10 +1179,12 @@ function useVideoUpload({
1134
1179
  (uploaded, total) => {
1135
1180
  const now = Date.now();
1136
1181
  const { speed, remaining } = calculateSpeed(uploaded, total, now);
1137
- const percent = Math.floor(uploaded * 100 / total);
1182
+ const rawPercent = Math.floor(uploaded * 100 / total);
1183
+ const percent = Math.max(rawPercent, maxDisplayedPercentRef.current);
1184
+ maxDisplayedPercentRef.current = percent;
1138
1185
  setUploadProgress({
1139
1186
  percent,
1140
- uploaded,
1187
+ uploaded: Math.max(uploaded, total * percent / 100),
1141
1188
  total,
1142
1189
  speed,
1143
1190
  remaining
@@ -1147,7 +1194,7 @@ function useVideoUpload({
1147
1194
  );
1148
1195
  const uploadFileChunks = useCallback(
1149
1196
  async (file, videoId, startOffset = 0) => {
1150
- var _a, _b, _c;
1197
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
1151
1198
  const state = uploadStateRef.current;
1152
1199
  const total = file.size;
1153
1200
  let offset = Math.max(0, startOffset);
@@ -1170,6 +1217,8 @@ function useVideoUpload({
1170
1217
  }
1171
1218
  }, 1e3);
1172
1219
  let consecutiveConflicts = 0;
1220
+ let chunkRetries = 0;
1221
+ let totalRetries = 0;
1173
1222
  let hasSuccessfulChunk = false;
1174
1223
  if (offset > 0) {
1175
1224
  updateProgress(offset, total);
@@ -1177,7 +1226,7 @@ function useVideoUpload({
1177
1226
  updateProgress(0, total);
1178
1227
  }
1179
1228
  while (offset < total) {
1180
- if ((_a = state.signal) == null ? void 0 : _a.aborted) {
1229
+ if (state.cancelled || ((_a = state.signal) == null ? void 0 : _a.aborted)) {
1181
1230
  throw new Error("Upload cancelled");
1182
1231
  }
1183
1232
  if (state.isPaused) {
@@ -1192,13 +1241,17 @@ function useVideoUpload({
1192
1241
  const nowStall = Date.now();
1193
1242
  if (!state.uploadStallReported && nowStall - state.lastUploadAckAt > STALL_TIMEOUT_MS) {
1194
1243
  state.uploadStallReported = true;
1195
- console.warn("Upload stall detected", {
1196
- atOffset: offset,
1197
- stalledMs: nowStall - state.lastUploadAckAt
1198
- });
1199
1244
  }
1200
1245
  const end = Math.min(total - 1, offset + chunkSize - 1);
1201
1246
  const chunkStartOffset = offset;
1247
+ const chunkAc = new AbortController();
1248
+ state.chunkAbortController = chunkAc;
1249
+ const forwardAbort = () => chunkAc.abort();
1250
+ if (((_b = state.signal) == null ? void 0 : _b.aborted) || state.cancelled) {
1251
+ chunkAc.abort();
1252
+ } else if (state.signal) {
1253
+ state.signal.addEventListener("abort", forwardAbort, { once: true });
1254
+ }
1202
1255
  try {
1203
1256
  const response = await uploadChunkWithProgress(
1204
1257
  videoId,
@@ -1208,7 +1261,7 @@ function useVideoUpload({
1208
1261
  total,
1209
1262
  baseUrl,
1210
1263
  authHeaders,
1211
- state.signal || void 0,
1264
+ chunkAc.signal,
1212
1265
  (loaded) => {
1213
1266
  if (state.isPaused || state.isPausing) return;
1214
1267
  const estimatedOffset = chunkStartOffset + loaded;
@@ -1220,13 +1273,11 @@ function useVideoUpload({
1220
1273
  state.currentXhr = xhr;
1221
1274
  }
1222
1275
  );
1276
+ state.chunkAbortController = null;
1277
+ state.currentXhr = null;
1278
+ (_c = state.signal) == null ? void 0 : _c.removeEventListener("abort", forwardAbort);
1223
1279
  if (response.expectedOffset != null) {
1224
1280
  consecutiveConflicts++;
1225
- console.warn("Offset mismatch detected", {
1226
- expectedOffset: response.expectedOffset,
1227
- hadOffset: offset,
1228
- attempt: consecutiveConflicts
1229
- });
1230
1281
  if (consecutiveConflicts >= MAX_CONSECUTIVE_CONFLICTS) {
1231
1282
  throw {
1232
1283
  status: 409,
@@ -1238,10 +1289,14 @@ function useVideoUpload({
1238
1289
  continue;
1239
1290
  }
1240
1291
  consecutiveConflicts = 0;
1292
+ chunkRetries = 0;
1241
1293
  hasSuccessfulChunk = true;
1242
1294
  offset = response.nextOffset != null ? response.nextOffset : end + 1;
1295
+ if (offset < total) {
1296
+ await abortableSleep(500, state.signal ?? void 0);
1297
+ }
1243
1298
  state.currentOffset = offset;
1244
- state.currentXhr = null;
1299
+ state.chunkAbortController = null;
1245
1300
  state.lastUploadAckAt = Date.now();
1246
1301
  state.uploadStallReported = false;
1247
1302
  if (!state.isPaused && !state.isPausing) {
@@ -1255,26 +1310,54 @@ function useVideoUpload({
1255
1310
  }
1256
1311
  } catch (error) {
1257
1312
  state.currentXhr = null;
1313
+ state.chunkAbortController = null;
1314
+ (_d = state.signal) == null ? void 0 : _d.removeEventListener("abort", forwardAbort);
1258
1315
  if (state.isPaused || state.isPausing) {
1259
1316
  state.isPausing = false;
1317
+ state.isPaused = true;
1260
1318
  break;
1261
1319
  }
1262
- if ((error == null ? void 0 : error.name) === "AbortError" || ((_b = state.signal) == null ? void 0 : _b.aborted)) {
1320
+ if ((error == null ? void 0 : error.name) === "AbortError" || ((_e = state.signal) == null ? void 0 : _e.aborted) || state.cancelled) {
1263
1321
  throw new Error("Upload cancelled");
1264
1322
  }
1265
- if ((error == null ? void 0 : error.status) === 409 && ((_c = error == null ? void 0 : error.data) == null ? void 0 : _c.expectedOffset) != null) {
1323
+ if ((error == null ? void 0 : error.status) === 409 && ((_f = error == null ? void 0 : error.data) == null ? void 0 : _f.expectedOffset) != null) {
1266
1324
  consecutiveConflicts++;
1267
- console.warn("Offset conflict in error handler", {
1268
- expectedOffset: error.data.expectedOffset,
1269
- hadOffset: offset,
1270
- attempt: consecutiveConflicts
1271
- });
1272
1325
  if (consecutiveConflicts >= MAX_CONSECUTIVE_CONFLICTS) {
1273
1326
  throw error;
1274
1327
  }
1275
1328
  offset = error.data.expectedOffset;
1276
1329
  continue;
1277
1330
  }
1331
+ const isNetworkError = !(error == null ? void 0 : error.status) || (error == null ? void 0 : error.status) === 0 || (error == null ? void 0 : error.status) === 409;
1332
+ if (isNetworkError && chunkRetries < MAX_RETRIES_PER_CHUNK && totalRetries < MAX_TOTAL_RETRIES) {
1333
+ chunkRetries++;
1334
+ totalRetries++;
1335
+ const delay = RETRY_BASE_DELAY_MS * chunkRetries;
1336
+ await abortableSleep(delay, state.signal ?? void 0);
1337
+ if (state.cancelled || ((_g = state.signal) == null ? void 0 : _g.aborted) || state.isPaused || state.isPausing) {
1338
+ break;
1339
+ }
1340
+ try {
1341
+ const currentStatus = await fetchStatus(videoId, baseUrl, authHeaders);
1342
+ if (((_h = currentStatus.upload) == null ? void 0 : _h.nextOffset) != null) {
1343
+ const serverOffset = currentStatus.upload.nextOffset;
1344
+ if (serverOffset === offset && chunkRetries >= 2) {
1345
+ await abortableSleep(5e3, state.signal ?? void 0);
1346
+ const recheck = await fetchStatus(videoId, baseUrl, authHeaders);
1347
+ if (((_i = recheck.upload) == null ? void 0 : _i.nextOffset) != null && recheck.upload.nextOffset > offset) {
1348
+ offset = recheck.upload.nextOffset;
1349
+ state.currentOffset = offset;
1350
+ chunkRetries = 0;
1351
+ continue;
1352
+ }
1353
+ }
1354
+ offset = serverOffset;
1355
+ state.currentOffset = serverOffset;
1356
+ }
1357
+ } catch {
1358
+ }
1359
+ continue;
1360
+ }
1278
1361
  throw error;
1279
1362
  }
1280
1363
  }
@@ -1303,7 +1386,15 @@ function useVideoUpload({
1303
1386
  );
1304
1387
  const handleUpload = useCallback(
1305
1388
  async (file) => {
1306
- var _a, _b;
1389
+ var _a, _b, _c, _d, _e, _f;
1390
+ const state0 = uploadStateRef.current;
1391
+ if (state0.isPaused || state0.isPausing) {
1392
+ state0.isPaused = false;
1393
+ state0.isPausing = false;
1394
+ state0.cancelled = true;
1395
+ state0.videoId = null;
1396
+ state0.attachmentId = null;
1397
+ }
1307
1398
  setIsUploading(true);
1308
1399
  setIsPaused(false);
1309
1400
  const state = uploadStateRef.current;
@@ -1311,11 +1402,13 @@ function useVideoUpload({
1311
1402
  const mySession = state.uploadSession;
1312
1403
  state.isPaused = false;
1313
1404
  state.isPausing = false;
1405
+ state.cancelled = false;
1314
1406
  state.file = file;
1315
1407
  state.currentOffset = 0;
1316
1408
  state.lastTime = 0;
1317
1409
  state.lastBytes = 0;
1318
1410
  state.uploadStallReported = false;
1411
+ maxDisplayedPercentRef.current = 0;
1319
1412
  state.signal = start();
1320
1413
  try {
1321
1414
  if (!parentId) {
@@ -1338,19 +1431,19 @@ function useVideoUpload({
1338
1431
  state.videoId = videoId;
1339
1432
  state.attachmentId = attachmentId;
1340
1433
  setUploadVideoId(videoId);
1341
- if (initResponse.status) {
1342
- const status = initResponse.status;
1343
- if (((_a = status.upload) == null ? void 0 : _a.nextOffset) != null) {
1344
- startOffset = status.upload.nextOffset;
1434
+ try {
1435
+ const verifiedStatus = await fetchStatus(videoId, baseUrl, authHeaders);
1436
+ if (((_a = verifiedStatus.upload) == null ? void 0 : _a.nextOffset) != null && verifiedStatus.upload.nextOffset > 0) {
1437
+ startOffset = verifiedStatus.upload.nextOffset;
1438
+ } else if (((_c = (_b = initResponse.status) == null ? void 0 : _b.upload) == null ? void 0 : _c.nextOffset) != null) {
1439
+ startOffset = initResponse.status.upload.nextOffset;
1440
+ }
1441
+ } catch {
1442
+ if (((_e = (_d = initResponse.status) == null ? void 0 : _d.upload) == null ? void 0 : _e.nextOffset) != null) {
1443
+ startOffset = initResponse.status.upload.nextOffset;
1345
1444
  }
1346
1445
  }
1347
1446
  } catch (initError) {
1348
- console.error("Init video error:", {
1349
- status: initError == null ? void 0 : initError.status,
1350
- statusText: initError == null ? void 0 : initError.statusText,
1351
- data: initError == null ? void 0 : initError.data,
1352
- error: initError
1353
- });
1354
1447
  throw initError;
1355
1448
  }
1356
1449
  } else {
@@ -1360,7 +1453,7 @@ function useVideoUpload({
1360
1453
  baseUrl,
1361
1454
  authHeaders
1362
1455
  );
1363
- if (((_b = status.upload) == null ? void 0 : _b.nextOffset) != null) {
1456
+ if (((_f = status.upload) == null ? void 0 : _f.nextOffset) != null) {
1364
1457
  startOffset = status.upload.nextOffset;
1365
1458
  } else {
1366
1459
  videoId = null;
@@ -1415,20 +1508,14 @@ function useVideoUpload({
1415
1508
  if (state.isPaused || state.isPausing) {
1416
1509
  return;
1417
1510
  }
1418
- console.error("Upload error:", (error == null ? void 0 : error.message) || error);
1419
1511
  if ((error == null ? void 0 : error.message) === "Upload cancelled") {
1420
1512
  return;
1421
1513
  }
1422
1514
  if ((error == null ? void 0 : error.status) === 409) {
1423
1515
  onError("offset-mismatch", uploadStateRef.current.videoId, error);
1424
1516
  } else if (error == null ? void 0 : error.status) {
1425
- console.error(
1426
- `API error: ${error.status} ${error.statusText}`,
1427
- error.data
1428
- );
1429
1517
  onError("interrupted", uploadStateRef.current.videoId, error);
1430
1518
  } else {
1431
- console.error("Unknown upload error:", error);
1432
1519
  onError("interrupted", uploadStateRef.current.videoId, error);
1433
1520
  }
1434
1521
  setIsUploading(false);
@@ -1453,15 +1540,15 @@ function useVideoUpload({
1453
1540
  return;
1454
1541
  }
1455
1542
  state.isPausing = true;
1456
- state.isPaused = true;
1457
1543
  setIsPaused(true);
1458
1544
  if (progressIntervalRef.current) {
1459
1545
  clearInterval(progressIntervalRef.current);
1460
1546
  progressIntervalRef.current = null;
1461
1547
  }
1462
- if (state.currentXhr) {
1463
- state.currentXhr.abort();
1548
+ if (state.chunkAbortController) {
1549
+ state.chunkAbortController.abort();
1464
1550
  } else {
1551
+ state.isPaused = true;
1465
1552
  state.isPausing = false;
1466
1553
  }
1467
1554
  }, []);
@@ -1499,6 +1586,7 @@ function useVideoUpload({
1499
1586
  setIsPaused(false);
1500
1587
  state.isPaused = false;
1501
1588
  state.isPausing = false;
1589
+ state.cancelled = false;
1502
1590
  await uploadFileChunks(state.file, state.videoId, startOffset);
1503
1591
  if (state.uploadSession !== mySession) return;
1504
1592
  if (!state.isPaused && !state.isPausing && state.videoId) {
@@ -1513,7 +1601,7 @@ function useVideoUpload({
1513
1601
  if ((error == null ? void 0 : error.message) === "Upload cancelled") {
1514
1602
  return;
1515
1603
  }
1516
- if (((error == null ? void 0 : error.status) === 0 || (error == null ? void 0 : error.status) === 409) && state.file) {
1604
+ if (((error == null ? void 0 : error.status) === 0 || (error == null ? void 0 : error.status) === 409) && state.file && !state.cancelled) {
1517
1605
  state.videoId = null;
1518
1606
  state.attachmentId = null;
1519
1607
  handleUpload(state.file);
@@ -1532,10 +1620,14 @@ function useVideoUpload({
1532
1620
  }, [baseUrl, authHeaders, uploadFileChunks, onError, start, handleUpload]);
1533
1621
  const handleCancel = useCallback(() => {
1534
1622
  const state = uploadStateRef.current;
1535
- cancelUpload();
1536
- if (state.signal) {
1537
- state.signal = null;
1623
+ if (state.chunkAbortController) {
1624
+ try {
1625
+ state.chunkAbortController.abort();
1626
+ } catch {
1627
+ }
1538
1628
  }
1629
+ state.cancelled = true;
1630
+ cancelUpload();
1539
1631
  if (progressIntervalRef.current) {
1540
1632
  clearInterval(progressIntervalRef.current);
1541
1633
  progressIntervalRef.current = null;
@@ -1559,7 +1651,9 @@ function useVideoUpload({
1559
1651
  state.isPaused = false;
1560
1652
  state.isPausing = false;
1561
1653
  state.currentXhr = null;
1654
+ state.chunkAbortController = null;
1562
1655
  speedHistoryRef.current = [];
1656
+ maxDisplayedPercentRef.current = 0;
1563
1657
  setUploadVideoId(null);
1564
1658
  }, [cancelUpload]);
1565
1659
  return {
@@ -1669,7 +1763,7 @@ function CheckCircleIcon({
1669
1763
  "path",
1670
1764
  {
1671
1765
  d: "M16 1.66699C23.916 1.66699 30.333 8.08391 30.333 16C30.333 23.916 23.916 30.333 16 30.333C8.08391 30.333 1.66699 23.916 1.66699 16C1.66699 8.08392 8.08392 1.66699 16 1.66699Z",
1672
- fill: "var(--mantine-primary-color-filled, #3B5BDB)"
1766
+ fill: "var(--mantine-primary-color-filled, #4c6ef5)"
1673
1767
  }
1674
1768
  ),
1675
1769
  /* @__PURE__ */ jsx(
@@ -1816,7 +1910,7 @@ function ProgressBar({
1816
1910
  "%"
1817
1911
  ] }),
1818
1912
  isPaused ? /* @__PURE__ */ jsx(Text, { size: "sm", c: "var(--mantine-color-text)", children: t("editor.video.actions.pause") }) : showRemaining && progress.remaining > 0 && /* @__PURE__ */ jsxs(Text, { size: "sm", c: "var(--mantine-color-dimmed)", children: [
1819
- "Remaining:",
1913
+ t("editor.video.upload.remaining"),
1820
1914
  /* @__PURE__ */ jsxs(Text, { component: "span", c: "var(--mantine-color-text)", children: [
1821
1915
  " ",
1822
1916
  formatTime2(progress.remaining)
@@ -1826,7 +1920,7 @@ function ProgressBar({
1826
1920
  /* @__PURE__ */ jsx(Progress, { value: clampedPercent, size: 6, radius: "md" }),
1827
1921
  showInfo && /* @__PURE__ */ jsxs("div", { className: "video-upload-progress-info", children: [
1828
1922
  /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
1829
- "Uploading:",
1923
+ t("editor.video.upload.progress"),
1830
1924
  /* @__PURE__ */ jsxs(Text, { component: "span", size: "xs", c: "var(--mantine-color-text)", inherit: true, children: [
1831
1925
  " ",
1832
1926
  formatBytes(progress.uploaded),
@@ -2413,6 +2507,7 @@ function UploadState({
2413
2507
  onCancel,
2414
2508
  isPaused = false
2415
2509
  }) {
2510
+ const { t } = useTranslation();
2416
2511
  return /* @__PURE__ */ jsxs(
2417
2512
  Paper,
2418
2513
  {
@@ -2481,7 +2576,7 @@ function UploadState({
2481
2576
  ]
2482
2577
  }
2483
2578
  ),
2484
- /* @__PURE__ */ jsx(Text, { size: "md", fw: 500, c: "var(--mantine-color-bright)", children: "Uploading File" })
2579
+ /* @__PURE__ */ jsx(Text, { size: "md", fw: 500, c: "var(--mantine-color-bright)", children: t("editor.video.upload.uploading") })
2485
2580
  ]
2486
2581
  }
2487
2582
  ),
@@ -2953,6 +3048,138 @@ function VideoSpeedMenu({
2953
3048
  }
2954
3049
  );
2955
3050
  }
3051
+ function buildApiUrl$1(path, baseUrl) {
3052
+ if (path.startsWith("http://") || path.startsWith("https://")) {
3053
+ return path;
3054
+ }
3055
+ const base = baseUrl || (typeof window !== "undefined" ? window.location.origin : "");
3056
+ return `${base}${path}`;
3057
+ }
3058
+ function getAuthHeaders$1(authToken) {
3059
+ if (!authToken) return {};
3060
+ return { Authorization: `Bearer ${authToken}` };
3061
+ }
3062
+ async function parseJson$1(response) {
3063
+ const text = await response.text();
3064
+ if (!text) return null;
3065
+ try {
3066
+ return JSON.parse(text);
3067
+ } catch {
3068
+ return text;
3069
+ }
3070
+ }
3071
+ async function handleResponse$1(response) {
3072
+ if (!response.ok) {
3073
+ const data = await parseJson$1(response);
3074
+ const message = (data && typeof data === "object" && "message" in data ? data.message : null) || response.statusText || "Request failed";
3075
+ throw new Error(message);
3076
+ }
3077
+ return parseJson$1(response);
3078
+ }
3079
+ function normalizeSubtitleTrack(raw) {
3080
+ const id = (raw == null ? void 0 : raw.id) ?? (raw == null ? void 0 : raw.trackId) ?? (raw == null ? void 0 : raw.subtitleId) ?? "";
3081
+ const langRaw = (raw == null ? void 0 : raw.lang) ?? (raw == null ? void 0 : raw.language) ?? (raw == null ? void 0 : raw.srclang) ?? "";
3082
+ const lang = langRaw ? String(langRaw) : "und";
3083
+ const labelRaw = (raw == null ? void 0 : raw.label) ?? (raw == null ? void 0 : raw.name) ?? (raw == null ? void 0 : raw.title) ?? lang;
3084
+ const label = labelRaw ? String(labelRaw) : lang;
3085
+ const kind = (raw == null ? void 0 : raw.kind) ?? "subtitles";
3086
+ const isDefault = Boolean((raw == null ? void 0 : raw.isDefault) ?? (raw == null ? void 0 : raw.default) ?? false);
3087
+ const isDisabled = Boolean((raw == null ? void 0 : raw.isDisabled) ?? false);
3088
+ const url = typeof (raw == null ? void 0 : raw.url) === "string" ? raw.url : typeof (raw == null ? void 0 : raw.vttUrl) === "string" ? raw.vttUrl : typeof (raw == null ? void 0 : raw.src) === "string" ? raw.src : void 0;
3089
+ const createdAt = typeof (raw == null ? void 0 : raw.createdAt) === "string" ? raw.createdAt : void 0;
3090
+ const fileName = typeof (raw == null ? void 0 : raw.fileName) === "string" ? raw.fileName : void 0;
3091
+ const auto = Boolean((raw == null ? void 0 : raw.auto) ?? (raw == null ? void 0 : raw.isAuto) ?? (raw == null ? void 0 : raw.source) === "auto");
3092
+ return {
3093
+ id: String(id),
3094
+ lang,
3095
+ label,
3096
+ kind: String(kind),
3097
+ isDefault,
3098
+ isDisabled,
3099
+ url,
3100
+ createdAt,
3101
+ fileName,
3102
+ status: "ready",
3103
+ auto
3104
+ };
3105
+ }
3106
+ function isAutoGeneratedTrack(track) {
3107
+ return track.label.length <= 3;
3108
+ }
3109
+ async function listTracks(videoId, options = {}) {
3110
+ const url = buildApiUrl$1(`/v1/videos/${videoId}/subtitles`, options.baseUrl);
3111
+ const response = await fetch(url, {
3112
+ method: "GET",
3113
+ credentials: "include",
3114
+ headers: {
3115
+ ...getAuthHeaders$1(options.authToken)
3116
+ }
3117
+ });
3118
+ const data = await handleResponse$1(response);
3119
+ const rawTracks = Array.isArray(data) ? data : Array.isArray(data == null ? void 0 : data.tracks) ? data.tracks : [];
3120
+ return rawTracks.map(normalizeSubtitleTrack).filter((track) => track.id);
3121
+ }
3122
+ async function uploadTrack(videoId, file, uploadOptions, options = {}) {
3123
+ const params = new URLSearchParams();
3124
+ params.set("lang", uploadOptions.lang);
3125
+ if (uploadOptions.label) {
3126
+ params.set("label", uploadOptions.label);
3127
+ }
3128
+ if (uploadOptions.kind) {
3129
+ params.set("kind", uploadOptions.kind);
3130
+ }
3131
+ {
3132
+ params.set("default", "false");
3133
+ }
3134
+ const url = buildApiUrl$1(
3135
+ `/v1/videos/${videoId}/subtitles?${params.toString()}`,
3136
+ options.baseUrl
3137
+ );
3138
+ const formData = new FormData();
3139
+ formData.append("file", file);
3140
+ const response = await fetch(url, {
3141
+ method: "POST",
3142
+ credentials: "include",
3143
+ headers: {
3144
+ ...getAuthHeaders$1(options.authToken)
3145
+ },
3146
+ body: formData,
3147
+ signal: uploadOptions.signal
3148
+ });
3149
+ const data = await handleResponse$1(response);
3150
+ if (!data) return null;
3151
+ return normalizeSubtitleTrack(data);
3152
+ }
3153
+ async function patchTrackDisabled(videoId, trackId, isDisabled, options = {}) {
3154
+ const url = buildApiUrl$1(
3155
+ `/v1/videos/${videoId}/subtitles/${trackId}/disabled`,
3156
+ options.baseUrl
3157
+ );
3158
+ const response = await fetch(url, {
3159
+ method: "PATCH",
3160
+ credentials: "include",
3161
+ headers: {
3162
+ "Content-Type": "application/json",
3163
+ ...getAuthHeaders$1(options.authToken)
3164
+ },
3165
+ body: JSON.stringify({ isDisabled })
3166
+ });
3167
+ await handleResponse$1(response);
3168
+ }
3169
+ async function deleteTrack(videoId, trackId, options = {}) {
3170
+ const url = buildApiUrl$1(
3171
+ `/v1/videos/${videoId}/subtitles/${trackId}`,
3172
+ options.baseUrl
3173
+ );
3174
+ const response = await fetch(url, {
3175
+ method: "DELETE",
3176
+ credentials: "include",
3177
+ headers: {
3178
+ ...getAuthHeaders$1(options.authToken)
3179
+ }
3180
+ });
3181
+ await handleResponse$1(response);
3182
+ }
2956
3183
  const LANG_NAMES = {
2957
3184
  en: "English",
2958
3185
  es: "Español",
@@ -2988,7 +3215,7 @@ function VideoSubtitlesMenu({
2988
3215
  const items = settings.subtitles.map((item) => {
2989
3216
  const isActive = item.id === settings.currentSubtitleId;
2990
3217
  let label = item.label;
2991
- if (item.isDefault) {
3218
+ if (isAutoGeneratedTrack({ label })) {
2992
3219
  const fullName = item.lang ? LANG_NAMES[item.lang.toLowerCase()] : null;
2993
3220
  if (fullName) label = fullName;
2994
3221
  label = `${label} (${autoLabel})`;
@@ -3176,8 +3403,8 @@ function VideoCustomControls({
3176
3403
  const visibleBufferedRanges = isProcessing ? [] : bufferedRanges;
3177
3404
  const sortedChapters = useMemo(() => {
3178
3405
  if (!chapters || chapters.length === 0) return [];
3179
- return [...chapters].sort((a, b) => a.startSec - b.startSec);
3180
- }, [chapters]);
3406
+ return [...chapters].filter((ch) => duration > 0 && ch.startSec <= duration).sort((a, b) => a.startSec - b.startSec);
3407
+ }, [chapters, duration]);
3181
3408
  const chapterSegments = useMemo(() => {
3182
3409
  if (sortedChapters.length === 0 || duration <= 0) return [];
3183
3410
  const segments = [];
@@ -3538,10 +3765,6 @@ function VideoCustomControls({
3538
3765
  className: "video-player-button",
3539
3766
  type: "button",
3540
3767
  "aria-label": t("editor.video.player.settings"),
3541
- onClick: () => {
3542
- setIsSettingsOpen((prev) => !prev);
3543
- setActiveMenu("main");
3544
- },
3545
3768
  children: /* @__PURE__ */ jsx(SettingsPlayerIcon, { size: 20 })
3546
3769
  }
3547
3770
  ) }),
@@ -3579,6 +3802,7 @@ function VideoCustomControls({
3579
3802
  onSelect: (speed) => {
3580
3803
  setOptimisticSpeed(speed);
3581
3804
  onSpeedClick(speed);
3805
+ setIsSettingsOpen(false);
3582
3806
  }
3583
3807
  }
3584
3808
  ),
@@ -3588,7 +3812,10 @@ function VideoCustomControls({
3588
3812
  disabled: isSettingsDisabled,
3589
3813
  settings,
3590
3814
  onBack: () => setActiveMenu("main"),
3591
- onSelect: onSubtitleSelect
3815
+ onSelect: (subtitleId) => {
3816
+ onSubtitleSelect(subtitleId);
3817
+ setIsSettingsOpen(false);
3818
+ }
3592
3819
  }
3593
3820
  ),
3594
3821
  activeMenu === "quality" && /* @__PURE__ */ jsx(
@@ -3597,7 +3824,10 @@ function VideoCustomControls({
3597
3824
  disabled: isSettingsDisabled,
3598
3825
  settings,
3599
3826
  onBack: () => setActiveMenu("main"),
3600
- onSelect: onQualityClick
3827
+ onSelect: (qualityId) => {
3828
+ onQualityClick(qualityId);
3829
+ setIsSettingsOpen(false);
3830
+ }
3601
3831
  }
3602
3832
  )
3603
3833
  ]
@@ -3772,141 +4002,21 @@ const setSubtitle = (player, id, videoElement) => {
3772
4002
  track.mode = index === id ? "showing" : "disabled";
3773
4003
  });
3774
4004
  };
3775
- function buildApiUrl$1(path, baseUrl) {
3776
- if (path.startsWith("http://") || path.startsWith("https://")) {
3777
- return path;
4005
+ function normalizeUrl(url, baseUrl) {
4006
+ if (!url) return null;
4007
+ if (url.startsWith("http://") || url.startsWith("https://")) {
4008
+ return url;
3778
4009
  }
3779
- const base = baseUrl || (typeof window !== "undefined" ? window.location.origin : "");
3780
- return `${base}${path}`;
3781
- }
3782
- function getAuthHeaders$1(authToken) {
3783
- if (!authToken) return {};
3784
- return { Authorization: `Bearer ${authToken}` };
3785
- }
3786
- async function parseJson$1(response) {
3787
- const text = await response.text();
3788
- if (!text) return null;
3789
- try {
3790
- return JSON.parse(text);
3791
- } catch {
3792
- return text;
4010
+ if (url.startsWith("/")) {
4011
+ const base = baseUrl || window.location.origin;
4012
+ return `${base}${url}`;
3793
4013
  }
4014
+ return url;
3794
4015
  }
3795
- async function handleResponse$1(response) {
3796
- if (!response.ok) {
3797
- const data = await parseJson$1(response);
3798
- const message = (data && typeof data === "object" && "message" in data ? data.message : null) || response.statusText || "Request failed";
3799
- throw new Error(message);
3800
- }
3801
- return parseJson$1(response);
3802
- }
3803
- function normalizeSubtitleTrack(raw) {
3804
- const id = (raw == null ? void 0 : raw.id) ?? (raw == null ? void 0 : raw.trackId) ?? (raw == null ? void 0 : raw.subtitleId) ?? "";
3805
- const langRaw = (raw == null ? void 0 : raw.lang) ?? (raw == null ? void 0 : raw.language) ?? (raw == null ? void 0 : raw.srclang) ?? "";
3806
- const lang = langRaw ? String(langRaw) : "und";
3807
- const labelRaw = (raw == null ? void 0 : raw.label) ?? (raw == null ? void 0 : raw.name) ?? (raw == null ? void 0 : raw.title) ?? lang;
3808
- const label = labelRaw ? String(labelRaw) : lang;
3809
- const kind = (raw == null ? void 0 : raw.kind) ?? "subtitles";
3810
- const isDefault = Boolean((raw == null ? void 0 : raw.isDefault) ?? (raw == null ? void 0 : raw.default) ?? false);
3811
- const url = typeof (raw == null ? void 0 : raw.url) === "string" ? raw.url : typeof (raw == null ? void 0 : raw.vttUrl) === "string" ? raw.vttUrl : typeof (raw == null ? void 0 : raw.src) === "string" ? raw.src : void 0;
3812
- const createdAt = typeof (raw == null ? void 0 : raw.createdAt) === "string" ? raw.createdAt : void 0;
3813
- const fileName = typeof (raw == null ? void 0 : raw.fileName) === "string" ? raw.fileName : void 0;
3814
- const auto = Boolean((raw == null ? void 0 : raw.auto) ?? (raw == null ? void 0 : raw.isAuto) ?? (raw == null ? void 0 : raw.source) === "auto");
3815
- return {
3816
- id: String(id),
3817
- lang,
3818
- label,
3819
- kind: String(kind),
3820
- isDefault,
3821
- url,
3822
- createdAt,
3823
- fileName,
3824
- status: "ready",
3825
- auto
3826
- };
3827
- }
3828
- async function uploadTrack(videoId, file, uploadOptions, options = {}) {
3829
- const params = new URLSearchParams();
3830
- params.set("lang", uploadOptions.lang);
3831
- if (uploadOptions.label) {
3832
- params.set("label", uploadOptions.label);
3833
- }
3834
- if (uploadOptions.kind) {
3835
- params.set("kind", uploadOptions.kind);
3836
- }
3837
- {
3838
- params.set("default", "false");
3839
- }
3840
- const url = buildApiUrl$1(
3841
- `/v1/videos/${videoId}/subtitles?${params.toString()}`,
3842
- options.baseUrl
3843
- );
3844
- const formData = new FormData();
3845
- formData.append("file", file);
3846
- const response = await fetch(url, {
3847
- method: "POST",
3848
- credentials: "include",
3849
- headers: {
3850
- ...getAuthHeaders$1(options.authToken)
3851
- },
3852
- body: formData,
3853
- signal: uploadOptions.signal
3854
- });
3855
- const data = await handleResponse$1(response);
3856
- if (!data) return null;
3857
- return normalizeSubtitleTrack(data);
3858
- }
3859
- function getVttUrl(videoId, trackId, options = {}) {
3860
- return buildApiUrl$1(
3861
- `/v1/videos/${videoId}/subtitles/${trackId}.vtt`,
3862
- options.baseUrl
3863
- );
3864
- }
3865
- function normalizeUrl$1(path, baseUrl) {
3866
- if (path.startsWith("http://") || path.startsWith("https://")) {
3867
- return path;
3868
- }
3869
- const base = baseUrl || (typeof window !== "undefined" ? window.location.origin : "");
3870
- if (path.startsWith("/")) {
3871
- return `${base}${path}`;
3872
- }
3873
- return `${base}/${path}`;
3874
- }
3875
- function resolveSubtitleUrl(videoId, track, options = {}) {
3876
- if (track.url) {
3877
- return normalizeUrl$1(track.url, options.baseUrl);
3878
- }
3879
- return getVttUrl(videoId, track.id, options);
3880
- }
3881
- async function deleteTrack(videoId, trackId, options = {}) {
3882
- const url = buildApiUrl$1(
3883
- `/v1/videos/${videoId}/subtitles/${trackId}`,
3884
- options.baseUrl
3885
- );
3886
- const response = await fetch(url, {
3887
- method: "DELETE",
3888
- credentials: "include",
3889
- headers: {
3890
- ...getAuthHeaders$1(options.authToken)
3891
- }
3892
- });
3893
- await handleResponse$1(response);
3894
- }
3895
- function normalizeUrl(url, baseUrl) {
3896
- if (!url) return null;
3897
- if (url.startsWith("http://") || url.startsWith("https://")) {
3898
- return url;
3899
- }
3900
- if (url.startsWith("/")) {
3901
- const base = baseUrl || window.location.origin;
3902
- return `${base}${url}`;
3903
- }
3904
- return url;
3905
- }
3906
- function getNativeSubtitleTracks(videoElement) {
3907
- return Array.from(videoElement.textTracks || []).filter(
3908
- (track) => track.kind === "subtitles" || track.kind === "captions"
3909
- );
4016
+ function getNativeSubtitleTracks(videoElement) {
4017
+ return Array.from(videoElement.textTracks || []).filter(
4018
+ (track) => track.kind === "subtitles" || track.kind === "captions"
4019
+ );
3910
4020
  }
3911
4021
  function getSubtitleUrl(track, baseUrl) {
3912
4022
  if (!track.url) return null;
@@ -3926,7 +4036,8 @@ function VideoPlayerState({
3926
4036
  subtitles,
3927
4037
  isProcessing = false,
3928
4038
  posterUrl,
3929
- onPlayerInfo
4039
+ onPlayerInfo,
4040
+ onFallback
3930
4041
  }) {
3931
4042
  const { t, i18n } = useTranslation();
3932
4043
  const [currentVideoUrl, setCurrentVideoUrl] = useState(null);
@@ -3953,6 +4064,9 @@ function VideoPlayerState({
3953
4064
  );
3954
4065
  const shakaAddedIdsRef = useRef([]);
3955
4066
  const shakaFilterRegisteredRef = useRef(false);
4067
+ const triedFallbackUrlsRef = useRef(/* @__PURE__ */ new Set());
4068
+ const hlsUrlCheckedRef = useRef(null);
4069
+ const savedStateLockRef = useRef(false);
3956
4070
  useEffect(() => {
3957
4071
  if (!language) {
3958
4072
  return;
@@ -3966,16 +4080,17 @@ function VideoPlayerState({
3966
4080
  return;
3967
4081
  }
3968
4082
  if (subtitles != null) {
3969
- const normalized = (subtitles || []).map(normalizeSubtitleTrack).filter((track) => track.id);
4083
+ const normalized = (subtitles || []).map(normalizeSubtitleTrack).filter((track) => track.id && !track.isDisabled);
3970
4084
  setSubtitleTracks(normalized);
3971
4085
  }
3972
4086
  }, [videoId, subtitles]);
3973
4087
  useEffect(() => {
3974
4088
  setIsAutoQualityEnabled(true);
4089
+ hlsUrlCheckedRef.current = null;
3975
4090
  }, [videoId]);
3976
4091
  const handleSubtitlesOpen = useCallback(() => {
3977
4092
  if (!videoId || subtitles == null) return;
3978
- const normalized = (subtitles || []).map(normalizeSubtitleTrack).filter((track) => track.id);
4093
+ const normalized = (subtitles || []).map(normalizeSubtitleTrack).filter((track) => track.id && !track.isDisabled);
3979
4094
  setSubtitleTracks(normalized);
3980
4095
  }, [videoId, subtitles]);
3981
4096
  useEffect(() => {
@@ -4082,102 +4197,148 @@ function VideoPlayerState({
4082
4197
  selectedSubtitleMeta
4083
4198
  ]);
4084
4199
  useEffect(() => {
4085
- var _a;
4086
- if (!currentVideoUrl || !((_a = playerRef.current) == null ? void 0 : _a.player)) {
4087
- return;
4088
- }
4089
- const player = playerRef.current.player;
4090
- if (errorHandlerRef.current) {
4091
- try {
4092
- player.removeEventListener("error", errorHandlerRef.current);
4093
- } catch (e) {
4094
- console.warn("Error removing old event listener:", e);
4200
+ if (!currentVideoUrl) return;
4201
+ let cancelled = false;
4202
+ let player = null;
4203
+ const handleFatalError = (error) => {
4204
+ if (cancelled) return;
4205
+ const fallback = normalizeUrl(nativeVideoUrl, baseUrl) || normalizeUrl(rawUrl ?? null, baseUrl);
4206
+ if (fallback) {
4207
+ setHlsFailed(true);
4208
+ onFallback == null ? void 0 : onFallback(true);
4209
+ } else {
4210
+ setPlayerError(
4211
+ t("editor.video.player.playbackError", { code: error.code })
4212
+ );
4095
4213
  }
4096
- }
4214
+ };
4097
4215
  const errorHandler = (event) => {
4098
4216
  const error = event.detail;
4099
- console.error("Shaka Player error:", {
4100
- code: error.code,
4101
- category: error.category,
4102
- severity: error.severity,
4103
- data: error.data
4104
- });
4105
4217
  if (error.severity === 2) {
4106
- const fallback = normalizeUrl(nativeVideoUrl, baseUrl) || normalizeUrl(rawUrl ?? null, baseUrl);
4107
- if (fallback) {
4108
- console.warn(
4109
- `[VideoPlayer] HLS failed (code=${error.code}), falling back to native video:`,
4110
- fallback
4111
- );
4112
- setHlsFailed(true);
4113
- } else {
4114
- setPlayerError(
4115
- t("editor.video.player.playbackError", { code: error.code })
4116
- );
4117
- }
4218
+ handleFatalError(error);
4118
4219
  } else {
4119
4220
  try {
4120
- player.recover();
4121
- } catch (recoverError) {
4122
- console.error("Failed to recover from error:", recoverError);
4221
+ player == null ? void 0 : player.recover();
4222
+ } catch {
4123
4223
  }
4124
4224
  }
4125
4225
  };
4126
- errorHandlerRef.current = errorHandler;
4127
- player.addEventListener("error", errorHandler);
4226
+ const tryLoad = () => {
4227
+ var _a;
4228
+ player = (_a = playerRef.current) == null ? void 0 : _a.player;
4229
+ if (!player) {
4230
+ const raf2 = requestAnimationFrame(tryLoad);
4231
+ return () => cancelAnimationFrame(raf2);
4232
+ }
4233
+ if (errorHandlerRef.current) {
4234
+ try {
4235
+ player.removeEventListener("error", errorHandlerRef.current);
4236
+ } catch {
4237
+ }
4238
+ }
4239
+ errorHandlerRef.current = errorHandler;
4240
+ player.addEventListener("error", errorHandler);
4241
+ player.load(currentVideoUrl).catch((loadError) => {
4242
+ if (cancelled) return;
4243
+ if (loadError.code === 7e3) return;
4244
+ handleFatalError(loadError);
4245
+ });
4246
+ };
4247
+ const raf = requestAnimationFrame(tryLoad);
4128
4248
  return () => {
4249
+ cancelled = true;
4250
+ cancelAnimationFrame(raf);
4129
4251
  if (player && errorHandlerRef.current) {
4130
4252
  try {
4131
4253
  player.removeEventListener("error", errorHandlerRef.current);
4132
- } catch (e) {
4133
- console.warn("Error removing event listener on cleanup:", e);
4254
+ } catch {
4134
4255
  }
4135
4256
  }
4136
4257
  };
4137
- }, [currentVideoUrl, nativeVideoUrl, rawUrl, baseUrl]);
4258
+ }, [currentVideoUrl, nativeVideoUrl, rawUrl, baseUrl, onFallback]);
4138
4259
  useEffect(() => {
4139
- var _a, _b;
4260
+ let cancelled = false;
4261
+ let retryTimer = null;
4140
4262
  if (optimizedReady && primaryUrl) {
4141
4263
  const normalizedUrl = normalizeUrl(primaryUrl, baseUrl);
4142
4264
  if (normalizedUrl) {
4143
- if (previousVideoUrlRef.current && previousVideoUrlRef.current !== normalizedUrl) {
4144
- const videoElement = nativeVideoRef.current || ((_a = playerRef.current) == null ? void 0 : _a.videoElement);
4145
- if (videoElement) {
4146
- savedStateRef.current = {
4147
- time: videoElement.currentTime || 0,
4148
- paused: videoElement.paused
4149
- };
4265
+ if (!currentNativeVideoUrl && rawUrl) {
4266
+ const normalizedNative = normalizeUrl(rawUrl, baseUrl);
4267
+ if (normalizedNative) {
4268
+ setCurrentNativeVideoUrl(normalizedNative);
4150
4269
  }
4151
- } else if (nativeVideoRef.current && !savedStateRef.current) {
4152
- const nativeVideo = nativeVideoRef.current;
4270
+ }
4271
+ if (hlsUrlCheckedRef.current === normalizedUrl) {
4272
+ return;
4273
+ }
4274
+ const nv = nativeVideoRef.current;
4275
+ if (nv && nv.currentTime > 0) {
4153
4276
  savedStateRef.current = {
4154
- time: nativeVideo.currentTime || 0,
4155
- paused: nativeVideo.paused
4277
+ time: nv.currentTime,
4278
+ paused: nv.paused
4156
4279
  };
4280
+ savedStateLockRef.current = true;
4157
4281
  }
4158
- setCurrentVideoUrl(normalizedUrl);
4159
- setPlayerError(null);
4160
- setHlsFailed(false);
4161
- previousVideoUrlRef.current = normalizedUrl;
4282
+ const checkHls = (attempt) => {
4283
+ if (cancelled) return;
4284
+ fetch(normalizedUrl, { method: "HEAD", credentials: "include" }).then((res) => {
4285
+ if (cancelled) return;
4286
+ hlsUrlCheckedRef.current = normalizedUrl;
4287
+ if (res.ok) {
4288
+ setCurrentVideoUrl(normalizedUrl);
4289
+ setPlayerError(null);
4290
+ setHlsFailed(false);
4291
+ onFallback == null ? void 0 : onFallback(false);
4292
+ triedFallbackUrlsRef.current.clear();
4293
+ } else {
4294
+ triedFallbackUrlsRef.current.clear();
4295
+ setHlsFailed(true);
4296
+ onFallback == null ? void 0 : onFallback(true);
4297
+ scheduleRetry(attempt);
4298
+ }
4299
+ }).catch((err) => {
4300
+ if (cancelled) return;
4301
+ triedFallbackUrlsRef.current.clear();
4302
+ setHlsFailed(true);
4303
+ onFallback == null ? void 0 : onFallback(true);
4304
+ scheduleRetry(attempt);
4305
+ });
4306
+ };
4307
+ const MAX_RETRIES = 10;
4308
+ const RETRY_INTERVAL = 5e3;
4309
+ const scheduleRetry = (attempt) => {
4310
+ if (cancelled || attempt >= MAX_RETRIES) return;
4311
+ retryTimer = setTimeout(() => checkHls(attempt + 1), RETRY_INTERVAL);
4312
+ };
4313
+ checkHls(0);
4162
4314
  }
4163
4315
  } else if (nativeVideoUrl) {
4164
4316
  const normalizedUrl = normalizeUrl(nativeVideoUrl, baseUrl);
4165
4317
  if (normalizedUrl) {
4166
- if (previousVideoUrlRef.current && previousVideoUrlRef.current !== normalizedUrl) {
4167
- const videoElement = (_b = playerRef.current) == null ? void 0 : _b.videoElement;
4168
- if (videoElement) {
4169
- savedStateRef.current = {
4170
- time: videoElement.currentTime || 0,
4171
- paused: videoElement.paused
4172
- };
4173
- }
4318
+ if (previousVideoUrlRef.current === normalizedUrl) {
4319
+ return;
4320
+ }
4321
+ const nv = nativeVideoRef.current;
4322
+ if (nv && nv.readyState > 0 && !nv.error && nv.src) {
4323
+ previousVideoUrlRef.current = normalizedUrl;
4324
+ return;
4325
+ }
4326
+ if (previousVideoUrlRef.current && nv && nv.currentTime > 0) {
4327
+ savedStateRef.current = {
4328
+ time: nv.currentTime,
4329
+ paused: nv.paused
4330
+ };
4174
4331
  }
4175
4332
  setCurrentNativeVideoUrl(normalizedUrl);
4176
4333
  setCurrentVideoUrl(null);
4177
4334
  previousVideoUrlRef.current = normalizedUrl;
4178
4335
  }
4179
4336
  }
4180
- }, [videoUrl, primaryUrl, baseUrl, optimizedReady, nativeVideoUrl]);
4337
+ return () => {
4338
+ cancelled = true;
4339
+ if (retryTimer) clearTimeout(retryTimer);
4340
+ };
4341
+ }, [primaryUrl, baseUrl, optimizedReady, nativeVideoUrl]);
4181
4342
  useEffect(() => {
4182
4343
  if (optimizedReady && currentVideoUrl && playerRef.current) {
4183
4344
  const videoElement = playerRef.current.videoElement;
@@ -4191,6 +4352,9 @@ function VideoPlayerState({
4191
4352
  });
4192
4353
  }
4193
4354
  savedStateRef.current = null;
4355
+ savedStateLockRef.current = false;
4356
+ } else {
4357
+ savedStateLockRef.current = false;
4194
4358
  }
4195
4359
  }
4196
4360
  };
@@ -4224,10 +4388,35 @@ function VideoPlayerState({
4224
4388
  }
4225
4389
  }
4226
4390
  }, [optimizedReady, currentVideoUrl]);
4391
+ useEffect(() => {
4392
+ const nv = nativeVideoRef.current;
4393
+ if (!nv) return;
4394
+ if (currentVideoUrl && !hlsFailed) {
4395
+ if (nv.currentTime > 0) {
4396
+ savedStateRef.current = {
4397
+ time: nv.currentTime,
4398
+ paused: nv.paused
4399
+ };
4400
+ }
4401
+ if (!nv.paused) {
4402
+ nv.pause();
4403
+ }
4404
+ }
4405
+ }, [currentVideoUrl, hlsFailed]);
4227
4406
  useEffect(() => {
4228
4407
  var _a;
4229
- const nextVideoElement = optimizedReady && !hlsFailed ? ((_a = playerRef.current) == null ? void 0 : _a.videoElement) || null : nativeVideoRef.current;
4230
- setActiveVideoElement(nextVideoElement);
4408
+ if (currentVideoUrl && !hlsFailed) {
4409
+ const shakaVideo = ((_a = playerRef.current) == null ? void 0 : _a.videoElement) || null;
4410
+ if (shakaVideo && shakaVideo.readyState >= 1) {
4411
+ setActiveVideoElement(shakaVideo);
4412
+ } else if (shakaVideo) {
4413
+ const onReady = () => setActiveVideoElement(shakaVideo);
4414
+ shakaVideo.addEventListener("loadedmetadata", onReady, { once: true });
4415
+ return () => shakaVideo.removeEventListener("loadedmetadata", onReady);
4416
+ }
4417
+ } else {
4418
+ setActiveVideoElement(nativeVideoRef.current);
4419
+ }
4231
4420
  }, [optimizedReady, hlsFailed, currentVideoUrl, currentNativeVideoUrl]);
4232
4421
  useEffect(() => {
4233
4422
  if (!activeVideoElement) return;
@@ -4257,16 +4446,29 @@ function VideoPlayerState({
4257
4446
  return () => observer.disconnect();
4258
4447
  }, []);
4259
4448
  useEffect(() => {
4260
- var _a;
4449
+ var _a, _b;
4261
4450
  if (!onPlayerInfo) return;
4262
4451
  const shakaPlayer = ((_a = playerRef.current) == null ? void 0 : _a.player) || null;
4263
4452
  const nativeVideo = nativeVideoRef.current || null;
4264
4453
  const containerEl = containerRef.current || null;
4265
- const mode = optimizedReady && !hlsFailed && shakaPlayer ? "shaka" : "native";
4266
- onPlayerInfo({ shakaPlayer, nativeVideo, mode, containerEl });
4267
- }, [onPlayerInfo, optimizedReady, hlsFailed, currentVideoUrl, currentNativeVideoUrl]);
4454
+ const mode = currentVideoUrl && !hlsFailed && shakaPlayer ? "shaka" : "native";
4455
+ let videoDuration = null;
4456
+ try {
4457
+ const range = (_b = shakaPlayer == null ? void 0 : shakaPlayer.seekRange) == null ? void 0 : _b.call(shakaPlayer);
4458
+ if (range && Number.isFinite(range.end) && range.end > 0) {
4459
+ videoDuration = range.end;
4460
+ }
4461
+ } catch {
4462
+ }
4463
+ if (videoDuration == null && activeVideoElement) {
4464
+ const d = activeVideoElement.duration;
4465
+ if (Number.isFinite(d) && d > 0) videoDuration = d;
4466
+ }
4467
+ onPlayerInfo({ shakaPlayer, nativeVideo, mode, containerEl, videoDuration });
4468
+ }, [onPlayerInfo, optimizedReady, hlsFailed, currentVideoUrl, currentNativeVideoUrl, activeVideoElement]);
4268
4469
  useEffect(() => {
4269
4470
  var _a;
4471
+ console.log("[Cover] posterUrl effect:", posterUrl);
4270
4472
  const nativeVideo = nativeVideoRef.current;
4271
4473
  if (nativeVideo) {
4272
4474
  nativeVideo.poster = posterUrl || "";
@@ -4343,6 +4545,7 @@ function VideoPlayerState({
4343
4545
  nativeVideo.play().catch(() => {
4344
4546
  });
4345
4547
  }
4548
+ savedStateLockRef.current = false;
4346
4549
  }
4347
4550
  };
4348
4551
  if (nativeVideo.readyState >= 1) {
@@ -4353,10 +4556,12 @@ function VideoPlayerState({
4353
4556
  });
4354
4557
  }
4355
4558
  const handleTimeUpdate = () => {
4559
+ if (savedStateLockRef.current) return;
4356
4560
  if (timeUpdateThrottleRef.current) {
4357
4561
  clearTimeout(timeUpdateThrottleRef.current);
4358
4562
  }
4359
4563
  timeUpdateThrottleRef.current = setTimeout(() => {
4564
+ if (savedStateLockRef.current) return;
4360
4565
  if (!savedStateRef.current) {
4361
4566
  savedStateRef.current = { time: 0, paused: true };
4362
4567
  }
@@ -4364,6 +4569,7 @@ function VideoPlayerState({
4364
4569
  }, 100);
4365
4570
  };
4366
4571
  const handlePlay = () => {
4572
+ if (savedStateLockRef.current) return;
4367
4573
  if (!savedStateRef.current) {
4368
4574
  savedStateRef.current = { time: 0, paused: false };
4369
4575
  } else {
@@ -4371,6 +4577,7 @@ function VideoPlayerState({
4371
4577
  }
4372
4578
  };
4373
4579
  const handlePause = () => {
4580
+ if (savedStateLockRef.current) return;
4374
4581
  if (!savedStateRef.current) {
4375
4582
  savedStateRef.current = { time: nativeVideo.currentTime, paused: true };
4376
4583
  } else {
@@ -4388,6 +4595,12 @@ function VideoPlayerState({
4388
4595
  if (timeUpdateThrottleRef.current) {
4389
4596
  clearTimeout(timeUpdateThrottleRef.current);
4390
4597
  }
4598
+ if (nativeVideo.currentTime > 0) {
4599
+ savedStateRef.current = {
4600
+ time: nativeVideo.currentTime,
4601
+ paused: nativeVideo.paused
4602
+ };
4603
+ }
4391
4604
  };
4392
4605
  }, [currentNativeVideoUrl]);
4393
4606
  useEffect(() => {
@@ -4547,7 +4760,8 @@ function VideoPlayerState({
4547
4760
  }
4548
4761
  );
4549
4762
  }
4550
- const fallbackVideoUrl = currentNativeVideoUrl || normalizeUrl(rawUrl ?? null, baseUrl);
4763
+ const fallbackVideoUrl = normalizeUrl(rawUrl ?? null, baseUrl) || currentNativeVideoUrl;
4764
+ const shakaActive = !!(currentVideoUrl && !hlsFailed);
4551
4765
  const isCompact = playerWidth > 0 && playerWidth < 500;
4552
4766
  const isNarrow = playerWidth > 0 && playerWidth < 380;
4553
4767
  const hasChapters = ((chapters == null ? void 0 : chapters.length) ?? 0) > 0;
@@ -4576,25 +4790,41 @@ function VideoPlayerState({
4576
4790
  ref: containerRef,
4577
4791
  onClick: handleContainerClick,
4578
4792
  children: [
4579
- (!optimizedReady || hlsFailed) && /* @__PURE__ */ jsx(
4793
+ /* @__PURE__ */ jsx(
4580
4794
  "video",
4581
4795
  {
4582
4796
  ref: nativeVideoRef,
4583
- src: (hlsFailed ? fallbackVideoUrl : currentNativeVideoUrl) || void 0,
4797
+ src: currentNativeVideoUrl || (hlsFailed ? fallbackVideoUrl : null) || void 0,
4584
4798
  className: "video-player-media",
4585
- poster: posterUrl || void 0
4799
+ style: shakaActive ? { display: "none" } : void 0,
4800
+ poster: posterUrl || void 0,
4801
+ onError: (e) => {
4802
+ const video = e.currentTarget;
4803
+ const failedSrc = video.currentSrc || video.src;
4804
+ if (!failedSrc) return;
4805
+ const mediaError = video.error;
4806
+ if (!mediaError) return;
4807
+ triedFallbackUrlsRef.current.add(failedSrc);
4808
+ const candidates = [
4809
+ normalizeUrl(rawUrl ?? null, baseUrl),
4810
+ normalizeUrl(nativeVideoUrl, baseUrl)
4811
+ ].filter((url) => !!url && !triedFallbackUrlsRef.current.has(url));
4812
+ if (candidates.length > 0) {
4813
+ video.src = candidates[0];
4814
+ video.load();
4815
+ }
4816
+ }
4586
4817
  }
4587
4818
  ),
4588
- optimizedReady && !hlsFailed && currentVideoUrl && /* @__PURE__ */ jsx(
4819
+ shakaActive && /* @__PURE__ */ jsx(
4589
4820
  ShakaPlayer,
4590
4821
  {
4591
4822
  ref: playerRef,
4592
- src: currentVideoUrl,
4593
4823
  autoPlay: false,
4594
4824
  className: "video-player-media"
4595
4825
  }
4596
4826
  ),
4597
- (currentNativeVideoUrl || currentVideoUrl || hlsFailed && fallbackVideoUrl) && /* @__PURE__ */ jsxs(Fragment, { children: [
4827
+ (currentNativeVideoUrl || currentVideoUrl || primaryUrl || hlsFailed && fallbackVideoUrl) && /* @__PURE__ */ jsxs(Fragment, { children: [
4598
4828
  isPaused && !isCompact && /* @__PURE__ */ jsx(
4599
4829
  "button",
4600
4830
  {
@@ -4743,6 +4973,115 @@ function VideoPlayerState({
4743
4973
  }
4744
4974
  );
4745
4975
  }
4976
+ function VideoSelect({
4977
+ size = "sm",
4978
+ w = 150,
4979
+ comboboxProps,
4980
+ rightSection,
4981
+ renderOption,
4982
+ data,
4983
+ value,
4984
+ defaultValue,
4985
+ onChange,
4986
+ inputClassName,
4987
+ ...inputProps
4988
+ }) {
4989
+ var _a;
4990
+ const {
4991
+ onClick: onInputClick,
4992
+ type: _inputType,
4993
+ onCopy: _onCopy,
4994
+ onCut: _onCut,
4995
+ onPaste: _onPaste,
4996
+ classNames: parentClassNames,
4997
+ allowDeselect: _allowDeselect,
4998
+ ...restInputProps
4999
+ } = inputProps;
5000
+ const parentInputClass = typeof parentClassNames === "object" && parentClassNames && "input" in parentClassNames ? parentClassNames.input : void 0;
5001
+ const inputClass = [parentInputClass, inputClassName].filter(Boolean).join(" ") || void 0;
5002
+ const mergedClassNames = inputClass ? {
5003
+ ...typeof parentClassNames === "object" && parentClassNames ? parentClassNames : {},
5004
+ input: inputClass
5005
+ } : parentClassNames;
5006
+ const [search, setSearch] = useState("");
5007
+ const combobox = useCombobox({
5008
+ onDropdownClose: () => {
5009
+ combobox.resetSelectedOption();
5010
+ combobox.focusTarget();
5011
+ setSearch("");
5012
+ },
5013
+ onDropdownOpen: () => {
5014
+ combobox.focusSearchInput();
5015
+ }
5016
+ });
5017
+ const normalizedData = useMemo(
5018
+ () => (Array.isArray(data) ? data : []).map(
5019
+ (item) => typeof item === "string" ? { value: item, label: item } : item
5020
+ ),
5021
+ [data]
5022
+ );
5023
+ const selectedValue = value ?? defaultValue ?? null;
5024
+ const selectedLabel = ((_a = normalizedData.find((item) => item.value === selectedValue)) == null ? void 0 : _a.label) ?? "";
5025
+ const renderOptionNode = (optionValue, optionLabel, checked) => renderOption ? renderOption({
5026
+ option: { value: optionValue, label: optionLabel },
5027
+ checked
5028
+ }) : /* @__PURE__ */ jsxs(Group, { justify: "space-between", wrap: "nowrap", w: "100%", children: [
5029
+ optionLabel,
5030
+ checked && /* @__PURE__ */ jsx(IconCheck, { size: 16, color: "var(--primary-color-text, #4C6EF5)" })
5031
+ ] });
5032
+ const { width: comboboxWidth, ...restComboboxProps } = comboboxProps || {};
5033
+ const dropdownWidth = comboboxWidth ?? 250;
5034
+ return /* @__PURE__ */ jsxs(
5035
+ Combobox,
5036
+ {
5037
+ store: combobox,
5038
+ withinPortal: true,
5039
+ onOptionSubmit: (val) => {
5040
+ const option = normalizedData.find((item) => item.value === val) ?? {
5041
+ value: val,
5042
+ label: val
5043
+ };
5044
+ onChange == null ? void 0 : onChange(val, option);
5045
+ combobox.closeDropdown();
5046
+ },
5047
+ width: dropdownWidth,
5048
+ ...restComboboxProps,
5049
+ children: [
5050
+ /* @__PURE__ */ jsx(Combobox.Target, { children: /* @__PURE__ */ jsx(
5051
+ InputBase,
5052
+ {
5053
+ component: "button",
5054
+ size,
5055
+ w,
5056
+ classNames: mergedClassNames,
5057
+ pointer: true,
5058
+ rightSection: rightSection ?? /* @__PURE__ */ jsx(IconChevronDown, { size: 16, className: "editor-select__chevron" }),
5059
+ rightSectionPointerEvents: "none",
5060
+ onClick: (e) => {
5061
+ onInputClick == null ? void 0 : onInputClick(e);
5062
+ combobox.toggleDropdown();
5063
+ },
5064
+ ...restInputProps,
5065
+ children: selectedLabel
5066
+ }
5067
+ ) }),
5068
+ /* @__PURE__ */ jsx(Combobox.Dropdown, { children: /* @__PURE__ */ jsx(
5069
+ Combobox.Options,
5070
+ {
5071
+ style: {
5072
+ maxHeight: 200,
5073
+ overflowY: "auto"
5074
+ },
5075
+ children: normalizedData.length > 0 ? normalizedData.map((item) => {
5076
+ const checked = item.value === selectedValue;
5077
+ return /* @__PURE__ */ jsx(Combobox.Option, { value: item.value, children: renderOptionNode(item.value, item.label, checked) }, item.value);
5078
+ }) : /* @__PURE__ */ jsx(Combobox.Empty, { children: "Nothing found" })
5079
+ }
5080
+ ) })
5081
+ ]
5082
+ }
5083
+ );
5084
+ }
4746
5085
  const isLinkPlatform = (value) => value === "link" || value === "youtube" || value === "vk" || value === "rutube";
4747
5086
  const isUploadPlatform = (value) => value === "upload" || value === "download";
4748
5087
  function VideoUploadComponent({
@@ -4768,7 +5107,7 @@ function VideoUploadComponent({
4768
5107
  const { t, i18n } = useTranslation();
4769
5108
  const { config, dependencies, registerUpload, unregisterUpload, reportError } = useVideoPluginContext();
4770
5109
  const { maxFileSize = 5, pageId } = config;
4771
- const SelectComponent = dependencies.Select || Select;
5110
+ const SelectComponent = dependencies.Select || VideoSelect;
4772
5111
  const [currentState, setCurrentState] = useState(
4773
5112
  initialVideoState ?? "default"
4774
5113
  );
@@ -4780,6 +5119,7 @@ function VideoUploadComponent({
4780
5119
  const [videoId, setVideoId] = useState(initialVideoId ?? null);
4781
5120
  const [linkUrl, setLinkUrl] = useState("");
4782
5121
  const [linkError, setLinkError] = useState(null);
5122
+ const [isHlsFallback, setIsHlsFallback] = useState(false);
4783
5123
  const lastUploadFileRef = useRef(null);
4784
5124
  const uploadCallbackRef = useRef(false);
4785
5125
  const uploadIdRef = useRef(null);
@@ -4869,7 +5209,16 @@ function VideoUploadComponent({
4869
5209
  const resolvedRawPlayable = sourceType === "upload" ? rawPlayable : false;
4870
5210
  const resolvedStatusAvailable = sourceType === "upload" ? isStatusAvailable : false;
4871
5211
  const chapters = chaptersOverride ?? (resolvedStatus == null ? void 0 : resolvedStatus.chapters) ?? void 0;
4872
- const subtitles = subtitlesOverride ?? (resolvedStatus == null ? void 0 : resolvedStatus.subtitles) ?? void 0;
5212
+ const rawSubtitles = subtitlesOverride ?? (resolvedStatus == null ? void 0 : resolvedStatus.subtitles) ?? void 0;
5213
+ const subtitlesRef = useRef(rawSubtitles);
5214
+ const subtitles = useMemo(() => {
5215
+ if (rawSubtitles === subtitlesRef.current) return subtitlesRef.current;
5216
+ if (JSON.stringify(rawSubtitles) === JSON.stringify(subtitlesRef.current)) {
5217
+ return subtitlesRef.current;
5218
+ }
5219
+ subtitlesRef.current = rawSubtitles;
5220
+ return rawSubtitles;
5221
+ }, [rawSubtitles]);
4873
5222
  const inferredNativeVideoUrl = useMemo(() => {
4874
5223
  if (fallbackNativeVideoUrl) return fallbackNativeVideoUrl;
4875
5224
  if (platform === "link") {
@@ -4960,7 +5309,7 @@ function VideoUploadComponent({
4960
5309
  const isReadyForPreview = currentState === "video-preview" && (sourceType === "link" ? true : resolvedStatusAvailable ? resolvedOptimizedReady : resolvedOptimizedReady || resolvedRawPlayable || hasPreviewSource);
4961
5310
  const isProcessingStatus = (resolvedStatus == null ? void 0 : resolvedStatus.state) === "PROCESSING" || (resolvedStatus == null ? void 0 : resolvedStatus.state) === "UPLOADING";
4962
5311
  const hasProcessingPercent = typeof ((_a = resolvedStatus == null ? void 0 : resolvedStatus.processing) == null ? void 0 : _a.percent) === "number";
4963
- const showProcessingBanner = currentState === "video-preview" && sourceType === "upload" && !!resolvedNativeVideoUrl && resolvedStatusAvailable && !resolvedOptimizedReady && (isProcessingStatus || hasProcessingPercent);
5312
+ const showProcessingBanner = currentState === "video-preview" && sourceType === "upload" && !!resolvedNativeVideoUrl && resolvedStatusAvailable && !resolvedOptimizedReady && !isHlsFallback && (isProcessingStatus || hasProcessingPercent);
4964
5313
  const maxBannerPercentRef = useRef(0);
4965
5314
  const rawPercent = Math.max(0, Math.min(100, ((_b = resolvedStatus == null ? void 0 : resolvedStatus.processing) == null ? void 0 : _b.percent) ?? 0));
4966
5315
  if (rawPercent > maxBannerPercentRef.current) {
@@ -5015,12 +5364,17 @@ function VideoUploadComponent({
5015
5364
  setLinkError(null);
5016
5365
  setCurrentState("video-preview");
5017
5366
  };
5367
+ const videoIdRef = useRef(videoId);
5368
+ videoIdRef.current = videoId;
5369
+ const uploadVideoIdRef = useRef(uploadVideoId);
5370
+ uploadVideoIdRef.current = uploadVideoId;
5018
5371
  useEffect(() => {
5019
5372
  return editor.registerCommand(
5020
5373
  UPLOAD_VIDEO_COMMAND,
5021
5374
  (payload) => {
5022
- if (sourceType === "upload" && payload.length > 0) {
5375
+ if (sourceType === "upload" && payload.length > 0 && !videoIdRef.current && !uploadVideoIdRef.current) {
5023
5376
  handleFileSelect(payload[0]);
5377
+ return true;
5024
5378
  }
5025
5379
  return false;
5026
5380
  },
@@ -5075,7 +5429,7 @@ function VideoUploadComponent({
5075
5429
  onStateChange == null ? void 0 : onStateChange(currentState);
5076
5430
  }, [currentState, onStateChange]);
5077
5431
  const renderState = () => {
5078
- var _a2;
5432
+ var _a2, _b2;
5079
5433
  if (sourceType === "link" && currentState !== "video-preview") {
5080
5434
  return /* @__PURE__ */ jsx(
5081
5435
  LinkState,
@@ -5168,6 +5522,8 @@ function VideoUploadComponent({
5168
5522
  }
5169
5523
  ) });
5170
5524
  }
5525
+ const effectivePoster = posterUrl ?? ((_a2 = resolvedStatus == null ? void 0 : resolvedStatus.cover) == null ? void 0 : _a2.url) ?? null;
5526
+ console.log("[Cover] VideoUploadComponent render: posterUrl prop=", posterUrl, "cover.url=", (_b2 = resolvedStatus == null ? void 0 : resolvedStatus.cover) == null ? void 0 : _b2.url, "effective=", effectivePoster);
5171
5527
  return /* @__PURE__ */ jsx(
5172
5528
  VideoPlayerState,
5173
5529
  {
@@ -5182,8 +5538,9 @@ function VideoUploadComponent({
5182
5538
  chapters: showProcessingBanner ? void 0 : chapters,
5183
5539
  isProcessing: showProcessingBanner,
5184
5540
  subtitles,
5185
- posterUrl: posterUrl ?? ((_a2 = resolvedStatus == null ? void 0 : resolvedStatus.cover) == null ? void 0 : _a2.url) ?? null,
5186
- onPlayerInfo
5541
+ posterUrl: effectivePoster,
5542
+ onPlayerInfo,
5543
+ onFallback: setIsHlsFallback
5187
5544
  }
5188
5545
  );
5189
5546
  default:
@@ -5323,21 +5680,6 @@ async function handleResponse(response) {
5323
5680
  }
5324
5681
  return parseJson(response);
5325
5682
  }
5326
- async function listChaptersEffective(videoId, options = {}) {
5327
- const url = buildApiUrl(
5328
- `/v1/videos/${videoId}/chapters/effective`,
5329
- options.baseUrl
5330
- );
5331
- const response = await fetch(url, {
5332
- method: "GET",
5333
- credentials: "include",
5334
- headers: {
5335
- ...getAuthHeaders(options.authToken)
5336
- }
5337
- });
5338
- const data = await handleResponse(response);
5339
- return Array.isArray(data) ? data : [];
5340
- }
5341
5683
  async function saveChapters(videoId, chapters, options = {}) {
5342
5684
  const url = buildApiUrl(`/v1/videos/${videoId}/chapters`, options.baseUrl);
5343
5685
  const response = await fetch(url, {
@@ -5465,9 +5807,7 @@ function ChapterRow({
5465
5807
  /* @__PURE__ */ jsxs(
5466
5808
  Stack,
5467
5809
  {
5468
- gap: 10,
5469
5810
  className: "video-settings-modal-chapter-times",
5470
- style: { width: 72, flex: "0 0 72px" },
5471
5811
  children: [
5472
5812
  /* @__PURE__ */ jsx(
5473
5813
  TextInput,
@@ -5527,9 +5867,7 @@ function ChapterRow({
5527
5867
  /* @__PURE__ */ jsx(
5528
5868
  Flex,
5529
5869
  {
5530
- align: "center",
5531
5870
  className: "video-settings-modal-chapter-title",
5532
- style: { flex: 1, minWidth: 0, height: 82 },
5533
5871
  children: /* @__PURE__ */ jsx(
5534
5872
  Textarea,
5535
5873
  {
@@ -5566,10 +5904,7 @@ function ChapterRow({
5566
5904
  /* @__PURE__ */ jsx(
5567
5905
  Flex,
5568
5906
  {
5569
- align: "center",
5570
- justify: "center",
5571
5907
  className: "video-settings-modal-chapter-actions",
5572
- style: { width: 36, flex: "0 0 36px", height: 82 },
5573
5908
  children: /* @__PURE__ */ jsxs(Menu, { position: "bottom-end", children: [
5574
5909
  /* @__PURE__ */ jsx(Menu.Target, { children: /* @__PURE__ */ jsx(
5575
5910
  ActionIcon,
@@ -5595,6 +5930,8 @@ function ChaptersSection({
5595
5930
  videoId,
5596
5931
  initialChapters,
5597
5932
  nativeVideo,
5933
+ shakaPlayer,
5934
+ videoDuration: videoDurationProp,
5598
5935
  baseUrl,
5599
5936
  authToken,
5600
5937
  title: title2,
@@ -5614,7 +5951,7 @@ function ChaptersSection({
5614
5951
  addTranscriptManuallyLabel
5615
5952
  }) {
5616
5953
  const dependencies = useVideoPluginDependencies();
5617
- const SelectComponent = dependencies.Select || Select;
5954
+ const SelectComponent = dependencies.Select || VideoSelect;
5618
5955
  const [editableChapters, setEditableChapters] = useState(
5619
5956
  []
5620
5957
  );
@@ -5639,50 +5976,32 @@ function ChaptersSection({
5639
5976
  setEditableChapters(toEditable(list));
5640
5977
  }, []);
5641
5978
  useEffect(() => {
5642
- if (!nativeVideo) {
5643
- setDurationSec(null);
5979
+ var _a;
5980
+ if (videoDurationProp != null && videoDurationProp > 0) {
5981
+ setDurationSec(videoDurationProp);
5644
5982
  return;
5645
5983
  }
5646
- const updateDuration = () => {
5647
- const d = nativeVideo.duration;
5648
- setDurationSec(Number.isFinite(d) && d > 0 ? d : null);
5649
- };
5650
- updateDuration();
5651
- nativeVideo.addEventListener("loadedmetadata", updateDuration);
5652
- nativeVideo.addEventListener("durationchange", updateDuration);
5653
- return () => {
5654
- nativeVideo.removeEventListener("loadedmetadata", updateDuration);
5655
- nativeVideo.removeEventListener("durationchange", updateDuration);
5656
- };
5657
- }, [nativeVideo]);
5658
- const loadEffective = useCallback(async () => {
5659
- if (!videoId) return;
5660
- setIsLoading(true);
5661
- setError(null);
5662
- setValidationError(null);
5663
5984
  try {
5664
- const next = await listChaptersEffective(videoId, apiOptions);
5665
- applyChapters(next);
5666
- } catch (err) {
5667
- setError(err instanceof Error ? err.message : errorLabel);
5668
- } finally {
5669
- setIsLoading(false);
5985
+ const range = (_a = shakaPlayer == null ? void 0 : shakaPlayer.seekRange) == null ? void 0 : _a.call(shakaPlayer);
5986
+ if (range && Number.isFinite(range.end) && range.end > 0) {
5987
+ setDurationSec(range.end);
5988
+ return;
5989
+ }
5990
+ } catch {
5670
5991
  }
5671
- }, [videoId, apiOptions, applyChapters, errorLabel]);
5672
- const reliableDurationSec = useMemo(() => {
5673
- if (durationSec == null) return null;
5674
- const maxChapterStart = editableChapters.reduce(
5675
- (max, ch) => Math.max(max, ch.startSec),
5676
- 0
5677
- );
5678
- if (maxChapterStart > durationSec) return null;
5679
- return durationSec;
5680
- }, [durationSec, editableChapters]);
5992
+ if (nativeVideo) {
5993
+ const d = nativeVideo.duration;
5994
+ if (Number.isFinite(d) && d > 0) {
5995
+ setDurationSec(d);
5996
+ return;
5997
+ }
5998
+ }
5999
+ }, [videoDurationProp, shakaPlayer, nativeVideo]);
5681
6000
  const getMaxAllowedSec = useCallback(() => {
5682
- if (reliableDurationSec == null) return null;
5683
- return Math.floor(reliableDurationSec);
5684
- }, [reliableDurationSec]);
5685
- const validateChapters = useCallback(() => {
6001
+ if (durationSec == null) return null;
6002
+ return Math.floor(durationSec);
6003
+ }, [durationSec]);
6004
+ useCallback(() => {
5686
6005
  const list = [...editableChapters].filter((ch) => ch.title.trim()).sort((a, b) => a.startSec - b.startSec);
5687
6006
  const maxAllowed = getMaxAllowedSec();
5688
6007
  for (let i = 0; i < list.length; i++) {
@@ -5700,21 +6019,19 @@ function ChaptersSection({
5700
6019
  const handleSave = useCallback(async () => {
5701
6020
  if (!videoId) return null;
5702
6021
  setValidationError(null);
5703
- if (!validateChapters()) {
5704
- setValidationError(invalidRangeLabel);
5705
- return null;
5706
- }
5707
6022
  setIsSaving(true);
5708
6023
  isSavingRef.current = true;
5709
6024
  setError(null);
5710
6025
  try {
5711
- const payload = editableChapters.filter((ch) => ch.title.trim()).sort((a, b) => a.startSec - b.startSec).map(({ startSec, title: title22 }) => ({ startSec, title: title22 }));
6026
+ const payload = editableChapters.filter((ch) => ch.title.trim()).filter((ch) => durationSec == null || ch.startSec < durationSec).sort((a, b) => a.startSec - b.startSec).map(({ startSec, title: title22 }) => ({ startSec, title: title22 }));
5712
6027
  await saveChapters(videoId, payload, apiOptions);
5713
6028
  hasLocalEditsRef.current = false;
5714
- await loadEffective();
5715
6029
  return payload;
5716
6030
  } catch (err) {
5717
- setError(err instanceof Error ? err.message : errorLabel);
6031
+ const msg = err instanceof Error ? err.message : errorLabel;
6032
+ if (!/exceeds/i.test(msg)) {
6033
+ setError(msg);
6034
+ }
5718
6035
  return null;
5719
6036
  } finally {
5720
6037
  setIsSaving(false);
@@ -5724,10 +6041,8 @@ function ChaptersSection({
5724
6041
  videoId,
5725
6042
  editableChapters,
5726
6043
  apiOptions,
5727
- loadEffective,
5728
6044
  errorLabel,
5729
- validateChapters,
5730
- invalidRangeLabel
6045
+ getMaxAllowedSec
5731
6046
  ]);
5732
6047
  useEffect(() => {
5733
6048
  onSave == null ? void 0 : onSave(handleSave);
@@ -5736,7 +6051,7 @@ function ChaptersSection({
5736
6051
  hasLocalEditsRef.current = true;
5737
6052
  setValidationError(null);
5738
6053
  setEditableChapters((prev) => {
5739
- const maxAllowed = reliableDurationSec != null ? Math.floor(reliableDurationSec) : null;
6054
+ const maxAllowed = durationSec != null ? Math.floor(durationSec) : null;
5740
6055
  const sorted = [...prev].sort((a, b) => a.startSec - b.startSec);
5741
6056
  const lastStart = sorted.length > 0 ? Math.floor(sorted[sorted.length - 1].startSec) : -1;
5742
6057
  const nextStart = Math.max(0, lastStart + 1);
@@ -5807,11 +6122,8 @@ function ChaptersSection({
5807
6122
  }
5808
6123
  if (Array.isArray(initialChapters)) {
5809
6124
  applyChapters(initialChapters);
5810
- return;
5811
6125
  }
5812
- loadEffective().catch(() => {
5813
- });
5814
- }, [videoId, initialChapters, applyChapters, loadEffective]);
6126
+ }, [videoId, initialChapters, applyChapters]);
5815
6127
  return /* @__PURE__ */ jsxs(Stack, { gap: "xs", children: [
5816
6128
  /* @__PURE__ */ jsx(
5817
6129
  Text,
@@ -5851,7 +6163,7 @@ function ChaptersSection({
5851
6163
  /* @__PURE__ */ jsx(Stack, { gap: 12, children: [...editableChapters].sort((a, b) => a.startSec - b.startSec).map((chapter, index, list) => {
5852
6164
  const next = list[index + 1] || null;
5853
6165
  const nextNext = list[index + 2] || null;
5854
- const maxAllowed = reliableDurationSec != null ? Math.floor(reliableDurationSec) : null;
6166
+ const maxAllowed = durationSec != null ? Math.floor(durationSec) : null;
5855
6167
  const endSec = next != null ? Math.floor(next.startSec) : maxAllowed != null ? maxAllowed : null;
5856
6168
  const startSec = Math.floor(chapter.startSec);
5857
6169
  const prevStart = index > 0 ? Math.floor(list[index - 1].startSec) : null;
@@ -5879,7 +6191,7 @@ function ChaptersSection({
5879
6191
  endMax,
5880
6192
  disabled: false,
5881
6193
  startTimePlaceholder: timePlaceholder,
5882
- endTimePlaceholder: reliableDurationSec != null ? secondsToTimestamp(Math.floor(reliableDurationSec)) : endTimePlaceholder,
6194
+ endTimePlaceholder: durationSec != null ? secondsToTimestamp(Math.floor(durationSec)) : endTimePlaceholder,
5883
6195
  titlePlaceholder,
5884
6196
  deleteChapterLabel,
5885
6197
  onStartSecChange: handleStartSecChange,
@@ -5952,7 +6264,7 @@ function CoverSection({
5952
6264
  [(_a = status == null ? void 0 : status.cover) == null ? void 0 : _a.previewId, previews]
5953
6265
  );
5954
6266
  const effectiveSelected = selectedPreviewId ?? coverId;
5955
- const hasCoverCandidates = previews.length > 0;
6267
+ previews.length > 0;
5956
6268
  const slidesCount = previews.length + 1;
5957
6269
  const showControls = slidesCount > 3;
5958
6270
  const setPreviewLoaded = useCallback((id) => {
@@ -6000,7 +6312,7 @@ function CoverSection({
6000
6312
  }, [videoId]);
6001
6313
  const handleCoverDrop = useCallback(
6002
6314
  async (files) => {
6003
- var _a2, _b2, _c;
6315
+ var _a2, _b2, _c, _d;
6004
6316
  const file = files[0];
6005
6317
  if (!file || !onCoverUpload) return;
6006
6318
  const objectUrl = URL.createObjectURL(file);
@@ -6010,7 +6322,6 @@ function CoverSection({
6010
6322
  onSelectedPosterChange == null ? void 0 : onSelectedPosterChange(objectUrl);
6011
6323
  try {
6012
6324
  const nextStatus = await Promise.resolve(onCoverUpload(file));
6013
- console.log("cover upload response", nextStatus);
6014
6325
  setOptimisticPreviewUrl(null);
6015
6326
  if (objectUrl) URL.revokeObjectURL(objectUrl);
6016
6327
  const nextPreviews = nextStatus == null ? void 0 : nextStatus.previews;
@@ -6018,10 +6329,16 @@ function CoverSection({
6018
6329
  if (nextStatus && hasPreviews && nextPreviews) {
6019
6330
  scrollToEndAfterUpdateRef.current = true;
6020
6331
  setStatus(nextStatus);
6021
- const fallbackId = ((_a2 = nextPreviews[0]) == null ? void 0 : _a2.id) ?? ((_b2 = nextStatus == null ? void 0 : nextStatus.cover) == null ? void 0 : _b2.previewId);
6022
- setSelectedPreviewId(
6023
- ((_c = nextStatus == null ? void 0 : nextStatus.cover) == null ? void 0 : _c.previewId) ?? fallbackId ?? "uploaded"
6024
- );
6332
+ const uploadedPreviewId = ((_a2 = nextPreviews[nextPreviews.length - 1]) == null ? void 0 : _a2.id) ?? ((_b2 = nextStatus == null ? void 0 : nextStatus.cover) == null ? void 0 : _b2.previewId) ?? ((_c = nextPreviews[0]) == null ? void 0 : _c.id);
6333
+ setSelectedPreviewId(uploadedPreviewId ?? "uploaded");
6334
+ if (uploadedPreviewId) {
6335
+ const uploadedUrl = (_d = nextPreviews.find(
6336
+ (p) => p.id === uploadedPreviewId
6337
+ )) == null ? void 0 : _d.url;
6338
+ if (uploadedUrl) {
6339
+ onSelectedPosterChange == null ? void 0 : onSelectedPosterChange(uploadedUrl);
6340
+ }
6341
+ }
6025
6342
  setLoadingIds(/* @__PURE__ */ new Set());
6026
6343
  setErrorIds(/* @__PURE__ */ new Set());
6027
6344
  } else {
@@ -6038,7 +6355,7 @@ function CoverSection({
6038
6355
  loadStatus();
6039
6356
  }
6040
6357
  },
6041
- [onCoverUpload, loadStatus]
6358
+ [onCoverUpload, loadStatus, onSelectedPosterChange]
6042
6359
  );
6043
6360
  const selectedUrl = useMemo(() => {
6044
6361
  var _a2, _b2;
@@ -6159,6 +6476,7 @@ function CoverSection({
6159
6476
  className: `video-settings-modal-cover-cell video-settings-modal-cover-thumb ${isSelected && !isError ? "selected" : ""} ${isLoading2 ? "loading" : ""} ${isError ? "error" : ""}`,
6160
6477
  onClick: () => {
6161
6478
  if (isError) return;
6479
+ console.log("[Cover] click preview:", preview.id, preview.url);
6162
6480
  setSelectedPreviewId(preview.id);
6163
6481
  onSelectedPosterChange == null ? void 0 : onSelectedPosterChange(preview.url);
6164
6482
  },
@@ -6208,15 +6526,15 @@ function CoverSection({
6208
6526
  })
6209
6527
  ]
6210
6528
  }
6211
- ),
6212
- !hasCoverCandidates && !isLoading && !loadError && /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: emptyCoverLabel })
6529
+ )
6213
6530
  ] });
6214
6531
  }
6215
6532
  function FooterActions({
6216
6533
  cancelLabel,
6217
6534
  saveLabel,
6218
6535
  onCancel,
6219
- onSave
6536
+ onSave,
6537
+ isSaving
6220
6538
  }) {
6221
6539
  return /* @__PURE__ */ jsxs(Group, { justify: "flex-end", gap: 8, children: [
6222
6540
  /* @__PURE__ */ jsx(
@@ -6227,6 +6545,7 @@ function FooterActions({
6227
6545
  color: "gray",
6228
6546
  radius: "md",
6229
6547
  onClick: onCancel,
6548
+ disabled: isSaving,
6230
6549
  children: cancelLabel
6231
6550
  }
6232
6551
  ),
@@ -6234,9 +6553,9 @@ function FooterActions({
6234
6553
  Button,
6235
6554
  {
6236
6555
  size: "sm",
6237
- color: "blue",
6238
6556
  radius: "md",
6239
6557
  onClick: onSave,
6558
+ loading: isSaving,
6240
6559
  children: saveLabel
6241
6560
  }
6242
6561
  )
@@ -6276,23 +6595,29 @@ const LANGUAGE_LABELS = LANGUAGE_OPTIONS.reduce(
6276
6595
  );
6277
6596
  function ManualSubtitlesPanel({
6278
6597
  videoId,
6279
- mode,
6280
- shakaPlayer,
6281
- nativeVideo,
6282
6598
  baseUrl,
6283
6599
  authToken,
6284
6600
  statusSubtitles,
6285
- onTracksChange
6601
+ onTracksChange,
6602
+ onSave
6286
6603
  }) {
6287
6604
  const { t } = useTranslation();
6605
+ const { reportError } = useVideoPluginContext();
6288
6606
  const dependencies = useVideoPluginDependencies();
6289
- const SelectComponent = dependencies.Select || Select;
6607
+ const SelectComponent = dependencies.Select || VideoSelect;
6290
6608
  const [tracks, setTracks] = useState([]);
6291
6609
  const [drafts, setDrafts] = useState([]);
6292
- const [loadError, setLoadError] = useState(null);
6293
6610
  const uploadControllersRef = useRef(/* @__PURE__ */ new Map());
6294
- const shakaAddedIdsRef = useRef([]);
6295
- const shakaFilterRegisteredRef = useRef(false);
6611
+ const [autoSubtitlesDisabled, setAutoSubtitlesDisabled] = useState(false);
6612
+ const autoTrack = useMemo(
6613
+ () => tracks.find(isAutoGeneratedTrack) ?? null,
6614
+ [tracks]
6615
+ );
6616
+ useEffect(() => {
6617
+ if (autoTrack) {
6618
+ setAutoSubtitlesDisabled(autoTrack.isDisabled);
6619
+ }
6620
+ }, [autoTrack]);
6296
6621
  const apiOptions = useMemo(
6297
6622
  () => ({
6298
6623
  baseUrl,
@@ -6300,26 +6625,54 @@ function ManualSubtitlesPanel({
6300
6625
  }),
6301
6626
  [baseUrl, authToken]
6302
6627
  );
6628
+ const handleAutoSubtitlesToggle = useCallback(
6629
+ (disabled2) => {
6630
+ if (!autoTrack) return;
6631
+ setAutoSubtitlesDisabled(disabled2);
6632
+ setTracks(
6633
+ (prev) => prev.map(
6634
+ (t2) => t2.id === autoTrack.id ? { ...t2, isDisabled: disabled2 } : t2
6635
+ )
6636
+ );
6637
+ },
6638
+ [autoTrack]
6639
+ );
6640
+ const initialAutoDisabledRef = useRef(null);
6641
+ useEffect(() => {
6642
+ if (autoTrack && initialAutoDisabledRef.current === null) {
6643
+ initialAutoDisabledRef.current = autoTrack.isDisabled;
6644
+ }
6645
+ }, [autoTrack]);
6303
6646
  const refreshTracks = useCallback(
6304
6647
  async (forceApi = false) => {
6305
6648
  if (!videoId) return;
6306
- if (!forceApi && statusSubtitles != null) {
6307
- const nextTracks = (statusSubtitles || []).map(normalizeSubtitleTrack).filter((track) => track.id);
6308
- setTracks(nextTracks);
6309
- setLoadError(null);
6310
- return;
6649
+ if (forceApi || statusSubtitles == null) {
6650
+ try {
6651
+ const freshTracks = await listTracks(videoId, apiOptions);
6652
+ setTracks(freshTracks);
6653
+ return;
6654
+ } catch {
6655
+ }
6311
6656
  }
6312
6657
  try {
6313
- const status = await fetchStatus(videoId);
6314
- const nextTracks = ((status == null ? void 0 : status.subtitles) || []).map(normalizeSubtitleTrack).filter((track) => track.id);
6315
- setTracks(nextTracks);
6316
- setLoadError(null);
6317
- } catch (error) {
6318
- const message = error instanceof Error ? error.message : t("editor.video.errors.uploadFailed");
6319
- setLoadError(message);
6658
+ const freshTracks = await listTracks(videoId, apiOptions);
6659
+ setTracks(freshTracks);
6660
+ } catch {
6661
+ if (statusSubtitles != null) {
6662
+ const nextTracks = (statusSubtitles || []).map(normalizeSubtitleTrack).filter((track) => track.id);
6663
+ setTracks(nextTracks);
6664
+ } else {
6665
+ try {
6666
+ const status = await fetchStatus(videoId);
6667
+ const nextTracks = ((status == null ? void 0 : status.subtitles) || []).map(normalizeSubtitleTrack).filter((track) => track.id);
6668
+ setTracks(nextTracks);
6669
+ } catch (error) {
6670
+ reportError(error, "subtitles", videoId);
6671
+ }
6672
+ }
6320
6673
  }
6321
6674
  },
6322
- [videoId, t, statusSubtitles]
6675
+ [videoId, apiOptions, reportError, statusSubtitles]
6323
6676
  );
6324
6677
  useEffect(() => {
6325
6678
  if (!videoId) {
@@ -6333,18 +6686,44 @@ function ManualSubtitlesPanel({
6333
6686
  useEffect(() => {
6334
6687
  onTracksChange == null ? void 0 : onTracksChange(tracks);
6335
6688
  }, [tracks, onTracksChange]);
6689
+ useEffect(() => {
6690
+ onSave == null ? void 0 : onSave(async () => {
6691
+ if (!videoId || !autoTrack) return;
6692
+ if (autoSubtitlesDisabled !== initialAutoDisabledRef.current) {
6693
+ await patchTrackDisabled(videoId, autoTrack.id, autoSubtitlesDisabled, apiOptions);
6694
+ initialAutoDisabledRef.current = autoSubtitlesDisabled;
6695
+ }
6696
+ });
6697
+ }, [onSave, videoId, autoTrack, autoSubtitlesDisabled, apiOptions]);
6698
+ const manualTracks = useMemo(() => {
6699
+ const manual = tracks.filter((track) => !isAutoGeneratedTrack(track));
6700
+ const byLang = /* @__PURE__ */ new Map();
6701
+ for (const track of manual) {
6702
+ byLang.set(track.lang, track);
6703
+ }
6704
+ return Array.from(byLang.values());
6705
+ }, [tracks]);
6706
+ const usedLanguages = useMemo(() => {
6707
+ const langs = new Set(manualTracks.map((t2) => t2.lang));
6708
+ drafts.forEach((d) => {
6709
+ if (d.lang) langs.add(d.lang);
6710
+ });
6711
+ return langs;
6712
+ }, [manualTracks, drafts]);
6336
6713
  const handleAddDraft = useCallback(() => {
6337
- const defaultLabel = LANGUAGE_LABELS[DEFAULT_LANG] || DEFAULT_LANG;
6714
+ var _a;
6715
+ const availableLang = ((_a = LANGUAGE_OPTIONS.find((opt) => !usedLanguages.has(opt.value))) == null ? void 0 : _a.value) ?? (usedLanguages.has(DEFAULT_LANG) ? null : DEFAULT_LANG);
6716
+ const label = availableLang ? LANGUAGE_LABELS[availableLang] || availableLang : null;
6338
6717
  setDrafts((prev) => [
6339
6718
  ...prev,
6340
6719
  {
6341
6720
  id: crypto.randomUUID(),
6342
- lang: DEFAULT_LANG,
6343
- label: defaultLabel,
6721
+ lang: availableLang,
6722
+ label,
6344
6723
  status: "draft"
6345
6724
  }
6346
6725
  ]);
6347
- }, []);
6726
+ }, [usedLanguages]);
6348
6727
  const updateDraft = useCallback(
6349
6728
  (id, updates) => {
6350
6729
  setDrafts(
@@ -6388,12 +6767,13 @@ function ManualSubtitlesPanel({
6388
6767
  } else {
6389
6768
  await refreshTracks(true);
6390
6769
  }
6391
- } catch {
6770
+ } catch (error) {
6392
6771
  uploadControllersRef.current.delete(draftId);
6393
6772
  updateDraft(draftId, { status: "error" });
6773
+ reportError(error, "subtitles", videoId);
6394
6774
  }
6395
6775
  },
6396
- [videoId, drafts, apiOptions, refreshTracks, updateDraft]
6776
+ [videoId, drafts, apiOptions, refreshTracks, updateDraft, reportError]
6397
6777
  );
6398
6778
  const handleDelete = useCallback(
6399
6779
  async (trackId) => {
@@ -6403,96 +6783,38 @@ function ManualSubtitlesPanel({
6403
6783
  },
6404
6784
  [videoId, apiOptions]
6405
6785
  );
6406
- useEffect(() => {
6407
- if (mode !== "native" || !nativeVideo) return;
6408
- const existingTracks = nativeVideo.querySelectorAll(
6409
- 'track[data-subtitle-track="managed"]'
6410
- );
6411
- existingTracks.forEach((node) => node.remove());
6412
- tracks.forEach((track) => {
6413
- const element = document.createElement("track");
6414
- element.setAttribute("data-subtitle-track", "managed");
6415
- element.kind = track.kind || "subtitles";
6416
- element.label = track.label;
6417
- element.srclang = track.lang;
6418
- element.src = resolveSubtitleUrl(videoId || "", track, apiOptions);
6419
- element.default = track.isDefault;
6420
- nativeVideo.appendChild(element);
6421
- });
6422
- }, [mode, nativeVideo, tracks, videoId, apiOptions]);
6423
- useEffect(() => {
6424
- var _a;
6425
- if (mode !== "shaka" || !shakaPlayer) return;
6426
- const engine = (_a = shakaPlayer.getNetworkingEngine) == null ? void 0 : _a.call(shakaPlayer);
6427
- if (engine && !shakaFilterRegisteredRef.current) {
6428
- engine.registerRequestFilter((type, request) => {
6429
- request.allowCrossSiteCredentials = true;
6430
- if (authToken) {
6431
- request.headers = request.headers || {};
6432
- request.headers.Authorization = `Bearer ${authToken}`;
6433
- }
6434
- });
6435
- shakaFilterRegisteredRef.current = true;
6436
- }
6437
- if (shakaPlayer.getTextTracks && shakaPlayer.removeTextTrack) {
6438
- const existing = shakaPlayer.getTextTracks();
6439
- existing.forEach((track) => {
6440
- if (shakaAddedIdsRef.current.includes(track.id)) {
6441
- shakaPlayer.removeTextTrack(track);
6442
- }
6443
- });
6444
- shakaAddedIdsRef.current = [];
6445
- }
6446
- const addTracks = async () => {
6447
- var _a2, _b, _c;
6448
- const existingTracks = ((_a2 = shakaPlayer.getTextTracks) == null ? void 0 : _a2.call(shakaPlayer)) || [];
6449
- for (const track of tracks) {
6450
- const vttUrl = resolveSubtitleUrl(videoId || "", track, apiOptions);
6451
- const label = track.label || track.lang || "und";
6452
- const lang = track.lang || "und";
6453
- const kind = track.kind || "subtitles";
6454
- const alreadyExists = existingTracks.some(
6455
- (item) => (item.language || "") === lang && (item.label || "") === label && (item.kind || "subtitles") === kind
6456
- );
6457
- if (alreadyExists) {
6458
- continue;
6459
- }
6460
- let added = null;
6461
- if (typeof shakaPlayer.addTextTrackAsync === "function") {
6462
- added = await shakaPlayer.addTextTrackAsync(
6463
- vttUrl,
6464
- lang,
6465
- kind,
6466
- label
6467
- );
6468
- } else if (typeof shakaPlayer.addTextTrack === "function") {
6469
- added = shakaPlayer.addTextTrack(vttUrl, lang, kind, label);
6470
- }
6471
- if (added && typeof added.id === "number") {
6472
- shakaAddedIdsRef.current.push(added.id);
6473
- existingTracks.push(added);
6474
- }
6475
- }
6476
- const defaultTrack = tracks.find((item) => item.isDefault);
6477
- if (defaultTrack && shakaPlayer.selectTextTrack) {
6478
- const list = ((_b = shakaPlayer.getTextTracks) == null ? void 0 : _b.call(shakaPlayer)) || [];
6479
- const target = list.find(
6480
- (item) => item.language === defaultTrack.lang && item.label === defaultTrack.label
6481
- );
6482
- if (target) {
6483
- (_c = shakaPlayer.setTextTrackVisibility) == null ? void 0 : _c.call(shakaPlayer, true);
6484
- shakaPlayer.selectTextTrack(target);
6485
- }
6486
- }
6487
- };
6488
- addTracks().catch(() => {
6489
- });
6490
- }, [mode, shakaPlayer, tracks, videoId, apiOptions, authToken]);
6491
6786
  const handleRemoveDraft = useCallback((draftId) => {
6492
6787
  setDrafts((prev) => prev.filter((item) => item.id !== draftId));
6493
6788
  }, []);
6494
6789
  return /* @__PURE__ */ jsxs("div", { className: "video-settings-modal-subtitles", children: [
6495
- loadError && /* @__PURE__ */ jsx(Text, { size: "sm", c: "red", mb: "xs", children: loadError }),
6790
+ autoTrack && /* @__PURE__ */ jsxs(
6791
+ Flex,
6792
+ {
6793
+ align: "center",
6794
+ gap: "md",
6795
+ className: "video-settings-modal-subtitles-auto-toggle",
6796
+ children: [
6797
+ /* @__PURE__ */ jsx(
6798
+ Text,
6799
+ {
6800
+ size: "sm",
6801
+ fw: 500,
6802
+ c: "var(--mantine-color-bright)",
6803
+ flex: 1,
6804
+ children: t("editor.video.settingsModal.hideAutoSubtitles")
6805
+ }
6806
+ ),
6807
+ /* @__PURE__ */ jsx(
6808
+ Switch,
6809
+ {
6810
+ size: "sm",
6811
+ checked: autoSubtitlesDisabled,
6812
+ onChange: (event) => handleAutoSubtitlesToggle(event.currentTarget.checked)
6813
+ }
6814
+ )
6815
+ ]
6816
+ }
6817
+ ),
6496
6818
  /* @__PURE__ */ jsx(
6497
6819
  Text,
6498
6820
  {
@@ -6502,7 +6824,7 @@ function ManualSubtitlesPanel({
6502
6824
  children: t("editor.video.settingsModal.uploadSubtitlesManually")
6503
6825
  }
6504
6826
  ),
6505
- tracks.filter((track) => !track.isDefault).map((track) => /* @__PURE__ */ jsxs(
6827
+ manualTracks.map((track) => /* @__PURE__ */ jsxs(
6506
6828
  Flex,
6507
6829
  {
6508
6830
  align: "center",
@@ -6555,6 +6877,7 @@ function ManualSubtitlesPanel({
6555
6877
  DraftRow,
6556
6878
  {
6557
6879
  draft,
6880
+ usedLanguages,
6558
6881
  onChange: (updates) => updateDraft(draft.id, updates),
6559
6882
  onUpload: (file) => handleUploadDraft(draft.id, file),
6560
6883
  onRemove: () => handleRemoveDraft(draft.id),
@@ -6574,9 +6897,15 @@ function ManualSubtitlesPanel({
6574
6897
  )
6575
6898
  ] });
6576
6899
  }
6577
- function DraftRow({ draft, onChange, onUpload, onRemove, SelectComponent }) {
6900
+ function DraftRow({ draft, usedLanguages, onChange, onUpload, onRemove, SelectComponent }) {
6578
6901
  const { t } = useTranslation();
6579
6902
  const inputRef = useRef(null);
6903
+ const availableLanguages = useMemo(
6904
+ () => LANGUAGE_OPTIONS.filter(
6905
+ (opt) => opt.value === draft.lang || !usedLanguages.has(opt.value)
6906
+ ),
6907
+ [usedLanguages, draft.lang]
6908
+ );
6580
6909
  const handleFileChange = (e) => {
6581
6910
  var _a;
6582
6911
  const file = (_a = e.target.files) == null ? void 0 : _a[0];
@@ -6588,7 +6917,6 @@ function DraftRow({ draft, onChange, onUpload, onRemove, SelectComponent }) {
6588
6917
  Flex,
6589
6918
  {
6590
6919
  align: "center",
6591
- gap: "md",
6592
6920
  wrap: "nowrap",
6593
6921
  className: "video-settings-modal-subtitles-row",
6594
6922
  children: [
@@ -6597,7 +6925,7 @@ function DraftRow({ draft, onChange, onUpload, onRemove, SelectComponent }) {
6597
6925
  {
6598
6926
  size: "sm",
6599
6927
  placeholder: t("editor.video.settingsModal.selectLanguage"),
6600
- data: LANGUAGE_OPTIONS,
6928
+ data: availableLanguages,
6601
6929
  value: draft.lang,
6602
6930
  onChange: (value) => onChange({
6603
6931
  lang: value,
@@ -6610,7 +6938,7 @@ function DraftRow({ draft, onChange, onUpload, onRemove, SelectComponent }) {
6610
6938
  inputClassName: "video-settings-modal-language-select"
6611
6939
  }
6612
6940
  ),
6613
- /* @__PURE__ */ jsx(Flex, { align: "center", gap: 6, flex: 1, children: draft.status === "uploading" ? /* @__PURE__ */ jsx(Loader, { size: 22, color: "blue" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
6941
+ /* @__PURE__ */ jsx(Flex, { align: "center", gap: 6, flex: 1, children: draft.status === "uploading" ? /* @__PURE__ */ jsx(Loader, { size: 22 }) : /* @__PURE__ */ jsxs(Fragment, { children: [
6614
6942
  /* @__PURE__ */ jsx(
6615
6943
  "input",
6616
6944
  {
@@ -6668,6 +6996,7 @@ function VideoSettingsModal({
6668
6996
  chaptersDisabled = false,
6669
6997
  shakaPlayer,
6670
6998
  nativeVideo,
6999
+ videoDuration,
6671
7000
  playerMode = "native"
6672
7001
  }) {
6673
7002
  const { t } = useTranslation();
@@ -6682,17 +7011,24 @@ function VideoSettingsModal({
6682
7011
  const saveChaptersRef = useRef(
6683
7012
  null
6684
7013
  );
7014
+ const saveSubtitlesRef = useRef(null);
7015
+ const [isSaving, setIsSaving] = useState(false);
6685
7016
  const handlePosterSelect = useCallback((url) => {
7017
+ console.log("[Cover] handlePosterSelect:", url);
6686
7018
  setSelectedPosterUrl(url);
6687
7019
  }, []);
6688
7020
  useEffect(() => {
6689
- if (!opened || !videoId) {
6690
- setStatus(null);
6691
- return;
7021
+ if (initialStatus) {
7022
+ setStatus(initialStatus);
6692
7023
  }
6693
- fetchStatus(videoId).then((nextStatus) => {
6694
- setStatus(nextStatus);
6695
- onStatusChange == null ? void 0 : onStatusChange(nextStatus);
7024
+ }, [initialStatus]);
7025
+ useEffect(() => {
7026
+ if (!opened || !videoId) return;
7027
+ fetchStatus(videoId).then((freshStatus) => {
7028
+ if (freshStatus) {
7029
+ setStatus(freshStatus);
7030
+ onStatusChange == null ? void 0 : onStatusChange(freshStatus);
7031
+ }
6696
7032
  }).catch(() => {
6697
7033
  });
6698
7034
  }, [opened, videoId]);
@@ -6708,28 +7044,47 @@ function VideoSettingsModal({
6708
7044
  [videoId]
6709
7045
  );
6710
7046
  const handleSave = useCallback(async () => {
6711
- if (saveChaptersRef.current) {
6712
- const payload = await saveChaptersRef.current();
6713
- if (!Array.isArray(payload)) return;
6714
- onChaptersSaved == null ? void 0 : onChaptersSaved(payload);
6715
- }
6716
- onChaptersDisabledChange == null ? void 0 : onChaptersDisabledChange(chaptersDisabledValue);
6717
- onSubtitlesSaved == null ? void 0 : onSubtitlesSaved(subtitlesSnapshot);
6718
- if (selectedPosterUrl !== null) {
6719
- onPosterChange == null ? void 0 : onPosterChange(selectedPosterUrl);
7047
+ setIsSaving(true);
7048
+ try {
7049
+ if (saveChaptersRef.current) {
7050
+ const payload = await saveChaptersRef.current();
7051
+ if (Array.isArray(payload)) {
7052
+ onChaptersSaved == null ? void 0 : onChaptersSaved(payload);
7053
+ }
7054
+ }
7055
+ onChaptersDisabledChange == null ? void 0 : onChaptersDisabledChange(chaptersDisabledValue);
7056
+ if (saveSubtitlesRef.current) {
7057
+ await saveSubtitlesRef.current();
7058
+ }
7059
+ onSubtitlesSaved == null ? void 0 : onSubtitlesSaved(subtitlesSnapshot);
7060
+ console.log("[Cover] handleSave: selectedPosterUrl=", selectedPosterUrl, "onPosterChange=", !!onPosterChange);
7061
+ if (selectedPosterUrl !== null) {
7062
+ console.log("[Cover] calling onPosterChange with:", selectedPosterUrl);
7063
+ onPosterChange == null ? void 0 : onPosterChange(selectedPosterUrl);
7064
+ } else {
7065
+ console.log("[Cover] selectedPosterUrl is null, skipping onPosterChange");
7066
+ }
7067
+ if (videoId) {
7068
+ const updatedStatus = await fetchStatus(videoId);
7069
+ onStatusChange == null ? void 0 : onStatusChange(updatedStatus);
7070
+ }
7071
+ onClose();
7072
+ } finally {
7073
+ setIsSaving(false);
6720
7074
  }
6721
- onClose();
6722
7075
  }, [
7076
+ videoId,
6723
7077
  chaptersDisabledValue,
6724
7078
  onChaptersDisabledChange,
6725
7079
  onChaptersSaved,
6726
7080
  onClose,
6727
7081
  onPosterChange,
7082
+ onStatusChange,
6728
7083
  onSubtitlesSaved,
6729
7084
  selectedPosterUrl,
6730
7085
  subtitlesSnapshot
6731
7086
  ]);
6732
- return /* @__PURE__ */ jsx(
7087
+ return /* @__PURE__ */ jsxs(
6733
7088
  Modal,
6734
7089
  {
6735
7090
  opened,
@@ -6740,13 +7095,14 @@ function VideoSettingsModal({
6740
7095
  withinPortal: true,
6741
7096
  zIndex: 300,
6742
7097
  className: "video-settings-modal",
6743
- children: /* @__PURE__ */ jsx(Box, { className: "video-settings-modal-body", children: /* @__PURE__ */ jsxs(Stack, { gap: 24, children: [
6744
- /* @__PURE__ */ jsxs(Stack, { gap: 32, children: [
7098
+ children: [
7099
+ /* @__PURE__ */ jsx(Box, { className: "video-settings-modal-body", children: /* @__PURE__ */ jsxs(Stack, { gap: 32, children: [
6745
7100
  /* @__PURE__ */ jsx(
6746
7101
  CoverSection,
6747
7102
  {
6748
7103
  opened,
6749
7104
  videoId,
7105
+ initialStatus: status,
6750
7106
  title: t("editor.video.settingsModal.cover"),
6751
7107
  uploadLabel: t("editor.video.settingsModal.uploadFile"),
6752
7108
  cropLabel: t("editor.video.settingsModal.cropCoverImage"),
@@ -6785,7 +7141,10 @@ function VideoSettingsModal({
6785
7141
  shakaPlayer,
6786
7142
  nativeVideo,
6787
7143
  statusSubtitles: status == null ? void 0 : status.subtitles,
6788
- onTracksChange: setSubtitlesSnapshot
7144
+ onTracksChange: setSubtitlesSnapshot,
7145
+ onSave: (handler) => {
7146
+ saveSubtitlesRef.current = handler;
7147
+ }
6789
7148
  }
6790
7149
  )
6791
7150
  ] }),
@@ -6795,6 +7154,8 @@ function VideoSettingsModal({
6795
7154
  videoId,
6796
7155
  initialChapters: status == null ? void 0 : status.chapters,
6797
7156
  nativeVideo,
7157
+ shakaPlayer,
7158
+ videoDuration,
6798
7159
  title: t("editor.video.settingsModal.chapters"),
6799
7160
  customLabel: t("editor.video.settingsModal.chaptersCustom"),
6800
7161
  dontShowLabel: t("editor.video.settingsModal.chaptersDontShow"),
@@ -6824,17 +7185,18 @@ function VideoSettingsModal({
6824
7185
  )
6825
7186
  }
6826
7187
  )
6827
- ] }),
6828
- /* @__PURE__ */ jsx(
7188
+ ] }) }),
7189
+ /* @__PURE__ */ jsx(Box, { className: "video-settings-modal-footer", children: /* @__PURE__ */ jsx(
6829
7190
  FooterActions,
6830
7191
  {
6831
7192
  cancelLabel: t("editor.video.settingsModal.cancel"),
6832
7193
  saveLabel: t("editor.video.settingsModal.save"),
6833
7194
  onCancel: onClose,
6834
- onSave: handleSave
7195
+ onSave: handleSave,
7196
+ isSaving
6835
7197
  }
6836
- )
6837
- ] }) })
7198
+ ) })
7199
+ ]
6838
7200
  }
6839
7201
  );
6840
7202
  }
@@ -6875,7 +7237,8 @@ function VideoBlock({
6875
7237
  shakaPlayer: null,
6876
7238
  nativeVideo: null,
6877
7239
  mode: "native",
6878
- containerEl: null
7240
+ containerEl: null,
7241
+ videoDuration: null
6879
7242
  });
6880
7243
  const fallbackNativeVideoUrl = useMemo(() => {
6881
7244
  if (platform === "download" || platform === "link") {
@@ -6919,6 +7282,7 @@ function VideoBlock({
6919
7282
  };
6920
7283
  const handlePosterChange = useCallback(
6921
7284
  (posterUrl2) => {
7285
+ console.log("[Cover] handlePosterChange: setting posterUrl=", posterUrl2);
6922
7286
  setPosterUrl(posterUrl2);
6923
7287
  setVideoPoster(editor, nodeKey, posterUrl2);
6924
7288
  },
@@ -6947,7 +7311,7 @@ function VideoBlock({
6947
7311
  });
6948
7312
  }, [editor, nodeKey]);
6949
7313
  const resolvedChaptersOverride = chaptersDisabled ? [] : chaptersOverride;
6950
- const TrashIcon = /* @__PURE__ */ jsx(HugeiconsIcon, { icon: tI, size: 16, color: "var(--mantine-color-red-outline)" });
7314
+ const TrashIcon = /* @__PURE__ */ jsx("span", { style: { color: "var(--mantine-color-red-outline)", display: "flex", alignItems: "center" }, children: /* @__PURE__ */ jsx(HugeiconsIcon, { icon: tI, size: 16 }) });
6951
7315
  const uploadComponentProps = {
6952
7316
  className,
6953
7317
  format,
@@ -6986,7 +7350,7 @@ function VideoBlock({
6986
7350
  event.stopPropagation();
6987
7351
  setSettingsModalOpened(true);
6988
7352
  },
6989
- children: /* @__PURE__ */ jsx(SettingsIcon, { size: 16 })
7353
+ children: /* @__PURE__ */ jsx("span", { style: { color: "var(--mantine-color-gray-text)", display: "flex", alignItems: "center" }, children: /* @__PURE__ */ jsx(SettingsIcon, { size: 16 }) })
6990
7354
  }
6991
7355
  ) }),
6992
7356
  /* @__PURE__ */ jsx(
@@ -7005,7 +7369,8 @@ function VideoBlock({
7005
7369
  onChaptersDisabledChange: handleChaptersDisabledChange,
7006
7370
  shakaPlayer: playerInfo.shakaPlayer,
7007
7371
  nativeVideo: playerInfo.nativeVideo,
7008
- playerMode: playerInfo.mode
7372
+ playerMode: playerInfo.mode,
7373
+ videoDuration: playerInfo.videoDuration
7009
7374
  }
7010
7375
  )
7011
7376
  ] }),