@iobroker/adapter-react-v5
Version:
React components to develop ioBroker interfaces with react.
1,236 lines • 78.5 kB
JavaScript
/**
* 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