File Property Dialog Rewrite (phase 1)
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"socket_server_port": 80,
|
"socket_server_port": 8080,
|
||||||
"socket_server_address": "*",
|
"socket_server_address": "*",
|
||||||
"debug": true,
|
"debug": true,
|
||||||
"max_upload_size": 1048576,
|
"max_upload_size": 1048576,
|
||||||
|
|||||||
+1
-1
@@ -59,7 +59,7 @@ const theme = createTheme(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
mainBackground: "#222",
|
mainBackground: "#222",
|
||||||
toolbarColor: '#FFFFFF'
|
toolbarColor: '#222'
|
||||||
}
|
}
|
||||||
:
|
:
|
||||||
{
|
{
|
||||||
|
|||||||
+385
-130
@@ -19,18 +19,25 @@
|
|||||||
|
|
||||||
import React from 'react';
|
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 Button from '@mui/material/Button';
|
||||||
import FileUploadIcon from '@mui/icons-material/FileUpload';
|
import FileUploadIcon from '@mui/icons-material/FileUpload';
|
||||||
import AudioFileIcon from '@mui/icons-material/AudioFile';
|
import AudioFileIcon from '@mui/icons-material/AudioFile';
|
||||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
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 DialogActions from '@mui/material/DialogActions';
|
||||||
import DialogTitle from '@mui/material/DialogTitle';
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import OldDeleteIcon from './OldDeleteIcon';
|
import OldDeleteIcon from './OldDeleteIcon';
|
||||||
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
|
|
||||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||||
import { UiFileProperty } from './Lv2Plugin';
|
import { UiFileProperty } from './Lv2Plugin';
|
||||||
@@ -39,7 +46,8 @@ import Typography from '@mui/material/Typography';
|
|||||||
import DialogEx from './DialogEx';
|
import DialogEx from './DialogEx';
|
||||||
import UploadFileDialog from './UploadFileDialog';
|
import UploadFileDialog from './UploadFileDialog';
|
||||||
import OkCancelDialog from './OkCancelDialog';
|
import OkCancelDialog from './OkCancelDialog';
|
||||||
|
import Breadcrumbs from '@mui/material/Breadcrumbs';
|
||||||
|
import HomeIcon from '@mui/icons-material/Home';
|
||||||
export interface FilePropertyDialogProps {
|
export interface FilePropertyDialogProps {
|
||||||
open: boolean,
|
open: boolean,
|
||||||
fileProperty: UiFileProperty,
|
fileProperty: UiFileProperty,
|
||||||
@@ -51,16 +59,87 @@ export interface FilePropertyDialogProps {
|
|||||||
export interface FilePropertyDialogState {
|
export interface FilePropertyDialogState {
|
||||||
fullScreen: boolean;
|
fullScreen: boolean;
|
||||||
selectedFile: string;
|
selectedFile: string;
|
||||||
|
navDirectory: string;
|
||||||
hasSelection: boolean;
|
hasSelection: boolean;
|
||||||
files: string[];
|
canDelete: boolean;
|
||||||
|
fileEntries: FileEntry[];
|
||||||
columns: number;
|
columns: number;
|
||||||
columnWidth: number;
|
columnWidth: number;
|
||||||
openUploadFileDialog: boolean;
|
openUploadFileDialog: boolean;
|
||||||
openConfirmDeleteDialog: 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<FilePropertyDialogProps, FilePropertyDialogState> {
|
export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePropertyDialogProps, FilePropertyDialogState> {
|
||||||
|
|
||||||
|
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) {
|
constructor(props: FilePropertyDialogProps) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -71,29 +150,31 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
|||||||
this.state = {
|
this.state = {
|
||||||
fullScreen: false,
|
fullScreen: false,
|
||||||
selectedFile: props.selectedFile,
|
selectedFile: props.selectedFile,
|
||||||
|
navDirectory: this.getNavDirectoryFromFile(props.selectedFile, props.fileProperty),
|
||||||
hasSelection: false,
|
hasSelection: false,
|
||||||
|
canDelete: false,
|
||||||
columns: 0,
|
columns: 0,
|
||||||
columnWidth: 1,
|
columnWidth: 1,
|
||||||
files: [],
|
fileEntries: [],
|
||||||
openUploadFileDialog: false,
|
openUploadFileDialog: false,
|
||||||
openConfirmDeleteDialog: false
|
openConfirmDeleteDialog: false,
|
||||||
|
menuAnchorEl: null,
|
||||||
|
newFolderDialogOpen: false,
|
||||||
|
renameDialogOpen: false
|
||||||
};
|
};
|
||||||
this.requestScroll = true;
|
this.requestScroll = true;
|
||||||
this.requestFiles();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private scrollRef: HTMLDivElement | null = null;
|
private scrollRef: HTMLDivElement | null = null;
|
||||||
|
|
||||||
onScrollRef(element: HTMLDivElement | null) {
|
onScrollRef(element: HTMLDivElement | null) {
|
||||||
this.scrollRef = element;
|
this.scrollRef = element;
|
||||||
this.maybeScrollIntoView();
|
this.maybeScrollIntoView();
|
||||||
}
|
}
|
||||||
private maybeScrollIntoView()
|
private maybeScrollIntoView() {
|
||||||
{
|
if (this.scrollRef !== null && this.state.fileEntries.length !== 0 && this.mounted && this.requestScroll) {
|
||||||
if (this.scrollRef !== null && this.state.files.length !== 0 && this.mounted && this.requestScroll)
|
|
||||||
{
|
|
||||||
this.requestScroll = false;
|
this.requestScroll = false;
|
||||||
let options: ScrollIntoViewOptions = { block: "nearest"};
|
let options: ScrollIntoViewOptions = { block: "nearest" };
|
||||||
options.block = "nearest";
|
options.block = "nearest";
|
||||||
|
|
||||||
this.scrollRef.scrollIntoView(options);
|
this.scrollRef.scrollIntoView(options);
|
||||||
@@ -102,7 +183,7 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
|||||||
private mounted: boolean = false;
|
private mounted: boolean = false;
|
||||||
private model: PiPedalModel;
|
private model: PiPedalModel;
|
||||||
|
|
||||||
private requestFiles() {
|
private requestFiles(navPath: string) {
|
||||||
if (!this.props.open) {
|
if (!this.props.open) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -110,44 +191,44 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.model.requestFileList(this.props.fileProperty)
|
this.model.requestFileList2(navPath, this.props.fileProperty)
|
||||||
.then((files) => {
|
.then((files) => {
|
||||||
if (this.mounted) {
|
if (this.mounted) {
|
||||||
files.splice(0,0,"");
|
// let insertionPoint = files.length;
|
||||||
this.setState({ files: files, hasSelection: this.isFileInList(files, this.state.selectedFile) });
|
// 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) => {
|
}).catch((error) => {
|
||||||
this.model.showAlert(error.toString())
|
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;
|
this.lastDivRef = div;
|
||||||
if (div) {
|
if (div) {
|
||||||
let width = div.offsetWidth;
|
let width = div.offsetWidth;
|
||||||
if (width === 0) return;
|
if (width === 0) return;
|
||||||
let columns = Math.floor((width-40)/280);
|
let columns = 1;
|
||||||
if(columns === 0)
|
let columnWidth = (width - 40) / columns;
|
||||||
{
|
if (columns !== this.state.columns || columnWidth !== this.state.columnWidth) {
|
||||||
columns = 1;
|
this.setState({ columns: columns, columnWidth: columnWidth });
|
||||||
}
|
|
||||||
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 {
|
onWindowSizeChanged(width: number, height: number): void {
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
fullScreen: height < 200
|
fullScreen: height < 200
|
||||||
})
|
})
|
||||||
if (this.lastDivRef !== null)
|
if (this.lastDivRef !== null) {
|
||||||
{
|
|
||||||
this.onMeasureRef(this.lastDivRef);
|
this.onMeasureRef(this.lastDivRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,45 +247,31 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
|||||||
}
|
}
|
||||||
componentDidUpdate(prevProps: Readonly<FilePropertyDialogProps>, prevState: Readonly<FilePropertyDialogState>, snapshot?: any): void {
|
componentDidUpdate(prevProps: Readonly<FilePropertyDialogProps>, prevState: Readonly<FilePropertyDialogState>, snapshot?: any): void {
|
||||||
super.componentDidUpdate?.(prevProps, prevState, snapshot);
|
super.componentDidUpdate?.(prevProps, prevState, snapshot);
|
||||||
if (prevProps.open !== this.props.open)
|
if (prevProps.open !== this.props.open || prevProps.fileProperty !== this.props.fileProperty || prevProps.selectedFile !== this.props.selectedFile) {
|
||||||
{
|
if (this.props.open) {
|
||||||
if (this.props.open)
|
let navDirectory = this.getNavDirectoryFromFile(this.props.selectedFile, this.props.fileProperty);
|
||||||
{
|
this.setState({ selectedFile: this.props.selectedFile });
|
||||||
this.requestFiles()
|
this.requestFiles(navDirectory)
|
||||||
this.requestScroll = true;
|
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 {
|
private isDirectory(path: string): boolean {
|
||||||
let slashPos = path.lastIndexOf('/');
|
for (var fileEntry of this.state.fileEntries) {
|
||||||
if (slashPos < 0) {
|
if (fileEntry.filename === path) {
|
||||||
slashPos = 0;
|
return fileEntry.isDirectory;
|
||||||
} else {
|
}
|
||||||
++slashPos;
|
|
||||||
}
|
}
|
||||||
let extPos = path.lastIndexOf('.');
|
return false;
|
||||||
if (extPos < 0 || extPos < slashPos) {
|
|
||||||
extPos = path.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
return path.substring(slashPos, extPos);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
private isFileInList(files: FileEntry[], file: string) {
|
||||||
private isFileInList(files: string[], file: string) {
|
|
||||||
|
|
||||||
let hasSelection = false;
|
let hasSelection = false;
|
||||||
if (file === "") return true;
|
if (file === "") return true;
|
||||||
for (var listFile of files) {
|
for (var listFile of files) {
|
||||||
if (listFile === file) {
|
if (listFile.filename === file) {
|
||||||
hasSelection = true;
|
hasSelection = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -214,62 +281,110 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
|||||||
|
|
||||||
onSelectValue(selectedFile: string) {
|
onSelectValue(selectedFile: string) {
|
||||||
this.requestScroll = true;
|
this.requestScroll = true;
|
||||||
this.setState({ selectedFile: selectedFile, hasSelection: this.isFileInList(this.state.files, selectedFile) })
|
this.setState({ selectedFile: selectedFile, hasSelection: this.isFileInList(this.state.fileEntries, selectedFile) })
|
||||||
}
|
}
|
||||||
onDoubleClickValue(selectedFile: string) {
|
onDoubleClickValue(selectedFile: string) {
|
||||||
this.props.onOk(this.props.fileProperty,selectedFile);
|
this.openSelectedFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDelete() {
|
handleMenuOpen(event: React.MouseEvent<HTMLElement>) {
|
||||||
this.setState({openConfirmDeleteDialog: true});
|
this.setState({ menuAnchorEl: event.currentTarget });
|
||||||
|
|
||||||
}
|
}
|
||||||
handleConfirmDelete()
|
handleMenuClose() {
|
||||||
{
|
this.setState({ menuAnchorEl: null });
|
||||||
this.setState({openConfirmDeleteDialog: false});
|
}
|
||||||
if (this.state.hasSelection)
|
handleDelete() {
|
||||||
{
|
this.setState({ openConfirmDeleteDialog: true });
|
||||||
|
}
|
||||||
|
handleConfirmDelete() {
|
||||||
|
this.setState({ openConfirmDeleteDialog: false });
|
||||||
|
if (this.state.hasSelection) {
|
||||||
let selectedFile = this.state.selectedFile;
|
let selectedFile = this.state.selectedFile;
|
||||||
let position = -1;
|
let position = -1;
|
||||||
for (let i = 0; i < this.state.files.length;++i)
|
for (let i = 0; i < this.state.fileEntries.length; ++i) {
|
||||||
{
|
let file = this.state.fileEntries[i];
|
||||||
let file = this.state.files[i];
|
if (file.filename === selectedFile) {
|
||||||
if (file === selectedFile)
|
|
||||||
{
|
|
||||||
position = i;
|
position = i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let newSelection = "";
|
let newSelection = "";
|
||||||
if (position >= 0 && position < this.state.files.length-1)
|
if (position >= 0 && position < this.state.fileEntries.length - 1) {
|
||||||
{
|
newSelection = this.state.fileEntries[position + 1].filename;
|
||||||
newSelection = this.state.files[position+1];
|
} else if (position === this.state.fileEntries.length - 1) {
|
||||||
} else if (position === this.state.files.length-1)
|
|
||||||
{
|
|
||||||
if (position !== 0) {
|
if (position !== 0) {
|
||||||
newSelection = this.state.files[position-1];
|
newSelection = this.state.fileEntries[position - 1].filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.model.deleteUserFile(this.state.selectedFile)
|
this.model.deleteUserFile(this.state.selectedFile)
|
||||||
.then(
|
.then(
|
||||||
()=> {
|
() => {
|
||||||
this.setState({selectedFile: newSelection,hasSelection: newSelection !== ""});
|
this.setState({ selectedFile: newSelection, hasSelection: newSelection !== "" });
|
||||||
this.requestFiles();
|
this.requestFiles(this.state.navDirectory);
|
||||||
}
|
}
|
||||||
).catch(
|
).catch(
|
||||||
(e: any)=>
|
(e: any) => {
|
||||||
{
|
this.model.showAlert(e + "");
|
||||||
this.model.showAlert(e + "");
|
}
|
||||||
}
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private wantsScrollRef: boolean = true;
|
private wantsScrollRef: boolean = true;
|
||||||
|
|
||||||
mapKey: number = 0;
|
navigate(relativeDirectory: string) {
|
||||||
|
this.requestFiles(relativeDirectory);
|
||||||
|
this.setState({ navDirectory: relativeDirectory });
|
||||||
|
|
||||||
|
}
|
||||||
|
renderBreadcrumbs() {
|
||||||
|
if (this.state.navDirectory === "") {
|
||||||
|
return (<div style={{ height: 0 }} />);
|
||||||
|
}
|
||||||
|
let breadcrumbs: React.ReactElement[] = [(
|
||||||
|
<Link underline="hover"
|
||||||
|
color="inherit"
|
||||||
|
key="1" onClick={() => { this.navigate(""); }}
|
||||||
|
sx={{ display: 'flex', alignItems: 'center' }}
|
||||||
|
>
|
||||||
|
<HomeIcon sx={{ mr: 0.6 }} fontSize="inherit" />
|
||||||
|
Home
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
];
|
||||||
|
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((
|
||||||
|
<Link underline="hover" key={(i + 1).toString()}
|
||||||
|
color="inherit" onClick={() => { this.navigate(myTarget) }}>
|
||||||
|
{directories[i]}
|
||||||
|
</Link>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let lastdirectory = directories[directories.length - 1];
|
||||||
|
breadcrumbs.push((
|
||||||
|
<Typography key={directories.length.toString()} color="text.secondary"> {lastdirectory}</Typography>
|
||||||
|
));
|
||||||
|
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Breadcrumbs separator='/' aria-label="breadcrumb" style={{ marginLeft: 24 }}>
|
||||||
|
{breadcrumbs}
|
||||||
|
</Breadcrumbs>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasSelectedFileOrFolder(): boolean {
|
||||||
|
return this.state.hasSelection && this.state.selectedFile !== "";
|
||||||
|
}
|
||||||
render() {
|
render() {
|
||||||
this.mapKey = 0;
|
|
||||||
let columnWidth = this.state.columnWidth;
|
let columnWidth = this.state.columnWidth;
|
||||||
|
|
||||||
return this.props.open &&
|
return this.props.open &&
|
||||||
@@ -278,48 +393,97 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
|||||||
PaperProps={{ style: { minHeight: "90%", maxHeight: "90%", overflowY: "visible" } }}
|
PaperProps={{ style: { minHeight: "90%", maxHeight: "90%", overflowY: "visible" } }}
|
||||||
>
|
>
|
||||||
<DialogTitle >
|
<DialogTitle >
|
||||||
<div>
|
<Toolbar style={{ padding: 0 }}>
|
||||||
<IconButton edge="start" color="inherit" onClick={() => { this.props.onCancel(); }} aria-label="back"
|
<IconButton
|
||||||
|
edge="start"
|
||||||
|
color="inherit"
|
||||||
|
aria-label="back"
|
||||||
|
style={{ opacity: 0.6 }}
|
||||||
|
onClick={() => { this.props.onCancel(); }}
|
||||||
>
|
>
|
||||||
<ArrowBackIcon fontSize="small" style={{ opacity: "0.6" }} />
|
<ArrowBackIcon style={{ width: 24, height: 24 }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Typography display="inline" >{this.props.fileProperty.label}</Typography>
|
<Typography component="div" sx={{ flexGrow: 1 }}>
|
||||||
|
{this.props.fileProperty.label}
|
||||||
|
</Typography>
|
||||||
|
<IconButton
|
||||||
|
aria-label="display more actions"
|
||||||
|
edge="end"
|
||||||
|
color="inherit"
|
||||||
|
style={{ opacity: 0.6 }}
|
||||||
|
onClick={(ev) => { this.handleMenuOpen(ev); }}
|
||||||
|
>
|
||||||
|
<MoreIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Menu
|
||||||
|
id="menu-appbar"
|
||||||
|
anchorEl={this.state.menuAnchorEl}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'right',
|
||||||
|
}}
|
||||||
|
keepMounted
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'right',
|
||||||
|
}}
|
||||||
|
open={this.state.menuAnchorEl !== null}
|
||||||
|
onClose={() => { this.handleMenuClose(); }}
|
||||||
|
>
|
||||||
|
<MenuItem onClick={() => { this.handleMenuClose(); this.onNewFolder(); }}>New folder</MenuItem>
|
||||||
|
{this.hasSelectedFileOrFolder() && (<Divider />)}
|
||||||
|
{this.hasSelectedFileOrFolder() && (<MenuItem onClick={() => { this.handleMenuClose(); }}>Move</MenuItem>)}
|
||||||
|
{this.hasSelectedFileOrFolder() && (<MenuItem onClick={() => { this.handleMenuClose();this.onRename(); }}>Rename</MenuItem>)}
|
||||||
|
</Menu>
|
||||||
|
</Toolbar>
|
||||||
|
<div style={{ flex: "0 0 auto", marginLeft: "auto", marginRight: "auto" }}>
|
||||||
|
{this.renderBreadcrumbs()}
|
||||||
</div>
|
</div>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<div style={{ flex: "0 0 auto", height: "1px", background: "rgba(0,0,0,0.2" }}> </div>
|
<div style={{ flex: "0 0 auto", height: "1px", background: isDarkMode() ? "rgba(255,255,255,0.1" : "rgba(0,0,0,0.2" }}> </div>
|
||||||
<div style={{ flex: "1 1 auto", height: "300", display: "flex", flexFlow: "row wrap", overflowX: "hidden", overflowY: "auto" }}>
|
<div style={{ flex: "1 1 auto", height: "300", display: "flex", flexFlow: "row wrap", overflowX: "hidden", overflowY: "auto" }}>
|
||||||
<div
|
<div
|
||||||
ref={(element)=> this.onMeasureRef(element)}
|
ref={(element) => this.onMeasureRef(element)}
|
||||||
style={{ flex: "1 1 100%", display: "flex", flexFlow: "row wrap",
|
style={{
|
||||||
justifyContent: "flex-start", alignContent: "flex-start", paddingLeft: 16, paddingTop: 16,paddingBottom: 16 }}>
|
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.columns !== 0) && // don't render until we have number of columns derived from layout.
|
||||||
this.state.files.map(
|
this.state.fileEntries.map(
|
||||||
(value: string, index: number) => {
|
(value: FileEntry, index: number) => {
|
||||||
let displayValue = value;
|
let displayValue = value.filename;
|
||||||
if (displayValue === "")
|
if (displayValue === "") {
|
||||||
{
|
|
||||||
displayValue = "<none>";
|
displayValue = "<none>";
|
||||||
} else {
|
} 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)";
|
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;
|
let scrollRef: ((element: HTMLDivElement | null) => void) | undefined = undefined;
|
||||||
if (selected)
|
if (selected) {
|
||||||
{
|
scrollRef = (element) => { this.onScrollRef(element) };
|
||||||
scrollRef = (element)=> { this.onScrollRef(element)};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonBase key={this.mapKey++}
|
<ButtonBase key={value.filename}
|
||||||
|
|
||||||
style={{ width: columnWidth, flex: "0 0 auto", height: 48, position: "relative" }}
|
style={{ width: columnWidth, flex: "0 0 auto", height: 48, position: "relative" }}
|
||||||
onClick={() => this.onSelectValue(value)} onDoubleClick={()=> {this.onDoubleClickValue(value);}}
|
onClick={() => this.onSelectValue(value.filename)} onDoubleClick={() => { this.onDoubleClickValue(value.filename); }}
|
||||||
>
|
>
|
||||||
<div ref={scrollRef} style={{ position: "absolute", background: selectBg, width: "100%", height: "100%", borderRadius: 4 }} />
|
<div ref={scrollRef} style={{ position: "absolute", background: selectBg, width: "100%", height: "100%", borderRadius: 4 }} />
|
||||||
<div style={{ display: "flex", flexFlow: "row nowrap", textOverflow: "ellipsis", justifyContent: "start", alignItems: "center", width: "100%", height: "100%" }}>
|
<div style={{ display: "flex", flexFlow: "row nowrap", textOverflow: "ellipsis", justifyContent: "start", alignItems: "center", width: "100%", height: "100%" }}>
|
||||||
<AudioFileIcon style={{ flex: "0 0 auto", opacity: 0.7, marginRight: 8, marginLeft: 8 }} />
|
{value.filename === "" ?
|
||||||
|
(<InsertDriveFileOutlinedIcon style={{ flex: "0 0 auto", opacity: 0.7, marginRight: 8, marginLeft: 8 }} />)
|
||||||
|
: (
|
||||||
|
value.isDirectory ? (
|
||||||
|
<FolderIcon style={{ flex: "0 0 auto", opacity: 0.7, marginRight: 8, marginLeft: 8 }} />
|
||||||
|
) : (
|
||||||
|
<AudioFileIcon style={{ flex: "0 0 auto", opacity: 0.7, marginRight: 8, marginLeft: 8 }} />
|
||||||
|
))}
|
||||||
<Typography noWrap style={{ flex: "1 1 auto", textAlign: "left" }}>{displayValue}</Typography>
|
<Typography noWrap style={{ flex: "1 1 auto", textAlign: "left" }}>{displayValue}</Typography>
|
||||||
</div>
|
</div>
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
@@ -333,12 +497,13 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
|||||||
<DialogActions style={{ justifyContent: "stretch" }}>
|
<DialogActions style={{ justifyContent: "stretch" }}>
|
||||||
<div style={{ display: "flex", width: "100%", alignItems: "center", flexFlow: "row nowrap" }}>
|
<div style={{ display: "flex", width: "100%", alignItems: "center", flexFlow: "row nowrap" }}>
|
||||||
<IconButton style={{ visibility: (this.state.hasSelection ? "visible" : "hidden") }} aria-label="delete" component="label" color="primary"
|
<IconButton style={{ visibility: (this.state.hasSelection ? "visible" : "hidden") }} aria-label="delete" component="label" color="primary"
|
||||||
onClick={()=> this.handleDelete()} >
|
disabled={!this.state.hasSelection || this.state.selectedFile === ""}
|
||||||
|
onClick={() => this.handleDelete()} >
|
||||||
<OldDeleteIcon fontSize='small' />
|
<OldDeleteIcon fontSize='small' />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<Button style={{ flex: "0 0 auto" }} aria-label="upload" variant="text" startIcon={<FileUploadIcon />}
|
<Button style={{ flex: "0 0 auto" }} aria-label="upload" variant="text" startIcon={<FileUploadIcon />}
|
||||||
onClick={()=> { this.setState({openUploadFileDialog: true})}}
|
onClick={() => { this.setState({ openUploadFileDialog: true }) }}
|
||||||
>
|
>
|
||||||
<div>Upload</div>
|
<div>Upload</div>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -347,11 +512,11 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
|||||||
<Button onClick={() => { this.props.onCancel(); }} aria-label="cancel">
|
<Button onClick={() => { this.props.onCancel(); }} aria-label="cancel">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button style={{ flex: "0 0 auto" }}
|
<Button style={{ flex: "0 0 auto" }}
|
||||||
onClick={() => { this.props.onOk(this.props.fileProperty, this.state.selectedFile); }}
|
onClick={() => { this.openSelectedFile(); }}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
disabled={(!this.state.hasSelection) && this.state.selectedFile !== ""} aria-label="select"
|
disabled={(!this.state.hasSelection) && this.state.selectedFile !== ""} aria-label="select"
|
||||||
>
|
>
|
||||||
Select
|
Select
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -364,22 +529,112 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
|||||||
this.setState({ openUploadFileDialog: false });
|
this.setState({ openUploadFileDialog: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
uploadPage={ "uploadUserFile?directory="+ encodeURIComponent(this.props.fileProperty.directory)}
|
uploadPage={"uploadUserFile?directory=" + encodeURIComponent(this.props.fileProperty.directory)}
|
||||||
onUploaded={()=> { this.requestFiles();}}
|
onUploaded={() => { this.requestFiles(this.state.navDirectory); }}
|
||||||
fileProperty={this.props.fileProperty}
|
fileProperty={this.props.fileProperty}
|
||||||
|
|
||||||
|
|
||||||
/>
|
/>
|
||||||
<OkCancelDialog open={this.state.openConfirmDeleteDialog}
|
<OkCancelDialog open={this.state.openConfirmDeleteDialog}
|
||||||
text="Are you sure you want to delete the selected file?"
|
text={
|
||||||
|
(this.isDirectory(this.state.selectedFile))
|
||||||
|
? "Are you sure you want to delete the selected directory and all its contents?"
|
||||||
|
: "Are you sure you want to delete the selected file?"}
|
||||||
okButtonText="Delete"
|
okButtonText="Delete"
|
||||||
onOk={()=>this.handleConfirmDelete()}
|
onOk={() => this.handleConfirmDelete()}
|
||||||
onClose={()=>{
|
onClose={() => {
|
||||||
this.setState({openConfirmDeleteDialog: false});
|
this.setState({ openConfirmDeleteDialog: false });
|
||||||
}}
|
}}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
|
{
|
||||||
|
this.state.newFolderDialogOpen && (
|
||||||
|
<RenameDialog open={this.state.newFolderDialogOpen} defaultName=""
|
||||||
|
onOk={(newName) => { this.setState({ newFolderDialogOpen: false }); this.onExecuteNewFolder(newName) }}
|
||||||
|
onClose={() => { this.setState({ newFolderDialogOpen: false }); }}
|
||||||
|
acceptActionName="OK"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
this.state.renameDialogOpen && (
|
||||||
|
<RenameDialog open={this.state.renameDialogOpen} defaultName={this.renameDefaultName()}
|
||||||
|
onOk={(newName) => { this.setState({ renameDialogOpen: false }); this.onExecuteRename(newName) }}
|
||||||
|
onClose={() => { this.setState({ renameDialogOpen: false }); }}
|
||||||
|
acceptActionName="OK"
|
||||||
|
/>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
</DialogEx>
|
</DialogEx>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -52,6 +52,10 @@ export enum State {
|
|||||||
|
|
||||||
export type ControlValueChangedHandler = (key: string, value: number) => void;
|
export type ControlValueChangedHandler = (key: string, value: number) => void;
|
||||||
|
|
||||||
|
export interface FileEntry {
|
||||||
|
filename: string;
|
||||||
|
isDirectory: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type PluginPresetsChangedHandler = (pluginUri: string) => void;
|
export type PluginPresetsChangedHandler = (pluginUri: string) => void;
|
||||||
|
|
||||||
@@ -337,7 +341,7 @@ interface Vst3ControlChangedBody {
|
|||||||
|
|
||||||
export interface FavoritesList {
|
export interface FavoritesList {
|
||||||
[url: string]: boolean;
|
[url: string]: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
export class PiPedalModel //implements PiPedalModel
|
export class PiPedalModel //implements PiPedalModel
|
||||||
@@ -1630,14 +1634,21 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deprecated.
|
||||||
requestFileList(piPedalFileProperty: UiFileProperty): Promise<string[]> {
|
requestFileList(piPedalFileProperty: UiFileProperty): Promise<string[]> {
|
||||||
return nullCast(this.webSocket)
|
return nullCast(this.webSocket)
|
||||||
.request<string[]>('requestFileList', piPedalFileProperty);
|
.request<string[]>('requestFileList', piPedalFileProperty);
|
||||||
}
|
}
|
||||||
|
requestFileList2(relativeDirectoryPath: string,piPedalFileProperty: UiFileProperty): Promise<FileEntry[]> {
|
||||||
|
return nullCast(this.webSocket)
|
||||||
|
.request<FileEntry[]>('requestFileList2',
|
||||||
|
{relativePath: relativeDirectoryPath, fileProperty: piPedalFileProperty}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
deleteUserFile(fileName: string) : Promise<boolean>
|
deleteUserFile(fileName: string) : Promise<boolean>
|
||||||
{
|
{
|
||||||
return nullCast(this.webSocket).request<boolean>('deleteUserFile',fileName)
|
return nullCast(this.webSocket).request<boolean>('deleteUserFile',fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2247,7 +2258,11 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
|
||||||
let ws = this.webSocket;
|
let ws = this.webSocket;
|
||||||
if (!ws) return;
|
if (!ws)
|
||||||
|
{
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
ws.request<void>(
|
ws.request<void>(
|
||||||
"setGovernorSettings",
|
"setGovernorSettings",
|
||||||
governor
|
governor
|
||||||
@@ -2260,6 +2275,57 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
createNewSampleDirectory(relativePath: string, uiFileProperty: UiFileProperty) : Promise<string>
|
||||||
|
{
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
|
||||||
|
let ws = this.webSocket;
|
||||||
|
if (!ws) {
|
||||||
|
resolve("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ws.request<string>(
|
||||||
|
"createNewSampleDirectory",
|
||||||
|
{
|
||||||
|
relativePath: relativePath,
|
||||||
|
uiFileProperty: uiFileProperty
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((newPath) => {
|
||||||
|
resolve(newPath);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renameSampleFile(oldRelativePath: string, newRelativePath: string, uiFileProperty: UiFileProperty) : Promise<string>
|
||||||
|
{
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
|
||||||
|
let ws = this.webSocket;
|
||||||
|
if (!ws) {
|
||||||
|
resolve("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ws.request<string>(
|
||||||
|
"renameSampleFile",
|
||||||
|
{
|
||||||
|
oldRelativePath: oldRelativePath,
|
||||||
|
newRelativePath: newRelativePath,
|
||||||
|
uiFileProperty: uiFileProperty
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((newPath) => {
|
||||||
|
resolve(newPath);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setWifiConfigSettings(wifiConfigSettings: WifiConfigSettings): Promise<void> {
|
setWifiConfigSettings(wifiConfigSettings: WifiConfigSettings): Promise<void> {
|
||||||
let result = new Promise<void>((resolve, reject) => {
|
let result = new Promise<void>((resolve, reject) => {
|
||||||
let oldSettings = this.wifiConfigSettings.get();
|
let oldSettings = this.wifiConfigSettings.get();
|
||||||
@@ -2567,7 +2633,6 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let instance: PiPedalModel | undefined = undefined;
|
let instance: PiPedalModel | undefined = undefined;
|
||||||
|
|||||||
@@ -663,7 +663,7 @@ const PluginControlView =
|
|||||||
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.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 });
|
this.setState({ showFileDialog: false });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import DialogActions from '@mui/material/DialogActions';
|
|||||||
import DialogContent from '@mui/material/DialogContent';
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
import { nullCast } from './Utility';
|
import { nullCast } from './Utility';
|
||||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||||
|
import {PiPedalModel,PiPedalModelFactory} from './PiPedalModel';
|
||||||
|
|
||||||
|
|
||||||
export interface RenameDialogProps {
|
export interface RenameDialogProps {
|
||||||
@@ -74,6 +75,12 @@ export default class RenameDialog extends ResizeResponsiveComponent<RenameDialog
|
|||||||
componentDidUpdate()
|
componentDidUpdate()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
checkForIllegalCharacters(filename: string) {
|
||||||
|
if (filename.indexOf('/') !== -1)
|
||||||
|
{
|
||||||
|
throw new Error("Illegal character: '/'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let props = this.props;
|
let props = this.props;
|
||||||
@@ -86,6 +93,14 @@ export default class RenameDialog extends ResizeResponsiveComponent<RenameDialog
|
|||||||
const handleOk = () => {
|
const handleOk = () => {
|
||||||
let text = nullCast(this.refText.current).value;
|
let text = nullCast(this.refText.current).value;
|
||||||
text = text.trim();
|
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;
|
if (text.length === 0) return;
|
||||||
onOk(text);
|
onOk(text);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,6 +131,8 @@ else()
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
set (PIPEDAL_SOURCES
|
set (PIPEDAL_SOURCES
|
||||||
|
FilePropertyDirectoryTree.cpp FilePropertyDirectoryTree.hpp
|
||||||
|
FileEntry.cpp FileEntry.hpp
|
||||||
atom_object.hpp atom_object.cpp
|
atom_object.hpp atom_object.cpp
|
||||||
FileBrowserFiles.h
|
FileBrowserFiles.h
|
||||||
FileBrowserFilesFeature.hpp FileBrowserFilesFeature.cpp
|
FileBrowserFilesFeature.hpp FileBrowserFilesFeature.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()
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
@@ -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 <string>
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
namespace pipedal {
|
||||||
|
class FilePropertyDirectoryTree {
|
||||||
|
public:
|
||||||
|
using ptr = std::unique_ptr<FilePropertyDirectoryTree>;
|
||||||
|
|
||||||
|
FilePropertyDirectoryTree();
|
||||||
|
FilePropertyDirectoryTree(const std::string&directoryName);
|
||||||
|
std::string directoryName_;
|
||||||
|
std::vector<std::unique_ptr<FilePropertyDirectoryTree>> children_;
|
||||||
|
|
||||||
|
DECLARE_JSON_MAP(FilePropertyDirectoryTree);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -2039,6 +2039,27 @@ std::vector<std::string> PiPedalModel::GetFileList(const UiFileProperty &filePro
|
|||||||
return std::vector<std::string>(); // don't disclose to users what the problem is.
|
return std::vector<std::string>(); // don't disclose to users what the problem is.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
std::vector<FileEntry> 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<FileEntry>(); // 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<std::recursive_mutex> lock(mutex);
|
||||||
|
return storage.RenameSampleFile(oldRelativePath,newRelativePath,uiFileProperty);
|
||||||
|
}
|
||||||
|
|
||||||
void PiPedalModel::DeleteSampleFile(const std::filesystem::path &fileName)
|
void PiPedalModel::DeleteSampleFile(const std::filesystem::path &fileName)
|
||||||
{
|
{
|
||||||
@@ -2046,6 +2067,20 @@ void PiPedalModel::DeleteSampleFile(const std::filesystem::path &fileName)
|
|||||||
storage.DeleteSampleFile(fileName);
|
storage.DeleteSampleFile(fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string PiPedalModel::CreateNewSampleDirectory(const std::string&relativePath, const UiFileProperty&uiFileProperty)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(mutex);
|
||||||
|
return storage.CreateNewSampleDirectory(relativePath, uiFileProperty);
|
||||||
|
|
||||||
|
}
|
||||||
|
FilePropertyDirectoryTree::ptr PiPedalModel::GetSampleDirectoryTree(const UiFileProperty&uiFileProperty)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> 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)
|
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);
|
return storage.UploadUserFile(directory, patchProperty, filename, fileBody);
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
#include <thread>
|
#include <thread>
|
||||||
#include "Promise.hpp"
|
#include "Promise.hpp"
|
||||||
#include "AtomConverter.hpp"
|
#include "AtomConverter.hpp"
|
||||||
|
#include "FileEntry.hpp"
|
||||||
|
|
||||||
namespace pipedal
|
namespace pipedal
|
||||||
{
|
{
|
||||||
@@ -328,8 +329,13 @@ namespace pipedal
|
|||||||
void SetFavorites(const std::map<std::string, bool> &favorites);
|
void SetFavorites(const std::map<std::string, bool> &favorites);
|
||||||
|
|
||||||
std::vector<std::string> GetFileList(const UiFileProperty&fileProperty);
|
std::vector<std::string> GetFileList(const UiFileProperty&fileProperty);
|
||||||
|
std::vector<FileEntry> GetFileList2(const std::string &relativePath,const UiFileProperty&fileProperty);
|
||||||
|
|
||||||
void DeleteSampleFile(const std::filesystem::path &fileName);
|
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);
|
std::string UploadUserFile(const std::string &directory, const std::string &patchProperty,const std::string&filename,const std::string&fileBody);
|
||||||
uint64_t CreateNewPreset();
|
uint64_t CreateNewPreset();
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
#include "SysExec.hpp"
|
#include "SysExec.hpp"
|
||||||
#include "PiPedalAlsa.hpp"
|
#include "PiPedalAlsa.hpp"
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include "FileEntry.hpp"
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
using namespace pipedal;
|
using namespace pipedal;
|
||||||
@@ -58,6 +59,33 @@ JSON_MAP_REFERENCE(GetPatchPropertyBody, propertyUri)
|
|||||||
JSON_MAP_END()
|
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 {
|
class Lv2StateChangedBody {
|
||||||
public:
|
public:
|
||||||
uint64_t instanceId_;
|
uint64_t instanceId_;
|
||||||
@@ -198,6 +226,21 @@ JSON_MAP_REFERENCE(MonitorResultBody, subscriptionHandle)
|
|||||||
JSON_MAP_REFERENCE(MonitorResultBody, value)
|
JSON_MAP_REFERENCE(MonitorResultBody, value)
|
||||||
JSON_MAP_END()
|
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
|
class MonitorPortBody
|
||||||
{
|
{
|
||||||
@@ -1417,6 +1460,13 @@ public:
|
|||||||
std::vector<std::string> list = this->model.GetFileList(fileProperty);
|
std::vector<std::string> list = this->model.GetFileList(fileProperty);
|
||||||
this->Reply(replyTo,"requestFileList",list);
|
this->Reply(replyTo,"requestFileList",list);
|
||||||
}
|
}
|
||||||
|
else if (message == "requestFileList2")
|
||||||
|
{
|
||||||
|
FileRequestArgs requestArgs;
|
||||||
|
pReader->read(&requestArgs);
|
||||||
|
std::vector<FileEntry> list = this->model.GetFileList2(requestArgs.relativePath_, requestArgs.fileProperty_);
|
||||||
|
this->Reply(replyTo,"requestFileList2",list);
|
||||||
|
}
|
||||||
else if (message == "newPreset")
|
else if (message == "newPreset")
|
||||||
{
|
{
|
||||||
int64_t presetId = this->model.CreateNewPreset();
|
int64_t presetId = this->model.CreateNewPreset();
|
||||||
@@ -1430,6 +1480,29 @@ public:
|
|||||||
this->model.DeleteSampleFile(fileName);
|
this->model.DeleteSampleFile(fileName);
|
||||||
this->Reply(replyTo,"deleteUserFile",true);
|
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")
|
else if (message == "setOnboarding")
|
||||||
{
|
{
|
||||||
bool value;
|
bool value;
|
||||||
|
|||||||
+202
-22
@@ -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
|
// 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
|
// 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";
|
#define USER_SETTINGS_FILENAME "userSettings.json";
|
||||||
|
|
||||||
Storage::Storage()
|
Storage::Storage()
|
||||||
|
: locale("en_US.UTF-8")
|
||||||
{
|
{
|
||||||
SetConfigRoot("~/var/Config");
|
SetConfigRoot("~/var/Config");
|
||||||
SetDataRoot("~/var/PiPedal");
|
SetDataRoot("~/var/PiPedal");
|
||||||
@@ -1467,11 +1468,6 @@ std::vector<MidiBinding> Storage::GetSystemMidiBindings()
|
|||||||
return result;
|
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)
|
static bool containsDirectorySeparator(const std::string &value)
|
||||||
{
|
{
|
||||||
if (value.find("/") != std::string::npos)
|
if (value.find("/") != std::string::npos)
|
||||||
@@ -1483,19 +1479,12 @@ static bool containsDirectorySeparator(const std::string &value)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static void ThrowPermissionDeniedError()
|
static void ThrowPermissionDeniedError()
|
||||||
{
|
{
|
||||||
throw std::logic_error("Permission denied.");
|
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<std::string> Storage::GetFileList(const UiFileProperty &fileProperty)
|
std::vector<std::string> Storage::GetFileList(const UiFileProperty &fileProperty)
|
||||||
{
|
{
|
||||||
@@ -1537,16 +1526,109 @@ std::vector<std::string> Storage::GetFileList(const UiFileProperty &fileProperty
|
|||||||
|
|
||||||
// sort lexicographically
|
// 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;
|
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<FileEntry> Storage::GetFileList2(const std::string &relativePath,const UiFileProperty &fileProperty)
|
||||||
|
{
|
||||||
|
if (!ensureNoDotDot(relativePath))
|
||||||
|
{
|
||||||
|
ThrowPermissionDeniedError();
|
||||||
|
}
|
||||||
|
if (!UiFileProperty::IsDirectoryNameValid(fileProperty.directory()))
|
||||||
|
{
|
||||||
|
ThrowPermissionDeniedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<FileEntry> 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())
|
if (!fileName.is_absolute())
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!ensureNoDotDot(fileName))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
std::filesystem::path audioFilePath = this->GetPluginAudioFileDirectory();
|
std::filesystem::path audioFilePath = this->GetPluginAudioFileDirectory();
|
||||||
|
|
||||||
std::filesystem::path parentDirectory = fileName.parent_path();
|
std::filesystem::path parentDirectory = fileName.parent_path();
|
||||||
@@ -1557,9 +1639,6 @@ bool Storage::IsValidSampleFile(const std::filesystem::path &fileName)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
std::string name = parentDirectory.filename().string();
|
std::string name = parentDirectory.filename().string();
|
||||||
if (name == ".." || name == ".")
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (parentDirectory == audioFilePath)
|
if (parentDirectory == audioFilePath)
|
||||||
return true;
|
return true;
|
||||||
parentDirectory = parentDirectory.parent_path();
|
parentDirectory = parentDirectory.parent_path();
|
||||||
@@ -1571,7 +1650,7 @@ bool Storage::IsValidSampleFile(const std::filesystem::path &fileName)
|
|||||||
}
|
}
|
||||||
void Storage::DeleteSampleFile(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.");
|
throw std::logic_error("Permission denied.");
|
||||||
}
|
}
|
||||||
@@ -1581,7 +1660,16 @@ void Storage::DeleteSampleFile(const std::filesystem::path &fileName)
|
|||||||
}
|
}
|
||||||
try
|
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 &)
|
catch (const std::exception &)
|
||||||
{
|
{
|
||||||
@@ -1601,7 +1689,7 @@ std::filesystem::path Storage::MakeUserFilePath(const std::string &directory, co
|
|||||||
throw std::logic_error("Permission denied.");
|
throw std::logic_error("Permission denied.");
|
||||||
}
|
}
|
||||||
std::filesystem::path result = this->GetPluginAudioFileDirectory() / directory / filename;
|
std::filesystem::path result = this->GetPluginAudioFileDirectory() / directory / filename;
|
||||||
if (!this->IsValidSampleFile(result))
|
if (!this->IsValidSampleFileName(result))
|
||||||
{
|
{
|
||||||
throw std::logic_error("Permission denied.");
|
throw std::logic_error("Permission denied.");
|
||||||
}
|
}
|
||||||
@@ -1641,6 +1729,98 @@ std::string Storage::UploadUserFile(const std::string &directory, const std::str
|
|||||||
return path.string();
|
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<FilePropertyDirectoryTree>(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<FilePropertyDirectoryTree>("");
|
||||||
|
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()
|
const PluginPresetIndex &Storage::GetPluginPresetIndex()
|
||||||
{
|
{
|
||||||
return pluginPresetIndex;
|
return pluginPresetIndex;
|
||||||
|
|||||||
+14
-1
@@ -28,7 +28,10 @@
|
|||||||
#include "JackServerSettings.hpp"
|
#include "JackServerSettings.hpp"
|
||||||
#include "WifiConfigSettings.hpp"
|
#include "WifiConfigSettings.hpp"
|
||||||
#include "WifiDirectConfigSettings.hpp"
|
#include "WifiDirectConfigSettings.hpp"
|
||||||
|
#include "FileEntry.hpp"
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <locale>
|
||||||
|
#include "FilePropertyDirectoryTree.hpp"
|
||||||
|
|
||||||
|
|
||||||
namespace pipedal {
|
namespace pipedal {
|
||||||
@@ -64,6 +67,7 @@ struct PluginPresetValues {
|
|||||||
|
|
||||||
class Storage {
|
class Storage {
|
||||||
private:
|
private:
|
||||||
|
std::locale locale;
|
||||||
std::filesystem::path dataRoot;
|
std::filesystem::path dataRoot;
|
||||||
std::filesystem::path configRoot;
|
std::filesystem::path configRoot;
|
||||||
BankIndex bankIndex;
|
BankIndex bankIndex;
|
||||||
@@ -71,6 +75,7 @@ private:
|
|||||||
PluginPresetIndex pluginPresetIndex;
|
PluginPresetIndex pluginPresetIndex;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void FillSampleDirectoryTree(FilePropertyDirectoryTree*node, const std::filesystem::path&directory) const;
|
||||||
|
|
||||||
void MaybeCopyDefaultPresets();
|
void MaybeCopyDefaultPresets();
|
||||||
static std::string SafeEncodeName(const std::string& name);
|
static std::string SafeEncodeName(const std::string& name);
|
||||||
@@ -149,6 +154,7 @@ public:
|
|||||||
int64_t DeleteBank(int64_t bankId);
|
int64_t DeleteBank(int64_t bankId);
|
||||||
|
|
||||||
std::vector<std::string> GetFileList(const UiFileProperty&fileProperty);
|
std::vector<std::string> GetFileList(const UiFileProperty&fileProperty);
|
||||||
|
std::vector<FileEntry> GetFileList2(const std::string&relativePath,const UiFileProperty&fileProperty);
|
||||||
|
|
||||||
|
|
||||||
void SetJackChannelSelection(const JackChannelSelection&channelSelection);
|
void SetJackChannelSelection(const JackChannelSelection&channelSelection);
|
||||||
@@ -176,7 +182,7 @@ private:
|
|||||||
void SavePluginPresetIndex();
|
void SavePluginPresetIndex();
|
||||||
|
|
||||||
std::filesystem::path GetPluginPresetPath(const std::string &pluginUri) const;
|
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);
|
std::filesystem::path MakeUserFilePath(const std::string &directory, const std::string&filename);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -213,6 +219,13 @@ public:
|
|||||||
std::vector<MidiBinding> GetSystemMidiBindings();
|
std::vector<MidiBinding> GetSystemMidiBindings();
|
||||||
void DeleteSampleFile(const std::filesystem::path &fileName);
|
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 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;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
- make sure lv2-dev shows up in packaging. Make sure dhcpcd shows up in packaging
|
- 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.
|
- make app use the same theme settings as the website.
|
||||||
- BUG: gcs when we have an animated output control
|
- BUG: gcs when we have an animated output control
|
||||||
- Shutdown/reboot midi bindings
|
- Shutdown/reboot midi bindings
|
||||||
- versioning.
|
- versioning.
|
||||||
- do we want to take a second crack at establishin multi-p2p connections with the driver_param?
|
- 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
|
sudo apt install libsdbus-c++-dev libnm-dev
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user