OpenCut/apps/web/src/components/editor/panels/preview/toolbar.tsx

148 lines
4.0 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { useEditor } from "@/hooks/use-editor";
import { formatTimeCode } from "opencut-wasm";
import { invokeAction } from "@/lib/actions";
import { EditableTimecode } from "@/components/editable-timecode";
import { Button } from "@/components/ui/button";
import {
FullScreenIcon,
GridTableIcon,
PauseIcon,
PlayIcon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { getGuideById } from "@/lib/guides";
import { Separator } from "@/components/ui/separator";
import {
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectSeparator,
} from "@/components/ui/select";
import { PREVIEW_ZOOM_PRESETS } from "@/lib/preview/constants";
import { usePreviewViewport } from "./preview-viewport";
import { GridPopover } from "./guide-popover";
import { usePreviewStore } from "@/stores/preview-store";
export function PreviewToolbar({
onToggleFullscreen,
}: {
onToggleFullscreen: () => void;
}) {
const activeGuide = usePreviewStore((state) => state.activeGuide);
const activeGuideDefinition = getGuideById(activeGuide);
return (
<div className="grid grid-cols-[1fr_auto_1fr] items-center pb-3 pt-5 px-5">
<TimecodeDisplay />
<PlayPauseButton />
<div className="justify-self-end flex items-center gap-2.5">
<ZoomSelect />
<Separator orientation="vertical" className="h-4" />
<GridPopover>
<Button
variant={activeGuideDefinition ? "secondary" : "text"}
size="icon"
>
{activeGuideDefinition ? (
activeGuideDefinition.renderTriggerIcon()
) : (
<HugeiconsIcon icon={GridTableIcon} />
)}
</Button>
</GridPopover>
<Button variant="text" onClick={onToggleFullscreen}>
<HugeiconsIcon icon={FullScreenIcon} />
</Button>
</div>
</div>
);
}
function TimecodeDisplay() {
const editor = useEditor();
const totalDuration = useEditor((e) => e.timeline.getTotalDuration());
const fps = useEditor((e) => e.project.getActive().settings.fps);
const [currentTime, setCurrentTime] = useState(() =>
editor.playback.getCurrentTime(),
);
useEffect(() => {
const handler = (e: Event) =>
setCurrentTime((e as CustomEvent<{ time: number }>).detail.time);
window.addEventListener("playback-update", handler);
window.addEventListener("playback-seek", handler);
return () => {
window.removeEventListener("playback-update", handler);
window.removeEventListener("playback-seek", handler);
};
}, []);
return (
<div className="flex items-center">
<EditableTimecode
time={currentTime}
duration={totalDuration}
format="HH:MM:SS:FF"
fps={fps}
onTimeChange={({ time }) => editor.playback.seek({ time })}
className="text-center"
/>
<span className="text-muted-foreground px-2 font-mono text-xs">/</span>
<span className="text-muted-foreground font-mono text-xs">
{formatTimeCode({ timeInSeconds: totalDuration, format: "HH:MM:SS:FF", fps })}
</span>
</div>
);
}
function ZoomSelect() {
const { isAtFit, zoomPercent, fitToScreen, setViewportPercent } =
usePreviewViewport();
const displayLabel = isAtFit ? "Fit" : `${zoomPercent}%`;
const onValueChange = (value: string) => {
if (value === "fit") {
fitToScreen();
} else {
setViewportPercent({ percent: Number(value) });
}
};
return (
<Select
value={isAtFit ? "fit" : String(zoomPercent)}
onValueChange={onValueChange}
>
<SelectTrigger className="tabular-nums">{displayLabel}</SelectTrigger>
<SelectContent>
<SelectItem value="fit">Fit</SelectItem>
<SelectSeparator />
{PREVIEW_ZOOM_PRESETS.map((preset) => (
<SelectItem key={preset} value={String(preset)}>
{preset}%
</SelectItem>
))}
</SelectContent>
</Select>
);
}
function PlayPauseButton() {
const isPlaying = useEditor((e) => e.playback.getIsPlaying());
return (
<Button
variant="text"
size="icon"
onClick={() => invokeAction("toggle-play")}
>
<HugeiconsIcon icon={isPlaying ? PauseIcon : PlayIcon} />
</Button>
);
}