Files
Genarrative/src/components/image-editor/ImageCanvasAssetLibraryModel.ts
T
k88936 498b905bd8
Project CI / Repository checks (push) Successful in 1m21s
Project CI / Frontend tests (push) Successful in 3m9s
Project CI / Backend tests (push) Successful in 4m2s
Project CI / Native shell tests (push) Successful in 15m48s
优化素材库多选操作 (#114)
<video src="attachments/2377fc8b-50e4-4919-ba41-5027b957709f" title="2026-07-27 16-15-53.mp4" controls></video>

- 实现shift ctrl的范围选扩选
- 实现多选下载(为一个zip)
- 原本有搜索和文件夹折叠展开功能, 我的处理是: 只作为方便查找的用途. 最终操作会有"目前一些(比如待删除)元素被隐藏了,请再确认"的提示.

result:
```shell
root@k88936-t14p: ~/Downloads# unzip 未命名画布-选中素材-20260806-195631.zip
Archive:  未命名画布-选中素材-20260806-195631.zip
   creating: 未命名画布-选中素材/
   creating: 未命名画布-选中素材/images/
   creating: 未命名画布-选中素材/media/
 extracting: 未命名画布-选中素材/media/001-角色动作(原始视频).mp4
   creating: 未命名画布-选中素材/sequences/
   creating: 未命名画布-选中素材/sequences/002-角色动作/
   creating: 未命名画布-选中素材/sequences/002-角色动作/frames/
 extracting: 未命名画布-选中素材/sequences/002-角色动作/frames/frame-01.png
 extracting: 未命名画布-选中素材/sequences/002-角色动作/frames/frame-02.png
 extracting: 未命名画布-选中素材/sequences/002-角色动作/frames/frame-03.png
...
 extracting: 未命名画布-选中素材/sequences/002-角色动作/frames/frame-31.png
 extracting: 未命名画布-选中素材/sequences/002-角色动作/frames/frame-32.png
 extracting: 未命名画布-选中素材/sequences/002-角色动作/metadata.json
 extracting: 未命名画布-选中素材/sequences/002-角色动作/skeleton.json
 extracting: 未命名画布-选中素材/sequences/002-角色动作/README.md
 extracting: 未命名画布-选中素材/sequences/002-角色动作/manifest.txt
 extracting: 未命名画布-选中素材/metadata.json
 extracting: 未命名画布-选中素材/manifest.txt
 ```

~~动作的导出存在很大的问题, 在另一个pr #117 解决~~

---------

Co-authored-by: 段舒康 <kdletters@qq.com>
Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/114
Co-authored-by: 王德宇 <kvtodev@outlook.com>
Co-committed-by: 王德宇 <kvtodev@outlook.com>
2026-08-08 11:11:25 +08:00

447 lines
9.9 KiB
TypeScript

import { isEditorInternalProcessingModel } from './ImageCanvasEditorModel';
import type {
CanvasLayer,
EditorAsset,
EditorAssetFolder,
} from './ImageCanvasEditorTypes';
type ClientRectLike = Pick<DOMRect, 'bottom' | 'left' | 'right' | 'top'>;
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<string | null | undefined>,
) {
if (!query) {
return true;
}
return values.some((value) => value?.toLowerCase().includes(query));
}
function startsWithSearchText(
query: string,
values: Array<string | null | undefined>,
) {
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<string>,
) {
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,
);
}