diff --git a/theatre/studio/src/propEditors/simpleEditors/FilePropEditor.tsx b/theatre/studio/src/propEditors/simpleEditors/FilePropEditor.tsx new file mode 100644 index 0000000..fc45490 --- /dev/null +++ b/theatre/studio/src/propEditors/simpleEditors/FilePropEditor.tsx @@ -0,0 +1,154 @@ +import type {PropTypeConfig_File} from '@theatre/core/propTypes' +import {Package, Trash} from '@theatre/studio/uiComponents/icons' +import React, {useCallback, useEffect} from 'react' +import styled, {css} from 'styled-components' +import type {ISimplePropEditorReactProps} from './ISimplePropEditorReactProps' + +const Container = styled.div<{empty: boolean}>` + display: flex; + align-items: center; + height: 100%; + gap: 4px; +` + +const AddFile = styled.div` + position: absolute; + inset: -5px; + // rotate 45deg + transform: rotate(45deg); + --checker-color: #ededed36; + &:hover { + --checker-color: #ededed77; + } + // checkerboard background with 4px squares + background-image: linear-gradient( + 45deg, + var(--checker-color) 25%, + transparent 25% + ), + linear-gradient(-45deg, var(--checker-color) 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, var(--checker-color) 75%), + linear-gradient(-45deg, transparent 75%, var(--checker-color) 75%); + background-size: 5px 5px; +` + +const InputLabel = styled.label<{empty: boolean}>` + position: relative; + cursor: default; + box-sizing: border-box; + + height: 18px; + aspect-ratio: 1; + display: flex; + justify-content: center; + align-items: center; + font-size: 16px; + + overflow: hidden; + color: #ccc; + &:hover { + color: white; + } + + border-radius: 99999px; + border: 1px solid hwb(220deg 40% 52%); + &:hover { + border-color: hwb(220deg 45% 52%); + } + + ${(props) => (props.empty ? css`` : css``)} +` + +// file input +const Input = styled.input.attrs({type: 'file'})` + display: none; +` + +const DeleteButton = styled.button` + display: flex; + align-items: center; + justify-content: center; + outline: none; + background: transparent; + color: #a8a8a9; + + border: none; + height: 100%; + aspect-ratio: 1/1; + + opacity: 0; + + ${Container}:hover & { + opacity: 0.8; + } + + &:hover { + opacity: 1; + color: white; + } +` + +function FilePropEditor({ + propConfig, + editingTools, + value, + autoFocus, +}: ISimplePropEditorReactProps) { + const [previewUrl, setPreviewUrl] = React.useState() + + useEffect(() => { + if (value) { + setPreviewUrl(editingTools.getAssetUrl(value)) + } else { + setPreviewUrl(undefined) + } + }, [value]) + + const onChange = useCallback( + async (event) => { + const file = event.target.files[0] + editingTools.permanentlySetValue({type: 'file', id: undefined}) + const fileId = await editingTools.createAsset(file) + + if (!fileId) { + editingTools.permanentlySetValue(value) + } else { + editingTools.permanentlySetValue({ + type: 'file', + id: fileId, + }) + } + event.target.value = null + }, + [editingTools, value], + ) + + const empty = !value?.id + + return ( + + + + {previewUrl ? : } + + + {!empty && ( + { + editingTools.permanentlySetValue({type: 'file', id: undefined}) + }} + > + + + )} + + ) +} + +export default FilePropEditor diff --git a/theatre/studio/src/propEditors/simpleEditors/simplePropEditorByPropType.ts b/theatre/studio/src/propEditors/simpleEditors/simplePropEditorByPropType.ts index 5df8ba2..175da3a 100644 --- a/theatre/studio/src/propEditors/simpleEditors/simplePropEditorByPropType.ts +++ b/theatre/studio/src/propEditors/simpleEditors/simplePropEditorByPropType.ts @@ -8,6 +8,7 @@ import RgbaPropEditor from './RgbaPropEditor' import type {ISimplePropEditorReactProps} from './ISimplePropEditorReactProps' import type {PropConfigForType} from '@theatre/studio/propEditors/utils/PropConfigForType' import ImagePropEditor from './ImagePropEditor' +import FilePropEditor from './FilePropEditor' export const simplePropEditorByPropType: ISimplePropEditorByPropType = { number: NumberPropEditor, @@ -16,6 +17,7 @@ export const simplePropEditorByPropType: ISimplePropEditorByPropType = { stringLiteral: StringLiteralPropEditor, rgba: RgbaPropEditor, image: ImagePropEditor, + file: FilePropEditor, } type ISimplePropEditorByPropType = { diff --git a/theatre/studio/src/propEditors/useEditingToolsForSimpleProp.tsx b/theatre/studio/src/propEditors/useEditingToolsForSimpleProp.tsx index 3726c63..93fdeea 100644 --- a/theatre/studio/src/propEditors/useEditingToolsForSimpleProp.tsx +++ b/theatre/studio/src/propEditors/useEditingToolsForSimpleProp.tsx @@ -20,7 +20,7 @@ import type {NearbyKeyframes} from './getNearbyKeyframesOfTrack' import {getNearbyKeyframesOfTrack} from './getNearbyKeyframesOfTrack' import type {NearbyKeyframesControls} from './NextPrevKeyframeCursors' import NextPrevKeyframeCursors from './NextPrevKeyframeCursors' -import type {Asset} from '@theatre/shared/utils/assets' +import type {Asset, File as AssetFile} from '@theatre/shared/utils/assets' interface EditingToolsCommon { value: T @@ -33,7 +33,7 @@ interface EditingToolsCommon { discardTemporaryValue(): void permanentlySetValue(v: T): void - getAssetUrl: (asset: Asset) => string | undefined + getAssetUrl: (asset: Asset | AssetFile) => string | undefined createAsset(asset: File): Promise } @@ -116,7 +116,7 @@ function createPrism( const editAssets = { createAsset: (asset: File): Promise => obj.sheet.project.assetStorage.createAsset(asset), - getAssetUrl: (asset: Asset) => + getAssetUrl: (asset: Asset | AssetFile) => asset.id ? obj.sheet.project.assetStorage.getAssetUrl(asset.id) : undefined, diff --git a/theatre/studio/src/propEditors/utils/IEditingTools.tsx b/theatre/studio/src/propEditors/utils/IEditingTools.tsx index 499752f..d5295de 100644 --- a/theatre/studio/src/propEditors/utils/IEditingTools.tsx +++ b/theatre/studio/src/propEditors/utils/IEditingTools.tsx @@ -1,10 +1,10 @@ -import type {Asset} from '@theatre/shared/utils/assets' +import type {Asset, File} from '@theatre/shared/utils/assets' export interface IEditingTools { temporarilySetValue(v: T): void discardTemporaryValue(): void permanentlySetValue(v: T): void - getAssetUrl(asset: Asset): string | undefined + getAssetUrl(asset: Asset | File): string | undefined createAsset(asset: Blob): Promise }