From d605cd762cc3c784d908d36a12605d7d3547f1f1 Mon Sep 17 00:00:00 2001 From: aashishparuvada Date: Mon, 23 Jun 2025 22:49:56 +0530 Subject: [PATCH 01/22] hotfix:fixed-cmd-for-deselection --- apps/web/src/components/editor/timeline.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index 675b398..b95f983 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -1409,9 +1409,18 @@ function TimelineTrackContent({ style={{ width: `${clipWidth}px`, left: `${clipLeft}px` }} onClick={(e) => { e.stopPropagation(); + const isSelected = selectedClips.some( + (c) => c.trackId === track.id && c.clipId === clip.id + ); + if (e.metaKey || e.ctrlKey || e.shiftKey) { + // Multi-selection mode: toggle the clip selectClip(track.id, clip.id, true); + } else if (isSelected) { + // If clip is already selected, deselect it + deselectClip(track.id, clip.id); } else { + // If clip is not selected, select it (replacing other selections) selectClip(track.id, clip.id, false); } }} From 4a9b102ce1d6824167cdb57f514b66d115bf016e Mon Sep 17 00:00:00 2001 From: aashishparuvada Date: Mon, 23 Jun 2025 23:26:41 +0530 Subject: [PATCH 02/22] feature:deselect-clips-on-click --- apps/web/src/components/editor/timeline.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index b95f983..d6a6d62 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -299,9 +299,13 @@ export function Timeline() { }; const handleWheel = (e: React.WheelEvent) => { - e.preventDefault(); - const delta = e.deltaY > 0 ? -0.05 : 0.05; - setZoomLevel((prev) => Math.max(0.1, Math.min(10, prev + delta))); + // Only zoom if user is using pinch gesture (ctrlKey or metaKey is true) + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + const delta = e.deltaY > 0 ? -0.05 : 0.05; + setZoomLevel((prev) => Math.max(0.1, Math.min(10, prev + delta))); + } + // Otherwise, allow normal scrolling }; const dragProps = { @@ -620,8 +624,7 @@ export function Timeline() { className="relative cursor-pointer select-none" style={{ width: `${Math.max(1000, duration * 50 * zoomLevel)}px`, - minHeight: - tracks.length > 0 ? `${tracks.length * 60}px` : "200px", + minHeight: '600px', // Always at least 600px tall for easy empty area clicking }} onClick={handleTimelineAreaClick} onMouseDown={handleTimelineMouseDown} From f17d848b170361970d118074ded3553d6910f939 Mon Sep 17 00:00:00 2001 From: aashishparuvada Date: Mon, 23 Jun 2025 23:48:15 +0530 Subject: [PATCH 03/22] feat:assigned-actions-for-selected-clips --- apps/web/src/components/editor/timeline.tsx | 101 ++++++++++++++++++-- 1 file changed, 93 insertions(+), 8 deletions(-) diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index d6a6d62..967951e 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -40,7 +40,7 @@ export function Timeline() { // Timeline shows all tracks (video, audio, effects) and their clips. // You can drag media here to add it to your project. // Clips can be trimmed, deleted, and moved. - const { tracks, addTrack, addClipToTrack, removeTrack, toggleTrackMute, removeClipFromTrack, moveClipToTrack, getTotalDuration, selectedClips, selectClip, deselectClip, clearSelectedClips, setSelectedClips } = + const { tracks, addTrack, addClipToTrack, removeTrack, toggleTrackMute, removeClipFromTrack, moveClipToTrack, getTotalDuration, selectedClips, selectClip, deselectClip, clearSelectedClips, setSelectedClips, updateClipTrim } = useTimelineStore(); const { mediaItems, addMediaItem } = useMediaStore(); const { currentTime, duration, seek, setDuration, isPlaying, play, pause, toggle, setSpeed, speed } = usePlaybackStore(); @@ -315,6 +315,92 @@ export function Timeline() { onDrop: handleDrop, }; + // Action handlers for toolbar + const handleSplitSelected = () => { + if (selectedClips.length === 0) { + toast.error("No clips selected"); + return; + } + selectedClips.forEach(({ trackId, clipId }) => { + const track = tracks.find(t => t.id === trackId); + const clip = track?.clips.find(c => c.id === clipId); + if (clip && track) { + const splitTime = currentTime; + const effectiveStart = clip.startTime; + const effectiveEnd = clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); + if (splitTime > effectiveStart && splitTime < effectiveEnd) { + updateClipTrim(track.id, clip.id, clip.trimStart, clip.trimEnd + (effectiveEnd - splitTime)); + addClipToTrack(track.id, { + mediaId: clip.mediaId, + name: clip.name + " (split)", + duration: clip.duration, + startTime: splitTime, + trimStart: clip.trimStart + (splitTime - effectiveStart), + trimEnd: clip.trimEnd, + }); + } + } + }); + toast.success("Split selected clip(s)"); + }; + + const handleDuplicateSelected = () => { + if (selectedClips.length === 0) { + toast.error("No clips selected"); + return; + } + selectedClips.forEach(({ trackId, clipId }) => { + const track = tracks.find(t => t.id === trackId); + const clip = track?.clips.find(c => c.id === clipId); + if (clip && track) { + addClipToTrack(track.id, { + mediaId: clip.mediaId, + name: clip.name + " (copy)", + duration: clip.duration, + startTime: clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd) + 0.1, + trimStart: clip.trimStart, + trimEnd: clip.trimEnd, + }); + } + }); + toast.success("Duplicated selected clip(s)"); + }; + + const handleFreezeSelected = () => { + if (selectedClips.length === 0) { + toast.error("No clips selected"); + return; + } + selectedClips.forEach(({ trackId, clipId }) => { + const track = tracks.find(t => t.id === trackId); + const clip = track?.clips.find(c => c.id === clipId); + if (clip && track) { + // Add a new freeze frame clip at the playhead + addClipToTrack(track.id, { + mediaId: clip.mediaId, + name: clip.name + " (freeze)", + duration: 1, // 1 second freeze frame + startTime: currentTime, + trimStart: 0, + trimEnd: clip.duration - 1, + }); + } + }); + toast.success("Freeze frame added for selected clip(s)"); + }; + + const handleDeleteSelected = () => { + if (selectedClips.length === 0) { + toast.error("No clips selected"); + return; + } + selectedClips.forEach(({ trackId, clipId }) => { + removeClipFromTrack(trackId, clipId); + }); + clearSelectedClips(); + toast.success("Deleted selected clip(s)"); + }; + return (
- @@ -423,7 +509,7 @@ export function Timeline() { - @@ -432,7 +518,7 @@ export function Timeline() { - @@ -441,7 +527,7 @@ export function Timeline() { - @@ -748,7 +834,7 @@ export function Timeline() { const effectiveEnd = clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); if (splitTime > effectiveStart && splitTime < effectiveEnd) { - useTimelineStore.getState().updateClipTrim( + updateClipTrim( track.id, clip.id, clip.trimStart, @@ -1496,8 +1582,7 @@ function TimelineTrackContent({ {/* Drop position indicator */} {isDraggedOver && dropPosition !== null && (
Date: Mon, 23 Jun 2025 23:53:18 +0530 Subject: [PATCH 04/22] hotfix:disabled-vertical-scrolling-in-timeline --- apps/web/src/components/editor/timeline.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index 967951e..fb94b37 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -703,14 +703,14 @@ export function Timeline() {
{/* Timeline Tracks Content */} -
- +
+
+ {/* Timeline grid and clips area (with left margin for sidebar) */}
-
))} @@ -768,7 +767,7 @@ export function Timeline() { )}
- +
From f959ba87e20aaae42bc270edc127ae89b97241cb Mon Sep 17 00:00:00 2001 From: 3raphat <96657413+3raphat@users.noreply.github.com> Date: Tue, 24 Jun 2025 01:38:53 +0700 Subject: [PATCH 05/22] fix: update auth routes --- apps/web/src/app/(auth)/login/page.tsx | 26 +++++++++++++---- apps/web/src/app/(auth)/signup/page.tsx | 38 +++++++++++++++++++------ apps/web/src/components/header.tsx | 2 +- apps/web/src/middleware.ts | 2 +- 4 files changed, 51 insertions(+), 17 deletions(-) diff --git a/apps/web/src/app/(auth)/login/page.tsx b/apps/web/src/app/(auth)/login/page.tsx index ebf2eb3..5f37ced 100644 --- a/apps/web/src/app/(auth)/login/page.tsx +++ b/apps/web/src/app/(auth)/login/page.tsx @@ -77,14 +77,21 @@ function LoginForm() { size="lg" disabled={isAnyLoading} > - {isGoogleLoading ? : ()} Continue with Google + {isGoogleLoading ? ( + + ) : ( + + )}{" "} + Continue with Google
- Or continue with + + Or continue with +
@@ -144,14 +151,21 @@ export default function LoginPage() { - - -
}> + + + + } + >
Don't have an account?{" "} - + Sign up
diff --git a/apps/web/src/app/(auth)/signup/page.tsx b/apps/web/src/app/(auth)/signup/page.tsx index 3d125a0..d1feece 100644 --- a/apps/web/src/app/(auth)/signup/page.tsx +++ b/apps/web/src/app/(auth)/signup/page.tsx @@ -44,7 +44,7 @@ function SignUpForm() { return; } - router.push("/auth/login"); + router.push("/login"); }; const handleGoogleSignUp = async () => { @@ -80,7 +80,12 @@ function SignUpForm() { size="lg" disabled={isAnyLoading} > - {isGoogleLoading ? : ()} Continue with Google + {isGoogleLoading ? ( + + ) : ( + + )}{" "} + Continue with Google
@@ -88,7 +93,9 @@ function SignUpForm() {
- Or continue with + + Or continue with +
@@ -135,7 +142,11 @@ function SignUpForm() { className="w-full h-11" size="lg" > - {isEmailLoading ? : "Create account"} + {isEmailLoading ? ( + + ) : ( + "Create account" + )} @@ -157,20 +168,29 @@ export default function SignUpPage() { - Create your account + + Create your account + Get started with your free account today - - - }> + + + + } + >
Already have an account?{" "} - + Sign in
diff --git a/apps/web/src/components/header.tsx b/apps/web/src/components/header.tsx index 109d022..2ccc53f 100644 --- a/apps/web/src/components/header.tsx +++ b/apps/web/src/components/header.tsx @@ -23,7 +23,7 @@ export function Header() { GitHub - + - + {/* */} + - {/* */} - + diff --git a/apps/web/src/components/landing/hero.tsx b/apps/web/src/components/landing/hero.tsx index f2c70f5..b232003 100644 --- a/apps/web/src/components/landing/hero.tsx +++ b/apps/web/src/components/landing/hero.tsx @@ -7,6 +7,7 @@ import { ArrowRight } from "lucide-react"; import Link from "next/link"; import { useState } from "react"; import { useToast } from "@/hooks/use-toast"; +import { ghStars } from "@/lib/fetchGhStars"; interface HeroProps { signupCount: number; @@ -16,6 +17,7 @@ export function Hero({ signupCount }: HeroProps) { const [email, setEmail] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const { toast } = useToast(); + const stars = ghStars(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -152,7 +154,7 @@ export function Hero({ signupCount }: HeroProps) { href="https://github.com/OpenCut-app/OpenCut" className="text-foreground underline" > - GitHub + GitHub {stars}+ diff --git a/apps/web/src/lib/fetchGhStars.ts b/apps/web/src/lib/fetchGhStars.ts new file mode 100644 index 0000000..de3e2e8 --- /dev/null +++ b/apps/web/src/lib/fetchGhStars.ts @@ -0,0 +1,20 @@ +async function getStars(): Promise { + const res = await fetch("https://api.github.com/repos/OpenCut-app/OpenCut", { + // Cache for 1 hour (3600 seconds) + next: { revalidate: 3600 }, + }); + + const data = await res.json(); + const count = data.stargazers_count; + + if (count >= 1_000_000) + return (count / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M"; + if (count >= 1_000) + return (count / 1_000).toFixed(1).replace(/\.0$/, "") + "k"; + return count.toString(); +} + +export async function ghStars() { + const stars = await getStars(); + return stars; +} From 57e42e2c4539949e93b71f1125e0c0eecb2fb5e8 Mon Sep 17 00:00:00 2001 From: Dipanshu Rawat <144578298+Jaydeeprawat17@users.noreply.github.com> Date: Tue, 24 Jun 2025 02:06:55 +0530 Subject: [PATCH 14/22] ref: fetch api function --- apps/web/src/components/header.tsx | 21 ++++++++++-- apps/web/src/components/landing/hero.tsx | 21 +++++++++--- apps/web/src/lib/fetchGhStars.ts | 43 ++++++++++++++---------- 3 files changed, 61 insertions(+), 24 deletions(-) diff --git a/apps/web/src/components/header.tsx b/apps/web/src/components/header.tsx index cada527..82e0bc5 100644 --- a/apps/web/src/components/header.tsx +++ b/apps/web/src/components/header.tsx @@ -6,12 +6,27 @@ import { Button } from "./ui/button"; import { ArrowRight } from "lucide-react"; import { HeaderBase } from "./header-base"; import { useSession } from "@/lib/auth-client"; -import { ghStars } from "@/lib/fetchGhStars"; +import { getStars } from "@/lib/fetchGhStars"; import { Star } from "lucide-react"; +import { useEffect, useState } from "react"; export function Header() { - const stars = ghStars(); const { data: session } = useSession(); + const [star, setStar] = useState(""); + + useEffect(() => { + const fetchStars = async () => { + try { + const data = await getStars(); + setStar(data); + } catch (err) { + console.error("Failed to fetch GitHub stars", err); + } + }; + + fetchStars(); + }, []); + const leftContent = ( OpenCut Logo @@ -28,7 +43,7 @@ export function Header() { > GitHub - {stars}+ + {star}+ diff --git a/apps/web/src/components/landing/hero.tsx b/apps/web/src/components/landing/hero.tsx index b232003..5d9f3f1 100644 --- a/apps/web/src/components/landing/hero.tsx +++ b/apps/web/src/components/landing/hero.tsx @@ -5,19 +5,32 @@ import { Button } from "../ui/button"; import { Input } from "../ui/input"; import { ArrowRight } from "lucide-react"; import Link from "next/link"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useToast } from "@/hooks/use-toast"; -import { ghStars } from "@/lib/fetchGhStars"; +import { getStars } from "@/lib/fetchGhStars"; interface HeroProps { signupCount: number; } export function Hero({ signupCount }: HeroProps) { + const [star, setStar] = useState(); const [email, setEmail] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const { toast } = useToast(); - const stars = ghStars(); + + useEffect(() => { + const fetchStars = async () => { + try { + const data = await getStars(); + setStar(data); + } catch (err) { + console.error("Failed to fetch GitHub stars", err); + } + }; + + fetchStars(); + }, []); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -154,7 +167,7 @@ export function Hero({ signupCount }: HeroProps) { href="https://github.com/OpenCut-app/OpenCut" className="text-foreground underline" > - GitHub {stars}+ + GitHub {star}+ diff --git a/apps/web/src/lib/fetchGhStars.ts b/apps/web/src/lib/fetchGhStars.ts index de3e2e8..e023684 100644 --- a/apps/web/src/lib/fetchGhStars.ts +++ b/apps/web/src/lib/fetchGhStars.ts @@ -1,20 +1,29 @@ -async function getStars(): Promise { - const res = await fetch("https://api.github.com/repos/OpenCut-app/OpenCut", { - // Cache for 1 hour (3600 seconds) - next: { revalidate: 3600 }, - }); +export async function getStars(): Promise { + try { + const res = await fetch( + "https://api.github.com/repos/OpenCut-app/OpenCut", + { + next: { revalidate: 3600 }, + } + ); - const data = await res.json(); - const count = data.stargazers_count; + if (!res.ok) { + throw new Error(`GitHub API error: ${res.status} ${res.statusText}`); + } + const data = await res.json(); + const count = data.stargazers_count; - if (count >= 1_000_000) - return (count / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M"; - if (count >= 1_000) - return (count / 1_000).toFixed(1).replace(/\.0$/, "") + "k"; - return count.toString(); -} - -export async function ghStars() { - const stars = await getStars(); - return stars; + if (typeof count !== "number") { + throw new Error("Invalid stargazers_count from GitHub API"); + } + + if (count >= 1_000_000) + return (count / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M"; + if (count >= 1_000) + return (count / 1_000).toFixed(1).replace(/\.0$/, "") + "k"; + return count.toString(); + } catch (error) { + console.error("Failed to fetch GitHub stars:", error); + return "1.5k"; + } } From a3e6585306f6cf3cb4914cb5bd4a31396f22e25f Mon Sep 17 00:00:00 2001 From: babs <41580129+anagobabatunde@users.noreply.github.com> Date: Mon, 23 Jun 2025 20:59:31 +0000 Subject: [PATCH 15/22] docs: expand setup instructions with prerequisites and database setup steps --- README.md | 80 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 3ccb2b0..af2dbac 100644 --- a/README.md +++ b/README.md @@ -26,26 +26,66 @@ A free, open-source video editor for web, desktop, and mobile. ## Getting Started -1. **Clone the repository:** - ```bash - git clone - cd OpenCut - ``` -2. **Install dependencies:** - ```bash - cd apps/web - npm install - # or, with Bun - bun install - ``` -3. **Run the development server:** - ```bash - npm run dev - # or, with Bun - bun run dev - ``` -4. **Open in browser:** - Visit [http://localhost:3000](http://localhost:3000) +### Prerequisites + +Before you begin, ensure you have the following installed on your system: + +- [Bun](https://bun.sh/docs/installation) +- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/) +- [Node.js](https://nodejs.org/en/) (for `npm` alternative) + +### Setup + +1. **Clone the repository** + ```bash + git clone + cd OpenCut + ``` + +2. **Start backend services** + From the project root, start the PostgreSQL and Redis services: + ```bash + docker-compose up -d + ``` + +3. **Set up environment variables** + Navigate into the web app's directory and create a `.env` file from the example: + ```bash + cd apps/web + cp .env.example .env + ``` + *The default values in the `.env` file should work for local development.* + +4. **Install dependencies** + Install the project dependencies using `bun` (recommended) or `npm`. + ```bash + # With bun + bun install + + # Or with npm + npm install + ``` + +5. **Run database migrations** + Apply the database schema to your local database: + ```bash + # With bun + bun run db:push:local + + # Or with npm + npm run db:push:local + ``` + +6. **Start the development server** + ```bash + # With bun + bun run dev + + # Or with npm + npm run dev + ``` + +The application will be available at [http://localhost:3000](http://localhost:3000). ## Contributing From b772c6f68e81c4c88186219dc841a14ab278cf50 Mon Sep 17 00:00:00 2001 From: YaoSiQian <2229561981@qq.com> Date: Tue, 24 Jun 2025 05:00:34 +0800 Subject: [PATCH 16/22] fix(docker): web service --- README.md | 14 ++++++++++++++ apps/web/Dockerfile | 36 ++++++++++++++++++++++++++++++++++++ docker-compose.yaml | 41 ++++++++++++++++++++++++++++++++++++++--- 3 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 apps/web/Dockerfile diff --git a/README.md b/README.md index 3ccb2b0..998acd3 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,20 @@ A free, open-source video editor for web, desktop, and mobile. 4. **Open in browser:** Visit [http://localhost:3000](http://localhost:3000) +## Run with Docker +1. **Prepare environment variables:** + Edit [docker-compose.yaml](https://github.com/OpenCut-app/OpenCut/blob/main/docker-compose.yaml#L57-L64) +2. **Build and run:** + ```bash + docker-compose up -d --build + ``` +3. *(Optional)* **Migrate database:** + ```bash + docker-compose exec web bun run db:migrate + ``` +4. **Open in browser:** + Visit [http://localhost:3000](http://localhost:3000) + ## Contributing ## License diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile new file mode 100644 index 0000000..ca1f098 --- /dev/null +++ b/apps/web/Dockerfile @@ -0,0 +1,36 @@ +FROM oven/bun:latest AS base + +# Install dependencies +FROM base AS deps +WORKDIR /app +COPY package.json bun.lock ./ +RUN bun install --frozen-lockfile + +# Build the application +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN bun run build + +# Production image +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV=production + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +CMD ["bun", "server.js"] \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index c1c61a4..c654926 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -18,7 +18,8 @@ services: start_period: 10s redis: - image: redis + image: redis:7-alpine + restart: unless-stopped ports: - "6379:6379" healthcheck: @@ -36,12 +37,46 @@ services: SRH_MODE: env SRH_TOKEN: example_token SRH_CONNECTION_STRING: "redis://redis:6379" + depends_on: + redis: + condition: service_healthy healthcheck: test: ["CMD-SHELL", "wget --spider -q http://127.0.0.1:80 || exit 1"] interval: 30s timeout: 10s retries: 5 start_period: 10s - -volumes: + web: + build: + context: ./apps/web + dockerfile: ./apps/web/Dockerfile + restart: unless-stopped + ports: + - "3000:3000" + environment: + - NODE_ENV=production + - DATABASE_URL=postgresql://opencut:opencutthegoat@db:5432/opencut + - BETTER_AUTH_URL=http://localhost:3000 + - BETTER_AUTH_SECRET=your-production-secret-key-here + - UPSTASH_REDIS_REST_URL=http://serverless-redis-http:80 + - UPSTASH_REDIS_REST_TOKEN=example_token + - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID} + - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET} + depends_on: + db: + condition: service_healthy + serverless-redis-http: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 30s + +volumes: postgres_data: + +networks: + default: + name: opencut-network From 79ac2c28238642436bc57a55671731460ccf2640 Mon Sep 17 00:00:00 2001 From: YaoSiQian Date: Tue, 24 Jun 2025 05:06:21 +0800 Subject: [PATCH 17/22] style(README): hot fix at line break --- README.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 998acd3..1c377b6 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,13 @@ A free, open-source video editor for web, desktop, and mobile. ## Getting Started 1. **Clone the repository:** + ```bash git clone cd OpenCut ``` 2. **Install dependencies:** + ```bash cd apps/web npm install @@ -39,30 +41,37 @@ A free, open-source video editor for web, desktop, and mobile. bun install ``` 3. **Run the development server:** + ```bash npm run dev # or, with Bun bun run dev ``` -4. **Open in browser:** +4. **Open in browser:** + Visit [http://localhost:3000](http://localhost:3000) ## Run with Docker -1. **Prepare environment variables:** +1. **Prepare environment variables:** + Edit [docker-compose.yaml](https://github.com/OpenCut-app/OpenCut/blob/main/docker-compose.yaml#L57-L64) 2. **Build and run:** + ```bash docker-compose up -d --build ``` 3. *(Optional)* **Migrate database:** + ```bash docker-compose exec web bun run db:migrate ``` 4. **Open in browser:** + Visit [http://localhost:3000](http://localhost:3000) ## Contributing +Visit [CONTRIBUTING.md](.github/CONTRIBUTING.md) ## License -MIT [Details](LICENSE) +[MIT LICENSE](LICENSE) From bc4c064ad6a77d62ae19cfcb37258ddfdfd6420d Mon Sep 17 00:00:00 2001 From: yassinehaimouch Date: Mon, 23 Jun 2025 23:01:50 +0100 Subject: [PATCH 18/22] fix: hero landing page height --- apps/web/src/components/landing/hero.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/landing/hero.tsx b/apps/web/src/components/landing/hero.tsx index f2c70f5..6b6cc06 100644 --- a/apps/web/src/components/landing/hero.tsx +++ b/apps/web/src/components/landing/hero.tsx @@ -67,7 +67,7 @@ export function Hero({ signupCount }: HeroProps) { }; return ( -
+
Date: Tue, 24 Jun 2025 09:26:31 +0530 Subject: [PATCH 19/22] feat:implemented-undo-feature-for-timeline --- apps/web/src/components/editor/timeline.tsx | 14 +++- apps/web/src/stores/timeline-store.ts | 74 +++++++++++++++------ 2 files changed, 65 insertions(+), 23 deletions(-) diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index fb94b37..484e1a6 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -40,7 +40,7 @@ export function Timeline() { // Timeline shows all tracks (video, audio, effects) and their clips. // You can drag media here to add it to your project. // Clips can be trimmed, deleted, and moved. - const { tracks, addTrack, addClipToTrack, removeTrack, toggleTrackMute, removeClipFromTrack, moveClipToTrack, getTotalDuration, selectedClips, selectClip, deselectClip, clearSelectedClips, setSelectedClips, updateClipTrim } = + const { tracks, addTrack, addClipToTrack, removeTrack, toggleTrackMute, removeClipFromTrack, moveClipToTrack, getTotalDuration, selectedClips, selectClip, deselectClip, clearSelectedClips, setSelectedClips, updateClipTrim, undo } = useTimelineStore(); const { mediaItems, addMediaItem } = useMediaStore(); const { currentTime, duration, seek, setDuration, isPlaying, play, pause, toggle, setSpeed, speed } = usePlaybackStore(); @@ -98,6 +98,18 @@ export function Timeline() { return () => window.removeEventListener("keydown", handleKeyDown); }, [selectedClips, removeClipFromTrack, clearSelectedClips]); + // Keyboard event for undo (Cmd+Z) + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === "z" && !e.shiftKey) { + e.preventDefault(); + undo(); + } + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [undo]); + // Mouse down on timeline background to start marquee const handleTimelineMouseDown = (e: React.MouseEvent) => { if (e.target === e.currentTarget && e.button === 0) { diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts index 4b55a53..a369d57 100644 --- a/apps/web/src/stores/timeline-store.ts +++ b/apps/web/src/stores/timeline-store.ts @@ -20,6 +20,7 @@ export interface TimelineTrack { interface TimelineStore { tracks: TimelineTrack[]; + history: TimelineTrack[][]; // Multi-selection selectedClips: { trackId: string; clipId: string }[]; @@ -53,12 +54,30 @@ interface TimelineStore { // Computed values getTotalDuration: () => number; + + // New actions + undo: () => void; + pushHistory: () => void; } export const useTimelineStore = create((set, get) => ({ tracks: [], + history: [], selectedClips: [], + pushHistory: () => { + const { tracks, history } = get(); + // Deep copy tracks + set({ history: [...history, JSON.parse(JSON.stringify(tracks))] }); + }, + + undo: () => { + const { history } = get(); + if (history.length === 0) return; + const prev = history[history.length - 1]; + set({ tracks: prev, history: history.slice(0, -1) }); + }, + selectClip: (trackId, clipId, multi = false) => { set((state) => { const exists = state.selectedClips.some( @@ -86,6 +105,7 @@ export const useTimelineStore = create((set, get) => ({ setSelectedClips: (clips) => set({ selectedClips: clips }), addTrack: (type) => { + get().pushHistory(); const newTrack: TimelineTrack = { id: crypto.randomUUID(), name: `${type.charAt(0).toUpperCase() + type.slice(1)} Track`, @@ -100,12 +120,14 @@ export const useTimelineStore = create((set, get) => ({ }, removeTrack: (trackId) => { + get().pushHistory(); set((state) => ({ tracks: state.tracks.filter((track) => track.id !== trackId), })); }, addClipToTrack: (trackId, clipData) => { + get().pushHistory(); const newClip: TimelineClip = { ...clipData, id: crypto.randomUUID(), @@ -124,19 +146,21 @@ export const useTimelineStore = create((set, get) => ({ }, removeClipFromTrack: (trackId, clipId) => { + get().pushHistory(); set((state) => ({ - tracks: state.tracks.map((track) => - track.id === trackId - ? { - ...track, - clips: track.clips.filter((clip) => clip.id !== clipId), - } - : track - ), + tracks: state.tracks + .map((track) => + track.id === trackId + ? { ...track, clips: track.clips.filter((clip) => clip.id !== clipId) } + : track + ) + // Remove track if it becomes empty + .filter((track) => track.clips.length > 0), })); }, moveClipToTrack: (fromTrackId, toTrackId, clipId) => { + get().pushHistory(); set((state) => { const fromTrack = state.tracks.find((track) => track.id === fromTrackId); const clipToMove = fromTrack?.clips.find((clip) => clip.id === clipId); @@ -144,25 +168,29 @@ export const useTimelineStore = create((set, get) => ({ if (!clipToMove) return state; return { - tracks: state.tracks.map((track) => { - if (track.id === fromTrackId) { - return { - ...track, - clips: track.clips.filter((clip) => clip.id !== clipId), - }; - } else if (track.id === toTrackId) { - return { - ...track, - clips: [...track.clips, clipToMove], - }; - } - return track; - }), + tracks: state.tracks + .map((track) => { + if (track.id === fromTrackId) { + return { + ...track, + clips: track.clips.filter((clip) => clip.id !== clipId), + }; + } else if (track.id === toTrackId) { + return { + ...track, + clips: [...track.clips, clipToMove], + }; + } + return track; + }) + // Remove track if it becomes empty + .filter((track) => track.clips.length > 0), }; }); }, updateClipTrim: (trackId, clipId, trimStart, trimEnd) => { + get().pushHistory(); set((state) => ({ tracks: state.tracks.map((track) => track.id === trackId @@ -178,6 +206,7 @@ export const useTimelineStore = create((set, get) => ({ }, updateClipStartTime: (trackId, clipId, startTime) => { + get().pushHistory(); set((state) => ({ tracks: state.tracks.map((track) => track.id === trackId @@ -193,6 +222,7 @@ export const useTimelineStore = create((set, get) => ({ }, toggleTrackMute: (trackId) => { + get().pushHistory(); set((state) => ({ tracks: state.tracks.map((track) => track.id === trackId ? { ...track, muted: !track.muted } : track From 9570847edd5afc8d005d4c1596eb7ce84d3fcdd1 Mon Sep 17 00:00:00 2001 From: aashishparuvada Date: Tue, 24 Jun 2025 09:27:13 +0530 Subject: [PATCH 20/22] updated package-lock.json --- package-lock.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9891ac9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "OpenCut", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} From 073f0d89bd8ee6ccd81a3a4b3948f88816812bdf Mon Sep 17 00:00:00 2001 From: aashishparuvada Date: Tue, 24 Jun 2025 09:31:53 +0530 Subject: [PATCH 21/22] feat:enabled-redo-functionality --- apps/web/src/components/editor/timeline.tsx | 17 +++++++++++++- apps/web/src/stores/timeline-store.ts | 25 +++++++++++++++++---- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index 484e1a6..064d0dd 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -40,7 +40,7 @@ export function Timeline() { // Timeline shows all tracks (video, audio, effects) and their clips. // You can drag media here to add it to your project. // Clips can be trimmed, deleted, and moved. - const { tracks, addTrack, addClipToTrack, removeTrack, toggleTrackMute, removeClipFromTrack, moveClipToTrack, getTotalDuration, selectedClips, selectClip, deselectClip, clearSelectedClips, setSelectedClips, updateClipTrim, undo } = + const { tracks, addTrack, addClipToTrack, removeTrack, toggleTrackMute, removeClipFromTrack, moveClipToTrack, getTotalDuration, selectedClips, selectClip, deselectClip, clearSelectedClips, setSelectedClips, updateClipTrim, undo, redo } = useTimelineStore(); const { mediaItems, addMediaItem } = useMediaStore(); const { currentTime, duration, seek, setDuration, isPlaying, play, pause, toggle, setSpeed, speed } = usePlaybackStore(); @@ -110,6 +110,21 @@ export function Timeline() { return () => window.removeEventListener("keydown", handleKeyDown); }, [undo]); + // Keyboard event for redo (Cmd+Shift+Z or Cmd+Y) + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === "z" && e.shiftKey) { + e.preventDefault(); + redo(); + } else if ((e.metaKey || e.ctrlKey) && e.key === "y") { + e.preventDefault(); + redo(); + } + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [redo]); + // Mouse down on timeline background to start marquee const handleTimelineMouseDown = (e: React.MouseEvent) => { if (e.target === e.currentTarget && e.button === 0) { diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts index a369d57..e1474ba 100644 --- a/apps/web/src/stores/timeline-store.ts +++ b/apps/web/src/stores/timeline-store.ts @@ -21,6 +21,7 @@ export interface TimelineTrack { interface TimelineStore { tracks: TimelineTrack[]; history: TimelineTrack[][]; + redoStack: TimelineTrack[][]; // Multi-selection selectedClips: { trackId: string; clipId: string }[]; @@ -57,25 +58,34 @@ interface TimelineStore { // New actions undo: () => void; + redo: () => void; pushHistory: () => void; } export const useTimelineStore = create((set, get) => ({ tracks: [], history: [], + redoStack: [], selectedClips: [], pushHistory: () => { - const { tracks, history } = get(); + const { tracks, history, redoStack } = get(); // Deep copy tracks - set({ history: [...history, JSON.parse(JSON.stringify(tracks))] }); + set({ + history: [...history, JSON.parse(JSON.stringify(tracks))], + redoStack: [] // Clear redo stack when new action is performed + }); }, undo: () => { - const { history } = get(); + const { history, redoStack, tracks } = get(); if (history.length === 0) return; const prev = history[history.length - 1]; - set({ tracks: prev, history: history.slice(0, -1) }); + set({ + tracks: prev, + history: history.slice(0, -1), + redoStack: [...redoStack, JSON.parse(JSON.stringify(tracks))] // Add current state to redo stack + }); }, selectClip: (trackId, clipId, multi = false) => { @@ -244,4 +254,11 @@ export const useTimelineStore = create((set, get) => ({ return Math.max(...trackEndTimes, 0); }, + + redo: () => { + const { redoStack } = get(); + if (redoStack.length === 0) return; + const next = redoStack[redoStack.length - 1]; + set({ tracks: next, redoStack: redoStack.slice(0, -1) }); + }, })); From aacf4c5979812afeb07026de0c5a842acd9cb1bb Mon Sep 17 00:00:00 2001 From: Hyteq Date: Tue, 24 Jun 2025 07:56:52 +0300 Subject: [PATCH 22/22] fix: update login url in middleware --- apps/web/src/middleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts index 7f096cd..f2d3f59 100644 --- a/apps/web/src/middleware.ts +++ b/apps/web/src/middleware.ts @@ -7,7 +7,7 @@ export async function middleware(request: NextRequest) { const session = getSessionCookie(request); if (path === "/editor" && !session && process.env.NODE_ENV === "production") { - const loginUrl = new URL("/auth/login", request.url); + const loginUrl = new URL("/login", request.url); loginUrl.searchParams.set("redirect", request.url); return NextResponse.redirect(loginUrl); }