260 lines
9.4 KiB
TypeScript
260 lines
9.4 KiB
TypeScript
import { ImageIcon, ImagePlus } from 'lucide-react';
|
|
import {
|
|
type CSSProperties,
|
|
type Dispatch,
|
|
type ReactNode,
|
|
type RefObject,
|
|
type SetStateAction,
|
|
} from 'react';
|
|
|
|
import { EDITOR_ICON_DESCRIPTION_MAX_CHARS } from '../../services/image-editor/editorProjectClient';
|
|
import { AutoGrowTextArea } from '../common/AutoGrowTextArea';
|
|
import {
|
|
PlatformFloatingMenu,
|
|
PlatformFloatingMenuItem,
|
|
} from '../common/PlatformFloatingMenu';
|
|
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
|
import type {
|
|
GenerateDialogState,
|
|
SpecGenerationType,
|
|
UploadTarget,
|
|
} from './ImageCanvasEditorTypes';
|
|
import { ImageCanvasGenerationImageOptionsView } from './ImageCanvasGenerationImageOptionsView';
|
|
import { calculateEditorIconSpritesheetPrice } from './ImageCanvasGenerationModel';
|
|
import { ImageCanvasReferenceSlot } from './ImageCanvasReferenceSlot';
|
|
import { useImageCanvasFloatingOptionDismiss } from './useImageCanvasFloatingOptionDismiss';
|
|
|
|
function limitIconDescriptionText(value: string) {
|
|
return Array.from(value).slice(0, EDITOR_ICON_DESCRIPTION_MAX_CHARS).join('');
|
|
}
|
|
|
|
type ImageCanvasIconSpritesheetComposerViewProps = {
|
|
dialog: GenerateDialogState;
|
|
style: CSSProperties;
|
|
iconSpecButtonRef: RefObject<HTMLButtonElement | null>;
|
|
isIconSpecMenuOpen: boolean;
|
|
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
|
|
setIsIconSpecMenuOpen: Dispatch<SetStateAction<boolean>>;
|
|
setIsPickingIconSpecFromCanvas: Dispatch<SetStateAction<boolean>>;
|
|
renderEditorPortal: (node: ReactNode) => ReactNode;
|
|
buildPortalMenuStyle: (
|
|
anchor: HTMLElement | null,
|
|
placement: 'above' | 'below',
|
|
) => CSSProperties;
|
|
onOpenSpecDialog: (specType: SpecGenerationType) => void;
|
|
onRequestUpload: (target: UploadTarget) => void;
|
|
onUpdateIconDescriptionText: (value: string) => void;
|
|
onRememberImageModel: (model: string) => void;
|
|
hasPendingImageReferenceUploads?: boolean;
|
|
onSubmit: (dialog: GenerateDialogState) => void;
|
|
};
|
|
|
|
export function ImageCanvasIconSpritesheetComposerView({
|
|
dialog,
|
|
style,
|
|
iconSpecButtonRef,
|
|
isIconSpecMenuOpen,
|
|
setGenerateDialog,
|
|
setIsIconSpecMenuOpen,
|
|
setIsPickingIconSpecFromCanvas,
|
|
renderEditorPortal,
|
|
buildPortalMenuStyle,
|
|
onOpenSpecDialog,
|
|
onRequestUpload,
|
|
onUpdateIconDescriptionText,
|
|
onRememberImageModel,
|
|
hasPendingImageReferenceUploads = false,
|
|
onSubmit,
|
|
}: ImageCanvasIconSpritesheetComposerViewProps) {
|
|
const descriptionText =
|
|
dialog.prompt || (dialog.iconDescriptions ?? []).join('\n');
|
|
useImageCanvasFloatingOptionDismiss({
|
|
isOpen: isIconSpecMenuOpen,
|
|
boundaryRefs: [iconSpecButtonRef],
|
|
onDismiss: () => setIsIconSpecMenuOpen(false),
|
|
});
|
|
|
|
const pickIconSpecFromCanvas = () => {
|
|
setIsPickingIconSpecFromCanvas(true);
|
|
setIsIconSpecMenuOpen(false);
|
|
};
|
|
|
|
const openIconSpecDialog = () => {
|
|
setIsIconSpecMenuOpen(false);
|
|
onOpenSpecDialog('icon');
|
|
};
|
|
|
|
const requestIconSpecUpload = () => {
|
|
setIsIconSpecMenuOpen(false);
|
|
onRequestUpload('icon-spec');
|
|
};
|
|
const references = dialog.generationReferences ?? [];
|
|
|
|
return (
|
|
<div
|
|
className="image-canvas-editor__icon-composer"
|
|
style={style}
|
|
role="dialog"
|
|
aria-label="生成图标素材"
|
|
onPointerDown={(event) => event.stopPropagation()}
|
|
>
|
|
<div className="image-canvas-editor__reference-strip">
|
|
<ImageCanvasReferenceSlot
|
|
buttonRef={iconSpecButtonRef}
|
|
tone="icon"
|
|
className="image-canvas-editor__icon-spec-card"
|
|
icon={<ImageIcon className="h-4 w-4" aria-hidden="true" />}
|
|
imageSrc={dialog.iconSpecReference?.src}
|
|
objectKey={dialog.iconSpecReference?.objectKey}
|
|
label="图标规范"
|
|
ariaLabel={dialog.iconSpecReference?.label ?? '图标规范'}
|
|
title={dialog.iconSpecReference?.label ?? '图标规范'}
|
|
disabled={dialog.status === 'generating'}
|
|
isAdd={!dialog.iconSpecReference}
|
|
onClick={() => setIsIconSpecMenuOpen((open) => !open)}
|
|
onRemove={
|
|
dialog.iconSpecReference
|
|
? () =>
|
|
setGenerateDialog((currentDialog) =>
|
|
currentDialog?.mode === 'icon'
|
|
? {
|
|
...currentDialog,
|
|
status:
|
|
currentDialog.status === 'failed'
|
|
? 'idle'
|
|
: currentDialog.status,
|
|
errorMessage:
|
|
currentDialog.status === 'failed'
|
|
? undefined
|
|
: currentDialog.errorMessage,
|
|
iconSpecReference: null,
|
|
}
|
|
: currentDialog,
|
|
)
|
|
: undefined
|
|
}
|
|
removeLabel="删除图标规范"
|
|
/>
|
|
{isIconSpecMenuOpen
|
|
? renderEditorPortal(
|
|
<PlatformFloatingMenu
|
|
className="image-canvas-editor__character-spec-menu image-canvas-editor__portal-menu"
|
|
label="图标规范来源"
|
|
placement="top-start"
|
|
style={buildPortalMenuStyle(iconSpecButtonRef.current, 'above')}
|
|
>
|
|
<PlatformFloatingMenuItem
|
|
className="image-canvas-editor__context-menu-item"
|
|
onClick={pickIconSpecFromCanvas}
|
|
>
|
|
从画布中选择
|
|
</PlatformFloatingMenuItem>
|
|
<PlatformFloatingMenuItem
|
|
className="image-canvas-editor__context-menu-item"
|
|
onClick={openIconSpecDialog}
|
|
>
|
|
新建图标规范
|
|
</PlatformFloatingMenuItem>
|
|
<PlatformFloatingMenuItem
|
|
className="image-canvas-editor__context-menu-item"
|
|
onClick={requestIconSpecUpload}
|
|
>
|
|
上传图片
|
|
</PlatformFloatingMenuItem>
|
|
</PlatformFloatingMenu>,
|
|
)
|
|
: null}
|
|
{references.map((reference, index) => (
|
|
<ImageCanvasReferenceSlot
|
|
key={reference.id}
|
|
tone="default"
|
|
icon={<ImageIcon className="h-4 w-4" aria-hidden="true" />}
|
|
imageSrc={reference.src}
|
|
objectKey={reference.objectKey}
|
|
label={`参考图${index + 1}`}
|
|
ariaLabel={reference.label || `参考图${index + 1}`}
|
|
title={reference.label}
|
|
disabled={dialog.status === 'generating'}
|
|
onRemove={() =>
|
|
setGenerateDialog((currentDialog) =>
|
|
currentDialog?.mode === 'icon'
|
|
? {
|
|
...currentDialog,
|
|
status:
|
|
currentDialog.status === 'failed'
|
|
? 'idle'
|
|
: currentDialog.status,
|
|
errorMessage:
|
|
currentDialog.status === 'failed'
|
|
? undefined
|
|
: currentDialog.errorMessage,
|
|
generationReferences: (
|
|
currentDialog.generationReferences ?? []
|
|
).filter((item) => item.id !== reference.id),
|
|
}
|
|
: currentDialog,
|
|
)
|
|
}
|
|
removeLabel={`删除${reference.label || `参考图${index + 1}`}`}
|
|
/>
|
|
))}
|
|
<ImageCanvasReferenceSlot
|
|
tone="default"
|
|
icon={<ImagePlus className="h-4 w-4" aria-hidden="true" />}
|
|
label="参考图"
|
|
ariaLabel="上传图标素材参考图"
|
|
disabled={dialog.status === 'generating'}
|
|
isAdd
|
|
onClick={() => onRequestUpload('generation-reference')}
|
|
/>
|
|
</div>
|
|
<div className="image-canvas-editor__field-block image-canvas-editor__field-block--single">
|
|
<AutoGrowTextArea
|
|
aria-label="素材描述"
|
|
value={descriptionText}
|
|
disabled={dialog.status === 'generating'}
|
|
placeholder="你需要哪些图标?"
|
|
className="image-canvas-editor__generation-prompt"
|
|
onValueChange={(value) =>
|
|
onUpdateIconDescriptionText(limitIconDescriptionText(value))
|
|
}
|
|
/>
|
|
</div>
|
|
{dialog.status === 'failed' ? (
|
|
<PlatformStatusMessage
|
|
tone="error"
|
|
surface="platform"
|
|
size="xs"
|
|
className="image-canvas-editor__generate-status"
|
|
role="alert"
|
|
>
|
|
{dialog.errorMessage}
|
|
</PlatformStatusMessage>
|
|
) : null}
|
|
<div className="image-canvas-editor__generation-composer-footer">
|
|
<ImageCanvasGenerationImageOptionsView
|
|
dialog={dialog}
|
|
setGenerateDialog={setGenerateDialog}
|
|
includeDimensions
|
|
onRememberImageModel={onRememberImageModel}
|
|
hasPendingImageReferenceUploads={hasPendingImageReferenceUploads}
|
|
optionLabelPrefix="生成图片"
|
|
cost={calculateEditorIconSpritesheetPrice(
|
|
dialog.imageModel,
|
|
dialog.imageSize,
|
|
)}
|
|
submitLabel="生成"
|
|
submitAriaLabel="生成"
|
|
onSubmitRequest={() => {
|
|
if (dialog.status !== 'generating') {
|
|
onSubmit({ ...dialog, prompt: descriptionText });
|
|
}
|
|
}}
|
|
renderEditorPortal={renderEditorPortal}
|
|
buildPortalMenuStyle={buildPortalMenuStyle}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|