summaryrefslogtreecommitdiff
path: root/apps/web/src/components
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2025-07-12 12:48:31 +0200
committerMaze Winther <mazewinther@gmail.com>2025-07-12 12:48:31 +0200
commit8545d95070890edfcf76b788ee023f9d510c302d (patch)
treeb3e1f9bf0bf30a8e748094b0a52c483befd7accf /apps/web/src/components
parent0726c27221c5b3aa9a8442b0cf89bd6acd8dfb5a (diff)
downloadopencut-8545d95070890edfcf76b788ee023f9d510c302d.tar.gz
refactor: add FPS and remove turbopack (yes, fuck you turbopack)
Diffstat (limited to 'apps/web/src/components')
-rw-r--r--apps/web/src/components/editor-header.tsx15
-rw-r--r--apps/web/src/components/editor/preview-panel.tsx13
-rw-r--r--apps/web/src/components/editor/properties-panel/index.tsx36
-rw-r--r--apps/web/src/components/editor/timeline-track.tsx37
-rw-r--r--apps/web/src/components/editor/timeline.tsx17
5 files changed, 94 insertions, 24 deletions
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 5003e79b..894b6257 100644
--- a/apps/web/src/components/editor/preview-panel.tsx
+++ b/apps/web/src/components/editor/preview-panel.tsx
@@ -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>
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 4fee16fc..a2b798e1 100644
--- a/apps/web/src/components/editor/timeline.tsx
+++ b/apps/web/src/components/editor/timeline.tsx
@@ -81,14 +81,8 @@ export function Timeline() {
} = useTimelineStore();
const { mediaItems, addMediaItem } = useMediaStore();
const { activeProject } = useProjectStore();
- const {
- currentTime,
- duration,
- seek,
- setDuration,
- isPlaying,
- toggle,
- } = usePlaybackStore();
+ const { currentTime, duration, seek, setDuration, isPlaying, toggle } =
+ usePlaybackStore();
const [isDragOver, setIsDragOver] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
const [progress, setProgress] = useState(0);
@@ -215,7 +209,7 @@ export function Timeline() {
scrollLeft = tracksContent.scrollLeft;
}
- const time = Math.max(
+ const rawTime = Math.max(
0,
Math.min(
duration,
@@ -224,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);
},
[