@bendyline/squisq-react 2.4.0 → 2.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/{chunk-XYS7HMP4.js → chunk-EMSYAEWP.js} +33 -17
- package/dist/{chunk-HML6PFH2.js → chunk-R5XTAWQH.js} +586 -157
- package/dist/{chunk-M2HEFJZP.js → chunk-ZIMG7EQV.js} +1 -1
- package/dist/index.d.ts +57 -5
- package/dist/index.js +8 -4
- package/dist/layers/index.js +1 -1
- package/dist/page/index.js +2 -2
- package/dist/player/index.d.ts +22 -8
- package/dist/player/index.js +3 -3
- package/dist/squisq-player.full.global.js +457 -455
- package/dist/squisq-player.global.js +63 -61
- package/dist/standalone-source.js +1 -1
- package/dist/styles/index.css +18 -11
- package/package.json +3 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BlockRenderer,
|
|
3
3
|
LinearDocView
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-ZIMG7EQV.js";
|
|
5
5
|
import {
|
|
6
6
|
useAudioSync,
|
|
7
7
|
useDocPlayback,
|
|
@@ -12,13 +12,67 @@ import {
|
|
|
12
12
|
useAutoSurface
|
|
13
13
|
} from "./chunk-TT6ENR6T.js";
|
|
14
14
|
import {
|
|
15
|
-
|
|
15
|
+
useMediaProvider,
|
|
16
|
+
useMediaUrl,
|
|
17
|
+
useResourcePolicy
|
|
16
18
|
} from "./chunk-LR3AIGDD.js";
|
|
17
19
|
|
|
18
20
|
// src/MediaClipLayer.tsx
|
|
19
21
|
import { useEffect, useRef } from "react";
|
|
20
22
|
import { Fragment, jsx } from "react/jsx-runtime";
|
|
21
23
|
var DRIFT = 0.25;
|
|
24
|
+
function mediaGroupStyle(presentation) {
|
|
25
|
+
return {
|
|
26
|
+
position: "absolute",
|
|
27
|
+
inset: 0,
|
|
28
|
+
zIndex: presentation === "background" ? 0 : 10,
|
|
29
|
+
pointerEvents: "none"
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function pipCornerStyle(position) {
|
|
33
|
+
switch (position) {
|
|
34
|
+
case "top-left":
|
|
35
|
+
return { top: "4%", left: "3%" };
|
|
36
|
+
case "top-right":
|
|
37
|
+
return { top: "4%", right: "3%" };
|
|
38
|
+
case "bottom-left":
|
|
39
|
+
return { bottom: "6%", left: "3%" };
|
|
40
|
+
case "bottom-right":
|
|
41
|
+
return { right: "3%", bottom: "6%" };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function pipWidth(size, orientation) {
|
|
45
|
+
if (orientation === "portrait") return size === "large" ? "23%" : "15%";
|
|
46
|
+
return size === "large" ? "15%" : "9%";
|
|
47
|
+
}
|
|
48
|
+
function mediaVideoStyle({
|
|
49
|
+
presentation,
|
|
50
|
+
pipSize,
|
|
51
|
+
pipShape,
|
|
52
|
+
pipPosition,
|
|
53
|
+
pipOrientation,
|
|
54
|
+
pipFrameStyle,
|
|
55
|
+
active
|
|
56
|
+
}) {
|
|
57
|
+
const base = {
|
|
58
|
+
position: "absolute",
|
|
59
|
+
display: "block",
|
|
60
|
+
objectFit: "cover",
|
|
61
|
+
opacity: active ? 1 : 0,
|
|
62
|
+
visibility: active ? "visible" : "hidden",
|
|
63
|
+
pointerEvents: "none"
|
|
64
|
+
};
|
|
65
|
+
if (presentation !== "picture-in-picture") {
|
|
66
|
+
return { ...base, inset: 0, width: "100%", height: "100%" };
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
...base,
|
|
70
|
+
width: pipWidth(pipSize, pipOrientation),
|
|
71
|
+
aspectRatio: pipShape === "wide" ? "16 / 9" : "1",
|
|
72
|
+
...pipFrameStyle,
|
|
73
|
+
...pipCornerStyle(pipPosition)
|
|
74
|
+
};
|
|
75
|
+
}
|
|
22
76
|
function MediaClipLayer({
|
|
23
77
|
schedule,
|
|
24
78
|
currentTime,
|
|
@@ -27,8 +81,11 @@ function MediaClipLayer({
|
|
|
27
81
|
renderMode = false,
|
|
28
82
|
muted = false,
|
|
29
83
|
presentation = "background",
|
|
30
|
-
|
|
84
|
+
pipSize = "small",
|
|
85
|
+
pipShape = "square",
|
|
31
86
|
pipPosition = "bottom-right",
|
|
87
|
+
pipOrientation = "landscape",
|
|
88
|
+
pipFrameStyle,
|
|
32
89
|
honorClipPresentation = true
|
|
33
90
|
}) {
|
|
34
91
|
const { renderClips, activeIds } = useMediaSchedule(schedule, currentTime);
|
|
@@ -36,19 +93,31 @@ function MediaClipLayer({
|
|
|
36
93
|
const groups = /* @__PURE__ */ new Map();
|
|
37
94
|
for (const clip of renderClips) {
|
|
38
95
|
const clipPresentation = honorClipPresentation && clip.kind === "video" ? clip.placement === "picture-in-picture" ? "picture-in-picture" : clip.placement === "overlay" ? "full-frame" : presentation : presentation;
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
96
|
+
const clipPipSize = clip.pipSize ?? pipSize;
|
|
97
|
+
const clipPipShape = clip.pipShape ?? pipShape;
|
|
98
|
+
const clipPipPosition = clip.pipPosition ?? pipPosition;
|
|
99
|
+
const key = `${clipPresentation}:${clipPipSize}:${clipPipShape}:${clipPipPosition}`;
|
|
100
|
+
const group = groups.get(key) ?? {
|
|
101
|
+
presentation: clipPresentation,
|
|
102
|
+
pipSize: clipPipSize,
|
|
103
|
+
pipShape: clipPipShape,
|
|
104
|
+
pipPosition: clipPipPosition,
|
|
105
|
+
clips: []
|
|
106
|
+
};
|
|
107
|
+
group.clips.push(clip);
|
|
108
|
+
groups.set(key, group);
|
|
42
109
|
}
|
|
43
|
-
return /* @__PURE__ */ jsx(Fragment, { children: [...groups].map(([
|
|
110
|
+
return /* @__PURE__ */ jsx(Fragment, { children: [...groups].map(([key, group]) => /* @__PURE__ */ jsx(
|
|
44
111
|
"div",
|
|
45
112
|
{
|
|
46
|
-
className: `doc-player__media-clips doc-player__media-clips--${
|
|
47
|
-
"data-presentation":
|
|
48
|
-
"data-pip-
|
|
49
|
-
"data-pip-
|
|
113
|
+
className: `doc-player__media-clips doc-player__media-clips--${group.presentation}`,
|
|
114
|
+
"data-presentation": group.presentation,
|
|
115
|
+
"data-pip-size": group.pipSize,
|
|
116
|
+
"data-pip-shape": group.pipShape,
|
|
117
|
+
"data-pip-position": group.pipPosition,
|
|
50
118
|
"aria-hidden": true,
|
|
51
|
-
|
|
119
|
+
style: mediaGroupStyle(group.presentation),
|
|
120
|
+
children: group.clips.map((clip) => /* @__PURE__ */ jsx(
|
|
52
121
|
MediaClipElement,
|
|
53
122
|
{
|
|
54
123
|
clip,
|
|
@@ -57,12 +126,18 @@ function MediaClipLayer({
|
|
|
57
126
|
isPlaying,
|
|
58
127
|
basePath,
|
|
59
128
|
renderMode,
|
|
60
|
-
muted
|
|
129
|
+
muted,
|
|
130
|
+
presentation: group.presentation,
|
|
131
|
+
pipSize: group.pipSize,
|
|
132
|
+
pipShape: group.pipShape,
|
|
133
|
+
pipPosition: group.pipPosition,
|
|
134
|
+
pipOrientation,
|
|
135
|
+
pipFrameStyle
|
|
61
136
|
},
|
|
62
137
|
clip.id
|
|
63
138
|
))
|
|
64
139
|
},
|
|
65
|
-
|
|
140
|
+
key
|
|
66
141
|
)) });
|
|
67
142
|
}
|
|
68
143
|
function MediaClipElement({
|
|
@@ -72,7 +147,13 @@ function MediaClipElement({
|
|
|
72
147
|
isPlaying,
|
|
73
148
|
basePath,
|
|
74
149
|
renderMode,
|
|
75
|
-
muted
|
|
150
|
+
muted,
|
|
151
|
+
presentation,
|
|
152
|
+
pipSize,
|
|
153
|
+
pipShape,
|
|
154
|
+
pipPosition,
|
|
155
|
+
pipOrientation,
|
|
156
|
+
pipFrameStyle
|
|
76
157
|
}) {
|
|
77
158
|
const ref = useRef(null);
|
|
78
159
|
const src = useMediaUrl(clip.src, basePath);
|
|
@@ -118,7 +199,15 @@ function MediaClipElement({
|
|
|
118
199
|
"data-video-placement": clip.placement ?? "default",
|
|
119
200
|
muted: renderMode || muted,
|
|
120
201
|
playsInline: true,
|
|
121
|
-
style: {
|
|
202
|
+
style: mediaVideoStyle({
|
|
203
|
+
presentation,
|
|
204
|
+
pipSize,
|
|
205
|
+
pipShape,
|
|
206
|
+
pipPosition,
|
|
207
|
+
pipOrientation,
|
|
208
|
+
pipFrameStyle,
|
|
209
|
+
active
|
|
210
|
+
})
|
|
122
211
|
}
|
|
123
212
|
);
|
|
124
213
|
}
|
|
@@ -132,8 +221,123 @@ function MediaClipElement({
|
|
|
132
221
|
);
|
|
133
222
|
}
|
|
134
223
|
|
|
224
|
+
// src/hooks/useMediaClipDurations.ts
|
|
225
|
+
import { useEffect as useEffect2, useMemo, useState } from "react";
|
|
226
|
+
import { isResourceUrlAllowed } from "@bendyline/squisq/markdown";
|
|
227
|
+
var durationCache = /* @__PURE__ */ new Map();
|
|
228
|
+
var PROBE_TIMEOUT_MS = 8e3;
|
|
229
|
+
async function resolveMediaUrl(src, provider, basePath, policy) {
|
|
230
|
+
const isAbsolute = !src || /^(?:[a-z][a-z0-9+.-]*:|\/\/|\/)/i.test(src);
|
|
231
|
+
const fallback = isAbsolute ? src : `${basePath.replace(/\/$/, "")}/${src}`;
|
|
232
|
+
const gate = (url) => isResourceUrlAllowed(url, policy) ? url : "";
|
|
233
|
+
if (isAbsolute || !provider) return gate(fallback);
|
|
234
|
+
try {
|
|
235
|
+
return gate(await provider.resolveUrl(src));
|
|
236
|
+
} catch {
|
|
237
|
+
return gate(fallback);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function probeVideoDuration(url) {
|
|
241
|
+
if (typeof document === "undefined") return Promise.resolve(null);
|
|
242
|
+
return new Promise((resolve) => {
|
|
243
|
+
const el = document.createElement("video");
|
|
244
|
+
el.preload = "metadata";
|
|
245
|
+
el.muted = true;
|
|
246
|
+
el.setAttribute("playsinline", "");
|
|
247
|
+
let settled = false;
|
|
248
|
+
const timeout = {};
|
|
249
|
+
const finish = (seconds) => {
|
|
250
|
+
if (settled) return;
|
|
251
|
+
settled = true;
|
|
252
|
+
if (timeout.id !== void 0) clearTimeout(timeout.id);
|
|
253
|
+
el.removeEventListener("durationchange", onDurationProbe);
|
|
254
|
+
el.removeEventListener("timeupdate", onDurationProbe);
|
|
255
|
+
el.removeAttribute("src");
|
|
256
|
+
try {
|
|
257
|
+
el.load();
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
260
|
+
resolve(seconds);
|
|
261
|
+
};
|
|
262
|
+
const readFinite = () => {
|
|
263
|
+
const d = el.duration;
|
|
264
|
+
if (Number.isFinite(d) && d > 0) {
|
|
265
|
+
finish(d);
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
return false;
|
|
269
|
+
};
|
|
270
|
+
const onDurationProbe = () => {
|
|
271
|
+
readFinite();
|
|
272
|
+
};
|
|
273
|
+
el.addEventListener(
|
|
274
|
+
"loadedmetadata",
|
|
275
|
+
() => {
|
|
276
|
+
if (readFinite()) return;
|
|
277
|
+
el.addEventListener("durationchange", onDurationProbe);
|
|
278
|
+
el.addEventListener("timeupdate", onDurationProbe);
|
|
279
|
+
try {
|
|
280
|
+
el.currentTime = 1e101;
|
|
281
|
+
} catch {
|
|
282
|
+
finish(null);
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
{ once: true }
|
|
286
|
+
);
|
|
287
|
+
el.addEventListener("error", () => finish(null), { once: true });
|
|
288
|
+
timeout.id = setTimeout(() => finish(null), PROBE_TIMEOUT_MS);
|
|
289
|
+
el.src = url;
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
function useMediaClipDurations(schedule, basePath, mediaProviderOverride) {
|
|
293
|
+
const contextProvider = useMediaProvider();
|
|
294
|
+
const provider = mediaProviderOverride !== void 0 ? mediaProviderOverride : contextProvider;
|
|
295
|
+
const resourcePolicy = useResourcePolicy();
|
|
296
|
+
const srcKey = useMemo(() => {
|
|
297
|
+
const set = /* @__PURE__ */ new Set();
|
|
298
|
+
for (const clip of schedule) {
|
|
299
|
+
if (clip.kind === "video" && clip.src) set.add(clip.src);
|
|
300
|
+
}
|
|
301
|
+
return Array.from(set).sort().join("\n");
|
|
302
|
+
}, [schedule]);
|
|
303
|
+
const [durations, setDurations] = useState(() => /* @__PURE__ */ new Map());
|
|
304
|
+
useEffect2(() => {
|
|
305
|
+
const srcs = srcKey ? srcKey.split("\n") : [];
|
|
306
|
+
if (srcs.length === 0) {
|
|
307
|
+
setDurations((prev) => prev.size === 0 ? prev : /* @__PURE__ */ new Map());
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
let cancelled = false;
|
|
311
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
312
|
+
const commit = () => {
|
|
313
|
+
if (!cancelled) setDurations(new Map(resolved));
|
|
314
|
+
};
|
|
315
|
+
for (const src of srcs) {
|
|
316
|
+
resolveMediaUrl(src, provider, basePath, resourcePolicy).then((url) => {
|
|
317
|
+
if (cancelled || !url) return;
|
|
318
|
+
const cached = durationCache.get(url);
|
|
319
|
+
if (cached != null) {
|
|
320
|
+
resolved.set(src, cached);
|
|
321
|
+
commit();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
probeVideoDuration(url).then((seconds) => {
|
|
325
|
+
if (cancelled || seconds == null) return;
|
|
326
|
+
durationCache.set(url, seconds);
|
|
327
|
+
resolved.set(src, seconds);
|
|
328
|
+
commit();
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
return () => {
|
|
333
|
+
cancelled = true;
|
|
334
|
+
};
|
|
335
|
+
}, [srcKey, provider, basePath, resourcePolicy]);
|
|
336
|
+
return durations;
|
|
337
|
+
}
|
|
338
|
+
|
|
135
339
|
// src/SocialCaptionOverlay.tsx
|
|
136
|
-
import { useMemo } from "react";
|
|
340
|
+
import { useMemo as useMemo2 } from "react";
|
|
137
341
|
import { resolveFontFamily } from "@bendyline/squisq/schemas";
|
|
138
342
|
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
139
343
|
var TARGET_CHUNK_SIZE = 4;
|
|
@@ -189,7 +393,7 @@ function SocialCaptionOverlay({
|
|
|
189
393
|
theme,
|
|
190
394
|
viewport
|
|
191
395
|
}) {
|
|
192
|
-
const { chunks } =
|
|
396
|
+
const { chunks } = useMemo2(
|
|
193
397
|
() => captions ? buildWordStream(captions) : { words: [], chunks: [] },
|
|
194
398
|
[captions]
|
|
195
399
|
);
|
|
@@ -391,7 +595,7 @@ function formatTime(seconds) {
|
|
|
391
595
|
}
|
|
392
596
|
|
|
393
597
|
// src/DocProgressBar.tsx
|
|
394
|
-
import { useRef as useRef2, useState, useCallback } from "react";
|
|
598
|
+
import { useRef as useRef2, useState as useState2, useCallback } from "react";
|
|
395
599
|
import { jsx as jsx4, jsxs } from "react/jsx-runtime";
|
|
396
600
|
function DocProgressBar({
|
|
397
601
|
state,
|
|
@@ -401,7 +605,7 @@ function DocProgressBar({
|
|
|
401
605
|
getBlockTitle
|
|
402
606
|
}) {
|
|
403
607
|
const progressBarRef = useRef2(null);
|
|
404
|
-
const [hoverPosition, setHoverPosition] =
|
|
608
|
+
const [hoverPosition, setHoverPosition] = useState2(null);
|
|
405
609
|
const playProgress = state.totalDuration > 0 ? Math.max(0, Math.min(1, state.currentTime / state.totalDuration)) : 0;
|
|
406
610
|
const handleProgressHover = useCallback((e) => {
|
|
407
611
|
const bar = progressBarRef.current;
|
|
@@ -728,7 +932,7 @@ function DocControlsOverlay({
|
|
|
728
932
|
}
|
|
729
933
|
|
|
730
934
|
// src/DocControlsSlideshow.tsx
|
|
731
|
-
import { useCallback as useCallback2, useEffect as
|
|
935
|
+
import { useCallback as useCallback2, useEffect as useEffect3, useId, useLayoutEffect, useRef as useRef3, useState as useState3 } from "react";
|
|
732
936
|
import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
733
937
|
function DocControlsSlideshow({
|
|
734
938
|
state,
|
|
@@ -737,7 +941,7 @@ function DocControlsSlideshow({
|
|
|
737
941
|
pickerOpen,
|
|
738
942
|
onPickerOpenChange
|
|
739
943
|
}) {
|
|
740
|
-
const [uncontrolledPickerOpen, setUncontrolledPickerOpen] =
|
|
944
|
+
const [uncontrolledPickerOpen, setUncontrolledPickerOpen] = useState3(false);
|
|
741
945
|
const isPickerOpen = pickerOpen ?? uncontrolledPickerOpen;
|
|
742
946
|
const setPickerOpen = useCallback2(
|
|
743
947
|
(open) => {
|
|
@@ -746,7 +950,7 @@ function DocControlsSlideshow({
|
|
|
746
950
|
},
|
|
747
951
|
[pickerOpen, onPickerOpenChange]
|
|
748
952
|
);
|
|
749
|
-
const [pickerMaxHeight, setPickerMaxHeight] =
|
|
953
|
+
const [pickerMaxHeight, setPickerMaxHeight] = useState3(280);
|
|
750
954
|
const controlsRef = useRef3(null);
|
|
751
955
|
const triggerRef = useRef3(null);
|
|
752
956
|
const menuRef = useRef3(null);
|
|
@@ -761,7 +965,7 @@ function DocControlsSlideshow({
|
|
|
761
965
|
const isFirst = currentBlockIndex <= 0;
|
|
762
966
|
const isLast = currentBlockIndex >= totalBlocks - 1;
|
|
763
967
|
const counterText = totalBlocks > 0 ? currentSlideLabel ?? `${currentSlideNumber ?? currentBlockIndex + 1} / ${totalSlideNumber ?? totalBlocks}` : "\u2014";
|
|
764
|
-
|
|
968
|
+
useEffect3(() => {
|
|
765
969
|
if (!isPickerOpen) return;
|
|
766
970
|
const handlePointerDown = (event) => {
|
|
767
971
|
if (!controlsRef.current?.contains(event.target)) setPickerOpen(false);
|
|
@@ -800,7 +1004,7 @@ function DocControlsSlideshow({
|
|
|
800
1004
|
window.removeEventListener("resize", updateMaxHeight);
|
|
801
1005
|
};
|
|
802
1006
|
}, [isPickerOpen]);
|
|
803
|
-
|
|
1007
|
+
useEffect3(() => {
|
|
804
1008
|
if (!isPickerOpen) return;
|
|
805
1009
|
menuRef.current?.querySelector('[aria-current="true"]')?.focus();
|
|
806
1010
|
}, [isPickerOpen]);
|
|
@@ -1038,18 +1242,105 @@ function DocControlsSlideshow({
|
|
|
1038
1242
|
);
|
|
1039
1243
|
}
|
|
1040
1244
|
|
|
1245
|
+
// src/docPlayer/playerAppearance.ts
|
|
1246
|
+
import { resolveThemeForDoc } from "@bendyline/squisq/doc";
|
|
1247
|
+
function readFrontmatterSetting(frontmatter, canonical, legacy) {
|
|
1248
|
+
if (!frontmatter) return void 0;
|
|
1249
|
+
return Object.prototype.hasOwnProperty.call(frontmatter, canonical) ? frontmatter[canonical] : frontmatter[legacy];
|
|
1250
|
+
}
|
|
1251
|
+
function resolveVideoPresentation(value) {
|
|
1252
|
+
if (typeof value !== "string") return void 0;
|
|
1253
|
+
const normalized = value.trim().toLowerCase();
|
|
1254
|
+
if (normalized === "background" || normalized === "behind") return "background";
|
|
1255
|
+
if (normalized === "full-frame" || normalized === "fullscreen" || normalized === "full") {
|
|
1256
|
+
return "full-frame";
|
|
1257
|
+
}
|
|
1258
|
+
if (normalized === "picture-in-picture" || normalized === "pip" || normalized === "overlay") {
|
|
1259
|
+
return "picture-in-picture";
|
|
1260
|
+
}
|
|
1261
|
+
return void 0;
|
|
1262
|
+
}
|
|
1263
|
+
function resolvePipSize(value) {
|
|
1264
|
+
if (typeof value !== "string") return void 0;
|
|
1265
|
+
const normalized = value.trim().toLowerCase();
|
|
1266
|
+
if (normalized === "small" || normalized === "sm" || normalized === "pip-small") return "small";
|
|
1267
|
+
if (normalized === "large" || normalized === "lg" || normalized === "big" || normalized === "pip-large") {
|
|
1268
|
+
return "large";
|
|
1269
|
+
}
|
|
1270
|
+
return void 0;
|
|
1271
|
+
}
|
|
1272
|
+
function resolvePipShape(value) {
|
|
1273
|
+
if (typeof value !== "string") return void 0;
|
|
1274
|
+
const normalized = value.trim().toLowerCase();
|
|
1275
|
+
if (normalized === "square" || normalized === "1:1") return "square";
|
|
1276
|
+
if (normalized === "wide" || normalized === "16:9" || normalized === "widescreen") return "wide";
|
|
1277
|
+
return void 0;
|
|
1278
|
+
}
|
|
1279
|
+
function resolvePipPosition(value) {
|
|
1280
|
+
if (typeof value !== "string") return void 0;
|
|
1281
|
+
const normalized = value.trim().toLowerCase().replace(/[_\s]+/g, "-");
|
|
1282
|
+
const aliases = {
|
|
1283
|
+
"top-left": "top-left",
|
|
1284
|
+
"upper-left": "top-left",
|
|
1285
|
+
"top-right": "top-right",
|
|
1286
|
+
"upper-right": "top-right",
|
|
1287
|
+
"bottom-left": "bottom-left",
|
|
1288
|
+
"lower-left": "bottom-left",
|
|
1289
|
+
"bottom-right": "bottom-right",
|
|
1290
|
+
"lower-right": "bottom-right"
|
|
1291
|
+
};
|
|
1292
|
+
return aliases[normalized];
|
|
1293
|
+
}
|
|
1294
|
+
function resolveBoolean(value) {
|
|
1295
|
+
if (typeof value === "boolean") return value;
|
|
1296
|
+
if (typeof value !== "string") return void 0;
|
|
1297
|
+
const normalized = value.trim().toLowerCase();
|
|
1298
|
+
if (normalized === "true" || normalized === "yes" || normalized === "on" || normalized === "show" || normalized === "visible") {
|
|
1299
|
+
return true;
|
|
1300
|
+
}
|
|
1301
|
+
if (normalized === "false" || normalized === "no" || normalized === "off" || normalized === "hide" || normalized === "hidden") {
|
|
1302
|
+
return false;
|
|
1303
|
+
}
|
|
1304
|
+
return void 0;
|
|
1305
|
+
}
|
|
1306
|
+
function resolveDocPlayerAppearance(doc, overrides = {}) {
|
|
1307
|
+
const frontmatter = doc.frontmatter;
|
|
1308
|
+
return {
|
|
1309
|
+
theme: overrides.theme ?? resolveThemeForDoc(doc),
|
|
1310
|
+
videoPresentation: overrides.videoPresentation ?? resolveVideoPresentation(
|
|
1311
|
+
readFrontmatterSetting(frontmatter, "squisq-video-presentation", "video-presentation")
|
|
1312
|
+
) ?? "background",
|
|
1313
|
+
pipSize: overrides.pipSize ?? resolvePipSize(readFrontmatterSetting(frontmatter, "squisq-pip-size", "pip-size")) ?? "small",
|
|
1314
|
+
pipShape: overrides.pipShape ?? resolvePipShape(readFrontmatterSetting(frontmatter, "squisq-pip-shape", "pip-shape")) ?? "square",
|
|
1315
|
+
pipPosition: overrides.pipPosition ?? resolvePipPosition(
|
|
1316
|
+
readFrontmatterSetting(frontmatter, "squisq-pip-position", "pip-position")
|
|
1317
|
+
) ?? "bottom-right",
|
|
1318
|
+
showCoverSlide: overrides.showCoverSlide ?? resolveBoolean(readFrontmatterSetting(frontmatter, "squisq-cover-slide", "cover-slide")) ?? true
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1041
1322
|
// src/DocPlayer.tsx
|
|
1042
|
-
import {
|
|
1323
|
+
import {
|
|
1324
|
+
Fragment as Fragment2,
|
|
1325
|
+
useId as useId2,
|
|
1326
|
+
useRef as useRef5,
|
|
1327
|
+
useState as useState5,
|
|
1328
|
+
useEffect as useEffect5,
|
|
1329
|
+
useLayoutEffect as useLayoutEffect2,
|
|
1330
|
+
useCallback as useCallback4,
|
|
1331
|
+
useMemo as useMemo3
|
|
1332
|
+
} from "react";
|
|
1333
|
+
import { flushSync } from "react-dom";
|
|
1043
1334
|
import {
|
|
1044
1335
|
isTemplateBlock as isTemplateBlock2,
|
|
1045
1336
|
getCaptionAtTime as getCaptionAtTime2,
|
|
1046
1337
|
resolveMediaSchedule,
|
|
1047
1338
|
getDocPlaybackDuration
|
|
1048
1339
|
} from "@bendyline/squisq/schemas";
|
|
1049
|
-
import { applySurface } from "@bendyline/squisq/schemas";
|
|
1340
|
+
import { applySurface, pipStyleVars } from "@bendyline/squisq/schemas";
|
|
1050
1341
|
|
|
1051
1342
|
// src/hooks/useSlideSwipe.ts
|
|
1052
|
-
import { useCallback as useCallback3, useEffect as
|
|
1343
|
+
import { useCallback as useCallback3, useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
|
|
1053
1344
|
var DISTANCE_RATIO = 0.3;
|
|
1054
1345
|
var FLICK_VELOCITY = 0.5;
|
|
1055
1346
|
var MIN_FLICK_DISTANCE = 12;
|
|
@@ -1073,8 +1364,8 @@ function decideSwipe({
|
|
|
1073
1364
|
}
|
|
1074
1365
|
function useSlideSwipe(opts) {
|
|
1075
1366
|
const settleMs = opts.settleMs ?? DEFAULT_SETTLE_MS;
|
|
1076
|
-
const [offsetPx, setOffsetPx] =
|
|
1077
|
-
const [phase, setPhase] =
|
|
1367
|
+
const [offsetPx, setOffsetPx] = useState4(0);
|
|
1368
|
+
const [phase, setPhase] = useState4("idle");
|
|
1078
1369
|
const optsRef = useRef4(opts);
|
|
1079
1370
|
optsRef.current = opts;
|
|
1080
1371
|
const dragRef = useRef4(null);
|
|
@@ -1112,7 +1403,7 @@ function useSlideSwipe(opts) {
|
|
|
1112
1403
|
setPhase("dragging");
|
|
1113
1404
|
setOffsetPx(0);
|
|
1114
1405
|
}, []);
|
|
1115
|
-
|
|
1406
|
+
useEffect4(() => {
|
|
1116
1407
|
function currentWidth() {
|
|
1117
1408
|
return optsRef.current.containerRef.current?.getBoundingClientRect().width ?? 0;
|
|
1118
1409
|
}
|
|
@@ -1182,7 +1473,7 @@ function useSlideSwipe(opts) {
|
|
|
1182
1473
|
window.removeEventListener("pointercancel", onCancel);
|
|
1183
1474
|
};
|
|
1184
1475
|
}, [settleMs]);
|
|
1185
|
-
|
|
1476
|
+
useEffect4(() => {
|
|
1186
1477
|
if (!opts.enabled) {
|
|
1187
1478
|
dragRef.current = null;
|
|
1188
1479
|
clearPending();
|
|
@@ -1190,7 +1481,7 @@ function useSlideSwipe(opts) {
|
|
|
1190
1481
|
setOffsetPx(0);
|
|
1191
1482
|
}
|
|
1192
1483
|
}, [opts.enabled, clearPending]);
|
|
1193
|
-
|
|
1484
|
+
useEffect4(() => clearPending, [clearPending]);
|
|
1194
1485
|
return { offsetPx, phase, onPointerDown };
|
|
1195
1486
|
}
|
|
1196
1487
|
|
|
@@ -1199,7 +1490,6 @@ import {
|
|
|
1199
1490
|
expandCoverBlock,
|
|
1200
1491
|
createTemplateContext,
|
|
1201
1492
|
markdownToDoc,
|
|
1202
|
-
DEFAULT_THEME,
|
|
1203
1493
|
VIEWPORT_PRESETS
|
|
1204
1494
|
} from "@bendyline/squisq/doc";
|
|
1205
1495
|
import { parseMarkdown } from "@bendyline/squisq/markdown";
|
|
@@ -1248,6 +1538,86 @@ function buildSegmentTitleMap(doc) {
|
|
|
1248
1538
|
return map;
|
|
1249
1539
|
}
|
|
1250
1540
|
|
|
1541
|
+
// src/docPlayer/renderReadiness.ts
|
|
1542
|
+
var DEFAULT_VIDEO_FRAME_TIMEOUT_MS = 2e3;
|
|
1543
|
+
var VIDEO_TIME_TOLERANCE_SECONDS = 0.01;
|
|
1544
|
+
function formatMediaTime(time) {
|
|
1545
|
+
return Number.isFinite(time) ? `${time.toFixed(3)}s` : String(time);
|
|
1546
|
+
}
|
|
1547
|
+
function isVisiblyPresented(video) {
|
|
1548
|
+
if (!video.isConnected) return false;
|
|
1549
|
+
const view = video.ownerDocument.defaultView;
|
|
1550
|
+
if (!view) return false;
|
|
1551
|
+
for (let element = video; element; element = element.parentElement) {
|
|
1552
|
+
const style = view.getComputedStyle(element);
|
|
1553
|
+
if (style.display === "none" || style.visibility === "hidden" || style.visibility === "collapse" || Number.parseFloat(style.opacity || "1") <= 0) {
|
|
1554
|
+
return false;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
return true;
|
|
1558
|
+
}
|
|
1559
|
+
function seekVideoToFrame(video, targetTime, timeoutMs = DEFAULT_VIDEO_FRAME_TIMEOUT_MS) {
|
|
1560
|
+
video.pause();
|
|
1561
|
+
const alreadyReady = !video.seeking && video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && Math.abs(video.currentTime - targetTime) <= VIDEO_TIME_TOLERANCE_SECONDS;
|
|
1562
|
+
if (alreadyReady) return Promise.resolve();
|
|
1563
|
+
return new Promise((resolve, reject) => {
|
|
1564
|
+
let settled = false;
|
|
1565
|
+
let videoFrameRequest = null;
|
|
1566
|
+
const cleanup = () => {
|
|
1567
|
+
clearTimeout(timeout);
|
|
1568
|
+
video.removeEventListener("seeked", handleMediaReady);
|
|
1569
|
+
video.removeEventListener("loadeddata", handleMediaReady);
|
|
1570
|
+
video.removeEventListener("canplay", handleMediaReady);
|
|
1571
|
+
if (videoFrameRequest !== null && typeof video.cancelVideoFrameCallback === "function") {
|
|
1572
|
+
video.cancelVideoFrameCallback(videoFrameRequest);
|
|
1573
|
+
}
|
|
1574
|
+
};
|
|
1575
|
+
const finish = () => {
|
|
1576
|
+
if (settled) return;
|
|
1577
|
+
settled = true;
|
|
1578
|
+
cleanup();
|
|
1579
|
+
resolve();
|
|
1580
|
+
};
|
|
1581
|
+
const fail = () => {
|
|
1582
|
+
if (settled) return;
|
|
1583
|
+
settled = true;
|
|
1584
|
+
cleanup();
|
|
1585
|
+
reject(
|
|
1586
|
+
new Error(
|
|
1587
|
+
`Video frame did not become ready at ${formatMediaTime(targetTime)} within ${timeoutMs}ms (currentTime=${formatMediaTime(video.currentTime)}, readyState=${video.readyState}, seeking=${String(video.seeking)}, visible=${String(isVisiblyPresented(video))}).`
|
|
1588
|
+
)
|
|
1589
|
+
);
|
|
1590
|
+
};
|
|
1591
|
+
const requestPresentedFrame = () => {
|
|
1592
|
+
if (settled || video.seeking || video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) return;
|
|
1593
|
+
if (typeof video.requestVideoFrameCallback !== "function" || !isVisiblyPresented(video)) {
|
|
1594
|
+
finish();
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
if (videoFrameRequest !== null) return;
|
|
1598
|
+
videoFrameRequest = video.requestVideoFrameCallback(() => {
|
|
1599
|
+
videoFrameRequest = null;
|
|
1600
|
+
finish();
|
|
1601
|
+
});
|
|
1602
|
+
};
|
|
1603
|
+
function handleMediaReady() {
|
|
1604
|
+
requestPresentedFrame();
|
|
1605
|
+
}
|
|
1606
|
+
const timeout = setTimeout(fail, timeoutMs);
|
|
1607
|
+
video.addEventListener("seeked", handleMediaReady);
|
|
1608
|
+
video.addEventListener("loadeddata", handleMediaReady);
|
|
1609
|
+
video.addEventListener("canplay", handleMediaReady);
|
|
1610
|
+
try {
|
|
1611
|
+
video.currentTime = targetTime;
|
|
1612
|
+
queueMicrotask(requestPresentedFrame);
|
|
1613
|
+
} catch (error) {
|
|
1614
|
+
settled = true;
|
|
1615
|
+
cleanup();
|
|
1616
|
+
reject(error);
|
|
1617
|
+
}
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1251
1621
|
// src/DocPlayer.tsx
|
|
1252
1622
|
import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1253
1623
|
function isDevEnvironment() {
|
|
@@ -1259,6 +1629,7 @@ function isDevEnvironment() {
|
|
|
1259
1629
|
}
|
|
1260
1630
|
var warnedMissingStyles = false;
|
|
1261
1631
|
var VISUAL_UPDATE_FALLBACK_MS = 100;
|
|
1632
|
+
var RENDER_TIME_EPSILON_SECONDS = 1e-6;
|
|
1262
1633
|
function waitForVisualUpdate() {
|
|
1263
1634
|
return new Promise((resolve) => {
|
|
1264
1635
|
let settled = false;
|
|
@@ -1280,7 +1651,7 @@ function waitForVisualUpdate() {
|
|
|
1280
1651
|
}
|
|
1281
1652
|
function DocPlayer(props) {
|
|
1282
1653
|
const { doc, markdown } = props;
|
|
1283
|
-
const markdownDoc =
|
|
1654
|
+
const markdownDoc = useMemo3(
|
|
1284
1655
|
() => !doc && markdown !== void 0 ? markdownToDoc(parseMarkdown(markdown)) : void 0,
|
|
1285
1656
|
[doc, markdown]
|
|
1286
1657
|
);
|
|
@@ -1314,14 +1685,15 @@ function DocPlayerContent({
|
|
|
1314
1685
|
onBlockMarkers,
|
|
1315
1686
|
forceViewport,
|
|
1316
1687
|
displayMode = "video",
|
|
1317
|
-
showCoverSlide
|
|
1688
|
+
showCoverSlide,
|
|
1318
1689
|
coverVisible,
|
|
1319
1690
|
theme,
|
|
1320
1691
|
surface,
|
|
1321
1692
|
captionStyle = "standard",
|
|
1322
|
-
videoPresentation
|
|
1323
|
-
|
|
1324
|
-
|
|
1693
|
+
videoPresentation,
|
|
1694
|
+
pipSize,
|
|
1695
|
+
pipShape,
|
|
1696
|
+
pipPosition,
|
|
1325
1697
|
enableSwipe = true,
|
|
1326
1698
|
globalKeyboardShortcuts = false
|
|
1327
1699
|
}) {
|
|
@@ -1330,11 +1702,12 @@ function DocPlayerContent({
|
|
|
1330
1702
|
const audioRef = useRef5(null);
|
|
1331
1703
|
const containerRef = useRef5(null);
|
|
1332
1704
|
const playerId = `squisq-player-${useId2().replace(/:/g, "")}`;
|
|
1333
|
-
const [tapFeedback, setTapFeedback] =
|
|
1705
|
+
const [tapFeedback, setTapFeedback] = useState5(null);
|
|
1334
1706
|
const tapFeedbackTimer = useRef5();
|
|
1335
|
-
const { viewport
|
|
1707
|
+
const { viewport } = useViewportOrientation();
|
|
1336
1708
|
const activeViewport = forceViewport || (renderMode ? VIEWPORT_PRESETS.landscape : viewport);
|
|
1337
|
-
const
|
|
1709
|
+
const activeOrientation = activeViewport.height > activeViewport.width ? "portrait" : "landscape";
|
|
1710
|
+
const isDebugMode = useMemo3(() => {
|
|
1338
1711
|
if (typeof window === "undefined") return false;
|
|
1339
1712
|
const params = new URLSearchParams(window.location.search);
|
|
1340
1713
|
return params.get("debug") === "true";
|
|
@@ -1347,7 +1720,7 @@ function DocPlayerContent({
|
|
|
1347
1720
|
audioMode
|
|
1348
1721
|
);
|
|
1349
1722
|
const audio = externalAudioController || internalAudio;
|
|
1350
|
-
|
|
1723
|
+
useEffect5(() => {
|
|
1351
1724
|
if (warnedMissingStyles || !isDevEnvironment()) return;
|
|
1352
1725
|
const el = containerRef.current;
|
|
1353
1726
|
if (!el || typeof getComputedStyle !== "function") return;
|
|
@@ -1360,7 +1733,7 @@ function DocPlayerContent({
|
|
|
1360
1733
|
}
|
|
1361
1734
|
}, []);
|
|
1362
1735
|
const {
|
|
1363
|
-
currentTime,
|
|
1736
|
+
currentTime: audioCurrentTime,
|
|
1364
1737
|
isPlaying,
|
|
1365
1738
|
currentSegment,
|
|
1366
1739
|
totalDuration,
|
|
@@ -1375,12 +1748,46 @@ function DocPlayerContent({
|
|
|
1375
1748
|
skipToSegment: _skipToSegment,
|
|
1376
1749
|
restart
|
|
1377
1750
|
} = audio;
|
|
1378
|
-
const
|
|
1751
|
+
const [renderClock, setRenderClock] = useState5(null);
|
|
1752
|
+
const renderTimeOverride = renderClock?.doc === doc ? renderClock.time : null;
|
|
1753
|
+
const currentTime = renderMode ? renderTimeOverride ?? audioCurrentTime : audioCurrentTime;
|
|
1754
|
+
const rawSchedule = useMemo3(() => resolveMediaSchedule(doc), [doc]);
|
|
1755
|
+
const clipDurations = useMediaClipDurations(rawSchedule, basePath);
|
|
1756
|
+
const mediaSchedule = useMemo3(
|
|
1757
|
+
() => resolveMediaSchedule(doc, { intrinsicDuration: (clip) => clipDurations.get(clip.src) }),
|
|
1758
|
+
[doc, clipDurations]
|
|
1759
|
+
);
|
|
1379
1760
|
const currentTimeRef = useRef5(currentTime);
|
|
1380
1761
|
currentTimeRef.current = currentTime;
|
|
1762
|
+
const committedRenderTimeRef = useRef5(currentTime);
|
|
1763
|
+
const renderCommitWaitersRef = useRef5([]);
|
|
1381
1764
|
const totalDurationRef = useRef5(totalDuration);
|
|
1382
1765
|
totalDurationRef.current = totalDuration;
|
|
1383
1766
|
const expandedBlocksLenRef = useRef5(0);
|
|
1767
|
+
useLayoutEffect2(() => {
|
|
1768
|
+
committedRenderTimeRef.current = currentTime;
|
|
1769
|
+
const pending = renderCommitWaitersRef.current;
|
|
1770
|
+
renderCommitWaitersRef.current = pending.filter((waiter) => {
|
|
1771
|
+
if (Math.abs(waiter.time - currentTime) > RENDER_TIME_EPSILON_SECONDS) return true;
|
|
1772
|
+
waiter.resolve();
|
|
1773
|
+
return false;
|
|
1774
|
+
});
|
|
1775
|
+
}, [currentTime]);
|
|
1776
|
+
useEffect5(
|
|
1777
|
+
() => () => {
|
|
1778
|
+
const error = new Error("DocPlayer unmounted before the requested frame committed.");
|
|
1779
|
+
renderCommitWaitersRef.current.splice(0).forEach((waiter) => waiter.reject(error));
|
|
1780
|
+
},
|
|
1781
|
+
[]
|
|
1782
|
+
);
|
|
1783
|
+
const waitForRenderCommit = useCallback4((time) => {
|
|
1784
|
+
if (Math.abs(committedRenderTimeRef.current - time) <= RENDER_TIME_EPSILON_SECONDS) {
|
|
1785
|
+
return Promise.resolve();
|
|
1786
|
+
}
|
|
1787
|
+
return new Promise((resolve, reject) => {
|
|
1788
|
+
renderCommitWaitersRef.current.push({ time, resolve, reject });
|
|
1789
|
+
});
|
|
1790
|
+
}, []);
|
|
1384
1791
|
const handleContainerClick = useCallback4(
|
|
1385
1792
|
(e) => {
|
|
1386
1793
|
if (renderMode || isLinearMode) return;
|
|
@@ -1406,10 +1813,31 @@ function DocPlayerContent({
|
|
|
1406
1813
|
);
|
|
1407
1814
|
const autoSurface = useAutoSurface(surface === "auto");
|
|
1408
1815
|
const resolvedSurface = surface === "auto" ? autoSurface : surface;
|
|
1409
|
-
const
|
|
1410
|
-
|
|
1816
|
+
const appearance = useMemo3(
|
|
1817
|
+
() => resolveDocPlayerAppearance(doc, {
|
|
1818
|
+
theme,
|
|
1819
|
+
videoPresentation,
|
|
1820
|
+
pipSize,
|
|
1821
|
+
pipShape,
|
|
1822
|
+
pipPosition,
|
|
1823
|
+
showCoverSlide
|
|
1824
|
+
}),
|
|
1825
|
+
[doc, theme, videoPresentation, pipSize, pipShape, pipPosition, showCoverSlide]
|
|
1826
|
+
);
|
|
1827
|
+
const effectiveTheme = useMemo3(() => {
|
|
1828
|
+
const base = appearance.theme;
|
|
1411
1829
|
return resolvedSurface ? applySurface(base, resolvedSurface) : base;
|
|
1412
|
-
}, [theme, resolvedSurface]);
|
|
1830
|
+
}, [appearance.theme, resolvedSurface]);
|
|
1831
|
+
const resolvedPipStyle = useMemo3(() => pipStyleVars(effectiveTheme), [effectiveTheme]);
|
|
1832
|
+
const pipVars = resolvedPipStyle;
|
|
1833
|
+
const pipFrameStyle = useMemo3(
|
|
1834
|
+
() => ({
|
|
1835
|
+
border: resolvedPipStyle["--squisq-pip-border"],
|
|
1836
|
+
borderRadius: resolvedPipStyle["--squisq-pip-radius"],
|
|
1837
|
+
boxShadow: resolvedPipStyle["--squisq-pip-shadow"]
|
|
1838
|
+
}),
|
|
1839
|
+
[resolvedPipStyle]
|
|
1840
|
+
);
|
|
1413
1841
|
const {
|
|
1414
1842
|
currentBlock,
|
|
1415
1843
|
currentBlockIndex,
|
|
@@ -1432,9 +1860,9 @@ function DocPlayerContent({
|
|
|
1432
1860
|
// remove authored slides from the default loss-averse projection.
|
|
1433
1861
|
useAudioSegmentTiming: audioMode !== "synthetic"
|
|
1434
1862
|
});
|
|
1435
|
-
const coverBlock =
|
|
1863
|
+
const coverBlock = useMemo3(() => {
|
|
1436
1864
|
const startBlockConfig = doc.startBlock;
|
|
1437
|
-
if (!showCoverSlide) return null;
|
|
1865
|
+
if (!appearance.showCoverSlide) return null;
|
|
1438
1866
|
if (!startBlockConfig) return null;
|
|
1439
1867
|
const context = createTemplateContext(effectiveTheme, 0, 1, activeViewport);
|
|
1440
1868
|
const layers = expandCoverBlock(startBlockConfig, context);
|
|
@@ -1447,15 +1875,15 @@ function DocPlayerContent({
|
|
|
1447
1875
|
audioSegment: -1,
|
|
1448
1876
|
layers
|
|
1449
1877
|
};
|
|
1450
|
-
}, [doc.startBlock, activeViewport, effectiveTheme, showCoverSlide]);
|
|
1878
|
+
}, [doc.startBlock, activeViewport, effectiveTheme, appearance.showCoverSlide]);
|
|
1451
1879
|
const hasManagedCover = !!coverBlock;
|
|
1452
|
-
const [slideshowCoverVisible, setSlideshowCoverVisible] =
|
|
1453
|
-
const [isSlideshowPickerOpen, setIsSlideshowPickerOpen] =
|
|
1880
|
+
const [slideshowCoverVisible, setSlideshowCoverVisible] = useState5(false);
|
|
1881
|
+
const [isSlideshowPickerOpen, setIsSlideshowPickerOpen] = useState5(false);
|
|
1454
1882
|
const slideshowCoverInitKeyRef = useRef5("");
|
|
1455
|
-
|
|
1883
|
+
useEffect5(() => {
|
|
1456
1884
|
slideshowCoverInitKeyRef.current = "";
|
|
1457
1885
|
}, [doc]);
|
|
1458
|
-
|
|
1886
|
+
useEffect5(() => {
|
|
1459
1887
|
const initKey = `${isSlideshowMode}:${hasManagedCover}:${renderMode}`;
|
|
1460
1888
|
if (slideshowCoverInitKeyRef.current === initKey) return;
|
|
1461
1889
|
slideshowCoverInitKeyRef.current = initKey;
|
|
@@ -1466,12 +1894,12 @@ function DocPlayerContent({
|
|
|
1466
1894
|
setSlideshowCoverVisible(false);
|
|
1467
1895
|
}
|
|
1468
1896
|
}, [isSlideshowMode, hasManagedCover, renderMode, pause]);
|
|
1469
|
-
const [coverForced, setCoverForced] =
|
|
1470
|
-
const [coverGraceActive, setCoverGraceActive] =
|
|
1897
|
+
const [coverForced, setCoverForced] = useState5(false);
|
|
1898
|
+
const [coverGraceActive, setCoverGraceActive] = useState5(false);
|
|
1471
1899
|
const coverGraceTimer = useRef5();
|
|
1472
1900
|
const coverWasShowing = useRef5(false);
|
|
1473
1901
|
const hasPlayedOnce = useRef5(false);
|
|
1474
|
-
|
|
1902
|
+
useEffect5(() => {
|
|
1475
1903
|
hasPlayedOnce.current = false;
|
|
1476
1904
|
coverWasShowing.current = false;
|
|
1477
1905
|
clearTimeout(coverGraceTimer.current);
|
|
@@ -1480,7 +1908,7 @@ function DocPlayerContent({
|
|
|
1480
1908
|
}, [doc]);
|
|
1481
1909
|
const atRest = !!(coverBlock && !isSlideshowMode && !isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay);
|
|
1482
1910
|
if (atRest) coverWasShowing.current = true;
|
|
1483
|
-
|
|
1911
|
+
useEffect5(() => {
|
|
1484
1912
|
if (isPlaying && coverWasShowing.current && coverBlock && !renderMode && !isSlideshowMode) {
|
|
1485
1913
|
coverWasShowing.current = false;
|
|
1486
1914
|
hasPlayedOnce.current = true;
|
|
@@ -1488,8 +1916,8 @@ function DocPlayerContent({
|
|
|
1488
1916
|
coverGraceTimer.current = setTimeout(() => setCoverGraceActive(false), 3e3);
|
|
1489
1917
|
}
|
|
1490
1918
|
}, [isPlaying, coverBlock, renderMode, isSlideshowMode]);
|
|
1491
|
-
|
|
1492
|
-
|
|
1919
|
+
useEffect5(() => () => clearTimeout(coverGraceTimer.current), []);
|
|
1920
|
+
useEffect5(() => () => clearTimeout(tapFeedbackTimer.current), []);
|
|
1493
1921
|
const showVideoCoverBlock = !isSlideshowMode && !isLinearMode && !!coverBlock && (coverForced || coverGraceActive || !isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay);
|
|
1494
1922
|
const effectiveSlideshowCoverVisible = coverVisible ?? slideshowCoverVisible;
|
|
1495
1923
|
const showSlideshowCover = !!(isSlideshowMode && !isLinearMode && !renderMode && coverBlock && effectiveSlideshowCoverVisible);
|
|
@@ -1498,19 +1926,19 @@ function DocPlayerContent({
|
|
|
1498
1926
|
const slideshowSlideIndex = slideshowHasCover ? effectiveSlideshowCoverVisible ? 0 : currentBlockIndex + 1 : currentBlockIndex;
|
|
1499
1927
|
const slideshowTotalSlides = slideshowHasCover ? expandedBlocks.length + 1 : expandedBlocks.length;
|
|
1500
1928
|
const hasAutoPlayed = useRef5(false);
|
|
1501
|
-
|
|
1929
|
+
useEffect5(() => {
|
|
1502
1930
|
hasAutoPlayed.current = false;
|
|
1503
1931
|
}, [doc]);
|
|
1504
|
-
|
|
1932
|
+
useEffect5(() => {
|
|
1505
1933
|
if (isAudioReady && autoPlay && !hasAutoPlayed.current) {
|
|
1506
1934
|
hasAutoPlayed.current = true;
|
|
1507
1935
|
play();
|
|
1508
1936
|
}
|
|
1509
1937
|
}, [isAudioReady, autoPlay, play]);
|
|
1510
|
-
|
|
1938
|
+
useEffect5(() => {
|
|
1511
1939
|
onTimeUpdate?.(currentTime);
|
|
1512
1940
|
}, [currentTime, onTimeUpdate]);
|
|
1513
|
-
|
|
1941
|
+
useEffect5(() => {
|
|
1514
1942
|
if (isEnded) {
|
|
1515
1943
|
onEnded?.();
|
|
1516
1944
|
if (loop && !isSlideshowMode && !isLinearMode) {
|
|
@@ -1528,6 +1956,7 @@ function DocPlayerContent({
|
|
|
1528
1956
|
};
|
|
1529
1957
|
stableRenderAPIRef.current = {
|
|
1530
1958
|
seekTo: (time) => current().seekTo(time),
|
|
1959
|
+
getRenderedTime: () => current().getRenderedTime(),
|
|
1531
1960
|
getDuration: () => current().getDuration(),
|
|
1532
1961
|
getBlocks: () => current().getBlocks(),
|
|
1533
1962
|
getAudioSegments: () => current().getAudioSegments(),
|
|
@@ -1539,7 +1968,7 @@ function DocPlayerContent({
|
|
|
1539
1968
|
};
|
|
1540
1969
|
}
|
|
1541
1970
|
const stableRenderAPI = stableRenderAPIRef.current;
|
|
1542
|
-
|
|
1971
|
+
useEffect5(() => {
|
|
1543
1972
|
if (!renderMode && !isDebugMode) {
|
|
1544
1973
|
liveRenderAPIRef.current = null;
|
|
1545
1974
|
return;
|
|
@@ -1549,77 +1978,58 @@ function DocPlayerContent({
|
|
|
1549
1978
|
liveRenderAPIRef.current = null;
|
|
1550
1979
|
return;
|
|
1551
1980
|
}
|
|
1552
|
-
const renderSeekTo = (time) => {
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
} else {
|
|
1591
|
-
video.addEventListener("seeked", () => r(), { once: true });
|
|
1592
|
-
setTimeout(r, 200);
|
|
1593
|
-
}
|
|
1594
|
-
})
|
|
1595
|
-
);
|
|
1596
|
-
});
|
|
1597
|
-
}
|
|
1598
|
-
root.querySelectorAll("video[data-clip-id]").forEach((el) => {
|
|
1599
|
-
const video = el;
|
|
1600
|
-
const absStart = parseFloat(video.dataset.absStart || "0");
|
|
1601
|
-
const absEnd = parseFloat(video.dataset.absEnd || "0");
|
|
1602
|
-
const sourceIn = parseFloat(video.dataset.sourceIn || "0");
|
|
1603
|
-
video.pause();
|
|
1604
|
-
if (time < absStart || time >= absEnd) return;
|
|
1605
|
-
const targetTime = sourceIn + (time - absStart);
|
|
1606
|
-
video.currentTime = targetTime;
|
|
1607
|
-
videoSeekPromises.push(
|
|
1608
|
-
new Promise((r) => {
|
|
1609
|
-
if (Math.abs(video.currentTime - targetTime) < 0.1) {
|
|
1610
|
-
r();
|
|
1611
|
-
} else {
|
|
1612
|
-
video.addEventListener("seeked", () => r(), { once: true });
|
|
1613
|
-
setTimeout(r, 200);
|
|
1614
|
-
}
|
|
1615
|
-
})
|
|
1616
|
-
);
|
|
1617
|
-
});
|
|
1618
|
-
Promise.all(videoSeekPromises).then(() => {
|
|
1619
|
-
void waitForVisualUpdate().then(resolve);
|
|
1620
|
-
});
|
|
1981
|
+
const renderSeekTo = async (time) => {
|
|
1982
|
+
if (renderMode) {
|
|
1983
|
+
const committed = waitForRenderCommit(time);
|
|
1984
|
+
flushSync(() => setRenderClock({ doc, time }));
|
|
1985
|
+
if (externalAudioController) void seekTo(time).catch(() => void 0);
|
|
1986
|
+
await committed;
|
|
1987
|
+
} else {
|
|
1988
|
+
await seekTo(time);
|
|
1989
|
+
}
|
|
1990
|
+
let blockStartTime = 0;
|
|
1991
|
+
for (let i = expandedBlocks.length - 1; i >= 0; i--) {
|
|
1992
|
+
if (time >= expandedBlocks[i].startTime) {
|
|
1993
|
+
blockStartTime = expandedBlocks[i].startTime;
|
|
1994
|
+
break;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
const elapsedMs = (time - blockStartTime) * 1e3;
|
|
1998
|
+
(root.getAnimations?.() ?? []).forEach((anim) => {
|
|
1999
|
+
const target = anim.effect?.target;
|
|
2000
|
+
if (!target) return;
|
|
2001
|
+
if (target.closest(".doc-player__block--active")) {
|
|
2002
|
+
anim.currentTime = Math.max(0, elapsedMs);
|
|
2003
|
+
} else if (target.closest(".doc-player__block--previous")) {
|
|
2004
|
+
anim.currentTime = Math.max(0, elapsedMs);
|
|
2005
|
+
}
|
|
2006
|
+
});
|
|
2007
|
+
const blockElapsed = time - blockStartTime;
|
|
2008
|
+
const videoSeekPromises = [];
|
|
2009
|
+
const activeBlockEl = root.querySelector(".doc-player__block--active");
|
|
2010
|
+
if (activeBlockEl) {
|
|
2011
|
+
const videos = activeBlockEl.querySelectorAll("video[data-clip-start]");
|
|
2012
|
+
videos.forEach((el) => {
|
|
2013
|
+
const video = el;
|
|
2014
|
+
const clipStart = parseFloat(video.dataset.clipStart || "0");
|
|
2015
|
+
const clipEnd = parseFloat(video.dataset.clipEnd || "0");
|
|
2016
|
+
const startAt = parseFloat(video.dataset.startAt || "0");
|
|
2017
|
+
const targetTime = Math.min(clipStart + Math.max(0, blockElapsed - startAt), clipEnd);
|
|
2018
|
+
videoSeekPromises.push(seekVideoToFrame(video, targetTime));
|
|
1621
2019
|
});
|
|
2020
|
+
}
|
|
2021
|
+
root.querySelectorAll("video[data-clip-id]").forEach((el) => {
|
|
2022
|
+
const video = el;
|
|
2023
|
+
const absStart = parseFloat(video.dataset.absStart || "0");
|
|
2024
|
+
const absEnd = parseFloat(video.dataset.absEnd || "0");
|
|
2025
|
+
const sourceIn = parseFloat(video.dataset.sourceIn || "0");
|
|
2026
|
+
video.pause();
|
|
2027
|
+
if (time < absStart || time >= absEnd) return;
|
|
2028
|
+
const targetTime = sourceIn + (time - absStart);
|
|
2029
|
+
videoSeekPromises.push(seekVideoToFrame(video, targetTime));
|
|
1622
2030
|
});
|
|
2031
|
+
await Promise.all(videoSeekPromises);
|
|
2032
|
+
await waitForVisualUpdate();
|
|
1623
2033
|
};
|
|
1624
2034
|
const getDuration = () => {
|
|
1625
2035
|
const mediaDuration = getDocPlaybackDuration(doc);
|
|
@@ -1662,6 +2072,7 @@ function DocPlayerContent({
|
|
|
1662
2072
|
const hasCoverBlock = () => !!coverBlock;
|
|
1663
2073
|
const api = {
|
|
1664
2074
|
seekTo: renderSeekTo,
|
|
2075
|
+
getRenderedTime: () => committedRenderTimeRef.current,
|
|
1665
2076
|
getDuration,
|
|
1666
2077
|
getBlocks,
|
|
1667
2078
|
getAudioSegments,
|
|
@@ -1675,8 +2086,18 @@ function DocPlayerContent({
|
|
|
1675
2086
|
return () => {
|
|
1676
2087
|
if (liveRenderAPIRef.current === api) liveRenderAPIRef.current = null;
|
|
1677
2088
|
};
|
|
1678
|
-
}, [
|
|
1679
|
-
|
|
2089
|
+
}, [
|
|
2090
|
+
renderMode,
|
|
2091
|
+
isDebugMode,
|
|
2092
|
+
seekTo,
|
|
2093
|
+
totalDuration,
|
|
2094
|
+
expandedBlocks,
|
|
2095
|
+
coverBlock,
|
|
2096
|
+
doc,
|
|
2097
|
+
externalAudioController,
|
|
2098
|
+
waitForRenderCommit
|
|
2099
|
+
]);
|
|
2100
|
+
useEffect5(() => {
|
|
1680
2101
|
if (!renderMode && !isDebugMode || !containerRef.current) {
|
|
1681
2102
|
onRenderAPIReady?.(null);
|
|
1682
2103
|
return;
|
|
@@ -1685,8 +2106,8 @@ function DocPlayerContent({
|
|
|
1685
2106
|
return () => onRenderAPIReady?.(null);
|
|
1686
2107
|
}, [renderMode, isDebugMode, onRenderAPIReady, stableRenderAPI]);
|
|
1687
2108
|
const defaultMode = captionsEnabledProp === false ? "off" : captionStyle || "standard";
|
|
1688
|
-
const [captionMode, setCaptionMode] =
|
|
1689
|
-
|
|
2109
|
+
const [captionMode, setCaptionMode] = useState5(defaultMode);
|
|
2110
|
+
useEffect5(() => {
|
|
1690
2111
|
setCaptionMode(defaultMode);
|
|
1691
2112
|
}, [defaultMode]);
|
|
1692
2113
|
const captionsEnabled = captionMode !== "off";
|
|
@@ -1706,8 +2127,8 @@ function DocPlayerContent({
|
|
|
1706
2127
|
});
|
|
1707
2128
|
}, [onCaptionsToggle]);
|
|
1708
2129
|
const hasCaptions = doc.captions && doc.captions.phrases.length > 0;
|
|
1709
|
-
const segmentTitleMap =
|
|
1710
|
-
const playbackState =
|
|
2130
|
+
const segmentTitleMap = useMemo3(() => buildSegmentTitleMap(doc), [doc]);
|
|
2131
|
+
const playbackState = useMemo3(
|
|
1711
2132
|
() => ({
|
|
1712
2133
|
isPlaying,
|
|
1713
2134
|
currentTime,
|
|
@@ -1750,7 +2171,7 @@ function DocPlayerContent({
|
|
|
1750
2171
|
expandedBlocks.length
|
|
1751
2172
|
]
|
|
1752
2173
|
);
|
|
1753
|
-
const playbackActions =
|
|
2174
|
+
const playbackActions = useMemo3(
|
|
1754
2175
|
() => ({
|
|
1755
2176
|
toggle,
|
|
1756
2177
|
restart,
|
|
@@ -1761,7 +2182,7 @@ function DocPlayerContent({
|
|
|
1761
2182
|
}),
|
|
1762
2183
|
[toggle, restart, seekTo, setCaptionsEnabled, cycleCaptionMode, onFullscreenToggle]
|
|
1763
2184
|
);
|
|
1764
|
-
const slideNavActions =
|
|
2185
|
+
const slideNavActions = useMemo3(
|
|
1765
2186
|
() => ({
|
|
1766
2187
|
nextSlide: () => {
|
|
1767
2188
|
if (slideshowHasCover && slideshowCoverVisible) {
|
|
@@ -1850,10 +2271,10 @@ function DocPlayerContent({
|
|
|
1850
2271
|
onNext: handleSwipeNext,
|
|
1851
2272
|
onPrev: handleSwipePrev
|
|
1852
2273
|
});
|
|
1853
|
-
|
|
2274
|
+
useEffect5(() => {
|
|
1854
2275
|
onPlaybackStateChange?.(playbackState);
|
|
1855
2276
|
}, [playbackState, onPlaybackStateChange]);
|
|
1856
|
-
|
|
2277
|
+
useEffect5(() => {
|
|
1857
2278
|
onControlsReady?.({ play, pause, ...playbackActions });
|
|
1858
2279
|
}, [play, pause, playbackActions, onControlsReady]);
|
|
1859
2280
|
const getBlockTitle = useCallback4((block) => {
|
|
@@ -1880,7 +2301,7 @@ function DocPlayerContent({
|
|
|
1880
2301
|
}
|
|
1881
2302
|
return block.id.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1882
2303
|
}, []);
|
|
1883
|
-
const slideshowPickerItems =
|
|
2304
|
+
const slideshowPickerItems = useMemo3(() => {
|
|
1884
2305
|
const blockItems = expandedBlocks.map((block, index) => ({
|
|
1885
2306
|
id: block.id,
|
|
1886
2307
|
label: String(index + 1),
|
|
@@ -1896,7 +2317,7 @@ function DocPlayerContent({
|
|
|
1896
2317
|
...blockItems
|
|
1897
2318
|
];
|
|
1898
2319
|
}, [coverBlock, expandedBlocks, getBlockTitle, slideshowHasCover]);
|
|
1899
|
-
const blockMarkers =
|
|
2320
|
+
const blockMarkers = useMemo3(() => {
|
|
1900
2321
|
if (!totalDuration || !expandedBlocks.length) return [];
|
|
1901
2322
|
let prevSegment = -1;
|
|
1902
2323
|
return expandedBlocks.map((block, index) => {
|
|
@@ -1911,7 +2332,7 @@ function DocPlayerContent({
|
|
|
1911
2332
|
};
|
|
1912
2333
|
});
|
|
1913
2334
|
}, [expandedBlocks, totalDuration, getBlockTitle]);
|
|
1914
|
-
|
|
2335
|
+
useEffect5(() => {
|
|
1915
2336
|
if (blockMarkers.length > 0) {
|
|
1916
2337
|
onBlockMarkers?.(blockMarkers);
|
|
1917
2338
|
}
|
|
@@ -1988,7 +2409,7 @@ function DocPlayerContent({
|
|
|
1988
2409
|
(e) => handleKeyboardShortcut(e, false),
|
|
1989
2410
|
[handleKeyboardShortcut]
|
|
1990
2411
|
);
|
|
1991
|
-
|
|
2412
|
+
useEffect5(() => {
|
|
1992
2413
|
if (!globalKeyboardShortcuts || renderMode || isLinearMode) return;
|
|
1993
2414
|
const handleDocumentKeyDown = (event) => {
|
|
1994
2415
|
handleKeyboardShortcut(event, true);
|
|
@@ -2028,7 +2449,9 @@ function DocPlayerContent({
|
|
|
2028
2449
|
{
|
|
2029
2450
|
ref: containerRef,
|
|
2030
2451
|
"data-player-id": playerId,
|
|
2031
|
-
"data-orientation":
|
|
2452
|
+
"data-orientation": activeOrientation,
|
|
2453
|
+
"data-playback-state": isPlaying ? "playing" : "paused",
|
|
2454
|
+
"data-swipe-phase": swipe.phase,
|
|
2032
2455
|
tabIndex: renderMode ? -1 : 0,
|
|
2033
2456
|
"aria-label": "Document player",
|
|
2034
2457
|
onKeyDown: renderMode ? void 0 : handleKeyDown,
|
|
@@ -2036,6 +2459,7 @@ function DocPlayerContent({
|
|
|
2036
2459
|
onClick: handleContainerClick,
|
|
2037
2460
|
onPointerDown: swipe.onPointerDown,
|
|
2038
2461
|
style: {
|
|
2462
|
+
...pipVars,
|
|
2039
2463
|
position: "relative",
|
|
2040
2464
|
width: "100%",
|
|
2041
2465
|
aspectRatio: `${activeViewport.width} / ${activeViewport.height}`,
|
|
@@ -2057,9 +2481,12 @@ function DocPlayerContent({
|
|
|
2057
2481
|
basePath,
|
|
2058
2482
|
renderMode,
|
|
2059
2483
|
muted,
|
|
2060
|
-
presentation: videoPresentation,
|
|
2061
|
-
|
|
2062
|
-
|
|
2484
|
+
presentation: appearance.videoPresentation,
|
|
2485
|
+
pipSize: appearance.pipSize,
|
|
2486
|
+
pipShape: appearance.pipShape,
|
|
2487
|
+
pipPosition: appearance.pipPosition,
|
|
2488
|
+
pipOrientation: activeOrientation,
|
|
2489
|
+
pipFrameStyle
|
|
2063
2490
|
}
|
|
2064
2491
|
),
|
|
2065
2492
|
/* @__PURE__ */ jsxs4("div", { className: "doc-player__viewport", children: [
|
|
@@ -2211,7 +2638,7 @@ function DocPlayerContent({
|
|
|
2211
2638
|
" ",
|
|
2212
2639
|
/* @__PURE__ */ jsxs4("span", { style: { color: "#666" }, children: [
|
|
2213
2640
|
"(",
|
|
2214
|
-
|
|
2641
|
+
activeOrientation,
|
|
2215
2642
|
")"
|
|
2216
2643
|
] })
|
|
2217
2644
|
] }),
|
|
@@ -2477,7 +2904,7 @@ function DocControlsSidebar({ state, actions }) {
|
|
|
2477
2904
|
}
|
|
2478
2905
|
|
|
2479
2906
|
// src/DocPlayerWithSidebar.tsx
|
|
2480
|
-
import { useRef as useRef6, useState as
|
|
2907
|
+
import { useRef as useRef6, useState as useState6, useCallback as useCallback5, useEffect as useEffect6 } from "react";
|
|
2481
2908
|
import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2482
2909
|
var DEFAULT_STATE = {
|
|
2483
2910
|
isPlaying: false,
|
|
@@ -2513,7 +2940,7 @@ function DocPlayerWithSidebar({
|
|
|
2513
2940
|
const stateRef = useRef6(DEFAULT_STATE);
|
|
2514
2941
|
const actionsRef = useRef6(null);
|
|
2515
2942
|
const wasPlayingRef = useRef6(false);
|
|
2516
|
-
const [, setTick] =
|
|
2943
|
+
const [, setTick] = useState6(0);
|
|
2517
2944
|
const handleStateChange = useCallback5(
|
|
2518
2945
|
(state) => {
|
|
2519
2946
|
stateRef.current = state;
|
|
@@ -2533,7 +2960,7 @@ function DocPlayerWithSidebar({
|
|
|
2533
2960
|
},
|
|
2534
2961
|
[]
|
|
2535
2962
|
);
|
|
2536
|
-
|
|
2963
|
+
useEffect6(() => {
|
|
2537
2964
|
const interval = setInterval(() => {
|
|
2538
2965
|
if (!stateRef.current.isPlaying) return;
|
|
2539
2966
|
setTick((t) => t + 1);
|
|
@@ -2570,12 +2997,14 @@ function DocPlayerWithSidebar({
|
|
|
2570
2997
|
|
|
2571
2998
|
export {
|
|
2572
2999
|
MediaClipLayer,
|
|
3000
|
+
useMediaClipDurations,
|
|
2573
3001
|
SocialCaptionOverlay,
|
|
2574
3002
|
CaptionOverlay,
|
|
2575
3003
|
formatTime,
|
|
2576
3004
|
DocProgressBar,
|
|
2577
3005
|
DocControlsOverlay,
|
|
2578
3006
|
DocControlsSlideshow,
|
|
3007
|
+
resolveDocPlayerAppearance,
|
|
2579
3008
|
DocPlayer,
|
|
2580
3009
|
DocControlsBottom,
|
|
2581
3010
|
DocControlsSidebar,
|