Files
ttstd-infinite-canvas/web/src/components/canvas/asset-picker-modal.tsx

127 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useMemo } from "react";
import { AssetLibraryPickerModal, type AssetLibraryPickerItem } from "@/components/assets/asset-library-picker-modal";
import { useExternalAssetSources } from "@/hooks/use-external-asset-sources";
import { ASSET_CATEGORY_LABELS, normalizeAssetCategory } from "@/lib/asset-category";
import type { ExternalAssetPickerReference } from "@/lib/plugins/plugin-types";
import { useAssetStore, type Asset } from "@/stores/use-asset-store";
type InsertableAsset = Extract<Asset, { kind: "text" | "image" | "video" | "audio" }>;
export type InsertAssetPayload =
| { kind: "text"; content: string; title: string; assetId?: string }
| { kind: "image"; dataUrl: string; title: string; url?: string; storageKey?: string; width?: number; height?: number; bytes?: number; mimeType?: string; assetId?: string }
| { kind: "video"; url: string; title: string; storageKey?: string; width?: number; height?: number; durationMs?: number; hasAudio?: boolean; bytes?: number; mimeType?: string; assetId?: string }
| { kind: "audio"; url: string; title: string; storageKey?: string; durationMs?: number; bytes?: number; mimeType?: string; assetId?: string }
| {
kind: "character";
title: string;
assetId: string;
versionId: string;
prompt: string;
aliases: string[];
definition: Record<string, unknown>;
coverUrl?: string;
visualStatus: string;
voiceStatus: string;
voiceName?: string;
voiceProfile?: { name: string; provider: string; language: string; timbre: string };
voiceInstructions?: string;
};
type Props = {
open: boolean;
multiple?: boolean;
onInsert: (payloads: InsertAssetPayload[]) => Promise<void> | void;
onClose: () => void;
};
const categoryLabels: Record<string, string> = { all: "全部素材", ...ASSET_CATEGORY_LABELS, archived: "回收站" };
export function AssetPickerModal({ open, multiple = true, onInsert, onClose }: Props) {
const assets = useAssetStore((state) => state.assets);
const externalAssetSources = useExternalAssetSources(open);
const insertableAssets = useMemo(() => assets.filter((asset): asset is InsertableAsset => asset.kind === "text" || asset.kind === "image" || asset.kind === "video" || asset.kind === "audio"), [assets]);
const items = useMemo<AssetLibraryPickerItem[]>(
() => [
...insertableAssets.map((asset) => ({
id: asset.id,
title: asset.title,
category: normalizeAssetCategory(asset.category),
archived: asset.status === "archived",
kindLabel: asset.kind === "image" ? "图片" : asset.kind === "video" ? "视频" : asset.kind === "audio" ? "音频" : "文本",
mediaKind: asset.kind,
asset,
searchText: (asset.tags || []).join(" "),
})),
...externalAssetSources.items,
],
[externalAssetSources.items, insertableAssets],
);
return (
<AssetLibraryPickerModal
remoteLibrary
open={open}
mediaKinds={["image", "video", "audio", "text"]}
items={items}
categoryLabels={{ ...categoryLabels, ...externalAssetSources.categoryLabels }}
folders={externalAssetSources.folders}
footerNote={externalAssetSources.error || undefined}
multiple={multiple}
confirmLabel={(count) => `插入已选素材${count ? `(${count})` : ""}`}
emptyDescription="先在素材库中添加图片、视频、音频或文本。"
onClose={onClose}
onConfirm={async (ids) => {
await onInsert(assetPickerItemsToInsertPayloads(ids, items));
onClose();
}}
/>
);
}
export function assetPickerItemsToInsertPayloads(ids: string[], items: AssetLibraryPickerItem[]): InsertAssetPayload[] {
const itemsById = new Map(items.map((item) => [item.id, item]));
return ids.map((id) => {
const pickerItem = itemsById.get(id);
if (!pickerItem) throw new Error("所选素材已不存在,请重新选择");
if (pickerItem.external) return externalAssetToInsertPayload(pickerItem.external);
const asset = pickerItem.asset;
if (!asset || (asset.kind !== "text" && asset.kind !== "image" && asset.kind !== "video" && asset.kind !== "audio")) {
throw new Error(`“${pickerItem.title}”不是可插入画布的素材`);
}
return localAssetToInsertPayload(asset);
});
}
function localAssetToInsertPayload(asset: InsertableAsset): InsertAssetPayload {
if (asset.kind === "text") return { kind: "text", content: asset.data.content, title: asset.title, assetId: asset.id };
if (asset.kind === "audio") return { kind: "audio", url: asset.data.url, storageKey: asset.data.storageKey, title: asset.title, durationMs: asset.data.durationMs, bytes: asset.data.bytes, mimeType: asset.data.mimeType, assetId: asset.id };
if (asset.kind === "video")
return {
kind: "video",
url: asset.data.url,
storageKey: asset.data.storageKey,
title: asset.title,
width: asset.data.width,
height: asset.data.height,
durationMs: asset.data.durationMs,
hasAudio: asset.data.hasAudio,
bytes: asset.data.bytes,
mimeType: asset.data.mimeType,
assetId: asset.id,
};
return { kind: "image", dataUrl: asset.data.dataUrl, storageKey: asset.data.storageKey, title: asset.title, width: asset.data.width, height: asset.data.height, bytes: asset.data.bytes, mimeType: asset.data.mimeType, assetId: asset.id };
}
export function externalAssetToInsertPayload(reference: ExternalAssetPickerReference): InsertAssetPayload {
const item = reference.item;
const url = item.fileUrl || "";
if (!url) throw new Error(`“${item.title}”暂时无法读取,请先在 Eagle 中确认文件可用`);
const assetId = `external:${reference.sourceId}:${item.id}`;
if (item.kind === "image") return { kind: "image", dataUrl: url, url, title: item.title || "素材图片", width: item.width, height: item.height, bytes: item.bytes, mimeType: item.mimeType, assetId };
if (item.kind === "video") return { kind: "video", url, title: item.title, width: item.width, height: item.height, bytes: item.bytes, mimeType: item.mimeType, assetId };
if (item.kind === "audio") return { kind: "audio", url, title: item.title, bytes: item.bytes, mimeType: item.mimeType, assetId };
throw new Error(`“${item.title}”不是可插入画布的媒体文件`);
}