import { ChevronDown, ImageIcon } from 'lucide-react'; import { type CSSProperties, type Dispatch, type ReactNode, type RefObject, type SetStateAction, } from 'react'; import { createPortal } from 'react-dom'; import { PlatformFloatingMenu, PlatformFloatingMenuItem, } from '../common/PlatformFloatingMenu'; import { PlatformActionButton } from '../common/PlatformActionButton'; import { PlatformIconButton } from '../common/PlatformIconButton'; import { PlatformInlineOptionButton } from '../common/PlatformInlineOptionButton'; import { PlatformTextField } from '../common/PlatformTextField'; import { ImageCanvasBasicGenerationComposerView } from './ImageCanvasBasicGenerationComposerView'; import { ImageCanvasCharacterAnimationPanelView } from './ImageCanvasCharacterAnimationPanelView'; import { ImageCanvasCharacterGenerationComposerView } from './ImageCanvasCharacterGenerationComposerView'; import { ImageCanvasEditGenerationModalView } from './ImageCanvasEditGenerationModalView'; import { ImageCanvasIconSpritesheetComposerView } from './ImageCanvasIconSpritesheetComposerView'; import { ImageCanvasQuickEditPanelView } from './ImageCanvasQuickEditPanelView'; import { ImageCanvasSpecGenerationPanelView } from './ImageCanvasSpecGenerationPanelView'; import { EDITOR_VIDEO_MODEL_OPTIONS, SPEC_TYPE_LABEL, calculateEditorVideoPrice, } from './ImageCanvasGenerationModel'; import type { CharacterAnimationPanelState, CanvasLayer, GenerateDialogState, QuickEditPanelState, SpecFormValues, SpecGenerationType, UploadTarget, } from './ImageCanvasEditorTypes'; type ImageCanvasGenerationComposerViewProps = { specToolWrapRef: RefObject; characterSpecButtonRef: RefObject; characterReferenceButtonRef: RefObject; generationReferenceButtonRef: RefObject; iconSpecButtonRef: RefObject; isSpecMenuOpen: boolean; isGenerationReferenceMenuOpen: boolean; isCharacterSpecMenuOpen: boolean; isCharacterReferenceMenuOpen: boolean; isIconSpecMenuOpen: boolean; isUiDesignSpecMenuOpen: boolean; isPickingGenerationReferenceFromCanvas: boolean; isPickingCharacterSpecFromCanvas: boolean; isPickingCharacterReferenceFromCanvas: boolean; isPickingIconSpecFromCanvas: boolean; isPickingUiDesignSpecFromCanvas: boolean; generateDialog: GenerateDialogState | null; generationComposerStyle: CSSProperties | null; iconComposerStyle: CSSProperties | null; quickEditPanel: QuickEditPanelState | null; quickEditSourceLayer: CanvasLayer | null; quickEditPanelStyle: CSSProperties | null; quickEditSizeOptions: string[]; quickEditModelOptions: Array<{ label: string; value: string }>; characterAnimationPanel: CharacterAnimationPanelState | null; characterAnimationSourceLayer: CanvasLayer | null; characterAnimationPanelStyle: CSSProperties | null; characterAnimationPrice: number; setGenerateDialog: Dispatch>; setQuickEditPanel: Dispatch>; setCharacterAnimationPanel: Dispatch< SetStateAction >; setIsGenerationReferenceMenuOpen: Dispatch>; setIsCharacterSpecMenuOpen: Dispatch>; setIsCharacterReferenceMenuOpen: Dispatch>; setIsIconSpecMenuOpen: Dispatch>; setIsUiDesignSpecMenuOpen: Dispatch>; setIsPickingGenerationReferenceFromCanvas: Dispatch>; setIsPickingCharacterSpecFromCanvas: Dispatch>; setIsPickingCharacterReferenceFromCanvas: Dispatch>; setIsPickingIconSpecFromCanvas: Dispatch>; setIsPickingUiDesignSpecFromCanvas: Dispatch>; onOpenSpecDialog: (specType: SpecGenerationType) => void; onRequestUpload: (target: UploadTarget) => void; onSubmitImageGeneration: (dialog: GenerateDialogState) => void; onSubmitIconSpritesheetGeneration: (dialog: GenerateDialogState) => void; onSubmitQuickEdit: () => void; onSubmitCharacterAnimation: () => void; onCloseGenerateComposer: () => void; onUpdateSpecFormValue: (key: keyof SpecFormValues, value: string) => void; onUpdateIconDescription: (index: number, value: string) => void; onAddIconDescription: () => void; onUpdateCharacterAnimationDuration: (frameCountValue: string) => void; onRememberImageModel: (model: string) => void; }; function buildPortalMenuStyle( anchor: HTMLElement | null, placement: 'above' | 'below', ): CSSProperties { const rect = anchor?.getBoundingClientRect(); if (!rect) { return { position: 'fixed', left: 0, top: 0, right: 'auto', bottom: 'auto', zIndex: 70, }; } return { position: 'fixed', left: Math.round(rect.left), top: placement === 'above' ? Math.round(rect.top) : Math.round(rect.bottom + 8), right: 'auto', bottom: 'auto', zIndex: 70, transform: placement === 'above' ? 'translateY(calc(-100% - 0.45rem))' : undefined, }; } function renderEditorPortal(node: ReactNode) { if (typeof document === 'undefined') { return node; } return createPortal(node, document.body); } function ImageCanvasVideoGenerationComposerView({ dialog, style, generationReferenceButtonRef, isGenerationReferenceMenuOpen, setGenerateDialog, setIsGenerationReferenceMenuOpen, setIsPickingGenerationReferenceFromCanvas, onRequestUpload, onSubmit, }: { dialog: GenerateDialogState; style: CSSProperties; generationReferenceButtonRef: RefObject; isGenerationReferenceMenuOpen: boolean; setGenerateDialog: Dispatch>; setIsGenerationReferenceMenuOpen: Dispatch>; setIsPickingGenerationReferenceFromCanvas: Dispatch>; onRequestUpload: (target: UploadTarget) => void; onSubmit: (dialog: GenerateDialogState) => void; }) { const resolution = dialog.videoResolution ?? '480p'; const durationSeconds = dialog.videoDurationSeconds ?? 4; const currentModel = EDITOR_VIDEO_MODEL_OPTIONS.find((item) => item.value === dialog.videoModel) ?? EDITOR_VIDEO_MODEL_OPTIONS[0]; const price = calculateEditorVideoPrice(resolution, durationSeconds); return ( <>
event.stopPropagation()} onSubmit={(event) => { event.preventDefault(); if (dialog.status !== 'generating') { onSubmit(dialog); } }} > setIsGenerationReferenceMenuOpen((open) => !open)} icon={} > 参考图 setGenerateDialog((currentDialog) => currentDialog ? { ...currentDialog, prompt: event.target.value, status: currentDialog.status === 'failed' ? 'idle' : currentDialog.status, errorMessage: currentDialog.status === 'failed' ? undefined : currentDialog.errorMessage, } : currentDialog, ) } />
} onClick={() => { setGenerateDialog((currentDialog) => currentDialog?.mode === 'video' ? { ...currentDialog, videoDurationSeconds: currentDialog.videoDurationSeconds === 5 ? 4 : 5, status: currentDialog.status === 'failed' ? 'idle' : currentDialog.status, errorMessage: currentDialog.status === 'failed' ? undefined : currentDialog.errorMessage, } : currentDialog, ); }} > 16:9 · {resolution} · {durationSeconds}秒 } onClick={() => { setGenerateDialog((currentDialog) => { if (currentDialog?.mode !== 'video') { return currentDialog; } const currentIndex = EDITOR_VIDEO_MODEL_OPTIONS.findIndex( (item) => item.value === currentDialog.videoModel, ); const nextModel = EDITOR_VIDEO_MODEL_OPTIONS[ (Math.max(currentIndex, 0) + 1) % EDITOR_VIDEO_MODEL_OPTIONS.length ] ?? EDITOR_VIDEO_MODEL_OPTIONS[0]; if (!nextModel) { return currentDialog; } return { ...currentDialog, videoModel: nextModel.value, status: currentDialog.status === 'failed' ? 'idle' : currentDialog.status, errorMessage: currentDialog.status === 'failed' ? undefined : currentDialog.errorMessage, }; }); }} > {currentModel.label} } onClick={() => { setGenerateDialog((currentDialog) => currentDialog?.mode === 'video' ? { ...currentDialog, videoResolution: currentDialog.videoResolution === '720p' ? '480p' : '720p', status: currentDialog.status === 'failed' ? 'idle' : currentDialog.status, errorMessage: currentDialog.status === 'failed' ? undefined : currentDialog.errorMessage, } : currentDialog, ); }} > {resolution} {dialog.status === 'generating' ? '生成中' : `${price}`}
{isGenerationReferenceMenuOpen ? renderEditorPortal( { setIsGenerationReferenceMenuOpen(false); setIsPickingGenerationReferenceFromCanvas(true); }} > 从画布中选择 { setIsGenerationReferenceMenuOpen(false); setIsPickingGenerationReferenceFromCanvas(false); onRequestUpload('generation-reference'); }} > 上传图片 , ) : null} ); } export function ImageCanvasGenerationComposerView({ specToolWrapRef, characterSpecButtonRef, characterReferenceButtonRef, generationReferenceButtonRef, iconSpecButtonRef, isSpecMenuOpen, isGenerationReferenceMenuOpen, isCharacterSpecMenuOpen, isCharacterReferenceMenuOpen, isIconSpecMenuOpen, isUiDesignSpecMenuOpen, isPickingGenerationReferenceFromCanvas, isPickingCharacterSpecFromCanvas, isPickingCharacterReferenceFromCanvas, isPickingIconSpecFromCanvas, isPickingUiDesignSpecFromCanvas, generateDialog, generationComposerStyle, iconComposerStyle, quickEditPanel, quickEditSourceLayer, quickEditPanelStyle, quickEditSizeOptions, quickEditModelOptions, characterAnimationPanel, characterAnimationSourceLayer, characterAnimationPanelStyle, characterAnimationPrice, setGenerateDialog, setQuickEditPanel, setCharacterAnimationPanel, setIsGenerationReferenceMenuOpen, setIsCharacterSpecMenuOpen, setIsCharacterReferenceMenuOpen, setIsIconSpecMenuOpen, setIsUiDesignSpecMenuOpen, setIsPickingGenerationReferenceFromCanvas, setIsPickingCharacterSpecFromCanvas, setIsPickingCharacterReferenceFromCanvas, setIsPickingIconSpecFromCanvas, setIsPickingUiDesignSpecFromCanvas, onOpenSpecDialog, onRequestUpload, onSubmitImageGeneration, onSubmitIconSpritesheetGeneration, onSubmitQuickEdit, onSubmitCharacterAnimation, onCloseGenerateComposer, onUpdateSpecFormValue, onUpdateIconDescription, onAddIconDescription, onUpdateCharacterAnimationDuration, onRememberImageModel, }: ImageCanvasGenerationComposerViewProps) { return ( <> {isSpecMenuOpen ? renderEditorPortal( {(['character', 'ui', 'custom'] as const).map((specType) => ( onOpenSpecDialog(specType)} > {SPEC_TYPE_LABEL[specType]} ))} , ) : null} {generateDialog?.mode === 'generate' && generateDialog.composerOpen !== false && generationComposerStyle ? ( setIsGenerationReferenceMenuOpen((open) => !open) } onSubmit={onSubmitImageGeneration} onClose={onCloseGenerateComposer} /> ) : null} {generateDialog?.mode === 'spec' && generateDialog.composerOpen !== false && generationComposerStyle ? ( ) : null} {generateDialog?.mode === 'ui-design' && generateDialog.composerOpen !== false && generationComposerStyle ? ( ) : null} {generateDialog?.mode === 'video' && generateDialog.composerOpen !== false && generationComposerStyle ? ( ) : null} {generateDialog?.mode === 'character' && generationComposerStyle ? ( ) : null} {generateDialog?.mode === 'icon' && generateDialog.composerOpen !== false && iconComposerStyle ? ( ) : null} {isPickingCharacterSpecFromCanvas ? (
请选择画布中的图片作为角色形象规范,按 Esc 退出
) : null} {isPickingCharacterReferenceFromCanvas ? (
请选择画布中的图片作为常规参考图,按 Esc 退出
) : null} {isPickingGenerationReferenceFromCanvas ? (
请选择画布中的图片作为参考图,按 Esc 退出
) : null} {isPickingIconSpecFromCanvas ? (
请选择画布中的图标素材规范,按 Esc 退出
) : null} {isPickingUiDesignSpecFromCanvas ? (
请选择画布中的图标素材规范,按 Esc 退出
) : null} {quickEditPanel && quickEditPanel.status !== 'generating' && quickEditSourceLayer && quickEditPanelStyle ? ( ) : null} {characterAnimationPanel && characterAnimationSourceLayer && characterAnimationPanelStyle ? ( ) : null} ); }