diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-03-31 04:03:52 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-03-31 04:03:52 +0200 |
| commit | dcf54c1250431f5a4a2848f7c07781ef5eebacaf (patch) | |
| tree | 64270f82d879ce0b0a028384e513ea87940da630 /apps/web/src/components/editor | |
| parent | 5cc6abc3a44c9d046a1cb71a8c916f1d35827e8f (diff) | |
| download | opencut-dcf54c1250431f5a4a2848f7c07781ef5eebacaf.tar.gz | |
feat: migrate time utilities from typescript to rust WASM
Diffstat (limited to 'apps/web/src/components/editor')
3 files changed, 228 insertions, 235 deletions
diff --git a/apps/web/src/components/editor/dialogs/project-info-dialog.tsx b/apps/web/src/components/editor/dialogs/project-info-dialog.tsx index fad711e3..c31c8e96 100644 --- a/apps/web/src/components/editor/dialogs/project-info-dialog.tsx +++ b/apps/web/src/components/editor/dialogs/project-info-dialog.tsx @@ -1,83 +1,80 @@ -import {
- Dialog,
- DialogBody,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import type { TProjectMetadata } from "@/lib/project/types";
-import { formatDate } from "@/utils/date";
-import { formatTimeCode } from "@/lib/time";
-import { Button } from "@/components/ui/button";
-
-function InfoRow({
- label,
- value,
-}: {
- label: string;
- value: string | React.ReactNode;
-}) {
- return (
- <div className="flex justify-between items-center py-0 last:pb-0">
- <span className="text-muted-foreground text-sm">{label}</span>
- <span className="text-sm font-medium">{value}</span>
- </div>
- );
-}
-
-export function ProjectInfoDialog({
- isOpen,
- onOpenChange,
- project,
-}: {
- isOpen: boolean;
- onOpenChange: (open: boolean) => void;
- project: TProjectMetadata;
-}) {
- const durationFormatted =
- project.duration > 0
- ? formatTimeCode({
- timeInSeconds: project.duration,
- format: project.duration >= 3600 ? "HH:MM:SS" : "MM:SS",
- })
- : "0:00";
-
- return (
- <Dialog open={isOpen} onOpenChange={onOpenChange}>
- <DialogContent onOpenAutoFocus={(event) => event.preventDefault()}>
- <DialogHeader>
- <DialogTitle className="truncate max-w-[350px]">
- {project.name}
- </DialogTitle>
- </DialogHeader>
-
- <DialogBody className="flex flex-col">
- <InfoRow label="Duration" value={durationFormatted} />
- <InfoRow
- label="Created"
- value={formatDate({ date: project.createdAt })}
- />
- <InfoRow
- label="Modified"
- value={formatDate({ date: project.updatedAt })}
- />
- <InfoRow
- label="Project ID"
- value={
- <code className="text-xs bg-muted px-1.5 py-0.5 rounded">
- {project.id.slice(0, 8)}
- </code>
- }
- />
- </DialogBody>
- <DialogFooter>
- <Button variant="outline" onClick={() => onOpenChange(false)}>
- Close
- </Button>
- <Button onClick={() => onOpenChange(false)}>Done</Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- );
-}
+import { + Dialog, + DialogBody, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import type { TProjectMetadata } from "@/lib/project/types"; +import { formatDate } from "@/utils/date"; +import { formatTimeCode } from "opencut-wasm"; +import { Button } from "@/components/ui/button"; + +function InfoRow({ + label, + value, +}: { + label: string; + value: string | React.ReactNode; +}) { + return ( + <div className="flex justify-between items-center py-0 last:pb-0"> + <span className="text-muted-foreground text-sm">{label}</span> + <span className="text-sm font-medium">{value}</span> + </div> + ); +} + +export function ProjectInfoDialog({ + isOpen, + onOpenChange, + project, +}: { + isOpen: boolean; + onOpenChange: (open: boolean) => void; + project: TProjectMetadata; +}) { + const durationFormatted = + project.duration > 0 + ? (formatTimeCode({ timeInSeconds: project.duration, format: project.duration >= 3600 ? "HH:MM:SS" : "MM:SS" }) ?? "") + : "0:00"; + + return ( + <Dialog open={isOpen} onOpenChange={onOpenChange}> + <DialogContent onOpenAutoFocus={(event) => event.preventDefault()}> + <DialogHeader> + <DialogTitle className="truncate max-w-[350px]"> + {project.name} + </DialogTitle> + </DialogHeader> + + <DialogBody className="flex flex-col"> + <InfoRow label="Duration" value={durationFormatted} /> + <InfoRow + label="Created" + value={formatDate({ date: project.createdAt })} + /> + <InfoRow + label="Modified" + value={formatDate({ date: project.updatedAt })} + /> + <InfoRow + label="Project ID" + value={ + <code className="text-xs bg-muted px-1.5 py-0.5 rounded"> + {project.id.slice(0, 8)} + </code> + } + /> + </DialogBody> + <DialogFooter> + <Button variant="outline" onClick={() => onOpenChange(false)}> + Close + </Button> + <Button onClick={() => onOpenChange(false)}>Done</Button> + </DialogFooter> + </DialogContent> + </Dialog> + ); +} diff --git a/apps/web/src/components/editor/panels/preview/toolbar.tsx b/apps/web/src/components/editor/panels/preview/toolbar.tsx index 5c183bbb..340058d0 100644 --- a/apps/web/src/components/editor/panels/preview/toolbar.tsx +++ b/apps/web/src/components/editor/panels/preview/toolbar.tsx @@ -1,151 +1,147 @@ -"use client";
-
-import { useState, useEffect } from "react";
-import { useEditor } from "@/hooks/use-editor";
-import { formatTimeCode } from "@/lib/time";
-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 "@/constants/editor-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>
- );
-}
+"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 "@/constants/editor-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> + ); +} diff --git a/apps/web/src/components/editor/panels/timeline/bookmarks.tsx b/apps/web/src/components/editor/panels/timeline/bookmarks.tsx index b47f9d46..e2dc09f1 100644 --- a/apps/web/src/components/editor/panels/timeline/bookmarks.tsx +++ b/apps/web/src/components/editor/panels/timeline/bookmarks.tsx @@ -10,7 +10,7 @@ import { TIMELINE_BOOKMARK_ROW_HEIGHT, } from "@/constants/timeline-constants"; import { DEFAULT_FPS } from "@/constants/project-constants"; -import { getSnappedSeekTime } from "@/lib/time"; +import { getSnappedSeekTime } from "opencut-wasm"; import { ArrowTurnBackwardIcon, Delete02Icon, |
