{captionsOpen && (
apply({ ...plan, subtitles: !plan.subtitles })}
>
{plan.subtitles ? : }
)}
c.gain > 0) ? "静音视频原声" : "恢复视频原声"}
onClick={() =>
apply({
...plan,
clips: plan.clips.map((c) => ({
...c,
gain: plan.clips.some((c) => c.gain > 0) ? 0 : 1,
})),
})
}
>
{plan.clips.some((c) => c.gain > 0) ? : }
{
seek(position(e.clientX));
drag(e, (event) => seek(snapped(position(event.clientX))));
}}
>
{Array.from(
{ length: Math.ceil(Math.max(duration + 1, (viewportWidth - 112) / pps) * 10) },
(_, i) => (
{i % 10 === 0 && {stamp(i / 10)}}
),
)}
{captionsOpen && (
{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) && (
),
)}
{context && (
e.stopPropagation()}
>
{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 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)) onChange(clamp(v, min, max));
else setText(value.toFixed(precision));
};
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) && (
)}
)}
);
}