diff options
Diffstat (limited to 'apps/web/src/components')
| -rw-r--r-- | apps/web/src/components/background-settings.tsx | 4 | ||||
| -rw-r--r-- | apps/web/src/components/editor-header.tsx | 15 | ||||
| -rw-r--r-- | apps/web/src/components/editor/preview-panel.tsx | 24 | ||||
| -rw-r--r-- | apps/web/src/components/editor/properties-panel/index.tsx | 36 | ||||
| -rw-r--r-- | apps/web/src/components/editor/timeline-track.tsx | 37 | ||||
| -rw-r--r-- | apps/web/src/components/editor/timeline.tsx | 40 |
6 files changed, 105 insertions, 51 deletions
diff --git a/apps/web/src/components/background-settings.tsx b/apps/web/src/components/background-settings.tsx index bcefac1c..721564ea 100644 --- a/apps/web/src/components/background-settings.tsx +++ b/apps/web/src/components/background-settings.tsx @@ -40,9 +40,9 @@ export function BackgroundSettings() { <Button
variant="text"
size="icon"
- className="!size-5 border border-muted-foreground"
+ className="!size-4 border border-muted-foreground"
>
- <BackgroundIcon className="!size-4" />
+ <BackgroundIcon className="!size-3" />
</Button>
</PopoverTrigger>
<PopoverContent className="flex flex-col items-start w-[20rem] h-[16rem] overflow-hidden p-0">
diff --git a/apps/web/src/components/editor-header.tsx b/apps/web/src/components/editor-header.tsx index 666ddaed..c4081c75 100644 --- a/apps/web/src/components/editor-header.tsx +++ b/apps/web/src/components/editor-header.tsx @@ -31,13 +31,24 @@ export function EditorHeader() { const centerContent = (
<div className="flex items-center gap-2 text-xs">
- <span>{formatTimeCode(getTotalDuration(), "HH:MM:SS:CS")}</span>
+ <span>
+ {formatTimeCode(
+ getTotalDuration(),
+ "HH:MM:SS:FF",
+ activeProject?.fps || 30
+ )}
+ </span>
</div>
);
const rightContent = (
<nav className="flex items-center gap-2">
- <Button size="sm" variant="primary" className="h-7 text-xs" onClick={handleExport}>
+ <Button
+ size="sm"
+ variant="primary"
+ className="h-7 text-xs"
+ onClick={handleExport}
+ >
<Download className="h-4 w-4" />
<span className="text-sm">Export</span>
</Button>
diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx index 2f76af5a..894b6257 100644 --- a/apps/web/src/components/editor/preview-panel.tsx +++ b/apps/web/src/components/editor/preview-panel.tsx @@ -16,7 +16,7 @@ import { DropdownMenuTrigger, DropdownMenuSeparator, } from "@/components/ui/dropdown-menu"; -import { Play, Pause } from "lucide-react"; +import { Play, Pause, Expand } from "lucide-react"; import { useState, useRef, useEffect } from "react"; import { cn } from "@/lib/utils"; import { formatTimeCode } from "@/lib/time"; @@ -390,6 +390,7 @@ function PreviewToolbar({ hasAnyElements }: { hasAnyElements: boolean }) { const { isPlaying, toggle, currentTime } = usePlaybackStore(); const { setCanvasSize, setCanvasSizeToOriginal } = useEditorStore(); const { getTotalDuration } = useTimelineStore(); + const { activeProject } = useProjectStore(); const { currentPreset, isOriginal, @@ -420,11 +421,19 @@ function PreviewToolbar({ hasAnyElements }: { hasAnyElements: boolean }) { )} > <span className="text-primary tabular-nums"> - {formatTimeCode(currentTime, "HH:MM:SS:CS")} + {formatTimeCode( + currentTime, + "HH:MM:SS:FF", + activeProject?.fps || 30 + )} </span> <span className="opacity-50">/</span> <span className="tabular-nums"> - {formatTimeCode(getTotalDuration(), "HH:MM:SS:CS")} + {formatTimeCode( + getTotalDuration(), + "HH:MM:SS:FF", + activeProject?.fps || 30 + )} </span> </p> </div> @@ -447,7 +456,7 @@ function PreviewToolbar({ hasAnyElements }: { hasAnyElements: boolean }) { <DropdownMenuTrigger asChild> <Button size="sm" - className="!bg-panel-accent text-foreground/85 text-[0.75rem] h-auto rounded-none border border-muted-foreground px-0.5 py-0 font-light" + className="!bg-panel-accent text-foreground/85 text-[0.70rem] h-4 rounded-none border border-muted-foreground px-0.5 py-0 font-light" disabled={!hasAnyElements} > {getDisplayName()} @@ -475,6 +484,13 @@ function PreviewToolbar({ hasAnyElements }: { hasAnyElements: boolean }) { ))} </DropdownMenuContent> </DropdownMenu> + <Button + variant="text" + size="icon" + className="!size-4 text-muted-foreground" + > + <Expand className="!size-4" /> + </Button> </div> </div> ); diff --git a/apps/web/src/components/editor/properties-panel/index.tsx b/apps/web/src/components/editor/properties-panel/index.tsx index 0e6cb8a8..9e28850f 100644 --- a/apps/web/src/components/editor/properties-panel/index.tsx +++ b/apps/web/src/components/editor/properties-panel/index.tsx @@ -9,13 +9,28 @@ import { useMediaStore } from "@/stores/media-store"; import { AudioProperties } from "./audio-properties"; import { MediaProperties } from "./media-properties"; import { TextProperties } from "./text-properties"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "../../ui/select"; +import { FPS_PRESETS } from "@/constants/timeline-constants"; export function PropertiesPanel() { - const { activeProject } = useProjectStore(); + const { activeProject, updateProjectFps } = useProjectStore(); const { getDisplayName, canvasSize } = useAspectRatio(); const { selectedElements, tracks } = useTimelineStore(); const { mediaItems } = useMediaStore(); + const handleFpsChange = (value: string) => { + const fps = parseFloat(value); + if (!isNaN(fps) && fps > 0) { + updateProjectFps(fps); + } + }; + const emptyView = ( <div className="space-y-4 p-5"> {/* Media Properties */} @@ -26,7 +41,24 @@ export function PropertiesPanel() { label="Resolution:" value={`${canvasSize.width} × ${canvasSize.height}`} /> - <PropertyItem label="Frame rate:" value="30.00fps" /> + <div className="flex justify-between items-center"> + <Label className="text-xs text-muted-foreground">Frame rate:</Label> + <Select + value={(activeProject?.fps || 30).toString()} + onValueChange={handleFpsChange} + > + <SelectTrigger className="w-32 h-6 text-xs"> + <SelectValue /> + </SelectTrigger> + <SelectContent> + {FPS_PRESETS.map(({ value, label }) => ( + <SelectItem key={value} value={value} className="text-xs"> + {label} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> </div> </div> ); diff --git a/apps/web/src/components/editor/timeline-track.tsx b/apps/web/src/components/editor/timeline-track.tsx index 7e2e93e0..c3a16b9a 100644 --- a/apps/web/src/components/editor/timeline-track.tsx +++ b/apps/web/src/components/editor/timeline-track.tsx @@ -17,7 +17,11 @@ import type { TimelineElement as TimelineElementType, DragData, } from "@/types/timeline"; -import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; +import { + snapTimeToFrame, + TIMELINE_CONSTANTS, +} from "@/constants/timeline-constants"; +import { useProjectStore } from "@/stores/project-store"; export function TimelineTrackContent({ track, @@ -80,7 +84,10 @@ export function TimelineTrackContent({ mouseX / (TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel) ); const adjustedTime = Math.max(0, mouseTime - dragState.clickOffsetTime); - const snappedTime = Math.round(adjustedTime * 10) / 10; + // Use frame snapping if project has FPS, otherwise use decimal snapping + const projectStore = useProjectStore.getState(); + const projectFps = projectStore.activeProject?.fps || 30; + const snappedTime = snapTimeToFrame(adjustedTime, projectFps); updateDragTime(snappedTime); }; @@ -342,7 +349,9 @@ export function TimelineTrackContent({ if (dragData.type === "text") { // Text elements have default duration of 5 seconds const newElementDuration = 5; - const snappedTime = Math.round(dropTime * 10) / 10; + const projectStore = useProjectStore.getState(); + const projectFps = projectStore.activeProject?.fps || 30; + const snappedTime = snapTimeToFrame(dropTime, projectFps); const newElementEnd = snappedTime + newElementDuration; wouldOverlap = track.elements.some((existingElement) => { @@ -361,7 +370,9 @@ export function TimelineTrackContent({ ); if (mediaItem) { const newElementDuration = mediaItem.duration || 5; - const snappedTime = Math.round(dropTime * 10) / 10; + const projectStore = useProjectStore.getState(); + const projectFps = projectStore.activeProject?.fps || 30; + const snappedTime = snapTimeToFrame(dropTime, projectFps); const newElementEnd = snappedTime + newElementDuration; wouldOverlap = track.elements.some((existingElement) => { @@ -401,7 +412,9 @@ export function TimelineTrackContent({ movingElement.duration - movingElement.trimStart - movingElement.trimEnd; - const snappedTime = Math.round(dropTime * 10) / 10; + const projectStore = useProjectStore.getState(); + const projectFps = projectStore.activeProject?.fps || 30; + const snappedTime = snapTimeToFrame(dropTime, projectFps); const movingElementEnd = snappedTime + movingElementDuration; wouldOverlap = track.elements.some((existingElement) => { @@ -428,13 +441,17 @@ export function TimelineTrackContent({ if (wouldOverlap) { e.dataTransfer.dropEffect = "none"; setWouldOverlap(true); - setDropPosition(Math.round(dropTime * 10) / 10); + const projectStore = useProjectStore.getState(); + const projectFps = projectStore.activeProject?.fps || 30; + setDropPosition(snapTimeToFrame(dropTime, projectFps)); return; } e.dataTransfer.dropEffect = hasTimelineElement ? "move" : "copy"; setWouldOverlap(false); - setDropPosition(Math.round(dropTime * 10) / 10); + const projectStore = useProjectStore.getState(); + const projectFps = projectStore.activeProject?.fps || 30; + setDropPosition(snapTimeToFrame(dropTime, projectFps)); }; const handleTrackDragEnter = (e: React.DragEvent) => { @@ -502,7 +519,9 @@ export function TimelineTrackContent({ const mouseY = e.clientY - rect.top; // Get Y position relative to this track const newStartTime = mouseX / (TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel); - const snappedTime = Math.round(newStartTime * 10) / 10; + const projectStore = useProjectStore.getState(); + const projectFps = projectStore.activeProject?.fps || 30; + const snappedTime = snapTimeToFrame(newStartTime, projectFps); // Calculate drop position relative to tracks const currentTrackIndex = tracks.findIndex((t) => t.id === track.id); @@ -548,7 +567,7 @@ export function TimelineTrackContent({ const adjustedStartTime = snappedTime - clickOffsetTime; const finalStartTime = Math.max( 0, - Math.round(adjustedStartTime * 10) / 10 + snapTimeToFrame(adjustedStartTime, projectFps) ); // Check for overlaps with existing elements (excluding the moving element itself) diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index b250bda1..a2b798e1 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -81,16 +81,8 @@ export function Timeline() { } = useTimelineStore(); const { mediaItems, addMediaItem } = useMediaStore(); const { activeProject } = useProjectStore(); - const { - currentTime, - duration, - seek, - setDuration, - isPlaying, - toggle, - setSpeed, - speed, - } = usePlaybackStore(); + const { currentTime, duration, seek, setDuration, isPlaying, toggle } = + usePlaybackStore(); const [isDragOver, setIsDragOver] = useState(false); const [isProcessing, setIsProcessing] = useState(false); const [progress, setProgress] = useState(0); @@ -217,7 +209,7 @@ export function Timeline() { scrollLeft = tracksContent.scrollLeft; } - const time = Math.max( + const rawTime = Math.max( 0, Math.min( duration, @@ -226,6 +218,11 @@ export function Timeline() { ) ); + // Use frame snapping for timeline clicking + const projectFps = activeProject?.fps || 30; + const { snapTimeToFrame } = require("@/constants/timeline-constants"); + const time = snapTimeToFrame(rawTime, projectFps); + seek(time); }, [ @@ -770,27 +767,6 @@ export function Timeline() { </TooltipTrigger> <TooltipContent>Delete element (Delete)</TooltipContent> </Tooltip> - <div className="w-px h-6 bg-border mx-1" /> - {/* Speed Control */} - <Tooltip> - <TooltipTrigger asChild> - <Select - value={speed.toFixed(1)} - onValueChange={(value) => setSpeed(parseFloat(value))} - > - <SelectTrigger className="w-[90px] h-8"> - <SelectValue placeholder="1.0x" /> - </SelectTrigger> - <SelectContent> - <SelectItem value="0.5">0.5x</SelectItem> - <SelectItem value="1.0">1.0x</SelectItem> - <SelectItem value="1.5">1.5x</SelectItem> - <SelectItem value="2.0">2.0x</SelectItem> - </SelectContent> - </Select> - </TooltipTrigger> - <TooltipContent>Playback Speed</TooltipContent> - </Tooltip> </TooltipProvider> </div> |
