diff --git a/CLAUDE.md b/CLAUDE.md index 8bd9863dd..78eecef62 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -109,7 +109,7 @@ DATABASE_URL="postgresql://opencut:opencutthegoat@localhost:5432/opencut" # Authentication BETTER_AUTH_SECRET="your-generated-secret-here" -BETTER_AUTH_URL="http://localhost:3000" +BETTER_AUTH_URL="http://localhost:5555" # Redis UPSTASH_REDIS_REST_URL="http://localhost:8079" diff --git a/README.md b/README.md index 9bad77386..f03eba733 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ Before you begin, ensure you have the following installed on your system: # Generate a secure secret for Better Auth BETTER_AUTH_SECRET="your-generated-secret-here" - BETTER_AUTH_URL="http://localhost:3000" + BETTER_AUTH_URL="http://localhost:5555" # Redis (matches docker-compose.yaml) UPSTASH_REDIS_REST_URL="http://localhost:8079" @@ -140,7 +140,7 @@ Before you begin, ensure you have the following installed on your system: 5. Run database migrations: `bun run db:migrate` from (inside apps/web) 6. Start the development server: `bun run dev` from (inside apps/web) -The application will be available at [http://localhost:3000](http://localhost:3000). +The application will be available at [http://localhost:5555](http://localhost:5555). ## Contributing diff --git a/apps/web/.env.example b/apps/web/.env.example index e77d3c5a0..5072fdab0 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -4,7 +4,7 @@ DATABASE_URL="postgresql://opencut:opencutthegoat@localhost:5432/opencut" # Better Auth -NEXT_PUBLIC_BETTER_AUTH_URL=http://localhost:3000 +NEXT_PUBLIC_BETTER_AUTH_URL=http://localhost:5555 BETTER_AUTH_SECRET=your-secret-key-here # Development Environment @@ -30,4 +30,7 @@ R2_SECRET_ACCESS_KEY=your-secret-access-key R2_BUCKET_NAME=opencut-transcription # Modal transcription endpoint (from modal deploy transcription.py) -MODAL_TRANSCRIPTION_URL=https://your-username--opencut-transcription-transcribe-audio.modal.run \ No newline at end of file +MODAL_TRANSCRIPTION_URL=https://your-username--opencut-transcription-transcribe-audio.modal.run + +# ND Super Agent Integration +NEXT_PUBLIC_ND_SUPER_AGENT_API_URL=http://localhost:3001/api/base \ No newline at end of file diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index 6c42e7081..20426f022 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -29,7 +29,7 @@ ENV DATABASE_URL="postgresql://opencut:opencutthegoat@localhost:5432/opencut" ENV BETTER_AUTH_SECRET="build-time-secret" ENV UPSTASH_REDIS_REST_URL="http://localhost:8079" ENV UPSTASH_REDIS_REST_TOKEN="example_token" -ENV NEXT_PUBLIC_BETTER_AUTH_URL="http://localhost:3000" +ENV NEXT_PUBLIC_BETTER_AUTH_URL="http://localhost:5555" ENV FREESOUND_CLIENT_ID=$FREESOUND_CLIENT_ID ENV FREESOUND_API_KEY=$FREESOUND_API_KEY diff --git a/apps/web/package.json b/apps/web/package.json index a794741dd..2416ce818 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -4,7 +4,7 @@ "private": true, "packageManager": "bun@1.2.18", "scripts": { - "dev": "next dev --turbopack", + "dev": "next dev --turbopack --port 5555", "build": "next build", "start": "next start", "lint": "biome check src/", diff --git a/apps/web/src/app/editor/[project_id]/page.tsx b/apps/web/src/app/editor/[project_id]/page.tsx index 1b53b6b5b..b1a355e55 100644 --- a/apps/web/src/app/editor/[project_id]/page.tsx +++ b/apps/web/src/app/editor/[project_id]/page.tsx @@ -1,24 +1,24 @@ "use client"; -import { useEffect, useRef } from "react"; -import { useParams, useRouter } from "next/navigation"; -import { - ResizablePanelGroup, - ResizablePanel, - ResizableHandle, -} from "@/components/ui/resizable"; +import { EditorHeader } from "@/components/editor/editor-header"; import { MediaPanel } from "@/components/editor/media-panel"; +import { Onboarding } from "@/components/editor/onboarding"; +import { PreviewPanel } from "@/components/editor/preview-panel"; import { PropertiesPanel } from "@/components/editor/properties-panel"; import { Timeline } from "@/components/editor/timeline"; -import { PreviewPanel } from "@/components/editor/preview-panel"; -import { EditorHeader } from "@/components/editor/editor-header"; -import { usePanelStore } from "@/stores/panel-store"; -import { useProjectStore } from "@/stores/project-store"; import { EditorProvider } from "@/components/providers/editor-provider"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "@/components/ui/resizable"; import { usePlaybackControls } from "@/hooks/use-playback-controls"; -import { Onboarding } from "@/components/editor/onboarding"; +import { usePanelStore } from "@/stores/panel-store"; +import { useProjectStore } from "@/stores/project-store"; +import { useParams, useRouter, useSearchParams } from "next/navigation"; +import { Suspense, useEffect, useRef } from "react"; -export default function Editor() { +function EditorContent() { const { toolsPanel, previewPanel, @@ -43,12 +43,100 @@ export default function Editor() { } = useProjectStore(); const params = useParams(); const router = useRouter(); + const searchParams = useSearchParams(); const projectId = params.project_id as string; const handledProjectIds = useRef>(new Set()); const isInitializingRef = useRef(false); usePlaybackControls(); + // 处理从 nd-super-agent 传递过来的参数 + useEffect(() => { + const handleExternalParams = async () => { + console.log('🔍 开始检查外部参数...'); + + // 获取URL参数 + const token = searchParams.get('token'); + const userId = searchParams.get('user_id'); + const tenantId = searchParams.get('tenant_id'); + const materialIds = searchParams.get('material_ids'); + + // 从环境变量获取 API 基础 URL + const apiBaseUrl = process.env.NEXT_PUBLIC_ND_SUPER_AGENT_API_URL; + + console.log('📋 外部参数检查:', { + hasToken: !!token, + hasUserId: !!userId, + hasTenantId: !!tenantId, + hasMaterialIds: !!materialIds, + hasApiBaseUrl: !!apiBaseUrl, + materialIds + }); + + + // 如果有外部参数,处理素材加载 + if (token && materialIds && apiBaseUrl) { + console.log('检测到外部参数,开始加载素材:', { + token: token.substring(0, 10) + '...', + userId, + tenantId, + materialIds, + apiBaseUrl: apiBaseUrl?.substring(0, 30) + '...' + }); + + try { + // 解析素材ID列表 + const ids = materialIds.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id)); + + if (ids.length > 0) { + console.log('🔗 调用素材接口:', { ids, apiUrl: `${apiBaseUrl}/gallery/batch-query` }); + + // 调用 nd-super-agent 的素材接口 + const response = await fetch(`${apiBaseUrl}/gallery/batch-query`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + ...(tenantId && { 'X-Tenant-ID': tenantId }), + ...(userId && { 'X-User-ID': userId }) + }, + body: JSON.stringify({ ids }) + }); + + if (response.ok) { + const result = await response.json(); + console.log('✅ 素材加载成功:', { + 找到数量: result.data?.found_count || 0, + 请求数量: result.data?.requested_count || 0 + }); + + if (result.success && result.data && result.data.items) { + console.log('📋 获取到素材:', result.data.items.map(item => ({ + id: item.id, + name: item.name || item.title, + type: item.type + }))); + + // TODO: 将素材添加到 OpenCut 的媒体存储中 + // 可能需要调用 useMediaStore 或相关的存储管理 + } + } else { + console.error('❌ 素材加载失败:', response.status, response.statusText); + } + } else { + console.warn('⚠️ 没有有效的素材ID'); + } + } catch (error) { + console.error('💥 素材加载失败:', error.message); + } + } + }; + + // 延迟执行,确保组件已经初始化 + const timer = setTimeout(handleExternalParams, 1000); + return () => clearTimeout(timer); + }, [searchParams]); + useEffect(() => { let isCancelled = false; @@ -457,3 +545,20 @@ export default function Editor() { ); } + +export default function Editor() { + return ( + +
+
+

正在加载编辑器...

+
+
+ } + > + +
+ ); +} 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..b7f031dc4 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -1,21 +1,30 @@ -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: Promise<{ [key: string]: string | string[] | undefined }>; +} -export default async function Home() { - return ( -
-
- -
-
- ); +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(params)) { + 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 = ( - + OpenCut Logo
+ + +