import * as React from "react"; import { FileDrop, type ControlChangeMeta, type FileDropPresentationItem, } from "@repo/ui"; import type { ToolcraftCanvasSize, ToolcraftControlSchema } from "../../../schema/types"; import type { ToolcraftSourceAssetCoordinator } from "../../../source-assets/source-asset-coordinator"; import type { ToolcraftCommand, ToolcraftImageAsset, ToolcraftMediaAsset, ToolcraftMediaTransformOperation, } from "../../../state/types"; import { useToolcraftMediaPresentationUrls } from "../../app-shell/toolcraft-source-asset-context"; import { useToolcraftSelector } from "../../app-shell/use-toolcraft"; import { ControlsPanelFileDropItemControls } from "./controls-panel-file-drop-item-controls"; export type FileDropControlRenderArgs = { canvasSize: ToolcraftCanvasSize; control: ToolcraftControlSchema; dispatchCommand: (command: ToolcraftCommand) => void; id: string; layerSelectionEnabled?: boolean; mediaAssets: readonly ToolcraftMediaAsset[]; name: string; setControlValue?: ( target: string, value: unknown, label?: string, meta?: ControlChangeMeta, ) => void; sourceAssetCoordinator: ToolcraftSourceAssetCoordinator; value?: unknown; }; const IMAGE_TRANSFORM_OPERATIONS = new Set([ "flip-horizontal", "flip-vertical", "rotate-left", "rotate-right", ]); const MODEL_REPAIR_ACTION = "model.fix"; function isImageTransformOperation( value: string, ): value is ToolcraftMediaTransformOperation { return IMAGE_TRANSFORM_OPERATIONS.has(value as ToolcraftMediaTransformOperation); } function FileDropControlRenderer({ control, dispatchCommand, id, layerSelectedImageAsset = null, mediaAssets, name, setControlValue = () => undefined, sourceAssetCoordinator, suppressMediaCollection = false, value, }: Omit< FileDropControlRenderArgs, "canvasSize" | "layerSelectionEnabled" > & { layerSelectedImageAsset?: ToolcraftImageAsset | null; suppressMediaCollection?: boolean; }): React.JSX.Element | null { const assetKind = control.assetKind === "file" ? "file" : control.assetKind === "model" ? "model" : "image"; const getOperation = React.useCallback( () => sourceAssetCoordinator.getOperation(control.target), [control.target, sourceAssetCoordinator], ); const operation = React.useSyncExternalStore( sourceAssetCoordinator.subscribe, getOperation, getOperation, ); const previewUrls = useToolcraftMediaPresentationUrls(mediaAssets); const [selectedMediaId, setSelectedMediaId] = React.useState(null); const unresolvedPresentation = sourceAssetCoordinator.getPresentation( control, mediaAssets, operation, ); const basePresentation = unresolvedPresentation ? { ...unresolvedPresentation, items: unresolvedPresentation.items.map((item) => { const previewSrc = previewUrls.get(item.id); return previewSrc ? { ...item, previewSrc } : item; }), } : null; const selectedLayerPresentation = layerSelectedImageAsset ? sourceAssetCoordinator.getPresentation( control, [layerSelectedImageAsset], operation, ) : null; const previewMediaIds = basePresentation?.items.map((item) => item.id) ?? []; const activeImageId = assetKind === "image" ? suppressMediaCollection ? layerSelectedImageAsset?.id ?? null : basePresentation?.items.length === 1 ? basePresentation.items[0]?.id ?? null : selectedMediaId : null; const activeModelId = assetKind === "model" ? basePresentation?.items[0]?.id ?? null : null; const presentation = basePresentation ? suppressMediaCollection ? { ...basePresentation, items: [], secondaryActions: selectedLayerPresentation?.secondaryActions ?? [], } : assetKind === "image" && !activeImageId ? { ...basePresentation, secondaryActions: [] } : basePresentation : null; const usesCollectionActions = control.variant === "collection-actions"; const canAddCollectionItem = typeof control.hardMaxItems !== "number" || (basePresentation?.items.length ?? 0) < control.hardMaxItems; React.useEffect(() => { if (selectedMediaId && !previewMediaIds.includes(selectedMediaId)) { setSelectedMediaId(null); } }, [previewMediaIds, selectedMediaId]); if (!basePresentation || !presentation) { return null; } const reorderItems = (orderedIds: readonly string[]): void => { if (orderedIds.length !== previewMediaIds.length) { return; } const previewIdSet = new Set(previewMediaIds); let orderedPreviewIndex = 0; const mediaIds = mediaAssets.map((asset) => { if (!previewIdSet.has(asset.id)) { return asset.id; } const nextId = orderedIds[orderedPreviewIndex]; orderedPreviewIndex += 1; return nextId ?? asset.id; }); dispatchCommand({ mediaIds, type: "media.reorder" }); }; const importFiles = (files: File[]): void => { void sourceAssetCoordinator.importBatch( { files, origin: "panel", target: control.target, }, control, ); }; return ( ( ), } : {}), } : undefined } key={id} onAction={ activeImageId ? (value) => { if (!isImageTransformOperation(value)) { return; } dispatchCommand({ mediaId: activeImageId, operation: value, type: "media.transform", }); } : activeModelId ? (value) => { if (value !== MODEL_REPAIR_ACTION) return; void sourceAssetCoordinator.repairModel(activeModelId); } : undefined } onFilesSelect={importFiles} onFolderSelect={ presentation.allowsFolderSelection ? importFiles : undefined } onItemRemove={(item) => { dispatchCommand({ mediaId: item.id, type: "media.delete" }); }} onItemSelect={ assetKind === "image" && basePresentation.presenter === "image-grid" ? (item) => setSelectedMediaId(item.id) : undefined } onItemsReorder={ basePresentation.presenter === "file-list" || basePresentation.presenter === "image-grid" ? (items) => reorderItems(items.map((item) => item.id)) : undefined } presentation={presentation} /> ); } function LayerSelectedFileDropControlRenderer( props: Omit< FileDropControlRenderArgs, "canvasSize" | "layerSelectionEnabled" >, ): React.JSX.Element | null { const { control } = props; const selectedImageAsset = useToolcraftSelector( React.useCallback( (state) => state.mediaAssets.find( (asset): asset is ToolcraftImageAsset => asset.assetKind === "image" && asset.layerId === state.selectedLayerId && (asset.sourceTarget === undefined || asset.sourceTarget === control.target), ) ?? null, [control.target], ), ); return ( ); } export function renderFileDropControl({ canvasSize: _canvasSize, control, layerSelectionEnabled = false, ...rendererProps }: FileDropControlRenderArgs): React.ReactNode { return layerSelectionEnabled ? ( ) : ( ); }