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>
384 lines
13 KiB
TypeScript
384 lines
13 KiB
TypeScript
import { CheckSquare, FolderPlus, Square } from 'lucide-react';
|
|
import type {
|
|
Dispatch,
|
|
PointerEvent as ReactPointerEvent,
|
|
RefObject,
|
|
SetStateAction,
|
|
} from 'react';
|
|
import { useMemo, useState } from 'react';
|
|
|
|
import { PlatformTextField } from '../common/PlatformTextField';
|
|
import {
|
|
filterGroupedAssetsBySearch,
|
|
layerMatchesSearch,
|
|
layerStartsWithSearch,
|
|
} from './ImageCanvasAssetLibraryModel';
|
|
import {
|
|
type GroupedEditorAssetFolder,
|
|
ImageCanvasAssetLibraryPanelView,
|
|
type UploadFilesOptions,
|
|
type UploadRequestOptions,
|
|
} from './ImageCanvasAssetLibraryPanelView';
|
|
import { EditorIconButton } from './ImageCanvasEditorPrimitives';
|
|
import type {
|
|
AssetMarqueeState,
|
|
AssetPointerDragState,
|
|
CanvasContextMenuState,
|
|
CanvasLayer,
|
|
EditorAsset,
|
|
EditorAssetFolder,
|
|
ImageContextMenuState,
|
|
SidebarPanel,
|
|
UploadTarget,
|
|
} from './ImageCanvasEditorTypes';
|
|
import { ImageCanvasLayerPanelView } from './ImageCanvasLayerPanelView';
|
|
import {
|
|
type AssetSelectionInteraction,
|
|
getSelectableAssets,
|
|
} from './useImageCanvasAssetSelection';
|
|
|
|
export type { GroupedEditorAssetFolder, UploadFilesOptions };
|
|
|
|
export type ImageCanvasSidebarViewProps = {
|
|
activeSidebarPanel: SidebarPanel | null;
|
|
assetListRef: RefObject<HTMLDivElement | null>;
|
|
assetPointerDragRef: { current: AssetPointerDragState | null };
|
|
suppressAssetClickRef: { current: boolean };
|
|
assets: EditorAsset[];
|
|
groupedAssets: GroupedEditorAssetFolder[];
|
|
assetFolders: EditorAssetFolder[];
|
|
layers: CanvasLayer[];
|
|
selectedLayerId: string | null;
|
|
selectedLayerIds: string[];
|
|
isAssetSelectionMode: boolean;
|
|
selectedAssetIds: Set<string>;
|
|
assetMoveDropFolderId: string | null;
|
|
pinnedAssetMoveFolderId: string | null;
|
|
creatingFolder: boolean;
|
|
newFolderName: string;
|
|
renamingFolder: { folderId: string; value: string } | null;
|
|
renamingAsset: { assetId: string; value: string } | null;
|
|
isExportingSelectedAssets: boolean;
|
|
assetMarquee: AssetMarqueeState | null;
|
|
setCreatingFolder: Dispatch<SetStateAction<boolean>>;
|
|
setNewFolderName: Dispatch<SetStateAction<string>>;
|
|
setRenamingFolder: Dispatch<
|
|
SetStateAction<{ folderId: string; value: string } | null>
|
|
>;
|
|
setRenamingAsset: Dispatch<
|
|
SetStateAction<{ assetId: string; value: string } | null>
|
|
>;
|
|
setActiveUploadFolderId: Dispatch<SetStateAction<string>>;
|
|
setUploadDropTarget: Dispatch<SetStateAction<'canvas' | 'assets' | null>>;
|
|
setAssetPointerDrag: Dispatch<SetStateAction<AssetPointerDragState | null>>;
|
|
setImageContextMenu: Dispatch<SetStateAction<ImageContextMenuState | null>>;
|
|
setContextMenu: Dispatch<SetStateAction<CanvasContextMenuState | null>>;
|
|
onAssetMarqueePointerDown: (event: ReactPointerEvent<HTMLDivElement>) => void;
|
|
onAssetMarqueePointerMove: (event: ReactPointerEvent<HTMLDivElement>) => void;
|
|
onAssetMarqueePointerUp: (event: ReactPointerEvent<HTMLDivElement>) => void;
|
|
onAssetMarqueePointerCancel: (
|
|
event: ReactPointerEvent<HTMLDivElement>,
|
|
) => void;
|
|
onAssetMarqueeLostPointerCapture: (
|
|
event: ReactPointerEvent<HTMLDivElement>,
|
|
) => void;
|
|
updateAssetMoveDropFolder: (folderId: string | null) => void;
|
|
addUploadedFiles: (
|
|
files: FileList | File[],
|
|
options?: UploadFilesOptions,
|
|
) => void;
|
|
requestUpload: (target: UploadTarget, options?: UploadRequestOptions) => void;
|
|
moveAssetToFolder: (assetId: string, folderId: string) => void;
|
|
commitNewAssetFolder: () => void | Promise<void>;
|
|
toggleAssetFolder: (folderId: string) => void;
|
|
startRenamingFolder: (folder: EditorAssetFolder) => void;
|
|
commitFolderRename: (folder: EditorAssetFolder) => void;
|
|
deleteAssetFolder: (folder: EditorAssetFolder) => void;
|
|
startRenamingAsset: (asset: EditorAsset) => void;
|
|
commitAssetRename: (asset: EditorAsset) => void;
|
|
deleteUploadedAsset: (asset: EditorAsset) => void;
|
|
submitAssetToShowcase: (asset: EditorAsset) => void;
|
|
toggleAssetSelected: (
|
|
assetId: string,
|
|
interaction: AssetSelectionInteraction,
|
|
) => void;
|
|
addAssetLayer: (asset: EditorAsset) => void;
|
|
onDownloadAsset: (asset: EditorAsset) => void;
|
|
toggleAllAssetsSelected: (visibleAssetIds: string[]) => void;
|
|
downloadSelectedAssets: () => void | Promise<void>;
|
|
deleteSelectedAssets: (assetIds?: ReadonlySet<string>) => void;
|
|
openAssetSelectionMode: () => void;
|
|
closeAssetSelectionMode: () => void;
|
|
groupSelectedLayers: () => void;
|
|
selectSingleLayer: (layerId: string | null) => void;
|
|
resolveContextMenuPosition: (
|
|
clientX: number,
|
|
clientY: number,
|
|
menuKind: 'blank' | 'layer',
|
|
) => Omit<CanvasContextMenuState, 'kind' | 'layerId' | 'canvasPoint'>;
|
|
getCanvasPointFromClient: (
|
|
clientX: number,
|
|
clientY: number,
|
|
) => { x: number; y: number };
|
|
};
|
|
|
|
export function ImageCanvasSidebarView({
|
|
activeSidebarPanel,
|
|
assetListRef,
|
|
assetPointerDragRef,
|
|
suppressAssetClickRef,
|
|
groupedAssets,
|
|
assetFolders,
|
|
layers,
|
|
selectedLayerId,
|
|
selectedLayerIds,
|
|
isAssetSelectionMode,
|
|
selectedAssetIds,
|
|
assetMoveDropFolderId,
|
|
pinnedAssetMoveFolderId,
|
|
creatingFolder,
|
|
newFolderName,
|
|
renamingFolder,
|
|
renamingAsset,
|
|
isExportingSelectedAssets,
|
|
assetMarquee,
|
|
setCreatingFolder,
|
|
setNewFolderName,
|
|
setRenamingFolder,
|
|
setRenamingAsset,
|
|
setActiveUploadFolderId,
|
|
setUploadDropTarget,
|
|
setAssetPointerDrag,
|
|
setImageContextMenu,
|
|
setContextMenu,
|
|
onAssetMarqueePointerDown,
|
|
onAssetMarqueePointerMove,
|
|
onAssetMarqueePointerUp,
|
|
onAssetMarqueePointerCancel,
|
|
onAssetMarqueeLostPointerCapture,
|
|
updateAssetMoveDropFolder,
|
|
addUploadedFiles,
|
|
requestUpload,
|
|
moveAssetToFolder,
|
|
commitNewAssetFolder,
|
|
toggleAssetFolder,
|
|
startRenamingFolder,
|
|
commitFolderRename,
|
|
deleteAssetFolder,
|
|
startRenamingAsset,
|
|
commitAssetRename,
|
|
deleteUploadedAsset,
|
|
submitAssetToShowcase,
|
|
toggleAssetSelected,
|
|
addAssetLayer,
|
|
onDownloadAsset,
|
|
toggleAllAssetsSelected,
|
|
downloadSelectedAssets,
|
|
deleteSelectedAssets,
|
|
openAssetSelectionMode,
|
|
closeAssetSelectionMode,
|
|
groupSelectedLayers,
|
|
selectSingleLayer,
|
|
resolveContextMenuPosition,
|
|
getCanvasPointFromClient,
|
|
}: ImageCanvasSidebarViewProps) {
|
|
const [assetSearchQuery, setAssetSearchQuery] = useState('');
|
|
const [layerSearchQuery, setLayerSearchQuery] = useState('');
|
|
const filteredGroupedAssets = useMemo(
|
|
() => filterGroupedAssetsBySearch(groupedAssets, assetSearchQuery),
|
|
[assetSearchQuery, groupedAssets],
|
|
);
|
|
const filteredAssetsCount = useMemo(
|
|
() =>
|
|
filteredGroupedAssets.reduce(
|
|
(totalCount, folder) => totalCount + folder.assets.length,
|
|
0,
|
|
),
|
|
[filteredGroupedAssets],
|
|
);
|
|
const visibleAssetIds = useMemo(
|
|
() =>
|
|
filteredGroupedAssets.flatMap((folder) =>
|
|
folder.collapsed
|
|
? []
|
|
: getSelectableAssets(folder.assets).map((asset) => asset.id),
|
|
),
|
|
[filteredGroupedAssets],
|
|
);
|
|
const allVisibleAssetsSelected =
|
|
visibleAssetIds.length > 0 &&
|
|
visibleAssetIds.every((assetId) => selectedAssetIds.has(assetId));
|
|
|
|
const normalizedLayerSearchQuery = layerSearchQuery.trim().toLowerCase();
|
|
const filteredLayers = useMemo(
|
|
() =>
|
|
normalizedLayerSearchQuery
|
|
? [
|
|
...layers.filter((layer) =>
|
|
layerStartsWithSearch(layer, normalizedLayerSearchQuery),
|
|
),
|
|
...layers.filter(
|
|
(layer) =>
|
|
!layerStartsWithSearch(layer, normalizedLayerSearchQuery) &&
|
|
layerMatchesSearch(layer, normalizedLayerSearchQuery),
|
|
),
|
|
]
|
|
: layers,
|
|
[layers, normalizedLayerSearchQuery],
|
|
);
|
|
|
|
if (!activeSidebarPanel) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<aside
|
|
className={[
|
|
'image-canvas-editor__sidebar',
|
|
activeSidebarPanel === 'assets' && isAssetSelectionMode
|
|
? 'image-canvas-editor__sidebar--asset-selection'
|
|
: '',
|
|
]
|
|
.filter(Boolean)
|
|
.join(' ')}
|
|
aria-label="图片资源栏"
|
|
>
|
|
<div className="image-canvas-editor__sidebar-header">
|
|
<div className="min-w-0">
|
|
<h2 className="image-canvas-editor__sidebar-title">
|
|
{activeSidebarPanel === 'assets' ? '素材' : '图层'}
|
|
</h2>
|
|
<div className="image-canvas-editor__sidebar-count">
|
|
{activeSidebarPanel === 'assets'
|
|
? filteredAssetsCount
|
|
: filteredLayers.length}
|
|
</div>
|
|
</div>
|
|
{activeSidebarPanel === 'assets' ? (
|
|
<div className="image-canvas-editor__sidebar-header-actions">
|
|
<EditorIconButton
|
|
className="image-canvas-editor__icon-button"
|
|
label="素材选择模式"
|
|
title="选择"
|
|
icon={isAssetSelectionMode ? CheckSquare : Square}
|
|
pressed={isAssetSelectionMode}
|
|
onClick={
|
|
isAssetSelectionMode
|
|
? closeAssetSelectionMode
|
|
: openAssetSelectionMode
|
|
}
|
|
/>
|
|
<EditorIconButton
|
|
className="image-canvas-editor__icon-button"
|
|
label="新建素材文件夹"
|
|
title="新建文件夹"
|
|
icon={FolderPlus}
|
|
onClick={() => setCreatingFolder(true)}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<EditorIconButton
|
|
className="image-canvas-editor__icon-button"
|
|
label="图层打组"
|
|
title="打组"
|
|
icon={FolderPlus}
|
|
disabled={!selectedLayerId && selectedLayerIds.length === 0}
|
|
onClick={groupSelectedLayers}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
<div className="image-canvas-editor__sidebar-search">
|
|
<PlatformTextField
|
|
aria-label={
|
|
activeSidebarPanel === 'assets' ? '搜索素材' : '搜索画布图层'
|
|
}
|
|
placeholder={
|
|
activeSidebarPanel === 'assets' ? '搜索素材' : '搜索图层'
|
|
}
|
|
value={
|
|
activeSidebarPanel === 'assets'
|
|
? assetSearchQuery
|
|
: layerSearchQuery
|
|
}
|
|
size="xs"
|
|
density="compact"
|
|
className="image-canvas-editor__sidebar-search-input"
|
|
onChange={(event) => {
|
|
if (activeSidebarPanel === 'assets') {
|
|
setAssetSearchQuery(event.target.value);
|
|
} else {
|
|
setLayerSearchQuery(event.target.value);
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{activeSidebarPanel === 'assets' ? (
|
|
<ImageCanvasAssetLibraryPanelView
|
|
assetListRef={assetListRef}
|
|
assetPointerDragRef={assetPointerDragRef}
|
|
suppressAssetClickRef={suppressAssetClickRef}
|
|
groupedAssets={filteredGroupedAssets}
|
|
assetFolders={assetFolders}
|
|
isAssetSelectionMode={isAssetSelectionMode}
|
|
selectedAssetIds={selectedAssetIds}
|
|
visibleAssetIds={visibleAssetIds}
|
|
assetMoveDropFolderId={assetMoveDropFolderId}
|
|
pinnedAssetMoveFolderId={pinnedAssetMoveFolderId}
|
|
creatingFolder={creatingFolder}
|
|
newFolderName={newFolderName}
|
|
renamingFolder={renamingFolder}
|
|
renamingAsset={renamingAsset}
|
|
allVisibleAssetsSelected={allVisibleAssetsSelected}
|
|
isExportingSelectedAssets={isExportingSelectedAssets}
|
|
assetMarquee={assetMarquee}
|
|
setCreatingFolder={setCreatingFolder}
|
|
setNewFolderName={setNewFolderName}
|
|
setRenamingFolder={setRenamingFolder}
|
|
setRenamingAsset={setRenamingAsset}
|
|
setActiveUploadFolderId={setActiveUploadFolderId}
|
|
setUploadDropTarget={setUploadDropTarget}
|
|
setAssetPointerDrag={setAssetPointerDrag}
|
|
onAssetMarqueePointerDown={onAssetMarqueePointerDown}
|
|
onAssetMarqueePointerMove={onAssetMarqueePointerMove}
|
|
onAssetMarqueePointerUp={onAssetMarqueePointerUp}
|
|
onAssetMarqueePointerCancel={onAssetMarqueePointerCancel}
|
|
onAssetMarqueeLostPointerCapture={onAssetMarqueeLostPointerCapture}
|
|
updateAssetMoveDropFolder={updateAssetMoveDropFolder}
|
|
addUploadedFiles={addUploadedFiles}
|
|
requestUpload={requestUpload}
|
|
moveAssetToFolder={moveAssetToFolder}
|
|
commitNewAssetFolder={commitNewAssetFolder}
|
|
toggleAssetFolder={toggleAssetFolder}
|
|
startRenamingFolder={startRenamingFolder}
|
|
commitFolderRename={commitFolderRename}
|
|
deleteAssetFolder={deleteAssetFolder}
|
|
startRenamingAsset={startRenamingAsset}
|
|
commitAssetRename={commitAssetRename}
|
|
deleteUploadedAsset={deleteUploadedAsset}
|
|
submitAssetToShowcase={submitAssetToShowcase}
|
|
toggleAssetSelected={toggleAssetSelected}
|
|
addAssetLayer={addAssetLayer}
|
|
onDownloadAsset={onDownloadAsset}
|
|
toggleAllAssetsSelected={toggleAllAssetsSelected}
|
|
downloadSelectedAssets={downloadSelectedAssets}
|
|
deleteSelectedAssets={deleteSelectedAssets}
|
|
closeAssetSelectionMode={closeAssetSelectionMode}
|
|
/>
|
|
) : (
|
|
<ImageCanvasLayerPanelView
|
|
layers={filteredLayers}
|
|
selectedLayerId={selectedLayerId}
|
|
selectedLayerIds={selectedLayerIds}
|
|
setImageContextMenu={setImageContextMenu}
|
|
setContextMenu={setContextMenu}
|
|
selectSingleLayer={selectSingleLayer}
|
|
resolveContextMenuPosition={resolveContextMenuPosition}
|
|
getCanvasPointFromClient={getCanvasPointFromClient}
|
|
/>
|
|
)}
|
|
</aside>
|
|
);
|
|
}
|