From 3ac11189a3583d5550e3b421b68dc65673f8fbbd Mon Sep 17 00:00:00 2001
From: zhangwei <775925302@qq.com>
Date: Fri, 19 Sep 2025 00:32:10 +0800
Subject: [PATCH 1/2] =?UTF-8?q?=E5=A4=84=E7=90=86=E4=BA=86=E8=B0=83?=
=?UTF-8?q?=E6=95=B4=E9=93=BE=E6=8E=A5=E5=92=8C=E5=8F=82=E6=95=B0=E4=BF=9D?=
=?UTF-8?q?=E7=95=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
apps/web/src/app/editor/page.tsx | 66 +++++++++++++++++
apps/web/src/app/landing/page.tsx | 23 ++++++
apps/web/src/app/page.tsx | 42 ++++++-----
apps/web/src/app/projects/page.tsx | 113 ++++++++++++++++++++++-------
apps/web/src/components/header.tsx | 13 +++-
5 files changed, 207 insertions(+), 50 deletions(-)
create mode 100644 apps/web/src/app/editor/page.tsx
create mode 100644 apps/web/src/app/landing/page.tsx
diff --git a/apps/web/src/app/editor/page.tsx b/apps/web/src/app/editor/page.tsx
new file mode 100644
index 000000000..49fbded88
--- /dev/null
+++ b/apps/web/src/app/editor/page.tsx
@@ -0,0 +1,66 @@
+"use client";
+
+import { useProjectStore } from "@/stores/project-store";
+import { Loader2 } from "lucide-react";
+import { useRouter, useSearchParams } from "next/navigation";
+import { Suspense, useEffect } from "react";
+
+function EditorIndexContent() {
+ const { createNewProject } = useProjectStore();
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ useEffect(() => {
+ const createAndRedirect = async () => {
+ try {
+ // 创建新项目
+ const projectId = await createNewProject("新项目");
+
+ // 保留查询参数
+ const queryString = searchParams.toString();
+ const redirectUrl = queryString
+ ? `/editor/${projectId}?${queryString}`
+ : `/editor/${projectId}`;
+
+ // 重定向到编辑器页面
+ router.replace(redirectUrl);
+ } catch (error) {
+ console.error("创建项目失败:", error);
+ // 如果创建失败,重定向到项目列表页
+ const queryString = searchParams.toString();
+ const fallbackUrl = queryString
+ ? `/projects?${queryString}`
+ : "/projects";
+ router.replace(fallbackUrl);
+ }
+ };
+
+ createAndRedirect();
+ }, [createNewProject, router, searchParams]);
+
+ return (
+
+ );
+}
+
+export default function EditorIndex() {
+ return (
+
+
+
+ }
+ >
+
+
+ );
+}
diff --git a/apps/web/src/app/landing/page.tsx b/apps/web/src/app/landing/page.tsx
new file mode 100644
index 000000000..3f501a56b
--- /dev/null
+++ b/apps/web/src/app/landing/page.tsx
@@ -0,0 +1,23 @@
+import { Footer } from "@/components/footer";
+import { Header } from "@/components/header";
+import { Hero } from "@/components/landing/hero";
+import { SITE_URL } from "@/constants/site";
+import type { Metadata } from "next";
+
+export const metadata: Metadata = {
+ title: "OpenCut - 免费开源视频编辑器",
+ description: "一个免费、开源的网页、桌面和移动端视频编辑器。保护隐私,功能完整,无水印。",
+ alternates: {
+ canonical: `${SITE_URL}/landing`,
+ },
+};
+
+export default async function LandingPage() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx
index 818884ed0..403c24c1e 100644
--- a/apps/web/src/app/page.tsx
+++ b/apps/web/src/app/page.tsx
@@ -1,21 +1,27 @@
-import { Hero } from "@/components/landing/hero";
-import { Header } from "@/components/header";
-import { Footer } from "@/components/footer";
-import type { Metadata } from "next";
-import { SITE_URL } from "@/constants/site";
+import { redirect } from "next/navigation";
-export const metadata: Metadata = {
- alternates: {
- canonical: SITE_URL,
- },
-};
+interface HomeProps {
+ searchParams: { [key: string]: string | string[] | undefined };
+}
-export default async function Home() {
- return (
-
-
-
-
-
- );
+export default async function Home({ searchParams }: HomeProps) {
+ // 构建查询字符串
+ const queryString = new URLSearchParams();
+
+ for (const [key, value] of Object.entries(searchParams)) {
+ if (value !== undefined) {
+ if (Array.isArray(value)) {
+ for (const v of value) {
+ queryString.append(key, v);
+ }
+ } else {
+ queryString.set(key, value);
+ }
+ }
+ }
+
+ const queryStr = queryString.toString();
+ const redirectUrl = queryStr ? `/editor?${queryStr}` : "/editor";
+
+ redirect(redirectUrl);
}
diff --git a/apps/web/src/app/projects/page.tsx b/apps/web/src/app/projects/page.tsx
index 3d66a3c56..30af961eb 100644
--- a/apps/web/src/app/projects/page.tsx
+++ b/apps/web/src/app/projects/page.tsx
@@ -1,46 +1,46 @@
"use client";
-import {
- Calendar,
- ChevronLeft,
- Loader2,
- MoreHorizontal,
- ArrowDown01,
- Plus,
- Search,
- Trash2,
- Video,
- X,
-} from "lucide-react";
-import Image from "next/image";
-import Link from "next/link";
-import { useRouter } from "next/navigation";
-import { useCallback, useEffect, useState } from "react";
import { DeleteProjectDialog } from "@/components/delete-project-dialog";
import { RenameProjectDialog } from "@/components/rename-project-dialog";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
+import { Skeleton } from "@/components/ui/skeleton";
import {
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
} from "@/components/ui/tooltip";
-import { Skeleton } from "@/components/ui/skeleton";
import { useProjectStore } from "@/stores/project-store";
import { useTimelineStore } from "@/stores/timeline-store";
import type { TProject } from "@/types/project";
+import {
+ ArrowDown01,
+ Calendar,
+ ChevronLeft,
+ Loader2,
+ MoreHorizontal,
+ Plus,
+ Search,
+ Trash2,
+ Video,
+ X,
+} from "lucide-react";
+import Image from "next/image";
+import Link from "next/link";
+import { useRouter, useSearchParams } from "next/navigation";
+import { Suspense, useCallback, useEffect, useState } from "react";
-export default function ProjectsPage() {
+function ProjectsPageContent() {
const {
savedProjects,
isLoading,
@@ -63,6 +63,7 @@ export default function ProjectsPage() {
const [searchQuery, setSearchQuery] = useState("");
const [sortOption, setSortOption] = useState("createdAt-desc");
const router = useRouter();
+ const searchParams = useSearchParams();
const getProjectThumbnail = useCallback(
async (projectId: string): Promise => {
@@ -92,7 +93,14 @@ export default function ProjectsPage() {
const handleCreateProject = async () => {
const projectId = await createNewProject("New Project");
console.log("projectId", projectId);
- router.push(`/editor/${projectId}`);
+
+ // 保留查询参数
+ const queryString = searchParams.toString();
+ const redirectUrl = queryString
+ ? `/editor/${projectId}?${queryString}`
+ : `/editor/${projectId}`;
+
+ router.push(redirectUrl);
};
const handleSelectProject = (projectId: string, checked: boolean) => {
@@ -644,3 +652,52 @@ function NoResults({
);
}
+
+export default function ProjectsPage() {
+ return (
+
+
+
+
+ Back
+
+
+
+
+
+
+ Your Projects
+
+
Loading...
+
+
+
+ {Array.from({ length: 8 }, (_, index) => (
+
+ ))}
+
+
+
+ }
+ >
+
+
+ );
+}
diff --git a/apps/web/src/components/header.tsx b/apps/web/src/components/header.tsx
index f8877330d..d779ffd4e 100644
--- a/apps/web/src/components/header.tsx
+++ b/apps/web/src/components/header.tsx
@@ -1,15 +1,15 @@
"use client";
-import Link from "next/link";
-import { Button } from "./ui/button";
import { ArrowRight } from "lucide-react";
-import { HeaderBase } from "./header-base";
import Image from "next/image";
+import Link from "next/link";
+import { HeaderBase } from "./header-base";
import { ThemeToggle } from "./theme-toggle";
+import { Button } from "./ui/button";
export function Header() {
const leftContent = (
-
+
+
+
+
+ }
+ >
+
+
+ );
+}
diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx
index 403c24c1e..b7f031dc4 100644
--- a/apps/web/src/app/page.tsx
+++ b/apps/web/src/app/page.tsx
@@ -1,14 +1,17 @@
import { redirect } from "next/navigation";
interface HomeProps {
- searchParams: { [key: string]: string | string[] | undefined };
+ searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}
export default async function Home({ searchParams }: HomeProps) {
+ // 在 Next.js 15 中需要 await searchParams
+ const params = await searchParams;
+
// 构建查询字符串
const queryString = new URLSearchParams();
- for (const [key, value] of Object.entries(searchParams)) {
+ for (const [key, value] of Object.entries(params)) {
if (value !== undefined) {
if (Array.isArray(value)) {
for (const v of value) {
diff --git a/apps/web/start.sh b/apps/web/start.sh
new file mode 100755
index 000000000..076a35390
--- /dev/null
+++ b/apps/web/start.sh
@@ -0,0 +1,79 @@
+#!/bin/bash
+
+# OpenCut Web 应用启动脚本
+# 从 apps/web 目录启动开发服务器
+
+set -e
+
+PORT=5555
+
+echo "🚀 启动 OpenCut Web 应用..."
+echo "📍 目标端口: $PORT"
+
+# 检查端口是否被占用
+check_port() {
+ if lsof -Pi :$PORT -sTCP:LISTEN -t >/dev/null 2>&1; then
+ return 0 # 端口被占用
+ else
+ return 1 # 端口空闲
+ fi
+}
+
+# 停止占用端口的进程
+kill_port_processes() {
+ echo "⚠️ 检测到端口 $PORT 被占用"
+
+ PIDS=$(lsof -Pi :$PORT -sTCP:LISTEN -t 2>/dev/null || true)
+
+ if [ -n "$PIDS" ]; then
+ echo "🛑 正在停止占用端口的进程..."
+
+ # 尝试优雅地终止进程
+ for PID in $PIDS; do
+ kill -TERM $PID 2>/dev/null && echo " ✅ 已发送 TERM 信号给进程 $PID"
+ done
+
+ sleep 2
+
+ # 强制终止剩余进程
+ REMAINING_PIDS=$(lsof -Pi :$PORT -sTCP:LISTEN -t 2>/dev/null || true)
+ if [ -n "$REMAINING_PIDS" ]; then
+ for PID in $REMAINING_PIDS; do
+ kill -KILL $PID 2>/dev/null && echo " ✅ 已强制终止进程 $PID"
+ done
+ fi
+
+ sleep 1
+
+ if check_port; then
+ echo "❌ 无法释放端口 $PORT"
+ exit 1
+ else
+ echo "✅ 端口 $PORT 已释放"
+ fi
+ fi
+}
+
+# 检查是否在 web 应用目录
+if [ ! -f "package.json" ] || [ ! -d "src" ]; then
+ echo "❌ 错误: 请在 apps/web 目录下运行此脚本"
+ exit 1
+fi
+
+# 检查并处理端口占用
+if check_port; then
+ kill_port_processes
+fi
+
+echo "🔧 检查依赖..."
+if [ ! -d "node_modules" ]; then
+ echo "📦 安装依赖..."
+ bun install
+fi
+
+echo "🎯 启动开发服务器..."
+echo "🌐 应用将在 http://localhost:$PORT 启动"
+echo ""
+
+# 启动开发服务器
+exec bun run dev
diff --git a/docker-compose.yaml b/docker-compose.yaml
index e4f9672f9..73b9f81e0 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -55,11 +55,11 @@ services:
- FREESOUND_API_KEY=${FREESOUND_API_KEY}
restart: unless-stopped
ports:
- - "3100:3000" # app is running on 3000 so we run this at 3100
+ - "5555:3000" # app is running on 3000 internally, exposed on 5555
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://opencut:opencutthegoat@db:5432/opencut
- - BETTER_AUTH_URL=http://localhost:3000
+ - BETTER_AUTH_URL=http://localhost:5555
- BETTER_AUTH_SECRET=your-production-secret-key-here
- UPSTASH_REDIS_REST_URL=http://serverless-redis-http:80
- UPSTASH_REDIS_REST_TOKEN=example_token
diff --git a/packages/auth/src/server.ts b/packages/auth/src/server.ts
index 84b91b391..cbcc5aa2f 100644
--- a/packages/auth/src/server.ts
+++ b/packages/auth/src/server.ts
@@ -1,8 +1,8 @@
+import { db } from "@opencut/db";
+import { Redis } from "@upstash/redis";
import { betterAuth, RateLimit } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
-import { db } from "@opencut/db";
import { keys } from "./keys";
-import { Redis } from "@upstash/redis";
const {
NEXT_PUBLIC_BETTER_AUTH_URL,
@@ -44,7 +44,7 @@ export const auth = betterAuth({
},
baseURL: NEXT_PUBLIC_BETTER_AUTH_URL,
appName: "OpenCut",
- trustedOrigins: ["http://localhost:3000"],
+ trustedOrigins: ["http://localhost:5555"],
});
export type Auth = typeof auth;
diff --git a/start.sh b/start.sh
new file mode 100755
index 000000000..0c2d3d68c
--- /dev/null
+++ b/start.sh
@@ -0,0 +1,103 @@
+#!/bin/bash
+
+# OpenCut 开发服务器启动脚本
+# 检查并处理端口占用,然后启动开发服务器
+
+set -e
+
+PORT=5555
+PROJECT_NAME="OpenCut"
+
+echo "🚀 启动 $PROJECT_NAME 开发服务器..."
+echo "📍 目标端口: $PORT"
+
+# 检查端口是否被占用
+check_port() {
+ if lsof -Pi :$PORT -sTCP:LISTEN -t >/dev/null 2>&1; then
+ return 0 # 端口被占用
+ else
+ return 1 # 端口空闲
+ fi
+}
+
+# 停止占用端口的进程
+kill_port_processes() {
+ echo "⚠️ 检测到端口 $PORT 被占用"
+
+ # 获取占用端口的进程ID
+ PIDS=$(lsof -Pi :$PORT -sTCP:LISTEN -t 2>/dev/null || true)
+
+ if [ -n "$PIDS" ]; then
+ echo "🔍 发现以下进程占用端口 $PORT:"
+ for PID in $PIDS; do
+ PROCESS_INFO=$(ps -p $PID -o pid,ppid,comm,args --no-headers 2>/dev/null || echo "$PID unknown unknown")
+ echo " PID: $PROCESS_INFO"
+ done
+
+ echo "🛑 正在停止占用端口的进程..."
+
+ # 尝试优雅地终止进程
+ for PID in $PIDS; do
+ if kill -TERM $PID 2>/dev/null; then
+ echo " ✅ 已发送 TERM 信号给进程 $PID"
+ fi
+ done
+
+ # 等待进程优雅退出
+ sleep 2
+
+ # 检查是否还有进程占用端口
+ REMAINING_PIDS=$(lsof -Pi :$PORT -sTCP:LISTEN -t 2>/dev/null || true)
+
+ if [ -n "$REMAINING_PIDS" ]; then
+ echo "⚡ 强制终止剩余进程..."
+ for PID in $REMAINING_PIDS; do
+ if kill -KILL $PID 2>/dev/null; then
+ echo " ✅ 已强制终止进程 $PID"
+ fi
+ done
+ sleep 1
+ fi
+
+ # 最终检查
+ if check_port; then
+ echo "❌ 无法释放端口 $PORT,请手动检查"
+ exit 1
+ else
+ echo "✅ 端口 $PORT 已释放"
+ fi
+ fi
+}
+
+# 检查是否在正确的目录
+if [ ! -f "package.json" ] || [ ! -d "apps" ]; then
+ echo "❌ 错误: 请在 OpenCut 项目根目录下运行此脚本"
+ echo "📁 当前目录: $(pwd)"
+ exit 1
+fi
+
+# 检查 bun 是否安装
+if ! command -v bun &> /dev/null; then
+ echo "❌ 错误: 未找到 bun 命令"
+ echo "💡 请先安装 bun: https://bun.sh/docs/installation"
+ exit 1
+fi
+
+# 检查并处理端口占用
+if check_port; then
+ kill_port_processes
+fi
+
+echo "🔧 检查依赖..."
+if [ ! -d "node_modules" ]; then
+ echo "📦 安装依赖..."
+ bun install
+fi
+
+echo "🎯 启动开发服务器..."
+echo "🌐 应用将在 http://localhost:$PORT 启动"
+echo "📝 按 Ctrl+C 停止服务器"
+echo ""
+
+# 启动开发服务器
+exec bun dev