diff options
| author | vadi25 <57663552+vadi25@users.noreply.github.com> | 2025-07-17 12:20:47 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-07-17 12:20:47 +0200 |
| commit | 9af8905c0c945766defb9b77990928e099a650bd (patch) | |
| tree | b8b217d71bf895ee93739e67dd49d1e713cc0be3 /apps/web/src | |
| parent | 651cfbfccbbfe55ce8f48ca9bce240eeb8d1728e (diff) | |
| parent | 7d4cbca1190edc12ff9ae17029613b77b08436e0 (diff) | |
| download | opencut-9af8905c0c945766defb9b77990928e099a650bd.tar.gz | |
Merge branch 'OpenCut-app:main' into main
Diffstat (limited to 'apps/web/src')
| -rw-r--r-- | apps/web/src/app/editor/[project_id]/page.tsx | 6 | ||||
| -rw-r--r-- | apps/web/src/app/projects/page.tsx | 12 | ||||
| -rw-r--r-- | apps/web/src/components/editor/preview-panel.tsx | 2 | ||||
| -rw-r--r-- | apps/web/src/components/editor/timeline-track.tsx | 166 | ||||
| -rw-r--r-- | apps/web/src/components/footer.tsx | 10 | ||||
| -rw-r--r-- | apps/web/src/stores/project-store.ts | 11 |
6 files changed, 141 insertions, 66 deletions
diff --git a/apps/web/src/app/editor/[project_id]/page.tsx b/apps/web/src/app/editor/[project_id]/page.tsx index 41bef8bd..aa5b7315 100644 --- a/apps/web/src/app/editor/[project_id]/page.tsx +++ b/apps/web/src/app/editor/[project_id]/page.tsx @@ -140,12 +140,6 @@ export default function Editor() { </ResizablePanelGroup> </div> </div> - <Onboarding - isOpen={isOnboardingOpen} - onClose={() => { - setIsOnboardingOpen(false); - }} - /> </EditorProvider> ); } diff --git a/apps/web/src/app/projects/page.tsx b/apps/web/src/app/projects/page.tsx index 9fcb025b..499b999a 100644 --- a/apps/web/src/app/projects/page.tsx +++ b/apps/web/src/app/projects/page.tsx @@ -1,8 +1,7 @@ -"use client" -import { redirect } from "next/navigation"; +"use client"; import Link from "next/link"; -import React, { useState } from "react"; +import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; @@ -29,15 +28,8 @@ import { useProjectStore } from "@/stores/project-store"; import { useRouter } from "next/navigation"; import { DeleteProjectDialog } from "@/components/delete-project-dialog"; import { RenameProjectDialog } from "@/components/rename-project-dialog"; -import { toast } from "sonner"; export default function ProjectsPage() { - - if (process.env.NODE_ENV !== "development") { - toast.error("You are not allowed to access this page"); - redirect("/"); - } - const { createNewProject, savedProjects, diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx index 894b6257..2ecee391 100644 --- a/apps/web/src/components/editor/preview-panel.tsx +++ b/apps/web/src/components/editor/preview-panel.tsx @@ -347,7 +347,7 @@ export function PreviewPanel() { {hasAnyElements ? ( <div ref={previewRef} - className="relative overflow-hidden rounded-sm border" + className="relative overflow-hidden border" style={{ width: previewDimensions.width, height: previewDimensions.height, diff --git a/apps/web/src/components/editor/timeline-track.tsx b/apps/web/src/components/editor/timeline-track.tsx index 2be143eb..91b3ffa3 100644 --- a/apps/web/src/components/editor/timeline-track.tsx +++ b/apps/web/src/components/editor/timeline-track.tsx @@ -54,12 +54,59 @@ export function TimelineTrackContent({ const { currentTime } = usePlaybackStore(); // Initialize snapping hook - const { snapElementPosition } = useTimelineSnapping({ + const { snapElementPosition, snapElementEdge } = useTimelineSnapping({ snapThreshold: 10, enableElementSnapping: snappingEnabled, enablePlayheadSnapping: snappingEnabled, }); + // Helper function for drop snapping that tries both edges + const getDropSnappedTime = ( + dropTime: number, + elementDuration: number, + excludeElementId?: string + ) => { + if (!snappingEnabled) { + // Use frame snapping if project has FPS, otherwise use decimal snapping + const projectStore = useProjectStore.getState(); + const projectFps = projectStore.activeProject?.fps || 30; + return snapTimeToFrame(dropTime, projectFps); + } + + // Try snapping both start and end edges for drops + const startSnapResult = snapElementEdge( + dropTime, + elementDuration, + tracks, + currentTime, + zoomLevel, + excludeElementId, + true // snap to start edge + ); + + const endSnapResult = snapElementEdge( + dropTime, + elementDuration, + tracks, + currentTime, + zoomLevel, + excludeElementId, + false // snap to end edge + ); + + // Choose the snap result with the smaller distance (closer snap) + let bestSnapResult = startSnapResult; + if ( + endSnapResult.snapPoint && + (!startSnapResult.snapPoint || + endSnapResult.snapDistance < startSnapResult.snapDistance) + ) { + bestSnapResult = endSnapResult; + } + + return bestSnapResult.snappedTime; + }; + const timelineRef = useRef<HTMLDivElement>(null); const [isDropping, setIsDropping] = useState(false); const [dropPosition, setDropPosition] = useState<number | null>(null); @@ -103,15 +150,52 @@ export function TimelineTrackContent({ let finalTime = adjustedTime; let snapPoint = null; if (snappingEnabled) { - const snapResult = snapElementPosition( + // Find the element being dragged to get its duration + let elementDuration = 5; // fallback duration + if (dragState.elementId && dragState.trackId) { + const sourceTrack = tracks.find((t) => t.id === dragState.trackId); + const element = sourceTrack?.elements.find( + (e) => e.id === dragState.elementId + ); + if (element) { + elementDuration = + element.duration - element.trimStart - element.trimEnd; + } + } + + // Try snapping both start and end edges + const startSnapResult = snapElementEdge( adjustedTime, + elementDuration, tracks, currentTime, zoomLevel, - dragState.elementId || undefined + dragState.elementId || undefined, + true // snap to start edge ); - finalTime = snapResult.snappedTime; - snapPoint = snapResult.snapPoint; + + const endSnapResult = snapElementEdge( + adjustedTime, + elementDuration, + tracks, + currentTime, + zoomLevel, + dragState.elementId || undefined, + false // snap to end edge + ); + + // Choose the snap result with the smaller distance (closer snap) + let bestSnapResult = startSnapResult; + if ( + endSnapResult.snapPoint && + (!startSnapResult.snapPoint || + endSnapResult.snapDistance < startSnapResult.snapDistance) + ) { + bestSnapResult = endSnapResult; + } + + finalTime = bestSnapResult.snappedTime; + snapPoint = bestSnapResult.snapPoint; // Notify parent component about snap point change onSnapPointChange?.(snapPoint); @@ -390,9 +474,10 @@ export function TimelineTrackContent({ if (dragData.type === "text") { // Text elements have default duration of 5 seconds const newElementDuration = 5; - const projectStore = useProjectStore.getState(); - const projectFps = projectStore.activeProject?.fps || 30; - const snappedTime = snapTimeToFrame(dropTime, projectFps); + const snappedTime = getDropSnappedTime( + dropTime, + newElementDuration + ); const newElementEnd = snappedTime + newElementDuration; wouldOverlap = track.elements.some((existingElement) => { @@ -411,9 +496,10 @@ export function TimelineTrackContent({ ); if (mediaItem) { const newElementDuration = mediaItem.duration || 5; - const projectStore = useProjectStore.getState(); - const projectFps = projectStore.activeProject?.fps || 30; - const snappedTime = snapTimeToFrame(dropTime, projectFps); + const snappedTime = getDropSnappedTime( + dropTime, + newElementDuration + ); const newElementEnd = snappedTime + newElementDuration; wouldOverlap = track.elements.some((existingElement) => { @@ -453,9 +539,11 @@ export function TimelineTrackContent({ movingElement.duration - movingElement.trimStart - movingElement.trimEnd; - const projectStore = useProjectStore.getState(); - const projectFps = projectStore.activeProject?.fps || 30; - const snappedTime = snapTimeToFrame(dropTime, projectFps); + const snappedTime = getDropSnappedTime( + dropTime, + movingElementDuration, + elementId + ); const movingElementEnd = snappedTime + movingElementDuration; wouldOverlap = track.elements.some((existingElement) => { @@ -482,17 +570,15 @@ export function TimelineTrackContent({ if (wouldOverlap) { e.dataTransfer.dropEffect = "none"; setWouldOverlap(true); - const projectStore = useProjectStore.getState(); - const projectFps = projectStore.activeProject?.fps || 30; - setDropPosition(snapTimeToFrame(dropTime, projectFps)); + // Use default duration for position indicator + setDropPosition(getDropSnappedTime(dropTime, 5)); return; } e.dataTransfer.dropEffect = hasTimelineElement ? "move" : "copy"; setWouldOverlap(false); - const projectStore = useProjectStore.getState(); - const projectFps = projectStore.activeProject?.fps || 30; - setDropPosition(snapTimeToFrame(dropTime, projectFps)); + // Use default duration for position indicator + setDropPosition(getDropSnappedTime(dropTime, 5)); }; const handleTrackDragEnter = (e: React.DragEvent) => { @@ -614,18 +700,20 @@ export function TimelineTrackContent({ return; } - // Adjust position based on where user clicked on the element - const adjustedStartTime = snappedTime - clickOffsetTime; - const finalStartTime = Math.max( - 0, - snapTimeToFrame(adjustedStartTime, projectFps) - ); - // Check for overlaps with existing elements (excluding the moving element itself) const movingElementDuration = movingElement.duration - movingElement.trimStart - movingElement.trimEnd; + + // Adjust position based on where user clicked on the element + const adjustedStartTime = newStartTime - clickOffsetTime; + const snappedStartTime = getDropSnappedTime( + adjustedStartTime, + movingElementDuration, + elementId + ); + const finalStartTime = Math.max(0, snappedStartTime); const movingElementEnd = finalStartTime + movingElementDuration; const hasOverlap = track.elements.some((existingElement) => { @@ -711,7 +799,11 @@ export function TimelineTrackContent({ // Check for overlaps with existing elements in target track const newElementDuration = 5; // Default text duration - const newElementEnd = snappedTime + newElementDuration; + const textSnappedTime = getDropSnappedTime( + newStartTime, + newElementDuration + ); + const newElementEnd = textSnappedTime + newElementDuration; const hasOverlap = targetTrack.elements.some((existingElement) => { const existingStart = existingElement.startTime; @@ -722,7 +814,9 @@ export function TimelineTrackContent({ existingElement.trimEnd); // Check if elements overlap - return snappedTime < existingEnd && newElementEnd > existingStart; + return ( + textSnappedTime < existingEnd && newElementEnd > existingStart + ); }); if (hasOverlap) { @@ -737,7 +831,7 @@ export function TimelineTrackContent({ name: dragData.name || "Text", content: dragData.content || "Default Text", duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION, - startTime: snappedTime, + startTime: textSnappedTime, trimStart: 0, trimEnd: 0, fontSize: 48, @@ -857,7 +951,11 @@ export function TimelineTrackContent({ // Check for overlaps with existing elements in target track const newElementDuration = mediaItem.duration || 5; - const newElementEnd = snappedTime + newElementDuration; + const mediaSnappedTime = getDropSnappedTime( + newStartTime, + newElementDuration + ); + const newElementEnd = mediaSnappedTime + newElementDuration; const hasOverlap = targetTrack.elements.some((existingElement) => { const existingStart = existingElement.startTime; @@ -868,7 +966,9 @@ export function TimelineTrackContent({ existingElement.trimEnd); // Check if elements overlap - return snappedTime < existingEnd && newElementEnd > existingStart; + return ( + mediaSnappedTime < existingEnd && newElementEnd > existingStart + ); }); if (hasOverlap) { @@ -883,7 +983,7 @@ export function TimelineTrackContent({ mediaId: mediaItem.id, name: mediaItem.name, duration: mediaItem.duration || 5, - startTime: snappedTime, + startTime: mediaSnappedTime, trimStart: 0, trimEnd: 0, }); diff --git a/apps/web/src/components/footer.tsx b/apps/web/src/components/footer.tsx index 81505d4a..5b7f2303 100644 --- a/apps/web/src/components/footer.tsx +++ b/apps/web/src/components/footer.tsx @@ -34,15 +34,15 @@ export function Footer() { <div className="grid grid-cols-1 md:grid-cols-2 gap-12 mb-8"> {/* Brand Section */} <div className="md:col-span-1 max-w-sm"> - <div className="flex items-center gap-2 mb-4"> + <div className="flex justify-start items-center gap-2 mb-4"> <Image src="/logo.svg" alt="OpenCut" width={24} height={24} /> <span className="font-bold text-lg">OpenCut</span> </div> - <p className="text-sm text-muted-foreground mb-5"> + <p className="text-sm md:text-left text-muted-foreground mb-5"> The open source video editor that gets the job done. Simple, powerful, and works on any platform. </p> - <div className="flex gap-3"> + <div className="flex justify-start gap-3"> <Link href="https://github.com/OpenCut-app/OpenCut" className="text-muted-foreground hover:text-foreground transition-colors" @@ -70,7 +70,7 @@ export function Footer() { </div> </div> - <div className="flex gap-12 justify-end items-start py-2"> + <div className="flex gap-12 justify-start items-start py-2"> <div> <h3 className="font-semibold text-foreground mb-4">Resources</h3> <ul className="space-y-2 text-sm"> @@ -129,7 +129,7 @@ export function Footer() { </div> {/* Bottom Section */} - <div className="pt-2 flex flex-col md:flex-row justify-between items-center gap-4"> + <div className="pt-2 flex flex-col md:flex-row justify-between items-start gap-4"> <div className="flex items-center gap-4 text-sm text-muted-foreground"> <span>© 2025 OpenCut, All Rights Reserved</span> </div> diff --git a/apps/web/src/stores/project-store.ts b/apps/web/src/stores/project-store.ts index 5017be2d..efb9cc71 100644 --- a/apps/web/src/stores/project-store.ts +++ b/apps/web/src/stores/project-store.ts @@ -5,7 +5,6 @@ import { toast } from "sonner"; import { useMediaStore } from "./media-store";
import { useTimelineStore } from "./timeline-store";
import { generateUUID } from "@/lib/utils";
-import { env } from "@/env";
interface ProjectStore {
activeProject: TProject | null;
@@ -37,16 +36,6 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({ isInitialized: false,
createNewProject: async (name: string) => {
- try {
- if (process.env.NODE_ENV !== "development") {
- toast.error("Project creation is disabled outside development environment");
- throw new Error("Not allowed in production");
- }
- } catch (error) {
- toast.error("Failed to create new project");
- throw error;
- }
-
const newProject: TProject = {
id: generateUUID(),
name,
|
