// 展示项目资产库,按来源区分「本地上传」与「项目素材」;点击资产 → makeClipFromAsset // → dispatch addClip 加入时间线(添加到匹配 kind 的轨道末尾)。资产是 // "仅时间线作用域"直连媒体(nodeId=asset:)。导入链路:uploadResourceFile → // linkProjectAsset(后端按资源元数据合成资产记录)→ refreshAssets。 import { useEffect, useRef, useState } from "react"; import { Boxes, Check, ChevronDown, ChevronRight, Clapperboard, Film, FolderOpen, HardDrive, Image as ImageIcon, Loader2, Music2, Plus, X } from "lucide-react"; import { useEditorHostContext, useEditorStoreContext } from "@/components/editor/editor-context"; import { defaultAssetCategoryForKind } from "@/lib/asset-category"; import { probeMediaDurationMs } from "@/lib/media-metadata"; import { makeClipFromAsset } from "@/lib/timeline/asset-ingest"; import { DEFAULT_AUDIO_TRACK_ID, DEFAULT_SUBTITLE_TRACK_ID, DEFAULT_VIDEO_TRACK_ID } from "@/lib/timeline/timeline-tracks"; import { linkProjectAsset } from "@/services/api/projects"; import { uploadResourceFile, type ResourceUploadMeta } from "@/services/api/resources"; import { resolveMediaUrl } from "@/services/file-storage"; import type { ProjectAsset } from "@/services/api/projects"; import type { TimelineProject } from "@/types/timeline"; const MEDIA_ACCEPT = "video/*,audio/*,image/*"; /** 库内重复判定用的归一键(文件名+媒体类型)。 */ function assetDedupeKey(title: string | undefined, mediaType: string): string { return `${(title || "").trim().toLowerCase()}|${mediaType}`; } /** 毫秒 → m:ss 时长文案。 */ function formatDurationMs(ms: number | undefined): string { if (!ms || ms <= 0) return ""; const total = Math.round(ms / 1000); return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, "0")}`; } const IMAGE_RE = /\.(png|jpe?g|webp|gif|avif)$/i; const AUDIO_RE = /\.(mp3|wav|m4a|ogg|flac|aac)$/i; const MEDIA_RE = /\.(mp4|mov|webm|mkv|m4v|avi)$/i; /** 本地上传标记(assetFromUploadedResource 写入 payload.data.source)。画布产物同步时写入 canvas,仅作数据追溯,面板不再单列分组。 */ const SOURCE_UPLOADED = "uploaded"; type AssetFilter = "all" | "project" | "uploaded"; const FILTER_TABS: { id: AssetFilter; label: string }[] = [ { id: "all", label: "全部" }, { id: "project", label: "项目素材" }, { id: "uploaded", label: "本地上传" }, ]; /** 按 MIME 与扩展名推断媒体 kind;非媒体文件返回 null。 */ function kindFromFile(file: File): "image" | "video" | "audio" | null { if (file.type.startsWith("video/")) return "video"; if (file.type.startsWith("audio/")) return "audio"; if (file.type.startsWith("image/")) return "image"; if (IMAGE_RE.test(file.name)) return "image"; if (AUDIO_RE.test(file.name)) return "audio"; if (MEDIA_RE.test(file.name)) return "video"; return null; } function trackIdForKind(kind: string, project: TimelineProject): string { const track = project.tracks.find((t) => t.kind === kind); if (track) return track.id; if (kind === "video" || kind === "image") return project.tracks.find((t) => t.kind === "video")?.id ?? DEFAULT_VIDEO_TRACK_ID; if (kind === "audio") return project.tracks.find((t) => t.kind === "audio")?.id ?? DEFAULT_AUDIO_TRACK_ID; return DEFAULT_SUBTITLE_TRACK_ID; } function trackEndMs(project: TimelineProject, trackId: string): number { return project.clips.filter((c) => c.trackId === trackId).reduce((max, c) => Math.max(max, c.startMs + c.durationMs), 0); } function AssetIcon({ mediaType }: { mediaType: string }) { const cls = "size-4"; if (mediaType === "video") return ; if (mediaType === "audio") return ; if (mediaType === "image") return ; return ; } /** 列表行小缩略图:图片媒体显示真实画面;视频/音频无法在 解码, * 统一渲染类型色块+图标占位(不再出现破图),视频缩略图由展开卡片提供。 */ function AssetThumb({ asset }: { asset: ProjectAsset }) { const isImage = asset.mediaType === "image"; const [url, setUrl] = useState(null); useEffect(() => { if (!isImage || !asset.storageKey) return; let alive = true; resolveMediaUrl(asset.storageKey) .then((resolved) => alive && setUrl(resolved ?? null)) .catch(() => alive && setUrl(null)); return () => { alive = false; }; }, [asset.storageKey, isImage]); if (url) { return (
); } const kind = asset.mediaType === "video" ? { icon: , tone: "text-[var(--director-accent)]", tile: "bg-[var(--director-accent)]/15" } : asset.mediaType === "audio" ? { icon: , tone: "text-[var(--director-dock-fg)]/70", tile: "bg-[var(--director-control-hover)]" } : { icon: , tone: "text-[var(--director-dock-fg)]/60", tile: "bg-[var(--director-control-hover)]" }; return (
{kind.icon}
); } /** 尝试用