diff --git a/react/src/FilePropertyControl.tsx b/react/src/FilePropertyControl.tsx index 00e66ae..a3d987b 100644 --- a/react/src/FilePropertyControl.tsx +++ b/react/src/FilePropertyControl.tsx @@ -29,7 +29,7 @@ import createStyles from '@mui/styles/createStyles'; import withStyles from '@mui/styles/withStyles'; import { UiFileProperty } from './Lv2Plugin'; import Typography from '@mui/material/Typography'; -import { PiPedalModel, PiPedalModelFactory, ListenHandle} from './PiPedalModel'; +import { PiPedalModel, PiPedalModelFactory, ListenHandle,State} from './PiPedalModel'; import ButtonBase from '@mui/material/ButtonBase' import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; import {PedalboardItem} from './Pedalboard'; @@ -98,8 +98,18 @@ const FilePropertyControl = value: "", }; this.model = PiPedalModelFactory.getInstance(); + this.onStateChanged = this.onStateChanged.bind(this); } + + onStateChanged(state: State) { + if (this.mounted) + { + if (state === State.Ready) { + this.subscribeToPatchProperty(); + } + } + } private propertyGetHandle?: ListenHandle; monitorPropertyHandle?: ListenHandle; @@ -119,6 +129,7 @@ const FilePropertyControl = } } ); + } unsubscribeToPatchProperty() { if (this.monitorPropertyHandle !== undefined) @@ -126,15 +137,18 @@ const FilePropertyControl = this.model.cancelMonitorPatchProperty(this.monitorPropertyHandle); this.monitorPropertyHandle = undefined; } + } private mounted: boolean = false; componentDidMount() { + this.model.state.addOnChangedHandler(this.onStateChanged); this.mounted = true; this.subscribeToPatchProperty(); } componentWillUnmount() { this.unsubscribeToPatchProperty(); this.mounted = false; + this.model.state.removeOnChangedHandler(this.onStateChanged); } componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any): void { if (prevProps.fileProperty.patchProperty !== this.props.fileProperty.patchProperty diff --git a/react/src/FilePropertyDialog.tsx b/react/src/FilePropertyDialog.tsx index 2784912..72c1b6f 100644 --- a/react/src/FilePropertyDialog.tsx +++ b/react/src/FilePropertyDialog.tsx @@ -18,7 +18,7 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import React from 'react'; - +import { Theme, createStyles } from '@mui/material/styles'; import RenameDialog from './RenameDialog'; import Divider from '@mui/material/Divider'; import MenuItem from '@mui/material/MenuItem'; @@ -38,6 +38,8 @@ import DialogTitle from '@mui/material/DialogTitle'; import IconButton from '@mui/material/IconButton'; import OldDeleteIcon from './OldDeleteIcon'; import Toolbar from '@mui/material/Toolbar'; +import { withStyles, WithStyles } from '@mui/styles'; + import ResizeResponsiveComponent from './ResizeResponsiveComponent'; import { UiFileProperty } from './Lv2Plugin'; @@ -48,7 +50,18 @@ import UploadFileDialog from './UploadFileDialog'; import OkCancelDialog from './OkCancelDialog'; import Breadcrumbs from '@mui/material/Breadcrumbs'; import HomeIcon from '@mui/icons-material/Home'; -export interface FilePropertyDialogProps { +import FilePropertyDirectorySelectDialog from './FilePropertyDirectorySelectDialog'; + + + +const styles = (theme: Theme) => createStyles({ + secondaryText: { + color: theme.palette.text.secondary + }, +}); + + +export interface FilePropertyDialogProps extends WithStyles { open: boolean, fileProperty: UiFileProperty, selectedFile: string, @@ -70,6 +83,7 @@ export interface FilePropertyDialogState { menuAnchorEl: null | HTMLElement; newFolderDialogOpen: boolean; renameDialogOpen: boolean; + moveDialogOpen: boolean; }; function pathExtension(path: string) @@ -85,7 +99,7 @@ function pathExtension(path: string) return path.substring(dotPos); // include the '.'. } -function concatPath(left: string, right: string) { +function pathConcat(left: string, right: string) { if (left === "") return right; if (right === "") return left; if (left.endsWith('/')) { @@ -123,7 +137,9 @@ export function pathFileName(path: string): string { return path.substring(slashPos); } -export default class FilePropertyDialog extends ResizeResponsiveComponent { + +export default withStyles(styles, { withTheme: true })( + class FilePropertyDialog extends ResizeResponsiveComponent { getNavDirectoryFromFile(selectedFile: string, fileProperty: UiFileProperty): string { @@ -160,7 +176,8 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent {lastdirectory} + {lastdirectory} )); } @@ -380,13 +402,35 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent this.props.onCancel()} open={this.props.open} tag="fileProperty" fullWidth maxWidth="xl" style={{ height: "90%" }} @@ -403,7 +447,7 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent - + {this.props.fileProperty.label} { this.handleMenuClose(); this.onNewFolder(); }}>New folder {this.hasSelectedFileOrFolder() && ()} - {this.hasSelectedFileOrFolder() && ( { this.handleMenuClose(); }}>Move)} + {this.hasSelectedFileOrFolder() && ( { this.handleMenuClose();this.onMove(); }}>Move)} {this.hasSelectedFileOrFolder() && ( { this.handleMenuClose();this.onRename(); }}>Rename)} @@ -484,7 +528,7 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent ))} - {displayValue} + {displayValue} ); @@ -566,13 +610,36 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent {this.setState({moveDialogOpen: false});}} + onOk={ + (path) => { + this.setState({moveDialogOpen: false}); + this.setDefaultPath(path); + this.onExecuteMove(path); + } + } + + /> + ) + ) + } ); } openSelectedFile(): void { if (this.isDirectory(this.state.selectedFile)) { let directoryName = pathFileNameOnly(this.state.selectedFile); - let navDirectory = concatPath(this.state.navDirectory, directoryName); + let navDirectory = pathConcat(this.state.navDirectory, directoryName); this.requestFiles(navDirectory); this.setState({ navDirectory: navDirectory }); } else { @@ -590,6 +657,25 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent{ + this.requestFiles(this.state.navDirectory); + }) + .catch((e)=>{ + this.model.showAlert(e.toString()); + }); + + } + private onRename(): void { this.setState({ renameDialogOpen: true }); } @@ -600,16 +686,16 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent{ this.setState({selectedFile: newPath}); this.requestFiles(this.state.navDirectory); @@ -626,7 +712,7 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent { this.setState({selectedFile: newPath}); this.requestFiles(this.state.navDirectory); @@ -637,4 +723,4 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent void, + onClose: () => void +}; + +export interface FilePropertyDirectorySelectDialogState { + fullScreen: boolean; + selectedPath: string; + directoryTree?: DirectoryTree; + hasSelection: boolean; + directoryTreeInvalidatecount: number; + +}; + +export default class FilePropertyDirectorySelectDialog extends ResizeResponsiveComponent { + + private model: PiPedalModel; + private refSelected: React.Ref; + + constructor(props: FilePropertyDirectorySelectDialogProps) { + super(props); + this.model = PiPedalModelFactory.getInstance(); + this.state = { + fullScreen: false, + selectedPath: props.defaultPath, + directoryTree: undefined, + hasSelection: false, + directoryTreeInvalidatecount: 0 + + }; + this.refSelected = React.createRef(); + + this.requestDirectoryTree(); + } + mounted: boolean = false; + + + + onWindowSizeChanged(width: number, height: number): void { + this.setState({ fullScreen: height < 200 }) + } + + + componentDidMount() { + super.componentDidMount(); + this.mounted = true; + } + componentWillUnmount() { + super.componentWillUnmount(); + this.mounted = false; + } + + private requestScroll = true; + requestDirectoryTree() { + if (!this.props.open) return; + + this.model.getFilePropertyDirectoryTree(this.props.uiFileProperty) + .then((filePropertyDirectoryTree) => { + let myTree = new DirectoryTree(filePropertyDirectoryTree); + if (this.props.excludeDirectory) + { + myTree.remove(this.props.excludeDirectory); + } + myTree.expand(this.state.selectedPath); + + let hasSelection = myTree.find(this.state.selectedPath) != null; + this.setState({ directoryTree: myTree, hasSelection: hasSelection }); + this.requestScroll = true; + }) + .catch((e) => { + this.model.showAlert(e.toString()); + this.setState({ hasSelection: false }); + }); + + } + + componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot: any): void { + super.componentDidUpdate?.(prevProps, prevState, snapshot); + + if (prevProps.open !== this.props.open) { + if (this.props.open) { + this.setState({ selectedPath: this.props.defaultPath, hasSelection: false }); + this.requestDirectoryTree(); + this.requestScroll = true; + } + } + } + + + onOK() { + if (this.state.hasSelection) { + this.props.onOk(this.state.selectedPath); + } + } + onClose() { + this.props.onClose(); + } + private onToggleExpand(directoryTree: DirectoryTree) + { + directoryTree.expanded = !directoryTree.expanded; + this.setState({directoryTreeInvalidatecount: this.state.directoryTreeInvalidatecount+1}); + } + private onTreeClick(directoryTree: DirectoryTree) { + if (!this.state.directoryTree) return; + this.state.directoryTree.expand(directoryTree.path); + this.setState({ selectedPath: directoryTree.path, hasSelection: true, directoryTreeInvalidatecount: this.state.directoryTreeInvalidatecount + 1 }); + } + private renderTree_(directoryTree: DirectoryTree) { + let ref: ((element: HTMLButtonElement | null) => void) | undefined = undefined; + let selected = directoryTree.path === this.state.selectedPath; + if (selected) { + ref = (element) => { + if (this.requestScroll) { + this.requestScroll = false; + } + + } + } + 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 showToggle = directoryTree.canExpand() && directoryTree.path.length !== 0; + return ( +
+
+ { this.onToggleExpand(directoryTree); }} + style={{opacity: showToggle ? 1: 0}} + disabled={!showToggle} + > + { + directoryTree.expanded && + ( + + ) + } + { + !directoryTree.expanded && + ( + + ) + } + + { this.onTreeClick(directoryTree); }}> +
+
+ { + directoryTree.path === "" ? ( + + ) : ( + + ) + } + + {directoryTree.name === "" ? "Home" : directoryTree.name} +
+ +
+ {directoryTree.expanded + && directoryTree.canExpand() + && ( +
+ { + directoryTree.children.map( + (child, index) => { + return this.renderTree_(child); + } + ) + } +
+ ) + } +
+ ); + } + private renderTree() { + if (!this.state.directoryTree) { return undefined; } + return this.renderTree_(this.state.directoryTree); + } + render() { + + + return ( + { this.onClose(); }} aria-labelledby="Rename-dialog-title" + fullScreen={this.state.fullScreen} + style={{ userSelect: "none" }} + PaperProps={{ style: { minHeight: "80%", maxHeight: "80%", minWidth: "75%", maxWidth: "75%", overflowY: "visible" } }} + > + + Select folder + + + +
+ { + this.renderTree() + } +
+
+ + + + + +
+ ); + } +} \ No newline at end of file diff --git a/react/src/FilePropertyDirectoryTree.tsx b/react/src/FilePropertyDirectoryTree.tsx new file mode 100644 index 0000000..4a9191b --- /dev/null +++ b/react/src/FilePropertyDirectoryTree.tsx @@ -0,0 +1,39 @@ +// Copyright (c) 2024 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. + + +export default class FilePropertyDirectoryTree { + deserialize(input: any) : FilePropertyDirectoryTree { + this.directoryName = input.directoryName; + this.children = FilePropertyDirectoryTree.deserialize_array(input.children); + return this; + } + static deserialize_array(input: any): FilePropertyDirectoryTree[] { + let result: FilePropertyDirectoryTree[] = []; + for (let i = 0; i < input.length; ++i) + { + result[i] = new FilePropertyDirectoryTree().deserialize(input[i]); + } + return result; + } + + + directoryName: string = ""; + children: FilePropertyDirectoryTree[] = []; +} \ No newline at end of file diff --git a/react/src/PiPedalModel.tsx b/react/src/PiPedalModel.tsx index a03556c..fad9d26 100644 --- a/react/src/PiPedalModel.tsx +++ b/react/src/PiPedalModel.tsx @@ -39,6 +39,7 @@ import WifiChannel from './WifiChannel'; import AlsaDeviceInfo from './AlsaDeviceInfo'; import { AndroidHostInterface, FakeAndroidHost } from './AndroidHost'; import {ColorTheme, getColorScheme,setColorScheme} from './DarkMode'; +import FilePropertyDirectoryTree from './FilePropertyDirectoryTree'; export enum State { @@ -657,6 +658,11 @@ export class PiPedalModel //implements PiPedalModel } onSocketConnectionLost() { + // remove all the events and subscriptions we have. + // yyy + this.vuSubscriptions = []; + this.monitorPatchPropertyListeners = []; + if (this.isAndroidHosted()) { this.androidHost?.setDisconnected(true); } @@ -922,6 +928,7 @@ export class PiPedalModel //implements PiPedalModel // MUST not allow reconnect until at least one complete load has finished. this.webSocket.canReconnect = true; } + this.setState(State.Ready); return true; }) .catch((error) => { @@ -2064,10 +2071,10 @@ export class PiPedalModel //implements PiPedalModel for (let i = 0; i < this.monitorPatchPropertyListeners.length; ++i) { if (this.monitorPatchPropertyListeners[i].handle === listenHandle._handle) { this.monitorPatchPropertyListeners.splice(i, 1); + this.webSocket?.send("cancelMonitorPatchProperty", listenHandle._handle); break; } } - this.webSocket?.send("cancelMonitorPatchProperty", listenHandle._handle); } @@ -2300,7 +2307,24 @@ export class PiPedalModel //implements PiPedalModel }); } - renameSampleFile(oldRelativePath: string, newRelativePath: string, uiFileProperty: UiFileProperty) : Promise + getFilePropertyDirectoryTree(uiFileProperty: UiFileProperty) : Promise { + return new Promise((resolve, reject) => { + let ws = this.webSocket; + if (!ws) { + resolve(new FilePropertyDirectoryTree()); + return; + } + ws.request( + "getFilePropertyDirectoryTree", + uiFileProperty + ).then((result)=> { + resolve(new FilePropertyDirectoryTree().deserialize(result)); + }).catch((e) => { + reject(e); + }); + }); + } + renameFilePropertyFile(oldRelativePath: string, newRelativePath: string, uiFileProperty: UiFileProperty) : Promise { return new Promise((resolve, reject) => { @@ -2310,7 +2334,7 @@ export class PiPedalModel //implements PiPedalModel return; } ws.request( - "renameSampleFile", + "renameFilePropertyFile", { oldRelativePath: oldRelativePath, newRelativePath: newRelativePath, @@ -2428,7 +2452,6 @@ export class PiPedalModel //implements PiPedalModel .catch((err) => { //resolve(); }); - // yyy resolve(); }); diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index 2bf9432..45f5a6e 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -2052,13 +2052,13 @@ std::vector PiPedalModel::GetFileList2(const std::string &relativePat } } -std::string PiPedalModel::RenameSampleFile( +std::string PiPedalModel::RenameFilePropertyFile( const std::string&oldRelativePath, const std::string&newRelativePath, const UiFileProperty&uiFileProperty) { std::lock_guard lock(mutex); - return storage.RenameSampleFile(oldRelativePath,newRelativePath,uiFileProperty); + return storage.RenameFilePropertyFile(oldRelativePath,newRelativePath,uiFileProperty); } void PiPedalModel::DeleteSampleFile(const std::filesystem::path &fileName) @@ -2073,10 +2073,10 @@ std::string PiPedalModel::CreateNewSampleDirectory(const std::string&relativePat return storage.CreateNewSampleDirectory(relativePath, uiFileProperty); } -FilePropertyDirectoryTree::ptr PiPedalModel::GetSampleDirectoryTree(const UiFileProperty&uiFileProperty) +FilePropertyDirectoryTree::ptr PiPedalModel::GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty) { std::lock_guard lock(mutex); - return storage.GetSampleDirectoryTree(uiFileProperty); + return storage.GetFilePropertydirectoryTree(uiFileProperty); } diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index a73b254..8b2aef0 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -333,8 +333,8 @@ namespace pipedal void DeleteSampleFile(const std::filesystem::path &fileName); std::string CreateNewSampleDirectory(const std::string&relativePath, const UiFileProperty&uiFileProperty); - std::string RenameSampleFile(const std::string&oldRelativePath,const std::string&newRelativePath,const UiFileProperty&uiFileProperty); - FilePropertyDirectoryTree::ptr GetSampleDirectoryTree(const UiFileProperty&uiFileProperty); + std::string RenameFilePropertyFile(const std::string&oldRelativePath,const std::string&newRelativePath,const UiFileProperty&uiFileProperty); + FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty); std::string UploadUserFile(const std::string &directory, const std::string &patchProperty,const std::string&filename,const std::string&fileBody); uint64_t CreateNewPreset(); diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index 02c675d..4c6cadb 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -1488,18 +1488,18 @@ public: std::string newFileName = this->model.CreateNewSampleDirectory(args.relativePath_,args.uiFileProperty_); this->Reply(replyTo,"createNewSampleDirectory",newFileName); } - else if (message == "renameSampleFile") + else if (message == "renameFilePropertyFile") { RenameSampleFileArgs args; pReader->read(&args); - std::string newFileName = this->model.RenameSampleFile(args.oldRelativePath_,args.newRelativePath_,args.uiFileProperty_); - this->Reply(replyTo,"renameSampleFile",newFileName); - } else if (message == "GetSampleDirectoryTree"){ + std::string newFileName = this->model.RenameFilePropertyFile(args.oldRelativePath_,args.newRelativePath_,args.uiFileProperty_); + this->Reply(replyTo,"renameFilePropertyFile",newFileName); + } else if (message == "getFilePropertyDirectoryTree"){ UiFileProperty uiFileProperty; pReader->read(&uiFileProperty); - FilePropertyDirectoryTree::ptr result = model.GetSampleDirectoryTree(uiFileProperty); - this->Reply(replyTo,"GetSampleDirectoryTree",result); + FilePropertyDirectoryTree::ptr result = model.GetFilePropertydirectoryTree(uiFileProperty); + this->Reply(replyTo,"GetFilePropertydirectoryTree",result); } diff --git a/src/Storage.cpp b/src/Storage.cpp index 2d08a50..959a8ef 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -1748,7 +1748,7 @@ std::string Storage::CreateNewSampleDirectory(const std::string&relativePath, co return path; } -std::string Storage::RenameSampleFile( +std::string Storage::RenameFilePropertyFile( const std::string&oldRelativePath, const std::string&newRelativePath, const UiFileProperty&uiFileProperty) @@ -1789,12 +1789,15 @@ std::string Storage::RenameSampleFile( void Storage::FillSampleDirectoryTree(FilePropertyDirectoryTree*node, const std::filesystem::path&directory) const { - for (auto child: std::filesystem::recursive_directory_iterator(directory)) + for (auto child: std::filesystem::directory_iterator(directory)) { - const auto& childPath = child.path(); - FilePropertyDirectoryTree::ptr childTree = std::make_unique(childPath.filename()); - FillSampleDirectoryTree(childTree.get(),childPath); - node->children_.push_back(std::move(childTree)); + if (child.is_directory()) + { + const auto& childPath = child.path(); + FilePropertyDirectoryTree::ptr childTree = std::make_unique(childPath.filename()); + FillSampleDirectoryTree(childTree.get(),childPath); + node->children_.push_back(std::move(childTree)); + } } std::sort(node->children_.begin(),node->children_.end(), [this](const FilePropertyDirectoryTree::ptr&left,const FilePropertyDirectoryTree::ptr&right) @@ -1802,7 +1805,7 @@ void Storage::FillSampleDirectoryTree(FilePropertyDirectoryTree*node, const std: return this->locale(left->directoryName_,right->directoryName_); }); } -FilePropertyDirectoryTree::ptr Storage::GetSampleDirectoryTree(const UiFileProperty&uiFileProperty) const +FilePropertyDirectoryTree::ptr Storage::GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty) const { FilePropertyDirectoryTree::ptr result = std::make_unique(""); if (uiFileProperty.directory().empty()) diff --git a/src/Storage.hpp b/src/Storage.hpp index a87d9ec..67fec3d 100644 --- a/src/Storage.hpp +++ b/src/Storage.hpp @@ -220,11 +220,11 @@ public: void DeleteSampleFile(const std::filesystem::path &fileName); std::string UploadUserFile(const std::string &directory, const std::string &patchProperty,const std::string&filename,const std::string&fileBody); std::string CreateNewSampleDirectory(const std::string&relativePath, const UiFileProperty&uiFileProperty); - std::string RenameSampleFile( + std::string RenameFilePropertyFile( const std::string&oldRelativePath, const std::string&newRelativePath, const UiFileProperty&uiFileProperty); - FilePropertyDirectoryTree::ptr GetSampleDirectoryTree(const UiFileProperty&uiFileProperty) const; + FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty) const; };