perf(*): 创作工作台 - 优化首页按需加载与画布交互并修复历史迁移冲突
This commit is contained in:
@@ -1,25 +1,25 @@
|
||||
import { lazy, Suspense } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Alert, App, Tooltip } from "antd";
|
||||
import { ArrowLeft, BookOpenText, Clapperboard, Images, LayoutDashboard, LayoutGrid, Plus, Scissors, Settings2, type LucideIcon } from "lucide-react";
|
||||
import { Link, Navigate, useNavigate, useParams } from "react-router";
|
||||
|
||||
import { createCanvasProjectWithRemoteSync } from "@/services/user-data-sync";
|
||||
import { getProjectCore, getProjectOverview, getProjectUnitWorkspace, linkCanvasUnit, listProjectUnits } from "@/services/api/projects";
|
||||
import { WorkspacePage } from "@/components/layout/workspace-page";
|
||||
import { WorkspaceErrorState, WorkspaceLoadingState } from "@/components/layout/workspace-state";
|
||||
import { useWorkspaceTopBarExtension } from "@/components/layout/workspace-top-bar-extension";
|
||||
import { upsertProjectChapterStoryboard } from "@/lib/canvas/project-chapter-storyboard";
|
||||
import type { ProjectDetail } from "@/services/api/projects";
|
||||
|
||||
import ProjectAssetsView from "./detail/assets";
|
||||
import ProjectCanvasesView from "./detail/canvases";
|
||||
import ProjectChaptersView from "./detail/chapters";
|
||||
import ProjectOverviewView from "./detail/overview";
|
||||
import ProjectSettingsView from "./detail/settings";
|
||||
import ProjectWorkflowView from "./detail/workflow";
|
||||
import ProjectEditorView from "./detail/editor";
|
||||
import { WorkflowChapterNavigator } from "./detail/workflow-chapter-navigator";
|
||||
|
||||
const ProjectAssetsView = lazy(() => import("./detail/assets"));
|
||||
const ProjectCanvasesView = lazy(() => import("./detail/canvases"));
|
||||
const ProjectChaptersView = lazy(() => import("./detail/chapters"));
|
||||
const ProjectOverviewView = lazy(() => import("./detail/overview"));
|
||||
const ProjectSettingsView = lazy(() => import("./detail/settings"));
|
||||
const ProjectWorkflowView = lazy(() => import("./detail/workflow"));
|
||||
const ProjectEditorView = lazy(() => import("./detail/editor"));
|
||||
|
||||
type DetailView = "overview" | "chapters" | "workflow" | "canvases" | "editor" | "assets" | "settings";
|
||||
|
||||
const views: Array<{ key: DetailView; label: string; icon: LucideIcon }> = [
|
||||
@@ -75,17 +75,22 @@ export default function ProjectDetailPage() {
|
||||
tasks: workspace?.tasks || [],
|
||||
} : undefined;
|
||||
const refreshProject = () => { void queryClient.invalidateQueries({ queryKey: ["project", projectId] }); void queryClient.invalidateQueries({ queryKey: ["projects"] }); };
|
||||
const createCanvas = () => {
|
||||
const createCanvas = async () => {
|
||||
if (detail?.project.status === "archived") { message.warning("项目已归档,请先在项目设置中恢复"); return; }
|
||||
const activeChapterId = chapterId || sessionStorage.getItem(`project-active-chapter:${projectId}`) || "";
|
||||
const unit = activeView === "chapters"
|
||||
? detail?.units.find((item) => item.id === activeChapterId) || detail?.units.slice().sort((left, right) => left.position - right.position)[0]
|
||||
: undefined;
|
||||
const shots = unit ? detail?.shots.filter((shot) => shot.unitId === unit.id) || [] : [];
|
||||
const seed = unit && shots.length ? upsertProjectChapterStoryboard([], [], { unit, shots }) : undefined;
|
||||
const initialContent = seed ? { nodes: seed.nodes, connections: seed.connections } : undefined;
|
||||
const title = unit ? `${unit.title} · ${shots.length ? "分镜画布" : "画布"}` : `${detail?.project.name || "项目"} · 新画布`;
|
||||
void createCanvasProjectWithRemoteSync(title, projectId, initialContent).then(async ({ id, syncError }) => {
|
||||
try {
|
||||
const [{ createCanvasProjectWithRemoteSync }, storyboard] = await Promise.all([
|
||||
import("@/services/user-data-sync"),
|
||||
unit && shots.length ? import("@/lib/canvas/project-chapter-storyboard") : Promise.resolve(null),
|
||||
]);
|
||||
const seed = unit && shots.length ? storyboard?.upsertProjectChapterStoryboard([], [], { unit, shots }) : undefined;
|
||||
const initialContent = seed ? { nodes: seed.nodes, connections: seed.connections } : undefined;
|
||||
const title = unit ? `${unit.title} · ${shots.length ? "分镜画布" : "画布"}` : `${detail?.project.name || "项目"} · 新画布`;
|
||||
const { id, syncError } = await createCanvasProjectWithRemoteSync(title, projectId, initialContent);
|
||||
if (syncError) {
|
||||
message.warning(syncError instanceof Error ? `画布已保存在本地,项目关联稍后重试:${syncError.message}` : "画布已保存在本地,项目关联稍后重试");
|
||||
navigate(`/canvas/${id}`);
|
||||
@@ -103,7 +108,9 @@ export default function ProjectDetailPage() {
|
||||
refreshProject();
|
||||
message.success(unit && shots.length ? `已创建章节画布并导入 ${shots.length} 个分镜` : unit ? "章节画布已创建并关联" : "项目画布已创建");
|
||||
navigate(`/canvas/${id}`);
|
||||
}).catch((error) => message.error(error instanceof Error ? error.message : "画布创建失败"));
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : "画布创建失败");
|
||||
}
|
||||
};
|
||||
const chapterHref = detail ? projectChapterHref(detail.units, projectId, chapterId) : `/projects/${projectId}/chapters`;
|
||||
const workflowHref = detail ? projectWorkflowHref(detail.units, projectId, unitId, stage) : `/projects/${projectId}/workflow`;
|
||||
@@ -131,15 +138,17 @@ export default function ProjectDetailPage() {
|
||||
{detail.project.status === "archived" ? <Alert type="warning" showIcon banner message="项目已归档,恢复后才能创建画布和生成任务" className="!border-x-0 !border-t-0" /> : null}
|
||||
<main className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<div className={activeView === "chapters" || activeView === "workflow" || activeView === "editor" ? "min-h-0 flex-1" : "thin-scrollbar min-h-0 flex-1 overflow-y-auto px-3 py-5 sm:px-5 lg:px-8 lg:py-7"}>
|
||||
<div className={activeView === "overview" ? "w-full" : activeView === "chapters" || activeView === "workflow" || activeView === "editor" ? "h-full w-full" : "w-full"}>
|
||||
{activeView === "overview" ? overviewQuery.isLoading ? <WorkspaceLoadingState label="正在统计制作进度" detail="只读取聚合数据,不加载全部镜头历史" /> : overviewQuery.data ? <ProjectOverviewView detail={detail} overview={overviewQuery.data} refreshProject={refreshProject} onCreateCanvas={createCanvas} /> : <WorkspaceErrorState title="制作概览读取失败" description="请稍后重试。" onRetry={() => void overviewQuery.refetch()} /> : null}
|
||||
{activeView === "chapters" ? workspaceQuery.isLoading ? <WorkspaceLoadingState label="正在读取当前章节" detail="正文与制作数据按章节加载" /> : workspaceQuery.isError ? <WorkspaceErrorState title="章节读取失败" description="当前章节可能已被删除,或服务暂时不可用。" onRetry={() => void workspaceQuery.refetch()} /> : <ProjectChaptersView detail={detail} refreshProject={refreshProject} onCreateCanvas={createCanvas} /> : null}
|
||||
{activeView === "workflow" ? workspaceQuery.isLoading ? <WorkspaceLoadingState label="正在读取当前章节分镜" detail="仅加载本章镜头、版本和产物" /> : workspaceQuery.isError ? <WorkspaceErrorState title="分镜工作区读取失败" description="当前章节制作数据暂时不可用。" onRetry={() => void workspaceQuery.refetch()} /> : <ProjectWorkflowView detail={detail} projectId={projectId} unitId={unitId || ""} stage={stage || "video"} /> : null}
|
||||
{activeView === "canvases" ? <ProjectCanvasesView detail={detail} refreshProject={refreshProject} onCreateCanvas={createCanvas} /> : null}
|
||||
{activeView === "assets" ? <ProjectAssetsView detail={detail} refreshProject={refreshProject} onCreateCanvas={createCanvas} /> : null}
|
||||
{activeView === "settings" ? <ProjectSettingsView detail={detail} refreshProject={refreshProject} onCreateCanvas={createCanvas} /> : null}
|
||||
{activeView === "editor" ? <ProjectEditorView detail={detail} /> : null}
|
||||
</div>
|
||||
<Suspense fallback={<WorkspaceLoadingState label="正在准备当前项目视图" detail="只加载当前使用的工作区模块" />}>
|
||||
<div className={activeView === "overview" ? "w-full" : activeView === "chapters" || activeView === "workflow" || activeView === "editor" ? "h-full w-full" : "w-full"}>
|
||||
{activeView === "overview" ? overviewQuery.isLoading ? <WorkspaceLoadingState label="正在统计制作进度" detail="只读取聚合数据,不加载全部镜头历史" /> : overviewQuery.data ? <ProjectOverviewView detail={detail} overview={overviewQuery.data} refreshProject={refreshProject} onCreateCanvas={createCanvas} /> : <WorkspaceErrorState title="制作概览读取失败" description="请稍后重试。" onRetry={() => void overviewQuery.refetch()} /> : null}
|
||||
{activeView === "chapters" ? workspaceQuery.isLoading ? <WorkspaceLoadingState label="正在读取当前章节" detail="正文与制作数据按章节加载" /> : workspaceQuery.isError ? <WorkspaceErrorState title="章节读取失败" description="当前章节可能已被删除,或服务暂时不可用。" onRetry={() => void workspaceQuery.refetch()} /> : <ProjectChaptersView detail={detail} refreshProject={refreshProject} onCreateCanvas={createCanvas} /> : null}
|
||||
{activeView === "workflow" ? workspaceQuery.isLoading ? <WorkspaceLoadingState label="正在读取当前章节分镜" detail="仅加载本章镜头、版本和产物" /> : workspaceQuery.isError ? <WorkspaceErrorState title="分镜工作区读取失败" description="当前章节制作数据暂时不可用。" onRetry={() => void workspaceQuery.refetch()} /> : <ProjectWorkflowView detail={detail} projectId={projectId} unitId={unitId || ""} stage={stage || "video"} /> : null}
|
||||
{activeView === "canvases" ? <ProjectCanvasesView detail={detail} refreshProject={refreshProject} onCreateCanvas={createCanvas} /> : null}
|
||||
{activeView === "assets" ? <ProjectAssetsView detail={detail} refreshProject={refreshProject} onCreateCanvas={createCanvas} /> : null}
|
||||
{activeView === "settings" ? <ProjectSettingsView detail={detail} refreshProject={refreshProject} onCreateCanvas={createCanvas} /> : null}
|
||||
{activeView === "editor" ? <ProjectEditorView detail={detail} /> : null}
|
||||
</div>
|
||||
</Suspense>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@@ -151,14 +160,14 @@ function ProjectWorkspaceTopBar({ detail, projectId, activeView, unitId, stage,
|
||||
const navigate = useNavigate();
|
||||
const createCanvasLabel = activeView === "chapters" && detail.units.length ? "新建当前章节画布" : "新建项目画布";
|
||||
return (
|
||||
<div className="project-workspace-topbar flex min-w-0 translate-y-px items-center gap-2">
|
||||
<button type="button" onClick={() => navigate("/projects")} className="grid size-8 shrink-0 place-items-center rounded-md text-foreground/42 transition-colors hover:bg-surface-hover hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" aria-label="返回项目" title="返回项目"><ArrowLeft className="size-4" /></button>
|
||||
<div className="hidden min-w-0 items-center gap-2 md:flex lg:w-44 xl:w-56">
|
||||
<h1 className="m-0! min-w-0 truncate text-[var(--fs-caption)] font-semibold text-foreground/90">{detail.project.name}</h1>
|
||||
<span className={`size-1.5 shrink-0 rounded-full ${detail.project.status === "archived" ? "bg-foreground/30" : "bg-[var(--workspace-accent)]"}`} />
|
||||
<span className="hidden shrink-0 text-[var(--fs-tiny)] text-foreground/42 xl:inline">{detail.project.status === "archived" ? "已归档" : "进行中"}</span>
|
||||
<div className="project-workspace-topbar">
|
||||
<button type="button" onClick={() => navigate("/projects")} className="project-workspace-back" aria-label="返回项目" title="返回项目"><ArrowLeft /></button>
|
||||
<div className="project-workspace-identity">
|
||||
<h1 title={detail.project.name}>{detail.project.name}</h1>
|
||||
<span className={`project-workspace-status-dot ${detail.project.status === "archived" ? "is-archived" : ""}`} />
|
||||
<span className="project-workspace-status-label">{detail.project.status === "archived" ? "已归档" : "进行中"}</span>
|
||||
</div>
|
||||
<nav className="thin-scrollbar flex h-11 min-w-0 flex-1 items-center gap-0.5 overflow-x-auto" aria-label="项目导航">
|
||||
<nav className="project-workspace-tabs thin-scrollbar" aria-label="项目导航">
|
||||
{views.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const active = item.key === activeView;
|
||||
@@ -169,9 +178,10 @@ function ProjectWorkspaceTopBar({ detail, projectId, activeView, unitId, stage,
|
||||
to={href}
|
||||
aria-label={item.label}
|
||||
aria-current={active ? "page" : undefined}
|
||||
className={`relative grid size-8 shrink-0 place-items-center rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${active ? "bg-[var(--workspace-accent-soft)] text-[var(--workspace-accent)] after:absolute after:inset-x-2 after:bottom-0 after:h-0.5 after:rounded-full after:bg-[var(--workspace-accent)]" : "text-foreground/45 hover:bg-surface-hover hover:text-foreground"}`}
|
||||
className={`project-workspace-tab ${active ? "is-active" : ""}`}
|
||||
>
|
||||
<Icon className="size-4" />
|
||||
<Icon />
|
||||
<span>{item.label}</span>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
);
|
||||
@@ -180,7 +190,7 @@ function ProjectWorkspaceTopBar({ detail, projectId, activeView, unitId, stage,
|
||||
{activeView === "workflow" ? (
|
||||
<WorkflowChapterNavigator projectId={projectId} units={detail.units} unitId={unitId} stage={stage} />
|
||||
) : (
|
||||
<Tooltip title={createCanvasLabel} mouseEnterDelay={0.15}><button type="button" onClick={onCreateCanvas} className="grid size-8 shrink-0 place-items-center rounded-md text-foreground/42 transition-colors hover:bg-surface-hover hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" aria-label={createCanvasLabel}><Plus className="size-4" /></button></Tooltip>
|
||||
<Tooltip title={createCanvasLabel} mouseEnterDelay={0.15}><button type="button" onClick={onCreateCanvas} className="project-workspace-create" aria-label={createCanvasLabel}><Plus /></button></Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -441,6 +441,7 @@ export default function ProjectAssetsView({ detail, refreshProject }: ProjectDet
|
||||
|
||||
<AssetLibraryPickerModal
|
||||
open={addOpen}
|
||||
remoteLibrary
|
||||
items={availablePickerItems}
|
||||
categoryLabels={{ ...pickerCategoryLabels, ...externalAssetSources.categoryLabels }}
|
||||
folders={externalAssetSources.folders}
|
||||
@@ -462,6 +463,8 @@ export default function ProjectAssetsView({ detail, refreshProject }: ProjectDet
|
||||
</Modal>
|
||||
<AssetLibraryPickerModal
|
||||
open={Boolean(imageAsset)}
|
||||
remoteLibrary
|
||||
remoteKind="image"
|
||||
items={imagePickerItems}
|
||||
categoryLabels={{ ...pickerCategoryLabels, ...externalAssetSources.categoryLabels }}
|
||||
folders={externalAssetSources.folders}
|
||||
@@ -480,6 +483,8 @@ export default function ProjectAssetsView({ detail, refreshProject }: ProjectDet
|
||||
/>
|
||||
<AssetLibraryPickerModal
|
||||
open={voicePickerOpen}
|
||||
remoteLibrary
|
||||
remoteKind="audio"
|
||||
items={audioPickerItems}
|
||||
categoryLabels={{ all: "全部音频", audio: "声音素材" }}
|
||||
initialCategory="audio"
|
||||
|
||||
@@ -13,7 +13,6 @@ import { modelQuoteRequest } from "@/lib/model-pricing";
|
||||
import { customShotTitle, formatShotOrdinal, normalizeDefaultShotTitle } from "@/lib/shot-label";
|
||||
import { modelCompatibilityError, resolveCompatibleModel, resolveModelVideoBooleanOptions, type ModelRequirements } from "@/lib/model-selection";
|
||||
import { formatVideoResolutionLabel } from "@/lib/video-generation-options";
|
||||
import { captureVideoPoster } from "@/lib/video-poster";
|
||||
import { submitBackendGenerationTask } from "@/services/api/generation-task";
|
||||
import { quoteLogicalModel } from "@/services/api/logical-models";
|
||||
import { type GenerationTask } from "@/services/api/task-center";
|
||||
@@ -696,37 +695,22 @@ function LatestPreview({ artifact, emptyText, onPreviewImage }: { artifact?: Sho
|
||||
}
|
||||
|
||||
function VideoArtifactPreview({ src, title }: { src: string; title: string }) {
|
||||
const [posterUrl, setPosterUrl] = useState("");
|
||||
const [playing, setPlaying] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
let objectUrl = "";
|
||||
setPosterUrl("");
|
||||
setPlaying(false);
|
||||
void captureVideoPoster(src, { maxWidth: 960 })
|
||||
.then((captured) => {
|
||||
if (!active || !captured.poster) return;
|
||||
objectUrl = URL.createObjectURL(captured.poster);
|
||||
setPosterUrl(objectUrl);
|
||||
})
|
||||
.catch(() => undefined);
|
||||
return () => {
|
||||
active = false;
|
||||
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
||||
};
|
||||
}, [src]);
|
||||
|
||||
if (playing) return <video className="workflow-preview-media" src={src} poster={posterUrl || undefined} controls autoPlay playsInline preload="metadata" aria-label={title} />;
|
||||
if (playing) return <video className="workflow-preview-media" src={src} controls autoPlay playsInline preload="metadata" aria-label={title} />;
|
||||
return <button type="button" className="workflow-preview-media-button workflow-video-poster" onClick={() => setPlaying(true)} aria-label={`点击播放${title}`}>
|
||||
{posterUrl ? <img className="workflow-preview-media" src={posterUrl} alt={`${title}首帧`} /> : <video className="workflow-preview-media" src={src} muted playsInline preload="auto" aria-hidden="true" />}
|
||||
<span className="workflow-preview-media workflow-video-placeholder" aria-hidden="true"><Film /></span>
|
||||
<span className="workflow-video-play" aria-hidden="true"><Play className="size-6" fill="currentColor" /></span>
|
||||
</button>;
|
||||
}
|
||||
|
||||
function ArtifactHistory({ artifacts, activeId, onSelect, compact = false }: { artifacts: ShotArtifact[]; activeId?: string; onSelect: (artifact: ShotArtifact) => void; compact?: boolean }) {
|
||||
if (!artifacts.length) return compact ? null : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无历史版本" />;
|
||||
return <section className={`workflow-history ${compact ? "is-compact" : ""}`}><div className="workflow-history-title">历史版本</div>{artifacts.map((artifact) => <button key={artifact.id} type="button" className={artifact.id === activeId ? "is-active" : ""} onClick={() => onSelect(artifact)}>{artifact.resourceId ? artifact.type === "video" ? <video src={resourceFileUrl(artifact.resourceId)} muted preload="metadata" /> : <img src={resourceFileUrl(artifact.resourceId)} alt="" loading="lazy" /> : <span className="workflow-history-placeholder"><Layers3 /></span>}<span className="min-w-0 flex-1"><strong>v{artifact.version}{artifact.selected ? " · 当前" : ""}</strong><small>{new Date(artifact.createdAt).toLocaleString("zh-CN", { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" })}</small></span><ArtifactStatus artifact={artifact} compact /></button>)}</section>;
|
||||
return <section className={`workflow-history ${compact ? "is-compact" : ""}`}><div className="workflow-history-title">历史版本</div>{artifacts.map((artifact) => <button key={artifact.id} type="button" className={artifact.id === activeId ? "is-active" : ""} onClick={() => onSelect(artifact)}>{artifact.resourceId ? artifact.type === "video" ? <span className="workflow-history-placeholder"><Film /></span> : <img src={resourceFileUrl(artifact.resourceId)} alt="" loading="lazy" /> : <span className="workflow-history-placeholder"><Layers3 /></span>}<span className="min-w-0 flex-1"><strong>v{artifact.version}{artifact.selected ? " · 当前" : ""}</strong><small>{new Date(artifact.createdAt).toLocaleString("zh-CN", { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" })}</small></span><ArtifactStatus artifact={artifact} compact /></button>)}</section>;
|
||||
}
|
||||
|
||||
function ShotTimeline({ activeStage, detail, shots, selectedShotId, submittingShotIds, onSelectShot, onAddShot, addingShot }: { activeStage: ShortDramaWorkflowStage; detail: ProjectDetail; shots: ProjectShot[]; selectedShotId: string; submittingShotIds: Set<string>; onSelectShot: (id: string) => void; onAddShot: () => void; addingShot: boolean }) {
|
||||
@@ -753,7 +737,7 @@ function TimelineShot({ artifactType, detail, shot, task, submitting, index, sel
|
||||
const revision = currentRevision(detail, shot);
|
||||
const stageLabel = artifactType === "video" ? "镜头视频" : artifactType === "action_board" ? "动作预演" : "分镜画面";
|
||||
const cameraMeta = [revision?.shotSize, revision?.cameraMovement].filter(Boolean).join(" · ") || "等待补充镜头参数";
|
||||
return <button type="button" className={`workflow-timeline-shot ${selected ? "is-active" : ""}`} onClick={onSelect}><span className="workflow-timeline-media">{preview?.resourceId ? preview.type === "video" ? <video src={resourceFileUrl(preview.resourceId)} muted preload="metadata" /> : <img src={resourceFileUrl(preview.resourceId)} alt="" loading="lazy" /> : <Film />}</span><span className="workflow-timeline-copy"><span className="workflow-timeline-heading"><strong>{formatShotOrdinal(index)}</strong><b>{formatDuration(shot.durationMs)}</b></span>{customShotTitle(shot.title, index) ? <em className="workflow-timeline-title">{customShotTitle(shot.title, index)}</em> : null}<small className="workflow-timeline-meta">{cameraMeta}</small><span className="workflow-timeline-status"><span>{stageLabel}{stateArtifact ? ` · v${stateArtifact.version}` : ""}</span><ArtifactStatus artifact={stateArtifact} taskStatus={submitting ? "queued" : task?.status} compact /></span></span></button>;
|
||||
return <button type="button" className={`workflow-timeline-shot ${selected ? "is-active" : ""}`} onClick={onSelect}><span className="workflow-timeline-media">{preview?.resourceId && preview.type !== "video" ? <img src={resourceFileUrl(preview.resourceId)} alt="" loading="lazy" /> : <Film />}</span><span className="workflow-timeline-copy"><span className="workflow-timeline-heading"><strong>{formatShotOrdinal(index)}</strong><b>{formatDuration(shot.durationMs)}</b></span>{customShotTitle(shot.title, index) ? <em className="workflow-timeline-title">{customShotTitle(shot.title, index)}</em> : null}<small className="workflow-timeline-meta">{cameraMeta}</small><span className="workflow-timeline-status"><span>{stageLabel}{stateArtifact ? ` · v${stateArtifact.version}` : ""}</span><ArtifactStatus artifact={stateArtifact} taskStatus={submitting ? "queued" : task?.status} compact /></span></span></button>;
|
||||
}
|
||||
|
||||
function revisionInput(values: ShotEditorValues): ShotRevisionInput {
|
||||
|
||||
@@ -631,6 +631,18 @@
|
||||
|
||||
.workflow-preview-media-button:hover .workflow-preview-media { transform: scale(1.005); }
|
||||
|
||||
.workflow-video-placeholder {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1px solid var(--workspace-border);
|
||||
color: color-mix(in srgb, var(--foreground) 30%, transparent);
|
||||
}
|
||||
|
||||
.workflow-video-placeholder svg {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.workflow-preview-expand,
|
||||
.workflow-video-play {
|
||||
position: absolute;
|
||||
|
||||
Reference in New Issue
Block a user