@bycrux/editor 0.12.0 → 1.0.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/package.json +3 -1
- package/src/ControlsInfoModal.tsx +251 -61
- package/src/__tests__/ControlsInfoModal.test.tsx +43 -0
- package/src/__tests__/adapter.test.ts +59 -1
- package/src/__tests__/schema-assignability.test.ts +93 -0
- package/src/__tests__/schema.test.ts +15 -0
- package/src/__tests__/video-adapter-contract.test.ts +128 -5
- package/src/carousel/AddElementMenu.tsx +10 -4
- package/src/carousel/CarouselEditor.tsx +46 -8
- package/src/carousel/CarouselRenderModal.tsx +15 -10
- package/src/carousel/OverlayPicker.tsx +10 -3
- package/src/carousel/SlidePropertyPanel.tsx +43 -21
- package/src/components/FilmstripScrubber.tsx +277 -0
- package/src/components/__tests__/FilmstripScrubber.test.tsx +216 -0
- package/src/engine/__tests__/audio-clock.test.ts +655 -0
- package/src/engine/__tests__/audio-worklet-source.test.ts +316 -0
- package/src/engine/__tests__/batch-planner.test.ts +298 -0
- package/src/engine/__tests__/decode-worker-source.test.ts +249 -0
- package/src/engine/__tests__/demux-ranged.test.ts +495 -0
- package/src/engine/__tests__/demux-truncated.test.ts +136 -0
- package/src/engine/__tests__/demux.test.ts +305 -0
- package/src/engine/__tests__/eligibility.test.ts +146 -0
- package/src/engine/__tests__/engine-recovery.test.ts +263 -0
- package/src/engine/__tests__/engine.test.ts +326 -0
- package/src/engine/__tests__/frame-server-ranged.test.ts +261 -0
- package/src/engine/__tests__/frame-server.test.ts +326 -0
- package/src/engine/__tests__/media-loader-ranged.test.ts +289 -0
- package/src/engine/__tests__/media-loader.test.ts +100 -0
- package/src/engine/__tests__/scheduler-crop.test.ts +353 -0
- package/src/engine/__tests__/scheduler.test.ts +1310 -0
- package/src/engine/__tests__/scrub-resolve.test.ts +96 -0
- package/src/engine/__tests__/scrub-source.test.ts +195 -0
- package/src/engine/__tests__/source-host.test.ts +455 -0
- package/src/engine/__tests__/time-stretch.test.ts +182 -0
- package/src/engine/audio-clock.ts +1179 -0
- package/src/engine/audio-worklet-source.ts +160 -0
- package/src/engine/batch-planner.ts +308 -0
- package/src/engine/debug-hud.tsx +54 -0
- package/src/engine/decode-worker-source.ts +142 -0
- package/src/engine/demux.ts +900 -0
- package/src/engine/eligibility.ts +140 -0
- package/src/engine/frame-server.ts +644 -0
- package/src/engine/index.ts +950 -0
- package/src/engine/media-loader.ts +380 -0
- package/src/engine/mp4box.d.ts +214 -0
- package/src/engine/scheduler.ts +1324 -0
- package/src/engine/scrub-resolve.ts +66 -0
- package/src/engine/scrub-source.ts +496 -0
- package/src/engine/time-stretch.ts +224 -0
- package/src/index.ts +75 -1
- package/src/preview/OverlayPreview.tsx +2 -24
- package/src/schema.ts +134 -2
- package/src/state/__tests__/use-project-sync.test.tsx +56 -0
- package/src/state/use-project-sync.ts +17 -0
- package/src/test-setup.ts +32 -0
- package/src/text/FontPicker.tsx +85 -51
- package/src/text/TextFormattingToolbar.tsx +6 -1
- package/src/text/__tests__/FontPicker.options.test.ts +43 -0
- package/src/text/__tests__/TextFormattingToolbar.test.tsx +4 -4
- package/src/theme.ts +108 -3
- package/src/types.ts +586 -22
- package/src/ui/Loader.tsx +64 -0
- package/src/ui/NumberField.tsx +254 -0
- package/src/ui/Slider.tsx +128 -0
- package/src/ui/Tooltip.tsx +118 -0
- package/src/ui/__tests__/NumberField.test.tsx +298 -0
- package/src/ui/__tests__/Slider.test.tsx +150 -0
- package/src/ui/__tests__/Tooltip.test.tsx +105 -0
- package/src/ui/__tests__/usePersistentState.test.tsx +112 -0
- package/src/ui/badge.tsx +12 -4
- package/src/ui/index.ts +6 -0
- package/src/ui/input.tsx +1 -1
- package/src/ui/select.tsx +1 -1
- package/src/ui/switch.tsx +12 -2
- package/src/ui/textarea.tsx +1 -1
- package/src/ui/usePersistentState.ts +63 -0
- package/src/video/AudioPolishModal.tsx +983 -0
- package/src/video/CaptionListPanel.test.tsx +944 -0
- package/src/video/CaptionListPanel.tsx +1106 -0
- package/src/video/CaptionRegenModal.tsx +37 -9
- package/src/video/CaptionSpecimen.tsx +161 -0
- package/src/video/CaptionStyleGallery.tsx +466 -0
- package/src/video/CommandPalette.tsx +165 -0
- package/src/video/ImageToneMenu.tsx +6 -2
- package/src/video/OverlayInspector.tsx +977 -0
- package/src/video/RenderModal.tsx +1035 -62
- package/src/video/VersionCompare.tsx +258 -0
- package/src/video/VersionPanel.tsx +117 -42
- package/src/video/VideoEditor.tsx +2202 -251
- package/src/video/__tests__/AudioPolishModal.test.tsx +825 -0
- package/src/video/__tests__/CaptionListPanel.font.test.tsx +397 -0
- package/src/video/__tests__/CaptionListPanel.generate.test.tsx +153 -0
- package/src/video/__tests__/CaptionRegenModal.test.tsx +29 -0
- package/src/video/__tests__/CaptionSpecimen.test.tsx +213 -0
- package/src/video/__tests__/CaptionStyleGallery.test.tsx +362 -0
- package/src/video/__tests__/CommandPalette.test.tsx +119 -0
- package/src/video/__tests__/OverlayInspector.test.tsx +1367 -0
- package/src/video/__tests__/RenderModal.exportControls.test.tsx +319 -0
- package/src/video/__tests__/RenderModal.options.test.tsx +562 -0
- package/src/video/__tests__/RenderModal.progress.test.ts +65 -0
- package/src/video/__tests__/VersionCompare.test.tsx +182 -0
- package/src/video/__tests__/VersionPanel.test.tsx +279 -0
- package/src/video/__tests__/VideoEditor.audioPolish.test.tsx +241 -0
- package/src/video/__tests__/VideoEditor.captionDelete.test.tsx +147 -0
- package/src/video/__tests__/VideoEditor.captionGesture.test.tsx +170 -0
- package/src/video/__tests__/VideoEditor.captionSeam.test.tsx +134 -0
- package/src/video/__tests__/VideoEditor.clipKeyframes.test.tsx +59 -0
- package/src/video/__tests__/VideoEditor.context.test.tsx +44 -0
- package/src/video/__tests__/VideoEditor.editFocus.test.tsx +55 -0
- package/src/video/__tests__/VideoEditor.keymap.test.tsx +725 -0
- package/src/video/__tests__/VideoEditor.layout.test.tsx +338 -0
- package/src/video/__tests__/VideoEditor.propertiesPanel.test.tsx +765 -0
- package/src/video/__tests__/VideoEditor.rippleDeleteCaptions.test.tsx +159 -0
- package/src/video/__tests__/VideoEditor.sourcePreview.test.tsx +122 -0
- package/src/video/__tests__/VideoEditor.test.tsx +403 -50
- package/src/video/__tests__/audioMagnet.test.ts +158 -0
- package/src/video/__tests__/audioPolish.test.ts +1202 -0
- package/src/video/__tests__/captionActiveWord.test.ts +107 -0
- package/src/video/__tests__/captionLanes.test.ts +262 -0
- package/src/video/__tests__/captionPositioning.test.tsx +8 -3
- package/src/video/__tests__/captionWordFloor.test.ts +228 -0
- package/src/video/__tests__/clipboard-ops.test.ts +430 -0
- package/src/video/__tests__/cuts.insert.test.ts +244 -0
- package/src/video/__tests__/cuts.test.ts +1213 -34
- package/src/video/__tests__/export-limits.test.ts +195 -0
- package/src/video/__tests__/hover-scrub.test.ts +163 -0
- package/src/video/__tests__/keyframeOps.canKeyframeProp.test.ts +61 -0
- package/src/video/__tests__/keyframeOps.test.ts +643 -0
- package/src/video/__tests__/keymap.test.tsx +171 -0
- package/src/video/__tests__/render-progress.test.tsx +20 -4
- package/src/video/__tests__/shuttle.test.ts +210 -0
- package/src/video/__tests__/source-preview.test.ts +60 -0
- package/src/video/__tests__/timecode.test.ts +77 -0
- package/src/video/__tests__/use-report-context.test.tsx +101 -0
- package/src/video/audioMagnet.ts +72 -0
- package/src/video/audioPolish.ts +774 -0
- package/src/video/captionActiveWord.ts +74 -0
- package/src/video/captionLanes.ts +202 -0
- package/src/video/captionRepair.ts +9 -5
- package/src/video/captionStyleDefaults.ts +100 -0
- package/src/video/captionWordFloor.ts +83 -0
- package/src/video/clipboard-ops.ts +377 -0
- package/src/video/cuts.ts +713 -37
- package/src/video/export-limits.ts +102 -0
- package/src/video/hover-scrub.ts +102 -0
- package/src/video/keyframeOps.ts +384 -0
- package/src/video/keymap.ts +146 -0
- package/src/video/panels/ClipPropertiesPanel.tsx +706 -0
- package/src/video/panels/LeftPanelTabs.tsx +187 -0
- package/src/video/panels/OverlayContentPanel.tsx +351 -0
- package/src/video/panels/TabNav.tsx +60 -0
- package/src/video/panels/__tests__/ClipPropertiesPanel.test.tsx +645 -0
- package/src/video/panels/__tests__/LeftPanelTabs.test.tsx +189 -0
- package/src/video/panels/__tests__/OverlayContentPanel.test.tsx +222 -0
- package/src/video/panels/__tests__/TabNav.test.tsx +71 -0
- package/src/video/preview/CaptionPreview.tsx +57 -69
- package/src/video/preview/EngineSurface.tsx +103 -0
- package/src/video/preview/OverlayItemsLayer.tsx +315 -103
- package/src/video/preview/PreviewPlayer.tsx +337 -56
- package/src/video/preview/SocialPreviewMenu.tsx +214 -0
- package/src/video/preview/SocialSafeZoneOverlay.tsx +478 -0
- package/src/video/preview/__tests__/CaptionPreview.fonts.test.tsx +97 -0
- package/src/video/preview/__tests__/EngineSurface.test.tsx +114 -0
- package/src/video/preview/__tests__/OverlayItemsLayer.edit.test.tsx +4 -2
- package/src/video/preview/__tests__/OverlayItemsLayer.keyframes.test.tsx +363 -0
- package/src/video/preview/__tests__/OverlayItemsLayer.selection.test.tsx +329 -0
- package/src/video/preview/__tests__/PreviewPlayer.engine.test.tsx +139 -0
- package/src/video/preview/__tests__/SocialPreviewMenu.test.tsx +121 -0
- package/src/video/preview/__tests__/SocialSafeZoneOverlay.test.tsx +165 -0
- package/src/video/preview/__tests__/captionDragState.test.ts +120 -1
- package/src/video/preview/__tests__/latencyCompensation.test.tsx +451 -0
- package/src/video/preview/__tests__/proxySupport.test.ts +75 -0
- package/src/video/preview/__tests__/transformStyle.test.ts +29 -1
- package/src/video/preview/__tests__/useDragOverlay.perAxis.test.ts +197 -0
- package/src/video/preview/__tests__/useEnginePlayback.test.tsx +530 -0
- package/src/video/preview/__tests__/useVideoPlayback.corpus.test.ts +313 -0
- package/src/video/preview/__tests__/useVideoPlayback.test.ts +38 -5
- package/src/video/preview/__tests__/useVideoPlayback.trackAudio.test.ts +278 -0
- package/src/video/preview/audio-context.ts +111 -0
- package/src/video/preview/captionDragState.ts +91 -1
- package/src/video/preview/proxySupport.ts +86 -0
- package/src/video/preview/transformStyle.ts +22 -12
- package/src/video/preview/useDragOverlay.ts +92 -18
- package/src/video/preview/useEnginePlayback.ts +625 -0
- package/src/video/preview/useVideoPlayback.ts +211 -167
- package/src/video/sdrCurves.ts +56 -0
- package/src/video/shuttle.ts +159 -0
- package/src/video/source-preview.ts +66 -0
- package/src/video/timecode.ts +59 -0
- package/src/video/timeline/EditableSegment.tsx +1 -1
- package/src/video/timeline/Scrubber.tsx +46 -174
- package/src/video/timeline/SpeedControl.tsx +95 -0
- package/src/video/timeline/Timeline.tsx +1061 -328
- package/src/video/timeline/TimelineContext.ts +17 -10
- package/src/video/timeline/TrackGutter.tsx +560 -0
- package/src/video/timeline/TrackSettingsPopover.tsx +228 -0
- package/src/video/timeline/VolumeControl.tsx +113 -0
- package/src/video/timeline/__tests__/Timeline.backgroundClick.test.tsx +110 -0
- package/src/video/timeline/__tests__/Timeline.crossfade.test.tsx +104 -0
- package/src/video/timeline/__tests__/Timeline.fadeCurveMenu.test.tsx +174 -0
- package/src/video/timeline/__tests__/Timeline.keyframeDelete.test.tsx +273 -0
- package/src/video/timeline/__tests__/Timeline.keyframeFollow.test.tsx +215 -0
- package/src/video/timeline/__tests__/Timeline.keyframeMenu.test.tsx +253 -0
- package/src/video/timeline/__tests__/Timeline.keymap.test.tsx +372 -0
- package/src/video/timeline/__tests__/Timeline.subcutRegen.test.tsx +351 -0
- package/src/video/timeline/__tests__/TrackGutter.test.tsx +372 -0
- package/src/video/timeline/__tests__/_canvasSelect.test.tsx +273 -0
- package/src/video/timeline/__tests__/_canvasSelect.ts +414 -0
- package/src/video/timeline/__tests__/dragdrop-math.test.ts +135 -0
- package/src/video/timeline/__tests__/effectiveItemAudio.test.ts +50 -0
- package/src/video/timeline/__tests__/enabledTrackItems.test.ts +164 -0
- package/src/video/timeline/__tests__/moveItemAcrossTracks.test.ts +363 -0
- package/src/video/timeline/__tests__/multiSelectOps.test.ts +447 -0
- package/src/video/timeline/__tests__/placement.test.ts +278 -0
- package/src/video/timeline/__tests__/resizeWindowedItem.test.ts +140 -0
- package/src/video/timeline/__tests__/timeline-model.test.ts +576 -0
- package/src/video/timeline/__tests__/visualItemLabel.test.ts +69 -0
- package/src/video/timeline/canvas/TimelineCanvas.tsx +1447 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.drop.test.tsx +439 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.edgeScroll.test.tsx +346 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.panefill.test.tsx +122 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.pendingDrops.test.tsx +316 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.pointer.test.tsx +606 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.test.tsx +407 -0
- package/src/video/timeline/canvas/__tests__/clip-bands.test.ts +77 -0
- package/src/video/timeline/canvas/__tests__/draw.test.ts +2198 -0
- package/src/video/timeline/canvas/__tests__/fade-curve.test.ts +187 -0
- package/src/video/timeline/canvas/__tests__/filmstrips.test.ts +561 -0
- package/src/video/timeline/canvas/__tests__/hit-test.test.ts +818 -0
- package/src/video/timeline/canvas/__tests__/pending-drop.test.ts +210 -0
- package/src/video/timeline/canvas/__tests__/pointer-machine.test.ts +3358 -0
- package/src/video/timeline/canvas/__tests__/snap.test.ts +257 -0
- package/src/video/timeline/canvas/__tests__/viewport.test.ts +399 -0
- package/src/video/timeline/canvas/__tests__/waveforms.test.ts +946 -0
- package/src/video/timeline/canvas/clip-bands.ts +56 -0
- package/src/video/timeline/canvas/draw.ts +2187 -0
- package/src/video/timeline/canvas/fade-curve.ts +111 -0
- package/src/video/timeline/canvas/filmstrips.ts +418 -0
- package/src/video/timeline/canvas/hit-test.ts +501 -0
- package/src/video/timeline/canvas/keyframe-strip.ts +73 -0
- package/src/video/timeline/canvas/pointer-machine.ts +1828 -0
- package/src/video/timeline/canvas/snap.ts +232 -0
- package/src/video/timeline/canvas/viewport.ts +457 -0
- package/src/video/timeline/canvas/waveforms.ts +664 -0
- package/src/video/timeline/makeCaptionEdit.ts +5 -1
- package/src/video/timeline/multiSelectOps.ts +214 -55
- package/src/video/timeline/placement.ts +282 -0
- package/src/video/timeline/timeline-model.ts +919 -0
- package/src/video/timeline/useItemDragDrop.ts +151 -178
- package/src/video/timeline/useTimelineZoom.ts +25 -60
- package/src/video/timeline/utils.ts +0 -12
- package/src/video/use-report-context.ts +75 -0
- package/src/video/preview/OverlayPropsModal.tsx +0 -292
- package/src/video/preview/__tests__/OverlayPropsModal.test.tsx +0 -32
- package/src/video/timeline/AudioTrackRow.tsx +0 -404
- package/src/video/timeline/AudioWaveformLayer.tsx +0 -117
- package/src/video/timeline/CaptionTrackRow.tsx +0 -235
- package/src/video/timeline/PlayheadLine.tsx +0 -18
- package/src/video/timeline/TranscriptModal.tsx +0 -70
- package/src/video/timeline/TranscriptPanel.tsx +0 -273
- package/src/video/timeline/VisualTrackRow.tsx +0 -300
- package/src/video/timeline/__tests__/CaptionTrackRow.test.tsx +0 -241
- package/src/video/timeline/__tests__/PlayheadLine.test.tsx +0 -60
- package/src/video/timeline/__tests__/TranscriptModal.test.tsx +0 -41
- package/src/video/timeline/__tests__/TranscriptPanel.test.tsx +0 -184
- package/src/video/timeline/__tests__/useItemDragDrop.test.ts +0 -72
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The drag/resize arithmetic the timeline's gestures are built out of.
|
|
3
|
+
*
|
|
4
|
+
* This module once also held `useItemDragDrop`, a React hook that wired these
|
|
5
|
+
* functions to mouse events for the DOM track rows. Those rows are retired
|
|
6
|
+
* and the hook went with them: the canvas timeline's pointer machine
|
|
7
|
+
* (`canvas/pointer-machine.ts`) owns gesture handling now, and calls straight
|
|
8
|
+
* into three of the five pure functions below — `DRAG_THRESHOLD_PX`,
|
|
9
|
+
* `computeResizedItem`, `resizeWindowedItem` — with times it derived from its
|
|
10
|
+
* own viewport and snapped with hysteresis (`canvas/snap.ts`). The other two,
|
|
11
|
+
* `snapToBoundaries` and `snapMovedSpan`, have no production caller any more
|
|
12
|
+
* (canvas snapping lives in `canvas/snap.ts`) and are kept deliberately,
|
|
13
|
+
* test-covered only. Nothing here touches the DOM or React any more.
|
|
14
|
+
*/
|
|
2
15
|
|
|
3
16
|
export interface Draggable {
|
|
4
17
|
id: string
|
|
@@ -9,199 +22,159 @@ export interface Draggable {
|
|
|
9
22
|
normalizedInPoint?: number
|
|
10
23
|
type?: string
|
|
11
24
|
sourceDuration?: number
|
|
25
|
+
/** Per-clip playback speed S (video only). A clip at S consumes S source
|
|
26
|
+
* seconds per timeline second, so every timeline↔source conversion here
|
|
27
|
+
* carries it. Absent/1 is a strict no-op. */
|
|
28
|
+
speed?: number
|
|
12
29
|
}
|
|
13
30
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
/** Vertical delta in raw pixels from initial position */
|
|
20
|
-
dy: number
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface UseItemDragDropConfig {
|
|
24
|
-
totalDuration: number
|
|
25
|
-
snapBoundaries: number[]
|
|
26
|
-
snapThresholdPx?: number // default 8
|
|
27
|
-
/** Element used for getBoundingClientRect() → pixel-to-time conversion */
|
|
28
|
-
scrollRef: React.RefObject<HTMLDivElement | null>
|
|
29
|
-
/** Timeline zoom factor — content width is zoom × scrollRef.width. Required so
|
|
30
|
-
* pixel-to-time conversion matches the visible scale (without this, drag/resize
|
|
31
|
-
* is `zoom`× too sensitive when zoomed in). */
|
|
32
|
-
zoomRef?: React.RefObject<number>
|
|
33
|
-
draggedFlagRef?: React.MutableRefObject<boolean> // sets true during drag (click suppression)
|
|
34
|
-
}
|
|
31
|
+
/** The floor on an item's timeline duration. At speed S the source window
|
|
32
|
+
* floor follows from it (`MIN_DURATION · S`), so only this one is needed.
|
|
33
|
+
* Module-local: it constrains the two resize functions below and nothing
|
|
34
|
+
* outside this file (`cuts.ts` has a same-valued constant of its own). */
|
|
35
|
+
const MIN_DURATION = 0.1
|
|
35
36
|
|
|
36
37
|
/** A press must travel at least this many pixels before it becomes a drag.
|
|
37
38
|
* Below it the gesture stays a click. Without this threshold, a single pixel of
|
|
38
39
|
* pointer drift during a click flips the drag flag, and the timeline suppresses
|
|
39
40
|
* click-to-select — so clips/overlays can't be selected (or deleted), which
|
|
40
|
-
* bites constantly on trackpads and touchscreens where clicks always jitter.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
): void {
|
|
62
|
-
e.stopPropagation()
|
|
63
|
-
e.preventDefault()
|
|
64
|
-
|
|
65
|
-
const initX = e.clientX
|
|
66
|
-
const initTime = edge === 'start' ? item.start : item.end
|
|
67
|
-
|
|
68
|
-
function computeResized(moveE: MouseEvent): Draggable {
|
|
69
|
-
if (!scrollRef.current) return item
|
|
70
|
-
const rect = scrollRef.current.getBoundingClientRect()
|
|
71
|
-
const contentWidth = rect.width * (zoomRef?.current ?? 1)
|
|
72
|
-
const dt = ((moveE.clientX - initX) / contentWidth) * totalDuration
|
|
73
|
-
const raw = Math.max(0, Math.min(totalDuration, initTime + dt))
|
|
74
|
-
|
|
75
|
-
// Snap to any boundary within threshold
|
|
76
|
-
const snapThreshold = (snapThresholdPx / contentWidth) * totalDuration
|
|
77
|
-
let t = raw
|
|
78
|
-
let bestDist = snapThreshold
|
|
79
|
-
for (const b of snapBoundaries) {
|
|
80
|
-
const d = Math.abs(raw - b)
|
|
81
|
-
if (d < bestDist) {
|
|
82
|
-
bestDist = d
|
|
83
|
-
t = b
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (edge === 'start') {
|
|
88
|
-
const newStart = Math.min(t, item.end - 0.1)
|
|
89
|
-
if (item.type !== 'video') return { ...item, start: newStart }
|
|
90
|
-
const dtActual = newStart - item.start
|
|
91
|
-
return {
|
|
92
|
-
...item,
|
|
93
|
-
start: newStart,
|
|
94
|
-
inPoint: Math.min(
|
|
95
|
-
Math.max(0, (item.inPoint ?? 0) + dtActual),
|
|
96
|
-
(item.outPoint ?? (item.inPoint ?? 0) + (item.end - item.start)) - 0.1,
|
|
97
|
-
),
|
|
98
|
-
}
|
|
99
|
-
} else {
|
|
100
|
-
const newEnd = Math.max(t, item.start + 0.1)
|
|
101
|
-
if (item.type !== 'video') return { ...item, end: newEnd }
|
|
102
|
-
const origOut = item.outPoint ?? (item.inPoint ?? 0) + (item.end - item.start)
|
|
103
|
-
const dtActual = newEnd - item.end
|
|
104
|
-
return {
|
|
105
|
-
...item,
|
|
106
|
-
end: newEnd,
|
|
107
|
-
outPoint: Math.max(
|
|
108
|
-
(item.inPoint ?? 0) + 0.1,
|
|
109
|
-
Math.min(origOut + dtActual, item.sourceDuration ?? Infinity),
|
|
110
|
-
),
|
|
111
|
-
}
|
|
112
|
-
}
|
|
41
|
+
* bites constantly on trackpads and touchscreens where clicks always jitter.
|
|
42
|
+
*
|
|
43
|
+
* Exported for the canvas pointer machine (SP5 T5), which has the same
|
|
44
|
+
* click-vs-drag decision to make and must make it at the same distance. */
|
|
45
|
+
export const DRAG_THRESHOLD_PX = 4
|
|
46
|
+
|
|
47
|
+
// ── Pure drag/resize math ────────────────────────────────────────────────
|
|
48
|
+
// Extracted from the retired hook's body so the canvas timeline's pointer
|
|
49
|
+
// machine (SP5 T5) reuses the exact trim arithmetic rather than
|
|
50
|
+
// reimplementing it, and so the arithmetic is testable without a component.
|
|
51
|
+
|
|
52
|
+
/** Nearest boundary within `threshold` seconds of `raw`, else `raw` itself.
|
|
53
|
+
* Ties keep the earlier entry — `<` is strict, as in the original loop. */
|
|
54
|
+
export function snapToBoundaries(raw: number, boundaries: readonly number[], threshold: number): number {
|
|
55
|
+
let t = raw
|
|
56
|
+
let bestDist = threshold
|
|
57
|
+
for (const b of boundaries) {
|
|
58
|
+
const d = Math.abs(raw - b)
|
|
59
|
+
if (d < bestDist) {
|
|
60
|
+
bestDist = d
|
|
61
|
+
t = b
|
|
113
62
|
}
|
|
63
|
+
}
|
|
64
|
+
return t
|
|
65
|
+
}
|
|
114
66
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
67
|
+
/** Snap a moving span of length `duration` by either of its edges, nearest
|
|
68
|
+
* first. The end edge is tested before the start edge and both share one
|
|
69
|
+
* running best distance, so an exact tie is resolved in favour of the START
|
|
70
|
+
* edge — preserved verbatim from the hook's original loop. Returns the raw
|
|
71
|
+
* span unchanged when nothing is in range; the caller re-clamps. */
|
|
72
|
+
export function snapMovedSpan(
|
|
73
|
+
rawStart: number,
|
|
74
|
+
duration: number,
|
|
75
|
+
boundaries: readonly number[],
|
|
76
|
+
threshold: number,
|
|
77
|
+
): { start: number; end: number } {
|
|
78
|
+
const rawEnd = rawStart + duration
|
|
79
|
+
let start = rawStart
|
|
80
|
+
let end = rawEnd
|
|
81
|
+
let bestDist = threshold
|
|
82
|
+
for (const b of boundaries) {
|
|
83
|
+
const dEnd = Math.abs(rawEnd - b)
|
|
84
|
+
if (dEnd < bestDist) {
|
|
85
|
+
bestDist = dEnd
|
|
86
|
+
start = b - duration
|
|
87
|
+
end = b
|
|
118
88
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
89
|
+
const dStart = Math.abs(rawStart - b)
|
|
90
|
+
if (dStart < bestDist) {
|
|
91
|
+
bestDist = dStart
|
|
92
|
+
start = b
|
|
93
|
+
end = b + duration
|
|
123
94
|
}
|
|
124
|
-
document.addEventListener('mousemove', onMove)
|
|
125
|
-
document.addEventListener('mouseup', onUp)
|
|
126
95
|
}
|
|
96
|
+
return { start, end }
|
|
97
|
+
}
|
|
127
98
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
},
|
|
135
|
-
): void {
|
|
136
|
-
e.stopPropagation()
|
|
137
|
-
|
|
138
|
-
const initX = e.clientX
|
|
139
|
-
const initY = e.clientY
|
|
140
|
-
const initStart = item.start
|
|
141
|
-
const initEnd = item.end
|
|
142
|
-
const duration = initEnd - initStart
|
|
143
|
-
let dragStarted = false
|
|
144
|
-
|
|
145
|
-
function onMove(moveE: MouseEvent) {
|
|
146
|
-
// Ignore sub-threshold pointer drift — it's a click, not a drag. Only once
|
|
147
|
-
// the press travels past DRAG_THRESHOLD_PX do we set the drag flag (which
|
|
148
|
-
// suppresses click-to-select) and start moving the item.
|
|
149
|
-
if (!dragStarted) {
|
|
150
|
-
const moved = Math.hypot(moveE.clientX - initX, moveE.clientY - initY)
|
|
151
|
-
if (moved < DRAG_THRESHOLD_PX) return
|
|
152
|
-
dragStarted = true
|
|
153
|
-
}
|
|
154
|
-
if (draggedFlagRef) draggedFlagRef.current = true
|
|
155
|
-
|
|
156
|
-
const rect = scrollRef.current?.getBoundingClientRect()
|
|
157
|
-
const contentWidth = rect ? rect.width * (zoomRef?.current ?? 1) : 0
|
|
158
|
-
const dx = contentWidth ? ((moveE.clientX - initX) / contentWidth) * totalDuration : 0
|
|
159
|
-
const dy = moveE.clientY - initY
|
|
160
|
-
|
|
161
|
-
const rawStart = Math.max(0, Math.min(totalDuration - duration, initStart + dx))
|
|
162
|
-
const rawEnd = rawStart + duration
|
|
99
|
+
/** A clip's speed, defended against a project carrying 0, a negative, or a
|
|
100
|
+
* non-finite value — every conversion below divides by it. */
|
|
101
|
+
function speedOf(item: Draggable): number {
|
|
102
|
+
const s = item.speed ?? 1
|
|
103
|
+
return Number.isFinite(s) && s > 0 ? s : 1
|
|
104
|
+
}
|
|
163
105
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
for (const b of snapBoundaries) {
|
|
170
|
-
const dEnd = Math.abs(rawEnd - b)
|
|
171
|
-
if (dEnd < bestDist) {
|
|
172
|
-
bestDist = dEnd
|
|
173
|
-
newStart = b - duration
|
|
174
|
-
newEnd = b
|
|
175
|
-
}
|
|
176
|
-
const dStart = Math.abs(rawStart - b)
|
|
177
|
-
if (dStart < bestDist) {
|
|
178
|
-
bestDist = dStart
|
|
179
|
-
newStart = b
|
|
180
|
-
newEnd = b + duration
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
newStart = Math.max(0, Math.min(totalDuration - duration, newStart))
|
|
184
|
-
newEnd = newStart + duration
|
|
106
|
+
function clamp(v: number, lo: number, hi: number): number {
|
|
107
|
+
// `hi` last, so a degenerate lo > hi keeps the min-duration constraint
|
|
108
|
+
// rather than inverting the item.
|
|
109
|
+
return Math.min(Math.max(v, lo), hi)
|
|
110
|
+
}
|
|
185
111
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Move one edge of a source-windowed item to time `t`.
|
|
114
|
+
*
|
|
115
|
+
* THE INVARIANT: the timeline span and the source window stay in lockstep —
|
|
116
|
+
* `outPoint − inPoint === (end − start) · speed` — for every input, always.
|
|
117
|
+
* That is what lets the frames, the waveform, the preview and the export all
|
|
118
|
+
* agree about what a clip is showing.
|
|
119
|
+
*
|
|
120
|
+
* The way it is kept is the whole point of this function. The timeline edge is
|
|
121
|
+
* clamped FIRST, against every limit there is (the item's minimum duration and
|
|
122
|
+
* the media actually available past the window), and the source window is then
|
|
123
|
+
* DERIVED from how far the edge really travelled. There is exactly one clamp,
|
|
124
|
+
* on one value.
|
|
125
|
+
*
|
|
126
|
+
* The previous version clamped the two independently — `newStart` against
|
|
127
|
+
* `end − 0.1`, `inPoint` against `[0, outPoint − 0.1]` — and whichever limit
|
|
128
|
+
* bit first left the other free to keep going. Drag the in edge of a clip that
|
|
129
|
+
* already starts at the head of its media and `start` slid left while
|
|
130
|
+
* `inPoint` stayed pinned at 0; drag the out edge of a clip that already runs
|
|
131
|
+
* to the end of its media and `end` grew while `outPoint` stayed at
|
|
132
|
+
* `sourceDuration`. Either way the clip ended up claiming more timeline than
|
|
133
|
+
* it had source for, silently. Downstream, the filmstrip walked off the end of
|
|
134
|
+
* the media and froze on the last frame it could find while the waveform,
|
|
135
|
+
* which windows to inPoint..outPoint, drew the real (shorter) window stretched
|
|
136
|
+
* across the whole clip — two halves of one clip disagreeing, with no error
|
|
137
|
+
* anywhere. Trimming past the media now simply stops at the media.
|
|
138
|
+
*/
|
|
139
|
+
export function resizeWindowedItem<T extends Draggable>(item: T, edge: 'start' | 'end', t: number): T {
|
|
140
|
+
const s = speedOf(item)
|
|
141
|
+
const inPoint = item.inPoint ?? 0
|
|
142
|
+
const outPoint = item.outPoint ?? inPoint + (item.end - item.start) * s
|
|
143
|
+
|
|
144
|
+
// BOTH window bounds come back written out, even the one this edge did not
|
|
145
|
+
// move. They were implicit before (an end-trim wrote `outPoint` and left
|
|
146
|
+
// `inPoint` undefined), and an implicit half of the window is what made this
|
|
147
|
+
// class of bug invisible: you cannot check `outPoint − inPoint === span · S`
|
|
148
|
+
// against a field that isn't there.
|
|
149
|
+
if (edge === 'start') {
|
|
150
|
+
// Leftward travel is limited by the source ahead of `inPoint`: that is
|
|
151
|
+
// `inPoint` seconds of media, which is `inPoint / s` of timeline.
|
|
152
|
+
const newStart = clamp(t, item.start - inPoint / s, item.end - MIN_DURATION)
|
|
153
|
+
return { ...item, start: newStart, inPoint: inPoint + (newStart - item.start) * s, outPoint }
|
|
154
|
+
}
|
|
189
155
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
draggedFlagRef!.current = false
|
|
198
|
-
}, 0)
|
|
199
|
-
}
|
|
200
|
-
}
|
|
156
|
+
// Rightward travel is limited by the source after `outPoint`. With no
|
|
157
|
+
// `sourceDuration` there is no known tail, so the edge stays unbounded —
|
|
158
|
+
// the same benefit of the doubt the old code gave it.
|
|
159
|
+
const tail = item.sourceDuration == null ? Infinity : item.sourceDuration - outPoint
|
|
160
|
+
const newEnd = clamp(t, item.start + MIN_DURATION, item.end + tail / s)
|
|
161
|
+
return { ...item, end: newEnd, inPoint, outPoint: outPoint + (newEnd - item.end) * s }
|
|
162
|
+
}
|
|
201
163
|
|
|
202
|
-
|
|
203
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Move one edge of an item to time `t`, keeping its source window consistent.
|
|
166
|
+
*
|
|
167
|
+
* `t` is expected to be already clamped to the timeline and already snapped —
|
|
168
|
+
* this function only enforces the item's own invariants. Non-video items
|
|
169
|
+
* (images, overlays) carry no source window and only move their timeline edge;
|
|
170
|
+
* video clips go through `resizeWindowedItem`, which holds the span/window
|
|
171
|
+
* invariant documented there.
|
|
172
|
+
*/
|
|
173
|
+
export function computeResizedItem(item: Draggable, edge: 'start' | 'end', t: number): Draggable {
|
|
174
|
+
if (item.type !== 'video') {
|
|
175
|
+
return edge === 'start'
|
|
176
|
+
? { ...item, start: Math.min(t, item.end - MIN_DURATION) }
|
|
177
|
+
: { ...item, end: Math.max(t, item.start + MIN_DURATION) }
|
|
204
178
|
}
|
|
205
|
-
|
|
206
|
-
return { beginDrag, beginResize }
|
|
179
|
+
return resizeWindowedItem(item, edge, t)
|
|
207
180
|
}
|
|
@@ -1,63 +1,28 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useRef } from 'react'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
/**
|
|
4
|
+
* The timeline column's horizontal scroll container.
|
|
5
|
+
*
|
|
6
|
+
* All this hook has left is the ref. It used to own a whole zoom model: a
|
|
7
|
+
* `zoom` multiplier, a pivot-preserving `zoomTo` that wrote `scrollLeft` in the
|
|
8
|
+
* same layout pass as the new width, and a non-passive wheel listener for
|
|
9
|
+
* ⌘/Ctrl+wheel zoom and Alt+wheel pan. That model existed because the DOM
|
|
10
|
+
* track rows laid items out as percentages of the whole project, so the only
|
|
11
|
+
* way to zoom them was to widen their container and scroll over it.
|
|
12
|
+
*
|
|
13
|
+
* The canvas surface zooms a px-per-second viewport held in an external store
|
|
14
|
+
* (`canvas/viewport.ts`) instead, and owns its own wheel handling — it even
|
|
15
|
+
* `stopPropagation`s the wheel events it consumes, specifically so this
|
|
16
|
+
* container could not zoom its multiplier off the same gesture. With the rows
|
|
17
|
+
* gone there is no second zoom model left to drive: the surface always fits
|
|
18
|
+
* the width it is given, so the container never overflows and its scroll
|
|
19
|
+
* position is always 0.
|
|
20
|
+
*
|
|
21
|
+
* It is kept as a hook (rather than inlined) because the ref is what makes the
|
|
22
|
+
* container addressable, and a wide child appearing here should scroll rather
|
|
23
|
+
* than stretch the page.
|
|
24
|
+
*/
|
|
25
|
+
export function useTimelineZoom() {
|
|
7
26
|
const scrollRef = useRef<HTMLDivElement>(null)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
useLayoutEffect(() => {
|
|
11
|
-
if (pendingScrollRef.current !== null && scrollRef.current) {
|
|
12
|
-
scrollRef.current.scrollLeft = pendingScrollRef.current
|
|
13
|
-
pendingScrollRef.current = null
|
|
14
|
-
}
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
function zoomTo(newZoom: number, pivotClientX?: number) {
|
|
18
|
-
const clamped = Math.max(1, Math.min(20, newZoom))
|
|
19
|
-
if (!scrollRef.current || totalDuration === 0) { setZoom(clamped); return }
|
|
20
|
-
const container = scrollRef.current
|
|
21
|
-
const containerWidth = container.clientWidth
|
|
22
|
-
const currentScrollLeft = container.scrollLeft
|
|
23
|
-
const rect = container.getBoundingClientRect()
|
|
24
|
-
const pivotOffset = pivotClientX !== undefined
|
|
25
|
-
? Math.max(0, Math.min(containerWidth, pivotClientX - rect.left))
|
|
26
|
-
: containerWidth / 2
|
|
27
|
-
const pivotPct = Math.max(0, Math.min(1,
|
|
28
|
-
(currentScrollLeft + pivotOffset) / (containerWidth * zoomRef.current)
|
|
29
|
-
))
|
|
30
|
-
pendingScrollRef.current = Math.max(0, pivotPct * containerWidth * clamped - pivotOffset)
|
|
31
|
-
setZoom(clamped)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// ⌘/Ctrl + wheel = zoom; Alt + wheel = horizontal scroll. Both call
|
|
35
|
-
// preventDefault, so the listener MUST be non-passive. React's onWheel prop is
|
|
36
|
-
// registered passive at the root, which makes preventDefault a no-op and spams
|
|
37
|
-
// "Unable to preventDefault inside passive event listener" — so attach a native
|
|
38
|
-
// listener with { passive: false } instead. A ref to the latest handler keeps
|
|
39
|
-
// it current without re-binding on every render.
|
|
40
|
-
const wheelHandlerRef = useRef<(e: WheelEvent) => void>(() => {})
|
|
41
|
-
wheelHandlerRef.current = (e: WheelEvent) => {
|
|
42
|
-
if (e.ctrlKey || e.metaKey) {
|
|
43
|
-
e.preventDefault()
|
|
44
|
-
// Multiplicative step so perceived speed is consistent across zoom levels.
|
|
45
|
-
// deltaY ≈ 100 per mouse-wheel tick → factor ≈ 0.82 (≈ 18% zoom change).
|
|
46
|
-
const factor = Math.exp(-e.deltaY * 0.002)
|
|
47
|
-
zoomTo(zoomRef.current * factor, e.clientX)
|
|
48
|
-
} else if (e.altKey) {
|
|
49
|
-
e.preventDefault()
|
|
50
|
-
if (scrollRef.current) scrollRef.current.scrollLeft += e.deltaY
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
useEffect(() => {
|
|
55
|
-
const el = scrollRef.current
|
|
56
|
-
if (!el) return
|
|
57
|
-
const onWheel = (e: WheelEvent) => wheelHandlerRef.current(e)
|
|
58
|
-
el.addEventListener('wheel', onWheel, { passive: false })
|
|
59
|
-
return () => el.removeEventListener('wheel', onWheel)
|
|
60
|
-
}, [])
|
|
61
|
-
|
|
62
|
-
return { zoom, zoomRef, scrollRef, pendingScrollRef, zoomTo }
|
|
27
|
+
return { scrollRef }
|
|
63
28
|
}
|
|
@@ -3,15 +3,3 @@ export function formatTime(s: number): string {
|
|
|
3
3
|
const sec = (s % 60).toFixed(1)
|
|
4
4
|
return `${m}:${sec.padStart(4, '0')}`
|
|
5
5
|
}
|
|
6
|
-
|
|
7
|
-
export function pct(t: number, totalDuration: number): number {
|
|
8
|
-
return totalDuration > 0 ? (t / totalDuration) * 100 : 0
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function ratioFromClientX(clientX: number, scrubberRect: DOMRect): number {
|
|
12
|
-
const x = clientX - scrubberRect.left
|
|
13
|
-
return Math.max(0, Math.min(1, x / scrubberRect.width))
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const trackRow = 'relative h-10 bg-gray-100 dark:bg-gray-900 rounded overflow-hidden cursor-pointer'
|
|
17
|
-
export const trackRowTall = 'relative h-14 bg-gray-100 dark:bg-gray-900 rounded overflow-hidden cursor-pointer'
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { EditorAdapter, EditorContext } from '../types'
|
|
4
|
+
import type { PlaybackClock } from './playback-clock'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* How often the playhead is reported during playback.
|
|
8
|
+
*
|
|
9
|
+
* The clock is written ~60Hz; reporting every tick would be 60 POSTs/sec for
|
|
10
|
+
* information no agent can consume that fast. One per second is well inside
|
|
11
|
+
* serve's CONTEXT_TTL_SEC while costing effectively nothing.
|
|
12
|
+
*
|
|
13
|
+
* Note this is an unconditional heartbeat, not change detection: a paused
|
|
14
|
+
* editor keeps reporting the same playhead once a second, and must. The TTL
|
|
15
|
+
* governs freshness, so going quiet while parked would make serve answer
|
|
16
|
+
* "no editor open" about an editor that is plainly open.
|
|
17
|
+
*/
|
|
18
|
+
export const REPORT_INTERVAL_MS = 1000
|
|
19
|
+
|
|
20
|
+
interface Options {
|
|
21
|
+
adapter: EditorAdapter
|
|
22
|
+
projectId: string
|
|
23
|
+
clock: PlaybackClock
|
|
24
|
+
selectedIds: string[]
|
|
25
|
+
selectedCaptionId: string | null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Report playhead + selection to the host on a throttle.
|
|
30
|
+
*
|
|
31
|
+
* The throttle is deliberately asymmetric. The PLAYHEAD is sampled on an
|
|
32
|
+
* interval — it moves continuously and an agent only needs roughly where it
|
|
33
|
+
* is. SELECTION is reported the moment it changes — it is discrete, it is
|
|
34
|
+
* usually the more meaningful half of "this section", and waiting up to a
|
|
35
|
+
* second to report a click would make the agent answer about the previous
|
|
36
|
+
* selection.
|
|
37
|
+
*/
|
|
38
|
+
export function useReportContext({
|
|
39
|
+
adapter, projectId, clock, selectedIds, selectedCaptionId,
|
|
40
|
+
}: Options): void {
|
|
41
|
+
// Kept in a ref so the interval effect never re-subscribes on a selection
|
|
42
|
+
// change — it always reads the latest values without depending on them.
|
|
43
|
+
const latest = useRef({ selectedIds, selectedCaptionId })
|
|
44
|
+
latest.current = { selectedIds, selectedCaptionId }
|
|
45
|
+
|
|
46
|
+
const send = useRef<(ctx: EditorContext) => void>(() => {})
|
|
47
|
+
send.current = (ctx: EditorContext) => {
|
|
48
|
+
// Fire and forget. A down serve, a 404, an offline host — none of it is
|
|
49
|
+
// the editor's problem, and none of it reaches the user.
|
|
50
|
+
void adapter.reportContext?.(projectId, ctx)?.catch(() => {})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Playhead, on an interval. Reports on mount so an agent asked something the
|
|
54
|
+
// instant the editor opens still gets an answer.
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!adapter.reportContext) return
|
|
57
|
+
const emit = () => send.current({
|
|
58
|
+
playheadSec: clock.get(),
|
|
59
|
+
selectedIds: latest.current.selectedIds,
|
|
60
|
+
selectedCaptionId: latest.current.selectedCaptionId,
|
|
61
|
+
})
|
|
62
|
+
emit()
|
|
63
|
+
const id = setInterval(emit, REPORT_INTERVAL_MS)
|
|
64
|
+
return () => clearInterval(id)
|
|
65
|
+
}, [adapter, projectId, clock])
|
|
66
|
+
|
|
67
|
+
// Selection, immediately on change. Skips the mount emit above by comparing
|
|
68
|
+
// against what the interval effect already sent.
|
|
69
|
+
const mounted = useRef(false)
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (!adapter.reportContext) return
|
|
72
|
+
if (!mounted.current) { mounted.current = true; return }
|
|
73
|
+
send.current({ playheadSec: clock.get(), selectedIds, selectedCaptionId })
|
|
74
|
+
}, [adapter, clock, selectedIds, selectedCaptionId])
|
|
75
|
+
}
|