From fcfe612cd66c6c38c4ea27513dbcb9a70bd89c92 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 16 Jul 2025 17:36:47 +0200 Subject: fix: handlers component on mobile --- apps/web/src/components/landing/handlebars.tsx | 48 +++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 4 deletions(-) (limited to 'apps/web/src') diff --git a/apps/web/src/components/landing/handlebars.tsx b/apps/web/src/components/landing/handlebars.tsx index 36705e90..e2eb2573 100644 --- a/apps/web/src/components/landing/handlebars.tsx +++ b/apps/web/src/components/landing/handlebars.tsx @@ -19,6 +19,7 @@ export function Handlebars({ const [leftHandle, setLeftHandle] = useState(0); const [rightHandle, setRightHandle] = useState(maxWidth); const [contentWidth, setContentWidth] = useState(maxWidth); + const [isDragging, setIsDragging] = useState(false); const leftHandleX = useMotionValue(0); const rightHandleX = useMotionValue(maxWidth); @@ -33,6 +34,27 @@ export function Handlebars({ const containerRef = useRef(null); const measureRef = useRef(null); + // Prevent scroll when dragging on mobile + useEffect(() => { + const preventDefault = (e: TouchEvent) => { + if (isDragging) { + e.preventDefault(); + } + }; + + if (isDragging) { + document.addEventListener("touchmove", preventDefault, { + passive: false, + }); + document.body.style.overflow = "hidden"; + } + + return () => { + document.removeEventListener("touchmove", preventDefault); + document.body.style.overflow = ""; + }; + }, [isDragging]); + useEffect(() => { if (!measureRef.current) return; @@ -80,6 +102,14 @@ export function Handlebars({ setRightHandle(newRight); }; + const handleDragStart = () => { + setIsDragging(true); + }; + + const handleDragEnd = () => { + setIsDragging(false); + }; + return (
+ {/* Left Handle */}
+ {/* Right Handle */}
@@ -161,4 +201,4 @@ export function Handlebars({
); -}; +} -- cgit v1.3.1 From c1c4b114eb583489afdf1e49cf96197fbb10bca2 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 16 Jul 2025 17:38:29 +0200 Subject: fix: snapping for both directions (left/right) --- apps/web/src/components/editor/timeline-track.tsx | 47 ++++++++++++++++++++--- 1 file changed, 42 insertions(+), 5 deletions(-) (limited to 'apps/web/src') diff --git a/apps/web/src/components/editor/timeline-track.tsx b/apps/web/src/components/editor/timeline-track.tsx index 2be143eb..9b94ee6d 100644 --- a/apps/web/src/components/editor/timeline-track.tsx +++ b/apps/web/src/components/editor/timeline-track.tsx @@ -54,7 +54,7 @@ export function TimelineTrackContent({ const { currentTime } = usePlaybackStore(); // Initialize snapping hook - const { snapElementPosition } = useTimelineSnapping({ + const { snapElementPosition, snapElementEdge } = useTimelineSnapping({ snapThreshold: 10, enableElementSnapping: snappingEnabled, enablePlayheadSnapping: snappingEnabled, @@ -103,15 +103,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); -- cgit v1.3.1 From 218625b61a344e371688bbffc152ec3632615e46 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 16 Jul 2025 17:44:44 +0200 Subject: feat: advance snapping --- apps/web/src/components/editor/timeline-track.tsx | 119 +++++++++++++++++----- 1 file changed, 91 insertions(+), 28 deletions(-) (limited to 'apps/web/src') diff --git a/apps/web/src/components/editor/timeline-track.tsx b/apps/web/src/components/editor/timeline-track.tsx index 9b94ee6d..91b3ffa3 100644 --- a/apps/web/src/components/editor/timeline-track.tsx +++ b/apps/web/src/components/editor/timeline-track.tsx @@ -60,6 +60,53 @@ export function TimelineTrackContent({ 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(null); const [isDropping, setIsDropping] = useState(false); const [dropPosition, setDropPosition] = useState(null); @@ -427,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) => { @@ -448,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) => { @@ -490,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) => { @@ -519,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) => { @@ -651,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) => { @@ -748,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; @@ -759,7 +814,9 @@ export function TimelineTrackContent({ existingElement.trimEnd); // Check if elements overlap - return snappedTime < existingEnd && newElementEnd > existingStart; + return ( + textSnappedTime < existingEnd && newElementEnd > existingStart + ); }); if (hasOverlap) { @@ -774,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, @@ -894,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; @@ -905,7 +966,9 @@ export function TimelineTrackContent({ existingElement.trimEnd); // Check if elements overlap - return snappedTime < existingEnd && newElementEnd > existingStart; + return ( + mediaSnappedTime < existingEnd && newElementEnd > existingStart + ); }); if (hasOverlap) { @@ -920,7 +983,7 @@ export function TimelineTrackContent({ mediaId: mediaItem.id, name: mediaItem.name, duration: mediaItem.duration || 5, - startTime: snappedTime, + startTime: mediaSnappedTime, trimStart: 0, trimEnd: 0, }); -- cgit v1.3.1 From caf6fef3adb0425e3a89ca318136adc3e5f5d17c Mon Sep 17 00:00:00 2001 From: Sahil Sobhani Date: Wed, 16 Jul 2025 23:34:20 +0530 Subject: Redesigned the footer for mobile devices: UI Changes --- apps/web/.env.example | 15 --------------- apps/web/src/components/footer.tsx | 8 ++++---- 2 files changed, 4 insertions(+), 19 deletions(-) delete mode 100644 apps/web/.env.example (limited to 'apps/web/src') diff --git a/apps/web/.env.example b/apps/web/.env.example deleted file mode 100644 index d63df851..00000000 --- a/apps/web/.env.example +++ /dev/null @@ -1,15 +0,0 @@ -# Environment Variables Example -# Copy this file to .env.local and update the values as needed - -DATABASE_URL="postgresql://opencut:opencutthegoat@localhost:5432/opencut" - -# Better Auth -NEXT_PUBLIC_BETTER_AUTH_URL=http://localhost:3000 -BETTER_AUTH_SECRET=your-secret-key-here - -# Development Environment -NODE_ENV=development - -# Redis -UPSTASH_REDIS_REST_URL=http://localhost:8079 -UPSTASH_REDIS_REST_TOKEN=example_token diff --git a/apps/web/src/components/footer.tsx b/apps/web/src/components/footer.tsx index 81505d4a..731aa2f7 100644 --- a/apps/web/src/components/footer.tsx +++ b/apps/web/src/components/footer.tsx @@ -34,15 +34,15 @@ export function Footer() {
{/* Brand Section */}
-
+
OpenCut OpenCut
-

+

The open source video editor that gets the job done. Simple, powerful, and works on any platform.

-
+
-
+

Resources

    -- cgit v1.3.1 From 87dd275e573958af32412147378b73dcf988ed18 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 16 Jul 2025 21:05:28 +0200 Subject: fix: no more rounded corners in preview --- apps/web/src/components/editor/preview-panel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'apps/web/src') 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 ? (
    Date: Wed, 16 Jul 2025 22:39:33 +0200 Subject: style(footer): align everything to left --- apps/web/src/components/footer.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'apps/web/src') diff --git a/apps/web/src/components/footer.tsx b/apps/web/src/components/footer.tsx index 731aa2f7..5b7f2303 100644 --- a/apps/web/src/components/footer.tsx +++ b/apps/web/src/components/footer.tsx @@ -34,15 +34,15 @@ export function Footer() {
    {/* Brand Section */}
    -
    +
    OpenCut OpenCut
    -

    +

    The open source video editor that gets the job done. Simple, powerful, and works on any platform.

    -
    +
    -
    +

    Resources

      @@ -129,7 +129,7 @@ export function Footer() {
    {/* Bottom Section */} -
    +
    © 2025 OpenCut, All Rights Reserved
    -- cgit v1.3.1 From 7d1f99c20ca60933984fe08ef83601248c6e0765 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 16 Jul 2025 22:49:10 +0200 Subject: fix --- apps/web/src/app/projects/page.tsx | 12 ++---------- apps/web/src/stores/project-store.ts | 11 ----------- 2 files changed, 2 insertions(+), 21 deletions(-) (limited to 'apps/web/src') 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/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((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, -- cgit v1.3.1 From 36516b874f86daee8dea5b559de4fda2a4c602fa Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 16 Jul 2025 22:50:40 +0200 Subject: remove onboarding --- apps/web/src/app/editor/[project_id]/page.tsx | 6 ------ 1 file changed, 6 deletions(-) (limited to 'apps/web/src') 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() {
    - { - setIsOnboardingOpen(false); - }} - /> ); } -- cgit v1.3.1 From a3c84e4a199add132340fb4f0a5e74e9062d5d5f Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 16 Jul 2025 23:28:26 +0200 Subject: fix handlebars --- apps/web/src/components/landing/handlebars.tsx | 48 +++----------------------- 1 file changed, 4 insertions(+), 44 deletions(-) (limited to 'apps/web/src') diff --git a/apps/web/src/components/landing/handlebars.tsx b/apps/web/src/components/landing/handlebars.tsx index e2eb2573..36705e90 100644 --- a/apps/web/src/components/landing/handlebars.tsx +++ b/apps/web/src/components/landing/handlebars.tsx @@ -19,7 +19,6 @@ export function Handlebars({ const [leftHandle, setLeftHandle] = useState(0); const [rightHandle, setRightHandle] = useState(maxWidth); const [contentWidth, setContentWidth] = useState(maxWidth); - const [isDragging, setIsDragging] = useState(false); const leftHandleX = useMotionValue(0); const rightHandleX = useMotionValue(maxWidth); @@ -34,27 +33,6 @@ export function Handlebars({ const containerRef = useRef(null); const measureRef = useRef(null); - // Prevent scroll when dragging on mobile - useEffect(() => { - const preventDefault = (e: TouchEvent) => { - if (isDragging) { - e.preventDefault(); - } - }; - - if (isDragging) { - document.addEventListener("touchmove", preventDefault, { - passive: false, - }); - document.body.style.overflow = "hidden"; - } - - return () => { - document.removeEventListener("touchmove", preventDefault); - document.body.style.overflow = ""; - }; - }, [isDragging]); - useEffect(() => { if (!measureRef.current) return; @@ -102,14 +80,6 @@ export function Handlebars({ setRightHandle(newRight); }; - const handleDragStart = () => { - setIsDragging(true); - }; - - const handleDragEnd = () => { - setIsDragging(false); - }; - return (
    - {/* Left Handle */}
    - {/* Right Handle */}
    @@ -201,4 +161,4 @@ export function Handlebars({
    ); -} +}; -- cgit v1.3.1