import { isEditorInternalProcessingModel } from './ImageCanvasEditorModel'; import type { CanvasLayer, EditorAsset, EditorAssetFolder, } from './ImageCanvasEditorTypes'; type ClientRectLike = Pick; type ClientPoint = { clientX: number; clientY: number; }; type AssetFolderHitTarget = { folderId: string; rect: ClientRectLike; }; export type GroupedAssetFolder = EditorAssetFolder & { assets: EditorAsset[]; }; export const EDITOR_UPLOAD_ASSET_FOLDER_LABEL = '上传素材'; function isPointInRect(point: ClientPoint, rect: ClientRectLike) { return ( point.clientX >= rect.left && point.clientX <= rect.right && point.clientY >= rect.top && point.clientY <= rect.bottom ); } export function resolveAssetFolderIdFromPoint({ point, listRect, folders, }: { point: ClientPoint; listRect: ClientRectLike | null | undefined; folders: AssetFolderHitTarget[]; }) { if (!listRect || !isPointInRect(point, listRect)) { return null; } const matchedFolder = folders.find((folder) => isPointInRect(point, folder.rect), ); return matchedFolder?.folderId ?? null; } export function resolvePinnedAssetMoveFolderId({ folderId, listRect, headerRect, }: { folderId: string | null; listRect: ClientRectLike | null | undefined; headerRect: ClientRectLike | null | undefined; }) { if ( !folderId || !listRect || !headerRect || (headerRect.bottom >= listRect.top && headerRect.top <= listRect.bottom) ) { return null; } return folderId; } export function groupAssetsByFolder( assetFolders: EditorAssetFolder[], assets: EditorAsset[], ): GroupedAssetFolder[] { return assetFolders.map((folder) => ({ ...folder, assets: assets.filter((asset) => asset.folderId === folder.id), })); } function normalizeSearchQuery(query: string) { return query.trim().toLowerCase(); } function matchesSearchText( query: string, values: Array, ) { if (!query) { return true; } return values.some((value) => value?.toLowerCase().includes(query)); } function startsWithSearchText( query: string, values: Array, ) { if (!query) { return true; } return values.some((value) => value?.trim().toLowerCase().startsWith(query)); } function resolveSearchableEditorModel(model: string | null | undefined) { const normalizedModel = model?.trim(); return normalizedModel && !isEditorInternalProcessingModel(normalizedModel) ? normalizedModel : undefined; } function buildAssetSearchValues(asset: EditorAsset) { return [ asset.label, asset.prompt, asset.actualPrompt, resolveSearchableEditorModel(asset.model), asset.taskId, asset.assetKind, asset.mediaType, ]; } function buildLayerSearchValues(layer: CanvasLayer) { return [ layer.title, layer.prompt, layer.actualPrompt, resolveSearchableEditorModel(layer.model), layer.taskId, layer.assetKind, layer.mediaType, ]; } function assetMatchesSearch(asset: EditorAsset, query: string) { return matchesSearchText(query, buildAssetSearchValues(asset)); } function assetStartsWithSearch(asset: EditorAsset, query: string) { return startsWithSearchText(query, buildAssetSearchValues(asset)); } export function layerMatchesSearch(layer: CanvasLayer, query: string) { return matchesSearchText(query, buildLayerSearchValues(layer)); } export function layerStartsWithSearch(layer: CanvasLayer, query: string) { return startsWithSearchText(query, buildLayerSearchValues(layer)); } export function filterGroupedAssetsBySearch( groupedAssets: GroupedAssetFolder[], query: string, ) { const normalizedQuery = normalizeSearchQuery(query); if (!normalizedQuery) { return groupedAssets; } return groupedAssets .map((folder) => { const folderMatches = startsWithSearchText(normalizedQuery, [folder.label]) || matchesSearchText(normalizedQuery, [folder.label]); const prefixAssets = folder.assets.filter((asset) => assetStartsWithSearch(asset, normalizedQuery), ); const matchingAssets = folder.assets.filter((asset) => assetMatchesSearch(asset, normalizedQuery), ); return { ...folder, assets: folderMatches ? folder.assets : [ ...prefixAssets, ...matchingAssets.filter( (asset) => !prefixAssets.some( (prefixAsset) => prefixAsset.id === asset.id, ), ), ], }; }) .filter((folder) => folder.assets.length > 0); } export type AssetShowcaseSubmissionState = { canSubmit: boolean; label: string; reason: string | null; }; function normalizeAssetShowcaseReason(value: string | null | undefined) { return value?.trim() || null; } export function resolveAssetShowcaseSubmissionState( asset: EditorAsset, ): AssetShowcaseSubmissionState { const reviewStatus = asset.showcaseReviewStatus ?? null; if (reviewStatus === 'pending') { return { canSubmit: false, label: '精选审核中', reason: '素材正在审核中,请等待审核结果', }; } if (reviewStatus === 'approved') { return { canSubmit: false, label: asset.showcaseDisplayEnabled === false ? '已入选精选,暂未展示' : '已入选精选', reason: asset.showcaseDisplayEnabled === false ? '素材已通过审核,运营后台开启展示后才会出现在前台' : '素材已经入选精选', }; } if (reviewStatus === 'rejected') { return { canSubmit: false, label: '精选未通过', reason: '该素材审核未通过,不能重复提交', }; } if (asset.sourceType !== 'generated') { return { canSubmit: false, label: '提交精选审核', reason: '只有生成素材可以提交精选审核', }; } if (!asset.persisted) { return { canSubmit: false, label: '提交精选审核', reason: '素材保存完成后才能提交精选审核', }; } if (!asset.src.trim()) { return { canSubmit: false, label: '提交精选审核', reason: '没有媒体内容的素材不能提交精选审核', }; } return { canSubmit: true, label: '提交精选审核', reason: normalizeAssetShowcaseReason(asset.showcaseSubmitError), }; } export function renameAssetById( assets: EditorAsset[], assetId: string, label: string, ) { return assets.map((asset) => asset.id === assetId ? { ...asset, label, } : asset, ); } export function toggleAssetFolderCollapsed( assetFolders: EditorAssetFolder[], folderId: string, ) { return assetFolders.map((folder) => folder.id === folderId ? { ...folder, collapsed: !folder.collapsed, } : folder, ); } export function createLocalAssetFolder({ folderId, label, }: { folderId: string; label: string; }): EditorAssetFolder { return { id: folderId, label, collapsed: false, systemDefault: false, persisted: false, }; } export function replaceLocalAssetFolder({ folders, assets, localFolderId, persistedFolder, }: { folders: EditorAssetFolder[]; assets: EditorAsset[]; localFolderId: string; persistedFolder: { folderId: string; label: string; collapsed: boolean; systemDefault: boolean; }; }) { return { folders: folders.map((folder) => folder.id === localFolderId ? { id: persistedFolder.folderId, label: persistedFolder.label, collapsed: persistedFolder.collapsed, systemDefault: persistedFolder.systemDefault, persisted: true, } : folder, ), assets: assets.map((asset) => asset.folderId === localFolderId ? { ...asset, folderId: persistedFolder.folderId, } : asset, ), }; } export function removeAssetById(assets: EditorAsset[], assetId: string) { return assets.filter((asset) => asset.id !== assetId); } export function renameAssetFolderById( assetFolders: EditorAssetFolder[], folderId: string, label: string, ) { return assetFolders.map((folder) => folder.id === folderId ? { ...folder, label, } : folder, ); } export function resolveDefaultAssetFolder(assetFolders: EditorAssetFolder[]) { return ( assetFolders.find((folder) => folder.systemDefault) ?? assetFolders[0] ?? null ); } export function resolveUploadAssetFolder(assetFolders: EditorAssetFolder[]) { return ( assetFolders.find( (folder) => folder.label.trim() === EDITOR_UPLOAD_ASSET_FOLDER_LABEL, ) ?? null ); } export function resolvePreferredAssetUploadFolder( assetFolders: EditorAssetFolder[], ) { return ( resolveUploadAssetFolder(assetFolders) ?? resolveDefaultAssetFolder(assetFolders) ); } export function deleteAssetFolderLocally({ folders, assets, folderId, defaultFolderId, }: { folders: EditorAssetFolder[]; assets: EditorAsset[]; folderId: string; defaultFolderId: string; }) { return { folders: folders.filter((folder) => folder.id !== folderId), assets: assets.map((asset) => asset.folderId === folderId ? { ...asset, folderId: defaultFolderId, } : asset, ), }; } export function removeSelectedAssets( assets: EditorAsset[], selectedAssetIds: Set, ) { const deletedAssets = assets.filter((asset) => selectedAssetIds.has(asset.id), ); return { assets: assets.filter((asset) => !selectedAssetIds.has(asset.id)), deletedAssets, }; } export function moveAssetToFolderLocally( assets: EditorAsset[], assetId: string, folderId: string, ) { return assets.map((asset) => asset.id === assetId ? { ...asset, folderId, } : asset, ); }