feat(canvas): enhance multi-image generation controls and improve image preview behavior
This commit is contained in:
@@ -53,7 +53,7 @@ type CanvasNodeProps = {
|
||||
onDeleteBatchImage?: (nodeId: string, imageId: string) => void;
|
||||
onRetry?: (node: CanvasNodeData) => void;
|
||||
onGenerateImage?: (node: CanvasNodeData) => void;
|
||||
onViewImage?: (node: CanvasNodeData) => void;
|
||||
onViewImage?: (node: CanvasNodeData, imageId?: string) => void;
|
||||
onContextMenu: (event: React.MouseEvent, nodeId: string) => void;
|
||||
};
|
||||
|
||||
@@ -78,6 +78,7 @@ type NodeContentRendererProps = {
|
||||
onDownloadBatchImage?: (imageId: string) => void;
|
||||
onRetryBatchImage?: (imageId: string) => void;
|
||||
onDeleteBatchImage?: (imageId: string) => void;
|
||||
onViewBatchImage?: (imageId: string) => void;
|
||||
groupChildCount: number;
|
||||
};
|
||||
|
||||
@@ -365,11 +366,6 @@ export const CanvasNode = React.memo(function CanvasNode({
|
||||
}}
|
||||
onMouseDown={(event) => onMouseDown(event, data.id)}
|
||||
onDoubleClick={(event) => {
|
||||
if (isBatchRoot) {
|
||||
event.stopPropagation();
|
||||
onToggleBatch?.(data.id);
|
||||
return;
|
||||
}
|
||||
if (definition?.onDoubleClick && pluginContext) {
|
||||
if (definition.onDoubleClick(pluginContext)) event.stopPropagation();
|
||||
return;
|
||||
@@ -414,6 +410,7 @@ export const CanvasNode = React.memo(function CanvasNode({
|
||||
onDownloadBatchImage={(imageId) => onDownloadBatchImage?.(data, imageId)}
|
||||
onRetryBatchImage={(imageId) => onRetryBatchImage?.(data, imageId)}
|
||||
onDeleteBatchImage={(imageId) => onDeleteBatchImage?.(data.id, imageId)}
|
||||
onViewBatchImage={(imageId) => onViewImage?.(data, imageId)}
|
||||
groupChildCount={groupChildCount}
|
||||
/>
|
||||
</div>
|
||||
@@ -590,6 +587,7 @@ function ImageNodeContent(props: NodeContentRendererProps) {
|
||||
onDownloadBatchImage={props.onDownloadBatchImage}
|
||||
onRetryBatchImage={props.onRetryBatchImage}
|
||||
onDeleteBatchImage={props.onDeleteBatchImage}
|
||||
onViewBatchImage={props.onViewBatchImage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -647,6 +645,7 @@ function ImageContent({
|
||||
onDownloadBatchImage,
|
||||
onRetryBatchImage,
|
||||
onDeleteBatchImage,
|
||||
onViewBatchImage,
|
||||
}: {
|
||||
node: CanvasNodeData;
|
||||
batchExpanded: boolean;
|
||||
@@ -656,6 +655,7 @@ function ImageContent({
|
||||
onDownloadBatchImage?: (imageId: string) => void;
|
||||
onRetryBatchImage?: (imageId: string) => void;
|
||||
onDeleteBatchImage?: (imageId: string) => void;
|
||||
onViewBatchImage?: (imageId: string) => void;
|
||||
}) {
|
||||
const theme = canvasThemes[useThemeStore((state) => state.theme)];
|
||||
const { t } = useTranslation();
|
||||
@@ -667,11 +667,11 @@ function ImageContent({
|
||||
const primaryContent = primaryImage?.content || node.metadata?.content;
|
||||
|
||||
return (
|
||||
<BatchFrame batchCount={batchCount} batchExpanded={batchExpanded} onToggleBatch={onToggleBatch}>
|
||||
<BatchFrame batchCount={batchCount} batchExpanded={batchExpanded}>
|
||||
{batchExpanded
|
||||
? images
|
||||
.filter((image) => image.id !== primaryImageId)
|
||||
.map((image, index) => <ExpandedImageCard key={image.id} node={node} image={image} index={index} onSetPrimary={() => onSetBatchPrimary?.(image.id)} onDuplicate={() => onDuplicateBatchImage?.(image.id)} onDownload={() => onDownloadBatchImage?.(image.id)} onRetry={() => onRetryBatchImage?.(image.id)} onDelete={() => onDeleteBatchImage?.(image.id)} />)
|
||||
.map((image, index) => <ExpandedImageCard key={image.id} node={node} image={image} index={index} onView={() => onViewBatchImage?.(image.id)} onSetPrimary={() => onSetBatchPrimary?.(image.id)} onDuplicate={() => onDuplicateBatchImage?.(image.id)} onDownload={() => onDownloadBatchImage?.(image.id)} onRetry={() => onRetryBatchImage?.(image.id)} onDelete={() => onDeleteBatchImage?.(image.id)} />)
|
||||
: null}
|
||||
<div className="h-full w-full overflow-hidden rounded-3xl">
|
||||
{primaryContent ? (
|
||||
@@ -714,7 +714,7 @@ function ImageContent({
|
||||
);
|
||||
}
|
||||
|
||||
function ExpandedImageCard({ node, image, index, onSetPrimary, onDuplicate, onDownload, onRetry, onDelete }: { node: CanvasNodeData; image: CanvasNodeImage; index: number; onSetPrimary: () => void; onDuplicate: () => void; onDownload: () => void; onRetry: () => void; onDelete: () => void }) {
|
||||
function ExpandedImageCard({ node, image, index, onView, onSetPrimary, onDuplicate, onDownload, onRetry, onDelete }: { node: CanvasNodeData; image: CanvasNodeImage; index: number; onView: () => void; onSetPrimary: () => void; onDuplicate: () => void; onDownload: () => void; onRetry: () => void; onDelete: () => void }) {
|
||||
const theme = canvasThemes[useThemeStore((state) => state.theme)];
|
||||
const { t } = useTranslation();
|
||||
const count = node.metadata?.images?.length || 0;
|
||||
@@ -746,7 +746,11 @@ function ExpandedImageCard({ node, image, index, onSetPrimary, onDuplicate, onDo
|
||||
}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onDoubleClick={(event) => event.stopPropagation()}
|
||||
onDoubleClick={(event) => {
|
||||
if (!image.content || (event.target instanceof Element && event.target.closest("button"))) return;
|
||||
event.stopPropagation();
|
||||
onView();
|
||||
}}
|
||||
>
|
||||
{image.content ? <img src={image.content} alt={node.title} draggable={false} className="pointer-events-none h-full w-full select-none object-contain" /> : <ImageSlotStatus image={image} />}
|
||||
{image.content ? (
|
||||
@@ -812,21 +816,11 @@ function ImageInfoBar({ node }: { node: CanvasNodeData }) {
|
||||
);
|
||||
}
|
||||
|
||||
function BatchFrame({ batchCount, batchExpanded, onToggleBatch, children }: { batchCount: number; batchExpanded: boolean; onToggleBatch?: () => void; children: ReactNode }) {
|
||||
function BatchFrame({ batchCount, batchExpanded, children }: { batchCount: number; batchExpanded: boolean; children: ReactNode }) {
|
||||
const theme = canvasThemes[useThemeStore((state) => state.theme)];
|
||||
const isBatchRoot = batchCount > 1;
|
||||
return (
|
||||
<div
|
||||
className="group/batch relative h-full w-full overflow-visible"
|
||||
onDoubleClick={
|
||||
isBatchRoot
|
||||
? (event) => {
|
||||
event.stopPropagation();
|
||||
onToggleBatch?.();
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<div className="group/batch relative h-full w-full overflow-visible">
|
||||
{isBatchRoot ? (
|
||||
<div className="pointer-events-none absolute inset-0 overflow-visible">
|
||||
{Array.from({ length: Math.min(batchCount - 1, 3) }).map((_, index) => (
|
||||
|
||||
@@ -232,6 +232,7 @@ function InfiniteCanvasPage() {
|
||||
const [superResolveNodeId, setSuperResolveNodeId] = useState<string | null>(null);
|
||||
const [angleNodeId, setAngleNodeId] = useState<string | null>(null);
|
||||
const [previewNodeId, setPreviewNodeId] = useState<string | null>(null);
|
||||
const [previewImageId, setPreviewImageId] = useState<string | null>(null);
|
||||
const [titleEditing, setTitleEditing] = useState(false);
|
||||
const [titleDraft, setTitleDraft] = useState("");
|
||||
const [historyState, setHistoryState] = useState({ canUndo: false, canRedo: false });
|
||||
@@ -590,6 +591,7 @@ function InfiniteCanvasPage() {
|
||||
const superResolveNode = superResolveNodeId ? nodeById.get(superResolveNodeId) || null : null;
|
||||
const angleNode = angleNodeId ? nodeById.get(angleNodeId) || null : null;
|
||||
const previewNode = previewNodeId ? nodeById.get(previewNodeId) || null : null;
|
||||
const previewContent = previewImageId ? previewNode?.metadata?.images?.find((image) => image.id === previewImageId)?.content : previewNode?.metadata?.content;
|
||||
const hasMultipleSelectedNodes = selectedNodeIds.size > 1;
|
||||
const activeNodeId = hasMultipleSelectedNodes ? null : hoveredNodeId || (selectedNodeIds.size === 1 ? Array.from(selectedNodeIds)[0] : null);
|
||||
const groupChildCountById = useMemo(() => {
|
||||
@@ -2693,7 +2695,10 @@ function InfiniteCanvasPage() {
|
||||
const handleNodeHoverEnd = useCallback((nodeId: string) => {
|
||||
setHoveredNodeId((current) => (current === nodeId ? null : current));
|
||||
}, []);
|
||||
const handleNodeViewImage = useCallback((node: CanvasNodeData) => setPreviewNodeId(node.id), []);
|
||||
const handleNodeViewImage = useCallback((node: CanvasNodeData, imageId?: string) => {
|
||||
setPreviewNodeId(node.id);
|
||||
setPreviewImageId(imageId || null);
|
||||
}, []);
|
||||
const handleNodeRetry = useCallback((node: CanvasNodeData) => void handleRetryNode(node), [handleRetryNode]);
|
||||
const handleNodeContextMenu = useCallback((event: ReactMouseEvent, nodeId: string) => {
|
||||
event.preventDefault();
|
||||
@@ -2918,7 +2923,7 @@ function InfiniteCanvasPage() {
|
||||
onUpscale={(node) => setUpscaleNodeId(node.id)}
|
||||
onSuperResolve={(node) => setSuperResolveNodeId(node.id)}
|
||||
onAngle={(node) => setAngleNodeId(node.id)}
|
||||
onViewImage={(node) => setPreviewNodeId(node.id)}
|
||||
onViewImage={handleNodeViewImage}
|
||||
onReversePrompt={createImageReversePromptNodes}
|
||||
onRetry={(node) => void handleRetryNode(node)}
|
||||
onToggleFreeResize={(node) => toggleNodeFreeResize(node.id)}
|
||||
@@ -2998,14 +3003,14 @@ function InfiniteCanvasPage() {
|
||||
|
||||
<Modal
|
||||
title={t("canvas.projectPage.imageDetails")}
|
||||
open={Boolean(previewNode?.metadata?.content)}
|
||||
open={Boolean(previewContent)}
|
||||
centered
|
||||
onCancel={() => setPreviewNodeId(null)}
|
||||
footer={null}
|
||||
width="auto"
|
||||
styles={{ body: { padding: 0, display: "flex", justifyContent: "center", alignItems: "center", maxHeight: "80vh" } }}
|
||||
>
|
||||
{previewNode?.metadata?.content ? <img src={previewNode.metadata.content} alt={previewNode.title || t("assets.kinds.image")} style={{ maxWidth: "100%", maxHeight: "80vh", objectFit: "contain" }} /> : null}
|
||||
{previewContent ? <img src={previewContent} alt={previewNode?.title || t("assets.kinds.image")} style={{ maxWidth: "100%", maxHeight: "80vh", objectFit: "contain" }} /> : null}
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
|
||||
Reference in New Issue
Block a user