@bendyline/squisq-react 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +766 -35
- package/dist/index.js +4793 -33
- package/dist/index.js.map +1 -1
- package/dist/squisq-player.css +1 -1
- package/dist/squisq-player.css.map +1 -1
- package/dist/squisq-player.global.js +17 -13
- package/dist/squisq-player.global.js.map +1 -1
- package/dist/standalone-source.js +1 -1
- package/package.json +3 -2
- package/src/BlockRenderer.tsx +15 -7
- package/src/DocPlayer.tsx +65 -12
- package/src/DocProgressBar.tsx +21 -3
- package/src/LinearDocView.tsx +11 -197
- package/src/MarkdownRenderer.tsx +165 -41
- package/src/MediaClipLayer.tsx +135 -0
- package/src/__tests__/DocPlayer.test.tsx +51 -0
- package/src/__tests__/DocProgressBar.test.tsx +76 -0
- package/src/__tests__/MarkdownRenderer.test.tsx +95 -1
- package/src/__tests__/PathLayer.test.tsx +73 -0
- package/src/__tests__/fillStyle.test.tsx +112 -0
- package/src/__tests__/transitionStyles.test.ts +125 -0
- package/src/__tests__/useDocPlayback.transition.test.ts +70 -0
- package/src/hooks/useAudioSync.ts +14 -1
- package/src/hooks/useDocPlayback.ts +81 -100
- package/src/hooks/useMediaSchedule.ts +39 -0
- package/src/index.ts +7 -0
- package/src/layers/ImageLayer.tsx +11 -1
- package/src/layers/PathLayer.tsx +146 -0
- package/src/layers/ShapeLayer.tsx +27 -5
- package/src/layers/TextLayer.tsx +395 -22
- package/src/layers/VideoLayer.tsx +16 -9
- package/src/layers/index.ts +1 -0
- package/src/styles/doc-animations.css +1857 -2
- package/src/utils/fillStyle.tsx +148 -0
- package/dist/BlockRenderer.d.ts +0 -37
- package/dist/BlockRenderer.d.ts.map +0 -1
- package/dist/BlockRenderer.js +0 -54
- package/dist/BlockRenderer.js.map +0 -1
- package/dist/CaptionOverlay.d.ts +0 -33
- package/dist/CaptionOverlay.d.ts.map +0 -1
- package/dist/CaptionOverlay.js +0 -44
- package/dist/CaptionOverlay.js.map +0 -1
- package/dist/DocControlsBottom.d.ts +0 -28
- package/dist/DocControlsBottom.d.ts.map +0 -1
- package/dist/DocControlsBottom.js +0 -27
- package/dist/DocControlsBottom.js.map +0 -1
- package/dist/DocControlsOverlay.d.ts +0 -28
- package/dist/DocControlsOverlay.d.ts.map +0 -1
- package/dist/DocControlsOverlay.js +0 -80
- package/dist/DocControlsOverlay.js.map +0 -1
- package/dist/DocControlsSidebar.d.ts +0 -27
- package/dist/DocControlsSidebar.d.ts.map +0 -1
- package/dist/DocControlsSidebar.js +0 -10
- package/dist/DocControlsSidebar.js.map +0 -1
- package/dist/DocControlsSlideshow.d.ts +0 -20
- package/dist/DocControlsSlideshow.d.ts.map +0 -1
- package/dist/DocControlsSlideshow.js +0 -69
- package/dist/DocControlsSlideshow.js.map +0 -1
- package/dist/DocPlayer.d.ts +0 -97
- package/dist/DocPlayer.d.ts.map +0 -1
- package/dist/DocPlayer.js +0 -657
- package/dist/DocPlayer.js.map +0 -1
- package/dist/DocPlayerWithSidebar.d.ts +0 -41
- package/dist/DocPlayerWithSidebar.d.ts.map +0 -1
- package/dist/DocPlayerWithSidebar.js +0 -69
- package/dist/DocPlayerWithSidebar.js.map +0 -1
- package/dist/DocProgressBar.d.ts +0 -31
- package/dist/DocProgressBar.d.ts.map +0 -1
- package/dist/DocProgressBar.js +0 -122
- package/dist/DocProgressBar.js.map +0 -1
- package/dist/InlineAudioPlayer.d.ts +0 -15
- package/dist/InlineAudioPlayer.d.ts.map +0 -1
- package/dist/InlineAudioPlayer.js +0 -22
- package/dist/InlineAudioPlayer.js.map +0 -1
- package/dist/InlineVideoPlayer.d.ts +0 -21
- package/dist/InlineVideoPlayer.d.ts.map +0 -1
- package/dist/InlineVideoPlayer.js +0 -29
- package/dist/InlineVideoPlayer.js.map +0 -1
- package/dist/LinearDocView.d.ts +0 -73
- package/dist/LinearDocView.d.ts.map +0 -1
- package/dist/LinearDocView.js +0 -451
- package/dist/LinearDocView.js.map +0 -1
- package/dist/MarkdownRenderer.d.ts +0 -32
- package/dist/MarkdownRenderer.d.ts.map +0 -1
- package/dist/MarkdownRenderer.js +0 -259
- package/dist/MarkdownRenderer.js.map +0 -1
- package/dist/SocialCaptionOverlay.d.ts +0 -27
- package/dist/SocialCaptionOverlay.d.ts.map +0 -1
- package/dist/SocialCaptionOverlay.js +0 -185
- package/dist/SocialCaptionOverlay.js.map +0 -1
- package/dist/__tests__/BlockRenderer.test.d.ts +0 -2
- package/dist/__tests__/BlockRenderer.test.d.ts.map +0 -1
- package/dist/__tests__/BlockRenderer.test.js +0 -82
- package/dist/__tests__/BlockRenderer.test.js.map +0 -1
- package/dist/__tests__/DocControlsSlideshow.test.d.ts +0 -2
- package/dist/__tests__/DocControlsSlideshow.test.d.ts.map +0 -1
- package/dist/__tests__/DocControlsSlideshow.test.js +0 -89
- package/dist/__tests__/DocControlsSlideshow.test.js.map +0 -1
- package/dist/__tests__/JsonView.test.d.ts +0 -2
- package/dist/__tests__/JsonView.test.d.ts.map +0 -1
- package/dist/__tests__/JsonView.test.js +0 -93
- package/dist/__tests__/JsonView.test.js.map +0 -1
- package/dist/__tests__/LinearDocView.test.d.ts +0 -2
- package/dist/__tests__/LinearDocView.test.d.ts.map +0 -1
- package/dist/__tests__/LinearDocView.test.js +0 -250
- package/dist/__tests__/LinearDocView.test.js.map +0 -1
- package/dist/__tests__/MarkdownRenderer.test.d.ts +0 -2
- package/dist/__tests__/MarkdownRenderer.test.d.ts.map +0 -1
- package/dist/__tests__/MarkdownRenderer.test.js +0 -208
- package/dist/__tests__/MarkdownRenderer.test.js.map +0 -1
- package/dist/__tests__/TableLayer.test.d.ts +0 -2
- package/dist/__tests__/TableLayer.test.d.ts.map +0 -1
- package/dist/__tests__/TableLayer.test.js +0 -121
- package/dist/__tests__/TableLayer.test.js.map +0 -1
- package/dist/__tests__/exports.test.d.ts +0 -2
- package/dist/__tests__/exports.test.d.ts.map +0 -1
- package/dist/__tests__/exports.test.js +0 -48
- package/dist/__tests__/exports.test.js.map +0 -1
- package/dist/hooks/AudioProvider.d.ts +0 -71
- package/dist/hooks/AudioProvider.d.ts.map +0 -1
- package/dist/hooks/AudioProvider.js +0 -52
- package/dist/hooks/AudioProvider.js.map +0 -1
- package/dist/hooks/MediaContext.d.ts +0 -35
- package/dist/hooks/MediaContext.d.ts.map +0 -1
- package/dist/hooks/MediaContext.js +0 -72
- package/dist/hooks/MediaContext.js.map +0 -1
- package/dist/hooks/index.d.ts +0 -6
- package/dist/hooks/index.d.ts.map +0 -1
- package/dist/hooks/index.js +0 -5
- package/dist/hooks/index.js.map +0 -1
- package/dist/hooks/useAudioSync.d.ts +0 -18
- package/dist/hooks/useAudioSync.d.ts.map +0 -1
- package/dist/hooks/useAudioSync.js +0 -340
- package/dist/hooks/useAudioSync.js.map +0 -1
- package/dist/hooks/useAutoSurface.d.ts +0 -11
- package/dist/hooks/useAutoSurface.d.ts.map +0 -1
- package/dist/hooks/useAutoSurface.js +0 -24
- package/dist/hooks/useAutoSurface.js.map +0 -1
- package/dist/hooks/useDocPlayback.d.ts +0 -47
- package/dist/hooks/useDocPlayback.d.ts.map +0 -1
- package/dist/hooks/useDocPlayback.js +0 -179
- package/dist/hooks/useDocPlayback.js.map +0 -1
- package/dist/hooks/useViewportOrientation.d.ts +0 -31
- package/dist/hooks/useViewportOrientation.d.ts.map +0 -1
- package/dist/hooks/useViewportOrientation.js +0 -92
- package/dist/hooks/useViewportOrientation.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/jsonView/JsonView.d.ts +0 -26
- package/dist/jsonView/JsonView.d.ts.map +0 -1
- package/dist/jsonView/JsonView.js +0 -12
- package/dist/jsonView/JsonView.js.map +0 -1
- package/dist/jsonView/RenderNode.d.ts +0 -22
- package/dist/jsonView/RenderNode.d.ts.map +0 -1
- package/dist/jsonView/RenderNode.js +0 -30
- package/dist/jsonView/RenderNode.js.map +0 -1
- package/dist/jsonView/index.d.ts +0 -3
- package/dist/jsonView/index.d.ts.map +0 -1
- package/dist/jsonView/index.js +0 -2
- package/dist/jsonView/index.js.map +0 -1
- package/dist/jsonView/useJsonViewTokens.d.ts +0 -14
- package/dist/jsonView/useJsonViewTokens.d.ts.map +0 -1
- package/dist/jsonView/useJsonViewTokens.js +0 -34
- package/dist/jsonView/useJsonViewTokens.js.map +0 -1
- package/dist/jsonView/viewers.d.ts +0 -30
- package/dist/jsonView/viewers.d.ts.map +0 -1
- package/dist/jsonView/viewers.js +0 -226
- package/dist/jsonView/viewers.js.map +0 -1
- package/dist/layers/ImageLayer.d.ts +0 -23
- package/dist/layers/ImageLayer.d.ts.map +0 -1
- package/dist/layers/ImageLayer.js +0 -104
- package/dist/layers/ImageLayer.js.map +0 -1
- package/dist/layers/MapLayer.d.ts +0 -29
- package/dist/layers/MapLayer.d.ts.map +0 -1
- package/dist/layers/MapLayer.js +0 -97
- package/dist/layers/MapLayer.js.map +0 -1
- package/dist/layers/ShapeLayer.d.ts +0 -20
- package/dist/layers/ShapeLayer.d.ts.map +0 -1
- package/dist/layers/ShapeLayer.js +0 -39
- package/dist/layers/ShapeLayer.js.map +0 -1
- package/dist/layers/TableLayer.d.ts +0 -23
- package/dist/layers/TableLayer.d.ts.map +0 -1
- package/dist/layers/TableLayer.js +0 -58
- package/dist/layers/TableLayer.js.map +0 -1
- package/dist/layers/TextLayer.d.ts +0 -23
- package/dist/layers/TextLayer.d.ts.map +0 -1
- package/dist/layers/TextLayer.js +0 -126
- package/dist/layers/TextLayer.js.map +0 -1
- package/dist/layers/VideoLayer.d.ts +0 -40
- package/dist/layers/VideoLayer.d.ts.map +0 -1
- package/dist/layers/VideoLayer.js +0 -105
- package/dist/layers/VideoLayer.js.map +0 -1
- package/dist/layers/index.d.ts +0 -6
- package/dist/layers/index.d.ts.map +0 -1
- package/dist/layers/index.js +0 -6
- package/dist/layers/index.js.map +0 -1
- package/dist/standalone-entry.d.ts +0 -69
- package/dist/standalone-entry.d.ts.map +0 -1
- package/dist/standalone-entry.js +0 -185
- package/dist/standalone-entry.js.map +0 -1
- package/dist/types.d.ts +0 -154
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -21
- package/dist/types.js.map +0 -1
- package/dist/utils/animationUtils.d.ts +0 -8
- package/dist/utils/animationUtils.d.ts.map +0 -1
- package/dist/utils/animationUtils.js +0 -8
- package/dist/utils/animationUtils.js.map +0 -1
- package/dist/utils/layerUtils.d.ts +0 -17
- package/dist/utils/layerUtils.d.ts.map +0 -1
- package/dist/utils/layerUtils.js +0 -37
- package/dist/utils/layerUtils.js.map +0 -1
- package/dist/utils/mapTileUtils.d.ts +0 -105
- package/dist/utils/mapTileUtils.d.ts.map +0 -1
- package/dist/utils/mapTileUtils.js +0 -268
- package/dist/utils/mapTileUtils.js.map +0 -1
package/dist/DocPlayer.js
DELETED
|
@@ -1,657 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
/**
|
|
3
|
-
* DocPlayer Component
|
|
4
|
-
*
|
|
5
|
-
* Main component for playing visual stories. Combines audio playback
|
|
6
|
-
* with synchronized SVG block animations. Supports both interactive
|
|
7
|
-
* browser playback and headless rendering for video export.
|
|
8
|
-
*
|
|
9
|
-
* Features:
|
|
10
|
-
* - Audio synchronization with multiple MP3 segments
|
|
11
|
-
* - Block transitions (fade, dissolve, slide)
|
|
12
|
-
* - Playback controls (play/pause, seek, next/prev)
|
|
13
|
-
* - Progress display
|
|
14
|
-
* - Render mode for video capture (via window.seekTo)
|
|
15
|
-
* - Pluggable audio provider for different environments (browser, EFB)
|
|
16
|
-
* - Multiple control layouts: overlay (default), sidebar, bottom
|
|
17
|
-
*
|
|
18
|
-
* Related Files:
|
|
19
|
-
* - DocControlsOverlay.tsx -- Default overlay controls
|
|
20
|
-
* - DocProgressBar.tsx -- Extracted progress bar
|
|
21
|
-
* - DocControlsSidebar.tsx -- Vertical sidebar controls
|
|
22
|
-
* - DocPlayerWithSidebar.tsx -- Wrapper for sidebar layout
|
|
23
|
-
* - types.ts -- Shared control types
|
|
24
|
-
*/
|
|
25
|
-
import { Fragment, useRef, useState, useEffect, useCallback, useMemo } from 'react';
|
|
26
|
-
import { isTemplateBlock, getCaptionAtTime } from '@bendyline/squisq/schemas';
|
|
27
|
-
import { applySurface } from '@bendyline/squisq/schemas';
|
|
28
|
-
import { BlockRenderer } from './BlockRenderer';
|
|
29
|
-
import { CaptionOverlay } from './CaptionOverlay';
|
|
30
|
-
import { useAutoSurface } from './hooks/useAutoSurface';
|
|
31
|
-
import { useAudioSync } from './hooks/useAudioSync';
|
|
32
|
-
import { useDocPlayback } from './hooks/useDocPlayback';
|
|
33
|
-
import { useViewportOrientation } from './hooks/useViewportOrientation';
|
|
34
|
-
import { expandCoverBlock, createTemplateContext, DEFAULT_THEME, VIEWPORT_PRESETS, } from '@bendyline/squisq/doc';
|
|
35
|
-
import { DocControlsOverlay } from './DocControlsOverlay';
|
|
36
|
-
import { DocControlsSlideshow } from './DocControlsSlideshow';
|
|
37
|
-
import { DocProgressBar } from './DocProgressBar';
|
|
38
|
-
import { LinearDocView } from './LinearDocView';
|
|
39
|
-
const SMALL_WORDS = new Set([
|
|
40
|
-
'a',
|
|
41
|
-
'an',
|
|
42
|
-
'the',
|
|
43
|
-
'and',
|
|
44
|
-
'but',
|
|
45
|
-
'or',
|
|
46
|
-
'for',
|
|
47
|
-
'nor',
|
|
48
|
-
'on',
|
|
49
|
-
'at',
|
|
50
|
-
'to',
|
|
51
|
-
'in',
|
|
52
|
-
'of',
|
|
53
|
-
'by',
|
|
54
|
-
'is',
|
|
55
|
-
]);
|
|
56
|
-
/**
|
|
57
|
-
* Build a map of audio segment index -> display-friendly title.
|
|
58
|
-
* Uses sectionHeader blocks to find real titles, with fallbacks
|
|
59
|
-
* for "intro" and slug-based names.
|
|
60
|
-
*/
|
|
61
|
-
function buildSegmentTitleMap(script) {
|
|
62
|
-
const map = new Map();
|
|
63
|
-
// Scan blocks for sectionHeader templates which carry the real title
|
|
64
|
-
for (const block of script.blocks) {
|
|
65
|
-
if (isTemplateBlock(block) && block.template === 'sectionHeader' && 'title' in block) {
|
|
66
|
-
const segIdx = block.audioSegment;
|
|
67
|
-
if (!map.has(segIdx)) {
|
|
68
|
-
map.set(segIdx, block.title);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
// Fill in any segments that weren't covered by sectionHeader blocks
|
|
73
|
-
for (let i = 0; i < script.audio.segments.length; i++) {
|
|
74
|
-
if (!map.has(i)) {
|
|
75
|
-
const name = script.audio.segments[i].name;
|
|
76
|
-
if (name === 'intro' || name.includes('intro')) {
|
|
77
|
-
map.set(i, 'Introduction');
|
|
78
|
-
}
|
|
79
|
-
else if (name === 'flight-context' || name.includes('flight-context')) {
|
|
80
|
-
map.set(i, 'Flight Context');
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
// Title-case the slug: "hands-on-history" -> "Hands on History"
|
|
84
|
-
const words = name.split('-');
|
|
85
|
-
const titled = words
|
|
86
|
-
.map((w, idx) => idx === 0 || !SMALL_WORDS.has(w) ? w.charAt(0).toUpperCase() + w.slice(1) : w)
|
|
87
|
-
.join(' ');
|
|
88
|
-
map.set(i, titled);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return map;
|
|
93
|
-
}
|
|
94
|
-
export function DocPlayer({ script, basePath, renderMode = false, autoPlay = false, onEnded, onTimeUpdate, audioProvider: externalAudioProvider, showControls = true, showScrubber = false, muted = false, captionsEnabled: captionsEnabledProp, onCaptionsToggle, onPlaybackStateChange, onControlsReady, isFullscreen = false, onFullscreenToggle, onBlockMarkers, forceViewport, displayMode = 'video', theme, surface, captionStyle = 'standard', }) {
|
|
95
|
-
const isSlideshowMode = displayMode === 'slideshow';
|
|
96
|
-
const isLinearMode = displayMode === 'linear';
|
|
97
|
-
const audioRef = useRef(null);
|
|
98
|
-
const containerRef = useRef(null);
|
|
99
|
-
// Tap-to-toggle play/pause feedback animation
|
|
100
|
-
const [tapFeedback, setTapFeedback] = useState(null);
|
|
101
|
-
const tapFeedbackTimer = useRef();
|
|
102
|
-
// Detect viewport orientation for responsive docs
|
|
103
|
-
// forceViewport takes precedence (used by render mode with explicit viewport and constrained panels)
|
|
104
|
-
// In render mode without forceViewport, default to landscape for backward compatibility
|
|
105
|
-
const { viewport, orientation } = useViewportOrientation();
|
|
106
|
-
const activeViewport = forceViewport || (renderMode ? VIEWPORT_PRESETS.landscape : viewport);
|
|
107
|
-
// Check for debug mode via URL parameter
|
|
108
|
-
const isDebugMode = useMemo(() => {
|
|
109
|
-
if (typeof window === 'undefined')
|
|
110
|
-
return false;
|
|
111
|
-
const params = new URLSearchParams(window.location.search);
|
|
112
|
-
return params.get('debug') === 'true';
|
|
113
|
-
}, []);
|
|
114
|
-
// Use internal HTML5 audio sync if no external provider is given
|
|
115
|
-
const internalAudio = useAudioSync(audioRef, script.audio, basePath);
|
|
116
|
-
// Use external provider if provided, otherwise fall back to internal
|
|
117
|
-
const audio = externalAudioProvider || internalAudio;
|
|
118
|
-
// Destructure for convenience
|
|
119
|
-
const { currentTime, isPlaying, currentSegment, totalDuration, isEnded, isReady: isAudioReady, isAvailable, unavailableMessage, play, pause, toggle, seekTo, skipToSegment: _skipToSegment, restart, } = audio;
|
|
120
|
-
// Refs for frequently-changing values used in the keyboard handler,
|
|
121
|
-
// so the handler callback doesn't need to be recreated every frame.
|
|
122
|
-
const currentTimeRef = useRef(currentTime);
|
|
123
|
-
currentTimeRef.current = currentTime;
|
|
124
|
-
const totalDurationRef = useRef(totalDuration);
|
|
125
|
-
totalDurationRef.current = totalDuration;
|
|
126
|
-
const expandedBlocksLenRef = useRef(0);
|
|
127
|
-
// Tap the player surface to toggle play/pause (disabled in slideshow and linear mode)
|
|
128
|
-
const handleContainerClick = useCallback((e) => {
|
|
129
|
-
if (renderMode || isSlideshowMode || isLinearMode)
|
|
130
|
-
return;
|
|
131
|
-
const target = e.target;
|
|
132
|
-
// Don't toggle if user clicked a control element
|
|
133
|
-
if (target.closest('button, a, input, .doc-player__controls, .doc-player__scrubber, .doc-controls-sidebar, .doc-controls-slideshow'))
|
|
134
|
-
return;
|
|
135
|
-
toggle();
|
|
136
|
-
// Show visual feedback (show the state we're transitioning TO)
|
|
137
|
-
const nextState = isPlaying ? 'play' : 'pause';
|
|
138
|
-
setTapFeedback(nextState);
|
|
139
|
-
clearTimeout(tapFeedbackTimer.current);
|
|
140
|
-
tapFeedbackTimer.current = setTimeout(() => setTapFeedback(null), 600);
|
|
141
|
-
}, [renderMode, toggle, isPlaying, isSlideshowMode, isLinearMode]);
|
|
142
|
-
// Resolve surface (light/dark paper) and apply it to the theme before
|
|
143
|
-
// handing off to downstream renderers. Orthogonal to the editorial theme.
|
|
144
|
-
const autoSurface = useAutoSurface(surface === 'auto');
|
|
145
|
-
const resolvedSurface = surface === 'auto' ? autoSurface : surface;
|
|
146
|
-
const effectiveTheme = useMemo(() => {
|
|
147
|
-
const base = theme ?? DEFAULT_THEME;
|
|
148
|
-
return resolvedSurface ? applySurface(base, resolvedSurface) : base;
|
|
149
|
-
}, [theme, resolvedSurface]);
|
|
150
|
-
// Doc playback hook - pass viewport for responsive template expansion
|
|
151
|
-
const { currentBlock, currentBlockIndex, previousBlock, isEntering, isExiting, blockTime, blockProgress: _blockProgress, docProgress, nextBlock: _nextBlock, prevBlock: _prevBlock, blocks: expandedBlocks, } = useDocPlayback(script, currentTime, activeViewport, renderMode, effectiveTheme);
|
|
152
|
-
// Expand cover block (startBlock) if present - uses active viewport
|
|
153
|
-
const coverBlock = useMemo(() => {
|
|
154
|
-
const startBlockConfig = script.startBlock;
|
|
155
|
-
if (!startBlockConfig)
|
|
156
|
-
return null;
|
|
157
|
-
const context = createTemplateContext(effectiveTheme, 0, 1, activeViewport);
|
|
158
|
-
const layers = expandCoverBlock(startBlockConfig, context);
|
|
159
|
-
return {
|
|
160
|
-
id: 'cover-block',
|
|
161
|
-
startTime: -1, // Not part of timeline
|
|
162
|
-
duration: 0, // Static
|
|
163
|
-
audioSegment: -1,
|
|
164
|
-
layers,
|
|
165
|
-
};
|
|
166
|
-
}, [script.startBlock, activeViewport, effectiveTheme]);
|
|
167
|
-
// Render-mode cover block control: allows Playwright to force-show the cover block
|
|
168
|
-
const [coverForced, setCoverForced] = useState(false);
|
|
169
|
-
// Grace period: keep cover block visible for 3s after first play press
|
|
170
|
-
const [coverGraceActive, setCoverGraceActive] = useState(false);
|
|
171
|
-
const coverGraceTimer = useRef();
|
|
172
|
-
const coverWasShowing = useRef(false);
|
|
173
|
-
// Track whether playback has ever been initiated — prevents the cover block
|
|
174
|
-
// from re-appearing when paused at currentTime === 0 (e.g., no audio source).
|
|
175
|
-
const hasPlayedOnce = useRef(false);
|
|
176
|
-
// Track when cover is showing at rest (before play)
|
|
177
|
-
const atRest = !!(coverBlock &&
|
|
178
|
-
!isPlaying &&
|
|
179
|
-
currentTime === 0 &&
|
|
180
|
-
!hasPlayedOnce.current &&
|
|
181
|
-
!renderMode &&
|
|
182
|
-
!autoPlay);
|
|
183
|
-
if (atRest)
|
|
184
|
-
coverWasShowing.current = true;
|
|
185
|
-
useEffect(() => {
|
|
186
|
-
if (isPlaying && coverWasShowing.current && coverBlock && !renderMode) {
|
|
187
|
-
coverWasShowing.current = false;
|
|
188
|
-
hasPlayedOnce.current = true;
|
|
189
|
-
setCoverGraceActive(true);
|
|
190
|
-
// Intentionally no cleanup: if coverBlock's memoized reference changes
|
|
191
|
-
// mid-grace (e.g., due to a preview re-render), clearing the timer would
|
|
192
|
-
// leave coverGraceActive stuck at true because the effect body won't
|
|
193
|
-
// re-run (coverWasShowing.current is now false).
|
|
194
|
-
coverGraceTimer.current = setTimeout(() => setCoverGraceActive(false), 3000);
|
|
195
|
-
}
|
|
196
|
-
}, [isPlaying, coverBlock, renderMode]);
|
|
197
|
-
// Always clear the grace timer on unmount
|
|
198
|
-
useEffect(() => () => clearTimeout(coverGraceTimer.current), []);
|
|
199
|
-
// Determine if we should show the cover block
|
|
200
|
-
// Show cover when: has cover block, not playing, at time 0, not in render mode
|
|
201
|
-
// OR during the grace period after first play, OR when coverForced (render mode)
|
|
202
|
-
// Cover block is suppressed in slideshow and linear mode — start directly on content
|
|
203
|
-
const showCoverBlock = !isSlideshowMode &&
|
|
204
|
-
!isLinearMode &&
|
|
205
|
-
coverBlock &&
|
|
206
|
-
(coverForced ||
|
|
207
|
-
coverGraceActive ||
|
|
208
|
-
(!isPlaying && currentTime === 0 && !hasPlayedOnce.current && !renderMode && !autoPlay));
|
|
209
|
-
// Auto-play if enabled (wait for audio to be ready)
|
|
210
|
-
// Use a ref to track if we've already auto-played to avoid repeating on every render
|
|
211
|
-
const hasAutoPlayed = useRef(false);
|
|
212
|
-
useEffect(() => {
|
|
213
|
-
if (isAudioReady && autoPlay && !hasAutoPlayed.current) {
|
|
214
|
-
hasAutoPlayed.current = true;
|
|
215
|
-
play();
|
|
216
|
-
}
|
|
217
|
-
}, [isAudioReady, autoPlay, play]);
|
|
218
|
-
// Callback for time updates
|
|
219
|
-
useEffect(() => {
|
|
220
|
-
onTimeUpdate?.(currentTime);
|
|
221
|
-
}, [currentTime, onTimeUpdate]);
|
|
222
|
-
// Callback for ended
|
|
223
|
-
useEffect(() => {
|
|
224
|
-
if (isEnded) {
|
|
225
|
-
onEnded?.();
|
|
226
|
-
}
|
|
227
|
-
}, [isEnded, onEnded]);
|
|
228
|
-
// Expose seekTo globally for render mode (Playwright) and debug mode (testing)
|
|
229
|
-
useEffect(() => {
|
|
230
|
-
if ((renderMode || isDebugMode) && typeof window !== 'undefined') {
|
|
231
|
-
const w = window;
|
|
232
|
-
w.seekTo = (time) => {
|
|
233
|
-
seekTo(time);
|
|
234
|
-
// After React renders the correct block, advance CSS animations
|
|
235
|
-
// (Ken Burns, transitions) to match the doc timeline position.
|
|
236
|
-
// Without this, animations restart from zero on each seekTo because
|
|
237
|
-
// they run on the browser's real clock, not doc time.
|
|
238
|
-
return new Promise((resolve) => {
|
|
239
|
-
requestAnimationFrame(() => {
|
|
240
|
-
// Find the current block's start time
|
|
241
|
-
let blockStartTime = 0;
|
|
242
|
-
for (let i = expandedBlocks.length - 1; i >= 0; i--) {
|
|
243
|
-
if (time >= expandedBlocks[i].startTime) {
|
|
244
|
-
blockStartTime = expandedBlocks[i].startTime;
|
|
245
|
-
break;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
const elapsedMs = (time - blockStartTime) * 1000;
|
|
249
|
-
// Set all CSS animations to the correct timeline position
|
|
250
|
-
document.getAnimations().forEach((anim) => {
|
|
251
|
-
const target = anim.effect?.target;
|
|
252
|
-
if (!target)
|
|
253
|
-
return;
|
|
254
|
-
// Animations on the active block: use current block elapsed time
|
|
255
|
-
if (target.closest('.doc-player__block--active')) {
|
|
256
|
-
anim.currentTime = Math.max(0, elapsedMs);
|
|
257
|
-
}
|
|
258
|
-
// Animations on the exiting block (during crossfade): use current
|
|
259
|
-
// block elapsed for transition animations, keep Ken Burns at their
|
|
260
|
-
// natural position based on when that block started
|
|
261
|
-
// eslint-disable-next-line sonarjs/no-duplicated-branches
|
|
262
|
-
else if (target.closest('.doc-player__block--previous')) {
|
|
263
|
-
anim.currentTime = Math.max(0, elapsedMs);
|
|
264
|
-
}
|
|
265
|
-
});
|
|
266
|
-
// Seek <video> elements in the active block to the correct clip position.
|
|
267
|
-
// Each <video> carries data-clip-start/data-clip-end attributes set by
|
|
268
|
-
// VideoLayer.tsx; we calculate targetTime = clipStart + blockElapsed.
|
|
269
|
-
const blockElapsed = time - blockStartTime;
|
|
270
|
-
const videoSeekPromises = [];
|
|
271
|
-
const activeBlockEl = document.querySelector('.doc-player__block--active');
|
|
272
|
-
if (activeBlockEl) {
|
|
273
|
-
const videos = activeBlockEl.querySelectorAll('video[data-clip-start]');
|
|
274
|
-
videos.forEach((el) => {
|
|
275
|
-
const video = el;
|
|
276
|
-
const clipStart = parseFloat(video.dataset.clipStart || '0');
|
|
277
|
-
const clipEnd = parseFloat(video.dataset.clipEnd || '0');
|
|
278
|
-
const targetTime = Math.min(clipStart + Math.max(0, blockElapsed), clipEnd);
|
|
279
|
-
video.pause();
|
|
280
|
-
video.currentTime = targetTime;
|
|
281
|
-
videoSeekPromises.push(new Promise((r) => {
|
|
282
|
-
if (Math.abs(video.currentTime - targetTime) < 0.1) {
|
|
283
|
-
r();
|
|
284
|
-
}
|
|
285
|
-
else {
|
|
286
|
-
video.addEventListener('seeked', () => r(), { once: true });
|
|
287
|
-
setTimeout(r, 200); // Fallback if seeked never fires
|
|
288
|
-
}
|
|
289
|
-
}));
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
// Wait for video seeks + one more frame for the browser to render
|
|
293
|
-
Promise.all(videoSeekPromises).then(() => {
|
|
294
|
-
requestAnimationFrame(() => resolve());
|
|
295
|
-
});
|
|
296
|
-
});
|
|
297
|
-
});
|
|
298
|
-
};
|
|
299
|
-
w.getDuration = () => {
|
|
300
|
-
// When audio is present totalDuration comes from audio segments.
|
|
301
|
-
// For audio-less docs, compute from block timings instead.
|
|
302
|
-
if (totalDuration > 0)
|
|
303
|
-
return totalDuration;
|
|
304
|
-
if (expandedBlocks.length > 0) {
|
|
305
|
-
const last = expandedBlocks[expandedBlocks.length - 1];
|
|
306
|
-
return last.startTime + last.duration;
|
|
307
|
-
}
|
|
308
|
-
return 0;
|
|
309
|
-
};
|
|
310
|
-
// Expose block metadata for testing -- allows tests to find specific templates
|
|
311
|
-
w.getBlocks = () => expandedBlocks.map((s) => ({
|
|
312
|
-
id: s.id,
|
|
313
|
-
template: s.template ?? 'raw',
|
|
314
|
-
startTime: s.startTime,
|
|
315
|
-
duration: s.duration,
|
|
316
|
-
}));
|
|
317
|
-
// Audio segment info for video production -- returns the actual files in composition order
|
|
318
|
-
w.getAudioSegments = () => script.audio.segments.map((seg) => ({
|
|
319
|
-
src: seg.src,
|
|
320
|
-
name: seg.name,
|
|
321
|
-
duration: seg.duration,
|
|
322
|
-
startTime: seg.startTime,
|
|
323
|
-
}));
|
|
324
|
-
// Caption phrases for SRT/subtitle export
|
|
325
|
-
w.getCaptions = () => script.captions?.phrases?.map((p) => ({
|
|
326
|
-
text: p.text,
|
|
327
|
-
startTime: p.startTime,
|
|
328
|
-
endTime: p.endTime,
|
|
329
|
-
})) || [];
|
|
330
|
-
// Chapter markers for YouTube timestamps -- uses segment titles from sectionHeader blocks
|
|
331
|
-
w.getChapters = () => {
|
|
332
|
-
const titleMap = buildSegmentTitleMap(script);
|
|
333
|
-
return script.audio.segments.map((seg, i) => ({
|
|
334
|
-
title: titleMap.get(i) || seg.name,
|
|
335
|
-
startTime: seg.startTime,
|
|
336
|
-
duration: seg.duration,
|
|
337
|
-
}));
|
|
338
|
-
};
|
|
339
|
-
// Cover block control for video pre-roll -- force-show or hide the cover block
|
|
340
|
-
w.showCover = () => {
|
|
341
|
-
setCoverForced(true);
|
|
342
|
-
return new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
343
|
-
};
|
|
344
|
-
w.hideCover = () => {
|
|
345
|
-
setCoverForced(false);
|
|
346
|
-
return new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
347
|
-
};
|
|
348
|
-
w.hasCoverBlock = () => !!coverBlock;
|
|
349
|
-
}
|
|
350
|
-
return () => {
|
|
351
|
-
if (typeof window !== 'undefined') {
|
|
352
|
-
const w = window;
|
|
353
|
-
delete w.seekTo;
|
|
354
|
-
delete w.getDuration;
|
|
355
|
-
delete w.getBlocks;
|
|
356
|
-
delete w.getAudioSegments;
|
|
357
|
-
delete w.getCaptions;
|
|
358
|
-
delete w.getChapters;
|
|
359
|
-
delete w.showCover;
|
|
360
|
-
delete w.hideCover;
|
|
361
|
-
delete w.hasCoverBlock;
|
|
362
|
-
}
|
|
363
|
-
};
|
|
364
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- script is a stable prop; re-registering on every script change is unnecessary
|
|
365
|
-
}, [renderMode, isDebugMode, seekTo, totalDuration, expandedBlocks, coverBlock]);
|
|
366
|
-
// Caption mode state: cycles through off → standard → social → off
|
|
367
|
-
// The captionStyle prop sets the default active style; captionsEnabledProp
|
|
368
|
-
// can override the initial on/off state.
|
|
369
|
-
const defaultMode = captionsEnabledProp === false ? 'off' : captionStyle || 'standard';
|
|
370
|
-
const [captionMode, setCaptionMode] = useState(defaultMode);
|
|
371
|
-
// Derive captionsEnabled and active style from the mode
|
|
372
|
-
const captionsEnabled = captionMode !== 'off';
|
|
373
|
-
const activeCaptionStyle = captionMode === 'social' ? 'social' : 'standard';
|
|
374
|
-
const setCaptionsEnabled = useCallback((enabled) => {
|
|
375
|
-
// When re-enabling, restore the prop-specified style rather than
|
|
376
|
-
// always defaulting to 'standard'
|
|
377
|
-
setCaptionMode(enabled ? captionStyle || 'standard' : 'off');
|
|
378
|
-
onCaptionsToggle?.(enabled);
|
|
379
|
-
}, [onCaptionsToggle, captionStyle]);
|
|
380
|
-
const cycleCaptionMode = useCallback(() => {
|
|
381
|
-
setCaptionMode((prev) => {
|
|
382
|
-
const next = prev === 'off' ? 'standard' : prev === 'standard' ? 'social' : 'off';
|
|
383
|
-
onCaptionsToggle?.(next !== 'off');
|
|
384
|
-
return next;
|
|
385
|
-
});
|
|
386
|
-
}, [onCaptionsToggle]);
|
|
387
|
-
const hasCaptions = script.captions && script.captions.phrases.length > 0;
|
|
388
|
-
// Map segment indices to human-readable titles (from sectionHeader blocks)
|
|
389
|
-
const segmentTitleMap = useMemo(() => buildSegmentTitleMap(script), [script]);
|
|
390
|
-
// Build shared playback state for extracted controls
|
|
391
|
-
const playbackState = useMemo(() => ({
|
|
392
|
-
isPlaying,
|
|
393
|
-
currentTime,
|
|
394
|
-
totalDuration,
|
|
395
|
-
currentBlockIndex,
|
|
396
|
-
totalBlocks: expandedBlocks.length,
|
|
397
|
-
docProgress,
|
|
398
|
-
hasCaptions: !!hasCaptions,
|
|
399
|
-
captionsEnabled,
|
|
400
|
-
captionMode,
|
|
401
|
-
isFullscreen,
|
|
402
|
-
currentSegmentIndex: currentSegment,
|
|
403
|
-
currentSegmentName: segmentTitleMap.get(currentSegment) ?? script.audio.segments[currentSegment]?.name ?? null,
|
|
404
|
-
currentBlock: currentBlock ?? null,
|
|
405
|
-
}),
|
|
406
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- script.audio.segments is stable within a given script
|
|
407
|
-
[
|
|
408
|
-
isPlaying,
|
|
409
|
-
currentTime,
|
|
410
|
-
totalDuration,
|
|
411
|
-
currentBlockIndex,
|
|
412
|
-
expandedBlocks.length,
|
|
413
|
-
docProgress,
|
|
414
|
-
hasCaptions,
|
|
415
|
-
captionsEnabled,
|
|
416
|
-
captionMode,
|
|
417
|
-
isFullscreen,
|
|
418
|
-
currentSegment,
|
|
419
|
-
segmentTitleMap,
|
|
420
|
-
currentBlock,
|
|
421
|
-
]);
|
|
422
|
-
// Build shared playback actions for extracted controls
|
|
423
|
-
const playbackActions = useMemo(() => ({
|
|
424
|
-
toggle,
|
|
425
|
-
restart,
|
|
426
|
-
seekTo,
|
|
427
|
-
setCaptionsEnabled,
|
|
428
|
-
cycleCaptionMode,
|
|
429
|
-
toggleFullscreen: onFullscreenToggle,
|
|
430
|
-
}), [toggle, restart, seekTo, setCaptionsEnabled, cycleCaptionMode, onFullscreenToggle]);
|
|
431
|
-
// Slide navigation actions for slideshow mode
|
|
432
|
-
// These seek to the target block's startTime and keep the player paused.
|
|
433
|
-
const slideNavActions = useMemo(() => ({
|
|
434
|
-
nextSlide: () => {
|
|
435
|
-
if (currentBlockIndex < expandedBlocks.length - 1) {
|
|
436
|
-
const target = expandedBlocks[currentBlockIndex + 1];
|
|
437
|
-
if (target) {
|
|
438
|
-
seekTo(target.startTime);
|
|
439
|
-
pause();
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
},
|
|
443
|
-
prevSlide: () => {
|
|
444
|
-
if (currentBlockIndex > 0) {
|
|
445
|
-
const target = expandedBlocks[currentBlockIndex - 1];
|
|
446
|
-
if (target) {
|
|
447
|
-
seekTo(target.startTime);
|
|
448
|
-
pause();
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
},
|
|
452
|
-
goToSlide: (index) => {
|
|
453
|
-
if (index >= 0 && index < expandedBlocks.length) {
|
|
454
|
-
const target = expandedBlocks[index];
|
|
455
|
-
if (target) {
|
|
456
|
-
seekTo(target.startTime);
|
|
457
|
-
pause();
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
},
|
|
461
|
-
}), [currentBlockIndex, expandedBlocks, seekTo, pause]);
|
|
462
|
-
// Callback for playback state changes (for external controls)
|
|
463
|
-
useEffect(() => {
|
|
464
|
-
onPlaybackStateChange?.(playbackState);
|
|
465
|
-
}, [playbackState, onPlaybackStateChange]);
|
|
466
|
-
// Callback when controls are ready (for external controls)
|
|
467
|
-
// Fires every time playbackActions change so the external sidebar always holds
|
|
468
|
-
// fresh function references (toggle closes over isPlaying, so it changes often).
|
|
469
|
-
useEffect(() => {
|
|
470
|
-
onControlsReady?.({ play, pause, ...playbackActions });
|
|
471
|
-
}, [play, pause, playbackActions, onControlsReady]);
|
|
472
|
-
// Extract display title from a block (handles both template and expanded blocks)
|
|
473
|
-
const getBlockTitle = useCallback((block) => {
|
|
474
|
-
// For template blocks, extract title from template-specific properties first
|
|
475
|
-
const docBlock = block;
|
|
476
|
-
if (isTemplateBlock(docBlock)) {
|
|
477
|
-
const props = docBlock;
|
|
478
|
-
if (typeof props.title === 'string')
|
|
479
|
-
return props.title;
|
|
480
|
-
if (typeof props.stat === 'string')
|
|
481
|
-
return props.stat;
|
|
482
|
-
if (typeof props.quote === 'string') {
|
|
483
|
-
const firstLine = props.quote.split('\n')[0];
|
|
484
|
-
if (firstLine.length <= 30)
|
|
485
|
-
return firstLine;
|
|
486
|
-
return firstLine.slice(0, 27) + '...';
|
|
487
|
-
}
|
|
488
|
-
if (typeof props.date === 'string')
|
|
489
|
-
return props.date;
|
|
490
|
-
if (typeof props.fact === 'string')
|
|
491
|
-
return props.fact;
|
|
492
|
-
}
|
|
493
|
-
// For expanded blocks with layers, try to find text content
|
|
494
|
-
if (block.layers && Array.isArray(block.layers)) {
|
|
495
|
-
const textLayer = block.layers.find((l) => l.type === 'text');
|
|
496
|
-
if (textLayer?.content?.text) {
|
|
497
|
-
// Get first line of text, truncate if too long
|
|
498
|
-
const firstLine = textLayer.content.text.split('\n')[0];
|
|
499
|
-
if (firstLine.length <= 30)
|
|
500
|
-
return firstLine;
|
|
501
|
-
return firstLine.slice(0, 27) + '...';
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
// Fallback to formatted id
|
|
505
|
-
return block.id.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
506
|
-
}, []);
|
|
507
|
-
// Compute block markers for progress bar (using expanded blocks)
|
|
508
|
-
const blockMarkers = useMemo(() => {
|
|
509
|
-
if (!totalDuration || !expandedBlocks.length)
|
|
510
|
-
return [];
|
|
511
|
-
let prevSegment = -1;
|
|
512
|
-
return expandedBlocks.map((block, index) => {
|
|
513
|
-
const isSectionStart = block.audioSegment !== prevSegment;
|
|
514
|
-
prevSegment = block.audioSegment;
|
|
515
|
-
return {
|
|
516
|
-
block,
|
|
517
|
-
index,
|
|
518
|
-
position: (block.startTime / totalDuration) * 100,
|
|
519
|
-
title: getBlockTitle(block),
|
|
520
|
-
isSectionStart,
|
|
521
|
-
};
|
|
522
|
-
});
|
|
523
|
-
}, [expandedBlocks, totalDuration, getBlockTitle]);
|
|
524
|
-
// Notify parent when block markers are computed
|
|
525
|
-
useEffect(() => {
|
|
526
|
-
if (blockMarkers.length > 0) {
|
|
527
|
-
onBlockMarkers?.(blockMarkers);
|
|
528
|
-
}
|
|
529
|
-
}, [blockMarkers, onBlockMarkers]);
|
|
530
|
-
// Keep expandedBlocks length in a ref so keyboard handler stays stable
|
|
531
|
-
expandedBlocksLenRef.current = expandedBlocks.length;
|
|
532
|
-
// Handle keyboard controls — uses refs for frequently-changing values
|
|
533
|
-
// (currentTime, totalDuration, expandedBlocks.length) to avoid
|
|
534
|
-
// re-registering the event listener on every animation frame.
|
|
535
|
-
const handleKeyDown = useCallback((e) => {
|
|
536
|
-
// Don't capture keyboard events when focus is on an input/textarea
|
|
537
|
-
const activeEl = document.activeElement;
|
|
538
|
-
if (activeEl &&
|
|
539
|
-
(activeEl.tagName === 'INPUT' ||
|
|
540
|
-
activeEl.tagName === 'TEXTAREA' ||
|
|
541
|
-
activeEl.tagName === 'SELECT')) {
|
|
542
|
-
return;
|
|
543
|
-
}
|
|
544
|
-
// Linear mode: no keyboard shortcuts (native scrolling handles it)
|
|
545
|
-
if (isLinearMode)
|
|
546
|
-
return;
|
|
547
|
-
if (isSlideshowMode) {
|
|
548
|
-
// Slideshow mode: arrow keys navigate slides
|
|
549
|
-
switch (e.key) {
|
|
550
|
-
case 'ArrowRight':
|
|
551
|
-
case 'ArrowDown':
|
|
552
|
-
case ' ':
|
|
553
|
-
e.preventDefault();
|
|
554
|
-
slideNavActions.nextSlide();
|
|
555
|
-
break;
|
|
556
|
-
case 'ArrowLeft':
|
|
557
|
-
case 'ArrowUp':
|
|
558
|
-
e.preventDefault();
|
|
559
|
-
slideNavActions.prevSlide();
|
|
560
|
-
break;
|
|
561
|
-
case 'Home':
|
|
562
|
-
e.preventDefault();
|
|
563
|
-
slideNavActions.goToSlide(0);
|
|
564
|
-
break;
|
|
565
|
-
case 'End':
|
|
566
|
-
e.preventDefault();
|
|
567
|
-
slideNavActions.goToSlide(expandedBlocksLenRef.current - 1);
|
|
568
|
-
break;
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
else {
|
|
572
|
-
// Video mode: standard playback controls
|
|
573
|
-
switch (e.key) {
|
|
574
|
-
case ' ':
|
|
575
|
-
e.preventDefault();
|
|
576
|
-
toggle();
|
|
577
|
-
break;
|
|
578
|
-
case 'ArrowRight':
|
|
579
|
-
seekTo(Math.min(currentTimeRef.current + 10, totalDurationRef.current));
|
|
580
|
-
break;
|
|
581
|
-
case 'ArrowLeft':
|
|
582
|
-
seekTo(Math.max(currentTimeRef.current - 10, 0));
|
|
583
|
-
break;
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
}, [isSlideshowMode, isLinearMode, toggle, seekTo, slideNavActions]);
|
|
587
|
-
useEffect(() => {
|
|
588
|
-
if (renderMode)
|
|
589
|
-
return; // No keyboard in render mode
|
|
590
|
-
window.addEventListener('keydown', handleKeyDown);
|
|
591
|
-
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
592
|
-
}, [handleKeyDown, renderMode]);
|
|
593
|
-
// ── Linear mode: render as scrollable document ──────────────────
|
|
594
|
-
if (isLinearMode) {
|
|
595
|
-
return (_jsx("div", { ref: containerRef, className: "doc-player doc-player--linear", style: {
|
|
596
|
-
position: 'relative',
|
|
597
|
-
width: '100%',
|
|
598
|
-
height: '100%',
|
|
599
|
-
overflow: 'hidden',
|
|
600
|
-
}, children: _jsx(LinearDocView, { doc: script, basePath: basePath, viewport: activeViewport, theme: theme, surface: surface }) }));
|
|
601
|
-
}
|
|
602
|
-
return (_jsxs("div", { ref: containerRef, className: "doc-player", onClick: handleContainerClick, style: {
|
|
603
|
-
position: 'relative',
|
|
604
|
-
width: '100%',
|
|
605
|
-
aspectRatio: `${activeViewport.width} / ${activeViewport.height}`,
|
|
606
|
-
margin: '0 auto',
|
|
607
|
-
overflow: 'hidden',
|
|
608
|
-
cursor: renderMode ? undefined : 'pointer',
|
|
609
|
-
}, children: [_jsx("audio", { ref: audioRef, preload: "auto", muted: muted }), _jsxs("div", { className: "doc-player__viewport", children: [showCoverBlock && coverBlock && (_jsx("div", { className: "doc-player__block doc-player__block--cover", children: _jsx(BlockRenderer, { block: coverBlock, blockTime: 0, basePath: basePath, isEntering: false, viewport: activeViewport }) })), !showCoverBlock && previousBlock && isExiting && (_jsx("div", { className: "doc-player__block doc-player__block--previous", children: _jsx(BlockRenderer, { block: previousBlock, blockTime: blockTime, basePath: basePath, isExiting: true, viewport: activeViewport }) })), !showCoverBlock && currentBlock && (_jsx("div", { className: "doc-player__block doc-player__block--active", children: _jsx(BlockRenderer, { block: currentBlock, blockTime: blockTime, basePath: basePath, isEntering: isEntering, viewport: activeViewport, isPlaying: isPlaying }) })), hasCaptions && (renderMode ? captionsEnabled : true) && (_jsx(CaptionOverlay, { captions: script.captions, currentTime: currentTime, enabled: captionsEnabled && (renderMode || isPlaying || currentTime > 0), fontSize: 16, captionStyle: activeCaptionStyle, theme: effectiveTheme, viewport: activeViewport })), isDebugMode && (_jsxs("div", { className: "doc-player__debug", style: {
|
|
610
|
-
position: 'absolute',
|
|
611
|
-
top: '8px',
|
|
612
|
-
right: '8px',
|
|
613
|
-
padding: '8px 12px',
|
|
614
|
-
background: 'rgba(0, 0, 0, 0.85)',
|
|
615
|
-
borderRadius: '6px',
|
|
616
|
-
color: '#00ff00',
|
|
617
|
-
fontFamily: 'monospace',
|
|
618
|
-
fontSize: '11px',
|
|
619
|
-
lineHeight: '1.5',
|
|
620
|
-
zIndex: 200,
|
|
621
|
-
maxWidth: '280px',
|
|
622
|
-
pointerEvents: 'none',
|
|
623
|
-
textAlign: 'left',
|
|
624
|
-
}, children: [_jsx("div", { style: { color: '#ffcc00', fontWeight: 'bold', marginBottom: '4px' }, children: "DEBUG MODE" }), _jsxs("div", { children: [_jsx("span", { style: { color: '#888' }, children: "template:" }), ' ', _jsx("span", { style: { color: '#ff6b6b' }, children: currentBlock?.template ?? 'raw' })] }), _jsxs("div", { children: [_jsx("span", { style: { color: '#888' }, children: "block:" }), " ", currentBlockIndex + 1, "/", expandedBlocks.length, ' ', _jsxs("span", { style: { color: '#666' }, children: ["(", currentBlock?.id || 'none', ")"] })] }), _jsxs("div", { children: [_jsx("span", { style: { color: '#888' }, children: "time:" }), " ", currentTime.toFixed(2), "s /", ' ', totalDuration.toFixed(1), "s", ' ', _jsxs("span", { style: { color: '#666' }, children: ["(progress: ", (docProgress * 100).toFixed(1), "%, scriptDur:", ' ', script.duration.toFixed(1), ")"] })] }), _jsxs("div", { children: [_jsx("span", { style: { color: '#888' }, children: "blockTime:" }), " ", blockTime.toFixed(2), "s /", ' ', (currentBlock?.duration || 0).toFixed(1), "s"] }), _jsxs("div", { children: [_jsx("span", { style: { color: '#888' }, children: "segment:" }), " ", currentSegment, "/", script.audio.segments.length - 1, ' ', _jsxs("span", { style: { color: '#666' }, children: ["(", script.audio.segments[currentSegment]?.name || 'none', ")"] })] }), _jsxs("div", { children: [_jsx("span", { style: { color: '#888' }, children: "viewport:" }), ' ', activeViewport.name || `${activeViewport.width}x${activeViewport.height}`, ' ', _jsxs("span", { style: { color: '#666' }, children: ["(", orientation, ")"] })] }), _jsxs("div", { children: [_jsx("span", { style: { color: '#888' }, children: "playing:" }), ' ', _jsx("span", { style: { color: isPlaying ? '#4ade80' : '#f87171' }, children: isPlaying ? 'yes' : 'no' }), showCoverBlock && _jsx("span", { style: { color: '#60a5fa' }, children: " (cover)" })] }), hasCaptions &&
|
|
625
|
-
(() => {
|
|
626
|
-
const debugPhrase = getCaptionAtTime(script.captions, currentTime);
|
|
627
|
-
const debugEnabled = captionsEnabled && (isPlaying || currentTime > 0);
|
|
628
|
-
return (_jsxs(Fragment, { children: [_jsxs("div", { children: [_jsx("span", { style: { color: '#888' }, children: "captions:" }), ' ', script.captions?.phrases.length || 0, " phrases", ' ', _jsxs("span", { style: { color: captionsEnabled ? '#4ade80' : '#666' }, children: ["(", captionsEnabled ? 'on' : 'off', ")"] })] }), _jsxs("div", { children: [_jsx("span", { style: { color: '#888' }, children: "cc.enabled:" }), ' ', _jsx("span", { style: { color: debugEnabled ? '#4ade80' : '#f87171' }, children: String(debugEnabled) }), ' ', _jsxs("span", { style: { color: '#666' }, children: ["(playing=", String(isPlaying), " t>0=", String(currentTime > 0), ")"] })] }), _jsxs("div", { children: [_jsx("span", { style: { color: '#888' }, children: "cc.phrase:" }), ' ', _jsx("span", { style: { color: debugPhrase ? '#4ade80' : '#f87171' }, children: debugPhrase ? `"${debugPhrase.text.slice(0, 30)}..."` : 'null' })] }), debugPhrase && (_jsxs("div", { children: [_jsx("span", { style: { color: '#888' }, children: "cc.range:" }), ' ', _jsxs("span", { style: { color: '#60a5fa' }, children: [debugPhrase.startTime.toFixed(2), "-", debugPhrase.endTime.toFixed(2)] })] }))] }));
|
|
629
|
-
})()] }))] }), !isAvailable && unavailableMessage && (_jsxs("div", { className: "doc-player__unavailable", style: {
|
|
630
|
-
position: 'absolute',
|
|
631
|
-
top: 0,
|
|
632
|
-
left: 0,
|
|
633
|
-
right: 0,
|
|
634
|
-
bottom: 0,
|
|
635
|
-
display: 'flex',
|
|
636
|
-
alignItems: 'center',
|
|
637
|
-
justifyContent: 'center',
|
|
638
|
-
flexDirection: 'column',
|
|
639
|
-
gap: '16px',
|
|
640
|
-
background: 'rgba(0, 0, 0, 0.7)',
|
|
641
|
-
color: 'rgba(255, 255, 255, 0.9)',
|
|
642
|
-
fontSize: '14px',
|
|
643
|
-
zIndex: 50,
|
|
644
|
-
}, children: [_jsx("span", { style: { fontSize: '32px' }, children: "\uD83D\uDD0A" }), _jsx("span", { children: unavailableMessage })] })), !renderMode && !isSlideshowMode && showControls && (_jsx(DocControlsOverlay, { state: playbackState, actions: playbackActions, blockMarkers: blockMarkers, expandedBlocks: expandedBlocks, getBlockTitle: getBlockTitle })), !renderMode && !isSlideshowMode && !showControls && showScrubber && (_jsx("div", { className: "doc-player__scrubber", style: {
|
|
645
|
-
position: 'absolute',
|
|
646
|
-
bottom: 0,
|
|
647
|
-
left: 0,
|
|
648
|
-
right: 0,
|
|
649
|
-
padding: '12px 16px 8px',
|
|
650
|
-
background: 'linear-gradient(transparent, rgba(0,0,0,0.6))',
|
|
651
|
-
display: 'flex',
|
|
652
|
-
alignItems: 'center',
|
|
653
|
-
zIndex: 100,
|
|
654
|
-
}, children: _jsx(DocProgressBar, { state: playbackState, actions: playbackActions, blockMarkers: blockMarkers, expandedBlocks: expandedBlocks, getBlockTitle: getBlockTitle }) })), !renderMode && isSlideshowMode && (_jsx(DocControlsSlideshow, { state: playbackState, slideNav: slideNavActions })), !isSlideshowMode && tapFeedback && (_jsx("div", { className: "doc-player__tap-feedback", children: _jsx("svg", { viewBox: "0 0 24 24", fill: "white", width: "48", height: "48", children: tapFeedback === 'pause' ? (_jsx("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" })) : (_jsx("path", { d: "M8 5v14l11-7z" })) }) }, Date.now()))] }));
|
|
655
|
-
}
|
|
656
|
-
export default DocPlayer;
|
|
657
|
-
//# sourceMappingURL=DocPlayer.js.map
|