UNPKG

@iobroker/adapter-react-v5

Version:

React components to develop ioBroker interfaces with react.

1,236 lines 78.5 kB
/** * Copyright 2020-2025, Denis Haev <dogafox@gmail.com> * * MIT License * */ import React, { Component } from 'react'; import Dropzone from 'react-dropzone'; import { LinearProgress, ListItemIcon, ListItemText, Menu, MenuItem, Tooltip, CircularProgress, Toolbar, IconButton, Fab, Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button, Input, Breadcrumbs, Box, Checkbox, FormControlLabel, } from '@mui/material'; // MUI Icons import { ArrowBack as IconBack, AudioFile as TypeIconAudio, Bookmark as JsonIcon, BookmarkBorder as CssIcon, Brightness6 as Brightness5Icon, Close as CloseIcon, Code as JSIcon, CreateNewFolder as AddFolderIcon, Delete as DeleteIcon, Description as HtmlIcon, Edit as EditIcon, FolderOpen as EmptyFilterIcon, FolderSpecial as RestrictedIcon, FontDownload as TypeIconTxt, Image as TypeIconImages, InsertDriveFile as FileIcon, KeyboardReturn as EnterIcon, List as IconList, MusicNote as MusicIcon, Publish as UploadIcon, Refresh as RefreshIcon, SaveAlt as DownloadIcon, Videocam as TypeIconVideo, ViewModule as IconTile, } from '@mui/icons-material'; import { DialogError } from '../Dialogs/Error'; import { Utils } from './Utils'; import { DialogTextInput } from '../Dialogs/TextInput'; // Custom Icons import { IconExpert } from '../icons/IconExpert'; import { IconClosed } from '../icons/IconClosed'; import { IconOpen } from '../icons/IconOpen'; import { IconNoIcon } from '../icons/IconNoIcon'; import { Icon } from './Icon'; import { withWidth } from './withWidth'; import { FileViewer, EXTENSIONS } from './FileViewer'; const ROW_HEIGHT = 32; const BUTTON_WIDTH = 32; const TILE_HEIGHT = 120; const TILE_WIDTH = 64; const NOT_FOUND = 'Not found'; const FILE_TYPE_ICONS = { all: FileIcon, images: TypeIconImages, code: JSIcon, txt: TypeIconTxt, audio: TypeIconAudio, video: TypeIconVideo, }; const styles = { root: { width: '100%', overflow: 'hidden', height: '100%', position: 'relative', }, filesDiv: { width: 'calc(100% - 8px)', overflowX: 'hidden', overflowY: 'auto', padding: 8, }, filesDivHint: { position: 'absolute', bottom: 0, left: 20, opacity: 0.7, fontStyle: 'italic', fontSize: 12, }, filesDivTable: { height: 'calc(100% - 56px)', }, filesDivTile: { height: `calc(100% - ${48 * 2 + 8}px)`, display: 'flex', alignContent: 'flex-start', alignItems: 'stretch', flexWrap: 'wrap', flex: `0 0 ${TILE_WIDTH}px`, }, itemTile: (theme) => ({ position: 'relative', userSelect: 'none', cursor: 'pointer', height: TILE_HEIGHT, width: TILE_WIDTH, display: 'inline-block', textAlign: 'center', opacity: 0.1, transition: 'opacity 1s', margin: '4px', borderRadius: '4px', '&:hover': { background: theme.palette.secondary.light, color: Utils.invertColor(theme.palette.secondary.main, true), }, }), itemNameFolderTile: { fontWeight: 'bold', }, itemNameTile: { width: '100%', height: 32, overflow: 'hidden', textOverflow: 'ellipsis', fontSize: 12, textAlign: 'center', wordBreak: 'break-all', }, itemFolderIconTile: (theme) => ({ width: '100%', height: TILE_HEIGHT - 32 - 16 - 8, // name + size display: 'block', pl: 1, color: theme.palette.secondary.main || '#fbff7d', }), itemFolderIconBack: (theme) => ({ position: 'absolute', top: 22, left: 18, zIndex: 1, color: theme.palette.mode === 'dark' ? '#FFF' : '#FFF', }), itemSizeTile: { width: '100%', height: 16, textAlign: 'center', fontSize: 10, }, itemImageTile: { width: 'calc(100% - 8px)', height: TILE_HEIGHT - 32 - 16 - 8, // name + size margin: 4, display: 'block', textAlign: 'center', objectFit: 'contain', }, itemIconTile: { width: '100%', height: TILE_HEIGHT - 32 - 16 - 8, // name + size display: 'block', objectFit: 'contain', }, itemSelected: (theme) => ({ background: theme.palette.primary.main, color: Utils.invertColor(theme.palette.primary.main, true), }), itemTable: (theme) => ({ userSelect: 'none', cursor: 'pointer', height: ROW_HEIGHT, display: 'inline-flex', lineHeight: `${ROW_HEIGHT}px`, '&:hover': { background: theme.palette.secondary.light, color: Utils.invertColor(theme.palette.secondary.main, true), }, }), itemNameTable: { display: 'inline-block', pl: '10px', fontSize: '1rem', verticalAlign: 'top', flexGrow: 1, textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflow: 'hidden', '@media screen and (max-width: 500px)': { textAlign: 'end', direction: 'rtl', }, }, itemNameFolderTable: { fontWeight: 'bold', }, itemSizeTable: { display: 'inline-block', width: 60, verticalAlign: 'top', textAlign: 'right', whiteSpace: 'nowrap', }, itemAccessTable: { // display: 'inline-block', verticalAlign: 'top', width: 60, textAlign: 'right', paddingRight: 5, display: 'flex', justifyContent: 'center', }, itemImageTable: { display: 'inline-block', width: 30, marginTop: 1, objectFit: 'contain', maxHeight: 30, }, itemNoImageTable: { marginTop: 6, }, itemIconTable: { display: 'inline-block', marginTop: 1, width: 30, height: 30, }, itemFolderTable: {}, itemFolderTemp: { opacity: 0.4, }, itemFolderIconTable: (theme) => ({ marginTop: '1px', marginLeft: '8px', display: 'inline-block', width: 30, height: 30, color: theme.palette.secondary.main || '#fbff7d', }), itemDownloadButtonTable: (theme) => ({ display: 'inline-block', width: BUTTON_WIDTH, height: ROW_HEIGHT, minWidth: BUTTON_WIDTH, verticalAlign: 'middle', textAlign: 'center', padding: 0, borderRadius: `${BUTTON_WIDTH / 2}px`, '&:hover': { backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.08)', }, '& span': { pt: '9px', }, '& svg': { width: 14, height: 14, fontSize: '1rem', mt: '-3px', verticalAlign: 'middle', color: theme.palette.mode === 'dark' ? '#EEE' : '#111', }, }), itemDownloadEmptyTable: { display: 'inline-block', width: BUTTON_WIDTH, height: ROW_HEIGHT, minWidth: BUTTON_WIDTH, padding: 0, }, itemAclButtonTable: { width: BUTTON_WIDTH, height: ROW_HEIGHT, minWidth: BUTTON_WIDTH, verticalAlign: 'top', padding: 0, fontSize: 12, display: 'flex', }, itemDeleteButtonTable: { display: 'inline-block', width: BUTTON_WIDTH, height: ROW_HEIGHT, minWidth: BUTTON_WIDTH, verticalAlign: 'top', padding: 0, '& svg': { width: 18, height: 18, fontSize: '1.5rem', }, }, uploadDiv: { top: 0, zIndex: 1, bottom: 0, left: 0, right: 0, position: 'absolute', opacity: 0.9, textAlign: 'center', background: '#FFFFFF', }, uploadDivDragging: { opacity: 1, }, uploadCenterDiv: (theme) => ({ m: '20px', border: '3px dashed grey', borderRadius: '30px', width: 'calc(100% - 40px)', height: 'calc(100% - 40px)', position: 'relative', color: theme.palette.mode === 'dark' ? '#222' : '#CCC', display: 'flex', alignItems: 'center', justifyContent: 'center', }), uploadCenterIcon: { width: '25%', height: '25%', }, uploadCenterText: { fontSize: 24, fontWeight: 'bold', }, uploadCloseButton: { zIndex: 2, position: 'absolute', top: 30, right: 30, }, uploadCenterTextAndIcon: { position: 'absolute', height: '30%', width: '100%', margin: 'auto', opacity: 0.3, }, menuButtonExpertActive: { color: '#c00000', }, menuButtonRestrictActive: { color: '#c05000', }, pathDiv: (theme) => ({ display: 'flex', width: 'calc(100% - 16px)', ml: 1, mr: 1, textOverflow: 'clip', overflow: 'hidden', whiteSpace: 'nowrap', backgroundColor: theme.palette.secondary.main, color: theme.palette.secondary.contrastText, borderRadius: '4px 4px 0 0', }), pathDivInput: { width: '100%', }, pathDivBreadcrumbDir: (theme) => ({ pl: '2px', pr: '2px', cursor: 'pointer', color: 'white', '&:hover': { backgroundColor: theme.palette.primary.main, color: theme.palette.primary.contrastText, }, }), pathDivBreadcrumbSelected: { // todo: add style color: '#FFF', }, backgroundImageLight: { background: 'white', }, backgroundImageDark: { background: 'black', }, backgroundImageColored: { background: 'silver', }, specialFolder: (theme) => ({ color: theme.palette.mode === 'dark' ? '#229b0f' : '#5dd300', }), tooltip: { pointerEvents: 'none', }, }; const USER_DATA = '0_userdata.0'; function getParentDir(dir) { const parts = (dir || '').split('/'); if (parts.length) { parts.pop(); } return parts.join('/'); } function isFile(path) { const ext = Utils.getFileExtension(path); return !!(ext?.toLowerCase().match(/[a-z]+/) && ext.length < 5); } const TABLE = 'Table'; const TILE = 'Tile'; function sortFolders(a, b) { if (a.folder && b.folder) { return a.name > b.name ? 1 : a.name < b.name ? -1 : 0; } if (a.folder) { return -1; } if (b.folder) { return 1; } return a.name > b.name ? 1 : a.name < b.name ? -1 : 0; } export class FileBrowserClass extends Component { imagePrefix; levelPadding; mounted; suppressDeleteConfirm; browseList; browseListRunning; initialReadFinished; supportSubscribes; _tempTimeout; limitToObjectID = null; limitToPath = null; lastSelect = null; setOpacityTimer = null; cacheFoldersTimeout = null; foldersLoading = null; cacheFolders = null; localStorage; scrollPositions = {}; refFileDiv; constructor(props) { super(props); this.localStorage = window._localStorage || window.localStorage; const expandedStr = this.localStorage.getItem('files.expanded') || '[]'; this.refFileDiv = React.createRef(); if (this.props.limitPath) { const parts = this.props.limitPath.split('/'); this.limitToObjectID = parts[0]; this.limitToPath = !parts.length ? null : parts.length === 1 && parts[0] === '' ? null : parts.join('/'); if (this.limitToPath && this.limitToPath.endsWith('/')) { this.limitToPath.substring(0, this.limitToPath.length - 1); } } let expanded; try { expanded = JSON.parse(expandedStr); if (this.limitToPath) { expanded = expanded.filter(id => id.startsWith(`${this.limitToPath}/`) || id === this.limitToPath || this.limitToPath?.startsWith(`${id}/`)); } } catch { expanded = []; } let viewType; if (this.props.showViewTypeButton) { viewType = this.localStorage.getItem('files.viewType') || TABLE; } else { viewType = TABLE; } let selected = this.props.selected || this.localStorage.getItem('files.selected') || USER_DATA; let currentDir; if (props.restrictToFolder) { selected = props.restrictToFolder; currentDir = props.restrictToFolder; const parts = props.restrictToFolder.split('/'); expanded = []; let path = ''; for (let i = 0; i < parts.length; i++) { path += (path ? '/' : '') + parts[i]; expanded.push(path); } } else { // TODO: Now we do not support multiple selection if (Array.isArray(selected)) { selected = selected[0]; } if (isFile(selected)) { currentDir = getParentDir(selected); } else { currentDir = selected; } } const backgroundImage = this.localStorage.getItem('files.backgroundImage') || null; this.state = { viewType, folders: {}, filterEmpty: this.localStorage.getItem('files.empty') !== 'false', expanded, currentDir, expertMode: !!props.expertMode, addFolder: false, uploadFile: false, deleteItem: '', // marked: [], viewer: '', formatEditFile: '', path: selected, selected, errorText: '', modalEditOfAccess: false, backgroundImage, queueLength: 0, loadAllFolders: false, // allFoldersLoaded: false, fileErrors: [], filterByType: props.filterByType || window.localStorage.getItem('files.filterByType') || '', showTypesMenu: null, restrictToFolder: props.restrictToFolder || '', pathFocus: false, suppressDeleteConfirm: false, }; this.imagePrefix = this.props.imagePrefix || './files/'; this.levelPadding = this.props.levelPadding || 20; this.mounted = true; this.suppressDeleteConfirm = 0; this.browseList = []; this.browseListRunning = false; this.initialReadFinished = false; this.supportSubscribes = null; this._tempTimeout = {}; } static getDerivedStateFromProps(props, state) { if (props.expertMode !== undefined && props.expertMode !== state.expertMode) { return { expertMode: props.expertMode, loadAllFolders: true }; } return null; } async loadFolders() { this.initialReadFinished = false; let folders = (await this.browseFolder('/')); if (this.state.viewType === TABLE) { folders = (await this.browseFolders([...this.state.expanded], folders)); } else if (this.state.currentDir && this.state.currentDir !== '/' && (!this.limitToObjectID || this.state.currentDir.startsWith(this.limitToObjectID))) { folders = (await this.browseFolder(this.state.currentDir, folders)); } this.setState({ folders }, () => { if (this.state.viewType === TABLE && !this.findItem(this.state.selected)) { const parts = this.state.selected.split('/'); while (parts.length && !this.findItem(parts.join('/'))) { parts.pop(); } let selected; if (parts.length) { selected = parts.join('/'); } else { selected = USER_DATA; } this.setState({ selected, path: selected, pathFocus: false }, () => this.scrollToSelected()); } else { this.scrollToSelected(); } this.initialReadFinished = true; }); } scrollToSelected() { if (this.mounted) { const el = document.getElementById(this.state.selected); el?.scrollIntoView(); } } async componentDidMount() { this.mounted = true; this.loadFolders().catch(error => console.error(`Cannot load folders: ${error}`)); this.browseList = []; this.browseListRunning = false; this.supportSubscribes = await this.props.socket.checkFeatureSupported('BINARY_STATE_EVENT'); if (this.supportSubscribes) { await this.props.socket.subscribeFiles('*', '*', this.onFileChange); } } componentWillUnmount() { if (this.supportSubscribes) { this.props.socket.unsubscribeFiles('*', '*', this.onFileChange); } this.mounted = false; this.browseList = null; this.browseListRunning = false; Object.values(this._tempTimeout).forEach(timer => { if (timer) { clearTimeout(timer); } }); this._tempTimeout = {}; } browseFoldersCb(foldersList, newFoldersNotNull, cb) { if (!foldersList?.length) { cb(newFoldersNotNull); } else { const folder = foldersList.shift(); if (folder) { void this.browseFolder(folder, newFoldersNotNull) .catch((e) => console.error(`Cannot read folder ${folder}: ${e.message}`)) .then(() => { setTimeout(() => this.browseFoldersCb(foldersList, newFoldersNotNull, cb), 0); }); } else { setTimeout(() => this.browseFoldersCb(foldersList, newFoldersNotNull, cb), 0); } } } browseFolders(foldersList, _newFolders) { let newFoldersNotNull; if (!_newFolders) { newFoldersNotNull = {}; Object.keys(this.state.folders).forEach(folder => (newFoldersNotNull[folder] = this.state.folders[folder])); } else { newFoldersNotNull = _newFolders; } if (!foldersList?.length) { return Promise.resolve(newFoldersNotNull); } return new Promise(resolve => { this.browseFoldersCb(foldersList, newFoldersNotNull, resolve); }); } readDirSerial(adapter, relPath) { return new Promise((resolve, reject) => { if (this.browseList) { // if component still mounted this.browseList.push({ resolve: resolve, reject, adapter, relPath, }); if (!this.browseListRunning) { this.processBrowseList(); } } }); } processBrowseList(level = 0) { if (!this.browseListRunning && this.browseList && this.browseList.length) { this.browseListRunning = true; if (this.browseList.length > 10) { // not too often if (!(this.browseList.length % 10)) { this.setState({ queueLength: this.browseList.length }); } } else { this.setState({ queueLength: this.browseList.length }); } this.browseList[0].processing = true; this.props.socket .readDir(this.browseList[0].adapter, this.browseList[0].relPath || '') .then(files => { if (this.browseList) { // if component still mounted const item = this.browseList.shift(); if (item) { const resolve = item.resolve; item.resolve = null; item.reject = null; item.adapter = null; item.relPath = null; if (resolve) { resolve(files); } this.browseListRunning = false; if (this.browseList.length) { if (level < 5) { this.processBrowseList(level + 1); } else { setTimeout(() => this.processBrowseList(0), 0); } } else { this.setState({ queueLength: 0 }); } } else { this.setState({ queueLength: 0 }); } } }) .catch(e => { if (this.browseList) { // if component still mounted const item = this.browseList.shift(); if (item) { const reject = item.reject; item.resolve = null; item.reject = null; item.adapter = null; item.relPath = null; if (reject) { reject(e); } this.browseListRunning = false; if (this.browseList.length) { if (level < 5) { this.processBrowseList(level + 1); } else { setTimeout(() => this.processBrowseList(0), 0); } } else { this.setState({ queueLength: 0 }); } } else { this.setState({ queueLength: 0 }); } } }); } } async browseFolder(folderId, _newFolders, _checkEmpty, force) { let newFoldersNotNull; if (!_newFolders) { newFoldersNotNull = {}; Object.keys(this.state.folders).forEach(folder => { newFoldersNotNull[folder] = this.state.folders[folder]; }); } else { newFoldersNotNull = _newFolders; } if (newFoldersNotNull[folderId] && !force) { if (!_checkEmpty) { return new Promise((resolve, reject) => { Promise.all(newFoldersNotNull[folderId] .filter(item => item.folder) .map(item => this.browseFolder(item.id, newFoldersNotNull, true).catch(() => undefined))) .then(() => resolve(newFoldersNotNull)) .catch((error) => reject(new Error(error))); }); } return Promise.resolve(newFoldersNotNull); } // if root folder if (!folderId || folderId === '/') { try { let objs = (await this.props.socket.readMetaItems()); const _folders = []; let userData = null; if (this.state.restrictToFolder) { const adapter = this.state.restrictToFolder.split('/')[0]; objs = objs.filter(obj => obj._id === adapter); } else if (!this.state.expertMode) { // load only adapter.admin and not other meta files like hm-rpc.0.devices.blablabla objs = objs.filter(obj => !obj._id.endsWith('.admin')); } const pos = objs.findIndex(obj => obj._id === 'system.meta.uuid'); if (pos !== -1) { objs.splice(pos, 1); } objs.forEach(obj => { if (this.limitToObjectID && this.limitToObjectID !== obj._id) { return; } const item = { id: obj._id, name: obj._id, title: (obj.common && obj.common.name) || obj._id, meta: true, from: obj.from, ts: obj.ts, color: obj.common && obj.common.color, icon: obj.common && obj.common.icon, folder: true, acl: obj.acl, level: 0, }; if (item.id === USER_DATA) { // user data must be first userData = item; } else { _folders.push(item); } }); _folders.sort((a, b) => (a.id > b.id ? 1 : a.id < b.id ? -1 : 0)); if (!this.limitToObjectID || this.limitToObjectID === USER_DATA) { if (userData) { _folders.unshift(userData); } } newFoldersNotNull[folderId || '/'] = _folders; if (!_checkEmpty) { return Promise.all(_folders .filter(item => item.folder) .map(item => this.browseFolder(item.id, newFoldersNotNull, true).catch(() => undefined))).then(() => newFoldersNotNull); } } catch (e) { const knownError = e; if (this.initialReadFinished) { window.alert(`Cannot read meta items: ${knownError.message}`); } newFoldersNotNull[folderId || '/'] = []; } return newFoldersNotNull; } const parts = folderId.split('/'); const level = parts.length; const adapter = parts.shift(); const relPath = parts.join('/'); // make all requests here serial let files; try { files = await this.readDirSerial(adapter || '', relPath); } catch (error) { // work around: 0_userdata.0 is a special folder, that should exist event when other folders and itself do not exit, as the browser shows it anyway. if (error === 'Not exists' && adapter === '0_userdata.0') { files = []; } else { throw error; } } try { const _folders = []; files.forEach(file => { const item = { id: `${folderId}/${file.file}`, ext: Utils.getFileExtension(file.file), folder: file.isDir, name: file.file, size: file.stats?.size, modified: file.modifiedAt, acl: file.acl, level, }; if (this.state.restrictToFolder) { if (item.folder && (item.id.startsWith(`${this.state.restrictToFolder}/`) || item.id === this.state.restrictToFolder || this.state.restrictToFolder.startsWith(`${item.id}/`))) { _folders.push(item); } else if (item.id.startsWith(`${this.state.restrictToFolder}/`)) { _folders.push(item); } } else if (this.limitToPath) { if (item.folder && (item.id.startsWith(`${this.limitToPath}/`) || item.id === this.limitToPath || this.limitToPath.startsWith(`${item.id}/`))) { _folders.push(item); } else if (item.id.startsWith(`${this.limitToPath}/`)) { _folders.push(item); } } else { _folders.push(item); } }); _folders.sort(sortFolders); newFoldersNotNull[folderId] = _folders; if (!_checkEmpty) { return Promise.all(_folders .filter(item => item.folder) .map(item => this.browseFolder(item.id, newFoldersNotNull, true))).then(() => newFoldersNotNull); } } catch (e) { const knownError = e; if (this.initialReadFinished) { window.alert(`Cannot read ${adapter}${relPath ? `/${relPath}` : ''}: ${knownError?.message}`); } newFoldersNotNull[folderId] = []; } return newFoldersNotNull; } toggleFolder(item, e) { e?.stopPropagation(); const expanded = [...this.state.expanded]; const pos = expanded.indexOf(item.id); if (pos === -1) { expanded.push(item.id); expanded.sort(); this.localStorage.setItem('files.expanded', JSON.stringify(expanded)); if (!item.temp) { this.browseFolder(item.id) .then(folders => this.setState({ expanded, folders })) .catch(err => window.alert(err === NOT_FOUND ? this.props.t('ra_Cannot find "%s"', item.id) : this.props.t('ra_Cannot read "%s"', item.id))); } else { this.setState({ expanded }); } } else { expanded.splice(pos, 1); this.localStorage.setItem('files.expanded', JSON.stringify(expanded)); this.setState({ expanded }); } } onFileChange = (id, fileName, size) => { const key = `${id}/${fileName}`; const pos = key.lastIndexOf('/'); const folder = key.substring(0, pos); console.log(`File changed ${key}[${size}]`); if (this.state.folders[folder]) { if (this._tempTimeout[folder]) { clearTimeout(this._tempTimeout[folder]); } this._tempTimeout[folder] = setTimeout(() => { delete this._tempTimeout[folder]; this.browseFolder(folder, null, false, true) .then(folders => this.setState({ folders })) .catch(e => console.error(`Cannot read folder: ${e.message}`)); }, 300); } }; changeFolder(e, folder) { e?.stopPropagation(); this.lastSelect = Date.now(); let _folder = folder || getParentDir(this.state.currentDir); if (_folder === '/') { _folder = ''; } // remember scroll position for this folder if (this.state.viewType === 'Tile' && this.refFileDiv.current?.scrollTop) { this.scrollPositions[this.state.currentDir] = this.refFileDiv.current.scrollTop; } this.localStorage.setItem('files.currentDir', _folder); if (folder && e && (e.altKey || e.shiftKey || e.ctrlKey || e.metaKey)) { this.setState({ selected: _folder }); return; } // If desired folder is not yet loaded if (_folder && !this.state.folders[_folder]) { this.browseFolder(_folder) .then(folders => this.setState({ folders, path: _folder, currentDir: _folder, selected: _folder, pathFocus: false, }, () => this.props.onSelect && this.props.onSelect(''))) .catch(_e => console.error(`Cannot read folder: ${_e.message}`)); return; } this.setState({ currentDir: _folder, selected: _folder, path: _folder, pathFocus: false, }, () => { if (this.props.onSelect) { this.props.onSelect(''); } // scroll to previous position if (this.state.viewType === 'Tile' && this.scrollPositions[this.state.currentDir]) { const pos = this.scrollPositions[this.state.currentDir]; delete this.scrollPositions[this.state.currentDir]; if (this.refFileDiv.current) { this.refFileDiv.current.scrollTop = pos; } } }); } select(id, e, cb) { if (e) { e.stopPropagation(); } this.lastSelect = Date.now(); this.localStorage.setItem('files.selected', id); this.setState({ selected: id, path: id, pathFocus: false }, () => { if (this.props.onSelect) { const ext = Utils.getFileExtension(id); if ((!this.props.filterFiles || (ext && this.props.filterFiles.includes(ext))) && (!this.state.filterByType || (ext && EXTENSIONS[this.state.filterByType].includes(ext)))) { this.props.onSelect(id, false, !!this.state.folders[id]); } else { this.props.onSelect(''); } } if (cb) { cb(); } }); } getText(text) { if (text) { if (typeof text === 'object') { return text[this.props.lang] || text.en || undefined; } return text; } return undefined; } renderFolder(item, expanded) { if ( // this.state.viewType === TABLE && this.state.filterEmpty && !this.state.folders[item.id]?.length && // if the folder is empty item.id !== USER_DATA && !item.temp) { return null; } const IconEl = expanded ? IconOpen : IconClosed; const padding = this.state.viewType === TABLE ? item.level * this.levelPadding : 0; const isUserData = item.name === USER_DATA; const isSpecialData = isUserData || item.name === 'vis.0' || item.name === 'vis-2.0'; const iconStyle = Utils.getStyle(this.props.theme, styles[`itemFolderIcon${this.state.viewType}`], isSpecialData && styles.specialFolder); return (React.createElement(Box, { component: "div", key: item.id, id: item.id, style: this.state.viewType === TABLE ? { marginLeft: padding, width: `calc(100% - ${padding}px)` } : undefined, onClick: e => (this.state.viewType === TABLE ? this.select(item.id, e) : this.changeFolder(e, item.id)), onDoubleClick: e => this.state.viewType === TABLE && this.toggleFolder(item, e), title: this.getText(item.title), className: "browserItem", sx: Utils.getStyle(this.props.theme, styles[`item${this.state.viewType}`], styles[`itemFolder${this.state.viewType}`], this.state.selected === item.id ? styles.itemSelected : {}, item.temp ? styles.itemFolderTemp : {}) }, React.createElement(IconEl, { style: iconStyle, onClick: this.state.viewType === TABLE ? (e) => this.toggleFolder(item, e) : undefined }), React.createElement(Box, { component: "div", sx: Utils.getStyle(this.props.theme, styles[`itemName${this.state.viewType}`], styles[`itemNameFolder${this.state.viewType}`]) }, isUserData ? this.props.t('ra_User files') : item.name), React.createElement(Box, { component: "div", style: styles[`itemSize${this.state.viewType}`], sx: { display: { md: 'inline-block', sm: 'none' } } }, this.state.viewType === TABLE && this.state.folders[item.id] ? this.state.folders[item.id].length : ''), React.createElement(Box, { component: "div", sx: { display: { md: 'inline-block', sm: 'none' } } }, this.state.viewType === TABLE && this.props.expertMode ? this.formatAcl(item.acl) : null), this.state.viewType === TABLE && this.props.expertMode ? (React.createElement(Box, { component: "div", sx: { ...styles.itemDeleteButtonTable, display: { md: 'inline-block', sm: 'none' } } })) : null, this.state.viewType === TABLE && this.props.allowDownload ? (React.createElement("div", { style: styles[`itemDownloadEmpty${this.state.viewType}`] })) : null, this.state.viewType === TABLE && this.props.allowDelete && this.state.folders[item.id] && this.state.folders[item.id].length ? (React.createElement(IconButton, { "aria-label": "delete", onClick: e => { e.stopPropagation(); if (this.suppressDeleteConfirm > Date.now()) { this.deleteItem(item.id); } else { this.setState({ deleteItem: item.id }); } }, sx: styles[`itemDeleteButton${this.state.viewType}`], size: "large" }, React.createElement(DeleteIcon, { fontSize: "small" }))) : this.state.viewType === TABLE && this.props.allowDelete ? (React.createElement(Box, { component: "div", sx: styles[`itemDeleteButton${this.state.viewType}`] })) : null)); } renderBackFolder() { return (React.createElement(Box, { component: "div", key: this.state.currentDir, id: this.state.currentDir, onClick: e => this.changeFolder(e), title: this.props.t('ra_Back to %s', getParentDir(this.state.currentDir)), className: "browserItem", sx: Utils.getStyle(this.props.theme, styles[`item${this.state.viewType}`], styles[`itemFolder${this.state.viewType}`]) }, React.createElement(IconClosed, { style: Utils.getStyle(this.props.theme, styles[`itemFolderIcon${this.state.viewType}`]) }), React.createElement(IconBack, { sx: styles.itemFolderIconBack }), React.createElement(Box, { component: "div", sx: Utils.getStyle(this.props.theme, styles[`itemName${this.state.viewType}`], styles[`itemNameFolder${this.state.viewType}`]) }, ".."))); } formatSize(size) { return (React.createElement("div", { style: styles[`itemSize${this.state.viewType}`] }, size || size === 0 ? Utils.formatBytes(size) : '')); } formatAcl(acl) { const access = acl ? acl.permissions || acl.file : 0; let accessStr; if (access) { accessStr = access.toString(16).padStart(3, '0'); } else { accessStr = ''; } return (React.createElement("div", { style: styles[`itemAccess${this.state.viewType}`] }, this.props.modalEditOfAccessControl ? (React.createElement(IconButton, { size: "large", onClick: () => this.setState({ modalEditOfAccess: true }), sx: styles[`itemAclButton${this.state.viewType}`] }, accessStr || '---')) : (accessStr || '---'))); } getFileIcon(ext) { switch (ext) { case 'json': case 'json5': return React.createElement(JsonIcon, { style: styles[`itemIcon${this.state.viewType}`] }); case 'css': return React.createElement(CssIcon, { style: styles[`itemIcon${this.state.viewType}`] }); case 'js': case 'ts': return React.createElement(JSIcon, { style: styles[`itemIcon${this.state.viewType}`] }); case 'html': case 'md': return React.createElement(HtmlIcon, { style: styles[`itemIcon${this.state.viewType}`] }); case 'mp3': case 'ogg': case 'wav': case 'm4a': case 'mp4': case 'flac': return React.createElement(MusicIcon, { style: styles[`itemIcon${this.state.viewType}`] }); default: return React.createElement(FileIcon, { style: styles[`itemIcon${this.state.viewType}`] }); } } static getEditFile(ext) { switch (ext) { case 'json': case 'json5': case 'js': case 'html': case 'txt': case 'css': case 'log': case 'csv': return true; default: return false; } } setStateBackgroundImage = () => { const array = ['light', 'dark', 'colored', 'delete']; this.setState(({ backgroundImage }) => { if (backgroundImage && array.indexOf(backgroundImage) !== -1 && array.length - 1 !== array.indexOf(backgroundImage)) { this.localStorage.setItem('files.backgroundImage', array[array.indexOf(backgroundImage) + 1]); return { backgroundImage: array[array.indexOf(backgroundImage) + 1] }; } this.localStorage.setItem('files.backgroundImage', array[0]); return { backgroundImage: array[0] }; }); }; getStyleBackgroundImage = () => { // ['light', 'dark', 'colored', 'delete'] switch (this.state.backgroundImage) { case 'light': return styles.backgroundImageLight; case 'dark': return styles.backgroundImageDark; case 'colored': return styles.backgroundImageColored; case 'delete': return null; default: return null; } }; renderFile(item) { const padding = this.state.viewType === TABLE ? item.level * this.levelPadding : 0; const ext = Utils.getFileExtension(item.name); return (React.createElement(Box, { component: "div", key: item.id, id: item.id, onDoubleClick: e => { e.stopPropagation(); if (!this.props.onSelect) { this.setState({ viewer: this.imagePrefix + item.id, formatEditFile: ext }); } else if ((!this.props.filterFiles || (item.ext && this.props.filterFiles.includes(item.ext))) && (!this.state.filterByType || (item.ext && EXTENSIONS[this.state.filterByType].includes(item.ext)))) { this.props.onSelect(item.id, true, !!this.state.folders[item.id]); } }, onClick: e => this.select(item.id, e), style: this.state.viewType === TABLE ? { marginLeft: padding, width: `calc(100% - ${padding}px)` } : undefined, className: "browserItem", sx: Utils.getStyle(this.props.theme, styles[`item${this.state.viewType}`], styles[`itemFile${this.state.viewType}`], this.state.selected === item.id ? styles.itemSelected : undefined) }, ext && EXTENSIONS.images.includes(ext) ? (this.state.fileErrors.includes(item.id) ? (React.createElement(IconNoIcon, { style: { ...styles[`itemImage${this.state.viewType}`], ...this.getStyleBackgroundImage(), ...styles[`itemNoImage${this.state.viewType}`], } })) : (React.createElement(Icon, { onError: e => { e.target.onerror = null; const fileErrors = [...this.state.fileErrors]; if (!fileErrors.includes(item.id)) { fileErrors.push(item.id); this.setState({ fileErrors }); } }, style: { ...styles[`itemImage${this.state.viewType}`], ...this.getStyleBackgroundImage() }, src: this.imagePrefix + item.id, alt: item.name }))) : (this.getFileIcon(ext)), React.createElement(Box, { component: "div", sx: styles[`itemName${this.state.viewType}`] }, item.name), React.createElement(Box, { component: "div", sx: { display: { md: 'inline-block', sm: 'none' } } }, this.formatSize(item.size)), React.createElement(Box, { component: "div", sx: { display: { md: 'inline-block', sm: 'none' } } }, this.state.viewType === TABLE && this.props.expertMode ? this.formatAcl(item.acl) : null), React.createElement(Box, { component: "div", sx: { display: { md: 'inline-block', sm: 'none' } } }, this.state.viewType === TABLE && this.props.expertMode && FileBrowserClass.getEditFile(ext) ? (React.createElement(IconButton, { "aria-label": "edit", onClick: e => { e.stopPropagation(); if (!this.props.onSelect) { this.setState({ viewer: this.imagePrefix + item.id, formatEditFile: ext }); } else if ((!this.props.filterFiles || (item.ext && this.props.filterFiles.includes(item.ext))) && (!this.state.filterByType || (item.ext && EXTENSIONS[this.state.filterByType].includes(item.ext)))) { this.props.onSelect(item.id, true, !!this.state.folders[item.id]); } }, sx: styles.itemDeleteButtonTable, size: "large" }, React.createElement(EditIcon, { fontSize: "small" }))) : (React.createElement(Box, { component: "div", sx: styles[`itemDeleteButton${this.state.viewType}`] }))), this.state.viewType === TABLE && this.props.allowDownload ? (React.createElement(Box, { component: "a", className: "MuiButtonBase-root MuiIconButton-root MuiIconButton-sizeLarge", sx: styles.itemDownloadButtonTable, tabIndex: 0, download: item.id, href: this.imagePrefix + item.id, onClick: e => e.stopPropagation() }, React.createElement(DownloadIcon, null))) : null, this.state.viewType === TABLE && this.props.allowDelete && item.id !== 'vis.0/' && item.id !== 'vis-2.0/' && item.id !== USER_DATA ? (React.createElement(IconButton, { "aria-label": "delete", onClick: e => { e.stopPropagation(); if (this.suppressDeleteConfirm > Date.now()) { this.deleteItem(item.id); } else { this.setState({ deleteItem: item.id }); } }, sx: styles[`itemDeleteButton${this.state.viewType}`], size: "large" }, React.createElement(DeleteIcon, { fontSize: "small" }))) : this.state.viewType === TABLE && this.props.allowDelete ? (React.createElement(Box, { component: "div", sx: styles[`itemDeleteButton${this.state.viewType}`] })) : null)); } renderItems(folderId) { if (this.state.folders?.[folderId]) { // tile if (this.state.viewType === TILE) { const res = []; if (folderId && folderId !== '/') { res.push(this.renderBackFolder()); } this.state.folders[folderId].forEach(item => { if (item.folder) { res.push(this.renderFolder(item)); } else if ((!this.props.filterFiles || (item.ext && this.props.filterFiles.includes(item.ext))) && (!this.state.filterByType || (item.ext && EXTENSIONS[this.state.filterByType].includes(item.ext)))) { res.push(this.renderFile(item)); } }); return res; } // table const totalResult = []; this.state.folders[folderId].forEach(item => { if (item.folder) { const expanded = this.state.expanded.includes(item.id); const folders = this.renderFolder(item, expanded); if (Array.isArray(folders)) { folders.forEach(folder => totalResult.push(folder)); } else { totalResult.push(folders); } if (this.state.folders[item.id] && expanded) { const items = this.renderItems(item.id); if (Array.isArray(items)) { items.forEach(_item => totalResult.push(_item)); } else { totalResult.push(items); } } } else if ((!this.props.filterFiles || (item.ext && this.props.filterFiles.includes(item.ext))) && (!this.state.filterByType || (item.ext && EXTENSIONS[this.state.filterByType].includes(item.ext)))) { totalResult.push(this.renderFile(item)); } }); return totalResult; } return (React.createElement("div", { style: { position: 'relative' } }, React.createElement(CircularProgress, { key: folderId, color: "secondary", size: 24 }), React.createElement("div", { style: { position: 'absolute', zIndex: 2, top: 4, width: 24, textAlign: 'center', } }, this.state.queueLength))); } renderToolbar() { const IconType = this.props.showTypeSelector ? FILE_TYPE_ICONS[this.state.filterByType || 'all'] || FILE_TYPE_ICONS.all : null; const isInFold