// 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 Tone3000Dialog from './Tone3000Dialog'; import Tone3000HelpDialog from './Tone3000HelpDialog'; import GuitarMLHelpDialog from './GuitarMlHelpDialog'; import HelpOutlineIcon from '@mui/icons-material/HelpOutline'; import LinkEx from './LinkEx'; import DraggableButtonBase from './DraggableButtonBase'; import CloseIcon from '@mui/icons-material/Close'; 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 ButtonEx from './ButtonEx'; 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 IconButtonEx from './IconButtonEx'; import OldDeleteIcon from './OldDeleteIcon'; import Toolbar from '@mui/material/Toolbar'; import WithStyles from './WithStyles'; import { withStyles } from "tss-react/mui"; import CircularProgress from '@mui/material/CircularProgress'; import { pathConcat, pathParentDirectory, pathFileName, pathFileNameOnly, pathExtension } from './FileUtils'; './FileUtils'; import ResizeResponsiveComponent from './ResizeResponsiveComponent'; import { UiFileProperty } from './Lv2Plugin'; 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'; import { getAlbumArtUri, getTrackTitle } from './AudioFileMetadata'; const ToobNamModelFileUrl = "http://two-play.com/plugins/toob-nam#modelFile"; const ToobMlModelFileUrl = "http://two-play.com/plugins/toob-ml#modelFile"; const AUTOSCROLL_TICK_DELAY = 30; const AUTOSCROLL_THRESHOLD = 48; const AUTOSCROLL_SCROLL_PER_SECOND = 500; const AUTOSCROLL_SCROLL_RATE = AUTOSCROLL_SCROLL_PER_SECOND * AUTOSCROLL_TICK_DELAY / 1000; interface Point { x: number; y: number; } const styles = (theme: Theme) => createStyles({ secondaryText: { color: theme.palette.text.secondary }, }); const audioFileExtensions: { [name: string]: boolean } = { ".wav": true, ".flac": true, ".mp3": true, ".m4a": true, ".ogg": true, ".aac": true, ".au": true, ".snd": true, ".mid": true, ".rmi": true, ".mp4": true, ".aif": true, ".aifc": true, ".aiff": true, ".ra": true }; function screenToClient(currentTarget: HTMLElement, e: React.PointerEvent | React.MouseEvent): Point { let rect = currentTarget.getBoundingClientRect(); const x = e.clientX + rect.left; const y = e.clientY + rect.right; return { x: x, y: y }; } 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 { reordering: boolean; loading: boolean; dragState: DragState | null; showProgress: boolean; fullScreen: boolean; selectedFile: string; selectedFileIsDirectory: boolean; selectedFileProtected: boolean; hasSelection: boolean; hasFileSelection: boolean; canDelete: boolean; fileResult: FileRequestResult; navDirectory: string; windowWidth: number; windowHeight: number; currentDirectory: string; isProtectedDirectory: boolean; columns: number; columnWidth: number; openUploadFileDialog: boolean; openConfirmDeleteDialog: boolean; confirmCopyDialogState: { fileName: string; oldFilePath: string; newFilePath: string; } | null; menuAnchorEl: null | HTMLElement; menuMultiselectAnchorEl: null | HTMLElement; newFolderDialogOpen: boolean; renameDialogOpen: boolean; moveDialogOpen: boolean; copyDialogOpen: boolean; initialSelection: string; multiSelect: boolean, selectedFiles: string[], //openTone3000Dialog: boolean, openTone3000Help: boolean, openGuitarMlHelp: boolean }; interface DragState { activeItem: string; height: number; dragElement: HTMLElement; dragElementDy: number; from: number; to: number; lastMousePoint: Point; }; export default withStyles( class FilePropertyDialog extends ResizeResponsiveComponent { getNavDirectoryFromFile(selectedFile: string, fileProperty: UiFileProperty): string { if (selectedFile === "") { return ""; } return pathParentDirectory(selectedFile); } isTracksDirectory(): boolean { return this.state.currentDirectory.startsWith("/var/pipedal/audio_uploads/shared/audio/Tracks"); } isFolderArtwork(filePath: string): boolean { if (!this.isTracksDirectory()) return false; let extension = pathExtension(filePath); return extension === ".png" || extension === ".jpg" || extension === ".jpeg"; } constructor(props: FilePropertyDialogProps) { super(props); this.model = PiPedalModelFactory.getInstance(); let selectedFile = props.selectedFile; this.state = { reordering: false, loading: false, dragState: null, showProgress: false, fullScreen: this.getFullScreen(), selectedFile: selectedFile, selectedFileProtected: true, windowWidth: this.windowSize.width, windowHeight: this.windowSize.height, selectedFileIsDirectory: false, navDirectory: this.getNavDirectoryFromFile(selectedFile, props.fileProperty), currentDirectory: "", hasSelection: false, hasFileSelection: false, canDelete: false, columns: 0, columnWidth: 1, fileResult: new FileRequestResult(), isProtectedDirectory: true, openUploadFileDialog: false, openConfirmDeleteDialog: false, confirmCopyDialogState: null, menuAnchorEl: null, menuMultiselectAnchorEl: null, newFolderDialogOpen: false, renameDialogOpen: false, moveDialogOpen: false, copyDialogOpen: false, initialSelection: this.props.selectedFile, multiSelect: false, selectedFiles: [], //openTone3000Dialog: false, openTone3000Help: false, openGuitarMlHelp: false }; this.requestScroll = true; } getFullScreen() { return document.documentElement.clientWidth < 450 || document.documentElement.clientHeight < 450; } hProgressTimeout: number | null = null; private scrollRef: HTMLDivElement | null = null; cancelProgressTimeout() { if (this.hProgressTimeout !== null) { window.clearTimeout(this.hProgressTimeout); this.hProgressTimeout = 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; transformDragPoint(element: HTMLElement, point: Point): Point { // STUB: will have to deal with scroll offset at some point. return point; } private requestFiles(navPath: string) { if (!this.props.open) { return; } if (this.props.fileProperty.directory === "") { return; } this.setState({ loading: true, showProgress: false, fileResult: new FileRequestResult(), hasFileSelection: false, hasSelection: false, }); this.hProgressTimeout = window.setTimeout(() => { if (this.mounted) { this.setState({ showProgress: true }); } }, 1000); this.model.requestFileList2(navPath, this.props.fileProperty) .then((filesResult) => { if (this.mounted) { this.cancelProgressTimeout(); filesResult.files.splice(0, 0, { pathname: "", displayName: "", isDirectory: false, isProtected: true }); let fileEntry = this.getFileEntry(filesResult.files, this.state.selectedFile); this.setState({ loading: false, showProgress: false, isProtectedDirectory: filesResult.isProtected, fileResult: filesResult, hasSelection: !!fileEntry, hasFileSelection: !!fileEntry && (!fileEntry.isDirectory) && (!fileEntry.isProtected), selectedFileProtected: fileEntry ? fileEntry.isProtected : true, navDirectory: navPath, currentDirectory: filesResult.currentDirectory }); } }).catch((error) => { this.cancelProgressTimeout(); if (!this.mounted) return; if (navPath !== "") // deleted sample directory maybe? { this.navigate(""); } else { this.setState({ loading: false, showProgress: false, fileResult: new FileRequestResult(), isProtectedDirectory: false, navDirectory: "", currentDirectory: "" }); this.model.showAlert(error.toString()) } }); } private lastDivRef: HTMLDivElement | null = null; longPressStartPoint: Point | null = null; maxPosition: number = 0; handleMultisSelectClose() { this.setState({ multiSelect: false, selectedFiles: [], menuMultiselectAnchorEl: null }); } handleLongPressStart( currentTarget: HTMLButtonElement, e: React.PointerEvent | React.MouseEvent, fileEntry: FileEntry): boolean { if (this.state.reordering) { if (!this.isTracksDirectory()) { return false; } let element = currentTarget; let index = parseInt(element.getAttribute("data-position") as string, 10); if (this.listContainerElementRef) { this.longPressStartPoint = screenToClient(currentTarget, e); let dragState: DragState = { activeItem: fileEntry.pathname, dragElement: element, height: element.clientHeight, dragElementDy: 0, from: index, to: index, lastMousePoint: { ...this.longPressStartPoint } }; this.setState({ dragState: dragState }); } return true; } else if (!this.state.multiSelect) { if (fileEntry.isProtected) { return false; } this.setState( { multiSelect: true, selectedFiles: [fileEntry.pathname], } ); return true; } return false; } dragTarget: HTMLButtonElement | null = null; updateReorderPosition(element: HTMLButtonElement, point: Point) { if (this.longPressStartPoint) { let dragDy = point.y - this.longPressStartPoint.y; let height = element.clientHeight; let dragFromPosition = parseInt(element.getAttribute("data-position") as string, 10); let elementOffset = dragFromPosition * height; let newOffset = elementOffset + dragDy; if (newOffset < 0) { dragDy = -elementOffset; newOffset = 0; } let maxOffset = (this.maxPosition - 1) * height; if (newOffset > maxOffset) { dragDy = maxOffset - elementOffset; newOffset = maxOffset; } let toPosition = Math.floor((newOffset + height / 2) / height); if (toPosition < 0) { toPosition = 0; } if (toPosition > this.maxPosition) { toPosition = this.maxPosition; } // console.log("drag from " + dragFromPosition + " to " + toPosition + " dy: " + dragDy); this.setState({ dragState: { activeItem: element.getAttribute("data-pathname") || "", dragElement: element, height: height, dragElementDy: dragDy, from: dragFromPosition, to: toPosition, lastMousePoint: point } }); } } handleLongPressMove(e: React.PointerEvent) { if (this.state.reordering) { if (!this.isTracksDirectory()) { return; } if (this.listContainerElementRef) { let element = e.target as HTMLButtonElement; let point = screenToClient(e.currentTarget, e); this.updateReorderPosition(element, point); this.startAutoScroll() } } } handleReorderFiles(from: number, to: number) { // Update the local state with the new order so that we don't // flash the old order while waiting for a server update. let oldFileResult = this.state.fileResult; let newFileResult = new FileRequestResult(); newFileResult.files = oldFileResult.files.slice(); newFileResult.isProtected = oldFileResult.isProtected; newFileResult.currentDirectory = oldFileResult.currentDirectory; newFileResult.breadcrumbs = oldFileResult.breadcrumbs.slice(); let ixFrom = -1; let ixTo = -1; let ix = 0; for (let i = 0; i < newFileResult.files.length; ++i) { let fileEntry = newFileResult.files[i]; if (fileEntry.metadata) { if (ix === from) { ixFrom = i; } if (ix === to) { ixTo = i; } ++ix; } } if (ixTo === -1 || ixFrom === -1) { return; } if (ixTo == ixFrom) return; if (ixTo < ixFrom) { // move up. newFileResult.files.splice(ixTo, 0, newFileResult.files[ixFrom]); newFileResult.files.splice(ixFrom + 1, 1); } else { // move down. newFileResult.files.splice(ixTo + 1, 0, newFileResult.files[ixFrom]); newFileResult.files.splice(ixFrom, 1); } this.setState({ fileResult: newFileResult, dragState: null }); // Now send the reorder request to the server. this.model.moveAudioFile( this.state.currentDirectory, from, to); } handleLongPressEnd(e: React.PointerEvent | React.MouseEvent, fileEntry: FileEntry) { if (this.state.reordering) { if (!this.isTracksDirectory()) { return; } let dragState = this.state.dragState; if (dragState && dragState.to != dragState.from) { this.handleReorderFiles(dragState.from, dragState.to); } // prevent onClick from firing. e.preventDefault(); e.stopPropagation(); this.setState({ dragState: null }); this.stopAutoScroll(); } else if (!this.state.multiSelect) { let selectedFile = fileEntry.pathname; if (selectedFile === "") { return; } this.setState({ multiSelect: true, selectedFiles: [selectedFile], }); e.stopPropagation(); e.preventDefault(); } } 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, windowHeight: height }) 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() { this.stopAutoScroll(); this.cancelProgressTimeout(); 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 ) { 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, copyDialogOpen: 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; } handleFileClick(event: React.MouseEvent, fileEntry: FileEntry) { if (this.state.reordering) { return; } if (event.ctrlKey || this.state.multiSelect) { if (fileEntry.isProtected) { // cannot be multiselected. return; } if (this.state.multiSelect) { let selectedFiles = this.state.selectedFiles.slice(); let ix = selectedFiles.indexOf(fileEntry.pathname); if (ix === -1) { selectedFiles.push(fileEntry.pathname); } else { selectedFiles.splice(ix, 1); } if (selectedFiles.length === 0) { this.setState({ multiSelect: false, selectedFiles: [] }); } else { this.setState({ multiSelect: true, selectedFiles: selectedFiles }); } } else { let selectedFiles = [fileEntry.pathname]; if (this.state.hasSelection) { if (this.state.selectedFile !== "") { selectedFiles.push(this.state.selectedFile); } } this.setState({ multiSelect: true, selectedFiles: selectedFiles }); } event.stopPropagation(); event.preventDefault(); return; } if (event.shiftKey) { event.stopPropagation(); event.preventDefault(); return; } this.requestScroll = true; if (!fileEntry.isDirectory) { if (!this.isFolderArtwork(fileEntry.pathname)) { 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) { if (this.state.reordering) { return; } this.openSelectedFile(); } handleMenuOpen(event: React.MouseEvent) { this.setState({ menuAnchorEl: event.currentTarget }); } handleMenuClose() { this.setState({ menuAnchorEl: null }); } handleMenuMultiSelectOpen(event: React.MouseEvent) { this.setState({ menuMultiselectAnchorEl: event.currentTarget }); } handleMenuMultiSelectClose() { this.setState({ menuMultiselectAnchorEl: null }); } canDeleteEntry(fileEntry: FileEntry): boolean { return fileEntry.isProtected; } handleDelete() { if (this.state.selectedFileProtected && !this.state.multiSelect) { return; } this.setState({ openConfirmDeleteDialog: true }); } handleDownloadFile() { if (this.state.selectedFileProtected || !this.state.hasFileSelection) { return; } let file = this.state.selectedFile; this.model.downloadAudioFile(file); } async handleConfirmDelete() { this.setState({ openConfirmDeleteDialog: false }); let files: string[] = []; if (this.state.multiSelect) { files = this.state.selectedFiles; } else { if (this.state.selectedFileProtected) { return; } files = [this.state.selectedFile]; } let selectedFile = (!this.state.multiSelect && this.state.hasSelection) ? this.state.selectedFile : undefined; let newSelectedFiles = this.state.selectedFiles.slice(); let multiSelect = this.state.multiSelect; let resultFiles = this.state.fileResult.files; try { for (let file of files) { if (!multiSelect) { if (selectedFile) { let position = -1; for (let i = 0; i < resultFiles.length; ++i) { let file = resultFiles[i]; if (file.pathname === selectedFile) { position = i; break; } } if (position >= 0 && position < resultFiles.length - 1) { selectedFile = this.state.fileResult.files[position + 1].pathname; } else if (position === this.state.fileResult.files.length - 1) { if (position !== 0) { selectedFile = this.state.fileResult.files[position - 1].pathname; } else { selectedFile = undefined; } } else { selectedFile = undefined; } } } else { let ix = newSelectedFiles.indexOf(file); if (ix >= 0) { newSelectedFiles.splice(ix, 1); } } await this.model.deleteUserFile(file); } } catch (e: any) { this.model.showAlert(e); } // update state for what we've done. if (!this.mounted) { return; } if (multiSelect) { if (newSelectedFiles.length === 0) { this.setState({ multiSelect: false, selectedFiles: [], }); } else { this.setState({ multiSelect: true, selectedFiles: newSelectedFiles, }); } } else { if (selectedFile) { this.setState({ selectedFile: selectedFile }); } } this.requestFiles(this.state.navDirectory); } navigate(relativeDirectory: string) { this.requestFiles(relativeDirectory); this.setState({ navDirectory: relativeDirectory }); } handleBreadcrumbNavigate(relativeDirectory: string) { if (!this.state.reordering && !this.state.multiSelect) { this.navigate(relativeDirectory); } } getCompactTrackTitle(fileEntry: FileEntry): string { let metadata = fileEntry.metadata; let title = getTrackTitle(fileEntry.pathname, metadata); if (!metadata) { return title; } if (metadata.album !== "") { title += " (" + metadata.album + ")"; } return title; } getAlbumTitle(fileEntry: FileEntry): string { let artist = fileEntry.metadata?.artist || ""; if (artist == "") { artist = fileEntry.metadata?.albumArtist || ""; } let album = fileEntry.metadata?.album || ""; let joiner = (artist !== "" && album !== "") ? " - " : ""; return album + joiner + artist; } getTrackThumbnail(fileEntry: FileEntry): string { return getAlbumArtUri(this.model, fileEntry.metadata, fileEntry.pathname); } 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, largeIcon: boolean) { let style = largeIcon ? { flex: "0 0 auto", opacity: 0.7, width: 32, height: 32, marginLeft: 16, marginRight: 24, marginTop: 16, marginBottom: 16, } : { flex: "0 0 auto", opacity: 0.7, marginRight: 8, marginLeft: 8 }; if (fileEntry.pathname === "") { return (); } if (fileEntry.isDirectory) { return ( ); } if (isAudioFile(fileEntry.pathname)) { return (); } return (); } private listContainerElementRef: HTMLDivElement | null = null; private scrollContainerElementRef: HTMLDivElement | null = null; private autoScrollTimer: number | null = null; handleAutoScrollTick() { let dragState = this.state.dragState; if (!dragState) return; let scrollContainer = this.scrollContainerElementRef; if (!scrollContainer) { return; } let scrollBounds_ = scrollContainer.getBoundingClientRect(); let scrollClientTop = scrollBounds_.top; let scrollClientBottom = scrollClientTop + scrollContainer.clientHeight; let dragBounds = dragState.dragElement?.getBoundingClientRect(); let dy = 0; let y = scrollContainer.scrollTop; if (dragBounds.top < scrollClientTop + AUTOSCROLL_THRESHOLD) { dy = -AUTOSCROLL_SCROLL_RATE; y = scrollContainer.scrollTop + dy; if (y < 0) y = 0; dy = y - scrollContainer.scrollTop; } else if (dragBounds.bottom > scrollClientBottom - AUTOSCROLL_THRESHOLD) { dy = AUTOSCROLL_SCROLL_RATE; y = scrollContainer.scrollTop + dy; let maxScrollTop = scrollContainer.scrollHeight - scrollContainer.clientHeight; if (y > maxScrollTop) { y = maxScrollTop; } dy = y - scrollContainer.scrollTop; } else { return; } if (this.longPressStartPoint === null) { return; } if (dy === 0) { return; } this.longPressStartPoint = { x: this.longPressStartPoint.x, y: this.longPressStartPoint.y - dy }; let dragElementDy = dragState.lastMousePoint.y - this.longPressStartPoint.y; let originalY = dragState.from * dragState.height; let newY = originalY + dragElementDy; if (newY < 0) { newY = 0; dragElementDy = -originalY; } let maxDy = (this.maxPosition - 1) * dragState.height; if (newY > maxDy) { newY = maxDy; dragElementDy = maxDy - originalY; } let newTo = Math.floor((newY + dragState.height / 2) / dragState.height); let newDragState = { ...dragState }; newDragState.dragElementDy = dragElementDy; newDragState.to = newTo; // console.log("autoscroll from " + dragState.from + " to " + newTo + " dy: " + dragElementDy); // avoid jitter. newDragState.dragElement.style.top = (newDragState.dragElementDy + 5) + "px"; this.setState({ dragState: newDragState }); scrollContainer.scrollTo({ left: 0, top: y, behavior: "instant" }); this.startAutoScroll(); } stopAutoScroll() { if (this.autoScrollTimer !== null) { clearTimeout(this.autoScrollTimer); this.autoScrollTimer = null; } } startAutoScroll() { this.stopAutoScroll(); this.autoScrollTimer = setTimeout( () => { this.handleAutoScrollTick() }, AUTOSCROLL_TICK_DELAY); } // handleTone3000Dialog(e: React.MouseEvent) { // e.stopPropagation(); // e.preventDefault(); // this.setState({ openTone3000Dialog: true }); // } handleGuitarMlHelp(e: React.MouseEvent) { e.stopPropagation(); e.preventDefault(); this.setState({ openGuitarMlHelp: true }); } handleTone3000Help(e: React.MouseEvent) { e.stopPropagation(); e.preventDefault(); this.setState({ openTone3000Help: true }); } render() { const isTracksDirectory = this.isTracksDirectory(); const isToobNamModelFile = this.props.fileProperty.patchProperty === ToobNamModelFileUrl; const isToobMLModelFile = this.props.fileProperty.patchProperty === ToobMlModelFileUrl; 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 canMove = this.hasSelectedFileOrFolder() && !protectedItem; let canRename = this.hasSelectedFileOrFolder() && !protectedItem && !isTracksDirectory; let canReorder = isTracksDirectory; let needsDivider = canMove || canRename || canReorder; let trackPosition = 0; let compactVertical = this.state.windowHeight < 700; let canSelectFile = this.state.hasSelection && !this.isFolderArtwork(this.state.selectedFile); return this.props.open && ( { if (this.state.reordering) { this.setState({ reordering: false, dragState: null }); } else { this.props.onCancel(); } }} onEnterKey={() => { if (this.state.reordering) { this.setState({ reordering: false, dragState: null }); } else if (this.state.multiSelect) { this.setState({ multiSelect: false, selectedFiles: [] }); } else { this.openSelectedFile(); } }} open={this.props.open} tag="fileProperty" fullWidth maxWidth="md" PaperProps={ this.state.fullScreen ? {} : { style: { minHeight: "90%", maxHeight: "90%", } }} > {this.state.reordering && ( { this.setState({ reordering: false, dragState: null }); } } > Reorder files )} {this.state.multiSelect && ( { this.handleMultisSelectClose(); }} > {this.state.selectedFiles.length.toString() + " files selected."} { this.handleMenuMultiSelectOpen(ev); }} disabled={protectedDirectory} > { this.handleMenuMultiSelectClose(); }} > {canMove && ( { this.handleMenuMultiSelectClose(); this.handleMove(); }}>Move)} {canMove && ( { this.handleMenuMultiSelectClose(); this.handleCopy(); }}>Copy)} )} {!(this.state.reordering || this.state.multiSelect) && ( <> { this.props.onCancel(); }} > {this.props.fileProperty.label} { this.onNewFolder(); }} disabled={protectedDirectory} > { this.handleMenuOpen(ev); }} disabled={protectedDirectory} > { this.handleMenuClose(); }} > { this.handleMenuClose(); this.onNewFolder(); }}>New folder {(needsDivider) && ()} {canMove && ( { this.handleMenuClose(); this.handleMove(); }}>Move)} {canMove && ( { this.handleMenuClose(); this.handleCopy(); }}>Copy)} {canRename && this.hasSelectedFileOrFolder() && !protectedItem && ( { this.handleMenuClose(); this.onRename(); }}>Rename)} {canReorder && ( { this.handleMenuClose(); this.onReorder(); }}>Reorder)} )}
{this.renderBreadcrumbs()}
{ this.scrollContainerElementRef = element; }} >
{this.state.showProgress && ( <>
)}
{ this.listContainerElementRef = element; this.onMeasureRef(element); }} style={{ flex: "1 1 100%", display: "flex", flexFlow: "row wrap", position: "relative", justifyContent: "flex-start", alignContent: "flex-start", paddingLeft: 16, paddingBottom: 16, paddingTop: 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) => { if (this.state.reordering && !value.metadata) { return null; // don't render non-track files when reordering. } if (this.state.reordering && this.isFolderArtwork(value.pathname)) { return null; } let displayValue = value.displayName; if (displayValue === "") { displayValue = ""; } let dataPosition = ""; let myTrackPosition = trackPosition; if (value.metadata && value.metadata.track) { dataPosition = trackPosition.toString(); ++trackPosition; this.maxPosition = trackPosition; } let selected = false; if (this.state.reordering) { selected = false; } else if (this.state.multiSelect) { selected = this.state.selectedFiles.indexOf(value.pathname) !== -1; } else { 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) }; } let dragOffset = 0; let dragLeft = 0; let dragState = this.state.dragState; let zIndex: number | undefined = undefined; let background: string | undefined = undefined; if (dragState && value.metadata) { if (myTrackPosition !== dragState.from) { if (myTrackPosition >= dragState.to && myTrackPosition < dragState.from) { dragOffset = dragState.height; } else if (myTrackPosition > dragState.from && myTrackPosition <= dragState.to) { dragOffset = -dragState.height; } } else { dragOffset = dragState.dragElementDy + 5; dragLeft = 5; zIndex = 1000; background = isDarkMode() ? "#555" : "#EEF"; } } let dragButtonStyle: React.CSSProperties = { width: columnWidth, flex: "0 0 auto", height: (value.metadata && !compactVertical) ? 64 : 48, position: "relative", top: dragOffset, left: dragLeft, zIndex: zIndex, background: background, }; return ( this.handleFileClick(e, value)} onDoubleClick={() => { this.onDoubleClickValue(value.pathname); }} onLongPressEnd={(e) => { this.handleLongPressEnd(e, value); } } onLongPressStart={(currentTarget, e) => { return this.handleLongPressStart(currentTarget, e, value); } } onLongPressMove={(e) => { this.handleLongPressMove(e); } } >
{value.metadata ? (!compactVertical ? (
{ this.isFolderArtwork(value.pathname) ? ( { e.preventDefault(); }} src={this.getTrackThumbnail(value)} style={{ width: 24, height: 24, margin: 20, borderRadius: 4 }} /> ) : ( { e.preventDefault(); }} src={this.getTrackThumbnail(value)} style={{ width: 48, height: 48, margin: 8, borderRadius: 4 }} /> ) }
{getTrackTitle(value.pathname, value.metadata)} {this.getAlbumTitle(value)}
) : (
{ e.preventDefault(); }} src={this.getTrackThumbnail(value)} style={{ width: 24, height: 24, margin: 8, borderRadius: 4 }} />
{this.getCompactTrackTitle(value)}
) ) : (
{this.getIcon(value, this.isTracksDirectory() && !compactVertical)} {displayValue}
)} ); } ) }
{(!this.state.reordering && !this.state.multiSelect) && ( <>
{isToobNamModelFile && (
Download model files from TONE3000 { this.handleTone3000Help(e); }} aria-label="help" edge="end" color="inherit" style={{ opacity: 0.6, marginLeft: 8 }} >
)} {isToobMLModelFile && (
Download model files from GuitarML { this.handleGuitarMlHelp(e); }} aria-label="help" edge="end" color="inherit" style={{ opacity: 0.6, marginLeft: 8 }} >
)} {this.state.windowWidth > 500 ? (
this.handleDelete()} > } onClick={() => { this.setState({ openUploadFileDialog: true }) }} disabled={protectedDirectory} >
Upload
} onClick={() => { this.handleDownloadFile(); }} disabled={protectedDirectory || !this.state.hasFileSelection} >
Download
 
) : (
this.handleDelete()} >
 
 
)}
)} { this.setState({ openUploadFileDialog: false }); } } isTracksDirectory={this.isTracksDirectory()} uploadPage={ "uploadUserFile?directory=" + encodeURIComponent(this.state.currentDirectory) + "&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.handleConfirmCopy(); }} onClose={() => { this.setState({ confirmCopyDialogState: null }); }} /> { this.state.newFolderDialogOpen && ( { this.setState({ newFolderDialogOpen: false }); this.onExecuteNewFolder(newName) }} onClose={() => { this.setState({ newFolderDialogOpen: false }); }} title="New folder" acceptActionName="OK" /> ) } { this.state.renameDialogOpen && ( { this.setState({ renameDialogOpen: false }); this.onExecuteRename(newName) }} onClose={() => { this.setState({ renameDialogOpen: false }); }} acceptActionName="OK" /> ) } { (this.state.moveDialogOpen || this.state.copyDialogOpen) && ( ( { this.setState({ moveDialogOpen: false, copyDialogOpen: false }); }} onOk={ (path) => { this.setState({ moveDialogOpen: false, copyDialogOpen: false }); this.setDefaultPath(path); if (this.state.copyDialogOpen) { this.onExecuteCopy(path); } else { this.onExecuteMove(path); } } } /> ) ) } {/* {this.state.openTone3000Dialog && ( this.setState({ openTone3000Dialog: false })} /> )} */} {this.state.openTone3000Help && ( this.setState({ openTone3000Help: false })} /> )} {this.state.openGuitarMlHelp && ( this.setState({ openGuitarMlHelp: false })} /> )} ); } openSelectedFile(): void { if (this.state.multiSelect || this.state.reordering) { return; } if (this.isFolderArtwork(this.state.selectedFile)) { return; } 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 handleMove(): void { this.setState({ moveDialogOpen: true }); } private handleCopy(): void { this.setState({ copyDialogOpen: true }); } private async onExecuteMoveMulti(newDirectory: string) { let files = this.state.selectedFiles.slice(); let newFileList = this.state.selectedFiles.slice(); try { for (let file of files) { let fileName = pathFileName(file); let oldFilePath = pathConcat(this.state.navDirectory, fileName); let newFilePath = pathConcat(newDirectory, fileName); await this.model.renameFilePropertyFile(oldFilePath, newFilePath, this.props.fileProperty); let index = newFileList.indexOf(file); if (index !== -1) { newFileList.splice(index, 1); } } } catch (e) { this.model.showAlert(e); } // update state to refelect what we did. if (!this.mounted) { return; } if (newFileList.length === 0) { this.setState({ multiSelect: false, selectedFiles: [] }); } else { this.setState({ multiSelect: true, selectedFiles: newFileList, }); } this.requestFiles(this.state.navDirectory); } private onExecuteMove(newDirectory: string) { if (this.state.multiSelect) { this.onExecuteMoveMulti(newDirectory); return; } 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 handleConfirmCopy() { if (this.state.confirmCopyDialogState) { this.model.copyFilePropertyFile( this.state.confirmCopyDialogState.oldFilePath, this.state.confirmCopyDialogState.newFilePath, this.props.fileProperty, true) .then((filename) => { this.requestFiles(this.state.navDirectory); }) .catch((e) => { this.model.showAlert(e.toString()); }); this.setState({ confirmCopyDialogState: null }); } } private async onExecuteCopyMulti(newDirectory: string) { let files = this.state.selectedFiles.slice(); let fileProperty = this.props.fileProperty; try { for (let fileName of files) { let oldFilePath = pathConcat(this.state.navDirectory, fileName); let newFilePath = pathConcat(newDirectory, fileName); await this.model.copyFilePropertyFile(oldFilePath, newFilePath, fileProperty, true); } } catch (e) { this.model.showAlert(e); } // update state to reflect what we did. if (!this.mounted) { return; } this.requestFiles(this.state.navDirectory); } private onExecuteCopy(newDirectory: string) { this.setState({ copyDialogOpen: false }); if (this.state.multiSelect) { this.onExecuteCopyMulti(newDirectory); return; } let fileName = pathFileName(this.state.selectedFile); let oldFilePath = pathConcat(this.state.navDirectory, fileName); let newFilePath = pathConcat(newDirectory, fileName); this.model.copyFilePropertyFile(oldFilePath, newFilePath, this.props.fileProperty, false) .then((filename) => { if (filename === "") { // the file already exists. prompt for overrwrite. this.setState({ confirmCopyDialogState: { fileName: fileName, oldFilePath: oldFilePath, newFilePath } }); } else { this.requestFiles(this.state.navDirectory); this.setState({ selectedFile: filename, selectedFileIsDirectory: this.isDirectory(filename), selectedFileProtected: false }); this.requestScroll = true; } }) .catch((e) => { this.model.showAlert(e.toString()); }); } private onRename(): void { this.setState({ renameDialogOpen: true }); } private onReorder(): void { this.setState({ reordering: 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);