From b405752d728b2d81069a201703782be4341a401e Mon Sep 17 00:00:00 2001 From: Rezmason Date: Tue, 18 Jun 2024 17:12:56 -0700 Subject: [PATCH 01/10] Changing FileType to just be an alias of FileDataType, which is imported now from Files/container/index.tsx --- .../FileBrowser/FileBrowserContentPanel.tsx | 11 +---------- .../assets/FileBrowser/FilePropertiesPanel.tsx | 2 +- .../components/assets/ImageCompressionPanel.tsx | 2 +- .../src/components/assets/ImageConvertPanel.tsx | 2 +- .../components/assets/ModelCompressionPanel.tsx | 2 +- .../editor/panels/Files/container/index.tsx | 16 +++++----------- 6 files changed, 10 insertions(+), 25 deletions(-) diff --git a/packages/editor/src/components/assets/FileBrowser/FileBrowserContentPanel.tsx b/packages/editor/src/components/assets/FileBrowser/FileBrowserContentPanel.tsx index 3a87646bd7..1dc6fd887a 100644 --- a/packages/editor/src/components/assets/FileBrowser/FileBrowserContentPanel.tsx +++ b/packages/editor/src/components/assets/FileBrowser/FileBrowserContentPanel.tsx @@ -97,16 +97,7 @@ type DnDFileType = { export const FILES_PAGE_LIMIT = 100 -export type FileType = { - fullName: string - isFolder: boolean - key: string - name: string - path: string - size: string - type: string - url: string -} +type FileType = FileDataType const fileConsistsOfContentType = function (file: FileType, contentType: string): boolean { if (file.isFolder) { diff --git a/packages/editor/src/components/assets/FileBrowser/FilePropertiesPanel.tsx b/packages/editor/src/components/assets/FileBrowser/FilePropertiesPanel.tsx index e05c407e3b..3bfaa377f8 100644 --- a/packages/editor/src/components/assets/FileBrowser/FilePropertiesPanel.tsx +++ b/packages/editor/src/components/assets/FileBrowser/FilePropertiesPanel.tsx @@ -34,10 +34,10 @@ import { Engine } from '@etherealengine/ecs/src/Engine' import { getMutableState, NO_PROXY, State, useHookstate } from '@etherealengine/hyperflux' import { useFind } from '@etherealengine/spatial/src/common/functions/FeathersHooks' +import { FileType } from '@etherealengine/ui/src/components/editor/panels/Files/container' import { EditorState } from '../../../services/EditorServices' import { Button } from '../../inputs/Button' import styles from '../styles.module.scss' -import { FileType } from './FileBrowserContentPanel' export const FilePropertiesPanel = (props: { openProperties: State diff --git a/packages/editor/src/components/assets/ImageCompressionPanel.tsx b/packages/editor/src/components/assets/ImageCompressionPanel.tsx index 5c51db9273..06b165fbfa 100644 --- a/packages/editor/src/components/assets/ImageCompressionPanel.tsx +++ b/packages/editor/src/components/assets/ImageCompressionPanel.tsx @@ -47,7 +47,7 @@ import Slider from '@etherealengine/ui/src/primitives/tailwind/Slider' import Text from '@etherealengine/ui/src/primitives/tailwind/Text' import { useTranslation } from 'react-i18next' import { MdClose } from 'react-icons/md' -import { FileType } from './FileBrowser/FileBrowserContentPanel' +import { FileType } from '@etherealengine/ui/src/components/editor/panels/Files/container' const UASTCFlagOptions = [ { label: 'Fastest', value: 0 }, diff --git a/packages/editor/src/components/assets/ImageConvertPanel.tsx b/packages/editor/src/components/assets/ImageConvertPanel.tsx index 0129bcc6a3..096366af13 100644 --- a/packages/editor/src/components/assets/ImageConvertPanel.tsx +++ b/packages/editor/src/components/assets/ImageConvertPanel.tsx @@ -31,11 +31,11 @@ import { State } from '@etherealengine/hyperflux' import { imageConvertPath } from '@etherealengine/common/src/schema.type.module' import { Engine } from '@etherealengine/ecs' +import { FileType } from '@etherealengine/ui/src/components/editor/panels/Files/container' import BooleanInput from '../inputs/BooleanInput' import { Button } from '../inputs/Button' import NumericInput from '../inputs/NumericInput' import SelectInput from '../inputs/SelectInput' -import { FileType } from './FileBrowser/FileBrowserContentPanel' import styles from './styles.module.scss' export default function ImageConvertPanel({ diff --git a/packages/editor/src/components/assets/ModelCompressionPanel.tsx b/packages/editor/src/components/assets/ModelCompressionPanel.tsx index 7112b30a7c..eab54e9d75 100644 --- a/packages/editor/src/components/assets/ModelCompressionPanel.tsx +++ b/packages/editor/src/components/assets/ModelCompressionPanel.tsx @@ -52,11 +52,11 @@ import { } from '@etherealengine/spatial/src/transform/components/EntityTree' import { PopoverState } from '@etherealengine/client-core/src/common/services/PopoverState' +import { FileType } from '@etherealengine/ui/src/components/editor/panels/Files/container' import { useTranslation } from 'react-i18next' import { defaultLODs, LODList, LODVariantDescriptor } from '../../constants/GLTFPresets' import exportGLTF from '../../functions/exportGLTF' import { EditorState } from '../../services/EditorServices' -import { FileType } from './FileBrowser/FileBrowserContentPanel' import ConfirmDialog from '@etherealengine/ui/src/components/tailwind/ConfirmDialog' import Button from '@etherealengine/ui/src/primitives/tailwind/Button' diff --git a/packages/ui/src/components/editor/panels/Files/container/index.tsx b/packages/ui/src/components/editor/panels/Files/container/index.tsx index ac95266dbd..aee739e1cc 100644 --- a/packages/ui/src/components/editor/panels/Files/container/index.tsx +++ b/packages/ui/src/components/editor/panels/Files/container/index.tsx @@ -92,16 +92,7 @@ export type DnDFileType = { export const FILES_PAGE_LIMIT = 100 -export type FileType = { - fullName: string - isFolder: boolean - key: string - name: string - path: string - size: string - type: string - url: string -} +export type FileType = FileDataType function fileConsistsOfContentType(file: FileDataType, contentType: string): boolean { if (file.isFolder) { @@ -168,6 +159,10 @@ const FileBrowserContentPanel: React.FC = (props) const projectName = useValidProjectForFileBrowser(selectedDirectory.value) const orgName = projectName.includes('/') ? projectName.split('/')[0] : '' + const fileProperties = useHookstate(null) + const selectedFileKeys = useHookstate([]) + const anchorEl = useHookstate(null) + const filesViewMode = useMutableState(FilesViewModeState).viewMode const page = useHookstate(0) @@ -435,7 +430,6 @@ const FileBrowserContentPanel: React.FC = (props) drop: (dropItem) => dropItemsOnPanel(dropItem as any), collect: (monitor) => ({ isFileDropOver: monitor.canDrop() && monitor.isOver() }) }) - const selectedFileKeys = useHookstate([]) const isListView = filesViewMode.value === 'list' const staticResourceData = useFind(staticResourcePath, { From 1b461c82597f27ed84db4f8fafd53a61cd7fd92b Mon Sep 17 00:00:00 2001 From: Rezmason Date: Wed, 26 Jun 2024 14:21:41 -0700 Subject: [PATCH 02/10] ImageCompressionPanel, ModelCompressionPanel and FilePropertiesPanel now accept a FileType[] instead of a FileType, which is the files selected in the file browser. --- packages/client-core/i18n/en/editor.json | 6 +- .../FileBrowser/FilePropertiesPanel.tsx | 189 ++++++++------ .../assets/ImageCompressionPanel.tsx | 39 +-- .../assets/ModelCompressionPanel.tsx | 50 +++- .../Files/browserGrid/FilePropertiesModal.tsx | 235 ++++++++++-------- .../editor/panels/Files/browserGrid/index.tsx | 44 +++- .../editor/panels/Files/container/index.tsx | 135 ++++++++-- 7 files changed, 468 insertions(+), 230 deletions(-) diff --git a/packages/client-core/i18n/en/editor.json b/packages/client-core/i18n/en/editor.json index a0acdeb2b1..427b4054fc 100755 --- a/packages/client-core/i18n/en/editor.json +++ b/packages/client-core/i18n/en/editor.json @@ -1199,7 +1199,8 @@ "file": "File", "directory": "Directory", "fileProperties": { - "header": "{{fileName}} Info", + "header": "Info for {{fileName}}", + "header-plural": "Info for {{itemCount}} Items", "name": "Name", "type": "Type", "size": "Size", @@ -1211,7 +1212,8 @@ "addTag": "Add New Tag", "add": "Add", "save-changes": "Save Changes", - "discard": "Discard" + "discard": "Discard", + "mixed-values": "Mixed" }, "view-mode": { "icons": "View: Icons", diff --git a/packages/editor/src/components/assets/FileBrowser/FilePropertiesPanel.tsx b/packages/editor/src/components/assets/FileBrowser/FilePropertiesPanel.tsx index 3bfaa377f8..0794fef465 100644 --- a/packages/editor/src/components/assets/FileBrowser/FilePropertiesPanel.tsx +++ b/packages/editor/src/components/assets/FileBrowser/FilePropertiesPanel.tsx @@ -23,84 +23,106 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { Dialog, DialogTitle, Grid, Typography } from '@mui/material' +import { debounce, Dialog, DialogTitle, Grid, Typography } from '@mui/material' import React, { useCallback, useEffect } from 'react' import { useTranslation } from 'react-i18next' import InputText from '@etherealengine/client-core/src/common/components/InputText' -import { logger } from '@etherealengine/client-core/src/user/services/AuthService' import { staticResourcePath, StaticResourceType } from '@etherealengine/common/src/schema.type.module' import { Engine } from '@etherealengine/ecs/src/Engine' -import { getMutableState, NO_PROXY, State, useHookstate } from '@etherealengine/hyperflux' -import { useFind } from '@etherealengine/spatial/src/common/functions/FeathersHooks' +import { NO_PROXY, State, useHookstate } from '@etherealengine/hyperflux' -import { FileType } from '@etherealengine/ui/src/components/editor/panels/Files/container' -import { EditorState } from '../../../services/EditorServices' +import { + createFileDigest, + createStaticResourceDigest, + FileType +} from '@etherealengine/ui/src/components/editor/panels/Files/container' import { Button } from '../../inputs/Button' import styles from '../styles.module.scss' -export const FilePropertiesPanel = (props: { - openProperties: State - fileProperties: State -}) => { +export const FilePropertiesPanel = (props: { openProperties: State; fileProperties: State }) => { const { openProperties, fileProperties } = props - const { t } = useTranslation() if (!fileProperties.value) return null - const modifiableProperties: State = useHookstate( - JSON.parse(JSON.stringify(fileProperties.get(NO_PROXY))) as FileType - ) - - const isModified = useHookstate(false) + const { t } = useTranslation() - const onChange = useCallback((state: State) => { - isModified.set(true) + const fileStaticResources = useHookstate([]) + const fileDigest = createFileDigest(fileProperties.value) + const resourceDigest = useHookstate(createStaticResourceDigest([])) + const sharedFields = useHookstate([]) + const modifiedFields = useHookstate([]) + const sharedTags = useHookstate([]) + + let title: string + let filename: string + if (fileProperties.value.length === 1) { + const firstFile = fileProperties.value[0] + filename = firstFile.name + title = `${filename} ${firstFile.type == 'folder' ? 'folder' : 'file'} Properties` + } else { + filename = '' + title = `Properties of ${fileProperties.value.length} Items` + } + + const onChange = (fieldName: string, state: State) => { return (e) => { + if (!modifiedFields.value.includes(fieldName)) { + modifiedFields.set([...modifiedFields.value, fieldName]) + } state.set(e.target.value) } - }, []) + } const onSaveChanges = useCallback(async () => { - if (isModified.value && resourceProperties.value.id) { - const key = fileProperties.value!.key - await Engine.instance.api.service(staticResourcePath).patch(resourceProperties.id.value, { - key, - tags: resourceProperties.tags.value as string[], - licensing: resourceProperties.licensing.value, - attribution: resourceProperties.attribution.value - }) - isModified.set(false) + if (modifiedFields.value.length > 0) { + const addedTags: string[] = resourceDigest.tags.value!.filter((tag) => !sharedTags.value.includes(tag)) + const removedTags: string[] = sharedTags.value!.filter((tag) => !resourceDigest.tags.value!.includes(tag)) + for (const resource of fileStaticResources.value) { + const oldTags = resource.tags ?? [] + const newTags = Array.from(new Set([...addedTags, ...oldTags.filter((tag) => !removedTags.includes(tag))])) + await Engine.instance.api.service(staticResourcePath).patch(resource.id, { + key: resource.key, + tags: newTags, + licensing: resourceDigest.licensing.value, + attribution: resourceDigest.attribution.value + }) + } + modifiedFields.set([]) openProperties.set(false) } }, []) - const staticResource = useFind(staticResourcePath, { - query: { - key: fileProperties.value!.key, - project: getMutableState(EditorState).projectName.value! - } - }) - - const resourceProperties = useHookstate({ - tags: [] as string[], - id: '', - project: '', - attribution: '', - licensing: '' - }) useEffect(() => { - if (staticResource.data.length > 0) { - if (staticResource.data.length > 1) logger.warn('Multiple resources with same key found') - const resources = JSON.parse(JSON.stringify(staticResource.data[0])) as StaticResourceType - if (resources) { - resourceProperties.tags.set(resources.tags ?? []) - resourceProperties.id.set(resources.id) - resourceProperties.attribution.set(resources.attribution ?? '') - resourceProperties.licensing.set(resources.licensing ?? '') - resourceProperties.project.set(resources.project ?? '') + const staticResourcesFindApi = () => { + const query = { + key: { + $like: undefined, + $or: fileProperties.value.map(({ key }) => ({ + key + })) + }, + $limit: 10000 } + + Engine.instance.api + .service(staticResourcePath) + .find({ query }) + .then((resources) => { + fileStaticResources.set(resources.data) + const digest = createStaticResourceDigest(resources.data) + resourceDigest.set(digest) + sharedFields.set( + Object.keys(resourceDigest).filter((key) => { + const value = resourceDigest[key] + return value.length !== '' + }) + ) + sharedTags.set(resourceDigest.tags.get(NO_PROXY)!.slice() as string[]) + }) } - }, [staticResource.data]) + const debouncedQuery = debounce(staticResourcesFindApi, 500) + debouncedQuery() + }, [fileProperties]) return ( openProperties.set(false)} classes={{ paper: styles.paperDialog }} > - - {`${fileProperties.value.name} ${fileProperties.value.type == 'folder' ? 'folder' : 'file'} Properties`} - + {title}
- + {fileProperties.value.length === 1 && ( + + )} {t('editor:layout.filebrowser.fileProperties.type')} {t('editor:layout.filebrowser.fileProperties.size')} - {modifiableProperties.type.value} - {modifiableProperties.size.value} + {fileDigest.type} + + {fileProperties.value + .map((file) => file.size) + .reduce((total, value) => total + parseInt(value ?? '0'), 0)} + - {resourceProperties.id.value && ( + {fileStaticResources.value.length > 0 && ( <>
1 && !sharedFields.value.includes('attribution') + ? ' - ' + : resourceDigest.attribution.value + } /> 1 && !sharedFields.value.includes('licensing') + ? ' - ' + : resourceDigest.licensing.value + } />
- {(resourceProperties.tags.value ?? []).map((tag, index) => ( + {(resourceDigest.tags.value ?? []).map((_, index) => (
))}
- {isModified.value && ( + {modifiedFields.value.length > 0 && ( diff --git a/packages/editor/src/components/assets/ImageCompressionPanel.tsx b/packages/editor/src/components/assets/ImageCompressionPanel.tsx index 06b165fbfa..9ec48b3bdb 100644 --- a/packages/editor/src/components/assets/ImageCompressionPanel.tsx +++ b/packages/editor/src/components/assets/ImageCompressionPanel.tsx @@ -32,7 +32,7 @@ import { KTX2EncodeArguments, KTX2EncodeDefaultArguments } from '@etherealengine/engine/src/assets/constants/CompressionParms' -import { useHookstate } from '@etherealengine/hyperflux' +import { ImmutableArray, useHookstate } from '@etherealengine/hyperflux' import { KTX2Encoder } from '@etherealengine/xrui/core/textures/KTX2Encoder' import { PopoverState } from '@etherealengine/client-core/src/common/services/PopoverState' @@ -47,7 +47,9 @@ import Slider from '@etherealengine/ui/src/primitives/tailwind/Slider' import Text from '@etherealengine/ui/src/primitives/tailwind/Text' import { useTranslation } from 'react-i18next' import { MdClose } from 'react-icons/md' -import { FileType } from '@etherealengine/ui/src/components/editor/panels/Files/container' +import { FileType, createFileDigest } from '@etherealengine/ui/src/components/editor/panels/Files/container' +import CompoundNumericInput from '../inputs/CompoundNumericInput' +import styles from './styles.module.scss' const UASTCFlagOptions = [ { label: 'Fastest', value: 0 }, @@ -64,31 +66,33 @@ const UASTCFlagOptions = [ ] export default function ImageCompressionPanel({ - selectedFile, + selectedFiles, refreshDirectory }: { - selectedFile: FileType + selectedFiles: ImmutableArray refreshDirectory: () => Promise }) { const { t } = useTranslation() + const digest = createFileDigest(selectedFiles) + const compressProperties = useHookstate(KTX2EncodeDefaultArguments) const compressionLoading = useHookstate(false) const compressContentInBrowser = async () => { compressionLoading.set(true) - compressProperties.src.set( - selectedFile.type === 'folder' ? `${selectedFile.url}/${selectedFile.key}` : selectedFile.url - ) - - await compressImage() + for (const file of selectedFiles) { + await compressImage(file) + } await refreshDirectory() compressionLoading.set(false) PopoverState.hidePopupover() } - const compressImage = async () => { + const compressImage = async (props: FileType) => { + compressProperties.src.set(props.type === 'folder' ? `${props.url}/${props.key}` : props.url) + const ktx2Encoder = new KTX2Encoder() const img = await new Promise((resolve) => { @@ -118,9 +122,9 @@ export default function ImageCompressionPanel({ uastcZstandard: compressProperties.uastcZstandard.value }) - const newFileName = selectedFile.key.replace(/.*\/(.*)\..*/, '$1') + '.ktx2' - const path = selectedFile.key.replace(/(.*\/).*/, '$1') - const projectName = selectedFile.key.split('/')[1] // TODO: support projects with / in the name + const newFileName = props.key.replace(/.*\/(.*)\..*/, '$1') + '.ktx2' + const path = props.key.replace(/(.*\/).*/, '$1') + const projectName = props.key.split('/')[1] // TODO: support projects with / in the name const relativePath = path.replace('projects/' + projectName + '/', '') const file = new File([data], newFileName, { type: 'image/ktx2' }) @@ -140,6 +144,13 @@ export default function ImageCompressionPanel({ } } + let title: string + if (selectedFiles.length === 1) { + title = selectedFiles[0].name + } else { + title = selectedFiles.length + ' Items' + } + return (
@@ -159,7 +170,7 @@ export default function ImageCompressionPanel({ name="mode" label={t('editor:properties.model.transform.dst')} > - +
refreshDirectory: () => Promise }) { const { t } = useTranslation() @@ -163,7 +163,9 @@ export default function ModelCompressionPanel({ const compressContentInBrowser = async () => { compressionLoading.set(true) - await compressModel() + for (const file of selectedFiles) { + await compressModel(file) + } await refreshDirectory() compressionLoading.set(false) } @@ -196,12 +198,28 @@ export default function ModelCompressionPanel({ localStorage.setItem('presets', JSON.stringify(presetList.value)) } - const compressModel = async () => { + const compressModel = async (file: FileType) => { const clientside = true const exportCombined = true + let fileLODs = lods.value as LODVariantDescriptor[] + + if (selectedFiles.length > 1) { + fileLODs = fileLODs.map((lod) => { + const src = file.url + const fileName = src.split('/').pop()!.split('.').shift()! + const dst = fileName + lod.suffix + return { + ...lod, + src, + dst, + modelFormat: src.endsWith('.gltf') ? 'gltf' : src.endsWith('.vrm') ? 'vrm' : 'glb' + } + }) + } + const heuristic = Heuristic.BUDGET - await createLODVariants(lods.value as LODVariantDescriptor[], clientside, heuristic, exportCombined) + await createLODVariants(fileLODs, clientside, heuristic, exportCombined) } const deletePreset = (event: React.MouseEvent, idx: number) => { @@ -219,7 +237,12 @@ export default function ModelCompressionPanel({ } useEffect(() => { - const fullSrc = selectedFile.url + const firstFile = selectedFiles[0] + if (firstFile == null) { + return + } + + const fullSrc = firstFile.url const fileName = fullSrc.split('/').pop()!.split('.').shift()! const defaults = defaultLODs.map((defaultLOD) => { @@ -232,7 +255,7 @@ export default function ModelCompressionPanel({ }) lods.set(defaults) - }, [selectedFile.url]) + }, [selectedFiles]) const handleAddLOD = () => { const params = JSON.parse(JSON.stringify(lods[selectedLODIndex.value].params.value)) as ModelTransformParameters @@ -248,6 +271,17 @@ export default function ModelCompressionPanel({ selectedLODIndex.set(lods.length - 1) } + let title: string + let fileTypeText: string + if (selectedFiles.length === 1) { + const file = selectedFiles[0] + title = file.name + fileTypeText = file.isFolder ? 'Directory' : 'File' + } else { + title = selectedFiles.length + ' Items' + fileTypeText = 'Multiple Selection' + } + return (
diff --git a/packages/ui/src/components/editor/panels/Files/browserGrid/FilePropertiesModal.tsx b/packages/ui/src/components/editor/panels/Files/browserGrid/FilePropertiesModal.tsx index 75417298c2..2319886798 100644 --- a/packages/ui/src/components/editor/panels/Files/browserGrid/FilePropertiesModal.tsx +++ b/packages/ui/src/components/editor/panels/Files/browserGrid/FilePropertiesModal.tsx @@ -23,7 +23,7 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import React, { useEffect } from 'react' +import React, { useCallback, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { PopoverState } from '@etherealengine/client-core/src/common/services/PopoverState' @@ -36,7 +36,7 @@ import { import { Engine } from '@etherealengine/ecs' import { FileDataType } from '@etherealengine/editor/src/components/assets/FileBrowser/FileDataType' import { EditorState } from '@etherealengine/editor/src/services/EditorServices' -import { getMutableState, useHookstate } from '@etherealengine/hyperflux' +import { getMutableState, ImmutableArray, NO_PROXY, State, useHookstate } from '@etherealengine/hyperflux' import { useFind, useMutation } from '@etherealengine/spatial/src/common/functions/FeathersHooks' import { HiPencil, HiPlus, HiXMark } from 'react-icons/hi2' import { RiSave2Line } from 'react-icons/ri' @@ -44,95 +44,120 @@ import Button from '../../../../../primitives/tailwind/Button' import Input from '../../../../../primitives/tailwind/Input' import Modal from '../../../../../primitives/tailwind/Modal' import Text from '../../../../../primitives/tailwind/Text' +import { createFileDigest, createStaticResourceDigest, FileType } from '../container' +import { debounce } from '@mui/material' -export default function FilePropertiesModal({ projectName, file }: { projectName: string; file: FileDataType }) { +export default function FilePropertiesModal({ projectName, files }: { projectName: string; files: ImmutableArray }) { + const itemCount = files.length + if (itemCount === 0) return null const { t } = useTranslation() - const newFileName = useHookstate(file.name) - const fileService = useMutation(fileBrowserPath) - const handleSubmit = async () => { - fileService.update(null, { - oldProject: projectName, - newProject: projectName, - oldName: file.fullName, - newName: file.isFolder ? newFileName.value : `${newFileName.value}.${file.type}`, - oldPath: file.path, - newPath: file.path, - isCopy: false - }) - PopoverState.hidePopupover() + const fileStaticResources = useHookstate([]) + const fileDigest = createFileDigest(files) + const resourceDigest = useHookstate(createStaticResourceDigest([])) + const sharedFields = useHookstate([]) + const modifiedFields = useHookstate([]) + const editedField = useHookstate(null) + const tagInput = useHookstate('') + const sharedTags = useHookstate([]) + + let title: string + let filename: string + if (itemCount === 1) { + const firstFile = files[0] + filename = firstFile.name + title = t('editor:layout.filebrowser.fileProperties.header', { fileName: filename.toUpperCase() }) + } else { + filename = '' + title = t('editor:layout.filebrowser.fileProperties.header', { itemCount }) } - const staticResource = useFind(staticResourcePath, { - query: { - key: file.key, - project: getMutableState(EditorState).projectName.value! + const onChange = (fieldName: string, state: State) => { + return (e) => { + if (!modifiedFields.value.includes(fieldName)) { + modifiedFields.set([...modifiedFields.value, fieldName]) + } + state.set(e.target.value) } - }) + } - const staticResourceMutation = useMutation(staticResourcePath) + const handleSubmit = async () => { + if (modifiedFields.value.length > 0) { + const addedTags: string[] = resourceDigest.tags.value!.filter((tag) => !sharedTags.value.includes(tag)) + const removedTags: string[] = sharedTags.value!.filter((tag) => !resourceDigest.tags.value!.includes(tag)) + for (const resource of fileStaticResources.value) { + const oldTags = resource.tags ?? [] + const newTags = Array.from(new Set([...addedTags, ...oldTags.filter((tag) => !removedTags.includes(tag))])) + await Engine.instance.api.service(staticResourcePath).patch(resource.id, { + key: resource.key, + tags: newTags, + licensing: resourceDigest.licensing.value, + attribution: resourceDigest.attribution.value + }) + } + modifiedFields.set([]) + PopoverState.hidePopupover() + } + } - const resourceProperties = useHookstate({ - id: '', - project: '', - author: null as UserType | null, - tags: { - input: '', - all: [] as string[] - }, - attribution: { - editing: false, - input: '' - }, - licensing: { - editing: false, - input: '' + const staticResourcesFindApi = () => { + const query = { + key: { + $like: undefined, + $or: files.map(({ key }) => ({ + key + })) + }, + $limit: 10000 } - }) - useEffect(() => { - if (staticResource.data.length > 0) { - if (staticResource.data.length > 1) console.info('Multiple resources with same key found') - const resources = JSON.parse(JSON.stringify(staticResource.data[0])) as StaticResourceType - if (resources) { + Engine.instance.api + .service(staticResourcePath) + .find({ query }) + .then((resources) => { + Engine.instance.api .service('user') - .get(resources.userId) - .then((user) => resourceProperties.author.set(user)) - resourceProperties.id.set(resources.id) - resourceProperties.project.set(resources.project ?? '') - resourceProperties.tags.all.set(resources.tags ?? []) - resourceProperties.attribution.input.set(resources.attribution ?? '') - resourceProperties.licensing.input.set(resources.licensing ?? '') - } - } - }, [staticResource.data]) + .get(resources.data[0].userId) + .then((user) => author.set(user)) - const handleAddTag = () => { - if ( - resourceProperties.tags.input.value && - !resourceProperties.tags.all.value.find((tag) => tag === resourceProperties.tags.input.value) - ) { - const newTags = [...resourceProperties.tags.all.value, resourceProperties.tags.input.value] - staticResourceMutation.patch(resourceProperties.id.value, { - tags: newTags + fileStaticResources.set(resources.data) + const digest = createStaticResourceDigest(resources.data) + resourceDigest.set(digest) + sharedFields.set( + Object.keys(resourceDigest).filter((key) => { + const value = resourceDigest[key] + return value.length !== '' + }) + ) + sharedTags.set(resourceDigest.tags.get(NO_PROXY)!.slice() as string[]) }) - resourceProperties.tags.all.set(newTags) + } + const debouncedQuery = debounce(staticResourcesFindApi, 500) + debouncedQuery() + + const author = useHookstate(null) + + const handleAddTag = () => { + if (tagInput.value != '' && resourceDigest.tags.value!.includes(tagInput.value)) { + if (!modifiedFields.value.includes('tags')) { + modifiedFields.set([...modifiedFields.value, 'tags']) + } + resourceDigest.tags.set([...resourceDigest.tags.value!, tagInput.value]) } - resourceProperties.tags.input.set('') + tagInput.set('') } - const handleRemoveTag = (removedTag: string) => { - const currentTags = resourceProperties.tags.all.value.filter((tag) => tag !== removedTag) - staticResourceMutation.patch(resourceProperties.id.value, { - tags: currentTags - }) - resourceProperties.tags.all.set(currentTags) + const handleRemoveTag = (index: number) => { + if (!modifiedFields.value.includes('tags')) { + modifiedFields.set([...modifiedFields.value, 'tags']) + } + resourceDigest.tags.set(resourceDigest.tags.value!.filter((_, i) => i !== index)) } return (
{t('editor:layout.filebrowser.fileProperties.name')} - {file.name} + {filename}
{t('editor:layout.filebrowser.fileProperties.type')} - {file.type.toUpperCase()} + {fileDigest.type.toUpperCase()}
{t('editor:layout.filebrowser.fileProperties.size')} - {file.size} + { + files + .map((file) => file.size) + .reduce((total, value) => total + parseInt(value ?? '0'), 0) + }
- {resourceProperties.id.value && ( + {fileStaticResources.length > 0 && ( <>
{t('editor:layout.filebrowser.fileProperties.author')} - {resourceProperties.author.value?.name} + {author.value?.name}
{t('editor:layout.filebrowser.fileProperties.attribution')} - {resourceProperties.attribution.editing.value ? ( + {editedField.value === "attribution" ? ( <> resourceProperties.attribution.input.set(event.target.value)} + value={ + files.length > 1 && !sharedFields.value.includes('attribution') + ? t('editor:layout.filebrowser.fileProperties.mixed-values') + : resourceDigest.attribution.value! + } + onChange={onChange('attribution', resourceDigest.attribution)} />
- {resourceProperties.tags.all.value.map((tag, idx) => ( + {resourceDigest.tags.value!.map((tag, idx) => ( - {tag} handleRemoveTag(tag)} /> + {tag} handleRemoveTag(idx)} /> ))}
diff --git a/packages/ui/src/components/editor/panels/Files/browserGrid/index.tsx b/packages/ui/src/components/editor/panels/Files/browserGrid/index.tsx index b5f1b5b5ea..a8664a6f2e 100644 --- a/packages/ui/src/components/editor/panels/Files/browserGrid/index.tsx +++ b/packages/ui/src/components/editor/panels/Files/browserGrid/index.tsx @@ -31,8 +31,6 @@ import { availableTableColumns } from '@etherealengine/editor/src/components/assets/FileBrowser/FileBrowserState' import { FileDataType } from '@etherealengine/editor/src/components/assets/FileBrowser/FileDataType' -import ImageCompressionPanel from '@etherealengine/editor/src/components/assets/ImageCompressionPanel' -import ModelCompressionPanel from '@etherealengine/editor/src/components/assets/ModelCompressionPanel' import { SupportedFileTypes } from '@etherealengine/editor/src/constants/AssetTypes' import { addMediaNode } from '@etherealengine/editor/src/functions/addMediaNode' import { getSpawnPositionAtCenter } from '@etherealengine/editor/src/functions/screenSpaceFunctions' @@ -53,7 +51,6 @@ import { ContextMenu } from '../../../../tailwind/ContextMenu' import { FileType } from '../container' import { FileIcon } from '../icon' import DeleteFileModal from './DeleteFileModal' -import FilePropertiesModal from './FilePropertiesModal' import ImageConvertModal from './ImageConvertModal' import RenameFileModal from './RenameFileModal' @@ -201,6 +198,9 @@ type FileBrowserItemType = { item: FileDataType disableDnD?: boolean currentContent: MutableRefObject<{ item: FileDataType; isCopy: boolean }> + openModelCompress: () => void + openImageCompress: () => void + openFileProperties: () => void isFilesLoading: boolean projectName: string onClick: (event: React.MouseEvent, currentFile: FileDataType) => void @@ -228,6 +228,9 @@ export function FileBrowserItem({ projectName, onClick, handleDropItemsOnPanel, + openModelCompress, + openImageCompress, + openFileProperties, isFilesLoading, addFolder, isListView, @@ -413,12 +416,13 @@ export function FileBrowserItem({ size="small" fullWidth onClick={() => { - PopoverState.showPopupover() + openFileProperties() handleClose() }} > {t('editor:layout.filebrowser.viewAssetProperties')} + {/* + */} + + {fileConsistsOfContentType(item, 'model') && ( + + )} + + {fileConsistsOfContentType(item, 'image') && ( + + )} + diff --git a/packages/ui/src/components/editor/panels/Files/container/index.tsx b/packages/ui/src/components/editor/panels/Files/container/index.tsx index f3e57bdd77..3f369e61d3 100644 --- a/packages/ui/src/components/editor/panels/Files/container/index.tsx +++ b/packages/ui/src/components/editor/panels/Files/container/index.tsx @@ -47,7 +47,6 @@ import { availableTableColumns } from '@etherealengine/editor/src/components/assets/FileBrowser/FileBrowserState' import { FileDataType } from '@etherealengine/editor/src/components/assets/FileBrowser/FileDataType' -import FilePropertiesModal from '../browserGrid/FilePropertiesModal' import ImageCompressionPanel from '@etherealengine/editor/src/components/assets/ImageCompressionPanel' import ModelCompressionPanel from '@etherealengine/editor/src/components/assets/ModelCompressionPanel' import { DndWrapper } from '@etherealengine/editor/src/components/dnd/DndWrapper' @@ -86,6 +85,7 @@ import { Popup } from '../../../../tailwind/Popup' import BooleanInput from '../../../input/Boolean' import InputGroup from '../../../input/Group' import { FileBrowserItem, FileTableWrapper, canDropItemOverFolder } from '../browserGrid' +import FilePropertiesModal from '../browserGrid/FilePropertiesModal' type FileBrowserContentPanelProps = { projectName?: string @@ -564,18 +564,21 @@ const FileBrowserContentPanel: React.FC = (props) }} currentContent={currentContentRef} handleDropItemsOnPanel={(data, dropOn) => - dropItemsOnPanel(data, dropOn, fileProperties.value.map(file => file.key)) + dropItemsOnPanel( + data, + dropOn, + fileProperties.value.map((file) => file.key) + ) } openFileProperties={() => { - PopoverState.showPopupover() + PopoverState.showPopupover( + + ) }} openImageCompress={() => { if (filesConsistOfContentType(fileProperties.value, 'image')) { PopoverState.showPopupover( - From 2d1d1678822ca160e61d0f765ffd3e4266234048 Mon Sep 17 00:00:00 2001 From: Rezmason Date: Wed, 10 Jul 2024 14:52:29 -0700 Subject: [PATCH 09/10] Fixing bugs in file properties modal's feathers call and the static resource digest --- .../Files/browserGrid/FilePropertiesModal.tsx | 61 +++++++++---------- .../editor/panels/Files/container/index.tsx | 19 ++++-- 2 files changed, 42 insertions(+), 38 deletions(-) diff --git a/packages/ui/src/components/editor/panels/Files/browserGrid/FilePropertiesModal.tsx b/packages/ui/src/components/editor/panels/Files/browserGrid/FilePropertiesModal.tsx index d8bd72666a..0b5d93bcfe 100644 --- a/packages/ui/src/components/editor/panels/Files/browserGrid/FilePropertiesModal.tsx +++ b/packages/ui/src/components/editor/panels/Files/browserGrid/FilePropertiesModal.tsx @@ -23,14 +23,14 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import React from 'react' +import React, { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { PopoverState } from '@etherealengine/client-core/src/common/services/PopoverState' import { StaticResourceType, UserType, staticResourcePath } from '@etherealengine/common/src/schema.type.module' import { Engine } from '@etherealengine/ecs' import { ImmutableArray, NO_PROXY, State, useHookstate } from '@etherealengine/hyperflux' -import { debounce } from '@mui/material' +import { useFind } from '@etherealengine/spatial/src/common/functions/FeathersHooks' import { HiPencil, HiPlus, HiXMark } from 'react-icons/hi2' import { RiSave2Line } from 'react-icons/ri' import Button from '../../../../../primitives/tailwind/Button' @@ -98,45 +98,40 @@ export default function FilePropertiesModal({ } } - const staticResourcesFindApi = () => { - const query = { - key: { - $like: undefined, - $or: files.map(({ key }) => ({ - key - })) - }, - $limit: 10000 - } + const query = { + key: { + $like: undefined, + $or: files.map(({ key }) => ({ + key + })) + }, + $limit: 10000 + } + const resources = useFind(staticResourcePath, { query }) + useEffect(() => { + if (resources.data.length === 0) return Engine.instance.api - .service(staticResourcePath) - .find({ query }) - .then((resources) => { - Engine.instance.api - .service('user') - .get(resources.data[0].userId) - .then((user) => author.set(user)) + .service('user') + .get(resources.data[0].userId) + .then((user) => author.set(user)) - fileStaticResources.set(resources.data) - const digest = createStaticResourceDigest(resources.data) - resourceDigest.set(digest) - sharedFields.set( - Object.keys(resourceDigest).filter((key) => { - const value = resourceDigest[key] - return value.length !== '' - }) - ) - sharedTags.set(resourceDigest.tags.get(NO_PROXY)!.slice() as string[]) + fileStaticResources.set(resources.data) + const digest = createStaticResourceDigest(resources.data) + resourceDigest.set(digest) + sharedFields.set( + Object.keys(resourceDigest).filter((key) => { + const value = resourceDigest[key] + return value.length !== '' }) - } - const debouncedQuery = debounce(staticResourcesFindApi, 500) - debouncedQuery() + ) + sharedTags.set(resourceDigest.tags.get(NO_PROXY)!.slice() as string[]) + }, [resources.data.length]) const author = useHookstate(null) const handleAddTag = () => { - if (tagInput.value != '' && resourceDigest.tags.value!.includes(tagInput.value)) { + if (tagInput.value != '' && !resourceDigest.tags.value!.includes(tagInput.value)) { if (!modifiedFields.value.includes('tags')) { modifiedFields.set([...modifiedFields.value, 'tags']) } diff --git a/packages/ui/src/components/editor/panels/Files/container/index.tsx b/packages/ui/src/components/editor/panels/Files/container/index.tsx index 3f369e61d3..f440d57150 100644 --- a/packages/ui/src/components/editor/panels/Files/container/index.tsx +++ b/packages/ui/src/components/editor/panels/Files/container/index.tsx @@ -127,15 +127,24 @@ export const createFileDigest = (files: ImmutableArray): FileType => { export const createStaticResourceDigest = (staticResources: ImmutableArray): StaticResourceType => { const digest: StaticResourceType = { - type: '', - key: '', id: '', - url: '', + key: '', mimeType: '', - userId: '' as UserID, hash: '', + type: '', + project: '', + // dependencies: '', + attribution: '', + licensing: '', + description: '', + // stats: '', + thumbnailKey: '', + thumbnailMode: '', createdAt: '', - updatedAt: '' + updatedAt: '', + + url: '', + userId: '' as UserID } for (const key in digest) { const allValues = new Set(staticResources.map((resource) => resource[key])) From bcf1611de61dd64e8c84c8179486bdad1190cfee Mon Sep 17 00:00:00 2001 From: David Gordon Date: Wed, 10 Jul 2024 15:10:11 -0700 Subject: [PATCH 10/10] prettier --- .../src/components/editor/panels/Files/browserGrid/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/editor/panels/Files/browserGrid/index.tsx b/packages/ui/src/components/editor/panels/Files/browserGrid/index.tsx index 646516bc84..081ff21785 100644 --- a/packages/ui/src/components/editor/panels/Files/browserGrid/index.tsx +++ b/packages/ui/src/components/editor/panels/Files/browserGrid/index.tsx @@ -187,7 +187,7 @@ export const FileGridItem: React.FC = (props) => {
-
{props.item.fullName}
+
{props.item.fullName}
) @@ -357,7 +357,7 @@ export function FileBrowserItem({ )} setAnchorEvent(undefined)}> -
+