From 3ef263a16ee33eab2faafc6ac51f099bd53c3af5 Mon Sep 17 00:00:00 2001 From: Robin Davies Date: Wed, 14 Aug 2024 07:30:11 -0400 Subject: [PATCH] File Property Dialog Rewrite (phase 1) --- react/public/var/config.json | 2 +- react/src/App.tsx | 2 +- react/src/FilePropertyDialog.tsx | 515 ++++++++++++++++++++++-------- react/src/PiPedalModel.tsx | 75 ++++- react/src/PluginControlView.tsx | 2 +- react/src/RenameDialog.tsx | 15 + src/CMakeLists.txt | 2 + src/FileEntry.cpp | 28 ++ src/FileEntry.hpp | 40 +++ src/FilePropertyDirectoryTree.cpp | 35 ++ src/FilePropertyDirectoryTree.hpp | 37 +++ src/PiPedalModel.cpp | 35 ++ src/PiPedalModel.hpp | 6 + src/PiPedalSocket.cpp | 73 +++++ src/Storage.cpp | 224 +++++++++++-- src/Storage.hpp | 15 +- todo.txt | 8 +- 17 files changed, 948 insertions(+), 166 deletions(-) create mode 100644 src/FileEntry.cpp create mode 100644 src/FileEntry.hpp create mode 100644 src/FilePropertyDirectoryTree.cpp create mode 100644 src/FilePropertyDirectoryTree.hpp diff --git a/react/public/var/config.json b/react/public/var/config.json index 80d344a..bf8d4ce 100644 --- a/react/public/var/config.json +++ b/react/public/var/config.json @@ -1,5 +1,5 @@ { - "socket_server_port": 80, + "socket_server_port": 8080, "socket_server_address": "*", "debug": true, "max_upload_size": 1048576, diff --git a/react/src/App.tsx b/react/src/App.tsx index f0d5078..61277d0 100644 --- a/react/src/App.tsx +++ b/react/src/App.tsx @@ -59,7 +59,7 @@ const theme = createTheme( }, }, mainBackground: "#222", - toolbarColor: '#FFFFFF' + toolbarColor: '#222' } : { diff --git a/react/src/FilePropertyDialog.tsx b/react/src/FilePropertyDialog.tsx index b430359..2784912 100644 --- a/react/src/FilePropertyDialog.tsx +++ b/react/src/FilePropertyDialog.tsx @@ -19,18 +19,25 @@ import React from 'react'; -import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; - - +import RenameDialog from './RenameDialog'; +import Divider from '@mui/material/Divider'; +import MenuItem from '@mui/material/MenuItem'; +import Menu from '@mui/material/Menu'; +import MoreIcon from '@mui/icons-material/MoreVert'; +import { PiPedalModel, PiPedalModelFactory, FileEntry } from './PiPedalModel'; +import Link from '@mui/material/Link'; +import { isDarkMode } from './DarkMode'; import Button from '@mui/material/Button'; import FileUploadIcon from '@mui/icons-material/FileUpload'; import AudioFileIcon from '@mui/icons-material/AudioFile'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; - +import FolderIcon from '@mui/icons-material/Folder'; +import InsertDriveFileOutlinedIcon from '@mui/icons-material/InsertDriveFileOutlined'; import DialogActions from '@mui/material/DialogActions'; import DialogTitle from '@mui/material/DialogTitle'; import IconButton from '@mui/material/IconButton'; import OldDeleteIcon from './OldDeleteIcon'; +import Toolbar from '@mui/material/Toolbar'; import ResizeResponsiveComponent from './ResizeResponsiveComponent'; import { UiFileProperty } from './Lv2Plugin'; @@ -39,7 +46,8 @@ import Typography from '@mui/material/Typography'; import DialogEx from './DialogEx'; import UploadFileDialog from './UploadFileDialog'; import OkCancelDialog from './OkCancelDialog'; - +import Breadcrumbs from '@mui/material/Breadcrumbs'; +import HomeIcon from '@mui/icons-material/Home'; export interface FilePropertyDialogProps { open: boolean, fileProperty: UiFileProperty, @@ -51,16 +59,87 @@ export interface FilePropertyDialogProps { export interface FilePropertyDialogState { fullScreen: boolean; selectedFile: string; + navDirectory: string; hasSelection: boolean; - files: string[]; + canDelete: boolean; + fileEntries: FileEntry[]; columns: number; columnWidth: number; openUploadFileDialog: boolean; openConfirmDeleteDialog: boolean; + menuAnchorEl: null | HTMLElement; + newFolderDialogOpen: boolean; + renameDialogOpen: boolean; }; +function pathExtension(path: string) +{ + let dotPos = path.lastIndexOf('.'); + if (dotPos === -1) return ""; + + let slashPos = path.lastIndexOf('/'); + if (slashPos !== -1) + { + if (dotPos <= slashPos+1) return ""; + } + return path.substring(dotPos); // include the '.'. + +} +function concatPath(left: string, right: string) { + if (left === "") return right; + if (right === "") return left; + if (left.endsWith('/')) { + left = left.substring(0, left.length - 1); + } + if (right.startsWith("/")) { + right = right.substring(1); + } + return left + "/" + right; +} +export function pathFileNameOnly(path: string): string { + if (path === "..") return path; + let slashPos = path.lastIndexOf('/'); + if (slashPos < 0) { + slashPos = 0; + } else { + ++slashPos; + } + let extPos = path.lastIndexOf('.'); + if (extPos < 0 || extPos < slashPos) { + extPos = path.length; + } + + return path.substring(slashPos, extPos); +} + +export function pathFileName(path: string): string { + if (path === "..") return path; + let slashPos = path.lastIndexOf('/'); + if (slashPos < 0) { + slashPos = 0; + } else { + ++slashPos; + } + return path.substring(slashPos); +} + export default 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]; + } + return result; + } constructor(props: FilePropertyDialogProps) { super(props); @@ -71,29 +150,31 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent { if (this.mounted) { - files.splice(0,0,""); - this.setState({ files: files, hasSelection: this.isFileInList(files, this.state.selectedFile) }); + // 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 lastDivRef : HTMLDivElement | null = null; + private lastDivRef: HTMLDivElement | null = null; - onMeasureRef(div: HTMLDivElement | null) - { + onMeasureRef(div: HTMLDivElement | null) { this.lastDivRef = div; if (div) { let width = div.offsetWidth; if (width === 0) return; - let columns = Math.floor((width-40)/280); - if(columns === 0) - { - columns = 1; - } - let columnWidth = (width-40)/columns; - if (columns !== this.state.columns || columnWidth !== this.state.columnWidth) - { - this.setState({columns: columns, columnWidth: columnWidth}); + 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: height < 200 + this.setState({ + fullScreen: height < 200 }) - if (this.lastDivRef !== null) - { + if (this.lastDivRef !== null) { this.onMeasureRef(this.lastDivRef); } @@ -166,45 +247,31 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent, prevState: Readonly, snapshot?: any): void { super.componentDidUpdate?.(prevProps, prevState, snapshot); - if (prevProps.open !== this.props.open) - { - if (this.props.open) - { - this.requestFiles() + 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 }); + this.requestFiles(navDirectory) this.requestScroll = true; - if (this.state.selectedFile !== this.props.selectedFile) - { - this.setState({selectedFile: this.props.selectedFile}); - } } - } else if (prevProps.fileProperty !== this.props.fileProperty) { - this.requestFiles() } } - private fileNameOnly(path: string): string { - let slashPos = path.lastIndexOf('/'); - if (slashPos < 0) { - slashPos = 0; - } else { - ++slashPos; + private isDirectory(path: string): boolean { + for (var fileEntry of this.state.fileEntries) { + if (fileEntry.filename === path) { + return fileEntry.isDirectory; + } } - let extPos = path.lastIndexOf('.'); - if (extPos < 0 || extPos < slashPos) { - extPos = path.length; - } - - return path.substring(slashPos, extPos); - + return false; } - - private isFileInList(files: string[], file: string) { + private isFileInList(files: FileEntry[], file: string) { let hasSelection = false; if (file === "") return true; for (var listFile of files) { - if (listFile === file) { + if (listFile.filename === file) { hasSelection = true; break; } @@ -214,62 +281,110 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent) { + this.setState({ menuAnchorEl: event.currentTarget }); + } - handleConfirmDelete() - { - this.setState({openConfirmDeleteDialog: false}); - if (this.state.hasSelection) - { + 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.files.length;++i) - { - let file = this.state.files[i]; - if (file === selectedFile) - { + for (let i = 0; i < this.state.fileEntries.length; ++i) { + let file = this.state.fileEntries[i]; + if (file.filename === selectedFile) { position = i; } } let newSelection = ""; - if (position >= 0 && position < this.state.files.length-1) - { - newSelection = this.state.files[position+1]; - } else if (position === this.state.files.length-1) - { + 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.files[position-1]; + newSelection = this.state.fileEntries[position - 1].filename; } } this.model.deleteUserFile(this.state.selectedFile) - .then( - ()=> { - this.setState({selectedFile: newSelection,hasSelection: newSelection !== ""}); - this.requestFiles(); - } - ).catch( - (e: any)=> - { - this.model.showAlert(e + ""); - } - ); + .then( + () => { + this.setState({ selectedFile: newSelection, hasSelection: newSelection !== "" }); + this.requestFiles(this.state.navDirectory); + } + ).catch( + (e: any) => { + this.model.showAlert(e + ""); + } + ); } } private wantsScrollRef: boolean = true; - mapKey: number = 0; + navigate(relativeDirectory: string) { + this.requestFiles(relativeDirectory); + this.setState({ navDirectory: relativeDirectory }); + + } + renderBreadcrumbs() { + if (this.state.navDirectory === "") { + return (
); + } + let breadcrumbs: React.ReactElement[] = [( + { this.navigate(""); }} + sx={{ display: 'flex', alignItems: 'center' }} + > + + Home + + ) + ]; + let directories = this.state.navDirectory.split("/"); + let target = ""; + for (let i = 0; i < directories.length - 1; ++i) { + target = concatPath(target, directories[i]); + let myTarget = target; + breadcrumbs.push(( + { this.navigate(myTarget) }}> + {directories[i]} + + )); + } + { + let lastdirectory = directories[directories.length - 1]; + breadcrumbs.push(( + {lastdirectory} + )); + + } + return ( + + {breadcrumbs} + + ); + } + + hasSelectedFileOrFolder(): boolean { + return this.state.hasSelection && this.state.selectedFile !== ""; + } render() { - this.mapKey = 0; + let columnWidth = this.state.columnWidth; return this.props.open && @@ -278,48 +393,97 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent -
- { this.props.onCancel(); }} aria-label="back" + + { this.props.onCancel(); }} > - + - {this.props.fileProperty.label} + + {this.props.fileProperty.label} + + { this.handleMenuOpen(ev); }} + > + + + { this.handleMenuClose(); }} + > + { this.handleMenuClose(); this.onNewFolder(); }}>New folder + {this.hasSelectedFileOrFolder() && ()} + {this.hasSelectedFileOrFolder() && ( { this.handleMenuClose(); }}>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, paddingTop: 16,paddingBottom: 16 }}> +
this.onMeasureRef(element)} + style={{ + flex: "1 1 100%", display: "flex", flexFlow: "row wrap", + justifyContent: "flex-start", alignContent: "flex-start", paddingLeft: 16, paddingTop: 16, paddingBottom: 16 + }}> { (this.state.columns !== 0) && // don't render until we have number of columns derived from layout. - this.state.files.map( - (value: string, index: number) => { - let displayValue = value; - if (displayValue === "") - { + this.state.fileEntries.map( + (value: FileEntry, index: number) => { + let displayValue = value.filename; + if (displayValue === "") { displayValue = ""; } else { - displayValue = this.fileNameOnly(value); + displayValue = pathFileNameOnly(displayValue); } - let selected = value === this.state.selectedFile; + 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)}; + if (selected) { + scrollRef = (element) => { this.onScrollRef(element) }; } return ( - this.onSelectValue(value)} onDoubleClick={()=> {this.onDoubleClickValue(value);}} + onClick={() => this.onSelectValue(value.filename)} onDoubleClick={() => { this.onDoubleClickValue(value.filename); }} >
- + {value.filename === "" ? + () + : ( + value.isDirectory ? ( + + ) : ( + + ))} {displayValue}
@@ -333,12 +497,13 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent
this.handleDelete()} > + disabled={!this.state.hasSelection || this.state.selectedFile === ""} + onClick={() => this.handleDelete()} > @@ -347,11 +512,11 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent { this.props.onCancel(); }} aria-label="cancel"> Cancel -
@@ -364,22 +529,112 @@ export default class FilePropertyDialog extends ResizeResponsiveComponentthis.handleConfirmDelete()} - onClose={()=>{ - this.setState({openConfirmDeleteDialog: false}); + onOk={() => this.handleConfirmDelete()} + onClose={() => { + this.setState({ openConfirmDeleteDialog: false }); }} /> + { + this.state.newFolderDialogOpen && ( + { this.setState({ newFolderDialogOpen: false }); this.onExecuteNewFolder(newName) }} + onClose={() => { this.setState({ newFolderDialogOpen: false }); }} + acceptActionName="OK" + /> + ) + } + { + this.state.renameDialogOpen && ( + { this.setState({ renameDialogOpen: false }); this.onExecuteRename(newName) }} + onClose={() => { this.setState({ renameDialogOpen: false }); }} + acceptActionName="OK" + /> + + ) + } ); } + openSelectedFile(): void { + if (this.isDirectory(this.state.selectedFile)) { + let directoryName = pathFileNameOnly(this.state.selectedFile); + let navDirectory = concatPath(this.state.navDirectory, directoryName); + this.requestFiles(navDirectory); + this.setState({ navDirectory: navDirectory }); + } 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 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 = concatPath(this.state.navDirectory,oldName); + newPath = concatPath(this.state.navDirectory,newName);; + } else { + let oldName = pathFileNameOnly(this.state.selectedFile); + if (oldName === newName) return; + let extension = pathExtension(this.state.selectedFile); + oldPath = concatPath(this.state.navDirectory,oldName+extension); + newPath = concatPath(this.state.navDirectory,newName + extension); + } + this.model.renameSampleFile(oldPath,newPath,this.props.fileProperty) + .then((newPath)=>{ + this.setState({selectedFile: newPath}); + this.requestFiles(this.state.navDirectory); + this.requestScroll = true + + }) + .catch((e) =>{ + this.model.showAlert(e.toString()); + }); + } + + private onNewFolder(): void { + this.setState({ newFolderDialogOpen: true }); + } + + private onExecuteNewFolder(newName: string) { + this.model.createNewSampleDirectory(concatPath(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 diff --git a/react/src/PiPedalModel.tsx b/react/src/PiPedalModel.tsx index 3b98ccf..a03556c 100644 --- a/react/src/PiPedalModel.tsx +++ b/react/src/PiPedalModel.tsx @@ -52,6 +52,10 @@ export enum State { export type ControlValueChangedHandler = (key: string, value: number) => void; +export interface FileEntry { + filename: string; + isDirectory: boolean; +}; export type PluginPresetsChangedHandler = (pluginUri: string) => void; @@ -337,7 +341,7 @@ interface Vst3ControlChangedBody { export interface FavoritesList { [url: string]: boolean; -} +}; export class PiPedalModel //implements PiPedalModel @@ -1630,14 +1634,21 @@ export class PiPedalModel //implements PiPedalModel }); } + // deprecated. requestFileList(piPedalFileProperty: UiFileProperty): Promise { return nullCast(this.webSocket) .request('requestFileList', piPedalFileProperty); } - + requestFileList2(relativeDirectoryPath: string,piPedalFileProperty: UiFileProperty): Promise { + return nullCast(this.webSocket) + .request('requestFileList2', + {relativePath: relativeDirectoryPath, fileProperty: piPedalFileProperty} + ); + } + deleteUserFile(fileName: string) : Promise { - return nullCast(this.webSocket).request('deleteUserFile',fileName) + return nullCast(this.webSocket).request('deleteUserFile',fileName); } @@ -2247,7 +2258,11 @@ export class PiPedalModel //implements PiPedalModel return new Promise((resolve, reject) => { let ws = this.webSocket; - if (!ws) return; + if (!ws) + { + resolve(); + return; + } ws.request( "setGovernorSettings", governor @@ -2260,6 +2275,57 @@ export class PiPedalModel //implements PiPedalModel }); }); } + createNewSampleDirectory(relativePath: string, uiFileProperty: UiFileProperty) : Promise + { + return new Promise((resolve, reject) => { + + let ws = this.webSocket; + if (!ws) { + resolve(""); + return; + } + ws.request( + "createNewSampleDirectory", + { + relativePath: relativePath, + uiFileProperty: uiFileProperty + } + ) + .then((newPath) => { + resolve(newPath); + }) + .catch((err) => { + reject(err); + }); + }); + } + + renameSampleFile(oldRelativePath: string, newRelativePath: string, uiFileProperty: UiFileProperty) : Promise + { + return new Promise((resolve, reject) => { + + let ws = this.webSocket; + if (!ws) { + resolve(""); + return; + } + ws.request( + "renameSampleFile", + { + oldRelativePath: oldRelativePath, + newRelativePath: newRelativePath, + uiFileProperty: uiFileProperty + } + ) + .then((newPath) => { + resolve(newPath); + }) + .catch((err) => { + reject(err); + }); + }); + } + setWifiConfigSettings(wifiConfigSettings: WifiConfigSettings): Promise { let result = new Promise((resolve, reject) => { let oldSettings = this.wifiConfigSettings.get(); @@ -2567,7 +2633,6 @@ export class PiPedalModel //implements PiPedalModel window.location.reload(); } - }; let instance: PiPedalModel | undefined = undefined; diff --git a/react/src/PluginControlView.tsx b/react/src/PluginControlView.tsx index 52b5233..20f5283 100644 --- a/react/src/PluginControlView.tsx +++ b/react/src/PluginControlView.tsx @@ -663,7 +663,7 @@ const PluginControlView = }) .catch((error) => { - this.model.showAlert("setPatchProperty failed: " + error); + this.model.showAlert("Unable to complete the operation. Audio is not running." + error); }); this.setState({ showFileDialog: false }); } diff --git a/react/src/RenameDialog.tsx b/react/src/RenameDialog.tsx index a99c631..72a68f6 100644 --- a/react/src/RenameDialog.tsx +++ b/react/src/RenameDialog.tsx @@ -25,6 +25,7 @@ import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import { nullCast } from './Utility'; import ResizeResponsiveComponent from './ResizeResponsiveComponent'; +import {PiPedalModel,PiPedalModelFactory} from './PiPedalModel'; export interface RenameDialogProps { @@ -74,6 +75,12 @@ export default class RenameDialog extends ResizeResponsiveComponent { let text = nullCast(this.refText.current).value; text = text.trim(); + try { + this.checkForIllegalCharacters(text); + } catch (e:any) + { + let model:PiPedalModel = PiPedalModelFactory.getInstance(); + model.showAlert(e.toString()); + return; + } if (text.length === 0) return; onOk(text); } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f0b5dd3..bb86400 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -131,6 +131,8 @@ else() endif() set (PIPEDAL_SOURCES + FilePropertyDirectoryTree.cpp FilePropertyDirectoryTree.hpp + FileEntry.cpp FileEntry.hpp atom_object.hpp atom_object.cpp FileBrowserFiles.h FileBrowserFilesFeature.hpp FileBrowserFilesFeature.cpp diff --git a/src/FileEntry.cpp b/src/FileEntry.cpp new file mode 100644 index 0000000..f9e787e --- /dev/null +++ b/src/FileEntry.cpp @@ -0,0 +1,28 @@ +// 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 "FileEntry.hpp" + +using namespace pipedal; + +JSON_MAP_BEGIN(FileEntry) + JSON_MAP_REFERENCE(FileEntry,filename) + JSON_MAP_REFERENCE(FileEntry,isDirectory) + +JSON_MAP_END() diff --git a/src/FileEntry.hpp b/src/FileEntry.hpp new file mode 100644 index 0000000..0acf5be --- /dev/null +++ b/src/FileEntry.hpp @@ -0,0 +1,40 @@ +// 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 "json.hpp" + +namespace pipedal { + + class FileEntry { + public: + FileEntry() { } + FileEntry(std::string filename,bool isDirectory) + :filename_(filename), isDirectory_(isDirectory) + { + + } + std::string filename_; + bool isDirectory_ = false; + + DECLARE_JSON_MAP(FileEntry); + + }; +} \ No newline at end of file diff --git a/src/FilePropertyDirectoryTree.cpp b/src/FilePropertyDirectoryTree.cpp new file mode 100644 index 0000000..d6fafdd --- /dev/null +++ b/src/FilePropertyDirectoryTree.cpp @@ -0,0 +1,35 @@ +// 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 "FilePropertyDirectoryTree.hpp" + +using namespace pipedal; + +FilePropertyDirectoryTree::FilePropertyDirectoryTree() +{ +} +FilePropertyDirectoryTree::FilePropertyDirectoryTree(const std::string &directoryName) + : directoryName_(directoryName) +{ +} + +JSON_MAP_BEGIN(FilePropertyDirectoryTree) +JSON_MAP_REFERENCE(FilePropertyDirectoryTree, directoryName) +JSON_MAP_REFERENCE(FilePropertyDirectoryTree, children) +JSON_MAP_END() diff --git a/src/FilePropertyDirectoryTree.hpp b/src/FilePropertyDirectoryTree.hpp new file mode 100644 index 0000000..4ab8c4d --- /dev/null +++ b/src/FilePropertyDirectoryTree.hpp @@ -0,0 +1,37 @@ +// 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 "json.hpp" +#include +#include +#include +namespace pipedal { + class FilePropertyDirectoryTree { + public: + using ptr = std::unique_ptr; + + FilePropertyDirectoryTree(); + FilePropertyDirectoryTree(const std::string&directoryName); + std::string directoryName_; + std::vector> children_; + + DECLARE_JSON_MAP(FilePropertyDirectoryTree); + }; +} diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index 59c0bc2..2bf9432 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -2039,6 +2039,27 @@ 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) +{ + try + { + return this->storage.GetFileList2(relativePath,fileProperty); + } + catch (const std::exception &e) + { + Lv2Log::warning("GetFileList() failed: (%s)", e.what()); + return std::vector(); // don't disclose to users what the problem is. + } +} + +std::string PiPedalModel::RenameSampleFile( + const std::string&oldRelativePath, + const std::string&newRelativePath, + const UiFileProperty&uiFileProperty) +{ + std::lock_guard lock(mutex); + return storage.RenameSampleFile(oldRelativePath,newRelativePath,uiFileProperty); +} void PiPedalModel::DeleteSampleFile(const std::filesystem::path &fileName) { @@ -2046,6 +2067,20 @@ void PiPedalModel::DeleteSampleFile(const std::filesystem::path &fileName) storage.DeleteSampleFile(fileName); } +std::string PiPedalModel::CreateNewSampleDirectory(const std::string&relativePath, const UiFileProperty&uiFileProperty) +{ + std::lock_guard lock(mutex); + return storage.CreateNewSampleDirectory(relativePath, uiFileProperty); + +} +FilePropertyDirectoryTree::ptr PiPedalModel::GetSampleDirectoryTree(const UiFileProperty&uiFileProperty) +{ + std::lock_guard lock(mutex); + return storage.GetSampleDirectoryTree(uiFileProperty); + +} + + std::string PiPedalModel::UploadUserFile(const std::string &directory, const std::string &patchProperty, const std::string &filename, const std::string &fileBody) { return storage.UploadUserFile(directory, patchProperty, filename, fileBody); diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index cbe74fb..a73b254 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -39,6 +39,7 @@ #include #include "Promise.hpp" #include "AtomConverter.hpp" +#include "FileEntry.hpp" namespace pipedal { @@ -328,8 +329,13 @@ namespace pipedal void SetFavorites(const std::map &favorites); std::vector GetFileList(const UiFileProperty&fileProperty); + std::vector 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); + std::string RenameSampleFile(const std::string&oldRelativePath,const std::string&newRelativePath,const UiFileProperty&uiFileProperty); + FilePropertyDirectoryTree::ptr GetSampleDirectoryTree(const UiFileProperty&uiFileProperty); + std::string UploadUserFile(const std::string &directory, const std::string &patchProperty,const std::string&filename,const std::string&fileBody); uint64_t CreateNewPreset(); diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index 3ab0abd..02c675d 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -40,6 +40,7 @@ #include "SysExec.hpp" #include "PiPedalAlsa.hpp" #include +#include "FileEntry.hpp" using namespace std; using namespace pipedal; @@ -58,6 +59,33 @@ JSON_MAP_REFERENCE(GetPatchPropertyBody, propertyUri) JSON_MAP_END() +class CreateNewSampleDirectoryArgs { +public: + std::string relativePath_; + UiFileProperty uiFileProperty_; + DECLARE_JSON_MAP(CreateNewSampleDirectoryArgs); +}; + +JSON_MAP_BEGIN(CreateNewSampleDirectoryArgs) +JSON_MAP_REFERENCE(CreateNewSampleDirectoryArgs, relativePath) +JSON_MAP_REFERENCE(CreateNewSampleDirectoryArgs, uiFileProperty) +JSON_MAP_END() + +class RenameSampleFileArgs { +public: + std::string oldRelativePath_; + std::string newRelativePath_; + UiFileProperty uiFileProperty_; + DECLARE_JSON_MAP(RenameSampleFileArgs); +}; + +JSON_MAP_BEGIN(RenameSampleFileArgs) +JSON_MAP_REFERENCE(RenameSampleFileArgs, oldRelativePath) +JSON_MAP_REFERENCE(RenameSampleFileArgs, newRelativePath) +JSON_MAP_REFERENCE(RenameSampleFileArgs, uiFileProperty) +JSON_MAP_END() + + class Lv2StateChangedBody { public: uint64_t instanceId_; @@ -198,6 +226,21 @@ JSON_MAP_REFERENCE(MonitorResultBody, subscriptionHandle) JSON_MAP_REFERENCE(MonitorResultBody, value) JSON_MAP_END() +class FileRequestArgs +{ +public: + std::string relativePath_; + UiFileProperty fileProperty_; + + DECLARE_JSON_MAP(FileRequestArgs); +}; +JSON_MAP_BEGIN(FileRequestArgs) +JSON_MAP_REFERENCE(FileRequestArgs, relativePath) +JSON_MAP_REFERENCE(FileRequestArgs, fileProperty) +JSON_MAP_END() + + + class MonitorPortBody { @@ -1417,6 +1460,13 @@ public: std::vector list = this->model.GetFileList(fileProperty); this->Reply(replyTo,"requestFileList",list); } + else if (message == "requestFileList2") + { + FileRequestArgs requestArgs; + pReader->read(&requestArgs); + std::vector list = this->model.GetFileList2(requestArgs.relativePath_, requestArgs.fileProperty_); + this->Reply(replyTo,"requestFileList2",list); + } else if (message == "newPreset") { int64_t presetId = this->model.CreateNewPreset(); @@ -1430,6 +1480,29 @@ public: this->model.DeleteSampleFile(fileName); this->Reply(replyTo,"deleteUserFile",true); } + else if (message == "createNewSampleDirectory") + { + CreateNewSampleDirectoryArgs args; + pReader->read(&args); + + std::string newFileName = this->model.CreateNewSampleDirectory(args.relativePath_,args.uiFileProperty_); + this->Reply(replyTo,"createNewSampleDirectory",newFileName); + } + else if (message == "renameSampleFile") + { + RenameSampleFileArgs args; + pReader->read(&args); + + std::string newFileName = this->model.RenameSampleFile(args.oldRelativePath_,args.newRelativePath_,args.uiFileProperty_); + this->Reply(replyTo,"renameSampleFile",newFileName); + } else if (message == "GetSampleDirectoryTree"){ + UiFileProperty uiFileProperty; + pReader->read(&uiFileProperty); + FilePropertyDirectoryTree::ptr result = model.GetSampleDirectoryTree(uiFileProperty); + this->Reply(replyTo,"GetSampleDirectoryTree",result); + + } + else if (message == "setOnboarding") { bool value; diff --git a/src/Storage.cpp b/src/Storage.cpp index 87cd435..2d08a50 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2022-2023 Robin Davies +// Copyright (c) 2022-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 @@ -40,6 +40,7 @@ const char *BANKS_FILENAME = "index.banks"; #define USER_SETTINGS_FILENAME "userSettings.json"; Storage::Storage() +: locale("en_US.UTF-8") { SetConfigRoot("~/var/Config"); SetDataRoot("~/var/PiPedal"); @@ -1467,11 +1468,6 @@ std::vector Storage::GetSystemMidiBindings() return result; } -static bool containsDotDot(const std::string &value) -{ - std::size_t offset = value.find(".."); - return offset != std::string::npos; -} static bool containsDirectorySeparator(const std::string &value) { if (value.find("/") != std::string::npos) @@ -1483,19 +1479,12 @@ static bool containsDirectorySeparator(const std::string &value) return false; } + static void ThrowPermissionDeniedError() { throw std::logic_error("Permission denied."); } -class LexicographicCompare -{ -public: - bool operator()(const std::string &left, const std::string &right) - { - return std::lexicographical_compare(left.begin(), left.end(), right.begin(), right.end()); - } -} lexicographicCompare; std::vector Storage::GetFileList(const UiFileProperty &fileProperty) { @@ -1537,16 +1526,109 @@ std::vector Storage::GetFileList(const UiFileProperty &fileProperty // sort lexicographically - std::sort(result.begin(), result.end(), lexicographicCompare); + std::sort(result.begin(), result.end(), [this](const std::string&left,const std::string&right) { + return this->locale(left,right) < 0; + }); return result; } -bool Storage::IsValidSampleFile(const std::filesystem::path &fileName) +static bool ensureNoDotDot(const std::filesystem::path&path) +{ + for (auto segment_: path) + { + std::string segment = segment_.string(); + if (segment.starts_with(".")) + { + // the linux rule: any path that consists of all '.'s. + bool valid = false; + for (auto c: segment) + { + if (c != '.') + { + valid = true; + break; + } + } + if (!valid) + { + return false; + } + + } + } + return true; +} + + +std::vector Storage::GetFileList2(const std::string &relativePath,const UiFileProperty &fileProperty) +{ + if (!ensureNoDotDot(relativePath)) + { + ThrowPermissionDeniedError(); + } + if (!UiFileProperty::IsDirectoryNameValid(fileProperty.directory())) + { + ThrowPermissionDeniedError(); + } + + std::vector result; + + // if fileProperty has a user-accessible directory, push the entire file path. + if (fileProperty.directory().size() != 0) + { + std::filesystem::path audioFileDirectory = this->GetPluginAudioFileDirectory() / fileProperty.directory() / relativePath; + try + { + for (auto const &dir_entry : std::filesystem::directory_iterator(audioFileDirectory)) + { + if (dir_entry.is_regular_file()) + { + 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())) + { + // a relative path! + result.push_back(FileEntry(path,false)); + } + } + } else if (dir_entry.is_directory()) { + result.push_back(FileEntry(dir_entry.path(),true)); + } + } + } + catch (const std::exception &error) + { + throw std::logic_error("GetFileList failed. Directory not found: " + audioFileDirectory.string()); + } + } + + // sort lexicographically + + std::sort(result.begin(), result.end(),[this](const FileEntry&l, const FileEntry&r) { + if (l.isDirectory_ != r.isDirectory_) + { + return l.isDirectory_ > r.isDirectory_; + } + return this->locale(l.filename_,r.filename_); + + }); + return result; +} + + +bool Storage::IsValidSampleFileName(const std::filesystem::path &fileName) { if (!fileName.is_absolute()) { return false; } + if (!ensureNoDotDot(fileName)) + { + return false; + } + std::filesystem::path audioFilePath = this->GetPluginAudioFileDirectory(); std::filesystem::path parentDirectory = fileName.parent_path(); @@ -1557,9 +1639,6 @@ bool Storage::IsValidSampleFile(const std::filesystem::path &fileName) return false; } std::string name = parentDirectory.filename().string(); - if (name == ".." || name == ".") - return false; - if (parentDirectory == audioFilePath) return true; parentDirectory = parentDirectory.parent_path(); @@ -1571,7 +1650,7 @@ bool Storage::IsValidSampleFile(const std::filesystem::path &fileName) } void Storage::DeleteSampleFile(const std::filesystem::path &fileName) { - if (!IsValidSampleFile(fileName)) + if (!IsValidSampleFileName(fileName)) { throw std::logic_error("Permission denied."); } @@ -1581,7 +1660,16 @@ void Storage::DeleteSampleFile(const std::filesystem::path &fileName) } try { - std::filesystem::remove(fileName); + if (std::filesystem::is_directory(fileName)) + { + if (fileName.string().length() > 1) // guard against rm -rf / (bitter experience) + { + std::filesystem::remove_all(fileName); + } + } + else { + std::filesystem::remove(fileName); + } } catch (const std::exception &) { @@ -1601,7 +1689,7 @@ std::filesystem::path Storage::MakeUserFilePath(const std::string &directory, co throw std::logic_error("Permission denied."); } std::filesystem::path result = this->GetPluginAudioFileDirectory() / directory / filename; - if (!this->IsValidSampleFile(result)) + if (!this->IsValidSampleFileName(result)) { throw std::logic_error("Permission denied."); } @@ -1641,6 +1729,98 @@ std::string Storage::UploadUserFile(const std::string &directory, const std::str return path.string(); } +std::string Storage::CreateNewSampleDirectory(const std::string&relativePath, const UiFileProperty&uiFileProperty) +{ + if (uiFileProperty.directory().empty()) + { + throw std::runtime_error("Invalid UI File Property."); + } + std::filesystem::path path = this->GetPluginAudioFileDirectory() / uiFileProperty.directory() / relativePath; + if (!this->IsValidSampleFileName(path)) + { + throw std::runtime_error("Invalid file name."); + } + if (std::filesystem::exists(path)) + { + throw std::runtime_error("A directory with that name already exists."); + } + std::filesystem::create_directories(path); + return path; + +} +std::string Storage::RenameSampleFile( + const std::string&oldRelativePath, + const std::string&newRelativePath, + const UiFileProperty&uiFileProperty) +{ + if (uiFileProperty.directory().empty()) + { + throw std::runtime_error("Invalid UI File Property."); + } + std::filesystem::path oldPath = this->GetPluginAudioFileDirectory() / uiFileProperty.directory() / oldRelativePath; + if (!this->IsValidSampleFileName(oldPath)) + { + throw std::runtime_error("Invalid file name."); + } + if (!std::filesystem::exists(oldPath)) + { + throw std::runtime_error("Original path does not exist."); + } + + std::filesystem::path newPath = this->GetPluginAudioFileDirectory() / uiFileProperty.directory() / newRelativePath; + if (!this->IsValidSampleFileName(newPath)) + { + throw std::runtime_error("Invalid file name."); + } + if (std::filesystem::exists(newPath)) + { + if (std::filesystem::is_directory(newPath)) + { + throw std::runtime_error("A directory with that name already exists."); + } else { + throw std::runtime_error("A file with that name already exists."); + } + } + + std::filesystem::rename(oldPath,newPath); + return newPath; + +} + +void Storage::FillSampleDirectoryTree(FilePropertyDirectoryTree*node, const std::filesystem::path&directory) const +{ + for (auto child: std::filesystem::recursive_directory_iterator(directory)) + { + const auto& childPath = child.path(); + FilePropertyDirectoryTree::ptr childTree = std::make_unique(childPath.filename()); + FillSampleDirectoryTree(childTree.get(),childPath); + node->children_.push_back(std::move(childTree)); + } + std::sort(node->children_.begin(),node->children_.end(), + [this](const FilePropertyDirectoryTree::ptr&left,const FilePropertyDirectoryTree::ptr&right) + { + return this->locale(left->directoryName_,right->directoryName_); + }); +} +FilePropertyDirectoryTree::ptr Storage::GetSampleDirectoryTree(const UiFileProperty&uiFileProperty) const +{ + FilePropertyDirectoryTree::ptr result = std::make_unique(""); + if (uiFileProperty.directory().empty()) + { + throw std::runtime_error("Invalid uiFileProperty"); + } + if (!ensureNoDotDot(uiFileProperty.directory())) + { + throw std::runtime_error("Invalid uiFileProperty"); + } + std::filesystem::path rootDirectory = this->GetPluginAudioFileDirectory() / uiFileProperty.directory(); + + FillSampleDirectoryTree(result.get(),rootDirectory); + + return result; +} + + const PluginPresetIndex &Storage::GetPluginPresetIndex() { return pluginPresetIndex; diff --git a/src/Storage.hpp b/src/Storage.hpp index 4cbee39..a87d9ec 100644 --- a/src/Storage.hpp +++ b/src/Storage.hpp @@ -28,7 +28,10 @@ #include "JackServerSettings.hpp" #include "WifiConfigSettings.hpp" #include "WifiDirectConfigSettings.hpp" +#include "FileEntry.hpp" #include +#include +#include "FilePropertyDirectoryTree.hpp" namespace pipedal { @@ -64,6 +67,7 @@ struct PluginPresetValues { class Storage { private: + std::locale locale; std::filesystem::path dataRoot; std::filesystem::path configRoot; BankIndex bankIndex; @@ -71,6 +75,7 @@ private: PluginPresetIndex pluginPresetIndex; private: + void FillSampleDirectoryTree(FilePropertyDirectoryTree*node, const std::filesystem::path&directory) const; void MaybeCopyDefaultPresets(); static std::string SafeEncodeName(const std::string& name); @@ -149,6 +154,7 @@ public: int64_t DeleteBank(int64_t bankId); std::vector GetFileList(const UiFileProperty&fileProperty); + std::vector GetFileList2(const std::string&relativePath,const UiFileProperty&fileProperty); void SetJackChannelSelection(const JackChannelSelection&channelSelection); @@ -176,7 +182,7 @@ private: void SavePluginPresetIndex(); std::filesystem::path GetPluginPresetPath(const std::string &pluginUri) const; - bool IsValidSampleFile(const std::filesystem::path&fileName); + bool IsValidSampleFileName(const std::filesystem::path&fileName); std::filesystem::path MakeUserFilePath(const std::string &directory, const std::string&filename); public: @@ -213,6 +219,13 @@ public: std::vector GetSystemMidiBindings(); void DeleteSampleFile(const std::filesystem::path &fileName); std::string UploadUserFile(const std::string &directory, const std::string &patchProperty,const std::string&filename,const std::string&fileBody); + std::string CreateNewSampleDirectory(const std::string&relativePath, const UiFileProperty&uiFileProperty); + std::string RenameSampleFile( + const std::string&oldRelativePath, + const std::string&newRelativePath, + const UiFileProperty&uiFileProperty); + FilePropertyDirectoryTree::ptr GetSampleDirectoryTree(const UiFileProperty&uiFileProperty) const; + }; diff --git a/todo.txt b/todo.txt index 2e0b1e4..9236fa2 100644 --- a/todo.txt +++ b/todo.txt @@ -1,14 +1,12 @@ - make sure lv2-dev shows up in packaging. Make sure dhcpcd shows up in packaging -- Toob nam input VU is not working. -- x TooB NAM: dbVu telltale is zero0. -- x TooB NAM: bass [] mid trebble. -- X check FLAC decode. + + - make app use the same theme settings as the website. - BUG: gcs when we have an animated output control - Shutdown/reboot midi bindings - versioning. - do we want to take a second crack at establishin multi-p2p connections with the driver_param? - +- property subscriptions don't get refreshed after a disconnect. sudo apt install libsdbus-c++-dev libnm-dev