cursedbelt 5.112.1 → 5.113.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/react/media/CutTimeline.d.ts +31 -0
- package/dist/react/media/CutTimeline.d.ts.map +1 -0
- package/dist/react/media/CutTimeline.js +197 -0
- package/dist/react/media/CutTimeline.js.map +1 -0
- package/dist/react/media/VideoCutEditor.d.ts +32 -6
- package/dist/react/media/VideoCutEditor.d.ts.map +1 -1
- package/dist/react/media/VideoCutEditor.js +97 -63
- package/dist/react/media/VideoCutEditor.js.map +1 -1
- package/dist/react/media/cutTimelineMath.d.ts +80 -0
- package/dist/react/media/cutTimelineMath.d.ts.map +1 -0
- package/dist/react/media/cutTimelineMath.js +116 -0
- package/dist/react/media/cutTimelineMath.js.map +1 -0
- package/dist/react/media/useFrameRate.d.ts +34 -0
- package/dist/react/media/useFrameRate.d.ts.map +1 -0
- package/dist/react/media/useFrameRate.js +66 -0
- package/dist/react/media/useFrameRate.js.map +1 -0
- package/dist/react/media/videoCutEditor.css +249 -63
- package/dist/react/media-gallery/MediaGallery.d.ts +10 -1
- package/dist/react/media-gallery/MediaGallery.d.ts.map +1 -1
- package/dist/react/media-gallery/MediaGallery.js +29 -7
- package/dist/react/media-gallery/MediaGallery.js.map +1 -1
- package/dist/react/media-gallery/galleryDeepLink.d.ts +14 -0
- package/dist/react/media-gallery/galleryDeepLink.d.ts.map +1 -1
- package/dist/react/media-gallery/galleryDeepLink.js +44 -5
- package/dist/react/media-gallery/galleryDeepLink.js.map +1 -1
- package/dist/react/media-gallery/index.d.ts +1 -1
- package/dist/react/media-gallery/index.d.ts.map +1 -1
- package/dist/react/media-gallery/index.js +1 -1
- package/dist/react/media-gallery/index.js.map +1 -1
- package/dist/react/media-gallery/types.d.ts +5 -0
- package/dist/react/media-gallery/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/scripts/guardrails-baseline.json +1 -1
- package/src/react/media/CutTimeline.tsx +379 -0
- package/src/react/media/VideoCutEditor.spec.tsx +1 -1
- package/src/react/media/VideoCutEditor.tsx +277 -214
- package/src/react/media/cutTimelineMath.spec.ts +90 -0
- package/src/react/media/cutTimelineMath.ts +140 -0
- package/src/react/media/useFrameRate.ts +96 -0
- package/src/react/media/videoCutEditor.css +249 -63
- package/src/react/media-gallery/MediaGallery.tsx +38 -7
- package/src/react/media-gallery/galleryDeepLink.spec.tsx +87 -2
- package/src/react/media-gallery/galleryDeepLink.ts +58 -5
- package/src/react/media-gallery/index.ts +1 -0
- package/src/react/media-gallery/types.ts +5 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type PlanSegment, type VideoCut } from 'cursedbelt-core/media/video-cuts';
|
|
3
|
+
/**
|
|
4
|
+
* The Trim page's timeline: a ruler, the clip's pieces, and a playhead, at any zoom from the
|
|
5
|
+
* whole clip down to one frame per 12 px (forge task 4311).
|
|
6
|
+
*
|
|
7
|
+
* The gestures are a map's and an NLE's, so nothing here has to be learned:
|
|
8
|
+
* · ⌘/Ctrl + wheel, or a trackpad pinch (which Chromium and Safari deliver as a ctrl-wheel),
|
|
9
|
+
* zooms around the POINTER; two fingers on a touch screen pinch the same way.
|
|
10
|
+
* · a plain wheel, or a drag anywhere on the track, pans.
|
|
11
|
+
* · a click on the ruler moves the playhead there, onto the frame under the pointer.
|
|
12
|
+
* · a click on a piece is the ✗ it always was — a drag that started on one never toggles it.
|
|
13
|
+
* The slider, ± and Fit do the same with no gesture at all (CNV-1: the wheel alone never zooms;
|
|
14
|
+
* CNV-3: a fit control on every canvas).
|
|
15
|
+
*
|
|
16
|
+
* 🔴 Drawn VIRTUALLY: only what is on screen is in the DOM, each piece clipped to the view. At
|
|
17
|
+
* 12 px a frame a two-hour 60 fps film is ten million pixels wide, which no browser will lay out
|
|
18
|
+
* as one element — so the track is always exactly the view, and the view is two numbers.
|
|
19
|
+
*/
|
|
20
|
+
export interface CutTimelineProps {
|
|
21
|
+
duration: number;
|
|
22
|
+
fps: number;
|
|
23
|
+
playhead: number;
|
|
24
|
+
segments: readonly PlanSegment[];
|
|
25
|
+
onSeek: (seconds: number) => void;
|
|
26
|
+
onToggle: (segment: VideoCut) => void;
|
|
27
|
+
/** The transport, drawn at the start of the timeline's one-line toolbar (FRM-14). */
|
|
28
|
+
transport?: React.ReactNode;
|
|
29
|
+
}
|
|
30
|
+
export declare function CutTimeline({ duration, fps, playhead, segments, onSeek, onToggle, transport, ...passThrough }: CutTimelineProps): React.JSX.Element;
|
|
31
|
+
//# sourceMappingURL=CutTimeline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CutTimeline.d.ts","sourceRoot":"","sources":["../../../src/react/media/CutTimeline.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAe,KAAK,WAAW,EAAE,KAAK,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAmBhG;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,SAAS,WAAW,EAAE,CAAC;IACjC,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,QAAQ,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,IAAI,CAAC;IACtC,qFAAqF;IACrF,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC7B;AAeD,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,GAAG,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,WAAW,EAAE,EAAE,gBAAgB,qBA4T/H"}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Maximize2, X, ZoomIn, ZoomOut } from 'lucide-react';
|
|
4
|
+
import { cutTimecode } from 'cursedbelt-core/media/video-cuts';
|
|
5
|
+
import { IconButton } from '../components/IconButton.js';
|
|
6
|
+
import { Button } from '../components/Button.js';
|
|
7
|
+
import { Slider } from '../components/Slider.js';
|
|
8
|
+
import { Tooltip } from '../components/Tooltip.js';
|
|
9
|
+
import { cn, RAW_BUTTON } from '../lib/cn.js';
|
|
10
|
+
import { clampViewStart, frameAt, frameStart, rulerLabel, rulerStep, sliderToZoom, snapToFrame, zoomAround, zoomBounds, zoomToSlider, } from './cutTimelineMath.js';
|
|
11
|
+
/** A drag shorter than this is a click — the slop every pointer library uses. */
|
|
12
|
+
const DRAG_SLOP_PX = 4;
|
|
13
|
+
/** The width drawn at before the track is measured. */
|
|
14
|
+
const NOMINAL_WIDTH_PX = 1000;
|
|
15
|
+
/** Frame ticks are drawn once frames are at least this far apart. */
|
|
16
|
+
const FRAME_TICK_MIN_PX = 6;
|
|
17
|
+
export function CutTimeline({ duration, fps, playhead, segments, onSeek, onToggle, transport, ...passThrough }) {
|
|
18
|
+
const trackRef = React.useRef(null);
|
|
19
|
+
const [measured, setWidth] = React.useState(0);
|
|
20
|
+
// Until the track has been measured (and in a DOM with no layout at all), draw at a nominal
|
|
21
|
+
// width rather than at zero — zero would put every piece off screen, and the first real
|
|
22
|
+
// measurement lands in a layout effect, before anything paints.
|
|
23
|
+
const width = measured > 0 ? measured : NOMINAL_WIDTH_PX;
|
|
24
|
+
const [view, setView] = React.useState({ start: 0, pps: null });
|
|
25
|
+
React.useLayoutEffect(() => {
|
|
26
|
+
const track = trackRef.current;
|
|
27
|
+
if (!track)
|
|
28
|
+
return;
|
|
29
|
+
const measure = () => setWidth(track.clientWidth);
|
|
30
|
+
measure();
|
|
31
|
+
const observer = new ResizeObserver(measure);
|
|
32
|
+
observer.observe(track);
|
|
33
|
+
return () => observer.disconnect();
|
|
34
|
+
}, []);
|
|
35
|
+
const bounds = zoomBounds(width, duration, fps);
|
|
36
|
+
const pps = Math.min(bounds.max, Math.max(bounds.fit, view.pps ?? bounds.fit));
|
|
37
|
+
const start = clampViewStart(view.start, pps, width, duration);
|
|
38
|
+
const span = pps > 0 ? width / pps : 0;
|
|
39
|
+
const canZoom = bounds.max > bounds.fit && bounds.fit > 0;
|
|
40
|
+
// The handlers below are bound once (the wheel has to be non-passive, so it is a native
|
|
41
|
+
// listener) and read the current numbers from here.
|
|
42
|
+
const now = React.useRef({ pps, start, width, duration, bounds });
|
|
43
|
+
now.current = { pps, start, width, duration, bounds };
|
|
44
|
+
const zoomTo = React.useCallback((next, anchorX) => {
|
|
45
|
+
const n = now.current;
|
|
46
|
+
setView(zoomAround({ start: n.start, pps: n.pps }, next, anchorX, n.width, n.duration, n.bounds));
|
|
47
|
+
}, []);
|
|
48
|
+
/** Zoom by a factor around the playhead when it is on screen, else around the middle. */
|
|
49
|
+
const zoomBy = (factor) => {
|
|
50
|
+
const x = playhead >= start && playhead <= start + span ? (playhead - start) * pps : width / 2;
|
|
51
|
+
zoomTo(pps * factor, x);
|
|
52
|
+
};
|
|
53
|
+
React.useEffect(() => {
|
|
54
|
+
const track = trackRef.current;
|
|
55
|
+
if (!track)
|
|
56
|
+
return;
|
|
57
|
+
const onWheel = (event) => {
|
|
58
|
+
event.preventDefault();
|
|
59
|
+
const n = now.current;
|
|
60
|
+
const lines = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? n.width : 1;
|
|
61
|
+
if (event.ctrlKey || event.metaKey) {
|
|
62
|
+
const x = event.clientX - track.getBoundingClientRect().left;
|
|
63
|
+
zoomTo(n.pps * Math.exp(-event.deltaY * lines * 0.01), x);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// A vertical wheel pans too: most mice have no other, and the page has nothing to scroll here.
|
|
67
|
+
const dx = Math.abs(event.deltaX) > Math.abs(event.deltaY) ? event.deltaX : event.deltaY;
|
|
68
|
+
setView({ pps: n.pps, start: clampViewStart(n.start + (dx * lines) / n.pps, n.pps, n.width, n.duration) });
|
|
69
|
+
};
|
|
70
|
+
track.addEventListener('wheel', onWheel, { passive: false });
|
|
71
|
+
return () => track.removeEventListener('wheel', onWheel);
|
|
72
|
+
}, [zoomTo]);
|
|
73
|
+
// Keep the playhead in view while it moves (playing, stepping, a seek from the cuts list) —
|
|
74
|
+
// a page-turn, as Resolve does, rather than a scroll that never lets the eye settle.
|
|
75
|
+
React.useEffect(() => {
|
|
76
|
+
const n = now.current;
|
|
77
|
+
if (n.width <= 0 || n.pps <= n.bounds.fit + 1e-9)
|
|
78
|
+
return;
|
|
79
|
+
const viewSpan = n.width / n.pps;
|
|
80
|
+
if (playhead < n.start || playhead > n.start + viewSpan) {
|
|
81
|
+
setView({ pps: n.pps, start: clampViewStart(playhead - viewSpan * 0.1, n.pps, n.width, n.duration) });
|
|
82
|
+
}
|
|
83
|
+
}, [playhead]);
|
|
84
|
+
/*
|
|
85
|
+
* Pointers. One pointer that moves past the slop pans; two pinch. One that does not move is a
|
|
86
|
+
* click: on the ruler it seeks, on a piece the piece's own button does its job.
|
|
87
|
+
*/
|
|
88
|
+
const gesture = React.useRef({ pointers: new Map(), fromX: 0, fromStart: 0, dragged: false, pinch: null });
|
|
89
|
+
const onPointerDown = (event) => {
|
|
90
|
+
if (event.pointerType === 'mouse' && event.button !== 0)
|
|
91
|
+
return;
|
|
92
|
+
const g = gesture.current;
|
|
93
|
+
g.pointers.set(event.pointerId, event.clientX);
|
|
94
|
+
if (g.pointers.size === 1) {
|
|
95
|
+
g.fromX = event.clientX;
|
|
96
|
+
g.fromStart = start;
|
|
97
|
+
g.dragged = false;
|
|
98
|
+
g.pinch = null;
|
|
99
|
+
}
|
|
100
|
+
else if (g.pointers.size === 2) {
|
|
101
|
+
const [a, b] = [...g.pointers.values()];
|
|
102
|
+
g.pinch = { distance: Math.max(1, Math.abs(a - b)), pps };
|
|
103
|
+
g.dragged = true;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const onPointerMove = (event) => {
|
|
107
|
+
const g = gesture.current;
|
|
108
|
+
if (!g.pointers.has(event.pointerId))
|
|
109
|
+
return;
|
|
110
|
+
g.pointers.set(event.pointerId, event.clientX);
|
|
111
|
+
const left = event.currentTarget.getBoundingClientRect().left;
|
|
112
|
+
if (g.pinch && g.pointers.size >= 2) {
|
|
113
|
+
const [a, b] = [...g.pointers.values()];
|
|
114
|
+
zoomTo(g.pinch.pps * (Math.max(1, Math.abs(a - b)) / g.pinch.distance), (a + b) / 2 - left);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const dx = event.clientX - g.fromX;
|
|
118
|
+
if (!g.dragged && Math.abs(dx) < DRAG_SLOP_PX)
|
|
119
|
+
return;
|
|
120
|
+
if (!g.dragged) {
|
|
121
|
+
g.dragged = true;
|
|
122
|
+
// Captured only once it is a drag: capturing on press would retarget the click of a
|
|
123
|
+
// piece's button to the track, and the ✗ would stop working.
|
|
124
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
125
|
+
}
|
|
126
|
+
const n = now.current;
|
|
127
|
+
setView({ pps: n.pps, start: clampViewStart(g.fromStart - dx / n.pps, n.pps, n.width, n.duration) });
|
|
128
|
+
};
|
|
129
|
+
const onPointerUp = (event) => {
|
|
130
|
+
const g = gesture.current;
|
|
131
|
+
if (!g.pointers.delete(event.pointerId))
|
|
132
|
+
return;
|
|
133
|
+
if (g.pointers.size > 0)
|
|
134
|
+
return;
|
|
135
|
+
g.pinch = null;
|
|
136
|
+
if (g.dragged || event.target.closest('.cbvc-seg'))
|
|
137
|
+
return;
|
|
138
|
+
const n = now.current;
|
|
139
|
+
const x = event.clientX - event.currentTarget.getBoundingClientRect().left;
|
|
140
|
+
onSeek(Math.min(n.duration, snapToFrame(n.start + x / n.pps, fps)));
|
|
141
|
+
};
|
|
142
|
+
// A drag that ended over a piece must not also toggle it.
|
|
143
|
+
const onClickCapture = (event) => {
|
|
144
|
+
if (gesture.current.dragged) {
|
|
145
|
+
event.preventDefault();
|
|
146
|
+
event.stopPropagation();
|
|
147
|
+
gesture.current.dragged = false;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const x = (seconds) => (seconds - start) * pps;
|
|
151
|
+
const end = start + span;
|
|
152
|
+
// The ruler: a labeled tick per step, and a tick per FRAME once frames are far enough apart.
|
|
153
|
+
const step = rulerStep(pps, fps);
|
|
154
|
+
const labels = [];
|
|
155
|
+
if (pps > 0 && duration > 0) {
|
|
156
|
+
// Counted in frames when the step is, so 0.1 s × 3 lands on frame 3 and not on 0.30000000000000004.
|
|
157
|
+
const stepFrames = Math.round(step.label * fps);
|
|
158
|
+
for (let k = Math.floor(start / step.label); k * step.label <= end + 1e-9; k += 1) {
|
|
159
|
+
const t = step.frames ? frameStart(k * stepFrames, fps) : k * step.label;
|
|
160
|
+
if (t >= start - 1e-9 && t <= duration + 1e-9)
|
|
161
|
+
labels.push(t);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const frameTicks = [];
|
|
165
|
+
if (pps / fps >= FRAME_TICK_MIN_PX) {
|
|
166
|
+
for (let k = frameAt(start, fps); frameStart(k, fps) <= Math.min(end, duration) + 1e-9; k += 1)
|
|
167
|
+
frameTicks.push(k);
|
|
168
|
+
}
|
|
169
|
+
return (_jsxs("div", { ...passThrough, className: 'cbvc-timeline', children: [_jsxs("div", { className: 'cbvc-tools', children: [transport, _jsxs("div", { className: 'cbvc-zoom', children: [_jsx(IconButton, { size: 'sm', label: 'Zoom out', tooltip: 'Zoom out (\u2318 or Ctrl + wheel, or pinch)', disabled: !canZoom || pps <= bounds.fit + 1e-9, onClick: () => zoomBy(0.5), children: _jsx(ZoomOut, { "aria-hidden": 'true' }) }), _jsx(Slider, { className: 'cbvc-zoom-slider', "aria-label": 'Zoom', "aria-valuetext": `${(pps / fps).toFixed(pps / fps < 1 ? 2 : 1)} px per frame`, min: 0, max: 1000, step: 1, disabled: !canZoom, value: Math.round(zoomToSlider(pps, bounds) * 1000), onChange: (value) => {
|
|
170
|
+
const next = sliderToZoom(value / 1000, bounds);
|
|
171
|
+
const x0 = playhead >= start && playhead <= end ? (playhead - start) * pps : width / 2;
|
|
172
|
+
zoomTo(next, x0);
|
|
173
|
+
} }), _jsx(IconButton, { size: 'sm', label: 'Zoom in', tooltip: 'Zoom in \u2014 down to single frames', disabled: !canZoom || pps >= bounds.max - 1e-9, onClick: () => zoomBy(2), children: _jsx(ZoomIn, { "aria-hidden": 'true' }) }), _jsxs(Button, { size: 'sm', variant: 'ghost', tooltip: 'Fit the whole clip in the timeline', disabled: view.pps === null || pps <= bounds.fit + 1e-9, onClick: () => setView({ start: 0, pps: null }), children: [_jsx(Maximize2, { "aria-hidden": 'true', size: 14 }), " Fit"] })] })] }), _jsxs("div", { ref: trackRef, className: 'cbvc-track', "data-px-per-second": pps, "data-view-start": start, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onPointerCancel: onPointerUp, onClickCapture: onClickCapture, children: [_jsxs("div", { className: 'cbvc-ruler', "aria-hidden": 'true', children: [frameTicks.map((k) => (_jsx("span", { className: 'cbvc-frame-tick', "data-frame": k, style: { left: `${x(frameStart(k, fps))}px` } }, `f${k}`))), labels.map((t) => (_jsx("span", { className: 'cbvc-tick', style: { left: `${x(t)}px` }, children: _jsx("span", { className: 'cbvc-tick-label', children: rulerLabel(t, fps, step.frames, duration) }) }, `l${t}`)))] }), _jsx("ul", { className: 'cbvc-strip', "aria-label": segments.length <= 1
|
|
174
|
+
? 'The whole clip, uncut'
|
|
175
|
+
: `${segments.length} sections; ${segments.filter((segment) => segment.removed).length} marked for removal`, children: segments
|
|
176
|
+
.filter((segment) => segment.end > start && segment.start < end)
|
|
177
|
+
.map((segment) => {
|
|
178
|
+
// Clipped to the view (a pixel past each edge, so no border shows at the cut-off).
|
|
179
|
+
const left = Math.max(-1, x(segment.start));
|
|
180
|
+
const right = Math.min(width + 1, x(segment.end));
|
|
181
|
+
const words = segment.removed
|
|
182
|
+
? `Keep ${cutTimecode(segment.start)} to ${cutTimecode(segment.end)} after all`
|
|
183
|
+
: `Remove ${cutTimecode(segment.start)} to ${cutTimecode(segment.end)}`;
|
|
184
|
+
return (_jsx("li", { className: 'cbvc-seg-item', style: { left: `${left}px`, width: `${Math.max(0, right - left)}px` }, children: _jsx(Tooltip, { content: words, children: _jsxs("button", {
|
|
185
|
+
// guardrails-ignore no-raw-action-button: a segment of the timeline, sized to
|
|
186
|
+
// its own duration — a `Button` carries padding and a min-width, which is
|
|
187
|
+
// exactly wrong for a control whose WIDTH is the information.
|
|
188
|
+
type: 'button', className: cn(RAW_BUTTON, `cbvc-seg${segment.removed ? ' is-removed' : ''}`), "aria-pressed": segment.removed, "aria-label": words, "data-start": segment.start, "data-end": segment.end, onClick: () => onToggle(segment), children: [_jsx(X, { className: 'cbvc-seg-x', "aria-hidden": 'true', size: 12 }), _jsx("span", { className: 'cbvc-seg-label', children: segment.removed ? 'Removed' : cutTimecode(segment.end - segment.start) })] }) }) }, segment.key));
|
|
189
|
+
}) }), playhead >= start - 1e-9 && playhead <= end + 1e-9 ? (_jsx("span", { className: 'cbvc-strip-head', style: { left: `${x(playhead)}px` } })) : null] }), pps > bounds.fit + 1e-9 ? (_jsxs("div", { className: 'cbvc-overview', "aria-hidden": 'true', onClick: (event) => {
|
|
190
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
191
|
+
const t = ((event.clientX - rect.left) / rect.width) * duration;
|
|
192
|
+
setView({ pps, start: clampViewStart(t - span / 2, pps, width, duration) });
|
|
193
|
+
}, children: [segments
|
|
194
|
+
.filter((segment) => segment.removed)
|
|
195
|
+
.map((segment) => (_jsx("span", { className: 'cbvc-overview-cut', style: { left: `${(segment.start / duration) * 100}%`, width: `${((segment.end - segment.start) / duration) * 100}%` } }, segment.key))), _jsx("span", { className: 'cbvc-overview-window', style: { left: `${(start / duration) * 100}%`, width: `${Math.max(0.5, (span / duration) * 100)}%` } }), _jsx("span", { className: 'cbvc-overview-head', style: { left: `${(playhead / duration) * 100}%` } })] })) : null] }));
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=CutTimeline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CutTimeline.js","sourceRoot":"","sources":["../../../src/react/media/CutTimeline.tsx"],"names":[],"mappings":"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"}
|
|
@@ -12,7 +12,7 @@ import { type VideoCut } from 'cursedbelt-core/media/video-cuts';
|
|
|
12
12
|
* it uses ... It can then remove the parts of the video I don't want, save a new copy and I
|
|
13
13
|
* can view it when it's done."*
|
|
14
14
|
*
|
|
15
|
-
* So this
|
|
15
|
+
* So this page never touches a byte. It produces a `VideoCut[]`, hands it to `onSubmit`,
|
|
16
16
|
* and closes. Everything about what makes a list valid is `videoCuts.ts`, shared with the
|
|
17
17
|
* server that executes it, so the dialog and the encoder cannot disagree about what was
|
|
18
18
|
* asked for.
|
|
@@ -51,7 +51,28 @@ import { type VideoCut } from 'cursedbelt-core/media/video-cuts';
|
|
|
51
51
|
* ONE `InfoHint` — the fleet's standard ⓘ, which costs a 16px glyph at rest and opens on
|
|
52
52
|
* hover, focus or tap. That is the answer to *"tooltips or an info icon or something we
|
|
53
53
|
* standardize can make it clear without taking up space when out of use"*: the explanation
|
|
54
|
-
* exists, in one predictable place, and pays no permanent rent
|
|
54
|
+
* exists, in one predictable place, and pays no permanent rent on the page.
|
|
55
|
+
*
|
|
56
|
+
* ── A PAGE, with a zoomable timeline, down to the frame (owner, 2026-11-03) ───────────
|
|
57
|
+
* *"a full Trim page with a zoomable timeline, down to frame-by-frame selection."* It was a
|
|
58
|
+
* `size='lg'` modal with one fixed strip, so on a two-hour film a pixel was several seconds
|
|
59
|
+
* and the frame he wanted could not be marked at all. Now it is the shape of an NLE's trim
|
|
60
|
+
* view (YouTube Studio's editor, Resolve's cut page): the picture on top, the transport and a
|
|
61
|
+
* full-width timeline under it, the actions bottom-right.
|
|
62
|
+
* · It COVERS the viewport and has an address of its own when the app gives it one
|
|
63
|
+
* (`MediaGallery`'s `trimming`, `useGalleryDeepLink({ trim: true })`), so Back closes it
|
|
64
|
+
* and a reload keeps it. Underneath it is still the belt's dialog primitive — that is what
|
|
65
|
+
* traps focus, takes Escape and makes the gallery's own keys stand down (`openDialogs`).
|
|
66
|
+
* · The timeline zooms from the whole clip to one frame per 12 px (`CutTimeline`).
|
|
67
|
+
* · `,` and `.` step one frame, every split lands on a frame boundary, and the readout is
|
|
68
|
+
* `hh:mm:ss:ff`. The rate is the catalog's probe, else measured from the clip
|
|
69
|
+
* (`useFrameRate`), and an assumed rate is labeled as one.
|
|
70
|
+
*
|
|
71
|
+
* 🔴 Frame-accurate is a promise only because binary-server KEEPS it: its trim decodes and
|
|
72
|
+
* re-encodes every kept span (`trim=start:end` + `concat` into libx264 — `cutArgs`,
|
|
73
|
+
* `apps/binary-server/src/media/processing/ffmpegJobs.ts`), so a cut lands on the frame it
|
|
74
|
+
* names, not on the keyframe before it. The note under the timeline says so; a stream-copy
|
|
75
|
+
* trim would make that sentence false, and it must change with it.
|
|
55
76
|
*/
|
|
56
77
|
export interface VideoCutEditorProps {
|
|
57
78
|
/**
|
|
@@ -78,21 +99,26 @@ export interface VideoCutEditorProps {
|
|
|
78
99
|
/**
|
|
79
100
|
* The clip's length, when the app already knows it. The element's own metadata still wins
|
|
80
101
|
* once it loads: a duration the catalog rounded to the second would put every mark a
|
|
81
|
-
* fraction out, and the marks are the entire output of this
|
|
102
|
+
* fraction out, and the marks are the entire output of this page.
|
|
82
103
|
*/
|
|
83
104
|
durationSeconds?: number | null;
|
|
105
|
+
/**
|
|
106
|
+
* Frames per second from the catalog's probe, when it has one. Without it the page measures
|
|
107
|
+
* the clip itself (`useFrameRate`) and, until it can, says the rate it draws with is assumed.
|
|
108
|
+
*/
|
|
109
|
+
frameRate?: number | null;
|
|
84
110
|
open?: boolean;
|
|
85
111
|
onClose: () => void;
|
|
86
112
|
/**
|
|
87
|
-
* Send the marks. Resolving CLOSES the
|
|
113
|
+
* Send the marks. Resolving CLOSES the page; rejecting leaves it open with the marks
|
|
88
114
|
* intact, because the alternative is losing several minutes of careful scrubbing to one
|
|
89
115
|
* failed request.
|
|
90
116
|
*/
|
|
91
117
|
onSubmit: (cuts: VideoCut[]) => void | Promise<void>;
|
|
92
118
|
/** Wording for the confirm button — apps queue this differently. */
|
|
93
119
|
submitLabel?: string;
|
|
94
|
-
/** Shown under the
|
|
120
|
+
/** Shown under the timeline: what happens after Send, in the app's own words. */
|
|
95
121
|
note?: React.ReactNode;
|
|
96
122
|
}
|
|
97
|
-
export declare function VideoCutEditor({ src, hlsUrl, posterUrl, resolving, name, durationSeconds, open, onClose, onSubmit, submitLabel, note, ...passThrough }: VideoCutEditorProps): React.JSX.Element;
|
|
123
|
+
export declare function VideoCutEditor({ src, hlsUrl, posterUrl, resolving, name, durationSeconds, frameRate, open, onClose, onSubmit, submitLabel, note, ...passThrough }: VideoCutEditorProps): React.JSX.Element;
|
|
98
124
|
//# sourceMappingURL=VideoCutEditor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoCutEditor.d.ts","sourceRoot":"","sources":["../../../src/react/media/VideoCutEditor.tsx"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAC9B,OAAO,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"VideoCutEditor.d.ts","sourceRoot":"","sources":["../../../src/react/media/VideoCutEditor.tsx"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAC9B,OAAO,KAAK,MAAM,OAAO,CAAC;AAU1B,OAAO,EAYL,KAAK,QAAQ,EACd,MAAM,kCAAkC,CAAC;AAO1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwEG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;;;;OAQG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,4FAA4F;IAC5F,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,kFAAkF;IAClF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB;;;;OAIG;IACH,QAAQ,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,oEAAoE;IACpE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CACxB;AAED,wBAAgB,cAAc,CAAC,EAC7B,GAAG,EACH,MAAM,EACN,SAAS,EACT,SAAiB,EACjB,IAAI,EACJ,eAAe,EACf,SAAS,EACT,IAAW,EACX,OAAO,EACP,QAAQ,EACR,WAAmC,EACnC,IAAI,EACJ,GAAG,WAAW,EACf,EAAE,mBAAmB,qBAmXrB"}
|
|
@@ -2,32 +2,44 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import './videoCutEditor.css';
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { useEditorVideoSource } from './hlsSource.js';
|
|
5
|
-
import { Scissors,
|
|
5
|
+
import { ArrowLeft, Pause, Play, Scissors, StepBack, StepForward, Undo2 } from 'lucide-react';
|
|
6
6
|
import { ActionBar } from '../components/ActionBar.js';
|
|
7
7
|
import { Button } from '../components/Button.js';
|
|
8
|
+
import { Dialog, DialogContent, DialogTitle, focusPanelOnOpen } from '../components/dialog.js';
|
|
9
|
+
import { IconButton } from '../components/IconButton.js';
|
|
10
|
+
import { useDiscardGuard } from '../hooks/useDiscardGuard.js';
|
|
8
11
|
import { InfoHint } from '../components/InfoHint.js';
|
|
9
12
|
import { Tooltip } from '../components/Tooltip.js';
|
|
10
|
-
import { Modal } from '../components/Modal.js';
|
|
11
13
|
import { addSplit, cutSeconds, cutTimecode, EMPTY_PLAN, keptSeconds, planCuts, planProblem, planSegments, removeSplit, toggleSegment, } from 'cursedbelt-core/media/video-cuts';
|
|
12
14
|
import { cn, RAW_BUTTON } from '../lib/cn.js';
|
|
13
15
|
import { Kbd } from '../palette/Kbd.js';
|
|
14
|
-
|
|
16
|
+
import { CutTimeline } from './CutTimeline.js';
|
|
17
|
+
import { frameAt, frameStart, frameTimecode, snapToFrame } from './cutTimelineMath.js';
|
|
18
|
+
import { useFrameRate } from './useFrameRate.js';
|
|
19
|
+
export function VideoCutEditor({ src, hlsUrl, posterUrl, resolving = false, name, durationSeconds, frameRate, open = true, onClose, onSubmit, submitLabel = 'Send for processing', note, ...passThrough }) {
|
|
15
20
|
const videoRef = React.useRef(null);
|
|
16
21
|
const [duration, setDuration] = React.useState(durationSeconds && Number.isFinite(durationSeconds) ? durationSeconds : 0);
|
|
17
22
|
const [playhead, setPlayhead] = React.useState(0);
|
|
23
|
+
const [playing, setPlaying] = React.useState(false);
|
|
18
24
|
const [plan, setPlan] = React.useState(EMPTY_PLAN);
|
|
19
25
|
const [submitting, setSubmitting] = React.useState(false);
|
|
20
26
|
const [error, setError] = React.useState(null);
|
|
27
|
+
// Marked cuts are minutes of scrubbing: Back, Escape and Cancel ask before dropping them.
|
|
28
|
+
const { guardedClose, discardOverlay, isConfirming, cancelDiscard } = useDiscardGuard({
|
|
29
|
+
onClose,
|
|
30
|
+
isDirty: plan.splits.length > 0,
|
|
31
|
+
open,
|
|
32
|
+
});
|
|
21
33
|
// A source that changes is a fresh attempt: it must not inherit the previous one's failure,
|
|
22
34
|
// the same rule `VideoTurner` and the gallery's own `ThumbImg` keep.
|
|
23
35
|
const [failedSrc, setFailedSrc] = React.useState(null);
|
|
24
36
|
/**
|
|
25
37
|
* Why there is no film to mark up — `null` when there is one.
|
|
26
38
|
*
|
|
27
|
-
* 🔴 The
|
|
39
|
+
* 🔴 The page SAYS it rather than rendering the black rectangle a source-less `<video>`
|
|
28
40
|
* paints. Marking a cut means scrubbing to a moment, so with nothing to play there is
|
|
29
41
|
* genuinely nothing to mark — and the one thing worse than that is a player that looks
|
|
30
|
-
* broken and a Send button whose refusal ("Nothing is marked") reads as the
|
|
42
|
+
* broken and a Send button whose refusal ("Nothing is marked") reads as the page's fault.
|
|
31
43
|
*/
|
|
32
44
|
const cannotShow = resolving
|
|
33
45
|
? 'Finding this film…'
|
|
@@ -37,54 +49,79 @@ export function VideoCutEditor({ src, hlsUrl, posterUrl, resolving = false, name
|
|
|
37
49
|
? 'The film could not be loaded, so there is nothing to mark up. Nothing has been sent.'
|
|
38
50
|
: null;
|
|
39
51
|
const source = useEditorVideoSource(videoRef, { src, hlsUrl }, cannotShow === null);
|
|
52
|
+
const { rate, onLoadedData } = useFrameRate(videoRef, frameRate, source.key);
|
|
53
|
+
const fps = rate.fps;
|
|
40
54
|
const segments = React.useMemo(() => planSegments(plan, duration), [plan, duration]);
|
|
41
55
|
const cuts = React.useMemo(() => planCuts(plan, duration), [plan, duration]);
|
|
42
56
|
const problem = planProblem(plan, duration);
|
|
43
|
-
const pct = React.useCallback((seconds) => (duration > 0 ? Math.min(100, Math.max(0, (seconds / duration) * 100)) : 0), [duration]);
|
|
44
57
|
const seek = React.useCallback((seconds) => {
|
|
45
58
|
const video = videoRef.current;
|
|
46
|
-
if (!
|
|
59
|
+
if (!Number.isFinite(seconds))
|
|
47
60
|
return;
|
|
48
|
-
|
|
49
|
-
|
|
61
|
+
const at = Math.max(0, seconds);
|
|
62
|
+
if (video)
|
|
63
|
+
video.currentTime = at;
|
|
64
|
+
setPlayhead(at);
|
|
50
65
|
}, []);
|
|
51
|
-
/**
|
|
52
|
-
const
|
|
66
|
+
/** Where the playhead really is — the element's own clock, which a `timeupdate` trails. */
|
|
67
|
+
const now = () => videoRef.current?.currentTime ?? playhead;
|
|
68
|
+
/** `,` / `.` — one frame back or forward, pausing first, as YouTube and mpv do. */
|
|
69
|
+
const stepFrame = (direction) => {
|
|
70
|
+
const video = videoRef.current;
|
|
71
|
+
if (video && !video.paused)
|
|
72
|
+
video.pause();
|
|
73
|
+
const last = duration > 0 ? frameAt(duration - 1e-6, fps) : 0;
|
|
74
|
+
const frame = Math.min(last, Math.max(0, frameAt(now(), fps) + direction));
|
|
75
|
+
seek(frameStart(frame, fps));
|
|
76
|
+
};
|
|
77
|
+
const togglePlay = () => {
|
|
78
|
+
const video = videoRef.current;
|
|
79
|
+
if (!video)
|
|
80
|
+
return;
|
|
81
|
+
if (video.paused)
|
|
82
|
+
void video.play().catch(() => undefined);
|
|
83
|
+
else
|
|
84
|
+
video.pause();
|
|
85
|
+
};
|
|
86
|
+
/** ✂ — divide the clip at the start of the frame the playhead is on. */
|
|
87
|
+
const cutHere = () => {
|
|
53
88
|
setError(null);
|
|
89
|
+
const at = snapToFrame(now(), fps);
|
|
54
90
|
setPlan((current) => {
|
|
55
|
-
const next = addSplit(current,
|
|
91
|
+
const next = addSplit(current, at, duration);
|
|
56
92
|
if (next === current)
|
|
57
93
|
setError("There is already a cut there, or the playhead is too close to an end of the clip to cut.");
|
|
58
94
|
return next;
|
|
59
95
|
});
|
|
60
|
-
}
|
|
96
|
+
};
|
|
61
97
|
/** ✗ on a piece — mark it out, or put it back. */
|
|
62
98
|
const toggle = React.useCallback((segment) => {
|
|
63
99
|
setError(null);
|
|
64
100
|
setPlan((current) => toggleSegment(current, segment, duration));
|
|
65
101
|
}, [duration]);
|
|
66
102
|
/** Undo a cut, merging the pieces either side. */
|
|
67
|
-
const uncut =
|
|
103
|
+
const uncut = (at) => {
|
|
68
104
|
setError(null);
|
|
69
105
|
setPlan((current) => removeSplit(current, at, duration));
|
|
70
|
-
}
|
|
106
|
+
};
|
|
71
107
|
/*
|
|
72
|
-
* `C` cuts
|
|
108
|
+
* `C` cuts, `Space` plays, `,` and `.` step a frame — and nothing else is bound, deliberately.
|
|
73
109
|
*
|
|
74
|
-
* 🔴 Removing a piece has NO key. It is the only irreversible-feeling action
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
110
|
+
* 🔴 Removing a piece has NO key. It is the only irreversible-feeling action on the page and
|
|
111
|
+
* the one whose target is ambiguous from the keyboard: with the playhead between two splits,
|
|
112
|
+
* "delete" could mean the piece behind it or the piece ahead. Clicking the piece says which,
|
|
113
|
+
* which is the entire point of the model. Every segment is a real focusable button, so the
|
|
114
|
+
* keyboard route is Tab-then-Enter and it names its target out loud.
|
|
79
115
|
*
|
|
80
|
-
* Bound on the
|
|
81
|
-
* and a global listener would keep firing after this closed.
|
|
82
|
-
* dialog really does have the keystrokes while it is open.
|
|
116
|
+
* Bound on the PAGE, not the window: the gallery has its own ⇧←/⇧→ and Space handling (it
|
|
117
|
+
* stands down while this is open), and a global listener would keep firing after this closed.
|
|
83
118
|
*/
|
|
84
119
|
const onKeyDown = (event) => {
|
|
85
120
|
const target = event.target;
|
|
86
121
|
// Never steal a key from a text field — the app may render one in `note`.
|
|
87
|
-
if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA'))
|
|
122
|
+
if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable))
|
|
123
|
+
return;
|
|
124
|
+
if (event.metaKey || event.ctrlKey || event.altKey || isConfirming)
|
|
88
125
|
return;
|
|
89
126
|
if (event.key === 'c' || event.key === 'C') {
|
|
90
127
|
event.preventDefault();
|
|
@@ -92,13 +129,11 @@ export function VideoCutEditor({ src, hlsUrl, posterUrl, resolving = false, name
|
|
|
92
129
|
}
|
|
93
130
|
else if (event.key === ' ') {
|
|
94
131
|
event.preventDefault();
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
else
|
|
101
|
-
video.pause();
|
|
132
|
+
togglePlay();
|
|
133
|
+
}
|
|
134
|
+
else if (event.key === ',' || event.key === '.') {
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
stepFrame(event.key === ',' ? -1 : 1);
|
|
102
137
|
}
|
|
103
138
|
};
|
|
104
139
|
const submit = async () => {
|
|
@@ -121,36 +156,35 @@ export function VideoCutEditor({ src, hlsUrl, posterUrl, resolving = false, name
|
|
|
121
156
|
setSubmitting(false);
|
|
122
157
|
}
|
|
123
158
|
};
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
type: 'button', className: cn(RAW_BUTTON, 'cbvc-row-goto'), onClick: () => seek(segment.start), children: [_jsx(Scissors, { "aria-hidden": 'true', size: 12 }), " Cut at ", cutTimecode(segment.start)] }), _jsx(Button, { size: 'sm', variant: 'ghost', tooltip: 'Undo this cut \u2014 the sections either side become one again', "aria-label": `Undo the cut at ${cutTimecode(segment.start)}`, onClick: () => uncut(segment.start), children: "Undo" })] }, `cut-${segment.key}`))) })) : null, note ? _jsx("div", { className: 'cbvc-note', children: note }) : null, error ? (_jsx("p", { className: 'cbvc-error', role: 'alert', children: error })) : null] }) }));
|
|
159
|
+
const transport = (_jsxs("div", { className: 'cbvc-transport', children: [_jsx(IconButton, { size: 'sm', label: 'Back one frame', tooltip: 'Back one frame (,)', onClick: () => stepFrame(-1), disabled: cannotShow !== null, children: _jsx(StepBack, { "aria-hidden": 'true' }) }), _jsx(IconButton, { size: 'sm', label: playing ? 'Pause' : 'Play', tooltip: playing ? 'Pause (Space)' : 'Play (Space)', onClick: togglePlay, disabled: cannotShow !== null, children: playing ? _jsx(Pause, { "aria-hidden": 'true' }) : _jsx(Play, { "aria-hidden": 'true' }) }), _jsx(IconButton, { size: 'sm', label: 'Forward one frame', tooltip: 'Forward one frame (.)', onClick: () => stepFrame(1), disabled: cannotShow !== null, children: _jsx(StepForward, { "aria-hidden": 'true' }) }), _jsxs("span", { className: 'cbvc-readout', "data-testid": 'trim-readout', children: [_jsx("strong", { children: frameTimecode(playhead, fps) }), _jsxs("span", { className: 'cbvc-readout-of', children: [" / ", frameTimecode(duration, fps)] })] }), _jsx(Tooltip, { content: rate.source === 'probe'
|
|
160
|
+
? 'The frame rate the file was probed at'
|
|
161
|
+
: rate.source === 'measured'
|
|
162
|
+
? 'The frame rate, measured from the clip'
|
|
163
|
+
: 'Assumed — the clip has not said its frame rate, so frame steps may not match its frames', children: _jsx("span", { className: 'cbvc-fps', "data-source": rate.source, tabIndex: 0, children: rate.source === 'assumed' ? `${Math.round(fps)} fps?` : `${Number(fps.toFixed(3))} fps` }) }), _jsxs(Button, { size: 'sm', variant: 'secondary', onClick: cutHere, tooltip: 'Cut the clip at this frame (C)', disabled: cannotShow !== null, children: [_jsx(Scissors, { "aria-hidden": 'true', size: 14 }), " Cut here"] })] }));
|
|
164
|
+
return (_jsx(Dialog, { open: open, onOpenChange: (next) => !next && guardedClose(), children: _jsxs(DialogContent, { ...passThrough, className: 'cbvc-page', overlayClassName: 'cbvc-page-scrim', "aria-describedby": undefined, onOpenAutoFocus: focusPanelOnOpen, onEscapeKeyDown: (event) => {
|
|
165
|
+
event.preventDefault();
|
|
166
|
+
if (isConfirming)
|
|
167
|
+
cancelDiscard();
|
|
168
|
+
else
|
|
169
|
+
guardedClose();
|
|
170
|
+
}, onKeyDown: onKeyDown, children: [_jsxs("header", { className: 'cbvc-bar', children: [_jsx(IconButton, { label: 'Back', tooltip: 'Back to the file, sending nothing (Esc)', onClick: guardedClose, children: _jsx(ArrowLeft, { "aria-hidden": 'true' }) }), _jsx(DialogTitle, { className: 'cbvc-title', children: name ? `Trim ${name}` : 'Trim video' }), _jsxs(InfoHint, { title: 'Marking a video to trim', align: 'right', children: [_jsxs("p", { children: ["Press ", _jsx("strong", { children: "Cut here" }), " (or ", _jsx(Kbd, { children: "C" }), ") to divide the clip at the frame the playhead is on. Every piece between two cuts is a section."] }), _jsxs("p", { children: ["Click ", _jsx("strong", { children: "\u2717" }), " on a section to remove it \u2014 click again to keep it after all.", _jsx(Kbd, { children: "," }), " and ", _jsx(Kbd, { children: "." }), " step one frame, ", _jsx(Kbd, { children: "Space" }), " plays and pauses."] }), _jsx("p", { children: "Zoom the timeline with \u2318 or Ctrl and the wheel, a pinch, or the slider; drag or scroll to move along it, and click the ruler to put the playhead there." }), _jsx("p", { children: "Nothing here changes your video. Sending makes a trimmed COPY beside the original, and you choose which to keep." })] })] }), _jsx("div", { className: cn('cbvc-stage', cannotShow !== null && 'cbvc-stage-empty'), children: cannotShow === null ? (_jsx("video", { ref: videoRef, className: 'cbvc-video', src: source.src, poster: posterUrl, playsInline: true, preload: 'auto', onClick: togglePlay, onLoadedMetadata: (event) => {
|
|
171
|
+
const seen = event.currentTarget.duration;
|
|
172
|
+
// The element's own metadata wins over anything the catalog rounded.
|
|
173
|
+
if (Number.isFinite(seen) && seen > 0)
|
|
174
|
+
setDuration(seen);
|
|
175
|
+
}, onLoadedData: onLoadedData, onPlay: () => setPlaying(true), onPause: () => setPlaying(false), onTimeUpdate: (event) => setPlayhead(event.currentTarget.currentTime), onSeeked: (event) => setPlayhead(event.currentTarget.currentTime), onError: (event) => {
|
|
176
|
+
if (source.exhausted(event.currentTarget))
|
|
177
|
+
setFailedSrc(source.key);
|
|
178
|
+
} }, source.key)) : (_jsx("p", { className: 'cbvc-video cbvc-nofilm', role: 'status', children: cannotShow })) }), _jsxs("div", { className: 'cbvc-deck', children: [duration > 0 ? (_jsx(CutTimeline, { duration: duration, fps: fps, playhead: playhead, segments: segments, onSeek: seek, onToggle: toggle, transport: transport })) : (_jsx("div", { className: 'cbvc-tools', children: transport })), plan.splits.length > 0 ? (_jsx("ul", { className: 'cbvc-list', "aria-label": 'Cuts', children: segments.slice(1).map((segment) => (_jsxs("li", { className: 'cbvc-row', children: [_jsxs("button", {
|
|
179
|
+
// guardrails-ignore no-raw-action-button: a plain-CSS row inside a lazily
|
|
180
|
+
// imported, deliberately Tailwind-free editor — see `pictureEditor.css`.
|
|
181
|
+
type: 'button', className: cn(RAW_BUTTON, 'cbvc-row-goto'), onClick: () => seek(segment.start), children: [_jsx(Scissors, { "aria-hidden": 'true', size: 12 }), " ", frameTimecode(segment.start, fps)] }), _jsx(IconButton, { size: 'sm', label: `Undo the cut at ${frameTimecode(segment.start, fps)}`, tooltip: 'Undo this cut \u2014 the sections either side become one again', onClick: () => uncut(segment.start), children: _jsx(Undo2, { "aria-hidden": 'true' }) })] }, `cut-${segment.key}`))) })) : null, _jsxs("div", { className: 'cbvc-note', children: [_jsx("p", { children: "Each cut lands on the frame you mark: the trimmed copy is re-encoded at every join." }), note ? _jsx("div", { children: note }) : null] }), error ? (_jsx("p", { className: 'cbvc-error', role: 'alert', children: error })) : null] }), _jsx(ActionBar, { className: 'cbvc-foot', secondary: _jsx(Button, { variant: 'secondary', onClick: guardedClose, tooltip: 'Close without sending anything', children: "Cancel" }), primary: _jsx(Button, { onClick: () => void submit(), disabled: submitting || problem !== null, tooltip: problem ?? 'Queue the cuts — the edited copy arrives beside the original', children: submitting ? 'Sending…' : submitLabel }), children: _jsx("p", { className: 'cbvc-summary', children: duration > 0 ? (_jsxs(_Fragment, { children: [_jsx("strong", { children: cutTimecode(keptSeconds(cuts, duration)) }), " kept of ", cutTimecode(duration), cuts.length > 0
|
|
182
|
+
? ` — removing ${cutTimecode(cutSeconds(cuts, duration))} in ${cuts.length === 1 ? '1 section' : `${cuts.length} sections`}`
|
|
183
|
+
: segments.length > 1
|
|
184
|
+
? ' — nothing marked for removal yet'
|
|
185
|
+
: ''] })) : cannotShow === null ? ('Reading the clip…') : (
|
|
186
|
+
// 🔴 Only a mounted film is being read. With none, this said "Reading the clip…"
|
|
187
|
+
// forever beside the reason there was nothing to read (owner, 2026-10-27).
|
|
188
|
+
'Its length is not known.') }) }), discardOverlay] }) }));
|
|
155
189
|
}
|
|
156
190
|
//# sourceMappingURL=VideoCutEditor.js.map
|