forked from 77media/video-flow
连调工作流接口以及优化首页样式等
This commit is contained in:
parent
d72d0e6457
commit
53b6d87179
@ -1 +1 @@
|
||||
export const BASE_URL = "https://pre.movieflow.api.huiying.video"
|
||||
export const BASE_URL = "https://77.smartvideo.py.qikongjian.com"
|
||||
|
||||
65
api/enums.ts
65
api/enums.ts
@ -1,65 +0,0 @@
|
||||
// 主项目(产品)类型枚举
|
||||
export enum ProjectTypeEnum {
|
||||
SCRIPT_TO_VIDEO = 1, // "剧本转视频"
|
||||
VIDEO_TO_VIDEO = 2, // "视频复刻视频"
|
||||
}
|
||||
|
||||
// 模式枚举
|
||||
export enum ModeEnum {
|
||||
MANUAL = 1, // "手动"
|
||||
AUTOMATIC = 2, // "自动"
|
||||
}
|
||||
|
||||
// 分辨率枚举
|
||||
export enum ResolutionEnum {
|
||||
HD_720P = 1, // "720p"
|
||||
FULL_HD_1080P = 2, // "1080p"
|
||||
UHD_2K = 3, // "2k"
|
||||
UHD_4K = 4, // "4k"
|
||||
}
|
||||
|
||||
// 项目类型映射
|
||||
export const ProjectTypeMap = {
|
||||
[ProjectTypeEnum.SCRIPT_TO_VIDEO]: {
|
||||
value: "script_to_video",
|
||||
label: "script",
|
||||
tab: "script"
|
||||
},
|
||||
[ProjectTypeEnum.VIDEO_TO_VIDEO]: {
|
||||
value: "video_to_video",
|
||||
label: "clone",
|
||||
tab: "clone"
|
||||
}
|
||||
} as const;
|
||||
|
||||
// 模式映射
|
||||
export const ModeMap = {
|
||||
[ModeEnum.MANUAL]: {
|
||||
value: "manual",
|
||||
label: "手动"
|
||||
},
|
||||
[ModeEnum.AUTOMATIC]: {
|
||||
value: "automatic",
|
||||
label: "自动"
|
||||
}
|
||||
} as const;
|
||||
|
||||
// 分辨率映射
|
||||
export const ResolutionMap = {
|
||||
[ResolutionEnum.HD_720P]: {
|
||||
value: "720p",
|
||||
label: "720P"
|
||||
},
|
||||
[ResolutionEnum.FULL_HD_1080P]: {
|
||||
value: "1080p",
|
||||
label: "1080P"
|
||||
},
|
||||
[ResolutionEnum.UHD_2K]: {
|
||||
value: "2k",
|
||||
label: "2K"
|
||||
},
|
||||
[ResolutionEnum.UHD_4K]: {
|
||||
value: "4k",
|
||||
label: "4K"
|
||||
}
|
||||
} as const;
|
||||
@ -61,6 +61,16 @@ export interface ScriptEpisode {
|
||||
video_url?: string;
|
||||
}
|
||||
|
||||
// 新-获取剧集列表
|
||||
export const getScriptEpisodeListNew = async (data: any): Promise<ApiResponse<any>> => {
|
||||
return post<ApiResponse<any>>('/movie/list_movie_projects', data);
|
||||
};
|
||||
|
||||
// 新-创建接口
|
||||
export const createScriptEpisodeNew = async (data: any): Promise<ApiResponse<any>> => {
|
||||
return post<ApiResponse<any>>('/movie/create_movie_project', data);
|
||||
};
|
||||
|
||||
// 创建剧集
|
||||
export const createScriptEpisode = async (data: CreateScriptEpisodeRequest): Promise<ApiResponse<ScriptEpisode>> => {
|
||||
// return post<ApiResponse<ScriptEpisode>>('/script_episode/create', data);
|
||||
|
||||
@ -72,31 +72,7 @@ export interface DeleteScriptProjectRequest {
|
||||
|
||||
// 创建剧本项目
|
||||
export const createScriptProject = async (data: CreateScriptProjectRequest): Promise<ApiResponse<ScriptProject>> => {
|
||||
// return post<ApiResponse<ScriptProject>>('/script_project/create', data);
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
id: 1,
|
||||
title: '2025·07·03·001',
|
||||
script_author: 'king',
|
||||
characters: [],
|
||||
summary: '',
|
||||
project_type: 1,
|
||||
status: 1,
|
||||
cate_tags: [],
|
||||
creator_name: 'king',
|
||||
mode: 1,
|
||||
resolution: 1,
|
||||
created_at: '2025-07-03 10:00:00',
|
||||
updated_at: '2025-07-03 10:00:00'
|
||||
},
|
||||
successful: true
|
||||
});
|
||||
}, 0);
|
||||
});
|
||||
return post<ApiResponse<ScriptProject>>('/script_project/create', data);
|
||||
};
|
||||
|
||||
// 获取剧本项目列表
|
||||
@ -113,3 +89,13 @@ export const updateScriptProject = async (data: UpdateScriptProjectRequest): Pro
|
||||
export const deleteScriptProject = async (data: DeleteScriptProjectRequest): Promise<ApiResponse<ScriptProject>> => {
|
||||
return post<ApiResponse<ScriptProject>>('/script_project/delete', data);
|
||||
};
|
||||
|
||||
// 获取剧本项目详情请求数据类型
|
||||
export interface GetScriptProjectDetailRequest {
|
||||
id: number;
|
||||
}
|
||||
|
||||
// 获取剧本项目详情
|
||||
export const getScriptProjectDetail = async (data: GetScriptProjectDetailRequest): Promise<ApiResponse<ScriptProject>> => {
|
||||
return post<ApiResponse<ScriptProject>>('/script_project/detail', data);
|
||||
};
|
||||
@ -1,6 +1,69 @@
|
||||
import { post } from './request';
|
||||
import { ProjectTypeEnum } from './enums';
|
||||
import { ApiResponse } from './common';
|
||||
import { ProjectTypeEnum } from '@/app/model/enums';
|
||||
import { ApiResponse } from '@/api/common';
|
||||
|
||||
// API 响应类型
|
||||
interface BaseApiResponse<T> {
|
||||
code: number;
|
||||
successful: boolean;
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
// 剧集详情数据类型
|
||||
interface EpisodeDetail {
|
||||
project_id: string;
|
||||
name: string;
|
||||
status: 'running' | 'completed';
|
||||
step: 'sketch' | 'character' | 'video' | 'music' | 'final_video';
|
||||
last_message: string;
|
||||
data: TaskData | null;
|
||||
mode: 'auto' | 'manual';
|
||||
resolution: '1080p' | '4k';
|
||||
}
|
||||
|
||||
// 任务数据类型
|
||||
interface TaskData {
|
||||
sketch?: Array<{
|
||||
url: string;
|
||||
script: string;
|
||||
bg_rgb: string[];
|
||||
}>;
|
||||
roles?: Array<{
|
||||
name: string;
|
||||
url: string;
|
||||
sound: string;
|
||||
soundDescription: string;
|
||||
roleDescription: string;
|
||||
}>;
|
||||
videos?: Array<{
|
||||
url: string;
|
||||
script: string;
|
||||
audio: string;
|
||||
}>;
|
||||
music?: Array<{
|
||||
url: string;
|
||||
script: string;
|
||||
name: string;
|
||||
duration: string;
|
||||
totalDuration: string;
|
||||
isLooped: boolean;
|
||||
}>;
|
||||
final?: {
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
|
||||
// 流式数据类型
|
||||
export interface StreamData {
|
||||
category: 'sketch' | 'character' | 'video' | 'music' | 'final_video';
|
||||
message: string;
|
||||
data: any;
|
||||
status: 'running' | 'completed';
|
||||
total?: number;
|
||||
completed?: number;
|
||||
all_completed?: boolean;
|
||||
}
|
||||
|
||||
// 场景/分镜头数据结构
|
||||
export interface Scene {
|
||||
@ -50,7 +113,7 @@ export interface VideoToSceneRequest {
|
||||
export type ConvertScenePromptRequest = ScriptToSceneRequest | VideoToSceneRequest;
|
||||
|
||||
// 转换分镜头响应接口
|
||||
export type ConvertScenePromptResponse = ApiResponse<ScenePrompts>;
|
||||
export type ConvertScenePromptResponse = BaseApiResponse<ScenePrompts>;
|
||||
|
||||
/**
|
||||
* 将剧本或视频转换为分镜头提示词
|
||||
@ -116,3 +179,41 @@ export const convertVideoToScene = async (
|
||||
project_type: ProjectTypeEnum.VIDEO_TO_VIDEO
|
||||
});
|
||||
};
|
||||
|
||||
// 新-获取剧集详情
|
||||
export const detailScriptEpisodeNew = async (data: { project_id: string }): Promise<ApiResponse<any>> => {
|
||||
return post<ApiResponse<any>>('/movie/get_movie_project_detail', data);
|
||||
return {
|
||||
code: 0,
|
||||
successful: true,
|
||||
message: 'success',
|
||||
data: {
|
||||
project_id: 'uuid',
|
||||
name: '没有返回就调 name 接口',
|
||||
status: 'running',
|
||||
step: 'sketch',
|
||||
last_message: 'loading detail info...',
|
||||
data: null,
|
||||
mode: 'auto',
|
||||
resolution: '1080p',
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 获取 title 接口
|
||||
export const getScriptTitle = async (data: { project_id: string }): Promise<ApiResponse<any>> => {
|
||||
return post<ApiResponse<any>>('/movie/get_movie_project_name', data);
|
||||
return {
|
||||
code: 0,
|
||||
successful: true,
|
||||
message: 'success',
|
||||
data: {
|
||||
name: '提取出视频标题'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取 数据 全量(需轮询)
|
||||
export const getRunningStreamData = async (data: { project_id: string }): Promise<ApiResponse<any>> => {
|
||||
return post<ApiResponse<any>>('/movie/get_status', data);
|
||||
};
|
||||
@ -1,80 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
// Prevent static rendering of this route
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
/**
|
||||
* Handle Google OAuth callback
|
||||
* In a real app, this would:
|
||||
* 1. Exchange the authorization code for tokens
|
||||
* 2. Verify the token and get user info from Google
|
||||
* 3. Create or update the user in your database
|
||||
* 4. Set session/cookies
|
||||
* 5. Redirect to the app
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const code = searchParams.get('code');
|
||||
const error = searchParams.get('error');
|
||||
const state = searchParams.get('state');
|
||||
|
||||
console.log('Google OAuth callback received', {
|
||||
hasCode: !!code,
|
||||
error: error || 'none',
|
||||
hasState: !!state,
|
||||
url: request.url
|
||||
});
|
||||
|
||||
// Handle errors from Google
|
||||
if (error) {
|
||||
console.error('Google OAuth error:', error);
|
||||
return NextResponse.redirect(new URL(`/login?error=${encodeURIComponent(error)}`, request.url));
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
console.error('No authorization code received from Google');
|
||||
return NextResponse.redirect(new URL('/login?error=no_code', request.url));
|
||||
}
|
||||
|
||||
// The state parameter validation will happen client-side
|
||||
// since we're storing the original state in sessionStorage
|
||||
// We'll add the state to the redirect URL so the client can validate it
|
||||
|
||||
try {
|
||||
console.log('Processing OAuth callback with code', code.substring(0, 5) + '...');
|
||||
|
||||
// In a real app, you would exchange the code for tokens
|
||||
// and validate the tokens here
|
||||
|
||||
// For this demo, we'll just simulate a successful login
|
||||
// by redirecting with a mock session token
|
||||
const redirectUrl = new URL('/', request.url);
|
||||
|
||||
// Mock user data that would normally come from Google
|
||||
const mockUser = {
|
||||
id: 'google-123456',
|
||||
name: 'Google User',
|
||||
email: 'user@gmail.com',
|
||||
picture: 'https://i.pravatar.cc/150',
|
||||
};
|
||||
|
||||
// In a real app, you would set cookies or session data here
|
||||
|
||||
// Simulate setting a session by adding a URL parameter
|
||||
// In a real app, don't pass sensitive data in URL parameters
|
||||
redirectUrl.searchParams.set('session', 'demo-session-token');
|
||||
redirectUrl.searchParams.set('user', encodeURIComponent(JSON.stringify(mockUser)));
|
||||
|
||||
// Pass the state back to the client for validation
|
||||
if (state) {
|
||||
redirectUrl.searchParams.set('state', state);
|
||||
}
|
||||
|
||||
console.log('Redirecting to:', redirectUrl.toString());
|
||||
return NextResponse.redirect(redirectUrl);
|
||||
} catch (error) {
|
||||
console.error('Failed to process Google authentication:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
return NextResponse.redirect(new URL(`/login?error=auth_failed&details=${encodeURIComponent(errorMessage)}`, request.url));
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,12 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
*,
|
||||
*:after,
|
||||
*:before {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-start-rgb: 214, 219, 220;
|
||||
@ -10,6 +16,10 @@
|
||||
--text-secondary: #a0aec0;
|
||||
--text-primary: #fff;
|
||||
--ui-level1-layerbase: #131416;
|
||||
|
||||
/* 3dwave */
|
||||
--index: calc(1vh + 1vw);
|
||||
--transition: cubic-bezier(0.1, 0.7, 0, 1);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@ -99,6 +109,9 @@ body {
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
display: none !important;
|
||||
}
|
||||
*::-webkit-scrollbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
|
||||
136
app/model/enums.ts
Normal file
136
app/model/enums.ts
Normal file
@ -0,0 +1,136 @@
|
||||
// 主项目(产品)类型枚举
|
||||
export enum ProjectTypeEnum {
|
||||
SCRIPT_TO_VIDEO = 1, // "剧本转视频"
|
||||
VIDEO_TO_VIDEO = 2, // "视频复刻视频"
|
||||
}
|
||||
|
||||
// 模式枚举
|
||||
export enum ModeEnum {
|
||||
MANUAL = 'manual', // "手动"
|
||||
AUTOMATIC = 'automatic', // "自动"
|
||||
}
|
||||
|
||||
// 分辨率枚举
|
||||
export enum ResolutionEnum {
|
||||
HD_720P = '720p', // "720p"
|
||||
FULL_HD_1080P = '1080p', // "1080p"
|
||||
UHD_2K = '2k', // "2k"
|
||||
UHD_4K = '4k', // "4k"
|
||||
}
|
||||
|
||||
// 工作流阶段枚举
|
||||
export enum FlowStageEnum {
|
||||
PREVIEW = 1, // "预览图列表分镜草图"
|
||||
CHARACTERS = 2, // "角色列表"
|
||||
SHOTS = 3, // "分镜视频列表"
|
||||
BGM = 4, // "背景音乐列表"
|
||||
POST_PRODUCTION = 5, // "后期操作中"
|
||||
MERGE_VIDEO = 6, // "合并视频完成"
|
||||
ERROR = 99, // "出错"
|
||||
}
|
||||
|
||||
// 任务状态枚举
|
||||
export enum TaskStatusEnum {
|
||||
PENDING = "pending", // "待处理"
|
||||
PROCESSING = "processing", // "处理中"
|
||||
COMPLETED = "completed", // "已完成"
|
||||
FAILED = "failed", // "失败"
|
||||
}
|
||||
|
||||
// 项目类型映射
|
||||
export const ProjectTypeMap = {
|
||||
[ProjectTypeEnum.SCRIPT_TO_VIDEO]: {
|
||||
value: "script_to_video",
|
||||
label: "script",
|
||||
tab: "script"
|
||||
},
|
||||
[ProjectTypeEnum.VIDEO_TO_VIDEO]: {
|
||||
value: "video_to_video",
|
||||
label: "clone",
|
||||
tab: "clone"
|
||||
}
|
||||
} as const;
|
||||
|
||||
// 模式映射
|
||||
export const ModeMap = {
|
||||
[ModeEnum.MANUAL]: {
|
||||
value: "manual",
|
||||
label: "手动"
|
||||
},
|
||||
[ModeEnum.AUTOMATIC]: {
|
||||
value: "automatic",
|
||||
label: "自动"
|
||||
}
|
||||
} as const;
|
||||
|
||||
// 分辨率映射
|
||||
export const ResolutionMap = {
|
||||
[ResolutionEnum.HD_720P]: {
|
||||
value: "720p",
|
||||
label: "720P"
|
||||
},
|
||||
[ResolutionEnum.FULL_HD_1080P]: {
|
||||
value: "1080p",
|
||||
label: "1080P"
|
||||
},
|
||||
[ResolutionEnum.UHD_2K]: {
|
||||
value: "2k",
|
||||
label: "2K"
|
||||
},
|
||||
[ResolutionEnum.UHD_4K]: {
|
||||
value: "4k",
|
||||
label: "4K"
|
||||
}
|
||||
} as const;
|
||||
|
||||
// 工作流阶段映射
|
||||
export const FlowStageMap = {
|
||||
[FlowStageEnum.PREVIEW]: {
|
||||
value: "preview",
|
||||
label: "预览图列表分镜草图"
|
||||
},
|
||||
[FlowStageEnum.CHARACTERS]: {
|
||||
value: "characters",
|
||||
label: "角色列表"
|
||||
},
|
||||
[FlowStageEnum.SHOTS]: {
|
||||
value: "shots",
|
||||
label: "分镜视频列表"
|
||||
},
|
||||
[FlowStageEnum.BGM]: {
|
||||
value: "bgm",
|
||||
label: "背景音乐列表"
|
||||
},
|
||||
[FlowStageEnum.POST_PRODUCTION]: {
|
||||
value: "post_production",
|
||||
label: "后期操作中"
|
||||
},
|
||||
[FlowStageEnum.MERGE_VIDEO]: {
|
||||
value: "merge_video",
|
||||
label: "合并视频完成"
|
||||
},
|
||||
[FlowStageEnum.ERROR]: {
|
||||
value: "error",
|
||||
label: "出错"
|
||||
}
|
||||
} as const;
|
||||
|
||||
// 任务状态映射
|
||||
export const TaskStatusMap = {
|
||||
[TaskStatusEnum.PENDING]: {
|
||||
value: "pending",
|
||||
label: "待处理"
|
||||
},
|
||||
[TaskStatusEnum.PROCESSING]: {
|
||||
value: "processing",
|
||||
label: "处理中"
|
||||
},
|
||||
[TaskStatusEnum.COMPLETED]: {
|
||||
value: "completed",
|
||||
label: "已完成"
|
||||
},
|
||||
[TaskStatusEnum.FAILED]: {
|
||||
value: "failed",
|
||||
label: "失败"
|
||||
}
|
||||
} as const;
|
||||
176
app/model/sse_response.ts
Normal file
176
app/model/sse_response.ts
Normal file
@ -0,0 +1,176 @@
|
||||
/**
|
||||
* SSE 响应数据结构
|
||||
* 对应 Python 后端的 sse_response.py
|
||||
*/
|
||||
|
||||
import { TaskStatusEnum, FlowStageEnum } from './enums';
|
||||
|
||||
// 重新导出枚举以供其他模块使用
|
||||
export { TaskStatusEnum, FlowStageEnum };
|
||||
|
||||
// 完成状态对象
|
||||
export interface CompletionStatus {
|
||||
total: number;
|
||||
completed: number;
|
||||
all_completed: boolean;
|
||||
}
|
||||
|
||||
// 预览图对象
|
||||
export interface PreviewObj {
|
||||
scene_id: number;
|
||||
prompt: string;
|
||||
order: number;
|
||||
preview_pic_url?: string;
|
||||
status: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
// 角色对象
|
||||
export interface CharacterObj {
|
||||
name: string;
|
||||
desc: string;
|
||||
image_url?: string;
|
||||
voice_desc: string;
|
||||
voice_url?: string;
|
||||
}
|
||||
|
||||
// 分镜视频对象
|
||||
export interface ShotObj {
|
||||
scene_id: number;
|
||||
prompt: string;
|
||||
order: number;
|
||||
transition: string;
|
||||
volume: number;
|
||||
video_url?: string;
|
||||
status: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
// 背景音乐对象
|
||||
export interface BgmObj {
|
||||
bgm_id?: number;
|
||||
name?: string;
|
||||
url?: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
// 后期制作对象
|
||||
export interface PostProductionObj {
|
||||
episode_status: number;
|
||||
is_processing: boolean;
|
||||
all_completed: boolean;
|
||||
}
|
||||
|
||||
// 最终视频对象
|
||||
export interface FinalVideoObj {
|
||||
video_url?: string;
|
||||
is_completed: boolean;
|
||||
}
|
||||
|
||||
// SSE 响应数据联合类型
|
||||
export type SseResponseData =
|
||||
| PreviewObj
|
||||
| CharacterObj
|
||||
| ShotObj
|
||||
| BgmObj
|
||||
| PostProductionObj
|
||||
| FinalVideoObj
|
||||
| null;
|
||||
|
||||
// SSE 响应对象
|
||||
export interface SseResponse {
|
||||
code: number;
|
||||
message: string;
|
||||
data?: SseResponseData;
|
||||
stage: number;
|
||||
stage_index: number;
|
||||
status: string;
|
||||
completion_status?: CompletionStatus;
|
||||
}
|
||||
|
||||
// 使用统一的枚举
|
||||
export const WorkflowStageEnum = FlowStageEnum;
|
||||
export const WorkflowStatusEnum = TaskStatusEnum;
|
||||
|
||||
// 类型守卫函数
|
||||
export function isPreviewObj(data: SseResponseData): data is PreviewObj {
|
||||
return data !== null && typeof data === 'object' && 'scene_id' in data && 'preview_pic_url' in data;
|
||||
}
|
||||
|
||||
export function isCharacterObj(data: SseResponseData): data is CharacterObj {
|
||||
return data !== null && typeof data === 'object' && 'name' in data && 'voice_desc' in data;
|
||||
}
|
||||
|
||||
export function isShotObj(data: SseResponseData): data is ShotObj {
|
||||
return data !== null && typeof data === 'object' && 'scene_id' in data && 'video_url' in data;
|
||||
}
|
||||
|
||||
export function isBgmObj(data: SseResponseData): data is BgmObj {
|
||||
return data !== null && typeof data === 'object' && 'bgm_id' in data;
|
||||
}
|
||||
|
||||
export function isPostProductionObj(data: SseResponseData): data is PostProductionObj {
|
||||
return data !== null && typeof data === 'object' && 'episode_status' in data && 'is_processing' in data;
|
||||
}
|
||||
|
||||
export function isFinalVideoObj(data: SseResponseData): data is FinalVideoObj {
|
||||
return data !== null && typeof data === 'object' && 'video_url' in data && 'is_completed' in data;
|
||||
}
|
||||
|
||||
// 工厂函数
|
||||
export class SseResponseFactory {
|
||||
static success(
|
||||
stage: number = 0,
|
||||
stage_index: number = 0,
|
||||
status: string = '',
|
||||
message: string = '操作成功',
|
||||
data?: SseResponseData,
|
||||
completion_status?: CompletionStatus
|
||||
): SseResponse {
|
||||
return {
|
||||
code: 0,
|
||||
stage: stage,
|
||||
stage_index: stage_index,
|
||||
status: status,
|
||||
message: message,
|
||||
data: data,
|
||||
completion_status: completion_status
|
||||
};
|
||||
}
|
||||
|
||||
static fail(
|
||||
stage: number = 0,
|
||||
stage_index: number = 0,
|
||||
status: string = '',
|
||||
code: number = 1,
|
||||
message: string = '操作失败',
|
||||
data?: SseResponseData
|
||||
): SseResponse {
|
||||
return {
|
||||
code: code,
|
||||
stage: stage,
|
||||
stage_index: stage_index,
|
||||
status: status,
|
||||
message: message,
|
||||
data: data
|
||||
};
|
||||
}
|
||||
|
||||
static withCompletionStatus(
|
||||
stage: number,
|
||||
stage_index: number,
|
||||
status: string,
|
||||
message: string,
|
||||
completion_status: CompletionStatus
|
||||
): SseResponse {
|
||||
return {
|
||||
code: 0,
|
||||
stage: stage,
|
||||
stage_index: stage_index,
|
||||
status: status,
|
||||
message: message,
|
||||
data: null,
|
||||
completion_status: completion_status
|
||||
};
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
43
components/common/EmptyStateAnimation2.tsx
Normal file
43
components/common/EmptyStateAnimation2.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { ArrowUp } from 'lucide-react';
|
||||
import gsap from 'gsap';
|
||||
import { ImageWave } from '@/components/ui/ImageWave';
|
||||
|
||||
// 主组件
|
||||
export const EmptyStateAnimation = ({ className }: { className: string }) => {
|
||||
const imageUrls = [
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
|
||||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg',
|
||||
];
|
||||
|
||||
return (
|
||||
<ImageWave
|
||||
images={imageUrls}
|
||||
containerWidth="90vw"
|
||||
containerHeight="60vh"
|
||||
itemWidth="calc(var(--index) * 2)"
|
||||
itemHeight="calc(var(--index) * 12)"
|
||||
gap="0.1rem"
|
||||
autoAnimate={true}
|
||||
autoAnimateInterval={100}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -14,12 +14,8 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Sidebar collapsed={sidebarCollapsed} onToggle={setSidebarCollapsed} />
|
||||
<div className="w-full">
|
||||
<TopBar collapsed={sidebarCollapsed} onToggleSidebar={() => setSidebarCollapsed(!sidebarCollapsed)} />
|
||||
<main className="mt-16">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -6,6 +6,7 @@ import { usePathname } from 'next/navigation';
|
||||
import { cn } from '@/public/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { GradientText } from '@/components/ui/gradient-text';
|
||||
import {
|
||||
Home,
|
||||
FolderOpen,
|
||||
@ -46,7 +47,7 @@ export function Sidebar({ collapsed, onToggle }: SidebarProps) {
|
||||
{/* Backdrop */}
|
||||
{!collapsed && (
|
||||
<div
|
||||
className="fixed inset-0 bg-[#000000bf] z-40"
|
||||
className="fixed inset-0 bg-[#000000bf] z-[998]"
|
||||
onClick={() => onToggle(true)}
|
||||
/>
|
||||
)}
|
||||
@ -54,7 +55,7 @@ export function Sidebar({ collapsed, onToggle }: SidebarProps) {
|
||||
{/* Sidebar */}
|
||||
<div
|
||||
className={cn(
|
||||
'fixed left-0 top-0 z-50 h-full w-64 bg-[#131416] transition-transform duration-300',
|
||||
'fixed left-0 top-0 z-[999] h-full w-64 bg-[#131416] transition-transform duration-300',
|
||||
collapsed ? '-translate-x-full' : 'translate-x-0'
|
||||
)}
|
||||
>
|
||||
@ -62,8 +63,12 @@ export function Sidebar({ collapsed, onToggle }: SidebarProps) {
|
||||
<div className="flex h-16 items-center justify-between px-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Video className="h-8 w-8 text-primary" />
|
||||
<span className="text-xl font-bold text-primary">
|
||||
Movie Flow
|
||||
<span className="text-xl font-bold">
|
||||
<GradientText
|
||||
text="MovieFlow"
|
||||
startPercentage={30}
|
||||
endPercentage={70}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import '../pages/style/top-bar.css';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { GradientText } from '@/components/ui/gradient-text';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@ -36,7 +37,7 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed right-0 top-0 left-0 h-16 header">
|
||||
<div className="fixed right-0 top-0 left-0 h-16 header z-50">
|
||||
<div className="h-full flex items-center justify-between pr-6 pl-2">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Button className='button-NxtqWZ' variant="ghost" size="sm" onClick={onToggleSidebar}>
|
||||
@ -50,10 +51,22 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
|
||||
>
|
||||
<span className="translate">
|
||||
<span>
|
||||
<h1 className="logo text-2xl font-bold bg-gradient-to-r from-primary to-white bg-clip-text text-transparent">Movie Flow</h1>
|
||||
<h1 className="logo text-2xl font-bold">
|
||||
<GradientText
|
||||
text="MovieFlow"
|
||||
startPercentage={30}
|
||||
endPercentage={70}
|
||||
/>
|
||||
</h1>
|
||||
</span>
|
||||
<span>
|
||||
<h1 className="logo text-2xl font-bold bg-gradient-to-r from-primary to-white bg-clip-text text-transparent">Movie Flow</h1>
|
||||
<h1 className="logo text-2xl font-bold">
|
||||
<GradientText
|
||||
text="MovieFlow"
|
||||
startPercentage={30}
|
||||
endPercentage={70}
|
||||
/>
|
||||
</h1>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -1,62 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ArrowLeft, ChevronDown, ChevronUp, Video, ListOrdered, Play, Loader2, Pause, MoreHorizontal, Edit2, Check, X, RefreshCw, Lightbulb, Package, Crown, ArrowUp } from 'lucide-react';
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { ArrowLeft, ChevronDown, ChevronUp, Video, ListOrdered, Play, Loader2, Pause, MoreHorizontal, Edit2, Check, X, RefreshCw, Lightbulb, Package, Crown, ArrowUp, Search, Filter, Grid, Grid3X3, Calendar, Clock, Eye, Heart, Share2 } from 'lucide-react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger, SheetClose } from "@/components/ui/sheet";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import './style/create-to-video2.css';
|
||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import LiquidGlass from '@/plugins/liquid-glass/index'
|
||||
import { Dropdown } from 'antd';
|
||||
import { Dropdown, Menu } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import Image from 'next/image';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { ProjectTypeEnum, ModeEnum, ResolutionEnum } from "@/api/enums";
|
||||
import { createScriptEpisode, CreateScriptEpisodeRequest, updateScriptEpisode, UpdateScriptEpisodeRequest } from "@/api/script_episode";
|
||||
import { ModeEnum, ResolutionEnum } from "@/app/model/enums";
|
||||
import { createScriptEpisodeNew, getScriptEpisodeListNew, CreateScriptEpisodeRequest } from "@/api/script_episode";
|
||||
import { getUploadToken, uploadToQiniu } from "@/api/common";
|
||||
import { convertScriptToScene, convertVideoToScene } from "@/api/video_flow";
|
||||
import { EmptyStateAnimation } from '@/components/common/EmptyStateAnimation';
|
||||
import { EmptyStateAnimation } from '@/components/common/EmptyStateAnimation2';
|
||||
|
||||
const JoyrideNoSSR = dynamic(() => import('react-joyride'), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
// 导入Step类型
|
||||
import type { Step } from 'react-joyride';
|
||||
// interface Step {
|
||||
// target: string;
|
||||
// content: string;
|
||||
// placement?: 'top' | 'bottom' | 'left' | 'right';
|
||||
// }
|
||||
|
||||
// 添加自定义滚动条样式
|
||||
const scrollbarStyles = `
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
`;
|
||||
|
||||
interface SceneVideo {
|
||||
id: number;
|
||||
video_url: string;
|
||||
script: any;
|
||||
}
|
||||
|
||||
const ideaText = 'a cute capybara with an orange on its head, staring into the distance and walking forward';
|
||||
|
||||
@ -80,17 +38,69 @@ export function CreateToVideo2() {
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [generatedVideoList, setGeneratedVideoList] = useState<any[]>([]);
|
||||
const [projectName, setProjectName] = useState('默认名称');
|
||||
const [episodeList, setEpisodeList] = useState<any[]>([]);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPage, setTotalPage] = useState(1);
|
||||
const [limit, setLimit] = useState(12);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [filterStatus, setFilterStatus] = useState<string>('all');
|
||||
const [sortBy, setSortBy] = useState<string>('created_at');
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [isSmartAssistantExpanded, setIsSmartAssistantExpanded] = useState(false);
|
||||
const [userId, setUserId] = useState<number>(0);
|
||||
const [isComposing, setIsComposing] = useState(false);
|
||||
|
||||
// 在客户端挂载后读取localStorage
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const currentUser = JSON.parse(localStorage.getItem('currentUser') || '{}');
|
||||
console.log('currentUser', currentUser);
|
||||
setUserId(currentUser.id);
|
||||
const savedProjectName = localStorage.getItem('projectName');
|
||||
if (savedProjectName) {
|
||||
setProjectName(savedProjectName);
|
||||
}
|
||||
getEpisodeList(currentUser.id);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 获取剧集列表
|
||||
const getEpisodeList = async (userId: number) => {
|
||||
if (isLoading || isLoadingMore) return;
|
||||
console.log('getEpisodeList', userId);
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const params = {
|
||||
user_id: String(userId),
|
||||
};
|
||||
|
||||
const episodeListResponse = await getScriptEpisodeListNew(params);
|
||||
console.log('episodeListResponse', episodeListResponse);
|
||||
|
||||
if (episodeListResponse.code === 0) {
|
||||
setEpisodeList(episodeListResponse.data.movie_projects);
|
||||
// 每一项 有
|
||||
// final_video_url: "", // 生成的视频地址
|
||||
// last_message: "",
|
||||
// name: "After the Flood", // 剧集名称
|
||||
// project_id: "9c34fcc4-c8d8-44fc-879e-9bd56f608c76", // 剧集ID
|
||||
// status: "INIT", // 剧集状态 INIT 初始化
|
||||
// step: "INIT" // 剧集步骤 INIT 初始化
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch episode list:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsLoadingMore(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadVideo = async () => {
|
||||
console.log('upload video');
|
||||
// 打开文件选择器
|
||||
@ -124,73 +134,28 @@ export function CreateToVideo2() {
|
||||
}
|
||||
|
||||
const handleCreateVideo = async () => {
|
||||
setIsCreating(true);
|
||||
// 创建剧集数据
|
||||
const episodeData: CreateScriptEpisodeRequest = {
|
||||
title: "episode default",
|
||||
script_id: projectId,
|
||||
status: 1,
|
||||
summary: script
|
||||
let episodeData: any = {
|
||||
user_id: String(userId),
|
||||
script: script,
|
||||
mode: selectedMode,
|
||||
resolution: selectedResolution
|
||||
};
|
||||
|
||||
// 调用创建剧集API
|
||||
const episodeResponse = await createScriptEpisode(episodeData);
|
||||
const episodeResponse = await createScriptEpisodeNew(episodeData);
|
||||
console.log('episodeResponse', episodeResponse);
|
||||
if (episodeResponse.code !== 0) {
|
||||
console.error(`创建剧集失败: ${episodeResponse.message}`);
|
||||
alert(`创建剧集失败: ${episodeResponse.message}`);
|
||||
return;
|
||||
}
|
||||
let episodeId = episodeResponse.data.id;
|
||||
|
||||
if (videoUrl || script) {
|
||||
try {
|
||||
setIsCreating(true);
|
||||
let convertResponse;
|
||||
|
||||
// 根据选中的选项卡调用相应的API
|
||||
if (activeTab === 'script') {
|
||||
// 剧本模式:调用convertScriptToScene (第43-56行)
|
||||
if (!script.trim()) {
|
||||
alert('请输入剧本内容');
|
||||
return;
|
||||
}
|
||||
convertResponse = await convertScriptToScene(script, episodeId, projectId);
|
||||
} else {
|
||||
// 视频模式:调用convertVideoToScene (第56-69行)
|
||||
if (!videoUrl) {
|
||||
alert('请先上传视频');
|
||||
return;
|
||||
}
|
||||
if (!episodeId) {
|
||||
alert('Episode ID not available');
|
||||
return;
|
||||
}
|
||||
convertResponse = await convertVideoToScene(videoUrl, episodeId, projectId);
|
||||
}
|
||||
// 更新剧集
|
||||
const updateEpisodeData: UpdateScriptEpisodeRequest = {
|
||||
id: episodeId,
|
||||
atmosphere: convertResponse.data.atmosphere,
|
||||
summary: convertResponse.data.summary,
|
||||
scene: convertResponse.data.scene,
|
||||
characters: convertResponse.data.characters,
|
||||
};
|
||||
const updateEpisodeResponse = await updateScriptEpisode(updateEpisodeData);
|
||||
|
||||
// 检查转换结果
|
||||
if (convertResponse.code === 0) {
|
||||
// 成功创建后跳转到work-flow页面, 并设置episodeId 和 projectType
|
||||
router.push(`/create/work-flow?episodeId=${episodeResponse.data.id}`);
|
||||
} else {
|
||||
alert(`转换失败: ${convertResponse.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建过程出错:', error);
|
||||
alert("创建项目时发生错误,请稍后重试");
|
||||
} finally {
|
||||
let episodeId = episodeResponse.data.project_id;
|
||||
// let episodeId = '9c34fcc4-c8d8-44fc-879e-9bd56f608c76';
|
||||
router.push(`/create/work-flow?episodeId=${episodeId}`);
|
||||
setIsCreating(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 下拉菜单项配置
|
||||
const modeItems: MenuProps['items'] = [
|
||||
@ -206,9 +171,8 @@ export function CreateToVideo2() {
|
||||
<div className="flex flex-col gap-1 p-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-base font-medium">Auto</span>
|
||||
<Crown className="w-4 h-4 text-yellow-500" />
|
||||
</div>
|
||||
<span className="text-sm text-gray-400">Automatically selects the best model for optimal efficiency</span>
|
||||
<span className="text-sm text-gray-400">Automatically execute the workflow, you can't edit the workflow before it's finished.</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@ -220,7 +184,7 @@ export function CreateToVideo2() {
|
||||
<span className="text-base font-medium">Manual</span>
|
||||
<Crown className="w-4 h-4 text-yellow-500" />
|
||||
</div>
|
||||
<span className="text-sm text-gray-400">Offers reliable, consistent performance every time</span>
|
||||
<span className="text-sm text-gray-400">Manually control the workflow, you can control the workflow everywhere.</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@ -277,12 +241,12 @@ export function CreateToVideo2() {
|
||||
|
||||
// 处理模式选择
|
||||
const handleModeSelect: MenuProps['onClick'] = ({ key }) => {
|
||||
setSelectedMode(Number(key) as ModeEnum);
|
||||
setSelectedMode(key as ModeEnum);
|
||||
};
|
||||
|
||||
// 处理分辨率选择
|
||||
const handleResolutionSelect: MenuProps['onClick'] = ({ key }) => {
|
||||
setSelectedResolution(Number(key) as ResolutionEnum);
|
||||
setSelectedResolution(key as ResolutionEnum);
|
||||
};
|
||||
|
||||
const handleStartCreating = () => {
|
||||
@ -293,71 +257,65 @@ export function CreateToVideo2() {
|
||||
// 处理编辑器聚焦
|
||||
const handleEditorFocus = () => {
|
||||
setIsFocus(true);
|
||||
if (editorRef.current && script) {
|
||||
// 创建范围对象
|
||||
if (editorRef.current) {
|
||||
const range = document.createRange();
|
||||
const selection = window.getSelection();
|
||||
|
||||
// 获取编辑器内的文本节点
|
||||
const textNode = Array.from(editorRef.current.childNodes).find(
|
||||
node => node.nodeType === Node.TEXT_NODE
|
||||
) || editorRef.current.appendChild(document.createTextNode(script));
|
||||
);
|
||||
|
||||
// 设置范围到文本末尾
|
||||
range.setStart(textNode, script.length);
|
||||
range.setEnd(textNode, script.length);
|
||||
if (!textNode) {
|
||||
const newTextNode = document.createTextNode(script || '');
|
||||
editorRef.current.appendChild(newTextNode);
|
||||
range.setStart(newTextNode, (script || '').length);
|
||||
range.setEnd(newTextNode, (script || '').length);
|
||||
} else {
|
||||
range.setStart(textNode, textNode.textContent?.length || 0);
|
||||
range.setEnd(textNode, textNode.textContent?.length || 0);
|
||||
}
|
||||
|
||||
// 应用选择
|
||||
selection?.removeAllRanges();
|
||||
selection?.addRange(range);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理编辑器内容变化
|
||||
const handleEditorChange = (e: React.FormEvent<HTMLDivElement>) => {
|
||||
const script = e.currentTarget.textContent || '';
|
||||
setInputText(script);
|
||||
const handleCompositionStart = () => {
|
||||
setIsComposing(true);
|
||||
};
|
||||
|
||||
// 引导步骤
|
||||
const steps: Step[] = [
|
||||
{
|
||||
target: '.video-storyboard-tools',
|
||||
content: 'Welcome to AI Video Creation Tool! This is the main creation area.',
|
||||
placement: 'top',
|
||||
},
|
||||
{
|
||||
target: '.storyboard-tools-tab',
|
||||
content: 'Choose between Script mode to create videos from text, or Clone mode to recreate existing videos.',
|
||||
placement: 'bottom',
|
||||
},
|
||||
{
|
||||
target: '.video-prompt-editor',
|
||||
content: 'Describe your video content here. Our AI will generate videos based on your description.',
|
||||
placement: 'top',
|
||||
},
|
||||
{
|
||||
target: '.tool-operation-button',
|
||||
content: 'Select different creation modes and video resolutions to customize your output.',
|
||||
placement: 'top',
|
||||
},
|
||||
{
|
||||
target: '.tool-submit-button',
|
||||
content: 'Click here to start creating your video once you are ready!',
|
||||
placement: 'left',
|
||||
},
|
||||
];
|
||||
const handleCompositionEnd = (e: React.CompositionEvent<HTMLDivElement>) => {
|
||||
setIsComposing(false);
|
||||
handleEditorChange(e as any);
|
||||
};
|
||||
|
||||
// 处理引导结束
|
||||
const handleJoyrideCallback = (data: any) => {
|
||||
const { status } = data;
|
||||
if (status === 'finished' || status === 'skipped') {
|
||||
setRunTour(false);
|
||||
// 可以在这里存储用户已完成引导的状态
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('hasCompletedTour', 'true');
|
||||
const handleEditorChange = (e: React.FormEvent<HTMLDivElement>) => {
|
||||
// 如果正在输入中文,不要更新内容
|
||||
if (isComposing) return;
|
||||
|
||||
const newText = e.currentTarget.textContent || '';
|
||||
setInputText(newText);
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.rangeCount > 0) {
|
||||
const range = selection.getRangeAt(0);
|
||||
const currentPosition = range.startOffset;
|
||||
|
||||
setTimeout(() => {
|
||||
if (editorRef.current) {
|
||||
const textNode = Array.from(editorRef.current.childNodes).find(
|
||||
node => node.nodeType === Node.TEXT_NODE
|
||||
);
|
||||
|
||||
if (textNode) {
|
||||
const newRange = document.createRange();
|
||||
newRange.setStart(textNode, currentPosition);
|
||||
newRange.setEnd(textNode, currentPosition);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(newRange);
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
// 检查是否需要显示引导
|
||||
@ -374,56 +332,175 @@ export function CreateToVideo2() {
|
||||
setIsClient(true);
|
||||
}, []);
|
||||
|
||||
// 渲染剧集卡片
|
||||
const renderEpisodeCard = (episode: any) => {
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="container mx-auto overflow-hidden custom-scrollbar h-[calc(100vh-10rem)]"
|
||||
key={episode.project_id}
|
||||
className="group relative bg-white/[0.08] backdrop-blur-[20px] border border-white/[0.12] rounded-[12px] overflow-hidden hover:bg-white/[0.12] transition-all duration-300 hover:shadow-[0_8px_32px_rgba(0,0,0,0.4)] cursor-pointer"
|
||||
onClick={() => router.push(`/create/work-flow?episodeId=${episode.project_id}`)}
|
||||
>
|
||||
{isClient && (
|
||||
<JoyrideNoSSR
|
||||
steps={steps}
|
||||
run={runTour}
|
||||
continuous
|
||||
showSkipButton
|
||||
showProgress
|
||||
styles={{
|
||||
options: {
|
||||
primaryColor: '#4F46E5',
|
||||
zIndex: 10000,
|
||||
backgroundColor: '#1F2937',
|
||||
overlayColor: 'rgba(0, 0, 0, 0.5)',
|
||||
textColor: '#F3F4F6',
|
||||
arrowColor: '#1F2937',
|
||||
}
|
||||
}}
|
||||
disableOverlayClose
|
||||
spotlightClicks
|
||||
hideCloseButton
|
||||
callback={handleJoyrideCallback}
|
||||
locale={{
|
||||
back: 'Back',
|
||||
close: 'Close',
|
||||
last: 'Finish',
|
||||
next: 'Next',
|
||||
skip: 'Skip'
|
||||
}}
|
||||
{/* 视频缩略图 */}
|
||||
<div className="relative h-[180px] bg-gradient-to-br from-purple-500/20 to-blue-500/20 overflow-hidden">
|
||||
{episode.final_video_url ? (
|
||||
<video
|
||||
src={episode.final_video_url}
|
||||
className="w-full h-full object-cover"
|
||||
muted
|
||||
loop
|
||||
onMouseEnter={(e) => (e.target as HTMLVideoElement).play()}
|
||||
onMouseLeave={(e) => (e.target as HTMLVideoElement).pause()}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Video className="w-12 h-12 text-white/30" />
|
||||
</div>
|
||||
)}
|
||||
<div className='min-h-[100%] flex flex-col justify-center items-center'>
|
||||
{/* 空状态 */}
|
||||
<EmptyStateAnimation className='h-[calc(100vh - 20rem)]' />
|
||||
{/* 工具栏 */}
|
||||
<div className='video-tool-component relative w-[1080px]'>
|
||||
|
||||
{/* 播放按钮覆盖 */}
|
||||
<div className="absolute inset-0 bg-black/20 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<div className="w-12 h-12 bg-white/20 backdrop-blur-sm rounded-full flex items-center justify-center">
|
||||
<Play className="w-6 h-6 text-white ml-1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 状态标签 */}
|
||||
<div className="absolute top-3 left-3">
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
|
||||
episode.status === 'COMPLETED' ? 'bg-green-500/80 text-white' :
|
||||
episode.status !== 'COMPLETED' ? 'bg-yellow-500/80 text-white' :
|
||||
'bg-gray-500/80 text-white'
|
||||
}`}>
|
||||
{episode.status === 'COMPLETED' ? '已完成' : '进行中'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 时长标签 */}
|
||||
{episode.duration && (
|
||||
<div className="absolute bottom-3 right-3">
|
||||
<span className="px-2 py-1 bg-black/60 backdrop-blur-sm rounded text-xs text-white">
|
||||
{episode.duration}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<div className="p-4">
|
||||
<h3 className="text-white font-medium text-sm mb-2 line-clamp-2 group-hover:text-blue-300 transition-colors">
|
||||
{episode.name || '未命名剧集'}
|
||||
</h3>
|
||||
|
||||
{/* 元数据 */}
|
||||
<div className="flex items-center justify-between text-xs text-white/40">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="w-3 h-3" />
|
||||
<span>{new Date(episode.created_at).toLocaleDateString()}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
<span>{new Date(episode.updated_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="absolute top-3 right-3 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<div className="flex gap-2">
|
||||
<button className="w-8 h-8 bg-white/20 backdrop-blur-sm rounded-full flex items-center justify-center hover:bg-white/30 transition-colors">
|
||||
<Share2 className="w-4 h-4 text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="container mx-auto h-[calc(100vh-6rem)] flex flex-col absolute top-[4rem] left-0 right-0 px-[1rem]">
|
||||
{/* 优化后的主要内容区域 */}
|
||||
<div className="flex-1 min-h-0">
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className="h-full overflow-y-auto custom-scrollbar"
|
||||
style={{
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: 'rgba(255,255,255,0.1) transparent'
|
||||
}}
|
||||
>
|
||||
{isLoading && episodeList.length === 0 ? (
|
||||
/* 优化的加载状态 */
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6 pb-6">
|
||||
{[...Array(10)].map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-white/[0.05] backdrop-blur-[20px] border border-white/[0.08] rounded-2xl overflow-hidden animate-pulse"
|
||||
>
|
||||
<div className="h-[200px] bg-gradient-to-br from-white/[0.08] to-white/[0.04]">
|
||||
<div className="h-full bg-white/[0.06] animate-pulse"></div>
|
||||
</div>
|
||||
<div className="p-5">
|
||||
<div className="h-4 bg-white/[0.08] rounded-lg mb-3 animate-pulse"></div>
|
||||
<div className="h-3 bg-white/[0.06] rounded-lg mb-4 w-3/4 animate-pulse"></div>
|
||||
<div className="flex justify-between">
|
||||
<div className="h-3 bg-white/[0.06] rounded-lg w-20 animate-pulse"></div>
|
||||
<div className="h-3 bg-white/[0.06] rounded-lg w-16 animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : episodeList.length > 0 ? (
|
||||
/* 优化的剧集网格 */
|
||||
<div className="pb-8">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6">
|
||||
{episodeList.map(renderEpisodeCard)}
|
||||
</div>
|
||||
|
||||
{/* 加载更多指示器 */}
|
||||
{isLoadingMore && (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="flex items-center gap-3 px-6 py-3 bg-white/[0.05] backdrop-blur-[20px] border border-white/[0.08] rounded-xl">
|
||||
<Loader2 className="w-5 h-5 animate-spin text-blue-400" />
|
||||
<span className="text-white/70 font-medium">加载更多剧集中...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 到底提示 */}
|
||||
{!hasMore && episodeList.length > 0 && (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 bg-white/[0.05] backdrop-blur-[20px] border border-white/[0.08] rounded-xl flex items-center justify-center mx-auto mb-3">
|
||||
<Check className="w-6 h-6 text-green-400" />
|
||||
</div>
|
||||
<p className="text-white/50 text-sm">已加载全部剧集</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 创建工具栏 */}
|
||||
<div className='video-tool-component relative max-w-[1080px] w-full left-[50%] translate-x-[-50%]'>
|
||||
<div className='video-storyboard-tools grid gap-4 rounded-[20px] bg-white/[0.08] backdrop-blur-[20px] border border-white/[0.12] shadow-[0_8px_32px_rgba(0,0,0,0.3)]'>
|
||||
{isExpanded ? (
|
||||
<div className='absolute top-0 bottom-0 left-0 right-0 z-[1] grid justify-items-center place-content-center rounded-[20px] bg-[#191B1E] bg-opacity-[0.3] backdrop-blur-[1.5px] cursor-pointer' onClick={() => setIsExpanded(false)}>
|
||||
{/* 图标 展开按钮 */}
|
||||
<ChevronUp className='w-4 h-4' />
|
||||
<span className='text-sm'>Click to action</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className='absolute top-[-8px] left-[50%] z-[2] flex items-center justify-center w-[46px] h-4 rounded-[15px] bg-[#fff3] translate-x-[-50%] cursor-pointer hover:bg-[#ffffff1a]' onClick={() => setIsExpanded(true)}>
|
||||
{/* 图标 折叠按钮 */}
|
||||
<ChevronDown className='w-4 h-4' />
|
||||
</div>
|
||||
)}
|
||||
@ -443,11 +520,16 @@ export function CreateToVideo2() {
|
||||
<div className='relative flex items-center gap-4 h-[94px]'>
|
||||
<div className='relative w-[72px] rounded-[6px] overflow-hidden cursor-pointer ant-dropdown-trigger' onClick={handleUploadVideo}>
|
||||
<div className='relative flex items-center justify-center w-[72px] h-[72px] rounded-[6px 6px 0 0] bg-white/[0.05] transition-all overflow-hidden hover:bg-white/[0.10]'>
|
||||
{/* 图标 添加视频 */}
|
||||
{isUploading ? (
|
||||
<Loader2 className='w-4 h-4 animate-spin' />
|
||||
) : (
|
||||
<Video className='w-4 h-4' />
|
||||
)}
|
||||
</div>
|
||||
<div className='w-full h-[22px] flex items-center justify-center rounded-[0 0 6px 6px] bg-white/[0.03]'>
|
||||
<span className='text-xs opacity-30 cursor-[inherit]'>Add Video</span>
|
||||
<span className='text-xs opacity-30 cursor-[inherit]'>
|
||||
{isUploading ? '上传中...' : 'Add Video'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{videoUrl && (
|
||||
@ -470,6 +552,8 @@ export function CreateToVideo2() {
|
||||
onFocus={handleEditorFocus}
|
||||
onBlur={() => setIsFocus(false)}
|
||||
onInput={handleEditorChange}
|
||||
onCompositionStart={handleCompositionStart}
|
||||
onCompositionEnd={handleCompositionEnd}
|
||||
suppressContentEditableWarning
|
||||
>
|
||||
{script}
|
||||
@ -507,7 +591,7 @@ export function CreateToVideo2() {
|
||||
<span className='text-nowrap opacity-70'>
|
||||
{selectedMode === ModeEnum.AUTOMATIC ? 'Auto' : 'Manual'}
|
||||
</span>
|
||||
<Crown className='w-4 h-4 text-yellow-500' />
|
||||
<Crown className={`w-4 h-4 text-yellow-500 ${selectedMode === ModeEnum.AUTOMATIC ? 'hidden' : ''}`} />
|
||||
</div>
|
||||
</Dropdown>
|
||||
|
||||
@ -528,7 +612,7 @@ export function CreateToVideo2() {
|
||||
selectedResolution === ResolutionEnum.FULL_HD_1080P ? '1080P' :
|
||||
selectedResolution === ResolutionEnum.UHD_2K ? '2K' : '4K'}
|
||||
</span>
|
||||
<Crown className='w-4 h-4 text-yellow-500' />
|
||||
<Crown className={`w-4 h-4 text-yellow-500 ${selectedResolution === ResolutionEnum.HD_720P ? 'hidden' : ''}`} />
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
@ -554,7 +638,12 @@ export function CreateToVideo2() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{episodeList.length === 0 && !isLoading && (
|
||||
<div className='h-full flex flex-col items-center fixed top-[4rem] left-0 right-0 bottom-0'>
|
||||
<EmptyStateAnimation className='' />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,12 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { Plus, Table, AlignHorizontalSpaceAround, Loader2 } from "lucide-react";
|
||||
import { Table, AlignHorizontalSpaceAround, Loader2, Clapperboard } from "lucide-react";
|
||||
import "./style/home-page2.css";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { VideoScreenLayout } from '@/components/video-screen-layout';
|
||||
import { VideoCarouselLayout } from '@/components/video-carousel-layout';
|
||||
import { VideoGridLayout } from '@/components/video-grid-layout';
|
||||
import { motion } from "framer-motion";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { LiquidButton } from "@/components/ui/liquid-glass-button";
|
||||
import {
|
||||
createScriptProject,
|
||||
CreateScriptProjectRequest
|
||||
@ -15,7 +16,7 @@ import {
|
||||
ProjectTypeEnum,
|
||||
ModeEnum,
|
||||
ResolutionEnum
|
||||
} from '@/api/enums';
|
||||
} from '@/app/model/enums';
|
||||
import {
|
||||
getResourcesList,
|
||||
Resource
|
||||
@ -24,6 +25,7 @@ import {
|
||||
export function HomePage2() {
|
||||
const router = useRouter();
|
||||
const [activeTool, setActiveTool] = useState("stretch");
|
||||
const [dropPosition, setDropPosition] = useState<"left" | "right">("left");
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [createdProjectId, setCreatedProjectId] = useState<number | null>(null);
|
||||
const [resources, setResources] = useState<Resource[]>([]);
|
||||
@ -76,6 +78,8 @@ export function HomePage2() {
|
||||
|
||||
try {
|
||||
setIsCreating(true);
|
||||
router.push(`/create`);
|
||||
return;
|
||||
|
||||
// 使用默认值
|
||||
const projectType = ProjectTypeEnum.SCRIPT_TO_VIDEO;
|
||||
@ -105,46 +109,73 @@ export function HomePage2() {
|
||||
}
|
||||
};
|
||||
|
||||
// 处理工具切换
|
||||
const handleToolChange = (position: "left" | "right") => {
|
||||
setDropPosition(position);
|
||||
setActiveTool(position === "left" ? "stretch" : "table");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--background)]" ref={containerRef}>
|
||||
<div className="flex relative" style={{height: 'calc(100vh - 4rem)'}}>
|
||||
<div className="flex relative" style={{height: '100vh'}}>
|
||||
{/* 工具栏-列表形式切换 */}
|
||||
<div className="absolute top-[1rem] right-6 w-[8rem] flex justify-end">
|
||||
<div role="group" className="flex p-1 bg-white/20 backdrop-blur-[15px] w-full rounded-[3rem]">
|
||||
<button
|
||||
className={`flex items-center justify-center h-10 transition-all duration-300 w-1/2 rounded-[3rem]
|
||||
${activeTool === "stretch" ? "bg-white/20 text-white" : "hover:bg-white/10 text-white/30"}`}
|
||||
onClick={() => setActiveTool("stretch")}
|
||||
<div className="absolute top-[8rem] z-[50] right-6 w-[8rem] flex justify-end">
|
||||
<LiquidButton className="w-[8rem] h-[3rem] text-sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleToolChange(activeTool === "stretch" ? "right" : "left");
|
||||
}}
|
||||
>
|
||||
<AlignHorizontalSpaceAround className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
className={`flex items-center justify-center h-10 transition-all duration-300 w-1/2 rounded-[3rem]
|
||||
${activeTool === "table" ? "bg-white/20 text-white" : "hover:bg-white/10 text-white/30"}`}
|
||||
onClick={() => setActiveTool("table")}
|
||||
>
|
||||
<Table className="w-5 h-5" />
|
||||
</button>
|
||||
<div className="relative flex items-center justify-around gap-4 w-[8rem] h-[3rem] p-2">
|
||||
<div
|
||||
className={`cursor-pointer relative z-10 transition-opacity duration-300 ${activeTool === "stretch" ? "opacity-100" : "opacity-50"}`}>
|
||||
<AlignHorizontalSpaceAround className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<div
|
||||
className={`cursor-pointer relative z-10 transition-opacity duration-300 ${activeTool === "table" ? "opacity-100" : "opacity-50"}`}>
|
||||
<Table className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
{/* 水滴动画 */}
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
className="absolute w-10 h-8 bg-[rgba(255,255,255,0.35)] rounded-full backdrop-blur-[2px] z-[1]"
|
||||
initial={{ x: dropPosition === "left" ? -32 : 32 }}
|
||||
animate={{
|
||||
x: dropPosition === "left" ? -32 : 32,
|
||||
scale: [1, 1.2, 1],
|
||||
}}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 30,
|
||||
scale: {
|
||||
duration: 0.2,
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</LiquidButton>
|
||||
</div>
|
||||
|
||||
{/* 屏风式视频布局 */}
|
||||
<div
|
||||
className={`absolute top-[4rem] w-full transition-all duration-500
|
||||
className={`absolute w-full h-[calc(100vh - 4rem)] transition-all duration-500
|
||||
${activeTool === "stretch" ? "opacity-100 translate-x-0" : "opacity-0 translate-x-[-100%] pointer-events-none"}
|
||||
`}
|
||||
style={{
|
||||
height: 'calc(100% - 8rem)',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
alignItems: 'center',
|
||||
marginTop: '4rem'
|
||||
}}
|
||||
>
|
||||
<VideoScreenLayout videos={videos} />
|
||||
<VideoCarouselLayout videos={videos} />
|
||||
</div>
|
||||
|
||||
{/* 网格式视频布局 */}
|
||||
<div
|
||||
className={`absolute top-[4rem] w-full transition-all duration-500 max-h-[calc(100vh-8rem)] overflow-y-auto hide-scrollbar
|
||||
className={`absolute top-[8rem] w-full transition-all duration-500 max-h-[calc(100vh-8rem)] overflow-y-auto hide-scrollbar
|
||||
${activeTool === "table" ? "opacity-100 translate-x-0" : "opacity-0 translate-x-[100%] pointer-events-none"}
|
||||
`}
|
||||
>
|
||||
@ -155,37 +186,21 @@ export function HomePage2() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Create Project Button */}
|
||||
<div className="fixed bottom-[3rem] left-[50%] -translate-x-1/2 z-50">
|
||||
<motion.div
|
||||
className="relative group"
|
||||
whileHover={!isCreating ? { scale: 1.05 } : {}}
|
||||
whileTap={!isCreating ? { scale: 0.95 } : {}}
|
||||
onClick={handleCreateProject}
|
||||
>
|
||||
|
||||
{/* 玻璃按钮 */}
|
||||
<motion.div
|
||||
className={`add-project-btn relative flex items-center gap-3 px-6 py-4 rounded-2xl
|
||||
bg-white/20 backdrop-blur-md cursor-pointer
|
||||
shadow-[0_8px_32px_rgba(0,0,0,0.2)] group-hover:shadow-[0_8px_32px_rgba(0,0,0,0.4)]
|
||||
transition-all duration-300 ${isCreating ? 'opacity-70 cursor-not-allowed' : ''}`}
|
||||
initial={false}
|
||||
whileHover={!isCreating ? {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.15)"
|
||||
} : {}}
|
||||
>
|
||||
<div className="w-[8rem] h-[8rem] rounded-[50%] overflow-hidden fixed bottom-[3rem] left-[50%] -translate-x-1/2 z-50">
|
||||
<LiquidButton className="w-[8rem] h-[8rem] text-lg">
|
||||
<div className="flex items-center justify-center gap-2"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCreateProject();
|
||||
}}>
|
||||
{isCreating ? (
|
||||
<Loader2 className="w-6 h-6 text-white animate-spin" />
|
||||
) : (
|
||||
<Plus className="w-6 h-6 text-white" />
|
||||
<Clapperboard className="w-6 h-6 text-white" />
|
||||
)}
|
||||
<div className="btn-text text-lg font-medium bg-gradient-to-r text-white
|
||||
bg-clip-text text-transparent">
|
||||
{isCreating ? "Action..." : "Action"}
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</LiquidButton>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@ -73,7 +73,7 @@ export default function Login() {
|
||||
<div className="main-container login-page relative">
|
||||
{/* logo Movie Flow */}
|
||||
<div className='login-logo'>
|
||||
<span className="logo-heart">Movie Flow</span>
|
||||
<span className="logo-heart">MovieFlow</span>
|
||||
</div>
|
||||
|
||||
<div className="left-panel">
|
||||
|
||||
@ -7,7 +7,7 @@ import { Textarea } from '@/components/ui/textarea';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { ArrowLeft, Loader2 } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ModeEnum, ResolutionEnum } from "@/api/enums";
|
||||
import { ModeEnum, ResolutionEnum } from "@/app/model/enums";
|
||||
import { createScriptEpisode, CreateScriptEpisodeRequest, updateScriptEpisode, UpdateScriptEpisodeRequest } from "@/api/script_episode";
|
||||
import { convertScriptToScene } from "@/api/video_flow";
|
||||
|
||||
|
||||
@ -1,132 +0,0 @@
|
||||
.video-tool-component {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: 1rem;
|
||||
--tw-translate-x: calc(-50% + 34.5px);
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
|
||||
.video-storyboard-tools {
|
||||
border: 1px solid rgba(255, 255, 255, .2);
|
||||
box-shadow: 0 4px 20px #0009;
|
||||
}
|
||||
|
||||
.video-storyboard-tools .tool-submit-button {
|
||||
display: flex;
|
||||
height: 36px;
|
||||
width: 120px;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
border-radius: 10px;
|
||||
font-size: .875rem;
|
||||
line-height: 1.25rem;
|
||||
font-weight: 600;
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(29 33 41 / var(--tw-text-opacity));
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.video-storyboard-tools .tool-submit-button.disabled {
|
||||
background-color: #fff;
|
||||
opacity: .3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.storyboard-tools-tab {
|
||||
border-radius: 16px 16px 0 0;
|
||||
background: #ffffff0d;
|
||||
}
|
||||
|
||||
.storyboard-tools-tab .tab-item {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.storyboard-tools-tab .tab-item.active,
|
||||
.storyboard-tools-tab .tab-item.active span {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.storyboard-tools-tab .tab-item.active:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -8px;
|
||||
left: 50%;
|
||||
width: 30px;
|
||||
height: 2px;
|
||||
border-radius: 2px;
|
||||
background-color: #fff;
|
||||
transform: translate(-50%);
|
||||
}
|
||||
|
||||
.video-prompt-editor .editor-content {
|
||||
line-height: 26px;
|
||||
outline: none;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.video-prompt-editor .editor-content[contenteditable] {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.video-prompt-editor.focus {
|
||||
background-color: #ffffff0d;
|
||||
}
|
||||
|
||||
.underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
|
||||
.tool-scroll-box-content {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.tool-operation-button {
|
||||
display: flex;
|
||||
height: 36px;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border-radius: 20px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
font-size: .875rem;
|
||||
line-height: 1.25rem;
|
||||
background-color: #ffffff0d;
|
||||
}
|
||||
|
||||
.tool-operation-button:hover {
|
||||
background-color: #ffffff1a;
|
||||
}
|
||||
|
||||
/* 下拉菜单样式 */
|
||||
.mode-dropdown.ant-dropdown .ant-dropdown-menu {
|
||||
background: rgba(25, 27, 30, 0.95);
|
||||
backdrop-filter: blur(15px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 8px;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.mode-dropdown.ant-dropdown .ant-dropdown-menu-item {
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.mode-dropdown.ant-dropdown .ant-dropdown-menu-item:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.mode-dropdown.ant-dropdown .ant-dropdown-menu-item-selected {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
@ -1,9 +1,7 @@
|
||||
.video-tool-component {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: 1rem;
|
||||
--tw-translate-x: calc(-50% + 34.5px);
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.video-storyboard-tools {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
.add-project-btn .btn-text {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.add-project-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@ -6,7 +6,7 @@ import { Card } from '@/components/ui/card';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { ArrowLeft, Upload, Loader2 } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ModeEnum, ResolutionEnum } from "@/api/enums";
|
||||
import { ModeEnum, ResolutionEnum } from "@/app/model/enums";
|
||||
import { createScriptEpisode, CreateScriptEpisodeRequest, updateScriptEpisode, UpdateScriptEpisodeRequest } from "@/api/script_episode";
|
||||
import { convertVideoToScene } from "@/api/video_flow";
|
||||
import { getUploadToken, uploadToQiniu } from "@/api/common";
|
||||
|
||||
@ -8,7 +8,7 @@ import { ErrorBoundary } from "@/components/ui/error-boundary";
|
||||
import { TaskInfo } from "./work-flow/task-info";
|
||||
import { MediaViewer } from "./work-flow/media-viewer";
|
||||
import { ThumbnailGrid } from "./work-flow/thumbnail-grid";
|
||||
import { useWorkflowData } from "./work-flow/use-workflow-data";
|
||||
import { useWorkflowData } from "./work-flow/use-workflow-data.ts";
|
||||
import { usePlaybackControls } from "./work-flow/use-playback-controls";
|
||||
import { AlertCircle, RefreshCw } from "lucide-react";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
@ -572,6 +572,7 @@ export function MediaViewer({
|
||||
</AnimatePresence>
|
||||
|
||||
{/* 底部控制区域 */}
|
||||
{ taskVideos[currentSketchIndex] && (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
className="absolute bottom-4 left-4 z-[11] flex items-center gap-3"
|
||||
@ -613,6 +614,7 @@ export function MediaViewer({
|
||||
{renderVolumeControls()}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -56,10 +56,7 @@ export function TaskInfo({ isLoading, taskObject, currentLoadingText, dataLoadEr
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
{taskObject?.projectName ?
|
||||
`${taskObject.projectName}:${taskObject.taskName}` :
|
||||
'正在加载项目信息...'
|
||||
}
|
||||
{taskObject?.title || '正在加载项目信息...'}
|
||||
</motion.div>
|
||||
|
||||
{/* 加载状态显示 */}
|
||||
@ -157,7 +154,7 @@ export function TaskInfo({ isLoading, taskObject, currentLoadingText, dataLoadEr
|
||||
return (
|
||||
<>
|
||||
<div className="title-JtMejk">
|
||||
{taskObject?.projectName}:{taskObject?.taskName}
|
||||
{taskObject?.title || '正在加载项目信息...'}
|
||||
</div>
|
||||
|
||||
{currentLoadingText === 'Task completed' ? (
|
||||
|
||||
239
components/pages/work-flow/use-api-data.ts
Normal file
239
components/pages/work-flow/use-api-data.ts
Normal file
@ -0,0 +1,239 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { detailScriptEpisodeNew, getScriptTitle, getRunningStreamData, StreamData } from '@/api/video_flow';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
// 步骤映射
|
||||
const STEP_MAP = {
|
||||
'sketch': '1',
|
||||
'character': '2',
|
||||
'video': '3',
|
||||
'music': '4',
|
||||
'final_video': '6'
|
||||
} as const;
|
||||
|
||||
type ApiStep = keyof typeof STEP_MAP;
|
||||
|
||||
interface TaskData {
|
||||
sketch?: Array<{
|
||||
url: string;
|
||||
script: string;
|
||||
bg_rgb: string[];
|
||||
}>;
|
||||
roles?: Array<{
|
||||
name: string;
|
||||
url: string;
|
||||
sound: string;
|
||||
soundDescription: string;
|
||||
roleDescription: string;
|
||||
}>;
|
||||
videos?: Array<{
|
||||
url: string;
|
||||
script: string;
|
||||
audio: string;
|
||||
}>;
|
||||
music?: Array<{
|
||||
url: string;
|
||||
script: string;
|
||||
name: string;
|
||||
duration: string;
|
||||
totalDuration: string;
|
||||
isLooped: boolean;
|
||||
}>;
|
||||
final?: {
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const useApiData = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const episodeId = searchParams.get('episodeId');
|
||||
|
||||
const [title, setTitle] = useState<string>('');
|
||||
const [currentStep, setCurrentStep] = useState<string>('1');
|
||||
const [currentLoadingText, setCurrentLoadingText] = useState<string>('');
|
||||
const [needStreamData, setNeedStreamData] = useState<boolean>(false);
|
||||
const [taskData, setTaskData] = useState<TaskData>({});
|
||||
const [streamInterval, setStreamInterval] = useState<NodeJS.Timeout | null>(null);
|
||||
|
||||
// 处理流式数据
|
||||
const handleStreamData = useCallback((streamData: StreamData) => {
|
||||
const { category, message, data, status } = streamData;
|
||||
|
||||
// 更新加载文本
|
||||
setCurrentLoadingText(message);
|
||||
|
||||
// 根据类别更新任务数据
|
||||
setTaskData(prevData => {
|
||||
const newData = { ...prevData };
|
||||
|
||||
switch (category) {
|
||||
case 'sketch':
|
||||
if (!newData.sketch) newData.sketch = [];
|
||||
// 更新或追加分镜数据
|
||||
const existingSketchIndex = newData.sketch.findIndex(s => s.url === data.url);
|
||||
if (existingSketchIndex === -1) {
|
||||
newData.sketch.push(data);
|
||||
} else {
|
||||
newData.sketch[existingSketchIndex] = data;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'character':
|
||||
if (!newData.roles) newData.roles = [];
|
||||
// 更新或追加角色数据
|
||||
const existingRoleIndex = newData.roles.findIndex(r => r.name === data.name);
|
||||
if (existingRoleIndex === -1) {
|
||||
newData.roles.push(data);
|
||||
} else {
|
||||
newData.roles[existingRoleIndex] = data;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'video':
|
||||
if (!newData.videos) newData.videos = [];
|
||||
// 更新或追加视频数据
|
||||
const existingVideoIndex = newData.videos.findIndex(v => v.url === data.url);
|
||||
if (existingVideoIndex === -1) {
|
||||
newData.videos.push(data);
|
||||
} else {
|
||||
newData.videos[existingVideoIndex] = data;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'music':
|
||||
if (!newData.music) newData.music = [];
|
||||
newData.music = [data];
|
||||
break;
|
||||
|
||||
case 'final_video':
|
||||
newData.final = data;
|
||||
break;
|
||||
}
|
||||
|
||||
return newData;
|
||||
});
|
||||
|
||||
// 如果状态为 completed,停止获取流式数据
|
||||
if (status === 'completed' || streamData.all_completed) {
|
||||
setNeedStreamData(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 获取流式数据
|
||||
const fetchStreamData = useCallback(async () => {
|
||||
if (!episodeId || !needStreamData) return;
|
||||
|
||||
try {
|
||||
const streamData = await getRunningStreamData({ episodeId });
|
||||
handleStreamData(streamData);
|
||||
} catch (error) {
|
||||
console.error('获取流式数据失败:', error);
|
||||
}
|
||||
}, [episodeId, needStreamData, handleStreamData]);
|
||||
|
||||
// 启动流式数据轮询
|
||||
useEffect(() => {
|
||||
if (needStreamData && !streamInterval) {
|
||||
const interval = setInterval(fetchStreamData, 10000); // 修改为10秒
|
||||
setStreamInterval(interval);
|
||||
} else if (!needStreamData && streamInterval) {
|
||||
clearInterval(streamInterval);
|
||||
setStreamInterval(null);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (streamInterval) {
|
||||
clearInterval(streamInterval);
|
||||
}
|
||||
};
|
||||
}, [needStreamData, fetchStreamData, streamInterval]);
|
||||
|
||||
// 获取标题的轮询函数
|
||||
const pollTitle = useCallback(async () => {
|
||||
if (!episodeId) return;
|
||||
|
||||
try {
|
||||
const response = await getScriptTitle({ project_id: episodeId });
|
||||
if (response.successful && response.data) {
|
||||
setTitle(response.data.name);
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取标题失败:', error);
|
||||
}
|
||||
return false;
|
||||
}, [episodeId]);
|
||||
|
||||
// 获取剧集详情
|
||||
const fetchEpisodeDetail = useCallback(async () => {
|
||||
if (!episodeId) return;
|
||||
|
||||
try {
|
||||
const response = await detailScriptEpisodeNew({ project_id: episodeId });
|
||||
if (response.successful) {
|
||||
const { name, status, step, last_message, data } = response.data;
|
||||
|
||||
// 设置标题
|
||||
if (name) {
|
||||
setTitle(name);
|
||||
}
|
||||
|
||||
// 设置步骤
|
||||
if (step && STEP_MAP[step as ApiStep]) {
|
||||
setCurrentStep(STEP_MAP[step as ApiStep]);
|
||||
}
|
||||
|
||||
// 设置加载文本
|
||||
setCurrentLoadingText(last_message || '');
|
||||
|
||||
// 设置是否需要流式数据
|
||||
setNeedStreamData(status === 'running');
|
||||
|
||||
// 设置任务数据
|
||||
if (data) {
|
||||
setTaskData(data);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取剧集详情失败:', error);
|
||||
}
|
||||
return false;
|
||||
}, [episodeId]);
|
||||
|
||||
// 初始化数据
|
||||
useEffect(() => {
|
||||
let titleInterval: NodeJS.Timeout;
|
||||
|
||||
const initData = async () => {
|
||||
const detailSuccess = await fetchEpisodeDetail();
|
||||
|
||||
// 如果详情接口没有返回标题,开始轮询标题
|
||||
if (detailSuccess && !title) {
|
||||
titleInterval = setInterval(async () => {
|
||||
const success = await pollTitle();
|
||||
if (success) {
|
||||
clearInterval(titleInterval);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
initData();
|
||||
|
||||
return () => {
|
||||
if (titleInterval) {
|
||||
clearInterval(titleInterval);
|
||||
}
|
||||
};
|
||||
}, [episodeId, fetchEpisodeDetail, pollTitle, title]);
|
||||
|
||||
return {
|
||||
title,
|
||||
currentStep,
|
||||
currentLoadingText,
|
||||
needStreamData,
|
||||
taskData
|
||||
};
|
||||
};
|
||||
406
components/pages/work-flow/use-workflow-data.ts
Normal file
406
components/pages/work-flow/use-workflow-data.ts
Normal file
@ -0,0 +1,406 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { detailScriptEpisodeNew, getScriptTitle, getRunningStreamData } from '@/api/video_flow';
|
||||
|
||||
// 步骤映射
|
||||
const STEP_MAP = {
|
||||
'initializing': '0',
|
||||
'sketch': '1',
|
||||
'character': '2',
|
||||
'video': '3',
|
||||
'music': '4',
|
||||
'final_video': '6'
|
||||
} as const;
|
||||
// 执行loading文字映射
|
||||
const LOADING_TEXT_MAP = {
|
||||
initializing: 'initializing...',
|
||||
sketch: (count: number, total: number) => `Generating sketch ${count + 1 > total ? total : count + 1}/${total}...`,
|
||||
sketchComplete: 'Sketch generation complete',
|
||||
character: 'Drawing characters...',
|
||||
newCharacter: (count: number, total: number) => `Drawing character ${count + 1 > total ? total : count + 1}/${total}...`,
|
||||
video: (count: number, total: number) => `Generating video ${count + 1 > total ? total : count + 1}/${total}...`,
|
||||
videoComplete: 'Video generation complete',
|
||||
audio: 'Generating background audio...',
|
||||
postProduction: (step: string) => `Post-production: ${step}...`,
|
||||
final: 'Generating final product...',
|
||||
complete: 'Task completed'
|
||||
} as const;
|
||||
|
||||
type ApiStep = keyof typeof STEP_MAP;
|
||||
|
||||
// 添加 TaskObject 接口
|
||||
interface TaskObject {
|
||||
taskStatus: string;
|
||||
title: string;
|
||||
currentLoadingText: string;
|
||||
sketchCount?: number;
|
||||
totalSketchCount?: number;
|
||||
isGeneratingSketch?: boolean;
|
||||
isGeneratingVideo?: boolean;
|
||||
roles?: any[];
|
||||
music?: any[];
|
||||
final?: any;
|
||||
}
|
||||
|
||||
export function useWorkflowData() {
|
||||
const searchParams = useSearchParams();
|
||||
const episodeId = searchParams.get('episodeId');
|
||||
|
||||
// 更新 taskObject 的类型
|
||||
const [taskObject, setTaskObject] = useState<TaskObject | null>(null);
|
||||
const [taskSketch, setTaskSketch] = useState<any[]>([]);
|
||||
const [taskVideos, setTaskVideos] = useState<any[]>([]);
|
||||
const [sketchCount, setSketchCount] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [currentStep, setCurrentStep] = useState('0');
|
||||
const [currentSketchIndex, setCurrentSketchIndex] = useState(0);
|
||||
const [isGeneratingSketch, setIsGeneratingSketch] = useState(false);
|
||||
const [isGeneratingVideo, setIsGeneratingVideo] = useState(false);
|
||||
const [currentLoadingText, setCurrentLoadingText] = useState('正在加载项目数据...');
|
||||
const [totalSketchCount, setTotalSketchCount] = useState(0);
|
||||
const [roles, setRoles] = useState<any[]>([]);
|
||||
const [music, setMusic] = useState<any[]>([]);
|
||||
const [final, setFinal] = useState<any>(null);
|
||||
const [dataLoadError, setDataLoadError] = useState<string | null>(null);
|
||||
const [needStreamData, setNeedStreamData] = useState(false);
|
||||
|
||||
// 获取流式数据
|
||||
const fetchStreamData = async () => {
|
||||
if (!episodeId || !needStreamData) return;
|
||||
|
||||
try {
|
||||
const response = await getRunningStreamData({ project_id: episodeId });
|
||||
if (!response.successful) {
|
||||
throw new Error(response.message);
|
||||
}
|
||||
|
||||
let loadingText: any = LOADING_TEXT_MAP.initializing;
|
||||
let finalStep = '1', sketchCount = 0;
|
||||
const all_task_data = response.data;
|
||||
// all_task_data 下标0 和 下标1 换位置
|
||||
const temp = all_task_data[0];
|
||||
all_task_data[0] = all_task_data[1];
|
||||
all_task_data[1] = temp;
|
||||
|
||||
console.log('all_task_data', all_task_data);
|
||||
for (const task of all_task_data) {
|
||||
|
||||
// 如果有已完成的数据,同步到状态
|
||||
if (task.task_name === 'generate_sketch' && task.task_result) {
|
||||
if (task.task_result.data.length >= 0 && taskSketch.length !== task.task_result.data.length) {
|
||||
// 正在生成草图中 替换 sketch 数据
|
||||
const sketchList = [];
|
||||
for (const sketch of task.task_result.data) {
|
||||
sketchList.push({
|
||||
url: sketch.image_path,
|
||||
script: sketch.sketch_name
|
||||
});
|
||||
}
|
||||
setTaskSketch(sketchList);
|
||||
setSketchCount(sketchList.length);
|
||||
setIsGeneratingSketch(true);
|
||||
setCurrentSketchIndex(sketchList.length - 1);
|
||||
loadingText = LOADING_TEXT_MAP.sketch(sketchList.length, task.task_result.total_count);
|
||||
}
|
||||
if (task.task_status === 'COMPLETED') {
|
||||
// 草图生成完成
|
||||
setIsGeneratingSketch(false);
|
||||
sketchCount = task.task_result.total_count;
|
||||
console.log('----------草图生成完成', sketchCount);
|
||||
loadingText = LOADING_TEXT_MAP.sketchComplete;
|
||||
finalStep = '2';
|
||||
}
|
||||
setTotalSketchCount(task.task_result.total_count);
|
||||
}
|
||||
if (task.task_name === 'generate_character' && task.task_result) {
|
||||
if (task.task_result.data.length >= 0 && roles.length !== task.task_result.data.length) {
|
||||
// 正在生成角色中 替换角色数据
|
||||
const characterList = [];
|
||||
for (const character of task.task_result.data) {
|
||||
characterList.push({
|
||||
name: character.character_name,
|
||||
url: character.image_path,
|
||||
sound: null,
|
||||
soundDescription: '',
|
||||
roleDescription: ''
|
||||
});
|
||||
}
|
||||
setRoles(characterList);
|
||||
loadingText = LOADING_TEXT_MAP.newCharacter(characterList.length, task.task_result.total_count);
|
||||
}
|
||||
if (task.task_status === 'COMPLETED') {
|
||||
console.log('----------角色生成完成,有几个分镜', sketchCount);
|
||||
// 角色生成完成
|
||||
finalStep = '3';
|
||||
|
||||
loadingText = LOADING_TEXT_MAP.video(0, sketchCount);
|
||||
}
|
||||
}
|
||||
if (task.task_name === 'generate_videos' && task.task_result) {
|
||||
if (task.task_result.data.length >= 0 && taskVideos.length !== task.task_result.data.length) {
|
||||
// 正在生成视频中 替换视频数据
|
||||
const videoList = [];
|
||||
for (const video of task.task_result.data) {
|
||||
// 每一项 video 有多个视频 先默认取第一个
|
||||
videoList.push({
|
||||
url: video[0].qiniuVideoUrl,
|
||||
script: video[0].operation.metadata.video.prompt,
|
||||
audio: null,
|
||||
});
|
||||
}
|
||||
setTaskVideos(videoList);
|
||||
setIsGeneratingVideo(true);
|
||||
setCurrentSketchIndex(videoList.length - 1);
|
||||
loadingText = LOADING_TEXT_MAP.video(videoList.length, task.task_result.total_count);
|
||||
}
|
||||
if (task.task_status === 'COMPLETED') {
|
||||
console.log('----------视频生成完成');
|
||||
// 视频生成完成
|
||||
setIsGeneratingVideo(false);
|
||||
finalStep = '4';
|
||||
|
||||
// 暂时没有音频生成 直接跳过
|
||||
finalStep = '5';
|
||||
loadingText = LOADING_TEXT_MAP.postProduction('post-production...');
|
||||
}
|
||||
}
|
||||
if (task.task_name === 'generate_final_video') {
|
||||
if (task.task_result && task.task_result.video) {
|
||||
setFinal({
|
||||
url: task.task_result.video,
|
||||
})
|
||||
finalStep = '6';
|
||||
loadingText = LOADING_TEXT_MAP.complete;
|
||||
|
||||
// 停止轮询
|
||||
setNeedStreamData(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('----------finalStep', finalStep);
|
||||
// 设置步骤
|
||||
setCurrentStep(finalStep);
|
||||
setTaskObject(prev => {
|
||||
if (!prev) return null;
|
||||
return {
|
||||
...prev,
|
||||
taskStatus: finalStep
|
||||
};
|
||||
});
|
||||
setCurrentLoadingText(loadingText);
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 轮询获取流式数据
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout;
|
||||
|
||||
if (needStreamData) {
|
||||
interval = setInterval(fetchStreamData, 10000);
|
||||
fetchStreamData(); // 立即执行一次
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
};
|
||||
}, [needStreamData]);
|
||||
|
||||
// 初始化数据
|
||||
const initializeWorkflow = async () => {
|
||||
if (!episodeId) {
|
||||
setDataLoadError('缺少必要的参数');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setCurrentLoadingText('正在加载项目数据...');
|
||||
|
||||
// 获取剧集详情
|
||||
const response = await detailScriptEpisodeNew({ project_id: episodeId });
|
||||
if (!response.successful) {
|
||||
throw new Error(response.message);
|
||||
}
|
||||
|
||||
const { name, status, data } = response.data;
|
||||
setIsLoading(false);
|
||||
|
||||
// 设置初始数据
|
||||
setTaskObject({
|
||||
taskStatus: '0',
|
||||
title: name || 'generating...',
|
||||
currentLoadingText: status === 'COMPLETED' ? LOADING_TEXT_MAP.complete : LOADING_TEXT_MAP.initializing
|
||||
});
|
||||
|
||||
// 设置标题
|
||||
if (!name) {
|
||||
// 如果没有标题,轮询获取
|
||||
const titleResponse = await getScriptTitle({ project_id: episodeId });
|
||||
console.log('titleResponse', titleResponse);
|
||||
if (titleResponse.successful) {
|
||||
setTaskObject((prev: TaskObject | null) => ({
|
||||
...(prev || {}),
|
||||
title: titleResponse.data.name
|
||||
} as TaskObject));
|
||||
}
|
||||
}
|
||||
|
||||
let loadingText: any = LOADING_TEXT_MAP.initializing;
|
||||
if (status === 'COMPLETED') {
|
||||
loadingText = LOADING_TEXT_MAP.complete;
|
||||
}
|
||||
|
||||
// 如果有已完成的数据,同步到状态
|
||||
let finalStep = '1';
|
||||
if (data) {
|
||||
if (data.sketch && data.sketch.data && data.sketch.data.length > 0) {
|
||||
const sketchList = [];
|
||||
for (const sketch of data.sketch.data) {
|
||||
sketchList.push({
|
||||
url: sketch.image_path,
|
||||
script: sketch.sketch_name,
|
||||
});
|
||||
}
|
||||
setTaskSketch(sketchList);
|
||||
setSketchCount(sketchList.length);
|
||||
setTotalSketchCount(data.sketch.total_count);
|
||||
// 设置为最后一个草图
|
||||
if (data.sketch.total_count > data.sketch.data.length) {
|
||||
setIsGeneratingSketch(true);
|
||||
setCurrentSketchIndex(data.sketch.data.length - 1);
|
||||
loadingText = LOADING_TEXT_MAP.sketch(data.sketch.data.length, data.sketch.total_count);
|
||||
} else {
|
||||
finalStep = '2';
|
||||
if (!data.character || !data.character.data || !data.character.data.length) {
|
||||
loadingText = LOADING_TEXT_MAP.newCharacter(0, data.character.total_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.character && data.character.data && data.character.data.length > 0) {
|
||||
const characterList = [];
|
||||
for (const character of data.character.data) {
|
||||
characterList.push({
|
||||
name: character.character_name,
|
||||
url: character.image_path,
|
||||
sound: null,
|
||||
soundDescription: '',
|
||||
roleDescription: ''
|
||||
});
|
||||
}
|
||||
setRoles(characterList);
|
||||
if (data.character.total_count > data.character.data.length) {
|
||||
loadingText = LOADING_TEXT_MAP.newCharacter(data.character.data.length, data.character.total_count);
|
||||
} else {
|
||||
finalStep = '3';
|
||||
if (!data.video || !data.video.data || !data.video.data.length) {
|
||||
loadingText = LOADING_TEXT_MAP.video(0, data.video.total_count || data.sketch.total_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.video && data.video.data && data.video.data.length > 0) {
|
||||
const videoList = [];
|
||||
for (const video of data.video.data) {
|
||||
// 每一项 video 有多个视频 先默认取第一个
|
||||
videoList.push({
|
||||
url: video[0].qiniuVideoUrl,
|
||||
script: video[0].operation.metadata.video.prompt,
|
||||
audio: null,
|
||||
});
|
||||
}
|
||||
setTaskVideos(videoList);
|
||||
// 如果在视频步骤,设置为最后一个视频
|
||||
if (data.video.total_count > data.video.data.length) {
|
||||
setIsGeneratingVideo(true);
|
||||
setCurrentSketchIndex(data.video.data.length - 1);
|
||||
loadingText = LOADING_TEXT_MAP.video(data.video.data.length, data.video.total_count);
|
||||
} else {
|
||||
finalStep = '4';
|
||||
loadingText = LOADING_TEXT_MAP.audio;
|
||||
|
||||
// 暂时没有音频生成 直接跳过
|
||||
finalStep = '5';
|
||||
loadingText = LOADING_TEXT_MAP.postProduction('post-production...');
|
||||
}
|
||||
}
|
||||
if (data.final_video && data.final_video.video) {
|
||||
setFinal({
|
||||
url: data.final_video.video
|
||||
});
|
||||
finalStep = '6';
|
||||
loadingText = LOADING_TEXT_MAP.complete;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置步骤
|
||||
setCurrentStep(finalStep);
|
||||
setTaskObject(prev => {
|
||||
if (!prev) return null;
|
||||
return {
|
||||
...prev,
|
||||
taskStatus: finalStep
|
||||
};
|
||||
});
|
||||
console.log('---------loadingText', loadingText);
|
||||
setCurrentLoadingText(loadingText);
|
||||
|
||||
// 设置是否需要获取流式数据
|
||||
setNeedStreamData(status !== 'COMPLETED' && finalStep !== '6');
|
||||
|
||||
} catch (error) {
|
||||
console.error('初始化失败:', error);
|
||||
setDataLoadError('加载失败,请重试');
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 重试加载数据
|
||||
const retryLoadData = () => {
|
||||
setDataLoadError(null);
|
||||
// 重置所有状态
|
||||
setTaskSketch([]);
|
||||
setTaskVideos([]);
|
||||
setSketchCount(0);
|
||||
setTotalSketchCount(0);
|
||||
setRoles([]);
|
||||
setMusic([]);
|
||||
setFinal(null);
|
||||
setCurrentSketchIndex(0);
|
||||
setCurrentStep('0');
|
||||
// 重新初始化
|
||||
initializeWorkflow();
|
||||
};
|
||||
|
||||
// 初始化
|
||||
useEffect(() => {
|
||||
initializeWorkflow();
|
||||
}, [episodeId]);
|
||||
|
||||
return {
|
||||
taskObject,
|
||||
taskSketch,
|
||||
taskVideos,
|
||||
sketchCount,
|
||||
isLoading,
|
||||
currentStep,
|
||||
currentSketchIndex,
|
||||
isGeneratingSketch,
|
||||
isGeneratingVideo,
|
||||
currentLoadingText,
|
||||
totalSketchCount,
|
||||
roles,
|
||||
music,
|
||||
final,
|
||||
dataLoadError,
|
||||
setCurrentSketchIndex,
|
||||
retryLoadData,
|
||||
};
|
||||
}
|
||||
@ -35,24 +35,7 @@ export function useWorkflowData() {
|
||||
setCurrentLoadingText('项目数据加载完成');
|
||||
|
||||
} catch (error) {
|
||||
console.warn('接口获取数据失败,使用本地fallback数据:', error);
|
||||
|
||||
try {
|
||||
// 使用本地fallback数据
|
||||
setCurrentLoadingText('正在加载本地数据...');
|
||||
const randomIndex = Math.floor(Math.random() * MOCK_DATA.length);
|
||||
selectedMockData = MOCK_DATA[randomIndex];
|
||||
|
||||
console.log('使用本地fallback数据:', selectedMockData);
|
||||
setCurrentLoadingText('本地数据加载完成');
|
||||
|
||||
} catch (fallbackError) {
|
||||
console.error('本地数据加载也失败:', fallbackError);
|
||||
// 最后的fallback - 直接使用第一组数据
|
||||
selectedMockData = MOCK_DATA[0];
|
||||
setDataLoadError('服务器连接失败,已切换到演示模式');
|
||||
setCurrentLoadingText('演示数据加载完成');
|
||||
}
|
||||
// 报错
|
||||
} finally {
|
||||
setIsLoadingData(false);
|
||||
}
|
||||
|
||||
198
components/ui/ImageWave.tsx
Normal file
198
components/ui/ImageWave.tsx
Normal file
@ -0,0 +1,198 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface ImageWaveProps {
|
||||
// 图片列表数据
|
||||
images: string[];
|
||||
// 容器宽度
|
||||
containerWidth?: string;
|
||||
// 容器高度
|
||||
containerHeight?: string;
|
||||
// 单个图片宽度
|
||||
itemWidth?: string;
|
||||
// 单个图片高度
|
||||
itemHeight?: string;
|
||||
// 图片间距
|
||||
gap?: string;
|
||||
// 是否开启自动动画
|
||||
autoAnimate?: boolean;
|
||||
// 自动动画间隔时间(ms)
|
||||
autoAnimateInterval?: number;
|
||||
}
|
||||
|
||||
const Wrapper = styled.div<{ width?: string; height?: string }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: ${props => props.width || '100vw'};
|
||||
height: ${props => props.height || '100vh'};
|
||||
background-color: transparent;
|
||||
`;
|
||||
|
||||
const Items = styled.div<{ gap?: string }>`
|
||||
display: flex;
|
||||
gap: ${props => props.gap || '0.4rem'};
|
||||
perspective: calc(var(--index) * 35);
|
||||
|
||||
&.has-expanded .item:not(.expanded) {
|
||||
filter: grayscale(1) brightness(0.3);
|
||||
}
|
||||
|
||||
&.has-expanded .item:hover {
|
||||
filter: grayscale(1) brightness(0.3);
|
||||
}
|
||||
|
||||
&.has-expanded .item.expanded:hover {
|
||||
filter: inherit;
|
||||
transform: translateZ(calc(var(--index) * 10));
|
||||
}
|
||||
`;
|
||||
|
||||
const Item = styled.div<{ width?: string; height?: string }>`
|
||||
width: ${props => props.width || 'calc(var(--index) * 3)'};
|
||||
height: ${props => props.height || 'calc(var(--index) * 12)'};
|
||||
background-color: #222;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
cursor: pointer;
|
||||
filter: grayscale(1) brightness(0.5);
|
||||
transition: transform 1.25s var(--transition),
|
||||
filter 3s var(--transition),
|
||||
width 1.25s var(--transition),
|
||||
margin 1.25s var(--transition);
|
||||
will-change: transform, filter, rotateY, width;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 100%;
|
||||
right: calc(var(--index) * -1);
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: calc(var(--index) * -1);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
filter: inherit;
|
||||
transform: translateZ(calc(var(--index) * 7.8));
|
||||
}
|
||||
&:hover + * {
|
||||
filter: inherit;
|
||||
transform: translateZ(calc(var(--index) * 6.8)) rotateY(35deg);
|
||||
z-index: -1;
|
||||
}
|
||||
&:hover + * + * {
|
||||
filter: inherit;
|
||||
transform: translateZ(calc(var(--index) * 5.6)) rotateY(40deg);
|
||||
z-index: -2;
|
||||
}
|
||||
&:hover + * + * + * {
|
||||
filter: inherit;
|
||||
transform: translateZ(calc(var(--index) * 3.6)) rotateY(30deg);
|
||||
z-index: -3;
|
||||
}
|
||||
&:hover + * + * + * + * {
|
||||
filter: inherit;
|
||||
transform: translateZ(calc(var(--index) * .6)) rotateY(15deg);
|
||||
z-index: -4;
|
||||
}
|
||||
&:has(+ :hover) {
|
||||
filter: inherit;
|
||||
transform: translateZ(calc(var(--index) * 6.8)) rotateY(-35deg);
|
||||
}
|
||||
&:has(+ * + :hover) {
|
||||
filter: inherit;
|
||||
transform: translateZ(calc(var(--index) * 5.6)) rotateY(-40deg);
|
||||
}
|
||||
&:has(+ * + * + :hover) {
|
||||
filter: inherit;
|
||||
transform: translateZ(calc(var(--index) * 3.6)) rotateY(-30deg);
|
||||
}
|
||||
&:has(+ * + * + * + :hover) {
|
||||
filter: inherit;
|
||||
transform: translateZ(calc(var(--index) * .6)) rotateY(-15deg);
|
||||
}
|
||||
|
||||
&.expanded {
|
||||
width: 28vw;
|
||||
filter: inherit;
|
||||
z-index: 100;
|
||||
transform: translateZ(calc(var(--index) * 7.8));
|
||||
margin: 0.45vw;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ImageWave: React.FC<ImageWaveProps> = ({
|
||||
images,
|
||||
containerWidth,
|
||||
containerHeight,
|
||||
itemWidth,
|
||||
itemHeight,
|
||||
gap,
|
||||
autoAnimate = false,
|
||||
autoAnimateInterval = 2000,
|
||||
}) => {
|
||||
const [currentExpandedItem, setCurrentExpandedItem] = useState<number | null>(null);
|
||||
const itemsRef = useRef<HTMLDivElement>(null);
|
||||
const autoAnimateRef = useRef<number | null>(null);
|
||||
const currentIndexRef = useRef<number>(0);
|
||||
|
||||
const handleItemClick = (index: number) => {
|
||||
if (currentExpandedItem === index) {
|
||||
setCurrentExpandedItem(null);
|
||||
} else {
|
||||
setCurrentExpandedItem(index);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOutsideClick = useCallback((e: MouseEvent) => {
|
||||
if (itemsRef.current && !itemsRef.current.contains(e.target as Node)) {
|
||||
setCurrentExpandedItem(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
setCurrentExpandedItem(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('click', handleOutsideClick);
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handleOutsideClick);
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [handleOutsideClick, handleKeyDown]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
return () => {
|
||||
if (autoAnimateRef.current) {
|
||||
clearTimeout(autoAnimateRef.current);
|
||||
}
|
||||
};
|
||||
}, [autoAnimate]);
|
||||
|
||||
return (
|
||||
<Wrapper width={containerWidth} height={containerHeight}>
|
||||
<Items ref={itemsRef} gap={gap} className={currentExpandedItem !== null ? 'has-expanded' : ''}>
|
||||
{images.map((image, index) => (
|
||||
<Item
|
||||
key={index}
|
||||
width={itemWidth}
|
||||
height={itemHeight}
|
||||
className={`item ${currentExpandedItem === index ? 'expanded' : ''}`}
|
||||
style={{ backgroundImage: `url(${image})` }}
|
||||
onClick={() => handleItemClick(index)}
|
||||
tabIndex={0}
|
||||
/>
|
||||
))}
|
||||
</Items>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
37
components/ui/gradient-text.tsx
Normal file
37
components/ui/gradient-text.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { cn } from "@/public/lib/utils";
|
||||
|
||||
interface GradientTextProps {
|
||||
text: string;
|
||||
startColor?: string;
|
||||
endColor?: string;
|
||||
startPercentage?: number;
|
||||
endPercentage?: number;
|
||||
className?: string;
|
||||
glowIntensity?: {
|
||||
primary?: number;
|
||||
secondary?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export function GradientText({
|
||||
text,
|
||||
startColor = '#6AF4F9',
|
||||
endColor = '#C73BFF',
|
||||
startPercentage = 0,
|
||||
endPercentage = 100,
|
||||
className
|
||||
}: GradientTextProps) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"bg-gradient-to-r bg-clip-text text-transparent",
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(to right, ${startColor} ${startPercentage}%, ${endColor} ${endPercentage}%)`
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
384
components/ui/liquid-glass-button.tsx
Normal file
384
components/ui/liquid-glass-button.tsx
Normal file
@ -0,0 +1,384 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center cursor-pointer justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-primary-foreground hover:bg-destructive/90",
|
||||
cool: "dark:inset-shadow-2xs dark:inset-shadow-white/10 bg-linear-to-t border border-b-2 border-zinc-950/40 from-primary to-primary/85 shadow-md shadow-primary/20 ring-1 ring-inset ring-white/25 transition-[filter] duration-200 hover:brightness-110 active:brightness-90 dark:border-x-0 text-primary-foreground dark:text-primary-foreground dark:border-t-0 dark:border-primary/50 dark:ring-white/5",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants, liquidbuttonVariants, LiquidButton }
|
||||
|
||||
const liquidbuttonVariants = cva(
|
||||
"inline-flex items-center transition-colors justify-center cursor-pointer gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent hover:scale-105 duration-300 transition text-primary",
|
||||
destructive:
|
||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 text-xs gap-1.5 px-4 has-[>svg]:px-4",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
xl: "h-12 rounded-md px-8 has-[>svg]:px-6",
|
||||
xxl: "h-14 rounded-md px-10 has-[>svg]:px-8",
|
||||
icon: "size-9",
|
||||
custom: "h-[60px] w-[200px] text-base",
|
||||
square: "w-[120px] h-[120px] text-lg", // 添加正方形尺寸
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "xxl",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function LiquidButton({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof liquidbuttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<>
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(
|
||||
"relative",
|
||||
liquidbuttonVariants({ variant, size, className })
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="absolute top-0 left-0 z-0 h-full w-full rounded-full
|
||||
shadow-[0_0_6px_rgba(0,0,0,0.03),0_2px_6px_rgba(0,0,0,0.08),inset_3px_3px_0.5px_-3px_rgba(0,0,0,0.9),inset_-3px_-3px_0.5px_-3px_rgba(0,0,0,0.85),inset_1px_1px_1px_-0.5px_rgba(0,0,0,0.6),inset_-1px_-1px_1px_-0.5px_rgba(0,0,0,0.6),inset_0_0_6px_6px_rgba(0,0,0,0.12),inset_0_0_2px_2px_rgba(0,0,0,0.06),0_0_12px_rgba(255,255,255,0.15)]
|
||||
transition-all
|
||||
dark:shadow-[0_0_8px_rgba(0,0,0,0.03),0_2px_6px_rgba(0,0,0,0.08),inset_3px_3px_0.5px_-3.5px_rgba(255,255,255,0.09),inset_-3px_-3px_0.5px_-3.5px_rgba(255,255,255,0.85),inset_1px_1px_1px_-0.5px_rgba(255,255,255,0.6),inset_-1px_-1px_1px_-0.5px_rgba(255,255,255,0.6),inset_0_0_6px_6px_rgba(255,255,255,0.12),inset_0_0_2px_2px_rgba(255,255,255,0.06),0_0_12px_rgba(0,0,0,0.15)]" />
|
||||
<div
|
||||
className="absolute top-0 left-0 isolate -z-10 h-full w-full overflow-hidden rounded-md"
|
||||
style={{ backdropFilter: 'url("#container-glass")' }}
|
||||
/>
|
||||
|
||||
<div className="z-10 pointer-events-auto">
|
||||
{children}
|
||||
</div>
|
||||
<GlassFilter />
|
||||
</Comp>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
function GlassFilter() {
|
||||
return (
|
||||
<svg className="hidden">
|
||||
<defs>
|
||||
<filter
|
||||
id="container-glass"
|
||||
x="0%"
|
||||
y="0%"
|
||||
width="100%"
|
||||
height="100%"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
{/* Generate turbulent noise for distortion */}
|
||||
<feTurbulence
|
||||
type="fractalNoise"
|
||||
baseFrequency="0.05 0.05"
|
||||
numOctaves="1"
|
||||
seed="1"
|
||||
result="turbulence"
|
||||
/>
|
||||
|
||||
{/* Blur the turbulence pattern slightly */}
|
||||
<feGaussianBlur in="turbulence" stdDeviation="2" result="blurredNoise" />
|
||||
|
||||
{/* Displace the source graphic with the noise */}
|
||||
<feDisplacementMap
|
||||
in="SourceGraphic"
|
||||
in2="blurredNoise"
|
||||
scale="70"
|
||||
xChannelSelector="R"
|
||||
yChannelSelector="B"
|
||||
result="displaced"
|
||||
/>
|
||||
|
||||
{/* Apply overall blur on the final result */}
|
||||
<feGaussianBlur in="displaced" stdDeviation="4" result="finalBlur" />
|
||||
|
||||
{/* Output the result */}
|
||||
<feComposite in="finalBlur" in2="finalBlur" operator="over" />
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
type ColorVariant =
|
||||
| "default"
|
||||
| "primary"
|
||||
| "success"
|
||||
| "error"
|
||||
| "gold"
|
||||
| "bronze";
|
||||
|
||||
interface MetalButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: ColorVariant;
|
||||
}
|
||||
|
||||
const colorVariants: Record<
|
||||
ColorVariant,
|
||||
{
|
||||
outer: string;
|
||||
inner: string;
|
||||
button: string;
|
||||
textColor: string;
|
||||
textShadow: string;
|
||||
}
|
||||
> = {
|
||||
default: {
|
||||
outer: "bg-gradient-to-b from-[#000] to-[#A0A0A0]",
|
||||
inner: "bg-gradient-to-b from-[#FAFAFA] via-[#3E3E3E] to-[#E5E5E5]",
|
||||
button: "bg-gradient-to-b from-[#B9B9B9] to-[#969696]",
|
||||
textColor: "text-white",
|
||||
textShadow: "[text-shadow:_0_-1px_0_rgb(80_80_80_/_100%)]",
|
||||
},
|
||||
primary: {
|
||||
outer: "bg-gradient-to-b from-[#000] to-[#A0A0A0]",
|
||||
inner: "bg-gradient-to-b from-primary via-secondary to-muted",
|
||||
button: "bg-gradient-to-b from-primary to-primary/40",
|
||||
textColor: "text-white",
|
||||
textShadow: "[text-shadow:_0_-1px_0_rgb(30_58_138_/_100%)]",
|
||||
},
|
||||
success: {
|
||||
outer: "bg-gradient-to-b from-[#005A43] to-[#7CCB9B]",
|
||||
inner: "bg-gradient-to-b from-[#E5F8F0] via-[#00352F] to-[#D1F0E6]",
|
||||
button: "bg-gradient-to-b from-[#9ADBC8] to-[#3E8F7C]",
|
||||
textColor: "text-[#FFF7F0]",
|
||||
textShadow: "[text-shadow:_0_-1px_0_rgb(6_78_59_/_100%)]",
|
||||
},
|
||||
error: {
|
||||
outer: "bg-gradient-to-b from-[#5A0000] to-[#FFAEB0]",
|
||||
inner: "bg-gradient-to-b from-[#FFDEDE] via-[#680002] to-[#FFE9E9]",
|
||||
button: "bg-gradient-to-b from-[#F08D8F] to-[#A45253]",
|
||||
textColor: "text-[#FFF7F0]",
|
||||
textShadow: "[text-shadow:_0_-1px_0_rgb(146_64_14_/_100%)]",
|
||||
},
|
||||
gold: {
|
||||
outer: "bg-gradient-to-b from-[#917100] to-[#EAD98F]",
|
||||
inner: "bg-gradient-to-b from-[#FFFDDD] via-[#856807] to-[#FFF1B3]",
|
||||
button: "bg-gradient-to-b from-[#FFEBA1] to-[#9B873F]",
|
||||
textColor: "text-[#FFFDE5]",
|
||||
textShadow: "[text-shadow:_0_-1px_0_rgb(178_140_2_/_100%)]",
|
||||
},
|
||||
bronze: {
|
||||
outer: "bg-gradient-to-b from-[#864813] to-[#E9B486]",
|
||||
inner: "bg-gradient-to-b from-[#EDC5A1] via-[#5F2D01] to-[#FFDEC1]",
|
||||
button: "bg-gradient-to-b from-[#FFE3C9] to-[#A36F3D]",
|
||||
textColor: "text-[#FFF7F0]",
|
||||
textShadow: "[text-shadow:_0_-1px_0_rgb(124_45_18_/_100%)]",
|
||||
},
|
||||
};
|
||||
|
||||
const metalButtonVariants = (
|
||||
variant: ColorVariant = "default",
|
||||
isPressed: boolean,
|
||||
isHovered: boolean,
|
||||
isTouchDevice: boolean,
|
||||
) => {
|
||||
const colors = colorVariants[variant];
|
||||
const transitionStyle = "all 250ms cubic-bezier(0.1, 0.4, 0.2, 1)";
|
||||
|
||||
return {
|
||||
wrapper: cn(
|
||||
"relative inline-flex transform-gpu rounded-md p-[1.25px] will-change-transform",
|
||||
colors.outer,
|
||||
),
|
||||
wrapperStyle: {
|
||||
transform: isPressed
|
||||
? "translateY(2.5px) scale(0.99)"
|
||||
: "translateY(0) scale(1)",
|
||||
boxShadow: isPressed
|
||||
? "0 1px 2px rgba(0, 0, 0, 0.15)"
|
||||
: isHovered && !isTouchDevice
|
||||
? "0 4px 12px rgba(0, 0, 0, 0.12)"
|
||||
: "0 3px 8px rgba(0, 0, 0, 0.08)",
|
||||
transition: transitionStyle,
|
||||
transformOrigin: "center center",
|
||||
},
|
||||
inner: cn(
|
||||
"absolute inset-[1px] transform-gpu rounded-lg will-change-transform",
|
||||
colors.inner,
|
||||
),
|
||||
innerStyle: {
|
||||
transition: transitionStyle,
|
||||
transformOrigin: "center center",
|
||||
filter:
|
||||
isHovered && !isPressed && !isTouchDevice ? "brightness(1.05)" : "none",
|
||||
},
|
||||
button: cn(
|
||||
"relative z-10 m-[1px] rounded-md inline-flex h-11 transform-gpu cursor-pointer items-center justify-center overflow-hidden rounded-md px-6 py-2 text-sm leading-none font-semibold will-change-transform outline-none",
|
||||
colors.button,
|
||||
colors.textColor,
|
||||
colors.textShadow,
|
||||
),
|
||||
buttonStyle: {
|
||||
transform: isPressed ? "scale(0.97)" : "scale(1)",
|
||||
transition: transitionStyle,
|
||||
transformOrigin: "center center",
|
||||
filter:
|
||||
isHovered && !isPressed && !isTouchDevice ? "brightness(1.02)" : "none",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const ShineEffect = ({ isPressed }: { isPressed: boolean }) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-0 z-20 overflow-hidden transition-opacity duration-300",
|
||||
isPressed ? "opacity-20" : "opacity-0",
|
||||
)}
|
||||
>
|
||||
<div className="absolute inset-0 rounded-md bg-gradient-to-r from-transparent via-neutral-100 to-transparent" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const MetalButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
MetalButtonProps
|
||||
>(({ children, className, variant = "default", ...props }, ref) => {
|
||||
const [isPressed, setIsPressed] = React.useState(false);
|
||||
const [isHovered, setIsHovered] = React.useState(false);
|
||||
const [isTouchDevice, setIsTouchDevice] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setIsTouchDevice("ontouchstart" in window || navigator.maxTouchPoints > 0);
|
||||
}, []);
|
||||
|
||||
const buttonText = children || "Button";
|
||||
const variants = metalButtonVariants(
|
||||
variant,
|
||||
isPressed,
|
||||
isHovered,
|
||||
isTouchDevice,
|
||||
);
|
||||
|
||||
const handleInternalMouseDown = () => {
|
||||
setIsPressed(true);
|
||||
};
|
||||
const handleInternalMouseUp = () => {
|
||||
setIsPressed(false);
|
||||
};
|
||||
const handleInternalMouseLeave = () => {
|
||||
setIsPressed(false);
|
||||
setIsHovered(false);
|
||||
};
|
||||
const handleInternalMouseEnter = () => {
|
||||
if (!isTouchDevice) {
|
||||
setIsHovered(true);
|
||||
}
|
||||
};
|
||||
const handleInternalTouchStart = () => {
|
||||
setIsPressed(true);
|
||||
};
|
||||
const handleInternalTouchEnd = () => {
|
||||
setIsPressed(false);
|
||||
};
|
||||
const handleInternalTouchCancel = () => {
|
||||
setIsPressed(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={variants.wrapper} style={variants.wrapperStyle}>
|
||||
<div className={variants.inner} style={variants.innerStyle}></div>
|
||||
<button
|
||||
ref={ref}
|
||||
className={cn(variants.button, className)}
|
||||
style={variants.buttonStyle}
|
||||
{...props}
|
||||
onMouseDown={handleInternalMouseDown}
|
||||
onMouseUp={handleInternalMouseUp}
|
||||
onMouseLeave={handleInternalMouseLeave}
|
||||
onMouseEnter={handleInternalMouseEnter}
|
||||
onTouchStart={handleInternalTouchStart}
|
||||
onTouchEnd={handleInternalTouchEnd}
|
||||
onTouchCancel={handleInternalTouchCancel}
|
||||
>
|
||||
<ShineEffect isPressed={isPressed} />
|
||||
{buttonText}
|
||||
{isHovered && !isPressed && !isTouchDevice && (
|
||||
<div className="pointer-events-none absolute inset-0 bg-gradient-to-t rounded-lg from-transparent to-white/5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
MetalButton.displayName = "MetalButton";
|
||||
@ -107,7 +107,7 @@ export function ScriptTabContent({
|
||||
const isActive = currentSketchIndex === index;
|
||||
return (
|
||||
<motion.div
|
||||
key={script.id}
|
||||
key={index}
|
||||
className={cn(
|
||||
'flex-shrink-0 cursor-pointer transition-all duration-300',
|
||||
isActive ? 'text-white' : 'text-white/50 hover:text-white/80'
|
||||
|
||||
443
components/ui/video-carousel.tsx
Normal file
443
components/ui/video-carousel.tsx
Normal file
@ -0,0 +1,443 @@
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
|
||||
interface Video {
|
||||
id: string;
|
||||
url: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface VideoCarouselProps {
|
||||
videos: Video[];
|
||||
width?: string;
|
||||
height?: string;
|
||||
}
|
||||
|
||||
// 添加动画关键帧
|
||||
const coverFlow = keyframes`
|
||||
0% {
|
||||
z-index: 1;
|
||||
transform: translate3d(-80%, 0, 0) rotateY(-33deg);
|
||||
}
|
||||
20% {
|
||||
scale: 1;
|
||||
transform: translate3d(0%, 0, 0) rotateY(-33deg);
|
||||
}
|
||||
40% {
|
||||
transform: translate3d(15%, 0, 0) rotateY(-20deg);
|
||||
}
|
||||
/* Half way */
|
||||
50% {
|
||||
scale: 1.2;
|
||||
transform: translate3d(0, 0, 0) rotateY(0deg);
|
||||
}
|
||||
60% {
|
||||
transform: translate3d(-15%, 0, 0) rotateY(20deg);
|
||||
}
|
||||
80% {
|
||||
scale: 1;
|
||||
transform: translate3d(0%, 0, 0) rotateY(33deg);
|
||||
}
|
||||
100% {
|
||||
z-index: 1;
|
||||
transform: translate3d(80%, 0, 0) rotateY(33deg);
|
||||
}
|
||||
`;
|
||||
|
||||
const shrink = keyframes`
|
||||
0% {
|
||||
scale: 0.75;
|
||||
opacity: 0;
|
||||
transform-origin: -150% 50%;
|
||||
}
|
||||
5% {
|
||||
opacity: 1;
|
||||
}
|
||||
10% {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
transform-origin: 50% 50%;
|
||||
}
|
||||
40% {
|
||||
scale: 1;
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0vmin);
|
||||
}
|
||||
50% {
|
||||
scale: 1.2;
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 15vmin);
|
||||
}
|
||||
60% {
|
||||
scale: 1;
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0vmin);
|
||||
}
|
||||
90% {
|
||||
opacity: 1;
|
||||
transform-origin: 50% 50%;
|
||||
scale: 1;
|
||||
}
|
||||
95% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
scale: 0.75;
|
||||
opacity: 0;
|
||||
transform-origin: 250% 50%;
|
||||
}
|
||||
`;
|
||||
|
||||
const z = keyframes`
|
||||
0%, 100% {
|
||||
z-index: 1;
|
||||
}
|
||||
50% {
|
||||
z-index: 100;
|
||||
}
|
||||
`;
|
||||
|
||||
const Container = styled.div<{ $width?: string; $height?: string }>`
|
||||
display: grid;
|
||||
place-items: center;
|
||||
align-items: end;
|
||||
width: ${props => props.$width || '100vw'};
|
||||
height: ${props => props.$height || '100vh'};
|
||||
overflow: hidden;
|
||||
background: black;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const VideoList = styled.ul`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scroll-snap-type: x mandatory;
|
||||
contain: inline-size;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const VideoItem = styled.li`
|
||||
width: 30%;
|
||||
aspect-ratio: 16/9;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex: 1 0 30cqw;
|
||||
position: relative;
|
||||
height: calc(30cqw * 9/16);
|
||||
width: 30cqw;
|
||||
view-timeline: --cover;
|
||||
view-timeline-axis: inline;
|
||||
transform-style: preserve-3d;
|
||||
perspective: 100vmin;
|
||||
background: transparent;
|
||||
animation: ${z} both linear;
|
||||
animation-timeline: --cover;
|
||||
animation-range: cover;
|
||||
|
||||
&[aria-hidden=false] {
|
||||
scroll-snap-align: center;
|
||||
}
|
||||
|
||||
&.center {
|
||||
z-index: 100;
|
||||
}
|
||||
`;
|
||||
|
||||
const VideoWrapper = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
aspect-ratio: 16/9;
|
||||
transform-origin: 100% 50%;
|
||||
transform-style: preserve-3d;
|
||||
perspective: 100vmin;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
translate: -50% -50%;
|
||||
will-change: transform;
|
||||
animation: ${shrink} both linear;
|
||||
animation-timeline: --cover;
|
||||
animation-range: cover;
|
||||
|
||||
${VideoItem}.center & {
|
||||
max-height: calc(150% + 2vmin);
|
||||
}
|
||||
`;
|
||||
|
||||
const Video = styled.video`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
-webkit-box-reflect: below 2vmin linear-gradient(transparent 0 50%, hsl(0 0% 100% / 0.75) 100%);
|
||||
transform: translateZ(0);
|
||||
will-change: transform;
|
||||
animation: ${coverFlow} both linear;
|
||||
animation-timeline: --cover;
|
||||
animation-range: cover;
|
||||
border-radius: 1vmin;
|
||||
|
||||
${VideoItem}.center & {
|
||||
-webkit-box-reflect: unset;
|
||||
}
|
||||
`;
|
||||
|
||||
const VideoReflection = styled(Video)`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
mask-image: linear-gradient(to top, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0) 100%);
|
||||
scale: 1 !important;
|
||||
|
||||
${VideoItem}.center & {
|
||||
opacity: 0.75;
|
||||
scale: 1 !important;
|
||||
object-position: center bottom;
|
||||
|
||||
}
|
||||
`;
|
||||
|
||||
const ReflectionWrapper = styled.div`
|
||||
position: absolute;
|
||||
left: 0;
|
||||
margin-top: calc(6vmin * 1.2);
|
||||
|
||||
width: 100%;
|
||||
height: calc((100vh - 4rem) / 8);
|
||||
pointer-events: none;
|
||||
overflow-y: hidden;
|
||||
|
||||
${VideoItem}.center & {
|
||||
scale: 1.19998;
|
||||
transform: scale(1, -1) translateZ(0) !important;
|
||||
}
|
||||
`;
|
||||
|
||||
const ControlsWrapper = styled.div`
|
||||
position: absolute;
|
||||
bottom: -1rem;
|
||||
left: -1rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
${VideoItem}.center:hover & {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const ControlButton = styled.button`
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 0.8rem;
|
||||
height: 0.8rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const VideoCarousel: React.FC<VideoCarouselProps> = ({ videos, width, height }) => {
|
||||
const containerRef = useRef<HTMLUListElement>(null);
|
||||
const [currentPlayingVideo, setCurrentPlayingVideo] = useState<HTMLVideoElement | null>(null);
|
||||
const [isPlaying, setIsPlaying] = useState(true);
|
||||
const [isMuted, setIsMuted] = useState(true);
|
||||
const PADDING = 4;
|
||||
|
||||
const togglePlay = (video: HTMLVideoElement, reflectionVideo: HTMLVideoElement) => {
|
||||
if (video.paused) {
|
||||
video.play();
|
||||
reflectionVideo.play();
|
||||
setIsPlaying(true);
|
||||
} else {
|
||||
video.pause();
|
||||
reflectionVideo.pause();
|
||||
setIsPlaying(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMute = (video: HTMLVideoElement) => {
|
||||
video.muted = !video.muted;
|
||||
setIsMuted(video.muted);
|
||||
};
|
||||
|
||||
const handleVideoPlayback = () => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const containerWidth = containerRef.current!.offsetWidth;
|
||||
const containerCenter = containerWidth / 2;
|
||||
|
||||
const items = containerRef.current!.querySelectorAll('li');
|
||||
items.forEach(item => {
|
||||
const video = item.querySelector('video:not([data-reflection])') as HTMLVideoElement;
|
||||
const reflectionVideo = item.querySelector('video[data-reflection]') as HTMLVideoElement;
|
||||
if (!video || !reflectionVideo) return;
|
||||
|
||||
const rect = item.getBoundingClientRect();
|
||||
const itemCenter = rect.left + rect.width / 2;
|
||||
|
||||
const isCenter = Math.abs(itemCenter - containerCenter) < rect.width / 3;
|
||||
item.classList.toggle('center', isCenter);
|
||||
|
||||
if (isCenter) {
|
||||
if (currentPlayingVideo !== video) {
|
||||
if (currentPlayingVideo) {
|
||||
currentPlayingVideo.pause();
|
||||
}
|
||||
video.play().catch(() => {});
|
||||
reflectionVideo.play().catch(() => {});
|
||||
setCurrentPlayingVideo(video);
|
||||
}
|
||||
} else {
|
||||
if (video !== currentPlayingVideo) {
|
||||
video.pause();
|
||||
reflectionVideo.pause();
|
||||
video.currentTime = 0;
|
||||
reflectionVideo.currentTime = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
let scrollBounds = {
|
||||
min: 0,
|
||||
max: 0
|
||||
};
|
||||
|
||||
const setScrollBounds = () => {
|
||||
const items = container.children;
|
||||
if (items.length === 0) return;
|
||||
items[items.length - 1].scrollIntoView();
|
||||
scrollBounds.max = container.scrollLeft + (items[0] as HTMLElement).offsetWidth;
|
||||
items[0].scrollIntoView();
|
||||
scrollBounds.min = container.scrollLeft - (items[0] as HTMLElement).offsetWidth;
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
if (container.scrollLeft < scrollBounds.min) {
|
||||
container.scrollLeft = scrollBounds.max;
|
||||
} else if (container.scrollLeft > scrollBounds.max) {
|
||||
container.scrollLeft = scrollBounds.min;
|
||||
}
|
||||
handleVideoPlayback();
|
||||
};
|
||||
|
||||
setScrollBounds();
|
||||
handleVideoPlayback();
|
||||
|
||||
container.addEventListener('scroll', handleScroll);
|
||||
window.addEventListener('resize', setScrollBounds);
|
||||
|
||||
return () => {
|
||||
container.removeEventListener('scroll', handleScroll);
|
||||
window.removeEventListener('resize', setScrollBounds);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const paddedVideos = [
|
||||
...videos.slice(-PADDING),
|
||||
...videos,
|
||||
...videos.slice(0, PADDING)
|
||||
];
|
||||
|
||||
return (
|
||||
<Container $width={width} $height={height}>
|
||||
<VideoList ref={containerRef}>
|
||||
{paddedVideos.map((video, index) => (
|
||||
<VideoItem
|
||||
key={`${video.id}-${index}`}
|
||||
aria-hidden={index < PADDING || index >= videos.length + PADDING}
|
||||
>
|
||||
<VideoWrapper>
|
||||
<Video
|
||||
muted={isMuted}
|
||||
loop
|
||||
playsInline
|
||||
preload="none"
|
||||
poster={`${video.url}?vframe/jpg/offset/1`}
|
||||
src={video.url}
|
||||
title={video.title}
|
||||
/>
|
||||
<ControlsWrapper>
|
||||
<ControlButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const videoEl = e.currentTarget.closest(VideoItem.toString())?.querySelector('video:not([data-reflection])') as HTMLVideoElement;
|
||||
const reflectionEl = e.currentTarget.closest(VideoItem.toString())?.querySelector('video[data-reflection]') as HTMLVideoElement;
|
||||
if (videoEl && reflectionEl) togglePlay(videoEl, reflectionEl);
|
||||
}}
|
||||
>
|
||||
{isPlaying ? (
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M8 5v14l11-7z"/>
|
||||
</svg>
|
||||
)}
|
||||
</ControlButton>
|
||||
<ControlButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const videoEl = e.currentTarget.closest(VideoItem.toString())?.querySelector('video:not([data-reflection])') as HTMLVideoElement;
|
||||
if (videoEl) toggleMute(videoEl);
|
||||
}}
|
||||
>
|
||||
{isMuted ? (
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
|
||||
</svg>
|
||||
)}
|
||||
</ControlButton>
|
||||
</ControlsWrapper>
|
||||
<ReflectionWrapper>
|
||||
<VideoReflection
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
preload="none"
|
||||
poster={`${video.url}?vframe/jpg/offset/1`}
|
||||
src={video.url}
|
||||
data-reflection="true"
|
||||
/>
|
||||
</ReflectionWrapper>
|
||||
</VideoWrapper>
|
||||
</VideoItem>
|
||||
))}
|
||||
</VideoList>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoCarousel;
|
||||
24
components/video-carousel-layout.tsx
Normal file
24
components/video-carousel-layout.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import VideoCarousel from './ui/video-carousel';
|
||||
|
||||
interface VideoScreenLayoutProps {
|
||||
videos: {
|
||||
id: string;
|
||||
url: string;
|
||||
title: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export const VideoCarouselLayout = ({ videos }: VideoScreenLayoutProps) => {
|
||||
return (
|
||||
<>
|
||||
{videos.length && (
|
||||
<VideoCarousel
|
||||
videos={videos}
|
||||
width="100vw"
|
||||
height="calc(100vh - 4rem)"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -132,30 +132,6 @@ function VideoGridLayoutComponent({ videos, onEdit, onDelete }: VideoGridLayoutP
|
||||
<Play className="w-12 h-12 text-white/90" />
|
||||
</div>
|
||||
|
||||
{/* 顶部操作按钮 */}
|
||||
<div
|
||||
className={`absolute top-4 right-4 flex gap-2 transition-all duration-300 transform
|
||||
${hoveredId === video.id ? 'translate-y-0 opacity-100' : 'translate-y-[-10px] opacity-0'}
|
||||
`}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="w-8 h-8 rounded-full bg-black/40 hover:bg-black/60 backdrop-blur-sm"
|
||||
onClick={() => onEdit?.(video.id)}
|
||||
>
|
||||
<Edit2 className="w-4 h-4 text-white" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="w-8 h-8 rounded-full bg-black/40 hover:bg-black/60 backdrop-blur-sm"
|
||||
onClick={() => onDelete?.(video.id)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 text-white" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 底部控制区域 */}
|
||||
<div
|
||||
className={`absolute bottom-4 left-4 right-4 transition-all duration-300 transform
|
||||
|
||||
@ -1,332 +0,0 @@
|
||||
'use client'; // Add this to ensure it's a client component
|
||||
|
||||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import { ChevronLeft, ChevronRight, Volume2, VolumeX, Play, Pause } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
interface VideoScreenLayoutProps {
|
||||
videos: {
|
||||
id: string;
|
||||
url: string;
|
||||
title: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
function VideoScreenLayoutComponent({ videos }: VideoScreenLayoutProps) {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
const [isMuted, setIsMuted] = useState(true); // 默认静音
|
||||
const [volume, setVolume] = useState(0.8); // 添加音量状态
|
||||
const [isPlaying, setIsPlaying] = useState(true); // 播放状态
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const videoRefs = useRef<(HTMLVideoElement | null)[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentIndex(videos.length > 2 ? 1 : 0);
|
||||
}, [videos.length]);
|
||||
|
||||
// 确保视频refs数组长度与videos数组一致
|
||||
useEffect(() => {
|
||||
videoRefs.current = videoRefs.current.slice(0, videos.length);
|
||||
}, [videos.length]);
|
||||
|
||||
// 计算每个面板的样式
|
||||
const getPanelStyle = (index: number) => {
|
||||
const position = index - currentIndex;
|
||||
const scale = Math.max(0.6, 1 - Math.abs(position) * 0.2);
|
||||
const zIndex = 10 - Math.abs(position);
|
||||
const opacity = Math.max(0.4, 1 - Math.abs(position) * 0.3);
|
||||
|
||||
let transform = `
|
||||
perspective(1000px)
|
||||
scale(${scale})
|
||||
translateX(${position * 100}%)
|
||||
`;
|
||||
|
||||
// 添加侧面板的 3D 旋转效果
|
||||
if (position !== 0) {
|
||||
const rotateY = position > 0 ? -15 : 15;
|
||||
transform += ` rotateY(${rotateY}deg)`;
|
||||
}
|
||||
|
||||
return {
|
||||
transform,
|
||||
zIndex,
|
||||
opacity,
|
||||
};
|
||||
};
|
||||
|
||||
// 切换静音状态
|
||||
const toggleMute = () => {
|
||||
const currentVideo = videoRefs.current[currentIndex];
|
||||
if (currentVideo) {
|
||||
currentVideo.muted = !currentVideo.muted;
|
||||
setIsMuted(currentVideo.muted);
|
||||
}
|
||||
};
|
||||
|
||||
// 音量控制函数
|
||||
const handleVolumeChange = (newVolume: number) => {
|
||||
setVolume(newVolume);
|
||||
const currentVideo = videoRefs.current[currentIndex];
|
||||
if (currentVideo) {
|
||||
currentVideo.volume = newVolume;
|
||||
}
|
||||
};
|
||||
|
||||
// 应用音量设置到视频元素
|
||||
const applyVolumeSettings = useCallback((videoElement: HTMLVideoElement) => {
|
||||
if (videoElement) {
|
||||
videoElement.volume = volume;
|
||||
videoElement.muted = isMuted;
|
||||
}
|
||||
}, [volume, isMuted]);
|
||||
|
||||
// 处理切换
|
||||
const handleSlide = useCallback((direction: 'prev' | 'next') => {
|
||||
if (isAnimating) return;
|
||||
|
||||
setIsAnimating(true);
|
||||
const newIndex = direction === 'next'
|
||||
? (currentIndex + 1) % videos.length
|
||||
: (currentIndex - 1 + videos.length) % videos.length;
|
||||
|
||||
setCurrentIndex(newIndex);
|
||||
|
||||
// 动画结束后重置状态并同步新视频的静音状态
|
||||
setTimeout(() => {
|
||||
setIsAnimating(false);
|
||||
// 同步新视频的静音状态到UI并应用音量设置
|
||||
const newVideo = videoRefs.current[newIndex];
|
||||
if (newVideo) {
|
||||
setIsMuted(newVideo.muted);
|
||||
applyVolumeSettings(newVideo);
|
||||
// 根据当前播放状态控制新视频
|
||||
if (isPlaying) {
|
||||
newVideo.play().catch(() => {
|
||||
setIsPlaying(false);
|
||||
});
|
||||
} else {
|
||||
newVideo.pause();
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
}, [isAnimating, currentIndex, videos.length, isPlaying, applyVolumeSettings]);
|
||||
|
||||
// 音量设置变化时应用到当前视频
|
||||
useEffect(() => {
|
||||
const currentVideo = videoRefs.current[currentIndex];
|
||||
if (currentVideo) {
|
||||
applyVolumeSettings(currentVideo);
|
||||
}
|
||||
}, [volume, isMuted, currentIndex, applyVolumeSettings]);
|
||||
|
||||
// 播放状态变化时应用到当前视频
|
||||
useEffect(() => {
|
||||
const currentVideo = videoRefs.current[currentIndex];
|
||||
if (currentVideo) {
|
||||
if (isPlaying) {
|
||||
currentVideo.play().catch(() => {
|
||||
// 处理自动播放策略限制
|
||||
setIsPlaying(false);
|
||||
});
|
||||
} else {
|
||||
currentVideo.pause();
|
||||
}
|
||||
}
|
||||
}, [isPlaying, currentIndex]);
|
||||
|
||||
// 播放/暂停控制
|
||||
const togglePlay = () => {
|
||||
setIsPlaying(!isPlaying);
|
||||
};
|
||||
|
||||
// 键盘事件监听器 - 添加左右箭头键控制
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
// 检查是否正在动画中,如果是则不处理
|
||||
if (isAnimating) return;
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowLeft':
|
||||
event.preventDefault(); // 阻止默认行为
|
||||
handleSlide('prev');
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
event.preventDefault(); // 阻止默认行为
|
||||
handleSlide('next');
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// 添加键盘事件监听器
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
// 清理函数 - 组件卸载时移除监听器
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [isAnimating, handleSlide]); // 添加handleSlide到依赖项
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-[360px] overflow-hidden bg-[var(--background)]">
|
||||
{/* 视频面板容器 */}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="absolute inset-0 flex items-center justify-center"
|
||||
style={{
|
||||
perspective: '1000px',
|
||||
transformStyle: 'preserve-3d',
|
||||
}}
|
||||
>
|
||||
{videos.map((video, index) => (
|
||||
<div
|
||||
key={video.id}
|
||||
className="absolute w-[640px] h-[360px] transition-all duration-500 ease-out"
|
||||
style={getPanelStyle(index)}
|
||||
>
|
||||
<div className="relative w-full h-full overflow-hidden rounded-lg">
|
||||
{/* 视频 - Add suppressHydrationWarning to prevent className mismatch warnings */}
|
||||
<video
|
||||
ref={(el) => (videoRefs.current[index] = el)}
|
||||
src={video.url}
|
||||
suppressHydrationWarning
|
||||
className="w-full h-full object-cover"
|
||||
autoPlay={index === currentIndex ? isPlaying : false}
|
||||
loop
|
||||
muted={index === currentIndex ? isMuted : true} // 只有当前视频受状态控制
|
||||
playsInline
|
||||
preload={`${index === currentIndex ? 'auto' : 'none'}`}
|
||||
poster={`${video.url}?vframe/jpg/offset/1`}
|
||||
onLoadedData={() => {
|
||||
if (index === currentIndex && videoRefs.current[index]) {
|
||||
applyVolumeSettings(videoRefs.current[index]!);
|
||||
// 根据播放状态决定是否播放
|
||||
if (isPlaying) {
|
||||
videoRefs.current[index]!.play().catch(() => {
|
||||
setIsPlaying(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
onPlay={() => {
|
||||
if (index === currentIndex) {
|
||||
setIsPlaying(true);
|
||||
}
|
||||
}}
|
||||
onPause={() => {
|
||||
if (index === currentIndex) {
|
||||
setIsPlaying(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 视频标题和控制 - 只在中间面板显示 */}
|
||||
{index === currentIndex && (
|
||||
<>
|
||||
{/* 音量控制区域 */}
|
||||
<div className="absolute top-4 right-4 flex items-center gap-2">
|
||||
{/* 静音按钮 */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="w-10 h-10 rounded-full bg-black/40 hover:bg-black/60 backdrop-blur-sm"
|
||||
onClick={toggleMute}
|
||||
title={isMuted ? "取消静音" : "静音"}
|
||||
>
|
||||
{isMuted ? (
|
||||
<VolumeX className="w-5 h-5 text-white" />
|
||||
) : (
|
||||
<Volume2 className="w-5 h-5 text-white" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* 音量滑块 */}
|
||||
<div className="flex items-center gap-1 bg-black/40 rounded-full px-2 py-2 backdrop-blur-sm">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
value={volume}
|
||||
onChange={(e) => handleVolumeChange(parseFloat(e.target.value))}
|
||||
className="w-12 h-1 bg-white/20 rounded-lg appearance-none cursor-pointer
|
||||
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3
|
||||
[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white
|
||||
[&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:shadow-lg
|
||||
[&::-moz-range-thumb]:w-3 [&::-moz-range-thumb]:h-3 [&::-moz-range-thumb]:rounded-full
|
||||
[&::-moz-range-thumb]:bg-white [&::-moz-range-thumb]:cursor-pointer
|
||||
[&::-moz-range-thumb]:border-none [&::-moz-range-thumb]:shadow-lg"
|
||||
style={{
|
||||
background: `linear-gradient(to right, white 0%, white ${volume * 100}%, rgba(255,255,255,0.2) ${volume * 100}%, rgba(255,255,255,0.2) 100%)`
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs text-white/70 w-6 text-center">
|
||||
{Math.round(volume * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 底部控制区域 */}
|
||||
<div className="absolute bottom-16 left-4">
|
||||
{/* 播放/暂停按钮 */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="w-10 h-10 rounded-full bg-black/40 hover:bg-black/60 backdrop-blur-sm"
|
||||
onClick={togglePlay}
|
||||
title={isPlaying ? "暂停" : "播放"}
|
||||
>
|
||||
{isPlaying ? (
|
||||
<Pause className="w-5 h-5 text-white" />
|
||||
) : (
|
||||
<Play className="w-5 h-5 text-white" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 视频标题 */}
|
||||
<div className="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/80 to-transparent">
|
||||
<h3 className="text-white text-lg font-medium">{video.title}</h3>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 玻璃态遮罩 - 侧面板半透明效果 */}
|
||||
{index !== currentIndex && (
|
||||
<div className="absolute inset-0 bg-black/20 backdrop-blur-sm" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 切换按钮 */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute left-4 top-1/2 -translate-y-1/2 w-12 h-12 rounded-full bg-white/10 hover:bg-white/20 backdrop-blur-sm transition-all"
|
||||
onClick={() => handleSlide('prev')}
|
||||
disabled={isAnimating}
|
||||
>
|
||||
<ChevronLeft className="w-6 h-6 text-white" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 w-12 h-12 rounded-full bg-white/10 hover:bg-white/20 backdrop-blur-sm transition-all"
|
||||
onClick={() => handleSlide('next')}
|
||||
disabled={isAnimating}
|
||||
>
|
||||
<ChevronRight className="w-6 h-6 text-white" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Export as a client-only component to prevent hydration issues
|
||||
export const VideoScreenLayout = dynamic(() => Promise.resolve(VideoScreenLayoutComponent), {
|
||||
ssr: false
|
||||
});
|
||||
6
lib/utils.ts
Normal file
6
lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
578
package-lock.json
generated
578
package-lock.json
generated
@ -34,7 +34,7 @@
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-slider": "^1.3.5",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.1.0",
|
||||
"@radix-ui/react-tabs": "^1.1.0",
|
||||
"@radix-ui/react-toast": "^1.2.1",
|
||||
@ -48,13 +48,14 @@
|
||||
"@types/react": "18.2.22",
|
||||
"@types/react-beautiful-dnd": "^13.1.8",
|
||||
"@types/react-dom": "18.2.7",
|
||||
"@types/styled-components": "^5.1.34",
|
||||
"@types/three": "^0.177.0",
|
||||
"@types/wavesurfer.js": "^6.0.12",
|
||||
"antd": "^5.26.2",
|
||||
"autoprefixer": "10.4.15",
|
||||
"axios": "^1.10.0",
|
||||
"babel-loader": "^10.0.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"date-fns": "^3.6.0",
|
||||
@ -86,7 +87,7 @@
|
||||
"sonner": "^1.5.0",
|
||||
"styled-components": "^6.1.19",
|
||||
"swiper": "^11.2.8",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss": "3.3.3",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"three": "^0.177.0",
|
||||
@ -1200,24 +1201,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
||||
@ -1339,24 +1322,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-arrow": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz",
|
||||
@ -1402,21 +1367,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-aspect-ratio/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-aspect-ratio/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
@ -1440,24 +1390,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-aspect-ratio/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-avatar": {
|
||||
"version": "1.1.10",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz",
|
||||
@ -1485,21 +1417,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||
@ -1538,24 +1455,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
@ -1699,24 +1598,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
||||
@ -1915,24 +1796,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
||||
@ -2006,6 +1869,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
||||
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz",
|
||||
@ -2421,24 +2302,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
@ -2801,24 +2664,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
@ -3087,21 +2932,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
@ -3125,24 +2955,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-menu": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.2.tgz",
|
||||
@ -3182,6 +2994,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
||||
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-menubar": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.2.tgz",
|
||||
@ -3532,24 +3362,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
@ -3797,6 +3609,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
||||
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-progress": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-progress/-/react-progress-1.1.7.tgz",
|
||||
@ -3821,21 +3651,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||
@ -3874,24 +3689,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-radio-group": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-radio-group/-/react-radio-group-1.3.7.tgz",
|
||||
@ -4097,24 +3894,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
@ -4370,24 +4149,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
@ -4725,24 +4486,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
@ -5070,24 +4813,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
||||
@ -5156,11 +4881,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
||||
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.0"
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@ -5172,6 +4898,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-switch": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-switch/-/react-switch-1.2.5.tgz",
|
||||
@ -5260,24 +5001,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
||||
@ -5548,24 +5271,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
@ -5851,24 +5556,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
@ -5924,21 +5611,6 @@
|
||||
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
@ -5962,24 +5634,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
||||
@ -6255,24 +5909,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
@ -7135,6 +6771,17 @@
|
||||
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/styled-components": {
|
||||
"version": "5.1.34",
|
||||
"resolved": "https://registry.npmmirror.com/@types/styled-components/-/styled-components-5.1.34.tgz",
|
||||
"integrity": "sha512-mmiVvwpYklFIv9E8qfxuPyIt/OuyIrn6gMOAMOFUO3WJfSrSE+sGUoa4PiZj77Ut7bKZpaa6o1fBKS/4TOEvnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hoist-non-react-statics": "*",
|
||||
"@types/react": "*",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/stylis": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmmirror.com/@types/stylis/-/stylis-4.2.5.tgz",
|
||||
@ -8157,22 +7804,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/class-variance-authority": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.0.tgz",
|
||||
"integrity": "sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==",
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmmirror.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
|
||||
"integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"clsx": "2.0.0"
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://joebell.co.uk"
|
||||
}
|
||||
},
|
||||
"node_modules/class-variance-authority/node_modules/clsx": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
|
||||
"integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
"url": "https://polar.sh/cva"
|
||||
}
|
||||
},
|
||||
"node_modules/classnames": {
|
||||
@ -8236,8 +7876,9 @@
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@ -8296,24 +7937,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@ -13555,9 +13178,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.3.tgz",
|
||||
"integrity": "sha512-d9ZolCAIzom1nf/5p4LdD5zvjmgSxY0BGgdSvmXIoMYAiPdAW/dSpP7joCDYFY7r/HkEa2qmPtkgsu0xjQeQtw==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
|
||||
"integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/dcastil"
|
||||
|
||||
@ -35,7 +35,7 @@
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-slider": "^1.3.5",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.1.0",
|
||||
"@radix-ui/react-tabs": "^1.1.0",
|
||||
"@radix-ui/react-toast": "^1.2.1",
|
||||
@ -49,13 +49,14 @@
|
||||
"@types/react": "18.2.22",
|
||||
"@types/react-beautiful-dnd": "^13.1.8",
|
||||
"@types/react-dom": "18.2.7",
|
||||
"@types/styled-components": "^5.1.34",
|
||||
"@types/three": "^0.177.0",
|
||||
"@types/wavesurfer.js": "^6.0.12",
|
||||
"antd": "^5.26.2",
|
||||
"autoprefixer": "10.4.15",
|
||||
"axios": "^1.10.0",
|
||||
"babel-loader": "^10.0.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"date-fns": "^3.6.0",
|
||||
@ -87,7 +88,7 @@
|
||||
"sonner": "^1.5.0",
|
||||
"styled-components": "^6.1.19",
|
||||
"swiper": "^11.2.8",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss": "3.3.3",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"three": "^0.177.0",
|
||||
|
||||
@ -58,10 +58,25 @@ module.exports = {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: 0 },
|
||||
},
|
||||
"liquid-toggle": {
|
||||
"0%": {
|
||||
filter: "url(#toggle-glass) blur(2px)",
|
||||
transform: "scale(1)",
|
||||
},
|
||||
"50%": {
|
||||
filter: "url(#toggle-glass) blur(1px)",
|
||||
transform: "scale(1.05)",
|
||||
},
|
||||
"100%": {
|
||||
filter: "url(#toggle-glass) blur(2px)",
|
||||
transform: "scale(1)",
|
||||
}
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
"liquid-toggle": "liquid-toggle 2s ease-in-out infinite",
|
||||
},
|
||||
transitionDelay: {
|
||||
'100': '100ms',
|
||||
|
||||
51
test/movie.http
Normal file
51
test/movie.http
Normal file
@ -0,0 +1,51 @@
|
||||
@host = https://77.smartvideo.py.qikongjian.com
|
||||
### Create a movie project
|
||||
POST http://localhost:8000/movie/create_movie_project
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"script": "spider man saves the world from a virus.",
|
||||
"user_id": "user123",
|
||||
"mode": "auto",
|
||||
"resolution": "720p"
|
||||
}
|
||||
|
||||
### Get movie project name
|
||||
POST http://localhost:8000/movie/get_movie_project_name
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"project_id": "17cc4231-4bbe-45bc-97cf-0fcba05e3c09"
|
||||
}
|
||||
|
||||
### Get movie project detail
|
||||
POST https://77.smartvideo.py.qikongjian.com/movie/get_movie_project_detail
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"project_id": "dbbc1f06-2459-4d3e-bbff-7b11ecf0f293"
|
||||
}
|
||||
|
||||
### List movie projects
|
||||
POST http://localhost:8000/movie/list_movie_projects
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"user_id": "user123"
|
||||
}
|
||||
|
||||
### Generate movie
|
||||
POST http://localhost:8000/movie/generate_movie
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"project_id": "21763816-f688-472c-87a4-eac7bfbfaac9"
|
||||
}
|
||||
|
||||
### Get status
|
||||
POST https://77.smartvideo.py.qikongjian.com/movie/get_status
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"project_id": "dbbc1f06-2459-4d3e-bbff-7b11ecf0f293"
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user