498b905bd8
<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>
447 lines
9.9 KiB
TypeScript
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,
|
|
);
|
|
}
|