@intellectif/lk-react 15.0.0 → 16.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +2 -2
  3. package/dist/{WrittenResponse-BPbhERgk.d.cts → WrittenResponse-C2zw-1jK.d.cts} +1 -1
  4. package/dist/{WrittenResponse-ZAxLR_AU.d.ts → WrittenResponse-CTiNpSCC.d.ts} +1 -1
  5. package/dist/{chunk-ZH5WT2IE.cjs → chunk-2WM2KSO7.cjs} +25 -25
  6. package/dist/{chunk-ZH5WT2IE.cjs.map → chunk-2WM2KSO7.cjs.map} +1 -1
  7. package/dist/{chunk-XMJE34FY.cjs → chunk-2YZZL5FV.cjs} +13 -13
  8. package/dist/{chunk-XMJE34FY.cjs.map → chunk-2YZZL5FV.cjs.map} +1 -1
  9. package/dist/{chunk-2HAEQDHQ.js → chunk-3IHVDQRC.js} +492 -139
  10. package/dist/chunk-3IHVDQRC.js.map +1 -0
  11. package/dist/{chunk-IE5ZNCWN.js → chunk-4AQ4SWJ3.js} +5 -5
  12. package/dist/{chunk-IE5ZNCWN.js.map → chunk-4AQ4SWJ3.js.map} +1 -1
  13. package/dist/{chunk-RT5FTHUU.js → chunk-4X7NFEDB.js} +5 -5
  14. package/dist/{chunk-RT5FTHUU.js.map → chunk-4X7NFEDB.js.map} +1 -1
  15. package/dist/{chunk-SRD4KVAP.cjs → chunk-A2CPCOJA.cjs} +503 -149
  16. package/dist/chunk-A2CPCOJA.cjs.map +1 -0
  17. package/dist/{chunk-AF72QUBI.js → chunk-AEUP63CZ.js} +5 -5
  18. package/dist/{chunk-AF72QUBI.js.map → chunk-AEUP63CZ.js.map} +1 -1
  19. package/dist/{chunk-DQ3VVU5T.cjs → chunk-ALDGRDYD.cjs} +24 -24
  20. package/dist/{chunk-DQ3VVU5T.cjs.map → chunk-ALDGRDYD.cjs.map} +1 -1
  21. package/dist/{chunk-WKOV25NP.cjs → chunk-BO64SLHL.cjs} +8 -8
  22. package/dist/{chunk-WKOV25NP.cjs.map → chunk-BO64SLHL.cjs.map} +1 -1
  23. package/dist/{chunk-L7NE7PR4.cjs → chunk-DMCFARGP.cjs} +13 -13
  24. package/dist/{chunk-L7NE7PR4.cjs.map → chunk-DMCFARGP.cjs.map} +1 -1
  25. package/dist/{chunk-QM4AV6EG.js → chunk-HJLVWZOM.js} +4 -4
  26. package/dist/{chunk-QM4AV6EG.js.map → chunk-HJLVWZOM.js.map} +1 -1
  27. package/dist/{chunk-ZJMTHPHY.js → chunk-ICAYWVNF.js} +3 -3
  28. package/dist/{chunk-ZJMTHPHY.js.map → chunk-ICAYWVNF.js.map} +1 -1
  29. package/dist/{chunk-26GQO5I2.cjs → chunk-JYC2VV3Y.cjs} +12 -12
  30. package/dist/{chunk-26GQO5I2.cjs.map → chunk-JYC2VV3Y.cjs.map} +1 -1
  31. package/dist/{chunk-UCKA22ZJ.cjs → chunk-K63VPFOV.cjs} +9 -2
  32. package/dist/chunk-K63VPFOV.cjs.map +1 -0
  33. package/dist/{chunk-YWQH35VO.js → chunk-NOF6HJDV.js} +4 -4
  34. package/dist/{chunk-YWQH35VO.js.map → chunk-NOF6HJDV.js.map} +1 -1
  35. package/dist/{chunk-I2QGW4GD.js → chunk-OOYKKO3V.js} +11 -11
  36. package/dist/{chunk-I2QGW4GD.js.map → chunk-OOYKKO3V.js.map} +1 -1
  37. package/dist/{chunk-FKGUKFC7.cjs → chunk-OXUUE3HX.cjs} +5 -5
  38. package/dist/{chunk-FKGUKFC7.cjs.map → chunk-OXUUE3HX.cjs.map} +1 -1
  39. package/dist/{chunk-53YOZOGN.cjs → chunk-P5FGJMMI.cjs} +5 -5
  40. package/dist/{chunk-53YOZOGN.cjs.map → chunk-P5FGJMMI.cjs.map} +1 -1
  41. package/dist/{chunk-UAYWLYFW.cjs → chunk-QADR2XMQ.cjs} +18 -18
  42. package/dist/{chunk-UAYWLYFW.cjs.map → chunk-QADR2XMQ.cjs.map} +1 -1
  43. package/dist/{chunk-52GXJNQ3.js → chunk-QIA5FAAW.js} +8 -2
  44. package/dist/chunk-QIA5FAAW.js.map +1 -0
  45. package/dist/{chunk-EKBEKDWO.cjs → chunk-RJGCT35F.cjs} +13 -13
  46. package/dist/{chunk-EKBEKDWO.cjs.map → chunk-RJGCT35F.cjs.map} +1 -1
  47. package/dist/{chunk-33BQNWAQ.js → chunk-T33AJYLW.js} +5 -5
  48. package/dist/{chunk-33BQNWAQ.js.map → chunk-T33AJYLW.js.map} +1 -1
  49. package/dist/{chunk-EVBYEWHO.js → chunk-UHKZXOXD.js} +9 -2
  50. package/dist/chunk-UHKZXOXD.js.map +1 -0
  51. package/dist/{chunk-HA3VRF6E.js → chunk-UIGSH2XB.js} +3 -3
  52. package/dist/{chunk-HA3VRF6E.js.map → chunk-UIGSH2XB.js.map} +1 -1
  53. package/dist/{chunk-Z6TVOAO5.cjs → chunk-UISZWFI7.cjs} +8 -2
  54. package/dist/chunk-UISZWFI7.cjs.map +1 -0
  55. package/dist/{chunk-CE6OGJE6.js → chunk-UYRQEIB7.js} +5 -5
  56. package/dist/{chunk-CE6OGJE6.js.map → chunk-UYRQEIB7.js.map} +1 -1
  57. package/dist/{chunk-QEB6O6GU.js → chunk-W32ND3ZR.js} +6 -6
  58. package/dist/{chunk-QEB6O6GU.js.map → chunk-W32ND3ZR.js.map} +1 -1
  59. package/dist/{chunk-H6I2JDNW.js → chunk-WBLRUJ77.js} +10 -10
  60. package/dist/{chunk-H6I2JDNW.js.map → chunk-WBLRUJ77.js.map} +1 -1
  61. package/dist/{chunk-DP3DM7FR.cjs → chunk-WGIMLXRW.cjs} +6 -6
  62. package/dist/{chunk-DP3DM7FR.cjs.map → chunk-WGIMLXRW.cjs.map} +1 -1
  63. package/dist/{chunk-4H3JTX74.cjs → chunk-ZA5QUQ2V.cjs} +13 -13
  64. package/dist/{chunk-4H3JTX74.cjs.map → chunk-ZA5QUQ2V.cjs.map} +1 -1
  65. package/dist/components/ActivityPreview.cjs +12 -12
  66. package/dist/components/ActivityPreview.d.cts +2 -2
  67. package/dist/components/ActivityPreview.d.ts +2 -2
  68. package/dist/components/ActivityPreview.js +11 -11
  69. package/dist/components/ActivitySequence.cjs +13 -13
  70. package/dist/components/ActivitySequence.d.cts +2 -2
  71. package/dist/components/ActivitySequence.d.ts +2 -2
  72. package/dist/components/ActivitySequence.js +12 -12
  73. package/dist/components/Dictation.cjs +5 -5
  74. package/dist/components/Dictation.d.cts +1 -1
  75. package/dist/components/Dictation.d.ts +1 -1
  76. package/dist/components/Dictation.js +4 -4
  77. package/dist/components/FillInTheBlanks.cjs +5 -5
  78. package/dist/components/FillInTheBlanks.d.cts +1 -1
  79. package/dist/components/FillInTheBlanks.d.ts +1 -1
  80. package/dist/components/FillInTheBlanks.js +4 -4
  81. package/dist/components/GapSelect.cjs +5 -5
  82. package/dist/components/GapSelect.d.cts +1 -1
  83. package/dist/components/GapSelect.d.ts +1 -1
  84. package/dist/components/GapSelect.js +4 -4
  85. package/dist/components/InteractiveVideo.cjs +15 -11
  86. package/dist/components/InteractiveVideo.d.cts +50 -4
  87. package/dist/components/InteractiveVideo.d.ts +50 -4
  88. package/dist/components/InteractiveVideo.js +10 -10
  89. package/dist/components/MultipleChoice.cjs +5 -5
  90. package/dist/components/MultipleChoice.d.cts +1 -1
  91. package/dist/components/MultipleChoice.d.ts +1 -1
  92. package/dist/components/MultipleChoice.js +4 -4
  93. package/dist/components/PronunciationFeedback.cjs +4 -4
  94. package/dist/components/PronunciationFeedback.d.cts +1 -1
  95. package/dist/components/PronunciationFeedback.d.ts +1 -1
  96. package/dist/components/PronunciationFeedback.js +3 -3
  97. package/dist/components/ReadAloud.cjs +6 -6
  98. package/dist/components/ReadAloud.d.cts +2 -2
  99. package/dist/components/ReadAloud.d.ts +2 -2
  100. package/dist/components/ReadAloud.js +5 -5
  101. package/dist/components/StimulusPanel.cjs +4 -4
  102. package/dist/components/StimulusPanel.d.cts +1 -1
  103. package/dist/components/StimulusPanel.d.ts +1 -1
  104. package/dist/components/StimulusPanel.js +3 -3
  105. package/dist/components/WrittenResponse.cjs +5 -5
  106. package/dist/components/WrittenResponse.d.cts +3 -3
  107. package/dist/components/WrittenResponse.d.ts +3 -3
  108. package/dist/components/WrittenResponse.js +4 -4
  109. package/dist/i18n/LkIntlProvider.cjs +7 -7
  110. package/dist/i18n/LkIntlProvider.d.cts +2 -2
  111. package/dist/i18n/LkIntlProvider.d.ts +2 -2
  112. package/dist/i18n/LkIntlProvider.js +1 -1
  113. package/dist/index.cjs +41 -37
  114. package/dist/index.d.cts +3 -3
  115. package/dist/index.d.ts +3 -3
  116. package/dist/index.js +15 -15
  117. package/dist/{strings-D0IWcsUm.d.ts → strings-Dgc5-DeA.d.ts} +21 -2
  118. package/dist/{strings-5588Q4Te.d.cts → strings-zY2QQYpU.d.cts} +21 -2
  119. package/dist/theme/ThemeProvider.cjs +6 -6
  120. package/dist/theme/ThemeProvider.js +1 -1
  121. package/dist/theme/defaults.css +7 -0
  122. package/dist/theme/skin.css +44 -6
  123. package/package.json +3 -3
  124. package/dist/chunk-2HAEQDHQ.js.map +0 -1
  125. package/dist/chunk-52GXJNQ3.js.map +0 -1
  126. package/dist/chunk-EVBYEWHO.js.map +0 -1
  127. package/dist/chunk-SRD4KVAP.cjs.map +0 -1
  128. package/dist/chunk-UCKA22ZJ.cjs.map +0 -1
  129. package/dist/chunk-Z6TVOAO5.cjs.map +0 -1
@@ -1,13 +1,13 @@
1
1
  'use client';
2
2
  'use strict';
3
3
 
4
- var chunkL7NE7PR4_cjs = require('./chunk-L7NE7PR4.cjs');
5
- var chunk4H3JTX74_cjs = require('./chunk-4H3JTX74.cjs');
6
- var chunkEKBEKDWO_cjs = require('./chunk-EKBEKDWO.cjs');
7
- var chunkXMJE34FY_cjs = require('./chunk-XMJE34FY.cjs');
8
- var chunkZH5WT2IE_cjs = require('./chunk-ZH5WT2IE.cjs');
9
- var chunk53YOZOGN_cjs = require('./chunk-53YOZOGN.cjs');
10
- var chunkZ6TVOAO5_cjs = require('./chunk-Z6TVOAO5.cjs');
4
+ var chunkDMCFARGP_cjs = require('./chunk-DMCFARGP.cjs');
5
+ var chunkZA5QUQ2V_cjs = require('./chunk-ZA5QUQ2V.cjs');
6
+ var chunkRJGCT35F_cjs = require('./chunk-RJGCT35F.cjs');
7
+ var chunk2YZZL5FV_cjs = require('./chunk-2YZZL5FV.cjs');
8
+ var chunk2WM2KSO7_cjs = require('./chunk-2WM2KSO7.cjs');
9
+ var chunkP5FGJMMI_cjs = require('./chunk-P5FGJMMI.cjs');
10
+ var chunkUISZWFI7_cjs = require('./chunk-UISZWFI7.cjs');
11
11
  var lkCore = require('@intellectif/lk-core');
12
12
  var react = require('react');
13
13
  var jsxRuntime = require('react/jsx-runtime');
@@ -109,6 +109,7 @@ var SHORTCUT_KEYS = [
109
109
  { action: "chapter", keys: "[ / ]" },
110
110
  { action: "mute", keys: "M" },
111
111
  { action: "captions", keys: "C" },
112
+ { action: "secondary-captions", keys: "Shift + C" },
112
113
  { action: "transcript", keys: "T" },
113
114
  { action: "picture-in-picture", keys: "P" },
114
115
  { action: "fullscreen", keys: "F" },
@@ -116,17 +117,87 @@ var SHORTCUT_KEYS = [
116
117
  ];
117
118
  var JUMP_SECONDS = 10;
118
119
  var FRAME_SECONDS = 1 / 30;
120
+
121
+ // src/components/InteractiveVideo/tracks.ts
122
+ var primarySubtag = (tag) => (tag.split("-")[0] ?? tag).toLowerCase();
123
+ function preferCaptions(tracks) {
124
+ return tracks.find((track) => track.kind === "captions") ?? tracks[0];
125
+ }
126
+ function matchLanguage(tracks, language) {
127
+ const wanted = language.toLowerCase();
128
+ const exact = tracks.filter((track) => track.srclang.toLowerCase() === wanted);
129
+ if (exact.length > 0) {
130
+ return preferCaptions(exact);
131
+ }
132
+ const family = primarySubtag(wanted);
133
+ return preferCaptions(tracks.filter((track) => primarySubtag(track.srclang) === family));
134
+ }
135
+ function drawable(tracks) {
136
+ return tracks.filter((track) => track.kind === "captions" || track.kind === "subtitles");
137
+ }
138
+ function secondaryCandidates(tracks, primary) {
139
+ const taken = primary?.srclang.toLowerCase();
140
+ return drawable(tracks).filter((track) => track.srclang.toLowerCase() !== taken);
141
+ }
142
+ function resolveCaptionTracks(tracks, preferences) {
143
+ const usable = drawable(tracks);
144
+ if (usable.length === 0) {
145
+ return {};
146
+ }
147
+ const primary = (preferences.captionLanguage === null ? void 0 : matchLanguage(usable, preferences.captionLanguage)) ?? usable.find((track) => track.default === true) ?? usable[0];
148
+ const second = preferences.secondaryCaptionLanguage;
149
+ const secondary = second === null ? void 0 : matchLanguage(secondaryCandidates(usable, primary), second);
150
+ return secondary === void 0 ? { primary } : { primary, secondary };
151
+ }
152
+ var overlap = (a, b) => Math.min(a.end, b.end) - Math.max(a.start, b.start);
153
+ var LOOKBACK = 8;
154
+ function pairSecondaryCues(primary, secondary) {
155
+ const rows = primary.map(() => []);
156
+ for (const cue of secondary) {
157
+ let low = 0;
158
+ let high = primary.length;
159
+ while (low < high) {
160
+ const middle = low + high >> 1;
161
+ if (primary[middle].start < cue.end) {
162
+ low = middle + 1;
163
+ } else {
164
+ high = middle;
165
+ }
166
+ }
167
+ let best = -1;
168
+ let bestShare = 0;
169
+ let misses = 0;
170
+ for (let index = low - 1; index >= 0 && misses <= LOOKBACK; index -= 1) {
171
+ const share = overlap(primary[index], cue);
172
+ if (share > 0) {
173
+ misses = 0;
174
+ if (share >= bestShare) {
175
+ best = index;
176
+ bestShare = share;
177
+ }
178
+ } else {
179
+ misses += 1;
180
+ }
181
+ }
182
+ if (best !== -1) {
183
+ rows[best].push(cue);
184
+ }
185
+ }
186
+ return rows;
187
+ }
119
188
  function Menu({
120
189
  label,
121
190
  onClose,
122
- children
191
+ children,
192
+ returnTo
123
193
  }) {
124
194
  const ref = react.useRef(null);
125
195
  react.useEffect(() => {
126
196
  const items = ref.current?.querySelectorAll('[role^="menuitem"]');
197
+ const back = returnTo === void 0 ? null : ref.current?.querySelector(`[data-menu-key="${returnTo}"]`);
127
198
  const checked = ref.current?.querySelector('[aria-checked="true"]');
128
- (checked ?? items?.[0])?.focus();
129
- }, []);
199
+ (back ?? checked ?? items?.[0])?.focus();
200
+ }, [returnTo]);
130
201
  const onKeyDown = (event) => {
131
202
  const items = Array.from(
132
203
  ref.current?.querySelectorAll('[role^="menuitem"]') ?? []
@@ -178,66 +249,136 @@ function SpeedMenu({
178
249
  function SettingsMenu({
179
250
  preferences,
180
251
  tracks,
181
- activeTrack,
252
+ primaryTrack,
253
+ secondaryTrack,
182
254
  captionsFailed,
183
255
  strings,
184
256
  onChange,
257
+ onCaptionTrack,
258
+ onSecondaryTrack,
185
259
  onShowShortcuts,
186
260
  onClose
187
261
  }) {
188
262
  const [page, setPage] = react.useState("main");
189
- const captionsValue = !preferences.captions || activeTrack === void 0 ? strings.videoOff : activeTrack.label;
263
+ const [returnTo, setReturnTo] = react.useState(void 0);
264
+ const open = (next) => {
265
+ setReturnTo(void 0);
266
+ setPage(next);
267
+ };
268
+ const leave = () => {
269
+ setReturnTo(page);
270
+ setPage(page === "second" ? "captions" : "main");
271
+ };
272
+ const captionsValue = !preferences.captions || primaryTrack === void 0 ? strings.videoOff : secondaryTrack === void 0 ? primaryTrack.label : strings.videoCaptionPair(primaryTrack.label, secondaryTrack.label);
273
+ const partners = secondaryCandidates(tracks, primaryTrack);
274
+ const failedNotes = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
275
+ captionsFailed.primary && page === "captions" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "lk-iv-menu-note", role: "status", children: strings.videoCaptionsFailed }) : null,
276
+ captionsFailed.secondary ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "lk-iv-menu-note", role: "status", children: strings.videoSecondaryCaptionsFailed }) : null
277
+ ] });
190
278
  const back = /* @__PURE__ */ jsxRuntime.jsxs(
191
279
  "button",
192
280
  {
193
281
  type: "button",
194
282
  role: "menuitem",
195
283
  className: "lk-iv-menu-item lk-iv-menu-back",
196
- onClick: () => setPage("main"),
284
+ onClick: leave,
197
285
  children: [
198
286
  /* @__PURE__ */ jsxRuntime.jsx(ChevronIcon, { direction: "left" }),
199
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: page === "captions" ? strings.videoCaptionLanguage : strings.videoCaptionSize })
287
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: page === "captions" ? strings.videoCaptionLanguage : page === "second" ? strings.videoSecondCaptionLanguage : strings.videoCaptionSize })
200
288
  ]
201
289
  }
202
290
  );
291
+ const trackRadio = (track, on, choose) => /* @__PURE__ */ jsxRuntime.jsxs(
292
+ "button",
293
+ {
294
+ type: "button",
295
+ role: "menuitemradio",
296
+ "aria-checked": on,
297
+ className: "lk-iv-menu-item",
298
+ lang: track.srclang,
299
+ onClick: choose,
300
+ children: [
301
+ /* @__PURE__ */ jsxRuntime.jsx("span", { dir: "auto", children: track.label }),
302
+ on ? /* @__PURE__ */ jsxRuntime.jsx(CheckIcon, {}) : null
303
+ ]
304
+ },
305
+ `${track.kind}:${track.srclang}`
306
+ );
203
307
  if (page === "captions") {
204
- return /* @__PURE__ */ jsxRuntime.jsxs(Menu, { label: strings.videoCaptionLanguage, onClose, children: [
308
+ return /* @__PURE__ */ jsxRuntime.jsxs(
309
+ Menu,
310
+ {
311
+ label: strings.videoCaptionLanguage,
312
+ onClose,
313
+ returnTo,
314
+ children: [
315
+ back,
316
+ /* @__PURE__ */ jsxRuntime.jsxs(
317
+ "button",
318
+ {
319
+ type: "button",
320
+ role: "menuitemradio",
321
+ "aria-checked": !preferences.captions,
322
+ className: "lk-iv-menu-item",
323
+ onClick: () => onChange({ captions: false }),
324
+ children: [
325
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: strings.videoOff }),
326
+ !preferences.captions ? /* @__PURE__ */ jsxRuntime.jsx(CheckIcon, {}) : null
327
+ ]
328
+ }
329
+ ),
330
+ tracks.map(
331
+ (track) => trackRadio(
332
+ track,
333
+ preferences.captions && primaryTrack === track,
334
+ () => onCaptionTrack(track)
335
+ )
336
+ ),
337
+ partners.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(
338
+ "button",
339
+ {
340
+ type: "button",
341
+ role: "menuitem",
342
+ className: "lk-iv-menu-item",
343
+ "data-menu-key": "second",
344
+ onClick: () => open("second"),
345
+ children: [
346
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: strings.videoSecondCaptionLanguage }),
347
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "lk-iv-menu-value", children: [
348
+ /* @__PURE__ */ jsxRuntime.jsx("span", { dir: "auto", lang: secondaryTrack?.srclang, children: secondaryTrack === void 0 ? strings.videoOff : secondaryTrack.label }),
349
+ /* @__PURE__ */ jsxRuntime.jsx(ChevronIcon, { direction: "right" })
350
+ ] })
351
+ ]
352
+ }
353
+ ) : null,
354
+ failedNotes
355
+ ]
356
+ },
357
+ "captions"
358
+ );
359
+ }
360
+ if (page === "second") {
361
+ return /* @__PURE__ */ jsxRuntime.jsxs(Menu, { label: strings.videoSecondCaptionLanguage, onClose, children: [
205
362
  back,
206
363
  /* @__PURE__ */ jsxRuntime.jsxs(
207
364
  "button",
208
365
  {
209
366
  type: "button",
210
367
  role: "menuitemradio",
211
- "aria-checked": !preferences.captions,
368
+ "aria-checked": secondaryTrack === void 0,
212
369
  className: "lk-iv-menu-item",
213
- onClick: () => onChange({ captions: false }),
370
+ onClick: () => onSecondaryTrack(null),
214
371
  children: [
215
372
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: strings.videoOff }),
216
- !preferences.captions ? /* @__PURE__ */ jsxRuntime.jsx(CheckIcon, {}) : null
373
+ secondaryTrack === void 0 ? /* @__PURE__ */ jsxRuntime.jsx(CheckIcon, {}) : null
217
374
  ]
218
375
  }
219
376
  ),
220
- tracks.map((track) => {
221
- const on = preferences.captions && activeTrack === track;
222
- return /* @__PURE__ */ jsxRuntime.jsxs(
223
- "button",
224
- {
225
- type: "button",
226
- role: "menuitemradio",
227
- "aria-checked": on,
228
- className: "lk-iv-menu-item",
229
- lang: track.srclang,
230
- onClick: () => onChange({ captions: true, captionLanguage: track.srclang }),
231
- children: [
232
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: track.label }),
233
- on ? /* @__PURE__ */ jsxRuntime.jsx(CheckIcon, {}) : null
234
- ]
235
- },
236
- `${track.kind}:${track.srclang}`
237
- );
238
- }),
239
- captionsFailed ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "lk-iv-menu-note", role: "status", children: strings.videoCaptionsFailed }) : null
240
- ] }, "captions");
377
+ partners.map(
378
+ (track) => trackRadio(track, secondaryTrack === track, () => onSecondaryTrack(track))
379
+ ),
380
+ failedNotes
381
+ ] }, "second");
241
382
  }
242
383
  if (page === "size") {
243
384
  return /* @__PURE__ */ jsxRuntime.jsxs(Menu, { label: strings.videoCaptionSize, onClose, children: [
@@ -259,7 +400,7 @@ function SettingsMenu({
259
400
  ))
260
401
  ] }, "size");
261
402
  }
262
- return /* @__PURE__ */ jsxRuntime.jsxs(Menu, { label: strings.videoSettings, onClose, children: [
403
+ return /* @__PURE__ */ jsxRuntime.jsxs(Menu, { label: strings.videoSettings, onClose, returnTo, children: [
263
404
  tracks.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
264
405
  /* @__PURE__ */ jsxRuntime.jsxs(
265
406
  "button",
@@ -267,11 +408,12 @@ function SettingsMenu({
267
408
  type: "button",
268
409
  role: "menuitem",
269
410
  className: "lk-iv-menu-item",
270
- onClick: () => setPage("captions"),
411
+ "data-menu-key": "captions",
412
+ onClick: () => open("captions"),
271
413
  children: [
272
414
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: strings.videoCaptionLanguage }),
273
415
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "lk-iv-menu-value", children: [
274
- captionsValue,
416
+ /* @__PURE__ */ jsxRuntime.jsx("span", { dir: "auto", children: captionsValue }),
275
417
  /* @__PURE__ */ jsxRuntime.jsx(ChevronIcon, { direction: "right" })
276
418
  ] })
277
419
  ]
@@ -283,7 +425,8 @@ function SettingsMenu({
283
425
  type: "button",
284
426
  role: "menuitem",
285
427
  className: "lk-iv-menu-item",
286
- onClick: () => setPage("size"),
428
+ "data-menu-key": "size",
429
+ onClick: () => open("size"),
287
430
  children: [
288
431
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: strings.videoCaptionSize }),
289
432
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "lk-iv-menu-value", children: [
@@ -377,6 +520,9 @@ function ContentsPanel({
377
520
  chapters,
378
521
  quizzes,
379
522
  cues,
523
+ primaryLanguage,
524
+ secondaryCues,
525
+ secondaryLanguage,
380
526
  activeCue,
381
527
  limit,
382
528
  strings,
@@ -388,10 +534,18 @@ function ContentsPanel({
388
534
  const followUntil = react.useRef(0);
389
535
  const hasTranscript = cues.length > 0;
390
536
  const shownTab = hasTranscript ? tab : "contents";
537
+ const paired = react.useMemo(() => pairSecondaryCues(cues, secondaryCues), [cues, secondaryCues]);
391
538
  const lines = react.useMemo(() => {
392
539
  const trimmed = query.trim().toLowerCase();
393
- return cues.map((cue, index) => ({ cue, index })).filter(({ cue }) => cue.start <= limit).filter(({ cue }) => trimmed === "" || cue.text.toLowerCase().includes(trimmed));
394
- }, [cues, limit, query]);
540
+ return cues.map((cue, index) => ({
541
+ cue,
542
+ index,
543
+ // The spoiler limit holds for the second language as for the first.
544
+ second: (paired[index] ?? []).filter((line) => line.start <= limit).map((line) => line.text).join(" ")
545
+ })).filter(({ cue }) => cue.start <= limit).filter(
546
+ ({ cue, second }) => trimmed === "" || cue.text.toLowerCase().includes(trimmed) || second.toLowerCase().includes(trimmed)
547
+ );
548
+ }, [cues, paired, limit, query]);
395
549
  react.useEffect(() => {
396
550
  if (shownTab !== "transcript" || query.trim() !== "" || Date.now() < followUntil.current) {
397
551
  return;
@@ -514,7 +668,7 @@ function ContentsPanel({
514
668
  onTouchMove: () => {
515
669
  followUntil.current = Date.now() + 4e3;
516
670
  },
517
- children: lines.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "lk-iv-empty", children: strings.videoTranscriptNoMatch }) : lines.map(({ cue, index }) => /* @__PURE__ */ jsxRuntime.jsxs(
671
+ children: lines.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "lk-iv-empty", children: strings.videoTranscriptNoMatch }) : lines.map(({ cue, index, second }) => /* @__PURE__ */ jsxRuntime.jsxs(
518
672
  "button",
519
673
  {
520
674
  type: "button",
@@ -523,7 +677,10 @@ function ContentsPanel({
523
677
  onClick: () => onSeek(cue.start),
524
678
  children: [
525
679
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lk-iv-row-time", children: clock(cue.start) }),
526
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: highlight(cue.text) })
680
+ secondaryCues.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("span", { lang: primaryLanguage, dir: "auto", children: highlight(cue.text) }) : /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "lk-iv-line-text", children: [
681
+ /* @__PURE__ */ jsxRuntime.jsx("span", { lang: primaryLanguage, dir: "auto", children: highlight(cue.text) }),
682
+ second === "" ? null : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lk-iv-line-secondary", lang: secondaryLanguage, dir: "auto", children: highlight(second) })
683
+ ] })
527
684
  ]
528
685
  },
529
686
  `${cue.start}-${index}`
@@ -588,45 +745,73 @@ var DEFAULT_PREFERENCES = {
588
745
  muted: false,
589
746
  captions: true,
590
747
  captionLanguage: null,
748
+ secondaryCaptionLanguage: null,
591
749
  captionSize: "medium",
592
750
  captionBackground: true,
593
751
  panel: false,
594
752
  shortcuts: true
595
753
  };
596
754
  var STORAGE_KEY = "lk.video.v1";
597
- function sanitizePreferences(raw) {
598
- if (typeof raw !== "object" || raw === null) {
599
- return DEFAULT_PREFERENCES;
755
+ var LANGUAGE_TAG = /^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
756
+ var BOOLEAN_FIELDS = ["muted", "captions", "captionBackground", "panel", "shortcuts"];
757
+ var LANGUAGE_FIELDS = ["captionLanguage", "secondaryCaptionLanguage"];
758
+ function sanitizePartialPreferences(raw) {
759
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
760
+ return {};
600
761
  }
601
762
  const value = raw;
602
- const bool = (input, fallback) => typeof input === "boolean" ? input : fallback;
603
- const speed = SPEEDS.includes(value.speed) ? value.speed : DEFAULT_PREFERENCES.speed;
604
- const volume = typeof value.volume === "number" && Number.isFinite(value.volume) ? Math.min(1, Math.max(0, value.volume)) : DEFAULT_PREFERENCES.volume;
605
- const size = value.captionSize === "small" || value.captionSize === "medium" || value.captionSize === "large" ? value.captionSize : DEFAULT_PREFERENCES.captionSize;
606
- const language = typeof value.captionLanguage === "string" && /^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/.test(value.captionLanguage) ? value.captionLanguage : null;
607
- return {
608
- speed,
609
- volume,
610
- muted: bool(value.muted, DEFAULT_PREFERENCES.muted),
611
- captions: bool(value.captions, DEFAULT_PREFERENCES.captions),
612
- captionLanguage: language,
613
- captionSize: size,
614
- captionBackground: bool(value.captionBackground, DEFAULT_PREFERENCES.captionBackground),
615
- panel: bool(value.panel, DEFAULT_PREFERENCES.panel),
616
- shortcuts: bool(value.shortcuts, DEFAULT_PREFERENCES.shortcuts)
617
- };
763
+ const has = (key) => Object.hasOwn(value, key);
764
+ const out = {};
765
+ if (has("speed") && SPEEDS.includes(value.speed)) {
766
+ out.speed = value.speed;
767
+ }
768
+ if (has("volume") && typeof value.volume === "number" && Number.isFinite(value.volume)) {
769
+ out.volume = Math.min(1, Math.max(0, value.volume));
770
+ }
771
+ for (const field of BOOLEAN_FIELDS) {
772
+ if (has(field) && typeof value[field] === "boolean") {
773
+ out[field] = value[field];
774
+ }
775
+ }
776
+ for (const field of LANGUAGE_FIELDS) {
777
+ const tag = value[field];
778
+ if (has(field) && (tag === null || typeof tag === "string" && LANGUAGE_TAG.test(tag))) {
779
+ out[field] = tag;
780
+ }
781
+ }
782
+ if (has("captionSize") && (value.captionSize === "small" || value.captionSize === "medium" || value.captionSize === "large")) {
783
+ out.captionSize = value.captionSize;
784
+ }
785
+ return out;
786
+ }
787
+ function normalised(preferences) {
788
+ const { captionLanguage: first, secondaryCaptionLanguage: second } = preferences;
789
+ return first !== null && second !== null && first.toLowerCase() === second.toLowerCase() ? { ...preferences, secondaryCaptionLanguage: null } : preferences;
790
+ }
791
+ function resolvePreferences(layers) {
792
+ return normalised({
793
+ ...DEFAULT_PREFERENCES,
794
+ ...sanitizePartialPreferences(layers.defaults),
795
+ ...sanitizePartialPreferences(layers.stored),
796
+ ...sanitizePartialPreferences(layers.force),
797
+ ...sanitizePartialPreferences(layers.chosen)
798
+ });
618
799
  }
619
- function readPreferences() {
800
+ function applyPreferenceChange(previous, change) {
801
+ return normalised({ ...previous, ...sanitizePartialPreferences(change) });
802
+ }
803
+ function readStoredPreferences() {
620
804
  try {
621
805
  const stored = globalThis.localStorage?.getItem(STORAGE_KEY);
622
- return stored ? sanitizePreferences(JSON.parse(stored)) : DEFAULT_PREFERENCES;
806
+ return stored ? sanitizePartialPreferences(JSON.parse(stored)) : {};
623
807
  } catch {
624
- return DEFAULT_PREFERENCES;
808
+ return {};
625
809
  }
626
810
  }
627
- function writePreferences(preferences) {
811
+ function rememberPreferences(change) {
628
812
  try {
629
- globalThis.localStorage?.setItem(STORAGE_KEY, JSON.stringify(preferences));
813
+ const next = { ...readStoredPreferences(), ...sanitizePartialPreferences(change) };
814
+ globalThis.localStorage?.setItem(STORAGE_KEY, JSON.stringify(next));
630
815
  } catch {
631
816
  }
632
817
  }
@@ -946,6 +1131,55 @@ function onNextFrame(run) {
946
1131
  const timer = setTimeout(run, 33);
947
1132
  return () => clearTimeout(timer);
948
1133
  }
1134
+ function trackKey(track) {
1135
+ return `${track.kind}:${track.srclang}:${track.src}`;
1136
+ }
1137
+ var NO_CUES = [];
1138
+ function useCaptionLine(track, load, peek) {
1139
+ const key = track === void 0 ? void 0 : trackKey(track);
1140
+ const [line, setLine] = react.useState({
1141
+ cues: NO_CUES,
1142
+ failed: false
1143
+ });
1144
+ const trackRef = react.useRef(track);
1145
+ trackRef.current = track;
1146
+ react.useEffect(() => {
1147
+ const wanted = trackRef.current;
1148
+ if (key === void 0 || wanted === void 0) {
1149
+ return;
1150
+ }
1151
+ let live = true;
1152
+ load(wanted).then(
1153
+ (cues) => {
1154
+ if (live) {
1155
+ setLine({ key, cues, failed: false });
1156
+ }
1157
+ },
1158
+ () => {
1159
+ if (live) {
1160
+ setLine({ key, cues: NO_CUES, failed: true });
1161
+ }
1162
+ }
1163
+ );
1164
+ return () => {
1165
+ live = false;
1166
+ };
1167
+ }, [key, load]);
1168
+ if (key === void 0) {
1169
+ return { cues: NO_CUES, failed: false };
1170
+ }
1171
+ if (line.key === key) {
1172
+ return line;
1173
+ }
1174
+ return { cues: peek(key) ?? NO_CUES, failed: false };
1175
+ }
1176
+ function captionsShown(tracks, preferences) {
1177
+ if (!preferences.captions) {
1178
+ return { srclang: null, secondary: null };
1179
+ }
1180
+ const shown = resolveCaptionTracks(tracks, preferences);
1181
+ return { srclang: shown.primary?.srclang ?? null, secondary: shown.secondary?.srclang ?? null };
1182
+ }
949
1183
  var playerCount = 0;
950
1184
  var ITEM_TYPES = /* @__PURE__ */ new Set([
951
1185
  "multiple-choice",
@@ -971,13 +1205,13 @@ function Player(props) {
971
1205
  theme,
972
1206
  sanitizeHtml
973
1207
  } = props;
974
- const strings = chunkZ6TVOAO5_cjs.useLkStrings(props.strings);
1208
+ const strings = chunkUISZWFI7_cjs.useLkStrings(props.strings);
975
1209
  const group = react.useRef(props.group).current;
976
1210
  const timeline = group.timeline ?? { cues: [] };
977
1211
  const media = group.stimulus.media;
978
1212
  const chapters = timeline.chapters ?? [];
979
1213
  const devError = react.useMemo(() => {
980
- if (!chunk53YOZOGN_cjs.isDevelopment()) {
1214
+ if (!chunkP5FGJMMI_cjs.isDevelopment()) {
981
1215
  return null;
982
1216
  }
983
1217
  if (group.timeline === void 0) {
@@ -1023,21 +1257,25 @@ function Player(props) {
1023
1257
  playerCount += 1;
1024
1258
  return `lk-iv-${playerCount}`;
1025
1259
  });
1026
- const [preferences, setPreferences] = react.useState(() => ({
1027
- ...DEFAULT_PREFERENCES,
1028
- ...props.preferences
1029
- }));
1030
- const preferenceOverride = react.useRef(props.preferences);
1260
+ const forced = react.useRef(props.preferences).current;
1261
+ const defaultsKey = JSON.stringify(props.defaultPreferences ?? null);
1262
+ const defaults = react.useMemo(() => JSON.parse(defaultsKey), [defaultsKey]);
1263
+ const chosen = react.useRef({});
1264
+ const [preferences, setPreferences] = react.useState(
1265
+ () => resolvePreferences({ force: forced, defaults: props.defaultPreferences })
1266
+ );
1267
+ const preferencesRef = react.useRef(preferences);
1268
+ preferencesRef.current = preferences;
1031
1269
  react.useEffect(() => {
1032
- setPreferences({ ...readPreferences(), ...preferenceOverride.current });
1033
- }, []);
1034
- const changePreferences = react.useCallback((change) => {
1035
- setPreferences((previous) => {
1036
- const next = { ...previous, ...change };
1037
- writePreferences(next);
1038
- return next;
1039
- });
1040
- }, []);
1270
+ setPreferences(
1271
+ resolvePreferences({
1272
+ force: forced,
1273
+ stored: readStoredPreferences(),
1274
+ defaults,
1275
+ chosen: chosen.current
1276
+ })
1277
+ );
1278
+ }, [forced, defaults]);
1041
1279
  const [submitted] = react.useState(() => new Set(props.submittedSlotIds ?? []));
1042
1280
  const [answered, setAnswered] = react.useState(() => {
1043
1281
  const seeded = /* @__PURE__ */ new Set();
@@ -1306,43 +1544,124 @@ function Player(props) {
1306
1544
  }
1307
1545
  ] : [];
1308
1546
  }, [media, locale, strings.videoCaptionLanguage]);
1309
- const activeTrack = tracks.find((track) => track.srclang === preferences.captionLanguage) ?? tracks.find((track) => track.default === true) ?? tracks[0];
1310
- const [captionCues, setCaptionCues] = react.useState([]);
1311
- const [captionsFailed, setCaptionsFailed] = react.useState(false);
1547
+ const { primary: primaryTrack, secondary: secondaryTrack } = react.useMemo(
1548
+ () => resolveCaptionTracks(tracks, {
1549
+ captionLanguage: preferences.captionLanguage,
1550
+ secondaryCaptionLanguage: preferences.secondaryCaptionLanguage
1551
+ }),
1552
+ [tracks, preferences.captionLanguage, preferences.secondaryCaptionLanguage]
1553
+ );
1312
1554
  const loaderRef = react.useRef(captionsLoader);
1313
1555
  loaderRef.current = captionsLoader;
1314
- react.useEffect(() => {
1315
- if (activeTrack === void 0) {
1316
- setCaptionCues([]);
1317
- return;
1556
+ const loading = react.useRef(/* @__PURE__ */ new Map());
1557
+ const loaded = react.useRef(/* @__PURE__ */ new Map());
1558
+ const loadCues = react.useCallback((track) => {
1559
+ const key = trackKey(track);
1560
+ const pending = loading.current.get(key);
1561
+ if (pending !== void 0) {
1562
+ return pending;
1318
1563
  }
1319
- let live = true;
1320
- setCaptionsFailed(false);
1321
- const load = loaderRef.current ? loaderRef.current(activeTrack) : fetch(activeTrack.src, { credentials: "same-origin" }).then((response) => {
1322
- if (!response.ok) {
1323
- throw new Error(String(response.status));
1564
+ const request = Promise.resolve().then(
1565
+ () => loaderRef.current ? loaderRef.current(track) : fetch(track.src, { credentials: "same-origin" }).then((response) => {
1566
+ if (!response.ok) {
1567
+ throw new Error(String(response.status));
1568
+ }
1569
+ return response.text();
1570
+ })
1571
+ ).then((text) => {
1572
+ const parsed = parseWebVtt(text);
1573
+ if (parsed.length === 0) {
1574
+ throw new Error("The caption file held no cues.");
1324
1575
  }
1325
- return response.text();
1576
+ loaded.current.set(key, parsed);
1577
+ return parsed;
1326
1578
  });
1327
- Promise.resolve(load).then((text) => {
1328
- if (live) {
1329
- const parsed = parseWebVtt(text);
1330
- setCaptionCues(parsed);
1331
- setCaptionsFailed(parsed.length === 0);
1579
+ loading.current.set(key, request);
1580
+ request.catch(() => {
1581
+ loading.current.delete(key);
1582
+ });
1583
+ return request;
1584
+ }, []);
1585
+ const peekCues = react.useCallback((key) => loaded.current.get(key), []);
1586
+ const primaryLine = useCaptionLine(primaryTrack, loadCues, peekCues);
1587
+ const secondaryLine = useCaptionLine(secondaryTrack, loadCues, peekCues);
1588
+ const primaryCues = primaryLine.cues;
1589
+ const secondaryCues = secondaryLine.cues;
1590
+ const captionsFailed = { primary: primaryLine.failed, secondary: secondaryLine.failed };
1591
+ const hasCaptions = primaryCues.length > 0 || secondaryCues.length > 0;
1592
+ const hasTranscript = primaryCues.length > 0;
1593
+ const activeCueIndex = react.useMemo(() => cueIndexAt(primaryCues, current), [primaryCues, current]);
1594
+ const secondaryCueIndex = react.useMemo(
1595
+ () => cueIndexAt(secondaryCues, current),
1596
+ [secondaryCues, current]
1597
+ );
1598
+ const spoilerLimit = timeline.navigation === "no-skip-ahead" && renderMode !== "review" ? Math.max(furthestShown, current) : Number.POSITIVE_INFINITY;
1599
+ const onPreferencesChangeRef = react.useRef(props.onPreferencesChange);
1600
+ onPreferencesChangeRef.current = props.onPreferencesChange;
1601
+ const tracksRef = react.useRef(tracks);
1602
+ tracksRef.current = tracks;
1603
+ const lastSecondary = react.useRef(null);
1604
+ const changePreferences = react.useCallback(
1605
+ (change) => {
1606
+ const previous = preferencesRef.current;
1607
+ const next = applyPreferenceChange(previous, change);
1608
+ if (previous.secondaryCaptionLanguage !== null) {
1609
+ lastSecondary.current = previous.secondaryCaptionLanguage;
1332
1610
  }
1333
- }).catch(() => {
1334
- if (live) {
1335
- setCaptionCues([]);
1336
- setCaptionsFailed(true);
1611
+ chosen.current = { ...chosen.current, ...change };
1612
+ preferencesRef.current = next;
1613
+ setPreferences(next);
1614
+ rememberPreferences(change);
1615
+ onPreferencesChangeRef.current?.(next, change);
1616
+ const before = captionsShown(tracksRef.current, previous);
1617
+ const after = captionsShown(tracksRef.current, next);
1618
+ if (before.srclang !== after.srclang || before.secondary !== after.secondary) {
1619
+ emit("video-captions-changed", after);
1337
1620
  }
1621
+ },
1622
+ [emit]
1623
+ );
1624
+ const chooseCaptionTrack = (track) => {
1625
+ const swaps = primaryTrack !== void 0 && secondaryTrack !== void 0 && track.srclang.toLowerCase() === secondaryTrack.srclang.toLowerCase();
1626
+ changePreferences({
1627
+ captions: true,
1628
+ captionLanguage: track.srclang,
1629
+ ...swaps ? { secondaryCaptionLanguage: primaryTrack.srclang } : {}
1338
1630
  });
1339
- return () => {
1340
- live = false;
1341
- };
1342
- }, [activeTrack]);
1343
- const hasCaptions = captionCues.length > 0;
1344
- const activeCueIndex = react.useMemo(() => cueIndexAt(captionCues, current), [captionCues, current]);
1345
- const spoilerLimit = timeline.navigation === "no-skip-ahead" && renderMode !== "review" ? Math.max(furthestShown, current) : Number.POSITIVE_INFINITY;
1631
+ };
1632
+ const chooseSecondaryTrack = (track) => {
1633
+ changePreferences(
1634
+ track === null ? { secondaryCaptionLanguage: null } : { secondaryCaptionLanguage: track.srclang, captions: true }
1635
+ );
1636
+ };
1637
+ const toggleSecondary = () => {
1638
+ if (secondaryTrack !== void 0) {
1639
+ changePreferences(
1640
+ preferences.captions ? { secondaryCaptionLanguage: null } : { captions: true }
1641
+ );
1642
+ return;
1643
+ }
1644
+ const wanted = [
1645
+ lastSecondary.current,
1646
+ preferences.secondaryCaptionLanguage,
1647
+ sanitizePartialPreferences(defaults).secondaryCaptionLanguage ?? null,
1648
+ secondaryCandidates(tracks, primaryTrack)[0]?.srclang ?? null
1649
+ ];
1650
+ for (const language of wanted) {
1651
+ if (language === null) {
1652
+ continue;
1653
+ }
1654
+ const { secondary } = resolveCaptionTracks(tracks, {
1655
+ captionLanguage: preferences.captionLanguage,
1656
+ secondaryCaptionLanguage: language
1657
+ });
1658
+ if (secondary !== void 0) {
1659
+ changePreferences({ secondaryCaptionLanguage: secondary.srclang, captions: true });
1660
+ return;
1661
+ }
1662
+ }
1663
+ announce(strings.videoNoSecondLanguage);
1664
+ };
1346
1665
  react.useEffect(() => {
1347
1666
  const video = videoRef.current;
1348
1667
  if (video !== null) {
@@ -1509,14 +1828,17 @@ function Player(props) {
1509
1828
  return;
1510
1829
  case "c":
1511
1830
  case "C":
1512
- if (hasCaptions) {
1831
+ if (event.shiftKey) {
1832
+ done();
1833
+ toggleSecondary();
1834
+ } else if (hasCaptions) {
1513
1835
  done();
1514
1836
  changePreferences({ captions: !preferences.captions });
1515
1837
  }
1516
1838
  return;
1517
1839
  case "t":
1518
1840
  case "T":
1519
- if (hasCaptions) {
1841
+ if (hasTranscript) {
1520
1842
  done();
1521
1843
  setPanelTab("transcript");
1522
1844
  changePreferences({ panel: !(preferences.panel && panelTab === "transcript") });
@@ -1648,7 +1970,7 @@ function Player(props) {
1648
1970
  switch (activity.type) {
1649
1971
  case "multiple-choice":
1650
1972
  question = /* @__PURE__ */ jsxRuntime.jsx(
1651
- chunkL7NE7PR4_cjs.MultipleChoice,
1973
+ chunkDMCFARGP_cjs.MultipleChoice,
1652
1974
  {
1653
1975
  data: activity,
1654
1976
  ...common,
@@ -1657,11 +1979,11 @@ function Player(props) {
1657
1979
  );
1658
1980
  break;
1659
1981
  case "fill-in-the-blanks":
1660
- question = /* @__PURE__ */ jsxRuntime.jsx(chunk4H3JTX74_cjs.FillInTheBlanks, { data: activity, ...common });
1982
+ question = /* @__PURE__ */ jsxRuntime.jsx(chunkZA5QUQ2V_cjs.FillInTheBlanks, { data: activity, ...common });
1661
1983
  break;
1662
1984
  case "gap-select":
1663
1985
  question = /* @__PURE__ */ jsxRuntime.jsx(
1664
- chunkEKBEKDWO_cjs.GapSelect,
1986
+ chunkRJGCT35F_cjs.GapSelect,
1665
1987
  {
1666
1988
  data: activity,
1667
1989
  ...common,
@@ -1670,13 +1992,13 @@ function Player(props) {
1670
1992
  );
1671
1993
  break;
1672
1994
  case "dictation":
1673
- question = /* @__PURE__ */ jsxRuntime.jsx(chunkXMJE34FY_cjs.Dictation, { data: activity, ...common });
1995
+ question = /* @__PURE__ */ jsxRuntime.jsx(chunk2YZZL5FV_cjs.Dictation, { data: activity, ...common });
1674
1996
  break;
1675
1997
  case "read-aloud": {
1676
1998
  const binding = slotBindings.get(slot.slotId);
1677
1999
  const assessment = props.assessments !== void 0 && Object.hasOwn(props.assessments, slot.slotId) ? props.assessments[slot.slotId] : void 0;
1678
2000
  question = /* @__PURE__ */ jsxRuntime.jsx(
1679
- chunkZH5WT2IE_cjs.ReadAloud,
2001
+ chunk2WM2KSO7_cjs.ReadAloud,
1680
2002
  {
1681
2003
  data: activity,
1682
2004
  ...common,
@@ -1720,7 +2042,9 @@ function Player(props) {
1720
2042
  emit("video-quiz-question-shown", { cueId: openCue.id, slotId: slot.slotId });
1721
2043
  }
1722
2044
  };
1723
- const showCaption = preferences.captions && hasCaptions && activeCueIndex !== -1 && openQuiz === null && !ended && errorCode === null;
2045
+ const captionsHidden = !preferences.captions || openQuiz !== null || ended || errorCode !== null;
2046
+ const primaryText = captionsHidden ? void 0 : primaryCues[activeCueIndex]?.text;
2047
+ const secondaryText = captionsHidden ? void 0 : secondaryCues[secondaryCueIndex]?.text;
1724
2048
  const chromeVisible = !playing || chromeShown || menu !== null || scrubbing || focusInChrome || sheet || openQuiz !== null;
1725
2049
  const volumeLevel = preferences.muted || preferences.volume === 0 ? "muted" : preferences.volume < 0.5 ? "low" : "high";
1726
2050
  return (
@@ -1832,15 +2156,43 @@ function Player(props) {
1832
2156
  }
1833
2157
  ),
1834
2158
  label !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lk-iv-label", children: label }) : null,
1835
- showCaption ? /* @__PURE__ */ jsxRuntime.jsx(
1836
- "div",
1837
- {
1838
- className: "lk-iv-caption",
1839
- "data-size": preferences.captionSize,
1840
- "data-background": preferences.captionBackground || void 0,
1841
- lang: activeTrack?.srclang,
1842
- children: /* @__PURE__ */ jsxRuntime.jsx("span", { children: captionCues[activeCueIndex]?.text })
1843
- }
2159
+ primaryText !== void 0 || secondaryText !== void 0 ? (
2160
+ // The lines keep `.lk-iv-caption`, `data-size` and `data-background`
2161
+ // as well, so a stylesheet written against 15.x still matches them.
2162
+ /* @__PURE__ */ jsxRuntime.jsxs(
2163
+ "div",
2164
+ {
2165
+ className: "lk-iv-captions",
2166
+ "data-size": preferences.captionSize,
2167
+ "data-background": preferences.captionBackground || void 0,
2168
+ children: [
2169
+ primaryText !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(
2170
+ "div",
2171
+ {
2172
+ className: "lk-iv-caption",
2173
+ "data-role": "primary",
2174
+ "data-size": preferences.captionSize,
2175
+ "data-background": preferences.captionBackground || void 0,
2176
+ lang: primaryTrack?.srclang,
2177
+ dir: "auto",
2178
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { children: primaryText })
2179
+ }
2180
+ ) : null,
2181
+ secondaryText !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(
2182
+ "div",
2183
+ {
2184
+ className: "lk-iv-caption",
2185
+ "data-role": "secondary",
2186
+ "data-size": preferences.captionSize,
2187
+ "data-background": preferences.captionBackground || void 0,
2188
+ lang: secondaryTrack?.srclang,
2189
+ dir: "auto",
2190
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { children: secondaryText })
2191
+ }
2192
+ ) : null
2193
+ ]
2194
+ }
2195
+ )
1844
2196
  ) : null,
1845
2197
  waiting && errorCode === null && openQuiz === null ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lk-iv-spinner", "aria-hidden": "true" }) : null,
1846
2198
  errorCode !== null ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lk-iv-error", role: "alert", children: [
@@ -1889,7 +2241,7 @@ function Player(props) {
1889
2241
  buffered,
1890
2242
  chapters,
1891
2243
  markers,
1892
- cues: captionCues,
2244
+ cues: primaryCues,
1893
2245
  previewLimit: spoilerLimit,
1894
2246
  strings,
1895
2247
  onSeek: seek,
@@ -1974,7 +2326,7 @@ function Player(props) {
1974
2326
  )
1975
2327
  ] }),
1976
2328
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lk-iv-bar-group", children: [
1977
- quizzes.length > 0 || chapters.length > 0 || hasCaptions ? /* @__PURE__ */ jsxRuntime.jsx(
2329
+ quizzes.length > 0 || chapters.length > 0 || hasTranscript ? /* @__PURE__ */ jsxRuntime.jsx(
1978
2330
  "button",
1979
2331
  {
1980
2332
  type: "button",
@@ -1992,12 +2344,7 @@ function Player(props) {
1992
2344
  className: "lk-iv-button",
1993
2345
  "aria-label": preferences.captions ? strings.videoCaptionsHide : strings.videoCaptionsShow,
1994
2346
  "aria-pressed": preferences.captions,
1995
- onClick: () => {
1996
- changePreferences({ captions: !preferences.captions });
1997
- emit("video-captions-changed", {
1998
- srclang: preferences.captions ? null : activeTrack?.srclang ?? null
1999
- });
2000
- },
2347
+ onClick: () => changePreferences({ captions: !preferences.captions }),
2001
2348
  children: /* @__PURE__ */ jsxRuntime.jsx(CaptionsIcon, { on: preferences.captions })
2002
2349
  }
2003
2350
  ) : null,
@@ -2045,8 +2392,11 @@ function Player(props) {
2045
2392
  {
2046
2393
  preferences,
2047
2394
  tracks,
2048
- activeTrack,
2395
+ primaryTrack,
2396
+ secondaryTrack,
2049
2397
  captionsFailed,
2398
+ onCaptionTrack: chooseCaptionTrack,
2399
+ onSecondaryTrack: chooseSecondaryTrack,
2050
2400
  strings,
2051
2401
  onChange: changePreferences,
2052
2402
  onShowShortcuts: () => {
@@ -2227,7 +2577,10 @@ function Player(props) {
2227
2577
  onTab: setPanelTab,
2228
2578
  chapters,
2229
2579
  quizzes,
2230
- cues: captionCues,
2580
+ cues: primaryCues,
2581
+ primaryLanguage: primaryTrack?.srclang,
2582
+ secondaryCues: secondaryTrack === void 0 ? NO_CUES : secondaryCues,
2583
+ secondaryLanguage: secondaryTrack?.srclang,
2231
2584
  activeCue: activeCueIndex,
2232
2585
  limit: spoilerLimit,
2233
2586
  strings,
@@ -2268,7 +2621,7 @@ function QuestionPane({
2268
2621
  if (!hidden) {
2269
2622
  return;
2270
2623
  }
2271
- chunkZH5WT2IE_cjs.stopCaptureGroup(captureGroup);
2624
+ chunk2WM2KSO7_cjs.stopCaptureGroup(captureGroup);
2272
2625
  for (const media of ref.current?.querySelectorAll("audio, video") ?? []) {
2273
2626
  const element = media;
2274
2627
  if (!element.paused) {
@@ -2276,7 +2629,7 @@ function QuestionPane({
2276
2629
  }
2277
2630
  }
2278
2631
  }, [hidden, captureGroup]);
2279
- return /* @__PURE__ */ jsxRuntime.jsx("div", { ref, className: "lk-iv-question", hidden, children: /* @__PURE__ */ jsxRuntime.jsx(chunkZH5WT2IE_cjs.SequenceSlotContext.Provider, { value: channel, children }) });
2632
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { ref, className: "lk-iv-question", hidden, children: /* @__PURE__ */ jsxRuntime.jsx(chunk2WM2KSO7_cjs.SequenceSlotContext.Provider, { value: channel, children }) });
2280
2633
  }
2281
2634
  function bufferedAhead(video) {
2282
2635
  const ranges = video.buffered;
@@ -2316,9 +2669,9 @@ function markerState(quizSlots, answered, renderMode, outcomes) {
2316
2669
  return "unreached";
2317
2670
  }
2318
2671
  function InteractiveVideo2(props) {
2319
- const strings = chunkZ6TVOAO5_cjs.useLkStrings(props.strings);
2672
+ const strings = chunkUISZWFI7_cjs.useLkStrings(props.strings);
2320
2673
  return /* @__PURE__ */ jsxRuntime.jsx(
2321
- chunk53YOZOGN_cjs.ActivityErrorBoundary,
2674
+ chunkP5FGJMMI_cjs.ActivityErrorBoundary,
2322
2675
  {
2323
2676
  ...props.group?.title !== void 0 ? { activityTitle: props.group.title } : {},
2324
2677
  strings,
@@ -2328,5 +2681,6 @@ function InteractiveVideo2(props) {
2328
2681
  }
2329
2682
 
2330
2683
  exports.InteractiveVideo = InteractiveVideo2;
2331
- //# sourceMappingURL=chunk-SRD4KVAP.cjs.map
2332
- //# sourceMappingURL=chunk-SRD4KVAP.cjs.map
2684
+ exports.resolveCaptionTracks = resolveCaptionTracks;
2685
+ //# sourceMappingURL=chunk-A2CPCOJA.cjs.map
2686
+ //# sourceMappingURL=chunk-A2CPCOJA.cjs.map