import { Downgraded } from '@hookstate/core' import React, { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { API } from '@xrengine/client-core/src/API' import ConfirmDialog from '@xrengine/client-core/src/common/components/ConfirmDialog' import LoadingView from '@xrengine/client-core/src/common/components/LoadingView' import { FileBrowserService, FileBrowserServiceReceptor, FILES_PAGE_LIMIT, useFileBrowserState } from '@xrengine/client-core/src/common/services/FileBrowserService' import { uploadToFeathersService } from '@xrengine/client-core/src/util/upload' import { processFileName } from '@xrengine/common/src/utils/processFileName' import { KTX2EncodeArguments } from '@xrengine/engine/src/assets/constants/CompressionParms' import { KTX2EncodeDefaultArguments } from '@xrengine/engine/src/assets/constants/CompressionParms' import { MediaPrefabs } from '@xrengine/engine/src/audio/systems/MediaSystem' import { ScenePrefabs } from '@xrengine/engine/src/scene/systems/SceneObjectUpdateSystem' import { useState as useHFState } from '@xrengine/hyperflux' import { addActionReceptor, removeActionReceptor } from '@xrengine/hyperflux' import ArrowBackIcon from '@mui/icons-material/ArrowBack' import AutorenewIcon from '@mui/icons-material/Autorenew' import Breadcrumbs from '@mui/material/Breadcrumbs' import Dialog from '@mui/material/Dialog' import DialogTitle from '@mui/material/DialogTitle' import Grid from '@mui/material/Grid' import Link from '@mui/material/Link' import MenuItem from '@mui/material/MenuItem' import { PopoverPosition } from '@mui/material/Popover' import TablePagination from '@mui/material/TablePagination' import Typography from '@mui/material/Typography' import { prefabIcons } from '../../functions/PrefabEditors' import { unique } from '../../functions/utils' import BooleanInput from '../inputs/BooleanInput' import { Button } from '../inputs/Button' import Scrubber from '../inputs/Scrubber' import SelectInput from '../inputs/SelectInput' import { ContextMenu } from '../layout/ContextMenu' import { ToolButton } from '../toolbar/ToolButton' import { FileBrowserItem } from './FileBrowserGrid' import { FileDataType } from './FileDataType' import styles from './styles.module.scss' export const PrefabFileType = { gltf: ScenePrefabs.model, 'gltf-binary': ScenePrefabs.model, glb: ScenePrefabs.model, usdz: ScenePrefabs.model, fbx: ScenePrefabs.model, png: ScenePrefabs.image, jpeg: ScenePrefabs.image, jpg: ScenePrefabs.image, m3u8: MediaPrefabs.video, mp4: MediaPrefabs.video, mpeg: MediaPrefabs.audio, mp3: MediaPrefabs.audio, 'model/gltf-binary': ScenePrefabs.model, 'model/gltf': ScenePrefabs.model, 'model/glb': ScenePrefabs.model, 'model/usdz': ScenePrefabs.model, 'model/fbx': ScenePrefabs.model, 'image/png': ScenePrefabs.image, 'image/jpeg': ScenePrefabs.image, 'image/jpg': ScenePrefabs.image, 'application/pdf': null, 'application/vnd.apple.mpegurl': MediaPrefabs.video, 'video/mp4': MediaPrefabs.video, 'audio/mpeg': MediaPrefabs.audio, 'audio/mp3': MediaPrefabs.audio } type FileBrowserContentPanelProps = { onSelectionChanged: (AssetSelectionChangePropsType) => void disableDnD?: boolean selectedFile?: string } type DnDFileType = { dataTransfer: DataTransfer files: File[] items: DataTransferItemList } export function isFileDataType(value: any): value is FileDataType { return value && value.key } /** * FileBrowserPanel used to render view for AssetsPanel. * @constructor */ const FileBrowserContentPanel: React.FC = (props) => { const { t } = useTranslation() const [anchorEl, setAnchorEl] = React.useState(null) const [anchorPosition, setAnchorPosition] = React.useState(undefined) const open = Boolean(anchorEl) const [isLoading, setLoading] = useState(true) const [selectedDirectory, setSelectedDirectory] = useState( `/projects/${props.selectedFile ? props.selectedFile + '/' : ''}` ) const fileState = useFileBrowserState() const filesValue = fileState.files.attach(Downgraded).value const { skip, total, retrieving, updateNeeded } = fileState.value const [fileProperties, setFileProperties] = useState(null) const [openProperties, setOpenPropertiesModal] = useState(false) const [openCompress, setOpenCompress] = useState(false) const compressProperties = useHFState(KTX2EncodeDefaultArguments) const [openConfirm, setOpenConfirm] = useState(false) const [contentToDeletePath, setContentToDeletePath] = useState('') const [contentToDeleteType, setContentToDeleteType] = useState('') const page = skip / FILES_PAGE_LIMIT const breadcrumbs = selectedDirectory .slice(1, -1) .split('/') .map((file, index, arr) => { if (arr.length - 1 == index) { return ( {file} ) } else { return ( handleClick(file)} > {file} ) } }) const files = fileState.files.value.map((file) => { const prefabType = PrefabFileType[file.type] const isFolder = file.type === 'folder' const fullName = isFolder ? file.name : file.name + '.' + file.type return { ...file, path: isFolder ? file.key.split(file.name)[0] : file.key.split(fullName)[0], fullName, isFolder, prefabType, Icon: prefabIcons[prefabType] } }) useEffect(() => { addActionReceptor(FileBrowserServiceReceptor) return () => { removeActionReceptor(FileBrowserServiceReceptor) } }, []) useEffect(() => { setLoading(false) }, [filesValue]) useEffect(() => { if (updateNeeded) onRefreshDirectory() }, [updateNeeded]) useEffect(() => { FileBrowserService.fetchFiles(selectedDirectory) }, [selectedDirectory]) const onSelect = (params: FileDataType) => { if (params.type !== 'folder') { props.onSelectionChanged({ resourceUrl: params.url, name: params.name, contentType: params.type }) } else { const newPath = `${selectedDirectory}${params.name}/` setSelectedDirectory(newPath) } } const handleContextMenu = (event: React.MouseEvent) => { event.preventDefault() event.stopPropagation() setAnchorEl(event.currentTarget) setAnchorPosition({ left: event.clientX + 2, top: event.clientY - 6 }) } const handleClose = () => { setAnchorEl(null) setAnchorPosition(undefined) } const handlePageChange = async (_event, newPage: number) => { await FileBrowserService.fetchFiles(selectedDirectory, newPage) } const createNewFolder = async () => { handleClose() await FileBrowserService.addNewFolder(`${selectedDirectory}New_Folder`) await FileBrowserService.fetchFiles(selectedDirectory) } const dropItemsOnPanel = async (data: FileDataType | DnDFileType, dropOn?: FileDataType) => { if (isLoading) return setLoading(true) const path = dropOn?.isFolder ? dropOn.key : selectedDirectory if (isFileDataType(data)) { if (dropOn?.isFolder) { moveContent(data.fullName, data.fullName, data.path, path, false) } } else { await Promise.all( data.files.map(async (file) => { // If file is directory then it's type is going to be empty string if (!file.type) { await FileBrowserService.addNewFolder(`${path}${file.name}`) } else { const name = processFileName(file.name) await uploadToFeathersService('file-browser/upload', [file], { fileName: name, path, contentType: file.type }).promise } }) ) } await onRefreshDirectory() } const onRefreshDirectory = async () => { await FileBrowserService.fetchFiles(selectedDirectory, page) } const onBackDirectory = () => { const pattern = /([^\/]+)/g const result = selectedDirectory.match(pattern) if (!result) return let newPath = '/' for (let i = 0; i < result.length - 1; i++) { newPath += result[i] + '/' } setSelectedDirectory(newPath) } const moveContent = async ( oldName: string, newName: string, oldPath: string, newPath: string, isCopy = false ): Promise => { if (isLoading) return setLoading(true) await FileBrowserService.moveContent(oldName, newName, oldPath, newPath, isCopy) await onRefreshDirectory() } const handleConfirmDelete = (contentPath: string, type: string) => { setContentToDeletePath(contentPath) setContentToDeleteType(type) setOpenConfirm(true) } const handleConfirmClose = () => { setContentToDeletePath('') setContentToDeleteType('') setOpenConfirm(false) } const deleteContent = async (): Promise => { if (isLoading) return setLoading(true) setOpenConfirm(false) await FileBrowserService.deleteContent(contentToDeletePath, contentToDeleteType) props.onSelectionChanged({ resourceUrl: '', name: '', contentType: '' }) await onRefreshDirectory() } const pasteContent = async () => { handleClose() if (isLoading) return setLoading(true) await FileBrowserService.moveContent( currentContentRef.current.item.fullName, currentContentRef.current.item.fullName, currentContentRef.current.item.path, selectedDirectory, currentContentRef.current.isCopy ) await onRefreshDirectory() } const compressContent = async () => { compressProperties.src.set(fileProperties.url) const compressedPath = await API.instance.client.service('ktx2-encode').create(compressProperties.value) await onRefreshDirectory() setOpenCompress(false) } let currentContent = null! as { item: FileDataType; isCopy: boolean } const currentContentRef = useRef(currentContent) const headGrid = { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '20px' } function handleClick(targetFolder: string) { const pattern = /([^\/]+)/g const result = selectedDirectory.match(pattern) if (!result) return let newPath = '/' for (const folder of result) { if (folder != targetFolder) { newPath += folder + '/' } else { newPath += folder + '/' break } } setSelectedDirectory(newPath) } return (
{breadcrumbs}
{retrieving && ( )}
{unique(files, (file) => file.key).map((file, i) => ( ))} {total > 0 && fileState.files.value.length < total && ( )}
{t('editor:layout.filebrowser.addNewFolder')} {t('editor:layout.filebrowser.pasteAsset')} {openCompress && fileProperties && ( setOpenCompress(false)} aria-labelledby="alert-dialog-title" aria-describedby="alert-dialog-description" classes={{ paper: styles.paperDialog }} > {fileProperties?.name} Compress Mode: compressProperties.mode.set(val)} /> Quality: compressProperties.quality.set(val)} min={1} max={255} smallStep={1} mediumStep={1} largeStep={5} style={{ display: 'flex', alignItems: 'center', width: '100%' }} > Level: {compressProperties.quality.value} Mipmaps: compressProperties.mipmaps.set(val)} /> )} {openProperties && fileProperties && ( setOpenPropertiesModal(false)} aria-labelledby="alert-dialog-title" aria-describedby="alert-dialog-description" classes={{ paper: styles.paperDialog }} > {`${fileProperties?.name} ${fileProperties?.type == 'folder' ? 'folder' : 'file'} Properties`} Name: Type: Size: URL: {fileProperties?.name} {fileProperties?.type} {fileProperties?.size} {fileProperties?.url} )}
) } export default FileBrowserContentPanel