{captions.map((s) => (
{
setCaptionId(s.id);
setPanel("caption");
setSubTab("text");
}}
onContextMenu={(e) => {
setCaptionId(s.id);
setPanel("caption");
openContext(e, "caption");
}}
onPointerDown={(e) => {
const x = e.clientX,
original = plan;
let moved = false;
drag(
e,
(event) => {
moved = true;
const start = Math.round(
clamp(
snapped(s.start / SECOND + (event.clientX - x) / pps) * SECOND,
0,
timeline.duration - (s.end - s.start),
),
);
onChange({
...original,
captions: captions.map((c) =>
c.id === s.id ? { ...c, start, end: start + s.end - s.start } : c,
),
});
},
() => {
if (moved) {
undo.current.push(original);
redo.current = [];
}
},
);
}}
>
{s.text}
{captionId === s.id && (
<>
dragCaptionEdge(e, s, "start")}
/>
dragCaptionEdge(e, s, "end")}
/>
>
)}
))}
)}
{timeline.clips.map((c, i) => {
const asset = plan.assets.find((a) => a.id === c.assetId)!,
width = (c.duration / SECOND) * pps;
return (
dragClip(e, i)}
onClick={() => {
setSelected(c.id);
setPanel("video");
setCaptionId(undefined);
}}
onContextMenu={(e) => {
setSelected(c.id);
setPanel("video");
openContext(e, "clip");
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
setSelected(c.id);
setPanel("video");
}
}}
>
{Array.from({ length: Math.min(24, Math.ceil(width / 88)) }, (_, j) => (
))}
{asset.name} {frameStamp(c.duration / SECOND)}
{selected === c.id && (
<>
dragTrim(e, i, "left")}
/>
dragTrim(e, i, "right")}
/>
>
)}
);
})}
{timeline.clips.slice(1).map(
(c, i) =>
(transitionsOpen || c.overlap > 0) && (
),
)}
{timeline.audioClips.map((c) => (
a.id === c.assetId)?.name}`}
className={`se-audio-clip ${audioId === c.id ? "selected" : ""}`}
style={{ left: (c.start / SECOND) * pps, width: (c.duration / SECOND) * pps }}
onPointerDown={(e) => dragAudio(e, c)}
onClick={() => {
setAudioId(c.id);
setSelected(undefined);
setPanel("audio");
}}
onContextMenu={(e) => {
setAudioId(c.id);
setSelected(undefined);
setPanel("audio");
openContext(e, "audio");
}}
>
{plan.assets.find((a) => a.id === c.assetId)?.name}{" "}
{frameStamp(c.duration / SECOND)}
{audioId === c.id && (
<>
dragAudio(e, c, "left")}
/>
dragAudio(e, c, "right")}
/>
>
)}
))}
{context && (
e.stopPropagation()}
>
{context.kind === "clip" && (
)}
{context.kind !== "timeline" && (
)}
)}
);
}
function StyleNumber({
label,
value,
min,
max,
onChange,
}: {
label: string;
value: number;
min: number;
max: number;
onChange: (v: number) => void;
}) {
return (
);
}
function StyleColor({
label,
value,
onChange,
}: {
label: string;
value: string;
onChange: (v: string) => void;
}) {
return (
);
}
function TransitionDuration({
value,
disabled,
onChange,
}: {
value: number;
disabled: boolean;
onChange: (value: number) => void;
}) {
const [text, setText] = useState(value.toFixed(1));
useEffect(() => setText(value.toFixed(1)), [value]);
const commit = () => {
const n = Number(text);
if (text.trim() && Number.isFinite(n)) {
const v = clamp(Math.round(n * 10) / 10, 0.2, 4);
setText(v.toFixed(1));
onChange(v);
} else setText(value.toFixed(1));
};
return (
);
}
function ValueField({
label,
suffix,
value,
min,
max,
precision,
onChange,
}: {
label: string;
suffix: string;
value: number;
min: number;
max: number;
precision: number;
onChange: (v: number) => void;
}) {
const [text, setText] = useState(value.toFixed(precision));
useEffect(() => setText(value.toFixed(precision)), [value, precision]);
const commit = () => {
const v = Number(text);
if (text.trim() && Number.isFinite(v)) {
const bounded = clamp(v, min, max);
setText(bounded.toFixed(precision));
onChange(bounded);
} else setText(value.toFixed(precision));
};
const step = suffix.endsWith("dB") ? 0.5 : suffix.endsWith("x") ? 0.01 : 0.1;
return (
);
}
function CutIcon({ side }: { side?: "left" | "right" }) {
return (
);
}
function TransitionIcon() {
return (
);
}
export function SmartEditEmptyEditor({
onClose,
onConfigure,
onRetry,
loading,
message,
}: {
onClose: () => void;
onConfigure: () => void;
onRetry: () => void;
loading: boolean;
message: string;
}) {
return (
{message}
{!loading && (
{/失败|错误|中断/.test(message) && (
)}
)}
);
}