// Copyright (c) 2023 Robin Davies // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in // the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of // the Software, and to permit persons to whom the Software is furnished to do so, // subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import React from 'react'; import { createStyles } from './WithStyles'; import { Theme } from '@mui/material/styles'; import CreateNewFolderIcon from '@mui/icons-material/CreateNewFolder'; import RenameDialog from './RenameDialog'; import Divider from '@mui/material/Divider'; import MenuItem from '@mui/material/MenuItem'; import Menu from '@mui/material/Menu'; import MoreIcon from '@mui/icons-material/MoreVert'; import { PiPedalModel, PiPedalModelFactory, FileEntry, BreadcrumbEntry, FileRequestResult } from './PiPedalModel'; import { isDarkMode } from './DarkMode'; import Button from '@mui/material/Button'; import FileUploadIcon from '@mui/icons-material/FileUpload'; import FileDownloadIcon from '@mui/icons-material/FileDownload'; import AudioFileIcon from '@mui/icons-material/AudioFile'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import FolderIcon from '@mui/icons-material/Folder'; import InsertDriveFileOutlinedIcon from '@mui/icons-material/InsertDriveFileOutlined'; import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'; import DialogActions from '@mui/material/DialogActions'; import DialogTitle from '@mui/material/DialogTitle'; import DialogContent from '@mui/material/DialogContent'; import IconButton from '@mui/material/IconButton'; import OldDeleteIcon from './OldDeleteIcon'; import Toolbar from '@mui/material/Toolbar'; import WithStyles from './WithStyles'; import { withStyles } from "tss-react/mui"; import ResizeResponsiveComponent from './ResizeResponsiveComponent'; import { UiFileProperty } from './Lv2Plugin'; import ButtonBase from '@mui/material/ButtonBase'; import Typography from '@mui/material/Typography'; import DialogEx from './DialogEx'; import UploadFileDialog from './UploadFileDialog'; import OkCancelDialog from './OkCancelDialog'; import HomeIcon from '@mui/icons-material/Home'; import FilePropertyDirectorySelectDialog from './FilePropertyDirectorySelectDialog'; const styles = (theme: Theme) => createStyles({ secondaryText: { color: theme.palette.text.secondary }, }); const audioFileExtensions: { [name: string]: boolean } = { ".wav": true, ".flac": true, ".ogg": true, ".aac": true, ".au": true, ".snd": true, ".mid": true, ".rmi": true, ".mp3": true, ".mp4": true, ".aif": true, ".aifc": true, ".aiff": true, ".ra": true }; function isAudioFile(filename: string) { let npos = filename.lastIndexOf('.'); let nposSlash = filename.lastIndexOf('/'); if (nposSlash >= npos) { return false; } let extension = filename.substring(npos); return audioFileExtensions[extension] !== undefined; } export interface FilePropertyDialogProps extends WithStyles { open: boolean, fileProperty: UiFileProperty, instanceId: number, selectedFile: string, onOk: (fileProperty: UiFileProperty, selectedItem: string) => void, onApply: (fileProperty: UiFileProperty, selectedItem: string) => void, onCancel: () => void }; export interface FilePropertyDialogState { fullScreen: boolean; selectedFile: string; selectedFileIsDirectory: boolean; selectedFileProtected: boolean; hasSelection: boolean; hasFileSelection: boolean; canDelete: boolean; fileResult: FileRequestResult; navDirectory: string; windowWidth: number; uploadDirectory: string; isProtectedDirectory: boolean; columns: number; columnWidth: number; openUploadFileDialog: boolean; openConfirmDeleteDialog: boolean; menuAnchorEl: null | HTMLElement; newFolderDialogOpen: boolean; renameDialogOpen: boolean; moveDialogOpen: boolean; initialSelection: string; }; function pathExtension(path: string) { let dotPos = path.lastIndexOf('.'); if (dotPos === -1) return ""; let slashPos = path.lastIndexOf('/'); if (slashPos !== -1) { if (dotPos <= slashPos + 1) return ""; } return path.substring(dotPos); // include the '.'. } function pathParentDirectory(path: string) { let npos = path.lastIndexOf('/'); if (npos === -1) return ""; return path.substring(0, npos); } function pathConcat(left: string, right: string) { if (left === "") return right; if (right === "") return left; if (left.endsWith('/')) { left = left.substring(0, left.length - 1); } if (right.startsWith("/")) { right = right.substring(1); } return left + "/" + right; } export function pathFileNameOnly(path: string): string { if (path === "..") return path; let slashPos = path.lastIndexOf('/'); if (slashPos < 0) { slashPos = 0; } else { ++slashPos; } let extPos = path.lastIndexOf('.'); if (extPos < 0 || extPos < slashPos) { extPos = path.length; } return path.substring(slashPos, extPos); } export function pathFileName(path: string): string { if (path === "..") return path; let slashPos = path.lastIndexOf('/'); if (slashPos < 0) { slashPos = 0; } else { ++slashPos; } return path.substring(slashPos); } export default withStyles( class FilePropertyDialog extends ResizeResponsiveComponent { getNavDirectoryFromFile(selectedFile: string, fileProperty: UiFileProperty): string { if (selectedFile === "") { return ""; } return pathParentDirectory(selectedFile); } constructor(props: FilePropertyDialogProps) { super(props); this.model = PiPedalModelFactory.getInstance(); let selectedFile = props.selectedFile; this.state = { fullScreen: this.getFullScreen(), selectedFile: selectedFile, selectedFileProtected: true, windowWidth: this.windowSize.width, selectedFileIsDirectory: false, navDirectory: this.getNavDirectoryFromFile(selectedFile, props.fileProperty), uploadDirectory: "", hasSelection: false, hasFileSelection: false, canDelete: false, columns: 0, columnWidth: 1, fileResult: new FileRequestResult(), isProtectedDirectory: true, openUploadFileDialog: false, openConfirmDeleteDialog: false, menuAnchorEl: null, newFolderDialogOpen: false, renameDialogOpen: false, moveDialogOpen: false, initialSelection: this.props.selectedFile }; this.requestScroll = true; } getFullScreen() { return window.innerWidth < 450 || window.innerHeight < 450; } private scrollRef: HTMLDivElement | null = null; onScrollRef(element: HTMLDivElement | null) { this.scrollRef = element; this.maybeScrollIntoView(); } private maybeScrollIntoView() { if (this.scrollRef !== null && this.state.fileResult.files.length !== 0 && this.mounted && this.requestScroll) { this.requestScroll = false; let options: ScrollIntoViewOptions = { block: "nearest" }; options.block = "nearest"; this.scrollRef.scrollIntoView(options); } } private mounted: boolean = false; private model: PiPedalModel; private requestFiles(navPath: string) { if (!this.props.open) { return; } if (this.props.fileProperty.directory === "") { return; } this.model.requestFileList2(navPath, this.props.fileProperty) .then((filesResult) => { if (this.mounted) { // let insertionPoint = files.length; // for (let i = 0; i < files.length; ++i) { // if (!files[i].isDirectory) { // insertionPoint = i; // break; // } // } filesResult.files.splice(0, 0, { pathname: "", displayName: "", isDirectory: false, isProtected: true }); let fileEntry = this.getFileEntry(filesResult.files, this.state.selectedFile); this.setState({ isProtectedDirectory: filesResult.isProtected, fileResult: filesResult, hasSelection: !!fileEntry, hasFileSelection: !!fileEntry && (!fileEntry.isDirectory) && (!fileEntry.isProtected), selectedFileProtected: fileEntry ? fileEntry.isProtected : true, navDirectory: navPath, uploadDirectory: filesResult.currentDirectory }); } }).catch((error) => { if (!this.mounted) return; if (navPath !== "") // deleted sample directory maybe? { this.navigate(""); } else { this.model.showAlert(error.toString()) } }); } private lastDivRef: HTMLDivElement | null = null; onMeasureRef(div: HTMLDivElement | null) { this.lastDivRef = div; if (div) { let width = div.offsetWidth; if (width === 0) return; let columns = 1; let columnWidth = (width - 40) / columns; if (columns !== this.state.columns || columnWidth !== this.state.columnWidth) { this.setState({ columns: columns, columnWidth: columnWidth }); } } } onWindowSizeChanged(width: number, height: number): void { this.setState({ fullScreen: this.getFullScreen(), windowWidth: width }) if (this.lastDivRef !== null) { this.onMeasureRef(this.lastDivRef); } } private requestScroll: boolean = false; componentDidMount() { super.componentDidMount(); this.mounted = true; this.requestFiles(this.state.navDirectory) this.requestScroll = true; } componentWillUnmount() { super.componentWillUnmount(); this.mounted = false; this.lastDivRef = null; } componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any): void { super.componentDidUpdate?.(prevProps, prevState, snapshot); if (prevProps.open !== this.props.open || prevProps.fileProperty !== this.props.fileProperty || prevProps.selectedFile !== this.props.selectedFile) { if (this.props.open) { let selectedFile = this.props.selectedFile; let navDirectory = this.getNavDirectoryFromFile(selectedFile, this.props.fileProperty); this.setState({ selectedFile: selectedFile, selectedFileIsDirectory: false, selectedFileProtected: true, newFolderDialogOpen: false, renameDialogOpen: false, moveDialogOpen: false, navDirectory: navDirectory }); this.requestFiles(navDirectory) this.requestScroll = true; } } } private isDirectory(path: string): boolean { for (var fileEntry of this.state.fileResult.files) { if (fileEntry.pathname === path) { return fileEntry.isDirectory; } } return false; } private isFileInList(files: FileEntry[], file: string) { let hasSelection = false; if (file === "") return true; for (var listFile of files) { if (listFile.pathname === file) { hasSelection = true; break; } } return hasSelection; } private getFileEntry(files: FileEntry[], file: string): FileEntry | undefined { if (file === "") return undefined; for (var listFile of files) { if (listFile.pathname === file) { return listFile; } } return undefined; } onSelectValue(fileEntry: FileEntry) { this.requestScroll = true; if (!fileEntry.isDirectory) { this.props.onApply(this.props.fileProperty,fileEntry.pathname); } this.setState({ selectedFile: fileEntry.pathname, selectedFileIsDirectory: fileEntry.isDirectory, selectedFileProtected: fileEntry.isProtected, hasSelection: this.isFileInList(this.state.fileResult.files, fileEntry.pathname), hasFileSelection: !fileEntry.isDirectory && !fileEntry.isDirectory && !fileEntry.isProtected }) } onDoubleClickValue(selectedFile: string) { this.openSelectedFile(); } handleMenuOpen(event: React.MouseEvent) { this.setState({ menuAnchorEl: event.currentTarget }); } handleMenuClose() { this.setState({ menuAnchorEl: null }); } handleDelete() { if (this.state.selectedFileProtected) { return; } this.setState({ openConfirmDeleteDialog: true }); } handleDownloadFile() { if (this.state.selectedFileProtected || !this.state.hasFileSelection) { return; } let file = this.state.selectedFile; this.model.downloadAudioFile(file); } handleConfirmDelete() { if (this.state.selectedFileProtected) { return; } this.setState({ openConfirmDeleteDialog: false }); if (this.state.hasSelection) { let selectedFile = this.state.selectedFile; let position = -1; for (let i = 0; i < this.state.fileResult.files.length; ++i) { let file = this.state.fileResult.files[i]; if (file.pathname === selectedFile) { position = i; } } let newSelection: FileEntry | undefined = undefined; if (position >= 0 && position < this.state.fileResult.files.length - 1) { newSelection = this.state.fileResult.files[position + 1]; } else if (position === this.state.fileResult.files.length - 1) { if (position !== 0) { newSelection = this.state.fileResult.files[position - 1]; } } this.model.deleteUserFile(this.state.selectedFile) .then( () => { if (newSelection) { this.setState({ selectedFile: newSelection.pathname, selectedFileIsDirectory: newSelection.isDirectory, selectedFileProtected: newSelection.isProtected, hasSelection: true, hasFileSelection: !newSelection.isDirectory && !newSelection.isProtected }); } else { this.setState({ selectedFile: "", selectedFileIsDirectory: false, selectedFileProtected: true, hasSelection: false, hasFileSelection: false }); } this.requestFiles(this.state.navDirectory); } ).catch( (e: any) => { this.model.showAlert(e.toString()); } ); } } navigate(relativeDirectory: string) { this.requestFiles(relativeDirectory); this.setState({ navDirectory: relativeDirectory }); } renderBreadcrumbs() { let breadcrumbs: React.ReactElement[] = [( ) ]; for (let i = 1; i < this.state.fileResult.breadcrumbs.length - 1; ++i) { let breadcrumb: BreadcrumbEntry = this.state.fileResult.breadcrumbs[i]; breadcrumbs.push(( / ) ); breadcrumbs.push(( )); } if (this.state.fileResult.breadcrumbs.length > 1) { let lastdirectory = this.state.fileResult.breadcrumbs[this.state.fileResult.breadcrumbs.length - 1]; breadcrumbs.push(( / ) ); breadcrumbs.push((
{lastdirectory.displayName}
)); } return (
{breadcrumbs}
); } getDefaultPath(): string { try { let storage = window.localStorage; let result = storage.getItem("fpDefaultPath"); if (result) { return result; } } catch (e) { } return this.state.navDirectory; } setDefaultPath(path: string) { try { let storage = window.localStorage; storage.setItem("fpDefaultPath", path); } catch (e) { } } hasSelectedFileOrFolder(): boolean { return this.state.hasSelection && this.state.selectedFile !== ""; } getFileExtensionList(uiFileProperty: UiFileProperty): string { let result = ""; for (var fileType of uiFileProperty.fileTypes) { if (fileType.fileExtension !== "" && fileType.fileExtension !== ".zip") { if (result !== "") result = result + ","; result += fileType.fileExtension; } } return result; } private getIcon(fileEntry: FileEntry) { if (fileEntry.pathname === "") { return (); } if (fileEntry.isDirectory) { return ( ); } if (isAudioFile(fileEntry.pathname)) { return (); } return (); } render() { const classes = withStyles.getClasses(this.props); let columnWidth = this.state.columnWidth; let okButtonText = "Select"; if (this.state.hasSelection && this.state.selectedFileIsDirectory) { okButtonText = "Open"; } let protectedDirectory = this.state.fileResult.isProtected; let protectedItem = true; if (this.state.hasSelection) { protectedItem = this.state.selectedFileProtected; } let canMoveOrRename = this.hasSelectedFileOrFolder() && !protectedItem; return this.props.open && ( { this.props.onCancel(); }} onEnterKey={() => { this.openSelectedFile(); }} open={this.props.open} tag="fileProperty" fullWidth maxWidth="md" PaperProps={ this.state.fullScreen ? {} : { style: { minHeight: "90%", maxHeight: "90%", } }} > { this.props.onCancel(); }} > {this.props.fileProperty.label} { this.onNewFolder(); }} disabled={protectedDirectory} > { this.handleMenuOpen(ev); }} disabled={protectedDirectory} > { this.handleMenuClose(); }} > { this.handleMenuClose(); this.onNewFolder(); }}>New folder {canMoveOrRename && ()} {canMoveOrRename && ( { this.handleMenuClose(); this.onMove(); }}>Move)} {canMoveOrRename && this.hasSelectedFileOrFolder() && !protectedItem && ( { this.handleMenuClose(); this.onRename(); }}>Rename)}
{this.renderBreadcrumbs()}
this.onMeasureRef(element)} style={{ flex: "1 1 100%", display: "flex", flexFlow: "row wrap", justifyContent: "flex-start", alignContent: "flex-start", paddingLeft: 16, paddingBottom: 16 }}> { (this.state.columns !== 0) && // don't render until we have number of columns derived from layout. this.state.fileResult.files.map( (value: FileEntry, index: number) => { let displayValue = value.displayName; if (displayValue === "") { displayValue = ""; } let selected = value.pathname === this.state.selectedFile; let selectBg = selected ? "rgba(0,0,0,0.15)" : "rgba(0,0,0,0.0)"; if (isDarkMode()) { selectBg = selected ? "rgba(255,255,255,0.10)" : "rgba(0,0,0,0.0)"; } let scrollRef: ((element: HTMLDivElement | null) => void) | undefined = undefined; if (selected) { scrollRef = (element) => { this.onScrollRef(element) }; } return ( this.onSelectValue(value)} onDoubleClick={() => { this.onDoubleClickValue(value.pathname); }} >
{this.getIcon(value)} {displayValue}
); } ) }
{this.state.windowWidth > 500 ? (
this.handleDelete()} >
 
) : (
this.handleDelete()} >
 
 
)}
{ this.setState({ openUploadFileDialog: false }); } } uploadPage={ "uploadUserFile?directory=" + encodeURIComponent(this.state.uploadDirectory) + "&id=" + this.props.instanceId.toString() + "&property=" + encodeURIComponent(this.props.fileProperty.patchProperty) + "&ext=" + encodeURIComponent(this.getFileExtensionList(this.props.fileProperty)) } onUploaded={() => { this.requestFiles(this.state.navDirectory); }} fileProperty={this.props.fileProperty} /> this.handleConfirmDelete()} onClose={() => { this.setState({ openConfirmDeleteDialog: false }); }} /> { this.state.newFolderDialogOpen && ( { this.setState({ newFolderDialogOpen: false }); this.onExecuteNewFolder(newName) }} onClose={() => { this.setState({ newFolderDialogOpen: false }); }} acceptActionName="OK" /> ) } { this.state.renameDialogOpen && ( { this.setState({ renameDialogOpen: false }); this.onExecuteRename(newName) }} onClose={() => { this.setState({ renameDialogOpen: false }); }} acceptActionName="OK" /> ) } { this.state.moveDialogOpen && ( ( { this.setState({ moveDialogOpen: false }); }} onOk={ (path) => { this.setState({ moveDialogOpen: false }); this.setDefaultPath(path); this.onExecuteMove(path); } } /> ) ) } ); } openSelectedFile(): void { if (this.isDirectory(this.state.selectedFile)) { this.requestFiles(this.state.selectedFile); this.setState({ navDirectory: this.state.selectedFile }); } else { this.props.onOk(this.props.fileProperty, this.state.selectedFile); } } private renameDefaultName(): string { let name = this.state.selectedFile; if (name === "") return ""; if (this.isDirectory(name)) { return pathFileName(name); } else { return pathFileNameOnly(name); } } private onMove(): void { this.setState({ moveDialogOpen: true }); } private onExecuteMove(newDirectory: string) { let fileName = pathFileName(this.state.selectedFile); let oldFilePath = pathConcat(this.state.navDirectory, fileName); let newFilePath = pathConcat(newDirectory, fileName); this.model.renameFilePropertyFile(oldFilePath, newFilePath, this.props.fileProperty) .then(() => { this.requestFiles(this.state.navDirectory); }) .catch((e) => { this.model.showAlert(e.toString()); }); } private onRename(): void { this.setState({ renameDialogOpen: true }); } private onExecuteRename(newName: string) { let newPath: string = ""; let oldPath: string = ""; if (this.isDirectory(this.state.selectedFile)) { let oldName = pathFileName(this.state.selectedFile); if (oldName === newName) return; oldPath = pathConcat(this.state.navDirectory, oldName); newPath = pathConcat(this.state.navDirectory, newName);; } else { let oldName = pathFileNameOnly(this.state.selectedFile); if (oldName === newName) return; let extension = pathExtension(this.state.selectedFile); oldPath = pathConcat(this.state.navDirectory, oldName + extension); newPath = pathConcat(this.state.navDirectory, newName + extension); } this.model.renameFilePropertyFile(oldPath, newPath, this.props.fileProperty) .then((newPath) => { this.setState({ selectedFile: newPath }); this.requestFiles(this.state.navDirectory); this.requestScroll = true }) .catch((e) => { this.model.showAlert(e.toString()); }); } private onNewFolder(): void { this.setState({ newFolderDialogOpen: true }); } private onExecuteNewFolder(newName: string) { this.model.createNewSampleDirectory(pathConcat(this.state.navDirectory, newName), this.props.fileProperty) .then((newPath) => { this.setState({ selectedFile: newPath, selectedFileIsDirectory: true, selectedFileProtected: false }); this.requestFiles(this.state.navDirectory); this.requestScroll = true }) .catch((e) => { this.model.showAlert(e.toString()); } ); } }, styles);