From 998ee086e5f448c32fba1a61dbff5ef4f8a428d9 Mon Sep 17 00:00:00 2001 From: ga1az Date: Wed, 25 Jun 2025 01:21:09 -0400 Subject: chore(docker): update Dockerfile to set NODE_ENV to production and ensure telemetry is disabled --- apps/web/Dockerfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index 8b13ce38..46eeb102 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -19,7 +19,8 @@ COPY apps/web/ apps/web/ COPY packages/db/ packages/db/ COPY packages/auth/ packages/auth/ -ENV NEXT_TELEMETRY_DISABLED=1 +ENV NODE_ENV production +ENV NEXT_TELEMETRY_DISABLED 1 WORKDIR /app/apps/web RUN bun run build @@ -28,7 +29,8 @@ RUN bun run build FROM base AS runner WORKDIR /app -ENV NEXT_TELEMETRY_DISABLED=1 +ENV NODE_ENV production +ENV NEXT_TELEMETRY_DISABLED 1 RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs -- cgit v1.3.1 From d8494185e07915529bb70fde23f16bb972dc64af Mon Sep 17 00:00:00 2001 From: ILoveScratch Date: Wed, 25 Jun 2025 14:28:56 +0800 Subject: docs: change commands in README adapted to different platforms --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 615dbcd8..89828c0a 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,16 @@ Before you begin, ensure you have the following installed on your system: Navigate into the web app's directory and create a `.env` file from the example: ```bash cd apps/web - cp .env.example .env + + + # Unix/Linux/Mac + cp .env.example .env.local + + # Windows Command Prompt + copy .env.example .env.local + + # Windows PowerShell + Copy-Item .env.example .env.local ``` *The default values in the `.env` file should work for local development.* -- cgit v1.3.1 From d39edd85211d20bd402d3f1049d8e176969f5478 Mon Sep 17 00:00:00 2001 From: YaoSiQian <2229561981@qq.com> Date: Wed, 25 Jun 2025 16:03:36 +0800 Subject: style(issue_template): form style --- .github/ISSUE_TEMPLATE/bug_report.md | 31 ------------- .github/ISSUE_TEMPLATE/bug_report.yml | 71 ++++++++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 19 -------- .github/ISSUE_TEMPLATE/feature_request.yml | 43 ++++++++++++++++++ 4 files changed, 114 insertions(+), 50 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 85cc4b23..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '[BUG] ' -labels: bug -assignees: '' ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] - -**Additional context** -Add any other context about the problem here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..132d1b77 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,71 @@ +name: Bug report +about: Create a report to help us improve +title: '[BUG] ' +labels: bug +assignees: '' +body: + - type: input + id: Platform + attributes: + label: Platform + description: Please enter the platform on which you encountered the bug. + placeholder: e.g. Windows 11, Ubuntu 14.04 + validations: + required: true + - type: input + id: Browser + attributes: + label: Browser + description: Please enter the browser on which you encountered the bug. + placeholder: e.g. Chrome 137, Firefox 137, Safari 17 + validations: + required: true + - type: textarea + id: current-behavior + attributes: + label: Current Behavior + description: A concise description of what you're experiencing. + validations: + required: true + - type: textarea + id: expected-behavior + attributes: + label: Expected Behavior + description: A concise description of what you expected to happen. + validations: + required: false + - type: dropdown + id: recurrence-probability + attributes: + label: Recurrence Probability + description: How often does this bug occur? + options: + - Always + - Usually + - Sometimes + - Seldom + default: 0 + validations: + required: true + - type: textarea + id: steps-to-reproduce + attributes: + label: Steps To Reproduce + description: Steps to reproduce the behavior. + placeholder: | + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. See error + validations: + required: true + - type: textarea + id: additional-context + attributes: + label: Anything else? + description: | + Links? References? Anything that will give us more context about the issue you are encountering! + + Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 1d528f25..00000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '[FEATURE] ' -labels: enhancement -assignees: '' ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..f551d45d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,43 @@ +name: Feature request +about: Suggest an idea for OpenCut +title: '[FEATURE] ' +labels: enhancement +assignees: '' +body: + - type: markdown + attributes: + value: Please make sure that no duplicated issues has already been delivered. + - type: textarea + id: problem + attributes: + label: Problem + placeholder: Is your feature request related to a problem? Please describe. + description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + validations: + required: true + - type: textarea + id: solution + attributes: + label: Solution + placeholder: Describe the solution you'd like. + description: A clear and concise description of what you want to happen. + validations: + required: true + - type: textarea + id: alternative + attributes: + label: Alternative + placeholder: Describe alternatives you've considered. + description: A clear and concise description of any alternative solutions or features you've considered. + validations: + required: true + - type: textarea + id: additional-context + attributes: + label: Anything else? + description: | + Links? References? Anything that will give us more context about the issue you are encountering! + + Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. + validations: + required: false \ No newline at end of file -- cgit v1.3.1 From 1722055a721eae513750d54feab7018d1dc650f1 Mon Sep 17 00:00:00 2001 From: Zaid-maker <53424436+Zaid-maker@users.noreply.github.com> Date: Wed, 25 Jun 2025 13:53:51 +0500 Subject: update: bump Bun version to 1.2.17 in CI workflow --- .github/workflows/bun-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/bun-ci.yml b/.github/workflows/bun-ci.yml index 4df6212a..82bac280 100644 --- a/.github/workflows/bun-ci.yml +++ b/.github/workflows/bun-ci.yml @@ -31,7 +31,7 @@ jobs: - name: Install Bun uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 with: - bun-version: 1.2.2 + bun-version: 1.2.17 - name: Cache Bun modules uses: actions/cache@v4 -- cgit v1.3.1 From 51a83f9f216b2c496fba9729ff3dda9e9b720d80 Mon Sep 17 00:00:00 2001 From: Zaid-maker <53424436+Zaid-maker@users.noreply.github.com> Date: Wed, 25 Jun 2025 14:16:35 +0500 Subject: fix(ci): update cache key to include Bun version for consistency --- .github/workflows/bun-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/bun-ci.yml b/.github/workflows/bun-ci.yml index 82bac280..34b6d516 100644 --- a/.github/workflows/bun-ci.yml +++ b/.github/workflows/bun-ci.yml @@ -37,7 +37,7 @@ jobs: uses: actions/cache@v4 with: path: ~/.bun/install/cache - key: ${{ runner.os }}-bun-${{ hashFiles('apps/web/bun.lock') }} + key: ${{ runner.os }}-bun-1.2.17-${{ hashFiles('apps/web/bun.lock') }} - name: Install dependencies working-directory: apps/web -- cgit v1.3.1 From 56a8098442886653272e8f748c11360a012747dd Mon Sep 17 00:00:00 2001 From: YaoSiQian <2229561981@qq.com> Date: Wed, 25 Jun 2025 20:50:33 +0800 Subject: fix(issue_template): fix tags --- .github/ISSUE_TEMPLATE/bug_report.yml | 3 +-- .github/ISSUE_TEMPLATE/feature_request.yml | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 132d1b77..c9684d1f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,8 +1,7 @@ name: Bug report -about: Create a report to help us improve +description: Create a report to help us improve title: '[BUG] ' labels: bug -assignees: '' body: - type: input id: Platform diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index f551d45d..49a00eb6 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,8 +1,7 @@ name: Feature request -about: Suggest an idea for OpenCut +description: Suggest an idea for OpenCut title: '[FEATURE] ' labels: enhancement -assignees: '' body: - type: markdown attributes: -- cgit v1.3.1 From ba9870a9080ac4bdcc6b291a8908d1790ab99b98 Mon Sep 17 00:00:00 2001 From: yassinehaimouch Date: Wed, 25 Jun 2025 15:00:35 +0100 Subject: update: landing-page --- apps/web/public/frame.svg | 10 ++++++++++ apps/web/public/landing-page-bg.png | Bin 0 -> 230853 bytes apps/web/public/logo.svg | 4 ++++ apps/web/src/app/globals.css | 2 +- apps/web/src/app/page.tsx | 8 ++++++++ apps/web/src/components/header.tsx | 11 ++++++++--- apps/web/src/components/landing/hero.tsx | 27 +++++++++++++++++++-------- 7 files changed, 50 insertions(+), 12 deletions(-) create mode 100644 apps/web/public/frame.svg create mode 100644 apps/web/public/landing-page-bg.png create mode 100644 apps/web/public/logo.svg diff --git a/apps/web/public/frame.svg b/apps/web/public/frame.svg new file mode 100644 index 00000000..4dac68e9 --- /dev/null +++ b/apps/web/public/frame.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/apps/web/public/landing-page-bg.png b/apps/web/public/landing-page-bg.png new file mode 100644 index 00000000..4f873945 Binary files /dev/null and b/apps/web/public/landing-page-bg.png differ diff --git a/apps/web/public/logo.svg b/apps/web/public/logo.svg new file mode 100644 index 00000000..7de330af --- /dev/null +++ b/apps/web/public/logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index 2169dbbc..1c8a7887 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -39,7 +39,7 @@ --sidebar-ring: 0 0% 3.9%; } .dark { - --background: 0 0% 8%; + --background: 0 0% 6%; --foreground: 0 0% 98%; --card: 0 0% 3.9%; --card-foreground: 0 0% 98%; diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index dcedab32..e9957e1b 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -1,6 +1,7 @@ import { Hero } from "@/components/landing/hero"; import { Header } from "@/components/header"; import { getWaitlistCount } from "@/lib/waitlist"; +import Image from "next/image"; // Force dynamic rendering so waitlist count updates in real-time export const dynamic = "force-dynamic"; @@ -10,6 +11,13 @@ export default async function Home() { return (
+ landing-page.bg
diff --git a/apps/web/src/components/header.tsx b/apps/web/src/components/header.tsx index 27503cd0..d99a4fc5 100644 --- a/apps/web/src/components/header.tsx +++ b/apps/web/src/components/header.tsx @@ -29,8 +29,7 @@ export function Header() { const leftContent = ( - OpenCut Logo - OpenCut + OpenCut Logo ); @@ -59,5 +58,11 @@ export function Header() { ); - return ; + return ( + + ); } diff --git a/apps/web/src/components/landing/hero.tsx b/apps/web/src/components/landing/hero.tsx index 7b4588c8..fadc043a 100644 --- a/apps/web/src/components/landing/hero.tsx +++ b/apps/web/src/components/landing/hero.tsx @@ -8,6 +8,7 @@ import Link from "next/link"; import { useEffect, useState } from "react"; import { useToast } from "@/hooks/use-toast"; import { getStars } from "@/lib/fetchGhStars"; +import Image from "next/image"; interface HeroProps { signupCount: number; @@ -82,7 +83,7 @@ export function Hero({ signupCount }: HeroProps) { }; return ( -
+
-

- The open source -

-

- video editor -

+

The First Open Source

+
+ Online +
+ frame + + Video Editor + +
+
Date: Wed, 25 Jun 2025 19:57:35 +0530 Subject: made the project app PWA and moved metadata into a sperate file app folder --- apps/web/public/browserconfig.xml | 11 +++++ apps/web/public/favicon.ico | Bin 0 -> 1150 bytes apps/web/public/icons/android-icon-144x144.png | Bin 0 -> 1076 bytes apps/web/public/icons/android-icon-192x192.png | Bin 0 -> 741 bytes apps/web/public/icons/android-icon-36x36.png | Bin 0 -> 768 bytes apps/web/public/icons/android-icon-48x48.png | Bin 0 -> 802 bytes apps/web/public/icons/android-icon-72x72.png | Bin 0 -> 826 bytes apps/web/public/icons/android-icon-96x96.png | Bin 0 -> 906 bytes apps/web/public/icons/apple-icon-114x114.png | Bin 0 -> 985 bytes apps/web/public/icons/apple-icon-120x120.png | Bin 0 -> 998 bytes apps/web/public/icons/apple-icon-144x144.png | Bin 0 -> 1076 bytes apps/web/public/icons/apple-icon-152x152.png | Bin 0 -> 1163 bytes apps/web/public/icons/apple-icon-180x180.png | Bin 0 -> 1360 bytes apps/web/public/icons/apple-icon-57x57.png | Bin 0 -> 809 bytes apps/web/public/icons/apple-icon-60x60.png | Bin 0 -> 843 bytes apps/web/public/icons/apple-icon-72x72.png | Bin 0 -> 826 bytes apps/web/public/icons/apple-icon-76x76.png | Bin 0 -> 820 bytes apps/web/public/icons/favicon-16x16.png | Bin 0 -> 670 bytes apps/web/public/icons/favicon-32x32.png | Bin 0 -> 747 bytes apps/web/public/icons/favicon-96x96.png | Bin 0 -> 906 bytes apps/web/public/icons/ms-icon-144x144.png | Bin 0 -> 1076 bytes apps/web/public/icons/ms-icon-150x150.png | Bin 0 -> 1159 bytes apps/web/public/icons/ms-icon-310x310.png | Bin 0 -> 2749 bytes apps/web/public/icons/ms-icon-70x70.png | Bin 0 -> 814 bytes apps/web/public/manifest.json | 44 +++++++++++++++++ apps/web/src/app/favicon.ico | Bin 4286 -> 0 bytes apps/web/src/app/layout.tsx | 36 +------------- apps/web/src/app/metadata.ts | 64 +++++++++++++++++++++++++ 28 files changed, 120 insertions(+), 35 deletions(-) create mode 100644 apps/web/public/browserconfig.xml create mode 100644 apps/web/public/favicon.ico create mode 100644 apps/web/public/icons/android-icon-144x144.png create mode 100644 apps/web/public/icons/android-icon-192x192.png create mode 100644 apps/web/public/icons/android-icon-36x36.png create mode 100644 apps/web/public/icons/android-icon-48x48.png create mode 100644 apps/web/public/icons/android-icon-72x72.png create mode 100644 apps/web/public/icons/android-icon-96x96.png create mode 100644 apps/web/public/icons/apple-icon-114x114.png create mode 100644 apps/web/public/icons/apple-icon-120x120.png create mode 100644 apps/web/public/icons/apple-icon-144x144.png create mode 100644 apps/web/public/icons/apple-icon-152x152.png create mode 100644 apps/web/public/icons/apple-icon-180x180.png create mode 100644 apps/web/public/icons/apple-icon-57x57.png create mode 100644 apps/web/public/icons/apple-icon-60x60.png create mode 100644 apps/web/public/icons/apple-icon-72x72.png create mode 100644 apps/web/public/icons/apple-icon-76x76.png create mode 100644 apps/web/public/icons/favicon-16x16.png create mode 100644 apps/web/public/icons/favicon-32x32.png create mode 100644 apps/web/public/icons/favicon-96x96.png create mode 100644 apps/web/public/icons/ms-icon-144x144.png create mode 100644 apps/web/public/icons/ms-icon-150x150.png create mode 100644 apps/web/public/icons/ms-icon-310x310.png create mode 100644 apps/web/public/icons/ms-icon-70x70.png create mode 100644 apps/web/public/manifest.json delete mode 100644 apps/web/src/app/favicon.ico create mode 100644 apps/web/src/app/metadata.ts diff --git a/apps/web/public/browserconfig.xml b/apps/web/public/browserconfig.xml new file mode 100644 index 00000000..39060b21 --- /dev/null +++ b/apps/web/public/browserconfig.xml @@ -0,0 +1,11 @@ + + + + + + + + #ffffff + + + \ No newline at end of file diff --git a/apps/web/public/favicon.ico b/apps/web/public/favicon.ico new file mode 100644 index 00000000..9b2cba8f Binary files /dev/null and b/apps/web/public/favicon.ico differ diff --git a/apps/web/public/icons/android-icon-144x144.png b/apps/web/public/icons/android-icon-144x144.png new file mode 100644 index 00000000..7320ece7 Binary files /dev/null and b/apps/web/public/icons/android-icon-144x144.png differ diff --git a/apps/web/public/icons/android-icon-192x192.png b/apps/web/public/icons/android-icon-192x192.png new file mode 100644 index 00000000..f534f429 Binary files /dev/null and b/apps/web/public/icons/android-icon-192x192.png differ diff --git a/apps/web/public/icons/android-icon-36x36.png b/apps/web/public/icons/android-icon-36x36.png new file mode 100644 index 00000000..6b8db020 Binary files /dev/null and b/apps/web/public/icons/android-icon-36x36.png differ diff --git a/apps/web/public/icons/android-icon-48x48.png b/apps/web/public/icons/android-icon-48x48.png new file mode 100644 index 00000000..30a68fa5 Binary files /dev/null and b/apps/web/public/icons/android-icon-48x48.png differ diff --git a/apps/web/public/icons/android-icon-72x72.png b/apps/web/public/icons/android-icon-72x72.png new file mode 100644 index 00000000..e4019c84 Binary files /dev/null and b/apps/web/public/icons/android-icon-72x72.png differ diff --git a/apps/web/public/icons/android-icon-96x96.png b/apps/web/public/icons/android-icon-96x96.png new file mode 100644 index 00000000..85591ab4 Binary files /dev/null and b/apps/web/public/icons/android-icon-96x96.png differ diff --git a/apps/web/public/icons/apple-icon-114x114.png b/apps/web/public/icons/apple-icon-114x114.png new file mode 100644 index 00000000..c776e3f9 Binary files /dev/null and b/apps/web/public/icons/apple-icon-114x114.png differ diff --git a/apps/web/public/icons/apple-icon-120x120.png b/apps/web/public/icons/apple-icon-120x120.png new file mode 100644 index 00000000..76604df8 Binary files /dev/null and b/apps/web/public/icons/apple-icon-120x120.png differ diff --git a/apps/web/public/icons/apple-icon-144x144.png b/apps/web/public/icons/apple-icon-144x144.png new file mode 100644 index 00000000..7320ece7 Binary files /dev/null and b/apps/web/public/icons/apple-icon-144x144.png differ diff --git a/apps/web/public/icons/apple-icon-152x152.png b/apps/web/public/icons/apple-icon-152x152.png new file mode 100644 index 00000000..eb5ef0df Binary files /dev/null and b/apps/web/public/icons/apple-icon-152x152.png differ diff --git a/apps/web/public/icons/apple-icon-180x180.png b/apps/web/public/icons/apple-icon-180x180.png new file mode 100644 index 00000000..b90e62fb Binary files /dev/null and b/apps/web/public/icons/apple-icon-180x180.png differ diff --git a/apps/web/public/icons/apple-icon-57x57.png b/apps/web/public/icons/apple-icon-57x57.png new file mode 100644 index 00000000..7cfb2e56 Binary files /dev/null and b/apps/web/public/icons/apple-icon-57x57.png differ diff --git a/apps/web/public/icons/apple-icon-60x60.png b/apps/web/public/icons/apple-icon-60x60.png new file mode 100644 index 00000000..1d90a370 Binary files /dev/null and b/apps/web/public/icons/apple-icon-60x60.png differ diff --git a/apps/web/public/icons/apple-icon-72x72.png b/apps/web/public/icons/apple-icon-72x72.png new file mode 100644 index 00000000..e4019c84 Binary files /dev/null and b/apps/web/public/icons/apple-icon-72x72.png differ diff --git a/apps/web/public/icons/apple-icon-76x76.png b/apps/web/public/icons/apple-icon-76x76.png new file mode 100644 index 00000000..34575f42 Binary files /dev/null and b/apps/web/public/icons/apple-icon-76x76.png differ diff --git a/apps/web/public/icons/favicon-16x16.png b/apps/web/public/icons/favicon-16x16.png new file mode 100644 index 00000000..96db3a2f Binary files /dev/null and b/apps/web/public/icons/favicon-16x16.png differ diff --git a/apps/web/public/icons/favicon-32x32.png b/apps/web/public/icons/favicon-32x32.png new file mode 100644 index 00000000..f8f589dc Binary files /dev/null and b/apps/web/public/icons/favicon-32x32.png differ diff --git a/apps/web/public/icons/favicon-96x96.png b/apps/web/public/icons/favicon-96x96.png new file mode 100644 index 00000000..85591ab4 Binary files /dev/null and b/apps/web/public/icons/favicon-96x96.png differ diff --git a/apps/web/public/icons/ms-icon-144x144.png b/apps/web/public/icons/ms-icon-144x144.png new file mode 100644 index 00000000..7320ece7 Binary files /dev/null and b/apps/web/public/icons/ms-icon-144x144.png differ diff --git a/apps/web/public/icons/ms-icon-150x150.png b/apps/web/public/icons/ms-icon-150x150.png new file mode 100644 index 00000000..248ef48a Binary files /dev/null and b/apps/web/public/icons/ms-icon-150x150.png differ diff --git a/apps/web/public/icons/ms-icon-310x310.png b/apps/web/public/icons/ms-icon-310x310.png new file mode 100644 index 00000000..950a3d6b Binary files /dev/null and b/apps/web/public/icons/ms-icon-310x310.png differ diff --git a/apps/web/public/icons/ms-icon-70x70.png b/apps/web/public/icons/ms-icon-70x70.png new file mode 100644 index 00000000..7b9c2694 Binary files /dev/null and b/apps/web/public/icons/ms-icon-70x70.png differ diff --git a/apps/web/public/manifest.json b/apps/web/public/manifest.json new file mode 100644 index 00000000..fcbfad59 --- /dev/null +++ b/apps/web/public/manifest.json @@ -0,0 +1,44 @@ +{ + "name": "OpenCut", + "description": "A simple but powerful video editor that gets the job done. In your browser.", + "display": "standalone", + "start_url": "/", + "icons": [ + { + "src": "/icons/android-icon-36x36.png", + "sizes": "36x36", + "type": "image\/png", + "density": "0.75" + }, + { + "src": "/icons/android-icon-48x48.png", + "sizes": "48x48", + "type": "image\/png", + "density": "1.0" + }, + { + "src": "/icons/android-icon-72x72.png", + "sizes": "72x72", + "type": "image\/png", + "density": "1.5" + }, + { + "src": "/icons/android-icon-96x96.png", + "sizes": "96x96", + "type": "image\/png", + "density": "2.0" + }, + { + "src": "/icons/android-icon-144x144.png", + "sizes": "144x144", + "type": "image\/png", + "density": "3.0" + }, + { + "src": "/icons/android-icon-192x192.png", + "sizes": "192x192", + "type": "image\/png", + "density": "4.0" + } + ] +} \ No newline at end of file diff --git a/apps/web/src/app/favicon.ico b/apps/web/src/app/favicon.ico deleted file mode 100644 index cb20127b..00000000 Binary files a/apps/web/src/app/favicon.ico and /dev/null differ diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 5d33100d..c76dc451 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -1,4 +1,3 @@ -import type { Metadata } from "next"; import { Inter } from "next/font/google"; import { ThemeProvider } from "next-themes"; import { Analytics } from "@vercel/analytics/react"; @@ -12,40 +11,7 @@ const inter = Inter({ variable: "--font-inter", }); -export const metadata: Metadata = { - title: "OpenCut", - description: - "A simple but powerful video editor that gets the job done. In your browser.", - openGraph: { - title: "OpenCut", - description: - "A simple but powerful video editor that gets the job done. In your browser.", - url: "https://opencut.app", - siteName: "OpenCut", - locale: "en_US", - type: "website", - images: [ - { - url: "https://opencut.app/opengraph-image.jpg", - width: 1200, - height: 630, - alt: "OpenCut", - }, - ], - }, - twitter: { - card: "summary_large_image", - title: "OpenCut", - description: - "A simple but powerful video editor that gets the job done. In your browser.", - creator: "@opencutapp", - images: ["/opengraph-image.jpg"], - }, - robots: { - index: true, - follow: true, - }, -}; +export { metadata } from './metadata' export default function RootLayout({ children, diff --git a/apps/web/src/app/metadata.ts b/apps/web/src/app/metadata.ts new file mode 100644 index 00000000..4db2e8de --- /dev/null +++ b/apps/web/src/app/metadata.ts @@ -0,0 +1,64 @@ +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "OpenCut", + description: + "A simple but powerful video editor that gets the job done. In your browser.", + openGraph: { + title: "OpenCut", + description: + "A simple but powerful video editor that gets the job done. In your browser.", + url: "https://opencut.app", + siteName: "OpenCut", + locale: "en_US", + type: "website", + images: [ + { + url: "https://opencut.app/opengraph-image.jpg", + width: 1200, + height: 630, + alt: "OpenCut", + }, + ], + }, + twitter: { + card: "summary_large_image", + title: "OpenCut", + description: + "A simple but powerful video editor that gets the job done. In your browser.", + creator: "@opencutapp", + images: ["/opengraph-image.jpg"], + }, + robots: { + index: true, + follow: true, + }, + icons: { + icon: [ + { url: "/favicon.ico" }, + { url: "/icons/favicon-16x16.png", sizes: "16x16", type: "image/png" }, + { url: "/icons/favicon-32x32.png", sizes: "32x32", type: "image/png" }, + { url: "/icons/favicon-96x96.png", sizes: "96x96", type: "image/png" }, + ], + apple: [ + { url: "/icons/apple-icon-57x57.png", sizes: "57x57", type: "image/png" }, + { url: "/icons/apple-icon-60x60.png", sizes: "60x60", type: "image/png" }, + { url: "/icons/apple-icon-72x72.png", sizes: "72x72", type: "image/png" }, + { url: "/icons/apple-icon-76x76.png", sizes: "76x76", type: "image/png" }, + { url: "/icons/apple-icon-114x114.png", sizes: "114x114", type: "image/png" }, + { url: "/icons/apple-icon-120x120.png", sizes: "120x120", type: "image/png" }, + { url: "/icons/apple-icon-144x144.png", sizes: "144x144", type: "image/png" }, + { url: "/icons/apple-icon-152x152.png", sizes: "152x152", type: "image/png" }, + { url: "/icons/apple-icon-180x180.png", sizes: "180x180", type: "image/png" }, + ], + shortcut: ["/favicon.ico"] + }, + appleWebApp: { + capable: true, + title: "OpenCut", + }, + manifest: "/manifest.json", + other: { + "msapplication-config": "/browserconfig.xml" + } +}; \ No newline at end of file -- cgit v1.3.1 From 2e5b623d90ba250dd5c50b623bd8044fd6d62e2a Mon Sep 17 00:00:00 2001 From: muhammed sinan Date: Wed, 25 Jun 2025 20:14:55 +0530 Subject: metadata declared in layout file --- apps/web/src/app/layout.tsx | 3 ++- apps/web/src/app/metadata.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index c76dc451..ee045647 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -5,13 +5,14 @@ import Script from "next/script"; import "./globals.css"; import { Toaster } from "../components/ui/sonner"; import { TooltipProvider } from "../components/ui/tooltip"; +import { baseMetaData } from "./metadata"; const inter = Inter({ subsets: ["latin"], variable: "--font-inter", }); -export { metadata } from './metadata' +export const metadata = baseMetaData; export default function RootLayout({ children, diff --git a/apps/web/src/app/metadata.ts b/apps/web/src/app/metadata.ts index 4db2e8de..fe674381 100644 --- a/apps/web/src/app/metadata.ts +++ b/apps/web/src/app/metadata.ts @@ -1,6 +1,6 @@ import { Metadata } from "next"; -export const metadata: Metadata = { +export const baseMetaData: Metadata = { title: "OpenCut", description: "A simple but powerful video editor that gets the job done. In your browser.", -- cgit v1.3.1 From cca25a61039b1d09c9ffaee2f42046c09ab72c3b Mon Sep 17 00:00:00 2001 From: muhammed sinan Date: Wed, 25 Jun 2025 20:22:17 +0530 Subject: Reduce code duplication by extracting common metadata values. --- apps/web/src/app/metadata.ts | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/apps/web/src/app/metadata.ts b/apps/web/src/app/metadata.ts index fe674381..4e387014 100644 --- a/apps/web/src/app/metadata.ts +++ b/apps/web/src/app/metadata.ts @@ -1,20 +1,23 @@ import { Metadata } from "next"; +const title = "OpenCut"; +const description = "A simple but powerful video editor that gets the job done. In your browser."; +const openGraphImageUrl = "https://opencut.app/opengraph-image.jpg"; +const twitterImageUrl = "/opengraph-image.jpg"; + export const baseMetaData: Metadata = { - title: "OpenCut", - description: - "A simple but powerful video editor that gets the job done. In your browser.", + title: title, + description: description, openGraph: { - title: "OpenCut", - description: - "A simple but powerful video editor that gets the job done. In your browser.", + title: title, + description: description, url: "https://opencut.app", siteName: "OpenCut", locale: "en_US", type: "website", images: [ { - url: "https://opencut.app/opengraph-image.jpg", + url: openGraphImageUrl, width: 1200, height: 630, alt: "OpenCut", @@ -23,11 +26,10 @@ export const baseMetaData: Metadata = { }, twitter: { card: "summary_large_image", - title: "OpenCut", - description: - "A simple but powerful video editor that gets the job done. In your browser.", + title: title, + description: description, creator: "@opencutapp", - images: ["/opengraph-image.jpg"], + images: [twitterImageUrl], }, robots: { index: true, @@ -55,10 +57,10 @@ export const baseMetaData: Metadata = { }, appleWebApp: { capable: true, - title: "OpenCut", + title: title, }, manifest: "/manifest.json", other: { "msapplication-config": "/browserconfig.xml" } -}; \ No newline at end of file +}; -- cgit v1.3.1 From a511c57729eff4b388b0ba42ed4484483bca86a4 Mon Sep 17 00:00:00 2001 From: DevloperAmanSingh Date: Wed, 25 Jun 2025 21:41:10 +0530 Subject: feat: enhance media processing with progress tracking --- apps/web/src/components/editor/media-panel.tsx | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/apps/web/src/components/editor/media-panel.tsx b/apps/web/src/components/editor/media-panel.tsx index f2b3f657..cdd67a86 100644 --- a/apps/web/src/components/editor/media-panel.tsx +++ b/apps/web/src/components/editor/media-panel.tsx @@ -17,27 +17,28 @@ export function MediaPanel() { const { mediaItems, addMediaItem, removeMediaItem } = useMediaStore(); const fileInputRef = useRef(null); const [isProcessing, setIsProcessing] = useState(false); + const [progress, setProgress] = useState(0); const [searchQuery, setSearchQuery] = useState(""); const [mediaFilter, setMediaFilter] = useState("all"); const processFiles = async (files: FileList | File[]) => { - // If no files, do nothing - if (!files?.length) return; - + if (!files || files.length === 0) return; setIsProcessing(true); + setProgress(0); try { // Process files (extract metadata, generate thumbnails, etc.) - const items = await processMediaFiles(files); + const processedItems = await processMediaFiles(files, (p) => + setProgress(p) + ); // Add each processed media item to the store - items.forEach((item) => { - addMediaItem(item); - }); + processedItems.forEach((item) => addMediaItem(item)); } catch (error) { - // Show error if processing fails - console.error("File processing failed:", error); + // Show error toast if processing fails + console.error("Error processing files:", error); toast.error("Failed to process files"); } finally { setIsProcessing(false); + setProgress(0); } }; @@ -241,15 +242,12 @@ export function MediaPanel() { {isProcessing ? ( <> - Processing... + {progress}% ) : ( <> - + Add -- cgit v1.3.1 From f9d0be20d0a9b4c5ba5916838b05426fbb652fa7 Mon Sep 17 00:00:00 2001 From: DevloperAmanSingh Date: Wed, 25 Jun 2025 21:41:16 +0530 Subject: feat: implement progress tracking for media file processing in timeline --- apps/web/src/components/editor/timeline.tsx | 53 ++++++++++++++++------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index c1b8f3e8..1ecbe545 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -69,6 +69,7 @@ export function Timeline() { } = usePlaybackStore(); const [isDragOver, setIsDragOver] = useState(false); const [isProcessing, setIsProcessing] = useState(false); + const [progress, setProgress] = useState(0); const [zoomLevel, setZoomLevel] = useState(1); const dragCounterRef = useRef(0); const timelineRef = useRef(null); @@ -334,8 +335,12 @@ export function Timeline() { } else if (e.dataTransfer.files?.length > 0) { // Handle file drops by creating new tracks setIsProcessing(true); + setProgress(0); try { - const processedItems = await processMediaFiles(e.dataTransfer.files); + const processedItems = await processMediaFiles( + e.dataTransfer.files, + (p) => setProgress(p) + ); for (const processedItem of processedItems) { addMediaItem(processedItem); const currentMediaItems = useMediaStore.getState().mediaItems; @@ -363,6 +368,7 @@ export function Timeline() { toast.error("Failed to process dropped files"); } finally { setIsProcessing(false); + setProgress(0); } } }; @@ -594,10 +600,11 @@ export function Timeline() {
{/* Time Display */} -
- {currentTime.toFixed(1)}s / {duration.toFixed(1)}s +
+ {currentTime.toFixed(1)}s / {duration.toFixed(1)}s
{/* Test Clip Button - for debugging */} @@ -946,14 +953,12 @@ export function Timeline() { )} {isDragOver && ( -
-
Drop media here to add a new track
+
+
+ {isProcessing + ? `Processing ${progress}%` + : "Drop media here to add to timeline"} +
)}
@@ -1630,18 +1635,18 @@ function TimelineTrackContent({ } if (mediaItem.type === "audio") { - return ( -
-
- + return ( +
+
+ +
-
- ); - } + ); + } // Fallback for videos without thumbnails return ( -- cgit v1.3.1 From 0723623eafcfce9aba2c7e6b4a9343b6e38dacf1 Mon Sep 17 00:00:00 2001 From: DevloperAmanSingh Date: Wed, 25 Jun 2025 21:41:23 +0530 Subject: feat: add progress callback to media file processing function --- apps/web/src/lib/media-processing.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/web/src/lib/media-processing.ts b/apps/web/src/lib/media-processing.ts index d241f411..f0f80fe4 100644 --- a/apps/web/src/lib/media-processing.ts +++ b/apps/web/src/lib/media-processing.ts @@ -11,11 +11,15 @@ import { export interface ProcessedMediaItem extends Omit {} export async function processMediaFiles( - files: FileList | File[] + files: FileList | File[], + onProgress?: (progress: number) => void ): Promise { const fileArray = Array.from(files); const processedItems: ProcessedMediaItem[] = []; + const total = fileArray.length; + let completed = 0; + for (const file of fileArray) { const fileType = getFileType(file); @@ -57,6 +61,12 @@ export async function processMediaFiles( duration, aspectRatio, }); + + completed += 1; + if (onProgress) { + const percent = Math.round((completed / total) * 100); + onProgress(percent); + } } catch (error) { console.error("Error processing file:", file.name, error); toast.error(`Failed to process ${file.name}`); -- cgit v1.3.1 From 5977a5d25324c07c015eee90e4e6869277d9423f Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 25 Jun 2025 18:19:28 +0200 Subject: fix: netlify config --- netlify.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netlify.toml b/netlify.toml index a895edd5..6e360f20 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,6 +1,6 @@ [build] - base = "." - command = "bun install && bunx turbo build --filter=opencut" + base = "apps/web" + command = "bun install && bun run build" publish = "apps/web/.next" [[plugins]] -- cgit v1.3.1 From 181d3fca06b49ff2f1252bed387af3f5817bb406 Mon Sep 17 00:00:00 2001 From: DevloperAmanSingh Date: Wed, 25 Jun 2025 21:50:12 +0530 Subject: fix: improve UI responsiveness during media file processing --- apps/web/src/lib/media-processing.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/web/src/lib/media-processing.ts b/apps/web/src/lib/media-processing.ts index f0f80fe4..daf22b34 100644 --- a/apps/web/src/lib/media-processing.ts +++ b/apps/web/src/lib/media-processing.ts @@ -62,6 +62,9 @@ export async function processMediaFiles( aspectRatio, }); + // Yield back to the event loop to keep the UI responsive + await new Promise((resolve) => setTimeout(resolve, 0)); + completed += 1; if (onProgress) { const percent = Math.round((completed / total) * 100); -- cgit v1.3.1 From 94d39dfa7e9ee38d31c052c4aec8c4cba902cd8a Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 25 Jun 2025 18:46:22 +0200 Subject: fix: netlify config --- apps/web/netlify.toml | 8 ++++++++ netlify.toml | 13 ------------- 2 files changed, 8 insertions(+), 13 deletions(-) create mode 100644 apps/web/netlify.toml delete mode 100644 netlify.toml diff --git a/apps/web/netlify.toml b/apps/web/netlify.toml new file mode 100644 index 00000000..7422ce98 --- /dev/null +++ b/apps/web/netlify.toml @@ -0,0 +1,8 @@ +[[plugins]] + package = "@netlify/plugin-nextjs" + +[[redirects]] + from = "https://appcut.app/*" + to = "https://opencut.app/:splat" + status = 301 + force = true \ No newline at end of file diff --git a/netlify.toml b/netlify.toml deleted file mode 100644 index 6e360f20..00000000 --- a/netlify.toml +++ /dev/null @@ -1,13 +0,0 @@ -[build] - base = "apps/web" - command = "bun install && bun run build" - publish = "apps/web/.next" - -[[plugins]] - package = "@netlify/plugin-nextjs" - -[[redirects]] - from = "https://appcut.app/*" - to = "https://opencut.app/:splat" - status = 301 - force = true \ No newline at end of file -- cgit v1.3.1 From 0f343dfbdabdbd3824750c37f5d1fd2973e7cd62 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 25 Jun 2025 18:57:52 +0200 Subject: i hate netlify --- apps/web/netlify.toml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/web/netlify.toml b/apps/web/netlify.toml index 7422ce98..23f9486e 100644 --- a/apps/web/netlify.toml +++ b/apps/web/netlify.toml @@ -1,8 +1,25 @@ +# Build configuration +[build] + # Base directory - where Netlify installs dependencies and runs build + base = "../.." + + # Publish directory - relative to base directory + publish = "apps/web/.next" + + # Build command + command = "cd apps/web && npm run build" + +# Next.js plugin [[plugins]] package = "@netlify/plugin-nextjs" +# Redirects for domain migration [[redirects]] from = "https://appcut.app/*" to = "https://opencut.app/:splat" status = 301 - force = true \ No newline at end of file + force = true + +# Functions directory +[functions] + directory = "apps/web/netlify/functions" \ No newline at end of file -- cgit v1.3.1 From 3bb640c453df669247b2aae328adb5e56cc46998 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 25 Jun 2025 19:06:41 +0200 Subject: oh god (netlify) --- apps/web/netlify.toml | 25 ------------------------- netlify.toml | 10 ++++++++++ 2 files changed, 10 insertions(+), 25 deletions(-) delete mode 100644 apps/web/netlify.toml create mode 100644 netlify.toml diff --git a/apps/web/netlify.toml b/apps/web/netlify.toml deleted file mode 100644 index 23f9486e..00000000 --- a/apps/web/netlify.toml +++ /dev/null @@ -1,25 +0,0 @@ -# Build configuration -[build] - # Base directory - where Netlify installs dependencies and runs build - base = "../.." - - # Publish directory - relative to base directory - publish = "apps/web/.next" - - # Build command - command = "cd apps/web && npm run build" - -# Next.js plugin -[[plugins]] - package = "@netlify/plugin-nextjs" - -# Redirects for domain migration -[[redirects]] - from = "https://appcut.app/*" - to = "https://opencut.app/:splat" - status = 301 - force = true - -# Functions directory -[functions] - directory = "apps/web/netlify/functions" \ No newline at end of file diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 00000000..ec820937 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,10 @@ +# Next.js plugin +[[plugins]] + package = "@netlify/plugin-nextjs" + +# Redirects for domain migration +[[redirects]] + from = "https://appcut.app/*" + to = "https://opencut.app/:splat" + status = 301 + force = true \ No newline at end of file -- cgit v1.3.1 From de3d3e210e3e9d8b19eb15cdc61fdfe030ca2432 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 25 Jun 2025 19:23:01 +0200 Subject: fix: update heading in hero for clarity about first oss video editor --- apps/web/src/components/landing/hero.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/landing/hero.tsx b/apps/web/src/components/landing/hero.tsx index fadc043a..a4341493 100644 --- a/apps/web/src/components/landing/hero.tsx +++ b/apps/web/src/components/landing/hero.tsx @@ -96,10 +96,9 @@ export function Hero({ signupCount }: HeroProps) { transition={{ delay: 0.2, duration: 0.8 }} className="inline-block font-bold tracking-tighter text-4xl md:text-[4rem]" > -

The First Open Source

+

The open source

- Online -
+
frame - + Video Editor
-- cgit v1.3.1 From bf64ec11334c39fde04c09b44460a489bcab1f2d Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 25 Jun 2025 19:24:05 +0200 Subject: style: make join waitlist responsive --- 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 a4341493..af33a3dd 100644 --- a/apps/web/src/components/landing/hero.tsx +++ b/apps/web/src/components/landing/hero.tsx @@ -129,7 +129,7 @@ export function Hero({ signupCount }: HeroProps) { animate={{ opacity: 1 }} transition={{ delay: 0.6, duration: 0.8 }} > -
+ Date: Wed, 25 Jun 2025 19:51:50 +0200 Subject: feat: improve clips dragging experience --- apps/web/src/components/editor/timeline.tsx | 70 +++++++++++++---------------- 1 file changed, 32 insertions(+), 38 deletions(-) diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index 1ecbe545..584a45f7 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -1241,7 +1241,18 @@ function TimelineTrackContent({ }; const handleClipDragStart = (e: React.DragEvent, clip: any) => { - const dragData = { clipId: clip.id, trackId: track.id, name: clip.name }; + // Calculate the offset from the left edge of the clip to where the user clicked + const clipElement = e.currentTarget.parentElement as HTMLElement; + const clipRect = clipElement.getBoundingClientRect(); + const clickOffsetX = e.clientX - clipRect.left; + const clickOffsetTime = clickOffsetX / (50 * zoomLevel); + + const dragData = { + clipId: clip.id, + trackId: track.id, + name: clip.name, + clickOffsetTime: clickOffsetTime, + }; e.dataTransfer.setData( "application/x-timeline-clip", @@ -1467,7 +1478,11 @@ function TimelineTrackContent({ ); if (!timelineClipData) return; - const { clipId, trackId: fromTrackId } = JSON.parse(timelineClipData); + const { + clipId, + trackId: fromTrackId, + clickOffsetTime = 0, + } = JSON.parse(timelineClipData); // Find the clip being moved const sourceTrack = tracks.find( @@ -1480,10 +1495,17 @@ function TimelineTrackContent({ return; } + // Adjust position based on where user clicked on the clip + const adjustedStartTime = snappedTime - clickOffsetTime; + const finalStartTime = Math.max( + 0, + Math.round(adjustedStartTime * 10) / 10 + ); + // Check for overlaps with existing clips (excluding the moving clip itself) const movingClipDuration = movingClip.duration - movingClip.trimStart - movingClip.trimEnd; - const movingClipEnd = snappedTime + movingClipDuration; + const movingClipEnd = finalStartTime + movingClipDuration; const hasOverlap = track.clips.some((existingClip) => { // Skip the clip being moved if it's on the same track @@ -1498,7 +1520,7 @@ function TimelineTrackContent({ existingClip.trimEnd); // Check if clips overlap - return snappedTime < existingEnd && movingClipEnd > existingStart; + return finalStartTime < existingEnd && movingClipEnd > existingStart; }); if (hasOverlap) { @@ -1510,12 +1532,12 @@ function TimelineTrackContent({ if (fromTrackId === track.id) { // Moving within same track - updateClipStartTime(track.id, clipId, snappedTime); + updateClipStartTime(track.id, clipId, finalStartTime); } else { // Moving to different track moveClipToTrack(fromTrackId, track.id, clipId); requestAnimationFrame(() => { - updateClipStartTime(track.id, clipId, snappedTime); + updateClipStartTime(track.id, clipId, finalStartTime); }); } } else if (hasMediaItem) { @@ -1612,6 +1634,7 @@ function TimelineTrackContent({ src={mediaItem.url} alt={mediaItem.name} className="w-full h-full object-cover" + draggable={false} />
); @@ -1625,6 +1648,7 @@ function TimelineTrackContent({ src={mediaItem.thumbnailUrl} alt={mediaItem.name} className="w-full h-full object-cover rounded-sm" + draggable={false} />
@@ -1684,13 +1708,7 @@ function TimelineTrackContent({ return (
{ e.preventDefault(); // Only show track menu if we didn't click on a clip @@ -1744,7 +1762,7 @@ function TimelineTrackContent({ return (
{ e.stopPropagation(); @@ -1835,30 +1853,6 @@ function TimelineTrackContent({
); })} - - {/* Drop position indicator */} - {isDraggedOver && dropPosition !== null && ( -
-
-
-
- {wouldOverlap ? "⚠️" : ""} - {dropPosition.toFixed(1)}s -
-
- )} )}
-- cgit v1.3.1 From ebcb898e4e70805b556a25ac0a293d67a800fc59 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 25 Jun 2025 21:02:34 +0200 Subject: feat: use mouse-based dragging for clips fixes an issue where dragging a clip would create a duplicate instead of dragging clip directly, and gives us more control moved the drag to a hook for cleaner code --- apps/web/src/components/editor/timeline.tsx | 75 +++++----- apps/web/src/hooks/use-drag-clip.ts | 213 ++++++++++++++++++++++++++++ 2 files changed, 249 insertions(+), 39 deletions(-) create mode 100644 apps/web/src/hooks/use-drag-clip.ts diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index 584a45f7..31e0dd50 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -25,6 +25,7 @@ import { import { useTimelineStore, type TimelineTrack } from "@/stores/timeline-store"; import { useMediaStore } from "@/stores/media-store"; import { usePlaybackStore } from "@/stores/playback-store"; +import { useDragClip } from "@/hooks/use-drag-clip"; import { processMediaFiles } from "@/lib/media-processing"; import { toast } from "sonner"; import { useState, useRef, useEffect, useCallback } from "react"; @@ -1163,9 +1164,19 @@ function TimelineTrackContent({ deselectClip, } = useTimelineStore(); const { currentTime } = usePlaybackStore(); + + // Mouse-based drag hook + const { + isDragging, + draggedClipId, + startDrag, + endDrag, + getDraggedClipPosition, + isValidDropTarget, + timelineRef, + } = useDragClip(zoomLevel); const [isDropping, setIsDropping] = useState(false); const [dropPosition, setDropPosition] = useState(null); - const [isDraggedOver, setIsDraggedOver] = useState(false); const [wouldOverlap, setWouldOverlap] = useState(false); const [resizing, setResizing] = useState<{ clipId: string; @@ -1240,37 +1251,14 @@ function TimelineTrackContent({ setResizing(null); }; - const handleClipDragStart = (e: React.DragEvent, clip: any) => { + const handleClipMouseDown = (e: React.MouseEvent, clip: any) => { // Calculate the offset from the left edge of the clip to where the user clicked const clipElement = e.currentTarget.parentElement as HTMLElement; const clipRect = clipElement.getBoundingClientRect(); const clickOffsetX = e.clientX - clipRect.left; const clickOffsetTime = clickOffsetX / (50 * zoomLevel); - const dragData = { - clipId: clip.id, - trackId: track.id, - name: clip.name, - clickOffsetTime: clickOffsetTime, - }; - - e.dataTransfer.setData( - "application/x-timeline-clip", - JSON.stringify(dragData) - ); - e.dataTransfer.effectAllowed = "move"; - - // Add visual feedback to the dragged element - const target = e.currentTarget.parentElement as HTMLElement; - target.style.opacity = "0.5"; - target.style.transform = "scale(0.95)"; - }; - - const handleClipDragEnd = (e: React.DragEvent) => { - // Reset visual feedback - const target = e.currentTarget.parentElement as HTMLElement; - target.style.opacity = ""; - target.style.transform = ""; + startDrag(e, clip.id, track.id, clip.startTime, clickOffsetTime); }; const handleTrackDragOver = (e: React.DragEvent) => { @@ -1388,14 +1376,12 @@ function TimelineTrackContent({ if (wouldOverlap) { e.dataTransfer.dropEffect = "none"; - setIsDraggedOver(true); setWouldOverlap(true); setDropPosition(Math.round(dropTime * 10) / 10); return; } e.dataTransfer.dropEffect = hasTimelineClip ? "move" : "copy"; - setIsDraggedOver(true); setWouldOverlap(false); setDropPosition(Math.round(dropTime * 10) / 10); }; @@ -1414,7 +1400,6 @@ function TimelineTrackContent({ dragCounterRef.current++; setIsDropping(true); - setIsDraggedOver(true); }; const handleTrackDragLeave = (e: React.DragEvent) => { @@ -1433,7 +1418,6 @@ function TimelineTrackContent({ if (dragCounterRef.current === 0) { setIsDropping(false); - setIsDraggedOver(false); setWouldOverlap(false); setDropPosition(null); } @@ -1446,7 +1430,6 @@ function TimelineTrackContent({ // Reset all drag states dragCounterRef.current = 0; setIsDropping(false); - setIsDraggedOver(false); setWouldOverlap(false); const currentDropPosition = dropPosition; setDropPosition(null); @@ -1726,10 +1709,18 @@ function TimelineTrackContent({ onDragLeave={handleTrackDragLeave} onDrop={handleTrackDrop} onMouseMove={handleResizeMove} - onMouseUp={handleResizeEnd} + onMouseUp={(e) => { + handleResizeEnd(); + if (isDragging) { + endDrag(track.id); + } + }} onMouseLeave={handleResizeEnd} > -
+
{track.clips.length === 0 ? (
c.trackId === track.id && c.clipId === clip.id ); + + const isBeingDragged = draggedClipId === clip.id; return (
{ e.stopPropagation(); @@ -1808,10 +1807,8 @@ function TimelineTrackContent({ /> {/* Clip content */}
handleClipDragStart(e, clip)} - onDragEnd={handleClipDragEnd} + className={`flex-1 relative ${isBeingDragged ? "cursor-grabbing" : "cursor-grab"}`} + onMouseDown={(e) => handleClipMouseDown(e, clip)} > {renderClipContent(clip)} {/* Clip options menu */} diff --git a/apps/web/src/hooks/use-drag-clip.ts b/apps/web/src/hooks/use-drag-clip.ts new file mode 100644 index 00000000..7eb24daa --- /dev/null +++ b/apps/web/src/hooks/use-drag-clip.ts @@ -0,0 +1,213 @@ +import { useState, useEffect, useCallback, useRef } from "react"; +import { useTimelineStore } from "@/stores/timeline-store"; + +interface DragState { + isDragging: boolean; + clipId: string | null; + trackId: string | null; + startMouseX: number; + startClipTime: number; + clickOffsetTime: number; + currentTime: number; +} + +export function useDragClip(zoomLevel: number) { + const { tracks, updateClipStartTime, moveClipToTrack } = useTimelineStore(); + + const [dragState, setDragState] = useState({ + isDragging: false, + clipId: null, + trackId: null, + startMouseX: 0, + startClipTime: 0, + clickOffsetTime: 0, + currentTime: 0, + }); + + const timelineRef = useRef(null); + + const startDrag = useCallback( + ( + e: React.MouseEvent, + clipId: string, + trackId: string, + clipStartTime: number, + clickOffsetTime: number + ) => { + e.preventDefault(); + e.stopPropagation(); + + setDragState({ + isDragging: true, + clipId, + trackId, + startMouseX: e.clientX, + startClipTime: clipStartTime, + clickOffsetTime, + currentTime: clipStartTime, + }); + }, + [] + ); + + const updateDrag = useCallback( + (e: MouseEvent) => { + if (!dragState.isDragging || !timelineRef.current) return; + + const timelineRect = timelineRef.current.getBoundingClientRect(); + const mouseX = e.clientX - timelineRect.left; + const mouseTime = Math.max(0, mouseX / (50 * zoomLevel)); + const adjustedTime = Math.max(0, mouseTime - dragState.clickOffsetTime); + const snappedTime = Math.round(adjustedTime * 10) / 10; + + setDragState((prev) => ({ + ...prev, + currentTime: snappedTime, + })); + }, + [dragState.isDragging, dragState.clickOffsetTime, zoomLevel] + ); + + const endDrag = useCallback( + (targetTrackId?: string) => { + if (!dragState.isDragging || !dragState.clipId || !dragState.trackId) + return; + + const finalTrackId = targetTrackId || dragState.trackId; + const finalTime = dragState.currentTime; + + // Check for overlaps + const sourceTrack = tracks.find((t) => t.id === dragState.trackId); + const targetTrack = tracks.find((t) => t.id === finalTrackId); + const movingClip = sourceTrack?.clips.find( + (c) => c.id === dragState.clipId + ); + + if (!movingClip || !targetTrack) { + setDragState((prev) => ({ ...prev, isDragging: false })); + return; + } + + const movingClipDuration = + movingClip.duration - movingClip.trimStart - movingClip.trimEnd; + const movingClipEnd = finalTime + movingClipDuration; + + const hasOverlap = targetTrack.clips.some((existingClip) => { + // Skip the clip being moved if it's on the same track + if ( + dragState.trackId === finalTrackId && + existingClip.id === dragState.clipId + ) { + return false; + } + + const existingStart = existingClip.startTime; + const existingEnd = + existingClip.startTime + + (existingClip.duration - + existingClip.trimStart - + existingClip.trimEnd); + + return finalTime < existingEnd && movingClipEnd > existingStart; + }); + + if (!hasOverlap) { + if (dragState.trackId === finalTrackId) { + // Moving within same track + updateClipStartTime(finalTrackId, dragState.clipId!, finalTime); + } else { + // Moving to different track + moveClipToTrack(dragState.trackId!, finalTrackId, dragState.clipId!); + requestAnimationFrame(() => { + updateClipStartTime(finalTrackId, dragState.clipId!, finalTime); + }); + } + } + + setDragState({ + isDragging: false, + clipId: null, + trackId: null, + startMouseX: 0, + startClipTime: 0, + clickOffsetTime: 0, + currentTime: 0, + }); + }, + [dragState, tracks, updateClipStartTime, moveClipToTrack] + ); + + const cancelDrag = useCallback(() => { + setDragState({ + isDragging: false, + clipId: null, + trackId: null, + startMouseX: 0, + startClipTime: 0, + clickOffsetTime: 0, + currentTime: 0, + }); + }, []); + + // Global mouse events + useEffect(() => { + if (!dragState.isDragging) return; + + const handleMouseMove = (e: MouseEvent) => updateDrag(e); + const handleMouseUp = () => endDrag(); + const handleEscape = (e: KeyboardEvent) => { + if (e.key === "Escape") cancelDrag(); + }; + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + document.addEventListener("keydown", handleEscape); + + return () => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + document.removeEventListener("keydown", handleEscape); + }; + }, [dragState.isDragging, updateDrag, endDrag, cancelDrag]); + + const getDraggedClipPosition = useCallback( + (clipId: string) => { + if (dragState.isDragging && dragState.clipId === clipId) { + return dragState.currentTime; + } + return null; + }, + [dragState] + ); + + const isValidDropTarget = useCallback( + (trackId: string) => { + if (!dragState.isDragging) return false; + + const sourceTrack = tracks.find((t) => t.id === dragState.trackId); + const targetTrack = tracks.find((t) => t.id === trackId); + + if (!sourceTrack || !targetTrack) return false; + + // For now, allow drops on same track type + return sourceTrack.type === targetTrack.type; + }, + [dragState.isDragging, dragState.trackId, tracks] + ); + + return { + // State + isDragging: dragState.isDragging, + draggedClipId: dragState.clipId, + + // Methods + startDrag, + endDrag, + cancelDrag, + getDraggedClipPosition, + isValidDropTarget, + + // Refs + timelineRef, + }; +} -- cgit v1.3.1 From d1e313450db64c95237ebcefe928dbde39043fee Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 25 Jun 2025 21:06:34 +0200 Subject: fix: show clip trim handles on focus rather than hover --- apps/web/src/components/editor/timeline.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index 31e0dd50..b9e78839 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -1802,7 +1802,7 @@ function TimelineTrackContent({ > {/* Left trim handle */}
handleResizeStart(e, clip.id, "left")} /> {/* Clip content */} @@ -1844,7 +1844,7 @@ function TimelineTrackContent({
{/* Right trim handle */}
handleResizeStart(e, clip.id, "right")} />
-- cgit v1.3.1 From f5c546d416a84d577f45a1fdb79e5e67e5a9495c Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 25 Jun 2025 21:08:52 +0200 Subject: fix: clip getting deselected after a drag --- apps/web/src/components/editor/timeline.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index b9e78839..7e2089a7 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -1251,6 +1251,8 @@ function TimelineTrackContent({ setResizing(null); }; + const [justFinishedDrag, setJustFinishedDrag] = useState(false); + const handleClipMouseDown = (e: React.MouseEvent, clip: any) => { // Calculate the offset from the left edge of the clip to where the user clicked const clipElement = e.currentTarget.parentElement as HTMLElement; @@ -1261,6 +1263,16 @@ function TimelineTrackContent({ startDrag(e, clip.id, track.id, clip.startTime, clickOffsetTime); }; + // Reset drag flag when drag ends + useEffect(() => { + if (!isDragging && justFinishedDrag) { + const timer = setTimeout(() => setJustFinishedDrag(false), 50); + return () => clearTimeout(timer); + } else if (isDragging && !justFinishedDrag) { + setJustFinishedDrag(true); + } + }, [isDragging, justFinishedDrag]); + const handleTrackDragOver = (e: React.DragEvent) => { e.preventDefault(); @@ -1766,6 +1778,11 @@ function TimelineTrackContent({ onClick={(e) => { e.stopPropagation(); + // Don't handle click if we just finished dragging + if (justFinishedDrag) { + return; + } + // Close context menu if it's open if (contextMenu) { setContextMenu(null); -- cgit v1.3.1 From 2775ac427de8bd9c2bb6323a216f1875038f68d8 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 25 Jun 2025 21:10:40 +0200 Subject: fix: remove hover effects on clip drag --- apps/web/src/components/editor/timeline.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index 7e2089a7..18e1338e 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -1773,7 +1773,7 @@ function TimelineTrackContent({ return (
{ e.stopPropagation(); -- cgit v1.3.1 From 926aebe00423b5b0b2fd0d8cf0ee97c6e85ec1fd Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 25 Jun 2025 21:22:08 +0200 Subject: fix: make whole clip draggable --- apps/web/src/components/editor/timeline.tsx | 53 ++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index 18e1338e..210a943d 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -1254,8 +1254,24 @@ function TimelineTrackContent({ const [justFinishedDrag, setJustFinishedDrag] = useState(false); const handleClipMouseDown = (e: React.MouseEvent, clip: any) => { + // Handle selection first + if (!justFinishedDrag) { + 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 not selected, select it (replacing other selections) + selectClip(track.id, clip.id, false); + } + // Note: Don't deselect if already selected, as user might want to drag + } + // Calculate the offset from the left edge of the clip to where the user clicked - const clipElement = e.currentTarget.parentElement as HTMLElement; + const clipElement = e.currentTarget as HTMLElement; const clipRect = clipElement.getBoundingClientRect(); const clickOffsetX = e.clientX - clipRect.left; const clickOffsetTime = clickOffsetX / (50 * zoomLevel); @@ -1773,8 +1789,9 @@ function TimelineTrackContent({ return (
handleClipMouseDown(e, clip)} onClick={(e) => { e.stopPropagation(); @@ -1789,19 +1806,14 @@ function TimelineTrackContent({ return; // Don't handle selection when closing context menu } + // Only handle deselection here (selection is handled in mouseDown) 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 + if (isSelected && !e.metaKey && !e.ctrlKey && !e.shiftKey) { + // If clip is already selected and no modifier keys, deselect it deselectClip(track.id, clip.id); - } else { - // If clip is not selected, select it (replacing other selections) - selectClip(track.id, clip.id, false); } }} tabIndex={0} @@ -1820,13 +1832,13 @@ function TimelineTrackContent({ {/* Left trim handle */}
handleResizeStart(e, clip.id, "left")} + onMouseDown={(e) => { + e.stopPropagation(); // Prevent triggering clip drag + handleResizeStart(e, clip.id, "left"); + }} /> {/* Clip content */} -
handleClipMouseDown(e, clip)} - > +
{renderClipContent(clip)} {/* Clip options menu */}
@@ -1835,11 +1847,15 @@ function TimelineTrackContent({ size="icon" className="opacity-0 group-hover:opacity-100 transition-opacity" onClick={() => setClipMenuOpen(clip.id)} + onMouseDown={(e) => e.stopPropagation()} > {clipMenuOpen === clip.id && ( -
+
e.stopPropagation()} + > - )} - - - - -
- - {/* Preview Area */} -
-
- {activeClips.length === 0 ? ( -
- {tracks.length === 0 - ? "Drop media to start editing" - : "No clips at current time"} -
- ) : ( - activeClips.map((clipData, index) => renderClip(clipData, index)) - )} -
-
- - {/* Debug Info Panel - Conditionally rendered */} - {showDebug && ( -
-
- Debug: Active Clips ({activeClips.length}) -
-
- {activeClips.map((clipData, index) => ( -
- - {index + 1} - - {clipData.clip.name} - - ({clipData.mediaItem?.type || "test"}) - -
- ))} - {activeClips.length === 0 && ( - No active clips - )} -
-
- )} -
- ); -} +"use client"; + +import { + useTimelineStore, + type TimelineClip, + type TimelineTrack, +} from "@/stores/timeline-store"; +import { useMediaStore, type MediaItem } from "@/stores/media-store"; +import { usePlaybackStore } from "@/stores/playback-store"; +import { VideoPlayer } from "@/components/ui/video-player"; +import { Button } from "@/components/ui/button"; +import { Play, Pause, Volume2, VolumeX } from "lucide-react"; +import { useState, useRef, useEffect } from "react"; + +// Debug flag - set to false to hide active clips info +const SHOW_DEBUG_INFO = process.env.NODE_ENV === "development"; + +interface ActiveClip { + clip: TimelineClip; + track: TimelineTrack; + mediaItem: MediaItem | null; +} + +export function PreviewPanel() { + const { tracks } = useTimelineStore(); + const { mediaItems } = useMediaStore(); + const { isPlaying, toggle, currentTime, muted, toggleMute, volume } = + usePlaybackStore(); + const [canvasSize, setCanvasSize] = useState({ width: 1920, height: 1080 }); + const [showDebug, setShowDebug] = useState(SHOW_DEBUG_INFO); + const previewRef = useRef(null); + const containerRef = useRef(null); + const [previewDimensions, setPreviewDimensions] = useState({ + width: 0, + height: 0, + }); + + // Calculate optimal preview size that fits in container while maintaining aspect ratio + useEffect(() => { + const updatePreviewSize = () => { + if (!containerRef.current) return; + + const container = containerRef.current.getBoundingClientRect(); + const targetRatio = canvasSize.width / canvasSize.height; + const containerRatio = container.width / container.height; + + let width, height; + + if (containerRatio > targetRatio) { + // Container is wider - constrain by height + height = container.height; + width = height * targetRatio; + } else { + // Container is taller - constrain by width + width = container.width; + height = width / targetRatio; + } + + setPreviewDimensions({ width, height }); + }; + + updatePreviewSize(); + + const resizeObserver = new ResizeObserver(updatePreviewSize); + if (containerRef.current) { + resizeObserver.observe(containerRef.current); + } + + return () => resizeObserver.disconnect(); + }, [canvasSize.width, canvasSize.height]); + + // Get active clips at current time + const getActiveClips = (): ActiveClip[] => { + const activeClips: ActiveClip[] = []; + + tracks.forEach((track) => { + track.clips.forEach((clip) => { + const clipStart = clip.startTime; + const clipEnd = + clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); + + if (currentTime >= clipStart && currentTime < clipEnd) { + const mediaItem = + clip.mediaId === "test" + ? null // Test clips don't have a real media item + : mediaItems.find((item) => item.id === clip.mediaId) || null; + + activeClips.push({ clip, track, mediaItem }); + } + }); + }); + + return activeClips; + }; + + const activeClips = getActiveClips(); + + // Render a clip + const renderClip = (clipData: ActiveClip, index: number) => { + const { clip, mediaItem } = clipData; + + // Test clips + if (!mediaItem || clip.mediaId === "test") { + return ( +
+
+
🎬
+

{clip.name}

+
+
+ ); + } + + // Video clips + if (mediaItem.type === "video") { + return ( +
+ +
+ ); + } + + // Image clips + if (mediaItem.type === "image") { + return ( +
+ {mediaItem.name} +
+ ); + } + + // Audio clips (visual representation) + if (mediaItem.type === "audio") { + return ( +
+
+
🎵
+

{mediaItem.name}

+
+
+ ); + } + + return null; + }; + + // Canvas presets + const canvasPresets = [ + { name: "16:9 HD", width: 1920, height: 1080 }, + { name: "16:9 4K", width: 3840, height: 2160 }, + { name: "9:16 Mobile", width: 1080, height: 1920 }, + { name: "1:1 Square", width: 1080, height: 1080 }, + { name: "4:3 Standard", width: 1440, height: 1080 }, + ]; + + return ( +
+ {/* Controls */} +
+ Canvas: + + + {/* Debug Toggle - Only show in development */} + {SHOW_DEBUG_INFO && ( + + )} + + + + +
+ + {/* Preview Area */} +
+
+ {activeClips.length === 0 ? ( +
+ {tracks.length === 0 + ? "Drop media to start editing" + : "No clips at current time"} +
+ ) : ( + activeClips.map((clipData, index) => renderClip(clipData, index)) + )} +
+
+ + {/* Debug Info Panel - Conditionally rendered */} + {showDebug && ( +
+
+ Debug: Active Clips ({activeClips.length}) +
+
+ {activeClips.map((clipData, index) => ( +
+ + {index + 1} + + {clipData.clip.name} + + ({clipData.mediaItem?.type || "test"}) + +
+ ))} + {activeClips.length === 0 && ( + No active clips + )} +
+
+ )} +
+ ); +} -- cgit v1.3.1 From 3eac1bcb0b1f006fb6a7ae8a9aa76de4ad4edf1d Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 25 Jun 2025 23:05:14 +0200 Subject: refactor: move timeline clip into its separate component and type into /types --- apps/web/src/components/editor/timeline-clip.tsx | 277 ++++++++++++++++ apps/web/src/components/editor/timeline.tsx | 384 ++++------------------- apps/web/src/types/timeline.ts | 27 ++ 3 files changed, 361 insertions(+), 327 deletions(-) create mode 100644 apps/web/src/components/editor/timeline-clip.tsx create mode 100644 apps/web/src/types/timeline.ts diff --git a/apps/web/src/components/editor/timeline-clip.tsx b/apps/web/src/components/editor/timeline-clip.tsx new file mode 100644 index 00000000..1bb2b77f --- /dev/null +++ b/apps/web/src/components/editor/timeline-clip.tsx @@ -0,0 +1,277 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "../ui/button"; +import { MoreVertical, Scissors, Trash2 } from "lucide-react"; +import { useMediaStore } from "@/stores/media-store"; +import { useTimelineStore } from "@/stores/timeline-store"; +import { usePlaybackStore } from "@/stores/playback-store"; +import { useDragClip } from "@/hooks/use-drag-clip"; +import AudioWaveform from "./audio-waveform"; +import { toast } from "sonner"; +import { TimelineClipProps, ResizeState } from "@/types/timeline"; + +export function TimelineClip({ + clip, + track, + zoomLevel, + isSelected, + onContextMenu, + onClipMouseDown, + onClipClick, +}: TimelineClipProps) { + const { mediaItems } = useMediaStore(); + const { updateClipTrim, addClipToTrack, removeClipFromTrack } = + useTimelineStore(); + const { currentTime } = usePlaybackStore(); + const { draggedClipId, getDraggedClipPosition } = + useDragClip(zoomLevel); + + const [resizing, setResizing] = useState(null); + const [clipMenuOpen, setClipMenuOpen] = useState(false); + + const effectiveDuration = clip.duration - clip.trimStart - clip.trimEnd; + const clipWidth = Math.max(80, effectiveDuration * 50 * zoomLevel); + + // Use real-time position during drag, otherwise use stored position + const dragPosition = getDraggedClipPosition(clip.id); + const clipStartTime = dragPosition !== null ? dragPosition : clip.startTime; + const clipLeft = clipStartTime * 50 * zoomLevel; + + const isBeingDragged = draggedClipId === clip.id; + + const getTrackColor = (type: string) => { + switch (type) { + case "video": + return "bg-blue-500/20 border-blue-500/30"; + case "audio": + return "bg-green-500/20 border-green-500/30"; + case "effects": + return "bg-purple-500/20 border-purple-500/30"; + default: + return "bg-gray-500/20 border-gray-500/30"; + } + }; + + const handleResizeStart = ( + e: React.MouseEvent, + clipId: string, + side: "left" | "right" + ) => { + e.stopPropagation(); + e.preventDefault(); + + setResizing({ + clipId, + side, + startX: e.clientX, + initialTrimStart: clip.trimStart, + initialTrimEnd: clip.trimEnd, + }); + }; + + const updateTrimFromMouseMove = (e: { clientX: number }) => { + if (!resizing) return; + + const deltaX = e.clientX - resizing.startX; + const deltaTime = deltaX / (50 * zoomLevel); + + if (resizing.side === "left") { + const newTrimStart = Math.max( + 0, + Math.min( + clip.duration - clip.trimEnd - 0.1, + resizing.initialTrimStart + deltaTime + ) + ); + updateClipTrim(track.id, clip.id, newTrimStart, clip.trimEnd); + } else { + const newTrimEnd = Math.max( + 0, + Math.min( + clip.duration - clip.trimStart - 0.1, + resizing.initialTrimEnd - deltaTime + ) + ); + updateClipTrim(track.id, clip.id, clip.trimStart, newTrimEnd); + } + }; + + const handleResizeMove = (e: React.MouseEvent) => { + updateTrimFromMouseMove(e); + }; + + const handleResizeEnd = () => { + setResizing(null); + }; + + const handleDeleteClip = () => { + removeClipFromTrack(track.id, clip.id); + setClipMenuOpen(false); + }; + + const handleSplitClip = () => { + // Use current playback time as split point + const splitTime = currentTime; + // Only split if splitTime is within the clip's effective range + const effectiveStart = clip.startTime; + const effectiveEnd = + clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); + + if (splitTime <= effectiveStart || splitTime >= effectiveEnd) { + toast.error("Playhead must be within clip to split"); + return; + } + + const firstDuration = splitTime - effectiveStart; + const secondDuration = effectiveEnd - splitTime; + + // First part: adjust original clip + updateClipTrim( + track.id, + clip.id, + clip.trimStart, + clip.trimEnd + secondDuration + ); + + // Second part: add new clip after split + addClipToTrack(track.id, { + mediaId: clip.mediaId, + name: clip.name + " (cut)", + duration: clip.duration, + startTime: splitTime, + trimStart: clip.trimStart + firstDuration, + trimEnd: clip.trimEnd, + }); + + setClipMenuOpen(false); + toast.success("Clip split successfully"); + }; + + const renderClipContent = () => { + const mediaItem = mediaItems.find((item) => item.id === clip.mediaId); + + if (!mediaItem) { + return ( + {clip.name} + ); + } + + if (mediaItem.type === "image") { + return ( +
+ {mediaItem.name} +
+ ); + } + + if (mediaItem.type === "video" && mediaItem.thumbnailUrl) { + return ( +
+
+ {mediaItem.name} +
+ + {clip.name} + +
+ ); + } + + if (mediaItem.type === "audio") { + return ( +
+
+ +
+
+ ); + } + + // Fallback for videos without thumbnails + return ( + {clip.name} + ); + }; + + return ( +
onClipMouseDown(e, clip)} + onClick={(e) => onClipClick(e, clip)} + onMouseMove={handleResizeMove} + onMouseUp={handleResizeEnd} + onMouseLeave={handleResizeEnd} + tabIndex={0} + onContextMenu={(e) => onContextMenu(e, clip.id)} + > + {/* Left trim handle */} +
handleResizeStart(e, clip.id, "left")} + /> + + {/* Clip content */} +
+ {renderClipContent()} + + {/* Clip options menu */} +
+ + + {clipMenuOpen && ( +
e.stopPropagation()} + > + + +
+ )} +
+
+ + {/* Right trim handle */} +
handleResizeStart(e, clip.id, "right")} + /> +
+ ); +} diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index de2740a6..ef45eb5d 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -10,7 +10,6 @@ import { Snowflake, Copy, SplitSquareHorizontal, - MoreVertical, Volume2, VolumeX, Pause, @@ -25,7 +24,7 @@ import { import { useTimelineStore, type TimelineTrack, - type TimelineClip, + type TimelineClip as TypeTimelineClip, } from "@/stores/timeline-store"; import { useMediaStore } from "@/stores/media-store"; import { usePlaybackStore } from "@/stores/playback-store"; @@ -40,7 +39,8 @@ import { SelectTrigger, SelectValue, } from "../ui/select"; -import AudioWaveform from "./audio-waveform"; +import { TimelineClip } from "./timeline-clip"; +import { ContextMenuState } from "@/types/timeline"; export function Timeline() { // Timeline shows all tracks (video, audio, effects) and their clips. @@ -81,13 +81,7 @@ export function Timeline() { const [isInTimeline, setIsInTimeline] = useState(false); // Unified context menu state - const [contextMenu, setContextMenu] = useState<{ - type: "track" | "clip"; - trackId: string; - clipId?: string; - x: number; - y: number; - } | null>(null); + const [contextMenu, setContextMenu] = useState(null); // Marquee selection state const [marquee, setMarquee] = useState<{ @@ -1137,127 +1131,31 @@ function TimelineTrackContent({ }: { track: TimelineTrack; zoomLevel: number; - setContextMenu: ( - menu: { - type: "track" | "clip"; - trackId: string; - clipId?: string; - x: number; - y: number; - } | null - ) => void; - contextMenu: { - type: "track" | "clip"; - trackId: string; - clipId?: string; - x: number; - y: number; - } | null; + setContextMenu: (menu: ContextMenuState | null) => void; + contextMenu: ContextMenuState | null; }) { const { mediaItems } = useMediaStore(); const { tracks, moveClipToTrack, - updateClipTrim, updateClipStartTime, addClipToTrack, - removeClipFromTrack, - toggleTrackMute, selectedClips, selectClip, deselectClip, } = useTimelineStore(); - const { currentTime } = usePlaybackStore(); // Mouse-based drag hook - const { - isDragging, - draggedClipId, - startDrag, - endDrag, - getDraggedClipPosition, - isValidDropTarget, - timelineRef, - } = useDragClip(zoomLevel); + const { isDragging, startDrag, endDrag, timelineRef } = + useDragClip(zoomLevel); const [isDropping, setIsDropping] = useState(false); const [dropPosition, setDropPosition] = useState(null); const [wouldOverlap, setWouldOverlap] = useState(false); - const [resizing, setResizing] = useState<{ - clipId: string; - side: "left" | "right"; - startX: number; - initialTrimStart: number; - initialTrimEnd: number; - } | null>(null); const dragCounterRef = useRef(0); - const [clipMenuOpen, setClipMenuOpen] = useState(null); - - // Handle clip deletion - const handleDeleteClip = (clipId: string) => { - removeClipFromTrack(track.id, clipId); - }; - - const handleResizeStart = ( - e: React.MouseEvent, - clipId: string, - side: "left" | "right" - ) => { - e.stopPropagation(); - e.preventDefault(); - - const clip = track.clips.find((c) => c.id === clipId); - if (!clip) return; - - setResizing({ - clipId, - side, - startX: e.clientX, - initialTrimStart: clip.trimStart, - initialTrimEnd: clip.trimEnd, - }); - }; - - const updateTrimFromMouseMove = (e: { clientX: number }) => { - if (!resizing) return; - - const clip = track.clips.find((c) => c.id === resizing.clipId); - if (!clip) return; - - const deltaX = e.clientX - resizing.startX; - const deltaTime = deltaX / (50 * zoomLevel); - - if (resizing.side === "left") { - const newTrimStart = Math.max( - 0, - Math.min( - clip.duration - clip.trimEnd - 0.1, - resizing.initialTrimStart + deltaTime - ) - ); - updateClipTrim(track.id, clip.id, newTrimStart, clip.trimEnd); - } else { - const newTrimEnd = Math.max( - 0, - Math.min( - clip.duration - clip.trimStart - 0.1, - resizing.initialTrimEnd - deltaTime - ) - ); - updateClipTrim(track.id, clip.id, clip.trimStart, newTrimEnd); - } - }; - - const handleResizeMove = (e: React.MouseEvent) => { - updateTrimFromMouseMove(e); - }; - - const handleResizeEnd = () => { - setResizing(null); - }; const [justFinishedDrag, setJustFinishedDrag] = useState(false); - const handleClipMouseDown = (e: React.MouseEvent, clip: TimelineClip) => { + const handleClipMouseDown = (e: React.MouseEvent, clip: TypeTimelineClip) => { // Handle selection first if (!justFinishedDrag) { const isSelected = selectedClips.some( @@ -1283,6 +1181,43 @@ function TimelineTrackContent({ startDrag(e, clip.id, track.id, clip.startTime, clickOffsetTime); }; + const handleClipClick = (e: React.MouseEvent, clip: TypeTimelineClip) => { + e.stopPropagation(); + + // Don't handle click if we just finished dragging + if (justFinishedDrag) { + return; + } + + // Close context menu if it's open + if (contextMenu) { + setContextMenu(null); + return; // Don't handle selection when closing context menu + } + + // Only handle deselection here (selection is handled in mouseDown) + const isSelected = selectedClips.some( + (c) => c.trackId === track.id && c.clipId === clip.id + ); + + if (isSelected && !e.metaKey && !e.ctrlKey && !e.shiftKey) { + // If clip is already selected and no modifier keys, deselect it + deselectClip(track.id, clip.id); + } + }; + + const handleClipContextMenu = (e: React.MouseEvent, clipId: string) => { + e.preventDefault(); + e.stopPropagation(); + setContextMenu({ + type: "clip", + trackId: track.id, + clipId: clipId, + x: e.clientX, + y: e.clientY, + }); + }; + // Reset drag flag when drag ends useEffect(() => { if (!isDragging && justFinishedDrag) { @@ -1378,7 +1313,7 @@ function TimelineTrackContent({ (t: TimelineTrack) => t.id === fromTrackId ); const movingClip = sourceTrack?.clips.find( - (c: TimelineClip) => c.id === clipId + (c: TypeTimelineClip) => c.id === clipId ); if (movingClip) { @@ -1504,7 +1439,7 @@ function TimelineTrackContent({ (t: TimelineTrack) => t.id === fromTrackId ); const movingClip = sourceTrack?.clips.find( - (c: TimelineClip) => c.id === clipId + (c: TypeTimelineClip) => c.id === clipId ); if (!movingClip) { @@ -1622,107 +1557,6 @@ function TimelineTrackContent({ } }; - const getTrackColor = (type: string) => { - switch (type) { - case "video": - return "bg-blue-500/20 border-blue-500/30"; - case "audio": - return "bg-green-500/20 border-green-500/30"; - case "effects": - return "bg-purple-500/20 border-purple-500/30"; - default: - return "bg-gray-500/20 border-gray-500/30"; - } - }; - - const renderClipContent = (clip: TimelineClip) => { - const mediaItem = mediaItems.find((item) => item.id === clip.mediaId); - - if (!mediaItem) { - return ( - {clip.name} - ); - } - - if (mediaItem.type === "image") { - return ( -
- {mediaItem.name} -
- ); - } - - if (mediaItem.type === "video" && mediaItem.thumbnailUrl) { - return ( -
-
- {mediaItem.name} -
- - {clip.name} - -
- ); - } - - if (mediaItem.type === "audio") { - return ( -
-
- -
-
- ); - } - - // Fallback for videos without thumbnails - return ( - {clip.name} - ); - }; - - const handleSplitClip = (clip: TimelineClip) => { - // Use current playback time as split point - const splitTime = currentTime; - // Only split if splitTime is within the clip's effective range - const effectiveStart = clip.startTime; - const effectiveEnd = - clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); - if (splitTime <= effectiveStart || splitTime >= effectiveEnd) return; - const firstDuration = splitTime - effectiveStart; - const secondDuration = effectiveEnd - splitTime; - // First part: adjust original clip - updateClipTrim( - track.id, - clip.id, - clip.trimStart, - clip.trimEnd + secondDuration - ); - // Second part: add new clip after split - addClipToTrack(track.id, { - mediaId: clip.mediaId, - name: clip.name + " (cut)", - duration: clip.duration, - startTime: splitTime, - trimStart: clip.trimStart + firstDuration, - trimEnd: clip.trimEnd, - }); - }; - return (
{ - handleResizeEnd(); if (isDragging) { endDrag(track.id); } }} - onMouseLeave={handleResizeEnd} >
{track.clips.map((clip) => { - const effectiveDuration = - clip.duration - clip.trimStart - clip.trimEnd; - const clipWidth = Math.max( - 80, - effectiveDuration * 50 * zoomLevel - ); - - // Use real-time position during drag, otherwise use stored position - const dragPosition = getDraggedClipPosition(clip.id); - const clipStartTime = - dragPosition !== null ? dragPosition : clip.startTime; - const clipLeft = clipStartTime * 50 * zoomLevel; - const isSelected = selectedClips.some( (c) => c.trackId === track.id && c.clipId === clip.id ); - const isBeingDragged = draggedClipId === clip.id; return ( -
handleClipMouseDown(e, clip)} - onClick={(e) => { - e.stopPropagation(); - - // Don't handle click if we just finished dragging - if (justFinishedDrag) { - return; - } - - // Close context menu if it's open - if (contextMenu) { - setContextMenu(null); - return; // Don't handle selection when closing context menu - } - - // Only handle deselection here (selection is handled in mouseDown) - const isSelected = selectedClips.some( - (c) => c.trackId === track.id && c.clipId === clip.id - ); - - if (isSelected && !e.metaKey && !e.ctrlKey && !e.shiftKey) { - // If clip is already selected and no modifier keys, deselect it - deselectClip(track.id, clip.id); - } - }} - tabIndex={0} - onContextMenu={(e) => { - e.preventDefault(); - e.stopPropagation(); - setContextMenu({ - type: "clip", - trackId: track.id, - clipId: clip.id, - x: e.clientX, - y: e.clientY, - }); - }} - > - {/* Left trim handle */} -
{ - e.stopPropagation(); // Prevent triggering clip drag - handleResizeStart(e, clip.id, "left"); - }} - /> - {/* Clip content */} -
- {renderClipContent(clip)} - {/* Clip options menu */} -
- - {clipMenuOpen === clip.id && ( -
e.stopPropagation()} - > - - -
- )} -
-
- {/* Right trim handle */} -
{ - e.stopPropagation(); // Prevent triggering clip drag - handleResizeStart(e, clip.id, "right"); - }} - /> -
+ clip={clip} + track={track} + zoomLevel={zoomLevel} + isSelected={isSelected} + onContextMenu={handleClipContextMenu} + onClipMouseDown={handleClipMouseDown} + onClipClick={handleClipClick} + /> ); })} diff --git a/apps/web/src/types/timeline.ts b/apps/web/src/types/timeline.ts new file mode 100644 index 00000000..21be520e --- /dev/null +++ b/apps/web/src/types/timeline.ts @@ -0,0 +1,27 @@ +import { TimelineTrack, TimelineClip } from "@/stores/timeline-store"; + +export interface TimelineClipProps { + clip: TimelineClip; + track: TimelineTrack; + zoomLevel: number; + isSelected: boolean; + onContextMenu: (e: React.MouseEvent, clipId: string) => void; + onClipMouseDown: (e: React.MouseEvent, clip: TimelineClip) => void; + onClipClick: (e: React.MouseEvent, clip: TimelineClip) => void; +} + +export interface ResizeState { + clipId: string; + side: "left" | "right"; + startX: number; + initialTrimStart: number; + initialTrimEnd: number; +} + +export interface ContextMenuState { + type: "track" | "clip"; + trackId: string; + clipId?: string; + x: number; + y: number; +} -- cgit v1.3.1 From 0c97bc8c3f83c9dca9e92181d9dc5bc718d0c4e9 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 25 Jun 2025 23:08:31 +0200 Subject: refactor: remov eunused things --- apps/web/src/components/editor-header.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/web/src/components/editor-header.tsx b/apps/web/src/components/editor-header.tsx index 6f9f0b2f..5131230f 100644 --- a/apps/web/src/components/editor-header.tsx +++ b/apps/web/src/components/editor-header.tsx @@ -3,13 +3,11 @@ import Link from "next/link"; import { Button } from "./ui/button"; import { ChevronLeft, Download } from "lucide-react"; -import { useProjectStore } from "@/stores/project-store"; import { useTimelineStore } from "@/stores/timeline-store"; import { HeaderBase } from "./header-base"; import { ProjectNameEditor } from "./editor/project-name-editor"; export function EditorHeader() { - const { activeProject } = useProjectStore(); const { getTotalDuration } = useTimelineStore(); const handleExport = () => { -- cgit v1.3.1 From e5fc3f9bbbde4967dfb72374d74626de72c18811 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 25 Jun 2025 23:09:40 +0200 Subject: style: adjust height in editor header --- apps/web/src/components/header-base.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/header-base.tsx b/apps/web/src/components/header-base.tsx index 5f7b2b8d..f27eb7fc 100644 --- a/apps/web/src/components/header-base.tsx +++ b/apps/web/src/components/header-base.tsx @@ -29,7 +29,7 @@ export function HeaderBase({ return (
{leftContent &&
{leftContent}
} {centerContent && ( -- cgit v1.3.1 From 2f3a148dd4808a69619758b8640110b1aa859217 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 25 Jun 2025 23:22:59 +0200 Subject: refactor: move debug component to separate component and place absolutely --- apps/web/src/app/layout.tsx | 2 + apps/web/src/components/development-debug.tsx | 107 +++++++++++++++++++++++ apps/web/src/components/editor/preview-panel.tsx | 44 ---------- 3 files changed, 109 insertions(+), 44 deletions(-) create mode 100644 apps/web/src/components/development-debug.tsx diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index ee045647..8188d31b 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -5,6 +5,7 @@ import Script from "next/script"; import "./globals.css"; import { Toaster } from "../components/ui/sonner"; import { TooltipProvider } from "../components/ui/tooltip"; +import { DevelopmentDebug } from "../components/development-debug"; import { baseMetaData } from "./metadata"; const inter = Inter({ @@ -27,6 +28,7 @@ export default function RootLayout({ {children} +