diff --git a/CMakeLists.txt b/CMakeLists.txt index aee3c403..aa0506ef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,10 +1,10 @@ cmake_minimum_required(VERSION 3.16.0) project(pipedal - VERSION 1.3.61 + VERSION 1.3.62 DESCRIPTION "PiPedal Guitar Effect Pedal For Raspberry Pi" HOMEPAGE_URL "https://rerdavies.github.io/pipedal" ) -set (DISPLAY_VERSION "PiPedal v1.3.61-Release") +set (DISPLAY_VERSION "PiPedal v1.3.62-Beta") set (PACKAGE_ARCHITECTURE "arm64") set (CMAKE_INSTALL_PREFIX "/usr/") diff --git a/PiPedalCommon/src/include/util.hpp b/PiPedalCommon/src/include/util.hpp index 588dca88..405b6e26 100644 --- a/PiPedalCommon/src/include/util.hpp +++ b/PiPedalCommon/src/include/util.hpp @@ -51,6 +51,22 @@ namespace pipedal { } + template + inline bool contains(const std::vector&vector,const T&value) + { + for (auto i = vector.begin(); i != vector.end(); ++i) + { + if ((*i) == value) { + return true; + } + } + return false; + } + inline bool contains(const std::vector&vector,const char*value) + { + return contains(vector,std::string(value)); + } + class NoCopy { public: NoCopy() { } diff --git a/README.md b/README.md index b1bf26f1..adee28ec 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,13 @@ -Download: v1.3.61 +Download: v1.3.62 Website: [https://rerdavies.github.io/pipedal](https://rerdavies.github.io/pipedal). Documentation: [https://rerdavies.github.io/pipedal/Documentation.html](https://rerdavies.github.io/pipedal/Documentation.html).   -#### NEW version 1.3.61 Release, providing [snapshots](https://rerdavies.github.io/pipedal/Snaphots.html), and a new Performance View. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details. +#### NEW version 1.3.62 Release, providing [snapshots](https://rerdavies.github.io/pipedal/Snaphots.html), and a new Performance View. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details.   diff --git a/docs/Installing.md b/docs/Installing.md index 2c1f2a5a..104b917a 100644 --- a/docs/Installing.md +++ b/docs/Installing.md @@ -13,17 +13,17 @@ page_icon: img/Install4.jpg Download the most recent Debian (.deb) package for your platform: -- [Raspberry Pi OS bookworm (64-bit) v1.3.61](https://github.com/rerdavies/pipedal/releases/download/) +- [Raspberry Pi OS bookworm (64-bit) v1.3.62](https://github.com/rerdavies/pipedal/releases/download/) - [Ubuntu/Raspberry Pi OS bullseyeye (64-bit) v1.2.31](https://github.com/rerdavies/pipedal/releases/download/v1.1.31/pipedal_1.1.31_arm64.deb) -Version 1.3.61 has not yet been tested on Ubuntu or Raspberry Pi OS bullseye. On these platforms, we recommend that you use version 1.1.31. +Version 1.3.62 has not yet been tested on Ubuntu or Raspberry Pi OS bullseye. On these platforms, we recommend that you use version 1.1.31. Install the package by running ``` sudo apt update cd ~/Downloads - sudo apt-get install pipedal_1.3.61_arm64.deb + sudo apt-get install pipedal_1.3.62_arm64.deb ``` Adjust accordingly if you have downloaded v1.1.31. diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 48f49846..5fcde280 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -1,5 +1,31 @@ # Release Notes +## PiPedal 1.3.62 Beta + +Features: +- Plugins can now share uploaded model and IR files by type. For plugins with appropriate mod:fileTypes attributes, uploaded + files will be shared by category (Nam model, IR Files, Audio Tracks, Midi Songs, etc). Sharing of ML models are also supported, although + that requires use of an extended mod:fileType (mlmodel) that MOD does not support. +- TooB Tuner stability issues fixed. The pitch detection algorithm has been improved, and the tuner now does filtering to avoid + displaying a value when the current input isn't a steady note. +- LV2 trigger controls are now displayed as buttons instead of toggle controls (maybe useful for 3rd-party looper plugins). + +Known issues: + +- There are a number of outstanding minor compatibility issues with Ratatouille. I intend to work with the author to address them. + +### Backwards Compatibility With the Previous Upload Directory Scheme + +Sharing file types poses a problem with previous versions of PiPedal which did not share uploads by file type, but instead placed uploads in a private directory for each plugin. +In order to accomodate this, Pipedal provides legacy support for this situation. If a private upload directory for a plugin exists, the file property selection dialog will show +both the new shared directories, and the old private directory. The private directory has a name that reflects the name of the plugin (e.g. "Ratatoille.lv2"). This directory only +shows up if you have used the plugin on previous versiouns of Pipedal. If you have nothing of particular value in the old private upload directory, you can delete the plugin's private directory (which can be found in `/var/pipedal/audio_uploads`), and the private directory will no longer be displayed in the PiPedal UI. + +I anticipate providing a migration utility in the near future which will clean up and migrate legacy upload directories to the new directory structure (automatically uploading +presets which reference uploaded files that have moved). In the meantime, I think you will find the current occomodation for legacy upload directories perfectly functional. + + + ## PiPedal 1.3.61 Release Bug fixes: diff --git a/docs/download.md b/docs/download.md index 00963071..c8e3e97d 100644 --- a/docs/download.md +++ b/docs/download.md @@ -4,7 +4,7 @@ Download the most recent Debian (.deb) package for your platform: -- Raspberry Pi OS Bookworm (64-bit) v1.3.61 +- Raspberry Pi OS Bookworm (64-bit) v1.3.62 Install the package by running @@ -12,7 +12,7 @@ Install the package by running ``` sudo apt update cd ~/Downloads - sudo apt-get install ./pipedal_1.3.61_arm64.deb + sudo apt-get install ./pipedal_1.3.62_arm64.deb ``` Follow the instructions in [_Configuring PiPedal After Installation_](https://rerdavies.github.io/pipedal/Configuring.html) to complete the installation. diff --git a/docs/index.md b/docs/index.md index ce93fa4a..707999a7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,7 +1,7 @@ -v1.3.61 +v1.3.62   @@ -9,7 +9,7 @@ To download PiPedal, click [here](download.md). To view PiPedal documentation, click [here](Documentation.md). -#### NEW version 1.3.61 Release. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details. +#### NEW version 1.3.62 Release. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details.   diff --git a/lv2/aarch64/ToobAmp.lv2/CabIR.ttl b/lv2/aarch64/ToobAmp.lv2/CabIR.ttl index 714d882b..b4f19996 100644 --- a/lv2/aarch64/ToobAmp.lv2/CabIR.ttl +++ b/lv2/aarch64/ToobAmp.lv2/CabIR.ttl @@ -55,7 +55,7 @@ cabir:impulseFile a lv2:Parameter; rdfs:label "IR"; pg:group cabir:impulseFile1Group ; - mod:fileTypes "wav,flac"; + mod:fileTypes "ir,wav,flac"; rdfs:range atom:Path. @@ -63,14 +63,14 @@ cabir:impulseFile2 a lv2:Parameter; rdfs:label "IR"; pg:group cabir:impulseFile2Group ; - mod:fileTypes "wav,flac"; + mod:fileTypes "ir,wav,flac"; rdfs:range atom:Path. cabir:impulseFile3 a lv2:Parameter; rdfs:label "IR"; pg:group cabir:impulseFile3Group ; - mod:fileTypes "wav,flac"; + mod:fileTypes "ir,wav,flac"; rdfs:range atom:Path. @@ -93,7 +93,7 @@ cabir:impulseFile3 doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 51 ; + lv2:microVersion 52 ; rdfs:comment """ TooB Cab IR is a convolution-based guitar cabinet impulse response simulator. diff --git a/lv2/aarch64/ToobAmp.lv2/CabSim.ttl b/lv2/aarch64/ToobAmp.lv2/CabSim.ttl index 4fba15ba..f7732492 100644 --- a/lv2/aarch64/ToobAmp.lv2/CabSim.ttl +++ b/lv2/aarch64/ToobAmp.lv2/CabSim.ttl @@ -49,7 +49,7 @@ toob:frequencyResponseVector doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 51 ; + lv2:microVersion 52 ; mod:brand "TooB"; mod:label "TooB CabSim"; diff --git a/lv2/aarch64/ToobAmp.lv2/ConvolutionReverb.ttl b/lv2/aarch64/ToobAmp.lv2/ConvolutionReverb.ttl index 3951547b..8743ff1d 100644 --- a/lv2/aarch64/ToobAmp.lv2/ConvolutionReverb.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ConvolutionReverb.ttl @@ -32,7 +32,7 @@ toobimpulse:impulseFile a lv2:Parameter; rdfs:label "Reverb Impulse File"; - mod:fileTypes "wav,flac"; + mod:fileTypes "ir,wav,flac"; rdfs:range atom:Path. @@ -53,7 +53,7 @@ toobimpulse:impulseFile doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 51 ; + lv2:microVersion 52 ; rdfs:comment """ Convolution reverb is a notoriously compute-intensive effect. If you are having performance issues, use the Max T control to constrain the length of the impulse file to diff --git a/lv2/aarch64/ToobAmp.lv2/ConvolutionReverbStereo.ttl b/lv2/aarch64/ToobAmp.lv2/ConvolutionReverbStereo.ttl index c8769996..cfdc3fa0 100644 --- a/lv2/aarch64/ToobAmp.lv2/ConvolutionReverbStereo.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ConvolutionReverbStereo.ttl @@ -31,7 +31,7 @@ toobimpulse:impulseFile a lv2:Parameter; rdfs:label "File"; - mod:fileTypes "wav,flac"; + mod:fileTypes "ir,wav,flac"; rdfs:range atom:Path. @@ -51,7 +51,7 @@ toobimpulse:impulseFile doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 51 ; + lv2:microVersion 52 ; rdfs:comment """ diff --git a/lv2/aarch64/ToobAmp.lv2/InputStage.ttl b/lv2/aarch64/ToobAmp.lv2/InputStage.ttl index 0395376b..81db6952 100644 --- a/lv2/aarch64/ToobAmp.lv2/InputStage.ttl +++ b/lv2/aarch64/ToobAmp.lv2/InputStage.ttl @@ -65,7 +65,7 @@ inputStage:filterGroup doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 51 ; + lv2:microVersion 52 ; mod:brand "TooB"; mod:label "TooB Input"; diff --git a/lv2/aarch64/ToobAmp.lv2/PowerStage2l.ttl b/lv2/aarch64/ToobAmp.lv2/PowerStage2l.ttl index 3c2c8759..cfd715ae 100644 --- a/lv2/aarch64/ToobAmp.lv2/PowerStage2l.ttl +++ b/lv2/aarch64/ToobAmp.lv2/PowerStage2l.ttl @@ -67,7 +67,7 @@ pstage:stage3 doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 51 ; + lv2:microVersion 52 ; mod:brand "TooB"; mod:label "Power Stage"; diff --git a/lv2/aarch64/ToobAmp.lv2/SpectrumAnalyzer.ttl b/lv2/aarch64/ToobAmp.lv2/SpectrumAnalyzer.ttl index 1db4b1b6..d55d6f29 100644 --- a/lv2/aarch64/ToobAmp.lv2/SpectrumAnalyzer.ttl +++ b/lv2/aarch64/ToobAmp.lv2/SpectrumAnalyzer.ttl @@ -58,7 +58,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 51 ; + lv2:microVersion 52 ; rdfs:comment "TooB spectrum analyzer" ; mod:brand "TooB"; diff --git a/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl b/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl index 0b50f321..451e884d 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl @@ -55,7 +55,7 @@ tonestack:eqGroup doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 51 ; + lv2:microVersion 52 ; uiext:ui ; diff --git a/lv2/aarch64/ToobAmp.lv2/ToobAmp.so b/lv2/aarch64/ToobAmp.lv2/ToobAmp.so index 5662220a..a7aa3330 100644 Binary files a/lv2/aarch64/ToobAmp.lv2/ToobAmp.so and b/lv2/aarch64/ToobAmp.lv2/ToobAmp.so differ diff --git a/lv2/aarch64/ToobAmp.lv2/ToobAmpUI.so b/lv2/aarch64/ToobAmp.lv2/ToobAmpUI.so index f0e88921..8a63a3e1 100644 Binary files a/lv2/aarch64/ToobAmp.lv2/ToobAmpUI.so and b/lv2/aarch64/ToobAmp.lv2/ToobAmpUI.so differ diff --git a/lv2/aarch64/ToobAmp.lv2/ToobChorus.ttl b/lv2/aarch64/ToobAmp.lv2/ToobChorus.ttl index f8d3f193..aece98e9 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobChorus.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobChorus.ttl @@ -40,7 +40,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 51 ; + lv2:microVersion 52 ; rdfs:comment """ Emulation of a Boss CE-2 Chorus. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobDelay.ttl b/lv2/aarch64/ToobAmp.lv2/ToobDelay.ttl index ef3939d9..b2330bae 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobDelay.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobDelay.ttl @@ -41,7 +41,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 51 ; + lv2:microVersion 52 ; rdfs:comment """ A straightforward no-frills digital delay. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobFlanger.ttl b/lv2/aarch64/ToobAmp.lv2/ToobFlanger.ttl index d92266c4..88495ec0 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobFlanger.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobFlanger.ttl @@ -41,7 +41,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 51 ; + lv2:microVersion 52 ; rdfs:comment """ Emulation of a Boss BF-2 Flanger, based on circuit analysis and simulation of the original. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobFlangerStereo.ttl b/lv2/aarch64/ToobAmp.lv2/ToobFlangerStereo.ttl index 1cb31df7..a3a1dce1 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobFlangerStereo.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobFlangerStereo.ttl @@ -41,7 +41,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 51 ; + lv2:microVersion 52 ; rdfs:comment """ Digital emulation of a Boss BF-2 Flanger. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobFreeverb.ttl b/lv2/aarch64/ToobAmp.lv2/ToobFreeverb.ttl index 19f365f2..22120716 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobFreeverb.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobFreeverb.ttl @@ -41,7 +41,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 51 ; + lv2:microVersion 52 ; rdfs:comment """ Toob Freeverb is an Lv2 implementation of the famous Freeverb reverb effect. FreeVerb delivers a well-balanced reverb with very little tonal coloration. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobML.ttl b/lv2/aarch64/ToobAmp.lv2/ToobML.ttl index 68e39a53..c8420551 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobML.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobML.ttl @@ -31,7 +31,7 @@ toobml:modelFile a lv2:Parameter; rdfs:label "Model"; - mod:fileTypes "nam,nammodel"; + mod:fileTypes "json,mlmodels"; rdfs:range atom:Path. toobml:filterGroup @@ -65,7 +65,7 @@ toobml:sagGroup doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 51 ; + lv2:microVersion 52 ; rdfs:comment """ The TooB ML Amplifier plugin provides emulation of a variety of amplifiers and overdrive pedals that are implemented using neural-network-based machine learning models of real amplifiers. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobNeuralAmpModeler.ttl b/lv2/aarch64/ToobAmp.lv2/ToobNeuralAmpModeler.ttl index 52326aa9..0d628f82 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobNeuralAmpModeler.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobNeuralAmpModeler.ttl @@ -61,7 +61,7 @@ toobNam:eqGroup doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 51 ; + lv2:microVersion 52 ; rdfs:comment """ A port of Steven Atkinson's Neural Amp Modeler to LV2. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobTuner.ttl b/lv2/aarch64/ToobAmp.lv2/ToobTuner.ttl index 01ba793b..e3b1a4ba 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobTuner.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobTuner.ttl @@ -40,7 +40,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 51 ; + lv2:microVersion 52 ; rdfs:comment """ TooB Tuner is a chromatic guitar tuner. """ ; diff --git a/react/src/DialogEx.tsx b/react/src/DialogEx.tsx index 0fb78d94..70c765ae 100644 --- a/react/src/DialogEx.tsx +++ b/react/src/DialogEx.tsx @@ -198,6 +198,7 @@ class DialogEx extends React.Component implements I { this.onEnterKey(); } + evt.stopPropagation(); } render() { let { tag,onClose,...extra} = this.props; diff --git a/react/src/FilePropertyDialog.tsx b/react/src/FilePropertyDialog.tsx index 885a2100..5d79c9a9 100644 --- a/react/src/FilePropertyDialog.tsx +++ b/react/src/FilePropertyDialog.tsx @@ -17,6 +17,7 @@ // 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 { Theme, createStyles } from '@mui/material/styles'; import CreateNewFolderIcon from '@mui/icons-material/CreateNewFolder'; @@ -25,7 +26,7 @@ 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 } from './PiPedalModel'; +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'; @@ -61,7 +62,7 @@ const styles = (theme: Theme) => createStyles({ }, }); -const audioFileExtensions: {[name: string]: boolean} = { +const audioFileExtensions: { [name: string]: boolean } = { ".wav": true, ".flac": true, ".ogg": true, @@ -88,22 +89,23 @@ function isAudioFile(filename: string) { return audioFileExtensions[extension] !== undefined; } -export interface FilePropertyDialogProps extends WithStyles { +export interface FilePropertyDialogProps extends WithStyles { open: boolean, fileProperty: UiFileProperty, selectedFile: string, onOk: (fileProperty: UiFileProperty, selectedItem: string) => void, onCancel: () => void }; - export interface FilePropertyDialogState { fullScreen: boolean; selectedFile: string; selectedFileIsDirectory: boolean; - navDirectory: string; + selectedFileProtected: boolean; hasSelection: boolean; canDelete: boolean; - fileEntries: FileEntry[]; + fileResult: FileRequestResult; + navDirectory: string; + isProtectedDirectory: boolean; columns: number; columnWidth: number; openUploadFileDialog: boolean; @@ -114,19 +116,22 @@ export interface FilePropertyDialogState { moveDialogOpen: boolean; }; -function pathExtension(path: 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 ""; + 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; @@ -169,683 +174,723 @@ export function pathFileName(path: string): string { export default withStyles(styles, { withTheme: true })( class FilePropertyDialog extends ResizeResponsiveComponent { - getNavDirectoryFromFile(selectedFile: string, fileProperty: UiFileProperty): string { - - // would be easier if we had the data root (/var/pipedal/audio_downloads, but could be different when we're debugging. :-/ - let nPos = selectedFile.indexOf("/" + fileProperty.directory + "/"); - if (nPos === -1) return ""; - let relativePath = selectedFile.substring(nPos + fileProperty.directory.length + 2); - let segments = relativePath.split('/'); - - let result = ""; - for (let i = 0; i < segments.length - 1; ++i) { - if (result !== "") result += '/'; - result += segments[i]; + getNavDirectoryFromFile(selectedFile: string, fileProperty: UiFileProperty): string { + if (selectedFile === "") { + return ""; + } + return pathParentDirectory(selectedFile); } - return result; - } - constructor(props: FilePropertyDialogProps) { - super(props); - - - this.model = PiPedalModelFactory.getInstance(); - - this.state = { - fullScreen: this.getFullScreen(), - selectedFile: props.selectedFile, - selectedFileIsDirectory: false, - navDirectory: this.getNavDirectoryFromFile(props.selectedFile, props.fileProperty), - hasSelection: false, - canDelete: false, - columns: 0, - columnWidth: 1, - fileEntries: [], - openUploadFileDialog: false, - openConfirmDeleteDialog: false, - menuAnchorEl: null, - newFolderDialogOpen: false, - renameDialogOpen: false, - moveDialogOpen: false - }; - this.requestScroll = true; - } - getFullScreen() { - return window.innerWidth < 450 || window.innerHeight < 450; - } + constructor(props: FilePropertyDialogProps) { + super(props); + + + this.model = PiPedalModelFactory.getInstance(); + + this.state = { + fullScreen: this.getFullScreen(), + selectedFile: props.selectedFile, + selectedFileProtected: true, + selectedFileIsDirectory: false, + navDirectory: this.getNavDirectoryFromFile(props.selectedFile, props.fileProperty), + hasSelection: false, + canDelete: false, + columns: 0, + columnWidth: 1, + fileResult: new FileRequestResult(), + isProtectedDirectory: true, + openUploadFileDialog: false, + openConfirmDeleteDialog: false, + menuAnchorEl: null, + newFolderDialogOpen: false, + renameDialogOpen: false, + moveDialogOpen: false + }; + this.requestScroll = true; + } + getFullScreen() { + return window.innerWidth < 450 || window.innerHeight < 450; + } - private scrollRef: HTMLDivElement | null = null; + private scrollRef: HTMLDivElement | null = null; - onScrollRef(element: HTMLDivElement | null) { - this.scrollRef = element; - this.maybeScrollIntoView(); - } - private maybeScrollIntoView() { - if (this.scrollRef !== null && this.state.fileEntries.length !== 0 && this.mounted && this.requestScroll) { - this.requestScroll = false; - let options: ScrollIntoViewOptions = { block: "nearest" }; - options.block = "nearest"; + 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); + 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((files) => { - if (this.mounted) { - // let insertionPoint = files.length; - // for (let i = 0; i < files.length; ++i) { - // if (!files[i].isDirectory) { - // insertionPoint = i; - // break; - // } - // } - files.splice(0, 0, { filename: "", isDirectory: false }); - this.setState({ fileEntries: files, hasSelection: this.isFileInList(files, this.state.selectedFile), navDirectory: navPath }); - } - }).catch((error) => { - this.model.showAlert(error.toString()) - }); - } + private mounted: boolean = false; + private model: PiPedalModel; - 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 }); + 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, + selectedFileProtected: fileEntry ? fileEntry.isProtected: true, + navDirectory: navPath + }); + } + }).catch((error) => { + if (!this.mounted) return; + if (navPath !== "") // deleted sample directory maybe? + { + this.navigate(""); + } else { + this.model.showAlert(error.toString()) + } + }); } - } - onWindowSizeChanged(width: number, height: number): void { - this.setState({ - fullScreen: this.getFullScreen() - }) - if (this.lastDivRef !== null) { - this.onMeasureRef(this.lastDivRef); + 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() + }) + if (this.lastDivRef !== null) { + this.onMeasureRef(this.lastDivRef); + } + + } - private requestScroll: boolean = false; + private requestScroll: boolean = false; - componentDidMount() { - super.componentDidMount(); - this.mounted = 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 navDirectory = this.getNavDirectoryFromFile(this.props.selectedFile, this.props.fileProperty); - this.setState({ - selectedFile: this.props.selectedFile, - newFolderDialogOpen: false, - renameDialogOpen: false, - moveDialogOpen: false + componentDidMount() { + super.componentDidMount(); + this.mounted = 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 navDirectory = this.getNavDirectoryFromFile(this.props.selectedFile, this.props.fileProperty); + this.setState({ + selectedFile: this.props.selectedFile, + selectedFileIsDirectory: false, + selectedFileProtected: true, + newFolderDialogOpen: false, + renameDialogOpen: false, + moveDialogOpen: false }); - this.requestFiles(navDirectory) - this.requestScroll = true; + this.requestFiles(navDirectory) + this.requestScroll = true; + } } - } - } + } - private isDirectory(path: string): boolean { - for (var fileEntry of this.state.fileEntries) { - if (fileEntry.filename === path) { - return fileEntry.isDirectory; + private isDirectory(path: string): boolean { + for (var fileEntry of this.state.fileResult.files) { + if (fileEntry.pathname === path) { + return fileEntry.isDirectory; + } } + return false; } - return false; - } - private isFileInList(files: FileEntry[], file: string) { - - let hasSelection = false; - if (file === "") return true; - for (var listFile of files) { - if (listFile.filename === file) { - hasSelection = true; - break; + 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; } - return hasSelection; - } - - onSelectValue(selectedFile: string, isDirectory: boolean) { - this.requestScroll = true; - this.setState({ selectedFile: selectedFile, selectedFileIsDirectory: isDirectory, hasSelection: this.isFileInList(this.state.fileEntries, selectedFile) }) - } - onDoubleClickValue(selectedFile: string) { - this.openSelectedFile(); - } - - handleMenuOpen(event: React.MouseEvent) { - this.setState({ menuAnchorEl: event.currentTarget }); + private getFileEntry(files: FileEntry[], file: string): FileEntry | undefined { - } - handleMenuClose() { - this.setState({ menuAnchorEl: null }); - } - handleDelete() { - this.setState({ openConfirmDeleteDialog: true }); - } - handleConfirmDelete() { - this.setState({ openConfirmDeleteDialog: false }); - if (this.state.hasSelection) { - let selectedFile = this.state.selectedFile; - let position = -1; - for (let i = 0; i < this.state.fileEntries.length; ++i) { - let file = this.state.fileEntries[i]; - if (file.filename === selectedFile) { - position = i; + if (file === "") return undefined; + for (var listFile of files) { + if (listFile.pathname === file) { + return listFile; } } - let newSelection = ""; - if (position >= 0 && position < this.state.fileEntries.length - 1) { - newSelection = this.state.fileEntries[position + 1].filename; - } else if (position === this.state.fileEntries.length - 1) { - if (position !== 0) { - newSelection = this.state.fileEntries[position - 1].filename; - } + return undefined; + } - } + onSelectValue(fileEntry: FileEntry) { + this.requestScroll = true; + this.setState({ + selectedFile: fileEntry.pathname, + selectedFileIsDirectory: fileEntry.isDirectory, + selectedFileProtected: fileEntry.isProtected, + hasSelection: this.isFileInList(this.state.fileResult.files, fileEntry.pathname) + }) + } + onDoubleClickValue(selectedFile: string) { + this.openSelectedFile(); + } - this.model.deleteUserFile(this.state.selectedFile) - .then( - () => { - this.setState({ selectedFile: newSelection, hasSelection: newSelection !== "" }); - this.requestFiles(this.state.navDirectory); + handleMenuOpen(event: React.MouseEvent) { + this.setState({ menuAnchorEl: event.currentTarget }); + + } + handleMenuClose() { + this.setState({ menuAnchorEl: null }); + } + handleDelete() { + if (this.state.selectedFileProtected) { + return; + } + this.setState({ openConfirmDeleteDialog: true }); + } + 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; } - ).catch( - (e: any) => { - this.model.showAlert(e.toString()); + } + 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]; } - ); - } - } - private wantsScrollRef: boolean = true; + } - navigate(relativeDirectory: string) { - this.requestFiles(relativeDirectory); - this.setState({ navDirectory: relativeDirectory }); + this.model.deleteUserFile(this.state.selectedFile) + .then( + () => { + if (newSelection) + { + this.setState({ + selectedFile: newSelection.pathname, + selectedFileIsDirectory: newSelection.isDirectory, + selectedFileProtected: newSelection.isProtected, + hasSelection: true + }); + } else { + this.setState({ + selectedFile: "", + selectedFileIsDirectory: false, + selectedFileProtected: true, + hasSelection: false + }); - } - renderBreadcrumbs() { - if (this.state.navDirectory === "") { - return (); - } - let breadcrumbs: React.ReactElement[] = [( - - ) - ]; - let directories = this.state.navDirectory.split("/"); - let target = ""; - for (let i = 0; i < directories.length - 1; ++i) { - target = pathConcat(target, directories[i]); - let myTarget = target; - breadcrumbs.push(( - / + } + this.requestFiles(this.state.navDirectory); + } + ).catch( + (e: any) => { + this.model.showAlert(e.toString()); + } + ); + } + } + private wantsScrollRef: boolean = true; + + navigate(relativeDirectory: string) { + this.requestFiles(relativeDirectory); + this.setState({ navDirectory: relativeDirectory }); + + } + renderBreadcrumbs() { + if (this.state.navDirectory === "") { + return (); + } + let breadcrumbs: React.ReactElement[] = [( + ) - ); - breadcrumbs.push(( - - )); - } - { - let lastdirectory = directories[directories.length - 1]; - breadcrumbs.push(( - / - ) - ); - breadcrumbs.push(( -
- {lastdirectory} -
- )); + + )); + } + if (this.state.fileResult.breadcrumbs.length > 1) { + let lastdirectory = this.state.fileResult.breadcrumbs[this.state.fileResult.breadcrumbs.length - 1]; + breadcrumbs.push(( + / - } - return ( -
+ ) + ); + breadcrumbs.push(( +
+ {lastdirectory.displayName} +
+ )); + + } + return ( +
-
- {breadcrumbs} +
+ {breadcrumbs} +
+
- -
- ); - } + ); + } + + getDefaultPath(): string { + try { + let storage = window.localStorage; + let result = storage.getItem("fpDefaultPath"); + if (result) { + return result; + } + } catch (e) { - 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) { - 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; + 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; } - return result; - } - private getIcon(fileEntry: FileEntry) - { - if (fileEntry.filename === "") - { - return (); - } - if (fileEntry.isDirectory) - { - return ( + private getIcon(fileEntry: FileEntry) { + if (fileEntry.pathname === "") { + return (); + } + if (fileEntry.isDirectory) { + return ( - ); - } - if (isAudioFile(fileEntry.filename)) - { - return (); + ); + } + if (isAudioFile(fileEntry.pathname)) { + return (); + } + return (); } - return (); - } - render() { - let classes = this.props.classes; - let columnWidth = this.state.columnWidth; - let okButtonText = "Select"; - if (this.state.hasSelection && this.state.selectedFileIsDirectory) - { - okButtonText = "Open"; - } - 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(); }} - > - - - - - { this.handleMenuOpen(ev); }} - > - - - { + this.props.onCancel(); + }} + onEnterKey={() => { + this.openSelectedFile(); + }} + open={this.props.open} tag="fileProperty" + fullWidth maxWidth="md" + PaperProps={ + this.state.fullScreen ? + {} + : + { + style: { + minHeight: "90%", + maxHeight: "90%", + } }} - open={this.state.menuAnchorEl !== null} - onClose={() => { this.handleMenuClose(); }} - > - { this.handleMenuClose(); this.onNewFolder(); }}>New folder - {this.hasSelectedFileOrFolder() && ()} - {this.hasSelectedFileOrFolder() && ( { this.handleMenuClose();this.onMove(); }}>Move)} - {this.hasSelectedFileOrFolder() && ( { 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.fileEntries.map( - (value: FileEntry, index: number) => { - let displayValue = value.filename; - if (displayValue === "") { - displayValue = ""; - } else { - if (value.isDirectory) - { - displayValue = pathFileName(displayValue); - } else { - displayValue = pathFileName(displayValue); + > + + + { 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) }; } - } - let selected = value.filename === 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.filename,value.isDirectory)} onDoubleClick={() => { this.onDoubleClickValue(value.filename); }} - > -
-
- {this.getIcon(value)} - {displayValue} -
- - ); - } - ) + return ( + this.onSelectValue(value)} + onDoubleClick={() => { this.onDoubleClickValue(value.pathname); }} + > +
+
+ {this.getIcon(value)} + {displayValue} +
+ + ); + } + ) + } +
+ + + +
+ this.handleDelete()} > + + + + +
 
+ + + +
+
+ { + this.setState({ openUploadFileDialog: false }); + } } -
- - - -
- this.handleDelete()} > - - - - -
 
- - - -
-
- { - this.setState({ openUploadFileDialog: false }); + uploadPage={ + "uploadUserFile?directory=" + encodeURIComponent(this.state.navDirectory) + + "&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" + /> + ) } - uploadPage={ - "uploadUserFile?directory=" + encodeURIComponent( - pathConcat(this.props.fileProperty.directory,this.state.navDirectory) - ) - + "&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.renameDialogOpen && ( + { this.setState({ renameDialogOpen: false }); this.onExecuteRename(newName) }} + onClose={() => { this.setState({ renameDialogOpen: false }); }} + acceptActionName="OK" + /> - /> - { - 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); + ) + } + { + 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)) { - let directoryName = pathFileName(this.state.selectedFile); - let navDirectory = pathConcat(this.state.navDirectory, directoryName); - this.requestFiles(navDirectory); - this.setState({ navDirectory: navDirectory }); - } else { - this.props.onOk(this.props.fileProperty, this.state.selectedFile); + } + + ); + } + 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 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 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 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 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}); - this.requestFiles(this.state.navDirectory); - this.requestScroll = true - }) - .catch((e) => { - this.model.showAlert(e.toString()); - } - ); - } -}); \ No newline at end of file + 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()); + } + ); + } + }); \ No newline at end of file diff --git a/react/src/FilePropertyDirectorySelectDialog.tsx b/react/src/FilePropertyDirectorySelectDialog.tsx index a9b341f9..0c514233 100644 --- a/react/src/FilePropertyDirectorySelectDialog.tsx +++ b/react/src/FilePropertyDirectorySelectDialog.tsx @@ -38,28 +38,18 @@ import { isDarkMode } from './DarkMode'; import IconButton from '@mui/material/IconButton'; -function pathConcat(l: string, r: string): string -{ - if (l.length === 0) return r; - if (r.length === 0) return l; - return l +'/' + r; -} - class DirectoryTree { name: string = ""; path: string = ""; expanded: boolean = false; + isProtected: boolean = true; children: DirectoryTree[] = []; constructor(tree: FilePropertyDirectoryTree, parentTree: (DirectoryTree | null) = null) { this.name = tree.directoryName; - if (parentTree) { - this.path = pathConcat(parentTree.path,tree.directoryName); - } else { - this.name = "Home"; - this.path = tree.directoryName; - this.expanded = true; - } + this.path = tree.directoryName; + this.name = tree.displayName; + this.isProtected = tree.isProtected; for (var treeChild of tree.children) { this.children.push(new DirectoryTree(treeChild, this)); } @@ -143,6 +133,7 @@ export interface FilePropertyDirectorySelectDialogState { selectedPath: string; directoryTree?: DirectoryTree; hasSelection: boolean; + isProtected: boolean; directoryTreeInvalidatecount: number; }; @@ -160,6 +151,7 @@ export default class FilePropertyDirectorySelectDialog extends ResizeResponsiveC selectedPath: props.defaultPath, directoryTree: undefined, hasSelection: false, + isProtected: true, directoryTreeInvalidatecount: 0 }; @@ -192,14 +184,19 @@ export default class FilePropertyDirectorySelectDialog extends ResizeResponsiveC this.model.getFilePropertyDirectoryTree(this.props.uiFileProperty) .then((filePropertyDirectoryTree) => { let myTree = new DirectoryTree(filePropertyDirectoryTree); - if (this.props.excludeDirectory) + if (this.props.excludeDirectory.length !== 0) { myTree.remove(this.props.excludeDirectory); } myTree.expand(this.state.selectedPath); - let hasSelection = myTree.find(this.state.selectedPath) != null; - this.setState({ directoryTree: myTree, hasSelection: hasSelection }); + let selection = myTree.find(this.state.selectedPath); + let hasSelection = !!selection; + this.setState({ + directoryTree: myTree, + hasSelection: hasSelection, + isProtected: selection ? selection.isProtected : false + }); this.requestScroll = true; }) .catch((e) => { @@ -223,6 +220,9 @@ export default class FilePropertyDirectorySelectDialog extends ResizeResponsiveC onOK() { + if (this.state.isProtected) { + return; + } if (this.state.hasSelection) { this.props.onOk(this.state.selectedPath); } @@ -238,7 +238,11 @@ export default class FilePropertyDirectorySelectDialog extends ResizeResponsiveC 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 }); + this.setState({ + selectedPath: directoryTree.path, + hasSelection: true, + isProtected: directoryTree.isProtected, + directoryTreeInvalidatecount: this.state.directoryTreeInvalidatecount + 1 }); } private renderTree_(directoryTree: DirectoryTree) { let ref: ((element: HTMLButtonElement | null) => void) | undefined = undefined; @@ -338,7 +342,8 @@ export default class FilePropertyDirectorySelectDialog extends ResizeResponsiveC - diff --git a/react/src/FilePropertyDirectoryTree.tsx b/react/src/FilePropertyDirectoryTree.tsx index 4a9191be..8275a738 100644 --- a/react/src/FilePropertyDirectoryTree.tsx +++ b/react/src/FilePropertyDirectoryTree.tsx @@ -21,6 +21,8 @@ export default class FilePropertyDirectoryTree { deserialize(input: any) : FilePropertyDirectoryTree { this.directoryName = input.directoryName; + this.displayName = input.displayName; + this.isProtected = input.isProtected; this.children = FilePropertyDirectoryTree.deserialize_array(input.children); return this; } @@ -35,5 +37,7 @@ export default class FilePropertyDirectoryTree { directoryName: string = ""; + displayName: string = ""; + isProtected: boolean = false; children: FilePropertyDirectoryTree[] = []; } \ No newline at end of file diff --git a/react/src/Lv2Plugin.tsx b/react/src/Lv2Plugin.tsx index 29b54c30..c451ba8f 100644 --- a/react/src/Lv2Plugin.tsx +++ b/react/src/Lv2Plugin.tsx @@ -246,6 +246,8 @@ export class UiFileProperty { this.index = input.index; this.portGroup = input.portGroup; this.resourceDirectory = input.resourceDirectory ?? ""; + this.modDirectories = input.modDirectories; + this.useLegacyModDirectory = input.useLegacyModDirectory; return this; } static deserialize_array(input: any): UiFileProperty[] { @@ -297,6 +299,8 @@ export class UiFileProperty { index: number = -1; portGroup: string = ""; resourceDirectory: string = ""; + modDirectories: string[] = []; + useLegacyModDirectory: boolean = false; }; export class Lv2Plugin implements Deserializable { @@ -436,6 +440,7 @@ export enum ControlType { OnOffSwitch, ABSwitch, Select, + Trigger, Tuner, Vu, @@ -444,7 +449,7 @@ export enum ControlType { } export class UiControl implements Deserializable { - deserialize(input: any): UiControl { +deserialize(input: any): UiControl { this.symbol = input.symbol; this.name = input.name; this.index = input.index; @@ -458,7 +463,7 @@ export class UiControl implements Deserializable { this.integer_property = input.integer_property; this.enumeration_property = input.enumeration_property; this.toggled_property = input.toggled_property; - this.trigger = input.trigger; + this.trigger_property = input.trigger_property; this.not_on_gui = input.not_on_gui; this.scale_points = ScalePoint.deserialize_array(input.scale_points); this.port_group = input.port_group; @@ -499,6 +504,10 @@ export class UiControl implements Deserializable { this.controlType = ControlType.OnOffSwitch; } } + if (this.is_input && this.trigger_property) + { + this.controlType = ControlType.Trigger; + } return this; } applyProperties(properties: Partial): UiControl { @@ -546,7 +555,7 @@ export class UiControl implements Deserializable { range_steps: number = 0; integer_property: boolean = false; enumeration_property: boolean = false; - trigger: boolean = false; + trigger_property: boolean = false; not_on_gui: boolean = false; toggled_property: boolean = false; scale_points: ScalePoint[] = []; @@ -590,6 +599,9 @@ export class UiControl implements Deserializable { isSelect(): boolean { return this.controlType === ControlType.Select; } + isTrigger(): boolean { + return this.controlType === ControlType.Trigger; + } isOutputSelect(): boolean { return !this.is_input && this.controlType === ControlType.OutputSelect; } diff --git a/react/src/MidiBindingView.tsx b/react/src/MidiBindingView.tsx index 4df072fb..2a9c8fc5 100644 --- a/react/src/MidiBindingView.tsx +++ b/react/src/MidiBindingView.tsx @@ -160,7 +160,7 @@ const MidiBindingView = isBinaryControl = (port.isAbToggle() || port.isOnOffSwitch()); if (midiBinding.bindingType !== MidiBinding.BINDING_TYPE_NONE) { canLatch = isBinaryControl; - canTrigger = port.trigger; + canTrigger = port.trigger_property; showLinearControlTypeSelect = !(canLatch || canTrigger); showLinearRange = showLinearControlTypeSelect && midiBinding.linearControlType === MidiBinding.LINEAR_CONTROL_TYPE; canRotaryScale = showLinearControlTypeSelect && midiBinding.linearControlType === MidiBinding.CIRCULAR_CONTROL_TYPE; diff --git a/react/src/ModFileTypes.tsx b/react/src/ModFileTypes.tsx new file mode 100644 index 00000000..26241062 --- /dev/null +++ b/react/src/ModFileTypes.tsx @@ -0,0 +1,60 @@ +// Copyright (c) 2022 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 interface ModDirectory { + modType: string; + pipedalPath: string; + displayName: string; + defaultFileExtensions: string[]; +}; + + +export const modDirectories: ModDirectory[] = [ + {modType: "audioloop", pipedalPath: "shared/audio/Loops",displayName: "Loops", defaultFileExtensions: ["audio/*"]}, // Audio Loops, meant to be used for looper-style plugins + //"audiorecording","shared/audio/Audio Recordings", defaultFileExtensions: ["audio/*"]}, : Audio Recordings, triggered by plugins and stored in the unit + {modType: "audiosample", pipedalPath: "shared/audio/Samples", displayName: "Samples", defaultFileExtensions: ["audio/*"]}, // One-shot Audio Samples, meant to be used for sampler-style plugins + {modType: "audiotrack", pipedalPath: "shared/audio/Tracks",displayName: "Tracks", defaultFileExtensions: ["audio/*"]}, // Audio Tracks, meant to be used as full-performance/song or backtrack + {modType: "cabsim", pipedalPath: "CabIR", displayName: "Cab IRs", defaultFileExtensions: ["audio/*"]}, // Speaker Cabinets, meant as small IR audio files + + /// - h2drumkit: Hydrogen Drumkits, must use h2drumkit file extension + {modType: "ir", pipedalPath: "ReverbImpulseFiles", displayName: "Impulse Responses", defaultFileExtensions: ["audio/*"]}, // Impulse Responses + {modType: "midiclip", pipedalPath: "shared/midiClips",displayName: "MIDI Clips", defaultFileExtensions: [".mid", ".midi"]}, // MIDI Clips, to be used in sync with host tempo, must have mid or midi file extension + {modType: "midisong", pipedalPath: "shared/midiSongs", displayName: "MIDI Songs", defaultFileExtensions: [".mid", ".midi"]}, // MIDI Songs, meant to be used as full-performance/song or backtrack + {modType: "sf2", pipedalPath: "shared/sf2",displayName: "Sound Fonts", defaultFileExtensions: ["sf2", "sf3"]}, // SF2 Instruments, must have sf2 or sf3 file extension + {modType: "sfz", pipedalPath: "shared/sfz",displayName: "Sfz Files", defaultFileExtensions: ["sfz"]}, // SFZ Instruments, must have sfz file extension + // extensions observed in the field. + {modType: "audio", pipedalPath: "shared/audio", displayName: "Audio", defaultFileExtensions: ["audio/*"]}, // all audio files (Ratatoille) + {modType: "nammodel", pipedalPath: "NeuralAmpModels", displayName: "Neural Amp Models", defaultFileExtensions: [".nam"]}, // Ratatoille, Mike's NAM. + {modType: "aidadspmodel", pipedalPath: "shared/aidaaix", displayName: "AIDA IAX Models", defaultFileExtensions: [".json", ".aidaiax"]}, // Ratatoille + {modType: "mlmodel", pipedalPath: "ToobMlModels", displayName: "ML Models", defaultFileExtensions: [".json"]}, // + +]; + +export function getModDirectory(modFileType: string): ModDirectory | undefined { + for (let i = 0; i < modDirectories.length; ++i) + { + if (modDirectories[i].modType === modFileType) + { + return modDirectories[i]; + } + } + return undefined; +} \ No newline at end of file diff --git a/react/src/PiPedalModel.tsx b/react/src/PiPedalModel.tsx index 196688c1..5b188c80 100644 --- a/react/src/PiPedalModel.tsx +++ b/react/src/PiPedalModel.tsx @@ -73,8 +73,20 @@ export enum ReconnectReason { export type ControlValueChangedHandler = (key: string, value: number) => void; export interface FileEntry { - filename: string; + pathname: string; + displayName: string; isDirectory: boolean; + isProtected: boolean; +}; + +export interface BreadcrumbEntry { + pathname: string; + displayName: string; +}; +export class FileRequestResult { + files: FileEntry[] = []; + isProtected: boolean = false; + breadcrumbs: BreadcrumbEntry[] = []; }; export type PluginPresetsChangedHandler = (pluginUri: string) => void; @@ -1989,9 +2001,9 @@ export class PiPedalModel //implements PiPedalModel return nullCast(this.webSocket) .request('requestFileList', piPedalFileProperty); } - requestFileList2(relativeDirectoryPath: string, piPedalFileProperty: UiFileProperty): Promise { + requestFileList2(relativeDirectoryPath: string, piPedalFileProperty: UiFileProperty): Promise { return nullCast(this.webSocket) - .request('requestFileList2', + .request('requestFileList2', { relativePath: relativeDirectoryPath, fileProperty: piPedalFileProperty } ); } @@ -2498,7 +2510,7 @@ export class PiPedalModel //implements PiPedalModel link.click(); } - uploadFile(uploadPage: string, file: File, contentType: string = "application/octet-stream", abortController?: AbortController): Promise { + uploadUserFile(uploadPage: string, file: File, contentType: string = "application/octet-stream", abortController?: AbortController): Promise { let result = new Promise((resolve, reject) => { try { if (file.size > this.maxFileUploadSize) { @@ -2539,10 +2551,20 @@ export class PiPedalModel //implements PiPedalModel } }) .then((json) => { - resolve(json as string); + let response = json as {errorMessage: string, path: string}; + if (response.errorMessage !== "") + { + throw new Error(response.errorMessage); + } + resolve(response.path); }) .catch((error) => { - reject("Upload failed. " + error); + if (error instanceof Error) + { + reject("Upload failed. " + (error as Error).message); + } else { + reject("Upload failed. " + error); + } }) ; } catch (error) { diff --git a/react/src/PluginControl.tsx b/react/src/PluginControl.tsx index 64a6dd14..dcfdb871 100644 --- a/react/src/PluginControl.tsx +++ b/react/src/PluginControl.tsx @@ -18,6 +18,7 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import React, { TouchEvent, PointerEvent, ReactNode, Component, SyntheticEvent } from 'react'; +import Button from '@mui/material/Button'; import { Theme } from '@mui/material/styles'; import { WithStyles } from '@mui/styles'; import createStyles from '@mui/styles/createStyles'; @@ -27,10 +28,11 @@ import Typography from '@mui/material/Typography'; import Input from '@mui/material/Input'; import Select from '@mui/material/Select'; import Switch from '@mui/material/Switch'; -import Utility, {nullCast} from './Utility'; +import Utility, { nullCast } from './Utility'; import MenuItem from '@mui/material/MenuItem'; -import {PiPedalModel,PiPedalModelFactory} from './PiPedalModel'; -import {ReactComponent as DialIcon} from './svg/fx_dial.svg'; +import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; +import { ReactComponent as DialIcon } from './svg/fx_dial.svg'; +import { isDarkMode } from './DarkMode'; const MIN_ANGLE = -135; const MAX_ANGLE = 135; @@ -94,8 +96,7 @@ type PluginControlState = { const PluginControl = withStyles(styles, { withTheme: true })( - class extends Component - { + class extends Component { frameRef: React.RefObject; imgRef: React.RefObject; @@ -140,23 +141,18 @@ const PluginControl = return Utility.needsZoomedControls(); } - showZoomedControl() - { - if (this.props.uiControl && this.frameRef.current) - { - this.model.zoomUiControl(this.frameRef.current,this.props.instanceId,this.props.uiControl); + showZoomedControl() { + if (this.props.uiControl && this.frameRef.current) { + this.model.zoomUiControl(this.frameRef.current, this.props.instanceId, this.props.uiControl); } } - hideZoomedControl() - { - if (this.frameRef.current && this.model.zoomedUiControl.get()?.source === this.frameRef.current) - { + hideZoomedControl() { + if (this.frameRef.current && this.model.zoomedUiControl.get()?.source === this.frameRef.current) { this.model.clearZoomedControl(); } } - componentWillUnmount() - { + componentWillUnmount() { this.hideZoomedControl(); } inputChanged: boolean = false; @@ -172,12 +168,11 @@ const PluginControl = } onInputFocus(event: SyntheticEvent): void { this.displayValueRef.current!.style.display = "none"; - if (Utility.hasIMEKeyboard()) - { + if (Utility.hasIMEKeyboard()) { event.preventDefault(); event.stopPropagation(); this.inputRef.current?.blur(); - this.props.requestIMEEdit(nullCast(this.props.uiControl),this.props.value) + this.props.requestIMEEdit(nullCast(this.props.uiControl), this.props.value) } } onInputKeyPress(e: any): void { @@ -221,7 +216,7 @@ const PluginControl = // clamp and quantize. let range = this.valueToRange(result); result = this.rangeToValue(range); - let displayVal = this.props.uiControl?.formatShortValue(result)??""; + let displayVal = this.props.uiControl?.formatShortValue(result) ?? ""; if (event.currentTarget) { event.currentTarget.value = displayVal; } @@ -270,11 +265,9 @@ const PluginControl = capturedPointers: number[] = []; - onBodyPointerDownCapture(e_: any): any - { + onBodyPointerDownCapture(e_: any): any { let e = e_ as PointerEvent; - if (this.isExtraTouch(e)) - { + if (this.isExtraTouch(e)) { this.isTap = false; this.captureElement!.setPointerCapture(e.pointerId); this.capturedPointers.push(e.pointerId); @@ -305,10 +298,8 @@ const PluginControl = e.preventDefault(); e.stopPropagation(); - if (this.isTouchDevice()) - { - if (this.props.uiControl?.isDial()??false) - { + if (this.isTouchDevice()) { + if (this.props.uiControl?.isDial() ?? false) { this.isTap = false; this.showZoomedControl(); return; @@ -318,8 +309,7 @@ const PluginControl = ++this.pointersDown; this.mouseDown = true; - if (this.pointersDown === 1) - { + if (this.pointersDown === 1) { this.isTap = true; this.tapStartMs = Date.now(); } else { @@ -340,8 +330,8 @@ const PluginControl = this.captureElement = img; document.body.addEventListener( "pointerdown", - this.onBodyPointerDownCapture,true - ); + this.onBodyPointerDownCapture, true + ); img.setPointerCapture(e.pointerId); if (img.style) { @@ -350,11 +340,10 @@ const PluginControl = } } else { - if (this.isExtraTouch(e)) - { + if (this.isExtraTouch(e)) { ++this.pointersDown; this.isTap = false; - + } } } @@ -367,7 +356,7 @@ const PluginControl = if (this.isCapturedPointer(e)) { --this.pointersDown; this.isTap = false; - + this.releaseCapture(e); } @@ -378,21 +367,16 @@ const PluginControl = let ultraHigh = false; let high = false; - if (e.ctrlKey) - { + if (e.ctrlKey) { ultraHigh = true; } - if (e.shiftKey) - { + if (e.shiftKey) { high = true; } - if (e.pointerType === "touch") - { - if (this.pointersDown >= 3) - { + if (e.pointerType === "touch") { + if (this.pointersDown >= 3) { ultraHigh = true; - } else if (this.pointersDown === 2) - { + } else if (this.pointersDown === 2) { high = true; } } @@ -411,28 +395,24 @@ const PluginControl = } private lastTapMs = 0; - resetToDefaultValue(uiControl: UiControl): void - { + resetToDefaultValue(uiControl: UiControl): void { let value = uiControl.default_value; - this.model.setPedalboardControl(this.props.instanceId,uiControl.symbol,value); + this.model.setPedalboardControl(this.props.instanceId, uiControl.symbol, value); } onPointerDoubleTap() { let uiControl = this.props.uiControl; - if (uiControl) - { - if (uiControl.isDial()) - { + if (uiControl) { + if (uiControl.isDial()) { this.resetToDefaultValue(uiControl); } } } onPointerTap() { let tapTime = Date.now(); - let dT = tapTime-this.lastTapMs; + let dT = tapTime - this.lastTapMs; this.lastTapMs = tapTime; - if (dT < 500) - { + if (dT < 500) { this.onPointerDoubleTap(); } } @@ -442,32 +422,29 @@ const PluginControl = if (this.isCapturedPointer(e)) { --this.pointersDown; - + e.preventDefault(); let dRange = this.updateRange(e) this.previewRange(dRange, true); this.releaseCapture(e); - if (this.isTap) - { - let ms = Date.now()-this.tapStartMs; - if (ms < 200) - { + if (this.isTap) { + let ms = Date.now() - this.tapStartMs; + if (ms < 200) { this.onPointerTap(); } } } else { --this.pointersDown; - + } } - releaseCapture(e: PointerEvent) - { + releaseCapture(e: PointerEvent) { let img = this.imgRef.current; - + if (img && img.style) { img.releasePointerCapture(e.pointerId); img.style.opacity = "" + DEFAULT_OPACITY; @@ -480,14 +457,14 @@ const PluginControl = } document.body.removeEventListener( "pointerdown", - this.onBodyPointerDownCapture,true - ); + this.onBodyPointerDownCapture, true + ); this.mouseDown = false; } - clickSlop() { + clickSlop() { return 3.5; // maybe larger on touch devices. } onPointerMove(e: PointerEvent): void { @@ -499,15 +476,27 @@ const PluginControl = let x = e.clientX; let y = e.clientY; let dx = x - this.startX; - let dy = y-this.startY; - let distance = Math.sqrt(dx*dx+dy*dy); - if (distance >= this.clickSlop()) - { + let dy = y - this.startY; + let distance = Math.sqrt(dx * dx + dy * dy); + if (distance >= this.clickSlop()) { this.isTap = false; } } } - + handleTriggerMouseDown() { + let uiControl = this.props.uiControl; + if (uiControl) + { + this.model.setPedalboardControl(this.props.instanceId,uiControl.symbol,1); + } + } + handleTriggerMouseUp() { + let uiControl = this.props.uiControl; + if (uiControl) + { + this.model.setPedalboardControl(this.props.instanceId,uiControl.symbol,0); + } + } previewInputValue(value: number, commitValue: boolean) { let range = this.valueToRange(value); value = this.rangeToValue(range); @@ -547,13 +536,12 @@ const PluginControl = } let inputElement = this.inputRef.current; if (inputElement) { - let v = this.props.uiControl?.formatShortValue(value)??""; + let v = this.props.uiControl?.formatShortValue(value) ?? ""; inputElement.value = v; } let displayValue = this.displayValueRef.current; - if (displayValue) - { - let v = this.formatDisplayValue(this.props.uiControl,value); + if (displayValue) { + let v = this.formatDisplayValue(this.props.uiControl, value); displayValue.childNodes[0].textContent = v; } let selectElement = this.selectRef.current; @@ -577,9 +565,9 @@ const PluginControl = } onSelectChanged(e: any, value: any) { let target = e.target; - setTimeout(()=> { + setTimeout(() => { this.props.onChange(target.value); - },0); + }, 0); } @@ -610,7 +598,7 @@ const PluginControl = ); } else { return ( -
{ this.inputRef.current!.focus(); }} > - - {this.formatDisplayValue(control, value)} + + {this.formatDisplayValue(control, value)}
) diff --git a/react/src/SplitUiControls.tsx b/react/src/SplitUiControls.tsx index 0dc28e0e..e7206dc5 100644 --- a/react/src/SplitUiControls.tsx +++ b/react/src/SplitUiControls.tsx @@ -37,6 +37,7 @@ export const SplitTypeControl: UiControl = new UiControl().deserialize({ enumeration_property: true, not_on_gui: false, toggled_property: false, + trigger_property: false, scale_points: ScalePoint.deserialize_array( [ { value: 0, label: "A/B" }, @@ -60,6 +61,7 @@ export const SplitAbControl: UiControl = new UiControl().deserialize({ enumeration_property: true, not_on_gui: false, toggled_property: false, + trigger_property: false, scale_points: ScalePoint.deserialize_array( [ { value: 0, label: "A" }, @@ -83,6 +85,8 @@ export const SplitMixControl: UiControl = new UiControl().deserialize({ enumeration_property: false, not_on_gui: false, toggled_property: false, + trigger_property: false, + scale_points: [] }); @@ -100,6 +104,8 @@ export const SplitPanLeftControl: UiControl = new UiControl().deserialize({ enumeration_property: false, not_on_gui: false, toggled_property: false, + trigger_property: false, + scale_points: [] }); @@ -117,6 +123,8 @@ export const SplitVolLeftControl: UiControl = new UiControl().deserialize({ enumeration_property: false, not_on_gui: false, toggled_property: false, + trigger_property: false, + units: Units.db, scale_points: [ new ScalePoint().deserialize({ @@ -141,6 +149,8 @@ export const SplitPanRightControl: UiControl = new UiControl().deserialize({ enumeration_property: false, not_on_gui: false, toggled_property: false, + trigger_property: false, + scale_points: [] }); @@ -158,6 +168,8 @@ export const SplitVolRightControl: UiControl = new UiControl().deserialize({ enumeration_property: false, not_on_gui: false, toggled_property: false, + trigger_property: false, + units: Units.db, scale_points: [ new ScalePoint().deserialize({ diff --git a/react/src/UploadFileDialog.tsx b/react/src/UploadFileDialog.tsx index 689ff5d7..7a55e3fc 100644 --- a/react/src/UploadFileDialog.tsx +++ b/react/src/UploadFileDialog.tsx @@ -208,7 +208,7 @@ export default class UploadFileDialog extends ResizeResponsiveComponent #include "CommandLineParser.hpp" #include "SystemConfigFile.hpp" +#include "ModFileTypes.hpp" #include #include @@ -951,6 +952,7 @@ void Install(const fs::path &programPrefix, const std::string endpointAddress) } try { + // apply policy changes we dropped into the polkit configuration files (allows pipedal_d to use NetworkManager dbus apis). silentSysExec(SYSTEMCTL_BIN " restart polkit.service"); @@ -1158,6 +1160,8 @@ void Install(const fs::path &programPrefix, const std::string endpointAddress) sysExec(SYSTEMCTL_BIN " daemon-reload"); FixPermissions(); + ModFileTypes::CreateDefaultDirectories("/var/pipedal/audio_uploads"); + StopService(false); AvahiInstall(); diff --git a/src/FileEntry.cpp b/src/FileEntry.cpp index f9e787ea..2a6da62f 100644 --- a/src/FileEntry.cpp +++ b/src/FileEntry.cpp @@ -22,7 +22,20 @@ using namespace pipedal; JSON_MAP_BEGIN(FileEntry) - JSON_MAP_REFERENCE(FileEntry,filename) + JSON_MAP_REFERENCE(FileEntry,pathname) + JSON_MAP_REFERENCE(FileEntry,displayName) + JSON_MAP_REFERENCE(FileEntry,isProtected) JSON_MAP_REFERENCE(FileEntry,isDirectory) +JSON_MAP_END() + +JSON_MAP_BEGIN(BreadcrumbEntry) + JSON_MAP_REFERENCE(BreadcrumbEntry,pathname) + JSON_MAP_REFERENCE(BreadcrumbEntry,displayName) +JSON_MAP_END() + +JSON_MAP_BEGIN(FileRequestResult) + JSON_MAP_REFERENCE(FileRequestResult,files) + JSON_MAP_REFERENCE(FileRequestResult,isProtected) + JSON_MAP_REFERENCE(FileRequestResult,breadcrumbs) JSON_MAP_END() diff --git a/src/FileEntry.hpp b/src/FileEntry.hpp index 0acf5bed..f1dc3ff7 100644 --- a/src/FileEntry.hpp +++ b/src/FileEntry.hpp @@ -26,15 +26,33 @@ namespace pipedal { class FileEntry { public: FileEntry() { } - FileEntry(std::string filename,bool isDirectory) - :filename_(filename), isDirectory_(isDirectory) + FileEntry(const std::string&pathname,const std::string &displayName,bool isDirectory, bool isProtected = false) + :pathname_(pathname), displayName_(displayName), isDirectory_(isDirectory),isProtected_(isProtected) { } - std::string filename_; + std::string pathname_; + std::string displayName_; bool isDirectory_ = false; + bool isProtected_ = false; DECLARE_JSON_MAP(FileEntry); }; + class BreadcrumbEntry { + public: + std::string pathname_; + std::string displayName_; + + DECLARE_JSON_MAP(BreadcrumbEntry); + }; + + class FileRequestResult { + public: + std::vector files_; + bool isProtected_ = false; + std::vector breadcrumbs_; + DECLARE_JSON_MAP(FileRequestResult); + + }; } \ No newline at end of file diff --git a/src/FilePropertyDirectoryTree.cpp b/src/FilePropertyDirectoryTree.cpp index d6fafdd4..c73bbed6 100644 --- a/src/FilePropertyDirectoryTree.cpp +++ b/src/FilePropertyDirectoryTree.cpp @@ -20,16 +20,26 @@ #include "FilePropertyDirectoryTree.hpp" using namespace pipedal; +namespace fs = std::filesystem; FilePropertyDirectoryTree::FilePropertyDirectoryTree() { } FilePropertyDirectoryTree::FilePropertyDirectoryTree(const std::string &directoryName) - : directoryName_(directoryName) + : directoryName_(directoryName), + displayName_(fs::path(directoryName).filename().string()) +{ +} + +FilePropertyDirectoryTree::FilePropertyDirectoryTree(const std::string &directoryName, const std::string &displayName) + : directoryName_(directoryName), + displayName_(displayName) { } JSON_MAP_BEGIN(FilePropertyDirectoryTree) JSON_MAP_REFERENCE(FilePropertyDirectoryTree, directoryName) +JSON_MAP_REFERENCE(FilePropertyDirectoryTree, displayName) +JSON_MAP_REFERENCE(FilePropertyDirectoryTree, isProtected) JSON_MAP_REFERENCE(FilePropertyDirectoryTree, children) JSON_MAP_END() diff --git a/src/FilePropertyDirectoryTree.hpp b/src/FilePropertyDirectoryTree.hpp index 4ab8c4df..c29abc06 100644 --- a/src/FilePropertyDirectoryTree.hpp +++ b/src/FilePropertyDirectoryTree.hpp @@ -29,7 +29,10 @@ namespace pipedal { FilePropertyDirectoryTree(); FilePropertyDirectoryTree(const std::string&directoryName); + FilePropertyDirectoryTree(const std::string&directoryName,const std::string&displayName); std::string directoryName_; + std::string displayName_; + bool isProtected_ = false; std::vector> children_; DECLARE_JSON_MAP(FilePropertyDirectoryTree); diff --git a/src/ModFileTypes.cpp b/src/ModFileTypes.cpp new file mode 100644 index 00000000..e9c67372 --- /dev/null +++ b/src/ModFileTypes.cpp @@ -0,0 +1,156 @@ +// 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. + +#include "ModFileTypes.hpp" +#include "util.hpp" +#include +#include +#include "ss.hpp" + +using namespace pipedal; + +const std::vector ModFileTypes::ModDirectories = + { + {"audioloop", "shared/audio/Loops", "Loops", {"audio/*"}}, // Audio Loops, meant to be used for looper-style plugins + //"audiorecording","shared/audio/Audio Recordings", {"audio/*"}}, : Audio Recordings, triggered by plugins and stored in the unit + {"audiosample", "shared/audio/Samples", "Samples", {"audio/*"}}, // One-shot Audio Samples, meant to be used for sampler-style plugins + {"audiotrack", "shared/audio/Tracks", "Tracks", {"audio/*"}}, // Audio Tracks, meant to be used as full-performance/song or backtrack + {"cabsim", "CabIR", "Cab IRs", {"audio/*"}}, // Speaker Cabinets, meant as small IR audio files + + /// - h2drumkit: Hydrogen Drumkits, must use h2drumkit file extension + {"ir", "ReverbImpulseFiles", "Impulse Responses", {"audio/*"}}, // Impulse Responses + {"midiclip", "shared/midiClips", "MIDI Clips", {".mid", ".midi"}}, // MIDI Clips, to be used in sync with host tempo, must have mid or midi file extension + {"midisong", "shared/midiSongs", "MIDI Songs", {".mid", ".midi"}}, // MIDI Songs, meant to be used as full-performance/song or backtrack + {"sf2", "shared/sf2", "Sound Fonts", {"sf2", "sf3"}}, // SF2 Instruments, must have sf2 or sf3 file extension + {"sfz", "shared/sfz", "Sfz Files", {"sfz"}}, // SFZ Instruments, must have sfz file extension + // extensions observed in the field. + {"audio", "shared/audio", "Audio", {"audio/*"}}, // all audio files (Ratatoille) + {"nammodel", "NeuralAmpModels", "Neural Amp Models", {".nam"}}, // Ratatoille, Mike's NAM. + {"aidadspmodel", "shared/aidaaix", "AIDA IAX Models", {".json", ".aidaiax"}}, // Ratatoille + {"mlmodel", "ToobMlModels", "ML Models", {".json"}}, // + // +}; + +const ModFileTypes::ModDirectory *ModFileTypes::GetModDirectory(const std::string &type) +{ + for (const ModFileTypes::ModDirectory &modType : ModFileTypes::ModDirectories) + { + if (modType.modType == type) + { + return &modType; + } + } + return nullptr; +} + +ModFileTypes::ModFileTypes(const std::string &fileTypes) +{ + std::vector types = split(fileTypes, ','); + for (const auto &type : types) + { + const ModDirectory *wellKnownType = GetModDirectory(type); + if (wellKnownType) + { + rootDirectories_.push_back(type); + } + else + { + fileTypes_.push_back(type); + } + } + // for Ratatoille.lv2. + // If rootDirectories contains "nammodel" and fileTypes contains "json", add "mlmodel" too. + // if (contains(rootDirectories_,"nammodel") && contains(fileTypes_,"json")) + // { + // if (!contains(rootDirectories_,"mlmodel")) + // { + // rootDirectories_.push_back("mlmodel"); + // } + // } + if (fileTypes_.empty()) + { + for (const auto &type : types) + { + const ModDirectory *wellKnownType = GetModDirectory(type); + if (wellKnownType) + { + for (const std::string &newType : wellKnownType->defaultFileExtensions) + { + bool found = false; + for (const std::string &oldType : fileTypes_) + { + if (newType == oldType) + { + found = true; + break; + } + } + if (!found) + { + fileTypes_.push_back(newType); + } + } + } + } + } +} + +static std::filesystem::path getModDirectoryPath( + const std::filesystem::path &rootDirectory, + const std::string &modType) +{ + auto wellKnownType = ModFileTypes::GetModDirectory(modType); + if (!wellKnownType) + { + throw std::runtime_error(SS("Can't find modFileType " << modType)); + } + return rootDirectory / wellKnownType->pipedalPath; +} +void ModFileTypes::CreateDefaultDirectories(const std::filesystem::path &rootDirectory) +{ + namespace fs = std::filesystem; + using namespace std; + try + { + for (const auto &modType : ModDirectories) + { + fs::path path = rootDirectory / modType.pipedalPath; + + fs::create_directories(path); + } + + if (!fs::exists(rootDirectory / "shared" / "audio" / "Cab IR Files")) + { + fs::create_symlink( + getModDirectoryPath(rootDirectory, "cabsim"), + rootDirectory / "shared" / "audio" / "Cab IR Files"); + } + if (!fs::exists(rootDirectory / "shared" / "audio" / "Impulse Responses")) + { + fs::create_symlink( + getModDirectoryPath(rootDirectory, "ir"), + rootDirectory / "shared" / "audio" / "Impulse Responses"); + } + } + catch (const std::exception &e) + { + cout << "Can't create default MOD directories. " << e.what() << endl; + } +} + diff --git a/src/ModFileTypes.hpp b/src/ModFileTypes.hpp new file mode 100644 index 00000000..bcf49d92 --- /dev/null +++ b/src/ModFileTypes.hpp @@ -0,0 +1,53 @@ +// 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. + +#pragma once +#include +#include +#include + +namespace pipedal { + class ModFileTypes { + + public: + ModFileTypes(const std::string&fileTypes); + const std::vector &rootDirectories() { return rootDirectories_; } + const std::vector &fileTypes() { return fileTypes_;} + + private: + std::vector rootDirectories_; + std::vector fileTypes_; + + + public: + class ModDirectory { + public: + const std::string modType; + const std::string pipedalPath; + const std::string displayName; + const std::vector defaultFileExtensions; + }; + + const static std::vector ModDirectories; + static const ModDirectory* GetModDirectory(const std::string&modType); + + static void CreateDefaultDirectories(const std::filesystem::path&rootDirectory); + + }; +} \ No newline at end of file diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index 6cbe83f1..ace88736 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -2296,7 +2296,7 @@ std::vector PiPedalModel::GetFileList(const UiFileProperty &filePro return std::vector(); // don't disclose to users what the problem is. } } -std::vector PiPedalModel::GetFileList2(const std::string &relativePath, const UiFileProperty &fileProperty) +FileRequestResult PiPedalModel::GetFileList2(const std::string &relativePath, const UiFileProperty &fileProperty) { try { @@ -2305,7 +2305,7 @@ std::vector PiPedalModel::GetFileList2(const std::string &relativePat catch (const std::exception &e) { Lv2Log::warning("GetFileList() failed: (%s)", e.what()); - return std::vector(); // don't disclose to users what the problem is. + throw; } } diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index cc874fbc..e8963d75 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -432,7 +432,7 @@ namespace pipedal void SetUpdatePolicy(UpdatePolicyT updatePolicy); void ForceUpdateCheck(); std::vector GetFileList(const UiFileProperty &fileProperty); - std::vector GetFileList2(const std::string &relativePath, const UiFileProperty &fileProperty); + FileRequestResult GetFileList2(const std::string &relativePath, const UiFileProperty &fileProperty); void DeleteSampleFile(const std::filesystem::path &fileName); std::string CreateNewSampleDirectory(const std::string &relativePath, const UiFileProperty &uiFileProperty); diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index 67bd4170..612ee1cd 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -1589,8 +1589,8 @@ class PiPedalSocketHandler : public SocketHandler, public IPiPedalModelSubscribe { FileRequestArgs requestArgs; pReader->read(&requestArgs); - std::vector list = this->model.GetFileList2(requestArgs.relativePath_, requestArgs.fileProperty_); - this->Reply(replyTo, "requestFileList2", list); + FileRequestResult result = this->model.GetFileList2(requestArgs.relativePath_, requestArgs.fileProperty_); + this->Reply(replyTo, "requestFileList2", result); } else if (message == "newPreset") { diff --git a/src/PiPedalUI.cpp b/src/PiPedalUI.cpp index 3a2d637e..03cb0733 100644 --- a/src/PiPedalUI.cpp +++ b/src/PiPedalUI.cpp @@ -465,6 +465,8 @@ JSON_MAP_REFERENCE(UiFileProperty, patchProperty) JSON_MAP_REFERENCE(UiFileProperty, fileTypes) JSON_MAP_REFERENCE(UiFileProperty, portGroup) JSON_MAP_REFERENCE(UiFileProperty, resourceDirectory) +JSON_MAP_REFERENCE(UiFileProperty, modDirectories) +JSON_MAP_REFERENCE(UiFileProperty, useLegacyModDirectory) JSON_MAP_END() JSON_MAP_BEGIN(UiFrequencyPlot) diff --git a/src/PiPedalUI.hpp b/src/PiPedalUI.hpp index 6c99527a..b6e96a6a 100644 --- a/src/PiPedalUI.hpp +++ b/src/PiPedalUI.hpp @@ -114,6 +114,8 @@ namespace pipedal { std::string patchProperty_; std::string portGroup_; std::string resourceDirectory_; + std::vector modDirectories_; + bool useLegacyModDirectory_= false; public: using ptr = std::shared_ptr; UiFileProperty() { } @@ -121,9 +123,17 @@ namespace pipedal { UiFileProperty(const std::string&name, const std::string&patchProperty,const std::string &directory); + std::vector& modDirectories() { return modDirectories_; } + const std::vector& modDirectories() const { return modDirectories_; } + + bool useLegacyModDirectory() const { return useLegacyModDirectory_; } + void useLegacyModDirectory(bool value) { useLegacyModDirectory_ = value; } const std::string &label() const { return label_; } int32_t index() const { return index_; } + const std::string &directory() const { return directory_; } + void directory(const std::string &path) { directory_ = path; } + const std::string&portGroup() const { return portGroup_; } const std::vector &fileTypes() const { return fileTypes_; } diff --git a/src/PluginHost.cpp b/src/PluginHost.cpp index 8239316c..d987f67d 100644 --- a/src/PluginHost.cpp +++ b/src/PluginHost.cpp @@ -46,6 +46,7 @@ #include "PiPedalException.hpp" #include "StdErrorCapture.hpp" #include "util.hpp" +#include "ModFileTypes.hpp" #include "Locale.hpp" @@ -118,6 +119,7 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld) core__toggled = lilv_new_uri(pWorld, LV2_CORE__toggled); core__connectionOptional = lilv_new_uri(pWorld, LV2_CORE__connectionOptional); portprops__not_on_gui_property_uri = lilv_new_uri(pWorld, LV2_PORT_PROPS__notOnGUI); + portprops__trigger = lilv_new_uri(pWorld, LV2_PORT_PROPS__trigger); midi__event = lilv_new_uri(pWorld, LV2_MIDI__MidiEvent); core__designation = lilv_new_uri(pWorld, LV2_CORE__designation); portgroups__group = lilv_new_uri(pWorld, LV2_PORT_GROUPS__group); @@ -191,8 +193,7 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld) dc__format = lilv_new_uri(pWorld, "http://purl.org/dc/terms/format"); - mod__fileTypes = lilv_new_uri(pWorld,"http://moddevices.com/ns/mod#fileTypes"); - + mod__fileTypes = lilv_new_uri(pWorld, "http://moddevices.com/ns/mod#fileTypes"); } void PluginHost::LilvUris::Free() @@ -477,8 +478,7 @@ void PluginHost::Load(const char *lv2Path) const char *pb1 = left->name().c_str(); const char *pb2 = right->name().c_str(); return collator->Compare( - left->name(),right->name()) - < 0; + left->name(), right->name()) < 0; }; std::sort(this->plugins_.begin(), this->plugins_.end(), compare); @@ -624,15 +624,47 @@ std::shared_ptr Lv2PluginInfo::FindWritablePathProperties(PluginHost strLabel, propertyUri.AsUri(), lv2DirectoryName); AutoLilvNodes mod__fileTypes = lilv_world_find_nodes(pWorld, propertyUri, lv2Host->lilvUris->mod__fileTypes, nullptr); - LILV_FOREACH(nodes,i,mod__fileTypes) + LILV_FOREACH(nodes, i, mod__fileTypes) { - // "nam,nammodel" - AutoLilvNode lilvfileType {lilv_nodes_get(mod__fileTypes, i)}; + // "nam,nammodel" + AutoLilvNode lilvfileType{lilv_nodes_get(mod__fileTypes, i)}; std::string fileTypes = lilvfileType.AsString(); + ModFileTypes modFileTypes(fileTypes); + + for (const std::string &rootDirectory : modFileTypes.rootDirectories()) + { + fileProperty->modDirectories().push_back(rootDirectory); + } + for (const std::string &type : modFileTypes.fileTypes()) + { + fileProperty->fileTypes().push_back(UiFileType(SS(type << " file"), SS('.' << type))); + } + // Legacy case: audio_uploads/ exists. + + std::filesystem::path bundleDirectoryName = std::filesystem::path(bundle_path()).parent_path().filename(); + std::filesystem::path legacyUploadPath = lv2Host->MapPath(bundleDirectoryName.string()); - for (std::string&type: split(fileTypes,',')) + if (std::filesystem::exists(legacyUploadPath)) { - fileProperty->fileTypes().push_back(UiFileType(SS(type << " file"),SS('.' << type))); + if (!std::filesystem::exists(legacyUploadPath / ".migrated")) + { + fileProperty->useLegacyModDirectory(true); + fileProperty->directory(bundleDirectoryName); + } + } + if (fileProperty->modDirectories().size() == 0) + { + fileProperty->directory(bundleDirectoryName); + } + else if (fileProperty->modDirectories().size() == 1 && !fileProperty->useLegacyModDirectory()) // no synthetic root. + { + auto modType = ModFileTypes::GetModDirectory(fileProperty->modDirectories()[0]); + if (modType) + { + fileProperty->directory(modType->pipedalPath); + } + } else { + // handled at request time. } } if (!mod__fileTypes) @@ -932,6 +964,7 @@ Lv2PortInfo::Lv2PortInfo(PluginHost *host, const LilvPlugin *plugin, const LilvP this->toggled_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris->core__toggled); this->not_on_gui_ = lilv_port_has_property(plugin, pPort, host->lilvUris->portprops__not_on_gui_property_uri); this->connection_optional_ = lilv_port_has_property(plugin, pPort, host->lilvUris->core__connectionOptional); + this->trigger_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris->portprops__trigger); LilvScalePoints *pScalePoints = lilv_port_get_scale_points(plugin, pPort); LILV_FOREACH(scale_points, iSP, pScalePoints) @@ -1387,33 +1420,36 @@ std::shared_ptr PluginHost::GetHostWorkerThread() return pHostWorkerThread; } -class ResourceInfo { +class ResourceInfo +{ public: - ResourceInfo(const std::string&filePropertyDirectory,const std::string&resourceDirectory) - :filePropertyDirectory(filePropertyDirectory), resourceDirectory(resourceDirectory) {} + ResourceInfo(const std::string &filePropertyDirectory, const std::string &resourceDirectory) + : filePropertyDirectory(filePropertyDirectory), resourceDirectory(resourceDirectory) {} std::string filePropertyDirectory; std::string resourceDirectory; - bool operator==(const ResourceInfo&other) const { + bool operator==(const ResourceInfo &other) const + { return this->filePropertyDirectory == other.filePropertyDirectory && this->resourceDirectory == other.resourceDirectory; - } - bool operator<(const ResourceInfo&other) const { + bool operator<(const ResourceInfo &other) const + { if (this->filePropertyDirectory < other.filePropertyDirectory) return true; if (this->filePropertyDirectory > other.filePropertyDirectory) return false; - return this->resourceDirectory < other.resourceDirectory; - + return this->resourceDirectory < other.resourceDirectory; } }; -static bool anyTargetFilesExist(const std::filesystem::path &sourceDirectory, const std::filesystem::path&targetDirectory) +static bool anyTargetFilesExist(const std::filesystem::path &sourceDirectory, const std::filesystem::path &targetDirectory) { namespace fs = std::filesystem; - try { - if (!fs::exists(targetDirectory)) return false; - for (auto&directoryEntry : fs::directory_iterator(sourceDirectory)) + try + { + if (!fs::exists(targetDirectory)) + return false; + for (auto &directoryEntry : fs::directory_iterator(sourceDirectory)) { fs::path thisPath = directoryEntry.path(); if (directoryEntry.is_directory()) @@ -1421,11 +1457,13 @@ static bool anyTargetFilesExist(const std::filesystem::path &sourceDirectory, co auto name = thisPath.filename(); fs::path childSource = sourceDirectory / name; fs::path childTarget = targetDirectory / name; - if (anyTargetFilesExist(childSource,childTarget)) + if (anyTargetFilesExist(childSource, childTarget)) { return true; } - } else { + } + else + { fs::path targetPath = targetDirectory / thisPath.filename(); if (fs::exists(targetPath)) { @@ -1433,9 +1471,9 @@ static bool anyTargetFilesExist(const std::filesystem::path &sourceDirectory, co } } } - - } catch (const std::exception&) { - + } + catch (const std::exception &) + { } return false; } @@ -1450,48 +1488,52 @@ static void createTargetLinks(const std::filesystem::path &sourceDirectory, cons { fs::path childSource = dirEntry.path(); fs::path childTarget = targetDirectory / childSource.filename(); - if (dirEntry.is_directory()) { - createTargetLinks(childSource,childTarget); - } else { - fs::create_symlink(childSource,childTarget); + if (dirEntry.is_directory()) + { + createTargetLinks(childSource, childTarget); + } + else + { + fs::create_symlink(childSource, childTarget); } } } -void PluginHost::CheckForResourceInitialization(const std::string &pluginUri,const std::filesystem::path&pluginUploadDirectory) +void PluginHost::CheckForResourceInitialization(const std::string &pluginUri, const std::filesystem::path &pluginUploadDirectory) { auto plugin = GetPluginInfo(pluginUri); if (plugin) { std::filesystem::path bundlePath = plugin->bundle_path(); - if (!plugin->piPedalUI()) + if (!plugin->piPedalUI()) return; - const auto& fileProperties = plugin->piPedalUI()->fileProperties(); + const auto &fileProperties = plugin->piPedalUI()->fileProperties(); if (fileProperties.size() != 0 && !pluginsThatHaveBeenCheckedForResources.contains(pluginUri)) { pluginsThatHaveBeenCheckedForResources.insert(pluginUri); // eliminate duplicates. std::set resourceInfoSet; - for (const auto&fileProperty: fileProperties) + for (const auto &fileProperty : fileProperties) { if (!fileProperty->resourceDirectory().empty() && !fileProperty->directory().empty()) { - resourceInfoSet.insert(ResourceInfo(fileProperty->directory(),fileProperty->resourceDirectory())); + resourceInfoSet.insert(ResourceInfo(fileProperty->directory(), fileProperty->resourceDirectory())); } } - try { - for (const ResourceInfo&resourceInfo: resourceInfoSet) + try { - std::filesystem::path sourcePath = bundlePath / resourceInfo.resourceDirectory; - std::filesystem::path targetPath = pluginUploadDirectory / resourceInfo.filePropertyDirectory; - if (!anyTargetFilesExist(sourcePath,targetPath)) + for (const ResourceInfo &resourceInfo : resourceInfoSet) { - createTargetLinks(sourcePath,targetPath); + std::filesystem::path sourcePath = bundlePath / resourceInfo.resourceDirectory; + std::filesystem::path targetPath = pluginUploadDirectory / resourceInfo.filePropertyDirectory; + if (!anyTargetFilesExist(sourcePath, targetPath)) + { + createTargetLinks(sourcePath, targetPath); + } } - } - } catch (const std::exception &e) + catch (const std::exception &e) { Lv2Log::error(SS("CheckForResourceInitialization: " << e.what())); } @@ -1499,20 +1541,17 @@ void PluginHost::CheckForResourceInitialization(const std::string &pluginUri,con } } -json_variant PluginHost::MapPath(const json_variant&json) +json_variant PluginHost::MapPath(const json_variant &json) { AtomConverter converter(GetMapFeature()); - return converter.MapPath(json,GetPluginStoragePath()); - + return converter.MapPath(json, GetPluginStoragePath()); } -json_variant PluginHost::AbstractPath(const json_variant&json) +json_variant PluginHost::AbstractPath(const json_variant &json) { AtomConverter converter(GetMapFeature()); - return converter.AbstractPath(json,GetPluginStoragePath()); - + return converter.AbstractPath(json, GetPluginStoragePath()); } - std::string PluginHost::MapPath(const std::string &abstractPath) { auto storagePath = GetPluginStoragePath(); @@ -1521,10 +1560,8 @@ std::string PluginHost::MapPath(const std::string &abstractPath) return ""; } return SS(storagePath << '/' << abstractPath); - - } -std::string PluginHost::AbstractPath(const std::string&path) +std::string PluginHost::AbstractPath(const std::string &path) { auto storagePath = GetPluginStoragePath(); if (path.starts_with(storagePath)) @@ -1593,7 +1630,7 @@ json_map::storage_type Lv2PortInfo::jmap{ MAP_REF(Lv2PortInfo, is_logarithmic), MAP_REF(Lv2PortInfo, display_priority), MAP_REF(Lv2PortInfo, range_steps), - MAP_REF(Lv2PortInfo, trigger), + MAP_REF(Lv2PortInfo, trigger_property), MAP_REF(Lv2PortInfo, integer_property), MAP_REF(Lv2PortInfo, enumeration_property), MAP_REF(Lv2PortInfo, toggled_property), @@ -1662,6 +1699,7 @@ json_map::storage_type Lv2PluginUiPort::jmap{{ MAP_REF(Lv2PluginUiPort, enumeration_property), MAP_REF(Lv2PluginUiPort, not_on_gui), MAP_REF(Lv2PluginUiPort, toggled_property), + MAP_REF(Lv2PluginUiPort, trigger_property), MAP_REF(Lv2PluginUiPort, scale_points), MAP_REF(Lv2PluginUiPort, port_group), diff --git a/src/PluginHost.hpp b/src/PluginHost.hpp index 88339d3c..26382074 100644 --- a/src/PluginHost.hpp +++ b/src/PluginHost.hpp @@ -214,7 +214,7 @@ namespace pipedal bool is_logarithmic_ = false; int display_priority_ = -1; int range_steps_ = 0; - bool trigger_ = false; + bool trigger_property_ = false; bool integer_property_ = false; bool enumeration_property_ = false; bool toggled_property_ = false; @@ -285,7 +285,7 @@ namespace pipedal LV2_PROPERTY_GETSET_SCALAR(is_logarithmic); LV2_PROPERTY_GETSET_SCALAR(display_priority); LV2_PROPERTY_GETSET_SCALAR(range_steps); - LV2_PROPERTY_GETSET_SCALAR(trigger); + LV2_PROPERTY_GETSET_SCALAR(trigger_property); LV2_PROPERTY_GETSET_SCALAR(integer_property); LV2_PROPERTY_GETSET_SCALAR(enumeration_property); LV2_PROPERTY_GETSET_SCALAR(toggled_property); @@ -496,6 +496,7 @@ namespace pipedal default_value_(pPort->default_value()), range_steps_(pPort->range_steps()), display_priority_(pPort->display_priority()), is_logarithmic_(pPort->is_logarithmic()), integer_property_(pPort->integer_property()), enumeration_property_(pPort->enumeration_property()), toggled_property_(pPort->toggled_property()), not_on_gui_(pPort->not_on_gui()), scale_points_(pPort->scale_points()), + trigger_property_(pPort->trigger_property()), comment_(pPort->comment()), units_(pPort->units()), connection_optional_(pPort->connection_optional()) { @@ -536,6 +537,7 @@ namespace pipedal bool enumeration_property_ = false; bool not_on_gui_ = false; bool toggled_property_ = false; + bool trigger_property_ = false; std::vector scale_points_; std::string port_group_; @@ -561,6 +563,7 @@ namespace pipedal LV2_PROPERTY_GETSET_SCALAR(integer_property); LV2_PROPERTY_GETSET_SCALAR(enumeration_property); LV2_PROPERTY_GETSET_SCALAR(toggled_property); + LV2_PROPERTY_GETSET_SCALAR(trigger_property); LV2_PROPERTY_GETSET_SCALAR(not_on_gui); LV2_PROPERTY_GETSET(scale_points); LV2_PROPERTY_GETSET(units); @@ -664,6 +667,7 @@ namespace pipedal AutoLilvNode core__toggled; AutoLilvNode core__connectionOptional; AutoLilvNode portprops__not_on_gui_property_uri; + AutoLilvNode portprops__trigger; AutoLilvNode midi__event; AutoLilvNode core__designation; AutoLilvNode portgroups__group; diff --git a/src/Storage.cpp b/src/Storage.cpp index fbea14fb..9f1a96c2 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -22,6 +22,7 @@ #include "Storage.hpp" #include "AudioConfig.hpp" #include "PiPedalException.hpp" +#include #include #include "json.hpp" #include @@ -32,14 +33,50 @@ #include "PluginHost.hpp" #include "ss.hpp" #include "ofstream_synced.hpp" +#include "ModFileTypes.hpp" using namespace pipedal; +namespace fs = std::filesystem; const char *BANK_EXTENSION = ".bank"; const char *BANKS_FILENAME = "index.banks"; #define USER_SETTINGS_FILENAME "userSettings.json"; + +static bool isSubdirectory(const fs::path&path, const fs::path&basePath) +{ + auto iPath = path.begin(); + for (auto i = basePath.begin(); i != basePath.end(); ++i) + { + if (iPath == path.end()) + { + return false; + } + + if ((*i) != (*iPath)) + { + return false; + } + ++iPath; + } + while (iPath != path.end()) + { + if (iPath->string() == "..") + { + return false; + } + ++iPath; + } + return true; +} + +static bool hasSyntheticModRoot(const UiFileProperty &fileProperty) +{ + return (fileProperty.modDirectories().size() > 1 || (fileProperty.modDirectories().size() == 1 && fileProperty.useLegacyModDirectory())); + +} + Storage::Storage() { SetConfigRoot("~/var/Config"); @@ -1648,74 +1685,197 @@ static bool ensureNoDotDot(const std::filesystem::path&path) return true; } - -std::vector Storage::GetFileList2(const std::string &relativePath,const UiFileProperty &fileProperty) +static void AddFilesToResult( + FileRequestResult&result, + const UiFileProperty &fileProperty, + const fs::path&rootPath) { - if (!ensureNoDotDot(relativePath)) - { - ThrowPermissionDeniedError(); - } - if (!UiFileProperty::IsDirectoryNameValid(fileProperty.directory())) + if (!fs::exists(rootPath)) { - ThrowPermissionDeniedError(); + return; // silently without error. } - - std::vector result; - - // if fileProperty has a user-accessible directory, push the entire file path. - if (fileProperty.directory().size() != 0) + auto & resultFiles = result.files_; + try { - std::filesystem::path audioFileDirectory = this->GetPluginUploadDirectory() / fileProperty.directory() / relativePath; - try + for (auto const &dir_entry : std::filesystem::directory_iterator(rootPath)) { - for (auto const &dir_entry : std::filesystem::directory_iterator(audioFileDirectory)) + const auto &path = dir_entry.path(); + auto name = path.filename().string(); + if (dir_entry.is_regular_file()) { - if (dir_entry.is_regular_file()) + if (name.length() > 0 && name[0] != '.') // don't show hidden files. { - auto &path = dir_entry.path(); - auto name = path.filename().string(); - if (name.length() > 0 && name[0] != '.') // don't show hidden files. + if (fileProperty.IsValidExtension(path.extension().string())) { - if (fileProperty.IsValidExtension(path.extension().string())) - { - // a relative path! - result.push_back(FileEntry(path,false)); - } + resultFiles.push_back( + FileEntry(path,name,false,false) + ); } - } else if (dir_entry.is_directory()) { - result.push_back(FileEntry(dir_entry.path(),true)); } + } else if (dir_entry.is_directory()) { + resultFiles.push_back(FileEntry{path,name,true,fs::is_symlink(path)}); } } - catch (const std::exception &error) - { - throw std::logic_error("GetFileList failed. Directory not found: " + audioFileDirectory.string()); - } + } + catch (const std::exception &error) + { + throw std::logic_error("GetFileList failed. Directory not found: " + rootPath.string()); } // sort lexicographically auto collator = Locale::GetInstance()->GetCollator(); - std::sort(result.begin(), result.end(),[&collator](const FileEntry&l, const FileEntry&r) { + std::sort(resultFiles.begin(), resultFiles.end(),[&collator](const FileEntry&l, const FileEntry&r) { if (l.isDirectory_ != r.isDirectory_) { return l.isDirectory_ > r.isDirectory_; } - return collator->Compare(l.filename_,r.filename_) < 0; - + return collator->Compare(l.displayName_,r.displayName_) < 0; }); +} +FileRequestResult Storage::GetModFileList2(const std::string &relativePath,const UiFileProperty &fileProperty) +{ + FileRequestResult result; + fs::path uploadsDirectory = GetPluginUploadDirectory(); + + if (relativePath.empty()) + { + // return the synthetic root. + result.isProtected_ = true; + + for (const auto&modDirectory: fileProperty.modDirectories()) + { + const auto directoryInfo = ModFileTypes::GetModDirectory(modDirectory); + if (directoryInfo) + { + result.files_.push_back( + FileEntry(uploadsDirectory / directoryInfo->pipedalPath,directoryInfo->displayName,true,true) + ); + } + + } + if (fileProperty.useLegacyModDirectory()) + { + result.files_.push_back( + FileEntry(uploadsDirectory / fileProperty.directory(),fs::path(fileProperty.directory()).filename().string(),true,true) + ); + } + result.breadcrumbs_.push_back({"","Home"}); + return result; + } + fs::path modDirectoryPath; + + result.breadcrumbs_.push_back({"","Home"}); + fs::path fsRelativePath { relativePath}; + + for (const auto &modDirectory: fileProperty.modDirectories()) + { + const ModFileTypes::ModDirectory*modDirectoryInfo = ModFileTypes::GetModDirectory(modDirectory); + if (modDirectoryInfo) + { + if (isSubdirectory(fsRelativePath, uploadsDirectory / modDirectoryInfo->pipedalPath)) + { + modDirectoryPath = uploadsDirectory / modDirectoryInfo->pipedalPath; + result.breadcrumbs_.push_back({modDirectoryPath.string(),modDirectoryInfo->displayName}); + break; + } + + } + } + if (modDirectoryPath.empty() && fileProperty.useLegacyModDirectory()) + { + if (isSubdirectory(fsRelativePath, uploadsDirectory / fileProperty.directory())) + { + modDirectoryPath = uploadsDirectory / fileProperty.directory(); + result.breadcrumbs_.push_back({modDirectoryPath.string(),fs::path(fileProperty.directory()).filename().string()}); + } else { + ThrowPermissionDeniedError(); + } + } + + // add remaing path segements as breadcrumbs. + { + fs::path modPath {modDirectoryPath}; + fs::path rp { relativePath}; + auto iRp = rp.begin(); + // skip past one or more segments in the modDiretoryPath. + for (auto iModPath = modPath.begin(); iModPath != modPath.end(); ++iModPath) + { + if (iRp != rp.end()) { + ++iRp; + } + } + while (iRp != rp.end()) + { + result.breadcrumbs_.push_back({*iRp,*iRp}); + ++iRp; + } + } + + AddFilesToResult(result,fileProperty,relativePath); return result; } -bool Storage::IsValidSampleFileName(const std::filesystem::path &fileName) +FileRequestResult Storage::GetFileList2(const std::string &relativePath_,const UiFileProperty &fileProperty) { - if (!fileName.is_absolute()) + std::string absolutePath = relativePath_; + if (!ensureNoDotDot(absolutePath)) { - return false; + ThrowPermissionDeniedError(); + } + if (hasSyntheticModRoot(fileProperty)) + { + return Storage::GetModFileList2(absolutePath,fileProperty); + } + + FileRequestResult result; + + if (fileProperty.directory().empty()) + { + throw std::runtime_error("fileProperty.directory() not specified."); + } + std::filesystem::path pluginRootDirectory = this->GetPluginUploadDirectory() / fileProperty.directory(); + if (absolutePath == "") + { + absolutePath = pluginRootDirectory.string(); } - if (!ensureNoDotDot(fileName)) + + + { + result.breadcrumbs_.push_back({"","Home"}); + fs::path fsAbsolutePath {absolutePath}; + auto iAbsolutePath = fsAbsolutePath.begin(); + for (auto i = pluginRootDirectory.begin(); i != pluginRootDirectory.end(); ++i) + { + if (iAbsolutePath == fsAbsolutePath.end() || (*i) != *iAbsolutePath) + { + throw std::runtime_error("Directory is not a subdirectory of the plugin root directory."); + } + ++iAbsolutePath; + } + auto cumulativePath = pluginRootDirectory; + + while (iAbsolutePath != fsAbsolutePath.end()) + { + cumulativePath /= (*iAbsolutePath); + result.breadcrumbs_.push_back({cumulativePath.string(),iAbsolutePath->string()}); + ++iAbsolutePath; + } + } + if (!isSubdirectory(absolutePath,pluginRootDirectory)) + { + throw std::runtime_error(SS("Improper location. " << absolutePath)); + } + AddFilesToResult(result,fileProperty,absolutePath); + return result; +} + + +bool Storage::IsValidSampleFileName(const std::filesystem::path &fileName) +{ + if (!fileName.is_absolute()) { return false; } @@ -1723,21 +1883,28 @@ bool Storage::IsValidSampleFileName(const std::filesystem::path &fileName) std::filesystem::path audioFilePath = this->GetPluginUploadDirectory(); std::filesystem::path parentDirectory = fileName.parent_path(); - while (true) + + auto iTarget = parentDirectory.begin(); + + for (auto i = audioFilePath.begin(); i != audioFilePath.end(); ++i) { - if (!parentDirectory.has_parent_path()) - { + if (iTarget == parentDirectory.end()) { return false; } - std::string name = parentDirectory.filename().string(); - if (parentDirectory == audioFilePath) - return true; - parentDirectory = parentDirectory.parent_path(); - if (parentDirectory.string().length() < audioFilePath.string().length()) + if (*i != *iTarget) { + return false; + } + ++iTarget; + } + while (iTarget != parentDirectory.end()) + { + if (iTarget->string() == "..") { return false; } + ++iTarget; } + return true; } void Storage::DeleteSampleFile(const std::filesystem::path &fileName) { @@ -1753,9 +1920,18 @@ void Storage::DeleteSampleFile(const std::filesystem::path &fileName) { if (std::filesystem::is_directory(fileName)) { - if (fileName.string().length() > 1) // guard against rm -rf / (bitter experience) + if (fileName.string().length() <= 1) // guard against rm -rf / (bitter experience) + { + throw std::logic_error("Invalid filename."); + } + if (std::filesystem::is_symlink(fileName)) { - std::filesystem::remove_all(fileName); + std::filesystem::remove(fileName); + } else { + if (fileName.string().length() > 1) // guard against rm -rf / (bitter experience) + { + std::filesystem::remove_all(fileName); + } } } else { @@ -1769,13 +1945,13 @@ void Storage::DeleteSampleFile(const std::filesystem::path &fileName) } std::filesystem::path Storage::MakeUserFilePath(const std::string &directory, const std::string &filename) { - if (!ensureNoDotDot(directory)) - { - throw std::logic_error(SS("Invalide filename: " << filename)); - } std::filesystem::path filePath{filename}; - std::filesystem::path result = this->GetPluginUploadDirectory() / directory / filename; + std::filesystem::path result = fs::path(directory) / filename; + if (!result.is_absolute()) + { + result = this->GetPluginUploadDirectory() / result; + } if (!this->IsValidSampleFileName(result)) { throw std::logic_error(SS("Invalid upload path: " << result)); @@ -1898,8 +2074,8 @@ void Storage::FillSampleDirectoryTree(FilePropertyDirectoryTree*node, const std: { if (child.is_directory()) { - const auto& childPath = child.path(); - FilePropertyDirectoryTree::ptr childTree = std::make_unique(childPath.filename()); + const auto& childPath = child.path(); + FilePropertyDirectoryTree::ptr childTree = std::make_unique(childPath); FillSampleDirectoryTree(childTree.get(),childPath); node->children_.push_back(std::move(childTree)); } @@ -1911,22 +2087,53 @@ void Storage::FillSampleDirectoryTree(FilePropertyDirectoryTree*node, const std: return collator->Compare(left->directoryName_,right->directoryName_) < 0; }); } -FilePropertyDirectoryTree::ptr Storage::GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty) const +FilePropertyDirectoryTree::ptr Storage::GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty) { - FilePropertyDirectoryTree::ptr result = std::make_unique(""); - if (uiFileProperty.directory().empty()) - { - throw std::runtime_error("Invalid uiFileProperty"); - } - if (!ensureNoDotDot(uiFileProperty.directory())) + fs::path uploadDirectory = this->GetPluginUploadDirectory(); + + if (hasSyntheticModRoot(uiFileProperty)) { - throw std::runtime_error("Invalid uiFileProperty"); - } - std::filesystem::path rootDirectory = this->GetPluginUploadDirectory() / uiFileProperty.directory(); + FilePropertyDirectoryTree::ptr result = std::make_unique("","Home"); + result->isProtected_ = true; + for (const auto& modDirectory: uiFileProperty.modDirectories()) + { + auto modDirectoryInfo = ModFileTypes::GetModDirectory(modDirectory); + if (modDirectoryInfo) + { + auto childPath = uploadDirectory / modDirectoryInfo->pipedalPath; + FilePropertyDirectoryTree::ptr child = std::make_unique( + childPath,modDirectoryInfo->displayName + ); + FillSampleDirectoryTree(child.get(),childPath); + result->children_.push_back(std::move(child)); + } + } + if (uiFileProperty.useLegacyModDirectory()) { + auto childPath = uploadDirectory / uiFileProperty.directory(); + FilePropertyDirectoryTree::ptr child = std::make_unique( + childPath + ); + FillSampleDirectoryTree(child.get(),childPath); + result->children_.push_back(std::move(child)); + } + return result; - FillSampleDirectoryTree(result.get(),rootDirectory); + } else { + if (uiFileProperty.directory().empty()) + { + throw std::runtime_error("Invalid uiFileProperty"); + } + if (!ensureNoDotDot(uiFileProperty.directory())) + { + throw std::runtime_error("Invalid uiFileProperty"); + } + std::filesystem::path rootDirectory = uploadDirectory / uiFileProperty.directory(); + FilePropertyDirectoryTree::ptr result = std::make_unique(rootDirectory.string(),"Home"); + + FillSampleDirectoryTree(result.get(),rootDirectory); + return result; + } - return result; } diff --git a/src/Storage.hpp b/src/Storage.hpp index e00da98a..15df9e99 100644 --- a/src/Storage.hpp +++ b/src/Storage.hpp @@ -152,7 +152,9 @@ class Storage { int64_t DeleteBank(int64_t bankId); std::vector GetFileList(const UiFileProperty&fileProperty); - std::vector GetFileList2(const std::string&relativePath,const UiFileProperty&fileProperty); + FileRequestResult GetFileList2(const std::string&relativePath,const UiFileProperty&fileProperty); + + FileRequestResult GetModFileList2(const std::string &relativePath,const UiFileProperty &fileProperty); void SetJackChannelSelection(const JackChannelSelection&channelSelection); @@ -224,7 +226,7 @@ class Storage { const std::string&oldRelativePath, const std::string&newRelativePath, const UiFileProperty&uiFileProperty); - FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty) const; + FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty); }; diff --git a/src/WebServerConfig.cpp b/src/WebServerConfig.cpp index e516139c..4097268b 100644 --- a/src/WebServerConfig.cpp +++ b/src/WebServerConfig.cpp @@ -1,4 +1,4 @@ - // Copyright (c) 2024 Robin Davies +// 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 @@ -32,7 +32,7 @@ #include "UpdaterSecurity.hpp" #include "TemporaryFile.hpp" #include "PresetBundle.hpp" - +#include "json.hpp" #define OLD_PRESET_EXTENSION ".piPreset" #define PRESET_EXTENSION ".piPreset" @@ -50,40 +50,52 @@ using namespace pipedal; using namespace boost::system; namespace fs = std::filesystem; +class UserUploadResponse +{ +public: + std::string errorMessage_; + std::string path_; + DECLARE_JSON_MAP(UserUploadResponse); +}; +JSON_MAP_BEGIN(UserUploadResponse) +JSON_MAP_REFERENCE(UserUploadResponse, errorMessage) +JSON_MAP_REFERENCE(UserUploadResponse, path) +JSON_MAP_END() static bool IsZipFile(const std::filesystem::path &path) { std::ifstream f(path); - if (!f.is_open()) return false; + if (!f.is_open()) + return false; char c[4]; - memset(c,0,sizeof(c)); + memset(c, 0, sizeof(c)); f >> c[0] >> c[1] >> c[2] >> c[3]; // official file header according to PKware documetnation. return c[0] == 0x50 && c[1] == 0x4B && c[2] == 0x03 && c[3] == 0x04; - - } -class ExtensionChecker { +class ExtensionChecker +{ public: - ExtensionChecker(const std::string&extensionList) - :extensions(split(extensionList,',')) + ExtensionChecker(const std::string &extensionList) + : extensions(split(extensionList, ',')) { } - bool IsValidExtension(const std::string&extension) + bool IsValidExtension(const std::string &extension) { - if (extensions.size() == 0) + if (extensions.size() == 0) return true; - for (const auto&ext: extensions) + for (const auto &ext : extensions) { - if (ext == extension) + if (ext == extension) return true; } return false; } + private: std::vector extensions; }; @@ -133,7 +145,8 @@ class DownloadIntercept : public RequestHandler else if (segment == "uploadBank") { return true; - } else if (segment == "uploadUserFile") + } + else if (segment == "uploadUserFile") { return true; } @@ -159,7 +172,7 @@ class DownloadIntercept : public RequestHandler PluginPresets pluginPresets = model->GetPluginPresets(pluginUri); std::stringstream s; - json_writer writer(s,false); + json_writer writer(s, false); writer.write(pluginPresets); *pContent = s.str(); } @@ -176,7 +189,7 @@ class DownloadIntercept : public RequestHandler file.selectedPreset(newInstanceId); std::stringstream s; - json_writer writer(s,false); + json_writer writer(s, false); writer.write(file); *pContent = s.str(); *pName = pedalboard.name(); @@ -210,12 +223,11 @@ class DownloadIntercept : public RequestHandler std::string content; GetPluginPresets(request_uri, &name, &content); - TemporaryFile tmpFile { WEB_TEMP_DIR}; - PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePluginPresetsFile(*(this->model),content); + TemporaryFile tmpFile{WEB_TEMP_DIR}; + PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePluginPresetsFile(*(this->model), content); presetbundleWriter->WriteToFile(tmpFile.Path()); size_t contentLength = std::filesystem::file_size(tmpFile.Path()); - res.set(HttpField::content_type, PLUGIN_PRESETS_MIME_TYPE); res.set(HttpField::cache_control, "no-cache"); res.setContentLength(content.length()); @@ -228,8 +240,8 @@ class DownloadIntercept : public RequestHandler std::string content; GetPreset(request_uri, &name, &content); - TemporaryFile tmpFile { WEB_TEMP_DIR}; - PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePresetsFile(*(this->model),content); + TemporaryFile tmpFile{WEB_TEMP_DIR}; + PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePresetsFile(*(this->model), content); presetbundleWriter->WriteToFile(tmpFile.Path()); size_t contentLength = std::filesystem::file_size(tmpFile.Path()); @@ -245,14 +257,11 @@ class DownloadIntercept : public RequestHandler std::string content; GetBank(request_uri, &name, &content); - TemporaryFile tmpFile { WEB_TEMP_DIR}; - PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePresetsFile(*(this->model),content); + TemporaryFile tmpFile{WEB_TEMP_DIR}; + PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePresetsFile(*(this->model), content); presetbundleWriter->WriteToFile(tmpFile.Path()); size_t contentLength = std::filesystem::file_size(tmpFile.Path()); - - - res.set(HttpField::content_type, BANK_MIME_TYPE); res.set(HttpField::cache_control, "no-cache"); res.set(HttpField::content_disposition, GetContentDispositionHeader(name, BANK_EXTENSION)); @@ -290,12 +299,11 @@ class DownloadIntercept : public RequestHandler std::string content; GetPluginPresets(request_uri, &name, &content); - std::shared_ptr tmpFile =std::make_shared(WEB_TEMP_DIR); - PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePluginPresetsFile(*(this->model),content); + std::shared_ptr tmpFile = std::make_shared(WEB_TEMP_DIR); + PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePluginPresetsFile(*(this->model), content); presetbundleWriter->WriteToFile(tmpFile->Path()); size_t contentLength = std::filesystem::file_size(tmpFile->Path()); - res.set(HttpField::content_type, PLUGIN_PRESETS_MIME_TYPE); res.set(HttpField::cache_control, "no-cache"); res.setContentLength(contentLength); @@ -308,12 +316,11 @@ class DownloadIntercept : public RequestHandler std::string content; GetPreset(request_uri, &name, &content); - std::shared_ptr tmpFile =std::make_shared(WEB_TEMP_DIR); - PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePresetsFile(*(this->model),content); + std::shared_ptr tmpFile = std::make_shared(WEB_TEMP_DIR); + PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePresetsFile(*(this->model), content); presetbundleWriter->WriteToFile(tmpFile->Path()); size_t contentLength = std::filesystem::file_size(tmpFile->Path()); - res.set(HttpField::content_type, PRESET_MIME_TYPE); res.set(HttpField::cache_control, "no-cache"); res.setContentLength(contentLength); @@ -326,13 +333,11 @@ class DownloadIntercept : public RequestHandler std::string content; GetBank(request_uri, &name, &content); - std::shared_ptr tmpFile =std::make_shared(WEB_TEMP_DIR); - PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePresetsFile(*(this->model),content); + std::shared_ptr tmpFile = std::make_shared(WEB_TEMP_DIR); + PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePresetsFile(*(this->model), content); presetbundleWriter->WriteToFile(tmpFile->Path()); size_t contentLength = std::filesystem::file_size(tmpFile->Path()); - - res.set(HttpField::content_type, BANK_MIME_TYPE); res.set(HttpField::cache_control, "no-cache"); res.setContentLength(contentLength); @@ -356,40 +361,49 @@ class DownloadIntercept : public RequestHandler } } } - static std::string GetFirstFolderOrFile(const std::vector&fileNames) + static std::string GetFirstFolderOrFile(const std::vector &fileNames) { - for (const auto &fileName: fileNames) + for (const auto &fileName : fileNames) { size_t nPos = fileName.find('/'); - if (nPos != std::string::npos) { - return fileName.substr(0,nPos); + if (nPos != std::string::npos) + { + return fileName.substr(0, nPos); } } if (fileNames.size() == 0) { return 0; - } else { + } + else + { return fileNames[0]; } } - static bool HasSingleRootDirectory(ZipFileReader&zipFile) { + static bool HasSingleRootDirectory(ZipFileReader &zipFile) + { bool hasDirectory = false; std::string previousDirectory; - for (auto& file: zipFile.GetFiles()) + for (auto &file : zipFile.GetFiles()) { auto pos = file.find('/'); - if (pos == std::string::npos) { + if (pos == std::string::npos) + { // a file in the root. return false; - } else + } + else { - std::string currentRootDirectory = file.substr(0,pos); - if (hasDirectory) { + std::string currentRootDirectory = file.substr(0, pos); + if (hasDirectory) + { if (currentRootDirectory != previousDirectory) { return false; } - } else { + } + else + { hasDirectory = true; previousDirectory = currentRootDirectory; } @@ -412,26 +426,26 @@ class DownloadIntercept : public RequestHandler { PluginPresets presets; fs::path filePath = req.get_body_temporary_file(); - if (filePath.empty()) + if (filePath.empty()) { throw std::runtime_error("Unexpected."); } if (IsZipFile(filePath)) { - auto presetReader = PresetBundleReader::LoadPluginPresetsFile(*(this->model),filePath); + auto presetReader = PresetBundleReader::LoadPluginPresetsFile(*(this->model), filePath); presetReader->ExtractMediaFiles(); std::stringstream ss(presetReader->GetPluginPresetsJson()); json_reader reader(ss); reader.read(&presets); - - } else { + } + else + { json_reader reader(req.get_body_input_stream()); reader.read(&presets); } model->UploadPluginPresets(presets); - res.set(HttpField::content_type, "application/json"); res.set(HttpField::cache_control, "no-cache"); std::stringstream sResult; @@ -447,19 +461,21 @@ class DownloadIntercept : public RequestHandler BankFile bankFile; fs::path filePath = req.get_body_temporary_file(); - if (filePath.empty()) + if (filePath.empty()) { throw std::runtime_error("Unexpected."); } if (IsZipFile(filePath)) - { - auto presetReader = PresetBundleReader::LoadPresetsFile(*(this->model),filePath); + { + auto presetReader = PresetBundleReader::LoadPresetsFile(*(this->model), filePath); presetReader->ExtractMediaFiles(); std::stringstream ss(presetReader->GetPresetJson()); json_reader reader(ss); reader.read(&bankFile); - } else { + } + else + { // legacy json format, no zip, no media files. json_reader reader(req.get_body_input_stream()); reader.read(&bankFile); @@ -487,19 +503,21 @@ class DownloadIntercept : public RequestHandler BankFile bankFile; fs::path filePath = req.get_body_temporary_file(); - if (filePath.empty()) + if (filePath.empty()) { throw std::runtime_error("Unexpected."); } if (IsZipFile(filePath)) - { - auto presetReader = PresetBundleReader::LoadPresetsFile(*(this->model),filePath); + { + auto presetReader = PresetBundleReader::LoadPresetsFile(*(this->model), filePath); presetReader->ExtractMediaFiles(); std::stringstream ss(presetReader->GetPresetJson()); json_reader reader(ss); reader.read(&bankFile); - } else { + } + else + { // legacy json format, no zip, no media files. json_reader reader(req.get_body_input_stream()); reader.read(&bankFile); @@ -522,70 +540,88 @@ class DownloadIntercept : public RequestHandler res.setContentLength(result.length()); res.setBody(result); - } else if (segment == "uploadUserFile") + } + else if (segment == "uploadUserFile") { - res.set(HttpField::content_type, "application/json"); - res.set(HttpField::cache_control, "no-cache"); - std::string instanceId = request_uri.query("id"); - std::string directory = request_uri.query("directory"); - std::string filename = request_uri.query("filename"); - std::string patchProperty = request_uri.query("property"); + UserUploadResponse result; + try + { + res.set(HttpField::content_type, "application/json"); + res.set(HttpField::cache_control, "no-cache"); + std::string instanceId = request_uri.query("id"); + std::string directory = request_uri.query("directory"); + std::string filename = request_uri.query("filename"); + std::string patchProperty = request_uri.query("property"); - if (patchProperty.length() == 0 && directory.length() == 0) - { - throw PiPedalException("Malformed request."); + if (patchProperty.length() == 0 && directory.length() == 0) + { + throw PiPedalException("Malformed request."); + } - } + res.set(HttpField::content_type, "application/json"); + res.set(HttpField::cache_control, "no-cache"); - res.set(HttpField::content_type, "application/json"); - res.set(HttpField::cache_control, "no-cache"); + fs::path outputFileName = std::filesystem::path(directory) / filename; - std::string outputFileName = std::filesystem::path(directory) / filename; + if (filename.ends_with(".zip")) + { + ExtensionChecker extensionChecker{request_uri.query("ext")}; + namespace fs = std::filesystem; - if (filename.ends_with(".zip")) - { - ExtensionChecker extensionChecker { request_uri.query("ext") }; - namespace fs = std::filesystem; - - try { - auto zipFile = ZipFileReader::Create(req.get_body_temporary_file()); - std::vector files = zipFile->GetFiles(); - bool hasSingleRootDirectory = HasSingleRootDirectory(*zipFile); - if (!hasSingleRootDirectory) { - directory = (fs::path(directory) / fs::path(filename).filename().replace_extension("")).string(); - } - for (const auto&inputFile : files) + try { - if (!inputFile.ends_with("/")) // don't process directory entries. + auto zipFile = ZipFileReader::Create(req.get_body_temporary_file()); + std::vector files = zipFile->GetFiles(); + bool hasSingleRootDirectory = HasSingleRootDirectory(*zipFile); + if (!hasSingleRootDirectory) + { + directory = (fs::path(directory) / fs::path(filename).filename().replace_extension("")).string(); + } + for (const auto &inputFile : files) { - fs::path inputPath { inputFile}; - std::string extension = inputPath.extension(); - if (extensionChecker.IsValidExtension(extension)) + if (!inputFile.ends_with("/")) // don't process directory entries. { - auto si = zipFile->GetFileInputStream(inputFile); - std::string path = this->model->UploadUserFile(directory,patchProperty,inputFile, si,zipFile->GetFileSize(inputFile)); + fs::path inputPath{inputFile}; + std::string extension = inputPath.extension(); + if (extensionChecker.IsValidExtension(extension)) + { + auto si = zipFile->GetFileInputStream(inputFile); + std::string path = this->model->UploadUserFile(directory, patchProperty, inputFile, si, zipFile->GetFileSize(inputFile)); + } } } + // set outputPath to the file or folder we would like focus to go to. + // almost always a single folder in the root. + std::string returnPath = GetFirstFolderOrFile(files); + outputFileName = fs::path(directory) / returnPath; + } + catch (const std::exception &e) + { + Lv2Log::error(SS("Unzip failed. " << e.what())); + throw; } - // set outputPath to the file or folder we would like focus to go to. - // almost always a single folder in the root. - std::string returnPath = GetFirstFolderOrFile(files); - outputFileName = this->model->GetPluginUploadDirectory() / directory / returnPath; - } catch (const std::exception &e) + FileSystemSync(); + } + else { - Lv2Log::error(SS("Unzip failed. " << e.what())); - throw; + outputFileName = this->model->UploadUserFile(directory, patchProperty, filename, req.get_body_input_stream(), req.content_length()); } - FileSystemSync(); - } else { - outputFileName = this->model->UploadUserFile(directory,patchProperty,filename,req.get_body_input_stream(), req.content_length()); + if (outputFileName.is_relative()) + { + outputFileName = this->model->GetPluginUploadDirectory() / outputFileName; + } + result.path_ = outputFileName.string(); + } + catch (const std::exception &e) + { + result.errorMessage_ = e.what(); } std::stringstream ss; json_writer writer(ss); - writer.write(outputFileName); + writer.write(result); std::string response = ss.str(); res.setContentLength(response.length()); @@ -598,7 +634,7 @@ class DownloadIntercept : public RequestHandler } catch (const std::exception &e) { - Lv2Log::error(SS("Error uploading file: " << e.what()) ); + Lv2Log::error(SS("Error uploading file: " << e.what())); if (strcmp(e.what(), "Not found") == 0) { ec = boost::system::errc::make_error_code(boost::system::errc::no_such_file_or_directory); @@ -611,7 +647,6 @@ class DownloadIntercept : public RequestHandler } }; - static std::string StripPortNumber(const std::string &fromAddress) { std::string address = fromAddress; @@ -663,10 +698,10 @@ class InterceptConfig : public RequestHandler std::stringstream s; s << "{ \"socket_server_port\": " << portNumber - << ", \"socket_server_address\": \"" << webSocketAddress + << ", \"socket_server_address\": \"" << webSocketAddress << "\", \"ui_plugins\": [ ], \"max_upload_size\": " << maxUploadSize - << ", \"enable_auto_update\": " << (ENABLE_AUTO_UPDATE ? " true": "false") - << " }"; + << ", \"enable_auto_update\": " << (ENABLE_AUTO_UPDATE ? " true" : "false") + << " }"; return s.str(); } @@ -720,16 +755,14 @@ class InterceptConfig : public RequestHandler }; void pipedal::ConfigureWebServer( - WebServer&server, - PiPedalModel&model, + WebServer &server, + PiPedalModel &model, int port, size_t maxUploadSize) { - std::shared_ptr interceptConfig{new InterceptConfig(port, maxUploadSize)}; - server.AddRequestHandler(interceptConfig); - - std::shared_ptr downloadIntercept = std::make_shared(&model); - server.AddRequestHandler(downloadIntercept); - + std::shared_ptr interceptConfig{new InterceptConfig(port, maxUploadSize)}; + server.AddRequestHandler(interceptConfig); + std::shared_ptr downloadIntercept = std::make_shared(&model); + server.AddRequestHandler(downloadIntercept); }